axiolid_reference/
segment_triangle.rs

1//! Certified segment/triangle topology classification.
2//!
3//! This module decides topology without a caller epsilon. Metric witness
4//! construction is deliberately separate: a representable closest point is not
5//! a certified proof of intersection.
6
7use axiolid_contracts::Sign;
8use axiolid_core::Point3;
9
10use crate::orient3d;
11
12/// The topological relation between a finite segment and one triangle.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum SegmentTriangleRelation {
15    /// The closed primitives have no common point.
16    Disjoint,
17    /// The segment crosses the triangle interior at one point.
18    Proper,
19    /// The primitives meet at an endpoint, edge, or vertex.
20    Touching,
21    /// Both segment endpoints lie in the triangle plane.
22    ///
23    /// This deliberately does not claim overlap: callers needing coplanar
24    /// coverage use a projected 2D predicate with an explicit policy.
25    Coplanar,
26    /// The segment endpoints are identical.
27    DegenerateSegment,
28    /// The three triangle vertices are collinear.
29    DegenerateTriangle,
30}
31
32/// Classify a segment against a triangle using certified orientation signs.
33///
34/// No tolerance is accepted: this reports the exact topology of the supplied
35/// binary64 coordinates. Nearness is a metric/policy question for the caller.
36#[must_use]
37pub fn segment_triangle_relation(
38    start: Point3,
39    end: Point3,
40    triangle: [Point3; 3],
41) -> SegmentTriangleRelation {
42    if start == end {
43        return SegmentTriangleRelation::DegenerateSegment;
44    }
45
46    let [a, b, c] = triangle;
47    if (b - a).cross(c - a).length_squared() == 0.0 {
48        return SegmentTriangleRelation::DegenerateTriangle;
49    }
50
51    let start_side = sign(orient3d(a, b, c, start));
52    let end_side = sign(orient3d(a, b, c, end));
53    if start_side == Sign::Zero && end_side == Sign::Zero {
54        return SegmentTriangleRelation::Coplanar;
55    }
56    if start_side != Sign::Zero && start_side == end_side {
57        return SegmentTriangleRelation::Disjoint;
58    }
59
60    // These three oriented tetrahedra are the barycentric side tests of the
61    // segment's plane crossing. They are exact even when the crossing point is
62    // not representable as binary64.
63    let edge_signs = [
64        sign(orient3d(start, end, a, b)),
65        sign(orient3d(start, end, b, c)),
66        sign(orient3d(start, end, c, a)),
67    ];
68    let all_nonnegative = edge_signs.iter().all(|&value| value != Sign::Negative);
69    let all_nonpositive = edge_signs.iter().all(|&value| value != Sign::Positive);
70    if !all_nonnegative && !all_nonpositive {
71        return SegmentTriangleRelation::Disjoint;
72    }
73
74    if start_side == Sign::Zero || end_side == Sign::Zero || edge_signs.contains(&Sign::Zero) {
75        SegmentTriangleRelation::Touching
76    } else {
77        SegmentTriangleRelation::Proper
78    }
79}
80
81fn sign(value: axiolid_contracts::Certified) -> Sign {
82    // `orient3d` always escalates to an exact, certain sign.
83    value
84        .sign()
85        .expect("orient3d is a total certified predicate")
86}