From 8796aa9d85b11d04ebe4db5a2f60348530f08487 Mon Sep 17 00:00:00 2001 From: Murat Vural Date: Wed, 14 Oct 2026 02:02:15 +0300 Subject: [PATCH] feat: marquee selection selects unlocked ruler guides --- crates/tools/src/direct.rs | 37 +++++++++++++++++- crates/tools/src/rulerguide.rs | 23 +++++++++++- crates/tools/src/select.rs | 69 +++++++++++++++++++++++++++++++++- docs/ui-parity.md | 2 +- 4 files changed, 127 insertions(+), 4 deletions(-) diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index 35c71926..56c68f0e 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -730,7 +730,22 @@ impl Tool for DirectSelectionTool { } sel.retain(|(id, _)| cx.doc.is_editable(*id)); let items: Vec = sel.iter().map(|(id, v)| json!({"id": id.0, "anchors": anchors_json(v)})).collect(); - vec![Action::Exec("select.anchorsMany".into(), json!({"items": items, "mode": if toggle { "toggle" } else { "set" }}))] + let guides = crate::rulerguide::guides_in_rect(cx, r); + let mut out = vec![]; + if toggle { + if !items.is_empty() { + out.push(Action::Exec("select.anchorsMany".into(), json!({"items": items, "mode": "toggle"}))); + } + if !guides.is_empty() { + out.push(Action::Exec("guide.select".into(), json!({ "indexes": guides, "toggle": true }))); + } + } else { + out.push(Action::Exec("select.anchorsMany".into(), json!({"items": items, "mode": "set"}))); + if !guides.is_empty() { + out.push(Action::Exec("guide.select".into(), json!({ "indexes": guides, "toggle": true }))); + } + } + out } _ => vec![], } @@ -1100,6 +1115,26 @@ mod tests { assert_eq!(a, vec![Action::Exec("select.anchorsMany".into(), json!({"items": [{"id": id.0, "anchors": [[0, 1]]}], "mode": "set"}))]); } + #[test] + fn marquee_selects_ruler_guides() { + let (mut d, id) = doc_with_rect(); + d.guides.push(vectorcraft_doc::Guide::new(false, 80.0)); + let s = Selection::default(); + let p = paint(); + let cx = cx(&d, &s, &p); + let mut t = DirectSelectionTool::new(false); + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 50.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 150.0)); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0)); + assert_eq!( + a, + vec![ + Action::Exec("select.anchorsMany".into(), json!({"items": [{"id": id.0, "anchors": [[0, 1]]}], "mode": "set"})), + Action::Exec("guide.select".into(), json!({"indexes": [0], "toggle": true})), + ] + ); + } + /// A path inside an inner group, a sibling path outside it, and the groups' ids. /// The inside path is the rectangle at (100, 100); the outside one at (300, 300). fn nested_group_doc() -> (vectorcraft_doc::Document, NodeId, NodeId, NodeId, NodeId) { diff --git a/crates/tools/src/rulerguide.rs b/crates/tools/src/rulerguide.rs index 477ebdc3..53f98bc6 100644 --- a/crates/tools/src/rulerguide.rs +++ b/crates/tools/src/rulerguide.rs @@ -9,7 +9,7 @@ //! or locked layer, can't be picked. use serde_json::json; -use vectorcraft_geom::Point; +use vectorcraft_geom::{Point, Rect}; use crate::guides::Targets; use crate::{Action, Cursor, Overlay, PointerEvent, PointerKind, ToolContext}; @@ -32,6 +32,27 @@ pub fn guide_at(cx: &ToolContext, p: Point) -> Option { .map(|(i, _)| i) } +/// The editable ruler guides intersecting marquee rectangle `r`. +pub fn guides_in_rect(cx: &ToolContext, r: Rect) -> Vec { + if !cx.guides { + return vec![]; + } + cx.doc + .guides + .iter() + .enumerate() + .filter(|(_, g)| cx.doc.guide_editable(g)) + .filter(|(_, g)| { + if g.vertical { + r.min_x() <= g.pos && g.pos <= r.max_x() && cx.doc.guide_span(g).is_none_or(|(y0, y1)| r.max_y() >= y0 && r.min_y() <= y1) + } else { + r.min_y() <= g.pos && g.pos <= r.max_y() && cx.doc.guide_span(g).is_none_or(|(x0, x1)| r.max_x() >= x0 && r.min_x() <= x1) + } + }) + .map(|(i, _)| i) + .collect() +} + /// The pointer for a guide: it moves sideways if vertical, up and down if horizontal. fn guide_cursor(vertical: bool) -> Cursor { if vertical { Cursor::ResizeH } else { Cursor::ResizeV } diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index e828364e..262bf536 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -459,7 +459,22 @@ impl Tool for SelectionTool { return if toggle { vec![] } else { vec![Action::Exec("select.none".into(), json!({}))] }; } let ids: Vec = marquee(cx.doc, r, cx.isolation, false); - vec![Action::Exec(if toggle { "select.toggle" } else { "select.set" }.into(), json!({ "ids": json_ids(&ids) }))] + let guides = crate::rulerguide::guides_in_rect(cx, r); + let mut out = vec![]; + if toggle { + if !ids.is_empty() { + out.push(Action::Exec("select.toggle".into(), json!({ "ids": json_ids(&ids) }))); + } + if !guides.is_empty() { + out.push(Action::Exec("guide.select".into(), json!({ "indexes": guides, "toggle": true }))); + } + } else { + out.push(Action::Exec("select.set".into(), json!({ "ids": json_ids(&ids) }))); + if !guides.is_empty() { + out.push(Action::Exec("guide.select".into(), json!({ "indexes": guides, "toggle": true }))); + } + } + out } _ => vec![], } @@ -993,6 +1008,58 @@ mod tests { assert!(t.pointer(&cx, &ev(PointerKind::Up, 50.0, 50.0).with_mods(shift)).is_empty()); } + #[test] + fn marquee_selects_ruler_guides() { + let (mut d, id) = doc_with_rect(); + d.guides.push(vectorcraft_doc::Guide::new(false, 80.0)); + let s = Selection::default(); + let p = paint(); + let cx = cx(&d, &s, &p); + let mut t = SelectionTool::default(); + t.pointer(&cx, &ev(PointerKind::Down, 50.0, 50.0)); + t.pointer(&cx, &ev(PointerKind::Drag, 120.0, 120.0)); + let a = t.pointer(&cx, &ev(PointerKind::Up, 120.0, 120.0)); + assert_eq!( + a, + vec![ + Action::Exec("select.set".into(), json!({"ids": [id.0]})), + Action::Exec("guide.select".into(), json!({"indexes": [0], "toggle": true})), + ] + ); + } + + #[test] + fn marquee_selects_only_guide() { + let (mut d, _) = doc_with_rect(); + d.guides.push(vectorcraft_doc::Guide::new(true, 20.0)); + let s = Selection::default(); + let p = paint(); + let cx = cx(&d, &s, &p); + let mut t = SelectionTool::default(); + t.pointer(&cx, &ev(PointerKind::Down, 10.0, 10.0)); + t.pointer(&cx, &ev(PointerKind::Drag, 30.0, 30.0)); + let a = t.pointer(&cx, &ev(PointerKind::Up, 30.0, 30.0)); + assert_eq!( + a, + vec![Action::Exec("select.set".into(), json!({"ids": []})), Action::Exec("guide.select".into(), json!({"indexes": [0], "toggle": true})),] + ); + } + + #[test] + fn marquee_ignores_locked_guides() { + let (mut d, id) = doc_with_rect(); + d.guides.push(vectorcraft_doc::Guide::new(false, 80.0)); + let s = Selection::default(); + let p = paint(); + let mut cx_locked = cx(&d, &s, &p); + cx_locked.guides = false; + let mut t = SelectionTool::default(); + t.pointer(&cx_locked, &ev(PointerKind::Down, 50.0, 50.0)); + t.pointer(&cx_locked, &ev(PointerKind::Drag, 120.0, 120.0)); + let a = t.pointer(&cx_locked, &ev(PointerKind::Up, 120.0, 120.0)); + assert_eq!(a, vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))]); + } + #[test] fn handle_drag_scales() { let (d, id) = doc_with_rect(); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index a992bede..7f9ed3a1 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -100,7 +100,7 @@ tools, never checked side by side) isn't captured row by row. | Tangent and perpendicular snapping for line endpoints (new in 30.0) | missing | | | Snapping panel/popover consolidating the snap options (30.0) | missing | | | Customisable rotation snap angle (#973) | missing | Illustrator: Constrain Angle and construction angles only | -| Ruler guides: drag out, select, move, delete, lock, Make/Release Guides | done | #414, #451 | +| Ruler guides: drag out, select, move, delete, lock, Make/Release Guides | done | #414, #451; marquee selects unlocked ruler guides | | Ruler origin: corner drag with preview, double-click reset to the active artboard, relative X/Y fields, native save/reopen | done | #1212; `document.setRulerOrigin`, `document.resetRulerOrigin`; geometry and command coordinates stay absolute | | Ruler guides as Layers rows: shown, hidden, locked and deleted with their layer; rows dragged to another layer | done | `guide.setLayer`; guides from older files go on a layer | | Objects made into guides: drawn in the guide colour and style of Preferences; Lock Guides locks them too (no select, move or delete); a dragged selection snaps to ruler guides with Smart Guides on | done | #1534 (#1543, #1583) |