Skip to content

Commit a616a9e

Browse files
committed
[add] physics point and aabb queries
1 parent 3045a69 commit a616a9e

5 files changed

Lines changed: 374 additions & 4 deletions

File tree

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ struct RigidBodySlot2D {
150150
struct ColliderSlot2D {
151151
/// The handle to the Rapier collider stored in the `ColliderSet`.
152152
rapier_handle: ColliderHandle,
153+
/// The parent rigid body slot index that owns this collider.
154+
parent_slot_index: u32,
155+
/// The parent rigid body slot generation that owns this collider.
156+
parent_slot_generation: u32,
153157
/// A monotonically increasing counter used to validate stale handles.
154158
generation: u32,
155159
}
@@ -620,6 +624,85 @@ impl PhysicsBackend2D {
620624
.is_ok();
621625
}
622626

627+
/// Returns all rigid bodies whose colliders contain the provided point.
628+
///
629+
/// # Arguments
630+
/// - `point`: The world-space point to test.
631+
///
632+
/// # Returns
633+
/// Returns backend rigid body slot pairs for each matching collider.
634+
pub fn query_point_2d(&self, point: [f32; 2]) -> Vec<(u32, u32)> {
635+
if validate_position(point[0], point[1]).is_err() {
636+
return Vec::new();
637+
}
638+
639+
let point = Vector::new(point[0], point[1]);
640+
let mut body_slots = Vec::new();
641+
642+
for (collider_handle, collider) in self.colliders.iter() {
643+
if !collider.shape().contains_point(collider.position(), point) {
644+
continue;
645+
}
646+
647+
let Some(body_slot) =
648+
self.query_hit_to_parent_body_slot_2d(collider_handle)
649+
else {
650+
continue;
651+
};
652+
653+
body_slots.push(body_slot);
654+
}
655+
656+
return body_slots;
657+
}
658+
659+
/// Returns all rigid bodies whose colliders overlap the provided AABB.
660+
///
661+
/// # Arguments
662+
/// - `min`: The minimum world-space corner of the query box.
663+
/// - `max`: The maximum world-space corner of the query box.
664+
///
665+
/// # Returns
666+
/// Returns backend rigid body slot pairs for each matching collider.
667+
pub fn query_aabb_2d(&self, min: [f32; 2], max: [f32; 2]) -> Vec<(u32, u32)> {
668+
if validate_position(min[0], min[1]).is_err()
669+
|| validate_position(max[0], max[1]).is_err()
670+
{
671+
return Vec::new();
672+
}
673+
674+
let half_extents =
675+
Vector::new((max[0] - min[0]) * 0.5, (max[1] - min[1]) * 0.5);
676+
let center = Vector::new((min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5);
677+
let query_shape = Cuboid::new(half_extents);
678+
let query_pose = Pose::from_translation(center);
679+
let query_dispatcher = self.narrow_phase.query_dispatcher();
680+
let mut body_slots = Vec::new();
681+
682+
for (collider_handle, collider) in self.colliders.iter() {
683+
let shape_to_collider = query_pose.inv_mul(collider.position());
684+
let intersects = query_dispatcher.intersection_test(
685+
&shape_to_collider,
686+
&query_shape,
687+
collider.shape(),
688+
);
689+
690+
if intersects != Ok(true) {
691+
continue;
692+
}
693+
694+
let Some(body_slot) =
695+
self.query_hit_to_parent_body_slot_2d(collider_handle)
696+
else {
697+
continue;
698+
};
699+
700+
body_slots.push(body_slot);
701+
}
702+
703+
return body_slots;
704+
}
705+
623706
/// Sets the current position for the referenced body.
624707
///
625708
/// # Arguments
@@ -1101,6 +1184,8 @@ impl PhysicsBackend2D {
11011184
let slot_generation = 1;
11021185
self.collider_slots_2d.push(ColliderSlot2D {
11031186
rapier_handle,
1187+
parent_slot_index,
1188+
parent_slot_generation,
11041189
generation: slot_generation,
11051190
});
11061191

@@ -1222,10 +1307,41 @@ impl PhysicsBackend2D {
12221307
self.colliders.get(slot.rapier_handle).is_some(),
12231308
"collider slot references missing Rapier collider"
12241309
);
1310+
debug_assert!(
1311+
self
1312+
.rigid_body_slot_2d(
1313+
slot.parent_slot_index,
1314+
slot.parent_slot_generation
1315+
)
1316+
.is_ok(),
1317+
"collider slot references missing parent rigid body slot"
1318+
);
12251319
}
12261320

12271321
return;
12281322
}
1323+
/// Resolves a query hit collider back to its owning rigid body slot.
1324+
///
1325+
/// # Arguments
1326+
/// - `collider_handle`: The Rapier collider handle returned by a query.
1327+
///
1328+
/// # Returns
1329+
/// Returns the owning backend rigid body slot pair when the collider slot is
1330+
/// still tracked by the backend.
1331+
fn query_hit_to_parent_body_slot_2d(
1332+
&self,
1333+
collider_handle: ColliderHandle,
1334+
) -> Option<(u32, u32)> {
1335+
let collider_slot = self
1336+
.collider_slots_2d
1337+
.iter()
1338+
.find(|slot| slot.rapier_handle == collider_handle)?;
1339+
1340+
return Some((
1341+
collider_slot.parent_slot_index,
1342+
collider_slot.parent_slot_generation,
1343+
));
1344+
}
12291345
}
12301346

