mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2900 from nrq6993-a11y/stream/vc-issue-2723-reshape-falloff
Fix #2723: Reshape tool falloff follows the path, so nodes stretch progressively
This commit is contained in:
@@ -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 (default: half the subpath's size)",
|
||||
"{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,18 +680,27 @@ fn reshape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some((i, dist)) if dist <= tol => i,
|
||||
_ => sp.insert_anchor(seg, t),
|
||||
};
|
||||
let origin = sp.anchors[grabbed].p;
|
||||
// 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 w = if i == grabbed {
|
||||
1.0
|
||||
} else {
|
||||
let q = (a.p.distance(origin) / r).min(1.0);
|
||||
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;
|
||||
|
||||
@@ -2343,6 +2343,29 @@ fn reshape_moves_the_grabbed_point_and_its_neighbourhood() {
|
||||
assert!(pts.iter().filter(|p| (p.y - 100.0).abs() < 1e-9).count() == 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reshape_falloff_follows_arc_length_on_open_paths() {
|
||||
let mut s = session();
|
||||
let id = NodeId(
|
||||
s.execute(
|
||||
"path.create",
|
||||
&json!({
|
||||
"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 200, "y": 0}, {"x": 300, "y": 0}],
|
||||
"closed": false
|
||||
}),
|
||||
)
|
||||
.unwrap()["id"]
|
||||
.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();
|
||||
assert!((pts[3].y - 100.0).abs() < 1e-6, "grabbed end moves full delta: {pts:?}");
|
||||
assert!(pts[0].y.abs() < 1e-6, "far end stays put: {pts:?}");
|
||||
assert!(pts[2].y > 1.0 && pts[2].y < 100.0, "mid-anchor moves partially: {pts:?}");
|
||||
assert!(pts[1].y < pts[2].y, "nearer anchors move more: {pts:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_tool_drag_creates_a_graph_and_asks_for_data() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -221,6 +221,19 @@ impl SubPath {
|
||||
let b = &self.anchors[(i + 1) % n];
|
||||
!a.has_out() && !b.has_in()
|
||||
}
|
||||
/// Arc-length at each anchor from the start of this subpath (one entry per anchor).
|
||||
pub fn anchor_arc_positions(&self) -> Vec<f64> {
|
||||
let n = self.anchors.len();
|
||||
if n == 0 {
|
||||
return vec![];
|
||||
}
|
||||
let mut pos = vec![0.0];
|
||||
for i in 0..self.segment_count() {
|
||||
let len = kurbo::ParamCurveArclen::arclen(&self.segment(i), 1e-3);
|
||||
pos.push(pos.last().copied().unwrap_or(0.0) + len);
|
||||
}
|
||||
pos
|
||||
}
|
||||
pub fn to_bezpath_into(&self, out: &mut BezPath) {
|
||||
let Some(first) = self.anchors.first() else { return };
|
||||
out.move_to(first.p);
|
||||
|
||||
Reference in New Issue
Block a user