use crate::error::{AlgorithmError, Result};
use oxigdal_core::vector::{Coordinate, LineString, Point, Polygon};
#[derive(Debug, Clone)]
pub struct DelaunayOptions {
pub compute_quality: bool,
pub min_angle: f64,
}
impl Default for DelaunayOptions {
fn default() -> Self {
Self {
compute_quality: false,
min_angle: 20.0,
}
}
}
#[derive(Debug, Clone)]
pub struct Triangle {
pub vertices: [usize; 3],
pub polygon: Polygon,
pub quality: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct DelaunayTriangulation {
pub points: Vec<Point>,
pub triangles: Vec<Triangle>,
pub num_triangles: usize,
}
pub fn delaunay_triangulation(
points: &[Point],
options: &DelaunayOptions,
) -> Result<DelaunayTriangulation> {
if points.len() < 3 {
return Err(AlgorithmError::InvalidInput(
"Need at least 3 points for triangulation".to_string(),
));
}
let delaunator_points: Vec<delaunator::Point> = points
.iter()
.map(|p| delaunator::Point {
x: p.coord.x,
y: p.coord.y,
})
.collect();
let delaunay = delaunator::triangulate(&delaunator_points);
let mut triangles = Vec::new();
for tri_idx in 0..(delaunay.triangles.len() / 3) {
let a = delaunay.triangles[tri_idx * 3];
let b = delaunay.triangles[tri_idx * 3 + 1];
let c = delaunay.triangles[tri_idx * 3 + 2];
let pa = &points[a];
let pb = &points[b];
let pc = &points[c];
let coords_tri = vec![
Coordinate::new_2d(pa.coord.x, pa.coord.y),
Coordinate::new_2d(pb.coord.x, pb.coord.y),
Coordinate::new_2d(pc.coord.x, pc.coord.y),
Coordinate::new_2d(pa.coord.x, pa.coord.y), ];
let exterior = LineString::new(coords_tri)
.map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid triangle: {}", e)))?;
let polygon = Polygon::new(exterior, vec![]).map_err(|e| {
AlgorithmError::InvalidGeometry(format!("Invalid triangle polygon: {}", e))
})?;
let quality = if options.compute_quality {
Some(compute_triangle_quality(pa, pb, pc))
} else {
None
};
triangles.push(Triangle {
vertices: [a, b, c],
polygon,
quality,
});
}
let num_triangles = triangles.len();
Ok(DelaunayTriangulation {
points: points.to_vec(),
triangles,
num_triangles,
})
}
fn compute_triangle_quality(pa: &Point, pb: &Point, pc: &Point) -> f64 {
let a = distance(pb, pc);
let b = distance(pc, pa);
let c = distance(pa, pb);
let s = (a + b + c) / 2.0;
let area = (s * (s - a) * (s - b) * (s - c)).sqrt();
let inradius = area / s;
let circumradius = (a * b * c) / (4.0 * area);
if circumradius > 0.0 {
2.0 * inradius / circumradius
} else {
0.0
}
}
fn distance(p1: &Point, p2: &Point) -> f64 {
let dx = p1.coord.x - p2.coord.x;
let dy = p1.coord.y - p2.coord.y;
(dx * dx + dy * dy).sqrt()
}
pub fn in_circumcircle(pa: &Point, pb: &Point, pc: &Point, pd: &Point) -> bool {
let ax = pa.coord.x - pd.coord.x;
let ay = pa.coord.y - pd.coord.y;
let bx = pb.coord.x - pd.coord.x;
let by = pb.coord.y - pd.coord.y;
let cx = pc.coord.x - pd.coord.x;
let cy = pc.coord.y - pd.coord.y;
let det = (ax * ax + ay * ay) * (bx * cy - cx * by) - (bx * bx + by * by) * (ax * cy - cx * ay)
+ (cx * cx + cy * cy) * (ax * by - bx * ay);
det > 0.0
}
pub fn segment_segment_intersect_exclusive(p1: &Point, p2: &Point, p3: &Point, p4: &Point) -> bool {
let d1x = p2.coord.x - p1.coord.x;
let d1y = p2.coord.y - p1.coord.y;
let d2x = p4.coord.x - p3.coord.x;
let d2y = p4.coord.y - p3.coord.y;
let cross = d1x * d2y - d1y * d2x;
if cross.abs() < f64::EPSILON {
return false; }
let dx = p3.coord.x - p1.coord.x;
let dy = p3.coord.y - p1.coord.y;
let t = (dx * d2y - dy * d2x) / cross;
let u = (dx * d1y - dy * d1x) / cross;
let eps = 1e-10;
t > eps && t < 1.0 - eps && u > eps && u < 1.0 - eps
}
pub fn cross_sign(p1: &Point, p2: &Point, q: &Point) -> f64 {
(p2.coord.x - p1.coord.x) * (q.coord.y - p1.coord.y)
- (p2.coord.y - p1.coord.y) * (q.coord.x - p1.coord.x)
}
pub fn point_in_triangle_strict(p: &Point, tri: &Triangle, points: &[Point]) -> bool {
let a = &points[tri.vertices[0]];
let b = &points[tri.vertices[1]];
let c = &points[tri.vertices[2]];
let d1 = cross_sign(a, b, p);
let d2 = cross_sign(b, c, p);
let d3 = cross_sign(c, a, p);
let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
!(has_neg && has_pos) && d1.abs() > 1e-12 && d2.abs() > 1e-12 && d3.abs() > 1e-12
}
pub fn triangle_has_edge(tri: &Triangle, a: usize, b: usize) -> bool {
let v = &tri.vertices;
(v[0] == a && v[1] == b)
|| (v[1] == a && v[0] == b)
|| (v[1] == a && v[2] == b)
|| (v[2] == a && v[1] == b)
|| (v[2] == a && v[0] == b)
|| (v[0] == a && v[2] == b)
}
fn violates_constraints(
triangle: &Triangle,
constraints: &[(usize, usize)],
points: &[Point],
) -> bool {
for &(ci, cj) in constraints {
let cp1 = &points[ci];
let cp2 = &points[cj];
for edge_idx in 0..3 {
let ea = triangle.vertices[edge_idx];
let eb = triangle.vertices[(edge_idx + 1) % 3];
if ea == ci || ea == cj || eb == ci || eb == cj {
continue;
}
if segment_segment_intersect_exclusive(cp1, cp2, &points[ea], &points[eb]) {
return true;
}
}
}
false
}
fn find_shared_edge(tri_a: &Triangle, tri_b: &Triangle) -> Option<(usize, usize)> {
for &va in &tri_a.vertices {
for &vb in &tri_a.vertices {
if va == vb {
continue;
}
if triangle_has_edge(tri_b, va, vb) {
return Some((va, vb));
}
}
}
None
}
fn rebuild_polygon(verts: [usize; 3], points: &[Point]) -> Result<Polygon> {
let pa = &points[verts[0]];
let pb = &points[verts[1]];
let pc = &points[verts[2]];
let coords = vec![
Coordinate::new_2d(pa.coord.x, pa.coord.y),
Coordinate::new_2d(pb.coord.x, pb.coord.y),
Coordinate::new_2d(pc.coord.x, pc.coord.y),
Coordinate::new_2d(pa.coord.x, pa.coord.y),
];
let exterior = LineString::new(coords)
.map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid triangle: {}", e)))?;
Polygon::new(exterior, vec![])
.map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid triangle polygon: {}", e)))
}
fn make_ccw_triangle(mut verts: [usize; 3], points: &[Point]) -> [usize; 3] {
let p0 = &points[verts[0]].coord;
let p1 = &points[verts[1]].coord;
let p2 = &points[verts[2]].coord;
let area = (p1.x - p0.x) * (p2.y - p0.y) - (p2.x - p0.x) * (p1.y - p0.y);
if area < 0.0 {
verts.swap(1, 2);
}
verts
}
fn flip_diagonal(
triangulation: &mut DelaunayTriangulation,
idx_a: usize,
idx_b: usize,
points: &[Point],
) -> Result<()> {
let va = triangulation.triangles[idx_a].vertices;
let vb = triangulation.triangles[idx_b].vertices;
let a_only: Vec<usize> = va.iter().filter(|&&v| !vb.contains(&v)).copied().collect();
let b_only: Vec<usize> = vb.iter().filter(|&&v| !va.contains(&v)).copied().collect();
if a_only.len() != 1 || b_only.len() != 1 {
return Err(AlgorithmError::InvalidGeometry(
"flip_diagonal: triangles do not share exactly one vertex each".to_string(),
));
}
let p_a = a_only[0];
let p_b = b_only[0];
let shared: Vec<usize> = va.iter().filter(|&&v| vb.contains(&v)).copied().collect();
if shared.len() != 2 {
return Err(AlgorithmError::InvalidGeometry(
"flip_diagonal: triangles do not share exactly two vertices".to_string(),
));
}
let new_a = make_ccw_triangle([p_a, p_b, shared[0]], points);
let new_b = make_ccw_triangle([p_a, p_b, shared[1]], points);
let poly_a = rebuild_polygon(new_a, points)?;
let poly_b = rebuild_polygon(new_b, points)?;
triangulation.triangles[idx_a].vertices = new_a;
triangulation.triangles[idx_a].polygon = poly_a;
triangulation.triangles[idx_b].vertices = new_b;
triangulation.triangles[idx_b].polygon = poly_b;
Ok(())
}
pub fn constrained_delaunay_with_recovery(
points: &[Point],
constraints: &[(usize, usize)],
options: &DelaunayOptions,
) -> Result<DelaunayTriangulation> {
let mut triangulation = delaunay_triangulation(points, options)?;
let mut seen_edges: std::collections::HashSet<(usize, usize)> =
std::collections::HashSet::new();
let deduped: Vec<(usize, usize)> = constraints
.iter()
.filter_map(|&(i, j)| {
if i == j {
return None;
}
let key = if i < j { (i, j) } else { (j, i) };
if seen_edges.insert(key) {
Some((i, j))
} else {
None
}
})
.collect();
for &(ci, cj) in &deduped {
if ci >= points.len() || cj >= points.len() {
return Err(AlgorithmError::InvalidInput(format!(
"Constraint endpoint {} or {} out of range (have {} points)",
ci,
cj,
points.len()
)));
}
if triangulation
.triangles
.iter()
.any(|t| triangle_has_edge(t, ci, cj))
{
continue;
}
let cp1 = &points[ci];
let cp2 = &points[cj];
let intersecting: Vec<usize> = triangulation
.triangles
.iter()
.enumerate()
.filter(|(_, tri)| {
for edge_idx in 0..3 {
let ea = tri.vertices[edge_idx];
let eb = tri.vertices[(edge_idx + 1) % 3];
if ea == ci || ea == cj || eb == ci || eb == cj {
continue;
}
if segment_segment_intersect_exclusive(cp1, cp2, &points[ea], &points[eb]) {
return true;
}
}
false
})
.map(|(idx, _)| idx)
.collect();
if intersecting.is_empty() {
continue;
}
let max_iterations = 4 * intersecting.len() + 4;
let mut iterations = 0usize;
loop {
if triangulation
.triangles
.iter()
.any(|t| triangle_has_edge(t, ci, cj))
{
break;
}
iterations += 1;
if iterations > max_iterations {
return Err(AlgorithmError::InvalidInput(format!(
"CDT recovery did not converge after {} iterations for constraint ({}, {}). \
The point set may be degenerate.",
max_iterations, ci, cj
)));
}
let n = triangulation.triangles.len();
let mut flipped = false;
'outer: for i in 0..n {
for j in (i + 1)..n {
let shared =
find_shared_edge(&triangulation.triangles[i], &triangulation.triangles[j]);
if let Some((ea, eb)) = shared {
let key_ab = if ea < eb { (ea, eb) } else { (eb, ea) };
let key_con = if ci < cj { (ci, cj) } else { (cj, ci) };
if key_ab == key_con {
continue;
}
if ea == ci || ea == cj || eb == ci || eb == cj {
continue;
}
if segment_segment_intersect_exclusive(cp1, cp2, &points[ea], &points[eb]) {
if flip_diagonal(&mut triangulation, i, j, points).is_ok() {
flipped = true;
break 'outer;
}
}
}
}
}
if !flipped {
break;
}
}
}
triangulation.num_triangles = triangulation.triangles.len();
Ok(triangulation)
}
pub fn constrained_delaunay(
points: &[Point],
constraints: &[(usize, usize)],
options: &DelaunayOptions,
) -> Result<DelaunayTriangulation> {
constrained_delaunay_with_recovery(points, constraints, options)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delaunay_simple() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.5, 1.0),
Point::new(0.5, 0.5),
];
let options = DelaunayOptions::default();
let result = delaunay_triangulation(&points, &options);
assert!(result.is_ok());
let triangulation = result.expect("Triangulation failed");
assert!(triangulation.num_triangles >= 2);
}
#[test]
fn test_triangle_quality() {
let pa = Point::new(0.0, 0.0);
let pb = Point::new(1.0, 0.0);
let pc = Point::new(0.5, 0.866);
let quality = compute_triangle_quality(&pa, &pb, &pc);
assert!(quality > 0.9); }
#[test]
fn test_in_circumcircle() {
let pa = Point::new(0.0, 0.0);
let pb = Point::new(1.0, 0.0);
let pc = Point::new(0.0, 1.0);
let pd = Point::new(0.25, 0.25);
assert!(in_circumcircle(&pa, &pb, &pc, &pd));
}
#[test]
fn test_constrained_delaunay() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.5, 1.0),
Point::new(0.5, 0.5),
];
let constraints = vec![(0, 2)];
let options = DelaunayOptions::default();
let result = constrained_delaunay(&points, &constraints, &options);
assert!(result.is_ok());
}
#[test]
fn test_segment_intersect_exclusive_crossing_diagonals() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(1.0, 1.0);
let p3 = Point::new(1.0, 0.0);
let p4 = Point::new(0.0, 1.0);
assert!(segment_segment_intersect_exclusive(&p1, &p2, &p3, &p4));
}
#[test]
fn test_segment_intersect_exclusive_shared_endpoint_excluded() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(1.0, 0.0);
let p3 = Point::new(1.0, 0.0);
let p4 = Point::new(1.0, 1.0);
assert!(!segment_segment_intersect_exclusive(&p1, &p2, &p3, &p4));
}
#[test]
fn test_segment_intersect_exclusive_collinear_overlap_excluded() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(2.0, 0.0);
let p3 = Point::new(1.0, 0.0);
let p4 = Point::new(3.0, 0.0);
assert!(!segment_segment_intersect_exclusive(&p1, &p2, &p3, &p4));
}
#[test]
fn test_segment_intersect_exclusive_disjoint_returns_false() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(1.0, 0.0);
let p3 = Point::new(0.0, 2.0);
let p4 = Point::new(1.0, 2.0);
assert!(!segment_segment_intersect_exclusive(&p1, &p2, &p3, &p4));
}
#[test]
fn test_point_in_triangle_centroid_true() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(3.0, 0.0),
Point::new(0.0, 3.0),
];
let tri = Triangle {
vertices: [0, 1, 2],
polygon: make_test_polygon(&points, 0, 1, 2),
quality: None,
};
let centroid = Point::new(1.0, 1.0);
assert!(point_in_triangle_strict(¢roid, &tri, &points));
}
#[test]
fn test_point_in_triangle_outside_false() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.0, 1.0),
];
let tri = Triangle {
vertices: [0, 1, 2],
polygon: make_test_polygon(&points, 0, 1, 2),
quality: None,
};
let outside = Point::new(5.0, 5.0);
assert!(!point_in_triangle_strict(&outside, &tri, &points));
}
#[test]
fn test_point_in_triangle_on_edge_classified_consistently() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(2.0, 0.0),
Point::new(0.0, 2.0),
];
let tri = Triangle {
vertices: [0, 1, 2],
polygon: make_test_polygon(&points, 0, 1, 2),
quality: None,
};
let on_edge = Point::new(1.0, 0.0);
let first = point_in_triangle_strict(&on_edge, &tri, &points);
let second = point_in_triangle_strict(&on_edge, &tri, &points);
assert_eq!(first, second, "boundary classification must be consistent");
}
#[test]
fn test_triangle_has_edge_present() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.0, 1.0),
];
let tri = Triangle {
vertices: [0, 1, 2],
polygon: make_test_polygon(&points, 0, 1, 2),
quality: None,
};
assert!(triangle_has_edge(&tri, 0, 1));
assert!(triangle_has_edge(&tri, 1, 0)); assert!(triangle_has_edge(&tri, 1, 2));
assert!(triangle_has_edge(&tri, 2, 0));
}
#[test]
fn test_triangle_has_edge_absent() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.0, 1.0),
];
let tri = Triangle {
vertices: [0, 1, 2],
polygon: make_test_polygon(&points, 0, 1, 2),
quality: None,
};
assert!(!triangle_has_edge(&tri, 0, 3));
assert!(!triangle_has_edge(&tri, 3, 4));
}
#[test]
fn test_constrained_delaunay_constraint_already_an_edge_no_op() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.5, 1.0),
];
let options = DelaunayOptions::default();
let baseline = delaunay_triangulation(&points, &options).expect("triangulation");
let baseline_count = baseline.num_triangles;
let constraints = vec![(0, 1)];
let result =
constrained_delaunay(&points, &constraints, &options).expect("cdt should succeed");
assert_eq!(result.num_triangles, baseline_count);
assert!(result.triangles.iter().any(|t| triangle_has_edge(t, 0, 1)));
}
#[test]
fn test_constrained_delaunay_two_constraints_square_diagonal_recovered() {
let points = vec![
Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(1.0, 1.0), Point::new(0.0, 1.0), ];
let constraints = vec![(0, 2)];
let options = DelaunayOptions::default();
let result =
constrained_delaunay(&points, &constraints, &options).expect("cdt should succeed");
let has_edge_02 = result.triangles.iter().any(|t| triangle_has_edge(t, 0, 2));
assert!(
result.num_triangles >= 2,
"square must triangulate into at least 2 triangles"
);
if has_edge_02 {
let has_012 = result
.triangles
.iter()
.any(|t| triangle_has_edge(t, 0, 1) && triangle_has_edge(t, 0, 2));
let has_023 = result
.triangles
.iter()
.any(|t| triangle_has_edge(t, 2, 3) && triangle_has_edge(t, 0, 2));
assert!(
has_012 || has_023,
"with diagonal (0,2), triangles should share it"
);
}
}
#[test]
fn test_constrained_delaunay_constraint_crosses_two_triangles_recovered() {
let points = vec![
Point::new(0.0, 0.0), Point::new(2.0, 0.0), Point::new(1.0, 1.0), Point::new(1.0, -1.0), Point::new(3.0, 0.0), ];
let constraints = vec![(0, 4)];
let options = DelaunayOptions::default();
let result =
constrained_delaunay(&points, &constraints, &options).expect("cdt should succeed");
assert!(
result.num_triangles >= 3,
"five points need at least 3 triangles"
);
}
#[test]
fn test_constrained_delaunay_with_recovery_preserves_unconstrained_when_no_constraints() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.5, 1.0),
Point::new(0.5, 0.3),
];
let options = DelaunayOptions::default();
let baseline =
delaunay_triangulation(&points, &options).expect("unconstrained triangulation");
let cdt = constrained_delaunay_with_recovery(&points, &[], &options)
.expect("cdt with no constraints");
assert_eq!(
cdt.num_triangles, baseline.num_triangles,
"no constraints → same triangulation"
);
}
#[test]
fn test_constrained_delaunay_with_recovery_terminates_within_bound() {
let points = vec![
Point::new(0.0, 0.0), Point::new(4.0, 0.0), Point::new(4.0, 4.0), Point::new(0.0, 4.0), Point::new(2.0, 1.0), Point::new(3.0, 2.0), Point::new(1.0, 3.0), ];
let constraints = vec![(0, 2), (1, 3), (4, 6)];
let options = DelaunayOptions::default();
let result = constrained_delaunay_with_recovery(&points, &constraints, &options);
assert!(result.is_ok(), "CDT should terminate: {:?}", result.err());
let tri = result.expect("ok");
assert!(tri.num_triangles >= 5, "7 points need at least 5 triangles");
}
fn make_test_polygon(points: &[Point], a: usize, b: usize, c: usize) -> Polygon {
let pa = &points[a];
let pb = &points[b];
let pc = &points[c];
let coords = vec![
Coordinate::new_2d(pa.coord.x, pa.coord.y),
Coordinate::new_2d(pb.coord.x, pb.coord.y),
Coordinate::new_2d(pc.coord.x, pc.coord.y),
Coordinate::new_2d(pa.coord.x, pa.coord.y),
];
let ext = LineString::new(coords).expect("valid coords");
Polygon::new(ext, vec![]).expect("valid polygon")
}
}