Skip to main content

fast_mesh_query/
tri.rs

1//! Watertight ray-triangle intersection (Woop, Benthin and Wald 2013), closest
2//! point on a triangle, and box and sphere overlap tests.
3
4use fast_mesh_bvh::Aabb;
5
6/// Which triangle faces a ray ignores. A triangle's front is the side its
7/// counter-clockwise winding is seen from.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum Cull {
10    /// Hit both faces.
11    #[default]
12    None,
13    /// Ignore back faces.
14    Back,
15    /// Ignore front faces.
16    Front,
17}
18
19/// Per-ray constants, computed once and reused for every triangle.
20#[derive(Debug, Clone, Copy)]
21pub(crate) struct RayPre {
22    pub origin: [f32; 3],
23    pub dir: [f32; 3],
24    /// `1 / dir`, with zero components giving infinities.
25    pub inv_dir: [f32; 3],
26    kx: usize,
27    ky: usize,
28    kz: usize,
29    sx: f32,
30    sy: f32,
31    sz: f32,
32}
33
34impl RayPre {
35    pub(crate) fn new(origin: [f32; 3], dir: [f32; 3]) -> RayPre {
36        let a = [dir[0].abs(), dir[1].abs(), dir[2].abs()];
37        let kz = if a[0] >= a[1] && a[0] >= a[2] {
38            0
39        } else if a[1] >= a[2] {
40            1
41        } else {
42            2
43        };
44        let mut kx = (kz + 1) % 3;
45        let mut ky = (kx + 1) % 3;
46        if dir[kz] < 0.0 {
47            std::mem::swap(&mut kx, &mut ky);
48        }
49        RayPre {
50            origin,
51            dir,
52            inv_dir: [1.0 / dir[0], 1.0 / dir[1], 1.0 / dir[2]],
53            kx,
54            ky,
55            kz,
56            sx: dir[kx] / dir[kz],
57            sy: dir[ky] / dir[kz],
58            sz: 1.0 / dir[kz],
59        }
60    }
61}
62
63/// A ray-triangle hit.
64#[derive(Debug, Clone, Copy)]
65pub(crate) struct TriHit {
66    /// Distance along the ray, in units of `dir`.
67    pub t: f32,
68    /// Weights of the triangle's three corners at the hit.
69    pub barycentric: [f32; 3],
70}
71
72/// Watertight test of a ray against the triangle `p`, hitting when
73/// `min_t <= t <= max_t`. Rays through a shared edge or vertex hit at least one
74/// of the triangles meeting there.
75///
76/// A zero-area triangle is never hit. "Zero area" means that `det`, the sum of
77/// the three edge functions, is exactly zero as computed (after the `f64`
78/// fallback). A triangle that is collinear in real numbers but whose `det`
79/// rounds to a nonzero value may still be hit, at a point that is valid for
80/// the rounded geometry. A hit whose `t` or weights are not finite is
81/// rejected.
82pub(crate) fn ray_triangle(
83    pre: &RayPre,
84    p: [[f32; 3]; 3],
85    cull: Cull,
86    min_t: f32,
87    max_t: f32,
88) -> Option<TriHit> {
89    let (kx, ky, kz) = (pre.kx, pre.ky, pre.kz);
90    let a = sub(p[0], pre.origin);
91    let b = sub(p[1], pre.origin);
92    let c = sub(p[2], pre.origin);
93    let ax = a[kx] - pre.sx * a[kz];
94    let ay = a[ky] - pre.sy * a[kz];
95    let bx = b[kx] - pre.sx * b[kz];
96    let by = b[ky] - pre.sy * b[kz];
97    let cx = c[kx] - pre.sx * c[kz];
98    let cy = c[ky] - pre.sy * c[kz];
99
100    let mut u = cx * by - cy * bx;
101    let mut v = ax * cy - ay * cx;
102    let mut w = bx * ay - by * ax;
103    if u == 0.0 || v == 0.0 || w == 0.0 {
104        let (ax, ay, bx, by, cx, cy) = (
105            f64::from(ax),
106            f64::from(ay),
107            f64::from(bx),
108            f64::from(by),
109            f64::from(cx),
110            f64::from(cy),
111        );
112        u = (cx * by - cy * bx) as f32;
113        v = (ax * cy - ay * cx) as f32;
114        w = (bx * ay - by * ax) as f32;
115    }
116
117    let inside = match cull {
118        Cull::None => (u >= 0.0 && v >= 0.0 && w >= 0.0) || (u <= 0.0 && v <= 0.0 && w <= 0.0),
119        Cull::Back => u >= 0.0 && v >= 0.0 && w >= 0.0,
120        Cull::Front => u <= 0.0 && v <= 0.0 && w <= 0.0,
121    };
122    if !inside {
123        return None;
124    }
125    let det = u + v + w;
126    if det == 0.0 {
127        return None;
128    }
129    let t_scaled = u * (pre.sz * a[kz]) + v * (pre.sz * b[kz]) + w * (pre.sz * c[kz]);
130    let t = t_scaled / det;
131    // `t` overflows to infinity when `t_scaled` does, which an infinite `max_t`
132    // would otherwise accept.
133    if !(t.is_finite() && min_t <= t && t <= max_t) {
134        return None;
135    }
136    // Dividing rather than multiplying by `1 / det` keeps the weights finite
137    // when `det` is subnormal (its reciprocal overflows).
138    let barycentric = [u / det, v / det, w / det];
139    if !barycentric.iter().all(|b| b.is_finite()) {
140        return None;
141    }
142    Some(TriHit { t, barycentric })
143}
144
145/// The part of a triangle that is closest to a query point.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum Feature {
148    /// The corner with this index (0, 1 or 2).
149    Vertex(u8),
150    /// The edge joining corners `k` and `(k + 1) % 3`.
151    Edge(u8),
152    /// The interior of the triangle.
153    Face,
154}
155
156/// The scalar type of [`closest_on_triangle`]: `f32` for the mesh queries and
157/// `f64` for triangle-triangle distance. Only what that function needs.
158pub(crate) trait Real:
159    Copy
160    + PartialOrd
161    + std::ops::Add<Output = Self>
162    + std::ops::Sub<Output = Self>
163    + std::ops::Mul<Output = Self>
164    + std::ops::Div<Output = Self>
165{
166    const ZERO: Self;
167    const ONE: Self;
168    /// The sliver threshold; see [`closest_on_triangle`].
169    const SLIVER: Self;
170    fn is_nan(self) -> bool;
171    fn is_finite(self) -> bool;
172}
173
174macro_rules! impl_real {
175    ($t:ty) => {
176        impl Real for $t {
177            const ZERO: Self = 0.0;
178            const ONE: Self = 1.0;
179            const SLIVER: Self = 1e-5;
180            fn is_nan(self) -> bool {
181                <$t>::is_nan(self)
182            }
183            fn is_finite(self) -> bool {
184                <$t>::is_finite(self)
185            }
186        }
187    };
188}
189impl_real!(f32);
190impl_real!(f64);
191
192/// The point of a triangle closest to a query point.
193#[derive(Debug, Clone, Copy)]
194pub(crate) struct TriClosest<T = f32> {
195    pub point: [T; 3],
196    /// Squared distance from the query to `point`.
197    pub dist2: T,
198    /// Weights of the triangle's three corners at `point`.
199    pub barycentric: [T; 3],
200    pub feature: Feature,
201}
202
203fn dot<T: Real>(a: [T; 3], b: [T; 3]) -> T {
204    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
205}
206
207fn dist2<T: Real>(a: [T; 3], b: [T; 3]) -> T {
208    let d = sub(a, b);
209    dot(d, d)
210}
211
212/// The point `a + t * (b - a)`.
213fn lerp_point<T: Real>(a: [T; 3], b: [T; 3], t: T) -> [T; 3] {
214    std::array::from_fn(|k| a[k] + t * (b[k] - a[k]))
215}
216
217/// Closest point of the segment `p[i]..p[j]` to `q`, as an edge or an end vertex.
218fn on_segment<T: Real>(q: [T; 3], p: [[T; 3]; 3], i: usize, j: usize, edge: u8) -> TriClosest<T> {
219    let ab = sub(p[j], p[i]);
220    let len2 = dot(ab, ab);
221    let t = if len2 > T::ZERO {
222        dot(sub(q, p[i]), ab) / len2
223    } else {
224        T::ZERO
225    };
226    let (t, feature) = if t.is_nan() || t <= T::ZERO {
227        (T::ZERO, Feature::Vertex(i as u8))
228    } else if t >= T::ONE {
229        (T::ONE, Feature::Vertex(j as u8))
230    } else {
231        (t, Feature::Edge(edge))
232    };
233    let mut barycentric = [T::ZERO; 3];
234    barycentric[i] = T::ONE - t;
235    barycentric[j] = t;
236    let point = lerp_point(p[i], p[j], t);
237    TriClosest {
238        point,
239        dist2: dist2(q, point),
240        barycentric,
241        feature,
242    }
243}
244
245/// The point of the triangle `p` closest to `q` (Ericson, Real-Time Collision
246/// Detection, 5.1.5), with the feature it lies on.
247///
248/// The result always lies on the triangle. A degenerate or sliver triangle
249/// (`sin^2` of its angle at corner 0 at most [`Real::SLIVER`], or a non-finite
250/// area term) is also tried as its three edges, and the nearest candidate wins,
251/// ties going to the earlier of edge 0, 1, 2 and then to Ericson's result. For
252/// such slivers the feature may be an `Edge` or `Vertex` although the true
253/// nearest point is interior; the error is bounded by the sliver's width.
254///
255/// Slivers: `sum` below is `|ab x ac|^2`, so `sum <= SLIVER * |ab|^2 * |ac|^2`
256/// means `sin^2(angle at a) <= 1e-5`. There the region tests of Ericson's
257/// method are dominated by cancellation noise.
258pub(crate) fn closest_on_triangle<T: Real>(q: [T; 3], p: [[T; 3]; 3]) -> TriClosest<T> {
259    let [a, b, c] = p;
260    let ab = sub(b, a);
261    let ac = sub(c, a);
262    let ap = sub(q, a);
263    let bp = sub(q, b);
264    let cp = sub(q, c);
265    let (d1, d2) = (dot(ab, ap), dot(ac, ap));
266    let (d3, d4) = (dot(ab, bp), dot(ac, bp));
267    let (d5, d6) = (dot(ab, cp), dot(ac, cp));
268    let vc = d1 * d4 - d3 * d2;
269    let vb = d5 * d2 - d1 * d6;
270    let va = d3 * d6 - d5 * d4;
271    let sum = va + vb + vc;
272
273    let ericson = || -> TriClosest<T> {
274        let vertex = |k: u8| {
275            let mut barycentric = [T::ZERO; 3];
276            barycentric[k as usize] = T::ONE;
277            let point = p[k as usize];
278            TriClosest {
279                point,
280                dist2: dist2(q, point),
281                barycentric,
282                feature: Feature::Vertex(k),
283            }
284        };
285        let edge = |i: usize, j: usize, t: T, k: u8| {
286            let mut barycentric = [T::ZERO; 3];
287            barycentric[i] = T::ONE - t;
288            barycentric[j] = t;
289            let point = lerp_point(p[i], p[j], t);
290            TriClosest {
291                point,
292                dist2: dist2(q, point),
293                barycentric,
294                feature: Feature::Edge(k),
295            }
296        };
297
298        let z = T::ZERO;
299        if d1 <= z && d2 <= z {
300            return vertex(0);
301        }
302        if d3 >= z && d4 <= d3 {
303            return vertex(1);
304        }
305        if vc <= z && d1 >= z && d3 <= z {
306            return edge(0, 1, d1 / (d1 - d3), 0);
307        }
308        if d6 >= z && d5 <= d6 {
309            return vertex(2);
310        }
311        if vb <= z && d2 >= z && d6 <= z {
312            return edge(0, 2, d2 / (d2 - d6), 2);
313        }
314        if va <= z && d4 - d3 >= z && d5 - d6 >= z {
315            return edge(1, 2, (d4 - d3) / ((d4 - d3) + (d5 - d6)), 1);
316        }
317        let v = vb / sum;
318        let w = vc / sum;
319        let point = std::array::from_fn(|k| a[k] + ab[k] * v + ac[k] * w);
320        TriClosest {
321            point,
322            dist2: dist2(q, point),
323            barycentric: [T::ONE - v - w, v, w],
324            feature: Feature::Face,
325        }
326    };
327
328    let band = !sum.is_finite() || sum <= T::SLIVER * dot(ab, ab) * dot(ac, ac);
329    if !band {
330        return ericson();
331    }
332    let mut best = on_segment(q, p, 0, 1, 0);
333    for (i, j, edge) in [(1, 2, 1), (2, 0, 2)] {
334        let c = on_segment(q, p, i, j, edge);
335        if c.dist2 < best.dist2 {
336            best = c;
337        }
338    }
339    if sum > T::ZERO && sum.is_finite() {
340        let e = ericson();
341        if e.dist2 < best.dist2 {
342            best = e;
343        }
344    }
345    best
346}
347
348fn sub<T: Real>(a: [T; 3], b: [T; 3]) -> [T; 3] {
349    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
350}
351
352/// Whether the triangle `p` and the closed box `b` share a point: the
353/// separating-axis test of Akenine-Moller with 13 axes (the three box normals,
354/// the triangle normal and the nine edge-by-box-axis crosses), in `f64`.
355///
356/// An axis separates only when the projected intervals are strictly disjoint,
357/// so touching counts as overlap. An axis that is the zero vector (a
358/// degenerate triangle's normal, or a cross of parallel vectors) is skipped. An
359/// empty box overlaps nothing.
360///
361/// The box is first clipped to the triangle's own bounding box, which is exact
362/// (the box and the triangle meet iff the clipped box and the triangle do) and
363/// makes every coordinate finite and of the triangle's scale, so infinite or
364/// huge boxes lose no precision.
365pub(crate) fn box_triangle(b: &Aabb, p: [[f32; 3]; 3]) -> bool {
366    if b.is_empty() {
367        return false;
368    }
369    let mut lo = [0.0f32; 3];
370    let mut hi = [0.0f32; 3];
371    for k in 0..3 {
372        let tmin = p[0][k].min(p[1][k]).min(p[2][k]);
373        let tmax = p[0][k].max(p[1][k]).max(p[2][k]);
374        lo[k] = b.min[k].max(tmin);
375        hi[k] = b.max[k].min(tmax);
376        // Closed boxes: touching is overlap, so only a strict gap is empty.
377        if lo[k] > hi[k] {
378            return false;
379        }
380    }
381    let c: [f64; 3] = std::array::from_fn(|k| (f64::from(lo[k]) + f64::from(hi[k])) * 0.5);
382    let h: [f64; 3] = std::array::from_fn(|k| (f64::from(hi[k]) - f64::from(lo[k])) * 0.5);
383    let v: [[f64; 3]; 3] = p.map(|q| std::array::from_fn(|k| f64::from(q[k]) - c[k]));
384
385    // Box normals.
386    for k in 0..3 {
387        let lo = v[0][k].min(v[1][k]).min(v[2][k]);
388        let hi = v[0][k].max(v[1][k]).max(v[2][k]);
389        if lo > h[k] || hi < -h[k] {
390            return false;
391        }
392    }
393
394    let e: [[f64; 3]; 3] = std::array::from_fn(|i| {
395        let (a, b) = (v[i], v[(i + 1) % 3]);
396        [b[0] - a[0], b[1] - a[1], b[2] - a[2]]
397    });
398    let separates = |axis: [f64; 3]| {
399        if axis == [0.0; 3] {
400            return false;
401        }
402        let d = v.map(|q| axis[0] * q[0] + axis[1] * q[1] + axis[2] * q[2]);
403        let r = h[0] * axis[0].abs() + h[1] * axis[1].abs() + h[2] * axis[2].abs();
404        d[0].min(d[1]).min(d[2]) > r || d[0].max(d[1]).max(d[2]) < -r
405    };
406
407    // Triangle normal.
408    if separates(cross64(e[0], e[1])) {
409        return false;
410    }
411    // Edge crosses.
412    for edge in e {
413        for k in 0..3 {
414            let mut unit = [0.0; 3];
415            unit[k] = 1.0;
416            if separates(cross64(edge, unit)) {
417                return false;
418            }
419        }
420    }
421    true
422}
423
424fn cross64(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
425    [
426        a[1] * b[2] - a[2] * b[1],
427        a[2] * b[0] - a[0] * b[2],
428        a[0] * b[1] - a[1] * b[0],
429    ]
430}
431
432/// Whether the triangle `p` comes within `radius` of `center` (touching
433/// counts).
434pub(crate) fn sphere_triangle(center: [f32; 3], radius: f32, p: [[f32; 3]; 3]) -> bool {
435    closest_on_triangle(center, p).dist2 <= radius * radius
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    const T: [[f32; 3]; 3] = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
443    const DOWN: [f32; 3] = [0.0, 0.0, -1.0];
444    const UP: [f32; 3] = [0.0, 0.0, 1.0];
445
446    fn cast(o: [f32; 3], d: [f32; 3], p: [[f32; 3]; 3], cull: Cull) -> Option<TriHit> {
447        ray_triangle(&RayPre::new(o, d), p, cull, 0.0, f32::INFINITY)
448    }
449
450    #[test]
451    fn hits_the_interior() {
452        let h = cast([0.25, 0.25, 1.0], DOWN, T, Cull::None).unwrap();
453        assert_eq!(h.t, 1.0);
454        let sum: f32 = h.barycentric.iter().sum();
455        assert!((sum - 1.0).abs() < 1e-6);
456        for (a, b) in h.barycentric.iter().zip([0.5, 0.25, 0.25]) {
457            assert!((a - b).abs() < 1e-6);
458        }
459    }
460
461    #[test]
462    fn misses_outside_and_behind() {
463        assert!(cast([2.0, 2.0, 1.0], DOWN, T, Cull::None).is_none());
464        assert!(cast([0.25, 0.25, -1.0], DOWN, T, Cull::None).is_none());
465    }
466
467    #[test]
468    fn culling() {
469        let o = [0.25, 0.25, 1.0];
470        assert!(cast(o, DOWN, T, Cull::None).is_some());
471        assert!(cast(o, DOWN, T, Cull::Back).is_some());
472        assert!(cast(o, DOWN, T, Cull::Front).is_none());
473        let u = [0.25, 0.25, -1.0];
474        assert!(cast(u, UP, T, Cull::None).is_some());
475        assert!(cast(u, UP, T, Cull::Front).is_some());
476        assert!(cast(u, UP, T, Cull::Back).is_none());
477    }
478
479    #[test]
480    fn range_is_closed() {
481        let pre = RayPre::new([0.25, 0.25, 1.0], DOWN);
482        assert!(ray_triangle(&pre, T, Cull::None, 1.0, f32::INFINITY).is_some());
483        assert!(ray_triangle(&pre, T, Cull::None, 0.0, 1.0).is_some());
484        assert!(ray_triangle(&pre, T, Cull::None, 0.0, 0.999).is_none());
485    }
486
487    #[test]
488    fn parallel_and_degenerate_miss() {
489        assert!(cast([0.25, 0.25, 0.0], [1.0, 0.0, 0.0], T, Cull::None).is_none());
490        let d = [[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 2.0, 0.0]];
491        assert!(cast([1.0, 1.0, 1.0], DOWN, d, Cull::None).is_none());
492    }
493
494    #[test]
495    fn subnormal_det_gives_finite_weights() {
496        // Edge functions of about 1e-44 are subnormal in f32, so `1 / det`
497        // overflows. The hit must still have finite weights.
498        let s = 1e-22;
499        let p = [[0.0, 0.0, 0.0], [s, 0.0, 0.0], [0.0, s, 0.0]];
500        let o = [s / 4.0, s / 4.0, 1.0];
501        let h = cast(o, DOWN, p, Cull::None).unwrap();
502        assert_eq!(h.t, 1.0);
503        assert!(h.barycentric.iter().all(|b| b.is_finite()));
504        let sum: f32 = h.barycentric.iter().sum();
505        assert!((sum - 1.0).abs() < 1e-6);
506    }
507
508    #[test]
509    fn overflowing_t_is_not_a_hit() {
510        // The real t is 3e38, but t_scaled = u*z + v*z + w*z overflows to
511        // infinity, which `max_t = INFINITY` would otherwise accept.
512        let z = -3e38;
513        let p = [[-10.0, -10.0, z], [10.0, -10.0, z], [0.0, 10.0, z]];
514        let o = [0.0, 0.0, 0.0];
515        let pre = RayPre::new(o, DOWN);
516        let a = sub(p[0], o);
517        assert!((pre.sz * a[2]).is_finite());
518        assert_eq!(cast(o, DOWN, p, Cull::None).map(|h| h.t), None);
519    }
520
521    #[test]
522    fn shared_edge_is_watertight() {
523        let t2 = [[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]];
524        for i in 1..=1000 {
525            let s = i as f32 / 1001.0;
526            let o = [s, 1.0 - s, 1.0];
527            assert!(
528                cast(o, DOWN, T, Cull::None).is_some() || cast(o, DOWN, t2, Cull::None).is_some(),
529                "leak at s = {s}"
530            );
531        }
532        // The shared vertex (1,0,0) belongs to both and to a third triangle.
533        let t3 = [[1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
534        let o = [1.0, 0.0, 1.0];
535        assert!(
536            [T, t2, t3]
537                .iter()
538                .any(|p| cast(o, DOWN, *p, Cull::None).is_some())
539        );
540    }
541
542    fn close(a: [f32; 3], b: [f32; 3]) -> bool {
543        a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
544    }
545
546    /// One Voronoi-region case: the query, the feature, the closest point
547    /// and the barycentric weights.
548    type VoronoiCase = ([f64; 3], Feature, [f64; 3], [f64; 3]);
549
550    /// Voronoi-region cases shared by the `f32` and `f64` tests. Every value
551    /// is exactly representable in both types.
552    const VORONOI: [VoronoiCase; 7] = [
553        (
554            [-1.0, -1.0, 0.0],
555            Feature::Vertex(0),
556            [0.0, 0.0, 0.0],
557            [1.0, 0.0, 0.0],
558        ),
559        (
560            [2.0, -1.0, 0.0],
561            Feature::Vertex(1),
562            [1.0, 0.0, 0.0],
563            [0.0, 1.0, 0.0],
564        ),
565        (
566            [-1.0, 2.0, 0.0],
567            Feature::Vertex(2),
568            [0.0, 1.0, 0.0],
569            [0.0, 0.0, 1.0],
570        ),
571        (
572            [0.5, -1.0, 0.0],
573            Feature::Edge(0),
574            [0.5, 0.0, 0.0],
575            [0.5, 0.5, 0.0],
576        ),
577        (
578            [1.0, 1.0, 0.0],
579            Feature::Edge(1),
580            [0.5, 0.5, 0.0],
581            [0.0, 0.5, 0.5],
582        ),
583        (
584            [-1.0, 0.5, 0.0],
585            Feature::Edge(2),
586            [0.0, 0.5, 0.0],
587            [0.5, 0.0, 0.5],
588        ),
589        (
590            [0.25, 0.25, 3.0],
591            Feature::Face,
592            [0.25, 0.25, 0.0],
593            [0.5, 0.25, 0.25],
594        ),
595    ];
596
597    #[test]
598    fn voronoi_regions() {
599        for (q, feature, point, bary) in VORONOI {
600            let (q, point, bary) = (
601                q.map(|v| v as f32),
602                point.map(|v| v as f32),
603                bary.map(|v| v as f32),
604            );
605            let c = closest_on_triangle(q, T);
606            assert_eq!(c.feature, feature, "{q:?}");
607            assert!(close(c.point, point), "{q:?}: {:?}", c.point);
608            assert!(close(c.barycentric, bary), "{q:?}: {:?}", c.barycentric);
609        }
610        assert_eq!(closest_on_triangle([0.25, 0.25, 3.0], T).dist2, 9.0);
611    }
612
613    #[test]
614    fn degenerate_triangle_uses_segments() {
615        let line: [[f32; 3]; 3] = [[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 2.0, 0.0]];
616        let c = closest_on_triangle([1.0, 0.0, 0.0], line);
617        assert!(close(c.point, [0.5, 0.5, 0.0]));
618        assert_eq!(c.dist2, 0.5);
619        assert!(matches!(c.feature, Feature::Edge(_)));
620        assert!(c.barycentric.iter().all(|b| b.is_finite()));
621
622        let dot: [[f32; 3]; 3] = [[1.0, 1.0, 1.0]; 3];
623        let c = closest_on_triangle([0.0, 0.0, 0.0], dot);
624        assert_eq!(c.feature, Feature::Vertex(0));
625        assert_eq!(c.dist2, 3.0);
626    }
627
628    /// Minimum distance from `q` to the three edges of `p`, in f64.
629    fn segment_distance(q: [f32; 3], p: [[f32; 3]; 3]) -> f64 {
630        let v = |a: [f32; 3]| a.map(f64::from);
631        let q = v(q);
632        (0..3)
633            .map(|i| {
634                let (a, b) = (v(p[i]), v(p[(i + 1) % 3]));
635                let ab: Vec<f64> = (0..3).map(|k| b[k] - a[k]).collect();
636                let len2: f64 = ab.iter().map(|x| x * x).sum();
637                let t = if len2 > 0.0 {
638                    ((0..3).map(|k| (q[k] - a[k]) * ab[k]).sum::<f64>() / len2).clamp(0.0, 1.0)
639                } else {
640                    0.0
641                };
642                (0..3)
643                    .map(|k| (q[k] - a[k] - t * ab[k]).powi(2))
644                    .sum::<f64>()
645                    .sqrt()
646            })
647            .fold(f64::INFINITY, f64::min)
648    }
649
650    #[test]
651    fn near_collinear_sliver_is_not_far_off() {
652        let p = [
653            [0.5389, -0.9795, -0.2304],
654            [0.4449, -1.3181, -0.0153],
655            [0.1759, -2.2879, 0.6010],
656        ];
657        let q = [-1.531, -0.251, 1.715];
658        let c = closest_on_triangle(q, p);
659        let got = f64::from(c.dist2).sqrt();
660        assert!((got - segment_distance(q, p)).abs() < 1e-4, "{got}");
661    }
662
663    #[test]
664    fn random_slivers_stay_close_and_on_the_triangle() {
665        let mut rng = crate::test_util::Lcg(0x511_7e75);
666        let mut r = |lo: f32, hi: f32| lo + (hi - lo) * rng.next();
667        for _ in 0..5000 {
668            let a = [r(-2.0, 2.0), r(-2.0, 2.0), r(-2.0, 2.0)];
669            let b = [r(-2.0, 2.0), r(-2.0, 2.0), r(-2.0, 2.0)];
670            let s = r(-1.0, 3.0);
671            let c = std::array::from_fn(|k| a[k] + s * (b[k] - a[k]));
672            let q = [r(-3.0, 3.0), r(-3.0, 3.0), r(-3.0, 3.0)];
673            let p = [a, b, c];
674            let got = closest_on_triangle(q, p);
675            let d = f64::from(got.dist2).sqrt();
676            assert!(d <= segment_distance(q, p) + 1e-4, "{p:?} {q:?}: {d}");
677            // The point is the barycentric combination of the corners.
678            let w = got.barycentric;
679            assert!(w.iter().all(|x| *x >= -1e-4), "{w:?}");
680            assert!((w.iter().sum::<f32>() - 1.0).abs() < 1e-4);
681            for k in 0..3 {
682                let m = w[0] * a[k] + w[1] * b[k] + w[2] * c[k];
683                assert!((m - got.point[k]).abs() < 1e-4, "{p:?} {q:?}");
684            }
685        }
686    }
687
688    #[test]
689    fn distant_query_on_small_sliver_stays_accurate() {
690        // The sliver band does not depend on the query distance, so a query
691        // far beyond the triangle's size takes the noisy path; the distance
692        // error stays second order (issue #19).
693        let p = [[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1e-4, 0.0]];
694        for dist in [10.0f32, 50.0] {
695            let q = [0.5, 0.5, dist];
696            let c = closest_on_triangle(q, p);
697            let got = f64::from(c.dist2).sqrt();
698            assert!((got - segment_distance(q, p)).abs() < 1e-4, "{dist}: {got}");
699            // The point is the barycentric combination of the corners.
700            let w = c.barycentric;
701            assert!(w.iter().all(|x| *x >= -1e-4), "{w:?}");
702            assert!((w.iter().sum::<f32>() - 1.0).abs() < 1e-4);
703            let m: [f32; 3] =
704                std::array::from_fn(|k| w[0] * p[0][k] + w[1] * p[1][k] + w[2] * p[2][k]);
705            for (got, want) in m.iter().zip(c.point.iter()) {
706                assert!((got - want).abs() < 1e-3, "{dist}");
707            }
708        }
709    }
710
711    #[test]
712    fn box_triangle_cases() {
713        let b = |min, max| Aabb::new(min, max);
714        // Overlap.
715        assert!(box_triangle(&b([0.2, 0.2, -1.0], [0.3, 0.3, 1.0]), T));
716        // Separated by a box face axis.
717        assert!(!box_triangle(&b([2.0, 2.0, -1.0], [3.0, 3.0, 1.0]), T));
718        // Separated by the triangle normal.
719        assert!(!box_triangle(&b([0.0, 0.0, 0.1], [1.0, 1.0, 1.0]), T));
720        // Separated only by an edge-cross axis (the hypotenuse): every
721        // coordinate range of the box overlaps the triangle's.
722        assert!(!box_triangle(&b([0.6, 0.6, -1.0], [1.0, 1.0, 1.0]), T));
723        // Touching at the corner (1, 0, 0) counts as overlap.
724        assert!(box_triangle(&b([1.0, 0.0, 0.0], [2.0, 1.0, 1.0]), T));
725        // An empty box overlaps nothing.
726        assert!(!box_triangle(&Aabb::EMPTY, T));
727    }
728
729    #[test]
730    fn box_triangle_unbounded_and_huge_boxes() {
731        let b = |min, max| Aabb::new(min, max);
732        let inf = f32::INFINITY;
733        // The triangle lies below x + y = 1, so x >= 0.6, y >= 0.6 misses it.
734        for big in [inf, 1e30, f32::MAX] {
735            assert!(!box_triangle(&b([0.6, 0.6, -big], [big, big, big]), T));
736            assert!(!box_triangle(&b([0.6, 0.6, -1.0], [big, big, 1.0]), T));
737            // The overlapping counterparts.
738            assert!(box_triangle(&b([0.2, 0.2, -big], [big, big, big]), T));
739            assert!(box_triangle(&b([0.2, 0.2, -1.0], [big, big, 1.0]), T));
740            assert!(box_triangle(&b([-big; 3], [big; 3]), T));
741        }
742        // Half-infinite on single axes.
743        assert!(!box_triangle(&b([0.6, 0.6, -inf], [inf, inf, inf]), T));
744        assert!(box_triangle(&b([-inf, -inf, -inf], [0.1, 0.1, inf]), T));
745        assert!(!box_triangle(&b([-inf, -inf, 0.5], [inf, inf, inf]), T));
746        assert!(!box_triangle(&b([-inf, -inf, -inf], [inf, inf, -0.5]), T));
747        assert!(box_triangle(&b([-inf, -inf, 0.0], [inf, inf, 0.0]), T));
748        // Touching the triangle's bounds at a single point counts.
749        assert!(box_triangle(&b([1.0, 0.0, 0.0], [inf, inf, inf]), T));
750        assert!(!box_triangle(&b([1.0001, 0.0, 0.0], [inf, inf, inf]), T));
751    }
752
753    /// Whether the triangle meets the closed box, by clipping the triangle
754    /// against the six half-spaces (Sutherland-Hodgman) in `f64`: they meet iff
755    /// something is left.
756    fn clip_oracle(p: [[f64; 3]; 3], min: [f64; 3], max: [f64; 3]) -> bool {
757        let mut poly: Vec<[f64; 3]> = p.to_vec();
758        for k in 0..3 {
759            for upper in [false, true] {
760                let dist = |v: &[f64; 3]| if upper { max[k] - v[k] } else { v[k] - min[k] };
761                let input = std::mem::take(&mut poly);
762                for i in 0..input.len() {
763                    let (a, b) = (input[i], input[(i + 1) % input.len()]);
764                    let (da, db) = (dist(&a), dist(&b));
765                    if da >= 0.0 {
766                        poly.push(a);
767                    }
768                    if (da >= 0.0) != (db >= 0.0) {
769                        let s = da / (da - db);
770                        poly.push(std::array::from_fn(|j| a[j] + (b[j] - a[j]) * s));
771                    }
772                }
773                if poly.is_empty() {
774                    return false;
775                }
776            }
777        }
778        true
779    }
780
781    #[test]
782    fn box_triangle_matches_the_clipping_oracle() {
783        use crate::test_util::Lcg;
784        let mut rng = Lcg(0xb0c5);
785        let (mut yes, mut no, mut skipped) = (0, 0, 0);
786        for _ in 0..4000 {
787            let p: [[f32; 3]; 3] =
788                std::array::from_fn(|_| std::array::from_fn(|_| rng.next() * 2.0 - 1.0));
789            let mut min = [0.0f32; 3];
790            let mut max = [0.0f32; 3];
791            for k in 0..3 {
792                let c = rng.next() * 3.0 - 1.5;
793                let h = 0.05 + rng.next() * 0.8;
794                min[k] = c - h;
795                max[k] = c + h;
796                // Now and then, an unbounded side.
797                match (rng.next() * 12.0) as u32 {
798                    0 => min[k] = f32::NEG_INFINITY,
799                    1 => max[k] = f32::INFINITY,
800                    _ => {}
801                }
802            }
803            let pd = p.map(|v| v.map(f64::from));
804            let widen = |eps: f64| {
805                clip_oracle(
806                    pd,
807                    min.map(|v| f64::from(v) - eps),
808                    max.map(|v| f64::from(v) + eps),
809                )
810            };
811            let (loose, tight) = (widen(1e-9), widen(-1e-9));
812            if loose != tight {
813                skipped += 1;
814                continue;
815            }
816            assert_eq!(
817                box_triangle(&Aabb::new(min, max), p),
818                loose,
819                "triangle {p:?}, box {min:?}..{max:?}"
820            );
821            if loose {
822                yes += 1;
823            } else {
824                no += 1;
825            }
826        }
827        assert!(
828            yes > 500 && no > 500,
829            "yes {yes}, no {no}, skipped {skipped}"
830        );
831        assert!(skipped < 100, "skipped {skipped}");
832    }
833
834    #[test]
835    fn sphere_triangle_cases() {
836        assert!(sphere_triangle([0.25, 0.25, 1.0], 1.0, T));
837        assert!(!sphere_triangle([0.25, 0.25, 1.0], 0.999, T));
838    }
839
840    #[test]
841    fn f64_matches_f32_on_simple_cases() {
842        let t: [[f64; 3]; 3] = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
843        for (q, feature, point, bary) in VORONOI {
844            let c = closest_on_triangle(q, t);
845            assert_eq!(c.feature, feature, "{q:?}");
846            assert_eq!(c.point, point, "{q:?}");
847            assert_eq!(c.barycentric, bary, "{q:?}");
848        }
849        assert_eq!(closest_on_triangle([0.25, 0.25, 3.0], t).dist2, 9.0);
850    }
851
852    #[test]
853    fn ordinary_triangle_keeps_interior_result() {
854        let p: [[f32; 3]; 3] = [[0.0, 0.0, 0.0], [4.0, 0.0, 0.0], [1.0, 3.0, 1.0]];
855        let q = [1.5, 1.0, 5.0];
856        let c = closest_on_triangle(q, p);
857        assert_eq!(c.feature, Feature::Face);
858        let w = c.barycentric;
859        for (k, &x) in c.point.iter().enumerate() {
860            let m = w[0] * p[0][k] + w[1] * p[1][k] + w[2] * p[2][k];
861            assert!((m - x).abs() < 1e-5);
862        }
863        // The point is the foot of the perpendicular from q.
864        let d = sub(q, c.point);
865        assert!(dot(d, sub(p[1], p[0])).abs() < 1e-4);
866        assert!(dot(d, sub(p[2], p[0])).abs() < 1e-4);
867    }
868}