1use crate::box_collider::OrientedBox;
20use crate::collider::{Capsule, ConvexHull, Sphere, Support, AABB};
21use crate::mass_properties::{
22 box_mass_properties, capsule_mass_properties, convex_hull_mass_properties,
23 sphere_mass_properties, translate_inertia, MassProperties,
24};
25use crate::math::{Fix128, Mat3Fix, QuatFix, Vec3Fix};
26
27#[cfg(not(feature = "std"))]
28use alloc::vec::Vec;
29
30#[derive(Clone, Debug)]
32pub enum ShapeRef {
33 Sphere(Sphere),
35 Capsule(Capsule),
37 Box(OrientedBox),
39 ConvexHull(ConvexHull),
41}
42
43impl ShapeRef {
44 #[must_use]
46 pub fn aabb(&self) -> AABB {
47 match self {
48 Self::Sphere(s) => {
49 let r = Vec3Fix::new(s.radius, s.radius, s.radius);
50 AABB::new(s.center - r, s.center + r)
51 }
52 Self::Capsule(c) => {
53 let r = Vec3Fix::new(c.radius, c.radius, c.radius);
54 let min = Vec3Fix::new(
55 if c.a.x < c.b.x { c.a.x } else { c.b.x },
56 if c.a.y < c.b.y { c.a.y } else { c.b.y },
57 if c.a.z < c.b.z { c.a.z } else { c.b.z },
58 );
59 let max = Vec3Fix::new(
60 if c.a.x > c.b.x { c.a.x } else { c.b.x },
61 if c.a.y > c.b.y { c.a.y } else { c.b.y },
62 if c.a.z > c.b.z { c.a.z } else { c.b.z },
63 );
64 AABB::new(min - r, max + r)
65 }
66 Self::Box(b) => b.aabb(),
67 Self::ConvexHull(hull) => {
68 let first = hull.vertices[0];
69 let mut min = first;
70 let mut max = first;
71 for &v in &hull.vertices[1..] {
72 if v.x < min.x {
73 min.x = v.x;
74 }
75 if v.y < min.y {
76 min.y = v.y;
77 }
78 if v.z < min.z {
79 min.z = v.z;
80 }
81 if v.x > max.x {
82 max.x = v.x;
83 }
84 if v.y > max.y {
85 max.y = v.y;
86 }
87 if v.z > max.z {
88 max.z = v.z;
89 }
90 }
91 AABB::new(min, max)
92 }
93 }
94 }
95}
96
97#[derive(Clone, Debug)]
99pub struct CompoundChild {
100 pub shape: ShapeRef,
102 pub local_position: Vec3Fix,
104 pub local_rotation: QuatFix,
106}
107
108#[derive(Clone, Debug)]
110pub struct CompoundShape {
111 pub children: Vec<CompoundChild>,
113 pub(crate) cached_aabb: AABB,
115 pub(crate) dirty: bool,
117}
118
119impl CompoundShape {
120 #[must_use]
122 pub const fn new() -> Self {
123 Self {
124 children: Vec::new(),
125 cached_aabb: AABB::new(Vec3Fix::ZERO, Vec3Fix::ZERO),
126 dirty: true,
127 }
128 }
129
130 pub fn add_sphere(&mut self, sphere: Sphere, position: Vec3Fix, rotation: QuatFix) {
132 self.children.push(CompoundChild {
133 shape: ShapeRef::Sphere(sphere),
134 local_position: position,
135 local_rotation: rotation,
136 });
137 self.dirty = true;
138 }
139
140 pub fn add_capsule(&mut self, capsule: Capsule, position: Vec3Fix, rotation: QuatFix) {
142 self.children.push(CompoundChild {
143 shape: ShapeRef::Capsule(capsule),
144 local_position: position,
145 local_rotation: rotation,
146 });
147 self.dirty = true;
148 }
149
150 pub fn add_convex_hull(&mut self, hull: ConvexHull, position: Vec3Fix, rotation: QuatFix) {
152 self.children.push(CompoundChild {
153 shape: ShapeRef::ConvexHull(hull),
154 local_position: position,
155 local_rotation: rotation,
156 });
157 self.dirty = true;
158 }
159
160 #[cfg(feature = "std")]
167 #[must_use]
168 pub fn from_decomposition(result: &crate::convex_decompose::DecompositionResult) -> Self {
169 let mut compound = Self::new();
170 for hull in &result.hulls {
171 compound.add_convex_hull(hull.clone(), Vec3Fix::ZERO, QuatFix::IDENTITY);
172 }
173 compound
174 }
175
176 #[cfg(feature = "std")]
180 #[must_use]
181 pub fn from_sdf(
182 sdf: &dyn crate::sdf_collider::SdfField,
183 min: Vec3Fix,
184 max: Vec3Fix,
185 config: &crate::convex_decompose::DecomposeConfig,
186 ) -> Self {
187 Self::from_decomposition(&crate::convex_decompose::decompose_sdf(
188 sdf, min, max, config,
189 ))
190 }
191
192 pub fn add_box(&mut self, obox: OrientedBox, position: Vec3Fix, rotation: QuatFix) {
194 self.children.push(CompoundChild {
195 shape: ShapeRef::Box(obox),
196 local_position: position,
197 local_rotation: rotation,
198 });
199 self.dirty = true;
200 }
201
202 #[inline]
204 #[must_use]
205 pub fn len(&self) -> usize {
206 self.children.len()
207 }
208
209 #[inline]
211 #[must_use]
212 pub fn is_empty(&self) -> bool {
213 self.children.is_empty()
214 }
215
216 pub fn compute_aabb(&mut self) -> AABB {
218 if self.children.is_empty() {
219 self.cached_aabb = AABB::new(Vec3Fix::ZERO, Vec3Fix::ZERO);
221 self.dirty = false;
222 return self.cached_aabb;
223 }
224
225 let first = self.child_world_aabb(0, Vec3Fix::ZERO, QuatFix::IDENTITY);
226 let mut result = first;
227
228 for i in 1..self.children.len() {
229 let child_aabb = self.child_world_aabb(i, Vec3Fix::ZERO, QuatFix::IDENTITY);
230 result = result.union(&child_aabb);
231 }
232
233 self.cached_aabb = result;
234 self.dirty = false;
235 result
236 }
237
238 #[must_use]
240 pub fn child_world_aabb(&self, child_idx: usize, body_pos: Vec3Fix, body_rot: QuatFix) -> AABB {
241 let child = &self.children[child_idx];
242 let body_rot = body_rot.unit_rotation();
243 let world_pos = body_pos + body_rot.rotate_vec(child.local_position);
244
245 match &child.shape {
246 ShapeRef::Sphere(s) => {
247 let r = Vec3Fix::new(s.radius, s.radius, s.radius);
248 let child_rot = body_rot.mul(child.local_rotation);
252 let center = world_pos + child_rot.rotate_vec(s.center);
253 AABB::new(center - r, center + r)
254 }
255 ShapeRef::Capsule(c) => {
256 let child_rot = body_rot.mul(child.local_rotation);
257 let a = world_pos + child_rot.rotate_vec(c.a);
258 let b = world_pos + child_rot.rotate_vec(c.b);
259 let r = Vec3Fix::new(c.radius, c.radius, c.radius);
260 let min = Vec3Fix::new(
261 if a.x < b.x { a.x } else { b.x },
262 if a.y < b.y { a.y } else { b.y },
263 if a.z < b.z { a.z } else { b.z },
264 );
265 let max = Vec3Fix::new(
266 if a.x > b.x { a.x } else { b.x },
267 if a.y > b.y { a.y } else { b.y },
268 if a.z > b.z { a.z } else { b.z },
269 );
270 AABB::new(min - r, max + r)
271 }
272 ShapeRef::Box(ob) => {
273 let child_rot = body_rot.mul(child.local_rotation);
274 let transformed = OrientedBox::new(
275 world_pos + child_rot.rotate_vec(ob.center),
276 ob.half_extents,
277 child_rot.mul(ob.rotation),
278 );
279 transformed.aabb()
280 }
281 ShapeRef::ConvexHull(hull) => {
282 let child_rot = body_rot.mul(child.local_rotation);
283 let first = world_pos + child_rot.rotate_vec(hull.vertices[0]);
284 let mut min = first;
285 let mut max = first;
286 for &v in &hull.vertices[1..] {
287 let wv = world_pos + child_rot.rotate_vec(v);
288 if wv.x < min.x {
289 min.x = wv.x;
290 }
291 if wv.y < min.y {
292 min.y = wv.y;
293 }
294 if wv.z < min.z {
295 min.z = wv.z;
296 }
297 if wv.x > max.x {
298 max.x = wv.x;
299 }
300 if wv.y > max.y {
301 max.y = wv.y;
302 }
303 if wv.z > max.z {
304 max.z = wv.z;
305 }
306 }
307 AABB::new(min, max)
308 }
309 }
310 }
311
312 #[must_use]
314 pub fn world_aabb(&self, body_pos: Vec3Fix, body_rot: QuatFix) -> AABB {
315 if self.children.is_empty() {
316 return AABB::new(body_pos, body_pos);
317 }
318
319 let mut result = self.child_world_aabb(0, body_pos, body_rot);
320 for i in 1..self.children.len() {
321 let child_aabb = self.child_world_aabb(i, body_pos, body_rot);
322 result = result.union(&child_aabb);
323 }
324 result
325 }
326
327 #[must_use]
330 pub fn overlapping_children(
331 &self,
332 target_aabb: &AABB,
333 body_pos: Vec3Fix,
334 body_rot: QuatFix,
335 ) -> Vec<usize> {
336 let mut result = Vec::new();
337 for i in 0..self.children.len() {
338 let child_aabb = self.child_world_aabb(i, body_pos, body_rot);
339 if child_aabb.intersects(target_aabb) {
340 result.push(i);
341 }
342 }
343 result
344 }
345
346 #[must_use]
348 pub fn support_world(
349 &self,
350 direction: Vec3Fix,
351 body_pos: Vec3Fix,
352 body_rot: QuatFix,
353 ) -> Vec3Fix {
354 if self.children.is_empty() {
355 return body_pos;
356 }
357 let mut best: Option<(Fix128, Vec3Fix)> = None;
361
362 for child in &self.children {
363 let s = child.support_world(direction, body_pos, body_rot);
364 let d = s.dot(direction);
365 if best.is_none_or(|(best_dot, _)| d > best_dot) {
366 best = Some((d, s));
367 }
368 }
369
370 best.map_or(body_pos, |(_, point)| point)
371 }
372
373 #[must_use]
387 pub fn mass_properties(&self, density: Fix128) -> MassProperties {
388 if density <= Fix128::ZERO {
389 return MassProperties::ZERO;
390 }
391 let parts: Vec<MassProperties> = self
393 .children
394 .iter()
395 .map(|child| child.mass_properties(density))
396 .collect();
397 let mass = parts.iter().fold(Fix128::ZERO, |acc, p| acc + p.mass);
398 if mass <= Fix128::ZERO {
399 return MassProperties::ZERO;
400 }
401 let first_moment = parts
402 .iter()
403 .fold(Vec3Fix::ZERO, |acc, p| acc + p.center_of_mass * p.mass);
404 let com = first_moment / mass;
405 let mut inertia = Mat3Fix::ZERO;
406 for p in &parts {
407 let about_com = translate_inertia(p, com - p.center_of_mass);
408 inertia = Mat3Fix::from_cols(
409 inertia.col0 + about_com.col0,
410 inertia.col1 + about_com.col1,
411 inertia.col2 + about_com.col2,
412 );
413 }
414 MassProperties {
415 mass,
416 center_of_mass: com,
417 inertia_tensor: inertia,
418 }
419 }
420}
421
422impl CompoundChild {
423 #[must_use]
426 pub fn support_world(
427 &self,
428 direction: Vec3Fix,
429 body_pos: Vec3Fix,
430 body_rot: QuatFix,
431 ) -> Vec3Fix {
432 let body_rot = body_rot.unit_rotation();
433 let child_rot = body_rot.mul(self.local_rotation);
434 let child_pos = body_pos + body_rot.rotate_vec(self.local_position);
435 match &self.shape {
436 ShapeRef::Sphere(sphere) => {
437 let shifted = Sphere::new(
438 child_pos + child_rot.rotate_vec(sphere.center),
439 sphere.radius,
440 );
441 shifted.support(direction)
442 }
443 ShapeRef::Capsule(cap) => {
444 let a = child_pos + child_rot.rotate_vec(cap.a);
445 let b = child_pos + child_rot.rotate_vec(cap.b);
446 let shifted = Capsule::new(a, b, cap.radius);
447 shifted.support(direction)
448 }
449 ShapeRef::Box(ob) => {
450 let shifted = OrientedBox::new(
451 child_pos + child_rot.rotate_vec(ob.center),
452 ob.half_extents,
453 child_rot.mul(ob.rotation),
454 );
455 shifted.support(direction)
456 }
457 ShapeRef::ConvexHull(hull) => {
458 let mut best_v = child_pos + child_rot.rotate_vec(hull.vertices[0]);
460 let mut best_d = best_v.dot(direction);
461 for &v in &hull.vertices[1..] {
462 let wv = child_pos + child_rot.rotate_vec(v);
463 let dd = wv.dot(direction);
464 if dd > best_d {
465 best_v = wv;
466 best_d = dd;
467 }
468 }
469 best_v
470 }
471 }
472 }
473
474 fn mass_properties(&self, density: Fix128) -> MassProperties {
477 let rot = self.local_rotation;
478 let (own, frame) = match &self.shape {
480 ShapeRef::Sphere(s) => {
481 let mut p = sphere_mass_properties(s.radius, density);
482 p.center_of_mass = s.center;
483 (p, QuatFix::IDENTITY)
484 }
485 ShapeRef::Box(ob) => {
486 let mut p = box_mass_properties(ob.half_extents, density);
487 p.center_of_mass = ob.center;
488 (p, ob.rotation)
489 }
490 ShapeRef::Capsule(cap) => {
491 let axis = cap.b - cap.a;
492 let length = axis.length();
493 let middle = (cap.a + cap.b) * Fix128::from_ratio(1, 2);
494 if length.is_zero() {
495 let mut p = sphere_mass_properties(cap.radius, density);
496 p.center_of_mass = middle;
497 (p, QuatFix::IDENTITY)
498 } else {
499 let p = capsule_mass_properties(cap.radius, length.half(), density);
503 let u = axis / length;
504 let d = p.inertia_tensor;
505 let (i_perp, i_axis) = (d.col0.x, d.col1.y);
506 let outer = Mat3Fix::from_cols(u * u.x, u * u.y, u * u.z);
507 let e = Mat3Fix::IDENTITY;
508 let diff = i_axis - i_perp;
509 let tensor = Mat3Fix::from_cols(
510 e.col0 * i_perp + outer.col0 * diff,
511 e.col1 * i_perp + outer.col1 * diff,
512 e.col2 * i_perp + outer.col2 * diff,
513 );
514 (
515 MassProperties {
516 mass: p.mass,
517 center_of_mass: middle,
518 inertia_tensor: tensor,
519 },
520 QuatFix::IDENTITY,
521 )
522 }
523 }
524 ShapeRef::ConvexHull(hull) => (
525 convex_hull_mass_properties(&hull.vertices, density),
526 QuatFix::IDENTITY,
527 ),
528 };
529 let turn = rot.mul(frame);
532 let r = rotation_matrix(turn);
533 let inertia = r.mul_mat(own.inertia_tensor).mul_mat(r.transpose());
534 MassProperties {
535 mass: own.mass,
536 center_of_mass: self.local_position + rot.rotate_vec(own.center_of_mass),
537 inertia_tensor: inertia,
538 }
539 }
540}
541
542fn rotation_matrix(q: QuatFix) -> Mat3Fix {
545 Mat3Fix::from_cols(
546 q.rotate_vec(Vec3Fix::UNIT_X),
547 q.rotate_vec(Vec3Fix::UNIT_Y),
548 q.rotate_vec(Vec3Fix::UNIT_Z),
549 )
550}
551
552impl Default for CompoundShape {
553 fn default() -> Self {
554 Self::new()
555 }
556}
557
558pub struct TransformedCompound<'a> {
560 pub compound: &'a CompoundShape,
562 pub position: Vec3Fix,
564 pub rotation: QuatFix,
566}
567
568impl Support for TransformedCompound<'_> {
569 fn support(&self, direction: Vec3Fix) -> Vec3Fix {
570 self.compound
571 .support_world(direction, self.position, self.rotation)
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 #[cfg(not(feature = "std"))]
579 use alloc::vec;
580
581 #[test]
582 fn test_compound_basic() {
583 let mut compound = CompoundShape::new();
584 assert!(compound.is_empty());
585
586 compound.add_sphere(
587 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
588 Vec3Fix::ZERO,
589 QuatFix::IDENTITY,
590 );
591 compound.add_sphere(
592 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
593 Vec3Fix::from_int(5, 0, 0),
594 QuatFix::IDENTITY,
595 );
596
597 assert_eq!(compound.len(), 2);
598 }
599
600 #[test]
601 fn test_compound_aabb() {
602 let mut compound = CompoundShape::new();
603 compound.add_sphere(
604 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
605 Vec3Fix::ZERO,
606 QuatFix::IDENTITY,
607 );
608 compound.add_sphere(
609 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
610 Vec3Fix::from_int(10, 0, 0),
611 QuatFix::IDENTITY,
612 );
613
614 let aabb = compound.world_aabb(Vec3Fix::ZERO, QuatFix::IDENTITY);
615 assert!(aabb.min.x <= -Fix128::ONE);
617 assert!(aabb.max.x >= Fix128::from_int(11));
618 }
619
620 #[test]
621 fn test_compound_support() {
622 let mut compound = CompoundShape::new();
623 compound.add_sphere(
624 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
625 Vec3Fix::ZERO,
626 QuatFix::IDENTITY,
627 );
628 compound.add_sphere(
629 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
630 Vec3Fix::from_int(10, 0, 0),
631 QuatFix::IDENTITY,
632 );
633
634 let s = compound.support_world(Vec3Fix::UNIT_X, Vec3Fix::ZERO, QuatFix::IDENTITY);
636 assert!(
637 s.x >= Fix128::from_int(10),
638 "Support should be from far sphere"
639 );
640 }
641
642 #[test]
643 fn test_compound_with_box() {
644 let mut compound = CompoundShape::new();
645 compound.add_box(
646 OrientedBox::axis_aligned(Vec3Fix::ZERO, Vec3Fix::from_int(1, 1, 1)),
647 Vec3Fix::ZERO,
648 QuatFix::IDENTITY,
649 );
650 compound.add_capsule(
651 Capsule::new(
652 Vec3Fix::from_int(0, -2, 0),
653 Vec3Fix::from_int(0, 2, 0),
654 Fix128::from_ratio(5, 10),
655 ),
656 Vec3Fix::from_int(3, 0, 0),
657 QuatFix::IDENTITY,
658 );
659
660 assert_eq!(compound.len(), 2);
661
662 let aabb = compound.world_aabb(Vec3Fix::ZERO, QuatFix::IDENTITY);
663 assert!(aabb.min.y <= -Fix128::from_int(2));
664 }
665
666 #[test]
667 fn test_transformed_compound_support() {
668 let mut compound = CompoundShape::new();
669 compound.add_sphere(
670 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
671 Vec3Fix::from_int(5, 0, 0),
672 QuatFix::IDENTITY,
673 );
674
675 let tc = TransformedCompound {
676 compound: &compound,
677 position: Vec3Fix::from_int(10, 0, 0),
678 rotation: QuatFix::IDENTITY,
679 };
680
681 let s = tc.support(Vec3Fix::UNIT_X);
682 assert!(s.x >= Fix128::from_int(15));
684 }
685
686 #[test]
687 fn test_compound_convex_hull() {
688 let mut compound = CompoundShape::new();
689
690 let hull = ConvexHull::new(vec![
692 Vec3Fix::from_int(0, 0, 0),
693 Vec3Fix::from_int(2, 0, 0),
694 Vec3Fix::from_int(1, 2, 0),
695 ]);
696 compound.add_convex_hull(hull, Vec3Fix::ZERO, QuatFix::IDENTITY);
697
698 assert_eq!(compound.len(), 1);
699
700 let aabb = compound.world_aabb(Vec3Fix::ZERO, QuatFix::IDENTITY);
701 assert!(aabb.min.x <= Fix128::ZERO);
702 assert!(aabb.max.x >= Fix128::from_int(2));
703 assert!(aabb.max.y >= Fix128::from_int(2));
704 }
705
706 #[test]
707 fn test_compound_convex_hull_support() {
708 let mut compound = CompoundShape::new();
709
710 let hull = ConvexHull::new(vec![
711 Vec3Fix::from_int(0, 0, 0),
712 Vec3Fix::from_int(5, 0, 0),
713 Vec3Fix::from_int(0, 5, 0),
714 ]);
715 compound.add_convex_hull(hull, Vec3Fix::from_int(10, 0, 0), QuatFix::IDENTITY);
716
717 let s = compound.support_world(Vec3Fix::UNIT_X, Vec3Fix::ZERO, QuatFix::IDENTITY);
719 assert!(
720 s.x >= Fix128::from_int(14),
721 "Support in +X should be at x=15, got {}",
722 s.x.to_f32()
723 );
724 }
725
726 #[test]
727 fn test_overlapping_children() {
728 let mut compound = CompoundShape::new();
729 compound.add_sphere(
731 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
732 Vec3Fix::from_int(-10, 0, 0),
733 QuatFix::IDENTITY,
734 );
735 compound.add_sphere(
737 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
738 Vec3Fix::from_int(10, 0, 0),
739 QuatFix::IDENTITY,
740 );
741
742 let query = AABB::new(Vec3Fix::from_int(9, -1, -1), Vec3Fix::from_int(12, 1, 1));
744 let result = compound.overlapping_children(&query, Vec3Fix::ZERO, QuatFix::IDENTITY);
745 assert_eq!(result.len(), 1);
746 assert_eq!(result[0], 1); let query_all = AABB::new(Vec3Fix::from_int(-11, -1, -1), Vec3Fix::from_int(11, 1, 1));
750 let result_all =
751 compound.overlapping_children(&query_all, Vec3Fix::ZERO, QuatFix::IDENTITY);
752 assert_eq!(result_all.len(), 2);
753 }
754
755 #[test]
756 fn compute_aabb_is_exact_union_of_children_in_local_space_and_clears_dirty() {
757 let mut compound = CompoundShape::new();
758 assert!(compound.dirty);
760 assert_eq!(
761 compound.compute_aabb(),
762 AABB::new(Vec3Fix::ZERO, Vec3Fix::ZERO)
763 );
764 assert!(!compound.dirty);
765 assert_eq!(
766 compound.cached_aabb,
767 AABB::new(Vec3Fix::ZERO, Vec3Fix::ZERO)
768 );
769
770 compound.add_sphere(
772 Sphere::new(Vec3Fix::ZERO, Fix128::ONE),
773 Vec3Fix::from_int(10, 0, 0),
774 QuatFix::IDENTITY,
775 );
776 assert!(compound.dirty, "add_* は dirty を立てる");
777 let one = compound.compute_aabb();
778 assert_eq!(one.min, Vec3Fix::from_int(9, -1, -1));
779 assert_eq!(one.max, Vec3Fix::from_int(11, 1, 1));
780 assert_eq!(compound.cached_aabb, one);
781 assert!(!compound.dirty);
782
783 compound.add_capsule(
785 Capsule::new(
786 Vec3Fix::from_int(0, -3, 0),
787 Vec3Fix::from_int(0, 3, 0),
788 Fix128::from_int(2),
789 ),
790 Vec3Fix::from_int(-4, 0, 5),
791 QuatFix::IDENTITY,
792 );
793 compound.add_sphere(
795 Sphere::new(Vec3Fix::from_int(0, 0, -8), Fix128::ONE),
796 Vec3Fix::ZERO,
797 QuatFix::IDENTITY,
798 );
799 let all = compound.compute_aabb();
800 assert_eq!(all.min, Vec3Fix::from_int(-6, -5, -9));
802 assert_eq!(all.max, Vec3Fix::from_int(11, 5, 7));
803 assert_eq!(compound.cached_aabb, all);
804 assert!(!compound.dirty);
805 assert_eq!(all, compound.world_aabb(Vec3Fix::ZERO, QuatFix::IDENTITY));
807 for i in 0..compound.len() {
808 let c = compound.child_world_aabb(i, Vec3Fix::ZERO, QuatFix::IDENTITY);
809 assert_eq!(all.union(&c), all, "child {i} not enclosed");
810 }
811 assert_eq!(compound.compute_aabb(), all);
813 let shifted = compound.world_aabb(Vec3Fix::from_int(100, 0, 0), QuatFix::IDENTITY);
815 assert_eq!(shifted.min, Vec3Fix::from_int(94, -5, -9));
816 assert_eq!(shifted.max, Vec3Fix::from_int(111, 5, 7));
817 assert_eq!(compound.compute_aabb(), all);
818 }
819}