bevy_math/bounding/bounded2d/
primitive_impls.rs

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