1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
9#![allow(clippy::too_many_arguments)]
10
11use std::f64::consts::PI;
12
13#[inline]
19fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
20 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
21}
22
23#[inline]
25fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
26 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
27}
28
29#[inline]
31fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
32 [a[0] * s, a[1] * s, a[2] * s]
33}
34
35#[inline]
37fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
38 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
39}
40
41#[inline]
43fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
44 [
45 a[1] * b[2] - a[2] * b[1],
46 a[2] * b[0] - a[0] * b[2],
47 a[0] * b[1] - a[1] * b[0],
48 ]
49}
50
51#[inline]
53fn len_sq(a: [f64; 3]) -> f64 {
54 dot(a, a)
55}
56
57#[inline]
59fn len(a: [f64; 3]) -> f64 {
60 len_sq(a).sqrt()
61}
62
63#[inline]
65fn normalize(a: [f64; 3]) -> [f64; 3] {
66 let l = len(a);
67 if l < 1e-14 {
68 [0.0; 3]
69 } else {
70 scale(a, 1.0 / l)
71 }
72}
73
74#[inline]
76fn vmin(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
77 [a[0].min(b[0]), a[1].min(b[1]), a[2].min(b[2])]
78}
79
80#[inline]
82fn vmax(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
83 [a[0].max(b[0]), a[1].max(b[1]), a[2].max(b[2])]
84}
85
86#[derive(Debug, Clone, Copy)]
92pub struct Ray3 {
93 pub origin: [f64; 3],
95 pub direction: [f64; 3],
97}
98
99impl Ray3 {
100 pub fn new(origin: [f64; 3], direction: [f64; 3]) -> Self {
102 Self {
103 origin,
104 direction: normalize(direction),
105 }
106 }
107
108 pub fn at(&self, t: f64) -> [f64; 3] {
110 add(self.origin, scale(self.direction, t))
111 }
112
113 pub fn intersect_sphere(&self, center: [f64; 3], radius: f64) -> Option<f64> {
115 let oc = sub(self.origin, center);
116 let a = dot(self.direction, self.direction);
117 let b = 2.0 * dot(oc, self.direction);
118 let c = dot(oc, oc) - radius * radius;
119 let disc = b * b - 4.0 * a * c;
120 if disc < 0.0 {
121 return None;
122 }
123 let sqrt_disc = disc.sqrt();
124 let t1 = (-b - sqrt_disc) / (2.0 * a);
125 let t2 = (-b + sqrt_disc) / (2.0 * a);
126 if t1 > 1e-8 {
127 Some(t1)
128 } else if t2 > 1e-8 {
129 Some(t2)
130 } else {
131 None
132 }
133 }
134
135 pub fn intersect_aabb(&self, aabb: &Aabb3) -> Option<(f64, f64)> {
137 let mut t_min = f64::NEG_INFINITY;
138 let mut t_max = f64::INFINITY;
139 for i in 0..3 {
140 let inv_d = if self.direction[i].abs() < 1e-14 {
141 f64::INFINITY
142 } else {
143 1.0 / self.direction[i]
144 };
145 let t0 = (aabb.min[i] - self.origin[i]) * inv_d;
146 let t1 = (aabb.max[i] - self.origin[i]) * inv_d;
147 let (ta, tb) = if inv_d >= 0.0 { (t0, t1) } else { (t1, t0) };
148 t_min = t_min.max(ta);
149 t_max = t_max.min(tb);
150 if t_max < t_min {
151 return None;
152 }
153 }
154 if t_max < 0.0 {
155 None
156 } else {
157 Some((t_min.max(0.0), t_max))
158 }
159 }
160
161 pub fn intersect_triangle(
164 &self,
165 v0: [f64; 3],
166 v1: [f64; 3],
167 v2: [f64; 3],
168 ) -> Option<(f64, f64, f64)> {
169 let eps = 1e-8;
170 let e1 = sub(v1, v0);
171 let e2 = sub(v2, v0);
172 let h = cross(self.direction, e2);
173 let a = dot(e1, h);
174 if a.abs() < eps {
175 return None;
176 }
177 let f = 1.0 / a;
178 let s = sub(self.origin, v0);
179 let u = f * dot(s, h);
180 if !(0.0..=1.0).contains(&u) {
181 return None;
182 }
183 let q = cross(s, e1);
184 let v = f * dot(self.direction, q);
185 if v < 0.0 || u + v > 1.0 {
186 return None;
187 }
188 let t = f * dot(e2, q);
189 if t > eps { Some((t, u, v)) } else { None }
190 }
191}
192
193#[derive(Debug, Clone, Copy)]
199pub struct Aabb3 {
200 pub min: [f64; 3],
202 pub max: [f64; 3],
204}
205
206impl Aabb3 {
207 pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
209 Self { min, max }
210 }
211
212 pub fn empty() -> Self {
214 Self {
215 min: [f64::INFINITY; 3],
216 max: [f64::NEG_INFINITY; 3],
217 }
218 }
219
220 pub fn center(&self) -> [f64; 3] {
222 scale(add(self.min, self.max), 0.5)
223 }
224
225 pub fn half_extents(&self) -> [f64; 3] {
227 scale(sub(self.max, self.min), 0.5)
228 }
229
230 pub fn contains_point(&self, p: [f64; 3]) -> bool {
232 p[0] >= self.min[0]
233 && p[0] <= self.max[0]
234 && p[1] >= self.min[1]
235 && p[1] <= self.max[1]
236 && p[2] >= self.min[2]
237 && p[2] <= self.max[2]
238 }
239
240 pub fn intersects_aabb(&self, other: &Aabb3) -> bool {
242 self.min[0] <= other.max[0]
243 && self.max[0] >= other.min[0]
244 && self.min[1] <= other.max[1]
245 && self.max[1] >= other.min[1]
246 && self.min[2] <= other.max[2]
247 && self.max[2] >= other.min[2]
248 }
249
250 pub fn intersects_sphere(&self, center: [f64; 3], radius: f64) -> bool {
252 let closest = [
253 center[0].clamp(self.min[0], self.max[0]),
254 center[1].clamp(self.min[1], self.max[1]),
255 center[2].clamp(self.min[2], self.max[2]),
256 ];
257 len_sq(sub(closest, center)) <= radius * radius
258 }
259
260 pub fn expand(&self, margin: f64) -> Aabb3 {
262 Aabb3 {
263 min: [
264 self.min[0] - margin,
265 self.min[1] - margin,
266 self.min[2] - margin,
267 ],
268 max: [
269 self.max[0] + margin,
270 self.max[1] + margin,
271 self.max[2] + margin,
272 ],
273 }
274 }
275
276 pub fn surface_area(&self) -> f64 {
278 let e = sub(self.max, self.min);
279 2.0 * (e[0] * e[1] + e[1] * e[2] + e[2] * e[0])
280 }
281
282 pub fn volume(&self) -> f64 {
284 let e = sub(self.max, self.min);
285 e[0] * e[1] * e[2]
286 }
287
288 pub fn merge(&self, other: &Aabb3) -> Aabb3 {
290 Aabb3 {
291 min: vmin(self.min, other.min),
292 max: vmax(self.max, other.max),
293 }
294 }
295}
296
297#[derive(Debug, Clone, Copy)]
303pub struct Sphere3 {
304 pub center: [f64; 3],
306 pub radius: f64,
308}
309
310impl Sphere3 {
311 pub fn new(center: [f64; 3], radius: f64) -> Self {
313 Self { center, radius }
314 }
315
316 pub fn contains_point(&self, p: [f64; 3]) -> bool {
318 len_sq(sub(p, self.center)) <= self.radius * self.radius
319 }
320
321 pub fn intersects_aabb(&self, aabb: &Aabb3) -> bool {
323 aabb.intersects_sphere(self.center, self.radius)
324 }
325
326 pub fn intersects_sphere(&self, other: &Sphere3) -> bool {
328 let dist_sq = len_sq(sub(self.center, other.center));
329 let r_sum = self.radius + other.radius;
330 dist_sq <= r_sum * r_sum
331 }
332
333 pub fn volume(&self) -> f64 {
335 (4.0 / 3.0) * PI * self.radius.powi(3)
336 }
337
338 pub fn surface_area(&self) -> f64 {
340 4.0 * PI * self.radius * self.radius
341 }
342
343 pub fn aabb(&self) -> Aabb3 {
345 Aabb3 {
346 min: [
347 self.center[0] - self.radius,
348 self.center[1] - self.radius,
349 self.center[2] - self.radius,
350 ],
351 max: [
352 self.center[0] + self.radius,
353 self.center[1] + self.radius,
354 self.center[2] + self.radius,
355 ],
356 }
357 }
358}
359
360#[derive(Debug, Clone, Copy)]
366pub struct Plane3 {
367 pub normal: [f64; 3],
369 pub offset: f64,
371}
372
373impl Plane3 {
374 pub fn new(normal: [f64; 3], offset: f64) -> Self {
376 Self {
377 normal: normalize(normal),
378 offset,
379 }
380 }
381
382 pub fn from_points(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> Self {
384 let n = normalize(cross(sub(b, a), sub(c, a)));
385 let d = dot(n, a);
386 Self {
387 normal: n,
388 offset: d,
389 }
390 }
391
392 pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
394 dot(self.normal, p) - self.offset
395 }
396
397 pub fn project_point(&self, p: [f64; 3]) -> [f64; 3] {
399 let d = self.signed_distance(p);
400 sub(p, scale(self.normal, d))
401 }
402
403 pub fn intersect_ray(&self, ray: &Ray3) -> Option<f64> {
405 let denom = dot(self.normal, ray.direction);
406 if denom.abs() < 1e-12 {
407 return None;
408 }
409 let t = (self.offset - dot(self.normal, ray.origin)) / denom;
410 if t >= 0.0 { Some(t) } else { None }
411 }
412
413 pub fn intersect_segment(&self, a: [f64; 3], b: [f64; 3]) -> Option<[f64; 3]> {
416 let da = self.signed_distance(a);
417 let db = self.signed_distance(b);
418 if da * db > 0.0 {
419 return None; }
421 let t = da / (da - db);
422 Some(add(a, scale(sub(b, a), t)))
423 }
424}
425
426#[derive(Debug, Clone, Copy)]
432pub struct Triangle3 {
433 pub v: [[f64; 3]; 3],
435}
436
437impl Triangle3 {
438 pub fn new(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> Self {
440 Self { v: [v0, v1, v2] }
441 }
442
443 pub fn normal_raw(&self) -> [f64; 3] {
445 cross(sub(self.v[1], self.v[0]), sub(self.v[2], self.v[0]))
446 }
447
448 pub fn normal(&self) -> [f64; 3] {
450 normalize(self.normal_raw())
451 }
452
453 pub fn area(&self) -> f64 {
455 len(self.normal_raw()) * 0.5
456 }
457
458 pub fn centroid(&self) -> [f64; 3] {
460 scale(add(add(self.v[0], self.v[1]), self.v[2]), 1.0 / 3.0)
461 }
462
463 pub fn barycentric_coords(&self, p: [f64; 3]) -> (f64, f64, f64) {
465 let v0 = sub(self.v[1], self.v[0]);
466 let v1 = sub(self.v[2], self.v[0]);
467 let v2 = sub(p, self.v[0]);
468 let d00 = dot(v0, v0);
469 let d01 = dot(v0, v1);
470 let d11 = dot(v1, v1);
471 let d20 = dot(v2, v0);
472 let d21 = dot(v2, v1);
473 let denom = d00 * d11 - d01 * d01;
474 if denom.abs() < 1e-14 {
475 return (1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0);
476 }
477 let v_bc = (d11 * d20 - d01 * d21) / denom;
478 let w_bc = (d00 * d21 - d01 * d20) / denom;
479 let u_bc = 1.0 - v_bc - w_bc;
480 (u_bc, v_bc, w_bc)
481 }
482
483 pub fn contains_point_2d(&self, p: [f64; 3]) -> bool {
485 let (u, v, w) = self.barycentric_coords(p);
486 u >= -1e-8 && v >= -1e-8 && w >= -1e-8
487 }
488
489 pub fn plane(&self) -> Plane3 {
491 Plane3::from_points(self.v[0], self.v[1], self.v[2])
492 }
493}
494
495#[derive(Debug, Clone, Copy)]
501pub struct Capsule3 {
502 pub a: [f64; 3],
504 pub b: [f64; 3],
506 pub radius: f64,
508}
509
510impl Capsule3 {
511 pub fn new(a: [f64; 3], b: [f64; 3], radius: f64) -> Self {
513 Self { a, b, radius }
514 }
515
516 pub fn closest_point_on_segment(&self, p: [f64; 3]) -> [f64; 3] {
518 closest_point_segment(self.a, self.b, p)
519 }
520
521 pub fn distance_to_point(&self, p: [f64; 3]) -> f64 {
523 let closest = self.closest_point_on_segment(p);
524 (len(sub(p, closest)) - self.radius).max(0.0)
525 }
526
527 pub fn intersects_sphere(&self, center: [f64; 3], radius: f64) -> bool {
529 let closest = self.closest_point_on_segment(center);
530 let dist_sq = len_sq(sub(closest, center));
531 let r_sum = self.radius + radius;
532 dist_sq <= r_sum * r_sum
533 }
534
535 pub fn segment_length(&self) -> f64 {
537 len(sub(self.b, self.a))
538 }
539}
540
541#[derive(Debug, Clone, Copy)]
547pub struct OrientedBox {
548 pub center: [f64; 3],
550 pub axes: [[f64; 3]; 3],
552 pub half_extents: [f64; 3],
554}
555
556impl OrientedBox {
557 pub fn new(center: [f64; 3], axes: [[f64; 3]; 3], half_extents: [f64; 3]) -> Self {
559 Self {
560 center,
561 axes: [normalize(axes[0]), normalize(axes[1]), normalize(axes[2])],
562 half_extents,
563 }
564 }
565
566 pub fn contains_point(&self, p: [f64; 3]) -> bool {
568 let d = sub(p, self.center);
569 for i in 0..3 {
570 let proj = dot(d, self.axes[i]).abs();
571 if proj > self.half_extents[i] + 1e-8 {
572 return false;
573 }
574 }
575 true
576 }
577
578 pub fn corners(&self) -> [[f64; 3]; 8] {
580 let mut corners = [[0.0; 3]; 8];
581 for i in 0..8 {
582 let sx = if i & 1 == 0 { 1.0 } else { -1.0 };
583 let sy = if i & 2 == 0 { 1.0 } else { -1.0 };
584 let sz = if i & 4 == 0 { 1.0 } else { -1.0 };
585 let offset = add(
586 add(
587 scale(self.axes[0], sx * self.half_extents[0]),
588 scale(self.axes[1], sy * self.half_extents[1]),
589 ),
590 scale(self.axes[2], sz * self.half_extents[2]),
591 );
592 corners[i] = add(self.center, offset);
593 }
594 corners
595 }
596
597 pub fn intersects_aabb(&self, aabb: &Aabb3) -> bool {
599 let obb_aabb = aabb_from_points(&self.corners());
601 obb_aabb.intersects_aabb(aabb)
602 }
603}
604
605#[derive(Debug, Clone)]
611pub struct Frustum {
612 pub planes: [Plane3; 6],
614}
615
616impl Frustum {
617 pub fn new(planes: [Plane3; 6]) -> Self {
619 Self { planes }
620 }
621
622 pub fn contains_aabb(&self, aabb: &Aabb3) -> bool {
624 for plane in &self.planes {
625 let positive = [
627 if plane.normal[0] >= 0.0 {
628 aabb.max[0]
629 } else {
630 aabb.min[0]
631 },
632 if plane.normal[1] >= 0.0 {
633 aabb.max[1]
634 } else {
635 aabb.min[1]
636 },
637 if plane.normal[2] >= 0.0 {
638 aabb.max[2]
639 } else {
640 aabb.min[2]
641 },
642 ];
643 if plane.signed_distance(positive) < 0.0 {
644 return false;
645 }
646 }
647 true
648 }
649
650 pub fn contains_sphere(&self, center: [f64; 3], radius: f64) -> bool {
652 for plane in &self.planes {
653 if plane.signed_distance(center) < -radius {
654 return false;
655 }
656 }
657 true
658 }
659}
660
661#[derive(Debug, Clone)]
667pub struct PolyhedronConvex {
668 pub vertices: Vec<[f64; 3]>,
670 pub face_planes: Vec<Plane3>,
672}
673
674impl PolyhedronConvex {
675 pub fn new(vertices: Vec<[f64; 3]>, face_planes: Vec<Plane3>) -> Self {
677 Self {
678 vertices,
679 face_planes,
680 }
681 }
682
683 pub fn support_point(&self, dir: [f64; 3]) -> [f64; 3] {
685 let mut best = self.vertices[0];
686 let mut best_d = dot(best, dir);
687 for &v in &self.vertices[1..] {
688 let d = dot(v, dir);
689 if d > best_d {
690 best = v;
691 best_d = d;
692 }
693 }
694 best
695 }
696
697 pub fn contains_point(&self, p: [f64; 3]) -> bool {
699 for plane in &self.face_planes {
700 if plane.signed_distance(p) > 1e-8 {
701 return false;
702 }
703 }
704 true
705 }
706
707 pub fn from_box(half_extents: [f64; 3]) -> Self {
709 let [hx, hy, hz] = half_extents;
710 let vertices = vec![
711 [-hx, -hy, -hz],
712 [hx, -hy, -hz],
713 [-hx, hy, -hz],
714 [hx, hy, -hz],
715 [-hx, -hy, hz],
716 [hx, -hy, hz],
717 [-hx, hy, hz],
718 [hx, hy, hz],
719 ];
720 let face_planes = vec![
721 Plane3::new([1.0, 0.0, 0.0], hx),
722 Plane3::new([-1.0, 0.0, 0.0], hx),
723 Plane3::new([0.0, 1.0, 0.0], hy),
724 Plane3::new([0.0, -1.0, 0.0], hy),
725 Plane3::new([0.0, 0.0, 1.0], hz),
726 Plane3::new([0.0, 0.0, -1.0], hz),
727 ];
728 Self {
729 vertices,
730 face_planes,
731 }
732 }
733}
734
735#[derive(Debug, Clone, Copy)]
741pub struct BarycentricCoords {
742 pub u: f64,
744 pub v: f64,
746 pub w: f64,
748}
749
750impl BarycentricCoords {
751 pub fn compute(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], p: [f64; 3]) -> Self {
753 let tri = Triangle3::new(v0, v1, v2);
754 let (u, v, w) = tri.barycentric_coords(p);
755 Self { u, v, w }
756 }
757
758 pub fn is_inside(&self) -> bool {
760 self.u >= -1e-8 && self.v >= -1e-8 && self.w >= -1e-8
761 }
762
763 pub fn interpolate_scalar(&self, s0: f64, s1: f64, s2: f64) -> f64 {
765 self.u * s0 + self.v * s1 + self.w * s2
766 }
767
768 pub fn interpolate_vec(&self, v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
770 add(add(scale(v0, self.u), scale(v1, self.v)), scale(v2, self.w))
771 }
772}
773
774pub fn closest_point_segment(a: [f64; 3], b: [f64; 3], p: [f64; 3]) -> [f64; 3] {
780 let ab = sub(b, a);
781 let ap = sub(p, a);
782 let len_sq_ab = dot(ab, ab);
783 if len_sq_ab < 1e-14 {
784 return a;
785 }
786 let t = (dot(ap, ab) / len_sq_ab).clamp(0.0, 1.0);
787 add(a, scale(ab, t))
788}
789
790pub fn closest_point_triangle(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3], p: [f64; 3]) -> [f64; 3] {
792 let tri = Triangle3::new(v0, v1, v2);
793 let (u, v, w) = tri.barycentric_coords(p);
794 if u >= 0.0 && v >= 0.0 && w >= 0.0 {
796 return add(add(scale(v0, u), scale(v1, v)), scale(v2, w));
797 }
798 let c0 = closest_point_segment(v0, v1, p);
800 let c1 = closest_point_segment(v1, v2, p);
801 let c2 = closest_point_segment(v2, v0, p);
802 let d0 = len_sq(sub(c0, p));
803 let d1 = len_sq(sub(c1, p));
804 let d2 = len_sq(sub(c2, p));
805 if d0 <= d1 && d0 <= d2 {
806 c0
807 } else if d1 <= d2 {
808 c1
809 } else {
810 c2
811 }
812}
813
814pub fn aabb_from_points(points: &[[f64; 3]]) -> Aabb3 {
816 let mut aabb = Aabb3::empty();
817 for &p in points {
818 aabb.min = vmin(aabb.min, p);
819 aabb.max = vmax(aabb.max, p);
820 }
821 aabb
822}
823
824pub fn frustum_from_vp(m: &[[f64; 4]; 4]) -> Frustum {
827 let extract = |a: [f64; 4], b: [f64; 4]| -> Plane3 {
829 let n = [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
830 let d = -(a[3] + b[3]);
831 Plane3::new(n, -d)
832 };
833 let r = m;
834 let row = |i: usize| [r[i][0], r[i][1], r[i][2], r[i][3]];
835 let row3 = row(3);
836 let planes = [
839 extract(row3, row(0)),
840 extract(row3, [-row(0)[0], -row(0)[1], -row(0)[2], -row(0)[3]]),
841 extract(row3, row(1)),
842 extract(row3, [-row(1)[0], -row(1)[1], -row(1)[2], -row(1)[3]]),
843 extract(row3, row(2)),
844 extract(row3, [-row(2)[0], -row(2)[1], -row(2)[2], -row(2)[3]]),
845 ];
846 Frustum::new(planes)
847}
848
849#[allow(dead_code)]
851const HALF_PI: f64 = PI / 2.0;
852
853#[cfg(test)]
858mod tests {
859 use super::*;
860
861 #[test]
864 fn ray_at_t_zero_is_origin() {
865 let ray = Ray3::new([1.0, 2.0, 3.0], [0.0, 0.0, 1.0]);
866 let p = ray.at(0.0);
867 assert!((p[0] - 1.0).abs() < 1e-10);
868 assert!((p[1] - 2.0).abs() < 1e-10);
869 assert!((p[2] - 3.0).abs() < 1e-10);
870 }
871
872 #[test]
873 fn ray_intersect_sphere_hit() {
874 let ray = Ray3::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
875 let t = ray.intersect_sphere([0.0, 0.0, 0.0], 1.0);
876 assert!(t.is_some(), "should hit");
877 assert!((t.unwrap() - 4.0).abs() < 1e-6, "t={}", t.unwrap());
878 }
879
880 #[test]
881 fn ray_intersect_sphere_miss() {
882 let ray = Ray3::new([5.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
883 let t = ray.intersect_sphere([0.0, 0.0, 0.0], 1.0);
884 assert!(t.is_none(), "should miss");
885 }
886
887 #[test]
888 fn ray_intersect_aabb_hit() {
889 let ray = Ray3::new([0.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
890 let aabb = Aabb3::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
891 let result = ray.intersect_aabb(&aabb);
892 assert!(result.is_some(), "should hit AABB");
893 }
894
895 #[test]
896 fn ray_intersect_aabb_miss() {
897 let ray = Ray3::new([5.0, 0.0, -5.0], [0.0, 0.0, 1.0]);
898 let aabb = Aabb3::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
899 let result = ray.intersect_aabb(&aabb);
900 assert!(result.is_none(), "should miss AABB");
901 }
902
903 #[test]
904 fn ray_intersect_triangle_hit() {
905 let ray = Ray3::new([0.0, 0.0, -1.0], [0.0, 0.0, 1.0]);
906 let v0 = [-1.0, -1.0, 0.0];
907 let v1 = [1.0, -1.0, 0.0];
908 let v2 = [0.0, 1.0, 0.0];
909 let result = ray.intersect_triangle(v0, v1, v2);
910 assert!(result.is_some(), "should hit triangle");
911 let (t, _u, _v) = result.unwrap();
912 assert!((t - 1.0).abs() < 1e-6, "t={t}");
913 }
914
915 #[test]
916 fn ray_intersect_triangle_miss() {
917 let ray = Ray3::new([10.0, 0.0, -1.0], [0.0, 0.0, 1.0]);
918 let v0 = [-1.0, -1.0, 0.0];
919 let v1 = [1.0, -1.0, 0.0];
920 let v2 = [0.0, 1.0, 0.0];
921 let result = ray.intersect_triangle(v0, v1, v2);
922 assert!(result.is_none(), "should miss triangle");
923 }
924
925 #[test]
928 fn aabb_contains_center() {
929 let aabb = Aabb3::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
930 assert!(aabb.contains_point([0.0, 0.0, 0.0]));
931 }
932
933 #[test]
934 fn aabb_excludes_outside() {
935 let aabb = Aabb3::new([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
936 assert!(!aabb.contains_point([2.0, 0.0, 0.0]));
937 }
938
939 #[test]
940 fn aabb_intersects_overlapping() {
941 let a = Aabb3::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
942 let b = Aabb3::new([1.0, 1.0, 1.0], [3.0, 3.0, 3.0]);
943 assert!(a.intersects_aabb(&b));
944 }
945
946 #[test]
947 fn aabb_intersects_sphere() {
948 let aabb = Aabb3::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
949 assert!(aabb.intersects_sphere([1.0, 1.0, 1.0], 0.5));
950 assert!(!aabb.intersects_sphere([5.0, 5.0, 5.0], 0.5));
951 }
952
953 #[test]
954 fn aabb_surface_area_unit_cube() {
955 let aabb = Aabb3::new([0.0; 3], [1.0; 3]);
956 assert!((aabb.surface_area() - 6.0).abs() < 1e-10);
957 }
958
959 #[test]
960 fn aabb_volume_unit_cube() {
961 let aabb = Aabb3::new([0.0; 3], [1.0; 3]);
962 assert!((aabb.volume() - 1.0).abs() < 1e-10);
963 }
964
965 #[test]
968 fn sphere_contains_center() {
969 let s = Sphere3::new([0.0; 3], 1.0);
970 assert!(s.contains_point([0.0, 0.0, 0.0]));
971 }
972
973 #[test]
974 fn sphere_excludes_far_point() {
975 let s = Sphere3::new([0.0; 3], 1.0);
976 assert!(!s.contains_point([2.0, 0.0, 0.0]));
977 }
978
979 #[test]
980 fn sphere_sphere_intersection() {
981 let s1 = Sphere3::new([0.0; 3], 1.0);
982 let s2 = Sphere3::new([1.5, 0.0, 0.0], 1.0);
983 assert!(s1.intersects_sphere(&s2));
984 let s3 = Sphere3::new([3.0, 0.0, 0.0], 1.0);
985 assert!(!s1.intersects_sphere(&s3));
986 }
987
988 #[test]
989 fn sphere_volume() {
990 let s = Sphere3::new([0.0; 3], 1.0);
991 let expected = (4.0 / 3.0) * PI;
992 assert!((s.volume() - expected).abs() < 1e-10);
993 }
994
995 #[test]
998 fn plane_signed_distance_positive_side() {
999 let p = Plane3::new([0.0, 1.0, 0.0], 0.0);
1000 assert!((p.signed_distance([0.0, 1.0, 0.0]) - 1.0).abs() < 1e-10);
1001 }
1002
1003 #[test]
1004 fn plane_signed_distance_negative_side() {
1005 let p = Plane3::new([0.0, 1.0, 0.0], 0.0);
1006 assert!((p.signed_distance([0.0, -1.0, 0.0]) + 1.0).abs() < 1e-10);
1007 }
1008
1009 #[test]
1010 fn plane_project_point() {
1011 let p = Plane3::new([0.0, 1.0, 0.0], 0.0);
1012 let proj = p.project_point([1.0, 5.0, 2.0]);
1013 assert!(proj[1].abs() < 1e-10, "proj.y={}", proj[1]);
1014 }
1015
1016 #[test]
1017 fn plane_intersect_ray() {
1018 let plane = Plane3::new([0.0, 1.0, 0.0], 0.0);
1019 let ray = Ray3::new([0.0, 5.0, 0.0], [0.0, -1.0, 0.0]);
1020 let t = plane.intersect_ray(&ray);
1021 assert!(t.is_some());
1022 assert!((t.unwrap() - 5.0).abs() < 1e-6, "t={}", t.unwrap());
1023 }
1024
1025 #[test]
1026 fn plane_from_points() {
1027 let p = Plane3::from_points([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1028 assert!(p.normal[2].abs() > 0.99);
1030 }
1031
1032 #[test]
1035 fn triangle_area_right_triangle() {
1036 let tri = Triangle3::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1037 assert!((tri.area() - 0.5).abs() < 1e-10, "area={}", tri.area());
1038 }
1039
1040 #[test]
1041 fn triangle_centroid() {
1042 let tri = Triangle3::new([0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]);
1043 let c = tri.centroid();
1044 assert!((c[0] - 1.0).abs() < 1e-10);
1045 assert!((c[1] - 1.0).abs() < 1e-10);
1046 }
1047
1048 #[test]
1049 fn triangle_barycentric_centroid() {
1050 let v0 = [0.0, 0.0, 0.0];
1051 let v1 = [3.0, 0.0, 0.0];
1052 let v2 = [0.0, 3.0, 0.0];
1053 let c = [1.0, 1.0, 0.0];
1054 let tri = Triangle3::new(v0, v1, v2);
1055 let (u, v, w) = tri.barycentric_coords(c);
1056 assert!((u + v + w - 1.0).abs() < 1e-6, "sum={}", u + v + w);
1057 assert!(u >= 0.0 && v >= 0.0 && w >= 0.0, "u={u}, v={v}, w={w}");
1058 }
1059
1060 #[test]
1063 fn capsule_distance_to_surface() {
1064 let cap = Capsule3::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1065 let d = cap.distance_to_point([1.0, 0.5, 0.0]);
1066 assert!((d - 0.5).abs() < 1e-6, "d={d}");
1067 }
1068
1069 #[test]
1070 fn capsule_inside_sphere_intersection() {
1071 let cap = Capsule3::new([0.0, 0.0, 0.0], [0.0, 2.0, 0.0], 0.5);
1072 assert!(cap.intersects_sphere([0.0, 1.0, 0.0], 0.3));
1073 }
1074
1075 #[test]
1076 fn capsule_no_intersection() {
1077 let cap = Capsule3::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.1);
1078 assert!(!cap.intersects_sphere([5.0, 0.5, 0.0], 0.1));
1079 }
1080
1081 #[test]
1084 fn obb_contains_center() {
1085 let obb = OrientedBox::new(
1086 [0.0; 3],
1087 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1088 [1.0, 1.0, 1.0],
1089 );
1090 assert!(obb.contains_point([0.0, 0.0, 0.0]));
1091 }
1092
1093 #[test]
1094 fn obb_excludes_far_point() {
1095 let obb = OrientedBox::new(
1096 [0.0; 3],
1097 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1098 [1.0, 1.0, 1.0],
1099 );
1100 assert!(!obb.contains_point([2.0, 0.0, 0.0]));
1101 }
1102
1103 #[test]
1104 fn obb_corners_count() {
1105 let obb = OrientedBox::new(
1106 [0.0; 3],
1107 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1108 [1.0, 1.0, 1.0],
1109 );
1110 let corners = obb.corners();
1111 assert_eq!(corners.len(), 8);
1112 }
1113
1114 #[test]
1117 fn frustum_contains_center_sphere() {
1118 let planes = [
1120 Plane3::new([1.0, 0.0, 0.0], -10.0),
1121 Plane3::new([-1.0, 0.0, 0.0], -10.0),
1122 Plane3::new([0.0, 1.0, 0.0], -10.0),
1123 Plane3::new([0.0, -1.0, 0.0], -10.0),
1124 Plane3::new([0.0, 0.0, 1.0], -10.0),
1125 Plane3::new([0.0, 0.0, -1.0], -10.0),
1126 ];
1127 let frustum = Frustum::new(planes);
1128 assert!(frustum.contains_sphere([0.0, 0.0, 0.0], 0.5));
1129 }
1130
1131 #[test]
1134 fn polyhedron_box_contains_center() {
1135 let poly = PolyhedronConvex::from_box([1.0, 1.0, 1.0]);
1136 assert!(poly.contains_point([0.0, 0.0, 0.0]));
1137 }
1138
1139 #[test]
1140 fn polyhedron_box_excludes_outside() {
1141 let poly = PolyhedronConvex::from_box([1.0, 1.0, 1.0]);
1142 assert!(!poly.contains_point([2.0, 0.0, 0.0]));
1143 }
1144
1145 #[test]
1146 fn polyhedron_support_point() {
1147 let poly = PolyhedronConvex::from_box([1.0, 1.0, 1.0]);
1148 let sp = poly.support_point([1.0, 0.0, 0.0]);
1149 assert!((sp[0] - 1.0).abs() < 1e-10, "support_x={}", sp[0]);
1150 }
1151
1152 #[test]
1155 fn barycentric_vertex_is_canonical() {
1156 let bc = BarycentricCoords::compute(
1157 [1.0, 0.0, 0.0],
1158 [0.0, 1.0, 0.0],
1159 [0.0, 0.0, 1.0],
1160 [1.0, 0.0, 0.0],
1161 );
1162 assert!((bc.u - 1.0).abs() < 1e-6, "u={}", bc.u);
1163 assert!(bc.v.abs() < 1e-6, "v={}", bc.v);
1164 assert!(bc.w.abs() < 1e-6, "w={}", bc.w);
1165 }
1166
1167 #[test]
1168 fn barycentric_interpolate_scalar() {
1169 let bc = BarycentricCoords {
1170 u: 0.5,
1171 v: 0.3,
1172 w: 0.2,
1173 };
1174 let val = bc.interpolate_scalar(1.0, 2.0, 3.0);
1175 assert!((val - 1.7).abs() < 1e-10, "val={val}");
1177 }
1178
1179 #[test]
1182 fn closest_point_segment_midpoint() {
1183 let a = [0.0, 0.0, 0.0];
1184 let b = [2.0, 0.0, 0.0];
1185 let p = [1.0, 1.0, 0.0];
1186 let cp = closest_point_segment(a, b, p);
1187 assert!((cp[0] - 1.0).abs() < 1e-10);
1188 assert!(cp[1].abs() < 1e-10);
1189 }
1190
1191 #[test]
1192 fn closest_point_segment_clamp_a() {
1193 let a = [0.0, 0.0, 0.0];
1194 let b = [1.0, 0.0, 0.0];
1195 let p = [-5.0, 0.0, 0.0];
1196 let cp = closest_point_segment(a, b, p);
1197 assert!((cp[0] - 0.0).abs() < 1e-10);
1198 }
1199
1200 #[test]
1201 fn aabb_from_points_simple() {
1202 let points = [[0.0, 0.0, 0.0], [1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]];
1203 let aabb = aabb_from_points(&points);
1204 assert!((aabb.min[0] + 1.0).abs() < 1e-10);
1205 assert!((aabb.max[2] - 3.0).abs() < 1e-10);
1206 }
1207
1208 #[test]
1209 fn aabb_expand_increases_bounds() {
1210 let aabb = Aabb3::new([0.0; 3], [1.0; 3]);
1211 let expanded = aabb.expand(0.5);
1212 assert!((expanded.min[0] + 0.5).abs() < 1e-10);
1213 assert!((expanded.max[0] - 1.5).abs() < 1e-10);
1214 }
1215
1216 #[test]
1217 fn sphere_aabb_correct_bounds() {
1218 let s = Sphere3::new([1.0, 2.0, 3.0], 1.0);
1219 let aabb = s.aabb();
1220 assert!((aabb.min[0] - 0.0).abs() < 1e-10);
1221 assert!((aabb.max[0] - 2.0).abs() < 1e-10);
1222 }
1223}