Skip to main content

use_geometry/
aabb.rs

1use crate::{error::GeometryError, point::Point2};
2
3/// An axis-aligned bounding box represented by inclusive minimum and maximum corners.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct Aabb2 {
6    min: Point2,
7    max: Point2,
8}
9
10impl Aabb2 {
11    /// Creates a validated axis-aligned bounding box from ordered corners.
12    ///
13    /// # Errors
14    ///
15    /// Returns [`GeometryError::NonFiniteComponent`] when either corner
16    /// contains a non-finite coordinate.
17    ///
18    /// Returns [`GeometryError::InvalidBounds`] when `min` is greater than
19    /// `max` on either axis.
20    ///
21    /// # Examples
22    ///
23    /// ```
24    /// use use_geometry::{Aabb2, GeometryError, Point2};
25    ///
26    /// let bounds = Aabb2::try_new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0))?;
27    /// assert_eq!(bounds.center(), Point2::new(2.5, 4.0));
28    /// # Ok::<(), GeometryError>(())
29    /// ```
30    pub fn try_new(min: Point2, max: Point2) -> Result<Self, GeometryError> {
31        let min = min.validate()?;
32        let max = max.validate()?;
33
34        if min.x() > max.x() || min.y() > max.y() {
35            return Err(GeometryError::InvalidBounds {
36                min_x: min.x(),
37                min_y: min.y(),
38                max_x: max.x(),
39                max_y: max.y(),
40            });
41        }
42
43        Ok(Self { min, max })
44    }
45
46    /// Creates a bounding box from any two corners, normalizing axis order.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use use_geometry::{Aabb2, Point2};
52    ///
53    /// let bounds = Aabb2::from_points(Point2::new(4.0, 1.0), Point2::new(1.0, 3.0));
54    ///
55    /// assert_eq!(bounds.min(), Point2::new(1.0, 1.0));
56    /// assert_eq!(bounds.max(), Point2::new(4.0, 3.0));
57    /// ```
58    #[must_use]
59    pub const fn from_points(a: Point2, b: Point2) -> Self {
60        Self {
61            min: Point2::new(a.x().min(b.x()), a.y().min(b.y())),
62            max: Point2::new(a.x().max(b.x()), a.y().max(b.y())),
63        }
64    }
65
66    /// Returns the inclusive minimum corner.
67    #[must_use]
68    pub const fn min(&self) -> Point2 {
69        self.min
70    }
71
72    /// Returns the inclusive maximum corner.
73    #[must_use]
74    pub const fn max(&self) -> Point2 {
75        self.max
76    }
77
78    /// Returns the box width.
79    #[must_use]
80    pub fn width(&self) -> f64 {
81        self.max.x() - self.min.x()
82    }
83
84    /// Returns the box height.
85    #[must_use]
86    pub fn height(&self) -> f64 {
87        self.max.y() - self.min.y()
88    }
89
90    /// Returns the box center.
91    #[must_use]
92    pub const fn center(&self) -> Point2 {
93        self.min.midpoint(self.max)
94    }
95
96    /// Returns the box area.
97    #[must_use]
98    pub fn area(&self) -> f64 {
99        self.width() * self.height()
100    }
101
102    /// Returns `true` when `point` lies inside or on the boundary.
103    #[must_use]
104    pub fn contains_point(&self, point: Point2) -> bool {
105        point.x() >= self.min.x()
106            && point.x() <= self.max.x()
107            && point.y() >= self.min.y()
108            && point.y() <= self.max.y()
109    }
110
111    /// Returns `true` when `point` lies inside the box expanded by `tolerance`.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`GeometryError::NonFiniteTolerance`] when `tolerance` is `NaN`
116    /// or infinite.
117    ///
118    /// Returns [`GeometryError::NegativeTolerance`] when `tolerance` is negative.
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use use_geometry::{Aabb2, GeometryError, Point2};
124    ///
125    /// let bounds = Aabb2::from_points(Point2::new(1.0, 1.0), Point2::new(4.0, 3.0));
126    /// assert!(bounds.contains_point_with_tolerance(Point2::new(4.25, 3.0), 0.25)?);
127    /// # Ok::<(), GeometryError>(())
128    /// ```
129    pub fn contains_point_with_tolerance(
130        &self,
131        point: Point2,
132        tolerance: f64,
133    ) -> Result<bool, GeometryError> {
134        let tolerance = GeometryError::validate_tolerance(tolerance)?;
135
136        Ok(point.x() >= self.min.x() - tolerance
137            && point.x() <= self.max.x() + tolerance
138            && point.y() >= self.min.y() - tolerance
139            && point.y() <= self.max.y() + tolerance)
140    }
141
142    /// Returns `true` when the box has zero width or height.
143    #[must_use]
144    pub fn is_degenerate(&self) -> bool {
145        self.width() == 0.0 || self.height() == 0.0
146    }
147
148    /// Returns `true` when the box width or height is within `tolerance` of zero.
149    ///
150    /// # Errors
151    ///
152    /// Returns [`GeometryError::NonFiniteTolerance`] when `tolerance` is `NaN`
153    /// or infinite.
154    ///
155    /// Returns [`GeometryError::NegativeTolerance`] when `tolerance` is negative.
156    pub fn is_degenerate_with_tolerance(&self, tolerance: f64) -> Result<bool, GeometryError> {
157        let tolerance = GeometryError::validate_tolerance(tolerance)?;
158
159        Ok(self.width() <= tolerance || self.height() <= tolerance)
160    }
161}
162
163/// Creates a bounding box from any two corners.
164#[must_use]
165pub const fn aabb_from_points(a: Point2, b: Point2) -> Aabb2 {
166    Aabb2::from_points(a, b)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::{Aabb2, aabb_from_points};
172    use crate::{Circle, GeometryError, Point2, Segment2, Triangle};
173
174    fn approx_eq(left: f64, right: f64) -> bool {
175        (left - right).abs() < 1.0e-10
176    }
177
178    #[test]
179    fn constructs_valid_aabbs() {
180        let bounds =
181            Aabb2::try_new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0)).expect("valid bounds");
182
183        assert_eq!(bounds.min(), Point2::new(1.0, 2.0));
184        assert_eq!(bounds.max(), Point2::new(4.0, 6.0));
185    }
186
187    #[test]
188    fn rejects_invalid_aabb_ordering() {
189        assert_eq!(
190            Aabb2::try_new(Point2::new(4.0, 1.0), Point2::new(1.0, 3.0)),
191            Err(GeometryError::InvalidBounds {
192                min_x: 4.0,
193                min_y: 1.0,
194                max_x: 1.0,
195                max_y: 3.0,
196            })
197        );
198    }
199
200    #[test]
201    fn normalizes_point_order() {
202        let bounds = Aabb2::from_points(Point2::new(4.0, 1.0), Point2::new(1.0, 3.0));
203
204        assert_eq!(bounds.min(), Point2::new(1.0, 1.0));
205        assert_eq!(bounds.max(), Point2::new(4.0, 3.0));
206        assert_eq!(
207            aabb_from_points(Point2::new(4.0, 1.0), Point2::new(1.0, 3.0)),
208            bounds
209        );
210    }
211
212    #[test]
213    fn computes_dimensions_center_and_area() {
214        let bounds = Aabb2::from_points(Point2::new(1.0, 1.0), Point2::new(4.0, 3.0));
215
216        assert!(approx_eq(bounds.width(), 3.0));
217        assert!(approx_eq(bounds.height(), 2.0));
218        assert_eq!(bounds.center(), Point2::new(2.5, 2.0));
219        assert!(approx_eq(bounds.area(), 6.0));
220    }
221
222    #[test]
223    fn contains_points_including_boundary() {
224        let bounds = Aabb2::from_points(Point2::new(1.0, 1.0), Point2::new(4.0, 3.0));
225
226        assert!(bounds.contains_point(Point2::new(2.0, 2.0)));
227        assert!(bounds.contains_point(Point2::new(1.0, 3.0)));
228        assert!(!bounds.contains_point(Point2::new(4.5, 3.0)));
229    }
230
231    #[test]
232    fn supports_tolerance_based_containment() {
233        let bounds = Aabb2::from_points(Point2::new(1.0, 1.0), Point2::new(4.0, 3.0));
234
235        assert_eq!(
236            bounds.contains_point_with_tolerance(Point2::new(4.25, 3.0), 0.25),
237            Ok(true)
238        );
239        assert_eq!(
240            bounds.contains_point_with_tolerance(Point2::new(4.25, 3.0), -0.25),
241            Err(GeometryError::NegativeTolerance(-0.25))
242        );
243    }
244
245    #[test]
246    fn detects_degenerate_bounds() {
247        let point_bounds = Aabb2::from_points(Point2::new(2.0, 2.0), Point2::new(2.0, 2.0));
248        let line_bounds = Aabb2::from_points(Point2::new(2.0, 1.0), Point2::new(2.0, 3.0));
249
250        assert!(point_bounds.is_degenerate());
251        assert!(line_bounds.is_degenerate());
252        assert_eq!(line_bounds.is_degenerate_with_tolerance(0.0), Ok(true));
253    }
254
255    #[test]
256    fn builds_bounds_from_primitives() {
257        let point = Point2::new(2.0, 3.0);
258        let segment = Segment2::new(Point2::new(1.0, 4.0), Point2::new(3.0, 2.0));
259        let circle = Circle::try_new(Point2::new(2.0, 2.0), 1.5).expect("valid circle");
260        let triangle = Triangle::new(
261            Point2::new(0.0, 1.0),
262            Point2::new(4.0, 3.0),
263            Point2::new(2.0, -1.0),
264        );
265
266        assert_eq!(point.aabb(), Aabb2::from_points(point, point));
267        assert_eq!(
268            segment.aabb(),
269            Aabb2::from_points(Point2::new(1.0, 2.0), Point2::new(3.0, 4.0))
270        );
271        assert_eq!(
272            circle.aabb(),
273            Aabb2::from_points(Point2::new(0.5, 0.5), Point2::new(3.5, 3.5))
274        );
275        assert_eq!(
276            triangle.aabb(),
277            Aabb2::from_points(Point2::new(0.0, -1.0), Point2::new(4.0, 3.0))
278        );
279    }
280}