bevy_quadtree/shape/
rotated_rect.rs

1use crate::{
2    CollisionCircle, CollisionRect,
3    collision::{Collision, CollisionQuery, DynCollision, Relation, UpdateCollision},
4};
5use bevy_ecs::prelude::*;
6#[cfg(feature = "sprite")]
7use bevy_log::warn;
8use bevy_math::prelude::*;
9#[cfg(feature = "sprite")]
10use bevy_sprite::Sprite;
11use bevy_transform::components::GlobalTransform;
12use core::fmt;
13use std::any::type_name;
14
15/// Rotated Rectagle shape to be used in the QuadTreePlugin
16/// and as a Component in the ECS.
17///
18/// Also, implemented [`CollisionQuery`] trait to be used as boundary in the [`QuadTree::query`](crate::QuadTree::query).
19#[derive(Component, Clone)]
20pub struct CollisionRotatedRect<const ID: usize = 0> {
21    pub(crate) init_size: Vec2,
22    pub(crate) scale: Vec2,
23    pub(crate) isometric: Isometry2d,
24}
25
26impl<const ID: usize> fmt::Debug for CollisionRotatedRect<ID> {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(
29            f,
30            "{}: center = ({}, {}); size = ({} x {}) x ({} x {}) = {} x {}; totation = {}",
31            type_name::<Self>(),
32            self.isometric.translation.x,
33            self.isometric.translation.y,
34            self.init_size.x,
35            self.scale.x,
36            self.init_size.y,
37            self.scale.y,
38            self.init_size.x * self.scale.x,
39            self.init_size.y * self.scale.y,
40            self.isometric.rotation.as_degrees()
41        )
42    }
43}
44
45impl<const ID: usize> From<&CollisionRotatedRect<ID>> for CollisionRotatedRect<0> {
46    /// Convert the shape with `ID` to the shape with `ID = 0`.
47    /// Used to eliminate the `ID` in the collision detection.
48    fn from(value: &CollisionRotatedRect<ID>) -> Self {
49        Self {
50            init_size: value.init_size,
51            scale: value.scale,
52            isometric: value.isometric,
53        }
54    }
55}
56
57impl From<Rect> for CollisionRotatedRect {
58    /// Create a new CollisionRotatedRect from a Rect with the default scale and rotation.
59    ///
60    /// See [`Self::new`] for the version with rotation.
61    fn from(rect: Rect) -> Self {
62        Self {
63            init_size: rect.size(),
64            scale: Vec2::ONE,
65            isometric: Isometry2d::new(rect.center(), Rot2::IDENTITY),
66        }
67    }
68}
69
70impl CollisionRotatedRect {
71    /// Create a new CollisionRotatedRect with `ID = 0`, and given Rect, rotation. See [`Self::new_id`] for the version with `ID`.
72    ///
73    /// The rotation is relative to the center of the rect.
74    /// When updating, the rotation from the GlobalTransform will directly cover the isometric,
75    /// instead of based on the initial rotation.
76    pub fn new(rect: Rect, rotation: Rot2) -> Self {
77        Self {
78            init_size: rect.size(),
79            scale: Vec2::ONE,
80            isometric: Isometry2d::new(rect.center(), rotation),
81        }
82    }
83}
84
85impl<const ID: usize> CollisionRotatedRect<ID> {
86    /// Create a new CollisionRotatedRect with the given ID, Rect and rotation.
87    pub fn new_id(rect: Rect, rotation: Rot2) -> Self {
88        Self {
89            init_size: rect.size(),
90            scale: Vec2::ONE,
91            isometric: Isometry2d::new(rect.center(), rotation),
92        }
93    }
94
95    /// Set the initial size of the rect, which is used to compute the size with the GlobalTransform's scale.
96    pub fn set_init_size(&mut self, size: Vec2) {
97        self.init_size = size;
98    }
99}
100
101impl Collision<CollisionRect> for CollisionRotatedRect {
102    fn detect(&self, rect: &CollisionRect) -> Relation {
103        match Collision::detect(rect, self) {
104            Relation::Contain => Relation::Contained,
105            Relation::Contained => Relation::Contain,
106            r => r,
107        }
108    }
109}
110
111impl Collision<CollisionRotatedRect> for CollisionRotatedRect {
112    fn detect(&self, r_rect: &CollisionRotatedRect) -> Relation {
113        let self_half_size = self.init_size * self.scale / 2.;
114        let r_half_size = r_rect.init_size * r_rect.scale / 2.;
115        let mut vetex = [
116            Vec2::new(r_half_size.x, r_half_size.y),
117            Vec2::new(-r_half_size.x, r_half_size.y),
118            Vec2::new(-r_half_size.x, -r_half_size.y),
119            Vec2::new(r_half_size.x, -r_half_size.y),
120        ];
121        let (mut min_x, mut max_x, mut min_y, mut max_y) = (
122            f32::INFINITY,
123            f32::NEG_INFINITY,
124            f32::INFINITY,
125            f32::NEG_INFINITY,
126        );
127        let inv = self.isometric.inverse();
128        for v in vetex.iter_mut() {
129            *v = inv * r_rect.isometric * *v;
130            min_x = min_x.min(v.x);
131            max_x = max_x.max(v.x);
132            min_y = min_y.min(v.y);
133            max_y = max_y.max(v.y);
134        }
135        if self_half_size.x < min_x
136            || -self_half_size.x > max_x
137            || self_half_size.y < min_y
138            || -self_half_size.y > max_y
139        {
140            return Relation::Disjoint;
141        } else if -self_half_size.x < min_x
142            && max_x < self_half_size.x
143            && -self_half_size.y < min_y
144            && max_y < self_half_size.y
145        {
146            return Relation::Contain;
147        }
148        let mut vetex = [
149            Vec2::new(self_half_size.x, self_half_size.y),
150            Vec2::new(-self_half_size.x, self_half_size.y),
151            Vec2::new(-self_half_size.x, -self_half_size.y),
152            Vec2::new(self_half_size.x, -self_half_size.y),
153        ];
154        let (mut min_x, mut max_x, mut min_y, mut max_y) = (
155            f32::INFINITY,
156            f32::NEG_INFINITY,
157            f32::INFINITY,
158            f32::NEG_INFINITY,
159        );
160        let inv = r_rect.isometric.inverse();
161        for v in vetex.iter_mut() {
162            *v = inv * self.isometric * *v;
163            min_x = min_x.min(v.x);
164            max_x = max_x.max(v.x);
165            min_y = min_y.min(v.y);
166            max_y = max_y.max(v.y);
167        }
168        if r_half_size.x < min_x
169            || -r_half_size.x > max_x
170            || r_half_size.y < min_y
171            || -r_half_size.y > max_y
172        {
173            return Relation::Disjoint;
174        } else if -r_half_size.x < min_x
175            && max_x < r_half_size.x
176            && -r_half_size.y < min_y
177            && max_y < r_half_size.y
178        {
179            return Relation::Contained;
180        }
181        Relation::Overlap
182    }
183}
184
185impl Collision<CollisionCircle> for CollisionRotatedRect {
186    fn detect(&self, circle: &CollisionCircle) -> Relation {
187        match Collision::detect(circle, self) {
188            Relation::Contain => Relation::Contained,
189            Relation::Contained => Relation::Contain,
190            r => r,
191        }
192    }
193}
194
195impl<const ID: usize> UpdateCollision<GlobalTransform> for CollisionRotatedRect<ID> {
196    fn update() -> impl FnOnce(Mut<Self>, &GlobalTransform) {
197        |mut rect, global_transform| {
198            debug_assert_eq!(
199                global_transform.rotation().to_axis_angle().0.z,
200                1.,
201                "Rotation for CollisionRotatedRect should around z-axis only"
202            );
203            rect.scale = global_transform.scale().truncate();
204            rect.isometric = Isometry2d::new(
205                global_transform.translation().truncate(),
206                Rot2::radians(global_transform.rotation().to_axis_angle().1),
207            );
208        }
209    }
210}
211
212#[cfg(feature = "sprite")]
213impl<const ID: usize> UpdateCollision<Sprite> for CollisionRotatedRect<ID> {
214    fn update() -> impl FnOnce(Mut<Self>, &Sprite) {
215        |mut rect, sprite| {
216            if let Some(size) = sprite.custom_size {
217                if size != rect.init_size {
218                    rect.init_size = size;
219                }
220            } else {
221                warn!("Tracking sprite with no custom size");
222            }
223        }
224    }
225}
226
227impl CollisionQuery for CollisionRotatedRect {
228    fn query(&self, obj: &dyn DynCollision) -> Relation {
229        match obj.detect(self) {
230            Relation::Contain => Relation::Contained,
231            Relation::Contained => Relation::Contain,
232            r => r,
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use std::f32::consts::*;
241
242    #[test]
243    fn collision_rotated_rect_detect() {
244        let rect = CollisionRotatedRect::new(
245            Rect::from_center_size(Vec2::ZERO, Vec2::ONE),
246            Rot2::radians(FRAC_PI_4),
247        );
248        let contain =
249            CollisionRotatedRect::from(Rect::from_center_size(Vec2::ZERO, Vec2::ONE / 2.));
250        let contained = CollisionRotatedRect::new(
251            Rect::from_center_size(Vec2::ZERO, Vec2::ONE * 1.4),
252            Rot2::degrees(15.),
253        );
254        let disjoint = CollisionRotatedRect::from(Rect::from_center_size(Vec2::ONE, Vec2::ONE));
255        let overlap =
256            CollisionRotatedRect::from(Rect::from_center_size(Vec2::new(0.5, 0.5), Vec2::ONE));
257        assert_eq!(rect.detect(&contain), Relation::Contain);
258        assert_eq!(rect.detect(&contained), Relation::Contained);
259        assert_eq!(rect.detect(&disjoint), Relation::Disjoint);
260        assert_eq!(rect.detect(&overlap), Relation::Overlap);
261    }
262
263    #[test]
264    fn collision_rotated_rect_detect_rect() {
265        let rect = CollisionRect::from(Rect::from_center_size(Vec2::ZERO, Vec2::ONE));
266        let contain = CollisionCircle::new(Vec2::ZERO, 0.4);
267        let contained = CollisionCircle::new(Vec2::ZERO, 2.);
268        let disjoint = CollisionCircle::new(Vec2::new(2., 2.), 1.);
269        let overlap = CollisionCircle::new(Vec2::new(0.5, 0.5), 1.);
270        assert_eq!(rect.detect(&contain), Relation::Contain);
271        assert_eq!(rect.detect(&contained), Relation::Contained);
272        assert_eq!(rect.detect(&disjoint), Relation::Disjoint);
273        assert_eq!(rect.detect(&overlap), Relation::Overlap);
274    }
275
276    #[test]
277    fn collision_rotated_rect_detect_circle() {
278        let rect = CollisionRect::from(Rect::from_center_size(Vec2::ZERO, Vec2::ONE));
279        let contain = CollisionRotatedRect::new(
280            Rect::from_center_size(Vec2::ZERO, Vec2::ONE / 2.),
281            Rot2::radians(FRAC_PI_4),
282        );
283        let contained = CollisionRotatedRect::new(
284            Rect::from_center_size(Vec2::ZERO, Vec2::ONE * 3.),
285            Rot2::radians(FRAC_PI_4),
286        );
287        let disjoint = CollisionRotatedRect::new(
288            Rect::from_center_size(Vec2::new(2., 2.), Vec2::ONE),
289            Rot2::radians(FRAC_PI_4),
290        );
291        let overlap = CollisionRotatedRect::new(
292            Rect::from_center_size(Vec2::new(0.5, 0.5), Vec2::ONE),
293            Rot2::radians(FRAC_PI_4),
294        );
295        assert_eq!(rect.detect(&contain), Relation::Contain);
296        assert_eq!(rect.detect(&contained), Relation::Contained);
297        assert_eq!(rect.detect(&disjoint), Relation::Disjoint);
298        assert_eq!(rect.detect(&overlap), Relation::Overlap);
299    }
300}