mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
Merge pull request #4552 from nevarem/fix/drag-masked-selection
Selection: a press where a mask hides a selected object's art grabs it
This commit is contained in:
+68
-1
@@ -110,6 +110,66 @@ pub fn hit_test_visible(doc: &Document, p: Point, opt: HitOptions) -> Option<Hit
|
||||
hit_test_skipping(doc, p, opt, &|_| false, false)
|
||||
}
|
||||
|
||||
/// The object of `selected` (topmost first) whose own art lies under `p` where a clipping mask
|
||||
/// hides it: a clip group (or a layer with a clipping mask) holding it doesn't clip to `p`. A
|
||||
/// click there hits nothing ([`hit_test`]), so the selection tools grab the selection by that
|
||||
/// art instead of starting a marquee: an image in a mask, picked in the Layers panel, can be
|
||||
/// dragged by its hidden part, whose frame shows while it is selected. An object that a click
|
||||
/// can't hit anyway doesn't count: hidden, locked, a template, a guide while guides are locked
|
||||
/// or hidden, in a Shaper group's part a click can't reach, outside the isolated container
|
||||
/// (`opt.scope`) or on a locked artboard. On a layer whose Preview is off it counts by its
|
||||
/// outline, as a click there hits it.
|
||||
pub fn selected_hidden_at(doc: &Document, p: Point, opt: HitOptions, selected: &[NodeId]) -> Option<NodeId> {
|
||||
let symbols = doc.symbol_boxes();
|
||||
let locks = doc.locked_artboards();
|
||||
selected.iter().rev().copied().find(|&id| {
|
||||
let Some(chain) = doc.ancestry(id) else { return false };
|
||||
let nodes: Vec<&Node> = chain.iter().filter_map(|a| doc.node(*a)).collect();
|
||||
if nodes.len() != chain.len()
|
||||
|| nodes.iter().any(|n| !n.visible || n.locked || n.is_template())
|
||||
|| opt.scope.is_some_and(|s| !chain.contains(&s))
|
||||
|| locks.holds_id(id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// How a click reaches it, if one does.
|
||||
let Some(opt) = path_options(&nodes, &chain, opt) else { return false };
|
||||
// Hidden by a mask: one of the containers around it clips, and not to `p`.
|
||||
let masked = nodes
|
||||
.iter()
|
||||
.rev()
|
||||
.skip(1)
|
||||
.any(|n| n.clips() && n.children().and_then(|c| c.first()).is_some_and(|clip| clip.id != id && !clip_contains(clip, p)));
|
||||
// A click may reach it there ([`may_hit`]: not a guide while guides are locked or
|
||||
// hidden, nor a Shaper group's hidden part), and its own art is what a click would hit
|
||||
// in it: a group's hidden, locked or masked contents don't count.
|
||||
let [.., parent, n] = nodes.as_slice() else { return false };
|
||||
let reachable =
|
||||
parent.children().and_then(|c| c.iter().position(|c| c.id == id)).is_some_and(|i| may_hit(parent, i, n, p, opt, &symbols, true));
|
||||
masked && reachable && object_hit(n, p, opt, &symbols, true)
|
||||
})
|
||||
}
|
||||
|
||||
/// The options a click goes down `nodes` (an object's ancestry, `chain`, the layer first) with, as
|
||||
/// [`hit_test`] goes: from the isolated container (`opt.scope`), else from the layer, in outline
|
||||
/// below a layer whose Preview is off. `None` when a Shaper group's part it can't reach lies on
|
||||
/// the way ([`shaper_hides`]).
|
||||
fn path_options(nodes: &[&Node], chain: &[NodeId], opt: HitOptions) -> Option<HitOptions> {
|
||||
let scope_at = opt.scope.and_then(|s| chain.iter().position(|a| *a == s));
|
||||
let child_index = |parent: &Node, child: &Node| parent.children().and_then(|c| c.iter().position(|c| c.id == child.id));
|
||||
let hidden = nodes.windows(2).skip(scope_at.unwrap_or(0)).any(|w| match w {
|
||||
[parent, child] => child_index(parent, child).is_some_and(|i| shaper_hides(parent, i, opt.scope)),
|
||||
_ => false,
|
||||
});
|
||||
if hidden {
|
||||
return None;
|
||||
}
|
||||
// The isolated container's own Preview doesn't count: a click starts inside it.
|
||||
let below = scope_at.map_or(0, |i| i + 1);
|
||||
let outline = nodes.iter().skip(below).any(|n| matches!(n.kind, NodeKind::Layer { preview: false, .. }));
|
||||
Some(if outline { HitOptions { outline: true, ..opt } } else { opt })
|
||||
}
|
||||
|
||||
/// The objects under `p` that [`Hit::top_object`] picks in `scope` (isolation mode), topmost
|
||||
/// first: what clicks there select, one below the other (Cmd/Ctrl-click selects behind).
|
||||
pub fn objects_at(doc: &Document, p: Point, opt: HitOptions, scope: Option<NodeId>) -> Vec<NodeId> {
|
||||
@@ -277,7 +337,14 @@ fn locked_children(parent: &Node, p: Point, opt: HitOptions, symbols: &SymbolBox
|
||||
|
||||
/// Would a click at `p` hit the locked object `n` if it were not locked?
|
||||
fn locked_hit(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> bool {
|
||||
let inside = || hit_children(n, p, opt, symbols, &|_| false, &mut Vec::new(), false).is_some();
|
||||
object_hit(n, p, opt, symbols, false)
|
||||
}
|
||||
|
||||
/// Would a click at `p` hit the object `n`, wherever it is? A container is hit by its contents as
|
||||
/// [`hit_test`] tries them (its own clipping mask, and with `editable_only` none of its hidden,
|
||||
/// locked or template contents).
|
||||
fn object_hit(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes, editable_only: bool) -> bool {
|
||||
let inside = || hit_children(n, p, opt, symbols, &|_| false, &mut Vec::new(), editable_only).is_some();
|
||||
match &n.kind {
|
||||
NodeKind::Group { .. } | NodeKind::Intertwine { .. } | NodeKind::Envelope { editing: true, .. } => inside(),
|
||||
NodeKind::Blend { .. } => inside() || hit_leaf(n, p, opt, symbols).is_some(),
|
||||
|
||||
@@ -77,6 +77,148 @@ fn half_clipped(d: &mut Document) -> (Node, Node) {
|
||||
(mask, art)
|
||||
}
|
||||
|
||||
/// The art a mask hides counts only for a selected object: there a click hits nothing, and the
|
||||
/// selection tools grab the selection by that art. Not where the art shows (the click hits it),
|
||||
/// beyond the art, for the clip group selected as a whole, nor for art a click can't hit (locked,
|
||||
/// hidden, outside the isolated container). A layer's clipping mask hides art as a clip group's
|
||||
/// does.
|
||||
#[test]
|
||||
fn a_selected_objects_art_hidden_by_a_mask_is_found_under_the_pointer() {
|
||||
let mut d = Document::new(300.0, 200.0);
|
||||
let l = d.layers[0].id;
|
||||
let (mask, art) = half_clipped(&mut d);
|
||||
let (g, art_id) = (d.alloc_id(), art.id);
|
||||
let mut clip = Node::group(g, vec![Arc::new(mask), Arc::new(art)]);
|
||||
clip.set_clips(true);
|
||||
d.insert(Some(l), 0, clip).unwrap();
|
||||
let (hidden, shown, beyond) = (Point::new(150.0, 50.0), Point::new(50.0, 50.0), Point::new(250.0, 50.0));
|
||||
let opt = HitOptions::default();
|
||||
assert!(hit_test(&d, hidden, opt).is_none());
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[art_id]), Some(art_id));
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[]), None);
|
||||
assert_eq!(selected_hidden_at(&d, shown, opt, &[art_id]), None);
|
||||
assert_eq!(selected_hidden_at(&d, beyond, opt, &[art_id]), None);
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), None);
|
||||
assert_eq!(selected_hidden_at(&d, hidden, HitOptions { scope: Some(g), ..opt }, &[art_id]), Some(art_id));
|
||||
assert_eq!(selected_hidden_at(&d, hidden, HitOptions { scope: Some(art_id), ..opt }, &[art_id]), Some(art_id));
|
||||
let other = d.add_layer(Some("Other"));
|
||||
assert_eq!(selected_hidden_at(&d, hidden, HitOptions { scope: Some(other), ..opt }, &[art_id]), None);
|
||||
for change in [|n: &mut Node| n.locked = true, |n: &mut Node| n.visible = false] {
|
||||
let mut d = d.clone();
|
||||
change(d.node_mut(art_id).unwrap());
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[art_id]), None);
|
||||
}
|
||||
// A layer's clipping mask.
|
||||
let mut d = Document::new(300.0, 200.0);
|
||||
let l = d.layers[0].id;
|
||||
let (mask, art) = half_clipped(&mut d);
|
||||
let art_id = art.id;
|
||||
d.insert(Some(l), 0, mask).unwrap();
|
||||
d.insert(Some(l), 1, art).unwrap();
|
||||
d.node_mut(l).unwrap().set_clips(true);
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[art_id]), Some(art_id));
|
||||
}
|
||||
|
||||
/// A selected group's own art is what a click would hit in it: not a hidden or locked child, nor
|
||||
/// art its own clipping mask hides, all under the pointer where an outer mask hides them.
|
||||
#[test]
|
||||
fn a_selected_groups_hidden_art_counts_only_where_a_click_would_hit_it() {
|
||||
// An outer clip group masking to the left half (0..100); the selected group inside it holds
|
||||
// `child` (100..200 × 0..100, under the pointer at (150, 50)) and `shown` (inside the mask).
|
||||
let build = |change: &dyn Fn(&mut Node, &mut Node)| {
|
||||
let mut d = Document::new(300.0, 200.0);
|
||||
let l = d.layers[0].id;
|
||||
let outer_mask = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default());
|
||||
let mut child = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(100.0, 0.0, 200.0, 100.0)), Appearance::default_art());
|
||||
let shown = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 40.0, 40.0)), Appearance::default_art());
|
||||
let (gid, cid) = (d.alloc_id(), d.alloc_id());
|
||||
let mut group = Node::group(gid, vec![Arc::new(shown)]);
|
||||
change(&mut group, &mut child);
|
||||
if let NodeKind::Group { children, .. } = &mut group.kind {
|
||||
children.push(Arc::new(child));
|
||||
}
|
||||
let mut clip = Node::group(cid, vec![Arc::new(outer_mask), Arc::new(group)]);
|
||||
clip.set_clips(true);
|
||||
d.insert(Some(l), 0, clip).unwrap();
|
||||
(d, gid)
|
||||
};
|
||||
let (hidden, opt) = (Point::new(150.0, 50.0), HitOptions::default());
|
||||
let (d, g) = build(&|_, _| {});
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), Some(g));
|
||||
let (d, g) = build(&|_, child| child.visible = false);
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), None, "a hidden child");
|
||||
let (d, g) = build(&|_, child| child.locked = true);
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), None, "a locked child");
|
||||
// The selected group is a clip group too, whose own mask (its first child, 10..40) leaves
|
||||
// `child` out.
|
||||
let (d, g) = build(&|group, _| group.set_clips(true));
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[g]), None, "its own mask");
|
||||
}
|
||||
|
||||
/// A selected guide path where a mask hides it counts only while guides can be clicked: not
|
||||
/// while they are locked or hidden (`skip_guides`), as a click on it then hits nothing.
|
||||
#[test]
|
||||
fn a_selected_guide_hidden_by_a_mask_counts_only_while_guides_can_be_clicked() {
|
||||
let mut d = Document::new(300.0, 200.0);
|
||||
let l = d.layers[0].id;
|
||||
let (mask, mut art) = half_clipped(&mut d);
|
||||
if let NodeKind::Path { guide, .. } = &mut art.kind {
|
||||
*guide = true;
|
||||
}
|
||||
let (g, guide_id) = (d.alloc_id(), art.id);
|
||||
let mut clip = Node::group(g, vec![Arc::new(mask), Arc::new(art)]);
|
||||
clip.set_clips(true);
|
||||
d.insert(Some(l), 0, clip).unwrap();
|
||||
let (hidden, opt) = (Point::new(150.0, 50.0), HitOptions::default());
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[guide_id]), Some(guide_id));
|
||||
assert_eq!(selected_hidden_at(&d, hidden, HitOptions { skip_guides: true, ..opt }, &[guide_id]), None);
|
||||
}
|
||||
|
||||
/// A selected path in a Shaper group's original shapes, where an outer mask hides it, counts only
|
||||
/// while those shapes can be clicked (Construction mode, with them isolated), however deep it
|
||||
/// lies in them.
|
||||
#[test]
|
||||
fn a_selected_path_in_a_shaper_groups_hidden_shapes_counts_only_in_construction_mode() {
|
||||
let mut d = Document::new(300.0, 200.0);
|
||||
let l = d.layers[0].id;
|
||||
// The outer clip group masks to 0..100; the path, 100..200, lies in the Shaper group's
|
||||
// original shapes (its first child), under (150, 50).
|
||||
let mask = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default());
|
||||
let path = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(100.0, 0.0, 200.0, 100.0)), Appearance::default_art());
|
||||
let path_id = path.id;
|
||||
let mut sources = Node::group(d.alloc_id(), vec![Arc::new(path)]);
|
||||
sources.name = Some(crate::shaper::SOURCES.into());
|
||||
let sources_id = sources.id;
|
||||
let face = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 40.0, 40.0)), Appearance::default_art());
|
||||
let mut shaper = Node::group(d.alloc_id(), vec![Arc::new(sources), Arc::new(face)]);
|
||||
shaper.extras_mut().shaper = Some(Box::default());
|
||||
let mut clip = Node::group(d.alloc_id(), vec![Arc::new(mask), Arc::new(shaper)]);
|
||||
clip.set_clips(true);
|
||||
d.insert(Some(l), 0, clip).unwrap();
|
||||
let (hidden, opt) = (Point::new(150.0, 50.0), HitOptions::default());
|
||||
assert_eq!(selected_hidden_at(&d, hidden, opt, &[path_id]), None);
|
||||
assert_eq!(selected_hidden_at(&d, hidden, HitOptions { scope: Some(sources_id), ..opt }, &[path_id]), Some(path_id));
|
||||
}
|
||||
|
||||
/// On a layer whose Preview is off, a click hits art in outline: a selected object's hidden art
|
||||
/// counts on its outline, not inside its fill.
|
||||
#[test]
|
||||
fn a_selected_objects_hidden_art_on_a_preview_off_layer_counts_on_its_outline() {
|
||||
let mut d = Document::new(300.0, 200.0);
|
||||
let l = d.layers[0].id;
|
||||
let (mask, art) = half_clipped(&mut d);
|
||||
let (g, art_id) = (d.alloc_id(), art.id);
|
||||
let mut clip = Node::group(g, vec![Arc::new(mask), Arc::new(art)]);
|
||||
clip.set_clips(true);
|
||||
d.insert(Some(l), 0, clip).unwrap();
|
||||
if let NodeKind::Layer { preview, .. } = &mut d.node_mut(l).unwrap().kind {
|
||||
*preview = false;
|
||||
}
|
||||
let opt = HitOptions::default();
|
||||
assert_eq!(selected_hidden_at(&d, Point::new(150.0, 50.0), opt, &[art_id]), None, "inside the fill");
|
||||
assert_eq!(selected_hidden_at(&d, Point::new(200.0, 50.0), opt, &[art_id]), Some(art_id), "on the outline");
|
||||
}
|
||||
|
||||
/// #2345: a marquee touches a clip group only where its art shows. Over the clipped-away
|
||||
/// half alone it selects nothing, and around a rectangle drawn beside the group's mask it
|
||||
/// takes that rectangle alone; reaching into the mask takes the group. The same holds for
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::hit::{HitKind, compound_part_at, hit_test};
|
||||
use vectorcraft_doc::hit::{HitKind, compound_part_at, hit_test, selected_hidden_at};
|
||||
use vectorcraft_doc::{AnchorRef, Node, NodeId, NodeKind, Selection};
|
||||
use vectorcraft_geom::{Anchor, PathData, Point, Rect, Vec2};
|
||||
|
||||
@@ -563,6 +563,9 @@ impl Tool for DirectSelectionTool {
|
||||
if let Some(h) = hit_test(cx.doc, p, cx.selection_hit_options()) {
|
||||
return self.press_object(cx, ev, p, tol, h);
|
||||
}
|
||||
if let Some(out) = self.press_hidden(cx, ev, p) {
|
||||
return out;
|
||||
}
|
||||
self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false };
|
||||
vec![]
|
||||
}
|
||||
@@ -851,12 +854,26 @@ impl DirectSelectionTool {
|
||||
mod tests;
|
||||
|
||||
impl DirectSelectionTool {
|
||||
/// A press where a clipping mask hides the art of a selected object (picked in the Layers
|
||||
/// panel, say) grabs the selection by it, as a press on its visible part does; `None`
|
||||
/// elsewhere, and with Shift (a marquee then adds to the selection).
|
||||
fn press_hidden(&mut self, cx: &ToolContext, ev: &PointerEvent, p: Point) -> Option<Vec<Action>> {
|
||||
if ev.mods.shift {
|
||||
return None;
|
||||
}
|
||||
selected_hidden_at(cx.doc, p, cx.selection_hit_options(), &cx.selection.objects)?;
|
||||
self.state = State::MoveObject { start: p, grab: None, hold: p, began: false, deselect: None };
|
||||
Some(vec![])
|
||||
}
|
||||
fn press_group(&mut self, cx: &ToolContext, ev: &PointerEvent, p: Point, tol: f64) -> Vec<Action> {
|
||||
if let Some(out) = self.guide.press(cx, ev) {
|
||||
self.state = State::Idle;
|
||||
return out;
|
||||
}
|
||||
let Some(mut h) = hit_test(cx.doc, p, cx.selection_hit_options()) else {
|
||||
if let Some(out) = self.press_hidden(cx, ev, p) {
|
||||
return out;
|
||||
}
|
||||
self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false };
|
||||
return vec![];
|
||||
};
|
||||
|
||||
@@ -2,6 +2,40 @@ use super::*;
|
||||
use crate::testutil::*;
|
||||
use vectorcraft_doc::Selection;
|
||||
|
||||
/// Direct Selection and Group Selection grab a selected clipped object (picked in the Layers
|
||||
/// panel) where its mask hides it, as on its visible part: a drag moves it. With nothing of it
|
||||
/// selected there, a press starts a marquee.
|
||||
#[test]
|
||||
fn a_press_where_a_mask_hides_a_selected_object_drags_it() {
|
||||
let (d, _, _, art) = doc_with_masked_square();
|
||||
let p = paint();
|
||||
let mut picked = Selection::default();
|
||||
picked.add(art);
|
||||
let none = Selection::default();
|
||||
// (370, 450): on the art, outside the mask (400..500).
|
||||
for group in [false, true] {
|
||||
let c = cx(&d, &picked, &p);
|
||||
let mut t = DirectSelectionTool::new(group);
|
||||
let down = t.pointer(&c, &PointerEvent::new(PointerKind::Down, 370.0, 450.0));
|
||||
assert!(!down.iter().any(|a| matches!(a, Action::Exec(c, _) if c.starts_with("select."))), "{down:?} (group: {group})");
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 390.0, 470.0));
|
||||
assert!(a.iter().any(|a| matches!(a, Action::Begin(n) if n == "Move")), "{a:?} (group: {group})");
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 390.0, 470.0));
|
||||
// With Shift, a marquee.
|
||||
let shift = Mods { shift: true, ..Mods::default() };
|
||||
let mut t = DirectSelectionTool::new(group);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 370.0, 450.0).with_mods(shift));
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 390.0, 470.0).with_mods(shift));
|
||||
assert!(!a.iter().any(|a| matches!(a, Action::Begin(_))), "{a:?} (group: {group})");
|
||||
assert!(t.overlays(&c).iter().any(|o| matches!(o, Overlay::Marquee(_))), "a marquee (group: {group})");
|
||||
let c = cx(&d, &none, &p);
|
||||
let mut t = DirectSelectionTool::new(group);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 370.0, 450.0));
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 390.0, 470.0));
|
||||
assert!(!a.iter().any(|a| matches!(a, Action::Begin(_))), "{a:?} (group: {group})");
|
||||
}
|
||||
}
|
||||
|
||||
/// #414: a ruler guide is picked over the art, but an anchor on it is picked first.
|
||||
#[test]
|
||||
fn direct_and_group_selection_pick_ruler_guides() {
|
||||
|
||||
@@ -37,7 +37,7 @@ use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::hit::{Hit, HitKind, hit_test, marquee, objects_at};
|
||||
use vectorcraft_doc::hit::{Hit, HitKind, hit_test, marquee, objects_at, selected_hidden_at};
|
||||
use vectorcraft_doc::{NodeId, OrientedBox};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
|
||||
|
||||
@@ -740,7 +740,7 @@ impl Tool for SelectionTool {
|
||||
out
|
||||
}
|
||||
None => {
|
||||
self.state = State::Marquee { start: p, cur: p, toggle: m.shift, enclosed: false };
|
||||
self.press_nothing(cx, p, grab, m);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
@@ -1191,6 +1191,17 @@ impl SelectionTool {
|
||||
}
|
||||
|
||||
impl SelectionTool {
|
||||
/// A press at `p` that hits no object starts a marquee, except where a clipping mask hides
|
||||
/// the art of a selected object (picked in the Layers panel, say): there, without Shift, it
|
||||
/// grabs the selection by that art (at `grab`), as a press on its visible part does.
|
||||
fn press_nothing(&mut self, cx: &ToolContext, p: Point, grab: Point, m: Mods) {
|
||||
self.state = if !m.shift && selected_hidden_at(cx.doc, p, cx.selection_hit_options(), &cx.selection.objects).is_some() {
|
||||
State::Moving { start: grab, press: p, hold: grab, began: false, deselect: None, key: None }
|
||||
} else {
|
||||
State::Marquee { start: p, cur: p, toggle: m.shift, enclosed: false }
|
||||
};
|
||||
}
|
||||
|
||||
fn drag_moving(&mut self, cx: &ToolContext, p: Point, m: Mods, (start, press, hold, began): (Point, Point, Point, bool)) -> Vec<Action> {
|
||||
let mut out = vec![];
|
||||
if !began {
|
||||
|
||||
@@ -79,6 +79,53 @@ fn a_press_on_a_masks_edge_selects_its_clip_group() {
|
||||
assert!(!a.iter().any(|a| matches!(a, Action::Exec(c, _) if c == "select.set")), "{a:?}");
|
||||
}
|
||||
|
||||
/// The art of a selected clipped object (picked in the Layers panel), pressed where its mask
|
||||
/// hides it, is grabbed: a drag moves the selection (the art, not its clip group) and a click
|
||||
/// keeps it selected. With nothing of it selected, or with Shift, a press there starts a marquee.
|
||||
#[test]
|
||||
fn a_press_where_a_mask_hides_a_selected_object_drags_it() {
|
||||
let (d, _, _, art) = doc_with_masked_square();
|
||||
let p = paint();
|
||||
let mut picked = Selection::default();
|
||||
picked.add(art);
|
||||
let shift = |kind, x, y| {
|
||||
let mut e = ev(kind, x, y);
|
||||
e.mods.shift = true;
|
||||
e
|
||||
};
|
||||
let changes_selection = |a: &[Action]| a.iter().any(|a| matches!(a, Action::Exec(c, _) if c.starts_with("select.")));
|
||||
// (370, 450): on the art, outside the mask (400..500).
|
||||
let mut t = SelectionTool::default();
|
||||
let c = cx(&d, &picked, &p);
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Down, 370.0, 450.0)).is_empty());
|
||||
let a = t.pointer(&c, &ev(PointerKind::Drag, 390.0, 470.0));
|
||||
assert!(a.iter().any(|a| matches!(a, Action::Begin(n) if n == "Move")), "{a:?}");
|
||||
assert!(a.iter().any(|a| matches!(a, Action::Preview(c, _) if c == "object.transform")), "{a:?}");
|
||||
assert_eq!(t.pointer(&c, &ev(PointerKind::Up, 390.0, 470.0)), vec![Action::Commit]);
|
||||
// A click there keeps the selection.
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&c, &ev(PointerKind::Down, 370.0, 450.0));
|
||||
assert!(!changes_selection(&t.pointer(&c, &ev(PointerKind::Up, 370.0, 450.0))));
|
||||
// Nothing selected: a marquee, and a click deselects.
|
||||
let none = Selection::default();
|
||||
let c = cx(&d, &none, &p);
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&c, &ev(PointerKind::Down, 370.0, 450.0));
|
||||
assert!(!t.pointer(&c, &ev(PointerKind::Drag, 390.0, 470.0)).iter().any(|a| matches!(a, Action::Begin(_))));
|
||||
// Shift: a marquee that adds to the selection.
|
||||
let c = cx(&d, &picked, &p);
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&c, &shift(PointerKind::Down, 370.0, 450.0));
|
||||
assert!(!t.pointer(&c, &shift(PointerKind::Drag, 390.0, 470.0)).iter().any(|a| matches!(a, Action::Begin(_))));
|
||||
// An object a click hits there wins: a press on a square over that point selects it.
|
||||
let mut d = d;
|
||||
let (front, l) = (d.alloc_id(), d.layers[0].id);
|
||||
let square = vectorcraft_geom::shapes::rectangle(Rect::new(350.0, 430.0, 390.0, 470.0));
|
||||
d.insert(Some(l), 2, vectorcraft_doc::Node::path(front, square, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let a = SelectionTool::default().pointer(&cx(&d, &picked, &p), &ev(PointerKind::Down, 370.0, 450.0));
|
||||
assert!(a.iter().any(|a| matches!(a, Action::Exec(c, v) if c == "select.set" && v["ids"] == json!([front.0]))), "{a:?}");
|
||||
}
|
||||
|
||||
/// #955: grabbed by an anchor and dropped near another object's anchor, the anchor lands
|
||||
/// exactly on it with Smart Guides, even where lining the bounds up would leave it beside it.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user