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.
This commit is contained in:
dotCooCoo
2026-10-29 13:00:37 +07:00
committed by bflatastic
parent edc2602ae4
commit 38542e02ba
3 changed files with 53 additions and 3 deletions
+23 -2
View File
@@ -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;
+29
View File
@@ -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<NodeId> = 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)
}
+1 -1
View File
@@ -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.