bevy_math/bounding/bounded2d/
primitive_impls.rs

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