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    #[allow(dead_code)]
25    pub fn volume_explicit(&self) -> Real {
26        8.0 * self.half_extents.x * self.half_extents.y * self.half_extents.z
27    }
28
29    /// Surface area: 2*(2hx*2hy + 2hy*2hz + 2hx*2hz).
30    #[allow(dead_code)]
31    pub fn surface_area(&self) -> Real {
32        let hx = self.half_extents.x;
33        let hy = self.half_extents.y;
34        let hz = self.half_extents.z;
35        let ax = 2.0 * hx;
36        let ay = 2.0 * hy;
37        let az = 2.0 * hz;
38        2.0 * (ax * ay + ay * az + ax * az)
39    }
40
41    /// Inertia tensor as \[\[f64;3\\];3] row-major.
42    /// Diagonal: m/12*(b²+c², a²+c², a²+b²) where a,b,c are full side lengths.
43    #[allow(dead_code)]
44    pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
45        let hx = self.half_extents.x;
46        let hy = self.half_extents.y;
47        let hz = self.half_extents.z;
48        let ax2 = (2.0 * hx).powi(2);
49        let ay2 = (2.0 * hy).powi(2);
50        let az2 = (2.0 * hz).powi(2);
51        let k = mass / 12.0;
52        [
53            [k * (ay2 + az2), 0.0, 0.0],
54            [0.0, k * (ax2 + az2), 0.0],
55            [0.0, 0.0, k * (ax2 + ay2)],
56        ]
57    }
58
59    /// Ray cast returning (t, normal) as plain arrays (slab method).
60    #[allow(dead_code)]
61    pub fn ray_cast_array(
62        &self,
63        origin: [f64; 3],
64        direction: [f64; 3],
65        max_toi: f64,
66    ) -> Option<(f64, [f64; 3])> {
67        let o = Vec3::new(origin[0], origin[1], origin[2]);
68        let d = Vec3::new(direction[0], direction[1], direction[2]);
69        let hit = self.ray_cast(&o, &d, max_toi)?;
70        Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
71    }
72
73    /// GJK support function: farthest point in `direction` (componentwise sign).
74    #[allow(dead_code)]
75    pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
76        [
77            self.half_extents.x.copysign(direction[0]),
78            self.half_extents.y.copysign(direction[1]),
79            self.half_extents.z.copysign(direction[2]),
80        ]
81    }
82
83    /// All 8 vertices of the box.
84    #[allow(dead_code)]
85    pub fn vertex_list(&self) -> [[f64; 3]; 8] {
86        let hx = self.half_extents.x;
87        let hy = self.half_extents.y;
88        let hz = self.half_extents.z;
89        [
90            [-hx, -hy, -hz],
91            [hx, -hy, -hz],
92            [hx, hy, -hz],
93            [-hx, hy, -hz],
94            [-hx, -hy, hz],
95            [hx, -hy, hz],
96            [hx, hy, hz],
97            [-hx, hy, hz],
98        ]
99    }
100
101    // ── New methods ──
102
103    /// Returns the 6 face normals of the box (axis-aligned, outward).
104    #[allow(dead_code)]
105    pub fn face_normals() -> [[f64; 3]; 6] {
106        [
107            [1.0, 0.0, 0.0],
108            [-1.0, 0.0, 0.0],
109            [0.0, 1.0, 0.0],
110            [0.0, -1.0, 0.0],
111            [0.0, 0.0, 1.0],
112            [0.0, 0.0, -1.0],
113        ]
114    }
115
116    /// Returns the 12 edges of the box as pairs of vertex indices into `vertex_list()`.
117    #[allow(dead_code)]
118    pub fn edge_list() -> [(usize, usize); 12] {
119        [
120            // Bottom face (y = -hy)
121            (0, 1),
122            (1, 5),
123            (5, 4),
124            (4, 0),
125            // Top face (y = +hy)
126            (2, 3),
127            (3, 7),
128            (7, 6),
129            (6, 2),
130            // Vertical edges
131            (0, 3),
132            (1, 2),
133            (5, 6),
134            (4, 7),
135        ]
136    }
137
138    /// Returns the 6 faces as groups of 4 vertex indices (into `vertex_list()`).
139    /// Each face's vertices are in counter-clockwise order from outside.
140    #[allow(dead_code)]
141    pub fn face_vertex_indices() -> [[usize; 4]; 6] {
142        [
143            [1, 2, 6, 5], // +X face
144            [0, 4, 7, 3], // -X face
145            [2, 3, 7, 6], // +Y face
146            [0, 1, 5, 4], // -Y face
147            [4, 5, 6, 7], // +Z face
148            [0, 3, 2, 1], // -Z face
149        ]
150    }
151
152    /// Area of each face: returns \[+X, -X, +Y, -Y, +Z, -Z\].
153    #[allow(dead_code)]
154    pub fn face_areas(&self) -> [f64; 6] {
155        let hx = self.half_extents.x;
156        let hy = self.half_extents.y;
157        let hz = self.half_extents.z;
158        let yz = 4.0 * hy * hz; // +X and -X faces
159        let xz = 4.0 * hx * hz; // +Y and -Y faces
160        let xy = 4.0 * hx * hy; // +Z and -Z faces
161        [yz, yz, xz, xz, xy, xy]
162    }
163
164    /// Closest point on (or inside) the box to point `p`.
165    #[allow(dead_code)]
166    pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
167        [
168            p[0].clamp(-self.half_extents.x, self.half_extents.x),
169            p[1].clamp(-self.half_extents.y, self.half_extents.y),
170            p[2].clamp(-self.half_extents.z, self.half_extents.z),
171        ]
172    }
173
174    /// Returns true if `p` is inside (or on the surface of) the box.
175    #[allow(dead_code)]
176    pub fn contains_point(&self, p: [f64; 3]) -> bool {
177        p[0].abs() <= self.half_extents.x
178            && p[1].abs() <= self.half_extents.y
179            && p[2].abs() <= self.half_extents.z
180    }
181
182    /// Signed distance from a point to the box surface.
183    /// Negative if inside, positive if outside.
184    #[allow(dead_code)]
185    pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
186        let dx = p[0].abs() - self.half_extents.x;
187        let dy = p[1].abs() - self.half_extents.y;
188        let dz = p[2].abs() - self.half_extents.z;
189
190        if dx <= 0.0 && dy <= 0.0 && dz <= 0.0 {
191            // Inside: return the largest (least negative) component
192            dx.max(dy).max(dz)
193        } else {
194            // Outside: Euclidean distance from the clamped point
195            let cx = dx.max(0.0);
196            let cy = dy.max(0.0);
197            let cz = dz.max(0.0);
198            (cx * cx + cy * cy + cz * cz).sqrt()
199        }
200    }
201
202    /// Clip a line segment (from `a` to `b`) against this box.
203    /// Returns `Some((t_enter, t_exit))` where 0 <= t_enter <= t_exit <= 1,
204    /// or `None` if the segment doesn't intersect the box.
205    #[allow(dead_code)]
206    pub fn clip_segment(&self, a: [f64; 3], b: [f64; 3]) -> Option<(f64, f64)> {
207        let dir = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
208        let half = [
209            self.half_extents.x,
210            self.half_extents.y,
211            self.half_extents.z,
212        ];
213
214        let mut tmin = 0.0_f64;
215        let mut tmax = 1.0_f64;
216
217        for i in 0..3 {
218            if dir[i].abs() < 1e-12 {
219                if a[i] < -half[i] || a[i] > half[i] {
220                    return None;
221                }
222            } else {
223                let inv_d = 1.0 / dir[i];
224                let mut t1 = (-half[i] - a[i]) * inv_d;
225                let mut t2 = (half[i] - a[i]) * inv_d;
226                if t1 > t2 {
227                    std::mem::swap(&mut t1, &mut t2);
228                }
229                tmin = tmin.max(t1);
230                tmax = tmax.min(t2);
231                if tmin > tmax {
232                    return None;
233                }
234            }
235        }
236
237        Some((tmin.max(0.0), tmax.min(1.0)))
238    }
239
240    /// Determine which face a surface point is on.
241    /// Returns the face index (0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z).
242    #[allow(dead_code)]
243    pub fn classify_face(&self, p: [f64; 3]) -> usize {
244        let dx_pos = (p[0] - self.half_extents.x).abs();
245        let dx_neg = (p[0] + self.half_extents.x).abs();
246        let dy_pos = (p[1] - self.half_extents.y).abs();
247        let dy_neg = (p[1] + self.half_extents.y).abs();
248        let dz_pos = (p[2] - self.half_extents.z).abs();
249        let dz_neg = (p[2] + self.half_extents.z).abs();
250
251        let dists = [dx_pos, dx_neg, dy_pos, dy_neg, dz_pos, dz_neg];
252        let mut min_idx = 0;
253        let mut min_val = dists[0];
254        for (i, &d) in dists.iter().enumerate().skip(1) {
255            if d < min_val {
256                min_val = d;
257                min_idx = i;
258            }
259        }
260        min_idx
261    }
262
263    /// Diagonal length of the box: 2 * sqrt(hx² + hy² + hz²).
264    #[allow(dead_code)]
265    pub fn diagonal_length(&self) -> f64 {
266        let hx = self.half_extents.x;
267        let hy = self.half_extents.y;
268        let hz = self.half_extents.z;
269        2.0 * (hx * hx + hy * hy + hz * hz).sqrt()
270    }
271
272    /// Edge lengths: \[2*hx, 2*hy, 2*hz\].
273    #[allow(dead_code)]
274    pub fn edge_lengths(&self) -> [f64; 3] {
275        [
276            2.0 * self.half_extents.x,
277            2.0 * self.half_extents.y,
278            2.0 * self.half_extents.z,
279        ]
280    }
281
282    /// Project the box onto an axis and return `(min, max)` interval.
283    #[allow(dead_code)]
284    pub fn project_on_axis(&self, axis: [f64; 3]) -> (f64, f64) {
285        // For an AABB, the projection extent is the sum of |axis_i * half_extent_i|
286        let extent = self.half_extents.x * axis[0].abs()
287            + self.half_extents.y * axis[1].abs()
288            + self.half_extents.z * axis[2].abs();
289        (-extent, extent)
290    }
291}
292
293impl Shape for BoxShape {
294    fn bounding_box(&self) -> Aabb {
295        Aabb::new(-self.half_extents, self.half_extents)
296    }
297
298    fn support_point(&self, direction: &Vec3) -> Vec3 {
299        Vec3::new(
300            self.half_extents.x.copysign(direction.x),
301            self.half_extents.y.copysign(direction.y),
302            self.half_extents.z.copysign(direction.z),
303        )
304    }
305
306    fn volume(&self) -> Real {
307        8.0 * self.half_extents.x * self.half_extents.y * self.half_extents.z
308    }
309
310    fn center_of_mass(&self) -> Vec3 {
311        Vec3::zeros()
312    }
313
314    fn inertia_tensor(&self, mass: Real) -> Mat3 {
315        let hx = self.half_extents.x;
316        let hy = self.half_extents.y;
317        let hz = self.half_extents.z;
318        // Full extents squared
319        let ex2 = (2.0 * hx).powi(2);
320        let ey2 = (2.0 * hy).powi(2);
321        let ez2 = (2.0 * hz).powi(2);
322        let k = mass / 12.0;
323        Mat3::new(
324            k * (ey2 + ez2),
325            0.0,
326            0.0,
327            0.0,
328            k * (ex2 + ez2),
329            0.0,
330            0.0,
331            0.0,
332            k * (ex2 + ey2),
333        )
334    }
335
336    fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
337        // Slab method for AABB ray intersection
338        let mut tmin = Real::NEG_INFINITY;
339        let mut tmax = Real::INFINITY;
340        let mut normal = Vec3::zeros();
341
342        for i in 0..3 {
343            let o = ray_origin[i];
344            let d = ray_direction[i];
345            let half = self.half_extents[i];
346
347            if d.abs() < 1e-12 {
348                if o < -half || o > half {
349                    return None;
350                }
351            } else {
352                let inv_d = 1.0 / d;
353                let mut t1 = (-half - o) * inv_d;
354                let mut t2 = (half - o) * inv_d;
355                let mut n = Vec3::zeros();
356                n[i] = -1.0;
357
358                if t1 > t2 {
359                    std::mem::swap(&mut t1, &mut t2);
360                    n[i] = 1.0;
361                }
362
363                if t1 > tmin {
364                    tmin = t1;
365                    normal = n;
366                }
367                if t2 < tmax {
368                    tmax = t2;
369                }
370
371                if tmin > tmax {
372                    return None;
373                }
374            }
375        }
376
377        let t = if tmin >= 0.0 { tmin } else { tmax };
378        if t < 0.0 || t > max_toi {
379            return None;
380        }
381
382        let point = ray_origin + ray_direction * t;
383        Some(RayHit {
384            point,
385            normal,
386            toi: t,
387        })
388    }
389}
390
391// ── OBB (Oriented Bounding Box) free functions ──
392// These work with rotation matrices (row-major [[f64;3];3]) and plain [f64;3] arrays,
393// so they can be used without nalgebra.
394
395/// Dot product of two \[f64;3\] vectors.
396#[allow(dead_code)]
397fn dot3b(a: [f64; 3], b: [f64; 3]) -> f64 {
398    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
399}
400
401/// Matrix-vector multiply: rot (row-major) * v.
402#[allow(dead_code)]
403fn mat3_mul_vec(rot: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
404    [dot3b(rot[0], v), dot3b(rot[1], v), dot3b(rot[2], v)]
405}
406
407/// Transpose of a 3×3 matrix.
408#[allow(dead_code)]
409fn mat3_transpose(rot: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
410    [
411        [rot[0][0], rot[1][0], rot[2][0]],
412        [rot[0][1], rot[1][1], rot[2][1]],
413        [rot[0][2], rot[1][2], rot[2][2]],
414    ]
415}
416
417/// 3×3 matrix multiply A*B.
418#[allow(dead_code)]
419fn mat3_mul(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
420    let bt = mat3_transpose(b);
421    [
422        [dot3b(a[0], bt[0]), dot3b(a[0], bt[1]), dot3b(a[0], bt[2])],
423        [dot3b(a[1], bt[0]), dot3b(a[1], bt[1]), dot3b(a[1], bt[2])],
424        [dot3b(a[2], bt[0]), dot3b(a[2], bt[1]), dot3b(a[2], bt[2])],
425    ]
426}
427
428/// Inertia tensor of an OBB in its own local frame (diagonal matrix).
429/// `half_extents`: half-widths along each local axis.
430/// Returns a 3×3 row-major array.
431#[allow(dead_code)]
432pub fn obb_inertia_tensor(half_extents: [f64; 3], mass: f64) -> [[f64; 3]; 3] {
433    let ax2 = (2.0 * half_extents[0]).powi(2);
434    let ay2 = (2.0 * half_extents[1]).powi(2);
435    let az2 = (2.0 * half_extents[2]).powi(2);
436    let k = mass / 12.0;
437    [
438        [k * (ay2 + az2), 0.0, 0.0],
439        [0.0, k * (ax2 + az2), 0.0],
440        [0.0, 0.0, k * (ax2 + ay2)],
441    ]
442}
443
444/// Inertia tensor of an OBB transformed to world frame via rotation matrix `rot`.
445/// `rot` maps local axes to world axes (each row is a world-space basis vector).
446/// Uses I_world = R * I_local * R^T.
447#[allow(dead_code)]
448pub fn obb_inertia_tensor_rotated(
449    half_extents: [f64; 3],
450    mass: f64,
451    rot: [[f64; 3]; 3],
452) -> [[f64; 3]; 3] {
453    let i_local = obb_inertia_tensor(half_extents, mass);
454    // I_world = rot * i_local * rot^T
455    let ri = mat3_mul(rot, i_local);
456    mat3_mul(ri, mat3_transpose(rot))
457}
458
459/// Closest point on the surface of an OBB to a world-space point `p`.
460/// `center`: OBB center in world space.
461/// `half_extents`: half-widths along each local axis.
462/// `rot`: rotation matrix (row i = world direction of local axis i).
463/// Returns world-space closest point (may be on interior if `p` is inside).
464#[allow(dead_code)]
465pub fn obb_closest_point(
466    p: [f64; 3],
467    center: [f64; 3],
468    half_extents: [f64; 3],
469    rot: [[f64; 3]; 3],
470) -> [f64; 3] {
471    // Transform p to local space: local = rot * (p - center)
472    let d = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
473    let local = mat3_mul_vec(rot, d);
474
475    // Clamp each component to half-extents
476    let clamped = [
477        local[0].clamp(-half_extents[0], half_extents[0]),
478        local[1].clamp(-half_extents[1], half_extents[1]),
479        local[2].clamp(-half_extents[2], half_extents[2]),
480    ];
481
482    // Transform back to world space: world = center + rot^T * clamped
483    let rt = mat3_transpose(rot);
484    let world_offset = mat3_mul_vec(rt, clamped);
485    [
486        center[0] + world_offset[0],
487        center[1] + world_offset[1],
488        center[2] + world_offset[2],
489    ]
490}
491
492/// Compute the projection half-extent of an OBB onto a world-space axis.
493/// This is the sum of |half_i * dot(rot_col_i, axis)| over i.
494#[allow(dead_code)]
495pub fn obb_projection_extent(half_extents: [f64; 3], rot: [[f64; 3]; 3], axis: [f64; 3]) -> f64 {
496    // rot row i = world direction of local axis i
497    // dot(rot row i, axis) = projection of local axis i onto axis
498    let mut ext = 0.0_f64;
499    for i in 0..3 {
500        ext += half_extents[i] * dot3b(rot[i], axis).abs();
501    }
502    ext
503}
504
505/// Ray-OBB intersection using the slab method in local OBB space.
506/// `ray_origin`, `ray_dir`: world-space ray.
507/// `center`, `half_extents`, `rot`: OBB parameters.
508/// Returns the hit parameter `t` if intersection within `[0, max_toi]`, else `None`.
509#[allow(dead_code)]
510pub fn obb_ray_intersection(
511    ray_origin: [f64; 3],
512    ray_dir: [f64; 3],
513    center: [f64; 3],
514    half_extents: [f64; 3],
515    rot: [[f64; 3]; 3],
516    max_toi: f64,
517) -> Option<f64> {
518    // Transform ray to OBB local space
519    let d_world = [
520        ray_origin[0] - center[0],
521        ray_origin[1] - center[1],
522        ray_origin[2] - center[2],
523    ];
524    let o_local = mat3_mul_vec(rot, d_world);
525    let dir_local = mat3_mul_vec(rot, ray_dir);
526
527    let mut tmin = f64::NEG_INFINITY;
528    let mut tmax = f64::INFINITY;
529
530    for i in 0..3 {
531        let o = o_local[i];
532        let d = dir_local[i];
533        let h = half_extents[i];
534
535        if d.abs() < 1e-12 {
536            if o < -h || o > h {
537                return None;
538            }
539        } else {
540            let inv_d = 1.0 / d;
541            let t1 = (-h - o) * inv_d;
542            let t2 = (h - o) * inv_d;
543            let (t_near, t_far) = if t1 <= t2 { (t1, t2) } else { (t2, t1) };
544            tmin = tmin.max(t_near);
545            tmax = tmax.min(t_far);
546            if tmin > tmax {
547                return None;
548            }
549        }
550    }
551
552    let t = if tmin >= 0.0 { tmin } else { tmax };
553    if t < 0.0 || t > max_toi {
554        None
555    } else {
556        Some(t)
557    }
558}
559
560/// Signed distance from the OBB to a plane (ax + by + cz + d = 0).
561/// Returns `(min_signed_dist, max_signed_dist)` for the OBB interval projected on the plane normal.
562/// If `min < 0 && max > 0`, the OBB straddles the plane.
563#[allow(dead_code)]
564pub fn obb_plane_distance(
565    center: [f64; 3],
566    half_extents: [f64; 3],
567    rot: [[f64; 3]; 3],
568    plane: [f64; 4],
569) -> (f64, f64) {
570    let normal = [plane[0], plane[1], plane[2]];
571    let plane_d = plane[3];
572
573    // Project OBB center onto plane normal
574    let center_dist = dot3b(center, normal) + plane_d;
575
576    // Compute projection half-extent
577    let extent = obb_projection_extent(half_extents, rot, normal);
578
579    (center_dist - extent, center_dist + extent)
580}
581
582/// Returns all 8 vertices of the OBB in world space.
583#[allow(dead_code)]
584pub fn obb_vertices(center: [f64; 3], half_extents: [f64; 3], rot: [[f64; 3]; 3]) -> Vec<[f64; 3]> {
585    let rt = mat3_transpose(rot);
586    let signs = [
587        [-1.0_f64, -1.0, -1.0],
588        [1.0, -1.0, -1.0],
589        [1.0, 1.0, -1.0],
590        [-1.0, 1.0, -1.0],
591        [-1.0, -1.0, 1.0],
592        [1.0, -1.0, 1.0],
593        [1.0, 1.0, 1.0],
594        [-1.0, 1.0, 1.0],
595    ];
596    signs
597        .iter()
598        .map(|s| {
599            let local = [
600                s[0] * half_extents[0],
601                s[1] * half_extents[1],
602                s[2] * half_extents[2],
603            ];
604            let world = mat3_mul_vec(rt, local);
605            [
606                center[0] + world[0],
607                center[1] + world[1],
608                center[2] + world[2],
609            ]
610        })
611        .collect()
612}
613
614/// Returns all 12 edges of the OBB in world space as (start, end) pairs.
615#[allow(dead_code)]
616pub fn obb_edges(
617    center: [f64; 3],
618    half_extents: [f64; 3],
619    rot: [[f64; 3]; 3],
620) -> Vec<([f64; 3], [f64; 3])> {
621    let verts = obb_vertices(center, half_extents, rot);
622    // Edge pairs using same winding as BoxShape::edge_list()
623    let pairs: [(usize, usize); 12] = [
624        (0, 1),
625        (1, 5),
626        (5, 4),
627        (4, 0),
628        (2, 3),
629        (3, 7),
630        (7, 6),
631        (6, 2),
632        (0, 3),
633        (1, 2),
634        (5, 6),
635        (4, 7),
636    ];
637    pairs.iter().map(|&(a, b)| (verts[a], verts[b])).collect()
638}
639
640/// Returns the 6 face centers of the OBB in world space.
641/// Order: +local_x, -local_x, +local_y, -local_y, +local_z, -local_z.
642#[allow(dead_code)]
643pub fn obb_face_centers(
644    center: [f64; 3],
645    half_extents: [f64; 3],
646    rot: [[f64; 3]; 3],
647) -> Vec<[f64; 3]> {
648    let rt = mat3_transpose(rot);
649    let mut fc = Vec::with_capacity(6);
650    for axis in 0..3 {
651        for sign in [1.0_f64, -1.0_f64] {
652            let mut local = [0.0_f64; 3];
653            local[axis] = sign * half_extents[axis];
654            let world = mat3_mul_vec(rt, local);
655            fc.push([
656                center[0] + world[0],
657                center[1] + world[1],
658                center[2] + world[2],
659            ]);
660        }
661    }
662    fc
663}
664
665/// GJK support function for an OBB: returns the farthest vertex in `direction`.
666#[allow(dead_code)]
667pub fn obb_support_fn(
668    center: [f64; 3],
669    half_extents: [f64; 3],
670    rot: [[f64; 3]; 3],
671    direction: [f64; 3],
672) -> [f64; 3] {
673    // Project direction onto local axes to get signs
674    let local_dir = mat3_mul_vec(rot, direction);
675    let local_support = [
676        half_extents[0].copysign(local_dir[0]),
677        half_extents[1].copysign(local_dir[1]),
678        half_extents[2].copysign(local_dir[2]),
679    ];
680    let rt = mat3_transpose(rot);
681    let world_offset = mat3_mul_vec(rt, local_support);
682    [
683        center[0] + world_offset[0],
684        center[1] + world_offset[1],
685        center[2] + world_offset[2],
686    ]
687}
688
689/// Compute the AABB bounds (lo, hi) of an OBB in world space.
690#[allow(dead_code)]
691pub fn obb_aabb_bounds(
692    center: [f64; 3],
693    half_extents: [f64; 3],
694    rot: [[f64; 3]; 3],
695) -> ([f64; 3], [f64; 3]) {
696    let mut lo = center;
697    let mut hi = center;
698    let rt = mat3_transpose(rot);
699    for axis in 0..3 {
700        // Each local axis contributes: |rot_col_axis| * half_extents[axis]
701        for k in 0..3 {
702            let contribution = rt[k][axis].abs() * half_extents[axis];
703            lo[k] -= contribution;
704            hi[k] += contribution;
705        }
706    }
707    (lo, hi)
708}
709
710#[cfg(test)]
711mod proptest_tests {
712    use super::*;
713    use crate::BoxShape;
714    use crate::box_shape::Vec3;
715
716    use proptest::prelude::*;
717
718    fn pos_half() -> impl Strategy<Value = f64> {
719        0.01_f64..100.0_f64
720    }
721
722    proptest! {
723        #[test]
724        fn prop_box_volume_nonneg(
725            hx in pos_half(),
726            hy in pos_half(),
727            hz in pos_half(),
728        ) {
729            let b = BoxShape::new(Vec3::new(hx, hy, hz));
730            prop_assert!(b.volume() >= 0.0, "volume negative: {}", b.volume());
731        }
732
733        #[test]
734        fn prop_box_inertia_positive_definite(
735            hx in pos_half(),
736            hy in pos_half(),
737            hz in pos_half(),
738            m in 0.01_f64..1000.0_f64,
739        ) {
740            let b = BoxShape::new(Vec3::new(hx, hy, hz));
741            let it = b.inertia_tensor(m);
742            prop_assert!(it[(0, 0)] > 0.0, "Ixx not positive: {}", it[(0, 0)]);
743            prop_assert!(it[(1, 1)] > 0.0, "Iyy not positive: {}", it[(1, 1)]);
744            prop_assert!(it[(2, 2)] > 0.0, "Izz not positive: {}", it[(2, 2)]);
745        }
746
747        #[test]
748        fn prop_box_volume_scales_cubically(
749            hx in pos_half(),
750            hy in pos_half(),
751            hz in pos_half(),
752        ) {
753            let b1 = BoxShape::new(Vec3::new(hx, hy, hz));
754            let b2 = BoxShape::new(Vec3::new(2.0 * hx, 2.0 * hy, 2.0 * hz));
755            let ratio = b2.volume() / b1.volume();
756            prop_assert!(
757                (ratio - 8.0).abs() < 1e-6,
758                "box volume ratio for 2x scale should be 8, got {}",
759                ratio
760            );
761        }
762    }
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768    use crate::BoxShape;
769    use crate::box_shape::Vec3;
770    use crate::box_shape::obb_aabb_bounds;
771    use crate::box_shape::obb_closest_point;
772    use crate::box_shape::obb_edges;
773    use crate::box_shape::obb_face_centers;
774    use crate::box_shape::obb_inertia_tensor;
775    use crate::box_shape::obb_inertia_tensor_rotated;
776    use crate::box_shape::obb_plane_distance;
777    use crate::box_shape::obb_projection_extent;
778    use crate::box_shape::obb_ray_intersection;
779    use crate::box_shape::obb_support_fn;
780    use crate::box_shape::obb_vertices;
781
782    #[test]
783    fn test_box_support() {
784        let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
785        let sp = b.support_point(&Vec3::new(1.0, 1.0, 1.0));
786        assert_eq!(sp, Vec3::new(1.0, 2.0, 3.0));
787    }
788
789    #[test]
790    fn test_box_support_array() {
791        let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
792        let sp = b.support([1.0, 1.0, 1.0]);
793        assert!((sp[0] - 1.0).abs() < 1e-10);
794        assert!((sp[1] - 2.0).abs() < 1e-10);
795        assert!((sp[2] - 3.0).abs() < 1e-10);
796    }
797
798    #[test]
799    fn test_box_volume() {
800        let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
801        assert!((b.volume() - 48.0).abs() < 1e-10);
802    }
803
804    #[test]
805    fn test_box_surface_area() {
806        // Half-extents (1,1,1): full box 2x2x2, SA = 6*4 = 24
807        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
808        assert!((b.surface_area() - 24.0).abs() < 1e-10);
809    }
810
811    #[test]
812    fn test_box_inertia() {
813        // Unit cube (half_extents = 0.5), mass = 1
814        let b = BoxShape::new(Vec3::new(0.5, 0.5, 0.5));
815        let it = b.inertia_tensor(1.0);
816        // I = m/12 * (h^2 + d^2) = 1/12 * (1+1) = 1/6
817        assert!((it[(0, 0)] - 1.0 / 6.0).abs() < 1e-10);
818    }
819
820    #[test]
821    fn test_box_inertia_array() {
822        let b = BoxShape::new(Vec3::new(0.5, 0.5, 0.5));
823        let it = b.inertia_tensor_array(1.0);
824        assert!((it[0][0] - 1.0 / 6.0).abs() < 1e-10);
825        assert!(it[0][1].abs() < 1e-10);
826    }
827
828    #[test]
829    fn test_box_raycast() {
830        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
831        let origin = Vec3::new(-5.0, 0.0, 0.0);
832        let dir = Vec3::new(1.0, 0.0, 0.0);
833        let hit = b.ray_cast(&origin, &dir, 100.0).unwrap();
834        assert!((hit.toi - 4.0).abs() < 1e-10);
835        assert!((hit.point.x + 1.0).abs() < 1e-10);
836    }
837
838    #[test]
839    fn test_box_raycast_array() {
840        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
841        let (t, n) = b
842            .ray_cast_array([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0)
843            .unwrap();
844        assert!((t - 4.0).abs() < 1e-10);
845        assert!((n[0] + 1.0).abs() < 1e-10);
846    }
847
848    #[test]
849    fn test_box_vertex_list() {
850        let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
851        let verts = b.vertex_list();
852        assert_eq!(verts.len(), 8);
853        // All vertices should have |x|=1, |y|=2, |z|=3
854        for v in &verts {
855            assert!((v[0].abs() - 1.0).abs() < 1e-10);
856            assert!((v[1].abs() - 2.0).abs() < 1e-10);
857            assert!((v[2].abs() - 3.0).abs() < 1e-10);
858        }
859    }
860
861    #[test]
862    fn test_box_raycast_miss() {
863        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
864        // Ray parallel to face and outside
865        let origin = Vec3::new(5.0, 5.0, 0.0);
866        let dir = Vec3::new(1.0, 0.0, 0.0);
867        assert!(b.ray_cast(&origin, &dir, 100.0).is_none());
868    }
869
870    // ── New tests ──
871
872    #[test]
873    fn test_box_face_normals() {
874        let normals = BoxShape::face_normals();
875        assert_eq!(normals.len(), 6);
876        // Each normal should have unit length
877        for n in &normals {
878            let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
879            assert!((len - 1.0).abs() < 1e-10);
880        }
881    }
882
883    #[test]
884    fn test_box_edge_list() {
885        let edges = BoxShape::edge_list();
886        assert_eq!(edges.len(), 12);
887        // All indices should be < 8
888        for (a, b) in &edges {
889            assert!(*a < 8);
890            assert!(*b < 8);
891        }
892    }
893
894    #[test]
895    fn test_box_face_vertex_indices() {
896        let faces = BoxShape::face_vertex_indices();
897        assert_eq!(faces.len(), 6);
898        for face in &faces {
899            for &idx in face {
900                assert!(idx < 8);
901            }
902        }
903    }
904
905    #[test]
906    fn test_box_face_areas() {
907        let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
908        let areas = b.face_areas();
909        // +X/-X faces: 4*2*3 = 24
910        assert!((areas[0] - 24.0).abs() < 1e-10);
911        assert!((areas[1] - 24.0).abs() < 1e-10);
912        // +Y/-Y faces: 4*1*3 = 12
913        assert!((areas[2] - 12.0).abs() < 1e-10);
914        assert!((areas[3] - 12.0).abs() < 1e-10);
915        // +Z/-Z faces: 4*1*2 = 8
916        assert!((areas[4] - 8.0).abs() < 1e-10);
917        assert!((areas[5] - 8.0).abs() < 1e-10);
918    }
919
920    #[test]
921    fn test_box_face_areas_sum_equals_surface_area() {
922        let b = BoxShape::new(Vec3::new(1.5, 2.5, 3.5));
923        let areas = b.face_areas();
924        let sum: f64 = areas.iter().sum();
925        assert!((sum - b.surface_area()).abs() < 1e-10);
926    }
927
928    #[test]
929    fn test_box_closest_point_inside() {
930        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
931        let cp = b.closest_point([0.5, 0.5, 0.5]);
932        assert_eq!(cp, [0.5, 0.5, 0.5]); // inside, returns same point
933    }
934
935    #[test]
936    fn test_box_closest_point_outside() {
937        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
938        let cp = b.closest_point([3.0, 0.0, 0.0]);
939        assert!((cp[0] - 1.0).abs() < 1e-10);
940        assert!(cp[1].abs() < 1e-10);
941        assert!(cp[2].abs() < 1e-10);
942    }
943
944    #[test]
945    fn test_box_closest_point_corner() {
946        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
947        let cp = b.closest_point([3.0, 3.0, 3.0]);
948        assert!((cp[0] - 1.0).abs() < 1e-10);
949        assert!((cp[1] - 1.0).abs() < 1e-10);
950        assert!((cp[2] - 1.0).abs() < 1e-10);
951    }
952
953    #[test]
954    fn test_box_contains_point() {
955        let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
956        assert!(b.contains_point([0.0, 0.0, 0.0]));
957        assert!(b.contains_point([1.0, 2.0, 3.0])); // on surface
958        assert!(!b.contains_point([1.1, 0.0, 0.0]));
959    }
960
961    #[test]
962    fn test_box_signed_distance_inside() {
963        let b = BoxShape::new(Vec3::new(2.0, 2.0, 2.0));
964        let d = b.signed_distance([0.0, 0.0, 0.0]);
965        assert!((d + 2.0).abs() < 1e-10); // center, min dist to face = 2
966    }
967
968    #[test]
969    fn test_box_signed_distance_outside() {
970        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
971        let d = b.signed_distance([3.0, 0.0, 0.0]);
972        assert!((d - 2.0).abs() < 1e-10);
973    }
974
975    #[test]
976    fn test_box_signed_distance_corner() {
977        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
978        let d = b.signed_distance([2.0, 2.0, 2.0]);
979        // Distance to corner (1,1,1) = sqrt(3)
980        assert!((d - 3.0_f64.sqrt()).abs() < 1e-10);
981    }
982
983    #[test]
984    fn test_box_clip_segment_through() {
985        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
986        let result = b.clip_segment([-3.0, 0.0, 0.0], [3.0, 0.0, 0.0]);
987        assert!(result.is_some());
988        let (t_enter, t_exit) = result.unwrap();
989        // Segment goes from x=-3 to x=3, box is [-1,1]
990        // t_enter = (1+(-3))/(3-(-3)) = (-3+1)/(6)... let's compute:
991        // x = -3 + t*6, hit at x=-1: t=(2)/6=1/3, x=1: t=4/6=2/3
992        assert!((t_enter - 1.0 / 3.0).abs() < 1e-10);
993        assert!((t_exit - 2.0 / 3.0).abs() < 1e-10);
994    }
995
996    #[test]
997    fn test_box_clip_segment_miss() {
998        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
999        let result = b.clip_segment([5.0, 5.0, 0.0], [6.0, 5.0, 0.0]);
1000        assert!(result.is_none());
1001    }
1002
1003    #[test]
1004    fn test_box_clip_segment_inside() {
1005        let b = BoxShape::new(Vec3::new(2.0, 2.0, 2.0));
1006        let result = b.clip_segment([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1007        assert!(result.is_some());
1008        let (t_enter, t_exit) = result.unwrap();
1009        assert!(t_enter.abs() < 1e-10);
1010        assert!((t_exit - 1.0).abs() < 1e-10);
1011    }
1012
1013    #[test]
1014    fn test_box_classify_face() {
1015        let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
1016        assert_eq!(b.classify_face([1.0, 0.0, 0.0]), 0); // +X
1017        assert_eq!(b.classify_face([-1.0, 0.0, 0.0]), 1); // -X
1018        assert_eq!(b.classify_face([0.0, 2.0, 0.0]), 2); // +Y
1019        assert_eq!(b.classify_face([0.0, -2.0, 0.0]), 3); // -Y
1020        assert_eq!(b.classify_face([0.0, 0.0, 3.0]), 4); // +Z
1021        assert_eq!(b.classify_face([0.0, 0.0, -3.0]), 5); // -Z
1022    }
1023
1024    #[test]
1025    fn test_box_diagonal_length() {
1026        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
1027        let expected = 2.0 * 3.0_f64.sqrt();
1028        assert!((b.diagonal_length() - expected).abs() < 1e-10);
1029    }
1030
1031    #[test]
1032    fn test_box_edge_lengths() {
1033        let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
1034        let el = b.edge_lengths();
1035        assert!((el[0] - 2.0).abs() < 1e-10);
1036        assert!((el[1] - 4.0).abs() < 1e-10);
1037        assert!((el[2] - 6.0).abs() < 1e-10);
1038    }
1039
1040    #[test]
1041    fn test_box_project_on_axis() {
1042        let b = BoxShape::new(Vec3::new(1.0, 2.0, 3.0));
1043        // Project onto X axis
1044        let (lo, hi) = b.project_on_axis([1.0, 0.0, 0.0]);
1045        assert!((lo + 1.0).abs() < 1e-10);
1046        assert!((hi - 1.0).abs() < 1e-10);
1047    }
1048
1049    #[test]
1050    fn test_box_project_on_diagonal_axis() {
1051        let b = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
1052        // Project onto (1,1,1)/sqrt(3) axis
1053        let s = 1.0 / 3.0_f64.sqrt();
1054        let (lo, hi) = b.project_on_axis([s, s, s]);
1055        // Extent = hx*|s| + hy*|s| + hz*|s| = 3*s = sqrt(3)
1056        let expected = 3.0_f64.sqrt();
1057        assert!((hi - expected).abs() < 1e-10);
1058        assert!((lo + expected).abs() < 1e-10);
1059    }
1060
1061    #[test]
1062    fn test_box_volume_explicit_matches() {
1063        let b = BoxShape::new(Vec3::new(1.5, 2.5, 3.5));
1064        assert!((b.volume_explicit() - b.volume()).abs() < 1e-10);
1065    }
1066
1067    // ── OBB free-function tests ──
1068
1069    #[test]
1070    fn test_obb_inertia_tensor_unit_cube() {
1071        // Unit cube: half_extents (0.5,0.5,0.5), mass=1
1072        // I_xx = m/12*(b²+c²) = 1/12*(1+1) = 1/6
1073        let it = obb_inertia_tensor([0.5, 0.5, 0.5], 1.0);
1074        assert!((it[0][0] - 1.0 / 6.0).abs() < 1e-10);
1075        assert!((it[1][1] - 1.0 / 6.0).abs() < 1e-10);
1076        assert!((it[2][2] - 1.0 / 6.0).abs() < 1e-10);
1077        // Off-diagonal must be zero
1078        assert!(it[0][1].abs() < 1e-10);
1079        assert!(it[0][2].abs() < 1e-10);
1080    }
1081
1082    #[test]
1083    fn test_obb_inertia_tensor_asymmetric() {
1084        // half_extents (1,2,3), mass=12
1085        // a=2,b=4,c=6 → Ixx=12/12*(16+36)=52, Iyy=12/12*(4+36)=40, Izz=12/12*(4+16)=20
1086        let it = obb_inertia_tensor([1.0, 2.0, 3.0], 12.0);
1087        assert!((it[0][0] - 52.0).abs() < 1e-10);
1088        assert!((it[1][1] - 40.0).abs() < 1e-10);
1089        assert!((it[2][2] - 20.0).abs() < 1e-10);
1090    }
1091
1092    #[test]
1093    fn test_obb_inertia_tensor_rotated_identity() {
1094        // With identity rotation, should match the axis-aligned formula
1095        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1096        let it = obb_inertia_tensor_rotated([1.0, 2.0, 3.0], 12.0, rot);
1097        let ref_it = obb_inertia_tensor([1.0, 2.0, 3.0], 12.0);
1098        for i in 0..3 {
1099            for j in 0..3 {
1100                assert!(
1101                    (it[i][j] - ref_it[i][j]).abs() < 1e-10,
1102                    "mismatch at [{i}][{j}]: {} vs {}",
1103                    it[i][j],
1104                    ref_it[i][j]
1105                );
1106            }
1107        }
1108    }
1109
1110    #[test]
1111    fn test_obb_closest_point_inside() {
1112        let center = [0.0, 0.0, 0.0];
1113        let half = [1.0, 1.0, 1.0];
1114        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1115        let cp = obb_closest_point([0.5, 0.3, -0.2], center, half, rot);
1116        // Point is inside; closest = the point itself
1117        assert!((cp[0] - 0.5).abs() < 1e-10);
1118        assert!((cp[1] - 0.3).abs() < 1e-10);
1119        assert!((cp[2] + 0.2).abs() < 1e-10);
1120    }
1121
1122    #[test]
1123    fn test_obb_closest_point_outside_x() {
1124        let center = [0.0, 0.0, 0.0];
1125        let half = [1.0, 1.0, 1.0];
1126        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1127        let cp = obb_closest_point([5.0, 0.0, 0.0], center, half, rot);
1128        assert!((cp[0] - 1.0).abs() < 1e-10);
1129        assert!(cp[1].abs() < 1e-10);
1130        assert!(cp[2].abs() < 1e-10);
1131    }
1132
1133    #[test]
1134    fn test_obb_closest_point_rotated() {
1135        // OBB rotated 90° around Z: x-axis becomes y-axis in world
1136        let center = [0.0, 0.0, 0.0];
1137        let half = [2.0, 1.0, 1.0];
1138        // Rotation: x-col=[0,1,0], y-col=[-1,0,0], z-col=[0,0,1]
1139        let rot = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
1140        // A point far in world-y should clamp to OBB local-x extent (2)
1141        let cp = obb_closest_point([0.0, 5.0, 0.0], center, half, rot);
1142        // local coords: p_local_x = dot([5y], [0,1,0]) = 5, clamped to 2
1143        // world closest = center + rot * [2, 0, 0] = [0, 2, 0]
1144        assert!((cp[0]).abs() < 1e-10);
1145        assert!((cp[1] - 2.0).abs() < 1e-10);
1146    }
1147
1148    #[test]
1149    fn test_obb_ray_intersection_hit() {
1150        let center = [0.0, 0.0, 0.0];
1151        let half = [1.0, 1.0, 1.0];
1152        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1153        let result =
1154            obb_ray_intersection([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], center, half, rot, 100.0);
1155        assert!(result.is_some());
1156        let t = result.unwrap();
1157        assert!((t - 4.0).abs() < 1e-10);
1158    }
1159
1160    #[test]
1161    fn test_obb_ray_intersection_miss() {
1162        let center = [0.0, 0.0, 0.0];
1163        let half = [1.0, 1.0, 1.0];
1164        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1165        let result =
1166            obb_ray_intersection([0.0, 5.0, 0.0], [1.0, 0.0, 0.0], center, half, rot, 100.0);
1167        assert!(result.is_none());
1168    }
1169
1170    #[test]
1171    fn test_obb_ray_intersection_rotated() {
1172        // OBB at (3,0,0) with identity rotation, half-extents (1,1,1)
1173        // Ray from origin along +x should hit at t=2
1174        let center = [3.0, 0.0, 0.0];
1175        let half = [1.0, 1.0, 1.0];
1176        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1177        let result =
1178            obb_ray_intersection([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], center, half, rot, 100.0);
1179        assert!(result.is_some());
1180        let t = result.unwrap();
1181        assert!((t - 2.0).abs() < 1e-10);
1182    }
1183
1184    #[test]
1185    fn test_obb_plane_distance_above() {
1186        // OBB at origin, half (1,1,1), plane normal (0,1,0) at height 3
1187        // Min signed dist from plane: 3 - half_projection = 3 - 1 = 2
1188        let center = [0.0, 0.0, 0.0];
1189        let half = [1.0, 1.0, 1.0];
1190        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1191        let plane = [0.0, 1.0, 0.0, -3.0]; // dot(n,p) + d = 0 → y = 3
1192        let (min_d, max_d) = obb_plane_distance(center, half, rot, plane);
1193        // center projected: 0*1+0*1+0*0-3 = -3 (below plane, so negative)
1194        // projection extent: 1
1195        // min = -3 - 1 = -4, max = -3 + 1 = -2
1196        assert!((min_d + 4.0).abs() < 1e-10, "min_d={min_d}");
1197        assert!((max_d + 2.0).abs() < 1e-10, "max_d={max_d}");
1198    }
1199
1200    #[test]
1201    fn test_obb_plane_distance_straddles() {
1202        // OBB at (0,0,0) half (2,2,2), plane y=0 → min<0, max>0
1203        let center = [0.0, 0.0, 0.0];
1204        let half = [2.0, 2.0, 2.0];
1205        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1206        let plane = [0.0, 1.0, 0.0, 0.0]; // y plane through origin
1207        let (min_d, max_d) = obb_plane_distance(center, half, rot, plane);
1208        assert!(min_d < 0.0);
1209        assert!(max_d > 0.0);
1210    }
1211
1212    #[test]
1213    fn test_obb_vertices_count() {
1214        let center = [1.0, 2.0, 3.0];
1215        let half = [1.0, 1.0, 1.0];
1216        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1217        let verts = obb_vertices(center, half, rot);
1218        assert_eq!(verts.len(), 8);
1219    }
1220
1221    #[test]
1222    fn test_obb_vertices_aabb_bounds() {
1223        // Identity rotation OBB: vertices should all be within AABB
1224        let center = [0.0, 0.0, 0.0];
1225        let half = [1.0, 2.0, 3.0];
1226        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1227        let verts = obb_vertices(center, half, rot);
1228        for v in &verts {
1229            assert!(v[0].abs() <= 1.0 + 1e-10);
1230            assert!(v[1].abs() <= 2.0 + 1e-10);
1231            assert!(v[2].abs() <= 3.0 + 1e-10);
1232        }
1233    }
1234
1235    #[test]
1236    fn test_obb_vertices_translated() {
1237        let center = [5.0, 0.0, 0.0];
1238        let half = [1.0, 1.0, 1.0];
1239        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1240        let verts = obb_vertices(center, half, rot);
1241        // All x-coords should be 4 or 6
1242        for v in &verts {
1243            assert!(
1244                (v[0] - 4.0).abs() < 1e-10 || (v[0] - 6.0).abs() < 1e-10,
1245                "unexpected x={}",
1246                v[0]
1247            );
1248        }
1249    }
1250
1251    #[test]
1252    fn test_obb_edges_count() {
1253        let center = [0.0, 0.0, 0.0];
1254        let half = [1.0, 1.0, 1.0];
1255        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1256        let edges = obb_edges(center, half, rot);
1257        assert_eq!(edges.len(), 12);
1258    }
1259
1260    #[test]
1261    fn test_obb_edges_lengths() {
1262        let center = [0.0, 0.0, 0.0];
1263        let half = [1.0, 2.0, 3.0];
1264        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1265        let edges = obb_edges(center, half, rot);
1266        for (a, b) in &edges {
1267            let dx = b[0] - a[0];
1268            let dy = b[1] - a[1];
1269            let dz = b[2] - a[2];
1270            let len = (dx * dx + dy * dy + dz * dz).sqrt();
1271            // Each edge should be 2, 4, or 6
1272            let is_valid =
1273                (len - 2.0).abs() < 1e-10 || (len - 4.0).abs() < 1e-10 || (len - 6.0).abs() < 1e-10;
1274            assert!(is_valid, "unexpected edge length {len}");
1275        }
1276    }
1277
1278    #[test]
1279    fn test_obb_face_centers_count() {
1280        let center = [0.0, 0.0, 0.0];
1281        let half = [1.0, 1.0, 1.0];
1282        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1283        let fc = obb_face_centers(center, half, rot);
1284        assert_eq!(fc.len(), 6);
1285    }
1286
1287    #[test]
1288    fn test_obb_face_centers_distance() {
1289        // For identity OBB with half (1,2,3), face centers should be at ±(1,2,3)
1290        let center = [0.0, 0.0, 0.0];
1291        let half = [1.0, 2.0, 3.0];
1292        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1293        let fc = obb_face_centers(center, half, rot);
1294        // Pair 0&1 along x: should be (±1, 0, 0)
1295        assert!((fc[0][0].abs() - 1.0).abs() < 1e-10);
1296        assert!((fc[1][0].abs() - 1.0).abs() < 1e-10);
1297        // Pair 2&3 along y: should be (0, ±2, 0)
1298        assert!((fc[2][1].abs() - 2.0).abs() < 1e-10);
1299        assert!((fc[3][1].abs() - 2.0).abs() < 1e-10);
1300        // Pair 4&5 along z: should be (0, 0, ±3)
1301        assert!((fc[4][2].abs() - 3.0).abs() < 1e-10);
1302        assert!((fc[5][2].abs() - 3.0).abs() < 1e-10);
1303    }
1304
1305    #[test]
1306    fn test_obb_support_function_identity() {
1307        let center = [0.0, 0.0, 0.0];
1308        let half = [1.0, 2.0, 3.0];
1309        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1310        // Support in +x direction should be (1, ±2, ±3), pick most +x = (1, ?, ?)
1311        let sp = obb_support_fn(center, half, rot, [1.0, 0.0, 0.0]);
1312        assert!((sp[0] - 1.0).abs() < 1e-10);
1313    }
1314
1315    #[test]
1316    fn test_obb_support_function_diagonal() {
1317        let center = [0.0, 0.0, 0.0];
1318        let half = [1.0, 1.0, 1.0];
1319        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1320        let sp = obb_support_fn(center, half, rot, [1.0, 1.0, 1.0]);
1321        // Farthest in (1,1,1) direction for unit cube = (1,1,1)
1322        assert!((sp[0] - 1.0).abs() < 1e-10);
1323        assert!((sp[1] - 1.0).abs() < 1e-10);
1324        assert!((sp[2] - 1.0).abs() < 1e-10);
1325    }
1326
1327    #[test]
1328    fn test_obb_support_function_translated() {
1329        let center = [3.0, 0.0, 0.0];
1330        let half = [1.0, 1.0, 1.0];
1331        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1332        let sp = obb_support_fn(center, half, rot, [1.0, 0.0, 0.0]);
1333        // Center at x=3, half=1, so farthest in +x is at x=4
1334        assert!((sp[0] - 4.0).abs() < 1e-10);
1335    }
1336
1337    #[test]
1338    fn test_obb_projection_extent_identity() {
1339        // For identity OBB, projection extent = sum of |half_i * axis_i|
1340        let half = [1.0, 2.0, 3.0];
1341        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1342        let axis = [1.0, 0.0, 0.0];
1343        let ext = obb_projection_extent(half, rot, axis);
1344        assert!((ext - 1.0).abs() < 1e-10);
1345    }
1346
1347    #[test]
1348    fn test_obb_projection_extent_diagonal() {
1349        let half = [1.0, 1.0, 1.0];
1350        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1351        let s = 1.0 / 3.0_f64.sqrt();
1352        let ext = obb_projection_extent(half, rot, [s, s, s]);
1353        assert!((ext - 3.0_f64.sqrt()).abs() < 1e-10);
1354    }
1355
1356    #[test]
1357    fn test_obb_aabb_bounds_identity() {
1358        let center = [1.0, 2.0, 3.0];
1359        let half = [1.0, 2.0, 3.0];
1360        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1361        let (lo, hi) = obb_aabb_bounds(center, half, rot);
1362        assert!((lo[0] - 0.0).abs() < 1e-10);
1363        assert!((hi[0] - 2.0).abs() < 1e-10);
1364        assert!((lo[1] - 0.0).abs() < 1e-10);
1365        assert!((hi[1] - 4.0).abs() < 1e-10);
1366    }
1367
1368    #[test]
1369    fn test_obb_aabb_bounds_contains_all_vertices() {
1370        let center = [0.5, -1.0, 2.0];
1371        let half = [1.0, 0.5, 1.5];
1372        let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1373        let (lo, hi) = obb_aabb_bounds(center, half, rot);
1374        let verts = obb_vertices(center, half, rot);
1375        for v in &verts {
1376            assert!(v[0] >= lo[0] - 1e-10 && v[0] <= hi[0] + 1e-10);
1377            assert!(v[1] >= lo[1] - 1e-10 && v[1] <= hi[1] + 1e-10);
1378            assert!(v[2] >= lo[2] - 1e-10 && v[2] <= hi[2] + 1e-10);
1379        }
1380    }
1381}