Skip to main content

oxiphysics_collision/
soft_body_collision.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Soft body and cloth collision detection and response.
5//!
6//! Covers:
7//! - Vertex-triangle distance queries for deformable surface meshes
8//! - Cloth self-collision using spatial hashing
9//! - Collision response impulses for deformable bodies
10//! - Continuous collision detection (CCD) for cloth edges
11//! - Penalty force springs for interpenetration
12//! - Bounding volume hierarchy (BVH) for dynamic meshes
13//! - Contact manifold generation for soft bodies
14//! - Friction model for cloth-rigid contact
15//! - Tearing and cutting along collision boundaries (conceptual)
16//! - GPU-friendly flat collision data structures
17
18use std::collections::HashMap;
19
20// ─────────────────────────────────────────────────────────────────────────────
21// Internal vector helpers — no nalgebra
22// ─────────────────────────────────────────────────────────────────────────────
23
24#[inline]
25fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
26    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
27}
28
29#[inline]
30fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
31    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
32}
33
34#[inline]
35fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
36    [a[0] * s, a[1] * s, a[2] * s]
37}
38
39#[inline]
40fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
41    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
42}
43
44#[inline]
45fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
46    [
47        a[1] * b[2] - a[2] * b[1],
48        a[2] * b[0] - a[0] * b[2],
49        a[0] * b[1] - a[1] * b[0],
50    ]
51}
52
53#[inline]
54fn len3(a: [f64; 3]) -> f64 {
55    dot3(a, a).sqrt()
56}
57
58#[inline]
59fn normalize3(a: [f64; 3]) -> [f64; 3] {
60    let l = len3(a);
61    if l < 1e-14 {
62        [0.0, 0.0, 1.0]
63    } else {
64        scale3(a, 1.0 / l)
65    }
66}
67
68// ─────────────────────────────────────────────────────────────────────────────
69// Vertex-triangle collision
70// ─────────────────────────────────────────────────────────────────────────────
71
72/// Closest point on a triangle (a, b, c) to point p, plus barycentric coords.
73///
74/// Returns (squared_distance, barycentric \[u, v, w\]).
75pub fn vertex_triangle_closest(
76    p: [f64; 3],
77    a: [f64; 3],
78    b: [f64; 3],
79    c: [f64; 3],
80) -> (f64, [f64; 3]) {
81    let ab = sub3(b, a);
82    let ac = sub3(c, a);
83    let ap = sub3(p, a);
84
85    let d1 = dot3(ab, ap);
86    let d2 = dot3(ac, ap);
87    if d1 <= 0.0 && d2 <= 0.0 {
88        let diff = sub3(p, a);
89        return (dot3(diff, diff), [1.0, 0.0, 0.0]);
90    }
91
92    let bp = sub3(p, b);
93    let d3 = dot3(ab, bp);
94    let d4 = dot3(ac, bp);
95    if d3 >= 0.0 && d4 <= d3 {
96        let diff = sub3(p, b);
97        return (dot3(diff, diff), [0.0, 1.0, 0.0]);
98    }
99
100    let vc = d1 * d4 - d3 * d2;
101    if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
102        let v = d1 / (d1 - d3);
103        let closest = add3(a, scale3(ab, v));
104        let diff = sub3(p, closest);
105        return (dot3(diff, diff), [1.0 - v, v, 0.0]);
106    }
107
108    let cp = sub3(p, c);
109    let d5 = dot3(ab, cp);
110    let d6 = dot3(ac, cp);
111    if d6 >= 0.0 && d5 <= d6 {
112        let diff = sub3(p, c);
113        return (dot3(diff, diff), [0.0, 0.0, 1.0]);
114    }
115
116    let vb = d5 * d2 - d1 * d6;
117    if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
118        let w = d2 / (d2 - d6);
119        let closest = add3(a, scale3(ac, w));
120        let diff = sub3(p, closest);
121        return (dot3(diff, diff), [1.0 - w, 0.0, w]);
122    }
123
124    let va = d3 * d6 - d5 * d4;
125    if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
126        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
127        let bc = sub3(c, b);
128        let closest = add3(b, scale3(bc, w));
129        let diff = sub3(p, closest);
130        return (dot3(diff, diff), [0.0, 1.0 - w, w]);
131    }
132
133    let denom = 1.0 / (va + vb + vc);
134    let v = vb * denom;
135    let w = vc * denom;
136    let closest = add3(add3(a, scale3(ab, v)), scale3(ac, w));
137    let diff = sub3(p, closest);
138    (dot3(diff, diff), [1.0 - v - w, v, w])
139}
140
141/// A vertex-triangle contact for a soft body surface mesh.
142#[derive(Debug, Clone)]
143pub struct VertexTriangleContact {
144    /// Vertex index (in the deformable mesh).
145    pub vertex_index: usize,
146    /// Triangle index (in the rigid or static mesh).
147    pub triangle_index: usize,
148    /// Contact normal pointing from triangle to vertex.
149    pub normal: [f64; 3],
150    /// Penetration depth (positive = interpenetrating).
151    pub depth: f64,
152    /// Barycentric coordinates of the closest point on the triangle.
153    pub bary: [f64; 3],
154}
155
156/// Detect contacts between deformable vertices and a triangle mesh.
157///
158/// Returns all vertex-triangle pairs where the vertex is within `thickness`
159/// of the triangle surface.
160pub fn detect_vertex_triangle_contacts(
161    vertices: &[[f64; 3]],
162    triangles: &[[usize; 3]],
163    tri_positions: &[[f64; 3]],
164    thickness: f64,
165) -> Vec<VertexTriangleContact> {
166    let threshold_sq = thickness * thickness;
167    let mut contacts = Vec::new();
168
169    for (vi, &vp) in vertices.iter().enumerate() {
170        for (ti, &tri) in triangles.iter().enumerate() {
171            let a = tri_positions[tri[0]];
172            let b = tri_positions[tri[1]];
173            let c = tri_positions[tri[2]];
174
175            let (dist_sq, bary) = vertex_triangle_closest(vp, a, b, c);
176            if dist_sq < threshold_sq {
177                let closest = add3(
178                    add3(scale3(a, bary[0]), scale3(b, bary[1])),
179                    scale3(c, bary[2]),
180                );
181                let to_vert = sub3(vp, closest);
182                let dist = dist_sq.sqrt();
183                let normal = if dist > 1e-14 {
184                    scale3(to_vert, 1.0 / dist)
185                } else {
186                    let ab = sub3(b, a);
187                    let ac = sub3(c, a);
188                    normalize3(cross3(ab, ac))
189                };
190                contacts.push(VertexTriangleContact {
191                    vertex_index: vi,
192                    triangle_index: ti,
193                    normal,
194                    depth: thickness - dist,
195                    bary,
196                });
197            }
198        }
199    }
200    contacts
201}
202
203// ─────────────────────────────────────────────────────────────────────────────
204// Cloth self-collision — spatial hashing
205// ─────────────────────────────────────────────────────────────────────────────
206
207/// Spatial hash for cloth vertex pairs, used for self-collision detection.
208pub struct ClothSpatialHash {
209    /// Cell size.
210    pub cell_size: f64,
211    /// Map from grid cell key to vertex indices.
212    cells: HashMap<(i64, i64, i64), Vec<usize>>,
213}
214
215impl ClothSpatialHash {
216    /// Construct an empty spatial hash with given cell size.
217    pub fn new(cell_size: f64) -> Self {
218        ClothSpatialHash {
219            cell_size,
220            cells: HashMap::new(),
221        }
222    }
223
224    fn cell_of(&self, p: [f64; 3]) -> (i64, i64, i64) {
225        (
226            (p[0] / self.cell_size).floor() as i64,
227            (p[1] / self.cell_size).floor() as i64,
228            (p[2] / self.cell_size).floor() as i64,
229        )
230    }
231
232    /// Insert all vertices into the spatial hash.
233    pub fn build(&mut self, positions: &[[f64; 3]]) {
234        self.cells.clear();
235        for (i, &p) in positions.iter().enumerate() {
236            let key = self.cell_of(p);
237            self.cells.entry(key).or_default().push(i);
238        }
239    }
240
241    /// Query all candidate self-collision vertex pairs within cell radius 1.
242    ///
243    /// Returns pairs (i, j) with i < j whose cells are adjacent.
244    pub fn candidate_pairs(&self) -> Vec<(usize, usize)> {
245        let mut pairs = Vec::new();
246        let all_keys: Vec<(i64, i64, i64)> = self.cells.keys().copied().collect();
247        for &(cx, cy, cz) in &all_keys {
248            for dx in -1_i64..=1 {
249                for dy in -1_i64..=1 {
250                    for dz in -1_i64..=1 {
251                        let neighbor = (cx + dx, cy + dy, cz + dz);
252                        if let Some(other) = self.cells.get(&neighbor)
253                            && let Some(this) = self.cells.get(&(cx, cy, cz))
254                        {
255                            for &i in this {
256                                for &j in other {
257                                    if i < j {
258                                        pairs.push((i, j));
259                                    }
260                                }
261                            }
262                        }
263                    }
264                }
265            }
266        }
267        pairs.sort_unstable();
268        pairs.dedup();
269        pairs
270    }
271
272    /// Detect self-collision contacts for all candidate pairs.
273    ///
274    /// Returns pairs with distances below `radius`.
275    pub fn detect_self_collisions(
276        &self,
277        positions: &[[f64; 3]],
278        radius: f64,
279    ) -> Vec<(usize, usize, [f64; 3], f64)> {
280        let candidates = self.candidate_pairs();
281        let mut contacts = Vec::new();
282        for (i, j) in candidates {
283            if i >= positions.len() || j >= positions.len() {
284                continue;
285            }
286            let diff = sub3(positions[i], positions[j]);
287            let dist = len3(diff);
288            if dist < radius && dist > 1e-14 {
289                let normal = scale3(diff, 1.0 / dist);
290                contacts.push((i, j, normal, radius - dist));
291            }
292        }
293        contacts
294    }
295}
296
297// ─────────────────────────────────────────────────────────────────────────────
298// Collision response impulse for deformable bodies
299// ─────────────────────────────────────────────────────────────────────────────
300
301/// Velocity impulse to apply to a vertex after collision resolution.
302#[derive(Debug, Clone)]
303pub struct DeformableImpulse {
304    /// Target vertex index.
305    pub vertex_index: usize,
306    /// Velocity delta to apply.
307    pub delta_v: [f64; 3],
308    /// Position correction vector.
309    pub correction: [f64; 3],
310}
311
312/// Compute collision response impulses for vertex-triangle contacts.
313///
314/// Uses a simple inelastic impulse model.  `restitution` ∈ \[0, 1\].
315pub fn compute_contact_impulses(
316    contacts: &[VertexTriangleContact],
317    velocities: &[[f64; 3]],
318    masses: &[f64],
319    restitution: f64,
320    friction_coeff: f64,
321) -> Vec<DeformableImpulse> {
322    let mut impulses = Vec::new();
323    for c in contacts {
324        let vi = c.vertex_index;
325        if vi >= velocities.len() || vi >= masses.len() {
326            continue;
327        }
328        let v = velocities[vi];
329        let m = masses[vi];
330        if m <= 0.0 {
331            continue;
332        }
333
334        let v_normal = dot3(v, c.normal);
335        if v_normal >= 0.0 {
336            // Already separating
337            continue;
338        }
339
340        // Normal impulse
341        let j_n = -(1.0 + restitution) * v_normal * m;
342        let impulse_n = scale3(c.normal, j_n / m);
343
344        // Tangential friction impulse
345        let v_tang = sub3(v, scale3(c.normal, v_normal));
346        let v_tang_len = len3(v_tang);
347        let impulse_t = if v_tang_len > 1e-14 {
348            let friction_mag = friction_coeff * j_n.abs() / m;
349            scale3(v_tang, -friction_mag.min(v_tang_len) / v_tang_len)
350        } else {
351            [0.0; 3]
352        };
353
354        let delta_v = add3(impulse_n, impulse_t);
355        let correction = scale3(c.normal, c.depth.max(0.0));
356
357        impulses.push(DeformableImpulse {
358            vertex_index: vi,
359            delta_v,
360            correction,
361        });
362    }
363    impulses
364}
365
366// ─────────────────────────────────────────────────────────────────────────────
367// CCD for cloth edges
368// ─────────────────────────────────────────────────────────────────────────────
369
370/// Edge-edge CCD result for cloth collision.
371#[derive(Debug, Clone)]
372pub struct EdgeCcdResult {
373    /// Edge index in mesh A.
374    pub edge_a: usize,
375    /// Edge index in mesh B (or same mesh for self-collision).
376    pub edge_b: usize,
377    /// Time of impact in \[0, 1\].
378    pub toi: f64,
379    /// Contact normal at impact.
380    pub normal: [f64; 3],
381}
382
383/// Continuous collision detection for a pair of linearly-moving edges.
384///
385/// Returns the earliest time of impact t ∈ \[0, 1\], or `None`.
386pub fn edge_edge_ccd(
387    p0a: [f64; 3],
388    p1a: [f64; 3],
389    q0a: [f64; 3],
390    q1a: [f64; 3],
391    p0b: [f64; 3],
392    p1b: [f64; 3],
393    q0b: [f64; 3],
394    q1b: [f64; 3],
395    radius: f64,
396) -> Option<f64> {
397    // Bisection-based CCD: sample 64 sub-intervals for the time axis
398    let steps = 64;
399    let dt = 1.0 / steps as f64;
400    for step in 0..steps {
401        let t = step as f64 * dt;
402        let lerp =
403            |a: [f64; 3], b: [f64; 3]| -> [f64; 3] { add3(scale3(a, 1.0 - t), scale3(b, t)) };
404        let pa = lerp(p0a, p1a);
405        let qa = lerp(q0a, q1a);
406        let pb = lerp(p0b, p1b);
407        let qb = lerp(q0b, q1b);
408        let da = sub3(qa, pa);
409        let db = sub3(qb, pb);
410        let r = sub3(pa, pb);
411        let a = dot3(da, da);
412        let e = dot3(db, db);
413        let f = dot3(db, r);
414        let eps = 1e-10;
415        let (s, t2) = if a <= eps && e <= eps {
416            (0.0, 0.0)
417        } else if a <= eps {
418            (0.0, (f / e).clamp(0.0, 1.0))
419        } else {
420            let c = dot3(da, r);
421            if e <= eps {
422                ((-c / a).clamp(0.0, 1.0), 0.0)
423            } else {
424                let b = dot3(da, db);
425                let denom = a * e - b * b;
426                let ss = if denom.abs() > eps {
427                    ((b * f - c * e) / denom).clamp(0.0, 1.0)
428                } else {
429                    0.0
430                };
431                let tt = (b * ss + f) / e;
432                if tt < 0.0 {
433                    ((-c / a).clamp(0.0, 1.0), 0.0)
434                } else if tt > 1.0 {
435                    (((b - c) / a).clamp(0.0, 1.0), 1.0)
436                } else {
437                    (ss, tt)
438                }
439            }
440        };
441        let closest_a = add3(pa, scale3(da, s));
442        let closest_b = add3(pb, scale3(db, t2));
443        let diff = sub3(closest_a, closest_b);
444        let dist_sq = dot3(diff, diff);
445        if dist_sq <= radius * radius {
446            return Some(t);
447        }
448    }
449    None
450}
451
452/// Batch CCD for all edge pairs in a cloth mesh against another mesh.
453///
454/// `edges_a` and `edges_b` are (start, end) index pairs.
455pub fn batch_edge_edge_ccd(
456    edges_a: &[(usize, usize)],
457    verts0_a: &[[f64; 3]],
458    verts1_a: &[[f64; 3]],
459    edges_b: &[(usize, usize)],
460    verts0_b: &[[f64; 3]],
461    verts1_b: &[[f64; 3]],
462    radius: f64,
463) -> Vec<EdgeCcdResult> {
464    let mut results = Vec::new();
465    for (ea_idx, &(ia0, ia1)) in edges_a.iter().enumerate() {
466        for (eb_idx, &(ib0, ib1)) in edges_b.iter().enumerate() {
467            if ia0 == ib0 || ia0 == ib1 || ia1 == ib0 || ia1 == ib1 {
468                continue; // skip shared vertices
469            }
470            if ia0 >= verts0_a.len()
471                || ia1 >= verts0_a.len()
472                || ib0 >= verts0_b.len()
473                || ib1 >= verts0_b.len()
474            {
475                continue;
476            }
477            if let Some(toi) = edge_edge_ccd(
478                verts0_a[ia0],
479                verts1_a[ia0],
480                verts0_a[ia1],
481                verts1_a[ia1],
482                verts0_b[ib0],
483                verts1_b[ib0],
484                verts0_b[ib1],
485                verts1_b[ib1],
486                radius,
487            ) {
488                // Interpolate to compute normal at toi
489                let lerp = |a: [f64; 3], b: [f64; 3]| add3(scale3(a, 1.0 - toi), scale3(b, toi));
490                let pa = lerp(verts0_a[ia0], verts1_a[ia0]);
491                let pb = lerp(verts0_b[ib0], verts1_b[ib0]);
492                let diff = sub3(pa, pb);
493                let normal = normalize3(diff);
494                results.push(EdgeCcdResult {
495                    edge_a: ea_idx,
496                    edge_b: eb_idx,
497                    toi,
498                    normal,
499                });
500            }
501        }
502    }
503    results
504}
505
506// ─────────────────────────────────────────────────────────────────────────────
507// Penalty force springs for interpenetration
508// ─────────────────────────────────────────────────────────────────────────────
509
510/// Penalty spring configuration for interpenetration resolution.
511pub struct PenaltySpring {
512    /// Spring stiffness (force per unit penetration depth).
513    pub stiffness: f64,
514    /// Damping coefficient.
515    pub damping: f64,
516}
517
518impl PenaltySpring {
519    /// Construct a penalty spring with given stiffness and damping.
520    pub fn new(stiffness: f64, damping: f64) -> Self {
521        PenaltySpring { stiffness, damping }
522    }
523
524    /// Compute penalty force for a single interpenetrating vertex.
525    ///
526    /// `depth` is the penetration depth (positive), `normal` is the contact
527    /// normal pointing outward, `rel_velocity` is the relative velocity at the
528    /// contact point.  Returns the penalty force vector.
529    pub fn force(&self, depth: f64, normal: [f64; 3], rel_velocity: [f64; 3]) -> [f64; 3] {
530        if depth <= 0.0 {
531            return [0.0; 3];
532        }
533        let v_n = dot3(rel_velocity, normal);
534        let f_mag = self.stiffness * depth - self.damping * v_n;
535        scale3(normal, f_mag.max(0.0))
536    }
537
538    /// Apply penalty forces to all vertex-triangle contacts.
539    ///
540    /// Returns force vectors indexed by vertex index (accumulated).
541    pub fn apply_all(
542        &self,
543        contacts: &[VertexTriangleContact],
544        velocities: &[[f64; 3]],
545    ) -> Vec<(usize, [f64; 3])> {
546        let mut forces = Vec::new();
547        for c in contacts {
548            let vi = c.vertex_index;
549            let vel = if vi < velocities.len() {
550                velocities[vi]
551            } else {
552                [0.0; 3]
553            };
554            let f = self.force(c.depth, c.normal, vel);
555            forces.push((vi, f));
556        }
557        forces
558    }
559}
560
561// ─────────────────────────────────────────────────────────────────────────────
562// BVH for dynamic meshes
563// ─────────────────────────────────────────────────────────────────────────────
564
565/// Axis-aligned bounding box for BVH nodes.
566#[derive(Debug, Clone)]
567pub struct BvhAabb {
568    /// Minimum corner.
569    pub min: [f64; 3],
570    /// Maximum corner.
571    pub max: [f64; 3],
572}
573
574impl BvhAabb {
575    /// Construct from min/max corners.
576    pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
577        BvhAabb { min, max }
578    }
579
580    /// Construct from three triangle vertices with an optional margin.
581    pub fn from_triangle(a: [f64; 3], b: [f64; 3], c: [f64; 3], margin: f64) -> Self {
582        let min = [
583            a[0].min(b[0]).min(c[0]) - margin,
584            a[1].min(b[1]).min(c[1]) - margin,
585            a[2].min(b[2]).min(c[2]) - margin,
586        ];
587        let max = [
588            a[0].max(b[0]).max(c[0]) + margin,
589            a[1].max(b[1]).max(c[1]) + margin,
590            a[2].max(b[2]).max(c[2]) + margin,
591        ];
592        BvhAabb { min, max }
593    }
594
595    /// Test overlap with another AABB.
596    pub fn overlaps(&self, other: &BvhAabb) -> bool {
597        self.min[0] <= other.max[0]
598            && self.max[0] >= other.min[0]
599            && self.min[1] <= other.max[1]
600            && self.max[1] >= other.min[1]
601            && self.min[2] <= other.max[2]
602            && self.max[2] >= other.min[2]
603    }
604
605    /// Merge two AABBs into their union.
606    pub fn merge(&self, other: &BvhAabb) -> BvhAabb {
607        BvhAabb {
608            min: [
609                self.min[0].min(other.min[0]),
610                self.min[1].min(other.min[1]),
611                self.min[2].min(other.min[2]),
612            ],
613            max: [
614                self.max[0].max(other.max[0]),
615                self.max[1].max(other.max[1]),
616                self.max[2].max(other.max[2]),
617            ],
618        }
619    }
620
621    /// Surface area of the AABB (for SAH cost).
622    pub fn surface_area(&self) -> f64 {
623        let dx = self.max[0] - self.min[0];
624        let dy = self.max[1] - self.min[1];
625        let dz = self.max[2] - self.min[2];
626        2.0 * (dx * dy + dy * dz + dz * dx)
627    }
628}
629
630/// A flat BVH node for dynamic soft body mesh.
631#[derive(Debug, Clone)]
632pub struct BvhNode {
633    /// Bounding box.
634    pub aabb: BvhAabb,
635    /// Left child index (usize::MAX = leaf).
636    pub left: usize,
637    /// Right child index (usize::MAX = leaf).
638    pub right: usize,
639    /// Triangle index for leaf nodes.
640    pub tri_index: usize,
641}
642
643impl BvhNode {
644    /// Is this a leaf node?
645    pub fn is_leaf(&self) -> bool {
646        self.left == usize::MAX
647    }
648}
649
650/// A BVH built over a triangle mesh (flat array representation).
651pub struct SoftBodyBvh {
652    /// Flat array of nodes (root = index 0).
653    pub nodes: Vec<BvhNode>,
654}
655
656impl SoftBodyBvh {
657    /// Build a BVH for a set of triangles with a collision margin.
658    ///
659    /// Uses a simple midpoint split along the longest axis.
660    pub fn build(triangles: &[[usize; 3]], positions: &[[f64; 3]], margin: f64) -> Self {
661        let mut aabbs: Vec<(usize, BvhAabb)> = triangles
662            .iter()
663            .enumerate()
664            .map(|(i, &tri)| {
665                let a = positions[tri[0]];
666                let b = positions[tri[1]];
667                let c = positions[tri[2]];
668                (i, BvhAabb::from_triangle(a, b, c, margin))
669            })
670            .collect();
671
672        let mut nodes = Vec::new();
673        Self::build_recursive(&mut aabbs, &mut nodes);
674        SoftBodyBvh { nodes }
675    }
676
677    fn build_recursive(leaves: &mut [(usize, BvhAabb)], nodes: &mut Vec<BvhNode>) -> usize {
678        if leaves.is_empty() {
679            return usize::MAX;
680        }
681        if leaves.len() == 1 {
682            let idx = nodes.len();
683            nodes.push(BvhNode {
684                aabb: leaves[0].1.clone(),
685                left: usize::MAX,
686                right: usize::MAX,
687                tri_index: leaves[0].0,
688            });
689            return idx;
690        }
691
692        // Compute union AABB
693        let union = leaves
694            .iter()
695            .fold(leaves[0].1.clone(), |acc, (_, b)| acc.merge(b));
696
697        // Split along longest axis
698        let dx = union.max[0] - union.min[0];
699        let dy = union.max[1] - union.min[1];
700        let dz = union.max[2] - union.min[2];
701        let axis = if dx >= dy && dx >= dz {
702            0
703        } else if dy >= dz {
704            1
705        } else {
706            2
707        };
708        let mid_val = (union.min[axis] + union.max[axis]) / 2.0;
709
710        let pivot = leaves
711            .iter()
712            .position(|(_, b)| (b.min[axis] + b.max[axis]) / 2.0 >= mid_val)
713            .unwrap_or(leaves.len() / 2)
714            .max(1)
715            .min(leaves.len() - 1);
716
717        let (left_leaves, right_leaves) = leaves.split_at_mut(pivot);
718
719        let left_idx = Self::build_recursive(left_leaves, nodes);
720        let right_idx = Self::build_recursive(right_leaves, nodes);
721
722        let node_idx = nodes.len();
723        let left_aabb = if left_idx < nodes.len() {
724            nodes[left_idx].aabb.clone()
725        } else {
726            union.clone()
727        };
728        let right_aabb = if right_idx < nodes.len() {
729            nodes[right_idx].aabb.clone()
730        } else {
731            union.clone()
732        };
733        nodes.push(BvhNode {
734            aabb: left_aabb.merge(&right_aabb),
735            left: left_idx,
736            right: right_idx,
737            tri_index: usize::MAX,
738        });
739        node_idx
740    }
741
742    /// Query which triangles have AABBs that overlap with a point sphere.
743    pub fn query_sphere(&self, center: [f64; 3], radius: f64) -> Vec<usize> {
744        let sphere_aabb = BvhAabb {
745            min: [center[0] - radius, center[1] - radius, center[2] - radius],
746            max: [center[0] + radius, center[1] + radius, center[2] + radius],
747        };
748        let mut result = Vec::new();
749        if self.nodes.is_empty() {
750            return result;
751        }
752        let root = self.nodes.len() - 1;
753        self.query_recursive(root, &sphere_aabb, &mut result);
754        result
755    }
756
757    fn query_recursive(&self, idx: usize, query: &BvhAabb, result: &mut Vec<usize>) {
758        if idx == usize::MAX || idx >= self.nodes.len() {
759            return;
760        }
761        let node = &self.nodes[idx];
762        if !node.aabb.overlaps(query) {
763            return;
764        }
765        if node.is_leaf() {
766            result.push(node.tri_index);
767            return;
768        }
769        self.query_recursive(node.left, query, result);
770        self.query_recursive(node.right, query, result);
771    }
772}
773
774// ─────────────────────────────────────────────────────────────────────────────
775// Contact manifold for soft bodies
776// ─────────────────────────────────────────────────────────────────────────────
777
778/// A single contact point in a soft-body contact manifold.
779#[derive(Debug, Clone)]
780pub struct SoftContact {
781    /// Vertex index on the deformable mesh.
782    pub vertex_index: usize,
783    /// Contact normal (pointing away from obstacle).
784    pub normal: [f64; 3],
785    /// Penetration depth.
786    pub depth: f64,
787    /// Contact point position in world space.
788    pub position: [f64; 3],
789}
790
791/// Contact manifold for a soft body.
792pub struct SoftContactManifold {
793    /// All active contact points.
794    pub contacts: Vec<SoftContact>,
795    /// Maximum allowed contacts.
796    pub max_contacts: usize,
797}
798
799impl SoftContactManifold {
800    /// Create an empty manifold with a capacity limit.
801    pub fn new(max_contacts: usize) -> Self {
802        SoftContactManifold {
803            contacts: Vec::new(),
804            max_contacts,
805        }
806    }
807
808    /// Add a contact, removing the shallowest if at capacity.
809    pub fn add(&mut self, contact: SoftContact) {
810        if self.contacts.len() < self.max_contacts {
811            self.contacts.push(contact);
812        } else {
813            // Replace shallowest contact
814            if let Some(idx) = self
815                .contacts
816                .iter()
817                .enumerate()
818                .min_by(|a, b| {
819                    a.1.depth
820                        .partial_cmp(&b.1.depth)
821                        .unwrap_or(std::cmp::Ordering::Equal)
822                })
823                .map(|(i, _)| i)
824                && self.contacts[idx].depth < contact.depth
825            {
826                self.contacts[idx] = contact;
827            }
828        }
829    }
830
831    /// Clear all contacts.
832    pub fn clear(&mut self) {
833        self.contacts.clear();
834    }
835
836    /// Return the deepest contact, if any.
837    pub fn deepest(&self) -> Option<&SoftContact> {
838        self.contacts.iter().max_by(|a, b| {
839            a.depth
840                .partial_cmp(&b.depth)
841                .unwrap_or(std::cmp::Ordering::Equal)
842        })
843    }
844}
845
846// ─────────────────────────────────────────────────────────────────────────────
847// Tearing / cutting along collision boundaries (conceptual)
848// ─────────────────────────────────────────────────────────────────────────────
849
850/// Edge in a mesh that can potentially be torn.
851#[derive(Debug, Clone, PartialEq)]
852pub struct TearableEdge {
853    /// Index of vertex A.
854    pub va: usize,
855    /// Index of vertex B.
856    pub vb: usize,
857    /// Current accumulated stress on the edge.
858    pub stress: f64,
859    /// Stress threshold above which the edge tears.
860    pub threshold: f64,
861}
862
863impl TearableEdge {
864    /// Construct a tearable edge.
865    pub fn new(va: usize, vb: usize, threshold: f64) -> Self {
866        TearableEdge {
867            va,
868            vb,
869            stress: 0.0,
870            threshold,
871        }
872    }
873
874    /// Returns true if the edge should tear.
875    pub fn should_tear(&self) -> bool {
876        self.stress >= self.threshold
877    }
878
879    /// Accumulate stress from a contact force magnitude.
880    pub fn accumulate_stress(&mut self, force_magnitude: f64) {
881        self.stress += force_magnitude;
882    }
883}
884
885/// Simulate one step of tearing propagation.
886///
887/// Marks edges exceeding their threshold as torn and returns their indices.
888pub fn propagate_tears(edges: &mut [TearableEdge]) -> Vec<usize> {
889    let mut torn = Vec::new();
890    for (i, edge) in edges.iter_mut().enumerate() {
891        if edge.should_tear() {
892            torn.push(i);
893        }
894    }
895    torn
896}
897
898/// Apply stress to edges adjacent to a collision contact.
899///
900/// Increases stress proportional to the contact depth for edges that share
901/// the contacted vertex.
902pub fn apply_collision_stress(
903    edges: &mut [TearableEdge],
904    vertex_index: usize,
905    contact_depth: f64,
906    stress_factor: f64,
907) {
908    for edge in edges.iter_mut() {
909        if edge.va == vertex_index || edge.vb == vertex_index {
910            edge.accumulate_stress(contact_depth * stress_factor);
911        }
912    }
913}
914
915// ─────────────────────────────────────────────────────────────────────────────
916// GPU-friendly flat collision data structures
917// ─────────────────────────────────────────────────────────────────────────────
918
919/// GPU-friendly flat vertex buffer for cloth simulation.
920///
921/// All data is stored in structure-of-arrays (SoA) layout for coalesced
922/// GPU memory access.
923pub struct GpuClothBuffer {
924    /// X positions of all vertices.
925    pub pos_x: Vec<f32>,
926    /// Y positions of all vertices.
927    pub pos_y: Vec<f32>,
928    /// Z positions of all vertices.
929    pub pos_z: Vec<f32>,
930    /// X velocities.
931    pub vel_x: Vec<f32>,
932    /// Y velocities.
933    pub vel_y: Vec<f32>,
934    /// Z velocities.
935    pub vel_z: Vec<f32>,
936    /// Inverse masses (0 = pinned).
937    pub inv_mass: Vec<f32>,
938}
939
940impl GpuClothBuffer {
941    /// Construct from Rust `f64` vertex arrays.
942    pub fn from_vertices(positions: &[[f64; 3]], velocities: &[[f64; 3]], masses: &[f64]) -> Self {
943        let n = positions.len();
944        let mut buf = GpuClothBuffer {
945            pos_x: Vec::with_capacity(n),
946            pos_y: Vec::with_capacity(n),
947            pos_z: Vec::with_capacity(n),
948            vel_x: Vec::with_capacity(n),
949            vel_y: Vec::with_capacity(n),
950            vel_z: Vec::with_capacity(n),
951            inv_mass: Vec::with_capacity(n),
952        };
953        for &p in positions.iter() {
954            buf.pos_x.push(p[0] as f32);
955            buf.pos_y.push(p[1] as f32);
956            buf.pos_z.push(p[2] as f32);
957        }
958        for &v in velocities.iter() {
959            buf.vel_x.push(v[0] as f32);
960            buf.vel_y.push(v[1] as f32);
961            buf.vel_z.push(v[2] as f32);
962        }
963        for &m in masses {
964            buf.inv_mass
965                .push(if m > 0.0 { (1.0 / m) as f32 } else { 0.0 });
966        }
967        buf
968    }
969
970    /// Number of vertices.
971    pub fn len(&self) -> usize {
972        self.pos_x.len()
973    }
974
975    /// Returns true if the buffer is empty.
976    pub fn is_empty(&self) -> bool {
977        self.pos_x.is_empty()
978    }
979
980    /// Read a vertex position back to `[f64; 3]`.
981    pub fn position(&self, i: usize) -> [f64; 3] {
982        [
983            self.pos_x[i] as f64,
984            self.pos_y[i] as f64,
985            self.pos_z[i] as f64,
986        ]
987    }
988}
989
990/// GPU-friendly flat contact buffer (SoA layout).
991///
992/// Suitable for uploading to a GPU for parallel impulse resolution.
993pub struct GpuContactBuffer {
994    /// Vertex indices for each contact.
995    pub vertex_indices: Vec<u32>,
996    /// Contact normal X.
997    pub normal_x: Vec<f32>,
998    /// Contact normal Y.
999    pub normal_y: Vec<f32>,
1000    /// Contact normal Z.
1001    pub normal_z: Vec<f32>,
1002    /// Penetration depths.
1003    pub depths: Vec<f32>,
1004}
1005
1006impl GpuContactBuffer {
1007    /// Construct an empty GPU contact buffer.
1008    pub fn new() -> Self {
1009        GpuContactBuffer {
1010            vertex_indices: Vec::new(),
1011            normal_x: Vec::new(),
1012            normal_y: Vec::new(),
1013            normal_z: Vec::new(),
1014            depths: Vec::new(),
1015        }
1016    }
1017
1018    /// Populate from vertex-triangle contacts.
1019    pub fn from_contacts(contacts: &[VertexTriangleContact]) -> Self {
1020        let mut buf = Self::new();
1021        for c in contacts {
1022            buf.vertex_indices.push(c.vertex_index as u32);
1023            buf.normal_x.push(c.normal[0] as f32);
1024            buf.normal_y.push(c.normal[1] as f32);
1025            buf.normal_z.push(c.normal[2] as f32);
1026            buf.depths.push(c.depth as f32);
1027        }
1028        buf
1029    }
1030
1031    /// Number of contacts.
1032    pub fn len(&self) -> usize {
1033        self.vertex_indices.len()
1034    }
1035
1036    /// Returns true if the buffer is empty.
1037    pub fn is_empty(&self) -> bool {
1038        self.vertex_indices.is_empty()
1039    }
1040}
1041
1042impl Default for GpuContactBuffer {
1043    fn default() -> Self {
1044        Self::new()
1045    }
1046}
1047
1048/// GPU-friendly edge buffer (SoA) for CCD.
1049pub struct GpuEdgeBuffer {
1050    /// Start vertex indices.
1051    pub start: Vec<u32>,
1052    /// End vertex indices.
1053    pub end: Vec<u32>,
1054}
1055
1056impl GpuEdgeBuffer {
1057    /// Construct from edge list.
1058    pub fn from_edges(edges: &[(usize, usize)]) -> Self {
1059        let start = edges.iter().map(|&(a, _)| a as u32).collect();
1060        let end = edges.iter().map(|&(_, b)| b as u32).collect();
1061        GpuEdgeBuffer { start, end }
1062    }
1063
1064    /// Number of edges.
1065    pub fn len(&self) -> usize {
1066        self.start.len()
1067    }
1068
1069    /// Returns true if no edges.
1070    pub fn is_empty(&self) -> bool {
1071        self.start.is_empty()
1072    }
1073}
1074
1075// ─────────────────────────────────────────────────────────────────────────────
1076// Unit tests
1077// ─────────────────────────────────────────────────────────────────────────────
1078
1079#[cfg(test)]
1080mod tests {
1081    use super::*;
1082
1083    const EPS: f64 = 1e-10;
1084
1085    // ── vertex_triangle_closest ───────────────────────────────────────────────
1086
1087    #[test]
1088    fn test_vtc_vertex_above_centre() {
1089        let a = [0.0, 0.0, 0.0];
1090        let b = [1.0, 0.0, 0.0];
1091        let c = [0.0, 1.0, 0.0];
1092        let p = [0.25, 0.25, 1.0];
1093        let (sq_d, bary) = vertex_triangle_closest(p, a, b, c);
1094        // closest point is (0.25, 0.25, 0) → dist = 1
1095        assert!((sq_d - 1.0).abs() < 1e-8);
1096        assert!((bary[0] + bary[1] + bary[2] - 1.0).abs() < EPS);
1097    }
1098
1099    #[test]
1100    fn test_vtc_vertex_at_corner() {
1101        let a = [0.0, 0.0, 0.0];
1102        let b = [1.0, 0.0, 0.0];
1103        let c = [0.0, 1.0, 0.0];
1104        let (sq_d, _bary) = vertex_triangle_closest(a, a, b, c);
1105        assert!(sq_d.abs() < EPS);
1106    }
1107
1108    #[test]
1109    fn test_vtc_vertex_outside_edge() {
1110        // Point projected off AB edge
1111        let a = [0.0, 0.0, 0.0];
1112        let b = [1.0, 0.0, 0.0];
1113        let c = [0.0, 1.0, 0.0];
1114        let p = [0.5, -1.0, 0.0];
1115        let (sq_d, _) = vertex_triangle_closest(p, a, b, c);
1116        // closest on edge AB at (0.5, 0, 0), dist = 1
1117        assert!((sq_d - 1.0).abs() < 1e-8);
1118    }
1119
1120    #[test]
1121    fn test_vtc_bary_sum_to_one() {
1122        let a = [0.0, 0.0, 0.0];
1123        let b = [2.0, 0.0, 0.0];
1124        let c = [0.0, 2.0, 0.0];
1125        let p = [0.5, 0.5, 0.5];
1126        let (_, bary) = vertex_triangle_closest(p, a, b, c);
1127        let sum = bary[0] + bary[1] + bary[2];
1128        assert!((sum - 1.0).abs() < 1e-8);
1129    }
1130
1131    #[test]
1132    fn test_vtc_distance_non_negative() {
1133        let a = [0.0, 0.0, 0.0];
1134        let b = [1.0, 0.0, 0.0];
1135        let c = [0.0, 1.0, 0.0];
1136        let p = [5.0, 5.0, 5.0];
1137        let (sq_d, _) = vertex_triangle_closest(p, a, b, c);
1138        assert!(sq_d >= 0.0);
1139    }
1140
1141    // ── detect_vertex_triangle_contacts ───────────────────────────────────────
1142
1143    #[test]
1144    fn test_detect_contacts_finds_close_vertex() {
1145        let verts = vec![[0.25, 0.25, 0.05]];
1146        let tris = vec![[0, 1, 2]];
1147        let tri_pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1148        let contacts = detect_vertex_triangle_contacts(&verts, &tris, &tri_pos, 0.1);
1149        assert_eq!(contacts.len(), 1);
1150        assert!(contacts[0].depth > 0.0);
1151    }
1152
1153    #[test]
1154    fn test_detect_contacts_misses_far_vertex() {
1155        let verts = vec![[0.25, 0.25, 5.0]];
1156        let tris = vec![[0, 1, 2]];
1157        let tri_pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1158        let contacts = detect_vertex_triangle_contacts(&verts, &tris, &tri_pos, 0.1);
1159        assert_eq!(contacts.len(), 0);
1160    }
1161
1162    #[test]
1163    fn test_detect_contacts_normal_points_away_from_tri() {
1164        let verts = vec![[0.25, 0.25, 0.05]];
1165        let tris = vec![[0, 1, 2]];
1166        let tri_pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1167        let contacts = detect_vertex_triangle_contacts(&verts, &tris, &tri_pos, 0.1);
1168        assert_eq!(contacts.len(), 1);
1169        // Normal should point toward vertex (positive z component)
1170        assert!(contacts[0].normal[2] > 0.0);
1171    }
1172
1173    // ── ClothSpatialHash ───────────────────────────────────────────────────────
1174
1175    #[test]
1176    fn test_spatial_hash_finds_nearby_pair() {
1177        let mut sh = ClothSpatialHash::new(0.5);
1178        let positions = vec![[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [10.0, 0.0, 0.0]];
1179        sh.build(&positions);
1180        let contacts = sh.detect_self_collisions(&positions, 0.3);
1181        assert_eq!(contacts.len(), 1);
1182        assert_eq!((contacts[0].0, contacts[0].1), (0, 1));
1183    }
1184
1185    #[test]
1186    fn test_spatial_hash_no_contacts_far_apart() {
1187        let mut sh = ClothSpatialHash::new(1.0);
1188        let positions = vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]];
1189        sh.build(&positions);
1190        let contacts = sh.detect_self_collisions(&positions, 0.3);
1191        assert_eq!(contacts.len(), 0);
1192    }
1193
1194    #[test]
1195    fn test_spatial_hash_contact_normal_unit_length() {
1196        let mut sh = ClothSpatialHash::new(0.5);
1197        let positions = vec![[0.0, 0.0, 0.0], [0.1, 0.0, 0.0]];
1198        sh.build(&positions);
1199        let contacts = sh.detect_self_collisions(&positions, 0.3);
1200        for (_, _, n, _) in &contacts {
1201            let l = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1202            assert!((l - 1.0).abs() < 1e-8);
1203        }
1204    }
1205
1206    // ── compute_contact_impulses ──────────────────────────────────────────────
1207
1208    #[test]
1209    fn test_impulse_separates_penetrating_vertex() {
1210        let contacts = vec![VertexTriangleContact {
1211            vertex_index: 0,
1212            triangle_index: 0,
1213            normal: [0.0, 1.0, 0.0],
1214            depth: 0.05,
1215            bary: [0.33, 0.33, 0.34],
1216        }];
1217        let velocities = vec![[0.0, -1.0, 0.0]]; // moving into surface
1218        let masses = vec![1.0];
1219        let impulses = compute_contact_impulses(&contacts, &velocities, &masses, 0.0, 0.0);
1220        assert_eq!(impulses.len(), 1);
1221        assert!(impulses[0].delta_v[1] > 0.0); // velocity should flip
1222    }
1223
1224    #[test]
1225    fn test_impulse_no_response_for_separating_vertex() {
1226        let contacts = vec![VertexTriangleContact {
1227            vertex_index: 0,
1228            triangle_index: 0,
1229            normal: [0.0, 1.0, 0.0],
1230            depth: 0.05,
1231            bary: [0.33, 0.33, 0.34],
1232        }];
1233        let velocities = vec![[0.0, 1.0, 0.0]]; // moving away
1234        let masses = vec![1.0];
1235        let impulses = compute_contact_impulses(&contacts, &velocities, &masses, 0.0, 0.0);
1236        assert_eq!(impulses.len(), 0);
1237    }
1238
1239    // ── edge_edge_ccd ─────────────────────────────────────────────────────────
1240
1241    #[test]
1242    fn test_edge_ccd_detects_approaching_edges() {
1243        // Edge A: from (0,0,0)→(1,0,0) moving toward edge B
1244        // Edge B: from (0.5,1,0)→(0.5,-1,0) moving toward edge A
1245        let toi = edge_edge_ccd(
1246            [0.0, 0.0, 0.0],
1247            [0.0, 0.0, 0.0],
1248            [1.0, 0.0, 0.0],
1249            [1.0, 0.0, 0.0],
1250            [0.5, 0.5, 0.0],
1251            [0.5, 0.02, 0.0],
1252            [0.5, 0.5, 0.5],
1253            [0.5, 0.02, 0.5],
1254            0.1,
1255        );
1256        assert!(toi.is_some());
1257    }
1258
1259    #[test]
1260    fn test_edge_ccd_no_collision_when_parallel() {
1261        // Two parallel non-approaching edges
1262        let toi = edge_edge_ccd(
1263            [0.0, 0.0, 0.0],
1264            [0.0, 0.0, 0.0],
1265            [1.0, 0.0, 0.0],
1266            [1.0, 0.0, 0.0],
1267            [0.0, 10.0, 0.0],
1268            [0.0, 10.0, 0.0],
1269            [1.0, 10.0, 0.0],
1270            [1.0, 10.0, 0.0],
1271            0.1,
1272        );
1273        assert!(toi.is_none());
1274    }
1275
1276    // ── PenaltySpring ─────────────────────────────────────────────────────────
1277
1278    #[test]
1279    fn test_penalty_force_zero_at_zero_depth() {
1280        let ps = PenaltySpring::new(1000.0, 10.0);
1281        let f = ps.force(0.0, [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]);
1282        assert!(f[0].abs() < EPS && f[1].abs() < EPS && f[2].abs() < EPS);
1283    }
1284
1285    #[test]
1286    fn test_penalty_force_positive_for_penetration() {
1287        let ps = PenaltySpring::new(1000.0, 0.0);
1288        let f = ps.force(0.01, [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]);
1289        assert!(f[1] > 0.0);
1290    }
1291
1292    #[test]
1293    fn test_penalty_force_direction_matches_normal() {
1294        let ps = PenaltySpring::new(500.0, 0.0);
1295        let normal = normalize3([1.0, 1.0, 0.0]);
1296        let f = ps.force(0.05, normal, [0.0, 0.0, 0.0]);
1297        let f_len = len3(f);
1298        if f_len > 1e-14 {
1299            let f_dir = scale3(f, 1.0 / f_len);
1300            assert!((f_dir[0] - normal[0]).abs() < 1e-6);
1301            assert!((f_dir[1] - normal[1]).abs() < 1e-6);
1302        }
1303    }
1304
1305    // ── BvhAabb ────────────────────────────────────────────────────────────────
1306
1307    #[test]
1308    fn test_bvh_aabb_overlaps() {
1309        let a = BvhAabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1310        let b = BvhAabb::new([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
1311        assert!(a.overlaps(&b));
1312    }
1313
1314    #[test]
1315    fn test_bvh_aabb_no_overlap() {
1316        let a = BvhAabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1317        let b = BvhAabb::new([2.0, 2.0, 2.0], [3.0, 3.0, 3.0]);
1318        assert!(!a.overlaps(&b));
1319    }
1320
1321    #[test]
1322    fn test_bvh_aabb_merge() {
1323        let a = BvhAabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1324        let b = BvhAabb::new([2.0, 2.0, 2.0], [3.0, 3.0, 3.0]);
1325        let merged = a.merge(&b);
1326        assert_eq!(merged.min, [0.0, 0.0, 0.0]);
1327        assert_eq!(merged.max, [3.0, 3.0, 3.0]);
1328    }
1329
1330    // ── SoftBodyBvh ────────────────────────────────────────────────────────────
1331
1332    #[test]
1333    fn test_bvh_build_and_query() {
1334        let positions = vec![
1335            [0.0, 0.0, 0.0],
1336            [1.0, 0.0, 0.0],
1337            [0.0, 1.0, 0.0],
1338            [5.0, 5.0, 5.0],
1339            [6.0, 5.0, 5.0],
1340            [5.0, 6.0, 5.0],
1341        ];
1342        let triangles = vec![[0, 1, 2], [3, 4, 5]];
1343        let bvh = SoftBodyBvh::build(&triangles, &positions, 0.01);
1344        let hits = bvh.query_sphere([0.25, 0.25, 0.0], 0.5);
1345        assert!(!hits.is_empty());
1346    }
1347
1348    #[test]
1349    fn test_bvh_query_misses_distant_triangle() {
1350        let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1351        let triangles = vec![[0, 1, 2]];
1352        let bvh = SoftBodyBvh::build(&triangles, &positions, 0.01);
1353        let hits = bvh.query_sphere([100.0, 100.0, 100.0], 0.5);
1354        assert_eq!(hits.len(), 0);
1355    }
1356
1357    // ── SoftContactManifold ────────────────────────────────────────────────────
1358
1359    #[test]
1360    fn test_manifold_add_and_deepest() {
1361        let mut manifold = SoftContactManifold::new(4);
1362        manifold.add(SoftContact {
1363            vertex_index: 0,
1364            normal: [0.0, 1.0, 0.0],
1365            depth: 0.1,
1366            position: [0.0, 0.0, 0.0],
1367        });
1368        manifold.add(SoftContact {
1369            vertex_index: 1,
1370            normal: [0.0, 1.0, 0.0],
1371            depth: 0.5,
1372            position: [1.0, 0.0, 0.0],
1373        });
1374        let deepest = manifold.deepest().unwrap();
1375        assert_eq!(deepest.vertex_index, 1);
1376    }
1377
1378    #[test]
1379    fn test_manifold_capacity_limit() {
1380        let mut manifold = SoftContactManifold::new(2);
1381        for i in 0..5 {
1382            manifold.add(SoftContact {
1383                vertex_index: i,
1384                normal: [0.0, 1.0, 0.0],
1385                depth: i as f64 * 0.1,
1386                position: [0.0, 0.0, 0.0],
1387            });
1388        }
1389        assert!(manifold.contacts.len() <= 2);
1390    }
1391
1392    #[test]
1393    fn test_manifold_clear() {
1394        let mut manifold = SoftContactManifold::new(4);
1395        manifold.add(SoftContact {
1396            vertex_index: 0,
1397            normal: [0.0, 1.0, 0.0],
1398            depth: 0.1,
1399            position: [0.0, 0.0, 0.0],
1400        });
1401        manifold.clear();
1402        assert!(manifold.contacts.is_empty());
1403    }
1404
1405    // ── TearableEdge / propagate_tears ─────────────────────────────────────────
1406
1407    #[test]
1408    fn test_edge_tears_when_stress_exceeded() {
1409        let mut edge = TearableEdge::new(0, 1, 1.0);
1410        edge.accumulate_stress(1.5);
1411        assert!(edge.should_tear());
1412    }
1413
1414    #[test]
1415    fn test_edge_does_not_tear_below_threshold() {
1416        let mut edge = TearableEdge::new(0, 1, 1.0);
1417        edge.accumulate_stress(0.5);
1418        assert!(!edge.should_tear());
1419    }
1420
1421    #[test]
1422    fn test_propagate_tears_returns_torn_indices() {
1423        let mut edges = vec![TearableEdge::new(0, 1, 1.0), TearableEdge::new(1, 2, 1.0)];
1424        edges[0].stress = 2.0;
1425        edges[1].stress = 0.1;
1426        let torn = propagate_tears(&mut edges);
1427        assert_eq!(torn, vec![0]);
1428    }
1429
1430    #[test]
1431    fn test_apply_collision_stress() {
1432        let mut edges = vec![TearableEdge::new(0, 1, 10.0), TearableEdge::new(2, 3, 10.0)];
1433        apply_collision_stress(&mut edges, 0, 0.5, 2.0);
1434        // Edge 0 shares vertex 0, should have stress 1.0
1435        assert!((edges[0].stress - 1.0).abs() < EPS);
1436        // Edge 1 does not share vertex 0
1437        assert!(edges[1].stress.abs() < EPS);
1438    }
1439
1440    // ── GpuClothBuffer ─────────────────────────────────────────────────────────
1441
1442    #[test]
1443    fn test_gpu_cloth_buffer_len() {
1444        let pos = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1445        let vel = vec![[0.0; 3]; 2];
1446        let mass = vec![1.0, 2.0];
1447        let buf = GpuClothBuffer::from_vertices(&pos, &vel, &mass);
1448        assert_eq!(buf.len(), 2);
1449    }
1450
1451    #[test]
1452    fn test_gpu_cloth_buffer_position_roundtrip() {
1453        let pos = vec![[1.5, -2.5, 3.0]];
1454        let vel = vec![[0.0; 3]];
1455        let mass = vec![1.0];
1456        let buf = GpuClothBuffer::from_vertices(&pos, &vel, &mass);
1457        let p = buf.position(0);
1458        assert!((p[0] - 1.5).abs() < 1e-5);
1459        assert!((p[1] - (-2.5)).abs() < 1e-5);
1460        assert!((p[2] - 3.0).abs() < 1e-5);
1461    }
1462
1463    #[test]
1464    fn test_gpu_cloth_buffer_pinned_vertex() {
1465        let pos = vec![[0.0; 3]];
1466        let vel = vec![[0.0; 3]];
1467        let mass = vec![0.0]; // pinned
1468        let buf = GpuClothBuffer::from_vertices(&pos, &vel, &mass);
1469        assert_eq!(buf.inv_mass[0], 0.0);
1470    }
1471
1472    // ── GpuContactBuffer ───────────────────────────────────────────────────────
1473
1474    #[test]
1475    fn test_gpu_contact_buffer_from_contacts() {
1476        let contacts = vec![VertexTriangleContact {
1477            vertex_index: 3,
1478            triangle_index: 0,
1479            normal: [0.0, 1.0, 0.0],
1480            depth: 0.05,
1481            bary: [0.33, 0.33, 0.34],
1482        }];
1483        let buf = GpuContactBuffer::from_contacts(&contacts);
1484        assert_eq!(buf.len(), 1);
1485        assert_eq!(buf.vertex_indices[0], 3);
1486        assert!((buf.depths[0] - 0.05).abs() < 1e-5);
1487    }
1488
1489    #[test]
1490    fn test_gpu_contact_buffer_empty() {
1491        let buf = GpuContactBuffer::new();
1492        assert!(buf.is_empty());
1493    }
1494
1495    // ── GpuEdgeBuffer ──────────────────────────────────────────────────────────
1496
1497    #[test]
1498    fn test_gpu_edge_buffer() {
1499        let edges = vec![(0, 1), (1, 2), (2, 0)];
1500        let buf = GpuEdgeBuffer::from_edges(&edges);
1501        assert_eq!(buf.len(), 3);
1502        assert_eq!(buf.start[0], 0);
1503        assert_eq!(buf.end[2], 0);
1504    }
1505
1506    // ── batch_edge_edge_ccd ────────────────────────────────────────────────────
1507
1508    #[test]
1509    fn test_batch_ccd_empty_edges() {
1510        let results = batch_edge_edge_ccd(&[], &[], &[], &[], &[], &[], 0.01);
1511        assert!(results.is_empty());
1512    }
1513
1514    #[test]
1515    fn test_batch_ccd_skips_shared_vertices() {
1516        // Edges sharing vertex 0: no collision should be reported
1517        let edges_a = vec![(0, 1)];
1518        let edges_b = vec![(0, 2)];
1519        let v0 = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1520        let v1 = v0.clone();
1521        let results = batch_edge_edge_ccd(&edges_a, &v0, &v1, &edges_b, &v0, &v1, 0.01);
1522        assert!(results.is_empty());
1523    }
1524}