From 6c781864d12b2aa39eca01cfafeb29609a5b46f0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 28 Oct 2026 10:32:06 -0700 Subject: [PATCH] Shaper: a press checks every selected original shape and keeps objects in front of its group While an opacity mask or a pattern is edited, a press on a selected Shaper original shape now moves it when it is not the first selected object or shares the selection with shapes of another Shaper Group. An object in front of the Shaper Group at the pointer is selected instead. --- crates/tools/src/select.rs | 23 ++++++++++++++++++----- crates/tools/src/select/tests.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 195cfcfbd..906260206 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -465,15 +465,28 @@ pub(crate) fn hold_point(cx: &ToolContext, h: &Hit, p: Point) -> Point { /// While an opacity mask or a pattern is edited, a Shaper double-click selects a Shaper Group's /// original shape without isolating the group's original shapes. A press at `p` on a selected /// original shape holds it for a move: the hit inside its Shaper Group's original shapes, and that -/// container. None otherwise. +/// container. Each selected original shape's Shaper Group is checked, front to back, and an object +/// in front of that group at `p` keeps the press. None otherwise. fn selected_source_hit(cx: &ToolContext, p: Point) -> Option<(Hit, NodeId)> { if cx.doc.mask_edit.is_none() && cx.doc.pattern_edit.is_none() { return None; } - let first = *cx.selection.objects.first()?; - let sources = cx.doc.parent_of(first).filter(|g| cx.doc.node(*g).is_some_and(|n| n.name.as_deref() == Some(vectorcraft_doc::shaper::SOURCES)))?; - let h = hit_test(cx.doc, p, vectorcraft_doc::hit::HitOptions { scope: Some(sources), ..cx.selection_hit_options() })?; - cx.selection.contains(h.top_object(Some(sources))).then_some((h, sources)) + let is_sources = |g: &NodeId| cx.doc.node(*g).is_some_and(|n| n.name.as_deref() == Some(vectorcraft_doc::shaper::SOURCES)); + let mut containers = cx.doc.paint_order(cx.selection.objects.iter().filter_map(|id| cx.doc.parent_of(*id)).filter(is_sources)); + containers.dedup(); + let front = hit_test(cx.doc, p, cx.selection_hit_options()); + containers.into_iter().rev().find_map(|sources| { + let group = cx.doc.parent_of(sources)?; + if let Some(f) = &front + && f.leaf != group + && !f.ancestry.contains(&group) + && cx.doc.paint_order([group, f.leaf]).last() == Some(&f.leaf) + { + return None; + } + let h = hit_test(cx.doc, p, vectorcraft_doc::hit::HitOptions { scope: Some(sources), ..cx.selection_hit_options() })?; + cx.selection.contains(h.top_object(Some(sources))).then_some((h, sources)) + }) } impl SelectionTool { diff --git a/crates/tools/src/select/tests.rs b/crates/tools/src/select/tests.rs index c511f8200..8ebbe9c68 100644 --- a/crates/tools/src/select/tests.rs +++ b/crates/tools/src/select/tests.rs @@ -31,6 +31,35 @@ fn a_press_on_a_selected_shaper_source_moves_it_while_a_mask_is_edited() { assert!(down.iter().any(|a| matches!(a, Action::Exec(c, v) if c == "select.set" && v["ids"] == json!([g.0]))), "{down:?}"); } +/// While an opacity mask is edited, a selected Shaper original shape that is not the first selected +/// object still moves on a press, and an object in front of the Shaper Group keeps the press. +#[test] +fn a_selected_shaper_source_moves_after_other_objects_and_behind_front_art() { + use vectorcraft_doc::{Appearance, Node}; + let (mut d, _, sources, _) = doc_with_shaper_group(); + let rect = d.layers[0].children().unwrap()[0].id; + let layer = d.layers[0].id; + d.mask_edit = Some(vectorcraft_doc::MaskEdit { object: rect, layer }); + let ids: Vec = d.node(sources).unwrap().children().unwrap().iter().map(|n| n.id).collect(); + let (mut s, p) = (Selection::default(), paint()); + s.set([rect, ids[1]]); + let c = ToolContext { isolation: Some(layer), ..cx(&d, &s, &p) }; + let mut t = SelectionTool::default(); + assert_eq!(t.cursor(&c, Point::new(125.0, 350.0), Mods::default()), Cursor::Move); + let down = t.pointer(&c, &ev(PointerKind::Down, 125.0, 350.0)); + assert!(!down.iter().any(|a| matches!(a, Action::Exec(c, _) if c == "select.set")), "{down:?}"); + let drag = t.pointer(&c, &ev(PointerKind::Drag, 145.0, 350.0)); + assert!(drag.iter().any(|a| matches!(a, Action::Preview(c, _) if c == "object.transform")), "{drag:?}"); + let front = d.alloc_id(); + let square = vectorcraft_geom::shapes::rectangle(Rect::new(110.0, 340.0, 140.0, 360.0)); + d.insert(Some(layer), 2, Node::path(front, square, Appearance::default_art())).unwrap(); + s.set([ids[1]]); + let c = ToolContext { isolation: Some(layer), ..cx(&d, &s, &p) }; + assert_ne!(SelectionTool::default().cursor(&c, Point::new(125.0, 350.0), Mods::default()), Cursor::Move); + let down = SelectionTool::default().pointer(&c, &ev(PointerKind::Down, 125.0, 350.0)); + assert!(down.iter().any(|a| matches!(a, Action::Exec(c, v) if c == "select.set" && v["ids"] == json!([front.0]))), "{down:?}"); +} + fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent { PointerEvent::new(kind, x, y) }