bevy_math/bounding/bounded3d/
mod.rs

1mod extrusion;
2mod primitive_impls;
3
4use glam::Mat3;
5
6use super::{BoundingVolume, IntersectsVolume};
7use crate::{Quat, Vec3, Vec3A};
8
9#[cfg(feature = "bevy_reflect")]
10use bevy_reflect::Reflect;
11pub use extrusion::BoundedExtrusion;
12
13/// Computes the geometric center of the given set of points.
14#[inline(always)]
15fn point_cloud_3d_center(points: impl Iterator<Item = impl Into<Vec3A>>) -> Vec3A {
16    let (acc, len) = points.fold((Vec3A::ZERO, 0), |(acc, len), point| {
17        (acc + point.into(), len + 1)
18    });
19
20    assert!(
21        len > 0,
22        "cannot compute the center of an empty set of points"
23    );
24    acc / len as f32
25}
26
27/// A trait with methods that return 3D bounded volumes for a shape
28pub trait Bounded3d {
29    /// Get an axis-aligned bounding box for the shape with the given translation and rotation
30    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d;
31    /// Get a bounding sphere for the shape with the given translation and rotation
32    fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere;
33}
34
35/// A 3D axis-aligned bounding box
36#[derive(Clone, Copy, Debug)]
37#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
38pub struct Aabb3d {
39    /// The minimum point of the box
40    pub min: Vec3A,
41    /// The maximum point of the box
42    pub max: Vec3A,
43}
44
45impl Aabb3d {
46    /// Constructs an AABB from its center and half-size.
47    #[inline(always)]
48    pub fn new(center: impl Into<Vec3A>, half_size: impl Into<Vec3A>) -> Self {
49        let (center, half_size) = (center.into(), half_size.into());
50        debug_assert!(half_size.x >= 0.0 && half_size.y >= 0.0 && half_size.z >= 0.0);
51        Self {
52            min: center - half_size,
53            max: center + half_size,
54        }
55    }
56
57    /// Computes the smallest [`Aabb3d`] containing the given set of points,
58    /// transformed by `translation` and `rotation`.
59    ///
60    /// # Panics
61    ///
62    /// Panics if the given set of points is empty.
63    #[inline(always)]
64    pub fn from_point_cloud(
65        translation: impl Into<Vec3A>,
66        rotation: Quat,
67        points: impl Iterator<Item = impl Into<Vec3A>>,
68    ) -> Aabb3d {
69        // Transform all points by rotation
70        let mut iter = points.map(|point| rotation * point.into());
71
72        let first = iter
73            .next()
74            .expect("point cloud must contain at least one point for Aabb3d construction");
75
76        let (min, max) = iter.fold((first, first), |(prev_min, prev_max), point| {
77            (point.min(prev_min), point.max(prev_max))
78        });
79
80        let translation = translation.into();
81        Aabb3d {
82            min: min + translation,
83            max: max + translation,
84        }
85    }
86
87    /// Computes the smallest [`BoundingSphere`] containing this [`Aabb3d`].
88    #[inline(always)]
89    pub fn bounding_sphere(&self) -> BoundingSphere {
90        let radius = self.min.distance(self.max) / 2.0;
91        BoundingSphere::new(self.center(), radius)
92    }
93
94    /// Finds the point on the AABB that is closest to the given `point`.
95    ///
96    /// If the point is outside the AABB, the returned point will be on the surface of the AABB.
97    /// Otherwise, it will be inside the AABB and returned as is.
98    #[inline(always)]
99    pub fn closest_point(&self, point: impl Into<Vec3A>) -> Vec3A {
100        // Clamp point coordinates to the AABB
101        point.into().clamp(self.min, self.max)
102    }
103}
104
105impl BoundingVolume for Aabb3d {
106    type Translation = Vec3A;
107    type Rotation = Quat;
108    type HalfSize = Vec3A;
109
110    #[inline(always)]
111    fn center(&self) -> Self::Translation {
112        (self.min + self.max) / 2.
113    }
114
115    #[inline(always)]
116    fn half_size(&self) -> Self::HalfSize {
117        (self.max - self.min) / 2.
118    }
119
120    #[inline(always)]
121    fn visible_area(&self) -> f32 {
122        let b = self.max - self.min;
123        b.x * (b.y + b.z) + b.y * b.z
124    }
125
126    #[inline(always)]
127    fn contains(&self, other: &Self) -> bool {
128        other.min.cmpge(self.min).all() && other.max.cmple(self.max).all()
129    }
130
131    #[inline(always)]
132    fn merge(&self, other: &Self) -> Self {
133        Self {
134            min: self.min.min(other.min),
135            max: self.max.max(other.max),
136        }
137    }
138
139    #[inline(always)]
140    fn grow(&self, amount: impl Into<Self::HalfSize>) -> Self {
141        let amount = amount.into();
142        let b = Self {
143            min: self.min - amount,
144            max: self.max + amount,
145        };
146        debug_assert!(b.min.cmple(b.max).all());
147        b
148    }
149
150    #[inline(always)]
151    fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {
152        let amount = amount.into();
153        let b = Self {
154            min: self.min + amount,
155            max: self.max - amount,
156        };
157        debug_assert!(b.min.cmple(b.max).all());
158        b
159    }
160
161    #[inline(always)]
162    fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
163        let scale = scale.into();
164        let b = Self {
165            min: self.center() - (self.half_size() * scale),
166            max: self.center() + (self.half_size() * scale),
167        };
168        debug_assert!(b.min.cmple(b.max).all());
169        b
170    }
171
172    /// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
173    ///
174    /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
175    ///
176    /// Note that the result may not be as tightly fitting as the original, and repeated rotations
177    /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
178    /// and consider storing the original AABB and rotating that every time instead.
179    #[inline(always)]
180    fn transformed_by(
181        mut self,
182        translation: impl Into<Self::Translation>,
183        rotation: impl Into<Self::Rotation>,
184    ) -> Self {
185        self.transform_by(translation, rotation);
186        self
187    }
188
189    /// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
190    ///
191    /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
192    ///
193    /// Note that the result may not be as tightly fitting as the original, and repeated rotations
194    /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
195    /// and consider storing the original AABB and rotating that every time instead.
196    #[inline(always)]
197    fn transform_by(
198        &mut self,
199        translation: impl Into<Self::Translation>,
200        rotation: impl Into<Self::Rotation>,
201    ) {
202        self.rotate_by(rotation);
203        self.translate_by(translation);
204    }
205
206    #[inline(always)]
207    fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
208        let translation = translation.into();
209        self.min += translation;
210        self.max += translation;
211    }
212
213    /// Rotates the bounding volume around the origin by the given rotation.
214    ///
215    /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
216    ///
217    /// Note that the result may not be as tightly fitting as the original, and repeated rotations
218    /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
219    /// and consider storing the original AABB and rotating that every time instead.
220    #[inline(always)]
221    fn rotated_by(mut self, rotation: impl Into<Self::Rotation>) -> Self {
222        self.rotate_by(rotation);
223        self
224    }
225
226    /// Rotates the bounding volume around the origin by the given rotation.
227    ///
228    /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
229    ///
230    /// Note that the result may not be as tightly fitting as the original, and repeated rotations
231    /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
232    /// and consider storing the original AABB and rotating that every time instead.
233    #[inline(always)]
234    fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
235        let rot_mat = Mat3::from_quat(rotation.into());
236        let abs_rot_mat = Mat3::from_cols(
237            rot_mat.x_axis.abs(),
238            rot_mat.y_axis.abs(),
239            rot_mat.z_axis.abs(),
240        );
241        let half_size = abs_rot_mat * self.half_size();
242        *self = Self::new(rot_mat * self.center(), half_size);
243    }
244}
245
246impl IntersectsVolume<Self> for Aabb3d {
247    #[inline(always)]
248    fn intersects(&self, other: &Self) -> bool {
249        self.min.cmple(other.max).all() && self.max.cmpge(other.min).all()
250    }
251}
252
253impl IntersectsVolume<BoundingSphere> for Aabb3d {
254    #[inline(always)]
255    fn intersects(&self, sphere: &BoundingSphere) -> bool {
256        let closest_point = self.closest_point(sphere.center);
257        let distance_squared = sphere.center.distance_squared(closest_point);
258        let radius_squared = sphere.radius().powi(2);
259        distance_squared <= radius_squared
260    }
261}
262
263#[cfg(test)]
264mod aabb3d_tests {
265    use super::Aabb3d;
266    use crate::{
267        bounding::{BoundingSphere, BoundingVolume, IntersectsVolume},
268        Quat, Vec3, Vec3A,
269    };
270
271    #[test]
272    fn center() {
273        let aabb = Aabb3d {
274            min: Vec3A::new(-0.5, -1., -0.5),
275            max: Vec3A::new(1., 1., 2.),
276        };
277        assert!((aabb.center() - Vec3A::new(0.25, 0., 0.75)).length() < f32::EPSILON);
278        let aabb = Aabb3d {
279            min: Vec3A::new(5., 5., -10.),
280            max: Vec3A::new(10., 10., -5.),
281        };
282        assert!((aabb.center() - Vec3A::new(7.5, 7.5, -7.5)).length() < f32::EPSILON);
283    }
284
285    #[test]
286    fn half_size() {
287        let aabb = Aabb3d {
288            min: Vec3A::new(-0.5, -1., -0.5),
289            max: Vec3A::new(1., 1., 2.),
290        };
291        assert!((aabb.half_size() - Vec3A::new(0.75, 1., 1.25)).length() < f32::EPSILON);
292    }
293
294    #[test]
295    fn area() {
296        let aabb = Aabb3d {
297            min: Vec3A::new(-1., -1., -1.),
298            max: Vec3A::new(1., 1., 1.),
299        };
300        assert!((aabb.visible_area() - 12.).abs() < f32::EPSILON);
301        let aabb = Aabb3d {
302            min: Vec3A::new(0., 0., 0.),
303            max: Vec3A::new(1., 0.5, 0.25),
304        };
305        assert!((aabb.visible_area() - 0.875).abs() < f32::EPSILON);
306    }
307
308    #[test]
309    fn contains() {
310        let a = Aabb3d {
311            min: Vec3A::new(-1., -1., -1.),
312            max: Vec3A::new(1., 1., 1.),
313        };
314        let b = Aabb3d {
315            min: Vec3A::new(-2., -1., -1.),
316            max: Vec3A::new(1., 1., 1.),
317        };
318        assert!(!a.contains(&b));
319        let b = Aabb3d {
320            min: Vec3A::new(-0.25, -0.8, -0.9),
321            max: Vec3A::new(1., 1., 0.9),
322        };
323        assert!(a.contains(&b));
324    }
325
326    #[test]
327    fn merge() {
328        let a = Aabb3d {
329            min: Vec3A::new(-1., -1., -1.),
330            max: Vec3A::new(1., 0.5, 1.),
331        };
332        let b = Aabb3d {
333            min: Vec3A::new(-2., -0.5, -0.),
334            max: Vec3A::new(0.75, 1., 2.),
335        };
336        let merged = a.merge(&b);
337        assert!((merged.min - Vec3A::new(-2., -1., -1.)).length() < f32::EPSILON);
338        assert!((merged.max - Vec3A::new(1., 1., 2.)).length() < f32::EPSILON);
339        assert!(merged.contains(&a));
340        assert!(merged.contains(&b));
341        assert!(!a.contains(&merged));
342        assert!(!b.contains(&merged));
343    }
344
345    #[test]
346    fn grow() {
347        let a = Aabb3d {
348            min: Vec3A::new(-1., -1., -1.),
349            max: Vec3A::new(1., 1., 1.),
350        };
351        let padded = a.grow(Vec3A::ONE);
352        assert!((padded.min - Vec3A::new(-2., -2., -2.)).length() < f32::EPSILON);
353        assert!((padded.max - Vec3A::new(2., 2., 2.)).length() < f32::EPSILON);
354        assert!(padded.contains(&a));
355        assert!(!a.contains(&padded));
356    }
357
358    #[test]
359    fn shrink() {
360        let a = Aabb3d {
361            min: Vec3A::new(-2., -2., -2.),
362            max: Vec3A::new(2., 2., 2.),
363        };
364        let shrunk = a.shrink(Vec3A::ONE);
365        assert!((shrunk.min - Vec3A::new(-1., -1., -1.)).length() < f32::EPSILON);
366        assert!((shrunk.max - Vec3A::new(1., 1., 1.)).length() < f32::EPSILON);
367        assert!(a.contains(&shrunk));
368        assert!(!shrunk.contains(&a));
369    }
370
371    #[test]
372    fn scale_around_center() {
373        let a = Aabb3d {
374            min: Vec3A::NEG_ONE,
375            max: Vec3A::ONE,
376        };
377        let scaled = a.scale_around_center(Vec3A::splat(2.));
378        assert!((scaled.min - Vec3A::splat(-2.)).length() < f32::EPSILON);
379        assert!((scaled.max - Vec3A::splat(2.)).length() < f32::EPSILON);
380        assert!(!a.contains(&scaled));
381        assert!(scaled.contains(&a));
382    }
383
384    #[test]
385    fn transform() {
386        let a = Aabb3d {
387            min: Vec3A::new(-2.0, -2.0, -2.0),
388            max: Vec3A::new(2.0, 2.0, 2.0),
389        };
390        let transformed = a.transformed_by(
391            Vec3A::new(2.0, -2.0, 4.0),
392            Quat::from_rotation_z(std::f32::consts::FRAC_PI_4),
393        );
394        let half_length = 2_f32.hypot(2.0);
395        assert_eq!(
396            transformed.min,
397            Vec3A::new(2.0 - half_length, -half_length - 2.0, 2.0)
398        );
399        assert_eq!(
400            transformed.max,
401            Vec3A::new(2.0 + half_length, half_length - 2.0, 6.0)
402        );
403    }
404
405    #[test]
406    fn closest_point() {
407        let aabb = Aabb3d {
408            min: Vec3A::NEG_ONE,
409            max: Vec3A::ONE,
410        };
411        assert_eq!(aabb.closest_point(Vec3A::X * 10.0), Vec3A::X);
412        assert_eq!(aabb.closest_point(Vec3A::NEG_ONE * 10.0), Vec3A::NEG_ONE);
413        assert_eq!(
414            aabb.closest_point(Vec3A::new(0.25, 0.1, 0.3)),
415            Vec3A::new(0.25, 0.1, 0.3)
416        );
417    }
418
419    #[test]
420    fn intersect_aabb() {
421        let aabb = Aabb3d {
422            min: Vec3A::NEG_ONE,
423            max: Vec3A::ONE,
424        };
425        assert!(aabb.intersects(&aabb));
426        assert!(aabb.intersects(&Aabb3d {
427            min: Vec3A::splat(0.5),
428            max: Vec3A::splat(2.0),
429        }));
430        assert!(aabb.intersects(&Aabb3d {
431            min: Vec3A::splat(-2.0),
432            max: Vec3A::splat(-0.5),
433        }));
434        assert!(!aabb.intersects(&Aabb3d {
435            min: Vec3A::new(1.1, 0.0, 0.0),
436            max: Vec3A::new(2.0, 0.5, 0.25),
437        }));
438    }
439
440    #[test]
441    fn intersect_bounding_sphere() {
442        let aabb = Aabb3d {
443            min: Vec3A::NEG_ONE,
444            max: Vec3A::ONE,
445        };
446        assert!(aabb.intersects(&BoundingSphere::new(Vec3::ZERO, 1.0)));
447        assert!(aabb.intersects(&BoundingSphere::new(Vec3::ONE * 1.5, 1.0)));
448        assert!(aabb.intersects(&BoundingSphere::new(Vec3::NEG_ONE * 1.5, 1.0)));
449        assert!(!aabb.intersects(&BoundingSphere::new(Vec3::ONE * 1.75, 1.0)));
450    }
451}
452
453use crate::primitives::Sphere;
454
455/// A bounding sphere
456#[derive(Clone, Copy, Debug)]
457#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
458pub struct BoundingSphere {
459    /// The center of the bounding sphere
460    pub center: Vec3A,
461    /// The sphere
462    pub sphere: Sphere,
463}
464
465impl BoundingSphere {
466    /// Constructs a bounding sphere from its center and radius.
467    pub fn new(center: impl Into<Vec3A>, radius: f32) -> Self {
468        debug_assert!(radius >= 0.);
469        Self {
470            center: center.into(),
471            sphere: Sphere { radius },
472        }
473    }
474
475    /// Computes a [`BoundingSphere`] containing the given set of points,
476    /// transformed by `translation` and `rotation`.
477    ///
478    /// The bounding sphere is not guaranteed to be the smallest possible.
479    #[inline(always)]
480    pub fn from_point_cloud(
481        translation: impl Into<Vec3A>,
482        rotation: Quat,
483        points: &[impl Copy + Into<Vec3A>],
484    ) -> BoundingSphere {
485        let center = point_cloud_3d_center(points.iter().map(|v| Into::<Vec3A>::into(*v)));
486        let mut radius_squared: f32 = 0.0;
487
488        for point in points {
489            // Get squared version to avoid unnecessary sqrt calls
490            let distance_squared = Into::<Vec3A>::into(*point).distance_squared(center);
491            if distance_squared > radius_squared {
492                radius_squared = distance_squared;
493            }
494        }
495
496        BoundingSphere::new(
497            rotation * center + translation.into(),
498            radius_squared.sqrt(),
499        )
500    }
501
502    /// Get the radius of the bounding sphere
503    #[inline(always)]
504    pub fn radius(&self) -> f32 {
505        self.sphere.radius
506    }
507
508    /// Computes the smallest [`Aabb3d`] containing this [`BoundingSphere`].
509    #[inline(always)]
510    pub fn aabb_3d(&self) -> Aabb3d {
511        Aabb3d {
512            min: self.center - self.radius(),
513            max: self.center + self.radius(),
514        }
515    }
516
517    /// Finds the point on the bounding sphere that is closest to the given `point`.
518    ///
519    /// If the point is outside the sphere, the returned point will be on the surface of the sphere.
520    /// Otherwise, it will be inside the sphere and returned as is.
521    #[inline(always)]
522    pub fn closest_point(&self, point: impl Into<Vec3A>) -> Vec3A {
523        let point = point.into();
524        let radius = self.radius();
525        let distance_squared = (point - self.center).length_squared();
526
527        if distance_squared <= radius.powi(2) {
528            // The point is inside the sphere.
529            point
530        } else {
531            // The point is outside the sphere.
532            // Find the closest point on the surface of the sphere.
533            let dir_to_point = point / distance_squared.sqrt();
534            self.center + radius * dir_to_point
535        }
536    }
537}
538
539impl BoundingVolume for BoundingSphere {
540    type Translation = Vec3A;
541    type Rotation = Quat;
542    type HalfSize = f32;
543
544    #[inline(always)]
545    fn center(&self) -> Self::Translation {
546        self.center
547    }
548
549    #[inline(always)]
550    fn half_size(&self) -> Self::HalfSize {
551        self.radius()
552    }
553
554    #[inline(always)]
555    fn visible_area(&self) -> f32 {
556        2. * std::f32::consts::PI * self.radius() * self.radius()
557    }
558
559    #[inline(always)]
560    fn contains(&self, other: &Self) -> bool {
561        let diff = self.radius() - other.radius();
562        self.center.distance_squared(other.center) <= diff.powi(2).copysign(diff)
563    }
564
565    #[inline(always)]
566    fn merge(&self, other: &Self) -> Self {
567        let diff = other.center - self.center;
568        let length = diff.length();
569        if self.radius() >= length + other.radius() {
570            return *self;
571        }
572        if other.radius() >= length + self.radius() {
573            return *other;
574        }
575        let dir = diff / length;
576        Self::new(
577            (self.center + other.center) / 2. + dir * ((other.radius() - self.radius()) / 2.),
578            (length + self.radius() + other.radius()) / 2.,
579        )
580    }
581
582    #[inline(always)]
583    fn grow(&self, amount: impl Into<Self::HalfSize>) -> Self {
584        let amount = amount.into();
585        debug_assert!(amount >= 0.);
586        Self {
587            center: self.center,
588            sphere: Sphere {
589                radius: self.radius() + amount,
590            },
591        }
592    }
593
594    #[inline(always)]
595    fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {
596        let amount = amount.into();
597        debug_assert!(amount >= 0.);
598        debug_assert!(self.radius() >= amount);
599        Self {
600            center: self.center,
601            sphere: Sphere {
602                radius: self.radius() - amount,
603            },
604        }
605    }
606
607    #[inline(always)]
608    fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
609        let scale = scale.into();
610        debug_assert!(scale >= 0.);
611        Self::new(self.center, self.radius() * scale)
612    }
613
614    #[inline(always)]
615    fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
616        self.center += translation.into();
617    }
618
619    #[inline(always)]
620    fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
621        let rotation: Quat = rotation.into();
622        self.center = rotation * self.center;
623    }
624}
625
626impl IntersectsVolume<Self> for BoundingSphere {
627    #[inline(always)]
628    fn intersects(&self, other: &Self) -> bool {
629        let center_distance_squared = self.center.distance_squared(other.center);
630        let radius_sum_squared = (self.radius() + other.radius()).powi(2);
631        center_distance_squared <= radius_sum_squared
632    }
633}
634
635impl IntersectsVolume<Aabb3d> for BoundingSphere {
636    #[inline(always)]
637    fn intersects(&self, aabb: &Aabb3d) -> bool {
638        aabb.intersects(self)
639    }
640}
641
642#[cfg(test)]
643mod bounding_sphere_tests {
644    use approx::assert_relative_eq;
645
646    use super::BoundingSphere;
647    use crate::{
648        bounding::{BoundingVolume, IntersectsVolume},
649        Quat, Vec3, Vec3A,
650    };
651
652    #[test]
653    fn area() {
654        let sphere = BoundingSphere::new(Vec3::ONE, 5.);
655        // Since this number is messy we check it with a higher threshold
656        assert!((sphere.visible_area() - 157.0796).abs() < 0.001);
657    }
658
659    #[test]
660    fn contains() {
661        let a = BoundingSphere::new(Vec3::ONE, 5.);
662        let b = BoundingSphere::new(Vec3::new(5.5, 1., 1.), 1.);
663        assert!(!a.contains(&b));
664        let b = BoundingSphere::new(Vec3::new(1., -3.5, 1.), 0.5);
665        assert!(a.contains(&b));
666    }
667
668    #[test]
669    fn contains_identical() {
670        let a = BoundingSphere::new(Vec3::ONE, 5.);
671        assert!(a.contains(&a));
672    }
673
674    #[test]
675    fn merge() {
676        // When merging two circles that don't contain each other, we find a center position that
677        // contains both
678        let a = BoundingSphere::new(Vec3::ONE, 5.);
679        let b = BoundingSphere::new(Vec3::new(1., 1., -4.), 1.);
680        let merged = a.merge(&b);
681        assert!((merged.center - Vec3A::new(1., 1., 0.5)).length() < f32::EPSILON);
682        assert!((merged.radius() - 5.5).abs() < f32::EPSILON);
683        assert!(merged.contains(&a));
684        assert!(merged.contains(&b));
685        assert!(!a.contains(&merged));
686        assert!(!b.contains(&merged));
687
688        // When one circle contains the other circle, we use the bigger circle
689        let b = BoundingSphere::new(Vec3::ZERO, 3.);
690        assert!(a.contains(&b));
691        let merged = a.merge(&b);
692        assert_eq!(merged.center, a.center);
693        assert_eq!(merged.radius(), a.radius());
694
695        // When two circles are at the same point, we use the bigger radius
696        let b = BoundingSphere::new(Vec3::ONE, 6.);
697        let merged = a.merge(&b);
698        assert_eq!(merged.center, a.center);
699        assert_eq!(merged.radius(), b.radius());
700    }
701
702    #[test]
703    fn merge_identical() {
704        let a = BoundingSphere::new(Vec3::ONE, 5.);
705        let merged = a.merge(&a);
706        assert_eq!(merged.center, a.center);
707        assert_eq!(merged.radius(), a.radius());
708    }
709
710    #[test]
711    fn grow() {
712        let a = BoundingSphere::new(Vec3::ONE, 5.);
713        let padded = a.grow(1.25);
714        assert!((padded.radius() - 6.25).abs() < f32::EPSILON);
715        assert!(padded.contains(&a));
716        assert!(!a.contains(&padded));
717    }
718
719    #[test]
720    fn shrink() {
721        let a = BoundingSphere::new(Vec3::ONE, 5.);
722        let shrunk = a.shrink(0.5);
723        assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON);
724        assert!(a.contains(&shrunk));
725        assert!(!shrunk.contains(&a));
726    }
727
728    #[test]
729    fn scale_around_center() {
730        let a = BoundingSphere::new(Vec3::ONE, 5.);
731        let scaled = a.scale_around_center(2.);
732        assert!((scaled.radius() - 10.).abs() < f32::EPSILON);
733        assert!(!a.contains(&scaled));
734        assert!(scaled.contains(&a));
735    }
736
737    #[test]
738    fn transform() {
739        let a = BoundingSphere::new(Vec3::ONE, 5.0);
740        let transformed = a.transformed_by(
741            Vec3::new(2.0, -2.0, 4.0),
742            Quat::from_rotation_z(std::f32::consts::FRAC_PI_4),
743        );
744        assert_relative_eq!(
745            transformed.center,
746            Vec3A::new(2.0, std::f32::consts::SQRT_2 - 2.0, 5.0)
747        );
748        assert_eq!(transformed.radius(), 5.0);
749    }
750
751    #[test]
752    fn closest_point() {
753        let sphere = BoundingSphere::new(Vec3::ZERO, 1.0);
754        assert_eq!(sphere.closest_point(Vec3::X * 10.0), Vec3A::X);
755        assert_eq!(
756            sphere.closest_point(Vec3::NEG_ONE * 10.0),
757            Vec3A::NEG_ONE.normalize()
758        );
759        assert_eq!(
760            sphere.closest_point(Vec3::new(0.25, 0.1, 0.3)),
761            Vec3A::new(0.25, 0.1, 0.3)
762        );
763    }
764
765    #[test]
766    fn intersect_bounding_sphere() {
767        let sphere = BoundingSphere::new(Vec3::ZERO, 1.0);
768        assert!(sphere.intersects(&BoundingSphere::new(Vec3::ZERO, 1.0)));
769        assert!(sphere.intersects(&BoundingSphere::new(Vec3::ONE * 1.1, 1.0)));
770        assert!(sphere.intersects(&BoundingSphere::new(Vec3::NEG_ONE * 1.1, 1.0)));
771        assert!(!sphere.intersects(&BoundingSphere::new(Vec3::ONE * 1.2, 1.0)));
772    }
773}