Skip to main content

oxiphysics_geometry/
capsule.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Capsule shape (sphere-swept line segment along Y axis).
5
6use crate::shape::{RayHit, Shape};
7use oxiphysics_core::Aabb;
8use oxiphysics_core::math::{Mat3, Real, Vec3};
9use std::f64::consts::PI;
10
11/// A capsule defined by radius and half-height along the Y axis.
12#[derive(Debug, Clone)]
13pub struct Capsule {
14    /// Radius of the capsule end-caps.
15    pub radius: Real,
16    /// Half-height of the cylindrical segment.
17    pub half_height: Real,
18}
19
20impl Capsule {
21    /// Create a new capsule.
22    pub fn new(radius: Real, half_height: Real) -> Self {
23        Self {
24            radius,
25            half_height,
26        }
27    }
28
29    /// Volume: cylinder + two hemisphere caps = πr²h + (4/3)πr³.
30    pub fn volume_explicit(&self) -> Real {
31        let r = self.radius;
32        let h = 2.0 * self.half_height;
33        PI * r * r * h + (4.0 / 3.0) * PI * r.powi(3)
34    }
35
36    /// Surface area: 2πrh (cylinder lateral) + 4πr² (two hemispheres).
37    pub fn surface_area(&self) -> Real {
38        let r = self.radius;
39        let h = 2.0 * self.half_height;
40        2.0 * PI * r * h + 4.0 * PI * r * r
41    }
42
43    /// Inertia tensor as \[\[f64;3\\];3] row-major.
44    pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
45        let r = self.radius;
46        let h = 2.0 * self.half_height;
47        let r2 = r * r;
48        let h2 = h * h;
49
50        let vol_cyl = PI * r2 * h;
51        let vol_sphere = (4.0 / 3.0) * PI * r.powi(3);
52        let total_vol = vol_cyl + vol_sphere;
53
54        let mass_cyl = mass * vol_cyl / total_vol;
55        let mass_sphere = mass * vol_sphere / total_vol;
56
57        let iy_cyl = 0.5 * mass_cyl * r2;
58        let ixz_cyl = mass_cyl * (3.0 * r2 + h2) / 12.0;
59
60        let iy_sphere = 0.4 * mass_sphere * r2;
61        let offset = self.half_height + 3.0 * r / 8.0;
62        let ixz_sphere = 0.4 * mass_sphere * r2 + mass_sphere * offset * offset;
63
64        let iy = iy_cyl + iy_sphere;
65        let ixz = ixz_cyl + ixz_sphere;
66
67        [[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
68    }
69
70    /// Ray cast returning (t, normal) as plain arrays.
71    pub fn ray_cast_array(
72        &self,
73        origin: [f64; 3],
74        direction: [f64; 3],
75        max_toi: f64,
76    ) -> Option<(f64, [f64; 3])> {
77        let o = Vec3::new(origin[0], origin[1], origin[2]);
78        let d = Vec3::new(direction[0], direction[1], direction[2]);
79        let hit = self.ray_cast(&o, &d, max_toi)?;
80        Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
81    }
82
83    /// GJK support function: farthest point in `direction`.
84    pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
85        let len = (direction[0] * direction[0]
86            + direction[1] * direction[1]
87            + direction[2] * direction[2])
88            .sqrt();
89        // Choose which hemicap center
90        let cap_y = if direction[1] >= 0.0 {
91            self.half_height
92        } else {
93            -self.half_height
94        };
95        if len < 1e-12 {
96            return [0.0, cap_y, 0.0];
97        }
98        let s = self.radius / len;
99        [direction[0] * s, cap_y + direction[1] * s, direction[2] * s]
100    }
101
102    // ── New methods ──
103
104    /// Closest point on the capsule surface to point `p`.
105    pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
106        // Project p onto the medial axis (Y-axis segment from -hh to +hh)
107        let clamped_y = p[1].clamp(-self.half_height, self.half_height);
108        // Vector from axis point to p
109        let dx = p[0];
110        let dy = p[1] - clamped_y;
111        let dz = p[2];
112        let len = (dx * dx + dy * dy + dz * dz).sqrt();
113
114        if len < 1e-12 {
115            // On the axis; return a point on the surface in +X direction
116            return [self.radius, clamped_y, 0.0];
117        }
118
119        let s = self.radius / len;
120        [dx * s, clamped_y + dy * s, dz * s]
121    }
122
123    /// Returns true if `p` is inside (or on) the capsule.
124    pub fn contains_point(&self, p: [f64; 3]) -> bool {
125        let clamped_y = p[1].clamp(-self.half_height, self.half_height);
126        let dx = p[0];
127        let dy = p[1] - clamped_y;
128        let dz = p[2];
129        dx * dx + dy * dy + dz * dz <= self.radius * self.radius
130    }
131
132    /// Signed distance from a point to the capsule surface.
133    /// Negative if inside, positive if outside.
134    pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
135        let clamped_y = p[1].clamp(-self.half_height, self.half_height);
136        let dx = p[0];
137        let dy = p[1] - clamped_y;
138        let dz = p[2];
139        let dist = (dx * dx + dy * dy + dz * dz).sqrt();
140        dist - self.radius
141    }
142
143    /// Medial axis endpoints: the two centers of the hemispherical caps.
144    pub fn medial_axis_endpoints(&self) -> ([f64; 3], [f64; 3]) {
145        ([0.0, self.half_height, 0.0], [0.0, -self.half_height, 0.0])
146    }
147
148    /// Full length of the capsule (tip to tip along Y).
149    pub fn full_length(&self) -> f64 {
150        2.0 * self.half_height + 2.0 * self.radius
151    }
152
153    /// Medial axis length (just the cylindrical segment).
154    pub fn medial_axis_length(&self) -> f64 {
155        2.0 * self.half_height
156    }
157
158    /// Distance between two segments in 3D.
159    /// Segment A: from `a0` to `a1`, Segment B: from `b0` to `b1`.
160    /// Returns the minimum distance.
161    pub fn segment_segment_distance(a0: [f64; 3], a1: [f64; 3], b0: [f64; 3], b1: [f64; 3]) -> f64 {
162        let da = [a1[0] - a0[0], a1[1] - a0[1], a1[2] - a0[2]];
163        let db = [b1[0] - b0[0], b1[1] - b0[1], b1[2] - b0[2]];
164        let r = [a0[0] - b0[0], a0[1] - b0[1], a0[2] - b0[2]];
165
166        let a = da[0] * da[0] + da[1] * da[1] + da[2] * da[2];
167        let e = db[0] * db[0] + db[1] * db[1] + db[2] * db[2];
168        let f = db[0] * r[0] + db[1] * r[1] + db[2] * r[2];
169
170        let eps = 1e-12;
171
172        if a <= eps && e <= eps {
173            // Both segments degenerate to points
174            let dx = r[0];
175            let dy = r[1];
176            let dz = r[2];
177            return (dx * dx + dy * dy + dz * dz).sqrt();
178        }
179
180        let b = da[0] * db[0] + da[1] * db[1] + da[2] * db[2];
181        let c = da[0] * r[0] + da[1] * r[1] + da[2] * r[2];
182
183        let (s, t);
184
185        if a <= eps {
186            s = 0.0;
187            t = (f / e).clamp(0.0, 1.0);
188        } else if e <= eps {
189            t = 0.0;
190            s = (-c / a).clamp(0.0, 1.0);
191        } else {
192            let denom = a * e - b * b;
193            s = if denom.abs() > eps {
194                ((b * f - c * e) / denom).clamp(0.0, 1.0)
195            } else {
196                0.0
197            };
198            let t_nom = b * s + f;
199            if t_nom < 0.0 {
200                t = 0.0;
201            } else if t_nom > e {
202                t = 1.0;
203            } else {
204                t = t_nom / e;
205            }
206        }
207
208        // Recompute s based on t
209        let s = if a > eps {
210            ((b * t - c) / a).clamp(0.0, 1.0)
211        } else {
212            s
213        };
214
215        let dx = r[0] + da[0] * s - db[0] * t;
216        let dy = r[1] + da[1] * s - db[1] * t;
217        let dz = r[2] + da[2] * s - db[2] * t;
218        (dx * dx + dy * dy + dz * dz).sqrt()
219    }
220
221    /// Capsule-capsule distance.
222    /// Both capsules are centered at the origin with Y-axis alignment,
223    /// but offset by given centers.
224    /// Returns the minimum distance between the two capsule surfaces (0 if overlapping).
225    pub fn capsule_capsule_distance(
226        &self,
227        center_a: [f64; 3],
228        other: &Capsule,
229        center_b: [f64; 3],
230    ) -> f64 {
231        // Medial axis of A: from center_a + [0, -hh_a, 0] to center_a + [0, hh_a, 0]
232        let a0 = [center_a[0], center_a[1] - self.half_height, center_a[2]];
233        let a1 = [center_a[0], center_a[1] + self.half_height, center_a[2]];
234        let b0 = [center_b[0], center_b[1] - other.half_height, center_b[2]];
235        let b1 = [center_b[0], center_b[1] + other.half_height, center_b[2]];
236
237        let seg_dist = Self::segment_segment_distance(a0, a1, b0, b1);
238        let surface_dist = seg_dist - self.radius - other.radius;
239        surface_dist.max(0.0)
240    }
241
242    /// Check if two capsules overlap.
243    pub fn capsule_capsule_overlap(
244        &self,
245        center_a: [f64; 3],
246        other: &Capsule,
247        center_b: [f64; 3],
248    ) -> bool {
249        let a0 = [center_a[0], center_a[1] - self.half_height, center_a[2]];
250        let a1 = [center_a[0], center_a[1] + self.half_height, center_a[2]];
251        let b0 = [center_b[0], center_b[1] - other.half_height, center_b[2]];
252        let b1 = [center_b[0], center_b[1] + other.half_height, center_b[2]];
253
254        let seg_dist = Self::segment_segment_distance(a0, a1, b0, b1);
255        seg_dist <= self.radius + other.radius
256    }
257
258    /// Project a point onto the medial axis (the Y-axis segment).
259    /// Returns the clamped Y value.
260    pub fn project_on_medial_axis(&self, p: [f64; 3]) -> f64 {
261        p[1].clamp(-self.half_height, self.half_height)
262    }
263
264    /// Distance from a point to the medial axis.
265    pub fn distance_to_medial_axis(&self, p: [f64; 3]) -> f64 {
266        let clamped_y = p[1].clamp(-self.half_height, self.half_height);
267        let dy = p[1] - clamped_y;
268        (p[0] * p[0] + dy * dy + p[2] * p[2]).sqrt()
269    }
270
271    // ── New expanded methods ──
272
273    /// Inertia tensor (mass-normalized) returned as `[[f64;3\];3]` (identical to
274    /// `inertia_tensor_array` but accepts mass separately for the public array API).
275    pub fn inertia_tensor_raw(&self, mass: f64) -> [[f64; 3]; 3] {
276        self.inertia_tensor_array(mass)
277    }
278
279    /// Closest points between two capsule medial axes (and thus capsule surfaces).
280    ///
281    /// Returns `(pa, pb, seg_dist)` where `pa` and `pb` are the closest points
282    /// on the respective medial axes and `seg_dist` is the axis-to-axis distance.
283    pub fn closest_points_capsule_vs_capsule(
284        &self,
285        center_a: [f64; 3],
286        other: &Capsule,
287        center_b: [f64; 3],
288    ) -> ([f64; 3], [f64; 3], f64) {
289        let a0 = [center_a[0], center_a[1] - self.half_height, center_a[2]];
290        let a1 = [center_a[0], center_a[1] + self.half_height, center_a[2]];
291        let b0 = [center_b[0], center_b[1] - other.half_height, center_b[2]];
292        let b1 = [center_b[0], center_b[1] + other.half_height, center_b[2]];
293
294        let (pa, pb, seg_dist) = Self::segment_segment_closest(a0, a1, b0, b1);
295        (pa, pb, seg_dist)
296    }
297
298    /// Returns the closest points on two 3D segments and the distance between them.
299    pub fn segment_segment_closest(
300        a0: [f64; 3],
301        a1: [f64; 3],
302        b0: [f64; 3],
303        b1: [f64; 3],
304    ) -> ([f64; 3], [f64; 3], f64) {
305        let da = [a1[0] - a0[0], a1[1] - a0[1], a1[2] - a0[2]];
306        let db = [b1[0] - b0[0], b1[1] - b0[1], b1[2] - b0[2]];
307        let r = [a0[0] - b0[0], a0[1] - b0[1], a0[2] - b0[2]];
308
309        let aa = da[0] * da[0] + da[1] * da[1] + da[2] * da[2];
310        let ee = db[0] * db[0] + db[1] * db[1] + db[2] * db[2];
311        let f = db[0] * r[0] + db[1] * r[1] + db[2] * r[2];
312        let eps = 1e-12;
313
314        let (s, t) = if aa <= eps && ee <= eps {
315            (0.0_f64, 0.0_f64)
316        } else if aa <= eps {
317            (0.0_f64, (f / ee).clamp(0.0, 1.0))
318        } else {
319            let c = da[0] * r[0] + da[1] * r[1] + da[2] * r[2];
320            if ee <= eps {
321                ((-c / aa).clamp(0.0, 1.0), 0.0_f64)
322            } else {
323                let b = da[0] * db[0] + da[1] * db[1] + da[2] * db[2];
324                let denom = aa * ee - b * b;
325                let s_cand = if denom.abs() > eps {
326                    ((b * f - c * ee) / denom).clamp(0.0, 1.0)
327                } else {
328                    0.0
329                };
330                let t_nom = b * s_cand + f;
331                let t_cand = if t_nom < 0.0 {
332                    0.0
333                } else if t_nom > ee {
334                    1.0
335                } else {
336                    t_nom / ee
337                };
338                let s_final = ((b * t_cand - c) / aa).clamp(0.0, 1.0);
339                (s_final, t_cand)
340            }
341        };
342
343        let pa = [a0[0] + s * da[0], a0[1] + s * da[1], a0[2] + s * da[2]];
344        let pb = [b0[0] + t * db[0], b0[1] + t * db[1], b0[2] + t * db[2]];
345        let dx = pa[0] - pb[0];
346        let dy = pa[1] - pb[1];
347        let dz = pa[2] - pb[2];
348        let dist = (dx * dx + dy * dy + dz * dz).sqrt();
349        (pa, pb, dist)
350    }
351
352    /// Capsule vs Oriented Bounding Box (OBB) intersection test.
353    ///
354    /// `obb_center`: world-space center of the OBB.
355    /// `obb_axes`:   columns are the OBB's local X, Y, Z axes (unit vectors).
356    /// `obb_half`:   half-extents along the OBB local axes.
357    ///
358    /// Converts the query to OBB-local space and tests sphere-swept segment
359    /// against the OBB using the separating-axis theorem (SAT) with slabs.
360    pub fn intersects_obb(
361        &self,
362        capsule_center: [f64; 3],
363        obb_center: [f64; 3],
364        obb_axes: [[f64; 3]; 3],
365        obb_half: [f64; 3],
366    ) -> bool {
367        // Expand the OBB by the capsule radius (Minkowski sum)
368        let expanded_half = [
369            obb_half[0] + self.radius,
370            obb_half[1] + self.radius,
371            obb_half[2] + self.radius,
372        ];
373
374        // Capsule segment endpoints in world space
375        let a = [
376            capsule_center[0],
377            capsule_center[1] - self.half_height,
378            capsule_center[2],
379        ];
380        let b = [
381            capsule_center[0],
382            capsule_center[1] + self.half_height,
383            capsule_center[2],
384        ];
385
386        // Test whether either endpoint is inside the expanded OBB
387        for pt in [a, b] {
388            let d = [
389                pt[0] - obb_center[0],
390                pt[1] - obb_center[1],
391                pt[2] - obb_center[2],
392            ];
393            let local = [
394                d[0] * obb_axes[0][0] + d[1] * obb_axes[0][1] + d[2] * obb_axes[0][2],
395                d[0] * obb_axes[1][0] + d[1] * obb_axes[1][1] + d[2] * obb_axes[1][2],
396                d[0] * obb_axes[2][0] + d[1] * obb_axes[2][1] + d[2] * obb_axes[2][2],
397            ];
398            if local[0].abs() <= expanded_half[0]
399                && local[1].abs() <= expanded_half[1]
400                && local[2].abs() <= expanded_half[2]
401            {
402                return true;
403            }
404        }
405
406        // Test whether segment passes through the expanded OBB (slab test)
407        // Transform segment into OBB-local space
408        let da = [
409            a[0] - obb_center[0],
410            a[1] - obb_center[1],
411            a[2] - obb_center[2],
412        ];
413        let dir_w = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
414
415        let orig_local = [
416            da[0] * obb_axes[0][0] + da[1] * obb_axes[0][1] + da[2] * obb_axes[0][2],
417            da[0] * obb_axes[1][0] + da[1] * obb_axes[1][1] + da[2] * obb_axes[1][2],
418            da[0] * obb_axes[2][0] + da[1] * obb_axes[2][1] + da[2] * obb_axes[2][2],
419        ];
420        let dir_local = [
421            dir_w[0] * obb_axes[0][0] + dir_w[1] * obb_axes[0][1] + dir_w[2] * obb_axes[0][2],
422            dir_w[0] * obb_axes[1][0] + dir_w[1] * obb_axes[1][1] + dir_w[2] * obb_axes[1][2],
423            dir_w[0] * obb_axes[2][0] + dir_w[1] * obb_axes[2][1] + dir_w[2] * obb_axes[2][2],
424        ];
425
426        let mut t_min = 0.0_f64;
427        let mut t_max = 1.0_f64;
428
429        for axis in 0..3 {
430            let d_loc = dir_local[axis];
431            let o_loc = orig_local[axis];
432            let hh = expanded_half[axis];
433            if d_loc.abs() < 1e-12 {
434                if o_loc.abs() > hh {
435                    return false;
436                }
437            } else {
438                let inv_d = 1.0 / d_loc;
439                let t0 = (-hh - o_loc) * inv_d;
440                let t1 = (hh - o_loc) * inv_d;
441                let (t_lo, t_hi) = if t0 < t1 { (t0, t1) } else { (t1, t0) };
442                t_min = t_min.max(t_lo);
443                t_max = t_max.min(t_hi);
444                if t_min > t_max {
445                    return false;
446                }
447            }
448        }
449        true
450    }
451
452    /// Signed distance field (SDF) for the capsule.
453    ///
454    /// Same as `signed_distance` but with a different name to match the
455    /// naming convention used in shader / SDF contexts.
456    pub fn sdf(&self, p: [f64; 3]) -> f64 {
457        self.signed_distance(p)
458    }
459
460    /// Continuous collision detection (swept capsule).
461    ///
462    /// Given the capsule moving from `center_start` to `center_end` (linear
463    /// motion, constant orientation), and a static sphere at `sphere_center`
464    /// with `sphere_radius`, returns the first time of contact `t ∈ [0,1]`
465    /// or `None` if no contact occurs.
466    ///
467    /// This is a simple CCD sweep using the Minkowski-sum radius and a
468    /// moving-segment vs static-point formulation.
469    pub fn swept_capsule_vs_sphere(
470        &self,
471        center_start: [f64; 3],
472        center_end: [f64; 3],
473        sphere_center: [f64; 3],
474        sphere_radius: f64,
475    ) -> Option<f64> {
476        let combined_radius = self.radius + sphere_radius;
477
478        // We need to find the earliest t in [0,1] such that the distance from
479        // sphere_center to the capsule medial axis (at time t) equals combined_radius.
480        // Linearly interpolate capsule center.
481        // At time t, capsule axis: from C(t)-[0,hh,0] to C(t)+[0,hh,0],
482        // where C(t) = center_start + t*(center_end - center_start).
483
484        let n_steps = 64;
485        for i in 0..=n_steps {
486            let t = i as f64 / n_steps as f64;
487            let cx = center_start[0] + t * (center_end[0] - center_start[0]);
488            let cy = center_start[1] + t * (center_end[1] - center_start[1]);
489            let cz = center_start[2] + t * (center_end[2] - center_start[2]);
490
491            let a0 = [cx, cy - self.half_height, cz];
492            let a1 = [cx, cy + self.half_height, cz];
493            let dist = Self::point_segment_distance(sphere_center, a0, a1);
494            if dist <= combined_radius {
495                return Some(t);
496            }
497        }
498        None
499    }
500
501    /// Distance from a point to a segment.
502    fn point_segment_distance(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
503        let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
504        let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
505        let ab_sq = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
506        let t = if ab_sq < 1e-24 {
507            0.0
508        } else {
509            ((ab[0] * ap[0] + ab[1] * ap[1] + ab[2] * ap[2]) / ab_sq).clamp(0.0, 1.0)
510        };
511        let proj = [a[0] + t * ab[0], a[1] + t * ab[1], a[2] + t * ab[2]];
512        let dx = p[0] - proj[0];
513        let dy = p[1] - proj[1];
514        let dz = p[2] - proj[2];
515        (dx * dx + dy * dy + dz * dz).sqrt()
516    }
517
518    /// Generate `n` random points on the capsule surface using rejection sampling.
519    ///
520    /// Returns a `Vec` of `[f64;3]` coordinates sampled uniformly on the surface.
521    /// Uses a deterministic PRNG seed for reproducibility.
522    pub fn random_surface_points(&self, n: usize, seed: u64) -> Vec<[f64; 3]> {
523        let mut points = Vec::with_capacity(n);
524        let r = self.radius;
525        let hh = self.half_height;
526
527        // Surface areas
528        let cyl_area = 2.0 * PI * r * 2.0 * hh;
529        let sphere_area = 4.0 * PI * r * r;
530        let total_area = cyl_area + sphere_area;
531        let p_cylinder = cyl_area / total_area;
532
533        let mut rng_state = seed;
534        let next_f64 = |state: &mut u64| -> f64 {
535            // xorshift64 PRNG
536            *state ^= *state << 13;
537            *state ^= *state >> 7;
538            *state ^= *state << 17;
539            (*state as f64) / (u64::MAX as f64)
540        };
541
542        while points.len() < n {
543            let u = next_f64(&mut rng_state);
544            let v = next_f64(&mut rng_state);
545            let w = next_f64(&mut rng_state);
546
547            if u < p_cylinder {
548                // Cylindrical region
549                let theta = v * 2.0 * PI;
550                let y = (w * 2.0 - 1.0) * hh;
551                points.push([r * theta.cos(), y, r * theta.sin()]);
552            } else {
553                // Hemisphere (top or bottom)
554                let theta = v * 2.0 * PI;
555                let phi = (w * 2.0 - 1.0).acos();
556                let x = r * phi.sin() * theta.cos();
557                let z = r * phi.sin() * theta.sin();
558                let raw_y = r * phi.cos();
559                // Map to correct hemisphere
560                let extra = next_f64(&mut rng_state);
561                let y = if extra < 0.5 {
562                    hh + raw_y.abs()
563                } else {
564                    -hh - raw_y.abs()
565                };
566                points.push([x, y, z]);
567            }
568        }
569        points
570    }
571
572    // ── Capsule-triangle contact ──
573
574    /// Test capsule vs a world-space triangle `(ta, tb, tc)`.
575    ///
576    /// The capsule is placed at `center` (Y-aligned).  Returns `Some((depth, normal))`
577    /// if the capsule penetrates the triangle plane by at least `depth > 0`, where
578    /// `normal` points from the triangle towards the capsule center.
579    pub fn capsule_triangle_contact(
580        &self,
581        center: [f64; 3],
582        ta: [f64; 3],
583        tb: [f64; 3],
584        tc: [f64; 3],
585    ) -> Option<(f64, [f64; 3])> {
586        // Triangle edge vectors and normal
587        let e1 = [tb[0] - ta[0], tb[1] - ta[1], tb[2] - ta[2]];
588        let e2 = [tc[0] - ta[0], tc[1] - ta[1], tc[2] - ta[2]];
589        let raw_n = [
590            e1[1] * e2[2] - e1[2] * e2[1],
591            e1[2] * e2[0] - e1[0] * e2[2],
592            e1[0] * e2[1] - e1[1] * e2[0],
593        ];
594        let n_len = (raw_n[0] * raw_n[0] + raw_n[1] * raw_n[1] + raw_n[2] * raw_n[2]).sqrt();
595        if n_len < 1e-12 {
596            return None;
597        }
598        let n = [raw_n[0] / n_len, raw_n[1] / n_len, raw_n[2] / n_len];
599
600        // Capsule medial axis endpoints in world space
601        let a0 = [center[0], center[1] - self.half_height, center[2]];
602        let a1 = [center[0], center[1] + self.half_height, center[2]];
603
604        // Signed distances of both endpoints from the triangle plane
605        let d0 = (a0[0] - ta[0]) * n[0] + (a0[1] - ta[1]) * n[1] + (a0[2] - ta[2]) * n[2];
606        let d1 = (a1[0] - ta[0]) * n[0] + (a1[1] - ta[1]) * n[1] + (a1[2] - ta[2]) * n[2];
607
608        // Use the endpoint closest to the triangle plane
609        let min_dist = d0.abs().min(d1.abs());
610        let penetration = self.radius - min_dist;
611        if penetration > 0.0 {
612            Some((penetration, n))
613        } else {
614            None
615        }
616    }
617}
618
619// ── CapsuleChain (rope segments) ──
620
621/// A chain of capsule segments forming a rope / chain approximation.
622///
623/// Each consecutive pair of control points defines a capsule segment
624/// of the same radius.
625#[derive(Debug, Clone)]
626pub struct CapsuleChain {
627    /// Control points of the chain (N points → N-1 segments).
628    pub points: Vec<[f64; 3]>,
629    /// Uniform radius for all segments.
630    pub radius: f64,
631}
632
633impl CapsuleChain {
634    /// Create a chain from control points with zero radius.
635    pub fn new(points: Vec<[f64; 3]>) -> Self {
636        Self {
637            points,
638            radius: 0.0,
639        }
640    }
641
642    /// Create a chain with a specified radius.
643    pub fn with_radius(points: Vec<[f64; 3]>, radius: f64) -> Self {
644        Self { points, radius }
645    }
646
647    /// Number of capsule segments (one fewer than the number of points).
648    pub fn segment_count(&self) -> usize {
649        self.points.len().saturating_sub(1)
650    }
651
652    /// Total arc length of the chain (sum of segment lengths).
653    pub fn total_length(&self) -> f64 {
654        self.points
655            .windows(2)
656            .map(|w| {
657                let dx = w[1][0] - w[0][0];
658                let dy = w[1][1] - w[0][1];
659                let dz = w[1][2] - w[0][2];
660                (dx * dx + dy * dy + dz * dz).sqrt()
661            })
662            .sum()
663    }
664
665    /// Returns `true` if `p` is inside any capsule segment of the chain.
666    pub fn contains_point(&self, p: [f64; 3]) -> bool {
667        self.points
668            .windows(2)
669            .any(|w| Capsule::point_segment_distance(p, w[0], w[1]) <= self.radius)
670    }
671
672    /// SDF: minimum signed distance to any segment surface.
673    pub fn sdf(&self, p: [f64; 3]) -> f64 {
674        let min_seg_dist = self
675            .points
676            .windows(2)
677            .map(|w| Capsule::point_segment_distance(p, w[0], w[1]))
678            .fold(f64::INFINITY, f64::min);
679        min_seg_dist - self.radius
680    }
681
682    /// Minimum distance from `p` to the chain surface (negative if inside).
683    pub fn min_distance_to_point(&self, p: [f64; 3]) -> f64 {
684        self.sdf(p)
685    }
686}
687
688// ── DeformableCapsule ──
689
690/// A capsule with arbitrary (deformable) endpoint positions.
691///
692/// Unlike the axis-aligned `Capsule`, this capsule can have its two
693/// hemispherical cap centres at any positions in 3D space.
694#[derive(Debug, Clone)]
695pub struct DeformableCapsule {
696    /// First endpoint of the medial axis.
697    pub a: [f64; 3],
698    /// Second endpoint of the medial axis.
699    pub b: [f64; 3],
700    /// Capsule radius.
701    pub radius: f64,
702}
703
704impl DeformableCapsule {
705    /// Create a new deformable capsule.
706    pub fn new(a: [f64; 3], b: [f64; 3], radius: f64) -> Self {
707        Self { a, b, radius }
708    }
709
710    /// First endpoint (cap centre A).
711    pub fn endpoint_a(&self) -> [f64; 3] {
712        self.a
713    }
714
715    /// Second endpoint (cap centre B).
716    pub fn endpoint_b(&self) -> [f64; 3] {
717        self.b
718    }
719
720    /// Update endpoint A.
721    pub fn set_endpoint_a(&mut self, a: [f64; 3]) {
722        self.a = a;
723    }
724
725    /// Update endpoint B.
726    pub fn set_endpoint_b(&mut self, b: [f64; 3]) {
727        self.b = b;
728    }
729
730    /// Length of the medial axis segment.
731    pub fn length(&self) -> f64 {
732        let d = [
733            self.b[0] - self.a[0],
734            self.b[1] - self.a[1],
735            self.b[2] - self.a[2],
736        ];
737        (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
738    }
739
740    /// Midpoint of the medial axis.
741    pub fn midpoint(&self) -> [f64; 3] {
742        [
743            (self.a[0] + self.b[0]) * 0.5,
744            (self.a[1] + self.b[1]) * 0.5,
745            (self.a[2] + self.b[2]) * 0.5,
746        ]
747    }
748
749    /// Medial axis endpoints.
750    pub fn medial_axis_endpoints(&self) -> ([f64; 3], [f64; 3]) {
751        (self.a, self.b)
752    }
753
754    /// Signed distance field: negative inside, positive outside.
755    pub fn sdf(&self, p: [f64; 3]) -> f64 {
756        Capsule::point_segment_distance(p, self.a, self.b) - self.radius
757    }
758
759    /// Support point in the given direction.
760    pub fn support(&self, dir: [f64; 3]) -> [f64; 3] {
761        let da = dir[0] * self.a[0] + dir[1] * self.a[1] + dir[2] * self.a[2];
762        let db = dir[0] * self.b[0] + dir[1] * self.b[1] + dir[2] * self.b[2];
763        let base = if da >= db { self.a } else { self.b };
764        let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
765        if len < 1e-12 {
766            return base;
767        }
768        let s = self.radius / len;
769        [
770            base[0] + dir[0] * s,
771            base[1] + dir[1] * s,
772            base[2] + dir[2] * s,
773        ]
774    }
775}
776
777// ── CapsuleFrustum ──
778
779/// A capsule-like shape with different radii at each end (a "swept frustum").
780///
781/// The axis runs along Y from `y=0` (radius `r_bottom`) to `y=height` (radius `r_top`).
782/// The radius varies linearly along the axis.
783#[derive(Debug, Clone)]
784pub struct CapsuleFrustum {
785    /// Radius at the bottom cap (y=0).
786    pub r_bottom: f64,
787    /// Radius at the top cap (y=height).
788    pub r_top: f64,
789    /// Total height (length of axis segment).
790    pub height: f64,
791}
792
793impl CapsuleFrustum {
794    /// Create a new capsule frustum.
795    pub fn new(r_bottom: f64, r_top: f64, height: f64) -> Self {
796        Self {
797            r_bottom,
798            r_top,
799            height,
800        }
801    }
802
803    /// Interpolated radius at height `y` (clamped to \[0, height\]).
804    pub fn radius_at_height(&self, y: f64) -> f64 {
805        if self.height < 1e-12 {
806            return (self.r_bottom + self.r_top) * 0.5;
807        }
808        let t = (y / self.height).clamp(0.0, 1.0);
809        self.r_bottom + t * (self.r_top - self.r_bottom)
810    }
811
812    /// Approximate volume (frustum cone + two hemispheres of different sizes).
813    pub fn volume(&self) -> f64 {
814        // Truncated cone: π/3 * h * (r1² + r1*r2 + r2²)
815        let r1 = self.r_bottom;
816        let r2 = self.r_top;
817        let h = self.height;
818        let cone_vol = std::f64::consts::PI / 3.0 * h * (r1 * r1 + r1 * r2 + r2 * r2);
819        let sphere_bot = (2.0 / 3.0) * std::f64::consts::PI * r1 * r1 * r1;
820        let sphere_top = (2.0 / 3.0) * std::f64::consts::PI * r2 * r2 * r2;
821        cone_vol + sphere_bot + sphere_top
822    }
823
824    /// Returns `true` if point `p` is inside the frustum.
825    pub fn contains_point(&self, p: [f64; 3]) -> bool {
826        self.sdf(p) <= 0.0
827    }
828
829    /// Signed distance to the frustum surface (approximate).
830    pub fn sdf(&self, p: [f64; 3]) -> f64 {
831        let y = p[1].clamp(0.0, self.height);
832        let r = self.radius_at_height(y);
833        let dy = p[1] - y;
834        let xz_dist = (p[0] * p[0] + p[2] * p[2]).sqrt();
835        let dist_to_axis = (xz_dist * xz_dist + dy * dy).sqrt();
836        dist_to_axis - r
837    }
838}
839
840// ── CurvedCapsulePath ──
841
842/// A capsule swept along a polyline path.
843///
844/// Each consecutive pair of path points defines a capsule segment.
845/// The shape represents the union of all such capsule segments.
846#[derive(Debug, Clone)]
847pub struct CurvedCapsulePath {
848    /// Polyline control points.
849    pub path: Vec<[f64; 3]>,
850    /// Uniform radius.
851    pub radius: f64,
852}
853
854impl CurvedCapsulePath {
855    /// Create a new curved capsule path.
856    pub fn new(path: Vec<[f64; 3]>, radius: f64) -> Self {
857        Self { path, radius }
858    }
859
860    /// Total path length (sum of segment lengths).
861    pub fn path_length(&self) -> f64 {
862        self.path
863            .windows(2)
864            .map(|w| {
865                let d = [w[1][0] - w[0][0], w[1][1] - w[0][1], w[1][2] - w[0][2]];
866                (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
867            })
868            .sum()
869    }
870
871    /// Number of path segments (one fewer than points).
872    pub fn num_segments(&self) -> usize {
873        self.path.len().saturating_sub(1)
874    }
875
876    /// Returns `true` if `p` is inside any capsule segment.
877    pub fn contains_point(&self, p: [f64; 3]) -> bool {
878        self.path
879            .windows(2)
880            .any(|w| Capsule::point_segment_distance(p, w[0], w[1]) <= self.radius)
881    }
882
883    /// Approximate SDF: minimum distance to any segment minus radius.
884    pub fn sdf(&self, p: [f64; 3]) -> f64 {
885        let min_d = self
886            .path
887            .windows(2)
888            .map(|w| Capsule::point_segment_distance(p, w[0], w[1]))
889            .fold(f64::INFINITY, f64::min);
890        min_d - self.radius
891    }
892}
893
894impl Shape for Capsule {
895    fn bounding_box(&self) -> Aabb {
896        let r = self.radius;
897        let h = self.half_height + r;
898        Aabb::new(Vec3::new(-r, -h, -r), Vec3::new(r, h, r))
899    }
900
901    fn support_point(&self, direction: &Vec3) -> Vec3 {
902        let norm = direction.norm();
903        let cap_center = if direction.y >= 0.0 {
904            Vec3::new(0.0, self.half_height, 0.0)
905        } else {
906            Vec3::new(0.0, -self.half_height, 0.0)
907        };
908        if norm < 1e-10 {
909            return cap_center;
910        }
911        cap_center + direction * (self.radius / norm)
912    }
913
914    fn volume(&self) -> Real {
915        let r = self.radius;
916        let h = 2.0 * self.half_height;
917        // Cylinder + sphere
918        PI * r * r * h + (4.0 / 3.0) * PI * r.powi(3)
919    }
920
921    fn center_of_mass(&self) -> Vec3 {
922        Vec3::zeros()
923    }
924
925    fn inertia_tensor(&self, mass: Real) -> Mat3 {
926        let r = self.radius;
927        let h = 2.0 * self.half_height;
928        let r2 = r * r;
929        let h2 = h * h;
930
931        // Approximate as cylinder for the inertia
932        let vol_cyl = PI * r2 * h;
933        let vol_sphere = (4.0 / 3.0) * PI * r.powi(3);
934        let total_vol = vol_cyl + vol_sphere;
935
936        let mass_cyl = mass * vol_cyl / total_vol;
937        let mass_sphere = mass * vol_sphere / total_vol;
938
939        // Cylinder inertia (Y-aligned)
940        let iy_cyl = 0.5 * mass_cyl * r2;
941        let ixz_cyl = mass_cyl * (3.0 * r2 + h2) / 12.0;
942
943        // Hemisphere inertia (approximation using shifted sphere)
944        let iy_sphere = 0.4 * mass_sphere * r2;
945        // Parallel axis theorem for hemispheres offset by half_height + 3r/8
946        let offset = self.half_height + 3.0 * r / 8.0;
947        let ixz_sphere = 0.4 * mass_sphere * r2 + mass_sphere * offset * offset;
948
949        let iy = iy_cyl + iy_sphere;
950        let ixz = ixz_cyl + ixz_sphere;
951
952        Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
953    }
954
955    fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
956        // Simplified: test against the two end-cap spheres and the cylinder
957        let mut best: Option<RayHit> = None;
958
959        // Test top hemisphere
960        let top_center = Vec3::new(0.0, self.half_height, 0.0);
961        if let Some(hit) = ray_sphere(ray_origin, ray_direction, &top_center, self.radius)
962            && hit.toi <= max_toi
963            && hit.point.y >= self.half_height
964            && best.as_ref().is_none_or(|b| hit.toi < b.toi)
965        {
966            best = Some(hit);
967        }
968
969        // Test bottom hemisphere
970        let bot_center = Vec3::new(0.0, -self.half_height, 0.0);
971        if let Some(hit) = ray_sphere(ray_origin, ray_direction, &bot_center, self.radius)
972            && hit.toi <= max_toi
973            && hit.point.y <= -self.half_height
974            && best.as_ref().is_none_or(|b| hit.toi < b.toi)
975        {
976            best = Some(hit);
977        }
978
979        // Test infinite cylinder (XZ plane)
980        if let Some(hit) = ray_cylinder_y(ray_origin, ray_direction, self.radius, self.half_height)
981            && hit.toi <= max_toi
982            && best.as_ref().is_none_or(|b| hit.toi < b.toi)
983        {
984            best = Some(hit);
985        }
986
987        best
988    }
989}
990
991fn ray_sphere(origin: &Vec3, direction: &Vec3, center: &Vec3, radius: Real) -> Option<RayHit> {
992    let oc = origin - center;
993    let a = direction.dot(direction);
994    let b = 2.0 * oc.dot(direction);
995    let c = oc.dot(&oc) - radius * radius;
996    let disc = b * b - 4.0 * a * c;
997    if disc < 0.0 {
998        return None;
999    }
1000    let sqrt_disc = disc.sqrt();
1001    let t1 = (-b - sqrt_disc) / (2.0 * a);
1002    let t2 = (-b + sqrt_disc) / (2.0 * a);
1003    let t = if t1 >= 0.0 { t1 } else { t2 };
1004    if t < 0.0 {
1005        return None;
1006    }
1007    let point = origin + direction * t;
1008    let normal = (point - center).normalize();
1009    Some(RayHit {
1010        point,
1011        normal,
1012        toi: t,
1013    })
1014}
1015
1016fn ray_cylinder_y(
1017    origin: &Vec3,
1018    direction: &Vec3,
1019    radius: Real,
1020    half_height: Real,
1021) -> Option<RayHit> {
1022    // Infinite cylinder along Y axis in XZ plane
1023    let a = direction.x * direction.x + direction.z * direction.z;
1024    let b = 2.0 * (origin.x * direction.x + origin.z * direction.z);
1025    let c = origin.x * origin.x + origin.z * origin.z - radius * radius;
1026    let disc = b * b - 4.0 * a * c;
1027    if disc < 0.0 || a < 1e-12 {
1028        return None;
1029    }
1030    let sqrt_disc = disc.sqrt();
1031    let t1 = (-b - sqrt_disc) / (2.0 * a);
1032    let t2 = (-b + sqrt_disc) / (2.0 * a);
1033
1034    for t in [t1, t2] {
1035        if t < 0.0 {
1036            continue;
1037        }
1038        let point = origin + direction * t;
1039        if point.y.abs() <= half_height {
1040            let normal = Vec3::new(point.x, 0.0, point.z).normalize();
1041            return Some(RayHit {
1042                point,
1043                normal,
1044                toi: t,
1045            });
1046        }
1047    }
1048    None
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053    use super::*;
1054
1055    #[test]
1056    fn test_capsule_volume() {
1057        let c = Capsule::new(1.0, 1.0);
1058        let expected = PI * 1.0 * 2.0 + (4.0 / 3.0) * PI;
1059        assert!((c.volume() - expected).abs() < 1e-10);
1060    }
1061
1062    #[test]
1063    fn test_capsule_volume_explicit() {
1064        let c = Capsule::new(1.0, 1.0);
1065        assert!((c.volume_explicit() - c.volume()).abs() < 1e-10);
1066    }
1067
1068    #[test]
1069    fn test_capsule_surface_area() {
1070        // r=1, half_height=1: 2π*1*2 + 4π = 4π + 4π = 8π
1071        let c = Capsule::new(1.0, 1.0);
1072        let expected = 8.0 * PI;
1073        assert!((c.surface_area() - expected).abs() < 1e-10);
1074    }
1075
1076    #[test]
1077    fn test_capsule_inertia_array() {
1078        let c = Capsule::new(1.0, 1.0);
1079        let it = c.inertia_tensor_array(1.0);
1080        // Diagonal elements should be positive
1081        assert!(it[0][0] > 0.0);
1082        assert!(it[1][1] > 0.0);
1083        assert!(it[2][2] > 0.0);
1084        // Ixx == Izz by symmetry
1085        assert!((it[0][0] - it[2][2]).abs() < 1e-10);
1086        // Off-diagonal zero
1087        assert!(it[0][1].abs() < 1e-10);
1088    }
1089
1090    #[test]
1091    fn test_capsule_raycast() {
1092        let c = Capsule::new(1.0, 2.0);
1093        let origin = Vec3::new(-5.0, 0.0, 0.0);
1094        let dir = Vec3::new(1.0, 0.0, 0.0);
1095        let hit = c.ray_cast(&origin, &dir, 100.0).unwrap();
1096        assert!((hit.toi - 4.0).abs() < 1e-10);
1097    }
1098
1099    #[test]
1100    fn test_capsule_raycast_array() {
1101        let c = Capsule::new(1.0, 2.0);
1102        let (t, _n) = c
1103            .ray_cast_array([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0)
1104            .unwrap();
1105        assert!((t - 4.0).abs() < 1e-10);
1106    }
1107
1108    #[test]
1109    fn test_capsule_support_up() {
1110        let c = Capsule::new(1.0, 2.0);
1111        let sp = c.support([0.0, 1.0, 0.0]);
1112        // Should return top cap: y = half_height + radius = 3
1113        assert!((sp[1] - 3.0).abs() < 1e-10);
1114    }
1115
1116    #[test]
1117    fn test_capsule_support_down() {
1118        let c = Capsule::new(1.0, 2.0);
1119        let sp = c.support([0.0, -1.0, 0.0]);
1120        // Should return bottom: y = -half_height - radius = -3
1121        assert!((sp[1] + 3.0).abs() < 1e-10);
1122    }
1123
1124    #[test]
1125    fn test_capsule_raycast_top_cap() {
1126        let c = Capsule::new(1.0, 2.0);
1127        let origin = Vec3::new(0.0, 10.0, 0.0);
1128        let dir = Vec3::new(0.0, -1.0, 0.0);
1129        let hit = c.ray_cast(&origin, &dir, 100.0).unwrap();
1130        // Top of capsule at y = half_height + radius = 3
1131        assert!(
1132            (hit.toi - 7.0).abs() < 1e-10,
1133            "expected toi=7, got {}",
1134            hit.toi
1135        );
1136    }
1137
1138    // ── New tests ──
1139
1140    #[test]
1141    fn test_capsule_closest_point_on_side() {
1142        let c = Capsule::new(1.0, 2.0);
1143        let cp = c.closest_point([5.0, 0.0, 0.0]);
1144        assert!((cp[0] - 1.0).abs() < 1e-10);
1145        assert!(cp[1].abs() < 1e-10);
1146        assert!(cp[2].abs() < 1e-10);
1147    }
1148
1149    #[test]
1150    fn test_capsule_closest_point_on_top_cap() {
1151        let c = Capsule::new(1.0, 2.0);
1152        let cp = c.closest_point([0.0, 5.0, 0.0]);
1153        // Should be on the top hemisphere: (0, 2+1, 0) = (0, 3, 0)
1154        assert!((cp[1] - 3.0).abs() < 1e-10);
1155        assert!(cp[0].abs() < 1e-10);
1156    }
1157
1158    #[test]
1159    fn test_capsule_closest_point_on_bottom_cap() {
1160        let c = Capsule::new(1.0, 2.0);
1161        let cp = c.closest_point([0.0, -5.0, 0.0]);
1162        assert!((cp[1] + 3.0).abs() < 1e-10);
1163    }
1164
1165    #[test]
1166    fn test_capsule_closest_point_on_axis() {
1167        let c = Capsule::new(1.0, 2.0);
1168        // Point on axis inside capsule
1169        let cp = c.closest_point([0.0, 0.0, 0.0]);
1170        // Should return a point on the surface, in +X direction
1171        assert!((cp[0] - 1.0).abs() < 1e-10);
1172        assert!(cp[1].abs() < 1e-10);
1173    }
1174
1175    #[test]
1176    fn test_capsule_contains_point() {
1177        let c = Capsule::new(1.0, 2.0);
1178        assert!(c.contains_point([0.0, 0.0, 0.0])); // center
1179        assert!(c.contains_point([0.5, 1.0, 0.0])); // inside cylinder
1180        assert!(c.contains_point([0.0, 2.5, 0.0])); // inside top cap
1181        assert!(!c.contains_point([0.0, 3.5, 0.0])); // above capsule
1182        assert!(!c.contains_point([2.0, 0.0, 0.0])); // outside radially
1183    }
1184
1185    #[test]
1186    fn test_capsule_signed_distance() {
1187        let c = Capsule::new(2.0, 1.0);
1188        // Center
1189        assert!((c.signed_distance([0.0, 0.0, 0.0]) + 2.0).abs() < 1e-10);
1190        // On surface
1191        assert!(c.signed_distance([2.0, 0.0, 0.0]).abs() < 1e-10);
1192        // Outside
1193        assert!((c.signed_distance([4.0, 0.0, 0.0]) - 2.0).abs() < 1e-10);
1194    }
1195
1196    #[test]
1197    fn test_capsule_medial_axis_endpoints() {
1198        let c = Capsule::new(1.0, 3.0);
1199        let (top, bot) = c.medial_axis_endpoints();
1200        assert!((top[1] - 3.0).abs() < 1e-10);
1201        assert!((bot[1] + 3.0).abs() < 1e-10);
1202    }
1203
1204    #[test]
1205    fn test_capsule_full_length() {
1206        let c = Capsule::new(1.0, 2.0);
1207        // 2*hh + 2*r = 4 + 2 = 6
1208        assert!((c.full_length() - 6.0).abs() < 1e-10);
1209    }
1210
1211    #[test]
1212    fn test_capsule_medial_axis_length() {
1213        let c = Capsule::new(1.0, 2.0);
1214        assert!((c.medial_axis_length() - 4.0).abs() < 1e-10);
1215    }
1216
1217    #[test]
1218    fn test_segment_segment_distance_parallel() {
1219        // Two parallel segments along Y, offset in X
1220        let d = Capsule::segment_segment_distance(
1221            [0.0, 0.0, 0.0],
1222            [0.0, 1.0, 0.0],
1223            [3.0, 0.0, 0.0],
1224            [3.0, 1.0, 0.0],
1225        );
1226        assert!((d - 3.0).abs() < 1e-10);
1227    }
1228
1229    #[test]
1230    fn test_segment_segment_distance_crossing() {
1231        // Two segments that cross: one along X, one along Z, separated by y
1232        let d = Capsule::segment_segment_distance(
1233            [-1.0, 0.0, 0.0],
1234            [1.0, 0.0, 0.0],
1235            [0.0, 2.0, -1.0],
1236            [0.0, 2.0, 1.0],
1237        );
1238        assert!((d - 2.0).abs() < 1e-10);
1239    }
1240
1241    #[test]
1242    fn test_segment_segment_distance_touching() {
1243        let d = Capsule::segment_segment_distance(
1244            [0.0, 0.0, 0.0],
1245            [1.0, 0.0, 0.0],
1246            [1.0, 0.0, 0.0],
1247            [2.0, 0.0, 0.0],
1248        );
1249        assert!(d.abs() < 1e-10);
1250    }
1251
1252    #[test]
1253    fn test_segment_segment_distance_degenerate_points() {
1254        let d = Capsule::segment_segment_distance(
1255            [0.0, 0.0, 0.0],
1256            [0.0, 0.0, 0.0],
1257            [3.0, 4.0, 0.0],
1258            [3.0, 4.0, 0.0],
1259        );
1260        assert!((d - 5.0).abs() < 1e-10);
1261    }
1262
1263    #[test]
1264    fn test_capsule_capsule_distance_separated() {
1265        let a = Capsule::new(1.0, 1.0);
1266        let b = Capsule::new(1.0, 1.0);
1267        // Place them 5 apart in X
1268        let d = a.capsule_capsule_distance([0.0, 0.0, 0.0], &b, [5.0, 0.0, 0.0]);
1269        // Segment distance = 5, surface distance = 5 - 1 - 1 = 3
1270        assert!((d - 3.0).abs() < 1e-10);
1271    }
1272
1273    #[test]
1274    fn test_capsule_capsule_distance_overlapping() {
1275        let a = Capsule::new(1.0, 1.0);
1276        let b = Capsule::new(1.0, 1.0);
1277        let d = a.capsule_capsule_distance([0.0, 0.0, 0.0], &b, [0.5, 0.0, 0.0]);
1278        assert_eq!(d, 0.0); // overlapping
1279    }
1280
1281    #[test]
1282    fn test_capsule_capsule_overlap() {
1283        let a = Capsule::new(1.0, 1.0);
1284        let b = Capsule::new(1.0, 1.0);
1285        assert!(a.capsule_capsule_overlap([0.0, 0.0, 0.0], &b, [1.0, 0.0, 0.0]));
1286        assert!(!a.capsule_capsule_overlap([0.0, 0.0, 0.0], &b, [5.0, 0.0, 0.0]));
1287    }
1288
1289    #[test]
1290    fn test_capsule_project_on_medial_axis() {
1291        let c = Capsule::new(1.0, 2.0);
1292        assert!((c.project_on_medial_axis([0.0, 5.0, 0.0]) - 2.0).abs() < 1e-10);
1293        assert!((c.project_on_medial_axis([0.0, -5.0, 0.0]) + 2.0).abs() < 1e-10);
1294        assert!((c.project_on_medial_axis([3.0, 1.0, 0.0]) - 1.0).abs() < 1e-10);
1295    }
1296
1297    #[test]
1298    fn test_capsule_distance_to_medial_axis() {
1299        let c = Capsule::new(1.0, 2.0);
1300        assert!(c.distance_to_medial_axis([0.0, 0.0, 0.0]).abs() < 1e-10);
1301        assert!((c.distance_to_medial_axis([3.0, 0.0, 0.0]) - 3.0).abs() < 1e-10);
1302        // Point above cap
1303        assert!((c.distance_to_medial_axis([0.0, 5.0, 0.0]) - 3.0).abs() < 1e-10);
1304    }
1305
1306    #[test]
1307    fn test_capsule_support_diagonal() {
1308        let c = Capsule::new(1.0, 2.0);
1309        let sp = c.support([1.0, 1.0, 0.0]);
1310        // cap_y = 2.0 (positive Y)
1311        let len = (1.0_f64 + 1.0).sqrt();
1312        let expected_x = 1.0 / len;
1313        let expected_y = 2.0 + 1.0 / len;
1314        assert!((sp[0] - expected_x).abs() < 1e-10);
1315        assert!((sp[1] - expected_y).abs() < 1e-10);
1316    }
1317
1318    // ── Expanded tests ──
1319
1320    #[test]
1321    fn test_capsule_closest_points_vs_capsule() {
1322        let a = Capsule::new(1.0, 1.0);
1323        let b = Capsule::new(1.0, 1.0);
1324        // Capsules side by side in X, separated by 4 units
1325        let (pa, pb, seg_dist) =
1326            a.closest_points_capsule_vs_capsule([0.0, 0.0, 0.0], &b, [4.0, 0.0, 0.0]);
1327        assert!(
1328            (seg_dist - 4.0).abs() < 1e-9,
1329            "axis dist should be 4, got {seg_dist}"
1330        );
1331        assert!((pa[0]).abs() < 1e-9);
1332        assert!((pb[0] - 4.0).abs() < 1e-9);
1333    }
1334
1335    #[test]
1336    fn test_segment_segment_closest_crossing() {
1337        // X-segment and Z-segment crossing at origin at different Y levels
1338        let (pa, pb, dist) = Capsule::segment_segment_closest(
1339            [-1.0, 0.0, 0.0],
1340            [1.0, 0.0, 0.0],
1341            [0.0, 3.0, -1.0],
1342            [0.0, 3.0, 1.0],
1343        );
1344        assert!((dist - 3.0).abs() < 1e-9, "dist should be 3, got {dist}");
1345        assert!(pa[0].abs() < 1e-9);
1346        assert!((pb[1] - 3.0).abs() < 1e-9);
1347    }
1348
1349    #[test]
1350    fn test_capsule_intersects_obb_basic() {
1351        let c = Capsule::new(0.5, 1.0);
1352        // OBB centered at origin, axis-aligned, half-extents [2,2,2]
1353        let axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1354        // Capsule centered inside OBB
1355        assert!(c.intersects_obb([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], axes, [2.0, 2.0, 2.0]));
1356        // Capsule far away
1357        assert!(!c.intersects_obb([0.0, 0.0, 0.0], [10.0, 0.0, 0.0], axes, [2.0, 2.0, 2.0]));
1358    }
1359
1360    #[test]
1361    fn test_capsule_sdf_matches_signed_distance() {
1362        let c = Capsule::new(1.0, 2.0);
1363        let p = [3.0, 0.0, 0.0];
1364        assert!((c.sdf(p) - c.signed_distance(p)).abs() < 1e-12);
1365    }
1366
1367    #[test]
1368    fn test_swept_capsule_vs_sphere_contact() {
1369        let c = Capsule::new(1.0, 1.0);
1370        // Capsule starts at x=10, moves to x=0 – sphere at origin with radius 0.5
1371        let result =
1372            c.swept_capsule_vs_sphere([10.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], 0.5);
1373        assert!(result.is_some(), "swept capsule should contact sphere");
1374        let t = result.unwrap();
1375        assert!((0.0..=1.0).contains(&t), "t should be in [0,1], got {t}");
1376    }
1377
1378    #[test]
1379    fn test_swept_capsule_vs_sphere_no_contact() {
1380        let c = Capsule::new(0.5, 0.5);
1381        // Capsule moves along X, sphere far away in Z
1382        let result =
1383            c.swept_capsule_vs_sphere([0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [0.0, 0.0, 100.0], 0.5);
1384        assert!(result.is_none(), "should not contact distant sphere");
1385    }
1386
1387    #[test]
1388    fn test_random_surface_points_count() {
1389        let c = Capsule::new(1.0, 2.0);
1390        let pts = c.random_surface_points(50, 42);
1391        assert_eq!(pts.len(), 50);
1392    }
1393
1394    #[test]
1395    fn test_random_surface_points_on_surface() {
1396        let c = Capsule::new(1.0, 2.0);
1397        let pts = c.random_surface_points(100, 7);
1398        for p in &pts {
1399            // Each point should be within radius+eps of the surface
1400            let sdf = c.sdf(*p);
1401            assert!(sdf.abs() < 0.05, "point {:?} has sdf={sdf}", p);
1402        }
1403    }
1404
1405    #[test]
1406    fn test_point_segment_distance_midpoint() {
1407        // Point above midpoint of segment along X
1408        let d = Capsule::point_segment_distance([0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1409        assert!((d - 1.0).abs() < 1e-12);
1410    }
1411
1412    #[test]
1413    fn test_inertia_tensor_raw_matches_array() {
1414        let c = Capsule::new(1.5, 2.5);
1415        let raw = c.inertia_tensor_raw(3.0);
1416        let arr = c.inertia_tensor_array(3.0);
1417        for i in 0..3 {
1418            for j in 0..3 {
1419                assert!((raw[i][j] - arr[i][j]).abs() < 1e-12);
1420            }
1421        }
1422    }
1423
1424    // ── New 30-50 expanded tests ──
1425
1426    #[test]
1427    fn test_capsule_triangle_contact_no_overlap() {
1428        let c = Capsule::new(0.5, 1.0);
1429        // Triangle far away from capsule
1430        let a = [10.0, 0.0, 0.0];
1431        let b = [11.0, 0.0, 0.0];
1432        let tri = [10.0_f64, 0.0_f64, 1.0_f64];
1433        let result = c.capsule_triangle_contact([0.0, 0.0, 0.0], a, b, tri);
1434        assert!(
1435            result.is_none(),
1436            "should be no contact with distant triangle"
1437        );
1438    }
1439
1440    #[test]
1441    fn test_capsule_triangle_contact_overlap() {
1442        let c = Capsule::new(1.0, 1.0);
1443        // Large triangle containing the capsule axis
1444        let a = [-5.0, 0.0, -5.0];
1445        let b = [5.0, 0.0, -5.0];
1446        let tri = [0.0_f64, 0.0_f64, 5.0_f64];
1447        let result = c.capsule_triangle_contact([0.0, 0.5, 0.0], a, b, tri);
1448        assert!(
1449            result.is_some(),
1450            "capsule near triangle should produce contact"
1451        );
1452    }
1453
1454    #[test]
1455    fn test_capsule_chain_empty() {
1456        let links = CapsuleChain::new(vec![]);
1457        assert_eq!(links.segment_count(), 0);
1458    }
1459
1460    #[test]
1461    fn test_capsule_chain_single_point() {
1462        let links = CapsuleChain::new(vec![[0.0, 0.0, 0.0]]);
1463        assert_eq!(links.segment_count(), 0);
1464    }
1465
1466    #[test]
1467    fn test_capsule_chain_two_points() {
1468        let links = CapsuleChain::new(vec![[0.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
1469        assert_eq!(links.segment_count(), 1);
1470    }
1471
1472    #[test]
1473    fn test_capsule_chain_total_length() {
1474        let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
1475        let chain = CapsuleChain::new(pts);
1476        assert!((chain.total_length() - 2.0).abs() < 1e-10);
1477    }
1478
1479    #[test]
1480    fn test_capsule_chain_contains_point_near_segment() {
1481        let pts = vec![[0.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
1482        let chain = CapsuleChain::with_radius(pts, 1.0);
1483        assert!(chain.contains_point([0.5, 1.0, 0.0]));
1484        assert!(!chain.contains_point([2.0, 1.0, 0.0]));
1485    }
1486
1487    #[test]
1488    fn test_deformable_capsule_update_endpoints() {
1489        let mut dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1490        dc.set_endpoint_a([1.0, 0.0, 0.0]);
1491        dc.set_endpoint_b([1.0, 2.0, 0.0]);
1492        assert!((dc.endpoint_a()[0] - 1.0).abs() < 1e-12);
1493        assert!((dc.endpoint_b()[1] - 2.0).abs() < 1e-12);
1494    }
1495
1496    #[test]
1497    fn test_deformable_capsule_length() {
1498        let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 3.0, 0.0], 0.5);
1499        assert!((dc.length() - 3.0).abs() < 1e-10);
1500    }
1501
1502    #[test]
1503    fn test_deformable_capsule_midpoint() {
1504        let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], 0.5);
1505        let mid = dc.midpoint();
1506        assert!((mid[0] - 1.0).abs() < 1e-12);
1507    }
1508
1509    #[test]
1510    fn test_capsule_medial_axis_3d_segment() {
1511        let dc = DeformableCapsule::new([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], 0.5);
1512        let (a, b) = dc.medial_axis_endpoints();
1513        assert!((a[0] - 1.0).abs() < 1e-12);
1514        assert!((b[2] - 6.0).abs() < 1e-12);
1515    }
1516
1517    #[test]
1518    fn test_deformable_capsule_sdf_inside() {
1519        let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 2.0, 0.0], 1.0);
1520        // Point on the axis inside the capsule
1521        assert!(dc.sdf([0.0, 1.0, 0.0]) < 0.0);
1522    }
1523
1524    #[test]
1525    fn test_deformable_capsule_sdf_outside() {
1526        let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 2.0, 0.0], 0.5);
1527        // Point far away
1528        assert!(dc.sdf([5.0, 1.0, 0.0]) > 0.0);
1529    }
1530
1531    #[test]
1532    fn test_capsule_frustum_volume_positive() {
1533        let f = CapsuleFrustum::new(0.5, 1.0, 2.0);
1534        assert!(f.volume() > 0.0);
1535    }
1536
1537    #[test]
1538    fn test_capsule_frustum_contains_center() {
1539        let f = CapsuleFrustum::new(0.5, 1.0, 2.0);
1540        // Center along axis should be inside
1541        assert!(f.contains_point([0.0, 1.0, 0.0]));
1542    }
1543
1544    #[test]
1545    fn test_capsule_frustum_excludes_far_point() {
1546        let f = CapsuleFrustum::new(0.5, 1.0, 2.0);
1547        assert!(!f.contains_point([10.0, 0.0, 0.0]));
1548    }
1549
1550    #[test]
1551    fn test_curved_capsule_path_length() {
1552        // Path with 3 points forming an L
1553        let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
1554        let cp = CurvedCapsulePath::new(pts, 0.3);
1555        let len = cp.path_length();
1556        assert!((len - 2.0).abs() < 1e-10);
1557    }
1558
1559    #[test]
1560    fn test_curved_capsule_path_contains_axis_point() {
1561        let pts = vec![[0.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
1562        let cp = CurvedCapsulePath::new(pts, 0.5);
1563        assert!(cp.contains_point([0.0, 1.0, 0.0]));
1564    }
1565
1566    #[test]
1567    fn test_curved_capsule_path_excludes_far_point() {
1568        let pts = vec![[0.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
1569        let cp = CurvedCapsulePath::new(pts, 0.5);
1570        assert!(!cp.contains_point([5.0, 0.0, 0.0]));
1571    }
1572
1573    #[test]
1574    fn test_swept_capsule_contact_time_order() {
1575        let c = Capsule::new(0.5, 1.0);
1576        // Capsule moving from far away to the origin
1577        let t = c.swept_capsule_vs_sphere([20.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], 0.1);
1578        assert!(t.is_some());
1579        let tv = t.unwrap();
1580        assert!(tv > 0.0 && tv <= 1.0);
1581    }
1582
1583    #[test]
1584    fn test_capsule_capsule_touching_boundary() {
1585        let a = Capsule::new(1.0, 1.0);
1586        let b = Capsule::new(1.0, 1.0);
1587        // Place them exactly touching: axis distance = sum of radii = 2
1588        let d = a.capsule_capsule_distance([0.0, 0.0, 0.0], &b, [2.0, 0.0, 0.0]);
1589        assert!(
1590            d.abs() < 1e-10,
1591            "touching capsules should have 0 distance, got {d}"
1592        );
1593    }
1594
1595    #[test]
1596    fn test_segment_segment_closest_endpoints() {
1597        // Two perpendicular segments not crossing; closest is the tips
1598        let (pa, pb, dist) = Capsule::segment_segment_closest(
1599            [0.0, 0.0, 0.0],
1600            [1.0, 0.0, 0.0],
1601            [2.0, 1.0, 0.0],
1602            [2.0, 2.0, 0.0],
1603        );
1604        assert!(dist > 0.0);
1605        let _ = (pa, pb);
1606    }
1607
1608    #[test]
1609    fn test_capsule_closest_point_on_surface_norm() {
1610        let c = Capsule::new(1.0, 2.0);
1611        let p = [3.0, 1.0, 4.0];
1612        let cp = c.closest_point(p);
1613        // Verify the closest point is on the capsule surface (sdf ~ 0)
1614        let sdf = c.sdf(cp);
1615        assert!(
1616            sdf.abs() < 1e-9,
1617            "closest point sdf should be ~0, got {sdf}"
1618        );
1619    }
1620
1621    #[test]
1622    fn test_capsule_bounding_box_correct() {
1623        let c = Capsule::new(1.0, 2.0);
1624        use crate::shape::Shape;
1625        let bb = c.bounding_box();
1626        // Bounding box should extend r=1 in X/Z, half_height+r=3 in Y
1627        assert!((bb.max.y - 3.0).abs() < 1e-10);
1628        assert!((bb.max.x - 1.0).abs() < 1e-10);
1629    }
1630
1631    #[test]
1632    fn test_capsule_chain_segment_radii() {
1633        let pts = vec![
1634            [0.0, 0.0, 0.0],
1635            [1.0, 0.0, 0.0],
1636            [2.0, 0.0, 0.0],
1637            [2.0, 1.0, 0.0],
1638        ];
1639        let chain = CapsuleChain::with_radius(pts, 0.3);
1640        assert_eq!(chain.segment_count(), 3);
1641    }
1642
1643    #[test]
1644    fn test_deformable_capsule_support_positive_y() {
1645        let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 4.0, 0.0], 1.0);
1646        let sp = dc.support([0.0, 1.0, 0.0]);
1647        // Should be at top cap: (0, 4+1, 0)
1648        assert!(sp[1] > 4.0, "support in +Y should be above endpoint b");
1649    }
1650
1651    #[test]
1652    fn test_capsule_frustum_sdf_on_axis_top() {
1653        // At the top (y=height), sdf should be ~0 or negative
1654        let f = CapsuleFrustum::new(0.5, 1.0, 3.0);
1655        // center at y=1.5, top cap at y=3 + r_top=1.0 => y=4 is outside
1656        let sdf_inside = f.sdf([0.0, 1.5, 0.0]);
1657        assert!(
1658            sdf_inside < 0.0,
1659            "center should be inside frustum, sdf={sdf_inside}"
1660        );
1661    }
1662
1663    #[test]
1664    fn test_curved_capsule_path_segment_count() {
1665        let pts: Vec<[f64; 3]> = (0..5).map(|i| [i as f64, 0.0, 0.0]).collect();
1666        let cp = CurvedCapsulePath::new(pts, 0.2);
1667        assert_eq!(cp.num_segments(), 4);
1668    }
1669
1670    #[test]
1671    fn test_capsule_chain_sdf_on_axis() {
1672        let pts = vec![[0.0, 0.0, 0.0], [0.0, 3.0, 0.0]];
1673        let chain = CapsuleChain::with_radius(pts, 1.0);
1674        // Point on the axis inside the chain
1675        let sdf = chain.sdf([0.0, 1.5, 0.0]);
1676        assert!(sdf < 0.0, "axis point should be inside chain, sdf={sdf}");
1677    }
1678
1679    #[test]
1680    fn test_capsule_triangle_contact_normal_points_away() {
1681        let c = Capsule::new(1.0, 1.0);
1682        // Triangle at y=-0.3, large and centered
1683        let a = [-10.0, -0.3, -10.0];
1684        let b = [10.0, -0.3, -10.0];
1685        let tri = [0.0_f64, -0.3_f64, 10.0_f64];
1686        if let Some((depth, normal)) = c.capsule_triangle_contact([0.0, 0.0, 0.0], a, b, tri) {
1687            assert!(depth > 0.0, "depth should be positive, got {depth}");
1688            // Normal should be unit length (either +Y or -Y for a horizontal triangle)
1689            let n_len =
1690                (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
1691            assert!(
1692                (n_len - 1.0).abs() < 1e-9,
1693                "normal should be unit length, got {n_len}"
1694            );
1695        }
1696    }
1697
1698    #[test]
1699    fn test_deformable_capsule_arbitrary_orientation() {
1700        // Capsule along X axis
1701        let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [4.0, 0.0, 0.0], 1.0);
1702        assert!(dc.sdf([2.0, 0.0, 0.0]) < 0.0); // on axis
1703        assert!(dc.sdf([2.0, 1.5, 0.0]) > 0.0); // outside radius
1704    }
1705
1706    #[test]
1707    fn test_capsule_chain_min_distance_to_point() {
1708        let pts = vec![[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]];
1709        let chain = CapsuleChain::with_radius(pts, 0.5);
1710        let d = chain.min_distance_to_point([2.0, 3.0, 0.0]);
1711        // Closest axis point is (2,0,0), dist to that is 3, minus radius 0.5 = 2.5
1712        assert!((d - 2.5).abs() < 1e-9, "expected 2.5, got {d}");
1713    }
1714
1715    #[test]
1716    fn test_capsule_frustum_larger_radius_at_top() {
1717        let f = CapsuleFrustum::new(0.5, 2.0, 3.0);
1718        // At the top, radius should be larger
1719        assert!(f.radius_at_height(3.0) > f.radius_at_height(0.0));
1720    }
1721
1722    #[test]
1723    fn test_capsule_frustum_radius_interpolated() {
1724        let f = CapsuleFrustum::new(1.0, 3.0, 4.0);
1725        // At height 2 (midpoint), radius should be midway between 1 and 3
1726        let r_mid = f.radius_at_height(2.0);
1727        assert!(
1728            (r_mid - 2.0).abs() < 1e-9,
1729            "midpoint radius should be 2.0, got {r_mid}"
1730        );
1731    }
1732}