1use std::collections::BTreeMap;
7use std::collections::HashMap;
8
9use nalgebra::SVector;
10use symtropy_math::Point;
11
12use crate::body::{BodyHandle, NetId, RigidBody};
13use crate::broadphase;
14use crate::contact::{CollisionEvent, ContactCache, ContactManifold};
15use crate::constraint::Constraint;
16use crate::gjk;
17use crate::integrator;
18use crate::manifold_gen;
19
20#[cfg(feature = "deterministic-net")]
25#[inline]
26fn quantize_q16_16(v: f64) -> f64 {
27 (v * 65536.0).round() / 65536.0
28}
29
30pub trait PhysicsCallback<const D: usize> {
36 fn modulate_force(&self, body: BodyHandle, force: &SVector<f64, D>) -> SVector<f64, D>;
39
40 fn modulate_impulse(&self, impulse: f64, contact_point: &SVector<f64, D>) -> f64;
43
44 fn friction_multiplier(&self, contact_point: &SVector<f64, D>, body: BodyHandle) -> f64;
47
48 fn on_collision(&mut self, event: &CollisionEvent<D>);
50
51 fn record_dissipation(&mut self, energy: f64);
53}
54
55pub struct NoOpCallback;
57
58impl<const D: usize> PhysicsCallback<D> for NoOpCallback {
59 fn modulate_force(&self, _: BodyHandle, force: &SVector<f64, D>) -> SVector<f64, D> { *force }
60 fn modulate_impulse(&self, impulse: f64, _: &SVector<f64, D>) -> f64 { impulse }
61 fn friction_multiplier(&self, _: &SVector<f64, D>, _: BodyHandle) -> f64 { 1.0 }
62 fn on_collision(&mut self, _: &CollisionEvent<D>) {}
63 fn record_dissipation(&mut self, _: f64) {}
64}
65
66pub struct PhysicsWorld<const D: usize> {
68 pub bodies: Vec<RigidBody<D>>,
69 pub constraints: Vec<Box<dyn Constraint<D>>>,
70 pub gravity: SVector<f64, D>,
71 pub contacts: Vec<ContactManifold<D>>,
73 pub collision_events: Vec<CollisionEvent<D>>,
75 pub sensor_events: Vec<crate::contact::SensorEvent>,
77 contact_cache: ContactCache<D>,
79 prev_cache: ContactCache<D>,
81 pub solver_iterations: usize,
83 pub sleep_threshold: f64,
85 pub sleep_ticks: u32,
87 pub slop: f64,
89 pub baumgarte: f64,
94 pub compliance: f64,
97 net_id_map: BTreeMap<NetId, BodyHandle>,
99 handle_to_index: HashMap<BodyHandle, usize>,
101 next_handle: usize,
102 static_broadphase: broadphase::StaticBroadphase<D>,
104 static_tree_dirty: bool,
106}
107
108impl<const D: usize> PhysicsWorld<D> {
109 pub fn new(gravity: SVector<f64, D>) -> Self {
111 Self {
112 bodies: Vec::new(),
113 constraints: Vec::new(),
114 gravity,
115 contacts: Vec::new(),
116 collision_events: Vec::new(),
117 sensor_events: Vec::new(),
118 contact_cache: ContactCache::new(),
119 prev_cache: ContactCache::new(),
120 solver_iterations: 4,
121 slop: 0.01,
122 baumgarte: 0.2,
123 compliance: 0.0,
124 sleep_threshold: 0.5,
125 sleep_ticks: 60, net_id_map: BTreeMap::new(),
127 handle_to_index: HashMap::new(),
128 next_handle: 0,
129 static_broadphase: broadphase::StaticBroadphase::new(),
130 static_tree_dirty: false,
131 }
132 }
133
134 pub fn add_sphere(
136 &mut self,
137 position: Point<D>,
138 radius: f64,
139 mass: f64,
140 ) -> BodyHandle {
141 let handle = self.allocate_handle();
142 let body = RigidBody::dynamic_sphere(handle, position, radius, mass);
143 let idx = self.bodies.len();
144 self.bodies.push(body);
145 self.handle_to_index.insert(handle, idx);
146 handle
147 }
148
149 pub fn add_body(&mut self, mut body: RigidBody<D>) -> BodyHandle {
151 use crate::body::BodyType;
152 let handle = self.allocate_handle();
153 body.handle = handle;
154 if body.body_type == BodyType::Static || body.body_type == BodyType::Kinematic {
155 self.static_tree_dirty = true;
156 }
157 let idx = self.bodies.len();
158 self.bodies.push(body);
159 self.handle_to_index.insert(handle, idx);
160 handle
161 }
162
163 pub fn add_bodies_deterministic(
169 &mut self,
170 mut bodies: Vec<(NetId, RigidBody<D>)>,
171 ) -> Result<Vec<BodyHandle>, String> {
172 bodies.sort_by_key(|(id, _)| *id);
173 let mut handles = Vec::with_capacity(bodies.len());
174 for (net_id, mut body) in bodies {
175 if self.net_id_map.contains_key(&net_id) {
176 return Err(format!("duplicate NetId({})", net_id.0));
177 }
178 let handle = self.allocate_handle();
179 body.handle = handle;
180 body.net_id = Some(net_id);
181 self.net_id_map.insert(net_id, handle);
182 let idx = self.bodies.len();
183 self.bodies.push(body);
184 self.handle_to_index.insert(handle, idx);
185 handles.push(handle);
186 }
187 Ok(handles)
188 }
189
190 pub fn handle_for_net_id(&self, net_id: NetId) -> Option<BodyHandle> {
192 self.net_id_map.get(&net_id).copied()
193 }
194
195 pub fn add_constraint(&mut self, constraint: Box<dyn Constraint<D>>) {
197 self.constraints.push(constraint);
198 }
199
200 pub fn body(&self, handle: BodyHandle) -> Option<&RigidBody<D>> {
202 self.handle_to_index
203 .get(&handle)
204 .and_then(|&idx| self.bodies.get(idx))
205 }
206
207 pub fn body_mut(&mut self, handle: BodyHandle) -> Option<&mut RigidBody<D>> {
209 self.handle_to_index
210 .get(&handle)
211 .copied()
212 .and_then(|idx| self.bodies.get_mut(idx))
213 }
214
215 pub fn step_with_callback(&mut self, dt: f64, callback: &mut dyn PhysicsCallback<D>) {
220 self.step_internal(dt, Some(callback));
221 }
222
223 pub fn step(&mut self, dt: f64) {
225 self.step_internal(dt, None);
226 }
227
228 fn step_internal(&mut self, dt: f64, mut callback: Option<&mut dyn PhysicsCallback<D>>) {
229 self.collision_events.clear();
231 self.sensor_events.clear();
232 std::mem::swap(&mut self.contact_cache, &mut self.prev_cache);
234 self.contact_cache.begin_frame();
235
236 if self.static_tree_dirty {
238 self.static_broadphase.rebuild(&self.bodies);
239 self.static_tree_dirty = false;
240 }
241
242 #[cfg(feature = "deterministic-net")]
244 for body in &mut self.bodies {
245 if !body.sleeping {
248 for i in 0..D {
249 body.force_accumulator[i] = quantize_q16_16(body.force_accumulator[i]);
250 }
251 }
252 }
253 for body in &mut self.bodies {
254 if !body.sleeping {
255 integrator::integrate(body, &self.gravity, dt);
256 }
257 }
258 #[cfg(feature = "deterministic-net")]
259 for body in &mut self.bodies {
260 for i in 0..D {
262 body.transform.translation.0[i] = quantize_q16_16(body.transform.translation.0[i]);
263 body.linear_velocity[i] = quantize_q16_16(body.linear_velocity[i]);
264 }
265 }
266
267 let pairs = broadphase::find_pairs_incremental(&self.bodies, &self.static_broadphase);
269
270 self.contacts.clear();
272 for pair in &pairs {
273 let (idx_a, idx_b) = self.find_body_indices(pair.0, pair.1);
274 if let (Some(a), Some(b)) = (idx_a, idx_b) {
275 let pos_a = self.bodies[a].transform.translation.0;
276 let pos_b = self.bodies[b].transform.translation.0;
277
278 if let Some(manifold) = self.try_halfspace_contact(a, b, pair.0, pair.1) {
280 if self.bodies[a].is_sensor || self.bodies[b].is_sensor {
281 let (sensor, other) = if self.bodies[a].is_sensor {
282 (pair.0, pair.1)
283 } else {
284 (pair.1, pair.0)
285 };
286 self.sensor_events.push(crate::contact::SensorEvent { sensor, other });
287 continue;
288 }
289 self.contacts.push(manifold);
290 continue;
291 }
292
293 let result = gjk::intersects(
294 self.bodies[a].collider.as_ref(),
295 &pos_a,
296 self.bodies[b].collider.as_ref(),
297 &pos_b,
298 );
299
300 if result.intersecting {
301 if self.bodies[a].is_sensor || self.bodies[b].is_sensor {
303 let (sensor, other) = if self.bodies[a].is_sensor {
304 (pair.0, pair.1)
305 } else {
306 (pair.1, pair.0)
307 };
308 self.sensor_events.push(crate::contact::SensorEvent { sensor, other });
309 continue;
310 }
311
312 if let Some(epa_result) = crate::epa::penetration(
314 self.bodies[a].collider.as_ref(),
315 &pos_a,
316 self.bodies[b].collider.as_ref(),
317 &pos_b,
318 &result.simplex,
319 ) {
320 if epa_result.depth > 0.0 {
321 let manifold = manifold_gen::generate_contact_manifold(
323 self.bodies[a].collider.as_ref(),
324 &pos_a,
325 self.bodies[b].collider.as_ref(),
326 &pos_b,
327 epa_result.normal,
328 epa_result.depth,
329 pair.0,
330 pair.1,
331 );
332 self.contacts.push(manifold);
333 }
334 }
335 }
336 }
337 }
338
339 let islands = crate::island::build_islands(
341 &self.bodies,
342 &self.contacts,
343 &self.constraints,
344 &self.handle_to_index,
345 );
346 let active_contact_indices: Vec<usize> = islands.iter()
347 .filter(|island| !island.sleeping)
348 .flat_map(|island| island.contact_indices.iter().copied())
349 .collect();
350
351 for &ci in &active_contact_indices {
354 if ci < self.contacts.len() {
355 self.warm_start_contact(ci);
356 }
357 }
358
359 for _ in 0..self.solver_iterations {
362 for &ci in &active_contact_indices {
363 if ci < self.contacts.len() {
364 let contact = self.contacts[ci].clone();
365 let updated = self.resolve_contact(contact, dt, &mut callback);
366 self.contacts[ci] = updated;
367 }
368 }
369 }
370
371 for &ci in &active_contact_indices {
373 if ci < self.contacts.len() {
374 let c = &self.contacts[ci];
375 let total_lambda: f64 = c.points.iter().map(|p| p.lambda).sum();
376 self.contact_cache.store(
377 c.body_a, c.body_b, c.point(), total_lambda, 0.0,
378 );
379 }
380 }
381
382 let active_constraint_indices: Vec<usize> = islands.iter()
384 .filter(|island| !island.sleeping)
385 .flat_map(|island| island.constraint_indices.iter().copied())
386 .collect();
387
388 for _ in 0..self.solver_iterations {
389 for &ci in &active_constraint_indices {
390 if ci >= self.constraints.len() { continue; }
391 let (ha, hb) = self.constraints[ci].bodies();
392 let (idx_a, idx_b) = self.find_body_indices(ha, hb);
393 if let (Some(a), Some(b)) = (idx_a, idx_b) {
394 if a < b {
395 let (left, right) = self.bodies.split_at_mut(b);
396 self.constraints[ci].solve(&mut left[a], &mut right[0], dt);
397 } else {
398 let (left, right) = self.bodies.split_at_mut(a);
399 self.constraints[ci].solve(&mut right[0], &mut left[b], dt);
400 }
401 }
402 }
403 }
404
405 for _ in 0..self.solver_iterations {
407 for &ci in &active_constraint_indices {
408 if ci >= self.constraints.len() { continue; }
409 let (ha, hb) = self.constraints[ci].bodies();
410 let (idx_a, idx_b) = self.find_body_indices(ha, hb);
411 if let (Some(a), Some(b)) = (idx_a, idx_b) {
412 if a < b {
413 let (left, right) = self.bodies.split_at_mut(b);
414 self.constraints[ci].solve_velocity(&mut left[a], &mut right[0], dt);
415 } else {
416 let (left, right) = self.bodies.split_at_mut(a);
417 self.constraints[ci].solve_velocity(&mut right[0], &mut left[b], dt);
418 }
419 }
420 }
421 }
422
423 let threshold = self.sleep_threshold;
425 let ticks = self.sleep_ticks;
426 for body in &mut self.bodies {
427 body.try_sleep(threshold, ticks);
428 }
429 }
430
431 fn warm_start_contact(&mut self, ci: usize) {
436 let contact = self.contacts[ci].clone();
437 let (idx_a, idx_b) = self.find_body_indices(contact.body_a, contact.body_b);
438 let (Some(a), Some(b)) = (idx_a, idx_b) else { return; };
439
440 let primary_pt = contact.primary_point().position;
441 let cached_total = self.prev_cache
442 .lookup(contact.body_a, contact.body_b, &primary_pt)
443 .map(|c| c.normal_impulse * 0.8) .unwrap_or(0.0);
445
446 if cached_total > 1e-15 {
447 let n_pts = contact.points.len().max(1) as f64;
448 let per_pt = cached_total / n_pts;
449
450 let impulse = contact.normal * cached_total;
452 integrator::apply_impulse(&mut self.bodies[a], &(-impulse));
453 integrator::apply_impulse(&mut self.bodies[b], &impulse);
454
455 for pt in &mut self.contacts[ci].points {
457 pt.lambda = per_pt;
458 }
459 }
460 }
461
462 fn resolve_contact(
471 &mut self,
472 mut contact: ContactManifold<D>,
473 dt: f64,
474 callback: &mut Option<&mut dyn PhysicsCallback<D>>,
475 ) -> ContactManifold<D> {
476 let (idx_a, idx_b) = self.find_body_indices(contact.body_a, contact.body_b);
477 let (Some(a), Some(b)) = (idx_a, idx_b) else {
478 return contact;
479 };
480
481 let inv_mass_a = self.bodies[a].inv_mass;
482 let inv_mass_b = self.bodies[b].inv_mass;
483 let total_inv_mass = inv_mass_a + inv_mass_b;
484 if total_inv_mass < 1e-15 {
485 return contact;
486 }
487
488 let alpha = self.compliance / (dt * dt).max(1e-20);
490 let safe_dt = dt.max(1e-10);
491
492 let baumgarte = self.baumgarte;
493 let slop = self.slop;
494
495 let mut total_normal_impulse = 0.0_f64;
497
498 for pt in &mut contact.points {
499 let v_rel_n = {
500 let va = self.bodies[a].linear_velocity;
501 let vb = self.bodies[b].linear_velocity;
502 (va - vb).dot(&contact.normal)
503 };
504
505 let bias = (pt.depth - slop).max(0.0) * baumgarte / safe_dt;
507
508 let denom = total_inv_mass + alpha;
510 let delta_lambda = (-v_rel_n + bias) / denom;
511
512 let new_lambda = (pt.lambda + delta_lambda).max(0.0);
514 let actual_delta = new_lambda - pt.lambda;
515 pt.lambda = new_lambda;
516
517 if actual_delta.abs() > 1e-15 {
518 let modulated_delta = if let Some(ref cb) = callback {
520 cb.modulate_impulse(actual_delta, &pt.position)
521 } else {
522 actual_delta
523 };
524
525 let impulse = contact.normal * modulated_delta;
526 integrator::apply_impulse(&mut self.bodies[a], &(-impulse));
527 integrator::apply_impulse(&mut self.bodies[b], &impulse);
528 total_normal_impulse += modulated_delta.abs();
529 }
530 }
531
532 if total_normal_impulse > 1e-15 {
534 let primary_pt = contact.point();
535 let v_rel = self.bodies[b].linear_velocity - self.bodies[a].linear_velocity;
536 let v_n = contact.normal * v_rel.dot(&contact.normal);
537 let v_t = v_rel - v_n;
538 let v_t_mag = v_t.norm();
539
540 if v_t_mag > 1e-10 {
541 let tangent = v_t / v_t_mag;
542 let mut mu = (self.bodies[a].friction * self.bodies[b].friction).sqrt();
543
544 if let Some(ref cb) = callback {
546 mu *= cb.friction_multiplier(&primary_pt, contact.body_a);
547 }
548
549 let j_t_desired = -v_t_mag / total_inv_mass;
550 let j_t = j_t_desired.clamp(-mu * total_normal_impulse, mu * total_normal_impulse);
551 let friction_impulse = tangent * j_t;
552 integrator::apply_impulse(&mut self.bodies[a], &(-friction_impulse));
553 integrator::apply_impulse(&mut self.bodies[b], &friction_impulse);
554
555 if let Some(ref mut cb) = callback {
557 cb.record_dissipation(j_t.abs() * 0.1);
558 }
559 }
560
561 let event = CollisionEvent {
563 body_a: contact.body_a,
564 body_b: contact.body_b,
565 impulse: total_normal_impulse,
566 normal: contact.normal,
567 depth: contact.depth(),
568 };
569
570 if let Some(ref mut cb) = callback {
572 cb.on_collision(&event);
573 }
574
575 self.collision_events.push(event);
576
577 self.bodies[a].wake();
579 self.bodies[b].wake();
580 }
581
582 contact
583 }
584
585 fn try_halfspace_contact(
587 &self,
588 idx_a: usize,
589 idx_b: usize,
590 handle_a: BodyHandle,
591 handle_b: BodyHandle,
592 ) -> Option<ContactManifold<D>> {
593 use symtropy_math::HalfSpace;
594
595 if let Some(hs) = self.bodies[idx_a].collider.as_any().downcast_ref::<HalfSpace<D>>() {
597 let (center_b, radius_b) = self.bodies[idx_b].collider.bounding_sphere();
598 let world_center_b = self.bodies[idx_b].transform.transform_point(¢er_b).0;
599 if let Some((contact_pt, depth)) = hs.contact_sphere(&world_center_b, radius_b) {
600 return Some(ContactManifold::single(
601 handle_a, handle_b, hs.normal, contact_pt, depth,
602 ));
603 }
604 return None;
605 }
606
607 if let Some(hs) = self.bodies[idx_b].collider.as_any().downcast_ref::<HalfSpace<D>>() {
609 let (center_a, radius_a) = self.bodies[idx_a].collider.bounding_sphere();
610 let world_center_a = self.bodies[idx_a].transform.transform_point(¢er_a).0;
611 if let Some((contact_pt, depth)) = hs.contact_sphere(&world_center_a, radius_a) {
612 return Some(ContactManifold::single(
613 handle_a, handle_b, -hs.normal, contact_pt, depth,
614 ));
615 }
616 return None;
617 }
618
619 None }
621
622 fn find_body_indices(&self, a: BodyHandle, b: BodyHandle) -> (Option<usize>, Option<usize>) {
623 (
624 self.handle_to_index.get(&a).copied(),
625 self.handle_to_index.get(&b).copied(),
626 )
627 }
628
629 fn allocate_handle(&mut self) -> BodyHandle {
630 let h = BodyHandle(self.next_handle);
631 self.next_handle += 1;
632 h
633 }
634
635 pub fn body_count(&self) -> usize {
637 self.bodies.len()
638 }
639
640 pub fn total_kinetic_energy(&self) -> f64 {
642 self.bodies.iter().map(|b| b.kinetic_energy()).sum()
643 }
644
645 pub fn drain_collision_events(&mut self) -> Vec<CollisionEvent<D>> {
647 std::mem::take(&mut self.collision_events)
648 }
649
650 pub fn sleeping_count(&self) -> usize {
652 self.bodies.iter().filter(|b| b.sleeping).count()
653 }
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659
660 #[test]
661 fn empty_world_steps() {
662 let mut world = PhysicsWorld::<3>::new(SVector::from([0.0, -9.81, 0.0]));
663 world.step(0.01);
664 assert_eq!(world.body_count(), 0);
665 }
666
667 #[test]
668 fn falling_sphere() {
669 let mut world = PhysicsWorld::<3>::new(SVector::from([0.0, -9.81, 0.0]));
670 let h = world.add_sphere(Point::new([0.0, 10.0, 0.0]), 0.5, 1.0);
671
672 for _ in 0..100 {
673 world.step(0.01);
674 }
675
676 let y = world.body(h).unwrap().transform.translation.coord(1);
677 assert!(y < 10.0, "sphere should have fallen, y = {y}");
678 assert!(y > 0.0, "sphere fell too far, y = {y}");
679 }
680
681 #[test]
682 fn two_spheres_collide() {
683 let mut world = PhysicsWorld::<3>::new(SVector::zeros());
684 let a = world.add_sphere(Point::new([0.0, 0.0, 0.0]), 1.0, 1.0);
685 let b = world.add_sphere(Point::new([3.0, 0.0, 0.0]), 1.0, 1.0);
686
687 world.body_mut(a).unwrap().linear_velocity = SVector::from([5.0, 0.0, 0.0]);
689 world.body_mut(b).unwrap().linear_velocity = SVector::from([-5.0, 0.0, 0.0]);
690
691 for _ in 0..100 {
692 world.step(0.01);
693 }
694
695 let va = world.body(a).unwrap().linear_velocity[0];
697 let vb = world.body(b).unwrap().linear_velocity[0];
698 assert!(va < 5.0, "va = {va}, should have changed after collision");
700 }
701
702 #[test]
703 fn sphere_bounces_off_static() {
704 let mut world = PhysicsWorld::<3>::new(SVector::from([0.0, -9.81, 0.0]));
705
706 let floor = RigidBody::<3>::static_body(
708 BodyHandle(999),
709 Point::new([0.0, -0.5, 0.0]),
710 Box::new(symtropy_math::Sphere::<3>::new(symtropy_math::Point::origin(), 5.0)),
711 );
712 world.add_body(floor);
713
714 let sphere = world.add_sphere(Point::new([0.0, 3.0, 0.0]), 0.5, 1.0);
716
717 for _ in 0..200 {
718 world.step(0.01);
719 }
720
721 let y = world.body(sphere).unwrap().transform.translation.coord(1);
723 assert!(y > -3.0, "sphere fell through floor, y = {y}");
724 }
725
726 #[test]
727 fn physics_4d() {
728 let mut world = PhysicsWorld::<4>::new(SVector::from([0.0, -9.81, 0.0, 0.0]));
729 let h = world.add_sphere(Point::new([0.0, 10.0, 0.0, 0.0]), 1.0, 1.0);
730
731 for _ in 0..100 {
732 world.step(0.01);
733 }
734
735 let y = world.body(h).unwrap().transform.translation.coord(1);
736 assert!(y < 10.0, "4D sphere should fall, y = {y}");
737 }
738
739 #[test]
740 fn physics_2d() {
741 let mut world = PhysicsWorld::<2>::new(SVector::from([0.0, -9.81]));
742 let h = world.add_sphere(Point::new([0.0, 10.0]), 1.0, 1.0);
743
744 for _ in 0..100 {
745 world.step(0.01);
746 }
747
748 let y = world.body(h).unwrap().transform.translation.coord(1);
749 assert!(y < 10.0, "2D sphere should fall, y = {y}");
750 }
751
752 #[test]
753 fn energy_decreases_with_damping() {
754 let mut world = PhysicsWorld::<3>::new(SVector::zeros());
755 let h = world.add_sphere(Point::origin(), 1.0, 1.0);
756 world.body_mut(h).unwrap().linear_velocity = SVector::from([10.0, 0.0, 0.0]);
757
758 let initial_ke = world.total_kinetic_energy();
759
760 for _ in 0..100 {
761 world.step(0.01);
762 }
763
764 let final_ke = world.total_kinetic_energy();
765 assert!(final_ke < initial_ke, "KE should decrease with damping");
766 }
767
768 #[test]
769 fn add_bodies_deterministic_assigns_net_ids() {
770 use symtropy_math::{Point, Sphere};
771 let mut world = PhysicsWorld::<3>::new(SVector::from([0.0, -9.81, 0.0]));
772
773 let bodies = vec![
774 (NetId(42), RigidBody::dynamic_sphere(BodyHandle(0), Point::new([0.0, 5.0, 0.0]), 1.0, 1.0)),
775 (NetId(7), RigidBody::dynamic_sphere(BodyHandle(0), Point::new([3.0, 5.0, 0.0]), 1.0, 2.0)),
776 (NetId(99), RigidBody::static_body(BodyHandle(0), Point::origin(), Box::new(Sphere::<3>::new(Point::origin(), 10.0)))),
777 ];
778
779 let handles = world.add_bodies_deterministic(bodies).unwrap();
780 assert_eq!(handles.len(), 3);
781 assert_eq!(world.body_count(), 3);
782
783 let h7 = world.handle_for_net_id(NetId(7)).unwrap();
785 let h42 = world.handle_for_net_id(NetId(42)).unwrap();
786 let h99 = world.handle_for_net_id(NetId(99)).unwrap();
787
788 assert!(h7 < h42, "NetId(7) inserted before NetId(42)");
790 assert!(h42 < h99, "NetId(42) inserted before NetId(99)");
791
792 assert_eq!(world.body(h42).unwrap().net_id, Some(NetId(42)));
794
795 assert!(world.handle_for_net_id(NetId(999)).is_none());
797 }
798
799 #[test]
800 fn add_bodies_deterministic_rejects_duplicates() {
801 let mut world = PhysicsWorld::<3>::new(SVector::zeros());
802 let bodies = vec![
803 (NetId(1), RigidBody::dynamic_sphere(BodyHandle(0), Point::origin(), 1.0, 1.0)),
804 (NetId(1), RigidBody::dynamic_sphere(BodyHandle(0), Point::origin(), 1.0, 1.0)),
805 ];
806 assert!(world.add_bodies_deterministic(bodies).is_err());
807 }
808
809 #[test]
810 fn collision_groups_prevent_collision() {
811 let mut world = PhysicsWorld::<3>::new(SVector::zeros());
812
813 let h1 = world.add_sphere(Point::new([0.0, 0.0, 0.0]), 1.0, 1.0);
815 let h2 = world.add_sphere(Point::new([0.5, 0.0, 0.0]), 1.0, 1.0);
816
817 world.body_mut(h1).unwrap().collision_group = 0x01;
819 world.body_mut(h1).unwrap().collision_mask = 0x01;
820 world.body_mut(h2).unwrap().collision_group = 0x02;
821 world.body_mut(h2).unwrap().collision_mask = 0x02;
822
823 world.step(0.016);
825 assert!(
826 world.collision_events.is_empty(),
827 "bodies in different groups should not collide"
828 );
829 }
830
831 #[test]
832 fn collision_groups_allow_matching() {
833 let mut world = PhysicsWorld::<3>::new(SVector::zeros());
834
835 let h1 = world.add_sphere(Point::new([0.0, 0.0, 0.0]), 1.0, 1.0);
837 let h2 = world.add_sphere(Point::new([0.5, 0.0, 0.0]), 1.0, 1.0);
838
839 world.body_mut(h1).unwrap().collision_group = 0x01;
840 world.body_mut(h1).unwrap().collision_mask = 0x01;
841 world.body_mut(h2).unwrap().collision_group = 0x01;
842 world.body_mut(h2).unwrap().collision_mask = 0x01;
843
844 world.body_mut(h1).unwrap().linear_velocity = SVector::from([1.0, 0.0, 0.0]);
846 world.body_mut(h2).unwrap().linear_velocity = SVector::from([-1.0, 0.0, 0.0]);
847
848 world.step(0.016);
849 assert!(
851 !world.contacts.is_empty(),
852 "bodies in matching groups should generate contacts"
853 );
854 }
855
856 #[test]
857 fn sensor_detects_overlap_without_impulse() {
858 let mut world = PhysicsWorld::<3>::new(SVector::zeros());
859
860 let h1 = world.add_sphere(Point::new([0.0, 0.0, 0.0]), 1.0, 1.0);
861 let h2 = world.add_sphere(Point::new([0.5, 0.0, 0.0]), 1.0, 1.0);
862
863 world.body_mut(h1).unwrap().is_sensor = true;
865
866 world.step(0.016);
867
868 assert!(
870 world.collision_events.is_empty(),
871 "sensor should not produce collision events"
872 );
873 assert!(
874 !world.sensor_events.is_empty(),
875 "sensor should produce sensor events"
876 );
877 assert_eq!(world.sensor_events[0].sensor, h1);
878 assert_eq!(world.sensor_events[0].other, h2);
879 }
880}