mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 19:22:48 +00:00
Merge pull request #6848 from dotCooCoo/test/direct-group-selection
Tests: Direct Selection marquee and hover and Group Selection pass through raster effects
This commit is contained in:
@@ -234,3 +234,160 @@ fn a_marquee_that_touches_only_a_drop_shadow_does_not_select_its_object() {
|
||||
// From above B, over B and the shadow: B only.
|
||||
assert_eq!(press(&mut s, "selection", (105.0, 5.0), (115.0, 30.0), Mods::default()), [b]);
|
||||
}
|
||||
|
||||
/// The selection after a drag with `tool` from `from` to `to`.
|
||||
fn marquee(s: &mut Session, tool: &str, from: (f64, f64), to: (f64, f64)) -> vectorcraft_doc::Selection {
|
||||
press(s, tool, from, to, Mods::default());
|
||||
s.doc().unwrap().selection.clone()
|
||||
}
|
||||
|
||||
/// The point of anchor `(si, ai)` of path `id`.
|
||||
fn anchor_point(s: &Session, id: NodeId, (si, ai): vectorcraft_doc::AnchorRef) -> Point {
|
||||
let path = s.doc().unwrap().doc.node(id).and_then(|n| n.path_data()).unwrap();
|
||||
path.anchors().find(|(s, i, _)| (*s, *i) == (si, ai)).map(|(.., a)| a.p).unwrap()
|
||||
}
|
||||
|
||||
/// The anchors a marquee or a click picked on `id`, as points.
|
||||
fn picked_points(s: &Session, id: NodeId) -> Vec<Point> {
|
||||
let refs: Vec<_> = s.doc().unwrap().selection.partial(id).into_iter().flatten().copied().collect();
|
||||
refs.into_iter().map(|r| anchor_point(s, id, r)).collect()
|
||||
}
|
||||
|
||||
/// The anchors the tool marks as hovered.
|
||||
fn anchor_marks(s: &mut Session) -> Vec<Point> {
|
||||
s.overlays(ViewInfo::default())
|
||||
.into_iter()
|
||||
.filter_map(|o| if let vectorcraft_tools::Overlay::Anchor { p, .. } = o { Some(p) } else { None })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn hover(s: &mut Session, x: f64, y: f64) -> Cursor {
|
||||
let v = ViewInfo::default();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Move, x, y), v).unwrap();
|
||||
s.cursor(Point::new(x, y), Mods::default(), v)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_direct_selection_marquee_leaves_out_a_drop_shadow() {
|
||||
let mut s = session();
|
||||
let (a, b) = shadow_over_b(&mut s);
|
||||
let (a_before, b_before) = (bounds(&s, a), bounds(&s, b));
|
||||
assert_ne!(pixel(&s, 90, 50), WHITE);
|
||||
run(&mut s, "select.none", json!({}));
|
||||
// Over the shadow alone: no object, no anchor, no segment.
|
||||
let sel = marquee(&mut s, "directSelection", (85.0, 40.0), (95.0, 60.0));
|
||||
assert!(sel.objects.is_empty() && sel.anchors.is_empty() && sel.segments.is_empty(), "{sel:?}");
|
||||
// Over the shadow and B's upper left corner: that anchor of B, and nothing of A.
|
||||
let sel = marquee(&mut s, "directSelection", (90.0, 5.0), (105.0, 30.0));
|
||||
assert_eq!(sel.objects, [b]);
|
||||
assert_eq!(picked_points(&s, b), [Point::new(100.0, 20.0)]);
|
||||
// Across B's top edge under the shadow, with neither of its anchors inside: that segment.
|
||||
run(&mut s, "select.none", json!({}));
|
||||
assert_ne!(pixel(&s, 112, 25), BLUE);
|
||||
let sel = marquee(&mut s, "directSelection", (110.0, 5.0), (115.0, 30.0));
|
||||
assert_eq!(sel.objects, [b]);
|
||||
assert!(sel.segments.contains_key(&b) && !sel.segments.contains_key(&a), "{sel:?}");
|
||||
assert_eq!((bounds(&s, a), bounds(&s, b)), (a_before, b_before));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_selection_hover_leaves_out_a_drop_shadow_and_marks_the_anchor_under_it() {
|
||||
let mut s = session();
|
||||
let (_, b) = shadow_over_b(&mut s);
|
||||
// B's upper left corner lies under the shadow.
|
||||
assert_ne!(pixel(&s, 101, 21), BLUE);
|
||||
run(&mut s, "select.none", json!({}));
|
||||
s.select_tool("directSelection", ViewInfo::default()).unwrap();
|
||||
for (x, y) in [(90.0, 50.0), (110.0, 50.0)] {
|
||||
assert_eq!(hover(&mut s, x, y), Cursor::ArrowHollow, "({x}, {y})");
|
||||
assert!(anchor_marks(&mut s).is_empty(), "({x}, {y})");
|
||||
}
|
||||
assert_eq!(hover(&mut s, 101.0, 21.0), Cursor::DirectAnchor);
|
||||
assert_eq!(anchor_marks(&mut s), [Point::new(100.0, 20.0)]);
|
||||
// A press there picks that anchor of B.
|
||||
assert_eq!(press(&mut s, "directSelection", (101.0, 21.0), (101.0, 21.0), Mods::default()), [b]);
|
||||
assert_eq!(picked_points(&s, b), [Point::new(100.0, 20.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_selection_click_on_a_drop_shadow_selects_the_object_under_it() {
|
||||
let mut s = session();
|
||||
let (a, b) = shadow_over_b(&mut s);
|
||||
run(&mut s, "select.none", json!({}));
|
||||
assert_eq!(press(&mut s, "groupSelection", (110.0, 50.0), (110.0, 50.0), Mods::default()), [b]);
|
||||
assert!(press(&mut s, "groupSelection", (90.0, 50.0), (90.0, 50.0), Mods::default()).is_empty());
|
||||
assert_eq!(press(&mut s, "groupSelection", (50.0, 50.0), (50.0, 50.0), Mods::default()), [a]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_selection_clicks_on_a_drop_shadow_climb_the_groups_of_the_object_under_it() {
|
||||
let mut s = session();
|
||||
// B and C in one group, A and D in another above it, and A's shadow over B.
|
||||
let b = rect(&mut s, 100.0, 20.0, 60.0, 60.0, "#0000ff");
|
||||
let c = rect(&mut s, 170.0, 20.0, 20.0, 20.0, "#0000ff");
|
||||
run(&mut s, "select.set", json!({"ids": [b.0, c.0]}));
|
||||
let gb = id_of(&run(&mut s, "object.group", json!({})));
|
||||
let a = rect(&mut s, 20.0, 20.0, 60.0, 60.0, "#ff0000");
|
||||
let d = rect(&mut s, 20.0, 120.0, 20.0, 20.0, "#ff0000");
|
||||
run(&mut s, "select.set", json!({"ids": [a.0, d.0]}));
|
||||
let ga = id_of(&run(&mut s, "object.group", json!({})));
|
||||
shadow(&mut s, a, 40.0);
|
||||
assert_ne!(pixel(&s, 110, 50), BLUE);
|
||||
assert_ne!(pixel(&s, 90, 50), WHITE);
|
||||
run(&mut s, "select.none", json!({}));
|
||||
let click = |s: &mut Session, x, y| {
|
||||
let mut sel = press(s, "groupSelection", (x, y), (x, y), Mods::default());
|
||||
sel.sort_by_key(|n| n.0);
|
||||
sel
|
||||
};
|
||||
assert_eq!(click(&mut s, 110.0, 50.0), [b]);
|
||||
let mut both = vec![b, gb];
|
||||
both.sort_by_key(|n| n.0);
|
||||
assert_eq!(click(&mut s, 110.0, 50.0), both);
|
||||
// With A selected, a click on its shadow over the canvas does not add A's group.
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
assert!(click(&mut s, 90.0, 50.0).is_empty());
|
||||
// On A itself the second click adds it.
|
||||
assert_eq!(click(&mut s, 50.0, 50.0), [a]);
|
||||
let mut both = vec![a, ga];
|
||||
both.sort_by_key(|n| n.0);
|
||||
assert_eq!(click(&mut s, 50.0, 50.0), both);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_selection_marquee_leaves_out_a_drop_shadow() {
|
||||
let mut s = session();
|
||||
let (a, b) = shadow_over_b(&mut s);
|
||||
run(&mut s, "select.none", json!({}));
|
||||
let sel = marquee(&mut s, "groupSelection", (85.0, 40.0), (95.0, 60.0));
|
||||
assert!(sel.objects.is_empty() && sel.anchors.is_empty(), "{sel:?}");
|
||||
// Over the shadow and B's upper left corner: that anchor of B.
|
||||
let sel = marquee(&mut s, "groupSelection", (90.0, 5.0), (105.0, 30.0));
|
||||
assert_eq!(sel.objects, [b]);
|
||||
assert_eq!(picked_points(&s, b), [Point::new(100.0, 20.0)]);
|
||||
// With E, a marquee around B that crosses A's shadow selects B whole, and not A.
|
||||
run(&mut s, "select.none", json!({}));
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("groupSelection", v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 95.0, 10.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 130.0, 50.0), v).unwrap();
|
||||
s.tool_key(vectorcraft_tools::ToolKey::Letter('e'), Mods::default(), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 165.0, 90.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 165.0, 90.0), v).unwrap();
|
||||
let sel = s.doc().unwrap().selection.clone();
|
||||
assert_eq!(sel.objects, [b]);
|
||||
assert!(sel.partial(b).is_none() && sel.partial(a).is_none(), "{sel:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_selection_hover_over_a_drop_shadow_marks_nothing() {
|
||||
let mut s = session();
|
||||
let (_, b) = shadow_over_b(&mut s);
|
||||
run(&mut s, "select.none", json!({}));
|
||||
s.select_tool("groupSelection", ViewInfo::default()).unwrap();
|
||||
for (x, y) in [(90.0, 50.0), (110.0, 50.0), (101.0, 21.0)] {
|
||||
assert_eq!(hover(&mut s, x, y), Cursor::GroupSelection, "({x}, {y})");
|
||||
assert!(anchor_marks(&mut s).is_empty(), "({x}, {y})");
|
||||
}
|
||||
assert_eq!(press(&mut s, "groupSelection", (110.0, 50.0), (110.0, 50.0), Mods::default()), [b]);
|
||||
}
|
||||
|
||||
@@ -600,7 +600,7 @@ since 2026-10-01.
|
||||
- **Copy and Cut of anchors picked with a lent Direct Selection (#6123):** with the Pen or the Curvature tool, Cmd/Ctrl held lends Direct Selection for the gesture; the anchors and segments it picks are what Edit › Copy and Cut take once the key is let go, as with Direct Selection chosen, while the last anchor the Pen leaves selected as it draws still makes Copy take the whole path (#2122). A Cmd gesture that leaves the selection as it was (a Shift marquee over nothing) picks nothing. Regression test: `tests::draw2::pen_cmd_selected_anchors_are_what_copy_and_cut_take`.
|
||||
- **Cut of picked anchors (#6361):** Edit › Cut removes direct-selected anchors from their path and from a compound path's member, with the segments attached to them, as Clear does. Picked segments still lose only those segments. When a path also has a picked segment, Cut removes and copies only its picked segments and ignores its picked anchors, on every contour, as Clear does; an anchor left without a surviving segment is removed with them. Regression tests: `cut_*_anchors_*` and `cut_of_an_anchor_with_a_segment_on_another_contour_leaves_the_source_as_clear_does` in `crates/engine/tests/it/copy_direct_selection.rs`.
|
||||
- **Escape in Selection tool drags:** Escape during a Selection tool move or Alt-drag copy, a bounding-box scale or rotation, a marquee, or a corner widget, pie widget or bracket drag cancels it: the art returns to its state at the press, no copy is left, no undo step is added, the release does nothing, and the tool stays chosen. This is VectorCraft's own behavior. Regression tests: `escape_cancels_*` in `select/tests.rs` and `pie.rs`, and `escape_cancels_a_selection_tool_move_copy_scale_or_rotation` in `tests/selectescape.rs`.
|
||||
- **Clicks pass through raster effects:** clicks with the Selection and Direct Selection tools, and the Selection tool's pointer and marquee, leave out the area that a drop shadow, a glow, a blur or another raster effect paints beyond an object. A click on a shadow selects the object under it, or nothing over an empty canvas, and a press on the shadow of a selected object does not move it ([the help page](https://helpx.adobe.com/illustrator/using/selecting-objects.html)). No setting changes this. This covers paths, groups, type, symbol instances and images. Regression tests: `crates/engine/src/tests/select_effects.rs`.
|
||||
- **Selection passes through raster effects:** clicks with the Selection, Direct Selection and Group Selection tools (Group Selection's repeated click included), the Selection tool's pointer and marquee, Direct Selection's marquee, pointer and anchor highlight, and Group Selection's marquee (E for enclosed objects included) leave out the area that a drop shadow, a glow, a blur or another raster effect paints beyond an object. A click on a shadow selects the object under it, or nothing over an empty canvas, a repeated Group Selection click there adds the groups of the object under it, and a press on the shadow of a selected object does not move it ([the help page](https://helpx.adobe.com/illustrator/using/selecting-objects.html)). Over a shadow, Direct Selection highlights only the anchors of the object under it, and Group Selection keeps its own pointer and highlights no anchor, as it does everywhere (VectorCraft's own rule). No setting changes this. This covers paths, groups, type, symbol instances and images. Regression tests: `crates/engine/src/tests/select_effects.rs`.
|
||||
- **Global Edit: Shift-click leaves a match out:** while Global Edit is on, a Shift-click with the Selection tool on a highlighted match, or on an object inside a matched group, leaves that match out (`select.globalEditExclude`): it is no longer highlighted and later edits leave it as it is; the object the edit started from stays, and Global Edit ends when fewer than two matches are left ([Update assets quickly with Global Edit](https://helpx.adobe.com/ph_fil/illustrator/how-to/global-editing-sensei.html)). Regression tests: `a_shift_click_*`, `an_object_left_out_*`, `a_group_left_out_*` and `leaving_out_all_but_one_*` in `crates/engine/src/tests/globaledit.rs`.
|
||||
- **Global Edit Options in the Control bar:** while Select › Start Global Edit can run, the Control bar shows a Global Edit Options dropdown that opens the same options as the Properties panel's (Match: Appearance, Size; On Artboards; Include Objects On Canvas; Result(s)); both set the same options. The dropdown is off while Global Edit is on; it sits after Recolor Artwork, ahead of the X/Y/W/H fields ([Edit similar objects globally](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)). Regression test: `the_control_bar_shows_the_global_edit_options` in `crates/ui-egui/src/chrome/tests.rs`.
|
||||
- **Global Edit's enabled check on large paths:** the check that decides whether Select › Start Global Edit can run on the selected path, compound path or symbol instance (which the Control bar makes every frame) measures the box of its anchors and handles instead of building and normalizing a copy of every anchor, with the same answer. On a 200,000-anchor path the check takes 0.7 ms instead of 2.2 to 3.0 ms. Regression tests: `support_*` in `crates/engine/src/cmd/globaledit.rs`.
|
||||
|
||||
Reference in New Issue
Block a user