use crate::Point2;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
const COS_MIN_ANGLE: f64 = 0.927_183_854_566_787_4;
const MAX_ASPECT: f64 = 7.0;
const MAX_REFINE_ITERS: usize = 20_000;
type P2 = [f64; 2];
const NONE: usize = usize::MAX;
#[inline]
fn p2(p: &Point2<f64>) -> P2 {
[p.x, p.y]
}
#[inline]
fn orient(a: P2, b: P2, c: P2) -> i32 {
let d = geometry_predicates::orient2d(a, b, c);
if d > 0.0 {
1
} else if d < 0.0 {
-1
} else {
0
}
}
#[inline]
fn in_circle_sign(a: P2, b: P2, c: P2, d: P2) -> i32 {
let v = geometry_predicates::incircle(a, b, c, d);
if v > 0.0 {
1
} else if v < 0.0 {
-1
} else {
0
}
}
#[inline]
fn ekey(a: usize, b: usize) -> (usize, usize) {
if a < b {
(a, b)
} else {
(b, a)
}
}
#[derive(Clone, Copy)]
struct Tri {
v: [usize; 3],
n: [usize; 3],
alive: bool,
}
impl Tri {
#[inline]
fn edge_of(&self, a: usize, b: usize) -> Option<usize> {
for e in 0..3 {
let x = self.v[e];
let y = self.v[(e + 1) % 3];
if (x == a && y == b) || (x == b && y == a) {
return Some(e);
}
}
None
}
}
struct Cdt {
points: Vec<P2>,
tris: Vec<Tri>,
constraints: BTreeSet<(usize, usize)>,
n_input: usize,
super_base: usize,
inside: Vec<bool>,
skinny: BTreeSet<usize>,
track: bool,
cos_min_angle: f64,
}
#[derive(Clone, Copy, PartialEq)]
enum Action {
SplitSegment(usize, usize),
AddPoint(P2),
Done,
}
impl Cdt {
fn build_from(mut points: Vec<P2>, segments: &[(usize, usize)]) -> Option<Cdt> {
let n_input = points.len();
if n_input < 3 {
return None;
}
let mut constraints = BTreeSet::new();
for &(a, b) in segments {
if a != b && a < n_input && b < n_input {
constraints.insert(ekey(a, b));
}
}
let (mut minx, mut miny, mut maxx, mut maxy) =
(f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for p in &points {
if !p[0].is_finite() || !p[1].is_finite() {
return None;
}
minx = minx.min(p[0]);
miny = miny.min(p[1]);
maxx = maxx.max(p[0]);
maxy = maxy.max(p[1]);
}
let span = (maxx - minx).max(maxy - miny);
if !(span > 0.0) || !span.is_finite() {
return None;
}
let cx = (minx + maxx) * 0.5;
let cy = (miny + maxy) * 0.5;
let big = span * 32.0;
let super_base = points.len();
points.push([cx - big, cy - big]);
points.push([cx + big, cy - big]);
points.push([cx, cy + big]);
let mut cdt = Cdt {
points,
tris: Vec::new(),
constraints,
n_input,
super_base,
inside: Vec::new(),
skinny: BTreeSet::new(),
track: false,
cos_min_angle: COS_MIN_ANGLE,
};
cdt.tris.push(Tri {
v: [super_base, super_base + 1, super_base + 2],
n: [NONE; 3],
alive: true,
});
cdt.inside.push(false);
for vi in 0..n_input {
cdt.insert_point(vi);
}
if !cdt.enforce_constraints() {
return None;
}
cdt.restore_constrained_delaunay();
Some(cdt)
}
fn insert_point(&mut self, vi: usize) {
let p = self.points[vi];
let Some(start) = self.locate(p) else {
return;
};
self.insert_point_at(vi, start);
}
fn insert_point_at(&mut self, vi: usize, start: usize) {
let p = self.points[vi];
let region = self.inside.get(start).copied().unwrap_or(false);
let mut bad: Vec<usize> = Vec::new();
let mut in_bad: BTreeSet<usize> = BTreeSet::new();
let mut queue: VecDeque<usize> = VecDeque::new();
queue.push_back(start);
let mut visited: BTreeSet<usize> = BTreeSet::new();
visited.insert(start);
while let Some(ti) = queue.pop_front() {
if !self.tris[ti].alive {
continue;
}
let v = self.tris[ti].v;
if in_circle_sign(self.points[v[0]], self.points[v[1]], self.points[v[2]], p) > 0 {
bad.push(ti);
in_bad.insert(ti);
for e in 0..3 {
let a = v[e];
let b = v[(e + 1) % 3];
if self.constraints.contains(&ekey(a, b)) {
continue; }
let nb = self.tris[ti].n[e];
if nb != NONE && visited.insert(nb) {
queue.push_back(nb);
}
}
}
}
if bad.is_empty() {
self.split_at(start, vi);
return;
}
let mut boundary: Vec<(usize, usize, usize)> = Vec::new();
for &ti in &bad {
let v = self.tris[ti].v;
for e in 0..3 {
let nb = self.tris[ti].n[e];
if nb == NONE || !in_bad.contains(&nb) {
let a = v[e];
let b = v[(e + 1) % 3];
boundary.push((a, b, nb));
}
}
}
boundary.sort_unstable();
for &ti in &bad {
self.tris[ti].alive = false;
}
let mut owner: BTreeMap<(usize, usize), (usize, usize)> = BTreeMap::new();
let mut new_tris: Vec<usize> = Vec::with_capacity(boundary.len());
for &(a, b, outside) in &boundary {
let ti = self.tris.len();
self.tris.push(Tri {
v: [a, b, vi],
n: [NONE; 3],
alive: true,
});
self.inside.push(region);
new_tris.push(ti);
self.tris[ti].n[0] = outside;
if outside != NONE {
if let Some(e) = self.tris[outside].edge_of(a, b) {
self.tris[outside].n[e] = ti;
}
}
self.link_internal(&mut owner, ekey(b, vi), ti, 1);
self.link_internal(&mut owner, ekey(vi, a), ti, 2);
}
let mut stack: Vec<(usize, usize)> = new_tris.iter().map(|&t| (t, 0usize)).collect();
self.legalize(&mut stack);
for t in new_tris {
self.track_tri(t);
}
}
fn link_internal(
&mut self,
owner: &mut BTreeMap<(usize, usize), (usize, usize)>,
key: (usize, usize),
ti: usize,
e: usize,
) {
if let Some(&(ot, oe)) = owner.get(&key) {
self.tris[ti].n[e] = ot;
self.tris[ot].n[oe] = ti;
} else {
owner.insert(key, (ti, e));
}
}
fn split_in_triangle(&mut self, t: usize, vi: usize) {
if !self.tris[t].alive {
return;
}
let region = self.inside.get(t).copied().unwrap_or(false);
let v = self.tris[t].v;
let n = self.tris[t].n;
self.tris[t].alive = false;
let mut owner: BTreeMap<(usize, usize), (usize, usize)> = BTreeMap::new();
let mut children: Vec<usize> = Vec::new();
for e in 0..3 {
let a = v[e];
let b = v[(e + 1) % 3];
if orient(self.points[a], self.points[b], self.points[vi]) == 0 {
continue; }
let ti = self.tris.len();
self.tris.push(Tri {
v: [a, b, vi],
n: [NONE; 3],
alive: true,
});
self.inside.push(region);
children.push(ti);
self.tris[ti].n[0] = n[e];
if n[e] != NONE {
if let Some(oe) = self.tris[n[e]].edge_of(a, b) {
self.tris[n[e]].n[oe] = ti;
}
}
self.link_internal(&mut owner, ekey(b, vi), ti, 1);
self.link_internal(&mut owner, ekey(vi, a), ti, 2);
}
let mut stack: Vec<(usize, usize)> = children.iter().map(|&c| (c, 0usize)).collect();
self.legalize(&mut stack);
for c in children {
self.track_tri(c);
}
}
fn split_at(&mut self, start: usize, vi: usize) {
if !self.tris[start].alive {
return;
}
let v = self.tris[start].v;
let p = self.points[vi];
for e in 0..3 {
let a = self.points[v[e]];
let b = self.points[v[(e + 1) % 3]];
if orient(a, b, p) == 0 && strictly_between(a, b, p) {
self.split_on_edge(start, e, vi);
return;
}
}
self.split_in_triangle(start, vi);
}
fn split_on_edge(&mut self, t: usize, e: usize, vi: usize) {
let v = self.tris[t].v;
let n = self.tris[t].n;
let a = v[e];
let b = v[(e + 1) % 3];
let c = v[(e + 2) % 3];
let nb = n[e];
let n_bc = n[(e + 1) % 3];
let n_ca = n[(e + 2) % 3];
let region_t = self.inside.get(t).copied().unwrap_or(false);
if self.constraints.remove(&ekey(a, b)) {
self.constraints.insert(ekey(a, vi));
self.constraints.insert(ekey(vi, b));
}
self.tris[t].alive = false;
if nb == NONE {
let t1 = self.tris.len(); let t2 = t1 + 1; self.tris.push(Tri { v: [vi, b, c], n: [NONE, n_bc, t2], alive: true });
self.tris.push(Tri { v: [a, vi, c], n: [NONE, t1, n_ca], alive: true });
self.inside.push(region_t);
self.inside.push(region_t);
for (ext, x, y, child) in [(n_bc, b, c, t1), (n_ca, c, a, t2)] {
if ext != NONE {
if let Some(oe) = self.tris[ext].edge_of(x, y) {
self.tris[ext].n[oe] = child;
}
}
}
let mut stack: Vec<(usize, usize)> = vec![(t1, 1), (t2, 2)];
self.legalize(&mut stack);
self.track_tri(t1);
self.track_tri(t2);
return;
}
let region_nb = self.inside.get(nb).copied().unwrap_or(false);
let d = self.tris[nb]
.v
.iter()
.copied()
.find(|&x| x != a && x != b)
.expect("cdt: neighbour across a shared edge must have an apex vertex");
let outer_of = |s: &Self, t: usize, x: usize, y: usize| -> usize {
s.tris[t].edge_of(x, y).map(|oe| s.tris[t].n[oe]).unwrap_or(NONE)
};
let n_ad = outer_of(self, nb, a, d);
let n_db = outer_of(self, nb, d, b);
self.tris[nb].alive = false;
let t1 = self.tris.len(); let t2 = t1 + 1; let t3 = t1 + 2; let t4 = t1 + 3; self.tris.push(Tri { v: [vi, b, c], n: [t3, n_bc, t2], alive: true });
self.tris.push(Tri { v: [a, vi, c], n: [t4, t1, n_ca], alive: true });
self.tris.push(Tri { v: [b, vi, d], n: [t1, t4, n_db], alive: true });
self.tris.push(Tri { v: [vi, a, d], n: [t2, n_ad, t3], alive: true });
self.inside.push(region_t);
self.inside.push(region_t);
self.inside.push(region_nb);
self.inside.push(region_nb);
for (ext, x, y, child) in [
(n_bc, b, c, t1),
(n_ca, c, a, t2),
(n_db, d, b, t3),
(n_ad, a, d, t4),
] {
if ext != NONE {
if let Some(oe) = self.tris[ext].edge_of(x, y) {
self.tris[ext].n[oe] = child;
}
}
}
let mut stack: Vec<(usize, usize)> = vec![(t1, 1), (t2, 2), (t3, 2), (t4, 1)];
self.legalize(&mut stack);
for ti in [t1, t2, t3, t4] {
self.track_tri(ti);
}
}
fn legalize(&mut self, stack: &mut Vec<(usize, usize)>) {
let mut guard = 0usize;
while let Some((ti, e)) = stack.pop() {
guard += 1;
if guard > 4_000_000 {
break;
}
if !self.tris[ti].alive {
continue;
}
let a = self.tris[ti].v[e];
let b = self.tris[ti].v[(e + 1) % 3];
let apex = self.tris[ti].v[(e + 2) % 3];
if self.constraints.contains(&ekey(a, b)) {
continue;
}
let opp = self.tris[ti].n[e];
if opp == NONE || !self.tris[opp].alive {
continue;
}
let ov = self.tris[opp].v;
let q = ov.iter().copied().find(|&x| x != a && x != b);
let Some(q) = q else { continue };
let tv = self.tris[ti].v;
if in_circle_sign(
self.points[tv[0]],
self.points[tv[1]],
self.points[tv[2]],
self.points[q],
) <= 0
{
continue;
}
let sa = orient(self.points[apex], self.points[q], self.points[a]);
let sb = orient(self.points[apex], self.points[q], self.points[b]);
if sa == 0 || sb == 0 || sa == sb {
continue;
}
self.flip(ti, opp, a, b, apex, q, stack);
}
}
#[allow(clippy::too_many_arguments)]
fn flip(
&mut self,
ti: usize,
opp: usize,
a: usize,
b: usize,
apex: usize,
q: usize,
stack: &mut Vec<(usize, usize)>,
) {
let outer = |s: &Self, t: usize, x: usize, y: usize| -> usize {
s.tris[t].edge_of(x, y).map(|e| s.tris[t].n[e]).unwrap_or(NONE)
};
let n_apex_a = outer(self, ti, apex, a); let n_apex_b = outer(self, ti, apex, b); let n_q_a = outer(self, opp, q, a); let n_q_b = outer(self, opp, q, b);
let mk = |s: &Self, x: usize, y: usize, z: usize| -> [usize; 3] {
if orient(s.points[x], s.points[y], s.points[z]) >= 0 {
[x, y, z]
} else {
[x, z, y]
}
};
let t0 = mk(self, apex, q, a);
let t1 = mk(self, apex, q, b);
self.tris[ti] = Tri {
v: t0,
n: [NONE; 3],
alive: true,
};
self.tris[opp] = Tri {
v: t1,
n: [NONE; 3],
alive: true,
};
let set_nb = |s: &mut Self, t: usize, x: usize, y: usize, val: usize| {
if let Some(e) = s.tris[t].edge_of(x, y) {
s.tris[t].n[e] = val;
}
};
set_nb(self, ti, apex, q, opp);
set_nb(self, opp, apex, q, ti);
set_nb(self, ti, apex, a, n_apex_a);
set_nb(self, ti, q, a, n_q_a);
set_nb(self, opp, apex, b, n_apex_b);
set_nb(self, opp, q, b, n_q_b);
if n_apex_a != NONE {
if let Some(e) = self.tris[n_apex_a].edge_of(apex, a) {
self.tris[n_apex_a].n[e] = ti;
}
}
if n_q_a != NONE {
if let Some(e) = self.tris[n_q_a].edge_of(q, a) {
self.tris[n_q_a].n[e] = ti;
}
}
if n_apex_b != NONE {
if let Some(e) = self.tris[n_apex_b].edge_of(apex, b) {
self.tris[n_apex_b].n[e] = opp;
}
}
if n_q_b != NONE {
if let Some(e) = self.tris[n_q_b].edge_of(q, b) {
self.tris[n_q_b].n[e] = opp;
}
}
for (t, x, y) in [
(ti, apex, a),
(ti, q, a),
(opp, apex, b),
(opp, q, b),
] {
if let Some(e) = self.tris[t].edge_of(x, y) {
stack.push((t, e));
}
}
self.track_tri(ti);
self.track_tri(opp);
}
#[inline]
fn track_tri(&mut self, ti: usize) {
if !self.track {
return;
}
let cand = self.tris[ti].alive
&& self.inside.get(ti).copied().unwrap_or(false)
&& !self.tris[ti].v.iter().any(|&x| x >= self.super_base)
&& self.tri_is_skinny(ti, self.cos_min_angle);
if cand {
self.skinny.insert(ti);
} else {
self.skinny.remove(&ti);
}
}
fn start_refinement(&mut self, cos_min_angle: f64) {
self.inside = self.inside_flags();
self.cos_min_angle = cos_min_angle;
self.track = true;
self.skinny.clear();
for ti in 0..self.tris.len() {
self.track_tri(ti);
}
}
fn locate(&self, p: P2) -> Option<usize> {
let mut on_edge = None;
for ti in 0..self.tris.len() {
if !self.tris[ti].alive {
continue;
}
let v = self.tris[ti].v;
let o0 = orient(self.points[v[0]], self.points[v[1]], p);
let o1 = orient(self.points[v[1]], self.points[v[2]], p);
let o2 = orient(self.points[v[2]], self.points[v[0]], p);
if o0 >= 0 && o1 >= 0 && o2 >= 0 {
if o0 > 0 && o1 > 0 && o2 > 0 {
return Some(ti);
}
on_edge.get_or_insert(ti);
}
}
on_edge
}
fn locate_from(&self, start: usize, p: P2) -> Option<usize> {
if !self.tris.get(start).is_some_and(|t| t.alive) {
return self.locate(p);
}
let mut cur = start;
let cap = self.tris.len() * 2 + 16;
for _ in 0..cap {
let v = self.tris[cur].v;
let mut moved = false;
for e in 0..3 {
let a = self.points[v[e]];
let b = self.points[v[(e + 1) % 3]];
if orient(a, b, p) < 0 {
let nb = self.tris[cur].n[e];
if nb == NONE || !self.tris[nb].alive {
return self.locate(p); }
cur = nb;
moved = true;
break;
}
}
if !moved {
return Some(cur);
}
}
self.locate(p)
}
fn enforce_constraints(&mut self) -> bool {
let segs: Vec<(usize, usize)> = self.constraints.iter().copied().collect();
for (a, b) in segs {
if !self.recover_segment(a, b) {
return false;
}
}
true
}
fn recover_segment(&mut self, a: usize, b: usize) -> bool {
if self.edge_exists(a, b) {
return true;
}
let pa = self.points[a];
let pb = self.points[b];
let mut guard = 0usize;
loop {
guard += 1;
if guard > 100_000 {
return false;
}
if self.edge_exists(a, b) {
return true;
}
let mut flipped = false;
'scan: for ti in 0..self.tris.len() {
if !self.tris[ti].alive {
continue;
}
for e in 0..3 {
let u = self.tris[ti].v[e];
let w = self.tris[ti].v[(e + 1) % 3];
if u == a || u == b || w == a || w == b {
continue;
}
if self.constraints.contains(&ekey(u, w)) {
continue;
}
if !segments_properly_cross(pa, pb, self.points[u], self.points[w]) {
continue;
}
let opp = self.tris[ti].n[e];
if opp == NONE || !self.tris[opp].alive {
continue;
}
let apex = self.tris[ti].v[(e + 2) % 3];
let q = self.tris[opp]
.v
.iter()
.copied()
.find(|&x| x != u && x != w);
let Some(q) = q else { continue };
if orient(self.points[u], self.points[apex], self.points[q]) == 0
|| orient(self.points[w], self.points[apex], self.points[q]) == 0
{
continue;
}
let s1 = orient(self.points[apex], self.points[q], self.points[u]);
let s2 = orient(self.points[apex], self.points[q], self.points[w]);
if s1 == 0 || s2 == 0 || s1 == s2 {
continue; }
let mut tmp = Vec::new();
self.flip(ti, opp, u, w, apex, q, &mut tmp);
flipped = true;
break 'scan;
}
}
if !flipped {
if self.edge_exists(a, b) {
return true;
}
return false;
}
}
}
#[inline]
fn edge_exists(&self, a: usize, b: usize) -> bool {
for ti in 0..self.tris.len() {
if self.tris[ti].alive && self.tris[ti].edge_of(a, b).is_some() {
return true;
}
}
false
}
fn restore_constrained_delaunay(&mut self) {
let mut stack: Vec<(usize, usize)> = Vec::new();
for ti in 0..self.tris.len() {
if self.tris[ti].alive {
for e in 0..3 {
stack.push((ti, e));
}
}
}
self.legalize(&mut stack);
}
fn inside_flags(&self) -> Vec<bool> {
let n = self.tris.len();
let mut depth: Vec<i32> = vec![-1; n]; let mut queue: VecDeque<usize> = VecDeque::new();
for ti in 0..n {
if !self.tris[ti].alive {
continue;
}
if self.tris[ti].v.iter().any(|&x| x >= self.super_base) {
if depth[ti] == -1 {
depth[ti] = 0;
queue.push_back(ti);
}
}
}
let mut bfs = |start_queue: &mut VecDeque<usize>, depth: &mut [i32]| {
while let Some(ti) = start_queue.pop_front() {
let d = depth[ti];
for e in 0..3 {
let nb = self.tris[ti].n[e];
if nb == NONE || !self.tris[nb].alive || depth[nb] != -1 {
continue;
}
let a = self.tris[ti].v[e];
let b = self.tris[ti].v[(e + 1) % 3];
let nd = if self.constraints.contains(&ekey(a, b)) {
d + 1
} else {
d
};
depth[nb] = nd;
start_queue.push_back(nb);
}
}
};
bfs(&mut queue, &mut depth);
for seed in 0..n {
if self.tris[seed].alive && depth[seed] == -1 {
depth[seed] = 0;
let mut q2 = VecDeque::new();
q2.push_back(seed);
bfs(&mut q2, &mut depth);
}
}
depth.iter().map(|&d| d > 0 && d % 2 == 1).collect()
}
fn next_action(&self, cos_min_angle: f64, allow_segment_split: bool) -> Action {
if allow_segment_split {
if let Some(seg) = self.find_encroached_segment() {
return Action::SplitSegment(seg.0, seg.1);
}
}
let inside = self.inside_flags();
for ti in 0..self.tris.len() {
if !self.tris[ti].alive || !inside[ti] {
continue;
}
if self.tris[ti].v.iter().any(|&x| x >= self.super_base) {
continue;
}
if !self.tri_is_skinny(ti, cos_min_angle) {
continue;
}
let Some(cc) = self.circumcenter(ti) else {
continue; };
if let Some((a, b)) = self.encroached_by_point(cc) {
if allow_segment_split {
return Action::SplitSegment(a, b);
}
continue;
}
match self.locate(cc) {
Some(loc) if inside.get(loc).copied().unwrap_or(false) => {
return Action::AddPoint(cc);
}
_ => continue,
}
}
Action::Done
}
fn next_steiner(&mut self) -> Option<(P2, usize)> {
loop {
let &ti = self.skinny.iter().next()?;
if !self.tris[ti].alive
|| !self.inside.get(ti).copied().unwrap_or(false)
|| self.tris[ti].v.iter().any(|&x| x >= self.super_base)
|| !self.tri_is_skinny(ti, self.cos_min_angle)
{
self.skinny.remove(&ti); continue;
}
let Some(cc) = self.circumcenter(ti) else {
self.skinny.remove(&ti);
continue;
};
if self.encroached_by_point(cc).is_some() {
self.skinny.remove(&ti); continue;
}
match self.locate_from(ti, cc) {
Some(loc) if self.inside.get(loc).copied().unwrap_or(false) => {
return Some((cc, loc));
}
_ => {
self.skinny.remove(&ti);
continue;
}
}
}
}
fn insert_steiner(&mut self, p: P2, loc: usize) {
let vi = self.super_base;
self.points.insert(vi, p);
self.super_base += 1;
for t in &mut self.tris {
for v in &mut t.v {
if *v >= vi {
*v += 1;
}
}
}
self.insert_point_at(vi, loc);
}
fn find_encroached_segment(&self) -> Option<(usize, usize)> {
for &(a, b) in &self.constraints {
let pa = self.points[a];
let pb = self.points[b];
let mid = [(pa[0] + pb[0]) * 0.5, (pa[1] + pb[1]) * 0.5];
let r2 = dist2(pa, pb) * 0.25;
for vi in 0..self.points.len() {
if vi == a || vi == b || vi >= self.super_base {
continue;
}
if dist2(self.points[vi], mid) < r2 * (1.0 - 1e-12) {
return Some((a, b));
}
}
}
None
}
fn encroached_by_point(&self, p: P2) -> Option<(usize, usize)> {
for &(a, b) in &self.constraints {
let pa = self.points[a];
let pb = self.points[b];
let mid = [(pa[0] + pb[0]) * 0.5, (pa[1] + pb[1]) * 0.5];
let r2 = dist2(pa, pb) * 0.25;
if dist2(p, mid) < r2 * (1.0 - 1e-12) {
return Some((a, b));
}
}
None
}
fn tri_is_skinny(&self, ti: usize, cos_min_angle: f64) -> bool {
let v = self.tris[ti].v;
let a = self.points[v[0]];
let b = self.points[v[1]];
let c = self.points[v[2]];
let la2 = dist2(b, c);
let lb2 = dist2(c, a);
let lc2 = dist2(a, b);
let (mut e0, mut e1, mut e2) = (la2.sqrt(), lb2.sqrt(), lc2.sqrt());
if e0 > e1 {
std::mem::swap(&mut e0, &mut e1);
}
if e1 > e2 {
std::mem::swap(&mut e1, &mut e2);
}
if e0 > e1 {
std::mem::swap(&mut e0, &mut e1);
}
if e0 <= 1e-15 {
return false; }
if e2 / e0 > MAX_ASPECT {
return true;
}
let cos_min = (e1 * e1 + e2 * e2 - e0 * e0) / (2.0 * e1 * e2);
cos_min > cos_min_angle
}
fn circumcenter(&self, ti: usize) -> Option<P2> {
let v = self.tris[ti].v;
let a = self.points[v[0]];
let b = self.points[v[1]];
let c = self.points[v[2]];
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let ex = c[0] - a[0];
let ey = c[1] - a[1];
let d = 2.0 * (dx * ey - dy * ex);
if d.abs() < 1e-20 {
return None;
}
let b2 = dx * dx + dy * dy;
let c2 = ex * ex + ey * ey;
let ux = (ey * b2 - dy * c2) / d;
let uy = (dx * c2 - ex * b2) / d;
let cc = [a[0] + ux, a[1] + uy];
if !cc[0].is_finite() || !cc[1].is_finite() {
return None;
}
Some(cc)
}
fn emit(&self) -> (Vec<P2>, Vec<usize>) {
let inside = self.inside_flags();
let keep_upto = self.super_base; let out_points: Vec<P2> = self.points[..keep_upto].to_vec();
let mut indices: Vec<usize> = Vec::new();
for ti in 0..self.tris.len() {
if !self.tris[ti].alive || !inside[ti] {
continue;
}
let v = self.tris[ti].v;
if v.iter().any(|&x| x >= keep_upto) {
continue;
}
let a = self.points[v[0]];
let b = self.points[v[1]];
let c = self.points[v[2]];
if orient(a, b, c) >= 0 {
indices.extend_from_slice(&[v[0], v[1], v[2]]);
} else {
indices.extend_from_slice(&[v[0], v[2], v[1]]);
}
}
(out_points, indices)
}
}
#[inline]
fn dist2(a: P2, b: P2) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
dx * dx + dy * dy
}
#[inline]
fn strictly_between(a: P2, b: P2, p: P2) -> bool {
let lt = |u: P2, w: P2| u[0] < w[0] || (u[0] == w[0] && u[1] < w[1]);
(lt(a, p) && lt(p, b)) || (lt(b, p) && lt(p, a))
}
fn segments_properly_cross(p1: P2, p2: P2, p3: P2, p4: P2) -> bool {
let d1 = orient(p3, p4, p1);
let d2 = orient(p3, p4, p2);
let d3 = orient(p1, p2, p3);
let d4 = orient(p1, p2, p4);
(d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0)
}
fn rings_to_pslg(rings: &[Vec<P2>]) -> (Vec<P2>, Vec<(usize, usize)>) {
let mut points: Vec<P2> = Vec::new();
let mut segments: Vec<(usize, usize)> = Vec::new();
for ring in rings {
if ring.len() < 3 {
continue;
}
let base = points.len();
points.extend_from_slice(ring);
let m = ring.len();
for i in 0..m {
let a = base + i;
let b = base + (i + 1) % m;
if a != b {
segments.push(ekey(a, b));
}
}
}
(points, segments)
}
fn refine_to_fixpoint(
mut points: Vec<P2>,
mut segments: Vec<(usize, usize)>,
allow_segment_split: bool,
) -> Option<Cdt> {
let n_input = points.len();
let max_steiner = (n_input * 3).max(32);
let mut steiner = 0usize;
let mut cdt = Cdt::build_from(points.clone(), &segments)?;
if !allow_segment_split {
cdt.start_refinement(COS_MIN_ANGLE);
while steiner < max_steiner.min(MAX_REFINE_ITERS) {
let Some((p, loc)) = cdt.next_steiner() else {
break;
};
cdt.insert_steiner(p, loc);
steiner += 1;
}
return Some(cdt);
}
for _ in 0..MAX_REFINE_ITERS {
if steiner >= max_steiner {
break;
}
match cdt.next_action(COS_MIN_ANGLE, allow_segment_split) {
Action::Done => break,
Action::AddPoint(p) => {
points.push(p);
steiner += 1;
match Cdt::build_from(points.clone(), &segments) {
Some(next) => cdt = next,
None => {
points.pop();
break;
}
}
}
Action::SplitSegment(a, b) => {
let pa = points[a];
let pb = points[b];
let mid = [(pa[0] + pb[0]) * 0.5, (pa[1] + pb[1]) * 0.5];
let vi = points.len();
points.push(mid);
segments.retain(|&s| s != ekey(a, b));
segments.push(ekey(a, vi));
segments.push(ekey(vi, b));
steiner += 1;
match Cdt::build_from(points.clone(), &segments) {
Some(next) => cdt = next,
None => {
points.pop();
segments.retain(|&s| s != ekey(a, vi) && s != ekey(vi, b));
segments.push(ekey(a, b));
break;
}
}
}
}
}
Some(cdt)
}
pub(crate) fn triangulate_refined(
outer: &[Point2<f64>],
holes: &[Vec<Point2<f64>>],
allow_boundary_split: bool,
) -> Option<(Vec<Point2<f64>>, Vec<usize>)> {
let mut rings: Vec<Vec<P2>> = Vec::with_capacity(1 + holes.len());
rings.push(outer.iter().map(p2).collect());
for h in holes {
if h.len() >= 3 {
rings.push(h.iter().map(p2).collect());
}
}
let (points, segments) = rings_to_pslg(&rings);
let cdt = refine_to_fixpoint(points, segments, allow_boundary_split)?;
let (pts, idx) = cdt.emit();
if idx.is_empty() {
return None;
}
let out_pts: Vec<Point2<f64>> = pts.iter().map(|p| Point2::new(p[0], p[1])).collect();
Some((out_pts, idx))
}
pub(crate) fn triangulate_simple_no_steiner(points: &[Point2<f64>]) -> Option<Vec<usize>> {
let rings = vec![points.iter().map(p2).collect::<Vec<P2>>()];
let (pts, segs) = rings_to_pslg(&rings);
let cdt = Cdt::build_from(pts, &segs)?;
let (_pts, idx) = cdt.emit();
if idx.is_empty() || idx.iter().any(|&i| i >= points.len()) {
return None;
}
Some(idx)
}
pub(crate) fn triangulate_holes_no_steiner(
outer: &[Point2<f64>],
holes: &[Vec<Point2<f64>>],
) -> Option<Vec<usize>> {
let mut rings: Vec<Vec<P2>> = Vec::with_capacity(1 + holes.len());
rings.push(outer.iter().map(p2).collect());
let mut total = outer.len();
for h in holes {
if h.len() >= 3 {
rings.push(h.iter().map(p2).collect());
total += h.len();
}
}
let (pts, segs) = rings_to_pslg(&rings);
let cdt = Cdt::build_from(pts, &segs)?;
let (_pts, idx) = cdt.emit();
if idx.is_empty() || idx.iter().any(|&i| i >= total) {
return None;
}
Some(idx)
}
#[cfg(test)]
mod tests {
use super::*;
fn pt(x: f64, y: f64) -> Point2<f64> {
Point2::new(x, y)
}
fn area_of(points: &[Point2<f64>], idx: &[usize]) -> f64 {
let mut a = 0.0;
for t in idx.chunks_exact(3) {
let p0 = points[t[0]];
let p1 = points[t[1]];
let p2 = points[t[2]];
a += ((p1.x - p0.x) * (p2.y - p0.y) - (p2.x - p0.x) * (p1.y - p0.y)).abs() * 0.5;
}
a
}
fn worst_aspect(points: &[Point2<f64>], idx: &[usize]) -> f64 {
let mut worst = 0.0_f64;
for t in idx.chunks_exact(3) {
let p0 = points[t[0]];
let p1 = points[t[1]];
let p2 = points[t[2]];
let d = |a: Point2<f64>, b: Point2<f64>| ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt();
let e0 = d(p0, p1);
let e1 = d(p1, p2);
let e2 = d(p2, p0);
let mn = e0.min(e1).min(e2);
let mx = e0.max(e1).max(e2);
if mn > 1e-12 {
worst = worst.max(mx / mn);
}
}
worst
}
#[test]
fn refined_thin_strip_with_hole_no_far_corner_sliver() {
let outer = vec![
pt(0.0, 0.0),
pt(40.0, 0.0),
pt(40.0, 1.0),
pt(0.0, 1.0),
];
let hole = vec![
pt(2.0, 0.4),
pt(2.6, 0.4),
pt(2.6, 0.6),
pt(2.0, 0.6),
];
let (pts, idx) =
triangulate_refined(&outer, &[hole], true).expect("refined triangulation");
assert_eq!(idx.len() % 3, 0);
let wa = worst_aspect(&pts, &idx);
assert!(
wa <= 8.0,
"thin-strip-with-hole worst aspect {wa:.2} should be <= 8 after refinement"
);
}
#[test]
fn refined_is_watertight_no_tjunction() {
let outer = vec![pt(0.0, 0.0), pt(40.0, 0.0), pt(40.0, 1.0), pt(0.0, 1.0)];
let hole = vec![pt(2.0, 0.4), pt(2.6, 0.4), pt(2.6, 0.6), pt(2.0, 0.6)];
let (pts, idx) = triangulate_refined(&outer, &[hole], true).expect("triangulation");
use std::collections::BTreeMap;
let mut undirected: BTreeMap<(usize, usize), u32> = BTreeMap::new();
for t in idx.chunks_exact(3) {
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
*undirected.entry(if a < b { (a, b) } else { (b, a) }).or_insert(0) += 1;
}
}
let non_manifold = undirected.values().filter(|&&c| c > 2).count();
assert_eq!(non_manifold, 0, "found {non_manifold} non-manifold edges (overlap)");
let mut boundary_len = 0.0_f64;
for (&(a, b), &c) in &undirected {
if c == 1 {
let pa = pts[a];
let pb = pts[b];
boundary_len += ((pa.x - pb.x).powi(2) + (pa.y - pb.y).powi(2)).sqrt();
}
}
let expected = 82.0 + 1.6;
assert!(
(boundary_len - expected).abs() < 1e-6,
"boundary length {boundary_len} != {expected}: a T-junction left a gap or overlap"
);
}
#[test]
fn refined_area_is_preserved() {
let outer = vec![
pt(0.0, 0.0),
pt(10.0, 0.0),
pt(10.0, 10.0),
pt(0.0, 10.0),
];
let hole = vec![
pt(3.0, 3.0),
pt(7.0, 3.0),
pt(7.0, 7.0),
pt(3.0, 7.0),
];
let (pts, idx) = triangulate_refined(&outer, &[hole], true).expect("triangulation");
let area = area_of(&pts, &idx);
let expected = 100.0 - 16.0; assert!(
(area - expected).abs() < 1e-6,
"area {area} should equal {expected} (outer minus hole)"
);
}
#[test]
fn refined_is_deterministic() {
let outer = vec![
pt(0.0, 0.0),
pt(12.0, 0.0),
pt(12.0, 3.0),
pt(6.0, 5.0),
pt(0.0, 3.0),
];
let hole = vec![pt(4.0, 1.0), pt(8.0, 1.0), pt(6.0, 2.5)];
let a = triangulate_refined(&outer, &[hole.clone()], true).unwrap();
let b = triangulate_refined(&outer, &[hole.clone()], true).unwrap();
let n_input = outer.len() + hole.len();
assert!(
a.0.len() > n_input,
"expected Steiner points to be added (got {} verts for {n_input} inputs)",
a.0.len()
);
assert_eq!(a.1, b.1, "index lists must be identical run-to-run");
assert_eq!(a.0.len(), b.0.len(), "vertex counts must match");
for (pa, pb) in a.0.iter().zip(b.0.iter()) {
assert_eq!(pa.x.to_bits(), pb.x.to_bits(), "x bits must be identical");
assert_eq!(pa.y.to_bits(), pb.y.to_bits(), "y bits must be identical");
}
}
#[test]
fn no_split_many_hole_refinement_is_fast_and_valid() {
let (w, h, step) = (12.0_f64, 10.0_f64, 1.0 / 3.0);
let mut outer: Vec<Point2<f64>> = Vec::new();
let n_x = (w / step).round() as usize;
let n_y = (h / step).round() as usize;
for i in 0..n_x {
outer.push(pt(i as f64 * step, 0.0));
}
for j in 0..n_y {
outer.push(pt(w, j as f64 * step));
}
for i in (1..=n_x).rev() {
outer.push(pt(i as f64 * step, h));
}
for j in (1..=n_y).rev() {
outer.push(pt(0.0, j as f64 * step));
}
let mut holes: Vec<Vec<Point2<f64>>> = Vec::new();
let r = 0.1_f64;
for gx in 0..4 {
for gy in 0..4 {
let (cx, cy) = (1.5 + 3.0 * gx as f64, 2.0 + 2.0 * gy as f64);
let ring: Vec<Point2<f64>> = (0..28)
.map(|k| {
let a = k as f64 / 28.0 * std::f64::consts::TAU;
pt(cx + r * a.cos(), cy + r * a.sin())
})
.collect();
holes.push(ring);
}
}
let n_input = outer.len() + holes.iter().map(|h| h.len()).sum::<usize>();
let t0 = std::time::Instant::now();
let (pts, idx) =
triangulate_refined(&outer, &holes, false).expect("no-split refinement");
let dt = t0.elapsed();
assert!(
pts.len() > n_input,
"expected Steiner points (got {} verts for {n_input} inputs)",
pts.len()
);
let hole_area: f64 = holes.iter().map(|h| {
let mut s = 0.0;
for i in 0..h.len() {
let j = (i + 1) % h.len();
s += h[i].x * h[j].y - h[j].x * h[i].y;
}
(s * 0.5).abs()
}).sum();
let area = area_of(&pts, &idx);
let expected = 12.0 * 10.0 - hole_area;
assert!(
(area - expected).abs() < 1e-6,
"area {area} != {expected} (outer minus 16 holes)"
);
let (pts2, idx2) = triangulate_refined(&outer, &holes, false).unwrap();
assert_eq!(idx, idx2, "index lists must be identical run-to-run");
assert_eq!(pts.len(), pts2.len());
for (a, b) in pts.iter().zip(pts2.iter()) {
assert_eq!(a.x.to_bits(), b.x.to_bits());
assert_eq!(a.y.to_bits(), b.y.to_bits());
}
#[cfg(not(debug_assertions))]
assert!(
dt < std::time::Duration::from_secs(2),
"no-split many-hole refinement took {dt:?} — the O(P³) rebuild-per-point driver is back"
);
let _ = dt;
}
fn assert_structurally_valid(cdt: &Cdt) {
let mut edge_count: BTreeMap<(usize, usize), u32> = BTreeMap::new();
for ti in 0..cdt.tris.len() {
let t = &cdt.tris[ti];
if !t.alive {
continue;
}
assert_ne!(
orient(cdt.points[t.v[0]], cdt.points[t.v[1]], cdt.points[t.v[2]]),
0,
"alive triangle {ti} {:?} has zero area",
t.v
);
for e in 0..3 {
let a = t.v[e];
let b = t.v[(e + 1) % 3];
*edge_count.entry(ekey(a, b)).or_insert(0) += 1;
let nb = t.n[e];
if nb != NONE {
assert!(
cdt.tris[nb].alive,
"triangle {ti} edge {a}-{b} points at dead triangle {nb}"
);
let back = cdt.tris[nb].edge_of(a, b).unwrap_or_else(|| {
panic!("neighbour {nb} of triangle {ti} lacks edge {a}-{b}")
});
assert_eq!(
cdt.tris[nb].n[back], ti,
"adjacency {ti} <-> {nb} over edge {a}-{b} is not mutual"
);
}
}
}
for (&(a, b), &c) in &edge_count {
assert!(c <= 2, "edge {a}-{b} is used by {c} alive triangles");
}
}
#[test]
fn on_shared_edge_insertion_splits_both_sides() {
let points: Vec<P2> = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let segments = vec![(0usize, 1usize), (1, 2), (2, 3), (3, 0)];
let mut cdt = Cdt::build_from(points, &segments).expect("square CDT");
assert_structurally_valid(&cdt);
let (d0, d1) = if cdt.edge_exists(0, 2) { (0, 2) } else { (1, 3) };
assert!(cdt.edge_exists(d0, d1), "expected a diagonal edge");
let vi = cdt.super_base;
cdt.points.insert(vi, [0.5, 0.5]);
cdt.super_base += 1;
for t in &mut cdt.tris {
for v in &mut t.v {
if *v >= vi {
*v += 1;
}
}
}
let start = (0..cdt.tris.len())
.find(|&ti| cdt.tris[ti].alive && cdt.tris[ti].edge_of(d0, d1).is_some())
.expect("a triangle incident to the diagonal");
cdt.split_at(start, vi);
let refs = (0..cdt.tris.len())
.filter(|&ti| cdt.tris[ti].alive && cdt.tris[ti].v.contains(&vi))
.count();
assert_eq!(refs, 4, "midpoint must be fanned by 4 triangles (both sides), got {refs}");
assert_structurally_valid(&cdt);
}
#[test]
fn no_steiner_square_is_two_triangles() {
let sq = vec![pt(0.0, 0.0), pt(1.0, 0.0), pt(1.0, 1.0), pt(0.0, 1.0)];
let idx = triangulate_simple_no_steiner(&sq).expect("square");
assert_eq!(idx.len(), 6);
assert!(idx.iter().all(|&i| i < 4));
let pts = sq.clone();
assert!((area_of(&pts, &idx) - 1.0).abs() < 1e-9);
}
#[test]
fn no_steiner_square_with_hole_preserves_hole() {
let outer = vec![
pt(0.0, 0.0),
pt(10.0, 0.0),
pt(10.0, 10.0),
pt(0.0, 10.0),
];
let hole = vec![
pt(3.0, 3.0),
pt(7.0, 3.0),
pt(7.0, 7.0),
pt(3.0, 7.0),
];
let idx = triangulate_holes_no_steiner(&outer, &[hole.clone()]).expect("holes");
let mut pts = outer.clone();
pts.extend(hole);
let area = area_of(&pts, &idx);
assert!(
(area - 84.0).abs() < 1e-6,
"area {area} should be 84 (100 - 16 hole)"
);
}
}