1use super::*;
2
3impl Default for BodyCollider {
5 fn default() -> BodyCollider {
11 BodyCollider::Aabb(AabbCollider::default())
12 }
13}
14
15impl Default for BodyCollider3D {
17 fn default() -> BodyCollider3D {
23 BodyCollider3D::Aabb(AabbCollider3D::default())
24 }
25}
26
27impl Default for PhysicsConfig {
29 fn default() -> PhysicsConfig {
35 PhysicsConfig::new(
36 Vector2D::new(0.0, DEFAULT_GRAVITY),
37 DEFAULT_LINEAR_DAMPING,
38 DEFAULT_ANGULAR_DAMPING,
39 )
40 }
41}
42
43impl RigidBody2D {
45 pub fn new_dynamic(id: u64, position: Vector2D) -> RigidBody2D {
56 let mass: f64 = PHYSICS_DEFAULT_MASS;
57 RigidBody2D::new(
58 id,
59 position,
60 mass,
61 1.0 / mass,
62 DEFAULT_RESTITUTION,
63 DEFAULT_FRICTION,
64 BodyType::Dynamic,
65 )
66 }
67
68 pub fn new_static(id: u64, position: Vector2D) -> RigidBody2D {
79 RigidBody2D::new(
80 id,
81 position,
82 PHYSICS_STATIC_MASS,
83 0.0,
84 DEFAULT_RESTITUTION,
85 DEFAULT_FRICTION,
86 BodyType::Static,
87 )
88 }
89
90 pub fn apply_force(&mut self, force: Vector2D) {
96 *self.get_mut_force_accumulator() += force;
97 }
98
99 pub fn apply_impulse(&mut self, impulse: Vector2D) {
105 let inverse_mass: f64 = self.get_inverse_mass();
106 if inverse_mass == 0.0 {
107 return;
108 }
109 *self.get_mut_velocity() += impulse.scaled(inverse_mass);
110 }
111
112 pub fn update_mass(&mut self, mass: f64) {
119 self.set_mass(mass);
120 self.set_inverse_mass(if mass > 0.0 { 1.0 / mass } else { 0.0 });
121 }
122
123 pub fn is_dynamic(&self) -> bool {
129 self.get_body_type() == BodyType::Dynamic
130 }
131
132 pub fn update_collider(&mut self, collider: BodyCollider) {
138 self.set_collider(Some(collider));
139 }
140
141 pub fn bounding_box(&self) -> Option<Rect> {
147 let collider: Option<BodyCollider> = self.get_collider();
148 match collider? {
149 BodyCollider::Aabb(aabb) => {
150 let aabb_rect: Rect = aabb.get_rect();
151 let mut offset_rect: Rect = aabb_rect;
152 offset_rect.set_x(
153 offset_rect.get_x() + self.get_position().get_x() - aabb_rect.get_width() * 0.5,
154 );
155 offset_rect.set_y(
156 offset_rect.get_y() + self.get_position().get_y()
157 - aabb_rect.get_height() * 0.5,
158 );
159 Some(offset_rect)
160 }
161 BodyCollider::Circle(circle) => {
162 let diameter: f64 = circle.get_circle().get_radius() * 2.0;
163 Some(Rect::from_center(self.get_position(), diameter, diameter))
164 }
165 }
166 }
167}
168
169impl PhysicsWorld2D {
171 pub fn with_config(config: PhysicsConfig) -> PhysicsWorld2D {
181 let mut world: PhysicsWorld2D = PhysicsWorld2D::new(config);
182 world.set_grid(SpatialHashGrid2D::with_default_size());
183 world
184 }
185
186 pub fn add_body(&mut self, body: RigidBody2D) {
192 self.get_mut_bodies().push(body);
193 }
194
195 pub fn remove_body(&mut self, id: u64) {
201 self.get_mut_bodies()
202 .retain(|body: &RigidBody2D| body.get_id() != id);
203 }
204
205 pub fn get_body(&self, id: u64) -> Option<&RigidBody2D> {
215 self.get_bodies()
216 .iter()
217 .find(|body: &&RigidBody2D| body.get_id() == id)
218 }
219
220 pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody2D> {
230 self.get_mut_bodies()
231 .iter_mut()
232 .find(|body: &&mut RigidBody2D| body.get_id() == id)
233 }
234}
235
236impl Default for PhysicsWorld2D {
238 fn default() -> PhysicsWorld2D {
244 PhysicsWorld2D::with_config(PhysicsConfig::default())
245 }
246}
247
248impl RigidBody2D {
250 fn check_collision_with(&self, other: &RigidBody2D) -> Option<CollisionResult> {
260 let a_bbox: Rect = self.bounding_box()?;
261 let b_bbox: Rect = other.bounding_box()?;
262 if !Rect::broad_phase_alias(a_bbox, b_bbox) {
263 return None;
264 }
265 let self_collider: Option<BodyCollider> = self.get_collider();
266 let other_collider: Option<BodyCollider> = other.get_collider();
267 let position_delta: Vector2D = other.get_position() - self.get_position();
268 match (self_collider, other_collider) {
269 (Some(BodyCollider::Aabb(aabb_a)), Some(BodyCollider::Aabb(aabb_b))) => {
270 let aabb_b_rect: Rect = aabb_b.get_rect();
271 let offset_aabb_b: AabbCollider = AabbCollider::new(Rect::new(
272 aabb_b_rect.get_x() + position_delta.get_x(),
273 aabb_b_rect.get_y() + position_delta.get_y(),
274 aabb_b_rect.get_width(),
275 aabb_b_rect.get_height(),
276 ));
277 aabb_a.collide_with_aabb(&offset_aabb_b)
278 }
279 (Some(BodyCollider::Circle(circle_a)), Some(BodyCollider::Circle(circle_b))) => {
280 let circle_b_inner: Circle = circle_b.get_circle();
281 let offset_circle_b: CircleCollider = CircleCollider::new(Circle::new(
282 circle_b_inner.get_center() + position_delta,
283 circle_b_inner.get_radius(),
284 ));
285 circle_a.collide_with_circle(&offset_circle_b)
286 }
287 (Some(BodyCollider::Aabb(aabb)), Some(BodyCollider::Circle(circle))) => {
288 let circle_inner: Circle = circle.get_circle();
289 let offset_circle: CircleCollider = CircleCollider::new(Circle::new(
290 circle_inner.get_center() + position_delta,
291 circle_inner.get_radius(),
292 ));
293 aabb.collide_with_circle(&offset_circle)
294 }
295 (Some(BodyCollider::Circle(circle)), Some(BodyCollider::Aabb(aabb))) => {
296 let aabb_rect: Rect = aabb.get_rect();
297 let offset_aabb: AabbCollider = AabbCollider::new(Rect::new(
298 aabb_rect.get_x() + position_delta.get_x(),
299 aabb_rect.get_y() + position_delta.get_y(),
300 aabb_rect.get_width(),
301 aabb_rect.get_height(),
302 ));
303 offset_aabb
304 .collide_with_circle(&circle)
305 .map(|mut result: CollisionResult| {
306 result.set_normal(-result.get_normal());
307 result
308 })
309 }
310 _ => None,
311 }
312 }
313
314 fn resolve_collision_with(&mut self, other: &mut RigidBody2D, result: &CollisionResult) {
322 let self_inverse_mass: f64 = self.get_inverse_mass();
323 let other_inverse_mass: f64 = other.get_inverse_mass();
324 let relative_velocity: Vector2D = other.get_velocity() - self.get_velocity();
325 let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
326 if velocity_along_normal > 0.0 {
327 return;
328 }
329 let restitution: f64 = self.get_restitution().min(other.get_restitution());
330 let inverse_mass_sum: f64 = self_inverse_mass + other_inverse_mass;
331 if inverse_mass_sum == 0.0 {
332 return;
333 }
334 let impulse_magnitude: f64 =
335 -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum;
336 let impulse: Vector2D = result.get_normal().scaled(impulse_magnitude);
337 *self.get_mut_velocity() -= impulse.scaled(self_inverse_mass);
338 *other.get_mut_velocity() += impulse.scaled(other_inverse_mass);
339 let correction: Vector2D = result
340 .get_normal()
341 .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
342 *self.get_mut_position() -= correction.scaled(self_inverse_mass);
343 *other.get_mut_position() += correction.scaled(other_inverse_mass);
344 }
345}
346
347impl PhysicsWorld2D {
349 pub fn step(&mut self, delta_time: f64) {
358 let config: PhysicsConfig = self.get_config();
359 let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
361 let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
362 let gravity: Vector2D = config.get_gravity();
363 for body in self.get_mut_bodies() {
364 if !body.is_dynamic() {
365 continue;
366 }
367 let body_mass: f64 = body.get_mass();
368 let body_inverse_mass: f64 = body.get_inverse_mass();
369 *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
370 let force: Vector2D = body.get_force_accumulator();
371 *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
372 *body.get_mut_velocity() *= damping_factor;
374 let current_velocity: Vector2D = body.get_velocity();
375 *body.get_mut_position() += current_velocity.scaled(delta_time);
376 body.set_force_accumulator(Vector2D::zero());
377 *body.get_mut_angular_velocity() *= angular_damping;
378 let current_angular_velocity: f64 = body.get_angular_velocity();
379 *body.get_mut_rotation() += current_angular_velocity * delta_time;
380 }
381 self.resolve_collisions();
382 }
383
384 fn resolve_collisions(&mut self) {
390 let body_count: usize = self.get_bodies().len();
391 if body_count < 2 {
392 return;
393 }
394 let mut pairs: Vec<(usize, usize)> = Vec::new();
399 {
400 let (bodies, grid, query_buffer, query_seen) = (
401 &self.bodies,
402 &mut self.grid,
403 &mut self.query_buffer,
404 &mut self.query_seen,
405 );
406 grid.clear();
407 for (index, body) in bodies.iter().enumerate() {
408 if let Some(bbox) = body.bounding_box() {
409 grid.insert(index, bbox.min(), bbox.max());
410 }
411 }
412 for (i, body) in bodies.iter().enumerate() {
413 let Some(bbox) = body.bounding_box() else {
414 continue;
415 };
416 grid.query_into(bbox.min(), bbox.max(), query_buffer, query_seen);
417 for &j in query_buffer.iter() {
418 if j > i {
419 pairs.push((i, j));
420 }
421 }
422 }
423 }
424 for iteration in 0..PHYSICS_MAX_ITERATIONS {
425 let mut any_collision: bool = false;
426 for &(i, j) in pairs.iter() {
427 let (left, right) = self.get_mut_bodies().split_at_mut(j);
428 let body_a: &mut RigidBody2D = &mut left[i];
429 let body_b: &mut RigidBody2D = &mut right[0];
430 if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
431 continue;
432 }
433 if let Some(result) = body_a.check_collision_with(body_b) {
434 body_a.resolve_collision_with(body_b, &result);
435 any_collision = true;
436 }
437 }
438 if !any_collision {
439 break;
440 }
441 let _: u32 = iteration;
442 }
443 }
444}
445
446impl Updatable for PhysicsWorld2D {
453 fn update(&mut self, delta_time: f64) {
459 PhysicsWorld2D::step(self, delta_time);
460 }
461}
462
463impl Default for PhysicsConfig3D {
465 fn default() -> PhysicsConfig3D {
471 PhysicsConfig3D::new(
472 Vector3D::new(0.0, DEFAULT_GRAVITY_3D, 0.0),
473 DEFAULT_LINEAR_DAMPING,
474 DEFAULT_ANGULAR_DAMPING,
475 )
476 }
477}
478
479impl RigidBody3D {
481 pub fn new_dynamic(id: u64, position: Vector3D) -> RigidBody3D {
492 let mass: f64 = PHYSICS_DEFAULT_MASS;
493 let mut body: RigidBody3D = RigidBody3D::new(
494 id,
495 position,
496 mass,
497 1.0 / mass,
498 DEFAULT_RESTITUTION,
499 DEFAULT_FRICTION,
500 BodyType::Dynamic,
501 );
502 body.update_inertia(mass);
503 body
504 }
505
506 pub fn new_static(id: u64, position: Vector3D) -> RigidBody3D {
517 RigidBody3D::new(
518 id,
519 position,
520 PHYSICS_STATIC_MASS,
521 0.0,
522 DEFAULT_RESTITUTION,
523 DEFAULT_FRICTION,
524 BodyType::Static,
525 )
526 }
527
528 pub fn apply_force(&mut self, force: Vector3D) {
534 *self.get_mut_force_accumulator() += force;
535 }
536
537 pub fn apply_torque(&mut self, torque: Vector3D) {
543 *self.get_mut_torque_accumulator() += torque;
544 }
545
546 pub fn apply_impulse(&mut self, impulse: Vector3D) {
552 let inverse_mass: f64 = self.get_inverse_mass();
553 if inverse_mass == 0.0 {
554 return;
555 }
556 *self.get_mut_velocity() += impulse.scaled(inverse_mass);
557 }
558
559 pub fn update_mass(&mut self, mass: f64) {
566 self.set_mass(mass);
567 self.set_inverse_mass(if mass > 0.0 { 1.0 / mass } else { 0.0 });
568 }
569
570 pub fn update_inertia(&mut self, inertia: f64) {
577 self.set_inverse_inertia(if inertia > 0.0 { 1.0 / inertia } else { 0.0 });
578 }
579
580 pub fn is_dynamic(&self) -> bool {
586 self.get_body_type() == BodyType::Dynamic
587 }
588
589 pub fn update_collider(&mut self, collider: BodyCollider3D) {
595 self.set_collider(Some(collider));
596 }
597
598 pub fn bounding_box(&self) -> Option<AABB3D> {
604 let collider: Option<BodyCollider3D> = self.get_collider();
605 let position: Vector3D = self.get_position();
606 match collider? {
607 BodyCollider3D::Aabb(aabb) => {
608 let center: Vector3D = aabb.get_aabb().center();
609 let size: Vector3D = aabb.get_aabb().size();
610 Some(AABB3D::from_center(
611 position + center,
612 size.get_x(),
613 size.get_y(),
614 size.get_z(),
615 ))
616 }
617 BodyCollider3D::Sphere(sphere) => {
618 let sphere_inner: Sphere = sphere.get_sphere();
619 let diameter: f64 = sphere_inner.get_radius() * 2.0;
620 Some(AABB3D::from_center(
621 position + sphere_inner.get_center(),
622 diameter,
623 diameter,
624 diameter,
625 ))
626 }
627 }
628 }
629}
630
631impl PhysicsWorld3D {
633 pub fn with_config(config: PhysicsConfig3D) -> PhysicsWorld3D {
643 let mut world: PhysicsWorld3D = PhysicsWorld3D::new(config);
644 world.set_grid(SpatialHashGrid3D::with_default_size());
645 world
646 }
647
648 pub fn add_body(&mut self, body: RigidBody3D) {
654 self.get_mut_bodies().push(body);
655 }
656
657 pub fn remove_body(&mut self, id: u64) {
663 self.get_mut_bodies()
664 .retain(|body: &RigidBody3D| body.get_id() != id);
665 }
666
667 pub fn get_body(&self, id: u64) -> Option<&RigidBody3D> {
677 self.get_bodies()
678 .iter()
679 .find(|body: &&RigidBody3D| body.get_id() == id)
680 }
681
682 pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody3D> {
692 self.get_mut_bodies()
693 .iter_mut()
694 .find(|body: &&mut RigidBody3D| body.get_id() == id)
695 }
696
697 pub fn step(&mut self, delta_time: f64) {
706 let config: PhysicsConfig3D = self.get_config();
707 let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
709 let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
710 let gravity: Vector3D = config.get_gravity();
711 for body in self.get_mut_bodies() {
712 if !body.is_dynamic() {
713 continue;
714 }
715 let body_mass: f64 = body.get_mass();
716 let body_inverse_mass: f64 = body.get_inverse_mass();
717 *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
718 let force: Vector3D = body.get_force_accumulator();
719 *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
720 *body.get_mut_velocity() *= damping_factor;
722 let current_velocity: Vector3D = body.get_velocity();
723 *body.get_mut_position() += current_velocity.scaled(delta_time);
724 body.set_force_accumulator(Vector3D::zero());
725 *body.get_mut_angular_velocity() *= angular_damping;
726 let body_inverse_inertia: f64 = body.get_inverse_inertia();
727 let torque: Vector3D = body.get_torque_accumulator();
728 *body.get_mut_angular_velocity() += torque.scaled(body_inverse_inertia * delta_time);
729 let angular_velocity: Vector3D = body.get_angular_velocity();
730 let rotation_delta: Quaternion = Quaternion::new(
731 angular_velocity.get_x() * delta_time * 0.5,
732 angular_velocity.get_y() * delta_time * 0.5,
733 angular_velocity.get_z() * delta_time * 0.5,
734 1.0,
735 );
736 body.set_rotation((rotation_delta * body.get_rotation()).normalized());
737 body.set_torque_accumulator(Vector3D::zero());
738 }
739 self.resolve_collisions();
740 }
741
742 fn resolve_collisions(&mut self) {
748 let body_count: usize = self.get_bodies().len();
749 if body_count < 2 {
750 return;
751 }
752 let mut pairs: Vec<(usize, usize)> = Vec::new();
757 let bboxes: Vec<(usize, AABB3D)> = self
761 .get_bodies()
762 .iter()
763 .enumerate()
764 .filter_map(|(index, body)| body.bounding_box().map(|bbox| (index, bbox)))
765 .collect();
766 {
767 let Self {
768 grid,
769 query_buffer,
770 query_seen,
771 ..
772 } = self;
773 let grid: &mut SpatialHashGrid3D = grid;
774 let query_buffer: &mut Vec<usize> = query_buffer;
775 let query_seen: &mut HashSet<usize> = query_seen;
776 grid.clear();
777 for (index, bbox) in &bboxes {
778 grid.insert(*index, bbox.get_min(), bbox.get_max());
779 }
780 for (i, (_, bbox)) in bboxes.iter().enumerate() {
781 grid.query_into(bbox.get_min(), bbox.get_max(), query_buffer, query_seen);
782 for &j in query_buffer.iter() {
783 if j > i {
784 pairs.push((i, j));
785 }
786 }
787 }
788 }
789 for iteration in 0..PHYSICS_MAX_ITERATIONS {
790 let mut any_collision: bool = false;
791 for &(i, j) in pairs.iter() {
792 let (left, right) = self.get_mut_bodies().split_at_mut(j);
793 let body_a: &mut RigidBody3D = &mut left[i];
794 let body_b: &mut RigidBody3D = &mut right[0];
795 if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
796 continue;
797 }
798 if let Some(result) = Self::check_collision_3d(body_a, body_b) {
799 Self::resolve_collision_3d(body_a, body_b, &result);
800 any_collision = true;
801 }
802 }
803 if !any_collision {
804 break;
805 }
806 let _: u32 = iteration;
807 }
808 }
809
810 fn check_collision_3d(a: &RigidBody3D, b: &RigidBody3D) -> Option<CollisionResult3D> {
821 let a_bbox: AABB3D = a.bounding_box()?;
822 let b_bbox: AABB3D = b.bounding_box()?;
823 if !AABB3D::broad_phase(a_bbox, b_bbox) {
824 return None;
825 }
826 let a_collider: Option<BodyCollider3D> = a.get_collider();
827 let b_collider: Option<BodyCollider3D> = b.get_collider();
828 let position_delta: Vector3D = b.get_position() - a.get_position();
829 match (a_collider, b_collider) {
830 (Some(BodyCollider3D::Aabb(aabb_a)), Some(BodyCollider3D::Aabb(aabb_b))) => {
831 let aabb_b_inner: AABB3D = aabb_b.get_aabb();
832 let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
833 aabb_b_inner.get_min() + position_delta,
834 aabb_b_inner.get_max() + position_delta,
835 ));
836 aabb_a.collide_with_aabb(&offset_aabb)
837 }
838 (Some(BodyCollider3D::Sphere(sphere_a)), Some(BodyCollider3D::Sphere(sphere_b))) => {
839 let sphere_b_inner: Sphere = sphere_b.get_sphere();
840 let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
841 sphere_b_inner.get_center() + position_delta,
842 sphere_b_inner.get_radius(),
843 ));
844 sphere_a.collide_with_sphere(&offset_sphere)
845 }
846 (Some(BodyCollider3D::Aabb(aabb)), Some(BodyCollider3D::Sphere(sphere))) => {
847 let sphere_inner: Sphere = sphere.get_sphere();
848 let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
849 sphere_inner.get_center() + position_delta,
850 sphere_inner.get_radius(),
851 ));
852 aabb.collide_with_sphere(&offset_sphere)
853 }
854 (Some(BodyCollider3D::Sphere(sphere)), Some(BodyCollider3D::Aabb(aabb))) => {
855 let aabb_inner: AABB3D = aabb.get_aabb();
856 let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
857 aabb_inner.get_min() + position_delta,
858 aabb_inner.get_max() + position_delta,
859 ));
860 offset_aabb
861 .collide_with_sphere(&sphere)
862 .map(|mut result: CollisionResult3D| {
863 result.set_normal(-result.get_normal());
864 result
865 })
866 }
867 _ => None,
868 }
869 }
870
871 fn resolve_collision_3d(a: &mut RigidBody3D, b: &mut RigidBody3D, result: &CollisionResult3D) {
880 let a_inverse_mass: f64 = a.get_inverse_mass();
881 let b_inverse_mass: f64 = b.get_inverse_mass();
882 let relative_velocity: Vector3D = b.get_velocity() - a.get_velocity();
883 let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
884 if velocity_along_normal > 0.0 {
885 return;
886 }
887 let restitution: f64 = a.get_restitution().min(b.get_restitution());
888 let inverse_mass_sum: f64 = a_inverse_mass + b_inverse_mass;
889 if inverse_mass_sum == 0.0 {
890 return;
891 }
892 let impulse_magnitude: f64 =
893 -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum;
894 let impulse: Vector3D = result.get_normal().scaled(impulse_magnitude);
895 *a.get_mut_velocity() -= impulse.scaled(a_inverse_mass);
896 *b.get_mut_velocity() += impulse.scaled(b_inverse_mass);
897 let correction: Vector3D = result
898 .get_normal()
899 .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
900 *a.get_mut_position() -= correction.scaled(a_inverse_mass);
901 *b.get_mut_position() += correction.scaled(b_inverse_mass);
902 }
903}
904
905impl Updatable for PhysicsWorld3D {
912 fn update(&mut self, delta_time: f64) {
918 PhysicsWorld3D::step(self, delta_time);
919 }
920}
921
922impl Default for PhysicsWorld3D {
924 fn default() -> PhysicsWorld3D {
930 PhysicsWorld3D::with_config(PhysicsConfig3D::default())
931 }
932}
933
934#[cfg(test)]
935mod tests {
936 use super::*;
944
945 const EPSILON: f64 = 1e-9;
946
947 #[test]
952 fn step_applies_torque_to_3d_angular_velocity() {
953 let mut world: PhysicsWorld3D = PhysicsWorld3D::default();
954 let mut body: RigidBody3D = RigidBody3D::new_dynamic(1, Vector3D::new(0.0, 0.0, 0.0));
955 body.apply_torque(Vector3D::new(0.0, 0.0, 2.0));
958 world.add_body(body);
959
960 world.step(1.0);
961 let omega: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
962 assert!(
963 omega.get_x().abs() < EPSILON,
964 "unexpected x angular velocity: {}",
965 omega.get_x(),
966 );
967 assert!(
968 omega.get_y().abs() < EPSILON,
969 "unexpected y angular velocity: {}",
970 omega.get_y(),
971 );
972 let expected_z: f64 = 2.0;
973 assert!(
974 (omega.get_z() - expected_z).abs() < EPSILON,
975 "expected z angular velocity {}, got {}",
976 expected_z,
977 omega.get_z(),
978 );
979
980 world.step(1.0);
983 let omega_after: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
984 assert!(
985 (omega_after.get_z() - expected_z).abs() < EPSILON,
986 "torque_accumulator leaked into a second step: z = {}",
987 omega_after.get_z(),
988 );
989 }
990
991 #[test]
994 fn step_static_3d_body_ignores_torque() {
995 let mut world: PhysicsWorld3D = PhysicsWorld3D::default();
996 let mut body: RigidBody3D = RigidBody3D::new_static(1, Vector3D::new(0.0, 0.0, 0.0));
997 body.apply_torque(Vector3D::new(1.0, 0.0, 0.0));
998 world.add_body(body);
999
1000 world.step(1.0);
1001 let omega: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
1002 assert!(
1003 omega.get_x().abs() < EPSILON
1004 && omega.get_y().abs() < EPSILON
1005 && omega.get_z().abs() < EPSILON,
1006 "static body must remain rotationally inert, got ({}, {}, {})",
1007 omega.get_x(),
1008 omega.get_y(),
1009 omega.get_z(),
1010 );
1011 }
1012
1013 #[test]
1016 fn step_torque_accumulates_over_multiple_steps() {
1017 let mut world: PhysicsWorld3D = PhysicsWorld3D::default();
1018 let body: RigidBody3D = RigidBody3D::new_dynamic(1, Vector3D::new(0.0, 0.0, 0.0));
1019 world.add_body(body);
1020
1021 for _ in 0..4 {
1022 world
1023 .get_body_mut(1)
1024 .unwrap()
1025 .apply_torque(Vector3D::new(0.0, 1.0, 0.0));
1026 world.step(1.0);
1027 }
1028 let omega: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
1029 assert!(
1032 (omega.get_y() - 4.0).abs() < EPSILON,
1033 "expected cumulative angular velocity 4.0 on y axis, got {}",
1034 omega.get_y(),
1035 );
1036 }
1037
1038 #[test]
1041 fn update_inertia_zeros_inverse_inertia() {
1042 let mut body: RigidBody3D = RigidBody3D::new_dynamic(1, Vector3D::new(0.0, 0.0, 0.0));
1043 assert!(
1044 (body.get_inverse_inertia() - 1.0).abs() < EPSILON,
1045 "default inverse_inertia must equal 1/mass = 1.0, got {}",
1046 body.get_inverse_inertia(),
1047 );
1048 body.update_inertia(0.0);
1049 assert!(
1050 body.get_inverse_inertia().abs() < EPSILON,
1051 "update_inertia(0) must zero out inverse_inertia, got {}",
1052 body.get_inverse_inertia(),
1053 );
1054 }
1055
1056 #[test]
1058 fn step_2d_angular_velocity_unchanged() {
1059 let mut world: PhysicsWorld2D = PhysicsWorld2D::default();
1060 let body: RigidBody2D = RigidBody2D::new_dynamic(1, Vector2D::new(0.0, 0.0));
1061 world.add_body(body);
1062
1063 world.step(1.0);
1064 let omega: f64 = world.get_body(1).unwrap().get_angular_velocity();
1065 assert!(
1066 omega.abs() < EPSILON,
1067 "2D angular velocity should remain 0 with no input, got {}",
1068 omega,
1069 );
1070 }
1071}