Skip to content

Commit 7ffecad

Browse files
committed
[add] physics collision query and event API scaffolding
1 parent 77a6bb7 commit 7ffecad

2 files changed

Lines changed: 172 additions & 0 deletions

File tree

crates/lambda-rs/src/physics/collider_2d.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::{
1313
use lambda_platform::physics::Collider2DBackendError;
1414

1515
use super::{
16+
CollisionFilter,
1617
PhysicsWorld2D,
1718
RigidBody2D,
1819
RigidBody2DError,
@@ -124,6 +125,7 @@ pub struct Collider2DBuilder {
124125
local_offset: [f32; 2],
125126
local_rotation: f32,
126127
material: ColliderMaterial2D,
128+
collision_filter: CollisionFilter,
127129
}
128130

129131
impl Collider2DBuilder {
@@ -234,6 +236,18 @@ impl Collider2DBuilder {
234236
return self;
235237
}
236238

239+
/// Sets the collision filtering configuration for the collider.
240+
///
241+
/// # Arguments
242+
/// - `filter`: The collision filter to assign to the collider.
243+
///
244+
/// # Returns
245+
/// Returns the updated builder.
246+
pub fn with_collision_filter(mut self, filter: CollisionFilter) -> Self {
247+
self.collision_filter = filter;
248+
return self;
249+
}
250+
237251
/// Sets the collider density, in kg/m².
238252
///
239253
/// When attaching a collider with `density > 0.0` to a dynamic body that did
@@ -396,6 +410,7 @@ impl Collider2DBuilder {
396410
local_offset: [DEFAULT_LOCAL_OFFSET_X, DEFAULT_LOCAL_OFFSET_Y],
397411
local_rotation: DEFAULT_LOCAL_ROTATION_RADIANS,
398412
material: ColliderMaterial2D::default(),
413+
collision_filter: CollisionFilter::default(),
399414
};
400415
}
401416
}
@@ -954,4 +969,29 @@ mod tests {
954969

955970
return;
956971
}
972+
973+
/// Uses a filter that collides with all groups unless overridden.
974+
#[test]
975+
fn builder_defaults_collision_filter_to_all_groups() {
976+
let builder = Collider2DBuilder::circle(1.0);
977+
978+
assert_eq!(builder.collision_filter, CollisionFilter::default());
979+
980+
return;
981+
}
982+
983+
/// Stores a caller-provided collision filter on the builder.
984+
#[test]
985+
fn builder_overrides_collision_filter() {
986+
let filter = CollisionFilter {
987+
group: 0b0001,
988+
mask: 0b0110,
989+
};
990+
991+
let builder = Collider2DBuilder::circle(1.0).with_collision_filter(filter);
992+
993+
assert_eq!(builder.collision_filter, filter);
994+
995+
return;
996+
}
957997
}

crates/lambda-rs/src/physics/mod.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,72 @@ pub use rigid_body_2d::{
3232
RigidBodyType,
3333
};
3434

35+
const DEFAULT_COLLISION_FILTER_GROUP: u32 = u32::MAX;
36+
const DEFAULT_COLLISION_FILTER_MASK: u32 = u32::MAX;
3537
const DEFAULT_GRAVITY_X: f32 = 0.0;
3638
const DEFAULT_GRAVITY_Y: f32 = -9.81;
3739
const DEFAULT_TIMESTEP_SECONDS: f32 = 1.0 / 60.0;
3840
const DEFAULT_SUBSTEPS: u32 = 1;
3941

4042
static NEXT_WORLD_ID: AtomicU64 = AtomicU64::new(1);
4143

