1use super::types::{
6 AllPairsRayResult, NearestShape, PointQueryResult, QueryBox, QueryCapsule, QueryShapeRef,
7 QuerySphere, RayCastResult, SegmentDistResult, SphereCastResult, ToiResult, ViewFrustum,
8};
9
10pub(super) fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
11 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
12}
13pub(super) fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
14 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
15}
16pub(super) fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
17 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
18}
19pub(super) fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
20 [a[0] * s, a[1] * s, a[2] * s]
21}
22pub(super) fn norm(a: [f64; 3]) -> f64 {
23 dot(a, a).sqrt()
24}
25pub(super) fn normalize(a: [f64; 3]) -> [f64; 3] {
26 let n = norm(a);
27 if n < 1e-15 {
28 [0.0, 0.0, 0.0]
29 } else {
30 scale(a, 1.0 / n)
31 }
32}
33pub(super) fn clamp(v: f64, lo: f64, hi: f64) -> f64 {
34 if v < lo {
35 lo
36 } else if v > hi {
37 hi
38 } else {
39 v
40 }
41}
42pub fn transform_point(p: [f64; 3], t: [[f64; 4]; 4]) -> [f64; 3] {
44 [
45 t[0][0] * p[0] + t[0][1] * p[1] + t[0][2] * p[2] + t[0][3],
46 t[1][0] * p[0] + t[1][1] * p[1] + t[1][2] * p[2] + t[1][3],
47 t[2][0] * p[0] + t[2][1] * p[1] + t[2][2] * p[2] + t[2][3],
48 ]
49}
50pub(super) fn invert_transform(t: [[f64; 4]; 4]) -> [[f64; 4]; 4] {
52 let r = [
53 [t[0][0], t[1][0], t[2][0]],
54 [t[0][1], t[1][1], t[2][1]],
55 [t[0][2], t[1][2], t[2][2]],
56 ];
57 let tr = [t[0][3], t[1][3], t[2][3]];
58 let neg_rt = [
59 -(r[0][0] * tr[0] + r[0][1] * tr[1] + r[0][2] * tr[2]),
60 -(r[1][0] * tr[0] + r[1][1] * tr[1] + r[1][2] * tr[2]),
61 -(r[2][0] * tr[0] + r[2][1] * tr[1] + r[2][2] * tr[2]),
62 ];
63 [
64 [r[0][0], r[0][1], r[0][2], neg_rt[0]],
65 [r[1][0], r[1][1], r[1][2], neg_rt[1]],
66 [r[2][0], r[2][1], r[2][2], neg_rt[2]],
67 [0.0, 0.0, 0.0, 1.0],
68 ]
69}
70pub fn transform_ray(origin: [f64; 3], dir: [f64; 3], t: [[f64; 4]; 4]) -> ([f64; 3], [f64; 3]) {
72 let inv = invert_transform(t);
73 let new_origin = transform_point(origin, inv);
74 let new_dir = [
75 inv[0][0] * dir[0] + inv[0][1] * dir[1] + inv[0][2] * dir[2],
76 inv[1][0] * dir[0] + inv[1][1] * dir[1] + inv[1][2] * dir[2],
77 inv[2][0] * dir[0] + inv[2][1] * dir[1] + inv[2][2] * dir[2],
78 ];
79 (new_origin, new_dir)
80}
81pub trait ShapeQuery {
83 fn ray_cast(
86 &self,
87 ray_origin: [f64; 3],
88 ray_dir: [f64; 3],
89 max_toi: f64,
90 ) -> Option<RayCastResult>;
91 fn point_query(&self, point: [f64; 3]) -> PointQueryResult;
94 fn aabb(&self) -> ([f64; 3], [f64; 3]);
96}
97pub fn closest_point_on_segment(p: [f64; 3], p0: [f64; 3], p1: [f64; 3]) -> ([f64; 3], f64) {
100 let ab = sub(p1, p0);
101 let ap = sub(p, p0);
102 let len_sq = dot(ab, ab);
103 if len_sq < 1e-30 {
104 return (p0, 0.0);
105 }
106 let t = clamp(dot(ap, ab) / len_sq, 0.0, 1.0);
107 let point = add(p0, scale(ab, t));
108 (point, t)
109}
110pub fn dist_point_to_segment(p: [f64; 3], p0: [f64; 3], p1: [f64; 3]) -> f64 {
112 let (cp, _) = closest_point_on_segment(p, p0, p1);
113 norm(sub(p, cp))
114}
115pub fn closest_point_on_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
118 let ab = sub(b, a);
119 let ac = sub(c, a);
120 let ap = sub(p, a);
121 let d1 = dot(ab, ap);
122 let d2 = dot(ac, ap);
123 if d1 <= 0.0 && d2 <= 0.0 {
124 return a;
125 }
126 let bp = sub(p, b);
127 let d3 = dot(ab, bp);
128 let d4 = dot(ac, bp);
129 if d3 >= 0.0 && d4 <= d3 {
130 return b;
131 }
132 let cp_pt = sub(p, c);
133 let d5 = dot(ab, cp_pt);
134 let d6 = dot(ac, cp_pt);
135 if d6 >= 0.0 && d5 <= d6 {
136 return c;
137 }
138 let vc = d1 * d4 - d3 * d2;
139 if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
140 let v = d1 / (d1 - d3);
141 return add(a, scale(ab, v));
142 }
143 let vb = d5 * d2 - d1 * d6;
144 if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
145 let w = d2 / (d2 - d6);
146 return add(a, scale(ac, w));
147 }
148 let va = d3 * d6 - d5 * d4;
149 if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
150 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
151 return add(b, scale(sub(c, b), w));
152 }
153 let denom = 1.0 / (va + vb + vc);
154 let v = vb * denom;
155 let w = vc * denom;
156 add(a, add(scale(ab, v), scale(ac, w)))
157}
158pub fn dist_point_to_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
161 let cp = closest_point_on_triangle(p, a, b, c);
162 norm(sub(p, cp))
163}
164pub fn point_in_convex_hull(p: [f64; 3], planes: &[[f64; 4]]) -> bool {
169 for plane in planes {
170 let n = [plane[0], plane[1], plane[2]];
171 let d = plane[3];
172 if dot(p, n) + d < -1e-10 {
173 return false;
174 }
175 }
176 true
177}
178pub fn point_in_convex_mesh(p: [f64; 3], triangles: &[([f64; 3], [f64; 3], [f64; 3])]) -> bool {
184 for &(a, b, c) in triangles {
185 let ab = sub(b, a);
186 let ac = sub(c, a);
187 let n = normalize(cross3(ab, ac));
188 let d = dot(n, a);
189 if dot(n, p) - d > 1e-8 {
190 return false;
191 }
192 }
193 true
194}
195pub fn signed_dist_to_sphere(p: [f64; 3], center: [f64; 3], radius: f64) -> f64 {
198 norm(sub(p, center)) - radius
199}
200pub fn signed_dist_to_box(p: [f64; 3], box_center: [f64; 3], he: [f64; 3]) -> f64 {
203 let q = [
204 (p[0] - box_center[0]).abs() - he[0],
205 (p[1] - box_center[1]).abs() - he[1],
206 (p[2] - box_center[2]).abs() - he[2],
207 ];
208 let outside_dist = norm([q[0].max(0.0), q[1].max(0.0), q[2].max(0.0)]);
209 let inside_dist = q[0].max(q[1]).max(q[2]).min(0.0);
210 outside_dist + inside_dist
211}
212pub fn signed_dist_to_capsule(p: [f64; 3], p0: [f64; 3], p1: [f64; 3], r: f64) -> f64 {
214 let (cp, _) = closest_point_on_segment(p, p0, p1);
215 norm(sub(p, cp)) - r
216}
217pub fn ray_vs_sphere(
220 ray_origin: [f64; 3],
221 ray_dir: [f64; 3],
222 center: [f64; 3],
223 radius: f64,
224 max_toi: f64,
225) -> Option<f64> {
226 let sphere = QuerySphere { center, radius };
227 sphere.ray_cast(ray_origin, ray_dir, max_toi).map(|r| r.toi)
228}
229pub fn ray_vs_box(
231 ray_origin: [f64; 3],
232 ray_dir: [f64; 3],
233 box_center: [f64; 3],
234 half_extents: [f64; 3],
235 max_toi: f64,
236) -> Option<f64> {
237 let b = QueryBox {
238 center: box_center,
239 half_extents,
240 };
241 b.ray_cast(ray_origin, ray_dir, max_toi).map(|r| r.toi)
242}
243pub fn ray_vs_triangle(
246 ray_origin: [f64; 3],
247 ray_dir: [f64; 3],
248 v0: [f64; 3],
249 v1: [f64; 3],
250 v2: [f64; 3],
251 max_toi: f64,
252) -> Option<f64> {
253 let edge1 = sub(v1, v0);
254 let edge2 = sub(v2, v0);
255 let h = cross3(ray_dir, edge2);
256 let a = dot(edge1, h);
257 if a.abs() < 1e-10 {
258 return None;
259 }
260 let f = 1.0 / a;
261 let s = sub(ray_origin, v0);
262 let u = f * dot(s, h);
263 if !(0.0..=1.0).contains(&u) {
264 return None;
265 }
266 let q = cross3(s, edge1);
267 let v = f * dot(ray_dir, q);
268 if v < 0.0 || u + v > 1.0 {
269 return None;
270 }
271 let t = f * dot(edge2, q);
272 if t >= 0.0 && t <= max_toi {
273 Some(t)
274 } else {
275 None
276 }
277}
278pub fn sphere_sweep_vs_sphere(
282 start: [f64; 3],
283 velocity: [f64; 3],
284 moving_radius: f64,
285 static_center: [f64; 3],
286 static_radius: f64,
287 max_t: f64,
288) -> Option<f64> {
289 let combined_r = moving_radius + static_radius;
290 let dx = start[0] - static_center[0];
291 let dy = start[1] - static_center[1];
292 let dz = start[2] - static_center[2];
293 let a = dot(velocity, velocity);
294 let b = 2.0 * (dx * velocity[0] + dy * velocity[1] + dz * velocity[2]);
295 let c = dx * dx + dy * dy + dz * dz - combined_r * combined_r;
296 if a < 1e-30 {
297 return if c <= 0.0 { Some(0.0) } else { None };
298 }
299 let disc = b * b - 4.0 * a * c;
300 if disc < 0.0 {
301 return None;
302 }
303 let sqrt_disc = disc.sqrt();
304 let t1 = (-b - sqrt_disc) / (2.0 * a);
305 let t2 = (-b + sqrt_disc) / (2.0 * a);
306 if t1 >= 0.0 && t1 <= max_t {
307 Some(t1)
308 } else if t2 >= 0.0 && t2 <= max_t {
309 Some(t2)
310 } else {
311 None
312 }
313}
314pub fn sphere_sweep_vs_box(
318 start: [f64; 3],
319 velocity: [f64; 3],
320 sphere_radius: f64,
321 box_center: [f64; 3],
322 box_half_extents: [f64; 3],
323 max_t: f64,
324) -> Option<f64> {
325 let expanded_he = [
326 box_half_extents[0] + sphere_radius,
327 box_half_extents[1] + sphere_radius,
328 box_half_extents[2] + sphere_radius,
329 ];
330 let b = QueryBox {
331 center: box_center,
332 half_extents: expanded_he,
333 };
334 b.ray_cast(start, velocity, max_t).map(|r| r.toi)
335}
336pub(super) fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
337 [
338 a[1] * b[2] - a[2] * b[1],
339 a[2] * b[0] - a[0] * b[2],
340 a[0] * b[1] - a[1] * b[0],
341 ]
342}
343pub fn aabb_overlap(min_a: [f64; 3], max_a: [f64; 3], min_b: [f64; 3], max_b: [f64; 3]) -> bool {
347 min_a[0] <= max_b[0]
348 && max_a[0] >= min_b[0]
349 && min_a[1] <= max_b[1]
350 && max_a[1] >= min_b[1]
351 && min_a[2] <= max_b[2]
352 && max_a[2] >= min_b[2]
353}
354pub fn sphere_sphere_overlap(
356 center_a: [f64; 3],
357 radius_a: f64,
358 center_b: [f64; 3],
359 radius_b: f64,
360) -> bool {
361 let d = sub(center_a, center_b);
362 let dist_sq = dot(d, d);
363 let r_sum = radius_a + radius_b;
364 dist_sq <= r_sum * r_sum
365}
366pub fn sphere_aabb_overlap(
370 center: [f64; 3],
371 radius: f64,
372 box_center: [f64; 3],
373 box_half_extents: [f64; 3],
374) -> bool {
375 let lo = sub(box_center, box_half_extents);
376 let hi = add(box_center, box_half_extents);
377 let closest = [
378 clamp(center[0], lo[0], hi[0]),
379 clamp(center[1], lo[1], hi[1]),
380 clamp(center[2], lo[2], hi[2]),
381 ];
382 let d = sub(center, closest);
383 dot(d, d) <= radius * radius
384}
385pub fn capsule_capsule_overlap(
390 p0_a: [f64; 3],
391 p1_a: [f64; 3],
392 radius_a: f64,
393 p0_b: [f64; 3],
394 p1_b: [f64; 3],
395 radius_b: f64,
396) -> bool {
397 let da = sub(p1_a, p0_a);
398 let db = sub(p1_b, p0_b);
399 let r = sub(p0_a, p0_b);
400 let a = dot(da, da);
401 let e = dot(db, db);
402 let f = dot(db, r);
403 let (s, t) = if a <= 1e-15 && e <= 1e-15 {
404 (0.0, 0.0)
405 } else if a <= 1e-15 {
406 (0.0, clamp(f / e, 0.0, 1.0))
407 } else {
408 let c = dot(da, r);
409 if e <= 1e-15 {
410 (clamp(-c / a, 0.0, 1.0), 0.0)
411 } else {
412 let b = dot(da, db);
413 let denom = a * e - b * b;
414 let s = if denom.abs() > 1e-15 {
415 clamp((b * f - c * e) / denom, 0.0, 1.0)
416 } else {
417 0.0
418 };
419 let t = (b * s + f) / e;
420 let (t_clamped, s_final) = if t < 0.0 {
421 (0.0, clamp(-c / a, 0.0, 1.0))
422 } else if t > 1.0 {
423 (1.0, clamp((b - c) / a, 0.0, 1.0))
424 } else {
425 (t, s)
426 };
427 (s_final, t_clamped)
428 }
429 };
430 let closest_a = add(p0_a, scale(da, s));
431 let closest_b = add(p0_b, scale(db, t));
432 let d = sub(closest_a, closest_b);
433 let r_sum = radius_a + radius_b;
434 dot(d, d) <= r_sum * r_sum
435}
436pub fn ray_cast_all_pairs(
440 ray_origin: [f64; 3],
441 ray_dir: [f64; 3],
442 max_toi: f64,
443 shapes: &[QueryShapeRef<'_>],
444) -> Vec<AllPairsRayResult> {
445 let mut results: Vec<AllPairsRayResult> = Vec::new();
446 for (idx, shape) in shapes.iter().enumerate() {
447 let hit = match shape {
448 QueryShapeRef::Sphere(s) => s.ray_cast(ray_origin, ray_dir, max_toi),
449 QueryShapeRef::Box(b) => b.ray_cast(ray_origin, ray_dir, max_toi),
450 QueryShapeRef::Capsule(c) => c.ray_cast(ray_origin, ray_dir, max_toi),
451 };
452 if let Some(r) = hit {
453 results.push(AllPairsRayResult {
454 shape_index: idx,
455 toi: r.toi,
456 normal: r.normal,
457 point: r.point,
458 });
459 }
460 }
461 results.sort_by(|a, b| {
462 a.toi
463 .partial_cmp(&b.toi)
464 .unwrap_or(std::cmp::Ordering::Equal)
465 });
466 results
467}
468pub fn point_containment_all_pairs(point: [f64; 3], shapes: &[QueryShapeRef<'_>]) -> Vec<usize> {
472 let mut inside = Vec::new();
473 for (idx, shape) in shapes.iter().enumerate() {
474 let r = match shape {
475 QueryShapeRef::Sphere(s) => s.point_query(point),
476 QueryShapeRef::Box(b) => b.point_query(point),
477 QueryShapeRef::Capsule(c) => c.point_query(point),
478 };
479 if r.inside {
480 inside.push(idx);
481 }
482 }
483 inside
484}
485pub fn frustum_query(frustum: &ViewFrustum, shapes: &[QueryShapeRef<'_>]) -> Vec<usize> {
489 let mut visible = Vec::new();
490 for (idx, shape) in shapes.iter().enumerate() {
491 let visible_flag = match shape {
492 QueryShapeRef::Sphere(s) => frustum.test_sphere(s.center, s.radius),
493 QueryShapeRef::Box(b) => {
494 let (lo, hi) = b.aabb();
495 frustum.test_aabb(lo, hi)
496 }
497 QueryShapeRef::Capsule(c) => {
498 let (lo, hi) = c.aabb();
499 frustum.test_aabb(lo, hi)
500 }
501 };
502 if visible_flag {
503 visible.push(idx);
504 }
505 }
506 visible
507}
508pub fn k_nearest_shapes(
512 query_point: [f64; 3],
513 shapes: &[QueryShapeRef<'_>],
514 k: usize,
515) -> Vec<NearestShape> {
516 let mut all: Vec<NearestShape> = shapes
517 .iter()
518 .enumerate()
519 .map(|(idx, shape)| {
520 let r = match shape {
521 QueryShapeRef::Sphere(s) => s.point_query(query_point),
522 QueryShapeRef::Box(b) => b.point_query(query_point),
523 QueryShapeRef::Capsule(c) => c.point_query(query_point),
524 };
525 NearestShape {
526 index: idx,
527 distance: r.distance,
528 closest_point: r.point,
529 }
530 })
531 .collect();
532 all.sort_by(|a, b| {
533 a.distance
534 .partial_cmp(&b.distance)
535 .unwrap_or(std::cmp::Ordering::Equal)
536 });
537 all.truncate(k);
538 all
539}
540pub fn sphere_cast_scene(
546 start: [f64; 3],
547 velocity: [f64; 3],
548 sphere_radius: f64,
549 max_t: f64,
550 shapes: &[QueryShapeRef<'_>],
551) -> Option<SphereCastResult> {
552 let mut best_toi = max_t + 1.0;
553 let mut best_result: Option<SphereCastResult> = None;
554 for (idx, shape) in shapes.iter().enumerate() {
555 match shape {
556 QueryShapeRef::Sphere(s) => {
557 let expanded = QuerySphere {
558 center: s.center,
559 radius: s.radius + sphere_radius,
560 };
561 if let Some(r) = expanded.ray_cast(start, velocity, max_t)
562 && r.toi < best_toi
563 {
564 best_toi = r.toi;
565 best_result = Some(SphereCastResult {
566 shape_index: idx,
567 toi: r.toi,
568 normal: r.normal,
569 });
570 }
571 }
572 QueryShapeRef::Box(b) => {
573 let expanded = QueryBox {
574 center: b.center,
575 half_extents: [
576 b.half_extents[0] + sphere_radius,
577 b.half_extents[1] + sphere_radius,
578 b.half_extents[2] + sphere_radius,
579 ],
580 };
581 if let Some(r) = expanded.ray_cast(start, velocity, max_t)
582 && r.toi < best_toi
583 {
584 best_toi = r.toi;
585 best_result = Some(SphereCastResult {
586 shape_index: idx,
587 toi: r.toi,
588 normal: r.normal,
589 });
590 }
591 }
592 QueryShapeRef::Capsule(c) => {
593 let expanded = QueryCapsule {
594 p0: c.p0,
595 p1: c.p1,
596 radius: c.radius + sphere_radius,
597 };
598 if let Some(r) = expanded.ray_cast(start, velocity, max_t)
599 && r.toi < best_toi
600 {
601 best_toi = r.toi;
602 best_result = Some(SphereCastResult {
603 shape_index: idx,
604 toi: r.toi,
605 normal: r.normal,
606 });
607 }
608 }
609 }
610 }
611 best_result
612}
613pub fn compute_time_of_impact_spheres(
618 center_a: [f64; 3],
619 vel_a: [f64; 3],
620 radius_a: f64,
621 center_b: [f64; 3],
622 vel_b: [f64; 3],
623 radius_b: f64,
624 max_t: f64,
625) -> Option<ToiResult> {
626 let rel_vel = sub(vel_a, vel_b);
627 let d = sub(center_a, center_b);
628 let combined_r = radius_a + radius_b;
629 let a = dot(rel_vel, rel_vel);
630 let b = 2.0 * dot(d, rel_vel);
631 let c = dot(d, d) - combined_r * combined_r;
632 if a < 1e-30 {
633 return if c <= 0.0 {
634 let n = if norm(d) > 1e-15 {
635 normalize(d)
636 } else {
637 [1.0, 0.0, 0.0]
638 };
639 let contact = add(center_b, scale(n, radius_b));
640 Some(ToiResult {
641 toi: 0.0,
642 normal: n,
643 point: contact,
644 })
645 } else {
646 None
647 };
648 }
649 let disc = b * b - 4.0 * a * c;
650 if disc < 0.0 {
651 return None;
652 }
653 let sqrt_disc = disc.sqrt();
654 let t1 = (-b - sqrt_disc) / (2.0 * a);
655 let t2 = (-b + sqrt_disc) / (2.0 * a);
656 let toi = if t1 >= 0.0 && t1 <= max_t {
657 t1
658 } else if t2 >= 0.0 && t2 <= max_t {
659 t2
660 } else {
661 return None;
662 };
663 let pos_a = add(center_a, scale(vel_a, toi));
664 let pos_b = add(center_b, scale(vel_b, toi));
665 let diff = sub(pos_a, pos_b);
666 let n = if norm(diff) > 1e-15 {
667 normalize(diff)
668 } else {
669 [1.0, 0.0, 0.0]
670 };
671 let point = add(pos_b, scale(n, radius_b));
672 Some(ToiResult {
673 toi,
674 normal: n,
675 point,
676 })
677}
678pub fn compute_time_of_impact_aabbs(
683 min_a: [f64; 3],
684 max_a: [f64; 3],
685 vel_a: [f64; 3],
686 min_b: [f64; 3],
687 max_b: [f64; 3],
688 vel_b: [f64; 3],
689 max_t: f64,
690) -> Option<f64> {
691 let rel_vel = sub(vel_a, vel_b);
692 let mut t_enter = 0.0_f64;
693 let mut t_exit = max_t;
694 for i in 0..3 {
695 let v = rel_vel[i];
696 let d0 = min_b[i] - max_a[i];
697 let d1 = max_b[i] - min_a[i];
698 if v.abs() < 1e-15 {
699 if min_a[i] > max_b[i] || min_b[i] > max_a[i] {
700 return None;
701 }
702 } else {
703 let (ta, tb) = if v > 0.0 {
704 (d0 / v, d1 / v)
705 } else {
706 (d1 / v, d0 / v)
707 };
708 t_enter = t_enter.max(ta);
709 t_exit = t_exit.min(tb);
710 if t_enter > t_exit {
711 return None;
712 }
713 }
714 }
715 if t_enter < 0.0 || t_enter > max_t {
716 return None;
717 }
718 Some(t_enter)
719}
720pub fn project_point_convex(point: [f64; 3], vertices: &[[f64; 3]]) -> [f64; 3] {
730 if vertices.is_empty() {
731 return point;
732 }
733 if vertices.len() == 1 {
734 return vertices[0];
735 }
736 if vertices.len() == 2 {
737 let (cp, _) = closest_point_on_segment(point, vertices[0], vertices[1]);
738 return cp;
739 }
740 let n = vertices.len() as f64;
741 let mut centroid = [0.0_f64; 3];
742 for v in vertices {
743 centroid[0] += v[0];
744 centroid[1] += v[1];
745 centroid[2] += v[2];
746 }
747 centroid[0] /= n;
748 centroid[1] /= n;
749 centroid[2] /= n;
750 let mut best_dist_sq = f64::INFINITY;
751 let mut best_pt = vertices[0];
752 let nv = vertices.len();
753 for i in 0..nv {
754 let a = vertices[i];
755 let b = vertices[(i + 1) % nv];
756 let cp = closest_point_on_triangle_local(point, centroid, a, b);
757 let d = sub(point, cp);
758 let dsq = dot(d, d);
759 if dsq < best_dist_sq {
760 best_dist_sq = dsq;
761 best_pt = cp;
762 }
763 }
764 best_pt
765}
766pub(super) fn closest_point_on_triangle_local(
768 point: [f64; 3],
769 v0: [f64; 3],
770 v1: [f64; 3],
771 v2: [f64; 3],
772) -> [f64; 3] {
773 let ab = sub(v1, v0);
774 let ac = sub(v2, v0);
775 let ap = sub(point, v0);
776 let d1 = dot(ab, ap);
777 let d2 = dot(ac, ap);
778 if d1 <= 0.0 && d2 <= 0.0 {
779 return v0;
780 }
781 let bp = sub(point, v1);
782 let d3 = dot(ab, bp);
783 let d4 = dot(ac, bp);
784 if d3 >= 0.0 && d4 <= d3 {
785 return v1;
786 }
787 let cp2 = sub(point, v2);
788 let d5 = dot(ab, cp2);
789 let d6 = dot(ac, cp2);
790 if d6 >= 0.0 && d5 <= d6 {
791 return v2;
792 }
793 let vc = d1 * d4 - d3 * d2;
794 if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
795 let v = d1 / (d1 - d3);
796 return add(v0, scale(ab, v));
797 }
798 let vb = d5 * d2 - d1 * d6;
799 if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
800 let w = d2 / (d2 - d6);
801 return add(v0, scale(ac, w));
802 }
803 let va = d3 * d6 - d5 * d4;
804 if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
805 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
806 return add(v1, scale(sub(v2, v1), w));
807 }
808 let denom = 1.0 / (va + vb + vc);
809 let v = vb * denom;
810 let w = vc * denom;
811 add(add(v0, scale(ab, v)), scale(ac, w))
812}
813pub fn distance_between_segments(
817 p0_a: [f64; 3],
818 p1_a: [f64; 3],
819 p0_b: [f64; 3],
820 p1_b: [f64; 3],
821) -> SegmentDistResult {
822 let da = sub(p1_a, p0_a);
823 let db = sub(p1_b, p0_b);
824 let r = sub(p0_a, p0_b);
825 let a = dot(da, da);
826 let e = dot(db, db);
827 let f = dot(db, r);
828 let (s, t) = if a <= 1e-15 {
829 let t = clamp(f / e.max(1e-15), 0.0, 1.0);
830 (0.0, t)
831 } else {
832 let c = dot(da, r);
833 if e <= 1e-15 {
834 let s = clamp(-c / a, 0.0, 1.0);
835 (s, 0.0)
836 } else {
837 let b = dot(da, db);
838 let denom = a * e - b * b;
839 let s = if denom.abs() > 1e-15 {
840 clamp((b * f - c * e) / denom, 0.0, 1.0)
841 } else {
842 0.0
843 };
844 let t_raw = (b * s + f) / e;
845 let t = clamp(t_raw, 0.0, 1.0);
846 let s = clamp((-c + b * t) / a, 0.0, 1.0);
847 (s, t)
848 }
849 };
850 let point_a = add(p0_a, scale(da, s));
851 let point_b = add(p0_b, scale(db, t));
852 let diff = sub(point_a, point_b);
853 let distance = norm(diff);
854 SegmentDistResult {
855 point_a,
856 point_b,
857 distance,
858 }
859}
860pub fn closest_point_on_sphere(point: [f64; 3], center: [f64; 3], radius: f64) -> [f64; 3] {
862 let s = QuerySphere { center, radius };
863 s.point_query(point).point
864}
865pub fn closest_point_on_box(
867 point: [f64; 3],
868 box_center: [f64; 3],
869 half_extents: [f64; 3],
870) -> [f64; 3] {
871 let b = QueryBox {
872 center: box_center,
873 half_extents,
874 };
875 b.point_query(point).point
876}
877pub fn closest_point_on_capsule(
879 point: [f64; 3],
880 p0: [f64; 3],
881 p1: [f64; 3],
882 radius: f64,
883) -> [f64; 3] {
884 let c = QueryCapsule { p0, p1, radius };
885 c.point_query(point).point
886}
887#[cfg(test)]
888mod tests {
889 use super::*;
890 use crate::query::CompoundQuery;
891 use crate::query::QueryBox;
892
893 use crate::query::QuerySphere;
894
895 #[test]
896 fn sphere_ray_cast_hits() {
897 let sphere = QuerySphere {
898 center: [0.0, 0.0, 0.0],
899 radius: 1.0,
900 };
901 let result = sphere.ray_cast([3.0, 0.0, 0.0], [-1.0, 0.0, 0.0], 100.0);
902 assert!(result.is_some(), "expected a hit");
903 let r = result.unwrap();
904 assert!((r.toi - 2.0).abs() < 1e-10, "toi={}", r.toi);
905 }
906 #[test]
907 fn sphere_ray_cast_miss() {
908 let sphere = QuerySphere {
909 center: [0.0, 0.0, 0.0],
910 radius: 1.0,
911 };
912 let result = sphere.ray_cast([3.0, 2.0, 0.0], [-1.0, 0.0, 0.0], 100.0);
913 assert!(result.is_none(), "expected a miss");
914 }
915 #[test]
916 fn sphere_point_query_outside() {
917 let sphere = QuerySphere {
918 center: [0.0, 0.0, 0.0],
919 radius: 1.0,
920 };
921 let r = sphere.point_query([2.0, 0.0, 0.0]);
922 assert!(!r.inside);
923 assert!((r.distance - 1.0).abs() < 1e-10, "distance={}", r.distance);
924 assert!((r.point[0] - 1.0).abs() < 1e-10);
925 }
926 #[test]
927 fn box_ray_cast_hits_face() {
928 let b = QueryBox {
929 center: [0.0, 0.0, 0.0],
930 half_extents: [1.0, 1.0, 1.0],
931 };
932 let result = b.ray_cast([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
933 assert!(result.is_some(), "expected a hit");
934 let r = result.unwrap();
935 assert!((r.toi - 4.0).abs() < 1e-10, "toi={}", r.toi);
936 assert!(
937 (r.normal[0] - (-1.0)).abs() < 1e-10,
938 "normal={:?}",
939 r.normal
940 );
941 }
942 #[test]
943 fn box_point_query_inside() {
944 let b = QueryBox {
945 center: [0.0, 0.0, 0.0],
946 half_extents: [2.0, 2.0, 2.0],
947 };
948 let r = b.point_query([0.0, 0.0, 0.0]);
949 assert!(r.inside, "expected inside");
950 assert!(r.distance < 0.0, "signed distance should be negative");
951 }
952 #[test]
953 fn box_point_query_outside() {
954 let b = QueryBox {
955 center: [0.0, 0.0, 0.0],
956 half_extents: [1.0, 1.0, 1.0],
957 };
958 let r = b.point_query([3.0, 0.0, 0.0]);
959 assert!(!r.inside);
960 assert!((r.distance - 2.0).abs() < 1e-10, "distance={}", r.distance);
961 }
962 #[test]
963 fn capsule_point_query_between_endpoints() {
964 let cap = QueryCapsule {
965 p0: [0.0, 0.0, -2.0],
966 p1: [0.0, 0.0, 2.0],
967 radius: 1.0,
968 };
969 let r = cap.point_query([2.0, 0.0, 0.0]);
970 assert!(!r.inside);
971 assert!((r.distance - 1.0).abs() < 1e-10, "distance={}", r.distance);
972 assert!((r.point[0] - 1.0).abs() < 1e-10, "closest_x={}", r.point[0]);
973 assert!(r.point[2].abs() < 1e-10, "closest_z={}", r.point[2]);
974 }
975 #[test]
976 fn capsule_ray_cast_hits() {
977 let cap = QueryCapsule {
978 p0: [0.0, -1.0, 0.0],
979 p1: [0.0, 1.0, 0.0],
980 radius: 1.0,
981 };
982 let result = cap.ray_cast([5.0, 0.0, 0.0], [-1.0, 0.0, 0.0], 100.0);
983 assert!(result.is_some(), "expected a hit");
984 let r = result.unwrap();
985 assert!((r.toi - 4.0).abs() < 1e-10, "toi={}", r.toi);
986 }
987 #[test]
988 fn transform_point_identity() {
989 let identity = [
990 [1.0, 0.0, 0.0, 0.0],
991 [0.0, 1.0, 0.0, 0.0],
992 [0.0, 0.0, 1.0, 0.0],
993 [0.0, 0.0, 0.0, 1.0],
994 ];
995 let p = [1.0, 2.0, 3.0];
996 let q = transform_point(p, identity);
997 assert!((q[0] - 1.0).abs() < 1e-10);
998 assert!((q[1] - 2.0).abs() < 1e-10);
999 assert!((q[2] - 3.0).abs() < 1e-10);
1000 }
1001 #[test]
1002 fn transform_point_translation() {
1003 let t = [
1004 [1.0, 0.0, 0.0, 3.0],
1005 [0.0, 1.0, 0.0, 5.0],
1006 [0.0, 0.0, 1.0, 7.0],
1007 [0.0, 0.0, 0.0, 1.0],
1008 ];
1009 let p = [1.0, 0.0, 0.0];
1010 let q = transform_point(p, t);
1011 assert!((q[0] - 4.0).abs() < 1e-10);
1012 assert!((q[1] - 5.0).abs() < 1e-10);
1013 assert!((q[2] - 7.0).abs() < 1e-10);
1014 }
1015 #[test]
1016 fn closest_on_segment_midpoint() {
1017 let (cp, t) = closest_point_on_segment([1.0, 1.0, 0.0], [0.0, 0.0, 0.0], [2.0, 0.0, 0.0]);
1018 assert!((t - 0.5).abs() < 1e-10, "t={t}");
1019 assert!((cp[0] - 1.0).abs() < 1e-10);
1020 assert!(cp[1].abs() < 1e-10);
1021 }
1022 #[test]
1023 fn closest_on_segment_clamps_start() {
1024 let (cp, t) = closest_point_on_segment([-1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1025 assert!((t).abs() < 1e-10, "t should be 0, got {t}");
1026 assert_eq!(cp, [0.0, 0.0, 0.0]);
1027 }
1028 #[test]
1029 fn closest_on_segment_clamps_end() {
1030 let (cp, t) = closest_point_on_segment([5.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1031 assert!((t - 1.0).abs() < 1e-10, "t should be 1, got {t}");
1032 assert!((cp[0] - 1.0).abs() < 1e-10);
1033 }
1034 #[test]
1035 fn closest_on_triangle_interior() {
1036 let a = [0.0, 0.0, 0.0];
1037 let b = [3.0, 0.0, 0.0];
1038 let c = [1.5, 2.0, 0.0];
1039 let p = [1.5, 0.6, 2.0];
1040 let cp = closest_point_on_triangle(p, a, b, c);
1041 assert!(cp[2].abs() < 1e-8, "z={}", cp[2]);
1042 }
1043 #[test]
1044 fn closest_on_triangle_vertex() {
1045 let a = [0.0, 0.0, 0.0];
1046 let b = [10.0, 0.0, 0.0];
1047 let c = [0.0, 10.0, 0.0];
1048 let p = [-2.0, -2.0, 0.0];
1049 let cp = closest_point_on_triangle(p, a, b, c);
1050 assert!((cp[0]).abs() < 1e-10);
1051 assert!((cp[1]).abs() < 1e-10);
1052 }
1053 #[test]
1054 fn point_in_unit_box_hull() {
1055 let planes: Vec<[f64; 4]> = vec![
1056 [1.0, 0.0, 0.0, 0.0],
1057 [-1.0, 0.0, 0.0, 1.0],
1058 [0.0, 1.0, 0.0, 0.0],
1059 [0.0, -1.0, 0.0, 1.0],
1060 [0.0, 0.0, 1.0, 0.0],
1061 [0.0, 0.0, -1.0, 1.0],
1062 ];
1063 assert!(point_in_convex_hull([0.5, 0.5, 0.5], &planes));
1064 assert!(!point_in_convex_hull([2.0, 0.5, 0.5], &planes));
1065 assert!(!point_in_convex_hull([-0.1, 0.5, 0.5], &planes));
1066 }
1067 #[test]
1068 fn signed_dist_sphere_outside() {
1069 let d = signed_dist_to_sphere([3.0, 0.0, 0.0], [0.0, 0.0, 0.0], 1.0);
1070 assert!((d - 2.0).abs() < 1e-10, "d={d}");
1071 }
1072 #[test]
1073 fn signed_dist_sphere_inside() {
1074 let d = signed_dist_to_sphere([0.5, 0.0, 0.0], [0.0, 0.0, 0.0], 1.0);
1075 assert!(d < 0.0, "should be negative inside, d={d}");
1076 assert!((d - (-0.5)).abs() < 1e-10, "d={d}");
1077 }
1078 #[test]
1079 fn signed_dist_box_outside() {
1080 let d = signed_dist_to_box([3.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1081 assert!((d - 2.0).abs() < 1e-10, "d={d}");
1082 }
1083 #[test]
1084 fn signed_dist_box_inside() {
1085 let d = signed_dist_to_box([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1086 assert!(d < 0.0, "center should be inside, d={d}");
1087 }
1088 #[test]
1089 fn signed_dist_capsule_outside() {
1090 let d = signed_dist_to_capsule([0.0, 3.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1091 assert!((d - 1.5).abs() < 1e-10, "d={d}");
1092 }
1093 #[test]
1094 fn signed_dist_capsule_inside() {
1095 let d = signed_dist_to_capsule([0.1, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1096 assert!(d < 0.0, "should be inside capsule, d={d}");
1097 }
1098 #[test]
1099 fn ray_vs_sphere_hit() {
1100 let t = ray_vs_sphere(
1101 [-5.0, 0.0, 0.0],
1102 [1.0, 0.0, 0.0],
1103 [0.0, 0.0, 0.0],
1104 1.0,
1105 100.0,
1106 );
1107 assert!(t.is_some());
1108 assert!((t.unwrap() - 4.0).abs() < 1e-10);
1109 }
1110 #[test]
1111 fn ray_vs_sphere_miss() {
1112 let t = ray_vs_sphere(
1113 [-5.0, 5.0, 0.0],
1114 [1.0, 0.0, 0.0],
1115 [0.0, 0.0, 0.0],
1116 1.0,
1117 100.0,
1118 );
1119 assert!(t.is_none());
1120 }
1121 #[test]
1122 fn ray_vs_box_hit() {
1123 let t = ray_vs_box(
1124 [-5.0, 0.0, 0.0],
1125 [1.0, 0.0, 0.0],
1126 [0.0, 0.0, 0.0],
1127 [1.0, 1.0, 1.0],
1128 100.0,
1129 );
1130 assert!(t.is_some());
1131 assert!((t.unwrap() - 4.0).abs() < 1e-10);
1132 }
1133 #[test]
1134 fn ray_vs_triangle_hit() {
1135 let t = ray_vs_triangle(
1136 [0.5, 0.5, 5.0],
1137 [0.0, 0.0, -1.0],
1138 [0.0, 0.0, 0.0],
1139 [2.0, 0.0, 0.0],
1140 [0.0, 2.0, 0.0],
1141 100.0,
1142 );
1143 assert!(t.is_some(), "ray straight down should hit triangle");
1144 assert!((t.unwrap() - 5.0).abs() < 1e-10);
1145 }
1146 #[test]
1147 fn ray_vs_triangle_miss_parallel() {
1148 let t = ray_vs_triangle(
1149 [0.5, 0.5, 1.0],
1150 [1.0, 0.0, 0.0],
1151 [0.0, 0.0, 0.0],
1152 [2.0, 0.0, 0.0],
1153 [0.0, 2.0, 0.0],
1154 100.0,
1155 );
1156 assert!(t.is_none());
1157 }
1158 #[test]
1159 fn sphere_sweep_vs_sphere_hit() {
1160 let t = sphere_sweep_vs_sphere(
1161 [0.0, 0.0, 0.0],
1162 [1.0, 0.0, 0.0],
1163 0.5,
1164 [5.0, 0.0, 0.0],
1165 0.5,
1166 10.0,
1167 );
1168 assert!(t.is_some(), "sweep should hit");
1169 assert!((t.unwrap() - 4.0).abs() < 1e-10, "t={}", t.unwrap());
1170 }
1171 #[test]
1172 fn sphere_sweep_vs_sphere_miss() {
1173 let t = sphere_sweep_vs_sphere(
1174 [0.0, 0.0, 0.0],
1175 [1.0, 0.0, 0.0],
1176 0.5,
1177 [0.0, 5.0, 0.0],
1178 0.5,
1179 10.0,
1180 );
1181 assert!(t.is_none());
1182 }
1183 #[test]
1184 fn sphere_sweep_vs_box_hit() {
1185 let t = sphere_sweep_vs_box(
1186 [-5.0, 0.0, 0.0],
1187 [1.0, 0.0, 0.0],
1188 0.5,
1189 [0.0, 0.0, 0.0],
1190 [1.0, 1.0, 1.0],
1191 100.0,
1192 );
1193 assert!(t.is_some(), "sweep should hit expanded box");
1194 }
1195 #[test]
1196 fn compound_ray_cast_finds_closest() {
1197 let mut compound = CompoundQuery::new();
1198 let identity = [
1199 [1.0, 0.0, 0.0, 0.0],
1200 [0.0, 1.0, 0.0, 0.0],
1201 [0.0, 0.0, 1.0, 0.0],
1202 [0.0, 0.0, 0.0, 1.0],
1203 ];
1204 compound.add_shape(
1205 Box::new(QuerySphere {
1206 center: [0.0, 0.0, 0.0],
1207 radius: 1.0,
1208 }),
1209 identity,
1210 );
1211 compound.add_shape(
1212 Box::new(QuerySphere {
1213 center: [10.0, 0.0, 0.0],
1214 radius: 1.0,
1215 }),
1216 identity,
1217 );
1218 let result = compound.ray_cast_any([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
1219 assert!(result.is_some());
1220 let r = result.unwrap();
1221 assert!((r.toi - 4.0).abs() < 1e-10, "toi={}", r.toi);
1222 }
1223}
1224#[cfg(test)]
1225mod tests_extended {
1226 use super::*;
1227
1228 use crate::query::QueryBox;
1229 use crate::query::QueryShapeRef;
1230 use crate::query::QuerySphere;
1231 use crate::query::ViewFrustum;
1232 use crate::query::aabb_overlap;
1233 use crate::query::capsule_capsule_overlap;
1234 use crate::query::closest_point_on_capsule;
1235 use crate::query::compute_time_of_impact_aabbs;
1236 use crate::query::compute_time_of_impact_spheres;
1237 use crate::query::distance_between_segments;
1238 use crate::query::frustum_query;
1239 use crate::query::k_nearest_shapes;
1240 use crate::query::norm;
1241 use crate::query::point_containment_all_pairs;
1242 use crate::query::project_point_convex;
1243 use crate::query::ray_cast_all_pairs;
1244 use crate::query::sphere_aabb_overlap;
1245 use crate::query::sphere_cast_scene;
1246 use crate::query::sphere_sphere_overlap;
1247 use crate::query::sub;
1248 #[test]
1249 fn aabb_overlap_true_when_touching() {
1250 assert!(aabb_overlap(
1251 [0.0, 0.0, 0.0],
1252 [1.0, 1.0, 1.0],
1253 [1.0, 0.0, 0.0],
1254 [2.0, 1.0, 1.0],
1255 ));
1256 }
1257 #[test]
1258 fn aabb_overlap_false_when_separated() {
1259 assert!(!aabb_overlap(
1260 [0.0, 0.0, 0.0],
1261 [1.0, 1.0, 1.0],
1262 [2.0, 0.0, 0.0],
1263 [3.0, 1.0, 1.0],
1264 ));
1265 }
1266 #[test]
1267 fn sphere_sphere_overlap_touching() {
1268 assert!(sphere_sphere_overlap(
1269 [0.0, 0.0, 0.0],
1270 1.0,
1271 [2.0, 0.0, 0.0],
1272 1.0
1273 ));
1274 }
1275 #[test]
1276 fn sphere_sphere_overlap_separated() {
1277 assert!(!sphere_sphere_overlap(
1278 [0.0, 0.0, 0.0],
1279 0.4,
1280 [2.0, 0.0, 0.0],
1281 0.4
1282 ));
1283 }
1284 #[test]
1285 fn sphere_aabb_overlap_inside() {
1286 assert!(sphere_aabb_overlap(
1287 [0.0, 0.0, 0.0],
1288 0.5,
1289 [0.0, 0.0, 0.0],
1290 [2.0, 2.0, 2.0]
1291 ));
1292 }
1293 #[test]
1294 fn sphere_aabb_overlap_separated() {
1295 assert!(!sphere_aabb_overlap(
1296 [5.0, 0.0, 0.0],
1297 0.5,
1298 [0.0, 0.0, 0.0],
1299 [1.0, 1.0, 1.0]
1300 ));
1301 }
1302 #[test]
1303 fn sphere_aabb_overlap_touching_face() {
1304 assert!(sphere_aabb_overlap(
1305 [1.5, 0.0, 0.0],
1306 0.5,
1307 [0.0, 0.0, 0.0],
1308 [1.0, 1.0, 1.0]
1309 ));
1310 }
1311 #[test]
1312 fn capsule_capsule_overlap_crossing() {
1313 assert!(capsule_capsule_overlap(
1314 [-2.0, 0.0, 0.0],
1315 [2.0, 0.0, 0.0],
1316 0.1,
1317 [0.0, -2.0, 0.0],
1318 [0.0, 2.0, 0.0],
1319 0.1,
1320 ));
1321 }
1322 #[test]
1323 fn capsule_capsule_overlap_separated() {
1324 assert!(!capsule_capsule_overlap(
1325 [-1.0, 0.0, 0.0],
1326 [1.0, 0.0, 0.0],
1327 0.1,
1328 [-1.0, 5.0, 0.0],
1329 [1.0, 5.0, 0.0],
1330 0.1,
1331 ));
1332 }
1333 #[test]
1334 fn all_pairs_ray_cast_hits_all() {
1335 let s1 = QuerySphere {
1336 center: [0.0, 0.0, 0.0],
1337 radius: 1.0,
1338 };
1339 let s2 = QuerySphere {
1340 center: [5.0, 0.0, 0.0],
1341 radius: 1.0,
1342 };
1343 let shapes = vec![QueryShapeRef::Sphere(&s1), QueryShapeRef::Sphere(&s2)];
1344 let hits = ray_cast_all_pairs([-10.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0, &shapes);
1345 assert_eq!(hits.len(), 2, "both spheres should be hit");
1346 assert!(hits[0].toi < hits[1].toi);
1347 }
1348 #[test]
1349 fn all_pairs_ray_cast_misses_all() {
1350 let s1 = QuerySphere {
1351 center: [0.0, 0.0, 0.0],
1352 radius: 0.5,
1353 };
1354 let shapes = vec![QueryShapeRef::Sphere(&s1)];
1355 let hits = ray_cast_all_pairs([0.0, 5.0, 0.0], [1.0, 0.0, 0.0], 100.0, &shapes);
1356 assert!(hits.is_empty(), "ray above sphere should miss");
1357 }
1358 #[test]
1359 fn all_pairs_point_containment() {
1360 let s = QuerySphere {
1361 center: [0.0, 0.0, 0.0],
1362 radius: 2.0,
1363 };
1364 let b = QueryBox {
1365 center: [10.0, 0.0, 0.0],
1366 half_extents: [1.0, 1.0, 1.0],
1367 };
1368 let shapes = vec![QueryShapeRef::Sphere(&s), QueryShapeRef::Box(&b)];
1369 let inside = point_containment_all_pairs([0.5, 0.0, 0.0], &shapes);
1370 assert!(inside.contains(&0), "point inside sphere");
1371 assert!(!inside.contains(&1), "point not inside box");
1372 }
1373 #[test]
1374 fn all_pairs_point_containment_both() {
1375 let s = QuerySphere {
1376 center: [0.0, 0.0, 0.0],
1377 radius: 1.0,
1378 };
1379 let b = QueryBox {
1380 center: [0.0, 0.0, 0.0],
1381 half_extents: [5.0, 5.0, 5.0],
1382 };
1383 let shapes = vec![QueryShapeRef::Sphere(&s), QueryShapeRef::Box(&b)];
1384 let inside = point_containment_all_pairs([0.5, 0.0, 0.0], &shapes);
1385 assert_eq!(inside.len(), 2, "point inside both shapes");
1386 }
1387 fn make_unit_frustum() -> ViewFrustum {
1388 ViewFrustum::new([
1389 [1.0, 0.0, 0.0, 1.0],
1390 [-1.0, 0.0, 0.0, 1.0],
1391 [0.0, 1.0, 0.0, 1.0],
1392 [0.0, -1.0, 0.0, 1.0],
1393 [0.0, 0.0, 1.0, 1.0],
1394 [0.0, 0.0, -1.0, 1.0],
1395 ])
1396 }
1397 #[test]
1398 fn frustum_test_sphere_inside() {
1399 let f = make_unit_frustum();
1400 assert!(
1401 f.test_sphere([0.0, 0.0, 0.0], 0.1),
1402 "sphere at origin should be visible"
1403 );
1404 }
1405 #[test]
1406 fn frustum_test_sphere_outside() {
1407 let f = make_unit_frustum();
1408 assert!(
1409 !f.test_sphere([5.0, 0.0, 0.0], 0.1),
1410 "sphere far away should be culled"
1411 );
1412 }
1413 #[test]
1414 fn frustum_test_sphere_straddling() {
1415 let f = make_unit_frustum();
1416 assert!(
1417 f.test_sphere([1.5, 0.0, 0.0], 1.0),
1418 "straddling sphere should not be culled"
1419 );
1420 }
1421 #[test]
1422 fn frustum_test_aabb_inside() {
1423 let f = make_unit_frustum();
1424 assert!(f.test_aabb([-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]));
1425 }
1426 #[test]
1427 fn frustum_test_aabb_outside() {
1428 let f = make_unit_frustum();
1429 assert!(!f.test_aabb([3.0, 3.0, 3.0], [4.0, 4.0, 4.0]));
1430 }
1431 #[test]
1432 fn frustum_query_filters_correctly() {
1433 let f = make_unit_frustum();
1434 let s_in = QuerySphere {
1435 center: [0.0, 0.0, 0.0],
1436 radius: 0.1,
1437 };
1438 let s_out = QuerySphere {
1439 center: [10.0, 0.0, 0.0],
1440 radius: 0.1,
1441 };
1442 let b_in = QueryBox {
1443 center: [0.0, 0.0, 0.0],
1444 half_extents: [0.5, 0.5, 0.5],
1445 };
1446 let shapes: Vec<QueryShapeRef<'_>> = vec![
1447 QueryShapeRef::Sphere(&s_in),
1448 QueryShapeRef::Sphere(&s_out),
1449 QueryShapeRef::Box(&b_in),
1450 ];
1451 let visible = frustum_query(&f, &shapes);
1452 assert!(visible.contains(&0), "in-frustum sphere should be visible");
1453 assert!(
1454 !visible.contains(&1),
1455 "out-of-frustum sphere should be culled"
1456 );
1457 assert!(visible.contains(&2), "in-frustum box should be visible");
1458 }
1459 #[test]
1460 fn k_nearest_returns_closest_first() {
1461 let s_near = QuerySphere {
1462 center: [1.0, 0.0, 0.0],
1463 radius: 0.1,
1464 };
1465 let s_far = QuerySphere {
1466 center: [10.0, 0.0, 0.0],
1467 radius: 0.1,
1468 };
1469 let shapes: Vec<QueryShapeRef<'_>> = vec![
1470 QueryShapeRef::Sphere(&s_far),
1471 QueryShapeRef::Sphere(&s_near),
1472 ];
1473 let nearest = k_nearest_shapes([0.0, 0.0, 0.0], &shapes, 2);
1474 assert_eq!(nearest.len(), 2);
1475 assert!(
1476 nearest[0].distance < nearest[1].distance,
1477 "nearest should be first"
1478 );
1479 assert_eq!(nearest[0].index, 1, "index 1 (near sphere) should be first");
1480 }
1481 #[test]
1482 fn k_nearest_k_larger_than_shapes() {
1483 let s = QuerySphere {
1484 center: [0.0, 0.0, 0.0],
1485 radius: 1.0,
1486 };
1487 let shapes: Vec<QueryShapeRef<'_>> = vec![QueryShapeRef::Sphere(&s)];
1488 let nearest = k_nearest_shapes([5.0, 0.0, 0.0], &shapes, 100);
1489 assert_eq!(nearest.len(), 1, "should only return what exists");
1490 }
1491 #[test]
1492 fn k_nearest_inside_shape_is_negative_distance() {
1493 let s = QuerySphere {
1494 center: [0.0, 0.0, 0.0],
1495 radius: 5.0,
1496 };
1497 let shapes: Vec<QueryShapeRef<'_>> = vec![QueryShapeRef::Sphere(&s)];
1498 let nearest = k_nearest_shapes([0.0, 0.0, 0.0], &shapes, 1);
1499 assert!(
1500 nearest[0].distance < 0.0,
1501 "inside sphere → negative distance"
1502 );
1503 }
1504 #[test]
1505 fn k_nearest_zero_k_returns_empty() {
1506 let s = QuerySphere {
1507 center: [0.0, 0.0, 0.0],
1508 radius: 1.0,
1509 };
1510 let shapes: Vec<QueryShapeRef<'_>> = vec![QueryShapeRef::Sphere(&s)];
1511 let nearest = k_nearest_shapes([5.0, 0.0, 0.0], &shapes, 0);
1512 assert!(nearest.is_empty());
1513 }
1514 #[test]
1515 fn sphere_cast_hits_sphere() {
1516 let target = QuerySphere {
1517 center: [5.0, 0.0, 0.0],
1518 radius: 1.0,
1519 };
1520 let shapes: Vec<QueryShapeRef<'_>> = vec![QueryShapeRef::Sphere(&target)];
1521 let result = sphere_cast_scene([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.5, 10.0, &shapes);
1522 assert!(result.is_some(), "sphere cast should hit expanded sphere");
1523 let r = result.unwrap();
1524 assert_eq!(r.shape_index, 0);
1525 assert!(r.toi > 0.0 && r.toi <= 10.0);
1526 }
1527 #[test]
1528 fn sphere_cast_hits_box() {
1529 let target = QueryBox {
1530 center: [5.0, 0.0, 0.0],
1531 half_extents: [1.0, 1.0, 1.0],
1532 };
1533 let shapes: Vec<QueryShapeRef<'_>> = vec![QueryShapeRef::Box(&target)];
1534 let result = sphere_cast_scene([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.5, 10.0, &shapes);
1535 assert!(result.is_some(), "sphere cast should hit box");
1536 }
1537 #[test]
1538 fn sphere_cast_misses() {
1539 let target = QuerySphere {
1540 center: [0.0, 5.0, 0.0],
1541 radius: 0.1,
1542 };
1543 let shapes: Vec<QueryShapeRef<'_>> = vec![QueryShapeRef::Sphere(&target)];
1544 let result = sphere_cast_scene([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.5, 100.0, &shapes);
1545 assert!(result.is_none(), "sphere cast perpendicular should miss");
1546 }
1547 #[test]
1548 fn sphere_cast_finds_nearest_of_two() {
1549 let near = QuerySphere {
1550 center: [3.0, 0.0, 0.0],
1551 radius: 0.5,
1552 };
1553 let far = QuerySphere {
1554 center: [8.0, 0.0, 0.0],
1555 radius: 0.5,
1556 };
1557 let shapes: Vec<QueryShapeRef<'_>> =
1558 vec![QueryShapeRef::Sphere(&far), QueryShapeRef::Sphere(&near)];
1559 let result = sphere_cast_scene([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.1, 100.0, &shapes);
1560 assert!(result.is_some());
1561 let r = result.unwrap();
1562 assert_eq!(
1563 r.shape_index, 1,
1564 "should hit the nearer sphere first (index 1)"
1565 );
1566 }
1567 #[test]
1568 fn closest_point_sphere_outside() {
1569 let p = closest_point_on_sphere([3.0, 0.0, 0.0], [0.0, 0.0, 0.0], 1.0);
1570 assert!((p[0] - 1.0).abs() < 1e-10, "closest point should be at x=1");
1571 }
1572 #[test]
1573 fn closest_point_box_outside() {
1574 let p = closest_point_on_box([3.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1575 assert!((p[0] - 1.0).abs() < 1e-10);
1576 }
1577 #[test]
1578 fn closest_point_capsule_side() {
1579 let p = closest_point_on_capsule([2.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1580 assert!(
1581 (p[0] - 0.5).abs() < 1e-10,
1582 "closest x should be at capsule surface"
1583 );
1584 assert!(
1585 p[1].abs() < 1e-10,
1586 "closest point should be at midpoint height"
1587 );
1588 }
1589 #[test]
1590 fn toi_spheres_approaching_head_on() {
1591 let result = compute_time_of_impact_spheres(
1592 [-5.0, 0.0, 0.0],
1593 [1.0, 0.0, 0.0],
1594 1.0,
1595 [5.0, 0.0, 0.0],
1596 [-1.0, 0.0, 0.0],
1597 1.0,
1598 100.0,
1599 );
1600 assert!(result.is_some(), "approaching spheres must collide");
1601 let r = result.unwrap();
1602 assert!(
1603 (r.toi - 4.0).abs() < 1e-10,
1604 "toi should be 4.0, got {}",
1605 r.toi
1606 );
1607 }
1608 #[test]
1609 fn toi_spheres_no_collision_diverging() {
1610 let result = compute_time_of_impact_spheres(
1611 [-5.0, 0.0, 0.0],
1612 [-1.0, 0.0, 0.0],
1613 1.0,
1614 [5.0, 0.0, 0.0],
1615 [1.0, 0.0, 0.0],
1616 1.0,
1617 100.0,
1618 );
1619 assert!(result.is_none(), "diverging spheres should not collide");
1620 }
1621 #[test]
1622 fn toi_aabbs_approaching() {
1623 let result = compute_time_of_impact_aabbs(
1624 [-6.0, -1.0, -1.0],
1625 [-4.0, 1.0, 1.0],
1626 [1.0, 0.0, 0.0],
1627 [4.0, -1.0, -1.0],
1628 [6.0, 1.0, 1.0],
1629 [-1.0, 0.0, 0.0],
1630 100.0,
1631 );
1632 assert!(result.is_some(), "approaching boxes must collide");
1633 let toi = result.unwrap();
1634 assert!((toi - 4.0).abs() < 1e-10, "toi should be 4.0, got {}", toi);
1635 }
1636 #[test]
1637 fn toi_aabbs_no_collision_miss() {
1638 let result = compute_time_of_impact_aabbs(
1639 [0.0, 0.0, 0.0],
1640 [1.0, 1.0, 1.0],
1641 [0.0, 1.0, 0.0],
1642 [5.0, 5.0, 5.0],
1643 [6.0, 6.0, 6.0],
1644 [0.0, 1.0, 0.0],
1645 100.0,
1646 );
1647 assert!(
1648 result.is_none(),
1649 "parallel-moving separated boxes should not collide"
1650 );
1651 }
1652 #[test]
1653 fn project_point_convex_square_outside() {
1654 let verts = vec![
1655 [1.0, 0.0, 0.0],
1656 [0.0, 0.0, 1.0],
1657 [-1.0, 0.0, 0.0],
1658 [0.0, 0.0, -1.0],
1659 ];
1660 let p = project_point_convex([3.0, 0.0, 0.0], &verts);
1661 let d = norm(sub(p, [1.0, 0.0, 0.0]));
1662 assert!(d < 0.5, "closest point should be near (1,0,0), got {:?}", p);
1663 }
1664 #[test]
1665 fn project_point_convex_single_vertex() {
1666 let p = project_point_convex([5.0, 5.0, 5.0], &[[1.0, 2.0, 3.0]]);
1667 assert_eq!(
1668 p,
1669 [1.0, 2.0, 3.0],
1670 "single vertex: project returns that vertex"
1671 );
1672 }
1673 #[test]
1674 fn segment_distance_perpendicular_segments() {
1675 let r = distance_between_segments(
1676 [0.0, 0.0, 0.0],
1677 [4.0, 0.0, 0.0],
1678 [2.0, -2.0, 0.0],
1679 [2.0, 2.0, 0.0],
1680 );
1681 assert!(
1682 r.distance < 1e-10,
1683 "crossing segments: distance should be ~0, got {}",
1684 r.distance
1685 );
1686 }
1687 #[test]
1688 fn segment_distance_parallel_segments() {
1689 let r = distance_between_segments(
1690 [0.0, 0.0, 0.0],
1691 [4.0, 0.0, 0.0],
1692 [0.0, 3.0, 0.0],
1693 [4.0, 3.0, 0.0],
1694 );
1695 assert!(
1696 (r.distance - 3.0).abs() < 1e-10,
1697 "parallel segments 3 apart: distance should be 3, got {}",
1698 r.distance
1699 );
1700 }
1701 #[test]
1702 fn segment_distance_disjoint_segments() {
1703 let r = distance_between_segments(
1704 [0.0, 0.0, 0.0],
1705 [1.0, 0.0, 0.0],
1706 [5.0, 0.0, 0.0],
1707 [6.0, 0.0, 0.0],
1708 );
1709 assert!(
1710 (r.distance - 4.0).abs() < 1e-10,
1711 "disjoint segments: distance should be 4, got {}",
1712 r.distance
1713 );
1714 }
1715 #[test]
1716 fn segment_distance_touching_at_endpoint() {
1717 let r = distance_between_segments(
1718 [0.0, 0.0, 0.0],
1719 [1.0, 0.0, 0.0],
1720 [1.0, 0.0, 0.0],
1721 [2.0, 0.0, 0.0],
1722 );
1723 assert!(
1724 r.distance < 1e-10,
1725 "touching segments: distance must be 0, got {}",
1726 r.distance
1727 );
1728 }
1729}