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