use crate::model::Vertex2D;
#[derive(Debug, thiserror::Error)]
pub enum TriangulationError {
#[error("Polygon has too few vertices: {0} (minimum 3 required)")]
TooFewVertices(usize),
#[error("Invalid hole: {0}")]
InvalidHole(String),
#[error("Triangulation failed: {0}")]
TriangulationFailed(String),
}
pub fn triangulate_simple(polygon: &[Vertex2D]) -> Result<Vec<usize>, TriangulationError> {
if polygon.len() < 3 {
return Err(TriangulationError::TooFewVertices(polygon.len()));
}
let mut coords = Vec::with_capacity(polygon.len() * 2);
for vertex in polygon {
coords.push(vertex.x);
coords.push(vertex.y);
}
let hole_indices: Vec<usize> = Vec::new();
let result = earcutr::earcut(&coords, &hole_indices, 2)
.map_err(|e| TriangulationError::TriangulationFailed(format!("Earcut error: {}", e)))?;
if result.is_empty() && polygon.len() >= 3 {
return Err(TriangulationError::TriangulationFailed(
"Earcut returned no triangles".to_string(),
));
}
Ok(result)
}
pub fn triangulate_with_holes(
outer: &[Vertex2D],
holes: &[Vec<Vertex2D>],
) -> Result<Vec<usize>, TriangulationError> {
if outer.len() < 3 {
return Err(TriangulationError::TooFewVertices(outer.len()));
}
for (i, hole) in holes.iter().enumerate() {
if hole.len() < 3 {
return Err(TriangulationError::InvalidHole(format!(
"Hole {} has only {} vertices (minimum 3 required)",
i,
hole.len()
)));
}
}
let total_vertices = outer
.len()
.saturating_add(holes.iter().map(|h| h.len()).sum::<usize>());
let mut coords = Vec::with_capacity(total_vertices.saturating_mul(2));
for vertex in outer {
coords.push(vertex.x);
coords.push(vertex.y);
}
let mut hole_indices = Vec::with_capacity(holes.len());
let mut current_index = outer.len();
for hole in holes {
hole_indices.push(current_index);
for vertex in hole {
coords.push(vertex.x);
coords.push(vertex.y);
}
current_index += hole.len();
}
let result = earcutr::earcut(&coords, &hole_indices, 2)
.map_err(|e| TriangulationError::TriangulationFailed(format!("Earcut error: {}", e)))?;
if result.is_empty() && outer.len() >= 3 {
return Err(TriangulationError::TriangulationFailed(
"Earcut returned no triangles".to_string(),
));
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_triangulate_simple_square() {
let vertices = vec![
Vertex2D::new(0.0, 0.0),
Vertex2D::new(10.0, 0.0),
Vertex2D::new(10.0, 10.0),
Vertex2D::new(0.0, 10.0),
];
let triangles = triangulate_simple(&vertices).expect("Failed to triangulate square");
assert_eq!(triangles.len(), 6, "Expected 2 triangles (6 indices)");
for &idx in &triangles {
assert!(idx < 4, "Triangle index {} out of bounds", idx);
}
}
#[test]
fn test_triangulate_simple_triangle() {
let vertices = vec![
Vertex2D::new(0.0, 0.0),
Vertex2D::new(10.0, 0.0),
Vertex2D::new(5.0, 10.0),
];
let triangles = triangulate_simple(&vertices).expect("Failed to triangulate triangle");
assert_eq!(triangles.len(), 3, "Expected 1 triangle (3 indices)");
for &idx in &triangles {
assert!(idx < 3, "Triangle index {} out of bounds", idx);
}
}
#[test]
fn test_triangulate_simple_pentagon() {
let vertices = vec![
Vertex2D::new(0.0, 5.0),
Vertex2D::new(4.75, 1.54),
Vertex2D::new(2.94, -4.05),
Vertex2D::new(-2.94, -4.05),
Vertex2D::new(-4.75, 1.54),
];
let triangles = triangulate_simple(&vertices).expect("Failed to triangulate pentagon");
assert_eq!(triangles.len(), 9, "Expected 3 triangles (9 indices)");
for &idx in &triangles {
assert!(idx < 5, "Triangle index {} out of bounds", idx);
}
}
#[test]
fn test_triangulate_too_few_vertices() {
let vertices = vec![Vertex2D::new(0.0, 0.0), Vertex2D::new(10.0, 0.0)];
let result = triangulate_simple(&vertices);
assert!(result.is_err(), "Should fail with too few vertices");
match result {
Err(TriangulationError::TooFewVertices(n)) => {
assert_eq!(n, 2);
}
_ => panic!("Expected TooFewVertices error"),
}
}
#[test]
fn test_triangulate_with_one_hole() {
let outer = vec![
Vertex2D::new(0.0, 0.0),
Vertex2D::new(100.0, 0.0),
Vertex2D::new(100.0, 100.0),
Vertex2D::new(0.0, 100.0),
];
let hole = vec![
Vertex2D::new(25.0, 25.0),
Vertex2D::new(75.0, 25.0),
Vertex2D::new(50.0, 75.0),
];
let triangles = triangulate_with_holes(&outer, &[hole])
.expect("Failed to triangulate polygon with hole");
assert!(!triangles.is_empty(), "Expected at least one triangle");
for &idx in &triangles {
assert!(idx < 7, "Triangle index {} out of bounds", idx);
}
assert_eq!(
triangles.len() % 3,
0,
"Number of indices should be divisible by 3"
);
}
#[test]
fn test_triangulate_with_multiple_holes() {
let outer = vec![
Vertex2D::new(0.0, 0.0),
Vertex2D::new(100.0, 0.0),
Vertex2D::new(100.0, 100.0),
Vertex2D::new(0.0, 100.0),
];
let hole1 = vec![
Vertex2D::new(10.0, 10.0),
Vertex2D::new(30.0, 10.0),
Vertex2D::new(30.0, 30.0),
Vertex2D::new(10.0, 30.0),
];
let hole2 = vec![
Vertex2D::new(70.0, 70.0),
Vertex2D::new(90.0, 70.0),
Vertex2D::new(90.0, 90.0),
Vertex2D::new(70.0, 90.0),
];
let triangles = triangulate_with_holes(&outer, &[hole1, hole2])
.expect("Failed to triangulate polygon with two holes");
assert!(!triangles.is_empty(), "Expected at least one triangle");
for &idx in &triangles {
assert!(idx < 12, "Triangle index {} out of bounds", idx);
}
assert_eq!(
triangles.len() % 3,
0,
"Number of indices should be divisible by 3"
);
}
#[test]
fn test_triangulate_invalid_hole() {
let outer = vec![
Vertex2D::new(0.0, 0.0),
Vertex2D::new(100.0, 0.0),
Vertex2D::new(100.0, 100.0),
Vertex2D::new(0.0, 100.0),
];
let invalid_hole = vec![Vertex2D::new(25.0, 25.0), Vertex2D::new(75.0, 25.0)];
let result = triangulate_with_holes(&outer, &[invalid_hole]);
assert!(result.is_err(), "Should fail with invalid hole");
match result {
Err(TriangulationError::InvalidHole(msg)) => {
assert!(msg.contains("Hole 0"));
}
_ => panic!("Expected InvalidHole error"),
}
}
#[test]
fn test_triangulate_empty_outer() {
let outer = vec![];
let result = triangulate_simple(&outer);
assert!(result.is_err(), "Should fail with empty polygon");
}
}