1use crate::{error::GeometryError, point::Point2};
2
3#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct Aabb2 {
6 min: Point2,
7 max: Point2,
8}
9
10impl Aabb2 {
11 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 #[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 #[must_use]
68 pub const fn min(&self) -> Point2 {
69 self.min
70 }
71
72 #[must_use]
74 pub const fn max(&self) -> Point2 {
75 self.max
76 }
77
78 #[must_use]
80 pub fn width(&self) -> f64 {
81 self.max.x() - self.min.x()
82 }
83
84 #[must_use]
86 pub fn height(&self) -> f64 {
87 self.max.y() - self.min.y()
88 }
89
90 #[must_use]
92 pub const fn center(&self) -> Point2 {
93 self.min.midpoint(self.max)
94 }
95
96 #[must_use]
98 pub fn area(&self) -> f64 {
99 self.width() * self.height()
100 }
101
102 #[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 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 #[must_use]
144 pub fn is_degenerate(&self) -> bool {
145 self.width() == 0.0 || self.height() == 0.0
146 }
147
148 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#[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}