mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
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:
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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(&[®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.
|
||||
|
||||
Reference in New Issue
Block a user