@@ -103,6 +103,9 @@ impl fmt::Display for Collider2DBackendError {
103103
104104impl Error for Collider2DBackendError { }
105105
106+ /// The fallback mass applied to dynamic bodies before density colliders exist.
107+ const DYNAMIC_BODY_FALLBACK_MASS_KG : f32 = 1.0 ;
108+
106109/// Stores per-body state that `lambda-rs` tracks alongside Rapier.
107110///
108111/// This slot exists because `lambda-rs` defines integration semantics that are
@@ -151,6 +154,21 @@ struct ColliderSlot2D {
151154 generation : u32 ,
152155}
153156
157+ /// Describes how collider attachment should affect dynamic-body mass semantics.
158+ ///
159+ /// This helper isolates `lambda-rs` mass rules from the Rapier attachment flow
160+ /// so body creation and collider attachment share one backend policy source.
161+ #[ derive( Debug , Clone , Copy , PartialEq ) ]
162+ struct ColliderAttachmentMassPlan2D {
163+ /// The density value that MUST be passed to the Rapier collider builder.
164+ rapier_density : f32 ,
165+ /// Whether attaching this collider transitions the body to density-driven
166+ /// mass computation.
167+ should_mark_has_positive_density_colliders : bool ,
168+ /// Whether the initial fallback mass MUST be removed before insertion.
169+ should_remove_fallback_mass : bool ,
170+ }
171+
154172/// Applies Lambda collision-material combination rules to solver contacts.
155173///
156174/// This hook implements backend-agnostic combination semantics without
@@ -1111,52 +1129,49 @@ impl PhysicsBackend2D {
11111129 parent_slot_generation : u32 ,
11121130 requested_density : f32 ,
11131131 ) -> Result < ( RigidBodyHandle , f32 ) , Collider2DBackendError > {
1114- let ( body_type, rapier_handle, explicit_dynamic_mass_kg) = {
1132+ let (
1133+ body_type,
1134+ rapier_handle,
1135+ explicit_dynamic_mass_kg,
1136+ has_positive_density_colliders,
1137+ ) = {
11151138 let body_slot = self
11161139 . rigid_body_slot_2d ( parent_slot_index, parent_slot_generation)
11171140 . map_err ( |_| Collider2DBackendError :: BodyNotFound ) ?;
11181141 (
11191142 body_slot. body_type ,
11201143 body_slot. rapier_handle ,
11211144 body_slot. explicit_dynamic_mass_kg ,
1145+ body_slot. has_positive_density_colliders ,
11221146 )
11231147 } ;
11241148
11251149 if self . bodies . get ( rapier_handle) . is_none ( ) {
11261150 return Err ( Collider2DBackendError :: BodyNotFound ) ;
11271151 }
11281152
1129- if body_type != RigidBodyType2D :: Dynamic {
1130- return Ok ( ( rapier_handle, requested_density) ) ;
1131- }
1153+ let attachment_mass_plan = resolve_collider_attachment_mass_plan_2d (
1154+ body_type,
1155+ explicit_dynamic_mass_kg,
1156+ has_positive_density_colliders,
1157+ requested_density,
1158+ ) ;
11321159
1133- if explicit_dynamic_mass_kg. is_some ( ) {
1134- return Ok ( ( rapier_handle, 0.0 ) ) ;
1160+ if attachment_mass_plan. should_mark_has_positive_density_colliders {
1161+ let body_slot = self
1162+ . rigid_body_slot_2d_mut ( parent_slot_index, parent_slot_generation)
1163+ . map_err ( |_| Collider2DBackendError :: BodyNotFound ) ?;
1164+ body_slot. has_positive_density_colliders = true ;
11351165 }
11361166
1137- if requested_density > 0.0 {
1138- let should_remove_fallback_mass = {
1139- let body_slot = self
1140- . rigid_body_slot_2d_mut ( parent_slot_index, parent_slot_generation)
1141- . map_err ( |_| Collider2DBackendError :: BodyNotFound ) ?;
1142-
1143- if body_slot. has_positive_density_colliders {
1144- false
1145- } else {
1146- body_slot. has_positive_density_colliders = true ;
1147- true
1148- }
1167+ if attachment_mass_plan. should_remove_fallback_mass {
1168+ let Some ( rapier_body) = self . bodies . get_mut ( rapier_handle) else {
1169+ return Err ( Collider2DBackendError :: BodyNotFound ) ;
11491170 } ;
1150-
1151- if should_remove_fallback_mass {
1152- let Some ( rapier_body) = self . bodies . get_mut ( rapier_handle) else {
1153- return Err ( Collider2DBackendError :: BodyNotFound ) ;
1154- } ;
1155- rapier_body. set_additional_mass ( 0.0 , true ) ;
1156- }
1171+ rapier_body. set_additional_mass ( 0.0 , true ) ;
11571172 }
11581173
1159- return Ok ( ( rapier_handle, requested_density ) ) ;
1174+ return Ok ( ( rapier_handle, attachment_mass_plan . rapier_density ) ) ;
11601175 }
11611176
11621177 /// Recomputes mass properties for a parent body after collider attachment.
@@ -1408,7 +1423,7 @@ fn resolve_additional_mass_kg(
14081423 return Ok ( explicit_mass_kg) ;
14091424 }
14101425
1411- let fallback_mass_kg = 1.0 ;
1426+ let fallback_mass_kg = DYNAMIC_BODY_FALLBACK_MASS_KG ;
14121427 if !fallback_mass_kg. is_finite ( ) || fallback_mass_kg <= 0.0 {
14131428 return Err ( RigidBody2DBackendError :: InvalidMassKg {
14141429 mass_kg : fallback_mass_kg,
@@ -1418,6 +1433,55 @@ fn resolve_additional_mass_kg(
14181433 return Ok ( fallback_mass_kg) ;
14191434}
14201435
1436+ /// Resolves how attaching a collider affects a body's backend mass state.
1437+ ///
1438+ /// This helper encodes the public density semantics without directly mutating
1439+ /// Rapier state or backend slots.
1440+ ///
1441+ /// # Arguments
1442+ /// - `body_type`: The parent rigid body integration mode.
1443+ /// - `explicit_dynamic_mass_kg`: The explicitly configured dynamic mass, if
1444+ /// any.
1445+ /// - `has_positive_density_colliders`: Whether the body already has at least
1446+ /// one collider with `density > 0.0`.
1447+ /// - `requested_density`: The density requested for the new collider.
1448+ ///
1449+ /// # Returns
1450+ /// Returns a plan describing the Rapier density and any required backend state
1451+ /// transitions.
1452+ fn resolve_collider_attachment_mass_plan_2d (
1453+ body_type : RigidBodyType2D ,
1454+ explicit_dynamic_mass_kg : Option < f32 > ,
1455+ has_positive_density_colliders : bool ,
1456+ requested_density : f32 ,
1457+ ) -> ColliderAttachmentMassPlan2D {
1458+ if body_type != RigidBodyType2D :: Dynamic {
1459+ return ColliderAttachmentMassPlan2D {
1460+ rapier_density : requested_density,
1461+ should_mark_has_positive_density_colliders : false ,
1462+ should_remove_fallback_mass : false ,
1463+ } ;
1464+ }
1465+
1466+ if explicit_dynamic_mass_kg. is_some ( ) {
1467+ return ColliderAttachmentMassPlan2D {
1468+ rapier_density : 0.0 ,
1469+ should_mark_has_positive_density_colliders : false ,
1470+ should_remove_fallback_mass : false ,
1471+ } ;
1472+ }
1473+
1474+ let is_first_positive_density_collider =
1475+ requested_density > 0.0 && !has_positive_density_colliders;
1476+
1477+ return ColliderAttachmentMassPlan2D {
1478+ rapier_density : requested_density,
1479+ should_mark_has_positive_density_colliders :
1480+ is_first_positive_density_collider,
1481+ should_remove_fallback_mass : is_first_positive_density_collider,
1482+ } ;
1483+ }
1484+
14211485#[ cfg( test) ]
14221486mod tests {
14231487 use super :: * ;
@@ -1605,6 +1669,72 @@ mod tests {
16051669 return ;
16061670 }
16071671
1672+ /// Removes fallback mass only for the first positive-density collider.
1673+ #[ test]
1674+ fn collider_attachment_mass_plan_marks_first_positive_density_collider ( ) {
1675+ let plan = resolve_collider_attachment_mass_plan_2d (
1676+ RigidBodyType2D :: Dynamic ,
1677+ None ,
1678+ false ,
1679+ 1.0 ,
1680+ ) ;
1681+
1682+ assert_eq ! (
1683+ plan,
1684+ ColliderAttachmentMassPlan2D {
1685+ rapier_density: 1.0 ,
1686+ should_mark_has_positive_density_colliders: true ,
1687+ should_remove_fallback_mass: true ,
1688+ }
1689+ ) ;
1690+
1691+ return ;
1692+ }
1693+
1694+ /// Preserves density-driven mass state after the first positive collider.
1695+ #[ test]
1696+ fn collider_attachment_mass_plan_does_not_remove_fallback_mass_twice ( ) {
1697+ let plan = resolve_collider_attachment_mass_plan_2d (
1698+ RigidBodyType2D :: Dynamic ,
1699+ None ,
1700+ true ,
1701+ 1.0 ,
1702+ ) ;
1703+
1704+ assert_eq ! (
1705+ plan,
1706+ ColliderAttachmentMassPlan2D {
1707+ rapier_density: 1.0 ,
1708+ should_mark_has_positive_density_colliders: false ,
1709+ should_remove_fallback_mass: false ,
1710+ }
1711+ ) ;
1712+
1713+ return ;
1714+ }
1715+
1716+ /// Keeps explicit dynamic mass authoritative over collider density.
1717+ #[ test]
1718+ fn collider_attachment_mass_plan_ignores_density_for_explicit_mass ( ) {
1719+ let plan = resolve_collider_attachment_mass_plan_2d (
1720+ RigidBodyType2D :: Dynamic ,
1721+ Some ( 2.0 ) ,
1722+ false ,
1723+ 5.0 ,
1724+ ) ;
1725+
1726+ assert_eq ! (
1727+ plan,
1728+ ColliderAttachmentMassPlan2D {
1729+ rapier_density: 0.0 ,
1730+ should_mark_has_positive_density_colliders: false ,
1731+ should_remove_fallback_mass: false ,
1732+ }
1733+ ) ;
1734+
1735+ return ;
1736+ }
1737+
16081738 /// Verifies that applying an impulse updates velocity immediately.
16091739 #[ test]
16101740 fn impulse_updates_velocity_immediately ( ) {
0 commit comments