use pixelcoords_core::geometry::{
Line, Point, Rect, ResizeHandle, Shape, Size, ToolKind, normalize_deg,
};
use proptest::prelude::*;
fn bounds() -> impl Strategy<Value = Size> {
(8i32..400, 8i32..400).prop_map(|(w, h)| Size::new(w, h))
}
fn point_within(bounds: Size) -> impl Strategy<Value = Point> {
(0..bounds.w, 0..bounds.h).prop_map(|(x, y)| Point::new(x, y))
}
fn shape_within(bounds: Size) -> impl Strategy<Value = Shape> {
let w = bounds.w;
let h = bounds.h;
prop_oneof![
(0..w / 2, 0..h / 2, 2..w / 2, 2..h / 2)
.prop_map(|(x, y, rw, rh)| Shape::Rect(Rect::new(x, y, rw, rh))),
(0..w, 0..h, 1..(w.min(h) / 2).max(2)).prop_map(|(cx, cy, r)| Shape::Circle { cx, cy, r }),
(0..w, 0..h, 1..(w / 2).max(2), 1..(h / 2).max(2))
.prop_map(|(cx, cy, rx, ry)| Shape::Ellipse { cx, cy, rx, ry }),
(0..w, 0..h, 0..w, 0..h, 0..w, 0..h).prop_map(|(ax, ay, bx, by, cx, cy)| {
Shape::Triangle {
ax,
ay,
bx,
by,
cx,
cy,
}
}),
]
}
proptest! {
#[test]
fn rotation_is_periodic(deg in -10_000i32..10_000, turns in -20i32..20) {
let normalized = normalize_deg(deg);
prop_assert!((0..360).contains(&normalized), "{normalized}");
prop_assert_eq!(normalized, normalize_deg(deg + turns * 360));
}
#[test]
fn a_bbox_contains_its_triangle(
(ax, ay, bx, by, cx, cy) in (0i32..400, 0i32..400, 0i32..400, 0i32..400, 0i32..400, 0i32..400),
) {
let tri = Shape::Triangle { ax, ay, bx, by, cx, cy };
let bb = tri.bbox();
for (x, y) in [(ax, ay), (bx, by), (cx, cy)] {
prop_assert!(x >= bb.x && x <= bb.x + bb.w, "{x} outside {bb:?}");
prop_assert!(y >= bb.y && y <= bb.y + bb.h, "{y} outside {bb:?}");
}
}
#[test]
fn a_clamped_move_stays_in_bounds(
b in bounds(),
(shape, grab, cursor) in bounds().prop_flat_map(|bb| {
(shape_within(bb), point_within(bb), point_within(bb))
}),
) {
let moved = shape.clamp_move(grab, cursor, Rect::new(0, 0, b.w, b.h));
let bb = moved.bbox();
prop_assume!(bb.w <= b.w && bb.h <= b.h);
prop_assert!(bb.x >= 0 && bb.y >= 0, "{bb:?} left {b:?}");
prop_assert!(bb.x + bb.w <= b.w, "{bb:?} right of {b:?}");
prop_assert!(bb.y + bb.h <= b.h, "{bb:?} below {b:?}");
}
#[test]
fn a_preview_never_leaves_the_capture(
b in bounds(),
tool in prop_oneof![Just(ToolKind::Rect), Just(ToolKind::Circle), Just(ToolKind::Triangle)],
(sx, sy, ex, ey) in (-500i32..900, -500i32..900, -500i32..900, -500i32..900),
) {
let preview = Shape::compute_preview(tool, Point::new(sx, sy), Point::new(ex, ey), Rect::new(0, 0, b.w, b.h), false);
let Some(shape) = preview else { return Ok(()) };
let bb = shape.bbox();
prop_assert!(bb.w > 0 && bb.h > 0, "degenerate {bb:?}");
}
#[test]
fn a_resize_never_collapses_a_shape(
b in bounds(),
deg in 0i32..360,
(left, right, top, bottom) in (any::<bool>(), any::<bool>(), any::<bool>(), any::<bool>()),
keep_aspect in any::<bool>(),
(rx, ry, rw, rh) in (0i32..200, 0i32..200, 2i32..200, 2i32..200),
cursor in (-200i32..600, -200i32..600),
) {
prop_assume!(left || right || top || bottom);
let shape = Shape::Rect(Rect::new(rx, ry, rw, rh));
let handle = ResizeHandle::RectEdges { left, right, top, bottom };
let bounds_rect = Rect::new(0, 0, b.w, b.h);
let resized = shape.resize_to_rotated(
deg,
handle,
Point::new(cursor.0, cursor.1),
bounds_rect,
keep_aspect,
);
let bb = resized.bbox();
prop_assert!(bb.w > 0 && bb.h > 0, "collapsed to {bb:?}");
}
#[test]
fn rotation_does_not_move_a_circle(
deg in -720i32..720,
(cx, cy, r) in (0i32..300, 0i32..300, 1i32..80),
) {
let circle = Shape::Circle { cx, cy, r };
prop_assert!(circle.hit_test_rotated(deg, Point::new(cx, cy)));
prop_assert_eq!(circle.rotated_bbox(deg), circle.bbox());
}
#[test]
fn a_click_point_lands_inside_its_shape(
shape in shape_within(Size::new(400, 400)),
deg in 0i32..360,
) {
if let Shape::Triangle { ax, ay, bx, by, cx, cy } = shape {
let cross = (i64::from(bx) - i64::from(ax)) * (i64::from(cy) - i64::from(ay))
- (i64::from(by) - i64::from(ay)) * (i64::from(cx) - i64::from(ax));
let edge = |x0: i32, y0: i32, x1: i32, y1: i32| {
f64::hypot(f64::from(x1 - x0), f64::from(y1 - y0))
};
let longest = edge(ax, ay, bx, by)
.max(edge(bx, by, cx, cy))
.max(edge(cx, cy, ax, ay));
prop_assume!(cross.abs() as f64 >= 5.0 * longest);
prop_assert!(shape.hit_test(shape.click_point()));
return Ok(());
}
prop_assert!(shape.hit_test_rotated(deg, shape.click_point()));
}
}
proptest! {
#[test]
fn angle_is_antisymmetric_under_endpoint_swap(
ax in -2000i32..2000, ay in -2000i32..2000,
bx in -2000i32..2000, by in -2000i32..2000,
) {
let a = Point::new(ax, ay);
let b = Point::new(bx, by);
prop_assume!(a != b);
let forward = Line::new(a, b).angle_deg();
let back = Line::new(b, a).angle_deg();
prop_assert!((forward - (back + 180.0) % 360.0).abs() < 1e-9);
}
#[test]
fn length_survives_translation(
ax in -2000i32..2000, ay in -2000i32..2000,
bx in -2000i32..2000, by in -2000i32..2000,
dx in -1000i32..1000, dy in -1000i32..1000,
) {
let line = Line::new(Point::new(ax, ay), Point::new(bx, by));
prop_assert!((line.length() - line.translated(dx, dy).length()).abs() < 1e-9);
}
#[test]
fn constraining_snaps_to_a_multiple_of_45_degrees(
ax in -500i32..500, ay in -500i32..500,
bx in -500i32..500, by in -500i32..500,
) {
let line = Line::new(Point::new(ax, ay), Point::new(bx, by));
let snapped = line.constrained();
prop_assume!(snapped.a != snapped.b);
let angle = snapped.angle_deg();
let nearest = (angle / 45.0).round() * 45.0;
prop_assert!(
(angle - nearest).abs() < 1.0 || (angle - nearest).abs() > 359.0,
"{angle} is not a multiple of 45"
);
}
#[test]
fn distance_to_a_segment_is_bounded_by_its_endpoints(
ax in -500i32..500, ay in -500i32..500,
bx in -500i32..500, by in -500i32..500,
px in -500i32..500, py in -500i32..500,
) {
let line = Line::new(Point::new(ax, ay), Point::new(bx, by));
let p = Point::new(px, py);
let to_a = f64::from(px - ax).hypot(f64::from(py - ay));
let to_b = f64::from(px - bx).hypot(f64::from(py - by));
let d = line.distance_to(p);
prop_assert!(d >= -1e-9, "distance is never negative: {d}");
prop_assert!(d <= to_a.min(to_b) + 1e-9, "{d} exceeded {to_a}/{to_b}");
}
}