Files
OneCAD-Simulation/crates/mesh/src/quad/cdt.rs
T
jyvikoandClaude Opus 5.5 d5bd74570f Rename the project to FieldCraft
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag
fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server
name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family,
docs and licences. No compatibility shim: no files of the earlier name exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:51:40 -04:00

776 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Planar constrained Delaunay triangulation with Delaunay refinement to a size field.
//!
//! - Insertion: Bowyer–Watson (Bowyer, Comput. J. 24(2), 1981; Watson, Comput. J. 24(2), 1981)
//! inside a super-triangle, with exact `orient2d`/`incircle` (Shewchuk 1997). The cavity grows
//! from the triangle containing the point and never crosses a constrained edge, which is the
//! constrained Delaunay insertion of Shewchuk, *Delaunay refinement algorithms for triangular
//! mesh generation*, CGTA 22:21–74, 2002, §2.
//! - Constraint recovery: Sloan, *A fast algorithm for generating constrained Delaunay
//! triangulations*, Computers & Structures 47(3):441–450, 1993: flip the edges crossing a
//! segment until it appears, then restore the Delaunay property with Lawson flips.
//! - Refinement: Ruppert, *A Delaunay refinement algorithm for quality 2-dimensional mesh
//! generation*, J. Algorithms 18(3):548–585, 1995 (circumcentres of triangles that are too
//! large for the size field or have a circumradius-to-shortest-edge ratio above √2), with one
//! change: boundary segments carry prescribed nodes and are never split, so a circumcentre that
//! falls outside the region, comes closer than a fraction of the local size to a vertex, or is
//! refused by the caller's guard (clearance from the true boundary) is not inserted (a
//! too-large triangle gets its centroid instead). Ruppert's encroachment rule (diametral
//! circles) would forbid any refinement next to a boundary coarser than the size field; the
//! clearances keep the packing argument for termination.
//!
//! Every loop is bounded (walk steps, cavity rounds, flips, refinement passes and the triangle
//! count), and a cavity is checked before it is retriangulated: the new triangles all have
//! positive exact orientation and no constrained edge is removed.
use std::collections::VecDeque;
use super::domain::Region;
use crate::predicates::{Point2, Sign, incircle, orient2d, snap_to_domain};
use crate::size::{SizeField, checked_size};
use crate::{Error, Result};
const NONE: u32 = u32::MAX;
/// Rounds of cavity shrinking before an insertion is rejected.
const MAX_CAVITY_ROUNDS: usize = 32;
/// Refinement passes over the triangles.
const MAX_REFINE_PASSES: usize = 64;
#[derive(Debug, Clone, Copy)]
struct Tri {
/// Counterclockwise vertices.
v: [u32; 3],
/// `n[i]`: the neighbour across the edge opposite `v[i]` (from `v[i+1]` to `v[i+2]`).
n: [u32; 3],
/// Whether the edge opposite `v[i]` is a constrained (boundary) edge.
fixed: [bool; 3],
inside: bool,
alive: bool,
}
/// The triangles of the region: counterclockwise vertex triples into `pts`.
#[derive(Debug, Clone)]
pub(crate) struct Triangulation {
pub pts: Vec<[f64; 2]>,
pub p2: Vec<Point2>,
pub tris: Vec<[u32; 3]>,
}
pub(crate) struct Cdt {
pts: Vec<[f64; 2]>,
p2: Vec<Point2>,
tris: Vec<Tri>,
free: Vec<u32>,
vt: Vec<u32>,
last: u32,
mark: Vec<u32>,
epoch: u32,
max_tris: usize,
}
/// Steiner points keep this fraction of the local size from vertices (and, through the
/// caller's guard, from the boundary).
pub(crate) const CLEARANCE: f64 = 0.3;
/// A constructed point snapped into the predicates' domain, or `None` when it is outside the
/// model's coordinate range.
fn steiner_point(c: [f64; 2]) -> Option<Point2> {
let c = [snap_to_domain(c[0]), snap_to_domain(c[1])];
if !(c[0].abs() <= fieldcraft_model::MAX_COORD && c[1].abs() <= fieldcraft_model::MAX_COORD) {
return None;
}
Point2::new(c[0], c[1]).ok()
}
fn err(what: &str) -> Error {
Error::Failed(format!("triangulation: {what}"))
}
/// The edge index `i` of `tri` that runs from `a` to `b` (opposite `v[i]`).
fn edge_index(tri: &Tri, a: u32, b: u32) -> Option<usize> {
(0..3).find(|&i| tri.v[(i + 1) % 3] == a && tri.v[(i + 2) % 3] == b)
}
/// Every edge of `tri` keeps `p` on its left or on it (exact).
fn contains(tri: &Tri, p2: &[Point2], p: &Point2) -> bool {
(0..3).all(|i| match (p2.get(tri.v[(i + 1) % 3] as usize), p2.get(tri.v[(i + 2) % 3] as usize)) {
(Some(a), Some(b)) => orient2d(a, b, p) != Sign::Negative,
_ => false,
})
}
impl Cdt {
fn t(&self, t: u32) -> Result<&Tri> {
self.tris.get(t as usize).ok_or_else(|| err("triangle index out of range"))
}
fn tm(&mut self, t: u32) -> Result<&mut Tri> {
self.tris.get_mut(t as usize).ok_or_else(|| err("triangle index out of range"))
}
fn p(&self, v: u32) -> Result<&Point2> {
self.p2.get(v as usize).ok_or_else(|| err("vertex index out of range"))
}
fn xy(&self, v: u32) -> Result<[f64; 2]> {
self.pts.get(v as usize).copied().ok_or_else(|| err("vertex index out of range"))
}
/// Triangulate `region`'s boundary points (ids preserved) and recover its loops as
/// constrained edges; triangles inside the region are marked.
pub fn build(region: &Region, max_tris: usize) -> Result<Cdt> {
let n = region.pts.len();
let (lo, hi) = super::domain::bbox(region.pts.iter().copied());
let c = [0.5 * (lo[0] + hi[0]), 0.5 * (lo[1] + hi[1])];
let d = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(1.0);
let corners = [[c[0] - 20.0 * d, c[1] - 10.0 * d], [c[0] + 20.0 * d, c[1] - 10.0 * d], [c[0], c[1] + 20.0 * d]];
let mut pts = region.pts.clone();
let mut p2 = region.p2.clone();
for k in corners {
let k = [snap_to_domain(k[0]), snap_to_domain(k[1])];
p2.push(Point2::new(k[0], k[1])?);
pts.push(k);
}
let s = n as u32;
let mut cdt = Cdt {
vt: vec![NONE; pts.len()],
pts,
p2,
tris: vec![Tri { v: [s, s + 1, s + 2], n: [NONE; 3], fixed: [false; 3], inside: false, alive: true }],
free: Vec::new(),
last: 0,
mark: vec![0],
epoch: 0,
max_tris,
};
for v in [s, s + 1, s + 2] {
if let Some(x) = cdt.vt.get_mut(v as usize) {
*x = 0;
}
}
// Insert in Morton order (short walks), ties by id.
let span = [(hi[0] - lo[0]).max(f64::MIN_POSITIVE), (hi[1] - lo[1]).max(f64::MIN_POSITIVE)];
let key = |p: &[f64; 2]| {
let q = |x: f64, l: f64, s: f64| ((((x - l) / s) * 65535.0) as u32).min(65535);
let (mut x, mut y) = (q(p[0], lo[0], span[0]), q(p[1], lo[1], span[1]));
let mut k = 0u64;
for bit in 0..16 {
k |= u64::from(x & 1) << (2 * bit) | u64::from(y & 1) << (2 * bit + 1);
x >>= 1;
y >>= 1;
}
k
};
let mut order: Vec<(u64, u32)> = region.pts.iter().enumerate().map(|(i, p)| (key(p), i as u32)).collect();
order.sort_unstable();
for (_, v) in order {
if !cdt.insert(v, None)? {
return Err(err("a boundary point could not be inserted"));
}
}
for l in &region.loops {
for (i, &a) in l.iter().enumerate() {
let b = l.get((i + 1) % l.len()).copied().ok_or_else(|| err("loop"))?;
cdt.recover(a, b)?;
}
}
cdt.classify()?;
#[cfg(test)]
assert_eq!(cdt.verify(), Ok(()), "after build");
Ok(cdt)
}
fn alloc(&mut self, tri: Tri) -> Result<u32> {
if let Some(t) = self.free.pop() {
*self.tm(t)? = tri;
return Ok(t);
}
if self.tris.len() >= self.max_tris {
return Err(Error::Limit(format!("the triangulation exceeds {} triangles", self.max_tris)));
}
self.tris.push(tri);
self.mark.push(0);
Ok(self.tris.len() as u32 - 1)
}
/// The triangle containing `p` (on its boundary counts), by a visibility walk from `start`
/// with a linear scan as the fallback.
fn locate(&self, p: &Point2, start: u32) -> Result<u32> {
let mut t = if self.t(start).is_ok_and(|x| x.alive) { start } else { self.any_alive()? };
let cap = 64 + 4 * self.tris.len();
'walk: for step in 0..cap {
let tri = *self.t(t)?;
for k in 0..3 {
let i = (k + step) % 3;
let (a, b) = (tri.v[(i + 1) % 3], tri.v[(i + 2) % 3]);
if orient2d(self.p(a)?, self.p(b)?, p) == Sign::Negative {
let next = tri.n[i];
if next == NONE {
return Err(err("point outside the super-triangle"));
}
t = next;
continue 'walk;
}
}
return Ok(t);
}
self.tris.iter().position(|tri| tri.alive && contains(tri, &self.p2, p)).map(|i| i as u32).ok_or_else(|| err("point location failed"))
}
fn any_alive(&self) -> Result<u32> {
self.tris.iter().position(|t| t.alive).map(|i| i as u32).ok_or_else(|| err("no triangles"))
}
/// Insert vertex `v` (already in `pts`). Boundary points (`steiner == None`) must be inserted.
/// Steiner points (`Some(min_distance)`) may be rejected (outside the region, closer than
/// `min_distance` to a vertex, or no valid cavity), leaving the triangulation unchanged;
/// returns whether the point went in.
fn insert(&mut self, v: u32, steiner: Option<f64>) -> Result<bool> {
let p = *self.p(v)?;
let t0 = self.locate(&p, self.last)?;
let tri0 = *self.t(t0)?;
for &u in &tri0.v {
if self.p(u)?.coords() == p.coords() {
return match steiner {
Some(_) => Ok(false),
None => Err(err("repeated point")),
};
}
}
if steiner.is_some() && !tri0.inside {
return Ok(false);
}
let mut excluded: Vec<u32> = Vec::new();
for _ in 0..MAX_CAVITY_ROUNDS {
let (cavity, edges) = self.cavity(t0, &p, &excluded)?;
let mut bad: Vec<u32> = Vec::new();
// A constrained edge inside the cavity: drop the triangle on the far side from p.
for &t in &cavity {
let tri = *self.t(t)?;
for i in 0..3 {
if tri.fixed[i] && tri.n[i] != NONE && self.marked(tri.n[i]) {
let (a, b) = (tri.v[(i + 1) % 3], tri.v[(i + 2) % 3]);
if orient2d(self.p(a)?, self.p(b)?, &p) != Sign::Positive {
bad.push(t);
}
}
}
}
// Every new triangle (a, b, p) must be counterclockwise.
for &(a, b, _, _, owner) in &edges {
if orient2d(self.p(a)?, self.p(b)?, &p) != Sign::Positive {
bad.push(owner);
}
}
if let Some(min_distance) = steiner {
let pc = p.coords();
for &(a, _, _, _, _) in &edges {
let pa = self.xy(a)?;
if (pa[0] - pc[0]).powi(2) + (pa[1] - pc[1]).powi(2) < min_distance * min_distance {
return Ok(false);
}
}
}
if bad.is_empty() && edges.len() == cavity.len() + 2 {
self.retriangulate(v, &cavity, &edges, tri0.inside)?;
return Ok(true);
}
if bad.contains(&t0) || bad.is_empty() {
break;
}
excluded.extend(bad);
excluded.sort_unstable();
excluded.dedup();
}
match steiner {
Some(_) => Ok(false),
None => Err(err("no valid cavity for a boundary point")),
}
}
fn marked(&self, t: u32) -> bool {
self.mark.get(t as usize).is_some_and(|&m| m == self.epoch)
}
/// The Bowyer–Watson cavity of `p` from `t0` (never crossing constrained edges, skipping
/// `excluded`), marked with the current epoch, and its boundary edges `(a, b, outside
/// neighbour, constrained, owner)` with the cavity on their left.
#[allow(clippy::type_complexity)]
fn cavity(&mut self, t0: u32, p: &Point2, excluded: &[u32]) -> Result<(Vec<u32>, Vec<(u32, u32, u32, bool, u32)>)> {
self.epoch = self.epoch.wrapping_add(1);
if self.epoch == 0 {
self.mark.iter_mut().for_each(|m| *m = 0);
self.epoch = 1;
}
let epoch = self.epoch;
let set = |mark: &mut Vec<u32>, t: u32| {
if let Some(m) = mark.get_mut(t as usize) {
*m = epoch;
}
};
set(&mut self.mark, t0);
let mut cavity = vec![t0];
let mut k = 0;
while let Some(&t) = cavity.get(k) {
k += 1;
let tri = *self.t(t)?;
for i in 0..3 {
let nb = tri.n[i];
if nb == NONE || tri.fixed[i] || self.marked(nb) || excluded.binary_search(&nb).is_ok() {
continue;
}
let o = *self.t(nb)?;
if incircle(self.p(o.v[0])?, self.p(o.v[1])?, self.p(o.v[2])?, p) == Sign::Positive {
set(&mut self.mark, nb);
cavity.push(nb);
}
}
}
let mut edges = Vec::new();
for &t in &cavity {
let tri = *self.t(t)?;
for i in 0..3 {
let nb = tri.n[i];
// Shared with another cavity triangle (a constrained one is reported by `insert`).
if nb != NONE && self.marked(nb) {
continue;
}
edges.push((tri.v[(i + 1) % 3], tri.v[(i + 2) % 3], nb, tri.fixed[i], t));
}
}
Ok((cavity, edges))
}
fn retriangulate(&mut self, v: u32, cavity: &[u32], edges: &[(u32, u32, u32, bool, u32)], inside: bool) -> Result<()> {
for &t in cavity {
self.tm(t)?.alive = false;
self.free.push(t);
}
let mut made: Vec<(u32, u32, u32)> = Vec::with_capacity(edges.len()); // (a, b, tri)
for &(a, b, nb, fixed, _) in edges {
let t = self.alloc(Tri { v: [a, b, v], n: [NONE, NONE, nb], fixed: [false, false, fixed], inside, alive: true })?;
made.push((a, b, t));
if nb != NONE {
self.relink(nb, a, b, t)?;
}
}
let mut by_a: Vec<(u32, u32)> = made.iter().map(|&(a, _, t)| (a, t)).collect();
let mut by_b: Vec<(u32, u32)> = made.iter().map(|&(_, b, t)| (b, t)).collect();
by_a.sort_unstable();
by_b.sort_unstable();
let find = |list: &[(u32, u32)], key: u32| -> Result<u32> {
let i = list.binary_search_by_key(&key, |e| e.0).map_err(|_| err("cavity boundary is not a closed loop"))?;
list.get(i).map(|e| e.1).ok_or_else(|| err("cavity lookup"))
};
for &(a, b, t) in &made {
let next = find(&by_a, b)?; // the triangle (b, c, v): shares edge b–v
let prev = find(&by_b, a)?; // the triangle (z, a, v): shares edge v–a
let tri = self.tm(t)?;
tri.n[0] = next;
tri.n[1] = prev;
for u in [a, b, v] {
if let Some(x) = self.vt.get_mut(u as usize) {
*x = t;
}
}
self.last = t;
}
Ok(())
}
/// In triangle `t`, point the edge from `b` to `a` at `to`.
fn relink(&mut self, t: u32, a: u32, b: u32, to: u32) -> Result<()> {
let tri = self.tm(t)?;
let i = edge_index(tri, b, a).ok_or_else(|| err("neighbour does not share the edge"))?;
tri.n[i] = to;
Ok(())
}
/// Visit the triangles around vertex `v` counterclockwise, each with `v`'s local index,
/// until `visit` returns a value (or the rotation closes: `None`).
fn around<R>(&self, v: u32, mut visit: impl FnMut(u32, &Tri, usize) -> Result<Option<R>>) -> Result<Option<R>> {
let start = self.vt.get(v as usize).copied().ok_or_else(|| err("vertex"))?;
let mut t = start;
for _ in 0..self.tris.len() + 1 {
let tri = self.t(t)?;
let k = tri.v.iter().position(|&x| x == v).ok_or_else(|| err("vertex map is stale"))?;
if let Some(r) = visit(t, tri, k)? {
return Ok(Some(r));
}
t = tri.n[(k + 1) % 3];
if t == NONE || t == start {
return Ok(None);
}
}
Err(err("vertex rotation did not close"))
}
/// The triangle and local index of the directed edge `a → b` (the edge opposite `v[i]` runs
/// from `v[i+1]` to `v[i+2]`).
fn find_edge(&self, a: u32, b: u32) -> Result<Option<(u32, usize)>> {
self.around(a, |t, tri, k| Ok((tri.v[(k + 1) % 3] == b).then_some((t, (k + 2) % 3))))
}
/// Flip the edge opposite `v[i]` of `t`. Returns the new edge's end points.
fn flip(&mut self, t: u32, i: usize) -> Result<(u32, u32)> {
let a = *self.t(t)?;
let (p, q, r) = (a.v[i], a.v[(i + 1) % 3], a.v[(i + 2) % 3]);
let t2 = a.n[i];
let b = *self.t(t2)?;
let j = edge_index(&b, r, q).ok_or_else(|| err("flip: neighbour mismatch"))?;
let s = b.v[j];
let (n_pq, f_pq) = (a.n[(i + 2) % 3], a.fixed[(i + 2) % 3]);
let (n_rp, f_rp) = (a.n[(i + 1) % 3], a.fixed[(i + 1) % 3]);
let (n_sr, f_sr) = (b.n[(j + 2) % 3], b.fixed[(j + 2) % 3]);
let (n_qs, f_qs) = (b.n[(j + 1) % 3], b.fixed[(j + 1) % 3]);
*self.tm(t)? = Tri { v: [p, q, s], n: [n_qs, t2, n_pq], fixed: [f_qs, false, f_pq], inside: a.inside, alive: true };
*self.tm(t2)? = Tri { v: [p, s, r], n: [n_sr, n_rp, t], fixed: [f_sr, f_rp, false], inside: b.inside, alive: true };
if n_qs != NONE {
self.relink(n_qs, q, s, t)?;
}
if n_rp != NONE {
self.relink(n_rp, r, p, t2)?;
}
for (u, x) in [(p, t), (q, t), (s, t2), (r, t2)] {
if let Some(y) = self.vt.get_mut(u as usize) {
*y = x;
}
}
Ok((p, s))
}
/// The edge opposite `v[i]` of `t` with its far vertex, when it is flippable (strictly convex
/// quadrilateral).
fn flippable(&self, t: u32, i: usize) -> Result<Option<u32>> {
let a = *self.t(t)?;
let t2 = a.n[i];
if t2 == NONE || a.fixed[i] {
return Ok(None);
}
let (p, q, r) = (a.v[i], a.v[(i + 1) % 3], a.v[(i + 2) % 3]);
let b = *self.t(t2)?;
let s = b.v[edge_index(&b, r, q).ok_or_else(|| err("neighbour mismatch"))?];
let (pp, ps) = (self.p(p)?, self.p(s)?);
Ok((orient2d(pp, ps, self.p(q)?) * orient2d(pp, ps, self.p(r)?) == Sign::Negative).then_some(s))
}
/// Make `a–b` an edge (Sloan 1993) and mark it constrained.
fn recover(&mut self, a: u32, b: u32) -> Result<()> {
if self.find_edge(a, b)?.is_none() {
let crossing = self.crossing_edges(a, b)?;
let (pa, pb) = (*self.p(a)?, *self.p(b)?);
let mut queue: VecDeque<(u32, u32)> = crossing.into_iter().collect();
let cap = 1000 + 16 * queue.len() * queue.len();
let mut fresh = Vec::new();
let mut steps = 0usize;
while let Some((u, w)) = queue.pop_front() {
steps += 1;
if steps > cap {
return Err(Error::Limit(format!("boundary edge recovery did not finish in {cap} flips")));
}
let (t, i) = self.find_edge(u, w)?.ok_or_else(|| err("crossing edge vanished"))?;
match self.flippable(t, i)? {
Some(_) => {
let (x, y) = self.flip(t, i)?;
let crosses =
x != a && x != b && y != a && y != b && orient2d(&pa, &pb, self.p(x)?) * orient2d(&pa, &pb, self.p(y)?) == Sign::Negative;
if crosses { queue.push_back((x, y)) } else { fresh.push((x, y)) }
}
None => queue.push_back((u, w)),
}
}
self.mark_fixed(a, b)?;
self.lawson(fresh)?;
return Ok(());
}
self.mark_fixed(a, b)
}
fn mark_fixed(&mut self, a: u32, b: u32) -> Result<()> {
let (t, i) = self.find_edge(a, b)?.ok_or_else(|| err("recovered edge is missing"))?;
let nb = self.t(t)?.n[i];
self.tm(t)?.fixed[i] = true;
if nb != NONE {
let tri = self.tm(nb)?;
if let Some(k) = edge_index(tri, b, a) {
tri.fixed[k] = true;
}
}
Ok(())
}
/// The edges crossed by the open segment `a–b`, from `a` to `b`.
fn crossing_edges(&self, a: u32, b: u32) -> Result<Vec<(u32, u32)>> {
let (pa, pb) = (*self.p(a)?, *self.p(b)?);
let found = self.around(a, |t, tri, k| {
let (u, w) = (tri.v[(k + 1) % 3], tri.v[(k + 2) % 3]);
let (ou, ow) = (orient2d(&pa, self.p(u)?, &pb), orient2d(&pa, self.p(w)?, &pb));
if ou.is_zero() || ow.is_zero() {
let x = if ou.is_zero() { u } else { w };
let px = self.xy(x)?;
let (qa, qb) = (pa.coords(), pb.coords());
if (px[0] - qa[0]) * (qb[0] - qa[0]) + (px[1] - qa[1]) * (qb[1] - qa[1]) > 0.0 {
return Err(Error::Invalid("a point lies on a boundary edge".into()));
}
}
Ok((ou == Sign::Positive && ow == Sign::Negative).then_some((t, u, w)))
})?;
let (mut t, mut right, mut left) = found.ok_or_else(|| err("segment start not found"))?;
let mut out = vec![(right, left)];
for _ in 0..self.tris.len() + 1 {
let tri = *self.t(t)?;
let i = tri.v.iter().position(|&x| x != right && x != left).ok_or_else(|| err("walk"))?;
let next = tri.n[i];
let nt = *self.t(next)?;
let d = *nt.v.iter().find(|&&x| x != right && x != left).ok_or_else(|| err("walk"))?;
if d == b {
return Ok(out);
}
match orient2d(&pa, &pb, self.p(d)?) {
Sign::Positive => left = d,
Sign::Negative => right = d,
Sign::Zero => return Err(Error::Invalid("a point lies on a boundary edge".into())),
}
out.push((right, left));
t = next;
}
Err(err("segment walk did not reach its end"))
}
/// Lawson flips from the given edges until every unconstrained edge is locally Delaunay.
fn lawson(&mut self, edges: Vec<(u32, u32)>) -> Result<()> {
let mut stack = edges;
let cap = 1000 + 64 * self.tris.len();
let mut steps = 0usize;
while let Some((u, w)) = stack.pop() {
steps += 1;
if steps > cap {
return Err(Error::Limit("Delaunay restoration did not finish".into()));
}
let Some((t, i)) = self.find_edge(u, w)? else { continue };
let tri = *self.t(t)?;
if tri.fixed[i] || tri.n[i] == NONE {
continue;
}
let Some(s) = self.flippable(t, i)? else { continue };
let [a, b, c] = tri.v;
if incircle(self.p(a)?, self.p(b)?, self.p(c)?, self.p(s)?) != Sign::Positive {
continue;
}
let (p, _) = self.flip(t, i)?;
let (q, r) = (tri.v[(i + 1) % 3], tri.v[(i + 2) % 3]);
stack.extend([(p, q), (q, s), (s, r), (r, p)]);
}
Ok(())
}
/// Mark the triangles inside the region: those separated from the super-triangle by an odd
/// number of constrained edges.
fn classify(&mut self) -> Result<()> {
let n = self.tris.len();
let mut depth = vec![u32::MAX; n];
let start = self.vt.get(self.pts.len().saturating_sub(1)).copied().ok_or_else(|| err("super vertex"))?;
let mut current = vec![start];
if let Some(d) = depth.get_mut(start as usize) {
*d = 0;
}
let mut level = 0u32;
while !current.is_empty() {
let mut next = Vec::new();
let mut k = 0;
while let Some(&t) = current.get(k) {
k += 1;
let tri = *self.t(t)?;
for i in 0..3 {
let nb = tri.n[i];
if nb == NONE || depth.get(nb as usize).is_none_or(|&d| d != u32::MAX) {
continue;
}
if tri.fixed[i] {
next.push(nb);
} else {
if let Some(d) = depth.get_mut(nb as usize) {
*d = level;
}
current.push(nb);
}
}
}
level += 1;
next.retain(|&t| depth.get(t as usize).is_some_and(|&d| d == u32::MAX));
next.sort_unstable();
next.dedup();
for &t in &next {
if let Some(d) = depth.get_mut(t as usize) {
*d = level;
}
}
current = next;
if level > 4 {
break;
}
}
for (tri, d) in self.tris.iter_mut().zip(depth) {
tri.inside = tri.alive && d == 1;
}
Ok(())
}
/// Try to insert Steiner point `p` (located from `hint`), at least `min_distance` from the
/// vertices of its cavity.
fn try_steiner(&mut self, p: Point2, hint: u32, min_distance: f64) -> Result<bool> {
let v = self.pts.len() as u32;
self.pts.push(p.coords());
self.p2.push(p);
self.vt.push(NONE);
self.last = hint;
let ok = self.insert(v, Some(min_distance))?;
if !ok {
self.pts.pop();
self.p2.pop();
self.vt.pop();
}
Ok(ok)
}
/// Delaunay refinement of the inside triangles to `scale × size field`. A Steiner point `p`
/// for local size `h` is skipped when `allows(p, h)` refuses it (the caller's clearance from
/// the true boundary) or when it comes closer than [`CLEARANCE`]` × h` to a vertex.
pub fn refine(&mut self, size: &dyn SizeField, scale: f64, allows: &dyn Fn(&Point2, f64) -> bool) -> Result<()> {
for _ in 0..MAX_REFINE_PASSES {
let mut inserted = 0usize;
let n = self.tris.len();
for t in 0..n as u32 {
let tri = *self.t(t)?;
if !tri.alive || !tri.inside {
continue;
}
let [a, b, c] = [self.xy(tri.v[0])?, self.xy(tri.v[1])?, self.xy(tri.v[2])?];
let centroid = [(a[0] + b[0] + c[0]) / 3.0, (a[1] + b[1] + c[1]) / 3.0];
let h = scale * checked_size(size, [centroid[0], centroid[1], 0.0])?;
let (bx, by, cx, cy) = (b[0] - a[0], b[1] - a[1], c[0] - a[0], c[1] - a[1]);
let d = 2.0 * (bx * cy - by * cx);
let (b2, c2) = (bx * bx + by * by, cx * cx + cy * cy);
let (ux, uy) = ((cy * b2 - by * c2) / d, (bx * c2 - cx * b2) / d);
let r = (ux * ux + uy * uy).sqrt();
let shortest = b2.min(c2).min((cx - bx).powi(2) + (cy - by).powi(2)).sqrt();
if !r.is_finite() || !(d > 0.0) {
continue;
}
let too_big = r > 0.75 * h;
let poor = r > std::f64::consts::SQRT_2 * shortest;
if !too_big && !poor {
continue;
}
let candidates = [Some([a[0] + ux, a[1] + uy]), too_big.then_some(centroid)];
for c in candidates.into_iter().flatten() {
if let Some(p) = steiner_point(c).filter(|p| allows(p, h))
&& self.try_steiner(p, t, CLEARANCE * h)?
{
inserted += 1;
break;
}
}
}
if inserted == 0 {
break;
}
}
#[cfg(test)]
assert_eq!(self.verify(), Ok(()), "after refinement");
Ok(())
}
/// The inside triangles around vertex `v`.
fn inside_fan(&self, v: u32) -> Result<Vec<u32>> {
let mut fan = Vec::new();
self.around(v, |t, tri, _| {
if tri.inside {
fan.push(t);
}
Ok(None::<()>)
})?;
Ok(fan)
}
/// [`Cdt::ensure_fan`] for every vertex with its count in `need`.
pub fn ensure_fans(&mut self, need: &[usize]) -> Result<()> {
for (v, &k) in need.iter().enumerate() {
if k > 1 {
self.ensure_fan(v as u32, k)?;
}
}
#[cfg(test)]
assert_eq!(self.verify(), Ok(()), "after fans");
Ok(())
}
/// Give boundary vertex `v` at least `count` inside triangles by inserting the centroid of
/// its widest triangle (bounded attempts; the caller's validity gate judges the result).
fn ensure_fan(&mut self, v: u32, count: usize) -> Result<()> {
for _ in 0..count + 2 {
let fan = self.inside_fan(v)?;
if fan.len() >= count {
return Ok(());
}
let pv = self.xy(v)?;
let mut widest: Option<(f64, u32, [f64; 2])> = None;
for &t in &fan {
let tri = *self.t(t)?;
let k = tri.v.iter().position(|&x| x == v).ok_or_else(|| err("fan"))?;
let (a, b) = (self.xy(tri.v[(k + 1) % 3])?, self.xy(tri.v[(k + 2) % 3])?);
let (u, w) = ([a[0] - pv[0], a[1] - pv[1]], [b[0] - pv[0], b[1] - pv[1]]);
let cos = (u[0] * w[0] + u[1] * w[1]) / ((u[0] * u[0] + u[1] * u[1]).sqrt() * (w[0] * w[0] + w[1] * w[1]).sqrt());
let c = [(pv[0] + a[0] + b[0]) / 3.0, (pv[1] + a[1] + b[1]) / 3.0];
if widest.is_none_or(|(m, _, _)| cos < m) {
widest = Some((cos, t, c));
}
}
let Some((t, p)) = widest.and_then(|(_, t, c)| steiner_point(c).map(|p| (t, p))) else { return Ok(()) };
if !self.try_steiner(p, t, 0.0)? {
return Ok(());
}
}
Ok(())
}
/// Consistency check for tests: every live triangle counterclockwise (exact), neighbours
/// mutual, constrained flags matching on both sides.
#[cfg(test)]
pub fn verify(&self) -> std::result::Result<(), String> {
for (t, tri) in self.tris.iter().enumerate() {
if !tri.alive {
continue;
}
let [a, b, c] = tri.v;
let o = orient2d(&self.p2[a as usize], &self.p2[b as usize], &self.p2[c as usize]);
if o != Sign::Positive {
return Err(format!("triangle {t} {:?} has orientation {o:?} (inside {})", tri.v, tri.inside));
}
for i in 0..3 {
let nb = tri.n[i];
if nb == NONE {
continue;
}
let o = &self.tris[nb as usize];
let (p, q) = (tri.v[(i + 1) % 3], tri.v[(i + 2) % 3]);
let back = (0..3).find(|&j| o.v[(j + 1) % 3] == q && o.v[(j + 2) % 3] == p);
match back {
Some(j) if o.n[j] == t as u32 && o.alive && o.fixed[j] == tri.fixed[i] => {
let s = o.v[j];
if !tri.fixed[i]
&& incircle(&self.p2[a as usize], &self.p2[b as usize], &self.p2[c as usize], &self.p2[s as usize]) == Sign::Positive
{
return Err(format!("triangle {t} edge {i} is not locally Delaunay (inside {})", tri.inside));
}
}
_ => return Err(format!("triangle {t} edge {i}: neighbour {nb} does not match")),
}
}
}
Ok(())
}
/// The inside triangles.
pub fn into_triangulation(self) -> Triangulation {
let tris = self.tris.iter().filter(|t| t.alive && t.inside).map(|t| t.v).collect();
Triangulation { pts: self.pts, p2: self.p2, tris }
}
}