Skip to main content

oxiphysics_core/
geometry_primitives.rs

1#![allow(clippy::needless_range_loop)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Low-level geometry primitives: Ray, AABB, Sphere, Plane, Triangle, Capsule,
6//! OrientedBox, Frustum, convex polyhedron, and barycentric coordinates.
7
8#![allow(dead_code)]
9#![allow(clippy::too_many_arguments)]
10
11use std::f64::consts::PI;
12
13// ─────────────────────────────────────────────────────────────────────────────
14// Vector helpers
15// ─────────────────────────────────────────────────────────────────────────────
16
17/// Compute `a + b`.
18#[inline]
19fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
20    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
21}
22
23/// Compute `a - b`.
24#[inline]
25fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
26    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
27}
28
29/// Scale vector by scalar.
30#[inline]
31fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
32    [a[0] * s, a[1] * s, a[2] * s]
33}
34
35/// Dot product.
36#[inline]
37fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
38    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
39}
40
41/// Cross product.
42#[inline]
43fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
44    [
45        a[1] * b[2] - a[2] * b[1],
46        a[2] * b[0] - a[0] * b[2],
47        a[0] * b[1] - a[1] * b[0],
48    ]
49}
50
51/// Squared length.
52#[inline]
53fn len_sq(a: [f64; 3]) -> f64 {
54    dot(a, a)
55}
56
57/// Length.
58#[inline]
59fn len(a: [f64; 3]) -> f64 {
60    len_sq(a).sqrt()
61}
62
63/// Normalize a vector. Returns zero vector if near-zero.
64#[inline]
65fn normalize(a: [f64; 3]) -> [f64; 3] {
66    let l = len(a);
67    if l < 1e-14 {
68        [0.0; 3]
69    } else {
70        scale(a, 1.0 / l)
71    }
72}
73
74/// Element-wise min.
75#[inline]
76fn vmin(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
77    [a[0].min(b[0]), a[1].min(b[1]), a[2].min(b[2])]
78}
79
80/// Element-wise max.
81#[inline]
82fn vmax(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
83    [a[0].max(b[0]), a[1].max(b[1]), a[2].max(b[2])]
84}
85
86// ─────────────────────────────────────────────────────────────────────────────
87// Ray3
88// ─────────────────────────────────────────────────────────────────────────────
89
90/// A 3D ray with origin and (pre-normalized) direction.
91#[derive(Debug, Clone, Copy)]
92pub struct Ray3 {
93    /// Ray origin.
94    pub origin: [f64; 3],
95    /// Ray direction (should be normalized).
96    pub direction: [f64; 3],
97}
98
99impl Ray3 {
100    /// Create a new ray. Direction is normalized.
101    pub fn new(origin: [f64; 3], direction: [f64; 3]) -> Self {
102        Self {
103            origin,
104            direction: normalize(direction),
105        }
106    }
107
108    /// Point at parameter `t`: P(t) = origin + t * direction.
109    pub fn at(&self, t: f64) -> [f64; 3] {
110        add(self.origin, scale(self.direction, t))
111    }
112
113    /// Intersect ray with a sphere. Returns nearest positive `t`, or `None`.
114    pub fn intersect_sphere(&self, center: [f64; 3], radius: f64) -> Option<f64> {
115        let oc = sub(self.origin, center);
116        let a = dot(self.direction, self.direction);
117        let b = 2.0 * dot(oc, self.direction);
118        let c = dot(oc, oc) - radius * radius;
119        let disc = b * b - 4.0 * a * c;
120        if disc < 0.0 {
121            return None;
122        }
123        let sqrt_disc = disc.sqrt();
124        let t1 = (-b - sqrt_disc) / (2.0 * a);
125        let t2 = (-b + sqrt_disc) / (2.0 * a);
126        if t1 > 1e-8 {
127            Some(t1)
128        } else if t2 > 1e-8 {
129            Some(t2)
130        } else {
131            None
132        }
133    }
134
135    /// Intersect ray with an AABB (slab method). Returns `(t_min, t_max)` or `None`.
136    pub fn intersect_aabb(&self, aabb: &Aabb3) -> Option<(f64, f64)> {
137        let mut t_min = f64::NEG_INFINITY;
138        let mut t_max = f64::INFINITY;
139        for i in 0..3 {
140            let inv_d = if self.direction[i].abs() < 1e-14 {
141                f64::INFINITY
142            } else {
143                1.0 / self.direction[i]
144            };
145            let t0 = (aabb.min[i] - self.origin[i]) * inv_d;
146            let t1 = (aabb.max[i] - self.origin[i]) * inv_d;
147            let (ta, tb) = if inv_d >= 0.0 { (t0, t1) } else { (t1, t0) };
148            t_min = t_min.max(ta);
149            t_max = t_max.min(tb);
150            if t_max < t_min {
151                return None;
152            }
153        }
154        if t_max < 0.0 {
155            None
156        } else {
157            Some((t_min.max(0.0), t_max))
158        }
159    }
160
161    /// Intersect ray with a triangle using the Möller–Trumbore algorithm.
162    /// Returns `(t, u, v)` where `(u,v)` are barycentric coordinates, or `None`.
163    pub fn intersect_triangle(
164        &self,
165        v0: [f64; 3],
166        v1: [f64; 3],
167        v2: [f64; 3],
168    ) -> Option<(f64, f64, f64)> {
169        let eps = 1e-8;
170        let e1 = sub(v1, v0);
171        let e2 = sub(v2, v0);
172        let h = cross(self.direction, e2);
173        let a = dot(e1, h);
174        if a.abs() < eps {
175            return None;
176        }
177        let f = 1.0 / a;
178        let s = sub(self.origin, v0);
179        let u = f * dot(s, h);
180        if !(0.0..=1.0).contains(&u) {
181            return None;
182        }
183        let q = cross(s, e1);
184        let v = f * dot(self.direction, q);
185        if v < 0.0 || u + v > 1.0 {
186            return None;
187        }
188        let t = f * dot(e2, q);
189        if t > eps { Some((t, u, v)) } else { None }
190    }
191}
192
193// ─────────────────────────────────────────────────────────────────────────────
194// Aabb3
195// ─────────────────────────────────────────────────────────────────────────────
196
197/// Axis-aligned bounding box in 3D.
198#[derive(Debug, Clone, Copy)]
199pub struct Aabb3 {
200    /// Minimum corner.
201    pub min: [f64; 3],
202    /// Maximum corner.
203    pub max: [f64; 3],
204}
205
206impl Aabb3 {
207    /// Create an AABB from min and max corners.
208    pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
209        Self { min, max }
210    }
211
212    /// Create an empty AABB (inverted infinite bounds).
213    pub fn empty() -> Self {
214        Self {
215            min: [f64::INFINITY; 3],
216            max: [f64::NEG_INFINITY; 3],
217        }
218    }
219
220    /// Center of the AABB.
221    pub fn center(&self) -> [f64; 3] {
222        scale(add(self.min, self.max), 0.5)
223    }
224
225    /// Half-extents.
226    pub fn half_extents(&self) -> [f64; 3] {
227        scale(sub(self.max, self.min), 0.5)
228    }
229
230    /// Check if a point is inside (inclusive).
231    pub fn contains_point(&self, p: [f64; 3]) -> bool {
232        p[0] >= self.min[0]
233            && p[0] <= self.max[0]
234            && p[1] >= self.min[1]
235            && p[1] <= self.max[1]
236            && p[2] >= self.min[2]
237            && p[2] <= self.max[2]
238    }
239
240    /// Check if two AABBs intersect.
241    pub fn intersects_aabb(&self, other: &Aabb3) -> bool {
242        self.min[0] <= other.max[0]
243            && self.max[0] >= other.min[0]
244            && self.min[1] <= other.max[1]
245            && self.max[1] >= other.min[1]
246            && self.min[2] <= other.max[2]
247            && self.max[2] >= other.min[2]
248    }
249
250    /// Check if an AABB intersects a sphere.
251    pub fn intersects_sphere(&self, center: [f64; 3], radius: f64) -> bool {
252        let closest = [
253            center[0].clamp(self.min[0], self.max[0]),
254            center[1].clamp(self.min[1], self.max[1]),
255            center[2].clamp(self.min[2], self.max[2]),
256        ];
257        len_sq(sub(closest, center)) <= radius * radius
258    }
259
260    /// Expand AABB by a margin.
261    pub fn expand(&self, margin: f64) -> Aabb3 {
262        Aabb3 {
263            min: [
264                self.min[0] - margin,
265                self.min[1] - margin,
266                self.min[2] - margin,
267            ],
268            max: [
269                self.max[0] + margin,
270                self.max[1] + margin,
271                self.max[2] + margin,
272            ],
273        }
274    }
275
276    /// Surface area.
277    pub fn surface_area(&self) -> f64 {
278        let e = sub(self.max, self.min);
279        2.0 * (e[0] * e[1] + e[1] * e[2] + e[2] * e[0])
280    }
281
282    /// Volume.
283    pub fn volume(&self) -> f64 {
284        let e = sub(self.max, self.min);
285        e[0] * e[1] * e[2]
286    }
287
288    /// Merge with another AABB.
289    pub fn merge(&self, other: &Aabb3) -> Aabb3 {
290        Aabb3 {
291            min: vmin(self.min, other.min),
292            max: vmax(self.max, other.max),
293        }
294    }
295}
296
297// ─────────────────────────────────────────────────────────────────────────────
298// Sphere3
299// ─────────────────────────────────────────────────────────────────────────────
300
301/// A sphere in 3D defined by center and radius.
302#[derive(Debug, Clone, Copy)]
303pub struct Sphere3 {
304    /// Center of the sphere.
305    pub center: [f64; 3],
306    /// Radius.
307    pub radius: f64,
308}
309
310impl Sphere3 {
311    /// Create a new sphere.
312    pub fn new(center: [f64; 3], radius: f64) -> Self {
313        Self { center, radius }
314    }
315
316    /// Check if a point is inside the sphere.
317    pub fn contains_point(&self, p: [f64; 3]) -> bool {
318        len_sq(sub(p, self.center)) <= self.radius * self.radius
319    }
320
321    /// Check if the sphere intersects an AABB.
322    pub fn intersects_aabb(&self, aabb: &Aabb3) -> bool {
323        aabb.intersects_sphere(self.center, self.radius)
324    }
325
326    /// Check if this sphere intersects another sphere.
327    pub fn intersects_sphere(&self, other: &Sphere3) -> bool {
328        let dist_sq = len_sq(sub(self.center, other.center));
329        let r_sum = self.radius + other.radius;
330        dist_sq <= r_sum * r_sum
331    }
332
333    /// Volume of the sphere.
334    pub fn volume(&self) -> f64 {
335        (4.0 / 3.0) * PI * self.radius.powi(3)
336    }
337
338    /// Surface area.
339    pub fn surface_area(&self) -> f64 {
340        4.0 * PI * self.radius * self.radius
341    }
342
343    /// Compute the AABB of this sphere.
344    pub fn aabb(&self) -> Aabb3 {
345        Aabb3 {
346            min: [
347                self.center[0] - self.radius,
348                self.center[1] - self.radius,
349                self.center[2] - self.radius,
350            ],
351            max: [
352                self.center[0] + self.radius,
353                self.center[1] + self.radius,
354                self.center[2] + self.radius,
355            ],
356        }
357    }
358}
359
360// ─────────────────────────────────────────────────────────────────────────────
361// Plane3
362// ─────────────────────────────────────────────────────────────────────────────
363
364/// An infinite plane in 3D: n·x = d.
365#[derive(Debug, Clone, Copy)]
366pub struct Plane3 {
367    /// Plane normal (unit vector).
368    pub normal: [f64; 3],
369    /// Plane offset: d = n·p for any point p on the plane.
370    pub offset: f64,
371}
372
373impl Plane3 {
374    /// Create from normal and offset.
375    pub fn new(normal: [f64; 3], offset: f64) -> Self {
376        Self {
377            normal: normalize(normal),
378            offset,
379        }
380    }
381
382    /// Create from three points.
383    pub fn from_points(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> Self {
384        let n = normalize(cross(sub(b, a), sub(c, a)));
385        let d = dot(n, a);
386        Self {
387            normal: n,
388            offset: d,
389        }
390    }
391
392    /// Signed distance from point to plane (positive on normal side).
393    pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
394        dot(self.normal, p) - self.offset
395    }
396
397    /// Project a point onto the plane.
398    pub fn project_point(&self, p: [f64; 3]) -> [f64; 3] {
399        let d = self.signed_distance(p);
400        sub(p, scale(self.normal, d))
401    }
402
403    /// Intersect ray with plane. Returns parameter `t` or `None` if parallel.
404    pub fn intersect_ray(&self, ray: &Ray3) -> Option<f64> {
405        let denom = dot(self.normal, ray.direction);
406        if denom.abs() < 1e-12 {
407            return None;
408        }
409        let t = (self.offset - dot(self.normal, ray.origin)) / denom;
410        if t >= 0.0 { Some(t) } else { None }
411    }
412
413    /// Intersect a line segment (a → b) with the plane.
414    /// Returns the intersection point or `None`.
415    pub fn intersect_segment(&self, a: [f64; 3], b: [f64; 3]) -> Option<[f64; 3]> {
416        let da = self.signed_distance(a);
417        let db = self.signed_distance(b);
418        if da * db > 0.0 {
419            return None; // Same side
420        }
421        let t = da / (da - db);
422        Some(add(a, scale(sub(b, a), t)))
423    }
424}
425
426// ─────────────────────────────────────────────────────────────────────────────
427// Triangle3
428// ─────────────────────────────────────────────────────────────────────────────
429
430/// A 3D triangle with three vertices.
431#[derive(Debug, Clone, Copy)]
432pub struct Triangle3 {
433    /// Vertices of the triangle.
434    pub v: [[f64; 3]; 3],
435}
436
437impl Triangle3 {
438    /// Create from three vertices.
439    pub fn new(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> Self {
440        Self { v: [v0, v1, v2] }
441    }
442
443    /// Outward normal (not normalized).
444    pub fn normal_raw(&self) -> [f64; 3] {
445        cross(sub(self.v[1], self.v[0]), sub(self.v[2], self.v[0]))
446    }
447
448    /// Unit normal.
449    pub fn normal(&self) -> [f64; 3] {
450        normalize(self.normal_raw())
451    }
452
453    /// Area of the triangle.
454    pub fn area(&self) -> f64 {
455        len(self.normal_raw()) * 0.5
456    }
457
458    /// Centroid.
459    pub fn centroid(&self) -> [f64; 3] {
460        scale(add(add(self.v[0], self.v[1]), self.v[2]), 1.0 / 3.0)
461    }
462
463    /// Compute barycentric coordinates (u, v, w) of point p projected onto triangle.
464    pub fn barycentric_coords(&self, p: [f64; 3]) -> (f64, f64, f64) {
465        let v0 = sub(self.v[1], self.v[0]);
466        let v1 = sub(self.v[2], self.v[0]);
467        let v2 = sub(p, self.v[0]);
468        let d00 = dot(v0, v0);
469        let d01 = dot(v0, v1);
470        let d11 = dot(v1, v1);
471        let d20 = dot(v2, v0);
472        let d21 = dot(v2, v1);
473        let denom = d00 * d11 - d01 * d01;
474        if denom.abs() < 1e-14 {
475            return (1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0);
476        }
477        let v_bc = (d11 * d20 - d01 * d21) / denom;
478        let w_bc = (d00 * d21 - d01 * d20) / denom;
479        let u_bc = 1.0 - v_bc - w_bc;
480        (u_bc, v_bc, w_bc)
481    }
482
483    /// Check if a point (projected) lies within the triangle (u, v, w ≥ 0).
484    pub fn contains_point_2d(&self, p: [f64; 3]) -> bool {
485        let (u, v, w) = self.barycentric_coords(p);
486        u >= -1e-8 && v >= -1e-8 && w >= -1e-8
487    }
488
489    /// Plane of the triangle.
490    pub fn plane(&self) -> Plane3 {
491        Plane3::from_points(self.v[0], self.v[1], self.v[2])
492    }
493}
494
495// ─────────────────────────────────────────────────────────────────────────────
496// Capsule3
497// ─────────────────────────────────────────────────────────────────────────────
498
499/// A 3D capsule: a swept sphere along a line segment.
500#[derive(Debug, Clone, Copy)]
501pub struct Capsule3 {
502    /// Start of the segment.
503    pub a: [f64; 3],
504    /// End of the segment.
505    pub b: [f64; 3],
506    /// Radius of the capsule.
507    pub radius: f64,
508}
509
510impl Capsule3 {
511    /// Create a new capsule.
512    pub fn new(a: [f64; 3], b: [f64; 3], radius: f64) -> Self {
513        Self { a, b, radius }
514    }
515
516    /// Closest point on the segment to an external point `p`.
517    pub fn closest_point_on_segment(&self, p: [f64; 3]) -> [f64; 3] {
518        closest_point_segment(self.a, self.b, p)
519    }
520
521    /// Distance from point `p` to the capsule surface.
522    pub fn distance_to_point(&self, p: [f64; 3]) -> f64 {
523        let closest = self.closest_point_on_segment(p);
524        (len(sub(p, closest)) - self.radius).max(0.0)
525    }
526
527    /// Check if a sphere intersects this capsule.
528    pub fn intersects_sphere(&self, center: [f64; 3], radius: f64) -> bool {
529        let closest = self.closest_point_on_segment(center);
530        let dist_sq = len_sq(sub(closest, center));
531        let r_sum = self.radius + radius;
532        dist_sq <= r_sum * r_sum
533    }
534
535    /// Length of the capsule segment.
536    pub fn segment_length(&self) -> f64 {
537        len(sub(self.b, self.a))
538    }
539}
540
541// ─────────────────────────────────────────────────────────────────────────────
542// OrientedBox
543// ─────────────────────────────────────────────────────────────────────────────
544
545/// An oriented bounding box (OBB) defined by center, local axes, and half-extents.
546#[derive(Debug, Clone, Copy)]
547pub struct OrientedBox {
548    /// Center position.
549    pub center: [f64; 3],
550    /// Local axes (column vectors): axes\[0\], axes\[1\], axes\[2\].
551    pub axes: [[f64; 3]; 3],
552    /// Half-extents along each axis.
553    pub half_extents: [f64; 3],
554}
555
556impl OrientedBox {
557    /// Create an oriented box.
558    pub fn new(center: [f64; 3], axes: [[f64; 3]; 3], half_extents: [f64; 3]) -> Self {
559        Self {
560            center,
561            axes: [normalize(axes[0]), normalize(axes[1]), normalize(axes[2])],
562            half_extents,
563        }
564    }
565
566    /// Check if a world-space point is inside the OBB.
567    pub fn contains_point(&self, p: [f64; 3]) -> bool {
568        let d = sub(p, self.center);
569        for i in 0..3 {
570            let proj = dot(d, self.axes[i]).abs();
571            if proj > self.half_extents[i] + 1e-8 {
572                return false;
573            }
574        }
575        true
576    }
577
578    /// Return the 8 corners of the OBB in world space.
579    pub fn corners(&self) -> [[f64; 3]; 8] {
580        let mut corners = [[0.0; 3]; 8];
581        for i in 0..8 {
582            let sx = if i & 1 == 0 { 1.0 } else { -1.0 };
583            let sy = if i & 2 == 0 { 1.0 } else { -1.0 };
584            let sz = if i & 4 == 0 { 1.0 } else { -1.0 };
585            let offset = add(
586                add(
587                    scale(self.axes[0], sx * self.half_extents[0]),
588                    scale(self.axes[1], sy * self.half_extents[1]),
589                ),
590                scale(self.axes[2], sz * self.half_extents[2]),
591            );
592            corners[i] = add(self.center, offset);
593        }
594        corners
595    }
596
597    /// Compute the AABB that contains this OBB.
598    pub fn intersects_aabb(&self, aabb: &Aabb3) -> bool {
599        // SAT: test 15 axes (3 AABB axes + 3 OBB axes + 9 cross products)
600        let obb_aabb = aabb_from_points(&self.corners());
601        obb_aabb.intersects_aabb(aabb)
602    }
603}
604
605// ─────────────────────────────────────────────────────────────────────────────
606// Frustum
607// ─────────────────────────────────────────────────────────────────────────────
608
609/// A view frustum defined by 6 planes: near, far, left, right, top, bottom.
610#[derive(Debug, Clone)]
611pub struct Frustum {
612    /// Six clipping planes: \[near, far, left, right, top, bottom\].
613    pub planes: [Plane3; 6],
614}
615
616impl Frustum {
617    /// Create a frustum from 6 planes.
618    pub fn new(planes: [Plane3; 6]) -> Self {
619        Self { planes }
620    }
621
622    /// Check if an AABB is at least partially inside the frustum.
623    pub fn contains_aabb(&self, aabb: &Aabb3) -> bool {
624        for plane in &self.planes {
625            // Find the positive vertex (most in the direction of the normal)
626            let positive = [
627                if plane.normal[0] >= 0.0 {
628                    aabb.max[0]
629                } else {
630                    aabb.min[0]
631                },
632                if plane.normal[1] >= 0.0 {
633                    aabb.max[1]
634                } else {
635                    aabb.min[1]
636                },
637                if plane.normal[2] >= 0.0 {
638                    aabb.max[2]
639                } else {
640                    aabb.min[2]
641                },
642            ];
643            if plane.signed_distance(positive) < 0.0 {
644                return false;
645            }
646        }
647        true
648    }
649
650    /// Check if a sphere is at least partially inside the frustum.
651    pub fn contains_sphere(&self, center: [f64; 3], radius: f64) -> bool {
652        for plane in &self.planes {
653            if plane.signed_distance(center) < -radius {
654                return false;
655            }
656        }
657        true
658    }
659}
660
661// ─────────────────────────────────────────────────────────────────────────────
662// PolyhedronConvex
663// ─────────────────────────────────────────────────────────────────────────────
664
665/// A convex polyhedron defined by vertices and face normals.
666#[derive(Debug, Clone)]
667pub struct PolyhedronConvex {
668    /// Vertex positions.
669    pub vertices: Vec<[f64; 3]>,
670    /// Outward-pointing face normals and a point on each face.
671    pub face_planes: Vec<Plane3>,
672}
673
674impl PolyhedronConvex {
675    /// Create a convex polyhedron.
676    pub fn new(vertices: Vec<[f64; 3]>, face_planes: Vec<Plane3>) -> Self {
677        Self {
678            vertices,
679            face_planes,
680        }
681    }
682
683    /// Support function: returns the vertex with the highest projection along `dir`.
684    pub fn support_point(&self, dir: [f64; 3]) -> [f64; 3] {
685        let mut best = self.vertices[0];
686        let mut best_d = dot(best, dir);
687        for &v in &self.vertices[1..] {
688            let d = dot(v, dir);
689            if d > best_d {
690                best = v;
691                best_d = d;
692            }
693        }
694        best
695    }
696
697    /// Check if a point is inside the convex polyhedron.
698    pub fn contains_point(&self, p: [f64; 3]) -> bool {
699        for plane in &self.face_planes {
700            if plane.signed_distance(p) > 1e-8 {
701                return false;
702            }
703        }
704        true
705    }
706
707    /// Build from convex hull faces (simplified: just store input).
708    pub fn from_box(half_extents: [f64; 3]) -> Self {
709        let [hx, hy, hz] = half_extents;
710        let vertices = vec![
711            [-hx, -hy, -hz],
712            [hx, -hy, -hz],
713            [-hx, hy, -hz],
714            [hx, hy, -hz],
715            [-hx, -hy, hz],
716            [hx, -hy, hz],
717            [-hx, hy, hz],
718            [hx, hy, hz],
719        ];
720        let face_planes = vec![
721            Plane3::new([1.0, 0.0, 0.0], hx),
722            Plane3::new([-1.0, 0.0, 0.0], hx),
723            Plane3::new([0.0, 1.0, 0.0], hy),
724            Plane3::new([0.0, -1.0, 0.0], hy),
725            Plane3::new([0.0, 0.0, 1.0], hz),
726            Plane3::new([0.0, 0.0, -1.0], hz),
727        ];
728        Self {
729            vertices,
730            face_planes,
731        }
732    }
733}
734
735// ─────────────────────────────────────────────────────────────────────────────
736// BarycentricCoords
737// ─────────────────────────────────────────────────────────────────────────────
738
739/// Barycentric coordinate utilities.
740#[derive(Debug, Clone, Copy)]
741pub struct BarycentricCoords {
742    /// First barycentric coordinate.
743    pub u: f64,
744    /// Second barycentric coordinate.
745    pub v: f64,
746    /// Third barycentric coordinate.
747    pub w: f64,
748}
749
750impl BarycentricCoords {
751    /// Compute barycentric coordinates of point `p` in triangle (v0,v1,v2).
752    pub fn compute(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], p: [f64; 3]) -> Self {
753        let tri = Triangle3::new(v0, v1, v2);
754        let (u, v, w) = tri.barycentric_coords(p);
755        Self { u, v, w }
756    }
757
758    /// Check if the barycentric coordinates represent a point inside the triangle.
759    pub fn is_inside(&self) -> bool {
760        self.u >= -1e-8 && self.v >= -1e-8 && self.w >= -1e-8
761    }
762
763    /// Interpolate a scalar value at the barycentric point.
764    pub fn interpolate_scalar(&self, s0: f64, s1: f64, s2: f64) -> f64 {
765        self.u * s0 + self.v * s1 + self.w * s2
766    }
767
768    /// Interpolate a 3D vector at the barycentric point.
769    pub fn interpolate_vec(&self, v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
770        add(add(scale(v0, self.u), scale(v1, self.v)), scale(v2, self.w))
771    }
772}
773
774// ─────────────────────────────────────────────────────────────────────────────
775// Helper Functions
776// ─────────────────────────────────────────────────────────────────────────────
777
778/// Find the closest point on a line segment (a → b) to point `p`.
779pub fn closest_point_segment(a: [f64; 3], b: [f64; 3], p: [f64; 3]) -> [f64; 3] {
780    let ab = sub(b, a);
781    let ap = sub(p, a);
782    let len_sq_ab = dot(ab, ab);
783    if len_sq_ab < 1e-14 {
784        return a;
785    }
786    let t = (dot(ap, ab) / len_sq_ab).clamp(0.0, 1.0);
787    add(a, scale(ab, t))
788}
789
790/// Find the closest point on a triangle to an external point `p`.
791pub fn closest_point_triangle(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], p: [f64; 3]) -> [f64; 3] {
792    let tri = Triangle3::new(v0, v1, v2);
793    let (u, v, w) = tri.barycentric_coords(p);
794    // If inside, project onto plane
795    if u >= 0.0 && v >= 0.0 && w >= 0.0 {
796        return add(add(scale(v0, u), scale(v1, v)), scale(v2, w));
797    }
798    // Otherwise, find closest point on each edge
799    let c0 = closest_point_segment(v0, v1, p);
800    let c1 = closest_point_segment(v1, v2, p);
801    let c2 = closest_point_segment(v2, v0, p);
802    let d0 = len_sq(sub(c0, p));
803    let d1 = len_sq(sub(c1, p));
804    let d2 = len_sq(sub(c2, p));
805    if d0 <= d1 && d0 <= d2 {
806        c0
807    } else if d1 <= d2 {
808        c1
809    } else {
810        c2
811    }
812}
813
814/// Build an AABB from a collection of points.
815pub fn aabb_from_points(points: &[[f64; 3]]) -> Aabb3 {
816    let mut aabb = Aabb3::empty();
817    for &p in points {
818        aabb.min = vmin(aabb.min, p);
819        aabb.max = vmax(aabb.max, p);
820    }
821    aabb
822}
823
824/// Build a frustum from a combined view-projection matrix (row-major, 4×4).
825/// Extracts the 6 Gribb-Hartmann planes.
826pub fn frustum_from_vp(m: &[[f64; 4]; 4]) -> Frustum {
827    // Each row contributes to a plane
828    let extract = |a: [f64; 4], b: [f64; 4]| -> Plane3 {
829        let n = [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
830        let d = -(a[3] + b[3]);
831        Plane3::new(n, -d)
832    };
833    let r = m;
834    let row = |i: usize| [r[i][0], r[i][1], r[i][2], r[i][3]];
835    let row3 = row(3);
836    // Left: row3 + row0, Right: row3 - row0, Bottom: row3 + row1
837    // Top: row3 - row1, Near: row3 + row2, Far: row3 - row2
838    let planes = [
839        extract(row3, row(0)),
840        extract(row3, [-row(0)[0], -row(0)[1], -row(0)[2], -row(0)[3]]),
841        extract(row3, row(1)),
842        extract(row3, [-row(1)[0], -row(1)[1], -row(1)[2], -row(1)[3]]),
843        extract(row3, row(2)),
844        extract(row3, [-row(2)[0], -row(2)[1], -row(2)[2], -row(2)[3]]),
845    ];
846    Frustum::new(planes)
847}
848
849/// Use PI for any angle-related computations.
850#[allow(dead_code)]
851const HALF_PI: f64 = PI / 2.0;
852
853// ─────────────────────────────────────────────────────────────────────────────
854// Tests
855// ─────────────────────────────────────────────────────────────────────────────
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860
861    // ── Ray3 tests ────────────────────────────────────────────────────────────
862
863    #[test]
864    fn ray_at_t_zero_is_origin() {
865        let ray = Ray3::new([1.0, 2.0, 3.0], [0.0, 0.0, 1.0]);
866        let p = ray.at(0.0);
867        assert!((p[0] - 1.0).abs() < 1e-10);
868        assert!((p[1] - 2.0).abs() < 1e-10);
869        assert!((p[2] - 3.0).abs() < 1e-10);
870    }
871
872    #[test]
873    fn ray_intersect_sphere_hit() {
874        let ray = Ray3::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
875        let t = ray.intersect_sphere([0.0, 0.0, 0.0], 1.0);
876        assert!(t.is_some(), "should hit");
877        assert!((t.unwrap() - 4.0).abs() < 1e-6, "t={}", t.unwrap());
878    }
879
880    #[test]
881    fn ray_intersect_sphere_miss() {
882        let ray = Ray3::new([5.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
883        let t = ray.intersect_sphere([0.0, 0.0, 0.0], 1.0);
884        assert!(t.is_none(), "should miss");
885    }
886
887    #[test]
888    fn ray_intersect_aabb_hit() {
889        let ray = Ray3::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
890        let aabb = Aabb3::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
891        let result = ray.intersect_aabb(&aabb);
892        assert!(result.is_some(), "should hit AABB");
893    }
894
895    #[test]
896    fn ray_intersect_aabb_miss() {
897        let ray = Ray3::new([5.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
898        let aabb = Aabb3::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
899        let result = ray.intersect_aabb(&aabb);
900        assert!(result.is_none(), "should miss AABB");
901    }
902
903    #[test]
904    fn ray_intersect_triangle_hit() {
905        let ray = Ray3::new([0.0, 0.0, -1.0], [0.0, 0.0, 1.0]);
906        let v0 = [-1.0, -1.0, 0.0];
907        let v1 = [1.0, -1.0, 0.0];
908        let v2 = [0.0, 1.0, 0.0];
909        let result = ray.intersect_triangle(v0, v1, v2);
910        assert!(result.is_some(), "should hit triangle");
911        let (t, _u, _v) = result.unwrap();
912        assert!((t - 1.0).abs() < 1e-6, "t={t}");
913    }
914
915    #[test]
916    fn ray_intersect_triangle_miss() {
917        let ray = Ray3::new([10.0, 0.0, -1.0], [0.0, 0.0, 1.0]);
918        let v0 = [-1.0, -1.0, 0.0];
919        let v1 = [1.0, -1.0, 0.0];
920        let v2 = [0.0, 1.0, 0.0];
921        let result = ray.intersect_triangle(v0, v1, v2);
922        assert!(result.is_none(), "should miss triangle");
923    }
924
925    // ── Aabb3 tests ───────────────────────────────────────────────────────────
926
927    #[test]
928    fn aabb_contains_center() {
929        let aabb = Aabb3::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
930        assert!(aabb.contains_point([0.0, 0.0, 0.0]));
931    }
932
933    #[test]
934    fn aabb_excludes_outside() {
935        let aabb = Aabb3::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
936        assert!(!aabb.contains_point([2.0, 0.0, 0.0]));
937    }
938
939    #[test]
940    fn aabb_intersects_overlapping() {
941        let a = Aabb3::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
942        let b = Aabb3::new([1.0, 1.0, 1.0], [3.0, 3.0, 3.0]);
943        assert!(a.intersects_aabb(&b));
944    }
945
946    #[test]
947    fn aabb_intersects_sphere() {
948        let aabb = Aabb3::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
949        assert!(aabb.intersects_sphere([1.0, 1.0, 1.0], 0.5));
950        assert!(!aabb.intersects_sphere([5.0, 5.0, 5.0], 0.5));
951    }
952
953    #[test]
954    fn aabb_surface_area_unit_cube() {
955        let aabb = Aabb3::new([0.0; 3], [1.0; 3]);
956        assert!((aabb.surface_area() - 6.0).abs() < 1e-10);
957    }
958
959    #[test]
960    fn aabb_volume_unit_cube() {
961        let aabb = Aabb3::new([0.0; 3], [1.0; 3]);
962        assert!((aabb.volume() - 1.0).abs() < 1e-10);
963    }
964
965    // ── Sphere3 tests ─────────────────────────────────────────────────────────
966
967    #[test]
968    fn sphere_contains_center() {
969        let s = Sphere3::new([0.0; 3], 1.0);
970        assert!(s.contains_point([0.0, 0.0, 0.0]));
971    }
972
973    #[test]
974    fn sphere_excludes_far_point() {
975        let s = Sphere3::new([0.0; 3], 1.0);
976        assert!(!s.contains_point([2.0, 0.0, 0.0]));
977    }
978
979    #[test]
980    fn sphere_sphere_intersection() {
981        let s1 = Sphere3::new([0.0; 3], 1.0);
982        let s2 = Sphere3::new([1.5, 0.0, 0.0], 1.0);
983        assert!(s1.intersects_sphere(&s2));
984        let s3 = Sphere3::new([3.0, 0.0, 0.0], 1.0);
985        assert!(!s1.intersects_sphere(&s3));
986    }
987
988    #[test]
989    fn sphere_volume() {
990        let s = Sphere3::new([0.0; 3], 1.0);
991        let expected = (4.0 / 3.0) * PI;
992        assert!((s.volume() - expected).abs() < 1e-10);
993    }
994
995    // ── Plane3 tests ──────────────────────────────────────────────────────────
996
997    #[test]
998    fn plane_signed_distance_positive_side() {
999        let p = Plane3::new([0.0, 1.0, 0.0], 0.0);
1000        assert!((p.signed_distance([0.0, 1.0, 0.0]) - 1.0).abs() < 1e-10);
1001    }
1002
1003    #[test]
1004    fn plane_signed_distance_negative_side() {
1005        let p = Plane3::new([0.0, 1.0, 0.0], 0.0);
1006        assert!((p.signed_distance([0.0, -1.0, 0.0]) + 1.0).abs() < 1e-10);
1007    }
1008
1009    #[test]
1010    fn plane_project_point() {
1011        let p = Plane3::new([0.0, 1.0, 0.0], 0.0);
1012        let proj = p.project_point([1.0, 5.0, 2.0]);
1013        assert!(proj[1].abs() < 1e-10, "proj.y={}", proj[1]);
1014    }
1015
1016    #[test]
1017    fn plane_intersect_ray() {
1018        let plane = Plane3::new([0.0, 1.0, 0.0], 0.0);
1019        let ray = Ray3::new([0.0, 5.0, 0.0], [0.0, -1.0, 0.0]);
1020        let t = plane.intersect_ray(&ray);
1021        assert!(t.is_some());
1022        assert!((t.unwrap() - 5.0).abs() < 1e-6, "t={}", t.unwrap());
1023    }
1024
1025    #[test]
1026    fn plane_from_points() {
1027        let p = Plane3::from_points([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1028        // Normal should be [0,0,1]
1029        assert!(p.normal[2].abs() > 0.99);
1030    }
1031
1032    // ── Triangle3 tests ───────────────────────────────────────────────────────
1033
1034    #[test]
1035    fn triangle_area_right_triangle() {
1036        let tri = Triangle3::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1037        assert!((tri.area() - 0.5).abs() < 1e-10, "area={}", tri.area());
1038    }
1039
1040    #[test]
1041    fn triangle_centroid() {
1042        let tri = Triangle3::new([0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]);
1043        let c = tri.centroid();
1044        assert!((c[0] - 1.0).abs() < 1e-10);
1045        assert!((c[1] - 1.0).abs() < 1e-10);
1046    }
1047
1048    #[test]
1049    fn triangle_barycentric_centroid() {
1050        let v0 = [0.0, 0.0, 0.0];
1051        let v1 = [3.0, 0.0, 0.0];
1052        let v2 = [0.0, 3.0, 0.0];
1053        let c = [1.0, 1.0, 0.0];
1054        let tri = Triangle3::new(v0, v1, v2);
1055        let (u, v, w) = tri.barycentric_coords(c);
1056        assert!((u + v + w - 1.0).abs() < 1e-6, "sum={}", u + v + w);
1057        assert!(u >= 0.0 && v >= 0.0 && w >= 0.0, "u={u}, v={v}, w={w}");
1058    }
1059
1060    // ── Capsule3 tests ────────────────────────────────────────────────────────
1061
1062    #[test]
1063    fn capsule_distance_to_surface() {
1064        let cap = Capsule3::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1065        let d = cap.distance_to_point([1.0, 0.5, 0.0]);
1066        assert!((d - 0.5).abs() < 1e-6, "d={d}");
1067    }
1068
1069    #[test]
1070    fn capsule_inside_sphere_intersection() {
1071        let cap = Capsule3::new([0.0, 0.0, 0.0], [0.0, 2.0, 0.0], 0.5);
1072        assert!(cap.intersects_sphere([0.0, 1.0, 0.0], 0.3));
1073    }
1074
1075    #[test]
1076    fn capsule_no_intersection() {
1077        let cap = Capsule3::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.1);
1078        assert!(!cap.intersects_sphere([5.0, 0.5, 0.0], 0.1));
1079    }
1080
1081    // ── OrientedBox tests ─────────────────────────────────────────────────────
1082
1083    #[test]
1084    fn obb_contains_center() {
1085        let obb = OrientedBox::new(
1086            [0.0; 3],
1087            [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1088            [1.0, 1.0, 1.0],
1089        );
1090        assert!(obb.contains_point([0.0, 0.0, 0.0]));
1091    }
1092
1093    #[test]
1094    fn obb_excludes_far_point() {
1095        let obb = OrientedBox::new(
1096            [0.0; 3],
1097            [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1098            [1.0, 1.0, 1.0],
1099        );
1100        assert!(!obb.contains_point([2.0, 0.0, 0.0]));
1101    }
1102
1103    #[test]
1104    fn obb_corners_count() {
1105        let obb = OrientedBox::new(
1106            [0.0; 3],
1107            [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1108            [1.0, 1.0, 1.0],
1109        );
1110        let corners = obb.corners();
1111        assert_eq!(corners.len(), 8);
1112    }
1113
1114    // ── Frustum tests ─────────────────────────────────────────────────────────
1115
1116    #[test]
1117    fn frustum_contains_center_sphere() {
1118        // Create a box frustum
1119        let planes = [
1120            Plane3::new([1.0, 0.0, 0.0], -10.0),
1121            Plane3::new([-1.0, 0.0, 0.0], -10.0),
1122            Plane3::new([0.0, 1.0, 0.0], -10.0),
1123            Plane3::new([0.0, -1.0, 0.0], -10.0),
1124            Plane3::new([0.0, 0.0, 1.0], -10.0),
1125            Plane3::new([0.0, 0.0, -1.0], -10.0),
1126        ];
1127        let frustum = Frustum::new(planes);
1128        assert!(frustum.contains_sphere([0.0, 0.0, 0.0], 0.5));
1129    }
1130
1131    // ── PolyhedronConvex tests ────────────────────────────────────────────────
1132
1133    #[test]
1134    fn polyhedron_box_contains_center() {
1135        let poly = PolyhedronConvex::from_box([1.0, 1.0, 1.0]);
1136        assert!(poly.contains_point([0.0, 0.0, 0.0]));
1137    }
1138
1139    #[test]
1140    fn polyhedron_box_excludes_outside() {
1141        let poly = PolyhedronConvex::from_box([1.0, 1.0, 1.0]);
1142        assert!(!poly.contains_point([2.0, 0.0, 0.0]));
1143    }
1144
1145    #[test]
1146    fn polyhedron_support_point() {
1147        let poly = PolyhedronConvex::from_box([1.0, 1.0, 1.0]);
1148        let sp = poly.support_point([1.0, 0.0, 0.0]);
1149        assert!((sp[0] - 1.0).abs() < 1e-10, "support_x={}", sp[0]);
1150    }
1151
1152    // ── BarycentricCoords tests ───────────────────────────────────────────────
1153
1154    #[test]
1155    fn barycentric_vertex_is_canonical() {
1156        let bc = BarycentricCoords::compute(
1157            [1.0, 0.0, 0.0],
1158            [0.0, 1.0, 0.0],
1159            [0.0, 0.0, 1.0],
1160            [1.0, 0.0, 0.0],
1161        );
1162        assert!((bc.u - 1.0).abs() < 1e-6, "u={}", bc.u);
1163        assert!(bc.v.abs() < 1e-6, "v={}", bc.v);
1164        assert!(bc.w.abs() < 1e-6, "w={}", bc.w);
1165    }
1166
1167    #[test]
1168    fn barycentric_interpolate_scalar() {
1169        let bc = BarycentricCoords {
1170            u: 0.5,
1171            v: 0.3,
1172            w: 0.2,
1173        };
1174        let val = bc.interpolate_scalar(1.0, 2.0, 3.0);
1175        // 0.5*1 + 0.3*2 + 0.2*3 = 0.5 + 0.6 + 0.6 = 1.7
1176        assert!((val - 1.7).abs() < 1e-10, "val={val}");
1177    }
1178
1179    // ── Helper function tests ─────────────────────────────────────────────────
1180
1181    #[test]
1182    fn closest_point_segment_midpoint() {
1183        let a = [0.0, 0.0, 0.0];
1184        let b = [2.0, 0.0, 0.0];
1185        let p = [1.0, 1.0, 0.0];
1186        let cp = closest_point_segment(a, b, p);
1187        assert!((cp[0] - 1.0).abs() < 1e-10);
1188        assert!(cp[1].abs() < 1e-10);
1189    }
1190
1191    #[test]
1192    fn closest_point_segment_clamp_a() {
1193        let a = [0.0, 0.0, 0.0];
1194        let b = [1.0, 0.0, 0.0];
1195        let p = [-5.0, 0.0, 0.0];
1196        let cp = closest_point_segment(a, b, p);
1197        assert!((cp[0] - 0.0).abs() < 1e-10);
1198    }
1199
1200    #[test]
1201    fn aabb_from_points_simple() {
1202        let points = [[0.0, 0.0, 0.0], [1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]];
1203        let aabb = aabb_from_points(&points);
1204        assert!((aabb.min[0] + 1.0).abs() < 1e-10);
1205        assert!((aabb.max[2] - 3.0).abs() < 1e-10);
1206    }
1207
1208    #[test]
1209    fn aabb_expand_increases_bounds() {
1210        let aabb = Aabb3::new([0.0; 3], [1.0; 3]);
1211        let expanded = aabb.expand(0.5);
1212        assert!((expanded.min[0] + 0.5).abs() < 1e-10);
1213        assert!((expanded.max[0] - 1.5).abs() < 1e-10);
1214    }
1215
1216    #[test]
1217    fn sphere_aabb_correct_bounds() {
1218        let s = Sphere3::new([1.0, 2.0, 3.0], 1.0);
1219        let aabb = s.aabb();
1220        assert!((aabb.min[0] - 0.0).abs() < 1e-10);
1221        assert!((aabb.max[0] - 2.0).abs() < 1e-10);
1222    }
1223}