Merge pull request #4079 from dotCooCoo/fix/shape-builder-merge-thin-overlap

Shape Builder: a merge across an overlap thinner than the precision gives one path
This commit is contained in:
BFlat
2026-10-26 09:16:54 +07:00
committed by GitHub
4 changed files with 361 additions and 25 deletions
+3 -2
View File
@@ -385,7 +385,8 @@ fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
.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)
};
+137
View File
@@ -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<NodeId> = 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::<Vec<_>>(), [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();
+119 -8
View File
@@ -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<PathData> {
fn planar(&self, faces: &[&Region], rest: Option<(usize, &[&Region])>) -> Option<PathData> {
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<Point> = faces.iter().map(|r| crate::planar::interior_point(&r.path)).collect::<Option<_>>()?;
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<PathData> {
fn exact(&self, faces: &[&Region], rest: Option<(usize, &[&Region])>) -> Option<PathData> {
// 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<BezPath> {
let mut picks: Vec<(Vec<bool>, 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<bool> {
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::<Vec<_>>();
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<bool>> = 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());
}
}
}
}
}
+102 -15
View File
@@ -1047,6 +1047,14 @@ fn span(sp: &SubPath, u0: f64, u1: f64) -> Option<SubPath> {
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<Point>,
pub(crate) cover: Vec<bool>,
}
/// 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<usize>, BezPath, f64)>,
face_index: RectIndex<usize>,
/// 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<usize>, Vec<usize>)>,
/// Outer boundaries of connected pieces, and the face each one is a hole in.
holes: Vec<(Vec<usize>, BezPath)>,
face_holes: Vec<Vec<usize>>,
@@ -1066,7 +1077,9 @@ pub(crate) struct Map {
snap: f64,
}
pub(crate) fn map(shapes: &[Shape]) -> Option<Map> {
/// 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<Map> {
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<Map> {
// face around them, if any).
let mut seen = vec![false; 2 * edges.len()];
let mut faces: Vec<(Vec<usize>, BezPath, f64)> = Vec::new();
let mut slivers: Vec<(Vec<usize>, BezPath)> = Vec::new();
let mut holes: Vec<(Vec<usize>, 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<Map> {
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<Map> {
}
}
// The inputs that cover each sliver, tested at a point inside it.
let fills: Vec<Fill> = if slivers.is_empty() || !cover { Vec::new() } else { shapes.iter().map(Fill::new).collect() };
let slivers: Vec<(Vec<usize>, Vec<usize>)> = 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<Point> = 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<PathData> {
/// (#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<PathData> {
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<Option<usize>> = 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<Region>, Vec<Shape>)> {
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<Fill> = 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<Point> = 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::<Vec<_>>();
assert_eq!(covered(true), [vec![0, 1]]);
assert_eq!(covered(false), [Vec::<usize>::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(&[&regions[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.