1#[inline]
24pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
25 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
26}
27
28#[inline]
30pub fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
31 [
32 a[1] * b[2] - a[2] * b[1],
33 a[2] * b[0] - a[0] * b[2],
34 a[0] * b[1] - a[1] * b[0],
35 ]
36}
37
38#[inline]
40pub fn norm_sq3(a: [f64; 3]) -> f64 {
41 dot3(a, a)
42}
43
44#[inline]
46pub fn norm3(a: [f64; 3]) -> f64 {
47 norm_sq3(a).sqrt()
48}
49
50#[inline]
52pub fn normalize3(a: [f64; 3]) -> [f64; 3] {
53 let n = norm3(a);
54 if n < 1e-15 {
55 return [0.0; 3];
56 }
57 [a[0] / n, a[1] / n, a[2] / n]
58}
59
60#[inline]
62pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
63 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
64}
65
66#[inline]
68pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
69 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
70}
71
72#[inline]
74pub fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
75 [a[0] * s, a[1] * s, a[2] * s]
76}
77
78#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct Aabb3 {
85 pub min: [f64; 3],
87 pub max: [f64; 3],
89}
90
91impl Aabb3 {
92 pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
94 Self { min, max }
95 }
96
97 pub fn empty() -> Self {
99 Self {
100 min: [f64::MAX; 3],
101 max: [f64::MIN; 3],
102 }
103 }
104
105 pub fn include_point(&mut self, p: [f64; 3]) {
107 self.min
108 .iter_mut()
109 .zip(p.iter())
110 .for_each(|(m, &pi)| *m = m.min(pi));
111 self.max
112 .iter_mut()
113 .zip(p.iter())
114 .for_each(|(m, &pi)| *m = m.max(pi));
115 }
116
117 pub fn include_aabb(&mut self, other: &Aabb3) {
119 self.min
120 .iter_mut()
121 .zip(other.min.iter())
122 .for_each(|(m, &o)| *m = m.min(o));
123 self.max
124 .iter_mut()
125 .zip(other.max.iter())
126 .for_each(|(m, &o)| *m = m.max(o));
127 }
128
129 pub fn center(&self) -> [f64; 3] {
131 [
132 0.5 * (self.min[0] + self.max[0]),
133 0.5 * (self.min[1] + self.max[1]),
134 0.5 * (self.min[2] + self.max[2]),
135 ]
136 }
137
138 pub fn half_extents(&self) -> [f64; 3] {
140 [
141 0.5 * (self.max[0] - self.min[0]),
142 0.5 * (self.max[1] - self.min[1]),
143 0.5 * (self.max[2] - self.min[2]),
144 ]
145 }
146
147 pub fn volume(&self) -> f64 {
149 let e = [
150 self.max[0] - self.min[0],
151 self.max[1] - self.min[1],
152 self.max[2] - self.min[2],
153 ];
154 e[0].max(0.0) * e[1].max(0.0) * e[2].max(0.0)
155 }
156
157 pub fn overlaps(&self, other: &Aabb3) -> bool {
159 self.min[0] <= other.max[0]
160 && self.max[0] >= other.min[0]
161 && self.min[1] <= other.max[1]
162 && self.max[1] >= other.min[1]
163 && self.min[2] <= other.max[2]
164 && self.max[2] >= other.min[2]
165 }
166
167 pub fn contains_point(&self, p: [f64; 3]) -> bool {
169 p[0] >= self.min[0]
170 && p[0] <= self.max[0]
171 && p[1] >= self.min[1]
172 && p[1] <= self.max[1]
173 && p[2] >= self.min[2]
174 && p[2] <= self.max[2]
175 }
176
177 pub fn inflate(&self, margin: f64) -> Aabb3 {
179 Aabb3::new(
180 [
181 self.min[0] - margin,
182 self.min[1] - margin,
183 self.min[2] - margin,
184 ],
185 [
186 self.max[0] + margin,
187 self.max[1] + margin,
188 self.max[2] + margin,
189 ],
190 )
191 }
192}
193
194#[derive(Debug, Clone, Copy)]
200pub struct LocalTransform {
201 pub position: [f64; 3],
203 pub rotation: [[f64; 3]; 3],
205}
206
207impl LocalTransform {
208 pub fn identity() -> Self {
210 Self {
211 position: [0.0; 3],
212 rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
213 }
214 }
215
216 pub fn from_translation(pos: [f64; 3]) -> Self {
218 let mut t = Self::identity();
219 t.position = pos;
220 t
221 }
222
223 pub fn apply(&self, p: [f64; 3]) -> [f64; 3] {
225 let r = &self.rotation;
226 let rp = [
227 r[0][0] * p[0] + r[0][1] * p[1] + r[0][2] * p[2],
228 r[1][0] * p[0] + r[1][1] * p[1] + r[1][2] * p[2],
229 r[2][0] * p[0] + r[2][1] * p[1] + r[2][2] * p[2],
230 ];
231 add3(rp, self.position)
232 }
233
234 pub fn inverse_apply(&self, p: [f64; 3]) -> [f64; 3] {
236 let dp = sub3(p, self.position);
237 let r = &self.rotation;
238 [
239 r[0][0] * dp[0] + r[1][0] * dp[1] + r[2][0] * dp[2],
240 r[0][1] * dp[0] + r[1][1] * dp[1] + r[2][1] * dp[2],
241 r[0][2] * dp[0] + r[1][2] * dp[1] + r[2][2] * dp[2],
242 ]
243 }
244}
245
246#[derive(Debug, Clone)]
252pub enum PrimitiveShape {
253 Sphere(f64),
255 Box([f64; 3]),
257 Capsule(f64, f64),
259 Cylinder(f64, f64),
261 ConvexHull(Vec<[f64; 3]>),
263}
264
265impl PrimitiveShape {
266 pub fn aabb(&self) -> Aabb3 {
268 match self {
269 PrimitiveShape::Sphere(r) => Aabb3::new([-r, -r, -r], [*r, *r, *r]),
270 PrimitiveShape::Box(he) => Aabb3::new([-he[0], -he[1], -he[2]], [he[0], he[1], he[2]]),
271 PrimitiveShape::Capsule(r, hh) => Aabb3::new([-r, -hh - r, -r], [*r, hh + r, *r]),
272 PrimitiveShape::Cylinder(r, hh) => Aabb3::new([-r, -hh, -r], [*r, *hh, *r]),
273 PrimitiveShape::ConvexHull(verts) => {
274 let mut aabb = Aabb3::empty();
275 for &v in verts {
276 aabb.include_point(v);
277 }
278 aabb
279 }
280 }
281 }
282
283 pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
285 match self {
286 PrimitiveShape::Sphere(r) => {
287 let n = norm3(d);
288 if n < 1e-15 {
289 return [0.0; 3];
290 }
291 scale3(d, r / n)
292 }
293 PrimitiveShape::Box(he) => [
294 he[0] * d[0].signum(),
295 he[1] * d[1].signum(),
296 he[2] * d[2].signum(),
297 ],
298 PrimitiveShape::Capsule(r, hh) => {
299 let n = norm3(d);
300 let dn = if n < 1e-15 {
301 [0.0, 1.0, 0.0]
302 } else {
303 scale3(d, 1.0 / n)
304 };
305 let cap_center = [0.0, dn[1].signum() * hh, 0.0];
306 add3(cap_center, scale3(dn, *r))
307 }
308 PrimitiveShape::Cylinder(r, hh) => {
309 let xy_len = (d[0] * d[0] + d[2] * d[2]).sqrt();
310 let x = if xy_len > 1e-15 {
311 r * d[0] / xy_len
312 } else {
313 0.0
314 };
315 let z = if xy_len > 1e-15 {
316 r * d[2] / xy_len
317 } else {
318 0.0
319 };
320 [x, hh * d[1].signum(), z]
321 }
322 PrimitiveShape::ConvexHull(verts) => {
323 let mut best = [0.0f64; 3];
324 let mut best_d = f64::NEG_INFINITY;
325 for &v in verts {
326 let proj = dot3(v, d);
327 if proj > best_d {
328 best_d = proj;
329 best = v;
330 }
331 }
332 best
333 }
334 }
335 }
336
337 pub fn volume(&self) -> f64 {
339 let pi = std::f64::consts::PI;
340 match self {
341 PrimitiveShape::Sphere(r) => 4.0 / 3.0 * pi * r * r * r,
342 PrimitiveShape::Box(he) => 8.0 * he[0] * he[1] * he[2],
343 PrimitiveShape::Capsule(r, hh) => pi * r * r * 2.0 * hh + 4.0 / 3.0 * pi * r * r * r,
344 PrimitiveShape::Cylinder(r, hh) => pi * r * r * 2.0 * hh,
345 PrimitiveShape::ConvexHull(verts) => {
346 let mut aabb = Aabb3::empty();
348 for &v in verts {
349 aabb.include_point(v);
350 }
351 aabb.volume()
352 }
353 }
354 }
355}
356
357#[derive(Debug, Clone)]
365pub struct CompoundShape {
366 pub children: Vec<(PrimitiveShape, LocalTransform)>,
368}
369
370impl CompoundShape {
371 pub fn new() -> Self {
373 Self {
374 children: Vec::new(),
375 }
376 }
377
378 pub fn add_child(&mut self, shape: PrimitiveShape, transform: LocalTransform) {
380 self.children.push((shape, transform));
381 }
382
383 pub fn aabb(&self) -> Aabb3 {
385 let mut aabb = Aabb3::empty();
386 for (shape, xform) in &self.children {
387 let local_aabb = shape.aabb();
388 let corners = [
390 [local_aabb.min[0], local_aabb.min[1], local_aabb.min[2]],
391 [local_aabb.max[0], local_aabb.min[1], local_aabb.min[2]],
392 [local_aabb.min[0], local_aabb.max[1], local_aabb.min[2]],
393 [local_aabb.max[0], local_aabb.max[1], local_aabb.min[2]],
394 [local_aabb.min[0], local_aabb.min[1], local_aabb.max[2]],
395 [local_aabb.max[0], local_aabb.min[1], local_aabb.max[2]],
396 [local_aabb.min[0], local_aabb.max[1], local_aabb.max[2]],
397 [local_aabb.max[0], local_aabb.max[1], local_aabb.max[2]],
398 ];
399 for c in &corners {
400 aabb.include_point(xform.apply(*c));
401 }
402 }
403 aabb
404 }
405
406 pub fn total_volume(&self) -> f64 {
408 self.children.iter().map(|(s, _)| s.volume()).sum()
409 }
410
411 pub fn center_of_mass(&self) -> [f64; 3] {
413 let mut total_vol = 0.0f64;
414 let mut com = [0.0f64; 3];
415 for (shape, xform) in &self.children {
416 let v = shape.volume();
417 let c = xform.position; com[0] += v * c[0];
419 com[1] += v * c[1];
420 com[2] += v * c[2];
421 total_vol += v;
422 }
423 if total_vol < 1e-20 {
424 return [0.0; 3];
425 }
426 scale3(com, 1.0 / total_vol)
427 }
428
429 pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
431 let mut best = [0.0f64; 3];
432 let mut best_proj = f64::NEG_INFINITY;
433 for (shape, xform) in &self.children {
434 let d_local = xform.inverse_apply(add3(d, xform.position));
436 let local_sup = shape.support(d_local);
437 let world_sup = xform.apply(local_sup);
438 let proj = dot3(world_sup, d);
439 if proj > best_proj {
440 best_proj = proj;
441 best = world_sup;
442 }
443 }
444 best
445 }
446
447 pub fn num_children(&self) -> usize {
449 self.children.len()
450 }
451}
452
453impl Default for CompoundShape {
454 fn default() -> Self {
455 Self::new()
456 }
457}
458
459pub fn compute_compound_aabb(compound: &CompoundShape) -> Aabb3 {
461 compound.aabb()
462}
463
464#[derive(Debug, Clone)]
473pub struct ConvexDecomposition {
474 pub max_hulls: usize,
476 pub max_concavity: f64,
478 pub resolution: usize,
480 pub hulls: Vec<Vec<[f64; 3]>>,
482}
483
484impl ConvexDecomposition {
485 pub fn new(max_hulls: usize, max_concavity: f64, resolution: usize) -> Self {
487 Self {
488 max_hulls,
489 max_concavity,
490 resolution,
491 hulls: Vec::new(),
492 }
493 }
494
495 pub fn decompose(&mut self, vertices: &[[f64; 3]], indices: &[[usize; 3]]) {
499 self.hulls.clear();
500 if vertices.is_empty() || indices.is_empty() {
501 return;
502 }
503
504 let mut parts: Vec<Vec<usize>> = vec![(0..vertices.len()).collect()];
506 let mut output_hulls = Vec::new();
507
508 while !parts.is_empty() && output_hulls.len() < self.max_hulls {
509 let part = parts.remove(0);
510 let hull_verts: Vec<[f64; 3]> = part.iter().map(|&i| vertices[i]).collect();
511 let concavity = Self::estimate_concavity(&hull_verts);
512
513 if concavity < self.max_concavity || part.len() < 6 {
514 output_hulls.push(hull_verts);
515 } else if parts.len() + output_hulls.len() < self.max_hulls * 2 {
516 let (left, right) = Self::split_part(&part, vertices);
518 if !left.is_empty() {
519 parts.push(left);
520 }
521 if !right.is_empty() {
522 parts.push(right);
523 }
524 } else {
525 output_hulls.push(hull_verts);
526 }
527 }
528
529 for part in parts {
531 let hull_verts: Vec<[f64; 3]> = part.iter().map(|&i| vertices[i]).collect();
532 output_hulls.push(hull_verts);
533 }
534
535 let _ = indices;
536 self.hulls = output_hulls;
537 }
538
539 fn estimate_concavity(verts: &[[f64; 3]]) -> f64 {
541 if verts.len() < 4 {
542 return 0.0;
543 }
544 let hull = Self::simple_convex_hull(verts);
545 let mut max_d = 0.0f64;
546 for &v in verts {
547 let d = Self::point_to_hull_distance(v, &hull);
548 max_d = max_d.max(d);
549 }
550 max_d
551 }
552
553 fn simple_convex_hull(verts: &[[f64; 3]]) -> Vec<[f64; 3]> {
555 let mut aabb = Aabb3::empty();
556 for &v in verts {
557 aabb.include_point(v);
558 }
559 vec![
560 aabb.min,
561 aabb.max,
562 [aabb.min[0], aabb.max[1], aabb.min[2]],
563 [aabb.max[0], aabb.min[1], aabb.max[2]],
564 ]
565 }
566
567 fn point_to_hull_distance(p: [f64; 3], hull: &[[f64; 3]]) -> f64 {
569 let mut aabb = Aabb3::empty();
570 for &v in hull {
571 aabb.include_point(v);
572 }
573 let dx = 0.0f64.max(aabb.min[0] - p[0]).max(p[0] - aabb.max[0]);
575 let dy = 0.0f64.max(aabb.min[1] - p[1]).max(p[1] - aabb.max[1]);
576 let dz = 0.0f64.max(aabb.min[2] - p[2]).max(p[2] - aabb.max[2]);
577 (dx * dx + dy * dy + dz * dz).sqrt()
578 }
579
580 fn split_part(indices: &[usize], vertices: &[[f64; 3]]) -> (Vec<usize>, Vec<usize>) {
582 if indices.is_empty() {
583 return (vec![], vec![]);
584 }
585 let verts: Vec<[f64; 3]> = indices.iter().map(|&i| vertices[i]).collect();
586 let mut aabb = Aabb3::empty();
587 for &v in &verts {
588 aabb.include_point(v);
589 }
590
591 let extents = [
593 aabb.max[0] - aabb.min[0],
594 aabb.max[1] - aabb.min[1],
595 aabb.max[2] - aabb.min[2],
596 ];
597 let axis = if extents[0] >= extents[1] && extents[0] >= extents[2] {
598 0
599 } else if extents[1] >= extents[2] {
600 1
601 } else {
602 2
603 };
604 let mid = 0.5 * (aabb.min[axis] + aabb.max[axis]);
605
606 let left: Vec<usize> = indices
607 .iter()
608 .copied()
609 .filter(|&i| vertices[i][axis] <= mid)
610 .collect();
611 let right: Vec<usize> = indices
612 .iter()
613 .copied()
614 .filter(|&i| vertices[i][axis] > mid)
615 .collect();
616 (left, right)
617 }
618
619 pub fn num_hulls(&self) -> usize {
621 self.hulls.len()
622 }
623}
624
625pub fn merge_hulls(hulls: &[Vec<[f64; 3]>]) -> Vec<[f64; 3]> {
627 let mut all_verts = Vec::new();
628 for h in hulls {
629 all_verts.extend_from_slice(h);
630 }
631 all_verts
632}
633
634#[derive(Debug, Clone)]
642pub struct TriangleMeshShape {
643 pub vertices: Vec<[f64; 3]>,
645 pub indices: Vec<[usize; 3]>,
647 pub normals: Vec<[f64; 3]>,
649 pub face_aabbs: Vec<Aabb3>,
651 pub aabb: Aabb3,
653}
654
655impl TriangleMeshShape {
656 pub fn new(vertices: Vec<[f64; 3]>, indices: Vec<[usize; 3]>) -> Self {
658 let normals = Self::compute_normals(&vertices, &indices);
659 let face_aabbs = Self::compute_face_aabbs(&vertices, &indices);
660 let mut aabb = Aabb3::empty();
661 for &v in &vertices {
662 aabb.include_point(v);
663 }
664 Self {
665 vertices,
666 indices,
667 normals,
668 face_aabbs,
669 aabb,
670 }
671 }
672
673 fn compute_normals(verts: &[[f64; 3]], indices: &[[usize; 3]]) -> Vec<[f64; 3]> {
675 indices
676 .iter()
677 .map(|tri| {
678 let a = verts[tri[0]];
679 let b = verts[tri[1]];
680 let c = verts[tri[2]];
681 let ab = sub3(b, a);
682 let ac = sub3(c, a);
683 normalize3(cross3(ab, ac))
684 })
685 .collect()
686 }
687
688 fn compute_face_aabbs(verts: &[[f64; 3]], indices: &[[usize; 3]]) -> Vec<Aabb3> {
690 indices
691 .iter()
692 .map(|tri| {
693 let mut aabb = Aabb3::empty();
694 for &vi in tri {
695 aabb.include_point(verts[vi]);
696 }
697 aabb
698 })
699 .collect()
700 }
701
702 pub fn num_triangles(&self) -> usize {
704 self.indices.len()
705 }
706
707 pub fn triangle(&self, i: usize) -> ([f64; 3], [f64; 3], [f64; 3]) {
709 let tri = &self.indices[i];
710 (
711 self.vertices[tri[0]],
712 self.vertices[tri[1]],
713 self.vertices[tri[2]],
714 )
715 }
716
717 pub fn surface_area(&self) -> f64 {
719 (0..self.num_triangles())
720 .map(|i| {
721 let (a, b, c) = self.triangle(i);
722 let ab = sub3(b, a);
723 let ac = sub3(c, a);
724 0.5 * norm3(cross3(ab, ac))
725 })
726 .sum()
727 }
728}
729
730#[derive(Debug, Clone)]
738pub struct MeshShapeQuery<'a> {
739 pub mesh: &'a TriangleMeshShape,
741}
742
743impl<'a> MeshShapeQuery<'a> {
744 pub fn new(mesh: &'a TriangleMeshShape) -> Self {
746 Self { mesh }
747 }
748
749 pub fn point_inside(&self, point: [f64; 3]) -> bool {
753 let dir = [1.0, 0.0, 0.0];
754 let count = (0..self.mesh.num_triangles())
755 .filter(|&i| {
756 let (a, b, c) = self.mesh.triangle(i);
757 Self::ray_triangle_intersect(point, dir, a, b, c).is_some()
758 })
759 .count();
760 count % 2 == 1
761 }
762
763 pub fn ray_triangle_intersect(
767 origin: [f64; 3],
768 direction: [f64; 3],
769 a: [f64; 3],
770 b: [f64; 3],
771 c: [f64; 3],
772 ) -> Option<f64> {
773 let edge1 = sub3(b, a);
774 let edge2 = sub3(c, a);
775 let h = cross3(direction, edge2);
776 let det = dot3(edge1, h);
777 if det.abs() < 1e-14 {
778 return None;
779 }
780 let inv_det = 1.0 / det;
781 let s = sub3(origin, a);
782 let u = inv_det * dot3(s, h);
783 if !(0.0..=1.0).contains(&u) {
784 return None;
785 }
786 let q = cross3(s, edge1);
787 let v = inv_det * dot3(direction, q);
788 if v < 0.0 || u + v > 1.0 {
789 return None;
790 }
791 let t = inv_det * dot3(edge2, q);
792 if t > 1e-10 { Some(t) } else { None }
793 }
794
795 pub fn closest_point(&self, query: [f64; 3]) -> ([f64; 3], usize, f64) {
799 let mut best_pt = [0.0f64; 3];
800 let mut best_face = 0;
801 let mut best_dist = f64::MAX;
802
803 for (i, _) in (0..self.mesh.num_triangles()).enumerate() {
804 let (a, b, c) = self.mesh.triangle(i);
805 let pt = Self::closest_point_triangle(query, a, b, c);
806 let d = norm3(sub3(query, pt));
807 if d < best_dist {
808 best_dist = d;
809 best_pt = pt;
810 best_face = i;
811 }
812 }
813 (best_pt, best_face, best_dist)
814 }
815
816 pub fn closest_point_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
818 let ab = sub3(b, a);
819 let ac = sub3(c, a);
820 let ap = sub3(p, a);
821
822 let d1 = dot3(ab, ap);
823 let d2 = dot3(ac, ap);
824 if d1 <= 0.0 && d2 <= 0.0 {
825 return a;
826 }
827
828 let bp = sub3(p, b);
829 let d3 = dot3(ab, bp);
830 let d4 = dot3(ac, bp);
831 if d3 >= 0.0 && d4 <= d3 {
832 return b;
833 }
834
835 let cp = sub3(p, c);
836 let d5 = dot3(ab, cp);
837 let d6 = dot3(ac, cp);
838 if d6 >= 0.0 && d5 <= d6 {
839 return c;
840 }
841
842 let vc = d1 * d4 - d3 * d2;
844 if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
845 let v = d1 / (d1 - d3);
846 return add3(a, scale3(ab, v));
847 }
848
849 let vb = d5 * d2 - d1 * d6;
851 if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
852 let w = d2 / (d2 - d6);
853 return add3(a, scale3(ac, w));
854 }
855
856 let va = d3 * d6 - d5 * d4;
858 if va <= 0.0 && d4 - d3 >= 0.0 && d5 - d6 >= 0.0 {
859 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
860 return add3(b, scale3(sub3(c, b), w));
861 }
862
863 let denom = 1.0 / (va + vb + vc);
865 let v = vb * denom;
866 let w = vc * denom;
867 add3(a, add3(scale3(ab, v), scale3(ac, w)))
868 }
869
870 pub fn normal_at(&self, query: [f64; 3]) -> [f64; 3] {
872 let (_, face_idx, _) = self.closest_point(query);
873 *self.mesh.normals.get(face_idx).unwrap_or(&[0.0, 1.0, 0.0])
874 }
875}
876
877#[derive(Debug, Clone)]
885pub struct GjkPolytope {
886 pub vertices: Vec<[f64; 3]>,
888 pub center: [f64; 3],
890}
891
892impl GjkPolytope {
893 pub fn new(vertices: Vec<[f64; 3]>) -> Self {
895 let n = vertices.len();
896 let center = if n > 0 {
897 let s: [f64; 3] = vertices.iter().fold([0.0; 3], |a, &v| add3(a, v));
898 scale3(s, 1.0 / n as f64)
899 } else {
900 [0.0; 3]
901 };
902 Self { vertices, center }
903 }
904
905 pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
907 let mut best = self.center;
908 let mut best_proj = f64::NEG_INFINITY;
909 for &v in &self.vertices {
910 let proj = dot3(v, d);
911 if proj > best_proj {
912 best_proj = proj;
913 best = v;
914 }
915 }
916 best
917 }
918
919 pub fn num_vertices(&self) -> usize {
921 self.vertices.len()
922 }
923
924 pub fn aabb(&self) -> Aabb3 {
926 let mut aabb = Aabb3::empty();
927 for &v in &self.vertices {
928 aabb.include_point(v);
929 }
930 aabb
931 }
932}
933
934#[derive(Debug, Clone)]
942pub struct MinkowskiSum {
943 pub shape_a: GjkPolytope,
945 pub shape_b: GjkPolytope,
947 pub offset_b: [f64; 3],
949}
950
951impl MinkowskiSum {
952 pub fn new(shape_a: GjkPolytope, shape_b: GjkPolytope, offset_b: [f64; 3]) -> Self {
954 Self {
955 shape_a,
956 shape_b,
957 offset_b,
958 }
959 }
960
961 pub fn support_difference(&self, d: [f64; 3]) -> [f64; 3] {
965 let s_a = self.shape_a.support(d);
966 let neg_d = [-d[0], -d[1], -d[2]];
967 let s_b = self.shape_b.support(neg_d);
968 let s_b_world = add3(s_b, self.offset_b);
970 sub3(s_a, s_b_world)
971 }
972
973 pub fn origin_in_md(&self) -> bool {
975 let dirs = [
977 [1.0, 0.0, 0.0_f64],
978 [-1.0, 0.0, 0.0],
979 [0.0, 1.0, 0.0],
980 [0.0, -1.0, 0.0],
981 [0.0, 0.0, 1.0],
982 [0.0, 0.0, -1.0],
983 ];
984 for d in &dirs {
986 let s = self.support_difference(*d);
987 if dot3(s, *d) < 0.0 {
988 return false;
989 }
990 }
991 true
992 }
993}
994
995#[derive(Debug, Clone)]
1001pub enum ShapeProxy {
1002 Primitive(PrimitiveShape),
1004 Compound(CompoundShape),
1006 TriangleMesh(TriangleMeshShape),
1008 Polytope(GjkPolytope),
1010}
1011
1012impl ShapeProxy {
1013 pub fn aabb(&self) -> Aabb3 {
1015 match self {
1016 ShapeProxy::Primitive(p) => p.aabb(),
1017 ShapeProxy::Compound(c) => c.aabb(),
1018 ShapeProxy::TriangleMesh(m) => m.aabb,
1019 ShapeProxy::Polytope(p) => p.aabb(),
1020 }
1021 }
1022
1023 pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
1025 match self {
1026 ShapeProxy::Primitive(p) => p.support(d),
1027 ShapeProxy::Compound(c) => c.support(d),
1028 ShapeProxy::TriangleMesh(m) => {
1029 let mut best = [0.0f64; 3];
1030 let mut best_proj = f64::NEG_INFINITY;
1031 for &v in &m.vertices {
1032 let p = dot3(v, d);
1033 if p > best_proj {
1034 best_proj = p;
1035 best = v;
1036 }
1037 }
1038 best
1039 }
1040 ShapeProxy::Polytope(p) => p.support(d),
1041 }
1042 }
1043
1044 pub fn name(&self) -> &str {
1046 match self {
1047 ShapeProxy::Primitive(PrimitiveShape::Sphere(_)) => "Sphere",
1048 ShapeProxy::Primitive(PrimitiveShape::Box(_)) => "Box",
1049 ShapeProxy::Primitive(PrimitiveShape::Capsule(_, _)) => "Capsule",
1050 ShapeProxy::Primitive(PrimitiveShape::Cylinder(_, _)) => "Cylinder",
1051 ShapeProxy::Primitive(PrimitiveShape::ConvexHull(_)) => "ConvexHull",
1052 ShapeProxy::Compound(_) => "Compound",
1053 ShapeProxy::TriangleMesh(_) => "TriangleMesh",
1054 ShapeProxy::Polytope(_) => "Polytope",
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Clone)]
1065pub struct CompoundBvhNode {
1066 pub aabb: Aabb3,
1068 pub children: [Option<usize>; 2],
1070 pub leaf_shape_idx: Option<usize>,
1072}
1073
1074#[derive(Debug, Clone)]
1076pub struct CompoundBvh {
1077 pub nodes: Vec<CompoundBvhNode>,
1079 pub root: usize,
1081 pub shapes: Vec<(ShapeProxy, LocalTransform)>,
1083}
1084
1085impl CompoundBvh {
1086 pub fn build(shapes: Vec<(ShapeProxy, LocalTransform)>) -> Self {
1088 let n = shapes.len();
1089 let mut nodes = Vec::new();
1090
1091 if n == 0 {
1092 let empty_node = CompoundBvhNode {
1093 aabb: Aabb3::empty(),
1094 children: [None, None],
1095 leaf_shape_idx: None,
1096 };
1097 nodes.push(empty_node);
1098 return Self {
1099 nodes,
1100 root: 0,
1101 shapes,
1102 };
1103 }
1104
1105 let shape_aabbs: Vec<Aabb3> = shapes
1107 .iter()
1108 .map(|(s, xform)| {
1109 let local_aabb = s.aabb();
1110 let mut world_aabb = Aabb3::empty();
1111 for corner in Self::aabb_corners(&local_aabb) {
1112 world_aabb.include_point(xform.apply(corner));
1113 }
1114 world_aabb
1115 })
1116 .collect();
1117
1118 let root = Self::build_recursive(&mut nodes, &(0..n).collect::<Vec<_>>(), &shape_aabbs);
1119
1120 Self {
1121 nodes,
1122 root,
1123 shapes,
1124 }
1125 }
1126
1127 fn aabb_corners(aabb: &Aabb3) -> [[f64; 3]; 8] {
1128 [
1129 [aabb.min[0], aabb.min[1], aabb.min[2]],
1130 [aabb.max[0], aabb.min[1], aabb.min[2]],
1131 [aabb.min[0], aabb.max[1], aabb.min[2]],
1132 [aabb.max[0], aabb.max[1], aabb.min[2]],
1133 [aabb.min[0], aabb.min[1], aabb.max[2]],
1134 [aabb.max[0], aabb.min[1], aabb.max[2]],
1135 [aabb.min[0], aabb.max[1], aabb.max[2]],
1136 [aabb.max[0], aabb.max[1], aabb.max[2]],
1137 ]
1138 }
1139
1140 fn build_recursive(
1141 nodes: &mut Vec<CompoundBvhNode>,
1142 indices: &[usize],
1143 aabbs: &[Aabb3],
1144 ) -> usize {
1145 let mut total_aabb = Aabb3::empty();
1146 for &i in indices {
1147 total_aabb.include_aabb(&aabbs[i]);
1148 }
1149
1150 if indices.len() == 1 {
1151 let node_idx = nodes.len();
1152 nodes.push(CompoundBvhNode {
1153 aabb: total_aabb,
1154 children: [None, None],
1155 leaf_shape_idx: Some(indices[0]),
1156 });
1157 return node_idx;
1158 }
1159
1160 let extents = total_aabb.half_extents();
1162 let axis = if extents[0] >= extents[1] && extents[0] >= extents[2] {
1163 0
1164 } else if extents[1] >= extents[2] {
1165 1
1166 } else {
1167 2
1168 };
1169 let mid = total_aabb.center()[axis];
1170 let (left, right): (Vec<_>, Vec<_>) = indices
1171 .iter()
1172 .partition(|&&i| 0.5 * (aabbs[i].min[axis] + aabbs[i].max[axis]) <= mid);
1173 let left = if left.is_empty() {
1174 indices[..indices.len() / 2].to_vec()
1175 } else {
1176 left
1177 };
1178 let right = if right.is_empty() {
1179 indices[indices.len() / 2..].to_vec()
1180 } else {
1181 right
1182 };
1183
1184 let left_idx = Self::build_recursive(nodes, &left, aabbs);
1185 let right_idx = Self::build_recursive(nodes, &right, aabbs);
1186
1187 let node_idx = nodes.len();
1188 nodes.push(CompoundBvhNode {
1189 aabb: total_aabb,
1190 children: [Some(left_idx), Some(right_idx)],
1191 leaf_shape_idx: None,
1192 });
1193 node_idx
1194 }
1195
1196 pub fn query_aabb(&self, query: &Aabb3) -> Vec<usize> {
1198 let mut result = Vec::new();
1199 if self.nodes.is_empty() {
1200 return result;
1201 }
1202 self.query_recursive(self.root, query, &mut result);
1203 result
1204 }
1205
1206 fn query_recursive(&self, node_idx: usize, query: &Aabb3, result: &mut Vec<usize>) {
1207 if node_idx >= self.nodes.len() {
1208 return;
1209 }
1210 let node = &self.nodes[node_idx];
1211 if !node.aabb.overlaps(query) {
1212 return;
1213 }
1214 if let Some(leaf_idx) = node.leaf_shape_idx {
1215 result.push(leaf_idx);
1216 return;
1217 }
1218 for &child in &node.children {
1219 if let Some(c) = child {
1220 self.query_recursive(c, query, result);
1221 }
1222 }
1223 }
1224}
1225
1226#[derive(Debug, Clone)]
1234pub struct ShapeOffset {
1235 pub inner: ShapeProxy,
1237 pub offset_radius: f64,
1239}
1240
1241impl ShapeOffset {
1242 pub fn new(inner: ShapeProxy, offset_radius: f64) -> Self {
1244 Self {
1245 inner,
1246 offset_radius,
1247 }
1248 }
1249
1250 pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
1252 let n = norm3(d);
1253 let s = self.inner.support(d);
1254 if n < 1e-15 {
1255 return s;
1256 }
1257 add3(s, scale3(d, self.offset_radius / n))
1258 }
1259
1260 pub fn aabb(&self) -> Aabb3 {
1262 self.inner.aabb().inflate(self.offset_radius)
1263 }
1264}
1265
1266#[derive(Debug, Clone, PartialEq)]
1272pub enum CollisionMeshMode {
1273 TriangleMesh,
1275 ConvexHull,
1277 ConvexDecomposition,
1279}
1280
1281#[derive(Debug, Clone)]
1285pub struct ImportedCollisionMesh {
1286 pub mode: CollisionMeshMode,
1288 pub vertices: Vec<[f64; 3]>,
1290 pub indices: Vec<[usize; 3]>,
1292 pub mesh: Option<TriangleMeshShape>,
1294 pub convex_hull: Vec<[f64; 3]>,
1296 pub decomposed_hulls: Vec<Vec<[f64; 3]>>,
1298}
1299
1300impl ImportedCollisionMesh {
1301 pub fn new(vertices: Vec<[f64; 3]>, indices: Vec<[usize; 3]>, mode: CollisionMeshMode) -> Self {
1303 let mut mesh = None;
1304 let mut convex_hull = Vec::new();
1305 let mut decomposed_hulls = Vec::new();
1306
1307 match &mode {
1308 CollisionMeshMode::TriangleMesh => {
1309 mesh = Some(TriangleMeshShape::new(vertices.clone(), indices.clone()));
1310 }
1311 CollisionMeshMode::ConvexHull => {
1312 convex_hull = Self::compute_convex_hull(&vertices);
1313 }
1314 CollisionMeshMode::ConvexDecomposition => {
1315 let mut decomp = ConvexDecomposition::new(16, 0.01, 64);
1316 decomp.decompose(&vertices, &indices);
1317 decomposed_hulls = decomp.hulls;
1318 }
1319 }
1320
1321 Self {
1322 mode,
1323 vertices,
1324 indices,
1325 mesh,
1326 convex_hull,
1327 decomposed_hulls,
1328 }
1329 }
1330
1331 fn compute_convex_hull(verts: &[[f64; 3]]) -> Vec<[f64; 3]> {
1333 if verts.is_empty() {
1335 return Vec::new();
1336 }
1337 let mut hull = Vec::new();
1338 let dirs = [
1339 [1.0_f64, 0.0, 0.0],
1340 [-1.0, 0.0, 0.0],
1341 [0.0, 1.0, 0.0],
1342 [0.0, -1.0, 0.0],
1343 [0.0, 0.0, 1.0],
1344 [0.0, 0.0, -1.0],
1345 ];
1346 for d in &dirs {
1347 let best = verts.iter().max_by(|a, b| {
1348 dot3(**a, *d)
1349 .partial_cmp(&dot3(**b, *d))
1350 .unwrap_or(std::cmp::Ordering::Equal)
1351 });
1352 if let Some(v) = best {
1353 hull.push(*v);
1354 }
1355 }
1356 hull.dedup_by(|a, b| {
1357 (a[0] - b[0]).abs() < 1e-10
1358 && (a[1] - b[1]).abs() < 1e-10
1359 && (a[2] - b[2]).abs() < 1e-10
1360 });
1361 hull
1362 }
1363
1364 pub fn aabb(&self) -> Aabb3 {
1366 match &self.mesh {
1367 Some(m) => m.aabb,
1368 None => {
1369 let mut aabb = Aabb3::empty();
1370 for &v in &self.vertices {
1371 aabb.include_point(v);
1372 }
1373 aabb
1374 }
1375 }
1376 }
1377
1378 pub fn support(&self, d: [f64; 3]) -> [f64; 3] {
1380 let verts = match &self.mode {
1381 CollisionMeshMode::ConvexHull => &self.convex_hull,
1382 _ => &self.vertices,
1383 };
1384 if verts.is_empty() {
1385 return [0.0; 3];
1386 }
1387 verts
1388 .iter()
1389 .max_by(|a, b| {
1390 dot3(**a, d)
1391 .partial_cmp(&dot3(**b, d))
1392 .unwrap_or(std::cmp::Ordering::Equal)
1393 })
1394 .copied()
1395 .unwrap_or([0.0; 3])
1396 }
1397}
1398
1399pub fn voxelized_collision(mesh: &TriangleMeshShape, resolution: usize) -> Vec<bool> {
1403 let aabb = mesh.aabb;
1404 let n = resolution;
1405 let mut grid = vec![false; n * n * n];
1406 let query = MeshShapeQuery::new(mesh);
1407
1408 for xi in 0..n {
1409 for yi in 0..n {
1410 for zi in 0..n {
1411 let p = [
1412 aabb.min[0] + (xi as f64 + 0.5) / n as f64 * (aabb.max[0] - aabb.min[0]),
1413 aabb.min[1] + (yi as f64 + 0.5) / n as f64 * (aabb.max[1] - aabb.min[1]),
1414 aabb.min[2] + (zi as f64 + 0.5) / n as f64 * (aabb.max[2] - aabb.min[2]),
1415 ];
1416 grid[xi * n * n + yi * n + zi] = query.point_inside(p);
1417 }
1418 }
1419 }
1420 grid
1421}
1422
1423pub fn shape_volume(shape: &ShapeProxy, resolution: usize) -> f64 {
1425 let aabb = shape.aabb();
1426 let vol = aabb.volume();
1427 if vol < 1e-20 || resolution == 0 {
1428 return vol;
1429 }
1430 let n = resolution;
1431 let mut inside_count = 0usize;
1432 let total = n * n * n;
1433
1434 for xi in 0..n {
1435 for yi in 0..n {
1436 for zi in 0..n {
1437 let p = [
1438 aabb.min[0] + (xi as f64 + 0.5) / n as f64 * (aabb.max[0] - aabb.min[0]),
1439 aabb.min[1] + (yi as f64 + 0.5) / n as f64 * (aabb.max[1] - aabb.min[1]),
1440 aabb.min[2] + (zi as f64 + 0.5) / n as f64 * (aabb.max[2] - aabb.min[2]),
1441 ];
1442 let s = shape.support(p);
1444 if dot3(s, p) > dot3(p, p) - 1e-6 {
1445 inside_count += 1;
1446 }
1447 }
1448 }
1449 }
1450 vol * inside_count as f64 / total as f64
1451}
1452
1453#[cfg(test)]
1458mod tests {
1459 use super::*;
1460
1461 fn make_unit_sphere() -> PrimitiveShape {
1462 PrimitiveShape::Sphere(1.0)
1463 }
1464
1465 fn make_box_shape() -> PrimitiveShape {
1466 PrimitiveShape::Box([1.0, 1.0, 1.0])
1467 }
1468
1469 fn make_tetra() -> TriangleMeshShape {
1470 let verts = vec![
1471 [0.0, 0.0, 0.0],
1472 [1.0, 0.0, 0.0],
1473 [0.5, 1.0, 0.0],
1474 [0.5, 0.5, 1.0],
1475 ];
1476 let idxs = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]];
1477 TriangleMeshShape::new(verts, idxs)
1478 }
1479
1480 #[test]
1481 fn test_aabb_volume() {
1482 let aabb = Aabb3::new([0.0; 3], [2.0, 3.0, 4.0]);
1483 assert!((aabb.volume() - 24.0).abs() < 1e-10);
1484 }
1485
1486 #[test]
1487 fn test_aabb_overlaps() {
1488 let a = Aabb3::new([0.0; 3], [1.0; 3]);
1489 let b = Aabb3::new([0.5; 3], [1.5; 3]);
1490 assert!(a.overlaps(&b));
1491 let c = Aabb3::new([2.0; 3], [3.0; 3]);
1492 assert!(!a.overlaps(&c));
1493 }
1494
1495 #[test]
1496 fn test_aabb_contains_point() {
1497 let aabb = Aabb3::new([0.0; 3], [1.0; 3]);
1498 assert!(aabb.contains_point([0.5, 0.5, 0.5]));
1499 assert!(!aabb.contains_point([1.5, 0.5, 0.5]));
1500 }
1501
1502 #[test]
1503 fn test_sphere_support() {
1504 let sphere = make_unit_sphere();
1505 let s = sphere.support([1.0, 0.0, 0.0]);
1506 assert!(
1507 (s[0] - 1.0).abs() < 1e-10,
1508 "sphere support in +x should be [1,0,0]"
1509 );
1510 }
1511
1512 #[test]
1513 fn test_box_support() {
1514 let b = make_box_shape();
1515 let s = b.support([1.0, 1.0, 1.0]);
1516 assert!((s[0] - 1.0).abs() < 1e-10);
1517 assert!((s[1] - 1.0).abs() < 1e-10);
1518 assert!((s[2] - 1.0).abs() < 1e-10);
1519 }
1520
1521 #[test]
1522 fn test_sphere_volume() {
1523 let s = make_unit_sphere();
1524 let v = s.volume();
1525 let expected = 4.0 / 3.0 * std::f64::consts::PI;
1526 assert!((v - expected).abs() < 1e-6);
1527 }
1528
1529 #[test]
1530 fn test_compound_shape_add_child() {
1531 let mut compound = CompoundShape::new();
1532 compound.add_child(make_unit_sphere(), LocalTransform::identity());
1533 compound.add_child(
1534 make_box_shape(),
1535 LocalTransform::from_translation([3.0, 0.0, 0.0]),
1536 );
1537 assert_eq!(compound.num_children(), 2);
1538 }
1539
1540 #[test]
1541 fn test_compound_aabb_contains_children() {
1542 let mut compound = CompoundShape::new();
1543 compound.add_child(
1544 PrimitiveShape::Sphere(1.0),
1545 LocalTransform::from_translation([0.0, 0.0, 0.0]),
1546 );
1547 compound.add_child(
1548 PrimitiveShape::Sphere(1.0),
1549 LocalTransform::from_translation([5.0, 0.0, 0.0]),
1550 );
1551 let aabb = compound.aabb();
1552 assert!(
1553 aabb.max[0] >= 6.0,
1554 "AABB should extend to 5+1=6 in x: {}",
1555 aabb.max[0]
1556 );
1557 assert!(
1558 aabb.min[0] <= -1.0,
1559 "AABB should extend to -1 in x: {}",
1560 aabb.min[0]
1561 );
1562 }
1563
1564 #[test]
1565 fn test_compound_center_of_mass() {
1566 let mut compound = CompoundShape::new();
1567 compound.add_child(
1568 PrimitiveShape::Sphere(1.0),
1569 LocalTransform::from_translation([0.0, 0.0, 0.0]),
1570 );
1571 compound.add_child(
1572 PrimitiveShape::Sphere(1.0),
1573 LocalTransform::from_translation([2.0, 0.0, 0.0]),
1574 );
1575 let com = compound.center_of_mass();
1576 assert!(
1577 (com[0] - 1.0).abs() < 1e-6,
1578 "COM should be at x=1 for equal spheres at 0 and 2: {}",
1579 com[0]
1580 );
1581 }
1582
1583 #[test]
1584 fn test_triangle_mesh_normals() {
1585 let mesh = make_tetra();
1586 assert_eq!(mesh.normals.len(), 4, "tetrahedron should have 4 normals");
1587 for n in &mesh.normals {
1588 let len = norm3(*n);
1589 assert!(
1590 (len - 1.0).abs() < 1e-6,
1591 "normal should be unit length: {}",
1592 len
1593 );
1594 }
1595 }
1596
1597 #[test]
1598 fn test_triangle_mesh_surface_area() {
1599 let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1601 let idxs = vec![[0, 1, 2]];
1602 let mesh = TriangleMeshShape::new(verts, idxs);
1603 let area = mesh.surface_area();
1604 assert!(
1605 (area - 0.5).abs() < 1e-10,
1606 "unit right triangle area should be 0.5: {}",
1607 area
1608 );
1609 }
1610
1611 #[test]
1612 fn test_closest_point_triangle() {
1613 let a = [0.0f64, 0.0, 0.0];
1614 let b = [1.0, 0.0, 0.0];
1615 let c = [0.0, 1.0, 0.0];
1616 let p = [2.0, 0.0, 0.0]; let cp = MeshShapeQuery::closest_point_triangle(p, a, b, c);
1618 assert!(
1619 (cp[0] - 1.0).abs() < 1e-6,
1620 "closest point should be at b=[1,0,0]: {:?}",
1621 cp
1622 );
1623 }
1624
1625 #[test]
1626 fn test_ray_triangle_intersect_hit() {
1627 let a = [0.0, 0.0, 0.0];
1628 let b = [1.0, 0.0, 0.0];
1629 let c = [0.0, 1.0, 0.0];
1630 let origin = [0.25, 0.25, -1.0];
1631 let dir = [0.0, 0.0, 1.0];
1632 let result = MeshShapeQuery::ray_triangle_intersect(origin, dir, a, b, c);
1633 assert!(result.is_some(), "ray should hit triangle");
1634 }
1635
1636 #[test]
1637 fn test_ray_triangle_intersect_miss() {
1638 let a = [0.0, 0.0, 0.0];
1639 let b = [1.0, 0.0, 0.0];
1640 let c = [0.0, 1.0, 0.0];
1641 let origin = [2.0, 2.0, -1.0];
1642 let dir = [0.0, 0.0, 1.0];
1643 let result = MeshShapeQuery::ray_triangle_intersect(origin, dir, a, b, c);
1644 assert!(result.is_none(), "ray should miss triangle");
1645 }
1646
1647 #[test]
1648 fn test_gjk_polytope_support() {
1649 let verts = vec![
1650 [1.0, 0.0, 0.0],
1651 [-1.0, 0.0, 0.0],
1652 [0.0, 1.0, 0.0],
1653 [0.0, -1.0, 0.0],
1654 ];
1655 let poly = GjkPolytope::new(verts);
1656 let s = poly.support([1.0, 0.0, 0.0]);
1657 assert!((s[0] - 1.0).abs() < 1e-10);
1658 }
1659
1660 #[test]
1661 fn test_minkowski_sum_support() {
1662 let a = GjkPolytope::new(vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]]);
1663 let b = GjkPolytope::new(vec![[0.5, 0.0, 0.0], [-0.5, 0.0, 0.0]]);
1664 let mk = MinkowskiSum::new(a, b, [3.0, 0.0, 0.0]);
1665 let s = mk.support_difference([1.0, 0.0, 0.0]);
1668 let _ = s; }
1670
1671 #[test]
1672 fn test_shape_proxy_name() {
1673 let proxy = ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0));
1674 assert_eq!(proxy.name(), "Sphere");
1675 let box_proxy = ShapeProxy::Primitive(PrimitiveShape::Box([1.0; 3]));
1676 assert_eq!(box_proxy.name(), "Box");
1677 }
1678
1679 #[test]
1680 fn test_compound_bvh_build() {
1681 let shapes = vec![
1682 (
1683 ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0)),
1684 LocalTransform::from_translation([0.0, 0.0, 0.0]),
1685 ),
1686 (
1687 ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0)),
1688 LocalTransform::from_translation([5.0, 0.0, 0.0]),
1689 ),
1690 ];
1691 let bvh = CompoundBvh::build(shapes);
1692 assert!(!bvh.nodes.is_empty());
1693 }
1694
1695 #[test]
1696 fn test_compound_bvh_query() {
1697 let shapes = vec![
1698 (
1699 ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0)),
1700 LocalTransform::from_translation([0.0, 0.0, 0.0]),
1701 ),
1702 (
1703 ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0)),
1704 LocalTransform::from_translation([10.0, 0.0, 0.0]),
1705 ),
1706 ];
1707 let bvh = CompoundBvh::build(shapes);
1708 let query = Aabb3::new([-2.0; 3], [2.0; 3]);
1709 let result = bvh.query_aabb(&query);
1710 assert!(
1711 !result.is_empty(),
1712 "query near origin should find first sphere"
1713 );
1714 }
1715
1716 #[test]
1717 fn test_shape_offset_inflates_aabb() {
1718 let inner = ShapeProxy::Primitive(PrimitiveShape::Sphere(1.0));
1719 let offset = ShapeOffset::new(inner, 0.5);
1720 let aabb = offset.aabb();
1721 assert!(
1722 aabb.max[0] >= 1.5,
1723 "offset should inflate AABB to at least 1.5"
1724 );
1725 }
1726
1727 #[test]
1728 fn test_imported_mesh_convex_hull() {
1729 let verts = vec![
1730 [0.0, 0.0, 0.0],
1731 [1.0, 0.0, 0.0],
1732 [0.0, 1.0, 0.0],
1733 [0.0, 0.0, 1.0],
1734 ];
1735 let idxs = vec![[0, 1, 2], [0, 1, 3]];
1736 let mesh = ImportedCollisionMesh::new(verts, idxs, CollisionMeshMode::ConvexHull);
1737 assert!(
1738 !mesh.convex_hull.is_empty(),
1739 "convex hull should be computed"
1740 );
1741 }
1742
1743 #[test]
1744 fn test_imported_mesh_triangle_mode() {
1745 let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1746 let idxs = vec![[0, 1, 2]];
1747 let mesh = ImportedCollisionMesh::new(verts, idxs, CollisionMeshMode::TriangleMesh);
1748 assert!(mesh.mesh.is_some(), "TriangleMesh mode should build mesh");
1749 }
1750
1751 #[test]
1752 fn test_convex_decomposition_basic() {
1753 let verts = vec![
1754 [0.0, 0.0, 0.0],
1755 [1.0, 0.0, 0.0],
1756 [0.0, 1.0, 0.0],
1757 [0.0, 0.0, 1.0],
1758 [1.0, 1.0, 0.0],
1759 [1.0, 0.0, 1.0],
1760 [0.0, 1.0, 1.0],
1761 [1.0, 1.0, 1.0],
1762 ];
1763 let idxs = vec![[0, 1, 2]];
1764 let mut decomp = ConvexDecomposition::new(4, 0.1, 32);
1765 decomp.decompose(&verts, &idxs);
1766 assert!(decomp.num_hulls() > 0, "should produce at least one hull");
1767 }
1768
1769 #[test]
1770 fn test_merge_hulls() {
1771 let hulls = vec![
1772 vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
1773 vec![[2.0, 0.0, 0.0], [3.0, 0.0, 0.0]],
1774 ];
1775 let merged = merge_hulls(&hulls);
1776 assert_eq!(merged.len(), 4, "merged should have all 4 vertices");
1777 }
1778
1779 #[test]
1780 fn test_local_transform_apply() {
1781 let t = LocalTransform::from_translation([1.0, 2.0, 3.0]);
1782 let p = t.apply([0.0; 3]);
1783 assert!((p[0] - 1.0).abs() < 1e-10);
1784 assert!((p[1] - 2.0).abs() < 1e-10);
1785 assert!((p[2] - 3.0).abs() < 1e-10);
1786 }
1787
1788 #[test]
1789 fn test_local_transform_round_trip() {
1790 let t = LocalTransform::from_translation([5.0, -3.0, 2.0]);
1791 let p = [1.0, 2.0, 3.0];
1792 let p2 = t.apply(p);
1793 let p3 = t.inverse_apply(p2);
1794 for i in 0..3 {
1795 assert!(
1796 (p3[i] - p[i]).abs() < 1e-10,
1797 "round trip failed at axis {}",
1798 i
1799 );
1800 }
1801 }
1802}