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