mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
Fix #3426: the Pathfinder panel's Pathfinder Options… sets Precision, Remove Redundant Points and Divide and Outline Will Remove Unpainted Artwork
Squashed from #4141 onto main.
This commit is contained in:
committed by
bflatastic
parent
a8345dcfb1
commit
cfd085f34a
@@ -687,6 +687,11 @@ pub fn boolean_n(paths: &[(&PathData, FillRule)], pred: impl Fn(&[bool]) -> bool
|
||||
/// [`unite_all`] that reports a refused input instead of leaving it out of the union or returning
|
||||
/// an empty path.
|
||||
pub fn try_unite_all(paths: &[(&PathData, FillRule)]) -> Result<PathData, PathOpsError> {
|
||||
try_unite_all_with(paths, DEFAULT_PRECISION)
|
||||
}
|
||||
|
||||
/// [`try_unite_all`] refit to `precision`, dropping slivers thinner than it.
|
||||
pub fn try_unite_all_with(paths: &[(&PathData, FillRule)], precision: f64) -> Result<PathData, PathOpsError> {
|
||||
let mut all = BezPath::new();
|
||||
for (p, r) in paths {
|
||||
let bp = fill_bezpath(p);
|
||||
@@ -700,13 +705,18 @@ pub fn try_unite_all(paths: &[(&PathData, FillRule)]) -> Result<PathData, PathOp
|
||||
if all.elements().is_empty() {
|
||||
return Ok(PathData::default());
|
||||
}
|
||||
let tidy = Tidy::keeping(DEFAULT_PRECISION, paths.iter().map(|(p, _)| fill_bezpath(p)).collect::<Vec<_>>().iter());
|
||||
let tidy = Tidy::keeping(precision, paths.iter().map(|(p, _)| fill_bezpath(p)).collect::<Vec<_>>().iter());
|
||||
Ok(all_contours_to_path(&normalize_bez(&all, FillRule::NonZero)?, &tidy))
|
||||
}
|
||||
|
||||
/// Union of many filled paths. Each input is first normalised on its own, then all are swept
|
||||
/// together with a single integer winding number, which keeps this fast for thousands of shapes.
|
||||
pub fn unite_all(paths: &[(&PathData, FillRule)]) -> PathData {
|
||||
unite_all_with(paths, DEFAULT_PRECISION)
|
||||
}
|
||||
|
||||
/// [`unite_all`] refit to `precision`, dropping slivers thinner than it.
|
||||
pub fn unite_all_with(paths: &[(&PathData, FillRule)], precision: f64) -> PathData {
|
||||
let mut all = BezPath::new();
|
||||
for (p, r) in paths {
|
||||
let bp = fill_bezpath(p);
|
||||
@@ -721,7 +731,7 @@ pub fn unite_all(paths: &[(&PathData, FillRule)]) -> PathData {
|
||||
if all.elements().is_empty() {
|
||||
return PathData::default();
|
||||
}
|
||||
let tidy = Tidy::keeping(DEFAULT_PRECISION, paths.iter().map(|(p, _)| fill_bezpath(p)).collect::<Vec<_>>().iter());
|
||||
let tidy = Tidy::keeping(precision, paths.iter().map(|(p, _)| fill_bezpath(p)).collect::<Vec<_>>().iter());
|
||||
match normalize_bez(&all, FillRule::NonZero) {
|
||||
Ok(c) => all_contours_to_path(&c, &tidy),
|
||||
Err(_) => PathData::default(),
|
||||
|
||||
@@ -16,14 +16,15 @@ mod planar;
|
||||
mod trap;
|
||||
|
||||
pub use boolean::{
|
||||
BoolOp, DEFAULT_PRECISION, area, boolean, boolean_n, normalize, try_area, try_boolean, try_boolean_n, try_normalize, try_unite_all, unite_all,
|
||||
BoolOp, DEFAULT_PRECISION, area, boolean, boolean_n, normalize, try_area, try_boolean, try_boolean_n, try_normalize, try_unite_all,
|
||||
try_unite_all_with, unite_all, unite_all_with,
|
||||
};
|
||||
pub use edit::{
|
||||
AverageAxis, SIMPLIFY_LEVEL, SimplifiedPart, SimplifyOptions, add_anchor_points, auto_simplify_level, average, join, remove_anchor,
|
||||
remove_redundant_points, simplify, simplify_level_tolerance, simplify_part, simplify_with, smooth, split_into_grid,
|
||||
};
|
||||
pub use offset::{Cap, Join, offset_path, outline_stroke, stroke_region};
|
||||
pub use pathfinder::{FaceMerger, PathfinderOp, Region, Shape, merge_regions, pathfinder, region_at, regions};
|
||||
pub use pathfinder::{FaceMerger, PathfinderOp, Region, Shape, merge_regions, pathfinder, pathfinder_with, region_at, regions};
|
||||
pub use planar::{
|
||||
BuilderArrangement, SHAPE_BUILDER_MAX_GAP, SHAPE_BUILDER_MAX_SEGMENTS, cut_out, encloses_area, find_gaps, has_open_path, interior_point,
|
||||
live_paint, live_paint_with_gaps, run_around, shape_builder, shape_builder_with_gap,
|
||||
|
||||
@@ -8,7 +8,8 @@ use linesweeper::topology::ContourIdx;
|
||||
use vectorcraft_geom::{FillRule, PathData};
|
||||
|
||||
use crate::boolean::{
|
||||
Arrangement, Multi, Seg, Tidy, all_contours_to_path, contours_to_path, eps_for, fill_bezpath, normalize_bez, segs_to_subpath, to_seg, unite_all,
|
||||
Arrangement, DEFAULT_PRECISION, Multi, Seg, Tidy, all_contours_to_path, contours_to_path, eps_for, fill_bezpath, normalize_bez, segs_to_subpath,
|
||||
to_seg, unite_all, unite_all_with,
|
||||
};
|
||||
|
||||
/// A filled shape in a Pathfinder stack. `key` identifies its paint (e.g. a hashed fill colour);
|
||||
@@ -78,9 +79,12 @@ impl Region {
|
||||
}
|
||||
}
|
||||
|
||||
fn arrangement(shapes: &[Shape]) -> Option<Arrangement> {
|
||||
/// The arrangement of `shapes`, its faces refit to `precision` and its slivers thinner than it dropped.
|
||||
fn arrangement(shapes: &[Shape], precision: f64) -> Option<Arrangement> {
|
||||
let bps: Vec<(BezPath, FillRule)> = shapes.iter().map(|s| (fill_bezpath(&s.path), s.rule)).collect();
|
||||
Arrangement::new(bps).ok()
|
||||
let mut arr = Arrangement::new(bps).ok()?;
|
||||
arr.tidy.precision = precision;
|
||||
Some(arr)
|
||||
}
|
||||
|
||||
/// The one item of `it`, if it has exactly one.
|
||||
@@ -95,6 +99,14 @@ fn one(path: PathData, key: u64) -> Vec<Shape> {
|
||||
|
||||
/// Run a Pathfinder operation over `shapes` (back → front).
|
||||
pub fn pathfinder(op: PathfinderOp, shapes: &[Shape]) -> Vec<Shape> {
|
||||
pathfinder_with(op, shapes, DEFAULT_PRECISION)
|
||||
}
|
||||
|
||||
/// [`pathfinder`] at Pathfinder Options › Precision: results are refit to within `precision`
|
||||
/// (document units) and pieces thinner than it are dropped. A smaller value follows the inputs more
|
||||
/// closely. A value that isn't positive and finite is [`DEFAULT_PRECISION`].
|
||||
pub fn pathfinder_with(op: PathfinderOp, shapes: &[Shape], precision: f64) -> Vec<Shape> {
|
||||
let precision = if precision.is_finite() && precision > 0.0 { precision } else { DEFAULT_PRECISION };
|
||||
let n = shapes.len();
|
||||
if n == 0 {
|
||||
return Vec::new();
|
||||
@@ -102,7 +114,7 @@ pub fn pathfinder(op: PathfinderOp, shapes: &[Shape]) -> Vec<Shape> {
|
||||
let front_key = shapes[n - 1].key;
|
||||
let unite = || {
|
||||
let v: Vec<(&PathData, FillRule)> = shapes.iter().map(|s| (&s.path, s.rule)).collect();
|
||||
one(unite_all(&v), front_key)
|
||||
one(unite_all_with(&v, precision), front_key)
|
||||
};
|
||||
// Unite doesn't need the arrangement.
|
||||
if op == PathfinderOp::Unite {
|
||||
@@ -111,7 +123,7 @@ pub fn pathfinder(op: PathfinderOp, shapes: &[Shape]) -> Vec<Shape> {
|
||||
// Open paths are cutting edges for Divide; the filled-only operations keep their
|
||||
// implicit closure. Reuse Shape Builder's bounded planar map and its fallback.
|
||||
if op == PathfinderOp::Divide && crate::has_open_path(shapes) {
|
||||
return crate::shape_builder(shapes, false)
|
||||
return crate::planar::builder(shapes, false, 0.0, precision)
|
||||
.regions
|
||||
.into_iter()
|
||||
.filter_map(|r| {
|
||||
@@ -120,7 +132,7 @@ pub fn pathfinder(op: PathfinderOp, shapes: &[Shape]) -> Vec<Shape> {
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let Some(arr) = arrangement(shapes) else { return Vec::new() };
|
||||
let Some(arr) = arrangement(shapes, precision) else { return Vec::new() };
|
||||
let p = &arr.tidy;
|
||||
match op {
|
||||
PathfinderOp::Unite => unite(),
|
||||
@@ -307,7 +319,12 @@ fn vertex_key(p: Point) -> (u64, u64) {
|
||||
/// All faces of the planar arrangement of `shapes` (every area covered by at least one shape,
|
||||
/// split wherever coverage changes).
|
||||
pub fn regions(shapes: &[Shape]) -> Vec<Region> {
|
||||
arrangement(shapes).map(|a| regions_of(&a)).unwrap_or_default()
|
||||
regions_with(shapes, DEFAULT_PRECISION)
|
||||
}
|
||||
|
||||
/// [`regions`] refit to `precision`.
|
||||
pub(crate) fn regions_with(shapes: &[Shape], precision: f64) -> Vec<Region> {
|
||||
arrangement(shapes, precision).map(|a| regions_of(&a)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The face under `point`, if any (Shape Builder hover/click).
|
||||
@@ -348,7 +365,12 @@ impl FaceMerger {
|
||||
// The Shape Builder's faces come from the planar map only up to its cap.
|
||||
let segments: usize = shapes.iter().flat_map(|s| &s.path.subpaths).map(|sp| sp.segment_count()).sum();
|
||||
let lines = (segments <= crate::SHAPE_BUILDER_MAX_SEGMENTS).then(|| (shapes.to_vec(), std::cell::OnceCell::new()));
|
||||
Self { arr: if has_open { None } else { arrangement(shapes) }, shapes: shapes.len(), lines, snap: std::cell::OnceCell::new() }
|
||||
Self {
|
||||
arr: if has_open { None } else { arrangement(shapes, DEFAULT_PRECISION) },
|
||||
shapes: shapes.len(),
|
||||
lines,
|
||||
snap: std::cell::OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge faces found with Gap Detection using the same temporary bridges as Live Paint.
|
||||
@@ -851,7 +873,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn same_as_one_mask_at_a_time(shapes: &[Shape]) {
|
||||
let arr = arrangement(shapes).expect("arrangement");
|
||||
let arr = arrangement(shapes, DEFAULT_PRECISION).expect("arrangement");
|
||||
assert_eq!(regions_of(&arr), regions_one_mask_at_a_time(&arr));
|
||||
}
|
||||
|
||||
@@ -859,6 +881,54 @@ mod tests {
|
||||
shapes::rectangle(Rect::new(x0, y0, x1, y1))
|
||||
}
|
||||
|
||||
/// Pathfinder Options › Precision: pieces thinner than the precision are dropped, so a 0.5 pt
|
||||
/// strip that the default keeps goes at a precision of 1 pt, whether the Pathfinder works on
|
||||
/// the arrangement (Divide), the planar map (Divide with an open path) or the union (Unite).
|
||||
#[test]
|
||||
fn precision_drops_pieces_thinner_than_it() {
|
||||
let overlapping =
|
||||
[Shape::new(rect(0.0, 0.0, 100.0, 100.0), FillRule::NonZero, 1), Shape::new(rect(0.0, 99.5, 100.0, 200.0), FillRule::NonZero, 2)];
|
||||
let line = PathData::single(SubPath::polyline(&[Point::new(-10.0, 99.5), Point::new(110.0, 99.5)], false));
|
||||
let cut = [Shape::new(rect(0.0, 0.0, 100.0, 100.0), FillRule::NonZero, 1), Shape::new(line, FillRule::NonZero, 2)];
|
||||
let apart =
|
||||
[Shape::new(rect(0.0, 0.0, 100.0, 100.0), FillRule::NonZero, 1), Shape::new(rect(0.0, 150.0, 100.0, 150.5), FillRule::NonZero, 2)];
|
||||
for (what, op, shapes, kept) in [
|
||||
("Divide", PathfinderOp::Divide, &overlapping, 3),
|
||||
("Divide with an open path", PathfinderOp::Divide, &cut, 2),
|
||||
("Unite", PathfinderOp::Unite, &apart, 2),
|
||||
] {
|
||||
let count = |r: Vec<Shape>| r.iter().map(|s| s.path.subpaths.len()).sum::<usize>();
|
||||
assert_eq!(pathfinder_with(op, shapes, DEFAULT_PRECISION), pathfinder(op, shapes), "{what}: the default is the plain Pathfinder");
|
||||
assert_eq!(count(pathfinder(op, shapes)), kept, "{what}: the default keeps the strip");
|
||||
assert_eq!(count(pathfinder_with(op, shapes, 1.0)), kept - 1, "{what}: 1 pt drops it");
|
||||
assert_eq!(pathfinder_with(op, shapes, f64::NAN), pathfinder(op, shapes), "{what}: an invalid precision is the default");
|
||||
}
|
||||
}
|
||||
|
||||
/// The planar map that Divide with an open path uses merges two edges between the same points
|
||||
/// only when they are closer than the precision: a lens 0.005 pt thick between two curves is a
|
||||
/// piece at a precision of 0.001 pt, and not at the default.
|
||||
#[test]
|
||||
fn open_path_divide_keeps_a_lens_thicker_than_the_precision() {
|
||||
// Curves monotonic in x and y, so the sweep keeps each one whole between the shared ends;
|
||||
// their middles are 0.005 pt apart.
|
||||
let shift = 0.005 / 0.75 / std::f64::consts::SQRT_2;
|
||||
let curve = |d: f64| [Point::new(0.0, 0.0), Point::new(d, 50.0 - d), Point::new(50.0 + d, 100.0 - d), Point::new(100.0, 100.0)];
|
||||
let (top, bottom) = (curve(0.0), curve(shift));
|
||||
let mut bp = kurbo::BezPath::new();
|
||||
bp.move_to(top[0]);
|
||||
bp.curve_to(top[1], top[2], top[3]);
|
||||
bp.curve_to(bottom[2], bottom[1], bottom[0]);
|
||||
bp.close_path();
|
||||
let lens = Shape::new(PathData::from_bezpath(&bp), FillRule::NonZero, 1);
|
||||
let line = PathData::single(SubPath::polyline(&[Point::new(200.0, 0.0), Point::new(300.0, 0.0)], false));
|
||||
let shapes = [lens, Shape::new(line, FillRule::NonZero, 2)];
|
||||
let area = |r: &[Shape]| r.iter().map(|s| crate::area(&s.path, FillRule::NonZero)).sum::<f64>();
|
||||
let fine = pathfinder_with(PathfinderOp::Divide, &shapes, 0.001);
|
||||
assert!(area(&fine) > 0.1, "the lens is a piece: {} pt²", area(&fine));
|
||||
assert_eq!(area(&pathfinder(PathfinderOp::Divide, &shapes)), 0.0, "thinner than the default precision");
|
||||
}
|
||||
|
||||
/// Shapes on a coarse grid, so that edges and corners coincide: rectangles, rectangles with
|
||||
/// a hole, ellipses and polygons that may cross themselves, under either fill rule.
|
||||
fn arb_shape() -> impl Strategy<Value = Shape> {
|
||||
|
||||
@@ -465,7 +465,7 @@ fn find(parent: &mut [usize], mut v: usize) -> usize {
|
||||
/// wherever they meet another path, keyed by the input they came from (the front-most where
|
||||
/// inputs share one). Empty when the paths can't be swept (non-finite or too degenerate).
|
||||
pub fn live_paint(shapes: &[Shape]) -> (Vec<Region>, Vec<Shape>) {
|
||||
build(shapes).unwrap_or_default()
|
||||
build(shapes, DEFAULT_PRECISION).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// [`live_paint`] with Gap Detection: the faces are those of `shapes` with a line across each gap
|
||||
@@ -809,22 +809,27 @@ pub(crate) fn gap_inputs(shapes: &[Shape], gap: f64) -> Vec<Shape> {
|
||||
/// found. A `gap` of 0 is [`shape_builder`]. The bridges are in no region's sources and in no piece
|
||||
/// of `lines` or `edges`.
|
||||
pub fn shape_builder_with_gap(shapes: &[Shape], edges: bool, gap: f64) -> BuilderArrangement {
|
||||
builder(shapes, edges, gap, DEFAULT_PRECISION)
|
||||
}
|
||||
|
||||
/// [`shape_builder_with_gap`] refitting its results to `precision` (Pathfinder Options › Precision).
|
||||
pub(crate) fn builder(shapes: &[Shape], edges: bool, gap: f64, precision: f64) -> BuilderArrangement {
|
||||
let has_open = has_open_path(shapes);
|
||||
let segments: usize = shapes.iter().flat_map(|s| &s.path.subpaths).map(|sp| sp.segment_count()).sum();
|
||||
let planar = (segments <= SHAPE_BUILDER_MAX_SEGMENTS)
|
||||
.then(|| {
|
||||
let keyed: Vec<Shape> = shapes.iter().enumerate().map(|(i, s)| Shape::new(s.path.clone(), s.rule, i as u64)).collect();
|
||||
if has_open && gap.is_finite() && gap > 0.0 {
|
||||
let (faces, pieces) = live_paint(&gap_inputs(&keyed, gap));
|
||||
let (faces, pieces) = build(&gap_inputs(&keyed, gap), precision).unwrap_or_default();
|
||||
// Gap targets split original outlines too, but the bridges themselves aren't art.
|
||||
(faces, pieces.into_iter().filter(|p| p.key < shapes.len() as u64).collect())
|
||||
} else {
|
||||
live_paint(&keyed)
|
||||
build(&keyed, precision).unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.filter(|(faces, pieces)| !faces.is_empty() || !pieces.is_empty());
|
||||
let Some((faces, pieces)) = planar else {
|
||||
return BuilderArrangement { regions: filled_regions(shapes), ..Default::default() };
|
||||
return BuilderArrangement { regions: filled_regions(shapes, precision), ..Default::default() };
|
||||
};
|
||||
let is_line = |i: u64| shapes.get(i as usize).is_some_and(|s| !s.path.subpaths.iter().any(|sp| sp.closed));
|
||||
let (lines, outline): (Vec<Shape>, Vec<Shape>) = pieces.into_iter().partition(|e| is_line(e.key));
|
||||
@@ -832,7 +837,7 @@ pub fn shape_builder_with_gap(shapes: &[Shape], edges: bool, gap: f64) -> Builde
|
||||
faces
|
||||
} else {
|
||||
// The filled areas from their arrangement, whose faces merge exactly, then the gaps.
|
||||
let mut regions = crate::regions(shapes);
|
||||
let mut regions = crate::pathfinder::regions_with(shapes, precision);
|
||||
regions.extend(faces.into_iter().filter(|f| f.sources.is_empty()));
|
||||
regions
|
||||
};
|
||||
@@ -842,14 +847,14 @@ pub fn shape_builder_with_gap(shapes: &[Shape], edges: bool, gap: f64) -> Builde
|
||||
/// The regions of the shapes' filled areas. A shape that fills nothing (open paths that enclose no
|
||||
/// area, such as cutting lines) is left out, so a flood of them cannot push the arrangement past its
|
||||
/// size budget and take the filled shapes down with it; the regions keep the original indices.
|
||||
fn filled_regions(shapes: &[Shape]) -> Vec<crate::Region> {
|
||||
fn filled_regions(shapes: &[Shape], precision: f64) -> Vec<crate::Region> {
|
||||
let fills = |s: &Shape| s.path.subpaths.iter().any(|sp| sp.closed || encloses_area(sp));
|
||||
let keep: Vec<usize> = (0..shapes.len()).filter(|&i| shapes.get(i).is_some_and(fills)).collect();
|
||||
if keep.len() == shapes.len() {
|
||||
return crate::regions(shapes);
|
||||
return crate::pathfinder::regions_with(shapes, precision);
|
||||
}
|
||||
let kept: Vec<Shape> = keep.iter().filter_map(|&i| shapes.get(i).cloned()).collect();
|
||||
crate::regions(&kept)
|
||||
crate::pathfinder::regions_with(&kept, precision)
|
||||
.into_iter()
|
||||
.map(|mut r| {
|
||||
r.sources = r.sources.iter().filter_map(|&s| keep.get(s).copied()).collect();
|
||||
@@ -1080,6 +1085,11 @@ pub(crate) struct 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> {
|
||||
map_with(shapes, cover, DEFAULT_PRECISION)
|
||||
}
|
||||
|
||||
/// [`map`] whose faces and pieces are refit to `precision`, and whose slivers thinner than it are dropped.
|
||||
fn map_with(shapes: &[Shape], cover: bool, precision: f64) -> Option<Map> {
|
||||
let mut inputs: Vec<(BezPath, usize)> = Vec::new();
|
||||
for (i, s) in shapes.iter().enumerate() {
|
||||
let mut bp = BezPath::new();
|
||||
@@ -1110,7 +1120,7 @@ pub(crate) fn map(shapes: &[Shape], cover: bool) -> Option<Map> {
|
||||
let sweep =
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| Topology::<Crossings>::from_paths(inputs.iter().map(|(p, i)| (p, *i)), eps)));
|
||||
let top = sweep.ok()?.ok()?;
|
||||
let tidy = Tidy::keeping(DEFAULT_PRECISION, inputs.iter().map(|p| &p.0));
|
||||
let tidy = Tidy::keeping(precision, inputs.iter().map(|p| &p.0));
|
||||
|
||||
// The planar graph: vertices are the sweep's points, edges its segments. Points closer than
|
||||
// the sweep's tolerance are one vertex (it can put a corner that lies on another path's edge
|
||||
@@ -1229,7 +1239,7 @@ pub(crate) fn map(shapes: &[Shape], cover: bool) -> Option<Map> {
|
||||
let twin_of = between.get(&key).and_then(|v| {
|
||||
v.iter().copied().find(|&e| {
|
||||
let other = edges.get(e).and_then(|e| mid(&e.segs));
|
||||
matches!((here, other), (Some(a), Some(b)) if a.distance(b) < DEFAULT_PRECISION)
|
||||
matches!((here, other), (Some(a), Some(b)) if a.distance(b) < precision)
|
||||
})
|
||||
});
|
||||
if let Some(e) = twin_of {
|
||||
@@ -1448,8 +1458,8 @@ impl Map {
|
||||
}
|
||||
}
|
||||
|
||||
fn build(shapes: &[Shape]) -> Option<(Vec<Region>, Vec<Shape>)> {
|
||||
let m = map(shapes, false)?;
|
||||
fn build(shapes: &[Shape], precision: f64) -> Option<(Vec<Region>, Vec<Shape>)> {
|
||||
let m = map_with(shapes, false, precision)?;
|
||||
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();
|
||||
@@ -1465,7 +1475,11 @@ fn build(shapes: &[Shape]) -> Option<(Vec<Region>, Vec<Shape>)> {
|
||||
subpaths.extend(hs.iter().filter_map(|&k| holes.get(k)).filter_map(|(c, _)| subpath(c)));
|
||||
let path = unshear(PathData::new(subpaths), anchors, snap);
|
||||
let sources =
|
||||
interior_point(&path).map(|p| fills.iter().enumerate().filter(|(_, f)| f.contains(p)).map(|(i, _)| i).collect()).unwrap_or_default();
|
||||
// A face finer than the default precision can need a finer outline to find a point in.
|
||||
interior_point(&path)
|
||||
.or_else(|| (precision < DEFAULT_PRECISION).then(|| interior_point_within(&path, precision / 4.0)).flatten())
|
||||
.map(|p| fills.iter().enumerate().filter(|(_, f)| f.contains(p)).map(|(i, _)| i).collect())
|
||||
.unwrap_or_default();
|
||||
Some(Region { path, sources })
|
||||
})
|
||||
.collect();
|
||||
@@ -1530,11 +1544,17 @@ fn build(shapes: &[Shape]) -> Option<(Vec<Region>, Vec<Shape>)> {
|
||||
/// A point strictly inside a filled path, well away from its edges where it can be (for re-finding
|
||||
/// a Live Paint face after a rebuild).
|
||||
pub fn interior_point(path: &PathData) -> Option<Point> {
|
||||
let bb = path.bounds()?;
|
||||
interior_point_within(path, (bb.width().max(bb.height()) * 1e-3).max(1e-4))
|
||||
}
|
||||
|
||||
/// [`interior_point`] with the path's curves flattened to within `tolerance`.
|
||||
fn interior_point_within(path: &PathData, tolerance: f64) -> Option<Point> {
|
||||
let bb = path.bounds()?;
|
||||
let bp = path.to_bezpath();
|
||||
let mut edges: Vec<(Point, Point)> = vec![];
|
||||
let (mut first, mut last) = (Point::ZERO, Point::ZERO);
|
||||
kurbo::flatten(bp.iter(), (bb.width().max(bb.height()) * 1e-3).max(1e-4), |el| match el {
|
||||
kurbo::flatten(bp.iter(), tolerance, |el| match el {
|
||||
kurbo::PathEl::MoveTo(p) => {
|
||||
if last != first {
|
||||
edges.push((last, first));
|
||||
|
||||
Reference in New Issue
Block a user