diff --git a/crates/engine/src/cmd/buildcmds.rs b/crates/engine/src/cmd/buildcmds.rs index 430ae5233..413f433b8 100644 --- a/crates/engine/src/cmd/buildcmds.rs +++ b/crates/engine/src/cmd/buildcmds.rs @@ -385,7 +385,8 @@ fn sb_merge(s: &mut Session, p: &Value) -> Result { let mut rest = if in_union { // What is left of it: its faces not merged or deleted, united. They share their // edges with the merged faces exactly, where subtracting the merged shape left - // slivers of the outline along them (#569). + // slivers of the outline along them (#569). A sliver goes in only where the + // shape covers it and the merged shape does not take it in. let kept: Vec<&po::Region> = map .regions .iter() @@ -393,7 +394,7 @@ fn sb_merge(s: &mut Session, p: &Value) -> Result { .filter(|(k, (r, ..))| r.sources.contains(&i) && !touched.regions.contains(k)) .map(|(_, (r, ..))| r) .collect(); - if kept.is_empty() { PathData::default() } else { faces.merge(&kept) } + if kept.is_empty() { PathData::default() } else { faces.merge_rest(i, &kept, &picked) } } else { b::node_outline(leaf).map_or_else(|| sh.path.clone(), |o| o.0) }; diff --git a/crates/engine/src/tests_build.rs b/crates/engine/src/tests_build.rs index 63488afb1..606be610e 100644 --- a/crates/engine/src/tests_build.rs +++ b/crates/engine/src/tests_build.rs @@ -115,6 +115,143 @@ fn shape_builder_drag_merges_a_narrow_overlap_when_far_art_is_selected() { assert!((area(&s, merged) - 19900.0).abs() < 1.0, "{}", area(&s, merged)); } +#[test] +fn shape_builder_drag_merges_across_an_overlap_thinner_than_the_precision() { + let mut s = session(); + let a = rect(&mut s, 100.0, 100.0, 100.0, 100.0); + let b = rect(&mut s, 199.999, 100.0, 100.0, 100.0); + s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap(); + let r = s.execute("shapeBuilder.merge", &json!({"points": [[150, 150], [250, 150]]})).unwrap(); + let out = ids(&r); + assert_eq!(out.len(), 1, "{out:?}"); + assert_eq!(outline(&node(&s, out[0])).0.subpaths.len(), 1, "no hairline gap at the 0.001 pt overlap"); + assert!((area(&s, out[0]) - 19999.9).abs() < 1.0, "{}", area(&s, out[0])); + // A click on one side leaves the sliver to the other square, which stays whole. + s.execute("edit.undo", &json!({})).unwrap(); + let r = s.execute("shapeBuilder.merge", &json!({"points": [[150, 150]]})).unwrap(); + let merged = NodeId(r["merged"].as_u64().unwrap()); + assert!((area(&s, merged) - 9999.9).abs() < 1e-3, "{}", area(&s, merged)); + let out = ids(&r); + assert_eq!(out.len(), 2, "{out:?}"); + let rest = out.iter().copied().find(|i| *i != merged).unwrap(); + assert!((area(&s, rest) - 10000.0).abs() < 1e-6, "{}", area(&s, rest)); +} + +#[test] +fn shape_builder_drag_merges_across_an_overlap_thinner_than_the_precision_with_a_line_selected() { + let mut s = session(); + let a = rect(&mut s, 100.0, 100.0, 100.0, 100.0); + let b = rect(&mut s, 199.999, 100.0, 100.0, 100.0); + let l = line(&mut s, (400.0, 400.0), (450.0, 450.0)); + s.execute("select.set", &json!({"ids": [a.0, b.0, l.0]})).unwrap(); + let r = s.execute("shapeBuilder.merge", &json!({"points": [[150, 150], [250, 150]]})).unwrap(); + let merged = NodeId(r["merged"].as_u64().unwrap()); + assert_eq!(ids(&r).len(), 2, "the merged shape and the line: {r}"); + assert_eq!(outline(&node(&s, merged)).0.subpaths.len(), 1, "no hairline gap at the 0.001 pt overlap"); + assert!((area(&s, merged) - 19999.9).abs() < 1.0, "{}", area(&s, merged)); +} + +#[test] +fn shape_builder_click_beside_a_sliver_leaves_it_to_one_shape() { + let mut s = session(); + // Two lines 0.005 pt apart cut a square; the strip between them is a sliver. + let a = rect(&mut s, 100.0, 100.0, 100.0, 100.0); + let l1 = line(&mut s, (150.0, 50.0), (150.0, 250.0)); + let l2 = line(&mut s, (150.005, 50.0), (150.005, 250.0)); + s.execute("select.set", &json!({"ids": [a.0, l1.0, l2.0]})).unwrap(); + let r = s.execute("shapeBuilder.merge", &json!({"points": [[120, 150]]})).unwrap(); + let merged = NodeId(r["merged"].as_u64().unwrap()); + let rest: Vec = ids(&r).into_iter().filter(|i| *i != merged && outline(&node(&s, *i)).0.subpaths.iter().all(|sp| sp.closed)).collect(); + assert_eq!(rest.len(), 1, "{r}"); + let total = area(&s, merged) + area(&s, rest[0]); + assert!((total - 10000.0).abs() < 0.1, "the merged shape and the rest overlap: {} + {}", area(&s, merged), area(&s, rest[0])); +} + +#[test] +fn shape_builder_click_beside_a_sliver_of_closed_shapes_leaves_it_to_one_shape() { + let mut s = session(); + // Two squares over a third leave a strip of it 0.005 pt wide between them. + let a = rect(&mut s, 100.0, 100.0, 100.0, 100.0); + let b = rect(&mut s, 100.0, 100.0, 49.9975, 100.0); + let c = rect(&mut s, 150.0025, 100.0, 49.9975, 100.0); + s.execute("select.set", &json!({"ids": [a.0, b.0, c.0]})).unwrap(); + let r = s.execute("shapeBuilder.merge", &json!({"points": [[125, 150]]})).unwrap(); + let out = ids(&r); + assert_eq!(out.len(), 3, "the merged shape, the rest of the back square and the right square: {r}"); + let total: f64 = out.iter().map(|i| area(&s, *i)).sum(); + assert!((total - 14999.75).abs() < 0.1, "the merged shape and the rest overlap: {total}"); +} + +#[test] +fn shape_builder_click_keeps_a_sliver_the_merge_leaves_out_in_the_rest() { + // Also with a line selected, which merges in the planar map. + for with_line in [false, true] { + let mut s = session(); + // B overlaps A and C by 0.005 pt more than C does: the strip of A and B between them is a + // sliver next to the region of A alone, which a click merges without it. + let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0); + let b = rect(&mut s, 49.995, 0.0, 50.005, 100.0); + let c = rect(&mut s, 50.0, 0.0, 50.0, 100.0); + let mut sel = vec![a.0, b.0, c.0]; + if with_line { + sel.push(line(&mut s, (400.0, 400.0), (450.0, 450.0)).0); + } + s.execute("select.set", &json!({ "ids": sel })).unwrap(); + let r = s.execute("shapeBuilder.merge", &json!({"points": [[25, 50]]})).unwrap(); + let merged = NodeId(r["merged"].as_u64().unwrap()); + assert!((area(&s, merged) - 4999.5).abs() < 0.1, "{}", area(&s, merged)); + let out = ids(&r); + assert_eq!(out.len(), sel.len() + 1, "{r}"); + // What is left of A, B and C: 5,000.5, 5,000.5 and 5,000. + let total: f64 = out.iter().map(|i| area(&s, *i)).sum(); + assert!((total - 20000.5).abs() < 0.1, "what is left of the back square lost the strip: {total} ({with_line})"); + } +} + +#[test] +fn shape_builder_click_keeps_a_sliver_away_from_the_merge_in_the_rest() { + let mut s = session(); + // Three squares over a fourth leave two strips of it 0.005 pt wide; a click merges the left + // square's region, next to the first strip only. + let a = rect(&mut s, 100.0, 100.0, 150.0, 100.0); + let b = rect(&mut s, 100.0, 100.0, 49.9975, 100.0); + let c = rect(&mut s, 150.0025, 100.0, 49.995, 100.0); + let e = rect(&mut s, 200.0025, 100.0, 49.9975, 100.0); + s.execute("select.set", &json!({"ids": [a.0, b.0, c.0, e.0]})).unwrap(); + let r = s.execute("shapeBuilder.merge", &json!({"points": [[125, 150]]})).unwrap(); + let out = ids(&r); + assert_eq!(out.len(), 4, "{r}"); + let rest = out.iter().copied().find(|i| area(&s, *i) > 9000.0).unwrap(); + assert_eq!(outline(&node(&s, rest)).0.subpaths.len(), 1, "no hairline gap at the second strip"); + let total: f64 = out.iter().map(|i| area(&s, *i)).sum(); + assert!((total - 24999.25).abs() < 0.1, "{total}"); +} + +#[test] +fn shape_builder_click_keeps_a_sliver_away_from_the_merge_in_the_rest_with_a_compound_path() { + let mut s = session(); + // The same with the three squares in one compound path, whose three regions have one coverage. + let a = rect(&mut s, 100.0, 100.0, 150.0, 100.0); + let b = rect(&mut s, 100.0, 100.0, 49.9975, 100.0); + let c = rect(&mut s, 150.0025, 100.0, 49.995, 100.0); + let e = rect(&mut s, 200.0025, 100.0, 49.9975, 100.0); + s.execute("select.set", &json!({"ids": [b.0, c.0, e.0]})).unwrap(); + let compound = NodeId(s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap()); + s.execute("select.set", &json!({"ids": [a.0, compound.0]})).unwrap(); + let r = s.execute("shapeBuilder.merge", &json!({"points": [[125, 150]]})).unwrap(); + let merged = NodeId(r["merged"].as_u64().unwrap()); + assert!((area(&s, merged) - 5000.25).abs() < 0.1, "{}", area(&s, merged)); + let out = ids(&r); + assert_eq!(out.len(), 3, "{r}"); + // What is left of the back square holds the second strip; what is left of the compound path + // keeps the gap it had there. + let mut rest: Vec<(usize, f64)> = + out.iter().copied().filter(|i| *i != merged).map(|i| (outline(&node(&s, i)).0.subpaths.len(), area(&s, i))).collect(); + rest.sort_by_key(|r| r.0); + assert_eq!(rest.iter().map(|r| r.0).collect::>(), [1, 2], "{rest:?}"); + assert!((rest[0].1 - 9999.75).abs() < 0.1 && (rest[1].1 - 9999.25).abs() < 0.1, "{rest:?}"); +} + #[test] fn shape_builder_click_splits_out_one_region() { let mut s = session(); diff --git a/crates/pathops/src/pathfinder.rs b/crates/pathops/src/pathfinder.rs index 593c6215c..67e28ada7 100644 --- a/crates/pathops/src/pathfinder.rs +++ b/crates/pathops/src/pathfinder.rs @@ -1,7 +1,7 @@ //! Illustrator's Pathfinder panel and Shape Builder regions, over an ordered stack of shapes //! (index 0 = back-most, last = front-most). -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use kurbo::{BezPath, Line, ParamCurve, ParamCurveNearest, PathSeg, Point, Shape as _}; use linesweeper::topology::ContourIdx; @@ -359,29 +359,61 @@ impl FaceMerger { /// The union of `faces`. pub fn merge(&self, faces: &[&Region]) -> PathData { - self.exact(faces).or_else(|| self.planar(faces)).filter(|p| !p.is_empty()).unwrap_or_else(|| merge_regions(faces)) + self.merge_with(faces, None) + } + + /// What is left of input `shape` when `merged` merge on their own: the union of `faces`, as + /// [`merge`](Self::merge) gives it, except that a sliver goes in only when `shape` covers it + /// and the merge of `merged` does not take it in. + pub fn merge_rest(&self, shape: usize, faces: &[&Region], merged: &[&Region]) -> PathData { + self.merge_with(faces, Some((shape, merged))) + } + + fn merge_with(&self, faces: &[&Region], rest: Option<(usize, &[&Region])>) -> PathData { + self.exact(faces, rest).or_else(|| self.planar(faces, rest)).filter(|p| !p.is_empty()).unwrap_or_else(|| merge_regions(faces)) } /// `faces` merged in the planar map of the shapes: each face found again by a point inside /// it, and the result checked to cover what they cover. - fn planar(&self, faces: &[&Region]) -> Option { + fn planar(&self, faces: &[&Region], rest: Option<(usize, &[&Region])>) -> Option { let (shapes, map) = self.lines.as_ref()?; - let map = map.get_or_init(|| crate::planar::map(shapes)).as_ref()?; + let map = map.get_or_init(|| crate::planar::map(shapes, true)).as_ref()?; let inside: Vec = faces.iter().map(|r| crate::planar::interior_point(&r.path)).collect::>()?; - let merged = map.merge(&inside)?; + let rest = rest.map(|(shape, merged)| crate::planar::Rest { + shape, + merged: merged.iter().filter_map(|r| crate::planar::interior_point(&r.path)).collect(), + cover: cover(merged, shapes.len()), + }); + let merged = map.merge(&inside, &cover(faces, shapes.len()), rest.as_ref())?; // A face not found as itself (two points in one face of the map) shows in the area. let want: f64 = faces.iter().map(|r| crate::area(&r.path, FillRule::NonZero)).sum(); let outlines: f64 = faces.iter().map(|r| r.path.to_bezpath().perimeter(1e-6)).sum(); ((crate::area(&merged, FillRule::NonZero) - want).abs() <= 1e-9 * want.max(1.0) + crate::DEFAULT_PRECISION * outlines).then_some(merged) } - fn exact(&self, faces: &[&Region]) -> Option { + fn exact(&self, faces: &[&Region], rest: Option<(usize, &[&Region])>) -> Option { // A gap no shape covers is no face of the arrangement: it merges in the planar map. if faces.iter().any(|r| r.sources.is_empty()) { return None; } let arr = self.arr.as_ref()?; - // Each face by its coverage and a point inside it. + let mut raw = self.contours_of(arr, faces)?; + // The faces merged on their own and their corners. + let rest = rest.map(|(shape, merged)| { + let merged: Vec<&Region> = merged.iter().copied().filter(|r| !r.sources.is_empty()).collect(); + let corners = self + .contours_of(arr, &merged) + .map(|c| c.elements().iter().filter_map(|el| el.end_point()).map(vertex_key).collect()) + .unwrap_or_default(); + Rest { shape, merged, corners } + }); + add_slivers(arr, faces, rest.as_ref(), &mut raw); + unite_faces(&raw, &arr.tidy, &self.snap) + } + + /// The contours of the faces of `arr` that are `faces`, each found by its coverage and a point + /// inside it. + fn contours_of(&self, arr: &Arrangement, faces: &[&Region]) -> Option { let mut picks: Vec<(Vec, Point)> = Vec::with_capacity(faces.len()); for r in faces { let mut mask = vec![false; self.shapes]; @@ -408,7 +440,86 @@ impl FaceMerger { } } } - unite_faces(&raw, &arr.tidy, &self.snap) + Some(raw) + } +} + +/// What is left of input `shape` when the faces `merged` merge on their own, and the corners of +/// those faces ([`add_slivers`]). +struct Rest<'a> { + shape: usize, + merged: Vec<&'a Region>, + corners: HashSet<(u64, u64)>, +} + +/// For each of `shapes` inputs, whether it covers one of `faces`. +pub(crate) fn cover(faces: &[&Region], shapes: usize) -> Vec { + let mut cover = vec![false; shapes]; + for &i in faces.iter().flat_map(|r| &r.sources) { + if let Some(c) = cover.get_mut(i) { + *c = true; + } + } + cover +} + +/// Adds to `raw` the faces of `arr` that [`regions`] drops as slivers ([`is_sliver`]) when they lie +/// next to a face with the coverage of one of `faces` and only shapes that cover `faces` cover +/// them, such as the overlap of two shapes that overlap by less than the precision. Without them, +/// merging the faces on both sides of a sliver leaves a hairline gap between them. A sliver that +/// touches none of `faces` stays a contour of its own, which [`unite_faces`] drops. +/// +/// With `rest`, the merge is what is left of an input when other faces merge on their own: a +/// sliver goes in only when that input covers it, and not when it has an edge along one of those +/// faces and only shapes that cover them cover it, as it goes to their merge. +/// +/// [`is_sliver`]: crate::boolean::is_sliver +fn add_slivers(arr: &Arrangement, faces: &[&Region], rest: Option<&Rest>, raw: &mut BezPath) { + let top = &arr.top; + let shapes = arr.rules.len(); + let cover = cover(faces, shapes); + let merged_cover = rest.map(|r| self::cover(&r.merged, shapes)).unwrap_or_default(); + let picked: HashSet<&[usize]> = faces.iter().map(|r| r.sources.as_slice()).collect(); + let beside: HashSet<&[usize]> = rest.iter().flat_map(|r| r.merged.iter().map(|f| f.sources.as_slice())).collect(); + let coverage = |w: &Multi| arr.covered(w).collect::>(); + let is_picked = |w: &Multi| picked.contains(coverage(w).as_slice()); + let within = |w: &Multi, c: &[bool]| arr.covered(w).next().is_some() && arr.covered(w).all(|i| c.get(i) == Some(&true)); + let ours = |w: &Multi| rest.is_none_or(|r| arr.covered(w).any(|i| i == r.shape)); + // The coverage of each face next to a picked one, if only shapes of `faces` cover it, and the + // ends of the edges along faces of `rest` that would go to their merge. + let mut next_to: Vec> = Vec::new(); + let mut taken: HashSet<(u64, u64)> = HashSet::new(); + for s in top.segment_indices() { + let h = s.first_half(); + let (a, b) = (top.winding_clockwise(h), top.winding_counter_clockwise(h)); + for (w, other) in [(a, b), (b, a)] { + if within(w, &cover) && ours(w) && !is_picked(w) && is_picked(other) { + let mask = arr.mask(w); + if !next_to.contains(&mask) { + next_to.push(mask); + } + } + if let Some(r) = rest + && within(w, &merged_cover) + && beside.contains(coverage(other).as_slice()) + { + let ends = [h, h.other_half()].map(|e| vertex_key(top.point(e).to_kurbo())); + if ends.iter().all(|k| r.corners.contains(k)) { + taken.extend(ends); + } + } + } + } + let along_merged = |path: &BezPath| path.elements().iter().any(|el| el.end_point().is_some_and(|p| taken.contains(&vertex_key(p)))); + for mask in next_to { + let c = arr.contours(|k| k == mask.as_slice()); + for group in c.grouped() { + if contours_to_path(&c, group.iter().copied(), &arr.tidy).is_empty() && !group.iter().any(|&i| along_merged(&c[i].path)) { + for i in group { + raw.extend(c[i].path.iter()); + } + } + } } } diff --git a/crates/pathops/src/planar.rs b/crates/pathops/src/planar.rs index d55a70e11..c647208f7 100644 --- a/crates/pathops/src/planar.rs +++ b/crates/pathops/src/planar.rs @@ -1047,6 +1047,14 @@ fn span(sp: &SubPath, u0: f64, u1: f64) -> Option { segs_to_subpath(&segs, false) } +/// What is left of input `shape` when the faces that hold `merged` merge on their own, with the +/// inputs that cover them marked in `cover` ([`Map::merge`]). +pub(crate) struct Rest { + pub(crate) shape: usize, + pub(crate) merged: Vec, + pub(crate) cover: Vec, +} + /// The planar graph of `shapes` (in the sweep's frame) with its faces walked: what [`build`] makes /// Live Paint faces and edges from, and what [`Map::merge`] merges faces in. pub(crate) struct Map { @@ -1057,6 +1065,9 @@ pub(crate) struct Map { /// hold a point, so finding the face around one tests only those. faces: Vec<(Vec, BezPath, f64)>, face_index: RectIndex, + /// Bounded faces left out as slivers ([`is_sliver`]): their cycle of half-edges and the inputs + /// whose fill covers them (none for a gap, such as a thin cutout). + slivers: Vec<(Vec, Vec)>, /// Outer boundaries of connected pieces, and the face each one is a hole in. holes: Vec<(Vec, BezPath)>, face_holes: Vec>, @@ -1066,7 +1077,9 @@ pub(crate) struct Map { snap: f64, } -pub(crate) fn map(shapes: &[Shape]) -> Option { +/// With `cover`, also finds which inputs cover each sliver, which only [`Map::merge`] reads; +/// without it the slivers' lists of inputs stay empty. +pub(crate) fn map(shapes: &[Shape], cover: bool) -> Option { let mut inputs: Vec<(BezPath, usize)> = Vec::new(); for (i, s) in shapes.iter().enumerate() { let mut bp = BezPath::new(); @@ -1277,6 +1290,7 @@ pub(crate) fn map(shapes: &[Shape]) -> Option { // face around them, if any). let mut seen = vec![false; 2 * edges.len()]; let mut faces: Vec<(Vec, BezPath, f64)> = Vec::new(); + let mut slivers: Vec<(Vec, BezPath)> = Vec::new(); let mut holes: Vec<(Vec, BezPath)> = Vec::new(); for start in 0..2 * edges.len() { if seen.get(start).copied().unwrap_or(true) { @@ -1300,6 +1314,8 @@ pub(crate) fn map(shapes: &[Shape]) -> Option { let area = bp.area(); if area > 0.0 && !is_sliver(&bp, tidy.precision) { faces.push((cycle, bp, area)); + } else if area > 0.0 { + slivers.push((cycle, bp)); } else if area < -(tidy.precision * tidy.precision) { holes.push((cycle, bp)); } @@ -1322,21 +1338,41 @@ pub(crate) fn map(shapes: &[Shape]) -> Option { } } + // The inputs that cover each sliver, tested at a point inside it. + let fills: Vec = if slivers.is_empty() || !cover { Vec::new() } else { shapes.iter().map(Fill::new).collect() }; + let slivers: Vec<(Vec, Vec)> = slivers + .into_iter() + .map(|(cycle, mut bp)| { + if !cover { + return (cycle, Vec::new()); + } + bp.apply_affine(UNSHEAR); + let sources = interior_point(&PathData::from_bezpath(&bp)) + .map(|p| fills.iter().enumerate().filter(|(_, f)| f.contains(p)).map(|(i, _)| i).collect()) + .unwrap_or_default(); + (cycle, sources) + }) + .collect(); + // The inputs' anchors, which results land on again up to the sweep's tolerance. let snap = 2.0 * eps; let mut anchors: Vec = shapes.iter().flat_map(|s| s.path.subpaths.iter().flat_map(|sp| sp.anchors.iter().map(|a| a.p))).collect(); anchors.sort_by(|a, b| a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y))); - Some(Map { edges, around, slot_of, faces, face_index, holes, face_holes, tidy, anchors, snap }) + Some(Map { edges, around, slot_of, faces, face_index, slivers, holes, face_holes, tidy, anchors, snap }) } impl Map { /// The union of the faces that hold `inside` (document points, one per face), traced along the /// map's own edges: those between two merged faces drop out exactly, where a boolean union of /// the faces' outlines can leave them in when its sweep rounds their shared corners apart - /// (#893). Outer outlines come out positive, holes negative. None when a point is in no face. - pub(crate) fn merge(&self, inside: &[Point]) -> Option { + /// (#893). A sliver next to a merged face that only inputs marked in `cover` fill merges with + /// them, so it doesn't stay as a hairline gap between them. With `rest`, a sliver goes in only + /// when its input covers it, and not when the merge of the faces merged on their own takes it + /// in. Outer outlines come out positive, holes negative. None when a point is in no face. + pub(crate) fn merge(&self, inside: &[Point], cover: &[bool], rest: Option<&Rest>) -> Option { let halves = 2 * self.edges.len(); - // The face on each half-edge's side; holes bound the face around them. + // The face on each half-edge's side; holes bound the face around them. Slivers come after + // the faces. let mut face_of: Vec> = vec![None; halves]; for (i, (cycle, ..)) in self.faces.iter().enumerate() { let hole_cycles = self.face_holes.get(i)?.iter().filter_map(|&k| self.holes.get(k)).map(|(c, _)| c); @@ -1344,17 +1380,40 @@ impl Map { *face_of.get_mut(h)? = Some(i); } } - let mut picked = vec![false; self.faces.len()]; - for &p in inside { + for (k, (cycle, _)) in self.slivers.iter().enumerate() { + for &h in cycle { + *face_of.get_mut(h)? = Some(self.faces.len() + k); + } + } + // The smallest face around a point: faces of a piece inside a hole lie within the face around. + let face_at = |p: Point| { let q = SHEAR * p; - // The smallest face around it: faces of a piece inside a hole lie within the face around. - let (i, _) = self - .face_index + self.face_index .at(q, 0.0) .filter_map(|&i| self.faces.get(i).map(|f| (i, f))) .filter(|(_, (_, bp, _))| bp.winding(q) != 0) - .min_by(|a, b| a.1.2.total_cmp(&b.1.2))?; - *picked.get_mut(i)? = true; + .min_by(|a, b| a.1.2.total_cmp(&b.1.2)) + .map(|(i, _)| i) + }; + let mut picked = vec![false; self.faces.len() + self.slivers.len()]; + for &p in inside { + *picked.get_mut(face_at(p)?)? = true; + } + let mut other = vec![false; self.faces.len()]; + for i in rest.iter().flat_map(|r| &r.merged).filter_map(|&p| face_at(p)) { + if let Some(o) = other.get_mut(i) { + *o = true; + } + } + let next_to = |cycle: &[usize], marked: &[bool]| { + cycle.iter().any(|&h| face_of.get(twin(h)).copied().flatten().is_some_and(|f| f < self.faces.len() && marked.get(f) == Some(&true))) + }; + for (k, (cycle, sources)) in self.slivers.iter().enumerate() { + let within = |c: &[bool]| !sources.is_empty() && sources.iter().all(|&i| c.get(i) == Some(&true)); + let ours = rest.is_none_or(|r| sources.contains(&r.shape) && !(within(&r.cover) && next_to(cycle, &other))); + if within(cover) && ours && next_to(cycle, &picked) { + *picked.get_mut(self.faces.len() + k)? = true; + } } let merged = |h: usize| face_of.get(h).copied().flatten().is_some_and(|f| picked.get(f) == Some(&true)); let boundary = |h: usize| merged(h) && !merged(twin(h)); @@ -1390,7 +1449,7 @@ impl Map { } fn build(shapes: &[Shape]) -> Option<(Vec, Vec)> { - let m = map(shapes)?; + let m = map(shapes, false)?; let Map { edges, around, faces, holes, face_holes, tidy, anchors, snap, .. } = &m; let snap = *snap; let fills: Vec = shapes.iter().map(Fill::new).collect(); @@ -1840,11 +1899,11 @@ mod tests { let (faces, _) = live_paint(&s); let cell = |x: f64, y: f64| at(&faces, x, y)[0]; let merger = crate::FaceMerger::new(&s); - let m = map(&s).unwrap(); + let m = map(&s, true).unwrap(); let check = |picked: &[&Region], subpaths: usize, area_want: f64| { // Merged in the map itself, not by the fallback union. let inside: Vec = picked.iter().map(|f| interior_point(&f.path).unwrap()).collect(); - assert_eq!(m.merge(&inside).as_ref(), Some(&merger.merge(picked))); + assert_eq!(m.merge(&inside, &crate::pathfinder::cover(picked, s.len()), None).as_ref(), Some(&merger.merge(picked))); let m = merger.merge(picked); assert_eq!(m.subpaths.len(), subpaths, "{m:?}"); assert!((crate::area(&m, FillRule::NonZero) - area_want).abs() < 1e-6, "{}", crate::area(&m, FillRule::NonZero)); @@ -1859,6 +1918,34 @@ mod tests { check(&[cell(5.0, 5.0), cell(15.0, 15.0)], 2, 200.0); } + /// Merging in the map takes in a sliver next to the merged faces that only their shapes fill, + /// such as the overlap of two squares that overlap by 0.001, and leaves a thin cutout that + /// nothing fills as it is. + #[test] + fn faces_merge_in_the_map_with_the_slivers_their_shapes_fill() { + let far = line((400.0, 400.0), (450.0, 450.0), 2); + let s = [rect(100.0, 100.0, 200.0, 200.0, 0), rect(199.999, 100.0, 299.999, 200.0, 1), far.clone()]; + let regions = shape_builder(&s, false).regions; + assert_eq!(regions.len(), 2, "the overlap is a sliver: {regions:?}"); + let both: Vec<&Region> = regions.iter().collect(); + let merged = crate::FaceMerger::new(&s).merge(&both); + assert_eq!(merged.subpaths.len(), 1, "{merged:?}"); + assert!((crate::area(&merged, FillRule::NonZero) - 19999.9).abs() < 1e-6, "{}", crate::area(&merged, FillRule::NonZero)); + // Live Paint's map does not test which inputs cover the sliver. + let covered = |cover: bool| map(&s, cover).unwrap().slivers.into_iter().map(|(_, sources)| sources).collect::>(); + assert_eq!(covered(true), [vec![0, 1]]); + assert_eq!(covered(false), [Vec::::new()]); + // A square with a cutout 0.005 wide. + let mut frame = shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)); + frame.subpaths.extend(shapes::rectangle(Rect::new(50.0, 10.0, 50.005, 90.0)).subpaths); + let s = [Shape::new(frame, FillRule::EvenOdd, 0), far]; + let regions = shape_builder(&s, false).regions; + assert_eq!(regions.len(), 1, "{regions:?}"); + let merged = crate::FaceMerger::new(&s).merge(&[®ions[0]]); + assert_eq!(merged.subpaths.len(), 2, "the cutout stays: {merged:?}"); + assert!((crate::area(&merged, FillRule::EvenOdd) - 9999.6).abs() < 1e-6, "{}", crate::area(&merged, FillRule::EvenOdd)); + } + #[test] fn shape_builder_lines_cut_regions() { // A line across a rectangle: two regions, the line in three pieces keyed by its index.