Skip to main content

oxiphysics_geometry/compound/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6use super::functions::*;
7use crate::shape::Shape;
8use oxiphysics_core::Transform;
9use std::sync::Arc;
10
11/// Classification of primitive shapes for lightweight compound representations.
12#[derive(Debug, Clone, Copy)]
13#[allow(dead_code)]
14pub enum ChildShapeKind {
15    /// A sphere with given radius.
16    Sphere {
17        /// Radius of the sphere.
18        radius: f64,
19    },
20    /// An axis-aligned box with half extents.
21    Box {
22        /// Half extents \[hx, hy, hz\].
23        half_extents: [f64; 3],
24    },
25    /// A capsule (cylinder with hemispherical caps) along the Y axis.
26    Capsule {
27        /// Radius of the capsule.
28        radius: f64,
29        /// Half-height of the cylindrical part.
30        half_height: f64,
31    },
32}
33/// A child shape in a lightweight compound, with center position and shape kind.
34#[derive(Debug, Clone)]
35#[allow(dead_code)]
36pub struct CompoundChild {
37    /// Center position of the child shape.
38    pub center: [f64; 3],
39    /// The kind of primitive shape.
40    pub shape_kind: ChildShapeKind,
41}
42/// All per-child AABBs plus the merged bounding box.
43#[derive(Debug, Clone)]
44#[allow(dead_code)]
45pub struct CompoundAabb {
46    /// Individual child AABBs (min, max).
47    pub all_aabbs: Vec<([f64; 3], [f64; 3])>,
48    /// Merged bounding box over all children.
49    pub merged_min: [f64; 3],
50    /// Merged bounding box over all children.
51    pub merged_max: [f64; 3],
52}
53/// A 3-D rigid transform: translation + 3×3 rotation matrix (row-major).
54///
55/// The rotation `rot[i]` is the i-th row of the rotation matrix.
56/// The matrix must be orthonormal; no validation is performed.
57#[derive(Debug, Clone)]
58#[allow(dead_code)]
59pub struct LocalTransform {
60    /// Translation vector.
61    pub translation: [f64; 3],
62    /// Rotation matrix stored row-major: `rot[row][col]`.
63    pub rot: [[f64; 3]; 3],
64}
65#[allow(dead_code)]
66impl LocalTransform {
67    /// Identity transform (no rotation, no translation).
68    pub fn identity() -> Self {
69        Self {
70            translation: [0.0; 3],
71            rot: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
72        }
73    }
74    /// Pure translation (no rotation).
75    pub fn from_translation(t: [f64; 3]) -> Self {
76        Self {
77            translation: t,
78            rot: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
79        }
80    }
81    /// Transform a point from local space to world space.
82    ///
83    /// `world_p = rot * local_p + translation`
84    pub fn local_to_world(&self, p: [f64; 3]) -> [f64; 3] {
85        let mut out = self.translation;
86        for i in 0..3 {
87            out[i] += self.rot[i][0] * p[0] + self.rot[i][1] * p[1] + self.rot[i][2] * p[2];
88        }
89        out
90    }
91    /// Transform a point from world space to local space.
92    ///
93    /// `local_p = rot^T * (world_p - translation)`
94    pub fn world_to_local(&self, p: [f64; 3]) -> [f64; 3] {
95        let d = [
96            p[0] - self.translation[0],
97            p[1] - self.translation[1],
98            p[2] - self.translation[2],
99        ];
100        [
101            self.rot[0][0] * d[0] + self.rot[1][0] * d[1] + self.rot[2][0] * d[2],
102            self.rot[0][1] * d[0] + self.rot[1][1] * d[1] + self.rot[2][1] * d[2],
103            self.rot[0][2] * d[0] + self.rot[1][2] * d[1] + self.rot[2][2] * d[2],
104        ]
105    }
106    /// Transform a direction vector from local space to world space (no translation).
107    pub fn local_to_world_dir(&self, v: [f64; 3]) -> [f64; 3] {
108        [
109            self.rot[0][0] * v[0] + self.rot[0][1] * v[1] + self.rot[0][2] * v[2],
110            self.rot[1][0] * v[0] + self.rot[1][1] * v[1] + self.rot[1][2] * v[2],
111            self.rot[2][0] * v[0] + self.rot[2][1] * v[1] + self.rot[2][2] * v[2],
112        ]
113    }
114    /// Transform a direction vector from world space to local space (no translation).
115    pub fn world_to_local_dir(&self, v: [f64; 3]) -> [f64; 3] {
116        [
117            self.rot[0][0] * v[0] + self.rot[1][0] * v[1] + self.rot[2][0] * v[2],
118            self.rot[0][1] * v[0] + self.rot[1][1] * v[1] + self.rot[2][1] * v[2],
119            self.rot[0][2] * v[0] + self.rot[1][2] * v[1] + self.rot[2][2] * v[2],
120        ]
121    }
122}
123/// A `CompoundShape` variant that stores each child with an explicit
124/// [`LocalTransform`], enabling full 6-DOF placement (translation + rotation).
125#[derive(Debug, Clone)]
126#[allow(dead_code)]
127pub struct CompoundShapeEx {
128    /// Children: (transform, shape kind).
129    pub children: Vec<(LocalTransform, ChildShapeKind)>,
130}
131impl Default for CompoundShapeEx {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136#[allow(dead_code)]
137impl CompoundShapeEx {
138    /// Create an empty compound shape.
139    pub fn new() -> Self {
140        Self {
141            children: Vec::new(),
142        }
143    }
144    /// Add a sphere child with a local transform.
145    pub fn add_sphere(&mut self, transform: LocalTransform, radius: f64) {
146        self.children
147            .push((transform, ChildShapeKind::Sphere { radius }));
148    }
149    /// Add a box child with a local transform.
150    pub fn add_box(&mut self, transform: LocalTransform, half_extents: [f64; 3]) {
151        self.children
152            .push((transform, ChildShapeKind::Box { half_extents }));
153    }
154    /// Add a capsule child with a local transform.
155    pub fn add_capsule(&mut self, transform: LocalTransform, radius: f64, half_height: f64) {
156        self.children.push((
157            transform,
158            ChildShapeKind::Capsule {
159                radius,
160                half_height,
161            },
162        ));
163    }
164    /// Compute the union AABB of all children in world space.
165    ///
166    /// Returns `(min, max)` as `[f64; 3]` arrays.
167    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
168        if self.children.is_empty() {
169            return ([0.0; 3], [0.0; 3]);
170        }
171        let mut min = [f64::INFINITY; 3];
172        let mut max = [f64::NEG_INFINITY; 3];
173        for (transform, kind) in &self.children {
174            let local_he = match kind {
175                ChildShapeKind::Sphere { radius } => [*radius, *radius, *radius],
176                ChildShapeKind::Box { half_extents } => *half_extents,
177                ChildShapeKind::Capsule {
178                    radius,
179                    half_height,
180                } => [*radius, half_height + radius, *radius],
181            };
182            let corners_local = [
183                [-local_he[0], -local_he[1], -local_he[2]],
184                [local_he[0], -local_he[1], -local_he[2]],
185                [-local_he[0], local_he[1], -local_he[2]],
186                [local_he[0], local_he[1], -local_he[2]],
187                [-local_he[0], -local_he[1], local_he[2]],
188                [local_he[0], -local_he[1], local_he[2]],
189                [-local_he[0], local_he[1], local_he[2]],
190                [local_he[0], local_he[1], local_he[2]],
191            ];
192            for corner in &corners_local {
193                let w = transform.local_to_world(*corner);
194                for i in 0..3 {
195                    if w[i] < min[i] {
196                        min[i] = w[i];
197                    }
198                    if w[i] > max[i] {
199                        max[i] = w[i];
200                    }
201                }
202            }
203        }
204        (min, max)
205    }
206    /// Test if a world-space point is inside any child shape.
207    pub fn contains_point(&self, p: [f64; 3]) -> bool {
208        for (transform, kind) in &self.children {
209            let local_p = transform.world_to_local(p);
210            if child_kind_contains(kind, local_p) {
211                return true;
212            }
213        }
214        false
215    }
216    /// Ray cast against all children; returns `(toi, normal)` for the nearest hit.
217    pub fn ray_cast(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<(f64, [f64; 3])> {
218        let mut best: Option<(f64, [f64; 3])> = None;
219        for (transform, kind) in &self.children {
220            let local_o = transform.world_to_local(origin);
221            let local_d = transform.world_to_local_dir(dir);
222            if let Some((t, local_n)) = ray_cast_kind(kind, local_o, local_d, f64::MAX * 0.5) {
223                let world_n = transform.local_to_world_dir(local_n);
224                if best.as_ref().is_none_or(|(bt, _)| t < *bt) {
225                    best = Some((t, world_n));
226                }
227            }
228        }
229        best
230    }
231    /// Total volume of all children.
232    pub fn volume(&self) -> f64 {
233        self.children
234            .iter()
235            .map(|(_, k)| CompoundShape::child_volume(k))
236            .sum()
237    }
238    /// Compute the inertia tensor about the world-origin using the parallel axis theorem.
239    ///
240    /// Assumes uniform density `density`.
241    #[allow(clippy::too_many_arguments)]
242    pub fn inertia_tensor(&self, density: f64) -> [[f64; 3]; 3] {
243        let mut i_xx = 0.0f64;
244        let mut i_yy = 0.0f64;
245        let mut i_zz = 0.0f64;
246        let mut i_xy = 0.0f64;
247        let mut i_xz = 0.0f64;
248        let mut i_yz = 0.0f64;
249        for (transform, kind) in &self.children {
250            let vol = CompoundShape::child_volume(kind);
251            let m = density * vol;
252            let (lxx, lyy, lzz) = CompoundShape::child_local_inertia(kind, m);
253            let r = transform.local_to_world([0.0; 3]);
254            let r2 = r[0] * r[0] + r[1] * r[1] + r[2] * r[2];
255            i_xx += lxx + m * (r2 - r[0] * r[0]);
256            i_yy += lyy + m * (r2 - r[1] * r[1]);
257            i_zz += lzz + m * (r2 - r[2] * r[2]);
258            i_xy -= m * r[0] * r[1];
259            i_xz -= m * r[0] * r[2];
260            i_yz -= m * r[1] * r[2];
261        }
262        [[i_xx, i_xy, i_xz], [i_xy, i_yy, i_yz], [i_xz, i_yz, i_zz]]
263    }
264}
265/// A compound shape made of multiple sub-shapes, each with its own transform.
266#[derive(Debug, Clone)]
267pub struct Compound {
268    /// Sub-shapes with their local transforms.
269    pub children: Vec<(Transform, Arc<dyn Shape>)>,
270}
271impl Compound {
272    /// Create a new compound shape.
273    pub fn new(children: Vec<(Transform, Arc<dyn Shape>)>) -> Self {
274        Self { children }
275    }
276}
277/// A compound shape made of multiple lightweight primitive children.
278///
279/// Unlike `Compound` which uses `Arc<dyn Shape>`, this uses concrete enum
280/// variants for common shapes, avoiding dynamic dispatch and allocations.
281#[derive(Debug, Clone)]
282#[allow(dead_code)]
283pub struct CompoundShape {
284    /// The child shapes.
285    pub children: Vec<CompoundChild>,
286}
287impl Default for CompoundShape {
288    fn default() -> Self {
289        Self::new()
290    }
291}
292#[allow(dead_code)]
293impl CompoundShape {
294    /// Create an empty compound shape.
295    pub fn new() -> Self {
296        Self {
297            children: Vec::new(),
298        }
299    }
300    /// Add a sphere child.
301    pub fn add_sphere(&mut self, center: [f64; 3], radius: f64) {
302        self.children.push(CompoundChild {
303            center,
304            shape_kind: ChildShapeKind::Sphere { radius },
305        });
306    }
307    /// Add a box child.
308    pub fn add_box(&mut self, center: [f64; 3], half_extents: [f64; 3]) {
309        self.children.push(CompoundChild {
310            center,
311            shape_kind: ChildShapeKind::Box { half_extents },
312        });
313    }
314    /// Add a capsule child.
315    pub fn add_capsule(&mut self, center: [f64; 3], radius: f64, half_height: f64) {
316        self.children.push(CompoundChild {
317            center,
318            shape_kind: ChildShapeKind::Capsule {
319                radius,
320                half_height,
321            },
322        });
323    }
324    /// Return the number of children.
325    pub fn child_count(&self) -> usize {
326        self.children.len()
327    }
328    /// Compute the volume of a single child shape kind.
329    fn child_volume(kind: &ChildShapeKind) -> f64 {
330        match kind {
331            ChildShapeKind::Sphere { radius } => {
332                (4.0 / 3.0) * std::f64::consts::PI * radius * radius * radius
333            }
334            ChildShapeKind::Box { half_extents } => {
335                8.0 * half_extents[0] * half_extents[1] * half_extents[2]
336            }
337            ChildShapeKind::Capsule {
338                radius,
339                half_height,
340            } => {
341                let sphere_vol = (4.0 / 3.0) * std::f64::consts::PI * radius * radius * radius;
342                let cyl_vol = std::f64::consts::PI * radius * radius * 2.0 * half_height;
343                sphere_vol + cyl_vol
344            }
345        }
346    }
347    /// Compute total volume of all children (no overlap correction).
348    pub fn total_volume(&self) -> f64 {
349        self.children
350            .iter()
351            .map(|c| Self::child_volume(&c.shape_kind))
352            .sum()
353    }
354    /// Compute axis-aligned bounding box of all children.
355    ///
356    /// Returns `(min, max)` as `[f64; 3]` arrays.
357    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
358        if self.children.is_empty() {
359            return ([0.0; 3], [0.0; 3]);
360        }
361        let mut min = [f64::INFINITY; 3];
362        let mut max = [f64::NEG_INFINITY; 3];
363        for child in &self.children {
364            let (cmin, cmax) = Self::child_aabb(child);
365            for i in 0..3 {
366                if cmin[i] < min[i] {
367                    min[i] = cmin[i];
368                }
369                if cmax[i] > max[i] {
370                    max[i] = cmax[i];
371                }
372            }
373        }
374        (min, max)
375    }
376    /// Compute the AABB of a single child.
377    fn child_aabb(child: &CompoundChild) -> ([f64; 3], [f64; 3]) {
378        let c = child.center;
379        match child.shape_kind {
380            ChildShapeKind::Sphere { radius } => (
381                [c[0] - radius, c[1] - radius, c[2] - radius],
382                [c[0] + radius, c[1] + radius, c[2] + radius],
383            ),
384            ChildShapeKind::Box { half_extents } => (
385                [
386                    c[0] - half_extents[0],
387                    c[1] - half_extents[1],
388                    c[2] - half_extents[2],
389                ],
390                [
391                    c[0] + half_extents[0],
392                    c[1] + half_extents[1],
393                    c[2] + half_extents[2],
394                ],
395            ),
396            ChildShapeKind::Capsule {
397                radius,
398                half_height,
399            } => (
400                [c[0] - radius, c[1] - half_height - radius, c[2] - radius],
401                [c[0] + radius, c[1] + half_height + radius, c[2] + radius],
402            ),
403        }
404    }
405    /// Compute volume-weighted center of mass.
406    pub fn center_of_mass(&self) -> [f64; 3] {
407        let total_vol = self.total_volume();
408        if total_vol < 1e-12 {
409            return [0.0; 3];
410        }
411        let mut com = [0.0; 3];
412        for child in &self.children {
413            let v = Self::child_volume(&child.shape_kind);
414            for i in 0..3 {
415                com[i] += child.center[i] * v;
416            }
417        }
418        for i in 0..3 {
419            com[i] /= total_vol;
420        }
421        com
422    }
423    /// Test if a point is inside any child shape.
424    pub fn contains_point(&self, p: [f64; 3]) -> bool {
425        for child in &self.children {
426            if Self::child_contains(child, p) {
427                return true;
428            }
429        }
430        false
431    }
432    /// Check if a single child contains a point.
433    fn child_contains(child: &CompoundChild, p: [f64; 3]) -> bool {
434        let dx = p[0] - child.center[0];
435        let dy = p[1] - child.center[1];
436        let dz = p[2] - child.center[2];
437        match child.shape_kind {
438            ChildShapeKind::Sphere { radius } => dx * dx + dy * dy + dz * dz <= radius * radius,
439            ChildShapeKind::Box { half_extents } => {
440                dx.abs() <= half_extents[0]
441                    && dy.abs() <= half_extents[1]
442                    && dz.abs() <= half_extents[2]
443            }
444            ChildShapeKind::Capsule {
445                radius,
446                half_height,
447            } => {
448                let clamped_y = dy.clamp(-half_height, half_height);
449                let ry = dy - clamped_y;
450                dx * dx + ry * ry + dz * dz <= radius * radius
451            }
452        }
453    }
454    /// Ray cast against all children, returning `(toi, normal, child_index)` for
455    /// the closest hit within `max_toi`.
456    pub fn ray_cast(
457        &self,
458        origin: [f64; 3],
459        dir: [f64; 3],
460        max_toi: f64,
461    ) -> Option<(f64, [f64; 3], usize)> {
462        let mut best: Option<(f64, [f64; 3], usize)> = None;
463        for (idx, child) in self.children.iter().enumerate() {
464            if let Some((t, n)) = Self::ray_cast_child(child, origin, dir, max_toi)
465                && best.as_ref().is_none_or(|(bt, _, _)| t < *bt)
466            {
467                best = Some((t, n, idx));
468            }
469        }
470        best
471    }
472    /// Ray cast against a single child.
473    fn ray_cast_child(
474        child: &CompoundChild,
475        origin: [f64; 3],
476        dir: [f64; 3],
477        max_toi: f64,
478    ) -> Option<(f64, [f64; 3])> {
479        let lo = [
480            origin[0] - child.center[0],
481            origin[1] - child.center[1],
482            origin[2] - child.center[2],
483        ];
484        match child.shape_kind {
485            ChildShapeKind::Sphere { radius } => ray_sphere(lo, dir, radius, max_toi),
486            ChildShapeKind::Box { half_extents } => ray_box(lo, dir, half_extents, max_toi),
487            ChildShapeKind::Capsule {
488                radius,
489                half_height,
490            } => ray_capsule(lo, dir, radius, half_height, max_toi),
491        }
492    }
493}
494impl CompoundShape {
495    /// Compute the inertia tensor of the compound shape about its center of mass.
496    ///
497    /// Uses the parallel axis theorem. `density` is the uniform mass density.
498    pub fn inertia_tensor(&self, density: f64) -> [[f64; 3]; 3] {
499        let com = self.center_of_mass();
500        let mut i_xx = 0.0f64;
501        let mut i_yy = 0.0f64;
502        let mut i_zz = 0.0f64;
503        let mut i_xy = 0.0f64;
504        let mut i_xz = 0.0f64;
505        let mut i_yz = 0.0f64;
506        for child in &self.children {
507            let vol = Self::child_volume(&child.shape_kind);
508            let m = density * vol;
509            let (lxx, lyy, lzz) = Self::child_local_inertia(&child.shape_kind, m);
510            let r = [
511                child.center[0] - com[0],
512                child.center[1] - com[1],
513                child.center[2] - com[2],
514            ];
515            let r2 = r[0] * r[0] + r[1] * r[1] + r[2] * r[2];
516            i_xx += lxx + m * (r2 - r[0] * r[0]);
517            i_yy += lyy + m * (r2 - r[1] * r[1]);
518            i_zz += lzz + m * (r2 - r[2] * r[2]);
519            i_xy -= m * r[0] * r[1];
520            i_xz -= m * r[0] * r[2];
521            i_yz -= m * r[1] * r[2];
522        }
523        [[i_xx, i_xy, i_xz], [i_xy, i_yy, i_yz], [i_xz, i_yz, i_zz]]
524    }
525    /// Local principal inertia components (I_xx, I_yy, I_zz) for a child at its own COM.
526    fn child_local_inertia(kind: &ChildShapeKind, mass: f64) -> (f64, f64, f64) {
527        match kind {
528            ChildShapeKind::Sphere { radius } => {
529                let i = 2.0 / 5.0 * mass * radius * radius;
530                (i, i, i)
531            }
532            ChildShapeKind::Box { half_extents } => {
533                let [hx, hy, hz] = *half_extents;
534                let i_xx = mass / 3.0 * (hy * hy + hz * hz);
535                let i_yy = mass / 3.0 * (hx * hx + hz * hz);
536                let i_zz = mass / 3.0 * (hx * hx + hy * hy);
537                (i_xx, i_yy, i_zz)
538            }
539            ChildShapeKind::Capsule {
540                radius,
541                half_height,
542            } => {
543                let r = radius;
544                let h = half_height * 2.0;
545                let m_cyl = mass * std::f64::consts::PI * r * r * h
546                    / (std::f64::consts::PI * r * r * h
547                        + (4.0 / 3.0) * std::f64::consts::PI * r * r * r);
548                let m_hemi = (mass - m_cyl) / 2.0;
549                let i_cyl_xx = m_cyl * (3.0 * r * r + h * h) / 12.0;
550                let i_hemi_xx = m_hemi * (2.0 * r * r / 5.0 + (3.0 * half_height / 8.0).powi(2));
551                let i_xx = i_cyl_xx + 2.0 * i_hemi_xx;
552                let i_yy = m_cyl * r * r / 2.0 + 2.0 * m_hemi * 2.0 * r * r / 5.0;
553                (i_xx, i_yy, i_xx)
554            }
555        }
556    }
557    /// Compute the bounding sphere (center, radius) of the compound shape.
558    pub fn bounding_sphere(&self) -> ([f64; 3], f64) {
559        let com = self.center_of_mass();
560        let mut max_r = 0.0f64;
561        for child in &self.children {
562            let child_r = self.child_bounding_radius(child);
563            let dist_to_com = {
564                let dx = child.center[0] - com[0];
565                let dy = child.center[1] - com[1];
566                let dz = child.center[2] - com[2];
567                (dx * dx + dy * dy + dz * dz).sqrt()
568            };
569            let r = dist_to_com + child_r;
570            if r > max_r {
571                max_r = r;
572            }
573        }
574        (com, max_r)
575    }
576    fn child_bounding_radius(&self, child: &CompoundChild) -> f64 {
577        match child.shape_kind {
578            ChildShapeKind::Sphere { radius } => radius,
579            ChildShapeKind::Box { half_extents } => {
580                (half_extents[0].powi(2) + half_extents[1].powi(2) + half_extents[2].powi(2)).sqrt()
581            }
582            ChildShapeKind::Capsule {
583                radius,
584                half_height,
585            } => half_height + radius,
586        }
587    }
588    /// Apply a uniform scaling to all child shape positions and sizes.
589    pub fn scale(&mut self, factor: f64) {
590        for child in &mut self.children {
591            child.center[0] *= factor;
592            child.center[1] *= factor;
593            child.center[2] *= factor;
594            match &mut child.shape_kind {
595                ChildShapeKind::Sphere { radius } => *radius *= factor,
596                ChildShapeKind::Box { half_extents } => {
597                    half_extents[0] *= factor;
598                    half_extents[1] *= factor;
599                    half_extents[2] *= factor;
600                }
601                ChildShapeKind::Capsule {
602                    radius,
603                    half_height,
604                } => {
605                    *radius *= factor;
606                    *half_height *= factor;
607                }
608            }
609        }
610    }
611    /// Translate all child centers by the given offset.
612    pub fn translate(&mut self, offset: [f64; 3]) {
613        for child in &mut self.children {
614            child.center[0] += offset[0];
615            child.center[1] += offset[1];
616            child.center[2] += offset[2];
617        }
618    }
619    /// Returns a new compound shape merged with another (concatenation of children).
620    pub fn merge_with(&self, other: &CompoundShape) -> CompoundShape {
621        let mut result = self.clone();
622        result.children.extend(other.children.iter().cloned());
623        result
624    }
625    /// Compute the overlap (intersection test) with a sphere at `center` with `radius`.
626    ///
627    /// Returns true if any child overlaps the query sphere.
628    pub fn overlaps_sphere(&self, center: [f64; 3], radius: f64) -> bool {
629        for child in &self.children {
630            let dx = child.center[0] - center[0];
631            let dy = child.center[1] - center[1];
632            let dz = child.center[2] - center[2];
633            let dist = (dx * dx + dy * dy + dz * dz).sqrt();
634            let child_r = self.child_bounding_radius(child);
635            if dist < child_r + radius {
636                return true;
637            }
638        }
639        false
640    }
641    /// Recursively ray-cast with early exit once `max_hits` are found.
642    pub fn ray_cast_all(
643        &self,
644        origin: [f64; 3],
645        dir: [f64; 3],
646        max_toi: f64,
647    ) -> Vec<(f64, [f64; 3], usize)> {
648        let mut hits: Vec<(f64, [f64; 3], usize)> = Vec::new();
649        for (idx, child) in self.children.iter().enumerate() {
650            if let Some((t, n)) = Self::ray_cast_child(child, origin, dir, max_toi) {
651                hits.push((t, n, idx));
652            }
653        }
654        hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
655        hits
656    }
657}
658#[allow(dead_code)]
659impl CompoundShape {
660    /// Compute merged AABB over all children.
661    pub fn merged_aabb(&self) -> ([f64; 3], [f64; 3]) {
662        self.aabb()
663    }
664    /// Ray cast returning `(t, child_index)` for the closest hit within `max_t`.
665    ///
666    /// Unlike `ray_cast` this omits the normal to match the requested signature.
667    pub fn raycast(
668        &self,
669        ray_origin: [f64; 3],
670        ray_dir: [f64; 3],
671        max_t: f64,
672    ) -> Option<(f64, usize)> {
673        self.ray_cast(ray_origin, ray_dir, max_t)
674            .map(|(t, _n, idx)| (t, idx))
675    }
676    /// Total volume of all children (sum of child volumes).
677    pub fn volume(&self) -> f64 {
678        self.total_volume()
679    }
680    /// Mass-weighted center of mass.
681    ///
682    /// `masses[i]` is the mass of child `i`.  If `masses` is shorter than
683    /// `children`, remaining children have zero mass.
684    pub fn center_of_mass_weighted(&self, masses: &[f64]) -> [f64; 3] {
685        let total: f64 = masses.iter().copied().take(self.children.len()).sum();
686        if total < 1e-30 {
687            return [0.0; 3];
688        }
689        let mut com = [0.0f64; 3];
690        for (i, child) in self.children.iter().enumerate() {
691            let m = if i < masses.len() { masses[i] } else { 0.0 };
692            for k in 0..3 {
693                com[k] += m * child.center[k];
694            }
695        }
696        for k in 0..3 {
697            com[k] /= total;
698        }
699        com
700    }
701    /// Inertia tensor about the compound center of mass using the parallel axis theorem.
702    ///
703    /// `masses[i]` is the mass of child `i`.
704    pub fn inertia_tensor_from_masses(&self, masses: &[f64]) -> [[f64; 3]; 3] {
705        let com = self.center_of_mass_weighted(masses);
706        let mut i_xx = 0.0f64;
707        let mut i_yy = 0.0f64;
708        let mut i_zz = 0.0f64;
709        let mut i_xy = 0.0f64;
710        let mut i_xz = 0.0f64;
711        let mut i_yz = 0.0f64;
712        for (idx, child) in self.children.iter().enumerate() {
713            let m = if idx < masses.len() { masses[idx] } else { 0.0 };
714            let (lxx, lyy, lzz) = Self::child_local_inertia(&child.shape_kind, m);
715            let r = [
716                child.center[0] - com[0],
717                child.center[1] - com[1],
718                child.center[2] - com[2],
719            ];
720            let r2 = r[0] * r[0] + r[1] * r[1] + r[2] * r[2];
721            i_xx += lxx + m * (r2 - r[0] * r[0]);
722            i_yy += lyy + m * (r2 - r[1] * r[1]);
723            i_zz += lzz + m * (r2 - r[2] * r[2]);
724            i_xy -= m * r[0] * r[1];
725            i_xz -= m * r[0] * r[2];
726            i_yz -= m * r[1] * r[2];
727        }
728        [[i_xx, i_xy, i_xz], [i_xy, i_yy, i_yz], [i_xz, i_yz, i_zz]]
729    }
730    /// Closest surface point to `p` among all children.
731    ///
732    /// Returns `(closest_point, child_index)`.
733    pub fn closest_point(&self, p: [f64; 3]) -> ([f64; 3], usize) {
734        let mut best_dist = f64::INFINITY;
735        let mut best_pt = p;
736        let mut best_idx = 0usize;
737        for (i, child) in self.children.iter().enumerate() {
738            let cp = Self::child_closest_point(child, p);
739            let dx = cp[0] - p[0];
740            let dy = cp[1] - p[1];
741            let dz = cp[2] - p[2];
742            let dist = (dx * dx + dy * dy + dz * dz).sqrt();
743            if dist < best_dist {
744                best_dist = dist;
745                best_pt = cp;
746                best_idx = i;
747            }
748        }
749        (best_pt, best_idx)
750    }
751    fn child_closest_point(child: &CompoundChild, p: [f64; 3]) -> [f64; 3] {
752        let c = child.center;
753        match child.shape_kind {
754            ChildShapeKind::Sphere { radius } => {
755                let dx = p[0] - c[0];
756                let dy = p[1] - c[1];
757                let dz = p[2] - c[2];
758                let dist = (dx * dx + dy * dy + dz * dz).sqrt();
759                if dist < 1e-30 {
760                    [c[0] + radius, c[1], c[2]]
761                } else {
762                    let scale = radius / dist;
763                    [c[0] + dx * scale, c[1] + dy * scale, c[2] + dz * scale]
764                }
765            }
766            ChildShapeKind::Box { half_extents } => {
767                let clamped = [
768                    (p[0] - c[0]).clamp(-half_extents[0], half_extents[0]) + c[0],
769                    (p[1] - c[1]).clamp(-half_extents[1], half_extents[1]) + c[1],
770                    (p[2] - c[2]).clamp(-half_extents[2], half_extents[2]) + c[2],
771                ];
772                let inside = (0..3).all(|i| {
773                    let he = [half_extents[0], half_extents[1], half_extents[2]][i];
774                    let d = [p[0] - c[0], p[1] - c[1], p[2] - c[2]][i].abs();
775                    d <= he
776                });
777                if inside {
778                    let dx_neg = [half_extents[0], half_extents[1], half_extents[2]][0]
779                        - (p[0] - c[0]).abs();
780                    let dy_neg = [half_extents[0], half_extents[1], half_extents[2]][1]
781                        - (p[1] - c[1]).abs();
782                    let dz_neg = [half_extents[0], half_extents[1], half_extents[2]][2]
783                        - (p[2] - c[2]).abs();
784                    if dx_neg <= dy_neg && dx_neg <= dz_neg {
785                        let sx = if p[0] >= c[0] { 1.0 } else { -1.0 };
786                        [c[0] + half_extents[0] * sx, p[1], p[2]]
787                    } else if dy_neg <= dx_neg && dy_neg <= dz_neg {
788                        let sy = if p[1] >= c[1] { 1.0 } else { -1.0 };
789                        [p[0], c[1] + half_extents[1] * sy, p[2]]
790                    } else {
791                        let sz = if p[2] >= c[2] { 1.0 } else { -1.0 };
792                        [p[0], p[1], c[2] + half_extents[2] * sz]
793                    }
794                } else {
795                    clamped
796                }
797            }
798            ChildShapeKind::Capsule {
799                radius,
800                half_height,
801            } => {
802                let dy = p[1] - c[1];
803                let clamped_y = dy.clamp(-half_height, half_height);
804                let axis_pt = [c[0], c[1] + clamped_y, c[2]];
805                let dx = p[0] - axis_pt[0];
806                let dpz = p[2] - axis_pt[2];
807                let dist_xz = (dx * dx + dpz * dpz).sqrt();
808                if dist_xz < 1e-30 {
809                    [axis_pt[0] + radius, axis_pt[1], axis_pt[2]]
810                } else {
811                    let scale = radius / dist_xz;
812                    [
813                        axis_pt[0] + dx * scale,
814                        axis_pt[1],
815                        axis_pt[2] + dpz * scale,
816                    ]
817                }
818            }
819        }
820    }
821}
822#[allow(dead_code)]
823impl CompoundShape {
824    /// Remove the child at the given index.  Panics if `index` is out of range.
825    pub fn remove_child(&mut self, index: usize) {
826        self.children.remove(index);
827    }
828    /// Remove the child at the given index by swapping with the last element.
829    ///
830    /// Faster than `remove_child` (O(1) vs O(n)) but changes the order of
831    /// remaining children.
832    pub fn swap_remove_child(&mut self, index: usize) {
833        self.children.swap_remove(index);
834    }
835    /// Replace the child at `index` with a new sphere.
836    pub fn replace_with_sphere(&mut self, index: usize, center: [f64; 3], radius: f64) {
837        self.children[index] = CompoundChild {
838            center,
839            shape_kind: ChildShapeKind::Sphere { radius },
840        };
841    }
842    /// Replace the child at `index` with a new box.
843    pub fn replace_with_box(&mut self, index: usize, center: [f64; 3], half_extents: [f64; 3]) {
844        self.children[index] = CompoundChild {
845            center,
846            shape_kind: ChildShapeKind::Box { half_extents },
847        };
848    }
849    /// Return `true` if the compound shape has no children.
850    pub fn is_empty(&self) -> bool {
851        self.children.is_empty()
852    }
853    /// Clear all children.
854    pub fn clear(&mut self) {
855        self.children.clear();
856    }
857    /// Closest surface point among all children together with the squared
858    /// distance and the child index.
859    ///
860    /// Returns `(closest_point, squared_distance, child_index)`.
861    pub fn closest_point_with_dist2(&self, p: [f64; 3]) -> ([f64; 3], f64, usize) {
862        let mut best_dist2 = f64::INFINITY;
863        let mut best_pt = p;
864        let mut best_idx = 0usize;
865        for (i, child) in self.children.iter().enumerate() {
866            let cp = Self::child_closest_point(child, p);
867            let dx = cp[0] - p[0];
868            let dy = cp[1] - p[1];
869            let dz = cp[2] - p[2];
870            let d2 = dx * dx + dy * dy + dz * dz;
871            if d2 < best_dist2 {
872                best_dist2 = d2;
873                best_pt = cp;
874                best_idx = i;
875            }
876        }
877        (best_pt, best_dist2, best_idx)
878    }
879    /// Check if two `CompoundShape` instances have any overlapping pair of
880    /// children using conservative bounding-sphere overlap tests.
881    ///
882    /// Returns the index pairs `(i, j)` of all overlapping child pairs.
883    pub fn broad_phase_pairs(&self, other: &CompoundShape) -> Vec<(usize, usize)> {
884        let mut pairs = Vec::new();
885        for (i, ci) in self.children.iter().enumerate() {
886            let ri = self.child_bounding_radius(ci);
887            for (j, cj) in other.children.iter().enumerate() {
888                let rj = other.child_bounding_radius(cj);
889                let dx = ci.center[0] - cj.center[0];
890                let dy = ci.center[1] - cj.center[1];
891                let dz = ci.center[2] - cj.center[2];
892                let dist = (dx * dx + dy * dy + dz * dz).sqrt();
893                if dist < ri + rj {
894                    pairs.push((i, j));
895                }
896            }
897        }
898        pairs
899    }
900    /// Test if two `CompoundShape` instances overlap at all (broad phase).
901    pub fn overlaps_compound(&self, other: &CompoundShape) -> bool {
902        !self.broad_phase_pairs(other).is_empty()
903    }
904    /// Compute the centroid of the compound (volume-weighted center) using
905    /// per-child densities.
906    ///
907    /// `densities[i]` is the density for child `i`.  If `densities` is shorter
908    /// than `children`, remaining children use density 1.0.
909    pub fn centroid_with_densities(&self, densities: &[f64]) -> [f64; 3] {
910        let mut total_mass = 0.0f64;
911        let mut com = [0.0f64; 3];
912        for (i, child) in self.children.iter().enumerate() {
913            let rho = if i < densities.len() {
914                densities[i]
915            } else {
916                1.0
917            };
918            let vol = Self::child_volume(&child.shape_kind);
919            let m = rho * vol;
920            total_mass += m;
921            for k in 0..3 {
922                com[k] += m * child.center[k];
923            }
924        }
925        if total_mass < 1e-30 {
926            return [0.0; 3];
927        }
928        for k in 0..3 {
929            com[k] /= total_mass;
930        }
931        com
932    }
933    /// Approximate penetration depth between this compound and a sphere at
934    /// `center` with `radius`.
935    ///
936    /// For each child, computes the signed distance to the child's surface
937    /// (negative inside).  Returns the minimum signed distance (most negative
938    /// = deepest penetration) together with the child index.
939    ///
940    /// Returns `None` if there is no penetration.
941    pub fn penetration_depth_sphere(&self, center: [f64; 3], radius: f64) -> Option<(f64, usize)> {
942        let mut best: Option<(f64, usize)> = None;
943        for (i, child) in self.children.iter().enumerate() {
944            let signed = self.signed_distance_child(child, center, radius);
945            if signed < 0.0 && best.as_ref().is_none_or(|(bd, _)| signed < *bd) {
946                best = Some((signed, i));
947            }
948        }
949        best
950    }
951    /// Signed distance between an external sphere and a single child.
952    ///
953    /// Negative when the sphere penetrates the child.
954    fn signed_distance_child(
955        &self,
956        child: &CompoundChild,
957        sphere_center: [f64; 3],
958        sphere_radius: f64,
959    ) -> f64 {
960        let cp = Self::child_closest_point(child, sphere_center);
961        let dx = cp[0] - sphere_center[0];
962        let dy = cp[1] - sphere_center[1];
963        let dz = cp[2] - sphere_center[2];
964        let dist = (dx * dx + dy * dy + dz * dz).sqrt();
965        dist - sphere_radius
966    }
967    /// Compute per-child masses from a uniform density.
968    pub fn child_masses(&self, density: f64) -> Vec<f64> {
969        self.children
970            .iter()
971            .map(|c| density * Self::child_volume(&c.shape_kind))
972            .collect()
973    }
974    /// Total mass of the compound given uniform density.
975    pub fn total_mass(&self, density: f64) -> f64 {
976        density * self.total_volume()
977    }
978    /// Axis-aligned bounding box of a single child (public accessor).
979    pub fn child_aabb_public(child: &CompoundChild) -> ([f64; 3], [f64; 3]) {
980        Self::child_aabb(child)
981    }
982    /// Compute the AABB expanded by a margin `margin` on all sides.
983    pub fn expanded_aabb(&self, margin: f64) -> ([f64; 3], [f64; 3]) {
984        let (mn, mx) = self.aabb();
985        (
986            [mn[0] - margin, mn[1] - margin, mn[2] - margin],
987            [mx[0] + margin, mx[1] + margin, mx[2] + margin],
988        )
989    }
990    /// Test if a sphere (center, radius) overlaps the compound's AABB.
991    pub fn sphere_overlaps_aabb(&self, center: [f64; 3], radius: f64) -> bool {
992        if self.children.is_empty() {
993            return false;
994        }
995        let (mn, mx) = self.aabb();
996        let cx = center[0].clamp(mn[0], mx[0]);
997        let cy = center[1].clamp(mn[1], mx[1]);
998        let cz = center[2].clamp(mn[2], mx[2]);
999        let dx = cx - center[0];
1000        let dy = cy - center[1];
1001        let dz = cz - center[2];
1002        dx * dx + dy * dy + dz * dz <= radius * radius
1003    }
1004}