From 3eed3be8e69894d0b27bfb8fe779d419bf65f109 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:52:35 +0200 Subject: [PATCH] Stop locked guide objects from blocking marquee selection Fixes #3036 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/hit.rs | 8 +++++++- crates/tools/src/guides.rs | 2 +- crates/tools/src/lib.rs | 1 + crates/tools/src/select.rs | 32 ++++++++++++++++++++++++++++++++ crates/ui-egui/src/canvas.rs | 1 + 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/crates/doc/src/hit.rs b/crates/doc/src/hit.rs index 8e16f1ed0..14d58e98b 100644 --- a/crates/doc/src/hit.rs +++ b/crates/doc/src/hit.rs @@ -62,11 +62,14 @@ pub struct HitOptions { /// (point type's baseline, area type's frame, type on a path's path), not anywhere in its /// bounds. pub type_path_only: bool, + /// Guide objects (paths made into guides) are not hittable: View › Guides › Lock Guides is + /// on or the guides are hidden. + pub skip_guides: bool, } impl Default for HitOptions { fn default() -> Self { - Self { tol: 3.0, outline: false, path_only: false, type_path_only: false, scope: None } + Self { tol: 3.0, outline: false, path_only: false, type_path_only: false, skip_guides: false, scope: None } } } @@ -318,6 +321,9 @@ fn may_hit(parent: &Node, i: usize, c: &Node, p: Point, opt: HitOptions, symbols if !c.visible || editable_only && (c.locked || c.is_template()) { return false; } + if opt.skip_guides && matches!(c.kind, NodeKind::Path { guide: true, .. }) { + return false; + } // (An envelope's content sits where it was, not where the envelope draws it.) edits_contents(c) || c.extras().shaper.is_some() && source_of(c, opt.scope) diff --git a/crates/tools/src/guides.rs b/crates/tools/src/guides.rs index f24ce03e7..235750667 100644 --- a/crates/tools/src/guides.rs +++ b/crates/tools/src/guides.rs @@ -1334,7 +1334,7 @@ pub fn snap_pick(cx: &ToolContext, p: Point) -> (Point, Vec) { } let tol = if cx.smart_guides { cx.snap_tol() } else { cx.tol(cx.snap_tolerance) }; let mut roots = cx.selection.objects.clone(); - roots.extend(hit_test(cx.doc, p, HitOptions { tol, ..cx.hit_options() }).map(|h| h.leaf)); + roots.extend(hit_test(cx.doc, p, HitOptions { tol, skip_guides: false, ..cx.hit_options() }).map(|h| h.leaf)); let near = Rect::new(p.x - tol, p.y - tol, p.x + tol, p.y + tol); Targets::points_near(cx.doc, &roots, near).styled(cx).snap_point(p, tol) } diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index f358d8938..76cd40634 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -403,6 +403,7 @@ impl ToolContext<'_> { path_only: self.path_only, type_path_only: self.type_path_only, scope: self.isolation, + skip_guides: !self.guides, } } } diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 7e969b0e6..5678f00c4 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -1679,6 +1679,38 @@ mod tests { assert_eq!(a, vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))]); } + /// #3036: with Lock Guides on, a guide object neither takes the press nor stops a marquee + /// from starting over it; with guides unlocked, the press still picks it. + #[test] + fn marquee_passes_through_locked_guide_objects() { + let (mut d, id) = doc_with_rect(); + let l = d.layers[0].id; + let g = d.alloc_id(); + let pts = [Point::new(20.0, 20.0), Point::new(400.0, 20.0), Point::new(400.0, 400.0), Point::new(20.0, 400.0)]; + let path = vectorcraft_geom::PathData::single(vectorcraft_geom::SubPath::polyline(&pts, true)); + let mut guide = vectorcraft_doc::Node::path(g, path, vectorcraft_doc::Appearance::default_art()); + let vectorcraft_doc::NodeKind::Path { guide: flag, .. } = &mut guide.kind else { panic!("a path") }; + *flag = true; + d.insert(Some(l), 1, guide).unwrap(); + let s = Selection::default(); + let p = paint(); + let locked = ToolContext { guides: false, ..cx(&d, &s, &p) }; + for (x, y) in [(20.0, 60.0), (50.0, 50.0)] { + let mut t = SelectionTool::default(); + assert!(t.pointer(&locked, &ev(PointerKind::Down, x, y)).is_empty(), "press at ({x}, {y})"); + t.pointer(&locked, &ev(PointerKind::Drag, 250.0, 250.0)); + assert!(matches!(t.state, State::Marquee { .. }), "press at ({x}, {y})"); + // The rectangle is rubber-band selected (the engine leaves locked guides out). + let a = t.pointer(&locked, &ev(PointerKind::Up, 250.0, 250.0)); + let Some(Action::Exec(c, v)) = a.first() else { panic!("press at ({x}, {y}): {a:?}") }; + assert_eq!(c, "select.set", "press at ({x}, {y})"); + assert!(v["ids"].as_array().unwrap().contains(&json!(id.0)), "press at ({x}, {y}): {a:?}"); + } + let unlocked = ToolContext { guides: true, ..cx(&d, &s, &p) }; + let mut t = SelectionTool::default(); + assert_eq!(t.pointer(&unlocked, &ev(PointerKind::Down, 20.0, 60.0)), vec![Action::Exec("select.set".into(), json!({"ids": [g.0]}))]); + } + #[test] fn handle_drag_scales() { let (d, id) = doc_with_rect(); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index faeac9d77..84704ad4d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1881,6 +1881,7 @@ fn hit_with( path_only: prefs.object_selection_by_path_only, type_path_only: prefs.type_selection_by_path_only, scope: app.session.active().and_then(|st| st.isolation), + ..Default::default() }; test(&app.session.active()?.doc, p, opt) }