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) }