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 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) {
*best = Some(BestLayout {
fitness,
placements: placements.to_vec(),
utilization,
});
}
}
pub fn best_handle(&self) -> Arc<Mutex<Option<BestLayout>>> {
Arc::clone(&self.best)
}
}
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 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 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);
}
}