Skip to main content

qualia_core_db/render/physics/
aabb.rs

1//! Axis-aligned bounding box for an artefact, and the rigid+scale transform of its extent.
2//!
3//! Zero-alloc: every operation works over fixed `[f32; 3]` arrays and an 8-corner loop — no `Vec`,
4//! no heap, suitable for the hot path. Rotation/translation come from the PGA motor oracle
5//! (`render::pga`), the same map the GPU projector uses, so artefact physics and rendering agree.
6
7use crate::render::pga::{sandwich_point, Motor};
8
9/// An axis-aligned bounding box (model or world space).
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct Aabb {
12    pub min: [f32; 3],
13    pub max: [f32; 3],
14}
15
16impl Aabb {
17    #[inline]
18    pub fn new(min: [f32; 3], max: [f32; 3]) -> Self {
19        Aabb { min, max }
20    }
21
22    /// AABB enclosing a point set; `None` if empty. (No allocation — single pass.)
23    pub fn from_points(points: &[[f32; 3]]) -> Option<Aabb> {
24        let first = *points.first()?;
25        let mut min = first;
26        let mut max = first;
27        for p in &points[1..] {
28            for k in 0..3 {
29                if p[k] < min[k] {
30                    min[k] = p[k];
31                }
32                if p[k] > max[k] {
33                    max[k] = p[k];
34                }
35            }
36        }
37        Some(Aabb { min, max })
38    }
39
40    #[inline]
41    pub fn center(&self) -> [f32; 3] {
42        [
43            0.5 * (self.min[0] + self.max[0]),
44            0.5 * (self.min[1] + self.max[1]),
45            0.5 * (self.min[2] + self.max[2]),
46        ]
47    }
48
49    /// Per-axis extent (`max - min`).
50    #[inline]
51    pub fn extent(&self) -> [f32; 3] {
52        [
53            self.max[0] - self.min[0],
54            self.max[1] - self.min[1],
55            self.max[2] - self.min[2],
56        ]
57    }
58
59    /// Enclosed volume (product of extents).
60    #[inline]
61    pub fn volume(&self) -> f32 {
62        let e = self.extent();
63        e[0] * e[1] * e[2]
64    }
65
66    #[inline]
67    pub fn contains_point(&self, p: [f32; 3]) -> bool {
68        (0..3).all(|k| p[k] >= self.min[k] && p[k] <= self.max[k])
69    }
70
71    /// Whether `self` fully encloses `other`.
72    #[inline]
73    pub fn contains(&self, other: &Aabb) -> bool {
74        (0..3).all(|k| other.min[k] >= self.min[k] && other.max[k] <= self.max[k])
75    }
76
77    /// Transform the box by a per-axis `scale` about its own centre, then a rigid PGA `motor`
78    /// (rotation + translation), and return the enclosing AABB of the result. Zero-alloc.
79    pub fn transformed(&self, motor: Motor, scale: [f32; 3]) -> Aabb {
80        let c = self.center();
81        let mut min = [f32::INFINITY; 3];
82        let mut max = [f32::NEG_INFINITY; 3];
83        for ix in 0..8u8 {
84            let corner = [
85                if ix & 1 == 0 {
86                    self.min[0]
87                } else {
88                    self.max[0]
89                },
90                if ix & 2 == 0 {
91                    self.min[1]
92                } else {
93                    self.max[1]
94                },
95                if ix & 4 == 0 {
96                    self.min[2]
97                } else {
98                    self.max[2]
99                },
100            ];
101            let scaled = [
102                c[0] + (corner[0] - c[0]) * scale[0],
103                c[1] + (corner[1] - c[1]) * scale[1],
104                c[2] + (corner[2] - c[2]) * scale[2],
105            ];
106            let w = sandwich_point(motor, scaled);
107            for k in 0..3 {
108                if w[k] < min[k] {
109                    min[k] = w[k];
110                }
111                if w[k] > max[k] {
112                    max[k] = w[k];
113                }
114            }
115        }
116        Aabb { min, max }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn unit() -> Aabb {
125        Aabb::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0])
126    }
127
128    #[test]
129    fn extent_volume_center() {
130        let b = unit();
131        assert_eq!(b.extent(), [2.0, 2.0, 2.0]);
132        assert_eq!(b.volume(), 8.0);
133        assert_eq!(b.center(), [0.0, 0.0, 0.0]);
134    }
135
136    #[test]
137    fn identity_transform_is_noop() {
138        let b = unit();
139        let out = b.transformed(Motor::identity(), [1.0, 1.0, 1.0]);
140        for k in 0..3 {
141            assert!((out.min[k] - b.min[k]).abs() < 1e-5);
142            assert!((out.max[k] - b.max[k]).abs() < 1e-5);
143        }
144    }
145
146    #[test]
147    fn scale_shrinks_extent_about_centre() {
148        let out = unit().transformed(Motor::identity(), [0.5, 0.5, 0.5]);
149        assert!((out.extent()[0] - 1.0).abs() < 1e-5); // 2.0 * 0.5
150        assert_eq!(out.center(), [0.0, 0.0, 0.0]); // centre preserved
151    }
152
153    #[test]
154    fn contains_and_from_points() {
155        let b = Aabb::from_points(&[[0.0, 0.0, 0.0], [1.0, 2.0, 3.0], [-1.0, 0.0, 0.0]]).unwrap();
156        assert_eq!(b.min, [-1.0, 0.0, 0.0]);
157        assert_eq!(b.max, [1.0, 2.0, 3.0]);
158        assert!(b.contains_point([0.0, 1.0, 1.0]));
159        assert!(!b.contains_point([5.0, 0.0, 0.0]));
160        assert!(Aabb::from_points(&[]).is_none());
161    }
162}