Skip to main content

neco_cdt/
cdt.rs

1//! 2D Constrained Delaunay Triangulation (CDT).
2//!
3//! Bowyer-Watson incremental insertion + edge-flip constraint recovery.
4//! Uses crate-local adaptive predicates for exact orientation and in-circle evaluation.
5
6use std::collections::HashSet;
7use std::error::Error;
8use std::fmt;
9
10use crate::predicates::{incircle, orient2d};
11
12/// Sentinel for no adjacent triangle.
13const NONE: usize = usize::MAX;
14
15/// 2D Constrained Delaunay Triangulation.
16#[derive(Debug, Clone)]
17pub struct Cdt {
18    /// Vertex coordinates (first 3 are the super-triangle).
19    vertices: Vec<[f64; 2]>,
20    /// Triangles (CCW vertex-index triples).
21    triangles: Vec<[usize; 3]>,
22    /// Adjacent triangles. `adjacency[t][i]` = triangle adjacent to edge (v[(i+1)%3], v[(i+2)%3]).
23    adjacency: Vec<[usize; 3]>,
24    /// Constraint edge set (normalized: (min, max)).
25    constraints: HashSet<(usize, usize)>,
26    /// Number of super-triangle vertices (always 3).
27    n_super: usize,
28    /// Start hint for walk-based point location.
29    last_triangle: usize,
30}
31
32/// Errors produced by constrained edge recovery.
33#[derive(Debug, Clone, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum CdtError {
36    /// Constraint edge recovery did not converge within the iteration budget.
37    ConstraintRecoveryDidNotConverge {
38        /// User-vertex indices of the failed edge.
39        edge: (usize, usize),
40        /// Maximum number of iterations attempted.
41        iterations: usize,
42    },
43}
44
45impl fmt::Display for CdtError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::ConstraintRecoveryDidNotConverge { edge, iterations } => write!(
49                f,
50                "constraint edge ({}, {}) recovery did not converge after {} iterations",
51                edge.0, edge.1, iterations
52            ),
53        }
54    }
55}
56
57impl Error for CdtError {}
58
59/// Normalized edge key.
60#[inline]
61fn edge_key(a: usize, b: usize) -> (usize, usize) {
62    if a < b {
63        (a, b)
64    } else {
65        (b, a)
66    }
67}
68
69impl Cdt {
70    /// Initialize an empty CDT with a super-triangle.
71    ///
72    /// Generates a super-triangle large enough to enclose `bounds` = (min_x, min_y, max_x, max_y).
73    pub fn new(bounds: (f64, f64, f64, f64)) -> Self {
74        let (min_x, min_y, max_x, max_y) = bounds;
75        let dx = max_x - min_x;
76        let dy = max_y - min_y;
77        let d = dx.max(dy).max(1e-10);
78        let cx = (min_x + max_x) * 0.5;
79        let cy = (min_y + max_y) * 0.5;
80
81        // Super-triangle: sufficiently large equilateral triangle
82        let margin = 10.0 * d;
83        let v0 = [cx - margin, cy - margin];
84        let v1 = [cx + margin, cy - margin];
85        let v2 = [cx, cy + margin];
86
87        Cdt {
88            vertices: vec![v0, v1, v2],
89            triangles: vec![[0, 1, 2]],
90            adjacency: vec![[NONE; 3]],
91            constraints: HashSet::new(),
92            n_super: 3,
93            last_triangle: 0,
94        }
95    }
96
97    /// Insert a vertex and return its index.
98    ///
99    /// Bowyer-Watson incremental insertion.
100    pub fn insert(&mut self, x: f64, y: f64) -> usize {
101        let vi = self.vertices.len();
102        self.vertices.push([x, y]);
103
104        // Find the triangle containing the point
105        let ti = match self.locate(x, y) {
106            Some(t) => t,
107            None => {
108                // Outside all triangles (beyond super-triangle) -- should not happen, fallback
109                // Use the nearest triangle
110                self.nearest_triangle(x, y)
111            }
112        };
113
114        // Bowyer-Watson: collect triangles whose circumcircle contains the point
115        let bad = self.find_bad_triangles(vi, ti);
116
117        // Find boundary edges of bad triangles
118        let boundary = self.find_boundary(&bad);
119
120        // Remove bad triangles and fill with new ones
121        self.re_triangulate(vi, &bad, &boundary);
122
123        vi
124    }
125
126    /// Add constraint edges.
127    ///
128    /// Inserts each point and registers constraint edges between consecutive points.
129    /// If `closed` is true, also connects the last point to the first.
130    pub fn add_constraint_edges(
131        &mut self,
132        points: &[[f64; 2]],
133        closed: bool,
134    ) -> Result<(), CdtError> {
135        if points.is_empty() {
136            return Ok(());
137        }
138
139        // Reuse exact duplicate vertices so closed polylines and sampled curves
140        // do not create overlapping vertices that make constraint recovery fail.
141        let indices: Vec<usize> = points
142            .iter()
143            .map(|p| self.find_or_insert_vertex(p[0], p[1]))
144            .collect();
145
146        // Register and recover constraint edges
147        let n = indices.len();
148        for i in 0..n {
149            let next = if i + 1 < n {
150                i + 1
151            } else if closed {
152                0
153            } else {
154                break;
155            };
156            let a = indices[i];
157            let b = indices[next];
158            if a == b {
159                continue;
160            }
161            let chain = self.constraint_chain(a, b);
162            for edge in chain.windows(2) {
163                let start = edge[0];
164                let end = edge[1];
165                if start != end {
166                    self.enforce_constraint(start, end)?;
167                }
168            }
169        }
170
171        Ok(())
172    }
173
174    /// Return the number of user vertices (excluding super-triangle).
175    pub fn n_user_vertices(&self) -> usize {
176        self.vertices.len() - self.n_super
177    }
178
179    /// Convert a vertex index to a user index (without super-triangle offset).
180    #[inline]
181    fn user_index(&self, vi: usize) -> usize {
182        debug_assert!(vi >= self.n_super);
183        vi - self.n_super
184    }
185
186    /// Return triangles that do not include super-triangle vertices.
187    /// Vertex indices in the returned triangles are user indices (0-based).
188    pub fn triangles(&self) -> Vec<[usize; 3]> {
189        let mut result = Vec::new();
190        for tri in &self.triangles {
191            // Exclude triangles containing super-triangle vertices
192            if tri[0] < self.n_super || tri[1] < self.n_super || tri[2] < self.n_super {
193                continue;
194            }
195            result.push([
196                self.user_index(tri[0]),
197                self.user_index(tri[1]),
198                self.user_index(tri[2]),
199            ]);
200        }
201        result
202    }
203
204    /// Return user vertex coordinates (excluding super-triangle).
205    pub fn user_vertices(&self) -> &[[f64; 2]] {
206        &self.vertices[self.n_super..]
207    }
208
209    // ── Internal methods ──
210
211    /// Find the triangle containing point (x, y) by adjacency walk.
212    fn locate(&self, x: f64, y: f64) -> Option<usize> {
213        if self.triangles.is_empty() {
214            return None;
215        }
216
217        let p = [x, y];
218        let mut ti = self
219            .last_triangle
220            .min(self.triangles.len().saturating_sub(1));
221
222        for _ in 0..self.triangles.len() {
223            let tri = self.triangles[ti];
224            let a = self.vertices[tri[0]];
225            let b = self.vertices[tri[1]];
226            let c = self.vertices[tri[2]];
227
228            let ab = orient2d(a, b, p);
229            let bc = orient2d(b, c, p);
230            let ca = orient2d(c, a, p);
231
232            if ab >= 0.0 && bc >= 0.0 && ca >= 0.0 {
233                return Some(ti);
234            }
235
236            let edge = if ab < bc && ab < ca {
237                0
238            } else if bc < ca {
239                1
240            } else {
241                2
242            };
243
244            let adj = self.adjacency[ti][edge];
245            if adj == NONE {
246                return self.locate_linear(x, y);
247            }
248            ti = adj;
249        }
250
251        self.locate_linear(x, y)
252    }
253
254    /// Linear fallback for point location.
255    fn locate_linear(&self, x: f64, y: f64) -> Option<usize> {
256        let p = [x, y];
257        for (ti, tri) in self.triangles.iter().enumerate() {
258            let a = self.vertices[tri[0]];
259            let b = self.vertices[tri[1]];
260            let c = self.vertices[tri[2]];
261            if orient2d(a, b, p) >= 0.0 && orient2d(b, c, p) >= 0.0 && orient2d(c, a, p) >= 0.0 {
262                return Some(ti);
263            }
264        }
265        None
266    }
267
268    /// Nearest triangle (fallback).
269    fn nearest_triangle(&self, x: f64, y: f64) -> usize {
270        let mut best = 0;
271        let mut best_dist = f64::INFINITY;
272        for (ti, tri) in self.triangles.iter().enumerate() {
273            let cx =
274                (self.vertices[tri[0]][0] + self.vertices[tri[1]][0] + self.vertices[tri[2]][0])
275                    / 3.0;
276            let cy =
277                (self.vertices[tri[0]][1] + self.vertices[tri[1]][1] + self.vertices[tri[2]][1])
278                    / 3.0;
279            let d = (cx - x) * (cx - x) + (cy - y) * (cy - y);
280            if d < best_dist {
281                best_dist = d;
282                best = ti;
283            }
284        }
285        best
286    }
287
288    fn find_or_insert_vertex(&mut self, x: f64, y: f64) -> usize {
289        if let Some((offset, _)) = self.vertices[self.n_super..]
290            .iter()
291            .enumerate()
292            .find(|(_, v)| v[0] == x && v[1] == y)
293        {
294            return self.n_super + offset;
295        }
296        self.insert(x, y)
297    }
298
299    /// Bowyer-Watson: BFS to collect connected triangles whose circumcircle contains vertex vi.
300    fn find_bad_triangles(&self, vi: usize, start: usize) -> Vec<usize> {
301        let p = self.vertices[vi];
302        let mut bad = Vec::new();
303        let mut stack = vec![start];
304        let mut visited = vec![false; self.triangles.len()];
305
306        while let Some(ti) = stack.pop() {
307            if visited[ti] {
308                continue;
309            }
310            visited[ti] = true;
311            let tri = self.triangles[ti];
312            let a = self.vertices[tri[0]];
313            let b = self.vertices[tri[1]];
314            let c = self.vertices[tri[2]];
315
316            let o = orient2d(a, b, c);
317            let in_circle = if o > 0.0 {
318                incircle(a, b, c, p) > 0.0
319            } else if o < 0.0 {
320                incircle(b, a, c, p) > 0.0
321            } else {
322                true
323            };
324
325            if in_circle {
326                bad.push(ti);
327                for &adj in &self.adjacency[ti] {
328                    if adj != NONE {
329                        stack.push(adj);
330                    }
331                }
332            }
333        }
334        bad
335    }
336
337    /// Find boundary edges of the bad triangle set.
338    fn find_boundary(&self, bad: &[usize]) -> Vec<(usize, usize, usize)> {
339        let mut is_bad = vec![false; self.triangles.len()];
340        for &ti in bad {
341            is_bad[ti] = true;
342        }
343        let mut boundary = Vec::new();
344
345        for &ti in bad {
346            let tri = self.triangles[ti];
347            for i in 0..3 {
348                let adj = self.adjacency[ti][i];
349                let va = tri[(i + 1) % 3];
350                let vb = tri[(i + 2) % 3];
351                if adj == NONE || !is_bad[adj] {
352                    boundary.push((va, vb, adj));
353                }
354            }
355        }
356        boundary
357    }
358
359    /// Remove bad triangles and create new triangles from vertex vi to boundary edges.
360    fn re_triangulate(&mut self, vi: usize, bad: &[usize], boundary: &[(usize, usize, usize)]) {
361        let mut bad_sorted: Vec<usize> = bad.to_vec();
362        bad_sorted.sort_unstable();
363
364        let n_new = boundary.len();
365
366        let mut removed: HashSet<usize> = bad_sorted.iter().copied().collect();
367
368        let mut new_indices = Vec::with_capacity(n_new);
369
370        let mut reuse: Vec<usize> = bad_sorted.clone();
371        for _ in 0..n_new {
372            if let Some(slot) = reuse.pop() {
373                new_indices.push(slot);
374                removed.remove(&slot);
375            } else {
376                let idx = self.triangles.len();
377                self.triangles.push([0; 3]);
378                self.adjacency.push([NONE; 3]);
379                new_indices.push(idx);
380            }
381        }
382
383        let mut remaining_bad: Vec<usize> = removed.into_iter().collect();
384        remaining_bad.sort_unstable();
385
386        use std::collections::HashMap;
387        let mut vertex_to_new: HashMap<usize, usize> = HashMap::new();
388        for (k, &(va, _vb, _)) in boundary.iter().enumerate() {
389            vertex_to_new.insert(va, k);
390        }
391
392        for (k, &(va, vb, adj_outside)) in boundary.iter().enumerate() {
393            let new_ti = new_indices[k];
394            self.triangles[new_ti] = [vi, va, vb];
395
396            self.adjacency[new_ti][0] = adj_outside;
397
398            self.adjacency[new_ti][1] = if let Some(&other_k) = vertex_to_new.get(&vb) {
399                new_indices[other_k]
400            } else {
401                NONE
402            };
403
404            let mut adj2 = NONE;
405            for (j, &(_jva, jvb, _)) in boundary.iter().enumerate() {
406                if jvb == va {
407                    adj2 = new_indices[j];
408                    break;
409                }
410            }
411            self.adjacency[new_ti][2] = adj2;
412
413            if adj_outside != NONE {
414                for i in 0..3 {
415                    if bad_sorted.contains(&self.adjacency[adj_outside][i])
416                        || self.adjacency[adj_outside][i] == NONE
417                    {
418                        let ot = self.triangles[adj_outside];
419                        let ea = ot[(i + 1) % 3];
420                        let eb = ot[(i + 2) % 3];
421                        if (ea == va && eb == vb) || (ea == vb && eb == va) {
422                            self.adjacency[adj_outside][i] = new_ti;
423                            break;
424                        }
425                    }
426                }
427            }
428        }
429
430        remaining_bad.sort_unstable_by(|a, b| b.cmp(a));
431        for &slot in &remaining_bad {
432            let last = self.triangles.len() - 1;
433            if slot < last {
434                self.triangles[slot] = self.triangles[last];
435                self.adjacency[slot] = self.adjacency[last];
436                for i in 0..3 {
437                    let adj = self.adjacency[slot][i];
438                    if adj != NONE && adj < self.triangles.len() {
439                        for j in 0..3 {
440                            if self.adjacency[adj][j] == last {
441                                self.adjacency[adj][j] = slot;
442                            }
443                        }
444                    }
445                }
446                for ni in new_indices.iter_mut() {
447                    if *ni == last {
448                        *ni = slot;
449                    }
450                }
451            }
452            self.triangles.pop();
453            self.adjacency.pop();
454        }
455
456        self.last_triangle = new_indices.first().copied().unwrap_or(0);
457    }
458
459    /// Enforce constraint edge (a, b).
460    fn enforce_constraint(&mut self, a: usize, b: usize) -> Result<(), CdtError> {
461        let key = edge_key(a, b);
462        self.constraints.insert(key);
463
464        if self.find_edge_triangle(a, b).is_some() {
465            return Ok(());
466        }
467
468        let max_iter = self.triangles.len() * 4;
469        for _ in 0..max_iter {
470            if self.find_edge_triangle(a, b).is_some() {
471                return Ok(());
472            }
473
474            if !self.flip_one_crossing_edge(a, b) {
475                if self.find_edge_triangle(a, b).is_some() {
476                    return Ok(());
477                }
478                return Err(CdtError::ConstraintRecoveryDidNotConverge {
479                    edge: (self.user_index(a), self.user_index(b)),
480                    iterations: max_iter,
481                });
482            }
483        }
484
485        Err(CdtError::ConstraintRecoveryDidNotConverge {
486            edge: (self.user_index(a), self.user_index(b)),
487            iterations: max_iter,
488        })
489    }
490
491    fn constraint_chain(&self, a: usize, b: usize) -> Vec<usize> {
492        let pa = self.vertices[a];
493        let pb = self.vertices[b];
494        let ab = [pb[0] - pa[0], pb[1] - pa[1]];
495        let ab_len_sq = ab[0] * ab[0] + ab[1] * ab[1];
496        if ab_len_sq <= 1e-24 {
497            return vec![a, b];
498        }
499
500        let tol = 1e-12 * ab_len_sq.sqrt().max(1.0);
501        let mut interior = Vec::new();
502        for vi in self.n_super..self.vertices.len() {
503            if vi == a || vi == b {
504                continue;
505            }
506            let p = self.vertices[vi];
507            let ap = [p[0] - pa[0], p[1] - pa[1]];
508            let cross = orient2d(pa, pb, p).abs();
509            if cross > tol {
510                continue;
511            }
512            let dot = ap[0] * ab[0] + ap[1] * ab[1];
513            if dot <= tol || dot >= ab_len_sq - tol {
514                continue;
515            }
516            interior.push((dot / ab_len_sq, vi));
517        }
518
519        interior.sort_unstable_by(|(ta, _), (tb, _)| ta.total_cmp(tb));
520        let mut chain = Vec::with_capacity(interior.len() + 2);
521        chain.push(a);
522        chain.extend(interior.into_iter().map(|(_, vi)| vi));
523        chain.push(b);
524        chain
525    }
526
527    fn find_edge_triangle(&self, a: usize, b: usize) -> Option<(usize, usize)> {
528        for (ti, tri) in self.triangles.iter().enumerate() {
529            for i in 0..3 {
530                let va = tri[(i + 1) % 3];
531                let vb = tri[(i + 2) % 3];
532                if (va == a && vb == b) || (va == b && vb == a) {
533                    return Some((ti, i));
534                }
535            }
536        }
537        None
538    }
539
540    fn flip_one_crossing_edge(&mut self, a: usize, b: usize) -> bool {
541        let pa = self.vertices[a];
542        let pb = self.vertices[b];
543
544        let n_tri = self.triangles.len();
545        for ti in 0..n_tri {
546            let tri = self.triangles[ti];
547            for i in 0..3 {
548                let va = tri[(i + 1) % 3];
549                let vb = tri[(i + 2) % 3];
550
551                if va == a || va == b || vb == a || vb == b {
552                    continue;
553                }
554                if self.is_constraint(va, vb) {
555                    continue;
556                }
557
558                let pva = self.vertices[va];
559                let pvb = self.vertices[vb];
560
561                if segments_intersect(pa, pb, pva, pvb) {
562                    let adj = self.adjacency[ti][i];
563                    if adj != NONE {
564                        self.flip_edge(ti, adj, i);
565                        return true;
566                    }
567                }
568            }
569        }
570        false
571    }
572
573    #[inline]
574    fn is_constraint(&self, a: usize, b: usize) -> bool {
575        self.constraints.contains(&edge_key(a, b))
576    }
577
578    fn flip_edge(&mut self, t1: usize, t2: usize, edge_in_t1: usize) {
579        let tri1 = self.triangles[t1];
580        let tri2 = self.triangles[t2];
581
582        let e = edge_in_t1;
583        let shared_a = tri1[(e + 1) % 3];
584        let shared_b = tri1[(e + 2) % 3];
585        let apex1 = tri1[e];
586
587        let edge_in_t2 = self.find_local_edge(t2, shared_a, shared_b);
588        if edge_in_t2 == NONE {
589            return;
590        }
591        let apex2 = tri2[edge_in_t2];
592
593        self.triangles[t1] = [apex1, apex2, shared_b];
594        self.triangles[t2] = [apex2, apex1, shared_a];
595
596        let old_adj1 = self.adjacency[t1];
597        let old_adj2 = self.adjacency[t2];
598
599        let t1_adj_sb_apex1 = old_adj1[(e + 1) % 3];
600        let t1_adj_apex1_sa = old_adj1[(e + 2) % 3];
601
602        let t2_adj_sb_apex2 = {
603            let mut found = NONE;
604            for i in 0..3 {
605                let va = tri2[(i + 1) % 3];
606                let vb = tri2[(i + 2) % 3];
607                if (va == shared_b && vb == apex2) || (va == apex2 && vb == shared_b) {
608                    found = old_adj2[i];
609                    break;
610                }
611            }
612            found
613        };
614
615        let t2_adj_apex2_sa = {
616            let mut found = NONE;
617            for i in 0..3 {
618                let va = tri2[(i + 1) % 3];
619                let vb = tri2[(i + 2) % 3];
620                if (va == shared_a && vb == apex2) || (va == apex2 && vb == shared_a) {
621                    found = old_adj2[i];
622                    break;
623                }
624            }
625            found
626        };
627
628        self.adjacency[t1] = [t2_adj_sb_apex2, t1_adj_sb_apex1, t2];
629        self.adjacency[t2] = [t1_adj_apex1_sa, t2_adj_apex2_sa, t1];
630
631        if t2_adj_sb_apex2 != NONE {
632            self.update_adjacency_ref(t2_adj_sb_apex2, t2, t1);
633        }
634        if t1_adj_apex1_sa != NONE {
635            self.update_adjacency_ref(t1_adj_apex1_sa, t1, t2);
636        }
637    }
638
639    fn update_adjacency_ref(&mut self, tri_idx: usize, old_ref: usize, new_ref: usize) {
640        for i in 0..3 {
641            if self.adjacency[tri_idx][i] == old_ref {
642                self.adjacency[tri_idx][i] = new_ref;
643                return;
644            }
645        }
646    }
647
648    fn find_local_edge(&self, t: usize, a: usize, b: usize) -> usize {
649        let tri = self.triangles[t];
650        for i in 0..3 {
651            let va = tri[(i + 1) % 3];
652            let vb = tri[(i + 2) % 3];
653            if (va == a && vb == b) || (va == b && vb == a) {
654                return i;
655            }
656        }
657        NONE
658    }
659}
660
661/// Test whether segments (p1,p2) and (p3,p4) intersect in their interiors (shared endpoints excluded).
662fn segments_intersect(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2], p4: [f64; 2]) -> bool {
663    let d1 = orient2d(p3, p4, p1);
664    let d2 = orient2d(p3, p4, p2);
665    let d3 = orient2d(p1, p2, p3);
666    let d4 = orient2d(p1, p2, p4);
667
668    if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
669        && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
670    {
671        return true;
672    }
673    false
674}
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679    use crate::orient3d;
680
681    fn triangle_area(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> f64 {
682        ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])).abs() * 0.5
683    }
684
685    fn total_triangle_area(cdt: &Cdt) -> f64 {
686        let tris = cdt.triangles();
687        let verts = cdt.user_vertices();
688        tris.iter()
689            .map(|t| triangle_area(verts[t[0]], verts[t[1]], verts[t[2]]))
690            .sum()
691    }
692
693    fn polygon_area(points: &[[f64; 2]]) -> f64 {
694        let mut sum = 0.0;
695        for i in 0..points.len() {
696            let j = (i + 1) % points.len();
697            sum += points[i][0] * points[j][1] - points[j][0] * points[i][1];
698        }
699        sum.abs() * 0.5
700    }
701
702    fn collect_edges(tris: &[[usize; 3]]) -> HashSet<(usize, usize)> {
703        let mut edges = HashSet::new();
704        for t in tris {
705            for i in 0..3 {
706                edges.insert(edge_key(t[i], t[(i + 1) % 3]));
707            }
708        }
709        edges
710    }
711
712    #[test]
713    fn insert_single_point() {
714        let mut cdt = Cdt::new((0.0, 0.0, 1.0, 1.0));
715        cdt.insert(0.5, 0.5);
716        let tris = cdt.triangles();
717        assert!(tris.is_empty());
718    }
719
720    #[test]
721    fn insert_triangle() {
722        let mut cdt = Cdt::new((0.0, 0.0, 1.0, 1.0));
723        cdt.insert(0.0, 0.0);
724        cdt.insert(1.0, 0.0);
725        cdt.insert(0.5, 1.0);
726        let tris = cdt.triangles();
727        assert_eq!(tris.len(), 1);
728    }
729
730    #[test]
731    fn square_triangulation() {
732        let mut cdt = Cdt::new((0.0, 0.0, 1.0, 1.0));
733        cdt.insert(0.0, 0.0);
734        cdt.insert(1.0, 0.0);
735        cdt.insert(1.0, 1.0);
736        cdt.insert(0.0, 1.0);
737        let tris = cdt.triangles();
738        assert_eq!(tris.len(), 2);
739
740        let verts = cdt.user_vertices();
741        let total_area: f64 = tris
742            .iter()
743            .map(|t| {
744                let a = verts[t[0]];
745                let b = verts[t[1]];
746                let c = verts[t[2]];
747                ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])).abs() * 0.5
748            })
749            .sum();
750        assert!((total_area - 1.0).abs() < 1e-10);
751    }
752
753    #[test]
754    fn constraint_edges_preserved() {
755        let pts = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
756        let mut cdt = Cdt::new((-0.5, -0.5, 1.5, 1.5));
757        cdt.add_constraint_edges(&pts, true).unwrap();
758
759        let tris = cdt.triangles();
760        let mut edges: HashSet<(usize, usize)> = HashSet::new();
761        for t in &tris {
762            for i in 0..3 {
763                edges.insert(edge_key(t[i], t[(i + 1) % 3]));
764            }
765        }
766
767        let n = pts.len();
768        for i in 0..n {
769            let j = (i + 1) % n;
770            assert!(
771                edges.contains(&edge_key(i, j)),
772                "constraint edge ({}, {}) not found in triangulation",
773                i,
774                j
775            );
776        }
777    }
778
779    #[test]
780    fn delaunay_property() {
781        let points = [
782            [0.1, 0.2],
783            [0.8, 0.1],
784            [0.9, 0.9],
785            [0.2, 0.8],
786            [0.5, 0.5],
787            [0.3, 0.4],
788            [0.7, 0.6],
789        ];
790        let mut cdt = Cdt::new((-0.5, -0.5, 1.5, 1.5));
791        for p in &points {
792            cdt.insert(p[0], p[1]);
793        }
794
795        let tris = cdt.triangles();
796        let verts = cdt.user_vertices();
797
798        for tri in &tris {
799            let a = verts[tri[0]];
800            let b = verts[tri[1]];
801            let c = verts[tri[2]];
802            let (a, b, c) = if orient2d(a, b, c) >= 0.0 {
803                (a, b, c)
804            } else {
805                (a, c, b)
806            };
807
808            for (vi, v) in verts.iter().enumerate() {
809                if vi == tri[0] || vi == tri[1] || vi == tri[2] {
810                    continue;
811                }
812                let ic = incircle(a, b, c, *v);
813                assert!(
814                    ic <= 1e-10,
815                    "Delaunay violation: triangle {:?}, vertex {} (incircle={})",
816                    tri,
817                    vi,
818                    ic
819                );
820            }
821        }
822    }
823
824    #[test]
825    fn closed_constraint_polygon_is_order_invariant() {
826        let points = [[0.0, 0.0], [2.0, 0.0], [2.5, 1.0], [1.0, 2.0], [-0.5, 1.0]];
827
828        let mut forward = Cdt::new((-1.0, -1.0, 3.0, 3.0));
829        forward.add_constraint_edges(&points, true).unwrap();
830
831        let mut reversed_points = points;
832        reversed_points.reverse();
833        let mut reversed = Cdt::new((-1.0, -1.0, 3.0, 3.0));
834        reversed
835            .add_constraint_edges(&reversed_points, true)
836            .unwrap();
837
838        let expected_area = polygon_area(&points);
839        let forward_area = total_triangle_area(&forward);
840        let reversed_area = total_triangle_area(&reversed);
841        let forward_edges = collect_edges(&forward.triangles());
842        let reversed_edges = collect_edges(&reversed.triangles());
843
844        assert_eq!(forward.n_user_vertices(), points.len());
845        assert_eq!(reversed.n_user_vertices(), points.len());
846        assert!(!forward.triangles().is_empty());
847        assert!(!reversed.triangles().is_empty());
848        assert_eq!(forward.triangles().len(), reversed.triangles().len());
849        for i in 0..points.len() {
850            let j = (i + 1) % points.len();
851            let edge = edge_key(i, j);
852            assert!(forward_edges.contains(&edge));
853            assert!(reversed_edges.contains(&edge));
854        }
855        assert!((forward_area - expected_area).abs() < 1e-10);
856        assert!((reversed_area - expected_area).abs() < 1e-10);
857    }
858
859    #[test]
860    fn nearly_degenerate_constraint_chain_remains_valid() {
861        let points = [
862            [0.0, 0.0],
863            [1.0e-12, 1.0e-12],
864            [2.0, 0.0],
865            [2.0, 1.0],
866            [1.0, 1.0 + 1.0e-12],
867            [0.0, 1.0],
868        ];
869
870        let mut cdt = Cdt::new((-0.5, -0.5, 2.5, 1.5));
871        cdt.add_constraint_edges(&points, true).unwrap();
872
873        let tris = cdt.triangles();
874        let total_area = total_triangle_area(&cdt);
875
876        assert_eq!(cdt.n_user_vertices(), points.len());
877        assert!(!tris.is_empty());
878        assert!((total_area - polygon_area(&points)).abs() < 1e-10);
879
880        let edges = collect_edges(&tris);
881        for i in 0..points.len() {
882            let j = (i + 1) % points.len();
883            assert!(
884                edges.contains(&edge_key(i, j)),
885                "constraint edge ({}, {}) not found in triangulation",
886                i,
887                j
888            );
889        }
890    }
891
892    #[test]
893    fn orient_sign_contract_is_fixed() {
894        assert!(orient2d([0.0, 0.0], [1.0, 0.0], [0.0, 1.0]) > 0.0);
895        assert!(orient2d([0.0, 0.0], [0.0, 1.0], [1.0, 0.0]) < 0.0);
896        assert_eq!(orient2d([0.0, 0.0], [1.0, 0.0], [2.0, 0.0]), 0.0);
897
898        assert!(
899            orient3d(
900                [0.0, 0.0, 0.0],
901                [1.0, 0.0, 0.0],
902                [0.0, 1.0, 0.0],
903                [0.0, 0.0, -1.0]
904            ) > 0.0
905        );
906        assert!(
907            orient3d(
908                [0.0, 0.0, 0.0],
909                [1.0, 0.0, 0.0],
910                [0.0, 1.0, 0.0],
911                [0.0, 0.0, 1.0]
912            ) < 0.0
913        );
914        assert_eq!(
915            orient3d(
916                [0.0, 0.0, 0.0],
917                [1.0, 0.0, 0.0],
918                [0.0, 1.0, 0.0],
919                [0.5, 0.5, 0.0]
920            ),
921            0.0
922        );
923    }
924
925    #[test]
926    fn constraint_chain_splits_at_existing_collinear_vertices() {
927        let mut cdt = Cdt::new((-0.5, -0.5, 2.5, 1.5));
928        let a = cdt.insert(0.0, 0.0);
929        let b = cdt.insert(2.0, 0.0);
930        let mid = cdt.insert(1.0, 0.0);
931        cdt.insert(0.5, 1.0);
932        cdt.insert(1.5, 1.0);
933
934        let chain = cdt.constraint_chain(a, b);
935        assert_eq!(chain, vec![a, mid, b]);
936
937        for edge in chain.windows(2) {
938            cdt.enforce_constraint(edge[0], edge[1]).unwrap();
939        }
940
941        let edges = collect_edges(&cdt.triangles());
942        assert!(edges.contains(&edge_key(0, 2)));
943        assert!(edges.contains(&edge_key(1, 2)));
944    }
945}