bevy_quadtree/shape/
circle.rs

1use bevy_ecs::prelude::*;
2use bevy_math::prelude::*;
3use bevy_transform::components::GlobalTransform;
4use core::fmt;
5use std::any::type_name;
6
7use crate::{
8    CollisionRect, CollisionRotatedRect,
9    collision::{Collision, CollisionQuery, DynCollision, Relation, UpdateCollision},
10};
11
12/// Circle shape to be used in the QuadTreePlugin
13/// and as a Component in the ECS.
14///
15/// Also, implemented [`CollisionQuery`] trait to be used as boundary in the [`QuadTree::query`](crate::QuadTree::query).
16///
17/// # Panic
18/// Do not perform scaling with different x and y values, it will cause the circle to be an ellipse,
19/// and the collision detection will be incorrect.
20#[derive(Component, Clone)]
21pub struct CollisionCircle<const ID: usize = 0> {
22    pub(crate) center: Vec2,
23    scale: f32,
24    init_radius: f32,
25}
26
27impl<const ID: usize> fmt::Debug for CollisionCircle<ID> {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        write!(
30            f,
31            "{}: center = ({}, {}); r = {} x {} = {}",
32            type_name::<Self>(),
33            self.center.x,
34            self.center.y,
35            self.init_radius,
36            self.scale,
37            self.init_radius * self.scale
38        )
39    }
40}
41
42impl<const ID: usize> From<&CollisionCircle<ID>> for CollisionCircle<0> {
43    /// Convert the shape with `ID` to the shape with `ID = 0`.
44    /// Used to eliminate the `ID` in the collision detection.
45    fn from(value: &CollisionCircle<ID>) -> Self {
46        Self {
47            center: value.center,
48            scale: value.scale,
49            init_radius: value.init_radius,
50        }
51    }
52}
53
54impl CollisionCircle {
55    /// Create a new circle with `ID = 0`. See [`Self::new_id`] for the version with `ID`.
56    ///
57    /// The initial radius is used to compute the size with the GlobalTransform's scale.
58    ///
59    /// The initial center is covered by the GlobalTransform's translation during the update.
60    pub fn new(center: Vec2, radius: f32) -> Self {
61        Self {
62            center,
63            scale: 1.,
64            init_radius: radius,
65        }
66    }
67}
68
69impl<const ID: usize> CollisionCircle<ID> {
70    /// Create a new circle with the given `ID`.
71    pub fn new_id(center: Vec2, radius: f32) -> Self {
72        Self {
73            center,
74            scale: 1.,
75            init_radius: radius,
76        }
77    }
78
79    fn radius(&self) -> f32 {
80        self.init_radius * self.scale
81    }
82
83    /// Set the initial radius of the circle, which is used to compute the radius with the GlobalTransform's scale.
84    pub fn set_init_radius(&mut self, radius: f32) {
85        self.init_radius = radius;
86    }
87}
88
89impl Collision<CollisionRect> for CollisionCircle {
90    fn detect(&self, rect: &CollisionRect) -> Relation {
91        let rect_max = rect.max();
92        let rect_min = rect.min();
93        let i = rect_max - rect.center; // move rect center to origin and get vertex in Quadrant I
94        let center = (self.center - rect.center).abs(); // move circle with rect and symmetrize the circle to Quadrant I
95        let ds = [
96            (self.center - rect_max).length(),
97            (self.center - Vec2::new(rect_min.x, rect_max.y)).length(),
98            (self.center - rect_min).length(),
99            (self.center - Vec2::new(rect_max.x, rect_min.y)).length(),
100        ];
101        let radius = self.radius();
102        if ds.iter().all(|&d| d < radius) {
103            Relation::Contain
104        } else if center.x > i.x + radius || center.y > i.y + radius {
105            Relation::Disjoint
106        } else if center.x < i.x - radius && center.y < i.y - radius {
107            Relation::Contained
108        } else if center.x > i.x && center.y > i.y && (i - center).length() > radius {
109            Relation::Disjoint
110        } else {
111            Relation::Overlap
112        }
113    }
114}
115
116impl Collision<CollisionRotatedRect> for CollisionCircle {
117    fn detect(&self, r_rect: &CollisionRotatedRect) -> Relation {
118        let r_rect_size = r_rect.init_size * r_rect.scale;
119        let i = r_rect_size / 2.; // Move rect center to origin, rotate back and get vertex in Quadrant I
120        let center = (r_rect.isometric.inverse() * self.center).abs(); // Move circle with rect, rotate around origin and symmetrize the circle to Quadrant I
121        let ds = [
122            (center - i).length(),
123            (center - Vec2::new(-i.x, i.y)).length(),
124            (center - Vec2::new(-i.x, -i.y)).length(),
125            (center - Vec2::new(i.x, -i.y)).length(),
126        ];
127        let radius = self.radius();
128        if ds.iter().all(|&d| d < radius) {
129            Relation::Contain
130        } else if center.x > i.x + radius || center.y > i.y + radius {
131            Relation::Disjoint
132        } else if center.x < i.x - radius && center.y < i.y - radius {
133            Relation::Contained
134        } else if center.x > i.x && center.y > i.y && ds[0] > radius {
135            Relation::Disjoint
136        } else {
137            Relation::Overlap
138        }
139    }
140}
141
142impl Collision<CollisionCircle> for CollisionCircle {
143    fn detect(&self, circle: &CollisionCircle) -> Relation {
144        let d = (self.center - circle.center).length();
145        let self_r = self.radius();
146        let circle_r = circle.radius();
147        if d + circle_r < self_r {
148            Relation::Contain
149        } else if d + self_r < circle_r {
150            Relation::Contained
151        } else if d > self_r + circle_r {
152            Relation::Disjoint
153        } else {
154            Relation::Overlap
155        }
156    }
157}
158
159impl<const ID: usize> UpdateCollision<GlobalTransform> for CollisionCircle<ID> {
160    fn update() -> impl FnOnce(Mut<Self>, &GlobalTransform) {
161        |mut circle, global_transform| {
162            circle.center = global_transform.translation().truncate();
163            debug_assert_eq!(
164                global_transform.scale().x, global_transform.scale().y,
165                "Do not perform scaling with different x and y values,
166                it will cause the circle to be an ellipse, and the collision detection will be incorrect."
167            );
168            circle.scale = global_transform.scale().x;
169        }
170    }
171}
172
173impl CollisionQuery for CollisionCircle {
174    fn query(&self, obj: &dyn DynCollision) -> Relation {
175        match obj.detect(self) {
176            Relation::Contain => Relation::Contained,
177            Relation::Contained => Relation::Contain,
178            r => r,
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use bevy_math::{Rect, Rot2};
186
187    use super::*;
188    use std::f32::consts::*;
189
190    #[test]
191    fn collision_circle_detect() {
192        let circle = CollisionCircle::new(Vec2::ZERO, 1.);
193        let contain = CollisionCircle::new(Vec2::ZERO, 0.5);
194        let contained = CollisionCircle::new(Vec2::ZERO, 2.);
195        let disjoint = CollisionCircle::new(Vec2::new(2., 2.), 1.);
196        let overlap = CollisionCircle::new(Vec2::new(0.5, 0.5), 1.);
197        assert_eq!(circle.detect(&contain), Relation::Contain);
198        assert_eq!(circle.detect(&contained), Relation::Contained);
199        assert_eq!(circle.detect(&disjoint), Relation::Disjoint);
200        assert_eq!(circle.detect(&overlap), Relation::Overlap);
201    }
202
203    #[test]
204    fn collision_circle_detect_rect() {
205        let circle = CollisionCircle::new(Vec2::ZERO, 1.);
206        let contain = CollisionRect::from(Rect::from_center_size(Vec2::ZERO, Vec2::ONE / 2.));
207        let contained = CollisionRect::from(Rect::from_center_size(Vec2::ZERO, Vec2::ONE * 3.));
208        let disjoint = CollisionRect::from(Rect::from_center_size(Vec2::new(2., 2.), Vec2::ONE));
209        let overlap = CollisionRect::from(Rect::from_center_size(Vec2::new(0.5, 0.5), Vec2::ONE));
210        assert_eq!(circle.detect(&contain), Relation::Contain);
211        assert_eq!(circle.detect(&contained), Relation::Contained);
212        assert_eq!(circle.detect(&disjoint), Relation::Disjoint);
213        assert_eq!(circle.detect(&overlap), Relation::Overlap);
214    }
215
216    #[test]
217    fn collision_circal_detect_rotated_rect() {
218        let circle = CollisionCircle::new(Vec2::ZERO, 1.);
219        let contain = CollisionRotatedRect::new(
220            Rect::from_center_size(Vec2::ZERO, Vec2::ONE / 2.),
221            Rot2::radians(FRAC_PI_4),
222        );
223        let contained = CollisionRotatedRect::new(
224            Rect::from_center_size(Vec2::ZERO, Vec2::ONE * 3.),
225            Rot2::radians(FRAC_PI_4),
226        );
227        let disjoint = CollisionRotatedRect::new(
228            Rect::from_center_size(Vec2::new(2., 2.), Vec2::ONE),
229            Rot2::radians(FRAC_PI_4),
230        );
231        let overlap = CollisionRotatedRect::new(
232            Rect::from_center_size(Vec2::new(0.5, 0.5), Vec2::ONE),
233            Rot2::radians(FRAC_PI_4),
234        );
235        assert_eq!(circle.detect(&contain), Relation::Contain);
236        assert_eq!(circle.detect(&contained), Relation::Contained);
237        assert_eq!(circle.detect(&disjoint), Relation::Disjoint);
238        assert_eq!(circle.detect(&overlap), Relation::Overlap);
239        let circle = CollisionCircle::new(Vec2::ZERO, 1.);
240        let contain = CollisionRotatedRect::new(
241            Rect::from_center_size(Vec2::new(0.9, 0.), Vec2::new(0.2, 0.001)),
242            Rot2::radians(FRAC_PI_2),
243        );
244        assert_eq!(circle.detect(&contain), Relation::Contain);
245    }
246}