From 6ecea97fdb30511050854ed2761cda91ce028d93 Mon Sep 17 00:00:00 2001 From: nevarem <349680237+nevarem@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:42:12 +0800 Subject: [PATCH] Selection: a selected group's masked art counts only where a click hits A selected group was grabbed by any of its contents under the pointer where an outer mask hides them, including a hidden or locked child and art the group's own clipping mask leaves out: none of these is drawn or hit by a click. A container's art now counts as a click would hit it: its contents as the hit test tries them, through its own mask and without its hidden, locked or template contents. The check for locked objects shares it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv --- crates/doc/src/hit.rs | 13 +++++++++++-- crates/doc/src/hit/tests.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/crates/doc/src/hit.rs b/crates/doc/src/hit.rs index 705842c39..d7c6278b3 100644 --- a/crates/doc/src/hit.rs +++ b/crates/doc/src/hit.rs @@ -136,7 +136,9 @@ pub fn selected_hidden_at(doc: &Document, p: Point, opt: HitOptions, selected: & .rev() .skip(1) .any(|n| n.clips() && n.children().and_then(|c| c.first()).is_some_and(|clip| clip.id != id && !clip_contains(clip, p))); - masked && nodes.last().is_some_and(|n| hit_any(n, p, opt, &symbols).is_some()) + // Its own art is what a click would hit in it: a group's hidden, locked or masked + // contents don't count. + masked && nodes.last().is_some_and(|n| object_hit(n, p, opt, &symbols, true)) }) } @@ -307,7 +309,14 @@ fn locked_children(parent: &Node, p: Point, opt: HitOptions, symbols: &SymbolBox /// Would a click at `p` hit the locked object `n` if it were not locked? fn locked_hit(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> bool { - let inside = || hit_children(n, p, opt, symbols, &|_| false, &mut Vec::new(), false).is_some(); + object_hit(n, p, opt, symbols, false) +} + +/// Would a click at `p` hit the object `n`, wherever it is? A container is hit by its contents as +/// [`hit_test`] tries them (its own clipping mask, and with `editable_only` none of its hidden, +/// locked or template contents). +fn object_hit(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes, editable_only: bool) -> bool { + let inside = || hit_children(n, p, opt, symbols, &|_| false, &mut Vec::new(), editable_only).is_some(); match &n.kind { NodeKind::Group { .. } | NodeKind::Intertwine { .. } | NodeKind::Envelope { editing: true, .. } => inside(), NodeKind::Blend { .. } => inside() || hit_leaf(n, p, opt, symbols).is_some(), diff --git a/crates/doc/src/hit/tests.rs b/crates/doc/src/hit/tests.rs index 641a349e4..895ee62e2 100644 --- a/crates/doc/src/hit/tests.rs +++ b/crates/doc/src/hit/tests.rs @@ -119,6 +119,42 @@ fn a_selected_objects_art_hidden_by_a_mask_is_found_under_the_pointer() { assert_eq!(selected_hidden_at(&d, hidden, opt, &[art_id]), Some(art_id)); } +/// A selected group's own art is what a click would hit in it: not a hidden or locked child, nor +/// art its own clipping mask hides, all under the pointer where an outer mask hides them. +#[test] +fn a_selected_groups_hidden_art_counts_only_where_a_click_would_hit_it() { + // An outer clip group masking to the left half (0..100); the selected group inside it holds + // `child` (100..200 × 0..100, under the pointer at (150, 50)) and `shown` (inside the mask). + let build = |change: &dyn Fn(&mut Node, &mut Node)| { + let mut d = Document::new(300.0, 200.0); + let l = d.layers[0].id; + let outer_mask = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default()); + let mut child = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(100.0, 0.0, 200.0, 100.0)), Appearance::default_art()); + let shown = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 40.0, 40.0)), Appearance::default_art()); + let (gid, cid) = (d.alloc_id(), d.alloc_id()); + let mut group = Node::group(gid, vec![Arc::new(shown)]); + change(&mut group, &mut child); + if let NodeKind::Group { children, .. } = &mut group.kind { + children.push(Arc::new(child)); + } + let mut clip = Node::group(cid, vec![Arc::new(outer_mask), Arc::new(group)]); + clip.set_clips(true); + d.insert(Some(l), 0, clip).unwrap(); + (d, gid) + }; + let (hidden, opt) = (Point::new(150.0, 50.0), HitOptions::default()); + let (d, g) = build(&|_, _| {}); + assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), Some(g)); + let (d, g) = build(&|_, child| child.visible = false); + assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), None, "a hidden child"); + let (d, g) = build(&|_, child| child.locked = true); + assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), None, "a locked child"); + // The selected group is a clip group too, whose own mask (its first child, 10..40) leaves + // `child` out. + let (d, g) = build(&|group, _| group.set_clips(true)); + assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), None, "its own mask"); +} + /// #2345: a marquee touches a clip group only where its art shows. Over the clipped-away /// half alone it selects nothing, and around a rectangle drawn beside the group's mask it /// takes that rectangle alone; reaching into the mask takes the group. The same holds for