1use crate::shape::{RayHit, Shape};
7use oxiphysics_core::Aabb;
8use oxiphysics_core::math::{Mat3, Real, Vec3};
9use std::f64::consts::PI;
10
11#[derive(Debug, Clone)]
13pub struct Capsule {
14 pub radius: Real,
16 pub half_height: Real,
18}
19
20impl Capsule {
21 pub fn new(radius: Real, half_height: Real) -> Self {
23 Self {
24 radius,
25 half_height,
26 }
27 }
28
29 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 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 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 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 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 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 pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
106 let clamped_y = p[1].clamp(-self.half_height, self.half_height);
108 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 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 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 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 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 pub fn full_length(&self) -> f64 {
150 2.0 * self.half_height + 2.0 * self.radius
151 }
152
153 pub fn medial_axis_length(&self) -> f64 {
155 2.0 * self.half_height
156 }
157
158 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 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 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 pub fn capsule_capsule_distance(
226 &self,
227 center_a: [f64; 3],
228 other: &Capsule,
229 center_b: [f64; 3],
230 ) -> f64 {
231 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 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 pub fn project_on_medial_axis(&self, p: [f64; 3]) -> f64 {
261 p[1].clamp(-self.half_height, self.half_height)
262 }
263
264 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 pub fn inertia_tensor_raw(&self, mass: f64) -> [[f64; 3]; 3] {
276 self.inertia_tensor_array(mass)
277 }
278
279 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 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 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 let expanded_half = [
369 obb_half[0] + self.radius,
370 obb_half[1] + self.radius,
371 obb_half[2] + self.radius,
372 ];
373
374 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 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 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 pub fn sdf(&self, p: [f64; 3]) -> f64 {
457 self.signed_distance(p)
458 }
459
460 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 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 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 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 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 *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 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
626pub struct CapsuleChain {
627 pub points: Vec<[f64; 3]>,
629 pub radius: f64,
631}
632
633impl CapsuleChain {
634 pub fn new(points: Vec<[f64; 3]>) -> Self {
636 Self {
637 points,
638 radius: 0.0,
639 }
640 }
641
642 pub fn with_radius(points: Vec<[f64; 3]>, radius: f64) -> Self {
644 Self { points, radius }
645 }
646
647 pub fn segment_count(&self) -> usize {
649 self.points.len().saturating_sub(1)
650 }
651
652 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 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 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 pub fn min_distance_to_point(&self, p: [f64; 3]) -> f64 {
684 self.sdf(p)
685 }
686}
687
688#[derive(Debug, Clone)]
695pub struct DeformableCapsule {
696 pub a: [f64; 3],
698 pub b: [f64; 3],
700 pub radius: f64,
702}
703
704impl DeformableCapsule {
705 pub fn new(a: [f64; 3], b: [f64; 3], radius: f64) -> Self {
707 Self { a, b, radius }
708 }
709
710 pub fn endpoint_a(&self) -> [f64; 3] {
712 self.a
713 }
714
715 pub fn endpoint_b(&self) -> [f64; 3] {
717 self.b
718 }
719
720 pub fn set_endpoint_a(&mut self, a: [f64; 3]) {
722 self.a = a;
723 }
724
725 pub fn set_endpoint_b(&mut self, b: [f64; 3]) {
727 self.b = b;
728 }
729
730 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 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 pub fn medial_axis_endpoints(&self) -> ([f64; 3], [f64; 3]) {
751 (self.a, self.b)
752 }
753
754 pub fn sdf(&self, p: [f64; 3]) -> f64 {
756 Capsule::point_segment_distance(p, self.a, self.b) - self.radius
757 }
758
759 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#[derive(Debug, Clone)]
784pub struct CapsuleFrustum {
785 pub r_bottom: f64,
787 pub r_top: f64,
789 pub height: f64,
791}
792
793impl CapsuleFrustum {
794 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 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 pub fn volume(&self) -> f64 {
814 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 pub fn contains_point(&self, p: [f64; 3]) -> bool {
826 self.sdf(p) <= 0.0
827 }
828
829 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#[derive(Debug, Clone)]
847pub struct CurvedCapsulePath {
848 pub path: Vec<[f64; 3]>,
850 pub radius: f64,
852}
853
854impl CurvedCapsulePath {
855 pub fn new(path: Vec<[f64; 3]>, radius: f64) -> Self {
857 Self { path, radius }
858 }
859
860 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 pub fn num_segments(&self) -> usize {
873 self.path.len().saturating_sub(1)
874 }
875
876 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 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 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 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 let iy_cyl = 0.5 * mass_cyl * r2;
941 let ixz_cyl = mass_cyl * (3.0 * r2 + h2) / 12.0;
942
943 let iy_sphere = 0.4 * mass_sphere * r2;
945 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 let mut best: Option<RayHit> = None;
958
959 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 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 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 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 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 assert!(it[0][0] > 0.0);
1082 assert!(it[1][1] > 0.0);
1083 assert!(it[2][2] > 0.0);
1084 assert!((it[0][0] - it[2][2]).abs() < 1e-10);
1086 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 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 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 assert!(
1132 (hit.toi - 7.0).abs() < 1e-10,
1133 "expected toi=7, got {}",
1134 hit.toi
1135 );
1136 }
1137
1138 #[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 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 let cp = c.closest_point([0.0, 0.0, 0.0]);
1170 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])); assert!(c.contains_point([0.5, 1.0, 0.0])); assert!(c.contains_point([0.0, 2.5, 0.0])); assert!(!c.contains_point([0.0, 3.5, 0.0])); assert!(!c.contains_point([2.0, 0.0, 0.0])); }
1184
1185 #[test]
1186 fn test_capsule_signed_distance() {
1187 let c = Capsule::new(2.0, 1.0);
1188 assert!((c.signed_distance([0.0, 0.0, 0.0]) + 2.0).abs() < 1e-10);
1190 assert!(c.signed_distance([2.0, 0.0, 0.0]).abs() < 1e-10);
1192 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 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 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 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 let d = a.capsule_capsule_distance([0.0, 0.0, 0.0], &b, [5.0, 0.0, 0.0]);
1269 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); }
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 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 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 #[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 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 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 let axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1354 assert!(c.intersects_obb([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], axes, [2.0, 2.0, 2.0]));
1356 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 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 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 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 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 #[test]
1427 fn test_capsule_triangle_contact_no_overlap() {
1428 let c = Capsule::new(0.5, 1.0);
1429 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 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 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 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 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 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 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 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 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 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 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 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 let f = CapsuleFrustum::new(0.5, 1.0, 3.0);
1655 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 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 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 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 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); assert!(dc.sdf([2.0, 1.5, 0.0]) > 0.0); }
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 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 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 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}