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 self.pair_buffer.clear();
403 {
404 let (bodies, grid, query_buffer, query_seen) = (
405 &self.bodies,
406 &mut self.grid,
407 &mut self.query_buffer,
408 &mut self.query_seen,
409 );
410 grid.clear();
411 for (index, body) in bodies.iter().enumerate() {
412 if let Some(bbox) = body.bounding_box() {
413 grid.insert(index, bbox.min(), bbox.max());
414 }
415 }
416 let pairs: &mut Vec<(usize, usize)> = &mut self.pair_buffer;
417 for (i, body) in bodies.iter().enumerate() {
418 let Some(bbox) = body.bounding_box() else {
419 continue;
420 };
421 grid.query_into(bbox.min(), bbox.max(), query_buffer, query_seen);
422 for &j in query_buffer.iter() {
423 if j > i {
424 pairs.push((i, j));
425 }
426 }
427 }
428 }
429 let pairs_snapshot: Vec<(usize, usize)> = self.pair_buffer.clone();
438 for iteration in 0..PHYSICS_MAX_ITERATIONS {
439 let mut any_collision: bool = false;
440 for &(i, j) in pairs_snapshot.iter() {
441 let (left, right) = self.get_mut_bodies().split_at_mut(j);
442 let body_a: &mut RigidBody2D = &mut left[i];
443 let body_b: &mut RigidBody2D = &mut right[0];
444 if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
445 continue;
446 }
447 if let Some(result) = body_a.check_collision_with(body_b) {
448 body_a.resolve_collision_with(body_b, &result);
449 any_collision = true;
450 }
451 }
452 if !any_collision {
453 break;
454 }
455 let _: u32 = iteration;
456 }
457 }
458}
459
460impl Updatable for PhysicsWorld2D {
467 fn update(&mut self, delta_time: f64) {
473 PhysicsWorld2D::step(self, delta_time);
474 }
475}
476
477impl Default for PhysicsConfig3D {
479 fn default() -> PhysicsConfig3D {
485 PhysicsConfig3D::new(
486 Vector3D::new(0.0, DEFAULT_GRAVITY_3D, 0.0),
487 DEFAULT_LINEAR_DAMPING,
488 DEFAULT_ANGULAR_DAMPING,
489 )
490 }
491}
492
493impl RigidBody3D {
495 pub fn new_dynamic(id: u64, position: Vector3D) -> RigidBody3D {
506 let mass: f64 = PHYSICS_DEFAULT_MASS;
507 let mut body: RigidBody3D = RigidBody3D::new(
508 id,
509 position,
510 mass,
511 1.0 / mass,
512 DEFAULT_RESTITUTION,
513 DEFAULT_FRICTION,
514 BodyType::Dynamic,
515 );
516 body.update_inertia(mass);
517 body
518 }
519
520 pub fn new_static(id: u64, position: Vector3D) -> RigidBody3D {
531 RigidBody3D::new(
532 id,
533 position,
534 PHYSICS_STATIC_MASS,
535 0.0,
536 DEFAULT_RESTITUTION,
537 DEFAULT_FRICTION,
538 BodyType::Static,
539 )
540 }
541
542 pub fn apply_force(&mut self, force: Vector3D) {
548 *self.get_mut_force_accumulator() += force;
549 }
550
551 pub fn apply_torque(&mut self, torque: Vector3D) {
557 *self.get_mut_torque_accumulator() += torque;
558 }
559
560 pub fn apply_impulse(&mut self, impulse: Vector3D) {
566 let inverse_mass: f64 = self.get_inverse_mass();
567 if inverse_mass == 0.0 {
568 return;
569 }
570 *self.get_mut_velocity() += impulse.scaled(inverse_mass);
571 }
572
573 pub fn update_mass(&mut self, mass: f64) {
580 self.set_mass(mass);
581 self.set_inverse_mass(if mass > 0.0 { 1.0 / mass } else { 0.0 });
582 }
583
584 pub fn update_inertia(&mut self, inertia: f64) {
591 self.set_inverse_inertia(if inertia > 0.0 { 1.0 / inertia } else { 0.0 });
592 }
593
594 pub fn is_dynamic(&self) -> bool {
600 self.get_body_type() == BodyType::Dynamic
601 }
602
603 pub fn update_collider(&mut self, collider: BodyCollider3D) {
609 self.set_collider(Some(collider));
610 }
611
612 pub fn bounding_box(&self) -> Option<AABB3D> {
618 let collider: Option<BodyCollider3D> = self.get_collider();
619 let position: Vector3D = self.get_position();
620 match collider? {
621 BodyCollider3D::Aabb(aabb) => {
622 let center: Vector3D = aabb.get_aabb().center();
623 let size: Vector3D = aabb.get_aabb().size();
624 Some(AABB3D::from_center(
625 position + center,
626 size.get_x(),
627 size.get_y(),
628 size.get_z(),
629 ))
630 }
631 BodyCollider3D::Sphere(sphere) => {
632 let sphere_inner: Sphere = sphere.get_sphere();
633 let diameter: f64 = sphere_inner.get_radius() * 2.0;
634 Some(AABB3D::from_center(
635 position + sphere_inner.get_center(),
636 diameter,
637 diameter,
638 diameter,
639 ))
640 }
641 }
642 }
643}
644
645impl PhysicsWorld3D {
647 pub fn with_config(config: PhysicsConfig3D) -> PhysicsWorld3D {
657 let mut world: PhysicsWorld3D = PhysicsWorld3D::new(config);
658 world.set_grid(SpatialHashGrid3D::with_default_size());
659 world
660 }
661
662 pub fn add_body(&mut self, body: RigidBody3D) {
668 self.get_mut_bodies().push(body);
669 }
670
671 pub fn remove_body(&mut self, id: u64) {
677 self.get_mut_bodies()
678 .retain(|body: &RigidBody3D| body.get_id() != id);
679 }
680
681 pub fn get_body(&self, id: u64) -> Option<&RigidBody3D> {
691 self.get_bodies()
692 .iter()
693 .find(|body: &&RigidBody3D| body.get_id() == id)
694 }
695
696 pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody3D> {
706 self.get_mut_bodies()
707 .iter_mut()
708 .find(|body: &&mut RigidBody3D| body.get_id() == id)
709 }
710
711 pub fn step(&mut self, delta_time: f64) {
720 let config: PhysicsConfig3D = self.get_config();
721 let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
723 let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
724 let gravity: Vector3D = config.get_gravity();
725 for body in self.get_mut_bodies() {
726 if !body.is_dynamic() {
727 continue;
728 }
729 let body_mass: f64 = body.get_mass();
730 let body_inverse_mass: f64 = body.get_inverse_mass();
731 *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
732 let force: Vector3D = body.get_force_accumulator();
733 *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
734 *body.get_mut_velocity() *= damping_factor;
736 let current_velocity: Vector3D = body.get_velocity();
737 *body.get_mut_position() += current_velocity.scaled(delta_time);
738 body.set_force_accumulator(Vector3D::zero());
739 *body.get_mut_angular_velocity() *= angular_damping;
740 let body_inverse_inertia: f64 = body.get_inverse_inertia();
741 let torque: Vector3D = body.get_torque_accumulator();
742 *body.get_mut_angular_velocity() += torque.scaled(body_inverse_inertia * delta_time);
743 let angular_velocity: Vector3D = body.get_angular_velocity();
744 let rotation_delta: Quaternion = Quaternion::new(
745 angular_velocity.get_x() * delta_time * 0.5,
746 angular_velocity.get_y() * delta_time * 0.5,
747 angular_velocity.get_z() * delta_time * 0.5,
748 1.0,
749 );
750 body.set_rotation((rotation_delta * body.get_rotation()).normalized());
751 body.set_torque_accumulator(Vector3D::zero());
752 }
753 self.resolve_collisions();
754 }
755
756 fn resolve_collisions(&mut self) {
762 let body_count: usize = self.get_bodies().len();
763 if body_count < 2 {
764 return;
765 }
766 self.pair_buffer.clear();
774 let body_count: usize = self.get_bodies().len();
786 let mut bboxes: Vec<(usize, AABB3D)> = Vec::with_capacity(body_count);
787 bboxes.extend(
788 self.get_bodies()
789 .iter()
790 .enumerate()
791 .filter_map(|(index, body)| body.bounding_box().map(|bbox| (index, bbox))),
792 );
793 {
794 let Self {
795 grid,
796 query_buffer,
797 query_seen,
798 ..
799 } = self;
800 let grid: &mut SpatialHashGrid3D = grid;
801 let query_buffer: &mut Vec<usize> = query_buffer;
802 let query_seen: &mut HashSet<usize> = query_seen;
803 grid.clear();
804 let pairs: &mut Vec<(usize, usize)> = &mut self.pair_buffer;
805 for (index, bbox) in bboxes.iter() {
806 grid.insert(*index, bbox.get_min(), bbox.get_max());
807 }
808 for (i, (_, bbox)) in bboxes.iter().enumerate() {
809 grid.query_into(bbox.get_min(), bbox.get_max(), query_buffer, query_seen);
810 for &j in query_buffer.iter() {
811 if j > i {
812 pairs.push((i, j));
813 }
814 }
815 }
816 }
817 let pairs_snapshot: Vec<(usize, usize)> = self.pair_buffer.clone();
822 for iteration in 0..PHYSICS_MAX_ITERATIONS {
823 let mut any_collision: bool = false;
824 for &(i, j) in pairs_snapshot.iter() {
825 let (left, right) = self.get_mut_bodies().split_at_mut(j);
826 let body_a: &mut RigidBody3D = &mut left[i];
827 let body_b: &mut RigidBody3D = &mut right[0];
828 if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
829 continue;
830 }
831 if let Some(result) = Self::check_collision_3d(body_a, body_b) {
832 Self::resolve_collision_3d(body_a, body_b, &result);
833 any_collision = true;
834 }
835 }
836 if !any_collision {
837 break;
838 }
839 let _: u32 = iteration;
840 }
841 }
842
843 fn check_collision_3d(a: &RigidBody3D, b: &RigidBody3D) -> Option<CollisionResult3D> {
854 let a_bbox: AABB3D = a.bounding_box()?;
855 let b_bbox: AABB3D = b.bounding_box()?;
856 if !AABB3D::broad_phase(a_bbox, b_bbox) {
857 return None;
858 }
859 let a_collider: Option<BodyCollider3D> = a.get_collider();
860 let b_collider: Option<BodyCollider3D> = b.get_collider();
861 let position_delta: Vector3D = b.get_position() - a.get_position();
862 match (a_collider, b_collider) {
863 (Some(BodyCollider3D::Aabb(aabb_a)), Some(BodyCollider3D::Aabb(aabb_b))) => {
864 let aabb_b_inner: AABB3D = aabb_b.get_aabb();
865 let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
866 aabb_b_inner.get_min() + position_delta,
867 aabb_b_inner.get_max() + position_delta,
868 ));
869 aabb_a.collide_with_aabb(&offset_aabb)
870 }
871 (Some(BodyCollider3D::Sphere(sphere_a)), Some(BodyCollider3D::Sphere(sphere_b))) => {
872 let sphere_b_inner: Sphere = sphere_b.get_sphere();
873 let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
874 sphere_b_inner.get_center() + position_delta,
875 sphere_b_inner.get_radius(),
876 ));
877 sphere_a.collide_with_sphere(&offset_sphere)
878 }
879 (Some(BodyCollider3D::Aabb(aabb)), Some(BodyCollider3D::Sphere(sphere))) => {
880 let sphere_inner: Sphere = sphere.get_sphere();
881 let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
882 sphere_inner.get_center() + position_delta,
883 sphere_inner.get_radius(),
884 ));
885 aabb.collide_with_sphere(&offset_sphere)
886 }
887 (Some(BodyCollider3D::Sphere(sphere)), Some(BodyCollider3D::Aabb(aabb))) => {
888 let aabb_inner: AABB3D = aabb.get_aabb();
889 let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
890 aabb_inner.get_min() + position_delta,
891 aabb_inner.get_max() + position_delta,
892 ));
893 offset_aabb
894 .collide_with_sphere(&sphere)
895 .map(|mut result: CollisionResult3D| {
896 result.set_normal(-result.get_normal());
897 result
898 })
899 }
900 _ => None,
901 }
902 }
903
904 fn resolve_collision_3d(a: &mut RigidBody3D, b: &mut RigidBody3D, result: &CollisionResult3D) {
913 let a_inverse_mass: f64 = a.get_inverse_mass();
914 let b_inverse_mass: f64 = b.get_inverse_mass();
915 let relative_velocity: Vector3D = b.get_velocity() - a.get_velocity();
916 let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
917 if velocity_along_normal > 0.0 {
918 return;
919 }
920 let restitution: f64 = a.get_restitution().min(b.get_restitution());
921 let inverse_mass_sum: f64 = a_inverse_mass + b_inverse_mass;
922 if inverse_mass_sum == 0.0 {
923 return;
924 }
925 let impulse_magnitude: f64 =
926 -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum;
927 let impulse: Vector3D = result.get_normal().scaled(impulse_magnitude);
928 *a.get_mut_velocity() -= impulse.scaled(a_inverse_mass);
929 *b.get_mut_velocity() += impulse.scaled(b_inverse_mass);
930 let correction: Vector3D = result
931 .get_normal()
932 .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
933 *a.get_mut_position() -= correction.scaled(a_inverse_mass);
934 *b.get_mut_position() += correction.scaled(b_inverse_mass);
935 }
936}
937
938impl Updatable for PhysicsWorld3D {
945 fn update(&mut self, delta_time: f64) {
951 PhysicsWorld3D::step(self, delta_time);
952 }
953}
954
955impl Default for PhysicsWorld3D {
957 fn default() -> PhysicsWorld3D {
963 PhysicsWorld3D::with_config(PhysicsConfig3D::default())
964 }
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
977
978 const EPSILON: f64 = 1e-9;
979
980 #[test]
985 fn step_applies_torque_to_3d_angular_velocity() {
986 let mut world: PhysicsWorld3D = PhysicsWorld3D::default();
987 let mut body: RigidBody3D = RigidBody3D::new_dynamic(1, Vector3D::new(0.0, 0.0, 0.0));
988 body.apply_torque(Vector3D::new(0.0, 0.0, 2.0));
991 world.add_body(body);
992
993 world.step(1.0);
994 let omega: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
995 assert!(
996 omega.get_x().abs() < EPSILON,
997 "unexpected x angular velocity: {}",
998 omega.get_x(),
999 );
1000 assert!(
1001 omega.get_y().abs() < EPSILON,
1002 "unexpected y angular velocity: {}",
1003 omega.get_y(),
1004 );
1005 let expected_z: f64 = 2.0;
1006 assert!(
1007 (omega.get_z() - expected_z).abs() < EPSILON,
1008 "expected z angular velocity {}, got {}",
1009 expected_z,
1010 omega.get_z(),
1011 );
1012
1013 world.step(1.0);
1016 let omega_after: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
1017 assert!(
1018 (omega_after.get_z() - expected_z).abs() < EPSILON,
1019 "torque_accumulator leaked into a second step: z = {}",
1020 omega_after.get_z(),
1021 );
1022 }
1023
1024 #[test]
1027 fn step_static_3d_body_ignores_torque() {
1028 let mut world: PhysicsWorld3D = PhysicsWorld3D::default();
1029 let mut body: RigidBody3D = RigidBody3D::new_static(1, Vector3D::new(0.0, 0.0, 0.0));
1030 body.apply_torque(Vector3D::new(1.0, 0.0, 0.0));
1031 world.add_body(body);
1032
1033 world.step(1.0);
1034 let omega: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
1035 assert!(
1036 omega.get_x().abs() < EPSILON
1037 && omega.get_y().abs() < EPSILON
1038 && omega.get_z().abs() < EPSILON,
1039 "static body must remain rotationally inert, got ({}, {}, {})",
1040 omega.get_x(),
1041 omega.get_y(),
1042 omega.get_z(),
1043 );
1044 }
1045
1046 #[test]
1049 fn step_torque_accumulates_over_multiple_steps() {
1050 let mut world: PhysicsWorld3D = PhysicsWorld3D::default();
1051 let body: RigidBody3D = RigidBody3D::new_dynamic(1, Vector3D::new(0.0, 0.0, 0.0));
1052 world.add_body(body);
1053
1054 for _ in 0..4 {
1055 world
1056 .get_body_mut(1)
1057 .unwrap()
1058 .apply_torque(Vector3D::new(0.0, 1.0, 0.0));
1059 world.step(1.0);
1060 }
1061 let omega: Vector3D = world.get_body(1).unwrap().get_angular_velocity();
1062 assert!(
1065 (omega.get_y() - 4.0).abs() < EPSILON,
1066 "expected cumulative angular velocity 4.0 on y axis, got {}",
1067 omega.get_y(),
1068 );
1069 }
1070
1071 #[test]
1074 fn update_inertia_zeros_inverse_inertia() {
1075 let mut body: RigidBody3D = RigidBody3D::new_dynamic(1, Vector3D::new(0.0, 0.0, 0.0));
1076 assert!(
1077 (body.get_inverse_inertia() - 1.0).abs() < EPSILON,
1078 "default inverse_inertia must equal 1/mass = 1.0, got {}",
1079 body.get_inverse_inertia(),
1080 );
1081 body.update_inertia(0.0);
1082 assert!(
1083 body.get_inverse_inertia().abs() < EPSILON,
1084 "update_inertia(0) must zero out inverse_inertia, got {}",
1085 body.get_inverse_inertia(),
1086 );
1087 }
1088
1089 #[test]
1091 fn step_2d_angular_velocity_unchanged() {
1092 let mut world: PhysicsWorld2D = PhysicsWorld2D::default();
1093 let body: RigidBody2D = RigidBody2D::new_dynamic(1, Vector2D::new(0.0, 0.0));
1094 world.add_body(body);
1095
1096 world.step(1.0);
1097 let omega: f64 = world.get_body(1).unwrap().get_angular_velocity();
1098 assert!(
1099 omega.abs() < EPSILON,
1100 "2D angular velocity should remain 0 with no input, got {}",
1101 omega,
1102 );
1103 }
1104}