axiolid_inspect/
cast.rs

1//! First surface hit along a ray.
2//!
3//! Which triangle is hit is decided by the same exact `orient3d` parity the
4//! containment query uses, so a caller cannot get "the ray hits nothing"
5//! from one and "inside" from the other. Only the hit PARAMETER is computed
6//! in f64, and it is a reported measurement rather than a decision input
7//! (ADR 0045).
8
9use crate::containment::ray_hits_triangle;
10use axiolid_core::{Point3, Vec3};
11use axiolid_mesh::TriMesh;
12
13/// Where a ray first meets a mesh.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct Hit {
16    /// Index of the triangle struck.
17    pub triangle: usize,
18    /// Ray parameter of the hit: `origin + direction * t`.
19    pub t: f64,
20    /// The hit position.
21    pub point: Point3,
22}
23
24/// First triangle the ray from `origin` along `direction` strikes.
25///
26/// Returns `None` when the ray misses the mesh entirely, or when it meets an
27/// edge or vertex exactly -- the degenerate case the exact test refuses to
28/// resolve arbitrarily rather than picking a neighbour at random.
29pub fn ray_cast(mesh: &TriMesh, origin: Point3, direction: Vec3) -> Option<Hit> {
30    if direction.length_squared() == 0.0 {
31        return None;
32    }
33    let mut nearest: Option<Hit> = None;
34    for index in 0..mesh.indices.len() / 3 {
35        let Some(triangle) = crate::clearance::triangle_at(mesh, index) else {
36            continue;
37        };
38        // Membership is exact; only the distance along the ray is f64.
39        if !ray_hits_triangle(triangle, origin, direction)? {
40            continue;
41        }
42        let Some(t) = ray_parameter(triangle, origin, direction) else {
43            continue;
44        };
45        if t < 0.0 {
46            continue;
47        }
48        if nearest.is_none_or(|hit| t < hit.t) {
49            nearest = Some(Hit {
50                triangle: index,
51                t,
52                point: origin + direction * t,
53            });
54        }
55    }
56    nearest
57}
58
59/// Ray parameter where the triangle's plane is met.
60///
61/// Membership has already been decided exactly by the caller; this only
62/// measures how far along. `None` when the ray runs parallel to the plane.
63fn ray_parameter(triangle: [Point3; 3], origin: Point3, direction: Vec3) -> Option<f64> {
64    let [a, b, c] = triangle;
65    let normal = (b - a).cross(c - a);
66    let denominator = normal.dot(direction);
67    if denominator == 0.0 {
68        return None;
69    }
70    let t = normal.dot(a - origin) / denominator;
71    t.is_finite().then_some(t)
72}