Skip to main content

oxiphysics_geometry/
box_shape.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Axis-aligned box shape.
5
6use crate::shape::{RayHit, Shape};
7use oxiphysics_core::Aabb;
8use oxiphysics_core::math::{Mat3, Real, Vec3};
9
10/// An axis-aligned box defined by half-extents.
11#[derive(Debug, Clone)]
12pub struct BoxShape {
13    /// Half-extents along each axis.
14    pub half_extents: Vec3,
15}
16
17impl BoxShape {
18    /// Create a new box with the given half-extents.
19    pub fn new(half_extents: Vec3) -> Self {
20        Self { half_extents }
21    }
22
23    /// Volume: (2hx)(2hy)(2hz).
24    pub fn volume_explicit(&self) -> Real {
25        8.0 * self.half_extents.x * self.half_extents.y * self.half_extents.z
26    }
27
28    /// Surface area: 2*(2hx*2hy + 2hy*2hz + 2hx*2hz).
29    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    /// Inertia tensor as \[\[f64;3\\];3] row-major.
40    /// Diagonal: m/12*(b²+c², a²+c², a²+b²) where a,b,c are full side lengths.
41    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    /// Ray cast returning (t, normal) as plain arrays (slab method).
57    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    /// GJK support function: farthest point in `direction` (componentwise sign).
70    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    /// All 8 vertices of the box.
79    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    // ── New methods ──
96
97    /// Returns the 6 face normals of the box (axis-aligned, outward).
98    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    /// Returns the 12 edges of the box as pairs of vertex indices into `vertex_list()`.
110    pub fn edge_list() -> [(usize, usize); 12] {
111        [
112            // Bottom face (y = -hy)
113            (0, 1),
114            (1, 5),
115            (5, 4),
116            (4, 0),
117            // Top face (y = +hy)
118            (2, 3),
119            (3, 7),
120            (7, 6),
121            (6, 2),
122            // Vertical edges
123            (0, 3),
124            (1, 2),
125            (5, 6),
126            (4, 7),
127        ]
128    }
129
130    /// Returns the 6 faces as groups of 4 vertex indices (into `vertex_list()`).
131    /// Each face's vertices are in counter-clockwise order from outside.
132    pub fn face_vertex_indices() -> [[usize; 4]; 6] {
133        [
134            [1, 2, 6, 5], // +X face
135            [0, 4, 7, 3], // -X face
136            [2, 3, 7, 6], // +Y face
137            [0, 1, 5, 4], // -Y face
138            [4, 5, 6, 7], // +Z face
139            [0, 3, 2, 1], // -Z face
140        ]
141    }
142
143    /// Area of each face: returns \[+X, -X, +Y, -Y, +Z, -Z\].
144    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; // +X and -X faces
149        let xz = 4.0 * hx * hz; // +Y and -Y faces
150        let xy = 4.0 * hx * hy; // +Z and -Z faces
151        [yz, yz, xz, xz, xy, xy]
152    }
153
154    /// Closest point on (or inside) the box to point `p`.
155    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    /// Returns true if `p` is inside (or on the surface of) the box.
164    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    /// Signed distance from a point to the box surface.
171    /// Negative if inside, positive if outside.
172    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            // Inside: return the largest (least negative) component
179            dx.max(dy).max(dz)
180        } else {
181            // Outside: Euclidean distance from the clamped point
182            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    /// Clip a line segment (from `a` to `b`) against this box.
190    /// Returns `Some((t_enter, t_exit))` where 0 <= t_enter <= t_exit <= 1,
191    /// or `None` if the segment doesn't intersect the box.
192    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    /// Determine which face a surface point is on.
227    /// Returns the face index (0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z).
228    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    /// Diagonal length of the box: 2 * sqrt(hx² + hy² + hz²).
249    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    /// Edge lengths: \[2*hx, 2*hy, 2*hz\].
257    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    /// Project the box onto an axis and return `(min, max)` interval.
266    pub fn project_on_axis(&self, axis: [f64; 3]) -> (f64, f64) {
267        // For an AABB, the projection extent is the sum of |axis_i * half_extent_i|
268        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        // Full extents squared
301        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        // Slab method for AABB ray intersection
320        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
373// ── OBB (Oriented Bounding Box) free functions ──
374// These work with rotation matrices (row-major [[f64;3];3]) and plain [f64;3] arrays,
375// so they can be used without nalgebra.
376
377/// Dot product of two \[f64;3\] vectors.
378fn dot3b(a: [f64; 3], b: [f64; 3]) -> f64 {
379    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
380}
381
382/// Matrix-vector multiply: rot (row-major) * v.
383fn 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
387/// Transpose of a 3×3 matrix.
388fn 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
396/// 3×3 matrix multiply A*B.
397fn 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
406/// Inertia tensor of an OBB in its own local frame (diagonal matrix).
407/// `half_extents`: half-widths along each local axis.
408/// Returns a 3×3 row-major array.
409pub 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
421/// Inertia tensor of an OBB transformed to world frame via rotation matrix `rot`.
422/// `rot` maps local axes to world axes (each row is a world-space basis vector).
423/// Uses I_world = R * I_local * R^T.
424pub 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    // I_world = rot * i_local * rot^T
431    let ri = mat3_mul(rot, i_local);
432    mat3_mul(ri, mat3_transpose(rot))
433}
434
435/// Closest point on the surface of an OBB to a world-space point `p`.
436/// `center`: OBB center in world space.
437/// `half_extents`: half-widths along each local axis.
438/// `rot`: rotation matrix (row i = world direction of local axis i).
439/// Returns world-space closest point (may be on interior if `p` is inside).
440pub 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    // Transform p to local space: local = rot * (p - center)
447    let d = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
448    let local = mat3_mul_vec(rot, d);
449
450    // Clamp each component to half-extents
451    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    // Transform back to world space: world = center + rot^T * clamped
458    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
467/// Compute the projection half-extent of an OBB onto a world-space axis.
468/// This is the sum of |half_i * dot(rot_col_i, axis)| over i.
469pub fn obb_projection_extent(half_extents: [f64; 3], rot: [[f64; 3]; 3], axis: [f64; 3]) -> f64 {
470    // rot row i = world direction of local axis i
471    // dot(rot row i, axis) = projection of local axis i onto axis
472    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
479/// Ray-OBB intersection using the slab method in local OBB space.
480/// `ray_origin`, `ray_dir`: world-space ray.
481/// `center`, `half_extents`, `rot`: OBB parameters.
482/// Returns the hit parameter `t` if intersection within `[0, max_toi]`, else `None`.
483pub 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    // Transform ray to OBB local space
492    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
533/// Signed distance from the OBB to a plane (ax + by + cz + d = 0).
534/// Returns `(min_signed_dist, max_signed_dist)` for the OBB interval projected on the plane normal.
535/// If `min < 0 && max > 0`, the OBB straddles the plane.
536pub 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    // Project OBB center onto plane normal
546    let center_dist = dot3b(center, normal) + plane_d;
547
548    // Compute projection half-extent
549    let extent = obb_projection_extent(half_extents, rot, normal);
550
551    (center_dist - extent, center_dist + extent)
552}
553
554/// Returns all 8 vertices of the OBB in world space.
555pub 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
585/// Returns all 12 edges of the OBB in world space as (start, end) pairs.
586pub 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    // Edge pairs using same winding as BoxShape::edge_list()
593    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
610/// Returns the 6 face centers of the OBB in world space.
611/// Order: +local_x, -local_x, +local_y, -local_y, +local_z, -local_z.
612pub 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
634/// GJK support function for an OBB: returns the farthest vertex in `direction`.
635pub 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    // Project direction onto local axes to get signs
642    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
657/// Compute the AABB bounds (lo, hi) of an OBB in world space.
658pub 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        // Each local axis contributes: |rot_col_axis| * half_extents[axis]
668        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        // Half-extents (1,1,1): full box 2x2x2, SA = 6*4 = 24
774        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        // Unit cube (half_extents = 0.5), mass = 1
781        let b = BoxShape::new(Vec3::new(0.5, 0.5, 0.5));
782        let it = b.inertia_tensor(1.0);
783        // I = m/12 * (h^2 + d^2) = 1/12 * (1+1) = 1/6
784        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        // All vertices should have |x|=1, |y|=2, |z|=3
821        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        // Ray parallel to face and outside
832        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    // ── New tests ──
838
839    #[test]
840    fn test_box_face_normals() {
841        let normals = BoxShape::face_normals();
842        assert_eq!(normals.len(), 6);
843        // Each normal should have unit length
844        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        // All indices should be < 8
855        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        // +X/-X faces: 4*2*3 = 24
877        assert!((areas[0] - 24.0).abs() < 1e-10);
878        assert!((areas[1] - 24.0).abs() < 1e-10);
879        // +Y/-Y faces: 4*1*3 = 12
880        assert!((areas[2] - 12.0).abs() < 1e-10);
881        assert!((areas[3] - 12.0).abs() < 1e-10);
882        // +Z/-Z faces: 4*1*2 = 8
883        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]); // inside, returns same point
900    }
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])); // on surface
925        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); // center, min dist to face = 2
933    }
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        // Distance to corner (1,1,1) = sqrt(3)
947        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        // Segment goes from x=-3 to x=3, box is [-1,1]
957        // t_enter = (1+(-3))/(3-(-3)) = (-3+1)/(6)... let's compute:
958        // x = -3 + t*6, hit at x=-1: t=(2)/6=1/3, x=1: t=4/6=2/3
959        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); // +X
984        assert_eq!(b.classify_face([-1.0, 0.0, 0.0]), 1); // -X
985        assert_eq!(b.classify_face([0.0, 2.0, 0.0]), 2); // +Y
986        assert_eq!(b.classify_face([0.0, -2.0, 0.0]), 3); // -Y
987        assert_eq!(b.classify_face([0.0, 0.0, 3.0]), 4); // +Z
988        assert_eq!(b.classify_face([0.0, 0.0, -3.0]), 5); // -Z
989    }
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        // Project onto X axis
1011        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        // Project onto (1,1,1)/sqrt(3) axis
1020        let s = 1.0 / 3.0_f64.sqrt();
1021        let (lo, hi) = b.project_on_axis([s, s, s]);
1022        // Extent = hx*|s| + hy*|s| + hz*|s| = 3*s = sqrt(3)
1023        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    // ── OBB free-function tests ──
1035
1036    #[test]
1037    fn test_obb_inertia_tensor_unit_cube() {
1038        // Unit cube: half_extents (0.5,0.5,0.5), mass=1
1039        // I_xx = m/12*(b²+c²) = 1/12*(1+1) = 1/6
1040        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        // Off-diagonal must be zero
1045        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        // half_extents (1,2,3), mass=12
1052        // a=2,b=4,c=6 → Ixx=12/12*(16+36)=52, Iyy=12/12*(4+36)=40, Izz=12/12*(4+16)=20
1053        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        // With identity rotation, should match the axis-aligned formula
1062        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        // Point is inside; closest = the point itself
1084        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        // OBB rotated 90° around Z: x-axis becomes y-axis in world
1103        let center = [0.0, 0.0, 0.0];
1104        let half = [2.0, 1.0, 1.0];
1105        // Rotation: x-col=[0,1,0], y-col=[-1,0,0], z-col=[0,0,1]
1106        let rot = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
1107        // A point far in world-y should clamp to OBB local-x extent (2)
1108        let cp = obb_closest_point([0.0, 5.0, 0.0], center, half, rot);
1109        // local coords: p_local_x = dot([5y], [0,1,0]) = 5, clamped to 2
1110        // world closest = center + rot * [2, 0, 0] = [0, 2, 0]
1111        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        // OBB at (3,0,0) with identity rotation, half-extents (1,1,1)
1140        // Ray from origin along +x should hit at t=2
1141        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        // OBB at origin, half (1,1,1), plane normal (0,1,0) at height 3
1154        // Min signed dist from plane: 3 - half_projection = 3 - 1 = 2
1155        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]; // dot(n,p) + d = 0 → y = 3
1159        let (min_d, max_d) = obb_plane_distance(center, half, rot, plane);
1160        // center projected: 0*1+0*1+0*0-3 = -3 (below plane, so negative)
1161        // projection extent: 1
1162        // min = -3 - 1 = -4, max = -3 + 1 = -2
1163        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        // OBB at (0,0,0) half (2,2,2), plane y=0 → min<0, max>0
1170        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]; // y plane through origin
1174        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        // Identity rotation OBB: vertices should all be within AABB
1191        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        // All x-coords should be 4 or 6
1209        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            // Each edge should be 2, 4, or 6
1239            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        // For identity OBB with half (1,2,3), face centers should be at ±(1,2,3)
1257        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        // Pair 0&1 along x: should be (±1, 0, 0)
1262        assert!((fc[0][0].abs() - 1.0).abs() < 1e-10);
1263        assert!((fc[1][0].abs() - 1.0).abs() < 1e-10);
1264        // Pair 2&3 along y: should be (0, ±2, 0)
1265        assert!((fc[2][1].abs() - 2.0).abs() < 1e-10);
1266        assert!((fc[3][1].abs() - 2.0).abs() < 1e-10);
1267        // Pair 4&5 along z: should be (0, 0, ±3)
1268        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        // Support in +x direction should be (1, ±2, ±3), pick most +x = (1, ?, ?)
1278        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        // Farthest in (1,1,1) direction for unit cube = (1,1,1)
1289        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        // Center at x=3, half=1, so farthest in +x is at x=4
1301        assert!((sp[0] - 4.0).abs() < 1e-10);
1302    }
1303
1304    #[test]
1305    fn test_obb_projection_extent_identity() {
1306        // For identity OBB, projection extent = sum of |half_i * axis_i|
1307        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}