use crate::error::{AlgorithmError, Result};
use oxigdal_core::vector::{Coordinate, LineString, Polygon};
#[cfg(feature = "std")]
use std::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipOperation {
Intersection,
Union,
Difference,
SymmetricDifference,
}
const EPSILON: f64 = 1e-10;
#[derive(Debug, Clone)]
struct ClipVertex {
coord: Coordinate,
is_intersection: bool,
entering: bool,
neighbor: Option<usize>,
alpha: f64,
next: usize,
visited: bool,
}
struct ClipPolygon {
verts: Vec<ClipVertex>,
}
impl ClipPolygon {
fn from_ring(coords: &[Coordinate]) -> Self {
let n = if coords.len() >= 2
&& (coords[0].x - coords[coords.len() - 1].x).abs() < EPSILON
&& (coords[0].y - coords[coords.len() - 1].y).abs() < EPSILON
{
coords.len() - 1
} else {
coords.len()
};
let mut verts = Vec::with_capacity(n);
for i in 0..n {
verts.push(ClipVertex {
coord: coords[i],
is_intersection: false,
entering: false,
neighbor: None,
alpha: 0.0,
next: (i + 1) % n,
visited: false,
});
}
Self { verts }
}
fn len(&self) -> usize {
self.verts.len()
}
}
pub fn clip_polygons(subject: &Polygon, clip: &Polygon, op: ClipOperation) -> Result<Vec<Polygon>> {
validate_polygon(subject, "subject")?;
validate_polygon(clip, "clip")?;
if op == ClipOperation::SymmetricDifference {
let a_minus_b = clip_polygons(subject, clip, ClipOperation::Difference)?;
let b_minus_a = clip_polygons(clip, subject, ClipOperation::Difference)?;
let mut result = a_minus_b;
result.extend(b_minus_a);
return Ok(result);
}
if let (Some(sb), Some(cb)) = (subject.bounds(), clip.bounds()) {
if sb.2 < cb.0 || cb.2 < sb.0 || sb.3 < cb.1 || cb.3 < sb.1 {
return Ok(disjoint_result(subject, clip, op));
}
}
let ixs = find_all_intersections(&subject.exterior.coords, &clip.exterior.coords);
let has_hole_crossings = check_hole_crossings(subject, clip);
if ixs.is_empty() && !has_hole_crossings {
return handle_no_intersections(subject, clip, op);
}
if ixs.is_empty() && has_hole_crossings {
return handle_hole_crossing_containment(subject, clip, op);
}
weiler_atherton_clip(subject, clip, op, &ixs)
}
pub fn clip_multi(
subjects: &[Polygon],
clips: &[Polygon],
op: ClipOperation,
) -> Result<Vec<Polygon>> {
if subjects.is_empty() {
return match op {
ClipOperation::Union => Ok(clips.to_vec()),
_ => Ok(vec![]),
};
}
if clips.is_empty() {
return match op {
ClipOperation::Intersection => Ok(vec![]),
_ => Ok(subjects.to_vec()),
};
}
let mut result = subjects.to_vec();
for clip_poly in clips {
let mut next_result = Vec::new();
for subj in &result {
let clipped = clip_polygons(subj, clip_poly, op)?;
next_result.extend(clipped);
}
result = next_result;
if result.is_empty() && op == ClipOperation::Intersection {
break;
}
}
Ok(result)
}
fn validate_polygon(poly: &Polygon, name: &str) -> Result<()> {
if poly.exterior.coords.len() < 4 {
return Err(AlgorithmError::InsufficientData {
operation: "clip_polygons",
message: format!(
"{name} exterior must have at least 4 coordinates, got {}",
poly.exterior.coords.len()
),
});
}
Ok(())
}
fn disjoint_result(subject: &Polygon, clip: &Polygon, op: ClipOperation) -> Vec<Polygon> {
match op {
ClipOperation::Intersection => vec![],
ClipOperation::Union => vec![subject.clone(), clip.clone()],
ClipOperation::Difference => vec![subject.clone()],
ClipOperation::SymmetricDifference => vec![subject.clone(), clip.clone()],
}
}
fn handle_no_intersections(
subject: &Polygon,
clip: &Polygon,
op: ClipOperation,
) -> Result<Vec<Polygon>> {
if are_rings_identical(&subject.exterior.coords, &clip.exterior.coords) {
return match op {
ClipOperation::Intersection => Ok(vec![subject.clone()]),
ClipOperation::Union => Ok(vec![subject.clone()]),
ClipOperation::Difference => Ok(vec![]),
ClipOperation::SymmetricDifference => Ok(vec![]),
};
}
let subj_in_clip = is_ring_contained_in_polygon(&subject.exterior.coords, clip)?;
let clip_in_subj = is_ring_contained_in_polygon(&clip.exterior.coords, subject)?;
match op {
ClipOperation::Intersection => {
if subj_in_clip {
let holes = collect_overlapping_holes(&clip.interiors, &subject.exterior.coords);
if holes.is_empty() {
Ok(vec![subject.clone()])
} else {
let mut all_holes = subject.interiors.clone();
all_holes.extend(holes);
let poly = Polygon::new(subject.exterior.clone(), all_holes)
.map_err(AlgorithmError::Core)?;
Ok(vec![poly])
}
} else if clip_in_subj {
let holes = collect_overlapping_holes(&subject.interiors, &clip.exterior.coords);
if holes.is_empty() {
Ok(vec![clip.clone()])
} else {
let mut all_holes = clip.interiors.clone();
all_holes.extend(holes);
let poly = Polygon::new(clip.exterior.clone(), all_holes)
.map_err(AlgorithmError::Core)?;
Ok(vec![poly])
}
} else {
Ok(vec![])
}
}
ClipOperation::Union => {
if subj_in_clip {
Ok(vec![clip.clone()])
} else if clip_in_subj {
Ok(vec![subject.clone()])
} else {
Ok(vec![subject.clone(), clip.clone()])
}
}
ClipOperation::Difference => {
if subj_in_clip {
Ok(vec![])
} else if clip_in_subj {
build_subject_with_hole(subject, clip)
} else {
Ok(vec![subject.clone()])
}
}
ClipOperation::SymmetricDifference => {
let a = clip_polygons(subject, clip, ClipOperation::Difference)?;
let b = clip_polygons(clip, subject, ClipOperation::Difference)?;
let mut r = a;
r.extend(b);
Ok(r)
}
}
}
fn check_hole_crossings(subject: &Polygon, clip: &Polygon) -> bool {
for hole in &subject.interiors {
let ixs = find_all_intersections(&hole.coords, &clip.exterior.coords);
if !ixs.is_empty() {
return true;
}
}
for hole in &clip.interiors {
let ixs = find_all_intersections(&hole.coords, &subject.exterior.coords);
if !ixs.is_empty() {
return true;
}
}
false
}
fn handle_hole_crossing_containment(
subject: &Polygon,
clip: &Polygon,
op: ClipOperation,
) -> Result<Vec<Polygon>> {
let clip_inside_subj =
is_ring_contained_in_polygon(&clip.exterior.coords, subject).unwrap_or(false);
let subj_inside_clip =
is_ring_contained_in_polygon(&subject.exterior.coords, clip).unwrap_or(false);
match op {
ClipOperation::Intersection => {
if clip_inside_subj {
let holes = collect_overlapping_holes(&subject.interiors, &clip.exterior.coords);
let mut all_holes = clip.interiors.clone();
all_holes.extend(holes);
let poly =
Polygon::new(clip.exterior.clone(), all_holes).map_err(AlgorithmError::Core)?;
Ok(vec![poly])
} else if subj_inside_clip {
let holes = collect_overlapping_holes(&clip.interiors, &subject.exterior.coords);
let mut all_holes = subject.interiors.clone();
all_holes.extend(holes);
let poly = Polygon::new(subject.exterior.clone(), all_holes)
.map_err(AlgorithmError::Core)?;
Ok(vec![poly])
} else {
Ok(vec![])
}
}
ClipOperation::Difference => {
if subj_inside_clip {
Ok(vec![])
} else if clip_inside_subj {
build_subject_with_hole(subject, clip)
} else {
Ok(vec![subject.clone()])
}
}
ClipOperation::Union => {
if subj_inside_clip {
Ok(vec![clip.clone()])
} else if clip_inside_subj {
Ok(vec![subject.clone()])
} else {
Ok(vec![subject.clone(), clip.clone()])
}
}
ClipOperation::SymmetricDifference => {
let a = clip_polygons(subject, clip, ClipOperation::Difference)?;
let b = clip_polygons(clip, subject, ClipOperation::Difference)?;
let mut r = a;
r.extend(b);
Ok(r)
}
}
}
fn build_subject_with_hole(subject: &Polygon, clip: &Polygon) -> Result<Vec<Polygon>> {
let mut interiors = filter_unaffected_holes(&subject.interiors, clip)?;
interiors.push(clip.exterior.clone());
let mut result_polygons = Vec::new();
let main_poly =
Polygon::new(subject.exterior.clone(), interiors).map_err(AlgorithmError::Core)?;
result_polygons.push(main_poly);
for hole in &clip.interiors {
if hole.coords.len() >= 4
&& !hole.coords.is_empty()
&& point_in_ring(&hole.coords[0], &subject.exterior.coords)
&& !is_point_in_any_hole(&hole.coords[0], subject)?
{
let hole_poly = Polygon::new(hole.clone(), vec![]).map_err(AlgorithmError::Core)?;
result_polygons.push(hole_poly);
}
}
Ok(result_polygons)
}
#[derive(Debug, Clone)]
struct SegmentIx {
coord: Coordinate,
subj_seg: usize,
subj_t: f64,
clip_seg: usize,
clip_t: f64,
}
fn find_all_intersections(
subj_coords: &[Coordinate],
clip_coords: &[Coordinate],
) -> Vec<SegmentIx> {
let sn = ring_vertex_count(subj_coords);
let cn = ring_vertex_count(clip_coords);
let mut result = Vec::new();
for i in 0..sn {
let i_next = (i + 1) % sn;
for j in 0..cn {
let j_next = (j + 1) % cn;
if let Some((t, u, coord)) = segment_intersect(
&subj_coords[i],
&subj_coords[i_next],
&clip_coords[j],
&clip_coords[j_next],
) {
let dominated = result.iter().any(|ix: &SegmentIx| {
(ix.coord.x - coord.x).abs() < EPSILON && (ix.coord.y - coord.y).abs() < EPSILON
});
if !dominated {
result.push(SegmentIx {
coord,
subj_seg: i,
subj_t: t,
clip_seg: j,
clip_t: u,
});
}
}
}
}
result
}
fn ring_vertex_count(coords: &[Coordinate]) -> usize {
if coords.len() >= 2
&& (coords[0].x - coords[coords.len() - 1].x).abs() < EPSILON
&& (coords[0].y - coords[coords.len() - 1].y).abs() < EPSILON
{
coords.len() - 1
} else {
coords.len()
}
}
fn segment_intersect(
a1: &Coordinate,
a2: &Coordinate,
b1: &Coordinate,
b2: &Coordinate,
) -> Option<(f64, f64, Coordinate)> {
let d1x = a2.x - a1.x;
let d1y = a2.y - a1.y;
let d2x = b2.x - b1.x;
let d2y = b2.y - b1.y;
let cross = d1x * d2y - d1y * d2x;
if cross.abs() < EPSILON {
return None; }
let dx = b1.x - a1.x;
let dy = b1.y - a1.y;
let t = (dx * d2y - dy * d2x) / cross;
let u = (dx * d1y - dy * d1x) / cross;
let inset = 1e-8;
if t >= -inset && t <= 1.0 + inset && u >= -inset && u <= 1.0 + inset {
let t_clamped = t.clamp(0.0, 1.0);
let u_clamped = u.clamp(0.0, 1.0);
let t_at_end = t_clamped < inset || t_clamped > 1.0 - inset;
let u_at_end = u_clamped < inset || u_clamped > 1.0 - inset;
if t_at_end && u_at_end {
return None; }
let x = a1.x + t_clamped * d1x;
let y = a1.y + t_clamped * d1y;
Some((t_clamped, u_clamped, Coordinate::new_2d(x, y)))
} else {
None
}
}
fn weiler_atherton_clip(
subject: &Polygon,
clip: &Polygon,
op: ClipOperation,
ixs: &[SegmentIx],
) -> Result<Vec<Polygon>> {
let mut subj_list = ClipPolygon::from_ring(&subject.exterior.coords);
let mut clip_list = ClipPolygon::from_ring(&clip.exterior.coords);
insert_intersections(&mut subj_list, &mut clip_list, ixs);
label_entry_exit(&mut subj_list, &clip.exterior.coords, op);
let clip_op_inv = invert_op_for_clip(op);
label_entry_exit(&mut clip_list, &subject.exterior.coords, clip_op_inv);
let rings = trace_result_rings(&mut subj_list, &mut clip_list, op);
if rings.is_empty() {
return fallback_vertex_clip(subject, clip, op);
}
let result = build_output_polygons(&rings, subject, clip, op)?;
let result = validate_result_area(result, subject, clip, op)?;
Ok(result)
}
fn insert_intersections(subj: &mut ClipPolygon, clip: &mut ClipPolygon, ixs: &[SegmentIx]) {
let mut subj_inserts: Vec<(usize, f64, Coordinate, usize)> = Vec::new(); let mut clip_inserts: Vec<(usize, f64, Coordinate, usize)> = Vec::new();
for (ix_idx, ix) in ixs.iter().enumerate() {
subj_inserts.push((ix.subj_seg, ix.subj_t, ix.coord, ix_idx));
clip_inserts.push((ix.clip_seg, ix.clip_t, ix.coord, ix_idx));
}
subj_inserts.sort_by(|a, b| {
a.0.cmp(&b.0)
.then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
});
clip_inserts.sort_by(|a, b| {
a.0.cmp(&b.0)
.then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
});
let mut subj_positions: Vec<Option<usize>> = vec![None; ixs.len()];
let mut clip_positions: Vec<Option<usize>> = vec![None; ixs.len()];
for &(seg, alpha, coord, ix_idx) in &subj_inserts {
let new_idx = subj.verts.len();
let seg_next = subj.verts[seg].next;
subj.verts.push(ClipVertex {
coord,
is_intersection: true,
entering: false,
neighbor: None,
alpha,
next: seg_next,
visited: false,
});
subj.verts[seg].next = new_idx;
subj_positions[ix_idx] = Some(new_idx);
}
for &(seg, alpha, coord, ix_idx) in &clip_inserts {
let new_idx = clip.verts.len();
let seg_next = clip.verts[seg].next;
clip.verts.push(ClipVertex {
coord,
is_intersection: true,
entering: false,
neighbor: None,
alpha,
next: seg_next,
visited: false,
});
clip.verts[seg].next = new_idx;
clip_positions[ix_idx] = Some(new_idx);
}
for i in 0..ixs.len() {
if let (Some(si), Some(ci)) = (subj_positions[i], clip_positions[i]) {
subj.verts[si].neighbor = Some(ci);
clip.verts[ci].neighbor = Some(si);
}
}
}
fn label_entry_exit(poly: &mut ClipPolygon, other_ring: &[Coordinate], op: ClipOperation) {
let n = poly.len();
if n == 0 {
return;
}
let want_inside = match op {
ClipOperation::Intersection => true,
ClipOperation::Union => false,
ClipOperation::Difference => false,
ClipOperation::SymmetricDifference => false, };
let mut idx = 0;
let max_steps = poly.verts.len() * 2; let mut steps = 0;
loop {
if poly.verts[idx].is_intersection {
let prev_coord = find_prev_original_coord(poly, idx);
let was_inside = point_in_ring(&prev_coord, other_ring);
poly.verts[idx].entering = if want_inside { !was_inside } else { was_inside };
}
idx = poly.verts[idx].next;
steps += 1;
if idx == 0 || steps > max_steps {
break;
}
}
}
fn find_prev_original_coord(poly: &ClipPolygon, target: usize) -> Coordinate {
let mut last_non_ix = poly.verts[0].coord;
let mut idx = 0;
let max_steps = poly.verts.len() * 2;
let mut steps = 0;
loop {
if idx == target {
break;
}
if !poly.verts[idx].is_intersection {
last_non_ix = poly.verts[idx].coord;
}
idx = poly.verts[idx].next;
steps += 1;
if steps > max_steps {
break;
}
}
if steps == 0 {
idx = poly.verts[0].next;
let mut count = 0;
while idx != 0 && count < max_steps {
if !poly.verts[idx].is_intersection {
last_non_ix = poly.verts[idx].coord;
}
idx = poly.verts[idx].next;
count += 1;
}
}
last_non_ix
}
fn invert_op_for_clip(op: ClipOperation) -> ClipOperation {
match op {
ClipOperation::Intersection => ClipOperation::Intersection,
ClipOperation::Union => ClipOperation::Union,
ClipOperation::Difference => ClipOperation::Intersection, ClipOperation::SymmetricDifference => ClipOperation::SymmetricDifference,
}
}
fn trace_result_rings(
subj: &mut ClipPolygon,
clip: &mut ClipPolygon,
op: ClipOperation,
) -> Vec<Vec<Coordinate>> {
let mut rings = Vec::new();
let max_total = (subj.verts.len() + clip.verts.len()) * 2;
loop {
let start = find_unvisited_entering(subj);
let start_idx = match start {
Some(idx) => idx,
None => break,
};
let mut ring_coords: Vec<Coordinate> = Vec::new();
let mut on_subject = true;
let mut current = start_idx;
let mut steps = 0;
loop {
if on_subject {
subj.verts[current].visited = true;
ring_coords.push(subj.verts[current].coord);
current = subj.verts[current].next;
steps += 1;
if current == start_idx {
break;
}
if steps > max_total {
break;
}
if subj.verts[current].is_intersection && !subj.verts[current].entering {
subj.verts[current].visited = true;
ring_coords.push(subj.verts[current].coord);
if let Some(neighbor) = subj.verts[current].neighbor {
current = clip.verts[neighbor].next;
on_subject = false;
}
}
} else {
clip.verts[current].visited = true;
ring_coords.push(clip.verts[current].coord);
current = clip.verts[current].next;
steps += 1;
if steps > max_total {
break;
}
if clip.verts[current].is_intersection && !clip.verts[current].entering {
clip.verts[current].visited = true;
ring_coords.push(clip.verts[current].coord);
if let Some(neighbor) = clip.verts[current].neighbor {
if neighbor == start_idx {
break;
}
current = subj.verts[neighbor].next;
on_subject = true;
}
}
}
}
if ring_coords.len() >= 3 {
close_ring(&mut ring_coords);
rings.push(ring_coords);
}
}
rings
}
fn find_unvisited_entering(poly: &ClipPolygon) -> Option<usize> {
for (i, v) in poly.verts.iter().enumerate() {
if v.is_intersection && v.entering && !v.visited {
return Some(i);
}
}
None
}
fn close_ring(coords: &mut Vec<Coordinate>) {
if let (Some(first), Some(last)) = (coords.first(), coords.last()) {
if (first.x - last.x).abs() > EPSILON || (first.y - last.y).abs() > EPSILON {
coords.push(*first);
}
}
}
fn build_output_polygons(
rings: &[Vec<Coordinate>],
subject: &Polygon,
clip: &Polygon,
op: ClipOperation,
) -> Result<Vec<Polygon>> {
let mut exteriors: Vec<Vec<Coordinate>> = Vec::new();
let mut holes: Vec<Vec<Coordinate>> = Vec::new();
for ring in rings {
if ring.len() < 4 {
continue;
}
let area = signed_ring_area(ring);
if area.abs() < EPSILON {
continue; }
if area > 0.0 {
exteriors.push(ring.clone());
} else {
holes.push(ring.clone());
}
}
if exteriors.is_empty() && !holes.is_empty() {
std::mem::swap(&mut exteriors, &mut holes);
}
let mut result = Vec::new();
for ext_ring in &exteriors {
let mut assigned_holes = Vec::new();
for hole_ring in &holes {
if !hole_ring.is_empty() && point_in_ring(&hole_ring[0], ext_ring) {
if hole_ring.len() >= 4 {
if let Ok(ls) = LineString::new(hole_ring.clone()) {
assigned_holes.push(ls);
}
}
}
}
if op == ClipOperation::Difference {
let preserved = filter_unaffected_holes(&subject.interiors, clip)?;
for h in preserved {
if point_in_ring(&h.coords[0], ext_ring) {
assigned_holes.push(h);
}
}
}
let ext_ls = LineString::new(ext_ring.clone()).map_err(AlgorithmError::Core)?;
let poly = Polygon::new(ext_ls, assigned_holes).map_err(AlgorithmError::Core)?;
result.push(poly);
}
if result.is_empty() {
return fallback_vertex_clip(subject, clip, op);
}
Ok(result)
}
fn validate_result_area(
result: Vec<Polygon>,
subject: &Polygon,
clip: &Polygon,
op: ClipOperation,
) -> Result<Vec<Polygon>> {
let total_area: f64 = result
.iter()
.map(|p| signed_ring_area(&p.exterior.coords).abs())
.sum();
let subj_area = signed_ring_area(&subject.exterior.coords).abs();
let clip_area = signed_ring_area(&clip.exterior.coords).abs();
let valid = match op {
ClipOperation::Intersection => total_area <= subj_area.min(clip_area) + EPSILON,
ClipOperation::Difference => total_area <= subj_area + EPSILON,
ClipOperation::Union => total_area <= subj_area + clip_area + EPSILON,
ClipOperation::SymmetricDifference => true,
};
if valid {
Ok(result)
} else {
fallback_vertex_clip(subject, clip, op)
}
}
fn fallback_vertex_clip(
subject: &Polygon,
clip: &Polygon,
op: ClipOperation,
) -> Result<Vec<Polygon>> {
let subj_coords = &subject.exterior.coords;
let clip_coords = &clip.exterior.coords;
let mut result_coords: Vec<Coordinate> = Vec::new();
match op {
ClipOperation::Intersection => {
for c in subj_coords {
if point_in_ring(c, clip_coords) && !is_point_in_any_hole(c, clip).unwrap_or(false)
{
push_unique(&mut result_coords, *c);
}
}
for c in clip_coords {
if point_in_ring(c, subj_coords)
&& !is_point_in_any_hole(c, subject).unwrap_or(false)
{
push_unique(&mut result_coords, *c);
}
}
let ixs = find_all_intersections(subj_coords, clip_coords);
for ix in &ixs {
push_unique(&mut result_coords, ix.coord);
}
}
ClipOperation::Difference => {
for c in subj_coords {
if !point_in_ring(c, clip_coords) || is_point_in_any_hole(c, clip).unwrap_or(false)
{
push_unique(&mut result_coords, *c);
}
}
let ixs = find_all_intersections(subj_coords, clip_coords);
for ix in &ixs {
push_unique(&mut result_coords, ix.coord);
}
}
ClipOperation::Union => {
for c in subj_coords {
push_unique(&mut result_coords, *c);
}
for c in clip_coords {
if !point_in_ring(c, subj_coords)
|| is_point_in_any_hole(c, subject).unwrap_or(false)
{
push_unique(&mut result_coords, *c);
}
}
}
ClipOperation::SymmetricDifference => {
return Ok(vec![]);
}
}
if result_coords.len() < 3 {
return match op {
ClipOperation::Difference => Ok(vec![subject.clone()]),
ClipOperation::Union => Ok(vec![subject.clone()]),
_ => Ok(vec![]),
};
}
order_points_ccw(&mut result_coords);
close_ring(&mut result_coords);
if result_coords.len() < 4 {
return match op {
ClipOperation::Difference => Ok(vec![subject.clone()]),
ClipOperation::Union => Ok(vec![subject.clone()]),
_ => Ok(vec![]),
};
}
let candidate_area = signed_ring_area(&result_coords).abs();
let subj_area = signed_ring_area(subj_coords).abs();
let clip_area = signed_ring_area(clip_coords).abs();
match op {
ClipOperation::Difference => {
if candidate_area > subj_area + EPSILON {
return Ok(vec![subject.clone()]);
}
}
ClipOperation::Intersection => {
let max_valid = subj_area.min(clip_area);
if candidate_area > max_valid + EPSILON {
return Ok(vec![]);
}
}
_ => {}
}
let interiors = if op == ClipOperation::Difference {
filter_unaffected_holes(&subject.interiors, clip)?
} else {
vec![]
};
let ext = LineString::new(result_coords).map_err(AlgorithmError::Core)?;
let poly = Polygon::new(ext, interiors).map_err(AlgorithmError::Core)?;
Ok(vec![poly])
}
fn push_unique(coords: &mut Vec<Coordinate>, c: Coordinate) {
let dominated = coords
.iter()
.any(|e| (e.x - c.x).abs() < EPSILON && (e.y - c.y).abs() < EPSILON);
if !dominated {
coords.push(c);
}
}
fn order_points_ccw(coords: &mut [Coordinate]) {
if coords.len() < 3 {
return;
}
let n = coords.len() as f64;
let cx: f64 = coords.iter().map(|c| c.x).sum::<f64>() / n;
let cy: f64 = coords.iter().map(|c| c.y).sum::<f64>() / n;
coords.sort_by(|a, b| {
let angle_a = (a.y - cy).atan2(a.x - cx);
let angle_b = (b.y - cy).atan2(b.x - cx);
angle_a
.partial_cmp(&angle_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
fn is_ring_contained_in_polygon(ring: &[Coordinate], polygon: &Polygon) -> Result<bool> {
let n = ring_vertex_count(ring);
if n == 0 {
return Ok(false);
}
for i in 0..n {
if !point_in_ring(&ring[i], &polygon.exterior.coords) {
return Ok(false);
}
if is_point_in_any_hole(&ring[i], polygon)? {
return Ok(false);
}
}
Ok(true)
}
fn collect_overlapping_holes(
holes: &[LineString],
contained_exterior: &[Coordinate],
) -> Vec<LineString> {
let mut result = Vec::new();
for hole in holes {
if hole.coords.is_empty() || hole.coords.len() < 4 {
continue;
}
let hole_centroid = compute_ring_centroid(&hole.coords);
if point_in_ring(&hole_centroid, contained_exterior) {
result.push(hole.clone());
}
}
result
}
fn are_rings_identical(a: &[Coordinate], b: &[Coordinate]) -> bool {
let na = ring_vertex_count(a);
let nb = ring_vertex_count(b);
if na != nb || na == 0 {
return false;
}
for i in 0..na {
if (a[i].x - b[i].x).abs() > EPSILON || (a[i].y - b[i].y).abs() > EPSILON {
return false;
}
}
true
}
fn find_interior_test_point(coords: &[Coordinate]) -> Coordinate {
let n = ring_vertex_count(coords);
if n < 3 {
return coords
.get(0)
.copied()
.unwrap_or(Coordinate::new_2d(0.0, 0.0));
}
let cx: f64 = coords[..n].iter().map(|c| c.x).sum::<f64>() / n as f64;
let cy: f64 = coords[..n].iter().map(|c| c.y).sum::<f64>() / n as f64;
Coordinate::new_2d(cx, cy)
}
pub(crate) fn point_in_ring(point: &Coordinate, ring: &[Coordinate]) -> bool {
let mut inside = false;
let n = ring.len();
if n < 3 {
return false;
}
let mut j = n - 1;
for i in 0..n {
let xi = ring[i].x;
let yi = ring[i].y;
let xj = ring[j].x;
let yj = ring[j].y;
let intersect = ((yi > point.y) != (yj > point.y))
&& (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi);
if intersect {
inside = !inside;
}
j = i;
}
inside
}
pub(crate) fn is_point_in_any_hole(point: &Coordinate, polygon: &Polygon) -> Result<bool> {
for hole in &polygon.interiors {
if point_in_ring(point, &hole.coords) {
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn signed_ring_area(coords: &[Coordinate]) -> f64 {
if coords.len() < 3 {
return 0.0;
}
let mut area = 0.0;
let n = coords.len();
for i in 0..n {
let j = (i + 1) % n;
area += coords[i].x * coords[j].y;
area -= coords[j].x * coords[i].y;
}
area / 2.0
}
pub(crate) fn filter_unaffected_holes(
holes: &[LineString],
clip: &Polygon,
) -> Result<Vec<LineString>> {
let mut result = Vec::new();
for hole in holes {
if hole.coords.is_empty() {
continue;
}
let centroid = compute_ring_centroid(&hole.coords);
if !point_in_ring(¢roid, &clip.exterior.coords) {
result.push(hole.clone());
}
}
Ok(result)
}
pub(crate) fn compute_ring_centroid(coords: &[Coordinate]) -> Coordinate {
if coords.is_empty() {
return Coordinate::new_2d(0.0, 0.0);
}
let n = coords.len() as f64;
let sx: f64 = coords.iter().map(|c| c.x).sum();
let sy: f64 = coords.iter().map(|c| c.y).sum();
Coordinate::new_2d(sx / n, sy / n)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_square(x: f64, y: f64, size: f64) -> Result<Polygon> {
let coords = vec![
Coordinate::new_2d(x, y),
Coordinate::new_2d(x + size, y),
Coordinate::new_2d(x + size, y + size),
Coordinate::new_2d(x, y + size),
Coordinate::new_2d(x, y),
];
let ext = LineString::new(coords).map_err(AlgorithmError::Core)?;
Polygon::new(ext, vec![]).map_err(AlgorithmError::Core)
}
fn make_square_with_hole(
x: f64,
y: f64,
size: f64,
hx: f64,
hy: f64,
hsize: f64,
) -> Result<Polygon> {
let ext_coords = vec![
Coordinate::new_2d(x, y),
Coordinate::new_2d(x + size, y),
Coordinate::new_2d(x + size, y + size),
Coordinate::new_2d(x, y + size),
Coordinate::new_2d(x, y),
];
let hole_coords = vec![
Coordinate::new_2d(hx, hy),
Coordinate::new_2d(hx + hsize, hy),
Coordinate::new_2d(hx + hsize, hy + hsize),
Coordinate::new_2d(hx, hy + hsize),
Coordinate::new_2d(hx, hy),
];
let ext = LineString::new(ext_coords).map_err(AlgorithmError::Core)?;
let hole = LineString::new(hole_coords).map_err(AlgorithmError::Core)?;
Polygon::new(ext, vec![hole]).map_err(AlgorithmError::Core)
}
fn poly_area(poly: &Polygon) -> f64 {
let ext_area = signed_ring_area(&poly.exterior.coords).abs();
let hole_area: f64 = poly
.interiors
.iter()
.map(|h| signed_ring_area(&h.coords).abs())
.sum();
ext_area - hole_area
}
#[test]
fn test_intersection_disjoint_squares() {
let a = make_square(0.0, 0.0, 1.0).expect("make a");
let b = make_square(5.0, 5.0, 1.0).expect("make b");
let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
assert!(
result.is_empty(),
"disjoint squares should have empty intersection"
);
}
#[test]
fn test_intersection_overlapping_squares() {
let a = make_square(0.0, 0.0, 1.0).expect("make a");
let b = make_square(0.5, 0.5, 1.0).expect("make b");
let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
assert!(
!result.is_empty(),
"overlapping squares should have intersection"
);
let total_area: f64 = result.iter().map(|p| poly_area(p)).sum();
assert!(
(total_area - 0.25).abs() < 0.05,
"intersection area should be ~0.25, got {total_area}"
);
}
#[test]
fn test_intersection_containment() {
let outer = make_square(0.0, 0.0, 10.0).expect("outer");
let inner = make_square(2.0, 2.0, 3.0).expect("inner");
let result = clip_polygons(&outer, &inner, ClipOperation::Intersection).expect("clip");
assert_eq!(
result.len(),
1,
"containment intersection should return inner polygon"
);
let area = poly_area(&result[0]);
assert!(
(area - 9.0).abs() < 0.1,
"intersection should have area ~9.0, got {area}"
);
}
#[test]
fn test_difference_disjoint() {
let a = make_square(0.0, 0.0, 5.0).expect("a");
let b = make_square(10.0, 10.0, 5.0).expect("b");
let result = clip_polygons(&a, &b, ClipOperation::Difference).expect("clip");
assert_eq!(result.len(), 1, "disjoint difference returns subject");
}
#[test]
fn test_difference_contained_creates_hole() {
let outer = make_square(0.0, 0.0, 10.0).expect("outer");
let inner = make_square(2.0, 2.0, 3.0).expect("inner");
let result = clip_polygons(&outer, &inner, ClipOperation::Difference).expect("clip");
assert_eq!(result.len(), 1);
assert_eq!(result[0].interiors.len(), 1, "should have a hole");
}
#[test]
fn test_difference_completely_subtracted() {
let inner = make_square(2.0, 2.0, 3.0).expect("inner");
let outer = make_square(0.0, 0.0, 10.0).expect("outer");
let result = clip_polygons(&inner, &outer, ClipOperation::Difference).expect("clip");
assert!(result.is_empty(), "subject fully inside clip => empty");
}
#[test]
fn test_difference_with_hole_in_subject() {
let a = make_square_with_hole(0.0, 0.0, 20.0, 5.0, 5.0, 5.0).expect("a");
let b = make_square(30.0, 30.0, 5.0).expect("b");
let result = clip_polygons(&a, &b, ClipOperation::Difference).expect("clip");
assert_eq!(result.len(), 1);
assert_eq!(result[0].interiors.len(), 1, "hole should be preserved");
}
#[test]
fn test_union_disjoint() {
let a = make_square(0.0, 0.0, 5.0).expect("a");
let b = make_square(10.0, 10.0, 5.0).expect("b");
let result = clip_polygons(&a, &b, ClipOperation::Union).expect("clip");
assert_eq!(result.len(), 2, "disjoint union returns both");
}
#[test]
fn test_union_containment() {
let outer = make_square(0.0, 0.0, 10.0).expect("outer");
let inner = make_square(2.0, 2.0, 3.0).expect("inner");
let result = clip_polygons(&outer, &inner, ClipOperation::Union).expect("clip");
assert_eq!(result.len(), 1, "containment union returns outer");
let area = poly_area(&result[0]);
assert!(
(area - 100.0).abs() < 0.1,
"union area should be ~100, got {area}"
);
}
#[test]
fn test_xor_identical_polygons() {
let a = make_square(0.0, 0.0, 5.0).expect("a");
let b = make_square(0.0, 0.0, 5.0).expect("b");
let result = clip_polygons(&a, &b, ClipOperation::SymmetricDifference).expect("clip");
assert!(
result.is_empty(),
"XOR of identical polygons should be empty"
);
}
#[test]
fn test_xor_disjoint() {
let a = make_square(0.0, 0.0, 5.0).expect("a");
let b = make_square(10.0, 10.0, 5.0).expect("b");
let result = clip_polygons(&a, &b, ClipOperation::SymmetricDifference).expect("clip");
assert_eq!(result.len(), 2, "XOR of disjoint returns both");
}
#[test]
fn test_intersection_l_shaped_concave() {
let l_coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(2.0, 0.0),
Coordinate::new_2d(2.0, 1.0),
Coordinate::new_2d(1.0, 1.0),
Coordinate::new_2d(1.0, 2.0),
Coordinate::new_2d(0.0, 2.0),
Coordinate::new_2d(0.0, 0.0),
];
let l_ext = LineString::new(l_coords).expect("l");
let l_shape = Polygon::new(l_ext, vec![]).expect("l poly");
let b = make_square(0.5, 0.5, 2.0).expect("b");
let result = clip_polygons(&l_shape, &b, ClipOperation::Intersection).expect("clip");
assert!(
!result.is_empty(),
"L-shape intersection should produce a result"
);
let total_area: f64 = result.iter().map(|p| poly_area(p)).sum();
assert!(total_area > 0.0, "intersection area should be positive");
assert!(
total_area < 3.0,
"intersection should be smaller than L-shape (area 3)"
);
}
#[test]
fn test_shared_edge_intersection() {
let a = make_square(0.0, 0.0, 1.0).expect("a");
let b = make_square(1.0, 0.0, 1.0).expect("b");
let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
let total_area: f64 = result.iter().map(|p| poly_area(p)).sum();
assert!(
total_area < 0.01,
"shared-edge intersection should be degenerate (area near 0), got {total_area}"
);
}
#[test]
fn test_clip_multi_intersection() {
let subjects = vec![make_square(0.0, 0.0, 10.0).expect("s")];
let clips = vec![
make_square(2.0, 2.0, 5.0).expect("c1"),
make_square(3.0, 3.0, 5.0).expect("c2"),
];
let result =
clip_multi(&subjects, &clips, ClipOperation::Intersection).expect("clip_multi");
let _ = result;
}
#[test]
fn test_bowtie_self_intersecting() {
let bowtie_coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 1.0),
Coordinate::new_2d(1.0, 0.0),
Coordinate::new_2d(0.0, 1.0),
Coordinate::new_2d(0.0, 0.0),
];
let bowtie_ext = LineString::new(bowtie_coords).expect("bowtie");
let bowtie = Polygon::new(bowtie_ext, vec![]).expect("bowtie poly");
let sq = make_square(0.0, 0.0, 2.0).expect("sq");
let result = clip_polygons(&bowtie, &sq, ClipOperation::Intersection);
assert!(result.is_ok(), "clipping with bowtie should not error");
}
#[test]
fn test_intersection_with_hole() {
let a = make_square_with_hole(0.0, 0.0, 10.0, 3.0, 3.0, 4.0).expect("a");
let b = make_square(2.0, 2.0, 6.0).expect("b");
let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
let total_area: f64 = result.iter().map(|p| poly_area(p)).sum();
assert!(total_area > 0.0, "should have some intersection");
assert!(
total_area < 36.1,
"should be less than b's area, got {total_area}"
);
}
#[test]
fn test_invalid_polygon_error() {
let bad_coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 0.0),
Coordinate::new_2d(0.0, 0.0),
];
let bad_ext = LineString::new(bad_coords);
if let Ok(ext) = bad_ext {
let poly = Polygon {
exterior: ext,
interiors: vec![],
};
let good = make_square(0.0, 0.0, 1.0).expect("good");
let result = clip_polygons(&poly, &good, ClipOperation::Intersection);
assert!(result.is_err(), "should reject polygon with < 4 coords");
}
}
#[test]
fn test_area_invariant_intersection_le_min() {
let a = make_square(0.0, 0.0, 3.0).expect("a");
let b = make_square(1.0, 1.0, 3.0).expect("b");
let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
let ix_area: f64 = result.iter().map(|p| poly_area(p)).sum();
let area_a = poly_area(&a);
let area_b = poly_area(&b);
let min_area = area_a.min(area_b);
assert!(
ix_area <= min_area + 0.1,
"intersection area ({ix_area}) should be <= min({area_a}, {area_b}) = {min_area}"
);
}
#[test]
fn test_area_invariant_difference_le_subject() {
let a = make_square(0.0, 0.0, 3.0).expect("a");
let b = make_square(1.0, 1.0, 3.0).expect("b");
let result = clip_polygons(&a, &b, ClipOperation::Difference).expect("clip");
let diff_area: f64 = result.iter().map(|p| poly_area(p)).sum();
let area_a = poly_area(&a);
assert!(
diff_area <= area_a + 0.1,
"difference area ({diff_area}) should be <= subject area ({area_a})"
);
}
}