1#![allow(missing_docs)]
12
13mod constraints;
15mod extensions;
16mod fem;
17mod geometry;
18mod lbm;
19mod materials;
20mod math_helpers;
21mod py_bindings;
22mod sph;
23mod stats;
24
25#[cfg(test)]
26mod tests;
27
28pub use constraints::{ConstraintType, PyConstraint};
30pub use extensions::{ContactPair, InertiaTensor, PyRigidBody};
31pub use fem::{FemBarElement, PyFemAssembly};
32pub use geometry::{PyAabb, PyConvexHull, PySphere};
33pub use lbm::{PyLbmConfig, PyLbmGrid};
34pub use materials::{MaterialClass, PyMaterial};
35pub use math_helpers::{
36 array_to_vec3, quat_conjugate, quat_from_axis_angle, quat_mul, quat_normalize, quat_rotate_vec,
37 vec3_to_array,
38};
39pub use py_bindings::{
40 MaterialProperties, PyFemBinding, PyLbmBinding, PyMdBinding, PySphBinding, py_p_wave_speed,
41 py_query_material, py_s_wave_speed,
42};
43pub use sph::{PySphConfig, PySphSim};
44pub use stats::SimStats;
45
46use crate::types::{
47 PyColliderDesc, PyColliderShape, PyContactResult, PyRigidBodyConfig, PyRigidBodyDesc,
48 PySimConfig, PyVec3,
49};
50use serde::{Deserialize, Serialize};
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
58struct InternalBody {
59 position: [f64; 3],
61 velocity: [f64; 3],
63 orientation: [f64; 4],
65 angular_velocity: [f64; 3],
67 mass: f64,
69 is_static: bool,
71 is_kinematic: bool,
73 is_sleeping: bool,
75 can_sleep: bool,
77 friction: f64,
79 restitution: f64,
81 linear_damping: f64,
83 angular_damping: f64,
85 accumulated_force: [f64; 3],
87 accumulated_torque: [f64; 3],
89 sleep_timer: f64,
91 shapes: Vec<PyColliderShape>,
93 tag: Option<String>,
95}
96
97impl InternalBody {
98 fn from_config(config: &PyRigidBodyConfig) -> Self {
99 Self {
100 position: config.position,
101 velocity: config.velocity,
102 orientation: config.orientation,
103 angular_velocity: config.angular_velocity,
104 mass: config.mass,
105 is_static: config.is_static,
106 is_kinematic: config.is_kinematic,
107 is_sleeping: false,
108 can_sleep: config.can_sleep,
109 friction: config.friction,
110 restitution: config.restitution,
111 linear_damping: config.linear_damping,
112 angular_damping: config.angular_damping,
113 accumulated_force: [0.0; 3],
114 accumulated_torque: [0.0; 3],
115 sleep_timer: 0.0,
116 shapes: config.shapes.clone(),
117 tag: config.tag.clone(),
118 }
119 }
120
121 fn inv_mass(&self) -> f64 {
123 if self.is_static || self.is_kinematic || self.mass <= 0.0 {
124 0.0
125 } else {
126 1.0 / self.mass
127 }
128 }
129
130 fn is_dynamic(&self) -> bool {
132 !self.is_static && !self.is_kinematic
133 }
134}
135
136#[derive(Debug, Clone)]
141struct Slot {
142 body: Option<InternalBody>,
143 generation: u32,
144}
145
146impl Slot {
147 #[allow(dead_code)]
148 fn empty() -> Self {
149 Self {
150 body: None,
151 generation: 0,
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
166pub struct PyPhysicsWorld {
167 slots: Vec<Slot>,
169 free_list: Vec<u32>,
171 config: PySimConfig,
173 time: f64,
175 contacts: Vec<PyContactResult>,
177 constraints: Vec<PyConstraint>,
179}
180
181impl PyPhysicsWorld {
182 pub fn new(config: PySimConfig) -> Self {
188 Self {
189 slots: Vec::new(),
190 free_list: Vec::new(),
191 config,
192 time: 0.0,
193 contacts: Vec::new(),
194 constraints: Vec::new(),
195 }
196 }
197
198 pub fn add_rigid_body(&mut self, config: PyRigidBodyConfig) -> u32 {
204 let body = InternalBody::from_config(&config);
205 if let Some(idx) = self.free_list.pop() {
206 let slot = &mut self.slots[idx as usize];
207 slot.body = Some(body);
208 idx
210 } else {
211 let idx = self.slots.len() as u32;
212 self.slots.push(Slot {
213 body: Some(body),
214 generation: 0,
215 });
216 idx
217 }
218 }
219
220 pub fn add_body_legacy(&mut self, desc: &PyRigidBodyDesc) -> u32 {
222 let config = PyRigidBodyConfig {
223 mass: desc.mass,
224 position: [desc.position.x, desc.position.y, desc.position.z],
225 velocity: [0.0; 3],
226 orientation: [0.0, 0.0, 0.0, 1.0],
227 angular_velocity: [0.0; 3],
228 shapes: vec![],
229 friction: 0.5,
230 restitution: 0.3,
231 is_static: desc.is_static,
232 is_kinematic: false,
233 can_sleep: true,
234 linear_damping: 0.0,
235 angular_damping: 0.0,
236 tag: None,
237 };
238 self.add_rigid_body(config)
239 }
240
241 pub fn add_collider(&mut self, handle: u32, desc: &PyColliderDesc) {
245 if let Some(body) = self.get_body_mut(handle) {
246 let shape = match desc.shape_type.as_str() {
247 "sphere" => {
248 let r = desc.radius.unwrap_or(0.5);
249 PyColliderShape::Sphere { radius: r }
250 }
251 "box" => {
252 let he = desc.half_extents.unwrap_or(PyVec3::new(0.5, 0.5, 0.5));
253 PyColliderShape::Box {
254 half_extents: [he.x, he.y, he.z],
255 }
256 }
257 _ => PyColliderShape::Sphere { radius: 0.5 },
258 };
259 body.shapes.push(shape);
260 }
261 }
262
263 pub fn remove_body(&mut self, handle: u32) -> bool {
265 if let Some(slot) = self.slots.get_mut(handle as usize)
266 && slot.body.is_some()
267 {
268 slot.body = None;
269 slot.generation = slot.generation.wrapping_add(1);
270 self.free_list.push(handle);
271 return true;
272 }
273 false
274 }
275
276 pub fn get_position(&self, handle: u32) -> Option<[f64; 3]> {
282 self.get_body(handle).map(|b| b.position)
283 }
284
285 pub fn get_velocity(&self, handle: u32) -> Option<[f64; 3]> {
287 self.get_body(handle).map(|b| b.velocity)
288 }
289
290 pub fn get_orientation(&self, handle: u32) -> Option<[f64; 4]> {
292 self.get_body(handle).map(|b| b.orientation)
293 }
294
295 pub fn get_angular_velocity(&self, handle: u32) -> Option<[f64; 3]> {
297 self.get_body(handle).map(|b| b.angular_velocity)
298 }
299
300 pub fn is_sleeping(&self, handle: u32) -> bool {
302 self.get_body(handle)
303 .map(|b| b.is_sleeping)
304 .unwrap_or(false)
305 }
306
307 pub fn get_tag(&self, handle: u32) -> Option<String> {
309 self.get_body(handle).and_then(|b| b.tag.clone())
310 }
311
312 pub fn set_position(&mut self, handle: u32, pos: [f64; 3]) {
318 if let Some(body) = self.get_body_mut(handle) {
319 body.position = pos;
320 body.is_sleeping = false;
321 }
322 }
323
324 pub fn set_velocity(&mut self, handle: u32, vel: [f64; 3]) {
326 if let Some(body) = self.get_body_mut(handle) {
327 body.velocity = vel;
328 body.is_sleeping = false;
329 }
330 }
331
332 pub fn set_orientation(&mut self, handle: u32, orientation: [f64; 4]) {
334 if let Some(body) = self.get_body_mut(handle) {
335 body.orientation = normalize_quat(orientation);
336 body.is_sleeping = false;
337 }
338 }
339
340 pub fn set_angular_velocity(&mut self, handle: u32, omega: [f64; 3]) {
342 if let Some(body) = self.get_body_mut(handle) {
343 body.angular_velocity = omega;
344 body.is_sleeping = false;
345 }
346 }
347
348 pub fn apply_force(&mut self, handle: u32, force: [f64; 3], point: Option<[f64; 3]>) {
357 if let Some(body) = self.get_body_mut(handle) {
358 if !body.is_dynamic() {
359 return;
360 }
361 body.accumulated_force[0] += force[0];
362 body.accumulated_force[1] += force[1];
363 body.accumulated_force[2] += force[2];
364
365 if let Some(pt) = point {
366 let r = [
368 pt[0] - body.position[0],
369 pt[1] - body.position[1],
370 pt[2] - body.position[2],
371 ];
372 let torque = cross3(r, force);
373 body.accumulated_torque[0] += torque[0];
374 body.accumulated_torque[1] += torque[1];
375 body.accumulated_torque[2] += torque[2];
376 }
377 body.is_sleeping = false;
378 }
379 }
380
381 pub fn apply_impulse(&mut self, handle: u32, impulse: [f64; 3], point: Option<[f64; 3]>) {
386 if let Some(body) = self.get_body_mut(handle) {
387 if !body.is_dynamic() {
388 return;
389 }
390 let inv_m = body.inv_mass();
391 body.velocity[0] += impulse[0] * inv_m;
392 body.velocity[1] += impulse[1] * inv_m;
393 body.velocity[2] += impulse[2] * inv_m;
394
395 if let Some(pt) = point {
396 let r = [
398 pt[0] - body.position[0],
399 pt[1] - body.position[1],
400 pt[2] - body.position[2],
401 ];
402 let ang_impulse = cross3(r, impulse);
403 let approx_inertia = approximate_inertia(body);
406 let inv_i = if approx_inertia > 1e-12 {
407 1.0 / approx_inertia
408 } else {
409 0.0
410 };
411 body.angular_velocity[0] += ang_impulse[0] * inv_i;
412 body.angular_velocity[1] += ang_impulse[1] * inv_i;
413 body.angular_velocity[2] += ang_impulse[2] * inv_i;
414 }
415 body.is_sleeping = false;
416 }
417 }
418
419 pub fn apply_torque(&mut self, handle: u32, torque: [f64; 3]) {
421 if let Some(body) = self.get_body_mut(handle) {
422 if !body.is_dynamic() {
423 return;
424 }
425 body.accumulated_torque[0] += torque[0];
426 body.accumulated_torque[1] += torque[1];
427 body.accumulated_torque[2] += torque[2];
428 body.is_sleeping = false;
429 }
430 }
431
432 pub fn wake_body(&mut self, handle: u32) {
434 if let Some(body) = self.get_body_mut(handle) {
435 body.is_sleeping = false;
436 body.sleep_timer = 0.0;
437 }
438 }
439
440 pub fn sleep_body(&mut self, handle: u32) {
442 if let Some(body) = self.get_body_mut(handle) {
443 body.is_sleeping = true;
444 body.velocity = [0.0; 3];
445 body.angular_velocity = [0.0; 3];
446 }
447 }
448
449 pub fn set_gravity(&mut self, g: [f64; 3]) {
455 self.config.gravity = g;
456 }
457
458 pub fn gravity(&self) -> [f64; 3] {
460 self.config.gravity
461 }
462
463 pub fn config(&self) -> &PySimConfig {
465 &self.config
466 }
467
468 pub fn body_count(&self) -> usize {
474 self.slots.iter().filter(|s| s.body.is_some()).count()
475 }
476
477 pub fn sleeping_count(&self) -> usize {
479 self.slots
480 .iter()
481 .filter_map(|s| s.body.as_ref())
482 .filter(|b| b.is_sleeping)
483 .count()
484 }
485
486 pub fn time(&self) -> f64 {
488 self.time
489 }
490
491 pub fn get_contacts(&self) -> Vec<PyContactResult> {
493 self.contacts.clone()
494 }
495
496 pub fn all_positions(&self) -> Vec<[f64; 3]> {
502 self.slots
503 .iter()
504 .filter_map(|s| s.body.as_ref().map(|b| b.position))
505 .collect()
506 }
507
508 pub fn all_velocities(&self) -> Vec<[f64; 3]> {
510 self.slots
511 .iter()
512 .filter_map(|s| s.body.as_ref().map(|b| b.velocity))
513 .collect()
514 }
515
516 pub fn active_handles(&self) -> Vec<u32> {
518 self.slots
519 .iter()
520 .enumerate()
521 .filter(|(_, s)| s.body.is_some())
522 .map(|(i, _)| i as u32)
523 .collect()
524 }
525
526 pub fn step(&mut self, dt: f64) {
543 let g = self.config.gravity;
544
545 for slot in &mut self.slots {
547 let body = match slot.body.as_mut() {
548 Some(b) => b,
549 None => continue,
550 };
551 if body.is_static || body.is_kinematic || body.is_sleeping {
552 body.accumulated_force = [0.0; 3];
554 body.accumulated_torque = [0.0; 3];
555 continue;
556 }
557
558 let inv_m = body.inv_mass();
559
560 let total_force = [
562 body.accumulated_force[0] + g[0] * body.mass,
563 body.accumulated_force[1] + g[1] * body.mass,
564 body.accumulated_force[2] + g[2] * body.mass,
565 ];
566
567 body.velocity[0] += total_force[0] * inv_m * dt;
569 body.velocity[1] += total_force[1] * inv_m * dt;
570 body.velocity[2] += total_force[2] * inv_m * dt;
571
572 let lin_damp = (1.0 - body.linear_damping * dt).max(0.0);
574 body.velocity[0] *= lin_damp;
575 body.velocity[1] *= lin_damp;
576 body.velocity[2] *= lin_damp;
577
578 body.position[0] += body.velocity[0] * dt;
580 body.position[1] += body.velocity[1] * dt;
581 body.position[2] += body.velocity[2] * dt;
582
583 let approx_i = approximate_inertia(body);
585 let inv_i = if approx_i > 1e-12 {
586 1.0 / approx_i
587 } else {
588 0.0
589 };
590 body.angular_velocity[0] += body.accumulated_torque[0] * inv_i * dt;
591 body.angular_velocity[1] += body.accumulated_torque[1] * inv_i * dt;
592 body.angular_velocity[2] += body.accumulated_torque[2] * inv_i * dt;
593
594 let ang_damp = (1.0 - body.angular_damping * dt).max(0.0);
596 body.angular_velocity[0] *= ang_damp;
597 body.angular_velocity[1] *= ang_damp;
598 body.angular_velocity[2] *= ang_damp;
599
600 body.orientation = integrate_orientation(body.orientation, body.angular_velocity, dt);
602
603 body.accumulated_force = [0.0; 3];
605 body.accumulated_torque = [0.0; 3];
606 }
607
608 self.contacts.clear();
610 self.detect_and_resolve_contacts(dt);
611
612 self.resolve_constraints();
614
615 if self.config.sleep_enabled {
617 self.update_sleep(dt);
618 }
619
620 self.time += dt;
621 }
622
623 pub fn reset(&mut self) {
625 self.slots.clear();
626 self.free_list.clear();
627 self.contacts.clear();
628 self.constraints.clear();
629 self.time = 0.0;
630 }
631
632 #[allow(clippy::too_many_arguments)]
646 pub fn step_substeps(&mut self, dt: f64, substeps: u32) {
647 let substeps = substeps.max(1);
648 let sub_dt = dt / substeps as f64;
649 for _ in 0..substeps {
650 self.step(sub_dt);
651 }
652 }
653
654 pub fn bodies_in_aabb(&self, aabb: [f64; 6]) -> Vec<u32> {
662 let [xmin, ymin, zmin, xmax, ymax, zmax] = aabb;
663 self.slots
664 .iter()
665 .enumerate()
666 .filter_map(|(i, slot)| {
667 let body = slot.body.as_ref()?;
668 let p = body.position;
669 if p[0] >= xmin
670 && p[0] <= xmax
671 && p[1] >= ymin
672 && p[1] <= ymax
673 && p[2] >= zmin
674 && p[2] <= zmax
675 {
676 Some(i as u32)
677 } else {
678 None
679 }
680 })
681 .collect()
682 }
683
684 pub fn raycast(&self, origin: [f64; 3], dir: [f64; 3], max_dist: f64) -> Option<(u32, f64)> {
691 let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
692 if len < 1e-12 {
693 return None;
694 }
695 let d = [dir[0] / len, dir[1] / len, dir[2] / len];
696
697 let mut best_handle: Option<u32> = None;
698 let mut best_t = max_dist;
699
700 for (i, slot) in self.slots.iter().enumerate() {
701 let body = match slot.body.as_ref() {
702 Some(b) => b,
703 None => continue,
704 };
705 let radius = first_sphere_radius(&body.shapes);
706 if radius <= 0.0 {
707 continue;
708 }
709 let oc = [
711 origin[0] - body.position[0],
712 origin[1] - body.position[1],
713 origin[2] - body.position[2],
714 ];
715 let b_coeff = oc[0] * d[0] + oc[1] * d[1] + oc[2] * d[2];
716 let c_coeff = oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2] - radius * radius;
717 let discriminant = b_coeff * b_coeff - c_coeff;
718 if discriminant < 0.0 {
719 continue;
720 }
721 let sqrt_disc = discriminant.sqrt();
722 let t = -b_coeff - sqrt_disc;
723 let t = if t >= 0.0 { t } else { -b_coeff + sqrt_disc };
724 if t >= 0.0 && t < best_t {
725 best_t = t;
726 best_handle = Some(i as u32);
727 }
728 }
729
730 best_handle.map(|h| (h, best_t))
731 }
732
733 pub fn contact_count(&self) -> usize {
735 self.contacts.len()
736 }
737
738 pub fn get_body_position(&self, handle: usize) -> Option<PyVec3> {
744 self.get_position(handle as u32).map(PyVec3::from_array)
745 }
746
747 pub fn get_body_velocity(&self, handle: usize) -> Option<PyVec3> {
749 self.get_velocity(handle as u32).map(PyVec3::from_array)
750 }
751
752 pub fn set_body_velocity(&mut self, handle: usize, vel: PyVec3) {
754 self.set_velocity(handle as u32, vel.to_array());
755 }
756
757 pub fn num_bodies(&self) -> usize {
759 self.body_count()
760 }
761
762 pub fn gravity_vec3(&self) -> PyVec3 {
764 PyVec3::from_array(self.config.gravity)
765 }
766
767 pub fn all_positions_vec3(&self) -> Vec<PyVec3> {
769 self.all_positions()
770 .into_iter()
771 .map(PyVec3::from_array)
772 .collect()
773 }
774
775 fn get_body(&self, handle: u32) -> Option<&InternalBody> {
780 self.slots.get(handle as usize)?.body.as_ref()
781 }
782
783 fn get_body_mut(&mut self, handle: u32) -> Option<&mut InternalBody> {
784 self.slots.get_mut(handle as usize)?.body.as_mut()
785 }
786
787 fn detect_and_resolve_contacts(&mut self, dt: f64) {
793 let n = self.slots.len();
794 for i in 0..n {
795 for j in (i + 1)..n {
796 let (pos_a, vel_a, mass_a, static_a, sleeping_a, friction_a, rest_a, radius_a) = {
798 let body = match self.slots[i].body.as_ref() {
799 Some(b) => b,
800 None => continue,
801 };
802 let radius = first_sphere_radius(&body.shapes);
803 if radius <= 0.0 {
804 continue;
805 }
806 (
807 body.position,
808 body.velocity,
809 body.mass,
810 body.is_static,
811 body.is_sleeping,
812 body.friction,
813 body.restitution,
814 radius,
815 )
816 };
817
818 let (pos_b, vel_b, mass_b, static_b, sleeping_b, friction_b, rest_b, radius_b) = {
819 let body = match self.slots[j].body.as_ref() {
820 Some(b) => b,
821 None => continue,
822 };
823 let radius = first_sphere_radius(&body.shapes);
824 if radius <= 0.0 {
825 continue;
826 }
827 (
828 body.position,
829 body.velocity,
830 body.mass,
831 body.is_static,
832 body.is_sleeping,
833 body.friction,
834 body.restitution,
835 radius,
836 )
837 };
838
839 let diff = [
841 pos_b[0] - pos_a[0],
842 pos_b[1] - pos_a[1],
843 pos_b[2] - pos_a[2],
844 ];
845 let dist_sq = diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2];
846 let min_dist = radius_a + radius_b;
847 if dist_sq >= min_dist * min_dist {
848 continue;
849 }
850
851 let dist = dist_sq.sqrt();
852 let depth = min_dist - dist;
853 let normal = if dist > 1e-12 {
854 [diff[0] / dist, diff[1] / dist, diff[2] / dist]
855 } else {
856 [0.0, 1.0, 0.0]
857 };
858
859 let cp = [
861 pos_a[0] + normal[0] * radius_a,
862 pos_a[1] + normal[1] * radius_a,
863 pos_a[2] + normal[2] * radius_a,
864 ];
865
866 let combined_friction = (friction_a + friction_b) * 0.5;
868 let combined_rest = (rest_a + rest_b) * 0.5;
869
870 let rel_vel = [
872 vel_a[0] - vel_b[0],
873 vel_a[1] - vel_b[1],
874 vel_a[2] - vel_b[2],
875 ];
876 let rel_vel_normal =
877 rel_vel[0] * normal[0] + rel_vel[1] * normal[1] + rel_vel[2] * normal[2];
878
879 let impulse_mag = if rel_vel_normal < 0.0 {
881 let inv_m_a = if static_a || mass_a <= 0.0 {
882 0.0
883 } else {
884 1.0 / mass_a
885 };
886 let inv_m_b = if static_b || mass_b <= 0.0 {
887 0.0
888 } else {
889 1.0 / mass_b
890 };
891 let denom = inv_m_a + inv_m_b;
892 if denom > 1e-12 {
893 -(1.0 + combined_rest) * rel_vel_normal / denom
894 } else {
895 0.0
896 }
897 } else {
898 0.0
899 };
900
901 let correction_mag = (depth * self.config.baumgarte_factor) / dt.max(1e-6);
903
904 let contact = PyContactResult {
905 body_a: i as u32,
906 body_b: j as u32,
907 contact_point: cp,
908 normal,
909 depth,
910 friction: combined_friction,
911 restitution: combined_rest,
912 impulse: impulse_mag,
913 };
914 self.contacts.push(contact);
915
916 if !static_a && !sleeping_a && mass_a > 0.0 {
918 let inv_m_a = 1.0 / mass_a;
919 if let Some(ba) = self.slots[i].body.as_mut() {
920 ba.velocity[0] += impulse_mag * normal[0] * inv_m_a;
921 ba.velocity[1] += impulse_mag * normal[1] * inv_m_a;
922 ba.velocity[2] += impulse_mag * normal[2] * inv_m_a;
923 ba.position[0] -= normal[0] * correction_mag * inv_m_a * dt;
925 ba.position[1] -= normal[1] * correction_mag * inv_m_a * dt;
926 ba.position[2] -= normal[2] * correction_mag * inv_m_a * dt;
927 }
928 }
929 if !static_b && !sleeping_b && mass_b > 0.0 {
931 let inv_m_b = 1.0 / mass_b;
932 if let Some(bb) = self.slots[j].body.as_mut() {
933 bb.velocity[0] -= impulse_mag * normal[0] * inv_m_b;
934 bb.velocity[1] -= impulse_mag * normal[1] * inv_m_b;
935 bb.velocity[2] -= impulse_mag * normal[2] * inv_m_b;
936 bb.position[0] += normal[0] * correction_mag * inv_m_b * dt;
938 bb.position[1] += normal[1] * correction_mag * inv_m_b * dt;
939 bb.position[2] += normal[2] * correction_mag * inv_m_b * dt;
940 }
941 }
942 }
943 }
944 }
945
946 fn update_sleep(&mut self, dt: f64) {
947 let lin_thresh = self.config.linear_sleep_threshold;
948 let ang_thresh = self.config.angular_sleep_threshold;
949 let time_thresh = self.config.time_before_sleep;
950
951 for slot in &mut self.slots {
952 let body = match slot.body.as_mut() {
953 Some(b) => b,
954 None => continue,
955 };
956 if !body.can_sleep || body.is_static || body.is_kinematic {
957 continue;
958 }
959 if body.is_sleeping {
960 continue;
961 }
962
963 let lin_speed = (body.velocity[0] * body.velocity[0]
964 + body.velocity[1] * body.velocity[1]
965 + body.velocity[2] * body.velocity[2])
966 .sqrt();
967 let ang_speed = (body.angular_velocity[0] * body.angular_velocity[0]
968 + body.angular_velocity[1] * body.angular_velocity[1]
969 + body.angular_velocity[2] * body.angular_velocity[2])
970 .sqrt();
971
972 if lin_speed < lin_thresh && ang_speed < ang_thresh {
973 body.sleep_timer += dt;
974 if body.sleep_timer >= time_thresh {
975 body.is_sleeping = true;
976 body.velocity = [0.0; 3];
977 body.angular_velocity = [0.0; 3];
978 }
979 } else {
980 body.sleep_timer = 0.0;
981 }
982 }
983 }
984}
985
986fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
991 [
992 a[1] * b[2] - a[2] * b[1],
993 a[2] * b[0] - a[0] * b[2],
994 a[0] * b[1] - a[1] * b[0],
995 ]
996}
997
998fn normalize_quat(q: [f64; 4]) -> [f64; 4] {
999 let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
1000 if norm < 1e-12 {
1001 [0.0, 0.0, 0.0, 1.0]
1002 } else {
1003 [q[0] / norm, q[1] / norm, q[2] / norm, q[3] / norm]
1004 }
1005}
1006
1007fn integrate_orientation(q: [f64; 4], omega: [f64; 3], dt: f64) -> [f64; 4] {
1011 let (qx, qy, qz, qw) = (q[0], q[1], q[2], q[3]);
1012 let (wx, wy, wz) = (omega[0], omega[1], omega[2]);
1013 let half_dt = 0.5 * dt;
1014 let dqx = half_dt * (qw * wx + qy * wz - qz * wy);
1015 let dqy = half_dt * (qw * wy - qx * wz + qz * wx);
1016 let dqz = half_dt * (qw * wz + qx * wy - qy * wx);
1017 let dqw = half_dt * (-qx * wx - qy * wy - qz * wz);
1018 normalize_quat([qx + dqx, qy + dqy, qz + dqz, qw + dqw])
1019}
1020
1021fn approximate_inertia(body: &InternalBody) -> f64 {
1026 if body.mass <= 0.0 {
1027 return 0.0;
1028 }
1029 for shape in &body.shapes {
1030 if let PyColliderShape::Sphere { radius } = shape {
1031 return 0.4 * body.mass * radius * radius;
1033 }
1034 if let PyColliderShape::Box { half_extents } = shape {
1035 let hx = half_extents[0];
1037 let hy = half_extents[1];
1038 let hz = half_extents[2];
1039 let ixx = (1.0 / 3.0) * body.mass * (hy * hy + hz * hz);
1040 let iyy = (1.0 / 3.0) * body.mass * (hx * hx + hz * hz);
1041 let izz = (1.0 / 3.0) * body.mass * (hx * hx + hy * hy);
1042 return (ixx + iyy + izz) / 3.0;
1043 }
1044 }
1045 0.4 * body.mass
1047}
1048
1049fn first_sphere_radius(shapes: &[PyColliderShape]) -> f64 {
1051 for shape in shapes {
1052 if let PyColliderShape::Sphere { radius } = shape {
1053 return *radius;
1054 }
1055 }
1056 0.0
1057}