Skip to main content

bevy_shape/bounding/bounded2d/
primitive_impls.rs

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