Skip to main content

bevy_shape/bounding/bounded3d/
primitive_impls.rs

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