Selection: a selected object's masked art counts only where a click reaches it

The selected object itself was checked for being hidden, locked or a
template, but not as a click checks it: a selected guide path was
grabbed where a mask hid it while guides were locked or hidden, though
a click on it there hits nothing.

The selected object now passes the same test a click applies to it
before its art is tried: guides while guides are locked or hidden, and
a Shaper group's hidden part, don't count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv
This commit is contained in:
nevarem
2026-10-29 18:46:47 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 6ecea97fdb
commit f793547a7f
2 changed files with 28 additions and 5 deletions
+9 -5
View File
@@ -115,8 +115,8 @@ pub fn hit_test_visible(doc: &Document, p: Point, opt: HitOptions) -> Option<Hit
/// click there hits nothing ([`hit_test`]), so the selection tools grab the selection by that
/// art instead of starting a marquee: an image in a mask, picked in the Layers panel, can be
/// dragged by its hidden part, whose frame shows while it is selected. An object that a click
/// can't hit anyway doesn't count: hidden, locked, a template, outside the isolated container
/// (`opt.scope`) or on a locked artboard.
/// can't hit anyway doesn't count: hidden, locked, a template, a guide while guides are locked
/// or hidden, outside the isolated container (`opt.scope`) or on a locked artboard.
pub fn selected_hidden_at(doc: &Document, p: Point, opt: HitOptions, selected: &[NodeId]) -> Option<NodeId> {
let symbols = doc.symbol_boxes();
let locks = doc.locked_artboards();
@@ -136,9 +136,13 @@ 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)));
// 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))
// A click may reach it there ([`may_hit`]: not a guide while guides are locked or
// hidden, nor a Shaper group's hidden part), and its own art is what a click would hit
// in it: a group's hidden, locked or masked contents don't count.
let [.., parent, n] = nodes.as_slice() else { return false };
let reachable =
parent.children().and_then(|c| c.iter().position(|c| c.id == id)).is_some_and(|i| may_hit(parent, i, n, p, opt, &symbols, true));
masked && reachable && object_hit(n, p, opt, &symbols, true)
})
}
+19
View File
@@ -155,6 +155,25 @@ fn a_selected_groups_hidden_art_counts_only_where_a_click_would_hit_it() {
assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), None, "its own mask");
}
/// A selected guide path where a mask hides it counts only while guides can be clicked: not
/// while they are locked or hidden (`skip_guides`), as a click on it then hits nothing.
#[test]
fn a_selected_guide_hidden_by_a_mask_counts_only_while_guides_can_be_clicked() {
let mut d = Document::new(300.0, 200.0);
let l = d.layers[0].id;
let (mask, mut art) = half_clipped(&mut d);
if let NodeKind::Path { guide, .. } = &mut art.kind {
*guide = true;
}
let (g, guide_id) = (d.alloc_id(), art.id);
let mut clip = Node::group(g, vec![Arc::new(mask), Arc::new(art)]);
clip.set_clips(true);
d.insert(Some(l), 0, clip).unwrap();
let (hidden, opt) = (Point::new(150.0, 50.0), HitOptions::default());
assert_eq!(selected_hidden_at(&d, hidden, opt, &[guide_id]), Some(guide_id));
assert_eq!(selected_hidden_at(&d, hidden, HitOptions { skip_guides: true, ..opt }, &[guide_id]), None);
}
/// #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