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 #[allow(dead_code)]
31 pub fn volume_explicit(&self) -> Real {
32 let r = self.radius;
33 let h = 2.0 * self.half_height;
34 PI * r * r * h + (4.0 / 3.0) * PI * r.powi(3)
35 }
36
37 #[allow(dead_code)]
39 pub fn surface_area(&self) -> Real {
40 let r = self.radius;
41 let h = 2.0 * self.half_height;
42 2.0 * PI * r * h + 4.0 * PI * r * r
43 }
44
45 #[allow(dead_code)]
47 pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
48 let r = self.radius;
49 let h = 2.0 * self.half_height;
50 let r2 = r * r;
51 let h2 = h * h;
52
53 let vol_cyl = PI * r2 * h;
54 let vol_sphere = (4.0 / 3.0) * PI * r.powi(3);
55 let total_vol = vol_cyl + vol_sphere;
56
57 let mass_cyl = mass * vol_cyl / total_vol;
58 let mass_sphere = mass * vol_sphere / total_vol;
59
60 let iy_cyl = 0.5 * mass_cyl * r2;
61 let ixz_cyl = mass_cyl * (3.0 * r2 + h2) / 12.0;
62
63 let iy_sphere = 0.4 * mass_sphere * r2;
64 let offset = self.half_height + 3.0 * r / 8.0;
65 let ixz_sphere = 0.4 * mass_sphere * r2 + mass_sphere * offset * offset;
66
67 let iy = iy_cyl + iy_sphere;
68 let ixz = ixz_cyl + ixz_sphere;
69
70 [[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
71 }
72
73 #[allow(dead_code)]
75 pub fn ray_cast_array(
76 &self,
77 origin: [f64; 3],
78 direction: [f64; 3],
79 max_toi: f64,
80 ) -> Option<(f64, [f64; 3])> {
81 let o = Vec3::new(origin[0], origin[1], origin[2]);
82 let d = Vec3::new(direction[0], direction[1], direction[2]);
83 let hit = self.ray_cast(&o, &d, max_toi)?;
84 Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
85 }
86
87 #[allow(dead_code)]
89 pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
90 let len = (direction[0] * direction[0]
91 + direction[1] * direction[1]
92 + direction[2] * direction[2])
93 .sqrt();
94 let cap_y = if direction[1] >= 0.0 {
96 self.half_height
97 } else {
98 -self.half_height
99 };
100 if len < 1e-12 {
101 return [0.0, cap_y, 0.0];
102 }
103 let s = self.radius / len;
104 [direction[0] * s, cap_y + direction[1] * s, direction[2] * s]
105 }
106
107 #[allow(dead_code)]
111 pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
112 let clamped_y = p[1].clamp(-self.half_height, self.half_height);
114 let dx = p[0];
116 let dy = p[1] - clamped_y;
117 let dz = p[2];
118 let len = (dx * dx + dy * dy + dz * dz).sqrt();
119
120 if len < 1e-12 {
121 return [self.radius, clamped_y, 0.0];
123 }
124
125 let s = self.radius / len;
126 [dx * s, clamped_y + dy * s, dz * s]
127 }
128
129 #[allow(dead_code)]
131 pub fn contains_point(&self, p: [f64; 3]) -> bool {
132 let clamped_y = p[1].clamp(-self.half_height, self.half_height);
133 let dx = p[0];
134 let dy = p[1] - clamped_y;
135 let dz = p[2];
136 dx * dx + dy * dy + dz * dz <= self.radius * self.radius
137 }
138
139 #[allow(dead_code)]
142 pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
143 let clamped_y = p[1].clamp(-self.half_height, self.half_height);
144 let dx = p[0];
145 let dy = p[1] - clamped_y;
146 let dz = p[2];
147 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
148 dist - self.radius
149 }
150
151 #[allow(dead_code)]
153 pub fn medial_axis_endpoints(&self) -> ([f64; 3], [f64; 3]) {
154 ([0.0, self.half_height, 0.0], [0.0, -self.half_height, 0.0])
155 }
156
157 #[allow(dead_code)]
159 pub fn full_length(&self) -> f64 {
160 2.0 * self.half_height + 2.0 * self.radius
161 }
162
163 #[allow(dead_code)]
165 pub fn medial_axis_length(&self) -> f64 {
166 2.0 * self.half_height
167 }
168
169 #[allow(dead_code)]
173 pub fn segment_segment_distance(a0: [f64; 3], a1: [f64; 3], b0: [f64; 3], b1: [f64; 3]) -> f64 {
174 let da = [a1[0] - a0[0], a1[1] - a0[1], a1[2] - a0[2]];
175 let db = [b1[0] - b0[0], b1[1] - b0[1], b1[2] - b0[2]];
176 let r = [a0[0] - b0[0], a0[1] - b0[1], a0[2] - b0[2]];
177
178 let a = da[0] * da[0] + da[1] * da[1] + da[2] * da[2];
179 let e = db[0] * db[0] + db[1] * db[1] + db[2] * db[2];
180 let f = db[0] * r[0] + db[1] * r[1] + db[2] * r[2];
181
182 let eps = 1e-12;
183
184 if a <= eps && e <= eps {
185 let dx = r[0];
187 let dy = r[1];
188 let dz = r[2];
189 return (dx * dx + dy * dy + dz * dz).sqrt();
190 }
191
192 let b = da[0] * db[0] + da[1] * db[1] + da[2] * db[2];
193 let c = da[0] * r[0] + da[1] * r[1] + da[2] * r[2];
194
195 let (s, t);
196
197 if a <= eps {
198 s = 0.0;
199 t = (f / e).clamp(0.0, 1.0);
200 } else if e <= eps {
201 t = 0.0;
202 s = (-c / a).clamp(0.0, 1.0);
203 } else {
204 let denom = a * e - b * b;
205 s = if denom.abs() > eps {
206 ((b * f - c * e) / denom).clamp(0.0, 1.0)
207 } else {
208 0.0
209 };
210 let t_nom = b * s + f;
211 if t_nom < 0.0 {
212 t = 0.0;
213 } else if t_nom > e {
214 t = 1.0;
215 } else {
216 t = t_nom / e;
217 }
218 }
219
220 let s = if a > eps {
222 ((b * t - c) / a).clamp(0.0, 1.0)
223 } else {
224 s
225 };
226
227 let dx = r[0] + da[0] * s - db[0] * t;
228 let dy = r[1] + da[1] * s - db[1] * t;
229 let dz = r[2] + da[2] * s - db[2] * t;
230 (dx * dx + dy * dy + dz * dz).sqrt()
231 }
232
233 #[allow(dead_code)]
238 pub fn capsule_capsule_distance(
239 &self,
240 center_a: [f64; 3],
241 other: &Capsule,
242 center_b: [f64; 3],
243 ) -> f64 {
244 let a0 = [center_a[0], center_a[1] - self.half_height, center_a[2]];
246 let a1 = [center_a[0], center_a[1] + self.half_height, center_a[2]];
247 let b0 = [center_b[0], center_b[1] - other.half_height, center_b[2]];
248 let b1 = [center_b[0], center_b[1] + other.half_height, center_b[2]];
249
250 let seg_dist = Self::segment_segment_distance(a0, a1, b0, b1);
251 let surface_dist = seg_dist - self.radius - other.radius;
252 surface_dist.max(0.0)
253 }
254
255 #[allow(dead_code)]
257 pub fn capsule_capsule_overlap(
258 &self,
259 center_a: [f64; 3],
260 other: &Capsule,
261 center_b: [f64; 3],
262 ) -> bool {
263 let a0 = [center_a[0], center_a[1] - self.half_height, center_a[2]];
264 let a1 = [center_a[0], center_a[1] + self.half_height, center_a[2]];
265 let b0 = [center_b[0], center_b[1] - other.half_height, center_b[2]];
266 let b1 = [center_b[0], center_b[1] + other.half_height, center_b[2]];
267
268 let seg_dist = Self::segment_segment_distance(a0, a1, b0, b1);
269 seg_dist <= self.radius + other.radius
270 }
271
272 #[allow(dead_code)]
275 pub fn project_on_medial_axis(&self, p: [f64; 3]) -> f64 {
276 p[1].clamp(-self.half_height, self.half_height)
277 }
278
279 #[allow(dead_code)]
281 pub fn distance_to_medial_axis(&self, p: [f64; 3]) -> f64 {
282 let clamped_y = p[1].clamp(-self.half_height, self.half_height);
283 let dy = p[1] - clamped_y;
284 (p[0] * p[0] + dy * dy + p[2] * p[2]).sqrt()
285 }
286
287 #[allow(dead_code)]
292 pub fn inertia_tensor_raw(&self, mass: f64) -> [[f64; 3]; 3] {
293 self.inertia_tensor_array(mass)
294 }
295
296 #[allow(dead_code)]
301 pub fn closest_points_capsule_vs_capsule(
302 &self,
303 center_a: [f64; 3],
304 other: &Capsule,
305 center_b: [f64; 3],
306 ) -> ([f64; 3], [f64; 3], f64) {
307 let a0 = [center_a[0], center_a[1] - self.half_height, center_a[2]];
308 let a1 = [center_a[0], center_a[1] + self.half_height, center_a[2]];
309 let b0 = [center_b[0], center_b[1] - other.half_height, center_b[2]];
310 let b1 = [center_b[0], center_b[1] + other.half_height, center_b[2]];
311
312 let (pa, pb, seg_dist) = Self::segment_segment_closest(a0, a1, b0, b1);
313 (pa, pb, seg_dist)
314 }
315
316 #[allow(dead_code)]
318 pub fn segment_segment_closest(
319 a0: [f64; 3],
320 a1: [f64; 3],
321 b0: [f64; 3],
322 b1: [f64; 3],
323 ) -> ([f64; 3], [f64; 3], f64) {
324 let da = [a1[0] - a0[0], a1[1] - a0[1], a1[2] - a0[2]];
325 let db = [b1[0] - b0[0], b1[1] - b0[1], b1[2] - b0[2]];
326 let r = [a0[0] - b0[0], a0[1] - b0[1], a0[2] - b0[2]];
327
328 let aa = da[0] * da[0] + da[1] * da[1] + da[2] * da[2];
329 let ee = db[0] * db[0] + db[1] * db[1] + db[2] * db[2];
330 let f = db[0] * r[0] + db[1] * r[1] + db[2] * r[2];
331 let eps = 1e-12;
332
333 let (s, t) = if aa <= eps && ee <= eps {
334 (0.0_f64, 0.0_f64)
335 } else if aa <= eps {
336 (0.0_f64, (f / ee).clamp(0.0, 1.0))
337 } else {
338 let c = da[0] * r[0] + da[1] * r[1] + da[2] * r[2];
339 if ee <= eps {
340 ((-c / aa).clamp(0.0, 1.0), 0.0_f64)
341 } else {
342 let b = da[0] * db[0] + da[1] * db[1] + da[2] * db[2];
343 let denom = aa * ee - b * b;
344 let s_cand = if denom.abs() > eps {
345 ((b * f - c * ee) / denom).clamp(0.0, 1.0)
346 } else {
347 0.0
348 };
349 let t_nom = b * s_cand + f;
350 let t_cand = if t_nom < 0.0 {
351 0.0
352 } else if t_nom > ee {
353 1.0
354 } else {
355 t_nom / ee
356 };
357 let s_final = ((b * t_cand - c) / aa).clamp(0.0, 1.0);
358 (s_final, t_cand)
359 }
360 };
361
362 let pa = [a0[0] + s * da[0], a0[1] + s * da[1], a0[2] + s * da[2]];
363 let pb = [b0[0] + t * db[0], b0[1] + t * db[1], b0[2] + t * db[2]];
364 let dx = pa[0] - pb[0];
365 let dy = pa[1] - pb[1];
366 let dz = pa[2] - pb[2];
367 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
368 (pa, pb, dist)
369 }
370
371 #[allow(dead_code)]
380 #[allow(clippy::too_many_arguments)]
381 pub fn intersects_obb(
382 &self,
383 capsule_center: [f64; 3],
384 obb_center: [f64; 3],
385 obb_axes: [[f64; 3]; 3],
386 obb_half: [f64; 3],
387 ) -> bool {
388 let expanded_half = [
390 obb_half[0] + self.radius,
391 obb_half[1] + self.radius,
392 obb_half[2] + self.radius,
393 ];
394
395 let a = [
397 capsule_center[0],
398 capsule_center[1] - self.half_height,
399 capsule_center[2],
400 ];
401 let b = [
402 capsule_center[0],
403 capsule_center[1] + self.half_height,
404 capsule_center[2],
405 ];
406
407 for pt in [a, b] {
409 let d = [
410 pt[0] - obb_center[0],
411 pt[1] - obb_center[1],
412 pt[2] - obb_center[2],
413 ];
414 let local = [
415 d[0] * obb_axes[0][0] + d[1] * obb_axes[0][1] + d[2] * obb_axes[0][2],
416 d[0] * obb_axes[1][0] + d[1] * obb_axes[1][1] + d[2] * obb_axes[1][2],
417 d[0] * obb_axes[2][0] + d[1] * obb_axes[2][1] + d[2] * obb_axes[2][2],
418 ];
419 if local[0].abs() <= expanded_half[0]
420 && local[1].abs() <= expanded_half[1]
421 && local[2].abs() <= expanded_half[2]
422 {
423 return true;
424 }
425 }
426
427 let da = [
430 a[0] - obb_center[0],
431 a[1] - obb_center[1],
432 a[2] - obb_center[2],
433 ];
434 let dir_w = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
435
436 let orig_local = [
437 da[0] * obb_axes[0][0] + da[1] * obb_axes[0][1] + da[2] * obb_axes[0][2],
438 da[0] * obb_axes[1][0] + da[1] * obb_axes[1][1] + da[2] * obb_axes[1][2],
439 da[0] * obb_axes[2][0] + da[1] * obb_axes[2][1] + da[2] * obb_axes[2][2],
440 ];
441 let dir_local = [
442 dir_w[0] * obb_axes[0][0] + dir_w[1] * obb_axes[0][1] + dir_w[2] * obb_axes[0][2],
443 dir_w[0] * obb_axes[1][0] + dir_w[1] * obb_axes[1][1] + dir_w[2] * obb_axes[1][2],
444 dir_w[0] * obb_axes[2][0] + dir_w[1] * obb_axes[2][1] + dir_w[2] * obb_axes[2][2],
445 ];
446
447 let mut t_min = 0.0_f64;
448 let mut t_max = 1.0_f64;
449
450 for axis in 0..3 {
451 let d_loc = dir_local[axis];
452 let o_loc = orig_local[axis];
453 let hh = expanded_half[axis];
454 if d_loc.abs() < 1e-12 {
455 if o_loc.abs() > hh {
456 return false;
457 }
458 } else {
459 let inv_d = 1.0 / d_loc;
460 let t0 = (-hh - o_loc) * inv_d;
461 let t1 = (hh - o_loc) * inv_d;
462 let (t_lo, t_hi) = if t0 < t1 { (t0, t1) } else { (t1, t0) };
463 t_min = t_min.max(t_lo);
464 t_max = t_max.min(t_hi);
465 if t_min > t_max {
466 return false;
467 }
468 }
469 }
470 true
471 }
472
473 #[allow(dead_code)]
478 pub fn sdf(&self, p: [f64; 3]) -> f64 {
479 self.signed_distance(p)
480 }
481
482 #[allow(dead_code)]
492 pub fn swept_capsule_vs_sphere(
493 &self,
494 center_start: [f64; 3],
495 center_end: [f64; 3],
496 sphere_center: [f64; 3],
497 sphere_radius: f64,
498 ) -> Option<f64> {
499 let combined_radius = self.radius + sphere_radius;
500
501 let n_steps = 64;
508 for i in 0..=n_steps {
509 let t = i as f64 / n_steps as f64;
510 let cx = center_start[0] + t * (center_end[0] - center_start[0]);
511 let cy = center_start[1] + t * (center_end[1] - center_start[1]);
512 let cz = center_start[2] + t * (center_end[2] - center_start[2]);
513
514 let a0 = [cx, cy - self.half_height, cz];
515 let a1 = [cx, cy + self.half_height, cz];
516 let dist = Self::point_segment_distance(sphere_center, a0, a1);
517 if dist <= combined_radius {
518 return Some(t);
519 }
520 }
521 None
522 }
523
524 #[allow(dead_code)]
526 fn point_segment_distance(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
527 let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
528 let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
529 let ab_sq = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
530 let t = if ab_sq < 1e-24 {
531 0.0
532 } else {
533 ((ab[0] * ap[0] + ab[1] * ap[1] + ab[2] * ap[2]) / ab_sq).clamp(0.0, 1.0)
534 };
535 let proj = [a[0] + t * ab[0], a[1] + t * ab[1], a[2] + t * ab[2]];
536 let dx = p[0] - proj[0];
537 let dy = p[1] - proj[1];
538 let dz = p[2] - proj[2];
539 (dx * dx + dy * dy + dz * dz).sqrt()
540 }
541
542 #[allow(dead_code)]
547 pub fn random_surface_points(&self, n: usize, seed: u64) -> Vec<[f64; 3]> {
548 let mut points = Vec::with_capacity(n);
549 let r = self.radius;
550 let hh = self.half_height;
551
552 let cyl_area = 2.0 * PI * r * 2.0 * hh;
554 let sphere_area = 4.0 * PI * r * r;
555 let total_area = cyl_area + sphere_area;
556 let p_cylinder = cyl_area / total_area;
557
558 let mut rng_state = seed;
559 let next_f64 = |state: &mut u64| -> f64 {
560 *state ^= *state << 13;
562 *state ^= *state >> 7;
563 *state ^= *state << 17;
564 (*state as f64) / (u64::MAX as f64)
565 };
566
567 while points.len() < n {
568 let u = next_f64(&mut rng_state);
569 let v = next_f64(&mut rng_state);
570 let w = next_f64(&mut rng_state);
571
572 if u < p_cylinder {
573 let theta = v * 2.0 * PI;
575 let y = (w * 2.0 - 1.0) * hh;
576 points.push([r * theta.cos(), y, r * theta.sin()]);
577 } else {
578 let theta = v * 2.0 * PI;
580 let phi = (w * 2.0 - 1.0).acos();
581 let x = r * phi.sin() * theta.cos();
582 let z = r * phi.sin() * theta.sin();
583 let raw_y = r * phi.cos();
584 let extra = next_f64(&mut rng_state);
586 let y = if extra < 0.5 {
587 hh + raw_y.abs()
588 } else {
589 -hh - raw_y.abs()
590 };
591 points.push([x, y, z]);
592 }
593 }
594 points
595 }
596
597 #[allow(dead_code)]
605 pub fn capsule_triangle_contact(
606 &self,
607 center: [f64; 3],
608 ta: [f64; 3],
609 tb: [f64; 3],
610 tc: [f64; 3],
611 ) -> Option<(f64, [f64; 3])> {
612 let e1 = [tb[0] - ta[0], tb[1] - ta[1], tb[2] - ta[2]];
614 let e2 = [tc[0] - ta[0], tc[1] - ta[1], tc[2] - ta[2]];
615 let raw_n = [
616 e1[1] * e2[2] - e1[2] * e2[1],
617 e1[2] * e2[0] - e1[0] * e2[2],
618 e1[0] * e2[1] - e1[1] * e2[0],
619 ];
620 let n_len = (raw_n[0] * raw_n[0] + raw_n[1] * raw_n[1] + raw_n[2] * raw_n[2]).sqrt();
621 if n_len < 1e-12 {
622 return None;
623 }
624 let n = [raw_n[0] / n_len, raw_n[1] / n_len, raw_n[2] / n_len];
625
626 let a0 = [center[0], center[1] - self.half_height, center[2]];
628 let a1 = [center[0], center[1] + self.half_height, center[2]];
629
630 let d0 = (a0[0] - ta[0]) * n[0] + (a0[1] - ta[1]) * n[1] + (a0[2] - ta[2]) * n[2];
632 let d1 = (a1[0] - ta[0]) * n[0] + (a1[1] - ta[1]) * n[1] + (a1[2] - ta[2]) * n[2];
633
634 let min_dist = d0.abs().min(d1.abs());
636 let penetration = self.radius - min_dist;
637 if penetration > 0.0 {
638 Some((penetration, n))
639 } else {
640 None
641 }
642 }
643}
644
645#[allow(dead_code)]
652#[derive(Debug, Clone)]
653pub struct CapsuleChain {
654 pub points: Vec<[f64; 3]>,
656 pub radius: f64,
658}
659
660#[allow(dead_code)]
661impl CapsuleChain {
662 pub fn new(points: Vec<[f64; 3]>) -> Self {
664 Self {
665 points,
666 radius: 0.0,
667 }
668 }
669
670 pub fn with_radius(points: Vec<[f64; 3]>, radius: f64) -> Self {
672 Self { points, radius }
673 }
674
675 pub fn segment_count(&self) -> usize {
677 self.points.len().saturating_sub(1)
678 }
679
680 pub fn total_length(&self) -> f64 {
682 self.points
683 .windows(2)
684 .map(|w| {
685 let dx = w[1][0] - w[0][0];
686 let dy = w[1][1] - w[0][1];
687 let dz = w[1][2] - w[0][2];
688 (dx * dx + dy * dy + dz * dz).sqrt()
689 })
690 .sum()
691 }
692
693 pub fn contains_point(&self, p: [f64; 3]) -> bool {
695 self.points
696 .windows(2)
697 .any(|w| Capsule::point_segment_distance(p, w[0], w[1]) <= self.radius)
698 }
699
700 pub fn sdf(&self, p: [f64; 3]) -> f64 {
702 let min_seg_dist = self
703 .points
704 .windows(2)
705 .map(|w| Capsule::point_segment_distance(p, w[0], w[1]))
706 .fold(f64::INFINITY, f64::min);
707 min_seg_dist - self.radius
708 }
709
710 pub fn min_distance_to_point(&self, p: [f64; 3]) -> f64 {
712 self.sdf(p)
713 }
714}
715
716#[allow(dead_code)]
723#[derive(Debug, Clone)]
724pub struct DeformableCapsule {
725 pub a: [f64; 3],
727 pub b: [f64; 3],
729 pub radius: f64,
731}
732
733#[allow(dead_code)]
734impl DeformableCapsule {
735 pub fn new(a: [f64; 3], b: [f64; 3], radius: f64) -> Self {
737 Self { a, b, radius }
738 }
739
740 pub fn endpoint_a(&self) -> [f64; 3] {
742 self.a
743 }
744
745 pub fn endpoint_b(&self) -> [f64; 3] {
747 self.b
748 }
749
750 pub fn set_endpoint_a(&mut self, a: [f64; 3]) {
752 self.a = a;
753 }
754
755 pub fn set_endpoint_b(&mut self, b: [f64; 3]) {
757 self.b = b;
758 }
759
760 pub fn length(&self) -> f64 {
762 let d = [
763 self.b[0] - self.a[0],
764 self.b[1] - self.a[1],
765 self.b[2] - self.a[2],
766 ];
767 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
768 }
769
770 pub fn midpoint(&self) -> [f64; 3] {
772 [
773 (self.a[0] + self.b[0]) * 0.5,
774 (self.a[1] + self.b[1]) * 0.5,
775 (self.a[2] + self.b[2]) * 0.5,
776 ]
777 }
778
779 pub fn medial_axis_endpoints(&self) -> ([f64; 3], [f64; 3]) {
781 (self.a, self.b)
782 }
783
784 pub fn sdf(&self, p: [f64; 3]) -> f64 {
786 Capsule::point_segment_distance(p, self.a, self.b) - self.radius
787 }
788
789 pub fn support(&self, dir: [f64; 3]) -> [f64; 3] {
791 let da = dir[0] * self.a[0] + dir[1] * self.a[1] + dir[2] * self.a[2];
792 let db = dir[0] * self.b[0] + dir[1] * self.b[1] + dir[2] * self.b[2];
793 let base = if da >= db { self.a } else { self.b };
794 let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
795 if len < 1e-12 {
796 return base;
797 }
798 let s = self.radius / len;
799 [
800 base[0] + dir[0] * s,
801 base[1] + dir[1] * s,
802 base[2] + dir[2] * s,
803 ]
804 }
805}
806
807#[allow(dead_code)]
814#[derive(Debug, Clone)]
815pub struct CapsuleFrustum {
816 pub r_bottom: f64,
818 pub r_top: f64,
820 pub height: f64,
822}
823
824#[allow(dead_code)]
825impl CapsuleFrustum {
826 pub fn new(r_bottom: f64, r_top: f64, height: f64) -> Self {
828 Self {
829 r_bottom,
830 r_top,
831 height,
832 }
833 }
834
835 pub fn radius_at_height(&self, y: f64) -> f64 {
837 if self.height < 1e-12 {
838 return (self.r_bottom + self.r_top) * 0.5;
839 }
840 let t = (y / self.height).clamp(0.0, 1.0);
841 self.r_bottom + t * (self.r_top - self.r_bottom)
842 }
843
844 pub fn volume(&self) -> f64 {
846 let r1 = self.r_bottom;
848 let r2 = self.r_top;
849 let h = self.height;
850 let cone_vol = std::f64::consts::PI / 3.0 * h * (r1 * r1 + r1 * r2 + r2 * r2);
851 let sphere_bot = (2.0 / 3.0) * std::f64::consts::PI * r1 * r1 * r1;
852 let sphere_top = (2.0 / 3.0) * std::f64::consts::PI * r2 * r2 * r2;
853 cone_vol + sphere_bot + sphere_top
854 }
855
856 pub fn contains_point(&self, p: [f64; 3]) -> bool {
858 self.sdf(p) <= 0.0
859 }
860
861 pub fn sdf(&self, p: [f64; 3]) -> f64 {
863 let y = p[1].clamp(0.0, self.height);
864 let r = self.radius_at_height(y);
865 let dy = p[1] - y;
866 let xz_dist = (p[0] * p[0] + p[2] * p[2]).sqrt();
867 let dist_to_axis = (xz_dist * xz_dist + dy * dy).sqrt();
868 dist_to_axis - r
869 }
870}
871
872#[allow(dead_code)]
879#[derive(Debug, Clone)]
880pub struct CurvedCapsulePath {
881 pub path: Vec<[f64; 3]>,
883 pub radius: f64,
885}
886
887#[allow(dead_code)]
888impl CurvedCapsulePath {
889 pub fn new(path: Vec<[f64; 3]>, radius: f64) -> Self {
891 Self { path, radius }
892 }
893
894 pub fn path_length(&self) -> f64 {
896 self.path
897 .windows(2)
898 .map(|w| {
899 let d = [w[1][0] - w[0][0], w[1][1] - w[0][1], w[1][2] - w[0][2]];
900 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
901 })
902 .sum()
903 }
904
905 pub fn num_segments(&self) -> usize {
907 self.path.len().saturating_sub(1)
908 }
909
910 pub fn contains_point(&self, p: [f64; 3]) -> bool {
912 self.path
913 .windows(2)
914 .any(|w| Capsule::point_segment_distance(p, w[0], w[1]) <= self.radius)
915 }
916
917 pub fn sdf(&self, p: [f64; 3]) -> f64 {
919 let min_d = self
920 .path
921 .windows(2)
922 .map(|w| Capsule::point_segment_distance(p, w[0], w[1]))
923 .fold(f64::INFINITY, f64::min);
924 min_d - self.radius
925 }
926}
927
928impl Shape for Capsule {
929 fn bounding_box(&self) -> Aabb {
930 let r = self.radius;
931 let h = self.half_height + r;
932 Aabb::new(Vec3::new(-r, -h, -r), Vec3::new(r, h, r))
933 }
934
935 fn support_point(&self, direction: &Vec3) -> Vec3 {
936 let norm = direction.norm();
937 let cap_center = if direction.y >= 0.0 {
938 Vec3::new(0.0, self.half_height, 0.0)
939 } else {
940 Vec3::new(0.0, -self.half_height, 0.0)
941 };
942 if norm < 1e-10 {
943 return cap_center;
944 }
945 cap_center + direction * (self.radius / norm)
946 }
947
948 fn volume(&self) -> Real {
949 let r = self.radius;
950 let h = 2.0 * self.half_height;
951 PI * r * r * h + (4.0 / 3.0) * PI * r.powi(3)
953 }
954
955 fn center_of_mass(&self) -> Vec3 {
956 Vec3::zeros()
957 }
958
959 fn inertia_tensor(&self, mass: Real) -> Mat3 {
960 let r = self.radius;
961 let h = 2.0 * self.half_height;
962 let r2 = r * r;
963 let h2 = h * h;
964
965 let vol_cyl = PI * r2 * h;
967 let vol_sphere = (4.0 / 3.0) * PI * r.powi(3);
968 let total_vol = vol_cyl + vol_sphere;
969
970 let mass_cyl = mass * vol_cyl / total_vol;
971 let mass_sphere = mass * vol_sphere / total_vol;
972
973 let iy_cyl = 0.5 * mass_cyl * r2;
975 let ixz_cyl = mass_cyl * (3.0 * r2 + h2) / 12.0;
976
977 let iy_sphere = 0.4 * mass_sphere * r2;
979 let offset = self.half_height + 3.0 * r / 8.0;
981 let ixz_sphere = 0.4 * mass_sphere * r2 + mass_sphere * offset * offset;
982
983 let iy = iy_cyl + iy_sphere;
984 let ixz = ixz_cyl + ixz_sphere;
985
986 Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
987 }
988
989 fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
990 let mut best: Option<RayHit> = None;
992
993 let top_center = Vec3::new(0.0, self.half_height, 0.0);
995 if let Some(hit) = ray_sphere(ray_origin, ray_direction, &top_center, self.radius)
996 && hit.toi <= max_toi
997 && hit.point.y >= self.half_height
998 && best.as_ref().is_none_or(|b| hit.toi < b.toi)
999 {
1000 best = Some(hit);
1001 }
1002
1003 let bot_center = Vec3::new(0.0, -self.half_height, 0.0);
1005 if let Some(hit) = ray_sphere(ray_origin, ray_direction, &bot_center, self.radius)
1006 && hit.toi <= max_toi
1007 && hit.point.y <= -self.half_height
1008 && best.as_ref().is_none_or(|b| hit.toi < b.toi)
1009 {
1010 best = Some(hit);
1011 }
1012
1013 if let Some(hit) = ray_cylinder_y(ray_origin, ray_direction, self.radius, self.half_height)
1015 && hit.toi <= max_toi
1016 && best.as_ref().is_none_or(|b| hit.toi < b.toi)
1017 {
1018 best = Some(hit);
1019 }
1020
1021 best
1022 }
1023}
1024
1025fn ray_sphere(origin: &Vec3, direction: &Vec3, center: &Vec3, radius: Real) -> Option<RayHit> {
1026 let oc = origin - center;
1027 let a = direction.dot(direction);
1028 let b = 2.0 * oc.dot(direction);
1029 let c = oc.dot(&oc) - radius * radius;
1030 let disc = b * b - 4.0 * a * c;
1031 if disc < 0.0 {
1032 return None;
1033 }
1034 let sqrt_disc = disc.sqrt();
1035 let t1 = (-b - sqrt_disc) / (2.0 * a);
1036 let t2 = (-b + sqrt_disc) / (2.0 * a);
1037 let t = if t1 >= 0.0 { t1 } else { t2 };
1038 if t < 0.0 {
1039 return None;
1040 }
1041 let point = origin + direction * t;
1042 let normal = (point - center).normalize();
1043 Some(RayHit {
1044 point,
1045 normal,
1046 toi: t,
1047 })
1048}
1049
1050fn ray_cylinder_y(
1051 origin: &Vec3,
1052 direction: &Vec3,
1053 radius: Real,
1054 half_height: Real,
1055) -> Option<RayHit> {
1056 let a = direction.x * direction.x + direction.z * direction.z;
1058 let b = 2.0 * (origin.x * direction.x + origin.z * direction.z);
1059 let c = origin.x * origin.x + origin.z * origin.z - radius * radius;
1060 let disc = b * b - 4.0 * a * c;
1061 if disc < 0.0 || a < 1e-12 {
1062 return None;
1063 }
1064 let sqrt_disc = disc.sqrt();
1065 let t1 = (-b - sqrt_disc) / (2.0 * a);
1066 let t2 = (-b + sqrt_disc) / (2.0 * a);
1067
1068 for t in [t1, t2] {
1069 if t < 0.0 {
1070 continue;
1071 }
1072 let point = origin + direction * t;
1073 if point.y.abs() <= half_height {
1074 let normal = Vec3::new(point.x, 0.0, point.z).normalize();
1075 return Some(RayHit {
1076 point,
1077 normal,
1078 toi: t,
1079 });
1080 }
1081 }
1082 None
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087 use super::*;
1088
1089 #[test]
1090 fn test_capsule_volume() {
1091 let c = Capsule::new(1.0, 1.0);
1092 let expected = PI * 1.0 * 2.0 + (4.0 / 3.0) * PI;
1093 assert!((c.volume() - expected).abs() < 1e-10);
1094 }
1095
1096 #[test]
1097 fn test_capsule_volume_explicit() {
1098 let c = Capsule::new(1.0, 1.0);
1099 assert!((c.volume_explicit() - c.volume()).abs() < 1e-10);
1100 }
1101
1102 #[test]
1103 fn test_capsule_surface_area() {
1104 let c = Capsule::new(1.0, 1.0);
1106 let expected = 8.0 * PI;
1107 assert!((c.surface_area() - expected).abs() < 1e-10);
1108 }
1109
1110 #[test]
1111 fn test_capsule_inertia_array() {
1112 let c = Capsule::new(1.0, 1.0);
1113 let it = c.inertia_tensor_array(1.0);
1114 assert!(it[0][0] > 0.0);
1116 assert!(it[1][1] > 0.0);
1117 assert!(it[2][2] > 0.0);
1118 assert!((it[0][0] - it[2][2]).abs() < 1e-10);
1120 assert!(it[0][1].abs() < 1e-10);
1122 }
1123
1124 #[test]
1125 fn test_capsule_raycast() {
1126 let c = Capsule::new(1.0, 2.0);
1127 let origin = Vec3::new(-5.0, 0.0, 0.0);
1128 let dir = Vec3::new(1.0, 0.0, 0.0);
1129 let hit = c.ray_cast(&origin, &dir, 100.0).unwrap();
1130 assert!((hit.toi - 4.0).abs() < 1e-10);
1131 }
1132
1133 #[test]
1134 fn test_capsule_raycast_array() {
1135 let c = Capsule::new(1.0, 2.0);
1136 let (t, _n) = c
1137 .ray_cast_array([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0)
1138 .unwrap();
1139 assert!((t - 4.0).abs() < 1e-10);
1140 }
1141
1142 #[test]
1143 fn test_capsule_support_up() {
1144 let c = Capsule::new(1.0, 2.0);
1145 let sp = c.support([0.0, 1.0, 0.0]);
1146 assert!((sp[1] - 3.0).abs() < 1e-10);
1148 }
1149
1150 #[test]
1151 fn test_capsule_support_down() {
1152 let c = Capsule::new(1.0, 2.0);
1153 let sp = c.support([0.0, -1.0, 0.0]);
1154 assert!((sp[1] + 3.0).abs() < 1e-10);
1156 }
1157
1158 #[test]
1159 fn test_capsule_raycast_top_cap() {
1160 let c = Capsule::new(1.0, 2.0);
1161 let origin = Vec3::new(0.0, 10.0, 0.0);
1162 let dir = Vec3::new(0.0, -1.0, 0.0);
1163 let hit = c.ray_cast(&origin, &dir, 100.0).unwrap();
1164 assert!(
1166 (hit.toi - 7.0).abs() < 1e-10,
1167 "expected toi=7, got {}",
1168 hit.toi
1169 );
1170 }
1171
1172 #[test]
1175 fn test_capsule_closest_point_on_side() {
1176 let c = Capsule::new(1.0, 2.0);
1177 let cp = c.closest_point([5.0, 0.0, 0.0]);
1178 assert!((cp[0] - 1.0).abs() < 1e-10);
1179 assert!(cp[1].abs() < 1e-10);
1180 assert!(cp[2].abs() < 1e-10);
1181 }
1182
1183 #[test]
1184 fn test_capsule_closest_point_on_top_cap() {
1185 let c = Capsule::new(1.0, 2.0);
1186 let cp = c.closest_point([0.0, 5.0, 0.0]);
1187 assert!((cp[1] - 3.0).abs() < 1e-10);
1189 assert!(cp[0].abs() < 1e-10);
1190 }
1191
1192 #[test]
1193 fn test_capsule_closest_point_on_bottom_cap() {
1194 let c = Capsule::new(1.0, 2.0);
1195 let cp = c.closest_point([0.0, -5.0, 0.0]);
1196 assert!((cp[1] + 3.0).abs() < 1e-10);
1197 }
1198
1199 #[test]
1200 fn test_capsule_closest_point_on_axis() {
1201 let c = Capsule::new(1.0, 2.0);
1202 let cp = c.closest_point([0.0, 0.0, 0.0]);
1204 assert!((cp[0] - 1.0).abs() < 1e-10);
1206 assert!(cp[1].abs() < 1e-10);
1207 }
1208
1209 #[test]
1210 fn test_capsule_contains_point() {
1211 let c = Capsule::new(1.0, 2.0);
1212 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])); }
1218
1219 #[test]
1220 fn test_capsule_signed_distance() {
1221 let c = Capsule::new(2.0, 1.0);
1222 assert!((c.signed_distance([0.0, 0.0, 0.0]) + 2.0).abs() < 1e-10);
1224 assert!(c.signed_distance([2.0, 0.0, 0.0]).abs() < 1e-10);
1226 assert!((c.signed_distance([4.0, 0.0, 0.0]) - 2.0).abs() < 1e-10);
1228 }
1229
1230 #[test]
1231 fn test_capsule_medial_axis_endpoints() {
1232 let c = Capsule::new(1.0, 3.0);
1233 let (top, bot) = c.medial_axis_endpoints();
1234 assert!((top[1] - 3.0).abs() < 1e-10);
1235 assert!((bot[1] + 3.0).abs() < 1e-10);
1236 }
1237
1238 #[test]
1239 fn test_capsule_full_length() {
1240 let c = Capsule::new(1.0, 2.0);
1241 assert!((c.full_length() - 6.0).abs() < 1e-10);
1243 }
1244
1245 #[test]
1246 fn test_capsule_medial_axis_length() {
1247 let c = Capsule::new(1.0, 2.0);
1248 assert!((c.medial_axis_length() - 4.0).abs() < 1e-10);
1249 }
1250
1251 #[test]
1252 fn test_segment_segment_distance_parallel() {
1253 let d = Capsule::segment_segment_distance(
1255 [0.0, 0.0, 0.0],
1256 [0.0, 1.0, 0.0],
1257 [3.0, 0.0, 0.0],
1258 [3.0, 1.0, 0.0],
1259 );
1260 assert!((d - 3.0).abs() < 1e-10);
1261 }
1262
1263 #[test]
1264 fn test_segment_segment_distance_crossing() {
1265 let d = Capsule::segment_segment_distance(
1267 [-1.0, 0.0, 0.0],
1268 [1.0, 0.0, 0.0],
1269 [0.0, 2.0, -1.0],
1270 [0.0, 2.0, 1.0],
1271 );
1272 assert!((d - 2.0).abs() < 1e-10);
1273 }
1274
1275 #[test]
1276 fn test_segment_segment_distance_touching() {
1277 let d = Capsule::segment_segment_distance(
1278 [0.0, 0.0, 0.0],
1279 [1.0, 0.0, 0.0],
1280 [1.0, 0.0, 0.0],
1281 [2.0, 0.0, 0.0],
1282 );
1283 assert!(d.abs() < 1e-10);
1284 }
1285
1286 #[test]
1287 fn test_segment_segment_distance_degenerate_points() {
1288 let d = Capsule::segment_segment_distance(
1289 [0.0, 0.0, 0.0],
1290 [0.0, 0.0, 0.0],
1291 [3.0, 4.0, 0.0],
1292 [3.0, 4.0, 0.0],
1293 );
1294 assert!((d - 5.0).abs() < 1e-10);
1295 }
1296
1297 #[test]
1298 fn test_capsule_capsule_distance_separated() {
1299 let a = Capsule::new(1.0, 1.0);
1300 let b = Capsule::new(1.0, 1.0);
1301 let d = a.capsule_capsule_distance([0.0, 0.0, 0.0], &b, [5.0, 0.0, 0.0]);
1303 assert!((d - 3.0).abs() < 1e-10);
1305 }
1306
1307 #[test]
1308 fn test_capsule_capsule_distance_overlapping() {
1309 let a = Capsule::new(1.0, 1.0);
1310 let b = Capsule::new(1.0, 1.0);
1311 let d = a.capsule_capsule_distance([0.0, 0.0, 0.0], &b, [0.5, 0.0, 0.0]);
1312 assert_eq!(d, 0.0); }
1314
1315 #[test]
1316 fn test_capsule_capsule_overlap() {
1317 let a = Capsule::new(1.0, 1.0);
1318 let b = Capsule::new(1.0, 1.0);
1319 assert!(a.capsule_capsule_overlap([0.0, 0.0, 0.0], &b, [1.0, 0.0, 0.0]));
1320 assert!(!a.capsule_capsule_overlap([0.0, 0.0, 0.0], &b, [5.0, 0.0, 0.0]));
1321 }
1322
1323 #[test]
1324 fn test_capsule_project_on_medial_axis() {
1325 let c = Capsule::new(1.0, 2.0);
1326 assert!((c.project_on_medial_axis([0.0, 5.0, 0.0]) - 2.0).abs() < 1e-10);
1327 assert!((c.project_on_medial_axis([0.0, -5.0, 0.0]) + 2.0).abs() < 1e-10);
1328 assert!((c.project_on_medial_axis([3.0, 1.0, 0.0]) - 1.0).abs() < 1e-10);
1329 }
1330
1331 #[test]
1332 fn test_capsule_distance_to_medial_axis() {
1333 let c = Capsule::new(1.0, 2.0);
1334 assert!(c.distance_to_medial_axis([0.0, 0.0, 0.0]).abs() < 1e-10);
1335 assert!((c.distance_to_medial_axis([3.0, 0.0, 0.0]) - 3.0).abs() < 1e-10);
1336 assert!((c.distance_to_medial_axis([0.0, 5.0, 0.0]) - 3.0).abs() < 1e-10);
1338 }
1339
1340 #[test]
1341 fn test_capsule_support_diagonal() {
1342 let c = Capsule::new(1.0, 2.0);
1343 let sp = c.support([1.0, 1.0, 0.0]);
1344 let len = (1.0_f64 + 1.0).sqrt();
1346 let expected_x = 1.0 / len;
1347 let expected_y = 2.0 + 1.0 / len;
1348 assert!((sp[0] - expected_x).abs() < 1e-10);
1349 assert!((sp[1] - expected_y).abs() < 1e-10);
1350 }
1351
1352 #[test]
1355 fn test_capsule_closest_points_vs_capsule() {
1356 let a = Capsule::new(1.0, 1.0);
1357 let b = Capsule::new(1.0, 1.0);
1358 let (pa, pb, seg_dist) =
1360 a.closest_points_capsule_vs_capsule([0.0, 0.0, 0.0], &b, [4.0, 0.0, 0.0]);
1361 assert!(
1362 (seg_dist - 4.0).abs() < 1e-9,
1363 "axis dist should be 4, got {seg_dist}"
1364 );
1365 assert!((pa[0]).abs() < 1e-9);
1366 assert!((pb[0] - 4.0).abs() < 1e-9);
1367 }
1368
1369 #[test]
1370 fn test_segment_segment_closest_crossing() {
1371 let (pa, pb, dist) = Capsule::segment_segment_closest(
1373 [-1.0, 0.0, 0.0],
1374 [1.0, 0.0, 0.0],
1375 [0.0, 3.0, -1.0],
1376 [0.0, 3.0, 1.0],
1377 );
1378 assert!((dist - 3.0).abs() < 1e-9, "dist should be 3, got {dist}");
1379 assert!(pa[0].abs() < 1e-9);
1380 assert!((pb[1] - 3.0).abs() < 1e-9);
1381 }
1382
1383 #[test]
1384 fn test_capsule_intersects_obb_basic() {
1385 let c = Capsule::new(0.5, 1.0);
1386 let axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1388 assert!(c.intersects_obb([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], axes, [2.0, 2.0, 2.0]));
1390 assert!(!c.intersects_obb([0.0, 0.0, 0.0], [10.0, 0.0, 0.0], axes, [2.0, 2.0, 2.0]));
1392 }
1393
1394 #[test]
1395 fn test_capsule_sdf_matches_signed_distance() {
1396 let c = Capsule::new(1.0, 2.0);
1397 let p = [3.0, 0.0, 0.0];
1398 assert!((c.sdf(p) - c.signed_distance(p)).abs() < 1e-12);
1399 }
1400
1401 #[test]
1402 fn test_swept_capsule_vs_sphere_contact() {
1403 let c = Capsule::new(1.0, 1.0);
1404 let result =
1406 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);
1407 assert!(result.is_some(), "swept capsule should contact sphere");
1408 let t = result.unwrap();
1409 assert!((0.0..=1.0).contains(&t), "t should be in [0,1], got {t}");
1410 }
1411
1412 #[test]
1413 fn test_swept_capsule_vs_sphere_no_contact() {
1414 let c = Capsule::new(0.5, 0.5);
1415 let result =
1417 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);
1418 assert!(result.is_none(), "should not contact distant sphere");
1419 }
1420
1421 #[test]
1422 fn test_random_surface_points_count() {
1423 let c = Capsule::new(1.0, 2.0);
1424 let pts = c.random_surface_points(50, 42);
1425 assert_eq!(pts.len(), 50);
1426 }
1427
1428 #[test]
1429 fn test_random_surface_points_on_surface() {
1430 let c = Capsule::new(1.0, 2.0);
1431 let pts = c.random_surface_points(100, 7);
1432 for p in &pts {
1433 let sdf = c.sdf(*p);
1435 assert!(sdf.abs() < 0.05, "point {:?} has sdf={sdf}", p);
1436 }
1437 }
1438
1439 #[test]
1440 fn test_point_segment_distance_midpoint() {
1441 let d = Capsule::point_segment_distance([0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1443 assert!((d - 1.0).abs() < 1e-12);
1444 }
1445
1446 #[test]
1447 fn test_inertia_tensor_raw_matches_array() {
1448 let c = Capsule::new(1.5, 2.5);
1449 let raw = c.inertia_tensor_raw(3.0);
1450 let arr = c.inertia_tensor_array(3.0);
1451 for i in 0..3 {
1452 for j in 0..3 {
1453 assert!((raw[i][j] - arr[i][j]).abs() < 1e-12);
1454 }
1455 }
1456 }
1457
1458 #[test]
1461 fn test_capsule_triangle_contact_no_overlap() {
1462 let c = Capsule::new(0.5, 1.0);
1463 let a = [10.0, 0.0, 0.0];
1465 let b = [11.0, 0.0, 0.0];
1466 let tri = [10.0_f64, 0.0_f64, 1.0_f64];
1467 let result = c.capsule_triangle_contact([0.0, 0.0, 0.0], a, b, tri);
1468 assert!(
1469 result.is_none(),
1470 "should be no contact with distant triangle"
1471 );
1472 }
1473
1474 #[test]
1475 fn test_capsule_triangle_contact_overlap() {
1476 let c = Capsule::new(1.0, 1.0);
1477 let a = [-5.0, 0.0, -5.0];
1479 let b = [5.0, 0.0, -5.0];
1480 let tri = [0.0_f64, 0.0_f64, 5.0_f64];
1481 let result = c.capsule_triangle_contact([0.0, 0.5, 0.0], a, b, tri);
1482 assert!(
1483 result.is_some(),
1484 "capsule near triangle should produce contact"
1485 );
1486 }
1487
1488 #[test]
1489 fn test_capsule_chain_empty() {
1490 let links = CapsuleChain::new(vec![]);
1491 assert_eq!(links.segment_count(), 0);
1492 }
1493
1494 #[test]
1495 fn test_capsule_chain_single_point() {
1496 let links = CapsuleChain::new(vec![[0.0, 0.0, 0.0]]);
1497 assert_eq!(links.segment_count(), 0);
1498 }
1499
1500 #[test]
1501 fn test_capsule_chain_two_points() {
1502 let links = CapsuleChain::new(vec![[0.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
1503 assert_eq!(links.segment_count(), 1);
1504 }
1505
1506 #[test]
1507 fn test_capsule_chain_total_length() {
1508 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
1509 let chain = CapsuleChain::new(pts);
1510 assert!((chain.total_length() - 2.0).abs() < 1e-10);
1511 }
1512
1513 #[test]
1514 fn test_capsule_chain_contains_point_near_segment() {
1515 let pts = vec![[0.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
1516 let chain = CapsuleChain::with_radius(pts, 1.0);
1517 assert!(chain.contains_point([0.5, 1.0, 0.0]));
1518 assert!(!chain.contains_point([2.0, 1.0, 0.0]));
1519 }
1520
1521 #[test]
1522 fn test_deformable_capsule_update_endpoints() {
1523 let mut dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1524 dc.set_endpoint_a([1.0, 0.0, 0.0]);
1525 dc.set_endpoint_b([1.0, 2.0, 0.0]);
1526 assert!((dc.endpoint_a()[0] - 1.0).abs() < 1e-12);
1527 assert!((dc.endpoint_b()[1] - 2.0).abs() < 1e-12);
1528 }
1529
1530 #[test]
1531 fn test_deformable_capsule_length() {
1532 let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 3.0, 0.0], 0.5);
1533 assert!((dc.length() - 3.0).abs() < 1e-10);
1534 }
1535
1536 #[test]
1537 fn test_deformable_capsule_midpoint() {
1538 let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], 0.5);
1539 let mid = dc.midpoint();
1540 assert!((mid[0] - 1.0).abs() < 1e-12);
1541 }
1542
1543 #[test]
1544 fn test_capsule_medial_axis_3d_segment() {
1545 let dc = DeformableCapsule::new([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], 0.5);
1546 let (a, b) = dc.medial_axis_endpoints();
1547 assert!((a[0] - 1.0).abs() < 1e-12);
1548 assert!((b[2] - 6.0).abs() < 1e-12);
1549 }
1550
1551 #[test]
1552 fn test_deformable_capsule_sdf_inside() {
1553 let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 2.0, 0.0], 1.0);
1554 assert!(dc.sdf([0.0, 1.0, 0.0]) < 0.0);
1556 }
1557
1558 #[test]
1559 fn test_deformable_capsule_sdf_outside() {
1560 let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 2.0, 0.0], 0.5);
1561 assert!(dc.sdf([5.0, 1.0, 0.0]) > 0.0);
1563 }
1564
1565 #[test]
1566 fn test_capsule_frustum_volume_positive() {
1567 let f = CapsuleFrustum::new(0.5, 1.0, 2.0);
1568 assert!(f.volume() > 0.0);
1569 }
1570
1571 #[test]
1572 fn test_capsule_frustum_contains_center() {
1573 let f = CapsuleFrustum::new(0.5, 1.0, 2.0);
1574 assert!(f.contains_point([0.0, 1.0, 0.0]));
1576 }
1577
1578 #[test]
1579 fn test_capsule_frustum_excludes_far_point() {
1580 let f = CapsuleFrustum::new(0.5, 1.0, 2.0);
1581 assert!(!f.contains_point([10.0, 0.0, 0.0]));
1582 }
1583
1584 #[test]
1585 fn test_curved_capsule_path_length() {
1586 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
1588 let cp = CurvedCapsulePath::new(pts, 0.3);
1589 let len = cp.path_length();
1590 assert!((len - 2.0).abs() < 1e-10);
1591 }
1592
1593 #[test]
1594 fn test_curved_capsule_path_contains_axis_point() {
1595 let pts = vec![[0.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
1596 let cp = CurvedCapsulePath::new(pts, 0.5);
1597 assert!(cp.contains_point([0.0, 1.0, 0.0]));
1598 }
1599
1600 #[test]
1601 fn test_curved_capsule_path_excludes_far_point() {
1602 let pts = vec![[0.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
1603 let cp = CurvedCapsulePath::new(pts, 0.5);
1604 assert!(!cp.contains_point([5.0, 0.0, 0.0]));
1605 }
1606
1607 #[test]
1608 fn test_swept_capsule_contact_time_order() {
1609 let c = Capsule::new(0.5, 1.0);
1610 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);
1612 assert!(t.is_some());
1613 let tv = t.unwrap();
1614 assert!(tv > 0.0 && tv <= 1.0);
1615 }
1616
1617 #[test]
1618 fn test_capsule_capsule_touching_boundary() {
1619 let a = Capsule::new(1.0, 1.0);
1620 let b = Capsule::new(1.0, 1.0);
1621 let d = a.capsule_capsule_distance([0.0, 0.0, 0.0], &b, [2.0, 0.0, 0.0]);
1623 assert!(
1624 d.abs() < 1e-10,
1625 "touching capsules should have 0 distance, got {d}"
1626 );
1627 }
1628
1629 #[test]
1630 fn test_segment_segment_closest_endpoints() {
1631 let (pa, pb, dist) = Capsule::segment_segment_closest(
1633 [0.0, 0.0, 0.0],
1634 [1.0, 0.0, 0.0],
1635 [2.0, 1.0, 0.0],
1636 [2.0, 2.0, 0.0],
1637 );
1638 assert!(dist > 0.0);
1639 let _ = (pa, pb);
1640 }
1641
1642 #[test]
1643 fn test_capsule_closest_point_on_surface_norm() {
1644 let c = Capsule::new(1.0, 2.0);
1645 let p = [3.0, 1.0, 4.0];
1646 let cp = c.closest_point(p);
1647 let sdf = c.sdf(cp);
1649 assert!(
1650 sdf.abs() < 1e-9,
1651 "closest point sdf should be ~0, got {sdf}"
1652 );
1653 }
1654
1655 #[test]
1656 fn test_capsule_bounding_box_correct() {
1657 let c = Capsule::new(1.0, 2.0);
1658 use crate::shape::Shape;
1659 let bb = c.bounding_box();
1660 assert!((bb.max.y - 3.0).abs() < 1e-10);
1662 assert!((bb.max.x - 1.0).abs() < 1e-10);
1663 }
1664
1665 #[test]
1666 fn test_capsule_chain_segment_radii() {
1667 let pts = vec![
1668 [0.0, 0.0, 0.0],
1669 [1.0, 0.0, 0.0],
1670 [2.0, 0.0, 0.0],
1671 [2.0, 1.0, 0.0],
1672 ];
1673 let chain = CapsuleChain::with_radius(pts, 0.3);
1674 assert_eq!(chain.segment_count(), 3);
1675 }
1676
1677 #[test]
1678 fn test_deformable_capsule_support_positive_y() {
1679 let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [0.0, 4.0, 0.0], 1.0);
1680 let sp = dc.support([0.0, 1.0, 0.0]);
1681 assert!(sp[1] > 4.0, "support in +Y should be above endpoint b");
1683 }
1684
1685 #[test]
1686 fn test_capsule_frustum_sdf_on_axis_top() {
1687 let f = CapsuleFrustum::new(0.5, 1.0, 3.0);
1689 let sdf_inside = f.sdf([0.0, 1.5, 0.0]);
1691 assert!(
1692 sdf_inside < 0.0,
1693 "center should be inside frustum, sdf={sdf_inside}"
1694 );
1695 }
1696
1697 #[test]
1698 fn test_curved_capsule_path_segment_count() {
1699 let pts: Vec<[f64; 3]> = (0..5).map(|i| [i as f64, 0.0, 0.0]).collect();
1700 let cp = CurvedCapsulePath::new(pts, 0.2);
1701 assert_eq!(cp.num_segments(), 4);
1702 }
1703
1704 #[test]
1705 fn test_capsule_chain_sdf_on_axis() {
1706 let pts = vec![[0.0, 0.0, 0.0], [0.0, 3.0, 0.0]];
1707 let chain = CapsuleChain::with_radius(pts, 1.0);
1708 let sdf = chain.sdf([0.0, 1.5, 0.0]);
1710 assert!(sdf < 0.0, "axis point should be inside chain, sdf={sdf}");
1711 }
1712
1713 #[test]
1714 fn test_capsule_triangle_contact_normal_points_away() {
1715 let c = Capsule::new(1.0, 1.0);
1716 let a = [-10.0, -0.3, -10.0];
1718 let b = [10.0, -0.3, -10.0];
1719 let tri = [0.0_f64, -0.3_f64, 10.0_f64];
1720 if let Some((depth, normal)) = c.capsule_triangle_contact([0.0, 0.0, 0.0], a, b, tri) {
1721 assert!(depth > 0.0, "depth should be positive, got {depth}");
1722 let n_len =
1724 (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
1725 assert!(
1726 (n_len - 1.0).abs() < 1e-9,
1727 "normal should be unit length, got {n_len}"
1728 );
1729 }
1730 }
1731
1732 #[test]
1733 fn test_deformable_capsule_arbitrary_orientation() {
1734 let dc = DeformableCapsule::new([0.0, 0.0, 0.0], [4.0, 0.0, 0.0], 1.0);
1736 assert!(dc.sdf([2.0, 0.0, 0.0]) < 0.0); assert!(dc.sdf([2.0, 1.5, 0.0]) > 0.0); }
1739
1740 #[test]
1741 fn test_capsule_chain_min_distance_to_point() {
1742 let pts = vec![[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]];
1743 let chain = CapsuleChain::with_radius(pts, 0.5);
1744 let d = chain.min_distance_to_point([2.0, 3.0, 0.0]);
1745 assert!((d - 2.5).abs() < 1e-9, "expected 2.5, got {d}");
1747 }
1748
1749 #[test]
1750 fn test_capsule_frustum_larger_radius_at_top() {
1751 let f = CapsuleFrustum::new(0.5, 2.0, 3.0);
1752 assert!(f.radius_at_height(3.0) > f.radius_at_height(0.0));
1754 }
1755
1756 #[test]
1757 fn test_capsule_frustum_radius_interpolated() {
1758 let f = CapsuleFrustum::new(1.0, 3.0, 4.0);
1759 let r_mid = f.radius_at_height(2.0);
1761 assert!(
1762 (r_mid - 2.0).abs() < 1e-9,
1763 "midpoint radius should be 2.0, got {r_mid}"
1764 );
1765 }
1766}