From fe5955e52173875f55107f83ecc1941de2ff27a6 Mon Sep 17 00:00:00 2001 From: Michael Gieson Date: Sat, 10 Oct 2026 21:39:54 -0400 Subject: [PATCH] Direct Selection: pressing an anchor of a path selected whole keeps all its anchors A path with all its anchors selected is stored whole (no partial entry), so pressing one of its anchors looked unselected and replaced the selection with that anchor alone; the drag then moved just it. Now an anchor of a path selected whole counts as selected: the press keeps the selection and the drag moves every anchor. Shift-press still deselects that one anchor. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/tools/src/direct.rs | 22 +++++++++++++++++++++- crates/ui-egui/src/canvas.rs | 4 +++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index 739423b27..242903376 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -368,7 +368,8 @@ impl Tool for DirectSelectionTool { if cx.doc.node(id).is_some_and(is_area_type) { return self.press_type_area(cx, id, vec![(si, ai)], p, ev.mods.shift); } - let already = cx.selection.partial(id).is_some_and(|s| s.contains(&(si, ai))); + // A path with all its anchors selected is stored whole (no partial entry). + let already = cx.selection.contains(id) && cx.selection.partial(id).is_none_or(|s| s.contains(&(si, ai))); self.state = State::MoveAnchors { start: p, grab, began: false }; if ev.mods.shift { return vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[si, ai]], "mode": "toggle"}))]; @@ -838,6 +839,25 @@ mod tests { assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "set"}))]); } + /// Pressing an anchor of a path whose anchors are all selected (stored as the whole path) + /// keeps them all selected, so the drag moves them all; Shift still deselects that anchor. + #[test] + fn pressing_an_anchor_of_a_fully_selected_path_keeps_the_selection() { + let (d, id) = doc_with_rect(); + let mut s = Selection::default(); + s.set([id]); + let p = paint(); + let cx = cx(&d, &s, &p); + let mut t = DirectSelectionTool::new(false); + assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)).is_empty()); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 120.0, 100.0)); + assert!(matches!(a.as_slice(), [Action::Begin(_), Action::Preview(c, _)] if c == "path.moveAnchors"), "{a:?}"); + t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 120.0, 100.0)); + let shift = Mods { shift: true, ..Default::default() }; + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0).with_mods(shift)); + assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "toggle"}))]); + } + #[test] fn marquee_selects_anchors() { let (d, id) = doc_with_rect(); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index d9a71f1c2..05ff7230b 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2508,8 +2508,10 @@ mod tests { used }; let bounds = |app: &VectorcraftApp| app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap(); - // Direct Selection used last: the drag moves the rectangle's bottom-right corner only. + // Direct Selection used last: the drag moves the rectangle's bottom-right corner only (once + // the rectangle isn't selected whole, which would move all its anchors). app.select_tool("directSelection"); + app.session.execute("select.none", &json!({})).unwrap(); let corner = xf.to_screen(Point::new(200.0, 150.0)); assert_eq!(cmd_drag(&mut app, corner, corner + vec2(20.0, 20.0)), "directSelection"); let b = bounds(&app);