mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
Fix #2723: Reshape falloff uses arc length along the path
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Cursor
parent
f2601137ab
commit
e8e88c14f2
@@ -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 along the path (default: half the subpath's length)",
|
||||
has_doc,
|
||||
reshape
|
||||
),
|
||||
@@ -680,21 +680,22 @@ 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;
|
||||
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 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();
|
||||
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);
|
||||
(1.0 - q * q).powi(2)
|
||||
};
|
||||
let v = delta * w;
|
||||
let v = delta * weights[i];
|
||||
a.p += v;
|
||||
a.h_in += v;
|
||||
a.h_out += v;
|
||||
|
||||
@@ -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,38 @@ 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-6);
|
||||
pos.push(pos[i] + 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);
|
||||
|
||||
Reference in New Issue
Block a user