Skip to main content

oxiphysics_core/math/
linear_algebra.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6pub use nalgebra::{Matrix3, Matrix4, Point3, Unit, UnitQuaternion, Vector3, Vector4};
7
8use super::geometry::{quat_exp, quat_log};
9use super::types::*;
10
11/// Scalar type used throughout the engine (f64 by default).
12pub type Real = f64;
13/// 3D vector type alias.
14pub type Vec3 = Vector3<Real>;
15/// 4D vector type alias.
16pub type Vec4 = Vector4<Real>;
17/// 3x3 matrix type alias.
18pub type Mat3 = Matrix3<Real>;
19/// 4x4 matrix type alias.
20pub type Mat4 = Matrix4<Real>;
21/// Quaternion type alias.
22pub type Quat = UnitQuaternion<Real>;
23/// Create a quaternion from an axis-angle representation.
24///
25/// `axis` must be a unit vector. `angle` is in radians.
26#[allow(dead_code)]
27pub fn quat_from_axis_angle(axis: &Vec3, angle: Real) -> Quat {
28    UnitQuaternion::from_axis_angle(&Unit::new_normalize(*axis), angle)
29}
30/// Convert a quaternion to Euler angles (roll, pitch, yaw) in radians.
31///
32/// Uses the ZYX (yaw-pitch-roll) convention.
33/// Returns `(roll, pitch, yaw)`.
34#[allow(dead_code)]
35pub fn quat_to_euler(q: &Quat) -> (Real, Real, Real) {
36    let (roll, pitch, yaw) = q.euler_angles();
37    (roll, pitch, yaw)
38}
39/// Create a quaternion from Euler angles (roll, pitch, yaw) in radians.
40///
41/// Uses the ZYX convention: rotation = Rz(yaw) * Ry(pitch) * Rx(roll).
42#[allow(dead_code)]
43pub fn quat_from_euler(roll: Real, pitch: Real, yaw: Real) -> Quat {
44    UnitQuaternion::from_euler_angles(roll, pitch, yaw)
45}
46/// Spherical linear interpolation between two quaternions.
47#[allow(dead_code)]
48pub fn quat_slerp(a: &Quat, b: &Quat, t: Real) -> Quat {
49    a.slerp(b, t)
50}
51/// Compute the determinant of a 3x3 matrix.
52#[allow(dead_code)]
53pub fn mat3_determinant(m: &Mat3) -> Real {
54    m.determinant()
55}
56/// Compute the inverse of a 3x3 matrix.
57///
58/// Returns `None` if the matrix is singular.
59#[allow(dead_code)]
60pub fn mat3_inverse(m: &Mat3) -> Option<Mat3> {
61    m.try_inverse()
62}
63/// Compute real eigenvalues of a symmetric 3x3 matrix.
64///
65/// Returns eigenvalues sorted in ascending order. For non-symmetric matrices
66/// the result is an approximation.
67#[allow(dead_code)]
68pub fn mat3_eigenvalues_symmetric(m: &Mat3) -> [Real; 3] {
69    let eigen = m.symmetric_eigen();
70    let mut vals = [
71        eigen.eigenvalues[0],
72        eigen.eigenvalues[1],
73        eigen.eigenvalues[2],
74    ];
75    vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
76    vals
77}
78/// Compute the trace of a 3x3 matrix.
79#[allow(dead_code)]
80pub fn mat3_trace(m: &Mat3) -> Real {
81    m.trace()
82}
83/// Compute the Frobenius norm of a 3x3 matrix.
84#[allow(dead_code)]
85pub fn mat3_frobenius_norm(m: &Mat3) -> Real {
86    let mut sum = 0.0;
87    for i in 0..3 {
88        for j in 0..3 {
89            sum += m[(i, j)] * m[(i, j)];
90        }
91    }
92    sum.sqrt()
93}
94/// Build a skew-symmetric matrix from a vector (for cross product as matrix multiply).
95#[allow(dead_code)]
96pub fn skew_symmetric(v: &Vec3) -> Mat3 {
97    Mat3::new(0.0, -v.z, v.y, v.z, 0.0, -v.x, -v.y, v.x, 0.0)
98}
99/// Create a perspective projection matrix (right-handed, depth \[0, 1\]).
100///
101/// * `fov_y` - Vertical field of view in radians.
102/// * `aspect` - Width / height ratio.
103/// * `near` - Near clipping plane distance (positive).
104/// * `far` - Far clipping plane distance (positive).
105#[allow(dead_code)]
106pub fn perspective(fov_y: Real, aspect: Real, near: Real, far: Real) -> Mat4 {
107    let f = 1.0 / (fov_y / 2.0).tan();
108    let nf = 1.0 / (near - far);
109    Mat4::new(
110        f / aspect,
111        0.0,
112        0.0,
113        0.0,
114        0.0,
115        f,
116        0.0,
117        0.0,
118        0.0,
119        0.0,
120        (far + near) * nf,
121        2.0 * far * near * nf,
122        0.0,
123        0.0,
124        -1.0,
125        0.0,
126    )
127}
128/// Create a look-at view matrix (right-handed).
129///
130/// * `eye` - Camera position.
131/// * `target` - Look-at target position.
132/// * `up` - World up direction.
133#[allow(dead_code)]
134pub fn look_at(eye: &Vec3, target: &Vec3, up: &Vec3) -> Mat4 {
135    let f = (target - eye).normalize();
136    let s = f.cross(up).normalize();
137    let u = s.cross(&f);
138    Mat4::new(
139        s.x,
140        s.y,
141        s.z,
142        -s.dot(eye),
143        u.x,
144        u.y,
145        u.z,
146        -u.dot(eye),
147        -f.x,
148        -f.y,
149        -f.z,
150        f.dot(eye),
151        0.0,
152        0.0,
153        0.0,
154        1.0,
155    )
156}
157/// Create an orthographic projection matrix (right-handed).
158#[allow(dead_code, clippy::too_many_arguments)]
159pub fn orthographic(
160    left: Real,
161    right: Real,
162    bottom: Real,
163    top: Real,
164    near: Real,
165    far: Real,
166) -> Mat4 {
167    let rml = right - left;
168    let tmb = top - bottom;
169    let fmn = far - near;
170    Mat4::new(
171        2.0 / rml,
172        0.0,
173        0.0,
174        -(right + left) / rml,
175        0.0,
176        2.0 / tmb,
177        0.0,
178        -(top + bottom) / tmb,
179        0.0,
180        0.0,
181        -2.0 / fmn,
182        -(far + near) / fmn,
183        0.0,
184        0.0,
185        0.0,
186        1.0,
187    )
188}
189/// Create a Vec4 from components.
190#[allow(dead_code)]
191pub fn vec4(x: Real, y: Real, z: Real, w: Real) -> Vec4 {
192    Vec4::new(x, y, z, w)
193}
194/// Homogeneous point (w=1).
195#[allow(dead_code)]
196pub fn vec4_point(v: &Vec3) -> Vec4 {
197    Vec4::new(v.x, v.y, v.z, 1.0)
198}
199/// Homogeneous direction (w=0).
200#[allow(dead_code)]
201pub fn vec4_direction(v: &Vec3) -> Vec4 {
202    Vec4::new(v.x, v.y, v.z, 0.0)
203}
204/// Project a Vec4 back to Vec3 by dividing by w.
205///
206/// Returns `None` if w is near zero.
207#[allow(dead_code)]
208pub fn vec4_to_vec3(v: &Vec4) -> Option<Vec3> {
209    if v.w.abs() < 1e-10 {
210        return None;
211    }
212    Some(Vec3::new(v.x / v.w, v.y / v.w, v.z / v.w))
213}
214/// Compute the intersection line of two planes.
215///
216/// Returns `None` if planes are parallel.
217/// On success, returns `(point_on_line, direction)`.
218#[allow(dead_code)]
219pub fn plane_plane_intersection(a: &Plane, b: &Plane) -> Option<(Vec3, Vec3)> {
220    let dir = a.normal.cross(&b.normal);
221    let len_sq = dir.norm_squared();
222    if len_sq < 1e-10 {
223        return None;
224    }
225    let p = (b.normal.cross(&dir) * a.distance + dir.cross(&a.normal) * b.distance) / len_sq;
226    Some((p, dir.normalize()))
227}
228/// Compute the intersection point of three planes.
229///
230/// Returns `None` if any two planes are parallel or all three meet in a line.
231#[allow(dead_code)]
232pub fn three_plane_intersection(a: &Plane, b: &Plane, c: &Plane) -> Option<Vec3> {
233    let denom = a.normal.dot(&b.normal.cross(&c.normal));
234    if denom.abs() < 1e-10 {
235        return None;
236    }
237    let p = (b.normal.cross(&c.normal) * a.distance
238        + c.normal.cross(&a.normal) * b.distance
239        + a.normal.cross(&b.normal) * c.distance)
240        / denom;
241    Some(p)
242}
243/// Compute the polar decomposition of a 3×3 matrix `M = R * S` where `R` is
244/// orthogonal (rotation) and `S` is symmetric positive semi-definite.
245///
246/// Uses iterative polar decomposition: `R_{k+1} = 0.5 * (R_k + (R_k^{-T}))`.
247/// Returns `(R, S)` after convergence or `max_iter` iterations.
248///
249/// Returns `None` if the matrix is singular.
250#[allow(dead_code)]
251pub fn polar_decomposition(m: &Mat3, max_iter: usize) -> Option<(Mat3, Mat3)> {
252    let mut r = *m;
253    for _ in 0..max_iter {
254        let r_inv_t = r.try_inverse()?.transpose();
255        let r_new = (r + r_inv_t) * 0.5;
256        let diff = (r_new - r).norm();
257        r = r_new;
258        if diff < 1e-12 {
259            break;
260        }
261    }
262    let s = r.transpose() * m;
263    Some((r, s))
264}
265/// Compute eigenvalues and eigenvectors of a real symmetric 3×3 matrix using
266/// the Jacobi iteration method.
267///
268/// Returns `(eigenvalues, eigenvectors)` where each column of `eigenvectors` is
269/// an eigenvector corresponding to the eigenvalue at the same index.
270/// Eigenvalues are *not* sorted.
271#[allow(dead_code)]
272pub fn symmetric_eigen3(m: &Mat3) -> ([Real; 3], Mat3) {
273    let mut a = *m;
274    let mut v = Mat3::identity();
275    for _ in 0..100 {
276        let mut max_val = 0.0_f64;
277        let mut p = 0;
278        let mut q = 1;
279        for i in 0..3 {
280            for j in (i + 1)..3 {
281                if a[(i, j)].abs() > max_val {
282                    max_val = a[(i, j)].abs();
283                    p = i;
284                    q = j;
285                }
286            }
287        }
288        if max_val < 1e-12 {
289            break;
290        }
291        let theta = (a[(q, q)] - a[(p, p)]) / (2.0 * a[(p, q)]);
292        let t = if theta >= 0.0 {
293            1.0 / (theta + (1.0 + theta * theta).sqrt())
294        } else {
295            1.0 / (theta - (1.0 + theta * theta).sqrt())
296        };
297        let cos = 1.0 / (1.0 + t * t).sqrt();
298        let sin = t * cos;
299        let a_pp = a[(p, p)];
300        let a_qq = a[(q, q)];
301        let a_pq = a[(p, q)];
302        a[(p, p)] = cos * cos * a_pp - 2.0 * sin * cos * a_pq + sin * sin * a_qq;
303        a[(q, q)] = sin * sin * a_pp + 2.0 * sin * cos * a_pq + cos * cos * a_qq;
304        a[(p, q)] = 0.0;
305        a[(q, p)] = 0.0;
306        for r in 0..3 {
307            if r != p && r != q {
308                let a_rp = a[(r, p)];
309                let a_rq = a[(r, q)];
310                a[(r, p)] = cos * a_rp - sin * a_rq;
311                a[(p, r)] = a[(r, p)];
312                a[(r, q)] = sin * a_rp + cos * a_rq;
313                a[(q, r)] = a[(r, q)];
314            }
315        }
316        for r in 0..3 {
317            let v_rp = v[(r, p)];
318            let v_rq = v[(r, q)];
319            v[(r, p)] = cos * v_rp - sin * v_rq;
320            v[(r, q)] = sin * v_rp + cos * v_rq;
321        }
322    }
323    ([a[(0, 0)], a[(1, 1)], a[(2, 2)]], v)
324}
325/// Evaluate real spherical harmonic Y_0^0 (degree 0, order 0).
326///
327/// Y_0^0 = 1 / (2 * sqrt(Ï€))
328#[allow(dead_code)]
329pub fn sh_y00() -> Real {
330    0.5 / std::f64::consts::PI.sqrt()
331}
332/// Evaluate real spherical harmonic Y_1^{-1} (degree 1, order -1).
333///
334/// Y_1^{-1}(θ,φ) = sqrt(3/(4π)) * sin(θ) * sin(φ) = sqrt(3/(4π)) * y/r
335#[allow(dead_code)]
336pub fn sh_y1m1(dir: &Vec3) -> Real {
337    let len = dir.norm();
338    if len < 1e-12 {
339        return 0.0;
340    }
341    let y = dir.y / len;
342    (3.0 / (4.0 * std::f64::consts::PI)).sqrt() * y
343}
344/// Evaluate real spherical harmonic Y_1^0 (degree 1, order 0).
345///
346/// Y_1^0(θ,φ) = sqrt(3/(4π)) * cos(θ) = sqrt(3/(4π)) * z/r
347#[allow(dead_code)]
348pub fn sh_y10(dir: &Vec3) -> Real {
349    let len = dir.norm();
350    if len < 1e-12 {
351        return 0.0;
352    }
353    let z = dir.z / len;
354    (3.0 / (4.0 * std::f64::consts::PI)).sqrt() * z
355}
356/// Evaluate real spherical harmonic Y_1^1 (degree 1, order 1).
357///
358/// Y_1^1(θ,φ) = sqrt(3/(4π)) * sin(θ) * cos(φ) = sqrt(3/(4π)) * x/r
359#[allow(dead_code)]
360pub fn sh_y11(dir: &Vec3) -> Real {
361    let len = dir.norm();
362    if len < 1e-12 {
363        return 0.0;
364    }
365    let x = dir.x / len;
366    (3.0 / (4.0 * std::f64::consts::PI)).sqrt() * x
367}
368/// Project a radiance function sampled at `n` uniformly-spread directions onto
369/// the 4 L0+L1 SH coefficients \[c00, c1m1, c10, c11\].
370///
371/// `radiance_fn` takes a unit direction and returns the sampled value.
372/// Directions are constructed from a uniform icosahedron-inspired distribution.
373#[allow(dead_code)]
374pub fn sh_project_l1(radiance_fn: impl Fn(&Vec3) -> Real, n: usize) -> [Real; 4] {
375    let golden = (1.0 + 5.0_f64.sqrt()) / 2.0;
376    let mut coeffs = [0.0_f64; 4];
377    let weight = 4.0 * std::f64::consts::PI / n as Real;
378    for i in 0..n {
379        let theta = (1.0 - 2.0 * (i as Real + 0.5) / n as Real).acos();
380        let phi = 2.0 * std::f64::consts::PI * (i as Real / golden).fract();
381        let dir = Vec3::new(
382            theta.sin() * phi.cos(),
383            theta.sin() * phi.sin(),
384            theta.cos(),
385        );
386        let rad = radiance_fn(&dir);
387        coeffs[0] += rad * sh_y00() * weight;
388        coeffs[1] += rad * sh_y1m1(&dir) * weight;
389        coeffs[2] += rad * sh_y10(&dir) * weight;
390        coeffs[3] += rad * sh_y11(&dir) * weight;
391    }
392    coeffs
393}
394/// Compute the derivative of `f` at `x` using dual numbers.
395///
396/// `f` must be a function `Dual -> Dual`.
397#[allow(dead_code)]
398pub fn dual_differentiate(f: impl Fn(Dual) -> Dual, x: Real) -> Real {
399    f(Dual::variable(x)).du
400}
401/// Hermite spline interpolation between two points `p0` and `p1` with
402/// tangents `m0` and `m1`.  Parameter `t` runs from 0 (at p0) to 1 (at p1).
403#[allow(dead_code)]
404pub fn hermite_interpolate(p0: &Vec3, m0: &Vec3, p1: &Vec3, m1: &Vec3, t: Real) -> Vec3 {
405    let t2 = t * t;
406    let t3 = t2 * t;
407    let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
408    let h10 = t3 - 2.0 * t2 + t;
409    let h01 = -2.0 * t3 + 3.0 * t2;
410    let h11 = t3 - t2;
411    p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11
412}
413/// Catmull-Rom spline interpolation.
414///
415/// Interpolates between `p1` and `p2` using the four control points
416/// `p0, p1, p2, p3`.  `t` ∈ \[0, 1\].
417#[allow(dead_code)]
418pub fn catmull_rom(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3, t: Real) -> Vec3 {
419    let m1 = (p2 - p0) * 0.5;
420    let m2 = (p3 - p1) * 0.5;
421    hermite_interpolate(p1, &m1, p2, &m2, t)
422}
423/// Cubic Bézier curve.
424///
425/// Evaluates at parameter `t` ∈ \[0, 1\] given control points `p0..p3`.
426#[allow(dead_code)]
427pub fn bezier_cubic(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3, t: Real) -> Vec3 {
428    let mt = 1.0 - t;
429    p0 * (mt * mt * mt) + p1 * (3.0 * mt * mt * t) + p2 * (3.0 * mt * t * t) + p3 * (t * t * t)
430}
431/// B-spline basis functions for a clamped uniform cubic B-spline (degree 3).
432///
433/// `i` is the span index, `t` ∈ \[0, 1\] within the span.
434/// Returns the four non-zero basis function values `[N_{i-3}, N_{i-2}, N_{i-1}, N_i]`.
435#[allow(dead_code)]
436pub fn bspline_basis3(t: Real) -> [Real; 4] {
437    let t2 = t * t;
438    let t3 = t2 * t;
439    let mt = 1.0 - t;
440    [
441        mt * mt * mt / 6.0,
442        (3.0 * t3 - 6.0 * t2 + 4.0) / 6.0,
443        (-3.0 * t3 + 3.0 * t2 + 3.0 * t + 1.0) / 6.0,
444        t3 / 6.0,
445    ]
446}
447/// Transpose a 3×3 matrix stored as `[[f64;3\];3]` (row-major).
448#[allow(dead_code)]
449pub fn mat3_transpose(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
450    [
451        [m[0][0], m[1][0], m[2][0]],
452        [m[0][1], m[1][1], m[2][1]],
453        [m[0][2], m[1][2], m[2][2]],
454    ]
455}
456/// Multiply a 3×3 matrix by a 3-vector: `m * v`.
457#[allow(dead_code)]
458pub fn mat3_mul_vec3(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
459    [
460        m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
461        m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
462        m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
463    ]
464}
465/// Multiply two 3×3 matrices: `a * b`.
466#[allow(dead_code)]
467pub fn mat3_mul_mat3(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
468    let mut r = [[0.0_f64; 3]; 3];
469    for i in 0..3 {
470        for j in 0..3 {
471            for k in 0..3 {
472                r[i][j] += a[i][k] * b[k][j];
473            }
474        }
475    }
476    r
477}
478/// Determinant of a 3×3 matrix.
479#[allow(dead_code)]
480pub fn mat3_det(m: [[f64; 3]; 3]) -> f64 {
481    m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
482        - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
483        + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
484}
485/// Inverse of a 3×3 matrix.  Returns `None` if singular (|det| < 1e-14).
486#[allow(dead_code)]
487pub fn mat3_arr_inverse(m: [[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
488    let det = mat3_det(m);
489    if det.abs() < 1e-14 {
490        return None;
491    }
492    let inv_det = 1.0 / det;
493    Some([
494        [
495            (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
496            (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
497            (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
498        ],
499        [
500            (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
501            (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
502            (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
503        ],
504        [
505            (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
506            (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
507            (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
508        ],
509    ])
510}
511/// Build a 3×3 matrix from three column vectors.
512#[allow(dead_code)]
513pub fn mat3_from_cols(c0: [f64; 3], c1: [f64; 3], c2: [f64; 3]) -> [[f64; 3]; 3] {
514    [
515        [c0[0], c1[0], c2[0]],
516        [c0[1], c1[1], c2[1]],
517        [c0[2], c1[2], c2[2]],
518    ]
519}
520/// Outer product of two 3-vectors: `a ⊗ b` (a 3×3 matrix).
521#[allow(dead_code)]
522pub fn mat3_outer_product(a: [f64; 3], b: [f64; 3]) -> [[f64; 3]; 3] {
523    [
524        [a[0] * b[0], a[0] * b[1], a[0] * b[2]],
525        [a[1] * b[0], a[1] * b[1], a[1] * b[2]],
526        [a[2] * b[0], a[2] * b[1], a[2] * b[2]],
527    ]
528}
529/// Create a unit quaternion `[x, y, z, w]` from an axis-angle pair.
530///
531/// `axis` should be a unit vector; `angle` is in radians.
532#[allow(dead_code)]
533pub fn quat_arr_from_axis_angle(axis: [f64; 3], angle: f64) -> [f64; 4] {
534    let half = angle * 0.5;
535    let s = half.sin();
536    [axis[0] * s, axis[1] * s, axis[2] * s, half.cos()]
537}
538/// Multiply two quaternions `p * q` (Hamilton product).  Format: `[x, y, z, w]`.
539#[allow(dead_code)]
540pub fn quat_multiply(p: [f64; 4], q: [f64; 4]) -> [f64; 4] {
541    let [px, py, pz, pw] = p;
542    let [qx, qy, qz, qw] = q;
543    [
544        pw * qx + px * qw + py * qz - pz * qy,
545        pw * qy - px * qz + py * qw + pz * qx,
546        pw * qz + px * qy - py * qx + pz * qw,
547        pw * qw - px * qx - py * qy - pz * qz,
548    ]
549}
550/// Convert a unit quaternion `[x, y, z, w]` to a 3×3 rotation matrix (row-major).
551#[allow(dead_code)]
552pub fn quat_to_mat3(q: [f64; 4]) -> [[f64; 3]; 3] {
553    let [x, y, z, w] = q;
554    let x2 = x * x;
555    let y2 = y * y;
556    let z2 = z * z;
557    let xy = x * y;
558    let xz = x * z;
559    let yz = y * z;
560    let wx = w * x;
561    let wy = w * y;
562    let wz = w * z;
563    [
564        [1.0 - 2.0 * (y2 + z2), 2.0 * (xy - wz), 2.0 * (xz + wy)],
565        [2.0 * (xy + wz), 1.0 - 2.0 * (x2 + z2), 2.0 * (yz - wx)],
566        [2.0 * (xz - wy), 2.0 * (yz + wx), 1.0 - 2.0 * (x2 + y2)],
567    ]
568}
569/// Spherical linear interpolation between two unit quaternions (format: `[x,y,z,w]`).
570#[allow(dead_code)]
571pub fn quat_arr_slerp(p: [f64; 4], q: [f64; 4], t: f64) -> [f64; 4] {
572    let dot = p[0] * q[0] + p[1] * q[1] + p[2] * q[2] + p[3] * q[3];
573    let (q2, dot2) = if dot < 0.0 {
574        ([-q[0], -q[1], -q[2], -q[3]], -dot)
575    } else {
576        (q, dot)
577    };
578    let dot2 = dot2.clamp(-1.0, 1.0);
579    if dot2 > 0.9995 {
580        let r = [
581            p[0] + t * (q2[0] - p[0]),
582            p[1] + t * (q2[1] - p[1]),
583            p[2] + t * (q2[2] - p[2]),
584            p[3] + t * (q2[3] - p[3]),
585        ];
586        let norm = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2] + r[3] * r[3]).sqrt();
587        [r[0] / norm, r[1] / norm, r[2] / norm, r[3] / norm]
588    } else {
589        let theta = dot2.acos();
590        let sin_theta = theta.sin();
591        let s0 = ((1.0 - t) * theta).sin() / sin_theta;
592        let s1 = (t * theta).sin() / sin_theta;
593        [
594            s0 * p[0] + s1 * q2[0],
595            s0 * p[1] + s1 * q2[1],
596            s0 * p[2] + s1 * q2[2],
597            s0 * p[3] + s1 * q2[3],
598        ]
599    }
600}
601/// Project vector `a` onto `onto`.  Returns the zero vector if `onto` is zero.
602#[allow(dead_code)]
603pub fn vec3_project(a: [f64; 3], onto: [f64; 3]) -> [f64; 3] {
604    let denom = onto[0] * onto[0] + onto[1] * onto[1] + onto[2] * onto[2];
605    if denom < 1e-30 {
606        return [0.0; 3];
607    }
608    let s = (a[0] * onto[0] + a[1] * onto[1] + a[2] * onto[2]) / denom;
609    [onto[0] * s, onto[1] * s, onto[2] * s]
610}
611/// Reflect vector `v` about unit normal `n`: `v - 2*(v·n)*n`.
612#[allow(dead_code)]
613pub fn vec3_reflect(v: [f64; 3], n: [f64; 3]) -> [f64; 3] {
614    let dot2 = 2.0 * (v[0] * n[0] + v[1] * n[1] + v[2] * n[2]);
615    [v[0] - dot2 * n[0], v[1] - dot2 * n[1], v[2] - dot2 * n[2]]
616}
617/// Linear interpolation between two 3-vectors: `a + t*(b-a)`.
618#[allow(dead_code)]
619pub fn vec3_lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
620    [
621        a[0] + t * (b[0] - a[0]),
622        a[1] + t * (b[1] - a[1]),
623        a[2] + t * (b[2] - a[2]),
624    ]
625}
626/// Create a plane `[nx, ny, nz, d]` from a point `p` and unit normal `n`.
627///
628/// The plane equation is `n·x = d` (equivalently `n·x - d = 0`).
629#[allow(dead_code)]
630pub fn plane_from_point_normal(p: [f64; 3], n: [f64; 3]) -> [f64; 4] {
631    let d = n[0] * p[0] + n[1] * p[1] + n[2] * p[2];
632    [n[0], n[1], n[2], d]
633}
634/// Signed distance from `point` to the plane `[nx, ny, nz, d]`.
635///
636/// Positive on the side the normal points toward.
637#[allow(dead_code)]
638pub fn plane_signed_dist(plane: [f64; 4], point: [f64; 3]) -> f64 {
639    plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] - plane[3]
640}
641/// Arc length of a cubic Bézier curve approximated with `n` segments.
642#[allow(dead_code)]
643pub fn bezier_arc_length(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3, n: usize) -> Real {
644    let mut len = 0.0;
645    let mut prev = *p0;
646    for i in 1..=n {
647        let t = i as Real / n as Real;
648        let pt = bezier_cubic(p0, p1, p2, p3, t);
649        len += (pt - prev).norm();
650        prev = pt;
651    }
652    len
653}
654/// Cross product of two 3-vectors: `a × b`.
655#[allow(dead_code)]
656pub fn vec3_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
657    [
658        a[1] * b[2] - a[2] * b[1],
659        a[2] * b[0] - a[0] * b[2],
660        a[0] * b[1] - a[1] * b[0],
661    ]
662}
663/// Scalar triple product: `a · (b × c)`.
664///
665/// Equals the signed volume of the parallelepiped spanned by `a`, `b`, `c`.
666#[allow(dead_code)]
667pub fn vec3_triple_product(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
668    let bc = vec3_cross(b, c);
669    a[0] * bc[0] + a[1] * bc[1] + a[2] * bc[2]
670}
671/// Rotate vector `v` around unit `axis` by `angle` radians (Rodrigues' formula).
672///
673/// `axis` must be a unit vector; the result has the same magnitude as `v`.
674#[allow(dead_code)]
675pub fn vec3_rotate_by_angle(v: [f64; 3], axis: [f64; 3], angle: f64) -> [f64; 3] {
676    let cos_a = angle.cos();
677    let sin_a = angle.sin();
678    let dot = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
679    let cross = vec3_cross(axis, v);
680    [
681        cos_a * v[0] + sin_a * cross[0] + (1.0 - cos_a) * dot * axis[0],
682        cos_a * v[1] + sin_a * cross[1] + (1.0 - cos_a) * dot * axis[1],
683        cos_a * v[2] + sin_a * cross[2] + (1.0 - cos_a) * dot * axis[2],
684    ]
685}
686/// Adjugate (classical adjoint) of a 3×3 matrix: `adj(M) = det(M) * M^{-1}`.
687///
688/// The adjugate exists even for singular matrices.
689#[allow(dead_code)]
690pub fn mat3_adjugate(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
691    let c = |r: usize, c_idx: usize| -> f64 {
692        let rows: Vec<usize> = (0..3).filter(|&x| x != r).collect();
693        let cols: Vec<usize> = (0..3).filter(|&x| x != c_idx).collect();
694        let det2 =
695            m[rows[0]][cols[0]] * m[rows[1]][cols[1]] - m[rows[0]][cols[1]] * m[rows[1]][cols[0]];
696        let sign = if (r + c_idx).is_multiple_of(2) {
697            1.0
698        } else {
699            -1.0
700        };
701        sign * det2
702    };
703    [
704        [c(0, 0), c(1, 0), c(2, 0)],
705        [c(0, 1), c(1, 1), c(2, 1)],
706        [c(0, 2), c(1, 2), c(2, 2)],
707    ]
708}
709/// Verify the Cayley-Hamilton theorem for a 3×3 matrix M.
710///
711/// Computes `p(M) = M^3 - tr(M)*M^2 + ((tr(M)^2 - tr(M^2))/2)*M - det(M)*I`
712/// and returns the result (should be the zero matrix by Cayley-Hamilton).
713#[allow(dead_code)]
714pub fn mat3_cayley_hamilton(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
715    let i3 = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
716    let tr_m = m[0][0] + m[1][1] + m[2][2];
717    let m2 = mat3_mul_mat3(m, m);
718    let m3 = mat3_mul_mat3(m2, m);
719    let tr_m2 = m2[0][0] + m2[1][1] + m2[2][2];
720    let c2 = (tr_m * tr_m - tr_m2) * 0.5;
721    let det = mat3_det(m);
722    let mut result = [[0.0_f64; 3]; 3];
723    for i in 0..3 {
724        for j in 0..3 {
725            result[i][j] = m3[i][j] - tr_m * m2[i][j] + c2 * m[i][j] - det * i3[i][j];
726        }
727    }
728    result
729}
730/// Quaternion logarithm for a unit quaternion `q = [x, y, z, w]`.
731///
732/// `log(q) = [θ * n̂, 0]` where `θ = acos(w)` and `n̂ = (x,y,z)/sin(θ)`.
733/// For the identity quaternion the result is the zero vector.
734#[allow(dead_code)]
735pub fn quat_arr_log(q: [f64; 4]) -> [f64; 4] {
736    let [x, y, z, w] = q;
737    let w_clamped = w.clamp(-1.0, 1.0);
738    let theta = w_clamped.acos();
739    let sin_theta = theta.sin();
740    if sin_theta.abs() < 1e-10 {
741        return [0.0, 0.0, 0.0, 0.0];
742    }
743    let s = theta / sin_theta;
744    [x * s, y * s, z * s, 0.0]
745}
746/// Quaternion exponential for a pure quaternion `v = [x, y, z, 0]`.
747///
748/// `exp(v) = [sin(|v|)*v̂, cos(|v|)]`.  If `v = 0` the identity is returned.
749#[allow(dead_code)]
750pub fn quat_arr_exp(v: [f64; 4]) -> [f64; 4] {
751    let [x, y, z, _] = v;
752    let theta = (x * x + y * y + z * z).sqrt();
753    if theta < 1e-15 {
754        return [0.0, 0.0, 0.0, 1.0];
755    }
756    let s = theta.sin() / theta;
757    [x * s, y * s, z * s, theta.cos()]
758}
759/// Convert a unit quaternion `[x, y, z, w]` to `(axis, angle)`.
760///
761/// Returns `([0,1,0], 0)` for the identity quaternion.
762#[allow(dead_code)]
763pub fn quat_to_axis_angle(q: [f64; 4]) -> ([f64; 3], f64) {
764    let [x, y, z, w] = q;
765    let w_c = w.clamp(-1.0, 1.0);
766    let angle = 2.0 * w_c.acos();
767    let sin_half = (1.0 - w_c * w_c).sqrt();
768    if sin_half < 1e-10 {
769        return ([0.0, 1.0, 0.0], 0.0);
770    }
771    ([x / sin_half, y / sin_half, z / sin_half], angle)
772}
773/// SQUAD (Spherical Quadrangle interpolation) for smooth quaternion paths.
774///
775/// Interpolates between `q1` and `q2` at parameter `t ∈ [0,1]` using the
776/// surrounding control quaternions `s1` and `s2`.
777#[allow(dead_code)]
778pub fn quat_squad(q1: [f64; 4], q2: [f64; 4], s1: [f64; 4], s2: [f64; 4], t: f64) -> [f64; 4] {
779    let slerp_q = quat_arr_slerp(q1, q2, t);
780    let slerp_s = quat_arr_slerp(s1, s2, t);
781    quat_arr_slerp(slerp_q, slerp_s, 2.0 * t * (1.0 - t))
782}
783#[cfg(test)]
784mod proptest_tests {
785
786    use crate::Vec3;
787
788    use crate::math::types::{Plane, Ray};
789
790    use proptest::prelude::*;
791    fn coord() -> impl Strategy<Value = f64> {
792        -1e3_f64..1e3_f64
793    }
794    fn pos_coord() -> impl Strategy<Value = f64> {
795        0.01_f64..100.0_f64
796    }
797    fn vec3() -> impl Strategy<Value = Vec3> {
798        (coord(), coord(), coord()).prop_map(|(x, y, z)| Vec3::new(x, y, z))
799    }
800    fn nonzero_vec3() -> impl Strategy<Value = Vec3> {
801        (coord(), coord(), coord())
802            .prop_filter("norm must be nonzero", |(x, y, z)| {
803                (x * x + y * y + z * z).sqrt() > 1e-6
804            })
805            .prop_map(|(x, y, z)| Vec3::new(x, y, z))
806    }
807    proptest! {
808        #[test] fn prop_vec3_dot_commutative(a in vec3(), b in vec3()) { let ab = a.dot(&
809        b); let ba = b.dot(& a); prop_assert!((ab - ba).abs() < 1e-10,
810        "dot not commutative: {} vs {}", ab, ba); } #[test] fn
811        prop_vec3_cross_anti_commutative(a in vec3(), b in vec3()) { let ab = a.cross(&
812        b); let ba = b.cross(& a); let diff = (ab + ba).norm(); prop_assert!(diff < 1e-6,
813        "cross not anti-commutative: diff norm={}", diff); } #[test] fn
814        prop_vec3_triangle_inequality(a in vec3(), b in vec3()) { let sum_norm = (a + b)
815        .norm(); let norm_sum = a.norm() + b.norm(); prop_assert!(sum_norm <= norm_sum +
816        1e-10, "triangle inequality violated: |a+b|={} > |a|+|b|={}", sum_norm,
817        norm_sum); } #[test] fn prop_vec3_normalize_unit(v in nonzero_vec3()) { let n = v
818        .normalize(); let len = n.norm(); prop_assert!((len - 1.0).abs() < 1e-10,
819        "normalized vector has norm {} != 1", len); } #[test] fn
820        prop_ray_point_at_parametric(ox in coord(), oy in coord(), oz in coord(), dx in
821        coord(), dy in coord(), dz in coord(), t in 0.0_f64..10.0_f64,) { let origin =
822        Vec3::new(ox, oy, oz); let direction = Vec3::new(dx, dy, dz); let ray =
823        Ray::new(origin, direction); let p = ray.point_at(t); let expected = origin +
824        direction * t; let diff = (p - expected).norm(); prop_assert!(diff < 1e-10,
825        "ray.point_at mismatch: diff={}", diff); } #[test] fn
826        prop_plane_signed_distance_positive_side(nx in pos_coord(), ny in pos_coord(), nz
827        in pos_coord(), d in coord(), s in 0.01_f64..10.0_f64,) { let n = Vec3::new(nx,
828        ny, nz).normalize(); let plane = Plane::new(n, d); let point = n * (d + s); let
829        dist = plane.signed_distance(& point); prop_assert!((dist - s).abs() < 1e-6,
830        "expected signed distance {}, got {}", s, dist); }
831    }
832}
833#[cfg(test)]
834mod tests {
835    use super::*;
836    use crate::Mat3;
837    use crate::Vec3;
838    use crate::differential_geometry::mat3_det;
839    use crate::differential_geometry::mat3_mul_vec3;
840    use crate::differential_geometry::mat3_transpose;
841    use crate::math::Vec4;
842    use crate::math::bezier_arc_length;
843    use crate::math::bezier_cubic;
844    use crate::math::bspline_basis3;
845    use crate::math::dual_differentiate;
846    use crate::math::hermite_interpolate;
847    use crate::math::look_at;
848    use crate::math::mat3_adjugate;
849    use crate::math::mat3_arr_inverse;
850    use crate::math::mat3_cayley_hamilton;
851    use crate::math::mat3_determinant;
852    use crate::math::mat3_eigenvalues_symmetric;
853    use crate::math::mat3_inverse;
854    use crate::math::mat3_mul_mat3;
855    use crate::math::mat3_outer_product;
856    use crate::math::orthographic;
857    use crate::math::perspective;
858    use crate::math::plane_from_point_normal;
859    use crate::math::plane_plane_intersection;
860    use crate::math::plane_signed_dist;
861    use crate::math::polar_decomposition;
862    use crate::math::quat_arr_exp;
863    use crate::math::quat_arr_from_axis_angle;
864    use crate::math::quat_arr_log;
865    use crate::math::quat_arr_slerp;
866    use crate::math::quat_from_axis_angle;
867    use crate::math::quat_from_euler;
868    use crate::math::quat_multiply;
869    use crate::math::quat_slerp;
870    use crate::math::quat_to_axis_angle;
871    use crate::math::quat_to_euler;
872    use crate::math::quat_to_mat3;
873    use crate::math::sh_project_l1;
874    use crate::math::sh_y00;
875    use crate::math::sh_y10;
876    use crate::math::skew_symmetric;
877    use crate::math::symmetric_eigen3;
878    use crate::math::three_plane_intersection;
879    use crate::math::types::{Aabb, Dual, Frustum, Plane, Ray};
880    use crate::math::vec3_cross;
881    use crate::math::vec3_lerp;
882    use crate::math::vec3_project;
883    use crate::math::vec3_reflect;
884    use crate::math::vec3_rotate_by_angle;
885    use crate::math::vec3_triple_product;
886    use crate::math::vec4_direction;
887    use crate::math::vec4_point;
888    use crate::math::vec4_to_vec3;
889    #[test]
890    fn test_ray_point_at() {
891        let ray = Ray::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
892        let p = ray.point_at(3.0);
893        assert!((p.x - 3.0).abs() < 1e-10);
894        assert!(p.y.abs() < 1e-10);
895    }
896    #[test]
897    fn test_plane_signed_distance() {
898        let plane = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
899        assert!((plane.signed_distance(&Vec3::new(0.0, 5.0, 0.0)) - 5.0).abs() < 1e-10);
900        assert!((plane.signed_distance(&Vec3::new(0.0, -3.0, 0.0)) + 3.0).abs() < 1e-10);
901    }
902    #[test]
903    fn test_ray_plane_intersection() {
904        let ray = Ray::new(Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, -1.0, 0.0));
905        let plane = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
906        let t = ray.intersect_plane(&plane).expect("should intersect");
907        assert!((t - 1.0).abs() < 1e-10, "expected t=1.0, got {}", t);
908        let hit = ray.point_at(t);
909        assert!(hit.y.abs() < 1e-10);
910    }
911    #[test]
912    fn test_ray_plane_miss() {
913        let ray = Ray::new(Vec3::new(0.0, 1.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
914        let plane = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
915        assert!(ray.intersect_plane(&plane).is_none());
916    }
917    #[test]
918    fn test_plane_project() {
919        let plane = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
920        let point = Vec3::new(3.0, 5.0, 7.0);
921        let d = plane.signed_distance(&point);
922        let projected = point - plane.normal * d;
923        assert!(plane.signed_distance(&projected).abs() < 1e-10);
924    }
925    #[test]
926    fn test_quat_from_axis_angle_identity() {
927        let q = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.0);
928        let (roll, pitch, yaw) = quat_to_euler(&q);
929        assert!(roll.abs() < 1e-10);
930        assert!(pitch.abs() < 1e-10);
931        assert!(yaw.abs() < 1e-10);
932    }
933    #[test]
934    fn test_quat_from_axis_angle_90_degrees() {
935        let q = quat_from_axis_angle(&Vec3::new(0.0, 0.0, 1.0), std::f64::consts::FRAC_PI_2);
936        let v = q.transform_vector(&Vec3::new(1.0, 0.0, 0.0));
937        assert!((v.x).abs() < 1e-10, "x={}", v.x);
938        assert!((v.y - 1.0).abs() < 1e-10, "y={}", v.y);
939        assert!((v.z).abs() < 1e-10, "z={}", v.z);
940    }
941    #[test]
942    fn test_quat_euler_roundtrip() {
943        let roll = 0.3;
944        let pitch = 0.5;
945        let yaw = 0.7;
946        let q = quat_from_euler(roll, pitch, yaw);
947        let (r2, p2, y2) = quat_to_euler(&q);
948        assert!((r2 - roll).abs() < 1e-10, "roll: {} vs {}", r2, roll);
949        assert!((p2 - pitch).abs() < 1e-10, "pitch: {} vs {}", p2, pitch);
950        assert!((y2 - yaw).abs() < 1e-10, "yaw: {} vs {}", y2, yaw);
951    }
952    #[test]
953    fn test_quat_slerp_endpoints() {
954        let a = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.0);
955        let b = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 1.0);
956        let q0 = quat_slerp(&a, &b, 0.0);
957        let q1 = quat_slerp(&a, &b, 1.0);
958        assert!(q0.angle_to(&a) < 1e-10);
959        assert!(q1.angle_to(&b) < 1e-10);
960    }
961    #[test]
962    fn test_quat_slerp_midpoint() {
963        let a = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.0);
964        let b = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 1.0);
965        let mid = quat_slerp(&a, &b, 0.5);
966        let expected = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.5);
967        assert!(mid.angle_to(&expected) < 1e-10);
968    }
969    #[test]
970    fn test_mat3_determinant_identity() {
971        let m = Mat3::identity();
972        assert!((mat3_determinant(&m) - 1.0).abs() < 1e-10);
973    }
974    #[test]
975    fn test_mat3_determinant_scaled() {
976        let m = Mat3::identity() * 2.0;
977        assert!((mat3_determinant(&m) - 8.0).abs() < 1e-10);
978    }
979    #[test]
980    fn test_mat3_inverse_identity() {
981        let m = Mat3::identity();
982        let inv = mat3_inverse(&m).expect("identity should be invertible");
983        let diff = (inv - Mat3::identity()).norm();
984        assert!(diff < 1e-10);
985    }
986    #[test]
987    fn test_mat3_inverse_product_is_identity() {
988        let m = Mat3::new(1.0, 2.0, 3.0, 0.0, 1.0, 4.0, 5.0, 6.0, 0.0);
989        let inv = mat3_inverse(&m).expect("should be invertible");
990        let product = m * inv;
991        let diff = (product - Mat3::identity()).norm();
992        assert!(diff < 1e-8, "M * M^-1 should be identity, diff={}", diff);
993    }
994    #[test]
995    fn test_mat3_singular_no_inverse() {
996        let m = Mat3::new(1.0, 2.0, 3.0, 2.0, 4.0, 6.0, 1.0, 1.0, 1.0);
997        assert!(mat3_inverse(&m).is_none());
998    }
999    #[test]
1000    fn test_mat3_eigenvalues_diagonal() {
1001        let m = Mat3::new(3.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0);
1002        let eigs = mat3_eigenvalues_symmetric(&m);
1003        assert!((eigs[0] - 1.0).abs() < 1e-10);
1004        assert!((eigs[1] - 2.0).abs() < 1e-10);
1005        assert!((eigs[2] - 3.0).abs() < 1e-10);
1006    }
1007    #[test]
1008    fn test_mat3_trace() {
1009        let m = Mat3::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
1010        assert!((mat3_trace(&m) - 15.0).abs() < 1e-10);
1011    }
1012    #[test]
1013    fn test_mat3_frobenius_norm_identity() {
1014        let m = Mat3::identity();
1015        assert!((mat3_frobenius_norm(&m) - 3.0_f64.sqrt()).abs() < 1e-10);
1016    }
1017    #[test]
1018    fn test_skew_symmetric_cross_product() {
1019        let a = Vec3::new(1.0, 2.0, 3.0);
1020        let b = Vec3::new(4.0, 5.0, 6.0);
1021        let cross_direct = a.cross(&b);
1022        let cross_matrix = skew_symmetric(&a) * b;
1023        let diff = (cross_direct - cross_matrix).norm();
1024        assert!(
1025            diff < 1e-10,
1026            "skew_symmetric cross product mismatch: diff={}",
1027            diff
1028        );
1029    }
1030    #[test]
1031    fn test_perspective_nonzero() {
1032        let m = perspective(std::f64::consts::FRAC_PI_4, 16.0 / 9.0, 0.1, 100.0);
1033        assert!(m.norm() > 0.0);
1034        assert!((m[(3, 0)]).abs() < 1e-10);
1035        assert!((m[(3, 1)]).abs() < 1e-10);
1036        assert!((m[(3, 2)] + 1.0).abs() < 1e-10);
1037        assert!((m[(3, 3)]).abs() < 1e-10);
1038    }
1039    #[test]
1040    fn test_look_at_origin() {
1041        let eye = Vec3::new(0.0, 0.0, 5.0);
1042        let target = Vec3::new(0.0, 0.0, 0.0);
1043        let up = Vec3::new(0.0, 1.0, 0.0);
1044        let m = look_at(&eye, &target, &up);
1045        let p = m * vec4_point(&Vec3::zeros());
1046        let v3 = vec4_to_vec3(&p).unwrap();
1047        assert!((v3.x).abs() < 1e-8, "x={}", v3.x);
1048        assert!((v3.y).abs() < 1e-8, "y={}", v3.y);
1049        assert!((v3.z + 5.0).abs() < 1e-8, "z={}", v3.z);
1050    }
1051    #[test]
1052    fn test_orthographic_identity_like() {
1053        let m = orthographic(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);
1054        let p = m * vec4_point(&Vec3::zeros());
1055        let v3 = vec4_to_vec3(&p).unwrap();
1056        assert!(v3.norm() < 1e-8);
1057    }
1058    #[test]
1059    fn test_vec4_point_w_is_one() {
1060        let p = vec4_point(&Vec3::new(1.0, 2.0, 3.0));
1061        assert!((p.w - 1.0).abs() < 1e-10);
1062    }
1063    #[test]
1064    fn test_vec4_direction_w_is_zero() {
1065        let d = vec4_direction(&Vec3::new(1.0, 0.0, 0.0));
1066        assert!(d.w.abs() < 1e-10);
1067    }
1068    #[test]
1069    fn test_vec4_to_vec3_perspective_divide() {
1070        let v = Vec4::new(4.0, 6.0, 8.0, 2.0);
1071        let v3 = vec4_to_vec3(&v).unwrap();
1072        assert!((v3.x - 2.0).abs() < 1e-10);
1073        assert!((v3.y - 3.0).abs() < 1e-10);
1074        assert!((v3.z - 4.0).abs() < 1e-10);
1075    }
1076    #[test]
1077    fn test_vec4_to_vec3_w_zero_returns_none() {
1078        let v = Vec4::new(1.0, 2.0, 3.0, 0.0);
1079        assert!(vec4_to_vec3(&v).is_none());
1080    }
1081    #[test]
1082    fn test_plane_from_points() {
1083        let a = Vec3::new(0.0, 0.0, 0.0);
1084        let b = Vec3::new(1.0, 0.0, 0.0);
1085        let c = Vec3::new(0.0, 1.0, 0.0);
1086        let plane = Plane::from_points(&a, &b, &c).unwrap();
1087        assert!((plane.normal.z.abs() - 1.0).abs() < 1e-10);
1088        assert!(plane.signed_distance(&a).abs() < 1e-10);
1089        assert!(plane.signed_distance(&b).abs() < 1e-10);
1090        assert!(plane.signed_distance(&c).abs() < 1e-10);
1091    }
1092    #[test]
1093    fn test_plane_from_collinear_returns_none() {
1094        let a = Vec3::new(0.0, 0.0, 0.0);
1095        let b = Vec3::new(1.0, 0.0, 0.0);
1096        let c = Vec3::new(2.0, 0.0, 0.0);
1097        assert!(Plane::from_points(&a, &b, &c).is_none());
1098    }
1099    #[test]
1100    fn test_plane_project_point() {
1101        let plane = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
1102        let point = Vec3::new(3.0, 5.0, 7.0);
1103        let projected = plane.project_point(&point);
1104        assert!(plane.signed_distance(&projected).abs() < 1e-10);
1105        assert!((projected.x - 3.0).abs() < 1e-10);
1106        assert!((projected.z - 7.0).abs() < 1e-10);
1107    }
1108    #[test]
1109    fn test_plane_reflect_point() {
1110        let plane = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
1111        let point = Vec3::new(3.0, 5.0, 7.0);
1112        let reflected = plane.reflect_point(&point);
1113        assert!((reflected.x - 3.0).abs() < 1e-10);
1114        assert!((reflected.y + 5.0).abs() < 1e-10);
1115        assert!((reflected.z - 7.0).abs() < 1e-10);
1116    }
1117    #[test]
1118    fn test_plane_classify_point() {
1119        let plane = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
1120        assert_eq!(plane.classify_point(&Vec3::new(0.0, 1.0, 0.0), 1e-6), 1);
1121        assert_eq!(plane.classify_point(&Vec3::new(0.0, -1.0, 0.0), 1e-6), -1);
1122        assert_eq!(plane.classify_point(&Vec3::new(0.0, 0.0, 0.0), 1e-6), 0);
1123    }
1124    #[test]
1125    fn test_plane_plane_intersection() {
1126        let a = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
1127        let b = Plane::new(Vec3::new(1.0, 0.0, 0.0), 0.0);
1128        let (point, dir) = plane_plane_intersection(&a, &b).unwrap();
1129        assert!(a.signed_distance(&point).abs() < 1e-8);
1130        assert!(b.signed_distance(&point).abs() < 1e-8);
1131        assert!((dir.z.abs() - 1.0).abs() < 1e-8, "dir={:?}", dir);
1132    }
1133    #[test]
1134    fn test_plane_plane_parallel_returns_none() {
1135        let a = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
1136        let b = Plane::new(Vec3::new(0.0, 1.0, 0.0), 5.0);
1137        assert!(plane_plane_intersection(&a, &b).is_none());
1138    }
1139    #[test]
1140    fn test_three_plane_intersection() {
1141        let a = Plane::new(Vec3::new(1.0, 0.0, 0.0), 1.0);
1142        let b = Plane::new(Vec3::new(0.0, 1.0, 0.0), 2.0);
1143        let c = Plane::new(Vec3::new(0.0, 0.0, 1.0), 3.0);
1144        let p = three_plane_intersection(&a, &b, &c).unwrap();
1145        assert!((p.x - 1.0).abs() < 1e-8);
1146        assert!((p.y - 2.0).abs() < 1e-8);
1147        assert!((p.z - 3.0).abs() < 1e-8);
1148    }
1149    #[test]
1150    fn test_polar_decomposition_identity() {
1151        let m = Mat3::identity();
1152        let (r, s) = polar_decomposition(&m, 100).expect("identity is invertible");
1153        let diff_r = (r - Mat3::identity()).norm();
1154        let diff_s = (s - Mat3::identity()).norm();
1155        assert!(diff_r < 1e-8, "R should be identity: diff={}", diff_r);
1156        assert!(diff_s < 1e-8, "S should be identity: diff={}", diff_s);
1157    }
1158    #[test]
1159    fn test_polar_decomposition_rotation_only() {
1160        let q = quat_from_axis_angle(&Vec3::new(0.0, 0.0, 1.0), std::f64::consts::FRAC_PI_4);
1161        let m = *q.to_rotation_matrix().matrix();
1162        let (r, s) = polar_decomposition(&m, 100).expect("rotation is invertible");
1163        let product = r * s;
1164        let diff = (product - m).norm();
1165        assert!(diff < 1e-6, "R*S should equal M: diff={}", diff);
1166        let s_diff = (s - Mat3::identity()).norm();
1167        assert!(s_diff < 1e-6, "S should be near identity: diff={}", s_diff);
1168    }
1169    #[test]
1170    fn test_polar_decomposition_product_equals_m() {
1171        let m = Mat3::new(1.0, 0.5, 0.0, 0.0, 1.0, 0.3, 0.0, 0.0, 1.0);
1172        let (r, s) = polar_decomposition(&m, 100).expect("M should be invertible");
1173        let product = r * s;
1174        let diff = (product - m).norm();
1175        assert!(diff < 1e-6, "R*S should equal M: diff={}", diff);
1176    }
1177    #[test]
1178    fn test_symmetric_eigen3_diagonal() {
1179        let m = Mat3::new(3.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0);
1180        let (eigs, _) = symmetric_eigen3(&m);
1181        let mut sorted = eigs;
1182        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
1183        assert!((sorted[0] - 1.0).abs() < 1e-8, "eig0={}", sorted[0]);
1184        assert!((sorted[1] - 2.0).abs() < 1e-8, "eig1={}", sorted[1]);
1185        assert!((sorted[2] - 3.0).abs() < 1e-8, "eig2={}", sorted[2]);
1186    }
1187    #[test]
1188    fn test_symmetric_eigen3_eigenvectors_orthonormal() {
1189        let m = Mat3::new(4.0, 1.0, 0.0, 1.0, 3.0, 0.0, 0.0, 0.0, 2.0);
1190        let (_, v) = symmetric_eigen3(&m);
1191        for i in 0..3 {
1192            let col_i = v.column(i);
1193            let norm = col_i.norm();
1194            assert!((norm - 1.0).abs() < 1e-6, "col {} norm={}", i, norm);
1195            for j in (i + 1)..3 {
1196                let col_j = v.column(j);
1197                let dot = col_i.dot(&col_j);
1198                assert!(
1199                    dot.abs() < 1e-6,
1200                    "cols {} {} not orthogonal: dot={}",
1201                    i,
1202                    j,
1203                    dot
1204                );
1205            }
1206        }
1207    }
1208    #[test]
1209    fn test_symmetric_eigen3_reconstruction() {
1210        let m = Mat3::new(2.0, 1.0, 0.0, 1.0, 3.0, 0.0, 0.0, 0.0, 5.0);
1211        let (eigs, v) = symmetric_eigen3(&m);
1212        let diag = Mat3::from_diagonal(&nalgebra::Vector3::new(eigs[0], eigs[1], eigs[2]));
1213        let reconstructed = v * diag * v.transpose();
1214        let diff = (reconstructed - m).norm();
1215        assert!(diff < 1e-6, "reconstruction diff={}", diff);
1216    }
1217    #[test]
1218    fn test_sh_y00_constant() {
1219        let expected = 0.5 / std::f64::consts::PI.sqrt();
1220        assert!((sh_y00() - expected).abs() < 1e-12);
1221    }
1222    #[test]
1223    fn test_sh_y1_orthogonal_to_constant() {
1224        let n = 1000;
1225        let golden = (1.0 + 5.0_f64.sqrt()) / 2.0;
1226        let mut sum_y10 = 0.0_f64;
1227        for i in 0..n {
1228            let theta = (1.0 - 2.0 * (i as f64 + 0.5) / n as f64).acos();
1229            let phi = 2.0 * std::f64::consts::PI * ((i as f64) / golden).fract();
1230            let dir = Vec3::new(
1231                theta.sin() * phi.cos(),
1232                theta.sin() * phi.sin(),
1233                theta.cos(),
1234            );
1235            sum_y10 += sh_y10(&dir);
1236        }
1237        let avg = sum_y10 / n as f64;
1238        assert!(avg.abs() < 0.01, "Y_1^0 average should be ~0, got {}", avg);
1239    }
1240    #[test]
1241    fn test_sh_project_l1_uniform_radiance() {
1242        let coeffs = sh_project_l1(|_| 1.0, 1000);
1243        let expected_c00 = 2.0 * std::f64::consts::PI.sqrt();
1244        assert!((coeffs[0] - expected_c00).abs() < 0.1, "c00={}", coeffs[0]);
1245        for &c in &coeffs[1..] {
1246            assert!(c.abs() < 0.1, "L1 coeff should be ~0 for uniform: {}", c);
1247        }
1248    }
1249    #[test]
1250    fn test_dual_add() {
1251        let a = Dual::new(3.0, 1.0);
1252        let b = Dual::new(2.0, 0.5);
1253        let c = a + b;
1254        assert!((c.re - 5.0).abs() < 1e-12);
1255        assert!((c.du - 1.5).abs() < 1e-12);
1256    }
1257    #[test]
1258    fn test_dual_mul() {
1259        let x = Dual::variable(3.0);
1260        let y = x * x;
1261        assert!((y.re - 9.0).abs() < 1e-12);
1262        assert!((y.du - 6.0).abs() < 1e-12);
1263    }
1264    #[test]
1265    fn test_dual_differentiate_polynomial() {
1266        let deriv = dual_differentiate(|x| x.powi(3) - Dual::constant(2.0) * x, 2.0);
1267        assert!((deriv - 10.0).abs() < 1e-10, "deriv={}", deriv);
1268    }
1269    #[test]
1270    fn test_dual_sin_derivative() {
1271        let x = std::f64::consts::FRAC_PI_4;
1272        let deriv = dual_differentiate(|d| d.sin(), x);
1273        assert!((deriv - x.cos()).abs() < 1e-12, "deriv={}", deriv);
1274    }
1275    #[test]
1276    fn test_dual_exp_derivative() {
1277        let deriv = dual_differentiate(|d| d.exp(), 1.0);
1278        assert!(
1279            (deriv - std::f64::consts::E).abs() < 1e-10,
1280            "deriv={}",
1281            deriv
1282        );
1283    }
1284    #[test]
1285    fn test_dual_sqrt_derivative() {
1286        let deriv = dual_differentiate(|d| d.sqrt(), 4.0);
1287        assert!((deriv - 0.25).abs() < 1e-10, "deriv={}", deriv);
1288    }
1289    #[test]
1290    fn test_hermite_interpolate_endpoints() {
1291        let p0 = Vec3::new(0.0, 0.0, 0.0);
1292        let p1 = Vec3::new(1.0, 0.0, 0.0);
1293        let m0 = Vec3::new(0.0, 1.0, 0.0);
1294        let m1 = Vec3::new(0.0, 1.0, 0.0);
1295        let at_0 = hermite_interpolate(&p0, &m0, &p1, &m1, 0.0);
1296        let at_1 = hermite_interpolate(&p0, &m0, &p1, &m1, 1.0);
1297        assert!((at_0 - p0).norm() < 1e-12, "t=0 should be p0");
1298        assert!((at_1 - p1).norm() < 1e-12, "t=1 should be p1");
1299    }
1300    #[test]
1301    fn test_bezier_cubic_endpoints() {
1302        let p0 = Vec3::new(0.0, 0.0, 0.0);
1303        let p1 = Vec3::new(1.0, 2.0, 0.0);
1304        let p2 = Vec3::new(2.0, 2.0, 0.0);
1305        let p3 = Vec3::new(3.0, 0.0, 0.0);
1306        let at_0 = bezier_cubic(&p0, &p1, &p2, &p3, 0.0);
1307        let at_1 = bezier_cubic(&p0, &p1, &p2, &p3, 1.0);
1308        assert!((at_0 - p0).norm() < 1e-12);
1309        assert!((at_1 - p3).norm() < 1e-12);
1310    }
1311    #[test]
1312    fn test_catmull_rom_interpolates_endpoints() {
1313        let p0 = Vec3::new(-1.0, 0.0, 0.0);
1314        let p1 = Vec3::new(0.0, 0.0, 0.0);
1315        let p2 = Vec3::new(1.0, 0.0, 0.0);
1316        let p3 = Vec3::new(2.0, 0.0, 0.0);
1317        let at_0 = catmull_rom(&p0, &p1, &p2, &p3, 0.0);
1318        let at_1 = catmull_rom(&p0, &p1, &p2, &p3, 1.0);
1319        assert!((at_0 - p1).norm() < 1e-12);
1320        assert!((at_1 - p2).norm() < 1e-12);
1321    }
1322    #[test]
1323    fn test_bspline_basis3_partition_of_unity() {
1324        for i in 0..=10 {
1325            let t = i as f64 / 10.0;
1326            let b = bspline_basis3(t);
1327            let sum: f64 = b.iter().sum();
1328            assert!((sum - 1.0).abs() < 1e-12, "t={}: sum={}", t, sum);
1329        }
1330    }
1331    #[test]
1332    fn test_bezier_arc_length_straight_line() {
1333        let p0 = Vec3::new(0.0, 0.0, 0.0);
1334        let p1 = Vec3::new(1.0, 0.0, 0.0);
1335        let p2 = Vec3::new(2.0, 0.0, 0.0);
1336        let p3 = Vec3::new(3.0, 0.0, 0.0);
1337        let len = bezier_arc_length(&p0, &p1, &p2, &p3, 100);
1338        assert!((len - 3.0).abs() < 1e-6, "arc length={}", len);
1339    }
1340    #[test]
1341    fn test_frustum_contains_origin() {
1342        let vp = perspective(std::f64::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
1343        let frustum = Frustum::from_view_projection(&vp);
1344        assert_eq!(frustum.planes.len(), 6);
1345        for plane in &frustum.planes {
1346            assert!((plane.normal.norm() - 1.0).abs() < 1e-6);
1347        }
1348    }
1349    #[test]
1350    fn test_frustum_sphere_outside() {
1351        let vp = perspective(std::f64::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
1352        let frustum = Frustum::from_view_projection(&vp);
1353        let result = frustum.intersects_sphere(&Vec3::new(1000.0, 0.0, 0.0), 1.0);
1354        let _ = result;
1355    }
1356    #[test]
1357    fn test_mat3_transpose_roundtrip() {
1358        let m = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1359        let t = mat3_transpose(m);
1360        let tt = mat3_transpose(t);
1361        for i in 0..3 {
1362            for j in 0..3 {
1363                assert!((m[i][j] - tt[i][j]).abs() < 1e-12);
1364            }
1365        }
1366    }
1367    #[test]
1368    fn test_mat3_mul_vec3_identity() {
1369        let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1370        let v = [3.0, 4.0, 5.0];
1371        let result = mat3_mul_vec3(identity, v);
1372        for i in 0..3 {
1373            assert!((result[i] - v[i]).abs() < 1e-12);
1374        }
1375    }
1376    #[test]
1377    fn test_mat3_arr_inverse() {
1378        let m = [[1.0, 2.0, 0.0], [0.0, 1.0, 3.0], [0.0, 0.0, 1.0]];
1379        let inv = mat3_arr_inverse(m).expect("should be invertible");
1380        let prod = mat3_mul_mat3(m, inv);
1381        for i in 0..3 {
1382            for j in 0..3 {
1383                let expected = if i == j { 1.0 } else { 0.0 };
1384                assert!(
1385                    (prod[i][j] - expected).abs() < 1e-10,
1386                    "prod[{i}][{j}]={}",
1387                    prod[i][j]
1388                );
1389            }
1390        }
1391    }
1392    #[test]
1393    fn test_mat3_outer_product() {
1394        let a = [1.0, 0.0, 0.0];
1395        let b = [0.0, 1.0, 0.0];
1396        let op = mat3_outer_product(a, b);
1397        assert!((op[0][1] - 1.0).abs() < 1e-12);
1398        assert!(op[0][0].abs() < 1e-12);
1399        assert!(op[1][1].abs() < 1e-12);
1400    }
1401    #[test]
1402    fn test_quat_arr_from_axis_angle_to_mat3_roundtrip() {
1403        let q = quat_arr_from_axis_angle([0.0, 0.0, 1.0], std::f64::consts::FRAC_PI_2);
1404        let m = quat_to_mat3(q);
1405        let v = mat3_mul_vec3(m, [1.0, 0.0, 0.0]);
1406        assert!(v[0].abs() < 1e-10, "x={}", v[0]);
1407        assert!((v[1] - 1.0).abs() < 1e-10, "y={}", v[1]);
1408        assert!(v[2].abs() < 1e-10, "z={}", v[2]);
1409    }
1410    #[test]
1411    fn test_quat_multiply_identity() {
1412        let id = [0.0, 0.0, 0.0, 1.0];
1413        let q = quat_arr_from_axis_angle([0.0, 1.0, 0.0], 0.5);
1414        let result = quat_multiply(id, q);
1415        for i in 0..4 {
1416            assert!((result[i] - q[i]).abs() < 1e-12, "component {i}");
1417        }
1418    }
1419    #[test]
1420    fn test_quat_arr_slerp_at_t0_and_t1() {
1421        let p = quat_arr_from_axis_angle([0.0, 1.0, 0.0], 0.0);
1422        let q = quat_arr_from_axis_angle([0.0, 1.0, 0.0], 1.0);
1423        let s0 = quat_arr_slerp(p, q, 0.0);
1424        let s1 = quat_arr_slerp(p, q, 1.0);
1425        for i in 0..4 {
1426            assert!(
1427                (s0[i] - p[i]).abs() < 1e-10,
1428                "t=0 component {i}: got {}, expected {}",
1429                s0[i],
1430                p[i]
1431            );
1432            assert!(
1433                (s1[i] - q[i]).abs() < 1e-10,
1434                "t=1 component {i}: got {}, expected {}",
1435                s1[i],
1436                q[i]
1437            );
1438        }
1439    }
1440    #[test]
1441    fn test_vec3_reflect() {
1442        let v = [1.0, -1.0, 0.0];
1443        let n = [0.0, 1.0, 0.0];
1444        let r = vec3_reflect(v, n);
1445        assert!((r[0] - 1.0).abs() < 1e-12);
1446        assert!((r[1] - 1.0).abs() < 1e-12);
1447        assert!(r[2].abs() < 1e-12);
1448    }
1449    #[test]
1450    fn test_vec3_project() {
1451        let a = [3.0, 4.0, 0.0];
1452        let onto = [1.0, 0.0, 0.0];
1453        let proj = vec3_project(a, onto);
1454        assert!((proj[0] - 3.0).abs() < 1e-12);
1455        assert!(proj[1].abs() < 1e-12);
1456        assert!(proj[2].abs() < 1e-12);
1457    }
1458    #[test]
1459    fn test_vec3_lerp() {
1460        let a = [0.0, 0.0, 0.0];
1461        let b = [2.0, 4.0, 6.0];
1462        let mid = vec3_lerp(a, b, 0.5);
1463        assert!((mid[0] - 1.0).abs() < 1e-12);
1464        assert!((mid[1] - 2.0).abs() < 1e-12);
1465        assert!((mid[2] - 3.0).abs() < 1e-12);
1466    }
1467    #[test]
1468    fn test_plane_from_point_normal_and_signed_dist() {
1469        let p = [0.0, 5.0, 0.0];
1470        let n = [0.0, 1.0, 0.0];
1471        let plane = plane_from_point_normal(p, n);
1472        let dist_above = plane_signed_dist(plane, [0.0, 8.0, 0.0]);
1473        let dist_below = plane_signed_dist(plane, [0.0, 3.0, 0.0]);
1474        assert!((dist_above - 3.0).abs() < 1e-12);
1475        assert!((dist_below + 2.0).abs() < 1e-12);
1476    }
1477    #[test]
1478    fn test_vec3_cross() {
1479        let a = [1.0_f64, 0.0, 0.0];
1480        let b = [0.0_f64, 1.0, 0.0];
1481        let c = vec3_cross(a, b);
1482        assert!(c[0].abs() < 1e-12);
1483        assert!(c[1].abs() < 1e-12);
1484        assert!((c[2] - 1.0).abs() < 1e-12);
1485    }
1486    #[test]
1487    fn test_vec3_triple_product() {
1488        let a = [1.0_f64, 0.0, 0.0];
1489        let b = [0.0_f64, 1.0, 0.0];
1490        let c = [0.0_f64, 0.0, 1.0];
1491        assert!((vec3_triple_product(a, b, c) - 1.0).abs() < 1e-12);
1492    }
1493    #[test]
1494    fn test_vec3_rotate_by_angle_around_z() {
1495        let v = [1.0_f64, 0.0, 0.0];
1496        let axis = [0.0_f64, 0.0, 1.0];
1497        let r = vec3_rotate_by_angle(v, axis, std::f64::consts::FRAC_PI_2);
1498        assert!(r[0].abs() < 1e-12, "x={}", r[0]);
1499        assert!((r[1] - 1.0).abs() < 1e-12, "y={}", r[1]);
1500        assert!(r[2].abs() < 1e-12, "z={}", r[2]);
1501    }
1502    #[test]
1503    fn test_vec3_rotate_zero_angle() {
1504        let v = [3.0_f64, 4.0, 5.0];
1505        let axis = [0.0_f64, 1.0, 0.0];
1506        let r = vec3_rotate_by_angle(v, axis, 0.0);
1507        for i in 0..3 {
1508            assert!((r[i] - v[i]).abs() < 1e-12, "component {i}");
1509        }
1510    }
1511    #[test]
1512    fn test_mat3_adjugate_identity() {
1513        let m = [[1.0_f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1514        let adj = mat3_adjugate(m);
1515        for i in 0..3 {
1516            for j in 0..3 {
1517                let exp = if i == j { 1.0 } else { 0.0 };
1518                assert!(
1519                    (adj[i][j] - exp).abs() < 1e-12,
1520                    "adj[{i}][{j}]={}",
1521                    adj[i][j]
1522                );
1523            }
1524        }
1525    }
1526    #[test]
1527    fn test_mat3_adjugate_relation() {
1528        let m = [[1.0_f64, 2.0, 0.0], [0.0, 1.0, 3.0], [0.0, 0.0, 1.0]];
1529        let adj = mat3_adjugate(m);
1530        let det = mat3_det(m);
1531        let prod = mat3_mul_mat3(m, adj);
1532        for i in 0..3 {
1533            for j in 0..3 {
1534                let exp = if i == j { det } else { 0.0 };
1535                assert!(
1536                    (prod[i][j] - exp).abs() < 1e-10,
1537                    "prod[{i}][{j}]={}  exp={}",
1538                    prod[i][j],
1539                    exp
1540                );
1541            }
1542        }
1543    }
1544    #[test]
1545    fn test_cayley_hamilton() {
1546        let m = [[1.0_f64, 2.0, 3.0], [0.0, 4.0, 5.0], [0.0, 0.0, 6.0]];
1547        let zero = mat3_cayley_hamilton(m);
1548        for i in 0..3 {
1549            for j in 0..3 {
1550                assert!(zero[i][j].abs() < 1e-8, "CH[{i}][{j}]={}", zero[i][j]);
1551            }
1552        }
1553    }
1554    #[test]
1555    fn test_quat_log_exp_roundtrip() {
1556        let q = quat_arr_from_axis_angle([0.0, 1.0, 0.0], 0.8);
1557        let log_q = quat_arr_log(q);
1558        let exp_log_q = quat_arr_exp(log_q);
1559        for i in 0..4 {
1560            assert!((exp_log_q[i] - q[i]).abs() < 1e-10, "component {i}");
1561        }
1562    }
1563    #[test]
1564    fn test_quat_log_identity() {
1565        let id = [0.0_f64, 0.0, 0.0, 1.0];
1566        let log_id = quat_arr_log(id);
1567        for &v in &log_id {
1568            assert!(v.abs() < 1e-12, "log(identity) should be zero: {}", v);
1569        }
1570    }
1571    #[test]
1572    fn test_quat_to_axis_angle_roundtrip() {
1573        let angle = 1.2_f64;
1574        let axis = [0.0_f64, 1.0, 0.0];
1575        let q = quat_arr_from_axis_angle(axis, angle);
1576        let (out_axis, out_angle) = quat_to_axis_angle(q);
1577        assert!(
1578            (out_angle - angle).abs() < 1e-10,
1579            "angle mismatch: {} vs {}",
1580            out_angle,
1581            angle
1582        );
1583        for i in 0..3 {
1584            assert!(
1585                (out_axis[i] - axis[i]).abs() < 1e-10,
1586                "axis[{i}]: {} vs {}",
1587                out_axis[i],
1588                axis[i]
1589            );
1590        }
1591    }
1592    #[test]
1593    fn test_aabb_contains() {
1594        let aabb = Aabb::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
1595        assert!(aabb.contains([1.0, 1.0, 1.0]));
1596        assert!(!aabb.contains([3.0, 1.0, 1.0]));
1597    }
1598    #[test]
1599    fn test_aabb_union() {
1600        let a = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1601        let b = Aabb::new([-1.0, -1.0, -1.0], [0.5, 0.5, 0.5]);
1602        let u = a.union(&b);
1603        for i in 0..3 {
1604            assert!((u.min[i] - (-1.0)).abs() < 1e-12);
1605        }
1606        assert!((u.max[0] - 1.0).abs() < 1e-12);
1607    }
1608    #[test]
1609    fn test_aabb_intersect() {
1610        let a = Aabb::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
1611        let b = Aabb::new([1.0, 1.0, 1.0], [3.0, 3.0, 3.0]);
1612        let inter = a.intersect(&b).expect("should overlap");
1613        for i in 0..3 {
1614            assert!((inter.min[i] - 1.0).abs() < 1e-12);
1615            assert!((inter.max[i] - 2.0).abs() < 1e-12);
1616        }
1617    }
1618    #[test]
1619    fn test_aabb_no_intersect() {
1620        let a = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1621        let b = Aabb::new([2.0, 2.0, 2.0], [3.0, 3.0, 3.0]);
1622        assert!(a.intersect(&b).is_none());
1623    }
1624    #[test]
1625    fn test_aabb_expand() {
1626        let a = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1627        let exp = a.expand(0.5);
1628        for i in 0..3 {
1629            assert!((exp.min[i] - (-0.5)).abs() < 1e-12);
1630            assert!((exp.max[i] - 1.5).abs() < 1e-12);
1631        }
1632    }
1633    #[test]
1634    fn test_aabb_intersect_ray() {
1635        let aabb = Aabb::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
1636        let hit = aabb.intersect_ray([-1.0, 1.0, 1.0], [1.0, 0.0, 0.0]);
1637        assert!(hit.is_some(), "ray should hit AABB");
1638        let (t0, _t1) = hit.unwrap();
1639        assert!((t0 - 1.0).abs() < 1e-10, "t0={}", t0);
1640    }
1641    #[test]
1642    fn test_aabb_ray_miss() {
1643        let aabb = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1644        let hit = aabb.intersect_ray([0.0, 5.0, 0.5], [1.0, 0.0, 0.0]);
1645        assert!(hit.is_none(), "ray should miss AABB");
1646    }
1647}
1648/// Outer product of two Vec3 vectors: `a ⊗ b` as a nalgebra `Mat3`.
1649///
1650/// The (i,j) element equals `a[i] * b[j]`.
1651#[allow(dead_code)]
1652pub fn vec3_outer_product(a: &Vec3, b: &Vec3) -> Mat3 {
1653    Mat3::new(
1654        a.x * b.x,
1655        a.x * b.y,
1656        a.x * b.z,
1657        a.y * b.x,
1658        a.y * b.y,
1659        a.y * b.z,
1660        a.z * b.x,
1661        a.z * b.y,
1662        a.z * b.z,
1663    )
1664}
1665/// Build the skew-symmetric (cross-product) matrix for `v`.
1666///
1667/// Equivalent to `skew_symmetric` but with an explicit name.
1668/// `cross_matrix(v) * w == v.cross(&w)`.
1669#[allow(dead_code)]
1670pub fn cross_matrix(v: &Vec3) -> Mat3 {
1671    skew_symmetric(v)
1672}
1673/// Gram-Schmidt orthogonalization of three linearly-independent vectors.
1674///
1675/// Returns an orthonormal basis `(e0, e1, e2)` where:
1676/// - `e0` is `v0` normalized,
1677/// - `e1` is `v1` minus its projection onto `e0`, normalized,
1678/// - `e2` is `v2` minus its projections onto `e0` and `e1`, normalized.
1679///
1680/// Returns `None` if any intermediate vector becomes near-zero.
1681#[allow(dead_code)]
1682pub fn gram_schmidt(v0: &Vec3, v1: &Vec3, v2: &Vec3) -> Option<(Vec3, Vec3, Vec3)> {
1683    let e0_len = v0.norm();
1684    if e0_len < 1e-12 {
1685        return None;
1686    }
1687    let e0 = v0 / e0_len;
1688    let u1 = v1 - e0 * e0.dot(v1);
1689    let u1_len = u1.norm();
1690    if u1_len < 1e-12 {
1691        return None;
1692    }
1693    let e1 = u1 / u1_len;
1694    let u2 = v2 - e0 * e0.dot(v2) - e1 * e1.dot(v2);
1695    let u2_len = u2.norm();
1696    if u2_len < 1e-12 {
1697        return None;
1698    }
1699    let e2 = u2 / u2_len;
1700    Some((e0, e1, e2))
1701}
1702/// Convert Cartesian coordinates `(x, y, z)` to spherical `(r, theta, phi)`.
1703///
1704/// - `r` = radial distance ≥ 0
1705/// - `theta` = polar angle ∈ `[0, π]` (from +Z axis)
1706/// - `phi` = azimuthal angle ∈ `(-π, π]` (in XY plane from +X axis)
1707#[allow(dead_code)]
1708pub fn cartesian_to_spherical(v: &Vec3) -> (Real, Real, Real) {
1709    let r = v.norm();
1710    if r < 1e-300 {
1711        return (0.0, 0.0, 0.0);
1712    }
1713    let theta = (v.z / r).clamp(-1.0, 1.0).acos();
1714    let phi = v.y.atan2(v.x);
1715    (r, theta, phi)
1716}
1717/// Convert spherical coordinates `(r, theta, phi)` to Cartesian `(x, y, z)`.
1718///
1719/// - `r` – radial distance
1720/// - `theta` – polar angle from +Z
1721/// - `phi` – azimuthal angle from +X in XY plane
1722#[allow(dead_code)]
1723pub fn spherical_to_cartesian(r: Real, theta: Real, phi: Real) -> Vec3 {
1724    Vec3::new(
1725        r * theta.sin() * phi.cos(),
1726        r * theta.sin() * phi.sin(),
1727        r * theta.cos(),
1728    )
1729}
1730/// Convert Cartesian coordinates to cylindrical `(rho, phi, z)`.
1731///
1732/// - `rho` = radial distance in XY plane
1733/// - `phi` = azimuthal angle ∈ `(-π, π]`
1734/// - `z`   = height
1735#[allow(dead_code)]
1736pub fn cartesian_to_cylindrical(v: &Vec3) -> (Real, Real, Real) {
1737    let rho = (v.x * v.x + v.y * v.y).sqrt();
1738    let phi = v.y.atan2(v.x);
1739    (rho, phi, v.z)
1740}
1741/// Convert cylindrical coordinates `(rho, phi, z)` to Cartesian.
1742#[allow(dead_code)]
1743pub fn cylindrical_to_cartesian(rho: Real, phi: Real, z: Real) -> Vec3 {
1744    Vec3::new(rho * phi.cos(), rho * phi.sin(), z)
1745}
1746/// Rodrigues rotation: rotate vector `v` around unit `axis` by `angle` radians.
1747///
1748/// Equivalent to `vec3_rotate_by_angle` but operates on `Vec3` directly.
1749/// `axis` must be a unit vector.
1750#[allow(dead_code)]
1751pub fn rodrigues_rotate(v: &Vec3, axis: &Vec3, angle: Real) -> Vec3 {
1752    let cos_a = angle.cos();
1753    let sin_a = angle.sin();
1754    v * cos_a + axis.cross(v) * sin_a + axis * axis.dot(v) * (1.0 - cos_a)
1755}
1756/// Matrix exponential of a skew-symmetric matrix (rotation matrix).
1757///
1758/// Computes `exp(S)` where `S` is the skew-symmetric matrix corresponding
1759/// to angular velocity `omega` via Rodrigues' formula:
1760///
1761/// `exp(S) = I + sin(θ)/θ * S + (1 - cos(θ))/θ² * S²`
1762///
1763/// where `θ = |omega|`.  For `θ ≈ 0` returns the identity.
1764#[allow(dead_code)]
1765pub fn mat3_exp_skew(omega: &Vec3) -> Mat3 {
1766    let theta = omega.norm();
1767    if theta < 1e-12 {
1768        return Mat3::identity();
1769    }
1770    let axis = omega / theta;
1771    let s = skew_symmetric(&axis);
1772    let s2 = s * s;
1773    Mat3::identity() + s * theta.sin() + s2 * (1.0 - theta.cos())
1774}
1775/// Matrix logarithm of a rotation matrix `R`.
1776///
1777/// Returns the skew-symmetric matrix `S` such that `exp(S) = R`.
1778/// The returned matrix encodes the axis-angle `theta * n̂` in its entries.
1779/// Returns the zero matrix for the identity rotation.
1780#[allow(dead_code)]
1781pub fn mat3_log_rotation(r: &Mat3) -> Mat3 {
1782    let trace = r.trace();
1783    let cos_theta = ((trace - 1.0) / 2.0).clamp(-1.0, 1.0);
1784    let theta = cos_theta.acos();
1785    if theta.abs() < 1e-12 {
1786        return Mat3::zeros();
1787    }
1788    (r - r.transpose()) * (theta / (2.0 * theta.sin()))
1789}
1790/// Normalized linear interpolation (nlerp) between two unit quaternions.
1791///
1792/// Faster but less accurate than slerp; the result is normalized to stay
1793/// on the unit sphere.
1794#[allow(dead_code)]
1795pub fn quat_nlerp(a: &Quat, b: &Quat, t: Real) -> Quat {
1796    let ai = a.into_inner();
1797    let bi = b.into_inner();
1798    let bi = if ai.dot(&bi) < 0.0 { -bi } else { bi };
1799    let lerped = ai * (1.0 - t) + bi * t;
1800    UnitQuaternion::new_normalize(lerped)
1801}
1802/// Build the inner control quaternion `s_i` needed for SQUAD interpolation.
1803///
1804/// Given three consecutive key quaternions `q_prev`, `q_curr`, `q_next`,
1805/// returns `s_i = q_curr * exp( -(log(q_curr^{-1} q_next) + log(q_curr^{-1} q_prev)) / 4 )`.
1806#[allow(dead_code)]
1807pub fn quat_squad_control(q_prev: &Quat, q_curr: &Quat, q_next: &Quat) -> Quat {
1808    let qi_inv = q_curr.inverse();
1809    let log_next = quat_log(&(qi_inv * q_next));
1810    let log_prev = quat_log(&(qi_inv * q_prev));
1811    let sum = log_next + log_prev;
1812    let half = sum * (-0.25);
1813    q_curr * quat_exp(&half)
1814}
1815/// Geodesic (angular) distance between two unit quaternions.
1816///
1817/// Returns the minimal angle in `[0, π]` needed to rotate from `a` to `b`.
1818#[allow(dead_code)]
1819pub fn quat_geodesic_distance(a: &Quat, b: &Quat) -> Real {
1820    a.angle_to(b)
1821}
1822/// Project `v` onto the unit direction `onto_unit`.
1823///
1824/// Returns the component of `v` parallel to `onto_unit`.
1825/// `onto_unit` must be a unit vector.
1826#[allow(dead_code)]
1827pub fn vec3_project_onto(v: &Vec3, onto_unit: &Vec3) -> Vec3 {
1828    onto_unit * v.dot(onto_unit)
1829}
1830/// Reject `v` from the unit direction `onto_unit` (perpendicular component).
1831///
1832/// Returns `v - project_onto(v, onto_unit)`.
1833/// `onto_unit` must be a unit vector.
1834#[allow(dead_code)]
1835pub fn vec3_reject_from(v: &Vec3, onto_unit: &Vec3) -> Vec3 {
1836    v - vec3_project_onto(v, onto_unit)
1837}
1838/// Reflect `v` about the unit normal `n`: `v - 2*(v·n)*n`.
1839#[allow(dead_code)]
1840pub fn vec3_reflect_about(v: &Vec3, n: &Vec3) -> Vec3 {
1841    v - n * (2.0 * v.dot(n))
1842}
1843/// Refract `v` about the unit normal `n` with relative index of refraction `eta`.
1844///
1845/// `v` must be normalized, `n` must be a unit normal pointing away from the
1846/// surface on the same side as `v`.  Returns `None` on total internal
1847/// reflection.
1848#[allow(dead_code)]
1849pub fn vec3_refract(v: &Vec3, n: &Vec3, eta: Real) -> Option<Vec3> {
1850    let cos_i = -(v.dot(n));
1851    let sin2_t = eta * eta * (1.0 - cos_i * cos_i);
1852    if sin2_t > 1.0 {
1853        return None;
1854    }
1855    let cos_t = (1.0 - sin2_t).sqrt();
1856    Some(v * eta + n * (eta * cos_i - cos_t))
1857}
1858/// Angle in radians between two non-zero vectors `a` and `b`.
1859///
1860/// Returns a value in `[0, π]`.
1861#[allow(dead_code)]
1862pub fn vec3_angle_between(a: &Vec3, b: &Vec3) -> Real {
1863    let denom = a.norm() * b.norm();
1864    if denom < 1e-300 {
1865        return 0.0;
1866    }
1867    (a.dot(b) / denom).clamp(-1.0, 1.0).acos()
1868}
1869/// Rotate `v` by quaternion `q`.
1870#[allow(dead_code)]
1871pub fn vec3_rotate_by_quat(v: &Vec3, q: &Quat) -> Vec3 {
1872    q.transform_vector(v)
1873}
1874/// Build a rotation matrix from an `axis` (unit vector) and `angle` (radians).
1875///
1876/// Uses the Rodrigues rotation formula.
1877#[allow(dead_code)]
1878pub fn mat3_from_axis_angle(axis: &Vec3, angle: Real) -> Mat3 {
1879    let c = angle.cos();
1880    let s = angle.sin();
1881    let t = 1.0 - c;
1882    let [x, y, z] = [axis.x, axis.y, axis.z];
1883    Mat3::new(
1884        t * x * x + c,
1885        t * x * y - s * z,
1886        t * x * z + s * y,
1887        t * x * y + s * z,
1888        t * y * y + c,
1889        t * y * z - s * x,
1890        t * x * z - s * y,
1891        t * y * z + s * x,
1892        t * z * z + c,
1893    )
1894}