use crate::boundary::Boundary2D;
use crate::geometry::Geometry2D;
#[cfg(feature = "milp")]
use crate::nfp::{compute_nfp, Nfp};
#[cfg(feature = "milp")]
use u_nesting_core::exact::{ExactConfig, ExactResult};
use u_nesting_core::geometry::{Boundary, Geometry};
use u_nesting_core::solver::Config;
use u_nesting_core::{Placement, SolveResult};
#[cfg(feature = "milp")]
use good_lp::{
constraint, default_solver, variable, Expression, ProblemVariables, Solution, SolverModel,
Variable,
};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
#[derive(Debug, Clone)]
struct CandidatePosition {
x: f64,
y: f64,
rotation: f64,
rotation_idx: usize,
}
#[derive(Debug, Clone)]
struct PieceInfo {
geometry_idx: usize,
instance_num: usize,
id: String,
area: f64,
widths: Vec<f64>,
heights: Vec<f64>,
candidates: Vec<CandidatePosition>,
}
#[derive(Debug, Clone)]
struct NfpCmSolution {
assignments: Vec<(usize, usize)>,
objective: f64,
exact_result: ExactResult,
}
#[cfg(feature = "milp")]
pub fn run_nfp_cm_nesting(
geometries: &[Geometry2D],
boundary: &Boundary2D,
config: &Config,
exact_config: &ExactConfig,
cancelled: Arc<AtomicBool>,
) -> SolveResult<f64> {
let start = Instant::now();
let mut result = SolveResult::new();
let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
if !exact_config.is_within_limit(total_instances) {
log::warn!(
"Instance count {} exceeds exact limit {}",
total_instances,
exact_config.max_items
);
result.computation_time_ms = start.elapsed().as_millis() as u64;
return result;
}
let (b_min, b_max) = boundary.aabb();
let margin = config.margin;
let bound_width = b_max[0] - b_min[0] - 2.0 * margin;
let bound_height = b_max[1] - b_min[1] - 2.0 * margin;
if bound_width <= 0.0 || bound_height <= 0.0 {
log::error!("Invalid boundary dimensions");
result.computation_time_ms = start.elapsed().as_millis() as u64;
return result;
}
let rotation_angles = exact_config.rotation_angles();
let grid_step = exact_config.grid_step;
let pieces = build_piece_info(
geometries,
boundary,
config,
&rotation_angles,
grid_step,
&cancelled,
);
if pieces.is_empty() {
result.computation_time_ms = start.elapsed().as_millis() as u64;
return result;
}
let conflicts = compute_conflicts(
&pieces,
geometries,
config.spacing,
&cancelled,
start,
exact_config.time_limit_ms,
);
if cancelled.load(Ordering::Relaxed) {
result.computation_time_ms = start.elapsed().as_millis() as u64;
return result;
}
match solve_nfp_cm_milp(
&pieces,
&conflicts,
bound_width,
b_min[0] + margin,
b_min[1] + margin,
&rotation_angles,
exact_config,
&cancelled,
start,
) {
Some(solution) => {
for (piece_idx, candidate_idx) in &solution.assignments {
let piece = &pieces[*piece_idx];
let candidate = &piece.candidates[*candidate_idx];
result.placements.push(Placement::new_2d(
piece.id.clone(),
piece.instance_num,
candidate.x,
candidate.y,
candidate.rotation,
));
}
result.boundaries_used = if result.placements.is_empty() { 0 } else { 1 };
result.utilization =
pieces.iter().map(|p| p.area).sum::<f64>() / (bound_width * bound_height);
result.best_fitness = Some(solution.objective);
result.strategy = Some("NfpCm".to_string());
result.iterations = Some(solution.exact_result.iterations);
if solution.exact_result.is_optimal {
log::info!("NFP-CM found optimal solution");
}
}
None => {
log::warn!("NFP-CM solver failed");
for piece in &pieces {
result.unplaced.push(piece.id.clone());
}
}
}
result.computation_time_ms = start.elapsed().as_millis() as u64;
result
}
fn build_piece_info(
geometries: &[Geometry2D],
boundary: &Boundary2D,
config: &Config,
rotation_angles: &[f64],
grid_step: f64,
cancelled: &Arc<AtomicBool>,
) -> Vec<PieceInfo> {
let (b_min, b_max) = boundary.aabb();
let margin = config.margin;
let mut pieces = Vec::new();
for (geom_idx, geom) in geometries.iter().enumerate() {
if cancelled.load(Ordering::Relaxed) {
return pieces;
}
let mut widths = Vec::new();
let mut heights = Vec::new();
for &angle in rotation_angles {
let (g_min, g_max) = geom.aabb_at_rotation(angle);
widths.push(g_max[0] - g_min[0]);
heights.push(g_max[1] - g_min[1]);
}
for instance in 0..geom.quantity() {
let mut candidates = Vec::new();
for (rot_idx, &angle) in rotation_angles.iter().enumerate() {
let w = widths[rot_idx];
let h = heights[rot_idx];
let min_x = b_min[0] + margin;
let max_x = b_max[0] - margin - w;
let min_y = b_min[1] + margin;
let max_y = b_max[1] - margin - h;
if max_x < min_x || max_y < min_y {
continue; }
let mut x = min_x;
while x <= max_x {
let mut y = min_y;
while y <= max_y {
candidates.push(CandidatePosition {
x,
y,
rotation: angle,
rotation_idx: rot_idx,
});
y += grid_step;
}
x += grid_step;
}
}
if candidates.len() > 1000 {
let step = candidates.len() / 1000;
candidates = candidates.into_iter().step_by(step).collect();
}
if !candidates.is_empty() {
pieces.push(PieceInfo {
geometry_idx: geom_idx,
instance_num: instance,
id: geom.id().to_string(),
area: geom.measure(),
widths: widths.clone(),
heights: heights.clone(),
candidates,
});
}
}
}
pieces
}
type Conflict = ((usize, usize), (usize, usize));
fn compute_conflicts(
pieces: &[PieceInfo],
geometries: &[Geometry2D],
spacing: f64,
cancelled: &Arc<AtomicBool>,
start: Instant,
time_limit_ms: u64,
) -> Vec<Conflict> {
let mut conflicts = Vec::new();
let mut nfp_cache: HashMap<(usize, usize, usize, usize), Option<Nfp>> = HashMap::new();
for i in 0..pieces.len() {
for j in (i + 1)..pieces.len() {
if cancelled.load(Ordering::Relaxed) {
return conflicts;
}
if start.elapsed().as_millis() as u64 > time_limit_ms / 4 {
log::warn!("Conflict computation taking too long, using simplified model");
return compute_aabb_conflicts(pieces);
}
let geom_i = &geometries[pieces[i].geometry_idx];
let geom_j = &geometries[pieces[j].geometry_idx];
for (ci, cand_i) in pieces[i].candidates.iter().enumerate() {
for (cj, cand_j) in pieces[j].candidates.iter().enumerate() {
let cache_key = (
pieces[i].geometry_idx,
pieces[j].geometry_idx,
cand_i.rotation_idx,
cand_j.rotation_idx,
);
let nfp_opt = nfp_cache.entry(cache_key).or_insert_with(|| {
compute_nfp(geom_i, geom_j, cand_j.rotation - cand_i.rotation).ok()
});
let overlaps = if let Some(nfp) = nfp_opt {
let rel_x = cand_j.x - cand_i.x;
let rel_y = cand_j.y - cand_i.y;
point_in_nfp_with_spacing(nfp, rel_x, rel_y, spacing)
} else {
aabb_overlap(
cand_i.x,
cand_i.y,
pieces[i].widths[cand_i.rotation_idx],
pieces[i].heights[cand_i.rotation_idx],
cand_j.x,
cand_j.y,
pieces[j].widths[cand_j.rotation_idx],
pieces[j].heights[cand_j.rotation_idx],
spacing,
)
};
if overlaps {
conflicts.push(((i, ci), (j, cj)));
}
}
}
}
}
conflicts
}
fn compute_aabb_conflicts(pieces: &[PieceInfo]) -> Vec<Conflict> {
let mut conflicts = Vec::new();
for i in 0..pieces.len() {
for j in (i + 1)..pieces.len() {
for (ci, cand_i) in pieces[i].candidates.iter().enumerate() {
for (cj, cand_j) in pieces[j].candidates.iter().enumerate() {
if aabb_overlap(
cand_i.x,
cand_i.y,
pieces[i].widths[cand_i.rotation_idx],
pieces[i].heights[cand_i.rotation_idx],
cand_j.x,
cand_j.y,
pieces[j].widths[cand_j.rotation_idx],
pieces[j].heights[cand_j.rotation_idx],
0.0,
) {
conflicts.push(((i, ci), (j, cj)));
}
}
}
}
}
conflicts
}
fn aabb_overlap(
x1: f64,
y1: f64,
w1: f64,
h1: f64,
x2: f64,
y2: f64,
w2: f64,
h2: f64,
spacing: f64,
) -> bool {
let overlap_x = x1 < x2 + w2 + spacing && x2 < x1 + w1 + spacing;
let overlap_y = y1 < y2 + h2 + spacing && y2 < y1 + h1 + spacing;
overlap_x && overlap_y
}
fn point_in_nfp_with_spacing(nfp: &Nfp, x: f64, y: f64, spacing: f64) -> bool {
for polygon in &nfp.polygons {
if point_in_polygon(x, y, polygon, spacing) {
return true;
}
}
false
}
fn point_in_polygon(x: f64, y: f64, polygon: &[(f64, f64)], buffer: f64) -> bool {
if polygon.len() < 3 {
return false;
}
let mut inside = false;
let n = polygon.len();
for i in 0..n {
let j = (i + 1) % n;
let (xi, yi) = polygon[i];
let (xj, yj) = polygon[j];
let xi = xi - buffer.copysign(xi);
let xj = xj - buffer.copysign(xj);
let yi = yi - buffer.copysign(yi);
let yj = yj - buffer.copysign(yj);
if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
inside = !inside;
}
}
inside
}
#[cfg(feature = "milp")]
fn solve_nfp_cm_milp(
pieces: &[PieceInfo],
conflicts: &[Conflict],
bound_width: f64,
origin_x: f64,
_origin_y: f64,
_rotation_angles: &[f64],
config: &ExactConfig,
cancelled: &Arc<AtomicBool>,
_start: Instant,
) -> Option<NfpCmSolution> {
let n = pieces.len();
let mut vars = ProblemVariables::new();
let z: Vec<Vec<Variable>> = pieces
.iter()
.enumerate()
.map(|(i, piece)| {
piece
.candidates
.iter()
.enumerate()
.map(|(c, _)| vars.add(variable().binary().name(format!("z_{}_{}", i, c))))
.collect()
})
.collect();
let strip_length = vars.add(variable().min(0.0).max(bound_width).name("strip_length"));
let mut problem = vars.minimise(strip_length).using(default_solver);
for (i, _piece) in pieces.iter().enumerate() {
if cancelled.load(Ordering::Relaxed) {
return None;
}
let sum: Expression = z[i].iter().map(|&v| Expression::from(v)).sum();
problem = problem.with(constraint!(sum == 1.0));
}
for (i, piece) in pieces.iter().enumerate() {
for (c, cand) in piece.candidates.iter().enumerate() {
let w = piece.widths[cand.rotation_idx];
let x_rel = cand.x - origin_x; let big_m = bound_width * 2.0;
problem = problem.with(constraint!(
x_rel + w - strip_length <= big_m * (1.0 - z[i][c])
));
}
}
for ((i, ci), (j, cj)) in conflicts {
problem = problem.with(constraint!(z[*i][*ci] + z[*j][*cj] <= 1.0));
}
if config.use_symmetry_breaking {
for i in 0..(n - 1) {
if pieces[i].geometry_idx == pieces[i + 1].geometry_idx {
for (c, _) in pieces[i].candidates.iter().enumerate() {
if c < pieces[i + 1].candidates.len() {
}
}
}
}
}
log::info!(
"Solving NFP-CM MILP with {} pieces, {} candidates, {} conflicts",
n,
pieces.iter().map(|p| p.candidates.len()).sum::<usize>(),
conflicts.len()
);
match problem.solve() {
Ok(solution) => {
let obj_value = solution.value(strip_length);
let mut assignments = Vec::new();
for (i, piece) in pieces.iter().enumerate() {
for (c, _) in piece.candidates.iter().enumerate() {
if solution.value(z[i][c]) > 0.5 {
assignments.push((i, c));
break;
}
}
}
let exact_result = ExactResult::optimal(obj_value);
Some(NfpCmSolution {
assignments,
objective: obj_value,
exact_result,
})
}
Err(e) => {
log::error!("NFP-CM MILP solver error: {:?}", e);
None
}
}
}
#[cfg(not(feature = "milp"))]
pub fn run_nfp_cm_nesting(
geometries: &[Geometry2D],
boundary: &Boundary2D,
_config: &Config,
_exact_config: &ExactConfig,
_cancelled: Arc<AtomicBool>,
) -> SolveResult<f64> {
log::warn!("NFP-CM solver not available (compile with 'milp' feature)");
let mut result = SolveResult::new();
for geom in geometries {
for _ in 0..geom.quantity() {
result.unplaced.push(geom.id().to_string());
}
}
result.strategy = Some("NfpCm (disabled)".to_string());
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aabb_overlap() {
assert!(aabb_overlap(
0.0, 0.0, 10.0, 10.0, 5.0, 5.0, 10.0, 10.0, 0.0
));
assert!(!aabb_overlap(
0.0, 0.0, 10.0, 10.0, 20.0, 20.0, 10.0, 10.0, 0.0
));
assert!(aabb_overlap(
0.0, 0.0, 10.0, 10.0, 10.0, 0.0, 10.0, 10.0, 1.0
));
}
#[test]
fn test_point_in_polygon() {
let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
assert!(point_in_polygon(5.0, 5.0, &square, 0.0));
assert!(!point_in_polygon(15.0, 5.0, &square, 0.0));
}
#[test]
#[cfg(feature = "milp")]
fn test_nfp_cm_simple() {
let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
let boundary = Boundary2D::rectangle(50.0, 50.0);
let config = Config::default();
let exact_config = ExactConfig::default()
.with_time_limit_ms(10000)
.with_rotation_steps(1)
.with_grid_step(5.0);
let cancelled = Arc::new(AtomicBool::new(false));
let result = run_nfp_cm_nesting(&geometries, &boundary, &config, &exact_config, cancelled);
assert!(!result.placements.is_empty());
}
}