mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
Shape Builder: a merge across an overlap thinner than the precision gives one path
Two shapes that overlap by less than the region precision (0.01 pt) leave their overlap out of the Shape Builder's regions as a sliver. Merging the regions on both sides of it united only the picked faces, so the result was a compound path with a hairline gap where the overlap had been. A merge now takes in each dropped sliver next to a merged face when only the merged faces' shapes fill it, both from the arrangement and from the planar map used when open paths are selected. A thin cutout that no shape fills stays a hole. What is left of a shape after the merge takes in a sliver only where the shape covers it and the merged shape does not take it in, so the two do not overlap. Pathfinder operations and Live Paint do not use the merger and are unchanged; Live Paint's planar map does not test which shapes cover a sliver.
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