bevy_math/bounding/bounded2d/
mod.rs

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