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