diff --git a/crates/render/src/kept_paths.rs b/crates/render/src/kept_paths.rs index 4525a0f1c..293e82466 100644 --- a/crates/render/src/kept_paths.rs +++ b/crates/render/src/kept_paths.rs @@ -1,25 +1,39 @@ //! 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. +//! only the path elements, boxes and tips of the segments that start or end at a moved anchor. +//! When anchors are added or deleted, or a subpath is closed or opened, it replaces only the path +//! elements of the segments between the first and the last anchor that changed, and measures again +//! only the runs that hold them. When a compound path gains or loses members, or a member gains or +//! loses subpaths, it makes the path of those members again. In each case it does not make the +//! object's path again or measure 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}; +use vectorcraft_geom::{Anchor, BezPath, PathData, PathEl, PathSeg, Point, 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; +/// Segments measured together into one box in a subpath measured whole. A run that anchors were +/// added to or deleted from holds more or fewer, and one that grows past twice this is split. +pub(crate) const RUN: usize = 128; + +/// The most subpaths one edit may add anchors to, delete anchors from, close or open: past this, +/// the kept path is made again whole. +const SPLICES: usize = 16; /// The boxes and the miter tips' boxes of a subpath's runs ([`KeptPath::runs`]). #[cfg(test)] pub(crate) type RunBoxes = (Vec>, Vec>); +/// A subpath's MoveTo, segments and anchors, whether it is closed, and the segments of each of its +/// runs ([`KeptPath::layout`]). +#[cfg(test)] +pub(crate) type Layout = ((usize, usize, usize, bool), Vec); + /// A subpath of a kept path. struct Sub { /// Where its MoveTo is in the path's elements. @@ -28,7 +42,9 @@ struct Sub { segs: usize, anchors: usize, closed: bool, - /// The box of each run of [`RUN`] segments ([`segment_bounds`]). + /// The segment after the last of each run of segments. + ends: Vec, + /// The box of each run of 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>, @@ -72,8 +88,14 @@ pub(crate) fn kept(n: &Node) -> bool { /// 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) + same_drawn(a, b, true, true) +} + +/// Whether anchors `a` and `b` have the same point, bit for bit, and the same incoming handle when +/// `h_in` and outgoing handle when `h_out`. +fn same_drawn(a: &Anchor, b: &Anchor, h_in: bool, h_out: bool) -> bool { + let bits = |p: Point| [p.x, p.y].map(f64::to_bits); + bits(a.p) == bits(b.p) && (!h_in || bits(a.h_in) == bits(b.h_in)) && (!h_out || bits(a.h_out) == bits(b.h_out)) } /// The path element of segment `i` of `sp`, as [`SubPath::to_bezpath_into`] writes it. @@ -82,15 +104,62 @@ fn element(sp: &SubPath, i: usize) -> PathEl { if sp.segment_is_line(i) { PathEl::LineTo(c.p3) } else { PathEl::CurveTo(c.p1, c.p2, c.p3) } } +/// Element `o` of the elements [`SubPath::to_bezpath_into`] writes for `sp`, a subpath with +/// anchors: its MoveTo, then its segments, then its ClosePath. +fn element_at(sp: &SubPath, o: usize) -> PathEl { + match o { + 0 => PathEl::MoveTo(sp.anchors.first().map_or(Point::ORIGIN, |a| a.p)), + o if o <= sp.segment_count() => element(sp, o - 1), + _ => PathEl::ClosePath, + } +} + /// The point a path element ends at (`None` for a ClosePath). -fn end(el: &PathEl) -> Option { +fn end(el: &PathEl) -> Option { match *el { PathEl::MoveTo(p) | PathEl::LineTo(p) | PathEl::QuadTo(_, p) | PathEl::CurveTo(_, _, p) => Some(p), PathEl::ClosePath => None, } } +/// The ends of the runs of a subpath of `segs` segments measured whole: [`RUN`] segments each. +fn whole_runs(segs: usize) -> Vec { + (1..=segs.div_ceil(RUN)).map(|r| (r * RUN).min(segs)).collect() +} + +/// The paths `ps` written over the `len` elements at `at` in `els`, and their subpaths measured +/// whole with the tips of `st`. `at` moves past them. +fn made_again(ps: &[&PathData], len: usize, els: &mut Vec, at: &mut usize, st: Option<&StrokeLayer>) -> Vec> { + let mut bp = BezPath::new(); + ps.iter().flat_map(|p| &p.subpaths).for_each(|sp| sp.to_bezpath_into(&mut bp)); + let to = (*at + len).min(els.len()); + els.splice((*at).min(to)..to, bp.into_elements()); + let mut measure = |sp: &SubPath| { + let sub = Sub::measured(sp, *at, els, st); + *at += sub.len(); + sub + }; + ps.iter().map(|p| p.subpaths.iter().map(&mut measure).collect()).collect() +} + impl Sub { + /// Subpath `sp`, its MoveTo at `start` in `els`, measured whole, with the tips of `st`. + fn measured(sp: &SubPath, start: usize, els: &[PathEl], st: Option<&StrokeLayer>) -> Self { + let segs = sp.segment_count(); + let mut sub = Sub { start, segs, anchors: sp.anchors.len(), closed: sp.closed, ends: whole_runs(segs), boxes: vec![], tips: vec![] }; + sub.boxes = (0..sub.runs()).map(|r| sub.run_box(els, r)).collect(); + if let Some(st) = st { + sub.tips = (0..sub.runs()).map(|r| sub.run_tips(els, r, st)).collect(); + } + sub + } + + /// Its elements in the path: none without anchors, otherwise its MoveTo, its segments and + /// its ClosePath when closed. + fn len(&self) -> usize { + if self.anchors == 0 { 0 } else { 1 + self.segs + usize::from(self.closed) } + } + /// 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)?)?; @@ -103,12 +172,23 @@ impl Sub { } fn runs(&self) -> usize { - self.segs.div_ceil(RUN) + self.ends.len() + } + + /// The segments of run `r`. + fn range(&self, r: usize) -> std::ops::Range { + let from = r.checked_sub(1).and_then(|p| self.ends.get(p)).copied().unwrap_or(0); + from..self.ends.get(r).copied().unwrap_or(from) + } + + /// The run that holds segment `i` (the last run for a segment past the end). + fn run_of(&self, i: usize) -> usize { + self.ends.partition_point(|&e| e <= i).min(self.runs().saturating_sub(1)) } /// 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)) + self.range(r).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 @@ -140,7 +220,7 @@ impl Sub { /// ([`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) { + for i in self.range(r) { 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) { @@ -149,6 +229,229 @@ impl Sub { } out } + + /// Bring the subpath, its MoveTo now at `at` in `els`, up to date from `was` to `now`, with the + /// tips of `st`. + fn update(&mut self, was: &SubPath, now: &SubPath, at: usize, els: &mut Vec, st: Option<&StrokeLayer>) { + self.start = at; + if was.anchors.len() == now.anchors.len() && was.closed == now.closed { + self.moved(was, now, els, st); + } else if was.anchors.len() < 2 || now.anchors.len() < 2 { + self.remade(now, at, self.len(), els, st); + } else { + let mut span = self.len(); + if self.spliced(was, now, els, st, &mut span).is_none() { + self.remade(now, at, span, els, st); + } + } + } + + /// Write the elements of `now` over the `span` elements at `at` in `els` that the subpath holds, + /// and measure it whole with the tips of `st`. + fn remade(&mut self, now: &SubPath, at: usize, span: usize, els: &mut Vec, st: Option<&StrokeLayer>) { + let to = at.saturating_add(span).min(els.len()); + let len = if now.anchors.is_empty() { 0 } else { 1 + now.segment_count() + usize::from(now.closed) }; + els.splice(at.min(to)..to, (0..len).map(|o| element_at(now, o))); + *self = Sub::measured(now, at, els, st); + } + + /// [`Self::update`] for a subpath with the same anchor count and closed state: 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. + fn moved(&mut self, was: &SubPath, now: &SubPath, els: &mut [PathEl], st: Option<&StrokeLayer>) { + let (mut first, mut segs) = (false, vec![]); + for (k, (a, b)) in was.anchors.iter().zip(&now.anchors).enumerate() { + if same(a, b) { + continue; + } + if k >= 1 && k - 1 < self.segs { + segs.push(k - 1); + } + if k == 0 { + first = true; + if self.closed && self.segs > 0 { + segs.push(self.segs - 1); + } + } + if k < self.segs { + segs.push(k); + } + } + if !first && segs.is_empty() { + return; + } + segs.sort_unstable(); + segs.dedup(); + if first && let (Some(a), Some(el)) = (now.anchors.first(), els.get_mut(self.start)) { + *el = PathEl::MoveTo(a.p); + } + for &i in &segs { + if let Some(el) = els.get_mut(self.start + 1 + i) { + *el = element(now, i); + } + } + let els: &[PathEl] = els; + // Many moved: the subpath is measured again whole. + let all = segs.len() * 4 > self.segs; + let mut boxes: Vec = if all { (0..self.runs()).collect() } else { segs.iter().map(|&i| self.run_of(i)).collect() }; + boxes.dedup(); + for r in boxes { + let b = self.run_box(els, r); + if let Some(slot) = self.boxes.get_mut(r) { + *slot = b; + } + } + let Some(st) = st else { return }; + // 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(self.run_of(i)); + match self.prev_drawn(els, i) { + Ok(Some(j)) => runs.push(self.run_of(j)), + Ok(None) => {} + Err(()) => whole = true, + } + } + self.measure_tips(els, runs, whole, st); + } + + /// [`Self::update`] for a subpath of at least two anchors before and after that gained or lost + /// anchors or was closed or opened. Its elements from the first anchor that changed to the + /// last are written again, and the closing segment and the ClosePath when the first or last + /// anchor changed or it was closed or opened; the elements before and after stay. The runs + /// that held the segments written are measured again, with the tips of the runs of the + /// segments with length before them. `span` is the count of elements the subpath holds in + /// `els`, kept up to date as they are written. `None` when the segments and runs kept do not + /// fit `was`: the caller then makes the subpath again from those `span` elements. + fn spliced(&mut self, was: &SubPath, now: &SubPath, els: &mut Vec, st: Option<&StrokeLayer>, span: &mut usize) -> Option<()> { + let (n0, n1) = (was.anchors.len(), now.anchors.len()); + let least = n0.min(n1); + // On a subpath open before and after, the handle before the first anchor and the one after + // the last draw nothing. + let open = !was.closed && !now.closed; + let a = was.anchors.iter().zip(&now.anchors).enumerate().take_while(|(i, (x, y))| same_drawn(x, y, !open || *i > 0, true)).count().min(least); + let b = was + .anchors + .iter() + .rev() + .zip(now.anchors.iter().rev()) + .take(least - a) + .enumerate() + .take_while(|(i, (x, y))| same_drawn(x, y, true, !open || *i > 0)) + .count(); + let (s0, s1) = (self.segs, now.segment_count()); + let (c0, c1) = (self.closed, now.closed); + let (len0, len1) = (1 + s0 + usize::from(c0), 1 + s1 + usize::from(c1)); + // Elements `a..mid` change: the MoveTo when the first anchor changed and the segments up + // to the first of those between the last `b` anchors. + let (mid0, mid1) = ((1 + n0.checked_sub(b)?).min(1 + s0), (1 + n1.checked_sub(b)?).min(1 + s1)); + // Elements `tail..len` follow the segments between the last `b` anchors: the closing + // segment and the ClosePath. They stay when the anchors either side of the closing segment + // stayed and the subpath was neither closed nor opened. + let (tail0, tail1) = if b >= 1 { (n0, n1) } else { (mid0, mid1) }; + let keep_tail = c0 == c1 && (!c1 || b == 0 || a >= 1); + // The `old` segments from segment `lo` become `new` segments, and when the tail is written, + // the segments from the one before it do too. + let lo = a.saturating_sub(1); + let (old, new) = (mid0.checked_sub(1)?.checked_sub(lo)?, mid1.checked_sub(1)?.checked_sub(lo)?); + let tail = match keep_tail { + true => None, + false => Some((tail0.checked_sub(1)?, (1 + s0).saturating_sub(tail0), (1 + s1).saturating_sub(tail1), tail1.checked_sub(1)?)), + }; + let at = self.start; + let mut stale = vec![false; self.runs()]; + let tips = st.is_some(); + // Writes elements `e0..e1` of `now` over elements `o0..o1` of the subpath and updates `span`. + let mut write = |els: &mut Vec, (o0, o1): (usize, usize), (e0, e1): (usize, usize)| { + let to = at.saturating_add(o1).min(els.len()); + let from = at.saturating_add(o0).min(to); + els.splice(from..to, (e0..e1).map(|o| element_at(now, o))); + *span = from.saturating_sub(at) + e1.saturating_sub(e0) + at.saturating_add(*span).saturating_sub(to); + }; + if let Some((lo, old, new, _)) = tail { + self.resize(lo, old, new, &mut stale, tips)?; + write(els, (tail0, len0), (tail1, len1)); + } + self.resize(lo, old, new, &mut stale, tips)?; + write(els, (a, mid0), (a, mid1)); + self.anchors = n1; + self.closed = c1; + let els: &[PathEl] = els; + for (r, _) in stale.iter().enumerate().filter(|(_, s)| **s) { + let b = self.run_box(els, r); + if let Some(slot) = self.boxes.get_mut(r) { + *slot = b; + } + } + let Some(st) = st else { return Some(()) }; + let mut runs: Vec = stale.iter().enumerate().filter(|(_, s)| **s).map(|(r, _)| r).collect(); + // Closed or opened: the tips of every joint change when the stroke reaches farther from one + // than from the other. + let mut whole = c0 != c1 && st.side_reach(c0).to_bits() != st.side_reach(c1).to_bits(); + for i in [lo].into_iter().chain(tail.map(|(.., last)| last)) { + match self.prev_drawn(els, i) { + Ok(Some(j)) => runs.push(self.run_of(j)), + Ok(None) => {} + Err(()) => whole = true, + } + } + self.measure_tips(els, runs, whole, st); + Some(()) + } + + /// Replace the `old` segments from segment `lo` with `new` segments in the runs: the runs that + /// held them become one run, split into runs of [`RUN`] segments when longer than twice that, + /// whose boxes and (with `tips`) tips are marked in `stale` to be measured again. `None`, and + /// nothing changed, when the runs kept do not hold those `old` segments. + fn resize(&mut self, lo: usize, old: usize, new: usize, stale: &mut Vec, tips: bool) -> Option<()> { + if old == 0 && new == 0 { + return Some(()); + } + let (r0, r1, first, last) = match self.runs() { + 0 => (0, 0, 0, new), + _ => { + let r0 = self.run_of(lo); + let r1 = if old == 0 { r0 } else { self.run_of(lo + old - 1) }; + let last = self.ends.get(r1).copied().unwrap_or(lo + old).checked_add(new)?.checked_sub(old)?; + (r0, r1 + 1, self.range(r0).start, last) + } + }; + let len = last.checked_sub(first)?; + let segs = self.segs.checked_add(new)?.checked_sub(old)?; + let after: Vec = self.ends.get(r1..)?.iter().map(|&e| e.checked_add(new)?.checked_sub(old)).collect::>()?; + if r1 > self.boxes.len() || r1 > stale.len() { + return None; + } + let count = if len > 2 * RUN { len.div_ceil(RUN) } else { usize::from(len > 0) }; + self.ends.truncate(r1); + self.ends.extend(after); + self.ends.splice(r0..r1, (1..=count).map(|j| if j == count { last } else { first + j * RUN })); + self.boxes.splice(r0..r1, std::iter::repeat_n(None, count)); + if tips { + self.tips.splice(r0.min(self.tips.len())..r1.min(self.tips.len()), std::iter::repeat_n(None, count)); + } + stale.splice(r0..r1, std::iter::repeat_n(true, count)); + self.segs = segs; + Some(()) + } + + /// Measure again the tips of `st` in `runs`, or in every run when `whole`. + fn measure_tips(&mut self, els: &[PathEl], mut runs: Vec, whole: bool, st: &StrokeLayer) { + if whole { + runs = (0..self.runs()).collect(); + } + runs.sort_unstable(); + runs.dedup(); + for r in runs { + let t = self.run_tips(els, r, st); + if let Some(slot) = self.tips.get_mut(r) { + *slot = t; + } + } + } } /// What [`KeptPath::tips`] measures `st`'s tips by: its reach from an open and a closed path and @@ -157,6 +460,24 @@ fn tips_key(st: &StrokeLayer) -> [u64; 3] { [st.side_reach(false).to_bits(), st.side_reach(true).to_bits(), st.miter_limit.to_bits()] } +/// Whether the subpaths of `was` and `now` differ: in number, in anchors or in being closed. +fn differs(was: &PathData, now: &PathData) -> bool { + !std::ptr::eq(was, now) + && (was.subpaths.len() != now.subpaths.len() + || was.subpaths.iter().zip(&now.subpaths).any(|(w, p)| { + w.closed != p.closed || w.anchors.len() != p.anchors.len() || w.anchors.iter().zip(&p.anchors).any(|(a, b)| !same(a, b)) + })) +} + +/// The subpaths of `was` and `now`, paths with as many subpaths, whose anchors were added or +/// deleted or that were closed or opened. +fn splices(was: &PathData, now: &PathData) -> usize { + if std::ptr::eq(was, now) || was.subpaths.len() != now.subpaths.len() { + return 0; + } + was.subpaths.iter().zip(&now.subpaths).filter(|(w, p)| w.anchors.len() != p.anchors.len() || w.closed != p.closed).count() +} + impl KeptPath { /// The kept path of `n`, or `None` when it has none ([`kept`]). pub(crate) fn new(n: &Arc, stamp: u64) -> Option { @@ -170,12 +491,8 @@ impl KeptPath { 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); - } + let sub = Sub::measured(sp, start, els, None); + start += sub.len(); subs.push(sub); } parts.push(subs); @@ -183,106 +500,90 @@ impl KeptPath { (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. + /// Bring the path up to date with `n`, a later version of the object it was made from. The + /// subpaths whose anchors moved change as [`Sub::moved`] says, and those whose anchors were + /// added or deleted or that were closed or opened as [`Sub::spliced`] says. A member of a + /// compound path that gained or lost subpaths is made again, and so are the members added + /// between the first and the last member that the edit left as they were, when the edit added + /// or removed members. False, and nothing changed, when `n` has no kept path, or when the edit + /// added or deleted anchors in or closed or opened more than [`SPLICES`] subpaths, or when the + /// members kept do not pair with those of `n`. A subpath whose segments or runs kept do not fit + /// its anchors before the edit is made again whole ([`Sub::spliced`]). False as well + /// when the path written does not end where the subpaths measured end. The renderer makes the + /// path again whole after a false. 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) { + if !kept(n) || self.parts.len() != was.len() || self.parts.iter().zip(&was).any(|(s, p)| s.len() != p.subpaths.len()) { 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)); - } - } + // The members paired with one of `was`: all when there are as many, otherwise those + // before the first and after the last that is not the same. The others are made again. + let (head, tail) = if was.len() == now.len() { + (was.len(), 0) + } else { + let head = was.iter().zip(&now).take_while(|(w, p)| std::ptr::eq(**w, **p)).count(); + let rest = (was.len() - head).min(now.len() - head); + (head, was.iter().rev().zip(now.iter().rev()).take(rest).take_while(|(w, p)| std::ptr::eq(**w, **p)).count()) + }; + // Where the members that are not paired end in `was` and in `now`. + let (Some(was_end), Some(now_end)) = (was.len().checked_sub(tail), now.len().checked_sub(tail)) else { return false }; + if head > was_end || head > now_end { + return false; + } + let paired = |i: usize| match i < head { + true => Some(i), + false => now.len().checked_sub(i).filter(|&back| back <= tail).and_then(|back| was.len().checked_sub(back)), + }; + let spliced: usize = (0..now.len()).filter_map(|i| paired(i).map(|w| splices(was[w], now[i]))).sum(); + if spliced > SPLICES { + return false; } self.node = n.clone(); - if moved.is_empty() { + // Nothing to change: the path is left as it is, shared or not. + if was.len() == now.len() && was.iter().zip(&now).all(|(w, p)| !differs(w, p)) { return true; } + let st = self.tips_of.clone(); + let st = st.as_ref(); 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 { + let mut els = std::mem::take(path).into_elements(); + let mut old_parts: Vec>> = std::mem::take(&mut self.parts).into_iter().map(Some).collect(); + let mut at = 0; + let mut parts = Vec::with_capacity(now.len()); + for (i, p) in now.iter().enumerate() { + if i == head { + // The members of `was` that are not paired give way to those of `now`. + let gone: usize = + old_parts.get_mut(head..was_end).into_iter().flatten().flat_map(|s| s.take().into_iter().flatten()).map(|s| s.len()).sum(); + parts.extend(made_again(now.get(head..now_end).unwrap_or_default(), gone, &mut els, &mut at, st)); + } + let Some(w) = paired(i) else { continue }; + let Some(mut subs) = old_parts.get_mut(w).and_then(Option::take) else { continue }; + if was[w].subpaths.len() != p.subpaths.len() { + let len = subs.iter().map(Sub::len).sum(); + parts.extend(made_again(&[*p], len, &mut els, &mut at, st)); 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 moved = !std::ptr::eq(was[w], *p); + for (sub, (ws, ps)) in subs.iter_mut().zip(was[w].subpaths.iter().zip(&p.subpaths)) { + if moved { + sub.update(ws, ps, at, &mut els, st); } + sub.start = at; + at += sub.len(); } + parts.push(subs); } - 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); - } + if head == now.len() && head < was.len() { + // Members were removed from the end. + let gone: usize = old_parts[head..].iter_mut().flat_map(|s| s.take().into_iter().flatten()).map(|s| s.len()).sum(); + parts.extend(made_again(&[], gone, &mut els, &mut at, st)); } - true + let made = at == els.len(); + *path = BezPath::from_vec(els); + self.parts = parts; + made } /// The object's geometric bounds ([`Node::geometric_bounds`]): each path's as @@ -318,9 +619,25 @@ impl KeptPath { 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. + /// Each subpath's MoveTo, segments and anchors, whether it is closed, and the segments of each + /// of its runs. #[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()) + pub(crate) fn layout(&self) -> Vec { + let runs = |s: &Sub| (0..s.runs()).map(|r| s.range(r).len()).collect(); + self.parts.iter().flatten().map(|s| ((s.start, s.segs, s.anchors, s.closed), runs(s))).collect() + } + + /// The boxes and tips kept per run of each subpath, the same measured again from the path + /// over the same runs, and the stroke the tips were measured for. + #[cfg(test)] + pub(crate) fn runs(&self) -> (Vec, Vec, Option) { + let els = self.path.elements(); + let kept = self.parts.iter().flatten().map(|s| (s.boxes.clone(), s.tips.clone())).collect(); + let again = self.parts.iter().flatten().map(|s| { + let boxes = (0..s.runs()).map(|r| s.run_box(els, r)).collect(); + let tips = self.tips_of.as_ref().map_or(vec![], |st| (0..s.runs()).map(|r| s.run_tips(els, r, st)).collect()); + (boxes, tips) + }); + (kept, again.collect(), self.tips_of.clone()) } } diff --git a/crates/render/src/tests/keptpath.rs b/crates/render/src/tests/keptpath.rs index 94c5621b9..a2f8b3ad8 100644 --- a/crates/render/src/tests/keptpath.rs +++ b/crates/render/src/tests/keptpath.rs @@ -71,19 +71,43 @@ fn check(r: &mut Renderer, d: &Document, id: NodeId, what: &str) { } let k = r.kept.get(&id).unwrap_or_else(|| panic!("{what}: its path kept")); assert!(std::ptr::eq(Arc::as_ptr(&k.node), n), "{what}: its path kept"); - // The boxes and miter tips kept, run by run, are those of the path measured whole. - let (runs, tips_of) = k.runs(); - let mut whole = crate::kept_paths::KeptPath::new(&Arc::new(n.clone()), 0).unwrap(); - if let Some(st) = &tips_of { - whole.tips(st); + // The subpaths kept are those of the path measured whole, each run holds from one segment to + // twice the segments of a run, and the boxes and miter tips kept, run by run, are those + // measured again from the path. + let whole = crate::kept_paths::KeptPath::new(&Arc::new(n.clone()), 0).unwrap(); + let (layout, fresh) = (k.layout(), whole.layout()); + assert!(layout.iter().map(|l| l.0).eq(fresh.iter().map(|l| l.0)), "{what}: the same subpaths as the path measured whole"); + for ((sub, runs), ((_, segs, ..), _)) in layout.iter().zip(&fresh) { + assert_eq!(runs.iter().sum::(), *segs, "{what}: the runs of {sub:?} hold its segments"); + assert!(runs.iter().all(|&s| (1..=2 * crate::kept_paths::RUN).contains(&s)), "{what}: the runs of {sub:?}: {runs:?}"); + } + let (runs, again, _) = k.runs(); + assert!(runs == again, "{what}: the same boxes and tips as measured again"); +} + +/// Check `d` as [`check`] does, then the document before the edit (undone) and `d` again (redone). +fn check_undo(r: &mut Renderer, before: &Document, d: &Document, id: NodeId, what: &str) { + check(r, d, id, what); + check(r, before, id, &format!("{what}, undone")); + check(r, d, id, &format!("{what}, redone")); +} + +/// The members of compound path `id` of `d`. +fn members(d: &mut Document, id: NodeId) -> &mut Vec> { + match d.node_mut(id).map(|n| &mut n.kind) { + Some(NodeKind::Compound { children, .. }) => children, + _ => panic!("a compound path"), } - assert!(runs == whole.runs().0, "{what}: the same boxes and tips as the path measured whole"); } fn path_of(d: &mut Document, id: NodeId) -> &mut PathData { d.node_mut(id).and_then(|n| n.path_data_mut()).unwrap() } +fn corner(x: f64, y: f64) -> Anchor { + Anchor::corner(Point::new(x, y)) +} + fn moved(a: &mut Anchor, by: Vec2) { *a = a.transform(Affine::translate(by)); } @@ -194,6 +218,352 @@ fn edits_in_runs_of_segments_without_length_measure_as_when_the_path_is_made_aga } } +#[test] +fn a_long_path_draws_and_culls_after_anchors_are_added_or_deleted_as_when_its_path_is_made_again() { + let mut d = Document::new(1000.0, 1000.0); + let path = PathData::new(vec![ + zigzag(3000, 0.0), + ring(1200, Point::new(12_000.0, 15.0), 45.0), + SubPath::new(vec![Anchor::corner(Point::new(11_990.0, 20.0))], false), + SubPath::new(vec![], false), + SubPath::new(vec![Anchor::corner(Point::new(11_995.0, 0.0)), Anchor::corner(Point::new(12_005.0, 30.0))], false), + ]); + let n = Node::path(d.alloc_id(), path, look(vec![stroke(6.0, LineJoin::Miter, LineCap::Butt), stroke(14.0, LineJoin::Round, LineCap::Round)])); + let id = add(&mut d, n); + let mut r = Renderer::new(); + check(&mut r, &d, id, "drawn first"); + type Edit = fn(&mut PathData); + let edits: [(&str, Edit); 28] = [ + // Anchor 1,152, a corner at (9,032, 30), ends the segments of the first nine runs: the + // joint there is in the run before. + ("an anchor added where a run of segments starts", |p| p.subpaths[0].anchors.insert(1153, corner(9_036.0, -30.0))), + ("an anchor added", |p| _ = p.subpaths[0].insert_anchor(1520, 0.5)), + ("an anchor deleted", |p| _ = p.subpaths[0].anchors.remove(1521)), + ("an anchor added at the start", |p| p.subpaths[0].anchors.insert(0, corner(-8.0, 30.0))), + ("an anchor added at the end", |p| p.subpaths[0].anchors.push(corner(24_100.0, 0.0))), + ("the first anchor deleted", |p| _ = p.subpaths[0].anchors.remove(0)), + ("the last anchor deleted", |p| _ = p.subpaths[0].anchors.pop()), + ("an anchor added onto the one before it", |p| { + let a = p.subpaths[0].anchors[1499]; + p.subpaths[0].anchors.insert(1500, Anchor::corner(a.p)); + }), + ("300 anchors deleted", |p| _ = p.subpaths[0].anchors.drain(1100..1400)), + ("400 anchors added", |p| { + let at: Vec = (0..400).map(|i| corner(8_800.0 + f64::from(i) * 0.5, if i % 2 == 0 { -20.0 } else { 50.0 })).collect(); + p.subpaths[0].anchors.splice(1100..1100, at); + }), + ("anchors moved and others deleted", |p| { + p.subpaths[0].anchors[10] = corner(80.0, -40.0); + p.subpaths[0].anchors.drain(2000..2010); + }), + ("the subpath closed", |p| p.subpaths[0].closed = true), + ("an anchor added at the start of the closed subpath", |p| p.subpaths[0].anchors.insert(0, corner(-8.0, 60.0))), + ("an anchor added after the first of the closed subpath", |p| p.subpaths[0].anchors.insert(1, corner(-20.0, -30.0))), + ("an anchor added at the end of the closed subpath", |p| p.subpaths[0].anchors.push(corner(24_000.0, 60.0))), + ("the first anchor of the closed subpath deleted", |p| _ = p.subpaths[0].anchors.remove(0)), + ("the last anchor of the closed subpath deleted", |p| _ = p.subpaths[0].anchors.pop()), + ("the subpath opened", |p| p.subpaths[0].closed = false), + ("an anchor of the ring added in the view", |p| p.subpaths[1].anchors.insert(300, corner(12_010.0, 10.0))), + ("an anchor of the ring deleted", |p| _ = p.subpaths[1].anchors.remove(600)), + ("the ring opened", |p| p.subpaths[1].closed = false), + ("the ring closed", |p| p.subpaths[1].closed = true), + ("the one-anchor subpath given a second anchor", |p| p.subpaths[2].anchors.push(corner(12_000.0, 40.0))), + ("the second anchor deleted again", |p| _ = p.subpaths[2].anchors.pop()), + ("the one-anchor subpath emptied", |p| p.subpaths[2].anchors.clear()), + ("the empty subpath given an anchor", |p| p.subpaths[3].anchors.push(corner(12_015.0, 5.0))), + ("the two-anchor subpath closed", |p| p.subpaths[4].closed = true), + ("an anchor deleted from the two-anchor subpath", |p| _ = p.subpaths[4].anchors.remove(0)), + ]; + for (what, edit) in edits { + let before = d.clone(); + edit(path_of(&mut d, id)); + check_undo(&mut r, &before, &d, id, what); + } +} + +#[test] +fn a_long_compound_path_draws_and_culls_after_members_are_added_or_removed_as_when_its_path_is_made_again() { + let mut d = Document::new(1000.0, 1000.0); + let none = Appearance::default(); + let member = |d: &mut Document, p: PathData| Arc::new(Node::path(d.alloc_id(), p, none.clone())); + let children = vec![ + member(&mut d, PathData::single(zigzag(1500, 6000.0))), + member(&mut d, PathData::single(ring(1500, Point::new(12_000.0, 15.0), 45.0))), + member(&mut d, PathData::single(SubPath::new(vec![Anchor::corner(Point::new(11_980.0, 0.0))], false))), + ]; + let mut n = Node::new(d.alloc_id(), NodeKind::Compound { children, rule: FillRule::EvenOdd }); + n.appearance = look(vec![stroke(4.0, LineJoin::Miter, LineCap::Square)]).into(); + let id = add(&mut d, n); + let mut r = Renderer::new(); + check(&mut r, &d, id, "drawn first"); + let edits: [(&str, PathData, usize); 4] = [ + ("a member added at the end", PathData::single(ring(40, Point::new(12_010.0, 20.0), 8.0)), 3), + ("a member added at the start", PathData::single(zigzag(30, 11_950.0)), 0), + ("a member added between", PathData::new(vec![]), 2), + ("a member of two subpaths added between", PathData::new(vec![ring(5, Point::new(11_990.0, 25.0), 4.0), zigzag(3, 11_970.0)]), 1), + ]; + for (what, p, at) in edits { + let before = d.clone(); + let m = member(&mut d, p); + members(&mut d, id).insert(at, m); + check_undo(&mut r, &before, &d, id, what); + } + // Seven members: the first, the last, then the third removed. + for (what, at) in [("the first member removed", 0), ("the last member removed", 5), ("a member removed between", 2)] { + let before = d.clone(); + members(&mut d, id).remove(at); + check_undo(&mut r, &before, &d, id, what); + } + // Left: the member of two subpaths, the zigzag, the ring and the one-anchor member. + let ids: Vec = members(&mut d, id).iter().map(|m| m.id).collect(); + let before = d.clone(); + path_of(&mut d, ids[1]).subpaths.push(SubPath::new(vec![corner(12_020.0, 30.0)], false)); + check_undo(&mut r, &before, &d, id, "a subpath added to a member"); + let before = d.clone(); + path_of(&mut d, ids[1]).subpaths[0].anchors.insert(10, corner(12_000.0, 60.0)); + path_of(&mut d, ids[2]).subpaths[0].anchors.remove(700); + check_undo(&mut r, &before, &d, id, "anchors added and deleted in two members"); +} + +#[test] +fn adding_and_deleting_anchors_of_a_long_path_changes_its_kept_path_in_place() { + let mut d = Document::new(1000.0, 1000.0); + let n = Node::path(d.alloc_id(), PathData::single(zigzag(3000, 0.0)), look(vec![stroke(6.0, LineJoin::Miter, LineCap::Butt)])); + let id = add(&mut d, n); + let mut r = Renderer::new(); + render(&mut r, &d); + let kept = |r: &Renderer| r.kept.get(&id).map(|k| Arc::as_ptr(&k.path) as usize); + let before = kept(&r); + assert!(before.is_some(), "the path is kept"); + type Edit = fn(&mut PathData); + let edits: [(&str, Edit); 3] = [ + ("an anchor added", |p| _ = p.subpaths[0].insert_anchor(1500, 0.5)), + ("an anchor deleted", |p| _ = p.subpaths[0].anchors.remove(1700)), + ("the subpath closed", |p| p.subpaths[0].closed = true), + ]; + for (what, edit) in edits { + edit(path_of(&mut d, id)); + check(&mut r, &d, id, what); + assert_eq!(kept(&r), before, "{what}: the kept path changed in place"); + } + // An edit that leaves the anchors as they were leaves the kept path as it is, also while + // something else holds it. + let held = r.kept.get(&id).map(|k| k.path.clone()).unwrap(); + d.node_mut(id).unwrap().appearance = look(vec![stroke(8.0, LineJoin::Miter, LineCap::Butt)]).into(); + check(&mut r, &d, id, "the stroke changed"); + assert!(Arc::ptr_eq(&held, &r.kept.get(&id).unwrap().path), "the stroke changed: the kept path left as it is"); +} + +#[test] +fn deleting_an_anchor_of_an_open_path_and_clearing_the_handles_past_its_ends_measures_one_run_again() { + let mut d = Document::new(1000.0, 1000.0); + let mut sp = zigzag(3000, 0.0); + if let Some(a) = sp.anchors.last_mut() { + a.h_out = a.p + Vec2::new(3.0, 4.0); + } + assert!(sp.anchors[0].has_in(), "the first anchor has a handle before it"); + let n = Node::path(d.alloc_id(), PathData::single(sp), look(vec![stroke(6.0, LineJoin::Miter, LineCap::Butt)])); + let id = add(&mut d, n); + let mut r = Renderer::new(); + render(&mut r, &d); + let runs = |r: &Renderer| r.kept.get(&id).map(|k| k.layout()[0].1.clone()).unwrap(); + let before = runs(&r); + // Anchor 1,500 deleted, and the handles before the first anchor and after the last cleared: + // they draw nothing on an open subpath. + let p = &mut path_of(&mut d, id).subpaths[0]; + p.anchors.remove(1500); + if let Some(a) = p.anchors.first_mut() { + a.h_in = a.p; + } + if let Some(a) = p.anchors.last_mut() { + a.h_out = a.p; + } + check(&mut r, &d, id, "an anchor deleted and the end handles cleared"); + let after = runs(&r); + let changed: Vec = (0..before.len().max(after.len())).filter(|&i| before.get(i) != after.get(i)).collect(); + assert_eq!(changed, vec![1499 / crate::kept_paths::RUN], "only the run that held the anchor changed: {before:?} -> {after:?}"); +} + +/// Numbers from a fixed seed (a linear congruential generator). +struct Seeded(u64); + +impl Seeded { + /// A number below `m` (0 when `m` is 0). + fn below(&mut self, m: usize) -> usize { + self.0 = self.0.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); + (self.0 >> 33) as usize % m.max(1) + } + + /// A whole number of points from -`r` to `r`. + fn offset(&mut self, r: usize) -> f64 { + self.below(2 * r + 1) as f64 - r as f64 + } + + /// Where in a subpath of `n` anchors to edit: at the start, at the end, where a run of segments + /// ends, or anywhere. + fn at(&mut self, n: usize) -> usize { + match self.below(4) { + 0 => self.below(3).min(n), + 1 => n.saturating_sub(self.below(3)), + 2 => (crate::kept_paths::RUN * (1 + self.below(24)) + self.below(3)).min(n), + _ => self.below(n + 1), + } + } +} + +/// An anchor to add after `before`: on it (a segment without length), a corner near it, or a +/// smooth anchor near it. +fn anchor_after(g: &mut Seeded, before: Point) -> Anchor { + let p = before + Vec2::new(g.offset(20), g.offset(40)); + match g.below(5) { + 0 => Anchor::corner(before), + 1 => Anchor::smooth(p, p + Vec2::new(g.offset(6), g.offset(6))), + _ => Anchor::corner(p), + } +} + +/// Edit `p` at random: anchors added or deleted at the start, middle or end of a subpath, a +/// subpath closed or opened, an anchor or a handle moved, or many anchors moved. Says what it did. +fn random_edit(g: &mut Seeded, p: &mut PathData) -> String { + let s = g.below(p.subpaths.len()); + let Some(sp) = p.subpaths.get_mut(s) else { return "nothing".into() }; + let n = sp.anchors.len(); + match g.below(6) { + 0 | 1 => { + let (at, k) = (g.at(n), 1 + g.below(3)); + for j in 0..k { + let before = sp.anchors.get((at + j).saturating_sub(1)).map_or(Point::new(12_000.0, 15.0), |a| a.p); + let a = anchor_after(g, before); + sp.anchors.insert(at + j, a); + } + format!("{k} anchors added at {at} of subpath {s} of {n}") + } + 2 | 3 if n > 0 => { + let at = g.at(n - 1); + let k = (1 + g.below(3)).min(n - at); + sp.anchors.drain(at..at + k); + format!("{k} anchors deleted at {at} of subpath {s} of {n}") + } + 4 => { + sp.closed = !sp.closed; + format!("subpath {s} of {n} closed: {}", sp.closed) + } + _ if n > 0 => { + let at = g.below(n); + match g.below(3) { + 0 => moved(&mut sp.anchors[at], Vec2::new(g.offset(30), g.offset(30))), + 1 => sp.anchors[at].h_out = sp.anchors[at].p + Vec2::new(g.offset(10), g.offset(10)), + _ => sp.anchors[at..].iter_mut().take(n / 10).for_each(|a| moved(a, Vec2::new(1.0, -1.0))), + } + format!("anchors moved at {at} of subpath {s} of {n}") + } + _ => "nothing".into(), + } +} + +/// Bring `k` up to date with `n` and check that it holds what [`KeptPath::new`] makes from `n`: +/// the same path elements and subpaths, runs that hold each subpath's segments, the boxes and miter +/// tips of `st` measured again over its runs, and the same bounds and tips in all. +fn same_as_made_again(k: &mut crate::kept_paths::KeptPath, n: &Arc, st: &StrokeLayer, what: &str) { + assert!(k.update(n), "{what}: brought up to date"); + let mut fresh = crate::kept_paths::KeptPath::new(n, 0).unwrap_or_else(|| panic!("{what}: kept")); + assert!(*k.path == *fresh.path, "{what}: the same path elements"); + let (layout, whole) = (k.layout(), fresh.layout()); + assert!(layout.iter().map(|l| l.0).eq(whole.iter().map(|l| l.0)), "{what}: the same subpaths: {layout:?}"); + for ((sub, runs), ((_, segs, ..), _)) in layout.iter().zip(&whole) { + assert_eq!(runs.iter().sum::(), *segs, "{what}: the runs of {sub:?} hold its segments"); + assert!(runs.iter().all(|&s| (1..=2 * crate::kept_paths::RUN).contains(&s)), "{what}: the runs of {sub:?}: {runs:?}"); + } + let (runs, again, _) = k.runs(); + assert!(runs == again, "{what}: the same boxes and tips as measured again"); + assert_eq!(k.geometric(), fresh.geometric(), "{what}: the same bounds"); + assert_eq!(k.tips(st), fresh.tips(st), "{what}: the same miter tips"); +} + +#[test] +fn random_edits_of_a_long_path_keep_what_making_its_path_again_makes() { + let st = stroke(6.0, LineJoin::Miter, LineCap::Butt); + let mut g = Seeded(5744); + let closed = |sp: SubPath| SubPath::new(sp.anchors, true); + let path = PathData::new(vec![ + zigzag(2, 11_990.0), + closed(zigzag(3, 12_000.0)), + zigzag(7, 11_960.0), + closed(zigzag(40, 11_800.0)), + ring(300, Point::new(12_000.0, 15.0), 60.0), + zigzag(3000, 0.0), + closed(zigzag(2000, 4000.0)), + ]); + let mut n = Arc::new(Node::path(NodeId(1), path, look(vec![st.clone()]))); + let mut k = crate::kept_paths::KeptPath::new(&n, 0).unwrap(); + k.tips(&st); + for step in 0..300 { + let mut next = (*n).clone(); + let what = random_edit(&mut g, next.path_data_mut().unwrap()); + n = Arc::new(next); + same_as_made_again(&mut k, &n, &st, &format!("step {step}, {what}")); + } +} + +#[test] +fn random_edits_of_a_long_compound_path_keep_what_making_its_path_again_makes() { + let st = stroke(4.0, LineJoin::Miter, LineCap::Square); + let mut g = Seeded(1050); + let ids = std::cell::Cell::new(10); + let member = |p: PathData| { + ids.set(ids.get() + 1); + Arc::new(Node::path(NodeId(ids.get()), p, Appearance::default())) + }; + let children = vec![ + member(PathData::new(vec![SubPath::new(zigzag(3, 11_990.0).anchors, true), zigzag(5, 12_010.0)])), + member(PathData::single(zigzag(3000, 0.0))), + member(PathData::single(ring(40, Point::new(12_000.0, 15.0), 30.0))), + member(PathData::single(zigzag(2, 12_020.0))), + ]; + let mut n = Node::new(NodeId(1), NodeKind::Compound { children, rule: FillRule::EvenOdd }); + n.appearance = look(vec![st.clone()]).into(); + let mut n = Arc::new(n); + let mut k = crate::kept_paths::KeptPath::new(&n, 0).unwrap(); + k.tips(&st); + for step in 0..300 { + let mut next = (*n).clone(); + let NodeKind::Compound { children, .. } = &mut next.kind else { panic!("a compound path") }; + let what = match g.below(8) { + 0 => { + let at = [0, children.len(), g.below(children.len() + 1)][g.below(3)]; + let size = [1, 2, 4, 30, 200][g.below(5)]; + let mut sp = zigzag(size, 11_900.0 + g.offset(100)); + sp.closed = g.below(2) == 0; + children.insert(at, member(PathData::single(sp))); + format!("a member of {size} anchors added at {at}") + } + // The member of 3,000 anchors stays, so that the path stays kept. + 1 if children.len() > 2 => { + let at = [0, children.len() - 1, g.below(children.len())][g.below(3)]; + if children[at].path_data().is_some_and(|p| p.subpaths.iter().map(|s| s.anchors.len()).sum::() > 1000) { + "nothing".into() + } else { + children.remove(at); + format!("member {at} removed") + } + } + 2 => { + let at = g.below(children.len()); + let m = Arc::make_mut(&mut children[at]).path_data_mut().unwrap(); + m.subpaths.push(zigzag(1 + g.below(4), 12_030.0)); + format!("a subpath added to member {at}") + } + _ => { + let at = g.below(children.len()); + let what = random_edit(&mut g, Arc::make_mut(&mut children[at]).path_data_mut().unwrap()); + format!("member {at}: {what}") + } + }; + n = Arc::new(next); + same_as_made_again(&mut k, &n, &st, &format!("step {step}, {what}")); + } +} + #[test] fn moving_one_anchor_of_a_long_path_changes_its_kept_path_in_place() { let mut d = Document::new(1000.0, 1000.0); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 27b63a921..305120225 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1280,6 +1280,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur With more than 10,000 type on a path objects selected, the Selection and Direct Selection tools neither draw nor hit-test the objects' brackets. The renderer outlines the stroke of a path object with more than 1,024 path elements, without dashes or a width profile, only in and around the view, with a margin of the view's size on each side, and outlines it again when the view moves past that margin. The pixels are the same. At 1600 × 1200, the frame after one anchor of an open path of 200,000 anchors moves takes 28 to 30 ms instead of 103 ms, and with a closed path of 50,000 anchors 10 ms instead of 30 ms (`cargo run --release -p vectorcraft-engine --example anchor_bench`). The renderer outlines a dashed stroke of such a path the same way, without a width profile, and draws its dashes and dots in the same places as when the whole path is dashed, so the pixels are the same. A pattern too fine to draw is drawn solid, as before, and its stroke is outlined near the view too. At 1600 × 1200, the frame after one anchor moves takes 14 ms instead of 102 ms with a filled closed path of 50,000 anchors and a stroke of 6 pt dashes and 4 pt gaps (`cargo run --release -p vectorcraft-engine --example anchor_bench`, which now includes that document). + When anchors of a path or compound path of 1,024 anchors or more are added or deleted, or a subpath is closed or opened, the next frame writes again only the path elements from the first anchor that changed to the last, and the closing segment when an end anchor changed or the subpath was closed or opened. It measures again only the runs of segments that held them; a run grows or shrinks with the segments added or deleted, and one that grows past 256 segments is split. When a member gains or loses subpaths, the renderer makes the path of that member again. When a compound path gains or loses members, it makes the path of those members and of every member between the first and the last of them again. An edit that adds or deletes anchors in, closes or opens more than 16 subpaths makes the whole path again. The pixels and the cull bounds are the same. The renderer keeps the path of a path or compound path of 1,024 anchors or more by object, with the bounds of its segments and of its miter spikes in runs of 128 segments. When anchors move and the subpaths and anchor counts stay the same, the next frame changes only the segments that start or end at a moved anchor and measures again only the runs that hold them or the segments with length before them, instead of making the whole path again and measuring it whole for culling. The pixels and the cull bounds are the same. On Windows at 1600 × 1200, the frame after one anchor moves takes 8.2 ms instead of 30.1 ms with an open path of 200,000 anchors, 4.3 ms instead of 10.4 ms with a closed path of 50,000 anchors, and 6.6 ms instead of 11.9 ms with the filled closed path of 50,000 anchors and a dashed stroke (medians of five runs of `anchor_bench`). When a path object moves without changing shape, the renderer draws its stroke from the outline it made before the move, moved by the same offset. It outlines the stroke again when the path is scaled, rotated or edited, when the stroke changes, and when the move takes the view past the part of a long path it outlined. The pixels are the same. At 1600 × 1200, the frame after a 1 pt nudge takes 123 ms instead of 403 ms with an open path of 200,000 anchors in view (103 ms with nothing changed), and 13 ms instead of 26 ms with a closed path of 50,000 anchors whose stroke is dashed and has a width profile. When a path object moves without changing shape, the renderer draws its stroke from the outline it made before the move, moved by the same offset. It outlines the stroke again when the path is scaled, rotated or edited, when the stroke changes, and when the move takes the view past the part of a long path it outlined. The pixels are the same. At 1600 × 1200, the frame after a 1 pt nudge takes 128 ms instead of 405 ms with an open path of 200,000 anchors in view (104 ms with nothing changed), and 14 ms instead of 28 ms with a closed path of 50,000 anchors whose stroke is dashed and has a width profile.