Skip to main content

euv_engine/collider/
impl.rs

1use super::*;
2
3/// Implements `Collider` trait and convenience methods for `AabbCollider`.
4impl AabbCollider {
5    /// Creates a new AABB collider from a center point and dimensions.
6    ///
7    /// # Arguments
8    ///
9    /// - `Vector2D` - The center point.
10    /// - `f64` - The width.
11    ///
12    /// # Returns
13    ///
14    /// - `AabbCollider` - The new collider.
15    pub fn from_center(center: Vector2D, width: f64, height: f64) -> AabbCollider {
16        AabbCollider::new(Rect::from_center(center, width, height))
17    }
18
19    /// Tests collision with another AABB collider and returns the collision result.
20    ///
21    /// # Arguments
22    ///
23    /// - `&AabbCollider` - The other collider.
24    ///
25    /// # Returns
26    ///
27    /// - `Option<CollisionResult>` - The collision result, or `None` if no collision.
28    pub fn collide_with_aabb(&self, other: &AabbCollider) -> Option<CollisionResult> {
29        let self_rect: Rect = self.get_rect();
30        let other_rect: Rect = other.get_rect();
31        let a_min: Vector2D = self_rect.min();
32        let a_max: Vector2D = self_rect.max();
33        let b_min: Vector2D = other_rect.min();
34        let b_max: Vector2D = other_rect.max();
35        let overlap_x: f64 =
36            (a_max.get_x().min(b_max.get_x()) - a_min.get_x().max(b_min.get_x())).max(0.0);
37        let overlap_y: f64 =
38            (a_max.get_y().min(b_max.get_y()) - a_min.get_y().max(b_min.get_y())).max(0.0);
39        if overlap_x <= COLLIDER_CONTACT_EPSILON || overlap_y <= COLLIDER_CONTACT_EPSILON {
40            return None;
41        }
42        let (normal, depth) = if overlap_x < overlap_y {
43            let direction: f64 = if self_rect.center().get_x() < other_rect.center().get_x() {
44                -1.0
45            } else {
46                1.0
47            };
48            (Vector2D::new(direction, 0.0), overlap_x)
49        } else {
50            let direction: f64 = if self_rect.center().get_y() < other_rect.center().get_y() {
51                -1.0
52            } else {
53                1.0
54            };
55            (Vector2D::new(0.0, direction), overlap_y)
56        };
57        let contact_point: Vector2D = Vector2D::new(
58            a_min
59                .get_x()
60                .max(b_min.get_x())
61                .min(a_max.get_x().min(b_max.get_x())),
62            a_min
63                .get_y()
64                .max(b_min.get_y())
65                .min(a_max.get_y().min(b_max.get_y())),
66        );
67        Some(CollisionResult::new(normal, depth, contact_point))
68    }
69
70    /// Tests collision with a circle collider and returns the collision result.
71    ///
72    /// # Arguments
73    ///
74    /// - `&CircleCollider` - The circle collider.
75    ///
76    /// # Returns
77    ///
78    /// - `Option<CollisionResult>` - The collision result, or `None` if no collision.
79    pub fn collide_with_circle(&self, circle: &CircleCollider) -> Option<CollisionResult> {
80        let self_rect: Rect = self.get_rect();
81        let circle_inner: Circle = circle.get_circle();
82        let rect_min: Vector2D = self_rect.min();
83        let rect_max: Vector2D = self_rect.max();
84        let closest_x: f64 = circle_inner
85            .get_center()
86            .get_x()
87            .clamp(rect_min.get_x(), rect_max.get_x());
88        let closest_y: f64 = circle_inner
89            .get_center()
90            .get_y()
91            .clamp(rect_min.get_y(), rect_max.get_y());
92        let delta: Vector2D = circle_inner.get_center() - Vector2D::new(closest_x, closest_y);
93        let distance_sq: f64 = delta.magnitude_squared();
94        if distance_sq >= circle_inner.get_radius() * circle_inner.get_radius() {
95            return None;
96        }
97        let distance: f64 = distance_sq.sqrt();
98        let normal: Vector2D = if distance < EPSILON {
99            let aabb_center: Vector2D = self_rect.center();
100            let center_delta: Vector2D = circle_inner.get_center() - aabb_center;
101            if center_delta.magnitude() < EPSILON {
102                Vector2D::up()
103            } else {
104                center_delta.normalized()
105            }
106        } else {
107            delta.scaled(1.0 / distance)
108        };
109        let depth: f64 = circle_inner.get_radius() - distance;
110        let contact_point: Vector2D = Vector2D::new(closest_x, closest_y);
111        Some(CollisionResult::new(normal, depth, contact_point))
112    }
113}
114
115/// Implements the `Collider` trait for `AabbCollider`.
116impl Collider for AabbCollider {
117    /// Returns the concrete shape variant of the collider.
118    ///
119    /// # Returns
120    ///
121    /// - `ColliderShape` - The concrete shape variant for this collider.
122    fn shape(&self) -> ColliderShape {
123        ColliderShape::Aabb
124    }
125
126    /// Returns the axis-aligned bounding rectangle (2D) or box (3D) of the collider.
127    ///
128    /// # Returns
129    ///
130    /// - `Rect` - The axis-aligned bounding box of the collider.
131    fn bounding_box(&self) -> Rect {
132        self.get_rect()
133    }
134
135    /// Returns `true` when the supplied point lies inside the collider.
136    ///
137    /// # Arguments
138    ///
139    /// - `Vector2D` - Point to test.
140    ///
141    /// # Returns
142    ///
143    /// - `bool` - `true` when the point lies inside the collider.
144    fn contains_point(&self, point: Vector2D) -> bool {
145        self.get_rect().contains(point)
146    }
147
148    /// Returns the geometric center of the collider.
149    ///
150    /// # Returns
151    ///
152    /// - `Vector2D` - The geometric centre of the collider.
153    fn center(&self) -> Vector2D {
154        self.get_rect().center()
155    }
156}
157
158/// Implements `Collider` trait and convenience methods for `CircleCollider`.
159impl CircleCollider {
160    /// Creates a new circle collider from a center point and radius.
161    ///
162    /// # Arguments
163    ///
164    /// - `Vector2D` - The center point.
165    /// - `f64` - The radius.
166    ///
167    /// # Returns
168    ///
169    /// - `CircleCollider` - The new collider.
170    pub fn from_center(center: Vector2D, radius: f64) -> CircleCollider {
171        CircleCollider::new(Circle::new(center, radius))
172    }
173
174    /// Tests collision with another circle collider and returns the collision result.
175    ///
176    /// # Arguments
177    ///
178    /// - `&CircleCollider` - The other collider.
179    ///
180    /// # Returns
181    ///
182    /// - `Option<CollisionResult>` - The collision result, or `None` if no collision.
183    pub fn collide_with_circle(&self, other: &CircleCollider) -> Option<CollisionResult> {
184        let self_circle: Circle = self.get_circle();
185        let other_circle: Circle = other.get_circle();
186        let delta: Vector2D = other_circle.get_center() - self_circle.get_center();
187        let distance: f64 = delta.magnitude();
188        let radius_sum: f64 = self_circle.get_radius() + other_circle.get_radius();
189        if distance >= radius_sum {
190            return None;
191        }
192        let normal: Vector2D = if distance < EPSILON {
193            Vector2D::right()
194        } else {
195            delta.scaled(1.0 / distance)
196        };
197        let depth: f64 = radius_sum - distance;
198        let contact_point: Vector2D =
199            self_circle.get_center() + normal.scaled(self_circle.get_radius());
200        Some(CollisionResult::new(normal, depth, contact_point))
201    }
202}
203
204/// Implements the `Collider` trait for `CircleCollider`.
205impl Collider for CircleCollider {
206    /// Returns the concrete shape variant of the collider.
207    ///
208    /// # Returns
209    ///
210    /// - `ColliderShape` - The concrete shape variant for this collider.
211    fn shape(&self) -> ColliderShape {
212        ColliderShape::Circle
213    }
214
215    /// Returns the axis-aligned bounding rectangle (2D) or box (3D) of the collider.
216    ///
217    /// # Returns
218    ///
219    /// - `Rect` - The axis-aligned bounding box of the collider.
220    fn bounding_box(&self) -> Rect {
221        let circle: Circle = self.get_circle();
222        let diameter: f64 = circle.get_radius() * 2.0;
223        Rect::from_center(circle.get_center(), diameter, diameter)
224    }
225
226    /// Returns `true` when the supplied point lies inside the collider.
227    ///
228    /// # Arguments
229    ///
230    /// - `Vector2D` - Point to test.
231    ///
232    /// # Returns
233    ///
234    /// - `bool` - `true` when the point lies inside the collider.
235    fn contains_point(&self, point: Vector2D) -> bool {
236        self.get_circle().contains(point)
237    }
238
239    /// Returns the geometric center of the collider.
240    ///
241    /// # Returns
242    ///
243    /// - `Vector2D` - The geometric centre of the collider.
244    fn center(&self) -> Vector2D {
245        self.get_circle().get_center()
246    }
247}
248
249/// Implements `Collider3D` trait and convenience methods for `AabbCollider3D`.
250impl AabbCollider3D {
251    /// Creates a new 3D AABB collider from a center point and dimensions.
252    ///
253    /// # Arguments
254    ///
255    /// - `Vector3D` - The center point.
256    /// - `f64` - The width.
257    ///
258    /// # Returns
259    ///
260    /// - `AabbCollider3D` - The new collider.
261    pub fn from_center(center: Vector3D, width: f64, height: f64, depth: f64) -> AabbCollider3D {
262        AabbCollider3D::new(AABB3D::from_center(center, width, height, depth))
263    }
264
265    /// Tests collision with another 3D AABB collider and returns the collision result.
266    ///
267    /// # Arguments
268    ///
269    /// - `&AabbCollider3D` - The other collider.
270    ///
271    /// # Returns
272    ///
273    /// - `Option<CollisionResult3D>` - The collision result, or `None` if no collision.
274    pub fn collide_with_aabb(&self, other: &AabbCollider3D) -> Option<CollisionResult3D> {
275        let self_aabb: AABB3D = self.get_aabb();
276        let other_aabb: AABB3D = other.get_aabb();
277        let a_center: Vector3D = self_aabb.center();
278        let b_center: Vector3D = other_aabb.center();
279        let a_size: Vector3D = self_aabb.size();
280        let b_size: Vector3D = other_aabb.size();
281        let overlap_x: f64 =
282            (a_size.get_x() + b_size.get_x()) * 0.5 - (a_center.get_x() - b_center.get_x()).abs();
283        if overlap_x <= COLLIDER_CONTACT_EPSILON {
284            return None;
285        }
286        let overlap_y: f64 =
287            (a_size.get_y() + b_size.get_y()) * 0.5 - (a_center.get_y() - b_center.get_y()).abs();
288        if overlap_y <= COLLIDER_CONTACT_EPSILON {
289            return None;
290        }
291        let overlap_z: f64 =
292            (a_size.get_z() + b_size.get_z()) * 0.5 - (a_center.get_z() - b_center.get_z()).abs();
293        if overlap_z <= COLLIDER_CONTACT_EPSILON {
294            return None;
295        }
296        let (normal, depth) = if overlap_x <= overlap_y && overlap_x <= overlap_z {
297            let direction: f64 = if a_center.get_x() < b_center.get_x() {
298                -1.0
299            } else {
300                1.0
301            };
302            (Vector3D::new(direction, 0.0, 0.0), overlap_x)
303        } else if overlap_y <= overlap_z {
304            let direction: f64 = if a_center.get_y() < b_center.get_y() {
305                -1.0
306            } else {
307                1.0
308            };
309            (Vector3D::new(0.0, direction, 0.0), overlap_y)
310        } else {
311            let direction: f64 = if a_center.get_z() < b_center.get_z() {
312                -1.0
313            } else {
314                1.0
315            };
316            (Vector3D::new(0.0, 0.0, direction), overlap_z)
317        };
318        let self_min: Vector3D = self_aabb.get_min();
319        let self_max: Vector3D = self_aabb.get_max();
320        let other_min: Vector3D = other_aabb.get_min();
321        let other_max: Vector3D = other_aabb.get_max();
322        let contact_point: Vector3D = Vector3D::new(
323            self_min
324                .get_x()
325                .max(other_min.get_x())
326                .min(self_max.get_x().min(other_max.get_x())),
327            self_min
328                .get_y()
329                .max(other_min.get_y())
330                .min(self_max.get_y().min(other_max.get_y())),
331            self_min
332                .get_z()
333                .max(other_min.get_z())
334                .min(self_max.get_z().min(other_max.get_z())),
335        );
336        Some(CollisionResult3D::new(normal, depth, contact_point))
337    }
338
339    /// Tests collision with a sphere collider and returns the collision result.
340    ///
341    /// # Arguments
342    ///
343    /// - `&SphereCollider3D` - The sphere collider.
344    ///
345    /// # Returns
346    ///
347    /// - `Option<CollisionResult3D>` - The collision result, or `None` if no collision.
348    pub fn collide_with_sphere(&self, sphere: &SphereCollider3D) -> Option<CollisionResult3D> {
349        let self_aabb: AABB3D = self.get_aabb();
350        let sphere_inner: Sphere = sphere.get_sphere();
351        let aabb_min: Vector3D = self_aabb.get_min();
352        let aabb_max: Vector3D = self_aabb.get_max();
353        let closest_x: f64 = sphere_inner
354            .get_center()
355            .get_x()
356            .clamp(aabb_min.get_x(), aabb_max.get_x());
357        let closest_y: f64 = sphere_inner
358            .get_center()
359            .get_y()
360            .clamp(aabb_min.get_y(), aabb_max.get_y());
361        let closest_z: f64 = sphere_inner
362            .get_center()
363            .get_z()
364            .clamp(aabb_min.get_z(), aabb_max.get_z());
365        let closest: Vector3D = Vector3D::new(closest_x, closest_y, closest_z);
366        let delta: Vector3D = sphere_inner.get_center() - closest;
367        let distance_sq: f64 = delta.magnitude_squared();
368        if distance_sq >= sphere_inner.get_radius() * sphere_inner.get_radius() {
369            return None;
370        }
371        let distance: f64 = distance_sq.sqrt();
372        let normal: Vector3D = if distance < EPSILON {
373            let aabb_center: Vector3D = self_aabb.center();
374            let center_delta: Vector3D = sphere_inner.get_center() - aabb_center;
375            if center_delta.magnitude() < EPSILON {
376                Vector3D::up()
377            } else {
378                center_delta.normalized()
379            }
380        } else {
381            delta.scaled(1.0 / distance)
382        };
383        let depth: f64 = sphere_inner.get_radius() - distance;
384        let contact_point: Vector3D = closest;
385        Some(CollisionResult3D::new(normal, depth, contact_point))
386    }
387}
388
389/// Implements the `Collider3D` trait for `AabbCollider3D`.
390impl Collider3D for AabbCollider3D {
391    /// Returns the concrete shape variant of the collider.
392    ///
393    /// # Returns
394    ///
395    /// - `ColliderShape3D` - The concrete shape variant for this collider.
396    fn shape(&self) -> ColliderShape3D {
397        ColliderShape3D::Aabb
398    }
399
400    /// Returns the axis-aligned bounding rectangle (2D) or box (3D) of the collider.
401    ///
402    /// # Returns
403    ///
404    /// - `AABB3D` - The axis-aligned bounding box of the collider.
405    fn bounding_box(&self) -> AABB3D {
406        self.get_aabb()
407    }
408
409    /// Returns `true` when the supplied point lies inside the collider.
410    ///
411    /// # Arguments
412    ///
413    /// - `Vector3D` - Point to test.
414    ///
415    /// # Returns
416    ///
417    /// - `bool` - `true` when the point lies inside the collider.
418    fn contains_point(&self, point: Vector3D) -> bool {
419        self.get_aabb().contains(point)
420    }
421
422    /// Returns the geometric center of the collider.
423    ///
424    /// # Returns
425    ///
426    /// - `Vector3D` - The geometric centre of the collider.
427    fn center(&self) -> Vector3D {
428        self.get_aabb().center()
429    }
430}
431
432/// Implements `Collider3D` trait and convenience methods for `SphereCollider3D`.
433impl SphereCollider3D {
434    /// Creates a new 3D sphere collider from a center point and radius.
435    ///
436    /// # Arguments
437    ///
438    /// - `Vector3D` - The center point.
439    /// - `f64` - The radius.
440    ///
441    /// # Returns
442    ///
443    /// - `SphereCollider3D` - The new collider.
444    pub fn from_center(center: Vector3D, radius: f64) -> SphereCollider3D {
445        SphereCollider3D::new(Sphere::new(center, radius))
446    }
447
448    /// Tests collision with another sphere collider and returns the collision result.
449    ///
450    /// # Arguments
451    ///
452    /// - `&SphereCollider3D` - The other collider.
453    ///
454    /// # Returns
455    ///
456    /// - `Option<CollisionResult3D>` - The collision result, or `None` if no collision.
457    pub fn collide_with_sphere(&self, other: &SphereCollider3D) -> Option<CollisionResult3D> {
458        let self_sphere: Sphere = self.get_sphere();
459        let other_sphere: Sphere = other.get_sphere();
460        let delta: Vector3D = other_sphere.get_center() - self_sphere.get_center();
461        let distance: f64 = delta.magnitude();
462        let radius_sum: f64 = self_sphere.get_radius() + other_sphere.get_radius();
463        if distance >= radius_sum {
464            return None;
465        }
466        let normal: Vector3D = if distance < EPSILON {
467            Vector3D::right()
468        } else {
469            delta.scaled(1.0 / distance)
470        };
471        let depth: f64 = radius_sum - distance;
472        let contact_point: Vector3D =
473            self_sphere.get_center() + normal.scaled(self_sphere.get_radius());
474        Some(CollisionResult3D::new(normal, depth, contact_point))
475    }
476}
477
478/// Implements the `Collider3D` trait for `SphereCollider3D`.
479impl Collider3D for SphereCollider3D {
480    /// Returns the concrete shape variant of the collider.
481    ///
482    /// # Returns
483    ///
484    /// - `ColliderShape3D` - The concrete shape variant for this collider.
485    fn shape(&self) -> ColliderShape3D {
486        ColliderShape3D::Sphere
487    }
488
489    /// Returns the axis-aligned bounding rectangle (2D) or box (3D) of the collider.
490    ///
491    /// # Returns
492    ///
493    /// - `AABB3D` - The axis-aligned bounding box of the collider.
494    fn bounding_box(&self) -> AABB3D {
495        let sphere: Sphere = self.get_sphere();
496        let diameter: f64 = sphere.get_radius() * 2.0;
497        AABB3D::from_center(sphere.get_center(), diameter, diameter, diameter)
498    }
499
500    /// Returns `true` when the supplied point lies inside the collider.
501    ///
502    /// # Arguments
503    ///
504    /// - `Vector3D` - Point to test.
505    ///
506    /// # Returns
507    ///
508    /// - `bool` - `true` when the point lies inside the collider.
509    fn contains_point(&self, point: Vector3D) -> bool {
510        self.get_sphere().contains(point)
511    }
512
513    /// Returns the geometric center of the collider.
514    ///
515    /// # Returns
516    ///
517    /// - `Vector3D` - The geometric centre of the collider.
518    fn center(&self) -> Vector3D {
519        self.get_sphere().get_center()
520    }
521}
522
523/// Implements broad-phase collision checking for `AABB3D`.
524impl AABB3D {
525    /// Performs a broad-phase check using 3D bounding boxes to quickly reject non-colliding pairs.
526    ///
527    /// # Arguments
528    ///
529    /// - `AABB3D` - The first bounding box.
530    ///
531    /// # Returns
532    ///
533    /// - `bool` - True if the bounding boxes overlap.
534    pub fn broad_phase(a: AABB3D, b: AABB3D) -> bool {
535        a.intersects(b)
536    }
537}