//! Live Paint planar maps: the faces and edges a set of paths divides the plane into. //! //! Unlike Pathfinder [`regions`](crate::regions), which splits the *filled areas* of closed shapes, //! every path here is an edge, open ones included: three crossing lines enclose a triangle, a line //! across a rectangle splits it in two, and an area enclosed by paths is a face whether or not //! anything fills it. //! //! The sweep (`linesweeper`) only takes closed paths, so an open path goes in traced there and //! back. Its two passes would cancel out as winding numbers, so the sweep counts boundary crossings //! instead ([`Crossings`]), which never cancel. Faces are then walked on the planar graph of the //! sweep's output segments; which inputs an edge came from and which fills cover a face are found //! geometrically (the sweep's winding numbers can't be trusted once open paths and coincident //! edges are in). //! //! The sweep runs in a sheared frame ([`SHEAR`]): it merges the two passes of a *horizontal* open //! path as if they cancelled out, and in the sheared frame no edge drawn at a common angle (0°, //! 45°, 90°…) is horizontal. The shear is by a power of two, so it maps typical coordinates //! exactly and keeps exact coincidences (a corner on another shape's edge) exact. use std::collections::HashMap; use kurbo::{Affine, BezPath, CubicBez, ParamCurve, ParamCurveArclen, PathSeg, Point, Shape as _}; use linesweeper::topology::{Topology, WindingNumber}; use vectorcraft_geom::{FillRule, PathData, SubPath}; use crate::boolean::{DEFAULT_PRECISION, Seg, Tidy, eps_for, is_sliver, segs_to_subpath, snap_horizontals, tidy_segments, to_seg}; use crate::pathfinder::{Region, Shape}; /// The sweep's frame: y + x / 32. Only edges sloping -1/32 in the document are horizontal in it. const SHEAR: Affine = Affine::new([1.0, 1.0 / 32.0, 0.0, 1.0, 0.0, 0.0]); const UNSHEAR: Affine = Affine::new([1.0, -1.0 / 32.0, 0.0, 1.0, 0.0, 0.0]); /// Boundary crossings per input, counted from the far west: slot `2i` counts input `i`'s /// segments running one way, slot `2i + 1` the other. An open path traced there and back adds one /// to each, so its edges never cancel out of the map. Only the slots that aren't 0 are kept, in /// order: the sweep keeps one of these for every edge, and a vector as long as the input's number /// (as this was) made disjoint shapes cost the square of their count in memory. #[derive(Clone, Debug, Default, PartialEq, Eq)] struct Crossings(Vec<(usize, u32)>); impl Crossings { fn get(&self, slot: usize) -> u32 { self.0.binary_search_by_key(&slot, |(s, _)| *s).map_or(0, |i| self.0.get(i).map_or(0, |(_, c)| *c)) } } impl std::ops::Add for Crossings { type Output = Crossings; fn add(mut self, rhs: Self) -> Crossings { self += rhs; self } } impl std::ops::AddAssign for Crossings { fn add_assign(&mut self, rhs: Self) { if rhs.0.is_empty() { return; } let mut out = Vec::with_capacity(self.0.len() + rhs.0.len()); let (mut a, mut b) = (std::mem::take(&mut self.0).into_iter().peekable(), rhs.0.into_iter().peekable()); loop { let next = match (a.peek(), b.peek()) { (Some(x), Some(y)) if x.0 == y.0 => { let (x, y) = (a.next(), b.next()); x.zip(y).map(|(x, y)| (x.0, x.1.saturating_add(y.1))) } (Some(x), Some(y)) if x.0 < y.0 => a.next(), (Some(_), Some(_)) | (None, Some(_)) => b.next(), (Some(_), None) => a.next(), (None, None) => break, }; out.extend(next.filter(|(_, c)| *c != 0)); } self.0 = out; } } impl WindingNumber for Crossings { type Tag = usize; fn single(tag: usize, positive: bool) -> Self { Crossings(vec![(2 * tag + usize::from(!positive), 1)]) } fn of_tag(&self, tag: usize) -> Self { Crossings([2 * tag, 2 * tag + 1].into_iter().map(|slot| (slot, self.get(slot))).filter(|(_, c)| *c != 0).collect()) } } /// A point in the middle of a run of segments. fn mid(segs: &[Seg]) -> Option { segs.get(segs.len() / 2).map(|s| s.c.eval(0.5)) } /// Rectangles looked up by a point inside them, when there are too many to test one by one for /// every point (the map asks once per edge and once per hole, so a scan of them all made it /// quadratic in the input, #937). A grid of cells over their bounds, each cell listing the /// rectangles touching it; a lookup reads the cells around the point. The grid has about one cell /// per rectangle, so a lookup reads about the rectangles of the point's neighbourhood. A rectangle /// covering too many cells (a long diagonal's bounds take in most of the art, so an index of the /// bounds alone scanned a diagonal hatch whole for every point) is filed under the cells of a /// smaller footprint instead ([`Self::with_footprint`]), or, failing that, scanned for every point. struct RectIndex { items: Vec<(kurbo::Rect, T)>, /// The grid's top-left corner, cell size and column count; `cells` lists the items touching /// each cell, row by row. origin: Point, cell: (f64, f64), cols: usize, cells: Vec>, /// Items the grid doesn't hold (bounds covering most of it, or not finite), scanned for /// every point. rest: Vec, } /// The fewest cells one footprint rectangle may always be filed under. The cap is this or two /// rows or columns of the grid, whichever is more: a thin stripe across the art (one of the faces /// a hatch cuts a shape into) is filed under the cells along it, while a rectangle covering most /// of the grid costs more to file and to read than to scan for every point. const CELLS_PER_FOOTPRINT: usize = 64; /// The cell references a grid of `cells` cells holds for `n` items before the remaining items go /// to the scanned list: a few per item, plus a few rows or columns' worth (a long segment is filed /// under a cell of each row or column it crosses, about `sqrt(cells)` of them in a square grid). /// Coincident segments make a grid of one column and a row per segment, where each would be filed /// under every row: a quadratic number of references, which this keeps to `n·sqrt(n)`. fn reference_budget(n: usize, cells: usize) -> usize { let across = (cells as f64).sqrt().ceil().min(1e9) as usize; n.saturating_mul(64 + 4 * across).saturating_add(1024) } /// The cells (columns or rows) an interval `lo..=hi` touches on one axis of a grid: clamped to the /// grid, so an interval past its edge gets the nearest cell (whose items `contains` then rejects), /// and `None` for an interval that isn't a number. fn cell_span(lo: f64, hi: f64, origin: f64, size: f64, count: usize) -> Option> { let last = count.saturating_sub(1) as f64; let a = ((lo - origin) / size).floor().clamp(0.0, last); let b = ((hi - origin) / size).floor().clamp(0.0, last); (a.is_finite() && b.is_finite()).then(|| (a as usize)..=(b.max(a) as usize)) } impl RectIndex { fn new(items: Vec<(kurbo::Rect, T)>) -> Self { Self::with_footprint(items, |r, _, _| vec![*r]) } /// `footprint(bounds, item, cell side)` gives the rectangles an item is filed under: a lookup /// finds the item when its bounds hold the point and one footprint rectangle is within the /// lookup's tolerance of it. The bounds themselves (`new`) suit items as compact as their /// bounds; a long diagonal segment is better filed under the bounds of its pieces, about a /// cell long each, which take in the cells along it rather than every cell its bounds cover. fn with_footprint(items: Vec<(kurbo::Rect, T)>, footprint: impl Fn(&kurbo::Rect, &T, f64) -> Vec) -> Self { let n = items.len().max(1); let all = items.iter().map(|(r, _)| *r).filter(kurbo::Rect::is_finite).reduce(|a, b| a.union(b)).unwrap_or(kurbo::Rect::ZERO); // A cell's side holds one rectangle on average; a grid of collinear rectangles (zero area) // is divided along its length. let side = (all.area() / n as f64).sqrt(); let side = if side > 0.0 { side } else { all.width().max(all.height()) / n as f64 }.max(1e-9); // Saturating casts: an extent of a billion cells is clamped to the item count, then the // grid is kept to a few cells per item however lopsided its bounds. let mut cols = ((all.width() / side).ceil() as usize).clamp(1, 2 * n); let mut rows = ((all.height() / side).ceil() as usize).clamp(1, 2 * n); while cols * rows > 4 * n + 4 { if cols > rows { cols = cols.div_ceil(2) } else { rows = rows.div_ceil(2) } } let cell = ((all.width() / cols as f64).max(1e-9), (all.height() / rows as f64).max(1e-9)); let cap = CELLS_PER_FOOTPRINT.max(2 * cols.max(rows)); let mut budget = reference_budget(n, cols * rows); let mut cells: Vec> = vec![Vec::new(); cols * rows]; let mut rest = Vec::new(); for (i, (r, t)) in items.iter().enumerate() { // Cells hold `u32` indices to halve the memory of a long stripe filed under a row of them. let Ok(index) = u32::try_from(i) else { rest.push(i); continue; }; let mut scan = !r.is_finite(); let mut spans = Vec::new(); for f in if scan { Vec::new() } else { footprint(r, t, cell.0.min(cell.1)) } { match (cell_span(f.x0, f.x1, all.x0, cell.0, cols), cell_span(f.y0, f.y1, all.y0, cell.1, rows)) { (Some(xs), Some(ys)) if (xs.end() - xs.start() + 1) * (ys.end() - ys.start() + 1) <= cap => spans.push((xs, ys)), _ => scan = true, } } // The item's cells, counted before they are filed: past the grid's budget the item, // and the ones after it, are scanned instead (the index stays bounded in memory). // Pieces of one segment that fall in the same cell (a degenerate grid's tiny cells) // count and file it once: the pieces come in order, so the repeats are consecutive. let mut mine: Vec = if scan { Vec::new() } else { spans.iter().flat_map(|(xs, ys)| ys.clone().flat_map(|y| xs.clone().map(move |x| y * cols + x))).collect() }; mine.dedup(); if !scan && mine.len() > budget { scan = true; } if scan { rest.push(i); continue; } budget -= mine.len(); for c in mine { if let Some(c) = cells.get_mut(c) && c.last() != Some(&index) { c.push(index); } } } Self { items, origin: all.origin(), cell, cols, cells, rest } } /// The items whose rectangle, grown by `tol`, holds `p` (and, with a footprint, one of whose /// footprint rectangles is within `tol` of it). fn at(&self, p: Point, tol: f64) -> impl Iterator { let mut found = self.rest.clone(); let rows = self.cells.len() / self.cols.max(1); if let (Some(xs), Some(ys)) = ( cell_span(p.x - tol, p.x + tol, self.origin.x, self.cell.0, self.cols), cell_span(p.y - tol, p.y + tol, self.origin.y, self.cell.1, rows), ) { for y in ys { for x in xs.clone() { found.extend(self.cells.get(y * self.cols + x).into_iter().flatten().map(|&i| i as usize)); } } } found.sort_unstable(); found.dedup(); found.into_iter().filter_map(move |i| self.items.get(i)).filter(move |(r, _)| r.inflate(tol, tol).contains(p)).map(|(_, t)| t) } } /// A segment's footprint in a [`RectIndex`]: the bounds of its pieces about a cell long, so a long /// diagonal is filed under the cells along it, not under every cell its bounds cover. fn segment_footprint(bounds: &kurbo::Rect, seg: &PathSeg, cell: f64) -> Vec { let pieces = (bounds.width().max(bounds.height()) / cell).ceil(); // A count past the cap gives longer pieces, each filed under more cells. if pieces.is_nan() || pieces <= 1.0 { return vec![*bounds]; } let pieces = pieces.min(4096.0) as usize; (0..pieces).map(|k| seg.subsegment(k as f64 / pieces as f64..(k + 1) as f64 / pieces as f64).bounding_box()).collect() } /// The inputs' segments (in the sweep's frame), to tell which inputs an edge lies on. struct Outlines { segs: RectIndex<(PathSeg, usize)>, tol: f64, } impl Outlines { fn new(inputs: &[(BezPath, usize)], eps: f64) -> Self { // A zero-length segment (a point) is the source of no edge, even one running through it. let segs = inputs .iter() .flat_map(|(bp, i)| bp.segments().map(move |s| (s.bounding_box(), (s, *i)))) .filter(|(r, ..)| r.width() > 0.0 || r.height() > 0.0) .collect(); // The sweep moves points by a few `eps` at most. Self { segs: RectIndex::with_footprint(segs, |r, (s, _), cell| segment_footprint(r, s, cell)), tol: 16.0 * eps } } /// The front-most input whose outline passes through `p`. fn source_at(&self, p: Point) -> Option { use kurbo::ParamCurveNearest; let tol = self.tol; self.segs.at(p, tol).filter(|(s, _)| s.nearest(p, 1e-9).distance_sq <= tol * tol).map(|(_, i)| *i).max() } } /// The area an input's fill covers: its closed subpaths (open ones bound no face). struct Fill { path: BezPath, bounds: kurbo::Rect, rule: FillRule, } impl Fill { fn new(s: &Shape) -> Self { let mut path = BezPath::new(); for sp in s.path.subpaths.iter().filter(|sp| sp.closed && sp.anchors.len() >= 2) { sp.to_bezpath_into(&mut path); } Self { bounds: path.bounding_box(), path, rule: s.rule } } fn contains(&self, p: Point) -> bool { !self.path.elements().is_empty() && self.bounds.contains(p) && crate::boolean::inside(self.rule, self.path.winding(p)) } } /// One output segment of the sweep: an edge of the planar graph between two vertices. struct Edge { segs: Vec, ends: [usize; 2], /// The input it came from (the front-most, where inputs share it). source: usize, } /// `segs` (a chain) moved to start at `a` and end at `b`, the handles next to each moving with it. pub(crate) fn pin_ends(segs: &mut [Seg], a: Point, b: Point) { if let Some(s) = segs.first_mut() { let d = a - s.c.p0; s.c.p0 = a; s.c.p1 += d; } if let Some(s) = segs.last_mut() { let d = b - s.c.p3; s.c.p3 = b; s.c.p2 += d; } } /// Half-edge `2e` runs along edge `e`, `2e + 1` back along it. fn twin(h: usize) -> usize { h ^ 1 } /// The segments of half-edge `h`, in its direction. fn half_segs(edges: &[Edge], h: usize) -> Vec { half_seg_iter(edges, h).collect() } /// The segments of half-edge `h` in its direction, without copying the edge first (an edge can /// hold thousands of segments, and the angle it leaves a vertex at needs the first few). fn half_seg_iter(edges: &[Edge], h: usize) -> impl Iterator + '_ { let segs = edges.get(h / 2).map(|e| e.segs.as_slice()).unwrap_or_default(); let forward = h.is_multiple_of(2); (0..segs.len()).filter_map(move |k| { let s = segs.get(if forward { k } else { segs.len() - 1 - k })?; Some(if forward { *s } else { Seg { c: CubicBez::new(s.c.p3, s.c.p2, s.c.p1, s.c.p0), line: s.line } }) }) } /// Where half-edge `h` ends (the vertex at the other end of its edge). fn far_end(edges: &[Edge], h: usize) -> Option { let e = edges.get(h / 2)?; if h.is_multiple_of(2) { e.segs.last().map(|s| s.c.p3) } else { e.segs.first().map(|s| s.c.p0) } } /// The vertex half-edge `h` leaves from. fn origin(edges: &[Edge], h: usize) -> Option { edges.get(h / 2).map(|e| e.ends[h % 2]) } /// Where half-edge `h` (leaving `v`) first gets `r` away from it, as an angle around `v`. /// /// Edges that only meet at `v` cross a circle around it in the order they leave `v` in, however /// they start out (tangent curves, or the sweep's short straight stubs at a vertex). fn leaving_angle(edges: &[Edge], h: usize, v: Point, r: f64) -> f64 { let first = half_seg_iter(edges, h).next(); let mut prev = first.map_or(v, |s| s.c.p0); let mut hit: Option = None; // One segment at a time, stopping at the first that gets r away: an edge can run on for // thousands of segments (a traced outline between two crossings), all of which flattening // the whole edge to this tolerance would visit, at every vertex. for s in half_seg_iter(edges, h) { let path = [kurbo::PathEl::MoveTo(s.c.p0), kurbo::PathEl::CurveTo(s.c.p1, s.c.p2, s.c.p3)]; kurbo::flatten(path, (r * 1e-4).max(1e-12), |el| { if let (None, kurbo::PathEl::LineTo(p)) = (hit, el) { if p.distance(v) >= r { // The point on prev → p at distance r (prev is closer). let (d, f) = (p - prev, prev - v); let (a, b, c) = (d.hypot2(), 2.0 * f.dot(d), f.hypot2() - r * r); let t = if a > 0.0 { ((-b + (b * b - 4.0 * a * c).max(0.0).sqrt()) / (2.0 * a)).clamp(0.0, 1.0) } else { 1.0 }; hit = Some(prev + d * t); } prev = p; } }); if hit.is_some() { break; } } // An edge too short to reach r (it can't, r is at most half of every chord) leaves along its // tangent. let far = hit.unwrap_or_else(|| first.map_or(v, |s| [s.c.p1, s.c.p2, s.c.p3].into_iter().find(|p| p.distance(s.c.p0) > 0.0).unwrap_or(s.c.p3))); (far - v).atan2() } /// Path of a closed run of half-edges, tidied (split pieces of one input curve joined again). fn cycle_bezpath(edges: &[Edge], cycle: &[usize]) -> BezPath { let mut bp = BezPath::new(); for (k, s) in cycle.iter().flat_map(|&h| half_segs(edges, h)).enumerate() { if k == 0 { bp.move_to(s.c.p0); } if s.line { bp.line_to(s.c.p3); } else { bp.curve_to(s.c.p1, s.c.p2, s.c.p3); } } bp.close_path(); bp } /// `cycle` without the spurs it runs out and back along (edges with the same face on both sides, /// such as a line's loose end). fn without_spurs(cycle: &[usize]) -> Vec { let mut out: Vec = Vec::with_capacity(cycle.len()); for &h in cycle { if out.last() == Some(&twin(h)) { out.pop(); } else { out.push(h); } } // A spur where the walk started wraps around the ends. while out.len() >= 2 && out.first().map(|&h| twin(h)) == out.last().copied() { out.pop(); out.remove(0); } out } /// Append `open` traced there and back (a closed, zero-area path along it) to `out`. fn there_and_back(open: &BezPath, out: &mut BezPath) { let segs: Vec = open.segments().collect(); let Some(first) = segs.first() else { return }; out.move_to(first.start()); for s in &segs { out.push(s.as_path_el()); } for s in segs.iter().rev() { out.push(s.reverse().as_path_el()); } out.close_path(); } /// `path` back from the sweep's frame, its anchors within `tol` of an input anchor (`sorted` by x) /// put exactly on it, handles moving with them. fn unshear(mut path: PathData, sorted: &[Point], tol: f64) -> PathData { path.transform(UNSHEAR); for a in path.subpaths.iter_mut().flat_map(|sp| sp.anchors.iter_mut()) { let lo = sorted.partition_point(|q| q.x < a.p.x - tol); if let Some(q) = sorted.get(lo..).unwrap_or_default().iter().take_while(|q| q.x <= a.p.x + tol).find(|q| (q.y - a.p.y).abs() <= tol) { let d = *q - a.p; a.p = *q; a.h_in += d; a.h_out += d; } } path } fn find(parent: &mut [usize], mut v: usize) -> usize { while let Some(&p) = parent.get(v) { if p == v { break; } let gp = parent.get(p).copied().unwrap_or(p); if let Some(slot) = parent.get_mut(v) { *slot = gp; } v = p; } v } /// The Live Paint planar map of `shapes` (back → front). /// /// Faces are the bounded areas the paths enclose, holes included, each with the inputs whose fill /// covers it (ascending; empty for an area only enclosed by paths). Edges are the paths split /// 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, Vec) { build(shapes, DEFAULT_PRECISION).unwrap_or_default() } /// [`live_paint`] with Gap Detection: the faces are those of `shapes` with a line across each gap /// [`find_gaps`] finds up to `max_gap`, so paint stops at the gaps, and the edges are those of /// `shapes` alone (the lines add none). Also returns the gaps. pub fn live_paint_with_gaps(shapes: &[Shape], max_gap: f64) -> (Vec, Vec, Vec<[Point; 2]>) { let gaps = find_gaps(shapes, max_gap); if gaps.is_empty() { let (faces, edges) = live_paint(shapes); return (faces, edges, gaps); } let mut closed = shapes.to_vec(); let lines = PathData::new(gaps.iter().map(|g| SubPath::polyline(g, false)).collect()); closed.push(Shape::new(lines, FillRule::NonZero, u64::MAX)); let (faces, _) = live_paint(&closed); let (_, edges) = live_paint(shapes); (faces, edges, gaps) } /// How many times longer than a gap the way between its two ends along the paths must be for it to /// count: a loose end that crosses a path just before it (an overshoot), or a path that curls back /// close to its own end (a hook), leaves no gap there. const GAP_DETOUR: f64 = 8.0; /// How exactly [`find_gaps`] measures lengths along the paths. const GAP_ARC_ACCURACY: f64 = 1e-4; /// How many points along a loose end's own segment [`find_gaps`] tries. const GAP_SAMPLES: u32 = 64; /// One subpath's segments for [`find_gaps`], with the length along it where each starts. struct Track { segs: Vec, /// The length at the start of each segment, then the whole length. at: Vec, closed: bool, bounds: kurbo::Rect, } impl Track { fn new(sp: &SubPath) -> Option { if sp.anchors.len() < 2 || !sp.anchors.iter().all(|a| a.p.is_finite() && a.h_in.is_finite() && a.h_out.is_finite()) { return None; } let mut bp = BezPath::new(); sp.to_bezpath_into(&mut bp); let segs: Vec = bp.segments().collect(); let mut at = Vec::with_capacity(segs.len() + 1); let mut len = 0.0; for s in &segs { at.push(len); len += s.arclen(GAP_ARC_ACCURACY); } at.push(len); (!segs.is_empty()).then(|| Self { bounds: bp.bounding_box(), segs, at, closed: sp.closed }) } fn len(&self) -> f64 { self.at.last().copied().unwrap_or(0.0) } /// The length along the subpath to parameter `t` of segment `k`. fn pos(&self, k: usize, t: f64) -> f64 { let Some(s) = self.segs.get(k) else { return self.len() }; self.at.get(k).copied().unwrap_or(0.0) + s.subsegment(0.0..t).arclen(GAP_ARC_ACCURACY) } /// The length along the subpath between two places on it (the shorter way round a closed one). fn along(&self, a: f64, b: f64) -> f64 { let d = (a - b).abs(); if self.closed { d.min(self.len() - d) } else { d } } } /// Do two boxes (lines' boxes included, which have no area) touch or overlap? fn boxes_meet(a: kurbo::Rect, b: kurbo::Rect) -> bool { a.x0 <= b.x1 && b.x0 <= a.x1 && a.y0 <= b.y1 && b.y0 <= a.y1 } /// Where track `o`, within `reach` along it of `from`, crosses the segments `near` of track `q`: /// (length along `o`, length along `q`) of each crossing. fn crossings(o: &Track, from: f64, reach: f64, q: &Track, near: impl Iterator) -> Vec<(f64, f64)> { let near: Vec<(usize, &PathSeg)> = near.filter_map(|j| Some((j, q.segs.get(j)?))).collect(); let mut out = vec![]; // `first` is the first segment that ends within reach. let first = o.at.partition_point(|&a| a < from - reach).saturating_sub(1); for (k, s) in o.segs.iter().enumerate().skip(first) { let (Some(&a0), Some(&a1)) = (o.at.get(k), o.at.get(k + 1)) else { continue }; if a0 > from + reach { break; } if a1 < from - reach || !boxes_meet(s.bounding_box(), q.bounds) { continue; } // A curve is tried as 16 chords. let n = if matches!(s, PathSeg::Line(_)) { 1 } else { 16 }; let mut prev = s.start(); for i in 1..=n { let (t0, t1) = (f64::from(i - 1) / f64::from(n), f64::from(i) / f64::from(n)); let p = s.eval(t1); let chord = kurbo::Line::new(prev, p); let cb = chord.bounding_box(); for &(j, qs) in &near { if !boxes_meet(qs.bounding_box(), cb) { continue; } for x in qs.intersect_line(chord) { out.push((o.pos(k, t0 + x.line_t * (t1 - t0)), q.pos(j, x.segment_t))); } } prev = p; } } out } /// Most grid cells one segment is listed in by [`SegGrid`]; a longer one is tried for every point. const GAP_GRID_SPAN: i64 = 1024; /// The farthest cell from the grid's origin, on each axis, that [`SegGrid`] numbers a point in. const GAP_GRID_LIMIT: f64 = 1_048_576.0; /// The segments of [`find_gaps`]' tracks by the cells of a square grid that their bounds cover. struct SegGrid { origin: Point, cell: f64, /// (track, segment) by cell. cells: HashMap<(i64, i64), Vec<(usize, usize)>>, /// Segments whose bounds cover more than [`GAP_GRID_SPAN`] cells, near every point. wide: Vec<(usize, usize)>, } impl SegGrid { fn new(tracks: &[Track], origin: Point, cell: f64) -> Self { let mut grid = Self { origin, cell, cells: HashMap::new(), wide: vec![] }; for (j, t) in tracks.iter().enumerate() { for (k, s) in t.segs.iter().enumerate() { let b = s.bounding_box(); let ((x0, y0), (x1, y1)) = (grid.cell_of(Point::new(b.x0, b.y0)), grid.cell_of(Point::new(b.x1, b.y1))); if (x1 - x0 + 1).saturating_mul(y1 - y0 + 1) > GAP_GRID_SPAN { grid.wide.push((j, k)); continue; } for x in x0..=x1 { for y in y0..=y1 { grid.cells.entry((x, y)).or_default().push((j, k)); } } } } grid } /// The cell `p` is in, counted from the origin (at most [`GAP_GRID_LIMIT`] cells away on each /// axis). fn cell_of(&self, p: Point) -> (i64, i64) { let at = |v: f64, o: f64| ((v - o) / self.cell).floor().clamp(-GAP_GRID_LIMIT, GAP_GRID_LIMIT) as i64; (at(p.x, self.origin.x), at(p.y, self.origin.y)) } /// The segments whose bounds come within `r` of `p` (and some farther ones), each once, in /// track order. fn near(&self, p: Point, r: f64) -> Vec<(usize, usize)> { let mut out = self.wide.clone(); let ((x0, y0), (x1, y1)) = (self.cell_of(Point::new(p.x - r, p.y - r)), self.cell_of(Point::new(p.x + r, p.y + r))); for x in x0..=x1 { for y in y0..=y1 { out.extend(self.cells.get(&(x, y)).into_iter().flatten()); } } out.sort_unstable(); out.dedup(); out } } /// The point a gap from the loose end `e` of track `own` (at `from` along it) closes to: the /// nearest point of any path within `max_gap` of it, leaving out points the paths already join to /// `e` along a way less than [`GAP_DETOUR`] times as long as the gap. None when there is no such /// point, or when `e` lies on a path (it is no loose end). fn gap_from(tracks: &[Track], grid: &SegGrid, own: usize, e: Point, from: f64, max_gap: f64, eps: f64) -> Option { use kurbo::ParamCurveNearest; let o = tracks.get(own)?; let reach = GAP_DETOUR * max_gap; let mut best: Option<(f64, Point)> = None; // `around` holds the segments a way of up to `reach` from `e` can cross, by track (grouped // once: filtering the whole list again for each track near `e` made clusters of parallel // paths cubic), and `crossed` the crossings with each track. let mut around: Option>> = None; let mut crossed: HashMap> = HashMap::new(); for (j, k) in grid.near(e, max_gap) { let Some(q) = tracks.get(j) else { continue }; let Some(s) = q.segs.get(k) else { continue }; let b = s.bounding_box(); if e.x < b.x0 - max_gap || e.x > b.x1 + max_gap || e.y < b.y0 - max_gap || e.y > b.y1 + max_gap { continue; } let near = s.nearest(e, 1e-9); let d = near.distance_sq.sqrt(); if d > max_gap { continue; } let at = q.pos(k, near.t); let way = if j == own { o.along(from, at) } else { let xs = crossed.entry(j).or_insert_with(|| { let by_track = around.get_or_insert_with(|| { let mut by_track: HashMap> = HashMap::new(); for (t, k) in grid.near(e, reach + max_gap) { by_track.entry(t).or_default().push(k); } by_track }); crossings(o, from, reach, q, by_track.get(&j).into_iter().flatten().copied()) }); xs.iter().map(|&(a, b)| o.along(from, a) + q.along(b, at)).fold(f64::INFINITY, f64::min) }; if d <= eps { // On another path, or on its own away from the end: the end is joined there. if j != own || way > 4.0 * eps.max(GAP_ARC_ACCURACY) { return None; } // The end's own segment: a curve that comes back round can still pass near it. for i in 0..=GAP_SAMPLES { let t = f64::from(i) / f64::from(GAP_SAMPLES); let p = s.eval(t); let d = p.distance(e); if d > eps && d <= max_gap && o.along(from, q.pos(k, t)) > GAP_DETOUR * d && best.is_none_or(|b| d < b.0) { best = Some((d, p)); } } continue; } if way > GAP_DETOUR * d && best.is_none_or(|b| d < b.0) { best = Some((d, s.eval(near.t))); } } best.map(|b| b.1) } /// The gaps Live Paint's Gap Detection closes in `shapes`: from each loose end of an open subpath /// (an end that lies on no path) to the nearest point of any path, its own included, at most /// `max_gap` away, unless the paths already join the two along a way less than [`GAP_DETOUR`] times /// the gap's length. Each gap is a pair of points: the loose end, then where it closes to. A gap /// between two loose ends that close to each other is listed once. pub fn find_gaps(shapes: &[Shape], max_gap: f64) -> Vec<[Point; 2]> { if !(max_gap.is_finite() && max_gap > 0.0) { return vec![]; } let tracks: Vec = shapes.iter().flat_map(|s| &s.path.subpaths).filter_map(Track::new).collect(); let eps = (max_gap * 1e-4).max(1e-6); // The grid cells are as wide as the gap, or 1/4096 of the art where that is wider. let bounds = tracks.iter().map(|t| t.bounds).reduce(|a, b| a.union(b)).unwrap_or_default(); let grid = SegGrid::new(&tracks, bounds.origin(), max_gap.max(bounds.width().max(bounds.height()) / 4096.0)); let mut gaps: Vec<[Point; 2]> = vec![]; // `by_end` lists the gaps by the cell of their loose end. let mut by_end: HashMap<(i64, i64), Vec> = HashMap::new(); for (i, o) in tracks.iter().enumerate() { let (Some(first), Some(last)) = (o.segs.first(), o.segs.last()) else { continue }; // An open subpath that ends where it starts encloses its area already. if o.closed || first.start().distance(last.end()) <= eps { continue; } for (e, from) in [(first.start(), 0.0), (last.end(), o.len())] { let Some(to) = gap_from(&tracks, &grid, i, e, from, max_gap, eps) else { continue }; let (cx, cy) = grid.cell_of(to); let listed = (cx - 1..=cx + 1) .flat_map(|x| (cy - 1..=cy + 1).map(move |y| (x, y))) .filter_map(|c| by_end.get(&c)) .flatten() .filter_map(|&g| gaps.get(g)) .any(|g| g[0].distance(to) <= eps && g[1].distance(e) <= eps); if !listed { by_end.entry(grid.cell_of(e)).or_default().push(gaps.len()); gaps.push([e, to]); } } } gaps } /// Most segments the Shape Builder sweeps into its planar map (open paths as edges, the gaps closed /// paths enclose, outline edges); past this only the filled areas count. The map costs about what /// the filled areas' arrangement does (both near linear in the input; the cap was 4096 while /// finding each edge's source scanned every segment, #937), so this only bounds the time a hover /// can take on art far past what a drawing holds. pub const SHAPE_BUILDER_MAX_SEGMENTS: usize = 65_536; /// The Shape Builder's arrangement: see [`shape_builder`]. #[derive(Clone, Debug, Default, PartialEq)] pub struct BuilderArrangement { pub regions: Vec, /// The pieces of the paths with no closed subpath, keyed by input index. pub lines: Vec, /// The pieces of the other paths' outlines, keyed by input index (the front-most where inputs /// share one); only when asked for. pub edges: Vec, } /// Does any of `shapes` have an open subpath (a cutting edge for the Shape Builder)? pub fn has_open_path(shapes: &[Shape]) -> bool { shapes.iter().flat_map(|s| &s.path.subpaths).any(|sp| !sp.closed && sp.anchors.len() >= 2) } /// Shape Builder regions of `shapes` (back → front), the pieces of their open paths and, with /// `edges`, the pieces of their outlines (what erasing an edge deletes). /// /// Closed paths bound regions with their filled areas, as in [`regions`](crate::regions), and the /// gaps they enclose that nothing fills are regions too (empty `sources`), such as the middle of /// four circles that touch (#1691). Open paths (the caller closes the ones whose fill should count) /// are cutting edges: a line across a shape splits it, lines around an area enclose a region /// nothing fills, and each path with no closed subpath comes back cut wherever another path meets /// it, as pieces keyed by its index in `shapes`. Outlines are cut the same way. With more than /// [`SHAPE_BUILDER_MAX_SEGMENTS`] segments or a map that can't be built, only the filled areas /// count and there are no pieces. pub fn shape_builder(shapes: &[Shape], edges: bool) -> BuilderArrangement { shape_builder_with_gap(shapes, edges, 0.0) } /// Most distance (document units) a Shape Builder gap may span. pub const SHAPE_BUILDER_MAX_GAP: f64 = 72.0; /// Original inputs and the shared detector's temporary, unfilled bridges. Keep the originals' /// order and keys so only their split pieces can be returned as art. pub(crate) fn gap_inputs(shapes: &[Shape], gap: f64) -> Vec { let mut closed = shapes.to_vec(); let segments: usize = shapes.iter().flat_map(|s| &s.path.subpaths).map(|sp| sp.segment_count()).sum(); if segments <= SHAPE_BUILDER_MAX_SEGMENTS && gap.is_finite() && gap > 0.0 && has_open_path(shapes) { let gaps = find_gaps(shapes, gap.min(SHAPE_BUILDER_MAX_GAP)); if !gaps.is_empty() { let path = PathData::new(gaps.iter().map(|g| SubPath::polyline(g, false)).collect()); closed.push(Shape::new(path, FillRule::NonZero, u64::MAX)); } } closed } /// [`shape_builder`] with Gap Detection: open paths whose ends come within `gap` (document units) /// of another path are bridged for the regions they bound, so a region left open by a small gap is /// 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 = 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) = 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 { 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, 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, Vec) = pieces.into_iter().partition(|e| is_line(e.key)); let regions = if has_open { faces } else { // The filled areas from their arrangement, whose faces merge exactly, then the gaps. let mut regions = crate::pathfinder::regions_with(shapes, precision); regions.extend(faces.into_iter().filter(|f| f.sources.is_empty())); regions }; BuilderArrangement { regions, lines: if has_open { lines } else { Vec::new() }, edges: if edges { outline } else { Vec::new() } } } /// 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], precision: f64) -> Vec { let fills = |s: &Shape| s.path.subpaths.iter().any(|sp| sp.closed || encloses_area(sp)); let keep: Vec = (0..shapes.len()).filter(|&i| shapes.get(i).is_some_and(fills)).collect(); if keep.len() == shapes.len() { return crate::pathfinder::regions_with(shapes, precision); } let kept: Vec = keep.iter().filter_map(|&i| shapes.get(i).cloned()).collect(); crate::pathfinder::regions_with(&kept, precision) .into_iter() .map(|mut r| { r.sources = r.sources.iter().filter_map(|&s| keep.get(s).copied()).collect(); r }) .collect() } /// Does `sp`, closed, enclose an area (rather than retrace itself)? The Shape Builder closes the /// open subpaths of filled paths that do. pub fn encloses_area(sp: &SubPath) -> bool { let mut closed = sp.clone(); closed.closed = true; let mut bp = BezPath::new(); closed.to_bezpath_into(&mut bp); let size = bp.bounding_box().size(); bp.area().abs() > 1e-9 * (size.width * size.width + size.height * size.height) } /// `path` without `piece`, a run along one of its subpaths between two points (a Shape Builder /// edge): a closed subpath opens there, an open one splits in two (or loses an end), and a piece /// that runs all the way round takes its subpath. With `fill_closes`, open subpaths that enclose an /// area ([`encloses_area`]) run on round an invisible closing edge, as the Shape Builder sees a /// filled path: a piece may run over it, and it never comes back as a visible segment. None when /// the piece doesn't lie on `path` within `tol`. pub fn cut_out(path: &PathData, piece: &SubPath, fill_closes: bool, tol: f64) -> Option { let n = piece.segment_count(); if n == 0 { return None; } // A point well inside the piece: the middle of its longest segment. let arclen = |k: usize| kurbo::ParamCurveArclen::arclen(&piece.segment(k), 1e-3); let mid = (0..n).map(|k| (arclen(k), k)).max_by(|x, y| x.0.total_cmp(&y.0)).map(|(_, k)| piece.segment(k).eval(0.5))?; let (a, b) = (piece.anchors.first()?.p, piece.anchors.last()?.p); let mut bp = BezPath::new(); piece.to_bezpath_into(&mut bp); if bp.perimeter(1e-6) <= tol { // A speck (where paths touch): there's nothing to cut. return None; } // A piece that ends where it starts runs all the way round. let whole = piece.closed || a.distance(b) <= tol; // Each subpath as the piece may run along it: round its closing edge when it has one. let ring = |sp: &SubPath| SubPath { anchors: sp.anchors.clone(), closed: sp.closed || (fill_closes && encloses_area(sp)) }; let (si, geom, um) = path .subpaths .iter() .enumerate() .filter_map(|(i, sp)| { let g = ring(sp); let (u, d) = nearest_u(&g, mid)?; (d <= tol).then_some((i, g, u, d)) }) .min_by(|x, y| x.3.total_cmp(&y.3)) .map(|(i, g, u, _)| (i, g, u))?; let len = geom.segment_count() as f64; let real = path.subpaths.get(si)?.segment_count() as f64; let at = |p: Point| nearest_u(&geom, p).filter(|(_, d)| *d <= tol).map(|(u, _)| u); // The parameter spans of `geom` that stay (unrolled past the end of a ring). let keep: Vec<(f64, f64)> = if whole { Vec::new() } else if geom.closed { let (ua, ub) = (at(a)?, at(b)?); let fwd = |from: f64, to: f64| (to - from).rem_euclid(len); // The piece runs from one end through `mid` to the other; what stays runs on round. let (start, end) = if fwd(ua, um) <= fwd(ua, ub) { (ub, ua) } else { (ua, ub) }; let rest = fwd(start, end); if rest <= 1e-9 || len - rest <= 1e-9 { Vec::new() } else { // Where the ring closes with an invisible edge (`real..len`), what stays stops at it. let parts: &[(f64, f64)] = if real >= len { &[(0.0, 2.0 * len)] } else { &[(0.0, real), (len, len + real)] }; parts.iter().map(|&(lo, hi)| (start.max(lo), (start + rest).min(hi))).filter(|(x, y)| y - x > 1e-9).collect() } } else { let (ua, ub) = (at(a)?, at(b)?); let (lo, hi) = (ua.min(ub), ua.max(ub)); if !(lo - 1e-9..=hi + 1e-9).contains(&um) { return None; } [(0.0, lo), (hi, len)].into_iter().filter(|(x, y)| y - x > 1e-9).collect() }; let mut subpaths: Vec = path.subpaths.iter().take(si).cloned().collect(); subpaths.extend(keep.into_iter().filter_map(|(u0, u1)| span(&geom, u0, u1)).map(|sp| snap_ends(sp, [a, b], tol))); subpaths.extend(path.subpaths.iter().skip(si + 1).cloned()); Some(PathData::new(subpaths)) } /// The run of `piece` (a Shape Builder edge or line piece) around the point of it nearest `at`, /// from the nearest of `stops` before that point to the nearest after it, open. Stops are the /// points within `tol` of the piece, such as the anchors of the path it lies on; an open piece's /// own ends bound the run where no stop comes first. None when the piece has no segments, or when /// it is closed and fewer than two different stops lie on it (the run would be all of it). pub fn run_around(piece: &SubPath, stops: &[Point], at: Point, tol: f64) -> Option { let n = piece.segment_count(); if n == 0 { return None; } let len = n as f64; let (u, _) = nearest_u(piece, at)?; // The stops by x, so that each segment measures only the stops within its bounds. let mut by_x: Vec = stops.iter().copied().filter(|s| s.x.is_finite() && s.y.is_finite()).collect(); by_x.sort_by(|a, b| a.x.total_cmp(&b.x)); // The parameters of the stops within `tol` of segment `j` whose nearest point on the whole // piece lies on it (a ring's segments repeat past either end, `len` apart). let on = |j: isize| -> Vec { use kurbo::ParamCurveNearest; let seg = piece.segment(j.rem_euclid(n as isize) as usize); let b = kurbo::Shape::bounding_box(&seg).inflate(tol, tol); let near = by_x.get(by_x.partition_point(|s| s.x < b.x0)..).unwrap_or_default(); near.iter() .take_while(|s| s.x <= b.x1) .filter(|s| s.y >= b.y0 && s.y <= b.y1) .filter_map(|s| { let m = seg.nearest(*s, 1e-9); let d = m.distance_sq.sqrt(); (d <= tol && nearest_u(piece, *s).is_some_and(|(_, nearest)| d <= nearest + 1e-9)).then_some(j as f64 + m.t) }) .collect() }; // From the segment under `at`, the walk goes back to the nearest stop at or before it and on // to the nearest stop after it; an open piece's ends bound it. let k0 = (u.floor().max(0.0) as isize).min(n as isize - 1); let (first, last) = if piece.closed { (k0 - n as isize, k0 + n as isize) } else { (0, n as isize - 1) }; let lo = (first..=k0).rev().find_map(|j| on(j).into_iter().filter(|&x| x <= u).max_by(f64::total_cmp)); let lo = match lo { Some(lo) => lo, None if !piece.closed => 0.0, None => return None, }; let after = u.max(lo + 1e-9); let hi = (k0..=last).find_map(|j| on(j).into_iter().filter(|&x| x > after).min_by(f64::total_cmp)); let hi = match hi { Some(hi) => hi, None if !piece.closed => len, None => return None, }; // On a ring, a lone stop would make the run all of it. if piece.closed && hi - lo >= len - 1e-9 { return None; } // A run that passes the ring's start begins before its end and is unrolled past it. let (lo, hi) = if lo < 0.0 { (lo + len, hi + len) } else { (lo, hi) }; let run = span(piece, lo, hi)?; // The run's ends go exactly on the stops they end at. let stop_at = |q: Point| stops.iter().copied().filter(|s| s.distance(q) <= tol).min_by(|a, b| a.distance(q).total_cmp(&b.distance(q))).unwrap_or(q); let ends = [run.anchors.first()?.p, run.anchors.last()?.p].map(stop_at); Some(snap_ends(run, ends, tol)) } /// The point of `sp` nearest `p`, as a parameter `segment + t`, with its distance. fn nearest_u(sp: &SubPath, p: Point) -> Option<(f64, f64)> { use kurbo::ParamCurveNearest; (0..sp.segment_count()) .map(|k| { let n = sp.segment(k).nearest(p, 1e-9); (k as f64 + n.t, n.distance_sq.sqrt()) }) .min_by(|x, y| x.1.total_cmp(&y.1)) } /// `sp` with its end anchors within `tol` of one of `to` put exactly on it (where the planar map /// put the junction a piece ends at), handles moving with them. fn snap_ends(mut sp: SubPath, to: [Point; 2], tol: f64) -> SubPath { let last = sp.anchors.len().saturating_sub(1); for i in [0, last] { if let Some(an) = sp.anchors.get_mut(i) && let Some(q) = to.iter().find(|q| q.distance(an.p) <= tol) { let d = *q - an.p; an.p = *q; an.h_in += d; an.h_out += d; } } sp } /// The run of `sp` from parameter `u0` to `u1` (`u0 < u1`, unrolled past the end of a closed one), /// open. fn span(sp: &SubPath, u0: f64, u1: f64) -> Option { let n = sp.segment_count(); if n == 0 || !(u0.is_finite() && u1.is_finite()) { return None; } let first = u0.floor().max(0.0) as usize; let last = (u1.ceil().max(0.0) as usize).min(first + 2 * n); let segs: Vec = (first..last) .filter_map(|k| { let (t0, t1) = ((u0 - k as f64).clamp(0.0, 1.0), (u1 - k as f64).clamp(0.0, 1.0)); (t1 - t0 > 1e-12).then(|| Seg { c: sp.segment(k % n).subsegment(t0..t1), line: sp.segment_is_line(k % n) }) }) .collect(); segs_to_subpath(&segs, false) } /// What is left of input `shape` when the faces that hold `merged` merge on their own, with the /// inputs that cover them marked in `cover` ([`Map::merge`]). pub(crate) struct Rest { pub(crate) shape: usize, pub(crate) merged: Vec, pub(crate) cover: Vec, } /// The planar graph of `shapes` (in the sweep's frame) with its faces walked: what [`build`] makes /// Live Paint faces and edges from, and what [`Map::merge`] merges faces in. pub(crate) struct Map { edges: Vec, around: Vec>, slot_of: Vec, /// Bounded faces: their cycle of half-edges, path and area; and, by their bounds, which can /// hold a point, so finding the face around one tests only those. faces: Vec<(Vec, BezPath, f64)>, face_index: RectIndex, /// Bounded faces left out as slivers ([`is_sliver`]): their cycle of half-edges and the inputs /// whose fill covers them (none for a gap, such as a thin cutout). slivers: Vec<(Vec, Vec)>, /// Outer boundaries of connected pieces, and the face each one is a hole in. holes: Vec<(Vec, BezPath)>, face_holes: Vec>, tidy: Tidy, /// The inputs' anchors (sorted by x) and how close results snap back onto them. anchors: Vec, snap: f64, } /// 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_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 { let mut inputs: Vec<(BezPath, usize)> = Vec::new(); for (i, s) in shapes.iter().enumerate() { let mut bp = BezPath::new(); for sp in s.path.subpaths.iter().filter(|sp| sp.anchors.len() >= 2) { if sp.closed { sp.to_bezpath_into(&mut bp); } else { let mut open = BezPath::new(); sp.to_bezpath_into(&mut open); there_and_back(&open, &mut bp); } } if !bp.elements().is_empty() { bp.apply_affine(SHEAR); inputs.push((bp, i)); } } if inputs.is_empty() { return None; } let eps = eps_for(inputs.iter().map(|p| &p.0)).ok()?; snap_horizontals(&mut inputs.iter_mut().map(|p| &mut p.0).collect::>(), eps); // Counted on the snapped geometry, which is what the sweep sees: snapping can bring boxes into contact. if !crate::budget::sweep_fits(inputs.iter().map(|p| &p.0)) { return None; } // The sweep can panic on rare near-degenerate input; that must never take the app down. let sweep = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| Topology::::from_paths(inputs.iter().map(|(p, i)| (p, *i)), eps))); let top = sweep.ok()?.ok()?; 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 // a rounding error away from where it cuts that edge). let positions = top.compute_positions(); let cell = 4.0 * eps; let mut grid: HashMap<(i64, i64), Vec> = HashMap::new(); let mut points: Vec = Vec::new(); let mut vertex_id = |p: Point| { let (cx, cy) = ((p.x / cell).floor() as i64, (p.y / cell).floor() as i64); let near = (cx - 1..=cx + 1) .flat_map(|x| (cy - 1..=cy + 1).map(move |y| (x, y))) .filter_map(|k| grid.get(&k)) .flatten() .copied() .find(|&v| points.get(v).is_some_and(|q| q.distance(p) <= eps)); near.unwrap_or_else(|| { points.push(p); grid.entry((cx, cy)).or_default().push(points.len() - 1); points.len() - 1 }) }; let mut pieces: Vec<(Vec, [usize; 2])> = Vec::new(); for idx in top.segment_indices() { let segs: Vec = positions[idx].path.segments().map(to_seg).collect(); let ends = [vertex_id(top.point(idx.first_half()).to_kurbo()), vertex_id(top.point(idx.second_half()).to_kurbo())]; if !segs.is_empty() && ends[0] != ends[1] { pieces.push((segs, ends)); } } // The sweep can cut the paths through one crossing at points more than `eps` apart, so copies of // one edge (such as an open path's two passes) can end on different vertices. Pieces whose // middles and ends lie within `tol` of each other are such copies, and their ends are one vertex. let tol = 16.0 * eps; let cell_of = |p: Point| ((p.x / tol).floor() as i64, (p.y / tol).floor() as i64); let mids: Vec> = pieces.iter().map(|(segs, _)| mid(segs)).collect(); let mut by_mid: HashMap<(i64, i64), Vec> = HashMap::new(); for (k, m) in mids.iter().enumerate() { if let Some(m) = m { by_mid.entry(cell_of(*m)).or_default().push(k); } } let close = |a: usize, b: usize| matches!((points.get(a), points.get(b)), (Some(p), Some(q)) if p.distance(*q) <= tol); let mut joined: Vec = (0..points.len()).collect(); // Where two passes of one path cross another (an open path's way there and back, coincident // outlines), the sweep can also join its two cuts, a rounding error apart, with a stub of that // length: a speck of a piece, and a vertex twice over. Such stubs are no edges: their ends are // one vertex. The sweep moves points by a few `eps` at most up and down its frame; along a path // that runs nearly level in it (where the height a cut is found at pins its position down // least) a cut can slide further, here up to a small fraction of the precision results are // tidied to, below which no edge is drawn anyway. A vertex's cluster stays within those bounds // as a whole: a run of short, nearly level segments (a finely flattened or traced path) is real // geometry, not one stub after another, and must not collapse into one vertex. let (stub_y, stub_x) = (4.0 * eps, (DEFAULT_PRECISION / 16.0).max(4.0 * eps)); let mut extent: Vec = points.iter().map(|&p| kurbo::Rect::from_points(p, p)).collect(); for (segs, [v0, v1]) in &pieces { let Some(&p0) = points.get(*v0) else { continue }; if !segs.iter().flat_map(|s| [s.c.p1, s.c.p2, s.c.p3]).all(|p| (p.y - p0.y).abs() <= stub_y && (p.x - p0.x).abs() <= stub_x) { continue; } let (a, b) = (find(&mut joined, *v0), find(&mut joined, *v1)); let (Some(&ra), Some(&rb)) = (extent.get(a), extent.get(b)) else { continue }; let both = ra.union(rb); if a == b || both.width() > stub_x || both.height() > stub_y { continue; } let (root, child) = (a.min(b), a.max(b)); if let Some(p) = joined.get_mut(child) { *p = root; } if let Some(r) = extent.get_mut(root) { *r = both; } } for (k, (_, [a, b])) in pieces.iter().enumerate() { let Some(&Some(m)) = mids.get(k) else { continue }; let (cx, cy) = cell_of(m); let others = (cx - 1..=cx + 1).flat_map(|x| (cy - 1..=cy + 1).map(move |y| (x, y))).filter_map(|c| by_mid.get(&c)).flatten(); for &j in others.filter(|&&j| j > k) { let (Some((_, [c, d])), Some(&Some(n))) = (pieces.get(j), mids.get(j)) else { continue }; if m.distance(n) > tol { continue; } let pairs = if close(*a, *c) && close(*b, *d) { [(*a, *c), (*b, *d)] } else if close(*a, *d) && close(*b, *c) { [(*a, *d), (*b, *c)] } else { continue; }; for (x, y) in pairs { let (x, y) = (find(&mut joined, x), find(&mut joined, y)); if let Some(r) = joined.get_mut(x.max(y)) { *r = x.min(y); } } } } let mut edges: Vec = Vec::new(); let mut between: HashMap<(usize, usize), Vec> = HashMap::new(); let outlines = Outlines::new(&inputs, eps); for (mut segs, ends) in pieces { let ends = ends.map(|v| find(&mut joined, v)); let (Some(&p0), Some(&p1)) = (points.get(ends[0]), points.get(ends[1])) else { continue }; if ends[0] == ends[1] { continue; } // Every edge at a vertex starts or ends exactly on it, so faces meeting there share its // coordinates: merging them (a union) then cancels their common edge, which it can't when // each face carries its own rounding of the corner (#893). pin_ends(&mut segs, p0, p1); let Some(source) = mid(&segs).and_then(|m| outlines.source_at(m)) else { continue }; // Coincident pieces the sweep left apart are one edge (the front-most input's). let key = (ends[0].min(ends[1]), ends[0].max(ends[1])); let here = mid(&segs); 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) < precision) }) }); if let Some(e) = twin_of { if let Some(e) = edges.get_mut(e) && source > e.source { e.source = source; } continue; } between.entry(key).or_default().push(edges.len()); edges.push(Edge { segs, ends, source }); } let nv = points.len(); // Half-edges leaving each vertex, sorted by direction; walking a face, the next half-edge is // the one just clockwise of the way back, so bounded faces come out with positive area and // the outer boundary of each connected piece with negative area. let mut around: Vec> = vec![Vec::new(); nv]; for h in 0..2 * edges.len() { if let Some(list) = origin(&edges, h).and_then(|v| around.get_mut(v)) { list.push(h); } } let mut slot_of = vec![0usize; 2 * edges.len()]; for (v, list) in around.iter_mut().enumerate() { let Some(&at) = points.get(v) else { continue }; // Half the shortest chord of the edges here: every one of them gets that far from it. let r = 0.5 * list.iter().filter_map(|&h| far_end(&edges, h).map(|p| p.distance(at))).fold(f64::INFINITY, f64::min); let mut keyed: Vec<(f64, usize)> = list.iter().map(|&h| (leaving_angle(&edges, h, at, r), h)).collect(); keyed.sort_by(|a, b| a.0.total_cmp(&b.0)); *list = keyed.into_iter().map(|(_, h)| h).collect(); for (k, &h) in list.iter().enumerate() { if let Some(s) = slot_of.get_mut(h) { *s = k; } } } let next = |h: usize| -> Option { let back = twin(h); let list = around.get(origin(&edges, back)?)?; let k = *slot_of.get(back)?; list.get((k + list.len() - 1) % list.len()).copied() }; // Connected pieces of the graph. let mut parent: Vec = (0..nv).collect(); for e in &edges { let (a, b) = (find(&mut parent, e.ends[0]), find(&mut parent, e.ends[1])); if let Some(p) = parent.get_mut(a) { *p = b; } } let piece: Vec = (0..nv).map(|v| find(&mut parent, v)).collect(); let piece_of = |cycle: &[usize]| cycle.first().and_then(|&h| origin(&edges, h)).and_then(|v| piece.get(v)).copied(); // Walk every cycle: positive ones are faces, negative ones outer boundaries (holes in the // face around them, if any). let mut seen = vec![false; 2 * edges.len()]; let mut faces: Vec<(Vec, BezPath, f64)> = Vec::new(); let mut slivers: Vec<(Vec, BezPath)> = Vec::new(); let mut holes: Vec<(Vec, BezPath)> = Vec::new(); for start in 0..2 * edges.len() { if seen.get(start).copied().unwrap_or(true) { continue; } let mut cycle = vec![]; let mut h = start; while let Some(s) = seen.get_mut(h).filter(|s| !**s) { *s = true; cycle.push(h); match next(h) { Some(n) => h = n, None => break, } } let cycle = without_spurs(&cycle); if cycle.is_empty() { continue; } let bp = cycle_bezpath(&edges, &cycle); 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)); } } // Each hole goes in the smallest face of another piece around it. let face_index = RectIndex::new(faces.iter().enumerate().map(|(i, (_, bp, _))| (bp.bounding_box(), i)).collect()); let mut face_holes: Vec> = vec![Vec::new(); faces.len()]; for (k, (cycle, _)) in holes.iter().enumerate() { let Some(p) = cycle.first().and_then(|&h| origin(&edges, h)).and_then(|v| points.get(v)).copied() else { continue }; let own = piece_of(cycle); let around_it = face_index .at(p, 0.0) .filter_map(|&i| faces.get(i).map(|f| (i, f))) .filter(|(_, (fc, bp, _))| piece_of(fc) != own && bp.winding(p) != 0) .min_by(|a, b| a.1.2.total_cmp(&b.1.2)) .map(|(i, _)| i); if let Some(list) = around_it.and_then(|i| face_holes.get_mut(i)) { list.push(k); } } // The inputs that cover each sliver, tested at a point inside it. let fills: Vec = if slivers.is_empty() || !cover { Vec::new() } else { shapes.iter().map(Fill::new).collect() }; let slivers: Vec<(Vec, Vec)> = slivers .into_iter() .map(|(cycle, mut bp)| { if !cover { return (cycle, Vec::new()); } bp.apply_affine(UNSHEAR); let sources = interior_point(&PathData::from_bezpath(&bp)) .map(|p| fills.iter().enumerate().filter(|(_, f)| f.contains(p)).map(|(i, _)| i).collect()) .unwrap_or_default(); (cycle, sources) }) .collect(); // The inputs' anchors, which results land on again up to the sweep's tolerance. let snap = 2.0 * eps; let mut anchors: Vec = shapes.iter().flat_map(|s| s.path.subpaths.iter().flat_map(|sp| sp.anchors.iter().map(|a| a.p))).collect(); anchors.sort_by(|a, b| a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y))); Some(Map { edges, around, slot_of, faces, face_index, 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). A sliver next to a merged face that only inputs marked in `cover` fill merges with /// them, so it doesn't stay as a hairline gap between them. With `rest`, a sliver goes in only /// when its input covers it, and not when the merge of the faces merged on their own takes it /// in. Outer outlines come out positive, holes negative. None when a point is in no face. pub(crate) fn merge(&self, inside: &[Point], cover: &[bool], rest: Option<&Rest>) -> Option { let halves = 2 * self.edges.len(); // The face on each half-edge's side; holes bound the face around them. Slivers come after // the faces. let mut face_of: Vec> = vec![None; halves]; for (i, (cycle, ..)) in self.faces.iter().enumerate() { let hole_cycles = self.face_holes.get(i)?.iter().filter_map(|&k| self.holes.get(k)).map(|(c, _)| c); for &h in std::iter::once(cycle).chain(hole_cycles).flatten() { *face_of.get_mut(h)? = Some(i); } } 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; 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)) .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)); let mut used = vec![false; halves]; let mut subpaths = Vec::new(); for start in 0..halves { if !boundary(start) || used.get(start).copied().unwrap_or(true) { continue; } let mut cycle = vec![]; let mut h = start; while !used.get(h).copied().unwrap_or(true) { if let Some(u) = used.get_mut(h) { *u = true; } cycle.push(h); // Turn from the way back as a face walk does, past the edges inside the merged // faces (between two of them, or a line's loose end), to the next boundary edge. let back = twin(h); let list = self.around.get(origin(&self.edges, back)?)?; let k = *self.slot_of.get(back)?; let n = list.len(); h = (1..=n).filter_map(|j| list.get((k + n * 2 - j) % n).copied()).find(|&g| boundary(g))?; } if h != start { return None; } let segs: Vec = cycle.iter().flat_map(|&h| half_segs(&self.edges, h)).collect(); subpaths.push(segs_to_subpath(&tidy_segments(segs, true, &self.tidy), true)?); } (!subpaths.is_empty()).then(|| unshear(PathData::new(subpaths), &self.anchors, self.snap)) } } fn build(shapes: &[Shape], precision: f64) -> Option<(Vec, Vec)> { 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 = shapes.iter().map(Fill::new).collect(); let subpath = |cycle: &[usize]| { let segs: Vec = cycle.iter().flat_map(|&h| half_segs(edges, h)).collect(); segs_to_subpath(&tidy_segments(segs, true, tidy), true) }; let regions = faces .iter() .zip(face_holes) .filter_map(|((cycle, _, _), hs)| { let mut subpaths = vec![subpath(cycle)?]; 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 = // 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(); // Edges: chains of segments through vertices where nothing else meets. let degree: Vec = around.iter().map(Vec::len).collect(); let passes = |v: usize| -> Option<(usize, usize)> { let list = around.get(v)?; let (&a, &b) = (list.first()?, list.get(1)?); (degree.get(v) == Some(&2) && edges.get(a / 2)?.source == edges.get(b / 2)?.source).then_some((a, b)) }; let mut used = vec![false; edges.len()]; let mut chains: Vec<(Vec, bool, usize)> = Vec::new(); // A chain from half-edge `h0` up to the next vertex where it doesn't pass straight through; // closed when it comes back round to where it started. let walk = |h0: usize, used: &mut Vec| -> Option<(Vec, bool, usize)> { let source = edges.get(h0 / 2)?.source; let mut segs = Vec::new(); let mut h = h0; loop { let u = used.get_mut(h / 2)?; if *u { return Some((segs, true, source)); } *u = true; segs.extend(half_segs(edges, h)); let v = origin(edges, twin(h))?; let Some((a, b)) = passes(v) else { return Some((segs, origin(edges, h0) == Some(v), source)) }; h = if a == twin(h) { b } else { a }; } }; for v in 0..around.len() { if passes(v).is_some() { continue; } for &h in around.get(v).map(Vec::as_slice).unwrap_or_default() { if !used.get(h / 2).copied().unwrap_or(true) && let Some(c) = walk(h, &mut used) { chains.push(c); } } } // What's left are closed loops with no junction on them. for e in 0..edges.len() { if !used.get(e).copied().unwrap_or(true) && let Some(c) = walk(2 * e, &mut used) { chains.push(c); } } let edge_shapes = chains .into_iter() .filter_map(|(segs, closed, source)| { let sp = segs_to_subpath(&tidy_segments(segs, closed, tidy), closed)?; Some(Shape::new(unshear(PathData::new(vec![sp]), anchors, snap), FillRule::NonZero, shapes.get(source)?.key)) }) .collect(); Some((regions, edge_shapes)) } /// 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 { 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 { 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(), tolerance, |el| match el { kurbo::PathEl::MoveTo(p) => { if last != first { edges.push((last, first)); } first = p; last = p; } kurbo::PathEl::LineTo(p) => { edges.push((last, p)); last = p; } kurbo::PathEl::ClosePath => { if last != first { edges.push((last, first)); } last = first; } _ => {} }); if last != first { edges.push((last, first)); } let mut best: Option<(f64, Point)> = None; for f in [0.5, 0.25, 0.75, 0.125, 0.375, 0.625, 0.875, 0.0625, 0.9375] { // A little off the round fractions, which on art drawn to a grid fall on the height of // some corner, where testing the point against other paths is unreliable. let y = bb.y0 + bb.height() * (f + 0.0061803); let mut xs: Vec = edges.iter().filter(|(a, c)| (a.y <= y) != (c.y <= y)).map(|(a, c)| a.x + (y - a.y) / (c.y - a.y) * (c.x - a.x)).collect(); xs.sort_by(f64::total_cmp); for w in xs.windows(2) { let m = Point::new((w[0] + w[1]) / 2.0, y); let width = w[1] - w[0]; if width > best.map_or(0.0, |b| b.0) && bp.winding(m) != 0 { best = Some((width, m)); } } } best.map(|b| b.1) } #[cfg(test)] mod tests;