Skip to main content

geometry_overlay/turn/
classify.rs

1//! OVL2.T4 — turn classification.
2//!
3//! Assigns each turn its [`Method`] and sets the two [`OperationType`]s
4//! that steer traversal. Mirrors the classification in
5//! `boost/geometry/algorithms/detail/overlay/get_turn_info.hpp` and the
6//! `turn_info` method/operation assignment.
7//!
8//! The v1 classifier covers the areal-areal cases the boolean overlay
9//! needs: a proper crossing, an endpoint touch, and a collinear
10//! overlap. The richer linear/areal (`get_turn_info_la.hpp`) and
11//! self-turn cases are deferred with the rest of the linear overlay.
12//!
13//! # Operation assignment
14//!
15//! At a proper crossing the two inputs swap interior/exterior sides, so
16//! one operation is [`OperationType::Union`] and the other
17//! [`OperationType::Intersection`] — which is which is decided at
18//! traversal time from the requested overlay op, so the classifier sets
19//! the *pair* consistently and leaves the union/intersection **choice**
20//! to [`operations_for_crossing`]. This matches Boost computing the
21//! operation pair up front and letting `traverse` pick the arm.
22
23use super::info::{Method, OperationType, Turn};
24use crate::predicate::segment_intersection::SegmentIntersection;
25use geometry_trait::Point;
26
27/// The [`Method`] implied by a raw segment-intersection outcome.
28///
29/// * [`SegmentIntersection::Single`] → [`Method::Crosses`] (a proper
30///   crossing) — endpoint-touch refinement is applied separately by
31///   [`refine_touch`] once the segment endpoints are known.
32/// * [`SegmentIntersection::Collinear`] → [`Method::Collinear`].
33/// * [`SegmentIntersection::Disjoint`] / [`SegmentIntersection::OutOfRange`]
34///   → [`Method::Disjoint`] (no turn should be emitted).
35///
36/// Mirrors the top-level case split in Boost's `get_turn_info`
37/// (`get_turn_info.hpp`).
38#[must_use]
39pub fn method_of<P>(outcome: &SegmentIntersection<P>) -> Method {
40    match outcome {
41        SegmentIntersection::Single(_) => Method::Crosses,
42        SegmentIntersection::Collinear { .. } => Method::Collinear,
43        SegmentIntersection::Disjoint | SegmentIntersection::OutOfRange => Method::Disjoint,
44    }
45}
46
47/// Refine a [`Method::Crosses`] to [`Method::Touch`] when the
48/// intersection point coincides with an endpoint of either segment —
49/// the lines meet but do not transversally cross.
50///
51/// `a0`,`a1` are the endpoints of the first segment, `b0`,`b1` the
52/// second; `point` is the intersection. Mirrors the endpoint check in
53/// `get_turn_info_for_endpoint.hpp`.
54#[must_use]
55pub fn refine_touch<P>(point: &P, a0: &P, a1: &P, b0: &P, b1: &P) -> Method
56where
57    P: Point,
58    P::Scalar: PartialEq,
59{
60    let at_endpoint = eq(point, a0) || eq(point, a1) || eq(point, b0) || eq(point, b1);
61    if at_endpoint {
62        Method::Touch
63    } else {
64        Method::Crosses
65    }
66}
67
68/// The operation pair for a proper crossing: the two inputs take
69/// opposite roles. Returns `[union_side, intersection_side]` so the
70/// caller stores one per source. Mirrors the operation assignment for a
71/// crossing turn in `get_turn_info.hpp`.
72#[must_use]
73pub const fn operations_for_crossing() -> [OperationType; 2] {
74    [OperationType::Union, OperationType::Intersection]
75}
76
77/// Set a turn's method and operations from a segment-intersection
78/// outcome and the four segment endpoints.
79///
80/// This is the entry the turn collector calls per emitted turn.
81pub fn set_from_outcome<P>(
82    turn: &mut Turn<P>,
83    outcome: &SegmentIntersection<P>,
84    a0: &P,
85    a1: &P,
86    b0: &P,
87    b1: &P,
88) where
89    P: Point,
90    P::Scalar: PartialEq,
91{
92    let base = method_of(outcome);
93    turn.method = match base {
94        Method::Crosses => refine_touch(&turn.point, a0, a1, b0, b1),
95        other => other,
96    };
97    match turn.method {
98        Method::Crosses => {
99            let ops = operations_for_crossing();
100            turn.operations[0].operation = ops[0];
101            turn.operations[1].operation = ops[1];
102            turn.touch_only = false;
103        }
104        Method::Touch => {
105            // A touch that does not cross: both sides continue on their
106            // own ring, blocked from swapping.
107            turn.operations[0].operation = OperationType::Union;
108            turn.operations[1].operation = OperationType::Union;
109            turn.touch_only = true;
110        }
111        Method::Collinear => {
112            turn.operations[0].operation = OperationType::Continue;
113            turn.operations[1].operation = OperationType::Continue;
114            turn.touch_only = false;
115        }
116        _ => {}
117    }
118}
119
120fn eq<P: Point>(a: &P, b: &P) -> bool
121where
122    P::Scalar: PartialEq,
123{
124    a.get::<0>() == b.get::<0>() && a.get::<1>() == b.get::<1>()
125}
126
127#[cfg(test)]
128mod tests {
129    //! OVL2.T4 done-when: method + operation assignment for each case.
130    //! Mirrors the classification checks in
131    //! `test/algorithms/overlay/get_turn_info.cpp`.
132
133    use super::{method_of, operations_for_crossing, refine_touch, set_from_outcome};
134    use crate::predicate::segment_intersection::SegmentIntersection;
135    use crate::turn::info::{Method, Operation, OperationType, RingKind, SegmentId, Turn};
136    use geometry_cs::Cartesian;
137    use geometry_model::Point2D;
138
139    type P = Point2D<f64, Cartesian>;
140
141    fn sid(src: usize) -> SegmentId {
142        SegmentId {
143            source_index: src,
144            ring: RingKind::Exterior,
145            segment_index: 0,
146        }
147    }
148
149    fn turn_at(x: f64, y: f64) -> Turn<P> {
150        Turn {
151            point: P::new(x, y),
152            method: Method::None,
153            operations: [Operation::new(sid(0)), Operation::new(sid(1))],
154            touch_only: false,
155        }
156    }
157
158    #[test]
159    fn method_from_outcome() {
160        assert_eq!(
161            method_of(&SegmentIntersection::Single(P::new(0.0, 0.0))),
162            Method::Crosses
163        );
164        assert_eq!(
165            method_of(&SegmentIntersection::Collinear {
166                from: P::new(0.0, 0.0),
167                to: P::new(1.0, 0.0)
168            }),
169            Method::Collinear
170        );
171        assert_eq!(
172            method_of::<P>(&SegmentIntersection::Disjoint),
173            Method::Disjoint
174        );
175    }
176
177    #[test]
178    fn crossing_at_interior_is_cross() {
179        // (1,1) is interior to both diagonals.
180        let m = refine_touch(
181            &P::new(1.0, 1.0),
182            &P::new(0.0, 0.0),
183            &P::new(2.0, 2.0),
184            &P::new(0.0, 2.0),
185            &P::new(2.0, 0.0),
186        );
187        assert_eq!(m, Method::Crosses);
188    }
189
190    #[test]
191    fn crossing_at_endpoint_is_touch() {
192        // (2,0) is an endpoint of the second segment.
193        let m = refine_touch(
194            &P::new(2.0, 0.0),
195            &P::new(0.0, 0.0),
196            &P::new(4.0, 0.0),
197            &P::new(2.0, 0.0),
198            &P::new(2.0, 3.0),
199        );
200        assert_eq!(m, Method::Touch);
201    }
202
203    #[test]
204    fn crossing_sets_opposite_operations() {
205        let mut t = turn_at(1.0, 1.0);
206        set_from_outcome(
207            &mut t,
208            &SegmentIntersection::Single(P::new(1.0, 1.0)),
209            &P::new(0.0, 0.0),
210            &P::new(2.0, 2.0),
211            &P::new(0.0, 2.0),
212            &P::new(2.0, 0.0),
213        );
214        assert_eq!(t.method, Method::Crosses);
215        let ops = operations_for_crossing();
216        assert_eq!(t.operations[0].operation, ops[0]);
217        assert_eq!(t.operations[1].operation, ops[1]);
218        assert_eq!(t.operations[0].operation, OperationType::Union);
219        assert_eq!(t.operations[1].operation, OperationType::Intersection);
220    }
221
222    #[test]
223    fn collinear_sets_continue() {
224        let mut t = turn_at(2.0, 0.0);
225        set_from_outcome(
226            &mut t,
227            &SegmentIntersection::Collinear {
228                from: P::new(2.0, 0.0),
229                to: P::new(4.0, 0.0),
230            },
231            &P::new(0.0, 0.0),
232            &P::new(4.0, 0.0),
233            &P::new(2.0, 0.0),
234            &P::new(6.0, 0.0),
235        );
236        assert_eq!(t.method, Method::Collinear);
237        assert_eq!(t.operations[0].operation, OperationType::Continue);
238    }
239}