use std::fs;
use std::path::{Path, PathBuf};
use polyops::{difference, intersection, union, xor, Geometry, MultiPolygon};
use serde::Deserialize;
#[derive(Deserialize)]
struct Golden {
subject: serde_json::Value,
clipping: serde_json::Value,
expected: Option<MultiPolygon>,
}
fn coords_to_geometry(value: serde_json::Value) -> Geometry {
if depth(&value) >= 4 {
let mp: MultiPolygon = serde_json::from_value(value).expect("multipolygon coords");
Geometry::MultiPolygon(mp)
} else {
let p: Vec<Vec<[f64; 2]>> = serde_json::from_value(value).expect("polygon coords");
Geometry::Polygon(p)
}
}
fn depth(value: &serde_json::Value) -> usize {
let mut d = 0;
let mut cur = value;
while let serde_json::Value::Array(arr) = cur {
d += 1;
match arr.first() {
Some(next) => cur = next,
None => break,
}
}
d
}
fn goldens_dir(operation: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("goldens")
.join(operation)
}
fn multipolygon_close(actual: &MultiPolygon, expected: &MultiPolygon, eps: f64) -> bool {
if actual.len() != expected.len() {
return false;
}
actual
.iter()
.zip(expected.iter())
.all(|(pa, pe)| polygon_close(pa, pe, eps))
}
fn polygon_close(a: &[Vec<[f64; 2]>], b: &[Vec<[f64; 2]>], eps: f64) -> bool {
if a.len() != b.len() {
return false;
}
a.iter()
.zip(b.iter())
.all(|(ra, rb)| ring_close(ra, rb, eps))
}
fn ring_close(a: &[[f64; 2]], b: &[[f64; 2]], eps: f64) -> bool {
if a.len() != b.len() {
return false;
}
a.iter()
.zip(b.iter())
.all(|(pa, pb)| (pa[0] - pb[0]).abs() <= eps && (pa[1] - pb[1]).abs() <= eps)
}
fn run_parity(operation: &str, op: fn(Geometry, Geometry) -> Option<MultiPolygon>) {
const EPS: f64 = 1e-10;
let dir = goldens_dir(operation);
assert!(
dir.exists(),
"goldens for {operation} not found at {dir:?} — run `cd parity && npm install && npm run generate`",
);
let mut failures: Vec<String> = Vec::new();
for entry in fs::read_dir(&dir).expect("read goldens dir") {
let entry = entry.expect("dir entry");
if entry.path().extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let name = entry
.path()
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("<unknown>")
.to_owned();
let raw = fs::read_to_string(entry.path()).expect("read golden");
let golden: Golden = serde_json::from_str(&raw).expect("parse golden");
let subject = coords_to_geometry(golden.subject);
let clipping = coords_to_geometry(golden.clipping);
let actual = op(subject, clipping);
match (actual, golden.expected) {
(None, None) => {}
(Some(a), Some(e)) if multipolygon_close(&a, &e, EPS) => {}
(a, e) => {
failures.push(format!("{name}: got {a:?}, expected {e:?}"));
}
}
}
assert!(
failures.is_empty(),
"{} {operation} parity failure(s):\n{}",
failures.len(),
failures.join("\n"),
);
}
#[test]
fn parity_intersection() {
run_parity("intersection", intersection);
}
#[test]
fn parity_union() {
run_parity("union", union);
}
#[test]
fn parity_difference() {
run_parity("difference", difference);
}
#[test]
fn parity_xor() {
run_parity("xor", xor);
}