use crate::geometry::primitives::Point;
use crate::geometry::shapes::Polygon;
use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::single::SingleFloatOverlay;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipOperation {
Intersection,
Union,
Difference,
Xor,
}
impl ClipOperation {
fn to_overlay_rule(self) -> OverlayRule {
match self {
ClipOperation::Intersection => OverlayRule::Intersect,
ClipOperation::Union => OverlayRule::Union,
ClipOperation::Difference => OverlayRule::Difference,
ClipOperation::Xor => OverlayRule::Xor,
}
}
}
pub fn polygon_clip(subject: &Polygon, clip: &Polygon, operation: ClipOperation) -> Vec<Polygon> {
if subject.vertices().is_empty() {
match operation {
ClipOperation::Union => {
if clip.vertices().is_empty() {
return vec![];
}
return vec![clip.clone()];
}
ClipOperation::Intersection | ClipOperation::Difference => return vec![],
ClipOperation::Xor => {
if clip.vertices().is_empty() {
return vec![];
}
return vec![clip.clone()];
}
}
}
if clip.vertices().is_empty() {
match operation {
ClipOperation::Union | ClipOperation::Difference | ClipOperation::Xor => {
return vec![subject.clone()];
}
ClipOperation::Intersection => return vec![],
}
}
let subject_points: Vec<Point> = subject.vertices().to_vec();
let clip_points: Vec<Point> = clip.vertices().to_vec();
let result =
subject_points.overlay(&clip_points, operation.to_overlay_rule(), FillRule::NonZero);
result
.into_iter()
.flat_map(|shape| shape.into_iter())
.map(Polygon::new)
.collect()
}
pub fn polygon_clip_many(
subjects: &[Polygon],
clip: &Polygon,
operation: ClipOperation,
) -> Vec<Polygon> {
subjects
.iter()
.flat_map(|subject| polygon_clip(subject, clip, operation))
.collect()
}
pub fn polygon_union_many(polygons: &[Polygon]) -> Vec<Polygon> {
let rings: Vec<Vec<Point>> = polygons
.iter()
.filter(|p| !p.vertices().is_empty())
.map(|p| p.vertices().to_vec())
.collect();
if rings.is_empty() {
return vec![];
}
let empty: Vec<Vec<Point>> = Vec::new();
let result = rings.overlay(&empty, OverlayRule::Union, FillRule::NonZero);
result
.into_iter()
.flat_map(|shape| shape.into_iter())
.map(Polygon::new)
.collect()
}
pub fn polygon_difference(subject: &Polygon, clips: &[Polygon]) -> Vec<Polygon> {
if subject.vertices().is_empty() {
return vec![];
}
let resource: Vec<Vec<Point>> = clips
.iter()
.filter(|p| !p.vertices().is_empty())
.map(|p| p.vertices().to_vec())
.collect();
if resource.is_empty() {
return vec![subject.clone()];
}
let subject_points: Vec<Point> = subject.vertices().to_vec();
let result = subject_points.overlay(&resource, OverlayRule::Difference, FillRule::NonZero);
result
.into_iter()
.flat_map(|shape| shape.into_iter())
.map(Polygon::new)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intersection_overlapping_squares() {
let square1 = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(2.0, 0.0),
Point::new(2.0, 2.0),
Point::new(0.0, 2.0),
]);
let square2 = Polygon::new(vec![
Point::new(1.0, 1.0),
Point::new(3.0, 1.0),
Point::new(3.0, 3.0),
Point::new(1.0, 3.0),
]);
let result = polygon_clip(&square1, &square2, ClipOperation::Intersection);
assert_eq!(result.len(), 1);
let intersection_area = result[0].area();
assert!((intersection_area - 1.0).abs() < 1e-6);
}
#[test]
fn test_union_overlapping_squares() {
let square1 = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(2.0, 0.0),
Point::new(2.0, 2.0),
Point::new(0.0, 2.0),
]);
let square2 = Polygon::new(vec![
Point::new(1.0, 1.0),
Point::new(3.0, 1.0),
Point::new(3.0, 3.0),
Point::new(1.0, 3.0),
]);
let result = polygon_clip(&square1, &square2, ClipOperation::Union);
assert_eq!(result.len(), 1);
let union_area = result[0].area();
assert!((union_area - 7.0).abs() < 1e-6);
}
#[test]
fn test_difference_overlapping_squares() {
let square1 = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(2.0, 0.0),
Point::new(2.0, 2.0),
Point::new(0.0, 2.0),
]);
let square2 = Polygon::new(vec![
Point::new(1.0, 1.0),
Point::new(3.0, 1.0),
Point::new(3.0, 3.0),
Point::new(1.0, 3.0),
]);
let result = polygon_clip(&square1, &square2, ClipOperation::Difference);
assert!(!result.is_empty());
let diff_area: f64 = result.iter().map(|p| p.area()).sum();
assert!((diff_area - 3.0).abs() < 1e-6);
}
#[test]
fn test_empty_subject() {
let empty = Polygon::new(vec![]);
let square = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(1.0, 1.0),
Point::new(0.0, 1.0),
]);
assert_eq!(
polygon_clip(&empty, &square, ClipOperation::Intersection).len(),
0
);
assert_eq!(polygon_clip(&empty, &square, ClipOperation::Union).len(), 1);
assert_eq!(
polygon_clip(&empty, &square, ClipOperation::Difference).len(),
0
);
}
#[test]
fn test_empty_clip() {
let square = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(1.0, 1.0),
Point::new(0.0, 1.0),
]);
let empty = Polygon::new(vec![]);
assert_eq!(
polygon_clip(&square, &empty, ClipOperation::Intersection).len(),
0
);
assert_eq!(polygon_clip(&square, &empty, ClipOperation::Union).len(), 1);
assert_eq!(
polygon_clip(&square, &empty, ClipOperation::Difference).len(),
1
);
}
#[test]
fn test_polygon_difference_no_clips_returns_subject() {
let square = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(1.0, 1.0),
Point::new(0.0, 1.0),
]);
let result = polygon_difference(&square, &[]);
assert_eq!(result.len(), 1);
assert!((result[0].area() - 1.0).abs() < 1e-9);
}
fn signed_area_sum(rings: &[Polygon]) -> f64 {
rings
.iter()
.map(|r| {
let v = r.vertices();
let n = v.len();
let mut s = 0.0;
for i in 0..n {
let j = (i + 1) % n;
s += v[i].x() * v[j].y() - v[j].x() * v[i].y();
}
0.5 * s
})
.sum()
}
#[test]
fn test_polygon_difference_overlapping_clips_preserve_pairing() {
let outer = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(6.0, 0.0),
Point::new(6.0, 6.0),
Point::new(0.0, 6.0),
]);
let a = Polygon::new(vec![
Point::new(2.0, 2.0),
Point::new(4.0, 2.0),
Point::new(4.0, 4.0),
Point::new(2.0, 4.0),
]);
let b = Polygon::new(vec![
Point::new(3.0, 3.0),
Point::new(5.0, 3.0),
Point::new(5.0, 5.0),
Point::new(3.0, 5.0),
]);
let result = polygon_difference(&outer, &[a, b]);
assert!((signed_area_sum(&result).abs() - 29.0).abs() < 1e-6);
}
#[test]
fn test_polygon_difference_with_nested_clip_yields_outer_and_hole() {
let outer = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
]);
let inner = Polygon::new(vec![
Point::new(3.0, 3.0),
Point::new(7.0, 3.0),
Point::new(7.0, 7.0),
Point::new(3.0, 7.0),
]);
let result = polygon_difference(&outer, &[inner]);
assert_eq!(result.len(), 2, "expected one outer + one hole ring");
assert!((signed_area_sum(&result).abs() - 84.0).abs() < 1e-6);
}
#[test]
fn test_polygon_difference_skips_empty_clips() {
let outer = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(2.0, 0.0),
Point::new(2.0, 2.0),
Point::new(0.0, 2.0),
]);
let empty = Polygon::new(vec![]);
let real = Polygon::new(vec![
Point::new(0.5, 0.5),
Point::new(1.5, 0.5),
Point::new(1.5, 1.5),
Point::new(0.5, 1.5),
]);
let r = polygon_difference(&outer, std::slice::from_ref(&empty));
assert_eq!(r.len(), 1);
assert!((r[0].area() - 4.0).abs() < 1e-9);
let r = polygon_difference(&outer, &[empty, real]);
assert!((signed_area_sum(&r).abs() - 3.0).abs() < 1e-6);
}
}