1use oxiphysics_core::math::{Mat3, Real, Vec3};
20
21#[inline]
26fn v3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
27 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
28}
29
30#[inline]
31fn v3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
32 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
33}
34
35#[inline]
36fn v3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
37 [a[0] * s, a[1] * s, a[2] * s]
38}
39
40#[inline]
41fn v3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
42 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
43}
44
45#[inline]
46fn v3_norm(a: [f64; 3]) -> f64 {
47 v3_dot(a, a).sqrt()
48}
49
50type Mat3x3 = [[f64; 3]; 3];
52
53fn mat3_zero() -> Mat3x3 {
54 [[0.0; 3]; 3]
55}
56
57fn mat3_identity() -> Mat3x3 {
58 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
59}
60
61fn mat3_mul(a: Mat3x3, b: Mat3x3) -> Mat3x3 {
62 let mut c = mat3_zero();
63 for i in 0..3 {
64 for j in 0..3 {
65 for k in 0..3 {
66 c[i][j] += a[i][k] * b[k][j];
67 }
68 }
69 }
70 c
71}
72
73#[cfg(test)]
74fn mat3_transpose(m: Mat3x3) -> Mat3x3 {
75 [
76 [m[0][0], m[1][0], m[2][0]],
77 [m[0][1], m[1][1], m[2][1]],
78 [m[0][2], m[1][2], m[2][2]],
79 ]
80}
81
82fn mat3_det(m: Mat3x3) -> f64 {
83 m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
84 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
85 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
86}
87
88fn mat3_mul_vec(m: Mat3x3, v: [f64; 3]) -> [f64; 3] {
90 [v3_dot(m[0], v), v3_dot(m[1], v), v3_dot(m[2], v)]
91}
92
93fn outer_product_f64(a: [f64; 3], b: [f64; 3]) -> Mat3x3 {
95 [
96 [a[0] * b[0], a[0] * b[1], a[0] * b[2]],
97 [a[1] * b[0], a[1] * b[1], a[1] * b[2]],
98 [a[2] * b[0], a[2] * b[1], a[2] * b[2]],
99 ]
100}
101
102fn mat3_add_scaled(a: Mat3x3, b: Mat3x3, s: f64) -> Mat3x3 {
103 let mut c = a;
104 for i in 0..3 {
105 for j in 0..3 {
106 c[i][j] += b[i][j] * s;
107 }
108 }
109 c
110}
111
112fn polar_rotation_f64(m: Mat3x3) -> Mat3x3 {
117 let nm = Mat3::new(
119 m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2],
120 );
121 let svd = nm.svd(true, true);
122 let u = svd.u.unwrap_or_else(Mat3::identity);
123 let vt = svd.v_t.unwrap_or_else(Mat3::identity);
124 let mut r_na = u * vt;
125 if r_na.determinant() < 0.0 {
126 let mut u_fixed = u;
127 u_fixed.set_column(2, &(-u.column(2)));
128 r_na = u_fixed * vt;
129 }
130 [
132 [r_na[(0, 0)], r_na[(0, 1)], r_na[(0, 2)]],
133 [r_na[(1, 0)], r_na[(1, 1)], r_na[(1, 2)]],
134 [r_na[(2, 0)], r_na[(2, 1)], r_na[(2, 2)]],
135 ]
136}
137
138#[derive(Debug, Clone)]
155pub struct ShapeMatchingBody {
156 pub positions: Vec<[f64; 3]>,
158 pub velocities: Vec<[f64; 3]>,
160 pub masses: Vec<f64>,
162 rest_positions: Vec<[f64; 3]>,
166 pub rotation: Mat3x3,
168 pub stiffness: f64,
170 pub plasticity_threshold: f64,
173 pub plasticity_rate: f64,
176 pub accumulated_plastic_deformation: f64,
178}
179
180impl ShapeMatchingBody {
181 pub fn new(positions: Vec<[f64; 3]>, masses: Vec<f64>, stiffness: f64) -> Self {
183 assert_eq!(
184 positions.len(),
185 masses.len(),
186 "positions and masses must have the same length"
187 );
188 let n = positions.len();
189
190 let total_mass: f64 = masses.iter().sum();
192 assert!(total_mass > 0.0, "total mass must be positive");
193 let mut cm = [0.0_f64; 3];
194 for (p, &m) in positions.iter().zip(masses.iter()) {
195 cm[0] += p[0] * m;
196 cm[1] += p[1] * m;
197 cm[2] += p[2] * m;
198 }
199 cm = v3_scale(cm, 1.0 / total_mass);
200
201 let rest_positions: Vec<[f64; 3]> = positions.iter().map(|p| v3_sub(*p, cm)).collect();
203
204 Self {
205 positions,
206 velocities: vec![[0.0; 3]; n],
207 masses,
208 rest_positions,
209 rotation: mat3_identity(),
210 stiffness,
211 plasticity_threshold: f64::INFINITY,
212 plasticity_rate: 0.0,
213 accumulated_plastic_deformation: 0.0,
214 }
215 }
216
217 pub fn total_mass(&self) -> f64 {
219 self.masses.iter().sum()
220 }
221
222 pub fn centre_of_mass(&self) -> [f64; 3] {
224 let total = self.total_mass();
225 let mut cm = [0.0_f64; 3];
226 for (p, &m) in self.positions.iter().zip(self.masses.iter()) {
227 cm[0] += p[0] * m;
228 cm[1] += p[1] * m;
229 cm[2] += p[2] * m;
230 }
231 v3_scale(cm, 1.0 / total)
232 }
233
234 pub fn compute_optimal_rotation(&mut self) -> Mat3x3 {
238 let xcm = self.centre_of_mass();
239
240 let mut a = mat3_zero();
242 for ((pos, q), &mi) in self
243 .positions
244 .iter()
245 .zip(self.rest_positions.iter())
246 .zip(self.masses.iter())
247 {
248 let p = v3_sub(*pos, xcm);
249 let op = outer_product_f64(p, *q);
250 a = mat3_add_scaled(a, op, mi);
251 }
252
253 self.rotation = polar_rotation_f64(a);
254 self.rotation
255 }
256
257 pub fn goal_positions(&mut self) -> Vec<[f64; 3]> {
261 let r = self.compute_optimal_rotation();
262 let xcm = self.centre_of_mass();
263 self.rest_positions
264 .iter()
265 .map(|q| v3_add(mat3_mul_vec(r, *q), xcm))
266 .collect()
267 }
268
269 pub fn apply_shape_matching_force(&mut self, stiffness: f64, dt: f64) {
277 let goals = self.goal_positions();
278 let n = self.positions.len();
279 for (i, goal) in goals.iter().enumerate().take(n) {
280 let delta = v3_sub(*goal, self.positions[i]);
281 let disp = v3_scale(delta, stiffness);
282 self.positions[i] = v3_add(self.positions[i], disp);
283 if dt > 0.0 {
285 self.velocities[i] = v3_add(self.velocities[i], v3_scale(disp, 1.0 / dt));
286 }
287
288 let err = v3_norm(delta);
290 if err > self.plasticity_threshold {
291 let excess = err - self.plasticity_threshold;
292 let plastic_delta = v3_scale(
293 v3_scale(delta, 1.0 / err.max(1e-12)),
294 excess * self.plasticity_rate,
295 );
296 self.rest_positions[i] = v3_add(self.rest_positions[i], plastic_delta);
298 self.accumulated_plastic_deformation += v3_norm(plastic_delta);
299 }
300 }
301 }
302
303 pub fn reset_rest_shape(&mut self) {
305 let cm = self.centre_of_mass();
306 self.rest_positions = self.positions.iter().map(|p| v3_sub(*p, cm)).collect();
307 self.accumulated_plastic_deformation = 0.0;
308 }
309}
310
311#[derive(Debug, Clone)]
317pub struct ShapeMatching {
318 rest_positions: Vec<Vec3>,
320 masses: Vec<Real>,
322 pub stiffness: Real,
324}
325
326impl ShapeMatching {
327 pub fn new(positions: &[Vec3], masses: &[Real], stiffness: Real) -> Self {
334 assert_eq!(
335 positions.len(),
336 masses.len(),
337 "positions and masses must have the same length"
338 );
339
340 let total_mass: Real = masses.iter().sum();
342 let rest_cm: Vec3 = positions
343 .iter()
344 .zip(masses.iter())
345 .map(|(p, &m)| p * m)
346 .fold(Vec3::zeros(), |a, b| a + b)
347 / total_mass;
348
349 let rest_positions: Vec<Vec3> = positions.iter().map(|p| p - rest_cm).collect();
351
352 Self {
353 rest_positions,
354 masses: masses.to_vec(),
355 stiffness,
356 }
357 }
358
359 pub fn goal_positions(&self, positions: &[Vec3]) -> Vec<Vec3> {
369 let n = self.rest_positions.len();
370 assert_eq!(positions.len(), n, "positions length mismatch");
371
372 let total_mass: Real = self.masses.iter().sum();
374 let xcm: Vec3 = positions
375 .iter()
376 .zip(self.masses.iter())
377 .map(|(p, &m)| p * m)
378 .fold(Vec3::zeros(), |a, b| a + b)
379 / total_mass;
380
381 let mut a = Mat3::zeros();
385 for ((pos, q), &mi) in positions
386 .iter()
387 .zip(self.rest_positions.iter())
388 .zip(self.masses.iter())
389 {
390 let p = pos - xcm;
391 a += outer_product(&p, q) * mi;
393 }
394
395 let r = polar_rotation(&a);
397
398 self.rest_positions
400 .iter()
401 .zip(positions.iter())
402 .map(|(q, cur)| {
403 let goal_rigid = r * q + xcm;
404 goal_rigid * self.stiffness + cur * (1.0 - self.stiffness)
406 })
407 .collect()
408 }
409}
410
411fn outer_product(a: &Vec3, b: &Vec3) -> Mat3 {
417 Mat3::new(
418 a.x * b.x,
419 a.x * b.y,
420 a.x * b.z,
421 a.y * b.x,
422 a.y * b.y,
423 a.y * b.z,
424 a.z * b.x,
425 a.z * b.y,
426 a.z * b.z,
427 )
428}
429
430fn polar_rotation(m: &Mat3) -> Mat3 {
437 let svd = m.svd(true, true);
438 let u = svd.u.unwrap_or_else(Mat3::identity);
440 let vt = svd.v_t.unwrap_or_else(Mat3::identity);
441 let mut r = u * vt;
442 if r.determinant() < 0.0 {
444 let mut u_fixed = u;
446 u_fixed.set_column(2, &(-u.column(2)));
447 r = u_fixed * vt;
448 }
449 r
450}
451
452#[derive(Debug, Clone)]
461pub struct LinearDeformationBody {
462 pub positions: Vec<[f64; 3]>,
464 pub velocities: Vec<[f64; 3]>,
466 pub masses: Vec<f64>,
468 rest_positions: Vec<[f64; 3]>,
470 q_inv: Mat3x3,
472 pub stiffness: f64,
474 pub volume_preservation: f64,
476}
477
478impl LinearDeformationBody {
479 pub fn new(positions: Vec<[f64; 3]>, masses: Vec<f64>, stiffness: f64) -> Self {
481 assert_eq!(positions.len(), masses.len());
482 let n = positions.len();
483
484 let total_mass: f64 = masses.iter().sum();
485 assert!(total_mass > 0.0);
486
487 let mut cm = [0.0_f64; 3];
488 for (p, &m) in positions.iter().zip(masses.iter()) {
489 cm[0] += p[0] * m;
490 cm[1] += p[1] * m;
491 cm[2] += p[2] * m;
492 }
493 cm = v3_scale(cm, 1.0 / total_mass);
494
495 let rest_positions: Vec<[f64; 3]> = positions.iter().map(|p| v3_sub(*p, cm)).collect();
496
497 let mut q_mat = mat3_zero();
499 for (&q, &m) in rest_positions.iter().zip(masses.iter()) {
500 let op = outer_product_f64(q, q);
501 q_mat = mat3_add_scaled(q_mat, op, m);
502 }
503 let q_inv = mat3_inverse_safe(q_mat);
504
505 Self {
506 positions,
507 velocities: vec![[0.0; 3]; n],
508 masses,
509 rest_positions,
510 q_inv,
511 stiffness,
512 volume_preservation: 0.5,
513 }
514 }
515
516 pub fn centre_of_mass(&self) -> [f64; 3] {
518 let total: f64 = self.masses.iter().sum();
519 let mut cm = [0.0_f64; 3];
520 for (p, &m) in self.positions.iter().zip(self.masses.iter()) {
521 cm[0] += p[0] * m;
522 cm[1] += p[1] * m;
523 cm[2] += p[2] * m;
524 }
525 v3_scale(cm, 1.0 / total)
526 }
527
528 pub fn deformation_matrix(&self) -> Mat3x3 {
530 let xcm = self.centre_of_mass();
531 let mut a = mat3_zero();
532 for ((pos, q), &mi) in self
533 .positions
534 .iter()
535 .zip(self.rest_positions.iter())
536 .zip(self.masses.iter())
537 {
538 let p = v3_sub(*pos, xcm);
539 let op = outer_product_f64(p, *q);
540 a = mat3_add_scaled(a, op, mi);
541 }
542 mat3_mul(a, self.q_inv)
543 }
544
545 pub fn goal_positions(&self) -> Vec<[f64; 3]> {
549 let a_lin = self.deformation_matrix();
550 let r = polar_rotation_f64(a_lin);
551 let xcm = self.centre_of_mass();
552
553 let det = mat3_det(a_lin).abs();
555 let vp = self.volume_preservation;
556 let scale_factor = if det > 1e-12 {
557 det.powf(1.0 / 3.0)
558 } else {
559 1.0
560 };
561
562 self.rest_positions
563 .iter()
564 .map(|q| {
565 let goal_affine = mat3_mul_vec(a_lin, *q);
566 let goal_rigid = mat3_mul_vec(r, *q);
567 let goal_vp = v3_scale(mat3_mul_vec(a_lin, *q), 1.0 / scale_factor.max(1e-12));
569 let blended = [
570 goal_affine[0] * (1.0 - vp) + goal_vp[0] * vp,
571 goal_affine[1] * (1.0 - vp) + goal_vp[1] * vp,
572 goal_affine[2] * (1.0 - vp) + goal_vp[2] * vp,
573 ];
574 let final_goal = [
575 blended[0] * (1.0 - (1.0 - self.stiffness))
576 + goal_rigid[0] * (1.0 - self.stiffness),
577 blended[1] * (1.0 - (1.0 - self.stiffness))
578 + goal_rigid[1] * (1.0 - self.stiffness),
579 blended[2] * (1.0 - (1.0 - self.stiffness))
580 + goal_rigid[2] * (1.0 - self.stiffness),
581 ];
582 v3_add(final_goal, xcm)
583 })
584 .collect()
585 }
586
587 pub fn apply_correction(&mut self, stiffness: f64, dt: f64) {
589 let goals = self.goal_positions();
590 for (i, goal) in goals.iter().enumerate() {
591 let delta = v3_sub(*goal, self.positions[i]);
592 let disp = v3_scale(delta, stiffness);
593 self.positions[i] = v3_add(self.positions[i], disp);
594 if dt > 0.0 {
595 self.velocities[i] = v3_add(self.velocities[i], v3_scale(disp, 1.0 / dt));
596 }
597 }
598 }
599}
600
601fn mat3_inverse_safe(m: Mat3x3) -> Mat3x3 {
604 let det = mat3_det(m);
605 if det.abs() < 1e-12 {
606 return mat3_identity();
607 }
608 let inv_det = 1.0 / det;
609 [
610 [
611 (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
612 (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
613 (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
614 ],
615 [
616 (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
617 (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
618 (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
619 ],
620 [
621 (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
622 (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
623 (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
624 ],
625 ]
626}
627
628#[derive(Debug, Clone)]
639pub struct QuadraticDeformationBody {
640 pub positions: Vec<[f64; 3]>,
642 pub velocities: Vec<[f64; 3]>,
644 pub masses: Vec<f64>,
646 rest_features: Vec<[f64; 9]>,
648 pub stiffness: f64,
650}
651
652impl QuadraticDeformationBody {
653 fn feature(q: [f64; 3]) -> [f64; 9] {
655 [
656 q[0],
657 q[1],
658 q[2],
659 q[0] * q[0],
660 q[1] * q[1],
661 q[2] * q[2],
662 q[0] * q[1],
663 q[1] * q[2],
664 q[0] * q[2],
665 ]
666 }
667
668 pub fn new(positions: Vec<[f64; 3]>, masses: Vec<f64>, stiffness: f64) -> Self {
670 assert_eq!(positions.len(), masses.len());
671 let total: f64 = masses.iter().sum();
672 assert!(total > 0.0);
673 let mut cm = [0.0_f64; 3];
674 for (p, &m) in positions.iter().zip(masses.iter()) {
675 cm[0] += p[0] * m;
676 cm[1] += p[1] * m;
677 cm[2] += p[2] * m;
678 }
679 cm = v3_scale(cm, 1.0 / total);
680
681 let n = positions.len();
682 let rest_features: Vec<[f64; 9]> = positions
683 .iter()
684 .map(|p| Self::feature(v3_sub(*p, cm)))
685 .collect();
686
687 Self {
688 positions,
689 velocities: vec![[0.0; 3]; n],
690 masses,
691 rest_features,
692 stiffness,
693 }
694 }
695
696 pub fn centre_of_mass(&self) -> [f64; 3] {
698 let total: f64 = self.masses.iter().sum();
699 let mut cm = [0.0_f64; 3];
700 for (p, &m) in self.positions.iter().zip(self.masses.iter()) {
701 cm[0] += p[0] * m;
702 cm[1] += p[1] * m;
703 cm[2] += p[2] * m;
704 }
705 v3_scale(cm, 1.0 / total)
706 }
707
708 pub fn compute_a39(&self) -> [[f64; 9]; 3] {
710 let xcm = self.centre_of_mass();
711 let mut a = [[0.0_f64; 9]; 3];
712 for ((pos, feat), &mi) in self
713 .positions
714 .iter()
715 .zip(self.rest_features.iter())
716 .zip(self.masses.iter())
717 {
718 let p = v3_sub(*pos, xcm);
719 for row in 0..3 {
720 for col in 0..9 {
721 a[row][col] += mi * p[row] * feat[col];
722 }
723 }
724 }
725 a
726 }
727
728 pub fn goal_positions(&self) -> Vec<[f64; 3]> {
732 let a39 = self.compute_a39();
733 let xcm = self.centre_of_mass();
734 self.rest_features
735 .iter()
736 .zip(self.positions.iter())
737 .map(|(feat, cur)| {
738 let mut goal = xcm;
739 for row in 0..3 {
740 let v: f64 = a39[row].iter().zip(feat.iter()).map(|(a, f)| a * f).sum();
741 goal[row] += v;
742 }
743 [
745 goal[0] * self.stiffness + cur[0] * (1.0 - self.stiffness),
746 goal[1] * self.stiffness + cur[1] * (1.0 - self.stiffness),
747 goal[2] * self.stiffness + cur[2] * (1.0 - self.stiffness),
748 ]
749 })
750 .collect()
751 }
752
753 pub fn apply_correction(&mut self, stiffness: f64, dt: f64) {
755 let goals = self.goal_positions();
756 for (i, goal) in goals.iter().enumerate() {
757 let delta = v3_sub(*goal, self.positions[i]);
758 let disp = v3_scale(delta, stiffness);
759 self.positions[i] = v3_add(self.positions[i], disp);
760 if dt > 0.0 {
761 self.velocities[i] = v3_add(self.velocities[i], v3_scale(disp, 1.0 / dt));
762 }
763 }
764 }
765}
766
767#[derive(Debug, Clone)]
775pub struct ShapeMatchingCluster {
776 pub particle_indices: Vec<usize>,
778 cluster_rest: Vec<[f64; 3]>,
780 pub stiffness: f64,
782}
783
784impl ShapeMatchingCluster {
785 pub fn new(
790 positions: &[[f64; 3]],
791 masses: &[f64],
792 indices: Vec<usize>,
793 stiffness: f64,
794 ) -> Self {
795 let total: f64 = indices.iter().map(|&i| masses[i]).sum();
796 assert!(total > 0.0);
797 let mut cm = [0.0_f64; 3];
798 for &i in &indices {
799 cm[0] += positions[i][0] * masses[i];
800 cm[1] += positions[i][1] * masses[i];
801 cm[2] += positions[i][2] * masses[i];
802 }
803 cm = v3_scale(cm, 1.0 / total);
804 let cluster_rest: Vec<[f64; 3]> =
805 indices.iter().map(|&i| v3_sub(positions[i], cm)).collect();
806 Self {
807 particle_indices: indices,
808 cluster_rest,
809 stiffness,
810 }
811 }
812
813 pub fn compute_corrections(
816 &self,
817 positions: &[[f64; 3]],
818 masses: &[f64],
819 ) -> Vec<(usize, [f64; 3])> {
820 let total: f64 = self.particle_indices.iter().map(|&i| masses[i]).sum();
822 if total < 1e-30 {
823 return vec![];
824 }
825 let mut xcm = [0.0_f64; 3];
826 for &i in &self.particle_indices {
827 xcm[0] += positions[i][0] * masses[i];
828 xcm[1] += positions[i][1] * masses[i];
829 xcm[2] += positions[i][2] * masses[i];
830 }
831 xcm = v3_scale(xcm, 1.0 / total);
832
833 let mut a = mat3_zero();
835 for (&i, q) in self.particle_indices.iter().zip(self.cluster_rest.iter()) {
836 let p = v3_sub(positions[i], xcm);
837 let op = outer_product_f64(p, *q);
838 a = mat3_add_scaled(a, op, masses[i]);
839 }
840 let r = polar_rotation_f64(a);
841
842 self.particle_indices
844 .iter()
845 .zip(self.cluster_rest.iter())
846 .map(|(&i, q)| {
847 let goal = v3_add(mat3_mul_vec(r, *q), xcm);
848 let delta = v3_sub(goal, positions[i]);
849 (i, v3_scale(delta, self.stiffness))
850 })
851 .collect()
852 }
853}
854
855pub struct ClusterShapeMatchingSystem {
860 pub positions: Vec<[f64; 3]>,
862 pub velocities: Vec<[f64; 3]>,
864 pub masses: Vec<f64>,
866 pub clusters: Vec<ShapeMatchingCluster>,
868}
869
870impl ClusterShapeMatchingSystem {
871 pub fn new(positions: Vec<[f64; 3]>, masses: Vec<f64>) -> Self {
873 let n = positions.len();
874 Self {
875 positions,
876 velocities: vec![[0.0; 3]; n],
877 masses,
878 clusters: Vec::new(),
879 }
880 }
881
882 pub fn add_cluster(&mut self, indices: Vec<usize>, stiffness: f64) {
884 let c = ShapeMatchingCluster::new(&self.positions, &self.masses, indices, stiffness);
885 self.clusters.push(c);
886 }
887
888 pub fn step(&mut self, dt: f64) {
892 let n = self.positions.len();
893 let mut total_correction = vec![[0.0_f64; 3]; n];
894 let mut correction_count = vec![0usize; n];
895
896 for cluster in &self.clusters {
897 let corrections = cluster.compute_corrections(&self.positions, &self.masses);
898 for (i, delta) in corrections {
899 total_correction[i] = v3_add(total_correction[i], delta);
900 correction_count[i] += 1;
901 }
902 }
903
904 for i in 0..n {
905 if correction_count[i] > 0 {
906 let avg = v3_scale(total_correction[i], 1.0 / correction_count[i] as f64);
907 self.positions[i] = v3_add(self.positions[i], avg);
908 if dt > 0.0 {
909 self.velocities[i] = v3_add(self.velocities[i], v3_scale(avg, 1.0 / dt));
910 }
911 }
912 }
913 }
914}
915
916#[derive(Debug, Clone)]
925pub struct PlasticityImprovedBody {
926 pub positions: Vec<[f64; 3]>,
928 pub velocities: Vec<[f64; 3]>,
930 pub masses: Vec<f64>,
932 plastic_rest: Vec<[f64; 3]>,
934 pub plastic_strain: Vec<f64>,
936 pub yield_stress: f64,
938 pub hardening_modulus: f64,
940 pub plasticity_rate: f64,
942 pub stiffness: f64,
944 pub rotation: Mat3x3,
946}
947
948impl PlasticityImprovedBody {
949 pub fn new(positions: Vec<[f64; 3]>, masses: Vec<f64>, stiffness: f64) -> Self {
951 assert_eq!(positions.len(), masses.len());
952 let n = positions.len();
953 let total: f64 = masses.iter().sum();
954 assert!(total > 0.0);
955 let mut cm = [0.0_f64; 3];
956 for (p, &m) in positions.iter().zip(masses.iter()) {
957 cm[0] += p[0] * m;
958 cm[1] += p[1] * m;
959 cm[2] += p[2] * m;
960 }
961 cm = v3_scale(cm, 1.0 / total);
962 let plastic_rest: Vec<[f64; 3]> = positions.iter().map(|p| v3_sub(*p, cm)).collect();
963 Self {
964 positions,
965 velocities: vec![[0.0; 3]; n],
966 masses,
967 plastic_rest,
968 plastic_strain: vec![0.0; n],
969 yield_stress: 0.1,
970 hardening_modulus: 0.0,
971 plasticity_rate: 0.3,
972 stiffness,
973 rotation: mat3_identity(),
974 }
975 }
976
977 pub fn centre_of_mass(&self) -> [f64; 3] {
979 let total: f64 = self.masses.iter().sum();
980 let mut cm = [0.0_f64; 3];
981 for (p, &m) in self.positions.iter().zip(self.masses.iter()) {
982 cm[0] += p[0] * m;
983 cm[1] += p[1] * m;
984 cm[2] += p[2] * m;
985 }
986 v3_scale(cm, 1.0 / total)
987 }
988
989 pub fn step(&mut self, dt: f64) {
993 let xcm = self.centre_of_mass();
994
995 let mut a = mat3_zero();
996 for ((pos, q), &mi) in self
997 .positions
998 .iter()
999 .zip(self.plastic_rest.iter())
1000 .zip(self.masses.iter())
1001 {
1002 let p = v3_sub(*pos, xcm);
1003 let op = outer_product_f64(p, *q);
1004 a = mat3_add_scaled(a, op, mi);
1005 }
1006 self.rotation = polar_rotation_f64(a);
1007 let r = self.rotation;
1008
1009 let n = self.positions.len();
1010 for i in 0..n {
1011 let goal = v3_add(mat3_mul_vec(r, self.plastic_rest[i]), xcm);
1012 let delta = v3_sub(goal, self.positions[i]);
1013 let disp = v3_scale(delta, self.stiffness);
1014 self.positions[i] = v3_add(self.positions[i], disp);
1015 if dt > 0.0 {
1016 self.velocities[i] = v3_add(self.velocities[i], v3_scale(disp, 1.0 / dt));
1017 }
1018
1019 let err = v3_norm(delta);
1021 let effective_yield =
1022 (self.yield_stress + self.hardening_modulus * self.plastic_strain[i]).max(1e-12);
1023 if err > effective_yield {
1024 let excess = err - effective_yield;
1025 let dir = v3_scale(delta, 1.0 / err.max(1e-12));
1026 let plastic_delta = v3_scale(dir, excess * self.plasticity_rate);
1027 self.plastic_rest[i] = v3_add(self.plastic_rest[i], plastic_delta);
1028 self.plastic_strain[i] += v3_norm(plastic_delta);
1029 }
1030 }
1031 }
1032
1033 pub fn total_plastic_strain(&self) -> f64 {
1035 self.plastic_strain.iter().sum()
1036 }
1037
1038 pub fn reset_plasticity(&mut self) {
1040 let cm = self.centre_of_mass();
1041 self.plastic_rest = self.positions.iter().map(|p| v3_sub(*p, cm)).collect();
1042 for s in &mut self.plastic_strain {
1043 *s = 0.0;
1044 }
1045 }
1046}
1047
1048#[cfg(test)]
1053mod tests {
1054 use super::*;
1055
1056 const EPS: f64 = 1e-6;
1057
1058 fn rotate(r: Mat3x3, v: [f64; 3]) -> [f64; 3] {
1060 mat3_mul_vec(r, v)
1061 }
1062
1063 fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
1065 v3_norm(v3_sub(a, b))
1066 }
1067
1068 #[test]
1073 fn test_rigid_body_no_deformation() {
1074 let rest = vec![
1076 [1.0, 0.0, 0.0],
1077 [-1.0, 0.0, 0.0],
1078 [0.0, 1.0, 0.0],
1079 [0.0, -1.0, 0.0],
1080 ];
1081 let masses = vec![1.0_f64; 4];
1082 let mut body = ShapeMatchingBody::new(rest.clone(), masses, 1.0);
1083
1084 let rz90: Mat3x3 = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
1086 body.positions = rest.iter().map(|p| rotate(rz90, *p)).collect();
1087
1088 let goals = body.goal_positions();
1089 for (i, (goal, cur)) in goals.iter().zip(body.positions.iter()).enumerate() {
1090 assert!(
1091 dist3(*goal, *cur) < EPS,
1092 "Particle {i}: goal {:?} should equal current {:?}",
1093 goal,
1094 cur
1095 );
1096 }
1097 }
1098
1099 #[test]
1104 fn test_pure_translation() {
1105 let rest = vec![
1106 [0.0, 0.0, 0.0],
1107 [1.0, 0.0, 0.0],
1108 [0.0, 1.0, 0.0],
1109 [0.0, 0.0, 1.0],
1110 ];
1111 let masses = vec![1.0_f64; 4];
1112 let mut body = ShapeMatchingBody::new(rest.clone(), masses, 1.0);
1113
1114 let offset = [5.0, -3.0, 2.0];
1116 body.positions = rest.iter().map(|p| v3_add(*p, offset)).collect();
1117
1118 let goals = body.goal_positions();
1119 for (i, (goal, cur)) in goals.iter().zip(body.positions.iter()).enumerate() {
1120 assert!(
1121 dist3(*goal, *cur) < EPS,
1122 "Particle {i}: goal {:?} should equal current {:?} after pure translation",
1123 goal,
1124 cur
1125 );
1126 }
1127 }
1128
1129 #[test]
1132 fn test_rotation_invariance() {
1133 let rest = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.5, 0.0]];
1134 let masses = vec![1.0_f64; 3];
1135
1136 let deformed: Vec<[f64; 3]> = vec![[2.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.5, 0.0]];
1138
1139 let mut body_a = ShapeMatchingBody::new(rest.clone(), masses.clone(), 1.0);
1141 body_a.positions = deformed.clone();
1142 let goals_a = body_a.goal_positions();
1143 let errors_a: Vec<f64> = goals_a
1144 .iter()
1145 .zip(deformed.iter())
1146 .map(|(g, c)| dist3(*g, *c))
1147 .collect();
1148
1149 let c45 = std::f64::consts::FRAC_PI_4.cos();
1151 let s45 = std::f64::consts::FRAC_PI_4.sin();
1152 let rz45: Mat3x3 = [[c45, -s45, 0.0], [s45, c45, 0.0], [0.0, 0.0, 1.0]];
1153
1154 let rotated_rest: Vec<[f64; 3]> = rest.iter().map(|p| rotate(rz45, *p)).collect();
1155 let rotated_deformed: Vec<[f64; 3]> = deformed.iter().map(|p| rotate(rz45, *p)).collect();
1156
1157 let mut body_b = ShapeMatchingBody::new(rotated_rest, masses.clone(), 1.0);
1158 body_b.positions = rotated_deformed.clone();
1159 let goals_b = body_b.goal_positions();
1160 let errors_b: Vec<f64> = goals_b
1161 .iter()
1162 .zip(rotated_deformed.iter())
1163 .map(|(g, c)| dist3(*g, *c))
1164 .collect();
1165
1166 for i in 0..3 {
1167 assert!(
1168 (errors_a[i] - errors_b[i]).abs() < 1e-5,
1169 "Rotation invariance violated at particle {i}: {:.6} vs {:.6}",
1170 errors_a[i],
1171 errors_b[i]
1172 );
1173 }
1174 }
1175
1176 #[test]
1178 fn test_apply_force_stiffness_one() {
1179 let rest = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1180 let masses = vec![1.0_f64; 3];
1181 let mut body = ShapeMatchingBody::new(rest.clone(), masses, 1.0);
1182
1183 body.positions[2] = [0.5, 1.5, 0.3];
1185
1186 let goals_before = body.goal_positions();
1187 body.positions = goals_before.clone(); let mut body2 = ShapeMatchingBody::new(rest, vec![1.0_f64; 3], 1.0);
1190 body2.positions[2] = [0.5, 1.5, 0.3];
1191 body2.apply_shape_matching_force(1.0, 0.016);
1192
1193 let goals_after = body2.goal_positions();
1195 for (i, (goal, pos)) in goals_after.iter().zip(body2.positions.iter()).enumerate() {
1196 assert!(
1197 dist3(*goal, *pos) < 1e-4,
1198 "Particle {i}: position {:?} should be close to goal {:?}",
1199 pos,
1200 goal
1201 );
1202 }
1203 }
1204
1205 #[test]
1207 fn test_plasticity_accumulates() {
1208 let rest = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1209 let masses = vec![1.0_f64; 3];
1210 let mut body = ShapeMatchingBody::new(rest, masses, 0.5);
1211 body.plasticity_threshold = 0.01;
1212 body.plasticity_rate = 0.5;
1213
1214 body.positions[0] = [3.0, 0.0, 0.0];
1216
1217 let plastic_before = body.accumulated_plastic_deformation;
1218 body.apply_shape_matching_force(0.5, 0.016);
1219 let plastic_after = body.accumulated_plastic_deformation;
1220
1221 assert!(
1222 plastic_after > plastic_before,
1223 "Plastic deformation should have accumulated: before={plastic_before}, after={plastic_after}"
1224 );
1225 }
1226
1227 #[test]
1229 fn test_reset_rest_shape() {
1230 let rest = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1231 let masses = vec![1.0_f64; 3];
1232 let mut body = ShapeMatchingBody::new(rest, masses, 0.5);
1233 body.accumulated_plastic_deformation = 99.0;
1234
1235 body.reset_rest_shape();
1236
1237 assert_eq!(
1238 body.accumulated_plastic_deformation, 0.0,
1239 "Accumulated plasticity should be zero after reset"
1240 );
1241
1242 let goals = body.goal_positions();
1244 for (i, (goal, pos)) in goals.iter().zip(body.positions.iter()).enumerate() {
1245 assert!(
1246 dist3(*goal, *pos) < EPS,
1247 "Particle {i}: after reset goals {:?} should equal positions {:?}",
1248 goal,
1249 pos
1250 );
1251 }
1252 }
1253
1254 #[test]
1256 fn test_mat3_mul_identity() {
1257 let i = mat3_identity();
1258 let r = mat3_mul(i, i);
1259 for (row, r_row) in r.iter().enumerate() {
1260 for (col, &v) in r_row.iter().enumerate() {
1261 let expected = if row == col { 1.0 } else { 0.0 };
1262 assert!(
1263 (v - expected).abs() < 1e-12,
1264 "mat3_mul(I,I)[{row}][{col}] = {} (expected {expected})",
1265 v
1266 );
1267 }
1268 }
1269 }
1270
1271 #[test]
1273 fn test_mat3_transpose() {
1274 let m: Mat3x3 = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1275 let mt = mat3_transpose(m);
1276 for i in 0..3 {
1277 for j in 0..3 {
1278 assert!(
1279 (mt[i][j] - m[j][i]).abs() < 1e-12,
1280 "Transpose mismatch at [{i}][{j}]"
1281 );
1282 }
1283 }
1284 }
1285
1286 #[test]
1288 fn test_mat3_inverse_safe_identity() {
1289 let i = mat3_identity();
1290 let inv = mat3_inverse_safe(i);
1291 for (r, inv_row) in inv.iter().enumerate() {
1292 for (c, &v) in inv_row.iter().enumerate() {
1293 let expected = if r == c { 1.0 } else { 0.0 };
1294 assert!(
1295 (v - expected).abs() < 1e-10,
1296 "Inverse of identity should be identity at [{r}][{c}]"
1297 );
1298 }
1299 }
1300 }
1301
1302 #[test]
1304 fn test_mat3_inverse_safe_singular() {
1305 let singular: Mat3x3 = [[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [1.0, 2.0, 3.0]];
1306 let inv = mat3_inverse_safe(singular);
1307 let _ = inv;
1309 }
1310
1311 #[test]
1313 fn test_linear_deformation_pure_translation() {
1314 let rest = vec![
1315 [0.0, 0.0, 0.0],
1316 [1.0, 0.0, 0.0],
1317 [0.0, 1.0, 0.0],
1318 [0.0, 0.0, 1.0],
1319 ];
1320 let masses = vec![1.0_f64; 4];
1321 let mut body = LinearDeformationBody::new(rest.clone(), masses, 1.0);
1322 let offset = [3.0, 2.0, 1.0];
1323 body.positions = rest.iter().map(|p| v3_add(*p, offset)).collect();
1324 let goals = body.goal_positions();
1325 for (i, (goal, cur)) in goals.iter().zip(body.positions.iter()).enumerate() {
1326 let d = v3_norm(v3_sub(*goal, *cur));
1327 assert!(
1328 d < 0.2,
1329 "LinearDeformation pure translation particle {i}: d={d}"
1330 );
1331 }
1332 }
1333
1334 #[test]
1336 fn test_linear_deformation_apply_correction() {
1337 let rest = vec![
1338 [1.0, 0.0, 0.0],
1339 [-1.0, 0.0, 0.0],
1340 [0.0, 1.0, 0.0],
1341 [0.0, -1.0, 0.0],
1342 ];
1343 let masses = vec![1.0_f64; 4];
1344 let mut body = LinearDeformationBody::new(rest, masses, 1.0);
1345 body.positions[0] = [2.0, 0.5, 0.3];
1347 body.apply_correction(0.5, 0.016);
1348 assert!(
1350 (body.positions[0][0] - 2.0).abs() > 0.01 || (body.positions[0][1] - 0.5).abs() > 0.01,
1351 "LinearDeformation: correction should move particles"
1352 );
1353 }
1354
1355 #[test]
1357 fn test_quadratic_feature_vector_length() {
1358 let q = [1.0, 2.0, 3.0];
1359 let feat = QuadraticDeformationBody::feature(q);
1360 assert_eq!(feat.len(), 9);
1361 assert!((feat[0] - 1.0).abs() < 1e-12);
1362 assert!((feat[3] - 1.0).abs() < 1e-12); assert!((feat[4] - 4.0).abs() < 1e-12); assert!((feat[6] - 2.0).abs() < 1e-12); }
1366
1367 #[test]
1369 fn test_quadratic_deformation_goals_exist() {
1370 let rest = vec![
1371 [1.0, 0.0, 0.0],
1372 [-1.0, 0.0, 0.0],
1373 [0.0, 1.0, 0.0],
1374 [0.0, -1.0, 0.0],
1375 [0.0, 0.0, 1.0],
1376 ];
1377 let masses = vec![1.0_f64; 5];
1378 let body = QuadraticDeformationBody::new(rest.clone(), masses, 1.0);
1379 let goals = body.goal_positions();
1380 assert_eq!(goals.len(), rest.len());
1381 for g in &goals {
1383 for &v in g {
1384 assert!(v.is_finite(), "goal component should be finite");
1385 }
1386 }
1387 }
1388
1389 #[test]
1391 fn test_cluster_single_cluster() {
1392 let positions = vec![
1393 [1.0, 0.0, 0.0],
1394 [-1.0, 0.0, 0.0],
1395 [0.0, 1.0, 0.0],
1396 [0.0, -1.0, 0.0],
1397 ];
1398 let masses = vec![1.0_f64; 4];
1399 let mut sys = ClusterShapeMatchingSystem::new(positions.clone(), masses.clone());
1400 sys.add_cluster(vec![0, 1, 2, 3], 1.0);
1401
1402 sys.positions[0] = [3.0, 0.0, 0.0];
1404 let before = sys.positions[0];
1405 sys.step(0.016);
1406 let after = sys.positions[0];
1407 let d_before = v3_norm(v3_sub(before, [1.0, 0.0, 0.0]));
1409 let d_after = v3_norm(v3_sub(after, [1.0, 0.0, 0.0]));
1410 assert!(
1411 d_after < d_before + 0.01,
1412 "Cluster SM should move particle closer to goal"
1413 );
1414 }
1415
1416 #[test]
1418 fn test_cluster_two_overlapping_clusters() {
1419 let positions = vec![
1420 [0.0, 0.0, 0.0],
1421 [1.0, 0.0, 0.0],
1422 [2.0, 0.0, 0.0],
1423 [3.0, 0.0, 0.0],
1424 ];
1425 let masses = vec![1.0_f64; 4];
1426 let mut sys = ClusterShapeMatchingSystem::new(positions, masses);
1427 sys.add_cluster(vec![0, 1, 2], 0.5);
1428 sys.add_cluster(vec![1, 2, 3], 0.5);
1429 sys.step(0.016);
1431 assert_eq!(sys.positions.len(), 4);
1432 }
1433
1434 #[test]
1436 fn test_improved_plasticity_accumulates() {
1437 let rest = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1438 let masses = vec![1.0_f64; 3];
1439 let mut body = PlasticityImprovedBody::new(rest, masses, 0.5);
1440 body.yield_stress = 0.01;
1441 body.plasticity_rate = 0.5;
1442 body.positions[0] = [5.0, 0.0, 0.0];
1444 let before = body.total_plastic_strain();
1445 body.step(0.016);
1446 let after = body.total_plastic_strain();
1447 assert!(
1448 after > before,
1449 "Plastic strain should accumulate: before={before}, after={after}"
1450 );
1451 }
1452
1453 #[test]
1455 fn test_improved_plasticity_reset() {
1456 let rest = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1457 let masses = vec![1.0_f64; 3];
1458 let mut body = PlasticityImprovedBody::new(rest, masses, 0.5);
1459 body.plastic_strain[0] = 5.0;
1460 body.reset_plasticity();
1461 assert_eq!(body.total_plastic_strain(), 0.0);
1462 }
1463
1464 #[test]
1466 fn test_mat3_det_rotation() {
1467 let c = std::f64::consts::FRAC_PI_4.cos();
1468 let s = std::f64::consts::FRAC_PI_4.sin();
1469 let rz: Mat3x3 = [[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]];
1470 let det = mat3_det(rz);
1471 assert!(
1472 (det - 1.0).abs() < 1e-12,
1473 "Rotation matrix det should be 1, got {det}"
1474 );
1475 }
1476
1477 #[test]
1479 fn test_centre_of_mass_mass_weighted() {
1480 let rest = vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1481 let masses = vec![1.0_f64, 3.0]; let body = ShapeMatchingBody::new(rest, masses, 1.0);
1483 let cm = body.centre_of_mass();
1484 assert!(
1485 (cm[0] - 1.5).abs() < 1e-12,
1486 "CM_x should be 1.5, got {}",
1487 cm[0]
1488 );
1489 assert!(cm[1].abs() < 1e-12);
1490 assert!(cm[2].abs() < 1e-12);
1491 }
1492}