1#[inline]
17pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
18 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
19}
20
21#[inline]
23pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
24 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
25}
26
27#[inline]
29pub fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
30 [a[0] * s, a[1] * s, a[2] * s]
31}
32
33#[inline]
35pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
36 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
37}
38
39#[inline]
41pub fn norm3(a: [f64; 3]) -> f64 {
42 dot3(a, a).sqrt()
43}
44
45#[inline]
47pub fn normalize3(a: [f64; 3]) -> [f64; 3] {
48 let n = norm3(a);
49 if n < 1e-14 {
50 [0.0; 3]
51 } else {
52 scale3(a, 1.0 / n)
53 }
54}
55
56#[inline]
58pub fn clampf(v: f64, lo: f64, hi: f64) -> f64 {
59 v.max(lo).min(hi)
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum JointType {
69 Revolute,
71 Prismatic,
73 BallSocket,
75}
76
77#[derive(Debug, Clone)]
83pub struct JointLimits {
84 pub lower: f64,
86 pub upper: f64,
88 pub enabled: bool,
90}
91
92impl JointLimits {
93 pub fn new(lower: f64, upper: f64) -> Self {
95 Self {
96 lower,
97 upper,
98 enabled: true,
99 }
100 }
101
102 #[inline]
104 pub fn is_violated(&self, value: f64) -> bool {
105 self.enabled && (value < self.lower || value > self.upper)
106 }
107
108 #[inline]
110 pub fn penetration(&self, value: f64) -> f64 {
111 if value < self.lower {
112 self.lower - value
113 } else if value > self.upper {
114 value - self.upper
115 } else {
116 0.0
117 }
118 }
119
120 #[inline]
122 pub fn clamp(&self, value: f64) -> f64 {
123 clampf(value, self.lower, self.upper)
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq)]
133pub enum StopType {
134 Hard,
136 Soft {
138 stiffness: f64,
140 damping: f64,
142 },
143}
144
145impl StopType {
146 pub fn constraint_force(&self, penetration: f64, velocity: f64) -> f64 {
151 match *self {
152 StopType::Hard => {
153 if penetration > 0.0 {
155 1e9 * penetration
156 } else {
157 0.0
158 }
159 }
160 StopType::Soft { stiffness, damping } => {
161 if penetration > 0.0 {
162 stiffness * penetration - damping * velocity.min(0.0)
163 } else {
164 0.0
165 }
166 }
167 }
168 }
169}
170
171#[derive(Debug, Clone)]
177pub struct JointLimitConstraint {
178 pub body_a: usize,
180 pub body_b: usize,
182 pub joint_type: JointType,
184 pub limits: JointLimits,
186 pub stop_type: StopType,
188 pub position: f64,
190 pub velocity: f64,
192 pub warm_impulse: f64,
194 pub axis: [f64; 3],
196}
197
198impl JointLimitConstraint {
199 pub fn new(
201 body_a: usize,
202 body_b: usize,
203 joint_type: JointType,
204 limits: JointLimits,
205 stop_type: StopType,
206 axis: [f64; 3],
207 ) -> Self {
208 Self {
209 body_a,
210 body_b,
211 joint_type,
212 limits,
213 stop_type,
214 position: 0.0,
215 velocity: 0.0,
216 warm_impulse: 0.0,
217 axis: normalize3(axis),
218 }
219 }
220
221 pub fn is_active(&self) -> bool {
223 self.limits.is_violated(self.position)
224 }
225
226 pub fn penetration_depth(&self) -> f64 {
228 self.limits.penetration(self.position)
229 }
230
231 pub fn compute_impulse(&self, inv_mass_a: f64, inv_mass_b: f64, dt: f64) -> f64 {
236 let pen = self.penetration_depth();
237 if pen <= 0.0 {
238 return 0.0;
239 }
240 let force = self.stop_type.constraint_force(pen, self.velocity);
241 let eff_mass = inv_mass_a + inv_mass_b;
242 if eff_mass < 1e-14 {
243 return 0.0;
244 }
245 force * dt / eff_mass
246 }
247
248 pub fn apply_warm_start(&self) -> f64 {
252 self.warm_impulse.max(0.0)
253 }
254
255 pub fn store_impulse(&mut self, impulse: f64) {
257 self.warm_impulse = impulse.max(0.0);
258 }
259}
260
261#[derive(Debug, Clone)]
269pub struct BallSocketLimitConstraint {
270 pub body_a: usize,
272 pub body_b: usize,
274 pub cone_axis: [f64; 3],
276 pub max_angle: f64,
278 pub stop_type: StopType,
280 pub current_angle: f64,
282 pub warm_impulse: f64,
284}
285
286impl BallSocketLimitConstraint {
287 pub fn new(
289 body_a: usize,
290 body_b: usize,
291 cone_axis: [f64; 3],
292 max_angle: f64,
293 stop_type: StopType,
294 ) -> Self {
295 Self {
296 body_a,
297 body_b,
298 cone_axis: normalize3(cone_axis),
299 max_angle,
300 stop_type,
301 current_angle: 0.0,
302 warm_impulse: 0.0,
303 }
304 }
305
306 pub fn update_angle(&mut self, child_axis_world: [f64; 3]) {
308 let d = dot3(self.cone_axis, normalize3(child_axis_world));
309 let d_clamped = clampf(d, -1.0, 1.0);
310 self.current_angle = d_clamped.acos();
311 }
312
313 pub fn penetration(&self) -> f64 {
315 (self.current_angle - self.max_angle).max(0.0)
316 }
317
318 pub fn is_violated(&self) -> bool {
320 self.current_angle > self.max_angle
321 }
322
323 pub fn compute_impulse(&self, inv_inertia_a: f64, inv_inertia_b: f64, dt: f64) -> f64 {
325 let pen = self.penetration();
326 if pen <= 0.0 {
327 return 0.0;
328 }
329 let force = self.stop_type.constraint_force(pen, 0.0);
330 let eff = inv_inertia_a + inv_inertia_b;
331 if eff < 1e-14 { 0.0 } else { force * dt / eff }
332 }
333}
334
335#[derive(Debug, Clone)]
344pub struct GearRatioCollision {
345 pub joint_a: usize,
347 pub joint_b: usize,
349 pub ratio: f64,
351 pub efficiency: f64,
353}
354
355impl GearRatioCollision {
356 pub fn new(joint_a: usize, joint_b: usize, ratio: f64, efficiency: f64) -> Self {
358 Self {
359 joint_a,
360 joint_b,
361 ratio,
362 efficiency: efficiency.clamp(0.0, 1.0),
363 }
364 }
365
366 pub fn propagate_impulse(&self, impulse_a: f64) -> f64 {
371 impulse_a * self.ratio * self.efficiency
372 }
373
374 pub fn reflected_inertia(&self, inertia_b: f64) -> f64 {
378 inertia_b * self.ratio * self.ratio
379 }
380}
381
382#[derive(Debug, Clone)]
391pub struct CableChainLimit {
392 pub body_a: usize,
394 pub body_b: usize,
396 pub min_length: f64,
398 pub max_length: f64,
400 pub current_length: f64,
402 pub relative_velocity: f64,
404 pub impulse: f64,
406 pub stiffness: f64,
408 pub damping: f64,
410}
411
412impl CableChainLimit {
413 pub fn new(
415 body_a: usize,
416 body_b: usize,
417 min_length: f64,
418 max_length: f64,
419 stiffness: f64,
420 damping: f64,
421 ) -> Self {
422 Self {
423 body_a,
424 body_b,
425 min_length,
426 max_length,
427 current_length: 0.0,
428 relative_velocity: 0.0,
429 impulse: 0.0,
430 stiffness,
431 damping,
432 }
433 }
434
435 pub fn extension_violation(&self) -> f64 {
437 (self.current_length - self.max_length).max(0.0)
438 }
439
440 pub fn compression_violation(&self) -> f64 {
442 (self.min_length - self.current_length).max(0.0)
443 }
444
445 pub fn net_penetration(&self) -> f64 {
447 self.extension_violation() + self.compression_violation()
448 }
449
450 pub fn compute_impulse(&self, inv_mass_a: f64, inv_mass_b: f64, dt: f64) -> f64 {
452 let pen = self.net_penetration();
453 if pen <= 0.0 {
454 return 0.0;
455 }
456 let force = self.stiffness * pen - self.damping * self.relative_velocity.min(0.0);
457 let eff = inv_mass_a + inv_mass_b;
458 if eff < 1e-14 { 0.0 } else { force * dt / eff }
459 }
460}
461
462#[derive(Debug, Clone)]
471pub struct ArticulatedBodyJointStop {
472 pub joint_id: usize,
474 pub effective_inertia: f64,
476 pub position: f64,
478 pub velocity: f64,
480 pub limits: JointLimits,
482 pub stop_type: StopType,
484 pub warm_impulse: f64,
486}
487
488impl ArticulatedBodyJointStop {
489 pub fn new(
491 joint_id: usize,
492 effective_inertia: f64,
493 limits: JointLimits,
494 stop_type: StopType,
495 ) -> Self {
496 Self {
497 joint_id,
498 effective_inertia,
499 position: 0.0,
500 velocity: 0.0,
501 limits,
502 stop_type,
503 warm_impulse: 0.0,
504 }
505 }
506
507 pub fn compute_impulse(&self, dt: f64) -> f64 {
509 let pen = self.limits.penetration(self.position);
510 if pen <= 0.0 {
511 return 0.0;
512 }
513 let force = self.stop_type.constraint_force(pen, self.velocity);
514 if self.effective_inertia < 1e-14 {
515 0.0
516 } else {
517 force * dt / self.effective_inertia
518 }
519 }
520
521 pub fn velocity_impulse(&self, restitution: f64) -> f64 {
526 if !self.limits.is_violated(self.position) {
527 return 0.0;
528 }
529 let vel_out = -(1.0 + restitution) * self.velocity;
530 if self.effective_inertia < 1e-14 {
531 0.0
532 } else {
533 vel_out / self.effective_inertia
534 }
535 }
536}
537
538#[derive(Debug, Clone)]
546pub struct MotorHardLimit {
547 pub max_torque: f64,
549 pub max_velocity: f64,
551 pub position_limits: JointLimits,
553 pub angle: f64,
555 pub angular_velocity: f64,
557 pub inertia: f64,
559 pub stalled: bool,
561}
562
563impl MotorHardLimit {
564 pub fn new(max_torque: f64, max_velocity: f64, lower: f64, upper: f64, inertia: f64) -> Self {
566 Self {
567 max_torque,
568 max_velocity,
569 position_limits: JointLimits::new(lower, upper),
570 angle: 0.0,
571 angular_velocity: 0.0,
572 inertia,
573 stalled: false,
574 }
575 }
576
577 pub fn clamp_torque(&self, torque: f64) -> f64 {
579 clampf(torque, -self.max_torque, self.max_torque)
580 }
581
582 pub fn clamp_velocity(&self, vel: f64) -> f64 {
584 clampf(vel, -self.max_velocity, self.max_velocity)
585 }
586
587 pub fn check_stall(&mut self) {
589 self.stalled = self.position_limits.is_violated(self.angle);
590 }
591
592 pub fn limit_impulse(&self, dt: f64) -> f64 {
594 if !self.stalled {
595 return 0.0;
596 }
597 let pen = self.position_limits.penetration(self.angle);
598 if self.inertia < 1e-14 {
599 0.0
600 } else {
601 1e6 * pen * dt / self.inertia
603 }
604 }
605}
606
607#[derive(Debug, Clone)]
615pub struct JointFrictionConstraint {
616 pub body_a: usize,
618 pub body_b: usize,
620 pub axis: [f64; 3],
622 pub friction_coefficient: f64,
624 pub normal_force: f64,
626 pub relative_velocity: f64,
628 pub warm_impulse: f64,
630}
631
632impl JointFrictionConstraint {
633 pub fn new(body_a: usize, body_b: usize, axis: [f64; 3], friction_coefficient: f64) -> Self {
635 Self {
636 body_a,
637 body_b,
638 axis: normalize3(axis),
639 friction_coefficient,
640 normal_force: 0.0,
641 relative_velocity: 0.0,
642 warm_impulse: 0.0,
643 }
644 }
645
646 pub fn max_friction_impulse(&self, inv_mass_eff: f64, dt: f64) -> f64 {
648 let _ = dt;
649 let max_f = self.friction_coefficient * self.normal_force.abs();
650 let _ = inv_mass_eff;
651 max_f
652 }
653
654 pub fn compute_impulse(&self, inv_mass_eff: f64, dt: f64) -> f64 {
657 if inv_mass_eff < 1e-14 {
658 return 0.0;
659 }
660 let delta_v = -self.relative_velocity;
661 let raw_impulse = delta_v / inv_mass_eff;
662 let max_imp = self.max_friction_impulse(inv_mass_eff, dt);
663 clampf(raw_impulse, -max_imp, max_imp)
664 }
665
666 pub fn warm_start_impulse(&self, inv_mass_eff: f64, dt: f64) -> f64 {
668 let max_imp = self.max_friction_impulse(inv_mass_eff, dt);
669 clampf(self.warm_impulse, -max_imp, max_imp)
670 }
671}
672
673#[derive(Debug, Clone, Default)]
682pub struct JointComplianceParams {
683 pub erp: f64,
686 pub cfm: f64,
688}
689
690impl JointComplianceParams {
691 pub fn new(erp: f64, cfm: f64) -> Self {
693 Self {
694 erp: clampf(erp, 0.0, 1.0),
695 cfm: cfm.max(0.0),
696 }
697 }
698
699 pub fn rigid() -> Self {
701 Self::new(0.2, 0.0)
702 }
703
704 pub fn soft(stiffness: f64, damping: f64, dt: f64) -> Self {
706 let denom = dt * stiffness + damping;
708 if denom < 1e-14 {
709 Self::rigid()
710 } else {
711 Self::new(dt * stiffness / denom, 1.0 / denom)
712 }
713 }
714
715 pub fn positional_bias(&self, error: f64, dt: f64) -> f64 {
719 if dt < 1e-14 {
720 0.0
721 } else {
722 self.erp * error / dt
723 }
724 }
725
726 pub fn softened_keff(&self, k_eff: f64) -> f64 {
730 k_eff + self.cfm
731 }
732}
733
734#[derive(Debug, Clone, Default)]
740pub struct JointLimitWarmStart {
741 entries: Vec<WarmEntry>,
743}
744
745#[derive(Debug, Clone)]
747struct WarmEntry {
748 body_a: usize,
749 body_b: usize,
750 axis_idx: usize,
751 impulse: f64,
752}
753
754impl JointLimitWarmStart {
755 pub fn new() -> Self {
757 Self {
758 entries: Vec::new(),
759 }
760 }
761
762 pub fn store(&mut self, body_a: usize, body_b: usize, axis_idx: usize, impulse: f64) {
764 for entry in &mut self.entries {
765 if entry.body_a == body_a && entry.body_b == body_b && entry.axis_idx == axis_idx {
766 entry.impulse = impulse;
767 return;
768 }
769 }
770 self.entries.push(WarmEntry {
771 body_a,
772 body_b,
773 axis_idx,
774 impulse,
775 });
776 }
777
778 pub fn retrieve(&self, body_a: usize, body_b: usize, axis_idx: usize) -> f64 {
780 for entry in &self.entries {
781 if entry.body_a == body_a && entry.body_b == body_b && entry.axis_idx == axis_idx {
782 return entry.impulse;
783 }
784 }
785 0.0
786 }
787
788 pub fn clear(&mut self) {
790 self.entries.clear();
791 }
792
793 pub fn len(&self) -> usize {
795 self.entries.len()
796 }
797
798 pub fn is_empty(&self) -> bool {
800 self.entries.is_empty()
801 }
802
803 pub fn scale(&mut self, factor: f64) {
805 for entry in &mut self.entries {
806 entry.impulse *= factor;
807 }
808 }
809}
810
811#[derive(Debug, Clone)]
819pub struct JointLimitSolver {
820 pub constraints: Vec<JointLimitConstraint>,
822 pub warm_start: JointLimitWarmStart,
824 pub compliance: JointComplianceParams,
826 pub iterations: usize,
828}
829
830impl JointLimitSolver {
831 pub fn new(iterations: usize) -> Self {
833 Self {
834 constraints: Vec::new(),
835 warm_start: JointLimitWarmStart::new(),
836 compliance: JointComplianceParams::rigid(),
837 iterations,
838 }
839 }
840
841 pub fn add_constraint(&mut self, c: JointLimitConstraint) {
843 self.constraints.push(c);
844 }
845
846 pub fn apply_warm_starts(&mut self) {
848 for c in &mut self.constraints {
849 let wi = self.warm_start.retrieve(c.body_a, c.body_b, 0);
850 c.warm_impulse = wi;
851 }
852 }
853
854 pub fn solve(&mut self, inv_masses: &[f64], dt: f64) -> f64 {
859 self.apply_warm_starts();
861 let mut total_impulse = 0.0f64;
862 for _iter in 0..self.iterations {
863 for c in &mut self.constraints {
864 let im_a = if c.body_a < inv_masses.len() {
865 inv_masses[c.body_a]
866 } else {
867 0.0
868 };
869 let im_b = if c.body_b < inv_masses.len() {
870 inv_masses[c.body_b]
871 } else {
872 0.0
873 };
874 let impulse = c.compute_impulse(im_a, im_b, dt);
875 let new_acc = (c.warm_impulse + impulse).max(0.0);
876 let delta = new_acc - c.warm_impulse;
877 c.warm_impulse = new_acc;
878 total_impulse += delta.abs();
879 }
880 }
881 for c in &self.constraints {
883 self.warm_start.store(c.body_a, c.body_b, 0, c.warm_impulse);
884 }
885 total_impulse
886 }
887
888 pub fn clear_constraints(&mut self) {
890 self.constraints.clear();
891 }
892}
893
894#[derive(Debug, Clone)]
900pub struct JointStopContact {
901 pub joint_id: usize,
903 pub normal: [f64; 3],
905 pub depth: f64,
907 pub relative_velocity: f64,
909 pub restitution: f64,
911}
912
913impl JointStopContact {
914 pub fn new(
916 joint_id: usize,
917 normal: [f64; 3],
918 depth: f64,
919 relative_velocity: f64,
920 restitution: f64,
921 ) -> Self {
922 Self {
923 joint_id,
924 normal: normalize3(normal),
925 depth,
926 relative_velocity,
927 restitution,
928 }
929 }
930
931 pub fn compute_impulse(&self, eff_mass: f64, beta: f64, dt: f64) -> f64 {
937 if eff_mass < 1e-14 {
938 return 0.0;
939 }
940 let baumgarte_bias = beta * self.depth / dt;
941 let restitution_bias = self.restitution * self.relative_velocity.min(0.0).abs();
942 let target_vel = baumgarte_bias + restitution_bias;
943 let delta_v = target_vel - self.relative_velocity.min(0.0);
944 (delta_v / eff_mass).max(0.0)
945 }
946}
947
948#[derive(Debug, Clone)]
954pub struct PrismaticJointLimit {
955 pub body_a: usize,
957 pub body_b: usize,
959 pub axis: [f64; 3],
961 pub limits: JointLimits,
963 pub stop_type: StopType,
965 pub translation: f64,
967 pub velocity: f64,
969 pub warm_impulse: f64,
971 pub compliance: JointComplianceParams,
973}
974
975impl PrismaticJointLimit {
976 pub fn new(
978 body_a: usize,
979 body_b: usize,
980 axis: [f64; 3],
981 lower: f64,
982 upper: f64,
983 stop_type: StopType,
984 compliance: JointComplianceParams,
985 ) -> Self {
986 Self {
987 body_a,
988 body_b,
989 axis: normalize3(axis),
990 limits: JointLimits::new(lower, upper),
991 stop_type,
992 translation: 0.0,
993 velocity: 0.0,
994 warm_impulse: 0.0,
995 compliance,
996 }
997 }
998
999 pub fn penetration(&self) -> f64 {
1001 self.limits.penetration(self.translation)
1002 }
1003
1004 pub fn bias_velocity(&self, dt: f64) -> f64 {
1006 self.compliance.positional_bias(self.penetration(), dt)
1007 }
1008
1009 pub fn constraint_row(&self) -> f64 {
1011 if self.translation < self.limits.lower {
1012 1.0
1013 } else if self.translation > self.limits.upper {
1014 -1.0
1015 } else {
1016 0.0
1017 }
1018 }
1019
1020 pub fn solve_impulse(&self, inv_mass_a: f64, inv_mass_b: f64, dt: f64) -> f64 {
1022 let pen = self.penetration();
1023 if pen <= 0.0 {
1024 return 0.0;
1025 }
1026 let bias = self.bias_velocity(dt);
1027 let k_eff_raw = inv_mass_a + inv_mass_b;
1028 let k_eff = self.compliance.softened_keff(k_eff_raw);
1029 if k_eff < 1e-14 {
1030 return 0.0;
1031 }
1032 let lambda = (bias - self.velocity) / k_eff;
1033 lambda.max(0.0)
1034 }
1035}
1036
1037#[derive(Debug, Clone)]
1043pub struct RevoluteJointLimit {
1044 pub body_a: usize,
1046 pub body_b: usize,
1048 pub axis: [f64; 3],
1050 pub limits: JointLimits,
1052 pub stop_type: StopType,
1054 pub angle: f64,
1056 pub ang_velocity: f64,
1058 pub warm_impulse: f64,
1060 pub compliance: JointComplianceParams,
1062 pub restitution: f64,
1064}
1065
1066impl RevoluteJointLimit {
1067 pub fn new(
1069 body_a: usize,
1070 body_b: usize,
1071 axis: [f64; 3],
1072 lower: f64,
1073 upper: f64,
1074 stop_type: StopType,
1075 compliance: JointComplianceParams,
1076 restitution: f64,
1077 ) -> Self {
1078 Self {
1079 body_a,
1080 body_b,
1081 axis: normalize3(axis),
1082 limits: JointLimits::new(lower, upper),
1083 stop_type,
1084 angle: 0.0,
1085 ang_velocity: 0.0,
1086 warm_impulse: 0.0,
1087 compliance,
1088 restitution,
1089 }
1090 }
1091
1092 pub fn penetration(&self) -> f64 {
1094 self.limits.penetration(self.angle)
1095 }
1096
1097 pub fn is_active(&self) -> bool {
1099 self.limits.is_violated(self.angle)
1100 }
1101
1102 pub fn solve_impulse(&self, inv_inertia_a: f64, inv_inertia_b: f64, dt: f64) -> f64 {
1107 let pen = self.penetration();
1108 if pen <= 0.0 {
1109 return 0.0;
1110 }
1111 let bias = self.compliance.positional_bias(pen, dt);
1112 let restitution_term = if self.ang_velocity.abs() > 1e-6 {
1113 self.restitution * self.ang_velocity.abs()
1114 } else {
1115 0.0
1116 };
1117 let k_eff_raw = inv_inertia_a + inv_inertia_b;
1118 let k_eff = self.compliance.softened_keff(k_eff_raw);
1119 if k_eff < 1e-14 {
1120 return 0.0;
1121 }
1122 let target = bias + restitution_term;
1123 let lambda = (target - self.ang_velocity) / k_eff;
1124 (self.warm_impulse + lambda).max(0.0) - self.warm_impulse
1125 }
1126}
1127
1128#[derive(Debug, Clone, Default)]
1134pub struct JointLimitPipeline {
1135 pub revolute: Vec<RevoluteJointLimit>,
1137 pub prismatic: Vec<PrismaticJointLimit>,
1139 pub ball_socket: Vec<BallSocketLimitConstraint>,
1141 pub cables: Vec<CableChainLimit>,
1143 pub ab_stops: Vec<ArticulatedBodyJointStop>,
1145 pub motors: Vec<MotorHardLimit>,
1147 pub warm_start: JointLimitWarmStart,
1149 pub compliance: JointComplianceParams,
1151 pub iterations: usize,
1153}
1154
1155impl JointLimitPipeline {
1156 pub fn new(iterations: usize) -> Self {
1158 Self {
1159 revolute: Vec::new(),
1160 prismatic: Vec::new(),
1161 ball_socket: Vec::new(),
1162 cables: Vec::new(),
1163 ab_stops: Vec::new(),
1164 motors: Vec::new(),
1165 warm_start: JointLimitWarmStart::new(),
1166 compliance: JointComplianceParams::rigid(),
1167 iterations,
1168 }
1169 }
1170
1171 pub fn step(&mut self, inv_masses: &[f64], inv_inertias: &[f64], dt: f64) -> f64 {
1175 let mut total = 0.0f64;
1176
1177 for c in &mut self.revolute {
1179 let ia = if c.body_a < inv_inertias.len() {
1180 inv_inertias[c.body_a]
1181 } else {
1182 0.0
1183 };
1184 let ib = if c.body_b < inv_inertias.len() {
1185 inv_inertias[c.body_b]
1186 } else {
1187 0.0
1188 };
1189 for _ in 0..self.iterations {
1190 let imp = c.solve_impulse(ia, ib, dt);
1191 c.warm_impulse = (c.warm_impulse + imp).max(0.0);
1192 total += imp.abs();
1193 }
1194 }
1195
1196 for c in &mut self.prismatic {
1198 let ma = if c.body_a < inv_masses.len() {
1199 inv_masses[c.body_a]
1200 } else {
1201 0.0
1202 };
1203 let mb = if c.body_b < inv_masses.len() {
1204 inv_masses[c.body_b]
1205 } else {
1206 0.0
1207 };
1208 for _ in 0..self.iterations {
1209 let imp = c.solve_impulse(ma, mb, dt);
1210 c.warm_impulse = (c.warm_impulse + imp).max(0.0);
1211 total += imp.abs();
1212 }
1213 }
1214
1215 for c in &mut self.ab_stops {
1217 for _ in 0..self.iterations {
1218 let imp = c.compute_impulse(dt);
1219 c.warm_impulse = (c.warm_impulse + imp).max(0.0);
1220 total += imp.abs();
1221 }
1222 }
1223
1224 for c in &self.cables {
1226 let ma = if c.body_a < inv_masses.len() {
1227 inv_masses[c.body_a]
1228 } else {
1229 0.0
1230 };
1231 let mb = if c.body_b < inv_masses.len() {
1232 inv_masses[c.body_b]
1233 } else {
1234 0.0
1235 };
1236 let imp = c.compute_impulse(ma, mb, dt);
1237 total += imp.abs();
1238 }
1239
1240 total
1241 }
1242
1243 pub fn active_count(&self) -> usize {
1245 self.revolute.iter().filter(|c| c.is_active()).count()
1246 + self
1247 .prismatic
1248 .iter()
1249 .filter(|c| c.limits.is_violated(c.translation))
1250 .count()
1251 + self.ball_socket.iter().filter(|c| c.is_violated()).count()
1252 }
1253}
1254
1255pub fn wrap_angle(angle: f64) -> f64 {
1261 use std::f64::consts::PI;
1262 let mut a = angle;
1263 while a > PI {
1264 a -= 2.0 * PI;
1265 }
1266 while a <= -PI {
1267 a += 2.0 * PI;
1268 }
1269 a
1270}
1271
1272pub fn angle_diff(a: f64, b: f64) -> f64 {
1274 wrap_angle(b - a)
1275}
1276
1277#[cfg(test)]
1282mod tests {
1283 use super::*;
1284 use std::f64::consts::PI;
1285
1286 #[test]
1288 fn test_joint_limits_violation() {
1289 let lim = JointLimits::new(-1.0, 1.0);
1290 assert!(lim.is_violated(-1.5));
1291 assert!(lim.is_violated(1.5));
1292 assert!(!lim.is_violated(0.0));
1293 assert!(!lim.is_violated(-1.0));
1294 assert!(!lim.is_violated(1.0));
1295 }
1296
1297 #[test]
1299 fn test_joint_limits_penetration() {
1300 let lim = JointLimits::new(-PI / 2.0, PI / 2.0);
1301 let pen = lim.penetration(-PI);
1302 assert!((pen - (PI / 2.0)).abs() < 1e-10);
1303 assert_eq!(lim.penetration(0.0), 0.0);
1304 }
1305
1306 #[test]
1308 fn test_joint_limits_clamp() {
1309 let lim = JointLimits::new(-1.0, 2.0);
1310 assert_eq!(lim.clamp(-5.0), -1.0);
1311 assert_eq!(lim.clamp(5.0), 2.0);
1312 assert_eq!(lim.clamp(0.5), 0.5);
1313 }
1314
1315 #[test]
1317 fn test_stop_type_hard_force() {
1318 let s = StopType::Hard;
1319 assert!(s.constraint_force(0.1, 0.0) > 0.0);
1320 assert_eq!(s.constraint_force(0.0, 0.0), 0.0);
1321 }
1322
1323 #[test]
1325 fn test_stop_type_soft_force() {
1326 let s = StopType::Soft {
1327 stiffness: 1000.0,
1328 damping: 10.0,
1329 };
1330 let f = s.constraint_force(0.01, 0.0);
1331 assert!((f - 10.0).abs() < 1e-10);
1332 }
1333
1334 #[test]
1336 fn test_stop_type_soft_no_violation() {
1337 let s = StopType::Soft {
1338 stiffness: 1000.0,
1339 damping: 10.0,
1340 };
1341 assert_eq!(s.constraint_force(-0.1, 0.0), 0.0);
1342 }
1343
1344 #[test]
1346 fn test_joint_limit_constraint_impulse() {
1347 let lim = JointLimits::new(-1.0, 1.0);
1348 let mut c = JointLimitConstraint::new(
1349 0,
1350 1,
1351 JointType::Revolute,
1352 lim,
1353 StopType::Soft {
1354 stiffness: 1000.0,
1355 damping: 0.0,
1356 },
1357 [0.0, 0.0, 1.0],
1358 );
1359 c.position = 1.5; let imp = c.compute_impulse(1.0, 1.0, 0.01);
1361 assert!(imp > 0.0, "impulse should be positive");
1362 }
1363
1364 #[test]
1366 fn test_warm_start_positive_clamped() {
1367 let lim = JointLimits::new(-1.0, 1.0);
1368 let mut c = JointLimitConstraint::new(
1369 0,
1370 1,
1371 JointType::Revolute,
1372 lim,
1373 StopType::Hard,
1374 [1.0, 0.0, 0.0],
1375 );
1376 c.store_impulse(-5.0);
1377 assert_eq!(c.apply_warm_start(), 0.0);
1379 c.store_impulse(3.0);
1380 assert_eq!(c.apply_warm_start(), 3.0);
1381 }
1382
1383 #[test]
1385 fn test_ball_socket_angle_update() {
1386 let mut bs =
1387 BallSocketLimitConstraint::new(0, 1, [0.0, 0.0, 1.0], PI / 4.0, StopType::Hard);
1388 bs.update_angle([0.0, 0.0, 1.0]);
1390 assert!(bs.current_angle.abs() < 1e-10);
1391 assert!(!bs.is_violated());
1392 bs.update_angle([1.0, 0.0, 0.0]);
1394 assert!((bs.current_angle - PI / 2.0).abs() < 1e-10);
1395 assert!(bs.is_violated());
1396 }
1397
1398 #[test]
1400 fn test_ball_socket_penetration() {
1401 let mut bs =
1402 BallSocketLimitConstraint::new(0, 1, [0.0, 0.0, 1.0], PI / 6.0, StopType::Hard);
1403 bs.current_angle = PI / 3.0;
1404 let pen = bs.penetration();
1405 assert!((pen - PI / 6.0).abs() < 1e-10);
1406 }
1407
1408 #[test]
1410 fn test_gear_propagate_impulse() {
1411 let gear = GearRatioCollision::new(0, 1, 3.0, 1.0);
1412 assert!((gear.propagate_impulse(10.0) - 30.0).abs() < 1e-10);
1413 }
1414
1415 #[test]
1417 fn test_gear_reflected_inertia() {
1418 let gear = GearRatioCollision::new(0, 1, 2.0, 0.9);
1419 assert!((gear.reflected_inertia(5.0) - 20.0).abs() < 1e-10);
1421 }
1422
1423 #[test]
1425 fn test_cable_extension_violation() {
1426 let mut cable = CableChainLimit::new(0, 1, 0.0, 2.0, 1000.0, 10.0);
1427 cable.current_length = 2.5;
1428 assert!((cable.extension_violation() - 0.5).abs() < 1e-10);
1429 assert_eq!(cable.compression_violation(), 0.0);
1430 }
1431
1432 #[test]
1434 fn test_chain_compression_violation() {
1435 let mut chain = CableChainLimit::new(0, 1, 1.0, 3.0, 500.0, 5.0);
1436 chain.current_length = 0.5;
1437 assert!((chain.compression_violation() - 0.5).abs() < 1e-10);
1438 }
1439
1440 #[test]
1442 fn test_cable_impulse_positive_on_violation() {
1443 let mut cable = CableChainLimit::new(0, 1, 0.0, 1.0, 1000.0, 0.0);
1444 cable.current_length = 1.2;
1445 let imp = cable.compute_impulse(1.0, 1.0, 0.01);
1446 assert!(imp > 0.0);
1447 }
1448
1449 #[test]
1451 fn test_ab_stop_impulse() {
1452 let lim = JointLimits::new(-1.0, 1.0);
1453 let mut stop = ArticulatedBodyJointStop::new(
1454 0,
1455 2.0,
1456 lim,
1457 StopType::Soft {
1458 stiffness: 500.0,
1459 damping: 0.0,
1460 },
1461 );
1462 stop.position = 1.5;
1463 let imp = stop.compute_impulse(0.01);
1464 assert!(imp > 0.0);
1465 }
1466
1467 #[test]
1469 fn test_ab_stop_velocity_impulse_restitution() {
1470 let lim = JointLimits::new(-1.0, 1.0);
1471 let mut stop = ArticulatedBodyJointStop::new(0, 1.0, lim, StopType::Hard);
1472 stop.position = 1.5;
1473 stop.velocity = -2.0;
1474 let vi = stop.velocity_impulse(0.5);
1476 assert!((vi - 3.0).abs() < 1e-10);
1477 }
1478
1479 #[test]
1481 fn test_motor_clamp_torque() {
1482 let m = MotorHardLimit::new(10.0, 5.0, -PI, PI, 0.1);
1483 assert_eq!(m.clamp_torque(20.0), 10.0);
1484 assert_eq!(m.clamp_torque(-20.0), -10.0);
1485 assert_eq!(m.clamp_torque(5.0), 5.0);
1486 }
1487
1488 #[test]
1490 fn test_motor_stall_detection() {
1491 let mut m = MotorHardLimit::new(10.0, 5.0, -PI, PI, 0.1);
1492 m.angle = PI + 0.1; m.check_stall();
1494 assert!(m.stalled);
1495 m.angle = 0.0;
1496 m.check_stall();
1497 assert!(!m.stalled);
1498 }
1499
1500 #[test]
1502 fn test_motor_limit_impulse_not_stalled() {
1503 let m = MotorHardLimit::new(10.0, 5.0, -PI, PI, 0.1);
1504 assert_eq!(m.limit_impulse(0.01), 0.0);
1505 }
1506
1507 #[test]
1509 fn test_friction_impulse_computed() {
1510 let mut fric = JointFrictionConstraint::new(0, 1, [0.0, 0.0, 1.0], 0.3);
1511 fric.normal_force = 100.0;
1512 fric.relative_velocity = 2.0;
1513 let imp = fric.compute_impulse(2.0, 0.01);
1514 assert!((imp - (-1.0)).abs() < 1e-10);
1516 }
1517
1518 #[test]
1520 fn test_friction_warm_start_clamped() {
1521 let mut fric = JointFrictionConstraint::new(0, 1, [1.0, 0.0, 0.0], 0.1);
1522 fric.normal_force = 50.0;
1523 fric.warm_impulse = 1000.0; let ws = fric.warm_start_impulse(1.0, 0.01);
1525 assert!(ws <= 5.0 + 1e-10);
1527 }
1528
1529 #[test]
1531 fn test_compliance_rigid() {
1532 let c = JointComplianceParams::rigid();
1533 assert!((c.erp - 0.2).abs() < 1e-10);
1534 assert_eq!(c.cfm, 0.0);
1535 }
1536
1537 #[test]
1539 fn test_compliance_soft() {
1540 let c = JointComplianceParams::soft(1000.0, 10.0, 0.01);
1541 assert!((c.erp - 0.5).abs() < 1e-10);
1543 assert!((c.cfm - 0.05).abs() < 1e-10);
1544 }
1545
1546 #[test]
1548 fn test_compliance_positional_bias() {
1549 let c = JointComplianceParams::new(0.5, 0.0);
1550 let bias = c.positional_bias(0.1, 0.01);
1551 assert!((bias - 5.0).abs() < 1e-10);
1553 }
1554
1555 #[test]
1557 fn test_warm_start_store_retrieve() {
1558 let mut ws = JointLimitWarmStart::new();
1559 ws.store(0, 1, 2, 5.5);
1560 assert!((ws.retrieve(0, 1, 2) - 5.5).abs() < 1e-10);
1561 assert_eq!(ws.retrieve(0, 1, 3), 0.0); }
1563
1564 #[test]
1566 fn test_warm_start_scale() {
1567 let mut ws = JointLimitWarmStart::new();
1568 ws.store(0, 1, 0, 10.0);
1569 ws.scale(0.5);
1570 assert!((ws.retrieve(0, 1, 0) - 5.0).abs() < 1e-10);
1571 }
1572
1573 #[test]
1575 fn test_warm_start_clear() {
1576 let mut ws = JointLimitWarmStart::new();
1577 ws.store(0, 1, 0, 1.0);
1578 ws.clear();
1579 assert!(ws.is_empty());
1580 }
1581
1582 #[test]
1584 fn test_solver_no_violation() {
1585 let mut solver = JointLimitSolver::new(4);
1586 let lim = JointLimits::new(-1.0, 1.0);
1587 let c = JointLimitConstraint::new(
1588 0,
1589 1,
1590 JointType::Revolute,
1591 lim,
1592 StopType::Soft {
1593 stiffness: 100.0,
1594 damping: 0.0,
1595 },
1596 [0.0, 0.0, 1.0],
1597 );
1598 solver.add_constraint(c);
1599 let inv_masses = vec![1.0, 1.0];
1601 let total = solver.solve(&inv_masses, 0.01);
1602 assert_eq!(total, 0.0);
1603 }
1604
1605 #[test]
1607 fn test_solver_violation_produces_impulse() {
1608 let mut solver = JointLimitSolver::new(4);
1609 let lim = JointLimits::new(-1.0, 1.0);
1610 let mut c = JointLimitConstraint::new(
1611 0,
1612 1,
1613 JointType::Revolute,
1614 lim,
1615 StopType::Soft {
1616 stiffness: 100.0,
1617 damping: 0.0,
1618 },
1619 [0.0, 0.0, 1.0],
1620 );
1621 c.position = 2.0; solver.add_constraint(c);
1623 let inv_masses = vec![1.0, 1.0];
1624 let total = solver.solve(&inv_masses, 0.01);
1625 assert!(total > 0.0, "expected positive impulse, got {}", total);
1626 }
1627
1628 #[test]
1630 fn test_prismatic_penetration() {
1631 let mut pj = PrismaticJointLimit::new(
1632 0,
1633 1,
1634 [1.0, 0.0, 0.0],
1635 -0.5,
1636 0.5,
1637 StopType::Hard,
1638 JointComplianceParams::rigid(),
1639 );
1640 pj.translation = 0.8;
1641 assert!((pj.penetration() - 0.3).abs() < 1e-10);
1642 }
1643
1644 #[test]
1646 fn test_prismatic_constraint_row() {
1647 let mut pj = PrismaticJointLimit::new(
1648 0,
1649 1,
1650 [0.0, 1.0, 0.0],
1651 -1.0,
1652 1.0,
1653 StopType::Hard,
1654 JointComplianceParams::rigid(),
1655 );
1656 pj.translation = 1.5;
1657 assert_eq!(pj.constraint_row(), -1.0);
1658 pj.translation = -1.5;
1659 assert_eq!(pj.constraint_row(), 1.0);
1660 pj.translation = 0.0;
1661 assert_eq!(pj.constraint_row(), 0.0);
1662 }
1663
1664 #[test]
1666 fn test_revolute_is_active() {
1667 let mut rev = RevoluteJointLimit::new(
1668 0,
1669 1,
1670 [0.0, 1.0, 0.0],
1671 -PI / 2.0,
1672 PI / 2.0,
1673 StopType::Hard,
1674 JointComplianceParams::rigid(),
1675 0.0,
1676 );
1677 rev.angle = PI;
1678 assert!(rev.is_active());
1679 rev.angle = 0.0;
1680 assert!(!rev.is_active());
1681 }
1682
1683 #[test]
1685 fn test_revolute_solve_impulse_positive() {
1686 let mut rev = RevoluteJointLimit::new(
1687 0,
1688 1,
1689 [0.0, 0.0, 1.0],
1690 -PI / 4.0,
1691 PI / 4.0,
1692 StopType::Hard,
1693 JointComplianceParams::new(0.8, 0.0),
1694 0.0,
1695 );
1696 rev.angle = PI / 2.0; rev.ang_velocity = 1.0;
1698 let imp = rev.solve_impulse(1.0, 1.0, 0.01);
1699 assert!(imp >= 0.0);
1700 }
1701
1702 #[test]
1704 fn test_joint_stop_contact_impulse() {
1705 let contact = JointStopContact::new(
1706 0,
1707 [0.0, 1.0, 0.0],
1708 0.05,
1709 -1.0, 0.2,
1711 );
1712 let imp = contact.compute_impulse(2.0, 0.2, 0.01);
1713 assert!(imp >= 0.0);
1714 }
1715
1716 #[test]
1718 fn test_pipeline_active_count() {
1719 let mut pipe = JointLimitPipeline::new(4);
1720 let mut rev = RevoluteJointLimit::new(
1721 0,
1722 1,
1723 [0.0, 0.0, 1.0],
1724 -1.0,
1725 1.0,
1726 StopType::Hard,
1727 JointComplianceParams::rigid(),
1728 0.0,
1729 );
1730 rev.angle = 2.0; pipe.revolute.push(rev);
1732 assert_eq!(pipe.active_count(), 1);
1733 }
1734
1735 #[test]
1737 fn test_wrap_angle() {
1738 assert!((wrap_angle(3.0 * PI) - PI).abs() < 1e-10);
1739 assert!(
1740 (wrap_angle(-3.0 * PI) - (-PI)).abs() < 1e-10
1741 || (wrap_angle(-3.0 * PI) - PI).abs() < 1e-10
1742 );
1743 assert!(wrap_angle(0.5).abs() - 0.5 < 1e-10);
1744 }
1745
1746 #[test]
1748 fn test_angle_diff_wrap() {
1749 let d = angle_diff(PI * 0.9, -PI * 0.9);
1750 assert!(d.abs() < PI + 1e-10);
1752 }
1753
1754 #[test]
1756 fn test_normalize3_zero() {
1757 let v = normalize3([0.0, 0.0, 0.0]);
1758 assert_eq!(v, [0.0, 0.0, 0.0]);
1759 }
1760
1761 #[test]
1763 fn test_normalize3_unit() {
1764 let v = normalize3([3.0, 4.0, 0.0]);
1765 let n = norm3(v);
1766 assert!((n - 1.0).abs() < 1e-10);
1767 }
1768
1769 #[test]
1771 fn test_clampf() {
1772 assert_eq!(clampf(5.0, 0.0, 3.0), 3.0);
1773 assert_eq!(clampf(-1.0, 0.0, 3.0), 0.0);
1774 assert_eq!(clampf(1.5, 0.0, 3.0), 1.5);
1775 }
1776
1777 #[test]
1779 fn test_solver_clear_constraints() {
1780 let mut solver = JointLimitSolver::new(2);
1781 let lim = JointLimits::new(-1.0, 1.0);
1782 let c = JointLimitConstraint::new(
1783 0,
1784 1,
1785 JointType::Prismatic,
1786 lim,
1787 StopType::Hard,
1788 [1.0, 0.0, 0.0],
1789 );
1790 solver.add_constraint(c);
1791 assert_eq!(solver.constraints.len(), 1);
1792 solver.clear_constraints();
1793 assert!(solver.constraints.is_empty());
1794 }
1795}