use crate::{Point2, Scalar, Vec2};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Aabb2 {
pub min: Point2,
pub max: Point2,
}
impl Aabb2 {
pub const fn empty() -> Self {
Self {
min: Vec2::splat(Scalar::INFINITY),
max: Vec2::splat(Scalar::NEG_INFINITY),
}
}
#[inline]
pub const fn from_point(point: Point2) -> Self {
Self {
min: point,
max: point,
}
}
#[inline]
pub fn extend(&mut self, point: Point2) {
self.min = self.min.min(point);
self.max = self.max.max(point);
}
#[inline]
pub fn union(&mut self, other: &Self) {
self.min = self.min.min(other.min);
self.max = self.max.max(other.max);
}
#[inline]
pub fn is_finite(&self) -> bool {
self.min.is_finite() && self.max.is_finite()
}
#[inline]
pub fn intersects(&self, other: &Self) -> bool {
self.min.x <= other.max.x
&& self.max.x >= other.min.x
&& self.min.y <= other.max.y
&& self.max.y >= other.min.y
}
#[inline]
pub fn contains(&self, point: Point2) -> bool {
point.x >= self.min.x
&& point.x <= self.max.x
&& point.y >= self.min.y
&& point.y <= self.max.y
}
#[inline]
pub fn is_empty(&self) -> bool {
self.min.x > self.max.x
}
pub fn diagonal(&self) -> Vec2 {
if self.is_empty() {
Vec2::ZERO
} else {
self.max - self.min
}
}
#[inline]
pub fn center(&self) -> Point2 {
if self.is_empty() {
Vec2::ZERO
} else {
(self.min + self.max) * 0.5
}
}
pub fn area(&self) -> Scalar {
let span = self.diagonal();
span.x * span.y
}
}
impl Default for Aabb2 {
fn default() -> Self {
Self::empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Triangle2 {
pub a: Point2,
pub b: Point2,
pub c: Point2,
}
impl Triangle2 {
pub const fn new(a: Point2, b: Point2, c: Point2) -> Self {
Self { a, b, c }
}
pub fn signed_area2(&self) -> Scalar {
let ab = self.b - self.a;
let ac = self.c - self.a;
ab.perp_dot(ac)
}
pub fn signed_area(&self) -> Scalar {
self.signed_area2() * 0.5
}
pub fn area(&self) -> Scalar {
self.signed_area().abs()
}
pub fn centroid(&self) -> Point2 {
(self.a + self.b + self.c) / 3.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rectangle2 {
pub origin: Point2,
pub x: Vec2,
pub y: Vec2,
}
impl Rectangle2 {
pub const fn new(origin: Point2, x: Vec2, y: Vec2) -> Self {
Self { origin, x, y }
}
pub fn from_aabb(bounds: &Aabb2) -> Option<Self> {
if bounds.is_empty() {
return None;
}
let span = bounds.diagonal();
Some(Self {
origin: bounds.min,
x: Vec2::new(span.x, 0.0),
y: Vec2::new(0.0, span.y),
})
}
pub fn corners(&self) -> [Point2; 4] {
[
self.origin,
self.origin + self.x,
self.origin + self.x + self.y,
self.origin + self.y,
]
}
pub fn signed_area(&self) -> Scalar {
self.x.perp_dot(self.y)
}
pub fn area(&self) -> Scalar {
self.signed_area().abs()
}
pub fn center(&self) -> Point2 {
self.origin + (self.x + self.y) * 0.5
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Polygon2 {
pub vertices: Vec<Point2>,
}
impl Polygon2 {
pub const fn new(vertices: Vec<Point2>) -> Self {
Self { vertices }
}
pub fn len(&self) -> usize {
self.vertices.len()
}
pub fn is_empty(&self) -> bool {
self.vertices.is_empty()
}
pub fn signed_area2(&self) -> Scalar {
if self.vertices.len() < 3 {
return 0.0;
}
let base = self.vertices[0];
let mut total = 0.0;
for pair in self.vertices[1..].windows(2) {
total += (pair[0] - base).perp_dot(pair[1] - base);
}
total
}
pub fn signed_area(&self) -> Scalar {
self.signed_area2() * 0.5
}
pub fn area(&self) -> Scalar {
self.signed_area().abs()
}
pub fn is_counter_clockwise(&self) -> bool {
self.signed_area2() > 0.0
}
pub fn bounds(&self) -> Aabb2 {
let mut bounds = Aabb2::empty();
for vertex in &self.vertices {
bounds.extend(*vertex);
}
bounds
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_bounds_absorb_the_first_point() {
let mut bounds = Aabb2::default();
assert!(bounds.is_empty());
bounds.extend(Point2::new(3.0, -1.0));
assert_eq!(bounds.min, bounds.max);
assert!(!bounds.is_empty());
assert_eq!(bounds.area(), 0.0);
}
#[test]
fn bounds_intersect_on_touch_and_contain_their_boundary() {
let mut left = Aabb2::from_point(Point2::ZERO);
left.extend(Point2::new(1.0, 1.0));
let mut right = Aabb2::from_point(Point2::new(1.0, 0.0));
right.extend(Point2::new(2.0, 1.0));
assert!(left.intersects(&right), "touching boxes overlap");
assert!(left.contains(Point2::new(1.0, 1.0)), "boundary is inside");
let mut away = Aabb2::from_point(Point2::new(5.0, 5.0));
away.extend(Point2::new(6.0, 6.0));
assert!(!left.intersects(&away));
}
#[test]
fn triangle_signed_area_carries_winding_but_area_does_not() {
let ccw = Triangle2::new(Point2::ZERO, Point2::new(4.0, 0.0), Point2::new(0.0, 2.0));
assert_eq!(ccw.signed_area(), 4.0);
assert!(ccw.signed_area2() > 0.0);
let cw = Triangle2::new(ccw.a, ccw.c, ccw.b);
assert_eq!(cw.signed_area(), -4.0);
assert_eq!(cw.area(), ccw.area());
}
#[test]
fn collinear_triangle_has_zero_area() {
let degenerate = Triangle2::new(Point2::ZERO, Point2::new(1.0, 1.0), Point2::new(3.0, 3.0));
assert_eq!(degenerate.signed_area2(), 0.0);
assert_eq!(degenerate.area(), 0.0);
}
#[test]
fn rectangle_from_bounds_matches_the_box_it_came_from() {
let mut bounds = Aabb2::from_point(Point2::new(1.0, 2.0));
bounds.extend(Point2::new(4.0, 6.0));
let rectangle = Rectangle2::from_aabb(&bounds).expect("non-empty bounds");
assert_eq!(rectangle.area(), bounds.area());
assert_eq!(rectangle.center(), bounds.center());
assert_eq!(rectangle.corners()[2], bounds.max);
}
#[test]
fn rectangle_from_empty_bounds_is_refused_rather_than_zero_sized() {
assert!(Rectangle2::from_aabb(&Aabb2::empty()).is_none());
}
#[test]
fn rotated_rectangle_keeps_its_area() {
let diagonal = Vec2::new(1.0, 1.0);
let rectangle = Rectangle2::new(Point2::ZERO, diagonal, Vec2::new(-1.0, 1.0));
assert_eq!(rectangle.area(), 2.0);
}
#[test]
fn polygon_winding_flips_with_vertex_order() {
let square = Polygon2::new(vec![
Point2::ZERO,
Point2::new(2.0, 0.0),
Point2::new(2.0, 2.0),
Point2::new(0.0, 2.0),
]);
assert_eq!(square.area(), 4.0);
assert!(square.is_counter_clockwise());
let mut reversed = square.vertices.clone();
reversed.reverse();
let reversed = Polygon2::new(reversed);
assert_eq!(reversed.area(), square.area());
assert!(!reversed.is_counter_clockwise());
assert_eq!(reversed.signed_area(), -square.signed_area());
}
#[test]
fn polygon_with_fewer_than_three_vertices_encloses_nothing() {
assert_eq!(Polygon2::new(Vec::new()).signed_area2(), 0.0);
assert_eq!(
Polygon2::new(vec![Point2::ZERO, Point2::new(1.0, 0.0)]).signed_area2(),
0.0
);
assert!(!Polygon2::new(Vec::new()).is_counter_clockwise());
}
#[test]
fn polygon_area_is_translation_invariant_far_from_the_origin() {
let offset = Vec2::splat(6_000_000.0);
let local = Polygon2::new(vec![
Point2::ZERO,
Point2::new(1.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(0.0, 1.0),
]);
let far = Polygon2::new(local.vertices.iter().map(|v| *v + offset).collect());
assert_eq!(far.area(), local.area());
}
#[test]
fn polygon_bounds_cover_every_vertex() {
let polygon = Polygon2::new(vec![
Point2::new(-1.0, 4.0),
Point2::new(3.0, -2.0),
Point2::new(0.0, 0.0),
]);
let bounds = polygon.bounds();
assert_eq!(bounds.min, Point2::new(-1.0, -2.0));
assert_eq!(bounds.max, Point2::new(3.0, 4.0));
for vertex in &polygon.vertices {
assert!(bounds.contains(*vertex));
}
}
}