Skip to main content

oxigdal_algorithms/vector/
clipping.rs

1//! Weiler-Atherton polygon clipping engine
2//!
3//! Production-grade polygon clipping supporting Intersection, Union,
4//! Difference, and Symmetric Difference (XOR) operations. Handles holes,
5//! degeneracies, self-intersecting polygons, and floating-point edge cases.
6//!
7//! # Algorithm
8//!
9//! The Weiler-Atherton algorithm proceeds in five phases:
10//! 1. **Intersection detection** -- segment-segment tests between polygon boundaries
11//! 2. **Entry/exit labeling** -- cross-product orientation classifies each
12//!    intersection as entering or exiting the other polygon
13//! 3. **Polygon tracing** -- walk along subject/clip polygons, switching at
14//!    intersections to build result rings
15//! 4. **Hole handling** -- process holes independently, classify output rings as
16//!    exterior/interior by winding, assign holes via point-in-polygon
17//! 5. **Degeneracy handling** -- epsilon snap, zero-length edge removal
18//!
19//! # Operations
20//!
21//! - [`ClipOperation::Intersection`] -- area in both A and B
22//! - [`ClipOperation::Union`] -- area in A or B (or both)
23//! - [`ClipOperation::Difference`] -- area in A but not B
24//! - [`ClipOperation::SymmetricDifference`] -- area in A or B but not both
25
26use crate::error::{AlgorithmError, Result};
27use oxigdal_core::vector::{Coordinate, LineString, Polygon};
28
29#[cfg(feature = "std")]
30use std::vec::Vec;
31
32// ---------------------------------------------------------------------------
33// Public types
34// ---------------------------------------------------------------------------
35
36/// Polygon clipping operation kind.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ClipOperation {
39    /// Area in both A and B.
40    Intersection,
41    /// Area in A or B (or both).
42    Union,
43    /// Area in A but not in B.
44    Difference,
45    /// Area in A or B but not both (XOR).
46    SymmetricDifference,
47}
48
49// ---------------------------------------------------------------------------
50// Internal vertex representation
51// ---------------------------------------------------------------------------
52
53/// Tolerance for coordinate comparisons (1e-10).
54const EPSILON: f64 = 1e-10;
55
56/// A vertex in the clip-polygon linked list.
57#[derive(Debug, Clone)]
58struct ClipVertex {
59    /// World coordinate.
60    coord: Coordinate,
61    /// True when this vertex was inserted as an intersection.
62    is_intersection: bool,
63    /// True when the traversal *enters* the other polygon at this vertex.
64    entering: bool,
65    /// Index of the corresponding vertex in the other polygon's list.
66    neighbor: Option<usize>,
67    /// Parametric position along the edge (0..1). Used for ordering
68    /// multiple intersections on the same segment.
69    alpha: f64,
70    /// Index of next vertex (circular).
71    next: usize,
72    /// Whether this vertex has been visited during tracing.
73    visited: bool,
74}
75
76/// A flat, index-based circular vertex list for one polygon.
77struct ClipPolygon {
78    verts: Vec<ClipVertex>,
79}
80
81impl ClipPolygon {
82    /// Build from a ring's coordinates (must be closed, we strip the
83    /// duplicate closing vertex).
84    fn from_ring(coords: &[Coordinate]) -> Self {
85        let n = if coords.len() >= 2
86            && (coords[0].x - coords[coords.len() - 1].x).abs() < EPSILON
87            && (coords[0].y - coords[coords.len() - 1].y).abs() < EPSILON
88        {
89            coords.len() - 1
90        } else {
91            coords.len()
92        };
93
94        let mut verts = Vec::with_capacity(n);
95        for i in 0..n {
96            verts.push(ClipVertex {
97                coord: coords[i],
98                is_intersection: false,
99                entering: false,
100                neighbor: None,
101                alpha: 0.0,
102                next: (i + 1) % n,
103                visited: false,
104            });
105        }
106        Self { verts }
107    }
108
109    fn len(&self) -> usize {
110        self.verts.len()
111    }
112}
113
114// ---------------------------------------------------------------------------
115// Public API
116// ---------------------------------------------------------------------------
117
118/// Clip two polygons with the given boolean operation.
119///
120/// Returns a (possibly empty) vector of result polygons.
121///
122/// # Errors
123///
124/// Returns [`AlgorithmError`] when either polygon has fewer than 4 exterior
125/// coordinates or when an internal invariant is violated.
126pub fn clip_polygons(subject: &Polygon, clip: &Polygon, op: ClipOperation) -> Result<Vec<Polygon>> {
127    validate_polygon(subject, "subject")?;
128    validate_polygon(clip, "clip")?;
129
130    // Symmetric difference = (A-B) union (B-A)
131    if op == ClipOperation::SymmetricDifference {
132        let a_minus_b = clip_polygons(subject, clip, ClipOperation::Difference)?;
133        let b_minus_a = clip_polygons(clip, subject, ClipOperation::Difference)?;
134        let mut result = a_minus_b;
135        result.extend(b_minus_a);
136        return Ok(result);
137    }
138
139    // Bounding-box quick rejection
140    if let (Some(sb), Some(cb)) = (subject.bounds(), clip.bounds()) {
141        if sb.2 < cb.0 || cb.2 < sb.0 || sb.3 < cb.1 || cb.3 < sb.1 {
142            return Ok(disjoint_result(subject, clip, op));
143        }
144    }
145
146    // Find boundary intersections (exterior vs exterior)
147    let ixs = find_all_intersections(&subject.exterior.coords, &clip.exterior.coords);
148
149    // Also check for intersections between exterior and holes
150    let has_hole_crossings = check_hole_crossings(subject, clip);
151
152    if ixs.is_empty() && !has_hole_crossings {
153        return handle_no_intersections(subject, clip, op);
154    }
155
156    if ixs.is_empty() && has_hole_crossings {
157        // Hole boundaries cross but exteriors don't -- one is contained in the other.
158        // Handle with containment + hole transfer logic.
159        return handle_hole_crossing_containment(subject, clip, op);
160    }
161
162    // Build vertex lists, insert intersections, label entry/exit, trace
163    weiler_atherton_clip(subject, clip, op, &ixs)
164}
165
166/// Clip subject against a set of clip polygons in sequence.
167pub fn clip_multi(
168    subjects: &[Polygon],
169    clips: &[Polygon],
170    op: ClipOperation,
171) -> Result<Vec<Polygon>> {
172    if subjects.is_empty() {
173        return match op {
174            ClipOperation::Union => Ok(clips.to_vec()),
175            _ => Ok(vec![]),
176        };
177    }
178    if clips.is_empty() {
179        return match op {
180            ClipOperation::Intersection => Ok(vec![]),
181            _ => Ok(subjects.to_vec()),
182        };
183    }
184
185    let mut result = subjects.to_vec();
186    for clip_poly in clips {
187        let mut next_result = Vec::new();
188        for subj in &result {
189            let clipped = clip_polygons(subj, clip_poly, op)?;
190            next_result.extend(clipped);
191        }
192        result = next_result;
193        if result.is_empty() && op == ClipOperation::Intersection {
194            break;
195        }
196    }
197    Ok(result)
198}
199
200// ---------------------------------------------------------------------------
201// Validation
202// ---------------------------------------------------------------------------
203
204fn validate_polygon(poly: &Polygon, name: &str) -> Result<()> {
205    if poly.exterior.coords.len() < 4 {
206        return Err(AlgorithmError::InsufficientData {
207            operation: "clip_polygons",
208            message: format!(
209                "{name} exterior must have at least 4 coordinates, got {}",
210                poly.exterior.coords.len()
211            ),
212        });
213    }
214    Ok(())
215}
216
217// ---------------------------------------------------------------------------
218// Disjoint / containment fast paths
219// ---------------------------------------------------------------------------
220
221fn disjoint_result(subject: &Polygon, clip: &Polygon, op: ClipOperation) -> Vec<Polygon> {
222    match op {
223        ClipOperation::Intersection => vec![],
224        ClipOperation::Union => vec![subject.clone(), clip.clone()],
225        ClipOperation::Difference => vec![subject.clone()],
226        ClipOperation::SymmetricDifference => vec![subject.clone(), clip.clone()],
227    }
228}
229
230fn handle_no_intersections(
231    subject: &Polygon,
232    clip: &Polygon,
233    op: ClipOperation,
234) -> Result<Vec<Polygon>> {
235    // Check for identical polygons (all vertices match)
236    if are_rings_identical(&subject.exterior.coords, &clip.exterior.coords) {
237        return match op {
238            ClipOperation::Intersection => Ok(vec![subject.clone()]),
239            ClipOperation::Union => Ok(vec![subject.clone()]),
240            ClipOperation::Difference => Ok(vec![]),
241            ClipOperation::SymmetricDifference => Ok(vec![]),
242        };
243    }
244
245    // Containment check: verify that ALL vertices of one polygon are inside the other.
246    // Using just a single test point (centroid) fails when the centroid happens to be
247    // inside the other polygon but the polygon itself extends beyond it.
248    let subj_in_clip = is_ring_contained_in_polygon(&subject.exterior.coords, clip)?;
249    let clip_in_subj = is_ring_contained_in_polygon(&clip.exterior.coords, subject)?;
250
251    match op {
252        ClipOperation::Intersection => {
253            if subj_in_clip {
254                // Subject is inside clip -- but clip may have holes that subtract from subject.
255                // Transfer any clip holes that overlap with subject.
256                let holes = collect_overlapping_holes(&clip.interiors, &subject.exterior.coords);
257                if holes.is_empty() {
258                    Ok(vec![subject.clone()])
259                } else {
260                    let mut all_holes = subject.interiors.clone();
261                    all_holes.extend(holes);
262                    let poly = Polygon::new(subject.exterior.clone(), all_holes)
263                        .map_err(AlgorithmError::Core)?;
264                    Ok(vec![poly])
265                }
266            } else if clip_in_subj {
267                // Clip is inside subject -- but subject may have holes that subtract from clip.
268                let holes = collect_overlapping_holes(&subject.interiors, &clip.exterior.coords);
269                if holes.is_empty() {
270                    Ok(vec![clip.clone()])
271                } else {
272                    let mut all_holes = clip.interiors.clone();
273                    all_holes.extend(holes);
274                    let poly = Polygon::new(clip.exterior.clone(), all_holes)
275                        .map_err(AlgorithmError::Core)?;
276                    Ok(vec![poly])
277                }
278            } else {
279                Ok(vec![])
280            }
281        }
282        ClipOperation::Union => {
283            if subj_in_clip {
284                Ok(vec![clip.clone()])
285            } else if clip_in_subj {
286                Ok(vec![subject.clone()])
287            } else {
288                Ok(vec![subject.clone(), clip.clone()])
289            }
290        }
291        ClipOperation::Difference => {
292            if subj_in_clip {
293                // subject fully inside clip => empty
294                Ok(vec![])
295            } else if clip_in_subj {
296                // clip fully inside subject => subject with clip as hole
297                build_subject_with_hole(subject, clip)
298            } else {
299                Ok(vec![subject.clone()])
300            }
301        }
302        ClipOperation::SymmetricDifference => {
303            // handled at top level but included for completeness
304            let a = clip_polygons(subject, clip, ClipOperation::Difference)?;
305            let b = clip_polygons(clip, subject, ClipOperation::Difference)?;
306            let mut r = a;
307            r.extend(b);
308            Ok(r)
309        }
310    }
311}
312
313/// Check if any hole boundaries cross with the other polygon's exterior or holes.
314fn check_hole_crossings(subject: &Polygon, clip: &Polygon) -> bool {
315    // Check subject's holes vs clip exterior
316    for hole in &subject.interiors {
317        let ixs = find_all_intersections(&hole.coords, &clip.exterior.coords);
318        if !ixs.is_empty() {
319            return true;
320        }
321    }
322    // Check clip's holes vs subject exterior
323    for hole in &clip.interiors {
324        let ixs = find_all_intersections(&hole.coords, &subject.exterior.coords);
325        if !ixs.is_empty() {
326            return true;
327        }
328    }
329    false
330}
331
332/// Handle the case where exteriors don't cross but hole boundaries do.
333/// This happens when one polygon's exterior contains the other, but a hole
334/// boundary of the container crosses the contained polygon.
335fn handle_hole_crossing_containment(
336    subject: &Polygon,
337    clip: &Polygon,
338    op: ClipOperation,
339) -> Result<Vec<Polygon>> {
340    // Determine which is the container and which is contained.
341    // Use all-vertex containment check for consistency with handle_no_intersections.
342    let clip_inside_subj =
343        is_ring_contained_in_polygon(&clip.exterior.coords, subject).unwrap_or(false);
344    let subj_inside_clip =
345        is_ring_contained_in_polygon(&subject.exterior.coords, clip).unwrap_or(false);
346
347    match op {
348        ClipOperation::Intersection => {
349            if clip_inside_subj {
350                // Clip is inside subject. Intersection = clip minus subject's holes that overlap.
351                let holes = collect_overlapping_holes(&subject.interiors, &clip.exterior.coords);
352                let mut all_holes = clip.interiors.clone();
353                all_holes.extend(holes);
354                let poly =
355                    Polygon::new(clip.exterior.clone(), all_holes).map_err(AlgorithmError::Core)?;
356                Ok(vec![poly])
357            } else if subj_inside_clip {
358                // Subject is inside clip. Intersection = subject minus clip's holes that overlap.
359                let holes = collect_overlapping_holes(&clip.interiors, &subject.exterior.coords);
360                let mut all_holes = subject.interiors.clone();
361                all_holes.extend(holes);
362                let poly = Polygon::new(subject.exterior.clone(), all_holes)
363                    .map_err(AlgorithmError::Core)?;
364                Ok(vec![poly])
365            } else {
366                Ok(vec![])
367            }
368        }
369        ClipOperation::Difference => {
370            if subj_inside_clip {
371                // Subject inside clip -- difference is empty unless subject is in a hole of clip
372                Ok(vec![])
373            } else if clip_inside_subj {
374                // Clip inside subject -- difference = subject with clip as hole
375                build_subject_with_hole(subject, clip)
376            } else {
377                Ok(vec![subject.clone()])
378            }
379        }
380        ClipOperation::Union => {
381            if subj_inside_clip {
382                Ok(vec![clip.clone()])
383            } else if clip_inside_subj {
384                Ok(vec![subject.clone()])
385            } else {
386                Ok(vec![subject.clone(), clip.clone()])
387            }
388        }
389        ClipOperation::SymmetricDifference => {
390            let a = clip_polygons(subject, clip, ClipOperation::Difference)?;
391            let b = clip_polygons(clip, subject, ClipOperation::Difference)?;
392            let mut r = a;
393            r.extend(b);
394            Ok(r)
395        }
396    }
397}
398
399/// Build result for Difference when clip is fully contained in subject.
400fn build_subject_with_hole(subject: &Polygon, clip: &Polygon) -> Result<Vec<Polygon>> {
401    let mut interiors = filter_unaffected_holes(&subject.interiors, clip)?;
402    interiors.push(clip.exterior.clone());
403
404    let mut result_polygons = Vec::new();
405    let main_poly =
406        Polygon::new(subject.exterior.clone(), interiors).map_err(AlgorithmError::Core)?;
407    result_polygons.push(main_poly);
408
409    // Holes in clip become filled regions (difference semantics)
410    for hole in &clip.interiors {
411        if hole.coords.len() >= 4
412            && !hole.coords.is_empty()
413            && point_in_ring(&hole.coords[0], &subject.exterior.coords)
414            && !is_point_in_any_hole(&hole.coords[0], subject)?
415        {
416            let hole_poly = Polygon::new(hole.clone(), vec![]).map_err(AlgorithmError::Core)?;
417            result_polygons.push(hole_poly);
418        }
419    }
420
421    Ok(result_polygons)
422}
423
424// ---------------------------------------------------------------------------
425// Intersection detection (Phase 1)
426// ---------------------------------------------------------------------------
427
428/// An intersection between two segments.
429#[derive(Debug, Clone)]
430struct SegmentIx {
431    /// Intersection coordinate (snapped).
432    coord: Coordinate,
433    /// Index of subject segment start vertex.
434    subj_seg: usize,
435    /// Parametric t along subject segment \[0,1\].
436    subj_t: f64,
437    /// Index of clip segment start vertex.
438    clip_seg: usize,
439    /// Parametric t along clip segment \[0,1\].
440    clip_t: f64,
441}
442
443fn find_all_intersections(
444    subj_coords: &[Coordinate],
445    clip_coords: &[Coordinate],
446) -> Vec<SegmentIx> {
447    let sn = ring_vertex_count(subj_coords);
448    let cn = ring_vertex_count(clip_coords);
449    let mut result = Vec::new();
450
451    for i in 0..sn {
452        let i_next = (i + 1) % sn;
453        for j in 0..cn {
454            let j_next = (j + 1) % cn;
455            if let Some((t, u, coord)) = segment_intersect(
456                &subj_coords[i],
457                &subj_coords[i_next],
458                &clip_coords[j],
459                &clip_coords[j_next],
460            ) {
461                // Deduplicate by coordinate proximity
462                let dominated = result.iter().any(|ix: &SegmentIx| {
463                    (ix.coord.x - coord.x).abs() < EPSILON && (ix.coord.y - coord.y).abs() < EPSILON
464                });
465                if !dominated {
466                    result.push(SegmentIx {
467                        coord,
468                        subj_seg: i,
469                        subj_t: t,
470                        clip_seg: j,
471                        clip_t: u,
472                    });
473                }
474            }
475        }
476    }
477
478    result
479}
480
481/// Vertex count excluding the duplicate closing vertex.
482fn ring_vertex_count(coords: &[Coordinate]) -> usize {
483    if coords.len() >= 2
484        && (coords[0].x - coords[coords.len() - 1].x).abs() < EPSILON
485        && (coords[0].y - coords[coords.len() - 1].y).abs() < EPSILON
486    {
487        coords.len() - 1
488    } else {
489        coords.len()
490    }
491}
492
493/// Compute intersection of two segments.  Returns `(t, u, coord)` if they
494/// intersect strictly or at a shared interior point.
495fn segment_intersect(
496    a1: &Coordinate,
497    a2: &Coordinate,
498    b1: &Coordinate,
499    b2: &Coordinate,
500) -> Option<(f64, f64, Coordinate)> {
501    let d1x = a2.x - a1.x;
502    let d1y = a2.y - a1.y;
503    let d2x = b2.x - b1.x;
504    let d2y = b2.y - b1.y;
505
506    let cross = d1x * d2y - d1y * d2x;
507    if cross.abs() < EPSILON {
508        return None; // parallel / collinear -- skip for clipping purposes
509    }
510
511    let dx = b1.x - a1.x;
512    let dy = b1.y - a1.y;
513
514    let t = (dx * d2y - dy * d2x) / cross;
515    let u = (dx * d1y - dy * d1x) / cross;
516
517    // Accept if within (0,1) strictly -- endpoint-only touches (t=0 or t=1
518    // combined with u=0 or u=1) don't represent true crossings for clipping.
519    // Use a small inset to avoid degeneracies at exact endpoints.
520    let inset = 1e-8;
521    if t >= -inset && t <= 1.0 + inset && u >= -inset && u <= 1.0 + inset {
522        let t_clamped = t.clamp(0.0, 1.0);
523        let u_clamped = u.clamp(0.0, 1.0);
524
525        // Skip pure endpoint-endpoint touches (both parameters at 0 or 1)
526        let t_at_end = t_clamped < inset || t_clamped > 1.0 - inset;
527        let u_at_end = u_clamped < inset || u_clamped > 1.0 - inset;
528        if t_at_end && u_at_end {
529            return None; // endpoint touch, not a crossing
530        }
531
532        let x = a1.x + t_clamped * d1x;
533        let y = a1.y + t_clamped * d1y;
534        Some((t_clamped, u_clamped, Coordinate::new_2d(x, y)))
535    } else {
536        None
537    }
538}
539
540// ---------------------------------------------------------------------------
541// Weiler-Atherton core (Phases 2-3)
542// ---------------------------------------------------------------------------
543
544fn weiler_atherton_clip(
545    subject: &Polygon,
546    clip: &Polygon,
547    op: ClipOperation,
548    ixs: &[SegmentIx],
549) -> Result<Vec<Polygon>> {
550    // Build vertex lists
551    let mut subj_list = ClipPolygon::from_ring(&subject.exterior.coords);
552    let mut clip_list = ClipPolygon::from_ring(&clip.exterior.coords);
553
554    // Phase 2a: Insert intersection vertices into both lists
555    insert_intersections(&mut subj_list, &mut clip_list, ixs);
556
557    // Phase 2b: Label entry/exit
558    label_entry_exit(&mut subj_list, &clip.exterior.coords, op);
559    let clip_op_inv = invert_op_for_clip(op);
560    label_entry_exit(&mut clip_list, &subject.exterior.coords, clip_op_inv);
561
562    // Phase 3: Trace result polygons
563    let rings = trace_result_rings(&mut subj_list, &mut clip_list, op);
564
565    if rings.is_empty() {
566        // Fallback: if tracing produces no rings, use the vertex-subset
567        // approach as a safety net for near-degenerate cases.
568        return fallback_vertex_clip(subject, clip, op);
569    }
570
571    // Phase 4: Build output polygons, handling holes
572    let result = build_output_polygons(&rings, subject, clip, op)?;
573
574    // Phase 5: Validate result area against geometric constraints
575    let result = validate_result_area(result, subject, clip, op)?;
576
577    Ok(result)
578}
579
580/// Insert intersection vertices into both subject and clip vertex lists,
581/// maintaining correct circular next pointers and cross-links.
582fn insert_intersections(subj: &mut ClipPolygon, clip: &mut ClipPolygon, ixs: &[SegmentIx]) {
583    // Group intersections by segment, sort by alpha
584    let mut subj_inserts: Vec<(usize, f64, Coordinate, usize)> = Vec::new(); // (seg, alpha, coord, ix_idx)
585    let mut clip_inserts: Vec<(usize, f64, Coordinate, usize)> = Vec::new();
586
587    for (ix_idx, ix) in ixs.iter().enumerate() {
588        subj_inserts.push((ix.subj_seg, ix.subj_t, ix.coord, ix_idx));
589        clip_inserts.push((ix.clip_seg, ix.clip_t, ix.coord, ix_idx));
590    }
591
592    // Sort by segment then alpha (descending alpha so we insert from end to start
593    // of each segment to keep indices stable)
594    subj_inserts.sort_by(|a, b| {
595        a.0.cmp(&b.0)
596            .then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
597    });
598    clip_inserts.sort_by(|a, b| {
599        a.0.cmp(&b.0)
600            .then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
601    });
602
603    // Track where each intersection ends up in each list so we can cross-link
604    let mut subj_positions: Vec<Option<usize>> = vec![None; ixs.len()];
605    let mut clip_positions: Vec<Option<usize>> = vec![None; ixs.len()];
606
607    // Insert into subject list
608    for &(seg, alpha, coord, ix_idx) in &subj_inserts {
609        let new_idx = subj.verts.len();
610        let seg_next = subj.verts[seg].next;
611        subj.verts.push(ClipVertex {
612            coord,
613            is_intersection: true,
614            entering: false,
615            neighbor: None,
616            alpha,
617            next: seg_next,
618            visited: false,
619        });
620        subj.verts[seg].next = new_idx;
621        subj_positions[ix_idx] = Some(new_idx);
622    }
623
624    // Insert into clip list
625    for &(seg, alpha, coord, ix_idx) in &clip_inserts {
626        let new_idx = clip.verts.len();
627        let seg_next = clip.verts[seg].next;
628        clip.verts.push(ClipVertex {
629            coord,
630            is_intersection: true,
631            entering: false,
632            neighbor: None,
633            alpha,
634            next: seg_next,
635            visited: false,
636        });
637        clip.verts[seg].next = new_idx;
638        clip_positions[ix_idx] = Some(new_idx);
639    }
640
641    // Cross-link
642    for i in 0..ixs.len() {
643        if let (Some(si), Some(ci)) = (subj_positions[i], clip_positions[i]) {
644            subj.verts[si].neighbor = Some(ci);
645            clip.verts[ci].neighbor = Some(si);
646        }
647    }
648}
649
650/// Label each intersection vertex as entering or exiting.
651///
652/// For Intersection: entering = going from outside to inside the other polygon.
653/// For Difference:   entering = going from inside to outside the clip polygon
654///                   (i.e., we want the part of subject that is *outside* clip).
655fn label_entry_exit(poly: &mut ClipPolygon, other_ring: &[Coordinate], op: ClipOperation) {
656    // Walk the vertex list in order; at each intersection vertex, classify
657    // based on whether the midpoint of the previous edge is inside the other polygon.
658    let n = poly.len();
659    if n == 0 {
660        return;
661    }
662
663    // Determine which "inside" status we want
664    let want_inside = match op {
665        ClipOperation::Intersection => true,
666        ClipOperation::Union => false,
667        ClipOperation::Difference => false,
668        ClipOperation::SymmetricDifference => false, // shouldn't reach here
669    };
670
671    // Walk the circular list starting from vertex 0
672    let mut idx = 0;
673    let max_steps = poly.verts.len() * 2; // safety bound
674    let mut steps = 0;
675    loop {
676        if poly.verts[idx].is_intersection {
677            // Look at previous original (non-intersection) vertex to determine
678            // if we were inside or outside the other polygon before this intersection
679            let prev_coord = find_prev_original_coord(poly, idx);
680            let was_inside = point_in_ring(&prev_coord, other_ring);
681
682            // For Intersection: if was_inside=false, we're entering (going inside)
683            // For Difference/Union: if was_inside=true, we're entering (going outside)
684            poly.verts[idx].entering = if want_inside { !was_inside } else { was_inside };
685        }
686
687        idx = poly.verts[idx].next;
688        steps += 1;
689        if idx == 0 || steps > max_steps {
690            break;
691        }
692    }
693}
694
695/// Find the coordinate of the previous non-intersection vertex for labeling.
696fn find_prev_original_coord(poly: &ClipPolygon, target: usize) -> Coordinate {
697    // Walk backwards through the circular list to find a non-intersection vertex.
698    // Since we don't have prev pointers, walk forwards from 0 and track the
699    // last non-intersection vertex before we reach `target`.
700    let mut last_non_ix = poly.verts[0].coord;
701    let mut idx = 0;
702    let max_steps = poly.verts.len() * 2;
703    let mut steps = 0;
704
705    loop {
706        if idx == target {
707            break;
708        }
709        if !poly.verts[idx].is_intersection {
710            last_non_ix = poly.verts[idx].coord;
711        }
712        idx = poly.verts[idx].next;
713        steps += 1;
714        if steps > max_steps {
715            break;
716        }
717    }
718
719    // If we started right at target (idx==0 == target), walk the whole ring
720    // to find the last non-intersection vertex
721    if steps == 0 {
722        idx = poly.verts[0].next;
723        let mut count = 0;
724        while idx != 0 && count < max_steps {
725            if !poly.verts[idx].is_intersection {
726                last_non_ix = poly.verts[idx].coord;
727            }
728            idx = poly.verts[idx].next;
729            count += 1;
730        }
731    }
732
733    last_non_ix
734}
735
736/// Invert the operation for the clip polygon's labeling.
737fn invert_op_for_clip(op: ClipOperation) -> ClipOperation {
738    match op {
739        ClipOperation::Intersection => ClipOperation::Intersection,
740        ClipOperation::Union => ClipOperation::Union,
741        ClipOperation::Difference => ClipOperation::Intersection, // clip wants: inside subject
742        ClipOperation::SymmetricDifference => ClipOperation::SymmetricDifference,
743    }
744}
745
746/// Phase 3: Trace result rings by walking the vertex lists.
747fn trace_result_rings(
748    subj: &mut ClipPolygon,
749    clip: &mut ClipPolygon,
750    op: ClipOperation,
751) -> Vec<Vec<Coordinate>> {
752    let mut rings = Vec::new();
753    let max_total = (subj.verts.len() + clip.verts.len()) * 2;
754
755    // Find unvisited entering intersection vertices on the subject list
756    loop {
757        let start = find_unvisited_entering(subj);
758        let start_idx = match start {
759            Some(idx) => idx,
760            None => break,
761        };
762
763        let mut ring_coords: Vec<Coordinate> = Vec::new();
764        let mut on_subject = true;
765        let mut current = start_idx;
766        let mut steps = 0;
767
768        loop {
769            if on_subject {
770                subj.verts[current].visited = true;
771                ring_coords.push(subj.verts[current].coord);
772
773                // If this is an intersection vertex and NOT the start (or we've gone around)
774                current = subj.verts[current].next;
775                steps += 1;
776
777                // Check if we've returned to start
778                if current == start_idx {
779                    break;
780                }
781
782                if steps > max_total {
783                    break;
784                }
785
786                // Check if current is an intersection that is exiting => switch to clip
787                if subj.verts[current].is_intersection && !subj.verts[current].entering {
788                    subj.verts[current].visited = true;
789                    ring_coords.push(subj.verts[current].coord);
790                    if let Some(neighbor) = subj.verts[current].neighbor {
791                        current = clip.verts[neighbor].next;
792                        on_subject = false;
793                    }
794                }
795            } else {
796                // On clip polygon
797                clip.verts[current].visited = true;
798                ring_coords.push(clip.verts[current].coord);
799
800                current = clip.verts[current].next;
801                steps += 1;
802
803                if steps > max_total {
804                    break;
805                }
806
807                // Check if current is an intersection that is exiting on clip => switch to subject
808                if clip.verts[current].is_intersection && !clip.verts[current].entering {
809                    clip.verts[current].visited = true;
810                    ring_coords.push(clip.verts[current].coord);
811                    if let Some(neighbor) = clip.verts[current].neighbor {
812                        // Check if we've returned to start on subject
813                        if neighbor == start_idx {
814                            break;
815                        }
816                        current = subj.verts[neighbor].next;
817                        on_subject = true;
818                    }
819                }
820            }
821        }
822
823        // Close ring and add if valid
824        if ring_coords.len() >= 3 {
825            close_ring(&mut ring_coords);
826            rings.push(ring_coords);
827        }
828    }
829
830    rings
831}
832
833fn find_unvisited_entering(poly: &ClipPolygon) -> Option<usize> {
834    for (i, v) in poly.verts.iter().enumerate() {
835        if v.is_intersection && v.entering && !v.visited {
836            return Some(i);
837        }
838    }
839    None
840}
841
842fn close_ring(coords: &mut Vec<Coordinate>) {
843    if let (Some(first), Some(last)) = (coords.first(), coords.last()) {
844        if (first.x - last.x).abs() > EPSILON || (first.y - last.y).abs() > EPSILON {
845            coords.push(*first);
846        }
847    }
848}
849
850// ---------------------------------------------------------------------------
851// Phase 4: Build output polygons
852// ---------------------------------------------------------------------------
853
854fn build_output_polygons(
855    rings: &[Vec<Coordinate>],
856    subject: &Polygon,
857    clip: &Polygon,
858    op: ClipOperation,
859) -> Result<Vec<Polygon>> {
860    // Separate exterior (CCW / positive area) from hole (CW / negative area) rings
861    let mut exteriors: Vec<Vec<Coordinate>> = Vec::new();
862    let mut holes: Vec<Vec<Coordinate>> = Vec::new();
863
864    for ring in rings {
865        if ring.len() < 4 {
866            continue;
867        }
868        let area = signed_ring_area(ring);
869        if area.abs() < EPSILON {
870            continue; // degenerate ring
871        }
872        // Positive area = CCW = exterior ring; negative = CW = hole
873        // (If the input uses CW exterior convention, this is reversed.
874        //  We detect based on the subject polygon's orientation.)
875        if area > 0.0 {
876            exteriors.push(ring.clone());
877        } else {
878            holes.push(ring.clone());
879        }
880    }
881
882    // If no exteriors were found, the rings might all be CW (different convention).
883    // Flip the classification.
884    if exteriors.is_empty() && !holes.is_empty() {
885        std::mem::swap(&mut exteriors, &mut holes);
886    }
887
888    let mut result = Vec::new();
889
890    for ext_ring in &exteriors {
891        // Collect holes that belong inside this exterior ring
892        let mut assigned_holes = Vec::new();
893        for hole_ring in &holes {
894            if !hole_ring.is_empty() && point_in_ring(&hole_ring[0], ext_ring) {
895                if hole_ring.len() >= 4 {
896                    if let Ok(ls) = LineString::new(hole_ring.clone()) {
897                        assigned_holes.push(ls);
898                    }
899                }
900            }
901        }
902
903        // Also preserve unaffected holes from the subject polygon (for Difference)
904        if op == ClipOperation::Difference {
905            let preserved = filter_unaffected_holes(&subject.interiors, clip)?;
906            for h in preserved {
907                if point_in_ring(&h.coords[0], ext_ring) {
908                    assigned_holes.push(h);
909                }
910            }
911        }
912
913        let ext_ls = LineString::new(ext_ring.clone()).map_err(AlgorithmError::Core)?;
914        let poly = Polygon::new(ext_ls, assigned_holes).map_err(AlgorithmError::Core)?;
915        result.push(poly);
916    }
917
918    if result.is_empty() {
919        // Tracing produced rings but none became valid polygons -- fallback
920        return fallback_vertex_clip(subject, clip, op);
921    }
922
923    Ok(result)
924}
925
926// ---------------------------------------------------------------------------
927// Phase 5: Validate result area
928// ---------------------------------------------------------------------------
929
930/// Validate that the result area satisfies geometric constraints.
931/// If the WA tracing produced incorrect results (wrong entry/exit labeling),
932/// fall back to vertex-subset clipping.
933fn validate_result_area(
934    result: Vec<Polygon>,
935    subject: &Polygon,
936    clip: &Polygon,
937    op: ClipOperation,
938) -> Result<Vec<Polygon>> {
939    let total_area: f64 = result
940        .iter()
941        .map(|p| signed_ring_area(&p.exterior.coords).abs())
942        .sum();
943    let subj_area = signed_ring_area(&subject.exterior.coords).abs();
944    let clip_area = signed_ring_area(&clip.exterior.coords).abs();
945
946    let valid = match op {
947        ClipOperation::Intersection => total_area <= subj_area.min(clip_area) + EPSILON,
948        ClipOperation::Difference => total_area <= subj_area + EPSILON,
949        ClipOperation::Union => total_area <= subj_area + clip_area + EPSILON,
950        ClipOperation::SymmetricDifference => true,
951    };
952
953    if valid {
954        Ok(result)
955    } else {
956        // WA tracing produced geometrically invalid result; fall back
957        fallback_vertex_clip(subject, clip, op)
958    }
959}
960
961// ---------------------------------------------------------------------------
962// Fallback: vertex-subset clipping (for near-degenerate / hard cases)
963// ---------------------------------------------------------------------------
964
965fn fallback_vertex_clip(
966    subject: &Polygon,
967    clip: &Polygon,
968    op: ClipOperation,
969) -> Result<Vec<Polygon>> {
970    let subj_coords = &subject.exterior.coords;
971    let clip_coords = &clip.exterior.coords;
972
973    let mut result_coords: Vec<Coordinate> = Vec::new();
974
975    match op {
976        ClipOperation::Intersection => {
977            // Vertices of subject inside clip + vertices of clip inside subject + intersections
978            for c in subj_coords {
979                if point_in_ring(c, clip_coords) && !is_point_in_any_hole(c, clip).unwrap_or(false)
980                {
981                    push_unique(&mut result_coords, *c);
982                }
983            }
984            for c in clip_coords {
985                if point_in_ring(c, subj_coords)
986                    && !is_point_in_any_hole(c, subject).unwrap_or(false)
987                {
988                    push_unique(&mut result_coords, *c);
989                }
990            }
991            // Add intersection points
992            let ixs = find_all_intersections(subj_coords, clip_coords);
993            for ix in &ixs {
994                push_unique(&mut result_coords, ix.coord);
995            }
996        }
997        ClipOperation::Difference => {
998            // Vertices of subject outside clip + intersection points
999            for c in subj_coords {
1000                if !point_in_ring(c, clip_coords) || is_point_in_any_hole(c, clip).unwrap_or(false)
1001                {
1002                    push_unique(&mut result_coords, *c);
1003                }
1004            }
1005            // Add intersection points
1006            let ixs = find_all_intersections(subj_coords, clip_coords);
1007            for ix in &ixs {
1008                push_unique(&mut result_coords, ix.coord);
1009            }
1010        }
1011        ClipOperation::Union => {
1012            // All vertices of subject + vertices of clip outside subject
1013            for c in subj_coords {
1014                push_unique(&mut result_coords, *c);
1015            }
1016            for c in clip_coords {
1017                if !point_in_ring(c, subj_coords)
1018                    || is_point_in_any_hole(c, subject).unwrap_or(false)
1019                {
1020                    push_unique(&mut result_coords, *c);
1021                }
1022            }
1023        }
1024        ClipOperation::SymmetricDifference => {
1025            // Handled at top level
1026            return Ok(vec![]);
1027        }
1028    }
1029
1030    if result_coords.len() < 3 {
1031        return match op {
1032            ClipOperation::Difference => Ok(vec![subject.clone()]),
1033            ClipOperation::Union => Ok(vec![subject.clone()]),
1034            _ => Ok(vec![]),
1035        };
1036    }
1037
1038    // Order the points to form a valid polygon (convex hull as approximation)
1039    order_points_ccw(&mut result_coords);
1040    close_ring(&mut result_coords);
1041
1042    if result_coords.len() < 4 {
1043        return match op {
1044            ClipOperation::Difference => Ok(vec![subject.clone()]),
1045            ClipOperation::Union => Ok(vec![subject.clone()]),
1046            _ => Ok(vec![]),
1047        };
1048    }
1049
1050    // Validate: for Difference, result area must not exceed subject area.
1051    // For Intersection, result area must not exceed min(subject, clip).
1052    let candidate_area = signed_ring_area(&result_coords).abs();
1053    let subj_area = signed_ring_area(subj_coords).abs();
1054    let clip_area = signed_ring_area(clip_coords).abs();
1055
1056    match op {
1057        ClipOperation::Difference => {
1058            if candidate_area > subj_area + EPSILON {
1059                // Fallback produced an invalid result -- return subject instead
1060                return Ok(vec![subject.clone()]);
1061            }
1062        }
1063        ClipOperation::Intersection => {
1064            let max_valid = subj_area.min(clip_area);
1065            if candidate_area > max_valid + EPSILON {
1066                return Ok(vec![]);
1067            }
1068        }
1069        _ => {}
1070    }
1071
1072    // Handle holes for difference
1073    let interiors = if op == ClipOperation::Difference {
1074        filter_unaffected_holes(&subject.interiors, clip)?
1075    } else {
1076        vec![]
1077    };
1078
1079    let ext = LineString::new(result_coords).map_err(AlgorithmError::Core)?;
1080    let poly = Polygon::new(ext, interiors).map_err(AlgorithmError::Core)?;
1081    Ok(vec![poly])
1082}
1083
1084fn push_unique(coords: &mut Vec<Coordinate>, c: Coordinate) {
1085    let dominated = coords
1086        .iter()
1087        .any(|e| (e.x - c.x).abs() < EPSILON && (e.y - c.y).abs() < EPSILON);
1088    if !dominated {
1089        coords.push(c);
1090    }
1091}
1092
1093/// Order points in counter-clockwise order around their centroid.
1094fn order_points_ccw(coords: &mut [Coordinate]) {
1095    if coords.len() < 3 {
1096        return;
1097    }
1098    let n = coords.len() as f64;
1099    let cx: f64 = coords.iter().map(|c| c.x).sum::<f64>() / n;
1100    let cy: f64 = coords.iter().map(|c| c.y).sum::<f64>() / n;
1101
1102    coords.sort_by(|a, b| {
1103        let angle_a = (a.y - cy).atan2(a.x - cx);
1104        let angle_b = (b.y - cy).atan2(b.x - cx);
1105        angle_a
1106            .partial_cmp(&angle_b)
1107            .unwrap_or(std::cmp::Ordering::Equal)
1108    });
1109}
1110
1111// ---------------------------------------------------------------------------
1112// Shared geometry helpers (used by this module and re-exported to difference.rs)
1113// ---------------------------------------------------------------------------
1114
1115/// Check if all vertices of a ring are contained within a polygon
1116/// (inside exterior and not inside any hole).
1117fn is_ring_contained_in_polygon(ring: &[Coordinate], polygon: &Polygon) -> Result<bool> {
1118    let n = ring_vertex_count(ring);
1119    if n == 0 {
1120        return Ok(false);
1121    }
1122    for i in 0..n {
1123        if !point_in_ring(&ring[i], &polygon.exterior.coords) {
1124            return Ok(false);
1125        }
1126        if is_point_in_any_hole(&ring[i], polygon)? {
1127            return Ok(false);
1128        }
1129    }
1130    Ok(true)
1131}
1132
1133/// Collect holes from one polygon that overlap with the exterior of another.
1134/// Used when one polygon is fully contained in another, to transfer the
1135/// container's holes into the intersection result.
1136fn collect_overlapping_holes(
1137    holes: &[LineString],
1138    contained_exterior: &[Coordinate],
1139) -> Vec<LineString> {
1140    let mut result = Vec::new();
1141    for hole in holes {
1142        if hole.coords.is_empty() || hole.coords.len() < 4 {
1143            continue;
1144        }
1145        // Check if the hole overlaps with the contained exterior
1146        let hole_centroid = compute_ring_centroid(&hole.coords);
1147        if point_in_ring(&hole_centroid, contained_exterior) {
1148            result.push(hole.clone());
1149        }
1150    }
1151    result
1152}
1153
1154/// Check if two closed rings are identical (same vertices in same order,
1155/// possibly with different closing-vertex duplication).
1156fn are_rings_identical(a: &[Coordinate], b: &[Coordinate]) -> bool {
1157    let na = ring_vertex_count(a);
1158    let nb = ring_vertex_count(b);
1159    if na != nb || na == 0 {
1160        return false;
1161    }
1162    for i in 0..na {
1163        if (a[i].x - b[i].x).abs() > EPSILON || (a[i].y - b[i].y).abs() > EPSILON {
1164            return false;
1165        }
1166    }
1167    true
1168}
1169
1170/// Find a point strictly interior to the ring (not on the boundary).
1171/// Uses the midpoint of the first non-degenerate edge, nudged inward.
1172fn find_interior_test_point(coords: &[Coordinate]) -> Coordinate {
1173    let n = ring_vertex_count(coords);
1174    if n < 3 {
1175        return coords
1176            .get(0)
1177            .copied()
1178            .unwrap_or(Coordinate::new_2d(0.0, 0.0));
1179    }
1180
1181    // Compute centroid of the ring (reliable interior point for convex polygons,
1182    // and generally works for simple non-pathological concave polygons)
1183    let cx: f64 = coords[..n].iter().map(|c| c.x).sum::<f64>() / n as f64;
1184    let cy: f64 = coords[..n].iter().map(|c| c.y).sum::<f64>() / n as f64;
1185    Coordinate::new_2d(cx, cy)
1186}
1187
1188/// Ray-casting point-in-ring test.
1189pub(crate) fn point_in_ring(point: &Coordinate, ring: &[Coordinate]) -> bool {
1190    let mut inside = false;
1191    let n = ring.len();
1192    if n < 3 {
1193        return false;
1194    }
1195
1196    let mut j = n - 1;
1197    for i in 0..n {
1198        let xi = ring[i].x;
1199        let yi = ring[i].y;
1200        let xj = ring[j].x;
1201        let yj = ring[j].y;
1202
1203        let intersect = ((yi > point.y) != (yj > point.y))
1204            && (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi);
1205
1206        if intersect {
1207            inside = !inside;
1208        }
1209        j = i;
1210    }
1211    inside
1212}
1213
1214/// Check if a point is inside any hole of a polygon.
1215pub(crate) fn is_point_in_any_hole(point: &Coordinate, polygon: &Polygon) -> Result<bool> {
1216    for hole in &polygon.interiors {
1217        if point_in_ring(point, &hole.coords) {
1218            return Ok(true);
1219        }
1220    }
1221    Ok(false)
1222}
1223
1224/// Signed area of a ring (positive = CCW, negative = CW).
1225pub(crate) fn signed_ring_area(coords: &[Coordinate]) -> f64 {
1226    if coords.len() < 3 {
1227        return 0.0;
1228    }
1229    let mut area = 0.0;
1230    let n = coords.len();
1231    for i in 0..n {
1232        let j = (i + 1) % n;
1233        area += coords[i].x * coords[j].y;
1234        area -= coords[j].x * coords[i].y;
1235    }
1236    area / 2.0
1237}
1238
1239/// Filter holes from subject that are not affected by the clip polygon.
1240pub(crate) fn filter_unaffected_holes(
1241    holes: &[LineString],
1242    clip: &Polygon,
1243) -> Result<Vec<LineString>> {
1244    let mut result = Vec::new();
1245    for hole in holes {
1246        if hole.coords.is_empty() {
1247            continue;
1248        }
1249        // Check if hole centroid is inside clip
1250        let centroid = compute_ring_centroid(&hole.coords);
1251        if !point_in_ring(&centroid, &clip.exterior.coords) {
1252            result.push(hole.clone());
1253        }
1254    }
1255    Ok(result)
1256}
1257
1258/// Compute the centroid of a ring.
1259pub(crate) fn compute_ring_centroid(coords: &[Coordinate]) -> Coordinate {
1260    if coords.is_empty() {
1261        return Coordinate::new_2d(0.0, 0.0);
1262    }
1263    let n = coords.len() as f64;
1264    let sx: f64 = coords.iter().map(|c| c.x).sum();
1265    let sy: f64 = coords.iter().map(|c| c.y).sum();
1266    Coordinate::new_2d(sx / n, sy / n)
1267}
1268
1269// ---------------------------------------------------------------------------
1270// Tests
1271// ---------------------------------------------------------------------------
1272
1273#[cfg(test)]
1274mod tests {
1275    use super::*;
1276
1277    /// Helper: create a square polygon.
1278    fn make_square(x: f64, y: f64, size: f64) -> Result<Polygon> {
1279        let coords = vec![
1280            Coordinate::new_2d(x, y),
1281            Coordinate::new_2d(x + size, y),
1282            Coordinate::new_2d(x + size, y + size),
1283            Coordinate::new_2d(x, y + size),
1284            Coordinate::new_2d(x, y),
1285        ];
1286        let ext = LineString::new(coords).map_err(AlgorithmError::Core)?;
1287        Polygon::new(ext, vec![]).map_err(AlgorithmError::Core)
1288    }
1289
1290    /// Helper: create a square with a rectangular hole.
1291    fn make_square_with_hole(
1292        x: f64,
1293        y: f64,
1294        size: f64,
1295        hx: f64,
1296        hy: f64,
1297        hsize: f64,
1298    ) -> Result<Polygon> {
1299        let ext_coords = vec![
1300            Coordinate::new_2d(x, y),
1301            Coordinate::new_2d(x + size, y),
1302            Coordinate::new_2d(x + size, y + size),
1303            Coordinate::new_2d(x, y + size),
1304            Coordinate::new_2d(x, y),
1305        ];
1306        let hole_coords = vec![
1307            Coordinate::new_2d(hx, hy),
1308            Coordinate::new_2d(hx + hsize, hy),
1309            Coordinate::new_2d(hx + hsize, hy + hsize),
1310            Coordinate::new_2d(hx, hy + hsize),
1311            Coordinate::new_2d(hx, hy),
1312        ];
1313        let ext = LineString::new(ext_coords).map_err(AlgorithmError::Core)?;
1314        let hole = LineString::new(hole_coords).map_err(AlgorithmError::Core)?;
1315        Polygon::new(ext, vec![hole]).map_err(AlgorithmError::Core)
1316    }
1317
1318    /// Helper: compute polygon area (absolute) using shoelace formula.
1319    fn poly_area(poly: &Polygon) -> f64 {
1320        let ext_area = signed_ring_area(&poly.exterior.coords).abs();
1321        let hole_area: f64 = poly
1322            .interiors
1323            .iter()
1324            .map(|h| signed_ring_area(&h.coords).abs())
1325            .sum();
1326        ext_area - hole_area
1327    }
1328
1329    // -----------------------------------------------------------------------
1330    // Intersection tests
1331    // -----------------------------------------------------------------------
1332
1333    #[test]
1334    fn test_intersection_disjoint_squares() {
1335        let a = make_square(0.0, 0.0, 1.0).expect("make a");
1336        let b = make_square(5.0, 5.0, 1.0).expect("make b");
1337        let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
1338        assert!(
1339            result.is_empty(),
1340            "disjoint squares should have empty intersection"
1341        );
1342    }
1343
1344    #[test]
1345    fn test_intersection_overlapping_squares() {
1346        // Two 1x1 squares overlapping by 0.5 in each direction.
1347        // Overlap area should be 0.25.
1348        let a = make_square(0.0, 0.0, 1.0).expect("make a");
1349        let b = make_square(0.5, 0.5, 1.0).expect("make b");
1350        let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
1351        assert!(
1352            !result.is_empty(),
1353            "overlapping squares should have intersection"
1354        );
1355        let total_area: f64 = result.iter().map(|p| poly_area(p)).sum();
1356        assert!(
1357            (total_area - 0.25).abs() < 0.05,
1358            "intersection area should be ~0.25, got {total_area}"
1359        );
1360    }
1361
1362    #[test]
1363    fn test_intersection_containment() {
1364        let outer = make_square(0.0, 0.0, 10.0).expect("outer");
1365        let inner = make_square(2.0, 2.0, 3.0).expect("inner");
1366        let result = clip_polygons(&outer, &inner, ClipOperation::Intersection).expect("clip");
1367        assert_eq!(
1368            result.len(),
1369            1,
1370            "containment intersection should return inner polygon"
1371        );
1372        let area = poly_area(&result[0]);
1373        assert!(
1374            (area - 9.0).abs() < 0.1,
1375            "intersection should have area ~9.0, got {area}"
1376        );
1377    }
1378
1379    // -----------------------------------------------------------------------
1380    // Difference tests
1381    // -----------------------------------------------------------------------
1382
1383    #[test]
1384    fn test_difference_disjoint() {
1385        let a = make_square(0.0, 0.0, 5.0).expect("a");
1386        let b = make_square(10.0, 10.0, 5.0).expect("b");
1387        let result = clip_polygons(&a, &b, ClipOperation::Difference).expect("clip");
1388        assert_eq!(result.len(), 1, "disjoint difference returns subject");
1389    }
1390
1391    #[test]
1392    fn test_difference_contained_creates_hole() {
1393        let outer = make_square(0.0, 0.0, 10.0).expect("outer");
1394        let inner = make_square(2.0, 2.0, 3.0).expect("inner");
1395        let result = clip_polygons(&outer, &inner, ClipOperation::Difference).expect("clip");
1396        assert_eq!(result.len(), 1);
1397        assert_eq!(result[0].interiors.len(), 1, "should have a hole");
1398    }
1399
1400    #[test]
1401    fn test_difference_completely_subtracted() {
1402        let inner = make_square(2.0, 2.0, 3.0).expect("inner");
1403        let outer = make_square(0.0, 0.0, 10.0).expect("outer");
1404        let result = clip_polygons(&inner, &outer, ClipOperation::Difference).expect("clip");
1405        assert!(result.is_empty(), "subject fully inside clip => empty");
1406    }
1407
1408    #[test]
1409    fn test_difference_with_hole_in_subject() {
1410        let a = make_square_with_hole(0.0, 0.0, 20.0, 5.0, 5.0, 5.0).expect("a");
1411        let b = make_square(30.0, 30.0, 5.0).expect("b");
1412        let result = clip_polygons(&a, &b, ClipOperation::Difference).expect("clip");
1413        assert_eq!(result.len(), 1);
1414        assert_eq!(result[0].interiors.len(), 1, "hole should be preserved");
1415    }
1416
1417    // -----------------------------------------------------------------------
1418    // Union tests
1419    // -----------------------------------------------------------------------
1420
1421    #[test]
1422    fn test_union_disjoint() {
1423        let a = make_square(0.0, 0.0, 5.0).expect("a");
1424        let b = make_square(10.0, 10.0, 5.0).expect("b");
1425        let result = clip_polygons(&a, &b, ClipOperation::Union).expect("clip");
1426        assert_eq!(result.len(), 2, "disjoint union returns both");
1427    }
1428
1429    #[test]
1430    fn test_union_containment() {
1431        let outer = make_square(0.0, 0.0, 10.0).expect("outer");
1432        let inner = make_square(2.0, 2.0, 3.0).expect("inner");
1433        let result = clip_polygons(&outer, &inner, ClipOperation::Union).expect("clip");
1434        assert_eq!(result.len(), 1, "containment union returns outer");
1435        let area = poly_area(&result[0]);
1436        assert!(
1437            (area - 100.0).abs() < 0.1,
1438            "union area should be ~100, got {area}"
1439        );
1440    }
1441
1442    // -----------------------------------------------------------------------
1443    // Symmetric difference (XOR) tests
1444    // -----------------------------------------------------------------------
1445
1446    #[test]
1447    fn test_xor_identical_polygons() {
1448        let a = make_square(0.0, 0.0, 5.0).expect("a");
1449        let b = make_square(0.0, 0.0, 5.0).expect("b");
1450        let result = clip_polygons(&a, &b, ClipOperation::SymmetricDifference).expect("clip");
1451        // XOR of identical polygons should be empty
1452        assert!(
1453            result.is_empty(),
1454            "XOR of identical polygons should be empty"
1455        );
1456    }
1457
1458    #[test]
1459    fn test_xor_disjoint() {
1460        let a = make_square(0.0, 0.0, 5.0).expect("a");
1461        let b = make_square(10.0, 10.0, 5.0).expect("b");
1462        let result = clip_polygons(&a, &b, ClipOperation::SymmetricDifference).expect("clip");
1463        assert_eq!(result.len(), 2, "XOR of disjoint returns both");
1464    }
1465
1466    // -----------------------------------------------------------------------
1467    // Concave polygon test (L-shaped)
1468    // -----------------------------------------------------------------------
1469
1470    #[test]
1471    fn test_intersection_l_shaped_concave() {
1472        // L-shape: (0,0)-(2,0)-(2,1)-(1,1)-(1,2)-(0,2)-(0,0)
1473        let l_coords = vec![
1474            Coordinate::new_2d(0.0, 0.0),
1475            Coordinate::new_2d(2.0, 0.0),
1476            Coordinate::new_2d(2.0, 1.0),
1477            Coordinate::new_2d(1.0, 1.0),
1478            Coordinate::new_2d(1.0, 2.0),
1479            Coordinate::new_2d(0.0, 2.0),
1480            Coordinate::new_2d(0.0, 0.0),
1481        ];
1482        let l_ext = LineString::new(l_coords).expect("l");
1483        let l_shape = Polygon::new(l_ext, vec![]).expect("l poly");
1484        let b = make_square(0.5, 0.5, 2.0).expect("b");
1485
1486        let result = clip_polygons(&l_shape, &b, ClipOperation::Intersection).expect("clip");
1487        // The intersection should exist and be smaller than both inputs
1488        assert!(
1489            !result.is_empty(),
1490            "L-shape intersection should produce a result"
1491        );
1492        let total_area: f64 = result.iter().map(|p| poly_area(p)).sum();
1493        assert!(total_area > 0.0, "intersection area should be positive");
1494        assert!(
1495            total_area < 3.0,
1496            "intersection should be smaller than L-shape (area 3)"
1497        );
1498    }
1499
1500    // -----------------------------------------------------------------------
1501    // Degenerate: shared edge
1502    // -----------------------------------------------------------------------
1503
1504    #[test]
1505    fn test_shared_edge_intersection() {
1506        // Two squares sharing an edge: [0,0]-[1,1] and [1,0]-[2,1]
1507        let a = make_square(0.0, 0.0, 1.0).expect("a");
1508        let b = make_square(1.0, 0.0, 1.0).expect("b");
1509        let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
1510        // Shared edge only => degenerate (zero-area) intersection
1511        let total_area: f64 = result.iter().map(|p| poly_area(p)).sum();
1512        assert!(
1513            total_area < 0.01,
1514            "shared-edge intersection should be degenerate (area near 0), got {total_area}"
1515        );
1516    }
1517
1518    // -----------------------------------------------------------------------
1519    // clip_multi test
1520    // -----------------------------------------------------------------------
1521
1522    #[test]
1523    fn test_clip_multi_intersection() {
1524        let subjects = vec![make_square(0.0, 0.0, 10.0).expect("s")];
1525        let clips = vec![
1526            make_square(2.0, 2.0, 5.0).expect("c1"),
1527            make_square(3.0, 3.0, 5.0).expect("c2"),
1528        ];
1529        let result =
1530            clip_multi(&subjects, &clips, ClipOperation::Intersection).expect("clip_multi");
1531        // Should produce a result since the clip polygons overlap with subject
1532        // Not asserting exact geometry but it should not panic
1533        let _ = result;
1534    }
1535
1536    // -----------------------------------------------------------------------
1537    // Self-intersecting "bowtie" polygon
1538    // -----------------------------------------------------------------------
1539
1540    #[test]
1541    fn test_bowtie_self_intersecting() {
1542        // Bowtie: (0,0)-(1,1)-(1,0)-(0,1)-(0,0)  -- self-intersecting at (0.5, 0.5)
1543        let bowtie_coords = vec![
1544            Coordinate::new_2d(0.0, 0.0),
1545            Coordinate::new_2d(1.0, 1.0),
1546            Coordinate::new_2d(1.0, 0.0),
1547            Coordinate::new_2d(0.0, 1.0),
1548            Coordinate::new_2d(0.0, 0.0),
1549        ];
1550        let bowtie_ext = LineString::new(bowtie_coords).expect("bowtie");
1551        let bowtie = Polygon::new(bowtie_ext, vec![]).expect("bowtie poly");
1552        let sq = make_square(0.0, 0.0, 2.0).expect("sq");
1553
1554        // Should not panic, may produce approximate result
1555        let result = clip_polygons(&bowtie, &sq, ClipOperation::Intersection);
1556        assert!(result.is_ok(), "clipping with bowtie should not error");
1557    }
1558
1559    // -----------------------------------------------------------------------
1560    // Polygon with hole: intersection
1561    // -----------------------------------------------------------------------
1562
1563    #[test]
1564    fn test_intersection_with_hole() {
1565        let a = make_square_with_hole(0.0, 0.0, 10.0, 3.0, 3.0, 4.0).expect("a");
1566        let b = make_square(2.0, 2.0, 6.0).expect("b");
1567        let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
1568        // b overlaps both the exterior and the hole of a
1569        // Result should exist and have area < area(b)=36
1570        // Expected: 36 - 16 = 20 (B minus overlap with A's hole)
1571        let total_area: f64 = result.iter().map(|p| poly_area(p)).sum();
1572        assert!(total_area > 0.0, "should have some intersection");
1573        assert!(
1574            total_area < 36.1,
1575            "should be less than b's area, got {total_area}"
1576        );
1577    }
1578
1579    // -----------------------------------------------------------------------
1580    // Validation error tests
1581    // -----------------------------------------------------------------------
1582
1583    #[test]
1584    fn test_invalid_polygon_error() {
1585        // Polygon with too few coordinates
1586        let bad_coords = vec![
1587            Coordinate::new_2d(0.0, 0.0),
1588            Coordinate::new_2d(1.0, 0.0),
1589            Coordinate::new_2d(0.0, 0.0),
1590        ];
1591        let bad_ext = LineString::new(bad_coords);
1592        // LineString::new might reject this, but if it doesn't, clip should
1593        if let Ok(ext) = bad_ext {
1594            let poly = Polygon {
1595                exterior: ext,
1596                interiors: vec![],
1597            };
1598            let good = make_square(0.0, 0.0, 1.0).expect("good");
1599            let result = clip_polygons(&poly, &good, ClipOperation::Intersection);
1600            assert!(result.is_err(), "should reject polygon with < 4 coords");
1601        }
1602    }
1603
1604    // -----------------------------------------------------------------------
1605    // Area invariant tests
1606    // -----------------------------------------------------------------------
1607
1608    #[test]
1609    fn test_area_invariant_intersection_le_min() {
1610        let a = make_square(0.0, 0.0, 3.0).expect("a");
1611        let b = make_square(1.0, 1.0, 3.0).expect("b");
1612        let result = clip_polygons(&a, &b, ClipOperation::Intersection).expect("clip");
1613        let ix_area: f64 = result.iter().map(|p| poly_area(p)).sum();
1614        let area_a = poly_area(&a);
1615        let area_b = poly_area(&b);
1616        let min_area = area_a.min(area_b);
1617        assert!(
1618            ix_area <= min_area + 0.1,
1619            "intersection area ({ix_area}) should be <= min({area_a}, {area_b}) = {min_area}"
1620        );
1621    }
1622
1623    #[test]
1624    fn test_area_invariant_difference_le_subject() {
1625        let a = make_square(0.0, 0.0, 3.0).expect("a");
1626        let b = make_square(1.0, 1.0, 3.0).expect("b");
1627        let result = clip_polygons(&a, &b, ClipOperation::Difference).expect("clip");
1628        let diff_area: f64 = result.iter().map(|p| poly_area(p)).sum();
1629        let area_a = poly_area(&a);
1630        assert!(
1631            diff_area <= area_a + 0.1,
1632            "difference area ({diff_area}) should be <= subject area ({area_a})"
1633        );
1634    }
1635}