khora_core/math/
geometry.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Provides geometric primitive shapes for spatial calculations.
16//!
17//! This module contains common geometric structures used in collision detection,
18//! culling, and other spatial reasoning tasks within the engine.
19
20use super::{Mat4, Vec3, Vec4, EPSILON};
21
22/// Represents an Axis-Aligned Bounding Box (AABB).
23///
24/// An AABB is a rectangular prism aligned with the coordinate axes, defined by its
25/// minimum and maximum corner points. It is a simple but highly efficient volume
26/// for broad-phase collision detection and visibility culling.
27#[derive(Debug, Clone, Copy, PartialEq)]
28#[repr(C)]
29pub struct Aabb {
30    /// The corner of the box with the smallest coordinates on all axes.
31    pub min: Vec3,
32    /// The corner of the box with the largest coordinates on all axes.
33    pub max: Vec3,
34}
35
36impl Aabb {
37    /// An invalid `Aabb` where `min` components are positive infinity and `max` are negative infinity.
38    ///
39    /// This is useful as a neutral starting point for merging operations. Merging any
40    /// valid `Aabb` with `INVALID` will result in that valid `Aabb`.
41    pub const INVALID: Self = Self {
42        min: Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY),
43        max: Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY),
44    };
45
46    /// Creates a new `Aabb` from two corner points.
47    ///
48    /// This constructor automatically ensures that the `min` field holds the
49    /// component-wise minimum and `max` holds the component-wise maximum,
50    /// regardless of the order the points are passed in.
51    #[inline]
52    pub fn from_min_max(min_pt: Vec3, max_pt: Vec3) -> Self {
53        Self {
54            min: Vec3::new(
55                min_pt.x.min(max_pt.x),
56                min_pt.y.min(max_pt.y),
57                min_pt.z.min(max_pt.z),
58            ),
59            max: Vec3::new(
60                min_pt.x.max(max_pt.x),
61                min_pt.y.max(max_pt.y),
62                min_pt.z.max(max_pt.z),
63            ),
64        }
65    }
66
67    /// Creates a new `Aabb` from a center point and its half-extents.
68    ///
69    /// The half-extents represent the distance from the center to the faces of the box.
70    /// The provided `half_extents` will be made non-negative.
71    #[inline]
72    pub fn from_center_half_extents(center: Vec3, half_extents: Vec3) -> Self {
73        let safe_half_extents = Vec3::new(
74            half_extents.x.abs(),
75            half_extents.y.abs(),
76            half_extents.z.abs(),
77        ); // Assuming Vec3 doesn't have .abs()
78        Self {
79            min: center - safe_half_extents,
80            max: center + safe_half_extents,
81        }
82    }
83
84    /// Creates a degenerate `Aabb` containing a single point (min and max are the same).
85    #[inline]
86    pub fn from_point(point: Vec3) -> Self {
87        Self {
88            min: point,
89            max: point,
90        }
91    }
92
93    /// Creates an `Aabb` that tightly encloses a given set of points.
94    ///
95    /// # Returns
96    ///
97    /// Returns `Some(Aabb)` if the input slice is not empty, otherwise `None`.
98    pub fn from_points(points: &[Vec3]) -> Option<Self> {
99        if points.is_empty() {
100            return None;
101        }
102
103        let mut min_pt = points[0];
104        let mut max_pt = points[0];
105
106        for point in points.iter().skip(1) {
107            min_pt.x = min_pt.x.min(point.x);
108            min_pt.y = min_pt.y.min(point.y);
109            min_pt.z = min_pt.z.min(point.z);
110
111            max_pt.x = max_pt.x.max(point.x);
112            max_pt.y = max_pt.y.max(point.y);
113            max_pt.z = max_pt.z.max(point.z);
114        }
115
116        Some(Self {
117            min: min_pt,
118            max: max_pt,
119        })
120    }
121
122    /// Calculates the center point of the `Aabb`.
123    #[inline]
124    pub fn center(&self) -> Vec3 {
125        (self.min + self.max) * 0.5
126    }
127
128    /// Calculates the half-extents (half the size on each axis) of the `Aabb`.
129    #[inline]
130    pub fn half_extents(&self) -> Vec3 {
131        (self.max - self.min) * 0.5
132    }
133
134    /// Calculates the full size (width, height, depth) of the `Aabb`.
135    #[inline]
136    pub fn size(&self) -> Vec3 {
137        self.max - self.min
138    }
139
140    /// Checks if the `Aabb` is valid (i.e., `min` <= `max` on all axes).
141    /// Degenerate boxes where `min == max` are considered valid.
142    #[inline]
143    pub fn is_valid(&self) -> bool {
144        self.min.x <= self.max.x && self.min.y <= self.max.y && self.min.z <= self.max.z
145    }
146
147    /// Checks if a point is contained within or on the boundary of the `Aabb`.
148    #[inline]
149    pub fn contains_point(&self, point: Vec3) -> bool {
150        point.x >= self.min.x
151            && point.x <= self.max.x
152            && point.y >= self.min.y
153            && point.y <= self.max.y
154            && point.z >= self.min.z
155            && point.z <= self.max.z
156    }
157
158    /// Checks if this `Aabb` intersects with another `Aabb`.
159    ///
160    /// Two `Aabb`s intersect if they overlap on all three axes. Boxes that only
161    /// touch at the boundary are considered to be intersecting.
162    #[inline]
163    pub fn intersects_aabb(&self, other: &Aabb) -> bool {
164        (self.min.x <= other.max.x && self.max.x >= other.min.x)
165            && (self.min.y <= other.max.y && self.max.y >= other.min.y)
166            && (self.min.z <= other.max.z && self.max.z >= other.min.z)
167    }
168
169    /// Creates a new `Aabb` that encompasses both this `Aabb` and another one.
170    #[inline]
171    pub fn merge(&self, other: &Aabb) -> Self {
172        Self {
173            min: Vec3::new(
174                self.min.x.min(other.min.x),
175                self.min.y.min(other.min.y),
176                self.min.z.min(other.min.z),
177            ),
178            max: Vec3::new(
179                self.max.x.max(other.max.x),
180                self.max.y.max(other.max.y),
181                self.max.z.max(other.max.z),
182            ),
183        }
184    }
185
186    /// Creates a new `Aabb` that encompasses both this `Aabb` and an additional point.
187    #[inline]
188    pub fn merged_with_point(&self, point: Vec3) -> Self {
189        Self {
190            min: Vec3::new(
191                self.min.x.min(point.x),
192                self.min.y.min(point.y),
193                self.min.z.min(point.z),
194            ),
195            max: Vec3::new(
196                self.max.x.max(point.x),
197                self.max.y.max(point.y),
198                self.max.z.max(point.z),
199            ),
200        }
201    }
202
203    /// Computes the bounding box that encloses this `Aabb` after a transformation.
204    ///
205    /// This method is more efficient than transforming all 8 corners of the box for
206    /// affine transformations. It works by transforming the center of the box and
207    /// then calculating the new extents by projecting the original extents onto
208    /// the axes of the transformed space.
209    ///
210    /// # Note
211    /// This method is designed for affine transformations (like rotation, translation, scale).
212    /// It may not produce the tightest-fitting box for transformations involving perspective.
213    pub fn transform(&self, matrix: &Mat4) -> Self {
214        let center = self.center();
215        let half_extents = self.half_extents();
216        let transformed_center_v4 = *matrix * Vec4::from_vec3(center, 1.0);
217
218        let transformed_center = if (transformed_center_v4.w - 1.0).abs() > EPSILON
219            && transformed_center_v4.w.abs() > EPSILON
220        {
221            transformed_center_v4.truncate() / transformed_center_v4.w
222        } else {
223            transformed_center_v4.truncate()
224        };
225
226        let x_abs_col = Vec3::new(
227            matrix.cols[0][0].abs(),
228            matrix.cols[0][1].abs(),
229            matrix.cols[0][2].abs(),
230        );
231        let y_abs_col = Vec3::new(
232            matrix.cols[1][0].abs(),
233            matrix.cols[1][1].abs(),
234            matrix.cols[1][2].abs(),
235        );
236        let z_abs_col = Vec3::new(
237            matrix.cols[2][0].abs(),
238            matrix.cols[2][1].abs(),
239            matrix.cols[2][2].abs(),
240        );
241
242        let new_half_extents =
243            x_abs_col * half_extents.x + y_abs_col * half_extents.y + z_abs_col * half_extents.z;
244
245        Aabb::from_center_half_extents(transformed_center, new_half_extents)
246    }
247}
248
249impl Default for Aabb {
250    /// Returns the default `Aabb`, which is `Aabb::INVALID`.
251    #[inline]
252    fn default() -> Self {
253        Self::INVALID
254    }
255}
256
257// --- Tests ---
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::math::{approx_eq, matrix::Mat4, vector::Vec3}; // Use helpers from parent
262    use std::f32::consts::PI;
263
264    fn vec3_approx_eq(a: Vec3, b: Vec3) -> bool {
265        approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z)
266    }
267
268    // Helper for AABB comparison
269    fn aabb_approx_eq(a: Aabb, b: Aabb) -> bool {
270        vec3_approx_eq(a.min, b.min) && vec3_approx_eq(a.max, b.max)
271    }
272
273    #[test]
274    fn test_aabb_from_min_max() {
275        let aabb = Aabb::from_min_max(Vec3::new(1.0, 2.0, 3.0), Vec3::new(4.0, 5.0, 6.0));
276        assert_eq!(aabb.min, Vec3::new(1.0, 2.0, 3.0));
277        assert_eq!(aabb.max, Vec3::new(4.0, 5.0, 6.0));
278
279        // Test swapped min/max
280        let aabb_swapped = Aabb::from_min_max(Vec3::new(4.0, 5.0, 6.0), Vec3::new(1.0, 2.0, 3.0));
281        assert_eq!(aabb_swapped.min, Vec3::new(1.0, 2.0, 3.0));
282        assert_eq!(aabb_swapped.max, Vec3::new(4.0, 5.0, 6.0));
283    }
284
285    #[test]
286    fn test_aabb_from_center_half_extents() {
287        let center = Vec3::new(10.0, 20.0, 30.0);
288        let half_extents = Vec3::new(1.0, 2.0, 3.0);
289        let aabb = Aabb::from_center_half_extents(center, half_extents);
290
291        assert_eq!(aabb.min, Vec3::new(9.0, 18.0, 27.0));
292        assert_eq!(aabb.max, Vec3::new(11.0, 22.0, 33.0));
293        assert!(aabb_approx_eq(aabb, Aabb::from_min_max(aabb.min, aabb.max)));
294    }
295
296    #[test]
297    fn test_aabb_from_point() {
298        let p = Vec3::new(5.0, 6.0, 7.0);
299        let aabb = Aabb::from_point(p);
300
301        assert_eq!(aabb.min, p);
302        assert_eq!(aabb.max, p);
303        assert!(aabb.is_valid());
304    }
305
306    #[test]
307    fn test_aabb_from_points() {
308        assert!(Aabb::from_points(&[]).is_none());
309
310        let points = [
311            Vec3::new(1.0, 5.0, -1.0),
312            Vec3::new(0.0, 2.0, 3.0),
313            Vec3::new(4.0, 8.0, 0.0),
314        ];
315        let aabb = Aabb::from_points(&points).unwrap();
316
317        assert_eq!(aabb.min, Vec3::new(0.0, 2.0, -1.0));
318        assert_eq!(aabb.max, Vec3::new(4.0, 8.0, 3.0));
319    }
320
321    #[test]
322    fn test_aabb_utils() {
323        let aabb = Aabb::from_min_max(Vec3::new(-1.0, 0.0, 1.0), Vec3::new(3.0, 2.0, 5.0));
324
325        assert!(vec3_approx_eq(aabb.center(), Vec3::new(1.0, 1.0, 3.0)));
326        assert!(vec3_approx_eq(aabb.size(), Vec3::new(4.0, 2.0, 4.0)));
327        assert!(vec3_approx_eq(
328            aabb.half_extents(),
329            Vec3::new(2.0, 1.0, 2.0)
330        ));
331        assert!(aabb.is_valid());
332        assert!(!Aabb::INVALID.is_valid());
333        assert!(Aabb::from_point(Vec3::ZERO).is_valid());
334    }
335
336    #[test]
337    fn test_aabb_contains_point() {
338        let aabb = Aabb::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0));
339        // Inside
340        assert!(aabb.contains_point(Vec3::new(0.5, 0.5, 0.5)));
341
342        // On boundary
343        assert!(aabb.contains_point(Vec3::new(0.0, 0.5, 0.5)));
344        assert!(aabb.contains_point(Vec3::new(1.0, 0.5, 0.5)));
345        assert!(aabb.contains_point(Vec3::new(0.5, 0.0, 0.5)));
346        assert!(aabb.contains_point(Vec3::new(0.5, 1.0, 0.5)));
347        assert!(aabb.contains_point(Vec3::new(0.5, 0.5, 0.0)));
348        assert!(aabb.contains_point(Vec3::new(0.5, 0.5, 1.0)));
349        assert!(aabb.contains_point(Vec3::new(0.0, 0.0, 0.0)));
350        assert!(aabb.contains_point(Vec3::new(1.0, 1.0, 1.0)));
351
352        // Outside
353        assert!(!aabb.contains_point(Vec3::new(1.1, 0.5, 0.5)));
354        assert!(!aabb.contains_point(Vec3::new(-0.1, 0.5, 0.5)));
355        assert!(!aabb.contains_point(Vec3::new(0.5, 1.1, 0.5)));
356        assert!(!aabb.contains_point(Vec3::new(0.5, -0.1, 0.5)));
357        assert!(!aabb.contains_point(Vec3::new(0.5, 0.5, 1.1)));
358        assert!(!aabb.contains_point(Vec3::new(0.5, 0.5, -0.1)));
359    }
360
361    #[test]
362    fn test_aabb_intersects_aabb() {
363        let aabb1 = Aabb::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
364
365        // Identical
366        let aabb2 = Aabb::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
367        assert!(aabb1.intersects_aabb(&aabb2));
368
369        // Overlapping
370        let aabb3 = Aabb::from_min_max(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 3.0, 3.0));
371        assert!(aabb1.intersects_aabb(&aabb3));
372        assert!(aabb3.intersects_aabb(&aabb1));
373
374        // Touching boundary
375        let aabb4 = Aabb::from_min_max(Vec3::new(2.0, 0.0, 0.0), Vec3::new(3.0, 2.0, 2.0));
376        assert!(aabb1.intersects_aabb(&aabb4));
377        assert!(aabb4.intersects_aabb(&aabb1));
378
379        // Containing
380        let aabb5 = Aabb::from_min_max(Vec3::new(0.5, 0.5, 0.5), Vec3::new(1.5, 1.5, 1.5));
381        assert!(aabb1.intersects_aabb(&aabb5));
382        assert!(aabb5.intersects_aabb(&aabb1));
383
384        // Non-overlapping X
385        let aabb6 = Aabb::from_min_max(Vec3::new(2.1, 0.0, 0.0), Vec3::new(3.0, 2.0, 2.0));
386        assert!(!aabb1.intersects_aabb(&aabb6));
387        assert!(!aabb6.intersects_aabb(&aabb1));
388
389        // Non-overlapping Y
390        let aabb7 = Aabb::from_min_max(Vec3::new(0.0, 2.1, 0.0), Vec3::new(2.0, 3.0, 2.0));
391        assert!(!aabb1.intersects_aabb(&aabb7));
392        assert!(!aabb7.intersects_aabb(&aabb1));
393
394        // Non-overlapping Z
395        let aabb8 = Aabb::from_min_max(Vec3::new(0.0, 0.0, 2.1), Vec3::new(2.0, 2.0, 3.0));
396        assert!(!aabb1.intersects_aabb(&aabb8));
397        assert!(!aabb8.intersects_aabb(&aabb1));
398    }
399
400    #[test]
401    fn test_aabb_merge() {
402        let aabb1 = Aabb::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0));
403        let aabb2 = Aabb::from_min_max(Vec3::new(0.5, 0.5, 0.5), Vec3::new(1.5, 1.5, 1.5));
404        let merged_aabb = aabb1.merge(&aabb2);
405
406        assert_eq!(merged_aabb.min, Vec3::new(0.0, 0.0, 0.0));
407        assert_eq!(merged_aabb.max, Vec3::new(1.5, 1.5, 1.5));
408
409        let point = Vec3::new(-1.0, 0.5, 2.0);
410        let merged_point = aabb1.merged_with_point(point);
411
412        assert_eq!(merged_point.min, Vec3::new(-1.0, 0.0, 0.0));
413        assert_eq!(merged_point.max, Vec3::new(1.0, 1.0, 2.0));
414
415        // Test merging with invalid starts correctly
416        let merged_with_invalid = Aabb::INVALID.merge(&aabb1);
417        assert!(aabb_approx_eq(merged_with_invalid, aabb1));
418
419        let merged_with_invalid_pt = Aabb::INVALID.merged_with_point(point);
420        assert!(aabb_approx_eq(
421            merged_with_invalid_pt,
422            Aabb::from_point(point)
423        ));
424    }
425
426    #[test]
427    fn test_aabb_transform() {
428        let aabb = Aabb::from_min_max(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0)); // Unit cube centered at origin
429        let matrix = Mat4::from_translation(Vec3::new(10.0, 0.0, 0.0)); // Translate +10 on X
430        let transformed_aabb = aabb.transform(&matrix);
431        let expected_aabb =
432            Aabb::from_min_max(Vec3::new(9.0, -1.0, -1.0), Vec3::new(11.0, 1.0, 1.0));
433
434        assert!(aabb_approx_eq(transformed_aabb, expected_aabb));
435
436        // Test with rotation (resulting AABB will be larger)
437        let matrix_rot = Mat4::from_rotation_y(PI / 4.0); // Rotate 45 deg around Y
438        let transformed_rot_aabb = aabb.transform(&matrix_rot);
439
440        // The exact min/max are harder to calculate manually, but it should contain the original corners rotated
441        // Max extent along X/Z should now be sqrt(1^2 + 1^2) = sqrt(2)
442        let sqrt2 = 2.0f32.sqrt();
443
444        assert!(approx_eq(transformed_rot_aabb.min.x, -sqrt2));
445        assert!(approx_eq(transformed_rot_aabb.max.x, sqrt2));
446        assert!(approx_eq(transformed_rot_aabb.min.y, -1.0)); // Y extent shouldn't change
447        assert!(approx_eq(transformed_rot_aabb.max.y, 1.0));
448        assert!(approx_eq(transformed_rot_aabb.min.z, -sqrt2));
449        assert!(approx_eq(transformed_rot_aabb.max.z, sqrt2));
450
451        // Test with scaling
452        let matrix_scale = Mat4::from_scale(Vec3::new(2.0, 1.0, 0.5));
453        let transformed_scale_aabb = aabb.transform(&matrix_scale);
454        let expected_scale_aabb =
455            Aabb::from_min_max(Vec3::new(-2.0, -1.0, -0.5), Vec3::new(2.0, 1.0, 0.5));
456
457        assert!(aabb_approx_eq(transformed_scale_aabb, expected_scale_aabb));
458    }
459}