use crate::{Error, Point2, Point3, Result, Vector3};
#[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 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(vec![0, 1, 2, 0, 2, 3]);
}
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 = earcutr::earcut(&vertices, &[], 2)
.map_err(|e| Error::TriangulationError(format!("{:?}", e)))?;
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 = earcutr::earcut(&vertices, &hole_indices, 2)
.map_err(|e| Error::TriangulationError(format!("{:?}", e)))?;
Ok(indices)
}
#[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)]
mod tests {
use super::*;
#[test]
fn test_triangulate_square() {
let points = vec![
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(0.0, 1.0),
];
let indices = triangulate_polygon(&points).unwrap();
assert_eq!(indices.len(), 6);
}
#[test]
fn test_triangulate_triangle() {
let points = vec![
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(0.5, 1.0),
];
let indices = triangulate_polygon(&points).unwrap();
assert_eq!(indices.len(), 3);
}
#[test]
fn test_triangulate_insufficient_points() {
let points = vec![Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)];
let result = triangulate_polygon(&points);
assert!(result.is_err());
}
#[test]
fn test_triangulate_square_with_hole() {
let outer = vec![
Point2::new(0.0, 0.0),
Point2::new(10.0, 0.0),
Point2::new(10.0, 10.0),
Point2::new(0.0, 10.0),
];
let hole = vec![
Point2::new(3.0, 3.0),
Point2::new(7.0, 3.0),
Point2::new(7.0, 7.0),
Point2::new(3.0, 7.0),
];
let indices = triangulate_polygon_with_holes(&outer, &[hole]).unwrap();
assert!(indices.len() > 6); assert_eq!(indices.len() % 3, 0); }
#[test]
fn test_triangulate_with_multiple_holes() {
let outer = vec![
Point2::new(0.0, 0.0),
Point2::new(20.0, 0.0),
Point2::new(20.0, 20.0),
Point2::new(0.0, 20.0),
];
let hole1 = vec![
Point2::new(2.0, 2.0),
Point2::new(5.0, 2.0),
Point2::new(5.0, 5.0),
Point2::new(2.0, 5.0),
];
let hole2 = vec![
Point2::new(10.0, 10.0),
Point2::new(15.0, 10.0),
Point2::new(15.0, 15.0),
Point2::new(10.0, 15.0),
];
let indices = triangulate_polygon_with_holes(&outer, &[hole1, hole2]).unwrap();
assert!(indices.len() > 6);
assert_eq!(indices.len() % 3, 0);
}
#[test]
fn test_calculate_polygon_normal() {
let points = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
let normal = calculate_polygon_normal(&points);
assert!((normal.z.abs() - 1.0).abs() < 0.001);
}
#[test]
fn test_project_to_2d() {
let points = vec![
Point3::new(0.0, 0.0, 5.0),
Point3::new(1.0, 0.0, 5.0),
Point3::new(1.0, 1.0, 5.0),
Point3::new(0.0, 1.0, 5.0),
];
let normal = Vector3::new(0.0, 0.0, 1.0);
let (projected, _, _, _) = project_to_2d(&points, &normal);
assert_eq!(projected.len(), 4);
}
}