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, Rectangle, RegularPolygon, Rhombus, 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 smallvec::SmallVec;
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>) -> SmallVec<[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 = SmallVec::<[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
431#[cfg(test)]
432#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
433mod tests {
434    use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU};
435    use std::println;
436
437    use approx::assert_abs_diff_eq;
438    use glam::Vec2;
439
440    use crate::{
441        bounding::Bounded2d,
442        ops::{self, FloatPow},
443        primitives::{
444            Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,
445            Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d,
446            Triangle2d,
447        },
448        Dir2, Isometry2d, Rot2,
449    };
450
451    #[test]
452    fn circle() {
453        let circle = Circle { radius: 1.0 };
454        let translation = Vec2::new(2.0, 1.0);
455        let isometry = Isometry2d::from_translation(translation);
456
457        let aabb = circle.aabb_2d(isometry);
458        assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
459        assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
460
461        let bounding_circle = circle.bounding_circle(isometry);
462        assert_eq!(bounding_circle.center, translation);
463        assert_eq!(bounding_circle.radius(), 1.0);
464    }
465
466    #[test]
467    // Arcs and circular segments have the same bounding shapes so they share test cases.
468    fn arc_and_segment() {
469        struct TestCase {
470            name: &'static str,
471            arc: Arc2d,
472            translation: Vec2,
473            rotation: f32,
474            aabb_min: Vec2,
475            aabb_max: Vec2,
476            bounding_circle_center: Vec2,
477            bounding_circle_radius: f32,
478        }
479
480        impl TestCase {
481            fn isometry(&self) -> Isometry2d {
482                Isometry2d::new(self.translation, self.rotation.into())
483            }
484        }
485
486        // The apothem of an arc covering 1/6th of a circle.
487        let apothem = ops::sqrt(3.0) / 2.0;
488        let tests = [
489            // Test case: a basic minor arc
490            TestCase {
491                name: "1/6th circle untransformed",
492                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
493                translation: Vec2::ZERO,
494                rotation: 0.0,
495                aabb_min: Vec2::new(-0.5, apothem),
496                aabb_max: Vec2::new(0.5, 1.0),
497                bounding_circle_center: Vec2::new(0.0, apothem),
498                bounding_circle_radius: 0.5,
499            },
500            // Test case: a smaller arc, verifying that radius scaling works
501            TestCase {
502                name: "1/6th circle with radius 0.5",
503                arc: Arc2d::from_radians(0.5, FRAC_PI_3),
504                translation: Vec2::ZERO,
505                rotation: 0.0,
506                aabb_min: Vec2::new(-0.25, apothem / 2.0),
507                aabb_max: Vec2::new(0.25, 0.5),
508                bounding_circle_center: Vec2::new(0.0, apothem / 2.0),
509                bounding_circle_radius: 0.25,
510            },
511            // Test case: a larger arc, verifying that radius scaling works
512            TestCase {
513                name: "1/6th circle with radius 2.0",
514                arc: Arc2d::from_radians(2.0, FRAC_PI_3),
515                translation: Vec2::ZERO,
516                rotation: 0.0,
517                aabb_min: Vec2::new(-1.0, 2.0 * apothem),
518                aabb_max: Vec2::new(1.0, 2.0),
519                bounding_circle_center: Vec2::new(0.0, 2.0 * apothem),
520                bounding_circle_radius: 1.0,
521            },
522            // Test case: translation of a minor arc
523            TestCase {
524                name: "1/6th circle translated",
525                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
526                translation: Vec2::new(2.0, 3.0),
527                rotation: 0.0,
528                aabb_min: Vec2::new(1.5, 3.0 + apothem),
529                aabb_max: Vec2::new(2.5, 4.0),
530                bounding_circle_center: Vec2::new(2.0, 3.0 + apothem),
531                bounding_circle_radius: 0.5,
532            },
533            // Test case: rotation of a minor arc
534            TestCase {
535                name: "1/6th circle rotated",
536                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
537                translation: Vec2::ZERO,
538                // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.
539                rotation: FRAC_PI_6,
540                aabb_min: Vec2::new(-apothem, 0.5),
541                aabb_max: Vec2::new(0.0, 1.0),
542                // The exact coordinates here are not obvious, but can be computed by constructing
543                // an altitude from the midpoint of the chord to the y-axis and using the right triangle
544                // similarity theorem.
545                bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.squared()),
546                bounding_circle_radius: 0.5,
547            },
548            // Test case: handling of axis-aligned extrema
549            TestCase {
550                name: "1/4er circle rotated to be axis-aligned",
551                arc: Arc2d::from_radians(1.0, FRAC_PI_2),
552                translation: Vec2::ZERO,
553                // 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.
554                rotation: -FRAC_PI_4,
555                aabb_min: Vec2::ZERO,
556                aabb_max: Vec2::splat(1.0),
557                bounding_circle_center: Vec2::splat(0.5),
558                bounding_circle_radius: ops::sqrt(2.0) / 2.0,
559            },
560            // Test case: a basic major arc
561            TestCase {
562                name: "5/6th circle untransformed",
563                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
564                translation: Vec2::ZERO,
565                rotation: 0.0,
566                aabb_min: Vec2::new(-1.0, -apothem),
567                aabb_max: Vec2::new(1.0, 1.0),
568                bounding_circle_center: Vec2::ZERO,
569                bounding_circle_radius: 1.0,
570            },
571            // Test case: a translated major arc
572            TestCase {
573                name: "5/6th circle translated",
574                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
575                translation: Vec2::new(2.0, 3.0),
576                rotation: 0.0,
577                aabb_min: Vec2::new(1.0, 3.0 - apothem),
578                aabb_max: Vec2::new(3.0, 4.0),
579                bounding_circle_center: Vec2::new(2.0, 3.0),
580                bounding_circle_radius: 1.0,
581            },
582            // Test case: a rotated major arc, with inverted left/right angles
583            TestCase {
584                name: "5/6th circle rotated",
585                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
586                translation: Vec2::ZERO,
587                // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.
588                rotation: FRAC_PI_6,
589                aabb_min: Vec2::new(-1.0, -1.0),
590                aabb_max: Vec2::new(1.0, 1.0),
591                bounding_circle_center: Vec2::ZERO,
592                bounding_circle_radius: 1.0,
593            },
594        ];
595
596        for test in tests {
597            #[cfg(feature = "std")]
598            println!("subtest case: {}", test.name);
599            let segment: CircularSegment = test.arc.into();
600
601            let arc_aabb = test.arc.aabb_2d(test.isometry());
602            assert_abs_diff_eq!(test.aabb_min, arc_aabb.min);
603            assert_abs_diff_eq!(test.aabb_max, arc_aabb.max);
604            let segment_aabb = segment.aabb_2d(test.isometry());
605            assert_abs_diff_eq!(test.aabb_min, segment_aabb.min);
606            assert_abs_diff_eq!(test.aabb_max, segment_aabb.max);
607
608            let arc_bounding_circle = test.arc.bounding_circle(test.isometry());
609            assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center);
610            assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius());
611            let segment_bounding_circle = segment.bounding_circle(test.isometry());
612            assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center);
613            assert_abs_diff_eq!(
614                test.bounding_circle_radius,
615                segment_bounding_circle.radius()
616            );
617        }
618    }
619
620    #[test]
621    fn circular_sector() {
622        struct TestCase {
623            name: &'static str,
624            arc: Arc2d,
625            translation: Vec2,
626            rotation: f32,
627            aabb_min: Vec2,
628            aabb_max: Vec2,
629            bounding_circle_center: Vec2,
630            bounding_circle_radius: f32,
631        }
632
633        impl TestCase {
634            fn isometry(&self) -> Isometry2d {
635                Isometry2d::new(self.translation, self.rotation.into())
636            }
637        }
638
639        // The apothem of an arc covering 1/6th of a circle.
640        let apothem = ops::sqrt(3.0) / 2.0;
641        let inv_sqrt_3 = ops::sqrt(3.0).recip();
642        let tests = [
643            // Test case: A sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center
644            TestCase {
645                name: "1/3rd circle",
646                arc: Arc2d::from_radians(1.0, TAU / 3.0),
647                translation: Vec2::ZERO,
648                rotation: 0.0,
649                aabb_min: Vec2::new(-apothem, 0.0),
650                aabb_max: Vec2::new(apothem, 1.0),
651                bounding_circle_center: Vec2::new(0.0, 0.5),
652                bounding_circle_radius: apothem,
653            },
654            // The remaining test cases are selected as for arc_and_segment.
655            TestCase {
656                name: "1/6th circle untransformed",
657                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
658                translation: Vec2::ZERO,
659                rotation: 0.0,
660                aabb_min: Vec2::new(-0.5, 0.0),
661                aabb_max: Vec2::new(0.5, 1.0),
662                // The bounding circle is a circumcircle of an equilateral triangle with side length 1.
663                // The distance from the corner to the center of such a triangle is 1/sqrt(3).
664                bounding_circle_center: Vec2::new(0.0, inv_sqrt_3),
665                bounding_circle_radius: inv_sqrt_3,
666            },
667            TestCase {
668                name: "1/6th circle with radius 0.5",
669                arc: Arc2d::from_radians(0.5, FRAC_PI_3),
670                translation: Vec2::ZERO,
671                rotation: 0.0,
672                aabb_min: Vec2::new(-0.25, 0.0),
673                aabb_max: Vec2::new(0.25, 0.5),
674                bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0),
675                bounding_circle_radius: inv_sqrt_3 / 2.0,
676            },
677            TestCase {
678                name: "1/6th circle with radius 2.0",
679                arc: Arc2d::from_radians(2.0, FRAC_PI_3),
680                translation: Vec2::ZERO,
681                rotation: 0.0,
682                aabb_min: Vec2::new(-1.0, 0.0),
683                aabb_max: Vec2::new(1.0, 2.0),
684                bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3),
685                bounding_circle_radius: 2.0 * inv_sqrt_3,
686            },
687            TestCase {
688                name: "1/6th circle translated",
689                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
690                translation: Vec2::new(2.0, 3.0),
691                rotation: 0.0,
692                aabb_min: Vec2::new(1.5, 3.0),
693                aabb_max: Vec2::new(2.5, 4.0),
694                bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3),
695                bounding_circle_radius: inv_sqrt_3,
696            },
697            TestCase {
698                name: "1/6th circle rotated",
699                arc: Arc2d::from_radians(1.0, FRAC_PI_3),
700                translation: Vec2::ZERO,
701                // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.
702                rotation: FRAC_PI_6,
703                aabb_min: Vec2::new(-apothem, 0.0),
704                aabb_max: Vec2::new(0.0, 1.0),
705                // The x-coordinate is now the inradius of the equilateral triangle, which is sqrt(3)/2.
706                bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5),
707                bounding_circle_radius: inv_sqrt_3,
708            },
709            TestCase {
710                name: "1/4er circle rotated to be axis-aligned",
711                arc: Arc2d::from_radians(1.0, FRAC_PI_2),
712                translation: Vec2::ZERO,
713                // 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.
714                rotation: -FRAC_PI_4,
715                aabb_min: Vec2::ZERO,
716                aabb_max: Vec2::splat(1.0),
717                bounding_circle_center: Vec2::splat(0.5),
718                bounding_circle_radius: ops::sqrt(2.0) / 2.0,
719            },
720            TestCase {
721                name: "5/6th circle untransformed",
722                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
723                translation: Vec2::ZERO,
724                rotation: 0.0,
725                aabb_min: Vec2::new(-1.0, -apothem),
726                aabb_max: Vec2::new(1.0, 1.0),
727                bounding_circle_center: Vec2::ZERO,
728                bounding_circle_radius: 1.0,
729            },
730            TestCase {
731                name: "5/6th circle translated",
732                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
733                translation: Vec2::new(2.0, 3.0),
734                rotation: 0.0,
735                aabb_min: Vec2::new(1.0, 3.0 - apothem),
736                aabb_max: Vec2::new(3.0, 4.0),
737                bounding_circle_center: Vec2::new(2.0, 3.0),
738                bounding_circle_radius: 1.0,
739            },
740            TestCase {
741                name: "5/6th circle rotated",
742                arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
743                translation: Vec2::ZERO,
744                // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.
745                rotation: FRAC_PI_6,
746                aabb_min: Vec2::new(-1.0, -1.0),
747                aabb_max: Vec2::new(1.0, 1.0),
748                bounding_circle_center: Vec2::ZERO,
749                bounding_circle_radius: 1.0,
750            },
751        ];
752
753        for test in tests {
754            #[cfg(feature = "std")]
755            println!("subtest case: {}", test.name);
756            let sector: CircularSector = test.arc.into();
757
758            let aabb = sector.aabb_2d(test.isometry());
759            assert_abs_diff_eq!(test.aabb_min, aabb.min);
760            assert_abs_diff_eq!(test.aabb_max, aabb.max);
761
762            let bounding_circle = sector.bounding_circle(test.isometry());
763            assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center);
764            assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius());
765        }
766    }
767
768    #[test]
769    fn ellipse() {
770        let ellipse = Ellipse::new(1.0, 0.5);
771        let translation = Vec2::new(2.0, 1.0);
772        let isometry = Isometry2d::from_translation(translation);
773
774        let aabb = ellipse.aabb_2d(isometry);
775        assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
776        assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
777
778        let bounding_circle = ellipse.bounding_circle(isometry);
779        assert_eq!(bounding_circle.center, translation);
780        assert_eq!(bounding_circle.radius(), 1.0);
781    }
782
783    #[test]
784    fn annulus() {
785        let annulus = Annulus::new(1.0, 2.0);
786        let translation = Vec2::new(2.0, 1.0);
787        let rotation = Rot2::radians(1.0);
788        let isometry = Isometry2d::new(translation, rotation);
789
790        let aabb = annulus.aabb_2d(isometry);
791        assert_eq!(aabb.min, Vec2::new(0.0, -1.0));
792        assert_eq!(aabb.max, Vec2::new(4.0, 3.0));
793
794        let bounding_circle = annulus.bounding_circle(isometry);
795        assert_eq!(bounding_circle.center, translation);
796        assert_eq!(bounding_circle.radius(), 2.0);
797    }
798
799    #[test]
800    fn rhombus() {
801        let rhombus = Rhombus::new(2.0, 1.0);
802        let translation = Vec2::new(2.0, 1.0);
803        let rotation = Rot2::radians(FRAC_PI_4);
804        let isometry = Isometry2d::new(translation, rotation);
805
806        let aabb = rhombus.aabb_2d(isometry);
807        assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323));
808        assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068));
809
810        let bounding_circle = rhombus.bounding_circle(isometry);
811        assert_eq!(bounding_circle.center, translation);
812        assert_eq!(bounding_circle.radius(), 1.0);
813
814        let rhombus = Rhombus::new(0.0, 0.0);
815        let translation = Vec2::new(0.0, 0.0);
816        let isometry = Isometry2d::new(translation, rotation);
817
818        let aabb = rhombus.aabb_2d(isometry);
819        assert_eq!(aabb.min, Vec2::new(0.0, 0.0));
820        assert_eq!(aabb.max, Vec2::new(0.0, 0.0));
821
822        let bounding_circle = rhombus.bounding_circle(isometry);
823        assert_eq!(bounding_circle.center, translation);
824        assert_eq!(bounding_circle.radius(), 0.0);
825    }
826
827    #[test]
828    fn plane() {
829        let translation = Vec2::new(2.0, 1.0);
830        let isometry = Isometry2d::from_translation(translation);
831
832        let aabb1 = Plane2d::new(Vec2::X).aabb_2d(isometry);
833        assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
834        assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
835
836        let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(isometry);
837        assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
838        assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
839
840        let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(isometry);
841        assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
842        assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
843
844        let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(isometry);
845        assert_eq!(bounding_circle.center, translation);
846        assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
847    }
848
849    #[test]
850    fn line() {
851        let translation = Vec2::new(2.0, 1.0);
852        let isometry = Isometry2d::from_translation(translation);
853
854        let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(isometry);
855        assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
856        assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
857
858        let aabb2 = Line2d { direction: Dir2::X }.aabb_2d(isometry);
859        assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
860        assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
861
862        let aabb3 = Line2d {
863            direction: Dir2::from_xy(1.0, 1.0).unwrap(),
864        }
865        .aabb_2d(isometry);
866        assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
867        assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
868
869        let bounding_circle = Line2d { direction: Dir2::Y }.bounding_circle(isometry);
870        assert_eq!(bounding_circle.center, translation);
871        assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
872    }
873
874    #[test]
875    fn segment() {
876        let segment = Segment2d::new(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5));
877        let translation = Vec2::new(2.0, 1.0);
878        let isometry = Isometry2d::from_translation(translation);
879
880        let aabb = segment.aabb_2d(isometry);
881        assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
882        assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
883
884        let bounding_circle = segment.bounding_circle(isometry);
885        assert_eq!(bounding_circle.center, translation);
886        assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));
887    }
888
889    #[test]
890    fn polyline() {
891        let polyline = Polyline2d::new([
892            Vec2::ONE,
893            Vec2::new(-1.0, 1.0),
894            Vec2::NEG_ONE,
895            Vec2::new(1.0, -1.0),
896        ]);
897        let translation = Vec2::new(2.0, 1.0);
898        let isometry = Isometry2d::from_translation(translation);
899
900        let aabb = polyline.aabb_2d(isometry);
901        assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
902        assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
903
904        let bounding_circle = polyline.bounding_circle(isometry);
905        assert_eq!(bounding_circle.center, translation);
906        assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);
907    }
908
909    #[test]
910    fn acute_triangle() {
911        let acute_triangle =
912            Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0));
913        let translation = Vec2::new(2.0, 1.0);
914        let isometry = Isometry2d::from_translation(translation);
915
916        let aabb = acute_triangle.aabb_2d(isometry);
917        assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
918        assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
919
920        // For acute triangles, the center is the circumcenter
921        let (Circle { radius }, circumcenter) = acute_triangle.circumcircle();
922        let bounding_circle = acute_triangle.bounding_circle(isometry);
923        assert_eq!(bounding_circle.center, circumcenter + translation);
924        assert_eq!(bounding_circle.radius(), radius);
925    }
926
927    #[test]
928    fn obtuse_triangle() {
929        let obtuse_triangle = Triangle2d::new(
930            Vec2::new(0.0, 1.0),
931            Vec2::new(-10.0, -1.0),
932            Vec2::new(10.0, -1.0),
933        );
934        let translation = Vec2::new(2.0, 1.0);
935        let isometry = Isometry2d::from_translation(translation);
936
937        let aabb = obtuse_triangle.aabb_2d(isometry);
938        assert_eq!(aabb.min, Vec2::new(-8.0, 0.0));
939        assert_eq!(aabb.max, Vec2::new(12.0, 2.0));
940
941        // For obtuse and right triangles, the center is the midpoint of the longest side (diameter of bounding circle)
942        let bounding_circle = obtuse_triangle.bounding_circle(isometry);
943        assert_eq!(bounding_circle.center, translation - Vec2::Y);
944        assert_eq!(bounding_circle.radius(), 10.0);
945    }
946
947    #[test]
948    fn rectangle() {
949        let rectangle = Rectangle::new(2.0, 1.0);
950        let translation = Vec2::new(2.0, 1.0);
951
952        let aabb = rectangle.aabb_2d(Isometry2d::new(translation, Rot2::radians(FRAC_PI_4)));
953        let expected_half_size = Vec2::splat(1.0606601);
954        assert_eq!(aabb.min, translation - expected_half_size);
955        assert_eq!(aabb.max, translation + expected_half_size);
956
957        let bounding_circle = rectangle.bounding_circle(Isometry2d::from_translation(translation));
958        assert_eq!(bounding_circle.center, translation);
959        assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));
960    }
961
962    #[test]
963    fn polygon() {
964        let polygon = Polygon::new([
965            Vec2::ONE,
966            Vec2::new(-1.0, 1.0),
967            Vec2::NEG_ONE,
968            Vec2::new(1.0, -1.0),
969        ]);
970        let translation = Vec2::new(2.0, 1.0);
971        let isometry = Isometry2d::from_translation(translation);
972
973        let aabb = polygon.aabb_2d(isometry);
974        assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
975        assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
976
977        let bounding_circle = polygon.bounding_circle(isometry);
978        assert_eq!(bounding_circle.center, translation);
979        assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);
980    }
981
982    #[test]
983    fn regular_polygon() {
984        let regular_polygon = RegularPolygon::new(1.0, 5);
985        let translation = Vec2::new(2.0, 1.0);
986        let isometry = Isometry2d::from_translation(translation);
987
988        let aabb = regular_polygon.aabb_2d(isometry);
989        assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6);
990        assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6);
991
992        let bounding_circle = regular_polygon.bounding_circle(isometry);
993        assert_eq!(bounding_circle.center, translation);
994        assert_eq!(bounding_circle.radius(), 1.0);
995    }
996
997    #[test]
998    fn capsule() {
999        let capsule = Capsule2d::new(0.5, 2.0);
1000        let translation = Vec2::new(2.0, 1.0);
1001        let isometry = Isometry2d::from_translation(translation);
1002
1003        let aabb = capsule.aabb_2d(isometry);
1004        assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5));
1005        assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5));
1006
1007        let bounding_circle = capsule.bounding_circle(isometry);
1008        assert_eq!(bounding_circle.center, translation);
1009        assert_eq!(bounding_circle.radius(), 1.5);
1010    }
1011}