use std::collections::{BTreeMap, BTreeSet};
use tess2_rust::{ElementType, Tessellator, WindingRule};
type CoordBits = (u64, u64);
fn vertex_multiset(flat: &[f64]) -> BTreeMap<CoordBits, usize> {
let mut m = BTreeMap::new();
for v in flat.chunks_exact(2) {
*m.entry((v[0].to_bits(), v[1].to_bits())).or_insert(0) += 1;
}
m
}
fn norm_triangle(a: CoordBits, b: CoordBits, c: CoordBits) -> [CoordBits; 3] {
let mut t = [a, b, c];
t.sort();
t
}
fn triangle_multiset(verts: &[f64], indices: &[u32]) -> BTreeSet<[CoordBits; 3]> {
let mut s = BTreeSet::new();
for tri in indices.chunks_exact(3) {
let i0 = tri[0] as usize;
let i1 = tri[1] as usize;
let i2 = tri[2] as usize;
let a = (verts[i0 * 2].to_bits(), verts[i0 * 2 + 1].to_bits());
let b = (verts[i1 * 2].to_bits(), verts[i1 * 2 + 1].to_bits());
let c = (verts[i2 * 2].to_bits(), verts[i2 * 2 + 1].to_bits());
s.insert(norm_triangle(a, b, c));
}
s
}
fn triangle_multiset_from_ref(r: &RefPolygon) -> BTreeSet<[CoordBits; 3]> {
let mut s = BTreeSet::new();
let vb: Vec<CoordBits> = r.vertices.iter()
.map(|&(x, y)| (x.to_bits(), y.to_bits()))
.collect();
for tri in r.tri_vertices.chunks_exact(3) {
let a = vb[tri[0].0 as usize];
let b = vb[tri[1].0 as usize];
let c = vb[tri[2].0 as usize];
s.insert(norm_triangle(a, b, c));
}
s
}
fn norm_edge(a: CoordBits, b: CoordBits) -> (CoordBits, CoordBits) {
if a <= b { (a, b) } else { (b, a) }
}
fn boundary_edge_multiset(verts: &[f64], indices: &[u32], flags: &[u8])
-> BTreeMap<(CoordBits, CoordBits), usize>
{
let mut m = BTreeMap::new();
for (t, tri) in indices.chunks_exact(3).enumerate() {
let pts: [CoordBits; 3] = [
(verts[tri[0] as usize * 2].to_bits(), verts[tri[0] as usize * 2 + 1].to_bits()),
(verts[tri[1] as usize * 2].to_bits(), verts[tri[1] as usize * 2 + 1].to_bits()),
(verts[tri[2] as usize * 2].to_bits(), verts[tri[2] as usize * 2 + 1].to_bits()),
];
for k in 0..3 {
if flags.get(t * 3 + k).copied().unwrap_or(0) == 1 {
let e = norm_edge(pts[k], pts[(k + 1) % 3]);
*m.entry(e).or_insert(0) += 1;
}
}
}
m
}
fn boundary_edge_multiset_ref(r: &RefPolygon) -> BTreeMap<(CoordBits, CoordBits), usize> {
let mut m = BTreeMap::new();
let vb: Vec<CoordBits> = r.vertices.iter()
.map(|&(x, y)| (x.to_bits(), y.to_bits()))
.collect();
for tri in r.tri_vertices.chunks_exact(3) {
let pts = [vb[tri[0].0 as usize], vb[tri[1].0 as usize], vb[tri[2].0 as usize]];
let flags = [tri[0].1, tri[1].1, tri[2].1];
for k in 0..3 {
if flags[k] == 1 {
let e = norm_edge(pts[k], pts[(k + 1) % 3]);
*m.entry(e).or_insert(0) += 1;
}
}
}
m
}
#[derive(Debug)]
struct RefPolygon {
index: usize,
input_count: usize,
vertices: Vec<(f64, f64)>, tri_vertices: Vec<(u32, u8)>, }
fn parse_reference(path: &str) -> Vec<RefPolygon> {
let content = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("read {path}: {e}"));
let mut polys: Vec<RefPolygon> = Vec::new();
let mut cur: Option<RefPolygon> = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') { continue; }
if let Some(rest) = line.strip_prefix("POLY ") {
if let Some(p) = cur.take() { polys.push(p); }
let mut it = rest.split_whitespace();
let idx: usize = it.next().unwrap().parse().unwrap();
let count: usize = it.next().unwrap().parse().unwrap();
cur = Some(RefPolygon {
index: idx,
input_count: count,
vertices: Vec::new(),
tri_vertices: Vec::new(),
});
continue;
}
let p = cur.as_mut().expect("V/I before POLY");
if let Some(rest) = line.strip_prefix("V ") {
let mut it = rest.split_whitespace();
let _idx: usize = it.next().unwrap().parse().unwrap();
let x: f64 = it.next().unwrap().parse().unwrap();
let y: f64 = it.next().unwrap().parse().unwrap();
p.vertices.push((x, y));
} else if let Some(rest) = line.strip_prefix("I ") {
let mut it = rest.split_whitespace();
let v: u32 = it.next().unwrap().parse().unwrap();
let f: u8 = it.next().unwrap().parse().unwrap();
p.tri_vertices.push((v, f));
} else {
panic!("unrecognised reference line: {line}");
}
}
if let Some(p) = cur { polys.push(p); }
polys
}
fn parse_lion_polygons() -> Vec<Vec<(f64, f64)>> {
const DATA: &str = include_str!("data/lion.txt");
let mut out: Vec<Vec<(f64, f64)>> = Vec::new();
for raw in DATA.lines() {
let line = raw.trim();
if line.is_empty() { continue; }
if line.len() == 6 && line.chars().all(|c| c.is_ascii_hexdigit()) { continue; }
if !line.starts_with('M') { continue; }
let mut verts: Vec<(f64, f64)> = Vec::new();
for tok in line.split_whitespace() {
if tok == "M" || tok == "L" { continue; }
let mut sp = tok.split(',');
let x: Option<f64> = sp.next().and_then(|s| s.parse().ok());
let y: Option<f64> = sp.next().and_then(|s| s.parse().ok());
if let (Some(x), Some(y)) = (x, y) {
verts.push((x, y));
}
}
while verts.len() >= 2 && verts[verts.len() - 1] == verts[verts.len() - 2] {
verts.pop();
}
if verts.len() >= 2 && verts[verts.len() - 1] == verts[0] {
verts.pop();
}
if verts.len() >= 3 { out.push(verts); }
}
out
}
#[test]
fn tess2_rust_matches_csharp_tesselator_on_lion() {
let refs = parse_reference("tests/data/lion_tess_reference.txt");
let inputs = parse_lion_polygons();
assert!(refs.len() > 0, "reference file must contain at least one polygon");
let mut refs_by_index: std::collections::HashMap<usize, &RefPolygon> =
refs.iter().map(|r| (r.index, r)).collect();
let mut mismatches: Vec<String> = Vec::new();
for (i, input) in inputs.iter().enumerate() {
let reference = match refs_by_index.remove(&i) {
Some(r) => r,
None => continue, };
let mut tess = Tessellator::new();
let flat: Vec<f64> = input.iter().flat_map(|&(x, y)| [x, y]).collect();
tess.add_contour(2, &flat);
let ok = tess.tessellate(WindingRule::Odd, ElementType::Polygons, 3, 2, None);
assert!(ok, "polygon #{i}: tess2-rust returned false");
let rust_verts = tess.vertices();
let rust_idx = tess.elements();
let rust_flags = tess.edge_flags();
let rust_v_set = vertex_multiset(rust_verts);
let csharp_v_set: std::collections::BTreeMap<(u64, u64), usize> =
reference.vertices.iter().fold(std::collections::BTreeMap::new(), |mut acc, &(x, y)| {
*acc.entry((x.to_bits(), y.to_bits())).or_insert(0) += 1;
acc
});
if rust_v_set != csharp_v_set {
mismatches.push(format!(
"polygon #{i}: vertex multiset differs — rust {} verts, csharp {}",
rust_verts.len() / 2, reference.vertices.len()
));
}
let rust_tris = triangle_multiset(rust_verts, rust_idx);
let csharp_tris = triangle_multiset_from_ref(reference);
if rust_tris != csharp_tris {
let rust_extra: Vec<_> = rust_tris.difference(&csharp_tris).take(3).collect();
let csharp_extra: Vec<_> = csharp_tris.difference(&rust_tris).take(3).collect();
mismatches.push(format!(
"polygon #{i}: triangle multiset differs — rust has {} tris, csharp has {} tris. \
Rust-only examples: {:?}. C#-only examples: {:?}",
rust_tris.len(), csharp_tris.len(), rust_extra, csharp_extra,
));
}
let rust_boundary = boundary_edge_multiset(rust_verts, rust_idx, rust_flags);
let csharp_boundary = boundary_edge_multiset_ref(reference);
if rust_boundary != csharp_boundary {
mismatches.push(format!(
"polygon #{i}: boundary-edge multiset differs — rust {} edges, csharp {}",
rust_boundary.len(), csharp_boundary.len(),
));
}
}
let known_different: std::collections::BTreeSet<usize> =
[].iter().copied().collect();
let surprising: Vec<&String> = mismatches.iter()
.filter(|m| {
!known_different.iter().any(|i| m.contains(&format!("polygon #{i}:")))
})
.collect();
if !surprising.is_empty() {
for m in surprising.iter().take(40) { eprintln!("{m}"); }
panic!(
"{} unexpected conformance mismatches (showing first {})",
surprising.len(),
40.min(surprising.len()),
);
}
}