From acc708ec83fe1d24a09c89b686ddb924090ab83f Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 8 Oct 2026 04:54:14 +0700 Subject: [PATCH] Direct Selection: Alt on a segment still picks the whole path; ROADMAP (review of #363) With Alt held, a press on a path's stroke selects the whole path and an Alt-drag copies it, as before #363 and as Group Selection does in Illustrator; only a plain press on a segment selects its anchors and reshapes it. Records the segment drag in ROADMAP.md. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/tools/src/direct.rs | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index ca89a32af..8dbdbd6b2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -105,7 +105,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). - On Windows and Linux the app bar is the window's title bar (its own minimize, maximize and close buttons, drag to move, edges to resize), with the dragon app icon as the brand mark; dialogs and menus size to their content. As on a native menu bar, once one menu is open, moving the pointer onto another title opens that one. - **Tools:** - - **Selection:** Selection, Direct/Group Selection, Magic Wand, Lasso. + - **Selection:** Selection, Direct/Group Selection, Magic Wand, Lasso. Dragging a path segment with Direct Selection bends it when it's curved (its anchors stay) and moves its two anchors when it's straight, one undo step; the fill, Shift or Alt still pick the whole path (Alt-drag copies it). - **Saved selections:** Select ▸ Save Selection… asks for a name and keeps the selection in the document (saved with the file, undoable, up to 25); each one is listed at the bottom of the Select menu and a click selects its objects again; Edit Selection… lists them to rename or delete, applied together on OK. A file's saved selections are capped and checked against its objects when it opens. Not yet: Update Selection and the Layers panel's saved-selection menu. - **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join. Direction handles dragged with Direct Selection or the Anchor Point tool snap to smart guides, and Shift keeps them at 45° steps from their anchor. - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index 722953732..04febdf95 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -12,8 +12,8 @@ //! the text reflows at its size. //! //! A press on the stroke of a path that isn't selected as a whole selects that segment's two anchors -//! (the fill selects the whole path); dragging the segment bends it if it's curved, else moves its -//! anchors. +//! (the fill, or Alt, selects the whole path); dragging the segment bends it if it's curved, else +//! moves its anchors. use std::borrow::Cow; @@ -302,10 +302,12 @@ impl Tool for DirectSelectionTool { } if let Some(h) = hit_test(cx.doc, p, cx.hit_options()) { // A segment of a path that isn't selected as a whole: its two anchors get - // selected, and dragging it reshapes it if it's curved, else moves it. + // selected, and dragging it reshapes it if it's curved, else moves it. Alt picks + // the whole path (and Alt-drag copies it), as with Group Selection. let whole = cx.selection.contains(h.leaf) && cx.selection.partial(h.leaf).is_none(); if !whole && !ev.mods.shift + && !ev.mods.alt && matches!(h.kind, HitKind::Stroke | HitKind::Outline) && let Some(s) = segment_at(cx, h.leaf, p) { @@ -681,6 +683,13 @@ mod tests { let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 81.0)); assert_eq!(a[1], Action::Preview("path.moveAnchors".into(), json!({"dx": 0.0, "dy": -20.0}))); t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 81.0)); + // Alt on the stroke picks the whole path, and Alt-drag copies it. + let alt = Mods { alt: true, ..Default::default() }; + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 101.0).with_mods(alt)); + assert!(matches!(&a[..], [Action::Exec(c, _)] if c == "select.set"), "{a:?}"); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 81.0).with_mods(alt)); + assert!(matches!(&a[..], [Action::Begin(_), Action::Preview(c, v)] if c == "object.transform" && v["copy"] == true), "{a:?}"); + t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 81.0).with_mods(alt)); // The fill still selects the whole path, and a path selected as a whole still moves whole. let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 150.0)); assert!(matches!(&a[..], [Action::Exec(c, _)] if c == "select.set"), "{a:?}");