use polyclip::predicates::orient;
use polyclip::*;
use proptest::prelude::*;
use std::collections::HashMap;
fn rings(range: i64, max_rings: usize, max_pts: usize) -> impl Strategy<Value = Vec<Ring>> {
prop::collection::vec(
prop::collection::vec((-range..=range, -range..=range), 0..max_pts)
.prop_map(|v| v.into_iter().map(Point::from).collect::<Ring>()),
0..max_rings,
)
}
fn rects(range: i64, max: usize) -> impl Strategy<Value = Vec<Ring>> {
prop::collection::vec(
(0..range, 0..range, 1..range / 2 + 2, 1..range / 2 + 2)
.prop_map(|(x, y, w, h)| Ring::from([(x, y), (x + w, y), (x + w, y + h), (x, y + h)])),
0..max,
)
}
fn rule() -> impl Strategy<Value = FillRule> {
prop_oneof![
Just(FillRule::EvenOdd),
Just(FillRule::NonZero),
Just(FillRule::Positive),
]
}
fn normalize(rings: &[Ring], rule: FillRule, keep_collinear: bool) -> PolygonSet {
Boolean::new()
.subject(rings, rule)
.keep_collinear(keep_collinear)
.execute()
.unwrap()
}
fn boundary(polys: &[Polygon]) -> Vec<(Point, Point)> {
let mut out = Vec::new();
for poly in polys {
for (ri, r) in poly.rings().enumerate() {
let mut v = r.0.clone();
if (ri == 0) != (r.signed_area2() > 0) {
v.reverse();
}
for i in 0..v.len() {
out.push((v[i], v[(i + 1) % v.len()]));
}
}
}
out
}
fn incircle_i128(a: Point, b: Point, c: Point, d: Point) -> i128 {
let f = |u: Point| ((u.x - d.x) as i128, (u.y - d.y) as i128);
let ((ax, ay), (bx, by), (cx, cy)) = (f(a), f(b), f(c));
(ax * ax + ay * ay) * (bx * cy - by * cx)
+ (bx * bx + by * by) * (cx * ay - cy * ax)
+ (cx * cx + cy * cy) * (ax * by - ay * bx)
}
fn check(
polys: &[Polygon],
t: &Triangulation,
delaunay: bool,
) -> core::result::Result<(), TestCaseError> {
prop_assert!(t.vertices.windows(2).all(|w| w[0] < w[1]));
prop_assert!(t.triangles.windows(2).all(|w| w[0] < w[1]));
let mut half: HashMap<(u32, u32), u32> = HashMap::new();
let mut sum: i128 = 0;
for (i, tri) in t.triangles.iter().enumerate() {
prop_assert!(tri[0] < tri[1] && tri[0] < tri[2]);
let [a, b, c] = t.triangle(i);
let o = orient(a, b, c);
prop_assert!(o > 0, "degenerate or clockwise triangle {:?}", (a, b, c));
sum += o;
for k in 0..3 {
*half.entry((tri[k], tri[(k + 1) % 3])).or_default() += 1;
}
}
let area: i128 = polys.iter().map(|p| p.signed_area2()).sum();
prop_assert_eq!(sum, area, "area");
prop_assert_eq!(t.area2(), area);
let mut verts: Vec<Point> = Vec::new();
let mut bnd: HashMap<(u32, u32), u32> = HashMap::new();
for (a, b) in boundary(polys) {
verts.push(a);
let ia = t.vertices.binary_search(&a);
let ib = t.vertices.binary_search(&b);
prop_assert!(ia.is_ok() && ib.is_ok(), "boundary vertex missing");
*bnd.entry((ia.unwrap() as u32, ib.unwrap() as u32))
.or_default() += 1;
}
verts.sort_unstable();
verts.dedup();
prop_assert_eq!(&verts, &t.vertices, "vertex set");
for (&(a, b), &n) in &bnd {
prop_assert_eq!(n, 1);
prop_assert_eq!(
half.get(&(a, b)),
Some(&1),
"boundary edge not a triangle edge"
);
prop_assert_eq!(half.get(&(b, a)), None, "triangle outside the polygon");
}
for (&(a, b), &n) in &half {
prop_assert_eq!(n, 1);
if !bnd.contains_key(&(a, b)) {
prop_assert_eq!(half.get(&(b, a)), Some(&1), "unmatched interior edge");
}
}
if delaunay {
let mut opp: HashMap<(u32, u32), u32> = HashMap::new();
for tri in &t.triangles {
for k in 0..3 {
opp.insert((tri[k], tri[(k + 1) % 3]), tri[(k + 2) % 3]);
}
}
let v = |i: u32| t.vertices[i as usize];
for tri in &t.triangles {
for k in 0..3 {
let (a, b, c) = (tri[k], tri[(k + 1) % 3], tri[(k + 2) % 3]);
if let Some(&d) = opp.get(&(b, a)) {
prop_assert!(incircle_i128(v(a), v(b), v(c), v(d)) <= 0, "not Delaunay");
}
}
}
}
Ok(())
}
fn check_all(polys: &PolygonSet) -> core::result::Result<(), TestCaseError> {
for poly in polys {
let one = core::slice::from_ref(poly);
let t = triangulate(poly).unwrap();
check(one, &t, false)?;
let d = triangulate_delaunay(poly).unwrap();
let small = poly
.outer
.iter()
.all(|p| p.x.abs() < 1 << 20 && p.y.abs() < 1 << 20);
check(one, &d, small)?;
prop_assert_eq!(&triangulate(poly).unwrap(), &t, "deterministic");
}
let t = triangulate_set(polys).unwrap();
check(polys, &t, false)?;
Ok(())
}
proptest! {
#[test]
fn random_rings(rs in rings(40, 6, 14), rule in rule(), keep in any::<bool>()) {
check_all(&normalize(&rs, rule, keep))?;
}
#[test]
fn random_rings_wide(rs in rings(1_000_000, 5, 20), rule in rule()) {
check_all(&normalize(&rs, rule, false))?;
}
#[test]
fn random_rings_full_range(rs in rings(MAX_COORD, 4, 12), rule in rule()) {
check_all(&normalize(&rs, rule, false))?;
}
#[test]
fn grid_rects(rs in rects(12, 14), rule in rule(), keep in any::<bool>()) {
check_all(&normalize(&rs, rule, keep))?;
}
#[test]
fn boxed_difference(rs in rings(30, 8, 8)) {
let outline = Ring::from([(-30, -30), (30, -30), (30, 30), (-30, 30)]);
let res = boolean(Op::Difference, &outline, &rs, FillRule::NonZero).unwrap();
check_all(&res)?;
}
#[test]
fn boxed_rects(rs in rects(16, 16), rule in rule()) {
let outline = Ring::from([(-1, -1), (30, -1), (30, 30), (-1, 30)]);
let res = boolean(Op::Difference, &outline, &rs, rule).unwrap();
check_all(&res)?;
let res = boolean(Op::Xor, &outline, &rs, rule).unwrap();
check_all(&res)?;
}
#[test]
fn arbitrary_input_never_panics(
rs in prop_oneof![rings(20, 5, 10), rings(MAX_COORD, 4, 8)],
big in any::<bool>(),
) {
let mut rs = rs;
if big && !rs.is_empty() && !rs[0].is_empty() {
rs[0][0] = Point::new(MAX_COORD + 1, 0);
}
let poly = match rs.split_first() {
Some((o, h)) => Polygon::new(o.clone(), h.to_vec()),
None => Polygon::default(),
};
for t in [triangulate(&poly), triangulate_delaunay(&poly)].into_iter().flatten() {
for i in 0..t.triangles.len() {
let [a, b, c] = t.triangle(i);
prop_assert!(orient(a, b, c) > 0);
}
}
let _ = triangulate_set(&[poly.clone(), poly]);
}
}