diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index 35c71926..f6ed7124 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -730,7 +730,8 @@ 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 art = Action::Exec("select.anchorsMany".into(), json!({"items": items, "mode": if toggle { "toggle" } else { "set" }})); + std::iter::once(art).chain(crate::rulerguide::marquee_select(cx, r)).collect() } _ => vec![], } @@ -1100,6 +1101,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..1683ac51 100644 --- a/crates/tools/src/rulerguide.rs +++ b/crates/tools/src/rulerguide.rs @@ -2,14 +2,15 @@ //! made where it is released over the canvas: with the Artboard tool, an artboard guide of the //! active artboard. For the selection tools ([`GuideEdit`]), a press on a guide selects it (Shift //! adds it to the selected guides or, released without a drag, takes it out); dragging moves the -//! selected guides (Alt copies them). A dragged guide snaps ([`GuideSnap`]): with Shift to the +//! selected guides (Alt copies them), and a marquee selects the guides it crosses along with the +//! art ([`marquee_select`]). A dragged guide snaps ([`GuideSnap`]): with Shift to the //! ruler's ticks, else to whole pixels, the grid, with Smart Guides the art's and the artboards' //! edges, centres and anchors, or with Snap to Point the anchors. Dropped off the canvas onto its //! ruler, a moved guide is deleted. Hidden or locked guides (View › Guides), and guides on a hidden //! 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 +33,35 @@ pub fn guide_at(cx: &ToolContext, p: Point) -> Option { .map(|(i, _)| i) } +/// What a selection tool's marquee `r` does to the ruler guides, after its action selecting the +/// art: the guides it crosses join the selection (with a Shift marquee, the selected ones leave +/// it, as with a Shift-click on a guide). +pub fn marquee_select(cx: &ToolContext, r: Rect) -> Option { + let guides = guides_in_rect(cx, r); + (!guides.is_empty()).then(|| Action::Exec("guide.select".into(), json!({ "indexes": guides, "toggle": true }))) +} + +/// The editable ruler guides intersecting marquee rectangle `r`. +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..92826893 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -459,7 +459,8 @@ 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 art = Action::Exec(if toggle { "select.toggle" } else { "select.set" }.into(), json!({ "ids": json_ids(&ids) })); + std::iter::once(art).chain(crate::rulerguide::marquee_select(cx, r)).collect() } _ => vec![], } @@ -993,6 +994,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) |