12311347
/// Builds a Rapier rigid body builder with `lambda-rs` invariants applied.

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

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! API is backend-agnostic and does not expose vendor types.
55
66
use std::{
7+
collections::HashSet,
78
error::Error,
89
fmt,
910
sync::atomic::{
@@ -157,8 +158,13 @@ impl PhysicsWorld2D {
157158
///
158159
/// # Returns
159160
/// Returns a vector of matching rigid body handles.
160-
pub fn query_point(&self, _point: [f32; 2]) -> Vec<RigidBody2D> {
161-
return Vec::new();
161+
pub fn query_point(&self, point: [f32; 2]) -> Vec<RigidBody2D> {
162+
if !is_valid_query_point(point) {
163+
return Vec::new();
164+
}
165+
166+
let body_slots = self.backend.query_point_2d(point);
167+
return self.deduplicate_query_body_hits(body_slots);
162168
}
163169

164170
/// Returns all bodies whose colliders overlap the provided axis-aligned box.
@@ -169,8 +175,16 @@ impl PhysicsWorld2D {
169175
///
170176
/// # Returns
171177
/// 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();
178+
pub fn query_aabb(&self, min: [f32; 2], max: [f32; 2]) -> Vec<RigidBody2D> {
179+
if !is_valid_query_point(min) || !is_valid_query_point(max) {
180+
return Vec::new();
181+
}
182+
183+
let normalized_min = [min[0].min(max[0]), min[1].min(max[1])];
184+
let normalized_max = [min[0].max(max[0]), min[1].max(max[1])];
185+
let body_slots = self.backend.query_aabb_2d(normalized_min, normalized_max);
186+
187+
return self.deduplicate_query_body_hits(body_slots);
174188
}
175189

176190
/// Returns the nearest rigid body hit by the provided ray.
@@ -190,6 +204,36 @@ impl PhysicsWorld2D {
190204
) -> Option<RaycastHit> {
191205
return None;
192206
}
207+
208+
/// Rebuilds and deduplicates rigid body handles from backend query hits.
209+
///
210+
/// # Arguments
211+
/// - `body_slots`: Backend `(slot_index, slot_generation)` pairs.
212+
///
213+
/// # Returns
214+
/// Returns one `RigidBody2D` handle per unique body, preserving first-hit
215+
/// order from the backend query.
216+
fn deduplicate_query_body_hits(
217+
&self,
218+
body_slots: Vec<(u32, u32)>,
219+
) -> Vec<RigidBody2D> {
220+
let mut seen_bodies = HashSet::new();
221+
let mut bodies = Vec::new();
222+
223+
for (slot_index, slot_generation) in body_slots {
224+
let body = RigidBody2D::from_backend_slot(
225+
self.world_id,
226+
slot_index,
227+
slot_generation,
228+
);
229+
230+
if seen_bodies.insert(body) {
231+
bodies.push(body);
232+
}
233+
}
234+
235+
return bodies;
236+
}
193237
}
194238

195239
/// Builder for `PhysicsWorld2D`.
@@ -354,6 +398,17 @@ fn validate_gravity(gravity: [f32; 2]) -> Result<(), PhysicsWorld2DError> {
354398
return Ok(());
355399
}
356400

401+
/// Returns whether a query point contains only finite coordinates.
402+
///
403+
/// # Arguments
404+
/// - `point`: The point to validate.
405+
///
406+
/// # Returns
407+
/// Returns `true` when both coordinates are finite.
408+
fn is_valid_query_point(point: [f32; 2]) -> bool {
409+
return point[0].is_finite() && point[1].is_finite();
410+
}
411+
357412
/// Allocates a non-zero unique world identifier.
358413
fn allocate_world_id() -> u64 {
359414
loop {

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,30 @@ pub struct RigidBody2D {
4444
}
4545

4646
impl RigidBody2D {
47+
/// Creates a world-scoped rigid body handle from backend slot identifiers.
48+
///
49+
/// This helper is an internal implementation detail used by query and event
50+
/// APIs that reconstruct public handles from backend state.
51+
///
52+
/// # Arguments
53+
/// - `world_id`: The owning physics world identifier.
54+
/// - `slot_index`: The backend body slot index.
55+
/// - `slot_generation`: The backend body slot generation counter.
56+
///
57+
/// # Returns
58+
/// Returns a `RigidBody2D` handle bound to the provided world.
59+
pub(super) fn from_backend_slot(
60+
world_id: u64,
61+
slot_index: u32,
62+
slot_generation: u32,
63+
) -> Self {
64+
return Self {
65+
world_id,
66+
slot_index,
67+
slot_generation,
68+
};
69+
}
70+
4771
/// Returns the backend slot identifiers for this handle.
4872
///
4973
/// This function is an internal implementation detail used by other

crates/lambda-rs/tests/physics_2d/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod colliders;
77
mod collision_filters;
88
mod compound_colliders;
99
mod materials;
10+
mod queries;
1011

1112
use lambda::physics::{
1213
PhysicsWorld2DBuilder,

0 commit comments

Comments
 (0)