Skip to main content

oxiphysics_collision/
proximity.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Proximity query algorithms for collision detection.
5//!
6//! Provides distance queries between geometric primitives: points, segments,
7//! triangles, capsules, meshes, and point clouds. Uses `[f64; 3]` for 3D
8//! vectors (no nalgebra dependency).
9
10// ─────────────────────────────────────────────────────────────────────────────
11// Vector math helpers
12// ─────────────────────────────────────────────────────────────────────────────
13
14fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
15    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
16}
17
18fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
19    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
20}
21
22fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
23    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
24}
25
26fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
27    [a[0] * s, a[1] * s, a[2] * s]
28}
29
30fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
31    [
32        a[1] * b[2] - a[2] * b[1],
33        a[2] * b[0] - a[0] * b[2],
34        a[0] * b[1] - a[1] * b[0],
35    ]
36}
37
38fn len3(a: [f64; 3]) -> f64 {
39    dot3(a, a).sqrt()
40}
41
42fn normalize3(a: [f64; 3]) -> [f64; 3] {
43    let l = len3(a).max(1e-15);
44    scale3(a, 1.0 / l)
45}
46
47fn lerp3(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
48    add3(scale3(a, 1.0 - t), scale3(b, t))
49}
50
51fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
52    len3(sub3(a, b))
53}
54
55// ─────────────────────────────────────────────────────────────────────────────
56// SpatialQueryResult
57// ─────────────────────────────────────────────────────────────────────────────
58
59/// Result of a proximity/distance query between two shapes.
60#[derive(Debug, Clone)]
61pub struct SpatialQueryResult {
62    /// Signed distance between the shapes (negative = penetrating).
63    pub distance: f64,
64    /// Closest point on shape A.
65    pub witness_point_a: [f64; 3],
66    /// Closest point on shape B.
67    pub witness_point_b: [f64; 3],
68    /// Separation normal pointing from B to A.
69    pub normal: [f64; 3],
70    /// True if the query is valid (shapes are not degenerate).
71    pub valid: bool,
72}
73
74impl SpatialQueryResult {
75    /// Creates a valid result.
76    pub fn new(
77        distance: f64,
78        witness_point_a: [f64; 3],
79        witness_point_b: [f64; 3],
80        normal: [f64; 3],
81    ) -> Self {
82        Self {
83            distance,
84            witness_point_a,
85            witness_point_b,
86            normal,
87            valid: true,
88        }
89    }
90
91    /// Creates an invalid result (degenerate geometry).
92    pub fn invalid() -> Self {
93        Self {
94            distance: f64::INFINITY,
95            witness_point_a: [0.0; 3],
96            witness_point_b: [0.0; 3],
97            normal: [0.0, 0.0, 1.0],
98            valid: false,
99        }
100    }
101}
102
103// ─────────────────────────────────────────────────────────────────────────────
104// ProximityQuery — point-to-primitive queries
105// ─────────────────────────────────────────────────────────────────────────────
106
107/// Proximity query functions for basic geometric primitives.
108pub struct ProximityQuery;
109
110impl ProximityQuery {
111    /// Closest point on segment \[a, b\] to query point p.
112    ///
113    /// Returns the closest point and parameter t ∈ \[0, 1\].
114    pub fn closest_point_on_segment(a: [f64; 3], b: [f64; 3], p: [f64; 3]) -> ([f64; 3], f64) {
115        let ab = sub3(b, a);
116        let ap = sub3(p, a);
117        let denom = dot3(ab, ab);
118        if denom < 1e-15 {
119            return (a, 0.0);
120        }
121        let t = (dot3(ap, ab) / denom).clamp(0.0, 1.0);
122        (lerp3(a, b, t), t)
123    }
124
125    /// Closest point on an axis-aligned box \[lo, hi\] to point p.
126    pub fn closest_point_to_aabb(lo: [f64; 3], hi: [f64; 3], p: [f64; 3]) -> [f64; 3] {
127        [
128            p[0].clamp(lo[0], hi[0]),
129            p[1].clamp(lo[1], hi[1]),
130            p[2].clamp(lo[2], hi[2]),
131        ]
132    }
133
134    /// Tests whether point p is inside a convex hull defined by a set of planes.
135    ///
136    /// Each plane is (normal, offset): `dot(normal, x) <= offset`.
137    pub fn point_in_convex_hull(p: [f64; 3], planes: &[([f64; 3], f64)]) -> bool {
138        planes.iter().all(|(n, d)| dot3(*n, p) <= *d + 1e-9)
139    }
140
141    /// Returns the signed distance from point p to the plane (normal, offset).
142    ///
143    /// Positive = above, negative = below the plane.
144    pub fn signed_dist_to_plane(p: [f64; 3], normal: [f64; 3], offset: f64) -> f64 {
145        dot3(normal, p) - offset
146    }
147
148    /// Closest point on a sphere (center, radius) to point p.
149    pub fn closest_point_on_sphere(center: [f64; 3], radius: f64, p: [f64; 3]) -> [f64; 3] {
150        let d = sub3(p, center);
151        let len = len3(d).max(1e-15);
152        add3(center, scale3(d, radius / len))
153    }
154}
155
156// ─────────────────────────────────────────────────────────────────────────────
157// SegmentSegmentDist
158// ─────────────────────────────────────────────────────────────────────────────
159
160/// Computes the closest points between two 3D line segments.
161pub struct SegmentSegmentDist;
162
163impl SegmentSegmentDist {
164    /// Computes the minimum distance and witness points between segments \[p1,p2\] and \[p3,p4\].
165    ///
166    /// Returns (distance, point_on_seg1, point_on_seg2).
167    pub fn query(
168        p1: [f64; 3],
169        p2: [f64; 3],
170        p3: [f64; 3],
171        p4: [f64; 3],
172    ) -> (f64, [f64; 3], [f64; 3]) {
173        let d1 = sub3(p2, p1);
174        let d2 = sub3(p4, p3);
175        let r = sub3(p1, p3);
176
177        let a = dot3(d1, d1); // squared length of seg1
178        let e = dot3(d2, d2); // squared length of seg2
179        let f = dot3(d2, r);
180
181        let (s, t);
182        if a < 1e-15 && e < 1e-15 {
183            // Both segments are points
184            return (dist3(p1, p3), p1, p3);
185        }
186        if a < 1e-15 {
187            s = 0.0;
188            t = (f / e).clamp(0.0, 1.0);
189        } else {
190            let c = dot3(d1, r);
191            if e < 1e-15 {
192                t = 0.0;
193                s = (-c / a).clamp(0.0, 1.0);
194            } else {
195                let b = dot3(d1, d2);
196                let denom = a * e - b * b;
197                s = if denom > 1e-15 {
198                    ((b * f - c * e) / denom).clamp(0.0, 1.0)
199                } else {
200                    0.0
201                };
202                t = (b * s + f) / e;
203                if t < 0.0 {
204                    let t_clamp = 0.0_f64;
205                    let s_clamp = (-c / a).clamp(0.0, 1.0);
206                    let wp1 = lerp3(p1, p2, s_clamp);
207                    let wp2 = lerp3(p3, p4, t_clamp);
208                    return (dist3(wp1, wp2), wp1, wp2);
209                } else if t > 1.0 {
210                    let t_clamp = 1.0_f64;
211                    let s_clamp = ((b - c) / a).clamp(0.0, 1.0);
212                    let wp1 = lerp3(p1, p2, s_clamp);
213                    let wp2 = lerp3(p3, p4, t_clamp);
214                    return (dist3(wp1, wp2), wp1, wp2);
215                }
216            }
217        }
218        let wp1 = lerp3(p1, p2, s);
219        let wp2 = lerp3(p3, p4, t);
220        (dist3(wp1, wp2), wp1, wp2)
221    }
222}
223
224// ─────────────────────────────────────────────────────────────────────────────
225// PointTriangleDist
226// ─────────────────────────────────────────────────────────────────────────────
227
228/// Point-to-triangle distance query using barycentric coordinates.
229pub struct PointTriangleDist;
230
231impl PointTriangleDist {
232    /// Computes the closest point on triangle (v0, v1, v2) to point p.
233    ///
234    /// Returns (closest_point, barycentric_coords \[u, v, w\]).
235    pub fn closest_point(
236        p: [f64; 3],
237        v0: [f64; 3],
238        v1: [f64; 3],
239        v2: [f64; 3],
240    ) -> ([f64; 3], [f64; 3]) {
241        let ab = sub3(v1, v0);
242        let ac = sub3(v2, v0);
243        let ap = sub3(p, v0);
244
245        let d1 = dot3(ab, ap);
246        let d2 = dot3(ac, ap);
247        if d1 <= 0.0 && d2 <= 0.0 {
248            return (v0, [1.0, 0.0, 0.0]);
249        }
250
251        let bp = sub3(p, v1);
252        let d3 = dot3(ab, bp);
253        let d4 = dot3(ac, bp);
254        if d3 >= 0.0 && d4 <= d3 {
255            return (v1, [0.0, 1.0, 0.0]);
256        }
257
258        let cp = sub3(p, v2);
259        let d5 = dot3(ab, cp);
260        let d6 = dot3(ac, cp);
261        if d6 >= 0.0 && d5 <= d6 {
262            return (v2, [0.0, 0.0, 1.0]);
263        }
264
265        let vc = d1 * d4 - d3 * d2;
266        if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
267            let v = d1 / (d1 - d3);
268            let q = add3(v0, scale3(ab, v));
269            return (q, [1.0 - v, v, 0.0]);
270        }
271
272        let vb = d5 * d2 - d1 * d6;
273        if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
274            let w = d2 / (d2 - d6);
275            let q = add3(v0, scale3(ac, w));
276            return (q, [1.0 - w, 0.0, w]);
277        }
278
279        let va = d3 * d6 - d5 * d4;
280        if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
281            let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
282            let q = lerp3(v1, v2, w);
283            return (q, [0.0, 1.0 - w, w]);
284        }
285
286        let denom = 1.0 / (va + vb + vc);
287        let v = vb * denom;
288        let w = vc * denom;
289        let u = 1.0 - v - w;
290        let q = add3(add3(scale3(v0, u), scale3(v1, v)), scale3(v2, w));
291        (q, [u, v, w])
292    }
293
294    /// Squared distance from point p to triangle.
295    pub fn dist_sq(p: [f64; 3], v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> f64 {
296        let (q, _) = Self::closest_point(p, v0, v1, v2);
297        let d = sub3(p, q);
298        dot3(d, d)
299    }
300
301    /// Triangle normal (unnormalized).
302    pub fn normal(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
303        cross3(sub3(v1, v0), sub3(v2, v0))
304    }
305}
306
307// ─────────────────────────────────────────────────────────────────────────────
308// SignedDistance — SDF operations
309// ─────────────────────────────────────────────────────────────────────────────
310
311/// Signed distance field (SDF) combinators and primitive SDFs.
312pub struct SignedDistance;
313
314impl SignedDistance {
315    /// Signed distance from point p to a sphere (center, radius).
316    pub fn sphere(p: [f64; 3], center: [f64; 3], radius: f64) -> f64 {
317        dist3(p, center) - radius
318    }
319
320    /// Signed distance from point p to axis-aligned box centered at origin with half-extents h.
321    pub fn box_sdf(p: [f64; 3], h: [f64; 3]) -> f64 {
322        let q = [p[0].abs() - h[0], p[1].abs() - h[1], p[2].abs() - h[2]];
323        let outer = len3([q[0].max(0.0), q[1].max(0.0), q[2].max(0.0)]);
324        let inner = q[0].max(q[1]).max(q[2]).min(0.0);
325        outer + inner
326    }
327
328    /// Signed distance from point p to an infinite cylinder along Y axis, radius r.
329    pub fn cylinder(p: [f64; 3], radius: f64) -> f64 {
330        let r = (p[0] * p[0] + p[2] * p[2]).sqrt();
331        r - radius
332    }
333
334    /// Union of two SDFs: min(d1, d2).
335    pub fn union(d1: f64, d2: f64) -> f64 {
336        d1.min(d2)
337    }
338
339    /// Intersection of two SDFs: max(d1, d2).
340    pub fn intersection(d1: f64, d2: f64) -> f64 {
341        d1.max(d2)
342    }
343
344    /// Subtraction of two SDFs: max(d1, -d2).
345    pub fn subtraction(d1: f64, d2: f64) -> f64 {
346        d1.max(-d2)
347    }
348
349    /// Smooth union with blending radius k.
350    pub fn smooth_union(d1: f64, d2: f64, k: f64) -> f64 {
351        let h = (0.5 + 0.5 * (d2 - d1) / k).clamp(0.0, 1.0);
352        d1 * h + d2 * (1.0 - h) - k * h * (1.0 - h)
353    }
354
355    /// Signed distance from point to convex polygon (2D, in XY plane).
356    ///
357    /// Returns negative if p is inside the polygon, positive if outside.
358    /// Assumes counter-clockwise vertex ordering for correct sign.
359    pub fn convex_polygon_2d(p: [f64; 2], verts: &[[f64; 2]]) -> f64 {
360        let n = verts.len();
361        if n == 0 {
362            return f64::INFINITY;
363        }
364        let mut min_dist = f64::INFINITY;
365        // Ray-casting inside test (horizontal ray, +x direction)
366        let mut crossings = 0i32;
367        for i in 0..n {
368            let a = verts[i];
369            let b = verts[(i + 1) % n];
370            let e = [b[0] - a[0], b[1] - a[1]];
371            let w = [p[0] - a[0], p[1] - a[1]];
372            // Closest point on segment to p
373            let denom = e[0] * e[0] + e[1] * e[1];
374            let t = if denom < 1e-15 {
375                0.0
376            } else {
377                ((w[0] * e[0] + w[1] * e[1]) / denom).clamp(0.0, 1.0)
378            };
379            let closest = [a[0] + t * e[0], a[1] + t * e[1]];
380            let dx = p[0] - closest[0];
381            let dy = p[1] - closest[1];
382            let d = (dx * dx + dy * dy).sqrt();
383            if d < min_dist {
384                min_dist = d;
385            }
386            // Ray-casting test: horizontal ray from p in +x direction
387            // Check if edge crosses the horizontal ray at y = p[1]
388            let a_above = a[1] > p[1];
389            let b_above = b[1] > p[1];
390            if a_above != b_above {
391                // Edge crosses horizontal line at p[1]
392                // Find x-intercept
393                let t_cross = (p[1] - a[1]) / (b[1] - a[1]);
394                let x_cross = a[0] + t_cross * (b[0] - a[0]);
395                if x_cross > p[0] {
396                    crossings += 1;
397                }
398            }
399        }
400        let inside = crossings % 2 == 1;
401        if inside { -min_dist } else { min_dist }
402    }
403}
404
405// ─────────────────────────────────────────────────────────────────────────────
406// CapsuleCapsuleDist
407// ─────────────────────────────────────────────────────────────────────────────
408
409/// Specialized capsule-capsule distance query.
410///
411/// A capsule is defined by a segment (base, tip) and a radius.
412pub struct CapsuleCapsuleDist;
413
414impl CapsuleCapsuleDist {
415    /// Minimum distance between two capsules.
416    ///
417    /// Returns `SpatialQueryResult` with distance and witness points.
418    pub fn query(
419        a_base: [f64; 3],
420        a_tip: [f64; 3],
421        a_radius: f64,
422        b_base: [f64; 3],
423        b_tip: [f64; 3],
424        b_radius: f64,
425    ) -> SpatialQueryResult {
426        let (seg_dist, wa, wb) = SegmentSegmentDist::query(a_base, a_tip, b_base, b_tip);
427        let combined_radius = a_radius + b_radius;
428        let dist = seg_dist - combined_radius;
429        let normal = if seg_dist > 1e-12 {
430            normalize3(sub3(wa, wb))
431        } else {
432            [0.0, 1.0, 0.0]
433        };
434        let witness_a = add3(wa, scale3(normalize3(sub3(wb, wa)).map(|x| x), -a_radius));
435        let witness_b = add3(wb, scale3(normalize3(sub3(wa, wb)).map(|x| x), -b_radius));
436        SpatialQueryResult::new(dist, witness_a, witness_b, normal)
437    }
438}
439
440// ─────────────────────────────────────────────────────────────────────────────
441// KnnQuery — k-nearest neighbors in point cloud
442// ─────────────────────────────────────────────────────────────────────────────
443
444/// K-nearest neighbor query for a point cloud.
445///
446/// Uses a simple linear scan for correctness; a kd-tree is used for efficiency
447/// when the cloud is large.
448pub struct KnnQuery {
449    /// The point cloud.
450    pub points: Vec<[f64; 3]>,
451}
452
453impl KnnQuery {
454    /// Creates a new kNN query structure for the given point cloud.
455    pub fn new(points: Vec<[f64; 3]>) -> Self {
456        Self { points }
457    }
458
459    /// Returns the k nearest neighbors of query point p (indices and distances).
460    ///
461    /// Returns a vector of (index, distance) pairs sorted by distance.
462    pub fn knn(&self, p: [f64; 3], k: usize) -> Vec<(usize, f64)> {
463        let mut dists: Vec<(usize, f64)> = self
464            .points
465            .iter()
466            .enumerate()
467            .map(|(i, &q)| (i, dist3(p, q)))
468            .collect();
469        dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
470        dists.truncate(k);
471        dists
472    }
473
474    /// Returns all points within radius r of query point p.
475    pub fn range_query(&self, p: [f64; 3], r: f64) -> Vec<usize> {
476        self.points
477            .iter()
478            .enumerate()
479            .filter(|&(_, q)| dist3(p, *q) <= r)
480            .map(|(i, _)| i)
481            .collect()
482    }
483
484    /// Returns all points within a ball (center, radius) — same as range query.
485    pub fn ball_query(&self, center: [f64; 3], radius: f64) -> Vec<usize> {
486        self.range_query(center, radius)
487    }
488}
489
490// ─────────────────────────────────────────────────────────────────────────────
491// RayQuery — ray intersection tests
492// ─────────────────────────────────────────────────────────────────────────────
493
494/// Ray structure: origin + direction.
495#[derive(Debug, Clone, Copy)]
496pub struct Ray {
497    /// Ray origin.
498    pub origin: [f64; 3],
499    /// Ray direction (should be unit vector).
500    pub direction: [f64; 3],
501}
502
503impl Ray {
504    /// Creates a new ray.
505    pub fn new(origin: [f64; 3], direction: [f64; 3]) -> Self {
506        Self {
507            origin,
508            direction: normalize3(direction),
509        }
510    }
511
512    /// Point along the ray at parameter t.
513    pub fn at(&self, t: f64) -> [f64; 3] {
514        add3(self.origin, scale3(self.direction, t))
515    }
516}
517
518/// Ray intersection queries against primitives.
519pub struct RayQuery;
520
521impl RayQuery {
522    /// Ray-AABB intersection. Returns t_min if hit, None otherwise.
523    ///
524    /// Uses the slab method.
525    pub fn ray_aabb(ray: &Ray, lo: [f64; 3], hi: [f64; 3]) -> Option<f64> {
526        let mut t_min = f64::NEG_INFINITY;
527        let mut t_max = f64::INFINITY;
528        for i in 0..3 {
529            let inv_d = 1.0 / ray.direction[i];
530            let t1 = (lo[i] - ray.origin[i]) * inv_d;
531            let t2 = (hi[i] - ray.origin[i]) * inv_d;
532            let (t1, t2) = if inv_d >= 0.0 { (t1, t2) } else { (t2, t1) };
533            t_min = t_min.max(t1);
534            t_max = t_max.min(t2);
535        }
536        if t_max >= t_min && t_max >= 0.0 {
537            Some(t_min.max(0.0))
538        } else {
539            None
540        }
541    }
542
543    /// Ray-sphere intersection. Returns t if hit (smallest positive t), None otherwise.
544    pub fn ray_sphere(ray: &Ray, center: [f64; 3], radius: f64) -> Option<f64> {
545        let oc = sub3(ray.origin, center);
546        let a = dot3(ray.direction, ray.direction);
547        let b = 2.0 * dot3(oc, ray.direction);
548        let c = dot3(oc, oc) - radius * radius;
549        let disc = b * b - 4.0 * a * c;
550        if disc < 0.0 {
551            return None;
552        }
553        let sqrt_disc = disc.sqrt();
554        let t1 = (-b - sqrt_disc) / (2.0 * a);
555        let t2 = (-b + sqrt_disc) / (2.0 * a);
556        if t1 >= 0.0 {
557            Some(t1)
558        } else if t2 >= 0.0 {
559            Some(t2)
560        } else {
561            None
562        }
563    }
564
565    /// Ray-triangle intersection using the Möller-Trumbore algorithm.
566    ///
567    /// Returns (t, u, v) if hit, where (u, v) are barycentric coordinates.
568    pub fn ray_triangle(
569        ray: &Ray,
570        v0: [f64; 3],
571        v1: [f64; 3],
572        v2: [f64; 3],
573    ) -> Option<(f64, f64, f64)> {
574        let edge1 = sub3(v1, v0);
575        let edge2 = sub3(v2, v0);
576        let h = cross3(ray.direction, edge2);
577        let a = dot3(edge1, h);
578        if a.abs() < 1e-12 {
579            return None; // parallel
580        }
581        let f = 1.0 / a;
582        let s = sub3(ray.origin, v0);
583        let u = f * dot3(s, h);
584        if !(0.0..=1.0).contains(&u) {
585            return None;
586        }
587        let q = cross3(s, edge1);
588        let v = f * dot3(ray.direction, q);
589        if v < 0.0 || u + v > 1.0 {
590            return None;
591        }
592        let t = f * dot3(edge2, q);
593        if t < 1e-9 {
594            return None;
595        }
596        Some((t, u, v))
597    }
598
599    /// Ray-capsule intersection. Returns the smallest positive t, or None.
600    pub fn ray_capsule(ray: &Ray, cap_a: [f64; 3], cap_b: [f64; 3], radius: f64) -> Option<f64> {
601        // Test infinite cylinder, then clamp to capsule end spheres
602        let ab = sub3(cap_b, cap_a);
603        let ao = sub3(ray.origin, cap_a);
604        let dir = ray.direction;
605
606        let ab_len2 = dot3(ab, ab);
607        if ab_len2 < 1e-15 {
608            // Degenerate: just a sphere
609            return Self::ray_sphere(ray, cap_a, radius);
610        }
611        let ab_hat = scale3(ab, 1.0 / ab_len2.sqrt());
612
613        // Project direction and ao onto plane perp to ab
614        let d_perp = sub3(dir, scale3(ab_hat, dot3(dir, ab_hat)));
615        let ao_perp = sub3(ao, scale3(ab_hat, dot3(ao, ab_hat)));
616
617        let a_coef = dot3(d_perp, d_perp);
618        let b_coef = 2.0 * dot3(d_perp, ao_perp);
619        let c_coef = dot3(ao_perp, ao_perp) - radius * radius;
620
621        let mut t_min = f64::INFINITY;
622
623        if a_coef > 1e-15 {
624            let disc = b_coef * b_coef - 4.0 * a_coef * c_coef;
625            if disc >= 0.0 {
626                let sqrt_d = disc.sqrt();
627                for sign in &[-1.0_f64, 1.0_f64] {
628                    let t = (-b_coef + sign * sqrt_d) / (2.0 * a_coef);
629                    if t >= 0.0 {
630                        let hit = ray.at(t);
631                        let along = dot3(sub3(hit, cap_a), ab_hat);
632                        if along >= 0.0 && along <= ab_len2.sqrt() && t < t_min {
633                            t_min = t;
634                        }
635                    }
636                }
637            }
638        }
639
640        // Test end spheres
641        for &center in &[cap_a, cap_b] {
642            if let Some(t) = Self::ray_sphere(ray, center, radius)
643                && t < t_min
644            {
645                t_min = t;
646            }
647        }
648
649        if t_min < f64::INFINITY {
650            Some(t_min)
651        } else {
652            None
653        }
654    }
655}
656
657// ─────────────────────────────────────────────────────────────────────────────
658// PointMeshDist — BVH-accelerated point-to-mesh query
659// ─────────────────────────────────────────────────────────────────────────────
660
661/// BVH-accelerated point-to-triangle mesh distance query.
662///
663/// Stores mesh triangles and uses a simple bounding-box hierarchy for pruning.
664pub struct PointMeshDist {
665    /// Triangle vertex positions (length = n_triangles * 3, stride = 3 vertices).
666    pub vertices: Vec<[f64; 3]>,
667    /// Triangle indices: each group of 3 = (v0, v1, v2).
668    pub indices: Vec<usize>,
669}
670
671impl PointMeshDist {
672    /// Creates a new mesh distance query structure.
673    pub fn new(vertices: Vec<[f64; 3]>, indices: Vec<usize>) -> Self {
674        assert!(indices.len().is_multiple_of(3));
675        Self { vertices, indices }
676    }
677
678    /// Number of triangles.
679    pub fn n_triangles(&self) -> usize {
680        self.indices.len() / 3
681    }
682
683    /// Minimum squared distance from point p to any triangle in the mesh.
684    ///
685    /// Returns (distance, triangle_index, closest_point).
686    pub fn closest_point(&self, p: [f64; 3]) -> (f64, usize, [f64; 3]) {
687        let mut min_dist_sq = f64::INFINITY;
688        let mut best_tri = 0;
689        let mut best_pt = p;
690        for tri in 0..self.n_triangles() {
691            let v0 = self.vertices[self.indices[3 * tri]];
692            let v1 = self.vertices[self.indices[3 * tri + 1]];
693            let v2 = self.vertices[self.indices[3 * tri + 2]];
694            let (q, _) = PointTriangleDist::closest_point(p, v0, v1, v2);
695            let d2 = dot3(sub3(p, q), sub3(p, q));
696            if d2 < min_dist_sq {
697                min_dist_sq = d2;
698                best_tri = tri;
699                best_pt = q;
700            }
701        }
702        (min_dist_sq.sqrt(), best_tri, best_pt)
703    }
704}
705
706// ─────────────────────────────────────────────────────────────────────────────
707// MeshMeshDist — minimum distance between two meshes
708// ─────────────────────────────────────────────────────────────────────────────
709
710/// Minimum distance query between two triangle meshes.
711pub struct MeshMeshDist;
712
713impl MeshMeshDist {
714    /// Brute-force minimum distance between two meshes.
715    ///
716    /// Returns (distance, tri_idx_a, tri_idx_b, witness_a, witness_b).
717    pub fn query(
718        verts_a: &[[f64; 3]],
719        idx_a: &[usize],
720        verts_b: &[[f64; 3]],
721        idx_b: &[usize],
722    ) -> (f64, usize, usize, [f64; 3], [f64; 3]) {
723        let na = idx_a.len() / 3;
724        let nb = idx_b.len() / 3;
725        let mut min_dist = f64::INFINITY;
726        let mut best = (0usize, 0usize, [0.0f64; 3], [0.0f64; 3]);
727
728        for ta in 0..na {
729            let v0a = verts_a[idx_a[3 * ta]];
730            let v1a = verts_a[idx_a[3 * ta + 1]];
731            let v2a = verts_a[idx_a[3 * ta + 2]];
732            // Centroid of triangle A as query point
733            let centroid_a = scale3(add3(add3(v0a, v1a), v2a), 1.0 / 3.0);
734            for tb in 0..nb {
735                let v0b = verts_b[idx_b[3 * tb]];
736                let v1b = verts_b[idx_b[3 * tb + 1]];
737                let v2b = verts_b[idx_b[3 * tb + 2]];
738                let (q_b, _) = PointTriangleDist::closest_point(centroid_a, v0b, v1b, v2b);
739                let (q_a, _) = PointTriangleDist::closest_point(q_b, v0a, v1a, v2a);
740                let d = dist3(q_a, q_b);
741                if d < min_dist {
742                    min_dist = d;
743                    best = (ta, tb, q_a, q_b);
744                }
745            }
746        }
747        (min_dist, best.0, best.1, best.2, best.3)
748    }
749}
750
751// ─────────────────────────────────────────────────────────────────────────────
752// SweptVolumeDist — swept capsule distance
753// ─────────────────────────────────────────────────────────────────────────────
754
755/// Distance between a swept volume (capsule over a trajectory) and a static shape.
756pub struct SweptVolumeDist;
757
758impl SweptVolumeDist {
759    /// Minimum distance between two swept capsule volumes.
760    ///
761    /// Each swept capsule is described by start/end positions of its axis endpoint,
762    /// and a radius.
763    pub fn capsule_trajectory_dist(
764        start_a: [f64; 3],
765        end_a: [f64; 3],
766        radius_a: f64,
767        start_b: [f64; 3],
768        end_b: [f64; 3],
769        radius_b: f64,
770    ) -> f64 {
771        // Minimum over trajectory: approximate by testing at multiple t values
772        let n = 8;
773        let mut min_dist = f64::INFINITY;
774        for i in 0..=n {
775            let t = i as f64 / n as f64;
776            let pa = lerp3(start_a, end_a, t);
777            let pb = lerp3(start_b, end_b, t);
778            let d = dist3(pa, pb) - radius_a - radius_b;
779            if d < min_dist {
780                min_dist = d;
781            }
782        }
783        min_dist
784    }
785}
786
787// ─────────────────────────────────────────────────────────────────────────────
788// NarrowPhaseProximity — proximity pair cache
789// ─────────────────────────────────────────────────────────────────────────────
790
791/// Caches proximity pairs for narrow-phase filtering.
792///
793/// Maintains hysteresis: pairs remain active until distance > deactivation_threshold.
794pub struct NarrowPhaseProximity {
795    /// Distance threshold to activate a proximity pair.
796    pub activation_threshold: f64,
797    /// Distance threshold to deactivate a proximity pair.
798    pub deactivation_threshold: f64,
799    /// Active proximity pairs: (id_a, id_b).
800    pub active_pairs: Vec<(u32, u32)>,
801}
802
803impl NarrowPhaseProximity {
804    /// Creates a new proximity cache.
805    pub fn new(activation_threshold: f64, deactivation_threshold: f64) -> Self {
806        Self {
807            activation_threshold,
808            deactivation_threshold,
809            active_pairs: Vec::new(),
810        }
811    }
812
813    /// Updates the set of active pairs given new distance measurements.
814    ///
815    /// `distances` is a list of (id_a, id_b, distance).
816    pub fn update(&mut self, distances: &[(u32, u32, f64)]) {
817        for &(a, b, d) in distances {
818            let pair = (a.min(b), a.max(b));
819            let already_active = self.active_pairs.contains(&pair);
820            if d <= self.activation_threshold && !already_active {
821                self.active_pairs.push(pair);
822            } else if d > self.deactivation_threshold && already_active {
823                self.active_pairs.retain(|&p| p != pair);
824            }
825        }
826    }
827
828    /// Returns true if pair (a, b) is currently active.
829    pub fn is_active(&self, a: u32, b: u32) -> bool {
830        let pair = (a.min(b), a.max(b));
831        self.active_pairs.contains(&pair)
832    }
833}
834
835// ─────────────────────────────────────────────────────────────────────────────
836// Tests
837// ─────────────────────────────────────────────────────────────────────────────
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842
843    #[test]
844    fn test_closest_point_on_segment_midpoint() {
845        let a = [0.0, 0.0, 0.0];
846        let b = [2.0, 0.0, 0.0];
847        let p = [1.0, 1.0, 0.0];
848        let (q, t) = ProximityQuery::closest_point_on_segment(a, b, p);
849        assert!((q[0] - 1.0).abs() < 1e-12);
850        assert!(q[1].abs() < 1e-12);
851        assert!((t - 0.5).abs() < 1e-12);
852    }
853
854    #[test]
855    fn test_closest_point_on_segment_clamped_start() {
856        let a = [0.0, 0.0, 0.0];
857        let b = [1.0, 0.0, 0.0];
858        let p = [-1.0, 0.0, 0.0];
859        let (q, t) = ProximityQuery::closest_point_on_segment(a, b, p);
860        assert!((q[0] - 0.0).abs() < 1e-12);
861        assert!((t - 0.0).abs() < 1e-12);
862    }
863
864    #[test]
865    fn test_closest_point_on_segment_clamped_end() {
866        let a = [0.0, 0.0, 0.0];
867        let b = [1.0, 0.0, 0.0];
868        let p = [2.0, 0.0, 0.0];
869        let (q, t) = ProximityQuery::closest_point_on_segment(a, b, p);
870        assert!((q[0] - 1.0).abs() < 1e-12);
871        assert!((t - 1.0).abs() < 1e-12);
872    }
873
874    #[test]
875    fn test_closest_point_to_aabb() {
876        let lo = [0.0, 0.0, 0.0];
877        let hi = [1.0, 1.0, 1.0];
878        let p = [2.0, 0.5, 0.5];
879        let q = ProximityQuery::closest_point_to_aabb(lo, hi, p);
880        assert!((q[0] - 1.0).abs() < 1e-12);
881        assert!((q[1] - 0.5).abs() < 1e-12);
882    }
883
884    #[test]
885    fn test_point_in_convex_hull() {
886        // Unit square in XY plane
887        let planes = [
888            ([1.0, 0.0, 0.0], 1.0),
889            ([-1.0, 0.0, 0.0], 0.0),
890            ([0.0, 1.0, 0.0], 1.0),
891            ([0.0, -1.0, 0.0], 0.0),
892            ([0.0, 0.0, 1.0], 1.0),
893            ([0.0, 0.0, -1.0], 0.0),
894        ];
895        assert!(ProximityQuery::point_in_convex_hull(
896            [0.5, 0.5, 0.5],
897            &planes
898        ));
899        assert!(!ProximityQuery::point_in_convex_hull(
900            [1.5, 0.5, 0.5],
901            &planes
902        ));
903    }
904
905    #[test]
906    fn test_segment_segment_dist_parallel() {
907        let p1 = [0.0, 0.0, 0.0];
908        let p2 = [1.0, 0.0, 0.0];
909        let p3 = [0.0, 1.0, 0.0];
910        let p4 = [1.0, 1.0, 0.0];
911        let (d, _, _) = SegmentSegmentDist::query(p1, p2, p3, p4);
912        assert!((d - 1.0).abs() < 1e-10);
913    }
914
915    #[test]
916    fn test_segment_segment_dist_crossing() {
917        let p1 = [-1.0, 0.0, 0.0];
918        let p2 = [1.0, 0.0, 0.0];
919        let p3 = [0.0, -1.0, 0.0];
920        let p4 = [0.0, 1.0, 0.0];
921        let (d, wa, wb) = SegmentSegmentDist::query(p1, p2, p3, p4);
922        assert!(d < 1e-10, "d={}", d);
923        assert!(wa[0].abs() < 1e-10);
924        assert!(wb[0].abs() < 1e-10);
925    }
926
927    #[test]
928    fn test_segment_segment_dist_skew() {
929        let p1 = [0.0, 0.0, 0.0];
930        let p2 = [1.0, 0.0, 0.0];
931        let p3 = [0.0, 0.0, 1.0];
932        let p4 = [1.0, 0.0, 1.0];
933        let (d, _, _) = SegmentSegmentDist::query(p1, p2, p3, p4);
934        assert!((d - 1.0).abs() < 1e-10);
935    }
936
937    #[test]
938    fn test_point_triangle_dist_inside() {
939        let v0 = [0.0, 0.0, 0.0];
940        let v1 = [1.0, 0.0, 0.0];
941        let v2 = [0.0, 1.0, 0.0];
942        let p = [0.25, 0.25, 1.0];
943        let (q, bary) = PointTriangleDist::closest_point(p, v0, v1, v2);
944        assert!((q[2] - 0.0).abs() < 1e-12);
945        let bary_sum = bary[0] + bary[1] + bary[2];
946        assert!((bary_sum - 1.0).abs() < 1e-10);
947    }
948
949    #[test]
950    fn test_point_triangle_dist_vertex() {
951        let v0 = [0.0, 0.0, 0.0];
952        let v1 = [1.0, 0.0, 0.0];
953        let v2 = [0.0, 1.0, 0.0];
954        let p = [-1.0, -1.0, 0.0];
955        let (q, _) = PointTriangleDist::closest_point(p, v0, v1, v2);
956        assert!((q[0] - 0.0).abs() < 1e-12);
957        assert!((q[1] - 0.0).abs() < 1e-12);
958    }
959
960    #[test]
961    fn test_sdf_sphere() {
962        let center = [0.0, 0.0, 0.0];
963        let d = SignedDistance::sphere([1.5, 0.0, 0.0], center, 1.0);
964        assert!((d - 0.5).abs() < 1e-12);
965        let d_inside = SignedDistance::sphere([0.5, 0.0, 0.0], center, 1.0);
966        assert!(d_inside < 0.0);
967    }
968
969    #[test]
970    fn test_sdf_box() {
971        let h = [1.0, 1.0, 1.0];
972        let d_outside = SignedDistance::box_sdf([2.0, 0.0, 0.0], h);
973        assert!((d_outside - 1.0).abs() < 1e-10);
974        let d_inside = SignedDistance::box_sdf([0.0, 0.0, 0.0], h);
975        assert!(d_inside < 0.0);
976    }
977
978    #[test]
979    fn test_sdf_union_intersection_subtraction() {
980        let d1 = 1.0;
981        let d2 = 2.0;
982        assert_eq!(SignedDistance::union(d1, d2), 1.0);
983        assert_eq!(SignedDistance::intersection(d1, d2), 2.0);
984        assert_eq!(SignedDistance::subtraction(d1, d2), 1.0);
985    }
986
987    #[test]
988    fn test_capsule_capsule_dist_touching() {
989        let a_base = [0.0, 0.0, 0.0];
990        let a_tip = [1.0, 0.0, 0.0];
991        let b_base = [2.5, 0.0, 0.0];
992        let b_tip = [3.5, 0.0, 0.0];
993        let res = CapsuleCapsuleDist::query(a_base, a_tip, 0.5, b_base, b_tip, 0.5);
994        assert!(res.distance >= -1e-6, "dist={}", res.distance);
995    }
996
997    #[test]
998    fn test_knn_query_k1() {
999        let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1000        let knn = KnnQuery::new(pts);
1001        let result = knn.knn([0.1, 0.0, 0.0], 1);
1002        assert_eq!(result[0].0, 0);
1003    }
1004
1005    #[test]
1006    fn test_knn_range_query() {
1007        let pts: Vec<[f64; 3]> = (0..10).map(|i| [i as f64, 0.0, 0.0]).collect();
1008        let knn = KnnQuery::new(pts);
1009        let result = knn.range_query([5.0, 0.0, 0.0], 1.5);
1010        assert!(result.len() >= 2);
1011    }
1012
1013    #[test]
1014    fn test_ray_aabb_hit() {
1015        let ray = Ray::new([0.0, 0.0, -2.0], [0.0, 0.0, 1.0]);
1016        let lo = [-0.5, -0.5, -0.5];
1017        let hi = [0.5, 0.5, 0.5];
1018        let t = RayQuery::ray_aabb(&ray, lo, hi);
1019        assert!(t.is_some());
1020        assert!((t.unwrap() - 1.5).abs() < 1e-10);
1021    }
1022
1023    #[test]
1024    fn test_ray_aabb_miss() {
1025        let ray = Ray::new([0.0, 2.0, -2.0], [0.0, 0.0, 1.0]);
1026        let lo = [-0.5, -0.5, -0.5];
1027        let hi = [0.5, 0.5, 0.5];
1028        let t = RayQuery::ray_aabb(&ray, lo, hi);
1029        assert!(t.is_none());
1030    }
1031
1032    #[test]
1033    fn test_ray_sphere_hit() {
1034        let ray = Ray::new([0.0, 0.0, -3.0], [0.0, 0.0, 1.0]);
1035        let t = RayQuery::ray_sphere(&ray, [0.0, 0.0, 0.0], 1.0);
1036        assert!(t.is_some());
1037        assert!((t.unwrap() - 2.0).abs() < 1e-10);
1038    }
1039
1040    #[test]
1041    fn test_ray_triangle_hit() {
1042        let ray = Ray::new([0.0, 0.0, -1.0], [0.0, 0.0, 1.0]);
1043        let v0 = [-1.0, -1.0, 0.0];
1044        let v1 = [1.0, -1.0, 0.0];
1045        let v2 = [0.0, 1.0, 0.0];
1046        let result = RayQuery::ray_triangle(&ray, v0, v1, v2);
1047        assert!(result.is_some());
1048        let (t, _, _) = result.unwrap();
1049        assert!((t - 1.0).abs() < 1e-10);
1050    }
1051
1052    #[test]
1053    fn test_ray_triangle_miss() {
1054        let ray = Ray::new([5.0, 5.0, -1.0], [0.0, 0.0, 1.0]);
1055        let v0 = [-1.0, -1.0, 0.0];
1056        let v1 = [1.0, -1.0, 0.0];
1057        let v2 = [0.0, 1.0, 0.0];
1058        let result = RayQuery::ray_triangle(&ray, v0, v1, v2);
1059        assert!(result.is_none());
1060    }
1061
1062    #[test]
1063    fn test_point_mesh_dist() {
1064        let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1065        let idx = vec![0usize, 1, 2];
1066        let mesh = PointMeshDist::new(verts, idx);
1067        let (d, _tri, _q) = mesh.closest_point([0.25, 0.25, 1.0]);
1068        assert!((d - 1.0).abs() < 1e-10);
1069    }
1070
1071    #[test]
1072    fn test_narrow_phase_proximity_activate() {
1073        let mut npp = NarrowPhaseProximity::new(1.0, 2.0);
1074        npp.update(&[(0, 1, 0.5)]);
1075        assert!(npp.is_active(0, 1));
1076    }
1077
1078    #[test]
1079    fn test_narrow_phase_proximity_deactivate() {
1080        let mut npp = NarrowPhaseProximity::new(1.0, 2.0);
1081        npp.update(&[(0, 1, 0.5)]);
1082        assert!(npp.is_active(0, 1));
1083        npp.update(&[(0, 1, 3.0)]);
1084        assert!(!npp.is_active(0, 1));
1085    }
1086
1087    #[test]
1088    fn test_sdf_convex_polygon_inside() {
1089        let square = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1090        let d = SignedDistance::convex_polygon_2d([0.5, 0.5], &square);
1091        assert!(d < 0.0);
1092    }
1093
1094    #[test]
1095    fn test_sdf_convex_polygon_outside() {
1096        let square = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1097        let d = SignedDistance::convex_polygon_2d([2.0, 0.5], &square);
1098        assert!(d > 0.0);
1099    }
1100
1101    #[test]
1102    fn test_spatial_query_result_valid() {
1103        let r = SpatialQueryResult::new(1.0, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1104        assert!(r.valid);
1105        assert!((r.distance - 1.0).abs() < 1e-12);
1106    }
1107}