Skip to main content

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