geometry_overlay/traverse/state.rs
1//! OVL3.T1 / OVL3.T2 — the traversal state machine and forward walk.
2//!
3//! Mirrors `boost/geometry/algorithms/detail/overlay/traverse.hpp`
4//! (`traverse::apply`). Given the enriched rings and the requested
5//! overlay operation, walks the turn graph and emits output rings.
6//!
7//! # The walk
8//!
9//! For the areal intersection / union of two simple polygons whose
10//! boundaries cross transversally, the crossing turns alternate between
11//! *entries* (the boundary of A enters B) and *exits*. The Weiler–
12//! Atherton walk is:
13//!
14//! 1. pick an unvisited crossing turn,
15//! 2. follow the current ring forward, appending nodes, until the next
16//! crossing turn,
17//! 3. at that turn **switch** to the other ring,
18//! 4. stop when the walk returns to the start turn — one output ring is
19//! complete,
20//! 5. repeat from (1) until no unvisited crossing turn remains.
21//!
22//! The direction chosen at the *start* turn selects intersection vs
23//! union: intersection follows the arc that dives into the other
24//! polygon, union the arc that stays outside. Both are realised by
25//! choosing which ring to begin the walk on, from the turn's
26//! [`OperationType`](crate::turn::OperationType).
27
28use alloc::vec::Vec;
29
30use geometry_coords::CoordinateScalar;
31use geometry_model::Ring;
32use geometry_trait::{Point, PointMut};
33
34use super::enrich::{EnrichedRings, Node};
35use crate::turn::info::Turn;
36
37/// Which boolean overlay to traverse for.
38///
39/// Mirrors Boost's `overlay_type` / `operation_type` steering
40/// (`overlay_type.hpp`).
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum OverlayOp {
43 /// Keep the region inside both inputs.
44 Intersection,
45 /// Keep the region inside either input.
46 Union,
47 /// Keep the region inside the first input but outside the second
48 /// (`A − B`). The walk follows `A` forward along its arcs that lie
49 /// outside `B`, and follows `B` **backward** along the arcs that
50 /// bound the removed overlap.
51 Difference,
52}
53
54/// A traversal that could not be completed with the v1 non-degenerate
55/// walk.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum TraversalError {
58 /// The turn graph hit a case the clean areal walk does not handle
59 /// (clustered turns, self-intersections, or a walk that failed to
60 /// close). These are the deferred Boost sub-problems — see the
61 /// module docs.
62 Unsupported,
63}
64
65/// Walk the enriched turn graph and emit the output rings for `op`.
66///
67/// Returns one [`Ring`] per closed output loop. An empty result means
68/// the inputs do not overlap (for intersection) or the walk produced no
69/// bounded region.
70///
71/// Mirrors `traverse::apply` (`traverse.hpp`). v1 scope: simple areal
72/// inputs with transversal crossings; anything else yields
73/// [`TraversalError::Unsupported`] rather than a wrong ring.
74///
75/// # Errors
76///
77/// Returns [`TraversalError::Unsupported`] when the turn graph hits a
78/// degenerate case the clean areal walk does not handle — clustered
79/// turns, self-intersections, collinear shared edges, or a walk that
80/// fails to close. These are the deferred Boost sub-problems.
81///
82/// # Examples
83///
84/// ```
85/// use geometry_cs::Cartesian;
86/// use geometry_model::{Point2D, Ring};
87/// use geometry_overlay::turn::{get_turns_ring_ring, RingKind};
88/// use geometry_overlay::traverse::{enrich, traverse, OverlayOp};
89/// use geometry_trait::Ring as _;
90///
91/// type P = Point2D<f64, Cartesian>;
92/// let a: Ring<P> = Ring::from_vec(vec![
93/// P::new(0.0, 0.0), P::new(2.0, 0.0), P::new(2.0, 2.0), P::new(0.0, 2.0), P::new(0.0, 0.0),
94/// ]);
95/// let b: Ring<P> = Ring::from_vec(vec![
96/// P::new(1.0, 1.0), P::new(3.0, 1.0), P::new(3.0, 3.0), P::new(1.0, 3.0), P::new(1.0, 1.0),
97/// ]);
98/// let turns = get_turns_ring_ring(&a, 0, RingKind::Exterior, &b, 1, RingKind::Exterior);
99/// let enriched = enrich(&a, &b, &turns);
100/// let rings = traverse(&enriched, &turns, OverlayOp::Intersection).unwrap();
101/// // The intersection of the two offset squares is a single square.
102/// assert_eq!(rings.len(), 1);
103/// assert_eq!(rings[0].points().count(), 5); // 4 corners + closing point
104/// ```
105pub fn traverse<P>(
106 enriched: &EnrichedRings<P>,
107 turns: &[Turn<P>],
108 op: OverlayOp,
109) -> Result<Vec<Ring<P>>, TraversalError>
110where
111 P: PointMut + Default + Copy,
112 P::Scalar: CoordinateScalar,
113{
114 let mut visited = alloc::vec![false; turns.len()];
115 let mut rings = Vec::new();
116
117 for start_turn in 0..turns.len() {
118 if visited[start_turn] {
119 continue;
120 }
121 match walk_from(enriched, start_turn, op, &mut visited) {
122 Some(ring) => rings.push(ring),
123 None => return Err(TraversalError::Unsupported),
124 }
125 }
126 Ok(rings)
127}
128
129/// Walk one output ring starting at `start_turn`. Returns `None` if the
130/// walk fails to close within the node budget or hits a configuration
131/// the v1 clean areal walk does not support.
132///
133/// The walk is a directed Weiler–Atherton trace. State is a
134/// `(ring, position, direction)` triple; between two consecutive nodes
135/// the walk moves along a directed segment. At each **turn** it may
136/// switch to the other ring and reverse direction; it picks the outgoing
137/// segment whose midpoint lies in the target region — *inside* the other
138/// polygon for [`OverlayOp::Intersection`], *outside* it for
139/// [`OverlayOp::Union`] / [`OverlayOp::Difference`] — and that is not the
140/// reverse of the segment it arrived on. Plain vertices are passed
141/// straight through. The walk ends when it returns to `start_turn`.
142///
143/// This selection-by-region rule is what makes a region with more than
144/// two crossings (e.g. the hexagonal overlap of two triangles) trace as
145/// a single ring instead of fragmenting: at every crossing the walk
146/// commits to the edge that keeps it inside the wanted region, rather
147/// than switching blindly.
148fn walk_from<P>(
149 enriched: &EnrichedRings<P>,
150 start_turn: usize,
151 op: OverlayOp,
152 visited: &mut [bool],
153) -> Option<Ring<P>>
154where
155 P: PointMut + Default + Copy,
156 P::Scalar: CoordinateScalar,
157{
158 // Seed the walk: pick the (ring, direction) whose first segment out
159 // of the start turn heads into the target region.
160 let (mut ring_index, mut dir) = choose_start_step(enriched, start_turn, op)?;
161 let mut pos = enriched.locate_turn(ring_index, start_turn)?;
162
163 let mut out: Vec<P> = Vec::new();
164 let total_nodes: usize = enriched.rings[0].len() + enriched.rings[1].len();
165 // A correct walk visits each directed segment at most once; give a
166 // generous cap so a stuck walk terminates as unsupported.
167 let budget = 2 * total_nodes + 2;
168
169 let mut closed = false;
170 for step in 0..budget {
171 let node = &enriched.rings[ring_index][pos];
172 out.push(*node.point());
173
174 // At a turn (after the start), decide the outgoing segment.
175 if let Some(turn_id) = node.turn_id() {
176 visited[turn_id] = true;
177 if step != 0 {
178 let (nr, np, nd) = step_at_turn(enriched, ring_index, pos, dir, op)?;
179 ring_index = nr;
180 pos = np;
181 dir = nd;
182 }
183 }
184 // Advance one node in the current direction.
185 pos = step_index(pos, dir, enriched.rings[ring_index].len());
186
187 // Closed once the next node to append is the start turn again.
188 if enriched.rings[ring_index][pos].turn_id() == Some(start_turn) {
189 closed = true;
190 break;
191 }
192 }
193
194 // Honour the contract: a walk that did not actually close is an
195 // unsupported degenerate case, not a garbage ring.
196 if !closed || out.len() < 3 {
197 return None;
198 }
199 // Close the ring by repeating the first point (model convention).
200 out.push(out[0]);
201 let ring: Ring<P> = Ring::from_vec(out);
202 Some(ring)
203}
204
205/// Advance one node index in `dir` (`+1` forward, `-1` backward),
206/// wrapping cyclically over a ring of length `len`.
207fn step_index(pos: usize, dir: i8, len: usize) -> usize {
208 if dir >= 0 {
209 (pos + 1) % len
210 } else {
211 (pos + len - 1) % len
212 }
213}
214
215/// At a turn reached on `ring_index` at `pos` having travelled in `dir`,
216/// choose the outgoing `(ring, position, direction)`.
217///
218/// Considers switching to the other ring in either direction (and, as a
219/// fallback, continuing on the same ring). Picks the candidate whose
220/// segment midpoint lies in the target region and that is not the reverse
221/// of the arriving segment. Returns `None` if no candidate qualifies —
222/// an unsupported degenerate turn.
223fn step_at_turn<P>(
224 enriched: &EnrichedRings<P>,
225 ring_index: usize,
226 pos: usize,
227 dir: i8,
228 op: OverlayOp,
229) -> Option<(usize, usize, i8)>
230where
231 P: PointMut + Default + Copy,
232 P::Scalar: CoordinateScalar,
233{
234 let turn_id = enriched.rings[ring_index][pos].turn_id()?;
235 let other = 1 - ring_index;
236 let other_pos = enriched.locate_turn(other, turn_id)?;
237 let here = *enriched.rings[ring_index][pos].point();
238
239 // Candidate outgoing segments, preferring a switch to the other ring.
240 let candidates = [
241 (other, other_pos, 1i8),
242 (other, other_pos, -1i8),
243 (ring_index, pos, dir),
244 ];
245 for &(r, p, d) in &candidates {
246 let next = step_index(p, d, enriched.rings[r].len());
247 let there = *enriched.rings[r][next].point();
248 let mid = midpoint::<P>(&here, &there);
249 if segment_in_region(enriched, r, &mid, op) {
250 return Some((r, p, d));
251 }
252 }
253 None
254}
255
256/// Whether a segment lying on ring `r`, sampled at `mid`, belongs to the
257/// boundary of the target-op output region.
258///
259/// * **Intersection** — the segment must run *inside* the other input.
260/// * **Union** — *outside* the other input.
261/// * **Difference** (`A − B`, ring 0 = A, ring 1 = B) — an A-segment must
262/// run *outside* B, and a B-segment *inside* A. That asymmetry is what
263/// selects `A`'s outer arcs together with `B`'s arcs that cut into `A`.
264fn segment_in_region<P>(enriched: &EnrichedRings<P>, r: usize, mid: &P, op: OverlayOp) -> bool
265where
266 P: Point,
267 P::Scalar: CoordinateScalar,
268{
269 let inside_other = point_in_ring(mid, &enriched.rings[1 - r]);
270 match op {
271 OverlayOp::Intersection => inside_other,
272 OverlayOp::Union => !inside_other,
273 OverlayOp::Difference => {
274 if r == 0 {
275 // A-segment: keep it where it is outside B.
276 !inside_other
277 } else {
278 // B-segment: keep it where it is inside A.
279 inside_other
280 }
281 }
282 }
283}
284
285/// Midpoint of two points.
286fn midpoint<P>(a: &P, b: &P) -> P
287where
288 P: PointMut + Default,
289 P::Scalar: CoordinateScalar,
290{
291 let two = P::Scalar::ONE + P::Scalar::ONE;
292 let mut m = P::default();
293 m.set::<0>((a.get::<0>() + b.get::<0>()) / two);
294 m.set::<1>((a.get::<1>() + b.get::<1>()) / two);
295 m
296}
297
298/// Choose the starting `(ring, direction)` whose first segment out of the
299/// start turn heads into the target region.
300fn choose_start_step<P>(
301 enriched: &EnrichedRings<P>,
302 start_turn: usize,
303 op: OverlayOp,
304) -> Option<(usize, i8)>
305where
306 P: PointMut + Default + Copy,
307 P::Scalar: CoordinateScalar,
308{
309 for ring_index in [0usize, 1] {
310 let at = enriched.locate_turn(ring_index, start_turn)?;
311 let here = *enriched.rings[ring_index][at].point();
312 for dir in [1i8, -1] {
313 let next = step_index(at, dir, enriched.rings[ring_index].len());
314 let there = *enriched.rings[ring_index][next].point();
315 let mid = midpoint::<P>(&here, &there);
316 if segment_in_region(enriched, ring_index, &mid, op) {
317 return Some((ring_index, dir));
318 }
319 }
320 }
321 None
322}
323
324/// Point-in-ring test by ray casting over the enriched ring's node
325/// points (odd crossings ⇒ inside). Boundary-exact cases are not
326/// expected here because the probe is a segment *midpoint*.
327fn point_in_ring<P>(p: &P, ring: &[Node<P>]) -> bool
328where
329 P: Point,
330 P::Scalar: CoordinateScalar,
331{
332 let px = p.get::<0>();
333 let py = p.get::<1>();
334 let pts: Vec<&P> = ring.iter().map(Node::point).collect();
335 let n = pts.len();
336 let mut inside = false;
337 let mut j = n - 1;
338 for i in 0..n {
339 let xi = pts[i].get::<0>();
340 let yi = pts[i].get::<1>();
341 let xj = pts[j].get::<0>();
342 let yj = pts[j].get::<1>();
343 let crosses = (yi > py) != (yj > py);
344 if crosses {
345 // x-coordinate of the edge at height py.
346 let t = (py - yi) / (yj - yi);
347 let x_at = xi + t * (xj - xi);
348 if px < x_at {
349 inside = !inside;
350 }
351 }
352 j = i;
353 }
354 inside
355}
356
357#[cfg(test)]
358mod tests {
359 use super::{OverlayOp, traverse};
360 use crate::traverse::enrich::enrich;
361 use crate::turn::{RingKind, get_turns_ring_ring};
362 use geometry_cs::Cartesian;
363 use geometry_model::{Point2D, Ring};
364 use geometry_trait::{Point as _, Ring as _};
365
366 type P = Point2D<f64, Cartesian>;
367
368 fn square(x: f64, y: f64, s: f64) -> Ring<P> {
369 Ring::from_vec(vec![
370 P::new(x, y),
371 P::new(x + s, y),
372 P::new(x + s, y + s),
373 P::new(x, y + s),
374 P::new(x, y),
375 ])
376 }
377
378 #[test]
379 fn intersection_of_offset_squares_is_one_ring() {
380 let a = square(0.0, 0.0, 2.0);
381 let b = square(1.0, 1.0, 2.0);
382 let turns = get_turns_ring_ring(&a, 0, RingKind::Exterior, &b, 1, RingKind::Exterior);
383 let e = enrich(&a, &b, &turns);
384 let rings = traverse(&e, &turns, OverlayOp::Intersection).unwrap();
385 assert_eq!(rings.len(), 1);
386 // The overlap of [0,2]² and [1,3]² is the unit square [1,2]².
387 let ring = &rings[0];
388 let pts: Vec<(f64, f64)> = ring
389 .points()
390 .map(|p| (p.get::<0>(), p.get::<1>()))
391 .collect();
392 // Every vertex is a corner of [1,2]².
393 for (x, y) in &pts {
394 assert!((*x - 1.0).abs() < 1e-9 || (*x - 2.0).abs() < 1e-9, "x={x}");
395 assert!((*y - 1.0).abs() < 1e-9 || (*y - 2.0).abs() < 1e-9, "y={y}");
396 }
397 // 4 distinct corners + closing repeat.
398 assert_eq!(ring.points().count(), 5);
399 }
400}