//! The paths of long paths and compound paths kept by object id, with the boxes of their segments //! and of their miter tips by runs of segments. When an object's anchors move, the renderer changes //! only the path elements, boxes and tips of the segments that start or end at a moved anchor, //! instead of making its path again and measuring it whole. use std::sync::Arc; use vectorcraft_doc::{LineJoin, Node, NodeKind, StrokeLayer}; use vectorcraft_geom::bez::has_length; use vectorcraft_geom::path::segment_bounds; use vectorcraft_geom::{Anchor, BezPath, PathData, PathEl, PathSeg, Rect, SubPath}; /// Paths and compound paths with at least this many anchors are kept ([`KeptPath`]). pub(crate) const KEPT_ANCHORS: usize = 1024; /// Segments measured together into one box. const RUN: usize = 128; /// The boxes and the miter tips' boxes of a subpath's runs ([`KeptPath::runs`]). #[cfg(test)] pub(crate) type RunBoxes = (Vec>, Vec>); /// A subpath of a kept path. struct Sub { /// Where its MoveTo is in the path's elements. start: usize, /// Its segments (as [`SubPath::segment_count`] counts them). segs: usize, anchors: usize, closed: bool, /// The box of each run of [`RUN`] segments ([`segment_bounds`]). boxes: Vec>, /// The box of the miter tips of `tips_of` at the joints whose first segment is in each run. tips: Vec>, } /// The path of a long path or compound path ([`Node::stroke_path`]), with its geometric bounds by /// runs of segments and the tips of a miter-joined stroke along it by runs of joints. pub(crate) struct KeptPath { /// The object the path was made from (kept alive). pub(crate) node: Arc, pub(crate) path: Arc, /// The subpaths of each path of the object: the path itself, or each member of a compound path. parts: Vec>, /// The stroke the subpaths' tips were measured for. tips_of: Option, /// The last frame that used it. pub(crate) stamp: u64, } /// The paths of `n`: its own, or those of the members of a compound path whose members are all /// paths and that is not a Shaper group or an Image Trace object. `None` for anything else. fn paths(n: &Node) -> Option> { match &n.kind { NodeKind::Path { path, .. } => Some(vec![path]), NodeKind::Compound { children, .. } if n.extras().shaper.is_none() && n.extras().trace.is_none() => { children.iter().map(|c| c.path_data()).collect() } _ => None, } } /// Whether `n` has a path that is kept ([`KeptPath`]): a path or compound path with at least /// [`KEPT_ANCHORS`] anchors, none of whose closed subpaths starts at a point that is not a number /// (its path closes with an extra segment there). pub(crate) fn kept(n: &Node) -> bool { let Some(paths) = paths(n) else { return false }; let subs = || paths.iter().flat_map(|p| &p.subpaths); subs().map(|s| s.anchors.len()).sum::() >= KEPT_ANCHORS && subs().filter(|s| s.closed).all(|s| s.anchors.first().is_none_or(|a| !a.p.x.is_nan() && !a.p.y.is_nan())) } /// Whether anchors `a` and `b` have the same points, bit for bit. fn same(a: &Anchor, b: &Anchor) -> bool { let bits = |a: &Anchor| [a.p.x, a.p.y, a.h_in.x, a.h_in.y, a.h_out.x, a.h_out.y].map(f64::to_bits); bits(a) == bits(b) } /// The path element of segment `i` of `sp`, as [`SubPath::to_bezpath_into`] writes it. fn element(sp: &SubPath, i: usize) -> PathEl { let c = sp.segment(i); if sp.segment_is_line(i) { PathEl::LineTo(c.p3) } else { PathEl::CurveTo(c.p1, c.p2, c.p3) } } /// The point a path element ends at (`None` for a ClosePath). fn end(el: &PathEl) -> Option { match *el { PathEl::MoveTo(p) | PathEl::LineTo(p) | PathEl::QuadTo(_, p) | PathEl::CurveTo(_, _, p) => Some(p), PathEl::ClosePath => None, } } impl Sub { /// Segment `i`, as kurbo's `segments` makes it from the path's elements. fn seg(&self, els: &[PathEl], i: usize) -> Option { let p0 = end(els.get(self.start + i)?)?; Some(match *els.get(self.start + 1 + i)? { PathEl::LineTo(p) => PathSeg::Line(vectorcraft_geom::Line::new(p0, p)), PathEl::QuadTo(a, p) => PathSeg::Quad(vectorcraft_geom::kurbo::QuadBez::new(p0, a, p)), PathEl::CurveTo(a, b, p) => PathSeg::Cubic(vectorcraft_geom::CubicBez::new(p0, a, b, p)), PathEl::MoveTo(_) | PathEl::ClosePath => return None, }) } fn runs(&self) -> usize { self.segs.div_ceil(RUN) } /// The box of the segments of run `r`. fn run_box(&self, els: &[PathEl], r: usize) -> Option { (r * RUN..((r + 1) * RUN).min(self.segs)).filter_map(|i| self.seg(els, i)).map(segment_bounds).reduce(|a, b| a.union(b)) } /// The segment with length that follows segment `i` (round to the start when closed), or /// `None` when there is none other than `i`. fn next_drawn(&self, els: &[PathEl], i: usize) -> Option { let after = i + 1..self.segs; let round = if self.closed { 0..i } else { 0..0 }; after.chain(round).filter_map(|j| self.seg(els, j)).find(has_length) } /// The segment with length before segment `i` (round from the end when closed), when it is /// within [`RUN`] segments of `i`: `Ok(None)` when there is none, `Err(())` when the search /// gave up. fn prev_drawn(&self, els: &[PathEl], i: usize) -> Result, ()> { let before = (0..i).rev(); let round = if self.closed { (i + 1..self.segs).rev() } else { (0..0).rev() }; for (steps, j) in before.chain(round).enumerate() { if steps >= RUN { return Err(()); } if self.seg(els, j).is_some_and(|s| has_length(&s)) { return Ok(Some(j)); } } Ok(None) } /// The box of the miter tips of `st` at the joints whose first segment is in run `r` /// ([`StrokeLayer::miter_tip`]). fn run_tips(&self, els: &[PathEl], r: usize, st: &StrokeLayer) -> Option { let mut out: Option = None; for i in r * RUN..((r + 1) * RUN).min(self.segs) { let Some(a) = self.seg(els, i).filter(has_length) else { continue }; let Some(b) = self.next_drawn(els, i) else { continue }; if let Some(tip) = st.miter_tip(self.closed, &a, &b) { out = Some(out.map_or(Rect::from_points(tip, tip), |o| o.union_pt(tip))); } } out } } /// What [`KeptPath::tips`] measures `st`'s tips by: its reach from an open and a closed path and /// its miter limit. fn tips_key(st: &StrokeLayer) -> [u64; 3] { [st.side_reach(false).to_bits(), st.side_reach(true).to_bits(), st.miter_limit.to_bits()] } impl KeptPath { /// The kept path of `n`, or `None` when it has none ([`kept`]). pub(crate) fn new(n: &Arc, stamp: u64) -> Option { if !kept(n) { return None; } let path = n.stroke_path()?; let els = path.elements(); let mut start = 0; let mut parts = vec![]; for pd in paths(n)? { let mut subs = vec![]; for sp in &pd.subpaths { let segs = sp.segment_count(); let mut sub = Sub { start, segs, anchors: sp.anchors.len(), closed: sp.closed, boxes: vec![], tips: vec![] }; sub.boxes = (0..sub.runs()).map(|r| sub.run_box(els, r)).collect(); if !sp.anchors.is_empty() { start += 1 + segs + usize::from(sp.closed); } subs.push(sub); } parts.push(subs); } (start == els.len()).then(|| Self { node: n.clone(), path: Arc::new(path), parts, tips_of: None, stamp }) } /// Bring the path up to date with `n`, a path with the same subpaths and anchor counts as the /// object it was made from: the elements of the segments that start or end at an anchor that /// moved are made again, with the boxes of their runs and the tips of those runs and of the /// runs of the segments with length before them. False, and nothing changed, when `n` has /// other subpaths, other anchor counts or no kept path. pub(crate) fn update(&mut self, n: &Arc) -> bool { let old = self.node.clone(); let (Some(was), Some(now)) = (paths(&old), paths(n)) else { return false }; let shape = |ps: &[&PathData]| ps.iter().map(|p| p.subpaths.iter().map(|s| (s.anchors.len(), s.closed)).collect::>()).collect::>(); if !kept(n) || shape(&was) != shape(&now) { return false; } // Per subpath whose anchors moved: (member, subpath, whether its first anchor moved, the // segments that start or end at a moved anchor). let mut moved: Vec<(usize, usize, bool, Vec)> = vec![]; for (pi, (w, p)) in was.iter().zip(&now).enumerate() { if std::ptr::eq(*w, *p) { continue; } for (si, (ws, ps)) in w.subpaths.iter().zip(&p.subpaths).enumerate() { let Some(sub) = self.parts.get(pi).and_then(|s| s.get(si)) else { return false }; let (mut first, mut segs) = (false, vec![]); for (k, (a, b)) in ws.anchors.iter().zip(&ps.anchors).enumerate() { if same(a, b) { continue; } if k >= 1 && k - 1 < sub.segs { segs.push(k - 1); } if k == 0 { first = true; if sub.closed && sub.segs > 0 { segs.push(sub.segs - 1); } } if k < sub.segs { segs.push(k); } } if first || !segs.is_empty() { segs.sort_unstable(); segs.dedup(); moved.push((pi, si, first, segs)); } } } self.node = n.clone(); if moved.is_empty() { return true; } let path = Arc::make_mut(&mut self.path); let els = path.elements_mut(); for (pi, si, first, segs) in &moved { let (Some(sub), Some(sp)) = (self.parts.get(*pi).and_then(|s| s.get(*si)), now.get(*pi).and_then(|p| p.subpaths.get(*si))) else { continue; }; if *first && let (Some(a), Some(el)) = (sp.anchors.first(), els.get_mut(sub.start)) { *el = PathEl::MoveTo(a.p); } for &i in segs { if let Some(el) = els.get_mut(sub.start + 1 + i) { *el = element(sp, i); } } } let els = path.elements(); for (pi, si, _, segs) in moved { let tips_of = self.tips_of.as_ref(); let Some(sub) = self.parts.get_mut(pi).and_then(|s| s.get_mut(si)) else { continue }; // Many moved: the subpath is measured again whole. let all = segs.len() * 4 > sub.segs; let mut boxes: Vec = if all { (0..sub.runs()).collect() } else { segs.iter().map(|i| i / RUN).collect() }; boxes.dedup(); for r in boxes { sub.boxes[r] = sub.run_box(els, r); } let Some(st) = tips_of else { continue }; // A joint's tip changes with its segments and with which segment with length comes // next: the runs of the moved segments and of the segments with length before them. let mut runs = vec![]; let mut whole = all; for &i in &segs { runs.push(i / RUN); match sub.prev_drawn(els, i) { Ok(Some(j)) => runs.push(j / RUN), Ok(None) => {} Err(()) => whole = true, } } if whole { runs = (0..sub.runs()).collect(); } runs.sort_unstable(); runs.dedup(); for r in runs { sub.tips[r] = sub.run_tips(els, r, st); } } true } /// The object's geometric bounds ([`Node::geometric_bounds`]): each path's as /// [`PathData::bounds`] measures it, joined. pub(crate) fn geometric(&self) -> Option { let els = self.path.elements(); self.parts.iter().fold(None, |acc, subs| { let anchors: usize = subs.iter().map(|s| s.anchors).sum(); let b = match anchors { 0 => None, // One anchor: the point itself. 1 => subs.iter().find(|s| s.anchors == 1).and_then(|s| els.get(s.start)).and_then(end).map(|p| Rect::from_points(p, p)), _ => Some(subs.iter().flat_map(|s| s.boxes.iter().flatten()).copied().reduce(|a, b| a.union(b)).unwrap_or_default()), }; vectorcraft_geom::union_opt(acc, b) }) } /// The box of the miter tips of `st` at every joint of the path ([`StrokeLayer::miter_tip`]), /// for [`StrokeLayer::bounds_with_tips`]: measured whole when the tips kept are another /// stroke's, then kept up to date with the path. pub(crate) fn tips(&mut self, st: &StrokeLayer) -> Option { if st.join != LineJoin::Miter { return None; } if self.tips_of.as_ref().is_none_or(|t| t.join != LineJoin::Miter || tips_key(t) != tips_key(st)) { let els = self.path.elements(); for sub in self.parts.iter_mut().flatten() { sub.tips = (0..sub.runs()).map(|r| sub.run_tips(els, r, st)).collect(); } self.tips_of = Some(st.clone()); } self.parts.iter().flatten().flat_map(|s| s.tips.iter().flatten()).copied().reduce(|a, b| a.union(b)) } /// The boxes and tips kept per run of each subpath, and the stroke the tips were measured for. #[cfg(test)] pub(crate) fn runs(&self) -> (Vec, Option) { (self.parts.iter().flatten().map(|s| (s.boxes.clone(), s.tips.clone())).collect(), self.tips_of.clone()) } }