Skip to main content

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}