1use crate::{cross3, dot3, length3, normalize3};
11
12#[inline]
17fn sub3(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]
22fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
23 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
24}
25
26#[inline]
27fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
28 [v[0] * s, v[1] * s, v[2] * s]
29}
30
31pub fn triangle_area(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> f64 {
39 let e1 = sub3(v1, v0);
40 let e2 = sub3(v2, v0);
41 length3(cross3(e1, e2)) * 0.5
42}
43
44pub fn dihedral_angle(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], v3: [f64; 3]) -> f64 {
54 let e = sub3(v2, v1);
55 let n1 = cross3(sub3(v0, v1), e);
56 let n2 = cross3(e, sub3(v3, v1));
57 let len1 = length3(n1);
58 let len2 = length3(n2);
59 if len1 < 1e-15 || len2 < 1e-15 {
60 return 0.0;
61 }
62 let cos_a = dot3(n1, n2) / (len1 * len2);
63 cos_a.clamp(-1.0, 1.0).acos()
64}
65
66#[derive(Debug, Clone)]
72pub struct ClothVertex {
73 pub position: [f64; 3],
75 pub velocity: [f64; 3],
77 pub mass: f64,
79 pub pinned: bool,
81}
82
83impl ClothVertex {
84 pub fn new(position: [f64; 3], mass: f64, pinned: bool) -> Self {
86 Self {
87 position,
88 velocity: [0.0; 3],
89 mass,
90 pinned,
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
101pub struct ClothEdge {
102 pub v0: usize,
104 pub v1: usize,
106 pub rest_length: f64,
108 pub stiffness: f64,
110 pub damping: f64,
112}
113
114impl ClothEdge {
115 pub fn new(v0: usize, v1: usize, rest_length: f64, stiffness: f64, damping: f64) -> Self {
117 Self {
118 v0,
119 v1,
120 rest_length,
121 stiffness,
122 damping,
123 }
124 }
125
126 pub fn spring_force(&self, verts: &[ClothVertex]) -> [f64; 3] {
130 let p0 = verts[self.v0].position;
131 let p1 = verts[self.v1].position;
132 let vel0 = verts[self.v0].velocity;
133 let vel1 = verts[self.v1].velocity;
134
135 let delta = sub3(p1, p0);
136 let dist = length3(delta);
137 if dist < 1e-15 {
138 return [0.0; 3];
139 }
140 let dir = scale3(delta, 1.0 / dist);
141
142 let rel_vel = dot3(sub3(vel1, vel0), dir);
144
145 let spring_f = self.stiffness * (dist - self.rest_length);
146 let damp_f = self.damping * rel_vel;
147 scale3(dir, spring_f + damp_f)
148 }
149}
150
151#[derive(Debug, Clone)]
159pub struct BendingConstraint {
160 pub v0: usize,
162 pub v1: usize,
164 pub v2: usize,
166 pub v3: usize,
168 pub rest_angle: f64,
170 pub stiffness: f64,
172}
173
174impl BendingConstraint {
175 pub fn new(
177 v0: usize,
178 v1: usize,
179 v2: usize,
180 v3: usize,
181 rest_angle: f64,
182 stiffness: f64,
183 ) -> Self {
184 Self {
185 v0,
186 v1,
187 v2,
188 v3,
189 rest_angle,
190 stiffness,
191 }
192 }
193
194 pub fn compute_force(&self, verts: &[ClothVertex]) -> Vec<[f64; 3]> {
198 let p0 = verts[self.v0].position;
199 let p1 = verts[self.v1].position;
200 let p2 = verts[self.v2].position;
201 let p3 = verts[self.v3].position;
202
203 let current_angle = dihedral_angle(p0, p1, p2, p3);
204 let angle_diff = current_angle - self.rest_angle;
205
206 if angle_diff.abs() < 1e-12 {
207 return vec![[0.0; 3]; 4];
208 }
209
210 let mag = self.stiffness * angle_diff;
212
213 let e = sub3(p2, p1);
215 let e_len = length3(e);
216 if e_len < 1e-15 {
217 return vec![[0.0; 3]; 4];
218 }
219
220 let n1 = cross3(sub3(p0, p1), e);
221 let n2 = cross3(e, sub3(p3, p1));
222 let len1 = length3(n1);
223 let len2 = length3(n2);
224 if len1 < 1e-15 || len2 < 1e-15 {
225 return vec![[0.0; 3]; 4];
226 }
227
228 let g0 = scale3(normalize3(n1), -mag / len1);
230 let g3 = scale3(normalize3(n2), -mag / len2);
231
232 let g1 = scale3(add3(g0, g3), -0.5);
234 let g2 = scale3(add3(g0, g3), -0.5);
235
236 vec![g0, g1, g2, g3]
237 }
238}
239
240#[derive(Debug, Clone)]
246pub struct ClothMesh {
247 pub vertices: Vec<ClothVertex>,
249 pub edges: Vec<ClothEdge>,
251}
252
253impl ClothMesh {
254 pub fn new() -> Self {
256 Self {
257 vertices: Vec::new(),
258 edges: Vec::new(),
259 }
260 }
261
262 pub fn build_grid(&mut self, rows: usize, cols: usize, spacing: f64) {
268 self.vertices.clear();
269 self.edges.clear();
270
271 for r in 0..rows {
273 for c in 0..cols {
274 let pos = [c as f64 * spacing, 0.0, r as f64 * spacing];
275 let pinned = r == 0;
276 self.vertices.push(ClothVertex::new(pos, 1.0, pinned));
277 }
278 }
279
280 let idx = |r: usize, c: usize| r * cols + c;
281
282 for r in 0..rows {
284 for c in 0..cols {
285 if c + 1 < cols {
286 self.edges.push(ClothEdge::new(
287 idx(r, c),
288 idx(r, c + 1),
289 spacing,
290 1000.0,
291 0.5,
292 ));
293 }
294 if r + 1 < rows {
295 self.edges.push(ClothEdge::new(
296 idx(r, c),
297 idx(r + 1, c),
298 spacing,
299 1000.0,
300 0.5,
301 ));
302 }
303 }
304 }
305
306 let diag = spacing * std::f64::consts::SQRT_2;
308 for r in 0..rows {
309 for c in 0..cols {
310 if r + 1 < rows && c + 1 < cols {
311 self.edges.push(ClothEdge::new(
312 idx(r, c),
313 idx(r + 1, c + 1),
314 diag,
315 500.0,
316 0.2,
317 ));
318 self.edges.push(ClothEdge::new(
319 idx(r + 1, c),
320 idx(r, c + 1),
321 diag,
322 500.0,
323 0.2,
324 ));
325 }
326 }
327 }
328 }
329
330 pub fn step(&mut self, dt: f64, gravity: [f64; 3]) {
334 let n = self.vertices.len();
335 let mut forces = vec![[0.0f64; 3]; n];
336
337 for (i, v) in self.vertices.iter().enumerate() {
339 if !v.pinned {
340 forces[i] = add3(forces[i], scale3(gravity, v.mass));
341 }
342 }
343
344 for edge in &self.edges {
346 let f = edge.spring_force(&self.vertices);
347 if !self.vertices[edge.v0].pinned {
348 forces[edge.v0] = add3(forces[edge.v0], f);
349 }
350 if !self.vertices[edge.v1].pinned {
351 forces[edge.v1] = sub3(forces[edge.v1], f);
352 }
353 }
354
355 for (i, v) in self.vertices.iter_mut().enumerate() {
357 if v.pinned {
358 continue;
359 }
360 let inv_m = 1.0 / v.mass;
361 let accel = scale3(forces[i], inv_m);
362 v.velocity = add3(v.velocity, scale3(accel, dt));
363 v.position = add3(v.position, scale3(v.velocity, dt));
364 }
365 }
366}
367
368impl Default for ClothMesh {
369 fn default() -> Self {
370 Self::new()
371 }
372}
373
374#[derive(Debug, Clone)]
380pub enum ClothCollider {
381 Sphere {
383 center: [f64; 3],
385 radius: f64,
387 },
388 Plane {
391 normal: [f64; 3],
393 d: f64,
395 },
396}
397
398impl ClothCollider {
399 pub fn penetration(&self, p: [f64; 3]) -> Option<(f64, [f64; 3])> {
404 match self {
405 ClothCollider::Sphere { center, radius } => {
406 let delta = sub3(p, *center);
407 let dist = length3(delta);
408 if dist < *radius {
409 let depth = radius - dist;
410 let dir = if dist < 1e-15 {
411 [0.0, 1.0, 0.0]
412 } else {
413 scale3(delta, 1.0 / dist)
414 };
415 Some((depth, dir))
416 } else {
417 None
418 }
419 }
420 ClothCollider::Plane { normal, d } => {
421 let signed = dot3(*normal, p) - d;
422 if signed < 0.0 {
423 Some((-signed, *normal))
424 } else {
425 None
426 }
427 }
428 }
429 }
430}
431
432#[derive(Debug, Clone)]
441pub struct GpuClothSolver {
442 pub mesh: ClothMesh,
444 pub colliders: Vec<ClothCollider>,
446 pub xpbd_iterations: usize,
448}
449
450impl GpuClothSolver {
451 pub fn new(mesh: ClothMesh) -> Self {
453 Self {
454 mesh,
455 colliders: Vec::new(),
456 xpbd_iterations: 8,
457 }
458 }
459
460 pub fn add_collider(&mut self, collider: ClothCollider) {
462 self.colliders.push(collider);
463 }
464
465 pub fn solve(&mut self, dt: f64) {
471 let gravity = [0.0, -9.81, 0.0];
472
473 let mut pred_pos: Vec<[f64; 3]> = self
475 .mesh
476 .vertices
477 .iter()
478 .map(|v| {
479 if v.pinned {
480 v.position
481 } else {
482 add3(
483 v.position,
484 scale3(add3(v.velocity, scale3(gravity, dt)), dt),
485 )
486 }
487 })
488 .collect();
489
490 for _ in 0..self.xpbd_iterations {
492 for edge in &self.mesh.edges {
493 let i = edge.v0;
494 let j = edge.v1;
495 let pi = pred_pos[i];
496 let pj = pred_pos[j];
497 let delta = sub3(pj, pi);
498 let dist = length3(delta);
499 if dist < 1e-15 {
500 continue;
501 }
502 let constraint = dist - edge.rest_length;
503 let dir = scale3(delta, 1.0 / dist);
504
505 let wi = if self.mesh.vertices[i].pinned {
506 0.0
507 } else {
508 1.0 / self.mesh.vertices[i].mass
509 };
510 let wj = if self.mesh.vertices[j].pinned {
511 0.0
512 } else {
513 1.0 / self.mesh.vertices[j].mass
514 };
515 let w_total = wi + wj;
516 if w_total < 1e-15 {
517 continue;
518 }
519
520 let alpha = 1.0 / (edge.stiffness * dt * dt);
521 let lambda = -constraint / (w_total + alpha);
522
523 if !self.mesh.vertices[i].pinned {
524 pred_pos[i] = sub3(pred_pos[i], scale3(dir, wi * lambda));
525 }
526 if !self.mesh.vertices[j].pinned {
527 pred_pos[j] = add3(pred_pos[j], scale3(dir, wj * lambda));
528 }
529 }
530 }
531
532 for collider in &self.colliders {
534 for (i, pred) in pred_pos.iter_mut().enumerate() {
535 if self.mesh.vertices[i].pinned {
536 continue;
537 }
538 if let Some((depth, dir)) = collider.penetration(*pred) {
539 *pred = add3(*pred, scale3(dir, depth));
540 }
541 }
542 }
543
544 for (i, &pred) in pred_pos.iter().enumerate() {
546 if !self.mesh.vertices[i].pinned {
547 let old_pos = self.mesh.vertices[i].position;
548 self.mesh.vertices[i].velocity = scale3(sub3(pred, old_pos), 1.0 / dt);
549 self.mesh.vertices[i].position = pred;
550 }
551 }
552 }
553}
554
555#[cfg(test)]
560mod tests {
561 use super::*;
562 use std::f64::consts::PI;
563
564 #[test]
567 fn test_triangle_area_unit() {
568 let v0 = [0.0, 0.0, 0.0];
569 let v1 = [1.0, 0.0, 0.0];
570 let v2 = [0.0, 1.0, 0.0];
571 let area = triangle_area(v0, v1, v2);
572 assert!((area - 0.5).abs() < 1e-12, "area={area}");
573 }
574
575 #[test]
576 fn test_triangle_area_degenerate() {
577 let v0 = [0.0, 0.0, 0.0];
579 let v1 = [1.0, 0.0, 0.0];
580 let v2 = [2.0, 0.0, 0.0];
581 assert!(triangle_area(v0, v1, v2) < 1e-12);
582 }
583
584 #[test]
585 fn test_triangle_area_equilateral() {
586 let v0 = [0.0, 0.0, 0.0];
588 let v1 = [2.0, 0.0, 0.0];
589 let v2 = [1.0, f64::sqrt(3.0), 0.0];
590 let expected = f64::sqrt(3.0);
591 assert!((triangle_area(v0, v1, v2) - expected).abs() < 1e-10);
592 }
593
594 #[test]
595 fn test_triangle_area_3d() {
596 let v0 = [0.0, 0.0, 0.0];
598 let v1 = [1.0, 0.0, 0.0];
599 let v2 = [0.0, 0.0, 1.0];
600 let area = triangle_area(v0, v1, v2);
601 assert!((area - 0.5).abs() < 1e-12);
602 }
603
604 #[test]
605 fn test_triangle_area_large() {
606 let v0 = [0.0, 0.0, 0.0];
607 let v1 = [10.0, 0.0, 0.0];
608 let v2 = [0.0, 10.0, 0.0];
609 let area = triangle_area(v0, v1, v2);
610 assert!((area - 50.0).abs() < 1e-10);
611 }
612
613 #[test]
616 fn test_dihedral_angle_flat() {
617 let v0 = [0.0, 0.0, -1.0];
619 let v1 = [-1.0, 0.0, 0.0];
620 let v2 = [1.0, 0.0, 0.0];
621 let v3 = [0.0, 0.0, 1.0];
622 let angle = dihedral_angle(v0, v1, v2, v3);
623 assert!(angle < 1e-10, "angle={angle}");
624 }
625
626 #[test]
627 fn test_dihedral_angle_ninety_degrees() {
628 let v0 = [0.0, 1.0, 0.0];
630 let v1 = [0.0, 0.0, 0.0];
631 let v2 = [1.0, 0.0, 0.0];
632 let v3 = [0.5, 0.0, 1.0];
633 let angle = dihedral_angle(v0, v1, v2, v3);
634 assert!((angle - PI / 2.0).abs() < 0.3, "angle={angle}");
636 }
637
638 #[test]
639 fn test_dihedral_angle_degenerate_edge() {
640 let v0 = [0.0, 1.0, 0.0];
642 let v1 = [0.0, 0.0, 0.0];
643 let v2 = [0.0, 0.0, 0.0];
644 let v3 = [0.0, -1.0, 0.0];
645 let angle = dihedral_angle(v0, v1, v2, v3);
646 assert!(angle.is_finite());
647 }
648
649 #[test]
650 fn test_dihedral_angle_range() {
651 let v0 = [1.0, 1.0, 0.0];
652 let v1 = [0.0, 0.0, 0.0];
653 let v2 = [1.0, 0.0, 0.0];
654 let v3 = [1.0, -1.0, 0.0];
655 let angle = dihedral_angle(v0, v1, v2, v3);
656 assert!((0.0..=PI + 1e-10).contains(&angle), "angle={angle}");
657 }
658
659 #[test]
662 fn test_cloth_vertex_new() {
663 let v = ClothVertex::new([1.0, 2.0, 3.0], 2.5, false);
664 assert_eq!(v.position, [1.0, 2.0, 3.0]);
665 assert_eq!(v.velocity, [0.0; 3]);
666 assert!((v.mass - 2.5).abs() < 1e-12);
667 assert!(!v.pinned);
668 }
669
670 #[test]
671 fn test_cloth_vertex_pinned() {
672 let v = ClothVertex::new([0.0; 3], 1.0, true);
673 assert!(v.pinned);
674 }
675
676 #[test]
679 fn test_spring_force_at_rest() {
680 let verts = vec![
681 ClothVertex::new([0.0, 0.0, 0.0], 1.0, false),
682 ClothVertex::new([1.0, 0.0, 0.0], 1.0, false),
683 ];
684 let edge = ClothEdge::new(0, 1, 1.0, 1000.0, 0.5);
685 let f = edge.spring_force(&verts);
686 assert!(length3(f) < 1e-10, "f={f:?}");
688 }
689
690 #[test]
691 fn test_spring_force_stretched() {
692 let verts = vec![
693 ClothVertex::new([0.0, 0.0, 0.0], 1.0, false),
694 ClothVertex::new([2.0, 0.0, 0.0], 1.0, false),
695 ];
696 let edge = ClothEdge::new(0, 1, 1.0, 1000.0, 0.0);
698 let f = edge.spring_force(&verts);
699 assert!(f[0] > 0.0, "force should be positive (toward v1)");
700 assert!(f[1].abs() < 1e-12);
701 assert!(f[2].abs() < 1e-12);
702 }
703
704 #[test]
705 fn test_spring_force_compressed() {
706 let verts = vec![
707 ClothVertex::new([0.0, 0.0, 0.0], 1.0, false),
708 ClothVertex::new([0.5, 0.0, 0.0], 1.0, false),
709 ];
710 let edge = ClothEdge::new(0, 1, 1.0, 1000.0, 0.0);
712 let f = edge.spring_force(&verts);
713 assert!(f[0] < 0.0, "force should be negative (push back)");
714 }
715
716 #[test]
717 fn test_spring_force_with_damping() {
718 let mut v0 = ClothVertex::new([0.0, 0.0, 0.0], 1.0, false);
719 let mut v1 = ClothVertex::new([2.0, 0.0, 0.0], 1.0, false);
720 v0.velocity = [-1.0, 0.0, 0.0];
721 v1.velocity = [1.0, 0.0, 0.0];
722 let verts = vec![v0, v1];
723 let edge = ClothEdge::new(0, 1, 1.0, 0.0, 10.0); let f = edge.spring_force(&verts);
725 assert!((f[0] - 20.0).abs() < 1e-10, "f={f:?}");
727 }
728
729 #[test]
732 fn test_build_grid_vertex_count() {
733 let mut mesh = ClothMesh::new();
734 mesh.build_grid(4, 5, 0.1);
735 assert_eq!(mesh.vertices.len(), 20);
736 }
737
738 #[test]
739 fn test_build_grid_top_row_pinned() {
740 let mut mesh = ClothMesh::new();
741 mesh.build_grid(4, 5, 0.1);
742 for c in 0..5 {
743 assert!(
744 mesh.vertices[c].pinned,
745 "vertex {c} in row 0 should be pinned"
746 );
747 }
748 for r in 1..4 {
749 for c in 0..5 {
750 assert!(!mesh.vertices[r * 5 + c].pinned);
751 }
752 }
753 }
754
755 #[test]
756 fn test_build_grid_spacing() {
757 let mut mesh = ClothMesh::new();
758 mesh.build_grid(2, 2, 0.5);
759 let d = sub3(mesh.vertices[1].position, mesh.vertices[0].position);
761 assert!((length3(d) - 0.5).abs() < 1e-12);
762 }
763
764 #[test]
765 fn test_mesh_step_gravity() {
766 let mut mesh = ClothMesh::new();
767 mesh.build_grid(2, 1, 1.0);
768 let y_before = mesh.vertices[1].position[1];
770 mesh.step(0.01, [0.0, -9.81, 0.0]);
771 let y_after = mesh.vertices[1].position[1];
772 assert!(y_after < y_before, "unpinned vertex should fall");
773 }
774
775 #[test]
776 fn test_mesh_step_pinned_unchanged() {
777 let mut mesh = ClothMesh::new();
778 mesh.build_grid(2, 1, 1.0);
779 let pos_before = mesh.vertices[0].position;
780 mesh.step(0.01, [0.0, -9.81, 0.0]);
781 assert_eq!(mesh.vertices[0].position, pos_before);
782 }
783
784 #[test]
785 fn test_mesh_default() {
786 let mesh = ClothMesh::default();
787 assert!(mesh.vertices.is_empty());
788 assert!(mesh.edges.is_empty());
789 }
790
791 #[test]
794 fn test_bending_at_rest_angle() {
795 let p0 = [0.0, 1.0, 0.0];
797 let p1 = [0.0, 0.0, 0.0];
798 let p2 = [1.0, 0.0, 0.0];
799 let p3 = [1.0, 0.0, -1.0];
800
801 let rest = dihedral_angle(p0, p1, p2, p3);
802
803 let verts = vec![
804 ClothVertex::new(p0, 1.0, false),
805 ClothVertex::new(p1, 1.0, false),
806 ClothVertex::new(p2, 1.0, false),
807 ClothVertex::new(p3, 1.0, false),
808 ];
809
810 let bc = BendingConstraint::new(0, 1, 2, 3, rest, 100.0);
811 let forces = bc.compute_force(&verts);
812 for f in &forces {
813 assert!(length3(*f) < 1e-8, "force should be ~zero at rest");
814 }
815 }
816
817 #[test]
818 fn test_bending_constraint_forces_len() {
819 let verts: Vec<ClothVertex> = (0..4)
820 .map(|i| ClothVertex::new([i as f64, 0.0, 0.0], 1.0, false))
821 .collect();
822 let bc = BendingConstraint::new(0, 1, 2, 3, 0.0, 10.0);
823 let forces = bc.compute_force(&verts);
824 assert_eq!(forces.len(), 4);
825 }
826
827 #[test]
830 fn test_sphere_collider_inside() {
831 let col = ClothCollider::Sphere {
832 center: [0.0; 3],
833 radius: 1.0,
834 };
835 let (depth, _dir) = col.penetration([0.5, 0.0, 0.0]).unwrap();
836 assert!((depth - 0.5).abs() < 1e-10);
837 }
838
839 #[test]
840 fn test_sphere_collider_outside() {
841 let col = ClothCollider::Sphere {
842 center: [0.0; 3],
843 radius: 1.0,
844 };
845 assert!(col.penetration([2.0, 0.0, 0.0]).is_none());
846 }
847
848 #[test]
849 fn test_sphere_collider_direction() {
850 let col = ClothCollider::Sphere {
851 center: [0.0; 3],
852 radius: 2.0,
853 };
854 let (_depth, dir) = col.penetration([1.0, 0.0, 0.0]).unwrap();
855 assert!((dir[0] - 1.0).abs() < 1e-10);
856 }
857
858 #[test]
859 fn test_sphere_collider_center() {
860 let col = ClothCollider::Sphere {
862 center: [0.0; 3],
863 radius: 1.0,
864 };
865 let result = col.penetration([0.0; 3]);
866 assert!(result.is_some());
867 }
868
869 #[test]
870 fn test_plane_collider_below() {
871 let col = ClothCollider::Plane {
873 normal: [0.0, 1.0, 0.0],
874 d: 0.0,
875 };
876 let (depth, dir) = col.penetration([0.0, -0.5, 0.0]).unwrap();
877 assert!((depth - 0.5).abs() < 1e-10);
878 assert!((dir[1] - 1.0).abs() < 1e-10);
879 }
880
881 #[test]
882 fn test_plane_collider_above() {
883 let col = ClothCollider::Plane {
884 normal: [0.0, 1.0, 0.0],
885 d: 0.0,
886 };
887 assert!(col.penetration([0.0, 1.0, 0.0]).is_none());
888 }
889
890 #[test]
893 fn test_solver_new() {
894 let mesh = ClothMesh::new();
895 let solver = GpuClothSolver::new(mesh);
896 assert_eq!(solver.xpbd_iterations, 8);
897 assert!(solver.colliders.is_empty());
898 }
899
900 #[test]
901 fn test_solver_add_collider() {
902 let mesh = ClothMesh::new();
903 let mut solver = GpuClothSolver::new(mesh);
904 solver.add_collider(ClothCollider::Plane {
905 normal: [0.0, 1.0, 0.0],
906 d: -1.0,
907 });
908 assert_eq!(solver.colliders.len(), 1);
909 }
910
911 #[test]
912 fn test_solver_solve_no_penetration() {
913 let mut mesh = ClothMesh::new();
914 mesh.build_grid(2, 2, 0.5);
915 let mut solver = GpuClothSolver::new(mesh);
916 solver.add_collider(ClothCollider::Plane {
918 normal: [0.0, 1.0, 0.0],
919 d: -10.0,
920 });
921 solver.solve(0.01);
922 for v in &solver.mesh.vertices {
924 if !v.pinned {
925 assert!(v.position[1] > -10.0);
926 }
927 }
928 }
929
930 #[test]
931 fn test_solver_sphere_prevents_penetration() {
932 let mut mesh = ClothMesh::new();
933 mesh.build_grid(2, 2, 0.1);
934 for v in mesh.vertices.iter_mut() {
936 if !v.pinned {
937 v.position = [0.0, 0.0, 0.0];
938 }
939 }
940 let mut solver = GpuClothSolver::new(mesh);
941 solver.add_collider(ClothCollider::Sphere {
942 center: [0.0; 3],
943 radius: 5.0,
944 });
945 solver.solve(0.01);
946 for v in &solver.mesh.vertices {
948 if !v.pinned {
949 let dist = length3(v.position);
950 assert!(dist >= 5.0 - 1e-6, "dist={dist}");
951 }
952 }
953 }
954
955 #[test]
956 fn test_solver_pinned_stays() {
957 let mut mesh = ClothMesh::new();
958 mesh.build_grid(3, 3, 0.5);
959 let pin_pos: Vec<_> = mesh
960 .vertices
961 .iter()
962 .filter(|v| v.pinned)
963 .map(|v| v.position)
964 .collect();
965
966 let mut solver = GpuClothSolver::new(mesh);
967 for _ in 0..10 {
968 solver.solve(0.01);
969 }
970
971 let pin_pos_after: Vec<_> = solver
972 .mesh
973 .vertices
974 .iter()
975 .filter(|v| v.pinned)
976 .map(|v| v.position)
977 .collect();
978
979 assert_eq!(pin_pos, pin_pos_after);
980 }
981
982 #[test]
983 fn test_cloth_grid_has_edges() {
984 let mut mesh = ClothMesh::new();
985 mesh.build_grid(3, 3, 0.5);
986 assert!(!mesh.edges.is_empty());
987 }
988
989 #[test]
990 fn test_dihedral_symmetric() {
991 let p0 = [0.0, 1.0, 0.0];
993 let p1 = [-1.0, 0.0, 0.0];
994 let p2 = [1.0, 0.0, 0.0];
995 let p3 = [0.0, -1.0, 0.5];
996 let a1 = dihedral_angle(p0, p1, p2, p3);
997 let a2 = dihedral_angle(p3, p1, p2, p0);
998 assert!((a1 - a2).abs() < 1e-10, "a1={a1} a2={a2}");
999 }
1000
1001 #[test]
1002 fn test_spring_force_zero_length() {
1003 let verts = vec![
1005 ClothVertex::new([0.0, 0.0, 0.0], 1.0, false),
1006 ClothVertex::new([0.0, 0.0, 0.0], 1.0, false),
1007 ];
1008 let edge = ClothEdge::new(0, 1, 1.0, 1000.0, 0.5);
1009 let f = edge.spring_force(&verts);
1010 assert!(length3(f) < 1e-12);
1011 }
1012
1013 #[test]
1014 fn test_multiple_steps_energy_decreases() {
1015 let mut mesh = ClothMesh::new();
1017 mesh.build_grid(2, 1, 2.0); let mut solver = GpuClothSolver::new(mesh);
1019 solver.solve(0.001);
1020 for v in &solver.mesh.vertices {
1022 for x in v.position {
1023 assert!(x.is_finite());
1024 }
1025 }
1026 }
1027
1028 #[test]
1029 fn test_cloth_vertex_clone() {
1030 let v = ClothVertex::new([1.0, 2.0, 3.0], 1.0, false);
1031 let v2 = v.clone();
1032 assert_eq!(v.position, v2.position);
1033 }
1034
1035 #[test]
1036 fn test_cloth_edge_clone() {
1037 let e = ClothEdge::new(0, 1, 1.0, 100.0, 0.1);
1038 let e2 = e.clone();
1039 assert_eq!(e.v0, e2.v0);
1040 assert_eq!(e.rest_length, e2.rest_length);
1041 }
1042
1043 #[test]
1044 fn test_collider_clone() {
1045 let c = ClothCollider::Sphere {
1046 center: [1.0, 2.0, 3.0],
1047 radius: 0.5,
1048 };
1049 let c2 = c.clone();
1050 if let ClothCollider::Sphere { radius, .. } = c2 {
1051 assert!((radius - 0.5).abs() < 1e-12);
1052 } else {
1053 panic!("wrong variant");
1054 }
1055 }
1056}