From 4f526b4e3f383487109dea013b5154eae1fa5746 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 19:52:07 +0700 Subject: [PATCH] Reshape: the lengths along the path are measured once per drag, and the default reach stays the path's size (#2900) anchor_arc_distance measured every segment again for each anchor, so a reshape cost the square of the anchor count; path.reshape now measures the positions once, and the two helpers only it used are gone. The default radius stays half the subpath's bounding-box diagonal, now measured along the path: half its length reached the far corners of a square, moving art the grab is nowhere near (reshape_moves_the_grabbed_point_and_its_neighbourhood). --- crates/engine/src/cmd/path.rs | 40 +++++++++++++++++++++-------------- crates/engine/src/tests.rs | 4 ++-- crates/geom/src/path.rs | 23 ++------------------ 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/crates/engine/src/cmd/path.rs b/crates/engine/src/cmd/path.rs index 4d3b5d870..0e2a8e0b0 100644 --- a/crates/engine/src/cmd/path.rs +++ b/crates/engine/src/cmd/path.rs @@ -100,7 +100,7 @@ pub fn specs() -> Vec { "Reshape", [], None, - "{id, x, y, dx, dy, tol=3, radius?} Reshape tool: grab the path at (x, y) (adding an anchor there unless one is within tol) and drag it by (dx, dy); nearby anchors follow with a smooth falloff over radius along the path (default: half the subpath's length)", + "{id, x, y, dx, dy, tol=3, radius?} Reshape tool: grab the path at (x, y) (adding an anchor there unless one is within tol) and drag it by (dx, dy); nearby anchors follow with a smooth falloff over radius, measured along the path (default: half the subpath's size)", has_doc, reshape ), @@ -680,22 +680,30 @@ fn reshape(s: &mut Session, p: &Value) -> Result { Some((i, dist)) if dist <= tol => i, _ => sp.insert_anchor(seg, t), }; - let r = radius.unwrap_or_else(|| (sp.arc_length() / 2.0).max(1.0)); - let weights: Vec = sp - .anchors - .iter() - .enumerate() - .map(|(i, _)| { - if i == grabbed { - 1.0 - } else { - let q = (sp.anchor_arc_distance(grabbed, i) / r).min(1.0); - (1.0 - q * q).powi(2) - } - }) - .collect(); + // Each anchor's distance along the path from the grabbed one, the shorter way round a + // closed path: the lengths are measured once, as a path can have many anchors. + let pos = sp.anchor_arc_positions(); + let total = pos.last().copied().unwrap_or(0.0); + // The default reach is the subpath's size, as before, now measured along it. + let r = radius.unwrap_or_else(|| { + let mut bp = vectorcraft_geom::BezPath::new(); + sp.to_bezpath_into(&mut bp); + let b = vectorcraft_geom::Shape::bounding_box(&bp); + (b.width().hypot(b.height()) / 2.0).max(1.0) + }); + let from = pos.get(grabbed).copied().unwrap_or(0.0); + let closed = sp.closed; for (i, a) in sp.anchors.iter_mut().enumerate() { - let v = delta * weights[i]; + let w = if i == grabbed { + 1.0 + } else { + let d = (pos.get(i).copied().unwrap_or(f64::INFINITY) - from).abs(); + let d = if closed { d.min(total - d) } else { d }; + // A distance or radius that isn't finite gives NaN here, which `min` reads as 1: no move. + let q = (d / r).min(1.0); + (1.0 - q * q).powi(2) + }; + let v = delta * w; a.p += v; a.h_in += v; a.h_out += v; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 53278f3b3..06c7ec2a4 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2355,8 +2355,8 @@ fn reshape_falloff_follows_arc_length_on_open_paths() { }), ) .unwrap()["id"] - .as_u64() - .unwrap(), + .as_u64() + .unwrap(), ); s.execute("path.reshape", &json!({"id": id.0, "x": 300.0, "y": 0.0, "dx": 0.0, "dy": 100.0})).unwrap(); let pts: Vec<_> = s.doc().unwrap().doc.node(id).unwrap().path_data().unwrap().subpaths[0].anchors.iter().map(|a| a.p).collect(); diff --git a/crates/geom/src/path.rs b/crates/geom/src/path.rs index 1a7bfbde8..110997efc 100644 --- a/crates/geom/src/path.rs +++ b/crates/geom/src/path.rs @@ -229,30 +229,11 @@ impl SubPath { } let mut pos = vec![0.0]; for i in 0..self.segment_count() { - let len = kurbo::ParamCurveArclen::arclen(&self.segment(i), 1e-6); - pos.push(pos[i] + len); + let len = kurbo::ParamCurveArclen::arclen(&self.segment(i), 1e-3); + pos.push(pos.last().copied().unwrap_or(0.0) + len); } pos } - /// Total length of all segments. - pub fn arc_length(&self) -> f64 { - self.anchor_arc_positions().last().copied().unwrap_or(0.0) - } - /// Shortest distance along this subpath between two anchors. - pub fn anchor_arc_distance(&self, a: usize, b: usize) -> f64 { - let pos = self.anchor_arc_positions(); - let n = self.anchors.len(); - if a >= n || b >= n { - return f64::INFINITY; - } - let d = (pos[a] - pos[b]).abs(); - if self.closed { - let total = pos.last().copied().unwrap_or(0.0); - if total > 0.0 { d.min(total - d) } else { d } - } else { - d - } - } pub fn to_bezpath_into(&self, out: &mut BezPath) { let Some(first) = self.anchors.first() else { return }; out.move_to(first.p);