bevy_math/bounding/bounded3d/
primitive_impls.rs

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