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