geometry_overlay/turn/info.rs
1//! OVL2.T1 — the turn data model.
2//!
3//! Mirrors `boost/geometry/algorithms/detail/overlay/turn_info.hpp`
4//! (`turn_info` + `turn_operation`), the `operation_type` enum from
5//! `overlay_type.hpp:31-39`, the `method_type` enum from
6//! `turn_info.hpp:29-40`, and the `segment_identifier` from
7//! `segment_identifier.hpp:35`.
8//!
9//! A turn belongs to exactly two geometries — the two inputs of an
10//! overlay — so it carries **two** [`Operation`]s, one per source.
11//! Boost stores them in a fixed `operations[2]` array; the port uses
12//! `[Operation; 2]` for the same reason: the arity is a hard invariant,
13//! not a runtime-variable list.
14
15use geometry_trait::Point;
16
17/// How a turn was formed — the case the segment-intersection
18/// classification decided.
19///
20/// Mirrors `method_type` in
21/// `boost/geometry/algorithms/detail/overlay/turn_info.hpp:29-40`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
23pub enum Method {
24 /// No method assigned yet. Boost's `method_none`.
25 #[default]
26 None,
27 /// The segments do not actually meet. Boost's `method_disjoint`.
28 Disjoint,
29 /// A proper crossing — the two segments cross transversally.
30 /// Boost's `method_crosses`.
31 Crosses,
32 /// The segments touch at an endpoint of one of them, from outside.
33 /// Boost's `method_touch`.
34 Touch,
35 /// One segment's endpoint touches the *interior* of the other.
36 /// Boost's `method_touch_interior`.
37 TouchInterior,
38 /// The segments are collinear and overlap. Boost's
39 /// `method_collinear`.
40 Collinear,
41 /// The segments are equal. Boost's `method_equal`.
42 Equal,
43 /// A start turn used to seed traversal. Boost's `method_start`.
44 Start,
45 /// An inconsistent turn the classifier could not resolve. Boost's
46 /// `method_error`.
47 Error,
48}
49
50/// What a single side of a turn does for the traversal — the role this
51/// geometry's segment plays at the turn.
52///
53/// Mirrors `operation_type` in
54/// `boost/geometry/algorithms/detail/overlay/overlay_type.hpp:31-39`.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
56pub enum OperationType {
57 /// Unset. Boost's `operation_none`.
58 #[default]
59 None,
60 /// Take this direction when computing a **union**. Boost's
61 /// `operation_union`.
62 Union,
63 /// Take this direction when computing an **intersection**. Boost's
64 /// `operation_intersection`.
65 Intersection,
66 /// This direction is blocked — traversal must not leave along it.
67 /// Boost's `operation_blocked`.
68 Blocked,
69 /// Continue straight along the same ring (collinear continuation).
70 /// Boost's `operation_continue`.
71 Continue,
72 /// The two geometries run in opposite directions here. Boost's
73 /// `operation_opposite`.
74 Opposite,
75}
76
77/// Which ring of a source geometry a segment came from.
78///
79/// Boost encodes this in `segment_identifier::ring_index`
80/// (`-1` = exterior, `>= 0` = the n-th interior ring). The port makes
81/// the exterior / interior distinction an explicit enum so call sites
82/// never compare against a magic `-1`.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84pub enum RingKind {
85 /// The exterior (outer) ring. Boost's `ring_index == -1`.
86 Exterior,
87 /// The `n`-th interior ring (hole). Boost's `ring_index == n`.
88 Interior(usize),
89}
90
91/// Names the segment a turn operation sits on, within one source
92/// geometry.
93///
94/// Mirrors `segment_identifier` in
95/// `boost/geometry/algorithms/detail/overlay/segment_identifier.hpp:35`,
96/// reduced to the fields the v1 areal-areal overlay needs: which input,
97/// which ring of it, and which segment index along that ring. Boost's
98/// `multi_index` / `piece_index` are omitted until multi-geometry and
99/// buffer support need them.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub struct SegmentId {
102 /// Which input geometry (`0` or `1`). Boost's `source_index`.
103 pub source_index: usize,
104 /// Which ring of that geometry. Boost's `ring_index`.
105 pub ring: RingKind,
106 /// The index of the segment's start vertex along the ring. Boost's
107 /// `segment_index`.
108 pub segment_index: usize,
109}
110
111/// One side of a turn — the operation for one of the two source
112/// geometries.
113///
114/// Mirrors `turn_operation` in
115/// `boost/geometry/algorithms/detail/overlay/turn_info.hpp:52-58`. The
116/// `fraction` field (Boost's `segment_ratio`) is the position of the
117/// turn along its segment; the port stores it as a `[0, 1]` fraction
118/// once enrichment (OVL2.5) needs it — the data-model task keeps only
119/// the operation and the segment it belongs to.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct Operation {
122 /// The traversal role of this side. Boost's
123 /// `turn_operation::operation`.
124 pub operation: OperationType,
125 /// The segment this side sits on. Boost's `turn_operation::seg_id`.
126 pub seg_id: SegmentId,
127}
128
129impl Operation {
130 /// Construct an operation with an as-yet-unclassified role.
131 #[must_use]
132 pub const fn new(seg_id: SegmentId) -> Self {
133 Self {
134 operation: OperationType::None,
135 seg_id,
136 }
137 }
138}
139
140/// An intersection point between the two inputs, plus the metadata
141/// traversal needs.
142///
143/// Mirrors `turn_info` in
144/// `boost/geometry/algorithms/detail/overlay/turn_info.hpp:78-88`:
145/// the intersection `point`, the `method` by which it was formed, and
146/// the two `operations` (one per input geometry). `touch_only` mirrors
147/// Boost's flag for a touch where the lines do not cross.
148///
149/// # Examples
150///
151/// ```
152/// use geometry_cs::Cartesian;
153/// use geometry_model::Point2D;
154/// use geometry_overlay::turn::info::{
155/// Method, Operation, RingKind, SegmentId, Turn,
156/// };
157///
158/// type P = Point2D<f64, Cartesian>;
159/// let seg = |src, seg| SegmentId { source_index: src, ring: RingKind::Exterior, segment_index: seg };
160/// let t = Turn {
161/// point: P::new(1.0, 1.0),
162/// method: Method::Crosses,
163/// operations: [Operation::new(seg(0, 3)), Operation::new(seg(1, 5))],
164/// touch_only: false,
165/// };
166/// assert_eq!(t.method, Method::Crosses);
167/// assert_eq!(t.operations[1].seg_id.segment_index, 5);
168/// ```
169#[derive(Debug, Clone, Copy, PartialEq)]
170pub struct Turn<P: Point> {
171 /// The intersection point. Boost's `turn_info::point`.
172 pub point: P,
173 /// How the turn was formed. Boost's `turn_info::method`.
174 pub method: Method,
175 /// One operation per input geometry. Boost's
176 /// `turn_info::operations[2]`.
177 pub operations: [Operation; 2],
178 /// True for a `Touch` / `TouchInterior` turn where the two lines do
179 /// not actually cross. Boost's `turn_info::touch_only`.
180 pub touch_only: bool,
181}
182
183impl<P: Point> Turn<P> {
184 /// True when both operations carry `op`. Mirrors
185 /// `turn_info::both` (`turn_info.hpp:102-105`).
186 #[must_use]
187 pub fn both(&self, op: OperationType) -> bool {
188 self.operations[0].operation == op && self.operations[1].operation == op
189 }
190
191 /// True when either operation carries `op`. Mirrors
192 /// `turn_info::has` (`turn_info.hpp:107-111`).
193 #[must_use]
194 pub fn has(&self, op: OperationType) -> bool {
195 self.operations[0].operation == op || self.operations[1].operation == op
196 }
197
198 /// True when the turn is blocked on both sides. Mirrors
199 /// `turn_info::blocked` (`turn_info.hpp:118-121`).
200 #[must_use]
201 pub fn blocked(&self) -> bool {
202 self.both(OperationType::Blocked)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 //! Construct a turn and round-trip its fields — the OVL2.T1
209 //! done-when. Mirrors the field-access checks in
210 //! `test/algorithms/overlay/get_turn_info.cpp`.
211
212 use super::{Method, Operation, OperationType, RingKind, SegmentId, Turn};
213 use geometry_cs::Cartesian;
214 use geometry_model::Point2D;
215 use geometry_trait::Point as _;
216
217 type P = Point2D<f64, Cartesian>;
218
219 fn seg(src: usize, ring: RingKind, idx: usize) -> SegmentId {
220 SegmentId {
221 source_index: src,
222 ring,
223 segment_index: idx,
224 }
225 }
226
227 #[test]
228 #[allow(clippy::float_cmp, reason = "Turn coordinates are exact literals.")]
229 fn turn_round_trips_its_fields() {
230 let t = Turn {
231 point: P::new(2.0, 3.0),
232 method: Method::Crosses,
233 operations: [
234 Operation::new(seg(0, RingKind::Exterior, 4)),
235 Operation::new(seg(1, RingKind::Interior(2), 7)),
236 ],
237 touch_only: false,
238 };
239 assert_eq!(t.point.get::<0>(), 2.0);
240 assert_eq!(t.point.get::<1>(), 3.0);
241 assert_eq!(t.method, Method::Crosses);
242 assert_eq!(t.operations[0].seg_id.source_index, 0);
243 assert_eq!(t.operations[0].seg_id.ring, RingKind::Exterior);
244 assert_eq!(t.operations[1].seg_id.ring, RingKind::Interior(2));
245 assert_eq!(t.operations[1].seg_id.segment_index, 7);
246 assert_eq!(t.operations[0].operation, OperationType::None);
247 }
248
249 #[test]
250 fn both_has_blocked_predicates() {
251 let mut t = Turn {
252 point: P::new(0.0, 0.0),
253 method: Method::Touch,
254 operations: [
255 Operation::new(seg(0, RingKind::Exterior, 0)),
256 Operation::new(seg(1, RingKind::Exterior, 0)),
257 ],
258 touch_only: true,
259 };
260 assert!(!t.has(OperationType::Union));
261 t.operations[0].operation = OperationType::Union;
262 assert!(t.has(OperationType::Union));
263 assert!(!t.both(OperationType::Union));
264
265 t.operations[0].operation = OperationType::Blocked;
266 t.operations[1].operation = OperationType::Blocked;
267 assert!(t.blocked());
268 assert!(t.both(OperationType::Blocked));
269 }
270
271 #[test]
272 fn default_enums() {
273 assert_eq!(Method::default(), Method::None);
274 assert_eq!(OperationType::default(), OperationType::None);
275 }
276}