55//! of the platform layer.
66
77use std:: {
8+ collections:: {
9+ HashMap ,
10+ HashSet ,
11+ } ,
812 error:: Error ,
913 fmt,
1014} ;
@@ -118,6 +122,36 @@ pub struct RaycastHit2DBackend {
118122 pub distance : f32 ,
119123}
120124
125+ /// Indicates whether a backend collision pair started or ended contact.
126+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
127+ pub enum CollisionEventKind2DBackend {
128+ /// The body pair started touching during the current backend step.
129+ Started ,
130+ /// The body pair stopped touching during the current backend step.
131+ Ended ,
132+ }
133+
134+ /// Backend-agnostic data describing one 2D collision event.
135+ #[ derive( Debug , Clone , Copy , PartialEq ) ]
136+ pub struct CollisionEvent2DBackend {
137+ /// The transition kind for the body pair.
138+ pub kind : CollisionEventKind2DBackend ,
139+ /// The first rigid body's slot index.
140+ pub body_a_slot_index : u32 ,
141+ /// The first rigid body's slot generation.
142+ pub body_a_slot_generation : u32 ,
143+ /// The second rigid body's slot index.
144+ pub body_b_slot_index : u32 ,
145+ /// The second rigid body's slot generation.
146+ pub body_b_slot_generation : u32 ,
147+ /// The representative world-space contact point, when available.
148+ pub contact_point : Option < [ f32 ; 2 ] > ,
149+ /// The representative world-space contact normal, when available.
150+ pub normal : Option < [ f32 ; 2 ] > ,
151+ /// The representative penetration depth, when available.
152+ pub penetration : Option < f32 > ,
153+ }
154+
121155/// The fallback mass applied to dynamic bodies before density colliders exist.
122156const DYNAMIC_BODY_FALLBACK_MASS_KG : f32 = 1.0 ;
123157
@@ -188,6 +222,30 @@ struct ColliderAttachmentMassPlan2D {
188222 should_remove_fallback_mass : bool ,
189223}
190224
225+ /// A normalized body-pair key used for backend collision tracking.
226+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash ) ]
227+ struct BodyPairKey2D {
228+ /// The first body slot index.
229+ body_a_slot_index : u32 ,
230+ /// The first body slot generation.
231+ body_a_slot_generation : u32 ,
232+ /// The second body slot index.
233+ body_b_slot_index : u32 ,
234+ /// The second body slot generation.
235+ body_b_slot_generation : u32 ,
236+ }
237+
238+ /// The representative contact selected for a body pair during one step.
239+ #[ derive( Debug , Clone , Copy , PartialEq ) ]
240+ struct BodyPairContact2D {
241+ /// The representative world-space contact point.
242+ point : [ f32 ; 2 ] ,
243+ /// The representative world-space normal from body A toward body B.
244+ normal : [ f32 ; 2 ] ,
245+ /// The non-negative penetration depth.
246+ penetration : f32 ,
247+ }
248+
191249/// A 2D physics backend powered by `rapier2d`.
192250///
193251/// This type is an internal implementation detail used by `lambda-rs`.
@@ -205,6 +263,8 @@ pub struct PhysicsBackend2D {
205263 pipeline : PhysicsPipeline ,
206264 rigid_body_slots_2d : Vec < RigidBodySlot2D > ,
207265 collider_slots_2d : Vec < ColliderSlot2D > ,
266+ active_body_pairs_2d : HashSet < BodyPairKey2D > ,
267+ queued_collision_events_2d : Vec < CollisionEvent2DBackend > ,
208268}
209269
210270impl PhysicsBackend2D {
@@ -244,6 +304,8 @@ impl PhysicsBackend2D {
244304 pipeline : PhysicsPipeline :: new ( ) ,
245305 rigid_body_slots_2d : Vec :: new ( ) ,
246306 collider_slots_2d : Vec :: new ( ) ,
307+ active_body_pairs_2d : HashSet :: new ( ) ,
308+ queued_collision_events_2d : Vec :: new ( ) ,
247309 } ;
248310 }
249311
@@ -1048,9 +1110,24 @@ impl PhysicsBackend2D {
10481110 & ( ) ,
10491111 ) ;
10501112
1113+ self . collect_collision_events_2d ( ) ;
1114+
10511115 return ;
10521116 }
10531117
1118+ /// Drains backend collision events queued by prior step calls.
1119+ ///
1120+ /// The backend accumulates transition events across substeps so
1121+ /// `PhysicsWorld2D` can expose one post-step drain point without exposing
1122+ /// Rapier's event machinery or borrowing backend internals through the
1123+ /// public iterator.
1124+ ///
1125+ /// # Returns
1126+ /// Returns all queued backend collision events in insertion order.
1127+ pub fn drain_collision_events_2d ( & mut self ) -> Vec < CollisionEvent2DBackend > {
1128+ return std:: mem:: take ( & mut self . queued_collision_events_2d ) ;
1129+ }
1130+
10541131 /// Returns an immutable reference to a rigid body slot after validation.
10551132 ///
10561133 /// # Arguments
@@ -1209,6 +1286,78 @@ impl PhysicsBackend2D {
12091286 return ;
12101287 }
12111288
1289+ /// Collects body-pair collision transitions from the current narrow phase.
1290+ ///
1291+ /// The public API reports one event per body pair, not one event per collider
1292+ /// pair. This pass aggregates Rapier collider contacts by owning bodies,
1293+ /// keeps the deepest active contact seen for each body pair, and compares the
1294+ /// resulting active set against the previous step to detect newly-started
1295+ /// contacts without repeating `Started` every frame.
1296+ ///
1297+ /// # Returns
1298+ /// Returns `()` after appending any newly-started events to the backend
1299+ /// queue.
1300+ fn collect_collision_events_2d ( & mut self ) {
1301+ let mut current_body_pair_contacts: HashMap <
1302+ BodyPairKey2D ,
1303+ BodyPairContact2D ,
1304+ > = HashMap :: new ( ) ;
1305+ let mut current_body_pair_order = Vec :: new ( ) ;
1306+
1307+ for contact_pair in self . narrow_phase . contact_pairs ( ) {
1308+ if !contact_pair. has_any_active_contact ( ) {
1309+ continue ;
1310+ }
1311+
1312+ let Some ( ( body_pair_key, body_pair_contact) ) =
1313+ self . body_pair_contact_from_contact_pair_2d ( contact_pair)
1314+ else {
1315+ continue ;
1316+ } ;
1317+
1318+ if let Some ( existing_contact) =
1319+ current_body_pair_contacts. get_mut ( & body_pair_key)
1320+ {
1321+ if body_pair_contact. penetration > existing_contact. penetration {
1322+ * existing_contact = body_pair_contact;
1323+ }
1324+
1325+ continue ;
1326+ }
1327+
1328+ current_body_pair_order. push ( body_pair_key) ;
1329+ current_body_pair_contacts. insert ( body_pair_key, body_pair_contact) ;
1330+ }
1331+
1332+ for body_pair_key in current_body_pair_order. iter ( ) . copied ( ) {
1333+ if self . active_body_pairs_2d . contains ( & body_pair_key) {
1334+ continue ;
1335+ }
1336+
1337+ let Some ( contact) = current_body_pair_contacts. get ( & body_pair_key) else {
1338+ continue ;
1339+ } ;
1340+
1341+ self
1342+ . queued_collision_events_2d
1343+ . push ( CollisionEvent2DBackend {
1344+ kind : CollisionEventKind2DBackend :: Started ,
1345+ body_a_slot_index : body_pair_key. body_a_slot_index ,
1346+ body_a_slot_generation : body_pair_key. body_a_slot_generation ,
1347+ body_b_slot_index : body_pair_key. body_b_slot_index ,
1348+ body_b_slot_generation : body_pair_key. body_b_slot_generation ,
1349+ contact_point : Some ( contact. point ) ,
1350+ normal : Some ( contact. normal ) ,
1351+ penetration : Some ( contact. penetration ) ,
1352+ } ) ;
1353+ }
1354+
1355+ self . active_body_pairs_2d =
1356+ current_body_pair_contacts. keys ( ) . copied ( ) . collect ( ) ;
1357+
1358+ return ;
1359+ }
1360+
12121361 /// Attaches a prepared Rapier collider builder to a parent rigid body.
12131362 ///
12141363 /// This helper applies the shared local transform and material properties,
@@ -1424,6 +1573,7 @@ impl PhysicsBackend2D {
14241573
14251574 return ;
14261575 }
1576+
14271577 /// Resolves a query hit collider back to its owning rigid body slot.
14281578 ///
14291579 /// # Arguments
@@ -1446,6 +1596,41 @@ impl PhysicsBackend2D {
14461596 collider_slot. parent_slot_generation ,
14471597 ) ) ;
14481598 }
1599+
1600+ /// Resolves one Rapier contact pair into a normalized body-pair contact.
1601+ ///
1602+ /// Rapier stores contacts per collider pair. The public API is body-oriented,
1603+ /// so this helper maps each collider pair back to its owning bodies, discards
1604+ /// self-contacts, normalizes body ordering for stable deduplication, and
1605+ /// returns the deepest active solver contact for that body pair.
1606+ ///
1607+ /// # Arguments
1608+ /// - `contact_pair`: The Rapier contact pair to inspect.
1609+ ///
1610+ /// # Returns
1611+ /// Returns the normalized body-pair key and representative contact when the
1612+ /// pair belongs to two distinct tracked bodies and has at least one active
1613+ /// solver contact.
1614+ fn body_pair_contact_from_contact_pair_2d (
1615+ & self ,
1616+ contact_pair : & ContactPair ,
1617+ ) -> Option < ( BodyPairKey2D , BodyPairContact2D ) > {
1618+ let body_a =
1619+ self . query_hit_to_parent_body_slot_2d ( contact_pair. collider1 ) ?;
1620+ let body_b =
1621+ self . query_hit_to_parent_body_slot_2d ( contact_pair. collider2 ) ?;
1622+
1623+ if body_a == body_b {
1624+ return None ;
1625+ }
1626+
1627+ let ( body_pair_key, should_flip_normal) =
1628+ normalize_body_pair_key_2d ( body_a, body_b) ;
1629+ let representative_contact =
1630+ representative_contact_from_pair_2d ( contact_pair, should_flip_normal) ?;
1631+
1632+ return Some ( ( body_pair_key, representative_contact) ) ;
1633+ }
14491634}
14501635
14511636/// Builds a Rapier rigid body builder with `lambda-rs` invariants applied.
@@ -1522,6 +1707,48 @@ fn build_collision_groups_2d(
15221707 ) ;
15231708}
15241709
1710+ /// Normalizes a body-pair key into a stable `body_a`/`body_b` ordering.
1711+ ///
1712+ /// Stable ordering lets the backend deduplicate collider contacts that belong
1713+ /// to the same bodies and keeps the public event stream from oscillating based
1714+ /// on Rapier's internal pair ordering. The returned boolean tells callers
1715+ /// whether normals reported from collider/body 1 toward collider/body 2 must be
1716+ /// flipped to match the normalized body ordering.
1717+ ///
1718+ /// # Arguments
1719+ /// - `body_a`: The first raw backend body slot pair.
1720+ /// - `body_b`: The second raw backend body slot pair.
1721+ ///
1722+ /// # Returns
1723+ /// Returns the normalized body-pair key and whether contact normals should be
1724+ /// flipped to point from normalized body A toward normalized body B.
1725+ fn normalize_body_pair_key_2d (
1726+ body_a : ( u32 , u32 ) ,
1727+ body_b : ( u32 , u32 ) ,
1728+ ) -> ( BodyPairKey2D , bool ) {
1729+ if body_a <= body_b {
1730+ return (
1731+ BodyPairKey2D {
1732+ body_a_slot_index : body_a. 0 ,
1733+ body_a_slot_generation : body_a. 1 ,
1734+ body_b_slot_index : body_b. 0 ,
1735+ body_b_slot_generation : body_b. 1 ,
1736+ } ,
1737+ false ,
1738+ ) ;
1739+ }
1740+
1741+ return (
1742+ BodyPairKey2D {
1743+ body_a_slot_index : body_b. 0 ,
1744+ body_a_slot_generation : body_b. 1 ,
1745+ body_b_slot_index : body_a. 0 ,
1746+ body_b_slot_generation : body_a. 1 ,
1747+ } ,
1748+ true ,
1749+ ) ;
1750+ }
1751+
15251752/// Validates a 2D position.
15261753///
15271754/// # Arguments
@@ -1601,6 +1828,58 @@ fn normalize_query_vector_2d(vector: [f32; 2]) -> Option<[f32; 2]> {
16011828 return Some ( [ vector[ 0 ] / length, vector[ 1 ] / length] ) ;
16021829}
16031830
1831+ /// Selects the deepest active solver contact from one Rapier contact pair.
1832+ ///
1833+ /// Rapier's collider pair may expose several manifolds and several active
1834+ /// solver contacts per manifold. The public start event only needs one
1835+ /// representative contact, so this helper keeps the active solver contact with
1836+ /// the greatest penetration depth. Solver contacts are used instead of raw
1837+ /// tracked contacts because they already provide world-space points and reflect
1838+ /// the contacts that actually participated in collision resolution this step.
1839+ ///
1840+ /// # Arguments
1841+ /// - `contact_pair`: The Rapier contact pair to inspect.
1842+ /// - `should_flip_normal`: Whether the selected normal should be inverted to
1843+ /// point from normalized body A toward normalized body B.
1844+ ///
1845+ /// # Returns
1846+ /// Returns the deepest active solver contact for the pair, if one exists.
1847+ fn representative_contact_from_pair_2d (
1848+ contact_pair : & ContactPair ,
1849+ should_flip_normal : bool ,
1850+ ) -> Option < BodyPairContact2D > {
1851+ let mut representative_contact = None ;
1852+
1853+ for manifold in & contact_pair. manifolds {
1854+ let mut normal = [ manifold. data . normal . x , manifold. data . normal . y ] ;
1855+
1856+ if should_flip_normal {
1857+ normal = [ -normal[ 0 ] , -normal[ 1 ] ] ;
1858+ }
1859+
1860+ for solver_contact in & manifold. data . solver_contacts {
1861+ let penetration = ( -solver_contact. dist ) . max ( 0.0 ) ;
1862+ let candidate_contact = BodyPairContact2D {
1863+ point : [ solver_contact. point . x , solver_contact. point . y ] ,
1864+ normal,
1865+ penetration,
1866+ } ;
1867+
1868+ if representative_contact. as_ref ( ) . is_some_and (
1869+ |existing : & BodyPairContact2D | {
1870+ candidate_contact. penetration <= existing. penetration
1871+ } ,
1872+ ) {
1873+ continue ;
1874+ }
1875+
1876+ representative_contact = Some ( candidate_contact) ;
1877+ }
1878+ }
1879+
1880+ return representative_contact;
1881+ }
1882+
16041883/// Casts a ray against one live collider and normalizes the reported normal.
16051884///
16061885/// When the origin lies inside a collider, Parry may report a zero normal for
0 commit comments