use std::collections::HashMap;
use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::single::SingleFloatOverlay;
use proptest::prelude::*;
use u_nesting_d2::{Boundary2D, Config, Geometry2D, Nester2D, SolveResult, Solver, Strategy};
fn ring_area(ring: &[(f64, f64)]) -> f64 {
let n = ring.len();
if n < 3 {
return 0.0;
}
let mut acc = 0.0;
for i in 0..n {
let (x0, y0) = ring[i];
let (x1, y1) = ring[(i + 1) % n];
acc += x0 * y1 - x1 * y0;
}
(acc * 0.5).abs()
}
fn intersection_area(a: &[(f64, f64)], b: &[(f64, f64)]) -> f64 {
let subj: Vec<[f64; 2]> = a.iter().map(|&(x, y)| [x, y]).collect();
let clip: Vec<[f64; 2]> = b.iter().map(|&(x, y)| [x, y]).collect();
let shapes = subj.overlay(&[clip], OverlayRule::Intersect, FillRule::NonZero);
let mut area = 0.0;
for shape in &shapes {
if let Some(outer) = shape.first() {
let ring: Vec<(f64, f64)> = outer.iter().map(|&[x, y]| (x, y)).collect();
area += ring_area(&ring);
}
}
area
}
fn star_ring(radii: &[f64]) -> Vec<(f64, f64)> {
let n = radii.len();
(0..n)
.map(|i| {
let ang = std::f64::consts::TAU * (i as f64) / (n as f64);
(radii[i] * ang.cos(), radii[i] * ang.sin())
})
.collect()
}
fn assert_no_overlap(pieces: &[(String, Geometry2D)], result: &SolveResult<f64>, eps: f64) {
let map: HashMap<&str, &Geometry2D> = pieces.iter().map(|(id, g)| (id.as_str(), g)).collect();
let placed: Vec<Vec<(f64, f64)>> = result
.placements
.iter()
.map(|p| {
let g = map[p.geometry_id.as_str()];
let rot = p.rotation.first().copied().unwrap_or(0.0);
g.transformed_exterior(p.position[0], p.position[1], rot)
})
.collect();
for i in 0..placed.len() {
for j in (i + 1)..placed.len() {
let overlap = intersection_area(&placed[i], &placed[j]);
assert!(
overlap < eps,
"placements {i} and {j} overlap by area {overlap} (> {eps})"
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn solve_never_panics_on_arbitrary_polygon(
verts in prop::collection::vec((-500.0f64..500.0, -500.0f64..500.0), 3..10),
strat in prop::sample::select(vec![Strategy::BottomLeftFill, Strategy::NfpGuided]),
spacing in 0.0f64..20.0,
margin in 0.0f64..10.0,
) {
let g = Geometry2D::new("p").with_polygon(verts).with_quantity(3);
let boundary = Boundary2D::rectangle(1000.0, 1000.0);
let config = Config::default()
.with_strategy(strat)
.with_spacing(spacing)
.with_margin(margin);
let _ = Nester2D::new(config).solve(&[g], &boundary);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(40))]
#[test]
fn placement_has_no_overlap_convex(
dims in prop::collection::vec((10.0f64..200.0, 10.0f64..200.0), 2..6),
strat in prop::sample::select(vec![Strategy::BottomLeftFill, Strategy::NfpGuided]),
) {
let pieces: Vec<(String, Geometry2D)> = dims
.iter()
.enumerate()
.map(|(i, &(w, h))| {
let id = format!("r{i}");
(id.clone(), Geometry2D::rectangle(id, w, h).with_quantity(2))
})
.collect();
let geoms: Vec<Geometry2D> = pieces.iter().map(|(_, g)| g.clone()).collect();
let boundary = Boundary2D::rectangle(1000.0, 5000.0);
let result = Nester2D::new(Config::default().with_strategy(strat).with_spacing(2.0))
.solve(&geoms, &boundary)
.expect("convex rectangles are always valid input");
assert_no_overlap(&pieces, &result, 1e-6);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(40))]
#[test]
fn placement_has_no_overlap_concave(
stars in prop::collection::vec(
prop::collection::vec(20.0f64..120.0, 5..9),
2..5,
),
strat in prop::sample::select(vec![Strategy::BottomLeftFill, Strategy::NfpGuided]),
) {
let pieces: Vec<(String, Geometry2D)> = stars
.iter()
.enumerate()
.map(|(i, radii)| {
let id = format!("s{i}");
let ring = star_ring(radii);
(id.clone(), Geometry2D::new(id).with_polygon(ring).with_quantity(2))
})
.collect();
let geoms: Vec<Geometry2D> = pieces.iter().map(|(_, g)| g.clone()).collect();
let boundary = Boundary2D::rectangle(1000.0, 5000.0);
if let Ok(result) = Nester2D::new(Config::default().with_strategy(strat).with_spacing(2.0))
.solve(&geoms, &boundary)
{
assert_no_overlap(&pieces, &result, 1e-2);
}
}
}