axiolid_measure/
proximity.rs

1//! Deterministic metric proximity queries for 3D primitives.
2//!
3//! These routines construct floating-point witnesses only. Certified topological
4//! classification belongs in `axiolid-reference`; callers that need an exact
5//! contact decision must perform that classification separately.
6
7use core::fmt;
8
9use axiolid_core::Point3;
10
11/// Two witnesses, ordered to match the first and second inputs.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct ClosestPoints3 {
14    /// Witness on the first input.
15    pub point_a: Point3,
16    /// Witness on the second input.
17    pub point_b: Point3,
18    /// Squared Euclidean separation of the witnesses.
19    pub distance_squared: f64,
20}
21
22impl ClosestPoints3 {
23    fn new(point_a: Point3, point_b: Point3) -> Self {
24        Self {
25            point_a,
26            point_b,
27            distance_squared: (point_a - point_b).length_squared(),
28        }
29    }
30}
31
32/// Metric-input failure for primitive proximity queries.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ProximityError {
35    /// At least one coordinate is NaN or infinite.
36    NonFiniteInput,
37    /// A triangle's three vertices are collinear or repeated.
38    DegenerateTriangle,
39}
40
41impl fmt::Display for ProximityError {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::NonFiniteInput => formatter.write_str("proximity input must be finite"),
45            Self::DegenerateTriangle => formatter.write_str("triangle has zero area"),
46        }
47    }
48}
49
50impl std::error::Error for ProximityError {}
51
52/// Closest witnesses on two finite segments.
53///
54/// Zero-length segments are intentionally treated as points. Equal-distance
55/// solutions preserve the first endpoint of each input segment.
56pub fn closest_points_on_segments(
57    first: [Point3; 2],
58    second: [Point3; 2],
59) -> Result<ClosestPoints3, ProximityError> {
60    if !first.into_iter().chain(second).all(Point3::is_finite) {
61        return Err(ProximityError::NonFiniteInput);
62    }
63
64    let [p1, q1] = first;
65    let [p2, q2] = second;
66    let d1 = q1 - p1;
67    let d2 = q2 - p2;
68    let r = p1 - p2;
69    let first_length_squared = d1.dot(d1);
70    let second_length_squared = d2.dot(d2);
71    let second_projection = d2.dot(r);
72
73    let (mut first_parameter, mut second_parameter);
74    if first_length_squared == 0.0 && second_length_squared == 0.0 {
75        return Ok(ClosestPoints3::new(p1, p2));
76    }
77    if first_length_squared == 0.0 {
78        first_parameter = 0.0;
79        second_parameter = (second_projection / second_length_squared).clamp(0.0, 1.0);
80    } else {
81        let first_projection = d1.dot(r);
82        if second_length_squared == 0.0 {
83            second_parameter = 0.0;
84            first_parameter = (-first_projection / first_length_squared).clamp(0.0, 1.0);
85        } else {
86            let directions_dot = d1.dot(d2);
87            let denominator =
88                first_length_squared * second_length_squared - directions_dot * directions_dot;
89            first_parameter = if denominator != 0.0 {
90                ((directions_dot * second_projection - first_projection * second_length_squared)
91                    / denominator)
92                    .clamp(0.0, 1.0)
93            } else {
94                0.0
95            };
96            second_parameter =
97                (directions_dot * first_parameter + second_projection) / second_length_squared;
98            if second_parameter < 0.0 {
99                second_parameter = 0.0;
100                first_parameter = (-first_projection / first_length_squared).clamp(0.0, 1.0);
101            } else if second_parameter > 1.0 {
102                second_parameter = 1.0;
103                first_parameter =
104                    ((directions_dot - first_projection) / first_length_squared).clamp(0.0, 1.0);
105            }
106        }
107    }
108
109    Ok(ClosestPoints3::new(
110        p1 + d1 * first_parameter,
111        p2 + d2 * second_parameter,
112    ))
113}
114
115/// Closest point on a non-degenerate triangle.
116pub fn closest_point_on_triangle(
117    point: Point3,
118    triangle: [Point3; 3],
119) -> Result<Point3, ProximityError> {
120    validate_triangle(triangle)?;
121    if !point.is_finite() {
122        return Err(ProximityError::NonFiniteInput);
123    }
124    Ok(closest_point_on_valid_triangle(point, triangle))
125}
126
127/// Deterministic metric witnesses on two non-degenerate triangles.
128///
129/// Candidate ties retain this stable order: first-triangle vertices,
130/// second-triangle vertices, then first/second edge pairs lexicographically.
131/// This function does not certify intersection; pair it with scalar predicates
132/// when zero separation changes control flow.
133pub fn closest_points_on_triangles(
134    first: [Point3; 3],
135    second: [Point3; 3],
136) -> Result<ClosestPoints3, ProximityError> {
137    validate_triangle(first)?;
138    validate_triangle(second)?;
139
140    let mut best = ClosestPoints3 {
141        point_a: Point3::ZERO,
142        point_b: Point3::ZERO,
143        distance_squared: f64::INFINITY,
144    };
145    for point in first {
146        update_best(
147            &mut best,
148            ClosestPoints3::new(point, closest_point_on_valid_triangle(point, second)),
149        );
150    }
151    for point in second {
152        update_best(
153            &mut best,
154            ClosestPoints3::new(closest_point_on_valid_triangle(point, first), point),
155        );
156    }
157    for first_edge in triangle_edges(first) {
158        for second_edge in triangle_edges(second) {
159            update_best(
160                &mut best,
161                closest_points_on_segments(first_edge, second_edge)?,
162            );
163        }
164    }
165    // Transverse crossings are realised where an EDGE passes THROUGH a FACE:
166    // no vertex is involved and no two edges approach, so the families above
167    // all miss it and the pair would report its nearest non-intersecting
168    // feature instead of zero. Checked last so it cannot disturb the tie order
169    // of the metric candidates; it only ever lowers the result to exactly zero.
170    if let Some(point) = crossing_point(first, second) {
171        update_best(
172            &mut best,
173            ClosestPoints3 {
174                point_a: point,
175                point_b: point,
176                distance_squared: 0.0,
177            },
178        );
179    }
180    Ok(best)
181}
182
183/// Where an edge of either triangle pierces the other's face, if anywhere.
184///
185/// Reports the intersection as a single coincident witness pair: the surfaces
186/// meet there, so both witnesses are the same point and the separation is
187/// exactly zero rather than a rounded near-zero.
188fn crossing_point(first: [Point3; 3], second: [Point3; 3]) -> Option<Point3> {
189    for edge in triangle_edges(first) {
190        if let Some(point) = segment_triangle_crossing(edge, second) {
191            return Some(point);
192        }
193    }
194    for edge in triangle_edges(second) {
195        if let Some(point) = segment_triangle_crossing(edge, first) {
196            return Some(point);
197        }
198    }
199    None
200}
201
202/// Möller–Trumbore segment/triangle intersection.
203///
204/// The determinant threshold is scaled by the operands' own magnitudes so the
205/// parallel test means the same thing for a millimetre model and a metre one;
206/// a fixed epsilon would be wrong in one of them.
207fn segment_triangle_crossing(segment: [Point3; 2], triangle: [Point3; 3]) -> Option<Point3> {
208    let [start, end] = segment;
209    let direction = end - start;
210    let edge1 = triangle[1] - triangle[0];
211    let edge2 = triangle[2] - triangle[0];
212    let cross = direction.cross(edge2);
213    let determinant = edge1.dot(cross);
214
215    let scale = direction.length() * edge1.length() * edge2.length();
216    if determinant.abs() <= scale * f64::EPSILON {
217        // Parallel: any contact is coplanar, which the edge/edge family
218        // already reports.
219        return None;
220    }
221
222    let inverse = 1.0 / determinant;
223    let to_start = start - triangle[0];
224    let u = to_start.dot(cross) * inverse;
225    if !(0.0..=1.0).contains(&u) {
226        return None;
227    }
228    let q = to_start.cross(edge1);
229    let v = direction.dot(q) * inverse;
230    if v < 0.0 || u + v > 1.0 {
231        return None;
232    }
233    let t = edge2.dot(q) * inverse;
234    (0.0..=1.0).contains(&t).then(|| start + direction * t)
235}
236
237fn validate_triangle(triangle: [Point3; 3]) -> Result<(), ProximityError> {
238    if !triangle.into_iter().all(Point3::is_finite) {
239        return Err(ProximityError::NonFiniteInput);
240    }
241    let [a, b, c] = triangle;
242    ((b - a).cross(c - a).length_squared() != 0.0)
243        .then_some(())
244        .ok_or(ProximityError::DegenerateTriangle)
245}
246
247fn update_best(best: &mut ClosestPoints3, candidate: ClosestPoints3) {
248    if candidate.distance_squared < best.distance_squared {
249        *best = candidate;
250    }
251}
252
253fn triangle_edges(triangle: [Point3; 3]) -> [[Point3; 2]; 3] {
254    [
255        [triangle[0], triangle[1]],
256        [triangle[1], triangle[2]],
257        [triangle[2], triangle[0]],
258    ]
259}
260
261fn closest_point_on_valid_triangle(point: Point3, triangle: [Point3; 3]) -> Point3 {
262    let [a, b, c] = triangle;
263    let ab = b - a;
264    let ac = c - a;
265    let ap = point - a;
266    let dot_ab_ap = ab.dot(ap);
267    let dot_ac_ap = ac.dot(ap);
268    if dot_ab_ap <= 0.0 && dot_ac_ap <= 0.0 {
269        return a;
270    }
271
272    let bp = point - b;
273    let dot_ab_bp = ab.dot(bp);
274    let dot_ac_bp = ac.dot(bp);
275    if dot_ab_bp >= 0.0 && dot_ac_bp <= dot_ab_bp {
276        return b;
277    }
278
279    let determinant_c = dot_ab_ap * dot_ac_bp - dot_ab_bp * dot_ac_ap;
280    if determinant_c <= 0.0 && dot_ab_ap >= 0.0 && dot_ab_bp <= 0.0 {
281        return a + ab * (dot_ab_ap / (dot_ab_ap - dot_ab_bp));
282    }
283
284    let cp = point - c;
285    let dot_ab_cp = ab.dot(cp);
286    let dot_ac_cp = ac.dot(cp);
287    if dot_ac_cp >= 0.0 && dot_ab_cp <= dot_ac_cp {
288        return c;
289    }
290
291    let determinant_b = dot_ab_cp * dot_ac_ap - dot_ab_ap * dot_ac_cp;
292    if determinant_b <= 0.0 && dot_ac_ap >= 0.0 && dot_ac_cp <= 0.0 {
293        return a + ac * (dot_ac_ap / (dot_ac_ap - dot_ac_cp));
294    }
295
296    let determinant_a = dot_ab_bp * dot_ac_cp - dot_ab_cp * dot_ac_bp;
297    if determinant_a <= 0.0 && dot_ac_bp - dot_ab_bp >= 0.0 && dot_ab_cp - dot_ac_cp >= 0.0 {
298        let edge_parameter =
299            (dot_ac_bp - dot_ab_bp) / ((dot_ac_bp - dot_ab_bp) + (dot_ab_cp - dot_ac_cp));
300        return b + (c - b) * edge_parameter;
301    }
302
303    let denominator = 1.0 / (determinant_a + determinant_b + determinant_c);
304    a + ab * (determinant_b * denominator) + ac * (determinant_c * denominator)
305}