44+
/// Indicates whether a collision pair started or ended contact this step.
45+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46+
pub enum CollisionEventKind {
47+
/// The two bodies started touching during the most recent simulation step.
48+
Started,
49+
/// The two bodies stopped touching during the most recent simulation step.
50+
Ended,
51+
}
52+
53+
/// Describes a body pair collision observed during simulation stepping.
54+
#[derive(Debug, Clone, Copy, PartialEq)]
55+
pub struct CollisionEvent {
56+
/// The event transition kind for this body pair.
57+
pub kind: CollisionEventKind,
58+
/// The first body participating in the collision pair.
59+
pub body_a: RigidBody2D,
60+
/// The second body participating in the collision pair.
61+
pub body_b: RigidBody2D,
62+
/// The representative contact point, when available.
63+
pub contact_point: Option<[f32; 2]>,
64+
/// The representative contact normal, when available.
65+
pub normal: Option<[f32; 2]>,
66+
/// The representative penetration depth, when available.
67+
pub penetration: Option<f32>,
68+
}
69+
70+
/// Configures collider collision group and mask bitfields.
71+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72+
pub struct CollisionFilter {
73+
/// The membership bitfield for this collider.
74+
pub group: u32,
75+
/// The bitfield describing which groups this collider can collide with.
76+
pub mask: u32,
77+
}
78+
79+
impl Default for CollisionFilter {
80+
fn default() -> Self {
81+
return Self {
82+
group: DEFAULT_COLLISION_FILTER_GROUP,
83+
mask: DEFAULT_COLLISION_FILTER_MASK,
84+
};
85+
}
86+
}
87+
88+
/// Describes the nearest body hit by a ray query.
89+
#[derive(Debug, Clone, Copy, PartialEq)]
90+
pub struct RaycastHit {
91+
/// The rigid body hit by the ray.
92+
pub body: RigidBody2D,
93+
/// The world-space hit position.
94+
pub point: [f32; 2],
95+
/// The world-space hit normal.
96+
pub normal: [f32; 2],
97+
/// The non-negative distance from the origin to the hit point.
98+
pub distance: f32,
99+
}
100+
42101
/// A 2D physics simulation world.
43102
pub struct PhysicsWorld2D {
44103
world_id: u64,
@@ -82,6 +141,55 @@ impl PhysicsWorld2D {
82141
pub fn timestep_seconds(&self) -> f32 {
83142
return self.timestep_seconds;
84143
}
144+
145+
/// Returns collision events collected during the most recent step.
146+
///
147+
/// # Returns
148+
/// Returns an iterator over collision events emitted by the world.
149+
pub fn collision_events(&self) -> impl Iterator<Item = CollisionEvent> {
150+
return std::iter::empty();
151+
}
152+
153+
/// Returns all bodies whose colliders contain the provided point.
154+
///
155+
/// # Arguments
156+
/// - `point`: The world-space point to test.
157+
///
158+
/// # Returns
159+
/// Returns a vector of matching rigid body handles.
160+
pub fn query_point(&self, _point: [f32; 2]) -> Vec<RigidBody2D> {
161+
return Vec::new();
162+
}
163+
164+
/// Returns all bodies whose colliders overlap the provided axis-aligned box.
165+
///
166+
/// # Arguments
167+
/// - `min`: The minimum world-space corner of the query box.
168+
/// - `max`: The maximum world-space corner of the query box.
169+
///
170+
/// # Returns
171+
/// Returns a vector of matching rigid body handles.
172+
pub fn query_aabb(&self, _min: [f32; 2], _max: [f32; 2]) -> Vec<RigidBody2D> {
173+
return Vec::new();
174+
}
175+
176+
/// Returns the nearest rigid body hit by the provided ray.
177+
///
178+
/// # Arguments
179+
/// - `origin`: The ray origin in world space.
180+
/// - `dir`: The ray direction in world space.
181+
/// - `max_dist`: The maximum query distance.
182+
///
183+
/// # Returns
184+
/// Returns the nearest hit, if one exists.
185+
pub fn raycast(
186+
&self,
187+
_origin: [f32; 2],
188+
_dir: [f32; 2],
189+
_max_dist: f32,
190+
) -> Option<RaycastHit> {
191+
return None;
192+
}
85193
}
86194

87195
/// Builder for `PhysicsWorld2D`.
@@ -404,4 +512,28 @@ mod tests {
404512

405513
return;
406514
}
515+
516+
/// Exposes stable defaults for collider collision filtering.
517+
#[test]
518+
fn collision_filter_default_collides_with_all_groups() {
519+
let filter = CollisionFilter::default();
520+
521+
assert_eq!(filter.group, u32::MAX);
522+
assert_eq!(filter.mask, u32::MAX);
523+
524+
return;
525+
}
526+
527+
/// Returns empty query and event results for an empty world.
528+
#[test]
529+
fn empty_world_collision_queries_return_no_results() {
530+
let world = PhysicsWorld2DBuilder::new().build().unwrap();
531+
532+
assert_eq!(world.collision_events().count(), 0);
533+
assert!(world.query_point([0.0, 0.0]).is_empty());
534+
assert!(world.query_aabb([-1.0, -1.0], [1.0, 1.0]).is_empty());
535+
assert_eq!(world.raycast([0.0, 0.0], [1.0, 0.0], 10.0), None);
536+
537+
return;
538+
}
407539
}

0 commit comments

Comments
 (0)