1#![allow(clippy::needless_range_loop)]
6use super::linear_algebra::*;
7#[cfg(test)]
8use super::types::*;
9
10#[allow(dead_code)]
17pub fn mat3_to_euler_zyx(m: &Mat3) -> (Real, Real, Real) {
18 let pitch = (-m[(2, 0)]).clamp(-1.0, 1.0).asin();
19 let cos_pitch = pitch.cos();
20 if cos_pitch.abs() < 1e-6 {
21 let roll = 0.0;
22 let yaw = m[(0, 1)].atan2(m[(1, 1)]);
23 (roll, pitch, yaw)
24 } else {
25 let roll = m[(2, 1)].atan2(m[(2, 2)]);
26 let yaw = m[(1, 0)].atan2(m[(0, 0)]);
27 (roll, pitch, yaw)
28 }
29}
30#[allow(dead_code)]
34pub fn mat3_from_euler_zyx(roll: Real, pitch: Real, yaw: Real) -> Mat3 {
35 let (cr, sr) = (roll.cos(), roll.sin());
36 let (cp, sp) = (pitch.cos(), pitch.sin());
37 let (cy, sy) = (yaw.cos(), yaw.sin());
38 Mat3::new(
39 cy * cp,
40 cy * sp * sr - sy * cr,
41 cy * sp * cr + sy * sr,
42 sy * cp,
43 sy * sp * sr + cy * cr,
44 sy * sp * cr - cy * sr,
45 -sp,
46 cp * sr,
47 cp * cr,
48 )
49}
50#[allow(dead_code)]
54pub fn mat3_cofactor(m: &Mat3) -> Mat3 {
55 let c = |r: usize, c_idx: usize| -> Real {
56 let rows: [usize; 2] = match r {
57 0 => [1, 2],
58 1 => [0, 2],
59 _ => [0, 1],
60 };
61 let cols: [usize; 2] = match c_idx {
62 0 => [1, 2],
63 1 => [0, 2],
64 _ => [0, 1],
65 };
66 let det2 = m[(rows[0], cols[0])] * m[(rows[1], cols[1])]
67 - m[(rows[0], cols[1])] * m[(rows[1], cols[0])];
68 let sign = if (r + c_idx).is_multiple_of(2) {
69 1.0
70 } else {
71 -1.0
72 };
73 sign * det2
74 };
75 Mat3::new(
76 c(0, 0),
77 c(0, 1),
78 c(0, 2),
79 c(1, 0),
80 c(1, 1),
81 c(1, 2),
82 c(2, 0),
83 c(2, 1),
84 c(2, 2),
85 )
86}
87#[allow(dead_code)]
91pub fn mat3_adjugate_na(m: &Mat3) -> Mat3 {
92 mat3_cofactor(m).transpose()
93}
94#[allow(dead_code)]
99pub fn quat_from_two_vectors(from: &Vec3, to: &Vec3) -> Quat {
100 let dot = from.dot(to).clamp(-1.0, 1.0);
101 if dot > 1.0 - 1e-10 {
102 return Quat::identity();
103 }
104 if dot < -1.0 + 1e-10 {
105 let perp = if from.x.abs() < 0.9 {
106 Vec3::new(1.0, 0.0, 0.0).cross(from)
107 } else {
108 Vec3::new(0.0, 1.0, 0.0).cross(from)
109 };
110 let axis = Unit::new_normalize(perp);
111 return UnitQuaternion::from_axis_angle(&axis, std::f64::consts::PI);
112 }
113 let axis = from.cross(to);
114 let w = 1.0 + dot;
115 let len = (w * w + axis.norm_squared()).sqrt();
116 let [x, y, z] = [axis.x / len, axis.y / len, axis.z / len];
117 UnitQuaternion::new_normalize(nalgebra::Quaternion::new(w / len, x, y, z))
118}
119#[allow(dead_code)]
124pub fn quat_log(q: &Quat) -> nalgebra::Quaternion<Real> {
125 let w = q.w.clamp(-1.0, 1.0);
126 let theta = w.acos();
127 let sin_t = theta.sin();
128 if sin_t.abs() < 1e-10 {
129 return nalgebra::Quaternion::new(0.0, 0.0, 0.0, 0.0);
130 }
131 let s = theta / sin_t;
132 nalgebra::Quaternion::new(0.0, q.i * s, q.j * s, q.k * s)
133}
134#[allow(dead_code)]
138pub fn quat_exp(v: &nalgebra::Quaternion<Real>) -> Quat {
139 let norm = (v.i * v.i + v.j * v.j + v.k * v.k).sqrt();
140 if norm < 1e-15 {
141 return Quat::identity();
142 }
143 let s = norm.sin() / norm;
144 UnitQuaternion::new_normalize(nalgebra::Quaternion::new(
145 norm.cos(),
146 v.i * s,
147 v.j * s,
148 v.k * s,
149 ))
150}
151#[allow(dead_code)]
154pub fn quat_double_cover_fix(q: Quat) -> Quat {
155 if q.w < 0.0 {
156 UnitQuaternion::new_normalize(-q.into_inner())
157 } else {
158 q
159 }
160}
161#[allow(dead_code)]
166pub fn quat_squad_na(q1: &Quat, q2: &Quat, s1: &Quat, s2: &Quat, t: Real) -> Quat {
167 let slerp_q = q1.slerp(q2, t);
168 let slerp_s = s1.slerp(s2, t);
169 slerp_q.slerp(&slerp_s, 2.0 * t * (1.0 - t))
170}
171#[cfg(test)]
172mod extended_math_tests {
173 use super::*;
174 use crate::Mat3;
175 use crate::Vec3;
176
177 use crate::math::mat3_from_axis_angle;
178
179 use crate::math::quat_from_axis_angle;
180
181 use crate::math::vec3_refract;
182
183 #[test]
184 fn test_vec3_project_onto_x_axis() {
185 let v = Vec3::new(3.0, 4.0, 5.0);
186 let x = Vec3::new(1.0, 0.0, 0.0);
187 let p = vec3_project_onto(&v, &x);
188 assert!((p.x - 3.0).abs() < 1e-12, "x={}", p.x);
189 assert!(p.y.abs() < 1e-12);
190 assert!(p.z.abs() < 1e-12);
191 }
192 #[test]
193 fn test_vec3_reject_from_perpendicular() {
194 let v = Vec3::new(3.0, 4.0, 0.0);
195 let x = Vec3::new(1.0, 0.0, 0.0);
196 let r = vec3_reject_from(&v, &x);
197 assert!(r.x.abs() < 1e-12, "x={}", r.x);
198 assert!((r.y - 4.0).abs() < 1e-12, "y={}", r.y);
199 }
200 #[test]
201 fn test_vec3_reflect_about_normal() {
202 let v = Vec3::new(1.0, -1.0, 0.0);
203 let n = Vec3::new(0.0, 1.0, 0.0);
204 let r = vec3_reflect_about(&v, &n);
205 assert!((r.x - 1.0).abs() < 1e-12);
206 assert!((r.y - 1.0).abs() < 1e-12);
207 assert!(r.z.abs() < 1e-12);
208 }
209 #[test]
210 fn test_vec3_refract_no_total_internal_reflection() {
211 let v = Vec3::new(0.0, -1.0, 0.0);
212 let n = Vec3::new(0.0, 1.0, 0.0);
213 let refr = vec3_refract(&v, &n, 1.0).expect("should not TIR");
214 assert!((refr.x).abs() < 1e-10);
215 assert!((refr.y + 1.0).abs() < 1e-10);
216 }
217 #[test]
218 fn test_vec3_refract_total_internal_reflection() {
219 let v = Vec3::new(0.9999, -0.0141, 0.0).normalize();
220 let n = Vec3::new(0.0, 1.0, 0.0);
221 let result = vec3_refract(&v, &n, 1.5);
222 if let Some(r) = result {
223 assert!(
224 (r.norm() - 1.0).abs() < 1e-8,
225 "refracted not unit: {}",
226 r.norm()
227 );
228 }
229 }
230 #[test]
231 fn test_vec3_angle_between_perpendicular() {
232 let a = Vec3::new(1.0, 0.0, 0.0);
233 let b = Vec3::new(0.0, 1.0, 0.0);
234 let angle = vec3_angle_between(&a, &b);
235 assert!(
236 (angle - std::f64::consts::FRAC_PI_2).abs() < 1e-10,
237 "angle={}",
238 angle
239 );
240 }
241 #[test]
242 fn test_vec3_angle_between_parallel() {
243 let a = Vec3::new(1.0, 0.0, 0.0);
244 let angle = vec3_angle_between(&a, &a);
245 assert!(angle.abs() < 1e-10, "angle={}", angle);
246 }
247 #[test]
248 fn test_vec3_rotate_by_quat() {
249 let v = Vec3::new(1.0, 0.0, 0.0);
250 let q = quat_from_axis_angle(&Vec3::new(0.0, 0.0, 1.0), std::f64::consts::FRAC_PI_2);
251 let r = vec3_rotate_by_quat(&v, &q);
252 assert!(r.x.abs() < 1e-10, "x={}", r.x);
253 assert!((r.y - 1.0).abs() < 1e-10, "y={}", r.y);
254 assert!(r.z.abs() < 1e-10, "z={}", r.z);
255 }
256 #[test]
257 fn test_mat3_from_axis_angle_identity_at_zero() {
258 let m = mat3_from_axis_angle(&Vec3::new(0.0, 0.0, 1.0), 0.0);
259 let diff = (m - Mat3::identity()).norm();
260 assert!(diff < 1e-10, "diff={}", diff);
261 }
262 #[test]
263 fn test_mat3_from_axis_angle_90z() {
264 let m = mat3_from_axis_angle(&Vec3::new(0.0, 0.0, 1.0), std::f64::consts::FRAC_PI_2);
265 let v = m * Vec3::new(1.0, 0.0, 0.0);
266 assert!(v.x.abs() < 1e-10, "x={}", v.x);
267 assert!((v.y - 1.0).abs() < 1e-10, "y={}", v.y);
268 assert!(v.z.abs() < 1e-10, "z={}", v.z);
269 }
270 #[test]
271 fn test_mat3_euler_roundtrip() {
272 let roll = 0.4_f64;
273 let pitch = 0.2_f64;
274 let yaw = -0.3_f64;
275 let m = mat3_from_euler_zyx(roll, pitch, yaw);
276 let (r2, p2, y2) = mat3_to_euler_zyx(&m);
277 assert!((r2 - roll).abs() < 1e-8, "roll: {} vs {}", r2, roll);
278 assert!((p2 - pitch).abs() < 1e-8, "pitch: {} vs {}", p2, pitch);
279 assert!((y2 - yaw).abs() < 1e-8, "yaw: {} vs {}", y2, yaw);
280 }
281 #[test]
282 fn test_mat3_cofactor_identity() {
283 let m = Mat3::identity();
284 let c = mat3_cofactor(&m);
285 let diff = (c - Mat3::identity()).norm();
286 assert!(diff < 1e-10, "cofactor(I) should be I: diff={}", diff);
287 }
288 #[test]
289 fn test_mat3_adjugate_na_relation() {
290 let m = Mat3::new(1.0, 2.0, 0.0, 0.0, 1.0, 3.0, 0.0, 0.0, 1.0);
291 let adj = mat3_adjugate_na(&m);
292 let det = m.determinant();
293 let prod = m * adj;
294 let expected = Mat3::identity() * det;
295 let diff = (prod - expected).norm();
296 assert!(diff < 1e-8, "M*adj(M) diff={}", diff);
297 }
298 #[test]
299 fn test_quat_from_two_vectors_basic() {
300 let from = Vec3::new(1.0, 0.0, 0.0);
301 let to = Vec3::new(0.0, 1.0, 0.0);
302 let q = quat_from_two_vectors(&from, &to);
303 let rotated = q.transform_vector(&from);
304 assert!((rotated.x).abs() < 1e-8, "x={}", rotated.x);
305 assert!((rotated.y - 1.0).abs() < 1e-8, "y={}", rotated.y);
306 }
307 #[test]
308 fn test_quat_from_two_vectors_same_direction() {
309 let from = Vec3::new(0.0, 0.0, 1.0);
310 let q = quat_from_two_vectors(&from, &from);
311 let rotated = q.transform_vector(&from);
312 assert!((rotated - from).norm() < 1e-8);
313 }
314 #[test]
315 fn test_quat_log_exp_roundtrip_na() {
316 let q = quat_from_axis_angle(&Vec3::new(1.0, 0.0, 0.0), 1.2);
317 let log_q = quat_log(&q);
318 let exp_q = quat_exp(&log_q);
319 let diff = q.angle_to(&exp_q);
320 assert!(diff < 1e-8, "log/exp roundtrip angle diff={}", diff);
321 }
322 #[test]
323 fn test_quat_double_cover_fix_positive_w() {
324 let q = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.5);
325 let fixed = quat_double_cover_fix(q);
326 assert!(fixed.w >= 0.0, "w should be non-negative after fix");
327 }
328 #[test]
329 fn test_quat_double_cover_fix_negative_w() {
330 let q = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.5);
331 let neg = UnitQuaternion::new_normalize(-q.into_inner());
332 let fixed = quat_double_cover_fix(neg);
333 assert!(fixed.w >= 0.0, "w should be non-negative after fix");
334 }
335 #[test]
336 fn test_quat_squad_na_endpoints() {
337 let q1 = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.0);
338 let q2 = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 1.0);
339 let r0 = quat_squad_na(&q1, &q2, &q1, &q2, 0.0);
340 let r1 = quat_squad_na(&q1, &q2, &q1, &q2, 1.0);
341 assert!(r0.angle_to(&q1) < 1e-6, "t=0 should be q1");
342 assert!(r1.angle_to(&q2) < 1e-6, "t=1 should be q2");
343 }
344 #[test]
345 fn test_plane_intersect_ray_method() {
346 let plane = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
347 let ray = Ray::new(Vec3::new(0.0, 2.0, 0.0), Vec3::new(0.0, -1.0, 0.0));
348 let t = plane.intersect_ray(&ray).expect("should intersect");
349 assert!((t - 2.0).abs() < 1e-10, "t={}", t);
350 }
351 #[test]
352 fn test_plane_intersect_ray_parallel() {
353 let plane = Plane::new(Vec3::new(0.0, 1.0, 0.0), 0.0);
354 let ray = Ray::new(Vec3::new(0.0, 1.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
355 assert!(plane.intersect_ray(&ray).is_none());
356 }
357 #[test]
358 fn test_frustum_contains_sphere_small() {
359 let vp = perspective(std::f64::consts::FRAC_PI_2, 1.0, 1.0, 100.0);
360 let frustum = Frustum::from_view_projection(&vp);
361 let _result = frustum.contains_sphere(&Vec3::new(0.0, 0.0, 0.0), 0.01);
362 }
363 #[test]
364 fn test_frustum_intersects_aabb_basic() {
365 let vp = perspective(std::f64::consts::FRAC_PI_2, 1.0, 1.0, 100.0);
366 let frustum = Frustum::from_view_projection(&vp);
367 let aabb_far = Aabb::new([1000.0, 1000.0, 1000.0], [1001.0, 1001.0, 1001.0]);
368 let _ = frustum.intersects_aabb(&aabb_far);
369 }
370 #[test]
371 fn test_frustum_extract_from_view_proj_same_as_constructor() {
372 let vp = perspective(std::f64::consts::FRAC_PI_4, 1.0, 0.5, 50.0);
373 let f1 = Frustum::from_view_projection(&vp);
374 let f2 = Frustum::extract_from_view_proj(&vp);
375 for (p1, p2) in f1.planes.iter().zip(f2.planes.iter()) {
376 assert!((p1.normal - p2.normal).norm() < 1e-10);
377 assert!((p1.distance - p2.distance).abs() < 1e-10);
378 }
379 }
380 #[test]
381 fn test_aabb_merge() {
382 let a = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
383 let b = Aabb::new([-1.0, 2.0, -1.0], [0.5, 3.0, 2.0]);
384 let m = a.merge(&b);
385 assert!((m.min[0] - (-1.0)).abs() < 1e-12);
386 assert!((m.min[1] - 0.0).abs() < 1e-12);
387 assert!((m.max[1] - 3.0).abs() < 1e-12);
388 }
389 #[test]
390 fn test_aabb_expand_by() {
391 let a = Aabb::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
392 let e = a.expand_by(1.0);
393 for i in 0..3 {
394 assert!((e.min[i] - (-1.0)).abs() < 1e-12);
395 assert!((e.max[i] - 3.0).abs() < 1e-12);
396 }
397 }
398 #[test]
399 fn test_aabb_closest_point_inside() {
400 let aabb = Aabb::new([0.0, 0.0, 0.0], [4.0, 4.0, 4.0]);
401 let p = [2.0, 2.0, 2.0];
402 let cp = aabb.closest_point(p);
403 for i in 0..3 {
404 assert!((cp[i] - 2.0).abs() < 1e-12);
405 }
406 }
407 #[test]
408 fn test_aabb_closest_point_outside() {
409 let aabb = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
410 let p = [-1.0, 0.5, 2.0];
411 let cp = aabb.closest_point(p);
412 assert!((cp[0] - 0.0).abs() < 1e-12, "x={}", cp[0]);
413 assert!((cp[1] - 0.5).abs() < 1e-12, "y={}", cp[1]);
414 assert!((cp[2] - 1.0).abs() < 1e-12, "z={}", cp[2]);
415 }
416 #[test]
417 fn test_aabb_contains_point_strict() {
418 let aabb = Aabb::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
419 assert!(aabb.contains_point_strict([1.0, 1.0, 1.0]));
420 assert!(!aabb.contains_point_strict([0.0, 1.0, 1.0]));
421 assert!(!aabb.contains_point_strict([3.0, 1.0, 1.0]));
422 }
423 #[test]
424 fn test_aabb_contains_point_on_boundary() {
425 let aabb = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
426 assert!(aabb.contains([0.0, 0.5, 0.5]));
427 assert!(aabb.contains([1.0, 0.5, 0.5]));
428 }
429 #[test]
430 fn test_aabb_ray_intersect_extended() {
431 let aabb = Aabb::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
432 let hit = aabb.intersect_ray([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
433 assert!(hit.is_some(), "should hit unit cube");
434 let (t0, t1) = hit.unwrap();
435 assert!((t0 - 4.0).abs() < 1e-10, "t0={}", t0);
436 assert!((t1 - 6.0).abs() < 1e-10, "t1={}", t1);
437 }
438}
439#[cfg(test)]
440mod new_math_tests {
441
442 use crate::Mat3;
443 use crate::Vec3;
444
445 use crate::math::cartesian_to_cylindrical;
446 use crate::math::cartesian_to_spherical;
447 use crate::math::cross_matrix;
448 use crate::math::cylindrical_to_cartesian;
449
450 use crate::math::gram_schmidt;
451
452 use crate::math::mat3_exp_skew;
453 use crate::math::mat3_from_axis_angle;
454
455 use crate::math::mat3_log_rotation;
456
457 use crate::math::quat_from_axis_angle;
458 use crate::math::quat_geodesic_distance;
459
460 use crate::math::quat_nlerp;
461 use crate::math::quat_squad_control;
462 use crate::math::rodrigues_rotate;
463
464 use crate::math::skew_symmetric;
465 use crate::math::spherical_to_cartesian;
466
467 use crate::math::vec3_outer_product;
468
469 #[test]
470 fn test_vec3_outer_product_basic() {
471 let a = Vec3::new(1.0, 2.0, 3.0);
472 let b = Vec3::new(4.0, 5.0, 6.0);
473 let op = vec3_outer_product(&a, &b);
474 assert!((op[(0, 0)] - 4.0).abs() < 1e-12);
475 assert!((op[(0, 1)] - 5.0).abs() < 1e-12);
476 assert!((op[(0, 2)] - 6.0).abs() < 1e-12);
477 assert!((op[(1, 0)] - 8.0).abs() < 1e-12);
478 assert!((op[(2, 2)] - 18.0).abs() < 1e-12);
479 }
480 #[test]
481 fn test_vec3_outer_product_unit_vectors() {
482 let e0 = Vec3::new(1.0, 0.0, 0.0);
483 let e1 = Vec3::new(0.0, 1.0, 0.0);
484 let op = vec3_outer_product(&e0, &e1);
485 assert!((op[(0, 1)] - 1.0).abs() < 1e-12);
486 assert!(op[(0, 0)].abs() < 1e-12);
487 assert!(op[(1, 0)].abs() < 1e-12);
488 }
489 #[test]
490 fn test_vec3_outer_product_trace() {
491 let a = Vec3::new(1.0, 2.0, 3.0);
492 let op = vec3_outer_product(&a, &a);
493 let trace = op.trace();
494 let norm_sq = a.norm_squared();
495 assert!(
496 (trace - norm_sq).abs() < 1e-10,
497 "trace={}, |a|²={}",
498 trace,
499 norm_sq
500 );
501 }
502 #[test]
503 fn test_cross_matrix_equals_skew_symmetric() {
504 let v = Vec3::new(2.0, -3.0, 1.0);
505 let cm = cross_matrix(&v);
506 let ss = skew_symmetric(&v);
507 let diff = (cm - ss).norm();
508 assert!(
509 diff < 1e-12,
510 "cross_matrix should equal skew_symmetric: diff={}",
511 diff
512 );
513 }
514 #[test]
515 fn test_cross_matrix_cross_product_equivalence() {
516 let a = Vec3::new(1.0, 2.0, 3.0);
517 let b = Vec3::new(-1.0, 0.5, 4.0);
518 let expected = a.cross(&b);
519 let actual = cross_matrix(&a) * b;
520 let diff = (expected - actual).norm();
521 assert!(
522 diff < 1e-10,
523 "cross matrix * b should equal a x b: diff={}",
524 diff
525 );
526 }
527 #[test]
528 fn test_gram_schmidt_orthonormality() {
529 let v0 = Vec3::new(1.0, 1.0, 0.0);
530 let v1 = Vec3::new(0.0, 1.0, 1.0);
531 let v2 = Vec3::new(1.0, 0.0, 1.0);
532 let (e0, e1, e2) = gram_schmidt(&v0, &v1, &v2).expect("should succeed");
533 assert!(
534 (e0.norm() - 1.0).abs() < 1e-10,
535 "e0 not unit: {}",
536 e0.norm()
537 );
538 assert!(
539 (e1.norm() - 1.0).abs() < 1e-10,
540 "e1 not unit: {}",
541 e1.norm()
542 );
543 assert!(
544 (e2.norm() - 1.0).abs() < 1e-10,
545 "e2 not unit: {}",
546 e2.norm()
547 );
548 assert!(e0.dot(&e1).abs() < 1e-10, "e0·e1={}", e0.dot(&e1));
549 assert!(e0.dot(&e2).abs() < 1e-10, "e0·e2={}", e0.dot(&e2));
550 assert!(e1.dot(&e2).abs() < 1e-10, "e1·e2={}", e1.dot(&e2));
551 }
552 #[test]
553 fn test_gram_schmidt_span_preserved() {
554 let v0 = Vec3::new(1.0, 0.0, 0.0);
555 let v1 = Vec3::new(1.0, 1.0, 0.0);
556 let v2 = Vec3::new(1.0, 1.0, 1.0);
557 let (e0, e1, e2) = gram_schmidt(&v0, &v1, &v2).expect("should succeed");
558 assert!((e0.x - 1.0).abs() < 1e-10);
559 assert!(e0.y.abs() < 1e-10);
560 assert!(e2.z.abs() > 0.99, "e2.z={}", e2.z);
561 let _ = e1;
562 }
563 #[test]
564 fn test_gram_schmidt_degenerate_returns_none() {
565 let v0 = Vec3::new(1.0, 0.0, 0.0);
566 let v1 = Vec3::new(2.0, 0.0, 0.0);
567 let v2 = Vec3::new(0.0, 1.0, 0.0);
568 let result = gram_schmidt(&v0, &v1, &v2);
569 assert!(result.is_none(), "collinear vectors should return None");
570 }
571 #[test]
572 fn test_cartesian_to_spherical_z_axis() {
573 let v = Vec3::new(0.0, 0.0, 2.0);
574 let (r, theta, _phi) = cartesian_to_spherical(&v);
575 assert!((r - 2.0).abs() < 1e-10, "r={}", r);
576 assert!(theta.abs() < 1e-10, "theta={}", theta);
577 }
578 #[test]
579 fn test_cartesian_to_spherical_x_axis() {
580 let v = Vec3::new(3.0, 0.0, 0.0);
581 let (r, theta, phi) = cartesian_to_spherical(&v);
582 assert!((r - 3.0).abs() < 1e-10, "r={}", r);
583 assert!(
584 (theta - std::f64::consts::FRAC_PI_2).abs() < 1e-10,
585 "theta={}",
586 theta
587 );
588 assert!(phi.abs() < 1e-10, "phi={}", phi);
589 }
590 #[test]
591 fn test_spherical_cartesian_roundtrip() {
592 let v = Vec3::new(1.0, 2.0, 3.0);
593 let (r, theta, phi) = cartesian_to_spherical(&v);
594 let v2 = spherical_to_cartesian(r, theta, phi);
595 let diff = (v - v2).norm();
596 assert!(diff < 1e-10, "roundtrip diff={}", diff);
597 }
598 #[test]
599 fn test_spherical_to_cartesian_unit_radius() {
600 let v = spherical_to_cartesian(1.0, std::f64::consts::FRAC_PI_2, 0.0);
601 assert!((v.x - 1.0).abs() < 1e-10);
602 assert!(v.y.abs() < 1e-10);
603 assert!(v.z.abs() < 1e-10);
604 }
605 #[test]
606 fn test_cylindrical_roundtrip() {
607 let v = Vec3::new(1.0, 2.0, 3.0);
608 let (rho, phi, z) = cartesian_to_cylindrical(&v);
609 let v2 = cylindrical_to_cartesian(rho, phi, z);
610 let diff = (v - v2).norm();
611 assert!(diff < 1e-10, "cylindrical roundtrip diff={}", diff);
612 }
613 #[test]
614 fn test_cylindrical_z_preserved() {
615 let v = Vec3::new(1.0, 1.0, 5.0);
616 let (_, _, z) = cartesian_to_cylindrical(&v);
617 assert!((z - 5.0).abs() < 1e-10, "z={}", z);
618 }
619 #[test]
620 fn test_rodrigues_rotate_90z() {
621 let v = Vec3::new(1.0, 0.0, 0.0);
622 let axis = Vec3::new(0.0, 0.0, 1.0);
623 let r = rodrigues_rotate(&v, &axis, std::f64::consts::FRAC_PI_2);
624 assert!(r.x.abs() < 1e-10, "x={}", r.x);
625 assert!((r.y - 1.0).abs() < 1e-10, "y={}", r.y);
626 assert!(r.z.abs() < 1e-10, "z={}", r.z);
627 }
628 #[test]
629 fn test_rodrigues_rotate_180() {
630 let v = Vec3::new(1.0, 0.0, 0.0);
631 let axis = Vec3::new(0.0, 1.0, 0.0);
632 let r = rodrigues_rotate(&v, &axis, std::f64::consts::PI);
633 assert!((r.x + 1.0).abs() < 1e-10, "x={}", r.x);
634 assert!(r.y.abs() < 1e-10, "y={}", r.y);
635 assert!(r.z.abs() < 1e-10, "z={}", r.z);
636 }
637 #[test]
638 fn test_rodrigues_rotate_zero_angle() {
639 let v = Vec3::new(1.0, 2.0, 3.0);
640 let axis = Vec3::new(0.0, 1.0, 0.0);
641 let r = rodrigues_rotate(&v, &axis, 0.0);
642 let diff = (r - v).norm();
643 assert!(
644 diff < 1e-10,
645 "zero rotation should leave vector unchanged: diff={}",
646 diff
647 );
648 }
649 #[test]
650 fn test_rodrigues_rotate_preserves_magnitude() {
651 let v = Vec3::new(1.0, 2.0, 3.0);
652 let axis = Vec3::new(0.0, 0.0, 1.0);
653 let r = rodrigues_rotate(&v, &axis, 1.234);
654 let diff = (r.norm() - v.norm()).abs();
655 assert!(
656 diff < 1e-10,
657 "rotation should preserve magnitude: diff={}",
658 diff
659 );
660 }
661 #[test]
662 fn test_mat3_exp_skew_zero() {
663 let omega = Vec3::zeros();
664 let r = mat3_exp_skew(&omega);
665 let diff = (r - Mat3::identity()).norm();
666 assert!(diff < 1e-10, "exp(0) should be identity: diff={}", diff);
667 }
668 #[test]
669 fn test_mat3_exp_skew_90z() {
670 let omega = Vec3::new(0.0, 0.0, std::f64::consts::FRAC_PI_2);
671 let r = mat3_exp_skew(&omega);
672 let v = r * Vec3::new(1.0, 0.0, 0.0);
673 assert!(v.x.abs() < 1e-10, "x={}", v.x);
674 assert!((v.y - 1.0).abs() < 1e-10, "y={}", v.y);
675 }
676 #[test]
677 fn test_mat3_exp_skew_is_rotation() {
678 let omega = Vec3::new(0.3, -0.5, 0.7);
679 let r = mat3_exp_skew(&omega);
680 let det = r.determinant();
681 let rrt = r * r.transpose();
682 let diff = (rrt - Mat3::identity()).norm();
683 assert!((det - 1.0).abs() < 1e-8, "det={}", det);
684 assert!(diff < 1e-8, "R R^T should be I: diff={}", diff);
685 }
686 #[test]
687 fn test_mat3_log_rotation_identity() {
688 let r = Mat3::identity();
689 let log = mat3_log_rotation(&r);
690 let diff = log.norm();
691 assert!(diff < 1e-10, "log(I) should be zero: norm={}", diff);
692 }
693 #[test]
694 fn test_mat3_exp_log_roundtrip() {
695 let omega = Vec3::new(0.2, 0.4, -0.3);
696 let r = mat3_exp_skew(&omega);
697 let log_r = mat3_log_rotation(&r);
698 let omega2 = Vec3::new(log_r[(2, 1)], log_r[(0, 2)], log_r[(1, 0)]);
699 let r2 = mat3_exp_skew(&omega2);
700 let diff = (r - r2).norm();
701 assert!(diff < 1e-7, "exp/log roundtrip diff={}", diff);
702 }
703 #[test]
704 fn test_quat_nlerp_at_endpoints() {
705 let a = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.0);
706 let b = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 1.0);
707 let q0 = quat_nlerp(&a, &b, 0.0);
708 let q1 = quat_nlerp(&a, &b, 1.0);
709 assert!(q0.angle_to(&a) < 1e-8, "nlerp(t=0) should be a");
710 assert!(q1.angle_to(&b) < 1e-8, "nlerp(t=1) should be b");
711 }
712 #[test]
713 fn test_quat_nlerp_is_unit() {
714 let a = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.3);
715 let b = quat_from_axis_angle(&Vec3::new(1.0, 0.0, 0.0), 0.9);
716 for i in 0..=10 {
717 let t = i as f64 / 10.0;
718 let q = quat_nlerp(&a, &b, t);
719 let norm = q.into_inner().norm();
720 assert!(
721 (norm - 1.0).abs() < 1e-10,
722 "nlerp result not unit at t={}: norm={}",
723 t,
724 norm
725 );
726 }
727 }
728 #[test]
729 fn test_quat_squad_control_produces_unit_quat() {
730 let q0 = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.0);
731 let q1 = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.5);
732 let q2 = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 1.0);
733 let s = quat_squad_control(&q0, &q1, &q2);
734 let norm = s.into_inner().norm();
735 assert!(
736 (norm - 1.0).abs() < 1e-8,
737 "squad control should be unit quat: norm={}",
738 norm
739 );
740 }
741 #[test]
742 fn test_quat_geodesic_distance_same() {
743 let q = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.7);
744 let d = quat_geodesic_distance(&q, &q);
745 assert!(
746 d.abs() < 1e-10,
747 "geodesic distance to self should be 0: d={}",
748 d
749 );
750 }
751 #[test]
752 fn test_quat_geodesic_distance_known() {
753 let a = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.0);
754 let b = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), std::f64::consts::FRAC_PI_2);
755 let d = quat_geodesic_distance(&a, &b);
756 assert!((d - std::f64::consts::FRAC_PI_2).abs() < 1e-8, "d={}", d);
757 }
758 #[test]
759 fn test_outer_product_rank_one() {
760 let a = Vec3::new(1.0, 2.0, 3.0);
761 let b = Vec3::new(1.0, -1.0, 2.0);
762 let op = vec3_outer_product(&a, &b);
763 let det = op.determinant();
764 assert!(
765 det.abs() < 1e-10,
766 "outer product should have det=0: det={}",
767 det
768 );
769 }
770 #[test]
771 fn test_cross_matrix_antisymmetric() {
772 let v = Vec3::new(1.0, -2.0, 3.0);
773 let cm = cross_matrix(&v);
774 let diff = (cm + cm.transpose()).norm();
775 assert!(
776 diff < 1e-10,
777 "cross matrix should be antisymmetric: diff={}",
778 diff
779 );
780 }
781 #[test]
782 fn test_cross_matrix_zero_diagonal() {
783 let v = Vec3::new(1.0, 2.0, 3.0);
784 let cm = cross_matrix(&v);
785 assert!(cm[(0, 0)].abs() < 1e-12);
786 assert!(cm[(1, 1)].abs() < 1e-12);
787 assert!(cm[(2, 2)].abs() < 1e-12);
788 }
789 #[test]
790 fn test_cartesian_to_spherical_origin() {
791 let v = Vec3::zeros();
792 let (r, theta, phi) = cartesian_to_spherical(&v);
793 assert!(r.abs() < 1e-10, "r should be 0 for origin");
794 assert!(theta.abs() < 1e-10);
795 assert!(phi.abs() < 1e-10);
796 }
797 #[test]
798 fn test_spherical_coordinate_r_is_norm() {
799 let v = Vec3::new(2.0, 3.0, 6.0);
800 let (r, _, _) = cartesian_to_spherical(&v);
801 assert!((r - v.norm()).abs() < 1e-10, "r should equal |v|");
802 }
803 #[test]
804 fn test_gram_schmidt_axis_aligned() {
805 let e0 = Vec3::new(1.0, 0.0, 0.0);
806 let e1 = Vec3::new(0.0, 1.0, 0.0);
807 let e2 = Vec3::new(0.0, 0.0, 1.0);
808 let (g0, g1, g2) = gram_schmidt(&e0, &e1, &e2).expect("axis-aligned should succeed");
809 assert!((g0.dot(&e0).abs() - 1.0).abs() < 1e-10);
810 assert!((g1.dot(&e1).abs() - 1.0).abs() < 1e-10);
811 assert!((g2.dot(&e2).abs() - 1.0).abs() < 1e-10);
812 }
813 #[test]
814 fn test_mat3_log_rotation_90z() {
815 let omega = Vec3::new(0.0, 0.0, std::f64::consts::FRAC_PI_2);
816 let r = mat3_exp_skew(&omega);
817 let log_r = mat3_log_rotation(&r);
818 let recovered_angle = log_r[(1, 0)];
819 assert!(
820 (recovered_angle - std::f64::consts::FRAC_PI_2).abs() < 1e-8,
821 "recovered angle={}",
822 recovered_angle
823 );
824 }
825 #[test]
826 fn test_rodrigues_matches_mat3_from_axis_angle() {
827 let axis = Vec3::new(0.0, 1.0, 0.0);
828 let angle = 0.8_f64;
829 let v = Vec3::new(1.0, 0.5, -0.3);
830 let r1 = rodrigues_rotate(&v, &axis, angle);
831 let r2 = mat3_from_axis_angle(&axis, angle) * v;
832 let diff = (r1 - r2).norm();
833 assert!(
834 diff < 1e-10,
835 "rodrigues_rotate should match mat3_from_axis_angle: diff={}",
836 diff
837 );
838 }
839 #[test]
840 fn test_quat_nlerp_symmetric() {
841 let a = quat_from_axis_angle(&Vec3::new(1.0, 0.0, 0.0), 0.3);
842 let b = quat_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.6);
843 let q1 = quat_nlerp(&a, &b, 0.3);
844 let q2 = quat_nlerp(&b, &a, 0.7);
845 let d = quat_geodesic_distance(&q1, &q2);
846 assert!(d < 1e-8, "nlerp should be symmetric: angle diff={}", d);
847 }
848}
849#[allow(dead_code)]
854pub fn vec3_project_onto_unit(v: &Vec3, onto_unit: &Vec3) -> Vec3 {
855 onto_unit * v.dot(onto_unit)
856}
857#[allow(dead_code)]
860pub fn vec3_reject(v: &Vec3, onto_unit: &Vec3) -> Vec3 {
861 v - vec3_project_onto_unit(v, onto_unit)
862}
863#[allow(dead_code)]
865pub fn vec3_reflect_about_normal(v: &Vec3, n: &Vec3) -> Vec3 {
866 v - n * (2.0 * v.dot(n))
867}
868#[allow(dead_code)]
873pub fn vec3_refract_ratio(v: &Vec3, n: &Vec3, eta_ratio: Real) -> Option<Vec3> {
874 let cos_i = -v.dot(n);
875 let sin2_t = eta_ratio * eta_ratio * (1.0 - cos_i * cos_i);
876 if sin2_t > 1.0 {
877 return None;
878 }
879 let cos_t = (1.0 - sin2_t).sqrt();
880 Some(v * eta_ratio + n * (eta_ratio * cos_i - cos_t))
881}
882#[allow(dead_code)]
884pub fn vec3_min(a: &Vec3, b: &Vec3) -> Vec3 {
885 Vec3::new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z))
886}
887#[allow(dead_code)]
889pub fn vec3_max(a: &Vec3, b: &Vec3) -> Vec3 {
890 Vec3::new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z))
891}
892#[allow(dead_code)]
894pub fn vec3_clamp(v: &Vec3, lo: Real, hi: Real) -> Vec3 {
895 Vec3::new(v.x.clamp(lo, hi), v.y.clamp(lo, hi), v.z.clamp(lo, hi))
896}
897#[allow(dead_code)]
899pub fn vec3_abs(v: &Vec3) -> Vec3 {
900 Vec3::new(v.x.abs(), v.y.abs(), v.z.abs())
901}
902#[allow(dead_code)]
907pub fn vec3_signed_angle(a: &Vec3, b: &Vec3, axis: &Vec3) -> Real {
908 let cross = a.cross(b);
909 let sin_angle = cross.dot(axis);
910 let cos_angle = a.dot(b);
911 sin_angle.atan2(cos_angle)
912}
913#[allow(dead_code)]
919pub fn build_orthonormal_basis(n: &Vec3) -> (Vec3, Vec3, Vec3) {
920 let sign = if n.z >= 0.0 { 1.0 } else { -1.0 };
921 let a = -1.0 / (sign + n.z);
922 let b = n.x * n.y * a;
923 let t = Vec3::new(1.0 + sign * n.x * n.x * a, sign * b, -sign * n.x);
924 let bt = Vec3::new(b, sign + n.y * n.y * a, -n.y);
925 (t, bt, *n)
926}
927#[allow(dead_code)]
932pub fn vec3_decompose(v: &Vec3, axis: &Vec3) -> (Vec3, Vec3) {
933 let parallel = vec3_project_onto_unit(v, axis);
934 let perp = v - parallel;
935 (parallel, perp)
936}
937#[allow(dead_code)]
939pub fn vec3_scalar_triple(a: &Vec3, b: &Vec3, c: &Vec3) -> Real {
940 a.dot(&b.cross(c))
941}
942#[allow(dead_code)]
944pub fn vec3_vector_triple(a: &Vec3, b: &Vec3, c: &Vec3) -> Vec3 {
945 b * a.dot(c) - c * a.dot(b)
946}
947#[allow(dead_code)]
949pub fn point_to_line_distance(p: &Vec3, origin: &Vec3, dir: &Vec3) -> Real {
950 let dp = p - origin;
951 vec3_reject(&dp, dir).norm()
952}
953#[allow(dead_code)]
956pub fn closest_point_on_line(p: &Vec3, origin: &Vec3, dir: &Vec3) -> Vec3 {
957 let t = (p - origin).dot(dir);
958 origin + dir * t
959}
960#[allow(dead_code)]
962pub fn closest_point_on_segment(p: &Vec3, a: &Vec3, b: &Vec3) -> Vec3 {
963 let ab = b - a;
964 let ab_len2 = ab.norm_squared();
965 if ab_len2 < 1e-30 {
966 return *a;
967 }
968 let t = ((p - a).dot(&ab) / ab_len2).clamp(0.0, 1.0);
969 a + ab * t
970}
971#[allow(dead_code)]
973pub fn point_to_segment_distance(p: &Vec3, a: &Vec3, b: &Vec3) -> Real {
974 (p - closest_point_on_segment(p, a, b)).norm()
975}
976#[allow(dead_code)]
982pub fn mat3_rotation_from_to(from: &Vec3, to: &Vec3) -> Mat3 {
983 let dot = from.dot(to).clamp(-1.0, 1.0);
984 if (dot - 1.0).abs() < 1e-12 {
985 return Mat3::identity();
986 }
987 if (dot + 1.0).abs() < 1e-6 {
988 let perp = if from.x.abs() < 0.9 {
989 Vec3::new(0.0, -from.z, from.y).normalize()
990 } else {
991 Vec3::new(-from.z, 0.0, from.x).normalize()
992 };
993 return 2.0 * perp * perp.transpose() - Mat3::identity();
994 }
995 let axis = from.cross(to);
996 let axis_len = axis.norm();
997 if axis_len < 1e-10 {
998 let perp = if from.x.abs() < 0.9 {
999 Vec3::new(0.0, -from.z, from.y).normalize()
1000 } else {
1001 Vec3::new(-from.z, 0.0, from.x).normalize()
1002 };
1003 return 2.0 * perp * perp.transpose() - Mat3::identity();
1004 }
1005 let angle = dot.acos();
1006 mat3_from_axis_angle(&(axis / axis_len), angle)
1007}
1008#[allow(dead_code)]
1012pub fn mat3_rotation_angle(r: &Mat3) -> Real {
1013 let tr = r.trace().clamp(-1.0, 3.0);
1014 ((tr - 1.0) / 2.0).clamp(-1.0, 1.0).acos()
1015}
1016#[allow(dead_code)]
1021pub fn mat3_rotation_axis(r: &Mat3) -> Option<Vec3> {
1022 let s = Vec3::new(
1023 r[(2, 1)] - r[(1, 2)],
1024 r[(0, 2)] - r[(2, 0)],
1025 r[(1, 0)] - r[(0, 1)],
1026 );
1027 let len = s.norm();
1028 if len < 1e-10 {
1029 return None;
1030 }
1031 Some(s / len)
1032}
1033#[allow(dead_code)]
1035pub fn mat3_compose_rotations(r1: &Mat3, r2: &Mat3) -> Mat3 {
1036 r2 * r1
1037}
1038#[allow(dead_code)]
1042pub fn mat3_slerp(r1: &Mat3, r2: &Mat3, t: Real) -> Mat3 {
1043 let r_rel = r1.transpose() * r2;
1044 let log_r_rel = mat3_log_rotation(&r_rel);
1045 let scaled_log = log_r_rel * t;
1046 let omega = Vec3::new(scaled_log[(2, 1)], scaled_log[(0, 2)], scaled_log[(1, 0)]);
1047 r1 * mat3_exp_skew(&omega)
1048}
1049#[allow(dead_code)]
1052pub fn mat3_weighted_mean(rotations: &[Mat3], weights: &[f64], max_iter: usize) -> Mat3 {
1053 assert_eq!(rotations.len(), weights.len());
1054 if rotations.is_empty() {
1055 return Mat3::identity();
1056 }
1057 let mut r_mean = rotations[0];
1058 for _ in 0..max_iter {
1059 let mut delta_sum = Mat3::zeros();
1060 for (r, &w) in rotations.iter().zip(weights.iter()) {
1061 let log = mat3_log_rotation(&(r_mean.transpose() * r));
1062 delta_sum += log * w;
1063 }
1064 if delta_sum.norm() < 1e-12 {
1065 break;
1066 }
1067 let omega = Vec3::new(delta_sum[(2, 1)], delta_sum[(0, 2)], delta_sum[(1, 0)]);
1068 r_mean *= mat3_exp_skew(&omega);
1069 }
1070 r_mean
1071}
1072#[allow(dead_code)]
1074pub fn quat_arr_conjugate(q: [f64; 4]) -> [f64; 4] {
1075 [-q[0], -q[1], -q[2], q[3]]
1076}
1077#[allow(dead_code)]
1079pub fn quat_arr_normalize(q: [f64; 4]) -> [f64; 4] {
1080 let len = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
1081 if len < 1e-30 {
1082 return [0.0, 0.0, 0.0, 1.0];
1083 }
1084 [q[0] / len, q[1] / len, q[2] / len, q[3] / len]
1085}
1086#[allow(dead_code)]
1088pub fn quat_arr_dot(p: [f64; 4], q: [f64; 4]) -> f64 {
1089 p[0] * q[0] + p[1] * q[1] + p[2] * q[2] + p[3] * q[3]
1090}
1091#[allow(dead_code)]
1093pub fn quat_arr_pow(q: [f64; 4], t: f64) -> [f64; 4] {
1094 let log_q = quat_arr_log(q);
1095 let scaled = [log_q[0] * t, log_q[1] * t, log_q[2] * t, 0.0];
1096 quat_arr_exp(scaled)
1097}
1098#[allow(dead_code)]
1101pub fn quat_arr_rotate_vec3(q: [f64; 4], v: [f64; 3]) -> [f64; 3] {
1102 let [qx, qy, qz, qw] = q;
1103 let [vx, vy, vz] = v;
1104 let tx = 2.0 * (qy * vz - qz * vy);
1105 let ty = 2.0 * (qz * vx - qx * vz);
1106 let tz = 2.0 * (qx * vy - qy * vx);
1107 [
1108 vx + qw * tx + qy * tz - qz * ty,
1109 vy + qw * ty + qz * tx - qx * tz,
1110 vz + qw * tz + qx * ty - qy * tx,
1111 ]
1112}
1113#[allow(dead_code)]
1120pub fn quat_arr_squad_control(q_prev: [f64; 4], q_curr: [f64; 4], q_next: [f64; 4]) -> [f64; 4] {
1121 let conj_curr = quat_arr_conjugate(q_curr);
1122 let log_a = quat_arr_log(quat_multiply(conj_curr, q_next));
1123 let log_b = quat_arr_log(quat_multiply(conj_curr, q_prev));
1124 let sum = [
1125 (log_a[0] + log_b[0]) * (-0.25),
1126 (log_a[1] + log_b[1]) * (-0.25),
1127 (log_a[2] + log_b[2]) * (-0.25),
1128 0.0,
1129 ];
1130 quat_multiply(q_curr, quat_arr_exp(sum))
1131}
1132#[allow(dead_code)]
1138pub fn gram_schmidt_vectors(vecs: &[Vec3]) -> Vec<Vec3> {
1139 let mut result: Vec<Vec3> = Vec::with_capacity(vecs.len());
1140 for v in vecs {
1141 let mut u = *v;
1142 for q in &result {
1143 u -= q * u.dot(q);
1144 }
1145 let len = u.norm();
1146 if len < 1e-10 {
1147 let fallbacks = [
1148 Vec3::new(1.0, 0.0, 0.0),
1149 Vec3::new(0.0, 1.0, 0.0),
1150 Vec3::new(0.0, 0.0, 1.0),
1151 ];
1152 for fb in &fallbacks {
1153 let mut candidate = *fb;
1154 for q in &result {
1155 candidate -= q * candidate.dot(q);
1156 }
1157 if candidate.norm() > 1e-10 {
1158 u = candidate.normalize();
1159 break;
1160 }
1161 }
1162 } else {
1163 u /= len;
1164 }
1165 result.push(u);
1166 }
1167 result
1168}
1169#[allow(dead_code)]
1174pub fn mat3_gram_schmidt(m: &Mat3) -> Option<Mat3> {
1175 let c0 = Vec3::new(m[(0, 0)], m[(1, 0)], m[(2, 0)]);
1176 let c1 = Vec3::new(m[(0, 1)], m[(1, 1)], m[(2, 1)]);
1177 let c2 = Vec3::new(m[(0, 2)], m[(1, 2)], m[(2, 2)]);
1178 let (q0, q1, q2) = gram_schmidt(&c0, &c1, &c2)?;
1179 Some(Mat3::from_columns(&[q0, q1, q2]))
1180}
1181#[allow(dead_code)]
1185pub fn vec3_project_onto_plane(v: &Vec3, plane_normal: &Vec3) -> Vec3 {
1186 v - plane_normal * v.dot(plane_normal)
1187}
1188#[allow(dead_code)]
1193pub fn reflect_direction(d: &Vec3, n: &Vec3) -> Vec3 {
1194 d - n * (2.0 * d.dot(n))
1195}
1196#[allow(dead_code)]
1201pub fn barycentric_coords(p: &Vec3, a: &Vec3, b: &Vec3, c: &Vec3) -> Option<(Real, Real, Real)> {
1202 let ab = b - a;
1203 let ac = c - a;
1204 let ap = p - a;
1205 let normal = ab.cross(&ac);
1206 let denom = normal.norm_squared();
1207 if denom < 1e-20 {
1208 return None;
1209 }
1210 let v = normal.dot(&ap.cross(&ac)) / denom;
1211 let w = normal.dot(&ab.cross(&ap)) / denom;
1212 let u = 1.0 - v - w;
1213 Some((u, v, w))
1214}
1215#[allow(dead_code)]
1217pub fn point_in_triangle(p: &Vec3, a: &Vec3, b: &Vec3, c: &Vec3) -> bool {
1218 match barycentric_coords(p, a, b, c) {
1219 None => false,
1220 Some((u, v, w)) => u >= -1e-10 && v >= -1e-10 && w >= -1e-10,
1221 }
1222}
1223#[allow(dead_code)]
1228pub fn cross_product_matrix(a: &Vec3) -> Mat3 {
1229 skew_symmetric(a)
1230}
1231#[allow(dead_code)]
1234pub fn double_cross_matrix(a: &Vec3) -> Mat3 {
1235 let aa = a.norm_squared();
1236 a * a.transpose() - Mat3::identity() * aa
1237}
1238#[allow(dead_code)]
1241pub fn spin_matrix(omega: &Vec3) -> Mat3 {
1242 skew_symmetric(omega)
1243}
1244#[allow(dead_code)]
1249pub fn quat_integrate_rk4(q: &Quat, omega: &Vec3, dt: Real) -> Quat {
1250 let half_dt = dt * 0.5;
1251 let omega_quat = |q_in: &Quat| -> Quat {
1252 let ow = UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(
1253 0.0, omega.x, omega.y, omega.z,
1254 ));
1255 UnitQuaternion::new_normalize(q_in.as_ref() * (ow.as_ref() * 0.5))
1256 };
1257 let k1 = omega_quat(q);
1258 let q2 = UnitQuaternion::new_normalize(q.as_ref() + k1.as_ref() * half_dt);
1259 let k2 = omega_quat(&q2);
1260 let q3 = UnitQuaternion::new_normalize(q.as_ref() + k2.as_ref() * half_dt);
1261 let k3 = omega_quat(&q3);
1262 let q4 = UnitQuaternion::new_normalize(q.as_ref() + k3.as_ref() * dt);
1263 let k4 = omega_quat(&q4);
1264 let sum = k1.as_ref() + k2.as_ref() * 2.0 + k3.as_ref() * 2.0 + k4.as_ref();
1265 UnitQuaternion::new_normalize(q.as_ref() + sum * (dt / 6.0))
1266}
1267#[allow(dead_code)]
1269pub fn quat_blend(q1: &Quat, q2: &Quat, t: Real) -> Quat {
1270 q1.slerp(q2, t)
1271}
1272#[allow(dead_code)]
1276pub fn vec3_to_cylindrical(v: &Vec3) -> (Real, Real, Real) {
1277 let rho = (v.x * v.x + v.y * v.y).sqrt();
1278 let phi = v.y.atan2(v.x);
1279 (rho, phi, v.z)
1280}
1281#[allow(dead_code)]
1283pub fn cylindrical_to_vec3(rho: Real, phi: Real, z: Real) -> Vec3 {
1284 Vec3::new(rho * phi.cos(), rho * phi.sin(), z)
1285}
1286#[allow(dead_code)]
1290pub fn vec3_to_spherical(v: &Vec3) -> (Real, Real, Real) {
1291 let r = v.norm();
1292 if r < 1e-30 {
1293 return (0.0, 0.0, 0.0);
1294 }
1295 let theta = (v.z / r).clamp(-1.0, 1.0).acos();
1296 let phi = v.y.atan2(v.x);
1297 (r, theta, phi)
1298}
1299#[allow(dead_code)]
1301pub fn spherical_to_vec3(r: Real, theta: Real, phi: Real) -> Vec3 {
1302 Vec3::new(
1303 r * theta.sin() * phi.cos(),
1304 r * theta.sin() * phi.sin(),
1305 r * theta.cos(),
1306 )
1307}
1308#[allow(dead_code)]
1313pub fn sample_hemisphere_uniform(u1: Real, u2: Real) -> Vec3 {
1314 let cos_theta = u1;
1315 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
1316 let phi = 2.0 * std::f64::consts::PI * u2;
1317 Vec3::new(sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta)
1318}
1319#[allow(dead_code)]
1323pub fn sample_hemisphere_cosine(u1: Real, u2: Real) -> Vec3 {
1324 let r = u1.sqrt();
1325 let phi = 2.0 * std::f64::consts::PI * u2;
1326 let x = r * phi.cos();
1327 let y = r * phi.sin();
1328 let z = (1.0 - u1).max(0.0).sqrt();
1329 Vec3::new(x, y, z)
1330}
1331#[allow(dead_code)]
1336pub fn sample_unit_disk_concentric(u1: Real, u2: Real) -> (Real, Real) {
1337 let a = 2.0 * u1 - 1.0;
1338 let b = 2.0 * u2 - 1.0;
1339 if a == 0.0 && b == 0.0 {
1340 return (0.0, 0.0);
1341 }
1342 let (r, phi) = if a.abs() > b.abs() {
1343 (a, std::f64::consts::FRAC_PI_4 * (b / a))
1344 } else {
1345 (
1346 b,
1347 std::f64::consts::FRAC_PI_2 - std::f64::consts::FRAC_PI_4 * (a / b),
1348 )
1349 };
1350 (r * phi.cos(), r * phi.sin())
1351}
1352#[cfg(test)]
1353mod extended_math_tests_v2 {
1354
1355 use crate::Mat3;
1356 use crate::Vec3;
1357 use crate::math::barycentric_coords;
1358 use crate::math::build_orthonormal_basis;
1359
1360 use crate::math::cylindrical_to_vec3;
1361 use crate::math::double_cross_matrix;
1362
1363 use crate::math::gram_schmidt_vectors;
1364
1365 use crate::math::mat3_from_axis_angle;
1366 use crate::math::mat3_gram_schmidt;
1367
1368 use crate::math::mat3_rotation_angle;
1369 use crate::math::mat3_rotation_axis;
1370 use crate::math::mat3_rotation_from_to;
1371 use crate::math::mat3_slerp;
1372 use crate::math::mat3_weighted_mean;
1373 use crate::math::point_in_triangle;
1374 use crate::math::point_to_segment_distance;
1375 use crate::math::quat_arr_conjugate;
1376 use crate::math::quat_arr_dot;
1377 use crate::math::quat_arr_from_axis_angle;
1378 use crate::math::quat_arr_normalize;
1379 use crate::math::quat_arr_pow;
1380 use crate::math::quat_arr_rotate_vec3;
1381
1382 use crate::math::quat_multiply;
1383
1384 use crate::math::sample_hemisphere_cosine;
1385 use crate::math::sample_hemisphere_uniform;
1386 use crate::math::sample_unit_disk_concentric;
1387
1388 use crate::math::spherical_to_vec3;
1389 use crate::math::vec3_abs;
1390 use crate::math::vec3_clamp;
1391 use crate::math::vec3_decompose;
1392 use crate::math::vec3_max;
1393 use crate::math::vec3_min;
1394
1395 use crate::math::vec3_project_onto_unit;
1396 use crate::math::vec3_reflect_about_normal;
1397 use crate::math::vec3_refract;
1398 use crate::math::vec3_reject;
1399 use crate::math::vec3_scalar_triple;
1400 use crate::math::vec3_to_cylindrical;
1401 use crate::math::vec3_to_spherical;
1402 #[test]
1403 fn test_project_onto_unit_along_axis() {
1404 let v = Vec3::new(3.0, 4.0, 0.0);
1405 let u = Vec3::new(1.0, 0.0, 0.0);
1406 let proj = vec3_project_onto_unit(&v, &u);
1407 assert!((proj.x - 3.0).abs() < 1e-12);
1408 assert!(proj.y.abs() < 1e-12);
1409 assert!(proj.z.abs() < 1e-12);
1410 }
1411 #[test]
1412 fn test_reject_perpendicular_to_axis() {
1413 let v = Vec3::new(3.0, 4.0, 5.0);
1414 let u = Vec3::new(0.0, 0.0, 1.0);
1415 let rej = vec3_reject(&v, &u);
1416 assert!(rej.dot(&u).abs() < 1e-12, "reject should be perp to axis");
1417 }
1418 #[test]
1419 fn test_project_plus_reject_equals_v() {
1420 let v = Vec3::new(1.0, -2.0, 3.0);
1421 let u = Vec3::new(0.0, 1.0, 0.0);
1422 let proj = vec3_project_onto_unit(&v, &u);
1423 let rej = vec3_reject(&v, &u);
1424 let diff = (proj + rej - v).norm();
1425 assert!(diff < 1e-12, "proj + rej should equal v, diff={}", diff);
1426 }
1427 #[test]
1428 fn test_reflect_normal_preserves_magnitude() {
1429 let v = Vec3::new(1.0, -1.0, 0.0).normalize();
1430 let n = Vec3::new(0.0, 1.0, 0.0);
1431 let r = vec3_reflect_about_normal(&v, &n);
1432 assert!((r.norm() - v.norm()).abs() < 1e-12);
1433 }
1434 #[test]
1435 fn test_reflect_vertical_normal() {
1436 let v = Vec3::new(1.0, -1.0, 0.0);
1437 let n = Vec3::new(0.0, 1.0, 0.0);
1438 let r = vec3_reflect_about_normal(&v, &n);
1439 assert!((r.x - 1.0).abs() < 1e-12);
1440 assert!((r.y - 1.0).abs() < 1e-12);
1441 assert!(r.z.abs() < 1e-12);
1442 }
1443 #[test]
1444 fn test_refract_normal_incidence() {
1445 let v = Vec3::new(0.0, -1.0, 0.0);
1446 let n = Vec3::new(0.0, 1.0, 0.0);
1447 let t = vec3_refract(&v, &n, 1.0).unwrap();
1448 let diff = (t - v).norm();
1449 assert!(diff < 1e-10, "normal incidence same-medium: diff={}", diff);
1450 }
1451 #[test]
1452 fn test_refract_total_internal_reflection() {
1453 let v = Vec3::new(0.99, -0.14, 0.0).normalize();
1454 let n = Vec3::new(0.0, 1.0, 0.0);
1455 let result = vec3_refract(&v, &n, 1.5);
1456 let _ = result;
1457 }
1458 #[test]
1459 fn test_vec3_min_max() {
1460 let a = Vec3::new(1.0, 5.0, 3.0);
1461 let b = Vec3::new(2.0, 3.0, 4.0);
1462 let mn = vec3_min(&a, &b);
1463 let mx = vec3_max(&a, &b);
1464 assert!((mn.x - 1.0).abs() < 1e-12);
1465 assert!((mn.y - 3.0).abs() < 1e-12);
1466 assert!((mn.z - 3.0).abs() < 1e-12);
1467 assert!((mx.x - 2.0).abs() < 1e-12);
1468 assert!((mx.y - 5.0).abs() < 1e-12);
1469 assert!((mx.z - 4.0).abs() < 1e-12);
1470 }
1471 #[test]
1472 fn test_vec3_abs_all_negative() {
1473 let v = Vec3::new(-1.0, -2.0, -3.0);
1474 let a = vec3_abs(&v);
1475 assert!((a.x - 1.0).abs() < 1e-12);
1476 assert!((a.y - 2.0).abs() < 1e-12);
1477 assert!((a.z - 3.0).abs() < 1e-12);
1478 }
1479 #[test]
1480 fn test_vec3_clamp() {
1481 let v = Vec3::new(-5.0, 0.5, 10.0);
1482 let c = vec3_clamp(&v, 0.0, 1.0);
1483 assert!((c.x - 0.0).abs() < 1e-12);
1484 assert!((c.y - 0.5).abs() < 1e-12);
1485 assert!((c.z - 1.0).abs() < 1e-12);
1486 }
1487 #[test]
1488 fn test_build_orthonormal_basis_orthogonal() {
1489 let n = Vec3::new(0.0, 1.0, 0.0);
1490 let (t, bt, nn) = build_orthonormal_basis(&n);
1491 assert!(t.dot(&bt).abs() < 1e-10, "t·bt should be 0");
1492 assert!(t.dot(&nn).abs() < 1e-10, "t·n should be 0");
1493 assert!(bt.dot(&nn).abs() < 1e-10, "bt·n should be 0");
1494 }
1495 #[test]
1496 fn test_build_orthonormal_basis_unit_vectors() {
1497 let n = Vec3::new(1.0, 2.0, 3.0).normalize();
1498 let (t, bt, _) = build_orthonormal_basis(&n);
1499 assert!((t.norm() - 1.0).abs() < 1e-10, "t should be unit");
1500 assert!((bt.norm() - 1.0).abs() < 1e-10, "bt should be unit");
1501 }
1502 #[test]
1503 fn test_vec3_decompose_sums_to_original() {
1504 let v = Vec3::new(1.0, 2.0, 3.0);
1505 let axis = Vec3::new(0.0, 0.0, 1.0);
1506 let (par, perp) = vec3_decompose(&v, &axis);
1507 let diff = (par + perp - v).norm();
1508 assert!(diff < 1e-12, "parallel + perp should be v, diff={}", diff);
1509 }
1510 #[test]
1511 fn test_vec3_decompose_parallel_along_axis() {
1512 let v = Vec3::new(1.0, 2.0, 3.0);
1513 let axis = Vec3::new(0.0, 0.0, 1.0);
1514 let (par, _) = vec3_decompose(&v, &axis);
1515 assert!(par.x.abs() < 1e-12);
1516 assert!(par.y.abs() < 1e-12);
1517 assert!((par.z - 3.0).abs() < 1e-12);
1518 }
1519 #[test]
1520 fn test_scalar_triple_basis_vectors() {
1521 let e0 = Vec3::new(1.0, 0.0, 0.0);
1522 let e1 = Vec3::new(0.0, 1.0, 0.0);
1523 let e2 = Vec3::new(0.0, 0.0, 1.0);
1524 let triple = vec3_scalar_triple(&e0, &e1, &e2);
1525 assert!((triple - 1.0).abs() < 1e-12);
1526 }
1527 #[test]
1528 fn test_scalar_triple_antisymmetry() {
1529 let a = Vec3::new(1.0, 2.0, 3.0);
1530 let b = Vec3::new(4.0, 5.0, 6.0);
1531 let c = Vec3::new(7.0, 8.0, 9.0);
1532 let abc = vec3_scalar_triple(&a, &b, &c);
1533 let bca = vec3_scalar_triple(&b, &c, &a);
1534 assert!(
1535 (abc - bca).abs() < 1e-10,
1536 "cyclic permutation should be equal"
1537 );
1538 }
1539 #[test]
1540 fn test_point_to_segment_endpoint() {
1541 let a = Vec3::new(0.0, 0.0, 0.0);
1542 let b = Vec3::new(1.0, 0.0, 0.0);
1543 let p = Vec3::new(2.0, 0.0, 0.0);
1544 let d = point_to_segment_distance(&p, &a, &b);
1545 assert!((d - 1.0).abs() < 1e-12, "dist should be 1.0, got {}", d);
1546 }
1547 #[test]
1548 fn test_point_to_segment_perpendicular() {
1549 let a = Vec3::new(0.0, 0.0, 0.0);
1550 let b = Vec3::new(2.0, 0.0, 0.0);
1551 let p = Vec3::new(1.0, 3.0, 0.0);
1552 let d = point_to_segment_distance(&p, &a, &b);
1553 assert!((d - 3.0).abs() < 1e-12, "dist should be 3.0, got {}", d);
1554 }
1555 #[test]
1556 fn test_mat3_rotation_from_to_identity() {
1557 let v = Vec3::new(1.0, 0.0, 0.0);
1558 let r = mat3_rotation_from_to(&v, &v);
1559 let diff = (r - Mat3::identity()).norm();
1560 assert!(diff < 1e-10, "from_to same direction should be identity");
1561 }
1562 #[test]
1563 fn test_mat3_rotation_from_to_correct() {
1564 let from = Vec3::new(1.0, 0.0, 0.0);
1565 let to = Vec3::new(0.0, 1.0, 0.0);
1566 let r = mat3_rotation_from_to(&from, &to);
1567 let result = r * from;
1568 let diff = (result - to).norm();
1569 assert!(diff < 1e-10, "rotation_from_to mismatch: diff={}", diff);
1570 }
1571 #[test]
1572 fn test_mat3_rotation_from_to_antiparallel() {
1573 let from = Vec3::new(1.0, 0.0, 0.0);
1574 let to = Vec3::new(-1.0, 0.0, 0.0);
1575 let r = mat3_rotation_from_to(&from, &to);
1576 let result = r * from;
1577 let len = result.norm();
1578 assert!(
1579 (len - 1.0).abs() < 1e-8,
1580 "rotation preserves length: len={}",
1581 len
1582 );
1583 }
1584 #[test]
1585 fn test_mat3_slerp_at_endpoints() {
1586 let r1 = mat3_from_axis_angle(&Vec3::new(0.0, 0.0, 1.0), 0.3);
1587 let r2 = mat3_from_axis_angle(&Vec3::new(0.0, 0.0, 1.0), 0.9);
1588 let s0 = mat3_slerp(&r1, &r2, 0.0);
1589 let s1 = mat3_slerp(&r1, &r2, 1.0);
1590 assert!((s0 - r1).norm() < 1e-8, "slerp(t=0) should be r1");
1591 assert!((s1 - r2).norm() < 1e-8, "slerp(t=1) should be r2");
1592 }
1593 #[test]
1594 fn test_mat3_slerp_midpoint_is_rotation() {
1595 let r1 = mat3_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.0);
1596 let r2 = mat3_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 1.0);
1597 let mid = mat3_slerp(&r1, &r2, 0.5);
1598 let det = mid.determinant();
1599 let rrt = mid * mid.transpose();
1600 let diff = (rrt - Mat3::identity()).norm();
1601 assert!((det - 1.0).abs() < 1e-7, "slerp midpoint det={}", det);
1602 assert!(diff < 1e-7, "slerp midpoint R R^T diff={}", diff);
1603 }
1604 #[test]
1605 fn test_quat_arr_conjugate_product_is_identity() {
1606 let q = quat_arr_from_axis_angle([0.0, 1.0, 0.0], 0.5);
1607 let conj = quat_arr_conjugate(q);
1608 let prod = quat_multiply(q, conj);
1609 assert!(prod[0].abs() < 1e-12);
1610 assert!(prod[1].abs() < 1e-12);
1611 assert!(prod[2].abs() < 1e-12);
1612 assert!((prod[3] - 1.0).abs() < 1e-12);
1613 }
1614 #[test]
1615 fn test_quat_arr_normalize_unit_input() {
1616 let q = [0.0, 0.0, 0.0, 1.0];
1617 let n = quat_arr_normalize(q);
1618 assert!((n[3] - 1.0).abs() < 1e-12);
1619 }
1620 #[test]
1621 fn test_quat_arr_rotate_vec3_z90() {
1622 let q = quat_arr_from_axis_angle([0.0, 0.0, 1.0], std::f64::consts::FRAC_PI_2);
1623 let v = [1.0_f64, 0.0, 0.0];
1624 let r = quat_arr_rotate_vec3(q, v);
1625 assert!(r[0].abs() < 1e-10, "x={}", r[0]);
1626 assert!((r[1] - 1.0).abs() < 1e-10, "y={}", r[1]);
1627 assert!(r[2].abs() < 1e-10, "z={}", r[2]);
1628 }
1629 #[test]
1630 fn test_quat_arr_pow_half_is_sqrt() {
1631 let q = quat_arr_from_axis_angle([0.0, 1.0, 0.0], 0.8);
1632 let q_half = quat_arr_pow(q, 0.5);
1633 let q_reconstructed = quat_multiply(q_half, q_half);
1634 let q_norm = quat_arr_normalize(q_reconstructed);
1635 let dot = quat_arr_dot(q, q_norm).abs();
1636 assert!(dot > 0.9999, "q^0.5 * q^0.5 should equal q: dot={}", dot);
1637 }
1638 #[test]
1639 fn test_gram_schmidt_vectors_orthonormal() {
1640 let vecs = vec![
1641 Vec3::new(1.0, 1.0, 0.0),
1642 Vec3::new(1.0, 0.0, 1.0),
1643 Vec3::new(0.0, 1.0, 1.0),
1644 ];
1645 let qs = gram_schmidt_vectors(&vecs);
1646 assert_eq!(qs.len(), 3);
1647 for i in 0..3 {
1648 assert!((qs[i].norm() - 1.0).abs() < 1e-10, "q[{}] not unit", i);
1649 for j in (i + 1)..3 {
1650 let dot = qs[i].dot(&qs[j]);
1651 assert!(dot.abs() < 1e-10, "q[{}]·q[{}]={} != 0", i, j, dot);
1652 }
1653 }
1654 }
1655 #[test]
1656 fn test_mat3_gram_schmidt_gives_orthogonal_result() {
1657 let m = Mat3::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0);
1658 let q = mat3_gram_schmidt(&m).expect("should succeed for non-singular");
1659 let diff = (q * q.transpose() - Mat3::identity()).norm();
1660 assert!(
1661 diff < 1e-8,
1662 "gram-schmidt result not orthogonal: diff={}",
1663 diff
1664 );
1665 }
1666 #[test]
1667 fn test_barycentric_vertex_a() {
1668 let a = Vec3::new(0.0, 0.0, 0.0);
1669 let b = Vec3::new(1.0, 0.0, 0.0);
1670 let c = Vec3::new(0.0, 1.0, 0.0);
1671 let (u, v, w) = barycentric_coords(&a, &a, &b, &c).unwrap();
1672 assert!((u - 1.0).abs() < 1e-10);
1673 assert!(v.abs() < 1e-10);
1674 assert!(w.abs() < 1e-10);
1675 }
1676 #[test]
1677 fn test_barycentric_centroid() {
1678 let a = Vec3::new(0.0, 0.0, 0.0);
1679 let b = Vec3::new(3.0, 0.0, 0.0);
1680 let c = Vec3::new(0.0, 3.0, 0.0);
1681 let centroid = (a + b + c) / 3.0;
1682 let (u, v, w) = barycentric_coords(¢roid, &a, &b, &c).unwrap();
1683 let third = 1.0 / 3.0;
1684 assert!((u - third).abs() < 1e-10, "u={}", u);
1685 assert!((v - third).abs() < 1e-10, "v={}", v);
1686 assert!((w - third).abs() < 1e-10, "w={}", w);
1687 }
1688 #[test]
1689 fn test_point_in_triangle_inside() {
1690 let a = Vec3::new(0.0, 0.0, 0.0);
1691 let b = Vec3::new(1.0, 0.0, 0.0);
1692 let c = Vec3::new(0.0, 1.0, 0.0);
1693 let p = Vec3::new(0.2, 0.2, 0.0);
1694 assert!(point_in_triangle(&p, &a, &b, &c));
1695 }
1696 #[test]
1697 fn test_point_in_triangle_outside() {
1698 let a = Vec3::new(0.0, 0.0, 0.0);
1699 let b = Vec3::new(1.0, 0.0, 0.0);
1700 let c = Vec3::new(0.0, 1.0, 0.0);
1701 let p = Vec3::new(0.8, 0.8, 0.0);
1702 assert!(!point_in_triangle(&p, &a, &b, &c));
1703 }
1704 #[test]
1705 fn test_sample_hemisphere_uniform_unit_length() {
1706 let v = sample_hemisphere_uniform(0.5, 0.5);
1707 assert!((v.norm() - 1.0).abs() < 1e-12, "norm={}", v.norm());
1708 }
1709 #[test]
1710 fn test_sample_hemisphere_uniform_nonneg_z() {
1711 for i in 0..10 {
1712 let u1 = i as f64 / 10.0;
1713 let u2 = (i + 1) as f64 / 11.0;
1714 let v = sample_hemisphere_uniform(u1, u2);
1715 assert!(v.z >= -1e-12, "z={}", v.z);
1716 }
1717 }
1718 #[test]
1719 fn test_sample_hemisphere_cosine_unit_length() {
1720 let v = sample_hemisphere_cosine(0.3, 0.7);
1721 assert!((v.norm() - 1.0).abs() < 1e-12, "norm={}", v.norm());
1722 }
1723 #[test]
1724 fn test_sample_unit_disk_in_unit_circle() {
1725 for i in 0..10 {
1726 let u1 = i as f64 / 10.0;
1727 let u2 = (i + 3) as f64 / 13.0;
1728 let (x, y) = sample_unit_disk_concentric(u1, u2);
1729 assert!(
1730 x * x + y * y <= 1.0 + 1e-12,
1731 "outside unit circle: {},{}",
1732 x,
1733 y
1734 );
1735 }
1736 }
1737 #[test]
1738 fn test_mat3_rotation_angle_known() {
1739 let angle = std::f64::consts::FRAC_PI_3;
1740 let r = mat3_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), angle);
1741 let extracted = mat3_rotation_angle(&r);
1742 assert!((extracted - angle).abs() < 1e-8, "angle={}", extracted);
1743 }
1744 #[test]
1745 fn test_mat3_rotation_axis_known() {
1746 let r = mat3_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.5);
1747 let axis = mat3_rotation_axis(&r).expect("should have axis");
1748 assert!(axis.y.abs() > 0.99, "axis y={}", axis.y);
1749 }
1750 #[test]
1751 fn test_cylindrical_roundtrip_new() {
1752 let v = Vec3::new(2.0, 3.0, 4.0);
1753 let (rho, phi, z) = vec3_to_cylindrical(&v);
1754 let v2 = cylindrical_to_vec3(rho, phi, z);
1755 let diff = (v - v2).norm();
1756 assert!(diff < 1e-12, "cylindrical roundtrip diff={}", diff);
1757 }
1758 #[test]
1759 fn test_spherical_roundtrip_new() {
1760 let v = Vec3::new(1.0, 2.0, 2.0);
1761 let (r, theta, phi) = vec3_to_spherical(&v);
1762 let v2 = spherical_to_vec3(r, theta, phi);
1763 let diff = (v - v2).norm();
1764 assert!(diff < 1e-12, "spherical roundtrip diff={}", diff);
1765 }
1766 #[test]
1767 fn test_double_cross_matrix_bac_cab() {
1768 let a = Vec3::new(1.0, 2.0, 3.0);
1769 let b = Vec3::new(4.0, 5.0, 6.0);
1770 let dcm = double_cross_matrix(&a);
1771 let via_matrix = dcm * b;
1772 let direct = a * a.dot(&b) - b * a.norm_squared();
1773 let diff = (via_matrix - direct).norm();
1774 assert!(diff < 1e-10, "double cross matrix diff={}", diff);
1775 }
1776 #[test]
1777 fn test_mat3_weighted_mean_single_rotation() {
1778 let r = mat3_from_axis_angle(&Vec3::new(0.0, 1.0, 0.0), 0.7);
1779 let mean = mat3_weighted_mean(&[r], &[1.0], 20);
1780 let diff = (mean - r).norm();
1781 assert!(
1782 diff < 1e-6,
1783 "mean of single rotation should be itself: diff={}",
1784 diff
1785 );
1786 }
1787 #[test]
1788 fn test_mat3_weighted_mean_two_equal_rotations() {
1789 let r = mat3_from_axis_angle(&Vec3::new(1.0, 0.0, 0.0), 0.4);
1790 let mean = mat3_weighted_mean(&[r, r], &[0.5, 0.5], 20);
1791 let diff = (mean - r).norm();
1792 assert!(diff < 1e-6, "mean of same rotation: diff={}", diff);
1793 }
1794}