Skip to main content

ezu_features/ops/
boolean.rs

1//! Polygon-vs-polygon set ops (union / intersection / difference /
2//! symmetric difference) via `i_overlay`'s float overlay.
3
4use i_overlay::core::fill_rule::FillRule;
5use i_overlay::core::overlay_rule::OverlayRule;
6use i_overlay::float::single::SingleFloatOverlay;
7
8use crate::Polygon;
9
10use super::convert::{polygon_to_f, polygons_from_shapes};
11
12#[derive(Debug, Clone, Copy)]
13pub enum BoolOp {
14    Union,
15    Intersection,
16    Difference,
17    SymmetricDifference,
18}
19
20impl BoolOp {
21    fn to_overlay_rule(self) -> OverlayRule {
22        match self {
23            BoolOp::Union => OverlayRule::Union,
24            BoolOp::Intersection => OverlayRule::Intersect,
25            BoolOp::Difference => OverlayRule::Difference,
26            BoolOp::SymmetricDifference => OverlayRule::Xor,
27        }
28    }
29}
30
31/// Apply `op` to two polygon sets and return the resulting polygons.
32/// Subject = `a`, clip = `b`. EvenOdd fill rule so holes and
33/// multi-ring inputs survive the round-trip.
34pub fn polygon_boolean(a: &[Polygon], b: &[Polygon], op: BoolOp) -> Vec<Polygon> {
35    if a.is_empty() && b.is_empty() {
36        return Vec::new();
37    }
38    // i_overlay's `overlay` wants a `Subject` (vec of contour-sets =
39    // polygons) and `Clip` (same shape). Flatten each polygon to a
40    // list of paths and pass as the float shapes.
41    let subj: Vec<Vec<Vec<[f64; 2]>>> = a.iter().map(polygon_to_f).collect();
42    let clip: Vec<Vec<Vec<[f64; 2]>>> = b.iter().map(polygon_to_f).collect();
43    let result = subj.overlay(&clip, op.to_overlay_rule(), FillRule::EvenOdd);
44    polygons_from_shapes(&result)
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    fn rect(x0: i32, y0: i32, x1: i32, y1: i32) -> Polygon {
52        Polygon {
53            exterior: vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)],
54            holes: vec![],
55        }
56    }
57
58    #[test]
59    fn union_of_overlapping_rects_yields_one_polygon() {
60        let a = vec![rect(0, 0, 10, 10)];
61        let b = vec![rect(5, 5, 15, 15)];
62        let u = polygon_boolean(&a, &b, BoolOp::Union);
63        assert_eq!(u.len(), 1, "union should merge into 1 polygon: {u:?}");
64    }
65
66    #[test]
67    fn intersection_returns_overlap_region() {
68        let a = vec![rect(0, 0, 10, 10)];
69        let b = vec![rect(5, 5, 15, 15)];
70        let i = polygon_boolean(&a, &b, BoolOp::Intersection);
71        assert_eq!(i.len(), 1);
72        // The overlap rect is roughly 5,5 -> 10,10 — check bounding extent.
73        let mut min_x = i32::MAX;
74        let mut max_x = i32::MIN;
75        for &(x, _) in &i[0].exterior {
76            min_x = min_x.min(x);
77            max_x = max_x.max(x);
78        }
79        assert_eq!((min_x, max_x), (5, 10));
80    }
81
82    #[test]
83    fn difference_removes_b_from_a() {
84        let a = vec![rect(0, 0, 10, 10)];
85        let b = vec![rect(5, 0, 15, 10)];
86        let d = polygon_boolean(&a, &b, BoolOp::Difference);
87        assert_eq!(d.len(), 1);
88        let mut max_x = i32::MIN;
89        for &(x, _) in &d[0].exterior {
90            max_x = max_x.max(x);
91        }
92        assert_eq!(max_x, 5);
93    }
94}