bevy_math/bounding/bounded3d/
primitive_impls.rs

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