1use crate::shape::{RayHit, Shape};
7use oxiphysics_core::Aabb;
8use oxiphysics_core::math::{Mat3, Real, Vec3};
9
10#[derive(Debug, Clone)]
12pub struct BoxShape {
13 pub half_extents: Vec3,
15}
16
17impl BoxShape {
18 pub fn new(half_extents: Vec3) -> Self {
20 Self { half_extents }
21 }
22
23 pub fn volume_explicit(&self) -> Real {
25 8.0 * self.half_extents.x * self.half_extents.y * self.half_extents.z
26 }
27
28 pub fn surface_area(&self) -> Real {
30 let hx = self.half_extents.x;
31 let hy = self.half_extents.y;
32 let hz = self.half_extents.z;
33 let ax = 2.0 * hx;
34 let ay = 2.0 * hy;
35 let az = 2.0 * hz;
36 2.0 * (ax * ay + ay * az + ax * az)
37 }
38
39 pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
42 let hx = self.half_extents.x;
43 let hy = self.half_extents.y;
44 let hz = self.half_extents.z;
45 let ax2 = (2.0 * hx).powi(2);
46 let ay2 = (2.0 * hy).powi(2);
47 let az2 = (2.0 * hz).powi(2);
48 let k = mass / 12.0;
49 [
50 [k * (ay2 + az2), 0.0, 0.0],
51 [0.0, k * (ax2 + az2), 0.0],
52 [0.0, 0.0, k * (ax2 + ay2)],
53 ]
54 }
55
56 pub fn ray_cast_array(
58 &self,
59 origin: [f64; 3],
60 direction: [f64; 3],
61 max_toi: f64,
62 ) -> Option<(f64, [f64; 3])> {
63 let o = Vec3::new(origin[0], origin[1], origin[2]);
64 let d = Vec3::new(direction[0], direction[1], direction[2]);
65 let hit = self.ray_cast(&o, &d, max_toi)?;
66 Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
67 }
68
69 pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
71 [
72 self.half_extents.x.copysign(direction[0]),
73 self.half_extents.y.copysign(direction[1]),
74 self.half_extents.z.copysign(direction[2]),
75 ]
76 }
77
78 pub fn vertex_list(&self) -> [[f64; 3]; 8] {
80 let hx = self.half_extents.x;
81 let hy = self.half_extents.y;
82 let hz = self.half_extents.z;
83 [
84 [-hx, -hy, -hz],
85 [hx, -hy, -hz],
86 [hx, hy, -hz],
87 [-hx, hy, -hz],
88 [-hx, -hy, hz],
89 [hx, -hy, hz],
90 [hx, hy, hz],
91 [-hx, hy, hz],
92 ]
93 }
94
95 pub fn face_normals() -> [[f64; 3]; 6] {
99 [
100 [1.0, 0.0, 0.0],
101 [-1.0, 0.0, 0.0],
102 [0.0, 1.0, 0.0],
103 [0.0, -1.0, 0.0],
104 [0.0, 0.0, 1.0],
105 [0.0, 0.0, -1.0],
106 ]
107 }
108
109 pub fn edge_list() -> [(usize, usize); 12] {
111 [
112 (0, 1),
114 (1, 5),
115 (5, 4),
116 (4, 0),
117 (2, 3),
119 (3, 7),
120 (7, 6),
121 (6, 2),
122 (0, 3),
124 (1, 2),
125 (5, 6),
126 (4, 7),
127 ]
128 }
129
130 pub fn face_vertex_indices() -> [[usize; 4]; 6] {
133 [
134 [1, 2, 6, 5], [0, 4, 7, 3], [2, 3, 7, 6], [0, 1, 5, 4], [4, 5, 6, 7], [0, 3, 2, 1], ]
141 }
142
143 pub fn face_areas(&self) -> [f64; 6] {
145 let hx = self.half_extents.x;
146 let hy = self.half_extents.y;
147 let hz = self.half_extents.z;
148 let yz = 4.0 * hy * hz; let xz = 4.0 * hx * hz; let xy = 4.0 * hx * hy; [yz, yz, xz, xz, xy, xy]
152 }
153
154 pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
156 [
157 p[0].clamp(-self.half_extents.x, self.half_extents.x),
158 p[1].clamp(-self.half_extents.y, self.half_extents.y),
159 p[2].clamp(-self.half_extents.z, self.half_extents.z),
160 ]
161 }
162
163 pub fn contains_point(&self, p: [f64; 3]) -> bool {
165 p[0].abs() <= self.half_extents.x
166 && p[1].abs() <= self.half_extents.y
167 && p[2].abs() <= self.half_extents.z
168 }
169
170 pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
173 let dx = p[0].abs() - self.half_extents.x;
174 let dy = p[1].abs() - self.half_extents.y;
175 let dz = p[2].abs() - self.half_extents.z;
176
177 if dx <= 0.0 && dy <= 0.0 && dz <= 0.0 {
178 dx.max(dy).max(dz)
180 } else {
181 let cx = dx.max(0.0);
183 let cy = dy.max(0.0);
184 let cz = dz.max(0.0);
185 (cx * cx + cy * cy + cz * cz).sqrt()
186 }
187 }
188
189 pub fn clip_segment(&self, a: [f64; 3], b: [f64; 3]) -> Option<(f64, f64)> {
193 let dir = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
194 let half = [
195 self.half_extents.x,
196 self.half_extents.y,
197 self.half_extents.z,
198 ];
199
200 let mut tmin = 0.0_f64;
201 let mut tmax = 1.0_f64;
202
203 for i in 0..3 {
204 if dir[i].abs() < 1e-12 {
205 if a[i] < -half[i] || a[i] > half[i] {
206 return None;
207 }
208 } else {
209 let inv_d = 1.0 / dir[i];
210 let mut t1 = (-half[i] - a[i]) * inv_d;
211 let mut t2 = (half[i] - a[i]) * inv_d;
212 if t1 > t2 {
213 std::mem::swap(&mut t1, &mut t2);
214 }
215 tmin = tmin.max(t1);
216 tmax = tmax.min(t2);
217 if tmin > tmax {
218 return None;
219 }
220 }
221 }
222
223 Some((tmin.max(0.0), tmax.min(1.0)))
224 }
225
226 pub fn classify_face(&self, p: [f64; 3]) -> usize {
229 let dx_pos = (p[0] - self.half_extents.x).abs();
230 let dx_neg = (p[0] + self.half_extents.x).abs();
231 let dy_pos = (p[1] - self.half_extents.y).abs();
232 let dy_neg = (p[1] + self.half_extents.y).abs();
233 let dz_pos = (p[2] - self.half_extents.z).abs();
234 let dz_neg = (p[2] + self.half_extents.z).abs();
235
236 let dists = [dx_pos, dx_neg, dy_pos, dy_neg, dz_pos, dz_neg];
237 let mut min_idx = 0;
238 let mut min_val = dists[0];
239 for (i, &d) in dists.iter().enumerate().skip(1) {
240 if d < min_val {
241 min_val = d;
242 min_idx = i;
243 }
244 }
245 min_idx
246 }
247
248 pub fn diagonal_length(&self) -> f64 {
250 let hx = self.half_extents.x;
251 let hy = self.half_extents.y;
252 let hz = self.half_extents.z;
253 2.0 * (hx * hx + hy * hy + hz * hz).sqrt()
254 }
255
256 pub fn edge_lengths(&self) -> [f64; 3] {
258 [
259 2.0 * self.half_extents.x,
260 2.0 * self.half_extents.y,
261 2.0 * self.half_extents.z,
262 ]
263 }
264
265 pub fn project_on_axis(&self, axis: [f64; 3]) -> (f64, f64) {
267 let extent = self.half_extents.x * axis[0].abs()
269 + self.half_extents.y * axis[1].abs()
270 + self.half_extents.z * axis[2].abs();
271 (-extent, extent)
272 }
273}
274
275impl Shape for BoxShape {
276 fn bounding_box(&self) -> Aabb {
277 Aabb::new(-self.half_extents, self.half_extents)
278 }
279
280 fn support_point(&self, direction: &Vec3) -> Vec3 {
281 Vec3::new(
282 self.half_extents.x.copysign(direction.x),
283 self.half_extents.y.copysign(direction.y),
284 self.half_extents.z.copysign(direction.z),
285 )
286 }
287
288 fn volume(&self) -> Real {
289 8.0 * self.half_extents.x * self.half_extents.y * self.half_extents.z
290 }
291
292 fn center_of_mass(&self) -> Vec3 {
293 Vec3::zeros()
294 }
295
296 fn inertia_tensor(&self, mass: Real) -> Mat3 {
297 let hx = self.half_extents.x;
298 let hy = self.half_extents.y;
299 let hz = self.half_extents.z;
300 let ex2 = (2.0 * hx).powi(2);
302 let ey2 = (2.0 * hy).powi(2);
303 let ez2 = (2.0 * hz).powi(2);
304 let k = mass / 12.0;
305 Mat3::new(
306 k * (ey2 + ez2),
307 0.0,
308 0.0,
309 0.0,
310 k * (ex2 + ez2),
311 0.0,
312 0.0,
313 0.0,
314 k * (ex2 + ey2),
315 )
316 }
317
318 fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
319 let mut tmin = Real::NEG_INFINITY;
321 let mut tmax = Real::INFINITY;
322 let mut normal = Vec3::zeros();
323
324 for i in 0..3 {
325 let o = ray_origin[i];
326 let d = ray_direction[i];
327 let half = self.half_extents[i];
328
329 if d.abs() < 1e-12 {
330 if o < -half || o > half {
331 return None;
332 }
333 } else {
334 let inv_d = 1.0 / d;
335 let mut t1 = (-half - o) * inv_d;
336 let mut t2 = (half - o) * inv_d;
337 let mut n = Vec3::zeros();
338 n[i] = -1.0;
339
340 if t1 > t2 {
341 std::mem::swap(&mut t1, &mut t2);
342 n[i] = 1.0;
343 }
344
345 if t1 > tmin {
346 tmin = t1;
347 normal = n;
348 }
349 if t2 < tmax {
350 tmax = t2;
351 }
352
353 if tmin > tmax {
354 return None;
355 }
356 }
357 }
358
359 let t = if tmin >= 0.0 { tmin } else { tmax };
360 if t < 0.0 || t > max_toi {
361 return None;
362 }
363
364 let point = ray_origin + ray_direction * t;
365 Some(RayHit {
366 point,
367 normal,
368 toi: t,
369 })
370 }
371}
372
373fn dot3b(a: [f64; 3], b: [f64; 3]) -> f64 {
379 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
380}
381
382fn mat3_mul_vec(rot: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
384 [dot3b(rot[0], v), dot3b(rot[1], v), dot3b(rot[2], v)]
385}
386
387fn mat3_transpose(rot: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
389 [
390 [rot[0][0], rot[1][0], rot[2][0]],
391 [rot[0][1], rot[1][1], rot[2][1]],
392 [rot[0][2], rot[1][2], rot[2][2]],
393 ]
394}
395
396fn mat3_mul(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
398 let bt = mat3_transpose(b);
399 [
400 [dot3b(a[0], bt[0]), dot3b(a[0], bt[1]), dot3b(a[0], bt[2])],
401 [dot3b(a[1], bt[0]), dot3b(a[1], bt[1]), dot3b(a[1], bt[2])],
402 [dot3b(a[2], bt[0]), dot3b(a[2], bt[1]), dot3b(a[2], bt[2])],
403 ]
404}
405
406pub fn obb_inertia_tensor(half_extents: [f64; 3], mass: f64) -> [[f64; 3]; 3] {
410 let ax2 = (2.0 * half_extents[0]).powi(2);
411 let ay2 = (2.0 * half_extents[1]).powi(2);
412 let az2 = (2.0 * half_extents[2]).powi(2);
413 let k = mass / 12.0;
414 [
415 [k * (ay2 + az2), 0.0, 0.0],
416 [0.0, k * (ax2 + az2), 0.0],
417 [0.0, 0.0, k * (ax2 + ay2)],
418 ]
419}
420
421pub fn obb_inertia_tensor_rotated(
425 half_extents: [f64; 3],
426 mass: f64,
427 rot: [[f64; 3]; 3],
428) -> [[f64; 3]; 3] {
429 let i_local = obb_inertia_tensor(half_extents, mass);
430 let ri = mat3_mul(rot, i_local);
432 mat3_mul(ri, mat3_transpose(rot))
433}
434
435pub fn obb_closest_point(
441 p: [f64; 3],
442 center: [f64; 3],
443 half_extents: [f64; 3],
444 rot: [[f64; 3]; 3],
445) -> [f64; 3] {
446 let d = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
448 let local = mat3_mul_vec(rot, d);
449
450 let clamped = [
452 local[0].clamp(-half_extents[0], half_extents[0]),
453 local[1].clamp(-half_extents[1], half_extents[1]),
454 local[2].clamp(-half_extents[2], half_extents[2]),
455 ];
456
457 let rt = mat3_transpose(rot);
459 let world_offset = mat3_mul_vec(rt, clamped);
460 [
461 center[0] + world_offset[0],
462 center[1] + world_offset[1],
463 center[2] + world_offset[2],
464 ]
465}
466
467pub fn obb_projection_extent(half_extents: [f64; 3], rot: [[f64; 3]; 3], axis: [f64; 3]) -> f64 {
470 let mut ext = 0.0_f64;
473 for i in 0..3 {
474 ext += half_extents[i] * dot3b(rot[i], axis).abs();
475 }
476 ext
477}
478
479pub fn obb_ray_intersection(
484 ray_origin: [f64; 3],
485 ray_dir: [f64; 3],
486 center: [f64; 3],
487 half_extents: [f64; 3],
488 rot: [[f64; 3]; 3],
489 max_toi: f64,
490) -> Option<f64> {
491 let d_world = [
493 ray_origin[0] - center[0],
494 ray_origin[1] - center[1],
495 ray_origin[2] - center[2],
496 ];
497 let o_local = mat3_mul_vec(rot, d_world);
498 let dir_local = mat3_mul_vec(rot, ray_dir);
499
500 let mut tmin = f64::NEG_INFINITY;
501 let mut tmax = f64::INFINITY;
502
503 for i in 0..3 {
504 let o = o_local[i];
505 let d = dir_local[i];
506 let h = half_extents[i];
507
508 if d.abs() < 1e-12 {
509 if o < -h || o > h {
510 return None;
511 }
512 } else {
513 let inv_d = 1.0 / d;
514 let t1 = (-h - o) * inv_d;
515 let t2 = (h - o) * inv_d;
516 let (t_near, t_far) = if t1 <= t2 { (t1, t2) } else { (t2, t1) };
517 tmin = tmin.max(t_near);
518 tmax = tmax.min(t_far);
519 if tmin > tmax {
520 return None;
521 }
522 }
523 }
524
525 let t = if tmin >= 0.0 { tmin } else { tmax };
526 if t < 0.0 || t > max_toi {
527 None
528 } else {
529 Some(t)
530 }
531}
532
533pub fn obb_plane_distance(
537 center: [f64; 3],
538 half_extents: [f64; 3],
539 rot: [[f64; 3]; 3],
540 plane: [f64; 4],
541) -> (f64, f64) {
542 let normal = [plane[0], plane[1], plane[2]];
543 let plane_d = plane[3];
544
545 let center_dist = dot3b(center, normal) + plane_d;
547
548 let extent = obb_projection_extent(half_extents, rot, normal);
550
551 (center_dist - extent, center_dist + extent)
552}
553
554pub fn obb_vertices(center: [f64; 3], half_extents: [f64; 3], rot: [[f64; 3]; 3]) -> Vec<[f64; 3]> {
556 let rt = mat3_transpose(rot);
557 let signs = [
558 [-1.0_f64, -1.0, -1.0],
559 [1.0, -1.0, -1.0],
560 [1.0, 1.0, -1.0],
561 [-1.0, 1.0, -1.0],
562 [-1.0, -1.0, 1.0],
563 [1.0, -1.0, 1.0],
564 [1.0, 1.0, 1.0],
565 [-1.0, 1.0, 1.0],
566 ];
567 signs
568 .iter()
569 .map(|s| {
570 let local = [
571 s[0] * half_extents[0],
572 s[1] * half_extents[1],
573 s[2] * half_extents[2],
574 ];
575 let world = mat3_mul_vec(rt, local);
576 [
577 center[0] + world[0],
578 center[1] + world[1],
579 center[2] + world[2],
580 ]
581 })
582 .collect()
583}
584
585pub fn obb_edges(
587 center: [f64; 3],
588 half_extents: [f64; 3],
589 rot: [[f64; 3]; 3],
590) -> Vec<([f64; 3], [f64; 3])> {
591 let verts = obb_vertices(center, half_extents, rot);
592 let pairs: [(usize, usize); 12] = [
594 (0, 1),
595 (1, 5),
596 (5, 4),
597 (4, 0),
598 (2, 3),
599 (3, 7),
600 (7, 6),
601 (6, 2),
602 (0, 3),
603 (1, 2),
604 (5, 6),
605 (4, 7),
606 ];
607 pairs.iter().map(|&(a, b)| (verts[a], verts[b])).collect()
608}
609
610pub fn obb_face_centers(
613 center: [f64; 3],
614 half_extents: [f64; 3],
615 rot: [[f64; 3]; 3],
616) -> Vec<[f64; 3]> {
617 let rt = mat3_transpose(rot);
618 let mut fc = Vec::with_capacity(6);
619 for axis in 0..3 {
620 for sign in [1.0_f64, -1.0_f64] {
621 let mut local = [0.0_f64; 3];
622 local[axis] = sign * half_extents[axis];
623 let world = mat3_mul_vec(rt, local);
624 fc.push([
625 center[0] + world[0],
626 center[1] + world[1],
627 center[2] + world[2],
628 ]);
629 }
630 }
631 fc
632}
633
634pub fn obb_support_fn(
636 center: [f64; 3],
637 half_extents: [f64; 3],
638 rot: [[f64; 3]; 3],
639 direction: [f64; 3],
640) -> [f64; 3] {
641 let local_dir = mat3_mul_vec(rot, direction);
643 let local_support = [
644 half_extents[0].copysign(local_dir[0]),
645 half_extents[1].copysign(local_dir[1]),
646 half_extents[2].copysign(local_dir[2]),
647 ];
648 let rt = mat3_transpose(rot);
649 let world_offset = mat3_mul_vec(rt, local_support);
650 [
651 center[0] + world_offset[0],
652 center[1] + world_offset[1],
653 center[2] + world_offset[2],
654 ]
655}
656
657pub fn obb_aabb_bounds(
659 center: [f64; 3],
660 half_extents: [f64; 3],
661 rot: [[f64; 3]; 3],
662) -> ([f64; 3], [f64; 3]) {
663 let mut lo = center;
664 let mut hi = center;
665 let rt = mat3_transpose(rot);
666 for axis in 0..3 {
667 for k in 0..3 {
669 let contribution = rt[k][axis].abs() * half_extents[axis];
670 lo[k] -= contribution;
671 hi[k] += contribution;
672 }
673 }
674 (lo, hi)
675}
676
677#[cfg(test)]
678mod proptest_tests {
679 use super::*;
680 use crate::BoxShape;
681 use crate::box_shape::Vec3;
682
683 use proptest::prelude::*;
684
685 fn pos_half() -> impl Strategy<Value = f64> {
686 0.01_f64..100.0_f64
687 }
688
689 proptest! {
690 #[test]
691 fn prop_box_volume_nonneg(
692 hx in pos_half(),
693 hy in pos_half(),
694 hz in pos_half(),
695 ) {
696 let b = BoxShape::new(Vec3::new(hx, hy, hz));
697 prop_assert!(b.volume() >= 0.0, "volume negative: {}", b.volume());
698 }
699
700 #[test]
701 fn prop_box_inertia_positive_definite(
702 hx in pos_half(),
703 hy in pos_half(),
704 hz in pos_half(),
705 m in 0.01_f64..1000.0_f64,
706 ) {
707 let b = BoxShape::new(Vec3::new(hx, hy, hz));
708 let it = b.inertia_tensor(m);
709 prop_assert!(it[(0, 0)] > 0.0, "Ixx not positive: {}", it[(0, 0)]);
710 prop_assert!(it[(1, 1)] > 0.0, "Iyy not positive: {}", it[(1, 1)]);
711 prop_assert!(it[(2, 2)] > 0.0, "Izz not positive: {}", it[(2, 2)]);
712 }
713
714 #[test]
715 fn prop_box_volume_scales_cubically(
716 hx in pos_half(),
717 hy in pos_half(),
718 hz in pos_half(),
719 ) {
720 let b1 = BoxShape::new(Vec3::new(hx, hy, hz));
721 let b2 = BoxShape::new(Vec3::new(2.0 * hx, 2.0 * hy, 2.0 * hz));
722 let ratio = b2.volume() / b1.volume();
723 prop_assert!(
724 (ratio - 8.0).abs() < 1e-6,
725 "box volume ratio for 2x scale should be 8, got {}",
726 ratio
727 );
728 }
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use crate::BoxShape;
736 use crate::box_shape::Vec3;
737 use crate::box_shape::obb_aabb_bounds;
738 use crate::box_shape::obb_closest_point;
739 use crate::box_shape::obb_edges;
740 use crate::box_shape::obb_face_centers;
741 use crate::box_shape::obb_inertia_tensor;
742 use crate::box_shape::obb_inertia_tensor_rotated;
743 use crate::box_shape::obb_plane_distance;
744 use crate::box_shape::obb_projection_extent;
745 use crate::box_shape::obb_ray_intersection;
746 use crate::box_shape::obb_support_fn;
747 use crate::box_shape::obb_vertices;
748
749 #[test]
750 fn test_box_support() {
751 let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
752 let sp = b.support_point(&Vec3::new(1.0, 1.0, 1.0));
753 assert_eq!(sp, Vec3::new(1.0, 2.0, 3.0));
754 }
755
756 #[test]
757 fn test_box_support_array() {
758 let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
759 let sp = b.support([1.0, 1.0, 1.0]);
760 assert!((sp[0] - 1.0).abs() < 1e-10);
761 assert!((sp[1] - 2.0).abs() < 1e-10);
762 assert!((sp[2] - 3.0).abs() < 1e-10);
763 }
764
765 #[test]
766 fn test_box_volume() {
767 let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
768 assert!((b.volume() - 48.0).abs() < 1e-10);
769 }
770
771 #[test]
772 fn test_box_surface_area() {
773 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
775 assert!((b.surface_area() - 24.0).abs() < 1e-10);
776 }
777
778 #[test]
779 fn test_box_inertia() {
780 let b = BoxShape::new(Vec3::new(0.5, 0.5, 0.5));
782 let it = b.inertia_tensor(1.0);
783 assert!((it[(0, 0)] - 1.0 / 6.0).abs() < 1e-10);
785 }
786
787 #[test]
788 fn test_box_inertia_array() {
789 let b = BoxShape::new(Vec3::new(0.5, 0.5, 0.5));
790 let it = b.inertia_tensor_array(1.0);
791 assert!((it[0][0] - 1.0 / 6.0).abs() < 1e-10);
792 assert!(it[0][1].abs() < 1e-10);
793 }
794
795 #[test]
796 fn test_box_raycast() {
797 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
798 let origin = Vec3::new(-5.0, 0.0, 0.0);
799 let dir = Vec3::new(1.0, 0.0, 0.0);
800 let hit = b.ray_cast(&origin, &dir, 100.0).unwrap();
801 assert!((hit.toi - 4.0).abs() < 1e-10);
802 assert!((hit.point.x + 1.0).abs() < 1e-10);
803 }
804
805 #[test]
806 fn test_box_raycast_array() {
807 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
808 let (t, n) = b
809 .ray_cast_array([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0)
810 .unwrap();
811 assert!((t - 4.0).abs() < 1e-10);
812 assert!((n[0] + 1.0).abs() < 1e-10);
813 }
814
815 #[test]
816 fn test_box_vertex_list() {
817 let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
818 let verts = b.vertex_list();
819 assert_eq!(verts.len(), 8);
820 for v in &verts {
822 assert!((v[0].abs() - 1.0).abs() < 1e-10);
823 assert!((v[1].abs() - 2.0).abs() < 1e-10);
824 assert!((v[2].abs() - 3.0).abs() < 1e-10);
825 }
826 }
827
828 #[test]
829 fn test_box_raycast_miss() {
830 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
831 let origin = Vec3::new(5.0, 5.0, 0.0);
833 let dir = Vec3::new(1.0, 0.0, 0.0);
834 assert!(b.ray_cast(&origin, &dir, 100.0).is_none());
835 }
836
837 #[test]
840 fn test_box_face_normals() {
841 let normals = BoxShape::face_normals();
842 assert_eq!(normals.len(), 6);
843 for n in &normals {
845 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
846 assert!((len - 1.0).abs() < 1e-10);
847 }
848 }
849
850 #[test]
851 fn test_box_edge_list() {
852 let edges = BoxShape::edge_list();
853 assert_eq!(edges.len(), 12);
854 for (a, b) in &edges {
856 assert!(*a < 8);
857 assert!(*b < 8);
858 }
859 }
860
861 #[test]
862 fn test_box_face_vertex_indices() {
863 let faces = BoxShape::face_vertex_indices();
864 assert_eq!(faces.len(), 6);
865 for face in &faces {
866 for &idx in face {
867 assert!(idx < 8);
868 }
869 }
870 }
871
872 #[test]
873 fn test_box_face_areas() {
874 let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
875 let areas = b.face_areas();
876 assert!((areas[0] - 24.0).abs() < 1e-10);
878 assert!((areas[1] - 24.0).abs() < 1e-10);
879 assert!((areas[2] - 12.0).abs() < 1e-10);
881 assert!((areas[3] - 12.0).abs() < 1e-10);
882 assert!((areas[4] - 8.0).abs() < 1e-10);
884 assert!((areas[5] - 8.0).abs() < 1e-10);
885 }
886
887 #[test]
888 fn test_box_face_areas_sum_equals_surface_area() {
889 let b = BoxShape::new(Vec3::new(1.5, 2.5, 3.5));
890 let areas = b.face_areas();
891 let sum: f64 = areas.iter().sum();
892 assert!((sum - b.surface_area()).abs() < 1e-10);
893 }
894
895 #[test]
896 fn test_box_closest_point_inside() {
897 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
898 let cp = b.closest_point([0.5, 0.5, 0.5]);
899 assert_eq!(cp, [0.5, 0.5, 0.5]); }
901
902 #[test]
903 fn test_box_closest_point_outside() {
904 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
905 let cp = b.closest_point([3.0, 0.0, 0.0]);
906 assert!((cp[0] - 1.0).abs() < 1e-10);
907 assert!(cp[1].abs() < 1e-10);
908 assert!(cp[2].abs() < 1e-10);
909 }
910
911 #[test]
912 fn test_box_closest_point_corner() {
913 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
914 let cp = b.closest_point([3.0, 3.0, 3.0]);
915 assert!((cp[0] - 1.0).abs() < 1e-10);
916 assert!((cp[1] - 1.0).abs() < 1e-10);
917 assert!((cp[2] - 1.0).abs() < 1e-10);
918 }
919
920 #[test]
921 fn test_box_contains_point() {
922 let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
923 assert!(b.contains_point([0.0, 0.0, 0.0]));
924 assert!(b.contains_point([1.0, 2.0, 3.0])); assert!(!b.contains_point([1.1, 0.0, 0.0]));
926 }
927
928 #[test]
929 fn test_box_signed_distance_inside() {
930 let b = BoxShape::new(Vec3::new(2.0, 2.0, 2.0));
931 let d = b.signed_distance([0.0, 0.0, 0.0]);
932 assert!((d + 2.0).abs() < 1e-10); }
934
935 #[test]
936 fn test_box_signed_distance_outside() {
937 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
938 let d = b.signed_distance([3.0, 0.0, 0.0]);
939 assert!((d - 2.0).abs() < 1e-10);
940 }
941
942 #[test]
943 fn test_box_signed_distance_corner() {
944 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
945 let d = b.signed_distance([2.0, 2.0, 2.0]);
946 assert!((d - 3.0_f64.sqrt()).abs() < 1e-10);
948 }
949
950 #[test]
951 fn test_box_clip_segment_through() {
952 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
953 let result = b.clip_segment([-3.0, 0.0, 0.0], [3.0, 0.0, 0.0]);
954 assert!(result.is_some());
955 let (t_enter, t_exit) = result.unwrap();
956 assert!((t_enter - 1.0 / 3.0).abs() < 1e-10);
960 assert!((t_exit - 2.0 / 3.0).abs() < 1e-10);
961 }
962
963 #[test]
964 fn test_box_clip_segment_miss() {
965 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
966 let result = b.clip_segment([5.0, 5.0, 0.0], [6.0, 5.0, 0.0]);
967 assert!(result.is_none());
968 }
969
970 #[test]
971 fn test_box_clip_segment_inside() {
972 let b = BoxShape::new(Vec3::new(2.0, 2.0, 2.0));
973 let result = b.clip_segment([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
974 assert!(result.is_some());
975 let (t_enter, t_exit) = result.unwrap();
976 assert!(t_enter.abs() < 1e-10);
977 assert!((t_exit - 1.0).abs() < 1e-10);
978 }
979
980 #[test]
981 fn test_box_classify_face() {
982 let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
983 assert_eq!(b.classify_face([1.0, 0.0, 0.0]), 0); assert_eq!(b.classify_face([-1.0, 0.0, 0.0]), 1); assert_eq!(b.classify_face([0.0, 2.0, 0.0]), 2); assert_eq!(b.classify_face([0.0, -2.0, 0.0]), 3); assert_eq!(b.classify_face([0.0, 0.0, 3.0]), 4); assert_eq!(b.classify_face([0.0, 0.0, -3.0]), 5); }
990
991 #[test]
992 fn test_box_diagonal_length() {
993 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
994 let expected = 2.0 * 3.0_f64.sqrt();
995 assert!((b.diagonal_length() - expected).abs() < 1e-10);
996 }
997
998 #[test]
999 fn test_box_edge_lengths() {
1000 let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
1001 let el = b.edge_lengths();
1002 assert!((el[0] - 2.0).abs() < 1e-10);
1003 assert!((el[1] - 4.0).abs() < 1e-10);
1004 assert!((el[2] - 6.0).abs() < 1e-10);
1005 }
1006
1007 #[test]
1008 fn test_box_project_on_axis() {
1009 let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
1010 let (lo, hi) = b.project_on_axis([1.0, 0.0, 0.0]);
1012 assert!((lo + 1.0).abs() < 1e-10);
1013 assert!((hi - 1.0).abs() < 1e-10);
1014 }
1015
1016 #[test]
1017 fn test_box_project_on_diagonal_axis() {
1018 let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
1019 let s = 1.0 / 3.0_f64.sqrt();
1021 let (lo, hi) = b.project_on_axis([s, s, s]);
1022 let expected = 3.0_f64.sqrt();
1024 assert!((hi - expected).abs() < 1e-10);
1025 assert!((lo + expected).abs() < 1e-10);
1026 }
1027
1028 #[test]
1029 fn test_box_volume_explicit_matches() {
1030 let b = BoxShape::new(Vec3::new(1.5, 2.5, 3.5));
1031 assert!((b.volume_explicit() - b.volume()).abs() < 1e-10);
1032 }
1033
1034 #[test]
1037 fn test_obb_inertia_tensor_unit_cube() {
1038 let it = obb_inertia_tensor([0.5, 0.5, 0.5], 1.0);
1041 assert!((it[0][0] - 1.0 / 6.0).abs() < 1e-10);
1042 assert!((it[1][1] - 1.0 / 6.0).abs() < 1e-10);
1043 assert!((it[2][2] - 1.0 / 6.0).abs() < 1e-10);
1044 assert!(it[0][1].abs() < 1e-10);
1046 assert!(it[0][2].abs() < 1e-10);
1047 }
1048
1049 #[test]
1050 fn test_obb_inertia_tensor_asymmetric() {
1051 let it = obb_inertia_tensor([1.0, 2.0, 3.0], 12.0);
1054 assert!((it[0][0] - 52.0).abs() < 1e-10);
1055 assert!((it[1][1] - 40.0).abs() < 1e-10);
1056 assert!((it[2][2] - 20.0).abs() < 1e-10);
1057 }
1058
1059 #[test]
1060 fn test_obb_inertia_tensor_rotated_identity() {
1061 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1063 let it = obb_inertia_tensor_rotated([1.0, 2.0, 3.0], 12.0, rot);
1064 let ref_it = obb_inertia_tensor([1.0, 2.0, 3.0], 12.0);
1065 for i in 0..3 {
1066 for j in 0..3 {
1067 assert!(
1068 (it[i][j] - ref_it[i][j]).abs() < 1e-10,
1069 "mismatch at [{i}][{j}]: {} vs {}",
1070 it[i][j],
1071 ref_it[i][j]
1072 );
1073 }
1074 }
1075 }
1076
1077 #[test]
1078 fn test_obb_closest_point_inside() {
1079 let center = [0.0, 0.0, 0.0];
1080 let half = [1.0, 1.0, 1.0];
1081 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1082 let cp = obb_closest_point([0.5, 0.3, -0.2], center, half, rot);
1083 assert!((cp[0] - 0.5).abs() < 1e-10);
1085 assert!((cp[1] - 0.3).abs() < 1e-10);
1086 assert!((cp[2] + 0.2).abs() < 1e-10);
1087 }
1088
1089 #[test]
1090 fn test_obb_closest_point_outside_x() {
1091 let center = [0.0, 0.0, 0.0];
1092 let half = [1.0, 1.0, 1.0];
1093 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1094 let cp = obb_closest_point([5.0, 0.0, 0.0], center, half, rot);
1095 assert!((cp[0] - 1.0).abs() < 1e-10);
1096 assert!(cp[1].abs() < 1e-10);
1097 assert!(cp[2].abs() < 1e-10);
1098 }
1099
1100 #[test]
1101 fn test_obb_closest_point_rotated() {
1102 let center = [0.0, 0.0, 0.0];
1104 let half = [2.0, 1.0, 1.0];
1105 let rot = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
1107 let cp = obb_closest_point([0.0, 5.0, 0.0], center, half, rot);
1109 assert!((cp[0]).abs() < 1e-10);
1112 assert!((cp[1] - 2.0).abs() < 1e-10);
1113 }
1114
1115 #[test]
1116 fn test_obb_ray_intersection_hit() {
1117 let center = [0.0, 0.0, 0.0];
1118 let half = [1.0, 1.0, 1.0];
1119 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1120 let result =
1121 obb_ray_intersection([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], center, half, rot, 100.0);
1122 assert!(result.is_some());
1123 let t = result.unwrap();
1124 assert!((t - 4.0).abs() < 1e-10);
1125 }
1126
1127 #[test]
1128 fn test_obb_ray_intersection_miss() {
1129 let center = [0.0, 0.0, 0.0];
1130 let half = [1.0, 1.0, 1.0];
1131 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1132 let result =
1133 obb_ray_intersection([0.0, 5.0, 0.0], [1.0, 0.0, 0.0], center, half, rot, 100.0);
1134 assert!(result.is_none());
1135 }
1136
1137 #[test]
1138 fn test_obb_ray_intersection_rotated() {
1139 let center = [3.0, 0.0, 0.0];
1142 let half = [1.0, 1.0, 1.0];
1143 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1144 let result =
1145 obb_ray_intersection([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], center, half, rot, 100.0);
1146 assert!(result.is_some());
1147 let t = result.unwrap();
1148 assert!((t - 2.0).abs() < 1e-10);
1149 }
1150
1151 #[test]
1152 fn test_obb_plane_distance_above() {
1153 let center = [0.0, 0.0, 0.0];
1156 let half = [1.0, 1.0, 1.0];
1157 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1158 let plane = [0.0, 1.0, 0.0, -3.0]; let (min_d, max_d) = obb_plane_distance(center, half, rot, plane);
1160 assert!((min_d + 4.0).abs() < 1e-10, "min_d={min_d}");
1164 assert!((max_d + 2.0).abs() < 1e-10, "max_d={max_d}");
1165 }
1166
1167 #[test]
1168 fn test_obb_plane_distance_straddles() {
1169 let center = [0.0, 0.0, 0.0];
1171 let half = [2.0, 2.0, 2.0];
1172 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1173 let plane = [0.0, 1.0, 0.0, 0.0]; let (min_d, max_d) = obb_plane_distance(center, half, rot, plane);
1175 assert!(min_d < 0.0);
1176 assert!(max_d > 0.0);
1177 }
1178
1179 #[test]
1180 fn test_obb_vertices_count() {
1181 let center = [1.0, 2.0, 3.0];
1182 let half = [1.0, 1.0, 1.0];
1183 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1184 let verts = obb_vertices(center, half, rot);
1185 assert_eq!(verts.len(), 8);
1186 }
1187
1188 #[test]
1189 fn test_obb_vertices_aabb_bounds() {
1190 let center = [0.0, 0.0, 0.0];
1192 let half = [1.0, 2.0, 3.0];
1193 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1194 let verts = obb_vertices(center, half, rot);
1195 for v in &verts {
1196 assert!(v[0].abs() <= 1.0 + 1e-10);
1197 assert!(v[1].abs() <= 2.0 + 1e-10);
1198 assert!(v[2].abs() <= 3.0 + 1e-10);
1199 }
1200 }
1201
1202 #[test]
1203 fn test_obb_vertices_translated() {
1204 let center = [5.0, 0.0, 0.0];
1205 let half = [1.0, 1.0, 1.0];
1206 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1207 let verts = obb_vertices(center, half, rot);
1208 for v in &verts {
1210 assert!(
1211 (v[0] - 4.0).abs() < 1e-10 || (v[0] - 6.0).abs() < 1e-10,
1212 "unexpected x={}",
1213 v[0]
1214 );
1215 }
1216 }
1217
1218 #[test]
1219 fn test_obb_edges_count() {
1220 let center = [0.0, 0.0, 0.0];
1221 let half = [1.0, 1.0, 1.0];
1222 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1223 let edges = obb_edges(center, half, rot);
1224 assert_eq!(edges.len(), 12);
1225 }
1226
1227 #[test]
1228 fn test_obb_edges_lengths() {
1229 let center = [0.0, 0.0, 0.0];
1230 let half = [1.0, 2.0, 3.0];
1231 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1232 let edges = obb_edges(center, half, rot);
1233 for (a, b) in &edges {
1234 let dx = b[0] - a[0];
1235 let dy = b[1] - a[1];
1236 let dz = b[2] - a[2];
1237 let len = (dx * dx + dy * dy + dz * dz).sqrt();
1238 let is_valid =
1240 (len - 2.0).abs() < 1e-10 || (len - 4.0).abs() < 1e-10 || (len - 6.0).abs() < 1e-10;
1241 assert!(is_valid, "unexpected edge length {len}");
1242 }
1243 }
1244
1245 #[test]
1246 fn test_obb_face_centers_count() {
1247 let center = [0.0, 0.0, 0.0];
1248 let half = [1.0, 1.0, 1.0];
1249 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1250 let fc = obb_face_centers(center, half, rot);
1251 assert_eq!(fc.len(), 6);
1252 }
1253
1254 #[test]
1255 fn test_obb_face_centers_distance() {
1256 let center = [0.0, 0.0, 0.0];
1258 let half = [1.0, 2.0, 3.0];
1259 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1260 let fc = obb_face_centers(center, half, rot);
1261 assert!((fc[0][0].abs() - 1.0).abs() < 1e-10);
1263 assert!((fc[1][0].abs() - 1.0).abs() < 1e-10);
1264 assert!((fc[2][1].abs() - 2.0).abs() < 1e-10);
1266 assert!((fc[3][1].abs() - 2.0).abs() < 1e-10);
1267 assert!((fc[4][2].abs() - 3.0).abs() < 1e-10);
1269 assert!((fc[5][2].abs() - 3.0).abs() < 1e-10);
1270 }
1271
1272 #[test]
1273 fn test_obb_support_function_identity() {
1274 let center = [0.0, 0.0, 0.0];
1275 let half = [1.0, 2.0, 3.0];
1276 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1277 let sp = obb_support_fn(center, half, rot, [1.0, 0.0, 0.0]);
1279 assert!((sp[0] - 1.0).abs() < 1e-10);
1280 }
1281
1282 #[test]
1283 fn test_obb_support_function_diagonal() {
1284 let center = [0.0, 0.0, 0.0];
1285 let half = [1.0, 1.0, 1.0];
1286 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1287 let sp = obb_support_fn(center, half, rot, [1.0, 1.0, 1.0]);
1288 assert!((sp[0] - 1.0).abs() < 1e-10);
1290 assert!((sp[1] - 1.0).abs() < 1e-10);
1291 assert!((sp[2] - 1.0).abs() < 1e-10);
1292 }
1293
1294 #[test]
1295 fn test_obb_support_function_translated() {
1296 let center = [3.0, 0.0, 0.0];
1297 let half = [1.0, 1.0, 1.0];
1298 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1299 let sp = obb_support_fn(center, half, rot, [1.0, 0.0, 0.0]);
1300 assert!((sp[0] - 4.0).abs() < 1e-10);
1302 }
1303
1304 #[test]
1305 fn test_obb_projection_extent_identity() {
1306 let half = [1.0, 2.0, 3.0];
1308 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1309 let axis = [1.0, 0.0, 0.0];
1310 let ext = obb_projection_extent(half, rot, axis);
1311 assert!((ext - 1.0).abs() < 1e-10);
1312 }
1313
1314 #[test]
1315 fn test_obb_projection_extent_diagonal() {
1316 let half = [1.0, 1.0, 1.0];
1317 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1318 let s = 1.0 / 3.0_f64.sqrt();
1319 let ext = obb_projection_extent(half, rot, [s, s, s]);
1320 assert!((ext - 3.0_f64.sqrt()).abs() < 1e-10);
1321 }
1322
1323 #[test]
1324 fn test_obb_aabb_bounds_identity() {
1325 let center = [1.0, 2.0, 3.0];
1326 let half = [1.0, 2.0, 3.0];
1327 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1328 let (lo, hi) = obb_aabb_bounds(center, half, rot);
1329 assert!((lo[0] - 0.0).abs() < 1e-10);
1330 assert!((hi[0] - 2.0).abs() < 1e-10);
1331 assert!((lo[1] - 0.0).abs() < 1e-10);
1332 assert!((hi[1] - 4.0).abs() < 1e-10);
1333 }
1334
1335 #[test]
1336 fn test_obb_aabb_bounds_contains_all_vertices() {
1337 let center = [0.5, -1.0, 2.0];
1338 let half = [1.0, 0.5, 1.5];
1339 let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1340 let (lo, hi) = obb_aabb_bounds(center, half, rot);
1341 let verts = obb_vertices(center, half, rot);
1342 for v in &verts {
1343 assert!(v[0] >= lo[0] - 1e-10 && v[0] <= hi[0] + 1e-10);
1344 assert!(v[1] >= lo[1] - 1e-10 && v[1] <= hi[1] + 1e-10);
1345 assert!(v[2] >= lo[2] - 1e-10 && v[2] <= hi[2] + 1e-10);
1346 }
1347 }
1348}