mod helpers;
use tess2_rust::{ElementType, Tessellator, WindingRule};
fn test_dat_file(data: &str, winding: WindingRule, min_tris: usize, label: &str) {
let contours = helpers::parse_contours(data);
assert!(
!contours.is_empty(),
"{}: should parse at least one contour",
label
);
let mut tess = Tessellator::new();
for contour in &contours {
tess.add_contour(2, contour);
}
let ok = tess.tessellate(winding, ElementType::Polygons, 3, 2, None);
assert!(ok, "{}: tessellation failed", label);
helpers::verify_valid_output(&tess);
let area = helpers::total_tessellation_area(&tess);
assert!(
area > 0.0,
"{}: tessellation should have non-zero area, got {}",
label,
area
);
assert!(
tess.element_count() >= min_tris,
"{}: expected at least {} triangles, got {}",
label,
min_tris,
tess.element_count()
);
}
fn test_dat_files_with_holes(
shape_data: &str,
holes_data: &str,
winding: WindingRule,
min_tris: usize,
label: &str,
) {
let shape_contours = helpers::parse_contours(shape_data);
let hole_contours = helpers::parse_contours(holes_data);
assert!(
!shape_contours.is_empty(),
"{}: should have shape contours",
label
);
let mut tess = Tessellator::new();
for contour in &shape_contours {
tess.add_contour(2, contour);
}
for contour in &hole_contours {
tess.add_contour(2, contour);
}
let ok = tess.tessellate(winding, ElementType::Polygons, 3, 2, None);
assert!(ok, "{}: tessellation failed", label);
helpers::verify_valid_output(&tess);
let area = helpers::total_tessellation_area(&tess);
assert!(area > 0.0, "{}: should have non-zero area", label);
assert!(
tess.element_count() >= min_tris,
"{}: expected at least {} triangles, got {}",
label,
min_tris,
tess.element_count()
);
}
#[test]
fn star_dat() {
test_dat_file(include_str!("data/star.dat"), WindingRule::Odd, 4, "star");
}
#[test]
fn diamond_dat() {
test_dat_file(
include_str!("data/diamond.dat"),
WindingRule::Odd,
4,
"diamond",
);
}
#[test]
fn spiral_dat() {
test_dat_file(
include_str!("data/spiral.dat"),
WindingRule::Odd,
10,
"spiral",
);
}
#[test]
fn test_dat() {
test_dat_file(include_str!("data/test.dat"), WindingRule::Odd, 2, "test");
}
#[test]
fn dude_dat() {
test_dat_file(include_str!("data/dude.dat"), WindingRule::Odd, 50, "dude");
}
#[test]
fn dude_tessellation_area_matches_polygon() {
let data = include_str!("data/dude.dat");
let contours = helpers::parse_contours(data);
assert_eq!(contours.len(), 1, "dude should be a single contour");
let polygon_area = helpers::polygon_signed_area(&contours[0]).abs();
assert!(polygon_area > 0.0, "dude polygon should have non-zero area");
let input_verts = &contours[0];
let mut ixmin = f64::INFINITY;
let mut ixmax = f64::NEG_INFINITY;
let mut iymin = f64::INFINITY;
let mut iymax = f64::NEG_INFINITY;
for i in (0..input_verts.len()).step_by(2) {
ixmin = ixmin.min(input_verts[i]);
ixmax = ixmax.max(input_verts[i]);
iymin = iymin.min(input_verts[i + 1]);
iymax = iymax.max(input_verts[i + 1]);
}
eprintln!(
"Input: {} vertices, bbox=({:.1},{:.1})-({:.1},{:.1}), signed_area={:.1}, abs_area={:.1}",
input_verts.len() / 2,
ixmin, iymin, ixmax, iymax,
helpers::polygon_signed_area(input_verts),
polygon_area
);
let tess = helpers::tessellate_contours(&contours, WindingRule::Odd);
let out_verts = tess.vertices();
let out_elems = tess.elements();
eprintln!(
"Output: {} vertices, {} elements (triangles={})",
tess.vertex_count(),
out_elems.len(),
tess.element_count()
);
let mut oxmin = f64::INFINITY;
let mut oxmax = f64::NEG_INFINITY;
let mut oymin = f64::INFINITY;
let mut oymax = f64::NEG_INFINITY;
for i in (0..out_verts.len()).step_by(2) {
oxmin = oxmin.min(out_verts[i]);
oxmax = oxmax.max(out_verts[i]);
oymin = oymin.min(out_verts[i + 1]);
oymax = oymax.max(out_verts[i + 1]);
}
eprintln!(
"Output bbox=({:.1},{:.1})-({:.1},{:.1})",
oxmin, oymin, oxmax, oymax
);
let tess_area = helpers::total_tessellation_area(&tess);
let mut outside_count = 0;
let mut outside_area = 0.0f64;
for tri in out_elems.chunks(3) {
if tri.len() < 3 { break; }
let (i0, i1, i2) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
let (x0, y0) = (out_verts[i0*2], out_verts[i0*2+1]);
let (x1, y1) = (out_verts[i1*2], out_verts[i1*2+1]);
let (x2, y2) = (out_verts[i2*2], out_verts[i2*2+1]);
let cx = (x0 + x1 + x2) / 3.0;
let cy = (y0 + y1 + y2) / 3.0;
let area = helpers::triangle_area(x0, y0, x1, y1, x2, y2).abs();
if cx < ixmin || cx > ixmax || cy < iymin || cy > iymax {
outside_count += 1;
outside_area += area;
if outside_count <= 5 {
eprintln!(
" Outside tri: centroid=({:.1},{:.1}), area={:.1}, verts=({:.1},{:.1}),({:.1},{:.1}),({:.1},{:.1})",
cx, cy, area, x0, y0, x1, y1, x2, y2
);
}
}
}
let tess_signed_area = helpers::total_tessellation_signed_area(&tess);
eprintln!(
"Tessellation abs_area={:.1}, signed_area={:.1}, polygon area={:.1}, ratio={:.4}",
tess_area, tess_signed_area, polygon_area, tess_area / polygon_area
);
eprintln!(
"{} triangles with centroid outside bbox, total outside area={:.1}",
outside_count, outside_area
);
let mut pos_count = 0;
let mut neg_count = 0;
let mut pos_area = 0.0f64;
let mut neg_area = 0.0f64;
for tri in out_elems.chunks(3) {
if tri.len() < 3 { break; }
let (i0, i1, i2) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
let a = helpers::triangle_area(
out_verts[i0*2], out_verts[i0*2+1],
out_verts[i1*2], out_verts[i1*2+1],
out_verts[i2*2], out_verts[i2*2+1],
);
if a >= 0.0 {
pos_count += 1;
pos_area += a;
} else {
neg_count += 1;
neg_area += a;
}
}
eprintln!(
"Positive triangles: {} (area={:.1}), Negative triangles: {} (area={:.1})",
pos_count, pos_area, neg_count, neg_area
);
let ratio = tess_area / polygon_area;
assert!(
(0.95..=1.05).contains(&ratio),
"dude: tessellation area ({:.1}) should match polygon area ({:.1}), ratio={:.4}",
tess_area,
polygon_area,
ratio,
);
}
#[test]
fn bird_dat() {
let data = include_str!("data/bird.dat");
let contours = helpers::parse_contours(data);
let mut tess = Tessellator::new();
for contour in &contours {
tess.add_contour(2, contour);
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tess.tessellate(WindingRule::Odd, ElementType::Polygons, 3, 2, None)
}));
match result {
Ok(true) => {
helpers::verify_valid_output(&tess);
assert!(tess.element_count() >= 100);
}
Ok(false) => {
}
Err(_) => {
eprintln!("WARNING: bird polygon triggered a panic in tessellator");
}
}
}
#[test]
fn tank_dat() {
test_dat_file(include_str!("data/tank.dat"), WindingRule::Odd, 30, "tank");
}
#[test]
fn funny_dat() {
test_dat_file(
include_str!("data/funny.dat"),
WindingRule::Odd,
50,
"funny",
);
}
#[test]
fn kzer_za_dat() {
test_dat_file(
include_str!("data/kzer_za.dat"),
WindingRule::Odd,
100,
"kzer-za",
);
}
#[test]
fn debug_butterfly_dat() {
test_dat_file(
include_str!("data/debug.dat"),
WindingRule::Odd,
50,
"debug/butterfly",
);
}
#[test]
#[ignore] fn dude_with_holes_dat() {
let shape_contours = helpers::parse_contours(include_str!("data/dude.dat"));
let hole_contours = helpers::parse_contours(include_str!("data/dude_holes.dat"));
let mut tess = Tessellator::new();
for contour in &shape_contours {
tess.add_contour(2, contour);
}
for contour in &hole_contours {
tess.add_contour(2, contour);
}
let ok = tess.tessellate(WindingRule::Odd, ElementType::Polygons, 3, 2, None);
assert!(ok, "dude+holes tessellation failed");
helpers::verify_valid_output(&tess);
}
#[test]
fn glu_example_dat() {
test_dat_file(
include_str!("data/glu_example.dat"),
WindingRule::Odd,
1,
"glu_example",
);
}
#[test]
fn dude_all_winding_rules() {
let data = include_str!("data/dude.dat");
for &rule in &[
WindingRule::Odd,
WindingRule::NonZero,
WindingRule::Positive,
WindingRule::Negative,
WindingRule::AbsGeqTwo,
] {
let contours = helpers::parse_contours(data);
let mut tess = Tessellator::new();
for contour in &contours {
tess.add_contour(2, contour);
}
let ok = tess.tessellate(rule, ElementType::Polygons, 3, 2, None);
assert!(ok, "dude tessellation failed for {:?}", rule);
helpers::verify_valid_output(&tess);
}
}
#[test]
fn bird_all_winding_rules() {
let data = include_str!("data/bird.dat");
for &rule in &[
WindingRule::Odd,
WindingRule::NonZero,
WindingRule::Positive,
] {
let contours = helpers::parse_contours(data);
let mut tess = Tessellator::new();
for contour in &contours {
tess.add_contour(2, contour);
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tess.tessellate(rule, ElementType::Polygons, 3, 2, None)
}));
match result {
Ok(true) => helpers::verify_valid_output(&tess),
Ok(false) => {} Err(_) => eprintln!("WARNING: bird polygon panicked with {:?}", rule),
}
}
}
#[test]
fn glu_winding_all_rules() {
let data = include_str!("data/glu_winding.dat");
let contours = helpers::parse_contours(data);
for &rule in &[
WindingRule::Odd,
WindingRule::NonZero,
WindingRule::Positive,
WindingRule::Negative,
WindingRule::AbsGeqTwo,
] {
let mut tess = Tessellator::new();
for contour in &contours {
tess.add_contour(2, contour);
}
let ok = tess.tessellate(rule, ElementType::Polygons, 3, 2, None);
assert!(ok, "glu_winding tessellation failed for {:?}", rule);
helpers::verify_valid_output(&tess);
}
}