1use crate::containment::ContainmentForest;
2use crate::diagnostics::{ContainmentStats, PolygonizerDiagnostics};
3use crate::error::Result;
4use crate::graph::{ExtractedRing, PlanarGraph};
5use crate::noding::advanced::AdvancedNoder;
6use crate::noding::snap::SnapNoder;
7use crate::options::DiagnosticsOptions;
8use crate::options::{DeterminismOptions, PolygonizerOptions, SnapStrategy};
9use crate::types::{Coord3D, Line3D, Polygon3D, RingGraphIdentity};
10use crate::utils::simd::SimdRing;
11use crate::utils::z_order_index;
12use geo::Contains;
13use geo_types::{Coord, Geometry, Polygon};
14use std::collections::HashMap;
15
16#[cfg(not(target_arch = "wasm32"))]
17use std::time::Instant;
18#[cfg(target_arch = "wasm32")]
19use web_time::Instant;
20
21fn get_time() -> Instant {
22 Instant::now()
23}
24
25fn get_elapsed(start: Instant) -> std::time::Duration {
26 start.elapsed()
27}
28
29#[cfg(feature = "parallel")]
30use rayon::prelude::*;
31
32pub struct Polygonizer {
34 graph: PlanarGraph,
35 pub check_valid_rings: bool,
37 pub options: PolygonizerOptions,
38
39 pub node_input: bool,
41 pub snap_grid_size: f64,
42 pub extract_only_polygonal: bool,
43 pub determinism: DeterminismOptions,
44 pub diagnostics_options: DiagnosticsOptions,
45
46 input_lines: Vec<Line3D>,
48 dirty: bool,
49}
50
51#[derive(Clone, Debug, serde::Serialize)]
52pub struct PolygonizerResult {
53 pub polygons: Vec<Polygon3D>,
54 pub dangles: Vec<Vec<Coord3D>>,
55 pub cut_edges: Vec<Vec<Coord3D>>,
56 pub invalid_rings: Vec<Vec<Coord3D>>,
57 pub diagnostics: Option<PolygonizerDiagnostics>,
58}
59
60impl Default for Polygonizer {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66pub fn polygonize_with_options(
68 lines: &[Line3D],
69 options: &PolygonizerOptions,
70) -> Result<PolygonizerResult> {
71 let mut polygonizer = Polygonizer::with_options(options.clone());
72 polygonizer.add_lines(lines.to_vec());
73 polygonizer.polygonize()
74}
75
76impl Polygonizer {
77 pub fn new() -> Self {
79 Self {
80 graph: PlanarGraph::new(),
81 check_valid_rings: true,
82 options: PolygonizerOptions::default(),
83 node_input: false,
84 snap_grid_size: 1e-10, extract_only_polygonal: false,
86 determinism: DeterminismOptions::default(),
87 diagnostics_options: DiagnosticsOptions::default(),
88 input_lines: Vec::new(),
89 dirty: false,
90 }
91 }
92 pub fn with_options(options: PolygonizerOptions) -> Self {
94 Self {
95 graph: PlanarGraph::new(),
96 check_valid_rings: true,
97 node_input: options.node_input,
98 snap_grid_size: options.snap_grid_size,
99 extract_only_polygonal: options.extract_only_polygonal,
100 determinism: options.determinism.clone(),
101 diagnostics_options: options.diagnostics.clone(),
102 options,
103 input_lines: Vec::new(),
104 dirty: false,
105 }
106 }
107
108 pub fn with_snap_grid(mut self, grid_size: f64) -> Self {
114 self.snap_grid_size = grid_size;
115 self
116 }
117
118 pub fn add_geometry(&mut self, geom: Geometry<f64>) {
120 extract_segments(&geom, &mut self.input_lines);
121 self.dirty = true;
122 }
123
124 pub fn add_borrowed_geometry(&mut self, geom: &Geometry<f64>) {
126 extract_segments(geom, &mut self.input_lines);
127 self.dirty = true;
128 }
129
130 pub fn add_lines(&mut self, lines: Vec<Line3D>) {
132 self.input_lines.extend(lines);
133 self.dirty = true;
134 }
135
136 fn build_graph(&mut self, diagnostics: Option<&mut PolygonizerDiagnostics>) -> Result<()> {
137 if !self.dirty {
138 return Ok(());
139 }
140
141 self.options.node_input = self.node_input;
143 self.options.snap_grid_size = self.snap_grid_size;
144 self.options.extract_only_polygonal = self.extract_only_polygonal;
145 self.options.determinism = self.determinism.clone();
146 self.options.diagnostics = self.diagnostics_options.clone();
147
148 let mut all_segments: Vec<Line3D> = self.input_lines.clone();
149
150 let mut segments;
151
152 if self.options.node_input {
153 let mut pre_snap_vertex_candidates = 0;
154 if self.options.pre_snap_tolerance > 0.0 {
155 let (snapped, candidates) = SnapNoder::pre_snap_to_reference_vertices_with_stats(
156 &all_segments,
157 self.options.pre_snap_tolerance,
158 );
159 all_segments = snapped;
160 pre_snap_vertex_candidates = candidates;
161 }
162
163 all_segments.sort_unstable_by(|a, b| {
165 a.start
166 .x
167 .total_cmp(&b.start.x)
168 .then(a.start.y.total_cmp(&b.start.y))
169 });
170 all_segments.dedup_by(|a, b| {
173 a.start.x == b.start.x && a.start.y == b.start.y
174 && a.end.x == b.end.x && a.end.y == b.end.y
175 && a.start.z == b.start.z && a.end.z == b.end.z
178 });
179
180 let mut numbered_lines: Vec<(u64, Line3D)> = all_segments
182 .iter()
183 .map(|l| (z_order_index(l.start.to_coord_2d()), *l))
184 .collect();
185
186 numbered_lines.sort_unstable_by_key(|k| k.0);
188
189 all_segments = numbered_lines.into_iter().map(|k| k.1).collect();
190
191 match self.options.noding.backend {
192 crate::options::NodingBackend::Snap => {
193 let noder = SnapNoder::new(self.options.snap_grid_size)
194 .with_snap_strategy(self.options.snap_strategy.clone());
195 let restore_coordinates =
198 matches!(self.options.snap_strategy, SnapStrategy::GeosCompat)
199 .then(|| restored_coordinates(&noder, &all_segments));
200 if let Some(diagnostics) = diagnostics {
201 let (noded, stats, mut work_stats) = noder.node_with_stats(all_segments);
202 work_stats.pre_snap_vertex_candidates = pre_snap_vertex_candidates;
203 diagnostics.intersection_stats.interpolated_intersections = stats
204 .iter()
205 .map(|iteration| iteration.intersections_found)
206 .sum();
207 diagnostics.noding_iterations = stats;
208 diagnostics.intersection_stats.exact_intersections =
209 work_stats.exact_intersection_calls;
210 diagnostics.noding_work_stats = work_stats;
211 segments = noded;
212 } else {
213 segments = noder.node(all_segments);
214 }
215 if let Some(coordinates) = restore_coordinates {
216 restore_noded_coordinates(&mut segments, &coordinates);
217 }
218 }
219 crate::options::NodingBackend::Advanced => {
220 let noder = AdvancedNoder::new();
221 if let Some(diagnostics) = diagnostics {
222 let (noded, stats, mut work_stats) =
223 SnapNoder::new(0.0).node_with_stats(all_segments);
224 work_stats.pre_snap_vertex_candidates = pre_snap_vertex_candidates;
225 diagnostics.intersection_stats.interpolated_intersections = stats
226 .iter()
227 .map(|iteration| iteration.intersections_found)
228 .sum();
229 diagnostics.noding_iterations = stats;
230 diagnostics.intersection_stats.exact_intersections =
231 work_stats.exact_intersection_calls;
232 diagnostics.noding_work_stats = work_stats;
233 segments = noded;
234 } else {
235 segments = noder.node(all_segments);
236 }
237 }
238 }
239 } else {
240 segments = all_segments;
241 }
242
243 self.graph.bulk_load(segments);
245
246 self.dirty = false;
247 Ok(())
248 }
249
250 pub fn polygonize(&mut self) -> Result<PolygonizerResult> {
255 self.options.node_input = self.node_input;
257 self.options.snap_grid_size = self.snap_grid_size;
258 self.options.extract_only_polygonal = self.extract_only_polygonal;
259 self.options.determinism = self.determinism.clone();
260 self.options.diagnostics = self.diagnostics_options.clone();
261
262 let mut diag = if self.options.diagnostics.enabled || self.options.diagnostics.timings {
263 let d = PolygonizerDiagnostics {
264 input_segment_count: self.input_lines.len(),
265 ..Default::default()
266 };
267 Some(d)
268 } else {
269 None
270 };
271
272 let t_ingest_start = get_time();
273 self.build_graph(if self.options.diagnostics.enabled {
274 diag.as_mut()
275 } else {
276 None
277 })?;
278 if let Some(ref mut d) = diag {
279 d.phase_times.ingest_and_node = get_elapsed(t_ingest_start);
280 d.noded_segment_count = self.graph.edges.len();
281 }
282
283 let t_graph_build_start = get_time();
284 self.graph.sort_edges();
286
287 let mut dangles = self.graph.prune_dangles();
289
290 let rings_with_ids = self
292 .graph
293 .get_edge_rings_with_graph_ids(self.options.node_input);
294
295 let mut cut_edges = self.graph.get_cut_edges();
297
298 if let Some(ref mut d) = diag {
299 d.phase_times.graph_build = get_elapsed(t_graph_build_start);
300 d.ring_count = rings_with_ids.len();
301 d.cut_edge_count = cut_edges.len();
302 d.dangle_count = dangles.len();
303 }
304
305 let t_ring_extraction_start = get_time();
307 let (shells, holes, invalid_rings_candidates) =
308 extract_and_classify_rings(rings_with_ids, self.options.node_input);
309
310 if let Some(ref mut d) = diag {
311 d.phase_times.ring_extraction = get_elapsed(t_ring_extraction_start);
312 d.shell_count = shells.len();
313 d.hole_count = holes.len();
314 }
315
316 let t_containment_start = get_time();
317 let (
319 shells,
320 shell_holes,
321 shell_holes_ids,
322 unassigned_hole_count,
323 unassigned_hole_area,
324 containment_stats,
325 ) = establish_topology(shells, holes, &self.options);
326
327 let mut invalid_rings = if invalid_rings_candidates.is_empty() {
330 Vec::new()
331 } else {
332 let shells_2d: Vec<Polygon<f64>> = shells.iter().map(|s| s.to_polygon_2d()).collect();
333 process_invalid_rings(invalid_rings_candidates, &shells_2d)
334 };
335
336 let mut result =
337 construct_final_polygons(shells, shell_holes, shell_holes_ids, &self.options);
338
339 result = apply_determinism(
340 result,
341 &mut dangles,
342 &mut cut_edges,
343 &mut invalid_rings,
344 &self.options,
345 );
346
347 if let Some(ref mut d) = diag {
348 d.phase_times.containment = get_elapsed(t_containment_start);
349 d.unassigned_hole_count = unassigned_hole_count;
350 d.unassigned_hole_area = unassigned_hole_area;
351 d.containment_stats = containment_stats;
352 d.invalid_ring_count = invalid_rings.len();
353 }
355 Ok(PolygonizerResult {
356 polygons: result,
357 dangles,
358 cut_edges,
359 invalid_rings,
360 diagnostics: diag,
361 })
362 }
363}
364
365fn restored_coordinates(noder: &SnapNoder, lines: &[Line3D]) -> HashMap<(u64, u64), Coord3D> {
366 let mut coordinates = HashMap::with_capacity(lines.len() * 2);
367 for coord in lines.iter().flat_map(|line| [line.start, line.end]) {
368 let snapped = noder.snap(coord);
369 let key = coordinate_key(snapped);
370 coordinates
371 .entry(key)
372 .and_modify(|current: &mut Coord3D| {
373 let distance = (coord.x - snapped.x).powi(2) + (coord.y - snapped.y).powi(2);
374 let current_distance =
375 (current.x - snapped.x).powi(2) + (current.y - snapped.y).powi(2);
376 if distance.total_cmp(¤t_distance).is_lt()
377 || (distance == current_distance
378 && (coord.x, coord.y, coord.z) < (current.x, current.y, current.z))
379 {
380 *current = coord;
381 }
382 })
383 .or_insert(coord);
384 }
385 coordinates
386}
387
388fn coordinate_key(coord: Coord3D) -> (u64, u64) {
389 let x = if coord.x == 0.0 { 0.0 } else { coord.x };
390 let y = if coord.y == 0.0 { 0.0 } else { coord.y };
391 (x.to_bits(), y.to_bits())
392}
393
394fn restore_noded_coordinates(lines: &mut [Line3D], coordinates: &HashMap<(u64, u64), Coord3D>) {
395 for line in lines {
396 line.start = coordinates
397 .get(&coordinate_key(line.start))
398 .copied()
399 .unwrap_or(line.start);
400 line.end = coordinates
401 .get(&coordinate_key(line.end))
402 .copied()
403 .unwrap_or(line.end);
404 }
405}
406
407pub(crate) fn canonicalize_ring(ring: &mut Vec<Coord3D>, mut ids: Option<&mut Vec<u32>>) {
408 if ring.is_empty() {
409 return;
410 }
411 let n = if ring.len() > 1 && ring.first() == ring.last() {
412 ring.len() - 1
413 } else {
414 ring.len()
415 };
416
417 let min_idx = ring[..n]
420 .iter()
421 .enumerate()
422 .min_by(|(_, a), (_, b)| {
423 a.x.total_cmp(&b.x)
424 .then(a.y.total_cmp(&b.y))
425 .then(a.z.total_cmp(&b.z))
426 })
427 .map(|(i, _)| i)
428 .unwrap_or(0);
429
430 if min_idx > 0 {
431 ring[..n].rotate_left(min_idx);
432 if n < ring.len() {
433 ring[n] = ring[0];
434 } else {
435 ring.push(ring[0]);
436 }
437
438 if let Some(ref mut ids_vec) = ids {
439 if !ids_vec.is_empty() {
440 ids_vec.rotate_left(min_idx);
441 }
442 }
443 }
444}
445
446pub(crate) fn canonicalize_open_line(line: &mut [Coord3D]) {
447 if line.len() > 1 {
448 let first = line.first().unwrap();
449 let last = line.last().unwrap();
450 let cmp = last
451 .x
452 .total_cmp(&first.x)
453 .then(last.y.total_cmp(&first.y))
454 .then(last.z.total_cmp(&first.z));
455 if cmp == std::cmp::Ordering::Less {
456 line.reverse();
457 }
458 }
459}
460
461type ClassifiedRing = (Polygon3D, Option<RingGraphIdentity>);
462
463pub(crate) fn extract_and_classify_rings(
464 rings_with_ids: Vec<ExtractedRing>,
465 include_graph_ids: bool,
466) -> (Vec<ClassifiedRing>, Vec<ClassifiedRing>, Vec<Polygon3D>) {
467 let mut shells = Vec::with_capacity(rings_with_ids.len() / 2);
468 let mut holes = Vec::with_capacity(rings_with_ids.len() / 2);
469 let mut invalid_rings_candidates = Vec::new();
470
471 for ring in rings_with_ids {
472 let poly3d = Polygon3D::new(ring.coords, vec![], ring.line_ids, vec![]);
473 let area = poly3d.signed_area_2d();
474
475 if !area.is_finite() || area.abs() < 1e-9 {
476 invalid_rings_candidates.push(poly3d);
477 continue;
478 }
479
480 let ring = (
481 poly3d,
482 include_graph_ids.then(|| RingGraphIdentity::new(ring.edge_keys, ring.node_ids)),
483 );
484 if area > 0.0 {
485 shells.push(ring);
486 } else {
487 holes.push(ring);
488 }
489 }
490
491 (shells, holes, invalid_rings_candidates)
492}
493
494#[allow(clippy::type_complexity)]
495fn establish_topology(
496 shells: Vec<ClassifiedRing>,
497 holes: Vec<ClassifiedRing>,
498 options: &PolygonizerOptions,
499) -> (
500 Vec<Polygon3D>,
501 Vec<Vec<Vec<Coord3D>>>,
502 Vec<Vec<Vec<u32>>>,
503 usize,
504 f64,
505 ContainmentStats,
506) {
507 let (mut shells, mut shell_graph_ids): (Vec<_>, Vec<_>) = shells.into_iter().unzip();
508 let collect_stats = options.diagnostics.enabled;
509 let mut containment_stats = ContainmentStats::default();
510 if collect_stats {
511 containment_stats.prepared_shells = shells.len();
512 }
513 let mut forest = ContainmentForest::new_with_graph_ids(&shells, &shell_graph_ids);
514
515 if options.extract_only_polygonal {
516 let keep_mask = if collect_stats {
517 let (keep_mask, stats) =
518 forest.filter_polygonal_with_stats(&shells, &options.containment.touch_policy);
519 containment_stats.merge(stats);
520 keep_mask
521 } else {
522 forest.filter_polygonal(&shells, &options.containment.touch_policy)
523 };
524 let removed_count = keep_mask.iter().filter(|&&keep| !keep).count();
525
526 if removed_count > 0 {
527 if collect_stats {
528 forest.record_locator_reuse(&mut containment_stats);
529 }
530 let mut new_shells = Vec::new();
531 let mut new_shell_graph_ids = Vec::new();
532
533 for ((keep, shell), graph_ids) in keep_mask.into_iter().zip(shells).zip(shell_graph_ids)
534 {
535 if keep {
536 new_shells.push(shell);
537 new_shell_graph_ids.push(graph_ids);
538 }
539 }
540 shells = new_shells;
541 shell_graph_ids = new_shell_graph_ids;
542 if collect_stats {
543 containment_stats.prepared_shells += shells.len();
544 }
545 forest = ContainmentForest::new_with_graph_ids(&shells, &shell_graph_ids);
546 }
547 }
548
549 let process_hole_assignment = |(hole_3d, graph_ids): (
550 Polygon3D,
551 Option<RingGraphIdentity>,
552 )|
553 -> (Option<usize>, Polygon3D, ContainmentStats) {
554 let (best_shell_idx, stats) = if collect_stats {
555 forest.assign_hole_with_graph_ids_and_stats(
556 &hole_3d,
557 graph_ids.as_ref(),
558 &shells,
559 &options.containment.touch_policy,
560 )
561 } else {
562 (
563 forest.assign_hole_with_graph_ids(
564 &hole_3d,
565 graph_ids.as_ref(),
566 &shells,
567 &options.containment.touch_policy,
568 ),
569 ContainmentStats::default(),
570 )
571 };
572 (best_shell_idx, hole_3d, stats)
573 };
574
575 let assignments: Vec<_>;
576 #[cfg(feature = "parallel")]
577 {
578 assignments = holes.into_par_iter().map(process_hole_assignment).collect();
579 }
580 #[cfg(not(feature = "parallel"))]
581 {
582 assignments = holes.into_iter().map(process_hole_assignment).collect();
583 }
584
585 let mut shell_holes: Vec<Vec<Vec<Coord3D>>> = vec![vec![]; shells.len()];
586 let mut shell_holes_ids: Vec<Vec<Vec<u32>>> = vec![vec![]; shells.len()];
587 let mut unassigned_hole_count = 0;
588 let mut unassigned_hole_area = 0.0;
589
590 for (idx, hole, stats) in assignments {
591 containment_stats.merge(stats);
592 if let Some(idx) = idx {
593 shell_holes[idx].push(hole.exterior);
594 shell_holes_ids[idx].push(hole.exterior_ids);
595 } else {
596 unassigned_hole_count += 1;
597 unassigned_hole_area += hole.exterior_unsigned_area_2d();
598 }
599 }
600 if collect_stats {
601 forest.record_locator_reuse(&mut containment_stats);
602 }
603
604 (
605 shells,
606 shell_holes,
607 shell_holes_ids,
608 unassigned_hole_count,
609 unassigned_hole_area,
610 containment_stats,
611 )
612}
613
614pub(crate) fn construct_final_polygons(
615 shells: Vec<Polygon3D>,
616 shell_holes: Vec<Vec<Vec<Coord3D>>>,
617 shell_holes_ids: Vec<Vec<Vec<u32>>>,
618 options: &PolygonizerOptions,
619) -> Vec<Polygon3D> {
620 let mut result = Vec::with_capacity(shells.len());
621 for ((shell, mut holes), mut holes_ids) in
622 shells.into_iter().zip(shell_holes).zip(shell_holes_ids)
623 {
624 let mut exterior = shell.exterior;
625 let mut exterior_ids = shell.exterior_ids;
626
627 if options.determinism.canonical_ring_rotation {
628 canonicalize_ring(&mut exterior, Some(&mut exterior_ids));
629 for (h, h_ids) in holes.iter_mut().zip(holes_ids.iter_mut()) {
630 canonicalize_ring(h, Some(h_ids));
631 }
632 }
633
634 if options.determinism.canonical_sort {
635 let mut combined_holes: Vec<_> = holes
636 .into_iter()
637 .zip(holes_ids)
638 .map(|(h, hi)| {
639 let area = Polygon3D::ring_signed_area_2d(&h).abs();
640 (h, hi, area)
641 })
642 .collect();
643
644 let use_stable_tie_breaks = options.determinism.stable_tie_breaks;
645 if use_stable_tie_breaks {
646 let mut combined_holes_with_bbox: Vec<_> = combined_holes
647 .into_iter()
648 .map(|(h, hi, area)| {
649 let b = bounding_rect_3d(&h).unwrap_or(geo::Rect::new(
650 geo::Coord { x: 0.0, y: 0.0 },
651 geo::Coord { x: 0.0, y: 0.0 },
652 ));
653 (h, hi, area, b)
654 })
655 .collect();
656 combined_holes_with_bbox.sort_unstable_by(
657 |(h1, _, area1, b1), (h2, _, area2, b2)| {
658 area2.total_cmp(area1).then_with(|| {
659 b1.min()
660 .x
661 .total_cmp(&b2.min().x)
662 .then(b1.min().y.total_cmp(&b2.min().y))
663 .then(h1.len().cmp(&h2.len()))
664 })
665 },
666 );
667
668 let mut h = Vec::with_capacity(combined_holes_with_bbox.len());
669 let mut hi = Vec::with_capacity(combined_holes_with_bbox.len());
670 for (hole, ids, _, _) in combined_holes_with_bbox {
671 h.push(hole);
672 hi.push(ids);
673 }
674 holes = h;
675 holes_ids = hi;
676 } else {
677 combined_holes
678 .sort_unstable_by(|(_, _, area1), (_, _, area2)| area2.total_cmp(area1));
679
680 let mut h = Vec::with_capacity(combined_holes.len());
681 let mut hi = Vec::with_capacity(combined_holes.len());
682 for (hole, ids, _) in combined_holes {
683 h.push(hole);
684 hi.push(ids);
685 }
686 holes = h;
687 holes_ids = hi;
688 }
689 }
690
691 let mut p = Polygon3D::new(exterior, holes, exterior_ids, holes_ids);
692
693 if options.provenance.enabled {
694 let mut b_ids = Vec::new();
695 if options.provenance.include_boundary_line_ids {
696 for &id in &p.exterior_ids {
697 if id != 0 {
698 b_ids.push(id as u64);
699 }
700 }
701 for hole_ids in &p.interiors_ids {
702 for &id in hole_ids {
703 if id != 0 {
704 b_ids.push(id as u64);
705 }
706 }
707 }
708 b_ids.sort_unstable();
709 b_ids.dedup();
710 }
711
712 p.provenance = Some(crate::types::PolygonProvenance {
713 boundary_line_ids: b_ids,
714 input_profile_id: options.input_profile_id.clone(),
715 });
716 }
717
718 if p.unsigned_area_2d() > 1e-6 {
719 result.push(p);
720 }
721 }
722 result
723}
724
725#[cfg(test)]
726mod topology_tests {
727 use super::*;
728
729 #[test]
730 fn establish_topology_reports_unassigned_holes() {
731 let hole = Polygon3D::new(
732 vec![
733 Coord3D::new(0.0, 0.0, 0.0),
734 Coord3D::new(0.0, 10.0, 0.0),
735 Coord3D::new(10.0, 10.0, 0.0),
736 Coord3D::new(10.0, 0.0, 0.0),
737 Coord3D::new(0.0, 0.0, 0.0),
738 ],
739 vec![],
740 vec![1, 2, 3, 4],
741 vec![],
742 );
743
744 let (_, _, _, unassigned_count, unassigned_area, _) =
745 establish_topology(vec![], vec![(hole, None)], &PolygonizerOptions::default());
746
747 assert_eq!(unassigned_count, 1);
748 assert!((unassigned_area - 100.0).abs() < 1e-9);
749 }
750
751 #[test]
752 fn reorder_polygons_applies_sorted_old_indices() {
753 let polygon =
754 |label| Polygon3D::new(vec![Coord3D::new(label, 0.0, 0.0)], vec![], vec![], vec![]);
755 let mut polygons = vec![polygon(0.0), polygon(1.0), polygon(2.0)];
756
757 reorder_polygons(&mut polygons, [2, 0, 1].into_iter());
758
759 let labels: Vec<_> = polygons.iter().map(|p| p.exterior[0].x).collect();
760 assert_eq!(labels, [2.0, 0.0, 1.0]);
761 }
762}
763
764pub(crate) fn apply_determinism(
765 mut result: Vec<Polygon3D>,
766 dangles: &mut [Vec<Coord3D>],
767 cut_edges: &mut [Vec<Coord3D>],
768 invalid_rings: &mut Vec<Vec<Coord3D>>,
769 options: &PolygonizerOptions,
770) -> Vec<Polygon3D> {
771 if options.determinism.canonical_sort {
772 let use_stable_tie_breaks = options.determinism.stable_tie_breaks;
773 if use_stable_tie_breaks {
774 let mut sort_keys: Vec<_> = result
775 .iter()
776 .enumerate()
777 .map(|(index, p)| {
778 let area = p.exterior_unsigned_area_2d();
779 let b = bounding_rect_3d(&p.exterior).unwrap_or(geo::Rect::new(
780 geo::Coord { x: 0.0, y: 0.0 },
781 geo::Coord { x: 0.0, y: 0.0 },
782 ));
783 (index, area, b, p.interiors.len())
784 })
785 .collect();
786
787 sort_keys.sort_unstable_by(|(_, area1, b1, holes1), (_, area2, b2, holes2)| {
788 area2.total_cmp(area1).then_with(|| {
789 b1.min()
790 .x
791 .total_cmp(&b2.min().x)
792 .then(b1.min().y.total_cmp(&b2.min().y))
793 .then(holes1.cmp(holes2))
794 })
795 });
796
797 reorder_polygons(
798 &mut result,
799 sort_keys.into_iter().map(|(index, _, _, _)| index),
800 );
801 } else {
802 let mut sort_keys: Vec<_> = result
803 .iter()
804 .enumerate()
805 .map(|(index, p)| {
806 let area = p.exterior_unsigned_area_2d();
807 (index, area)
808 })
809 .collect();
810
811 sort_keys.sort_unstable_by(|(_, area1), (_, area2)| area2.total_cmp(area1));
812
813 reorder_polygons(&mut result, sort_keys.into_iter().map(|(index, _)| index));
814 }
815
816 if options.determinism.canonical_ring_rotation {
817 for d in dangles.iter_mut() {
818 canonicalize_open_line(d);
819 }
820 for edge in cut_edges.iter_mut() {
821 canonicalize_open_line(edge);
822 }
823 for ir in invalid_rings.iter_mut() {
824 canonicalize_ring(ir, None);
825 }
826 }
827
828 if use_stable_tie_breaks {
829 let mut dangles_with_cache: Vec<_> = dangles
833 .iter_mut()
834 .map(|l| {
835 let b = bounding_rect_3d(l).unwrap_or(geo::Rect::new(
836 geo::Coord { x: 0.0, y: 0.0 },
837 geo::Coord { x: 0.0, y: 0.0 },
838 ));
839 (std::mem::take(l), b)
840 })
841 .collect();
842
843 dangles_with_cache.sort_unstable_by(|(l1, b1), (l2, b2)| {
844 b1.min()
845 .x
846 .total_cmp(&b2.min().x)
847 .then(b1.min().y.total_cmp(&b2.min().y))
848 .then(l1.len().cmp(&l2.len()))
849 });
850
851 for (i, (l, _)) in dangles_with_cache.into_iter().enumerate() {
852 dangles[i] = l;
853 }
854 sort_open_lines(cut_edges);
855 }
856
857 let mut combined_invalid: Vec<_> = invalid_rings
858 .drain(..)
859 .map(|r| {
860 let area = Polygon3D::ring_signed_area_2d(&r).abs();
861 (r, area)
862 })
863 .collect();
864
865 if use_stable_tie_breaks {
866 let mut invalid_with_bbox: Vec<_> = combined_invalid
867 .into_iter()
868 .map(|(r, area)| {
869 let b = bounding_rect_3d(&r).unwrap_or(geo::Rect::new(
870 geo::Coord { x: 0.0, y: 0.0 },
871 geo::Coord { x: 0.0, y: 0.0 },
872 ));
873 (r, area, b)
874 })
875 .collect();
876
877 invalid_with_bbox.sort_unstable_by(|(r1, area1, b1), (r2, area2, b2)| {
878 area2.total_cmp(area1).then_with(|| {
879 b1.min()
880 .x
881 .total_cmp(&b2.min().x)
882 .then(b1.min().y.total_cmp(&b2.min().y))
883 .then(r1.len().cmp(&r2.len()))
884 })
885 });
886 invalid_rings.extend(invalid_with_bbox.into_iter().map(|(r, _, _)| r));
887 } else {
888 combined_invalid.sort_unstable_by(|(_, area1), (_, area2)| area2.total_cmp(area1));
889 invalid_rings.extend(combined_invalid.into_iter().map(|(r, _)| r));
890 }
891 }
892 result
893}
894
895fn reorder_polygons(result: &mut [Polygon3D], old_indices: impl Iterator<Item = usize>) {
896 let mut destinations = vec![0; result.len()];
897 for (new_index, old_index) in old_indices.enumerate() {
898 destinations[old_index] = new_index;
899 }
900 for index in 0..destinations.len() {
901 while destinations[index] != index {
902 let destination = destinations[index];
903 result.swap(index, destination);
904 destinations.swap(index, destination);
905 }
906 }
907}
908
909fn sort_open_lines(lines: &mut [Vec<Coord3D>]) {
910 let mut with_cache: Vec<_> = lines
911 .iter_mut()
912 .map(|l| {
913 let b = bounding_rect_3d(l).unwrap_or(geo::Rect::new(
914 geo::Coord { x: 0.0, y: 0.0 },
915 geo::Coord { x: 0.0, y: 0.0 },
916 ));
917 (std::mem::take(l), b)
918 })
919 .collect();
920
921 with_cache.sort_unstable_by(|(l1, b1), (l2, b2)| {
922 b1.min()
923 .x
924 .total_cmp(&b2.min().x)
925 .then(b1.min().y.total_cmp(&b2.min().y))
926 .then(l1.len().cmp(&l2.len()))
927 });
928
929 for (i, (line, _)) in with_cache.into_iter().enumerate() {
930 lines[i] = line;
931 }
932}
933
934pub(crate) fn process_invalid_rings(
935 rings: Vec<Polygon3D>,
936 valid_shells_2d: &[Polygon<f64>],
937) -> Vec<Vec<Coord3D>> {
938 let mut processable = Vec::new();
939 let mut others = Vec::new();
940
941 for ring in rings {
942 if ring
943 .exterior
944 .iter()
945 .all(|c| c.x.is_finite() && c.y.is_finite())
946 {
947 processable.push(ring);
948 } else {
949 others.push(ring);
950 }
951 }
952
953 let mut processable_with_areas: Vec<_> = processable
955 .into_iter()
956 .map(|ring| {
957 let area = if ring.exterior.is_empty() {
958 0.0
959 } else {
960 let mut min_x = ring.exterior[0].x;
961 let mut max_x = ring.exterior[0].x;
962 let mut min_y = ring.exterior[0].y;
963 let mut max_y = ring.exterior[0].y;
964 for c in &ring.exterior[1..] {
965 if c.x < min_x {
966 min_x = c.x;
967 }
968 if c.x > max_x {
969 max_x = c.x;
970 }
971 if c.y < min_y {
972 min_y = c.y;
973 }
974 if c.y > max_y {
975 max_y = c.y;
976 }
977 }
978 (max_x - min_x) * (max_y - min_y)
979 };
980 (ring, area)
981 })
982 .collect();
983
984 processable_with_areas.sort_unstable_by(|(_, area_a), (_, area_b)| area_a.total_cmp(area_b));
985
986 let processable: Vec<_> = processable_with_areas.into_iter().map(|(r, _)| r).collect();
987
988 struct RingPair {
989 p3d: Polygon3D,
990 p2d: Polygon<f64>,
991 }
992
993 let mut accepted: Vec<RingPair> = Vec::new();
994
995 for ring in processable {
996 let p2d = ring.to_polygon_2d();
997 let contains_invalid = accepted.iter().any(|existing| p2d.contains(&existing.p2d));
1000 let contains_valid = valid_shells_2d.iter().any(|valid| p2d.contains(valid));
1001
1002 if !contains_invalid && !contains_valid {
1003 accepted.push(RingPair { p3d: ring, p2d });
1004 }
1005 }
1006
1007 let mut result: Vec<Vec<Coord3D>> = accepted.into_iter().map(|rp| rp.p3d.exterior).collect();
1008 result.extend(others.into_iter().map(|p| p.exterior));
1009
1010 result
1011}
1012
1013pub fn bounding_rect_3d(coords: &[Coord3D]) -> Option<geo::Rect<f64>> {
1014 if coords.is_empty() {
1015 return None;
1016 }
1017 let mut min_x = coords[0].x;
1018 let mut max_x = coords[0].x;
1019 let mut min_y = coords[0].y;
1020 let mut max_y = coords[0].y;
1021 for c in &coords[1..] {
1022 if c.x < min_x {
1023 min_x = c.x;
1024 }
1025 if c.x > max_x {
1026 max_x = c.x;
1027 }
1028 if c.y < min_y {
1029 min_y = c.y;
1030 }
1031 if c.y > max_y {
1032 max_y = c.y;
1033 }
1034 }
1035 Some(geo::Rect::new(
1036 geo::Coord { x: min_x, y: min_y },
1037 geo::Coord { x: max_x, y: max_y },
1038 ))
1039}
1040
1041pub fn guaranteed_interior_probe(coords: &[Coord3D]) -> Option<geo_types::Point<f64>> {
1042 if coords.len() < 4 {
1043 return None;
1044 }
1045
1046 let unique_n = coords.len().saturating_sub(1);
1047 if unique_n < 3 {
1048 return None;
1049 }
1050
1051 let area = Polygon3D::ring_signed_area_2d(coords);
1052 if !area.is_finite() || area.abs() < 1e-12 {
1053 return None;
1054 }
1055
1056 let locator = SimdRing::new_3d(coords);
1057 let bbox = bounding_rect_3d(coords);
1058 let diagonal = bbox
1059 .map(|bbox| {
1060 let dx = bbox.max().x - bbox.min().x;
1061 let dy = bbox.max().y - bbox.min().y;
1062 (dx * dx + dy * dy).sqrt()
1063 })
1064 .unwrap_or(1.0);
1065 guaranteed_interior_probe_prepared(coords, area, diagonal, &locator)
1066}
1067
1068pub(crate) fn guaranteed_interior_probe_prepared(
1069 coords: &[Coord3D],
1070 signed_area: f64,
1071 diagonal: f64,
1072 locator: &SimdRing,
1073) -> Option<geo_types::Point<f64>> {
1074 if coords.len() < 4 || !signed_area.is_finite() || signed_area.abs() < 1e-12 {
1075 return None;
1076 }
1077
1078 let unique_n = coords.len().saturating_sub(1);
1079 if unique_n < 3 {
1080 return None;
1081 }
1082
1083 let eps = (diagonal * 1e-9).max(1e-10);
1084
1085 for i in 0..unique_n {
1086 let prev = coords[(i + unique_n - 1) % unique_n];
1087 let curr = coords[i];
1088 let next = coords[(i + 1) % unique_n];
1089
1090 let in_edge = Coord {
1091 x: curr.x - prev.x,
1092 y: curr.y - prev.y,
1093 };
1094 let out_edge = Coord {
1095 x: next.x - curr.x,
1096 y: next.y - curr.y,
1097 };
1098
1099 let in_len = (in_edge.x * in_edge.x + in_edge.y * in_edge.y).sqrt();
1100 let out_len = (out_edge.x * out_edge.x + out_edge.y * out_edge.y).sqrt();
1101 if in_len < 1e-12 || out_len < 1e-12 {
1102 continue;
1103 }
1104
1105 let turn = in_edge.x * out_edge.y - in_edge.y * out_edge.x;
1106 let convex = if signed_area > 0.0 {
1107 turn > 1e-12
1108 } else {
1109 turn < -1e-12
1110 };
1111 if !convex {
1112 continue;
1113 }
1114
1115 let to_prev = Coord {
1116 x: (prev.x - curr.x) / in_len,
1117 y: (prev.y - curr.y) / in_len,
1118 };
1119 let to_next = Coord {
1120 x: (next.x - curr.x) / out_len,
1121 y: (next.y - curr.y) / out_len,
1122 };
1123
1124 let bisector = Coord {
1125 x: to_prev.x + to_next.x,
1126 y: to_prev.y + to_next.y,
1127 };
1128 let bisector_len = (bisector.x * bisector.x + bisector.y * bisector.y).sqrt();
1129 if bisector_len < 1e-12 {
1130 continue;
1131 }
1132
1133 let bisector_unit = Coord {
1134 x: bisector.x / bisector_len,
1135 y: bisector.y / bisector_len,
1136 };
1137
1138 for sign in [1.0, -1.0] {
1139 let candidate = Coord {
1140 x: curr.x + sign * bisector_unit.x * eps,
1141 y: curr.y + sign * bisector_unit.y * eps,
1142 };
1143 if locator.contains(candidate) {
1144 return Some(geo_types::Point(candidate));
1145 }
1146 }
1147 }
1148
1149 Some(geo_types::Point(coords[0].to_coord_2d()))
1150}
1151
1152pub fn rings_share_edge(shell: &[Coord3D], hole: &[Coord3D], eps: f64) -> bool {
1153 rings_share_edge_with_count(shell, hole, eps).0
1154}
1155
1156pub(crate) fn rings_share_edge_with_count(
1157 shell: &[Coord3D],
1158 hole: &[Coord3D],
1159 eps: f64,
1160) -> (bool, usize) {
1161 if shell.len() < 2 || hole.len() < 2 {
1162 return (false, 0);
1163 }
1164
1165 let mut pair_checks = 0;
1166 for shell_edge in shell.windows(2) {
1169 let a1 = shell_edge[0].to_coord_2d();
1170 let a2 = shell_edge[1].to_coord_2d();
1171 for hole_edge in hole.windows(2) {
1172 pair_checks += 1;
1173 let b1 = hole_edge[0].to_coord_2d();
1174 let b2 = hole_edge[1].to_coord_2d();
1175 if segments_overlap_with_length(a1, a2, b1, b2, eps) {
1176 return (true, pair_checks);
1177 }
1178 }
1179 }
1180
1181 (false, pair_checks)
1182}
1183
1184pub fn rings_touch_at_vertex(shell: &[Coord3D], hole: &[Coord3D], eps: f64) -> bool {
1185 rings_touch_at_vertex_with_count(shell, hole, eps).0
1186}
1187
1188pub(crate) fn rings_touch_at_vertex_with_count(
1189 shell: &[Coord3D],
1190 hole: &[Coord3D],
1191 eps: f64,
1192) -> (bool, usize) {
1193 let eps_sq = eps * eps;
1194 let mut pair_checks = 0;
1195 for s_pt in shell {
1196 for h_pt in hole {
1197 pair_checks += 1;
1198 let dx = s_pt.x - h_pt.x;
1199 let dy = s_pt.y - h_pt.y;
1200 if dx * dx + dy * dy <= eps_sq {
1201 return (true, pair_checks);
1202 }
1203 }
1204 }
1205 (false, pair_checks)
1206}
1207
1208fn segments_overlap_with_length(
1209 a1: Coord<f64>,
1210 a2: Coord<f64>,
1211 b1: Coord<f64>,
1212 b2: Coord<f64>,
1213 eps: f64,
1214) -> bool {
1215 let ax = a2.x - a1.x;
1216 let ay = a2.y - a1.y;
1217 let a_len_sq = ax * ax + ay * ay;
1218 if a_len_sq <= eps * eps {
1219 return false;
1220 }
1221
1222 let cross_b1 = ax * (b1.y - a1.y) - ay * (b1.x - a1.x);
1225 let cross_b2 = ax * (b2.y - a1.y) - ay * (b2.x - a1.x);
1226 let tol_sq = eps * eps * a_len_sq;
1227 if cross_b1 * cross_b1 > tol_sq || cross_b2 * cross_b2 > tol_sq {
1228 return false;
1229 }
1230
1231 let t1 = ((b1.x - a1.x) * ax + (b1.y - a1.y) * ay) / a_len_sq;
1232 let t2 = ((b2.x - a1.x) * ax + (b2.y - a1.y) * ay) / a_len_sq;
1233
1234 let min_t = t1.min(t2);
1235 let max_t = t1.max(t2);
1236 let overlap_start = 0.0_f64.max(min_t);
1237 let overlap_end = 1.0_f64.min(max_t);
1238
1239 if overlap_end <= overlap_start {
1240 return false;
1241 }
1242
1243 let overlap_len = overlap_end - overlap_start;
1244 overlap_len * overlap_len * a_len_sq > eps * eps
1245}
1246
1247fn extract_segments(geom: &Geometry<f64>, out: &mut Vec<Line3D>) {
1248 let mut stack = smallvec::SmallVec::<[&Geometry<f64>; 16]>::new();
1249 stack.push(geom);
1250 while let Some(current) = stack.pop() {
1251 match current {
1252 Geometry::LineString(ls) => {
1253 let len = ls.0.len().saturating_sub(1);
1254 out.reserve(len);
1255 out.extend(ls.lines().map(Line3D::from));
1256 }
1257 Geometry::MultiLineString(mls) => {
1258 for ls in &mls.0 {
1259 let len = ls.0.len().saturating_sub(1);
1260 out.reserve(len);
1261 out.extend(ls.lines().map(Line3D::from));
1262 }
1263 }
1264 Geometry::Polygon(poly) => {
1265 let ext = poly.exterior();
1266 let len = ext.0.len().saturating_sub(1);
1267 out.reserve(len);
1268 out.extend(ext.lines().map(Line3D::from));
1269 for interior in poly.interiors() {
1270 let len = interior.0.len().saturating_sub(1);
1271 out.reserve(len);
1272 out.extend(interior.lines().map(Line3D::from));
1273 }
1274 }
1275 Geometry::MultiPolygon(mpoly) => {
1276 for poly in mpoly {
1277 let ext = poly.exterior();
1278 let len = ext.0.len().saturating_sub(1);
1279 out.reserve(len);
1280 out.extend(ext.lines().map(Line3D::from));
1281 for interior in poly.interiors() {
1282 let len = interior.0.len().saturating_sub(1);
1283 out.reserve(len);
1284 out.extend(interior.lines().map(Line3D::from));
1285 }
1286 }
1287 }
1288 Geometry::GeometryCollection(gc) => {
1289 stack.extend(gc.0.iter().rev());
1290 }
1291 _ => {}
1292 }
1293 }
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298 use super::*;
1299 use geo::LineString;
1300
1301 #[test]
1302 fn test_with_snap_grid() {
1303 let polygonizer = Polygonizer::new().with_snap_grid(0.123);
1304 assert_eq!(polygonizer.snap_grid_size, 0.123);
1305 }
1306
1307 #[test]
1308 fn geos_compat_preserves_source_coordinates() {
1309 let points = [
1310 Coord3D::new(0.04, 0.04, 0.0),
1311 Coord3D::new(10.04, 0.04, 0.0),
1312 Coord3D::new(10.04, 10.04, 0.0),
1313 Coord3D::new(0.04, 10.04, 0.0),
1314 ];
1315 let lines = (0..4)
1316 .map(|i| Line3D::new(points[i], points[(i + 1) % 4], i as u32))
1317 .collect::<Vec<_>>();
1318 let mut options = PolygonizerOptions::default();
1319 options.node_input = true;
1320 options.snap_grid_size = 0.1;
1321 options.snap_strategy = SnapStrategy::GeosCompat;
1322
1323 let result = polygonize_with_options(&lines, &options).unwrap();
1324
1325 assert_eq!(result.polygons.len(), 1);
1326 assert!(points.iter().all(|point| result.polygons[0]
1327 .exterior
1328 .iter()
1329 .any(|actual| actual == point)));
1330 }
1331
1332 #[test]
1333 fn geos_compat_drops_sub_grid_face_from_near_coincident_endpoints() {
1334 let lines = vec![
1335 Line3D::new(
1336 Coord3D::new(139131.39157939597, 332081.47584308265, 0.0),
1337 Coord3D::new(139138.25173987588, 332179.7525957378, 0.0),
1338 1,
1339 ),
1340 Line3D::new(
1341 Coord3D::new(139128.6709763695, 332081.6202812562, 0.0),
1342 Coord3D::new(139131.39157939597, 332081.4758430829, 0.0),
1343 2,
1344 ),
1345 Line3D::new(
1346 Coord3D::new(139131.39157939597, 332081.4758430829, 0.0),
1347 Coord3D::new(139134.1124077957, 332081.33571445255, 0.0),
1348 3,
1349 ),
1350 ];
1351
1352 let result = polygonize_with_options(&lines, &PolygonizerOptions::cfb_robust_v1()).unwrap();
1353
1354 assert!(result.polygons.is_empty());
1355 }
1356
1357 #[test]
1358 fn test_add_lines() {
1359 let mut polygonizer = Polygonizer::new();
1360 assert!(!polygonizer.dirty);
1361 assert!(polygonizer.input_lines.is_empty());
1362
1363 let l1 = Line3D::new(Coord3D::new(0.0, 0.0, 0.0), Coord3D::new(1.0, 0.0, 0.0), 0);
1364 let l2 = Line3D::new(Coord3D::new(1.0, 0.0, 0.0), Coord3D::new(1.0, 1.0, 0.0), 1);
1365
1366 polygonizer.add_lines(vec![l1, l2]);
1367
1368 assert!(polygonizer.dirty);
1369 assert_eq!(polygonizer.input_lines.len(), 2);
1370 assert_eq!(polygonizer.input_lines[0].start.x, 0.0);
1371 assert_eq!(polygonizer.input_lines[1].end.y, 1.0);
1372 }
1373
1374 #[test]
1375 fn test_bounding_rect_3d() {
1376 assert_eq!(bounding_rect_3d(&[]), None);
1378
1379 let single = vec![Coord3D::new(5.0, 10.0, 15.0)];
1381 let rect1 = bounding_rect_3d(&single).unwrap();
1382 assert_eq!(rect1.min().x, 5.0);
1383 assert_eq!(rect1.max().x, 5.0);
1384 assert_eq!(rect1.min().y, 10.0);
1385 assert_eq!(rect1.max().y, 10.0);
1386
1387 let coords = vec![
1389 Coord3D::new(1.0, 2.0, 100.0),
1390 Coord3D::new(-5.0, 8.0, -50.0),
1391 Coord3D::new(10.0, -3.0, 0.0),
1392 Coord3D::new(4.0, 12.0, 20.0),
1393 ];
1394 let rect2 = bounding_rect_3d(&coords).unwrap();
1395 assert_eq!(rect2.min().x, -5.0);
1396 assert_eq!(rect2.max().x, 10.0);
1397 assert_eq!(rect2.min().y, -3.0);
1398 assert_eq!(rect2.max().y, 12.0);
1399
1400 let coords2 = vec![
1402 Coord3D::new(0.0, 0.0, 0.0),
1403 Coord3D::new(0.0, 0.0, 10.0),
1404 Coord3D::new(0.0, 0.0, -10.0),
1405 ];
1406 let rect3 = bounding_rect_3d(&coords2).unwrap();
1407 assert_eq!(rect3.min().x, 0.0);
1408 assert_eq!(rect3.max().x, 0.0);
1409 assert_eq!(rect3.min().y, 0.0);
1410 assert_eq!(rect3.max().y, 0.0);
1411 }
1412
1413 #[test]
1414 fn test_rings_share_edge() {
1415 let shell = vec![
1416 Coord3D::new(0.0, 0.0, 0.0),
1417 Coord3D::new(10.0, 0.0, 0.0),
1418 Coord3D::new(10.0, 10.0, 0.0),
1419 Coord3D::new(0.0, 10.0, 0.0),
1420 Coord3D::new(0.0, 0.0, 0.0),
1421 ];
1422
1423 let hole1 = vec![
1425 Coord3D::new(2.0, 0.0, 0.0),
1426 Coord3D::new(1.0, 0.0, 0.0),
1427 Coord3D::new(1.0, 1.0, 0.0),
1428 Coord3D::new(2.0, 1.0, 0.0),
1429 Coord3D::new(2.0, 0.0, 0.0),
1430 ];
1431 assert!(rings_share_edge(&shell, &hole1, 1e-10));
1433
1434 let hole2 = vec![
1436 Coord3D::new(-1.0, 0.0, 0.0),
1437 Coord3D::new(1.0, 0.0, 0.0),
1438 Coord3D::new(1.0, -1.0, 0.0),
1439 Coord3D::new(-1.0, -1.0, 0.0),
1440 Coord3D::new(-1.0, 0.0, 0.0),
1441 ];
1442 assert!(rings_share_edge(&shell, &hole2, 1e-10));
1444
1445 let hole3 = vec![
1447 Coord3D::new(0.0, 0.0, 0.0),
1448 Coord3D::new(-1.0, -1.0, 0.0),
1449 Coord3D::new(0.0, -1.0, 0.0),
1450 Coord3D::new(0.0, 0.0, 0.0),
1451 ];
1452 assert!(!rings_share_edge(&shell, &hole3, 1e-10));
1453
1454 let hole4 = vec![
1456 Coord3D::new(11.0, 0.0, 0.0),
1457 Coord3D::new(12.0, 0.0, 0.0),
1458 Coord3D::new(12.0, 1.0, 0.0),
1459 Coord3D::new(11.0, 1.0, 0.0),
1460 Coord3D::new(11.0, 0.0, 0.0),
1461 ];
1462 assert!(!rings_share_edge(&shell, &hole4, 1e-10));
1463
1464 let hole5 = vec![
1466 Coord3D::new(0.0, -1.0, 0.0),
1467 Coord3D::new(10.0, -1.0, 0.0),
1468 Coord3D::new(10.0, -2.0, 0.0),
1469 Coord3D::new(0.0, -2.0, 0.0),
1470 Coord3D::new(0.0, -1.0, 0.0),
1471 ];
1472 assert!(!rings_share_edge(&shell, &hole5, 1e-10));
1473
1474 let hole6 = vec![
1476 Coord3D::new(0.0, 1e-11, 0.0),
1477 Coord3D::new(10.0, 1e-11, 0.0),
1478 Coord3D::new(10.0, 1.0, 0.0),
1479 Coord3D::new(0.0, 1.0, 0.0),
1480 Coord3D::new(0.0, 1e-11, 0.0),
1481 ];
1482 assert!(rings_share_edge(&shell, &hole6, 1e-10));
1483
1484 let hole7 = vec![
1486 Coord3D::new(1.0, 1e-9, 0.0),
1487 Coord3D::new(9.0, 1e-9, 0.0),
1488 Coord3D::new(9.0, 1.0, 0.0),
1489 Coord3D::new(1.0, 1.0, 0.0),
1490 Coord3D::new(1.0, 1e-9, 0.0),
1491 ];
1492 assert!(!rings_share_edge(&shell, &hole7, 1e-10));
1493
1494 let shell_short = vec![
1496 Coord3D::new(0.0, 0.0, 0.0),
1497 Coord3D::new(0.01, 0.0, 0.0),
1498 Coord3D::new(0.01, 0.01, 0.0),
1499 Coord3D::new(0.0, 0.01, 0.0),
1500 Coord3D::new(0.0, 0.0, 0.0),
1501 ];
1502 let hole_short = vec![
1503 Coord3D::new(0.0, 0.0, 0.0),
1504 Coord3D::new(0.01, 0.0, 0.0),
1505 Coord3D::new(0.01, -0.01, 0.0),
1506 Coord3D::new(0.0, -0.01, 0.0),
1507 Coord3D::new(0.0, 0.0, 0.0),
1508 ];
1509 assert!(!rings_share_edge(&shell_short, &hole_short, 0.1));
1512 assert!(rings_share_edge(&shell_short, &hole_short, 0.001));
1514 }
1515
1516 #[test]
1517 fn test_guaranteed_interior_probe() {
1518 let coords1 = vec![
1520 Coord3D::new(0.0, 0.0, 0.0),
1521 Coord3D::new(1.0, 0.0, 0.0),
1522 Coord3D::new(0.0, 1.0, 0.0),
1523 ];
1524 assert_eq!(guaranteed_interior_probe(&coords1), None);
1525
1526 let coords2 = vec![
1528 Coord3D::new(0.0, 0.0, 0.0),
1529 Coord3D::new(1.0, 0.0, 0.0),
1530 Coord3D::new(1.0, 0.0, 0.0),
1531 Coord3D::new(0.0, 0.0, 0.0),
1532 ];
1533 assert_eq!(guaranteed_interior_probe(&coords2), None);
1534
1535 let coords3 = vec![
1537 Coord3D::new(0.0, 0.0, 0.0),
1538 Coord3D::new(1.0, 0.0, 0.0),
1539 Coord3D::new(2.0, 0.0, 0.0),
1540 Coord3D::new(0.0, 0.0, 0.0),
1541 ];
1542 assert_eq!(guaranteed_interior_probe(&coords3), None);
1543
1544 let coords4 = vec![
1546 Coord3D::new(0.0, 0.0, 0.0),
1547 Coord3D::new(10.0, 0.0, 0.0),
1548 Coord3D::new(10.0, 10.0, 0.0),
1549 Coord3D::new(0.0, 10.0, 0.0),
1550 Coord3D::new(0.0, 0.0, 0.0),
1551 ];
1552 let probe4 = guaranteed_interior_probe(&coords4).expect("Should find a point");
1553 let coords4_2d: Vec<_> = coords4.iter().map(|c| c.to_coord_2d()).collect();
1554 let p4 = Polygon::new(LineString::from(coords4_2d), vec![]);
1555 assert!(p4.contains(&probe4));
1556
1557 let coords5 = vec![
1561 Coord3D::new(0.0, 0.0, 0.0),
1562 Coord3D::new(10.0, 0.0, 0.0),
1563 Coord3D::new(10.0, 10.0, 0.0),
1564 Coord3D::new(8.0, 10.0, 0.0),
1565 Coord3D::new(8.0, 2.0, 0.0),
1566 Coord3D::new(2.0, 2.0, 0.0),
1567 Coord3D::new(2.0, 10.0, 0.0),
1568 Coord3D::new(0.0, 10.0, 0.0),
1569 Coord3D::new(0.0, 0.0, 0.0),
1570 ];
1571 let probe5 =
1572 guaranteed_interior_probe(&coords5).expect("Should find a point via bisector fallback");
1573 let coords5_2d: Vec<_> = coords5.iter().map(|c| c.to_coord_2d()).collect();
1574 let p5 = Polygon::new(LineString::from(coords5_2d), vec![]);
1575 assert!(p5.contains(&probe5));
1576 }
1577
1578 #[test]
1579 fn test_rings_touch_at_vertex() {
1580 let shell = vec![
1581 Coord3D::new(0.0, 0.0, 0.0),
1582 Coord3D::new(10.0, 0.0, 0.0),
1583 Coord3D::new(10.0, 10.0, 0.0),
1584 Coord3D::new(0.0, 10.0, 0.0),
1585 Coord3D::new(0.0, 0.0, 0.0),
1586 ];
1587
1588 let hole1 = vec![
1590 Coord3D::new(0.0, 0.0, 0.0),
1591 Coord3D::new(-1.0, -1.0, 0.0),
1592 Coord3D::new(1.0, -1.0, 0.0),
1593 Coord3D::new(0.0, 0.0, 0.0),
1594 ];
1595 assert!(rings_touch_at_vertex(&shell, &hole1, 1e-10));
1596
1597 let hole2 = vec![
1599 Coord3D::new(10.0 + 1e-11, 10.0, 0.0),
1600 Coord3D::new(11.0, 11.0, 0.0),
1601 Coord3D::new(11.0, 10.0, 0.0),
1602 Coord3D::new(10.0 + 1e-11, 10.0, 0.0),
1603 ];
1604 assert!(rings_touch_at_vertex(&shell, &hole2, 1e-10));
1605
1606 let hole3 = vec![
1608 Coord3D::new(10.0 + 1e-9, 10.0, 0.0),
1609 Coord3D::new(11.0, 11.0, 0.0),
1610 Coord3D::new(11.0, 10.0, 0.0),
1611 Coord3D::new(10.0 + 1e-9, 10.0, 0.0),
1612 ];
1613 assert!(!rings_touch_at_vertex(&shell, &hole3, 1e-10));
1614
1615 let hole4 = vec![
1617 Coord3D::new(0.0, 0.0, 0.0),
1618 Coord3D::new(10.0, 0.0, 0.0),
1619 Coord3D::new(5.0, -5.0, 0.0),
1620 Coord3D::new(0.0, 0.0, 0.0),
1621 ];
1622 assert!(rings_touch_at_vertex(&shell, &hole4, 1e-10));
1623
1624 let hole5 = vec![
1626 Coord3D::new(20.0, 20.0, 0.0),
1627 Coord3D::new(21.0, 20.0, 0.0),
1628 Coord3D::new(21.0, 21.0, 0.0),
1629 Coord3D::new(20.0, 20.0, 0.0),
1630 ];
1631 assert!(!rings_touch_at_vertex(&shell, &hole5, 1e-10));
1632
1633 assert!(!rings_touch_at_vertex(&[], &hole1, 1e-10));
1635 assert!(!rings_touch_at_vertex(&shell, &[], 1e-10));
1636 }
1637}