Blend: moving an end key of a closed replaced spine moves its end (#6809)

On a closed spine from Replace Spine both end keys sit on its first
point, so moving one moved that point onto it and then back onto the
other. Once a key is moved off, the spine is now opened at its first
point, with a copy of it at the end taking the closing segment and the
last key.
This commit is contained in:
HassanKhan
2026-11-12 13:15:22 +05:00
parent aec586b840
commit a8b5a5b6c9
2 changed files with 46 additions and 1 deletions
+17 -1
View File
@@ -1085,8 +1085,9 @@ fn layout_spine(keys: &[Arc<Node>], spec: &BlendSpec) -> Option<(PathData, Optio
if Spine::new(&path).is_none_or(|sp| keys_spread_along(keys, &sp)) {
return Some((path, None));
}
let Some(ka) = spread_anchors(&mut path, keys.len()) else { return Some((path, None)) };
let Some(mut ka) = spread_anchors(&mut path, keys.len()) else { return Some((path, None)) };
let mut first = path.subpaths.into_iter().next()?;
open_at_start(&mut first, &mut ka);
for (k, &ai) in keys.iter().zip(&ka) {
if let Some(a) = first.anchors.get_mut(ai) {
let d = center_of(k) - a.p;
@@ -1132,6 +1133,21 @@ fn keys_spread_along(keys: &[Arc<Node>], spine: &Spine) -> bool {
})
}
/// Open closed spine `sp` at its first anchor when the last key sits there with the first (as
/// [`spread_anchors`] puts them): a copy of that anchor at the end takes the closing segment and
/// the last key, so the two end keys move their ends of the spine apart.
fn open_at_start(sp: &mut SubPath, ka: &mut [usize]) {
let Some(&start) = sp.anchors.first() else { return };
if !sp.closed || ka.len() < 2 {
return;
}
if let Some(last) = ka.last_mut().filter(|a| **a == 0) {
*last = sp.anchors.len();
sp.anchors.push(start);
sp.closed = false;
}
}
/// The anchors of `path`'s first subpath that `k` keys spread evenly along it by arc length sit
/// on, splitting segments where none is.
fn spread_anchors(path: &mut PathData, k: usize) -> Option<Vec<usize>> {
+29
View File
@@ -455,6 +455,35 @@ fn moving_a_key_of_a_replaced_spine_blend_moves_what_is_drawn() {
assert!(center(&back[1]).distance(center(&before[1])) < 1e-6);
}
/// #6809 on a closed spine, where both end keys start on its first point: moving the first key
/// moves its end of the spine without dragging the last key's end along.
#[test]
fn moving_the_first_key_of_a_closed_replaced_spine_blend_moves_what_is_drawn() {
let mut s = session();
let (a, _, g) = blend(&mut s, json!({"steps": 5}));
let square = id_of(&s.execute("path.create", &json!({"d": "M100 100 L300 100 L300 300 L100 300 Z"})).unwrap());
sel(&mut s, &[g, square]);
s.execute("object.blend.replaceSpine", &json!({})).unwrap();
let center = |n: &Node| n.geometric_bounds().unwrap().center();
let before = steps_of(&s, g);
assert!(center(&before[0]).distance(Point::new(100.0, 100.0)) < 1e-6);
sel(&mut s, &[a]);
s.execute("object.move", &json!({"dx": -50, "dy": -40})).unwrap();
let key = center(&node(&s, a));
assert!(key.distance(Point::new(50.0, 60.0)) < 1e-6, "{key:?}");
let after = steps_of(&s, g);
assert_eq!(after.len(), before.len());
let drawn = center(&after[0]);
assert!(drawn.distance(key) < 1e-6, "the key is drawn where it was moved to: {drawn:?}, outline at {key:?}");
assert!(center(&after[1]).distance(center(&before[1])) > 1.0, "the first step follows the moved key");
let last = center(&after[after.len() - 1]);
assert!(last.distance(Point::new(100.0, 100.0)) < 1e-6, "the other key stays put: {last:?}");
s.execute("edit.undo", &json!({})).unwrap();
let back = steps_of(&s, g);
assert!(center(&back[0]).distance(Point::new(100.0, 100.0)) < 1e-6);
assert!(center(&back[1]).distance(center(&before[1])) < 1e-6);
}
#[test]
fn blend_align_to_path_rotates_steps() {
let mut s = session();