Skip to content

Commit 4d15791

Browse files
committed
[update] mass policy handling to be it's own data structure and simplify collider attachment
1 parent 0ab7198 commit 4d15791

File tree

1 file changed

+157
-27
lines changed

1 file changed

+157
-27
lines changed

crates/lambda-rs-platform/src/physics/rapier2d.rs

Lines changed: 157 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ impl fmt::Display for Collider2DBackendError {
103103

104104
impl 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)]
14221486
mod 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

Comments
 (0)