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);
}
fn tri_area2(p: &[Point2<f64>], i: &[usize]) -> f64 {
let (a, b, c) = (p[i[0]], p[i[1]], p[i[2]]);
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
}
fn ring_area2(p: &[Point2<f64>]) -> f64 {
(0..p.len())
.map(|i| {
let (a, b) = (p[i], p[(i + 1) % p.len()]);
a.x * b.y - b.x * a.y
})
.sum()
}
#[test]
fn concave_quad_is_split_across_an_interior_diagonal() {
let dart = vec![
Point2::new(0.0, 0.0),
Point2::new(2.0, 2.0),
Point2::new(4.0, 0.0),
Point2::new(2.0, 10.0),
];
let ring = ring_area2(&dart);
assert_eq!(ring, 32.0, "fixture precondition: the ring is CCW, 2*A = 32");
assert_eq!(tri_area2(&dart, &[0, 1, 2]), -8.0);
assert_eq!(tri_area2(&dart, &[0, 2, 3]), 40.0);
let idx = triangulate_polygon(&dart).unwrap();
assert_eq!(idx.len(), 6, "a quad yields exactly two triangles");
let areas: Vec<f64> = idx.chunks_exact(3).map(|t| tri_area2(&dart, t)).collect();
for a in &areas {
assert!(
a.signum() == ring.signum() && *a != 0.0,
"triangle 2*area {a} must carry the ring's winding {ring}, got {areas:?}"
);
}
let total: f64 = areas.iter().sum();
assert_eq!(
total, ring,
"the two triangles must tile the ring exactly, no double cover"
);
}
#[test]
fn concave_quad_split_is_correct_for_a_clockwise_ring() {
let dart = vec![
Point2::new(4.0, 0.0),
Point2::new(2.0, 2.0),
Point2::new(0.0, 0.0),
Point2::new(2.0, 10.0),
];
let ring = ring_area2(&dart);
assert_eq!(ring, -32.0, "fixture precondition: this ring is CW");
assert_eq!(tri_area2(&dart, &[0, 1, 2]), 8.0, "the old 0-2 split is backwards here");
let idx = triangulate_polygon(&dart).unwrap();
let areas: Vec<f64> = idx.chunks_exact(3).map(|t| tri_area2(&dart, t)).collect();
for a in &areas {
assert!(
*a < 0.0,
"triangle 2*area {a} must stay clockwise like the ring, got {areas:?}"
);
}
assert_eq!(areas.iter().sum::<f64>(), ring);
}
#[test]
fn convex_quad_still_uses_the_zero_two_diagonal() {
let square = 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),
];
assert_eq!(triangulate_polygon(&square).unwrap(), vec![0, 1, 2, 0, 2, 3]);
let flat = vec![
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(2.0, 0.0),
Point2::new(1.0, 1.0),
];
assert_eq!(triangulate_polygon(&flat).unwrap(), vec![0, 1, 2, 0, 2, 3]);
}
#[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_refined_vertex_layout_and_hole_constraints() {
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 (pts, idx) =
triangulate_polygon_with_holes_refined(&outer, std::slice::from_ref(&hole)).unwrap();
let n_input = outer.len() + hole.len();
assert!(pts.len() >= n_input, "input vertices must all be present");
for (i, p) in outer.iter().chain(hole.iter()).enumerate() {
assert_eq!(
(pts[i].x, pts[i].y),
(p.x, p.y),
"vertex {i} must be the input vertex (outer ++ holes order)"
);
}
assert!(!idx.is_empty());
assert_eq!(idx.len() % 3, 0);
assert!(idx.iter().all(|&i| i < pts.len()), "index out of range");
let mut edges = std::collections::BTreeSet::new();
for t in idx.chunks_exact(3) {
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
edges.insert(if a < b { (a, b) } else { (b, a) });
}
}
for k in 0..hole.len() {
let a = outer.len() + k;
let b = outer.len() + (k + 1) % hole.len();
let key = if a < b { (a, b) } else { (b, a) };
assert!(
edges.contains(&key),
"hole-ring constraint edge {key:?} missing from the triangulation"
);
}
}
#[test]
fn test_refined_collinear_outer_falls_back() {
let outer = vec![
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(2.0, 0.0),
Point2::new(3.0, 0.0),
];
let (pts, idx) = triangulate_polygon_with_holes_refined(&outer, &[])
.expect("degenerate input must fall back, not error");
assert_eq!(pts.len(), outer.len(), "fallback must return the input vertex set");
for (i, p) in outer.iter().enumerate() {
assert_eq!((pts[i].x, pts[i].y), (p.x, p.y));
}
assert_eq!(idx.len() % 3, 0);
assert!(idx.iter().all(|&i| i < pts.len()));
}
#[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);
}
#[test]
fn safe_earcut_terminates_on_outside_voids_and_renders_them() {
let data = vec![
0.0, -0.0, 0.0, 83.0, -2325.0, 83.0, -2325.0, -0.0, -2620.0, 83.0, -2620.0, -0.0, -2375.0, -0.0, -2375.0, 83.0, -2326.0, 83.0, -2374.0, 83.0, -2374.0, -0.0, -2326.0, -0.0, ];
let holes = vec![4, 8];
let indices = safe_earcut(&data, &holes, 2).expect("must triangulate");
assert_eq!(indices.len(), 18, "3 rects × 2 tris × 3 idx");
for tri in indices.chunks_exact(3) {
let ring = |v: usize| {
if v < 4 {
0
} else if v < 8 {
1
} else {
2
}
};
assert_eq!(ring(tri[0]), ring(tri[1]));
assert_eq!(ring(tri[1]), ring(tri[2]));
}
}
#[test]
fn safe_earcut_keeps_contained_holes() {
let data = vec![
0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0, 4.0, 4.0, 4.0, 6.0, 6.0, 6.0, 6.0, 4.0, ];
let indices = safe_earcut(&data, &[4], 2).expect("must triangulate");
assert_eq!(indices.len() / 3, 8);
assert!(indices.iter().any(|&i| i >= 4));
}
#[test]
fn safe_earcut_drops_duplicate_vertices_and_remaps() {
let data = vec![
0.0, 0.0, 10.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0, 0.0, 0.0, ];
let indices = safe_earcut(&data, &[], 2).expect("must triangulate");
assert_eq!(indices.len() / 3, 2, "a quad → 2 triangles");
assert!(indices.iter().all(|&i| i != 2 && i != 5 && i < 6));
}
#[test]
fn safe_earcut_rejects_non_finite() {
let data = vec![0.0, 0.0, 10.0, f64::NAN, 10.0, 10.0];
assert!(safe_earcut(&data, &[], 2).is_err());
}