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}