#[cfg(feature = "triangulation-alt")]
pub(crate) mod alt_oracle;
mod ring_geom;
use ring_geom::{point_in_ring, ring_bbox};
use crate::{Error, Point2, Point3, Result, Vector3};
fn ear_clip(data: &[f64], hole_indices: &[usize]) -> std::result::Result<Vec<usize>, String> {
#[cfg(feature = "triangulation-alt")]
if let Some(alt) = alt_oracle::maybe_earcut(data, hole_indices) {
return Ok(alt);
}
earcutr::earcut(data, hole_indices, 2).map_err(|e| format!("{e:?}"))
}
pub(crate) fn safe_earcut(
data: &[f64],
hole_indices: &[usize],
dims: usize,
) -> std::result::Result<Vec<usize>, String> {
debug_assert_eq!(dims, 2, "safe_earcut is used for 2D rings only");
let n = data.len() / dims;
if data.iter().any(|c| !c.is_finite()) {
return Err("non-finite coordinate in polygon ring".to_string());
}
if hole_indices.is_empty() {
let has_dup = n >= 2
&& ((0..n - 1).any(|v| {
data[v * 2] == data[v * 2 + 2] && data[v * 2 + 1] == data[v * 2 + 3]
}) || (data[0] == data[(n - 1) * 2] && data[1] == data[(n - 1) * 2 + 1]));
if !has_dup {
return ear_clip(data, &[]);
}
}
let mut ring_bounds: Vec<(usize, usize)> = Vec::with_capacity(hole_indices.len() + 1);
{
let mut start = 0usize;
for &h in hole_indices {
ring_bounds.push((start, h));
start = h;
}
ring_bounds.push((start, n));
}
let mut rings: Vec<(Vec<f64>, Vec<usize>)> = Vec::with_capacity(ring_bounds.len());
for &(start, end) in &ring_bounds {
let mut coords: Vec<f64> = Vec::with_capacity((end - start) * 2);
let mut orig: Vec<usize> = Vec::with_capacity(end - start);
for v in start..end {
let (x, y) = (data[v * 2], data[v * 2 + 1]);
if let (Some(&px), Some(&py)) = (
coords.len().checked_sub(2).and_then(|i| coords.get(i)),
coords.len().checked_sub(1).and_then(|i| coords.get(i)),
) {
if px == x && py == y {
continue;
}
}
coords.push(x);
coords.push(y);
orig.push(v);
}
if orig.len() >= 2
&& coords[0] == coords[coords.len() - 2]
&& coords[1] == coords[coords.len() - 1]
{
coords.truncate(coords.len() - 2);
orig.pop();
}
rings.push((coords, orig));
}
let (outer_coords, outer_orig) = &rings[0];
if outer_orig.len() < 3 {
return Ok(Vec::new());
}
let outer_bbox = ring_bbox(outer_coords);
let mut real_holes: Vec<usize> = Vec::new(); let mut separate_polys: Vec<usize> = Vec::new();
for (ring_no, (coords, orig)) in rings.iter().enumerate().skip(1) {
if orig.len() < 3 {
continue; }
let bbox = ring_bbox(coords);
let bbox_disjoint = bbox.2 < outer_bbox.0
|| bbox.0 > outer_bbox.2
|| bbox.3 < outer_bbox.1
|| bbox.1 > outer_bbox.3;
if bbox_disjoint {
separate_polys.push(ring_no);
} else if coords
.chunks_exact(2)
.any(|p| point_in_ring(p[0], p[1], outer_coords))
{
real_holes.push(ring_no);
}
}
let mut out: Vec<usize> = Vec::new();
{
let mut coords: Vec<f64> =
Vec::with_capacity(outer_coords.len() + real_holes.len() * 8);
let mut orig: Vec<usize> = Vec::with_capacity(outer_orig.len());
coords.extend_from_slice(outer_coords);
orig.extend_from_slice(outer_orig);
let mut holes: Vec<usize> = Vec::with_capacity(real_holes.len());
for &ring_no in &real_holes {
holes.push(orig.len());
coords.extend_from_slice(&rings[ring_no].0);
orig.extend_from_slice(&rings[ring_no].1);
}
let indices = ear_clip(&coords, &holes)?;
out.extend(indices.into_iter().map(|i| orig[i]));
}
for &ring_no in &separate_polys {
let (coords, orig) = &rings[ring_no];
let indices = ear_clip(coords, &[])?;
out.extend(indices.into_iter().map(|i| orig[i]));
}
Ok(out)
}
#[inline]
fn is_convex(points: &[Point2<f64>]) -> bool {
if points.len() < 3 {
return false;
}
let n = points.len();
let mut sign = 0i8;
for i in 0..n {
let p0 = &points[i];
let p1 = &points[(i + 1) % n];
let p2 = &points[(i + 2) % n];
let cross = (p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x);
if cross.abs() > 1e-10 {
let current_sign = if cross > 0.0 { 1i8 } else { -1i8 };
if sign == 0 {
sign = current_sign;
} else if sign != current_sign {
return false; }
}
}
true
}
#[inline]
fn quad_indices(p: &[Point2<f64>]) -> Vec<usize> {
let orient = |a: &Point2<f64>, b: &Point2<f64>, c: &Point2<f64>| {
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
};
let c1 = orient(&p[0], &p[1], &p[2]);
let c3 = orient(&p[2], &p[3], &p[0]);
if c1 * c3 >= 0.0 {
vec![0, 1, 2, 0, 2, 3]
} else {
vec![1, 2, 3, 1, 3, 0]
}
}
#[inline]
fn fan_triangulate(n: usize) -> Vec<usize> {
let mut indices = Vec::with_capacity((n - 2) * 3);
for i in 1..n - 1 {
indices.push(0);
indices.push(i);
indices.push(i + 1);
}
indices
}
#[inline]
pub fn triangulate_polygon(points: &[Point2<f64>]) -> Result<Vec<usize>> {
let n = points.len();
if n < 3 {
return Err(Error::TriangulationError(
"Need at least 3 points to triangulate".to_string(),
));
}
if n == 3 {
return Ok(vec![0, 1, 2]);
}
if n == 4 {
return Ok(quad_indices(points));
}
if n <= 8 && is_convex(points) {
return Ok(fan_triangulate(n));
}
let mut vertices = Vec::with_capacity(n * 2);
for p in points {
vertices.push(p.x);
vertices.push(p.y);
}
let indices = safe_earcut(&vertices, &[], 2).map_err(Error::TriangulationError)?;
Ok(indices)
}
#[inline]
pub fn triangulate_polygon_with_holes(
outer: &[Point2<f64>],
holes: &[Vec<Point2<f64>>],
) -> Result<Vec<usize>> {
if outer.len() < 3 {
return Err(Error::TriangulationError(
"Need at least 3 points in outer boundary".to_string(),
));
}
let valid_holes: Vec<&Vec<Point2<f64>>> = holes.iter().filter(|h| h.len() >= 3).collect();
if valid_holes.is_empty() {
return triangulate_polygon(outer);
}
let total_points: usize = outer.len() + valid_holes.iter().map(|h| h.len()).sum::<usize>();
let mut vertices = Vec::with_capacity(total_points * 2);
for p in outer {
vertices.push(p.x);
vertices.push(p.y);
}
let mut hole_indices = Vec::with_capacity(valid_holes.len());
for hole in valid_holes {
hole_indices.push(vertices.len() / 2);
for p in hole {
vertices.push(p.x);
vertices.push(p.y);
}
}
let indices =
safe_earcut(&vertices, &hole_indices, 2).map_err(Error::TriangulationError)?;
Ok(indices)
}
pub fn triangulate_polygon_with_holes_refined(
outer: &[Point2<f64>],
holes: &[Vec<Point2<f64>>],
) -> Result<(Vec<Point2<f64>>, Vec<usize>)> {
if outer.len() < 3 {
return Err(Error::TriangulationError(
"Need at least 3 points in outer boundary".to_string(),
));
}
let valid_holes: Vec<Vec<Point2<f64>>> =
holes.iter().filter(|h| h.len() >= 3).cloned().collect();
if let Some((pts, idx)) = crate::cdt::triangulate_refined(outer, &valid_holes) {
return Ok((pts, idx));
}
let mut all: Vec<Point2<f64>> = outer.to_vec();
for h in &valid_holes {
all.extend_from_slice(h);
}
let idx = if valid_holes.is_empty() {
triangulate_polygon(outer)?
} else {
triangulate_polygon_with_holes(outer, &valid_holes)?
};
Ok((all, idx))
}
#[inline]
pub fn project_to_2d(
points_3d: &[Point3<f64>],
normal: &Vector3<f64>,
) -> (Vec<Point2<f64>>, Vector3<f64>, Vector3<f64>, Point3<f64>) {
if points_3d.is_empty() {
return (
Vec::new(),
Vector3::zeros(),
Vector3::zeros(),
Point3::origin(),
);
}
let origin = points_3d[0];
let abs_x = normal.x.abs();
let abs_y = normal.y.abs();
let abs_z = normal.z.abs();
let reference = if abs_x <= abs_y && abs_x <= abs_z {
Vector3::new(1.0, 0.0, 0.0)
} else if abs_y <= abs_z {
Vector3::new(0.0, 1.0, 0.0)
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let u_axis = normal.cross(&reference).normalize();
let v_axis = normal.cross(&u_axis).normalize();
let points_2d = points_3d
.iter()
.map(|p| {
let v = p - origin;
Point2::new(v.dot(&u_axis), v.dot(&v_axis))
})
.collect();
(points_2d, u_axis, v_axis, origin)
}
#[inline]
pub fn project_to_2d_with_basis(
points_3d: &[Point3<f64>],
u_axis: &Vector3<f64>,
v_axis: &Vector3<f64>,
origin: &Point3<f64>,
) -> Vec<Point2<f64>> {
points_3d
.iter()
.map(|p| {
let v = p - origin;
Point2::new(v.dot(u_axis), v.dot(v_axis))
})
.collect()
}
#[inline]
pub fn calculate_polygon_normal(points: &[Point3<f64>]) -> Vector3<f64> {
let n = points.len();
if n < 3 {
return Vector3::new(0.0, 0.0, 1.0);
}
if n <= 4 {
let v1 = points[1] - points[0];
let v2 = points[2] - points[0];
let normal = v1.cross(&v2);
let len = normal.norm();
if len > 1e-10 {
return normal / len;
}
if n == 4 {
let v3 = points[3] - points[0];
let normal = v2.cross(&v3);
let len = normal.norm();
if len > 1e-10 {
return normal / len;
}
}
return Vector3::new(0.0, 0.0, 1.0);
}
let mut normal = Vector3::<f64>::zeros();
for i in 0..n {
let current = &points[i];
let next = &points[(i + 1) % n];
normal.x += (current.y - next.y) * (current.z + next.z);
normal.y += (current.z - next.z) * (current.x + next.x);
normal.z += (current.x - next.x) * (current.y + next.y);
}
let len = normal.norm();
if len > 1e-10 {
normal.normalize()
} else {
Vector3::new(0.0, 0.0, 1.0)
}
}
#[cfg(test)]
#[path = "triangulation_tests.rs"]
mod tests;