Skip to main content

oxiphysics_collision/
compound_shapes.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Compound and complex shape handling for collision detection.
5//!
6//! Provides:
7//! - `CompoundShape` — collection of child shapes with local transforms
8//! - `ConvexDecomposition` — iterative approximate convex decomposition
9//! - `TriangleMeshShape` — triangle mesh with BVH and normals
10//! - `MeshShapeQuery` — point-in-mesh, closest point, normal
11//! - `GjkPolytope` — convex polytope with support function
12//! - `MinkowskiSum` — Minkowski sum for GJK distance
13//! - `ShapeProxy` — dispatch wrapper for shape types
14//! - `CompoundBvh` — BVH over compound sub-shapes
15//! - `ShapeOffset` — Minkowski sum with sphere (rounded shapes)
16//! - `ImportedCollisionMesh` — mesh from vertex data
17
18// ---------------------------------------------------------------------------
19// Helper math
20// ---------------------------------------------------------------------------
21
22/// Dot product of two 3-vectors.
23#[inline]
24pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
25    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
26}
27
28/// Cross product of two 3-vectors.
29#[inline]
30pub fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
31    [
32        a[1] * b[2] - a[2] * b[1],
33        a[2] * b[0] - a[0] * b[2],
34        a[0] * b[1] - a[1] * b[0],
35    ]
36}
37
38/// Squared norm of a 3-vector.
39#[inline]
40pub fn norm_sq3(a: [f64; 3]) -> f64 {
41    dot3(a, a)
42}
43
44/// Norm of a 3-vector.
45#[inline]
46pub fn norm3(a: [f64; 3]) -> f64 {
47    norm_sq3(a).sqrt()
48}
49
50/// Normalize a 3-vector. Returns zero vector if degenerate.
51#[inline]
52pub fn normalize3(a: [f64; 3]) -> [f64; 3] {
53    let n = norm3(a);
54    if n < 1e-15 {
55        return [0.0; 3];
56    }
57    [a[0] / n, a[1] / n, a[2] / n]
58}
59
60/// Add two 3-vectors.
61#[inline]
62pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
63    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
64}
65
66/// Subtract two 3-vectors.
67#[inline]
68pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
69    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
70}
71
72/// Scale a 3-vector.
73#[inline]
74pub fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
75    [a[0] * s, a[1] * s, a[2] * s]
76}
77
78// ---------------------------------------------------------------------------
79// AABB (Axis-Aligned Bounding Box)
80// ---------------------------------------------------------------------------
81
82/// Axis-aligned bounding box.
83#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct Aabb3 {
85    /// Minimum corner.
86    pub min: [f64; 3],
87    /// Maximum corner.
88    pub max: [f64; 3],
89}
90
91impl Aabb3 {
92    /// Create a new AABB.
93    pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
94        Self { min, max }
95    }
96
97    /// Create a degenerate (empty) AABB.
98    pub fn empty() -> Self {
99        Self {
100            min: [f64::MAX; 3],
101            max: [f64::MIN; 3],
102        }
103    }
104
105    /// Expand AABB to include point p.
106    pub fn include_point(&mut self, p: [f64; 3]) {
107        self.min
108            .iter_mut()
109            .zip(p.iter())
110            .for_each(|(m, &pi)| *m = m.min(pi));
111        self.max
112            .iter_mut()
113            .zip(p.iter())
114            .for_each(|(m, &pi)| *m = m.max(pi));
115    }
116
117    /// Expand AABB to include another AABB.
118    pub fn include_aabb(&mut self, other: &Aabb3) {
119        self.min
120            .iter_mut()
121            .zip(other.min.iter())
122            .for_each(|(m, &o)| *m = m.min(o));
123        self.max
124            .iter_mut()
125            .zip(other.max.iter())
126            .for_each(|(m, &o)| *m = m.max(o));
127    }
128
129    /// Center of this AABB.
130    pub fn center(&self) -> [f64; 3] {
131        [
132            0.5 * (self.min[0] + self.max[0]),
133            0.5 * (self.min[1] + self.max[1]),
134            0.5 * (self.min[2] + self.max[2]),
135        ]
136    }
137
138    /// Half-extents of this AABB.
139    pub fn half_extents(&self) -> [f64; 3] {
140        [
141            0.5 * (self.max[0] - self.min[0]),
142            0.5 * (self.max[1] - self.min[1]),
143            0.5 * (self.max[2] - self.min[2]),
144        ]
145    }
146
147    /// Volume of this AABB.
148    pub fn volume(&self) -> f64 {
149        let e = [
150            self.max[0] - self.min[0],
151            self.max[1] - self.min[1],
152            self.max[2] - self.min[2],
153        ];
154        e[0].max(0.0) * e[1].max(0.0) * e[2].max(0.0)
155    }
156
157    /// Check if two AABBs overlap.
158    pub fn overlaps(&self, other: &Aabb3) -> bool {
159        self.min[0] <= other.max[0]
160            && self.max[0] >= other.min[0]
161            && self.min[1] <= other.max[1]
162            && self.max[1] >= other.min[1]
163            && self.min[2] <= other.max[2]
164            && self.max[2] >= other.min[2]
165    }
166
167    /// Check if point is inside AABB.
168    pub fn contains_point(&self, p: [f64; 3]) -> bool {
169        p[0] >= self.min[0]
170            && p[0] <= self.max[0]
171            && p[1] >= self.min[1]
172            && p[1] <= self.max[1]
173            && p[2] >= self.min[2]
174            && p[2] <= self.max[2]
175    }
176
177    /// Inflate AABB by margin on all sides.
178    pub fn inflate(&self, margin: f64) -> Aabb3 {
179        Aabb3::new(
180            [
181                self.min[0] - margin,
182                self.min[1] - margin,
183                self.min[2] - margin,
184            ],
185            [
186                self.max[0] + margin,
187                self.max[1] + margin,
188                self.max[2] + margin,
189            ],
190        )
191    }
192}
193
194// ---------------------------------------------------------------------------
195// Local Transform (position + orientation as 3x3 rotation)
196// ---------------------------------------------------------------------------
197
198/// Simple local transform: position + 3x3 rotation matrix.
199#[derive(Debug, Clone, Copy)]
200pub struct LocalTransform {
201    /// Translation.
202    pub position: [f64; 3],
203    /// Rotation matrix (row-major).
204    pub rotation: [[f64; 3]; 3],
205}
206
207impl LocalTransform {
208    /// Identity transform.
209    pub fn identity() -> Self {
210        Self {
211            position: [0.0; 3],
212            rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
213        }
214    }
215
216    /// Translation-only transform.
217    pub fn from_translation(pos: [f64; 3]) -> Self {
218        let mut t = Self::identity();
219        t.position = pos;
220        t
221    }
222
223    /// Apply transform to point p: p' = R * p + t.
224    pub fn apply(&self, p: [f64; 3]) -> [f64; 3] {
225        let r = &self.rotation;
226        let rp = [
227            r[0][0] * p[0] + r[0][1] * p[1] + r[0][2] * p[2],
228            r[1][0] * p[0] + r[1][1] * p[1] + r[1][2] * p[2],
229            r[2][0] * p[0] + r[2][1] * p[1] + r[2][2] * p[2],
230        ];
231        add3(rp, self.position)
232    }
233
234    /// Apply inverse transform: p_local = R^T * (p - t).
235    pub fn inverse_apply(&self, p: [f64; 3]) -> [f64; 3] {
236        let dp = sub3(p, self.position);
237        let r = &self.rotation;
238        [
239            r[0][0] * dp[0] + r[1][0] * dp[1] + r[2][0] * dp[2],
240            r[0][1] * dp[0] + r[1][1] * dp[1] + r[2][1] * dp[2],
241            r[0][2] * dp[0] + r[1][2] * dp[1] + r[2][2] * dp[2],
242        ]
243    }
244}
245
246// ---------------------------------------------------------------------------
247// Primitive shape types (for compound)
248// ---------------------------------------------------------------------------
249
250/// Primitive shape variant.
251#[derive(Debug, Clone)]
252pub enum PrimitiveShape {
253    /// Sphere with radius.
254    Sphere(f64),
255    /// Box with half-extents \[hx, hy, hz\].
256    Box([f64; 3]),
257    /// Capsule with radius and half-height.
258    Capsule(f64, f64),
259    /// Cylinder with radius and half-height.
260    Cylinder(f64, f64),
261    /// Convex hull with vertices.
262    ConvexHull(Vec<[f64; 3]>),
263}
264
265impl PrimitiveShape {
266    /// Compute AABB of this primitive shape.
267    pub fn aabb(&self) -> Aabb3 {
268        match self {
269            PrimitiveShape::Sphere(r) => Aabb3::new([-r, -r, -r], [*r, *r, *r]),
270            PrimitiveShape::Box(he) => Aabb3::new([-he[0], -he[1], -he[2]], [he[0], he[1], he[2]]),
271            PrimitiveShape::Capsule(r, hh) => Aabb3::new([-r, -hh - r, -r], [*r, hh + r, *r]),
272            PrimitiveShape::Cylinder(r, hh) => Aabb3::new([-r, -hh, -r], [*r, *hh, *r]),
273            PrimitiveShape::ConvexHull(verts) => {
274                let mut aabb = Aabb3::empty();
275                for &v in verts {
276                    aabb.include_point(v);
277                }
278                aabb
279            }
280        }
281    }
282
283    /// Compute support point in direction d.
284    pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
285        match self {
286            PrimitiveShape::Sphere(r) => {
287                let n = norm3(d);
288                if n < 1e-15 {
289                    return [0.0; 3];
290                }
291                scale3(d, r / n)
292            }
293            PrimitiveShape::Box(he) => [
294                he[0] * d[0].signum(),
295                he[1] * d[1].signum(),
296                he[2] * d[2].signum(),
297            ],
298            PrimitiveShape::Capsule(r, hh) => {
299                let n = norm3(d);
300                let dn = if n < 1e-15 {
301                    [0.0, 1.0, 0.0]
302                } else {
303                    scale3(d, 1.0 / n)
304                };
305                let cap_center = [0.0, dn[1].signum() * hh, 0.0];
306                add3(cap_center, scale3(dn, *r))
307            }
308            PrimitiveShape::Cylinder(r, hh) => {
309                let xy_len = (d[0] * d[0] + d[2] * d[2]).sqrt();
310                let x = if xy_len > 1e-15 {
311                    r * d[0] / xy_len
312                } else {
313                    0.0
314                };
315                let z = if xy_len > 1e-15 {
316                    r * d[2] / xy_len
317                } else {
318                    0.0
319                };
320                [x, hh * d[1].signum(), z]
321            }
322            PrimitiveShape::ConvexHull(verts) => {
323                let mut best = [0.0f64; 3];
324                let mut best_d = f64::NEG_INFINITY;
325                for &v in verts {
326                    let proj = dot3(v, d);
327                    if proj > best_d {
328                        best_d = proj;
329                        best = v;
330                    }
331                }
332                best
333            }
334        }
335    }
336
337    /// Approximate volume of this primitive.
338    pub fn volume(&self) -> f64 {
339        let pi = std::f64::consts::PI;
340        match self {
341            PrimitiveShape::Sphere(r) => 4.0 / 3.0 * pi * r * r * r,
342            PrimitiveShape::Box(he) => 8.0 * he[0] * he[1] * he[2],
343            PrimitiveShape::Capsule(r, hh) => pi * r * r * 2.0 * hh + 4.0 / 3.0 * pi * r * r * r,
344            PrimitiveShape::Cylinder(r, hh) => pi * r * r * 2.0 * hh,
345            PrimitiveShape::ConvexHull(verts) => {
346                // Approximate using bounding box
347                let mut aabb = Aabb3::empty();
348                for &v in verts {
349                    aabb.include_point(v);
350                }
351                aabb.volume()
352            }
353        }
354    }
355}
356
357// ---------------------------------------------------------------------------
358// CompoundShape
359// ---------------------------------------------------------------------------
360
361/// Compound shape: a collection of primitive shapes with local transforms.
362///
363/// Provides AABB, volume, and center-of-mass computation over all children.
364#[derive(Debug, Clone)]
365pub struct CompoundShape {
366    /// Child shapes with their local transforms.
367    pub children: Vec<(PrimitiveShape, LocalTransform)>,
368}
369
370impl CompoundShape {
371    /// Create an empty compound shape.
372    pub fn new() -> Self {
373        Self {
374            children: Vec::new(),
375        }
376    }
377
378    /// Add a child shape with a local transform.
379    pub fn add_child(&mut self, shape: PrimitiveShape, transform: LocalTransform) {
380        self.children.push((shape, transform));
381    }
382
383    /// Compute the world AABB of the compound shape.
384    pub fn aabb(&self) -> Aabb3 {
385        let mut aabb = Aabb3::empty();
386        for (shape, xform) in &self.children {
387            let local_aabb = shape.aabb();
388            // Transform all 8 corners
389            let corners = [
390                [local_aabb.min[0], local_aabb.min[1], local_aabb.min[2]],
391                [local_aabb.max[0], local_aabb.min[1], local_aabb.min[2]],
392                [local_aabb.min[0], local_aabb.max[1], local_aabb.min[2]],
393                [local_aabb.max[0], local_aabb.max[1], local_aabb.min[2]],
394                [local_aabb.min[0], local_aabb.min[1], local_aabb.max[2]],
395                [local_aabb.max[0], local_aabb.min[1], local_aabb.max[2]],
396                [local_aabb.min[0], local_aabb.max[1], local_aabb.max[2]],
397                [local_aabb.max[0], local_aabb.max[1], local_aabb.max[2]],
398            ];
399            for c in &corners {
400                aabb.include_point(xform.apply(*c));
401            }
402        }
403        aabb
404    }
405
406    /// Compute total volume (sum of child volumes).
407    pub fn total_volume(&self) -> f64 {
408        self.children.iter().map(|(s, _)| s.volume()).sum()
409    }
410
411    /// Compute center of mass (mass-weighted centroid, assuming uniform density).
412    pub fn center_of_mass(&self) -> [f64; 3] {
413        let mut total_vol = 0.0f64;
414        let mut com = [0.0f64; 3];
415        for (shape, xform) in &self.children {
416            let v = shape.volume();
417            let c = xform.position; // center of child at transform origin
418            com[0] += v * c[0];
419            com[1] += v * c[1];
420            com[2] += v * c[2];
421            total_vol += v;
422        }
423        if total_vol < 1e-20 {
424            return [0.0; 3];
425        }
426        scale3(com, 1.0 / total_vol)
427    }
428
429    /// Support point for the compound shape in direction d.
430    pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
431        let mut best = [0.0f64; 3];
432        let mut best_proj = f64::NEG_INFINITY;
433        for (shape, xform) in &self.children {
434            // Transform direction to local frame
435            let d_local = xform.inverse_apply(add3(d, xform.position));
436            let local_sup = shape.support(d_local);
437            let world_sup = xform.apply(local_sup);
438            let proj = dot3(world_sup, d);
439            if proj > best_proj {
440                best_proj = proj;
441                best = world_sup;
442            }
443        }
444        best
445    }
446
447    /// Number of child shapes.
448    pub fn num_children(&self) -> usize {
449        self.children.len()
450    }
451}
452
453impl Default for CompoundShape {
454    fn default() -> Self {
455        Self::new()
456    }
457}
458
459/// Compute the AABB of a compound shape (free function).
460pub fn compute_compound_aabb(compound: &CompoundShape) -> Aabb3 {
461    compound.aabb()
462}
463
464// ---------------------------------------------------------------------------
465// ConvexDecomposition
466// ---------------------------------------------------------------------------
467
468/// Approximate convex decomposition (VHACD-inspired).
469///
470/// Iteratively splits a mesh into approximately convex parts using
471/// a concavity-based splitting strategy.
472#[derive(Debug, Clone)]
473pub struct ConvexDecomposition {
474    /// Maximum number of convex hulls.
475    pub max_hulls: usize,
476    /// Maximum concavity threshold.
477    pub max_concavity: f64,
478    /// Resolution for voxelization.
479    pub resolution: usize,
480    /// Output convex hulls.
481    pub hulls: Vec<Vec<[f64; 3]>>,
482}
483
484impl ConvexDecomposition {
485    /// Create a new convex decomposition.
486    pub fn new(max_hulls: usize, max_concavity: f64, resolution: usize) -> Self {
487        Self {
488            max_hulls,
489            max_concavity,
490            resolution,
491            hulls: Vec::new(),
492        }
493    }
494
495    /// Decompose a mesh into convex parts.
496    ///
497    /// Uses iterative splitting based on approximate concavity.
498    pub fn decompose(&mut self, vertices: &[[f64; 3]], indices: &[[usize; 3]]) {
499        self.hulls.clear();
500        if vertices.is_empty() || indices.is_empty() {
501            return;
502        }
503
504        // Start with the whole mesh as one part
505        let mut parts: Vec<Vec<usize>> = vec![(0..vertices.len()).collect()];
506        let mut output_hulls = Vec::new();
507
508        while !parts.is_empty() && output_hulls.len() < self.max_hulls {
509            let part = parts.remove(0);
510            let hull_verts: Vec<[f64; 3]> = part.iter().map(|&i| vertices[i]).collect();
511            let concavity = Self::estimate_concavity(&hull_verts);
512
513            if concavity < self.max_concavity || part.len() < 6 {
514                output_hulls.push(hull_verts);
515            } else if parts.len() + output_hulls.len() < self.max_hulls * 2 {
516                // Split into two parts
517                let (left, right) = Self::split_part(&part, vertices);
518                if !left.is_empty() {
519                    parts.push(left);
520                }
521                if !right.is_empty() {
522                    parts.push(right);
523                }
524            } else {
525                output_hulls.push(hull_verts);
526            }
527        }
528
529        // Remaining unprocessed parts
530        for part in parts {
531            let hull_verts: Vec<[f64; 3]> = part.iter().map(|&i| vertices[i]).collect();
532            output_hulls.push(hull_verts);
533        }
534
535        let _ = indices;
536        self.hulls = output_hulls;
537    }
538
539    /// Estimate concavity as max distance between convex hull and original mesh.
540    fn estimate_concavity(verts: &[[f64; 3]]) -> f64 {
541        if verts.len() < 4 {
542            return 0.0;
543        }
544        let hull = Self::simple_convex_hull(verts);
545        let mut max_d = 0.0f64;
546        for &v in verts {
547            let d = Self::point_to_hull_distance(v, &hull);
548            max_d = max_d.max(d);
549        }
550        max_d
551    }
552
553    /// Simple convex hull approximation (just bounding box corners).
554    fn simple_convex_hull(verts: &[[f64; 3]]) -> Vec<[f64; 3]> {
555        let mut aabb = Aabb3::empty();
556        for &v in verts {
557            aabb.include_point(v);
558        }
559        vec![
560            aabb.min,
561            aabb.max,
562            [aabb.min[0], aabb.max[1], aabb.min[2]],
563            [aabb.max[0], aabb.min[1], aabb.max[2]],
564        ]
565    }
566
567    /// Approximate distance from point to convex hull (AABB-based).
568    fn point_to_hull_distance(p: [f64; 3], hull: &[[f64; 3]]) -> f64 {
569        let mut aabb = Aabb3::empty();
570        for &v in hull {
571            aabb.include_point(v);
572        }
573        // Distance to inside of AABB (zero if inside)
574        let dx = 0.0f64.max(aabb.min[0] - p[0]).max(p[0] - aabb.max[0]);
575        let dy = 0.0f64.max(aabb.min[1] - p[1]).max(p[1] - aabb.max[1]);
576        let dz = 0.0f64.max(aabb.min[2] - p[2]).max(p[2] - aabb.max[2]);
577        (dx * dx + dy * dy + dz * dz).sqrt()
578    }
579
580    /// Split a set of vertex indices at the median of the longest axis.
581    fn split_part(indices: &[usize], vertices: &[[f64; 3]]) -> (Vec<usize>, Vec<usize>) {
582        if indices.is_empty() {
583            return (vec![], vec![]);
584        }
585        let verts: Vec<[f64; 3]> = indices.iter().map(|&i| vertices[i]).collect();
586        let mut aabb = Aabb3::empty();
587        for &v in &verts {
588            aabb.include_point(v);
589        }
590
591        // Find longest axis
592        let extents = [
593            aabb.max[0] - aabb.min[0],
594            aabb.max[1] - aabb.min[1],
595            aabb.max[2] - aabb.min[2],
596        ];
597        let axis = if extents[0] >= extents[1] && extents[0] >= extents[2] {
598            0
599        } else if extents[1] >= extents[2] {
600            1
601        } else {
602            2
603        };
604        let mid = 0.5 * (aabb.min[axis] + aabb.max[axis]);
605
606        let left: Vec<usize> = indices
607            .iter()
608            .copied()
609            .filter(|&i| vertices[i][axis] <= mid)
610            .collect();
611        let right: Vec<usize> = indices
612            .iter()
613            .copied()
614            .filter(|&i| vertices[i][axis] > mid)
615            .collect();
616        (left, right)
617    }
618
619    /// Number of output hulls.
620    pub fn num_hulls(&self) -> usize {
621        self.hulls.len()
622    }
623}
624
625/// Merge multiple convex hulls into one (free function).
626pub fn merge_hulls(hulls: &[Vec<[f64; 3]>]) -> Vec<[f64; 3]> {
627    let mut all_verts = Vec::new();
628    for h in hulls {
629        all_verts.extend_from_slice(h);
630    }
631    all_verts
632}
633
634// ---------------------------------------------------------------------------
635// TriangleMeshShape
636// ---------------------------------------------------------------------------
637
638/// Triangle mesh as a collision shape.
639///
640/// Stores triangle data with precomputed normals and a BVH for fast queries.
641#[derive(Debug, Clone)]
642pub struct TriangleMeshShape {
643    /// Vertex positions.
644    pub vertices: Vec<[f64; 3]>,
645    /// Triangle indices (triplets).
646    pub indices: Vec<[usize; 3]>,
647    /// Precomputed per-face normals.
648    pub normals: Vec<[f64; 3]>,
649    /// Per-face AABB.
650    pub face_aabbs: Vec<Aabb3>,
651    /// Overall AABB.
652    pub aabb: Aabb3,
653}
654
655impl TriangleMeshShape {
656    /// Create a triangle mesh shape from vertices and indices.
657    pub fn new(vertices: Vec<[f64; 3]>, indices: Vec<[usize; 3]>) -> Self {
658        let normals = Self::compute_normals(&vertices, &indices);
659        let face_aabbs = Self::compute_face_aabbs(&vertices, &indices);
660        let mut aabb = Aabb3::empty();
661        for &v in &vertices {
662            aabb.include_point(v);
663        }
664        Self {
665            vertices,
666            indices,
667            normals,
668            face_aabbs,
669            aabb,
670        }
671    }
672
673    /// Compute per-face normals.
674    fn compute_normals(verts: &[[f64; 3]], indices: &[[usize; 3]]) -> Vec<[f64; 3]> {
675        indices
676            .iter()
677            .map(|tri| {
678                let a = verts[tri[0]];
679                let b = verts[tri[1]];
680                let c = verts[tri[2]];
681                let ab = sub3(b, a);
682                let ac = sub3(c, a);
683                normalize3(cross3(ab, ac))
684            })
685            .collect()
686    }
687
688    /// Compute per-face AABBs.
689    fn compute_face_aabbs(verts: &[[f64; 3]], indices: &[[usize; 3]]) -> Vec<Aabb3> {
690        indices
691            .iter()
692            .map(|tri| {
693                let mut aabb = Aabb3::empty();
694                for &vi in tri {
695                    aabb.include_point(verts[vi]);
696                }
697                aabb
698            })
699            .collect()
700    }
701
702    /// Number of triangles.
703    pub fn num_triangles(&self) -> usize {
704        self.indices.len()
705    }
706
707    /// Get a triangle by index.
708    pub fn triangle(&self, i: usize) -> ([f64; 3], [f64; 3], [f64; 3]) {
709        let tri = &self.indices[i];
710        (
711            self.vertices[tri[0]],
712            self.vertices[tri[1]],
713            self.vertices[tri[2]],
714        )
715    }
716
717    /// Compute total surface area.
718    pub fn surface_area(&self) -> f64 {
719        (0..self.num_triangles())
720            .map(|i| {
721                let (a, b, c) = self.triangle(i);
722                let ab = sub3(b, a);
723                let ac = sub3(c, a);
724                0.5 * norm3(cross3(ab, ac))
725            })
726            .sum()
727    }
728}
729
730// ---------------------------------------------------------------------------
731// MeshShapeQuery
732// ---------------------------------------------------------------------------
733
734/// Query operations on triangle mesh shapes.
735///
736/// Provides point-in-mesh test, closest point, and normal at hit.
737#[derive(Debug, Clone)]
738pub struct MeshShapeQuery<'a> {
739    /// Reference to the mesh.
740    pub mesh: &'a TriangleMeshShape,
741}
742
743impl<'a> MeshShapeQuery<'a> {
744    /// Create a new mesh query.
745    pub fn new(mesh: &'a TriangleMeshShape) -> Self {
746        Self { mesh }
747    }
748
749    /// Point-in-mesh test using ray-parity method.
750    ///
751    /// Casts a ray in +x direction and counts crossings.
752    pub fn point_inside(&self, point: [f64; 3]) -> bool {
753        let dir = [1.0, 0.0, 0.0];
754        let count = (0..self.mesh.num_triangles())
755            .filter(|&i| {
756                let (a, b, c) = self.mesh.triangle(i);
757                Self::ray_triangle_intersect(point, dir, a, b, c).is_some()
758            })
759            .count();
760        count % 2 == 1
761    }
762
763    /// Möller-Trumbore ray-triangle intersection.
764    ///
765    /// Returns t parameter if hit, None otherwise.
766    pub fn ray_triangle_intersect(
767        origin: [f64; 3],
768        direction: [f64; 3],
769        a: [f64; 3],
770        b: [f64; 3],
771        c: [f64; 3],
772    ) -> Option<f64> {
773        let edge1 = sub3(b, a);
774        let edge2 = sub3(c, a);
775        let h = cross3(direction, edge2);
776        let det = dot3(edge1, h);
777        if det.abs() < 1e-14 {
778            return None;
779        }
780        let inv_det = 1.0 / det;
781        let s = sub3(origin, a);
782        let u = inv_det * dot3(s, h);
783        if !(0.0..=1.0).contains(&u) {
784            return None;
785        }
786        let q = cross3(s, edge1);
787        let v = inv_det * dot3(direction, q);
788        if v < 0.0 || u + v > 1.0 {
789            return None;
790        }
791        let t = inv_det * dot3(edge2, q);
792        if t > 1e-10 { Some(t) } else { None }
793    }
794
795    /// Find closest point on mesh surface to query point.
796    ///
797    /// Returns (closest_point, face_index, distance).
798    pub fn closest_point(&self, query: [f64; 3]) -> ([f64; 3], usize, f64) {
799        let mut best_pt = [0.0f64; 3];
800        let mut best_face = 0;
801        let mut best_dist = f64::MAX;
802
803        for (i, _) in (0..self.mesh.num_triangles()).enumerate() {
804            let (a, b, c) = self.mesh.triangle(i);
805            let pt = Self::closest_point_triangle(query, a, b, c);
806            let d = norm3(sub3(query, pt));
807            if d < best_dist {
808                best_dist = d;
809                best_pt = pt;
810                best_face = i;
811            }
812        }
813        (best_pt, best_face, best_dist)
814    }
815
816    /// Closest point on triangle (a, b, c) to point p.
817    pub fn closest_point_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
818        let ab = sub3(b, a);
819        let ac = sub3(c, a);
820        let ap = sub3(p, a);
821
822        let d1 = dot3(ab, ap);
823        let d2 = dot3(ac, ap);
824        if d1 <= 0.0 && d2 <= 0.0 {
825            return a;
826        }
827
828        let bp = sub3(p, b);
829        let d3 = dot3(ab, bp);
830        let d4 = dot3(ac, bp);
831        if d3 >= 0.0 && d4 <= d3 {
832            return b;
833        }
834
835        let cp = sub3(p, c);
836        let d5 = dot3(ab, cp);
837        let d6 = dot3(ac, cp);
838        if d6 >= 0.0 && d5 <= d6 {
839            return c;
840        }
841
842        // Edge AB
843        let vc = d1 * d4 - d3 * d2;
844        if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
845            let v = d1 / (d1 - d3);
846            return add3(a, scale3(ab, v));
847        }
848
849        // Edge AC
850        let vb = d5 * d2 - d1 * d6;
851        if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
852            let w = d2 / (d2 - d6);
853            return add3(a, scale3(ac, w));
854        }
855
856        // Edge BC
857        let va = d3 * d6 - d5 * d4;
858        if va <= 0.0 && d4 - d3 >= 0.0 && d5 - d6 >= 0.0 {
859            let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
860            return add3(b, scale3(sub3(c, b), w));
861        }
862
863        // Inside triangle
864        let denom = 1.0 / (va + vb + vc);
865        let v = vb * denom;
866        let w = vc * denom;
867        add3(a, add3(scale3(ab, v), scale3(ac, w)))
868    }
869
870    /// Get surface normal at query point (closest face normal).
871    pub fn normal_at(&self, query: [f64; 3]) -> [f64; 3] {
872        let (_, face_idx, _) = self.closest_point(query);
873        *self.mesh.normals.get(face_idx).unwrap_or(&[0.0, 1.0, 0.0])
874    }
875}
876
877// ---------------------------------------------------------------------------
878// GjkPolytope
879// ---------------------------------------------------------------------------
880
881/// Generic convex polytope for GJK distance computation.
882///
883/// Defined by a finite set of vertices and a support function.
884#[derive(Debug, Clone)]
885pub struct GjkPolytope {
886    /// Vertex set.
887    pub vertices: Vec<[f64; 3]>,
888    /// Center (for numerical stability).
889    pub center: [f64; 3],
890}
891
892impl GjkPolytope {
893    /// Create a new GJK polytope from vertices.
894    pub fn new(vertices: Vec<[f64; 3]>) -> Self {
895        let n = vertices.len();
896        let center = if n > 0 {
897            let s: [f64; 3] = vertices.iter().fold([0.0; 3], |a, &v| add3(a, v));
898            scale3(s, 1.0 / n as f64)
899        } else {
900            [0.0; 3]
901        };
902        Self { vertices, center }
903    }
904
905    /// Compute support point in direction d.
906    pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
907        let mut best = self.center;
908        let mut best_proj = f64::NEG_INFINITY;
909        for &v in &self.vertices {
910            let proj = dot3(v, d);
911            if proj > best_proj {
912                best_proj = proj;
913                best = v;
914            }
915        }
916        best
917    }
918
919    /// Number of vertices.
920    pub fn num_vertices(&self) -> usize {
921        self.vertices.len()
922    }
923
924    /// AABB of this polytope.
925    pub fn aabb(&self) -> Aabb3 {
926        let mut aabb = Aabb3::empty();
927        for &v in &self.vertices {
928            aabb.include_point(v);
929        }
930        aabb
931    }
932}
933
934// ---------------------------------------------------------------------------
935// MinkowskiSum
936// ---------------------------------------------------------------------------
937
938/// Minkowski sum of two convex shapes (for GJK distance computation).
939///
940/// Support function: h_A⊕B(d) = h_A(d) + h_B(d)
941#[derive(Debug, Clone)]
942pub struct MinkowskiSum {
943    /// First convex shape.
944    pub shape_a: GjkPolytope,
945    /// Second convex shape (translated by offset_b).
946    pub shape_b: GjkPolytope,
947    /// Translation of shape B.
948    pub offset_b: [f64; 3],
949}
950
951impl MinkowskiSum {
952    /// Create a new Minkowski sum.
953    pub fn new(shape_a: GjkPolytope, shape_b: GjkPolytope, offset_b: [f64; 3]) -> Self {
954        Self {
955            shape_a,
956            shape_b,
957            offset_b,
958        }
959    }
960
961    /// Compute support of A ⊕ (-B) in direction d (for GJK).
962    ///
963    /// Support of Minkowski difference: s_A(d) - s_B(-d)
964    pub fn support_difference(&self, d: [f64; 3]) -> [f64; 3] {
965        let s_a = self.shape_a.support(d);
966        let neg_d = [-d[0], -d[1], -d[2]];
967        let s_b = self.shape_b.support(neg_d);
968        // Translate B by offset_b
969        let s_b_world = add3(s_b, self.offset_b);
970        sub3(s_a, s_b_world)
971    }
972
973    /// Check if origin is contained in Minkowski difference (shapes overlap).
974    pub fn origin_in_md(&self) -> bool {
975        // Simple check: sample some directions
976        let dirs = [
977            [1.0, 0.0, 0.0_f64],
978            [-1.0, 0.0, 0.0],
979            [0.0, 1.0, 0.0],
980            [0.0, -1.0, 0.0],
981            [0.0, 0.0, 1.0],
982            [0.0, 0.0, -1.0],
983        ];
984        // If origin is inside, all support points project positively
985        for d in &dirs {
986            let s = self.support_difference(*d);
987            if dot3(s, *d) < 0.0 {
988                return false;
989            }
990        }
991        true
992    }
993}
994
995// ---------------------------------------------------------------------------
996// ShapeProxy
997// ---------------------------------------------------------------------------
998
999/// Thin wrapper for dispatching different shape types.
1000#[derive(Debug, Clone)]
1001pub enum ShapeProxy {
1002    /// Primitive shape.
1003    Primitive(PrimitiveShape),
1004    /// Compound shape.
1005    Compound(CompoundShape),
1006    /// Triangle mesh.
1007    TriangleMesh(TriangleMeshShape),
1008    /// GJK polytope.
1009    Polytope(GjkPolytope),
1010}
1011
1012impl ShapeProxy {
1013    /// Compute AABB for this shape.
1014    pub fn aabb(&self) -> Aabb3 {
1015        match self {
1016            ShapeProxy::Primitive(p) => p.aabb(),
1017            ShapeProxy::Compound(c) => c.aabb(),
1018            ShapeProxy::TriangleMesh(m) => m.aabb,
1019            ShapeProxy::Polytope(p) => p.aabb(),
1020        }
1021    }
1022
1023    /// Compute support point in direction d.
1024    pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
1025        match self {
1026            ShapeProxy::Primitive(p) => p.support(d),
1027            ShapeProxy::Compound(c) => c.support(d),
1028            ShapeProxy::TriangleMesh(m) => {
1029                let mut best = [0.0f64; 3];
1030                let mut best_proj = f64::NEG_INFINITY;
1031                for &v in &m.vertices {
1032                    let p = dot3(v, d);
1033                    if p > best_proj {
1034                        best_proj = p;
1035                        best = v;
1036                    }
1037                }
1038                best
1039            }
1040            ShapeProxy::Polytope(p) => p.support(d),
1041        }
1042    }
1043
1044    /// Shape name string.
1045    pub fn name(&self) -> &str {
1046        match self {
1047            ShapeProxy::Primitive(PrimitiveShape::Sphere(_)) => "Sphere",
1048            ShapeProxy::Primitive(PrimitiveShape::Box(_)) => "Box",
1049            ShapeProxy::Primitive(PrimitiveShape::Capsule(_, _)) => "Capsule",
1050            ShapeProxy::Primitive(PrimitiveShape::Cylinder(_, _)) => "Cylinder",
1051            ShapeProxy::Primitive(PrimitiveShape::ConvexHull(_)) => "ConvexHull",
1052            ShapeProxy::Compound(_) => "Compound",
1053            ShapeProxy::TriangleMesh(_) => "TriangleMesh",
1054            ShapeProxy::Polytope(_) => "Polytope",
1055        }
1056    }
1057}
1058
1059// ---------------------------------------------------------------------------
1060// CompoundBvh
1061// ---------------------------------------------------------------------------
1062
1063/// BVH over compound sub-shapes for fast overlap tests.
1064#[derive(Debug, Clone)]
1065pub struct CompoundBvhNode {
1066    /// AABB for this node.
1067    pub aabb: Aabb3,
1068    /// Child indices (up to 2) or leaf shape index.
1069    pub children: [Option<usize>; 2],
1070    /// Leaf shape index (None if internal node).
1071    pub leaf_shape_idx: Option<usize>,
1072}
1073
1074/// BVH over compound shapes.
1075#[derive(Debug, Clone)]
1076pub struct CompoundBvh {
1077    /// BVH nodes.
1078    pub nodes: Vec<CompoundBvhNode>,
1079    /// Root node index.
1080    pub root: usize,
1081    /// Shape proxies at leaves.
1082    pub shapes: Vec<(ShapeProxy, LocalTransform)>,
1083}
1084
1085impl CompoundBvh {
1086    /// Build a BVH over a list of shapes.
1087    pub fn build(shapes: Vec<(ShapeProxy, LocalTransform)>) -> Self {
1088        let n = shapes.len();
1089        let mut nodes = Vec::new();
1090
1091        if n == 0 {
1092            let empty_node = CompoundBvhNode {
1093                aabb: Aabb3::empty(),
1094                children: [None, None],
1095                leaf_shape_idx: None,
1096            };
1097            nodes.push(empty_node);
1098            return Self {
1099                nodes,
1100                root: 0,
1101                shapes,
1102            };
1103        }
1104
1105        // Build a simple binary tree by AABB subdivision
1106        let shape_aabbs: Vec<Aabb3> = shapes
1107            .iter()
1108            .map(|(s, xform)| {
1109                let local_aabb = s.aabb();
1110                let mut world_aabb = Aabb3::empty();
1111                for corner in Self::aabb_corners(&local_aabb) {
1112                    world_aabb.include_point(xform.apply(corner));
1113                }
1114                world_aabb
1115            })
1116            .collect();
1117
1118        let root = Self::build_recursive(&mut nodes, &(0..n).collect::<Vec<_>>(), &shape_aabbs);
1119
1120        Self {
1121            nodes,
1122            root,
1123            shapes,
1124        }
1125    }
1126
1127    fn aabb_corners(aabb: &Aabb3) -> [[f64; 3]; 8] {
1128        [
1129            [aabb.min[0], aabb.min[1], aabb.min[2]],
1130            [aabb.max[0], aabb.min[1], aabb.min[2]],
1131            [aabb.min[0], aabb.max[1], aabb.min[2]],
1132            [aabb.max[0], aabb.max[1], aabb.min[2]],
1133            [aabb.min[0], aabb.min[1], aabb.max[2]],
1134            [aabb.max[0], aabb.min[1], aabb.max[2]],
1135            [aabb.min[0], aabb.max[1], aabb.max[2]],
1136            [aabb.max[0], aabb.max[1], aabb.max[2]],
1137        ]
1138    }
1139
1140    fn build_recursive(
1141        nodes: &mut Vec<CompoundBvhNode>,
1142        indices: &[usize],
1143        aabbs: &[Aabb3],
1144    ) -> usize {
1145        let mut total_aabb = Aabb3::empty();
1146        for &i in indices {
1147            total_aabb.include_aabb(&aabbs[i]);
1148        }
1149
1150        if indices.len() == 1 {
1151            let node_idx = nodes.len();
1152            nodes.push(CompoundBvhNode {
1153                aabb: total_aabb,
1154                children: [None, None],
1155                leaf_shape_idx: Some(indices[0]),
1156            });
1157            return node_idx;
1158        }
1159
1160        // Split by longest axis
1161        let extents = total_aabb.half_extents();
1162        let axis = if extents[0] >= extents[1] && extents[0] >= extents[2] {
1163            0
1164        } else if extents[1] >= extents[2] {
1165            1
1166        } else {
1167            2
1168        };
1169        let mid = total_aabb.center()[axis];
1170        let (left, right): (Vec<_>, Vec<_>) = indices
1171            .iter()
1172            .partition(|&&i| 0.5 * (aabbs[i].min[axis] + aabbs[i].max[axis]) <= mid);
1173        let left = if left.is_empty() {
1174            indices[..indices.len() / 2].to_vec()
1175        } else {
1176            left
1177        };
1178        let right = if right.is_empty() {
1179            indices[indices.len() / 2..].to_vec()
1180        } else {
1181            right
1182        };
1183
1184        let left_idx = Self::build_recursive(nodes, &left, aabbs);
1185        let right_idx = Self::build_recursive(nodes, &right, aabbs);
1186
1187        let node_idx = nodes.len();
1188        nodes.push(CompoundBvhNode {
1189            aabb: total_aabb,
1190            children: [Some(left_idx), Some(right_idx)],
1191            leaf_shape_idx: None,
1192        });
1193        node_idx
1194    }
1195
1196    /// Query shapes that overlap a given AABB.
1197    pub fn query_aabb(&self, query: &Aabb3) -> Vec<usize> {
1198        let mut result = Vec::new();
1199        if self.nodes.is_empty() {
1200            return result;
1201        }
1202        self.query_recursive(self.root, query, &mut result);
1203        result
1204    }
1205
1206    fn query_recursive(&self, node_idx: usize, query: &Aabb3, result: &mut Vec<usize>) {
1207        if node_idx >= self.nodes.len() {
1208            return;
1209        }
1210        let node = &self.nodes[node_idx];
1211        if !node.aabb.overlaps(query) {
1212            return;
1213        }
1214        if let Some(leaf_idx) = node.leaf_shape_idx {
1215            result.push(leaf_idx);
1216            return;
1217        }
1218        for &child in &node.children {
1219            if let Some(c) = child {
1220                self.query_recursive(c, query, result);
1221            }
1222        }
1223    }
1224}
1225
1226// ---------------------------------------------------------------------------
1227// ShapeOffset
1228// ---------------------------------------------------------------------------
1229
1230/// Outward offset (inflation) of a shape by a radius.
1231///
1232/// Equivalent to Minkowski sum of shape with sphere of radius r.
1233#[derive(Debug, Clone)]
1234pub struct ShapeOffset {
1235    /// Inner shape.
1236    pub inner: ShapeProxy,
1237    /// Inflation radius.
1238    pub offset_radius: f64,
1239}
1240
1241impl ShapeOffset {
1242    /// Create a new offset shape.
1243    pub fn new(inner: ShapeProxy, offset_radius: f64) -> Self {
1244        Self {
1245            inner,
1246            offset_radius,
1247        }
1248    }
1249
1250    /// Compute support point: s_inner(d) + r * d̂.
1251    pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
1252        let n = norm3(d);
1253        let s = self.inner.support(d);
1254        if n < 1e-15 {
1255            return s;
1256        }
1257        add3(s, scale3(d, self.offset_radius / n))
1258    }
1259
1260    /// Compute AABB (inflated by offset_radius).
1261    pub fn aabb(&self) -> Aabb3 {
1262        self.inner.aabb().inflate(self.offset_radius)
1263    }
1264}
1265
1266// ---------------------------------------------------------------------------
1267// ImportedCollisionMesh
1268// ---------------------------------------------------------------------------
1269
1270/// Collision mesh mode.
1271#[derive(Debug, Clone, PartialEq)]
1272pub enum CollisionMeshMode {
1273    /// Use as triangle mesh directly.
1274    TriangleMesh,
1275    /// Use convex hull.
1276    ConvexHull,
1277    /// Use approximate convex decomposition.
1278    ConvexDecomposition,
1279}
1280
1281/// Collision mesh imported from vertex/index data.
1282///
1283/// Can operate in triangle mesh, convex hull, or convex decomposition mode.
1284#[derive(Debug, Clone)]
1285pub struct ImportedCollisionMesh {
1286    /// Mesh mode.
1287    pub mode: CollisionMeshMode,
1288    /// Original vertices.
1289    pub vertices: Vec<[f64; 3]>,
1290    /// Triangle indices.
1291    pub indices: Vec<[usize; 3]>,
1292    /// Triangle mesh (for TriangleMesh mode).
1293    pub mesh: Option<TriangleMeshShape>,
1294    /// Convex hull vertices (for ConvexHull mode).
1295    pub convex_hull: Vec<[f64; 3]>,
1296    /// Decomposed hulls (for ConvexDecomposition mode).
1297    pub decomposed_hulls: Vec<Vec<[f64; 3]>>,
1298}
1299
1300impl ImportedCollisionMesh {
1301    /// Create a new imported collision mesh.
1302    pub fn new(vertices: Vec<[f64; 3]>, indices: Vec<[usize; 3]>, mode: CollisionMeshMode) -> Self {
1303        let mut mesh = None;
1304        let mut convex_hull = Vec::new();
1305        let mut decomposed_hulls = Vec::new();
1306
1307        match &mode {
1308            CollisionMeshMode::TriangleMesh => {
1309                mesh = Some(TriangleMeshShape::new(vertices.clone(), indices.clone()));
1310            }
1311            CollisionMeshMode::ConvexHull => {
1312                convex_hull = Self::compute_convex_hull(&vertices);
1313            }
1314            CollisionMeshMode::ConvexDecomposition => {
1315                let mut decomp = ConvexDecomposition::new(16, 0.01, 64);
1316                decomp.decompose(&vertices, &indices);
1317                decomposed_hulls = decomp.hulls;
1318            }
1319        }
1320
1321        Self {
1322            mode,
1323            vertices,
1324            indices,
1325            mesh,
1326            convex_hull,
1327            decomposed_hulls,
1328        }
1329    }
1330
1331    /// Compute convex hull (simplified: use all points, filter redundant).
1332    fn compute_convex_hull(verts: &[[f64; 3]]) -> Vec<[f64; 3]> {
1333        // Simplified: return extremal points in 6 directions
1334        if verts.is_empty() {
1335            return Vec::new();
1336        }
1337        let mut hull = Vec::new();
1338        let dirs = [
1339            [1.0_f64, 0.0, 0.0],
1340            [-1.0, 0.0, 0.0],
1341            [0.0, 1.0, 0.0],
1342            [0.0, -1.0, 0.0],
1343            [0.0, 0.0, 1.0],
1344            [0.0, 0.0, -1.0],
1345        ];
1346        for d in &dirs {
1347            let best = verts.iter().max_by(|a, b| {
1348                dot3(**a, *d)
1349                    .partial_cmp(&dot3(**b, *d))
1350                    .unwrap_or(std::cmp::Ordering::Equal)
1351            });
1352            if let Some(v) = best {
1353                hull.push(*v);
1354            }
1355        }
1356        hull.dedup_by(|a, b| {
1357            (a[0] - b[0]).abs() < 1e-10
1358                && (a[1] - b[1]).abs() < 1e-10
1359                && (a[2] - b[2]).abs() < 1e-10
1360        });
1361        hull
1362    }
1363
1364    /// Get AABB.
1365    pub fn aabb(&self) -> Aabb3 {
1366        match &self.mesh {
1367            Some(m) => m.aabb,
1368            None => {
1369                let mut aabb = Aabb3::empty();
1370                for &v in &self.vertices {
1371                    aabb.include_point(v);
1372                }
1373                aabb
1374            }
1375        }
1376    }
1377
1378    /// Compute support point.
1379    pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
1380        let verts = match &self.mode {
1381            CollisionMeshMode::ConvexHull => &self.convex_hull,
1382            _ => &self.vertices,
1383        };
1384        if verts.is_empty() {
1385            return [0.0; 3];
1386        }
1387        verts
1388            .iter()
1389            .max_by(|a, b| {
1390                dot3(**a, d)
1391                    .partial_cmp(&dot3(**b, d))
1392                    .unwrap_or(std::cmp::Ordering::Equal)
1393            })
1394            .copied()
1395            .unwrap_or([0.0; 3])
1396    }
1397}
1398
1399/// Compute voxelized collision mask (simplified).
1400///
1401/// Returns a flat boolean grid of occupied voxels.
1402pub fn voxelized_collision(mesh: &TriangleMeshShape, resolution: usize) -> Vec<bool> {
1403    let aabb = mesh.aabb;
1404    let n = resolution;
1405    let mut grid = vec![false; n * n * n];
1406    let query = MeshShapeQuery::new(mesh);
1407
1408    for xi in 0..n {
1409        for yi in 0..n {
1410            for zi in 0..n {
1411                let p = [
1412                    aabb.min[0] + (xi as f64 + 0.5) / n as f64 * (aabb.max[0] - aabb.min[0]),
1413                    aabb.min[1] + (yi as f64 + 0.5) / n as f64 * (aabb.max[1] - aabb.min[1]),
1414                    aabb.min[2] + (zi as f64 + 0.5) / n as f64 * (aabb.max[2] - aabb.min[2]),
1415                ];
1416                grid[xi * n * n + yi * n + zi] = query.point_inside(p);
1417            }
1418        }
1419    }
1420    grid
1421}
1422
1423/// Compute approximate shape volume using sampling.
1424pub fn shape_volume(shape: &ShapeProxy, resolution: usize) -> f64 {
1425    let aabb = shape.aabb();
1426    let vol = aabb.volume();
1427    if vol < 1e-20 || resolution == 0 {
1428        return vol;
1429    }
1430    let n = resolution;
1431    let mut inside_count = 0usize;
1432    let total = n * n * n;
1433
1434    for xi in 0..n {
1435        for yi in 0..n {
1436            for zi in 0..n {
1437                let p = [
1438                    aabb.min[0] + (xi as f64 + 0.5) / n as f64 * (aabb.max[0] - aabb.min[0]),
1439                    aabb.min[1] + (yi as f64 + 0.5) / n as f64 * (aabb.max[1] - aabb.min[1]),
1440                    aabb.min[2] + (zi as f64 + 0.5) / n as f64 * (aabb.max[2] - aabb.min[2]),
1441                ];
1442                // Simple test: point inside support
1443                let s = shape.support(p);
1444                if dot3(s, p) > dot3(p, p) - 1e-6 {
1445                    inside_count += 1;
1446                }
1447            }
1448        }
1449    }
1450    vol * inside_count as f64 / total as f64
1451}
1452
1453// ---------------------------------------------------------------------------
1454// Tests
1455// ---------------------------------------------------------------------------
1456
1457#[cfg(test)]
1458mod tests {
1459    use super::*;
1460
1461    fn make_unit_sphere() -> PrimitiveShape {
1462        PrimitiveShape::Sphere(1.0)
1463    }
1464
1465    fn make_box_shape() -> PrimitiveShape {
1466        PrimitiveShape::Box([1.0, 1.0, 1.0])
1467    }
1468
1469    fn make_tetra() -> TriangleMeshShape {
1470        let verts = vec![
1471            [0.0, 0.0, 0.0],
1472            [1.0, 0.0, 0.0],
1473            [0.5, 1.0, 0.0],
1474            [0.5, 0.5, 1.0],
1475        ];
1476        let idxs = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]];
1477        TriangleMeshShape::new(verts, idxs)
1478    }
1479
1480    #[test]
1481    fn test_aabb_volume() {
1482        let aabb = Aabb3::new([0.0; 3], [2.0, 3.0, 4.0]);
1483        assert!((aabb.volume() - 24.0).abs() < 1e-10);
1484    }
1485
1486    #[test]
1487    fn test_aabb_overlaps() {
1488        let a = Aabb3::new([0.0; 3], [1.0; 3]);
1489        let b = Aabb3::new([0.5; 3], [1.5; 3]);
1490        assert!(a.overlaps(&b));
1491        let c = Aabb3::new([2.0; 3], [3.0; 3]);
1492        assert!(!a.overlaps(&c));
1493    }
1494
1495    #[test]
1496    fn test_aabb_contains_point() {
1497        let aabb = Aabb3::new([0.0; 3], [1.0; 3]);
1498        assert!(aabb.contains_point([0.5, 0.5, 0.5]));
1499        assert!(!aabb.contains_point([1.5, 0.5, 0.5]));
1500    }
1501
1502    #[test]
1503    fn test_sphere_support() {
1504        let sphere = make_unit_sphere();
1505        let s = sphere.support([1.0, 0.0, 0.0]);
1506        assert!(
1507            (s[0] - 1.0).abs() < 1e-10,
1508            "sphere support in +x should be [1,0,0]"
1509        );
1510    }
1511
1512    #[test]
1513    fn test_box_support() {
1514        let b = make_box_shape();
1515        let s = b.support([1.0, 1.0, 1.0]);
1516        assert!((s[0] - 1.0).abs() < 1e-10);
1517        assert!((s[1] - 1.0).abs() < 1e-10);
1518        assert!((s[2] - 1.0).abs() < 1e-10);
1519    }
1520
1521    #[test]
1522    fn test_sphere_volume() {
1523        let s = make_unit_sphere();
1524        let v = s.volume();
1525        let expected = 4.0 / 3.0 * std::f64::consts::PI;
1526        assert!((v - expected).abs() < 1e-6);
1527    }
1528
1529    #[test]
1530    fn test_compound_shape_add_child() {
1531        let mut compound = CompoundShape::new();
1532        compound.add_child(make_unit_sphere(), LocalTransform::identity());
1533        compound.add_child(
1534            make_box_shape(),
1535            LocalTransform::from_translation([3.0, 0.0, 0.0]),
1536        );
1537        assert_eq!(compound.num_children(), 2);
1538    }
1539
1540    #[test]
1541    fn test_compound_aabb_contains_children() {
1542        let mut compound = CompoundShape::new();
1543        compound.add_child(
1544            PrimitiveShape::Sphere(1.0),
1545            LocalTransform::from_translation([0.0, 0.0, 0.0]),
1546        );
1547        compound.add_child(
1548            PrimitiveShape::Sphere(1.0),
1549            LocalTransform::from_translation([5.0, 0.0, 0.0]),
1550        );
1551        let aabb = compound.aabb();
1552        assert!(
1553            aabb.max[0] >= 6.0,
1554            "AABB should extend to 5+1=6 in x: {}",
1555            aabb.max[0]
1556        );
1557        assert!(
1558            aabb.min[0] <= -1.0,
1559            "AABB should extend to -1 in x: {}",
1560            aabb.min[0]
1561        );
1562    }
1563
1564    #[test]
1565    fn test_compound_center_of_mass() {
1566        let mut compound = CompoundShape::new();
1567        compound.add_child(
1568            PrimitiveShape::Sphere(1.0),
1569            LocalTransform::from_translation([0.0, 0.0, 0.0]),
1570        );
1571        compound.add_child(
1572            PrimitiveShape::Sphere(1.0),
1573            LocalTransform::from_translation([2.0, 0.0, 0.0]),
1574        );
1575        let com = compound.center_of_mass();
1576        assert!(
1577            (com[0] - 1.0).abs() < 1e-6,
1578            "COM should be at x=1 for equal spheres at 0 and 2: {}",
1579            com[0]
1580        );
1581    }
1582
1583    #[test]
1584    fn test_triangle_mesh_normals() {
1585        let mesh = make_tetra();
1586        assert_eq!(mesh.normals.len(), 4, "tetrahedron should have 4 normals");
1587        for n in &mesh.normals {
1588            let len = norm3(*n);
1589            assert!(
1590                (len - 1.0).abs() < 1e-6,
1591                "normal should be unit length: {}",
1592                len
1593            );
1594        }
1595    }
1596
1597    #[test]
1598    fn test_triangle_mesh_surface_area() {
1599        // Flat unit triangle
1600        let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1601        let idxs = vec![[0, 1, 2]];
1602        let mesh = TriangleMeshShape::new(verts, idxs);
1603        let area = mesh.surface_area();
1604        assert!(
1605            (area - 0.5).abs() < 1e-10,
1606            "unit right triangle area should be 0.5: {}",
1607            area
1608        );
1609    }
1610
1611    #[test]
1612    fn test_closest_point_triangle() {
1613        let a = [0.0f64, 0.0, 0.0];
1614        let b = [1.0, 0.0, 0.0];
1615        let c = [0.0, 1.0, 0.0];
1616        let p = [2.0, 0.0, 0.0]; // outside triangle, closest to b
1617        let cp = MeshShapeQuery::closest_point_triangle(p, a, b, c);
1618        assert!(
1619            (cp[0] - 1.0).abs() < 1e-6,
1620            "closest point should be at b=[1,0,0]: {:?}",
1621            cp
1622        );
1623    }
1624
1625    #[test]
1626    fn test_ray_triangle_intersect_hit() {
1627        let a = [0.0, 0.0, 0.0];
1628        let b = [1.0, 0.0, 0.0];
1629        let c = [0.0, 1.0, 0.0];
1630        let origin = [0.25, 0.25, -1.0];
1631        let dir = [0.0, 0.0, 1.0];
1632        let result = MeshShapeQuery::ray_triangle_intersect(origin, dir, a, b, c);
1633        assert!(result.is_some(), "ray should hit triangle");
1634    }
1635
1636    #[test]
1637    fn test_ray_triangle_intersect_miss() {
1638        let a = [0.0, 0.0, 0.0];
1639        let b = [1.0, 0.0, 0.0];
1640        let c = [0.0, 1.0, 0.0];
1641        let origin = [2.0, 2.0, -1.0];
1642        let dir = [0.0, 0.0, 1.0];
1643        let result = MeshShapeQuery::ray_triangle_intersect(origin, dir, a, b, c);
1644        assert!(result.is_none(), "ray should miss triangle");
1645    }
1646
1647    #[test]
1648    fn test_gjk_polytope_support() {
1649        let verts = vec![
1650            [1.0, 0.0, 0.0],
1651            [-1.0, 0.0, 0.0],
1652            [0.0, 1.0, 0.0],
1653            [0.0, -1.0, 0.0],
1654        ];
1655        let poly = GjkPolytope::new(verts);
1656        let s = poly.support([1.0, 0.0, 0.0]);
1657        assert!((s[0] - 1.0).abs() < 1e-10);
1658    }
1659
1660    #[test]
1661    fn test_minkowski_sum_support() {
1662        let a = GjkPolytope::new(vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]]);
1663        let b = GjkPolytope::new(vec![[0.5, 0.0, 0.0], [-0.5, 0.0, 0.0]]);
1664        let mk = MinkowskiSum::new(a, b, [3.0, 0.0, 0.0]);
1665        // s_A([1,0,0]) = [1,0,0], s_B([-1,0,0]) = [-0.5,0,0] (local) + [3,0,0] = [2.5,0,0]
1666        // support_diff = [1,0,0] - [2.5,0,0] = [-1.5,0,0]
1667        let s = mk.support_difference([1.0, 0.0, 0.0]);
1668        let _ = s; // Just check no panic
1669    }
1670
1671    #[test]
1672    fn test_shape_proxy_name() {
1673        let proxy = ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0));
1674        assert_eq!(proxy.name(), "Sphere");
1675        let box_proxy = ShapeProxy::Primitive(PrimitiveShape::Box([1.0; 3]));
1676        assert_eq!(box_proxy.name(), "Box");
1677    }
1678
1679    #[test]
1680    fn test_compound_bvh_build() {
1681        let shapes = vec![
1682            (
1683                ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0)),
1684                LocalTransform::from_translation([0.0, 0.0, 0.0]),
1685            ),
1686            (
1687                ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0)),
1688                LocalTransform::from_translation([5.0, 0.0, 0.0]),
1689            ),
1690        ];
1691        let bvh = CompoundBvh::build(shapes);
1692        assert!(!bvh.nodes.is_empty());
1693    }
1694
1695    #[test]
1696    fn test_compound_bvh_query() {
1697        let shapes = vec![
1698            (
1699                ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0)),
1700                LocalTransform::from_translation([0.0, 0.0, 0.0]),
1701            ),
1702            (
1703                ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0)),
1704                LocalTransform::from_translation([10.0, 0.0, 0.0]),
1705            ),
1706        ];
1707        let bvh = CompoundBvh::build(shapes);
1708        let query = Aabb3::new([-2.0; 3], [2.0; 3]);
1709        let result = bvh.query_aabb(&query);
1710        assert!(
1711            !result.is_empty(),
1712            "query near origin should find first sphere"
1713        );
1714    }
1715
1716    #[test]
1717    fn test_shape_offset_inflates_aabb() {
1718        let inner = ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0));
1719        let offset = ShapeOffset::new(inner, 0.5);
1720        let aabb = offset.aabb();
1721        assert!(
1722            aabb.max[0] >= 1.5,
1723            "offset should inflate AABB to at least 1.5"
1724        );
1725    }
1726
1727    #[test]
1728    fn test_imported_mesh_convex_hull() {
1729        let verts = vec![
1730            [0.0, 0.0, 0.0],
1731            [1.0, 0.0, 0.0],
1732            [0.0, 1.0, 0.0],
1733            [0.0, 0.0, 1.0],
1734        ];
1735        let idxs = vec![[0, 1, 2], [0, 1, 3]];
1736        let mesh = ImportedCollisionMesh::new(verts, idxs, CollisionMeshMode::ConvexHull);
1737        assert!(
1738            !mesh.convex_hull.is_empty(),
1739            "convex hull should be computed"
1740        );
1741    }
1742
1743    #[test]
1744    fn test_imported_mesh_triangle_mode() {
1745        let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1746        let idxs = vec![[0, 1, 2]];
1747        let mesh = ImportedCollisionMesh::new(verts, idxs, CollisionMeshMode::TriangleMesh);
1748        assert!(mesh.mesh.is_some(), "TriangleMesh mode should build mesh");
1749    }
1750
1751    #[test]
1752    fn test_convex_decomposition_basic() {
1753        let verts = vec![
1754            [0.0, 0.0, 0.0],
1755            [1.0, 0.0, 0.0],
1756            [0.0, 1.0, 0.0],
1757            [0.0, 0.0, 1.0],
1758            [1.0, 1.0, 0.0],
1759            [1.0, 0.0, 1.0],
1760            [0.0, 1.0, 1.0],
1761            [1.0, 1.0, 1.0],
1762        ];
1763        let idxs = vec![[0, 1, 2]];
1764        let mut decomp = ConvexDecomposition::new(4, 0.1, 32);
1765        decomp.decompose(&verts, &idxs);
1766        assert!(decomp.num_hulls() > 0, "should produce at least one hull");
1767    }
1768
1769    #[test]
1770    fn test_merge_hulls() {
1771        let hulls = vec![
1772            vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
1773            vec![[2.0, 0.0, 0.0], [3.0, 0.0, 0.0]],
1774        ];
1775        let merged = merge_hulls(&hulls);
1776        assert_eq!(merged.len(), 4, "merged should have all 4 vertices");
1777    }
1778
1779    #[test]
1780    fn test_local_transform_apply() {
1781        let t = LocalTransform::from_translation([1.0, 2.0, 3.0]);
1782        let p = t.apply([0.0; 3]);
1783        assert!((p[0] - 1.0).abs() < 1e-10);
1784        assert!((p[1] - 2.0).abs() < 1e-10);
1785        assert!((p[2] - 3.0).abs() < 1e-10);
1786    }
1787
1788    #[test]
1789    fn test_local_transform_round_trip() {
1790        let t = LocalTransform::from_translation([5.0, -3.0, 2.0]);
1791        let p = [1.0, 2.0, 3.0];
1792        let p2 = t.apply(p);
1793        let p3 = t.inverse_apply(p2);
1794        for i in 0..3 {
1795            assert!(
1796                (p3[i] - p[i]).abs() < 1e-10,
1797                "round trip failed at axis {}",
1798                i
1799            );
1800        }
1801    }
1802}