mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 04:07:12 -06:00
1601 lines
73 KiB
Rust
1601 lines
73 KiB
Rust
//! 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<Point> {
|
||
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<T> {
|
||
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<Vec<u32>>,
|
||
/// Items the grid doesn't hold (bounds covering most of it, or not finite), scanned for
|
||
/// every point.
|
||
rest: Vec<usize>,
|
||
}
|
||
|
||
/// 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<std::ops::RangeInclusive<usize>> {
|
||
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<T> RectIndex<T> {
|
||
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<kurbo::Rect>) -> 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<u32>> = 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<usize> = 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<Item = &T> {
|
||
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<kurbo::Rect> {
|
||
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<usize> {
|
||
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<Seg>,
|
||
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<Seg> {
|
||
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<Item = Seg> + '_ {
|
||
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<Point> {
|
||
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<usize> {
|
||
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<Point> = 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<usize> {
|
||
let mut out: Vec<usize> = 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<PathSeg> = 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<Region>, Vec<Shape>) {
|
||
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<Region>, Vec<Shape>, 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<PathSeg>,
|
||
/// The length at the start of each segment, then the whole length.
|
||
at: Vec<f64>,
|
||
closed: bool,
|
||
bounds: kurbo::Rect,
|
||
}
|
||
|
||
impl Track {
|
||
fn new(sp: &SubPath) -> Option<Self> {
|
||
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<PathSeg> = 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<Item = usize>) -> 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<Point> {
|
||
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<HashMap<usize, Vec<usize>>> = None;
|
||
let mut crossed: HashMap<usize, Vec<(f64, f64)>> = 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<usize, Vec<usize>> = 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<Track> = 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<usize>> = 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<Region>,
|
||
/// The pieces of the paths with no closed subpath, keyed by input index.
|
||
pub lines: Vec<Shape>,
|
||
/// 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<Shape>,
|
||
}
|
||
|
||
/// 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<Shape> {
|
||
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<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) = 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<Shape>, Vec<Shape>) = 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<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::pathfinder::regions_with(shapes, precision);
|
||
}
|
||
let kept: Vec<Shape> = 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<PathData> {
|
||
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<SubPath> = 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<SubPath> {
|
||
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<Point> = 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<f64> {
|
||
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<SubPath> {
|
||
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<Seg> = (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<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 {
|
||
edges: Vec<Edge>,
|
||
around: Vec<Vec<usize>>,
|
||
slot_of: Vec<usize>,
|
||
/// 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<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>>,
|
||
tidy: Tidy,
|
||
/// The inputs' anchors (sorted by x) and how close results snap back onto them.
|
||
anchors: Vec<Point>,
|
||
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> {
|
||
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();
|
||
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::<Vec<_>>(), 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::<Crossings>::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<usize>> = HashMap::new();
|
||
let mut points: Vec<Point> = 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<Seg>, [usize; 2])> = Vec::new();
|
||
for idx in top.segment_indices() {
|
||
let segs: Vec<Seg> = 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<Option<Point>> = pieces.iter().map(|(segs, _)| mid(segs)).collect();
|
||
let mut by_mid: HashMap<(i64, i64), Vec<usize>> = 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<usize> = (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<kurbo::Rect> = 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<Edge> = Vec::new();
|
||
let mut between: HashMap<(usize, usize), Vec<usize>> = 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<usize>> = 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<usize> {
|
||
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<usize> = (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<usize> = (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<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) {
|
||
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<usize>> = 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<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, 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<PathData> {
|
||
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<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);
|
||
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<Seg> = 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<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();
|
||
let subpath = |cycle: &[usize]| {
|
||
let segs: Vec<Seg> = 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<usize> = 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<Seg>, 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<bool>| -> Option<(Vec<Seg>, 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<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(), 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<f64> =
|
||
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;
|