mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
Selection: a selected object's masked art is reached the way a click goes
The check of a selected object's masked art looked at its own place only. A click goes down from the layer (or the isolated container) and never reaches a Shaper group's original shapes outside Construction mode, however deep the object lies in them; and below a layer whose Preview is off it hits art in outline. A press inside such a path's fill, or on a path in a Shaper group's hidden shapes, started a move. The way down to the selected object is now checked as a click goes: a Shaper group's part a click can't reach on it rules the object out, and a Preview-off layer on it (below the isolated container) makes the object count by its outline. Tests also cover Shift with Direct and Group Selection (a marquee) and an object a click hits over the masked art (it is selected). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
f793547a7f
commit
87fc61df79
+25
-1
@@ -116,7 +116,9 @@ pub fn hit_test_visible(doc: &Document, p: Point, opt: HitOptions) -> Option<Hit
|
||||
/// 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, a guide while guides are locked
|
||||
/// or hidden, outside the isolated container (`opt.scope`) or on a locked artboard.
|
||||
/// or hidden, in a Shaper group's part a click can't reach, outside the isolated container
|
||||
/// (`opt.scope`) or on a locked artboard. On a layer whose Preview is off it counts by its
|
||||
/// outline, as a click there hits it.
|
||||
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();
|
||||
@@ -130,6 +132,8 @@ pub fn selected_hidden_at(doc: &Document, p: Point, opt: HitOptions, selected: &
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// How a click reaches it, if one does.
|
||||
let Some(opt) = path_options(&nodes, &chain, opt) else { return false };
|
||||
// Hidden by a mask: one of the containers around it clips, and not to `p`.
|
||||
let masked = nodes
|
||||
.iter()
|
||||
@@ -146,6 +150,26 @@ pub fn selected_hidden_at(doc: &Document, p: Point, opt: HitOptions, selected: &
|
||||
})
|
||||
}
|
||||
|
||||
/// The options a click goes down `nodes` (an object's ancestry, `chain`, the layer first) with, as
|
||||
/// [`hit_test`] goes: from the isolated container (`opt.scope`), else from the layer, in outline
|
||||
/// below a layer whose Preview is off. `None` when a Shaper group's part it can't reach lies on
|
||||
/// the way ([`shaper_hides`]).
|
||||
fn path_options(nodes: &[&Node], chain: &[NodeId], opt: HitOptions) -> Option<HitOptions> {
|
||||
let scope_at = opt.scope.and_then(|s| chain.iter().position(|a| *a == s));
|
||||
let child_index = |parent: &Node, child: &Node| parent.children().and_then(|c| c.iter().position(|c| c.id == child.id));
|
||||
let hidden = nodes.windows(2).skip(scope_at.unwrap_or(0)).any(|w| match w {
|
||||
[parent, child] => child_index(parent, child).is_some_and(|i| shaper_hides(parent, i, opt.scope)),
|
||||
_ => false,
|
||||
});
|
||||
if hidden {
|
||||
return None;
|
||||
}
|
||||
// The isolated container's own Preview doesn't count: a click starts inside it.
|
||||
let below = scope_at.map_or(0, |i| i + 1);
|
||||
let outline = nodes.iter().skip(below).any(|n| matches!(n.kind, NodeKind::Layer { preview: false, .. }));
|
||||
Some(if outline { HitOptions { outline: true, ..opt } } else { opt })
|
||||
}
|
||||
|
||||
/// The objects under `p` that [`Hit::top_object`] picks in `scope` (isolation mode), topmost
|
||||
/// first: what clicks there select, one below the other (Cmd/Ctrl-click selects behind).
|
||||
pub fn objects_at(doc: &Document, p: Point, opt: HitOptions, scope: Option<NodeId>) -> Vec<NodeId> {
|
||||
|
||||
@@ -174,6 +174,51 @@ fn a_selected_guide_hidden_by_a_mask_counts_only_while_guides_can_be_clicked() {
|
||||
assert_eq!(selected_hidden_at(&d, hidden, HitOptions { skip_guides: true, ..opt }, &[guide_id]), None);
|
||||
}
|
||||
|
||||
/// A selected path in a Shaper group's original shapes, where an outer mask hides it, counts only
|
||||
/// while those shapes can be clicked (Construction mode, with them isolated), however deep it
|
||||
/// lies in them.
|
||||
#[test]
|
||||
fn a_selected_path_in_a_shaper_groups_hidden_shapes_counts_only_in_construction_mode() {
|
||||
let mut d = Document::new(300.0, 200.0);
|
||||
let l = d.layers[0].id;
|
||||
// The outer clip group masks to 0..100; the path, 100..200, lies in the Shaper group's
|
||||
// original shapes (its first child), under (150, 50).
|
||||
let mask = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default());
|
||||
let path = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(100.0, 0.0, 200.0, 100.0)), Appearance::default_art());
|
||||
let path_id = path.id;
|
||||
let mut sources = Node::group(d.alloc_id(), vec![Arc::new(path)]);
|
||||
sources.name = Some(crate::shaper::SOURCES.into());
|
||||
let sources_id = sources.id;
|
||||
let face = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 40.0, 40.0)), Appearance::default_art());
|
||||
let mut shaper = Node::group(d.alloc_id(), vec![Arc::new(sources), Arc::new(face)]);
|
||||
shaper.extras_mut().shaper = Some(Box::default());
|
||||
let mut clip = Node::group(d.alloc_id(), vec![Arc::new(mask), Arc::new(shaper)]);
|
||||
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, &[path_id]), None);
|
||||
assert_eq!(selected_hidden_at(&d, hidden, HitOptions { scope: Some(sources_id), ..opt }, &[path_id]), Some(path_id));
|
||||
}
|
||||
|
||||
/// On a layer whose Preview is off, a click hits art in outline: a selected object's hidden art
|
||||
/// counts on its outline, not inside its fill.
|
||||
#[test]
|
||||
fn a_selected_objects_hidden_art_on_a_preview_off_layer_counts_on_its_outline() {
|
||||
let mut d = Document::new(300.0, 200.0);
|
||||
let l = d.layers[0].id;
|
||||
let (mask, art) = half_clipped(&mut d);
|
||||
let (g, art_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();
|
||||
if let NodeKind::Layer { preview, .. } = &mut d.node_mut(l).unwrap().kind {
|
||||
*preview = false;
|
||||
}
|
||||
let opt = HitOptions::default();
|
||||
assert_eq!(selected_hidden_at(&d, Point::new(150.0, 50.0), opt, &[art_id]), None, "inside the fill");
|
||||
assert_eq!(selected_hidden_at(&d, Point::new(200.0, 50.0), opt, &[art_id]), Some(art_id), "on the outline");
|
||||
}
|
||||
|
||||
/// #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
|
||||
|
||||
@@ -21,6 +21,13 @@ fn a_press_where_a_mask_hides_a_selected_object_drags_it() {
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 390.0, 470.0));
|
||||
assert!(a.iter().any(|a| matches!(a, Action::Begin(n) if n == "Move")), "{a:?} (group: {group})");
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 390.0, 470.0));
|
||||
// With Shift, a marquee.
|
||||
let shift = Mods { shift: true, ..Mods::default() };
|
||||
let mut t = DirectSelectionTool::new(group);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 370.0, 450.0).with_mods(shift));
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 390.0, 470.0).with_mods(shift));
|
||||
assert!(!a.iter().any(|a| matches!(a, Action::Begin(_))), "{a:?} (group: {group})");
|
||||
assert!(t.overlays(&c).iter().any(|o| matches!(o, Overlay::Marquee(_))), "a marquee (group: {group})");
|
||||
let c = cx(&d, &none, &p);
|
||||
let mut t = DirectSelectionTool::new(group);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 370.0, 450.0));
|
||||
|
||||
@@ -117,6 +117,13 @@ fn a_press_where_a_mask_hides_a_selected_object_drags_it() {
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&c, &shift(PointerKind::Down, 370.0, 450.0));
|
||||
assert!(!t.pointer(&c, &shift(PointerKind::Drag, 390.0, 470.0)).iter().any(|a| matches!(a, Action::Begin(_))));
|
||||
// An object a click hits there wins: a press on a square over that point selects it.
|
||||
let mut d = d;
|
||||
let (front, l) = (d.alloc_id(), d.layers[0].id);
|
||||
let square = vectorcraft_geom::shapes::rectangle(Rect::new(350.0, 430.0, 390.0, 470.0));
|
||||
d.insert(Some(l), 2, vectorcraft_doc::Node::path(front, square, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let a = SelectionTool::default().pointer(&cx(&d, &picked, &p), &ev(PointerKind::Down, 370.0, 450.0));
|
||||
assert!(a.iter().any(|a| matches!(a, Action::Exec(c, v) if c == "select.set" && v["ids"] == json!([front.0]))), "{a:?}");
|
||||
}
|
||||
|
||||
/// #955: grabbed by an anchor and dropped near another object's anchor, the anchor lands
|
||||
|
||||
Reference in New Issue
Block a user