diff --git a/crates/doc/src/mem.rs b/crates/doc/src/mem.rs index 464e1b3f65..68aaedd68e 100644 --- a/crates/doc/src/mem.rs +++ b/crates/doc/src/mem.rs @@ -281,7 +281,13 @@ fn node_bytes(n: &Node, other: Option<&Node>, counted: &mut HashSet) -> u }); } b += match &n.kind { - NodeKind::Path { path, live, .. } => path_bytes(path) + boxed(live), + NodeKind::Path { path, live, .. } => { + let was = other.and_then(|o| match &o.kind { + NodeKind::Path { path, .. } => Some(path), + _ => None, + }); + path_bytes_besides(path, was) + boxed(live) + } NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } @@ -298,8 +304,30 @@ fn node_bytes(n: &Node, other: Option<&Node>, counted: &mut HashSet) -> u } fn path_bytes(p: &PathData) -> usize { - let anchors: usize = - p.subpaths.iter().map(|s| if s.anchors.capacity() == 0 { 0 } else { ALLOC + s.anchors.capacity() * size_of::() }).sum(); + path_bytes_besides(p, None) +} + +/// [`path_bytes`] without the anchor chunks that `other` (another version of the path) holds too: +/// a version made by changing one anchor shares every chunk but the one it changed. +fn path_bytes_besides(p: &PathData, other: Option<&PathData>) -> usize { + let mut shared: Vec = + other.map(|o| o.subpaths.iter().flat_map(|s| s.anchors.chunk_allocations()).map(|(id, _)| id).collect()).unwrap_or_default(); + shared.sort_unstable(); + let chunk = ALLOC + ARC + size_of::>() + ALLOC; + let anchors: usize = p + .subpaths + .iter() + .map(|s| { + let table = s.anchors.table_bytes(); + let own: usize = s + .anchors + .chunk_allocations() + .filter(|(id, _)| shared.binary_search(id).is_err()) + .map(|(_, capacity)| chunk + capacity * size_of::()) + .sum(); + if table == 0 { own } else { ALLOC + table + own } + }) + .sum(); anchors + if p.subpaths.capacity() == 0 { 0 } else { ALLOC + p.subpaths.capacity() * size_of::() } } diff --git a/crates/engine/src/cmd/addons/pathtools/tests.rs b/crates/engine/src/cmd/addons/pathtools/tests.rs index afa232a7ce..7d11eedd1a 100644 --- a/crates/engine/src/cmd/addons/pathtools/tests.rs +++ b/crates/engine/src/cmd/addons/pathtools/tests.rs @@ -1225,7 +1225,7 @@ fn expand_dashes_keeps_the_fill_below_and_follows_aligned_corners() { let mut centred = 0; for id in ids(&r) { let sp = path(&s, id).subpaths[0].clone(); - if let [a, b, c] = sp.anchors.as_slice() + if let [a, b, c] = &*sp.anchors.to_vec() && corners.iter().any(|k| b.p.distance(*k) < 1e-6) { assert!((a.p.distance(b.p) - 5.0).abs() < 1e-6 && (c.p.distance(b.p) - 5.0).abs() < 1e-6, "{sp:?}"); diff --git a/crates/engine/src/cmd/cut.rs b/crates/engine/src/cmd/cut.rs index 48f2c31e62..43f3aec881 100644 --- a/crates/engine/src/cmd/cut.rs +++ b/crates/engine/src/cmd/cut.rs @@ -356,7 +356,8 @@ fn mirror_open(sp: &SubPath, axis: &Axis, m: Affine) -> Vec { if !end_on { run.reverse(); } - let Some((last, body)) = run.anchors.split_last() else { return vec![] }; + let run = run.anchors.into_vec(); + let Some((last, body)) = run.split_last() else { return vec![] }; let q = axis.project(last.p); let mut anchors = body.to_vec(); anchors.push(Anchor::with_handles(q, last.h_in, m * last.h_in)); diff --git a/crates/engine/src/cmd/draw2/freehand.rs b/crates/engine/src/cmd/draw2/freehand.rs index 45c25eb72c..977f3f921a 100644 --- a/crates/engine/src/cmd/draw2/freehand.rs +++ b/crates/engine/src/cmd/draw2/freehand.rs @@ -46,7 +46,7 @@ fn fit_freehand(pts: &[Point], tol: f64, closed: bool) -> SubPath { let sp = vectorcraft_tools::draw2::catmull_rom(&marked, false); if sp.anchors.len() < 3 { // Two anchors closed would run back over the line itself. - return SubPath::new(sp.anchors, closed && n > 2); + return SubPath::of(sp.anchors, closed && n > 2); } let fitted = po::simplify_with( &PathData::single(sp.clone()), @@ -62,7 +62,7 @@ fn fit_freehand(pts: &[Point], tol: f64, closed: bool) -> SubPath { f.h_in = f.p; } } - SubPath::new(anchors, closed) + SubPath::of(anchors, closed) } /// The path of a freehand stroke: fitted at `tol` ([`fit_freehand`]), or with no `tol` the points @@ -133,13 +133,13 @@ fn redraw_section(pd: &PathData, pts: &[Point], tol: Option, within: f64) - *s0 = s1.p; let (Some(e0), Some(e1)) = (stroke.last_mut(), sp.anchors.get(end)) else { return None }; *e0 = e1.p; - let fit = stroke_subpath(&stroke, tol, false).anchors; + let fit = stroke_subpath(&stroke, tol, false).anchors.into_vec(); let (Some(f0), Some(f1)) = (fit.first().copied(), fit.last().copied()) else { return None }; let inner = fit.get(1..fit.len().saturating_sub(1)).unwrap_or_default(); let (before, after): (Vec, Vec) = if sp.closed { ((0..=(start + n - end) % n).filter_map(|k| sp.anchors.get((end + k) % n).copied()).collect(), vec![]) } else { - (sp.anchors.get(..=start)?.to_vec(), sp.anchors.get(end..)?.to_vec()) + (sp.anchors.get_range(..=start)?, sp.anchors.get_range(end..)?) }; let mut anchors = before; if let Some(a) = anchors.last_mut() { @@ -316,12 +316,12 @@ pub(crate) fn freehand_brushed(s: &mut Session, p: &Value, brush_name: Option<&s // outgoing handle. let mut other_len = 0.0; if let Some((_, _, y, _)) = &connect - && let Some((y0, rest)) = y.anchors.split_first() + && let Some(y0) = y.anchors.first() { if let Some(last) = sp.anchors.last_mut() { *last = Anchor::with_handles(y0.p, last.h_in + (y0.p - last.p), y0.h_out); } - sp.anchors.extend_from_slice(rest); + sp.anchors.extend(y.anchors.iter().skip(1)); other_len = subpath_len(y); } // What the pen recorded along the path (in the direction it is continued in), followed by diff --git a/crates/engine/src/cmd/draw2/smooth.rs b/crates/engine/src/cmd/draw2/smooth.rs index be7db9a1ae..883c9ba5ed 100644 --- a/crates/engine/src/cmd/draw2/smooth.rs +++ b/crates/engine/src/cmd/draw2/smooth.rs @@ -274,7 +274,7 @@ fn refit_run(anchors: &[Anchor], tol: f64, sigma: f64, free: [bool; 2]) -> Vec bool, join_kind: && before > 2 && picked(0) && picked(before - 1) - && let [first, .., last] = sp.anchors.as_mut_slice() - && same(first, last) + && let (Some(&first), Some(&last)) = (sp.anchors.first(), sp.anchors.last()) + && same(&first, &last) { - first.h_in = last.h_in; + if let Some(first) = sp.anchors.first_mut() { + first.h_in = last.h_in; + } sp.anchors.pop(); if let Some(smooth) = join_kind { join_point(sp, 0, smooth); @@ -565,10 +567,12 @@ fn merge_coincident(sp: &mut SubPath, picked: impl Fn(usize) -> bool, join_kind: for i in (1..len).rev() { if picked(i - 1) && picked(i) - && let Some([a, b]) = anchors.get_mut(i - 1..=i) - && same(a, b) + && let (Some(&a), Some(&b)) = (anchors.get(i - 1), anchors.get(i)) + && same(&a, &b) { - a.h_out = b.h_out; + if let Some(a) = anchors.get_mut(i - 1) { + a.h_out = b.h_out; + } if let Some(slot) = gone.get_mut(i) { *slot = true; } @@ -603,7 +607,7 @@ fn merge_coincident(sp: &mut SubPath, picked: impl Fn(usize) -> bool, join_kind: } /// [`join_point`] on anchor `ai` of `anchors` with its neighbors given by index. -fn join_point_between(anchors: &mut [Anchor], ai: usize, prev: Option, next: Option, smooth: bool) { +fn join_point_between(anchors: &mut Anchors, ai: usize, prev: Option, next: Option, smooth: bool) { let at = |j: Option| j.and_then(|j| anchors.get(j)).map(|a| a.p); let (prev, next) = (at(prev), at(next)); if let Some(a) = anchors.get_mut(ai) { @@ -1565,7 +1569,7 @@ enum Stretch { /// anchor, so the two handles of a smooth anchor stay in line with it. That share is below 0 or /// above 1 for a handle long enough beside the span. A closed subpath with no anchor that stays /// moves whole when it has a focal point. -fn stretch_weights(anchors: &[Anchor], closed: bool, roles: &[Stretch]) -> Vec<[f64; 3]> { +fn stretch_weights(anchors: &Anchors, closed: bool, roles: &[Stretch]) -> Vec<[f64; 3]> { let n = anchors.len(); let mut shares = vec![[0.0; 3]; n]; let role = |i: usize| match roles.get(i) { @@ -1891,10 +1895,10 @@ mod tests { && before > 2 && picked(0) && picked(before - 1) - && let [first, .., last] = sp.anchors.as_mut_slice() - && same(first, last) + && let (Some(&first), Some(&last)) = (sp.anchors.first(), sp.anchors.last()) + && same(&first, &last) { - first.h_in = last.h_in; + sp.anchors[0].h_in = last.h_in; sp.anchors.pop(); if let Some(smooth) = join_kind { join_point(sp, 0, smooth); @@ -1903,10 +1907,10 @@ mod tests { for i in (1..sp.anchors.len()).rev() { if picked(i - 1) && picked(i) - && let Some([a, b]) = sp.anchors.get_mut(i - 1..=i) - && same(a, b) + && let (Some(&a), Some(&b)) = (sp.anchors.get(i - 1), sp.anchors.get(i)) + && same(&a, &b) { - a.h_out = b.h_out; + sp.anchors[i - 1].h_out = b.h_out; sp.anchors.remove(i); if let Some(smooth) = join_kind { join_point(sp, i - 1, smooth); diff --git a/crates/engine/src/cmd/symbollib.rs b/crates/engine/src/cmd/symbollib.rs index 7ddc6e4bb2..63096c9d98 100644 --- a/crates/engine/src/cmd/symbollib.rs +++ b/crates/engine/src/cmd/symbollib.rs @@ -384,7 +384,7 @@ fn weather_icons() -> SymbolLibrary { Anchor { p: Point::new(5.0, 16.0), h_in: Point::new(-6.0, 16.0), h_out: Point::new(0.0, 8.0), kind: AnchorKind::Corner }, Anchor { p: Point::new(-4.0, -1.0), h_in: Point::new(-4.0, 6.0), h_out: Point::new(-4.0, -8.0), kind: AnchorKind::Smooth }, ]; - let moon = PathData::single(SubPath { anchors: moon_anchors, closed: true }); + let moon = PathData::single(SubPath::new(moon_anchors, true)); // Helper for cloud contour let cloud_subpath = |offset_y: f64, scale: f64| -> SubPath { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f150dfd465..1b420d7805 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2133,7 +2133,7 @@ mod pen_interactions { gesture(&mut s, &[(400.0, 500.0)], Mods::default(), v); let extended = path(&s, id); assert_eq!(extended.anchor_count(), 3); - assert_eq!(extended.subpaths[0].anchors[..2], reversed.subpaths[0].anchors); + assert_eq!(extended.subpaths[0].anchors.to_vec()[..2], reversed.subpaths[0].anchors); assert!(!s.in_interaction() && !s.tool_busy()); assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 1); } @@ -2168,7 +2168,7 @@ mod pen_interactions { gesture(&mut s, &[(300.0, 400.0)], Mods::default(), v); let extended = path(&s, id); assert_eq!(extended.anchor_count(), 3); - assert_eq!(extended.subpaths[0].anchors[..2], converted.subpaths[0].anchors); + assert_eq!(extended.subpaths[0].anchors.to_vec()[..2], converted.subpaths[0].anchors); assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 1); } @@ -2227,7 +2227,7 @@ mod pen_interactions { } let extended = path(&s, id); assert_eq!(extended.anchor_count(), 3); - assert_eq!(extended.subpaths[0].anchors[..2], before.subpaths[0].anchors); + assert_eq!(extended.subpaths[0].anchors.to_vec()[..2], before.subpaths[0].anchors); let a = extended.subpaths[0].anchors[2]; assert_eq!(a.p, Point::new(320.0, 450.0)); assert_eq!(a.h_in, Point::new(290.0, 430.0), "Alt freezes the incoming handle and Space translates it"); @@ -3381,7 +3381,7 @@ fn reshape_open_path(s: &mut Session, anchors: Value) -> NodeId { /// The anchors of the path's first subpath. fn reshape_anchors(s: &Session, id: NodeId) -> Vec { - s.doc().unwrap().doc.node(id).unwrap().path_data().unwrap().subpaths[0].anchors.clone() + s.doc().unwrap().doc.node(id).unwrap().path_data().unwrap().subpaths[0].anchors.to_vec() } /// The share of the drag an anchor takes at `u` of the way from an end to the grabbed point. diff --git a/crates/engine/src/tests/anchor_edit_work.rs b/crates/engine/src/tests/anchor_edit_work.rs index 1055156167..5401bc0bf8 100644 --- a/crates/engine/src/tests/anchor_edit_work.rs +++ b/crates/engine/src/tests/anchor_edit_work.rs @@ -1,6 +1,7 @@ //! An edit of one anchor of a long selected path does not measure the path's segments -//! ([`vectorcraft_geom::path::measured_segments`]) to check that it stays within the canvas, and -//! an edit that leaves the anchor as it was records no undo step. +//! ([`vectorcraft_geom::path::measured_segments`]) to check that it stays within the canvas, its +//! undo step keeps the chunk of anchors it changed rather than a copy of every anchor, and an edit +//! that leaves the anchor as it was records no undo step. use std::collections::BTreeSet; use std::sync::Arc; @@ -74,6 +75,46 @@ fn one_anchor_edit_of_a_long_path_measures_few_segments() { } } +/// Most bytes the undo step of one anchor edit may keep, however many anchors the path has: about +/// one chunk of anchors and the node, with room for the chunk tables of 200,000 anchors. +const MOST_STEP_BYTES: usize = 48 * 1024; + +#[test] +fn one_anchor_edit_of_a_long_path_keeps_one_chunk_in_its_undo_step() { + for n in [10_000, 200_000] { + let (mut s, id) = long_path(n); + let original = s.doc().unwrap().doc.clone(); + let edits = [ + ("path.moveAnchor", json!({"id": id.0, "anchor": 5, "x": 3, "y": 50})), + ("path.setHandle", json!({"id": id.0, "anchor": 5, "which": "out", "x": 4, "y": 40})), + ("path.moveAnchors", json!({"dx": 1, "dy": 0})), + ("object.nudge", json!({"dx": 1, "dy": 0})), + ]; + let mut docs = vec![]; + for (cmd, p) in &edits { + s.execute(cmd, p).unwrap(); + let step = s.doc().unwrap().history.undo.last().unwrap().bytes; + assert!(step <= MOST_STEP_BYTES, "{cmd} on {n} anchors kept {step} bytes in its undo step"); + docs.push(s.doc().unwrap().doc.clone()); + } + assert_eq!(anchor(&s, id, 5).p, Point::new(5.0, 50.0)); + assert_eq!(anchor(&s, id, 5).h_out, Point::new(6.0, 40.0)); + assert_eq!(anchor(&s, id, n - 1), original.node(id).unwrap().path_data().unwrap().subpaths[0].anchors[n - 1]); + // Undo gives back each version exactly, and redo the edits, each step as small. + for k in (0..edits.len()).rev() { + s.execute("edit.undo", &json!({})).unwrap(); + let expected = if k == 0 { &original } else { &docs[k - 1] }; + assert!(s.doc().unwrap().doc == *expected, "undo of {} on {n} anchors", edits[k].0); + let redo = s.doc().unwrap().history.redo.last().unwrap().bytes; + assert!(redo <= MOST_STEP_BYTES, "redo of {} on {n} anchors keeps {redo} bytes", edits[k].0); + } + for (k, doc) in docs.iter().enumerate() { + s.execute("edit.redo", &json!({})).unwrap(); + assert!(s.doc().unwrap().doc == *doc, "redo of {} on {n} anchors", edits[k].0); + } + } +} + /// A handle past the canvas is measured: the edit stands while the curve stays within the canvas, /// and is refused once the curve leaves it. #[test] diff --git a/crates/engine/src/tests/constrain_new.rs b/crates/engine/src/tests/constrain_new.rs index 382e3af31c..49fd29e94b 100644 --- a/crates/engine/src/tests/constrain_new.rs +++ b/crates/engine/src/tests/constrain_new.rs @@ -40,7 +40,7 @@ fn close(a: f64, b: f64) -> bool { fn segment_angles(p: &PathData) -> Vec { p.subpaths .iter() - .flat_map(|sp| sp.anchors.windows(2).map(|w| w[1].p - w[0].p)) + .flat_map(|sp| sp.anchors.iter().zip(sp.anchors.iter().skip(1)).map(|(a, b)| b.p - a.p)) .filter(|v| v.hypot() > 1e-9) .map(|v| (-v.y).atan2(v.x).to_degrees().rem_euclid(90.0)) .collect() diff --git a/crates/engine/src/tests/draw2.rs b/crates/engine/src/tests/draw2.rs index a0129c1cd1..45ff1f84c1 100644 --- a/crates/engine/src/tests/draw2.rs +++ b/crates/engine/src/tests/draw2.rs @@ -890,7 +890,7 @@ fn curvature_rubber_band_is_the_path_the_click_makes() { s.pointer(&PointerEvent::new(PointerKind::Up, 450.0, 400.0), v).unwrap(); let made = path(&s, id).subpaths[0].clone(); assert_eq!(made.anchors.len(), 4); - let tail = vectorcraft_geom::SubPath::new(made.anchors[1..].to_vec(), false); + let tail = vectorcraft_geom::SubPath::new(made.anchors.to_vec()[1..].to_vec(), false); assert_eq!(preview, bez(&tail)); } @@ -1218,7 +1218,7 @@ fn scissors_cut_ellipse_end_drags_alone() { s.pointer(&PointerEvent::new(PointerKind::Up, cut.x + 8.0, cut.y + 8.0).with_mods(m), v).unwrap(); assert_eq!(undo_steps(&s), undo + 1); let after = path(&s, id); - let (old, new) = (&sp.anchors, &after.subpaths[0].anchors); + let (old, new) = (&sp.anchors.to_vec(), &after.subpaths[0].anchors.to_vec()); assert_eq!(new.len(), old.len()); assert_eq!(new[1..], old[1..], "only the dragged end moves (at anchor: {at_anchor}, {m:?})"); let d = Point::new(cut.x + 8.0, cut.y + 8.0) - cut; @@ -1572,7 +1572,7 @@ fn a_short_smooth_drag_leaves_the_rest_of_long_segments_alone() { drag(&mut s, "smooth", &[(195.0, 194.0), (205.0, 194.0)], Mods::default()); let p = path(&s, id); assert_ne!(p, before, "the bump is smoothed"); - let a = &p.subpaths[0].anchors; + let a = &p.subpaths[0].anchors.to_vec(); let (first, second, last) = (a[0], a[1], a[a.len() - 1]); assert!(first.p == Point::new(50.0, 200.0) && !first.has_out() && !second.has_in(), "the left side is a line still: {first:?} {second:?}"); assert!(last.p == Point::new(350.0, 200.0) && !last.has_in() && !a[a.len() - 2].has_out(), "and the right one: {last:?}"); @@ -1783,7 +1783,7 @@ fn smoothing_over_a_closed_paths_first_anchor_keeps_its_start() { assert_ne!(after, before, "the bump is smoothed"); assert_starts_nearest(&after, old, 12.0); let kept = &after.subpaths[0].anchors; - for a in &before.subpaths[0].anchors[2..=10] { + for a in &before.subpaths[0].anchors.to_vec()[2..=10] { assert!(kept.iter().any(|k| k.p == a.p), "{:?} is out of reach and stays: {kept:?}", a.p); } s.execute("edit.undo", &json!({})).unwrap(); @@ -3885,7 +3885,7 @@ fn pen_cmd_selected_anchors_are_what_copy_and_cut_take() { assert!(left.is_empty()); assert!(s.doc().unwrap().selection.objects.is_empty()); assert_eq!(s.clipboard.nodes.len(), 1); - assert_eq!(s.clipboard.nodes[0].path_data().unwrap().subpaths[0].anchors, original.subpaths[0].anchors[1..3]); + assert_eq!(s.clipboard.nodes[0].path_data().unwrap().subpaths[0].anchors, original.subpaths[0].anchors.to_vec()[1..3]); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(paths(&s), vec![(id, original.clone())]); assert_eq!(s.doc().unwrap().selection, picked); diff --git a/crates/engine/src/tests/simplify_part.rs b/crates/engine/src/tests/simplify_part.rs index 12f3330595..f3ab93f274 100644 --- a/crates/engine/src/tests/simplify_part.rs +++ b/crates/engine/src/tests/simplify_part.rs @@ -73,8 +73,8 @@ fn simplify_refits_only_the_run_between_direct_selected_anchors() { assert_eq!(out["after"].as_u64(), Some(len as u64)); assert!(len < 101 - 30, "{len}"); let end = len - 41; - assert_eq!(sp.anchors[..20], original.anchors[..20], "the anchors before the run"); - assert_eq!(sp.anchors[end + 1..], original.anchors[61..], "the anchors after the run"); + assert_eq!(sp.anchors.to_vec()[..20], original.anchors.to_vec()[..20], "the anchors before the run"); + assert_eq!(sp.anchors.to_vec()[end + 1..], original.anchors.to_vec()[61..], "the anchors after the run"); assert_eq!((sp.anchors[20].p, sp.anchors[20].h_in), (original.anchors[20].p, original.anchors[20].h_in)); assert_eq!((sp.anchors[end].p, sp.anchors[end].h_out), (original.anchors[60].p, original.anchors[60].h_out)); for i in 0..20 { @@ -100,7 +100,7 @@ fn separate_runs_on_one_path_simplify_independently_in_one_step() { let id = wave(&mut s); select_anchors(&mut s, id, ranges); s.execute("object.path.simplify", &json!({"tolerance": 1})).unwrap(); - path_of(&s, id).subpaths[0].anchors.clone() + path_of(&s, id).subpaths[0].anchors.to_vec() }; let (first, second) = (alone(&[(10, 30)]), alone(&[(60, 80)])); let mut s = session(); @@ -110,13 +110,13 @@ fn separate_runs_on_one_path_simplify_independently_in_one_step() { let undo = undo_len(&s); s.execute("object.path.simplify", &json!({"tolerance": 1})).unwrap(); assert_eq!(undo_len(&s), undo + 1, "one undo step"); - let both = path_of(&s, id).subpaths[0].anchors.clone(); + let both = path_of(&s, id).subpaths[0].anchors.to_vec(); // Where old anchor 30 is in the first result, and old anchor 80 in the second. let (a30, a80) = (first.len() - 71, second.len() - 21); assert!(a30 < 30 && a80 < 80, "both runs lose anchors"); let mid = a30 + 30; assert_eq!(both[..=a30], first[..=a30]); - assert_eq!(both[a30 + 1..mid], original.anchors[31..60], "the anchors between the runs"); + assert_eq!(both[a30 + 1..mid], original.anchors.to_vec()[31..60], "the anchors between the runs"); assert_eq!(both[mid..], second[60..]); let sel: BTreeSet<_> = (10..=a30).chain(mid..=mid + a80 - 60).map(|i| (0, i)).collect(); assert_eq!(s.doc().unwrap().selection.anchors.get(&id), Some(&sel)); @@ -138,7 +138,7 @@ fn a_run_across_the_start_of_a_closed_path_simplifies_as_one() { assert!(sp.closed); assert!(sp.anchors.len() < 80 - 10, "{}", sp.anchors.len()); assert!(!sp.anchors.iter().any(|a| a.p == original.anchors[0].p), "anchor 0 was inside the run"); - assert_eq!(sp.anchors[1..60], original.anchors[11..70]); + assert_eq!(sp.anchors.to_vec()[1..60], original.anchors.to_vec()[11..70]); assert_eq!((sp.anchors[0].p, sp.anchors[0].h_out), (original.anchors[10].p, original.anchors[10].h_out)); assert_eq!((sp.anchors[60].p, sp.anchors[60].h_in), (original.anchors[70].p, original.anchors[70].h_in)); for i in 0..60 { @@ -162,8 +162,8 @@ fn picked_segments_simplify_and_stay_picked() { s.execute("object.path.simplify", &json!({"tolerance": 1})).unwrap(); let sp = path_of(&s, id).subpaths[0].clone(); let end = sp.anchors.len() - 41; - assert_eq!(sp.anchors[..20], original.anchors[..20]); - assert_eq!(sp.anchors[end + 1..], original.anchors[61..]); + assert_eq!(sp.anchors.to_vec()[..20], original.anchors.to_vec()[..20]); + assert_eq!(sp.anchors.to_vec()[end + 1..], original.anchors.to_vec()[61..]); let sel = &s.doc().unwrap().selection; assert_eq!(sel.segments.get(&id), Some(&(20..end).map(|i| (0, i)).collect())); assert_eq!(sel.anchors.get(&id), Some(&(20..=end).map(|i| (0, i)).collect())); @@ -200,8 +200,8 @@ fn slider_preview_and_auto_simplify_only_the_run() { let previewed = path_of(&s, id); let sp = &previewed.subpaths[0]; assert!(sp.anchors.len() < 101, "{p}"); - assert_eq!(sp.anchors[..20], original.anchors[..20], "{p}"); - assert_eq!(sp.anchors[sp.anchors.len() - 40..], original.anchors[61..], "{p}"); + assert_eq!(sp.anchors.to_vec()[..20], original.anchors.to_vec()[..20], "{p}"); + assert_eq!(sp.anchors.to_vec()[sp.anchors.len() - 40..], original.anchors.to_vec()[61..], "{p}"); s.execute("edit.undo", &json!({})).unwrap(); s.execute("object.path.simplify", &p).unwrap(); assert_eq!(path_of(&s, id), previewed, "{p}"); diff --git a/crates/engine/src/tests/smooth_path.rs b/crates/engine/src/tests/smooth_path.rs index 9ce6a21044..1ca7a6d5e0 100644 --- a/crates/engine/src/tests/smooth_path.rs +++ b/crates/engine/src/tests/smooth_path.rs @@ -93,7 +93,7 @@ fn more_smoothing_removes_the_jags_and_keeps_the_shape() { // Within the tolerance (2.5 % of 300 pt at most) plus the jag the refit smooths away. assert!(farthest(&before, &after) <= 7.5 + 1.5, "level {level}: {}", farthest(&before, &after)); // The end points of an open path stay put. - let (a, b) = (&before.subpaths[0].anchors, &after.subpaths[0].anchors); + let (a, b) = (&before.subpaths[0].anchors.to_vec(), &after.subpaths[0].anchors.to_vec()); assert_eq!((a[0].p, a[a.len() - 1].p), (b[0].p, b[b.len() - 1].p)); assert!(!after.subpaths[0].closed); s.execute("edit.undo", &json!({})).unwrap(); @@ -206,7 +206,7 @@ fn direct_selected_anchors_smooth_only_their_section() { s.execute("select.anchors", &json!({"id": id.0, "anchors": anchors})).unwrap(); s.execute("object.path.smooth", &json!({"level": 100})).unwrap(); let after = path_of(&s, id); - let (a, b) = (&before.subpaths[0].anchors, &after.subpaths[0].anchors); + let (a, b) = (&before.subpaths[0].anchors.to_vec(), &after.subpaths[0].anchors.to_vec()); assert!(b.len() < a.len(), "{} → {}", a.len(), b.len()); // Anchors 0..=10 and 30..=40 are untouched, the section between them refitted. assert_eq!(a[..=10].iter().map(|x| x.p).collect::>(), b[..=10].iter().map(|x| x.p).collect::>()); diff --git a/crates/engine/src/tests/toolsettings.rs b/crates/engine/src/tests/toolsettings.rs index 2ec7667bde..a9d9e3f5bd 100644 --- a/crates/engine/src/tests/toolsettings.rs +++ b/crates/engine/src/tests/toolsettings.rs @@ -286,7 +286,7 @@ fn legacy_spiral_decay_preferences_report_the_ratio_drawn_after_reopening() { assert_eq!(d.selection.objects.len(), 1); let id = d.selection.objects[0]; let path = d.doc.node(id).unwrap().path_data().unwrap().clone(); - let anchors = &path.subpaths[0].anchors; + let anchors = &path.subpaths[0].anchors.to_vec(); assert_eq!(anchors.len(), 5); assert!(!path.is_closed()); let center = Point::new(200.0, 200.0); diff --git a/crates/eps/src/ai_ops.rs b/crates/eps/src/ai_ops.rs index 6b1374cd83..aa819a77ad 100644 --- a/crates/eps/src/ai_ops.rs +++ b/crates/eps/src/ai_ops.rs @@ -36,10 +36,8 @@ pub(crate) fn subpath(s: &mut String, xf: Affine, sp: &SubPath) { s.push_str(if smooth { "c\n" } else { "C\n" }); } }; - for pair in sp.anchors.windows(2) { - if let [a, b] = pair { - segment(s, a, b); - } + for (a, b) in sp.anchors.iter().zip(sp.anchors.iter().skip(1)) { + segment(s, a, b); } if sp.closed && sp.anchors.len() > 1 diff --git a/crates/geom/src/anchors.rs b/crates/geom/src/anchors.rs new file mode 100644 index 0000000000..57b8081686 --- /dev/null +++ b/crates/geom/src/anchors.rs @@ -0,0 +1,790 @@ +//! The anchors of a subpath, held in shared chunks. +//! +//! [`Anchors`] keeps its anchors in chunks of at most [`CHUNK`] anchors, each behind an `Arc`. +//! Copying the list copies the table of chunks and shares the chunks themselves; changing an +//! anchor copies only the chunk that holds it when another copy still shares that chunk. An undo +//! step that keeps the version of a path before a one-anchor edit then keeps one chunk of its +//! own, however many anchors the path has. Inserting or removing an anchor copies the chunk it is +//! in and rebuilds the table, which has one entry per chunk. +//! +//! The list reads and writes as one flat sequence of anchors, as a `Vec` does. + +use std::borrow::Cow; +use std::fmt; +use std::ops::{Index, IndexMut, RangeBounds}; +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use crate::path::Anchor; + +/// An [`Anchors`] list of the anchors given, as `vec!` makes a `Vec`. +#[macro_export] +macro_rules! anchors { + ($($t:tt)*) => { + $crate::Anchors::from(vec![$($t)*]) + }; +} + +/// Most anchors one chunk holds. +pub const CHUNK: usize = 256; + +/// The anchors of a subpath: a sequence with the reads of a slice and the edits of a `Vec`, whose +/// copies share the chunks they have not changed. +#[derive(Clone, Default)] +pub struct Anchors { + /// The chunks in order, none of them empty. + chunks: Vec>>, + /// Empty while every chunk but the last holds [`CHUNK`] anchors (anchor `i` is then in chunk + /// `i / CHUNK`). Otherwise `ends[c]` is the number of anchors in chunks `0..=c`. + ends: Vec, + /// The number of anchors. + len: usize, +} + +impl Anchors { + /// An empty list. + pub const fn new() -> Self { + Self { chunks: Vec::new(), ends: Vec::new(), len: 0 } + } + + /// The number of anchors. + pub fn len(&self) -> usize { + self.len + } + + /// Whether the list has no anchors. + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// The chunk that holds anchor `i` and its place in that chunk. For `i` at or past the end the + /// chunk or the place is out of range. + fn locate(&self, i: usize) -> (usize, usize) { + if self.ends.is_empty() { + (i / CHUNK, i % CHUNK) + } else { + let c = self.ends.partition_point(|&e| e <= i); + let start = c.checked_sub(1).and_then(|p| self.ends.get(p)).copied().unwrap_or(0); + (c, i.saturating_sub(start)) + } + } + + /// Anchor `i`, or `None` past the end. + pub fn get(&self, i: usize) -> Option<&Anchor> { + if i >= self.len { + return None; + } + let (c, o) = self.locate(i); + self.chunks.get(c)?.get(o) + } + + /// Anchor `i` to change, or `None` past the end. Copies the chunk that holds it when another + /// list shares that chunk. + pub fn get_mut(&mut self, i: usize) -> Option<&mut Anchor> { + if i >= self.len { + return None; + } + let (c, o) = self.locate(i); + Arc::make_mut(self.chunks.get_mut(c)?).get_mut(o) + } + + /// The first anchor. + pub fn first(&self) -> Option<&Anchor> { + self.chunks.first()?.first() + } + + /// The last anchor. + pub fn last(&self) -> Option<&Anchor> { + self.chunks.last()?.last() + } + + /// The first anchor to change. + pub fn first_mut(&mut self) -> Option<&mut Anchor> { + Arc::make_mut(self.chunks.first_mut()?).first_mut() + } + + /// The last anchor to change. + pub fn last_mut(&mut self) -> Option<&mut Anchor> { + Arc::make_mut(self.chunks.last_mut()?).last_mut() + } + + /// The anchors in order. + pub fn iter(&self) -> Iter<'_> { + let (front, back): (&[Anchor], &[Anchor]) = (&[], &[]); + Iter { chunks: self.chunks.iter(), front: front.iter(), back: back.iter(), left: self.len } + } + + /// The anchors in order, to change. Each chunk is copied, when another list shares it, as the + /// iterator reaches it. + pub fn iter_mut(&mut self) -> IterMut<'_> { + let chunk: fn(&mut Arc>) -> std::slice::IterMut<'_, Anchor> = |c| Arc::make_mut(c).iter_mut(); + self.chunks.iter_mut().flat_map(chunk) + } + + /// The anchors as one slice: borrowed when they are in one chunk, copied otherwise. + pub fn contiguous(&self) -> Cow<'_, [Anchor]> { + match self.chunks.as_slice() { + [] => Cow::Borrowed(&[]), + [one] => Cow::Borrowed(one.as_slice()), + _ => Cow::Owned(self.to_vec()), + } + } + + /// The anchors in `range` copied into a `Vec`, or `None` when the range is out of bounds or + /// starts after it ends (as `slice::get` with a range). + pub fn get_range(&self, range: impl RangeBounds) -> Option> { + use std::ops::Bound; + let start = match range.start_bound() { + Bound::Included(&s) => s, + Bound::Excluded(&s) => s.checked_add(1)?, + Bound::Unbounded => 0, + }; + let end = match range.end_bound() { + Bound::Included(&e) => e.checked_add(1)?, + Bound::Excluded(&e) => e, + Bound::Unbounded => self.len, + }; + (start <= end && end <= self.len).then(|| self.iter().skip(start).take(end - start).copied().collect()) + } + + /// The anchors copied into a `Vec`. + pub fn to_vec(&self) -> Vec { + let mut v = Vec::with_capacity(self.len); + for c in &self.chunks { + v.extend_from_slice(c); + } + v + } + + /// The anchors as a `Vec`, without a copy when they are in one chunk no other list shares. + pub fn into_vec(mut self) -> Vec { + if self.chunks.len() == 1 + && let Some(one) = self.chunks.pop() + { + return Arc::try_unwrap(one).unwrap_or_else(|shared| (*shared).clone()); + } + self.to_vec() + } + + /// Change the anchors as a `Vec` (for edits that move many of them, such as reversing). + pub fn with_vec(&mut self, f: impl FnOnce(&mut Vec) -> R) -> R { + let mut v = std::mem::take(self).into_vec(); + let r = f(&mut v); + *self = Self::from(v); + r + } + + /// Rebuild `ends` and `len` after the chunks changed, dropping empty chunks. + fn reindex(&mut self) { + self.chunks.retain(|c| !c.is_empty()); + self.len = self.chunks.iter().map(|c| c.len()).sum(); + let n = self.chunks.len(); + let uniform = self.chunks.iter().enumerate().all(|(k, c)| if k + 1 == n { c.len() <= CHUNK } else { c.len() == CHUNK }); + self.ends.clear(); + if !uniform { + let mut total = 0; + self.ends.extend(self.chunks.iter().map(|c| { + total += c.len(); + total + })); + } + } + + /// Add `a` at the end. + pub fn push(&mut self, a: Anchor) { + match self.chunks.last_mut() { + Some(last) if last.len() < CHUNK => Arc::make_mut(last).push(a), + _ => { + self.chunks.push(Arc::new(vec![a])); + if !self.ends.is_empty() { + self.ends.push(self.len); + } + } + } + self.len += 1; + if let Some(end) = self.ends.last_mut() { + *end = self.len; + } + } + + /// Remove the last anchor. + pub fn pop(&mut self) -> Option { + let last = self.chunks.last_mut()?; + let a = Arc::make_mut(last).pop(); + if last.is_empty() { + self.chunks.pop(); + self.ends.pop(); + } + self.len = self.len.saturating_sub(1); + if let Some(end) = self.ends.last_mut() { + *end = self.len; + } + a + } + + /// Insert `a` before anchor `i`, or at the end when `i` is the length or more. + pub fn insert(&mut self, i: usize, a: Anchor) { + if i >= self.len { + self.push(a); + return; + } + let (c, o) = self.locate(i); + let Some(chunk) = self.chunks.get_mut(c) else { return }; + let chunk = Arc::make_mut(chunk); + chunk.insert(o.min(chunk.len()), a); + if chunk.len() > CHUNK { + let tail = chunk.split_off(chunk.len() / 2); + self.chunks.insert(c + 1, Arc::new(tail)); + } + self.reindex(); + } + + /// Remove anchor `i`, or `None` past the end. + pub fn try_remove(&mut self, i: usize) -> Option { + if i >= self.len { + return None; + } + let (c, o) = self.locate(i); + let chunk = Arc::make_mut(self.chunks.get_mut(c)?); + let a = (o < chunk.len()).then(|| chunk.remove(o)); + // A chunk left small joins the next one when the two fit in one chunk. + if chunk.len() < CHUNK / 4 + && let Some(next) = self.chunks.get(c + 1) + && self.chunks.get(c).map_or(0, |c| c.len()) + next.len() <= CHUNK + { + let next = self.chunks.remove(c + 1); + if let Some(chunk) = self.chunks.get_mut(c) { + Arc::make_mut(chunk).extend_from_slice(&next); + } + } + self.reindex(); + a + } + + /// Remove anchor `i`. Panics past the end, as `Vec::remove` does. + pub fn remove(&mut self, i: usize) -> Anchor { + match self.try_remove(i) { + Some(a) => a, + None => { + let none: &[Anchor] = &[]; + none[i] + } + } + } + + /// Keep the first `n` anchors. + pub fn truncate(&mut self, n: usize) { + if n >= self.len { + return; + } + let (c, o) = self.locate(n); + self.chunks.truncate(c + 1); + if let Some(last) = self.chunks.last_mut() { + Arc::make_mut(last).truncate(o); + } + self.reindex(); + } + + /// Remove every anchor. + pub fn clear(&mut self) { + *self = Self::new(); + } + + /// Add the anchors of `s` at the end. + pub fn extend_from_slice(&mut self, s: &[Anchor]) { + self.extend(s.iter().copied()); + } + + /// Keep the anchors for which `f` is true. + pub fn retain(&mut self, f: impl FnMut(&Anchor) -> bool) { + self.with_vec(|v| v.retain(f)); + } + + /// Keep the anchors for which `f` is true, letting `f` change them. + pub fn retain_mut(&mut self, f: impl FnMut(&mut Anchor) -> bool) { + self.with_vec(|v| v.retain_mut(f)); + } + + /// Reverse the order of the anchors. + pub fn reverse(&mut self) { + self.with_vec(|v| v.reverse()); + } + + /// Rotate the anchors `k` places towards the start. + pub fn rotate_left(&mut self, k: usize) { + self.with_vec(|v| v.rotate_left(k)); + } + + /// Rotate the anchors `k` places towards the end. + pub fn rotate_right(&mut self, k: usize) { + self.with_vec(|v| v.rotate_right(k)); + } + + /// Remove the anchors in `range` and return them. + pub fn drain(&mut self, range: impl RangeBounds) -> std::vec::IntoIter { + self.with_vec(|v| v.drain(range).collect::>()).into_iter() + } + + /// Replace the anchors in `range` with those of `with`, and return the anchors replaced. + pub fn splice(&mut self, range: impl RangeBounds, with: impl IntoIterator) -> std::vec::IntoIter { + self.with_vec(|v| v.splice(range, with).collect::>()).into_iter() + } + + /// The pairs of anchors in the same place in `self` and `other`, with that place, in order. + /// When the two are chunked alike, the chunks they share in the same place are skipped (their + /// anchors are the same); otherwise every pair is visited. Stops at the end of the shorter list. + pub fn pairs_not_shared<'a>(&'a self, other: &'a Self) -> impl Iterator + 'a { + let alike = self.len == other.len && self.chunks.len() == other.chunks.len() && self.ends == other.ends; + let chunked = alike.then(|| { + let starts = self.chunks.iter().scan(0, |start, c| { + let at = *start; + *start += c.len(); + Some(at) + }); + starts + .zip(self.chunks.iter().zip(&other.chunks)) + .filter(|(_, (a, b))| !Arc::ptr_eq(a, b)) + .flat_map(|(at, (a, b))| a.iter().zip(b.iter()).enumerate().map(move |(o, (x, y))| (at + o, x, y))) + }); + let flat = (!alike).then(|| self.iter().zip(other.iter()).enumerate().map(|(i, (a, b))| (i, a, b))); + chunked.into_iter().flatten().chain(flat.into_iter().flatten()) + } + + /// Each chunk's address and capacity (in anchors): two lists hold a chunk in common when they + /// give the same address. For memory estimates. + pub fn chunk_allocations(&self) -> impl Iterator + '_ { + self.chunks.iter().map(|c| (Arc::as_ptr(c) as usize, c.capacity())) + } + + /// The bytes the list's own tables hold on the heap (the chunk table and the running totals), + /// without the chunks. + pub fn table_bytes(&self) -> usize { + self.chunks.capacity() * size_of::>>() + self.ends.capacity() * size_of::() + } +} + +/// [`Anchors::iter_mut`]'s iterator. +pub type IterMut<'a> = std::iter::FlatMap< + std::slice::IterMut<'a, Arc>>, + std::slice::IterMut<'a, Anchor>, + fn(&mut Arc>) -> std::slice::IterMut<'_, Anchor>, +>; + +/// [`Anchors::iter`]'s iterator. +#[derive(Clone)] +pub struct Iter<'a> { + /// The chunks neither end has reached. + chunks: std::slice::Iter<'a, Arc>>, + /// What is left of the chunk the front is in. + front: std::slice::Iter<'a, Anchor>, + /// What is left of the chunk the back is in. + back: std::slice::Iter<'a, Anchor>, + /// The anchors left. + left: usize, +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a Anchor; + + fn next(&mut self) -> Option<&'a Anchor> { + loop { + if let Some(a) = self.front.next() { + self.left -= 1; + return Some(a); + } + match self.chunks.next() { + Some(c) => self.front = c.iter(), + None => { + let a = self.back.next()?; + self.left -= 1; + return Some(a); + } + } + } + } + + fn size_hint(&self) -> (usize, Option) { + (self.left, Some(self.left)) + } + + fn nth(&mut self, mut n: usize) -> Option<&'a Anchor> { + // Whole chunks are skipped without visiting their anchors. + loop { + let f = self.front.len(); + if n < f { + self.left -= n + 1; + return self.front.nth(n); + } + n -= f; + self.left -= f; + self.front = Default::default(); + match self.chunks.as_slice().first() { + Some(c) if c.len() <= n => { + n -= c.len(); + self.left -= c.len(); + self.chunks.next(); + } + Some(_) => { + if let Some(c) = self.chunks.next() { + self.front = c.iter(); + } + } + None => { + let b = self.back.len(); + if n < b { + self.left -= n + 1; + return self.back.nth(n); + } + self.left -= b; + self.back = Default::default(); + return None; + } + } + } + } + + fn count(self) -> usize { + self.left + } + + fn last(mut self) -> Option<&'a Anchor> { + self.next_back() + } +} + +impl<'a> DoubleEndedIterator for Iter<'a> { + fn next_back(&mut self) -> Option<&'a Anchor> { + loop { + if let Some(a) = self.back.next_back() { + self.left -= 1; + return Some(a); + } + match self.chunks.next_back() { + Some(c) => self.back = c.iter(), + None => { + let a = self.front.next_back()?; + self.left -= 1; + return Some(a); + } + } + } + } +} + +impl ExactSizeIterator for Iter<'_> {} + +impl std::iter::FusedIterator for Iter<'_> {} + +impl Index for Anchors { + type Output = Anchor; + + /// Anchor `i`. Panics past the end, as indexing a slice does. + fn index(&self, i: usize) -> &Anchor { + let (c, o) = self.locate(i); + &self.chunks[c][o] + } +} + +impl IndexMut for Anchors { + /// Anchor `i` to change ([`Anchors::get_mut`]). Panics past the end, as indexing a slice does. + fn index_mut(&mut self, i: usize) -> &mut Anchor { + let (c, o) = self.locate(i); + &mut Arc::make_mut(&mut self.chunks[c])[o] + } +} + +impl From> for Anchors { + fn from(v: Vec) -> Self { + if v.is_empty() { + return Self::new(); + } + let len = v.len(); + let chunks = if len <= CHUNK { vec![Arc::new(v)] } else { v.chunks(CHUNK).map(|c| Arc::new(c.to_vec())).collect() }; + Self { chunks, ends: Vec::new(), len } + } +} + +impl From<&[Anchor]> for Anchors { + fn from(s: &[Anchor]) -> Self { + Self::from(s.to_vec()) + } +} + +impl From<[Anchor; N]> for Anchors { + fn from(s: [Anchor; N]) -> Self { + Self::from(Vec::from(s)) + } +} + +impl From for Vec { + fn from(a: Anchors) -> Self { + a.into_vec() + } +} + +impl FromIterator for Anchors { + fn from_iter>(it: I) -> Self { + Self::from(it.into_iter().collect::>()) + } +} + +impl Extend for Anchors { + fn extend>(&mut self, it: I) { + for a in it { + self.push(a); + } + } +} + +impl<'a> Extend<&'a Anchor> for Anchors { + fn extend>(&mut self, it: I) { + self.extend(it.into_iter().copied()); + } +} + +impl<'a> IntoIterator for &'a Anchors { + type Item = &'a Anchor; + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Iter<'a> { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut Anchors { + type Item = &'a mut Anchor; + type IntoIter = IterMut<'a>; + + fn into_iter(self) -> IterMut<'a> { + self.iter_mut() + } +} + +impl IntoIterator for Anchors { + type Item = Anchor; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> std::vec::IntoIter { + self.into_vec().into_iter() + } +} + +impl PartialEq for Anchors { + fn eq(&self, other: &Self) -> bool { + self.len == other.len && self.iter().eq(other.iter()) + } +} + +impl PartialEq<[Anchor]> for Anchors { + fn eq(&self, other: &[Anchor]) -> bool { + self.len == other.len() && self.iter().eq(other.iter()) + } +} + +impl PartialEq<&[Anchor]> for Anchors { + fn eq(&self, other: &&[Anchor]) -> bool { + *self == **other + } +} + +impl PartialEq> for Anchors { + fn eq(&self, other: &Vec) -> bool { + *self == **other + } +} + +impl PartialEq<[Anchor; N]> for Anchors { + fn eq(&self, other: &[Anchor; N]) -> bool { + *self == other[..] + } +} + +impl PartialEq for [Anchor] { + fn eq(&self, other: &Anchors) -> bool { + *other == *self + } +} + +impl PartialEq for Vec { + fn eq(&self, other: &Anchors) -> bool { + *other == **self + } +} + +impl fmt::Debug for Anchors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.iter()).finish() + } +} + +impl Serialize for Anchors { + fn serialize(&self, s: S) -> Result { + s.collect_seq(self.iter()) + } +} + +impl<'de> Deserialize<'de> for Anchors { + fn deserialize>(d: D) -> Result { + Vec::::deserialize(d).map(Self::from) + } +} + +#[cfg(test)] +mod tests { + use kurbo::Point; + + use super::*; + + fn line(n: usize) -> Vec { + (0..n).map(|i| Anchor::corner(Point::new(i as f64, (i % 7) as f64))).collect() + } + + /// Chunks of `a` that `b` does not hold, with their capacity in anchors. + fn own_chunks(a: &Anchors, b: &Anchors) -> Vec { + let theirs: Vec = b.chunk_allocations().map(|(id, _)| id).collect(); + a.chunk_allocations().filter(|(id, _)| !theirs.contains(id)).map(|(_, cap)| cap).collect() + } + + #[test] + fn editing_one_anchor_copies_one_chunk_whatever_the_length() { + for n in [10_000, 200_000] { + let before = Anchors::from(line(n)); + let mut after = before.clone(); + after[n / 2].p = Point::new(-1.0, -1.0); + if let Some(a) = after.get_mut(n / 2 + 1) { + a.h_out = Point::new(-2.0, -2.0); + } + let copied = own_chunks(&after, &before); + assert_eq!(copied.len(), 1, "{n} anchors: one chunk copied"); + assert!(copied.iter().all(|&cap| cap <= CHUNK), "{n} anchors: {copied:?}"); + assert_eq!(own_chunks(&before, &after).len(), 1, "{n} anchors: the old version keeps one chunk of its own"); + // The old version is as it was; the new one differs only at the two anchors. + assert_eq!(before, line(n)); + let mut expected = line(n); + expected[n / 2].p = Point::new(-1.0, -1.0); + expected[n / 2 + 1].h_out = Point::new(-2.0, -2.0); + assert_eq!(after, expected); + } + } + + #[test] + fn inserting_and_removing_copy_only_the_chunk_touched() { + let n = 200_000; + let before = Anchors::from(line(n)); + let mut after = before.clone(); + let extra = Anchor::corner(Point::new(0.5, 0.5)); + after.insert(n / 2, extra); + assert!(own_chunks(&after, &before).len() <= 2, "the chunk split in two"); + assert_eq!(after.len(), n + 1); + assert_eq!(after[n / 2], extra); + assert_eq!(after[n / 2 + 1], line(n)[n / 2]); + assert_eq!(after.remove(n / 2), extra); + assert_eq!(after, line(n)); + assert!(own_chunks(&after, &before).len() <= 2); + assert_eq!(before, line(n)); + } + + #[test] + fn acts_as_a_vec_through_edits() { + // A run of edits applied to an `Anchors` and to a `Vec` leaves the two the same, across + // chunk splits, joins and the uniform and non-uniform layouts. + let mut v = line(700); + let mut a = Anchors::from(v.clone()); + let mut seed = 7u64; + let mut next = |m: usize| { + seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + (seed >> 33) as usize % m.max(1) + }; + for step in 0..3000 { + let x = Anchor::corner(Point::new(step as f64, 0.25)); + match next(8) { + 0 | 1 => { + let i = next(v.len() + 1); + v.insert(i, x); + a.insert(i, x); + } + 2 | 3 if !v.is_empty() => { + let i = next(v.len()); + assert_eq!(v.remove(i), a.remove(i)); + } + 4 => { + v.push(x); + a.push(x); + } + 5 => assert_eq!(v.pop(), a.pop()), + 6 if !v.is_empty() => { + let i = next(v.len()); + v[i].h_in = x.p; + a[i].h_in = x.p; + } + _ => { + let keep = v.len().saturating_sub(next(3)); + v.truncate(keep); + a.truncate(keep); + } + } + assert_eq!(a.len(), v.len(), "step {step}"); + if step % 50 == 0 { + assert_eq!(a, v, "step {step}"); + for i in 0..v.len() { + assert_eq!(a.get(i), v.get(i), "step {step}, anchor {i}"); + } + } + } + assert_eq!(a, v); + assert_eq!(a.iter().rev().copied().collect::>(), v.iter().rev().copied().collect::>()); + assert_eq!(a.get(v.len()), None); + } + + #[test] + fn iterators_skip_and_count_as_a_slice_does() { + let v = line(1000); + let a = Anchors::from(v.clone()); + for skip in [0, 1, 255, 256, 257, 999, 1000, 1001] { + assert_eq!(a.iter().nth(skip), v.get(skip), "nth {skip}"); + assert_eq!(a.iter().skip(skip).count(), v.iter().skip(skip).count(), "skip {skip}"); + assert_eq!(a.iter().skip(skip).len(), v.iter().skip(skip).len(), "len after {skip}"); + } + let mut both = a.iter(); + let mut vs = v.iter(); + for k in 0..1000 { + let (x, y) = if k % 3 == 0 { (both.next_back(), vs.next_back()) } else { (both.next(), vs.next()) }; + assert_eq!(x, y, "step {k}"); + assert_eq!(both.len(), vs.len()); + } + assert_eq!(both.next(), None); + assert_eq!(a.iter().last(), v.last()); + let mut m = a.clone(); + m.iter_mut().for_each(|x| x.p.x += 1.0); + assert!(m.iter().zip(&v).all(|(x, y)| x.p.x == y.p.x + 1.0)); + assert_eq!(a, v, "changing a copy left the original alone"); + } + + #[test] + fn reads_and_writes_as_a_flat_list() { + let v = line(600); + let a = Anchors::from(v.clone()); + assert_eq!(serde_json::to_string(&a).ok(), serde_json::to_string(&v).ok()); + let back: Anchors = serde_json::from_str(&serde_json::to_string(&v).unwrap_or_default()).unwrap_or_default(); + assert_eq!(back, v); + assert_eq!(format!("{a:?}"), format!("{v:?}")); + } + + #[test] + fn pairs_not_shared_skip_the_chunks_both_hold() { + let before = Anchors::from(line(10_000)); + let mut after = before.clone(); + after[5000].p = Point::new(-3.0, 0.0); + let pairs: Vec<_> = before.pairs_not_shared(&after).collect(); + assert!(pairs.len() <= CHUNK); + let changed: Vec = pairs.iter().filter(|(_, a, b)| a != b).map(|(i, _, _)| *i).collect(); + assert_eq!(changed, [5000]); + assert!(pairs.iter().all(|&(i, a, b)| *a == before[i] && *b == after[i])); + // Chunked differently: every pair. + let mut moved = before.clone(); + moved.insert(0, Anchor::corner(Point::ZERO)); + moved.remove(0); + assert_eq!(before.pairs_not_shared(&moved).count(), 10_000); + } +} diff --git a/crates/geom/src/lib.rs b/crates/geom/src/lib.rs index 12a7492e7f..7d3e3f0f4b 100644 --- a/crates/geom/src/lib.rs +++ b/crates/geom/src/lib.rs @@ -7,6 +7,7 @@ //! Coordinates are document points (1/72 in), y pointing down. #![forbid(unsafe_code)] +pub mod anchors; pub mod arc; pub mod bez; pub mod corners; @@ -17,6 +18,7 @@ pub mod recognize; pub mod shapes; pub mod snap; +pub use anchors::Anchors; pub use arc::ArcPath; pub use kurbo; pub use kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, Rect, Shape, Size, Vec2}; diff --git a/crates/geom/src/path.rs b/crates/geom/src/path.rs index bef18334ae..c5343171ef 100644 --- a/crates/geom/src/path.rs +++ b/crates/geom/src/path.rs @@ -6,6 +6,7 @@ use kurbo::{Affine, BezPath, CubicBez, ParamCurve, ParamCurveExtrema, ParamCurve use serde::{Deserialize, Serialize}; use crate::EPS; +use crate::anchors::Anchors; /// Fill rule for paths and compound paths. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -224,15 +225,15 @@ pub fn with_anchor_budget(max: usize, f: impl FnOnce() -> T) -> T { } /// A subpath's anchors read within [`with_anchor_budget`]. -fn anchors_within<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { +fn anchors_within<'de, D: serde::Deserializer<'de>>(d: D) -> Result { struct V; impl<'de> serde::de::Visitor<'de> for V { - type Value = Vec; + type Value = Anchors; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.write_str("a list of anchors") } - fn visit_seq>(self, mut a: A) -> Result, A::Error> { - let mut out = Vec::new(); + fn visit_seq>(self, mut a: A) -> Result { + let mut out = Anchors::new(); while let Some(anchor) = a.next_element::()? { if let Some(left) = ANCHORS_LEFT.get() { let left = left @@ -262,13 +263,17 @@ pub fn measured_segments() -> u64 { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct SubPath { #[serde(deserialize_with = "anchors_within")] - pub anchors: Vec, + pub anchors: Anchors, #[serde(default)] pub closed: bool, } impl SubPath { pub fn new(anchors: Vec, closed: bool) -> Self { + Self { anchors: anchors.into(), closed } + } + /// A subpath of `anchors`, sharing their chunks. + pub fn of(anchors: Anchors, closed: bool) -> Self { Self { anchors, closed } } /// Polyline from points. @@ -442,7 +447,7 @@ impl SubPath { let (run, cuts): (Vec, BTreeSet) = if self.closed { (self.anchors.iter().cycle().skip(first).take(n + 1).copied().collect(), cuts.map(|c| (c + n - first) % n).chain([n]).collect()) } else { - (self.anchors.clone(), cuts.collect()) + (self.anchors.to_vec(), cuts.collect()) }; let mut pieces = vec![]; let mut cur: Vec = vec![]; @@ -511,11 +516,11 @@ impl PathData { { subpaths.push(sp); } - cur = Some(SubPath { anchors: vec![Anchor::corner(p)], closed: false }); + cur = Some(SubPath::new(vec![Anchor::corner(p)], false)); last = p; } PathEl::LineTo(p) => { - let sp = cur.get_or_insert_with(|| SubPath { anchors: vec![Anchor::corner(last)], closed: false }); + let sp = cur.get_or_insert_with(|| SubPath::new(vec![Anchor::corner(last)], false)); sp.anchors.push(Anchor::corner(p)); last = p; } @@ -752,7 +757,7 @@ pub fn segment_bounds(seg: PathSeg) -> Rect { } fn push_curve(cur: &mut Option, last: Point, c1: Point, c2: Point, p: Point) { - let sp = cur.get_or_insert_with(|| SubPath { anchors: vec![Anchor::corner(last)], closed: false }); + let sp = cur.get_or_insert_with(|| SubPath::new(vec![Anchor::corner(last)], false)); if let Some(a) = sp.anchors.last_mut() { a.h_out = c1; } diff --git a/crates/geom/src/shapes.rs b/crates/geom/src/shapes.rs index 3baf55f147..0ae8a75190 100644 --- a/crates/geom/src/shapes.rs +++ b/crates/geom/src/shapes.rs @@ -814,7 +814,7 @@ mod tests { for decay in [20.0, 30.0, 50.0, 80.0, 95.0] { for clockwise in [true, false] { let p = spiral(center, 100.0, decay, 8, clockwise); - let anchors = &p.subpaths[0].anchors; + let anchors = &p.subpaths[0].anchors.to_vec(); let k = (decay / 100.0_f64).ln() / TAU; let mut worst = 0.0_f64; for pair in anchors.windows(2) { diff --git a/crates/pathops/src/addons/cleanup.rs b/crates/pathops/src/addons/cleanup.rs index 75cf45b583..b0c000be9a 100644 --- a/crates/pathops/src/addons/cleanup.rs +++ b/crates/pathops/src/addons/cleanup.rs @@ -104,7 +104,7 @@ pub fn merge_duplicate_anchors(sp: &mut SubPath, distance: f64) -> usize { { *f = Anchor::with_handles(f.p, moved(f.p, last.p, last.h_in), f.h_out); } - sp.anchors = out; + sp.anchors = out.into(); before - sp.anchors.len() } diff --git a/crates/pathops/src/addons/keep_shape.rs b/crates/pathops/src/addons/keep_shape.rs index a3721f8f22..12ba4b21df 100644 --- a/crates/pathops/src/addons/keep_shape.rs +++ b/crates/pathops/src/addons/keep_shape.rs @@ -27,7 +27,7 @@ const ACCURACY: f64 = 1e-6; fn merged(prev: &Anchor, cur: &Anchor, next: &Anchor) -> CubicBez { let mut run = SubPath::new(vec![*prev, *cur, *next], false); remove_anchor(&mut run, 1); - match run.anchors.as_slice() { + match &*run.anchors.contiguous() { [a, b] => CubicBez::new(prev.p, a.h_out, b.h_in, next.p), _ => CubicBez::new(prev.p, prev.h_out, next.h_in, next.p), } diff --git a/crates/pathops/src/addons/pathtools/shared_edge.rs b/crates/pathops/src/addons/pathtools/shared_edge.rs index 6b4e8bfb1e..5acee01def 100644 --- a/crates/pathops/src/addons/pathtools/shared_edge.rs +++ b/crates/pathops/src/addons/pathtools/shared_edge.rs @@ -377,7 +377,7 @@ fn splice(sp: &SubPath, st: Stretch, chain: &[CubicBez]) -> Option { let own = backward(chain); let chain = if st.reversed { own.as_slice() } else { chain }; let (first, last) = (chain.first()?, chain.last()?); - let mut out = sp.anchors.clone(); + let mut out = sp.anchors.to_vec(); let start = st.start.min(out.len()); out.rotate_left(start); let settle = |a: &mut Anchor| { diff --git a/crates/pathops/src/edit.rs b/crates/pathops/src/edit.rs index 87b4cb000d..3ad1281eb2 100644 --- a/crates/pathops/src/edit.rs +++ b/crates/pathops/src/edit.rs @@ -5,7 +5,7 @@ use std::collections::{BTreeMap, BTreeSet}; use kurbo::{CubicBez, Line, ParamCurve, ParamCurveNearest, PathSeg, Point, Rect, Vec2}; use vectorcraft_geom::path::measured_arclen; -use vectorcraft_geom::{Anchor, AnchorKind, PathData, SubPath}; +use vectorcraft_geom::{Anchor, AnchorKind, Anchors, PathData, SubPath}; use crate::boolean::{Seg, segs_to_subpath}; use crate::fit::{end_tangent, fit_cubics, fit_single, fit_single_from, is_straight, largest_second_difference, sample, start_tangent, unit}; @@ -1091,7 +1091,7 @@ pub fn remove_anchor(sp: &mut SubPath, index: usize) -> bool { /// Refit the facing handles of anchors `pi` and `ni` so one cubic follows the two segments that /// meet at anchor `index` between them ([`remove_anchor`]). Two straight segments are left as /// they are, and so are the handles when the fit is not finite. -fn refit_across(anchors: &mut [Anchor], pi: usize, index: usize, ni: usize) { +fn refit_across(anchors: &mut Anchors, pi: usize, index: usize, ni: usize) { let (Some(&a), Some(&b), Some(&c)) = (anchors.get(pi), anchors.get(index), anchors.get(ni)) else { return }; let line = |a: &Anchor, b: &Anchor| !a.has_out() && !b.has_in(); if line(&a, &b) && line(&b, &c) { @@ -1233,7 +1233,7 @@ pub fn add_anchor_points(path: &PathData) -> PathData { v[2 * seg + 1] = Anchor { p: l.p3, h_in: l.p2, h_out: r.p1, kind: AnchorKind::Smooth }; } } - sp.anchors = v; + sp.anchors = v.into(); } out } diff --git a/crates/pathops/tests/it/ops.rs b/crates/pathops/tests/it/ops.rs index 0b5b0bbf25..a31c9af2e4 100644 --- a/crates/pathops/tests/it/ops.rs +++ b/crates/pathops/tests/it/ops.rs @@ -769,7 +769,7 @@ fn add_anchor_points_doubles() { /// The one-pass build gives what inserting a midpoint into each segment (last first) gives, open or closed. #[test] fn add_anchor_points_matches_inserting() { - let open = PathData::single(SubPath::new(circle(0.0, 0.0, 10.0).subpaths[0].anchors.clone(), false)); + let open = PathData::single(SubPath::new(circle(0.0, 0.0, 10.0).subpaths[0].anchors.to_vec(), false)); for src in [circle(0.0, 0.0, 10.0), rect(0.0, 0.0, 10.0, 10.0), open] { let mut want = src.clone(); for sp in &mut want.subpaths { diff --git a/crates/render/src/kept_paths.rs b/crates/render/src/kept_paths.rs index a6deb8e334..c609590d16 100644 --- a/crates/render/src/kept_paths.rs +++ b/crates/render/src/kept_paths.rs @@ -290,7 +290,7 @@ impl Sub { /// 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() { + for (k, a, b) in was.anchors.pairs_not_shared(&now.anchors) { if same(a, b) { continue; } @@ -489,7 +489,7 @@ 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)) + w.closed != p.closed || w.anchors.len() != p.anchors.len() || w.anchors.pairs_not_shared(&p.anchors).any(|(_, a, b)| !same(a, b)) })) } diff --git a/crates/render/src/tests/keptpath.rs b/crates/render/src/tests/keptpath.rs index e7097c4850..12e87f272b 100644 --- a/crates/render/src/tests/keptpath.rs +++ b/crates/render/src/tests/keptpath.rs @@ -140,7 +140,7 @@ fn a_long_path_draws_and_culls_after_each_edit_as_when_its_path_is_made_again() }), ("that anchor moved off it", |p| moved(&mut p.subpaths[0].anchors[1535], Vec2::new(5.0, 5.0))), ("a doubled anchor moved off its twin", |p| moved(&mut p.subpaths[0].anchors[1536], Vec2::new(0.0, -12.0))), - ("1,200 anchors moved", |p| p.subpaths[0].anchors[..1200].iter_mut().for_each(|a| moved(a, Vec2::new(2.0, 1.0)))), + ("1,200 anchors moved", |p| p.subpaths[0].anchors.iter_mut().take(1200).for_each(|a| moved(a, Vec2::new(2.0, 1.0)))), ("an anchor added", |p| _ = p.subpaths[0].insert_anchor(1520, 0.5)), ("an anchor deleted", |p| _ = p.subpaths[0].anchors.remove(1521)), ("the subpath closed", |p| p.subpaths[0].closed = true), @@ -454,7 +454,7 @@ fn random_edit(g: &mut Seeded, p: &mut PathData) -> String { 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))), + _ => sp.anchors.iter_mut().skip(at).take(n / 10).for_each(|a| moved(a, Vec2::new(1.0, -1.0))), } format!("anchors moved at {at} of subpath {s} of {n}") } @@ -486,7 +486,7 @@ fn same_as_made_again(k: &mut crate::kept_paths::KeptPath, n: &Arc, st: &S 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 closed = |sp: SubPath| SubPath::of(sp.anchors, true); let path = PathData::new(vec![ zigzag(2, 11_990.0), closed(zigzag(3, 12_000.0)), @@ -517,7 +517,7 @@ fn random_edits_of_a_long_compound_path_keep_what_making_its_path_again_makes() 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::new(vec![SubPath::of(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))), diff --git a/crates/svg/src/export/output.rs b/crates/svg/src/export/output.rs index 36f215d032..818a64e293 100644 --- a/crates/svg/src/export/output.rs +++ b/crates/svg/src/export/output.rs @@ -500,8 +500,8 @@ impl Writer<'_> { /// `` when its radii are written the same, else ``. fn ellipse_shape(&self, sp: &SubPath, m: Affine) -> Option<(&'static str, String)> { let tol = 10f64.powi(-i32::from(self.opts.decimals)); - let (anchors, closed) = (sp.anchors.as_slice(), sp.closed); - let [a0, a1, a2, a3] = anchors else { return None }; + let (anchors, closed) = (sp.anchors.contiguous(), sp.closed); + let [a0, a1, a2, a3] = &*anchors else { return None }; let [p0, p1, p2, p3] = [a0.p, a1.p, a2.p, a3.p].map(|p| m * p); let c = p0.midpoint(p2); let near = |a: f64, b: f64| (a - b).abs() <= tol; diff --git a/crates/three-d/src/solid/tests.rs b/crates/three-d/src/solid/tests.rs index a75821e696..0d9566e5e5 100644 --- a/crates/three-d/src/solid/tests.rs +++ b/crates/three-d/src/solid/tests.rs @@ -1457,7 +1457,7 @@ fn touching_contours_keep_their_parts_and_holes() { #[test] fn an_open_subpath_closes_straight_whether_or_not_contours_meet() { let open = vectorcraft_geom::SubPath { - anchors: vec![ + anchors: vectorcraft_geom::anchors![ vectorcraft_geom::Anchor { p: Point::new(0.0, 0.0), h_in: Point::new(0.0, -50.0), @@ -1505,7 +1505,7 @@ fn flat_far_and_too_large_merges() { assert!(close(area(&cap), 200.0, 1e-3), "joined, not lost"); // Handles count too: a lobe whose handles reach 1.3e8 widens the boolean's snapping to 1.85e-6. let lobe = vectorcraft_geom::SubPath { - anchors: vec![ + anchors: vectorcraft_geom::anchors![ vectorcraft_geom::Anchor { p: Point::new(0.0, 1000.0), h_in: Point::new(0.0, 1000.0), @@ -1604,7 +1604,7 @@ fn meets_within_twice_the_boolean_tolerance() { // What the boolean is given decides the tolerance: an open subpath's end handles (closed // straight) and a one-anchor subpath (skipped) don't widen it. let ends = vectorcraft_geom::SubPath { - anchors: vec![ + anchors: vectorcraft_geom::anchors![ vectorcraft_geom::Anchor { p: Point::new(0.0, 0.0), h_in: Point::new(1e9, 0.0), @@ -1620,7 +1620,8 @@ fn meets_within_twice_the_boolean_tolerance() { ], closed: false, }; - let speck = vectorcraft_geom::SubPath { anchors: vec![vectorcraft_geom::Anchor::corner(Point::new(1e9, 1e9))], closed: true }; + let speck = + vectorcraft_geom::SubPath { anchors: vectorcraft_geom::anchors![vectorcraft_geom::Anchor::corner(Point::new(1e9, 1e9))], closed: true }; let quiet = PathData::new(vec![ends, speck]); assert_eq!(rings::touch(&rings::closed_straight(&quiet)), 1e-6); // A closing-side handle at either end alone is enough to close the subpath straight. @@ -1653,7 +1654,7 @@ fn the_merged_rings_are_turned_as_a_whole() { #[test] fn a_one_anchor_subpath_builds_nothing() { let loop_ = vectorcraft_geom::SubPath { - anchors: vec![vectorcraft_geom::Anchor { + anchors: vectorcraft_geom::anchors![vectorcraft_geom::Anchor { p: Point::new(0.0, 0.0), h_in: Point::new(-20.0, 30.0), h_out: Point::new(20.0, 30.0), diff --git a/crates/tools/src/addons/extend_path.rs b/crates/tools/src/addons/extend_path.rs index 2269396f90..816000ac9f 100644 --- a/crates/tools/src/addons/extend_path.rs +++ b/crates/tools/src/addons/extend_path.rs @@ -178,10 +178,10 @@ impl Tool for ExtendPathTool { let (k, n) = (end.sp.anchors.len(), ext.anchors.len()); // From the old end on; a stretched last curve (no anchor added) shows whole. let from = if n > k { k - 1 } else { n.saturating_sub(2) }; - let tail = if end.start { ext.anchors.get(..n - from) } else { ext.anchors.get(from..) }; + let tail = if end.start { ext.anchors.get_range(..n - from) } else { ext.anchors.get_range(from..) }; if let Some(tail) = tail { let mut bp = vectorcraft_geom::BezPath::new(); - SubPath::new(tail.to_vec(), false).to_bezpath_into(&mut bp); + SubPath::new(tail, false).to_bezpath_into(&mut bp); out.push(Overlay::Path { path: bp, color: GREY, width: 1.0, dashed: true }); } } diff --git a/crates/tools/src/addons/smart_pen.rs b/crates/tools/src/addons/smart_pen.rs index c25ab8f670..43269ed580 100644 --- a/crates/tools/src/addons/smart_pen.rs +++ b/crates/tools/src/addons/smart_pen.rs @@ -478,9 +478,9 @@ impl SmartPenTool { } else { straighten_segment(&mut sp, prev); } - let tail = sp.anchors.get(prev..)?; + let tail = sp.anchors.get_range(prev..)?; let mut bp = BezPath::new(); - SubPath::new(tail.to_vec(), false).to_bezpath_into(&mut bp); + SubPath::new(tail, false).to_bezpath_into(&mut bp); Some(bp) } } diff --git a/crates/tools/src/distort/liquify.rs b/crates/tools/src/distort/liquify.rs index efc68132a1..0dc599444f 100644 --- a/crates/tools/src/distort/liquify.rs +++ b/crates/tools/src/distort/liquify.rs @@ -547,7 +547,7 @@ fn simplify(sp: &mut SubPath, tol: f64, region: Rect) { None => out[0].h_in = h_in, } } - sp.anchors = out; + sp.anchors = out.into(); } /// Apply a liquify stroke (its dabs, at the brush's intensity) to `path`. `salt` decorrelates diff --git a/crates/tools/src/pen/tests.rs b/crates/tools/src/pen/tests.rs index 32c91318e6..badb8193a7 100644 --- a/crates/tools/src/pen/tests.rs +++ b/crates/tools/src/pen/tests.rs @@ -607,7 +607,7 @@ fn a_drag_from_the_first_anchor_always_closes() { fn alt_mid_close_keeps_the_turned_outgoing_handle() { use vectorcraft_geom::{Anchor, SubPath}; let anchors = vec![Anchor::smooth(Point::new(100.0, 300.0), Point::new(150.0, 300.0)), Anchor::corner(Point::new(200.0, 200.0))]; - let (mut d, id, s) = drawing(SubPath { anchors, closed: false }); + let (mut d, id, s) = drawing(SubPath::new(anchors, false)); let p = paint(); let mut t = PenTool { drawing: true, ..PenTool::default() }; assert_eq!(t.pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::Down, 100.0, 300.0)), close_press(id, Point::new(100.0, 300.0))); @@ -634,7 +634,7 @@ fn alt_mid_close_keeps_the_turned_outgoing_handle() { fn space_moves_the_closing_anchor() { use vectorcraft_geom::{Anchor, SubPath}; let anchors = vec![Anchor::smooth(Point::new(100.0, 300.0), Point::new(150.0, 300.0)), Anchor::corner(Point::new(200.0, 200.0))]; - let (mut d, id, s) = drawing(SubPath { anchors, closed: false }); + let (mut d, id, s) = drawing(SubPath::new(anchors, false)); let p = paint(); let mut t = PenTool { drawing: true, ..PenTool::default() }; assert_eq!(t.pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::Down, 100.0, 300.0)), close_press(id, Point::new(100.0, 300.0))); diff --git a/crates/ui-egui/src/dialogs/freehand/tests.rs b/crates/ui-egui/src/dialogs/freehand/tests.rs index 1b41fc0886..3a73d6f3bb 100644 --- a/crates/ui-egui/src/dialogs/freehand/tests.rs +++ b/crates/ui-egui/src/dialogs/freehand/tests.rs @@ -172,7 +172,7 @@ fn live_curve_fitting_off_shows_the_points_until_the_release_fits_them() { let pointer = |kind, (x, y)| vectorcraft_tools::PointerEvent::new(kind, x, y); let newest = |app: &VectorcraftApp| { let doc = &app.session.doc().unwrap().doc; - doc.layers[0].children().unwrap().last().unwrap().path_data().unwrap().subpaths[0].anchors.clone() + doc.layers[0].children().unwrap().last().unwrap().path_data().unwrap().subpaths[0].anchors.to_vec() }; crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Down, pts[0]), view); for p in &pts[1..] { diff --git a/crates/ui-egui/src/dialogs/tests_modal.rs b/crates/ui-egui/src/dialogs/tests_modal.rs index c202094102..06eef4d2c6 100644 --- a/crates/ui-egui/src/dialogs/tests_modal.rs +++ b/crates/ui-egui/src/dialogs/tests_modal.rs @@ -532,7 +532,7 @@ fn simplify_slider_and_dialog_simplify_only_the_direct_selected_part_of_a_path() let anchors: Vec<_> = (0..=40).map(|i| json!({"x": 20 + i * 8, "y": 150.0 + 60.0 * (f64::from(i) / 6.0).sin()})).collect(); let id = app.run("path.create", json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap(); let path = |app: &VectorcraftApp| { - app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).and_then(|n| n.path_data()).expect("a path").subpaths[0].anchors.clone() + app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).and_then(|n| n.path_data()).expect("a path").subpaths[0].anchors.to_vec() }; let original = path(&app); let run: Vec<_> = (10..=30).map(|i| json!([0, i])).collect(); diff --git a/crates/ui-egui/src/tests/taskbar.rs b/crates/ui-egui/src/tests/taskbar.rs index 1e3d946100..4a48309dbe 100644 --- a/crates/ui-egui/src/tests/taskbar.rs +++ b/crates/ui-egui/src/tests/taskbar.rs @@ -315,7 +315,7 @@ fn point_type_offers_dynamic_text() { /// The path's anchors (its first subpath). fn anchors(app: &VectorcraftApp, id: u64) -> Vec { match kind(app, id) { - NodeKind::Path { path, .. } => path.subpaths[0].anchors.clone(), + NodeKind::Path { path, .. } => path.subpaths[0].anchors.to_vec(), k => panic!("not a path: {k:?}"), } } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index bbd91681ac..4075f57123 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -2000,7 +2000,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur When the stroke of a path moved this way has a gradient along or across it, the renderer also draws the gradient from the slices it made before the move, moved by the same offset. It makes the slices again when the path is scaled, rotated or edited, and when the stroke or its gradient changes. The pixels are the same. On Windows at 1600 × 1000, the frame after a 1 pt nudge of an open path of 200,000 anchors takes 75 ms instead of 1,237 ms with the gradient along the stroke and 69 ms instead of 1,420 ms with it across, and with an open path of 50,000 anchors 22 ms instead of 313 ms and 21 ms instead of 352 ms (medians of five interleaved runs). When an edit changes a 3D effect (Extrude & Bevel, Inflate, Rotate, Revolve), the canvas draws again where its art was and is, measured by that art: type and groups were measured by their letters or members, so a shallower or turned solid left pieces of the old one on the canvas. When the canvas draws the part on its own thread, it measures through its renderer and draws the part with the art it made to measure it. Exports and Object › Rasterize take the solid of 3D type, where they cropped it to the letters, and Fit to Artwork Bounds and per-artboard SVG export measure every artboard through one renderer, building each solid once unless the object has brushed strokes: 30 extruded type objects measured over 30 artboards take about 2 ms in a release build, where measuring their letters took under 1 ms. A document's first frame builds every 3D object's solid, off the view too, to know whether it reaches into the view. When only anchors of a path or compound path move, the canvas draws again only around the segments that start or end at a moved anchor, grown by the farthest its strokes reach, instead of the whole object before and after. This holds while every fill and stroke is a solid color, and the strokes have no dashes, width profile, arrowheads or brush, and the object has no effects or opacity mask; any other object still counts whole. On a loaded Windows machine at 1600 × 1200, single-threaded in a development build, the frame after one anchor of a filled closed path moves takes 16.6 million CPU cycles instead of 56.4 million with 50,000 anchors and 65.6 million instead of 180.0 million with 200,000 anchors (medians of 15 edits, the two builds run in turn three times). - The check after an edit that a selected path or compound path stays within the canvas passes without measuring the path when every anchor and handle of it is within the canvas, and measures it only when one is not. Moving one anchor of a selected open path of 10,000 anchors (`path.moveAnchor`, `path.setHandle`, `path.moveAnchors` or a nudge) measured all 9,999 segments of the path and now measures at most 16 (counted in the engine's tests). The edit still copies the path's anchors for undo, and the renderer still compares every anchor of the path to find the moved ones. + The check after an edit that a selected path or compound path stays within the canvas passes without measuring the path when every anchor and handle of it is within the canvas, and measures it only when one is not. Moving one anchor of a selected open path of 10,000 anchors (`path.moveAnchor`, `path.setHandle`, `path.moveAnchors` or a nudge) measured all 9,999 segments of the path and now measures at most 16 (counted in the engine's tests). + A subpath keeps its anchors in chunks of up to 256 anchors, and the versions of a path share the chunks they have not changed. Editing one anchor copies only the chunk that holds it, so its undo step keeps that chunk instead of a copy of every anchor; adding or deleting an anchor copies the chunk it is in and rebuilds the subpath's table of chunks. When the renderer looks for the anchors that moved, it compares only the chunks the edit copied. Moving one anchor of a selected open path of 10,000 or 200,000 anchors (`path.moveAnchor`, `path.setHandle`, `path.moveAnchors` or a nudge) keeps at most 48 KiB in the undo step and in the redo step, where it kept a copy of every anchor at 56 bytes each (counted in the engine's tests). Files are written and read as before, each subpath's anchors as one list. Opening a document keeps the layouts it makes for its type objects, up to 128 MiB, and the first frame uses them instead of laying those objects out again. It does not keep a layout with bitmap or color glyphs or a text object with a gradient. The frame uses a layout only for a text object equal to the one laid out, and only while the fonts and the hyphenation exceptions have not changed. It uses each layout once. The renderer drops the rest when a frame that used one ends, and all of them after eight frames that use none. The frames of a threaded story are laid out while the file is read, and the first frame lays them out again. The pixels are the same. On Windows at 1600 × 1200, in medians of seven interleaved rounds of a local example, counted in the main thread's CPU cycles because other builds were running: with 5,000 point type objects in view the first frame takes 409 million cycles instead of 894 million and the open 816 million instead of 665 million, so the two together take 1,225 million instead of 1,559 million; with one area type object of 500 paragraphs the first frame takes 697 million instead of 1,193 million and the open 535 million instead of 568 million. Type › Show Hidden Characters keeps the marks it places on each text object from one frame to the next. The canvas lays a text object out again when an edit changes it or its threaded story, or when the last frame that showed the marks did not draw it, and it lays out all type in view again when the fonts or the hyphenation exceptions change. With 1,000 two-line labels in view, drawing the marks takes 0.8 ms per frame in a release build instead of 19.7 ms. A selection with more than 100,000 points to outline shows its bounds instead of each object's outline and anchors. Paths count their anchors. Area type and type on a path count the anchors of their frame or path, and point type counts six: its box's corners and the ends of its baseline marker. Images, symbol instances, placed documents, and area type or type on a path in perspective count the four corners of their box, and gradient meshes and envelopes count their points and the ends of their mesh lines. The hover highlight leaves out an object past that count, and such an object shows its bounds as the key object. With 32,768 type objects, images, symbol instances, gradient meshes or envelopes selected, the selection's outlines and anchors add fewer than 100 vertices to the canvas's mesh (#2460).