Skip to main content

dotloom_geometry/
aabb.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{Affine, Point, Vector};
4
5/// Axis-aligned bounding box. An empty box has `min > max`.
6#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
7pub struct Aabb {
8    /// Minimum corner.
9    pub min: Point,
10    /// Maximum corner.
11    pub max: Point,
12}
13
14impl Default for Aabb {
15    fn default() -> Self {
16        Self::EMPTY
17    }
18}
19
20impl Aabb {
21    /// The empty box (identity for [`Aabb::union`]).
22    pub const EMPTY: Self =
23        Self { min: Point::new(f64::INFINITY, f64::INFINITY), max: Point::new(f64::NEG_INFINITY, f64::NEG_INFINITY) };
24
25    /// Box from two corners in any order.
26    #[must_use]
27    pub fn from_corners(a: Point, b: Point) -> Self {
28        Self { min: Point::new(a.x.min(b.x), a.y.min(b.y)), max: Point::new(a.x.max(b.x), a.y.max(b.y)) }
29    }
30
31    /// Smallest box containing all points (empty for no points).
32    #[must_use]
33    pub fn from_points<I: IntoIterator<Item = Point>>(points: I) -> Self {
34        points.into_iter().fold(Self::EMPTY, Self::include)
35    }
36
37    /// Whether the box contains nothing.
38    #[must_use]
39    pub fn is_empty(self) -> bool {
40        !(self.min.x <= self.max.x && self.min.y <= self.max.y)
41    }
42
43    /// Grow to include `p` (non-finite points are ignored).
44    #[must_use]
45    pub fn include(self, p: Point) -> Self {
46        if !p.is_finite() {
47            return self;
48        }
49        Self {
50            min: Point::new(self.min.x.min(p.x), self.min.y.min(p.y)),
51            max: Point::new(self.max.x.max(p.x), self.max.y.max(p.y)),
52        }
53    }
54
55    /// Union of two boxes.
56    #[must_use]
57    pub fn union(self, o: Self) -> Self {
58        if o.is_empty() {
59            return self;
60        }
61        if self.is_empty() {
62            return o;
63        }
64        Self {
65            min: Point::new(self.min.x.min(o.min.x), self.min.y.min(o.min.y)),
66            max: Point::new(self.max.x.max(o.max.x), self.max.y.max(o.max.y)),
67        }
68    }
69
70    /// Grow by `d` in every direction.
71    #[must_use]
72    pub fn inflate(self, d: f64) -> Self {
73        if self.is_empty() {
74            return self;
75        }
76        Self { min: Point::new(self.min.x - d, self.min.y - d), max: Point::new(self.max.x + d, self.max.y + d) }
77    }
78
79    /// Width (0 for empty).
80    #[must_use]
81    pub fn width(self) -> f64 {
82        if self.is_empty() { 0.0 } else { self.max.x - self.min.x }
83    }
84
85    /// Height (0 for empty).
86    #[must_use]
87    pub fn height(self) -> f64 {
88        if self.is_empty() { 0.0 } else { self.max.y - self.min.y }
89    }
90
91    /// Center point.
92    #[must_use]
93    pub fn center(self) -> Point {
94        self.min.midpoint(self.max)
95    }
96
97    /// Size as a vector.
98    #[must_use]
99    pub fn size(self) -> Vector {
100        Vector::new(self.width(), self.height())
101    }
102
103    /// Point inside or on the boundary.
104    #[must_use]
105    pub fn contains_point(self, p: Point) -> bool {
106        p.x >= self.min.x && p.x <= self.max.x && p.y >= self.min.y && p.y <= self.max.y
107    }
108
109    /// `o` lies completely inside `self`.
110    #[must_use]
111    pub fn contains(self, o: Self) -> bool {
112        !o.is_empty()
113            && !self.is_empty()
114            && o.min.x >= self.min.x
115            && o.max.x <= self.max.x
116            && o.min.y >= self.min.y
117            && o.max.y <= self.max.y
118    }
119
120    /// Boxes overlap (touching counts).
121    #[must_use]
122    pub fn intersects(self, o: Self) -> bool {
123        !self.is_empty()
124            && !o.is_empty()
125            && self.min.x <= o.max.x
126            && o.min.x <= self.max.x
127            && self.min.y <= o.max.y
128            && o.min.y <= self.max.y
129    }
130
131    /// Distance from `p` to the box (0 inside).
132    #[must_use]
133    pub fn distance_to_point(self, p: Point) -> f64 {
134        if self.is_empty() {
135            return f64::INFINITY;
136        }
137        let dx = (self.min.x - p.x).max(0.0).max(p.x - self.max.x);
138        let dy = (self.min.y - p.y).max(0.0).max(p.y - self.max.y);
139        crate::math::hypot(dx, dy)
140    }
141
142    /// The four corners counter-clockwise from `min`.
143    #[must_use]
144    pub fn corners(self) -> [Point; 4] {
145        [self.min, Point::new(self.max.x, self.min.y), self.max, Point::new(self.min.x, self.max.y)]
146    }
147
148    /// Bounding box of the transformed box.
149    #[must_use]
150    pub fn transform(self, t: Affine) -> Self {
151        if self.is_empty() {
152            return self;
153        }
154        Self::from_points(self.corners().map(|c| t.apply(c)))
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn empty_behaviour() {
164        assert!(Aabb::EMPTY.is_empty());
165        let b = Aabb::from_corners(Point::new(1.0, 2.0), Point::new(-1.0, 0.0));
166        assert_eq!(Aabb::EMPTY.union(b), b);
167        assert_eq!(b.union(Aabb::EMPTY), b);
168        assert!(!Aabb::EMPTY.intersects(b));
169        assert_eq!(Aabb::from_points([]), Aabb::EMPTY);
170    }
171
172    #[test]
173    fn distance() {
174        let b = Aabb::from_corners(Point::ORIGIN, Point::new(2.0, 2.0));
175        assert_eq!(b.distance_to_point(Point::new(1.0, 1.0)), 0.0);
176        assert!((b.distance_to_point(Point::new(5.0, 6.0)) - 5.0).abs() < 1e-12);
177    }
178}