Skip to content

Commit 4313b4b

Browse files
authored
refactor(lambda-rs-platform): split rapier2d implementation across multiple (#202)
## Summary Refactor the internal `lambda-rs-platform` 2D Rapier backend from a single large source file into a `rapier2d` module. This keeps the existing backend behavior and public exports unchanged while making the code easier to navigate and maintain. ## Related Issues N/A ## Changes - Replace `crates/lambda-rs-platform/src/physics/rapier2d.rs` with a folder-backed module at `crates/lambda-rs-platform/src/physics/rapier2d/`. - Keep the existing `physics::rapier2d` module path and `PhysicsBackend2D` exports stable through `rapier2d/mod.rs`. - Split the backend implementation by responsibility: - `rigid_bodies.rs` - `colliders.rs` - `queries.rs` - `simulation.rs` - `helpers.rs` - `tests.rs` - Move the existing unit tests into `tests.rs` without changing their assertions or backend semantics. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] Feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation (updates to docs, specs, tutorials, or comments) - [x] Refactor (code change that neither fixes a bug nor adds a feature) - [ ] Performance (change that improves performance) - [ ] Test (adding or updating tests) - [ ] Build/CI (changes to build process or CI configuration) ## Affected Crates - [ ] `lambda-rs` - [x] `lambda-rs-platform` - [ ] `lambda-rs-args` - [ ] `lambda-rs-logging` - [ ] Other: ## Checklist - [x] Code follows the repository style guidelines (`cargo +nightly fmt --all`) - [ ] Code passes clippy (`cargo clippy --workspace --all-targets -- -D warnings`) - [ ] Tests pass (`cargo test --workspace`) - [x] New code includes appropriate documentation - [ ] Public API changes are documented - [ ] Breaking changes are noted in this PR description ## Testing **Commands run:** ```bash cargo +nightly fmt --all cargo test -p lambda-rs-platform rapier2d -- --nocapture cargo test -p lambda-rs-platform --features physics-2d --lib physics::rapier2d -- --nocapture ``` **Manual verification steps (if applicable):** ## Screenshots/Recordings N/A ## Platform Testing - [x] macOS - [ ] Windows - [ ] Linux ## Additional Notes
2 parents e8508bb + 3e1358e commit 4313b4b

File tree

8 files changed

+2509
-2435
lines changed

8 files changed

+2509
-2435
lines changed

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

Lines changed: 0 additions & 2435 deletions
This file was deleted.

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

Lines changed: 490 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 486 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
//! Rapier-backed 2D physics backend.
2+
//!
3+
//! This module provides a minimal wrapper around `rapier2d` to support the
4+
//! higher-level `lambda-rs` physics APIs without exposing vendor types outside
5+
//! of the platform layer.
6+
7+
use std::{
8+
collections::{
9+
HashMap,
10+
HashSet,
11+
},
12+
error::Error,
13+
fmt,
14+
};
15+
16+
use rapier2d::prelude::*;
17+
18+
mod colliders;
19+
mod helpers;
20+
mod queries;
21+
mod rigid_bodies;
22+
mod simulation;
23+
24+
#[cfg(test)]
25+
mod tests;
26+
27+
/// The rigid body integration mode.
28+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29+
pub enum RigidBodyType2D {
30+
/// A body that does not move under simulation.
31+
Static,
32+
/// A body affected by gravity and forces.
33+
Dynamic,
34+
/// A body integrated only by user-provided motion.
35+
Kinematic,
36+
}
37+
38+
/// Backend errors for 2D rigid body operations.
39+
#[derive(Debug, Clone, Copy, PartialEq)]
40+
pub enum RigidBody2DBackendError {
41+
/// The referenced rigid body was not found.
42+
BodyNotFound,
43+
/// The provided position is invalid.
44+
InvalidPosition { x: f32, y: f32 },
45+
/// The provided rotation is invalid.
46+
InvalidRotation { radians: f32 },
47+
/// The provided linear velocity is invalid.
48+
InvalidVelocity { x: f32, y: f32 },
49+
/// The provided force is invalid.
50+
InvalidForce { x: f32, y: f32 },
51+
/// The provided impulse is invalid.
52+
InvalidImpulse { x: f32, y: f32 },
53+
/// The provided dynamic mass is invalid.
54+
InvalidMassKg { mass_kg: f32 },
55+
/// The requested operation is unsupported for the body type.
56+
UnsupportedOperation { body_type: RigidBodyType2D },
57+
}
58+
59+
impl fmt::Display for RigidBody2DBackendError {
60+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61+
match self {
62+
Self::BodyNotFound => {
63+
return write!(formatter, "rigid body not found");
64+
}
65+
Self::InvalidPosition { x, y } => {
66+
return write!(formatter, "invalid position: ({x}, {y})");
67+
}
68+
Self::InvalidRotation { radians } => {
69+
return write!(formatter, "invalid rotation: {radians}");
70+
}
71+
Self::InvalidVelocity { x, y } => {
72+
return write!(formatter, "invalid velocity: ({x}, {y})");
73+
}
74+
Self::InvalidForce { x, y } => {
75+
return write!(formatter, "invalid force: ({x}, {y})");
76+
}
77+
Self::InvalidImpulse { x, y } => {
78+
return write!(formatter, "invalid impulse: ({x}, {y})");
79+
}
80+
Self::InvalidMassKg { mass_kg } => {
81+
return write!(formatter, "invalid mass_kg: {mass_kg}");
82+
}
83+
Self::UnsupportedOperation { body_type } => {
84+
return write!(
85+
formatter,
86+
"unsupported operation for body_type: {body_type:?}"
87+
);
88+
}
89+
}
90+
}
91+
}
92+
93+
impl Error for RigidBody2DBackendError {}
94+
95+
/// Backend errors for 2D collider operations.
96+
#[derive(Debug, Clone, Copy, PartialEq)]
97+
pub enum Collider2DBackendError {
98+
/// The referenced rigid body was not found.
99+
BodyNotFound,
100+
/// The provided polygon could not be represented as a convex hull.
101+
InvalidPolygonDegenerate,
102+
}
103+
104+
impl fmt::Display for Collider2DBackendError {
105+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106+
match self {
107+
Self::BodyNotFound => {
108+
return write!(formatter, "rigid body not found");
109+
}
110+
Self::InvalidPolygonDegenerate => {
111+
return write!(formatter, "invalid polygon: degenerate");
112+
}
113+
}
114+
}
115+
}
116+
117+
impl Error for Collider2DBackendError {}
118+
119+
/// Backend-agnostic data describing the nearest 2D raycast hit.
120+
#[derive(Debug, Clone, Copy, PartialEq)]
121+
pub struct RaycastHit2DBackend {
122+
/// The hit rigid body's slot index.
123+
pub body_slot_index: u32,
124+
/// The hit rigid body's slot generation.
125+
pub body_slot_generation: u32,
126+
/// The world-space hit point.
127+
pub point: [f32; 2],
128+
/// The world-space unit hit normal.
129+
pub normal: [f32; 2],
130+
/// The non-negative hit distance in meters.
131+
pub distance: f32,
132+
}
133+
134+
/// Indicates whether a backend collision pair started or ended contact.
135+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136+
pub enum CollisionEventKind2DBackend {
137+
/// The body pair started touching during the current backend step.
138+
Started,
139+
/// The body pair stopped touching during the current backend step.
140+
Ended,
141+
}
142+
143+
/// Backend-agnostic data describing one 2D collision event.
144+
#[derive(Debug, Clone, Copy, PartialEq)]
145+
pub struct CollisionEvent2DBackend {
146+
/// The transition kind for the body pair.
147+
pub kind: CollisionEventKind2DBackend,
148+
/// The first rigid body's slot index.
149+
pub body_a_slot_index: u32,
150+
/// The first rigid body's slot generation.
151+
pub body_a_slot_generation: u32,
152+
/// The second rigid body's slot index.
153+
pub body_b_slot_index: u32,
154+
/// The second rigid body's slot generation.
155+
pub body_b_slot_generation: u32,
156+
/// The representative world-space contact point, when available.
157+
pub contact_point: Option<[f32; 2]>,
158+
/// The representative world-space contact normal, when available.
159+
pub normal: Option<[f32; 2]>,
160+
/// The representative penetration depth, when available.
161+
pub penetration: Option<f32>,
162+
}
163+
164+
/// The fallback mass applied to dynamic bodies before density colliders exist.
165+
const DYNAMIC_BODY_FALLBACK_MASS_KG: f32 = 1.0;
166+
167+
/// Stores per-body state that `lambda-rs` tracks alongside Rapier.
168+
///
169+
/// This slot exists because `lambda-rs` defines integration semantics that are
170+
/// stricter than the vendor backend:
171+
/// - Forces are accumulated and cleared explicitly by the public API.
172+
/// - Impulses update velocity immediately.
173+
///
174+
/// # Invariants
175+
/// - `rapier_handle` MUST reference a body in `PhysicsBackend2D::bodies`.
176+
/// - `explicit_dynamic_mass_kg` MUST be `Some` only for dynamic bodies.
177+
/// - `generation` MUST be non-zero and is used to validate handles.
178+
#[derive(Debug, Clone, Copy)]
179+
struct RigidBodySlot2D {
180+
/// The rigid body's integration mode.
181+
body_type: RigidBodyType2D,
182+
/// The handle to the Rapier rigid body stored in the `RigidBodySet`.
183+
rapier_handle: RigidBodyHandle,
184+
/// Accumulated forces applied by the public API, in Newtons.
185+
force_accumulator: [f32; 2],
186+
/// The explicitly configured body mass in kilograms, if set.
187+
///
188+
/// When this value is `Some`, collider density MUST NOT affect body mass
189+
/// properties. The backend enforces this by creating attached colliders with
190+
/// zero density and using the configured value as the body's additional mass.
191+
explicit_dynamic_mass_kg: Option<f32>,
192+
/// Tracks whether the body has at least one positive-density collider.
193+
///
194+
/// This flag supports the spec requirement that bodies with no positive
195+
/// density colliders default to `1.0` kg, while bodies with at least one
196+
/// positive-density collider compute mass from collider density alone.
197+
has_positive_density_colliders: bool,
198+
/// A monotonically increasing counter used to validate stale handles.
199+
generation: u32,
200+
}
201+
202+
/// Stores per-collider state that `lambda-rs` tracks alongside Rapier.
203+
///
204+
/// # Invariants
205+
/// - `rapier_handle` MUST reference a collider in `PhysicsBackend2D::colliders`.
206+
/// - `generation` MUST be non-zero and is used to validate stale handles.
207+
#[derive(Debug, Clone, Copy)]
208+
struct ColliderSlot2D {
209+
/// The handle to the Rapier collider stored in the `ColliderSet`.
210+
rapier_handle: ColliderHandle,
211+
/// The parent rigid body slot index that owns this collider.
212+
parent_slot_index: u32,
213+
/// The parent rigid body slot generation that owns this collider.
214+
parent_slot_generation: u32,
215+
/// A monotonically increasing counter used to validate stale handles.
216+
generation: u32,
217+
}
218+
219+
/// Describes how collider attachment should affect dynamic-body mass semantics.
220+
///
221+
/// This helper isolates `lambda-rs` mass rules from the Rapier attachment flow
222+
/// so body creation and collider attachment share one backend policy source.
223+
#[derive(Debug, Clone, Copy, PartialEq)]
224+
struct ColliderAttachmentMassPlan2D {
225+
/// The density value that MUST be passed to the Rapier collider builder.
226+
rapier_density: f32,
227+
/// Whether attaching this collider transitions the body to density-driven
228+
/// mass computation.
229+
should_mark_has_positive_density_colliders: bool,
230+
/// Whether the initial fallback mass MUST be removed before insertion.
231+
should_remove_fallback_mass: bool,
232+
}
233+
234+
/// A normalized body-pair key used for backend collision tracking.
235+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
236+
struct BodyPairKey2D {
237+
/// The first body slot index.
238+
body_a_slot_index: u32,
239+
/// The first body slot generation.
240+
body_a_slot_generation: u32,
241+
/// The second body slot index.
242+
body_b_slot_index: u32,
243+
/// The second body slot generation.
244+
body_b_slot_generation: u32,
245+
}
246+
247+
/// The representative contact selected for a body pair during one step.
248+
#[derive(Debug, Clone, Copy, PartialEq)]
249+
struct BodyPairContact2D {
250+
/// The representative world-space contact point.
251+
point: [f32; 2],
252+
/// The representative world-space normal from body A toward body B.
253+
normal: [f32; 2],
254+
/// The non-negative penetration depth.
255+
penetration: f32,
256+
}
257+
258+
/// A 2D physics backend powered by `rapier2d`.
259+
///
260+
/// This type is an internal implementation detail used by `lambda-rs`.
261+
pub struct PhysicsBackend2D {
262+
gravity: Vector,
263+
integration_parameters: IntegrationParameters,
264+
islands: IslandManager,
265+
broad_phase: BroadPhaseBvh,
266+
narrow_phase: NarrowPhase,
267+
bodies: RigidBodySet,
268+
colliders: ColliderSet,
269+
impulse_joints: ImpulseJointSet,
270+
multibody_joints: MultibodyJointSet,
271+
ccd_solver: CCDSolver,
272+
pipeline: PhysicsPipeline,
273+
rigid_body_slots_2d: Vec<RigidBodySlot2D>,
274+
collider_slots_2d: Vec<ColliderSlot2D>,
275+
collider_parent_slots_2d: HashMap<ColliderHandle, (u32, u32)>,
276+
active_body_pairs_2d: HashSet<BodyPairKey2D>,
277+
active_body_pair_order_2d: Vec<BodyPairKey2D>,
278+
queued_collision_events_2d: Vec<CollisionEvent2DBackend>,
279+
}

0 commit comments

Comments
 (0)