Skip to main content

geometry_overlay/operation/
boolean.rs

1//! OVL5 — the boolean overlay free functions.
2//!
3//! The public entries route through a planar split-edge arrangement that
4//! performs turn collection, colocation handling, boundary classification,
5//! traversal, and [`assemble`](mod@crate::assemble). Mirrors
6//! `boost/geometry/algorithms/intersection.hpp`, `union.hpp`,
7//! `difference.hpp`, and `sym_difference.hpp`.
8//!
9//! # Where these live
10//!
11//! The overlay plan (`phase_03-…-overlay.md` §OVL5) placed these in
12//! `geometry-algorithm`. That would form a dependency cycle:
13//! `geometry-overlay` already depends on `geometry-algorithm` (for
14//! `within` / `ring_area`), so `geometry-algorithm` cannot depend back
15//! on `geometry-overlay`. The functions therefore live here in
16//! `geometry-overlay` and are re-exported by the `geometry` facade.
17//! This is the same class of spec-stub cycle already corrected
18//! elsewhere in the port.
19//!
20//! Polygon × polygon → `MultiPolygon`, including interior rings, contained
21//! holes/islands, shared edges, and colocated vertices. Coordinates outside
22//! the exact-predicate range surface as [`OverlayError::Unsupported`].
23
24use geometry_coords::CoordinateScalar;
25use geometry_cs::{CartesianFamily, CoordinateSystem};
26use geometry_model::{MultiPolygon, Polygon};
27use geometry_tag::SameAs;
28use geometry_trait::{MultiPolygon as MultiPolygonTrait, PointMut, Polygon as PolygonTrait};
29
30use crate::traverse::TraversalError;
31
32use super::areal::{ArealOp, overlay as areal_overlay, overlay_multi as areal_overlay_multi};
33
34/// Failure of a boolean overlay operation.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum OverlayError {
37    /// Coordinates exceeded the predicate range or the input could not form
38    /// a supported result boundary. Also propagated from legacy
39    /// [`TraversalError`] callers.
40    Unsupported,
41}
42
43impl From<TraversalError> for OverlayError {
44    fn from(_: TraversalError) -> Self {
45        OverlayError::Unsupported
46    }
47}
48
49/// Intersection of two polygons — the region inside **both**.
50///
51/// Mirrors `boost::geometry::intersection` from
52/// `algorithms/detail/intersection/interface.hpp:342-372`. Returns an empty
53/// `MultiPolygon` when the polygons do not overlap.
54///
55/// # Errors
56///
57/// [`OverlayError::Unsupported`] when coordinates exceed the predicate range.
58///
59/// # Examples
60///
61/// ```
62/// use geometry_cs::Cartesian;
63/// use geometry_model::{polygon, Point2D, Polygon};
64/// use geometry_overlay::operation::intersection;
65/// use geometry_trait::MultiPolygon as _;
66///
67/// type P = Point2D<f64, Cartesian>;
68/// let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
69/// let b: Polygon<P> = polygon![[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0), (1.0, 1.0)]];
70/// let out = intersection(&a, &b).unwrap();
71/// assert_eq!(out.polygons().count(), 1);
72/// ```
73#[inline]
74#[must_use = "intersection can fail and the resulting geometry should be used"]
75pub fn intersection<G1, G2, P>(g1: &G1, g2: &G2) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
76where
77    G1: PolygonTrait<Point = P>,
78    G2: PolygonTrait<Point = P>,
79    P: PointMut + Default + Copy,
80    P::Scalar: CoordinateScalar + Into<f64>,
81    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
82{
83    areal_overlay(g1, g2, ArealOp::Intersection)
84}
85
86/// Union of two polygons — the region inside **either**.
87///
88/// Mirrors `boost::geometry::union_` from `algorithms/union.hpp:851-881`; the
89/// C++ trailing underscore dodges the keyword, while this compatibility entry
90/// uses the unambiguous name `union_poly`.
91///
92/// # Errors
93///
94/// [`OverlayError::Unsupported`] when coordinates exceed the predicate range.
95///
96/// # Examples
97///
98/// ```
99/// use geometry_cs::Cartesian;
100/// use geometry_model::{polygon, Point2D, Polygon};
101/// use geometry_overlay::operation::union_poly;
102/// use geometry_trait::MultiPolygon as _;
103///
104/// type P = Point2D<f64, Cartesian>;
105/// let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
106/// let b: Polygon<P> = polygon![[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0), (1.0, 1.0)]];
107/// let out = union_poly(&a, &b).unwrap();
108/// assert_eq!(out.polygons().count(), 1);
109/// ```
110#[inline]
111#[must_use = "union can fail and the resulting geometry should be used"]
112pub fn union_poly<G1, G2, P>(g1: &G1, g2: &G2) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
113where
114    G1: PolygonTrait<Point = P>,
115    G2: PolygonTrait<Point = P>,
116    P: PointMut + Default + Copy,
117    P::Scalar: CoordinateScalar + Into<f64>,
118    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
119{
120    areal_overlay(g1, g2, ArealOp::Union)
121}
122
123/// Union of two polygons — the region inside either input.
124///
125/// This is the Boost-style public spelling of [`union_poly`]. Rust reserves
126/// `union`, so callers write the raw identifier `r#union(a, b)`; the exported
127/// symbol is still named `union`.
128///
129/// Mirrors `boost::geometry::union_` from
130/// `boost/geometry/algorithms/union.hpp:866-880`.
131///
132/// # Errors
133///
134/// Propagates [`OverlayError::Unsupported`] from [`union_poly`].
135#[inline]
136#[must_use = "union can fail and the resulting geometry should be used"]
137pub fn r#union<G1, G2, P>(g1: &G1, g2: &G2) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
138where
139    G1: PolygonTrait<Point = P>,
140    G2: PolygonTrait<Point = P>,
141    P: PointMut + Default + Copy,
142    P::Scalar: CoordinateScalar + Into<f64>,
143    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
144{
145    union_poly(g1, g2)
146}
147
148/// Difference of two polygons — the region inside the first but outside
149/// the second (`A − B`).
150///
151/// Mirrors `boost::geometry::difference` from
152/// `algorithms/difference.hpp:686-714`.
153///
154/// # Errors
155///
156/// [`OverlayError::Unsupported`] when coordinates exceed the predicate range.
157///
158/// # Examples
159///
160/// ```
161/// use geometry_cs::Cartesian;
162/// use geometry_model::{polygon, Point2D, Polygon};
163/// use geometry_overlay::operation::difference;
164/// use geometry_trait::MultiPolygon as _;
165///
166/// type P = Point2D<f64, Cartesian>;
167/// let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
168/// let b: Polygon<P> = polygon![[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0), (1.0, 1.0)]];
169/// let out = difference(&a, &b).unwrap();
170/// assert_eq!(out.polygons().count(), 1);
171/// ```
172#[inline]
173#[must_use = "difference can fail and the resulting geometry should be used"]
174pub fn difference<G1, G2, P>(g1: &G1, g2: &G2) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
175where
176    G1: PolygonTrait<Point = P>,
177    G2: PolygonTrait<Point = P>,
178    P: PointMut + Default + Copy,
179    P::Scalar: CoordinateScalar + Into<f64>,
180    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
181{
182    areal_overlay(g1, g2, ArealOp::Difference)
183}
184
185/// Symmetric difference of two polygons — the region inside exactly one
186/// of them (`(A − B) ∪ (B − A)`).
187///
188/// Mirrors `boost::geometry::sym_difference` from
189/// `algorithms/sym_difference.hpp:795-824`.
190///
191/// # Errors
192///
193/// [`OverlayError::Unsupported`] when coordinates exceed the predicate range.
194///
195/// # Examples
196///
197/// ```
198/// use geometry_cs::Cartesian;
199/// use geometry_model::{polygon, Point2D, Polygon};
200/// use geometry_overlay::operation::sym_difference;
201/// use geometry_trait::MultiPolygon as _;
202///
203/// type P = Point2D<f64, Cartesian>;
204/// let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
205/// let b: Polygon<P> = polygon![[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0), (1.0, 1.0)]];
206/// let out = sym_difference(&a, &b).unwrap();
207/// assert!(out.polygons().count() >= 1);
208/// ```
209#[inline]
210#[must_use = "symmetric difference can fail and the resulting geometry should be used"]
211pub fn sym_difference<G1, G2, P>(g1: &G1, g2: &G2) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
212where
213    G1: PolygonTrait<Point = P>,
214    G2: PolygonTrait<Point = P>,
215    P: PointMut + Default + Copy,
216    P::Scalar: CoordinateScalar + Into<f64>,
217    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
218{
219    areal_overlay(g1, g2, ArealOp::SymDifference)
220}
221
222// ---- multi-polygon operands ------------------------------------------
223//
224// Boost dispatches every areal Boolean through one overlay whatever the
225// operand arity — `bg::intersection(mp, box, out)` and
226// `bg::difference(mp1, mp2, out)` are the same algorithm as the polygon
227// pair. These are that same kernel with both operands' rings, so a caller
228// holding multi-polygons does not have to decompose them and re-combine
229// the pieces itself, which is not the same function.
230
231/// Intersection of two multi-polygons — the region inside **both**.
232///
233/// # Errors
234///
235/// [`OverlayError::Unsupported`] when coordinates exceed the predicate range.
236pub fn intersection_multi<G1, G2, P>(
237    g1: &G1,
238    g2: &G2,
239) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
240where
241    G1: MultiPolygonTrait<Point = P>,
242    G2: MultiPolygonTrait<Point = P>,
243    P: PointMut + Default + Copy,
244    P::Scalar: CoordinateScalar + Into<f64>,
245    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
246{
247    areal_overlay_multi(g1, g2, ArealOp::Intersection)
248}
249
250/// Union of two multi-polygons — the region inside **either**.
251///
252/// # Errors
253///
254/// [`OverlayError::Unsupported`] when coordinates exceed the predicate range.
255pub fn union_multi<G1, G2, P>(g1: &G1, g2: &G2) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
256where
257    G1: MultiPolygonTrait<Point = P>,
258    G2: MultiPolygonTrait<Point = P>,
259    P: PointMut + Default + Copy,
260    P::Scalar: CoordinateScalar + Into<f64>,
261    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
262{
263    areal_overlay_multi(g1, g2, ArealOp::Union)
264}
265
266/// Difference of two multi-polygons — the region inside `g1` but not `g2`.
267///
268/// # Errors
269///
270/// [`OverlayError::Unsupported`] when coordinates exceed the predicate range.
271pub fn difference_multi<G1, G2, P>(
272    g1: &G1,
273    g2: &G2,
274) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
275where
276    G1: MultiPolygonTrait<Point = P>,
277    G2: MultiPolygonTrait<Point = P>,
278    P: PointMut + Default + Copy,
279    P::Scalar: CoordinateScalar + Into<f64>,
280    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
281{
282    areal_overlay_multi(g1, g2, ArealOp::Difference)
283}
284
285/// Symmetric difference of two multi-polygons — inside exactly one of them.
286///
287/// # Errors
288///
289/// [`OverlayError::Unsupported`] when coordinates exceed the predicate range.
290pub fn sym_difference_multi<G1, G2, P>(
291    g1: &G1,
292    g2: &G2,
293) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
294where
295    G1: MultiPolygonTrait<Point = P>,
296    G2: MultiPolygonTrait<Point = P>,
297    P: PointMut + Default + Copy,
298    P::Scalar: CoordinateScalar + Into<f64>,
299    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
300{
301    areal_overlay_multi(g1, g2, ArealOp::SymDifference)
302}
303
304#[cfg(test)]
305mod tests {
306    use super::{OverlayError, intersection, union_poly};
307    use geometry_algorithm::area;
308    use geometry_cs::Cartesian;
309    use geometry_model::{Point2D, Polygon, polygon};
310    use geometry_trait::{MultiPolygon as _, Polygon as _};
311
312    type P = Point2D<f64, Cartesian>;
313
314    fn close(a: f64, b: f64) -> bool {
315        (a - b).abs() <= 1e-5 * a.abs().max(b.abs()).max(1.0)
316    }
317
318    fn total_area(mp: &geometry_model::MultiPolygon<Polygon<P>>) -> f64 {
319        mp.polygons().map(|pg| area(pg).abs()).sum()
320    }
321
322    #[test]
323    fn intersection_of_offset_squares() {
324        let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
325        let b: Polygon<P> = polygon![[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0), (1.0, 1.0)]];
326        let out = intersection(&a, &b).unwrap();
327        assert_eq!(out.polygons().count(), 1);
328        assert!(close(total_area(&out), 1.0), "area {}", total_area(&out));
329    }
330
331    #[test]
332    fn intersection_disjoint_is_empty() {
333        let a: Polygon<P> = polygon![[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)]];
334        let b: Polygon<P> = polygon![[(5.0, 5.0), (6.0, 5.0), (6.0, 6.0), (5.0, 6.0), (5.0, 5.0)]];
335        let out = intersection(&a, &b).unwrap();
336        assert_eq!(out.polygons().count(), 0);
337    }
338
339    #[test]
340    fn intersection_contained_is_inner() {
341        let big: Polygon<P> = polygon![[
342            (0.0, 0.0),
343            (10.0, 0.0),
344            (10.0, 10.0),
345            (0.0, 10.0),
346            (0.0, 0.0)
347        ]];
348        let small: Polygon<P> =
349            polygon![[(2.0, 2.0), (4.0, 2.0), (4.0, 4.0), (2.0, 4.0), (2.0, 2.0)]];
350        let out = intersection(&big, &small).unwrap();
351        assert_eq!(out.polygons().count(), 1);
352        assert!(close(total_area(&out), 4.0), "area {}", total_area(&out));
353    }
354
355    #[test]
356    fn union_of_offset_squares_area() {
357        // |A| + |B| - |A∩B| = 4 + 4 - 1 = 7.
358        let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
359        let b: Polygon<P> = polygon![[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0), (1.0, 1.0)]];
360        let out = union_poly(&a, &b).unwrap();
361        assert_eq!(out.polygons().count(), 1);
362        assert!(close(total_area(&out), 7.0), "area {}", total_area(&out));
363    }
364
365    #[test]
366    fn union_disjoint_is_two_polygons() {
367        let a: Polygon<P> = polygon![[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)]];
368        let b: Polygon<P> = polygon![[(5.0, 5.0), (6.0, 5.0), (6.0, 6.0), (5.0, 6.0), (5.0, 5.0)]];
369        let out = union_poly(&a, &b).unwrap();
370        assert_eq!(out.polygons().count(), 2);
371        assert!(close(total_area(&out), 2.0), "area {}", total_area(&out));
372    }
373
374    #[test]
375    fn difference_of_offset_squares_area() {
376        // |A − B| = |A| − |A∩B| = 4 − 1 = 3.
377        let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
378        let b: Polygon<P> = polygon![[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0), (1.0, 1.0)]];
379        let out = super::difference(&a, &b).unwrap();
380        assert_eq!(out.polygons().count(), 1);
381        assert!(close(total_area(&out), 3.0), "area {}", total_area(&out));
382    }
383
384    #[test]
385    fn difference_disjoint_is_first_whole() {
386        let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
387        let b: Polygon<P> = polygon![[(5.0, 5.0), (6.0, 5.0), (6.0, 6.0), (5.0, 6.0), (5.0, 5.0)]];
388        let out = super::difference(&a, &b).unwrap();
389        assert_eq!(out.polygons().count(), 1);
390        assert!(close(total_area(&out), 4.0), "area {}", total_area(&out));
391    }
392
393    #[test]
394    fn difference_a_inside_b_is_empty() {
395        let big: Polygon<P> = polygon![[
396            (0.0, 0.0),
397            (10.0, 0.0),
398            (10.0, 10.0),
399            (0.0, 10.0),
400            (0.0, 0.0)
401        ]];
402        let small: Polygon<P> =
403            polygon![[(2.0, 2.0), (4.0, 2.0), (4.0, 4.0), (2.0, 4.0), (2.0, 2.0)]];
404        let out = super::difference(&small, &big).unwrap();
405        assert_eq!(out.polygons().count(), 0);
406    }
407
408    #[test]
409    fn difference_with_contained_subtrahend_emits_a_hole() {
410        let big: Polygon<P> = polygon![[
411            (0.0, 0.0),
412            (10.0, 0.0),
413            (10.0, 10.0),
414            (0.0, 10.0),
415            (0.0, 0.0)
416        ]];
417        let small: Polygon<P> =
418            polygon![[(3.0, 3.0), (5.0, 3.0), (5.0, 5.0), (3.0, 5.0), (3.0, 3.0)]];
419        let difference = super::difference(&big, &small).unwrap();
420        assert_eq!(difference.polygons().count(), 1);
421        assert_eq!(difference.polygons().next().unwrap().interiors().count(), 1);
422        assert!(close(total_area(&difference), 96.0));
423        assert!(close(
424            total_area(&super::sym_difference(&big, &small).unwrap()),
425            96.0
426        ));
427    }
428
429    #[test]
430    fn input_with_holes_participates_in_all_operations() {
431        let donut: Polygon<P> = polygon![
432            [
433                (0.0, 0.0),
434                (10.0, 0.0),
435                (10.0, 10.0),
436                (0.0, 10.0),
437                (0.0, 0.0)
438            ],
439            [(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0), (3.0, 3.0)]
440        ];
441        let sq: Polygon<P> = polygon![[(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0), (2.0, 2.0)]];
442        assert!(close(total_area(&intersection(&donut, &sq).unwrap()), 20.0));
443        assert!(close(total_area(&union_poly(&donut, &sq).unwrap()), 100.0));
444        assert!(close(
445            total_area(&super::difference(&donut, &sq).unwrap()),
446            64.0
447        ));
448        assert!(close(
449            total_area(&super::sym_difference(&donut, &sq).unwrap()),
450            80.0
451        ));
452    }
453
454    #[test]
455    fn out_of_range_coordinates_are_refused_not_silently_wrong() {
456        // Regression: two huge overlapping squares (~1e14, past the ±2^26
457        // safe range) made the turn kernel silently drop every crossing as
458        // OutOfRange; the emptied turn graph was misread as "B inside A",
459        // over-reporting the intersection area ~4× as `Ok`. All ops must
460        // refuse rather than return a silently wrong result.
461        let a: Polygon<P> = polygon![[
462            (0.0, 0.0),
463            (2e14, 0.0),
464            (2e14, 2e14),
465            (0.0, 2e14),
466            (0.0, 0.0)
467        ]];
468        let b: Polygon<P> = polygon![[
469            (1e14, 1e14),
470            (3e14, 1e14),
471            (3e14, 3e14),
472            (1e14, 3e14),
473            (1e14, 1e14)
474        ]];
475        assert_eq!(intersection(&a, &b), Err(OverlayError::Unsupported));
476        assert_eq!(union_poly(&a, &b), Err(OverlayError::Unsupported));
477        assert_eq!(super::difference(&a, &b), Err(OverlayError::Unsupported));
478        assert_eq!(
479            super::sym_difference(&a, &b),
480            Err(OverlayError::Unsupported)
481        );
482    }
483
484    #[test]
485    fn sym_difference_of_offset_squares_area() {
486        // |A △ B| = |A| + |B| − 2|A∩B| = 4 + 4 − 2·1 = 6.
487        let a: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
488        let b: Polygon<P> = polygon![[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0), (1.0, 1.0)]];
489        let out = super::sym_difference(&a, &b).unwrap();
490        assert!(close(total_area(&out), 6.0), "area {}", total_area(&out));
491    }
492
493    #[test]
494    fn union_contained_is_outer() {
495        let big: Polygon<P> = polygon![[
496            (0.0, 0.0),
497            (10.0, 0.0),
498            (10.0, 10.0),
499            (0.0, 10.0),
500            (0.0, 0.0)
501        ]];
502        let small: Polygon<P> =
503            polygon![[(2.0, 2.0), (4.0, 2.0), (4.0, 4.0), (2.0, 4.0), (2.0, 2.0)]];
504        let out = union_poly(&big, &small).unwrap();
505        assert_eq!(out.polygons().count(), 1);
506        assert!(close(total_area(&out), 100.0), "area {}", total_area(&out));
507    }
508}