bevy_math/bounding/bounded2d/
mod.rs

1mod primitive_impls;
2
3use super::{BoundingVolume, IntersectsVolume};
4use crate::{
5    prelude::{Mat2, Rot2, Vec2},
6    FloatPow, Isometry2d,
7};
8
9#[cfg(feature = "bevy_reflect")]
10use bevy_reflect::Reflect;
11
12/// Computes the geometric center of the given set of points.
13#[inline(always)]
14fn point_cloud_2d_center(points: &[Vec2]) -> Vec2 {
15    assert!(
16        !points.is_empty(),
17        "cannot compute the center of an empty set of points"
18    );
19
20    let denom = 1.0 / points.len() as f32;
21    points.iter().fold(Vec2::ZERO, |acc, point| acc + *point) * denom
22}
23
24/// A trait with methods that return 2D bounding volumes for a shape.
25pub trait Bounded2d {
26    /// Get an axis-aligned bounding box for the shape translated and rotated by the given isometry.
27    fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d;
28    /// Get a bounding circle for the shape translated and rotated by the given isometry.
29    fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle;
30}
31
32/// A 2D axis-aligned bounding box, or bounding rectangle
33#[doc(alias = "BoundingRectangle")]
34#[derive(Clone, Copy, Debug)]
35#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
36pub struct Aabb2d {
37    /// The minimum, conventionally bottom-left, point of the box
38    pub min: Vec2,
39    /// The maximum, conventionally top-right, point of the box
40    pub max: Vec2,
41}
42
43impl Aabb2d {
44    /// Constructs an AABB from its center and half-size.
45    #[inline(always)]
46    pub fn new(center: Vec2, half_size: Vec2) -> Self {
47        debug_assert!(half_size.x >= 0.0 && half_size.y >= 0.0);
48        Self {
49            min: center - half_size,
50            max: center + half_size,
51        }
52    }
53
54    /// Computes the smallest [`Aabb2d`] containing the given set of points,
55    /// transformed by the rotation and translation of the given isometry.
56    ///
57    /// # Panics
58    ///
59    /// Panics if the given set of points is empty.
60    #[inline(always)]
61    pub fn from_point_cloud(isometry: impl Into<Isometry2d>, points: &[Vec2]) -> Aabb2d {
62        let isometry = isometry.into();
63
64        // Transform all points by rotation
65        let mut iter = points.iter().map(|point| isometry.rotation * *point);
66
67        let first = iter
68            .next()
69            .expect("point cloud must contain at least one point for Aabb2d construction");
70
71        let (min, max) = iter.fold((first, first), |(prev_min, prev_max), point| {
72            (point.min(prev_min), point.max(prev_max))
73        });
74
75        Aabb2d {
76            min: min + isometry.translation,
77            max: max + isometry.translation,
78        }
79    }
80
81    /// Computes the smallest [`BoundingCircle`] containing this [`Aabb2d`].
82    #[inline(always)]
83    pub fn bounding_circle(&self) -> BoundingCircle {
84        let radius = self.min.distance(self.max) / 2.0;
85        BoundingCircle::new(self.center(), radius)
86    }
87
88    /// Finds the point on the AABB that is closest to the given `point`.
89    ///
90    /// If the point is outside the AABB, the returned point will be on the perimeter of the AABB.
91    /// Otherwise, it will be inside the AABB and returned as is.
92    #[inline(always)]
93    pub fn closest_point(&self, point: Vec2) -> Vec2 {
94        // Clamp point coordinates to the AABB
95        point.clamp(self.min, self.max)
96    }
97}
98
99impl BoundingVolume for Aabb2d {
100    type Translation = Vec2;
101    type Rotation = Rot2;
102    type HalfSize = Vec2;
103
104    #[inline(always)]
105    fn center(&self) -> Self::Translation {
106        (self.min + self.max) / 2.
107    }
108
109    #[inline(always)]
110    fn half_size(&self) -> Self::HalfSize {
111        (self.max - self.min) / 2.
112    }
113
114    #[inline(always)]
115    fn visible_area(&self) -> f32 {
116        let b = self.max - self.min;
117        b.x * b.y
118    }
119
120    #[inline(always)]
121    fn contains(&self, other: &Self) -> bool {
122        other.min.x >= self.min.x
123            && other.min.y >= self.min.y
124            && other.max.x <= self.max.x
125            && other.max.y <= self.max.y
126    }
127
128    #[inline(always)]
129    fn merge(&self, other: &Self) -> Self {
130        Self {
131            min: self.min.min(other.min),
132            max: self.max.max(other.max),
133        }
134    }
135
136    #[inline(always)]
137    fn grow(&self, amount: impl Into<Self::HalfSize>) -> Self {
138        let amount = amount.into();
139        let b = Self {
140            min: self.min - amount,
141            max: self.max + amount,
142        };
143        debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
144        b
145    }
146
147    #[inline(always)]
148    fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {
149        let amount = amount.into();
150        let b = Self {
151            min: self.min + amount,
152            max: self.max - amount,
153        };
154        debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
155        b
156    }
157
158    #[inline(always)]
159    fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
160        let scale = scale.into();
161        let b = Self {
162            min: self.center() - (self.half_size() * scale),
163            max: self.center() + (self.half_size() * scale),
164        };
165        debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
166        b
167    }
168
169    /// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
170    ///
171    /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
172    ///
173    /// Note that the result may not be as tightly fitting as the original, and repeated rotations
174    /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
175    /// and consider storing the original AABB and rotating that every time instead.
176    #[inline(always)]
177    fn transformed_by(
178        mut self,
179        translation: impl Into<Self::Translation>,
180        rotation: impl Into<Self::Rotation>,
181    ) -> Self {
182        self.transform_by(translation, rotation);
183        self
184    }
185
186    /// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
187    ///
188    /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
189    ///
190    /// Note that the result may not be as tightly fitting as the original, and repeated rotations
191    /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
192    /// and consider storing the original AABB and rotating that every time instead.
193    #[inline(always)]
194    fn transform_by(
195        &mut self,
196        translation: impl Into<Self::Translation>,
197        rotation: impl Into<Self::Rotation>,
198    ) {
199        self.rotate_by(rotation);
200        self.translate_by(translation);
201    }
202
203    #[inline(always)]
204    fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
205        let translation = translation.into();
206        self.min += translation;
207        self.max += translation;
208    }
209
210    /// Rotates the bounding volume around the origin by the given rotation.
211    ///
212    /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
213    ///
214    /// Note that the result may not be as tightly fitting as the original, and repeated rotations
215    /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
216    /// and consider storing the original AABB and rotating that every time instead.
217    #[inline(always)]
218    fn rotated_by(mut self, rotation: impl Into<Self::Rotation>) -> Self {
219        self.rotate_by(rotation);
220        self
221    }
222
223    /// Rotates the bounding volume around the origin by the given rotation.
224    ///
225    /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
226    ///
227    /// Note that the result may not be as tightly fitting as the original, and repeated rotations
228    /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
229    /// and consider storing the original AABB and rotating that every time instead.
230    #[inline(always)]
231    fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
232        let rotation: Rot2 = rotation.into();
233        let abs_rot_mat = Mat2::from_cols(
234            Vec2::new(rotation.cos, rotation.sin),
235            Vec2::new(rotation.sin, rotation.cos),
236        );
237        let half_size = abs_rot_mat * self.half_size();
238        *self = Self::new(rotation * self.center(), half_size);
239    }
240}
241
242impl IntersectsVolume<Self> for Aabb2d {
243    #[inline(always)]
244    fn intersects(&self, other: &Self) -> bool {
245        let x_overlaps = self.min.x <= other.max.x && self.max.x >= other.min.x;
246        let y_overlaps = self.min.y <= other.max.y && self.max.y >= other.min.y;
247        x_overlaps && y_overlaps
248    }
249}
250
251impl IntersectsVolume<BoundingCircle> for Aabb2d {
252    #[inline(always)]
253    fn intersects(&self, circle: &BoundingCircle) -> bool {
254        let closest_point = self.closest_point(circle.center);
255        let distance_squared = circle.center.distance_squared(closest_point);
256        let radius_squared = circle.radius().squared();
257        distance_squared <= radius_squared
258    }
259}
260
261#[cfg(test)]
262mod aabb2d_tests {
263    use super::Aabb2d;
264    use crate::{
265        bounding::{BoundingCircle, BoundingVolume, IntersectsVolume},
266        ops, Vec2,
267    };
268
269    #[test]
270    fn center() {
271        let aabb = Aabb2d {
272            min: Vec2::new(-0.5, -1.),
273            max: Vec2::new(1., 1.),
274        };
275        assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < f32::EPSILON);
276        let aabb = Aabb2d {
277            min: Vec2::new(5., -10.),
278            max: Vec2::new(10., -5.),
279        };
280        assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < f32::EPSILON);
281    }
282
283    #[test]
284    fn half_size() {
285        let aabb = Aabb2d {
286            min: Vec2::new(-0.5, -1.),
287            max: Vec2::new(1., 1.),
288        };
289        let half_size = aabb.half_size();
290        assert!((half_size - Vec2::new(0.75, 1.)).length() < f32::EPSILON);
291    }
292
293    #[test]
294    fn area() {
295        let aabb = Aabb2d {
296            min: Vec2::new(-1., -1.),
297            max: Vec2::new(1., 1.),
298        };
299        assert!((aabb.visible_area() - 4.).abs() < f32::EPSILON);
300        let aabb = Aabb2d {
301            min: Vec2::new(0., 0.),
302            max: Vec2::new(1., 0.5),
303        };
304        assert!((aabb.visible_area() - 0.5).abs() < f32::EPSILON);
305    }
306
307    #[test]
308    fn contains() {
309        let a = Aabb2d {
310            min: Vec2::new(-1., -1.),
311            max: Vec2::new(1., 1.),
312        };
313        let b = Aabb2d {
314            min: Vec2::new(-2., -1.),
315            max: Vec2::new(1., 1.),
316        };
317        assert!(!a.contains(&b));
318        let b = Aabb2d {
319            min: Vec2::new(-0.25, -0.8),
320            max: Vec2::new(1., 1.),
321        };
322        assert!(a.contains(&b));
323    }
324
325    #[test]
326    fn merge() {
327        let a = Aabb2d {
328            min: Vec2::new(-1., -1.),
329            max: Vec2::new(1., 0.5),
330        };
331        let b = Aabb2d {
332            min: Vec2::new(-2., -0.5),
333            max: Vec2::new(0.75, 1.),
334        };
335        let merged = a.merge(&b);
336        assert!((merged.min - Vec2::new(-2., -1.)).length() < f32::EPSILON);
337        assert!((merged.max - Vec2::new(1., 1.)).length() < f32::EPSILON);
338        assert!(merged.contains(&a));
339        assert!(merged.contains(&b));
340        assert!(!a.contains(&merged));
341        assert!(!b.contains(&merged));
342    }
343
344    #[test]
345    fn grow() {
346        let a = Aabb2d {
347            min: Vec2::new(-1., -1.),
348            max: Vec2::new(1., 1.),
349        };
350        let padded = a.grow(Vec2::ONE);
351        assert!((padded.min - Vec2::new(-2., -2.)).length() < f32::EPSILON);
352        assert!((padded.max - Vec2::new(2., 2.)).length() < f32::EPSILON);
353        assert!(padded.contains(&a));
354        assert!(!a.contains(&padded));
355    }
356
357    #[test]
358    fn shrink() {
359        let a = Aabb2d {
360            min: Vec2::new(-2., -2.),
361            max: Vec2::new(2., 2.),
362        };
363        let shrunk = a.shrink(Vec2::ONE);
364        assert!((shrunk.min - Vec2::new(-1., -1.)).length() < f32::EPSILON);
365        assert!((shrunk.max - Vec2::new(1., 1.)).length() < f32::EPSILON);
366        assert!(a.contains(&shrunk));
367        assert!(!shrunk.contains(&a));
368    }
369
370    #[test]
371    fn scale_around_center() {
372        let a = Aabb2d {
373            min: Vec2::NEG_ONE,
374            max: Vec2::ONE,
375        };
376        let scaled = a.scale_around_center(Vec2::splat(2.));
377        assert!((scaled.min - Vec2::splat(-2.)).length() < f32::EPSILON);
378        assert!((scaled.max - Vec2::splat(2.)).length() < f32::EPSILON);
379        assert!(!a.contains(&scaled));
380        assert!(scaled.contains(&a));
381    }
382
383    #[test]
384    fn transform() {
385        let a = Aabb2d {
386            min: Vec2::new(-2.0, -2.0),
387            max: Vec2::new(2.0, 2.0),
388        };
389        let transformed = a.transformed_by(Vec2::new(2.0, -2.0), core::f32::consts::FRAC_PI_4);
390        let half_length = ops::hypot(2.0, 2.0);
391        assert_eq!(
392            transformed.min,
393            Vec2::new(2.0 - half_length, -half_length - 2.0)
394        );
395        assert_eq!(
396            transformed.max,
397            Vec2::new(2.0 + half_length, half_length - 2.0)
398        );
399    }
400
401    #[test]
402    fn closest_point() {
403        let aabb = Aabb2d {
404            min: Vec2::NEG_ONE,
405            max: Vec2::ONE,
406        };
407        assert_eq!(aabb.closest_point(Vec2::X * 10.0), Vec2::X);
408        assert_eq!(aabb.closest_point(Vec2::NEG_ONE * 10.0), Vec2::NEG_ONE);
409        assert_eq!(
410            aabb.closest_point(Vec2::new(0.25, 0.1)),
411            Vec2::new(0.25, 0.1)
412        );
413    }
414
415    #[test]
416    fn intersect_aabb() {
417        let aabb = Aabb2d {
418            min: Vec2::NEG_ONE,
419            max: Vec2::ONE,
420        };
421        assert!(aabb.intersects(&aabb));
422        assert!(aabb.intersects(&Aabb2d {
423            min: Vec2::new(0.5, 0.5),
424            max: Vec2::new(2.0, 2.0),
425        }));
426        assert!(aabb.intersects(&Aabb2d {
427            min: Vec2::new(-2.0, -2.0),
428            max: Vec2::new(-0.5, -0.5),
429        }));
430        assert!(!aabb.intersects(&Aabb2d {
431            min: Vec2::new(1.1, 0.0),
432            max: Vec2::new(2.0, 0.5),
433        }));
434    }
435
436    #[test]
437    fn intersect_bounding_circle() {
438        let aabb = Aabb2d {
439            min: Vec2::NEG_ONE,
440            max: Vec2::ONE,
441        };
442        assert!(aabb.intersects(&BoundingCircle::new(Vec2::ZERO, 1.0)));
443        assert!(aabb.intersects(&BoundingCircle::new(Vec2::ONE * 1.5, 1.0)));
444        assert!(aabb.intersects(&BoundingCircle::new(Vec2::NEG_ONE * 1.5, 1.0)));
445        assert!(!aabb.intersects(&BoundingCircle::new(Vec2::ONE * 1.75, 1.0)));
446    }
447}
448
449use crate::primitives::Circle;
450
451/// A bounding circle
452#[derive(Clone, Copy, Debug)]
453#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
454pub struct BoundingCircle {
455    /// The center of the bounding circle
456    pub center: Vec2,
457    /// The circle
458    pub circle: Circle,
459}
460
461impl BoundingCircle {
462    /// Constructs a bounding circle from its center and radius.
463    #[inline(always)]
464    pub fn new(center: Vec2, radius: f32) -> Self {
465        debug_assert!(radius >= 0.);
466        Self {
467            center,
468            circle: Circle { radius },
469        }
470    }
471
472    /// Computes a [`BoundingCircle`] containing the given set of points,
473    /// transformed by the rotation and translation of the given isometry.
474    ///
475    /// The bounding circle is not guaranteed to be the smallest possible.
476    #[inline(always)]
477    pub fn from_point_cloud(isometry: impl Into<Isometry2d>, points: &[Vec2]) -> BoundingCircle {
478        let isometry = isometry.into();
479
480        let center = point_cloud_2d_center(points);
481        let mut radius_squared = 0.0;
482
483        for point in points {
484            // Get squared version to avoid unnecessary sqrt calls
485            let distance_squared = point.distance_squared(center);
486            if distance_squared > radius_squared {
487                radius_squared = distance_squared;
488            }
489        }
490
491        BoundingCircle::new(isometry * center, radius_squared.sqrt())
492    }
493
494    /// Get the radius of the bounding circle
495    #[inline(always)]
496    pub fn radius(&self) -> f32 {
497        self.circle.radius
498    }
499
500    /// Computes the smallest [`Aabb2d`] containing this [`BoundingCircle`].
501    #[inline(always)]
502    pub fn aabb_2d(&self) -> Aabb2d {
503        Aabb2d {
504            min: self.center - Vec2::splat(self.radius()),
505            max: self.center + Vec2::splat(self.radius()),
506        }
507    }
508
509    /// Finds the point on the bounding circle that is closest to the given `point`.
510    ///
511    /// If the point is outside the circle, the returned point will be on the perimeter of the circle.
512    /// Otherwise, it will be inside the circle and returned as is.
513    #[inline(always)]
514    pub fn closest_point(&self, point: Vec2) -> Vec2 {
515        self.circle.closest_point(point - self.center) + self.center
516    }
517}
518
519impl BoundingVolume for BoundingCircle {
520    type Translation = Vec2;
521    type Rotation = Rot2;
522    type HalfSize = f32;
523
524    #[inline(always)]
525    fn center(&self) -> Self::Translation {
526        self.center
527    }
528
529    #[inline(always)]
530    fn half_size(&self) -> Self::HalfSize {
531        self.radius()
532    }
533
534    #[inline(always)]
535    fn visible_area(&self) -> f32 {
536        core::f32::consts::PI * self.radius() * self.radius()
537    }
538
539    #[inline(always)]
540    fn contains(&self, other: &Self) -> bool {
541        let diff = self.radius() - other.radius();
542        self.center.distance_squared(other.center) <= diff.squared().copysign(diff)
543    }
544
545    #[inline(always)]
546    fn merge(&self, other: &Self) -> Self {
547        let diff = other.center - self.center;
548        let length = diff.length();
549        if self.radius() >= length + other.radius() {
550            return *self;
551        }
552        if other.radius() >= length + self.radius() {
553            return *other;
554        }
555        let dir = diff / length;
556        Self::new(
557            (self.center + other.center) / 2. + dir * ((other.radius() - self.radius()) / 2.),
558            (length + self.radius() + other.radius()) / 2.,
559        )
560    }
561
562    #[inline(always)]
563    fn grow(&self, amount: impl Into<Self::HalfSize>) -> Self {
564        let amount = amount.into();
565        debug_assert!(amount >= 0.);
566        Self::new(self.center, self.radius() + amount)
567    }
568
569    #[inline(always)]
570    fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {
571        let amount = amount.into();
572        debug_assert!(amount >= 0.);
573        debug_assert!(self.radius() >= amount);
574        Self::new(self.center, self.radius() - amount)
575    }
576
577    #[inline(always)]
578    fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
579        let scale = scale.into();
580        debug_assert!(scale >= 0.);
581        Self::new(self.center, self.radius() * scale)
582    }
583
584    #[inline(always)]
585    fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
586        self.center += translation.into();
587    }
588
589    #[inline(always)]
590    fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
591        let rotation: Rot2 = rotation.into();
592        self.center = rotation * self.center;
593    }
594}
595
596impl IntersectsVolume<Self> for BoundingCircle {
597    #[inline(always)]
598    fn intersects(&self, other: &Self) -> bool {
599        let center_distance_squared = self.center.distance_squared(other.center);
600        let radius_sum_squared = (self.radius() + other.radius()).squared();
601        center_distance_squared <= radius_sum_squared
602    }
603}
604
605impl IntersectsVolume<Aabb2d> for BoundingCircle {
606    #[inline(always)]
607    fn intersects(&self, aabb: &Aabb2d) -> bool {
608        aabb.intersects(self)
609    }
610}
611
612#[cfg(test)]
613mod bounding_circle_tests {
614    use super::BoundingCircle;
615    use crate::{
616        bounding::{BoundingVolume, IntersectsVolume},
617        Vec2,
618    };
619
620    #[test]
621    fn area() {
622        let circle = BoundingCircle::new(Vec2::ONE, 5.);
623        // Since this number is messy we check it with a higher threshold
624        assert!((circle.visible_area() - 78.5398).abs() < 0.001);
625    }
626
627    #[test]
628    fn contains() {
629        let a = BoundingCircle::new(Vec2::ONE, 5.);
630        let b = BoundingCircle::new(Vec2::new(5.5, 1.), 1.);
631        assert!(!a.contains(&b));
632        let b = BoundingCircle::new(Vec2::new(1., -3.5), 0.5);
633        assert!(a.contains(&b));
634    }
635
636    #[test]
637    fn contains_identical() {
638        let a = BoundingCircle::new(Vec2::ONE, 5.);
639        assert!(a.contains(&a));
640    }
641
642    #[test]
643    fn merge() {
644        // When merging two circles that don't contain each other, we find a center position that
645        // contains both
646        let a = BoundingCircle::new(Vec2::ONE, 5.);
647        let b = BoundingCircle::new(Vec2::new(1., -4.), 1.);
648        let merged = a.merge(&b);
649        assert!((merged.center - Vec2::new(1., 0.5)).length() < f32::EPSILON);
650        assert!((merged.radius() - 5.5).abs() < f32::EPSILON);
651        assert!(merged.contains(&a));
652        assert!(merged.contains(&b));
653        assert!(!a.contains(&merged));
654        assert!(!b.contains(&merged));
655
656        // When one circle contains the other circle, we use the bigger circle
657        let b = BoundingCircle::new(Vec2::ZERO, 3.);
658        assert!(a.contains(&b));
659        let merged = a.merge(&b);
660        assert_eq!(merged.center, a.center);
661        assert_eq!(merged.radius(), a.radius());
662
663        // When two circles are at the same point, we use the bigger radius
664        let b = BoundingCircle::new(Vec2::ONE, 6.);
665        let merged = a.merge(&b);
666        assert_eq!(merged.center, a.center);
667        assert_eq!(merged.radius(), b.radius());
668    }
669
670    #[test]
671    fn merge_identical() {
672        let a = BoundingCircle::new(Vec2::ONE, 5.);
673        let merged = a.merge(&a);
674        assert_eq!(merged.center, a.center);
675        assert_eq!(merged.radius(), a.radius());
676    }
677
678    #[test]
679    fn grow() {
680        let a = BoundingCircle::new(Vec2::ONE, 5.);
681        let padded = a.grow(1.25);
682        assert!((padded.radius() - 6.25).abs() < f32::EPSILON);
683        assert!(padded.contains(&a));
684        assert!(!a.contains(&padded));
685    }
686
687    #[test]
688    fn shrink() {
689        let a = BoundingCircle::new(Vec2::ONE, 5.);
690        let shrunk = a.shrink(0.5);
691        assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON);
692        assert!(a.contains(&shrunk));
693        assert!(!shrunk.contains(&a));
694    }
695
696    #[test]
697    fn scale_around_center() {
698        let a = BoundingCircle::new(Vec2::ONE, 5.);
699        let scaled = a.scale_around_center(2.);
700        assert!((scaled.radius() - 10.).abs() < f32::EPSILON);
701        assert!(!a.contains(&scaled));
702        assert!(scaled.contains(&a));
703    }
704
705    #[test]
706    fn transform() {
707        let a = BoundingCircle::new(Vec2::ONE, 5.0);
708        let transformed = a.transformed_by(Vec2::new(2.0, -2.0), core::f32::consts::FRAC_PI_4);
709        assert_eq!(
710            transformed.center,
711            Vec2::new(2.0, core::f32::consts::SQRT_2 - 2.0)
712        );
713        assert_eq!(transformed.radius(), 5.0);
714    }
715
716    #[test]
717    fn closest_point() {
718        let circle = BoundingCircle::new(Vec2::ZERO, 1.0);
719        assert_eq!(circle.closest_point(Vec2::X * 10.0), Vec2::X);
720        assert_eq!(
721            circle.closest_point(Vec2::NEG_ONE * 10.0),
722            Vec2::NEG_ONE.normalize()
723        );
724        assert_eq!(
725            circle.closest_point(Vec2::new(0.25, 0.1)),
726            Vec2::new(0.25, 0.1)
727        );
728    }
729
730    #[test]
731    fn intersect_bounding_circle() {
732        let circle = BoundingCircle::new(Vec2::ZERO, 1.0);
733        assert!(circle.intersects(&BoundingCircle::new(Vec2::ZERO, 1.0)));
734        assert!(circle.intersects(&BoundingCircle::new(Vec2::ONE * 1.25, 1.0)));
735        assert!(circle.intersects(&BoundingCircle::new(Vec2::NEG_ONE * 1.25, 1.0)));
736        assert!(!circle.intersects(&BoundingCircle::new(Vec2::ONE * 1.5, 1.0)));
737    }
738}