Skip to main content

goud_engine/ecs/components/
collider.rs

1//! Collider component for physics collision detection.
2//!
3//! The [`Collider`] component defines the collision shape for an entity in the physics
4//! simulation. It works in conjunction with [`RigidBody`](crate::ecs::components::RigidBody) to enable collision detection
5//! and response.
6//!
7//! # Collision Shapes
8//!
9//! GoudEngine supports the following 2D collision shapes:
10//!
11//! - **Circle**: Defined by a radius, fastest collision detection
12//! - **Box**: Axis-aligned or rotated rectangle (AABB or OBB)
13//! - **Capsule**: Rounded rectangle, good for characters
14//! - **Polygon**: Convex polygons for complex shapes
15//!
16//! # Usage
17//!
18//! ```
19//! use goud_engine::ecs::components::{Collider, ColliderShape};
20//! use goud_engine::core::math::Vec2;
21//!
22//! // Create a circle collider (player, ball)
23//! let ball = Collider::circle(0.5)
24//!     .with_restitution(0.8) // Bouncy
25//!     .with_friction(0.2);
26//!
27//! // Create a box collider (walls, platforms)
28//! let wall = Collider::aabb(Vec2::new(10.0, 1.0))
29//!     .with_friction(0.5);
30//!
31//! // Create a capsule collider (character controller)
32//! let player = Collider::capsule(0.3, 1.0)
33//!     .with_friction(0.0) // Frictionless movement
34//!     .with_is_sensor(true); // Trigger only
35//! ```
36//!
37//! # Collision Layers
38//!
39//! Colliders support layer-based filtering to control which objects can collide:
40//!
41//! ```
42//! use goud_engine::ecs::components::Collider;
43//!
44//! // Player collides with enemies and walls
45//! let player = Collider::circle(0.5)
46//!     .with_layer(0b0001)
47//!     .with_mask(0b0110);
48//!
49//! // Enemy collides with player and walls
50//! let enemy = Collider::circle(0.4)
51//!     .with_layer(0b0010)
52//!     .with_mask(0b0101);
53//! ```
54//!
55//! # Sensors (Triggers)
56//!
57//! Set `is_sensor` to true to create a trigger volume that detects collisions but
58//! doesn't produce collision response:
59//!
60//! ```
61//! use goud_engine::ecs::components::Collider;
62//! use goud_engine::core::math::Vec2;
63//!
64//! // Pickup item trigger
65//! let pickup = Collider::aabb(Vec2::new(1.0, 1.0))
66//!     .with_is_sensor(true);
67//! ```
68//!
69//! # Integration with RigidBody
70//!
71//! Colliders work with RigidBody components:
72//!
73//! - **Dynamic bodies**: Full collision detection and response
74//! - **Kinematic bodies**: Collision detection only (no response)
75//! - **Static bodies**: Acts as immovable obstacles
76//!
77//! # Thread Safety
78//!
79//! Collider is `Send + Sync` and can be safely used in parallel systems.
80
81use crate::core::math::{Rect, Vec2};
82use crate::ecs::Component;
83
84// =============================================================================
85// ColliderShape Enum
86// =============================================================================
87
88/// The geometric shape of a collider.
89///
90/// Each shape type has different performance characteristics and use cases:
91///
92/// - **Circle**: Fastest collision detection, best for balls, projectiles
93/// - **Box**: Axis-aligned (AABB) or oriented (OBB), good for walls and platforms
94/// - **Capsule**: Good for characters, combines efficiency with smooth edges
95/// - **Polygon**: Most flexible but slowest, use sparingly for complex shapes
96#[derive(Debug, Clone, PartialEq)]
97pub enum ColliderShape {
98    /// Circle collider defined by radius.
99    ///
100    /// Center is at the entity's position. Fastest collision detection.
101    Circle {
102        /// Radius of the circle in world units
103        radius: f32,
104    },
105
106    /// Axis-Aligned Bounding Box (AABB).
107    ///
108    /// Defined by half-extents (half-width, half-height) from the center.
109    /// Fast collision detection, no rotation support.
110    Aabb {
111        /// Half-extents (half-width, half-height)
112        half_extents: Vec2,
113    },
114
115    /// Oriented Bounding Box (OBB).
116    ///
117    /// Similar to AABB but can be rotated. Slightly slower than AABB.
118    Obb {
119        /// Half-extents (half-width, half-height)
120        half_extents: Vec2,
121    },
122
123    /// Capsule collider (rounded rectangle).
124    ///
125    /// Defined by half-height and radius. Good for character controllers.
126    /// The capsule extends vertically from the center.
127    Capsule {
128        /// Half-height of the capsule's cylindrical section
129        half_height: f32,
130        /// Radius of the capsule's rounded ends
131        radius: f32,
132    },
133
134    /// Convex polygon collider.
135    ///
136    /// Vertices must be in counter-clockwise order and form a convex hull.
137    /// Slowest collision detection, use sparingly.
138    Polygon {
139        /// Vertices in local space, counter-clockwise order
140        vertices: Vec<Vec2>,
141    },
142}
143
144impl ColliderShape {
145    /// Returns the type name of this shape.
146    pub fn type_name(&self) -> &'static str {
147        match self {
148            ColliderShape::Circle { .. } => "Circle",
149            ColliderShape::Aabb { .. } => "AABB",
150            ColliderShape::Obb { .. } => "OBB",
151            ColliderShape::Capsule { .. } => "Capsule",
152            ColliderShape::Polygon { .. } => "Polygon",
153        }
154    }
155
156    /// Computes the axis-aligned bounding box (AABB) for this shape.
157    ///
158    /// Returns a rectangle in local space that fully contains the shape.
159    /// For rotated shapes (OBB, Polygon), this is a conservative bound.
160    pub fn compute_aabb(&self) -> Rect {
161        match self {
162            ColliderShape::Circle { radius } => {
163                Rect::new(-radius, -radius, radius * 2.0, radius * 2.0)
164            }
165            ColliderShape::Aabb { half_extents } | ColliderShape::Obb { half_extents } => {
166                Rect::new(
167                    -half_extents.x,
168                    -half_extents.y,
169                    half_extents.x * 2.0,
170                    half_extents.y * 2.0,
171                )
172            }
173            ColliderShape::Capsule {
174                half_height,
175                radius,
176            } => {
177                let width = radius * 2.0;
178                let height = (half_height + radius) * 2.0;
179                Rect::new(-radius, -(half_height + radius), width, height)
180            }
181            ColliderShape::Polygon { vertices } => {
182                if vertices.is_empty() {
183                    return Rect::unit();
184                }
185
186                let mut min_x = vertices[0].x;
187                let mut min_y = vertices[0].y;
188                let mut max_x = vertices[0].x;
189                let mut max_y = vertices[0].y;
190
191                for v in vertices.iter().skip(1) {
192                    min_x = min_x.min(v.x);
193                    min_y = min_y.min(v.y);
194                    max_x = max_x.max(v.x);
195                    max_y = max_y.max(v.y);
196                }
197
198                Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
199            }
200        }
201    }
202
203    /// Returns true if this shape is a circle.
204    pub fn is_circle(&self) -> bool {
205        matches!(self, ColliderShape::Circle { .. })
206    }
207
208    /// Returns true if this shape is an axis-aligned box (AABB).
209    pub fn is_aabb(&self) -> bool {
210        matches!(self, ColliderShape::Aabb { .. })
211    }
212
213    /// Returns true if this shape is an oriented box (OBB).
214    pub fn is_obb(&self) -> bool {
215        matches!(self, ColliderShape::Obb { .. })
216    }
217
218    /// Returns true if this shape is a capsule.
219    pub fn is_capsule(&self) -> bool {
220        matches!(self, ColliderShape::Capsule { .. })
221    }
222
223    /// Returns true if this shape is a polygon.
224    pub fn is_polygon(&self) -> bool {
225        matches!(self, ColliderShape::Polygon { .. })
226    }
227
228    /// Validates that the shape is well-formed.
229    ///
230    /// Returns `true` if:
231    /// - Radii and extents are positive
232    /// - Polygons have at least 3 vertices
233    /// - Polygon vertices form a convex hull (not checked, assumed by user)
234    pub fn is_valid(&self) -> bool {
235        match self {
236            ColliderShape::Circle { radius } => *radius > 0.0,
237            ColliderShape::Aabb { half_extents } | ColliderShape::Obb { half_extents } => {
238                half_extents.x > 0.0 && half_extents.y > 0.0
239            }
240            ColliderShape::Capsule {
241                half_height,
242                radius,
243            } => *half_height > 0.0 && *radius > 0.0,
244            ColliderShape::Polygon { vertices } => vertices.len() >= 3,
245        }
246    }
247}
248
249impl Default for ColliderShape {
250    /// Returns a unit circle (radius 1.0) as the default shape.
251    fn default() -> Self {
252        ColliderShape::Circle { radius: 1.0 }
253    }
254}
255
256// =============================================================================
257// Collider Component
258// =============================================================================
259
260/// Collider component for physics collision detection.
261///
262/// Defines the collision shape, material properties (friction, restitution),
263/// and filtering (layers, masks) for an entity.
264///
265/// # Examples
266///
267/// ```
268/// use goud_engine::ecs::components::Collider;
269/// use goud_engine::core::math::Vec2;
270///
271/// // Circle collider
272/// let ball = Collider::circle(0.5);
273///
274/// // Box collider
275/// let wall = Collider::aabb(Vec2::new(5.0, 1.0));
276///
277/// // Capsule collider
278/// let player = Collider::capsule(0.3, 1.0);
279///
280/// // Sensor (trigger)
281/// let trigger = Collider::circle(2.0).with_is_sensor(true);
282/// ```
283#[derive(Debug, Clone, PartialEq)]
284pub struct Collider {
285    /// The geometric shape of the collider
286    shape: ColliderShape,
287
288    /// Coefficient of restitution (bounciness)
289    ///
290    /// 0.0 = no bounce, 1.0 = perfect bounce
291    restitution: f32,
292
293    /// Coefficient of friction
294    ///
295    /// 0.0 = frictionless, 1.0 = high friction
296    friction: f32,
297
298    /// Density for mass calculation (kg/m²)
299    ///
300    /// If set, the mass is automatically calculated from shape area × density.
301    /// If None, the [`RigidBody`](crate::ecs::components::RigidBody)'s mass is used directly.
302    density: Option<f32>,
303
304    /// Collision layer bitmask (which layer this collider is on)
305    ///
306    /// Use powers of 2 for layer values: 0b0001, 0b0010, 0b0100, etc.
307    layer: u32,
308
309    /// Collision mask bitmask (which layers this collider can collide with)
310    ///
311    /// Collision occurs if: (layer_a & mask_b) != 0 && (layer_b & mask_a) != 0
312    mask: u32,
313
314    /// If true, this collider is a sensor (trigger) that detects collisions
315    /// but doesn't produce physical response
316    is_sensor: bool,
317
318    /// If true, this collider is enabled and participates in collision detection
319    enabled: bool,
320}
321
322impl Collider {
323    // -------------------------------------------------------------------------
324    // Constructors
325    // -------------------------------------------------------------------------
326
327    /// Creates a new circle collider with the given radius.
328    ///
329    /// # Examples
330    ///
331    /// ```
332    /// use goud_engine::ecs::components::Collider;
333    ///
334    /// let ball = Collider::circle(0.5);
335    /// ```
336    pub fn circle(radius: f32) -> Self {
337        Self {
338            shape: ColliderShape::Circle { radius },
339            restitution: 0.3,
340            friction: 0.5,
341            density: None,
342            layer: 0xFFFFFFFF, // Default: all layers
343            mask: 0xFFFFFFFF,  // Default: collide with all layers
344            is_sensor: false,
345            enabled: true,
346        }
347    }
348
349    /// Creates a new axis-aligned box collider with the given half-extents.
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// use goud_engine::ecs::components::Collider;
355    /// use goud_engine::core::math::Vec2;
356    ///
357    /// // 10x2 box (half-extents are half the size)
358    /// let wall = Collider::aabb(Vec2::new(5.0, 1.0));
359    /// ```
360    pub fn aabb(half_extents: Vec2) -> Self {
361        Self {
362            shape: ColliderShape::Aabb { half_extents },
363            restitution: 0.3,
364            friction: 0.5,
365            density: None,
366            layer: 0xFFFFFFFF,
367            mask: 0xFFFFFFFF,
368            is_sensor: false,
369            enabled: true,
370        }
371    }
372
373    /// Creates a new oriented box collider with the given half-extents.
374    ///
375    /// Similar to `aabb()` but supports rotation via the entity's Transform2D.
376    pub fn obb(half_extents: Vec2) -> Self {
377        Self {
378            shape: ColliderShape::Obb { half_extents },
379            restitution: 0.3,
380            friction: 0.5,
381            density: None,
382            layer: 0xFFFFFFFF,
383            mask: 0xFFFFFFFF,
384            is_sensor: false,
385            enabled: true,
386        }
387    }
388
389    /// Creates a new capsule collider with the given half-height and radius.
390    ///
391    /// Good for character controllers with smooth movement.
392    ///
393    /// # Examples
394    ///
395    /// ```
396    /// use goud_engine::ecs::components::Collider;
397    ///
398    /// // Capsule with 0.6 radius and 2.0 total height
399    /// let player = Collider::capsule(0.3, 1.0);
400    /// ```
401    pub fn capsule(half_height: f32, radius: f32) -> Self {
402        Self {
403            shape: ColliderShape::Capsule {
404                half_height,
405                radius,
406            },
407            restitution: 0.3,
408            friction: 0.5,
409            density: None,
410            layer: 0xFFFFFFFF,
411            mask: 0xFFFFFFFF,
412            is_sensor: false,
413            enabled: true,
414        }
415    }
416
417    /// Creates a new convex polygon collider with the given vertices.
418    ///
419    /// Vertices must be in counter-clockwise order and form a convex hull.
420    ///
421    /// # Panics
422    ///
423    /// Panics if fewer than 3 vertices are provided.
424    pub fn polygon(vertices: Vec<Vec2>) -> Self {
425        assert!(
426            vertices.len() >= 3,
427            "Polygon collider must have at least 3 vertices"
428        );
429        Self {
430            shape: ColliderShape::Polygon { vertices },
431            restitution: 0.3,
432            friction: 0.5,
433            density: None,
434            layer: 0xFFFFFFFF,
435            mask: 0xFFFFFFFF,
436            is_sensor: false,
437            enabled: true,
438        }
439    }
440
441    // -------------------------------------------------------------------------
442    // Builder Pattern
443    // -------------------------------------------------------------------------
444
445    /// Sets the restitution (bounciness) coefficient.
446    ///
447    /// 0.0 = no bounce, 1.0 = perfect bounce.
448    pub fn with_restitution(mut self, restitution: f32) -> Self {
449        self.restitution = restitution.clamp(0.0, 1.0);
450        self
451    }
452
453    /// Sets the friction coefficient.
454    ///
455    /// 0.0 = frictionless, 1.0 = high friction.
456    pub fn with_friction(mut self, friction: f32) -> Self {
457        self.friction = friction.max(0.0);
458        self
459    }
460
461    /// Sets the density for automatic mass calculation.
462    ///
463    /// Mass will be calculated as: area × density
464    pub fn with_density(mut self, density: f32) -> Self {
465        self.density = Some(density.max(0.0));
466        self
467    }
468
469    /// Sets the collision layer bitmask.
470    pub fn with_layer(mut self, layer: u32) -> Self {
471        self.layer = layer;
472        self
473    }
474
475    /// Sets the collision mask bitmask.
476    pub fn with_mask(mut self, mask: u32) -> Self {
477        self.mask = mask;
478        self
479    }
480
481    /// Sets whether this collider is a sensor (trigger).
482    pub fn with_is_sensor(mut self, is_sensor: bool) -> Self {
483        self.is_sensor = is_sensor;
484        self
485    }
486
487    /// Sets whether this collider is enabled.
488    pub fn with_enabled(mut self, enabled: bool) -> Self {
489        self.enabled = enabled;
490        self
491    }
492
493    // -------------------------------------------------------------------------
494    // Accessors
495    // -------------------------------------------------------------------------
496
497    /// Returns a reference to the collision shape.
498    pub fn shape(&self) -> &ColliderShape {
499        &self.shape
500    }
501
502    /// Returns the restitution coefficient.
503    pub fn restitution(&self) -> f32 {
504        self.restitution
505    }
506
507    /// Returns the friction coefficient.
508    pub fn friction(&self) -> f32 {
509        self.friction
510    }
511
512    /// Returns the density, if set.
513    pub fn density(&self) -> Option<f32> {
514        self.density
515    }
516
517    /// Returns the collision layer.
518    pub fn layer(&self) -> u32 {
519        self.layer
520    }
521
522    /// Returns the collision mask.
523    pub fn mask(&self) -> u32 {
524        self.mask
525    }
526
527    /// Returns true if this collider is a sensor (trigger).
528    pub fn is_sensor(&self) -> bool {
529        self.is_sensor
530    }
531
532    /// Returns true if this collider is enabled.
533    pub fn is_enabled(&self) -> bool {
534        self.enabled
535    }
536
537    /// Computes the axis-aligned bounding box (AABB) for this collider.
538    pub fn compute_aabb(&self) -> Rect {
539        self.shape.compute_aabb()
540    }
541
542    /// Checks if this collider can collide with another based on layer filtering.
543    ///
544    /// Returns true if: (self.layer & other.mask) != 0 && (other.layer & self.mask) != 0
545    pub fn can_collide_with(&self, other: &Collider) -> bool {
546        (self.layer & other.mask) != 0 && (other.layer & self.mask) != 0
547    }
548
549    // -------------------------------------------------------------------------
550    // Mutators
551    // -------------------------------------------------------------------------
552
553    /// Sets the restitution coefficient.
554    pub fn set_restitution(&mut self, restitution: f32) {
555        self.restitution = restitution.clamp(0.0, 1.0);
556    }
557
558    /// Sets the friction coefficient.
559    pub fn set_friction(&mut self, friction: f32) {
560        self.friction = friction.max(0.0);
561    }
562
563    /// Sets the density for automatic mass calculation.
564    pub fn set_density(&mut self, density: Option<f32>) {
565        self.density = density.map(|d| d.max(0.0));
566    }
567
568    /// Sets the collision layer.
569    pub fn set_layer(&mut self, layer: u32) {
570        self.layer = layer;
571    }
572
573    /// Sets the collision mask.
574    pub fn set_mask(&mut self, mask: u32) {
575        self.mask = mask;
576    }
577
578    /// Sets whether this collider is a sensor.
579    pub fn set_is_sensor(&mut self, is_sensor: bool) {
580        self.is_sensor = is_sensor;
581    }
582
583    /// Sets whether this collider is enabled.
584    pub fn set_enabled(&mut self, enabled: bool) {
585        self.enabled = enabled;
586    }
587
588    /// Replaces the collision shape.
589    pub fn set_shape(&mut self, shape: ColliderShape) {
590        self.shape = shape;
591    }
592}
593
594impl Default for Collider {
595    /// Returns a default circle collider with radius 0.5.
596    fn default() -> Self {
597        Self::circle(0.5)
598    }
599}
600
601impl std::fmt::Display for Collider {
602    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603        write!(
604            f,
605            "Collider({}, restitution: {:.2}, friction: {:.2}{}{})",
606            self.shape.type_name(),
607            self.restitution,
608            self.friction,
609            if self.is_sensor { ", sensor" } else { "" },
610            if !self.enabled { ", disabled" } else { "" }
611        )
612    }
613}
614
615// Implement Component trait
616impl Component for Collider {}
617
618// =============================================================================
619// AABB Utilities
620// =============================================================================
621
622/// Utilities for Axis-Aligned Bounding Box (AABB) calculations and operations.
623///
624/// These functions are used for broad-phase collision detection, spatial queries,
625/// and efficient geometric tests.
626pub mod aabb {
627    use crate::core::math::{Rect, Vec2};
628    use crate::ecs::components::{ColliderShape, Transform2D};
629
630    /// Computes the world-space AABB for a collider shape with a transform.
631    ///
632    /// This takes a collider's local AABB and transforms it to world space
633    /// using the entity's Transform2D. The result is always axis-aligned
634    /// even if the shape is rotated.
635    ///
636    /// # Examples
637    ///
638    /// ```
639    /// use goud_engine::ecs::components::{Collider, Transform2D, collider};
640    /// use goud_engine::core::math::Vec2;
641    ///
642    /// let collider = Collider::circle(1.0);
643    /// let transform = Transform2D::from_position(Vec2::new(10.0, 20.0));
644    ///
645    /// let world_aabb = collider::aabb::compute_world_aabb(collider.shape(), &transform);
646    /// assert_eq!(world_aabb.center(), Vec2::new(10.0, 20.0));
647    /// ```
648    pub fn compute_world_aabb(shape: &ColliderShape, transform: &Transform2D) -> Rect {
649        let local_aabb = shape.compute_aabb();
650
651        // For circles and AABBs without rotation, we can optimize
652        if matches!(shape, ColliderShape::Circle { .. })
653            || (matches!(shape, ColliderShape::Aabb { .. })
654                && transform.rotation.abs() < f32::EPSILON)
655        {
656            // Simple translation + scale
657            let half_size = local_aabb.size() * 0.5;
658            let scaled_half_size = Vec2::new(
659                half_size.x * transform.scale.x.abs(),
660                half_size.y * transform.scale.y.abs(),
661            );
662            let center = transform.position;
663            return Rect::from_min_max(center - scaled_half_size, center + scaled_half_size);
664        }
665
666        // For rotated shapes, transform all corners and compute bounding box
667        let corners = [
668            Vec2::new(local_aabb.x, local_aabb.y),
669            Vec2::new(local_aabb.x + local_aabb.width, local_aabb.y),
670            Vec2::new(
671                local_aabb.x + local_aabb.width,
672                local_aabb.y + local_aabb.height,
673            ),
674            Vec2::new(local_aabb.x, local_aabb.y + local_aabb.height),
675        ];
676
677        let matrix = transform.matrix();
678        let transformed_corners: Vec<Vec2> = corners
679            .iter()
680            .map(|&corner| matrix.transform_point(corner))
681            .collect();
682
683        // Find min/max of transformed corners
684        let mut min_x = transformed_corners[0].x;
685        let mut min_y = transformed_corners[0].y;
686        let mut max_x = transformed_corners[0].x;
687        let mut max_y = transformed_corners[0].y;
688
689        for corner in &transformed_corners[1..] {
690            min_x = min_x.min(corner.x);
691            min_y = min_y.min(corner.y);
692            max_x = max_x.max(corner.x);
693            max_y = max_y.max(corner.y);
694        }
695
696        Rect::from_min_max(Vec2::new(min_x, min_y), Vec2::new(max_x, max_y))
697    }
698
699    /// Tests if two AABBs overlap.
700    ///
701    /// Returns true if the rectangles intersect or touch.
702    #[inline]
703    pub fn overlaps(a: &Rect, b: &Rect) -> bool {
704        a.intersects(b)
705    }
706
707    /// Computes the intersection of two AABBs.
708    ///
709    /// Returns Some(Rect) with the overlapping region, or None if they don't overlap.
710    #[inline]
711    pub fn intersection(a: &Rect, b: &Rect) -> Option<Rect> {
712        a.intersection(b)
713    }
714
715    /// Expands an AABB by a margin on all sides.
716    ///
717    /// Useful for creating query regions or tolerance zones.
718    ///
719    /// # Examples
720    ///
721    /// ```
722    /// use goud_engine::ecs::components::collider;
723    /// use goud_engine::core::math::Rect;
724    ///
725    /// let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
726    /// let expanded = collider::aabb::expand(&aabb, 1.0);
727    /// assert_eq!(expanded.width, 12.0);
728    /// assert_eq!(expanded.height, 12.0);
729    /// ```
730    pub fn expand(aabb: &Rect, margin: f32) -> Rect {
731        Rect::new(
732            aabb.x - margin,
733            aabb.y - margin,
734            aabb.width + margin * 2.0,
735            aabb.height + margin * 2.0,
736        )
737    }
738
739    /// Merges two AABBs into a single AABB that contains both.
740    ///
741    /// # Examples
742    ///
743    /// ```
744    /// use goud_engine::ecs::components::collider;
745    /// use goud_engine::core::math::Rect;
746    ///
747    /// let a = Rect::new(0.0, 0.0, 5.0, 5.0);
748    /// let b = Rect::new(3.0, 3.0, 5.0, 5.0);
749    /// let merged = collider::aabb::merge(&a, &b);
750    /// assert_eq!(merged.x, 0.0);
751    /// assert_eq!(merged.y, 0.0);
752    /// assert_eq!(merged.width, 8.0);
753    /// assert_eq!(merged.height, 8.0);
754    /// ```
755    pub fn merge(a: &Rect, b: &Rect) -> Rect {
756        let min_x = a.x.min(b.x);
757        let min_y = a.y.min(b.y);
758        let max_x = (a.x + a.width).max(b.x + b.width);
759        let max_y = (a.y + a.height).max(b.y + b.height);
760        Rect::from_min_max(Vec2::new(min_x, min_y), Vec2::new(max_x, max_y))
761    }
762
763    /// Tests if a point is inside an AABB.
764    #[inline]
765    pub fn contains_point(aabb: &Rect, point: Vec2) -> bool {
766        aabb.contains(point)
767    }
768
769    /// Performs a raycast against an AABB.
770    ///
771    /// Returns Some(t) with the intersection parameter [0, 1] if the ray hits,
772    /// or None if it misses. The intersection point is: ray_origin + ray_direction * t.
773    ///
774    /// # Examples
775    ///
776    /// ```
777    /// use goud_engine::ecs::components::collider;
778    /// use goud_engine::core::math::{Rect, Vec2};
779    ///
780    /// let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
781    /// let ray_origin = Vec2::new(-5.0, 5.0);
782    /// let ray_direction = Vec2::new(1.0, 0.0);
783    ///
784    /// let hit = collider::aabb::raycast(&aabb, ray_origin, ray_direction, 100.0);
785    /// assert!(hit.is_some());
786    /// ```
787    pub fn raycast(
788        aabb: &Rect,
789        ray_origin: Vec2,
790        ray_direction: Vec2,
791        max_distance: f32,
792    ) -> Option<f32> {
793        // Slab method for AABB raycast
794        let inv_dir = Vec2::new(
795            if ray_direction.x.abs() < f32::EPSILON {
796                f32::INFINITY
797            } else {
798                1.0 / ray_direction.x
799            },
800            if ray_direction.y.abs() < f32::EPSILON {
801                f32::INFINITY
802            } else {
803                1.0 / ray_direction.y
804            },
805        );
806
807        let min = aabb.min();
808        let max = aabb.max();
809
810        let t1 = (min.x - ray_origin.x) * inv_dir.x;
811        let t2 = (max.x - ray_origin.x) * inv_dir.x;
812        let t3 = (min.y - ray_origin.y) * inv_dir.y;
813        let t4 = (max.y - ray_origin.y) * inv_dir.y;
814
815        let tmin = t1.min(t2).max(t3.min(t4)).max(0.0);
816        let tmax = t1.max(t2).min(t3.max(t4)).min(max_distance);
817
818        if tmax >= tmin && tmin <= max_distance {
819            Some(tmin)
820        } else {
821            None
822        }
823    }
824
825    /// Computes the closest point on an AABB to a given point.
826    ///
827    /// If the point is inside the AABB, returns the point itself.
828    ///
829    /// # Examples
830    ///
831    /// ```
832    /// use goud_engine::ecs::components::collider;
833    /// use goud_engine::core::math::{Rect, Vec2};
834    ///
835    /// let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
836    /// let point = Vec2::new(-5.0, 5.0);
837    ///
838    /// let closest = collider::aabb::closest_point(&aabb, point);
839    /// assert_eq!(closest, Vec2::new(0.0, 5.0));
840    /// ```
841    pub fn closest_point(aabb: &Rect, point: Vec2) -> Vec2 {
842        Vec2::new(
843            point.x.clamp(aabb.x, aabb.x + aabb.width),
844            point.y.clamp(aabb.y, aabb.y + aabb.height),
845        )
846    }
847
848    /// Computes the squared distance from a point to the surface of an AABB.
849    ///
850    /// Returns 0.0 if the point is inside the AABB.
851    pub fn distance_squared_to_point(aabb: &Rect, point: Vec2) -> f32 {
852        let closest = closest_point(aabb, point);
853        let dx = point.x - closest.x;
854        let dy = point.y - closest.y;
855        dx * dx + dy * dy
856    }
857
858    /// Computes the area of an AABB.
859    #[inline]
860    pub fn area(aabb: &Rect) -> f32 {
861        aabb.area()
862    }
863
864    /// Computes the perimeter of an AABB.
865    pub fn perimeter(aabb: &Rect) -> f32 {
866        2.0 * (aabb.width + aabb.height)
867    }
868}
869
870// =============================================================================
871// Tests
872// =============================================================================
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877
878    // -------------------------------------------------------------------------
879    // ColliderShape Tests
880    // -------------------------------------------------------------------------
881
882    #[test]
883    fn test_collider_shape_type_names() {
884        assert_eq!(ColliderShape::Circle { radius: 1.0 }.type_name(), "Circle");
885        assert_eq!(
886            ColliderShape::Aabb {
887                half_extents: Vec2::one()
888            }
889            .type_name(),
890            "AABB"
891        );
892        assert_eq!(
893            ColliderShape::Obb {
894                half_extents: Vec2::one()
895            }
896            .type_name(),
897            "OBB"
898        );
899        assert_eq!(
900            ColliderShape::Capsule {
901                half_height: 1.0,
902                radius: 0.5
903            }
904            .type_name(),
905            "Capsule"
906        );
907        assert_eq!(
908            ColliderShape::Polygon {
909                vertices: vec![Vec2::zero(), Vec2::unit_x(), Vec2::unit_y()]
910            }
911            .type_name(),
912            "Polygon"
913        );
914    }
915
916    #[test]
917    fn test_collider_shape_predicates() {
918        let circle = ColliderShape::Circle { radius: 1.0 };
919        assert!(circle.is_circle());
920        assert!(!circle.is_aabb());
921        assert!(!circle.is_obb());
922        assert!(!circle.is_capsule());
923        assert!(!circle.is_polygon());
924
925        let aabb = ColliderShape::Aabb {
926            half_extents: Vec2::one(),
927        };
928        assert!(!aabb.is_circle());
929        assert!(aabb.is_aabb());
930        assert!(!aabb.is_obb());
931    }
932
933    #[test]
934    fn test_collider_shape_is_valid() {
935        // Valid shapes
936        assert!(ColliderShape::Circle { radius: 1.0 }.is_valid());
937        assert!(ColliderShape::Aabb {
938            half_extents: Vec2::one()
939        }
940        .is_valid());
941        assert!(ColliderShape::Capsule {
942            half_height: 1.0,
943            radius: 0.5
944        }
945        .is_valid());
946        assert!(ColliderShape::Polygon {
947            vertices: vec![Vec2::zero(), Vec2::unit_x(), Vec2::unit_y()]
948        }
949        .is_valid());
950
951        // Invalid shapes
952        assert!(!ColliderShape::Circle { radius: 0.0 }.is_valid());
953        assert!(!ColliderShape::Circle { radius: -1.0 }.is_valid());
954        assert!(!ColliderShape::Aabb {
955            half_extents: Vec2::zero()
956        }
957        .is_valid());
958        assert!(!ColliderShape::Polygon {
959            vertices: vec![Vec2::zero(), Vec2::unit_x()]
960        }
961        .is_valid());
962    }
963
964    #[test]
965    fn test_collider_shape_compute_aabb_circle() {
966        let shape = ColliderShape::Circle { radius: 2.0 };
967        let aabb = shape.compute_aabb();
968        assert_eq!(aabb.x, -2.0);
969        assert_eq!(aabb.y, -2.0);
970        assert_eq!(aabb.width, 4.0);
971        assert_eq!(aabb.height, 4.0);
972    }
973
974    #[test]
975    fn test_collider_shape_compute_aabb_box() {
976        let shape = ColliderShape::Aabb {
977            half_extents: Vec2::new(3.0, 2.0),
978        };
979        let aabb = shape.compute_aabb();
980        assert_eq!(aabb.x, -3.0);
981        assert_eq!(aabb.y, -2.0);
982        assert_eq!(aabb.width, 6.0);
983        assert_eq!(aabb.height, 4.0);
984    }
985
986    #[test]
987    fn test_collider_shape_compute_aabb_capsule() {
988        let shape = ColliderShape::Capsule {
989            half_height: 1.0,
990            radius: 0.5,
991        };
992        let aabb = shape.compute_aabb();
993        assert_eq!(aabb.x, -0.5);
994        assert_eq!(aabb.y, -1.5);
995        assert_eq!(aabb.width, 1.0);
996        assert_eq!(aabb.height, 3.0);
997    }
998
999    #[test]
1000    fn test_collider_shape_compute_aabb_polygon() {
1001        let shape = ColliderShape::Polygon {
1002            vertices: vec![
1003                Vec2::new(-1.0, -1.0),
1004                Vec2::new(2.0, 0.0),
1005                Vec2::new(0.0, 3.0),
1006            ],
1007        };
1008        let aabb = shape.compute_aabb();
1009        assert_eq!(aabb.x, -1.0);
1010        assert_eq!(aabb.y, -1.0);
1011        assert_eq!(aabb.width, 3.0);
1012        assert_eq!(aabb.height, 4.0);
1013    }
1014
1015    #[test]
1016    fn test_collider_shape_compute_aabb_empty_polygon() {
1017        let shape = ColliderShape::Polygon { vertices: vec![] };
1018        let aabb = shape.compute_aabb();
1019        // Should return unit rect for empty polygon
1020        assert_eq!(aabb, Rect::unit());
1021    }
1022
1023    #[test]
1024    fn test_collider_shape_default() {
1025        let shape = ColliderShape::default();
1026        assert!(shape.is_circle());
1027        if let ColliderShape::Circle { radius } = shape {
1028            assert_eq!(radius, 1.0);
1029        }
1030    }
1031
1032    // -------------------------------------------------------------------------
1033    // Collider Tests
1034    // -------------------------------------------------------------------------
1035
1036    #[test]
1037    fn test_collider_circle() {
1038        let collider = Collider::circle(1.5);
1039        assert!(collider.shape().is_circle());
1040        assert_eq!(collider.restitution(), 0.3);
1041        assert_eq!(collider.friction(), 0.5);
1042        assert!(!collider.is_sensor());
1043        assert!(collider.is_enabled());
1044    }
1045
1046    #[test]
1047    fn test_collider_aabb() {
1048        let collider = Collider::aabb(Vec2::new(2.0, 3.0));
1049        assert!(collider.shape().is_aabb());
1050    }
1051
1052    #[test]
1053    fn test_collider_obb() {
1054        let collider = Collider::obb(Vec2::new(2.0, 3.0));
1055        assert!(collider.shape().is_obb());
1056    }
1057
1058    #[test]
1059    fn test_collider_capsule() {
1060        let collider = Collider::capsule(1.0, 0.5);
1061        assert!(collider.shape().is_capsule());
1062    }
1063
1064    #[test]
1065    fn test_collider_polygon() {
1066        let collider = Collider::polygon(vec![Vec2::zero(), Vec2::unit_x(), Vec2::new(0.5, 1.0)]);
1067        assert!(collider.shape().is_polygon());
1068    }
1069
1070    #[test]
1071    #[should_panic(expected = "must have at least 3 vertices")]
1072    fn test_collider_polygon_panics_with_too_few_vertices() {
1073        Collider::polygon(vec![Vec2::zero(), Vec2::unit_x()]);
1074    }
1075
1076    #[test]
1077    fn test_collider_builder_pattern() {
1078        let collider = Collider::circle(1.0)
1079            .with_restitution(0.8)
1080            .with_friction(0.1)
1081            .with_density(2.5)
1082            .with_layer(0b0010)
1083            .with_mask(0b1100)
1084            .with_is_sensor(true)
1085            .with_enabled(false);
1086
1087        assert_eq!(collider.restitution(), 0.8);
1088        assert_eq!(collider.friction(), 0.1);
1089        assert_eq!(collider.density(), Some(2.5));
1090        assert_eq!(collider.layer(), 0b0010);
1091        assert_eq!(collider.mask(), 0b1100);
1092        assert!(collider.is_sensor());
1093        assert!(!collider.is_enabled());
1094    }
1095
1096    #[test]
1097    fn test_collider_restitution_clamping() {
1098        let collider = Collider::circle(1.0).with_restitution(1.5);
1099        assert_eq!(collider.restitution(), 1.0);
1100
1101        let collider = Collider::circle(1.0).with_restitution(-0.5);
1102        assert_eq!(collider.restitution(), 0.0);
1103    }
1104
1105    #[test]
1106    fn test_collider_friction_clamping() {
1107        let collider = Collider::circle(1.0).with_friction(-1.0);
1108        assert_eq!(collider.friction(), 0.0);
1109
1110        // Friction can exceed 1.0
1111        let collider = Collider::circle(1.0).with_friction(2.0);
1112        assert_eq!(collider.friction(), 2.0);
1113    }
1114
1115    #[test]
1116    fn test_collider_density_clamping() {
1117        let collider = Collider::circle(1.0).with_density(-1.0);
1118        assert_eq!(collider.density(), Some(0.0));
1119    }
1120
1121    #[test]
1122    fn test_collider_mutators() {
1123        let mut collider = Collider::circle(1.0);
1124
1125        collider.set_restitution(0.9);
1126        assert_eq!(collider.restitution(), 0.9);
1127
1128        collider.set_friction(0.2);
1129        assert_eq!(collider.friction(), 0.2);
1130
1131        collider.set_density(Some(3.0));
1132        assert_eq!(collider.density(), Some(3.0));
1133
1134        collider.set_layer(0b0100);
1135        assert_eq!(collider.layer(), 0b0100);
1136
1137        collider.set_mask(0b1000);
1138        assert_eq!(collider.mask(), 0b1000);
1139
1140        collider.set_is_sensor(true);
1141        assert!(collider.is_sensor());
1142
1143        collider.set_enabled(false);
1144        assert!(!collider.is_enabled());
1145    }
1146
1147    #[test]
1148    fn test_collider_can_collide_with() {
1149        let collider_a = Collider::circle(1.0).with_layer(0b0001).with_mask(0b0010);
1150        let collider_b = Collider::circle(1.0).with_layer(0b0010).with_mask(0b0001);
1151        let collider_c = Collider::circle(1.0).with_layer(0b0100).with_mask(0b1000);
1152
1153        // A and B should collide (mutual layer/mask match)
1154        assert!(collider_a.can_collide_with(&collider_b));
1155        assert!(collider_b.can_collide_with(&collider_a));
1156
1157        // A and C should not collide (no layer/mask overlap)
1158        assert!(!collider_a.can_collide_with(&collider_c));
1159        assert!(!collider_c.can_collide_with(&collider_a));
1160
1161        // B and C should not collide
1162        assert!(!collider_b.can_collide_with(&collider_c));
1163    }
1164
1165    #[test]
1166    fn test_collider_compute_aabb() {
1167        let collider = Collider::circle(2.0);
1168        let aabb = collider.compute_aabb();
1169        assert_eq!(aabb.x, -2.0);
1170        assert_eq!(aabb.y, -2.0);
1171        assert_eq!(aabb.width, 4.0);
1172        assert_eq!(aabb.height, 4.0);
1173    }
1174
1175    #[test]
1176    fn test_collider_set_shape() {
1177        let mut collider = Collider::circle(1.0);
1178        assert!(collider.shape().is_circle());
1179
1180        collider.set_shape(ColliderShape::Aabb {
1181            half_extents: Vec2::one(),
1182        });
1183        assert!(collider.shape().is_aabb());
1184    }
1185
1186    #[test]
1187    fn test_collider_default() {
1188        let collider = Collider::default();
1189        assert!(collider.shape().is_circle());
1190        assert_eq!(collider.restitution(), 0.3);
1191        assert_eq!(collider.friction(), 0.5);
1192        assert!(!collider.is_sensor());
1193        assert!(collider.is_enabled());
1194    }
1195
1196    #[test]
1197    fn test_collider_display() {
1198        let collider = Collider::circle(1.0);
1199        let display = format!("{}", collider);
1200        assert!(display.contains("Circle"));
1201        assert!(display.contains("restitution"));
1202        assert!(display.contains("friction"));
1203
1204        let sensor = Collider::circle(1.0).with_is_sensor(true);
1205        let display = format!("{}", sensor);
1206        assert!(display.contains("sensor"));
1207
1208        let disabled = Collider::circle(1.0).with_enabled(false);
1209        let display = format!("{}", disabled);
1210        assert!(display.contains("disabled"));
1211    }
1212
1213    #[test]
1214    fn test_collider_is_component() {
1215        fn assert_component<T: Component>() {}
1216        assert_component::<Collider>();
1217    }
1218
1219    #[test]
1220    fn test_collider_is_send() {
1221        fn assert_send<T: Send>() {}
1222        assert_send::<Collider>();
1223    }
1224
1225    #[test]
1226    fn test_collider_is_sync() {
1227        fn assert_sync<T: Sync>() {}
1228        assert_sync::<Collider>();
1229    }
1230
1231    #[test]
1232    fn test_collider_clone() {
1233        let collider = Collider::circle(1.0).with_restitution(0.8);
1234        let cloned = collider.clone();
1235        assert_eq!(collider, cloned);
1236    }
1237
1238    #[test]
1239    fn test_collider_shape_clone() {
1240        let shape = ColliderShape::Circle { radius: 1.0 };
1241        let cloned = shape.clone();
1242        assert_eq!(shape, cloned);
1243    }
1244
1245    // -------------------------------------------------------------------------
1246    // AABB Utilities Tests
1247    // -------------------------------------------------------------------------
1248
1249    use super::aabb;
1250    use crate::ecs::components::Transform2D;
1251
1252    #[test]
1253    fn test_aabb_compute_world_aabb_circle_no_rotation() {
1254        let shape = ColliderShape::Circle { radius: 2.0 };
1255        let transform = Transform2D::from_position(Vec2::new(10.0, 20.0));
1256
1257        let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1258        assert_eq!(world_aabb.center(), Vec2::new(10.0, 20.0));
1259        assert_eq!(world_aabb.width, 4.0);
1260        assert_eq!(world_aabb.height, 4.0);
1261    }
1262
1263    #[test]
1264    fn test_aabb_compute_world_aabb_circle_with_scale() {
1265        let shape = ColliderShape::Circle { radius: 1.0 };
1266        let mut transform = Transform2D::from_position(Vec2::new(5.0, 5.0));
1267        transform.set_scale_uniform(2.0);
1268
1269        let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1270        assert_eq!(world_aabb.center(), Vec2::new(5.0, 5.0));
1271        assert_eq!(world_aabb.width, 4.0); // 2 * radius * scale
1272        assert_eq!(world_aabb.height, 4.0);
1273    }
1274
1275    #[test]
1276    fn test_aabb_compute_world_aabb_box_no_rotation() {
1277        let shape = ColliderShape::Aabb {
1278            half_extents: Vec2::new(3.0, 2.0),
1279        };
1280        let transform = Transform2D::from_position(Vec2::new(10.0, 20.0));
1281
1282        let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1283        assert_eq!(world_aabb.center(), Vec2::new(10.0, 20.0));
1284        assert_eq!(world_aabb.width, 6.0);
1285        assert_eq!(world_aabb.height, 4.0);
1286    }
1287
1288    #[test]
1289    fn test_aabb_compute_world_aabb_box_with_rotation() {
1290        let shape = ColliderShape::Obb {
1291            half_extents: Vec2::new(2.0, 1.0),
1292        };
1293        let mut transform = Transform2D::from_position(Vec2::new(0.0, 0.0));
1294        transform.set_rotation(std::f32::consts::PI / 4.0); // 45 degrees
1295
1296        let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1297
1298        // At 45 degrees, a 4x2 box should have AABB approximately sqrt(20) ≈ 4.47 on each side
1299        // But corners at (±2, ±1) rotated 45° gives max extent ≈ 2.12
1300        assert!(world_aabb.width > 4.0 && world_aabb.width < 4.5);
1301        assert!(world_aabb.height > 4.0 && world_aabb.height < 4.5);
1302        assert_eq!(world_aabb.center(), Vec2::new(0.0, 0.0));
1303    }
1304
1305    #[test]
1306    fn test_aabb_compute_world_aabb_capsule() {
1307        let shape = ColliderShape::Capsule {
1308            half_height: 1.0,
1309            radius: 0.5,
1310        };
1311        let transform = Transform2D::from_position(Vec2::new(5.0, 10.0));
1312
1313        let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1314        assert_eq!(world_aabb.center(), Vec2::new(5.0, 10.0));
1315        assert_eq!(world_aabb.width, 1.0); // 2 * radius
1316        assert_eq!(world_aabb.height, 3.0); // 2 * (half_height + radius)
1317    }
1318
1319    #[test]
1320    fn test_aabb_overlaps() {
1321        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
1322        let b = Rect::new(5.0, 5.0, 10.0, 10.0);
1323        let c = Rect::new(20.0, 20.0, 10.0, 10.0);
1324
1325        assert!(aabb::overlaps(&a, &b));
1326        assert!(aabb::overlaps(&b, &a));
1327        assert!(!aabb::overlaps(&a, &c));
1328    }
1329
1330    #[test]
1331    fn test_aabb_intersection() {
1332        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
1333        let b = Rect::new(5.0, 5.0, 10.0, 10.0);
1334
1335        let intersection = aabb::intersection(&a, &b);
1336        assert!(intersection.is_some());
1337        let rect = intersection.unwrap();
1338        assert_eq!(rect.x, 5.0);
1339        assert_eq!(rect.y, 5.0);
1340        assert_eq!(rect.width, 5.0);
1341        assert_eq!(rect.height, 5.0);
1342    }
1343
1344    #[test]
1345    fn test_aabb_intersection_none() {
1346        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
1347        let b = Rect::new(20.0, 20.0, 10.0, 10.0);
1348
1349        assert!(aabb::intersection(&a, &b).is_none());
1350    }
1351
1352    #[test]
1353    fn test_aabb_expand() {
1354        let aabb = Rect::new(5.0, 5.0, 10.0, 10.0);
1355        let expanded = aabb::expand(&aabb, 2.0);
1356
1357        assert_eq!(expanded.x, 3.0);
1358        assert_eq!(expanded.y, 3.0);
1359        assert_eq!(expanded.width, 14.0);
1360        assert_eq!(expanded.height, 14.0);
1361    }
1362
1363    #[test]
1364    fn test_aabb_expand_negative_margin() {
1365        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1366        let shrunk = aabb::expand(&aabb, -1.0);
1367
1368        assert_eq!(shrunk.x, 1.0);
1369        assert_eq!(shrunk.y, 1.0);
1370        assert_eq!(shrunk.width, 8.0);
1371        assert_eq!(shrunk.height, 8.0);
1372    }
1373
1374    #[test]
1375    fn test_aabb_merge() {
1376        let a = Rect::new(0.0, 0.0, 5.0, 5.0);
1377        let b = Rect::new(3.0, 3.0, 5.0, 5.0);
1378        let merged = aabb::merge(&a, &b);
1379
1380        assert_eq!(merged.x, 0.0);
1381        assert_eq!(merged.y, 0.0);
1382        assert_eq!(merged.width, 8.0);
1383        assert_eq!(merged.height, 8.0);
1384    }
1385
1386    #[test]
1387    fn test_aabb_merge_disjoint() {
1388        let a = Rect::new(0.0, 0.0, 5.0, 5.0);
1389        let b = Rect::new(10.0, 10.0, 5.0, 5.0);
1390        let merged = aabb::merge(&a, &b);
1391
1392        assert_eq!(merged.x, 0.0);
1393        assert_eq!(merged.y, 0.0);
1394        assert_eq!(merged.width, 15.0);
1395        assert_eq!(merged.height, 15.0);
1396    }
1397
1398    #[test]
1399    fn test_aabb_contains_point() {
1400        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1401
1402        assert!(aabb::contains_point(&aabb, Vec2::new(5.0, 5.0)));
1403        assert!(aabb::contains_point(&aabb, Vec2::new(0.0, 0.0)));
1404        assert!(!aabb::contains_point(&aabb, Vec2::new(-1.0, 5.0)));
1405        assert!(!aabb::contains_point(&aabb, Vec2::new(5.0, 11.0)));
1406    }
1407
1408    #[test]
1409    fn test_aabb_raycast_hit() {
1410        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1411        let ray_origin = Vec2::new(-5.0, 5.0);
1412        let ray_direction = Vec2::new(1.0, 0.0);
1413
1414        let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 100.0);
1415        assert!(hit.is_some());
1416        let t = hit.unwrap();
1417        assert!((t - 5.0).abs() < 0.001); // Should hit at t=5
1418    }
1419
1420    #[test]
1421    fn test_aabb_raycast_miss() {
1422        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1423        let ray_origin = Vec2::new(-5.0, 15.0);
1424        let ray_direction = Vec2::new(1.0, 0.0);
1425
1426        let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 100.0);
1427        assert!(hit.is_none());
1428    }
1429
1430    #[test]
1431    fn test_aabb_raycast_from_inside() {
1432        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1433        let ray_origin = Vec2::new(5.0, 5.0);
1434        let ray_direction = Vec2::new(1.0, 0.0);
1435
1436        let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 100.0);
1437        assert!(hit.is_some());
1438        assert_eq!(hit.unwrap(), 0.0); // Ray starts inside
1439    }
1440
1441    #[test]
1442    fn test_aabb_raycast_max_distance() {
1443        let aabb = Rect::new(100.0, 0.0, 10.0, 10.0);
1444        let ray_origin = Vec2::new(0.0, 5.0);
1445        let ray_direction = Vec2::new(1.0, 0.0);
1446
1447        // Should not hit because max_distance is too short
1448        let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 50.0);
1449        assert!(hit.is_none());
1450
1451        // Should hit with longer max_distance
1452        let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 200.0);
1453        assert!(hit.is_some());
1454    }
1455
1456    #[test]
1457    fn test_aabb_closest_point_outside() {
1458        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1459        let point = Vec2::new(-5.0, 5.0);
1460
1461        let closest = aabb::closest_point(&aabb, point);
1462        assert_eq!(closest, Vec2::new(0.0, 5.0));
1463    }
1464
1465    #[test]
1466    fn test_aabb_closest_point_inside() {
1467        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1468        let point = Vec2::new(5.0, 5.0);
1469
1470        let closest = aabb::closest_point(&aabb, point);
1471        assert_eq!(closest, point); // Point is inside, returns itself
1472    }
1473
1474    #[test]
1475    fn test_aabb_closest_point_corner() {
1476        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1477        let point = Vec2::new(-5.0, -5.0);
1478
1479        let closest = aabb::closest_point(&aabb, point);
1480        assert_eq!(closest, Vec2::new(0.0, 0.0));
1481    }
1482
1483    #[test]
1484    fn test_aabb_distance_squared_to_point_outside() {
1485        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1486        let point = Vec2::new(-3.0, 0.0);
1487
1488        let dist_sq = aabb::distance_squared_to_point(&aabb, point);
1489        assert_eq!(dist_sq, 9.0); // Distance is 3.0, squared is 9.0
1490    }
1491
1492    #[test]
1493    fn test_aabb_distance_squared_to_point_inside() {
1494        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1495        let point = Vec2::new(5.0, 5.0);
1496
1497        let dist_sq = aabb::distance_squared_to_point(&aabb, point);
1498        assert_eq!(dist_sq, 0.0);
1499    }
1500
1501    #[test]
1502    fn test_aabb_area() {
1503        let aabb = Rect::new(0.0, 0.0, 10.0, 5.0);
1504        assert_eq!(aabb::area(&aabb), 50.0);
1505    }
1506
1507    #[test]
1508    fn test_aabb_perimeter() {
1509        let aabb = Rect::new(0.0, 0.0, 10.0, 5.0);
1510        assert_eq!(aabb::perimeter(&aabb), 30.0); // 2 * (10 + 5)
1511    }
1512
1513    #[test]
1514    fn test_aabb_compute_world_aabb_polygon() {
1515        let vertices = vec![
1516            Vec2::new(-1.0, -1.0),
1517            Vec2::new(2.0, 0.0),
1518            Vec2::new(0.0, 3.0),
1519        ];
1520        let shape = ColliderShape::Polygon { vertices };
1521        let transform = Transform2D::from_position(Vec2::new(10.0, 20.0));
1522
1523        let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1524
1525        // Local AABB is (-1, -1) to (2, 3), size (3, 4)
1526        // Translated to (10, 20) should give center at (10.5, 22)
1527        let expected_min = Vec2::new(9.0, 19.0);
1528        let expected_max = Vec2::new(12.0, 23.0);
1529
1530        assert!((world_aabb.x - expected_min.x).abs() < 0.001);
1531        assert!((world_aabb.y - expected_min.y).abs() < 0.001);
1532        assert!((world_aabb.max().x - expected_max.x).abs() < 0.001);
1533        assert!((world_aabb.max().y - expected_max.y).abs() < 0.001);
1534    }
1535
1536    #[test]
1537    fn test_aabb_raycast_diagonal() {
1538        let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1539        let ray_origin = Vec2::new(-5.0, -5.0);
1540        let ray_direction = Vec2::new(1.0, 1.0).normalize();
1541
1542        let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 100.0);
1543        assert!(hit.is_some());
1544
1545        // Hit point should be near (0, 0)
1546        let t = hit.unwrap();
1547        let hit_point = ray_origin + ray_direction * t;
1548        assert!((hit_point.x - 0.0).abs() < 0.1);
1549        assert!((hit_point.y - 0.0).abs() < 0.1);
1550    }
1551
1552    #[test]
1553    fn test_aabb_expand_zero_margin() {
1554        let aabb = Rect::new(5.0, 5.0, 10.0, 10.0);
1555        let expanded = aabb::expand(&aabb, 0.0);
1556
1557        assert_eq!(expanded, aabb);
1558    }
1559}