1#![allow(clippy::needless_range_loop)]
2#![allow(missing_docs)]
12#![allow(dead_code)]
13
14use serde::{Deserialize, Serialize};
15
16#[inline]
21fn vec3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
22 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
23}
24
25#[inline]
26fn vec3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
27 [a[0] * s, a[1] * s, a[2] * s]
28}
29
30#[inline]
31fn vec3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
32 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
33}
34
35#[inline]
36fn vec3_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
37 [
38 a[1] * b[2] - a[2] * b[1],
39 a[2] * b[0] - a[0] * b[2],
40 a[0] * b[1] - a[1] * b[0],
41 ]
42}
43
44#[inline]
45fn vec3_length(a: [f64; 3]) -> f64 {
46 vec3_dot(a, a).sqrt()
47}
48
49#[inline]
50fn quat_identity() -> [f64; 4] {
51 [0.0, 0.0, 0.0, 1.0]
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
60pub enum PyShapeType {
61 Sphere {
63 radius: f64,
65 },
66 Box {
68 half_extents: [f64; 3],
70 },
71 Capsule {
73 half_height: f64,
75 radius: f64,
77 },
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct PyRigidBody {
90 pub id: u32,
92 pub mass: f64,
94 pub inertia: [f64; 3],
96 pub position: [f64; 3],
98 pub orientation: [f64; 4],
100 pub velocity: [f64; 3],
102 pub ang_vel: [f64; 3],
104 pub accumulated_force: [f64; 3],
106 pub accumulated_torque: [f64; 3],
108 pub sleeping: bool,
110 pub is_static: bool,
112 pub is_kinematic: bool,
114 pub linear_damping: f64,
116 pub angular_damping: f64,
118}
119
120impl PyRigidBody {
121 pub fn new(id: u32, mass: f64, position: [f64; 3]) -> Self {
123 Self {
124 id,
125 mass,
126 inertia: [mass * 0.1; 3],
127 position,
128 orientation: quat_identity(),
129 velocity: [0.0; 3],
130 ang_vel: [0.0; 3],
131 accumulated_force: [0.0; 3],
132 accumulated_torque: [0.0; 3],
133 sleeping: false,
134 is_static: false,
135 is_kinematic: false,
136 linear_damping: 0.01,
137 angular_damping: 0.01,
138 }
139 }
140
141 pub fn new_static(id: u32, position: [f64; 3]) -> Self {
143 let mut b = Self::new(id, 0.0, position);
144 b.is_static = true;
145 b
146 }
147
148 pub fn new_kinematic(id: u32, mass: f64, position: [f64; 3]) -> Self {
150 let mut b = Self::new(id, mass, position);
151 b.is_kinematic = true;
152 b
153 }
154
155 pub fn apply_force(&mut self, force: [f64; 3]) {
157 if self.is_static || self.sleeping {
158 return;
159 }
160 self.accumulated_force = vec3_add(self.accumulated_force, force);
161 }
162
163 pub fn apply_torque(&mut self, torque: [f64; 3]) {
165 if self.is_static || self.sleeping {
166 return;
167 }
168 self.accumulated_torque = vec3_add(self.accumulated_torque, torque);
169 }
170
171 pub fn apply_impulse(&mut self, impulse: [f64; 3]) {
173 if self.is_static {
174 return;
175 }
176 if self.mass > 0.0 {
177 let inv_m = 1.0 / self.mass;
178 self.velocity = vec3_add(self.velocity, vec3_scale(impulse, inv_m));
179 }
180 self.sleeping = false;
181 }
182
183 pub fn apply_angular_impulse(&mut self, ang_impulse: [f64; 3]) {
185 if self.is_static {
186 return;
187 }
188 for i in 0..3 {
189 if self.inertia[i] > 0.0 {
190 self.ang_vel[i] += ang_impulse[i] / self.inertia[i];
191 }
192 }
193 self.sleeping = false;
194 }
195
196 pub fn clear_forces(&mut self) {
198 self.accumulated_force = [0.0; 3];
199 self.accumulated_torque = [0.0; 3];
200 }
201
202 pub fn integrate(&mut self, dt: f64, gravity: [f64; 3]) {
204 if self.is_static || self.is_kinematic || self.sleeping {
205 return;
206 }
207 let inv_m = if self.mass > 0.0 {
208 1.0 / self.mass
209 } else {
210 0.0
211 };
212 let grav_force = vec3_scale(gravity, self.mass);
214 let total_force = vec3_add(self.accumulated_force, grav_force);
215 let accel = vec3_scale(total_force, inv_m);
216 self.velocity = vec3_add(self.velocity, vec3_scale(accel, dt));
217 let lin_damp = (1.0 - self.linear_damping * dt).max(0.0);
219 self.velocity = vec3_scale(self.velocity, lin_damp);
220 self.position = vec3_add(self.position, vec3_scale(self.velocity, dt));
221 for i in 0..3 {
223 let alpha = if self.inertia[i] > 0.0 {
224 self.accumulated_torque[i] / self.inertia[i]
225 } else {
226 0.0
227 };
228 self.ang_vel[i] += alpha * dt;
229 }
230 let ang_damp = (1.0 - self.angular_damping * dt).max(0.0);
231 self.ang_vel = vec3_scale(self.ang_vel, ang_damp);
232 }
233
234 pub fn kinetic_energy(&self) -> f64 {
236 let ke_lin = 0.5 * self.mass * vec3_dot(self.velocity, self.velocity);
237 let ke_rot: f64 = self
238 .inertia
239 .iter()
240 .zip(self.ang_vel.iter())
241 .map(|(&i, &w)| 0.5 * i * w * w)
242 .sum();
243 ke_lin + ke_rot
244 }
245
246 pub fn linear_momentum(&self) -> [f64; 3] {
248 vec3_scale(self.velocity, self.mass)
249 }
250
251 pub fn inv_mass(&self) -> f64 {
253 if self.is_static || self.mass <= 0.0 {
254 0.0
255 } else {
256 1.0 / self.mass
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize, Default)]
267pub struct PyRigidBodySet {
268 bodies: Vec<Option<PyRigidBody>>,
269 next_id: u32,
270 count: usize,
271}
272
273impl PyRigidBodySet {
274 pub fn new() -> Self {
276 Self::default()
277 }
278
279 pub fn add(&mut self, mut body: PyRigidBody) -> u32 {
281 let id = self.next_id;
282 self.next_id += 1;
283 body.id = id;
284 if let Some(slot) = self.bodies.iter_mut().find(|s| s.is_none()) {
286 *slot = Some(body);
287 } else {
288 self.bodies.push(Some(body));
289 }
290 self.count += 1;
291 id
292 }
293
294 pub fn remove(&mut self, id: u32) -> bool {
296 for slot in &mut self.bodies {
297 if slot.as_ref().is_some_and(|b| b.id == id) {
298 *slot = None;
299 self.count -= 1;
300 return true;
301 }
302 }
303 false
304 }
305
306 pub fn get(&self, id: u32) -> Option<&PyRigidBody> {
308 self.bodies.iter().flatten().find(|b| b.id == id)
309 }
310
311 pub fn get_mut(&mut self, id: u32) -> Option<&mut PyRigidBody> {
313 self.bodies.iter_mut().flatten().find(|b| b.id == id)
314 }
315
316 pub fn len(&self) -> usize {
318 self.count
319 }
320
321 pub fn is_empty(&self) -> bool {
323 self.count == 0
324 }
325
326 pub fn iter(&self) -> impl Iterator<Item = &PyRigidBody> {
328 self.bodies.iter().flatten()
329 }
330
331 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut PyRigidBody> {
333 self.bodies.iter_mut().flatten()
334 }
335
336 pub fn get_position(&self, id: u32) -> Option<[f64; 3]> {
338 self.get(id).map(|b| b.position)
339 }
340
341 pub fn get_velocity(&self, id: u32) -> Option<[f64; 3]> {
343 self.get(id).map(|b| b.velocity)
344 }
345
346 pub fn apply_gravity(&mut self, gravity: [f64; 3]) {
348 for body in self.iter_mut() {
349 if !body.is_static && !body.is_kinematic && !body.sleeping {
350 let grav_force = vec3_scale(gravity, body.mass);
351 body.apply_force(grav_force);
352 }
353 }
354 }
355
356 pub fn integrate_all(&mut self, dt: f64, gravity: [f64; 3]) {
358 for body in self.iter_mut() {
359 body.integrate(dt, gravity);
360 body.clear_forces();
361 }
362 }
363
364 pub fn total_kinetic_energy(&self) -> f64 {
366 self.iter().map(|b| b.kinetic_energy()).sum()
367 }
368
369 pub fn sleeping_count(&self) -> usize {
371 self.iter().filter(|b| b.sleeping).count()
372 }
373
374 pub fn awake_count(&self) -> usize {
376 self.count - self.sleeping_count()
377 }
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct PyCollider {
390 pub id: u32,
392 pub body_id: u32,
394 pub shape: PyShapeType,
396 pub friction: f64,
398 pub restitution: f64,
400 pub is_sensor: bool,
403 pub local_offset: [f64; 3],
405 pub local_orientation: [f64; 4],
407}
408
409impl PyCollider {
410 pub fn sphere(id: u32, body_id: u32, radius: f64) -> Self {
412 Self {
413 id,
414 body_id,
415 shape: PyShapeType::Sphere { radius },
416 friction: 0.5,
417 restitution: 0.3,
418 is_sensor: false,
419 local_offset: [0.0; 3],
420 local_orientation: quat_identity(),
421 }
422 }
423
424 pub fn box_collider(id: u32, body_id: u32, half_extents: [f64; 3]) -> Self {
426 Self {
427 id,
428 body_id,
429 shape: PyShapeType::Box { half_extents },
430 friction: 0.5,
431 restitution: 0.3,
432 is_sensor: false,
433 local_offset: [0.0; 3],
434 local_orientation: quat_identity(),
435 }
436 }
437
438 pub fn capsule(id: u32, body_id: u32, half_height: f64, radius: f64) -> Self {
440 Self {
441 id,
442 body_id,
443 shape: PyShapeType::Capsule {
444 half_height,
445 radius,
446 },
447 friction: 0.5,
448 restitution: 0.3,
449 is_sensor: false,
450 local_offset: [0.0; 3],
451 local_orientation: quat_identity(),
452 }
453 }
454
455 pub fn as_sensor(mut self) -> Self {
457 self.is_sensor = true;
458 self
459 }
460
461 pub fn bounding_radius(&self) -> f64 {
463 match &self.shape {
464 PyShapeType::Sphere { radius } => *radius,
465 PyShapeType::Box { half_extents } => vec3_length(*half_extents),
466 PyShapeType::Capsule {
467 half_height,
468 radius,
469 } => half_height + radius,
470 }
471 }
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct PyContact {
481 pub body_a: u32,
483 pub body_b: u32,
485 pub point: [f64; 3],
487 pub normal: [f64; 3],
489 pub depth: f64,
491 pub impulse: f64,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct PyRayResult {
498 pub body_id: u32,
500 pub point: [f64; 3],
502 pub normal: [f64; 3],
504 pub distance: f64,
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
514pub enum PyJointType {
515 Ball,
517 Hinge,
519 Slider,
521 Fixed,
523 Generic6Dof,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct PyJoint {
530 pub id: u32,
532 pub joint_type: PyJointType,
534 pub body_a: u32,
536 pub body_b: u32,
538 pub anchor_a: [f64; 3],
540 pub anchor_b: [f64; 3],
542 pub axis: [f64; 3],
544 pub lower_limit: f64,
546 pub upper_limit: f64,
548 pub motor_speed: f64,
550 pub motor_max_force: f64,
552 pub motor_enabled: bool,
554 pub reaction_force: [f64; 3],
556 pub reaction_torque: [f64; 3],
558}
559
560impl PyJoint {
561 pub fn new(
563 id: u32,
564 joint_type: PyJointType,
565 body_a: u32,
566 body_b: u32,
567 anchor_a: [f64; 3],
568 anchor_b: [f64; 3],
569 ) -> Self {
570 Self {
571 id,
572 joint_type,
573 body_a,
574 body_b,
575 anchor_a,
576 anchor_b,
577 axis: [0.0, 1.0, 0.0],
578 lower_limit: f64::NEG_INFINITY,
579 upper_limit: f64::INFINITY,
580 motor_speed: 0.0,
581 motor_max_force: 0.0,
582 motor_enabled: false,
583 reaction_force: [0.0; 3],
584 reaction_torque: [0.0; 3],
585 }
586 }
587
588 pub fn enable_motor(&mut self, speed: f64, max_force: f64) {
590 self.motor_enabled = true;
591 self.motor_speed = speed;
592 self.motor_max_force = max_force;
593 }
594
595 pub fn disable_motor(&mut self) {
597 self.motor_enabled = false;
598 }
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize, Default)]
607pub struct PyJointSet {
608 joints: Vec<Option<PyJoint>>,
609 next_id: u32,
610}
611
612impl PyJointSet {
613 pub fn new() -> Self {
615 Self::default()
616 }
617
618 #[allow(clippy::too_many_arguments)]
620 pub fn create_joint(
621 &mut self,
622 joint_type: PyJointType,
623 body_a: u32,
624 body_b: u32,
625 anchor_a: [f64; 3],
626 anchor_b: [f64; 3],
627 _params: Option<serde_json::Value>,
628 ) -> u32 {
629 let id = self.next_id;
630 self.next_id += 1;
631 let joint = PyJoint::new(id, joint_type, body_a, body_b, anchor_a, anchor_b);
632 self.joints.push(Some(joint));
633 id
634 }
635
636 pub fn remove(&mut self, id: u32) -> bool {
638 for slot in &mut self.joints {
639 if slot.as_ref().is_some_and(|j| j.id == id) {
640 *slot = None;
641 return true;
642 }
643 }
644 false
645 }
646
647 pub fn get(&self, id: u32) -> Option<&PyJoint> {
649 self.joints.iter().flatten().find(|j| j.id == id)
650 }
651
652 pub fn get_mut(&mut self, id: u32) -> Option<&mut PyJoint> {
654 self.joints.iter_mut().flatten().find(|j| j.id == id)
655 }
656
657 pub fn set_motor(&mut self, id: u32, speed: f64, max_force: f64) -> bool {
659 if let Some(j) = self.get_mut(id) {
660 j.enable_motor(speed, max_force);
661 true
662 } else {
663 false
664 }
665 }
666
667 pub fn get_forces(&self, id: u32) -> Option<([f64; 3], [f64; 3])> {
669 self.get(id).map(|j| (j.reaction_force, j.reaction_torque))
670 }
671
672 pub fn len(&self) -> usize {
674 self.joints.iter().flatten().count()
675 }
676
677 pub fn is_empty(&self) -> bool {
679 self.len() == 0
680 }
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize)]
689pub struct PyWorld {
690 pub bodies: PyRigidBodySet,
692 colliders: Vec<PyCollider>,
694 pub joints: PyJointSet,
696 pub gravity: [f64; 3],
698 contacts: Vec<PyContact>,
700 pub time: f64,
702 next_collider_id: u32,
704}
705
706impl PyWorld {
707 pub fn new(gravity: [f64; 3]) -> Self {
709 Self {
710 bodies: PyRigidBodySet::new(),
711 colliders: Vec::new(),
712 joints: PyJointSet::new(),
713 gravity,
714 contacts: Vec::new(),
715 time: 0.0,
716 next_collider_id: 0,
717 }
718 }
719
720 pub fn earth_gravity() -> Self {
722 Self::new([0.0, -9.81, 0.0])
723 }
724
725 pub fn create_body(&mut self, mass: f64, position: [f64; 3]) -> u32 {
727 let body = PyRigidBody::new(0, mass, position);
728 self.bodies.add(body)
729 }
730
731 pub fn create_static_body(&mut self, position: [f64; 3]) -> u32 {
733 let body = PyRigidBody::new_static(0, position);
734 self.bodies.add(body)
735 }
736
737 pub fn create_collider(&mut self, collider_desc: PyCollider) -> u32 {
739 let id = self.next_collider_id;
740 self.next_collider_id += 1;
741 let mut c = collider_desc;
742 c.id = id;
743 self.colliders.push(c);
744 id
745 }
746
747 pub fn step(&mut self, dt: f64) {
749 self.contacts.clear();
750 let ids: Vec<u32> = self.bodies.iter().map(|b| b.id).collect();
752 for (i, &id_a) in ids.iter().enumerate() {
753 for &id_b in &ids[i + 1..] {
754 if let (Some(ba), Some(bb)) = (self.bodies.get(id_a), self.bodies.get(id_b)) {
755 if ba.is_static && bb.is_static {
756 continue;
757 }
758 let dx = [
759 bb.position[0] - ba.position[0],
760 bb.position[1] - ba.position[1],
761 bb.position[2] - ba.position[2],
762 ];
763 let dist = vec3_length(dx);
764 let ra = self
766 .colliders
767 .iter()
768 .filter(|c| c.body_id == id_a)
769 .map(|c| c.bounding_radius())
770 .fold(0.0f64, f64::max);
771 let rb = self
772 .colliders
773 .iter()
774 .filter(|c| c.body_id == id_b)
775 .map(|c| c.bounding_radius())
776 .fold(0.0f64, f64::max);
777 if ra > 0.0 && rb > 0.0 && dist < ra + rb {
778 let depth = ra + rb - dist;
779 let normal = if dist > 1e-12 {
780 [dx[0] / dist, dx[1] / dist, dx[2] / dist]
781 } else {
782 [0.0, 1.0, 0.0]
783 };
784 let midpoint = [
785 (ba.position[0] + bb.position[0]) * 0.5,
786 (ba.position[1] + bb.position[1]) * 0.5,
787 (ba.position[2] + bb.position[2]) * 0.5,
788 ];
789 self.contacts.push(PyContact {
790 body_a: id_a,
791 body_b: id_b,
792 point: midpoint,
793 normal,
794 depth,
795 impulse: 0.0,
796 });
797 }
798 }
799 }
800 }
801 for contact in &self.contacts {
803 let inv_a = self
804 .bodies
805 .get(contact.body_a)
806 .map(|b| b.inv_mass())
807 .unwrap_or(0.0);
808 let inv_b = self
809 .bodies
810 .get(contact.body_b)
811 .map(|b| b.inv_mass())
812 .unwrap_or(0.0);
813 let denom = inv_a + inv_b;
814 if denom < 1e-15 {
815 continue;
816 }
817 let restitution = 0.3_f64;
818 let vel_a = self
819 .bodies
820 .get(contact.body_a)
821 .map(|b| b.velocity)
822 .unwrap_or([0.0; 3]);
823 let vel_b = self
824 .bodies
825 .get(contact.body_b)
826 .map(|b| b.velocity)
827 .unwrap_or([0.0; 3]);
828 let rel_vel = [
829 vel_a[0] - vel_b[0],
830 vel_a[1] - vel_b[1],
831 vel_a[2] - vel_b[2],
832 ];
833 let vn = vec3_dot(rel_vel, contact.normal);
834 if vn > 0.0 {
835 continue; }
837 let j_scalar = -(1.0 + restitution) * vn / denom;
838 let impulse = vec3_scale(contact.normal, j_scalar);
839 if let Some(ba) = self.bodies.get_mut(contact.body_a) {
840 ba.apply_impulse(impulse);
841 }
842 if let Some(bb) = self.bodies.get_mut(contact.body_b) {
843 bb.apply_impulse(vec3_scale(impulse, -1.0));
844 }
845 let correction_scale = (contact.depth - 0.01_f64).max(0.0) / denom * 0.4;
847 let correction = vec3_scale(contact.normal, correction_scale);
848 if let Some(ba) = self.bodies.get_mut(contact.body_a)
849 && !ba.is_static
850 {
851 let corr = vec3_scale(correction, ba.inv_mass());
852 ba.position = vec3_add(ba.position, corr);
853 }
854 if let Some(bb) = self.bodies.get_mut(contact.body_b)
855 && !bb.is_static
856 {
857 let corr = vec3_scale(correction, bb.inv_mass());
858 bb.position = [
859 bb.position[0] - corr[0],
860 bb.position[1] - corr[1],
861 bb.position[2] - corr[2],
862 ];
863 }
864 }
865 self.bodies.integrate_all(dt, self.gravity);
867 self.time += dt;
868 }
869
870 pub fn query_ray(
872 &self,
873 origin: [f64; 3],
874 direction: [f64; 3],
875 max_dist: f64,
876 ) -> Vec<PyRayResult> {
877 let mut results = Vec::new();
878 let dir_len = vec3_length(direction);
879 if dir_len < 1e-12 {
880 return results;
881 }
882 let dir_n = vec3_scale(direction, 1.0 / dir_len);
883 for body in self.bodies.iter() {
884 let to_body = [
886 body.position[0] - origin[0],
887 body.position[1] - origin[1],
888 body.position[2] - origin[2],
889 ];
890 let tc = vec3_dot(to_body, dir_n);
891 if tc < 0.0 {
892 continue;
893 }
894 let closest = [
895 origin[0] + dir_n[0] * tc,
896 origin[1] + dir_n[1] * tc,
897 origin[2] + dir_n[2] * tc,
898 ];
899 let miss = [
900 closest[0] - body.position[0],
901 closest[1] - body.position[1],
902 closest[2] - body.position[2],
903 ];
904 let r = self
906 .colliders
907 .iter()
908 .filter(|c| c.body_id == body.id)
909 .map(|c| c.bounding_radius())
910 .fold(0.5_f64, f64::max);
911 let miss_dist = vec3_length(miss);
912 if miss_dist > r {
913 continue;
914 }
915 let dist = tc - (r * r - miss_dist * miss_dist).max(0.0).sqrt();
916 if dist > max_dist {
917 continue;
918 }
919 let hit_point = [
920 origin[0] + dir_n[0] * dist,
921 origin[1] + dir_n[1] * dist,
922 origin[2] + dir_n[2] * dist,
923 ];
924 let normal = {
925 let n = [
926 hit_point[0] - body.position[0],
927 hit_point[1] - body.position[1],
928 hit_point[2] - body.position[2],
929 ];
930 let nl = vec3_length(n);
931 if nl > 1e-12 {
932 vec3_scale(n, 1.0 / nl)
933 } else {
934 [0.0, 1.0, 0.0]
935 }
936 };
937 results.push(PyRayResult {
938 body_id: body.id,
939 point: hit_point,
940 normal,
941 distance: dist,
942 });
943 }
944 results.sort_by(|a, b| {
945 a.distance
946 .partial_cmp(&b.distance)
947 .unwrap_or(std::cmp::Ordering::Equal)
948 });
949 results
950 }
951
952 pub fn query_overlap(&self) -> Vec<(u32, u32)> {
954 let mut pairs = Vec::new();
955 for i in 0..self.colliders.len() {
956 for j in (i + 1)..self.colliders.len() {
957 let ca = &self.colliders[i];
958 let cb = &self.colliders[j];
959 if ca.body_id == cb.body_id {
960 continue;
961 }
962 let ba = self.bodies.get(ca.body_id);
963 let bb = self.bodies.get(cb.body_id);
964 if let (Some(ba), Some(bb)) = (ba, bb) {
965 let d = vec3_length([
966 bb.position[0] - ba.position[0],
967 bb.position[1] - ba.position[1],
968 bb.position[2] - ba.position[2],
969 ]);
970 if d < ca.bounding_radius() + cb.bounding_radius() {
971 pairs.push((ca.body_id, cb.body_id));
972 }
973 }
974 }
975 }
976 pairs
977 }
978
979 pub fn get_contacts(&self) -> &[PyContact] {
981 &self.contacts
982 }
983}
984
985#[derive(Debug, Clone, Serialize, Deserialize)]
991pub struct PyPipelineConfig {
992 pub max_iterations: u32,
994 pub sleep_threshold: f64,
996 pub sleep_time: f64,
998 pub gravity: [f64; 3],
1000 pub substeps: u32,
1002}
1003
1004impl Default for PyPipelineConfig {
1005 fn default() -> Self {
1006 Self {
1007 max_iterations: 10,
1008 sleep_threshold: 0.05,
1009 sleep_time: 1.0,
1010 gravity: [0.0, -9.81, 0.0],
1011 substeps: 1,
1012 }
1013 }
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1018pub struct PyPhysicsPipeline {
1019 pub config: PyPipelineConfig,
1021 pub world: PyWorld,
1023 pub step_count: u64,
1025 pub last_broadphase_pairs: usize,
1027 pub last_contact_count: usize,
1029 pub last_solver_iterations: u32,
1031}
1032
1033impl PyPhysicsPipeline {
1034 pub fn new() -> Self {
1036 let config = PyPipelineConfig::default();
1037 let world = PyWorld::new(config.gravity);
1038 Self {
1039 config,
1040 world,
1041 step_count: 0,
1042 last_broadphase_pairs: 0,
1043 last_contact_count: 0,
1044 last_solver_iterations: 0,
1045 }
1046 }
1047
1048 pub fn with_config(config: PyPipelineConfig) -> Self {
1050 let world = PyWorld::new(config.gravity);
1051 Self {
1052 config,
1053 world,
1054 step_count: 0,
1055 last_broadphase_pairs: 0,
1056 last_contact_count: 0,
1057 last_solver_iterations: 0,
1058 }
1059 }
1060
1061 pub fn step(&mut self, dt: f64) {
1063 let sub_dt = dt / self.config.substeps as f64;
1064 for _ in 0..self.config.substeps {
1065 self.world.step(sub_dt);
1066 }
1067 self.last_contact_count = self.world.contacts.len();
1068 self.last_broadphase_pairs = self.last_contact_count;
1069 self.last_solver_iterations =
1070 self.config
1071 .max_iterations
1072 .min(if self.last_contact_count > 0 {
1073 self.config.max_iterations
1074 } else {
1075 0
1076 });
1077 self.step_count += 1;
1078 }
1079
1080 pub fn set_max_iterations(&mut self, n: u32) {
1082 self.config.max_iterations = n;
1083 }
1084
1085 pub fn world(&self) -> &PyWorld {
1087 &self.world
1088 }
1089
1090 pub fn world_mut(&mut self) -> &mut PyWorld {
1092 &mut self.world
1093 }
1094}
1095
1096impl Default for PyPhysicsPipeline {
1097 fn default() -> Self {
1098 Self::new()
1099 }
1100}
1101
1102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1108pub enum WakeReason {
1109 Force,
1111 Contact,
1113 Script,
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1119pub struct PyWakeEvent {
1120 pub body_id: u32,
1122 pub reason: WakeReason,
1124 pub time: f64,
1126}
1127
1128impl PyWakeEvent {
1129 pub fn new(body_id: u32, reason: WakeReason, time: f64) -> Self {
1131 Self {
1132 body_id,
1133 reason,
1134 time,
1135 }
1136 }
1137}
1138
1139#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1141pub struct PySleepManager {
1142 pub events: Vec<PyWakeEvent>,
1144 sleep_timers: Vec<(u32, f64)>,
1146}
1147
1148impl PySleepManager {
1149 pub fn new() -> Self {
1151 Self::default()
1152 }
1153
1154 pub fn wake_body(&mut self, body: &mut PyRigidBody, reason: WakeReason, time: f64) {
1156 if body.sleeping {
1157 body.sleeping = false;
1158 self.events.push(PyWakeEvent::new(body.id, reason, time));
1159 }
1160 }
1161
1162 pub fn sleep_body(&mut self, body: &mut PyRigidBody) {
1164 body.sleeping = true;
1165 body.velocity = [0.0; 3];
1166 body.ang_vel = [0.0; 3];
1167 }
1168
1169 pub fn update(
1171 &mut self,
1172 bodies: &mut PyRigidBodySet,
1173 threshold: f64,
1174 sleep_time: f64,
1175 dt: f64,
1176 ) {
1177 let ids: Vec<u32> = bodies.iter().map(|b| b.id).collect();
1178 for id in ids {
1179 if let Some(body) = bodies.get_mut(id) {
1180 if body.is_static || body.is_kinematic {
1181 continue;
1182 }
1183 let speed = vec3_length(body.velocity) + vec3_length(body.ang_vel);
1184 if speed < threshold {
1185 let timer = self.sleep_timers.iter_mut().find(|(bid, _)| *bid == id);
1186 match timer {
1187 Some((_, t)) => {
1188 *t += dt;
1189 if *t >= sleep_time && !body.sleeping {
1190 body.sleeping = true;
1191 }
1192 }
1193 None => {
1194 self.sleep_timers.push((id, dt));
1195 }
1196 }
1197 } else {
1198 self.sleep_timers.retain(|(bid, _)| *bid != id);
1199 }
1200 }
1201 }
1202 }
1203
1204 pub fn drain_events(&mut self) -> Vec<PyWakeEvent> {
1206 std::mem::take(&mut self.events)
1207 }
1208}
1209
1210#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1216pub struct PyBodyFilter {
1217 pub include_active: bool,
1219 pub include_sleeping: bool,
1221 pub include_static: bool,
1223 pub include_kinematic: bool,
1225}
1226
1227impl PyBodyFilter {
1228 pub fn all() -> Self {
1230 Self {
1231 include_active: true,
1232 include_sleeping: true,
1233 include_static: true,
1234 include_kinematic: true,
1235 }
1236 }
1237
1238 pub fn active_dynamic() -> Self {
1240 Self {
1241 include_active: true,
1242 include_sleeping: false,
1243 include_static: false,
1244 include_kinematic: false,
1245 }
1246 }
1247
1248 pub fn matches(&self, body: &PyRigidBody) -> bool {
1250 if body.is_static {
1251 return self.include_static;
1252 }
1253 if body.is_kinematic {
1254 return self.include_kinematic;
1255 }
1256 if body.sleeping {
1257 self.include_sleeping
1258 } else {
1259 self.include_active
1260 }
1261 }
1262
1263 pub fn filter_by_type<'a>(&self, bodies: &'a PyRigidBodySet) -> Vec<&'a PyRigidBody> {
1265 bodies.iter().filter(|b| self.matches(b)).collect()
1266 }
1267}
1268
1269#[derive(Debug, Clone, Serialize, Deserialize)]
1277pub struct PyIslandStats {
1278 pub island_id: u32,
1280 pub body_count: usize,
1282 pub constraint_count: usize,
1284 pub sleep_timer: f64,
1286 pub energy: f64,
1288}
1289
1290impl PyIslandStats {
1291 pub fn new(island_id: u32, body_count: usize, constraint_count: usize) -> Self {
1293 Self {
1294 island_id,
1295 body_count,
1296 constraint_count,
1297 sleep_timer: 0.0,
1298 energy: 0.0,
1299 }
1300 }
1301
1302 pub fn can_sleep(&self, threshold: f64, sleep_time: f64) -> bool {
1304 self.energy < threshold && self.sleep_timer >= sleep_time
1305 }
1306}
1307
1308pub fn compute_island_stats(bodies: &PyRigidBodySet) -> Vec<PyIslandStats> {
1310 bodies
1312 .iter()
1313 .enumerate()
1314 .map(|(i, b)| {
1315 let mut stats = PyIslandStats::new(i as u32, 1, 0);
1316 stats.energy = b.kinetic_energy();
1317 stats
1318 })
1319 .collect()
1320}
1321
1322#[derive(Debug, Clone, Serialize, Deserialize)]
1328pub struct PyBatchForce {
1329 pub body_ids: Vec<u32>,
1331 pub forces: Vec<[f64; 3]>,
1333 pub torques: Option<Vec<[f64; 3]>>,
1335}
1336
1337impl PyBatchForce {
1338 pub fn new(body_ids: Vec<u32>, forces: Vec<[f64; 3]>) -> Self {
1340 Self {
1341 body_ids,
1342 forces,
1343 torques: None,
1344 }
1345 }
1346
1347 pub fn with_torques(mut self, torques: Vec<[f64; 3]>) -> Self {
1349 self.torques = Some(torques);
1350 self
1351 }
1352
1353 pub fn apply(&self, bodies: &mut PyRigidBodySet) {
1355 for (idx, &id) in self.body_ids.iter().enumerate() {
1356 if let Some(body) = bodies.get_mut(id) {
1357 if idx < self.forces.len() {
1358 body.apply_force(self.forces[idx]);
1359 }
1360 if let Some(torques) = &self.torques
1361 && idx < torques.len()
1362 {
1363 body.apply_torque(torques[idx]);
1364 }
1365 }
1366 }
1367 }
1368
1369 pub fn is_valid(&self) -> bool {
1371 self.body_ids.len() == self.forces.len()
1372 }
1373}
1374
1375#[cfg(test)]
1380mod tests {
1381 use super::*;
1382
1383 #[test]
1386 fn test_rigid_body_new() {
1387 let b = PyRigidBody::new(1, 5.0, [0.0, 10.0, 0.0]);
1388 assert_eq!(b.mass, 5.0);
1389 assert_eq!(b.position, [0.0, 10.0, 0.0]);
1390 assert!(!b.sleeping);
1391 assert!(!b.is_static);
1392 }
1393
1394 #[test]
1395 fn test_rigid_body_static() {
1396 let b = PyRigidBody::new_static(2, [1.0, 0.0, 0.0]);
1397 assert!(b.is_static);
1398 assert_eq!(b.inv_mass(), 0.0);
1399 }
1400
1401 #[test]
1402 fn test_rigid_body_kinematic() {
1403 let b = PyRigidBody::new_kinematic(3, 2.0, [0.0; 3]);
1404 assert!(b.is_kinematic);
1405 assert!(!b.is_static);
1406 }
1407
1408 #[test]
1409 fn test_apply_force() {
1410 let mut b = PyRigidBody::new(1, 1.0, [0.0; 3]);
1411 b.apply_force([1.0, 0.0, 0.0]);
1412 assert!((b.accumulated_force[0] - 1.0).abs() < 1e-12);
1413 }
1414
1415 #[test]
1416 fn test_apply_force_static_ignored() {
1417 let mut b = PyRigidBody::new_static(1, [0.0; 3]);
1418 b.apply_force([999.0, 0.0, 0.0]);
1419 assert_eq!(b.accumulated_force[0], 0.0);
1420 }
1421
1422 #[test]
1423 fn test_apply_impulse() {
1424 let mut b = PyRigidBody::new(1, 2.0, [0.0; 3]);
1425 b.apply_impulse([4.0, 0.0, 0.0]);
1426 assert!((b.velocity[0] - 2.0).abs() < 1e-12);
1427 }
1428
1429 #[test]
1430 fn test_apply_impulse_static_ignored() {
1431 let mut b = PyRigidBody::new_static(1, [0.0; 3]);
1432 b.apply_impulse([999.0, 0.0, 0.0]);
1433 assert_eq!(b.velocity[0], 0.0);
1434 }
1435
1436 #[test]
1437 fn test_kinetic_energy() {
1438 let mut b = PyRigidBody::new(1, 2.0, [0.0; 3]);
1439 b.velocity = [1.0, 0.0, 0.0];
1440 assert!((b.kinetic_energy() - 1.0).abs() < 0.01);
1442 }
1443
1444 #[test]
1445 fn test_integrate_gravity() {
1446 let mut b = PyRigidBody::new(1, 1.0, [0.0, 10.0, 0.0]);
1447 let gravity = [0.0, -9.81, 0.0];
1448 b.integrate(0.1, gravity);
1449 assert!(b.position[1] < 10.0);
1451 }
1452
1453 #[test]
1454 fn test_linear_momentum() {
1455 let mut b = PyRigidBody::new(1, 3.0, [0.0; 3]);
1456 b.velocity = [2.0, 0.0, 0.0];
1457 let p = b.linear_momentum();
1458 assert!((p[0] - 6.0).abs() < 1e-12);
1459 }
1460
1461 #[test]
1464 fn test_body_set_add_remove() {
1465 let mut set = PyRigidBodySet::new();
1466 let id = set.add(PyRigidBody::new(0, 1.0, [0.0; 3]));
1467 assert_eq!(set.len(), 1);
1468 assert!(set.remove(id));
1469 assert_eq!(set.len(), 0);
1470 }
1471
1472 #[test]
1473 fn test_body_set_get() {
1474 let mut set = PyRigidBodySet::new();
1475 let id = set.add(PyRigidBody::new(0, 5.0, [1.0, 2.0, 3.0]));
1476 let b = set.get(id).unwrap();
1477 assert_eq!(b.mass, 5.0);
1478 }
1479
1480 #[test]
1481 fn test_body_set_is_empty() {
1482 let set = PyRigidBodySet::new();
1483 assert!(set.is_empty());
1484 }
1485
1486 #[test]
1487 fn test_body_set_total_ke() {
1488 let mut set = PyRigidBodySet::new();
1489 let id = set.add(PyRigidBody::new(0, 2.0, [0.0; 3]));
1490 let b = set.get_mut(id).unwrap();
1491 b.velocity = [1.0, 0.0, 0.0];
1492 let ke = set.total_kinetic_energy();
1493 assert!(ke > 0.0);
1494 }
1495
1496 #[test]
1499 fn test_collider_sphere() {
1500 let c = PyCollider::sphere(0, 1, 0.5);
1501 assert_eq!(c.bounding_radius(), 0.5);
1502 assert!(!c.is_sensor);
1503 }
1504
1505 #[test]
1506 fn test_collider_box() {
1507 let c = PyCollider::box_collider(0, 1, [1.0, 2.0, 3.0]);
1508 assert!(c.bounding_radius() > 0.0);
1509 }
1510
1511 #[test]
1512 fn test_collider_capsule() {
1513 let c = PyCollider::capsule(0, 1, 1.0, 0.5);
1514 assert_eq!(c.bounding_radius(), 1.5);
1515 }
1516
1517 #[test]
1518 fn test_collider_sensor() {
1519 let c = PyCollider::sphere(0, 1, 0.3).as_sensor();
1520 assert!(c.is_sensor);
1521 }
1522
1523 #[test]
1526 fn test_world_step_gravity() {
1527 let mut w = PyWorld::earth_gravity();
1528 let id = w.create_body(1.0, [0.0, 10.0, 0.0]);
1529 w.step(0.1);
1530 let pos = w.bodies.get_position(id).unwrap();
1531 assert!(pos[1] < 10.0);
1532 }
1533
1534 #[test]
1535 fn test_world_static_body_unmoved() {
1536 let mut w = PyWorld::earth_gravity();
1537 let id = w.create_static_body([0.0, 0.0, 0.0]);
1538 w.step(1.0);
1539 let pos = w.bodies.get_position(id).unwrap();
1540 assert_eq!(pos, [0.0, 0.0, 0.0]);
1541 }
1542
1543 #[test]
1544 fn test_world_ray_cast() {
1545 let mut w = PyWorld::earth_gravity();
1546 let id = w.create_body(1.0, [0.0, 0.0, 5.0]);
1547 w.create_collider(PyCollider::sphere(0, id, 1.0));
1548 let hits = w.query_ray([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], 100.0);
1549 assert!(!hits.is_empty());
1550 }
1551
1552 #[test]
1553 fn test_world_get_contacts() {
1554 let mut w = PyWorld::new([0.0; 3]);
1555 let a = w.create_body(1.0, [0.0; 3]);
1556 let b = w.create_body(1.0, [0.5, 0.0, 0.0]);
1557 w.create_collider(PyCollider::sphere(0, a, 0.4));
1558 w.create_collider(PyCollider::sphere(0, b, 0.4));
1559 w.step(0.001);
1560 let contacts = w.get_contacts();
1561 assert!(!contacts.is_empty());
1562 }
1563
1564 #[test]
1567 fn test_joint_set_create_remove() {
1568 let mut js = PyJointSet::new();
1569 let id = js.create_joint(PyJointType::Hinge, 0, 1, [0.0; 3], [0.0; 3], None);
1570 assert_eq!(js.len(), 1);
1571 assert!(js.remove(id));
1572 assert_eq!(js.len(), 0);
1573 }
1574
1575 #[test]
1576 fn test_joint_set_motor() {
1577 let mut js = PyJointSet::new();
1578 let id = js.create_joint(PyJointType::Hinge, 0, 1, [0.0; 3], [0.0; 3], None);
1579 assert!(js.set_motor(id, 5.0, 100.0));
1580 let j = js.get(id).unwrap();
1581 assert!(j.motor_enabled);
1582 assert_eq!(j.motor_speed, 5.0);
1583 }
1584
1585 #[test]
1588 fn test_pipeline_step() {
1589 let mut pipe = PyPhysicsPipeline::new();
1590 let _id = pipe.world_mut().create_body(1.0, [0.0, 5.0, 0.0]);
1591 pipe.step(0.016);
1592 assert_eq!(pipe.step_count, 1);
1593 }
1594
1595 #[test]
1596 fn test_pipeline_max_iterations() {
1597 let mut pipe = PyPhysicsPipeline::new();
1598 pipe.set_max_iterations(5);
1599 assert_eq!(pipe.config.max_iterations, 5);
1600 }
1601
1602 #[test]
1605 fn test_body_filter_all() {
1606 let f = PyBodyFilter::all();
1607 let static_b = PyRigidBody::new_static(1, [0.0; 3]);
1608 assert!(f.matches(&static_b));
1609 let dyn_b = PyRigidBody::new(2, 1.0, [0.0; 3]);
1610 assert!(f.matches(&dyn_b));
1611 }
1612
1613 #[test]
1614 fn test_body_filter_active_only() {
1615 let f = PyBodyFilter::active_dynamic();
1616 let mut b = PyRigidBody::new(1, 1.0, [0.0; 3]);
1617 b.sleeping = false;
1618 assert!(f.matches(&b));
1619 b.sleeping = true;
1620 assert!(!f.matches(&b));
1621 }
1622
1623 #[test]
1626 fn test_island_stats_new() {
1627 let s = PyIslandStats::new(0, 5, 4);
1628 assert_eq!(s.body_count, 5);
1629 assert!(!s.can_sleep(0.1, 1.0));
1630 }
1631
1632 #[test]
1635 fn test_batch_force_apply() {
1636 let mut set = PyRigidBodySet::new();
1637 let a = set.add(PyRigidBody::new(0, 1.0, [0.0; 3]));
1638 let b = set.add(PyRigidBody::new(0, 1.0, [1.0, 0.0, 0.0]));
1639 let batch = PyBatchForce::new(vec![a, b], vec![[10.0, 0.0, 0.0], [0.0, 10.0, 0.0]]);
1640 assert!(batch.is_valid());
1641 batch.apply(&mut set);
1642 assert!((set.get(a).unwrap().accumulated_force[0] - 10.0).abs() < 1e-12);
1643 assert!((set.get(b).unwrap().accumulated_force[1] - 10.0).abs() < 1e-12);
1644 }
1645
1646 #[test]
1647 fn test_batch_force_invalid() {
1648 let batch = PyBatchForce::new(vec![0, 1], vec![[1.0, 0.0, 0.0]]);
1649 assert!(!batch.is_valid());
1650 }
1651
1652 #[test]
1655 fn test_sleep_manager_wake() {
1656 let mut mgr = PySleepManager::new();
1657 let mut set = PyRigidBodySet::new();
1658 let id = set.add(PyRigidBody::new(0, 1.0, [0.0; 3]));
1659 let body = set.get_mut(id).unwrap();
1660 body.sleeping = true;
1661 mgr.wake_body(body, WakeReason::Script, 0.0);
1662 assert!(!body.sleeping);
1663 assert_eq!(mgr.events.len(), 1);
1664 }
1665
1666 #[test]
1667 fn test_sleep_manager_sleep() {
1668 let mut mgr = PySleepManager::new();
1669 let mut set = PyRigidBodySet::new();
1670 let id = set.add(PyRigidBody::new(0, 1.0, [0.0; 3]));
1671 let body = set.get_mut(id).unwrap();
1672 body.velocity = [10.0, 0.0, 0.0];
1673 mgr.sleep_body(body);
1674 assert!(body.sleeping);
1675 assert_eq!(body.velocity, [0.0; 3]);
1676 }
1677
1678 #[test]
1679 fn test_compute_island_stats() {
1680 let mut set = PyRigidBodySet::new();
1681 set.add(PyRigidBody::new(0, 1.0, [0.0; 3]));
1682 set.add(PyRigidBody::new(0, 2.0, [1.0, 0.0, 0.0]));
1683 let islands = compute_island_stats(&set);
1684 assert_eq!(islands.len(), 2);
1685 }
1686}