Skip to main content

brepkit_math/cdt/
mod.rs

1//! Constrained Delaunay Triangulation (CDT).
2//!
3//! Implements an incremental CDT using a triangle-adjacency data structure.
4//! Uses exact geometric predicates ([`orient2d`] and [`in_circle`]) for
5//! robustness.
6//!
7//! # Algorithm
8//!
9//! - **Point insertion**: Bowyer-Watson incremental insertion with edge
10//!   legalization.
11//! - **Constraint insertion**: Sloan-style edge recovery by flipping
12//!   intersecting edges.
13//! - **Exterior removal**: Flood-fill from super-triangle, stopping at
14//!   constrained edges.
15
16#![allow(
17    clippy::many_single_char_names,
18    clippy::similar_names,
19    clippy::needless_range_loop,
20    clippy::suboptimal_flops,
21    clippy::manual_slice_fill,
22    clippy::option_if_let_else,
23    clippy::let_and_return,
24    clippy::unnecessary_wraps,
25    clippy::doc_markdown,
26    clippy::cast_precision_loss,
27    clippy::missing_const_for_fn,
28    clippy::manual_let_else
29)]
30
31mod adjacency;
32mod constraints;
33mod insert;
34mod locate;
35#[cfg(test)]
36mod tests;
37
38use crate::det_hash::DetHashSet;
39
40use crate::MathError;
41use crate::predicates::{in_circle, orient2d};
42use crate::vec::Point2;
43
44/// Fast floating-point in-circle test with error bound.
45///
46/// Computes the in-circle determinant using standard f64 arithmetic.
47/// If the magnitude exceeds the error bound, returns the result directly.
48/// Otherwise, falls back to the exact `in_circle` predicate.
49///
50/// The error bound is derived from Shewchuk's analysis: the maximum
51/// rounding error of the 4×4 determinant is bounded by
52/// `εB * |det|` where εB depends on the matrix entries.
53#[inline]
54fn fast_in_circle(a: Point2, b: Point2, c: Point2, d: Point2) -> f64 {
55    let adx = a.x() - d.x();
56    let ady = a.y() - d.y();
57    let bdx = b.x() - d.x();
58    let bdy = b.y() - d.y();
59    let cdx = c.x() - d.x();
60    let cdy = c.y() - d.y();
61
62    let abdet = adx * bdy - bdx * ady;
63    let bcdet = bdx * cdy - cdx * bdy;
64    let cadet = cdx * ady - adx * cdy;
65    let alift = adx * adx + ady * ady;
66    let blift = bdx * bdx + bdy * bdy;
67    let clift = cdx * cdx + cdy * cdy;
68
69    let det = alift * bcdet + blift * cadet + clift * abdet;
70
71    // Error bound (conservative): if |det| >> sum of absolute products,
72    // the sign is reliable. Use Shewchuk's iccerrboundA ≈ 10ε where
73    // ε ≈ 2^-53. For our tolerance, 1e-10 * permanent works well.
74    let permanent = alift * ((bdx * cdy).abs() + (cdx * bdy).abs())
75        + blift * ((cdx * ady).abs() + (adx * cdy).abs())
76        + clift * ((adx * bdy).abs() + (bdx * ady).abs());
77
78    // Error bound coefficient: 10 * 2^-53 ≈ 1.11e-15
79    let errbound = 1.11e-15 * permanent;
80
81    if det > errbound || det < -errbound {
82        det
83    } else {
84        // Near zero — use exact predicate
85        in_circle(a, b, c, d)
86    }
87}
88
89// ---------------------------------------------------------------------------
90// Data structures
91// ---------------------------------------------------------------------------
92
93/// A triangle in the CDT.
94struct CdtTriangle {
95    /// Vertex indices in counter-clockwise order.
96    v: [usize; 3],
97    /// Adjacent triangle across the edge opposite vertex `v[i]`.
98    /// Edge opposite `v[i]` is `(v[(i+1)%3], v[(i+2)%3])`.
99    adj: [Option<usize>; 3],
100    /// Whether this triangle has been removed (exterior or deleted).
101    removed: bool,
102}
103
104/// Half-edge based Constrained Delaunay Triangulation.
105pub struct Cdt {
106    vertices: Vec<Point2>,
107    triangles: Vec<CdtTriangle>,
108    /// Set of constrained edges stored as sorted `(min, max)` vertex pairs.
109    constraints: DetHashSet<(usize, usize)>,
110    /// Number of super-triangle vertices at the start of the vertex list.
111    super_count: usize,
112    /// Spatial hash for O(1) amortized duplicate point detection.
113    dup_grid: std::collections::HashMap<(i64, i64), Vec<usize>>,
114    /// Last successfully located triangle — used as starting point for the
115    /// walking search to exploit spatial coherence in insertion order.
116    last_located: usize,
117    /// Vertex → one incident triangle index for O(1) edge lookups.
118    /// Updated on triangle creation/removal.
119    vertex_tri: Vec<usize>,
120}
121
122/// Duplicate point detection tolerance.
123///
124/// Aligned with the snap tolerance (1e-8) to avoid near-coincident points
125/// that pass the duplicate check but create degenerate triangles.
126const DUP_TOL: f64 = 1e-8;
127
128// ---------------------------------------------------------------------------
129// Public API
130// ---------------------------------------------------------------------------
131
132impl Cdt {
133    /// Create a new CDT with a super-triangle that contains the given bounds.
134    ///
135    /// The bounds `(min, max)` define an axis-aligned rectangle. The
136    /// super-triangle is constructed large enough to enclose this rectangle
137    /// with margin.
138    #[must_use]
139    pub fn new(bounds: (Point2, Point2)) -> Self {
140        Self::with_capacity(bounds, 0)
141    }
142
143    /// Create a new CDT with pre-allocated capacity for `n` points.
144    ///
145    /// Pre-allocates vertex and triangle storage to avoid reallocations
146    /// during bulk insertion. Each point insertion creates ~2 triangles,
147    /// so `2*n + 1` triangle slots are allocated.
148    #[must_use]
149    pub fn with_capacity(bounds: (Point2, Point2), n: usize) -> Self {
150        let (min, max) = bounds;
151        let dx = max.x() - min.x();
152        let dy = max.y() - min.y();
153        let margin = (dx.max(dy)).mul_add(10.0, 1.0);
154        let cx = 0.5 * (min.x() + max.x());
155        let cy = 0.5 * (min.y() + max.y());
156
157        // Super-triangle vertices (large enough to contain everything).
158        let s0 = Point2::new(cx - margin * 2.0, cy - margin);
159        let s1 = Point2::new(cx + margin * 2.0, cy - margin);
160        let s2 = Point2::new(cx, cy + margin * 2.0);
161
162        let mut vertices = Vec::with_capacity(n + 3);
163        vertices.push(s0);
164        vertices.push(s1);
165        vertices.push(s2);
166
167        let mut triangles = Vec::with_capacity(2 * n + 1);
168        triangles.push(CdtTriangle {
169            v: [0, 1, 2],
170            adj: [None, None, None],
171            removed: false,
172        });
173
174        let mut vertex_tri = Vec::with_capacity(n + 3);
175        vertex_tri.extend([0, 0, 0]); // all 3 super-verts → tri 0
176
177        Self {
178            vertices,
179            triangles,
180            constraints: DetHashSet::default(),
181            super_count: 3,
182            dup_grid: std::collections::HashMap::new(),
183            last_located: 0,
184            vertex_tri,
185        }
186    }
187
188    /// Insert a point into the triangulation.
189    ///
190    /// Returns the vertex index of the inserted point. If the point is a
191    /// duplicate of an existing vertex (within tolerance), the existing
192    /// vertex index is returned.
193    ///
194    /// # Errors
195    ///
196    /// Returns [`MathError::ConvergenceFailure`] if the point cannot be
197    /// located in any triangle (should not happen for valid inputs).
198    pub fn insert_point(&mut self, p: Point2) -> Result<usize, MathError> {
199        let cell = dup_grid_cell(p);
200        // Check the cell and its 8 neighbors to handle points near cell boundaries.
201        for dx in -1..=1_i64 {
202            for dy in -1..=1_i64 {
203                let neighbor = (cell.0 + dx, cell.1 + dy);
204                if let Some(indices) = self.dup_grid.get(&neighbor) {
205                    for &i in indices {
206                        let d = p - self.vertices[i];
207                        if d.length_squared() < DUP_TOL * DUP_TOL {
208                            return Ok(i);
209                        }
210                    }
211                }
212            }
213        }
214
215        let vi = self.vertices.len();
216        self.vertices.push(p);
217        self.vertex_tri.push(0); // will be updated by split_triangle/split_edge
218        self.dup_grid.entry(cell).or_default().push(vi);
219
220        let (tri_idx, location) = self.locate_point(p)?;
221        self.last_located = tri_idx;
222
223        match location {
224            locate::PointLocation::Inside => {
225                self.split_triangle(tri_idx, vi);
226            }
227            locate::PointLocation::OnEdge(local_edge) => {
228                self.split_edge(tri_idx, local_edge, vi);
229            }
230        }
231
232        Ok(vi)
233    }
234
235    /// Bulk-insert points sorted by Hilbert curve for O(1) amortized locate.
236    ///
237    /// Returns a `Vec` where `result[original_index]` is the CDT vertex index.
238    /// Points near the Hilbert curve walk path are inserted together, so each
239    /// `locate_point` call starts close to the target triangle.
240    ///
241    /// # Errors
242    ///
243    /// Returns [`MathError::ConvergenceFailure`] if any point cannot be located.
244    pub fn insert_points_hilbert(&mut self, points: &[Point2]) -> Result<Vec<usize>, MathError> {
245        if points.is_empty() {
246            return Ok(Vec::new());
247        }
248
249        let mut min_x = f64::INFINITY;
250        let mut max_x = f64::NEG_INFINITY;
251        let mut min_y = f64::INFINITY;
252        let mut max_y = f64::NEG_INFINITY;
253        for p in points {
254            min_x = min_x.min(p.x());
255            max_x = max_x.max(p.x());
256            min_y = min_y.min(p.y());
257            max_y = max_y.max(p.y());
258        }
259
260        let range = (max_x - min_x).max(max_y - min_y).max(1e-10);
261        let n = 1u32 << 16; // 65536 grid resolution
262        let scale = f64::from(n - 1) / range;
263
264        // Sort by Hilbert index for spatial locality.
265        let mut order: Vec<(u64, usize)> = points
266            .iter()
267            .enumerate()
268            .map(|(i, p)| {
269                let gx = ((p.x() - min_x) * scale) as u32;
270                let gy = ((p.y() - min_y) * scale) as u32;
271                (hilbert_xy_to_d(n, gx.min(n - 1), gy.min(n - 1)), i)
272            })
273            .collect();
274        order.sort_unstable_by_key(|&(h, _)| h);
275
276        // Insert in Hilbert order, storing results in original order.
277        let mut result = vec![0usize; points.len()];
278        for &(_, orig_idx) in &order {
279            let cdt_idx = self.insert_point(points[orig_idx])?;
280            result[orig_idx] = cdt_idx;
281        }
282
283        Ok(result)
284    }
285
286    /// Insert a constraint edge between two existing vertices.
287    ///
288    /// The edge is recovered by flipping intersecting unconstrained edges
289    /// until the constraint edge appears in the triangulation.
290    ///
291    /// # Errors
292    ///
293    /// Returns [`MathError::ConvergenceFailure`] if the constraint cannot
294    /// be recovered after the maximum number of iterations.
295    pub fn insert_constraint(&mut self, v0: usize, v1: usize) -> Result<(), MathError> {
296        if v0 == v1 {
297            return Ok(());
298        }
299        let key = sorted_pair(v0, v1);
300        if self.constraints.contains(&key) {
301            return Ok(());
302        }
303
304        // Scan for existing vertices that lie on the constraint segment.
305        // If found, recursively split the constraint through them so that
306        // recover_edge never encounters a collinear interior vertex (which
307        // causes flip-recovery deadlocks on full-revolution face seams).
308        //
309        // This is an O(V) scan per constraint. For typical tessellation CDTs
310        // (< 10K vertices, < 100 constraints) the cost is negligible. A spatial
311        // index could reduce this to O(k) but dup_grid's 1e-5 cell size makes
312        // AABB iteration pathological for long segments.
313        let p0 = self.vertices[v0];
314        let p1 = self.vertices[v1];
315        let dx = p1.x() - p0.x();
316        let dy = p1.y() - p0.y();
317        let seg_len_sq = dx * dx + dy * dy;
318
319        if seg_len_sq > 0.0 {
320            let mut collinear: Vec<(f64, usize)> = Vec::new();
321            for vi in self.super_count..self.vertices.len() {
322                if vi == v0 || vi == v1 {
323                    continue;
324                }
325                let px = self.vertices[vi].x() - p0.x();
326                let py = self.vertices[vi].y() - p0.y();
327                let t = (px * dx + py * dy) / seg_len_sq;
328                if t <= 1e-6 || t >= 1.0 - 1e-6 {
329                    continue;
330                }
331                let cross = px * dy - py * dx;
332                let dist_sq = cross * cross / seg_len_sq;
333                if dist_sq < 1e-12 * seg_len_sq {
334                    collinear.push((t, vi));
335                }
336            }
337
338            if !collinear.is_empty() {
339                collinear.sort_by(|a, b| a.0.total_cmp(&b.0));
340                collinear.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-8);
341                let mut prev = v0;
342                for &(_, vi) in &collinear {
343                    self.insert_constraint(prev, vi)?;
344                    prev = vi;
345                }
346                self.insert_constraint(prev, v1)?;
347                return Ok(());
348            }
349        }
350
351        // Recover the edge by flipping.
352        self.recover_edge(v0, v1)?;
353        self.constraints.insert(key);
354        Ok(())
355    }
356
357    /// Get the triangles as index triples (vertex indices).
358    ///
359    /// Only returns non-removed triangles that do not reference
360    /// super-triangle vertices.
361    #[must_use]
362    pub fn triangles(&self) -> Vec<(usize, usize, usize)> {
363        let sc = self.super_count;
364        self.triangles
365            .iter()
366            .filter(|t| !t.removed)
367            .filter(|t| t.v[0] >= sc && t.v[1] >= sc && t.v[2] >= sc)
368            .map(|t| (t.v[0], t.v[1], t.v[2]))
369            .collect()
370    }
371
372    /// Get the vertices.
373    #[must_use]
374    pub fn vertices(&self) -> &[Point2] {
375        &self.vertices
376    }
377
378    /// Remove triangles outside the boundary defined by constraint edges.
379    ///
380    /// Flood-fills from super-triangle-adjacent triangles, stopping at
381    /// constraint edges. Also removes any triangle that references a
382    /// super-triangle vertex.
383    pub fn remove_exterior(&mut self, boundary: &[(usize, usize)]) {
384        // Build the constraint set for boundary edges.
385        let boundary_set: DetHashSet<(usize, usize)> =
386            boundary.iter().map(|&(a, b)| sorted_pair(a, b)).collect();
387
388        // Merge with existing constraints for the flood-fill barrier.
389        let all_constraints: DetHashSet<(usize, usize)> =
390            self.constraints.union(&boundary_set).copied().collect();
391
392        // Start flood-fill from triangles touching super-triangle vertices.
393        let mut stack: Vec<usize> = Vec::new();
394        let sc = self.super_count;
395
396        for (i, tri) in self.triangles.iter().enumerate() {
397            if tri.removed {
398                continue;
399            }
400            if tri.v[0] < sc || tri.v[1] < sc || tri.v[2] < sc {
401                stack.push(i);
402            }
403        }
404
405        // Flood-fill, marking triangles as removed.
406        while let Some(ti) = stack.pop() {
407            if self.triangles[ti].removed {
408                continue;
409            }
410            self.triangles[ti].removed = true;
411
412            // Check each edge — if not a constraint boundary, propagate.
413            for local in 0..3 {
414                let va = self.triangles[ti].v[(local + 1) % 3];
415                let vb = self.triangles[ti].v[(local + 2) % 3];
416                let edge_key = sorted_pair(va, vb);
417
418                if all_constraints.contains(&edge_key) {
419                    continue; // Don't cross constraint edges.
420                }
421
422                if let Some(adj) = self.triangles[ti].adj[local]
423                    && !self.triangles[adj].removed
424                {
425                    stack.push(adj);
426                }
427            }
428        }
429
430        // Note: remove_hole_interiors is only needed when there are inner loops
431        // (holes within the boundary). For simple polygons, the exterior
432        // flood-fill is sufficient.
433    }
434
435    /// Remove all non-removed triangles reachable from the triangle containing
436    /// `seed`, stopping at constraint edges.
437    ///
438    /// This is the standard CDT hole-removal approach: given a point known to
439    /// be inside a hole, find its containing triangle and flood-fill remove.
440    ///
441    /// Returns `true` if the seed triangle was found and removal occurred,
442    /// `false` if no triangle contains the seed point (e.g. concave hole
443    /// centroid falling outside the polygon).
444    pub fn flood_remove_from_point(
445        &mut self,
446        seed: Point2,
447        constraints: &DetHashSet<(usize, usize)>,
448    ) -> bool {
449        // The flood must also respect the CDT's OWN constraints: edge
450        // recovery may have split a caller-known constraint into sub-pairs
451        // (Steiner points), and the caller's set only carries the original
452        // endpoints. Without the union the flood crosses the sub-edges.
453        let barrier: DetHashSet<(usize, usize)> =
454            constraints.union(&self.constraints).copied().collect();
455        let constraints = &barrier;
456        // Use the walking point-location search (O(sqrt(n))) instead of
457        // linear scan (O(n)) to find the seed triangle.
458        let seed_tri = self.locate_point(seed).ok().map(|(i, _)| i).or_else(|| {
459            // Fallback: linear scan for removed/degenerate cases.
460            self.triangles
461                .iter()
462                .enumerate()
463                .filter(|(_, t)| !t.removed)
464                .find(|(_, t)| {
465                    let p0 = self.vertices[t.v[0]];
466                    let p1 = self.vertices[t.v[1]];
467                    let p2 = self.vertices[t.v[2]];
468                    let d0 = orient2d(p0, p1, seed);
469                    let d1 = orient2d(p1, p2, seed);
470                    let d2 = orient2d(p2, p0, seed);
471                    (d0 >= 0.0 && d1 >= 0.0 && d2 >= 0.0) || (d0 <= 0.0 && d1 <= 0.0 && d2 <= 0.0)
472                })
473                .map(|(i, _)| i)
474        });
475
476        let Some(start) = seed_tri else {
477            return false;
478        };
479
480        let mut stack = vec![start];
481        while let Some(ti) = stack.pop() {
482            if self.triangles[ti].removed {
483                continue;
484            }
485            self.triangles[ti].removed = true;
486
487            for local in 0..3 {
488                let va = self.triangles[ti].v[(local + 1) % 3];
489                let vb = self.triangles[ti].v[(local + 2) % 3];
490                let edge_key = sorted_pair(va, vb);
491                if constraints.contains(&edge_key) {
492                    continue;
493                }
494                if let Some(adj) = self.triangles[ti].adj[local]
495                    && !self.triangles[adj].removed
496                {
497                    stack.push(adj);
498                }
499            }
500        }
501
502        true
503    }
504
505    /// Partition remaining (non-removed) interior triangles into connected
506    /// regions separated by the given separator edges.
507    ///
508    /// After calling [`Cdt::remove_exterior`], this method groups interior
509    /// triangles into connected components. Two adjacent triangles belong
510    /// to the same region unless the shared edge is in `separators`.
511    ///
512    /// Returns a list of polygonal boundaries, one per connected region,
513    /// ordered as closed loops in parameter space. Each polygon is the
514    /// boundary of the union of triangles in that region.
515    ///
516    /// # Arguments
517    ///
518    /// * `separators` — edges that act as region boundaries (typically the
519    ///   pcurve constraint edges inserted during NURBS boolean splitting).
520    ///   Stored as sorted `(min, max)` pairs.
521    #[must_use]
522    pub fn extract_regions(&self, separators: &[(usize, usize)]) -> Vec<Vec<Point2>> {
523        let sep_set: DetHashSet<(usize, usize)> =
524            separators.iter().map(|&(a, b)| sorted_pair(a, b)).collect();
525
526        let sc = self.super_count;
527
528        let live_tris: Vec<usize> = self
529            .triangles
530            .iter()
531            .enumerate()
532            .filter(|(_, t)| !t.removed)
533            .filter(|(_, t)| t.v[0] >= sc && t.v[1] >= sc && t.v[2] >= sc)
534            .map(|(i, _)| i)
535            .collect();
536
537        if live_tris.is_empty() {
538            return Vec::new();
539        }
540
541        // Map from triangle index → position in live_tris (for visited tracking).
542        let mut tri_to_idx: std::collections::HashMap<usize, usize> =
543            std::collections::HashMap::with_capacity(live_tris.len());
544        for (idx, &ti) in live_tris.iter().enumerate() {
545            tri_to_idx.insert(ti, idx);
546        }
547
548        let mut visited = vec![false; live_tris.len()];
549        let mut regions: Vec<Vec<usize>> = Vec::new();
550
551        // Flood-fill to find connected components.
552        for start_idx in 0..live_tris.len() {
553            if visited[start_idx] {
554                continue;
555            }
556
557            let mut component: Vec<usize> = Vec::new();
558            let mut stack: Vec<usize> = vec![live_tris[start_idx]];
559
560            while let Some(ti) = stack.pop() {
561                let Some(&idx) = tri_to_idx.get(&ti) else {
562                    continue;
563                };
564                if visited[idx] {
565                    continue;
566                }
567                visited[idx] = true;
568                component.push(ti);
569
570                // Traverse to adjacent triangles, stopping at separator edges.
571                let tri = &self.triangles[ti];
572                for local in 0..3 {
573                    let va = tri.v[(local + 1) % 3];
574                    let vb = tri.v[(local + 2) % 3];
575                    let edge_key = sorted_pair(va, vb);
576
577                    // Don't cross separator edges.
578                    if sep_set.contains(&edge_key) {
579                        continue;
580                    }
581
582                    if let Some(adj) = tri.adj[local]
583                        && let Some(&adj_idx) = tri_to_idx.get(&adj)
584                        && !visited[adj_idx]
585                    {
586                        stack.push(adj);
587                    }
588                }
589            }
590
591            if !component.is_empty() {
592                regions.push(component);
593            }
594        }
595
596        // Extract boundary polygon for each region.
597        regions
598            .iter()
599            .filter_map(|component| {
600                let polygon = walk_region_boundary(component, &self.triangles, &self.vertices, sc);
601                if polygon.len() >= 3 {
602                    Some(polygon)
603                } else {
604                    None
605                }
606            })
607            .collect()
608    }
609
610    /// Get the set of constraint edges (sorted pairs).
611    ///
612    /// Useful for distinguishing boundary constraints from interior
613    /// (separator) constraints in callers like NURBS boolean splitting.
614    #[must_use]
615    pub fn constraint_edges(&self) -> &DetHashSet<(usize, usize)> {
616        &self.constraints
617    }
618}
619
620// ---------------------------------------------------------------------------
621// Helpers
622// ---------------------------------------------------------------------------
623
624/// Walk the boundary edges of a set of triangles, producing an ordered polygon.
625///
626/// Given a set of triangle indices and the full triangle list + vertices,
627/// finds edges that appear exactly once in the set (boundary edges) and
628/// orders them into a polygon loop.
629fn walk_region_boundary(
630    region_tris: &[usize],
631    triangles: &[CdtTriangle],
632    vertices: &[Point2],
633    super_count: usize,
634) -> Vec<Point2> {
635    use crate::det_hash::DetHashMap;
636
637    // Count how many times each edge appears in the region.
638    // An edge appearing once is a boundary edge.
639    let mut edge_count: DetHashMap<(usize, usize), Vec<(usize, usize)>> = DetHashMap::default();
640    for &ti in region_tris {
641        let tri = &triangles[ti];
642        for local in 0..3 {
643            let va = tri.v[(local + 1) % 3];
644            let vb = tri.v[(local + 2) % 3];
645            let key = sorted_pair(va, vb);
646            // Store the directed edge (va, vb) — CCW winding of the triangle.
647            edge_count.entry(key).or_default().push((va, vb));
648        }
649    }
650
651    // Boundary edges: appear exactly once. Keep them directed (CCW winding).
652    let mut next_map: DetHashMap<usize, usize> = DetHashMap::default();
653    for directed_edges in edge_count.values() {
654        if directed_edges.len() == 1 {
655            let (va, vb) = directed_edges[0];
656            // Skip super-triangle vertices.
657            if va < super_count || vb < super_count {
658                continue;
659            }
660            next_map.insert(va, vb);
661        }
662    }
663
664    if next_map.is_empty() {
665        return Vec::new();
666    }
667
668    // Walk the boundary loop starting from any vertex.
669    let &start = next_map.keys().next().unwrap_or(&0);
670    let mut polygon = Vec::with_capacity(next_map.len());
671    let mut current = start;
672    let max_steps = next_map.len() + 1;
673    for _ in 0..max_steps {
674        polygon.push(vertices[current]);
675        match next_map.get(&current) {
676            Some(&next) => {
677                if next == start {
678                    break;
679                }
680                current = next;
681            }
682            None => break,
683        }
684    }
685
686    polygon
687}
688
689/// Return a sorted pair `(min, max)`.
690fn sorted_pair(a: usize, b: usize) -> (usize, usize) {
691    if a <= b { (a, b) } else { (b, a) }
692}
693
694/// Compute the intersection point of two line segments, if they cross.
695fn segment_intersection_point(a0: Point2, a1: Point2, b0: Point2, b1: Point2) -> Option<Point2> {
696    let dx_a = a1.x() - a0.x();
697    let dy_a = a1.y() - a0.y();
698    let dx_b = b1.x() - b0.x();
699    let dy_b = b1.y() - b0.y();
700    let denom = dx_a * dy_b - dy_a * dx_b;
701    if denom.abs() < 1e-15 {
702        return None;
703    }
704    let dx_ab = b0.x() - a0.x();
705    let dy_ab = b0.y() - a0.y();
706    let t = (dx_ab * dy_b - dy_ab * dx_b) / denom;
707    let u = (dx_ab * dy_a - dy_ab * dx_a) / denom;
708    if t > 0.0 && t < 1.0 && u > 0.0 && u < 1.0 {
709        Some(Point2::new(
710            dx_a.mul_add(t, a0.x()),
711            dy_a.mul_add(t, a0.y()),
712        ))
713    } else {
714        None
715    }
716}
717
718/// Test if two line segments properly intersect (crossing, not just touching).
719fn segments_properly_intersect(a0: Point2, a1: Point2, b0: Point2, b1: Point2) -> bool {
720    let d1 = orient2d(a0, a1, b0);
721    let d2 = orient2d(a0, a1, b1);
722    let d3 = orient2d(b0, b1, a0);
723    let d4 = orient2d(b0, b1, a1);
724
725    // Segments cross if endpoints of each are on opposite sides of the other.
726    if d1 * d2 < 0.0 && d3 * d4 < 0.0 {
727        return true;
728    }
729    false
730}
731
732/// Map a 2D point to a grid cell for duplicate detection.
733/// Cell size is much larger than `DUP_TOL` so neighbors cover the tolerance radius.
734#[allow(clippy::cast_possible_truncation)]
735fn dup_grid_cell(p: Point2) -> (i64, i64) {
736    // Cell size ~1e-5: 1000× DUP_TOL to keep neighbor checks cheap while
737    // ensuring points within DUP_TOL always land in the same or adjacent cells.
738    const CELL_INV: f64 = 1e5;
739    (
740        (p.x() * CELL_INV).floor() as i64,
741        (p.y() * CELL_INV).floor() as i64,
742    )
743}
744
745/// Map (x, y) in [0, n) × [0, n) to a Hilbert curve index (n must be power of 2).
746fn hilbert_xy_to_d(n: u32, mut x: u32, mut y: u32) -> u64 {
747    let mut d: u64 = 0;
748    let mut s = n / 2;
749    while s > 0 {
750        let rx = u32::from(x & s > 0);
751        let ry = u32::from(y & s > 0);
752        d += u64::from(s) * u64::from(s) * u64::from((3 * rx) ^ ry);
753        // Rotate quadrant.
754        if ry == 0 {
755            if rx == 1 {
756                x = 2u32.wrapping_mul(s).wrapping_sub(1).wrapping_sub(x);
757                y = 2u32.wrapping_mul(s).wrapping_sub(1).wrapping_sub(y);
758            }
759            std::mem::swap(&mut x, &mut y);
760        }
761        s /= 2;
762    }
763    d
764}