Files
vectorcraft/crates/tools/src/direct/tests.rs
T

1265 lines
68 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
use crate::testutil::*;
use vectorcraft_doc::Selection;
/// #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() {
let (mut d, id) = doc_with_rect();
d.guides.push(vectorcraft_doc::Guide::new(true, 100.0));
let (s, p) = (Selection::default(), paint());
let c = cx(&d, &s, &p);
let down = |t: &mut DirectSelectionTool, y: f64| t.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.0, y));
let pick = vec![Action::Exec("guide.select".into(), json!({"indexes": [0]}))];
for group in [false, true] {
let mut t = DirectSelectionTool::new(group);
assert_eq!(down(&mut t, 150.0), pick, "on the rect's edge (group: {group})");
assert!(t.busy());
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 100.0, 150.0));
assert_eq!(t.cursor(&c, Point::new(101.0, 300.0), Mods::default()), Cursor::ResizeH);
}
let mut t = DirectSelectionTool::new(false);
assert_eq!(down(&mut t, 100.0), vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "set"}))]);
}
/// A Cmd/Ctrl+Shift double-click with Direct Selection or Group Selection releases the ruler
/// guide or the guide path under the pointer; a plain double-click on a ruler guide, or one while
/// guides are hidden or locked, releases nothing.
#[test]
fn a_cmd_shift_double_click_releases_the_guide_under_the_pointer() {
let (mut d, id) = doc_with_rect();
d.guides.push(vectorcraft_doc::Guide::new(false, 300.0));
let Some(NodeKind::Path { guide, .. }) = d.node_mut(id).map(|n| &mut n.kind) else { panic!("the rectangle is a path") };
*guide = true;
let (s, p) = (Selection::default(), paint());
let both = Mods { cmd: true, shift: true, ..Mods::default() };
let index = vec![Action::Exec("view.guides.release".into(), json!({"index": 0}))];
let ids = vec![Action::Exec("view.guides.release".into(), json!({"ids": [id.0]}))];
let locked = ToolContext { guides: false, ..cx(&d, &s, &p) };
for group in [false, true] {
let dbl = |c: &ToolContext, x: f64, y: f64, m: Mods| {
DirectSelectionTool::new(group).pointer(c, &PointerEvent::new(PointerKind::DoubleClick, x, y).with_mods(m))
};
let c = cx(&d, &s, &p);
assert_eq!(dbl(&c, 350.0, 301.0, both), index, "on the ruler guide (group: {group})");
assert_eq!(dbl(&c, 200.0, 150.0, both), ids, "on the guide path (group: {group})");
assert!(dbl(&c, 350.0, 301.0, Mods::default()).is_empty(), "a plain double-click (group: {group})");
assert!(dbl(&c, 350.0, 301.0, Mods { cmd: true, ..Mods::default() }).is_empty(), "Cmd/Ctrl alone (group: {group})");
assert!(dbl(&locked, 350.0, 301.0, both).is_empty(), "guides locked (group: {group})");
assert!(dbl(&locked, 200.0, 150.0, both).is_empty(), "guides locked (group: {group})");
}
}
/// An anchor is picked from 2 px past its drawn square even where its segment is nearer, and
/// further with a larger anchor Size or Tolerance (#593); away from it the segment is.
#[test]
fn anchors_are_picked_before_the_segments_through_them() {
let (d, id) = doc_with_rect();
let (s, p) = (Selection::default(), paint());
let corner = vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "set"}))];
let press = |cx: &ToolContext, x: f64, y: f64| DirectSelectionTool::new(false).pointer(cx, &PointerEvent::new(PointerKind::Down, x, y));
let c = cx(&d, &s, &p);
// 4.3 px from the corner, 0.5 px from the top edge.
assert_eq!(press(&c, 104.2, 100.5), corner);
assert_ne!(press(&c, 110.0, 100.5), corner, "the segment, away from the anchor");
assert_ne!(press(&c, 105.0, 100.5), corner, "past the default reach (4.5 px)");
let large = ToolContext { anchor_size: 7, ..cx(&d, &s, &p) };
assert_eq!(press(&large, 106.0, 100.5), corner, "Size 7 draws them 9 px wide");
let loose = ToolContext { selection_tolerance: 8.0, ..cx(&d, &s, &p) };
assert_eq!(press(&loose, 107.5, 100.5), corner);
// On screen: at 200% the reach is half as far in the document.
let zoomed = ToolContext { zoom: 2.0, ..cx(&d, &s, &p) };
assert_eq!(press(&zoomed, 102.1, 100.2), corner);
assert_ne!(press(&zoomed, 102.6, 100.2), corner);
}
/// A press on a clip group's clipping path, where the art it clips lies over its edge, takes
/// the clipping path's segment, so the mask can be reshaped on the canvas; inside the mask the
/// art is taken, as before.
#[test]
fn a_press_on_a_clipping_path_takes_its_segment_over_the_art() {
let (d, _, clip, art) = doc_with_masked_square();
let (s, p) = (Selection::default(), paint());
let c = cx(&d, &s, &p);
let press = |x: f64, y: f64| DirectSelectionTool::new(false).pointer(&c, &PointerEvent::new(PointerKind::Down, x, y));
let names = |actions: &[Action], id: NodeId| {
actions.iter().any(|a| matches!(a, Action::Exec(_, v) if v["id"] == json!(id.0) || v["ids"] == json!([id.0])))
};
// The middle of the right edge, on it and just inside it.
for x in [500.0, 498.5] {
let a = press(x, 450.0);
assert!(names(&a, clip), "at {x}: {a:?}");
}
let a = press(450.0, 450.0);
assert!(names(&a, art) && !names(&a, clip), "{a:?}");
}
/// A blend of two 20 pt squares centred on (110, 310) and (210, 310), selected.
fn blend_doc() -> (vectorcraft_doc::Document, NodeId, NodeId) {
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let key = |id: NodeId, x: f64| {
let r = vectorcraft_geom::shapes::rectangle(Rect::new(x, 300.0, x + 20.0, 320.0));
std::sync::Arc::new(vectorcraft_doc::Node::path(id, r, vectorcraft_doc::Appearance::default_art()))
};
let (g, k1, k2) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
let b = vectorcraft_doc::Node::new(g, NodeKind::Blend { children: vec![key(k1, 100.0), key(k2, 200.0)], spec: Default::default() });
d.insert(Some(l), 1, b).unwrap();
(d, g, k1)
}
/// Highlight anchors on mouse over (#394): the anchor under the pointer, within the selection
/// tolerance, shows enlarged; none with the preference off or away from anchors.
#[test]
fn the_anchor_under_the_pointer_is_highlighted() {
let (d, id) = doc_with_rect();
let s = Selection::default();
let p = paint();
let big = |t: &DirectSelectionTool, cx: &ToolContext| {
t.overlays(cx).into_iter().find_map(|o| match o {
Overlay::Anchor { p, size, .. } if size > 5.0 => Some(p),
_ => None,
})
};
let mut t = DirectSelectionTool::new(false);
let on = cx(&d, &s, &p);
t.pointer(&on, &PointerEvent::new(PointerKind::Move, 102.0, 101.0));
assert_eq!(big(&t, &on), Some(Point::new(100.0, 100.0)), "the corner of {id:?}");
t.pointer(&on, &PointerEvent::new(PointerKind::Move, 150.0, 100.0));
assert_eq!(big(&t, &on), None, "mid-segment: no anchor");
let off = ToolContext { highlight_anchors: false, ..cx(&d, &s, &p) };
t.pointer(&off, &PointerEvent::new(PointerKind::Move, 102.0, 101.0));
assert_eq!(big(&t, &off), None, "off");
}
#[test]
fn dragging_a_spine_point_moves_it() {
let (d, g, _) = blend_doc();
let mut s = Selection::default();
s.set([g]);
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 211.0, 310.0)), vec![]);
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 221.0, 350.0));
assert_eq!(a[0], Action::Begin("Reshape Spine".into()));
assert_eq!(a[1], Action::Preview("object.blend.spine.moveAnchor".into(), json!({"id": g.0, "anchor": 1, "x": 220.0, "y": 350.0})));
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 221.0, 350.0)), vec![Action::Commit]);
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Anchor { filled: true, .. })), "the clicked point shows");
}
/// #1111: the Anchor Point tool bends a selected blend's straight spine; Direct Selection
/// reshapes a curved spine segment and leaves a straight one alone, as it does a path's.
#[test]
fn spine_segments_bend_with_the_anchor_point_tool_and_direct_selection() {
let (mut d, g, _) = blend_doc();
let mut s = Selection::default();
s.set([g]);
let p = paint();
let drag = |t: &mut dyn crate::Tool, cx: &ToolContext| {
t.pointer(cx, &PointerEvent::new(PointerKind::Down, 160.0, 311.0));
let a = t.pointer(cx, &PointerEvent::new(PointerKind::Drag, 160.0, 351.0));
t.pointer(cx, &PointerEvent::new(PointerKind::Up, 160.0, 351.0));
a
};
let straight = cx(&d, &s, &p);
let a = drag(crate::create("anchorPoint").as_mut(), &straight);
assert!(
matches!(a.last(), Some(Action::Preview(c, v)) if c == "object.blend.spine.reshapeSegment" && v["id"] == g.0 && v["segment"] == 0 && v["dy"] == 40.0),
"{a:?}"
);
let a = drag(&mut DirectSelectionTool::new(false), &straight);
assert!(!a.iter().any(|x| matches!(x, Action::Preview(c, _) if c == "object.blend.spine.reshapeSegment")), "straight: {a:?}");
// The spine bent upward: Direct Selection takes it from where it passes.
let mut sp = vectorcraft_geom::SubPath::polyline(&[Point::new(110.0, 310.0), Point::new(210.0, 310.0)], false);
sp.bend_segment(0, 0.5, vectorcraft_geom::Vec2::new(0.0, 20.0));
let Some(NodeKind::Blend { spec, .. }) = d.node_mut(g).map(|n| &mut n.kind) else { panic!("blend") };
(spec.spine, spec.key_anchors) = (Some(PathData::single(sp)), vec![0, 1]);
let mut t = DirectSelectionTool::new(false);
let curved = cx(&d, &s, &p);
t.pointer(&curved, &PointerEvent::new(PointerKind::Down, 160.0, 330.0));
let a = t.pointer(&curved, &PointerEvent::new(PointerKind::Drag, 160.0, 345.0));
assert_eq!(a[0], Action::Begin("Reshape Spine".into()));
assert!(
matches!(&a[1], Action::Preview(c, v) if c == "object.blend.spine.reshapeSegment" && v["segment"] == 0 && v["dx"] == 0.0 && v["dy"] == 15.0),
"{a:?}"
);
assert_eq!(t.pointer(&curved, &PointerEvent::new(PointerKind::Up, 160.0, 345.0)), vec![Action::Commit]);
}
/// Shift held while a curved segment is dragged sends `perpendicular`. The test drags a path's
/// segment with Direct Selection, and a blend spine's segment with Direct Selection and with the
/// Anchor Point tool.
#[test]
fn shift_reshapes_a_segment_perpendicular() {
let shift = Mods { shift: true, ..Mods::default() };
let p = paint();
let drag = |t: &mut dyn crate::Tool, cx: &ToolContext, from: (f64, f64), mods: Mods| {
t.pointer(cx, &PointerEvent::new(PointerKind::Down, from.0, from.1));
let a = t.pointer(cx, &PointerEvent::new(PointerKind::Drag, from.0 + 10.0, from.1 - 20.0).with_mods(mods));
t.pointer(cx, &PointerEvent::new(PointerKind::Up, from.0 + 10.0, from.1 - 20.0).with_mods(mods));
a
};
let sent = |a: &[Action], cmd: &str| match a.last() {
Some(Action::Preview(c, v)) if c == cmd => v["perpendicular"].as_bool(),
_ => None,
};
let (d, _) = arch_doc();
let s = Selection::default();
let arch = cx(&d, &s, &p);
let (mut b, g, _) = blend_doc();
let mut sp = vectorcraft_geom::SubPath::polyline(&[Point::new(110.0, 310.0), Point::new(210.0, 310.0)], false);
sp.bend_segment(0, 0.5, vectorcraft_geom::Vec2::new(0.0, 20.0));
let Some(NodeKind::Blend { spec, .. }) = b.node_mut(g).map(|n| &mut n.kind) else { panic!("blend") };
(spec.spine, spec.key_anchors) = (Some(PathData::single(sp)), vec![0, 1]);
let mut bs = Selection::default();
bs.set([g]);
let spine = cx(&b, &bs, &p);
for (mods, perpendicular) in [(Mods::default(), false), (shift, true)] {
let a = drag(&mut DirectSelectionTool::new(false), &arch, (150.0, 262.5), mods);
assert_eq!(sent(&a, "path.reshapeSegment"), Some(perpendicular), "{a:?}");
let a = drag(&mut DirectSelectionTool::new(false), &spine, (160.0, 330.0), mods);
assert_eq!(sent(&a, "object.blend.spine.reshapeSegment"), Some(perpendicular), "{a:?}");
let a = drag(crate::create("anchorPoint").as_mut(), &spine, (160.0, 330.0), mods);
assert_eq!(sent(&a, "object.blend.spine.reshapeSegment"), Some(perpendicular), "{a:?}");
}
}
#[test]
fn direct_and_group_selection_pick_blend_keys() {
let (d, g, k1) = blend_doc();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let a = DirectSelectionTool::new(false).pointer(&cx, &PointerEvent::new(PointerKind::Down, 104.0, 304.0));
assert_eq!(a, vec![Action::Exec("select.set".into(), json!({"ids": [k1.0]}))]);
let a = DirectSelectionTool::new(true).pointer(&cx, &PointerEvent::new(PointerKind::Down, 104.0, 304.0));
assert_eq!(a, vec![Action::Exec("select.set".into(), json!({"ids": [k1.0]}))]);
// A click on a step (between the keys) still selects the blend.
let a = DirectSelectionTool::new(true).pointer(&cx, &PointerEvent::new(PointerKind::Down, 160.0, 304.0));
assert_eq!(a, vec![Action::Exec("select.set".into(), json!({"ids": [g.0]}))]);
}
#[test]
fn dragging_a_frame_corner_or_edge_reshapes_area_type() {
let (d, text) = doc_with_area_type();
let p = paint();
let s = Selection::default();
let cx1 = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
// The bottom-right corner of the 120 × 40 frame at (300, 300) selects the text...
assert_eq!(
t.pointer(&cx1, &PointerEvent::new(PointerKind::Down, 421.0, 339.0)),
vec![Action::Exec("select.set".into(), json!({"ids": [text.0]}))]
);
// ...and drags that corner.
let a = t.pointer(&cx1, &PointerEvent::new(PointerKind::Drag, 461.0, 399.0));
assert_eq!(a[0], Action::Begin("Reshape Type Area".into()));
assert_eq!(a[1], Action::Preview("text.reshapeArea".into(), json!({"id": text.0, "anchors": [[0, 2]], "dx": 40.0, "dy": 60.0})));
assert_eq!(t.pointer(&cx1, &PointerEvent::new(PointerKind::Up, 461.0, 399.0)), vec![Action::Commit]);
// The top edge drags both its ends; a click alone changes nothing.
let mut s = Selection::default();
s.add(text);
let cx2 = cx(&d, &s, &p);
assert!(t.pointer(&cx2, &PointerEvent::new(PointerKind::Down, 350.0, 301.0)).is_empty());
let a = t.pointer(&cx2, &PointerEvent::new(PointerKind::Drag, 350.0, 281.0));
assert_eq!(a[1], Action::Preview("text.reshapeArea".into(), json!({"id": text.0, "anchors": [[0, 0], [0, 1]], "dx": 0.0, "dy": -20.0})));
t.pointer(&cx2, &PointerEvent::new(PointerKind::Up, 350.0, 281.0));
assert!(t.pointer(&cx2, &PointerEvent::new(PointerKind::Down, 300.0, 340.0)).is_empty());
assert!(t.pointer(&cx2, &PointerEvent::new(PointerKind::Up, 300.0, 340.0)).is_empty());
// The selected frame's corners show.
assert_eq!(t.overlays(&cx2).iter().filter(|o| matches!(o, Overlay::Anchor { filled: false, .. })).count(), 4);
}
/// #3980: type on a path shows its path's anchors and a drag of one reshapes the path, the last
/// one too although the end bracket stands on it; the bracket's stem above it drags the bracket.
#[test]
fn dragging_an_anchor_of_type_on_a_path_reshapes_its_path() {
let (d, text) = doc_with_path_type();
let p = paint();
let s = Selection::default();
let cx1 = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
assert_eq!(
t.pointer(&cx1, &PointerEvent::new(PointerKind::Down, 101.0, 301.0)),
vec![Action::Exec("select.set".into(), json!({"ids": [text.0]}))]
);
let a = t.pointer(&cx1, &PointerEvent::new(PointerKind::Drag, 121.0, 261.0));
assert_eq!(a[0], Action::Begin("Reshape Type Path".into()));
assert_eq!(a[1], Action::Preview("text.reshapePath".into(), json!({"id": text.0, "anchors": [[0, 0]], "dx": 20.0, "dy": -40.0})));
assert_eq!(t.pointer(&cx1, &PointerEvent::new(PointerKind::Up, 121.0, 261.0)), vec![Action::Commit]);
let mut s = Selection::default();
s.add(text);
let cx2 = cx(&d, &s, &p);
let anchors: Vec<Point> =
t.overlays(&cx2).iter().filter_map(|o| if let Overlay::Anchor { p, filled: false, .. } = o { Some(*p) } else { None }).collect();
assert_eq!(anchors, vec![Point::new(100.0, 300.0), Point::new(400.0, 300.0)]);
assert!(t.pointer(&cx2, &PointerEvent::new(PointerKind::Down, 400.0, 300.0)).is_empty());
let a = t.pointer(&cx2, &PointerEvent::new(PointerKind::Drag, 400.0, 260.0));
assert_eq!(a.last(), Some(&Action::Preview("text.reshapePath".into(), json!({"id": text.0, "anchors": [[0, 1]], "dx": 0.0, "dy": -40.0}))));
t.pointer(&cx2, &PointerEvent::new(PointerKind::Up, 400.0, 260.0));
t.pointer(&cx2, &PointerEvent::new(PointerKind::Down, 400.0, 290.0));
let a = t.pointer(&cx2, &PointerEvent::new(PointerKind::Drag, 340.0, 290.0));
assert!(matches!(a.last(), Some(Action::Preview(c, _)) if c == "type.pathOptions"), "{a:?}");
}
#[test]
fn click_anchor_selects_it() {
let (d, id) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "set"}))]);
}
/// Pressing an anchor of a path whose anchors are all selected (stored as the whole path)
/// keeps them all selected, so the drag moves them all; Shift still deselects that anchor.
#[test]
fn pressing_an_anchor_of_a_fully_selected_path_keeps_the_selection() {
let (d, id) = doc_with_rect();
let mut s = Selection::default();
s.set([id]);
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)).is_empty());
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 120.0, 100.0));
assert!(matches!(a.as_slice(), [Action::Begin(_), Action::Preview(c, _)] if c == "object.transform"), "{a:?}");
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 120.0, 100.0));
let shift = Mods { shift: true, ..Default::default() };
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0).with_mods(shift));
assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "toggle"}))]);
}
/// A click (no drag) on an anchor that is selected with others, whether the whole path is or
/// some of its anchors, selects that anchor alone, as in Illustrator: a press keeps the
/// selection so a drag moves it all, the release without a drag narrows it. Shift still
/// toggles, and a click on a path's only selected anchor changes nothing visible.
#[test]
fn a_click_on_a_selected_anchor_selects_it_alone() {
let (d, id) = doc_with_rect();
let p = paint();
let alone = vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "set"}))];
let mut whole = Selection::default();
whole.set([id]);
for s in [whole, anchors_of(id, &[(0, 0), (0, 1)])] {
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)).is_empty(), "{s:?}");
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 100.0, 100.0)), alone, "{s:?}");
// Dragged instead: moved, selection kept.
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)).is_empty());
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 120.0, 100.0));
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 120.0, 100.0)), vec![Action::Commit], "{s:?}");
}
// An anchor not yet selected is selected on the press, and the release adds nothing.
let none = Selection::default();
let cx = cx(&d, &none, &p);
let mut t = DirectSelectionTool::new(false);
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)), alone);
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 100.0, 100.0)).is_empty());
}
/// Alt-pressing an anchor picks the whole path, as Alt on a segment does, and the drag copies
/// it as the Selection tool's Alt-drag does.
#[test]
fn alt_dragging_an_anchor_copies_the_whole_path() {
let (d, id) = doc_with_rect();
let alt = Mods { alt: true, ..Default::default() };
let mut whole = Selection::default();
whole.set([id]);
let p = paint();
for (s, pick) in [(Selection::default(), true), (anchors_of(id, &[(0, 0)]), true), (whole, false)] {
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0).with_mods(alt));
let set = vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))];
assert_eq!(a, if pick { set } else { vec![] }, "{s:?}");
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 120.0, 100.0).with_mods(alt));
assert!(matches!(a.as_slice(), [Action::Begin(_), Action::Preview(c, v)] if c == "object.transform" && v["copy"] == true), "{a:?}");
}
}
#[test]
fn marquee_selects_anchors() {
let (d, id) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 50.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 150.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0));
assert_eq!(a, vec![Action::Exec("select.anchorsMany".into(), json!({"items": [{"id": id.0, "anchors": [[0, 1]]}], "mode": "set"}))]);
}
#[test]
fn marquee_selects_ruler_guides() {
let (mut d, id) = doc_with_rect();
d.guides.push(vectorcraft_doc::Guide::new(false, 80.0));
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 50.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 150.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0));
assert_eq!(
a,
vec![
Action::Exec("select.anchorsMany".into(), json!({"items": [{"id": id.0, "anchors": [[0, 1]]}], "mode": "set"})),
Action::Exec("guide.select".into(), json!({"indexes": [0], "toggle": true})),
]
);
}
/// #2441: Group Selection picks one path of a compound path (the outer shape or a hole);
/// a second click on it adds the compound path, like a group around a member.
#[test]
fn group_selection_picks_a_path_of_a_compound_path() {
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
let l = d.layers[0].id;
let (outer, hole, compound) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
let rect = |id, a: f64, b: f64| {
std::sync::Arc::new(vectorcraft_doc::Node::path(id, vectorcraft_geom::shapes::rectangle(Rect::new(a, a, b, b)), Default::default()))
};
let mut c = vectorcraft_doc::Node::new(
compound,
vectorcraft_doc::NodeKind::Compound {
children: vec![rect(outer, 0.0, 200.0), rect(hole, 50.0, 150.0)],
rule: vectorcraft_geom::FillRule::EvenOdd,
},
);
c.appearance = vectorcraft_doc::Appearance::default_art().into();
d.insert(Some(l), 0, c).unwrap();
let p = paint();
let none = Selection::default();
let click = |s: &Selection, x, y| DirectSelectionTool::new(true).pointer(&cx(&d, s, &p), &PointerEvent::new(PointerKind::Down, x, y));
let set = |id: NodeId| vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))];
assert_eq!(click(&none, 25.0, 100.0), set(outer), "the fill between the outline and the hole");
assert_eq!(click(&none, 50.0, 100.0), set(hole), "the hole's outline");
assert!(click(&none, 100.0, 100.0).is_empty(), "inside the hole nothing is hit");
let mut s = Selection::default();
s.set([outer]);
assert_eq!(click(&s, 25.0, 100.0), vec![Action::Exec("select.add".into(), json!({"ids": [compound.0]}))]);
}
/// Group Selection Shift-click on a selected compound-path member deselects it on release.
#[test]
fn group_selection_shift_click_deselects_a_compound_path_member() {
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
let l = d.layers[0].id;
let (outer, hole, compound) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
let rect = |id, a: f64, b: f64| {
std::sync::Arc::new(vectorcraft_doc::Node::path(id, vectorcraft_geom::shapes::rectangle(Rect::new(a, a, b, b)), Default::default()))
};
let mut c = vectorcraft_doc::Node::new(
compound,
vectorcraft_doc::NodeKind::Compound {
children: vec![rect(outer, 0.0, 200.0), rect(hole, 50.0, 150.0)],
rule: vectorcraft_geom::FillRule::EvenOdd,
},
);
c.appearance = vectorcraft_doc::Appearance::default_art().into();
d.insert(Some(l), 0, c).unwrap();
let p = paint();
let mut s = Selection::default();
s.set([outer]);
let shift = Mods { shift: true, ..Mods::default() };
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(true);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 25.0, 100.0).with_mods(shift));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 25.0, 100.0).with_mods(shift));
assert_eq!(a, vec![Action::Exec("select.toggle".into(), json!({"id": outer.0}))]);
}
/// A path inside an inner group, a sibling path outside it, and the groups' ids.
/// The inside path is the rectangle at (100, 100); the outside one at (300, 300).
fn nested_group_doc() -> (vectorcraft_doc::Document, NodeId, NodeId, NodeId, NodeId) {
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
let l = d.layers[0].id;
let (inside, outside, inner, outer) = (d.alloc_id(), d.alloc_id(), d.alloc_id(), d.alloc_id());
let path = |id, x| {
std::sync::Arc::new(vectorcraft_doc::Node::path(
id,
vectorcraft_geom::shapes::rectangle(Rect::new(x, x, x + 100.0, x + 100.0)),
vectorcraft_doc::Appearance::default_art(),
))
};
let inner_g = vectorcraft_doc::Node::group(inner, vec![path(inside, 100.0)]);
let outer_g = vectorcraft_doc::Node::group(outer, vec![std::sync::Arc::new(inner_g), path(outside, 300.0)]);
d.insert(Some(l), 0, outer_g).unwrap();
(d, inside, outside, inner, outer)
}
/// Isolation mode: a marquee and an anchor click take the path inside the isolated group and
/// leave the path beside it. Group Selection stops at the isolated group instead of adding
/// the group around it.
#[test]
fn isolation_mode_leaves_art_outside_the_isolated_group() {
let (d, inside, outside, inner, outer) = nested_group_doc();
let p = paint();
let none = Selection::default();
let isolated = ToolContext { isolation: Some(inner), ..cx(&d, &none, &p) };
let mut t = DirectSelectionTool::new(false);
t.pointer(&isolated, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
let a = t.pointer(&isolated, &PointerEvent::new(PointerKind::Up, 450.0, 450.0));
let items = a.first().and_then(|act| match act {
Action::Exec(c, v) if c == "select.anchorsMany" => v.get("items").cloned(),
_ => None,
});
let items = items.unwrap();
assert!(items.as_array().is_some_and(|a| a.iter().all(|item| item["id"] == inside.0)), "marquee: {a:?}");
assert!(items.as_array().is_some_and(|a| !a.is_empty()), "the path inside is still selected: {a:?}");
let a = DirectSelectionTool::new(false).pointer(&isolated, &PointerEvent::new(PointerKind::Down, 300.0, 300.0));
assert!(a.is_empty(), "a click on {outside:?}, outside isolation, starts a marquee: {a:?}");
let a = DirectSelectionTool::new(false).pointer(&isolated, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": inside.0, "anchors": [[0, 0]], "mode": "set"}))]);
let mut s = Selection::default();
s.set([inside, inner]);
let climbed = ToolContext { isolation: Some(inner), ..cx(&d, &s, &p) };
let a = DirectSelectionTool::new(true).pointer(&climbed, &PointerEvent::new(PointerKind::Down, 150.0, 150.0));
assert!(a.is_empty(), "does not add the group outside isolation ({outer:?}): {a:?}");
}
/// #927: a marquee across type selects the type object whole, beside the anchors it holds.
#[test]
fn marquee_selects_type_it_touches() {
// The rectangle at (100, 100)–(200, 200) and area type at (300, 300).
let (d, text) = doc_with_area_type();
let rect = d.layers[0].children().and_then(|c| c.first()).map(|n| n.id).unwrap();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
// From empty canvas across the type to inside the rectangle (its bottom-right corner).
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 450.0, 450.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 150.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 150.0));
let items = json!([{"id": rect.0, "anchors": [[0, 2]]}, {"id": text.0, "anchors": []}]);
assert_eq!(a, vec![Action::Exec("select.anchorsMany".into(), json!({"items": items, "mode": "set"}))]);
// Away from the type, only anchors.
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 50.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0));
assert_eq!(a, vec![Action::Exec("select.anchorsMany".into(), json!({"items": [{"id": rect.0, "anchors": [[0, 1]]}], "mode": "set"}))]);
}
/// A marquee across a member of an Intertwine group selects the member's anchors, as in a
/// plain group, and not the Intertwine group whole.
#[test]
fn marquee_selects_the_anchors_of_intertwine_members() {
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
let l = d.layers[0].id;
let (a, b, g) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
let path = |id, x| {
std::sync::Arc::new(vectorcraft_doc::Node::path(
id,
vectorcraft_geom::shapes::rectangle(Rect::new(x, 100.0, x + 100.0, 200.0)),
vectorcraft_doc::Appearance::default_art(),
))
};
let intertwine = vectorcraft_doc::Node::new(g, vectorcraft_doc::NodeKind::Intertwine { children: vec![path(a, 100.0), path(b, 300.0)] });
d.insert(Some(l), 0, intertwine).unwrap();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
for group in [false, true] {
let mut t = DirectSelectionTool::new(group);
// Around the first member's top-right corner.
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 50.0));
let actions = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0));
let items = json!([{"id": a.0, "anchors": [[0, 1]]}]);
assert_eq!(actions, vec![Action::Exec("select.anchorsMany".into(), json!({"items": items, "mode": "set"}))], "group: {group}");
}
}
/// Shift-drag a marquee: the anchors inside toggle (#483), with Group Selection too.
#[test]
fn shift_marquee_toggles_anchors() {
let (d, id) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let shift = Mods { shift: true, ..Default::default() };
for group in [false, true] {
let mut t = DirectSelectionTool::new(group);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 50.0).with_mods(shift));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 150.0).with_mods(shift));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0).with_mods(shift));
assert_eq!(a, vec![Action::Exec("select.anchorsMany".into(), json!({"items": [{"id": id.0, "anchors": [[0, 1]]}], "mode": "toggle"}))]);
}
}
/// Group Selection: E pressed during a marquee drag makes it select the objects entirely inside
/// it whole, a group's member without its group. Direct Selection does not take E.
#[test]
fn e_during_a_group_selection_marquee_selects_the_objects_inside() {
use vectorcraft_doc::{Appearance, Node};
let (mut d, id) = doc_with_rect();
let l = d.layers[0].id;
let rect = |id, r| Node::path(id, vectorcraft_geom::shapes::rectangle(r), Appearance::default_art());
let (g, a, b) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
d.insert(Some(l), 1, Node::group(g, vec![])).unwrap();
d.insert(Some(g), 0, rect(a, Rect::new(100.0, 220.0, 120.0, 240.0))).unwrap();
d.insert(Some(g), 1, rect(b, Rect::new(100.0, 300.0, 120.0, 320.0))).unwrap();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let e = ToolKey::Letter('e');
let drag = |group: bool, mods: Mods| {
let mut t = DirectSelectionTool::new(group);
assert!(!t.claims_key(&cx, e), "no marquee yet");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0).with_mods(mods));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 300.0, 250.0).with_mods(mods));
assert_eq!(t.claims_key(&cx, e), group);
t.key(&cx, e, Mods::default());
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 300.0, 250.0).with_mods(mods))
};
assert_eq!(drag(true, Mods::default()), vec![Action::Exec("select.set".into(), json!({"ids": [id.0, a.0]}))]);
let shift = Mods { shift: true, ..Default::default() };
assert_eq!(drag(true, shift), vec![Action::Exec("select.toggle".into(), json!({"ids": [id.0, a.0]}))]);
// Direct Selection's marquee still selects the anchors inside it.
let anchors = |id: NodeId, list: Value| json!({"id": id.0, "anchors": list});
let all = json!([[0, 0], [0, 1], [0, 2], [0, 3]]);
assert_eq!(
drag(false, Mods::default()),
vec![Action::Exec("select.anchorsMany".into(), json!({"items": [anchors(id, all.clone()), anchors(a, all)], "mode": "set"}))]
);
}
/// An open path from (100, 300) to (200, 300) arching up through (150, 262.5).
fn arch_doc() -> (vectorcraft_doc::Document, NodeId) {
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let id = d.alloc_id();
let mut sp = vectorcraft_geom::SubPath::polyline(&[Point::new(100.0, 300.0), Point::new(200.0, 300.0)], false);
sp.anchors[0].h_out = Point::new(100.0, 250.0);
sp.anchors[1].h_in = Point::new(200.0, 250.0);
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, PathData::single(sp), vectorcraft_doc::Appearance::default_art())).unwrap();
(d, id)
}
/// #1743: Escape mid-drag cancels it; the release after it does nothing more.
#[test]
fn escape_cancels_a_drag() {
let (d, _) = arch_doc();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 262.5));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 240.0));
assert!(t.busy());
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), vec![Action::Cancel]);
assert!(!t.busy());
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 220.0)), vec![]);
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 220.0)), vec![]);
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), vec![], "nothing to cancel");
}
#[test]
fn dragging_a_curved_segment_reshapes_it_and_a_straight_one_moves_its_anchors() {
let (d, arch) = arch_doc();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
// The arch: its segment gets selected, and the drag bends it.
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 262.5));
assert_eq!(a, vec![Action::Exec("select.segments".into(), json!({"id": arch.0, "segments": [[0, 0]]}))]);
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 242.5));
assert_eq!(a[0], Action::Begin("Reshape".into()));
let Action::Preview(cmd, v) = &a[1] else { panic!("{a:?}") };
assert_eq!(
(cmd.as_str(), v["id"].clone(), v["segment"].clone(), v["dx"].clone(), v["dy"].clone()),
("path.reshapeSegment", json!(arch.0), json!(0), json!(0.0), json!(-20.0))
);
assert!((v["t"].as_f64().unwrap() - 0.5).abs() < 0.01, "{v}");
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 242.5)), vec![Action::Commit]);
// A straight edge of the rectangle: its two anchors get selected and move.
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 101.0));
assert!(matches!(&a[..], [Action::Exec(c, v)] if c == "select.segments" && v["segments"] == json!([[0, 0]])), "{a:?}");
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 81.0));
assert_eq!(a[1], Action::Preview("path.moveAnchors".into(), json!({"dx": 0.0, "dy": -20.0})));
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 81.0));
// Alt on the stroke picks the whole path, and Alt-drag copies it.
let alt = Mods { alt: true, ..Default::default() };
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 101.0).with_mods(alt));
assert!(matches!(&a[..], [Action::Exec(c, _)] if c == "select.set"), "{a:?}");
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 81.0).with_mods(alt));
assert!(matches!(&a[..], [Action::Begin(_), Action::Preview(c, v)] if c == "object.transform" && v["copy"] == true), "{a:?}");
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 81.0).with_mods(alt));
// The fill still selects the whole path, and a path selected as a whole still moves whole.
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 150.0));
assert!(matches!(&a[..], [Action::Exec(c, _)] if c == "select.set"), "{a:?}");
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 150.0));
let rect = d.layers[0].children().unwrap()[0].id;
let mut whole = Selection::default();
whole.set([rect]);
let cx = crate::testutil::cx(&d, &whole, &p);
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 101.0)), vec![]);
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 81.0));
assert!(matches!(&a[..], [Action::Begin(_), Action::Preview(c, _)] if c == "object.transform"), "{a:?}");
}
/// #494: the handles shown are the handles dragged. A path selected as a whole (the one the
/// Pen just drew) shows all its handles, and they drag; the direct-selected anchors' handles
/// only otherwise; none with Show handles when multiple anchors are selected off and several
/// anchors selected. A handle shorter than the tolerance leaves its anchor to be picked, and
/// the tolerance is the preference's, in screen pixels at any zoom.
#[test]
fn the_handles_shown_are_the_handles_dragged() {
let (d, arch) = arch_doc();
let p = paint();
// Does a press at (x, y) take a handle?
let handle = |c: &ToolContext, x: f64, y: f64| {
DirectSelectionTool::new(false).pointer(c, &PointerEvent::new(PointerKind::Down, x, y)) == vec![Action::Begin("Reshape".into())]
};
let mut whole = Selection::default();
whole.set([arch]);
let c = cx(&d, &whole, &p);
assert!(handle(&c, 101.0, 251.0), "the first anchor's handle, the path selected whole");
let mut t = DirectSelectionTool::new(false);
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 101.0, 251.0));
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 120.0, 240.0));
assert!(matches!(&a[..], [Action::Preview(c, v)] if c == "path.setHandle" && v["anchor"] == 0 && v["which"] == "out"), "{a:?}");
// Only the second anchor direct-selected: the first one's handle is hidden.
let second = anchors_of(arch, &[(0, 1)]);
assert!(!handle(&cx(&d, &second, &p), 101.0, 251.0));
assert!(handle(&cx(&d, &second, &p), 201.0, 251.0));
// Show handles when multiple anchors are selected off: the whole path's are hidden.
assert!(!handle(&ToolContext { handles_multiple: false, ..cx(&d, &whole, &p) }, 101.0, 251.0));
assert!(handle(&ToolContext { handles_multiple: false, ..cx(&d, &second, &p) }, 201.0, 251.0));
// The tolerance is the preference's, in screen pixels: 3 px is 0.75 pt at 400%.
let zoomed = ToolContext { zoom: 4.0, ..cx(&d, &whole, &p) };
assert!(!handle(&zoomed, 101.0, 251.0));
assert!(handle(&zoomed, 100.5, 250.5));
assert!(handle(&ToolContext { selection_tolerance: 8.0, ..cx(&d, &whole, &p) }, 106.0, 254.0));
}
/// #494: a handle 2 pt long, inside the tolerance round its anchor, doesn't hide the anchor.
#[test]
fn a_short_handle_leaves_its_anchor_to_be_picked() {
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let id = d.alloc_id();
let mut sp = vectorcraft_geom::SubPath::polyline(&[Point::new(100.0, 300.0), Point::new(200.0, 300.0)], false);
sp.anchors[0].h_out = Point::new(102.0, 300.0);
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, PathData::single(sp), vectorcraft_doc::Appearance::default_art())).unwrap();
let s = anchors_of(id, &[(0, 0)]);
let p = paint();
let c = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.0, 300.0)), vec![], "the anchor, already selected");
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 90.0, 290.0));
assert_eq!(a[0], Action::Begin("Move".into()));
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 90.0, 290.0));
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 102.5, 300.0)), vec![Action::Begin("Reshape".into())], "the handle");
}
/// Direct Selection's cursors from Illustrator's manual: the white arrow at rest; over an anchor
/// a square, hollow on an unselected path and filled on a selected one; the Reshape Segment
/// cursor over a curved segment a drag reshapes, not over a straight one (which moves) or a
/// path selected whole (which moves whole); Group Selection's plus with Alt and as its tool.
#[test]
fn hovering_shows_the_anchor_square_reshape_and_group_cursors() {
let (d, arch) = arch_doc();
let p = paint();
let m = Mods::default();
let pt = Point::new;
let none = Selection::default();
let c = cx(&d, &none, &p);
let t = DirectSelectionTool::new(false);
assert_eq!(t.cursor(&c, pt(20.0, 20.0), m), Cursor::ArrowHollow);
assert_eq!(t.cursor(&c, pt(100.0, 300.0), m), Cursor::DirectAnchor);
assert_eq!(t.cursor(&c, pt(150.0, 262.5), m), Cursor::ReshapeSegment);
assert_eq!(t.cursor(&c, pt(150.0, 100.0), m), Cursor::ArrowHollow, "a straight segment moves");
assert_eq!(t.cursor(&c, pt(150.0, 150.0), m), Cursor::ArrowHollow, "the fill");
let alt = Mods { alt: true, ..m };
assert_eq!(t.cursor(&c, pt(100.0, 300.0), alt), Cursor::GroupSelection);
assert_eq!(DirectSelectionTool::new(true).cursor(&c, pt(100.0, 300.0), m), Cursor::GroupSelection);
let mut whole = Selection::default();
whole.set([arch]);
let c = cx(&d, &whole, &p);
assert_eq!(t.cursor(&c, pt(200.0, 300.0), m), Cursor::DirectAnchorSelected);
assert_eq!(t.cursor(&c, pt(150.0, 262.5), m), Cursor::ArrowHollow, "a path selected whole moves whole");
// One anchor selected: its handle end shows and keeps the plain arrow, Alt or not (a press
// takes the handle).
let s = anchors_of(arch, &[(0, 0)]);
let c = cx(&d, &s, &p);
assert_eq!(t.cursor(&c, pt(100.0, 250.0), m), Cursor::ArrowHollow, "a handle end");
assert_eq!(t.cursor(&c, pt(100.0, 250.0), alt), Cursor::ArrowHollow, "a handle end with Alt");
assert_eq!(t.cursor(&c, pt(200.0, 300.0), m), Cursor::DirectAnchorSelected);
// Bending the curve keeps the Reshape Segment cursor.
let mut t = DirectSelectionTool::new(false);
let c = cx(&d, &none, &p);
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 262.5));
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 150.0, 230.0));
assert_eq!(t.cursor(&c, pt(150.0, 230.0), m), Cursor::ReshapeSegment);
}
/// A drag shows the filled arrow, hollow while it snaps to a point; an Alt-drag copy shows the
/// double arrowhead.
#[test]
fn drags_show_the_filled_arrow_hollow_on_a_snap_and_alt_the_copy_cursor() {
let (d, _, b) = two_squares();
let p = paint();
let none = Mods::default();
let at = |kind, x, y, m: Mods| PointerEvent::new(kind, x, y).with_mods(m);
let one = anchors_of(b, &[(0, 0)]);
let c = cx(&d, &one, &p);
let mut t = DirectSelectionTool::new(false);
t.pointer(&c, &at(PointerKind::Down, 300.0, 300.0, none));
t.pointer(&c, &at(PointerKind::Drag, 250.0, 260.0, none));
assert_eq!(t.cursor(&c, Point::new(250.0, 260.0), none), Cursor::Arrow);
// Onto A's bottom-right corner.
t.pointer(&c, &at(PointerKind::Drag, 203.0, 199.0, none));
assert_eq!(t.cursor(&c, Point::new(203.0, 199.0), none), Cursor::ArrowHollow);
t.pointer(&c, &at(PointerKind::Up, 203.0, 199.0, none));
// Shift with a Constrain Angle of 10°: the constraint alone isn't a snap.
let shift = Mods { shift: true, ..none };
let tilted = ToolContext { constrain_angle: 10.0, ..cx(&d, &one, &p) };
t.pointer(&tilted, &at(PointerKind::Down, 300.0, 300.0, shift));
t.pointer(&tilted, &at(PointerKind::Drag, 380.0, 262.0, shift));
assert_eq!(t.cursor(&tilted, Point::new(380.0, 262.0), shift), Cursor::Arrow);
t.pointer(&tilted, &at(PointerKind::Up, 380.0, 262.0, shift));
let mut whole = Selection::default();
whole.set([b]);
let c = cx(&d, &whole, &p);
let alt = Mods { alt: true, ..none };
t.pointer(&c, &at(PointerKind::Down, 300.0, 300.0, alt));
t.pointer(&c, &at(PointerKind::Drag, 250.0, 260.0, alt));
assert_eq!(t.cursor(&c, Point::new(250.0, 260.0), alt), Cursor::SelectionCopy);
}
/// [`doc_with_rect`] (A, 100..200) and a second square B, 300..400: (A, B).
fn two_squares() -> (vectorcraft_doc::Document, NodeId, NodeId) {
let (mut d, a) = doc_with_rect();
let l = d.layers[0].id;
let b = d.alloc_id();
let sq = vectorcraft_geom::shapes::rectangle(Rect::new(300.0, 300.0, 400.0, 400.0));
d.insert(Some(l), 1, vectorcraft_doc::Node::path(b, sq, vectorcraft_doc::Appearance::default_art())).unwrap();
(d, a, b)
}
/// An anchor of a selected path is picked before the one of an unselected path on a layer
/// above that covers it, as Highlight anchors on mouse over already marks it (#1508); with
/// nothing selected, the topmost path's anchor is picked.
#[test]
fn a_selected_path_s_anchor_is_picked_before_one_above_it() {
let (mut d, a) = doc_with_rect();
let top = d.add_layer(None);
let b = d.alloc_id();
let sq = vectorcraft_geom::shapes::rectangle(Rect::new(101.0, 100.0, 201.0, 200.0));
d.insert(Some(top), 0, vectorcraft_doc::Node::path(b, sq, vectorcraft_doc::Appearance::default_art())).unwrap();
let p = paint();
let pick = |id: NodeId| vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "set"}))];
let s = anchors_of(a, &[(0, 2)]);
let mut t = DirectSelectionTool::new(false);
assert_eq!(t.pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::Down, 101.0, 100.0)), pick(a));
let mut whole = Selection::default();
whole.set([a]);
let mut t = DirectSelectionTool::new(false);
let cx2 = cx(&d, &whole, &p);
assert!(t.pointer(&cx2, &PointerEvent::new(PointerKind::Down, 101.0, 100.0)).is_empty());
assert_eq!(t.pointer(&cx2, &PointerEvent::new(PointerKind::Up, 101.0, 100.0)), pick(a));
let none = Selection::default();
let mut t = DirectSelectionTool::new(false);
assert_eq!(t.pointer(&cx(&d, &none, &p), &PointerEvent::new(PointerKind::Down, 101.0, 100.0)), pick(b));
}
/// The anchors `anchors` of path `id` direct-selected.
fn anchors_of(id: NodeId, anchors: &[AnchorRef]) -> Selection {
let mut s = Selection::default();
s.set([id]);
s.anchors.insert(id, anchors.iter().copied().collect());
s
}
/// Press at `from`, drag to `to`: the move the drag previews, and the smart guide labels shown.
fn drag(t: &mut DirectSelectionTool, cx: &ToolContext, from: (f64, f64), to: (f64, f64), mods: Mods) -> (Value, Vec<String>) {
t.pointer(cx, &PointerEvent::new(PointerKind::Down, from.0, from.1).with_mods(mods));
let a = t.pointer(cx, &PointerEvent::new(PointerKind::Drag, to.0, to.1).with_mods(mods));
let Some(Action::Preview(_, v)) = a.last() else { panic!("{a:?}") };
let labels = t
.overlays(cx)
.into_iter()
.filter_map(|o| match o {
Overlay::Label { text, .. } => Some(text),
_ => None,
})
.collect();
assert_eq!(t.pointer(cx, &PointerEvent::new(PointerKind::Up, to.0, to.1).with_mods(mods)), vec![Action::Commit]);
assert!(t.overlays(cx).iter().all(|o| !matches!(o, Overlay::Label { .. })), "the guides go with the drag");
(v.clone(), labels)
}
/// Alt-dragging a path by an anchor copies it, and that anchor snaps as a dragged anchor does:
/// onto another path's anchor, or onto an anchor of the original the copy leaves behind.
#[test]
fn alt_dragged_copies_snap_by_the_anchor_pressed() {
let (d, _, b) = two_squares();
let p = paint();
let alt = Mods { alt: true, ..Default::default() };
let mut whole = Selection::default();
whole.set([b]);
let c = cx(&d, &whole, &p);
let mut t = DirectSelectionTool::new(false);
let copy =
|dx: f64, dy: f64| json!({"matrix": matrix_json(vectorcraft_geom::Affine::translate(vectorcraft_geom::Vec2::new(dx, dy))), "copy": true});
// B's top-left corner, pressed 1 pt off, onto A's bottom-right one.
let (v, labels) = drag(&mut t, &c, (301.0, 301.0), (203.0, 199.0), alt);
assert_eq!((v, labels), (copy(-100.0, -100.0), vec!["anchor".to_string()]));
// B's top-left corner onto the original B's top-right one: the copy sits beside it.
let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (402.0, 301.0), alt);
assert_eq!((v, labels), (copy(100.0, 0.0), vec!["anchor".to_string()]));
// The history says what the drag did.
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 300.0, 300.0).with_mods(alt));
let out = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 250.0, 250.0).with_mods(alt));
assert_eq!(out.first(), Some(&Action::Begin("Copy".into())));
}
/// Dragging a fully selected path by an anchor moves it whole, so Alt pressed or released
/// during the drag turns the copy on and off, as with the Selection tool; the anchor pressed
/// snaps all along, onto the original only while it is left behind.
#[test]
fn alt_during_a_drag_by_an_anchor_toggles_the_copy() {
let (d, _, b) = two_squares();
let p = paint();
let (none, alt) = (Mods::default(), Mods { alt: true, ..Default::default() });
let mut whole = Selection::default();
whole.set([b]);
let c = cx(&d, &whole, &p);
let mut t = DirectSelectionTool::new(false);
let preview = |dx: f64, dy: f64, copy: bool| {
Action::Preview(
"object.transform".into(),
json!({"matrix": matrix_json(vectorcraft_geom::Affine::translate(vectorcraft_geom::Vec2::new(dx, dy))), "copy": copy}),
)
};
assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 300.0, 300.0)).is_empty());
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 250.0, 250.0)), vec![Action::Begin("Move".into()), preview(-50.0, -50.0, false)]);
// The tool sees the document as previewed: B moved along, not where the copy leaves it.
let (mut moved, _, b2) = two_squares();
let l = moved.layers[0].id;
let sq = vectorcraft_geom::shapes::rectangle(Rect::new(250.0, 250.0, 350.0, 350.0));
moved.remove(b2).unwrap();
moved.insert(Some(l), 1, vectorcraft_doc::Node::path(b, sq, vectorcraft_doc::Appearance::default_art())).unwrap();
let c = cx(&moved, &whole, &p);
// Alt down mid-drag: a copy, whose pressed corner can land on the original's top-right one.
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 402.0, 301.0).with_mods(alt)), vec![preview(100.0, 0.0, true)]);
// Alt up: a move again, which leaves nothing behind to snap to.
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 402.0, 301.0).with_mods(none)), vec![preview(102.0, 1.0, false)]);
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 402.0, 301.0)), vec![Action::Commit]);
// With only some anchors selected, the drag still moves just those.
let one = anchors_of(b, &[(0, 0)]);
let c = cx(&d, &one, &p);
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 300.0, 300.0));
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 250.0, 260.0).with_mods(alt));
assert!(matches!(a.last(), Some(Action::Preview(c, _)) if c == "path.moveAnchors"), "{a:?}");
}
/// A dragged anchor lands on another path's anchor or segment, lines up with the other anchors
/// of its own path, and with several selected the one pressed on snaps, the others following.
#[test]
fn dragged_anchors_snap_to_points_segments_and_smart_guides() {
let (d, _, b) = two_squares();
let p = paint();
let none = Mods::default();
let one = anchors_of(b, &[(0, 0)]);
let c = cx(&d, &one, &p);
let mut t = DirectSelectionTool::new(false);
// B's top-left corner, pressed 1 pt off, onto A's bottom-right one.
let (v, labels) = drag(&mut t, &c, (301.0, 301.0), (203.0, 199.0), none);
assert_eq!((v, labels), (json!({"dx": -100.0, "dy": -100.0}), vec!["anchor".to_string()]));
// Onto A's right side.
let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (203.0, 150.0), none);
assert_eq!((v, labels), (json!({"dx": -100.0, "dy": -150.0}), vec!["path".to_string()]));
// In line with B's own bottom-left corner, which stays put.
let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (303.0, 330.0), none);
assert_eq!((v, labels), (json!({"dx": 0.0, "dy": 30.0}), vec!["align".to_string()]));
// Shift keeps the move at 45° steps and still snaps along that line (#886): on the
// diagonal from B's corner, onto A's bottom-right corner…
let shift = Mods { shift: true, ..none };
let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (203.0, 199.0), shift);
let near = |v: &serde_json::Value, (x, y): (f64, f64)| (v["dx"].as_f64().unwrap() - x).hypot(v["dy"].as_f64().unwrap() - y) < 1e-9;
assert!(near(&v, (-100.0, -100.0)) && labels == ["anchor"], "{v} {labels:?}");
// …and with nothing in reach, at the pointer's distance along its angle.
let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (380.0, 302.0), shift);
let s = move_delta(Point::new(300.0, 300.0), Point::new(380.0, 302.0), true, 0.0);
assert!(near(&v, (s.x, s.y)) && labels.is_empty(), "{v} {labels:?}");
// Two anchors: the one pressed on (B's top-right) lands on A's corner.
let two = anchors_of(b, &[(0, 0), (0, 1)]);
let c = cx(&d, &two, &p);
let (v, _) = drag(&mut t, &c, (400.0, 300.0), (202.0, 198.0), none);
assert_eq!(v, json!({"dx": -200.0, "dy": -100.0}));
// Smart Guides off: Snap to Point pulls within its distance (2 px) only.
let c = ToolContext { smart_guides: false, ..cx(&d, &one, &p) };
assert_eq!(drag(&mut t, &c, (300.0, 300.0), (201.0, 199.0), none).0, json!({"dx": -100.0, "dy": -100.0}));
assert_eq!(drag(&mut t, &c, (300.0, 300.0), (203.0, 199.0), none).0, json!({"dx": -97.0, "dy": -101.0}));
let c = ToolContext { smart_guides: false, snap_to_point: false, ..cx(&d, &one, &p) };
assert_eq!(drag(&mut t, &c, (300.0, 300.0), (201.0, 199.0), none).0, json!({"dx": -99.0, "dy": -101.0}));
}
/// #2120: the end of a slanted straight line, dragged roughly along it, stays on its direction
/// (Line Extension) and the guide runs along it from the other end.
#[test]
fn dragged_end_of_a_slanted_line_stays_on_its_direction() {
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let id = d.alloc_id();
let line = vectorcraft_geom::shapes::line(Point::new(300.0, 400.0), Point::new(380.0, 340.0));
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, line, vectorcraft_doc::Appearance::default_art())).unwrap();
let p = paint();
let end = anchors_of(id, &[(0, 1)]);
let c = cx(&d, &end, &p);
let mut t = DirectSelectionTool::new(false);
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 380.0, 340.0));
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 421.5, 312.0));
let Some(Action::Preview(_, v)) = a.last() else { panic!("{a:?}") };
let (dx, dy) = (v["dx"].as_f64().unwrap(), v["dy"].as_f64().unwrap());
assert!((dx - 40.0).abs() < 1e-9 && (dy + 30.0).abs() < 1e-9, "{v}");
let guides: Vec<(Point, Point)> =
t.overlays(&c).into_iter().filter_map(|o| if let Overlay::Line { a, b, .. } = o { Some((a, b)) } else { None }).collect();
assert!(matches!(guides[..], [(a, b)] if a == Point::new(300.0, 400.0) && (b - Point::new(420.0, 310.0)).hypot() < 1e-9), "{guides:?}");
}
/// Group Selection (and Direct Selection on a whole path) moves objects snapping as the
/// Selection tool does: B's left side lines up with A's right one.
#[test]
fn moved_objects_snap_with_smart_guides() {
let (d, _, b) = two_squares();
let p = paint();
let mut s = Selection::default();
s.set([b]);
let c = cx(&d, &s, &p);
for group in [true, false] {
let mut t = DirectSelectionTool::new(group);
let (v, _) = drag(&mut t, &c, (350.0, 350.0), (253.0, 351.0), Mods::default());
assert_eq!(v["matrix"], json!([1.0, 0.0, 0.0, 1.0, -100.0, 1.0]), "group: {group}");
}
}
/// #1816: Group Selection and Direct Selection move a whole path with Snap to Grid as the
/// Selection tool does, landing an edge on the grid; the center does not snap. A 45 pt square at
/// 90–135 on the 9 pt grid, dragged right by 22 to 112–157, lands its left edge on the gridline
/// 108 and moves by 18. Its center, at 134.5 after the drag, is nearer the gridline 135 and
/// does not snap.
#[test]
fn a_move_of_objects_lands_an_edge_on_the_grid() {
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
let l = d.layers[0].id;
let id = d.alloc_id();
let sq = vectorcraft_geom::shapes::rectangle(Rect::new(90.0, 90.0, 135.0, 135.0));
d.insert(Some(l), 0, vectorcraft_doc::Node::path(id, sq, vectorcraft_doc::Appearance::default_art())).unwrap();
let p = paint();
let mut s = Selection::default();
s.set([id]);
let c = ToolContext { snap_to_grid: true, ..cx(&d, &s, &p) };
for group in [true, false] {
let mut t = DirectSelectionTool::new(group);
let (v, _) = drag(&mut t, &c, (110.0, 110.0), (132.0, 110.0), Mods::default());
assert_eq!(v["matrix"], json!([1.0, 0.0, 0.0, 1.0, 18.0, 0.0]), "group: {group}");
}
}
/// Group Selection and Direct Selection on a whole path keep a Shift-constrained move on its
/// line when it snaps, as the Selection tool does: level by 13.04 with Snap to Grid on, B's
/// right side (413.04) lands on the gridline 414, and its top stays at 300, between 297 and
/// 306.
#[test]
fn a_shift_move_of_objects_snaps_along_its_line() {
let (d, _, b) = two_squares();
let p = paint();
let mut s = Selection::default();
s.set([b]);
let c = ToolContext { snap_to_grid: true, ..cx(&d, &s, &p) };
let shift = Mods { shift: true, ..Mods::default() };
for group in [true, false] {
let mut t = DirectSelectionTool::new(group);
let (v, _) = drag(&mut t, &c, (350.0, 350.0), (363.0, 351.0), shift);
let (x, y) = (v["matrix"][4].as_f64().unwrap(), v["matrix"][5].as_f64().unwrap());
assert!((x - 14.0).abs() < 1e-9 && y.abs() < 1e-9, "group: {group}: {v}");
}
}
/// Direct Selection's double-click on type where a layer's clipping mask edge runs through it
/// edits the type, though a press there takes the mask's segment.
#[test]
fn a_double_click_on_type_at_a_layer_masks_edge_edits_it() {
let (d, _, at) = doc_with_type_on_a_layer_masks_edge();
let (s, p) = (Selection::default(), paint());
let acts = DirectSelectionTool::new(false).pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::DoubleClick, at.x, at.y));
assert_eq!(acts, vec![Action::SwitchTool("type".into())]);
}
#[test]
fn double_clicking_type_switches_to_type_tool() {
use std::sync::Arc;
let (mut d, id) = doc_with_area_type();
let (s, p) = (Selection::default(), paint());
let c = cx(&d, &s, &p);
let b = d.node(id).unwrap().geometric_bounds().unwrap();
let mid = b.center();
// Direct Selection (shortcut A, group == false) switches to the Type tool on double click.
let mut t = DirectSelectionTool::new(false);
let acts = t.pointer(&c, &PointerEvent::new(PointerKind::DoubleClick, mid.x, mid.y));
assert_eq!(acts, vec![Action::SwitchTool("type".into())]);
// Group Selection (group == true) does not switch to Type tool.
let mut gt = DirectSelectionTool::new(true);
let acts_group = gt.pointer(&c, &PointerEvent::new(PointerKind::DoubleClick, mid.x, mid.y));
assert!(acts_group.is_empty(), "group selection shouldn't switch: {acts_group:?}");
// Text inside a group also switches to Type tool with Direct Selection.
let gid = d.alloc_id();
let text_id = d.alloc_id();
let text_node = Node::new(
text_id,
vectorcraft_doc::NodeKind::Text(Box::new(vectorcraft_doc::TextObject::point(Point::new(50.0, 50.0), "In group", Default::default()))),
);
d.insert(Some(d.layers[0].id), 2, Node::group(gid, vec![Arc::new(text_node)])).unwrap();
let in_group_b = d.node(gid).unwrap().geometric_bounds().unwrap();
let c2 = cx(&d, &s, &p);
let acts_group_text = t.pointer(&c2, &PointerEvent::new(PointerKind::DoubleClick, in_group_b.center().x, in_group_b.center().y));
assert_eq!(acts_group_text, vec![Action::SwitchTool("type".into())]);
}
#[test]
fn direct_selection_click_and_drag_member_of_compound_path() {
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
let l = d.layers[0].id;
let (outer, hole, compound) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
let rect = |id, a: f64, b: f64| {
std::sync::Arc::new(vectorcraft_doc::Node::path(id, vectorcraft_geom::shapes::rectangle(Rect::new(a, a, b, b)), Default::default()))
};
let mut c = vectorcraft_doc::Node::new(
compound,
vectorcraft_doc::NodeKind::Compound {
children: vec![rect(outer, 0.0, 200.0), rect(hole, 50.0, 150.0)],
rule: vectorcraft_geom::FillRule::EvenOdd,
},
);
c.appearance = vectorcraft_doc::Appearance::default_art().into();
d.insert(Some(l), 0, c).unwrap();
let p = paint();
let none = Selection::default();
let mut t = DirectSelectionTool::new(false);
let ctx = cx(&d, &none, &p);
let down = t.pointer(&ctx, &PointerEvent::new(PointerKind::Down, 25.0, 100.0));
assert_eq!(
down,
vec![Action::Exec("select.set".into(), json!({"ids": [outer.0]}))],
"Direct Selection selects child member, not compound parent"
);
let mut s = Selection::default();
s.set([outer]);
let ctx_sel = cx(&d, &s, &p);
let drag = t.pointer(&ctx_sel, &PointerEvent::new(PointerKind::Drag, 35.0, 110.0));
assert!(drag.iter().any(|a| matches!(a, Action::Preview(cmd, _) if cmd == "object.transform")), "drags the selected child member: {drag:?}");
let up = t.pointer(&ctx_sel, &PointerEvent::new(PointerKind::Up, 35.0, 110.0));
assert_eq!(up, vec![Action::Commit]);
}
#[test]
fn shift_clicking_anchor_after_segment_pick_toggles_or_adds() {
let (d, id) = doc_with_rect();
let p = paint();
let mut s = Selection::default();
s.set([id]);
s.segments.insert(id, [(0, 0)].into());
s.anchors.insert(id, [(0, 0), (0, 1)].into());
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
let shift = Mods { shift: true, ..Default::default() };
// Shift-click on anchor (0, 0) (top-left at 100, 100) toggles it off
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0).with_mods(shift));
assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "toggle"}))]);
// Shift-click on anchor (0, 2) (bottom-right at 200, 200) toggles it on (adds it)
let a2 = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 200.0, 200.0).with_mods(shift));
assert_eq!(a2, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 2]], "mode": "toggle"}))]);
}
#[test]
fn shift_clicking_segment_toggles_segment() {
let (d, id) = doc_with_rect();
let p = paint();
let s = Selection::default();
let cx = cx(&d, &s, &p);
let mut t = DirectSelectionTool::new(false);
let shift = Mods { shift: true, ..Default::default() };
// The rectangle is at 100..200. Segment 0 is the top edge (y=100, x between 100 and 200).
// Shift-click at (150, 100) selects segment 0 with mode "toggle".
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 100.0).with_mods(shift));
assert_eq!(a, vec![Action::Exec("select.segments".into(), json!({"id": id.0, "segments": [[0, 0]], "mode": "toggle"}))]);
}
/// In normal viewmode, clicking the fill of an open path in front selects the open path's shape
/// and anchors, and NEVER clicks through to select or grab a path behind it (even if the path
/// behind has an anchor directly under the click).
#[test]
fn clicking_fill_of_open_path_in_front_never_selects_path_behind() {
use vectorcraft_geom::{Anchor, SubPath};
let (mut d, _behind) = doc_with_rect();
// `behind` has an anchor at (100.0, 100.0).
// Create an open path in front (id `front`), with a fill, covering (100.0, 100.0).
let front = d.alloc_id();
let l = d.layers[0].id;
// Triangle from (50, 50) to (150, 50) to (150, 150), open (implicit closing edge from (150, 150) to (50, 50)).
// (100.0, 100.0) is inside this filled triangle, but `front` has NO anchor at (100.0, 100.0).
let anchors = vec![Anchor::corner(Point::new(50.0, 50.0)), Anchor::corner(Point::new(150.0, 50.0)), Anchor::corner(Point::new(150.0, 150.0))];
let open_path = PathData::single(SubPath::new(anchors, false));
let ap = vectorcraft_doc::Appearance::default_art(); // has solid white fill
d.insert(Some(l), 1, vectorcraft_doc::Node::path(front, open_path, ap)).unwrap();
let (s, p) = (Selection::default(), paint());
let c = cx(&d, &s, &p);
// Hovering over (100.0, 100.0) does NOT hover the hidden anchor of `behind`.
assert_eq!(hovered_anchor(&c, Point::new(100.0, 100.0)), None);
// Clicking at (100.0, 100.0) selects `front` (the open path in front), NOT `behind`.
let mut t = DirectSelectionTool::new(false);
let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
assert_eq!(acts, vec![Action::Exec("select.set".into(), json!({"ids": [front.0]}))]);
}
/// A selected path's Bézier direction handle positioned over a background filled rectangle
/// can be hit and dragged without being occluded by the background fill.
#[test]
fn selected_path_handle_over_background_rect_is_draggable() {
use vectorcraft_geom::{Anchor, SubPath};
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
let l = d.layers[0].id;
// Background rectangle covering 0..300 with default white fill
let bg = d.alloc_id();
let bg_rect = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 300.0, 300.0));
d.insert(Some(l), 0, vectorcraft_doc::Node::path(bg, bg_rect, vectorcraft_doc::Appearance::default_art())).unwrap();
// Selected path with a Bézier curve whose out-handle is at (150, 150)
let fg = d.alloc_id();
let mut a0 = Anchor::corner(Point::new(100.0, 150.0));
a0.h_out = Point::new(150.0, 150.0); // handle over the background rect
let a1 = Anchor::corner(Point::new(200.0, 150.0));
let curve = PathData::single(SubPath::new(vec![a0, a1], false));
d.insert(Some(l), 1, vectorcraft_doc::Node::path(fg, curve, vectorcraft_doc::Appearance::default_art())).unwrap();
let mut s = Selection::default();
s.set([fg]);
s.anchors.insert(fg, [(0, 0)].into());
let p = paint();
let c = cx(&d, &s, &p);
// hit_handle on the handle at (150, 150) must find the handle of `fg`
assert_eq!(hit_handle(&c, Point::new(150.0, 150.0), c.point_tol()), Some((fg, 0, 0, true)));
// Direct Selection tool clicking on the handle initiates Reshape
let mut t = DirectSelectionTool::new(false);
let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 150.0));
assert_eq!(acts, vec![Action::Begin("Reshape".into())]);
}