use std::collections::{HashMap, HashSet};
use crate::boundary::Boundary2D;
use crate::geometry::Geometry2D;
use crate::polygon_ops::{mirror_polygon, ring_distance};
use u_nesting_core::geometry::{Boundary, Geometry};
use u_nesting_core::Placement;
#[derive(Debug, Clone, PartialEq)]
pub enum LayoutViolation {
UnknownGeometry {
placement: usize,
id: String,
},
InstanceOutOfRange {
placement: usize,
id: String,
instance: usize,
quantity: usize,
},
InstanceRepeated {
placement: usize,
id: String,
instance: usize,
},
RotationNotAllowed {
placement: usize,
id: String,
rotation: f64,
},
FlipNotAllowed {
placement: usize,
id: String,
},
SheetNotAllowed {
placement: usize,
sheet: usize,
},
OutsideBoundary {
placement: usize,
id: String,
},
Overlap {
placement: usize,
other: usize,
},
TooClose {
placement: usize,
other: usize,
distance: f64,
spacing: f64,
},
MissingFromUnplaced {
id: String,
},
WronglyUnplaced {
id: String,
},
}
#[cfg(feature = "serde")]
pub fn check_2d(
request: u_nesting_core::api_types::Check2DRequest,
) -> u_nesting_core::api_types::CheckResponse {
use u_nesting_core::api_types::{CheckResponse, ConfigRequest, LayoutViolationResponse as R};
use u_nesting_core::solver::Config;
use u_nesting_core::Error;
let solved = request.solve_result;
if !solved.success {
return CheckResponse::refused(&Error::invalid_option(
"solve_result",
"the solve result is a refusal (success: false); give a successful one to check"
.to_string(),
));
}
let problem = request.problem;
let read = || -> u_nesting_core::Result<(Vec<Geometry2D>, Boundary2D, LayoutRules)> {
let geometries = Geometry2D::from_requests(problem.geometries)?;
let boundary = Boundary2D::from_request(problem.boundary)?;
let multi_sheet = problem
.config
.as_ref()
.and_then(|c| c.multi_sheet)
.unwrap_or(false);
let config = problem
.config
.as_ref()
.map_or_else(|| Ok(Config::default()), ConfigRequest::to_config)?;
crate::nester::Nester2D::new(config.clone()).validate(&geometries, &boundary)?;
Ok((
geometries,
boundary,
LayoutRules {
spacing: config.spacing,
margin: config.margin,
multi_sheet,
},
))
};
let (geometries, boundary, rules) = match read() {
Ok(read) => read,
Err(e) => return CheckResponse::refused(&e),
};
let placements: Vec<Placement<f64>> = solved
.placements
.iter()
.map(|p| Placement {
geometry_id: p.geometry_id.clone(),
instance: p.instance,
position: vec![p.x, p.y],
rotation: vec![p.rotation.to_radians()],
boundary_index: p.sheet_index,
mirrored: p.flipped,
rotation_index: None,
})
.collect();
let violations = check_layout(
&geometries,
&boundary,
&rules,
&placements,
&solved.unplaced,
)
.into_iter()
.map(|v| match v {
LayoutViolation::UnknownGeometry { placement, id } => R::UnknownGeometry { placement, id },
LayoutViolation::InstanceOutOfRange {
placement,
id,
instance,
quantity,
} => R::InstanceOutOfRange {
placement,
id,
instance,
quantity,
},
LayoutViolation::InstanceRepeated {
placement,
id,
instance,
} => R::InstanceRepeated {
placement,
id,
instance,
},
LayoutViolation::RotationNotAllowed {
placement,
id,
rotation,
} => R::RotationNotAllowed {
placement,
id,
rotation: rotation.to_degrees(),
},
LayoutViolation::FlipNotAllowed { placement, id } => R::FlipNotAllowed { placement, id },
LayoutViolation::SheetNotAllowed { placement, sheet } => {
R::SheetNotAllowed { placement, sheet }
}
LayoutViolation::OutsideBoundary { placement, id } => R::OutsideBoundary { placement, id },
LayoutViolation::Overlap { placement, other } => R::Overlap { placement, other },
LayoutViolation::TooClose {
placement,
other,
distance,
spacing,
} => R::TooClose {
placement,
other,
distance,
spacing,
},
LayoutViolation::MissingFromUnplaced { id } => R::MissingFromUnplaced { id },
LayoutViolation::WronglyUnplaced { id } => R::WronglyUnplaced { id },
})
.collect();
CheckResponse::checked(violations)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LayoutRules {
pub spacing: f64,
pub margin: f64,
pub multi_sheet: bool,
}
struct Placed {
rings: Vec<Vec<(f64, f64)>>,
min: (f64, f64),
max: (f64, f64),
sheet: usize,
}
pub fn check_layout(
geometries: &[Geometry2D],
boundary: &Boundary2D,
rules: &LayoutRules,
placements: &[Placement<f64>],
unplaced: &[String],
) -> Vec<LayoutViolation> {
let mut out = Vec::new();
let by_id: HashMap<&str, &Geometry2D> =
geometries.iter().map(|g| (g.id().as_str(), g)).collect();
let (b_min, b_max) = boundary.aabb();
let scale = [b_min[0], b_min[1], b_max[0], b_max[1]]
.iter()
.filter(|v| v.is_finite() && v.abs() < 1e12)
.fold(1.0_f64, |m, v| m.max(v.abs()));
let tolerance = 1e-9 * scale + 1e-9;
let mut seen: HashSet<(&str, usize)> = HashSet::new();
let mut placed_count: HashMap<&str, usize> = HashMap::new();
let mut pieces: Vec<Option<Placed>> = Vec::with_capacity(placements.len());
for (i, p) in placements.iter().enumerate() {
let Some(geom) = by_id.get(p.geometry_id.as_str()) else {
out.push(LayoutViolation::UnknownGeometry {
placement: i,
id: p.geometry_id.clone(),
});
pieces.push(None);
continue;
};
let id = geom.id().as_str();
if p.instance >= geom.quantity() {
out.push(LayoutViolation::InstanceOutOfRange {
placement: i,
id: id.to_string(),
instance: p.instance,
quantity: geom.quantity(),
});
} else if !seen.insert((id, p.instance)) {
out.push(LayoutViolation::InstanceRepeated {
placement: i,
id: id.to_string(),
instance: p.instance,
});
} else {
*placed_count.entry(id).or_default() += 1;
}
let rotation = p.rotation.first().copied().unwrap_or(0.0);
if !geom.rotations().iter().any(|&a| same_angle(a, rotation)) {
out.push(LayoutViolation::RotationNotAllowed {
placement: i,
id: id.to_string(),
rotation,
});
}
if p.mirrored && !geom.allow_flip() {
out.push(LayoutViolation::FlipNotAllowed {
placement: i,
id: id.to_string(),
});
}
if p.boundary_index > 0 && !rules.multi_sheet {
out.push(LayoutViolation::SheetNotAllowed {
placement: i,
sheet: p.boundary_index,
});
}
if !crate::is_placement_within_bounds(p, geom, boundary, rules.margin, tolerance) {
out.push(LayoutViolation::OutsideBoundary {
placement: i,
id: id.to_string(),
});
}
pieces.push(Some(place(geom, p)));
}
for i in 0..pieces.len() {
let Some(a) = &pieces[i] else { continue };
for (j, b) in pieces.iter().enumerate().skip(i + 1) {
let Some(b) = b else { continue };
if a.sheet != b.sheet || apart(a, b, rules.spacing + tolerance) {
continue;
}
match separation(a, b, tolerance) {
None => out.push(LayoutViolation::Overlap {
placement: i,
other: j,
}),
Some(distance) if distance < rules.spacing - tolerance => {
out.push(LayoutViolation::TooClose {
placement: i,
other: j,
distance,
spacing: rules.spacing,
})
}
Some(_) => {}
}
}
}
let reported: HashSet<&str> = unplaced.iter().map(String::as_str).collect();
for g in geometries {
let id = g.id().as_str();
let short = placed_count.get(id).copied().unwrap_or(0) < g.quantity();
if short && !reported.contains(id) {
out.push(LayoutViolation::MissingFromUnplaced { id: id.to_string() });
}
}
let mut listed = HashSet::new();
for id in unplaced {
let complete = by_id
.get(id.as_str())
.is_some_and(|g| placed_count.get(id.as_str()).copied().unwrap_or(0) >= g.quantity());
if !by_id.contains_key(id.as_str()) || complete || !listed.insert(id.as_str()) {
out.push(LayoutViolation::WronglyUnplaced { id: id.clone() });
}
}
out
}
fn same_angle(a: f64, b: f64) -> bool {
let d = (a - b).rem_euclid(std::f64::consts::TAU);
d < 1e-6 || std::f64::consts::TAU - d < 1e-6
}
fn place(geom: &Geometry2D, p: &Placement<f64>) -> Placed {
let x = p.position.first().copied().unwrap_or(0.0);
let y = p.position.get(1).copied().unwrap_or(0.0);
let rotation = p.rotation.first().copied().unwrap_or(0.0);
let (sin, cos) = rotation.sin_cos();
let transform = |ring: &[(f64, f64)]| -> Vec<(f64, f64)> {
let ring = if p.mirrored {
mirror_polygon(ring)
} else {
ring.to_vec()
};
ring.into_iter()
.map(|(vx, vy)| (x + vx * cos - vy * sin, y + vx * sin + vy * cos))
.collect()
};
let rings: Vec<Vec<(f64, f64)>> = std::iter::once(geom.exterior())
.chain(geom.holes().iter().map(Vec::as_slice))
.map(transform)
.collect();
let (mut min, mut max) = (
(f64::INFINITY, f64::INFINITY),
(f64::NEG_INFINITY, f64::NEG_INFINITY),
);
for &(vx, vy) in &rings[0] {
min = (min.0.min(vx), min.1.min(vy));
max = (max.0.max(vx), max.1.max(vy));
}
Placed {
rings,
min,
max,
sheet: p.boundary_index,
}
}
fn apart(a: &Placed, b: &Placed, gap: f64) -> bool {
a.max.0 + gap < b.min.0
|| b.max.0 + gap < a.min.0
|| a.max.1 + gap < b.min.1
|| b.max.1 + gap < a.min.1
}
fn dist(a: (f64, f64), b: (f64, f64)) -> f64 {
(a.0 - b.0).hypot(a.1 - b.1)
}
fn separation(a: &Placed, b: &Placed, tolerance: f64) -> Option<f64> {
use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::single::SingleFloatOverlay;
let shape = |p: &Placed| -> Vec<Vec<[f64; 2]>> {
p.rings
.iter()
.map(|r| r.iter().map(|&(x, y)| [x, y]).collect())
.collect()
};
let area = |rings: &[Vec<(f64, f64)>]| -> f64 {
rings
.iter()
.map(|r| crate::polygon_ops::signed_area2(r).abs() / 2.0)
.fold(0.0, |acc, x| if acc == 0.0 { x } else { acc - x })
};
let common: f64 = shape(a)
.overlay(&shape(b), OverlayRule::Intersect, FillRule::EvenOdd)
.iter()
.flatten()
.map(|contour| {
let pts: Vec<(f64, f64)> = contour.iter().map(|p| (p[0], p[1])).collect();
crate::polygon_ops::signed_area2(&pts) / 2.0
})
.sum::<f64>()
.abs();
let outline = |p: &Placed| -> f64 {
p.rings.first().map_or(0.0, |r| {
(0..r.len()).map(|i| dist(r[i], r[(i + 1) % r.len()])).sum()
})
};
let sliver =
(tolerance * outline(a).min(outline(b))).max(area(&a.rings).min(area(&b.rings)) * 1e-9);
if common > sliver {
return None;
}
Some(
a.rings
.iter()
.flat_map(|ra| b.rings.iter().map(move |rb| ring_distance(ra, rb)))
.fold(f64::INFINITY, f64::min),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn square(id: &str, side: f64, quantity: usize) -> Geometry2D {
Geometry2D::rectangle(id, side, side).with_quantity(quantity)
}
fn at(id: &str, instance: usize, x: f64, y: f64) -> Placement<f64> {
Placement::new_2d(id.to_string(), instance, x, y, 0.0)
}
fn rules(spacing: f64, margin: f64) -> LayoutRules {
LayoutRules {
spacing,
margin,
multi_sheet: false,
}
}
#[test]
fn every_strategy_s_layout_passes_the_check() {
use crate::nester::Nester2D;
use u_nesting_core::solver::{Config, Solver, Strategy};
let geometries = vec![
Geometry2D::new("l")
.with_polygon(vec![
(0.0, 0.0),
(20.0, 0.0),
(20.0, 6.0),
(6.0, 6.0),
(6.0, 14.0),
(0.0, 14.0),
])
.with_quantity(3)
.with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
.with_flip(true),
Geometry2D::new("tri")
.with_polygon(vec![(0.0, 0.0), (12.0, 0.0), (0.0, 9.0)])
.with_quantity(3)
.with_rotations_deg(vec![0.0, 180.0]),
Geometry2D::rectangle("frame", 24.0, 24.0).with_hole(vec![
(6.0, 6.0),
(18.0, 6.0),
(18.0, 18.0),
(6.0, 18.0),
]),
Geometry2D::rectangle("chip", 4.0, 3.0).with_quantity(4),
];
let boundary = Boundary2D::rectangle(60.0, 45.0);
for strategy in Strategy::offered(u_nesting_core::solver::Dimension::TwoD) {
let mut config = Config::default()
.with_strategy(strategy)
.with_spacing(1.0)
.with_margin(0.5)
.with_time_limit(2_000);
if strategy.reads_setting(u_nesting_core::solver::Dimension::TwoD, "population_size") {
config = config.with_population_size(6).with_max_generations(4);
}
if strategy.reads_setting(u_nesting_core::solver::Dimension::TwoD, "seed") {
config = config.with_seed(7);
}
let rules = LayoutRules {
spacing: 1.0,
margin: 0.5,
multi_sheet: false,
};
let nester = Nester2D::new(config.clone());
let result = nester.solve(&geometries, &boundary).expect("solved");
let v = check_layout(
&geometries,
&boundary,
&rules,
&result.placements,
&result.unplaced,
);
assert!(v.is_empty(), "{strategy:?}: {v:?}");
let small = Boundary2D::rectangle(40.0, 30.0);
let mut multi = nester
.solve_multi_strip(&geometries, &small)
.expect("solved");
let (b_min, b_max) = small.aabb();
multi.to_boundary_local(b_max[0] - b_min[0]);
let multi_rules = LayoutRules {
multi_sheet: true,
..rules
};
let v = check_layout(
&geometries,
&small,
&multi_rules,
&multi.placements,
&multi.unplaced,
);
assert!(v.is_empty(), "{strategy:?} multi-sheet: {v:?}");
}
}
#[test]
fn a_valid_layout_has_no_violations() {
let g = [square("a", 10.0, 2)];
let b = Boundary2D::rectangle(30.0, 20.0);
let p = [at("a", 0, 1.0, 1.0), at("a", 1, 13.0, 1.0)];
assert!(check_layout(&g, &b, &rules(2.0, 1.0), &p, &[]).is_empty());
let touching = [at("a", 0, 0.0, 0.0), at("a", 1, 10.0, 0.0)];
assert!(check_layout(&g, &b, &rules(0.0, 0.0), &touching, &[]).is_empty());
}
#[test]
fn pieces_touching_along_a_slanted_edge_off_the_grid_do_not_overlap() {
let a = Geometry2D::new("a").with_polygon(vec![
(0.0, 0.0),
(4.0, 2.0),
(6.0, 10.0),
(4.0, 11.0),
(-4.0, 11.0),
(-6.0, 10.0),
(-4.0, 2.0),
]);
let b = Geometry2D::new("b").with_polygon(vec![
(0.0, 0.0),
(3.0, -2.0),
(8.0, 0.0),
(8.0, 8.0),
(2.0, 8.0),
]);
let boundary = Boundary2D::rectangle(300.0, 40.0);
let p = [
Placement::new_2d(
"a".to_string(),
0,
243.62777777777777,
3.4888888888889005,
0.0,
),
Placement::new_2d("b".to_string(), 0, 246.75555555555553, 2.0, 0.0),
];
let (a2, b2) = (a.clone(), b.clone());
let v = check_layout(&[a, b], &boundary, &rules(0.0, 0.0), &p, &[]);
assert!(v.is_empty(), "{v:?}");
let pushed = [
p[0].clone(),
Placement::new_2d("b".to_string(), 0, 246.74555555555553, 2.0, 0.0),
];
let v = check_layout(&[a2, b2], &boundary, &rules(0.0, 0.0), &pushed, &[]);
assert_eq!(
v,
[LayoutViolation::Overlap {
placement: 0,
other: 1
}]
);
}
#[test]
fn overlap_spacing_and_the_boundary_are_checked() {
let g = [square("a", 10.0, 3)];
let b = Boundary2D::rectangle(25.0, 20.0);
let p = [
at("a", 0, 0.0, 0.0),
at("a", 1, 5.0, 0.0),
at("a", 2, 16.0, 0.0),
];
let v = check_layout(&g, &b, &rules(2.0, 0.0), &p, &[]);
assert!(
v.contains(&LayoutViolation::Overlap {
placement: 0,
other: 1
}),
"{v:?}"
);
assert!(
v.contains(&LayoutViolation::TooClose {
placement: 1,
other: 2,
distance: 1.0,
spacing: 2.0
}),
"{v:?}"
);
assert!(
v.contains(&LayoutViolation::OutsideBoundary {
placement: 2,
id: "a".into()
}),
"{v:?}"
);
let same = [at("a", 0, 0.0, 0.0), at("a", 1, 0.0, 0.0)];
assert_eq!(
check_layout(&g, &b, &rules(0.0, 0.0), &same, &["a".into()]),
[LayoutViolation::Overlap {
placement: 0,
other: 1
}]
);
}
#[test]
fn a_piece_in_a_hole_is_judged_by_the_hole() {
let frame = Geometry2D::rectangle("frame", 20.0, 20.0).with_hole(vec![
(5.0, 5.0),
(15.0, 5.0),
(15.0, 15.0),
(5.0, 15.0),
]);
let small = square("small", 4.0, 1);
let g = [frame, small];
let b = Boundary2D::rectangle(30.0, 30.0);
let in_hole = [at("frame", 0, 0.0, 0.0), at("small", 0, 8.0, 8.0)];
assert!(check_layout(&g, &b, &rules(2.0, 0.0), &in_hole, &[]).is_empty());
let near = [at("frame", 0, 0.0, 0.0), at("small", 0, 6.0, 8.0)];
assert!(matches!(
check_layout(&g, &b, &rules(2.0, 0.0), &near, &[]).as_slice(),
[LayoutViolation::TooClose { distance, .. }] if (*distance - 1.0).abs() < 1e-9
));
let on_material = [at("frame", 0, 0.0, 0.0), at("small", 0, 0.5, 0.5)];
assert_eq!(
check_layout(&g, &b, &rules(0.0, 0.0), &on_material, &[]),
[LayoutViolation::Overlap {
placement: 0,
other: 1
}]
);
}
#[test]
fn copies_rotations_flips_sheets_and_unplaced_are_checked() {
let g = [square("a", 5.0, 2), square("b", 5.0, 1)];
let b = Boundary2D::rectangle(50.0, 50.0);
let mut turned = at("a", 0, 10.0, 10.0);
turned.rotation = vec![0.3];
let mut flipped = at("b", 0, 30.0, 30.0);
flipped.mirrored = true;
let mut second_sheet = at("a", 1, 40.0, 0.0);
second_sheet.boundary_index = 1;
let p = [
turned,
at("a", 0, 20.0, 0.0),
at("a", 2, 0.0, 20.0),
at("x", 0, 0.0, 40.0),
flipped,
second_sheet,
];
let v = check_layout(&g, &b, &rules(0.0, 0.0), &p, &["b".into(), "zz".into()]);
assert_eq!(
v,
[
LayoutViolation::RotationNotAllowed {
placement: 0,
id: "a".into(),
rotation: 0.3
},
LayoutViolation::InstanceRepeated {
placement: 1,
id: "a".into(),
instance: 0
},
LayoutViolation::InstanceOutOfRange {
placement: 2,
id: "a".into(),
instance: 2,
quantity: 2
},
LayoutViolation::UnknownGeometry {
placement: 3,
id: "x".into()
},
LayoutViolation::FlipNotAllowed {
placement: 4,
id: "b".into()
},
LayoutViolation::SheetNotAllowed {
placement: 5,
sheet: 1
},
LayoutViolation::WronglyUnplaced { id: "b".into() },
LayoutViolation::WronglyUnplaced { id: "zz".into() },
]
);
let short = [at("a", 0, 0.0, 0.0)];
assert_eq!(
check_layout(&g, &b, &rules(0.0, 0.0), &short, &[]),
[
LayoutViolation::MissingFromUnplaced { id: "a".into() },
LayoutViolation::MissingFromUnplaced { id: "b".into() },
]
);
}
}