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

508 lines
26 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 crate::{Mods, PointerKind};
use vectorcraft_doc::{Appearance, Node};
/// A live 100 × 100 rectangle at (100, 100) with corner radius `r`.
fn live_rect(r: f64, xf: Affine) -> (Document, NodeId) {
live_shape(100.0, 100.0, r, xf)
}
/// A live `w` × `h` rectangle with corner radius `r`.
fn live_shape(w: f64, h: f64, r: f64, xf: Affine) -> (Document, NodeId) {
let live = LiveShape::Rectangle { w, h, radii: [r; 4], kinds: Default::default(), radii_relative: false, xf };
doc_with(live.to_path(), Some(live))
}
/// A document holding one path with its live shape.
fn doc_with(path: PathData, live: Option<LiveShape>) -> (Document, NodeId) {
let mut d = Document::new(500.0, 500.0);
let l = d.layers[0].id;
let id = d.alloc_id();
let mut n = Node::path(id, path, Appearance::default_art());
if let NodeKind::Path { live: slot, .. } = &mut n.kind {
*slot = live.map(Box::new);
}
d.insert(Some(l), 0, n).unwrap();
(d, id)
}
/// A five-pointed star at (200, 200) of radii 60 and 30 (#511): a plain path.
fn star() -> (Document, NodeId) {
doc_with(vectorcraft_geom::shapes::star(Point::new(200.0, 200.0), 60.0, 30.0, 5, 0.0), None)
}
fn selected(id: NodeId) -> Selection {
let mut s = Selection::default();
s.add(id);
s
}
fn pts(w: &CornerWidgets) -> Vec<Point> {
w.visible().collect()
}
fn radius(a: &Action) -> f64 {
let Action::Preview(c, v) = a else { panic!("not a preview: {a:?}") };
assert_eq!(c, "object.setLiveShape");
v["radius"].as_f64().unwrap()
}
#[test]
fn widgets_sit_inside_each_corner() {
let (d, id) = live_rect(0.0, Affine::translate((100.0, 100.0)));
let w = CornerWidgets::of(&d, &selected(id), 1.0, false).unwrap();
assert_eq!(pts(&w), [Point::new(110.0, 110.0), Point::new(190.0, 110.0), Point::new(190.0, 190.0), Point::new(110.0, 190.0)]);
// A larger radius moves them to the arc centres; zooming in keeps a 10 px minimum.
let (d, id) = live_rect(20.0, Affine::translate((100.0, 100.0)));
assert_eq!(pts(&CornerWidgets::of(&d, &selected(id), 1.0, false).unwrap())[0], Point::new(120.0, 120.0));
assert_eq!(pts(&CornerWidgets::of(&d, &selected(id), 4.0, false).unwrap())[2], Point::new(180.0, 180.0));
// Hidden when the shape is tiny on screen and without a single selection; a plain path
// shows them with Direct Selection only.
assert!(CornerWidgets::of(&d, &selected(id), 0.2, true).is_none());
let (d, id) = doc_with_rect();
assert!(CornerWidgets::of(&d, &selected(id), 1.0, false).is_none());
assert_eq!(pts(&CornerWidgets::of(&d, &selected(id), 1.0, true).unwrap())[0], Point::new(110.0, 110.0));
assert!(CornerWidgets::of(&d, &Selection::default(), 1.0, true).is_none());
}
#[test]
fn dragging_a_widget_inward_rounds_all_corners_in_one_step() {
let (d, id) = live_rect(0.0, Affine::translate((100.0, 100.0)));
let s = selected(id);
let p = paint();
let cx = cx(&d, &s, &p);
for mut t in [crate::create("selection"), crate::create("directSelection")] {
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 190.0, 191.0)).is_empty());
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 175.0, 176.0));
assert_eq!(a[0], Action::Begin("Corner Radius".into()));
assert!((radius(&a[1]) - 15.0).abs() < 1e-9);
assert!(matches!(&a[1], Action::Preview(_, v) if v["id"] == id.0));
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Measure { text, .. } if text == "Radius: 15.00 pt")));
let quiet = ToolContext { measurement_labels: false, ..crate::testutil::cx(&d, &s, &p) };
assert!(!t.overlays(&quiet).iter().any(|o| matches!(o, Overlay::Measure { .. })));
// Past the middle the corner is fully round; back outside it is square.
assert_eq!(radius(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 100.0, 100.0))[0]), 50.0);
assert_eq!(radius(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 260.0, 260.0))[0]), 0.0);
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 260.0, 260.0)), vec![Action::Commit]);
}
}
/// #938: with two rectangles selected, both show their widgets, and a drag on either rounds
/// the corners of both in one step; with Direct Selection, each its shown corners.
#[test]
fn widgets_of_several_selected_paths_drag_together() {
let (mut d, a) = live_rect(0.0, Affine::translate((100.0, 100.0)));
let live = LiveShape::Rectangle {
w: 100.0,
h: 100.0,
radii: [0.0; 4],
kinds: Default::default(),
radii_relative: false,
xf: Affine::translate((300.0, 100.0)),
};
let b = d.alloc_id();
let mut n = Node::path(b, live.to_path(), Appearance::default_art());
if let NodeKind::Path { live: slot, .. } = &mut n.kind {
*slot = Some(Box::new(live));
}
let l = d.layers[0].id;
d.insert(Some(l), 1, n).unwrap();
let mut s = selected(a);
s.add(b);
assert_eq!(pts(&CornerWidgets::of(&d, &s, 1.0, false).unwrap()).len(), 8);
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = crate::create("selection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 390.0, 191.0));
let preview = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 375.0, 176.0));
assert!((radius(&preview[1]) - 15.0).abs() < 1e-9);
let Action::Preview(_, v) = &preview[1] else { panic!() };
assert_eq!(v["items"], json!([{"id": a.0}, {"id": b.0}]));
// Direct Selection with one anchor of the first: its corner, and the whole second.
s.anchors.insert(a, [(0, 2)].into());
let w = CornerWidgets::of(&d, &s, 1.0, true).unwrap();
assert_eq!(w.corners(), [2, 0, 1, 2, 3]);
assert_eq!(w.command("radius", json!(5.0)), json!({"items": [{"id": a.0, "corners": [2]}, {"id": b.0}], "radius": 5.0}));
}
/// #442: on a 200 × 80 rectangle the widgets sit at the centres of the corners as drawn, also
/// with a radius past half the shorter side; a drag past fully round stops every corner there
/// and outlines them in red.
#[test]
fn widgets_of_a_non_square_rectangle_follow_the_drawn_corners() {
let (d, id) = live_shape(200.0, 80.0, 60.0, Affine::translate((100.0, 100.0)));
let s = selected(id);
let w = CornerWidgets::of(&d, &s, 1.0, false).unwrap();
let centres = [Point::new(140.0, 140.0), Point::new(260.0, 140.0), Point::new(260.0, 140.0), Point::new(140.0, 140.0)];
assert_eq!(pts(&w), centres);
let p = paint();
let cx = cx(&d, &s, &p);
let red = |t: &dyn crate::Tool| {
t.overlays(&cx).into_iter().find_map(|o| match o {
Overlay::Path { path, color, .. } if color == crate::builder::HIGHLIGHT_RED => Some(path),
_ => None,
})
};
let mut t = crate::create("selection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 140.0, 140.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 200.0));
assert_eq!(radius(&a[1]), 40.0);
// The four arcs, each a quarter circle of radius 40 starting on a side.
let arcs = red(t.as_ref()).expect("the limit shows");
let starts: Vec<_> = arcs.elements().iter().filter_map(|e| if let vectorcraft_geom::PathEl::MoveTo(p) = e { Some(*p) } else { None }).collect();
assert_eq!(starts, [Point::new(260.0, 100.0), Point::new(300.0, 140.0), Point::new(140.0, 180.0), Point::new(100.0, 140.0)]);
// Back below the limit, the outline goes.
assert_eq!(radius(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 130.0, 130.0))[0]), 30.0);
assert!(red(t.as_ref()).is_none());
}
/// #442: a rectangle saved stretched (an uneven scale in its transform, before #291) shows its
/// widgets and drags its radius in document units, as `object.setLiveShape` sets it.
#[test]
fn a_stretched_rectangle_drags_its_radius_in_document_units() {
// 50 × 80 with 10 pt corners stretched 4 × 1: drawn 200 × 80, a mean radius of 20.
let (d, id) = live_shape(50.0, 80.0, 10.0, Affine::translate((100.0, 100.0)) * Affine::scale_non_uniform(4.0, 1.0));
let s = selected(id);
let w = CornerWidgets::of(&d, &s, 1.0, false).unwrap();
assert_eq!(pts(&w)[0], Point::new(120.0, 120.0));
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = crate::create("directSelection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 120.0, 120.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 110.0, 110.0));
assert!((radius(&a[1]) - 10.0).abs() < 1e-9, "{a:?}");
}
#[test]
fn drag_follows_a_rotated_shape_and_a_click_does_nothing() {
// Rotated 90°: the shape's top-left corner is at document (200, 100).
let xf = Affine::translate((200.0, 100.0)) * Affine::rotate(std::f64::consts::FRAC_PI_2);
let (d, id) = live_rect(10.0, xf);
let s = selected(id);
let p = paint();
let cx = cx(&d, &s, &p);
let w = CornerWidgets::of(&d, &s, 1.0, false).unwrap();
assert!(pts(&w)[0].distance(Point::new(190.0, 110.0)) < 1e-9);
let mut t = crate::create("directSelection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 190.0, 110.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 180.0, 120.0));
assert!((radius(&a[1]) - 20.0).abs() < 1e-9);
// A click without a drag leaves the shape (and the undo history) alone.
let mut t = crate::create("selection");
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 190.0, 110.0)).is_empty());
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 190.0, 110.0)).is_empty());
}
/// Direct Selection: a click on a widget (no drag) selects its corner, as a click on the
/// corner's anchor does, so the next drag on that widget rounds that corner alone; Shift
/// toggles the corner, except while every corner is selected (#5188, see
/// `a_shift_click_on_a_widget_of_a_whole_path_picks_its_corner`). The Selection tool's click
/// still does nothing.
#[test]
fn a_click_on_a_widget_with_direct_selection_selects_its_corner() {
let (d, id) = live_rect(10.0, Affine::translate((100.0, 100.0)));
let s = selected(id);
let p = paint();
let cx = cx(&d, &s, &p);
let w = pts(&CornerWidgets::of(&d, &s, 1.0, true).unwrap())[2];
// The bottom-right corner of a rounded rectangle is drawn with two anchors.
let anchors: Vec<(usize, usize)> = LiveCorners::of(d.node(id).unwrap()).unwrap().anchors_of(&[2].into()).into_iter().collect();
assert_eq!(anchors.len(), 2);
let mut t = crate::create("directSelection");
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, w.x, w.y)).is_empty());
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, w.x, w.y));
assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": anchors, "mode": "set"}))]);
// With that corner selected, its widget alone shows, and a drag rounds it alone
// (`direct_selected_corners_show_their_widgets_alone_and_round_alone`).
let mut part = selected(id);
part.anchors.insert(id, anchors.iter().copied().collect());
assert_eq!(CornerWidgets::of(&d, &part, 1.0, true).unwrap().corners(), [2]);
// A Shift-click on it then deselects it.
let shift = Mods { shift: true, ..Mods::default() };
let one = crate::testutil::cx(&d, &part, &p);
t.pointer(&one, &PointerEvent::new(PointerKind::Down, w.x, w.y).with_mods(shift));
let a = t.pointer(&one, &PointerEvent::new(PointerKind::Up, w.x, w.y).with_mods(shift));
assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": anchors, "mode": "toggle"}))]);
let mut t = crate::create("selection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, w.x, w.y));
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, w.x, w.y)).is_empty());
}
/// #5188: while every corner of a path is selected (the path selected whole), a Shift-click on a
/// widget picks that corner alone, as the first click of "click one widget, then Shift-click the
/// others" does, rather than deselecting it and leaving the others. Once some corners are picked,
/// Shift-clicks toggle.
#[test]
fn a_shift_click_on_a_widget_of_a_whole_path_picks_its_corner() {
let (d, id) = live_rect(10.0, Affine::translate((100.0, 100.0)));
let p = paint();
let lc = LiveCorners::of(d.node(id).unwrap()).unwrap();
let anchors = |k: usize| -> Vec<(usize, usize)> { lc.anchors_of(&[k].into()).into_iter().collect() };
let shift = Mods { shift: true, ..Mods::default() };
let click = |s: &Selection, at: Point| {
let cx = crate::testutil::cx(&d, s, &p);
let mut t = crate::create("directSelection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(shift));
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x, at.y).with_mods(shift))
};
let pick = |k: usize, mode: &str| vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": anchors(k), "mode": mode}))];
// Selected whole: every widget shows; a Shift-click on one picks its corner alone.
let whole = selected(id);
let w = pts(&CornerWidgets::of(&d, &whole, 1.0, true).unwrap());
assert_eq!(w.len(), 4);
assert_eq!(click(&whole, w[2]), pick(2, "set"));
// Every anchor direct-selected is every corner selected too.
let mut every = selected(id);
every.anchors.insert(id, (0..4).flat_map(anchors).collect());
assert_eq!(click(&every, w[1]), pick(1, "set"));
// Two corners picked: a Shift-click on either widget deselects that corner.
let mut two = selected(id);
two.anchors.insert(id, [0, 2].into_iter().flat_map(anchors).collect());
let shown = pts(&CornerWidgets::of(&d, &two, 1.0, true).unwrap());
assert_eq!(shown, [w[0], w[2]]);
assert_eq!(click(&two, w[0]), pick(0, "toggle"));
}
/// #5188: an anchor selected that is no corner (here half way along a side) shows every widget
/// but selects no corner, so a Shift-click on a widget adds its corner to that selection.
#[test]
fn a_shift_click_on_a_widget_adds_to_anchors_that_are_no_corner() {
let corners = [(100.0, 100.0), (150.0, 100.0), (200.0, 100.0), (200.0, 200.0), (100.0, 200.0)].map(|(x, y)| Point::new(x, y));
let (d, id) = doc_with(PathData::single(vectorcraft_geom::SubPath::polyline(&corners, true)), None);
let p = paint();
let mut s = selected(id);
s.anchors.insert(id, [(0, 1)].into());
let cx = cx(&d, &s, &p);
let widgets = CornerWidgets::for_tool(&cx, true).unwrap();
assert_eq!(widgets.corners().len(), 4, "no corner selected: every widget shows");
let at = pts(&widgets)[0];
let lc = LiveCorners::of(d.node(id).unwrap()).unwrap();
let corner = *lc.all().iter().next().unwrap();
let anchors: Vec<(usize, usize)> = lc.anchors_of(&[corner].into()).into_iter().collect();
let shift = Mods { shift: true, ..Mods::default() };
let mut t = crate::create("directSelection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(shift));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x, at.y).with_mods(shift));
assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": anchors, "mode": "toggle"}))]);
}
#[test]
fn direct_selected_corners_show_their_widgets_alone_and_round_alone() {
let (d, id) = live_rect(0.0, Affine::translate((100.0, 100.0)));
// Direct Selection picked the bottom-right anchor.
let mut s = selected(id);
s.anchors.insert(id, [(0, 2)].into());
let p = paint();
let cx = cx(&d, &s, &p);
let w = CornerWidgets::of(&d, &s, 1.0, true).unwrap();
assert_eq!(w.corners(), [2]);
assert_eq!(pts(&w), [Point::new(190.0, 190.0)]);
assert!(!over_widget(&cx, Point::new(110.0, 110.0), true), "the other corners hide theirs");
let mut t = crate::create("directSelection");
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 190.0, 191.0)).is_empty());
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 175.0, 176.0));
assert_eq!(a[0], Action::Begin("Corner Radius".into()));
assert_eq!(a[1], Action::Preview("object.setLiveShape".into(), json!({"id": id.0, "radius": 15.0, "corners": [2]})));
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 175.0, 176.0)), vec![Action::Commit]);
}
/// #2332: one corner rounded alone goes past half the side, until its cut reaches the
/// neighbouring anchors (the whole side of a square), or a rounded neighbour's cut.
#[test]
fn a_corner_rounded_alone_rounds_up_to_its_neighbours() {
for (top_right, max) in [(0.0, 100.0), (30.0, 70.0)] {
let live = LiveShape::Rectangle {
w: 100.0,
h: 100.0,
radii: [0.0, top_right, 0.0, 0.0],
kinds: Default::default(),
radii_relative: false,
xf: Affine::translate((100.0, 100.0)),
};
let (d, id) = doc_with(live.to_path(), Some(live));
// Direct Selection picked the bottom-right anchor.
let mut s = selected(id);
s.anchors.insert(id, [(0, if top_right > 0.0 { 3 } else { 2 })].into());
let p = paint();
let cx = cx(&d, &s, &p);
assert_eq!(CornerWidgets::of(&d, &s, 1.0, true).unwrap().corners(), [2]);
let mut t = crate::create("directSelection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 190.0, 190.0));
assert!((radius(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 130.0, 130.0))[1]) - 60.0).abs() < 1e-9);
assert!(!t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Path { .. })), "60 is below the limit");
assert_eq!(radius(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 0.0, 0.0))[0]), max);
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Path { .. })), "the limit shows");
}
}
#[test]
fn alt_click_cycles_the_kind_and_double_click_opens_the_dialog() {
let (d, id) = live_rect(10.0, Affine::translate((100.0, 100.0)));
let p = paint();
let alt = Mods { alt: true, ..Mods::default() };
let whole = selected(id);
let mut part = selected(id);
part.anchors.insert(id, [(0, 0), (0, 7)].into());
for (s, extra) in [(&whole, json!({})), (&part, json!({"corners": [0]}))] {
let cx = cx(&d, s, &p);
let mut want = json!({"id": id.0, "kind": "invertedRound"});
want.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
for tool in ["selection", "directSelection"] {
let mut t = crate::create(tool);
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 110.0, 110.0).with_mods(alt)).is_empty());
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 110.0, 110.0).with_mods(alt));
assert_eq!(a, vec![Action::Exec("object.setLiveShape".into(), want.clone())], "{tool}");
}
}
// A double-click on a widget opens Corners for the shown corners; elsewhere it doesn't.
let cx1 = cx(&d, &part, &p);
let a = crate::create("directSelection").pointer(&cx1, &PointerEvent::new(PointerKind::DoubleClick, 110.0, 110.0));
assert_eq!(a, vec![Action::Dialog(DIALOG.into(), json!({"id": id.0, "corners": [0]}))]);
let cx2 = cx(&d, &whole, &p);
let a = crate::create("selection").pointer(&cx2, &PointerEvent::new(PointerKind::DoubleClick, 190.0, 110.0));
assert_eq!(a, vec![Action::Dialog(DIALOG.into(), json!({"id": id.0, "corners": [0, 1, 2, 3]}))]);
// Off the widgets, the Selection tool's double-click isolates the shape (Double Click To
// Isolate) instead.
let a = crate::create("selection").pointer(&cx2, &PointerEvent::new(PointerKind::DoubleClick, 150.0, 150.0));
assert_eq!(a, vec![Action::Exec("object.isolate".into(), json!({"id": id.0}))]);
}
/// Hide Corner Widget for angles greater than (#394): a rectangle's right angles hide below
/// 90°; sheared, only its acute corners keep their widgets.
#[test]
fn corners_wider_than_the_preference_hide_their_widgets() {
let (d, id) = live_rect(0.0, Affine::translate((100.0, 100.0)));
let w = CornerWidgets::of(&d, &selected(id), 1.0, false).unwrap();
assert_eq!(w.clone().within_angle(177.0).map(|w| w.corners()), Some(vec![0, 1, 2, 3]));
assert_eq!(w.clone().within_angle(90.0).map(|w| w.corners()), Some(vec![0, 1, 2, 3]));
assert!(w.within_angle(89.0).is_none());
// Sheared by 30°: 60° at the top-left and bottom-right corners, 120° at the others.
let shear = Affine::translate((100.0, 100.0)) * Affine::new([1.0, 0.0, 30f64.to_radians().tan(), 1.0, 0.0, 0.0]);
let (d, id) = live_rect(0.0, shear);
let w = CornerWidgets::of(&d, &selected(id), 1.0, false).unwrap();
assert_eq!(w.clone().within_angle(100.0).map(|w| w.corners()), Some(vec![0, 2]));
let s = selected(id);
let p = paint();
let c = ToolContext { corner_widget_max_angle: 100.0, ..cx(&d, &s, &p) };
assert!(over_widget(&c, pts(&w)[0], false));
assert!(!over_widget(&c, pts(&w)[1], false), "the 120° corner's widget is hidden");
}
#[test]
fn widgets_off_or_elsewhere_keep_the_tools_behaviour() {
let (d, id) = live_rect(0.0, Affine::translate((100.0, 100.0)));
let s = selected(id);
let p = paint();
let mut c = cx(&d, &s, &p);
assert!(over_widget(&c, Point::new(110.0, 110.0), false));
assert!(!over_widget(&c, Point::new(150.0, 150.0), false));
c.corner_widgets = false;
assert!(!over_widget(&c, Point::new(110.0, 110.0), true));
// With the widgets hidden, a drag from the same spot moves the object.
let mut t = crate::create("selection");
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 110.0, 110.0));
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 130.0, 110.0));
assert_eq!(a[0], Action::Begin("Move".into()));
}
/// #511: Direct Selection shows a widget in each of a star's ten corners (the Selection tool
/// none: a star isn't a live shape); dragging a tip's inward rounds them all as one step.
#[test]
fn a_star_shows_widgets_in_every_corner_with_direct_selection() {
let (d, id) = star();
let s = selected(id);
let p = paint();
let cx = cx(&d, &s, &p);
assert!(CornerWidgets::for_tool(&cx, false).is_none());
let w = CornerWidgets::for_tool(&cx, true).expect("widgets on a plain path");
assert_eq!(w.corners(), (0..10).collect::<Vec<_>>());
// The top tip's widget sits 10 √2 px down its bisector.
let tip = pts(&w)[0];
assert!(tip.distance(Point::new(200.0, 140.0 + 10.0 * 2f64.sqrt())) < 1e-9, "{tip:?}");
let mut t = crate::create("directSelection");
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, tip.x, tip.y)).is_empty());
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, tip.x, tip.y + 10.0));
assert_eq!(a[0], Action::Begin("Corner Radius".into()));
// The widget follows the arc's centre: r / sin(angle / 2) along the bisector.
let c = vectorcraft_geom::corners::path_corners(&star_path(&d, id))[0];
let want = 10.0 * (c.angle_deg() / 2.0).to_radians().sin();
assert!((radius(&a[1]) - want).abs() < 1e-9, "{a:?} want {want}");
assert!(matches!(&a[1], Action::Preview(_, v) if v["id"] == id.0 && v.get("corners").is_none()), "every corner: {a:?}");
// The Selection tool drags the star instead.
let mut t = crate::create("selection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, tip.x, tip.y));
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, tip.x, tip.y + 10.0))[0], Action::Begin("Move".into()));
}
fn star_path(d: &Document, id: NodeId) -> PathData {
let Some(NodeKind::Path { path, .. }) = d.node(id).map(|n| &n.kind) else { panic!("not a path") };
path.clone()
}
/// #511: a Direct-Selected anchor of the star shows its corner's widget alone, and the
/// preference hides the star's wide inner corners (about 125°) below their angle.
#[test]
fn star_corners_follow_the_anchors_and_the_angle_preference() {
let (d, id) = star();
let mut s = selected(id);
s.anchors.insert(id, [(0, 3)].into());
let p = paint();
let c = cx(&d, &s, &p);
let w = CornerWidgets::for_tool(&c, true).unwrap();
assert_eq!(w.corners(), [3]);
let mut t = crate::create("directSelection");
let at = pts(&w)[0];
t.pointer(&c, &PointerEvent::new(PointerKind::Down, at.x, at.y));
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, at.x + 20.0, at.y + 20.0));
assert!(matches!(&a[1], Action::Preview(_, v) if v["corners"] == json!([3])), "{a:?}");
// Corners wider than 90° hide: the five tips keep theirs.
let whole = selected(id);
let c = ToolContext { corner_widget_max_angle: 90.0, ..cx(&d, &whole, &p) };
let w = CornerWidgets::for_tool(&c, true).unwrap();
assert_eq!(w.corners(), [0, 2, 4, 6, 8]);
assert_eq!(w.command("radius", json!(4.0))["corners"], json!([0, 2, 4, 6, 8]));
}
/// A live polygon shows its widgets to the Selection tool too; past every corner's limit the
/// cuts outline in red.
#[test]
fn a_live_polygon_rounds_with_either_tool() {
let live =
LiveShape::Polygon { radius: 80.0, sides: 6, xf: Affine::translate((200.0, 200.0)), radii: vec![], kinds: vec![], radii_relative: false };
let (d, id) = doc_with(live.to_path(), Some(live));
let s = selected(id);
let p = paint();
let cx = cx(&d, &s, &p);
for any_path in [false, true] {
assert_eq!(CornerWidgets::for_tool(&cx, any_path).unwrap().corners().len(), 6);
}
let tip = pts(&CornerWidgets::for_tool(&cx, false).unwrap())[0];
let mut t = crate::create("selection");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, tip.x, tip.y));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 300.0));
// A hexagon's 120° corners stop where their cuts meet: 40 · tan 60°.
assert!((radius(&a[1]) - 40.0 * 3f64.sqrt()).abs() < 1e-9, "{a:?}");
let red = t
.overlays(&cx)
.into_iter()
.any(|o| matches!(o, Overlay::Path { color, path, .. } if color == crate::builder::HIGHLIGHT_RED && path.elements().len() == 12));
assert!(red, "six arcs at the limit");
}
/// A pen path's corners show widgets, curved ones too (#1144); its ends don't.
#[test]
fn a_pen_path_shows_widgets_on_its_corners_only() {
let mut sp = vectorcraft_geom::SubPath::polyline(
&[Point::new(100.0, 100.0), Point::new(300.0, 100.0), Point::new(300.0, 300.0), Point::new(100.0, 300.0)],
false,
);
sp.anchors[2].h_out = Point::new(250.0, 350.0);
let (d, id) = doc_with(PathData::single(sp), None);
let s = selected(id);
let p = paint();
let w = CornerWidgets::for_tool(&cx(&d, &s, &p), true).unwrap();
assert_eq!(w.corners(), [1, 2]);
assert_eq!(pts(&w)[0], Point::new(290.0, 110.0));
}