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