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.
This commit is contained in:
dotCooCoo
2026-10-29 13:00:37 +07:00
committed by bflatastic
parent 38542e02ba
commit 6c781864d1
2 changed files with 47 additions and 5 deletions
+18 -5
View File
@@ -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 {
+29
View File
@@ -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<NodeId> = 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)
}