Skip to main content

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, Vec2, Vec3, Vec4, EPSILON};
21
22/// A 2D axis-aligned rectangle defined by its top-left corner (`min`)
23/// and bottom-right corner (`max`).
24///
25/// Mirrors the design of [`Aabb`] for 2D — used by UI surfaces (panels,
26/// painters, hit-tests). Y axis points downward, matching most window
27/// systems.
28#[derive(Debug, Clone, Copy, PartialEq, Default)]
29#[repr(C)]
30pub struct Rect2D {
31    /// Top-left corner (smallest x, smallest y).
32    pub min: Vec2,
33    /// Bottom-right corner (largest x, largest y).
34    pub max: Vec2,
35}
36
37impl Rect2D {
38    /// A zero-sized rectangle anchored at the origin.
39    pub const ZERO: Self = Self {
40        min: Vec2::ZERO,
41        max: Vec2::ZERO,
42    };
43
44    /// Creates a rectangle from its `min` and `max` corners.
45    pub const fn from_min_max(min: Vec2, max: Vec2) -> Self {
46        Self { min, max }
47    }
48
49    /// Creates a rectangle from a top-left corner and a size.
50    pub const fn from_min_size(min: Vec2, size: Vec2) -> Self {
51        Self {
52            min,
53            max: Vec2::new(min.x + size.x, min.y + size.y),
54        }
55    }
56
57    /// Creates a rectangle centered on `center` with the given `size`.
58    pub fn from_center_size(center: Vec2, size: Vec2) -> Self {
59        let half = Vec2::new(size.x * 0.5, size.y * 0.5);
60        Self {
61            min: Vec2::new(center.x - half.x, center.y - half.y),
62            max: Vec2::new(center.x + half.x, center.y + half.y),
63        }
64    }
65
66    /// Width of the rectangle.
67    pub fn width(&self) -> f32 {
68        self.max.x - self.min.x
69    }
70
71    /// Height of the rectangle.
72    pub fn height(&self) -> f32 {
73        self.max.y - self.min.y
74    }
75
76    /// Size as a [`Vec2`] (`width`, `height`).
77    pub fn size(&self) -> Vec2 {
78        Vec2::new(self.width(), self.height())
79    }
80
81    /// Center point of the rectangle.
82    pub fn center(&self) -> Vec2 {
83        Vec2::new(
84            (self.min.x + self.max.x) * 0.5,
85            (self.min.y + self.max.y) * 0.5,
86        )
87    }
88
89    /// Returns `true` if `point` lies inside this rectangle (inclusive
90    /// of `min`, exclusive of `max` — typical hit-test convention).
91    pub fn contains(&self, point: Vec2) -> bool {
92        point.x >= self.min.x
93            && point.x < self.max.x
94            && point.y >= self.min.y
95            && point.y < self.max.y
96    }
97}
98
99/// Represents an Axis-Aligned Bounding Box (AABB).
100///
101/// An AABB is a rectangular prism aligned with the coordinate axes, defined by its
102/// minimum and maximum corner points. It is a simple but highly efficient volume
103/// for broad-phase collision detection and visibility culling.
104#[derive(Debug, Clone, Copy, PartialEq)]
105#[repr(C)]
106pub struct Aabb {
107    /// The corner of the box with the smallest coordinates on all axes.
108    pub min: Vec3,
109    /// The corner of the box with the largest coordinates on all axes.
110    pub max: Vec3,
111}
112
113impl Aabb {
114    /// An invalid `Aabb` where `min` components are positive infinity and `max` are negative infinity.
115    ///
116    /// This is useful as a neutral starting point for merging operations. Merging any
117    /// valid `Aabb` with `INVALID` will result in that valid `Aabb`.
118    pub const INVALID: Self = Self {
119        min: Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY),
120        max: Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY),
121    };
122
123    /// Creates a new `Aabb` from two corner points.
124    ///
125    /// This constructor automatically ensures that the `min` field holds the
126    /// component-wise minimum and `max` holds the component-wise maximum,
127    /// regardless of the order the points are passed in.
128    #[inline]
129    pub fn from_min_max(min_pt: Vec3, max_pt: Vec3) -> Self {
130        Self {
131            min: Vec3::new(
132                min_pt.x.min(max_pt.x),
133                min_pt.y.min(max_pt.y),
134                min_pt.z.min(max_pt.z),
135            ),
136            max: Vec3::new(
137                min_pt.x.max(max_pt.x),
138                min_pt.y.max(max_pt.y),
139                min_pt.z.max(max_pt.z),
140            ),
141        }
142    }
143
144    /// Creates a new `Aabb` from a center point and its half-extents.
145    ///
146    /// The half-extents represent the distance from the center to the faces of the box.
147    /// The provided `half_extents` will be made non-negative.
148    #[inline]
149    pub fn from_center_half_extents(center: Vec3, half_extents: Vec3) -> Self {
150        let safe_half_extents = half_extents.abs();
151        Self {
152            min: center - safe_half_extents,
153            max: center + safe_half_extents,
154        }
155    }
156
157    /// Creates a new `Aabb` centered at the origin with the given half-extents.
158    #[inline]
159    pub fn from_half_extents(half_extents: Vec3) -> Self {
160        let safe_half_extents = half_extents.abs();
161        Self {
162            min: -safe_half_extents,
163            max: safe_half_extents,
164        }
165    }
166
167    /// Creates a degenerate `Aabb` containing a single point (min and max are the same).
168    #[inline]
169    pub fn from_point(point: Vec3) -> Self {
170        Self {
171            min: point,
172            max: point,
173        }
174    }
175
176    /// Creates an `Aabb` that tightly encloses a given set of points.
177    ///
178    /// # Returns
179    ///
180    /// Returns `Some(Aabb)` if the input slice is not empty, otherwise `None`.
181    pub fn from_points(points: &[Vec3]) -> Option<Self> {
182        if points.is_empty() {
183            return None;
184        }
185
186        let mut min_pt = points[0];
187        let mut max_pt = points[0];
188
189        for point in points.iter().skip(1) {
190            min_pt.x = min_pt.x.min(point.x);
191            min_pt.y = min_pt.y.min(point.y);
192            min_pt.z = min_pt.z.min(point.z);
193
194            max_pt.x = max_pt.x.max(point.x);
195            max_pt.y = max_pt.y.max(point.y);
196            max_pt.z = max_pt.z.max(point.z);
197        }
198
199        Some(Self {
200            min: min_pt,
201            max: max_pt,
202        })
203    }
204
205    /// Calculates the center point of the `Aabb`.
206    #[inline]
207    pub fn center(&self) -> Vec3 {
208        (self.min + self.max) * 0.5
209    }
210
211    /// Calculates the half-extents (half the size on each axis) of the `Aabb`.
212    #[inline]
213    pub fn half_extents(&self) -> Vec3 {
214        (self.max - self.min) * 0.5
215    }
216
217    /// Calculates the full size (width, height, depth) of the `Aabb`.
218    #[inline]
219    pub fn size(&self) -> Vec3 {
220        self.max - self.min
221    }
222
223    /// Checks if the `Aabb` is valid (i.e., `min` <= `max` on all axes).
224    /// Degenerate boxes where `min == max` are considered valid.
225    #[inline]
226    pub fn is_valid(&self) -> bool {
227        self.min.x <= self.max.x && self.min.y <= self.max.y && self.min.z <= self.max.z
228    }
229
230    /// Checks if a point is contained within or on the boundary of the `Aabb`.
231    #[inline]
232    pub fn contains_point(&self, point: Vec3) -> bool {
233        point.x >= self.min.x
234            && point.x <= self.max.x
235            && point.y >= self.min.y
236            && point.y <= self.max.y
237            && point.z >= self.min.z
238            && point.z <= self.max.z
239    }
240
241    /// Checks if this `Aabb` intersects with another `Aabb`.
242    ///
243    /// Two `Aabb`s intersect if they overlap on all three axes. Boxes that only
244    /// touch at the boundary are considered to be intersecting.
245    #[inline]
246    pub fn intersects_aabb(&self, other: &Aabb) -> bool {
247        (self.min.x <= other.max.x && self.max.x >= other.min.x)
248            && (self.min.y <= other.max.y && self.max.y >= other.min.y)
249            && (self.min.z <= other.max.z && self.max.z >= other.min.z)
250    }
251
252    /// Creates a new `Aabb` that encompasses both this `Aabb` and another one.
253    #[inline]
254    pub fn merge(&self, other: &Aabb) -> Self {
255        Self {
256            min: Vec3::new(
257                self.min.x.min(other.min.x),
258                self.min.y.min(other.min.y),
259                self.min.z.min(other.min.z),
260            ),
261            max: Vec3::new(
262                self.max.x.max(other.max.x),
263                self.max.y.max(other.max.y),
264                self.max.z.max(other.max.z),
265            ),
266        }
267    }
268
269    /// Creates a new `Aabb` that encompasses both this `Aabb` and an additional point.
270    #[inline]
271    pub fn merged_with_point(&self, point: Vec3) -> Self {
272        Self {
273            min: Vec3::new(
274                self.min.x.min(point.x),
275                self.min.y.min(point.y),
276                self.min.z.min(point.z),
277            ),
278            max: Vec3::new(
279                self.max.x.max(point.x),
280                self.max.y.max(point.y),
281                self.max.z.max(point.z),
282            ),
283        }
284    }
285
286    /// Computes the bounding box that encloses this `Aabb` after a transformation.
287    ///
288    /// This method is more efficient than transforming all 8 corners of the box for
289    /// affine transformations. It works by transforming the center of the box and
290    /// then calculating the new extents by projecting the original extents onto
291    /// the axes of the transformed space.
292    ///
293    /// # Note
294    /// This method is designed for affine transformations (like rotation, translation, scale).
295    /// It may not produce the tightest-fitting box for transformations involving perspective.
296    pub fn transform(&self, matrix: &Mat4) -> Self {
297        let center = self.center();
298        let half_extents = self.half_extents();
299        let transformed_center_v4 = *matrix * Vec4::from_vec3(center, 1.0);
300
301        let transformed_center = if (transformed_center_v4.w - 1.0).abs() > EPSILON
302            && transformed_center_v4.w.abs() > EPSILON
303        {
304            transformed_center_v4.truncate() / transformed_center_v4.w
305        } else {
306            transformed_center_v4.truncate()
307        };
308
309        let x_abs_col = Vec3::new(
310            matrix.cols[0][0].abs(),
311            matrix.cols[0][1].abs(),
312            matrix.cols[0][2].abs(),
313        );
314        let y_abs_col = Vec3::new(
315            matrix.cols[1][0].abs(),
316            matrix.cols[1][1].abs(),
317            matrix.cols[1][2].abs(),
318        );
319        let z_abs_col = Vec3::new(
320            matrix.cols[2][0].abs(),
321            matrix.cols[2][1].abs(),
322            matrix.cols[2][2].abs(),
323        );
324
325        let new_half_extents =
326            x_abs_col * half_extents.x + y_abs_col * half_extents.y + z_abs_col * half_extents.z;
327
328        Aabb::from_center_half_extents(transformed_center, new_half_extents)
329    }
330
331    /// Calculates the surface area of the `Aabb`.
332    ///
333    /// Useful for SAH (Surface Area Heuristic) in BVH construction.
334    #[inline]
335    pub fn surface_area(&self) -> f32 {
336        let d = self.max - self.min;
337        2.0 * (d.x * d.y + d.y * d.z + d.z * d.x)
338    }
339
340    /// Checks if this `Aabb` fully contains another `Aabb`.
341    #[inline]
342    pub fn contains_aabb(&self, other: &Aabb) -> bool {
343        self.min.x <= other.min.x
344            && self.max.x >= other.max.x
345            && self.min.y <= other.min.y
346            && self.max.y >= other.max.y
347            && self.min.z <= other.min.z
348            && self.max.z >= other.max.z
349    }
350
351    /// Intersection test against a ray using the Slab Method.
352    ///
353    /// `inv_dir` should be `1.0 / ray.direction`. If a component of direction is zero, `inv_dir` should be infinity.
354    /// Returns the distance to the intersection point if it occurs.
355    pub fn intersect_ray(&self, origin: Vec3, inv_dir: Vec3) -> Option<f32> {
356        let t1 = (self.min.x - origin.x) * inv_dir.x;
357        let t2 = (self.max.x - origin.x) * inv_dir.x;
358        let t3 = (self.min.y - origin.y) * inv_dir.y;
359        let t4 = (self.max.y - origin.y) * inv_dir.y;
360        let t5 = (self.min.z - origin.z) * inv_dir.z;
361        let t6 = (self.max.z - origin.z) * inv_dir.z;
362
363        let tmin = t1.min(t2).max(t3.min(t4)).max(t5.min(t6));
364        let tmax = t1.max(t2).min(t3.max(t4)).min(t5.max(t6));
365
366        if tmax < 0.0 || tmin > tmax {
367            return None;
368        }
369
370        Some(tmin)
371    }
372}
373
374impl Default for Aabb {
375    /// Returns the default `Aabb`, which is `Aabb::INVALID`.
376    #[inline]
377    fn default() -> Self {
378        Self::INVALID
379    }
380}
381
382// --- Tests ---
383#[cfg(test)]
384mod tests {
385    use super::*;
386    use crate::math::{approx_eq, matrix::Mat4, vector::Vec3}; // Use helpers from parent
387    use std::f32::consts::PI;
388
389    fn vec3_approx_eq(a: Vec3, b: Vec3) -> bool {
390        approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z)
391    }
392
393    // Helper for AABB comparison
394    fn aabb_approx_eq(a: Aabb, b: Aabb) -> bool {
395        vec3_approx_eq(a.min, b.min) && vec3_approx_eq(a.max, b.max)
396    }
397
398    #[test]
399    fn test_aabb_from_min_max() {
400        let aabb = Aabb::from_min_max(Vec3::new(1.0, 2.0, 3.0), Vec3::new(4.0, 5.0, 6.0));
401        assert_eq!(aabb.min, Vec3::new(1.0, 2.0, 3.0));
402        assert_eq!(aabb.max, Vec3::new(4.0, 5.0, 6.0));
403
404        // Test swapped min/max
405        let aabb_swapped = Aabb::from_min_max(Vec3::new(4.0, 5.0, 6.0), Vec3::new(1.0, 2.0, 3.0));
406        assert_eq!(aabb_swapped.min, Vec3::new(1.0, 2.0, 3.0));
407        assert_eq!(aabb_swapped.max, Vec3::new(4.0, 5.0, 6.0));
408    }
409
410    #[test]
411    fn test_aabb_from_center_half_extents() {
412        let center = Vec3::new(10.0, 20.0, 30.0);
413        let half_extents = Vec3::new(1.0, 2.0, 3.0);
414        let aabb = Aabb::from_center_half_extents(center, half_extents);
415
416        assert_eq!(aabb.min, Vec3::new(9.0, 18.0, 27.0));
417        assert_eq!(aabb.max, Vec3::new(11.0, 22.0, 33.0));
418        assert!(aabb_approx_eq(aabb, Aabb::from_min_max(aabb.min, aabb.max)));
419    }
420
421    #[test]
422    fn test_aabb_from_point() {
423        let p = Vec3::new(5.0, 6.0, 7.0);
424        let aabb = Aabb::from_point(p);
425
426        assert_eq!(aabb.min, p);
427        assert_eq!(aabb.max, p);
428        assert!(aabb.is_valid());
429    }
430
431    #[test]
432    fn test_aabb_from_points() {
433        assert!(Aabb::from_points(&[]).is_none());
434
435        let points = [
436            Vec3::new(1.0, 5.0, -1.0),
437            Vec3::new(0.0, 2.0, 3.0),
438            Vec3::new(4.0, 8.0, 0.0),
439        ];
440        let aabb = Aabb::from_points(&points).unwrap();
441
442        assert_eq!(aabb.min, Vec3::new(0.0, 2.0, -1.0));
443        assert_eq!(aabb.max, Vec3::new(4.0, 8.0, 3.0));
444    }
445
446    #[test]
447    fn test_aabb_utils() {
448        let aabb = Aabb::from_min_max(Vec3::new(-1.0, 0.0, 1.0), Vec3::new(3.0, 2.0, 5.0));
449
450        assert!(vec3_approx_eq(aabb.center(), Vec3::new(1.0, 1.0, 3.0)));
451        assert!(vec3_approx_eq(aabb.size(), Vec3::new(4.0, 2.0, 4.0)));
452        assert!(vec3_approx_eq(
453            aabb.half_extents(),
454            Vec3::new(2.0, 1.0, 2.0)
455        ));
456        assert!(aabb.is_valid());
457        assert!(!Aabb::INVALID.is_valid());
458        assert!(Aabb::from_point(Vec3::ZERO).is_valid());
459    }
460
461    #[test]
462    fn test_aabb_contains_point() {
463        let aabb = Aabb::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0));
464        // Inside
465        assert!(aabb.contains_point(Vec3::new(0.5, 0.5, 0.5)));
466
467        // On boundary
468        assert!(aabb.contains_point(Vec3::new(0.0, 0.5, 0.5)));
469        assert!(aabb.contains_point(Vec3::new(1.0, 0.5, 0.5)));
470        assert!(aabb.contains_point(Vec3::new(0.5, 0.0, 0.5)));
471        assert!(aabb.contains_point(Vec3::new(0.5, 1.0, 0.5)));
472        assert!(aabb.contains_point(Vec3::new(0.5, 0.5, 0.0)));
473        assert!(aabb.contains_point(Vec3::new(0.5, 0.5, 1.0)));
474        assert!(aabb.contains_point(Vec3::new(0.0, 0.0, 0.0)));
475        assert!(aabb.contains_point(Vec3::new(1.0, 1.0, 1.0)));
476
477        // Outside
478        assert!(!aabb.contains_point(Vec3::new(1.1, 0.5, 0.5)));
479        assert!(!aabb.contains_point(Vec3::new(-0.1, 0.5, 0.5)));
480        assert!(!aabb.contains_point(Vec3::new(0.5, 1.1, 0.5)));
481        assert!(!aabb.contains_point(Vec3::new(0.5, -0.1, 0.5)));
482        assert!(!aabb.contains_point(Vec3::new(0.5, 0.5, 1.1)));
483        assert!(!aabb.contains_point(Vec3::new(0.5, 0.5, -0.1)));
484    }
485
486    #[test]
487    fn test_aabb_intersects_aabb() {
488        let aabb1 = Aabb::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
489
490        // Identical
491        let aabb2 = Aabb::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
492        assert!(aabb1.intersects_aabb(&aabb2));
493
494        // Overlapping
495        let aabb3 = Aabb::from_min_max(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 3.0, 3.0));
496        assert!(aabb1.intersects_aabb(&aabb3));
497        assert!(aabb3.intersects_aabb(&aabb1));
498
499        // Touching boundary
500        let aabb4 = Aabb::from_min_max(Vec3::new(2.0, 0.0, 0.0), Vec3::new(3.0, 2.0, 2.0));
501        assert!(aabb1.intersects_aabb(&aabb4));
502        assert!(aabb4.intersects_aabb(&aabb1));
503
504        // Containing
505        let aabb5 = Aabb::from_min_max(Vec3::new(0.5, 0.5, 0.5), Vec3::new(1.5, 1.5, 1.5));
506        assert!(aabb1.intersects_aabb(&aabb5));
507        assert!(aabb5.intersects_aabb(&aabb1));
508
509        // Non-overlapping X
510        let aabb6 = Aabb::from_min_max(Vec3::new(2.1, 0.0, 0.0), Vec3::new(3.0, 2.0, 2.0));
511        assert!(!aabb1.intersects_aabb(&aabb6));
512        assert!(!aabb6.intersects_aabb(&aabb1));
513
514        // Non-overlapping Y
515        let aabb7 = Aabb::from_min_max(Vec3::new(0.0, 2.1, 0.0), Vec3::new(2.0, 3.0, 2.0));
516        assert!(!aabb1.intersects_aabb(&aabb7));
517        assert!(!aabb7.intersects_aabb(&aabb1));
518
519        // Non-overlapping Z
520        let aabb8 = Aabb::from_min_max(Vec3::new(0.0, 0.0, 2.1), Vec3::new(2.0, 2.0, 3.0));
521        assert!(!aabb1.intersects_aabb(&aabb8));
522        assert!(!aabb8.intersects_aabb(&aabb1));
523    }
524
525    #[test]
526    fn test_aabb_contains_aabb() {
527        let aabb1 = Aabb::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 10.0, 10.0));
528
529        // Fully inside
530        let aabb2 = Aabb::from_min_max(Vec3::new(2.0, 2.0, 2.0), Vec3::new(8.0, 8.0, 8.0));
531        assert!(aabb1.contains_aabb(&aabb2));
532        assert!(!aabb2.contains_aabb(&aabb1));
533
534        // Overlapping but not contained
535        let aabb3 = Aabb::from_min_max(Vec3::new(5.0, 5.0, 5.0), Vec3::new(15.0, 15.0, 15.0));
536        assert!(aabb1.intersects_aabb(&aabb3));
537        assert!(!aabb1.contains_aabb(&aabb3));
538
539        // Identical
540        assert!(aabb1.contains_aabb(&aabb1));
541    }
542
543    #[test]
544    fn test_aabb_merge() {
545        let aabb1 = Aabb::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0));
546        let aabb2 = Aabb::from_min_max(Vec3::new(0.5, 0.5, 0.5), Vec3::new(1.5, 1.5, 1.5));
547        let merged_aabb = aabb1.merge(&aabb2);
548
549        assert_eq!(merged_aabb.min, Vec3::new(0.0, 0.0, 0.0));
550        assert_eq!(merged_aabb.max, Vec3::new(1.5, 1.5, 1.5));
551
552        let point = Vec3::new(-1.0, 0.5, 2.0);
553        let merged_point = aabb1.merged_with_point(point);
554
555        assert_eq!(merged_point.min, Vec3::new(-1.0, 0.0, 0.0));
556        assert_eq!(merged_point.max, Vec3::new(1.0, 1.0, 2.0));
557
558        // Test merging with invalid starts correctly
559        let merged_with_invalid = Aabb::INVALID.merge(&aabb1);
560        assert!(aabb_approx_eq(merged_with_invalid, aabb1));
561
562        let merged_with_invalid_pt = Aabb::INVALID.merged_with_point(point);
563        assert!(aabb_approx_eq(
564            merged_with_invalid_pt,
565            Aabb::from_point(point)
566        ));
567    }
568
569    #[test]
570    fn test_aabb_transform() {
571        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
572        let matrix = Mat4::from_translation(Vec3::new(10.0, 0.0, 0.0)); // Translate +10 on X
573        let transformed_aabb = aabb.transform(&matrix);
574        let expected_aabb =
575            Aabb::from_min_max(Vec3::new(9.0, -1.0, -1.0), Vec3::new(11.0, 1.0, 1.0));
576
577        assert!(aabb_approx_eq(transformed_aabb, expected_aabb));
578
579        // Test with rotation (resulting AABB will be larger)
580        let matrix_rot = Mat4::from_rotation_y(PI / 4.0); // Rotate 45 deg around Y
581        let transformed_rot_aabb = aabb.transform(&matrix_rot);
582
583        // The exact min/max are harder to calculate manually, but it should contain the original corners rotated
584        // Max extent along X/Z should now be sqrt(1^2 + 1^2) = sqrt(2)
585        let sqrt2 = 2.0f32.sqrt();
586
587        assert!(approx_eq(transformed_rot_aabb.min.x, -sqrt2));
588        assert!(approx_eq(transformed_rot_aabb.max.x, sqrt2));
589        assert!(approx_eq(transformed_rot_aabb.min.y, -1.0)); // Y extent shouldn't change
590        assert!(approx_eq(transformed_rot_aabb.max.y, 1.0));
591        assert!(approx_eq(transformed_rot_aabb.min.z, -sqrt2));
592        assert!(approx_eq(transformed_rot_aabb.max.z, sqrt2));
593
594        // Test with scaling
595        let matrix_scale = Mat4::from_scale(Vec3::new(2.0, 1.0, 0.5));
596        let transformed_scale_aabb = aabb.transform(&matrix_scale);
597        let expected_scale_aabb =
598            Aabb::from_min_max(Vec3::new(-2.0, -1.0, -0.5), Vec3::new(2.0, 1.0, 0.5));
599
600        assert!(aabb_approx_eq(transformed_scale_aabb, expected_scale_aabb));
601    }
602}