Merge pull request #174 from theironchef/fix/regions-one-pass
Pathfinder: find all regions in a few contour passes, not one per mask
This commit is contained in:
commit
e35f75f733
|
|
@ -1,10 +1,13 @@
|
||||||
//! Illustrator's Pathfinder panel and Shape Builder regions, over an ordered stack of shapes
|
//! Illustrator's Pathfinder panel and Shape Builder regions, over an ordered stack of shapes
|
||||||
//! (index 0 = back-most, last = front-most).
|
//! (index 0 = back-most, last = front-most).
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use kurbo::{BezPath, ParamCurve, ParamCurveNearest, Point, Shape as _};
|
use kurbo::{BezPath, ParamCurve, ParamCurveNearest, Point, Shape as _};
|
||||||
|
use linesweeper::topology::ContourIdx;
|
||||||
use vectorcraft_geom::{FillRule, PathData};
|
use vectorcraft_geom::{FillRule, PathData};
|
||||||
|
|
||||||
use crate::boolean::{Arrangement, Seg, all_contours_to_path, contours_to_path, fill_bezpath, segs_to_subpath, unite_all};
|
use crate::boolean::{Arrangement, Multi, Seg, all_contours_to_path, contours_to_path, fill_bezpath, segs_to_subpath, unite_all};
|
||||||
|
|
||||||
/// A filled shape in a Pathfinder stack. `key` identifies its paint (e.g. a hashed fill colour);
|
/// A filled shape in a Pathfinder stack. `key` identifies its paint (e.g. a hashed fill colour);
|
||||||
/// results carry the key of the object whose paint they keep.
|
/// results carry the key of the object whose paint they keep.
|
||||||
|
|
@ -142,21 +145,108 @@ pub fn pathfinder(op: PathfinderOp, shapes: &[Shape]) -> Vec<Shape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The faces of `arr`, each distinct coverage mask's in turn (sorted), each face grouped as
|
||||||
|
/// `arr.contours(|m| m == mask).grouped()` groups it.
|
||||||
|
///
|
||||||
|
/// A contour pass walks the whole arrangement, so one pass per mask would cost masks × edges
|
||||||
|
/// (two blends crossing make thousands of each). Instead the masks go into batches whose faces
|
||||||
|
/// share no vertex (so no edge either): one pass per batch walks each face's contours exactly as
|
||||||
|
/// its own mask's pass would, and the mask a contour bounds is the one of its batch at its first
|
||||||
|
/// point.
|
||||||
fn regions_of(arr: &Arrangement) -> Vec<Region> {
|
fn regions_of(arr: &Arrangement) -> Vec<Region> {
|
||||||
|
let masks = arr.distinct_masks();
|
||||||
|
let ids: HashMap<&[bool], usize> = masks.iter().enumerate().map(|(i, m)| (m.as_slice(), i)).collect();
|
||||||
|
let id = |w: &Multi| ids.get(arr.mask(w).as_slice()).copied();
|
||||||
|
let top = &arr.top;
|
||||||
|
// The masks of the faces around each vertex.
|
||||||
|
let mut around: HashMap<(u64, u64), Vec<usize>> = HashMap::new();
|
||||||
|
for s in top.segment_indices() {
|
||||||
|
let h = s.first_half();
|
||||||
|
let sides = [id(top.winding_clockwise(h)), id(top.winding_counter_clockwise(h))];
|
||||||
|
for end in [h, s.second_half()] {
|
||||||
|
let at = around.entry(vertex_key(top.point(end).to_kurbo())).or_default();
|
||||||
|
for m in sides.into_iter().flatten() {
|
||||||
|
if !at.contains(&m) {
|
||||||
|
at.push(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut near = vec![Vec::new(); masks.len()];
|
||||||
|
for at in around.values() {
|
||||||
|
for &a in at {
|
||||||
|
near[a].extend(at.iter().copied().filter(|&b| b != a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Greedy colouring: each mask takes the first batch none of its neighbours is in.
|
||||||
|
let mut batch = vec![usize::MAX; masks.len()];
|
||||||
|
let mut batches = 0;
|
||||||
|
for m in 0..masks.len() {
|
||||||
|
let mut taken = vec![false; batches];
|
||||||
|
for &n in &near[m] {
|
||||||
|
if let Some(t) = taken.get_mut(batch[n]) {
|
||||||
|
*t = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
batch[m] = taken.iter().position(|t| !t).unwrap_or(batches);
|
||||||
|
batches = batches.max(batch[m] + 1);
|
||||||
|
}
|
||||||
|
let mut faces: Vec<Vec<PathData>> = vec![Vec::new(); masks.len()];
|
||||||
|
for b in 0..batches {
|
||||||
|
let c = top.contours(|w| id(w).is_some_and(|m| batch[m] == b));
|
||||||
|
let owner: Option<Vec<usize>> = c
|
||||||
|
.contours()
|
||||||
|
.map(|ct| {
|
||||||
|
let Some(kurbo::PathEl::MoveTo(p)) = ct.path.elements().first() else { return None };
|
||||||
|
around.get(&vertex_key(*p))?.iter().copied().find(|&m| batch[m] == b)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let Some(owner) = owner else {
|
||||||
|
// Not expected: every contour starts at a vertex of the arrangement. Fall back to a
|
||||||
|
// pass per mask for this batch.
|
||||||
|
for (m, mask) in masks.iter().enumerate().filter(|(m, _)| batch[*m] == b) {
|
||||||
|
let c = arr.contours(|k| k == mask.as_slice());
|
||||||
|
faces[m].extend(c.grouped().into_iter().map(|g| contours_to_path(&c, g, &arr.tidy)));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// A contour belongs under the nearest enclosing contour of the same mask, as in that
|
||||||
|
// mask's own pass.
|
||||||
|
let mut children = vec![Vec::new(); owner.len()];
|
||||||
|
let mut roots = Vec::new();
|
||||||
|
for (i, &m) in owner.iter().enumerate() {
|
||||||
|
let mut up = c[ContourIdx(i)].parent;
|
||||||
|
while let Some(p) = up.filter(|p| owner.get(p.0) != Some(&m)) {
|
||||||
|
up = c[p].parent;
|
||||||
|
}
|
||||||
|
match up {
|
||||||
|
Some(p) => children[p.0].push(i),
|
||||||
|
None => roots.push(i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for r in roots {
|
||||||
|
let mut group = Vec::new();
|
||||||
|
let mut stack = vec![r];
|
||||||
|
while let Some(i) = stack.pop() {
|
||||||
|
group.push(ContourIdx(i));
|
||||||
|
stack.extend(children[i].iter().rev());
|
||||||
|
}
|
||||||
|
faces[owner[r]].push(contours_to_path(&c, group, &arr.tidy));
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for mask in arr.distinct_masks() {
|
for (mask, paths) in masks.iter().zip(faces) {
|
||||||
let c = arr.contours(|m| m == mask.as_slice());
|
|
||||||
let sources: Vec<usize> = mask.iter().enumerate().filter(|(_, b)| **b).map(|(i, _)| i).collect();
|
let sources: Vec<usize> = mask.iter().enumerate().filter(|(_, b)| **b).map(|(i, _)| i).collect();
|
||||||
for g in c.grouped() {
|
out.extend(paths.into_iter().filter(|p| !p.is_empty()).map(|path| Region { path, sources: sources.clone() }));
|
||||||
let path = contours_to_path(&c, g, &arr.tidy);
|
|
||||||
if !path.is_empty() {
|
|
||||||
out.push(Region { path, sources: sources.clone() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A point as a map key (the arrangement's vertices are exact).
|
||||||
|
fn vertex_key(p: Point) -> (u64, u64) {
|
||||||
|
(p.x.to_bits(), p.y.to_bits())
|
||||||
|
}
|
||||||
|
|
||||||
/// All faces of the planar arrangement of `shapes` (every area covered by at least one shape,
|
/// All faces of the planar arrangement of `shapes` (every area covered by at least one shape,
|
||||||
/// split wherever coverage changes).
|
/// split wherever coverage changes).
|
||||||
pub fn regions(shapes: &[Shape]) -> Vec<Region> {
|
pub fn regions(shapes: &[Shape]) -> Vec<Region> {
|
||||||
|
|
@ -277,3 +367,82 @@ fn mid_point(c: &[Seg]) -> Point {
|
||||||
}
|
}
|
||||||
c[0].c.p0
|
c[0].c.p0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use vectorcraft_geom::{Rect, SubPath, shapes};
|
||||||
|
|
||||||
|
/// The faces one contour pass per mask finds: what [`regions_of`] must match exactly.
|
||||||
|
fn regions_one_mask_at_a_time(arr: &Arrangement) -> Vec<Region> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for mask in arr.distinct_masks() {
|
||||||
|
let c = arr.contours(|m| m == mask.as_slice());
|
||||||
|
let sources: Vec<usize> = mask.iter().enumerate().filter(|(_, b)| **b).map(|(i, _)| i).collect();
|
||||||
|
for g in c.grouped() {
|
||||||
|
let path = contours_to_path(&c, g, &arr.tidy);
|
||||||
|
if !path.is_empty() {
|
||||||
|
out.push(Region { path, sources: sources.clone() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_as_one_mask_at_a_time(shapes: &[Shape]) {
|
||||||
|
let arr = arrangement(shapes).expect("arrangement");
|
||||||
|
assert_eq!(regions_of(&arr), regions_one_mask_at_a_time(&arr));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> PathData {
|
||||||
|
shapes::rectangle(Rect::new(x0, y0, x1, y1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shapes on a coarse grid, so that edges and corners coincide: rectangles, rectangles with
|
||||||
|
/// a hole, ellipses and polygons that may cross themselves, under either fill rule.
|
||||||
|
fn arb_shape() -> impl Strategy<Value = Shape> {
|
||||||
|
let c = || (0..12i32).prop_map(|v| f64::from(v) * 10.0);
|
||||||
|
let r = (c(), c(), 1..6i32, 1..6i32).prop_map(|(x, y, w, h)| (x, y, x + f64::from(w) * 10.0, y + f64::from(h) * 10.0));
|
||||||
|
let path = prop_oneof![
|
||||||
|
r.clone().prop_map(|(x0, y0, x1, y1)| rect(x0, y0, x1, y1)),
|
||||||
|
r.clone().prop_map(|(x0, y0, x1, y1)| {
|
||||||
|
let (cx, cy) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
|
||||||
|
let mut p = rect(x0 - 10.0, y0 - 10.0, x1 + 10.0, y1 + 10.0);
|
||||||
|
p.subpaths.extend(rect(x0, y0, cx.max(x0 + 5.0), cy.max(y0 + 5.0)).subpaths);
|
||||||
|
p
|
||||||
|
}),
|
||||||
|
r.prop_map(|(x0, y0, x1, y1)| shapes::ellipse(Rect::new(x0, y0, x1, y1))),
|
||||||
|
prop::collection::vec((c(), c()), 3..7).prop_map(|pts| {
|
||||||
|
let pts: Vec<Point> = pts.into_iter().map(|(x, y)| Point::new(x, y)).collect();
|
||||||
|
PathData::single(SubPath::polyline(&pts, true))
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
(path, any::<bool>(), any::<u64>())
|
||||||
|
.prop_map(|(p, even_odd, key)| Shape::new(p, if even_odd { FillRule::EvenOdd } else { FillRule::NonZero }, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crossing_strips_match_one_mask_at_a_time() {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
for i in 0..12 {
|
||||||
|
let x = 10.0 + 20.0 * f64::from(i);
|
||||||
|
v.push(Shape::new(rect(x, 0.0, x + 4.0, 250.0), FillRule::NonZero, 1));
|
||||||
|
v.push(Shape::new(rect(0.0, x, 250.0, x + 4.0), FillRule::NonZero, 2));
|
||||||
|
}
|
||||||
|
same_as_one_mask_at_a_time(&v);
|
||||||
|
// Every crossing, and each strip in 13 pieces.
|
||||||
|
assert_eq!(regions(&v).len(), 12 * 12 + 2 * 12 * 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig { cases: 256, failure_persistence: None, rng_seed: proptest::test_runner::RngSeed::Fixed(0x5eed_9a7f), ..ProptestConfig::default() })]
|
||||||
|
|
||||||
|
/// Batching the masks changes nothing: the same regions, in the same order, with the same
|
||||||
|
/// contours in each.
|
||||||
|
#[test]
|
||||||
|
fn batched_regions_match_one_mask_at_a_time(v in prop::collection::vec(arb_shape(), 1..8)) {
|
||||||
|
same_as_one_mask_at_a_time(&v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue