@@ -103,6 +103,21 @@ impl fmt::Display for Collider2DBackendError {
103103
104104impl Error for Collider2DBackendError { }
105105
106+ /// Backend-agnostic data describing the nearest 2D raycast hit.
107+ #[ derive( Debug , Clone , Copy , PartialEq ) ]
108+ pub struct RaycastHit2DBackend {
109+ /// The hit rigid body's slot index.
110+ pub body_slot_index : u32 ,
111+ /// The hit rigid body's slot generation.
112+ pub body_slot_generation : u32 ,
113+ /// The world-space hit point.
114+ pub point : [ f32 ; 2 ] ,
115+ /// The world-space unit hit normal.
116+ pub normal : [ f32 ; 2 ] ,
117+ /// The non-negative hit distance in meters.
118+ pub distance : f32 ,
119+ }
120+
106121/// The fallback mass applied to dynamic bodies before density colliders exist.
107122const DYNAMIC_BODY_FALLBACK_MASS_KG : f32 = 1.0 ;
108123
@@ -703,6 +718,82 @@ impl PhysicsBackend2D {
703718 return body_slots;
704719 }
705720
721+ /// Returns the nearest rigid body hit by the provided finite ray segment.
722+ ///
723+ /// This iterates the live collider set directly instead of using Rapier's
724+ /// broad-phase query pipeline. The overlap-query work already established
725+ /// that direct iteration is the most reliable way to support read-only
726+ /// queries immediately after collider creation, before any simulation step
727+ /// has synchronized broad-phase acceleration structures.
728+ ///
729+ /// # Arguments
730+ /// - `origin`: The world-space ray origin.
731+ /// - `dir`: The world-space ray direction.
732+ /// - `max_dist`: The maximum query distance in meters.
733+ ///
734+ /// # Returns
735+ /// Returns the nearest hit data when any live collider intersects the ray.
736+ pub fn raycast_2d (
737+ & self ,
738+ origin : [ f32 ; 2 ] ,
739+ dir : [ f32 ; 2 ] ,
740+ max_dist : f32 ,
741+ ) -> Option < RaycastHit2DBackend > {
742+ if validate_position ( origin[ 0 ] , origin[ 1 ] ) . is_err ( )
743+ || validate_velocity ( dir[ 0 ] , dir[ 1 ] ) . is_err ( )
744+ || !max_dist. is_finite ( )
745+ || max_dist <= 0.0
746+ {
747+ return None ;
748+ }
749+
750+ let normalized_dir = normalize_query_vector_2d ( dir) ?;
751+ let ray = Ray :: new (
752+ Vector :: new ( origin[ 0 ] , origin[ 1 ] ) ,
753+ Vector :: new ( normalized_dir[ 0 ] , normalized_dir[ 1 ] ) ,
754+ ) ;
755+ let mut nearest_hit = None ;
756+
757+ for ( collider_handle, collider) in self . colliders . iter ( ) {
758+ // Resolve the public body handle data up front so the final hit payload
759+ // stays backend-agnostic and does not expose Rapier collider handles.
760+ let Some ( body_slot) =
761+ self . query_hit_to_parent_body_slot_2d ( collider_handle)
762+ else {
763+ continue ;
764+ } ;
765+
766+ let Some ( hit) =
767+ cast_live_collider_raycast_hit_2d ( collider, & ray, max_dist)
768+ else {
769+ continue ;
770+ } ;
771+ let hit_point = ray. point_at ( hit. time_of_impact ) ;
772+ let candidate = RaycastHit2DBackend {
773+ body_slot_index : body_slot. 0 ,
774+ body_slot_generation : body_slot. 1 ,
775+ point : [ hit_point. x , hit_point. y ] ,
776+ normal : [ hit. normal . x , hit. normal . y ] ,
777+ distance : hit. time_of_impact ,
778+ } ;
779+
780+ // The public API only returns the nearest hit, so keep the first minimum
781+ // distance we observe while scanning the live collider set.
782+ if nearest_hit
783+ . as_ref ( )
784+ . is_some_and ( |nearest : & RaycastHit2DBackend | {
785+ candidate. distance >= nearest. distance
786+ } )
787+ {
788+ continue ;
789+ }
790+
791+ nearest_hit = Some ( candidate) ;
792+ }
793+
794+ return nearest_hit;
795+ }
796+
706797 /// Sets the current position for the referenced body.
707798 ///
708799 /// # Arguments
@@ -1477,6 +1568,73 @@ fn validate_velocity(x: f32, y: f32) -> Result<(), RigidBody2DBackendError> {
14771568 return Ok ( ( ) ) ;
14781569}
14791570
1571+ /// Normalizes a finite 2D query vector.
1572+ ///
1573+ /// # Arguments
1574+ /// - `vector`: The vector to normalize.
1575+ ///
1576+ /// # Returns
1577+ /// Returns the normalized vector when the input has non-zero finite length.
1578+ ///
1579+ /// Ray queries normalize directions so Rapier's `time_of_impact` value matches
1580+ /// the world-space travel distance expected by the public API.
1581+ fn normalize_query_vector_2d ( vector : [ f32 ; 2 ] ) -> Option < [ f32 ; 2 ] > {
1582+ let length = vector[ 0 ] . hypot ( vector[ 1 ] ) ;
1583+
1584+ if !length. is_finite ( ) || length <= 0.0 {
1585+ return None ;
1586+ }
1587+
1588+ return Some ( [ vector[ 0 ] / length, vector[ 1 ] / length] ) ;
1589+ }
1590+
1591+ /// Casts a ray against one live collider and normalizes the reported normal.
1592+ ///
1593+ /// When the origin lies inside a collider, Parry may report a zero normal for
1594+ /// the solid hit at distance `0.0`. In that case, this helper performs one
1595+ /// non-solid cast along the same ray to recover a stable outward exit normal
1596+ /// while preserving the `0.0` contact distance expected by the public API.
1597+ ///
1598+ /// # Arguments
1599+ /// - `collider`: The live Rapier collider to test.
1600+ /// - `ray`: The normalized query ray.
1601+ /// - `max_dist`: The maximum query distance in meters.
1602+ ///
1603+ /// # Returns
1604+ /// Returns normalized hit data when the collider intersects the finite ray.
1605+ fn cast_live_collider_raycast_hit_2d (
1606+ collider : & Collider ,
1607+ ray : & Ray ,
1608+ max_dist : f32 ,
1609+ ) -> Option < RayIntersection > {
1610+ // Use a solid cast so callers starting inside geometry receive an immediate
1611+ // hit at distance `0.0` instead of only the exit point.
1612+ let mut hit = collider. shape ( ) . cast_ray_and_get_normal (
1613+ collider. position ( ) ,
1614+ ray,
1615+ max_dist,
1616+ true ,
1617+ ) ?;
1618+
1619+ let normalized_normal =
1620+ normalize_query_vector_2d ( [ hit. normal . x , hit. normal . y ] ) . or_else ( || {
1621+ // Some inside hits report a zero normal. A follow-up non-solid cast
1622+ // recovers the exit-face normal without changing the public `0.0`
1623+ // distance contract for origin-inside queries.
1624+ let exit_hit = collider. shape ( ) . cast_ray_and_get_normal (
1625+ collider. position ( ) ,
1626+ ray,
1627+ max_dist,
1628+ false ,
1629+ ) ?;
1630+
1631+ return normalize_query_vector_2d ( [ exit_hit. normal . x , exit_hit. normal . y ] ) ;
1632+ } ) ?;
1633+
1634+ hit. normal = Vector :: new ( normalized_normal[ 0 ] , normalized_normal[ 1 ] ) ;
1635+ return Some ( hit) ;
1636+ }
1637+
14801638/// Validates a 2D force vector.
14811639///
14821640/// # Arguments
0 commit comments