Paths: editing one anchor of a long path keeps one chunk of anchors for undo

A subpath's anchors are now an `Anchors` list held in chunks of up to
256 anchors behind `Arc`s. Copying a path copies the chunk table and
shares the chunks, and editing one anchor copies only the chunk that
holds it, so the undo step of a one-anchor edit no longer keeps a copy
of every anchor of the path. Inserting or removing an anchor copies the
chunk it is in and rebuilds the table. Reads and edits follow the Vec
and slice methods the code used; files write and read each subpath's
anchors as one list, as before, within the same anchor read budget.

The undo memory estimate counts only the chunks the other version of
the path does not hold, and the renderer's kept paths compare only the
chunks an edit copied when they look for moved anchors.

Moving one anchor of an open path of 10,000 or 200,000 anchors
(path.moveAnchor, path.setHandle, path.moveAnchors, a nudge) keeps at
most 48 KiB in its undo and redo steps, where it kept 56 bytes per
anchor.
This commit is contained in:
dotCooCoo
2026-11-14 21:25:15 -08:00
parent 4830ea0e83
commit a5493a1cd0
36 changed files with 979 additions and 108 deletions
+31 -3
View File
@@ -276,7 +276,13 @@ fn node_bytes(n: &Node, other: Option<&Node>, counted: &mut HashSet<usize>) -> 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, .. }
@@ -293,8 +299,30 @@ fn node_bytes(n: &Node, other: Option<&Node>, counted: &mut HashSet<usize>) -> 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::<Anchor>() }).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<usize> =
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::<Vec<Anchor>>() + 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::<Anchor>())
.sum();
if table == 0 { own } else { ALLOC + table + own }
})
.sum();
anchors + if p.subpaths.capacity() == 0 { 0 } else { ALLOC + p.subpaths.capacity() * size_of::<SubPath>() }
}
@@ -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:?}");
+2 -1
View File
@@ -356,7 +356,8 @@ fn mirror_open(sp: &SubPath, axis: &Axis, m: Affine) -> Vec<SubPath> {
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));
+6 -6
View File
@@ -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<f64>, 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<Anchor>, Vec<Anchor>) = 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() {
@@ -271,12 +271,12 @@ pub(super) fn freehand(s: &mut Session, p: &Value) -> Result<Value> {
// 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
+11 -11
View File
@@ -274,7 +274,7 @@ fn refit_run(anchors: &[Anchor], tol: f64, sigma: f64, free: [bool; 2]) -> Vec<A
for a in &mut out {
*a = Anchor::with_handles(a.p, a.h_in, a.h_out);
}
out
out.into_vec()
}
/// Distance (points) from an anchor within which a smoothed closed path starts at that anchor
@@ -549,7 +549,7 @@ fn smooth_subpath(sp: &SubPath, pts: &[Point], radius: f64, share: usize, tol: f
if sp.closed {
let start = sp.anchors[0].p;
let Some(k) = marked.iter().position(|m| !m) else {
let corners = smooth_corners(&sp.anchors, true, radius);
let corners = smooth_corners(&sp.anchors.contiguous(), true, radius);
let Some(c) = corners.iter().position(|c| *c) else {
// Everything is in range and nothing is a corner: smooth the whole closed path,
// across its start too.
@@ -557,7 +557,7 @@ fn smooth_subpath(sp: &SubPath, pts: &[Point], radius: f64, share: usize, tol: f
return f.subpaths.into_iter().next().map(|r| start_nearest(r, start, sp));
};
// Cut the path open at a corner, which then ends the last part as well as starting the first.
let mut open = sp.anchors.clone();
let mut open = sp.anchors.to_vec();
open.rotate_left(c);
let mut cut = corners;
cut.rotate_left(c);
@@ -576,15 +576,14 @@ fn smooth_subpath(sp: &SubPath, pts: &[Point], radius: f64, share: usize, tol: f
return Some(start_nearest(SubPath::new(res, true), start, sp));
};
// Rotate so an untouched anchor comes first, duplicate it at the end and treat as open.
let mut open = sp.clone();
open.anchors.rotate_left(k);
let mut open = sp.anchors.to_vec();
open.rotate_left(k);
let mut m = marked.clone();
m.rotate_left(k);
open.anchors.push(open.anchors[0]);
open.push(open[0]);
m.push(false);
open.closed = false;
let mut res = smooth_open(&open.anchors, &m, near, tol, sigma, radius, false);
if same_anchors(&res, &open.anchors) {
let mut res = smooth_open(&open, &m, near, tol, sigma, radius, false);
if same_anchors(&res, &open) {
return None;
}
let l = res.pop()?;
@@ -595,8 +594,9 @@ fn smooth_subpath(sp: &SubPath, pts: &[Point], radius: f64, share: usize, tol: f
let res = SubPath::new(res, true);
return Some(if k == 0 { res } else { start_nearest(res, start, sp) });
}
let res = smooth_open(&sp.anchors, &marked, near, tol, sigma, radius, true);
(!same_anchors(&res, &sp.anchors)).then(|| SubPath::new(res, false))
let anchors = sp.anchors.contiguous();
let res = smooth_open(&anchors, &marked, near, tol, sigma, radius, true);
(!same_anchors(&res, &anchors)).then(|| SubPath::new(res, false))
}
/// The segment `c`, which ends in the drag's reach (`near`) and starts outside it, split where it
+19 -15
View File
@@ -5,7 +5,7 @@ use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_doc::{Document, Node, NodeId, NodeKind, Scaling, Selection};
use vectorcraft_geom::{Anchor, AnchorKind, PathData, Point, SubPath, Vec2};
use vectorcraft_geom::{Anchor, AnchorKind, Anchors, PathData, Point, SubPath, Vec2};
use super::create::anchor_from_json;
use super::draw2::usize_opt;
@@ -544,10 +544,12 @@ fn merge_coincident(sp: &mut SubPath, picked: impl Fn(usize) -> 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<usize>, next: Option<usize>, smooth: bool) {
fn join_point_between(anchors: &mut Anchors, ai: usize, prev: Option<usize>, next: Option<usize>, smooth: bool) {
let at = |j: Option<usize>| 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);
+1 -1
View File
@@ -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 {
+4 -4
View File
@@ -2131,7 +2131,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);
}
@@ -2166,7 +2166,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);
}
@@ -2225,7 +2225,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");
@@ -3315,7 +3315,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<vectorcraft_geom::Anchor> {
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.
+43 -2
View File
@@ -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]
+1 -1
View File
@@ -40,7 +40,7 @@ fn close(a: f64, b: f64) -> bool {
fn segment_angles(p: &PathData) -> Vec<f64> {
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()
+5 -5
View File
@@ -796,7 +796,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));
}
@@ -1124,7 +1124,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;
@@ -1478,7 +1478,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:?}");
@@ -1689,7 +1689,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();
@@ -3791,7 +3791,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);
+10 -10
View File
@@ -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}");
+2 -2
View File
@@ -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::<Vec<_>>(), b[..=10].iter().map(|x| x.p).collect::<Vec<_>>());
+1 -1
View File
@@ -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);
+2 -4
View File
@@ -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
+790
View File
@@ -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<Anchor>` 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<Arc<Vec<Anchor>>>,
/// 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<usize>,
/// 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<Vec<Anchor>>) -> 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<usize>) -> Option<Vec<Anchor>> {
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<Anchor> {
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<Anchor> {
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<R>(&mut self, f: impl FnOnce(&mut Vec<Anchor>) -> 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<Anchor> {
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<Anchor> {
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<usize>) -> std::vec::IntoIter<Anchor> {
self.with_vec(|v| v.drain(range).collect::<Vec<_>>()).into_iter()
}
/// Replace the anchors in `range` with those of `with`, and return the anchors replaced.
pub fn splice(&mut self, range: impl RangeBounds<usize>, with: impl IntoIterator<Item = Anchor>) -> std::vec::IntoIter<Anchor> {
self.with_vec(|v| v.splice(range, with).collect::<Vec<_>>()).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<Item = (usize, &'a Anchor, &'a Anchor)> + '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<Item = (usize, usize)> + '_ {
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::<Arc<Vec<Anchor>>>() + self.ends.capacity() * size_of::<usize>()
}
}
/// [`Anchors::iter_mut`]'s iterator.
pub type IterMut<'a> = std::iter::FlatMap<
std::slice::IterMut<'a, Arc<Vec<Anchor>>>,
std::slice::IterMut<'a, Anchor>,
fn(&mut Arc<Vec<Anchor>>) -> 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<Vec<Anchor>>>,
/// 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<usize>) {
(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<usize> 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<usize> 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<Vec<Anchor>> for Anchors {
fn from(v: Vec<Anchor>) -> 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<const N: usize> From<[Anchor; N]> for Anchors {
fn from(s: [Anchor; N]) -> Self {
Self::from(Vec::from(s))
}
}
impl From<Anchors> for Vec<Anchor> {
fn from(a: Anchors) -> Self {
a.into_vec()
}
}
impl FromIterator<Anchor> for Anchors {
fn from_iter<I: IntoIterator<Item = Anchor>>(it: I) -> Self {
Self::from(it.into_iter().collect::<Vec<_>>())
}
}
impl Extend<Anchor> for Anchors {
fn extend<I: IntoIterator<Item = Anchor>>(&mut self, it: I) {
for a in it {
self.push(a);
}
}
}
impl<'a> Extend<&'a Anchor> for Anchors {
fn extend<I: IntoIterator<Item = &'a Anchor>>(&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<Anchor>;
fn into_iter(self) -> std::vec::IntoIter<Anchor> {
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<Vec<Anchor>> for Anchors {
fn eq(&self, other: &Vec<Anchor>) -> bool {
*self == **other
}
}
impl<const N: usize> PartialEq<[Anchor; N]> for Anchors {
fn eq(&self, other: &[Anchor; N]) -> bool {
*self == other[..]
}
}
impl PartialEq<Anchors> for [Anchor] {
fn eq(&self, other: &Anchors) -> bool {
*other == *self
}
}
impl PartialEq<Anchors> for Vec<Anchor> {
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<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_seq(self.iter())
}
}
impl<'de> Deserialize<'de> for Anchors {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Vec::<Anchor>::deserialize(d).map(Self::from)
}
}
#[cfg(test)]
mod tests {
use kurbo::Point;
use super::*;
fn line(n: usize) -> Vec<Anchor> {
(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<usize> {
let theirs: Vec<usize> = 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::<Vec<_>>(), v.iter().rev().copied().collect::<Vec<_>>());
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<usize> = 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);
}
}
+2
View File
@@ -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};
+14 -9
View File
@@ -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<T>(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<Vec<Anchor>, D::Error> {
fn anchors_within<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Anchors, D::Error> {
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = Vec<Anchor>;
type Value = Anchors;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a list of anchors")
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut a: A) -> Result<Vec<Anchor>, A::Error> {
let mut out = Vec::new();
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut a: A) -> Result<Anchors, A::Error> {
let mut out = Anchors::new();
while let Some(anchor) = a.next_element::<Anchor>()? {
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<Anchor>,
pub anchors: Anchors,
#[serde(default)]
pub closed: bool,
}
impl SubPath {
pub fn new(anchors: Vec<Anchor>, 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<Anchor>, BTreeSet<usize>) = 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<Anchor> = 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<SubPath>, 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;
}
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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()
}
+1 -1
View File
@@ -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),
}
@@ -377,7 +377,7 @@ fn splice(sp: &SubPath, st: Stretch, chain: &[CubicBez]) -> Option<SubPath> {
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| {
+3 -3
View File
@@ -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
}
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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))
}))
}
+4 -4
View File
@@ -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<Node>, 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))),
+2 -2
View File
@@ -500,8 +500,8 @@ impl Writer<'_> {
/// `<circle>` when its radii are written the same, else `<ellipse>`.
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;
+6 -5
View File
@@ -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),
+2 -2
View File
@@ -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 });
}
}
+2 -2
View File
@@ -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)
}
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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)));
+1 -1
View File
@@ -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..] {
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -314,7 +314,7 @@ fn point_type_offers_dynamic_text() {
/// The path's anchors (its first subpath).
fn anchors(app: &VectorcraftApp, id: u64) -> Vec<vectorcraft_geom::Anchor> {
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:?}"),
}
}
+2 -1
View File
@@ -1942,7 +1942,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).