use crate::Point;
use super::LineSegment;
#[must_use]
pub fn ccw(p1: Point, p2: Point, p3: Point) -> f64 {
(p3.y - p1.y) * (p2.x - p1.x) - (p2.y - p1.y) * (p3.x - p1.x)
}
#[must_use]
pub fn segments_intersect(first: LineSegment, second: LineSegment) -> bool {
let ccw1 = ccw(first.a, second.a, second.b);
let ccw2 = ccw(first.b, second.a, second.b);
let ccw3 = ccw(first.a, first.b, second.a);
let ccw4 = ccw(first.a, first.b, second.b);
ccw1 * ccw2 < 0.0 && ccw3 * ccw4 < 0.0
}
#[must_use]
pub fn signed_ring_area(ring: &[Point]) -> f64 {
if ring.len() < 3 {
return 0.0;
}
let mut area = 0.0;
for (current, next) in ring
.iter()
.zip(ring.iter().cycle().skip(1))
.take(ring.len())
{
area += current.x * next.y;
area -= next.x * current.y;
}
area / 2.0
}
#[must_use]
pub fn point_in_polygon(point: Point, polygon: &[Point]) -> bool {
if polygon.len() < 3 {
return false;
}
let mut inside = false;
let previous = polygon.iter().cycle().skip(polygon.len() - 1);
for (current, previous) in polygon.iter().zip(previous).take(polygon.len()) {
let crosses = (current.y > point.y) != (previous.y > point.y)
&& point.x
< (previous.x - current.x) * (point.y - current.y) / (previous.y - current.y)
+ current.x;
if crosses {
inside = !inside;
}
}
inside
}
#[must_use]
pub fn point_in_rings(point: Point, rings: &[Vec<Point>]) -> bool {
let mut rings = rings.iter();
let Some(hull) = rings.next() else {
return false;
};
point_in_polygon(point, hull) && !rings.any(|hole| point_in_polygon(point, hole))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::{ccw, point_in_polygon, point_in_rings, segments_intersect, signed_ring_area};
use crate::Point;
use crate::geometry::LineSegment;
fn p(x: f64, y: f64) -> Point {
Point::new(x, y)
}
fn seg(ax: f64, ay: f64, bx: f64, by: f64) -> LineSegment {
LineSegment::new(p(ax, ay), p(bx, by))
}
#[test]
fn ccw_signs_the_turn() {
assert!(ccw(p(0.0, 0.0), p(1.0, 0.0), p(0.0, 1.0)) > 0.0);
assert!(ccw(p(0.0, 0.0), p(1.0, 0.0), p(0.0, -1.0)) < 0.0);
assert!(ccw(p(0.0, 0.0), p(1.0, 0.0), p(2.0, 0.0)).abs() < f64::MIN_POSITIVE);
}
#[test]
fn a_proper_crossing_intersects() {
assert!(segments_intersect(
seg(-1.0, 0.0, 1.0, 0.0),
seg(0.0, -1.0, 0.0, 1.0)
));
}
#[test]
fn a_shared_endpoint_does_not_intersect() {
assert!(!segments_intersect(
seg(0.0, 0.0, 1.0, 0.0),
seg(1.0, 0.0, 1.0, 1.0)
));
assert!(!segments_intersect(
seg(-1.0, 0.0, 1.0, 0.0),
seg(0.0, 0.0, 0.0, 1.0)
));
}
#[test]
fn a_collinear_overlap_does_not_intersect() {
assert!(!segments_intersect(
seg(0.0, 0.0, 2.0, 0.0),
seg(1.0, 0.0, 3.0, 0.0)
));
assert!(!segments_intersect(
seg(0.0, 0.0, 2.0, 0.0),
seg(2.0, 0.0, 4.0, 0.0)
));
}
#[test]
fn parallel_segments_do_not_intersect() {
assert!(!segments_intersect(
seg(0.0, 0.0, 1.0, 0.0),
seg(0.0, 1.0, 1.0, 1.0)
));
}
#[test]
fn ring_area_signs_the_winding() {
let square = [p(0.0, 0.0), p(2.0, 0.0), p(2.0, 2.0), p(0.0, 2.0)];
let area = signed_ring_area(&square);
let mut reversed = square;
reversed.reverse();
assert!((area.abs() - 4.0).abs() < 1e-12);
assert!((signed_ring_area(&reversed) + area).abs() < 1e-12);
}
#[test]
fn ring_area_ignores_a_repeated_closing_point() {
let open = [p(0.0, 0.0), p(2.0, 0.0), p(2.0, 2.0), p(0.0, 2.0)];
let closed = [
p(0.0, 0.0),
p(2.0, 0.0),
p(2.0, 2.0),
p(0.0, 2.0),
p(0.0, 0.0),
];
assert!((signed_ring_area(&open) - signed_ring_area(&closed)).abs() < 1e-12);
}
#[test]
fn a_degenerate_ring_has_no_area() {
assert!(signed_ring_area(&[]).abs() < f64::MIN_POSITIVE);
assert!(signed_ring_area(&[p(0.0, 0.0), p(1.0, 1.0)]).abs() < f64::MIN_POSITIVE);
}
#[test]
fn point_in_polygon_counts_crossings() {
let square = [p(0.0, 0.0), p(2.0, 0.0), p(2.0, 2.0), p(0.0, 2.0)];
assert!(point_in_polygon(p(1.0, 1.0), &square));
assert!(!point_in_polygon(p(3.0, 1.0), &square));
assert!(!point_in_polygon(p(-1.0, 1.0), &square));
assert!(!point_in_polygon(p(1.0, 3.0), &square));
}
#[test]
fn a_degenerate_polygon_contains_nothing() {
assert!(!point_in_polygon(p(0.0, 0.0), &[]));
assert!(!point_in_polygon(p(0.0, 0.0), &[p(0.0, 0.0), p(1.0, 1.0)]));
}
#[test]
fn rings_are_a_hull_minus_its_holes() {
let hull = vec![p(0.0, 0.0), p(10.0, 0.0), p(10.0, 10.0), p(0.0, 10.0)];
let hole = vec![p(4.0, 4.0), p(6.0, 4.0), p(6.0, 6.0), p(4.0, 6.0)];
let rings = vec![hull, hole];
assert!(point_in_rings(p(1.0, 1.0), &rings));
assert!(
!point_in_rings(p(5.0, 5.0), &rings),
"the hole is not inside"
);
assert!(!point_in_rings(p(20.0, 5.0), &rings));
}
#[test]
fn a_ring_set_with_no_hull_contains_nothing() {
assert!(!point_in_rings(p(0.0, 0.0), &[]));
assert!(!point_in_rings(p(0.0, 0.0), &[Vec::new()]));
}
}