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