From 5e4794a65ccc315a4e9fd65f47cf331af5d97370 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 23:04:17 -0800 Subject: [PATCH] Align: a rejected select.key keeps the key anchor that was set select.key {id, anchor} set the new key anchor before checking it, so a point that is in the selection but not on the path cleared the key anchor and then returned an error. The point is now checked first: an anchor the path does not have is not a selected anchor point, and the other conditions are checked on a copy of the selection. --- crates/engine/src/cmd/select.rs | 14 ++++++++++---- crates/engine/src/tests/keyanchor.rs | 13 +++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index 9aa304e96e..7c8bbdda4d 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -670,14 +670,20 @@ fn key_anchor(s: &mut Session, id: Option, guide: Option, v: &Val return Err(bad(C, "give `anchor` or `guide`, not both")); } let id = id.ok_or_else(|| bad(C, "`anchor` needs `id`, the path the point is on"))?; - let sel = &s.doc()?.selection; - if sel.segments.contains_key(&id) || !sel.anchors.get(&id).is_some_and(|set| set.contains(&r)) { + let st = s.doc()?; + let sel = &st.selection; + let on_path = st.doc.node(id).and_then(|n| n.path_data()).is_some_and(|p| p.subpaths.get(r.0).is_some_and(|sp| r.1 < sp.anchors.len())); + if !on_path || sel.segments.contains_key(&id) || !sel.anchors.get(&id).is_some_and(|set| set.contains(&r)) { return Err(bad(C, format!("anchor [{}, {}] of object {id} is not a selected anchor point", r.0, r.1))); } - s.select(|_, sel| sel.key_anchor = Some((id, r)))?; - if s.doc()?.selection.key_anchor.is_none() { + // Check the new key on a copy first, so a request that fails leaves the key as it was. + let mut trial = sel.clone(); + trial.key_anchor = Some((id, r)); + trial.prune(&st.doc); + if trial.key_anchor.is_none() { return Err(bad(C, "select another anchor point to align to the key anchor")); } + s.select(|_, sel| sel.key_anchor = Some((id, r)))?; key_result(s) } diff --git a/crates/engine/src/tests/keyanchor.rs b/crates/engine/src/tests/keyanchor.rs index afa321ff73..0dc6675f2a 100644 --- a/crates/engine/src/tests/keyanchor.rs +++ b/crates/engine/src/tests/keyanchor.rs @@ -147,3 +147,16 @@ fn select_key_rejects_an_anchor_that_cannot_be_the_key() { assert!(err(&mut s, json!({"id": a.0, "anchor": [0, 0]})).contains("is not a selected anchor point")); assert_eq!(key_anchor(&s), None, "the key stays as it was"); } + +#[test] +fn a_rejected_select_key_keeps_the_key_anchor() { + let mut s = session(); + let a = path(&mut s, "M100 40 L200 60 L150 160 Z"); + s.execute("select.anchors", &json!({"id": a.0, "anchors": [[0, 0], [0, 9]], "mode": "set"})).unwrap(); + s.execute("select.anchors", &json!({"id": a.0, "anchors": [[0, 1]], "mode": "add"})).unwrap(); + assert_eq!(key_anchor(&s), Some((a, (0, 1)))); + // [0, 9] is in the selection but not on the path. + let err = s.execute("select.key", &json!({"id": a.0, "anchor": [0, 9]})).expect_err("a point the path does not have").to_string(); + assert!(err.contains("is not a selected anchor point"), "{err}"); + assert_eq!(key_anchor(&s), Some((a, (0, 1))), "the key stays as it was"); +}