use crate::boundary::Boundary2D;
use crate::geometry::Geometry2D;
use crate::nfp::Nfp;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use u_nesting_core::geometry::{Boundary, Geometry};
use u_nesting_core::timing::Timer;
use u_nesting_core::Placement;
#[derive(Debug, Clone)]
pub struct InstanceInfo {
pub geometry_idx: usize,
pub instance_num: usize,
}
pub fn polygon_centroid(polygon: &[(f64, f64)]) -> (f64, f64) {
if polygon.is_empty() {
return (0.0, 0.0);
}
let sum: (f64, f64) = polygon
.iter()
.fold((0.0, 0.0), |acc, &(x, y)| (acc.0 + x, acc.1 + y));
let n = polygon.len() as f64;
(sum.0 / n, sum.1 / n)
}
pub(crate) fn nfps_against_placed(
cache: &crate::nfp::NfpCache,
placed: &[crate::nfp::PlacedGeometry],
geom: &Geometry2D,
rotation: f64,
mirror: bool,
spacing: f64,
) -> Vec<Nfp> {
placed
.iter()
.filter_map(|p| {
let relative = rotation - p.rotation;
let at_origin = cache
.get_or_compute_mirrored(
(
p.geometry.id().as_str(),
geom.id().as_str(),
relative,
p.mirrored,
mirror,
),
|| {
crate::nfp::compute_nfp_mirrored(
&p.geometry, geom, relative, p.mirrored, mirror,
)
.map(|nfp| offset_nfp(&nfp, spacing))
},
)
.ok()?;
Some(crate::nfp::translate_nfp(
&crate::nfp::rotate_nfp(&at_origin, p.rotation),
p.position,
))
})
.collect()
}
pub fn offset_nfp(nfp: &Nfp, spacing: f64) -> Nfp {
if spacing <= 0.0 {
return nfp.clone();
}
Nfp::from_polygons(
nfp.polygons
.iter()
.flat_map(|polygon| crate::polygon_ops::offset_polygon(polygon, spacing))
.collect(),
)
}
pub fn inset_boundary(boundary: &Boundary2D, margin: f64) -> Vec<(f64, f64)> {
let rectangular =
boundary.is_infinite() || (boundary.width().is_some() && boundary.height().is_some());
if rectangular {
let (b_min, b_max) = boundary.aabb();
return vec![
(b_min[0] + margin, b_min[1] + margin),
(b_max[0] - margin, b_min[1] + margin),
(b_max[0] - margin, b_max[1] - margin),
(b_min[0] + margin, b_max[1] - margin),
];
}
let mut ring = crate::polygon_ops::offset_polygon(boundary.exterior(), -margin)
.into_iter()
.max_by(|a, b| {
let area = |r: &[(f64, f64)]| crate::polygon_ops::signed_area2(r).abs();
area(a).total_cmp(&area(b))
})
.unwrap_or_default();
if crate::polygon_ops::signed_area2(&ring) < 0.0 {
ring.reverse();
}
ring
}
pub fn hole_nfps(
boundary: &Boundary2D,
geometry: &Geometry2D,
rotation: f64,
mirror: bool,
margin: f64,
) -> Vec<Nfp> {
boundary
.holes()
.iter()
.enumerate()
.filter_map(|(i, hole)| {
let stationary = Geometry2D::new(format!("_hole_{i}")).with_polygon(hole.clone());
crate::nfp::compute_nfp_mirrored(&stationary, geometry, rotation, false, mirror).ok()
})
.map(|nfp| offset_nfp(&nfp, margin))
.collect()
}
#[derive(Debug, Clone)]
pub struct BestLayout {
pub fitness: f64,
pub placements: Vec<Placement<f64>>,
pub utilization: f64,
}
#[derive(Debug, Default)]
pub struct SearchState {
budget: Option<(Timer, Duration)>,
best: Arc<Mutex<Option<BestLayout>>>,
}
impl SearchState {
pub fn with_time_limit(limit: Option<Duration>) -> Self {
Self {
budget: limit.map(|limit| (Timer::now(), limit)),
best: Arc::default(),
}
}
pub fn out_of_time(&self) -> bool {
self.budget
.as_ref()
.is_some_and(|(start, limit)| start.elapsed() > *limit)
}
pub fn offer(&self, fitness: f64, placements: &[Placement<f64>], utilization: f64) {
let mut best = self
.best
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if best.as_ref().is_none_or(|b| {
fitness > b.fitness
|| (fitness == b.fitness && layout_order(placements, &b.placements).is_lt())
}) {
*best = Some(BestLayout {
fitness,
placements: placements.to_vec(),
utilization,
});
}
}
pub fn best_handle(&self) -> Arc<Mutex<Option<BestLayout>>> {
Arc::clone(&self.best)
}
}
fn layout_order(a: &[Placement<f64>], b: &[Placement<f64>]) -> std::cmp::Ordering {
let floats = |x: &[f64], y: &[f64]| {
x.iter()
.zip(y)
.map(|(p, q)| p.total_cmp(q))
.find(|o| o.is_ne())
.unwrap_or(x.len().cmp(&y.len()))
};
a.iter()
.zip(b)
.map(|(p, q)| {
p.geometry_id
.cmp(&q.geometry_id)
.then(p.instance.cmp(&q.instance))
.then_with(|| floats(&p.position, &q.position))
.then_with(|| floats(&p.rotation, &q.rotation))
.then(p.mirrored.cmp(&q.mirrored))
})
.find(|o| o.is_ne())
.unwrap_or(a.len().cmp(&b.len()))
}
pub fn take_best(handle: &Mutex<Option<BestLayout>>) -> Option<BestLayout> {
handle
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.take()
}
pub fn seed_genes(
geometries: &[Geometry2D],
placements: &[Placement<f64>],
) -> (Vec<usize>, Vec<bool>) {
let mut rotations = Vec::new();
let mut mirrors = Vec::new();
for geometry in geometries {
let angles = geometry.rotations();
for instance in 0..geometry.quantity() {
let placed = placements
.iter()
.find(|p| &p.geometry_id == geometry.id() && p.instance == instance);
let (rotation, mirrored) = placed.map_or((0, false), |p| {
let angle = p.rotation.first().copied().unwrap_or(0.0);
let turn = std::f64::consts::TAU;
let index = angles
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
let gap = |x: f64| {
(x - angle)
.rem_euclid(turn)
.min((angle - x).rem_euclid(turn))
};
gap(**a).total_cmp(&gap(**b))
})
.map_or(0, |(i, _)| i);
(index, p.mirrored)
});
rotations.push(rotation);
mirrors.push(mirrored);
}
}
(rotations, mirrors)
}
pub fn strip_frame(boundary: &Boundary2D) -> (crate::nfp::PackingAxis, f64, f64) {
let axis = crate::nfp::PackingAxis::of(boundary);
let (b_min, b_max) = boundary.aabb();
match axis {
crate::nfp::PackingAxis::X => (axis, b_min[0], b_max[1] - b_min[1]),
crate::nfp::PackingAxis::Y => (axis, b_min[1], b_max[0] - b_min[0]),
}
}
pub fn layout_density(
geometries: &[Geometry2D],
placements: &[Placement<f64>],
boundary: &Boundary2D,
) -> f64 {
let (axis, start, cross) = strip_frame(boundary);
let by_id: std::collections::HashMap<&str, &Geometry2D> =
geometries.iter().map(|g| (g.id().as_str(), g)).collect();
let (mut area, mut end) = (0.0, f64::NEG_INFINITY);
for p in placements {
let Some(geom) = by_id.get(p.geometry_id.as_str()) else {
continue;
};
let rotation = p.rotation.first().copied().unwrap_or(0.0);
let (_, hi) = geom.aabb_at_rotation_mirrored(rotation, p.mirrored);
let k = match axis {
crate::nfp::PackingAxis::X => 0,
crate::nfp::PackingAxis::Y => 1,
};
area += geom.measure();
end = end.max(p.position.get(k).copied().unwrap_or(0.0) + hi[k]);
}
let length = end - start;
if area <= 0.0 || length <= 0.0 || cross <= 0.0 {
return 0.0;
}
(area / (length * cross)).min(1.0)
}
pub fn nesting_fitness(placed_count: usize, total_count: usize, utilization: f64) -> f64 {
let placement_ratio = placed_count as f64 / total_count.max(1) as f64;
placement_ratio * 100.0 + utilization * 10.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn density_tells_a_short_layout_from_a_long_one() {
let g = [Geometry2D::rectangle("s", 10.0, 10.0).with_quantity(2)];
let strip = Boundary2D::rectangle(100.0, 20.0);
let at = |i: usize, x: f64, y: f64| Placement::new_2d("s".to_string(), i, x, y, 0.0);
let short = [at(0, 0.0, 0.0), at(1, 0.0, 10.0)];
let long = [at(0, 0.0, 0.0), at(1, 40.0, 0.0)];
assert!((layout_density(&g, &short, &strip) - 200.0 / (10.0 * 20.0)).abs() < 1e-12);
assert!((layout_density(&g, &long, &strip) - 200.0 / (50.0 * 20.0)).abs() < 1e-12);
assert_eq!(layout_density(&g, &[], &strip), 0.0);
let tall = Boundary2D::rectangle(20.0, 100.0);
let stacked = [at(0, 0.0, 0.0), at(1, 10.0, 0.0)];
assert!((layout_density(&g, &stacked, &tall) - 1.0).abs() < 1e-12);
}
#[test]
fn test_polygon_centroid_empty() {
assert_eq!(polygon_centroid(&[]), (0.0, 0.0));
}
#[test]
fn test_polygon_centroid_square() {
let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
let (cx, cy) = polygon_centroid(&square);
assert!((cx - 5.0).abs() < 1e-10);
assert!((cy - 5.0).abs() < 1e-10);
}
#[test]
fn offset_nfp_with_no_spacing_is_the_nfp() {
let nfp = Nfp::from_polygons(vec![vec![
(0.0, 0.0),
(10.0, 0.0),
(10.0, 10.0),
(0.0, 10.0),
]]);
assert_eq!(offset_nfp(&nfp, 0.0).polygons, nfp.polygons);
}
#[test]
fn offset_nfp_moves_every_edge_by_the_spacing() {
let nfp = Nfp::from_polygons(vec![vec![
(-300.0, -300.0),
(300.0, -300.0),
(300.0, 300.0),
(-300.0, 300.0),
]]);
let grown = offset_nfp(&nfp, 50.0);
assert_eq!(grown.polygons.len(), 1);
let ring = &grown.polygons[0];
let max_x = ring.iter().map(|p| p.0).fold(f64::MIN, f64::max);
let max_y = ring.iter().map(|p| p.1).fold(f64::MIN, f64::max);
assert!((350.0..350.1).contains(&max_x), "right edge at {max_x}");
assert!((350.0..350.1).contains(&max_y), "top edge at {max_y}");
}
#[test]
fn inset_boundary_applies_the_margin_once_to_a_rectangle() {
let rect = inset_boundary(&Boundary2D::rectangle(1000.0, 500.0), 50.0);
assert_eq!(
rect,
vec![(50.0, 50.0), (950.0, 50.0), (950.0, 450.0), (50.0, 450.0)]
);
}
#[test]
fn inset_boundary_follows_a_slanted_edge() {
let triangle = Boundary2D::new(vec![(0.0, 0.0), (100.0, 0.0), (0.0, 100.0)]);
let ring = inset_boundary(&triangle, 10.0);
assert!(ring.len() >= 3);
for &(x, y) in &ring {
let to_hypotenuse = (100.0 - x - y) / 2f64.sqrt();
assert!(x >= 10.0 - 1e-6 && y >= 10.0 - 1e-6 && to_hypotenuse >= 10.0 - 1e-6);
}
}
#[test]
fn test_nesting_fitness_all_placed() {
let f = nesting_fitness(10, 10, 0.85);
assert!((f - 108.5).abs() < 1e-10);
}
#[test]
fn test_nesting_fitness_partial() {
let f = nesting_fitness(5, 10, 0.40);
assert!((f - 54.0).abs() < 1e-10);
}
#[test]
fn test_nesting_fitness_none_placed() {
let f = nesting_fitness(0, 10, 0.0);
assert!((f - 0.0).abs() < 1e-10);
}
#[test]
fn test_nesting_fitness_empty_total() {
let f = nesting_fitness(0, 0, 0.0);
assert!((f - 0.0).abs() < 1e-10);
}
}