use crate::Point;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Orientation {
CounterClockwise,
Collinear,
Clockwise,
}
#[inline]
pub fn signed_area2(a: Point, b: Point, c: Point) -> i32 {
debug_assert!(
a.in_range() && b.in_range() && c.in_range(),
"signed_area2 is only exact within the coordinate cap"
);
let abx = b.x as i32 - a.x as i32;
let aby = b.y as i32 - a.y as i32;
let acx = c.x as i32 - a.x as i32;
let acy = c.y as i32 - a.y as i32;
abx * acy - aby * acx
}
#[inline]
pub fn orient2d(a: Point, b: Point, c: Point) -> Orientation {
match signed_area2(a, b, c) {
d if d > 0 => Orientation::CounterClockwise,
d if d < 0 => Orientation::Clockwise,
_ => Orientation::Collinear,
}
}
pub fn ring_area2(ring: &[Point]) -> i64 {
if ring.len() < 3 {
return 0;
}
let origin = ring[0];
let mut acc: i64 = 0;
for w in ring.windows(2) {
acc += signed_area2(origin, w[0], w[1]) as i64;
}
acc
}
#[inline]
pub fn is_ccw(ring: &[Point]) -> bool {
ring_area2(ring) > 0
}
#[inline]
pub fn segments_properly_cross(a1: Point, a2: Point, b1: Point, b2: Point) -> bool {
let d1 = orient2d(a1, a2, b1);
let d2 = orient2d(a1, a2, b2);
let d3 = orient2d(b1, b2, a1);
let d4 = orient2d(b1, b2, a2);
d1 != d2
&& d3 != d4
&& d1 != Orientation::Collinear
&& d2 != Orientation::Collinear
&& d3 != Orientation::Collinear
&& d4 != Orientation::Collinear
}
#[inline]
pub fn point_on_segment(p: Point, a: Point, b: Point) -> bool {
if orient2d(a, b, p) != Orientation::Collinear {
return false;
}
p.x >= a.x.min(b.x) && p.x <= a.x.max(b.x) && p.y >= a.y.min(b.y) && p.y <= a.y.max(b.y)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn orientation_basics() {
let a = Point::new(0, 0);
let b = Point::new(1, 0);
assert_eq!(
orient2d(a, b, Point::new(0, 1)),
Orientation::CounterClockwise
);
assert_eq!(orient2d(a, b, Point::new(0, -1)), Orientation::Clockwise);
assert_eq!(orient2d(a, b, Point::new(2, 0)), Orientation::Collinear);
}
#[test]
fn orientation_is_antisymmetric_under_swap() {
let a = Point::new(-5, 3);
let b = Point::new(7, -2);
let c = Point::new(1, 9);
assert_eq!(signed_area2(a, b, c), -signed_area2(a, c, b));
assert_eq!(signed_area2(a, b, c), signed_area2(b, c, a));
assert_eq!(signed_area2(a, b, c), signed_area2(c, a, b));
}
#[test]
fn orientation_is_exact_at_the_coordinate_cap() {
let a = Point::new(Point::MIN_COORD, Point::MIN_COORD);
let b = Point::new(Point::MAX_COORD, Point::MIN_COORD);
let c = Point::new(Point::MIN_COORD, Point::MAX_COORD);
assert_eq!(signed_area2(a, b, c), 32_767 * 32_767);
assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
assert!(2i64 * 32_767 * 32_767 <= i32::MAX as i64);
assert!(
2i64 * 65_535 * 65_535 > i32::MAX as i64,
"the uncapped range would not fit, which is why the cap exists"
);
}
#[test]
fn orientation_detects_one_unit_of_non_collinearity_at_full_scale() {
let a = Point::new(Point::MIN_COORD, Point::MIN_COORD);
let b = Point::new(Point::MAX_COORD, Point::MAX_COORD);
assert_eq!(orient2d(a, b, Point::new(0, 0)), Orientation::Collinear);
assert_eq!(
orient2d(a, b, Point::new(0, 1)),
Orientation::CounterClockwise
);
assert_eq!(orient2d(a, b, Point::new(0, -1)), Orientation::Clockwise);
}
#[test]
fn f32_would_report_a_real_turn_as_collinear_even_inside_the_cap() {
let (a, b, c) = (
Point::new(14176, -12146),
Point::new(-9937, 5341),
Point::new(4434, -5081),
);
assert!(
a.in_range() && b.in_range() && c.in_range(),
"the point is that capping the range does not save f32"
);
assert_eq!(signed_area2(a, b, c), 9);
assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
let naive_f32 = {
let (ax, ay) = (a.x as f32, a.y as f32);
let (bx, by) = (b.x as f32, b.y as f32);
let (cx, cy) = (c.x as f32, c.y as f32);
(bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
};
assert_eq!(naive_f32, 0.0, "f32 loses the turn entirely");
}
#[test]
fn beyond_the_cap_i32_would_report_the_wrong_side() {
let (a, b, c) = (
Point::new(21203, -24650),
Point::new(-22519, 1049),
Point::new(26449, 26335),
);
assert!(
!a.in_range() && !b.in_range() && !c.in_range(),
"the whole point is that these are out of range"
);
let exact = |p: Point, q: Point, r: Point| -> i64 {
(q.x as i64 - p.x as i64) * (r.y as i64 - p.y as i64)
- (q.y as i64 - p.y as i64) * (r.x as i64 - p.x as i64)
};
assert_eq!(exact(a, b, c), -2_363_983_124);
let naive_i32 = {
let (ax, ay) = (a.x as i32, a.y as i32);
let (bx, by) = (b.x as i32, b.y as i32);
let (cx, cy) = (c.x as i32, c.y as i32);
(bx - ax)
.wrapping_mul(cy - ay)
.wrapping_sub((by - ay).wrapping_mul(cx - ax))
};
assert_eq!(naive_i32, 1_930_984_172);
assert!(
naive_i32 > 0 && exact(a, b, c) < 0,
"i32 reports the opposite side once the cap is exceeded"
);
}
#[test]
fn ring_area_signs_and_magnitude() {
let square = [
Point::new(0, 0),
Point::new(10, 0),
Point::new(10, 10),
Point::new(0, 10),
];
assert_eq!(ring_area2(&square), 200);
assert!(is_ccw(&square));
let mut rev = square;
rev.reverse();
assert_eq!(ring_area2(&rev), -200);
assert!(!is_ccw(&rev));
}
#[test]
fn ring_area_is_translation_invariant() {
let tri = [Point::new(0, 0), Point::new(30, 0), Point::new(0, 40)];
let shifted: [Point; 3] =
core::array::from_fn(|i| Point::new(tri[i].x - 1000, tri[i].y + 500));
assert_eq!(ring_area2(&tri), ring_area2(&shifted));
assert_eq!(ring_area2(&tri), 1200);
}
#[test]
fn ring_area_of_degenerate_rings_is_zero() {
assert_eq!(ring_area2(&[]), 0);
assert_eq!(ring_area2(&[Point::new(1, 1)]), 0);
assert_eq!(ring_area2(&[Point::new(1, 1), Point::new(2, 2)]), 0);
assert_eq!(
ring_area2(&[Point::new(0, 0), Point::new(5, 0), Point::new(9, 0)]),
0
);
}
#[test]
fn ring_area_does_not_overflow_at_full_extent() {
let big = [
Point::new(Point::MIN_COORD, Point::MIN_COORD),
Point::new(Point::MAX_COORD, Point::MIN_COORD),
Point::new(Point::MAX_COORD, Point::MAX_COORD),
Point::new(Point::MIN_COORD, Point::MAX_COORD),
];
assert_eq!(ring_area2(&big), 2 * 32_767i64 * 32_767i64);
}
#[test]
fn proper_crossing_detection() {
assert!(segments_properly_cross(
Point::new(0, 0),
Point::new(10, 10),
Point::new(0, 10),
Point::new(10, 0),
));
assert!(!segments_properly_cross(
Point::new(0, 0),
Point::new(1, 1),
Point::new(5, 5),
Point::new(6, 6),
));
}
#[test]
fn touching_endpoints_are_not_proper_crossings() {
assert!(!segments_properly_cross(
Point::new(0, 0),
Point::new(5, 0),
Point::new(5, 0),
Point::new(5, 5),
));
assert!(!segments_properly_cross(
Point::new(0, 0),
Point::new(10, 0),
Point::new(5, 0),
Point::new(5, 5),
));
}
#[test]
fn collinear_overlap_is_not_a_proper_crossing() {
assert!(!segments_properly_cross(
Point::new(0, 0),
Point::new(10, 0),
Point::new(5, 0),
Point::new(15, 0),
));
}
#[test]
fn point_on_segment_cases() {
let a = Point::new(0, 0);
let b = Point::new(10, 5);
assert!(point_on_segment(Point::new(4, 2), a, b));
assert!(point_on_segment(a, a, b), "endpoints count as on-segment");
assert!(point_on_segment(b, a, b));
assert!(!point_on_segment(Point::new(20, 10), a, b));
assert!(!point_on_segment(Point::new(-2, -1), a, b));
assert!(!point_on_segment(Point::new(4, 3), a, b));
}
}