Skip to main content

gizmo_math/
aabb.rs

1use glam::{Mat4, Vec3A, Vec4Swizzles};
2use std::f32;
3
4/// Axis-Aligned Bounding Box (AABB) represented by min/max corners.
5///
6/// Uses `Vec3A` (16-byte aligned SIMD vector) for performance.
7/// An "empty" AABB has min = +INF, max = -INF and represents no volume.
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct Aabb {
10    pub min: Vec3A,
11    pub max: Vec3A,
12}
13
14impl Aabb {
15    // -----------------------------------------------------------------------
16    // Constructors
17    // -----------------------------------------------------------------------
18
19    /// Creates an AABB from explicit min/max corners.
20    /// Caller is responsible for ensuring `min <= max` on all axes.
21    #[inline]
22    pub fn new(min: impl Into<Vec3A>, max: impl Into<Vec3A>) -> Self {
23        Self {
24            min: min.into(),
25            max: max.into(),
26        }
27    }
28
29    /// Creates the canonical empty/invalid AABB (min = +INF, max = -INF).
30    #[inline]
31    pub fn empty() -> Self {
32        Self {
33            min: Vec3A::splat(f32::INFINITY),
34            max: Vec3A::splat(f32::NEG_INFINITY),
35        }
36    }
37
38    /// Creates an AABB from a center point and half-extents.
39    #[inline]
40    pub fn from_center_half_extents(
41        center: impl Into<Vec3A>,
42        half_extents: impl Into<Vec3A>,
43    ) -> Self {
44        let c = center.into();
45        let h = half_extents.into();
46        Self {
47            min: c - h,
48            max: c + h,
49        }
50    }
51
52    /// Creates an AABB that contains all the provided points.
53    /// Returns `Aabb::empty()` if the iterator is empty.
54    #[inline]
55    pub fn from_points(points: impl IntoIterator<Item = impl Into<Vec3A>>) -> Self {
56        let mut aabb = Self::empty();
57        for p in points {
58            aabb.extend(p);
59        }
60        aabb
61    }
62
63    // -----------------------------------------------------------------------
64    // State queries
65    // -----------------------------------------------------------------------
66
67    /// Returns `true` if the AABB has no valid volume and is strictly invalid (min > max on any axis).
68    /// Note: A degenerate point where `min == max` is NOT considered empty.
69    /// This is a deliberate design choice so zero-volume points can still overlap
70    /// with frustums or other shapes. To check for zero volume, use `is_degenerate()`.
71    #[inline]
72    pub fn is_empty(self) -> bool {
73        self.min.x > self.max.x || self.min.y > self.max.y || self.min.z > self.max.z
74    }
75
76    /// Returns `true` if the AABB has zero volume (min >= max on any axis).
77    /// This includes both strictly empty AABBs and degenerate points, lines, or planes.
78    #[inline]
79    pub fn is_degenerate(self) -> bool {
80        self.min.cmpge(self.max).any()
81    }
82
83    /// Returns `true` if the AABB has a valid volume.
84    #[inline]
85    pub fn is_valid(self) -> bool {
86        !self.is_empty()
87    }
88
89    // -----------------------------------------------------------------------
90    // Geometric properties
91    // -----------------------------------------------------------------------
92
93    /// Returns the center of the AABB.
94    #[inline]
95    pub fn center(self) -> Vec3A {
96        (self.min + self.max) * 0.5
97    }
98
99    /// Returns the half-extents (half the size on each axis).
100    #[inline]
101    pub fn half_extents(self) -> Vec3A {
102        (self.max - self.min) * 0.5
103    }
104
105    /// Returns the full size (extent) on each axis.
106    #[inline]
107    pub fn size(self) -> Vec3A {
108        self.max - self.min
109    }
110
111    /// Returns the total volume of the AABB.
112    /// Returns `0.0` for an empty AABB.
113    #[inline]
114    pub fn volume(self) -> f32 {
115        if self.is_empty() {
116            return 0.0;
117        }
118        let s = self.size();
119        s.x * s.y * s.z
120    }
121
122    /// Returns the surface area of the AABB.
123    /// Useful for SAH-based BVH construction.
124    /// Returns `0.0` for an empty AABB.
125    #[inline]
126    pub fn surface_area(self) -> f32 {
127        if self.is_empty() {
128            return 0.0;
129        }
130        let s = self.size();
131        2.0 * (s.x * s.y + s.y * s.z + s.z * s.x)
132    }
133
134    /// Returns the length of the diagonal of the AABB.
135    #[inline]
136    pub fn diagonal(self) -> f32 {
137        self.size().length()
138    }
139
140    // -----------------------------------------------------------------------
141    // Point / containment queries
142    // -----------------------------------------------------------------------
143
144    /// Returns `true` if the point lies inside or on the boundary of this AABB.
145    #[inline]
146    pub fn contains_point(self, pt: impl Into<Vec3A>) -> bool {
147        let p = pt.into();
148        p.cmpge(self.min).all() && p.cmple(self.max).all()
149    }
150
151    /// Returns `true` if `other` is entirely contained within this AABB.
152    #[inline]
153    pub fn contains_aabb(self, other: Self) -> bool {
154        if self.is_empty() || other.is_empty() {
155            return false;
156        }
157        other.min.cmpge(self.min).all() && other.max.cmple(self.max).all()
158    }
159
160    /// Returns the closest point on (or inside) the AABB to the given point.
161    #[inline]
162    pub fn closest_point(self, pt: impl Into<Vec3A>) -> Vec3A {
163        pt.into().clamp(self.min, self.max)
164    }
165
166    /// Returns the squared distance from the point to the AABB surface.
167    /// Returns `0.0` if the point is inside the AABB.
168    #[inline]
169    pub fn distance_sq_to_point(self, pt: impl Into<Vec3A>) -> f32 {
170        let p = pt.into();
171        let closest = self.closest_point(p);
172        (p - closest).length_squared()
173    }
174
175    /// Returns the distance from the point to the AABB surface.
176    /// Returns `0.0` if the point is inside the AABB.
177    #[inline]
178    pub fn distance_to_point(self, pt: impl Into<Vec3A>) -> f32 {
179        self.distance_sq_to_point(pt).sqrt()
180    }
181
182    // -----------------------------------------------------------------------
183    // Intersection / overlap queries
184    // -----------------------------------------------------------------------
185
186    /// Returns `true` if this AABB overlaps `other`.
187    /// Edge/face-touching counts as intersection.
188    #[inline]
189    pub fn intersects(self, other: Self) -> bool {
190        if self.is_empty() || other.is_empty() {
191            return false;
192        }
193        self.min.cmple(other.max).all() && self.max.cmpge(other.min).all()
194    }
195
196    /// Returns `true` if this AABB strictly overlaps `other`.
197    /// Edge/face-touching does NOT count as intersection.
198    #[inline]
199    pub fn intersects_exclusive(self, other: Self) -> bool {
200        if self.is_empty() || other.is_empty() {
201            return false;
202        }
203        self.min.cmplt(other.max).all() && self.max.cmpgt(other.min).all()
204    }
205
206    /// Returns the intersection volume as an `Aabb`, or `None` if they do not overlap.
207    /// Edge/face-touching yields a degenerate AABB (zero volume on at least one axis).
208    #[inline]
209    pub fn intersection(self, other: Self) -> Option<Self> {
210        if !self.intersects(other) {
211            return None;
212        }
213        Some(Self {
214            min: self.min.max(other.min),
215            max: self.max.min(other.max),
216        })
217    }
218
219    // -----------------------------------------------------------------------
220    // Modification / combination
221    // -----------------------------------------------------------------------
222
223    /// Expands this AABB to include the given point.
224    #[inline]
225    pub fn extend(&mut self, pt: impl Into<Vec3A>) {
226        let p = pt.into();
227        self.min = self.min.min(p);
228        self.max = self.max.max(p);
229    }
230
231    /// Returns a new AABB that is the union of `self` and `other`.
232    #[inline]
233    pub fn merge(self, other: Self) -> Self {
234        Self {
235            min: self.min.min(other.min),
236            max: self.max.max(other.max),
237        }
238    }
239
240    /// Returns a new AABB expanded (inflated) outward by `amount` on every face.
241    /// A negative `amount` shrinks the AABB. If shrinking causes inversion, returns empty.
242    #[inline]
243    pub fn expand(self, amount: f32) -> Self {
244        let delta = Vec3A::splat(amount);
245        let new_min = self.min - delta;
246        let new_max = self.max + delta;
247        if new_min.cmpgt(new_max).any() {
248            Self::empty()
249        } else {
250            Self {
251                min: new_min,
252                max: new_max,
253            }
254        }
255    }
256
257    // -----------------------------------------------------------------------
258    // Transformation
259    // -----------------------------------------------------------------------
260
261    /// Transforms this AABB by a `Mat4`, returning a new world-space AABB.
262    ///
263    /// Uses Arvo's method: applies the 3×3 rotation/scale part column-by-column,
264    /// so only 3 iterations instead of 8 corner transforms.
265    ///
266    /// Reference: James Arvo, "Transforming Axis-Aligned Bounding Boxes",
267    /// Graphics Gems, 1990.
268    #[inline]
269    pub fn transform(self, mat: &Mat4) -> Self {
270        if self.is_empty() {
271            return self;
272        }
273
274        // Translation goes directly into both min and max.
275        let translation = Vec3A::from(mat.w_axis.xyz());
276        let mut new_min = translation;
277        let mut new_max = translation;
278
279        // For each column of the 3×3 upper-left submatrix, accumulate the
280        // interval contribution to [new_min, new_max].
281        let cols = [
282            Vec3A::from(mat.x_axis.xyz()),
283            Vec3A::from(mat.y_axis.xyz()),
284            Vec3A::from(mat.z_axis.xyz()),
285        ];
286        let bounds = [self.min, self.max];
287
288        for (col_idx, col) in cols.iter().enumerate() {
289            // Pick which original bound (min or max) contributes positively.
290            for axis in 0..3 {
291                let col_val = col[axis];
292                let (lo, hi) = if col_val >= 0.0 {
293                    (bounds[0][col_idx] * col_val, bounds[1][col_idx] * col_val)
294                } else {
295                    (bounds[1][col_idx] * col_val, bounds[0][col_idx] * col_val)
296                };
297                new_min[axis] += lo;
298                new_max[axis] += hi;
299            }
300        }
301
302        Self {
303            min: new_min,
304            max: new_max,
305        }
306    }
307
308    // -----------------------------------------------------------------------
309    // Corners
310    // -----------------------------------------------------------------------
311
312    /// Returns all 8 corners of the AABB in a fixed order.
313    /// Order: --- -+- +-- ++- --+ -++ +-+ +++  (xyz sign pattern)
314    #[inline]
315    pub fn corners(self) -> [Vec3A; 8] {
316        let (mn, mx) = (self.min, self.max);
317        [
318            Vec3A::new(mn.x, mn.y, mn.z),
319            Vec3A::new(mn.x, mx.y, mn.z),
320            Vec3A::new(mx.x, mn.y, mn.z),
321            Vec3A::new(mx.x, mx.y, mn.z),
322            Vec3A::new(mn.x, mn.y, mx.z),
323            Vec3A::new(mn.x, mx.y, mx.z),
324            Vec3A::new(mx.x, mn.y, mx.z),
325            Vec3A::new(mx.x, mx.y, mx.z),
326        ]
327    }
328}
329
330// ---------------------------------------------------------------------------
331// Trait implementations
332// ---------------------------------------------------------------------------
333
334impl Default for Aabb {
335    #[inline]
336    fn default() -> Self {
337        Self::empty()
338    }
339}
340
341impl std::fmt::Display for Aabb {
342    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343        write!(
344            f,
345            "Aabb(min: [{:.3}, {:.3}, {:.3}], max: [{:.3}, {:.3}, {:.3}])",
346            self.min.x, self.min.y, self.min.z, self.max.x, self.max.y, self.max.z,
347        )
348    }
349}
350
351// ---------------------------------------------------------------------------
352// Tests
353// ---------------------------------------------------------------------------
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use glam::Vec3;
359
360    const EPS: f32 = 1e-5;
361
362    fn approx_eq(a: Vec3A, b: Vec3A) -> bool {
363        (a - b).length() < EPS
364    }
365
366    // --- Constructors ---
367
368    #[test]
369    fn test_empty() {
370        let a = Aabb::empty();
371        assert!(a.is_empty());
372        assert!(!a.is_valid());
373        assert!(a.is_degenerate());
374    }
375
376    #[test]
377    fn test_degenerate_point() {
378        let a = Aabb::new(Vec3::ZERO, Vec3::ZERO);
379        assert!(!a.is_empty()); // min == max is not empty
380        assert!(a.is_valid()); // therefore it is valid
381        assert!(a.is_degenerate()); // but it is degenerate (zero volume)
382    }
383
384    #[test]
385    fn test_from_center_half_extents() {
386        let a = Aabb::from_center_half_extents(Vec3::ZERO, Vec3::ONE);
387        assert!(approx_eq(a.min, Vec3A::splat(-1.0)));
388        assert!(approx_eq(a.max, Vec3A::splat(1.0)));
389        assert!(approx_eq(a.center(), Vec3A::ZERO));
390    }
391
392    #[test]
393    fn test_from_points_empty() {
394        let a = Aabb::from_points(std::iter::empty::<Vec3A>());
395        assert!(a.is_empty());
396    }
397
398    #[test]
399    fn test_from_points() {
400        let pts = [Vec3A::new(1.0, -2.0, 3.0), Vec3A::new(-1.0, 4.0, 0.0)];
401        let a = Aabb::from_points(pts);
402        assert!(approx_eq(a.min, Vec3A::new(-1.0, -2.0, 0.0)));
403        assert!(approx_eq(a.max, Vec3A::new(1.0, 4.0, 3.0)));
404    }
405
406    // --- Geometric properties ---
407
408    #[test]
409    fn test_center_size_half_extents() {
410        let a = Aabb::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(4.0, 6.0, 8.0));
411        assert!(approx_eq(a.center(), Vec3A::new(2.0, 3.0, 4.0)));
412        assert!(approx_eq(a.size(), Vec3A::new(4.0, 6.0, 8.0)));
413        assert!(approx_eq(a.half_extents(), Vec3A::new(2.0, 3.0, 4.0)));
414    }
415
416    #[test]
417    fn test_volume() {
418        let a = Aabb::new(Vec3::ZERO, Vec3::new(2.0, 3.0, 4.0));
419        assert!((a.volume() - 24.0).abs() < EPS);
420        assert!((Aabb::empty().volume()).abs() < EPS);
421    }
422
423    #[test]
424    fn test_surface_area() {
425        let a = Aabb::new(Vec3::ZERO, Vec3::new(1.0, 2.0, 3.0));
426        // 2*(1*2 + 2*3 + 3*1) = 2*(2+6+3) = 22
427        assert!((a.surface_area() - 22.0).abs() < EPS);
428    }
429
430    // --- Containment ---
431
432    #[test]
433    fn test_contains_point() {
434        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
435        assert!(a.contains_point(Vec3A::splat(0.5)));
436        assert!(a.contains_point(Vec3A::ZERO)); // on boundary
437        assert!(a.contains_point(Vec3A::ONE)); // on boundary
438        assert!(!a.contains_point(Vec3A::splat(1.1)));
439        assert!(!a.contains_point(Vec3A::splat(-0.1)));
440    }
441
442    #[test]
443    fn test_contains_aabb() {
444        let outer = Aabb::new(Vec3::ZERO, Vec3::splat(10.0));
445        let inner = Aabb::new(Vec3::ONE, Vec3::splat(9.0));
446        assert!(outer.contains_aabb(inner));
447        assert!(!inner.contains_aabb(outer));
448    }
449
450    #[test]
451    fn test_closest_point_inside() {
452        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
453        let p = Vec3A::splat(0.5);
454        assert!(approx_eq(a.closest_point(p), p)); // inside → same point
455    }
456
457    #[test]
458    fn test_closest_point_outside() {
459        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
460        let p = Vec3A::new(2.0, 0.5, 0.5);
461        assert!(approx_eq(a.closest_point(p), Vec3A::new(1.0, 0.5, 0.5)));
462    }
463
464    #[test]
465    fn test_distance_sq_to_point() {
466        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
467        assert!((a.distance_sq_to_point(Vec3A::splat(0.5))).abs() < EPS); // inside
468        assert!((a.distance_sq_to_point(Vec3A::new(2.0, 0.5, 0.5)) - 1.0).abs() < EPS);
469    }
470
471    // --- Intersection ---
472
473    #[test]
474    fn test_intersects_overlapping() {
475        let a = Aabb::new(Vec3::ZERO, Vec3::splat(2.0));
476        let b = Aabb::new(Vec3::ONE, Vec3::splat(3.0));
477        assert!(a.intersects(b));
478        assert!(b.intersects(a));
479    }
480
481    #[test]
482    fn test_intersects_disjoint() {
483        let a = Aabb::new(Vec3::ZERO, Vec3::splat(1.0));
484        let b = Aabb::new(Vec3::splat(2.0), Vec3::splat(3.0));
485        assert!(!a.intersects(b));
486    }
487
488    #[test]
489    fn test_intersects_edge_touching() {
490        let a = Aabb::new(Vec3::ZERO, Vec3::splat(1.0));
491        let b = Aabb::new(Vec3::new(1.0, 0.0, 0.0), Vec3::new(2.0, 1.0, 1.0));
492        assert!(a.intersects(b)); // inclusive: edge touch = true
493        assert!(!a.intersects_exclusive(b)); // exclusive: edge touch = false
494    }
495
496    #[test]
497    fn test_intersects_empty() {
498        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
499        let e = Aabb::empty();
500        assert!(!a.intersects(e));
501        assert!(!e.intersects(a));
502    }
503
504    #[test]
505    fn test_intersection_volume() {
506        let a = Aabb::new(Vec3::ZERO, Vec3::splat(2.0));
507        let b = Aabb::new(Vec3::ONE, Vec3::splat(3.0));
508        let i = a.intersection(b).expect("should intersect");
509        assert!(approx_eq(i.min, Vec3A::ONE));
510        assert!(approx_eq(i.max, Vec3A::splat(2.0)));
511    }
512
513    #[test]
514    fn test_intersection_none() {
515        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
516        let b = Aabb::new(Vec3::splat(2.0), Vec3::splat(3.0));
517        assert!(a.intersection(b).is_none());
518    }
519
520    // --- Modification ---
521
522    #[test]
523    fn test_extend() {
524        let mut a = Aabb::new(Vec3::ZERO, Vec3::splat(2.0));
525        a.extend(Vec3A::new(3.0, -1.0, 1.0));
526        assert!(approx_eq(a.min, Vec3A::new(0.0, -1.0, 0.0)));
527        assert!(approx_eq(a.max, Vec3A::new(3.0, 2.0, 2.0)));
528    }
529
530    #[test]
531    fn test_extend_empty() {
532        let mut a = Aabb::empty();
533        let pt = Vec3A::new(-5.0, 10.0, 3.0);
534        a.extend(pt);
535        assert!(approx_eq(a.min, pt));
536        assert!(approx_eq(a.max, pt));
537
538        let pt2 = Vec3A::new(5.0, -10.0, 6.0);
539        a.extend(pt2);
540        assert!(approx_eq(a.min, Vec3A::new(-5.0, -10.0, 3.0)));
541        assert!(approx_eq(a.max, Vec3A::new(5.0, 10.0, 6.0)));
542    }
543
544    #[test]
545    fn test_merge() {
546        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
547        let b = Aabb::new(Vec3::splat(-1.0), Vec3::splat(0.5));
548        let m = a.merge(b);
549        assert!(approx_eq(m.min, Vec3A::splat(-1.0)));
550        assert!(approx_eq(m.max, Vec3A::ONE));
551    }
552
553    #[test]
554    fn test_expand() {
555        let a = Aabb::new(Vec3::ZERO, Vec3::splat(2.0));
556        let expanded = a.expand(1.0);
557        assert!(approx_eq(expanded.min, Vec3A::splat(-1.0)));
558        assert!(approx_eq(expanded.max, Vec3A::splat(3.0)));
559
560        let shrunk = a.expand(-0.5);
561        assert!(approx_eq(shrunk.min, Vec3A::splat(0.5)));
562        assert!(approx_eq(shrunk.max, Vec3A::splat(1.5)));
563
564        // Over-shrink → empty
565        let over = a.expand(-10.0);
566        assert!(over.is_empty());
567    }
568
569    // --- Transform ---
570
571    #[test]
572    fn test_transform_identity() {
573        let a = Aabb::new(Vec3::new(1.0, 2.0, 3.0), Vec3::new(4.0, 5.0, 6.0));
574        let result = a.transform(&Mat4::IDENTITY);
575        assert!(approx_eq(result.min, a.min));
576        assert!(approx_eq(result.max, a.max));
577    }
578
579    #[test]
580    fn test_transform_translation() {
581        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
582        let t = Mat4::from_translation(glam::Vec3::new(1.0, 2.0, 3.0));
583        let result = a.transform(&t);
584        assert!(approx_eq(result.min, Vec3A::new(1.0, 2.0, 3.0)));
585        assert!(approx_eq(result.max, Vec3A::new(2.0, 3.0, 4.0)));
586    }
587
588    #[test]
589    fn test_transform_uniform_scale() {
590        let a = Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::ONE);
591        let s = Mat4::from_scale(glam::Vec3::splat(2.0));
592        let result = a.transform(&s);
593        assert!(approx_eq(result.min, Vec3A::splat(-2.0)));
594        assert!(approx_eq(result.max, Vec3A::splat(2.0)));
595    }
596
597    #[test]
598    fn test_transform_rotation_90_deg() {
599        // Rotating a unit AABB 90° around Z: (1,1,1) → still (1,1,1) by symmetry
600        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
601        let r = Mat4::from_rotation_z(std::f32::consts::FRAC_PI_2);
602        let result = a.transform(&r);
603        // The result should still contain the origin and be approximately unit-sized
604        assert!(result.is_valid());
605        assert!((result.surface_area() - a.surface_area()).abs() < 1e-4);
606    }
607
608    #[test]
609    fn test_transform_empty() {
610        let e = Aabb::empty();
611        let result = e.transform(&Mat4::IDENTITY);
612        assert!(result.is_empty());
613    }
614
615    // --- Corners ---
616
617    #[test]
618    fn test_corners_count_and_bounds() {
619        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
620        let corners = a.corners();
621        assert_eq!(corners.len(), 8);
622        for c in corners {
623            assert!(a.contains_point(c));
624        }
625    }
626
627    // --- Traits ---
628
629    #[test]
630    fn test_default_is_empty() {
631        let a = Aabb::default();
632        assert!(a.is_empty());
633    }
634
635    #[test]
636    fn test_display() {
637        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
638        let s = format!("{}", a);
639        assert!(s.contains("min") && s.contains("max"));
640    }
641}