From edc2602ae4cd837d39944800022343bafc517b5e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 28 Oct 2026 07:53:56 -0700 Subject: [PATCH 1/3] Shaper: Shaper Group edits keep the opacity mask and pattern editing layers isolated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While an opacity mask or a pattern was edited, a Shaper scribble and Object › Shaper › Release and Expand ended isolation, and a Shaper double-click on a Shaper Group isolated the group's original shapes. Each replaced the editing layer's isolation while the document stayed in mask or pattern editing. A click on the Shaper Group's mode widget did the same through object.isolate. A scribble, Release and Expand now keep the editing layer isolated in these modes, and end isolation as before outside them. A double-click with Shaper selects the original shape under the pointer without isolating the group's original shapes, and the mode widget does not show while a mask or a pattern is edited. Docs: docs/shaper.md, docs/target-app-parity.md. --- crates/engine/src/cmd/shaper.rs | 22 ++++++++++++---- crates/engine/src/tests/shaper.rs | 44 +++++++++++++++++++++++++++++++ crates/tools/src/shaperwidget.rs | 25 +++++++++++++++--- docs/shaper.md | 2 ++ docs/target-app-parity.md | 1 + 5 files changed, 86 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/cmd/shaper.rs b/crates/engine/src/cmd/shaper.rs index 0ba4ebc0d..8ba8a0c9f 100644 --- a/crates/engine/src/cmd/shaper.rs +++ b/crates/engine/src/cmd/shaper.rs @@ -31,7 +31,7 @@ pub fn specs() -> Vec { "Select Shaper Face", [], None, - "{point: [x,y], source?: bool} click selects a group, then a face; source enters construction isolation and selects an original; while a group's originals are isolated, it selects one of that group's originals → {id}", + "{point: [x,y], source?: bool} click selects a group, then a face; source enters construction isolation and selects an original (while an opacity mask or a pattern is edited, it selects the original and the editing layer stays isolated); while a group's originals are isolated, it selects one of that group's originals → {id}", has_doc, select ), @@ -678,7 +678,7 @@ fn scribble(s: &mut Session, p: &Value) -> Result { sel.set([id]); Ok(id) })?; - s.doc_mut()?.isolation = None; + end_isolation(s)?; Ok(json!({"id": id.0, "regions": touched.len(), "strokes": strokes})) } @@ -730,13 +730,25 @@ fn select(s: &mut Session, p: &Value) -> Result { } else { group }; - let isolation = source.then(|| source_group(node).map(|n| n.id)).flatten(); + // While an opacity mask or a pattern is edited, the source pick leaves its editing layer isolated. + let isolation = source.then(|| source_group(node).map(|n| n.id)).flatten().filter(|_| editing_layer(&st.doc).is_none()); s.select(|_, sel| sel.set([target]))?; if let Some(id) = isolation { s.doc_mut()?.isolation = Some(id); } Ok(json!({"id": target.0})) } +/// The layer an opacity mask or a pattern is edited in, which stays isolated while that mode is on. +fn editing_layer(d: &Document) -> Option { + d.mask_edit.map(|m| m.layer).or_else(|| d.pattern_edit.as_ref().map(|e| e.layer)) +} +/// Leave isolation mode, Construction mode included. While an opacity mask or a pattern is edited, +/// its editing layer stays isolated. +fn end_isolation(s: &mut Session) -> Result<()> { + let st = s.doc_mut()?; + st.isolation = editing_layer(&st.doc); + Ok(()) +} fn target(s: &Session, p: &Value) -> Result { let id = id_param(p, "id") .or_else(|| s.active().and_then(|d| d.selection.objects.first().copied())) @@ -757,7 +769,7 @@ fn release(s: &mut Session, p: &Value) -> Result { sel.set(ids.iter().copied()); Ok(ids) })?; - s.doc_mut()?.isolation = None; + end_isolation(s)?; Ok(json!({"ids": ids.iter().map(|i| i.0).collect::>()})) } fn expand(s: &mut Session, p: &Value) -> Result { @@ -774,6 +786,6 @@ fn expand(s: &mut Session, p: &Value) -> Result { sel.set([id]); Ok(()) })?; - s.doc_mut()?.isolation = None; + end_isolation(s)?; Ok(json!({"id": id.0})) } diff --git a/crates/engine/src/tests/shaper.rs b/crates/engine/src/tests/shaper.rs index c0b4ad888..fbd20ff4a 100644 --- a/crates/engine/src/tests/shaper.rs +++ b/crates/engine/src/tests/shaper.rs @@ -356,6 +356,50 @@ fn shaper_isolate_refuses_a_locked_or_hidden_source_group() { } } +/// While an opacity mask or a pattern is edited, its editing layer stays isolated: a Shaper source +/// pick selects the original shape without isolating it, and a scribble, Release and Expand keep the +/// layer isolated. Leaving the mode then ends isolation. +#[test] +fn shaper_keeps_the_mask_and_pattern_editing_layers_isolated() { + for mode in ["mask", "pattern"] { + let mut s = session(); + let object = rectangle(&mut s, 300.0, "#000000"); + let layer = if mode == "mask" { + let m = rectangle(&mut s, 300.0, "#ffffff"); + s.execute("select.set", &json!({"ids": [object.0, m.0]})).unwrap(); + s.execute("transparency.makeOpacityMask", &json!({})).unwrap(); + s.execute("transparency.editOpacityMask", &json!({})).unwrap()["layer"].as_u64().map(NodeId) + } else { + s.execute("object.pattern.make", &json!({"name": "P"})).unwrap(); + s.doc().unwrap().doc.pattern_edit.as_ref().map(|e| e.layer) + }; + let isolation = |s: &Session| s.doc().unwrap().isolation; + assert!(layer.is_some() && isolation(&s) == layer, "{mode}"); + let a = rectangle(&mut s, 0.0, "#ff0000"); + let b = rectangle(&mut s, 50.0, "#0000ff"); + let make = |s: &mut Session| { + let pts = [[25, 20], [75, 35], [30, 50], [125, 65]]; + NodeId(s.execute("shaper.scribble", &json!({"points": pts, "ids": [a.0, b.0]})).unwrap()["id"].as_u64().unwrap()) + }; + let g = make(&mut s); + assert_eq!(s.doc().unwrap().doc.parent_of(g), layer, "{mode}"); + assert_eq!(isolation(&s), layer, "{mode}: scribble"); + let picked = s.execute("shaper.select", &json!({"point": [25, 50], "source": true})).unwrap(); + assert_eq!(picked["id"], json!(a.0), "{mode}"); + assert_eq!(s.doc().unwrap().selection.objects, vec![a], "{mode}"); + assert_eq!(isolation(&s), layer, "{mode}: source pick"); + s.execute("shaper.release", &json!({"id": g.0})).unwrap(); + assert_eq!(isolation(&s), layer, "{mode}: release"); + let g = make(&mut s); + s.execute("shaper.expand", &json!({"id": g.0})).unwrap(); + assert_eq!(isolation(&s), layer, "{mode}: expand"); + let leave = if mode == "mask" { "transparency.stopEditingOpacityMask" } else { "object.pattern.done" }; + s.execute(leave, &json!({})).unwrap(); + let st = s.doc().unwrap(); + assert!(st.doc.mask_edit.is_none() && st.doc.pattern_edit.is_none() && st.isolation.is_none(), "{mode}"); + } +} + #[test] fn shaper_lines_keep_free_angles_with_or_without_shift() { for shift in [false, true] { diff --git a/crates/tools/src/shaperwidget.rs b/crates/tools/src/shaperwidget.rs index 0eb1bcb19..f44a622a1 100644 --- a/crates/tools/src/shaperwidget.rs +++ b/crates/tools/src/shaperwidget.rs @@ -1,6 +1,7 @@ //! The Shaper Group's mode widget: a small arrow beside the right side of the bounds of a Shaper //! Group's source shapes. It shows while the group or one of its faces is selected (Face Selection -//! mode) and while the group's source shapes are isolated (Construction mode). A click on it +//! mode) and while the group's source shapes are isolated (Construction mode), but not while an +//! opacity mask or a pattern is edited. A click on it //! switches between the two modes: it isolates the source shapes (`object.isolate`), or it leaves //! isolation with the group selected (`object.exitIsolation`). The Shaper and Selection tools hit //! it and the canvas draws it from the same geometry. @@ -33,9 +34,10 @@ impl ShaperWidget { /// The widget at `zoom` (screen px per document point), beside the bounds of the group's source /// shapes in both modes. Construction mode: `isolation` is a Shaper Group's source shapes. Face /// Selection mode: the selection is one Shaper Group, or one face or stroke of it. It returns - /// None otherwise, and when the group, or a layer or group that holds it, is hidden or locked. + /// None otherwise, when the group, or a layer or group that holds it, is hidden or locked, and + /// while an opacity mask or a pattern is edited. pub fn of(doc: &Document, selection: &Selection, isolation: Option, zoom: f64) -> Option { - if !zoom.is_finite() || zoom <= 0.0 { + if !zoom.is_finite() || zoom <= 0.0 || doc.mask_edit.is_some() || doc.pattern_edit.is_some() { return None; } let shaper = |id: NodeId| doc.node(id).is_some_and(|n| n.extras().shaper.is_some()); @@ -153,4 +155,21 @@ mod tests { assert!(widget(&d, &[g]).is_none()); assert!(widget(&d, &[face]).is_none()); } + + #[test] + fn no_widget_shows_while_an_opacity_mask_or_a_pattern_is_edited() { + let (mut d, g, sources, _) = doc_with_shaper_group(); + let rect = d.layers[0].children().unwrap()[0].id; + let layer = d.layers[0].id; + let mut s = Selection::default(); + s.add(g); + assert!(ShaperWidget::of(&d, &s, None, 1.0).is_some()); + d.mask_edit = Some(vectorcraft_doc::MaskEdit { object: rect, layer }); + assert!(ShaperWidget::of(&d, &s, Some(layer), 1.0).is_none(), "mask editing"); + assert!(ShaperWidget::of(&d, &Selection::default(), Some(sources), 1.0).is_none(), "mask editing, sources isolated"); + d.mask_edit = None; + d.pattern_edit = Some(vectorcraft_doc::pattern::PatternEdit { pattern: "P".into(), layer, original: None }); + assert!(ShaperWidget::of(&d, &s, Some(layer), 1.0).is_none(), "pattern editing"); + assert!(ShaperWidget::of(&d, &Selection::default(), Some(sources), 1.0).is_none(), "pattern editing, sources isolated"); + } } diff --git a/docs/shaper.md b/docs/shaper.md index 5a232bc4b..86379c822 100644 --- a/docs/shaper.md +++ b/docs/shaper.md @@ -17,6 +17,8 @@ Smart Guides and Snap to Point do not snap to the original shapes. The reference Object › Shaper › Release restores the original paths. Expand keeps the visible result as ordinary paths. Each scribble and source edit records one undo step. Sources in a composition must share a parent layer/group; Shaper limits a composition to 64 source paths and 4,096 segments. +While an opacity mask or a pattern is edited, its editing layer stays isolated and the arrow widget does not show. A double-click with Shaper on a Shaper Group in the mask or pattern art selects the original shape under the pointer without isolating the group's original shapes, and a scribble, Release and Expand leave the editing layer isolated. + Finer gesture heuristics remain to be implemented. Behavior reference: [public Shaper documentation](https://helpx.adobe.com/illustrator/using/building-new-shapes-using-shape.html). Rotation steps and independent fill/stroke cases follow the contributor’s supplied requirements. No reference images or product assets are included. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index eec34b44a..31d8df317 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -631,6 +631,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The Lasso picks anchor points only on art a click can pick, and the Add Anchor Point, Delete Anchor Point, Anchor Point and Scissors tools pick anchor points and segments only on the selected paths and on art a click can pick: not on a Shaper Group's original shapes outside Construction mode, and in isolation mode only inside the isolated group or object. Smart Guides and Snap to Point do not snap to a Shaper Group's original shapes, and the reference point of the transform tools snaps to one only when it is selected, or under the pointer in Construction mode. A marquee selects a clip group, or an object on a layer with a clipping mask, only when it reaches the area inside the clipping path, and it measures the art only within that path's bounds. Within the bounds of a mask that is not a rectangle, it can still select art that the mask hides. Direct Selection's marquee still selects the hidden anchor points inside it (#2345). A marquee selects a Shaper Group only when it reaches the art the group shows, not the original shapes kept under it, and Direct Selection's clicks and marquee do not select those shapes' anchor points. In Construction mode a marquee selects the original shapes, and Direct Selection selects their anchor points. + While an opacity mask or a pattern is edited, a Shaper double-click on a Shaper Group in the mask or pattern art selects the original shape under the pointer without entering Construction mode, the Shaper Group's mode widget does not show, and a Shaper scribble, Release and Expand keep the editing layer isolated. A marquee selects an Intertwine group only when it reaches one of the group's members, as for a plain group, and it selects a repeat only when it reaches one of the copies the repeat draws. Direct and Group Selection's marquees select the anchor points of an Intertwine group's members, not the group, and Group Selection's E marquee selects the members inside it. A key object needs another selected object outside it. A Shift-click or any other change that leaves one object selected clears the key, and `select.key` sets none on one object or on a group selected with only its own members. With one object selected, the Align panel, the Control bar and the Properties panel align it to the artboard whatever Align To is set to. With two or more objects selected and no key object, Align to Key Object makes the topmost of them the key, whatever order they were selected in, and the Align panel, the Control bar and the Properties panel align to it. Align To keeps its Selection or Artboard choice and shows it again once the key is cleared. From 38542e02ba7224be775a33375d661880125ffa37 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 28 Oct 2026 09:40:26 -0700 Subject: [PATCH 2/3] Shaper: the Selection tool moves a selected original shape while a mask or pattern is edited While an opacity mask or a pattern was edited, a Shaper double-click selected a Shaper Group's original shape without isolating the group's original shapes. A Selection tool press on that shape then selected and moved the whole Shaper Group over the art the group shows, and started a marquee over a part of the shape the group does not show. A press on the selected original shape now holds it for a move, and the pointer shows the move cursor over it. --- crates/tools/src/select.rs | 25 +++++++++++++++++++++++-- crates/tools/src/select/tests.rs | 29 +++++++++++++++++++++++++++++ docs/shaper.md | 2 +- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index c957066c7..195cfcfbd 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -462,6 +462,20 @@ pub(crate) fn hold_point(cx: &ToolContext, h: &Hit, p: Point) -> Point { cx.doc.node(h.leaf).and_then(|n| n.path_data()).and_then(|pd| hold_on_path(pd, p, reach)).unwrap_or(p) } +/// 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. +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)) +} + impl SelectionTool { fn drag_threshold(cx: &ToolContext) -> f64 { cx.tol(3.0) @@ -675,9 +689,13 @@ impl Tool for SelectionTool { self.state = State::Moving { start: p, press: p, hold: p, began: false, deselect: None, key: None }; return vec![Action::Exec("select.set".into(), json!({ "ids": [behind.0] }))]; } - match hit_test(cx.doc, p, cx.selection_hit_options()) { + let (hit, scope) = match selected_source_hit(cx, p) { + Some((h, sources)) => (Some(h), Some(sources)), + None => (hit_test(cx.doc, p, cx.selection_hit_options()), cx.isolation), + }; + match hit { Some(h) => { - let top = h.top_object(cx.isolation); + let top = h.top_object(scope); let mut out = vec![]; let (mut deselect, mut key) = (None, None); if m.shift { @@ -896,6 +914,9 @@ impl Tool for SelectionTool { if cx.snap_to_point && matches!(Self::hover_point(cx, p), Some((_, "anchor"))) { return Cursor::ArrowHollow; } + if selected_source_hit(cx, p).is_some() { + return Cursor::Move; + } if let Some(h) = hit_test(cx.doc, p, cx.selection_hit_options()) { if cx.selection.contains(h.top_object(cx.isolation)) || m.alt { return Cursor::Move; diff --git a/crates/tools/src/select/tests.rs b/crates/tools/src/select/tests.rs index 0819d56ad..c511f8200 100644 --- a/crates/tools/src/select/tests.rs +++ b/crates/tools/src/select/tests.rs @@ -2,6 +2,35 @@ use super::*; use crate::testutil::*; use vectorcraft_doc::Selection; +/// While an opacity mask is edited, a press on a selected Shaper original shape moves it, over the +/// art the group shows and over a part of the shape the group does not show. A press on an original +/// shape that is not selected selects the Shaper Group or starts a marquee, as before. +#[test] +fn a_press_on_a_selected_shaper_source_moves_it_while_a_mask_is_edited() { + let (mut d, g, 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([ids[1]]); + let c = ToolContext { isolation: Some(layer), ..cx(&d, &s, &p) }; + for x in [75.0, 125.0] { + let mut t = SelectionTool::default(); + assert_eq!(t.cursor(&c, Point::new(x, 350.0), Mods::default()), Cursor::Move, "at {x}"); + let down = t.pointer(&c, &ev(PointerKind::Down, x, 350.0)); + assert!(!down.iter().any(|a| matches!(a, Action::Exec(c, _) if c == "select.set")), "at {x}: {down:?}"); + let drag = t.pointer(&c, &ev(PointerKind::Drag, x + 20.0, 350.0)); + assert!(drag.iter().any(|a| matches!(a, Action::Preview(c, _) if c == "object.transform")), "at {x}: {drag:?}"); + } + s.set([ids[0]]); + let c = ToolContext { isolation: Some(layer), ..cx(&d, &s, &p) }; + let down = SelectionTool::default().pointer(&c, &ev(PointerKind::Down, 125.0, 350.0)); + assert!(down.is_empty(), "{down:?}"); + let down = SelectionTool::default().pointer(&c, &ev(PointerKind::Down, 75.0, 350.0)); + assert!(down.iter().any(|a| matches!(a, Action::Exec(c, v) if c == "select.set" && v["ids"] == json!([g.0]))), "{down:?}"); +} + fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent { PointerEvent::new(kind, x, y) } diff --git a/docs/shaper.md b/docs/shaper.md index 86379c822..b6d0fbc84 100644 --- a/docs/shaper.md +++ b/docs/shaper.md @@ -17,7 +17,7 @@ Smart Guides and Snap to Point do not snap to the original shapes. The reference Object › Shaper › Release restores the original paths. Expand keeps the visible result as ordinary paths. Each scribble and source edit records one undo step. Sources in a composition must share a parent layer/group; Shaper limits a composition to 64 source paths and 4,096 segments. -While an opacity mask or a pattern is edited, its editing layer stays isolated and the arrow widget does not show. A double-click with Shaper on a Shaper Group in the mask or pattern art selects the original shape under the pointer without isolating the group's original shapes, and a scribble, Release and Expand leave the editing layer isolated. +While an opacity mask or a pattern is edited, its editing layer stays isolated and the arrow widget does not show. A double-click with Shaper on a Shaper Group in the mask or pattern art selects the original shape under the pointer without isolating the group's original shapes, the Selection tool moves that selected shape, and a scribble, Release and Expand leave the editing layer isolated. Finer gesture heuristics remain to be implemented. From 6c781864d12b2aa39eca01cfafeb29609a5b46f0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 28 Oct 2026 10:32:06 -0700 Subject: [PATCH 3/3] 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) }