1use crate::error::{AlgorithmError, Result};
27use oxigeo_core::vector::{Coordinate, LineString, Polygon};
28
29#[cfg(feature = "std")]
30use std::vec::Vec;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ClipOperation {
39 Intersection,
41 Union,
43 Difference,
45 SymmetricDifference,
47}
48
49#[derive(Debug, Clone)]
59pub struct ClipResult {
60 pub polygons: Vec<Polygon>,
62 pub approximate: bool,
64}
65
66const EPSILON: f64 = 1e-10;
72
73#[derive(Debug, Clone)]
75struct ClipVertex {
76 coord: Coordinate,
78 is_intersection: bool,
80 entering: bool,
82 neighbor: Option<usize>,
84 alpha: f64,
87 next: usize,
89 visited: bool,
91}
92
93struct ClipPolygon {
95 verts: Vec<ClipVertex>,
96}
97
98impl ClipPolygon {
99 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
131pub 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
147pub 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 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 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 let ixs = find_all_intersections(&subject.exterior.coords, &clip.exterior.coords);
191
192 let has_hole_crossings = check_hole_crossings(subject, clip);
194
195 if ixs.is_empty() && !has_hole_crossings {
196 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 return Ok(ClipResult {
207 polygons: handle_hole_crossing_containment(subject, clip, op)?,
208 approximate: false,
209 });
210 }
211
212 let (polygons, approximate) = weiler_atherton_clip(subject, clip, op, &ixs)?;
214 Ok(ClipResult {
215 polygons,
216 approximate,
217 })
218}
219
220pub 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
254fn 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
271fn 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 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 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 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 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 Ok(vec![])
349 } else if clip_in_subj {
350 build_subject_with_hole(subject, clip)
352 } else {
353 Ok(vec![subject.clone()])
354 }
355 }
356 ClipOperation::SymmetricDifference => {
357 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
367fn check_hole_crossings(subject: &Polygon, clip: &Polygon) -> bool {
369 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 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
386fn handle_hole_crossing_containment(
390 subject: &Polygon,
391 clip: &Polygon,
392 op: ClipOperation,
393) -> Result<Vec<Polygon>> {
394 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 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 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 Ok(vec![])
427 } else if clip_inside_subj {
428 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
453fn 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 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#[derive(Debug, Clone)]
484struct SegmentIx {
485 coord: Coordinate,
487 subj_seg: usize,
489 subj_t: f64,
491 clip_seg: usize,
493 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 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
535fn 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
547fn 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; }
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 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 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; }
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
594fn weiler_atherton_clip(
602 subject: &Polygon,
603 clip: &Polygon,
604 op: ClipOperation,
605 ixs: &[SegmentIx],
606) -> Result<(Vec<Polygon>, bool)> {
607 let mut subj_list = ClipPolygon::from_ring(&subject.exterior.coords);
609 let mut clip_list = ClipPolygon::from_ring(&clip.exterior.coords);
610
611 insert_intersections(&mut subj_list, &mut clip_list, ixs);
613
614 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 let rings = trace_result_rings(&mut subj_list, &mut clip_list, op);
621
622 if rings.is_empty() {
623 return fallback_vertex_clip(subject, clip, op);
626 }
627
628 let (result, approx_build) = build_output_polygons(&rings, subject, clip, op)?;
630
631 let (result, approx_validate) = validate_result_area(result, subject, clip, op)?;
633
634 Ok((result, approx_build || approx_validate))
635}
636
637fn insert_intersections(subj: &mut ClipPolygon, clip: &mut ClipPolygon, ixs: &[SegmentIx]) {
640 let mut subj_inserts: Vec<(usize, f64, Coordinate, usize)> = Vec::new(); 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 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 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 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 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 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
707fn label_entry_exit(poly: &mut ClipPolygon, other_ring: &[Coordinate], op: ClipOperation) {
713 let n = poly.len();
716 if n == 0 {
717 return;
718 }
719
720 let want_inside = match op {
722 ClipOperation::Intersection => true,
723 ClipOperation::Union => false,
724 ClipOperation::Difference => false,
725 ClipOperation::SymmetricDifference => false, };
727
728 let mut idx = 0;
730 let max_steps = poly.verts.len() * 2; let mut steps = 0;
732 loop {
733 if poly.verts[idx].is_intersection {
734 let prev_coord = find_prev_original_coord(poly, idx);
737 let was_inside = point_in_ring(&prev_coord, other_ring);
738
739 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
752fn find_prev_original_coord(poly: &ClipPolygon, target: usize) -> Coordinate {
754 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 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
793fn 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, ClipOperation::SymmetricDifference => ClipOperation::SymmetricDifference,
800 }
801}
802
803fn 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 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 current = subj.verts[current].next;
832 steps += 1;
833
834 if current == start_idx {
836 break;
837 }
838
839 if steps > max_total {
840 break;
841 }
842
843 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 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 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 if neighbor == start_idx {
871 break;
872 }
873 current = subj.verts[neighbor].next;
874 on_subject = true;
875 }
876 }
877 }
878 }
879
880 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
907fn build_output_polygons(
912 rings: &[Vec<Coordinate>],
913 subject: &Polygon,
914 clip: &Polygon,
915 op: ClipOperation,
916) -> Result<(Vec<Polygon>, bool)> {
917 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; }
929 if area > 0.0 {
933 exteriors.push(ring.clone());
934 } else {
935 holes.push(ring.clone());
936 }
937 }
938
939 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 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 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 return fallback_vertex_clip(subject, clip, op);
978 }
979
980 Ok((result, false))
982}
983
984fn 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 fallback_vertex_clip(subject, clip, op)
1016 }
1017}
1018
1019fn 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 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 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 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 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 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 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 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 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 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 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 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
1168fn 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
1186fn 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
1208fn 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 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
1229fn 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
1245fn 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 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
1263pub(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
1289pub(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
1299pub(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
1314pub(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 let centroid = compute_ring_centroid(&hole.coords);
1326 if !point_in_ring(¢roid, &clip.exterior.coords) {
1327 result.push(hole.clone());
1328 }
1329 }
1330 Ok(result)
1331}
1332
1333pub(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#[cfg(test)]
1349mod tests {
1350 use super::*;
1351
1352 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 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 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 #[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 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 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 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 #[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 #[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 #[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 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 #[test]
1582 fn test_intersection_l_shaped_concave() {
1583 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 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 #[test]
1616 fn test_shared_edge_intersection() {
1617 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 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 #[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 let _ = result;
1645 }
1646
1647 #[test]
1652 fn test_bowtie_self_intersecting() {
1653 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 let result = clip_polygons(&bowtie, &sq, ClipOperation::Intersection);
1667 assert!(result.is_ok(), "clipping with bowtie should not error");
1668 }
1669
1670 #[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 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 #[test]
1695 fn test_invalid_polygon_error() {
1696 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 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 #[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}