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

310 lines
16 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;
#[test]
fn rect_drag() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = ShapeTool::new("rectangle");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 50.0, 30.0));
assert_eq!(a[0], Action::Begin("Rectangle".into()));
assert_eq!(a[1], Action::Preview("shape.rectangle".into(), json!({"x": 10.0, "y": 10.0, "width": 40.0, "height": 20.0})));
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 50.0, 30.0)), vec![Action::Commit]);
}
#[test]
fn space_moves_the_shape_being_drawn() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let space = Mods { space: true, ..Default::default() };
let mut t = ShapeTool::new("rectangle");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 120.0));
// Space held: the 100 × 70 rectangle follows the pointer.
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 170.0).with_mods(space));
assert_eq!(a.last(), Some(&Action::Preview("shape.rectangle".into(), json!({"x": 150.0, "y": 100.0, "width": 100.0, "height": 70.0}))));
// Let go of Space: it grows again, from where it was moved to.
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 270.0, 190.0));
assert_eq!(a.last(), Some(&Action::Preview("shape.rectangle".into(), json!({"x": 150.0, "y": 100.0, "width": 120.0, "height": 90.0}))));
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 270.0, 190.0)), vec![Action::Commit]);
// A star (drawn from its centre) moves its centre.
let mut t = ShapeTool::new("star");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 80.0, 50.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 60.0).with_mods(space));
assert!(matches!(a.last(), Some(Action::Preview(_, v)) if v["cx"] == 60.0 && v["cy"] == 60.0), "{a:?}");
}
#[test]
fn star_drag_measure_label_shows_outer_and_inner_radii() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = ToolContext { measurement_labels: true, smart_guides: false, ..cx(&d, &s, &p) };
let mut t = ShapeTool::new("star");
t.star_ratio = 0.5;
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 50.0, 10.0));
let want = cx.star_radii_label(40.0, 20.0);
let label = t
.overlays(&cx)
.into_iter()
.find_map(|o| match o {
Overlay::Measure { text, .. } => Some(text),
_ => None,
})
.unwrap_or_default();
assert_eq!(label, want, "{label}");
}
/// The star label shows the radii the preview draws, also with Ctrl/Cmd + Alt and when a
/// Ctrl/Cmd drag shrinks the outer radius below the held inner one (the ratio is clamped).
#[test]
fn star_measure_label_matches_the_preview_with_modifiers_and_shrink() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = ToolContext { measurement_labels: true, smart_guides: false, ..cx(&d, &s, &p) };
let label = |t: &ShapeTool| {
t.overlays(&cx)
.into_iter()
.find_map(|o| match o {
Overlay::Measure { text, .. } => Some(text),
_ => None,
})
.unwrap_or_default()
};
let radii = |a: &[Action]| match a.last() {
Some(Action::Preview(_, v)) => (v["radius1"].as_f64().unwrap(), v["radius2"].as_f64().unwrap()),
other => panic!("{other:?}"),
};
let cmd = Mods { cmd: true, ..Default::default() };
let cmd_alt = Mods { cmd: true, alt: true, ..Default::default() };
let mut t = ShapeTool::new("star");
t.star_ratio = 0.5;
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 50.0, 10.0));
// Ctrl/Cmd holds the 20 inner radius while the outer one grows.
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 70.0, 10.0).with_mods(cmd));
let (r1, r2) = radii(&a);
assert_eq!(label(&t), cx.star_radii_label(r1, r2));
// Ctrl/Cmd + Alt: the preview straightens the shoulders, and so does the label.
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 70.0, 10.0).with_mods(cmd_alt));
let (r1, r2) = radii(&a);
assert!((r2 - 20.0).abs() > 1e-6, "Alt should override the held radius: {r2}");
assert_eq!(label(&t), cx.star_radii_label(r1, r2));
// Shrinking below the held inner radius clamps the ratio; the label follows the preview.
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 20.0, 10.0).with_mods(cmd));
let (r1, r2) = radii(&a);
assert!(r2 <= r1 + 1e-9, "{r1} {r2}");
assert_eq!(label(&t), cx.star_radii_label(r1, r2));
}
/// During a star drag, Ctrl/Cmd keeps the inner radius while the outer one follows the pointer
/// (the new ratio stays once the key is let go), and Alt keeps the shoulders straight (#812).
#[test]
fn a_star_drag_holds_its_inner_radius_with_ctrl_and_straightens_with_alt() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let radii = |a: &[Action]| match a.last() {
Some(Action::Preview(_, v)) => (v["radius1"].as_f64().unwrap(), v["radius2"].as_f64().unwrap()),
other => panic!("{other:?}"),
};
let ctrl = Mods { cmd: true, ..Default::default() };
let mut t = ShapeTool::new("star");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
let (r1, r2) = radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 50.0)));
assert_eq!((r1, r2), (40.0, 20.0));
// Ctrl: the inner radius stays 20 as the outer one grows to 80.
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 50.0).with_mods(ctrl));
let (r1, r2) = radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 130.0, 50.0).with_mods(ctrl)));
assert!((r1 - 80.0).abs() < 1e-9 && (r2 - 20.0).abs() < 1e-9, "{r1} {r2}");
// Let go: the ratio (a quarter) stays.
let (r1, r2) = radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 50.0)));
assert!((r2 / r1 - 0.25).abs() < 1e-9, "{r1} {r2}");
// Alt: straight shoulders, about 0.382 for five points.
let alt = Mods { alt: true, ..Default::default() };
let (r1, r2) = radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 50.0).with_mods(alt)));
assert!((r2 / r1 - 0.381_966).abs() < 1e-5, "{}", r2 / r1);
assert_eq!(straight_ratio(4), None, "four points can't line up");
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 50.0)), vec![Action::Commit]);
}
#[test]
fn shift_square_alt_center() {
let t = ShapeTool::new("ellipse");
let (c, v) = t.command(Point::new(0.0, 0.0), Point::new(10.0, 4.0), Mods { shift: true, alt: true, ..Default::default() });
assert_eq!(c, "shape.ellipse");
assert_eq!(v, json!({"x": -10.0, "y": -10.0, "width": 20.0, "height": 20.0}));
}
/// General › Constrain Angle: a Shift-drag squares along the turned axes, and the shape turns
/// by the angle about its centre.
#[test]
fn constrain_angle_squares_along_the_turned_axes() {
let t = ShapeTool::new("rectangle");
let shift = Mods { shift: true, ..Default::default() };
// 45°: the drag straight right is a diagonal of the turned axes, so a square on it.
let (_, v) = t.command_at(Point::new(0.0, 0.0), Point::new(20.0, 0.0), shift, 45.0);
let side = 20.0 / 2f64.sqrt();
let near = |k: &str, want: f64| (v[k].as_f64().unwrap() - want).abs() < 1e-9;
assert!(near("width", side) && near("height", side) && near("rotation", -45.0), "{v}");
assert!(near("x", 10.0 - side / 2.0) && near("y", -side / 2.0), "centred on the drag's middle: {v}");
// Whole turns are the page's axes exactly: the same box as 0°, unturned.
let (a, b) = (Point::new(3.0, 4.0), Point::new(41.0, 17.0));
let flat = t.command_at(a, b, Mods::default(), 0.0);
for turn in [360.0, -360.0, 720.0] {
assert_eq!(t.command_at(a, b, Mods::default(), turn), flat, "{turn}°");
}
// A quarter turn is exact (no floating-point noise in the box).
let (_, v) = t.command_at(Point::new(0.0, 0.0), Point::new(20.0, 10.0), Mods::default(), 90.0);
assert_eq!((v["width"].as_f64(), v["height"].as_f64(), v["rotation"].as_f64()), (Some(10.0), Some(20.0), Some(-90.0)), "{v}");
let (_, w) = t.command_at(Point::new(0.0, 0.0), Point::new(20.0, 10.0), Mods::default(), 450.0);
assert_eq!(v, w, "450° is 90°");
}
/// The size label gives the box as drawn: measured along the Constrain Angle's axes.
#[test]
fn constrain_angle_size_label_measures_the_turned_box() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let mut cx = cx(&d, &s, &p);
cx.constrain_angle = 45.0;
let mut t = ShapeTool::new("rectangle");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 300.0, 300.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 340.0, 300.0));
let Some(Action::Preview(_, v)) = a.last() else { panic!("{a:?}") };
let (w, h) = (v["width"].as_f64().unwrap(), v["height"].as_f64().unwrap());
assert!((w - h).abs() < 1e-9 && (w - 40.0 / 2f64.sqrt()).abs() < 1e-9, "{v}");
let o = t.overlays(&cx);
assert!(o.iter().any(|o| matches!(o, Overlay::Measure { text, .. } if text == &cx.size_label(w, h))), "{o:?}");
assert!(!o.iter().any(|o| matches!(o, Overlay::Measure { text, .. } if text == &cx.size_label(40.0, 0.0))), "not the page box");
}
#[test]
fn click_opens_dialog() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = ShapeTool::new("star");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0));
assert_eq!(
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 10.0, 10.0)),
vec![Action::Dialog("star".into(), json!({"x": 10.0, "y": 10.0}))]
);
}
#[test]
fn alt_click_passes_draw_from_centre_to_the_dialog() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let alt = Mods { alt: true, ..Default::default() };
let mut t = ShapeTool::new("rectangle");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0).with_mods(alt));
assert_eq!(
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 10.0, 10.0).with_mods(alt)),
vec![Action::Dialog("rectangle".into(), json!({"x": 10.0, "y": 10.0, "alt": true}))]
);
}
#[test]
fn arrow_keys_change_sides() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = ShapeTool::new("polygon");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 50.0, 10.0));
let a = t.key(&cx, ToolKey::Up, Mods::default());
assert!(matches!(&a[0], Action::Preview(_, v) if v["sides"] == 7));
}
/// The Rounded Rectangle radius stops at General › Corner Radius's range on both of its input
/// paths: a Tool Options value is clamped to it, and ↑ at the top stays there.
#[test]
fn rounded_rectangle_radius_is_clamped_to_its_range() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = ShapeTool::new("roundedRectangle");
t.set_option("cornerRadius", &json!(5000));
assert_eq!(t.options()["cornerRadius"], json!(MAX_CORNER_RADIUS));
t.set_option("cornerRadius", &json!(f64::INFINITY));
assert_eq!(t.options()["cornerRadius"], json!(12.0), "a non-finite value is the default");
t.set_option("cornerRadius", &json!(-3));
assert_eq!(t.options()["cornerRadius"], json!(0.0));
t.set_option("cornerRadius", &json!(MAX_CORNER_RADIUS));
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 50.0, 40.0));
let a = t.key(&cx, ToolKey::Up, Mods::default());
assert!(matches!(&a[0], Action::Preview(_, v) if v["radius"] == json!(MAX_CORNER_RADIUS)), "{a:?}");
assert_eq!(t.options()["cornerRadius"], json!(MAX_CORNER_RADIUS));
}
/// The start and the dragged corner land on another object's anchors (#506), hovering shows
/// it before the press, and the measurement label gives the size drawn.
#[test]
fn corners_snap_to_smart_guides() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = ShapeTool::new("rectangle");
let anchor = |o: &[Overlay], at: Point| o.iter().any(|o| matches!(o, Overlay::Label { text, p, .. } if text == "anchor" && *p == at));
t.pointer(&cx, &PointerEvent::new(PointerKind::Move, 98.0, 102.0));
assert!(anchor(&t.overlays(&cx), Point::new(100.0, 100.0)));
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 98.0, 102.0));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 198.0, 202.0));
assert_eq!(a[1], Action::Preview("shape.rectangle".into(), json!({"x": 100.0, "y": 100.0, "width": 100.0, "height": 100.0})));
let o = t.overlays(&cx);
assert!(anchor(&o, Point::new(200.0, 200.0)), "{o:?}");
assert!(o.iter().any(|o| matches!(o, Overlay::Measure { text, .. } if text == &cx.size_label(100.0, 100.0))));
// Alt draws from the centre: the label gives the whole size.
let alt = Mods { alt: true, ..Mods::default() };
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 198.0, 202.0).with_mods(alt));
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Measure { text, .. } if text == &cx.size_label(200.0, 200.0))));
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 198.0, 202.0));
assert!(t.overlays(&cx).is_empty());
// A line with Shift keeps to 45° steps, sliding into line with the rect's right side.
let mut t = ShapeTool::new("lineSegment");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 300.0, 300.0));
let shift = Mods { shift: true, ..Mods::default() };
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 203.0, 302.0).with_mods(shift));
assert!(matches!(&a[..], [_, Action::Preview(_, v)] if v["x2"] == 200.0 && (v["y2"].as_f64().unwrap() - 300.0).abs() < 1e-9), "{a:?}");
let from = Point::new(300.0, 300.0);
let to = Point::new(200.0, 300.0);
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Measure { text, .. } if text == &cx.segment_measure_label(from, to))));
}
/// Line Segment Tool Options: Length and Angle are options (for a click's dialog), and Fill
/// Line makes the drag's line take the current fill.
#[test]
fn line_segment_options_and_fill_line() {
let mut t = ShapeTool::new("lineSegment");
let (a, b) = (Point::new(0.0, 0.0), Point::new(30.0, 40.0));
assert!(t.command(a, b, Mods::default()).1.get("fill").is_none());
for (k, v) in [("length", json!(-80.0)), ("angle", json!(-90.0)), ("fillLine", json!(true))] {
t.set_option(k, &v);
}
t.set_option("length", &json!(f64::NAN));
assert_eq!(
(t.options()["length"].clone(), t.options()["angle"].clone(), t.options()["fillLine"].clone()),
(json!(80.0), json!(270.0), json!(true))
);
assert_eq!(t.command(a, b, Mods::default()).1["fill"], json!(true));
}