Skip to main content

oxiphysics_collision/
softbody_collision.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Soft body collision detection: deformable meshes, cloth, self-collision.
5//!
6//! Provides deformable-vs-BVH, self-collision with spatial hashing and normal
7//! cone culling, cloth-vs-SDF contacts, edge-edge contacts, and CCD for
8//! deformable meshes.
9
10use std::collections::HashMap;
11
12// ---------------------------------------------------------------------------
13// Helper free functions
14// ---------------------------------------------------------------------------
15
16/// Compute the squared distance from point `p` to the triangle `(a, b, c)`.
17/// Returns (squared distance, barycentric coordinates (u, v, w)).
18pub fn vertex_triangle_distance(
19    p: [f64; 3],
20    a: [f64; 3],
21    b: [f64; 3],
22    c: [f64; 3],
23) -> (f64, [f64; 3]) {
24    let ab = sub3(b, a);
25    let ac = sub3(c, a);
26    let ap = sub3(p, a);
27
28    let d1 = dot3(ab, ap);
29    let d2 = dot3(ac, ap);
30    if d1 <= 0.0 && d2 <= 0.0 {
31        let bary = [1.0, 0.0, 0.0];
32        return (dot3(ap, ap), bary);
33    }
34
35    let bp = sub3(p, b);
36    let d3 = dot3(ab, bp);
37    let d4 = dot3(ac, bp);
38    if d3 >= 0.0 && d4 <= d3 {
39        let bary = [0.0, 1.0, 0.0];
40        let diff = sub3(p, b);
41        return (dot3(diff, diff), bary);
42    }
43
44    let vc = d1 * d4 - d3 * d2;
45    if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
46        let v = d1 / (d1 - d3);
47        let closest = add3(a, scale3(ab, v));
48        let diff = sub3(p, closest);
49        let bary = [1.0 - v, v, 0.0];
50        return (dot3(diff, diff), bary);
51    }
52
53    let cp = sub3(p, c);
54    let d5 = dot3(ab, cp);
55    let d6 = dot3(ac, cp);
56    if d6 >= 0.0 && d5 <= d6 {
57        let bary = [0.0, 0.0, 1.0];
58        let diff = sub3(p, c);
59        return (dot3(diff, diff), bary);
60    }
61
62    let vb = d5 * d2 - d1 * d6;
63    if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
64        let w = d2 / (d2 - d6);
65        let closest = add3(a, scale3(ac, w));
66        let diff = sub3(p, closest);
67        let bary = [1.0 - w, 0.0, w];
68        return (dot3(diff, diff), bary);
69    }
70
71    let va = d3 * d6 - d5 * d4;
72    if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
73        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
74        let bc = sub3(c, b);
75        let closest = add3(b, scale3(bc, w));
76        let diff = sub3(p, closest);
77        let bary = [0.0, 1.0 - w, w];
78        return (dot3(diff, diff), bary);
79    }
80
81    let denom = 1.0 / (va + vb + vc);
82    let v = vb * denom;
83    let w = vc * denom;
84    let closest = add3(add3(a, scale3(ab, v)), scale3(ac, w));
85    let diff = sub3(p, closest);
86    let bary = [1.0 - v - w, v, w];
87    (dot3(diff, diff), bary)
88}
89
90/// Compute the squared distance and closest points between two line segments
91/// `(p, p+d)` and `(q, q+e)`.
92/// Returns (sq_dist, s, t) where s,t ∈ \[0,1\] parameterise the closest points.
93pub fn edge_edge_distance(p: [f64; 3], d: [f64; 3], q: [f64; 3], e: [f64; 3]) -> (f64, f64, f64) {
94    let r = sub3(p, q);
95    let a = dot3(d, d);
96    let f = dot3(e, e);
97    let c = dot3(d, r);
98
99    let eps = 1e-10;
100    let (s, t) = if a <= eps && f <= eps {
101        (0.0, 0.0)
102    } else if a <= eps {
103        let t = (dot3(e, r) / f).clamp(0.0, 1.0);
104        (0.0, t)
105    } else {
106        let e_dot = dot3(e, r);
107        if f <= eps {
108            let s = (-c / a).clamp(0.0, 1.0);
109            (s, 0.0)
110        } else {
111            let b = dot3(d, e);
112            let denom = a * f - b * b;
113            let s = if denom.abs() > eps {
114                ((b * e_dot - c * f) / denom).clamp(0.0, 1.0)
115            } else {
116                0.0
117            };
118            let t = (b * s + e_dot) / f;
119            if t < 0.0 {
120                let s2 = (-c / a).clamp(0.0, 1.0);
121                (s2, 0.0)
122            } else if t > 1.0 {
123                let s2 = ((b - c) / a).clamp(0.0, 1.0);
124                (s2, 1.0)
125            } else {
126                (s, t)
127            }
128        }
129    };
130
131    let closest_p = add3(p, scale3(d, s));
132    let closest_q = add3(q, scale3(e, t));
133    let diff = sub3(closest_p, closest_q);
134    (dot3(diff, diff), s, t)
135}
136
137/// Compute the half-angle of the normal cone for a set of vertex normals.
138/// Returns the cosine of the maximum deviation angle from the average normal.
139pub fn normal_cone_angle(normals: &[[f64; 3]]) -> f64 {
140    if normals.is_empty() {
141        return 0.0;
142    }
143    let avg = normals.iter().fold([0.0f64; 3], |acc, n| add3(acc, *n));
144    let avg = normalize3(avg);
145    normals
146        .iter()
147        .map(|n| dot3(*n, avg).clamp(-1.0, 1.0))
148        .fold(f64::INFINITY, f64::min)
149}
150
151/// Compute the first time-of-impact for a pair of linearly-moving vertices,
152/// given vertex trajectory over one timestep.
153/// Returns fraction `t ∈ [0, 1]` or `None` if no impact within the step.
154pub fn pairwise_ccd_dt(
155    p0: [f64; 3],
156    p1: [f64; 3],
157    q0: [f64; 3],
158    q1: [f64; 3],
159    radius: f64,
160) -> Option<f64> {
161    let dp = sub3(p1, p0);
162    let dq = sub3(q1, q0);
163    let rel_vel = sub3(dp, dq);
164    let r0 = sub3(p0, q0);
165
166    let a = dot3(rel_vel, rel_vel);
167    let b = 2.0 * dot3(r0, rel_vel);
168    let c = dot3(r0, r0) - radius * radius;
169
170    if a < 1e-14 {
171        if c <= 0.0 {
172            return Some(0.0);
173        }
174        return None;
175    }
176
177    let disc = b * b - 4.0 * a * c;
178    if disc < 0.0 {
179        return None;
180    }
181    let sqrt_disc = disc.sqrt();
182    let t0 = (-b - sqrt_disc) / (2.0 * a);
183    let t1 = (-b + sqrt_disc) / (2.0 * a);
184    if t1 < 0.0 || t0 > 1.0 {
185        return None;
186    }
187    Some(t0.max(0.0))
188}
189
190// ---------------------------------------------------------------------------
191// Internal vector helpers (no nalgebra)
192// ---------------------------------------------------------------------------
193
194#[inline]
195fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
196    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
197}
198
199#[inline]
200fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
201    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
202}
203
204#[inline]
205fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
206    [a[0] * s, a[1] * s, a[2] * s]
207}
208
209#[inline]
210fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
211    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
212}
213
214#[inline]
215fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
216    [
217        a[1] * b[2] - a[2] * b[1],
218        a[2] * b[0] - a[0] * b[2],
219        a[0] * b[1] - a[1] * b[0],
220    ]
221}
222
223#[inline]
224fn len3(a: [f64; 3]) -> f64 {
225    dot3(a, a).sqrt()
226}
227
228#[inline]
229fn normalize3(a: [f64; 3]) -> [f64; 3] {
230    let l = len3(a);
231    if l < 1e-14 {
232        [0.0, 0.0, 1.0]
233    } else {
234        scale3(a, 1.0 / l)
235    }
236}
237
238// ---------------------------------------------------------------------------
239// Deformable vs BVH
240// ---------------------------------------------------------------------------
241
242/// Axis-aligned bounding box over a triangle for BVH nodes.
243#[derive(Debug, Clone)]
244pub struct TriAabb {
245    /// Minimum corner.
246    pub min: [f64; 3],
247    /// Maximum corner.
248    pub max: [f64; 3],
249    /// Triangle index (leaf node).
250    pub tri_index: usize,
251}
252
253impl TriAabb {
254    /// Construct from three vertices.
255    pub fn from_triangle(a: [f64; 3], b: [f64; 3], c: [f64; 3], idx: usize) -> Self {
256        let min = [
257            a[0].min(b[0]).min(c[0]),
258            a[1].min(b[1]).min(c[1]),
259            a[2].min(b[2]).min(c[2]),
260        ];
261        let max = [
262            a[0].max(b[0]).max(c[0]),
263            a[1].max(b[1]).max(c[1]),
264            a[2].max(b[2]).max(c[2]),
265        ];
266        TriAabb {
267            min,
268            max,
269            tri_index: idx,
270        }
271    }
272
273    /// Test overlap with another AABB.
274    pub fn overlaps(&self, other: &TriAabb) -> bool {
275        self.min[0] <= other.max[0]
276            && self.max[0] >= other.min[0]
277            && self.min[1] <= other.max[1]
278            && self.max[1] >= other.min[1]
279            && self.min[2] <= other.max[2]
280            && self.max[2] >= other.min[2]
281    }
282}
283
284/// Result of a deformable vs BVH query: a contact between a deformable vertex
285/// and a rigid triangle.
286#[derive(Debug, Clone)]
287pub struct DeformContact {
288    /// Index of the deformable vertex.
289    pub vertex_index: usize,
290    /// Index of the rigid triangle.
291    pub tri_index: usize,
292    /// Penetration depth (positive = overlapping).
293    pub depth: f64,
294    /// Contact normal (pointing from rigid surface to deformable vertex).
295    pub normal: [f64; 3],
296    /// Closest point on the rigid triangle.
297    pub point_on_rigid: [f64; 3],
298}
299
300/// Collision detection between a deformable mesh and a rigid BVH.
301///
302/// Queries each deformable vertex against the rigid BVH leaf triangles.
303pub struct DeformableVsBvh {
304    /// Flat list of rigid triangles (vertex triples).
305    pub rigid_triangles: Vec<[[f64; 3]; 3]>,
306    /// Per-triangle AABBs.
307    pub aabbs: Vec<TriAabb>,
308    /// Contact distance threshold.
309    pub contact_threshold: f64,
310}
311
312impl DeformableVsBvh {
313    /// Construct from rigid triangle soup.
314    pub fn new(triangles: Vec<[[f64; 3]; 3]>, contact_threshold: f64) -> Self {
315        let aabbs = triangles
316            .iter()
317            .enumerate()
318            .map(|(i, t)| TriAabb::from_triangle(t[0], t[1], t[2], i))
319            .collect();
320        DeformableVsBvh {
321            rigid_triangles: triangles,
322            aabbs,
323            contact_threshold,
324        }
325    }
326
327    /// Query all deformable vertices against rigid triangles.
328    /// Returns list of contacts.
329    pub fn query(&self, vertices: &[[f64; 3]]) -> Vec<DeformContact> {
330        let mut contacts = Vec::new();
331        for (vi, &v) in vertices.iter().enumerate() {
332            // Build a query AABB around the vertex
333            let r = self.contact_threshold;
334            let query_aabb = TriAabb {
335                min: [v[0] - r, v[1] - r, v[2] - r],
336                max: [v[0] + r, v[1] + r, v[2] + r],
337                tri_index: 0,
338            };
339            for aabb in &self.aabbs {
340                if !aabb.overlaps(&query_aabb) {
341                    continue;
342                }
343                let tri = &self.rigid_triangles[aabb.tri_index];
344                let (sq_dist, bary) = vertex_triangle_distance(v, tri[0], tri[1], tri[2]);
345                let dist = sq_dist.sqrt();
346                if dist < self.contact_threshold {
347                    let closest = add3(
348                        add3(scale3(tri[0], bary[0]), scale3(tri[1], bary[1])),
349                        scale3(tri[2], bary[2]),
350                    );
351                    let ab = sub3(tri[1], tri[0]);
352                    let ac = sub3(tri[2], tri[0]);
353                    let tri_normal = normalize3(cross3(ab, ac));
354                    contacts.push(DeformContact {
355                        vertex_index: vi,
356                        tri_index: aabb.tri_index,
357                        depth: self.contact_threshold - dist,
358                        normal: tri_normal,
359                        point_on_rigid: closest,
360                    });
361                }
362            }
363        }
364        contacts
365    }
366}
367
368// ---------------------------------------------------------------------------
369// Normal Cone Filter
370// ---------------------------------------------------------------------------
371
372/// Back-face culling filter for self-collision using vertex normals and a
373/// cone angle threshold.
374pub struct NormalConeFilter {
375    /// Cosine of the culling cone half-angle threshold.
376    /// Pairs whose normals are within this cone are candidates.
377    pub cos_threshold: f64,
378}
379
380impl NormalConeFilter {
381    /// Construct with a cone half-angle in radians.
382    pub fn new(half_angle_rad: f64) -> Self {
383        NormalConeFilter {
384            cos_threshold: half_angle_rad.cos(),
385        }
386    }
387
388    /// Returns `true` if this vertex-triangle pair should be tested for
389    /// self-collision (not culled by the normal cone).
390    pub fn should_test(&self, vertex_normal: [f64; 3], tri_normal: [f64; 3]) -> bool {
391        dot3(vertex_normal, tri_normal) < self.cos_threshold
392    }
393}
394
395// ---------------------------------------------------------------------------
396// Self Collision
397// ---------------------------------------------------------------------------
398
399/// Candidate self-collision pair between two vertices.
400#[derive(Debug, Clone)]
401pub struct SelfCollisionPair {
402    /// Index of vertex A.
403    pub a: usize,
404    /// Index of vertex B.
405    pub b: usize,
406    /// Squared distance between the two vertices.
407    pub sq_dist: f64,
408}
409
410/// Self-collision detection for cloth/soft mesh using a spatial hash and
411/// normal cone culling.
412pub struct SelfCollision {
413    /// Cell size for the spatial hash.
414    pub cell_size: f64,
415    /// Contact distance threshold.
416    pub contact_threshold: f64,
417    /// Normal cone filter.
418    pub cone_filter: NormalConeFilter,
419    hash: HashMap<(i64, i64, i64), Vec<usize>>,
420}
421
422impl SelfCollision {
423    /// Construct with cell size, contact threshold, and cone half-angle.
424    pub fn new(cell_size: f64, contact_threshold: f64, cone_half_angle: f64) -> Self {
425        SelfCollision {
426            cell_size,
427            contact_threshold,
428            cone_filter: NormalConeFilter::new(cone_half_angle),
429            hash: HashMap::new(),
430        }
431    }
432
433    fn cell_of(&self, p: [f64; 3]) -> (i64, i64, i64) {
434        (
435            (p[0] / self.cell_size).floor() as i64,
436            (p[1] / self.cell_size).floor() as i64,
437            (p[2] / self.cell_size).floor() as i64,
438        )
439    }
440
441    /// Update the spatial hash from the current vertex positions.
442    pub fn update(&mut self, vertices: &[[f64; 3]]) {
443        self.hash.clear();
444        for (i, &v) in vertices.iter().enumerate() {
445            let cell = self.cell_of(v);
446            self.hash.entry(cell).or_default().push(i);
447        }
448    }
449
450    /// Find candidate self-collision pairs, optionally filtered by normal cone.
451    pub fn find_pairs(
452        &self,
453        vertices: &[[f64; 3]],
454        normals: Option<&[[f64; 3]]>,
455    ) -> Vec<SelfCollisionPair> {
456        let mut pairs = Vec::new();
457        let threshold_sq = self.contact_threshold * self.contact_threshold;
458
459        for (&cell, indices) in &self.hash {
460            // Neighbour cells including self
461            for di in -1i64..=1 {
462                for dj in -1i64..=1 {
463                    for dk in -1i64..=1 {
464                        let nc = (cell.0 + di, cell.1 + dj, cell.2 + dk);
465                        if let Some(nb_indices) = self.hash.get(&nc) {
466                            for &a in indices {
467                                for &b in nb_indices {
468                                    if b <= a {
469                                        continue;
470                                    }
471                                    // Normal cone culling
472                                    if let Some(norms) = normals
473                                        && !self.cone_filter.should_test(norms[a], norms[b])
474                                    {
475                                        continue;
476                                    }
477                                    let diff = sub3(vertices[a], vertices[b]);
478                                    let sq = dot3(diff, diff);
479                                    if sq < threshold_sq {
480                                        pairs.push(SelfCollisionPair { a, b, sq_dist: sq });
481                                    }
482                                }
483                            }
484                        }
485                    }
486                }
487            }
488        }
489        pairs
490    }
491}
492
493// ---------------------------------------------------------------------------
494// Cloth SDF Contact
495// ---------------------------------------------------------------------------
496
497/// A contact from cloth vertex vs a rigid body's signed distance field.
498#[derive(Debug, Clone)]
499pub struct SdfContact {
500    /// Index of the cloth vertex.
501    pub vertex_index: usize,
502    /// Signed distance value (negative = inside).
503    pub sdf_value: f64,
504    /// Gradient of the SDF at the contact point (contact normal).
505    pub gradient: [f64; 3],
506}
507
508/// Cloth collision against a signed distance field (rigid body SDF).
509///
510/// The SDF is sampled on a uniform grid; gradients are computed by central
511/// differences.
512pub struct ClothSdfContact {
513    /// Grid dimensions.
514    pub nx: usize,
515    /// Grid dimensions.
516    pub ny: usize,
517    /// Grid dimensions.
518    pub nz: usize,
519    /// Cell size.
520    pub dx: f64,
521    /// Origin of the grid.
522    pub origin: [f64; 3],
523    /// Flat SDF values, row-major (x fastest).
524    pub values: Vec<f64>,
525    /// Contact thickness threshold.
526    pub thickness: f64,
527}
528
529impl ClothSdfContact {
530    /// Construct from a pre-computed SDF grid.
531    pub fn new(
532        nx: usize,
533        ny: usize,
534        nz: usize,
535        dx: f64,
536        origin: [f64; 3],
537        values: Vec<f64>,
538        thickness: f64,
539    ) -> Self {
540        ClothSdfContact {
541            nx,
542            ny,
543            nz,
544            dx,
545            origin,
546            values,
547            thickness,
548        }
549    }
550
551    fn index(&self, ix: usize, iy: usize, iz: usize) -> usize {
552        ix + self.nx * (iy + self.ny * iz)
553    }
554
555    fn sample(&self, ix: usize, iy: usize, iz: usize) -> f64 {
556        if ix < self.nx && iy < self.ny && iz < self.nz {
557            self.values[self.index(ix, iy, iz)]
558        } else {
559            f64::MAX
560        }
561    }
562
563    /// Trilinearly interpolate SDF at world position `p`.
564    pub fn interpolate(&self, p: [f64; 3]) -> f64 {
565        let lp = sub3(p, self.origin);
566        let fx = lp[0] / self.dx;
567        let fy = lp[1] / self.dx;
568        let fz = lp[2] / self.dx;
569        let ix = fx.floor() as isize;
570        let iy = fy.floor() as isize;
571        let iz = fz.floor() as isize;
572        if ix < 0
573            || iy < 0
574            || iz < 0
575            || ix + 1 >= self.nx as isize
576            || iy + 1 >= self.ny as isize
577            || iz + 1 >= self.nz as isize
578        {
579            return f64::MAX;
580        }
581        let (ix, iy, iz) = (ix as usize, iy as usize, iz as usize);
582        let tx = fx - ix as f64;
583        let ty = fy - iy as f64;
584        let tz = fz - iz as f64;
585        let v000 = self.sample(ix, iy, iz);
586        let v100 = self.sample(ix + 1, iy, iz);
587        let v010 = self.sample(ix, iy + 1, iz);
588        let v110 = self.sample(ix + 1, iy + 1, iz);
589        let v001 = self.sample(ix, iy, iz + 1);
590        let v101 = self.sample(ix + 1, iy, iz + 1);
591        let v011 = self.sample(ix, iy + 1, iz + 1);
592        let v111 = self.sample(ix + 1, iy + 1, iz + 1);
593        let lerp = |a: f64, b: f64, t: f64| a + (b - a) * t;
594        let c00 = lerp(v000, v100, tx);
595        let c10 = lerp(v010, v110, tx);
596        let c01 = lerp(v001, v101, tx);
597        let c11 = lerp(v011, v111, tx);
598        let c0 = lerp(c00, c10, ty);
599        let c1 = lerp(c01, c11, ty);
600        lerp(c0, c1, tz)
601    }
602
603    /// Compute the SDF gradient at world position `p` using central differences.
604    pub fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
605        let h = self.dx * 0.5;
606        let gx = (self.interpolate([p[0] + h, p[1], p[2]])
607            - self.interpolate([p[0] - h, p[1], p[2]]))
608            / (2.0 * h);
609        let gy = (self.interpolate([p[0], p[1] + h, p[2]])
610            - self.interpolate([p[0], p[1] - h, p[2]]))
611            / (2.0 * h);
612        let gz = (self.interpolate([p[0], p[1], p[2] + h])
613            - self.interpolate([p[0], p[1], p[2] - h]))
614            / (2.0 * h);
615        normalize3([gx, gy, gz])
616    }
617
618    /// Test all cloth vertices against the SDF.
619    pub fn test_vertices(&self, vertices: &[[f64; 3]]) -> Vec<SdfContact> {
620        vertices
621            .iter()
622            .enumerate()
623            .filter_map(|(i, &v)| {
624                let d = self.interpolate(v);
625                if d < self.thickness {
626                    Some(SdfContact {
627                        vertex_index: i,
628                        sdf_value: d,
629                        gradient: self.gradient(v),
630                    })
631                } else {
632                    None
633                }
634            })
635            .collect()
636    }
637}
638
639// ---------------------------------------------------------------------------
640// Edge-Edge Contact
641// ---------------------------------------------------------------------------
642
643/// Contact result for edge-edge (line-segment vs line-segment) collision.
644#[derive(Debug, Clone)]
645pub struct EdgeEdgeResult {
646    /// Index of edge A (in edge list).
647    pub edge_a: usize,
648    /// Index of edge B (in edge list).
649    pub edge_b: usize,
650    /// Closest distance between the edges.
651    pub distance: f64,
652    /// Parameter on edge A of the closest point.
653    pub s: f64,
654    /// Parameter on edge B of the closest point.
655    pub t: f64,
656    /// Contact normal (from B to A).
657    pub normal: [f64; 3],
658}
659
660/// Edge-edge contact detector for cloth edge collisions.
661///
662/// Each edge is defined by two vertex indices.
663pub struct EdgeEdgeContact {
664    /// Contact distance threshold.
665    pub threshold: f64,
666}
667
668impl EdgeEdgeContact {
669    /// Construct with a contact threshold.
670    pub fn new(threshold: f64) -> Self {
671        EdgeEdgeContact { threshold }
672    }
673
674    /// Test a single edge pair.
675    pub fn test_pair(
676        &self,
677        ei_a: usize,
678        p_a: [f64; 3],
679        d_a: [f64; 3],
680        ei_b: usize,
681        p_b: [f64; 3],
682        d_b: [f64; 3],
683    ) -> Option<EdgeEdgeResult> {
684        let (sq_dist, s, t) = edge_edge_distance(p_a, d_a, p_b, d_b);
685        let dist = sq_dist.sqrt();
686        if dist < self.threshold {
687            let ca = add3(p_a, scale3(d_a, s));
688            let cb = add3(p_b, scale3(d_b, t));
689            let diff = sub3(ca, cb);
690            let normal = if dist > 1e-14 {
691                scale3(diff, 1.0 / dist)
692            } else {
693                normalize3(cross3(d_a, d_b))
694            };
695            Some(EdgeEdgeResult {
696                edge_a: ei_a,
697                edge_b: ei_b,
698                distance: dist,
699                s,
700                t,
701                normal,
702            })
703        } else {
704            None
705        }
706    }
707
708    /// Test all edge pairs between two edge lists.
709    pub fn test_all(
710        &self,
711        edges_a: &[(usize, usize)],
712        verts_a: &[[f64; 3]],
713        edges_b: &[(usize, usize)],
714        verts_b: &[[f64; 3]],
715    ) -> Vec<EdgeEdgeResult> {
716        let mut results = Vec::new();
717        for (ia, &(a0, a1)) in edges_a.iter().enumerate() {
718            let pa = verts_a[a0];
719            let da = sub3(verts_a[a1], pa);
720            for (ib, &(b0, b1)) in edges_b.iter().enumerate() {
721                let pb = verts_b[b0];
722                let db = sub3(verts_b[b1], pb);
723                if let Some(r) = self.test_pair(ia, pa, da, ib, pb, db) {
724                    results.push(r);
725                }
726            }
727        }
728        results
729    }
730}
731
732// ---------------------------------------------------------------------------
733// CCD Deformable
734// ---------------------------------------------------------------------------
735
736/// Continuous collision result for a deformable mesh vertex trajectory.
737#[derive(Debug, Clone)]
738pub struct CcdDeformableResult {
739    /// Vertex index.
740    pub vertex_index: usize,
741    /// Time of impact in \[0, 1\].
742    pub toi: f64,
743    /// Contact normal at impact.
744    pub normal: [f64; 3],
745}
746
747/// Continuous collision detection for a deformable mesh.
748///
749/// Uses per-vertex linear trajectory CCD and edge-edge CCD.
750pub struct CcdDeformable {
751    /// Contact radius for vertex-vertex CCD.
752    pub vertex_radius: f64,
753    /// Contact radius for edge-edge CCD.
754    pub edge_radius: f64,
755}
756
757impl CcdDeformable {
758    /// Construct with per-feature radii.
759    pub fn new(vertex_radius: f64, edge_radius: f64) -> Self {
760        CcdDeformable {
761            vertex_radius,
762            edge_radius,
763        }
764    }
765
766    /// Vertex-vertex CCD between two meshes over a timestep.
767    /// `verts0_a/b` are start positions, `verts1_a/b` are end positions.
768    pub fn vertex_vertex_ccd(
769        &self,
770        verts0_a: &[[f64; 3]],
771        verts1_a: &[[f64; 3]],
772        verts0_b: &[[f64; 3]],
773        verts1_b: &[[f64; 3]],
774    ) -> Vec<CcdDeformableResult> {
775        let mut results = Vec::new();
776        for (ia, (&p0, &p1)) in verts0_a.iter().zip(verts1_a.iter()).enumerate() {
777            for (&q0, &q1) in verts0_b.iter().zip(verts1_b.iter()) {
778                if let Some(toi) = pairwise_ccd_dt(p0, p1, q0, q1, self.vertex_radius) {
779                    let pt = add3(p0, scale3(sub3(p1, p0), toi));
780                    let qt = add3(q0, scale3(sub3(q1, q0), toi));
781                    let diff = sub3(pt, qt);
782                    let normal = normalize3(diff);
783                    results.push(CcdDeformableResult {
784                        vertex_index: ia,
785                        toi,
786                        normal,
787                    });
788                }
789            }
790        }
791        results
792    }
793
794    /// Edge-edge CCD between two edge sets over a timestep.
795    pub fn edge_edge_ccd(
796        &self,
797        edges_a: &[(usize, usize)],
798        verts0_a: &[[f64; 3]],
799        verts1_a: &[[f64; 3]],
800        edges_b: &[(usize, usize)],
801        verts0_b: &[[f64; 3]],
802        verts1_b: &[[f64; 3]],
803    ) -> Vec<CcdDeformableResult> {
804        let mut results = Vec::new();
805        for (ia, &(a0, a1)) in edges_a.iter().enumerate() {
806            for &(b0, b1) in edges_b {
807                // Sample midpoints and test CCD as approximation
808                let pa0 = midpoint(verts0_a[a0], verts0_a[a1]);
809                let pa1 = midpoint(verts1_a[a0], verts1_a[a1]);
810                let pb0 = midpoint(verts0_b[b0], verts0_b[b1]);
811                let pb1 = midpoint(verts1_b[b0], verts1_b[b1]);
812                if let Some(toi) = pairwise_ccd_dt(pa0, pa1, pb0, pb1, self.edge_radius) {
813                    let pt = add3(pa0, scale3(sub3(pa1, pa0), toi));
814                    let qt = add3(pb0, scale3(sub3(pb1, pb0), toi));
815                    let normal = normalize3(sub3(pt, qt));
816                    results.push(CcdDeformableResult {
817                        vertex_index: ia,
818                        toi,
819                        normal,
820                    });
821                }
822            }
823        }
824        results
825    }
826}
827
828fn midpoint(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
829    scale3(add3(a, b), 0.5)
830}
831
832// ---------------------------------------------------------------------------
833// Primitive vs Deformable
834// ---------------------------------------------------------------------------
835
836/// Contact between a primitive (sphere/capsule/box) and a deformable vertex.
837#[derive(Debug, Clone)]
838pub struct PrimitiveDeformContact {
839    /// Vertex index.
840    pub vertex_index: usize,
841    /// Penetration depth (positive = overlapping).
842    pub depth: f64,
843    /// Contact normal pointing away from the primitive.
844    pub normal: [f64; 3],
845    /// Contact point on the primitive surface.
846    pub point: [f64; 3],
847}
848
849/// Sphere vs deformable mesh contact.
850pub struct PrimitiveVsDeformable {
851    /// Sphere centre.
852    pub sphere_center: [f64; 3],
853    /// Sphere radius.
854    pub sphere_radius: f64,
855}
856
857impl PrimitiveVsDeformable {
858    /// Construct a sphere vs deformable tester.
859    pub fn sphere(center: [f64; 3], radius: f64) -> Self {
860        PrimitiveVsDeformable {
861            sphere_center: center,
862            sphere_radius: radius,
863        }
864    }
865
866    /// Test all vertices against the sphere.
867    pub fn query_sphere(&self, vertices: &[[f64; 3]]) -> Vec<PrimitiveDeformContact> {
868        vertices
869            .iter()
870            .enumerate()
871            .filter_map(|(i, &v)| {
872                let diff = sub3(v, self.sphere_center);
873                let dist = len3(diff);
874                if dist < self.sphere_radius {
875                    let depth = self.sphere_radius - dist;
876                    let normal = if dist > 1e-14 {
877                        scale3(diff, 1.0 / dist)
878                    } else {
879                        [0.0, 1.0, 0.0]
880                    };
881                    let point = add3(self.sphere_center, scale3(normal, self.sphere_radius));
882                    Some(PrimitiveDeformContact {
883                        vertex_index: i,
884                        depth,
885                        normal,
886                        point,
887                    })
888                } else {
889                    None
890                }
891            })
892            .collect()
893    }
894
895    /// Test all vertices against an axis-aligned box.
896    pub fn query_box(
897        vertices: &[[f64; 3]],
898        box_min: [f64; 3],
899        box_max: [f64; 3],
900    ) -> Vec<PrimitiveDeformContact> {
901        vertices
902            .iter()
903            .enumerate()
904            .filter_map(|(i, &v)| {
905                // Closest point on box
906                let cx = v[0].clamp(box_min[0], box_max[0]);
907                let cy = v[1].clamp(box_min[1], box_max[1]);
908                let cz = v[2].clamp(box_min[2], box_max[2]);
909                let closest = [cx, cy, cz];
910                let diff = sub3(v, closest);
911                let dist = len3(diff);
912                if dist < 1e-6 {
913                    // Vertex inside box
914                    let normal = [0.0, 1.0, 0.0];
915                    Some(PrimitiveDeformContact {
916                        vertex_index: i,
917                        depth: 1e-6,
918                        normal,
919                        point: closest,
920                    })
921                } else {
922                    None
923                }
924            })
925            .collect()
926    }
927
928    /// Test all vertices against a capsule (segment + radius).
929    pub fn query_capsule(
930        vertices: &[[f64; 3]],
931        cap_a: [f64; 3],
932        cap_b: [f64; 3],
933        radius: f64,
934    ) -> Vec<PrimitiveDeformContact> {
935        let d = sub3(cap_b, cap_a);
936        let len_sq = dot3(d, d);
937        vertices
938            .iter()
939            .enumerate()
940            .filter_map(|(i, &v)| {
941                let t = if len_sq < 1e-14 {
942                    0.0
943                } else {
944                    (dot3(sub3(v, cap_a), d) / len_sq).clamp(0.0, 1.0)
945                };
946                let closest = add3(cap_a, scale3(d, t));
947                let diff = sub3(v, closest);
948                let dist = len3(diff);
949                if dist < radius {
950                    let depth = radius - dist;
951                    let normal = if dist > 1e-14 {
952                        scale3(diff, 1.0 / dist)
953                    } else {
954                        [0.0, 1.0, 0.0]
955                    };
956                    let point = add3(closest, scale3(normal, radius));
957                    Some(PrimitiveDeformContact {
958                        vertex_index: i,
959                        depth,
960                        normal,
961                        point,
962                    })
963                } else {
964                    None
965                }
966            })
967            .collect()
968    }
969}
970
971// ---------------------------------------------------------------------------
972// Contact Response
973// ---------------------------------------------------------------------------
974
975/// Position correction impulse for a deformable vertex in contact.
976#[derive(Debug, Clone)]
977pub struct VertexImpulse {
978    /// Index of the vertex.
979    pub vertex_index: usize,
980    /// Position correction vector (apply to vertex position).
981    pub correction: [f64; 3],
982    /// Velocity impulse to apply.
983    pub impulse: [f64; 3],
984}
985
986/// Contact response system: compute position corrections and velocity impulses
987/// for deformable vs rigid contacts.
988pub struct ContactResponse {
989    /// Restitution coefficient (0 = fully inelastic, 1 = elastic).
990    pub restitution: f64,
991    /// Friction coefficient.
992    pub friction: f64,
993}
994
995impl ContactResponse {
996    /// Construct with restitution and friction coefficients.
997    pub fn new(restitution: f64, friction: f64) -> Self {
998        ContactResponse {
999            restitution,
1000            friction,
1001        }
1002    }
1003
1004    /// Compute vertex impulses for deformable vs rigid contacts.
1005    pub fn solve_deform_rigid(
1006        &self,
1007        contacts: &[DeformContact],
1008        velocities: &[[f64; 3]],
1009        masses: &[f64],
1010    ) -> Vec<VertexImpulse> {
1011        contacts
1012            .iter()
1013            .map(|c| {
1014                let vi = c.vertex_index;
1015                let v = velocities[vi];
1016                let m = masses[vi];
1017                let vn = dot3(v, c.normal);
1018                // Correction: push vertex out of penetration
1019                let correction = scale3(c.normal, c.depth);
1020                // Impulse: reflect normal component
1021                let j_n = -(1.0 + self.restitution) * vn * m;
1022                let impulse_n = scale3(c.normal, j_n);
1023                // Tangential friction (simple Coulomb)
1024                let v_tang = sub3(v, scale3(c.normal, vn));
1025                let v_tang_len = len3(v_tang);
1026                let impulse_t = if v_tang_len > 1e-14 {
1027                    scale3(v_tang, -self.friction * j_n.abs() / v_tang_len)
1028                } else {
1029                    [0.0; 3]
1030                };
1031                let impulse = add3(impulse_n, impulse_t);
1032                VertexImpulse {
1033                    vertex_index: vi,
1034                    correction,
1035                    impulse,
1036                }
1037            })
1038            .collect()
1039    }
1040
1041    /// Compute vertex impulses for edge-edge contacts.
1042    pub fn solve_edge_edge(
1043        &self,
1044        results: &[EdgeEdgeResult],
1045        velocities: &[[f64; 3]],
1046        edges_a: &[(usize, usize)],
1047        masses: &[f64],
1048    ) -> Vec<VertexImpulse> {
1049        let mut impulses = Vec::new();
1050        for r in results {
1051            let (a0, a1) = edges_a[r.edge_a];
1052            for &vi in &[a0, a1] {
1053                let v = velocities[vi];
1054                let m = masses[vi];
1055                let vn = dot3(v, r.normal);
1056                let depth = (self.friction - r.distance).max(0.0);
1057                let correction = scale3(r.normal, depth);
1058                let j = -(1.0 + self.restitution) * vn * m;
1059                let impulse = scale3(r.normal, j);
1060                impulses.push(VertexImpulse {
1061                    vertex_index: vi,
1062                    correction,
1063                    impulse,
1064                });
1065            }
1066        }
1067        impulses
1068    }
1069}
1070
1071// ---------------------------------------------------------------------------
1072// Spatial Hash for Deformable
1073// ---------------------------------------------------------------------------
1074
1075/// Dynamic spatial hash for deformable mesh particles, supporting fast update.
1076pub struct SpatialHashDeformable {
1077    /// Cell size.
1078    pub cell_size: f64,
1079    cells: HashMap<(i64, i64, i64), Vec<usize>>,
1080}
1081
1082impl SpatialHashDeformable {
1083    /// Construct with a cell size.
1084    pub fn new(cell_size: f64) -> Self {
1085        SpatialHashDeformable {
1086            cell_size,
1087            cells: HashMap::new(),
1088        }
1089    }
1090
1091    fn cell_of(&self, p: [f64; 3]) -> (i64, i64, i64) {
1092        (
1093            (p[0] / self.cell_size).floor() as i64,
1094            (p[1] / self.cell_size).floor() as i64,
1095            (p[2] / self.cell_size).floor() as i64,
1096        )
1097    }
1098
1099    /// Update positions of all particles.
1100    pub fn update(&mut self, positions: &[[f64; 3]]) {
1101        self.cells.clear();
1102        for (i, &p) in positions.iter().enumerate() {
1103            let cell = self.cell_of(p);
1104            self.cells.entry(cell).or_default().push(i);
1105        }
1106    }
1107
1108    /// Query all particles within `radius` of `point`.
1109    pub fn query_radius(&self, point: [f64; 3], radius: f64) -> Vec<usize> {
1110        let r_cells = (radius / self.cell_size).ceil() as i64 + 1;
1111        let center_cell = self.cell_of(point);
1112        let mut result = Vec::new();
1113        for di in -r_cells..=r_cells {
1114            for dj in -r_cells..=r_cells {
1115                for dk in -r_cells..=r_cells {
1116                    let nc = (center_cell.0 + di, center_cell.1 + dj, center_cell.2 + dk);
1117                    if let Some(indices) = self.cells.get(&nc) {
1118                        for &idx in indices {
1119                            result.push(idx);
1120                        }
1121                    }
1122                }
1123            }
1124        }
1125        // Filter to actual radius
1126        result
1127            .into_iter()
1128            .filter(|_| true) // positions not available here; caller filters
1129            .collect()
1130    }
1131
1132    /// Return all particles near `point`, filtered by actual distance.
1133    pub fn query_radius_with_positions(
1134        &self,
1135        point: [f64; 3],
1136        radius: f64,
1137        positions: &[[f64; 3]],
1138    ) -> Vec<usize> {
1139        let candidates = self.query_radius(point, radius);
1140        let r2 = radius * radius;
1141        candidates
1142            .into_iter()
1143            .filter(|&i| dot3(sub3(positions[i], point), sub3(positions[i], point)) <= r2)
1144            .collect()
1145    }
1146}
1147
1148// ---------------------------------------------------------------------------
1149// Inflatable Contact
1150// ---------------------------------------------------------------------------
1151
1152/// Contact result for an inflatable (balloon) vs environment collision.
1153#[derive(Debug, Clone)]
1154pub struct InflatableContact {
1155    /// Index of the balloon triangle.
1156    pub tri_index: usize,
1157    /// Penetration depth.
1158    pub depth: f64,
1159    /// Contact normal.
1160    pub normal: [f64; 3],
1161    /// Contact point.
1162    pub point: [f64; 3],
1163    /// Pressure-based stiffness at this contact.
1164    pub stiffness: f64,
1165}
1166
1167/// Balloon vs environment collision using sphere sweeps per triangle and
1168/// pressure-based contact stiffness.
1169pub struct InflatableContactDetector {
1170    /// Internal pressure.
1171    pub pressure: f64,
1172    /// Thickness of balloon membrane.
1173    pub thickness: f64,
1174}
1175
1176impl InflatableContactDetector {
1177    /// Construct with internal pressure and membrane thickness.
1178    pub fn new(pressure: f64, thickness: f64) -> Self {
1179        InflatableContactDetector {
1180            pressure,
1181            thickness,
1182        }
1183    }
1184
1185    /// Compute contacts between balloon triangles and rigid triangles.
1186    pub fn query(
1187        &self,
1188        balloon_tris: &[[[f64; 3]; 3]],
1189        rigid_tris: &[[[f64; 3]; 3]],
1190    ) -> Vec<InflatableContact> {
1191        let mut contacts = Vec::new();
1192        for (bi, btri) in balloon_tris.iter().enumerate() {
1193            let bc = tri_centroid(*btri);
1194            for rtri in rigid_tris {
1195                let (sq_dist, bary) = vertex_triangle_distance(bc, rtri[0], rtri[1], rtri[2]);
1196                let dist = sq_dist.sqrt();
1197                if dist < self.thickness {
1198                    let closest = add3(
1199                        add3(scale3(rtri[0], bary[0]), scale3(rtri[1], bary[1])),
1200                        scale3(rtri[2], bary[2]),
1201                    );
1202                    let ab = sub3(rtri[1], rtri[0]);
1203                    let ac = sub3(rtri[2], rtri[0]);
1204                    let normal = normalize3(cross3(ab, ac));
1205                    let depth = self.thickness - dist;
1206                    // Pressure-based stiffness proportional to pressure and
1207                    // inversely to balloon area
1208                    let area = tri_area(*btri);
1209                    let stiffness = self.pressure * area;
1210                    contacts.push(InflatableContact {
1211                        tri_index: bi,
1212                        depth,
1213                        normal,
1214                        point: closest,
1215                        stiffness,
1216                    });
1217                }
1218            }
1219        }
1220        contacts
1221    }
1222}
1223
1224fn tri_centroid(tri: [[f64; 3]; 3]) -> [f64; 3] {
1225    scale3(add3(add3(tri[0], tri[1]), tri[2]), 1.0 / 3.0)
1226}
1227
1228fn tri_area(tri: [[f64; 3]; 3]) -> f64 {
1229    let ab = sub3(tri[1], tri[0]);
1230    let ac = sub3(tri[2], tri[0]);
1231    len3(cross3(ab, ac)) * 0.5
1232}
1233
1234// ---------------------------------------------------------------------------
1235// Tests
1236// ---------------------------------------------------------------------------
1237
1238#[cfg(test)]
1239mod tests {
1240    use super::*;
1241
1242    // --- vertex_triangle_distance ---
1243
1244    #[test]
1245    fn vtd_point_at_vertex() {
1246        let a = [0.0, 0.0, 0.0];
1247        let b = [1.0, 0.0, 0.0];
1248        let c = [0.0, 1.0, 0.0];
1249        let (sq, bary) = vertex_triangle_distance(a, a, b, c);
1250        assert!(sq < 1e-12, "sq={sq}");
1251        assert!((bary[0] - 1.0).abs() < 1e-9);
1252    }
1253
1254    #[test]
1255    fn vtd_point_above_triangle() {
1256        let a = [0.0, 0.0, 0.0];
1257        let b = [1.0, 0.0, 0.0];
1258        let c = [0.0, 1.0, 0.0];
1259        let p = [0.25, 0.25, 1.0];
1260        let (sq, _) = vertex_triangle_distance(p, a, b, c);
1261        assert!((sq.sqrt() - 1.0).abs() < 1e-9, "dist={}", sq.sqrt());
1262    }
1263
1264    #[test]
1265    fn vtd_point_inside_triangle_projection() {
1266        let a = [0.0, 0.0, 0.0];
1267        let b = [2.0, 0.0, 0.0];
1268        let c = [0.0, 2.0, 0.0];
1269        let p = [0.5, 0.5, 0.0];
1270        let (sq, _) = vertex_triangle_distance(p, a, b, c);
1271        assert!(sq < 1e-12, "sq={sq}");
1272    }
1273
1274    #[test]
1275    fn vtd_point_outside_triangle() {
1276        let a = [0.0, 0.0, 0.0];
1277        let b = [1.0, 0.0, 0.0];
1278        let c = [0.0, 1.0, 0.0];
1279        let p = [2.0, 0.0, 0.0];
1280        let (sq, _) = vertex_triangle_distance(p, a, b, c);
1281        assert!((sq.sqrt() - 1.0).abs() < 1e-9, "dist={}", sq.sqrt());
1282    }
1283
1284    // --- edge_edge_distance ---
1285
1286    #[test]
1287    fn eed_parallel_edges() {
1288        let p = [0.0, 0.0, 0.0];
1289        let d = [1.0, 0.0, 0.0];
1290        let q = [0.0, 1.0, 0.0];
1291        let e = [1.0, 0.0, 0.0];
1292        let (sq, _, _) = edge_edge_distance(p, d, q, e);
1293        assert!((sq.sqrt() - 1.0).abs() < 1e-9, "dist={}", sq.sqrt());
1294    }
1295
1296    #[test]
1297    fn eed_perpendicular_crossing() {
1298        let p = [0.0, 0.0, 0.0];
1299        let d = [1.0, 0.0, 0.0];
1300        let q = [0.5, -1.0, 0.0];
1301        let e = [0.0, 2.0, 0.0];
1302        let (sq, s, t) = edge_edge_distance(p, d, q, e);
1303        assert!(sq < 1e-12, "sq={sq}");
1304        assert!((s - 0.5).abs() < 1e-9, "s={s}");
1305        assert!((t - 0.5).abs() < 1e-9, "t={t}");
1306    }
1307
1308    #[test]
1309    fn eed_skew_edges() {
1310        let p = [0.0, 0.0, 0.0];
1311        let d = [1.0, 0.0, 0.0];
1312        let q = [0.5, 1.0, 1.0];
1313        let e = [0.0, 0.0, -1.0];
1314        let (sq, _, _) = edge_edge_distance(p, d, q, e);
1315        assert!((sq.sqrt() - 1.0).abs() < 1e-9, "dist={}", sq.sqrt());
1316    }
1317
1318    // --- normal_cone_angle ---
1319
1320    #[test]
1321    fn nca_identical_normals() {
1322        let n = [0.0, 1.0, 0.0];
1323        let normals = vec![n, n, n];
1324        let cos = normal_cone_angle(&normals);
1325        assert!((cos - 1.0).abs() < 1e-9, "cos={cos}");
1326    }
1327
1328    #[test]
1329    fn nca_empty() {
1330        let cos = normal_cone_angle(&[]);
1331        assert_eq!(cos, 0.0);
1332    }
1333
1334    #[test]
1335    fn nca_perpendicular() {
1336        let normals = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1337        let cos = normal_cone_angle(&normals);
1338        assert!(cos < 1.0);
1339    }
1340
1341    // --- pairwise_ccd_dt ---
1342
1343    #[test]
1344    fn ccd_dt_approaching() {
1345        let p0 = [0.0, 0.0, 0.0];
1346        let p1 = [0.0, 0.0, 0.0];
1347        let q0 = [2.0, 0.0, 0.0];
1348        let q1 = [0.5, 0.0, 0.0];
1349        let r = 0.6;
1350        let toi = pairwise_ccd_dt(p0, p1, q0, q1, r);
1351        assert!(toi.is_some(), "expected impact");
1352        let t = toi.unwrap();
1353        assert!((0.0..=1.0).contains(&t), "t={t}");
1354    }
1355
1356    #[test]
1357    fn ccd_dt_separating() {
1358        let p0 = [0.0, 0.0, 0.0];
1359        let p1 = [0.0, 0.0, 0.0];
1360        let q0 = [1.0, 0.0, 0.0];
1361        let q1 = [5.0, 0.0, 0.0];
1362        let toi = pairwise_ccd_dt(p0, p1, q0, q1, 0.1);
1363        assert!(toi.is_none());
1364    }
1365
1366    #[test]
1367    fn ccd_dt_already_overlapping() {
1368        let p0 = [0.0, 0.0, 0.0];
1369        let p1 = [0.0, 0.0, 0.0];
1370        let q0 = [0.05, 0.0, 0.0];
1371        let q1 = [0.05, 0.0, 0.0];
1372        let toi = pairwise_ccd_dt(p0, p1, q0, q1, 1.0);
1373        assert!(toi.is_some());
1374        assert_eq!(toi.unwrap(), 0.0);
1375    }
1376
1377    // --- DeformableVsBvh ---
1378
1379    #[test]
1380    fn dvb_no_contact() {
1381        let tris = vec![[[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]];
1382        let det = DeformableVsBvh::new(tris, 0.05);
1383        let verts = vec![[0.5f64, 0.5, 5.0]];
1384        let contacts = det.query(&verts);
1385        assert!(contacts.is_empty());
1386    }
1387
1388    #[test]
1389    fn dvb_contact_detected() {
1390        let tris = vec![[[0.0f64, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]]];
1391        let det = DeformableVsBvh::new(tris, 0.1);
1392        let verts = vec![[0.5f64, 0.5, 0.05]];
1393        let contacts = det.query(&verts);
1394        assert!(!contacts.is_empty(), "expected contact");
1395        assert_eq!(contacts[0].vertex_index, 0);
1396        assert!(contacts[0].depth > 0.0);
1397    }
1398
1399    // --- NormalConeFilter ---
1400
1401    #[test]
1402    fn ncf_opposite_normals_always_tested() {
1403        // With half_angle = 0.1, cos_threshold ≈ 0.995
1404        // dot([0,1,0],[0,-1,0]) = -1.0 < 0.995 → should_test = true
1405        let filter = NormalConeFilter::new(0.1);
1406        assert!(filter.should_test([0.0, 1.0, 0.0], [0.0, -1.0, 0.0]));
1407    }
1408
1409    #[test]
1410    fn ncf_same_normal_culled_small_threshold() {
1411        // With very small angle, cos_threshold ≈ 1.0
1412        // dot([0,1,0],[0,1,0]) = 1.0 which is NOT < threshold ≈ 1.0, so culled
1413        let filter = NormalConeFilter::new(0.0001);
1414        // cos(0.0001) ≈ 0.99999999, dot = 1.0 is NOT < 0.99999999 → culled
1415        assert!(!filter.should_test([0.0, 1.0, 0.0], [0.0, 1.0, 0.0]));
1416    }
1417
1418    // --- SelfCollision ---
1419
1420    #[test]
1421    fn self_collision_finds_close_pair() {
1422        let mut sc = SelfCollision::new(1.0, 0.5, std::f64::consts::PI);
1423        let verts = vec![[0.0f64, 0.0, 0.0], [0.2, 0.0, 0.0], [10.0, 0.0, 0.0]];
1424        sc.update(&verts);
1425        let pairs = sc.find_pairs(&verts, None);
1426        assert!(!pairs.is_empty(), "expected close pair");
1427    }
1428
1429    #[test]
1430    fn self_collision_no_pair_far_apart() {
1431        let mut sc = SelfCollision::new(1.0, 0.1, std::f64::consts::PI);
1432        let verts = vec![[0.0f64, 0.0, 0.0], [5.0, 0.0, 0.0]];
1433        sc.update(&verts);
1434        let pairs = sc.find_pairs(&verts, None);
1435        assert!(pairs.is_empty());
1436    }
1437
1438    // --- ClothSdfContact ---
1439
1440    fn make_sdf_sphere(
1441        center: [f64; 3],
1442        radius: f64,
1443        nx: usize,
1444        ny: usize,
1445        nz: usize,
1446        dx: f64,
1447        origin: [f64; 3],
1448    ) -> ClothSdfContact {
1449        let mut values = Vec::with_capacity(nx * ny * nz);
1450        for iz in 0..nz {
1451            for iy in 0..ny {
1452                for ix in 0..nx {
1453                    let p = [
1454                        origin[0] + ix as f64 * dx,
1455                        origin[1] + iy as f64 * dx,
1456                        origin[2] + iz as f64 * dx,
1457                    ];
1458                    let d = len3(sub3(p, center)) - radius;
1459                    values.push(d);
1460                }
1461            }
1462        }
1463        ClothSdfContact::new(nx, ny, nz, dx, origin, values, 0.1)
1464    }
1465
1466    #[test]
1467    fn cloth_sdf_contact_inside_sphere() {
1468        let sdf = make_sdf_sphere([2.5, 2.5, 2.5], 1.0, 6, 6, 6, 1.0, [0.0, 0.0, 0.0]);
1469        let verts = vec![[2.5f64, 2.5, 2.5]]; // inside sphere
1470        let contacts = sdf.test_vertices(&verts);
1471        assert!(!contacts.is_empty(), "vertex inside sphere should contact");
1472    }
1473
1474    #[test]
1475    fn cloth_sdf_contact_outside_sphere() {
1476        let sdf = make_sdf_sphere([2.5, 2.5, 2.5], 1.0, 6, 6, 6, 1.0, [0.0, 0.0, 0.0]);
1477        let verts = vec![[2.5f64, 2.5, 10.0]]; // far outside
1478        let contacts = sdf.test_vertices(&verts);
1479        assert!(
1480            contacts.is_empty(),
1481            "vertex far outside sphere should not contact"
1482        );
1483    }
1484
1485    // --- EdgeEdgeContact ---
1486
1487    #[test]
1488    fn eec_crossing_edges() {
1489        let det = EdgeEdgeContact::new(0.1);
1490        let pa = [0.0, 0.0, 0.0];
1491        let da = [2.0, 0.0, 0.0];
1492        let pb = [1.0, -1.0, 0.0];
1493        let db = [0.0, 2.0, 0.0];
1494        let result = det.test_pair(0, pa, da, 1, pb, db);
1495        assert!(result.is_some(), "crossing edges should contact");
1496    }
1497
1498    #[test]
1499    fn eec_distant_edges() {
1500        let det = EdgeEdgeContact::new(0.1);
1501        let pa = [0.0, 0.0, 0.0];
1502        let da = [1.0, 0.0, 0.0];
1503        let pb = [0.0, 5.0, 0.0];
1504        let db = [1.0, 0.0, 0.0];
1505        let result = det.test_pair(0, pa, da, 1, pb, db);
1506        assert!(result.is_none(), "distant edges should not contact");
1507    }
1508
1509    #[test]
1510    fn eec_test_all_returns_multiple() {
1511        let det = EdgeEdgeContact::new(0.2);
1512        let verts_a = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1513        let edges_a = vec![(0, 1), (1, 2)];
1514        let verts_b = vec![[0.5f64, -0.1, 0.0], [0.5, 0.1, 0.0]];
1515        let edges_b = vec![(0, 1)];
1516        let results = det.test_all(&edges_a, &verts_a, &edges_b, &verts_b);
1517        assert!(!results.is_empty());
1518    }
1519
1520    // --- CcdDeformable ---
1521
1522    #[test]
1523    fn ccd_deformable_vertex_impact() {
1524        let ccd = CcdDeformable::new(0.1, 0.1);
1525        let verts0_a = vec![[0.0f64, 0.0, 0.0]];
1526        let verts1_a = vec![[0.0f64, 0.0, 0.0]];
1527        let verts0_b = vec![[0.5, 0.0, 0.0]];
1528        let verts1_b = vec![[0.05, 0.0, 0.0]];
1529        let results = ccd.vertex_vertex_ccd(&verts0_a, &verts1_a, &verts0_b, &verts1_b);
1530        assert!(!results.is_empty(), "expected CCD impact");
1531    }
1532
1533    #[test]
1534    fn ccd_deformable_no_impact_separating() {
1535        let ccd = CcdDeformable::new(0.05, 0.05);
1536        let verts0_a = vec![[0.0f64, 0.0, 0.0]];
1537        let verts1_a = vec![[0.0f64, 0.0, 0.0]];
1538        let verts0_b = vec![[1.0, 0.0, 0.0]];
1539        let verts1_b = vec![[5.0, 0.0, 0.0]];
1540        let results = ccd.vertex_vertex_ccd(&verts0_a, &verts1_a, &verts0_b, &verts1_b);
1541        assert!(results.is_empty());
1542    }
1543
1544    // --- PrimitiveVsDeformable ---
1545
1546    #[test]
1547    fn pvd_sphere_contact() {
1548        let det = PrimitiveVsDeformable::sphere([0.0, 0.0, 0.0], 1.0);
1549        let verts = vec![[0.5f64, 0.0, 0.0], [2.0, 0.0, 0.0]];
1550        let contacts = det.query_sphere(&verts);
1551        assert_eq!(contacts.len(), 1);
1552        assert_eq!(contacts[0].vertex_index, 0);
1553        assert!(contacts[0].depth > 0.0);
1554    }
1555
1556    #[test]
1557    fn pvd_sphere_no_contact() {
1558        let det = PrimitiveVsDeformable::sphere([0.0, 0.0, 0.0], 0.5);
1559        let verts = vec![[2.0f64, 0.0, 0.0]];
1560        let contacts = det.query_sphere(&verts);
1561        assert!(contacts.is_empty());
1562    }
1563
1564    #[test]
1565    fn pvd_capsule_contact() {
1566        let verts = vec![[0.5f64, 0.5, 0.0]];
1567        let contacts =
1568            PrimitiveVsDeformable::query_capsule(&verts, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 1.0);
1569        assert!(!contacts.is_empty());
1570    }
1571
1572    #[test]
1573    fn pvd_box_no_contact() {
1574        let verts = vec![[5.0f64, 0.0, 0.0]];
1575        let contacts = PrimitiveVsDeformable::query_box(&verts, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1576        assert!(contacts.is_empty());
1577    }
1578
1579    // --- ContactResponse ---
1580
1581    #[test]
1582    fn cr_solve_deform_rigid_positive_depth() {
1583        let cr = ContactResponse::new(0.0, 0.1);
1584        let contact = DeformContact {
1585            vertex_index: 0,
1586            tri_index: 0,
1587            depth: 0.02,
1588            normal: [0.0, 1.0, 0.0],
1589            point_on_rigid: [0.0, 0.0, 0.0],
1590        };
1591        let velocities = vec![[0.0, -1.0, 0.0]];
1592        let masses = vec![1.0];
1593        let impulses = cr.solve_deform_rigid(&[contact], &velocities, &masses);
1594        assert_eq!(impulses.len(), 1);
1595        assert!(impulses[0].correction[1] > 0.0);
1596    }
1597
1598    // --- SpatialHashDeformable ---
1599
1600    #[test]
1601    fn shd_query_finds_nearby() {
1602        let mut sh = SpatialHashDeformable::new(1.0);
1603        let positions = vec![[0.0f64, 0.0, 0.0], [0.3, 0.0, 0.0], [10.0, 0.0, 0.0]];
1604        sh.update(&positions);
1605        let nearby = sh.query_radius_with_positions([0.0, 0.0, 0.0], 0.5, &positions);
1606        assert!(nearby.contains(&0));
1607        assert!(nearby.contains(&1));
1608        assert!(!nearby.contains(&2));
1609    }
1610
1611    #[test]
1612    fn shd_empty_after_clear() {
1613        let mut sh = SpatialHashDeformable::new(1.0);
1614        let positions = vec![[0.0f64, 0.0, 0.0]];
1615        sh.update(&positions);
1616        sh.update(&[]);
1617        let nearby = sh.query_radius_with_positions([0.0, 0.0, 0.0], 1.0, &[]);
1618        assert!(nearby.is_empty());
1619    }
1620
1621    // --- InflatableContactDetector ---
1622
1623    #[test]
1624    fn icd_contact_detected() {
1625        let det = InflatableContactDetector::new(1.0, 0.2);
1626        let balloon = vec![[[0.0f64, 0.0, 0.1], [1.0, 0.0, 0.1], [0.0, 1.0, 0.1]]];
1627        let rigid = vec![[[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]];
1628        let contacts = det.query(&balloon, &rigid);
1629        assert!(
1630            !contacts.is_empty(),
1631            "balloon close to rigid should contact"
1632        );
1633    }
1634
1635    #[test]
1636    fn icd_no_contact_far_apart() {
1637        let det = InflatableContactDetector::new(1.0, 0.05);
1638        let balloon = vec![[[0.0f64, 0.0, 5.0], [1.0, 0.0, 5.0], [0.0, 1.0, 5.0]]];
1639        let rigid = vec![[[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]];
1640        let contacts = det.query(&balloon, &rigid);
1641        assert!(contacts.is_empty());
1642    }
1643
1644    // --- tri_centroid / tri_area helpers ---
1645
1646    #[test]
1647    fn tri_centroid_correct() {
1648        let tri = [[0.0f64, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]];
1649        let c = tri_centroid(tri);
1650        assert!((c[0] - 1.0).abs() < 1e-9);
1651        assert!((c[1] - 1.0).abs() < 1e-9);
1652    }
1653
1654    #[test]
1655    fn tri_area_unit() {
1656        let tri = [[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1657        let a = tri_area(tri);
1658        assert!((a - 0.5).abs() < 1e-9, "area={a}");
1659    }
1660
1661    // --- vector helpers ---
1662
1663    #[test]
1664    fn vec_helpers_dot_cross() {
1665        let a = [1.0f64, 0.0, 0.0];
1666        let b = [0.0f64, 1.0, 0.0];
1667        assert!((dot3(a, b) - 0.0).abs() < 1e-14);
1668        let c = cross3(a, b);
1669        assert!((c[2] - 1.0).abs() < 1e-14);
1670    }
1671
1672    #[test]
1673    fn vec_normalize() {
1674        let v = [3.0f64, 4.0, 0.0];
1675        let n = normalize3(v);
1676        assert!((len3(n) - 1.0).abs() < 1e-12);
1677    }
1678
1679    #[test]
1680    fn midpoint_test() {
1681        let a = [0.0f64, 0.0, 0.0];
1682        let b = [2.0f64, 0.0, 0.0];
1683        let m = midpoint(a, b);
1684        assert!((m[0] - 1.0).abs() < 1e-14);
1685    }
1686
1687    #[test]
1688    fn sdf_interpolate_outside_grid_returns_max() {
1689        let sdf = ClothSdfContact::new(3, 3, 3, 1.0, [0.0, 0.0, 0.0], vec![1.0; 27], 0.1);
1690        let val = sdf.interpolate([100.0, 100.0, 100.0]);
1691        assert_eq!(val, f64::MAX);
1692    }
1693
1694    #[test]
1695    fn sdf_gradient_nonzero_in_sphere() {
1696        let sdf = make_sdf_sphere([2.5, 2.5, 2.5], 1.0, 6, 6, 6, 1.0, [0.0, 0.0, 0.0]);
1697        let g = sdf.gradient([2.5, 2.5, 1.8]);
1698        let gl = len3(g);
1699        assert!(gl > 0.5, "gradient should be nonzero: len={gl}");
1700    }
1701
1702    #[test]
1703    fn self_collision_update_and_query() {
1704        let mut sc = SelfCollision::new(0.5, 0.3, std::f64::consts::PI);
1705        let verts = vec![[0.0f64, 0.0, 0.0], [0.1, 0.0, 0.0], [0.2, 0.0, 0.0]];
1706        sc.update(&verts);
1707        let pairs = sc.find_pairs(&verts, None);
1708        assert!(!pairs.is_empty());
1709        for p in &pairs {
1710            assert!(p.a < p.b);
1711        }
1712    }
1713
1714    #[test]
1715    fn ccd_deformable_edge_edge() {
1716        let ccd = CcdDeformable::new(0.1, 0.1);
1717        let verts0_a = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0]];
1718        let verts1_a = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0]];
1719        let verts0_b = vec![[0.5, 1.0, 0.0], [0.5, 0.1, 0.0]];
1720        let verts1_b = vec![[0.5, 0.05, 0.0], [0.5, -0.85, 0.0]];
1721        let edges_a = vec![(0, 1)];
1722        let edges_b = vec![(0, 1)];
1723        let results = ccd.edge_edge_ccd(
1724            &edges_a, &verts0_a, &verts1_a, &edges_b, &verts0_b, &verts1_b,
1725        );
1726        // Just ensure no panic
1727        let _ = results;
1728    }
1729
1730    #[test]
1731    fn deformable_vs_bvh_multiple_triangles() {
1732        let tris = vec![
1733            [[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1734            [[2.0f64, 0.0, 0.0], [3.0, 0.0, 0.0], [2.0, 1.0, 0.0]],
1735        ];
1736        let det = DeformableVsBvh::new(tris, 0.1);
1737        let verts = vec![[0.3f64, 0.3, 0.05], [2.5, 0.3, 0.05]];
1738        let contacts = det.query(&verts);
1739        assert_eq!(contacts.len(), 2, "expected 2 contacts");
1740    }
1741}