bevy_math/bounding/bounded3d/
primitive_impls.rs

1//! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives).
2
3use crate::{
4    bounding::{Bounded2d, BoundingCircle},
5    primitives::{
6        BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d,
7        Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d,
8    },
9    Dir3, Mat3, Quat, Vec2, Vec3,
10};
11
12use super::{Aabb3d, Bounded3d, BoundingSphere};
13
14impl Bounded3d for Sphere {
15    fn aabb_3d(&self, translation: Vec3, _rotation: Quat) -> Aabb3d {
16        Aabb3d::new(translation, Vec3::splat(self.radius))
17    }
18
19    fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
20        BoundingSphere::new(translation, self.radius)
21    }
22}
23
24impl Bounded3d for InfinitePlane3d {
25    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
26        let normal = rotation * *self.normal;
27        let facing_x = normal == Vec3::X || normal == Vec3::NEG_X;
28        let facing_y = normal == Vec3::Y || normal == Vec3::NEG_Y;
29        let facing_z = normal == Vec3::Z || normal == Vec3::NEG_Z;
30
31        // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
32        // like growing or shrinking the AABB without breaking things.
33        let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 };
34        let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 };
35        let half_depth = if facing_z { 0.0 } else { f32::MAX / 2.0 };
36        let half_size = Vec3::new(half_width, half_height, half_depth);
37
38        Aabb3d::new(translation, half_size)
39    }
40
41    fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
42        BoundingSphere::new(translation, f32::MAX / 2.0)
43    }
44}
45
46impl Bounded3d for Line3d {
47    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
48        let direction = rotation * *self.direction;
49
50        // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
51        // like growing or shrinking the AABB without breaking things.
52        let max = f32::MAX / 2.0;
53        let half_width = if direction.x == 0.0 { 0.0 } else { max };
54        let half_height = if direction.y == 0.0 { 0.0 } else { max };
55        let half_depth = if direction.z == 0.0 { 0.0 } else { max };
56        let half_size = Vec3::new(half_width, half_height, half_depth);
57
58        Aabb3d::new(translation, half_size)
59    }
60
61    fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
62        BoundingSphere::new(translation, f32::MAX / 2.0)
63    }
64}
65
66impl Bounded3d for Segment3d {
67    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
68        // Rotate the segment by `rotation`
69        let direction = rotation * *self.direction;
70        let half_size = (self.half_length * direction).abs();
71
72        Aabb3d::new(translation, half_size)
73    }
74
75    fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
76        BoundingSphere::new(translation, self.half_length)
77    }
78}
79
80impl<const N: usize> Bounded3d for Polyline3d<N> {
81    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
82        Aabb3d::from_point_cloud(translation, rotation, self.vertices.iter().copied())
83    }
84
85    fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere {
86        BoundingSphere::from_point_cloud(translation, rotation, &self.vertices)
87    }
88}
89
90impl Bounded3d for BoxedPolyline3d {
91    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
92        Aabb3d::from_point_cloud(translation, rotation, self.vertices.iter().copied())
93    }
94
95    fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere {
96        BoundingSphere::from_point_cloud(translation, rotation, &self.vertices)
97    }
98}
99
100impl Bounded3d for Cuboid {
101    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
102        // Compute the AABB of the rotated cuboid by transforming the half-size
103        // by an absolute rotation matrix.
104        let rot_mat = Mat3::from_quat(rotation);
105        let abs_rot_mat = Mat3::from_cols(
106            rot_mat.x_axis.abs(),
107            rot_mat.y_axis.abs(),
108            rot_mat.z_axis.abs(),
109        );
110        let half_size = abs_rot_mat * self.half_size;
111
112        Aabb3d::new(translation, half_size)
113    }
114
115    fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
116        BoundingSphere::new(translation, self.half_size.length())
117    }
118}
119
120impl Bounded3d for Cylinder {
121    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
122        // Reference: http://iquilezles.org/articles/diskbbox/
123
124        let segment_dir = rotation * Vec3::Y;
125        let top = segment_dir * self.half_height;
126        let bottom = -top;
127
128        let e = (Vec3::ONE - segment_dir * segment_dir).max(Vec3::ZERO);
129        let half_size = self.radius * Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt());
130
131        Aabb3d {
132            min: (translation + (top - half_size).min(bottom - half_size)).into(),
133            max: (translation + (top + half_size).max(bottom + half_size)).into(),
134        }
135    }
136
137    fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
138        let radius = self.radius.hypot(self.half_height);
139        BoundingSphere::new(translation, radius)
140    }
141}
142
143impl Bounded3d for Capsule3d {
144    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
145        // Get the line segment between the hemispheres of the rotated capsule
146        let segment = Segment3d {
147            // Multiplying a normalized vector (Vec3::Y) with a rotation returns a normalized vector.
148            direction: rotation * Dir3::Y,
149            half_length: self.half_length,
150        };
151        let (a, b) = (segment.point1(), segment.point2());
152
153        // Expand the line segment by the capsule radius to get the capsule half-extents
154        let min = a.min(b) - Vec3::splat(self.radius);
155        let max = a.max(b) + Vec3::splat(self.radius);
156
157        Aabb3d {
158            min: (min + translation).into(),
159            max: (max + translation).into(),
160        }
161    }
162
163    fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
164        BoundingSphere::new(translation, self.radius + self.half_length)
165    }
166}
167
168impl Bounded3d for Cone {
169    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
170        // Reference: http://iquilezles.org/articles/diskbbox/
171
172        let segment_dir = rotation * Vec3::Y;
173        let top = segment_dir * 0.5 * self.height;
174        let bottom = -top;
175
176        let e = (Vec3::ONE - segment_dir * segment_dir).max(Vec3::ZERO);
177        let half_extents = Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt());
178
179        Aabb3d {
180            min: (translation + top.min(bottom - self.radius * half_extents)).into(),
181            max: (translation + top.max(bottom + self.radius * half_extents)).into(),
182        }
183    }
184
185    fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere {
186        // Get the triangular cross-section of the cone.
187        let half_height = 0.5 * self.height;
188        let triangle = Triangle2d::new(
189            half_height * Vec2::Y,
190            Vec2::new(-self.radius, -half_height),
191            Vec2::new(self.radius, -half_height),
192        );
193
194        // Because of circular symmetry, we can use the bounding circle of the triangle
195        // for the bounding sphere of the cone.
196        let BoundingCircle { circle, center } = triangle.bounding_circle(Vec2::ZERO, 0.0);
197
198        BoundingSphere::new(rotation * center.extend(0.0) + translation, circle.radius)
199    }
200}
201
202impl Bounded3d for ConicalFrustum {
203    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
204        // Reference: http://iquilezles.org/articles/diskbbox/
205
206        let segment_dir = rotation * Vec3::Y;
207        let top = segment_dir * 0.5 * self.height;
208        let bottom = -top;
209
210        let e = (Vec3::ONE - segment_dir * segment_dir).max(Vec3::ZERO);
211        let half_extents = Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt());
212
213        Aabb3d {
214            min: (translation
215                + (top - self.radius_top * half_extents)
216                    .min(bottom - self.radius_bottom * half_extents))
217            .into(),
218            max: (translation
219                + (top + self.radius_top * half_extents)
220                    .max(bottom + self.radius_bottom * half_extents))
221            .into(),
222        }
223    }
224
225    fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere {
226        let half_height = 0.5 * self.height;
227
228        // To compute the bounding sphere, we'll get the center and radius of the circumcircle
229        // passing through all four vertices of the trapezoidal cross-section of the conical frustum.
230        //
231        // If the circumcenter is inside the trapezoid, we can use that for the bounding sphere.
232        // Otherwise, we clamp it to the longer parallel side to get a more tightly fitting bounding sphere.
233        //
234        // The circumcenter is at the intersection of the bisectors perpendicular to the sides.
235        // For the isosceles trapezoid, the X coordinate is zero at the center, so a single bisector is enough.
236        //
237        //       A
238        //       *-------*
239        //      /    |    \
240        //     /     |     \
241        // AB / \    |    / \
242        //   /     \ | /     \
243        //  /        C        \
244        // *-------------------*
245        // B
246
247        let a = Vec2::new(-self.radius_top, half_height);
248        let b = Vec2::new(-self.radius_bottom, -half_height);
249        let ab = a - b;
250        let ab_midpoint = b + 0.5 * ab;
251        let bisector = ab.perp();
252
253        // Compute intersection between bisector and vertical line at x = 0.
254        //
255        // x = ab_midpoint.x + t * bisector.x = 0
256        // y = ab_midpoint.y + t * bisector.y = ?
257        //
258        // Because ab_midpoint.y = 0 for our conical frustum, we get:
259        // y = t * bisector.y
260        //
261        // Solve x for t:
262        // t = -ab_midpoint.x / bisector.x
263        //
264        // Substitute t to solve for y:
265        // y = -ab_midpoint.x / bisector.x * bisector.y
266        let circumcenter_y = -ab_midpoint.x / bisector.x * bisector.y;
267
268        // If the circumcenter is outside the trapezoid, the bounding circle is too large.
269        // In those cases, we clamp it to the longer parallel side.
270        let (center, radius) = if circumcenter_y <= -half_height {
271            (Vec2::new(0.0, -half_height), self.radius_bottom)
272        } else if circumcenter_y >= half_height {
273            (Vec2::new(0.0, half_height), self.radius_top)
274        } else {
275            let circumcenter = Vec2::new(0.0, circumcenter_y);
276            // We can use the distance from an arbitrary vertex because they all lie on the circumcircle.
277            (circumcenter, a.distance(circumcenter))
278        };
279
280        BoundingSphere::new(translation + rotation * center.extend(0.0), radius)
281    }
282}
283
284impl Bounded3d for Torus {
285    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
286        // Compute the AABB of a flat disc with the major radius of the torus.
287        // Reference: http://iquilezles.org/articles/diskbbox/
288        let normal = rotation * Vec3::Y;
289        let e = (Vec3::ONE - normal * normal).max(Vec3::ZERO);
290        let disc_half_size = self.major_radius * Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt());
291
292        // Expand the disc by the minor radius to get the torus half-size
293        let half_size = disc_half_size + Vec3::splat(self.minor_radius);
294
295        Aabb3d::new(translation, half_size)
296    }
297
298    fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
299        BoundingSphere::new(translation, self.outer_radius())
300    }
301}
302
303impl Bounded3d for Triangle3d {
304    /// Get the bounding box of the triangle.
305    fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d {
306        let [a, b, c] = self.vertices;
307
308        let a = rotation * a;
309        let b = rotation * b;
310        let c = rotation * c;
311
312        let min = a.min(b).min(c);
313        let max = a.max(b).max(c);
314
315        let bounding_center = (max + min) / 2.0 + translation;
316        let half_extents = (max - min) / 2.0;
317
318        Aabb3d::new(bounding_center, half_extents)
319    }
320
321    /// Get the bounding sphere of the triangle.
322    ///
323    /// The [`Triangle3d`] implements the minimal bounding sphere calculation. For acute triangles, the circumcenter is used as
324    /// the center of the sphere. For the others, the bounding sphere is the minimal sphere
325    /// that contains the largest side of the triangle.
326    fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere {
327        if self.is_degenerate() || self.is_obtuse() {
328            let (p1, p2) = self.largest_side();
329            let mid_point = (p1 + p2) / 2.0;
330            let radius = mid_point.distance(p1);
331            BoundingSphere::new(mid_point + translation, radius)
332        } else {
333            let [a, _, _] = self.vertices;
334
335            let circumcenter = self.circumcenter();
336            let radius = circumcenter.distance(a);
337            BoundingSphere::new(circumcenter + translation, radius)
338        }
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use crate::bounding::BoundingVolume;
345    use glam::{Quat, Vec3, Vec3A};
346
347    use crate::{
348        bounding::Bounded3d,
349        primitives::{
350            Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d,
351            Segment3d, Sphere, Torus, Triangle3d,
352        },
353        Dir3,
354    };
355
356    #[test]
357    fn sphere() {
358        let sphere = Sphere { radius: 1.0 };
359        let translation = Vec3::new(2.0, 1.0, 0.0);
360
361        let aabb = sphere.aabb_3d(translation, Quat::IDENTITY);
362        assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0));
363        assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0));
364
365        let bounding_sphere = sphere.bounding_sphere(translation, Quat::IDENTITY);
366        assert_eq!(bounding_sphere.center, translation.into());
367        assert_eq!(bounding_sphere.radius(), 1.0);
368    }
369
370    #[test]
371    fn plane() {
372        let translation = Vec3::new(2.0, 1.0, 0.0);
373
374        let aabb1 = InfinitePlane3d::new(Vec3::X).aabb_3d(translation, Quat::IDENTITY);
375        assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, -f32::MAX / 2.0));
376        assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, f32::MAX / 2.0));
377
378        let aabb2 = InfinitePlane3d::new(Vec3::Y).aabb_3d(translation, Quat::IDENTITY);
379        assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, -f32::MAX / 2.0));
380        assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, f32::MAX / 2.0));
381
382        let aabb3 = InfinitePlane3d::new(Vec3::Z).aabb_3d(translation, Quat::IDENTITY);
383        assert_eq!(aabb3.min, Vec3A::new(-f32::MAX / 2.0, -f32::MAX / 2.0, 0.0));
384        assert_eq!(aabb3.max, Vec3A::new(f32::MAX / 2.0, f32::MAX / 2.0, 0.0));
385
386        let aabb4 = InfinitePlane3d::new(Vec3::ONE).aabb_3d(translation, Quat::IDENTITY);
387        assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0));
388        assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0));
389
390        let bounding_sphere =
391            InfinitePlane3d::new(Vec3::Y).bounding_sphere(translation, Quat::IDENTITY);
392        assert_eq!(bounding_sphere.center, translation.into());
393        assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0);
394    }
395
396    #[test]
397    fn line() {
398        let translation = Vec3::new(2.0, 1.0, 0.0);
399
400        let aabb1 = Line3d { direction: Dir3::Y }.aabb_3d(translation, Quat::IDENTITY);
401        assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, 0.0));
402        assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, 0.0));
403
404        let aabb2 = Line3d { direction: Dir3::X }.aabb_3d(translation, Quat::IDENTITY);
405        assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, 0.0));
406        assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, 0.0));
407
408        let aabb3 = Line3d { direction: Dir3::Z }.aabb_3d(translation, Quat::IDENTITY);
409        assert_eq!(aabb3.min, Vec3A::new(2.0, 1.0, -f32::MAX / 2.0));
410        assert_eq!(aabb3.max, Vec3A::new(2.0, 1.0, f32::MAX / 2.0));
411
412        let aabb4 = Line3d {
413            direction: Dir3::from_xyz(1.0, 1.0, 1.0).unwrap(),
414        }
415        .aabb_3d(translation, Quat::IDENTITY);
416        assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0));
417        assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0));
418
419        let bounding_sphere =
420            Line3d { direction: Dir3::Y }.bounding_sphere(translation, Quat::IDENTITY);
421        assert_eq!(bounding_sphere.center, translation.into());
422        assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0);
423    }
424
425    #[test]
426    fn segment() {
427        let translation = Vec3::new(2.0, 1.0, 0.0);
428        let segment =
429            Segment3d::from_points(Vec3::new(-1.0, -0.5, 0.0), Vec3::new(1.0, 0.5, 0.0)).0;
430
431        let aabb = segment.aabb_3d(translation, Quat::IDENTITY);
432        assert_eq!(aabb.min, Vec3A::new(1.0, 0.5, 0.0));
433        assert_eq!(aabb.max, Vec3A::new(3.0, 1.5, 0.0));
434
435        let bounding_sphere = segment.bounding_sphere(translation, Quat::IDENTITY);
436        assert_eq!(bounding_sphere.center, translation.into());
437        assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(0.5));
438    }
439
440    #[test]
441    fn polyline() {
442        let polyline = Polyline3d::<4>::new([
443            Vec3::ONE,
444            Vec3::new(-1.0, 1.0, 1.0),
445            Vec3::NEG_ONE,
446            Vec3::new(1.0, -1.0, -1.0),
447        ]);
448        let translation = Vec3::new(2.0, 1.0, 0.0);
449
450        let aabb = polyline.aabb_3d(translation, Quat::IDENTITY);
451        assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0));
452        assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0));
453
454        let bounding_sphere = polyline.bounding_sphere(translation, Quat::IDENTITY);
455        assert_eq!(bounding_sphere.center, translation.into());
456        assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(1.0).hypot(1.0));
457    }
458
459    #[test]
460    fn cuboid() {
461        let cuboid = Cuboid::new(2.0, 1.0, 1.0);
462        let translation = Vec3::new(2.0, 1.0, 0.0);
463
464        let aabb = cuboid.aabb_3d(
465            translation,
466            Quat::from_rotation_z(std::f32::consts::FRAC_PI_4),
467        );
468        let expected_half_size = Vec3A::new(1.0606601, 1.0606601, 0.5);
469        assert_eq!(aabb.min, Vec3A::from(translation) - expected_half_size);
470        assert_eq!(aabb.max, Vec3A::from(translation) + expected_half_size);
471
472        let bounding_sphere = cuboid.bounding_sphere(translation, Quat::IDENTITY);
473        assert_eq!(bounding_sphere.center, translation.into());
474        assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(0.5).hypot(0.5));
475    }
476
477    #[test]
478    fn cylinder() {
479        let cylinder = Cylinder::new(0.5, 2.0);
480        let translation = Vec3::new(2.0, 1.0, 0.0);
481
482        let aabb = cylinder.aabb_3d(translation, Quat::IDENTITY);
483        assert_eq!(
484            aabb.min,
485            Vec3A::from(translation) - Vec3A::new(0.5, 1.0, 0.5)
486        );
487        assert_eq!(
488            aabb.max,
489            Vec3A::from(translation) + Vec3A::new(0.5, 1.0, 0.5)
490        );
491
492        let bounding_sphere = cylinder.bounding_sphere(translation, Quat::IDENTITY);
493        assert_eq!(bounding_sphere.center, translation.into());
494        assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(0.5));
495    }
496
497    #[test]
498    fn capsule() {
499        let capsule = Capsule3d::new(0.5, 2.0);
500        let translation = Vec3::new(2.0, 1.0, 0.0);
501
502        let aabb = capsule.aabb_3d(translation, Quat::IDENTITY);
503        assert_eq!(
504            aabb.min,
505            Vec3A::from(translation) - Vec3A::new(0.5, 1.5, 0.5)
506        );
507        assert_eq!(
508            aabb.max,
509            Vec3A::from(translation) + Vec3A::new(0.5, 1.5, 0.5)
510        );
511
512        let bounding_sphere = capsule.bounding_sphere(translation, Quat::IDENTITY);
513        assert_eq!(bounding_sphere.center, translation.into());
514        assert_eq!(bounding_sphere.radius(), 1.5);
515    }
516
517    #[test]
518    fn cone() {
519        let cone = Cone {
520            radius: 1.0,
521            height: 2.0,
522        };
523        let translation = Vec3::new(2.0, 1.0, 0.0);
524
525        let aabb = cone.aabb_3d(translation, Quat::IDENTITY);
526        assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0));
527        assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0));
528
529        let bounding_sphere = cone.bounding_sphere(translation, Quat::IDENTITY);
530        assert_eq!(
531            bounding_sphere.center,
532            Vec3A::from(translation) + Vec3A::NEG_Y * 0.25
533        );
534        assert_eq!(bounding_sphere.radius(), 1.25);
535    }
536
537    #[test]
538    fn conical_frustum() {
539        let conical_frustum = ConicalFrustum {
540            radius_top: 0.5,
541            radius_bottom: 1.0,
542            height: 2.0,
543        };
544        let translation = Vec3::new(2.0, 1.0, 0.0);
545
546        let aabb = conical_frustum.aabb_3d(translation, Quat::IDENTITY);
547        assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0));
548        assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0));
549
550        let bounding_sphere = conical_frustum.bounding_sphere(translation, Quat::IDENTITY);
551        assert_eq!(
552            bounding_sphere.center,
553            Vec3A::from(translation) + Vec3A::NEG_Y * 0.1875
554        );
555        assert_eq!(bounding_sphere.radius(), 1.2884705);
556    }
557
558    #[test]
559    fn wide_conical_frustum() {
560        let conical_frustum = ConicalFrustum {
561            radius_top: 0.5,
562            radius_bottom: 5.0,
563            height: 1.0,
564        };
565        let translation = Vec3::new(2.0, 1.0, 0.0);
566
567        let aabb = conical_frustum.aabb_3d(translation, Quat::IDENTITY);
568        assert_eq!(aabb.min, Vec3A::new(-3.0, 0.5, -5.0));
569        assert_eq!(aabb.max, Vec3A::new(7.0, 1.5, 5.0));
570
571        // For wide conical frusta like this, the circumcenter can be outside the frustum,
572        // so the center and radius should be clamped to the longest side.
573        let bounding_sphere = conical_frustum.bounding_sphere(translation, Quat::IDENTITY);
574        assert_eq!(
575            bounding_sphere.center,
576            Vec3A::from(translation) + Vec3A::NEG_Y * 0.5
577        );
578        assert_eq!(bounding_sphere.radius(), 5.0);
579    }
580
581    #[test]
582    fn torus() {
583        let torus = Torus {
584            minor_radius: 0.5,
585            major_radius: 1.0,
586        };
587        let translation = Vec3::new(2.0, 1.0, 0.0);
588
589        let aabb = torus.aabb_3d(translation, Quat::IDENTITY);
590        assert_eq!(aabb.min, Vec3A::new(0.5, 0.5, -1.5));
591        assert_eq!(aabb.max, Vec3A::new(3.5, 1.5, 1.5));
592
593        let bounding_sphere = torus.bounding_sphere(translation, Quat::IDENTITY);
594        assert_eq!(bounding_sphere.center, translation.into());
595        assert_eq!(bounding_sphere.radius(), 1.5);
596    }
597
598    #[test]
599    fn triangle3d() {
600        let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);
601
602        let br = zero_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY);
603        assert_eq!(
604            br.center(),
605            Vec3::ZERO.into(),
606            "incorrect bounding box center"
607        );
608        assert_eq!(
609            br.half_size(),
610            Vec3::ZERO.into(),
611            "incorrect bounding box half extents"
612        );
613
614        let bs = zero_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY);
615        assert_eq!(
616            bs.center,
617            Vec3::ZERO.into(),
618            "incorrect bounding sphere center"
619        );
620        assert_eq!(bs.sphere.radius, 0.0, "incorrect bounding sphere radius");
621
622        let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X);
623        let bs = dup_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY);
624        assert_eq!(
625            bs.center,
626            Vec3::new(0.5, 0.0, 0.0).into(),
627            "incorrect bounding sphere center"
628        );
629        assert_eq!(bs.sphere.radius, 0.5, "incorrect bounding sphere radius");
630        let br = dup_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY);
631        assert_eq!(
632            br.center(),
633            Vec3::new(0.5, 0.0, 0.0).into(),
634            "incorrect bounding box center"
635        );
636        assert_eq!(
637            br.half_size(),
638            Vec3::new(0.5, 0.0, 0.0).into(),
639            "incorrect bounding box half extents"
640        );
641
642        let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X);
643        let bs = collinear_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY);
644        assert_eq!(
645            bs.center,
646            Vec3::ZERO.into(),
647            "incorrect bounding sphere center"
648        );
649        assert_eq!(bs.sphere.radius, 1.0, "incorrect bounding sphere radius");
650        let br = collinear_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY);
651        assert_eq!(
652            br.center(),
653            Vec3::ZERO.into(),
654            "incorrect bounding box center"
655        );
656        assert_eq!(
657            br.half_size(),
658            Vec3::new(1.0, 0.0, 0.0).into(),
659            "incorrect bounding box half extents"
660        );
661    }
662}