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).
This commit is contained in:
bflatastic
2026-10-18 20:12:47 +07:00
parent e8e88c14f2
commit 4f526b4e3f
3 changed files with 28 additions and 39 deletions
+24 -16
View File
@@ -100,7 +100,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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<Value> {
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<f64> = 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;
+2 -2
View File
@@ -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();
+2 -21
View File
@@ -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);