bevy_math/bounding/bounded2d/
primitive_impls.rs

1//! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives).
2
3use crate::{
4    ops,
5    primitives::{
6        Annulus, Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector,
7        CircularSegment, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon,
8        Rhombus, Segment2d, Triangle2d,
9    },
10    Dir2, Isometry2d, Mat2, Rot2, Vec2,
11};
12use core::f32::consts::{FRAC_PI_2, PI, TAU};
13
14use smallvec::SmallVec;
15
16use super::{Aabb2d, Bounded2d, BoundingCircle};
17
18impl Bounded2d for Circle {
19    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
20        let isometry = isometry.into();
21        Aabb2d::new(isometry.translation, Vec2::splat(self.radius))
22    }
23
24    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
25        let isometry = isometry.into();
26        BoundingCircle::new(isometry.translation, self.radius)
27    }
28}
29
30// Compute the axis-aligned bounding points of a rotated arc, used for computing the AABB of arcs and derived shapes.
31// The return type has room for 7 points so that the CircularSector code can add an additional point.
32#[inline]
33fn arc_bounding_points(arc: Arc2d, rotation: impl Into<Rot2>) -> SmallVec<[Vec2; 7]> {
34    // Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle.
35    // We need to compute which axis-aligned extrema are actually contained within the rotated arc.
36    let mut bounds = SmallVec::<[Vec2; 7]>::new();
37    let rotation = rotation.into();
38    bounds.push(rotation * arc.left_endpoint());
39    bounds.push(rotation * arc.right_endpoint());
40
41    // The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y.
42    // Compute the normalized angles of the endpoints with the rotation taken into account, and then
43    // check if we are looking for an angle that is between or outside them.
44    let left_angle = (FRAC_PI_2 + arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
45    let right_angle = (FRAC_PI_2 - arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
46    let inverted = left_angle < right_angle;
47    for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] {
48        let angle = extremum.to_angle().rem_euclid(TAU);
49        // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them.
50        // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis.
51        // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine.
52        #[allow(clippy::nonminimal_bool)]
53        if !inverted && angle >= right_angle && angle <= left_angle
54            || inverted && (angle >= right_angle || angle <= left_angle)
55        {
56            bounds.push(extremum * arc.radius);
57        }
58    }
59    bounds
60}
61
62impl Bounded2d for Arc2d {
63    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
64        // If our arc covers more than a circle, just return the bounding box of the circle.
65        if self.half_angle >= PI {
66            return Circle::new(self.radius).aabb_2d(isometry);
67        }
68
69        let isometry = isometry.into();
70
71        Aabb2d::from_point_cloud(
72            Isometry2d::from_translation(isometry.translation),
73            &arc_bounding_points(*self, isometry.rotation),
74        )
75    }
76
77    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
78        let isometry = isometry.into();
79
80        // There are two possibilities for the bounding circle.
81        if self.is_major() {
82            // If the arc is major, then the widest distance between two points is a diameter of the arc's circle;
83            // therefore, that circle is the bounding radius.
84            BoundingCircle::new(isometry.translation, self.radius)
85        } else {
86            // Otherwise, the widest distance between two points is the chord,
87            // so a circle of that diameter around the midpoint will contain the entire arc.
88            let center = isometry.rotation * self.chord_midpoint();
89            BoundingCircle::new(center + isometry.translation, self.half_chord_length())
90        }
91    }
92}
93
94impl Bounded2d for CircularSector {
95    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
96        let isometry = isometry.into();
97
98        // If our sector covers more than a circle, just return the bounding box of the circle.
99        if self.half_angle() >= PI {
100            return Circle::new(self.radius()).aabb_2d(isometry);
101        }
102
103        // Otherwise, we use the same logic as for Arc2d, above, just with the circle's center as an additional possibility.
104        let mut bounds = arc_bounding_points(self.arc, isometry.rotation);
105        bounds.push(Vec2::ZERO);
106
107        Aabb2d::from_point_cloud(Isometry2d::from_translation(isometry.translation), &bounds)
108    }
109
110    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
111        if self.arc.is_major() {
112            let isometry = isometry.into();
113
114            // If the arc is major, that is, greater than a semicircle,
115            // then bounding circle is just the circle defining the sector.
116            BoundingCircle::new(isometry.translation, self.arc.radius)
117        } else {
118            // However, when the arc is minor,
119            // we need our bounding circle to include both endpoints of the arc as well as the circle center.
120            // This means we need the circumcircle of those three points.
121            // The circumcircle will always have a greater curvature than the circle itself, so it will contain
122            // the entire circular sector.
123            Triangle2d::new(
124                Vec2::ZERO,
125                self.arc.left_endpoint(),
126                self.arc.right_endpoint(),
127            )
128            .bounding_circle(isometry)
129        }
130    }
131}
132
133impl Bounded2d for CircularSegment {
134    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
135        self.arc.aabb_2d(isometry)
136    }
137
138    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
139        self.arc.bounding_circle(isometry)
140    }
141}
142
143impl Bounded2d for Ellipse {
144    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
145        let isometry = isometry.into();
146
147        //           V = (hh * cos(beta), hh * sin(beta))
148        //      #####*#####
149        //   ###     |     ###
150        //  #     hh |        #
151        // #         *---------* U = (hw * cos(alpha), hw * sin(alpha))
152        //  #            hw   #
153        //   ###           ###
154        //      ###########
155
156        let (hw, hh) = (self.half_size.x, self.half_size.y);
157
158        // Sine and cosine of rotation angle alpha.
159        let (alpha_sin, alpha_cos) = isometry.rotation.sin_cos();
160
161        // Sine and cosine of alpha + pi/2. We can avoid the trigonometric functions:
162        // sin(beta) = sin(alpha + pi/2) = cos(alpha)
163        // cos(beta) = cos(alpha + pi/2) = -sin(alpha)
164        let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin);
165
166        // Compute points U and V, the extremes of the ellipse
167        let (ux, uy) = (hw * alpha_cos, hw * alpha_sin);
168        let (vx, vy) = (hh * beta_cos, hh * beta_sin);
169
170        let half_size = Vec2::new(ops::hypot(ux, vx), ops::hypot(uy, vy));
171
172        Aabb2d::new(isometry.translation, half_size)
173    }
174
175    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
176        let isometry = isometry.into();
177        BoundingCircle::new(isometry.translation, self.semi_major())
178    }
179}
180
181impl Bounded2d for Annulus {
182    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
183        let isometry = isometry.into();
184        Aabb2d::new(isometry.translation, Vec2::splat(self.outer_circle.radius))
185    }
186
187    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
188        let isometry = isometry.into();
189        BoundingCircle::new(isometry.translation, self.outer_circle.radius)
190    }
191}
192
193impl Bounded2d for Rhombus {
194    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
195        let isometry = isometry.into();
196
197        let [rotated_x_half_diagonal, rotated_y_half_diagonal] = [
198            isometry.rotation * Vec2::new(self.half_diagonals.x, 0.0),
199            isometry.rotation * Vec2::new(0.0, self.half_diagonals.y),
200        ];
201        let aabb_half_extent = rotated_x_half_diagonal
202            .abs()
203            .max(rotated_y_half_diagonal.abs());
204
205        Aabb2d {
206            min: -aabb_half_extent + isometry.translation,
207            max: aabb_half_extent + isometry.translation,
208        }
209    }
210
211    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
212        let isometry = isometry.into();
213        BoundingCircle::new(isometry.translation, self.circumradius())
214    }
215}
216
217impl Bounded2d for Plane2d {
218    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
219        let isometry = isometry.into();
220
221        let normal = isometry.rotation * *self.normal;
222        let facing_x = normal == Vec2::X || normal == Vec2::NEG_X;
223        let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y;
224
225        // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
226        // like growing or shrinking the AABB without breaking things.
227        let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 };
228        let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 };
229        let half_size = Vec2::new(half_width, half_height);
230
231        Aabb2d::new(isometry.translation, half_size)
232    }
233
234    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
235        let isometry = isometry.into();
236        BoundingCircle::new(isometry.translation, f32::MAX / 2.0)
237    }
238}
239
240impl Bounded2d for Line2d {
241    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
242        let isometry = isometry.into();
243
244        let direction = isometry.rotation * *self.direction;
245
246        // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
247        // like growing or shrinking the AABB without breaking things.
248        let max = f32::MAX / 2.0;
249        let half_width = if direction.x == 0.0 { 0.0 } else { max };
250        let half_height = if direction.y == 0.0 { 0.0 } else { max };
251        let half_size = Vec2::new(half_width, half_height);
252
253        Aabb2d::new(isometry.translation, half_size)
254    }
255
256    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
257        let isometry = isometry.into();
258        BoundingCircle::new(isometry.translation, f32::MAX / 2.0)
259    }
260}
261
262impl Bounded2d for Segment2d {
263    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
264        let isometry = isometry.into();
265
266        // Rotate the segment by `rotation`
267        let direction = isometry.rotation * *self.direction;
268        let half_size = (self.half_length * direction).abs();
269
270        Aabb2d::new(isometry.translation, half_size)
271    }
272
273    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
274        let isometry = isometry.into();
275        BoundingCircle::new(isometry.translation, self.half_length)
276    }
277}
278
279impl<const N: usize> Bounded2d for Polyline2d<N> {
280    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
281        Aabb2d::from_point_cloud(isometry, &self.vertices)
282    }
283
284    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
285        BoundingCircle::from_point_cloud(isometry, &self.vertices)
286    }
287}
288
289impl Bounded2d for BoxedPolyline2d {
290    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
291        Aabb2d::from_point_cloud(isometry, &self.vertices)
292    }
293
294    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
295        BoundingCircle::from_point_cloud(isometry, &self.vertices)
296    }
297}
298
299impl Bounded2d for Triangle2d {
300    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
301        let isometry = isometry.into();
302        let [a, b, c] = self.vertices.map(|vtx| isometry.rotation * vtx);
303
304        let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y));
305        let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y));
306
307        Aabb2d {
308            min: min + isometry.translation,
309            max: max + isometry.translation,
310        }
311    }
312
313    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
314        let isometry = isometry.into();
315        let [a, b, c] = self.vertices;
316
317        // The points of the segment opposite to the obtuse or right angle if one exists
318        let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 {
319            Some((b, c))
320        } else if (c - b).dot(a - b) <= 0.0 {
321            Some((c, a))
322        } else if (a - c).dot(b - c) <= 0.0 {
323            Some((a, b))
324        } else {
325            // The triangle is acute.
326            None
327        };
328
329        // Find the minimum bounding circle. If the triangle is obtuse, the circle passes through two vertices.
330        // Otherwise, it's the circumcircle and passes through all three.
331        if let Some((point1, point2)) = side_opposite_to_non_acute {
332            // The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side.
333            // We can compute the minimum bounding circle from the line segment of the longest side.
334            let (segment, center) = Segment2d::from_points(point1, point2);
335            segment.bounding_circle(isometry * Isometry2d::from_translation(center))
336        } else {
337            // The triangle is acute, so the smallest bounding circle is the circumcircle.
338            let (Circle { radius }, circumcenter) = self.circumcircle();
339            BoundingCircle::new(isometry * circumcenter, radius)
340        }
341    }
342}
343
344impl Bounded2d for Rectangle {
345    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
346        let isometry = isometry.into();
347
348        // Compute the AABB of the rotated rectangle by transforming the half-extents
349        // by an absolute rotation matrix.
350        let (sin, cos) = isometry.rotation.sin_cos();
351        let abs_rot_mat = Mat2::from_cols_array(&[cos.abs(), sin.abs(), sin.abs(), cos.abs()]);
352        let half_size = abs_rot_mat * self.half_size;
353
354        Aabb2d::new(isometry.translation, half_size)
355    }
356
357    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
358        let isometry = isometry.into();
359        let radius = self.half_size.length();
360        BoundingCircle::new(isometry.translation, radius)
361    }
362}
363
364impl<const N: usize> Bounded2d for Polygon<N> {
365    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
366        Aabb2d::from_point_cloud(isometry, &self.vertices)
367    }
368
369    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
370        BoundingCircle::from_point_cloud(isometry, &self.vertices)
371    }
372}
373
374impl Bounded2d for BoxedPolygon {
375    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
376        Aabb2d::from_point_cloud(isometry, &self.vertices)
377    }
378
379    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
380        BoundingCircle::from_point_cloud(isometry, &self.vertices)
381    }
382}
383
384impl Bounded2d for RegularPolygon {
385    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
386        let isometry = isometry.into();
387
388        let mut min = Vec2::ZERO;
389        let mut max = Vec2::ZERO;
390
391        for vertex in self.vertices(isometry.rotation.as_radians()) {
392            min = min.min(vertex);
393            max = max.max(vertex);
394        }
395
396        Aabb2d {
397            min: min + isometry.translation,
398            max: max + isometry.translation,
399        }
400    }
401
402    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
403        let isometry = isometry.into();
404        BoundingCircle::new(isometry.translation, self.circumcircle.radius)
405    }
406}
407
408impl Bounded2d for Capsule2d {
409    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
410        let isometry = isometry.into();
411
412        // Get the line segment between the hemicircles of the rotated capsule
413        let segment = Segment2d {
414            // Multiplying a normalized vector (Vec2::Y) with a rotation returns a normalized vector.
415            direction: isometry.rotation * Dir2::Y,
416            half_length: self.half_length,
417        };
418        let (a, b) = (segment.point1(), segment.point2());
419
420        // Expand the line segment by the capsule radius to get the capsule half-extents
421        let min = a.min(b) - Vec2::splat(self.radius);
422        let max = a.max(b) + Vec2::splat(self.radius);
423
424        Aabb2d {
425            min: min + isometry.translation,
426            max: max + isometry.translation,
427        }
428    }
429
430    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
431        let isometry = isometry.into();
432        BoundingCircle::new(isometry.translation, self.radius + self.half_length)
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU};
439
440    // use approx::assert_abs_diff_eq;
441    use glam::Vec2;
442
443    use crate::{
444        bounding::Bounded2d,
445        ops::{self, FloatPow},
446        primitives::{
447            Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,
448            Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d,
449            Triangle2d,
450        },
451        Dir2, Isometry2d, Rot2,
452    };
453
454    #[test]
455    fn circle() {
456        let circle = Circle { radius: 1.0 };
457        let translation = Vec2::new(2.0, 1.0);
458        let isometry = Isometry2d::from_translation(translation);
459
460        let aabb = circle.aabb_2d(isometry);
461        assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
462        assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
463
464        let bounding_circle = circle.bounding_circle(isometry);
465        assert_eq!(bounding_circle.center, translation);
466        assert_eq!(bounding_circle.radius(), 1.0);
467    }
468
469    #[test]
470    // Arcs and circular segments have the same bounding shapes so they share test cases.
471    fn arc_and_segment() {
472        struct TestCase {
473            name: &'static str,
474            arc: Arc2d,
475            translation: Vec2,
476            rotation: f32,
477            aabb_min: Vec2,
478            aabb_max: Vec2,
479            bounding_circle_center: Vec2,
480            bounding_circle_radius: f32,
481        }
482
483        impl TestCase {
484            fn isometry(&self) -> Isometry2d {
485                Isometry2d::new(self.translation, self.rotation.into())
486            }
487        }
488
489        // The apothem of an arc covering 1/6th of a circle.
490        let apothem = f32::sqrt(3.0) / 2.0;
491        let tests = [
492            // Test case: a basic minor arc
493            TestCase {
494                name: "1/6th circle untransformed",
495                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
496                translation: Vec2::ZERO,
497                rotation: 0.0,
498                aabb_min: Vec2::new(-0.5, apothem),
499                aabb_max: Vec2::new(0.5, 1.0),
500                bounding_circle_center: Vec2::new(0.0, apothem),
501                bounding_circle_radius: 0.5,
502            },
503            // Test case: a smaller arc, verifying that radius scaling works
504            TestCase {
505                name: "1/6th circle with radius 0.5",
506                arc: Arc2d::from_radians(0.5, FRAC_PI_3),
507                translation: Vec2::ZERO,
508                rotation: 0.0,
509                aabb_min: Vec2::new(-0.25, apothem / 2.0),
510                aabb_max: Vec2::new(0.25, 0.5),
511                bounding_circle_center: Vec2::new(0.0, apothem / 2.0),
512                bounding_circle_radius: 0.25,
513            },
514            // Test case: a larger arc, verifying that radius scaling works
515            TestCase {
516                name: "1/6th circle with radius 2.0",
517                arc: Arc2d::from_radians(2.0, FRAC_PI_3),
518                translation: Vec2::ZERO,
519                rotation: 0.0,
520                aabb_min: Vec2::new(-1.0, 2.0 * apothem),
521                aabb_max: Vec2::new(1.0, 2.0),
522                bounding_circle_center: Vec2::new(0.0, 2.0 * apothem),
523                bounding_circle_radius: 1.0,
524            },
525            // Test case: translation of a minor arc
526            TestCase {
527                name: "1/6th circle translated",
528                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
529                translation: Vec2::new(2.0, 3.0),
530                rotation: 0.0,
531                aabb_min: Vec2::new(1.5, 3.0 + apothem),
532                aabb_max: Vec2::new(2.5, 4.0),
533                bounding_circle_center: Vec2::new(2.0, 3.0 + apothem),
534                bounding_circle_radius: 0.5,
535            },
536            // Test case: rotation of a minor arc
537            TestCase {
538                name: "1/6th circle rotated",
539                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
540                translation: Vec2::ZERO,
541                // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.
542                rotation: FRAC_PI_6,
543                aabb_min: Vec2::new(-apothem, 0.5),
544                aabb_max: Vec2::new(0.0, 1.0),
545                // The exact coordinates here are not obvious, but can be computed by constructing
546                // an altitude from the midpoint of the chord to the y-axis and using the right triangle
547                // similarity theorem.
548                bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.squared()),
549                bounding_circle_radius: 0.5,
550            },
551            // Test case: handling of axis-aligned extrema
552            TestCase {
553                name: "1/4er circle rotated to be axis-aligned",
554                arc: Arc2d::from_radians(1.0, FRAC_PI_2),
555                translation: Vec2::ZERO,
556                // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis.
557                rotation: -FRAC_PI_4,
558                aabb_min: Vec2::ZERO,
559                aabb_max: Vec2::splat(1.0),
560                bounding_circle_center: Vec2::splat(0.5),
561                bounding_circle_radius: f32::sqrt(2.0) / 2.0,
562            },
563            // Test case: a basic major arc
564            TestCase {
565                name: "5/6th circle untransformed",
566                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
567                translation: Vec2::ZERO,
568                rotation: 0.0,
569                aabb_min: Vec2::new(-1.0, -apothem),
570                aabb_max: Vec2::new(1.0, 1.0),
571                bounding_circle_center: Vec2::ZERO,
572                bounding_circle_radius: 1.0,
573            },
574            // Test case: a translated major arc
575            TestCase {
576                name: "5/6th circle translated",
577                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
578                translation: Vec2::new(2.0, 3.0),
579                rotation: 0.0,
580                aabb_min: Vec2::new(1.0, 3.0 - apothem),
581                aabb_max: Vec2::new(3.0, 4.0),
582                bounding_circle_center: Vec2::new(2.0, 3.0),
583                bounding_circle_radius: 1.0,
584            },
585            // Test case: a rotated major arc, with inverted left/right angles
586            TestCase {
587                name: "5/6th circle rotated",
588                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
589                translation: Vec2::ZERO,
590                // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.
591                rotation: FRAC_PI_6,
592                aabb_min: Vec2::new(-1.0, -1.0),
593                aabb_max: Vec2::new(1.0, 1.0),
594                bounding_circle_center: Vec2::ZERO,
595                bounding_circle_radius: 1.0,
596            },
597        ];
598
599        for test in tests {
600            println!("subtest case: {}", test.name);
601            let segment: CircularSegment = test.arc.into();
602
603            let arc_aabb = test.arc.aabb_2d(test.isometry());
604            // assert_abs_diff_eq!(test.aabb_min, arc_aabb.min);
605            // assert_abs_diff_eq!(test.aabb_max, arc_aabb.max);
606            let segment_aabb = segment.aabb_2d(test.isometry());
607            // assert_abs_diff_eq!(test.aabb_min, segment_aabb.min);
608            // assert_abs_diff_eq!(test.aabb_max, segment_aabb.max);
609
610            let arc_bounding_circle = test.arc.bounding_circle(test.isometry());
611            // assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center);
612            // assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius());
613            let segment_bounding_circle = segment.bounding_circle(test.isometry());
614            // assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center);
615            // assert_abs_diff_eq!(
616            //     test.bounding_circle_radius,
617            //     segment_bounding_circle.radius()
618            // );
619        }
620    }
621
622    #[test]
623    fn circular_sector() {
624        struct TestCase {
625            name: &'static str,
626            arc: Arc2d,
627            translation: Vec2,
628            rotation: f32,
629            aabb_min: Vec2,
630            aabb_max: Vec2,
631            bounding_circle_center: Vec2,
632            bounding_circle_radius: f32,
633        }
634
635        impl TestCase {
636            fn isometry(&self) -> Isometry2d {
637                Isometry2d::new(self.translation, self.rotation.into())
638            }
639        }
640
641        // The apothem of an arc covering 1/6th of a circle.
642        let apothem = f32::sqrt(3.0) / 2.0;
643        let inv_sqrt_3 = f32::sqrt(3.0).recip();
644        let tests = [
645            // Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center
646            TestCase {
647                name: "1/3rd circle",
648                arc: Arc2d::from_radians(1.0, TAU / 3.0),
649                translation: Vec2::ZERO,
650                rotation: 0.0,
651                aabb_min: Vec2::new(-apothem, 0.0),
652                aabb_max: Vec2::new(apothem, 1.0),
653                bounding_circle_center: Vec2::new(0.0, 0.5),
654                bounding_circle_radius: apothem,
655            },
656            // The remaining test cases are selected as for arc_and_segment.
657            TestCase {
658                name: "1/6th circle untransformed",
659                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
660                translation: Vec2::ZERO,
661                rotation: 0.0,
662                aabb_min: Vec2::new(-0.5, 0.0),
663                aabb_max: Vec2::new(0.5, 1.0),
664                // The bounding circle is a circumcircle of an equilateral triangle with side length 1.
665                // The distance from the corner to the center of such a triangle is 1/sqrt(3).
666                bounding_circle_center: Vec2::new(0.0, inv_sqrt_3),
667                bounding_circle_radius: inv_sqrt_3,
668            },
669            TestCase {
670                name: "1/6th circle with radius 0.5",
671                arc: Arc2d::from_radians(0.5, FRAC_PI_3),
672                translation: Vec2::ZERO,
673                rotation: 0.0,
674                aabb_min: Vec2::new(-0.25, 0.0),
675                aabb_max: Vec2::new(0.25, 0.5),
676                bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0),
677                bounding_circle_radius: inv_sqrt_3 / 2.0,
678            },
679            TestCase {
680                name: "1/6th circle with radius 2.0",
681                arc: Arc2d::from_radians(2.0, FRAC_PI_3),
682                translation: Vec2::ZERO,
683                rotation: 0.0,
684                aabb_min: Vec2::new(-1.0, 0.0),
685                aabb_max: Vec2::new(1.0, 2.0),
686                bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3),
687                bounding_circle_radius: 2.0 * inv_sqrt_3,
688            },
689            TestCase {
690                name: "1/6th circle translated",
691                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
692                translation: Vec2::new(2.0, 3.0),
693                rotation: 0.0,
694                aabb_min: Vec2::new(1.5, 3.0),
695                aabb_max: Vec2::new(2.5, 4.0),
696                bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3),
697                bounding_circle_radius: inv_sqrt_3,
698            },
699            TestCase {
700                name: "1/6th circle rotated",
701                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
702                translation: Vec2::ZERO,
703                // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.
704                rotation: FRAC_PI_6,
705                aabb_min: Vec2::new(-apothem, 0.0),
706                aabb_max: Vec2::new(0.0, 1.0),
707                // The x-coordinate is now the inradius of the equilateral triangle, which is sqrt(3)/2.
708                bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5),
709                bounding_circle_radius: inv_sqrt_3,
710            },
711            TestCase {
712                name: "1/4er circle rotated to be axis-aligned",
713                arc: Arc2d::from_radians(1.0, FRAC_PI_2),
714                translation: Vec2::ZERO,
715                // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis.
716                rotation: -FRAC_PI_4,
717                aabb_min: Vec2::ZERO,
718                aabb_max: Vec2::splat(1.0),
719                bounding_circle_center: Vec2::splat(0.5),
720                bounding_circle_radius: f32::sqrt(2.0) / 2.0,
721            },
722            TestCase {
723                name: "5/6th circle untransformed",
724                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
725                translation: Vec2::ZERO,
726                rotation: 0.0,
727                aabb_min: Vec2::new(-1.0, -apothem),
728                aabb_max: Vec2::new(1.0, 1.0),
729                bounding_circle_center: Vec2::ZERO,
730                bounding_circle_radius: 1.0,
731            },
732            TestCase {
733                name: "5/6th circle translated",
734                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
735                translation: Vec2::new(2.0, 3.0),
736                rotation: 0.0,
737                aabb_min: Vec2::new(1.0, 3.0 - apothem),
738                aabb_max: Vec2::new(3.0, 4.0),
739                bounding_circle_center: Vec2::new(2.0, 3.0),
740                bounding_circle_radius: 1.0,
741            },
742            TestCase {
743                name: "5/6th circle rotated",
744                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
745                translation: Vec2::ZERO,
746                // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.
747                rotation: FRAC_PI_6,
748                aabb_min: Vec2::new(-1.0, -1.0),
749                aabb_max: Vec2::new(1.0, 1.0),
750                bounding_circle_center: Vec2::ZERO,
751                bounding_circle_radius: 1.0,
752            },
753        ];
754
755        for test in tests {
756            println!("subtest case: {}", test.name);
757            let sector: CircularSector = test.arc.into();
758
759            let aabb = sector.aabb_2d(test.isometry());
760            // assert_abs_diff_eq!(test.aabb_min, aabb.min);
761            // assert_abs_diff_eq!(test.aabb_max, aabb.max);
762
763            let bounding_circle = sector.bounding_circle(test.isometry());
764            // assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center);
765            // assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius());
766        }
767    }
768
769    #[test]
770    fn ellipse() {
771        let ellipse = Ellipse::new(1.0, 0.5);
772        let translation = Vec2::new(2.0, 1.0);
773        let isometry = Isometry2d::from_translation(translation);
774
775        let aabb = ellipse.aabb_2d(isometry);
776        assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
777        assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
778
779        let bounding_circle = ellipse.bounding_circle(isometry);
780        assert_eq!(bounding_circle.center, translation);
781        assert_eq!(bounding_circle.radius(), 1.0);
782    }
783
784    #[test]
785    fn annulus() {
786        let annulus = Annulus::new(1.0, 2.0);
787        let translation = Vec2::new(2.0, 1.0);
788        let rotation = Rot2::radians(1.0);
789        let isometry = Isometry2d::new(translation, rotation);
790
791        let aabb = annulus.aabb_2d(isometry);
792        assert_eq!(aabb.min, Vec2::new(0.0, -1.0));
793        assert_eq!(aabb.max, Vec2::new(4.0, 3.0));
794
795        let bounding_circle = annulus.bounding_circle(isometry);
796        assert_eq!(bounding_circle.center, translation);
797        assert_eq!(bounding_circle.radius(), 2.0);
798    }
799
800    #[test]
801    fn rhombus() {
802        let rhombus = Rhombus::new(2.0, 1.0);
803        let translation = Vec2::new(2.0, 1.0);
804        let rotation = Rot2::radians(FRAC_PI_4);
805        let isometry = Isometry2d::new(translation, rotation);
806
807        let aabb = rhombus.aabb_2d(isometry);
808        assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323));
809        assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068));
810
811        let bounding_circle = rhombus.bounding_circle(isometry);
812        assert_eq!(bounding_circle.center, translation);
813        assert_eq!(bounding_circle.radius(), 1.0);
814
815        let rhombus = Rhombus::new(0.0, 0.0);
816        let translation = Vec2::new(0.0, 0.0);
817        let isometry = Isometry2d::new(translation, rotation);
818
819        let aabb = rhombus.aabb_2d(isometry);
820        assert_eq!(aabb.min, Vec2::new(0.0, 0.0));
821        assert_eq!(aabb.max, Vec2::new(0.0, 0.0));
822
823        let bounding_circle = rhombus.bounding_circle(isometry);
824        assert_eq!(bounding_circle.center, translation);
825        assert_eq!(bounding_circle.radius(), 0.0);
826    }
827
828    #[test]
829    fn plane() {
830        let translation = Vec2::new(2.0, 1.0);
831        let isometry = Isometry2d::from_translation(translation);
832
833        let aabb1 = Plane2d::new(Vec2::X).aabb_2d(isometry);
834        assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
835        assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
836
837        let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(isometry);
838        assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
839        assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
840
841        let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(isometry);
842        assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
843        assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
844
845        let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(isometry);
846        assert_eq!(bounding_circle.center, translation);
847        assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
848    }
849
850    #[test]
851    fn line() {
852        let translation = Vec2::new(2.0, 1.0);
853        let isometry = Isometry2d::from_translation(translation);
854
855        let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(isometry);
856        assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
857        assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
858
859        let aabb2 = Line2d { direction: Dir2::X }.aabb_2d(isometry);
860        assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
861        assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
862
863        let aabb3 = Line2d {
864            direction: Dir2::from_xy(1.0, 1.0).unwrap(),
865        }
866        .aabb_2d(isometry);
867        assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
868        assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
869
870        let bounding_circle = Line2d { direction: Dir2::Y }.bounding_circle(isometry);
871        assert_eq!(bounding_circle.center, translation);
872        assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
873    }
874
875    #[test]
876    fn segment() {
877        let translation = Vec2::new(2.0, 1.0);
878        let isometry = Isometry2d::from_translation(translation);
879        let segment = Segment2d::from_points(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5)).0;
880
881        let aabb = segment.aabb_2d(isometry);
882        assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
883        assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
884
885        let bounding_circle = segment.bounding_circle(isometry);
886        assert_eq!(bounding_circle.center, translation);
887        assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));
888    }
889
890    #[test]
891    fn polyline() {
892        let polyline = Polyline2d::<4>::new([
893            Vec2::ONE,
894            Vec2::new(-1.0, 1.0),
895            Vec2::NEG_ONE,
896            Vec2::new(1.0, -1.0),
897        ]);
898        let translation = Vec2::new(2.0, 1.0);
899        let isometry = Isometry2d::from_translation(translation);
900
901        let aabb = polyline.aabb_2d(isometry);
902        assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
903        assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
904
905        let bounding_circle = polyline.bounding_circle(isometry);
906        assert_eq!(bounding_circle.center, translation);
907        assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);
908    }
909
910    #[test]
911    fn acute_triangle() {
912        let acute_triangle =
913            Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0));
914        let translation = Vec2::new(2.0, 1.0);
915        let isometry = Isometry2d::from_translation(translation);
916
917        let aabb = acute_triangle.aabb_2d(isometry);
918        assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
919        assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
920
921        // For acute triangles, the center is the circumcenter
922        let (Circle { radius }, circumcenter) = acute_triangle.circumcircle();
923        let bounding_circle = acute_triangle.bounding_circle(isometry);
924        assert_eq!(bounding_circle.center, circumcenter + translation);
925        assert_eq!(bounding_circle.radius(), radius);
926    }
927
928    #[test]
929    fn obtuse_triangle() {
930        let obtuse_triangle = Triangle2d::new(
931            Vec2::new(0.0, 1.0),
932            Vec2::new(-10.0, -1.0),
933            Vec2::new(10.0, -1.0),
934        );
935        let translation = Vec2::new(2.0, 1.0);
936        let isometry = Isometry2d::from_translation(translation);
937
938        let aabb = obtuse_triangle.aabb_2d(isometry);
939        assert_eq!(aabb.min, Vec2::new(-8.0, 0.0));
940        assert_eq!(aabb.max, Vec2::new(12.0, 2.0));
941
942        // For obtuse and right triangles, the center is the midpoint of the longest side (diameter of bounding circle)
943        let bounding_circle = obtuse_triangle.bounding_circle(isometry);
944        assert_eq!(bounding_circle.center, translation - Vec2::Y);
945        assert_eq!(bounding_circle.radius(), 10.0);
946    }
947
948    #[test]
949    fn rectangle() {
950        let rectangle = Rectangle::new(2.0, 1.0);
951        let translation = Vec2::new(2.0, 1.0);
952
953        let aabb = rectangle.aabb_2d(Isometry2d::new(translation, Rot2::radians(FRAC_PI_4)));
954        let expected_half_size = Vec2::splat(1.0606601);
955        assert_eq!(aabb.min, translation - expected_half_size);
956        assert_eq!(aabb.max, translation + expected_half_size);
957
958        let bounding_circle = rectangle.bounding_circle(Isometry2d::from_translation(translation));
959        assert_eq!(bounding_circle.center, translation);
960        assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));
961    }
962
963    #[test]
964    fn polygon() {
965        let polygon = Polygon::<4>::new([
966            Vec2::ONE,
967            Vec2::new(-1.0, 1.0),
968            Vec2::NEG_ONE,
969            Vec2::new(1.0, -1.0),
970        ]);
971        let translation = Vec2::new(2.0, 1.0);
972        let isometry = Isometry2d::from_translation(translation);
973
974        let aabb = polygon.aabb_2d(isometry);
975        assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
976        assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
977
978        let bounding_circle = polygon.bounding_circle(isometry);
979        assert_eq!(bounding_circle.center, translation);
980        assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);
981    }
982
983    #[test]
984    fn regular_polygon() {
985        let regular_polygon = RegularPolygon::new(1.0, 5);
986        let translation = Vec2::new(2.0, 1.0);
987        let isometry = Isometry2d::from_translation(translation);
988
989        let aabb = regular_polygon.aabb_2d(isometry);
990        assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6);
991        assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6);
992
993        let bounding_circle = regular_polygon.bounding_circle(isometry);
994        assert_eq!(bounding_circle.center, translation);
995        assert_eq!(bounding_circle.radius(), 1.0);
996    }
997
998    #[test]
999    fn capsule() {
1000        let capsule = Capsule2d::new(0.5, 2.0);
1001        let translation = Vec2::new(2.0, 1.0);
1002        let isometry = Isometry2d::from_translation(translation);
1003
1004        let aabb = capsule.aabb_2d(isometry);
1005        assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5));
1006        assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5));
1007
1008        let bounding_circle = capsule.bounding_circle(isometry);
1009        assert_eq!(bounding_circle.center, translation);
1010        assert_eq!(bounding_circle.radius(), 1.5);
1011    }
1012}