1use alloc::vec::Vec;
2
3use he3d_math::{Quat, Vec3};
4
5use crate::aabb::Aabb;
6use crate::body::{Body, BodyDesc, BodyId, MotionType, NO_SLEEP_GROUP};
7use crate::broadphase::{DEFAULT_CELL_SIZE, SpatialGrid};
8use crate::contact::{
9 ConstraintPoint, Contact, ContactConstraint, ContactPartner, ContactPhase, MAX_MANIFOLD_POINTS,
10 tangent_basis,
11};
12use crate::narrowphase;
13use crate::shape::{MassProperties, Shape};
14use crate::solver::{self, SolverSettings};
15use crate::static_geometry::{StaticMesh, StaticMeshId, StaticTopology};
16use crate::storage::{STATIC_PARTNER, Slot};
17
18pub const SLEEP_LINEAR_THRESHOLD: f32 = 0.05;
21
22pub const SLEEP_ANGULAR_THRESHOLD: f32 = 0.05;
25
26pub const SLEEP_TIME: f32 = 0.5;
29
30pub const DEFAULT_GRAVITY: Vec3 = Vec3::new(0.0, -9.81, 0.0);
33
34#[derive(Clone, Debug)]
37struct StaticSlot {
38 generation: u32,
39 mesh: Option<StaticMesh>,
40}
41
42#[derive(Clone, Debug)]
68pub struct World {
69 slots: Vec<Slot>,
70 free: Vec<u32>,
71 gravity: Vec3,
72 settings: SolverSettings,
73 grid: SpatialGrid,
74 active: Vec<BodyId>,
76 aabbs: Vec<Aabb>,
78 constraints: Vec<ContactConstraint>,
80 previous: Vec<ContactConstraint>,
82 wakers: Vec<usize>,
84 contacts: Vec<Contact>,
86 static_meshes: Vec<StaticSlot>,
88 static_free: Vec<u32>,
90 topology: StaticTopology,
95 static_friction: f32,
98 sleep_groups: Vec<Vec<u32>>,
101 free_sleep_groups: Vec<u32>,
104 woken_groups: Vec<u32>,
107 island_parent: Vec<u32>,
110 island_root: Vec<u32>,
113 island_settled: Vec<bool>,
116 island_has_dynamic: Vec<bool>,
119 island_group: Vec<u32>,
122}
123
124impl Default for World {
125 fn default() -> Self {
126 Self::new()
127 }
128}
129
130impl World {
131 #[must_use]
134 pub fn new() -> Self {
135 Self {
136 slots: Vec::new(),
137 free: Vec::new(),
138 gravity: DEFAULT_GRAVITY,
139 settings: SolverSettings::default(),
140 grid: SpatialGrid::new(DEFAULT_CELL_SIZE),
141 active: Vec::new(),
142 aabbs: Vec::new(),
143 constraints: Vec::new(),
144 previous: Vec::new(),
145 wakers: Vec::new(),
146 contacts: Vec::new(),
147 static_meshes: Vec::new(),
148 static_free: Vec::new(),
149 topology: StaticTopology::default(),
150 static_friction: crate::body::DEFAULT_FRICTION,
151 sleep_groups: Vec::new(),
152 free_sleep_groups: Vec::new(),
153 woken_groups: Vec::new(),
154 island_parent: Vec::new(),
155 island_root: Vec::new(),
156 island_settled: Vec::new(),
157 island_has_dynamic: Vec::new(),
158 island_group: Vec::new(),
159 }
160 }
161
162 #[must_use]
165 pub fn gravity(&self) -> Vec3 {
166 self.gravity
167 }
168
169 pub fn set_gravity(&mut self, gravity: Vec3) {
172 if gravity.is_finite() {
173 self.gravity = gravity;
174 }
175 }
176
177 pub fn set_broadphase_cell_size(&mut self, cell_size: f32) {
183 if cell_size.is_finite() && cell_size > 0.0 {
184 self.grid = SpatialGrid::new(cell_size);
185 }
186 }
187
188 pub fn set_velocity_iterations(&mut self, iterations: u32) {
191 self.settings.velocity_iterations = iterations.max(1);
192 }
193
194 pub fn set_static_friction(&mut self, friction: f32) {
201 if friction.is_finite() && friction >= 0.0 {
202 self.static_friction = friction;
203 }
204 }
205
206 pub fn add_static_mesh(&mut self, positions: &[Vec3]) -> Option<StaticMeshId> {
224 if positions.is_empty() || positions.len() % 3 != 0 {
225 return None;
226 }
227 if !positions.iter().all(|vertex| vertex.is_finite()) {
228 return None;
229 }
230
231 let triangle_count = positions.len() / 3;
232 let mut owned = Vec::with_capacity(positions.len());
233 let mut triangle_bounds = Vec::with_capacity(triangle_count);
234 let mut welded = Vec::with_capacity(triangle_count);
235 for triangle_index in 0..triangle_count {
236 let base = triangle_index * 3;
237 let vertices = [positions[base], positions[base + 1], positions[base + 2]];
238 let ids = [
239 self.topology.weld(vertices[0]),
240 self.topology.weld(vertices[1]),
241 self.topology.weld(vertices[2]),
242 ];
243 self.topology.add_triangle(ids);
244 triangle_bounds.push(Aabb::new(
245 vertices[0].min(vertices[1]).min(vertices[2]),
246 vertices[0].max(vertices[1]).max(vertices[2]),
247 ));
248 welded.push(ids);
249 owned.extend(vertices);
250 }
251 let mut bounds = *triangle_bounds.first()?;
252 for triangle_aabb in &triangle_bounds[1..] {
253 bounds = bounds.merged(triangle_aabb);
254 }
255
256 let mesh = StaticMesh {
257 positions: owned,
258 triangle_bounds,
259 welded,
260 bounds,
261 };
262 if let Some(index) = self.static_free.pop() {
263 let slot = &mut self.static_meshes[index as usize];
264 slot.mesh = Some(mesh);
265 Some(StaticMeshId::new(index, slot.generation))
266 } else {
267 let index = u32::try_from(self.static_meshes.len()).ok()?;
268 self.static_meshes.push(StaticSlot {
269 generation: 0,
270 mesh: Some(mesh),
271 });
272 Some(StaticMeshId::new(index, 0))
273 }
274 }
275
276 pub fn remove_static_mesh(&mut self, id: StaticMeshId) -> bool {
284 let Some(slot) = self.static_meshes.get_mut(id.index() as usize) else {
285 return false;
286 };
287 if slot.generation != id.generation() {
288 return false;
289 }
290 let Some(mesh) = slot.mesh.take() else {
291 return false;
292 };
293 let bounds = mesh.bounds;
294 for triangle in &mesh.welded {
295 self.topology.remove_triangle(*triangle);
296 }
297 slot.generation = slot.generation.wrapping_add(1);
298 self.static_free.push(id.index());
299
300 for body_slot in &mut self.slots {
312 let Some(body) = body_slot.get_mut() else {
313 continue;
314 };
315 if body.sleeping && body.motion.is_simulated() && body.aabb().overlaps(&bounds) {
316 body.wake();
317 }
318 }
319 true
320 }
321
322 #[must_use]
325 pub fn contains_static_mesh(&self, id: StaticMeshId) -> bool {
326 self
327 .static_meshes
328 .get(id.index() as usize)
329 .is_some_and(|slot| slot.generation == id.generation() && slot.mesh.is_some())
330 }
331
332 #[must_use]
335 pub fn static_mesh_count(&self) -> usize {
336 self
337 .static_meshes
338 .iter()
339 .filter(|slot| slot.mesh.is_some())
340 .count()
341 }
342
343 #[must_use]
349 pub fn contacts(&self) -> &[Contact] {
350 &self.contacts
351 }
352
353 #[must_use]
356 pub fn len(&self) -> usize {
357 self
358 .slots
359 .iter()
360 .filter(|slot| slot.get().is_some())
361 .count()
362 }
363
364 #[must_use]
367 pub fn is_empty(&self) -> bool {
368 self.slots.iter().all(|slot| slot.get().is_none())
369 }
370
371 pub fn add_body(&mut self, desc: BodyDesc) -> BodyId {
381 let body = Body::from_desc(&desc);
382 if let Some(index) = self.free.pop() {
383 let slot = &mut self.slots[index as usize];
384 slot.body = Some(body);
385 BodyId::new(index, slot.generation)
386 } else {
387 let index = u32::try_from(self.slots.len()).expect("body count fits u32");
388 self.slots.push(Slot {
389 generation: 0,
390 body: Some(body),
391 });
392 BodyId::new(index, 0)
393 }
394 }
395
396 pub fn remove_body(&mut self, id: BodyId) -> bool {
399 let Some(slot) = self.slots.get_mut(id.index() as usize) else {
400 return false;
401 };
402 if slot.generation != id.generation() || slot.get().is_none() {
403 return false;
404 }
405 slot.body = None;
406 slot.generation = slot.generation.wrapping_add(1);
407 self.free.push(id.index());
408 true
409 }
410
411 #[must_use]
414 pub fn contains(&self, id: BodyId) -> bool {
415 self.body(id).is_some()
416 }
417
418 pub fn ids(&self) -> impl Iterator<Item = BodyId> + '_ {
426 self.slots.iter().enumerate().filter_map(|(index, slot)| {
427 slot.get().map(|_| {
428 BodyId::new(
429 u32::try_from(index).expect("slot count fits u32"),
430 slot.generation,
431 )
432 })
433 })
434 }
435
436 #[must_use]
439 pub fn shape(&self, id: BodyId) -> Option<Shape> {
440 self.body(id).map(|body| body.shape)
441 }
442
443 #[must_use]
446 pub fn motion(&self, id: BodyId) -> Option<MotionType> {
447 self.body(id).map(|body| body.motion)
448 }
449
450 #[must_use]
453 pub fn mass_properties(&self, id: BodyId) -> Option<MassProperties> {
454 self.body(id).map(|body| body.mass)
455 }
456
457 #[must_use]
460 pub fn position(&self, id: BodyId) -> Option<Vec3> {
461 self.body(id).map(|body| body.position)
462 }
463
464 #[must_use]
467 pub fn orientation(&self, id: BodyId) -> Option<Quat> {
468 self.body(id).map(|body| body.orientation)
469 }
470
471 #[must_use]
474 pub fn aabb(&self, id: BodyId) -> Option<Aabb> {
475 self.body(id).map(Body::aabb)
476 }
477
478 #[must_use]
481 pub fn velocity(&self, id: BodyId) -> Option<Vec3> {
482 self.body(id).map(|body| body.linear_velocity)
483 }
484
485 #[must_use]
488 pub fn angular_velocity(&self, id: BodyId) -> Option<Vec3> {
489 self.body(id).map(|body| body.angular_velocity)
490 }
491
492 #[must_use]
495 pub fn friction(&self, id: BodyId) -> Option<f32> {
496 self.body(id).map(|body| body.friction)
497 }
498
499 #[must_use]
502 pub fn restitution(&self, id: BodyId) -> Option<f32> {
503 self.body(id).map(|body| body.restitution)
504 }
505
506 #[must_use]
509 pub fn is_sleeping(&self, id: BodyId) -> Option<bool> {
510 self.body(id).map(|body| body.sleeping)
511 }
512
513 pub fn set_pose(&mut self, id: BodyId, position: Vec3, orientation: Quat) -> bool {
522 if !position.is_finite() || !orientation.is_finite() || orientation.length_squared() <= 0.0 {
523 return false;
524 }
525 let Some(body) = self.body_mut(id) else {
526 return false;
527 };
528 body.position = position;
529 body.orientation = orientation.normalize();
530 if body.motion.is_simulated() {
531 body.wake();
532 }
533 true
534 }
535
536 pub fn set_velocity(&mut self, id: BodyId, velocity: Vec3) -> bool {
539 if !velocity.is_finite() {
540 return false;
541 }
542 let Some(body) = self.body_mut(id) else {
543 return false;
544 };
545 if !body.motion.moves() {
546 return false;
547 }
548 body.linear_velocity = velocity;
549 if body.motion.is_simulated() {
550 body.wake();
551 }
552 true
553 }
554
555 pub fn set_angular_velocity(&mut self, id: BodyId, angular_velocity: Vec3) -> bool {
558 if !angular_velocity.is_finite() {
559 return false;
560 }
561 let Some(body) = self.body_mut(id) else {
562 return false;
563 };
564 if !body.motion.moves() {
565 return false;
566 }
567 body.angular_velocity = angular_velocity;
568 if body.motion.is_simulated() {
569 body.wake();
570 }
571 true
572 }
573
574 pub fn set_friction(&mut self, id: BodyId, friction: f32) -> bool {
577 if !friction.is_finite() {
578 return false;
579 }
580 let Some(body) = self.body_mut(id) else {
581 return false;
582 };
583 body.friction = friction.max(0.0);
584 true
585 }
586
587 pub fn set_restitution(&mut self, id: BodyId, restitution: f32) -> bool {
590 if !restitution.is_finite() {
591 return false;
592 }
593 let Some(body) = self.body_mut(id) else {
594 return false;
595 };
596 body.restitution = restitution.max(0.0);
597 true
598 }
599
600 pub fn set_damping(&mut self, id: BodyId, linear: f32, angular: f32) -> bool {
603 if !linear.is_finite() || !angular.is_finite() {
604 return false;
605 }
606 let Some(body) = self.body_mut(id) else {
607 return false;
608 };
609 body.linear_damping = linear.max(0.0);
610 body.angular_damping = angular.max(0.0);
611 true
612 }
613
614 pub fn set_gravity_scale(&mut self, id: BodyId, scale: f32) -> bool {
629 let scale = if scale.is_finite() && scale > 0.0 {
630 scale
631 } else {
632 0.0
633 };
634 let Some(body) = self.body_mut(id) else {
635 return false;
636 };
637 #[expect(clippy::float_cmp)]
641 let changed = body.gravity_scale != scale;
642 if changed {
643 body.wake();
644 }
645 body.gravity_scale = scale;
646 true
647 }
648
649 pub fn apply_force(&mut self, id: BodyId, force: Vec3) -> bool {
656 if !force.is_finite() {
657 return false;
658 }
659 let Some(body) = self.body_mut(id) else {
660 return false;
661 };
662 if !body.motion.is_simulated() {
663 return false;
664 }
665 body.force += force;
666 body.wake();
667 true
668 }
669
670 pub fn apply_torque(&mut self, id: BodyId, torque: Vec3) -> bool {
673 if !torque.is_finite() {
674 return false;
675 }
676 let Some(body) = self.body_mut(id) else {
677 return false;
678 };
679 if !body.motion.is_simulated() {
680 return false;
681 }
682 body.torque += torque;
683 body.wake();
684 true
685 }
686
687 pub fn apply_impulse(&mut self, id: BodyId, impulse: Vec3) -> bool {
690 if !impulse.is_finite() {
691 return false;
692 }
693 let Some(body) = self.body_mut(id) else {
694 return false;
695 };
696 if !body.motion.is_simulated() {
697 return false;
698 }
699 body.linear_velocity += impulse * body.mass.inv_mass;
700 body.wake();
701 true
702 }
703
704 pub fn apply_angular_impulse(&mut self, id: BodyId, impulse: Vec3) -> bool {
707 if !impulse.is_finite() {
708 return false;
709 }
710 let Some(body) = self.body_mut(id) else {
711 return false;
712 };
713 if !body.motion.is_simulated() {
714 return false;
715 }
716 let delta = body.apply_inv_inertia(impulse);
717 body.angular_velocity += delta;
718 body.wake();
719 true
720 }
721
722 pub fn wake(&mut self, id: BodyId) -> bool {
725 let Some(body) = self.body_mut(id) else {
726 return false;
727 };
728 body.wake();
729 true
730 }
731
732 pub fn clear_forces(&mut self) {
735 for slot in &mut self.slots {
736 if let Some(body) = slot.get_mut() {
737 body.force = Vec3::ZERO;
738 body.torque = Vec3::ZERO;
739 }
740 }
741 }
742
743 pub fn step(&mut self, delta_time: f32) {
752 if !delta_time.is_finite() || delta_time <= 0.0 {
753 return;
754 }
755 self.integrate_velocities(delta_time);
756 self.detect_contacts();
757 self.carry_impulses();
758 solver::prepare(&self.slots, &mut self.constraints, delta_time);
759 solver::warm_start(&mut self.slots, &self.constraints);
760 solver::solve_velocities(&mut self.slots, &mut self.constraints, self.settings);
761 solver::solve_positions(&mut self.slots, &mut self.constraints);
762 self.integrate_positions(delta_time);
763 self.publish_contacts();
764 core::mem::swap(&mut self.constraints, &mut self.previous);
765 self.update_sleep(delta_time);
766 self.refresh_movement();
767 self.clear_forces();
768 }
769
770 fn refresh_movement(&mut self) {
777 for slot in &mut self.slots {
778 if let Some(body) = slot.get_mut() {
779 body.refresh_movement(SLEEP_LINEAR_THRESHOLD, SLEEP_ANGULAR_THRESHOLD);
780 }
781 }
782 }
783
784 fn integrate_velocities(&mut self, delta_time: f32) {
787 let gravity = self.gravity;
788 for slot in &mut self.slots {
789 let Some(body) = slot.get_mut() else {
790 continue;
791 };
792 if !body.motion.is_simulated() || body.sleeping {
793 continue;
794 }
795 if body.mass.inv_mass > 0.0 && body.gravity_scale > 0.0 {
801 body.linear_velocity += gravity * (body.gravity_scale * delta_time);
802 }
803 body.linear_velocity += body.force * body.mass.inv_mass * delta_time;
804 body.linear_velocity *= 1.0 / (1.0 + body.linear_damping * delta_time);
805
806 let angular_acceleration = body.apply_inv_inertia(body.torque);
807 body.angular_velocity += angular_acceleration * delta_time;
808 body.angular_velocity *= 1.0 / (1.0 + body.angular_damping * delta_time);
809 }
810 }
811
812 fn integrate_positions(&mut self, delta_time: f32) {
816 for slot in &mut self.slots {
817 let Some(body) = slot.get_mut() else {
818 continue;
819 };
820 if !body.motion.moves() || body.sleeping {
821 continue;
822 }
823 let linear = body.linear_velocity + body.pseudo_linear_velocity;
824 body.position += linear * delta_time;
825
826 let spin = body.angular_velocity + body.pseudo_angular_velocity;
830 let speed = spin.length();
831 if speed > 1e-8 {
832 let axis = spin / speed;
833 let step = Quat::from_axis_angle(axis, speed * delta_time);
834 body.orientation = (step * body.orientation).normalize();
835 }
836 body.clear_pseudo_velocities();
837 }
838 }
839
840 fn detect_contacts(&mut self) {
848 self.active.clear();
849 self.aabbs.clear();
850 for (index, slot) in self.slots.iter().enumerate() {
851 if let Some(body) = slot.get() {
852 let Ok(index) = u32::try_from(index) else {
853 break;
854 };
855 self.active.push(BodyId::new(index, slot.generation));
856 self.aabbs.push(body.aabb());
857 }
858 }
859
860 let static_friction = self.static_friction;
861 let Self {
862 slots,
863 grid,
864 active,
865 aabbs,
866 constraints,
867 wakers,
868 static_meshes,
869 topology,
870 ..
871 } = self;
872 grid.rebuild(aabbs);
873 constraints.clear();
874 wakers.clear();
875
876 grid.for_each_candidate(aabbs, |first_compact, second_compact| {
877 let first_index = active[first_compact as usize].index() as usize;
878 let second_index = active[second_compact as usize].index() as usize;
879 let (Some(first), Some(second)) = (slots[first_index].get(), slots[second_index].get())
880 else {
881 return;
882 };
883 if !solver::is_solvable(first, second) {
884 return;
885 }
886 match (first.sleeping, second.sleeping) {
887 (true, true) => return,
888 (true, false) => {
889 if second.was_moving {
895 wakers.push(first_index);
896 } else {
897 return;
898 }
899 }
900 (false, true) => {
901 if first.was_moving {
902 wakers.push(second_index);
903 } else {
904 return;
905 }
906 }
907 (false, false) => {}
908 }
909
910 let Some(manifold) = narrowphase::collide(
911 first.shape,
912 first.position,
913 first.orientation,
914 second.shape,
915 second.position,
916 second.orientation,
917 ) else {
918 return;
919 };
920
921 let mut constraint = ContactConstraint {
922 id_a: active[first_compact as usize],
923 id_b: ContactPartner::Body(active[second_compact as usize]),
924 a: u32::try_from(first_index).unwrap_or(0),
925 b: u32::try_from(second_index).unwrap_or(0),
926 partner_key: 0,
927 normal: manifold.normal,
928 tangents: tangent_basis(manifold.normal),
929 friction: solver::combine_friction(first.friction, second.friction),
930 point_count: 0,
931 points: [ConstraintPoint::default(); MAX_MANIFOLD_POINTS],
932 };
933 for (slot, point) in manifold.points[..manifold.count].iter().enumerate() {
934 constraint.points[slot] = ConstraintPoint {
935 position: point.position,
936 anchor_a: point.position - first.position,
937 anchor_b: point.position - second.position,
938 depth: point.depth,
939 feature: point.feature,
940 ..ConstraintPoint::default()
941 };
942 }
943 constraint.point_count = u8::try_from(manifold.count).unwrap_or(0);
944 constraints.push(constraint);
945 });
946
947 detect_static_contacts(slots, static_friction, static_meshes, topology, constraints);
948 for index in wakers.drain(..) {
949 if let Some(body) = slots[index].get_mut() {
950 body.wake();
951 }
952 }
953 }
954
955 fn carry_impulses(&mut self) {
959 self
962 .constraints
963 .sort_unstable_by_key(ContactConstraint::sort_key);
964 for current in &mut self.constraints {
965 let key = current.sort_key();
966 let Ok(found) = self
967 .previous
968 .binary_search_by_key(&key, ContactConstraint::sort_key)
969 else {
970 continue;
971 };
972 let old = &self.previous[found];
973 for point in current.points_mut() {
974 if let Some(previous) = old
975 .points()
976 .iter()
977 .find(|entry| entry.feature == point.feature)
978 {
979 point.normal_impulse = previous.normal_impulse;
980 point.tangent_impulse = previous.tangent_impulse;
981 }
982 }
983 }
984 }
985
986 fn publish_contacts(&mut self) {
989 let Self {
990 constraints,
991 previous,
992 contacts,
993 ..
994 } = self;
995 contacts.clear();
996
997 for current in constraints.iter() {
998 let key = current.sort_key();
999 let old = previous
1000 .binary_search_by_key(&key, ContactConstraint::sort_key)
1001 .ok()
1002 .map(|found| &previous[found]);
1003 for point in current.points() {
1004 let existed = old.is_some_and(|entry| {
1005 entry
1006 .points()
1007 .iter()
1008 .any(|candidate| candidate.feature == point.feature)
1009 });
1010 contacts.push(Contact {
1011 a: current.id_a,
1012 b: current.id_b,
1013 point: point.position,
1014 normal: current.normal,
1015 depth: point.depth,
1016 phase: if existed {
1017 ContactPhase::Persist
1018 } else {
1019 ContactPhase::Begin
1020 },
1021 });
1022 }
1023 }
1024
1025 for old in previous.iter() {
1031 let key = old.sort_key();
1032 let current = constraints
1033 .binary_search_by_key(&key, ContactConstraint::sort_key)
1034 .ok()
1035 .map(|found| &constraints[found]);
1036 for point in old.points() {
1037 let survives = current.is_some_and(|entry| {
1038 entry
1039 .points()
1040 .iter()
1041 .any(|candidate| candidate.feature == point.feature)
1042 });
1043 if !survives {
1044 contacts.push(Contact {
1045 a: old.id_a,
1046 b: old.id_b,
1047 point: point.position,
1048 normal: old.normal,
1049 depth: point.depth,
1050 phase: ContactPhase::End,
1051 });
1052 }
1053 }
1054 }
1055 }
1056
1057 fn update_sleep(&mut self, delta_time: f32) {
1067 self.dissolve_woken_groups();
1068
1069 for slot in &mut self.slots {
1070 let Some(body) = slot.get_mut() else {
1071 continue;
1072 };
1073 if !body.motion.is_simulated() || body.sleeping {
1074 continue;
1075 }
1076 if body.is_below_sleep_threshold(SLEEP_LINEAR_THRESHOLD, SLEEP_ANGULAR_THRESHOLD) {
1077 body.sleep_timer += delta_time;
1078 } else {
1079 body.sleep_timer = 0.0;
1080 }
1081 }
1082
1083 self.sleep_settled_islands();
1084 }
1085
1086 fn dissolve_woken_groups(&mut self) {
1096 self.woken_groups.clear();
1097 for slot in &self.slots {
1098 let Some(body) = slot.get() else {
1099 continue;
1100 };
1101 if !body.sleeping
1102 && body.sleep_group != NO_SLEEP_GROUP
1103 && !self.woken_groups.contains(&body.sleep_group)
1104 {
1105 self.woken_groups.push(body.sleep_group);
1106 }
1107 }
1108 if self.woken_groups.is_empty() {
1109 return;
1110 }
1111 for slot in &mut self.slots {
1112 let Some(body) = slot.get_mut() else {
1113 continue;
1114 };
1115 if self.woken_groups.contains(&body.sleep_group) {
1116 body.wake();
1117 body.sleep_group = NO_SLEEP_GROUP;
1118 }
1119 }
1120 for group in self.woken_groups.drain(..) {
1121 if let Some(members) = self.sleep_groups.get_mut(group as usize) {
1122 members.clear();
1123 }
1124 self.free_sleep_groups.push(group);
1125 }
1126 }
1127
1128 fn sleep_settled_islands(&mut self) {
1132 let count = self.slots.len();
1133 let Ok(count_u32) = u32::try_from(count) else {
1134 return;
1135 };
1136 if count == 0 {
1137 return;
1138 }
1139
1140 self.island_parent.clear();
1141 self.island_parent.extend(0..count_u32);
1142 self.island_root.clear();
1143 self.island_root.resize(count, NO_SLEEP_GROUP);
1144 self.island_settled.clear();
1145 self.island_settled.resize(count, true);
1146 self.island_has_dynamic.clear();
1147 self.island_has_dynamic.resize(count, false);
1148 self.island_group.clear();
1149 self.island_group.resize(count, NO_SLEEP_GROUP);
1150
1151 for constraint in &self.constraints {
1157 if constraint.b == STATIC_PARTNER {
1158 continue;
1159 }
1160 let first = find_root(&mut self.island_parent, constraint.a);
1161 let second = find_root(&mut self.island_parent, constraint.b);
1162 if first != second {
1163 self.island_parent[second as usize] = first;
1164 }
1165 }
1166
1167 for index in 0..count {
1168 let Some(body) = self.slots[index].get() else {
1169 continue;
1170 };
1171 if !body.motion.is_simulated() || body.sleeping {
1172 continue;
1173 }
1174 let root = find_root(&mut self.island_parent, u32::try_from(index).unwrap_or(0));
1175 self.island_root[index] = root;
1176 self.island_has_dynamic[root as usize] = true;
1177 if body.sleep_timer < SLEEP_TIME {
1178 self.island_settled[root as usize] = false;
1179 }
1180 }
1181
1182 for index in 0..count {
1183 let root = self.island_root[index];
1184 if root == NO_SLEEP_GROUP
1185 || !self.island_has_dynamic[root as usize]
1186 || !self.island_settled[root as usize]
1187 {
1188 continue;
1189 }
1190 let mut group = self.island_group[root as usize];
1191 if group == NO_SLEEP_GROUP {
1192 group = self.allocate_sleep_group();
1193 self.island_group[root as usize] = group;
1194 }
1195 if let Some(members) = self.sleep_groups.get_mut(group as usize) {
1196 members.push(u32::try_from(index).unwrap_or(0));
1197 }
1198 if let Some(body) = self.slots[index].get_mut() {
1199 body.put_to_sleep();
1200 body.sleep_group = group;
1201 }
1202 }
1203 }
1204
1205 fn allocate_sleep_group(&mut self) -> u32 {
1208 if let Some(group) = self.free_sleep_groups.pop() {
1209 return group;
1210 }
1211 let index = u32::try_from(self.sleep_groups.len()).unwrap_or(u32::MAX);
1212 self.sleep_groups.push(Vec::new());
1213 index
1214 }
1215
1216 fn body(&self, id: BodyId) -> Option<&Body> {
1218 let slot = self.slots.get(id.index() as usize)?;
1219 if slot.generation != id.generation() {
1220 return None;
1221 }
1222 slot.get()
1223 }
1224
1225 fn body_mut(&mut self, id: BodyId) -> Option<&mut Body> {
1227 let slot = self.slots.get_mut(id.index() as usize)?;
1228 if slot.generation != id.generation() {
1229 return None;
1230 }
1231 slot.get_mut()
1232 }
1233}
1234
1235fn detect_static_contacts(
1244 slots: &[Slot],
1245 static_friction: f32,
1246 static_meshes: &[StaticSlot],
1247 topology: &StaticTopology,
1248 constraints: &mut Vec<ContactConstraint>,
1249) {
1250 for (mesh_index, slot) in static_meshes.iter().enumerate() {
1256 let Some(mesh) = slot.mesh.as_ref() else {
1257 continue;
1258 };
1259 let Ok(mesh_index) = u32::try_from(mesh_index) else {
1260 break;
1261 };
1262 let mesh_id = StaticMeshId::new(mesh_index, slot.generation);
1263 for (body_index, body_slot) in slots.iter().enumerate() {
1264 let Some(body) = body_slot.get() else {
1265 continue;
1266 };
1267 if body.sleeping || !body.motion.moves() {
1272 continue;
1273 }
1274 let body_bounds = body.aabb();
1275 if !body_bounds.overlaps(&mesh.bounds) {
1276 continue;
1277 }
1278 let Ok(body_index) = u32::try_from(body_index) else {
1279 break;
1280 };
1281 for triangle_index in 0..mesh.triangle_count() {
1282 if !body_bounds.overlaps(&mesh.triangle_bounds[triangle_index]) {
1283 continue;
1284 }
1285 let triangle = mesh.triangle(triangle_index);
1286 let hit = match body.shape {
1287 Shape::Box { half_extents } => {
1288 let frame =
1289 narrowphase::BoxFrame::new(body.position, body.orientation, half_extents);
1290 narrowphase::box_triangle(&frame, triangle)
1291 }
1292 Shape::Sphere { radius } => {
1293 narrowphase::sphere_triangle(body.position, radius, triangle).map(|manifold| {
1294 narrowphase::TriangleHit {
1295 manifold,
1296 source_edge: None,
1297 }
1298 })
1299 }
1300 };
1301 let Some(hit) = hit else {
1302 continue;
1303 };
1304
1305 if let Some(edge) = hit.source_edge {
1313 let ids = mesh.welded[triangle_index];
1314 if topology.is_internal_edge(ids[edge], ids[(edge + 1) % 3]) {
1315 continue;
1316 }
1317 }
1318
1319 let key =
1320 (u64::from(mesh_id.index()) << 32) | u64::try_from(triangle_index).unwrap_or(0);
1321 let mut constraint = ContactConstraint {
1322 id_a: BodyId::new(body_index, body_slot.generation),
1323 id_b: ContactPartner::Static(mesh_id),
1324 a: body_index,
1325 b: STATIC_PARTNER,
1326 partner_key: key,
1327 normal: hit.manifold.normal,
1328 tangents: tangent_basis(hit.manifold.normal),
1329 friction: solver::combine_friction(body.friction, static_friction),
1330 point_count: 0,
1331 points: [ConstraintPoint::default(); MAX_MANIFOLD_POINTS],
1332 };
1333 for (slot, point) in hit.manifold.points[..hit.manifold.count].iter().enumerate() {
1334 constraint.points[slot] = ConstraintPoint {
1335 position: point.position,
1336 anchor_a: point.position - body.position,
1337 anchor_b: Vec3::ZERO,
1343 depth: point.depth,
1344 feature: point.feature,
1345 ..ConstraintPoint::default()
1346 };
1347 }
1348 constraint.point_count = u8::try_from(hit.manifold.count).unwrap_or(0);
1349 constraints.push(constraint);
1350 }
1351 }
1352 }
1353}
1354fn find_root(parent: &mut [u32], start: u32) -> u32 {
1357 let mut index = start;
1358 loop {
1359 let Some(&up) = parent.get(index as usize) else {
1360 return index;
1361 };
1362 if up == index {
1363 return index;
1364 }
1365 let grandparent = parent.get(up as usize).copied().unwrap_or(up);
1366 parent[index as usize] = grandparent;
1367 index = grandparent;
1368 }
1369}
1370
1371#[cfg(test)]
1372#[allow(
1378 clippy::cast_precision_loss,
1379 clippy::cast_possible_truncation,
1380 clippy::cast_sign_loss
1381)]
1382mod tests {
1383 use super::{DEFAULT_GRAVITY, SLEEP_TIME, World};
1384 use crate::body::{BodyDesc, BodyId};
1385 use crate::contact::{ContactPartner, ContactPhase};
1386 use crate::shape::Shape;
1387 use he3d_math::Vec3;
1388
1389 fn free_fall_world() -> (World, BodyId) {
1390 let mut world = World::new();
1391 let ball =
1392 world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0).at(Vec3::new(0.0, 10.0, 0.0)));
1393 (world, ball)
1394 }
1395
1396 fn ground(world: &mut World, top: f32) -> BodyId {
1398 let half = Vec3::new(20.0, 0.5, 20.0);
1399 world.add_body(BodyDesc::stationary(Shape::cuboid(half)).at(Vec3::new(0.0, top - 0.5, 0.0)))
1400 }
1401
1402 fn settle(world: &mut World, seconds: f32) {
1403 let dt = 1.0 / 120.0;
1404 let steps = (seconds / dt) as u32;
1405 for _ in 0..steps {
1406 world.step(dt);
1407 }
1408 }
1409
1410 #[test]
1411 fn gravity_accelerates_a_dynamic_body() {
1412 let (mut world, ball) = free_fall_world();
1413 world.step(0.5);
1414 let velocity = world.velocity(ball).expect("ball exists");
1415 assert!((velocity.y + 4.905).abs() < 1e-3, "got {velocity:?}");
1416 }
1417
1418 #[test]
1419 fn a_static_body_never_moves() {
1420 let mut world = World::new();
1421 let slab = world.add_body(BodyDesc::stationary(Shape::cuboid(Vec3::splat(5.0))));
1422 for _ in 0..120 {
1423 world.step(1.0 / 60.0);
1424 }
1425 assert_eq!(world.position(slab), Some(Vec3::ZERO));
1426 assert_eq!(world.velocity(slab), Some(Vec3::ZERO));
1427 }
1428
1429 #[test]
1430 fn a_kinematic_body_ignores_gravity_and_holds_its_velocity() {
1431 let mut world = World::new();
1432 let platform = world.add_body(BodyDesc::kinematic(Shape::cuboid(Vec3::splat(0.5))));
1433 assert!(world.set_velocity(platform, Vec3::new(2.0, 0.0, 0.0)));
1434 for _ in 0..120 {
1435 world.step(1.0 / 60.0);
1436 }
1437 let position = world.position(platform).expect("platform exists");
1438 assert!((position.x - 4.0).abs() < 1e-3, "got {position:?}");
1439 assert!(position.y.abs() < 1e-6, "got {position:?}");
1440 }
1441
1442 #[test]
1443 fn a_kinematic_body_ignores_forces_and_keeps_its_speed() {
1444 let mut world = World::new();
1445 let platform = world.add_body(BodyDesc::kinematic(Shape::cuboid(Vec3::splat(0.5))));
1446 world.set_velocity(platform, Vec3::new(1.0, 0.0, 0.0));
1447 assert!(!world.apply_force(platform, Vec3::new(1000.0, 0.0, 0.0)));
1448 world.step(0.1);
1449 let velocity = world.velocity(platform).expect("platform exists");
1450 assert!((velocity.x - 1.0).abs() < 1e-6, "got {velocity:?}");
1451 }
1452
1453 #[test]
1454 fn damping_decays_velocity_by_the_discrete_factor_each_step() {
1455 let mut world = World::new();
1456 world.set_gravity(Vec3::ZERO);
1457 let dt = 1.0 / 60.0;
1458 let damping = 2.0;
1459 let body = world.add_body(
1460 BodyDesc::dynamic(Shape::sphere(0.5), 1.0)
1461 .with_velocity(Vec3::new(10.0, 0.0, 0.0))
1462 .with_damping(damping, 0.0),
1463 );
1464 let factor = 1.0 / (1.0 + damping * dt);
1465 let mut expected = 10.0;
1466 for _ in 0..120 {
1467 world.step(dt);
1468 expected *= factor;
1469 let speed = world.velocity(body).expect("body exists").x;
1470 assert!(speed > 0.0, "damping must not reverse the direction");
1471 assert!(
1472 (speed - expected).abs() < 1e-3,
1473 "got {speed}, expected {expected}"
1474 );
1475 }
1476 }
1477
1478 #[test]
1479 fn a_zero_mass_dynamic_body_ignores_gravity() {
1480 let mut world = World::new();
1481 let ghost = world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 0.0).at(Vec3::Y));
1482 world.step(1.0);
1483 assert_eq!(world.position(ghost), Some(Vec3::Y));
1484 }
1485
1486 #[test]
1487 fn removing_a_body_invalidates_its_handle_without_disturbing_others() {
1488 let mut world = World::new();
1489 let first = world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0));
1490 let second = world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0));
1491 assert!(world.remove_body(first));
1492 assert!(!world.contains(first));
1493 assert!(world.contains(second));
1494 assert!(!world.remove_body(first), "double removal must fail");
1495
1496 let third = world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0));
1497 assert_eq!(third.index(), first.index());
1498 assert_ne!(third.generation(), first.generation());
1499 assert!(!world.contains(first));
1500 }
1501
1502 #[test]
1503 fn a_degenerate_step_is_ignored() {
1504 let (mut world, ball) = free_fall_world();
1505 let before = world.position(ball).expect("ball exists");
1506 world.step(0.0);
1507 world.step(-1.0);
1508 world.step(f32::NAN);
1509 world.step(f32::INFINITY);
1510 assert_eq!(world.position(ball), Some(before));
1511 }
1512
1513 #[test]
1514 fn a_force_acts_for_exactly_one_step() {
1515 let mut world = World::new();
1516 world.set_gravity(Vec3::ZERO);
1517 let body = world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0));
1518 assert!(world.apply_force(body, Vec3::new(0.0, 100.0, 0.0)));
1519 world.step(0.1);
1520 let after_push = world.velocity(body).expect("body exists").y;
1521 assert!((after_push - 10.0).abs() < 1e-3, "got {after_push}");
1522 world.step(0.1);
1523 let after_idle = world.velocity(body).expect("body exists").y;
1524 assert!(
1525 (after_idle - after_push).abs() < 1e-3,
1526 "the force must not persist: {after_idle}"
1527 );
1528 }
1529
1530 #[test]
1531 fn a_spinning_body_keeps_a_unit_quaternion() {
1532 let mut world = World::new();
1533 world.set_gravity(Vec3::ZERO);
1534 let body = world.add_body(
1535 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
1536 .with_angular_velocity(Vec3::new(3.0, 1.0, 2.0)),
1537 );
1538 for _ in 0..2000 {
1539 world.step(1.0 / 120.0);
1540 let orientation = world.orientation(body).expect("body exists");
1541 assert!(
1542 (orientation.length() - 1.0).abs() < 1e-4,
1543 "quaternion drifted: {}",
1544 orientation.length()
1545 );
1546 }
1547 }
1548
1549 #[test]
1550 fn ids_reports_every_live_body() {
1551 let mut world = World::new();
1552 let a = world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0));
1553 let b = world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0));
1554 let mut ids: alloc::vec::Vec<_> = world.ids().collect();
1555 ids.sort_unstable();
1556 assert_eq!(ids, alloc::vec![a.min(b), a.max(b)]);
1557 assert_eq!(world.len(), 2);
1558 assert!(!world.is_empty());
1559 world.remove_body(a);
1560 assert_eq!(world.len(), 1);
1561 }
1562
1563 #[test]
1566 fn a_ball_lands_on_the_ground_and_stops() {
1567 let mut world = World::new();
1568 let slab = ground(&mut world, 0.0);
1569 let ball = world.add_body(
1570 BodyDesc::dynamic(Shape::sphere(0.5), 1.0)
1571 .at(Vec3::new(0.0, 3.0, 0.0))
1572 .with_friction(0.6),
1573 );
1574 settle(&mut world, 3.0);
1575
1576 let position = world.position(ball).expect("ball exists");
1577 assert!(
1578 (position.y - 0.5).abs() < 0.05,
1579 "the ball should rest on the slab, got {position:?}"
1580 );
1581 let speed = world.velocity(ball).expect("ball exists").length();
1582 assert!(speed < 0.1, "the ball should be at rest, got {speed}");
1583 assert_eq!(world.position(slab), Some(Vec3::new(0.0, -0.5, 0.0)));
1584 }
1585
1586 #[test]
1587 fn a_ball_does_not_fall_through_the_ground() {
1588 let mut world = World::new();
1589 ground(&mut world, 0.0);
1590 let ball = world.add_body(
1591 BodyDesc::dynamic(Shape::sphere(0.5), 1.0)
1592 .at(Vec3::new(0.0, 3.0, 0.0))
1593 .with_velocity(Vec3::new(0.0, -20.0, 0.0)),
1594 );
1595 settle(&mut world, 2.0);
1596 let y = world.position(ball).expect("ball exists").y;
1597 assert!(y > -0.5, "the ball tunnelled through, got y={y}");
1598 }
1599
1600 #[test]
1601 fn a_single_box_lands_flat_and_stops() {
1602 let mut world = World::new();
1603 ground(&mut world, 0.0);
1604 let cube = world.add_body(
1605 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0).at(Vec3::new(0.0, 2.0, 0.0)),
1606 );
1607 settle(&mut world, 3.0);
1608 let position = world.position(cube).expect("cube exists");
1609 assert!(
1610 (position.y - 0.5).abs() < 0.05,
1611 "cube should rest at 0.5, got {position:?}"
1612 );
1613 assert!(
1614 world.velocity(cube).expect("cube exists").length() < 0.1,
1615 "cube should be at rest"
1616 );
1617 }
1618
1619 #[test]
1620 fn a_stack_of_boxes_settles_without_sinking() {
1621 let mut world = World::new();
1622 ground(&mut world, 0.0);
1623 let mut cubes = alloc::vec::Vec::new();
1624 for level in 0..5 {
1625 let y = 0.5 + level as f32 * 1.01;
1626 cubes.push(
1627 world.add_body(
1628 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
1629 .at(Vec3::new(0.0, y, 0.0))
1630 .with_friction(0.9),
1631 ),
1632 );
1633 }
1634 settle(&mut world, 6.0);
1635
1636 for (level, cube) in cubes.iter().enumerate() {
1637 let y = world.position(*cube).expect("cube exists").y;
1638 let expected = 0.5 + level as f32;
1639 assert!(
1640 (y - expected).abs() < 0.15,
1641 "level {level} should rest near {expected}, got {y}"
1642 );
1643 }
1644 }
1645
1646 #[test]
1647 fn a_resting_stack_does_not_jitter() {
1648 let mut world = World::new();
1649 ground(&mut world, 0.0);
1650 let mut cubes = alloc::vec::Vec::new();
1651 for level in 0..4 {
1652 cubes.push(
1653 world.add_body(
1654 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
1655 .at(Vec3::new(0.0, 0.5 + level as f32 * 1.01, 0.0))
1656 .with_friction(0.9),
1657 ),
1658 );
1659 }
1660 settle(&mut world, 4.0);
1661 let before: alloc::vec::Vec<_> = cubes
1662 .iter()
1663 .map(|cube| world.position(*cube).expect("cube exists"))
1664 .collect();
1665 settle(&mut world, 2.0);
1666 for (index, cube) in cubes.iter().enumerate() {
1667 let after = world.position(*cube).expect("cube exists");
1668 assert!(
1669 (after - before[index]).length() < 0.02,
1670 "level {index} drifted from {:?} to {after:?}",
1671 before[index]
1672 );
1673 }
1674 }
1675
1676 #[test]
1677 fn a_resting_stack_falls_asleep() {
1678 let mut world = World::new();
1679 ground(&mut world, 0.0);
1680 let mut cubes = alloc::vec::Vec::new();
1681 for level in 0..3 {
1682 cubes.push(
1683 world.add_body(
1684 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
1685 .at(Vec3::new(0.0, 0.5 + level as f32 * 1.01, 0.0))
1686 .with_friction(0.9),
1687 ),
1688 );
1689 }
1690 settle(&mut world, SLEEP_TIME + 6.0);
1691 for cube in &cubes {
1692 assert_eq!(
1693 world.is_sleeping(*cube),
1694 Some(true),
1695 "a settled stack should sleep"
1696 );
1697 }
1698 }
1699
1700 #[test]
1701 fn a_sleeping_stack_wakes_when_a_neighbour_arrives() {
1702 let mut world = World::new();
1703 ground(&mut world, 0.0);
1704 let cube = world.add_body(
1705 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
1706 .at(Vec3::new(0.0, 0.5, 0.0))
1707 .with_friction(0.9),
1708 );
1709 settle(&mut world, SLEEP_TIME + 4.0);
1710 assert_eq!(world.is_sleeping(cube), Some(true));
1711
1712 let projectile = world.add_body(
1713 BodyDesc::dynamic(Shape::sphere(0.4), 1.0)
1714 .at(Vec3::new(-3.0, 0.5, 0.0))
1715 .with_velocity(Vec3::new(12.0, 0.0, 0.0)),
1716 );
1717 settle(&mut world, 0.5);
1718 assert_eq!(
1719 world.is_sleeping(cube),
1720 Some(false),
1721 "the impact should have woken the settled cube"
1722 );
1723
1724 let moved = world.position(cube).expect("cube exists").x;
1726 assert!(moved.abs() > 1e-3, "the cube should have been pushed");
1727 let _ = projectile;
1728 }
1729
1730 #[test]
1731 fn contacts_report_begin_then_persist_then_end() {
1732 let mut world = World::new();
1733 let slab = ground(&mut world, 0.0);
1734 world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0).at(Vec3::new(0.0, 1.2, 0.0)));
1735
1736 let mut saw_begin = false;
1738 for _ in 0..240 {
1739 world.step(1.0 / 120.0);
1740 if world
1741 .contacts()
1742 .iter()
1743 .any(|contact| contact.phase == ContactPhase::Begin)
1744 {
1745 saw_begin = true;
1746 break;
1747 }
1748 }
1749 assert!(saw_begin, "landing must report a Begin phase");
1750
1751 settle(&mut world, 0.5);
1753 assert!(
1754 world
1755 .contacts()
1756 .iter()
1757 .any(|contact| contact.phase == ContactPhase::Persist),
1758 "a resting contact must report Persist"
1759 );
1760
1761 assert!(world.remove_body(slab));
1763 world.step(1.0 / 120.0);
1764 assert!(
1765 world
1766 .contacts()
1767 .iter()
1768 .any(|contact| contact.phase == ContactPhase::End),
1769 "removing the ground must report an End"
1770 );
1771 }
1772
1773 #[test]
1774 fn contacts_are_not_reported_for_bodies_that_are_apart() {
1775 let mut world = World::new();
1776 world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0).at(Vec3::new(0.0, 10.0, 0.0)));
1777 world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0).at(Vec3::new(50.0, 10.0, 0.0)));
1778 world.step(1.0 / 120.0);
1779 assert_eq!(world.contacts(), []);
1780 }
1781
1782 #[test]
1783 fn two_static_bodies_are_never_solved_against_each_other() {
1784 let mut world = World::new();
1785 world.add_body(BodyDesc::stationary(Shape::cuboid(Vec3::splat(1.0))));
1786 world.add_body(
1787 BodyDesc::stationary(Shape::cuboid(Vec3::splat(1.0))).at(Vec3::new(0.5, 0.0, 0.0)),
1788 );
1789 world.step(1.0 / 120.0);
1790 assert_eq!(world.contacts(), []);
1791 }
1792
1793 #[test]
1794 fn a_settled_tower_sleeps_and_then_does_not_move_at_all() {
1795 let mut world = World::new();
1796 ground(&mut world, 0.0);
1797 let mut cubes = alloc::vec::Vec::new();
1798 for level in 0..4 {
1799 cubes.push(
1800 world.add_body(
1801 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
1802 .at(Vec3::new(0.0, 0.5 + level as f32 * 1.02, 0.0))
1803 .with_friction(1.0),
1804 ),
1805 );
1806 }
1807 settle(&mut world, 8.0);
1808 for (index, cube) in cubes.iter().enumerate() {
1809 assert_eq!(
1810 world.is_sleeping(*cube),
1811 Some(true),
1812 "level {index} of a settled tower should sleep"
1813 );
1814 let y = world.position(*cube).expect("cube exists").y;
1815 assert!(
1816 (y - (0.5 + index as f32)).abs() < 0.1,
1817 "level {index} sits at {y}"
1818 );
1819 }
1820
1821 let resting: alloc::vec::Vec<_> = cubes
1824 .iter()
1825 .map(|cube| world.position(*cube).expect("cube exists"))
1826 .collect();
1827 settle(&mut world, 10.0);
1828 for (index, cube) in cubes.iter().enumerate() {
1829 let after = world.position(*cube).expect("cube exists");
1830 assert!(
1831 (after - resting[index]).length() < 1e-5,
1832 "level {index} moved from {:?} to {after:?} after sleeping",
1833 resting[index]
1834 );
1835 }
1836 }
1837
1838 #[test]
1839 fn a_taller_tower_holds_its_height_without_sinking() {
1840 let mut world = World::new();
1846 ground(&mut world, 0.0);
1847 let mut cubes = alloc::vec::Vec::new();
1848 for level in 0..6 {
1849 cubes.push(
1850 world.add_body(
1851 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
1852 .at(Vec3::new(0.0, 0.5 + level as f32 * 1.02, 0.0))
1853 .with_friction(1.0),
1854 ),
1855 );
1856 }
1857 settle(&mut world, 12.0);
1858 for (index, cube) in cubes.iter().enumerate() {
1859 let y = world.position(*cube).expect("cube exists").y;
1860 let expected = 0.5 + index as f32;
1861 assert!(
1862 (y - expected).abs() < 0.1,
1863 "level {index} should rest near {expected}, got {y}"
1864 );
1865 }
1866 }
1867
1868 fn flat_terrain_cells(start_x: usize, cells: usize, step: f32) -> alloc::vec::Vec<Vec3> {
1875 let mut positions = alloc::vec::Vec::new();
1876 for z in 0..cells {
1877 for x in start_x..(start_x + cells) {
1878 let x0 = x as f32 * step;
1879 let x1 = x0 + step;
1880 let z0 = z as f32 * step;
1881 let z1 = z0 + step;
1882 positions.extend([
1883 Vec3::new(x0, 0.0, z0),
1884 Vec3::new(x0, 0.0, z1),
1885 Vec3::new(x1, 0.0, z0),
1886 Vec3::new(x1, 0.0, z0),
1887 Vec3::new(x0, 0.0, z1),
1888 Vec3::new(x1, 0.0, z1),
1889 ]);
1890 }
1891 }
1892 positions
1893 }
1894
1895 fn flat_terrain(cells: usize, step: f32) -> alloc::vec::Vec<Vec3> {
1896 flat_terrain_cells(0, cells, step)
1897 }
1898
1899 #[test]
1900 fn a_body_rests_on_static_terrain() {
1901 let mut world = World::new();
1902 world
1903 .add_static_mesh(&flat_terrain(2, 2.0))
1904 .expect("terrain");
1905 let ball =
1906 world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0).at(Vec3::new(1.0, 3.0, 1.0)));
1907 settle(&mut world, 3.0);
1908 let y = world.position(ball).expect("ball exists").y;
1909 assert!(
1910 (y - 0.5).abs() < 0.1,
1911 "the ball should rest on the terrain, got y={y}"
1912 );
1913 }
1914
1915 #[test]
1916 fn a_contact_with_static_terrain_names_the_mesh() {
1917 let mut world = World::new();
1918 let mesh = world
1919 .add_static_mesh(&flat_terrain(1, 2.0))
1920 .expect("terrain");
1921 world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0).at(Vec3::new(1.0, 0.4, 1.0)));
1922 settle(&mut world, 0.2);
1923 assert!(
1924 world
1925 .contacts()
1926 .iter()
1927 .any(|contact| contact.b == ContactPartner::Static(mesh)),
1928 "the terrain contact must name the mesh"
1929 );
1930 }
1931
1932 #[test]
1933 fn a_box_slides_across_triangle_seams_without_catching() {
1934 let mut world = World::new();
1935 world
1936 .add_static_mesh(&flat_terrain(4, 2.0))
1937 .expect("terrain");
1938 let cube = world.add_body(
1939 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
1940 .at(Vec3::new(0.5, 0.55, 0.5))
1941 .with_friction(0.0)
1942 .with_velocity(Vec3::new(4.0, 0.0, 0.0)),
1943 );
1944 settle(&mut world, 1.0);
1945 let position = world.position(cube).expect("cube exists");
1946 assert!(
1950 position.x > 3.5,
1951 "the box caught on a triangle seam, ended at {position:?}"
1952 );
1953 assert!(
1954 (position.y - 0.5).abs() < 0.1,
1955 "the box sank or floated, ended at {position:?}"
1956 );
1957 }
1958
1959 #[test]
1960 fn a_box_slides_across_a_seam_between_two_separate_meshes() {
1961 let mut world = World::new();
1962 world
1968 .add_static_mesh(&flat_terrain_cells(0, 2, 2.0))
1969 .expect("left tile");
1970 world
1971 .add_static_mesh(&flat_terrain_cells(2, 2, 2.0))
1972 .expect("right tile");
1973 let cube = world.add_body(
1974 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
1975 .at(Vec3::new(0.5, 0.55, 0.5))
1976 .with_friction(0.0)
1977 .with_velocity(Vec3::new(4.0, 0.0, 0.0)),
1978 );
1979 settle(&mut world, 1.5);
1980 let position = world.position(cube).expect("cube exists");
1981 assert!(
1982 position.x > 5.0,
1983 "the box caught on the seam between two meshes, ended at {position:?}"
1984 );
1985 assert!(
1986 (position.y - 0.5).abs() < 0.1,
1987 "the box sank or floated, ended at {position:?}"
1988 );
1989 }
1990
1991 #[test]
1992 fn removing_static_geometry_lets_bodies_fall_through() {
1993 let mut world = World::new();
1994 let mesh = world
1995 .add_static_mesh(&flat_terrain(2, 2.0))
1996 .expect("terrain");
1997 let ball =
1998 world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0).at(Vec3::new(1.0, 0.5, 1.0)));
1999 settle(&mut world, 1.0);
2000 assert!((world.position(ball).expect("ball").y - 0.5).abs() < 0.1);
2001
2002 assert!(world.remove_static_mesh(mesh));
2003 assert!(!world.contains_static_mesh(mesh));
2004 assert_eq!(world.static_mesh_count(), 0);
2005 assert!(!world.remove_static_mesh(mesh), "double removal must fail");
2006 settle(&mut world, 1.0);
2007 assert!(
2008 world.position(ball).expect("ball").y < -1.0,
2009 "the ball should fall once its terrain is gone"
2010 );
2011 }
2012
2013 #[test]
2014 fn removing_one_mesh_releases_only_its_own_edges() {
2015 let mut world = World::new();
2016 world
2017 .add_static_mesh(&flat_terrain_cells(0, 2, 2.0))
2018 .expect("left tile");
2019 let right = world
2020 .add_static_mesh(&flat_terrain_cells(1, 2, 2.0))
2021 .expect("right tile");
2022 let cube = world.add_body(
2027 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
2028 .at(Vec3::new(0.5, 0.55, 0.5))
2029 .with_friction(0.0)
2030 .with_velocity(Vec3::new(4.0, 0.0, 0.0)),
2031 );
2032 settle(&mut world, 0.6);
2033 assert!(
2034 world.position(cube).expect("cube").x > 2.0,
2035 "the box should have crossed the shared boundary"
2036 );
2037
2038 assert!(world.remove_static_mesh(right));
2041 settle(&mut world, 0.6);
2042 assert!(world.position(cube).expect("cube").is_finite());
2043 }
2044
2045 #[test]
2046 fn malformed_static_geometry_is_rejected() {
2047 let mut world = World::new();
2048 assert!(world.add_static_mesh(&[]).is_none(), "empty input");
2049 assert!(
2050 world.add_static_mesh(&[Vec3::ZERO, Vec3::X]).is_none(),
2051 "not a whole number of triangles"
2052 );
2053 assert!(
2054 world
2055 .add_static_mesh(&[Vec3::ZERO, Vec3::X, Vec3::new(f32::NAN, 0.0, 0.0)])
2056 .is_none(),
2057 "a non-finite coordinate"
2058 );
2059 assert_eq!(world.static_mesh_count(), 0);
2060 }
2061
2062 fn ledge_block(overhang: f32, platform_half: Vec3, block_half: f32) -> (Vec3, f32, bool) {
2068 let mut world = World::new();
2069 world.add_body(BodyDesc::stationary(Shape::cuboid(platform_half)).at(Vec3::ZERO));
2070 let centre_x = platform_half.x - block_half + overhang * block_half * 2.0;
2075 let block = world.add_body(
2076 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(block_half)), 1.0)
2077 .at(Vec3::new(
2078 centre_x,
2079 platform_half.y + block_half + 0.02,
2080 0.0,
2081 ))
2082 .with_friction(0.6),
2083 );
2084 for _ in 0..600 {
2085 world.step(1.0 / 120.0);
2086 }
2087 let position = world.position(block).expect("block exists");
2088 let up = world.orientation(block).expect("block exists") * Vec3::Y;
2089 let tilt = up.dot(Vec3::Y).clamp(-1.0, 1.0).acos().to_degrees();
2090 (position, tilt, world.is_sleeping(block) == Some(true))
2091 }
2092
2093 #[test]
2094 fn a_block_tips_off_once_its_centre_passes_the_edge() {
2095 let platform_half = Vec3::new(3.0, 0.5, 3.0);
2102 let block_half = 0.5;
2103 let resting_height = platform_half.y + block_half;
2104
2105 let mut worst_tilt = 0.0_f32;
2106 for overhang in [0.1_f32, 0.2, 0.3, 0.4] {
2107 let (position, tilt, sleeping) = ledge_block(overhang, platform_half, block_half);
2108 assert!(
2109 (position.y - resting_height).abs() < 0.02,
2110 "{overhang} overhang should rest on the ledge, got y={}",
2111 position.y
2112 );
2113 assert!(sleeping, "{overhang} overhang should settle and sleep");
2114 worst_tilt = worst_tilt.max(tilt);
2115 }
2116 assert!(
2120 worst_tilt < 0.01,
2121 "a block resting on a ledge must stay level, it tilted {worst_tilt} deg"
2122 );
2123
2124 for overhang in [0.6_f32, 0.7, 0.8, 0.9] {
2125 let (position, _, _) = ledge_block(overhang, platform_half, block_half);
2126 assert!(
2127 position.y < 0.0,
2128 "{overhang} overhang is past the centre of mass and must fall, got y={}",
2129 position.y
2130 );
2131 }
2132 }
2133
2134 fn edge_pivot_twist(impulse: f32) -> (u32, f32) {
2145 let mut world = World::new();
2146 ground(&mut world, 0.0);
2147 let cube = world.add_body(
2148 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
2149 .at(Vec3::new(0.0, 0.5, 0.0))
2150 .with_friction(1.0),
2151 );
2152 settle(&mut world, 2.0);
2153 world.apply_angular_impulse(cube, Vec3::new(0.0, 0.0, impulse));
2154 let mut edge_steps = 0;
2155 let mut worst_twist = 0.0_f32;
2156 for _ in 0..2400 {
2157 world.step(1.0 / 120.0);
2158 let touching = world
2159 .contacts()
2160 .iter()
2161 .filter(|contact| contact.involves(cube))
2162 .count();
2163 if touching == 2 {
2164 edge_steps += 1;
2165 let spin = world.angular_velocity(cube).expect("the cube exists");
2166 worst_twist = worst_twist.max(spin.x.abs());
2167 }
2168 }
2169 (edge_steps, worst_twist)
2170 }
2171
2172 #[test]
2173 fn a_cube_pivoting_on_its_edge_does_not_twist_about_the_edge() {
2174 let (edge_steps, twist) = edge_pivot_twist(0.6);
2192 assert!(
2193 edge_steps > 10,
2194 "the cube must actually pivot on an edge, it did for {edge_steps} steps"
2195 );
2196 assert!(
2197 twist < 1e-5,
2198 "the cube twisted {twist} rad/s about the edge it was pivoting on"
2199 );
2200 }
2201
2202 #[test]
2203 fn a_block_sliding_towards_a_ledge_leaves_it_instead_of_hanging() {
2204 let mut world = World::new();
2217 world.add_body(
2218 BodyDesc::stationary(Shape::cuboid(Vec3::new(3.0, 0.5, 3.0)))
2219 .at(Vec3::ZERO)
2220 .with_friction(0.02),
2221 );
2222 let block = world.add_body(
2223 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
2224 .at(Vec3::new(0.0, 1.02, 0.0))
2225 .with_friction(0.02)
2226 .with_velocity(Vec3::new(3.0, 0.0, 0.0)),
2227 );
2228
2229 let mut left_the_ledge = false;
2230 for _ in 0..600 {
2231 world.step(1.0 / 120.0);
2232 let position = world.position(block).expect("block exists");
2233 if position.x > 3.0 && position.y < 0.0 {
2234 left_the_ledge = true;
2235 break;
2236 }
2237 }
2238 assert!(
2239 left_the_ledge,
2240 "the block should have slid off the ledge, ended at {:?}",
2241 world.position(block).expect("block exists")
2242 );
2243 }
2244
2245 fn spanning_plank(gap: f32, plank_half: f32, platform_half: f32) -> (f32, f32, bool) {
2249 let mut world = World::new();
2250 let offset = gap / 2.0 + platform_half;
2251 for side in [-1.0_f32, 1.0] {
2252 world.add_body(
2253 BodyDesc::stationary(Shape::cuboid(Vec3::new(platform_half, 0.5, 3.0))).at(Vec3::new(
2254 side * offset,
2255 0.0,
2256 0.0,
2257 )),
2258 );
2259 }
2260 let plank = world.add_body(
2261 BodyDesc::dynamic(Shape::cuboid(Vec3::new(plank_half, 0.25, 0.5)), 1.0)
2262 .at(Vec3::new(0.0, 0.5 + 0.25 + 0.02, 0.0))
2263 .with_friction(0.8),
2264 );
2265 for _ in 0..600 {
2266 world.step(1.0 / 120.0);
2267 }
2268 let y = world.position(plank).expect("plank exists").y;
2269 let up = world.orientation(plank).expect("plank exists") * Vec3::Y;
2270 let tilt = up.dot(Vec3::Y).clamp(-1.0, 1.0).acos().to_degrees();
2271 (y, tilt, world.is_sleeping(plank) == Some(true))
2272 }
2273
2274 #[test]
2275 fn a_plank_spanning_a_gap_rests_level_until_the_gap_outgrows_it() {
2276 let plank_half = 0.9;
2282 let platform_half = 1.5;
2283 let resting = 0.5 + 0.25;
2284 for gap in [0.4_f32, 0.8, 1.2, 1.6, 1.7] {
2285 let (y, tilt, sleeping) = spanning_plank(gap, plank_half, platform_half);
2286 assert!(
2287 (y - resting).abs() < 0.02,
2288 "gap {gap} should rest at {resting}, got y={y}"
2289 );
2290 assert!(tilt < 0.01, "gap {gap} tilted the plank by {tilt} deg");
2291 assert!(sleeping, "gap {gap} should settle and sleep");
2292 }
2293 let (y, _, _) = spanning_plank(1.9, plank_half, platform_half);
2296 assert!(
2297 y < 0.0,
2298 "a gap wider than the plank must let it through, got y={y}"
2299 );
2300 }
2301
2302 #[test]
2303 fn a_sphere_rests_on_a_ledge_up_to_the_edge_and_falls_past_it() {
2304 let radius = 0.4;
2310 let resting = 0.5 + radius;
2311 for centre_x in [2.0_f32, 2.6, 2.99] {
2312 let mut world = World::new();
2313 world
2314 .add_body(BodyDesc::stationary(Shape::cuboid(Vec3::new(3.0, 0.5, 3.0))).at(Vec3::ZERO));
2315 let ball = world.add_body(
2316 BodyDesc::dynamic(Shape::sphere(radius), 1.0)
2317 .at(Vec3::new(centre_x, resting + 0.02, 0.0))
2318 .with_friction(0.6),
2319 );
2320 for _ in 0..600 {
2321 world.step(1.0 / 120.0);
2322 }
2323 let y = world.position(ball).expect("ball exists").y;
2324 assert!(
2325 (y - resting).abs() < 0.02,
2326 "centre at {centre_x} is over the ledge and should rest, got y={y}"
2327 );
2328 assert_eq!(world.is_sleeping(ball), Some(true));
2329 }
2330
2331 let mut world = World::new();
2332 world.add_body(BodyDesc::stationary(Shape::cuboid(Vec3::new(3.0, 0.5, 3.0))).at(Vec3::ZERO));
2333 let ball = world.add_body(
2334 BodyDesc::dynamic(Shape::sphere(radius), 1.0)
2335 .at(Vec3::new(3.01, resting + 0.02, 0.0))
2336 .with_friction(0.6),
2337 );
2338 for _ in 0..600 {
2339 world.step(1.0 / 120.0);
2340 }
2341 assert!(
2342 world.position(ball).expect("ball exists").y < 0.0,
2343 "a sphere whose centre is past the edge must fall"
2344 );
2345 }
2346
2347 #[test]
2348 fn a_sphere_rolling_across_mesh_seams_keeps_its_speed() {
2349 let mut world = World::new();
2359 world
2360 .add_static_mesh(&flat_terrain_cells(0, 2, 2.0))
2361 .expect("left tile");
2362 world
2363 .add_static_mesh(&flat_terrain_cells(2, 2, 2.0))
2364 .expect("right tile");
2365 let ball = world.add_body(
2366 BodyDesc::dynamic(Shape::sphere(0.4), 1.0)
2367 .at(Vec3::new(0.5, 0.4005, 0.5))
2368 .with_friction(0.0)
2369 .with_velocity(Vec3::new(4.0, 0.0, 0.0)),
2370 );
2371
2372 let mut lowest = f32::MAX;
2373 for _ in 0..180 {
2374 world.step(1.0 / 120.0);
2375 lowest = lowest.min(world.position(ball).expect("ball exists").y);
2376 }
2377 let velocity = world.velocity(ball).expect("ball exists");
2378 assert!(
2379 (velocity.x - 4.0).abs() < 0.05,
2380 "the seam stole horizontal speed, vx={}",
2381 velocity.x
2382 );
2383 assert!(
2384 lowest > 0.39,
2385 "the ball dipped into the seam, lowest y={lowest}"
2386 );
2387 }
2388
2389 fn corner_slide(centre: f32) -> (Vec3, f32) {
2396 let mut world = World::new();
2397 world.add_body(BodyDesc::stationary(Shape::cuboid(Vec3::new(3.0, 0.5, 3.0))).at(Vec3::ZERO));
2398 let block = world.add_body(
2399 BodyDesc::dynamic(Shape::cuboid(Vec3::new(0.5, 0.25, 0.5)), 1.0)
2400 .at(Vec3::new(centre, 0.5 + 0.25 + 0.02, centre))
2401 .with_friction(0.7),
2402 );
2403 for _ in 0..900 {
2404 world.step(1.0 / 120.0);
2405 }
2406 let position = world.position(block).expect("block exists");
2407 let up = world.orientation(block).expect("block exists") * Vec3::Y;
2408 (
2409 position,
2410 up.dot(Vec3::Y).clamp(-1.0, 1.0).acos().to_degrees(),
2411 )
2412 }
2413
2414 #[test]
2415 fn a_box_leaves_a_platform_corner_once_its_centre_passes_it() {
2416 let resting = 0.5 + 0.25;
2430 for centre in [1.0_f32, 2.0, 2.5, 2.9, 2.99] {
2431 let (position, tilt) = corner_slide(centre);
2432 assert!(
2433 (position.y - resting).abs() < 0.02,
2434 "centre {centre} is over the platform and should rest, got y={}",
2435 position.y
2436 );
2437 assert!(
2438 tilt < 1.0,
2439 "centre {centre} should stay level, tilted {tilt} deg"
2440 );
2441 }
2442
2443 for centre in [3.2_f32, 3.4, 4.0] {
2444 let (position, _) = corner_slide(centre);
2445 assert!(
2446 position.y < 0.0,
2447 "centre {centre} is past the platform corner and must fall, got y={}",
2448 position.y
2449 );
2450 }
2451 }
2452
2453 #[test]
2454 fn a_stack_pushed_over_a_ledge_leaves_no_body_hanging() {
2455 for friction in [0.05_f32, 0.8] {
2468 let mut world = World::new();
2469 world.add_body(
2470 BodyDesc::stationary(Shape::cuboid(Vec3::new(4.0, 0.5, 3.0)))
2471 .at(Vec3::ZERO)
2472 .with_friction(friction),
2473 );
2474 let mut stack = alloc::vec::Vec::new();
2475 for level in 0..4 {
2476 stack.push(
2477 world.add_body(
2478 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
2479 .at(Vec3::new(2.0, 0.5 + level as f32 * 1.01, 0.0))
2480 .with_friction(friction)
2481 .with_velocity(Vec3::new(2.0, 0.0, 0.0)),
2482 ),
2483 );
2484 }
2485 settle(&mut world, 10.0);
2486
2487 let mut left_the_platform = 0;
2488 for (level, body) in stack.iter().enumerate() {
2489 let position = world.position(*body).expect("body exists");
2490 if position.y < 0.0 {
2491 left_the_platform += 1;
2492 }
2493 assert!(
2494 position.y < 0.0 || position.x.abs() <= 4.0,
2495 "friction {friction}: level {level} is hanging past the ledge at {position:?}"
2496 );
2497 }
2498 assert!(
2499 left_the_platform > 0,
2500 "friction {friction}: the stack never reached the ledge, so nothing was tested"
2501 );
2502 }
2503 }
2504
2505 struct SlabOutcome {
2508 slab_y: f32,
2509 slab_tilt: f32,
2510 box_y: f32,
2511 box_slept: bool,
2512 }
2513
2514 fn loaded_slab(box_x: f32, box_mass: f32, friction: f32) -> SlabOutcome {
2526 let mut world = World::new();
2527 world.add_body(
2528 BodyDesc::stationary(Shape::cuboid(Vec3::new(2.0, 0.5, 3.0)))
2529 .at(Vec3::new(-2.0, -0.5, 0.0))
2530 .with_friction(friction),
2531 );
2532 let slab = world.add_body(
2533 BodyDesc::dynamic(Shape::cuboid(Vec3::new(2.0, 0.1, 1.0)), 1.0)
2534 .at(Vec3::new(-0.5, 0.1 + 0.02, 0.0))
2535 .with_friction(friction),
2536 );
2537 let box_body = world.add_body(
2538 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.4)), box_mass)
2539 .at(Vec3::new(box_x, 0.2 + 0.4 + 0.04, 0.0))
2540 .with_friction(friction),
2541 );
2542 settle(&mut world, 10.0);
2543
2544 let slab_position = world.position(slab).expect("slab exists");
2545 let box_position = world.position(box_body).expect("box exists");
2546 SlabOutcome {
2547 slab_y: slab_position.y,
2548 slab_tilt: {
2549 let up = world.orientation(slab).expect("slab exists") * Vec3::Y;
2550 up.dot(Vec3::Y).clamp(-1.0, 1.0).acos().to_degrees()
2551 },
2552 box_y: box_position.y,
2553 box_slept: world.is_sleeping(box_body) == Some(true),
2554 }
2555 }
2556
2557 #[test]
2558 fn a_loaded_dynamic_slab_tips_by_its_combined_centre_of_mass() {
2559 let combined = |box_x: f32, mass: f32| (1.0 * -0.5 + mass * box_x) / (1.0 + mass);
2572
2573 let stable = [
2574 (-0.5_f32, 1.0_f32),
2575 (0.0, 1.0),
2576 (0.4, 1.0),
2577 (0.0, 3.0),
2578 (0.1, 3.0),
2579 ];
2580 for (box_x, mass) in stable {
2581 let centre = combined(box_x, mass);
2582 assert!(centre < 0.0, "the stable case must be inside the ledge");
2583 let outcome = loaded_slab(box_x, mass, 0.6);
2584 assert!(
2585 outcome.slab_y > -0.5,
2586 "combined centre {centre} is over the ledge, so the slab should stand; got slab y={}",
2587 outcome.slab_y
2588 );
2589 assert!(
2590 outcome.box_y > 0.0,
2591 "the box rode a standing slab, so it should still be up; got y={}",
2592 outcome.box_y
2593 );
2594 assert!(
2595 outcome.slab_tilt < 2.0,
2596 "a standing slab should stay level, tilted {} deg",
2597 outcome.slab_tilt
2598 );
2599 if centre < -0.2 {
2603 assert!(
2604 outcome.box_slept,
2605 "a box riding a settled slab should sleep, centre was {centre}"
2606 );
2607 }
2608 }
2609
2610 let tipping = [(0.6_f32, 1.0_f32), (1.0, 1.0), (1.4, 1.0), (0.3, 3.0)];
2611 for (box_x, mass) in tipping {
2612 let centre = combined(box_x, mass);
2613 assert!(centre > 0.0, "the tipping case must be past the ledge");
2614 let outcome = loaded_slab(box_x, mass, 0.6);
2615 assert!(
2616 outcome.slab_y < -1.0,
2617 "combined centre {centre} is past the ledge, so the slab should go over; got slab y={}",
2618 outcome.slab_y
2619 );
2620 assert!(
2621 outcome.slab_tilt > 5.0,
2622 "the slab should have rotated rather than slid off, tilted only {} deg",
2623 outcome.slab_tilt
2624 );
2625 assert!(
2627 outcome.box_y < 0.0,
2628 "the box should have followed the slab down, got y={}",
2629 outcome.box_y
2630 );
2631 }
2632 }
2633
2634 #[test]
2637 fn gravity_scale_decides_how_much_gravity_reaches_a_body() {
2638 let mut world = World::new();
2642 let normal =
2643 world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0).at(Vec3::new(0.0, 10.0, 0.0)));
2644 let weightless = world.add_body(
2645 BodyDesc::dynamic(Shape::sphere(0.5), 1.0)
2646 .at(Vec3::new(10.0, 10.0, 0.0))
2647 .with_gravity_scale(0.0),
2648 );
2649 let heavy = world.add_body(
2650 BodyDesc::dynamic(Shape::sphere(0.5), 1.0)
2651 .at(Vec3::new(20.0, 10.0, 0.0))
2652 .with_gravity_scale(2.0),
2653 );
2654
2655 world.step(1.0 / 120.0);
2656 let vertical = |body| world.velocity(body).expect("body exists").y;
2657 let one_step = DEFAULT_GRAVITY.y / 120.0;
2658
2659 assert!(
2660 vertical(weightless).abs() < 1e-6,
2661 "a zero scale must ignore gravity entirely, got {}",
2662 vertical(weightless)
2663 );
2664 assert!(
2665 (vertical(normal) - one_step).abs() < 1e-4,
2666 "the default must be full gravity, got {}",
2667 vertical(normal)
2668 );
2669 assert!(
2670 (vertical(heavy) - 2.0 * one_step).abs() < 1e-4,
2671 "the scale must multiply, got {}",
2672 vertical(heavy)
2673 );
2674 }
2675
2676 #[test]
2677 fn gravity_scale_can_be_changed_on_a_live_body() {
2678 let mut world = World::new();
2684 let body =
2685 world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0).at(Vec3::new(0.0, 10.0, 0.0)));
2686
2687 assert!(world.set_gravity_scale(body, 0.0));
2688 for _ in 0..600 {
2689 world.step(1.0 / 120.0);
2690 }
2691 let held = world.position(body).expect("body exists").y;
2692 assert!(
2693 (held - 10.0).abs() < 1e-3,
2694 "a zero scale must hover, got y={held}"
2695 );
2696 assert_eq!(world.is_sleeping(body), Some(true), "it should have slept");
2697
2698 assert!(world.set_gravity_scale(body, 1.0));
2699 assert_eq!(
2700 world.is_sleeping(body),
2701 Some(false),
2702 "changing the scale must wake the Body"
2703 );
2704 world.step(1.0 / 120.0);
2705 assert!(
2706 world.velocity(body).expect("body exists").y < 0.0,
2707 "gravity must reach it again"
2708 );
2709
2710 let dead = world.add_body(BodyDesc::dynamic(Shape::sphere(0.5), 1.0));
2711 assert!(world.remove_body(dead));
2712 assert!(
2713 !world.set_gravity_scale(dead, 1.0),
2714 "a dead handle must be rejected"
2715 );
2716 }
2717
2718 #[test]
2719 fn a_negative_or_non_finite_gravity_scale_is_clamped_to_zero() {
2720 let mut world = World::new();
2724 let negative = world.add_body(
2725 BodyDesc::dynamic(Shape::sphere(0.5), 1.0)
2726 .at(Vec3::new(0.0, 10.0, 0.0))
2727 .with_gravity_scale(-1.0),
2728 );
2729 let nan = world.add_body(
2730 BodyDesc::dynamic(Shape::sphere(0.5), 1.0)
2731 .at(Vec3::new(10.0, 10.0, 0.0))
2732 .with_gravity_scale(f32::NAN),
2733 );
2734
2735 world.step(1.0 / 120.0);
2736 for body in [negative, nan] {
2737 let velocity = world.velocity(body).expect("body exists");
2738 assert!(
2739 velocity.is_finite() && velocity.y.abs() < 1e-6,
2740 "expected no gravity, got {velocity:?}"
2741 );
2742 }
2743 }
2744
2745 #[test]
2748 fn disturbing_one_member_wakes_the_whole_island() {
2749 let mut world = World::new();
2750 ground(&mut world, 0.0);
2751 let mut cubes = alloc::vec::Vec::new();
2752 for level in 0..4 {
2753 cubes.push(
2754 world.add_body(
2755 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
2756 .at(Vec3::new(0.0, 0.5 + level as f32 * 1.01, 0.0))
2757 .with_friction(0.9),
2758 ),
2759 );
2760 }
2761 settle(&mut world, 8.0);
2762 for cube in &cubes {
2763 assert_eq!(
2764 world.is_sleeping(*cube),
2765 Some(true),
2766 "the stack should be asleep before it is disturbed"
2767 );
2768 }
2769
2770 assert!(world.apply_impulse(cubes[0], Vec3::new(3.0, 0.0, 0.0)));
2775 world.step(1.0 / 120.0);
2776 for (index, cube) in cubes.iter().enumerate() {
2777 assert_eq!(
2778 world.is_sleeping(*cube),
2779 Some(false),
2780 "level {index} of the Island should have woken"
2781 );
2782 }
2783 }
2784
2785 #[test]
2786 fn an_island_sleeps_as_a_unit() {
2787 let mut world = World::new();
2788 ground(&mut world, 0.0);
2789 let mut cubes = alloc::vec::Vec::new();
2790 for level in 0..5 {
2791 cubes.push(
2792 world.add_body(
2793 BodyDesc::dynamic(Shape::cuboid(Vec3::splat(0.5)), 1.0)
2794 .at(Vec3::new(0.0, 0.5 + level as f32 * 1.01, 0.0))
2795 .with_friction(0.9),
2796 ),
2797 );
2798 }
2799 let mut slept = false;
2802 for step in 0..1200 {
2803 world.step(1.0 / 120.0);
2804 let asleep = cubes
2805 .iter()
2806 .filter(|cube| world.is_sleeping(**cube) == Some(true))
2807 .count();
2808 assert!(
2809 asleep == 0 || asleep == cubes.len(),
2810 "the Island must sleep as a unit, {asleep} of {} were asleep at step {step}",
2811 cubes.len()
2812 );
2813 if asleep == cubes.len() {
2814 slept = true;
2815 break;
2816 }
2817 }
2818 assert!(slept, "the stack never slept");
2819 }
2820}