1use core::fmt;
14
15use ogeom_core::Tolerances;
16
17use crate::{Point, Vector};
18
19#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Aabb {
27 extent: Option<(Point, Point)>,
29}
30
31impl Default for Aabb {
32 fn default() -> Self {
33 Self::EMPTY
34 }
35}
36
37impl Aabb {
38 pub const EMPTY: Self = Self { extent: None };
40
41 #[must_use]
43 pub const fn of_point(p: Point) -> Self {
44 Self {
45 extent: Some((p, p)),
46 }
47 }
48
49 #[must_use]
51 pub fn of_corners(a: Point, b: Point) -> Self {
52 Self {
53 extent: Some((a.min(b), a.max(b))),
54 }
55 }
56
57 #[must_use]
59 pub fn of_points(points: &[Point]) -> Self {
60 points.iter().fold(Self::EMPTY, |acc, p| acc.with_point(*p))
61 }
62
63 #[must_use]
65 pub const fn is_empty(&self) -> bool {
66 self.extent.is_none()
67 }
68
69 #[must_use]
71 pub fn low(&self) -> Option<Point> {
72 self.extent.map(|(low, _)| low)
73 }
74
75 #[must_use]
77 pub fn high(&self) -> Option<Point> {
78 self.extent.map(|(_, high)| high)
79 }
80
81 #[must_use]
83 pub fn centre(&self) -> Option<Point> {
84 self.extent.map(|(low, high)| low.midpoint(high))
85 }
86
87 #[must_use]
89 pub fn size(&self) -> Vector {
90 self.extent.map_or(Vector::ZERO, |(low, high)| high - low)
91 }
92
93 #[must_use]
95 pub fn extent_max(&self) -> f64 {
96 let s = self.size();
97 s.x.max(s.y).max(s.z)
98 }
99
100 #[must_use]
102 pub fn diagonal(&self) -> f64 {
103 self.size().magnitude()
104 }
105
106 #[must_use]
108 pub fn volume(&self) -> f64 {
109 let s = self.size();
110 s.x * s.y * s.z
111 }
112
113 #[must_use]
115 pub fn with_point(&self, p: Point) -> Self {
116 match self.extent {
117 None => Self::of_point(p),
118 Some((low, high)) => Self {
119 extent: Some((low.min(p), high.max(p))),
120 },
121 }
122 }
123
124 #[must_use]
126 pub fn union(&self, other: &Self) -> Self {
127 match (self.extent, other.extent) {
128 (None, _) => *other,
129 (_, None) => *self,
130 (Some((al, ah)), Some((bl, bh))) => Self {
131 extent: Some((al.min(bl), ah.max(bh))),
132 },
133 }
134 }
135
136 #[must_use]
141 pub fn expanded(&self, margin: f64) -> Self {
142 match self.extent {
143 None => Self::EMPTY,
144 Some((low, high)) => {
145 let m = Vector::splat(margin.max(0.0));
146 Self {
147 extent: Some((low - m, high + m)),
148 }
149 }
150 }
151 }
152
153 #[must_use]
155 pub fn with_tolerance(&self, tol: Tolerances) -> Self {
156 self.expanded(tol.confusion())
157 }
158
159 #[must_use]
161 pub fn contains(&self, p: Point) -> bool {
162 self.extent.is_some_and(|(low, high)| {
163 p.x >= low.x
164 && p.x <= high.x
165 && p.y >= low.y
166 && p.y <= high.y
167 && p.z >= low.z
168 && p.z <= high.z
169 })
170 }
171
172 #[must_use]
174 pub fn contains_box(&self, other: &Self) -> bool {
175 match other.extent {
176 None => true,
179 Some((low, high)) => self.contains(low) && self.contains(high),
180 }
181 }
182
183 #[must_use]
188 pub fn intersects(&self, other: &Self) -> bool {
189 let (Some((al, ah)), Some((bl, bh))) = (self.extent, other.extent) else {
190 return false;
191 };
192 al.x <= bh.x && ah.x >= bl.x && al.y <= bh.y && ah.y >= bl.y && al.z <= bh.z && ah.z >= bl.z
193 }
194
195 #[must_use]
197 pub fn intersection(&self, other: &Self) -> Self {
198 if !self.intersects(other) {
199 return Self::EMPTY;
200 }
201 let (Some((al, ah)), Some((bl, bh))) = (self.extent, other.extent) else {
202 return Self::EMPTY;
203 };
204 Self {
205 extent: Some((al.max(bl), ah.min(bh))),
206 }
207 }
208
209 #[must_use]
211 pub fn distance_to(&self, p: Point) -> f64 {
212 let Some((low, high)) = self.extent else {
213 return f64::INFINITY;
214 };
215 let outside = Vector::new(
217 (low.x - p.x).max(p.x - high.x).max(0.0),
218 (low.y - p.y).max(p.y - high.y).max(0.0),
219 (low.z - p.z).max(p.z - high.z).max(0.0),
220 );
221 outside.magnitude()
222 }
223
224 #[must_use]
230 pub fn distance_to_box(&self, other: &Self) -> f64 {
231 let (Some((al, ah)), Some((bl, bh))) = (self.extent, other.extent) else {
232 return f64::INFINITY;
233 };
234 let gap = Vector::new(
235 (bl.x - ah.x).max(al.x - bh.x).max(0.0),
236 (bl.y - ah.y).max(al.y - bh.y).max(0.0),
237 (bl.z - ah.z).max(al.z - bh.z).max(0.0),
238 );
239 gap.magnitude()
240 }
241
242 #[must_use]
244 pub fn corners(&self) -> Vec<Point> {
245 let Some((low, high)) = self.extent else {
246 return Vec::new();
247 };
248 let mut out = Vec::with_capacity(8);
249 for i in 0..8 {
250 out.push(Point::new(
251 if i & 1 == 0 { low.x } else { high.x },
252 if i & 2 == 0 { low.y } else { high.y },
253 if i & 4 == 0 { low.z } else { high.z },
254 ));
255 }
256 out
257 }
258
259 #[must_use]
267 pub fn transformed(&self, t: &crate::Transform) -> Self {
268 Self::of_points(
269 &self
270 .corners()
271 .iter()
272 .map(|p| t.apply(*p))
273 .collect::<Vec<_>>(),
274 )
275 }
276}
277
278impl fmt::Display for Aabb {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 match self.extent {
281 None => f.write_str("empty"),
282 Some((low, high)) => write!(
283 f,
284 "[{:.6}, {:.6}, {:.6}] .. [{:.6}, {:.6}, {:.6}]",
285 low.x, low.y, low.z, high.x, high.y, high.z
286 ),
287 }
288 }
289}
290
291impl FromIterator<Point> for Aabb {
292 fn from_iter<I: IntoIterator<Item = Point>>(iter: I) -> Self {
293 iter.into_iter()
294 .fold(Self::EMPTY, |acc, p| acc.with_point(p))
295 }
296}
297
298#[cfg(test)]
299#[allow(clippy::unwrap_used)]
300mod tests {
301 use super::*;
302 use crate::{Axis, Transform};
303 use approx::assert_relative_eq;
304
305 const T: Tolerances = Tolerances::millimetres();
306
307 fn unit() -> Aabb {
308 Aabb::of_corners(Point::ORIGIN, Point::new(1.0, 1.0, 1.0))
309 }
310
311 #[test]
312 fn the_empty_box_is_distinct_from_a_zero_sized_one() {
313 let empty = Aabb::EMPTY;
316 let degenerate = Aabb::of_point(Point::ORIGIN);
317
318 assert!(empty.is_empty());
319 assert!(!degenerate.is_empty());
320 assert!(!empty.contains(Point::ORIGIN));
321 assert!(degenerate.contains(Point::ORIGIN));
322 assert_eq!(empty.centre(), None);
323 assert_eq!(degenerate.centre(), Some(Point::ORIGIN));
324 assert!(!empty.intersects(°enerate));
325 assert_eq!(empty.distance_to(Point::ORIGIN), f64::INFINITY);
326 }
327
328 #[test]
329 fn corners_are_taken_in_either_order() {
330 let a = Aabb::of_corners(Point::new(1.0, 2.0, 3.0), Point::ORIGIN);
331 let b = Aabb::of_corners(Point::ORIGIN, Point::new(1.0, 2.0, 3.0));
332 assert_eq!(a, b);
333 assert_eq!(a.low(), Some(Point::ORIGIN));
334 assert_eq!(a.high(), Some(Point::new(1.0, 2.0, 3.0)));
335 }
336
337 #[test]
338 fn union_grows_and_the_empty_box_is_its_identity() {
339 let a = Aabb::of_point(Point::ORIGIN);
340 let b = Aabb::of_point(Point::new(1.0, 2.0, 3.0));
341 let joined = a.union(&b);
342 assert_eq!(joined.low(), Some(Point::ORIGIN));
343 assert_eq!(joined.high(), Some(Point::new(1.0, 2.0, 3.0)));
344
345 assert_eq!(a.union(&Aabb::EMPTY), a);
346 assert_eq!(Aabb::EMPTY.union(&a), a);
347 assert_eq!(Aabb::EMPTY.union(&Aabb::EMPTY), Aabb::EMPTY);
348 }
349
350 #[test]
351 fn measurements_of_a_unit_box() {
352 let b = unit();
353 assert_eq!(b.size(), Vector::new(1.0, 1.0, 1.0));
354 assert_relative_eq!(b.volume(), 1.0);
355 assert_relative_eq!(b.extent_max(), 1.0);
356 assert_relative_eq!(b.diagonal(), 3.0_f64.sqrt());
357 assert_eq!(b.centre(), Some(Point::new(0.5, 0.5, 0.5)));
358 assert_eq!(b.corners().len(), 8);
359 assert!(Aabb::EMPTY.corners().is_empty());
360 assert_relative_eq!(Aabb::EMPTY.volume(), 0.0);
361 }
362
363 #[test]
364 fn containment_includes_the_boundary() {
365 let b = unit();
368 assert!(b.contains(Point::ORIGIN));
369 assert!(b.contains(Point::new(1.0, 1.0, 1.0)));
370 assert!(b.contains(Point::new(0.5, 0.0, 1.0)));
371 assert!(!b.contains(Point::new(1.000_001, 0.5, 0.5)));
372 }
373
374 #[test]
375 fn boxes_that_touch_are_reported_as_intersecting() {
376 let a = unit();
379 let touching = Aabb::of_corners(Point::new(1.0, 0.0, 0.0), Point::new(2.0, 1.0, 1.0));
380 let apart = Aabb::of_corners(Point::new(1.001, 0.0, 0.0), Point::new(2.0, 1.0, 1.0));
381
382 assert!(a.intersects(&touching));
383 assert!(!a.intersects(&apart));
384 assert_relative_eq!(a.distance_to_box(&touching), 0.0);
385 assert_relative_eq!(a.distance_to_box(&apart), 0.001, epsilon = 1e-12);
386 }
387
388 #[test]
389 fn intersection_is_the_overlap_and_empty_when_there_is_none() {
390 let a = unit();
391 let b = Aabb::of_corners(Point::new(0.5, 0.5, 0.5), Point::new(2.0, 2.0, 2.0));
392 let overlap = a.intersection(&b);
393 assert_eq!(overlap.low(), Some(Point::new(0.5, 0.5, 0.5)));
394 assert_eq!(overlap.high(), Some(Point::new(1.0, 1.0, 1.0)));
395
396 let apart = Aabb::of_corners(Point::new(5.0, 5.0, 5.0), Point::new(6.0, 6.0, 6.0));
397 assert!(a.intersection(&apart).is_empty());
398 assert!(a.intersection(&Aabb::EMPTY).is_empty());
399 }
400
401 #[test]
402 fn distance_is_zero_inside_and_measured_from_the_nearest_face() {
403 let b = unit();
404 assert_relative_eq!(b.distance_to(Point::new(0.5, 0.5, 0.5)), 0.0);
405 assert_relative_eq!(
406 b.distance_to(Point::new(0.5, 0.5, 1.0)),
407 0.0,
408 epsilon = 1e-15
409 );
410 assert_relative_eq!(b.distance_to(Point::new(0.5, 0.5, 3.0)), 2.0);
411 assert_relative_eq!(
413 b.distance_to(Point::new(-3.0, -4.0, 0.5)),
414 5.0,
415 epsilon = 1e-15
416 );
417 }
418
419 #[test]
420 fn expansion_errs_outward_and_never_shrinks() {
421 let b = unit();
422 let grown = b.expanded(0.5);
423 assert_eq!(grown.low(), Some(Point::new(-0.5, -0.5, -0.5)));
424 assert!(grown.contains_box(&b));
425
426 assert_eq!(b.expanded(-1.0), b);
429 assert!(Aabb::EMPTY.expanded(1.0).is_empty());
430 assert!(b.with_tolerance(T).contains_box(&b));
431 }
432
433 #[test]
434 fn transforming_a_box_errs_outward() {
435 let b = unit();
440 let rotated = b.transformed(&Transform::rotation(Axis::Z, core::f64::consts::FRAC_PI_4));
441
442 for corner in b.corners() {
443 let moved = Transform::rotation(Axis::Z, core::f64::consts::FRAC_PI_4).apply(corner);
444 assert!(
445 rotated.contains(moved),
446 "corner {moved:?} escaped the bound"
447 );
448 }
449 assert!(
450 rotated.volume() > b.volume(),
451 "a rotation cannot tighten a bound"
452 );
453
454 let moved = b.transformed(&Transform::translation(Vector::new(10.0, 0.0, 0.0)));
456 assert_relative_eq!(moved.volume(), b.volume(), epsilon = 1e-12);
457 }
458
459 #[test]
460 fn the_empty_box_is_contained_by_everything() {
461 assert!(unit().contains_box(&Aabb::EMPTY));
462 assert!(Aabb::EMPTY.contains_box(&Aabb::EMPTY));
463 assert!(!Aabb::EMPTY.contains_box(&unit()));
464 }
465
466 #[test]
467 fn collecting_points_builds_their_bound() {
468 let points = [
469 Point::new(1.0, 0.0, 0.0),
470 Point::new(-2.0, 5.0, 1.0),
471 Point::new(0.0, -1.0, 3.0),
472 ];
473 let from_iter: Aabb = points.iter().copied().collect();
474 assert_eq!(from_iter, Aabb::of_points(&points));
475 assert_eq!(from_iter.low(), Some(Point::new(-2.0, -1.0, 0.0)));
476 assert_eq!(from_iter.high(), Some(Point::new(1.0, 5.0, 3.0)));
477 for p in points {
478 assert!(from_iter.contains(p));
479 }
480 assert!(Aabb::of_points(&[]).is_empty());
481 }
482
483 #[test]
484 fn display_distinguishes_empty_from_degenerate() {
485 assert_eq!(Aabb::EMPTY.to_string(), "empty");
486 assert!(
487 Aabb::of_point(Point::ORIGIN)
488 .to_string()
489 .contains("0.000000")
490 );
491 }
492}