From 4149a09d617f27f6e03a48ebd359c952eddea364 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 12 Oct 2026 06:54:09 +0700 Subject: [PATCH] Direct Selection: a Shift-click on a segment pick's endpoint selects that anchor (#1278) The endpoints of a selected segment are hollow, so select.anchors and select.anchorsMany now start from no anchors of that path instead of toggling the endpoint off. --- crates/engine/src/cmd/select.rs | 9 +++++++-- crates/engine/src/tests_draw2.rs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index 4e306575..b3dee740 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -377,7 +377,10 @@ fn anchors(s: &mut Session, p: &Value) -> Result { // A path selected as a whole has every anchor selected: toggle and add start from them all. let whole = sel.contains(id) && sel.partial(id).is_none(); sel.add(id); - sel.segments.remove(&id); + // A segment pick's endpoints aren't selected anchors: start from none of them. + if sel.segments.remove(&id).is_some() { + sel.anchors.remove(&id); + } let e = sel.anchors.entry(id).or_default(); if whole && let Some(path) = d.node(id).and_then(|n| n.path_data()) { e.extend(path.anchors().map(|(s, i, _)| (s, i))); @@ -416,7 +419,8 @@ fn anchors_many(s: &mut Session, p: &Value) -> Result { sel.clear(); } for (id, refs) in items { - sel.segments.remove(&id); + // A segment pick's endpoints aren't selected anchors: start from none of them. + let segment_pick = sel.segments.remove(&id).is_some(); let all: BTreeSet<(usize, usize)> = d.node(id).and_then(|n| n.path_data()).map(|p| p.anchors().map(|(s, i, _)| (s, i)).collect()).unwrap_or_default(); // Anything but a path is selected, toggled or deselected as a whole. @@ -430,6 +434,7 @@ fn anchors_many(s: &mut Session, p: &Value) -> Result { } // From the anchors selected now (all of them for a path selected as a whole). let now = match sel.partial(id) { + _ if segment_pick => BTreeSet::new(), Some(a) => a.clone(), None if sel.contains(id) => all.clone(), None => BTreeSet::new(), diff --git a/crates/engine/src/tests_draw2.rs b/crates/engine/src/tests_draw2.rs index 2eaa068a..b5a6aa2d 100644 --- a/crates/engine/src/tests_draw2.rs +++ b/crates/engine/src/tests_draw2.rs @@ -724,6 +724,25 @@ fn direct_segment_delete_keeps_the_other_rectangle_edges_and_one_undo() { } } +/// A segment pick's endpoints are hollow: Shift-clicking or Shift-marqueeing one selects that +/// anchor, rather than toggling it off and leaving the other endpoint selected. +#[test] +fn selecting_an_anchor_after_a_segment_pick_starts_from_no_anchors() { + let mut s = session(); + let id = rect(&mut s, 100.0, 100.0, 200.0, 150.0); + let segment = json!({"id": id.0, "segments": [[0, 0]]}); + s.execute("select.segments", &segment).unwrap(); + s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 0]], "mode": "toggle"})).unwrap(); + let sel = &s.doc().unwrap().selection; + assert_eq!(sel.partial(id), Some(&[(0, 0)].into())); + assert!(sel.segments.is_empty()); + s.execute("select.segments", &segment).unwrap(); + s.execute("select.anchorsMany", &json!({"items": [{"id": id.0, "anchors": [[0, 1]]}], "mode": "add"})).unwrap(); + let sel = &s.doc().unwrap().selection; + assert_eq!(sel.partial(id), Some(&[(0, 1)].into())); + assert!(sel.segments.is_empty()); +} + #[test] fn segment_delete_preserves_curves_appearance_and_unrelated_contours() { let data = "M100 100C110 70 150 130 170 100C180 75 230 125 250 100C270 140 280 80 310 100C320 120 140 140 100 100Z M400 100L500 100L500 200Z";