Skip to main content

oxiphysics_collision/query/
functions.rs

1// Auto-generated module
2//
3// 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use 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}
42/// Apply a 4×4 homogeneous transform to a point.
43pub 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}
50/// Invert a rigid-body 4×4 transform (rotation + translation only).
51pub(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}
70/// Transform a ray by the *inverse* of `t` so the query can be done in local space.
71pub 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}
81/// Common shape query interface.
82pub trait ShapeQuery {
83    /// Cast a ray from `ray_origin` in direction `ray_dir` (need not be normalised)
84    /// up to parameter `max_toi`. Returns `None` when there is no hit.
85    fn ray_cast(
86        &self,
87        ray_origin: [f64; 3],
88        ray_dir: [f64; 3],
89        max_toi: f64,
90    ) -> Option<RayCastResult>;
91    /// Find the closest point on the shape to `point` and return it together
92    /// with the signed distance and an inside flag.
93    fn point_query(&self, point: [f64; 3]) -> PointQueryResult;
94    /// Axis-aligned bounding box as (min, max) corner pair.
95    fn aabb(&self) -> ([f64; 3], [f64; 3]);
96}
97/// Closest point on segment \[p0, p1\] to a given point.
98/// Returns `(point, t)` where t ∈ \[0,1\].
99pub 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}
110/// Signed distance from point `p` to the segment \[p0, p1\] (unsigned, always ≥ 0).
111pub 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}
115/// Closest point on triangle (a, b, c) to point `p`.
116/// Returns the closest point as a `[f64; 3]`.
117pub 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}
158/// Signed distance from point `p` to the triangle (a, b, c) surface.
159/// Always returns a non-negative distance (unsigned).
160pub 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}
164/// Test whether point `p` is inside a convex hull defined by a set of half-space
165/// planes.  Each plane is `[a, b, c, d]` where `ax + by + cz + d >= 0` is "inside".
166///
167/// Returns `true` if `p` satisfies all half-spaces.
168pub 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}
178/// Test whether point `p` is inside a convex polygon mesh (list of triangles).
179/// Computes the half-spaces from each triangle and checks all.
180///
181/// `triangles` is a list of `(a, b, c)` triangles; the mesh must be convex
182/// and outward-facing normals.
183pub 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}
195/// Signed distance from point `p` to a convex sphere (centered at `center`, radius `r`).
196/// Negative if inside.
197pub fn signed_dist_to_sphere(p: [f64; 3], center: [f64; 3], radius: f64) -> f64 {
198    norm(sub(p, center)) - radius
199}
200/// Signed distance from point `p` to an axis-aligned box centered at `box_center`
201/// with half-extents `he`.  Negative if inside.
202pub 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}
212/// Signed distance from point `p` to a capsule with segment \[p0, p1\] and radius r.
213pub 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}
217/// Ray vs sphere intersection returning the first hit time.
218/// Returns `Some(t)` if hit, `None` otherwise.
219pub 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}
229/// Ray vs axis-aligned box intersection returning the first hit time.
230pub 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}
243/// Ray vs triangle intersection (Möller-Trumbore algorithm).
244/// Returns `Some(t)` if the ray hits the triangle front face.
245pub 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}
278/// Moving sphere sweep vs static sphere.
279///
280/// Returns `Some(t)` in `[0, max_t]` where the sphere first touches the static sphere.
281pub 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}
314/// Moving sphere sweep vs static AABB.
315///
316/// Uses the Minkowski sum: expand box by sphere radius and cast a ray.
317pub 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}
343/// Test whether two axis-aligned boxes overlap (no contact data).
344///
345/// Returns `true` if the AABB `[min_a, max_a]` overlaps `[min_b, max_b]`.
346pub 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}
354/// Test whether two spheres overlap (no contact data).
355pub 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}
366/// Test whether a sphere overlaps an axis-aligned box.
367///
368/// Uses the standard closest-point projection.
369pub 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}
385/// Test whether two capsules overlap (no contact data).
386///
387/// Computes the closest distance between the two central segments and compares
388/// it against the sum of radii.
389pub 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}
436/// Cast a ray against a list of shapes (brute-force all-pairs).
437///
438/// Returns a sorted list of hits (nearest first).
439pub 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}
468/// Find all shapes that contain the given point (brute-force all-pairs).
469///
470/// Returns a list of shape indices whose interiors contain `point`.
471pub 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}
485/// Query shapes potentially visible from the given frustum (brute-force).
486///
487/// Returns a list of shape indices that are not fully culled by the frustum.
488pub 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}
508/// Find the K nearest shapes to `query_point`, sorted by ascending distance.
509///
510/// Shapes that contain the point have a negative distance and appear first.
511pub 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}
540/// Cast a moving sphere of radius `sphere_radius` from `start` along `velocity`
541/// against a scene of static shapes.  Returns the first hit within `max_t`.
542///
543/// Implemented by expanding each shape by `sphere_radius` (Minkowski sum) and
544/// casting a ray from `start` in the velocity direction.
545pub 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}
613/// Compute the time of impact for two linearly-moving spheres.
614///
615/// Both spheres move at constant velocity over the interval `[0, max_t]`.
616/// Returns `None` if no collision occurs within `max_t`.
617pub 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}
678/// Compute the time of impact for two linearly-moving AABBs.
679///
680/// Uses the separating axis theorem on all three axes.  Returns `None` if the
681/// AABBs do not collide within `[0, max_t]`.
682pub 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}
720/// Project a point onto the surface of a convex shape represented by a set of
721/// support vertices (convex hull vertices).
722///
723/// Returns the closest point on the convex hull surface to `point`.
724///
725/// **Algorithm**: finds the closest vertex, then checks each edge and face
726/// (triangle fan from centroid) to refine.  For a full convex hull this is a
727/// simplified GJK-like approach; here we use the closest-point-to-triangle
728/// test on the convex polygon formed by adjacent vertices.
729pub 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}
766/// Closest point on a triangle `(v0, v1, v2)` to `point` (internal helper).
767pub(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}
813/// Compute the minimum distance (and closest points) between two line segments.
814///
815/// Segments are defined by their endpoints: `(p0_a, p1_a)` and `(p0_b, p1_b)`.
816pub 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}
860/// Closest point on a sphere surface to `point`.
861pub 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}
865/// Closest point on an AABB surface to `point`.
866pub 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}
877/// Closest point on a capsule surface to `point`.
878pub 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}