1use crate::alns_nesting::run_alns_nesting;
4use crate::boundary::Boundary2D;
5use crate::brkga_nesting::run_brkga_nesting;
6use crate::clamp_placement_to_boundary_with_margin;
7use crate::ga_nesting::{run_ga_nesting, run_ga_nesting_with_progress};
8use crate::gdrr_nesting::run_gdrr_nesting;
9use crate::geometry::Geometry2D;
10#[cfg(feature = "milp")]
11use crate::milp_solver::run_milp_nesting;
12use crate::nfp::{
13 compute_ifp_with_margin, compute_nfp, find_bottom_left_placement, rotate_nfp, translate_nfp,
14 Nfp, NfpCache, PlacedGeometry,
15};
16#[cfg(feature = "milp")]
17#[allow(unused_imports)]
18use crate::nfp_cm_solver::run_nfp_cm_nesting;
19use crate::sa_nesting::run_sa_nesting;
20use crate::validate_and_filter_placements;
21use u_nesting_core::alns::AlnsConfig;
22use u_nesting_core::brkga::BrkgaConfig;
23#[cfg(feature = "milp")]
24use u_nesting_core::exact::ExactConfig;
25use u_nesting_core::ga::GaConfig;
26use u_nesting_core::gdrr::GdrrConfig;
27use u_nesting_core::geometry::{Boundary, Geometry};
28use u_nesting_core::sa::SaConfig;
29use u_nesting_core::solver::{Config, ProgressCallback, ProgressInfo, Solver, Strategy};
30use u_nesting_core::{Placement, Result, SolveResult};
31
32use crate::placement_utils::{expand_nfp, shrink_ifp};
33use std::sync::atomic::{AtomicBool, Ordering};
34use std::sync::Arc;
35use u_nesting_core::timing::Timer;
36
37fn solution_quality(
51 result: &SolveResult<f64>,
52 geometries: &[Geometry2D],
53 boundary: &Boundary2D,
54) -> (usize, f64) {
55 use std::collections::HashMap;
56 let geom_map: HashMap<_, _> = geometries.iter().map(|g| (g.id().clone(), g)).collect();
57
58 let mut min_x = f64::INFINITY;
59 let mut min_y = f64::INFINITY;
60 let mut max_x = f64::NEG_INFINITY;
61 let mut max_y = f64::NEG_INFINITY;
62
63 for p in &result.placements {
64 if let Some(geom) = geom_map.get(&p.geometry_id) {
65 let x = p.position.first().copied().unwrap_or(0.0);
66 let y = p.position.get(1).copied().unwrap_or(0.0);
67 let rot = p.rotation.first().copied().unwrap_or(0.0);
68 let (g_min, g_max) = geom.aabb_at_rotation(rot);
69 min_x = min_x.min(x + g_min[0]);
70 min_y = min_y.min(y + g_min[1]);
71 max_x = max_x.max(x + g_max[0]);
72 max_y = max_y.max(y + g_max[1]);
73 }
74 }
75
76 if result.placements.is_empty() {
77 return (0, f64::INFINITY);
78 }
79
80 let (b_min, b_max) = boundary.aabb();
82 let bound_w = b_max[0] - b_min[0];
83 let bound_h = b_max[1] - b_min[1];
84 let strip_length = if bound_h >= bound_w {
85 max_y - min_y
86 } else {
87 max_x - min_x
88 };
89 (result.placements.len(), strip_length)
90}
91
92pub struct Nester2D {
94 config: Config,
95 cancelled: Arc<AtomicBool>,
96 #[allow(dead_code)] nfp_cache: NfpCache,
98}
99
100impl Nester2D {
101 pub fn new(config: Config) -> Self {
103 Self {
104 config,
105 cancelled: Arc::new(AtomicBool::new(false)),
106 nfp_cache: NfpCache::new(),
107 }
108 }
109
110 pub fn default_config() -> Self {
112 Self::new(Config::default())
113 }
114
115 fn bottom_left_fill(
117 &self,
118 geometries: &[Geometry2D],
119 boundary: &Boundary2D,
120 ) -> Result<SolveResult<f64>> {
121 let start = Timer::now();
122 let mut result = SolveResult::new();
123 let mut placements = Vec::new();
124
125 let (b_min, b_max) = boundary.aabb();
127 let margin = self.config.margin;
128 let spacing = self.config.spacing;
129
130 let bound_min_x = b_min[0] + margin;
131 let bound_min_y = b_min[1] + margin;
132 let bound_max_x = b_max[0] - margin;
133 let bound_max_y = b_max[1] - margin;
134
135 let strip_width = bound_max_x - bound_min_x;
136 let strip_height = bound_max_y - bound_min_y;
137
138 let mut current_x = bound_min_x;
140 let mut current_y = bound_min_y;
141 let mut row_height = 0.0_f64;
142
143 let mut total_placed_area = 0.0;
144
145 for geom in geometries {
146 geom.validate()?;
147
148 let rotations = geom.rotations();
150 let rotation_angles: Vec<f64> = if rotations.is_empty() {
151 vec![0.0]
152 } else {
153 rotations
154 };
155
156 for instance in 0..geom.quantity() {
157 if self.cancelled.load(Ordering::Relaxed) {
158 result.computation_time_ms = start.elapsed_ms();
159 return Ok(result);
160 }
161
162 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
164 {
165 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
166 result.utilization = total_placed_area / boundary.measure();
167 result.computation_time_ms = start.elapsed_ms();
168 result.placements = placements;
169 return Ok(result);
170 }
171
172 let mut best_fit: Option<(f64, f64, f64, f64, f64, [f64; 2])> = None; for &rotation in &rotation_angles {
176 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
177 let g_width = g_max[0] - g_min[0];
178 let g_height = g_max[1] - g_min[1];
179
180 if g_width > strip_width || g_height > strip_height {
182 continue;
183 }
184
185 let mut place_x = current_x;
187 let mut place_y = current_y;
188
189 if place_x + g_width > bound_max_x {
191 place_x = bound_min_x;
193 place_y += row_height + spacing;
194 }
195
196 if place_y + g_height > bound_max_y {
198 continue; }
200
201 let score = if place_x == bound_min_x && place_y > current_y {
204 place_y - bound_min_y + g_height
206 } else {
207 place_x - bound_min_x + g_width
209 };
210
211 let is_better = match &best_fit {
212 None => true,
213 Some((_, _, _, _, _, _)) => {
214 let best_score = if let Some((_, _, _, bx, by, _)) = best_fit {
216 if bx == bound_min_x && by > current_y {
217 by - bound_min_y + g_height
218 } else {
219 bx - bound_min_x + g_width
220 }
221 } else {
222 f64::INFINITY
223 };
224 score < best_score - 1e-6
225 }
226 };
227
228 if is_better {
229 best_fit = Some((rotation, g_width, g_height, place_x, place_y, g_min));
230 }
231 }
232
233 if let Some((rotation, g_width, g_height, place_x, place_y, g_min)) = best_fit {
235 if place_x == bound_min_x && place_y > current_y {
237 row_height = 0.0;
238 }
239
240 let origin_x = place_x - g_min[0];
242 let origin_y = place_y - g_min[1];
243
244 let geom_aabb = geom.aabb_at_rotation(rotation);
246 let boundary_aabb = (b_min, b_max);
247
248 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
249 origin_x,
250 origin_y,
251 geom_aabb,
252 boundary_aabb,
253 margin,
254 ) {
255 let placement = Placement::new_2d(
256 geom.id().clone(),
257 instance,
258 clamped_x,
259 clamped_y,
260 rotation,
261 );
262
263 placements.push(placement);
264 total_placed_area += geom.measure();
265
266 let actual_place_x = clamped_x + g_min[0];
269 let actual_place_y = clamped_y + g_min[1];
270 current_x = actual_place_x + g_width + spacing;
271 current_y = actual_place_y;
272 row_height = row_height.max(g_height);
273 }
274 } else {
275 result.unplaced.push(geom.id().clone());
277 }
278 }
279 }
280
281 result.placements = placements;
282 result.boundaries_used = 1;
283 result.utilization = total_placed_area / boundary.measure();
284 result.computation_time_ms = start.elapsed_ms();
285
286 Ok(result)
287 }
288
289 fn nfp_guided_blf(
294 &self,
295 geometries: &[Geometry2D],
296 boundary: &Boundary2D,
297 ) -> Result<SolveResult<f64>> {
298 let start = Timer::now();
299 let mut result = SolveResult::new();
300 let mut placements = Vec::new();
301 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
302
303 let margin = self.config.margin;
304 let spacing = self.config.spacing;
305
306 let boundary_polygon = self.get_boundary_polygon_with_margin(boundary, margin);
308
309 let mut total_placed_area = 0.0;
310
311 let sample_step = self.compute_sample_step(geometries);
313
314 for geom in geometries {
315 geom.validate()?;
316
317 let rotations = geom.rotations();
319 let rotation_angles: Vec<f64> = if rotations.is_empty() {
320 vec![0.0]
321 } else {
322 rotations
323 };
324
325 for instance in 0..geom.quantity() {
326 if self.cancelled.load(Ordering::Relaxed) {
327 result.computation_time_ms = start.elapsed_ms();
328 return Ok(result);
329 }
330
331 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
333 {
334 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
335 result.utilization = total_placed_area / boundary.measure();
336 result.computation_time_ms = start.elapsed_ms();
337 result.placements = placements;
338 return Ok(result);
339 }
340
341 let mut best_placement: Option<(f64, f64, f64)> = None; for &rotation in &rotation_angles {
345 let ifp =
347 match compute_ifp_with_margin(&boundary_polygon, geom, rotation, margin) {
348 Ok(ifp) => ifp,
349 Err(_) => continue,
350 };
351
352 if ifp.is_empty() {
353 continue;
354 }
355
356 let mut nfps: Vec<Nfp> = Vec::new();
358 for placed in &placed_geometries {
359 let cache_key = (
362 placed.geometry.id().as_str(),
363 geom.id().as_str(),
364 rotation - placed.rotation, );
366
367 let nfp_at_origin = match self.nfp_cache.get_or_compute(cache_key, || {
372 let placed_at_origin = placed.geometry.clone();
373 compute_nfp(&placed_at_origin, geom, rotation - placed.rotation)
374 }) {
375 Ok(nfp) => nfp,
376 Err(_) => continue,
377 };
378
379 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
382 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
383 let expanded = self.expand_nfp(&translated_nfp, spacing);
384 nfps.push(expanded);
385 }
386
387 let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
389
390 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
392 if let Some((x, y)) =
393 find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
394 {
395 let is_better = match best_placement {
397 None => true,
398 Some((best_x, best_y, _)) => {
399 x < best_x - 1e-6 || (x < best_x + 1e-6 && y < best_y - 1e-6)
400 }
401 };
402 if is_better {
403 best_placement = Some((x, y, rotation));
404 }
405 }
406 }
407
408 if let Some((x, y, rotation)) = best_placement {
410 let geom_aabb = geom.aabb_at_rotation(rotation);
412 let boundary_aabb = boundary.aabb();
413
414 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
415 x,
416 y,
417 geom_aabb,
418 boundary_aabb,
419 margin,
420 ) {
421 let placement = Placement::new_2d(
422 geom.id().clone(),
423 instance,
424 clamped_x,
425 clamped_y,
426 rotation,
427 );
428
429 placements.push(placement);
430 placed_geometries.push(PlacedGeometry::new(
431 geom.clone(),
432 (clamped_x, clamped_y),
433 rotation,
434 ));
435 total_placed_area += geom.measure();
436 } else {
437 result.unplaced.push(geom.id().clone());
439 }
440 } else {
441 result.unplaced.push(geom.id().clone());
443 }
444 }
445 }
446
447 result.placements = placements;
448 result.boundaries_used = 1;
449 result.utilization = total_placed_area / boundary.measure();
450 result.computation_time_ms = start.elapsed_ms();
451
452 Ok(result)
453 }
454
455 fn get_boundary_polygon_with_margin(
457 &self,
458 boundary: &Boundary2D,
459 margin: f64,
460 ) -> Vec<(f64, f64)> {
461 let (b_min, b_max) = boundary.aabb();
462
463 vec![
465 (b_min[0] + margin, b_min[1] + margin),
466 (b_max[0] - margin, b_min[1] + margin),
467 (b_max[0] - margin, b_max[1] - margin),
468 (b_min[0] + margin, b_max[1] - margin),
469 ]
470 }
471
472 fn compute_sample_step(&self, geometries: &[Geometry2D]) -> f64 {
474 if geometries.is_empty() {
475 return 1.0;
476 }
477
478 let mut min_dim = f64::INFINITY;
480 for geom in geometries {
481 let (g_min, g_max) = geom.aabb();
482 let width = g_max[0] - g_min[0];
483 let height = g_max[1] - g_min[1];
484 min_dim = min_dim.min(width).min(height);
485 }
486
487 (min_dim / 4.0).clamp(0.5, 10.0)
489 }
490
491 fn expand_nfp(&self, nfp: &Nfp, spacing: f64) -> Nfp {
493 expand_nfp(nfp, spacing)
494 }
495
496 fn shrink_ifp(&self, ifp: &Nfp, spacing: f64) -> Nfp {
498 shrink_ifp(ifp, spacing)
499 }
500
501 fn not_worse_than_blf(
510 &self,
511 meta: SolveResult<f64>,
512 geometries: &[Geometry2D],
513 boundary: &Boundary2D,
514 ) -> SolveResult<f64> {
515 let blf = match self.bottom_left_fill(geometries, boundary) {
516 Ok(b) => b,
517 Err(_) => return meta,
518 };
519 let (meta_placed, meta_len) = solution_quality(&meta, geometries, boundary);
520 let (blf_placed, blf_len) = solution_quality(&blf, geometries, boundary);
521 if blf_placed > meta_placed || (blf_placed == meta_placed && blf_len < meta_len - 1e-6) {
524 let mut floored = blf;
530 floored.strategy = meta.strategy;
531 floored.generations = meta.generations;
532 floored.best_fitness = meta.best_fitness;
533 floored.fitness_history = meta.fitness_history;
534 floored.target_reached = meta.target_reached;
535 floored
536 } else {
537 meta
538 }
539 }
540
541 fn genetic_algorithm(
546 &self,
547 geometries: &[Geometry2D],
548 boundary: &Boundary2D,
549 ) -> Result<SolveResult<f64>> {
550 let time_limit_ms = if self.config.time_limit_ms > 0 {
552 (self.config.time_limit_ms / 4)
557 .max(5000)
558 .min(self.config.time_limit_ms)
559 } else {
560 15000 };
562
563 let ga_config = GaConfig::default()
564 .with_population_size(self.config.population_size.min(30)) .with_max_generations(self.config.max_generations.min(50)) .with_crossover_rate(self.config.crossover_rate)
567 .with_mutation_rate(self.config.mutation_rate)
568 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
569
570 let result = run_ga_nesting(
571 geometries,
572 boundary,
573 &self.config,
574 ga_config,
575 self.cancelled.clone(),
576 );
577
578 Ok(self.not_worse_than_blf(result, geometries, boundary))
579 }
580
581 fn brkga(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
585 let time_limit_ms = if self.config.time_limit_ms > 0 {
587 (self.config.time_limit_ms / 4)
592 .max(5000)
593 .min(self.config.time_limit_ms)
594 } else {
595 15000 };
597
598 let brkga_config = BrkgaConfig::default()
599 .with_population_size(self.config.population_size.min(30))
603 .with_max_generations(50) .with_elite_fraction(0.2)
605 .with_mutant_fraction(0.15)
606 .with_elite_bias(0.7)
607 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
608
609 let result = run_brkga_nesting(
610 geometries,
611 boundary,
612 &self.config,
613 brkga_config,
614 self.cancelled.clone(),
615 );
616
617 Ok(self.not_worse_than_blf(result, geometries, boundary))
618 }
619
620 fn simulated_annealing(
625 &self,
626 geometries: &[Geometry2D],
627 boundary: &Boundary2D,
628 ) -> Result<SolveResult<f64>> {
629 let time_limit_ms = if self.config.time_limit_ms > 0 {
632 (self.config.time_limit_ms / 4)
637 .max(5000)
638 .min(self.config.time_limit_ms)
639 } else {
640 10000 };
642
643 let sa_config = SaConfig::default()
644 .with_initial_temp(50.0) .with_final_temp(1.0) .with_cooling_rate(0.9) .with_iterations_per_temp(20) .with_max_iterations(500) .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
650
651 let result = run_sa_nesting(
652 geometries,
653 boundary,
654 &self.config,
655 sa_config,
656 self.cancelled.clone(),
657 );
658
659 Ok(self.not_worse_than_blf(result, geometries, boundary))
660 }
661
662 fn gdrr(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
664 let time_limit = if self.config.time_limit_ms > 0 {
667 (self.config.time_limit_ms / 4)
672 .max(5000)
673 .min(self.config.time_limit_ms)
674 } else {
675 10000 };
677 let gdrr_config = GdrrConfig::default()
678 .with_max_iterations(1000) .with_time_limit_ms(time_limit)
680 .with_ruin_ratio(0.1, 0.3) .with_lahc_list_length(30); let result = run_gdrr_nesting(
684 geometries,
685 boundary,
686 &self.config,
687 &gdrr_config,
688 self.cancelled.clone(),
689 );
690
691 Ok(result)
692 }
693
694 fn alns(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
696 let time_limit = if self.config.time_limit_ms > 0 {
699 (self.config.time_limit_ms / 4)
704 .max(5000)
705 .min(self.config.time_limit_ms)
706 } else {
707 10000 };
709 let alns_config = AlnsConfig::default()
710 .with_max_iterations(1000) .with_time_limit_ms(time_limit)
712 .with_segment_size(50) .with_scores(33.0, 9.0, 13.0)
714 .with_reaction_factor(0.15) .with_temperature(100.0, 0.999, 0.1); let result = run_alns_nesting(
718 geometries,
719 boundary,
720 &self.config,
721 &alns_config,
722 self.cancelled.clone(),
723 );
724
725 Ok(result)
726 }
727
728 #[cfg(feature = "milp")]
730 fn milp_exact(
731 &self,
732 geometries: &[Geometry2D],
733 boundary: &Boundary2D,
734 ) -> Result<SolveResult<f64>> {
735 let exact_config = ExactConfig::default()
736 .with_time_limit_ms(self.config.time_limit_ms.max(60000))
737 .with_max_items(15)
738 .with_rotation_steps(4)
739 .with_grid_step(1.0);
740
741 let result = run_milp_nesting(
742 geometries,
743 boundary,
744 &self.config,
745 &exact_config,
746 self.cancelled.clone(),
747 );
748
749 Ok(result)
750 }
751
752 #[cfg(feature = "milp")]
754 fn hybrid_exact(
755 &self,
756 geometries: &[Geometry2D],
757 boundary: &Boundary2D,
758 ) -> Result<SolveResult<f64>> {
759 let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
761
762 if total_instances <= 15 {
764 let exact_config = ExactConfig::default()
765 .with_time_limit_ms((self.config.time_limit_ms / 2).max(30000))
766 .with_max_items(15);
767
768 let exact_result = run_milp_nesting(
769 geometries,
770 boundary,
771 &self.config,
772 &exact_config,
773 self.cancelled.clone(),
774 );
775
776 if !exact_result.placements.is_empty() {
778 return Ok(exact_result);
779 }
780 }
781
782 self.alns(geometries, boundary)
784 }
785
786 fn bottom_left_fill_with_progress(
788 &self,
789 geometries: &[Geometry2D],
790 boundary: &Boundary2D,
791 callback: &ProgressCallback,
792 ) -> Result<SolveResult<f64>> {
793 let start = Timer::now();
794 let mut result = SolveResult::new();
795 let mut placements = Vec::new();
796
797 let (b_min, b_max) = boundary.aabb();
799 let margin = self.config.margin;
800 let spacing = self.config.spacing;
801
802 let bound_min_x = b_min[0] + margin;
803 let bound_min_y = b_min[1] + margin;
804 let bound_max_x = b_max[0] - margin;
805 let bound_max_y = b_max[1] - margin;
806
807 let strip_width = bound_max_x - bound_min_x;
808 let strip_height = bound_max_y - bound_min_y;
809
810 let mut current_x = bound_min_x;
811 let mut current_y = bound_min_y;
812 let mut row_height = 0.0_f64;
813 let mut total_placed_area = 0.0;
814
815 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
817 let mut placed_count = 0usize;
818
819 callback(
821 ProgressInfo::new()
822 .with_phase("BLF Placement")
823 .with_items(0, total_pieces)
824 .with_elapsed(0),
825 );
826
827 for geom in geometries {
828 geom.validate()?;
829
830 let rotations = geom.rotations();
831 let rotation_angles: Vec<f64> = if rotations.is_empty() {
832 vec![0.0]
833 } else {
834 rotations
835 };
836
837 for instance in 0..geom.quantity() {
838 if self.cancelled.load(Ordering::Relaxed) {
839 result.computation_time_ms = start.elapsed_ms();
840 callback(
841 ProgressInfo::new()
842 .with_phase("Cancelled")
843 .with_items(placed_count, total_pieces)
844 .with_elapsed(result.computation_time_ms)
845 .finished(),
846 );
847 return Ok(result);
848 }
849
850 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
852 {
853 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
854 result.utilization = total_placed_area / boundary.measure();
855 result.computation_time_ms = start.elapsed_ms();
856 result.placements = placements;
857 callback(
858 ProgressInfo::new()
859 .with_phase("Time Limit Reached")
860 .with_items(placed_count, total_pieces)
861 .with_elapsed(result.computation_time_ms)
862 .finished(),
863 );
864 return Ok(result);
865 }
866
867 let mut best_fit: Option<(f64, f64, f64, f64, f64, [f64; 2])> = None;
868
869 for &rotation in &rotation_angles {
870 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
871 let g_width = g_max[0] - g_min[0];
872 let g_height = g_max[1] - g_min[1];
873
874 if g_width > strip_width || g_height > strip_height {
875 continue;
876 }
877
878 let mut place_x = current_x;
879 let mut place_y = current_y;
880
881 if place_x + g_width > bound_max_x {
882 place_x = bound_min_x;
883 place_y += row_height + spacing;
884 }
885
886 if place_y + g_height > bound_max_y {
887 continue;
888 }
889
890 let score = if place_x == bound_min_x && place_y > current_y {
891 place_y - bound_min_y + g_height
892 } else {
893 place_x - bound_min_x + g_width
894 };
895
896 let is_better = match &best_fit {
897 None => true,
898 Some((_, _, _, bx, by, _)) => {
899 let best_score = if *bx == bound_min_x && *by > current_y {
900 by - bound_min_y
901 } else {
902 bx - bound_min_x
903 };
904 score < best_score - 1e-6
905 }
906 };
907
908 if is_better {
909 best_fit = Some((rotation, g_width, g_height, place_x, place_y, g_min));
910 }
911 }
912
913 if let Some((rotation, g_width, g_height, place_x, place_y, g_min)) = best_fit {
914 if place_x == bound_min_x && place_y > current_y {
915 row_height = 0.0;
916 }
917
918 let origin_x = place_x - g_min[0];
920 let origin_y = place_y - g_min[1];
921
922 let geom_aabb = geom.aabb_at_rotation(rotation);
924 let boundary_aabb = (b_min, b_max);
925
926 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
927 origin_x,
928 origin_y,
929 geom_aabb,
930 boundary_aabb,
931 margin,
932 ) {
933 let placement = Placement::new_2d(
934 geom.id().clone(),
935 instance,
936 clamped_x,
937 clamped_y,
938 rotation,
939 );
940
941 placements.push(placement);
942 total_placed_area += geom.measure();
943 placed_count += 1;
944
945 current_x = place_x + g_width + spacing;
946 current_y = place_y;
947 row_height = row_height.max(g_height);
948
949 callback(
951 ProgressInfo::new()
952 .with_phase("BLF Placement")
953 .with_items(placed_count, total_pieces)
954 .with_utilization(total_placed_area / boundary.measure())
955 .with_elapsed(start.elapsed_ms()),
956 );
957 } else {
958 result.unplaced.push(geom.id().clone());
959 }
960 } else {
961 result.unplaced.push(geom.id().clone());
962 }
963 }
964 }
965
966 result.placements = placements;
967 result.boundaries_used = 1;
968 result.utilization = total_placed_area / boundary.measure();
969 result.computation_time_ms = start.elapsed_ms();
970
971 callback(
973 ProgressInfo::new()
974 .with_phase("Complete")
975 .with_items(placed_count, total_pieces)
976 .with_utilization(result.utilization)
977 .with_elapsed(result.computation_time_ms)
978 .finished(),
979 );
980
981 Ok(result)
982 }
983
984 fn nfp_guided_blf_with_progress(
986 &self,
987 geometries: &[Geometry2D],
988 boundary: &Boundary2D,
989 callback: &ProgressCallback,
990 ) -> Result<SolveResult<f64>> {
991 let start = Timer::now();
992 let mut result = SolveResult::new();
993 let mut placements = Vec::new();
994 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
995
996 let margin = self.config.margin;
997 let spacing = self.config.spacing;
998 let boundary_polygon = self.get_boundary_polygon_with_margin(boundary, margin);
999
1000 let mut total_placed_area = 0.0;
1001 let sample_step = self.compute_sample_step(geometries);
1002
1003 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
1005 let mut placed_count = 0usize;
1006
1007 callback(
1009 ProgressInfo::new()
1010 .with_phase("NFP Placement")
1011 .with_items(0, total_pieces)
1012 .with_elapsed(0),
1013 );
1014
1015 for geom in geometries {
1016 geom.validate()?;
1017
1018 let rotations = geom.rotations();
1019 let rotation_angles: Vec<f64> = if rotations.is_empty() {
1020 vec![0.0]
1021 } else {
1022 rotations
1023 };
1024
1025 for instance in 0..geom.quantity() {
1026 if self.cancelled.load(Ordering::Relaxed) {
1027 result.computation_time_ms = start.elapsed_ms();
1028 callback(
1029 ProgressInfo::new()
1030 .with_phase("Cancelled")
1031 .with_items(placed_count, total_pieces)
1032 .with_elapsed(result.computation_time_ms)
1033 .finished(),
1034 );
1035 return Ok(result);
1036 }
1037
1038 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
1040 {
1041 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
1042 result.utilization = total_placed_area / boundary.measure();
1043 result.computation_time_ms = start.elapsed_ms();
1044 result.placements = placements;
1045 callback(
1046 ProgressInfo::new()
1047 .with_phase("Time Limit Reached")
1048 .with_items(placed_count, total_pieces)
1049 .with_elapsed(result.computation_time_ms)
1050 .finished(),
1051 );
1052 return Ok(result);
1053 }
1054
1055 let mut best_placement: Option<(f64, f64, f64)> = None;
1056
1057 for &rotation in &rotation_angles {
1058 let ifp =
1059 match compute_ifp_with_margin(&boundary_polygon, geom, rotation, margin) {
1060 Ok(ifp) => ifp,
1061 Err(_) => continue,
1062 };
1063
1064 if ifp.is_empty() {
1065 continue;
1066 }
1067
1068 let mut nfps: Vec<Nfp> = Vec::new();
1069 for placed in &placed_geometries {
1070 let cache_key = (
1072 placed.geometry.id().as_str(),
1073 geom.id().as_str(),
1074 rotation - placed.rotation,
1075 );
1076
1077 let nfp_at_origin = match self.nfp_cache.get_or_compute(cache_key, || {
1080 let placed_at_origin = placed.geometry.clone();
1081 compute_nfp(&placed_at_origin, geom, rotation - placed.rotation)
1082 }) {
1083 Ok(nfp) => nfp,
1084 Err(_) => continue,
1085 };
1086
1087 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
1089 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
1090 let expanded = self.expand_nfp(&translated_nfp, spacing);
1091 nfps.push(expanded);
1092 }
1093
1094 let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
1095 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
1096
1097 if let Some((x, y)) =
1098 find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
1099 {
1100 let is_better = match best_placement {
1101 None => true,
1102 Some((best_x, best_y, _)) => {
1103 x < best_x - 1e-6 || (x < best_x + 1e-6 && y < best_y - 1e-6)
1104 }
1105 };
1106 if is_better {
1107 best_placement = Some((x, y, rotation));
1108 }
1109 }
1110 }
1111
1112 if let Some((x, y, rotation)) = best_placement {
1113 let geom_aabb = geom.aabb_at_rotation(rotation);
1115 let boundary_aabb = boundary.aabb();
1116
1117 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
1118 x,
1119 y,
1120 geom_aabb,
1121 boundary_aabb,
1122 margin,
1123 ) {
1124 let placement = Placement::new_2d(
1125 geom.id().clone(),
1126 instance,
1127 clamped_x,
1128 clamped_y,
1129 rotation,
1130 );
1131 placements.push(placement);
1132 placed_geometries.push(PlacedGeometry::new(
1133 geom.clone(),
1134 (clamped_x, clamped_y),
1135 rotation,
1136 ));
1137 total_placed_area += geom.measure();
1138 placed_count += 1;
1139
1140 callback(
1142 ProgressInfo::new()
1143 .with_phase("NFP Placement")
1144 .with_items(placed_count, total_pieces)
1145 .with_utilization(total_placed_area / boundary.measure())
1146 .with_elapsed(start.elapsed_ms()),
1147 );
1148 } else {
1149 result.unplaced.push(geom.id().clone());
1150 }
1151 } else {
1152 result.unplaced.push(geom.id().clone());
1153 }
1154 }
1155 }
1156
1157 result.placements = placements;
1158 result.boundaries_used = 1;
1159 result.utilization = total_placed_area / boundary.measure();
1160 result.computation_time_ms = start.elapsed_ms();
1161
1162 callback(
1164 ProgressInfo::new()
1165 .with_phase("Complete")
1166 .with_items(placed_count, total_pieces)
1167 .with_utilization(result.utilization)
1168 .with_elapsed(result.computation_time_ms)
1169 .finished(),
1170 );
1171
1172 Ok(result)
1173 }
1174
1175 fn validate_geometries(&self, geometries: &[Geometry2D]) -> Result<()> {
1186 use u_nesting_core::geometry::Geometry;
1187 for geom in geometries {
1188 geom.validate()?;
1189 }
1190 Ok(())
1191 }
1192
1193 pub fn solve_multi_strip(
1195 &self,
1196 geometries: &[Geometry2D],
1197 boundary: &Boundary2D,
1198 ) -> Result<SolveResult<f64>> {
1199 boundary.validate()?;
1200 self.validate_geometries(geometries)?;
1201 self.cancelled.store(false, Ordering::Relaxed);
1202
1203 let (b_min, b_max) = boundary.aabb();
1204 let strip_width = b_max[0] - b_min[0];
1205
1206 let mut final_result = SolveResult::new();
1207 let mut remaining_geometries: Vec<Geometry2D> = geometries.to_vec();
1208 let mut strip_index = 0;
1209 let max_strips = 100; let mut placed_total: std::collections::HashMap<String, usize> =
1214 std::collections::HashMap::new();
1215
1216 while !remaining_geometries.is_empty() && strip_index < max_strips {
1217 if self.cancelled.load(Ordering::Relaxed) {
1218 break;
1219 }
1220
1221 let strip_result = match self.config.strategy {
1223 Strategy::BottomLeftFill => self.bottom_left_fill(&remaining_geometries, boundary),
1224 Strategy::NfpGuided => self.nfp_guided_blf(&remaining_geometries, boundary),
1225 Strategy::GeneticAlgorithm => {
1226 self.genetic_algorithm(&remaining_geometries, boundary)
1227 }
1228 Strategy::Brkga => self.brkga(&remaining_geometries, boundary),
1229 Strategy::SimulatedAnnealing => {
1230 self.simulated_annealing(&remaining_geometries, boundary)
1231 }
1232 Strategy::Gdrr => self.gdrr(&remaining_geometries, boundary),
1233 Strategy::Alns => self.alns(&remaining_geometries, boundary),
1234 #[cfg(feature = "milp")]
1235 Strategy::MilpExact => self.milp_exact(&remaining_geometries, boundary),
1236 #[cfg(feature = "milp")]
1237 Strategy::HybridExact => self.hybrid_exact(&remaining_geometries, boundary),
1238 _ => self.nfp_guided_blf(&remaining_geometries, boundary),
1239 }?;
1240
1241 let strip_result =
1243 validate_and_filter_placements(strip_result, &remaining_geometries, boundary);
1244
1245 if strip_result.placements.is_empty() {
1246 break;
1249 }
1250
1251 let mut strip_placed: std::collections::HashMap<String, usize> =
1254 std::collections::HashMap::new();
1255
1256 for mut placement in strip_result.placements {
1258 let gid = placement.geometry_id.clone();
1259 let prior = placed_total.get(&gid).copied().unwrap_or(0);
1262 let in_strip = strip_placed.get(&gid).copied().unwrap_or(0);
1263 placement.instance = prior + in_strip;
1264 if !placement.position.is_empty() {
1266 placement.position[0] += strip_index as f64 * strip_width;
1267 }
1268 placement.boundary_index = strip_index;
1269 *strip_placed.entry(gid).or_insert(0) += 1;
1270 final_result.placements.push(placement);
1271 }
1272
1273 for (gid, cnt) in &strip_placed {
1277 *placed_total.entry(gid.clone()).or_insert(0) += cnt;
1278 }
1279 remaining_geometries = remaining_geometries
1280 .into_iter()
1281 .filter_map(|g| {
1282 let placed_here = strip_placed.get(g.id()).copied().unwrap_or(0);
1283 let new_quantity = g.quantity().saturating_sub(placed_here);
1284 if new_quantity == 0 {
1285 None
1286 } else {
1287 Some(g.with_quantity(new_quantity))
1288 }
1289 })
1290 .collect();
1291
1292 strip_index += 1;
1293 }
1294
1295 for g in &remaining_geometries {
1298 final_result.unplaced.push(g.id().clone());
1299 }
1300
1301 final_result.boundaries_used = strip_index;
1302 final_result.deduplicate_unplaced();
1303 final_result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1305
1306 let (b_min, b_max) = boundary.aabb();
1308 let strip_height = b_max[1] - b_min[1]; let mut strip_stats_map: std::collections::HashMap<usize, (f64, f64, usize)> =
1312 std::collections::HashMap::new(); for placement in &final_result.placements {
1315 let strip_idx = placement.boundary_index;
1316 if let Some(geom) = geometries.iter().find(|g| g.id() == &placement.geometry_id) {
1318 use u_nesting_core::geometry::Geometry;
1319 let piece_area = geom.measure();
1320 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1321 let (_g_min, g_max) = geom.aabb_at_rotation(rotation);
1322 let local_x = placement.position[0] - (strip_idx as f64 * strip_width);
1325 let right_edge = local_x + g_max[0];
1326
1327 let entry = strip_stats_map.entry(strip_idx).or_insert((0.0, 0.0, 0));
1328 entry.0 = entry.0.max(right_edge); entry.1 += piece_area; entry.2 += 1; }
1332 }
1333
1334 use u_nesting_core::result::StripStats;
1336 let mut strip_stats: Vec<StripStats> = strip_stats_map
1337 .into_iter()
1338 .map(|(idx, (used_length, piece_area, count))| StripStats {
1339 strip_index: idx,
1340 used_length,
1341 piece_area,
1342 piece_count: count,
1343 strip_width, strip_height, })
1346 .collect();
1347 strip_stats.sort_by_key(|s| s.strip_index);
1348
1349 let total_piece_area: f64 = strip_stats.iter().map(|s| s.piece_area).sum();
1352 let total_material_used: f64 = strip_stats
1353 .iter()
1354 .map(|s| s.strip_height * s.used_length)
1355 .sum();
1356
1357 final_result.strip_stats = strip_stats;
1358 final_result.total_piece_area = total_piece_area;
1359 final_result.total_material_used = total_material_used;
1360
1361 if total_material_used > 0.0 {
1362 final_result.utilization = total_piece_area / total_material_used;
1363 }
1364
1365 Ok(final_result)
1366 }
1367}
1368
1369impl Solver for Nester2D {
1370 type Geometry = Geometry2D;
1371 type Boundary = Boundary2D;
1372 type Scalar = f64;
1373
1374 fn solve(
1375 &self,
1376 geometries: &[Self::Geometry],
1377 boundary: &Self::Boundary,
1378 ) -> Result<SolveResult<f64>> {
1379 boundary.validate()?;
1380 self.validate_geometries(geometries)?;
1381
1382 self.cancelled.store(false, Ordering::Relaxed);
1384
1385 let initial_result = match self.config.strategy {
1386 Strategy::BottomLeftFill => self.bottom_left_fill(geometries, boundary),
1387 Strategy::NfpGuided => self.nfp_guided_blf(geometries, boundary),
1388 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
1389 Strategy::Brkga => self.brkga(geometries, boundary),
1390 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
1391 Strategy::Gdrr => self.gdrr(geometries, boundary),
1392 Strategy::Alns => self.alns(geometries, boundary),
1393 #[cfg(feature = "milp")]
1394 Strategy::MilpExact => self.milp_exact(geometries, boundary),
1395 #[cfg(feature = "milp")]
1396 Strategy::HybridExact => self.hybrid_exact(geometries, boundary),
1397 _ => {
1398 log::warn!(
1400 "Strategy {:?} not yet implemented, using NfpGuided",
1401 self.config.strategy
1402 );
1403 self.nfp_guided_blf(geometries, boundary)
1404 }
1405 }?;
1406
1407 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1409
1410 result.deduplicate_unplaced();
1412 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1415 Ok(result)
1416 }
1417
1418 fn solve_with_progress(
1419 &self,
1420 geometries: &[Self::Geometry],
1421 boundary: &Self::Boundary,
1422 callback: ProgressCallback,
1423 ) -> Result<SolveResult<f64>> {
1424 boundary.validate()?;
1425 self.validate_geometries(geometries)?;
1426
1427 self.cancelled.store(false, Ordering::Relaxed);
1429
1430 let initial_result = match self.config.strategy {
1431 Strategy::BottomLeftFill => {
1432 self.bottom_left_fill_with_progress(geometries, boundary, &callback)?
1433 }
1434 Strategy::NfpGuided => {
1435 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1436 }
1437 Strategy::GeneticAlgorithm => {
1438 let mut ga_config = GaConfig::default()
1444 .with_population_size(self.config.population_size.min(30))
1445 .with_max_generations(self.config.max_generations.min(50))
1446 .with_crossover_rate(self.config.crossover_rate)
1447 .with_mutation_rate(self.config.mutation_rate);
1448
1449 if self.config.time_limit_ms > 0 {
1451 ga_config = ga_config.with_time_limit(std::time::Duration::from_millis(
1452 self.config.time_limit_ms,
1453 ));
1454 }
1455
1456 let ga_result = run_ga_nesting_with_progress(
1457 geometries,
1458 boundary,
1459 &self.config,
1460 ga_config,
1461 self.cancelled.clone(),
1462 callback,
1463 );
1464 self.not_worse_than_blf(ga_result, geometries, boundary)
1469 }
1470 _ => {
1472 log::warn!(
1473 "Strategy {:?} not yet implemented, using NfpGuided",
1474 self.config.strategy
1475 );
1476 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1477 }
1478 };
1479
1480 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1482
1483 result.deduplicate_unplaced();
1485 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1488 Ok(result)
1489 }
1490
1491 fn cancel(&self) {
1492 self.cancelled.store(true, Ordering::Relaxed);
1493 }
1494}
1495
1496#[cfg(test)]
1497mod tests {
1498 use super::*;
1499 use crate::placement_utils::polygon_centroid;
1500
1501 #[test]
1502 fn test_simple_nesting() {
1503 let geometries = vec![
1504 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1505 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1506 ];
1507
1508 let boundary = Boundary2D::rectangle(100.0, 50.0);
1509 let nester = Nester2D::default_config();
1510
1511 let result = nester.solve(&geometries, &boundary).unwrap();
1512
1513 assert!(result.utilization > 0.0);
1514 assert!(result.placements.len() <= 5); }
1516
1517 #[test]
1518 fn test_placement_within_bounds() {
1519 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1520
1521 let boundary = Boundary2D::rectangle(50.0, 50.0);
1522 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1523 let nester = Nester2D::new(config);
1524
1525 let result = nester.solve(&geometries, &boundary).unwrap();
1526
1527 assert_eq!(result.placements.len(), 4);
1529 assert!(result.unplaced.is_empty());
1530
1531 for p in &result.placements {
1533 assert!(p.position[0] >= 5.0);
1534 assert!(p.position[1] >= 5.0);
1535 }
1536 }
1537
1538 #[test]
1539 fn test_nfp_guided_basic() {
1540 let geometries = vec![
1541 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1542 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1543 ];
1544
1545 let boundary = Boundary2D::rectangle(100.0, 50.0);
1546 let config = Config::default().with_strategy(Strategy::NfpGuided);
1547 let nester = Nester2D::new(config);
1548
1549 let result = nester.solve(&geometries, &boundary).unwrap();
1550
1551 assert!(result.utilization > 0.0);
1552 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1554 }
1555
1556 #[test]
1557 fn test_nfp_guided_with_spacing() {
1558 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1559
1560 let boundary = Boundary2D::rectangle(50.0, 50.0);
1561 let config = Config::default()
1562 .with_strategy(Strategy::NfpGuided)
1563 .with_margin(2.0)
1564 .with_spacing(3.0);
1565 let nester = Nester2D::new(config);
1566
1567 let result = nester.solve(&geometries, &boundary).unwrap();
1568
1569 assert_eq!(result.placements.len(), 4);
1571 assert!(result.unplaced.is_empty());
1572
1573 assert!(result.utilization > 0.0);
1575 }
1576
1577 #[test]
1578 fn test_nfp_guided_no_overlap() {
1579 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1580
1581 let boundary = Boundary2D::rectangle(100.0, 100.0);
1582 let config = Config::default().with_strategy(Strategy::NfpGuided);
1583 let nester = Nester2D::new(config);
1584
1585 let result = nester.solve(&geometries, &boundary).unwrap();
1586
1587 assert_eq!(result.placements.len(), 3);
1588
1589 for i in 0..result.placements.len() {
1591 for j in (i + 1)..result.placements.len() {
1592 let p1 = &result.placements[i];
1593 let p2 = &result.placements[j];
1594
1595 let r1_min_x = p1.position[0];
1597 let r1_max_x = p1.position[0] + 20.0;
1598 let r1_min_y = p1.position[1];
1599 let r1_max_y = p1.position[1] + 20.0;
1600
1601 let r2_min_x = p2.position[0];
1602 let r2_max_x = p2.position[0] + 20.0;
1603 let r2_min_y = p2.position[1];
1604 let r2_max_y = p2.position[1] + 20.0;
1605
1606 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1608 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1609
1610 assert!(
1611 !(overlaps_x && overlaps_y),
1612 "Placements {} and {} overlap",
1613 i,
1614 j
1615 );
1616 }
1617 }
1618 }
1619
1620 #[test]
1621 fn test_nfp_guided_utilization() {
1622 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
1624
1625 let boundary = Boundary2D::rectangle(100.0, 50.0);
1626 let config = Config::default().with_strategy(Strategy::NfpGuided);
1627 let nester = Nester2D::new(config);
1628
1629 let result = nester.solve(&geometries, &boundary).unwrap();
1630
1631 assert_eq!(result.placements.len(), 4);
1633
1634 assert!(result.utilization > 0.45);
1636 }
1637
1638 #[test]
1639 fn test_polygon_centroid() {
1640 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1642 let (cx, cy) = polygon_centroid(&square);
1643 assert!((cx - 5.0).abs() < 0.01);
1644 assert!((cy - 5.0).abs() < 0.01);
1645
1646 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
1647 let (cx, cy) = polygon_centroid(&triangle);
1648 assert!((cx - 3.0).abs() < 0.01);
1649 assert!((cy - 2.0).abs() < 0.01);
1650 }
1651
1652 #[test]
1653 fn test_ga_strategy_basic() {
1654 let geometries = vec![
1655 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1656 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1657 ];
1658
1659 let boundary = Boundary2D::rectangle(100.0, 50.0);
1660 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1661 let nester = Nester2D::new(config);
1662
1663 let result = nester.solve(&geometries, &boundary).unwrap();
1664
1665 assert!(result.utilization > 0.0);
1666 assert!(!result.placements.is_empty());
1667 assert!(result.generations.is_some());
1669 assert!(result.best_fitness.is_some());
1670 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
1671 }
1672
1673 #[test]
1674 fn test_ga_strategy_all_placed() {
1675 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1677
1678 let boundary = Boundary2D::rectangle(100.0, 100.0);
1679 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1680 let nester = Nester2D::new(config);
1681
1682 let result = nester.solve(&geometries, &boundary).unwrap();
1683
1684 assert_eq!(result.placements.len(), 4);
1686 assert!(result.unplaced.is_empty());
1687 }
1688
1689 #[test]
1690 fn test_brkga_strategy_basic() {
1691 let geometries = vec![
1692 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1693 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1694 ];
1695
1696 let boundary = Boundary2D::rectangle(100.0, 50.0);
1697 let config = Config::default().with_strategy(Strategy::Brkga);
1698 let nester = Nester2D::new(config);
1699
1700 let result = nester.solve(&geometries, &boundary).unwrap();
1701
1702 assert!(result.utilization > 0.0);
1703 assert!(!result.placements.is_empty());
1704 assert!(result.generations.is_some());
1706 assert!(result.best_fitness.is_some());
1707 assert!(result.strategy == Some("BRKGA".to_string()));
1708 }
1709
1710 #[test]
1711 fn test_brkga_strategy_all_placed() {
1712 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1714
1715 let boundary = Boundary2D::rectangle(100.0, 100.0);
1716 let config = Config::default()
1718 .with_strategy(Strategy::Brkga)
1719 .with_time_limit(30000); let nester = Nester2D::new(config);
1721
1722 let result = nester.solve(&geometries, &boundary).unwrap();
1723
1724 assert!(
1727 result.placements.len() >= 3,
1728 "Expected at least 3 placements, got {}",
1729 result.placements.len()
1730 );
1731 }
1732
1733 #[test]
1734 fn test_gdrr_strategy_basic() {
1735 let geometries = vec![
1736 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1737 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1738 ];
1739
1740 let boundary = Boundary2D::rectangle(100.0, 50.0);
1741 let config = Config::default().with_strategy(Strategy::Gdrr);
1742 let nester = Nester2D::new(config);
1743
1744 let result = nester.solve(&geometries, &boundary).unwrap();
1745
1746 assert!(result.utilization > 0.0);
1747 assert!(!result.placements.is_empty());
1748 assert!(result.iterations.is_some());
1750 assert!(result.best_fitness.is_some());
1751 assert!(result.strategy == Some("GDRR".to_string()));
1752 }
1753
1754 #[test]
1755 fn test_gdrr_strategy_all_placed() {
1756 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1758
1759 let boundary = Boundary2D::rectangle(100.0, 100.0);
1760 let config = Config::default().with_strategy(Strategy::Gdrr);
1761 let nester = Nester2D::new(config);
1762
1763 let result = nester.solve(&geometries, &boundary).unwrap();
1764
1765 assert_eq!(result.placements.len(), 4);
1767 assert!(result.unplaced.is_empty());
1768 }
1769
1770 #[test]
1771 fn test_alns_strategy_basic() {
1772 let geometries = vec![
1773 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1774 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1775 ];
1776
1777 let boundary = Boundary2D::rectangle(100.0, 50.0);
1778 let config = Config::default().with_strategy(Strategy::Alns);
1779 let nester = Nester2D::new(config);
1780
1781 let result = nester.solve(&geometries, &boundary).unwrap();
1782
1783 assert!(result.utilization > 0.0);
1784 assert!(!result.placements.is_empty());
1785 assert!(result.iterations.is_some());
1787 assert!(result.best_fitness.is_some());
1788 assert!(result.strategy == Some("ALNS".to_string()));
1789 }
1790
1791 #[test]
1792 fn test_alns_strategy_all_placed() {
1793 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1795
1796 let boundary = Boundary2D::rectangle(100.0, 100.0);
1797 let config = Config::default().with_strategy(Strategy::Alns);
1798 let nester = Nester2D::new(config);
1799
1800 let result = nester.solve(&geometries, &boundary).unwrap();
1801
1802 assert_eq!(result.placements.len(), 4);
1804 assert!(result.unplaced.is_empty());
1805 }
1806
1807 #[test]
1808 fn test_blf_rotation_optimization() {
1809 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
1812 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
1814
1815 let boundary = Boundary2D::rectangle(35.0, 95.0);
1818 let nester = Nester2D::default_config();
1819
1820 let result = nester.solve(&geometries, &boundary).unwrap();
1821
1822 assert_eq!(
1824 result.placements.len(),
1825 3,
1826 "All pieces should be placed with rotation optimization"
1827 );
1828 assert!(result.unplaced.is_empty());
1829 }
1830
1831 #[test]
1832 fn test_blf_selects_best_rotation() {
1833 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
1835 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
1837
1838 let boundary = Boundary2D::rectangle(45.0, 50.0);
1842 let nester = Nester2D::default_config();
1843
1844 let result = nester.solve(&geometries, &boundary).unwrap();
1845
1846 assert_eq!(result.placements.len(), 2);
1847 assert!(result.unplaced.is_empty());
1848 }
1849
1850 #[test]
1851 fn test_progress_callback_blf() {
1852 use std::sync::atomic::{AtomicUsize, Ordering};
1853 use std::sync::Arc;
1854
1855 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1856 let boundary = Boundary2D::rectangle(50.0, 50.0);
1857 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1858 let nester = Nester2D::new(config);
1859
1860 let callback_count = Arc::new(AtomicUsize::new(0));
1861 let callback_count_clone = callback_count.clone();
1862 let last_items_placed = Arc::new(AtomicUsize::new(0));
1863 let last_items_placed_clone = last_items_placed.clone();
1864
1865 let callback: ProgressCallback = Box::new(move |info| {
1866 callback_count_clone.fetch_add(1, Ordering::Relaxed);
1867 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
1868 });
1869
1870 let result = nester
1871 .solve_with_progress(&geometries, &boundary, callback)
1872 .unwrap();
1873
1874 let count = callback_count.load(Ordering::Relaxed);
1876 assert!(
1877 count >= 5,
1878 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
1879 count
1880 );
1881
1882 let final_placed = last_items_placed.load(Ordering::Relaxed);
1884 assert_eq!(final_placed, 4, "Should report 4 items placed");
1885
1886 assert_eq!(result.placements.len(), 4);
1888 }
1889
1890 #[test]
1891 fn test_progress_callback_nfp() {
1892 use std::sync::atomic::{AtomicUsize, Ordering};
1893 use std::sync::Arc;
1894
1895 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
1896 let boundary = Boundary2D::rectangle(50.0, 50.0);
1897 let config = Config::default().with_strategy(Strategy::NfpGuided);
1898 let nester = Nester2D::new(config);
1899
1900 let callback_count = Arc::new(AtomicUsize::new(0));
1901 let callback_count_clone = callback_count.clone();
1902
1903 let callback: ProgressCallback = Box::new(move |info| {
1904 callback_count_clone.fetch_add(1, Ordering::Relaxed);
1905 assert!(info.items_placed <= info.total_items);
1906 });
1907
1908 let result = nester
1909 .solve_with_progress(&geometries, &boundary, callback)
1910 .unwrap();
1911
1912 let count = callback_count.load(Ordering::Relaxed);
1914 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
1915
1916 assert_eq!(result.placements.len(), 2);
1918 }
1919
1920 #[test]
1921 fn test_time_limit_honored() {
1922 let geometries: Vec<Geometry2D> = (0..100)
1924 .map(|i| Geometry2D::rectangle(format!("R{}", i), 5.0, 5.0))
1925 .collect();
1926 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
1927
1928 let config = Config::default()
1930 .with_strategy(Strategy::BottomLeftFill)
1931 .with_time_limit(1);
1932 let nester = Nester2D::new(config);
1933
1934 let result = nester.solve(&geometries, &boundary).unwrap();
1935
1936 assert!(
1939 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
1941 result.computation_time_ms
1942 );
1943 }
1944
1945 #[test]
1946 fn test_time_limit_zero_unlimited() {
1947 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1949 let boundary = Boundary2D::rectangle(50.0, 50.0);
1950
1951 let config = Config::default()
1952 .with_strategy(Strategy::BottomLeftFill)
1953 .with_time_limit(0); let nester = Nester2D::new(config);
1955
1956 let result = nester.solve(&geometries, &boundary).unwrap();
1957
1958 assert_eq!(result.placements.len(), 4);
1960 }
1961
1962 #[test]
1963 fn test_blf_bounds_clamping() {
1964 let gear_like = Geometry2D::new("gear")
1968 .with_polygon(vec![
1969 (50.0, 5.0), (65.0, 15.0),
1971 (77.0, 18.0),
1972 (80.0, 32.0),
1973 (95.0, 50.0), (80.0, 68.0),
1975 (77.0, 82.0),
1976 (65.0, 85.0),
1977 (50.0, 95.0), (35.0, 85.0),
1979 (23.0, 82.0),
1980 (20.0, 68.0),
1981 (5.0, 50.0), (20.0, 32.0),
1983 (23.0, 18.0),
1984 (35.0, 15.0),
1985 ])
1986 .with_quantity(1);
1987
1988 let boundary = Boundary2D::rectangle(100.0, 100.0);
1990
1991 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1992 let nester = Nester2D::new(config);
1993
1994 let result = nester
1995 .solve(std::slice::from_ref(&gear_like), &boundary)
1996 .unwrap();
1997
1998 assert_eq!(result.placements.len(), 1);
1999 let placement = &result.placements[0];
2000
2001 let origin_x = placement.position[0];
2003 let origin_y = placement.position[1];
2004
2005 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2007
2008 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2010
2011 let actual_min_x = origin_x + g_min[0];
2013 let actual_max_x = origin_x + g_max[0];
2014 let actual_min_y = origin_y + g_min[1];
2015 let actual_max_y = origin_y + g_max[1];
2016
2017 assert!(
2019 actual_min_x >= 0.0,
2020 "Left edge {} should be >= 0",
2021 actual_min_x
2022 );
2023 assert!(
2024 actual_max_x <= 100.0,
2025 "Right edge {} should be <= 100",
2026 actual_max_x
2027 );
2028 assert!(
2029 actual_min_y >= 0.0,
2030 "Bottom edge {} should be >= 0",
2031 actual_min_y
2032 );
2033 assert!(
2034 actual_max_y <= 100.0,
2035 "Top edge {} should be <= 100",
2036 actual_max_y
2037 );
2038 }
2039
2040 #[test]
2041 fn test_blf_bounds_clamping_many_pieces() {
2042 let gear_like = Geometry2D::new("gear")
2045 .with_polygon(vec![
2046 (50.0, 5.0),
2047 (65.0, 15.0),
2048 (77.0, 18.0),
2049 (80.0, 32.0),
2050 (95.0, 50.0),
2051 (80.0, 68.0),
2052 (77.0, 82.0),
2053 (65.0, 85.0),
2054 (50.0, 95.0),
2055 (35.0, 85.0),
2056 (23.0, 82.0),
2057 (20.0, 68.0),
2058 (5.0, 50.0),
2059 (20.0, 32.0),
2060 (23.0, 18.0),
2061 (35.0, 15.0),
2062 ])
2063 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
2067
2068 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2069 let nester = Nester2D::new(config);
2070
2071 let result = nester
2072 .solve(std::slice::from_ref(&gear_like), &boundary)
2073 .unwrap();
2074
2075 for (i, placement) in result.placements.iter().enumerate() {
2077 let origin_x = placement.position[0];
2078 let origin_y = placement.position[1];
2079 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2080
2081 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2082
2083 let actual_min_x = origin_x + g_min[0];
2084 let actual_max_x = origin_x + g_max[0];
2085 let actual_min_y = origin_y + g_min[1];
2086 let actual_max_y = origin_y + g_max[1];
2087
2088 assert!(
2089 actual_min_x >= -0.01,
2090 "Piece {}: Left edge {} should be >= 0",
2091 i,
2092 actual_min_x
2093 );
2094 assert!(
2095 actual_max_x <= 500.01,
2096 "Piece {}: Right edge {} should be <= 500",
2097 i,
2098 actual_max_x
2099 );
2100 assert!(
2101 actual_min_y >= -0.01,
2102 "Piece {}: Bottom edge {} should be >= 0",
2103 i,
2104 actual_min_y
2105 );
2106 assert!(
2107 actual_max_y <= 500.01,
2108 "Piece {}: Top edge {} should be <= 500",
2109 i,
2110 actual_max_y
2111 );
2112 }
2113 }
2114
2115 #[test]
2116 fn test_blf_bounds_trace() {
2117 let gear = Geometry2D::new("gear").with_polygon(vec![
2119 (50.0, 5.0),
2120 (65.0, 15.0),
2121 (77.0, 18.0),
2122 (80.0, 32.0),
2123 (95.0, 50.0),
2124 (80.0, 68.0),
2125 (77.0, 82.0),
2126 (65.0, 85.0),
2127 (50.0, 95.0),
2128 (35.0, 85.0),
2129 (23.0, 82.0),
2130 (20.0, 68.0),
2131 (5.0, 50.0),
2132 (20.0, 32.0),
2133 (23.0, 18.0),
2134 (35.0, 15.0),
2135 ]);
2136
2137 let (g_min, g_max) = gear.aabb();
2139 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
2140 assert!(
2141 (g_min[0] - 5.0).abs() < 0.01,
2142 "g_min[0] should be 5, got {}",
2143 g_min[0]
2144 );
2145 assert!(
2146 (g_max[0] - 95.0).abs() < 0.01,
2147 "g_max[0] should be 95, got {}",
2148 g_max[0]
2149 );
2150
2151 let b_max_x = 500.0;
2153 let margin = 0.0;
2154 let max_valid_x = b_max_x - margin - g_max[0];
2155 println!(
2156 "max_valid_x = {} - {} - {} = {}",
2157 b_max_x, margin, g_max[0], max_valid_x
2158 );
2159 assert!(
2160 (max_valid_x - 405.0).abs() < 0.01,
2161 "max_valid_x should be 405, got {}",
2162 max_valid_x
2163 );
2164
2165 let boundary = Boundary2D::rectangle(500.0, 500.0);
2167 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2168 let nester = Nester2D::new(config);
2169
2170 let result = nester
2171 .solve(&[gear.clone().with_quantity(1)], &boundary)
2172 .unwrap();
2173
2174 assert_eq!(result.placements.len(), 1);
2175 let p = &result.placements[0];
2176 let origin_x = p.position[0];
2177 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2178
2179 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2180 let actual_max_x = origin_x + g_max_r[0];
2181
2182 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2183 println!(
2184 "At rotation {}: g_min={:?}, g_max={:?}",
2185 rotation, g_min_r, g_max_r
2186 );
2187 println!(
2188 "Actual max x: {} + {} = {}",
2189 origin_x, g_max_r[0], actual_max_x
2190 );
2191
2192 assert!(
2193 actual_max_x <= 500.01,
2194 "Geometry exceeds boundary: max_x={} > 500",
2195 actual_max_x
2196 );
2197 }
2198
2199 #[test]
2200 fn test_blf_bounds_many_pieces_direct() {
2201 let gear = Geometry2D::new("gear")
2203 .with_polygon(vec![
2204 (50.0, 5.0),
2205 (65.0, 15.0),
2206 (77.0, 18.0),
2207 (80.0, 32.0),
2208 (95.0, 50.0),
2209 (80.0, 68.0),
2210 (77.0, 82.0),
2211 (65.0, 85.0),
2212 (50.0, 95.0),
2213 (35.0, 85.0),
2214 (23.0, 82.0),
2215 (20.0, 68.0),
2216 (5.0, 50.0),
2217 (20.0, 32.0),
2218 (23.0, 18.0),
2219 (35.0, 15.0),
2220 ])
2221 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2224 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2225 let nester = Nester2D::new(config);
2226
2227 let result = nester
2228 .solve(std::slice::from_ref(&gear), &boundary)
2229 .unwrap();
2230
2231 println!("Placed {} pieces", result.placements.len());
2232
2233 for (i, p) in result.placements.iter().enumerate() {
2235 let origin_x = p.position[0];
2236 let origin_y = p.position[1];
2237 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2238
2239 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2240
2241 let actual_min_x = origin_x + g_min_r[0];
2242 let actual_max_x = origin_x + g_max_r[0];
2243 let actual_min_y = origin_y + g_min_r[1];
2244 let actual_max_y = origin_y + g_max_r[1];
2245
2246 println!(
2247 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2248 i,
2249 origin_x,
2250 origin_y,
2251 rotation,
2252 actual_min_x,
2253 actual_max_x,
2254 actual_min_y,
2255 actual_max_y
2256 );
2257
2258 assert!(
2259 actual_max_x <= 500.01,
2260 "Piece {}: Right edge {} > 500",
2261 i,
2262 actual_max_x
2263 );
2264 assert!(
2265 actual_max_y <= 500.01,
2266 "Piece {}: Top edge {} > 500",
2267 i,
2268 actual_max_y
2269 );
2270 }
2271 }
2272
2273 #[test]
2274 fn test_blf_bounds_multi_strip() {
2275 let gear = Geometry2D::new("gear")
2277 .with_polygon(vec![
2278 (50.0, 5.0),
2279 (65.0, 15.0),
2280 (77.0, 18.0),
2281 (80.0, 32.0),
2282 (95.0, 50.0),
2283 (80.0, 68.0),
2284 (77.0, 82.0),
2285 (65.0, 85.0),
2286 (50.0, 95.0),
2287 (35.0, 85.0),
2288 (23.0, 82.0),
2289 (20.0, 68.0),
2290 (5.0, 50.0),
2291 (20.0, 32.0),
2292 (23.0, 18.0),
2293 (35.0, 15.0),
2294 ])
2295 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2298 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2299 let nester = Nester2D::new(config);
2300
2301 let result = nester
2303 .solve_multi_strip(std::slice::from_ref(&gear), &boundary)
2304 .unwrap();
2305
2306 println!(
2307 "Placed {} pieces across {} strips",
2308 result.placements.len(),
2309 result.boundaries_used
2310 );
2311
2312 let strip_width = 500.0;
2314 for (i, p) in result.placements.iter().enumerate() {
2315 let origin_x = p.position[0];
2316 let origin_y = p.position[1];
2317 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2318 let strip_idx = p.boundary_index;
2319
2320 let local_x = origin_x - (strip_idx as f64 * strip_width);
2322
2323 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2324
2325 let local_max_x = local_x + g_max_r[0];
2326 let local_max_y = origin_y + g_max_r[1];
2327
2328 println!(
2329 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2330 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2331 );
2332
2333 assert!(
2334 local_max_x <= 500.01,
2335 "Piece {}: In strip {}, local right edge {:.1} > 500",
2336 i,
2337 strip_idx,
2338 local_max_x
2339 );
2340 assert!(
2341 local_max_y <= 500.01,
2342 "Piece {}: Top edge {:.1} > 500",
2343 i,
2344 local_max_y
2345 );
2346 }
2347 }
2348
2349 #[test]
2350 fn test_blf_bounds_mixed_shapes() {
2351 let shapes = vec![
2353 Geometry2D::new("shape0")
2355 .with_polygon(vec![
2356 (0.0, 0.0),
2357 (180.0, 0.0),
2358 (195.0, 15.0),
2359 (200.0, 50.0),
2360 (200.0, 150.0),
2361 (195.0, 185.0),
2362 (180.0, 200.0),
2363 (20.0, 200.0),
2364 (5.0, 185.0),
2365 (0.0, 150.0),
2366 (0.0, 50.0),
2367 (5.0, 15.0),
2368 ])
2369 .with_quantity(2),
2370 Geometry2D::new("shape1")
2372 .with_polygon(vec![
2373 (60.0, 0.0),
2374 (85.0, 7.0),
2375 (104.0, 25.0),
2376 (118.0, 50.0),
2377 (120.0, 60.0),
2378 (118.0, 70.0),
2379 (104.0, 95.0),
2380 (85.0, 113.0),
2381 (60.0, 120.0),
2382 (35.0, 113.0),
2383 (16.0, 95.0),
2384 (2.0, 70.0),
2385 (0.0, 60.0),
2386 (2.0, 50.0),
2387 (16.0, 25.0),
2388 (35.0, 7.0),
2389 ])
2390 .with_quantity(4),
2391 Geometry2D::new("shape2")
2393 .with_polygon(vec![
2394 (0.0, 0.0),
2395 (80.0, 0.0),
2396 (80.0, 20.0),
2397 (20.0, 20.0),
2398 (20.0, 80.0),
2399 (0.0, 80.0),
2400 ])
2401 .with_quantity(6),
2402 Geometry2D::new("shape3")
2404 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2405 .with_quantity(6),
2406 Geometry2D::new("shape4")
2408 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2409 .with_quantity(4),
2410 Geometry2D::new("shape5")
2412 .with_polygon(vec![
2413 (15.0, 0.0),
2414 (45.0, 0.0),
2415 (60.0, 26.0),
2416 (45.0, 52.0),
2417 (15.0, 52.0),
2418 (0.0, 26.0),
2419 ])
2420 .with_quantity(8),
2421 Geometry2D::new("shape6")
2423 .with_polygon(vec![
2424 (0.0, 0.0),
2425 (90.0, 0.0),
2426 (90.0, 12.0),
2427 (55.0, 12.0),
2428 (55.0, 60.0),
2429 (35.0, 60.0),
2430 (35.0, 12.0),
2431 (0.0, 12.0),
2432 ])
2433 .with_quantity(4),
2434 Geometry2D::new("shape7")
2436 .with_polygon(vec![
2437 (0.0, 10.0),
2438 (10.0, 0.0),
2439 (70.0, 0.0),
2440 (80.0, 10.0),
2441 (80.0, 70.0),
2442 (70.0, 80.0),
2443 (10.0, 80.0),
2444 (0.0, 70.0),
2445 ])
2446 .with_quantity(3),
2447 Geometry2D::new("shape8_gear")
2449 .with_polygon(vec![
2450 (50.0, 5.0),
2451 (65.0, 15.0),
2452 (77.0, 18.0),
2453 (80.0, 32.0),
2454 (95.0, 50.0),
2455 (80.0, 68.0),
2456 (77.0, 82.0),
2457 (65.0, 85.0),
2458 (50.0, 95.0),
2459 (35.0, 85.0),
2460 (23.0, 82.0),
2461 (20.0, 68.0),
2462 (5.0, 50.0),
2463 (20.0, 32.0),
2464 (23.0, 18.0),
2465 (35.0, 15.0),
2466 ])
2467 .with_quantity(13),
2468 ];
2469
2470 let boundary = Boundary2D::rectangle(500.0, 500.0);
2472 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2473 let nester = Nester2D::new(config);
2474
2475 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2476
2477 println!(
2478 "Placed {} pieces across {} strips",
2479 result.placements.len(),
2480 result.boundaries_used
2481 );
2482
2483 let strip_width = 500.0;
2485 let gear_aabb = shapes[8].aabb();
2486 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2487
2488 let mut violations = Vec::new();
2489 for p in &result.placements {
2490 if p.geometry_id.as_str().starts_with("shape8") {
2491 let origin_x = p.position[0];
2492 let _origin_y = p.position[1];
2493 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2494 let strip_idx = p.boundary_index;
2495 let local_x = origin_x - (strip_idx as f64 * strip_width);
2496
2497 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2498 let local_max_x = local_x + g_max_r[0];
2499
2500 println!(
2501 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2502 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2503 );
2504
2505 if local_max_x > 500.01 {
2506 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2507 }
2508 }
2509 }
2510
2511 assert!(
2512 violations.is_empty(),
2513 "Found {} Gear pieces exceeding boundary: {:?}",
2514 violations.len(),
2515 violations
2516 );
2517 }
2518
2519 #[test]
2520 fn test_blf_bounds_expanded_like_benchmark() {
2521 type ShapeDef = (Vec<(f64, f64)>, usize, Vec<f64>);
2525 let shape_defs: Vec<ShapeDef> = vec![
2526 (
2527 vec![
2528 (0.0, 0.0),
2529 (180.0, 0.0),
2530 (195.0, 15.0),
2531 (200.0, 50.0),
2532 (200.0, 150.0),
2533 (195.0, 185.0),
2534 (180.0, 200.0),
2535 (20.0, 200.0),
2536 (5.0, 185.0),
2537 (0.0, 150.0),
2538 (0.0, 50.0),
2539 (5.0, 15.0),
2540 ],
2541 2,
2542 vec![0.0, 90.0, 180.0, 270.0],
2543 ),
2544 (
2545 vec![
2546 (60.0, 0.0),
2547 (85.0, 7.0),
2548 (104.0, 25.0),
2549 (118.0, 50.0),
2550 (120.0, 60.0),
2551 (118.0, 70.0),
2552 (104.0, 95.0),
2553 (85.0, 113.0),
2554 (60.0, 120.0),
2555 (35.0, 113.0),
2556 (16.0, 95.0),
2557 (2.0, 70.0),
2558 (0.0, 60.0),
2559 (2.0, 50.0),
2560 (16.0, 25.0),
2561 (35.0, 7.0),
2562 ],
2563 4,
2564 vec![0.0, 45.0, 90.0, 135.0],
2565 ),
2566 (
2567 vec![
2568 (0.0, 0.0),
2569 (80.0, 0.0),
2570 (80.0, 20.0),
2571 (20.0, 20.0),
2572 (20.0, 80.0),
2573 (0.0, 80.0),
2574 ],
2575 6,
2576 vec![0.0, 90.0, 180.0, 270.0],
2577 ),
2578 (
2579 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
2580 6,
2581 vec![0.0, 90.0, 180.0, 270.0],
2582 ),
2583 (
2584 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
2585 4,
2586 vec![0.0, 90.0],
2587 ),
2588 (
2589 vec![
2590 (15.0, 0.0),
2591 (45.0, 0.0),
2592 (60.0, 26.0),
2593 (45.0, 52.0),
2594 (15.0, 52.0),
2595 (0.0, 26.0),
2596 ],
2597 8,
2598 vec![0.0, 60.0, 120.0],
2599 ),
2600 (
2601 vec![
2602 (0.0, 0.0),
2603 (90.0, 0.0),
2604 (90.0, 12.0),
2605 (55.0, 12.0),
2606 (55.0, 60.0),
2607 (35.0, 60.0),
2608 (35.0, 12.0),
2609 (0.0, 12.0),
2610 ],
2611 4,
2612 vec![0.0, 90.0, 180.0, 270.0],
2613 ),
2614 (
2615 vec![
2616 (0.0, 10.0),
2617 (10.0, 0.0),
2618 (70.0, 0.0),
2619 (80.0, 10.0),
2620 (80.0, 70.0),
2621 (70.0, 80.0),
2622 (10.0, 80.0),
2623 (0.0, 70.0),
2624 ],
2625 3,
2626 vec![0.0, 90.0],
2627 ),
2628 (
2630 vec![
2631 (50.0, 5.0),
2632 (65.0, 15.0),
2633 (77.0, 18.0),
2634 (80.0, 32.0),
2635 (95.0, 50.0),
2636 (80.0, 68.0),
2637 (77.0, 82.0),
2638 (65.0, 85.0),
2639 (50.0, 95.0),
2640 (35.0, 85.0),
2641 (23.0, 82.0),
2642 (20.0, 68.0),
2643 (5.0, 50.0),
2644 (20.0, 32.0),
2645 (23.0, 18.0),
2646 (35.0, 15.0),
2647 ],
2648 13,
2649 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
2650 ),
2651 ];
2652
2653 let mut geometries = Vec::new();
2655 let mut piece_id = 0;
2656 for (vertices, demand, rotations) in shape_defs.iter() {
2657 for _ in 0..*demand {
2658 let geom = Geometry2D::new(format!("piece_{}", piece_id))
2659 .with_polygon(vertices.clone())
2660 .with_rotations_deg(rotations.clone());
2661 geometries.push(geom);
2662 piece_id += 1;
2663 }
2664 }
2665
2666 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
2668 let (gear_min, gear_max) = gear_geom.aabb();
2669 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
2670
2671 let boundary = Boundary2D::rectangle(500.0, 500.0);
2672 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2673 let nester = Nester2D::new(config);
2674
2675 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
2676
2677 println!(
2678 "Placed {} pieces across {} strips",
2679 result.placements.len(),
2680 result.boundaries_used
2681 );
2682
2683 let strip_width = 500.0;
2685 let mut violations = Vec::new();
2686
2687 for p in &result.placements {
2688 let id_num: usize = p
2689 .geometry_id
2690 .as_str()
2691 .strip_prefix("piece_")
2692 .and_then(|s| s.parse().ok())
2693 .unwrap_or(0);
2694
2695 if (37..=49).contains(&id_num) {
2697 let origin_x = p.position[0];
2698 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2699 let strip_idx = p.boundary_index;
2700 let local_x = origin_x - (strip_idx as f64 * strip_width);
2701
2702 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
2703 let local_max_x = local_x + g_max_r[0];
2704
2705 println!(
2706 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2707 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2708 );
2709
2710 if local_max_x > 500.01 {
2711 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2712 }
2713 }
2714 }
2715
2716 assert!(
2717 violations.is_empty(),
2718 "Found {} Gear pieces exceeding boundary: {:?}",
2719 violations.len(),
2720 violations
2721 );
2722 }
2723
2724 fn aabbs_overlap(
2726 a_min: [f64; 2],
2727 a_max: [f64; 2],
2728 b_min: [f64; 2],
2729 b_max: [f64; 2],
2730 tolerance: f64,
2731 ) -> bool {
2732 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
2734 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
2735 x_overlap && y_overlap
2736 }
2737
2738 #[test]
2740 fn test_all_strategies_boundary_and_overlap() {
2741 use std::collections::HashMap;
2742
2743 let shapes = vec![
2745 Geometry2D::new("shape0")
2746 .with_polygon(vec![
2747 (0.0, 0.0),
2748 (180.0, 0.0),
2749 (195.0, 15.0),
2750 (200.0, 50.0),
2751 (200.0, 150.0),
2752 (195.0, 185.0),
2753 (180.0, 200.0),
2754 (20.0, 200.0),
2755 (5.0, 185.0),
2756 (0.0, 150.0),
2757 (0.0, 50.0),
2758 (5.0, 15.0),
2759 ])
2760 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2761 .with_quantity(2),
2762 Geometry2D::new("shape1_flange")
2763 .with_polygon(vec![
2764 (60.0, 0.0),
2765 (85.0, 7.0),
2766 (104.0, 25.0),
2767 (118.0, 50.0),
2768 (120.0, 60.0),
2769 (118.0, 70.0),
2770 (104.0, 95.0),
2771 (85.0, 113.0),
2772 (60.0, 120.0),
2773 (35.0, 113.0),
2774 (16.0, 95.0),
2775 (2.0, 70.0),
2776 (0.0, 60.0),
2777 (2.0, 50.0),
2778 (16.0, 25.0),
2779 (35.0, 7.0),
2780 ])
2781 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
2782 .with_quantity(4),
2783 Geometry2D::new("shape2_lbracket")
2784 .with_polygon(vec![
2785 (0.0, 0.0),
2786 (80.0, 0.0),
2787 (80.0, 20.0),
2788 (20.0, 20.0),
2789 (20.0, 80.0),
2790 (0.0, 80.0),
2791 ])
2792 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2793 .with_quantity(6),
2794 Geometry2D::new("shape3_triangle")
2795 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2796 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2797 .with_quantity(6),
2798 Geometry2D::new("shape4_rect")
2799 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2800 .with_rotations_deg(vec![0.0, 90.0])
2801 .with_quantity(4),
2802 Geometry2D::new("shape5_hexagon")
2803 .with_polygon(vec![
2804 (15.0, 0.0),
2805 (45.0, 0.0),
2806 (60.0, 26.0),
2807 (45.0, 52.0),
2808 (15.0, 52.0),
2809 (0.0, 26.0),
2810 ])
2811 .with_rotations_deg(vec![0.0, 60.0, 120.0])
2812 .with_quantity(8),
2813 Geometry2D::new("shape6_tstiff")
2814 .with_polygon(vec![
2815 (0.0, 0.0),
2816 (90.0, 0.0),
2817 (90.0, 12.0),
2818 (55.0, 12.0),
2819 (55.0, 60.0),
2820 (35.0, 60.0),
2821 (35.0, 12.0),
2822 (0.0, 12.0),
2823 ])
2824 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2825 .with_quantity(4),
2826 Geometry2D::new("shape7_mount")
2827 .with_polygon(vec![
2828 (0.0, 10.0),
2829 (10.0, 0.0),
2830 (70.0, 0.0),
2831 (80.0, 10.0),
2832 (80.0, 70.0),
2833 (70.0, 80.0),
2834 (10.0, 80.0),
2835 (0.0, 70.0),
2836 ])
2837 .with_rotations_deg(vec![0.0, 90.0])
2838 .with_quantity(3),
2839 Geometry2D::new("shape8_gear")
2840 .with_polygon(vec![
2841 (50.0, 5.0),
2842 (65.0, 15.0),
2843 (77.0, 18.0),
2844 (80.0, 32.0),
2845 (95.0, 50.0),
2846 (80.0, 68.0),
2847 (77.0, 82.0),
2848 (65.0, 85.0),
2849 (50.0, 95.0),
2850 (35.0, 85.0),
2851 (23.0, 82.0),
2852 (20.0, 68.0),
2853 (5.0, 50.0),
2854 (20.0, 32.0),
2855 (23.0, 18.0),
2856 (35.0, 15.0),
2857 ])
2858 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
2859 .with_quantity(13),
2860 ];
2861
2862 let geom_map: HashMap<String, &Geometry2D> =
2864 shapes.iter().map(|g| (g.id().clone(), g)).collect();
2865
2866 let boundary = Boundary2D::rectangle(500.0, 500.0);
2867 let strip_width = 500.0;
2868
2869 let strategies = vec![
2871 Strategy::BottomLeftFill,
2872 Strategy::NfpGuided,
2873 Strategy::GeneticAlgorithm,
2874 Strategy::Brkga,
2875 Strategy::SimulatedAnnealing,
2876 Strategy::Gdrr,
2877 Strategy::Alns,
2878 ];
2879
2880 for strategy in strategies {
2881 println!("\n========== Testing {:?} ==========", strategy);
2882
2883 let config = Config::default()
2884 .with_strategy(strategy)
2885 .with_time_limit(30000); let nester = Nester2D::new(config);
2887
2888 let result = match nester.solve_multi_strip(&shapes, &boundary) {
2889 Ok(r) => r,
2890 Err(e) => {
2891 println!(" Strategy {:?} failed: {}", strategy, e);
2892 continue;
2893 }
2894 };
2895
2896 println!(
2897 " Placed {} pieces across {} strips",
2898 result.placements.len(),
2899 result.boundaries_used
2900 );
2901
2902 let mut boundary_violations = Vec::new();
2904 for p in &result.placements {
2905 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
2907 let full_id = if base_id.starts_with("shape") {
2908 shapes
2910 .iter()
2911 .find(|g| p.geometry_id.starts_with(g.id()))
2912 .map(|g| g.id().as_str())
2913 } else {
2914 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
2915 };
2916
2917 let geom = match full_id.and_then(|id| geom_map.get(id)) {
2918 Some(g) => *g,
2919 None => {
2920 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
2922 Some(g) => g,
2923 None => {
2924 println!(
2925 " WARNING: Could not find geometry for {}",
2926 p.geometry_id
2927 );
2928 continue;
2929 }
2930 }
2931 }
2932 };
2933
2934 let origin_x = p.position[0];
2935 let origin_y = p.position[1];
2936 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2937 let strip_idx = p.boundary_index;
2938
2939 let local_x = origin_x - (strip_idx as f64 * strip_width);
2941
2942 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
2943
2944 let local_min_x = local_x + g_min[0];
2946 let local_max_x = local_x + g_max[0];
2947 let local_min_y = origin_y + g_min[1];
2948 let local_max_y = origin_y + g_max[1];
2949
2950 let tolerance = 0.1;
2952 if local_min_x < -tolerance
2953 || local_max_x > 500.0 + tolerance
2954 || local_min_y < -tolerance
2955 || local_max_y > 500.0 + tolerance
2956 {
2957 boundary_violations.push(format!(
2958 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
2959 p.geometry_id,
2960 strip_idx,
2961 local_min_x,
2962 local_min_y,
2963 local_max_x,
2964 local_max_y
2965 ));
2966 }
2967 }
2968
2969 if !boundary_violations.is_empty() {
2970 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
2971 for v in &boundary_violations {
2972 println!(" - {}", v);
2973 }
2974 }
2975
2976 let mut overlaps = Vec::new();
2978 let placements: Vec<_> = result.placements.iter().collect();
2979
2980 for i in 0..placements.len() {
2981 for j in (i + 1)..placements.len() {
2982 let p1 = placements[i];
2983 let p2 = placements[j];
2984
2985 if p1.boundary_index != p2.boundary_index {
2987 continue;
2988 }
2989
2990 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
2992 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
2993
2994 let (g1, g2) = match (g1, g2) {
2995 (Some(a), Some(b)) => (a, b),
2996 _ => continue,
2997 };
2998
2999 let strip_idx = p1.boundary_index;
3000 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
3001 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
3002
3003 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
3004 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
3005
3006 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
3007 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
3008
3009 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
3010 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
3011 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
3012 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
3013
3014 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
3015 overlaps.push(format!(
3016 "{} and {} in strip {}",
3017 p1.geometry_id, p2.geometry_id, strip_idx
3018 ));
3019 }
3020 }
3021 }
3022
3023 if !overlaps.is_empty() {
3024 println!(" OVERLAPS ({}):", overlaps.len());
3025 for o in overlaps.iter().take(10) {
3026 println!(" - {}", o);
3027 }
3028 if overlaps.len() > 10 {
3029 println!(" ... and {} more", overlaps.len() - 10);
3030 }
3031 }
3032
3033 assert!(
3035 boundary_violations.is_empty(),
3036 "{:?}: Found {} boundary violations",
3037 strategy,
3038 boundary_violations.len()
3039 );
3040
3041 println!(" ✓ All placements within boundary");
3042 println!(" ✓ No AABB overlaps detected");
3043 }
3044 }
3045
3046 #[test]
3054 fn test_multi_strip_distributes_all_instances() {
3055 let geometries = vec![Geometry2D::rectangle("part", 100.0, 100.0).with_quantity(20)];
3056 let boundary = Boundary2D::rectangle(300.0, 300.0);
3057 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3058 let nester = Nester2D::new(config);
3059
3060 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3061
3062 assert_eq!(
3064 result.placements.len(),
3065 20,
3066 "all 20 instances must be placed"
3067 );
3068 assert_eq!(
3069 result.boundaries_used, 3,
3070 "20 of 100x100 in 300x300 => 3 sheets"
3071 );
3072 assert!(
3073 result.unplaced.is_empty(),
3074 "nothing should be unplaced, got {:?}",
3075 result.unplaced
3076 );
3077 assert_eq!(result.total_requested, 20);
3079
3080 let mut seen = std::collections::HashSet::new();
3083 for p in &result.placements {
3084 assert!(
3085 seen.insert((p.geometry_id.clone(), p.instance)),
3086 "duplicate (id, instance) = ({}, {}) across sheets",
3087 p.geometry_id,
3088 p.instance
3089 );
3090 }
3091 let mut sheets: Vec<usize> = result.placements.iter().map(|p| p.boundary_index).collect();
3093 sheets.sort_unstable();
3094 sheets.dedup();
3095 assert_eq!(sheets, vec![0, 1, 2]);
3096 }
3097
3098 #[test]
3101 fn test_multi_strip_oversized_reported_unplaced() {
3102 let geometries = vec![
3103 Geometry2D::rectangle("ok", 50.0, 50.0).with_quantity(2),
3104 Geometry2D::rectangle("toobig", 400.0, 400.0).with_quantity(3),
3105 ];
3106 let boundary = Boundary2D::rectangle(300.0, 300.0);
3107 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3108 let nester = Nester2D::new(config);
3109
3110 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3111
3112 assert_eq!(result.total_requested, 5, "2 + 3 instances requested");
3113 assert_eq!(result.placements.len(), 2);
3115 assert!(
3116 result.unplaced.contains(&"toobig".to_string()),
3117 "oversized geometry must surface in unplaced, got {:?}",
3118 result.unplaced
3119 );
3120 }
3121}