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 std::sync::atomic::{AtomicBool, Ordering};
33use std::sync::Arc;
34use std::time::Instant;
35
36pub struct Nester2D {
38 config: Config,
39 cancelled: Arc<AtomicBool>,
40 #[allow(dead_code)] nfp_cache: NfpCache,
42}
43
44impl Nester2D {
45 pub fn new(config: Config) -> Self {
47 Self {
48 config,
49 cancelled: Arc::new(AtomicBool::new(false)),
50 nfp_cache: NfpCache::new(),
51 }
52 }
53
54 pub fn default_config() -> Self {
56 Self::new(Config::default())
57 }
58
59 fn bottom_left_fill(
61 &self,
62 geometries: &[Geometry2D],
63 boundary: &Boundary2D,
64 ) -> Result<SolveResult<f64>> {
65 let start = Instant::now();
66 let mut result = SolveResult::new();
67 let mut placements = Vec::new();
68
69 let (b_min, b_max) = boundary.aabb();
71 let margin = self.config.margin;
72 let spacing = self.config.spacing;
73
74 let bound_min_x = b_min[0] + margin;
75 let bound_min_y = b_min[1] + margin;
76 let bound_max_x = b_max[0] - margin;
77 let bound_max_y = b_max[1] - margin;
78
79 let strip_width = bound_max_x - bound_min_x;
80 let strip_height = bound_max_y - bound_min_y;
81
82 let mut current_x = bound_min_x;
84 let mut current_y = bound_min_y;
85 let mut row_height = 0.0_f64;
86
87 let mut total_placed_area = 0.0;
88
89 for geom in geometries {
90 geom.validate()?;
91
92 let rotations = geom.rotations();
94 let rotation_angles: Vec<f64> = if rotations.is_empty() {
95 vec![0.0]
96 } else {
97 rotations
98 };
99
100 for instance in 0..geom.quantity() {
101 if self.cancelled.load(Ordering::Relaxed) {
102 result.computation_time_ms = start.elapsed().as_millis() as u64;
103 return Ok(result);
104 }
105
106 if self.config.time_limit_ms > 0
108 && start.elapsed().as_millis() as u64 >= self.config.time_limit_ms
109 {
110 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
111 result.utilization = total_placed_area / boundary.measure();
112 result.computation_time_ms = start.elapsed().as_millis() as u64;
113 result.placements = placements;
114 return Ok(result);
115 }
116
117 let mut best_fit: Option<(f64, f64, f64, f64, f64, [f64; 2])> = None; for &rotation in &rotation_angles {
121 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
122 let g_width = g_max[0] - g_min[0];
123 let g_height = g_max[1] - g_min[1];
124
125 if g_width > strip_width || g_height > strip_height {
127 continue;
128 }
129
130 let mut place_x = current_x;
132 let mut place_y = current_y;
133
134 if place_x + g_width > bound_max_x {
136 place_x = bound_min_x;
138 place_y += row_height + spacing;
139 }
140
141 if place_y + g_height > bound_max_y {
143 continue; }
145
146 let score = if place_x == bound_min_x && place_y > current_y {
149 place_y - bound_min_y + g_height
151 } else {
152 place_x - bound_min_x + g_width
154 };
155
156 let is_better = match &best_fit {
157 None => true,
158 Some((_, _, _, _, _, _)) => {
159 let best_score = if let Some((_, _, _, bx, by, _)) = best_fit {
161 if bx == bound_min_x && by > current_y {
162 by - bound_min_y + g_height
163 } else {
164 bx - bound_min_x + g_width
165 }
166 } else {
167 f64::INFINITY
168 };
169 score < best_score - 1e-6
170 }
171 };
172
173 if is_better {
174 best_fit = Some((rotation, g_width, g_height, place_x, place_y, g_min));
175 }
176 }
177
178 if let Some((rotation, g_width, g_height, place_x, place_y, g_min)) = best_fit {
180 if place_x == bound_min_x && place_y > current_y {
182 row_height = 0.0;
183 }
184
185 let origin_x = place_x - g_min[0];
187 let origin_y = place_y - g_min[1];
188
189 let geom_aabb = geom.aabb_at_rotation(rotation);
191 let boundary_aabb = (b_min, b_max);
192
193 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
194 origin_x,
195 origin_y,
196 geom_aabb,
197 boundary_aabb,
198 margin,
199 ) {
200 let placement = Placement::new_2d(
201 geom.id().clone(),
202 instance,
203 clamped_x,
204 clamped_y,
205 rotation,
206 );
207
208 placements.push(placement);
209 total_placed_area += geom.measure();
210
211 let actual_place_x = clamped_x + g_min[0];
214 let actual_place_y = clamped_y + g_min[1];
215 current_x = actual_place_x + g_width + spacing;
216 current_y = actual_place_y;
217 row_height = row_height.max(g_height);
218 }
219 } else {
220 result.unplaced.push(geom.id().clone());
222 }
223 }
224 }
225
226 result.placements = placements;
227 result.boundaries_used = 1;
228 result.utilization = total_placed_area / boundary.measure();
229 result.computation_time_ms = start.elapsed().as_millis() as u64;
230
231 Ok(result)
232 }
233
234 fn nfp_guided_blf(
239 &self,
240 geometries: &[Geometry2D],
241 boundary: &Boundary2D,
242 ) -> Result<SolveResult<f64>> {
243 let start = Instant::now();
244 let mut result = SolveResult::new();
245 let mut placements = Vec::new();
246 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
247
248 let margin = self.config.margin;
249 let spacing = self.config.spacing;
250
251 let boundary_polygon = self.get_boundary_polygon_with_margin(boundary, margin);
253
254 let mut total_placed_area = 0.0;
255
256 let sample_step = self.compute_sample_step(geometries);
258
259 for geom in geometries {
260 geom.validate()?;
261
262 let rotations = geom.rotations();
264 let rotation_angles: Vec<f64> = if rotations.is_empty() {
265 vec![0.0]
266 } else {
267 rotations
268 };
269
270 for instance in 0..geom.quantity() {
271 if self.cancelled.load(Ordering::Relaxed) {
272 result.computation_time_ms = start.elapsed().as_millis() as u64;
273 return Ok(result);
274 }
275
276 if self.config.time_limit_ms > 0
278 && start.elapsed().as_millis() as u64 >= self.config.time_limit_ms
279 {
280 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
281 result.utilization = total_placed_area / boundary.measure();
282 result.computation_time_ms = start.elapsed().as_millis() as u64;
283 result.placements = placements;
284 return Ok(result);
285 }
286
287 let mut best_placement: Option<(f64, f64, f64)> = None; for &rotation in &rotation_angles {
291 let ifp =
293 match compute_ifp_with_margin(&boundary_polygon, geom, rotation, margin) {
294 Ok(ifp) => ifp,
295 Err(_) => continue,
296 };
297
298 if ifp.is_empty() {
299 continue;
300 }
301
302 let mut nfps: Vec<Nfp> = Vec::new();
304 for placed in &placed_geometries {
305 let cache_key = (
308 placed.geometry.id().as_str(),
309 geom.id().as_str(),
310 rotation - placed.rotation, );
312
313 let nfp_at_origin = match self.nfp_cache.get_or_compute(cache_key, || {
318 let placed_at_origin = placed.geometry.clone();
319 compute_nfp(&placed_at_origin, geom, rotation - placed.rotation)
320 }) {
321 Ok(nfp) => nfp,
322 Err(_) => continue,
323 };
324
325 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
328 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
329 let expanded = self.expand_nfp(&translated_nfp, spacing);
330 nfps.push(expanded);
331 }
332
333 let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
335
336 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
338 if let Some((x, y)) =
339 find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
340 {
341 let is_better = match best_placement {
343 None => true,
344 Some((best_x, best_y, _)) => {
345 x < best_x - 1e-6 || (x < best_x + 1e-6 && y < best_y - 1e-6)
346 }
347 };
348 if is_better {
349 best_placement = Some((x, y, rotation));
350 }
351 }
352 }
353
354 if let Some((x, y, rotation)) = best_placement {
356 let geom_aabb = geom.aabb_at_rotation(rotation);
358 let boundary_aabb = boundary.aabb();
359
360 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
361 x,
362 y,
363 geom_aabb,
364 boundary_aabb,
365 margin,
366 ) {
367 let placement = Placement::new_2d(
368 geom.id().clone(),
369 instance,
370 clamped_x,
371 clamped_y,
372 rotation,
373 );
374
375 placements.push(placement);
376 placed_geometries.push(PlacedGeometry::new(
377 geom.clone(),
378 (clamped_x, clamped_y),
379 rotation,
380 ));
381 total_placed_area += geom.measure();
382 } else {
383 result.unplaced.push(geom.id().clone());
385 }
386 } else {
387 result.unplaced.push(geom.id().clone());
389 }
390 }
391 }
392
393 result.placements = placements;
394 result.boundaries_used = 1;
395 result.utilization = total_placed_area / boundary.measure();
396 result.computation_time_ms = start.elapsed().as_millis() as u64;
397
398 Ok(result)
399 }
400
401 fn get_boundary_polygon_with_margin(
403 &self,
404 boundary: &Boundary2D,
405 margin: f64,
406 ) -> Vec<(f64, f64)> {
407 let (b_min, b_max) = boundary.aabb();
408
409 vec![
411 (b_min[0] + margin, b_min[1] + margin),
412 (b_max[0] - margin, b_min[1] + margin),
413 (b_max[0] - margin, b_max[1] - margin),
414 (b_min[0] + margin, b_max[1] - margin),
415 ]
416 }
417
418 fn compute_sample_step(&self, geometries: &[Geometry2D]) -> f64 {
420 if geometries.is_empty() {
421 return 1.0;
422 }
423
424 let mut min_dim = f64::INFINITY;
426 for geom in geometries {
427 let (g_min, g_max) = geom.aabb();
428 let width = g_max[0] - g_min[0];
429 let height = g_max[1] - g_min[1];
430 min_dim = min_dim.min(width).min(height);
431 }
432
433 (min_dim / 4.0).clamp(0.5, 10.0)
435 }
436
437 fn expand_nfp(&self, nfp: &Nfp, spacing: f64) -> Nfp {
439 if spacing <= 0.0 {
440 return nfp.clone();
441 }
442
443 let expanded_polygons: Vec<Vec<(f64, f64)>> = nfp
447 .polygons
448 .iter()
449 .map(|polygon| {
450 let (cx, cy) = polygon_centroid(polygon);
452
453 polygon
455 .iter()
456 .map(|&(x, y)| {
457 let dx = x - cx;
458 let dy = y - cy;
459 let dist = (dx * dx + dy * dy).sqrt();
460 if dist > 1e-10 {
461 let scale = (dist + spacing) / dist;
462 (cx + dx * scale, cy + dy * scale)
463 } else {
464 (x, y)
465 }
466 })
467 .collect()
468 })
469 .collect();
470
471 Nfp::from_polygons(expanded_polygons)
472 }
473
474 fn shrink_ifp(&self, ifp: &Nfp, spacing: f64) -> Nfp {
476 if spacing <= 0.0 {
477 return ifp.clone();
478 }
479
480 let shrunk_polygons: Vec<Vec<(f64, f64)>> = ifp
482 .polygons
483 .iter()
484 .filter_map(|polygon| {
485 let (cx, cy) = polygon_centroid(polygon);
487
488 let shrunk: Vec<(f64, f64)> = polygon
490 .iter()
491 .map(|&(x, y)| {
492 let dx = x - cx;
493 let dy = y - cy;
494 let dist = (dx * dx + dy * dy).sqrt();
495 if dist > spacing + 1e-10 {
496 let scale = (dist - spacing) / dist;
497 (cx + dx * scale, cy + dy * scale)
498 } else {
499 (cx, cy)
501 }
502 })
503 .collect();
504
505 if shrunk.len() >= 3 {
507 Some(shrunk)
508 } else {
509 None
510 }
511 })
512 .collect();
513
514 Nfp::from_polygons(shrunk_polygons)
515 }
516
517 fn genetic_algorithm(
522 &self,
523 geometries: &[Geometry2D],
524 boundary: &Boundary2D,
525 ) -> Result<SolveResult<f64>> {
526 let time_limit_ms = if self.config.time_limit_ms > 0 {
528 (self.config.time_limit_ms / 4).max(5000)
530 } else {
531 15000 };
533
534 let ga_config = GaConfig::default()
535 .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)
538 .with_mutation_rate(self.config.mutation_rate)
539 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
540
541 let result = run_ga_nesting(
542 geometries,
543 boundary,
544 &self.config,
545 ga_config,
546 self.cancelled.clone(),
547 );
548
549 Ok(result)
550 }
551
552 fn brkga(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
556 let time_limit_ms = if self.config.time_limit_ms > 0 {
558 (self.config.time_limit_ms / 4).max(5000)
560 } else {
561 15000 };
563
564 let brkga_config = BrkgaConfig::default()
565 .with_population_size(30) .with_max_generations(50) .with_elite_fraction(0.2)
568 .with_mutant_fraction(0.15)
569 .with_elite_bias(0.7)
570 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
571
572 let result = run_brkga_nesting(
573 geometries,
574 boundary,
575 &self.config,
576 brkga_config,
577 self.cancelled.clone(),
578 );
579
580 Ok(result)
581 }
582
583 fn simulated_annealing(
588 &self,
589 geometries: &[Geometry2D],
590 boundary: &Boundary2D,
591 ) -> Result<SolveResult<f64>> {
592 let time_limit_ms = if self.config.time_limit_ms > 0 {
595 (self.config.time_limit_ms / 4).max(5000)
597 } else {
598 10000 };
600
601 let sa_config = SaConfig::default()
602 .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));
608
609 let result = run_sa_nesting(
610 geometries,
611 boundary,
612 &self.config,
613 sa_config,
614 self.cancelled.clone(),
615 );
616
617 Ok(result)
618 }
619
620 fn gdrr(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
622 let time_limit = if self.config.time_limit_ms > 0 {
625 (self.config.time_limit_ms / 4).max(5000)
627 } else {
628 10000 };
630 let gdrr_config = GdrrConfig::default()
631 .with_max_iterations(1000) .with_time_limit_ms(time_limit)
633 .with_ruin_ratio(0.1, 0.3) .with_lahc_list_length(30); let result = run_gdrr_nesting(
637 geometries,
638 boundary,
639 &self.config,
640 &gdrr_config,
641 self.cancelled.clone(),
642 );
643
644 Ok(result)
645 }
646
647 fn alns(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
649 let time_limit = if self.config.time_limit_ms > 0 {
652 (self.config.time_limit_ms / 4).max(5000)
654 } else {
655 10000 };
657 let alns_config = AlnsConfig::default()
658 .with_max_iterations(1000) .with_time_limit_ms(time_limit)
660 .with_segment_size(50) .with_scores(33.0, 9.0, 13.0)
662 .with_reaction_factor(0.15) .with_temperature(100.0, 0.999, 0.1); let result = run_alns_nesting(
666 geometries,
667 boundary,
668 &self.config,
669 &alns_config,
670 self.cancelled.clone(),
671 );
672
673 Ok(result)
674 }
675
676 #[cfg(feature = "milp")]
678 fn milp_exact(
679 &self,
680 geometries: &[Geometry2D],
681 boundary: &Boundary2D,
682 ) -> Result<SolveResult<f64>> {
683 let exact_config = ExactConfig::default()
684 .with_time_limit_ms(self.config.time_limit_ms.max(60000))
685 .with_max_items(15)
686 .with_rotation_steps(4)
687 .with_grid_step(1.0);
688
689 let result = run_milp_nesting(
690 geometries,
691 boundary,
692 &self.config,
693 &exact_config,
694 self.cancelled.clone(),
695 );
696
697 Ok(result)
698 }
699
700 #[cfg(feature = "milp")]
702 fn hybrid_exact(
703 &self,
704 geometries: &[Geometry2D],
705 boundary: &Boundary2D,
706 ) -> Result<SolveResult<f64>> {
707 let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
709
710 if total_instances <= 15 {
712 let exact_config = ExactConfig::default()
713 .with_time_limit_ms((self.config.time_limit_ms / 2).max(30000))
714 .with_max_items(15);
715
716 let exact_result = run_milp_nesting(
717 geometries,
718 boundary,
719 &self.config,
720 &exact_config,
721 self.cancelled.clone(),
722 );
723
724 if !exact_result.placements.is_empty() {
726 return Ok(exact_result);
727 }
728 }
729
730 self.alns(geometries, boundary)
732 }
733
734 fn bottom_left_fill_with_progress(
736 &self,
737 geometries: &[Geometry2D],
738 boundary: &Boundary2D,
739 callback: &ProgressCallback,
740 ) -> Result<SolveResult<f64>> {
741 let start = Instant::now();
742 let mut result = SolveResult::new();
743 let mut placements = Vec::new();
744
745 let (b_min, b_max) = boundary.aabb();
747 let margin = self.config.margin;
748 let spacing = self.config.spacing;
749
750 let bound_min_x = b_min[0] + margin;
751 let bound_min_y = b_min[1] + margin;
752 let bound_max_x = b_max[0] - margin;
753 let bound_max_y = b_max[1] - margin;
754
755 let strip_width = bound_max_x - bound_min_x;
756 let strip_height = bound_max_y - bound_min_y;
757
758 let mut current_x = bound_min_x;
759 let mut current_y = bound_min_y;
760 let mut row_height = 0.0_f64;
761 let mut total_placed_area = 0.0;
762
763 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
765 let mut placed_count = 0usize;
766
767 callback(
769 ProgressInfo::new()
770 .with_phase("BLF Placement")
771 .with_items(0, total_pieces)
772 .with_elapsed(0),
773 );
774
775 for geom in geometries {
776 geom.validate()?;
777
778 let rotations = geom.rotations();
779 let rotation_angles: Vec<f64> = if rotations.is_empty() {
780 vec![0.0]
781 } else {
782 rotations
783 };
784
785 for instance in 0..geom.quantity() {
786 if self.cancelled.load(Ordering::Relaxed) {
787 result.computation_time_ms = start.elapsed().as_millis() as u64;
788 callback(
789 ProgressInfo::new()
790 .with_phase("Cancelled")
791 .with_items(placed_count, total_pieces)
792 .with_elapsed(result.computation_time_ms)
793 .finished(),
794 );
795 return Ok(result);
796 }
797
798 if self.config.time_limit_ms > 0
800 && start.elapsed().as_millis() as u64 >= self.config.time_limit_ms
801 {
802 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
803 result.utilization = total_placed_area / boundary.measure();
804 result.computation_time_ms = start.elapsed().as_millis() as u64;
805 result.placements = placements;
806 callback(
807 ProgressInfo::new()
808 .with_phase("Time Limit Reached")
809 .with_items(placed_count, total_pieces)
810 .with_elapsed(result.computation_time_ms)
811 .finished(),
812 );
813 return Ok(result);
814 }
815
816 let mut best_fit: Option<(f64, f64, f64, f64, f64, [f64; 2])> = None;
817
818 for &rotation in &rotation_angles {
819 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
820 let g_width = g_max[0] - g_min[0];
821 let g_height = g_max[1] - g_min[1];
822
823 if g_width > strip_width || g_height > strip_height {
824 continue;
825 }
826
827 let mut place_x = current_x;
828 let mut place_y = current_y;
829
830 if place_x + g_width > bound_max_x {
831 place_x = bound_min_x;
832 place_y += row_height + spacing;
833 }
834
835 if place_y + g_height > bound_max_y {
836 continue;
837 }
838
839 let score = if place_x == bound_min_x && place_y > current_y {
840 place_y - bound_min_y + g_height
841 } else {
842 place_x - bound_min_x + g_width
843 };
844
845 let is_better = match &best_fit {
846 None => true,
847 Some((_, _, _, bx, by, _)) => {
848 let best_score = if *bx == bound_min_x && *by > current_y {
849 by - bound_min_y
850 } else {
851 bx - bound_min_x
852 };
853 score < best_score - 1e-6
854 }
855 };
856
857 if is_better {
858 best_fit = Some((rotation, g_width, g_height, place_x, place_y, g_min));
859 }
860 }
861
862 if let Some((rotation, g_width, g_height, place_x, place_y, g_min)) = best_fit {
863 if place_x == bound_min_x && place_y > current_y {
864 row_height = 0.0;
865 }
866
867 let origin_x = place_x - g_min[0];
869 let origin_y = place_y - g_min[1];
870
871 let geom_aabb = geom.aabb_at_rotation(rotation);
873 let boundary_aabb = (b_min, b_max);
874
875 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
876 origin_x,
877 origin_y,
878 geom_aabb,
879 boundary_aabb,
880 margin,
881 ) {
882 let placement = Placement::new_2d(
883 geom.id().clone(),
884 instance,
885 clamped_x,
886 clamped_y,
887 rotation,
888 );
889
890 placements.push(placement);
891 total_placed_area += geom.measure();
892 placed_count += 1;
893
894 current_x = place_x + g_width + spacing;
895 current_y = place_y;
896 row_height = row_height.max(g_height);
897
898 callback(
900 ProgressInfo::new()
901 .with_phase("BLF Placement")
902 .with_items(placed_count, total_pieces)
903 .with_utilization(total_placed_area / boundary.measure())
904 .with_elapsed(start.elapsed().as_millis() as u64),
905 );
906 } else {
907 result.unplaced.push(geom.id().clone());
908 }
909 } else {
910 result.unplaced.push(geom.id().clone());
911 }
912 }
913 }
914
915 result.placements = placements;
916 result.boundaries_used = 1;
917 result.utilization = total_placed_area / boundary.measure();
918 result.computation_time_ms = start.elapsed().as_millis() as u64;
919
920 callback(
922 ProgressInfo::new()
923 .with_phase("Complete")
924 .with_items(placed_count, total_pieces)
925 .with_utilization(result.utilization)
926 .with_elapsed(result.computation_time_ms)
927 .finished(),
928 );
929
930 Ok(result)
931 }
932
933 fn nfp_guided_blf_with_progress(
935 &self,
936 geometries: &[Geometry2D],
937 boundary: &Boundary2D,
938 callback: &ProgressCallback,
939 ) -> Result<SolveResult<f64>> {
940 let start = Instant::now();
941 let mut result = SolveResult::new();
942 let mut placements = Vec::new();
943 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
944
945 let margin = self.config.margin;
946 let spacing = self.config.spacing;
947 let boundary_polygon = self.get_boundary_polygon_with_margin(boundary, margin);
948
949 let mut total_placed_area = 0.0;
950 let sample_step = self.compute_sample_step(geometries);
951
952 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
954 let mut placed_count = 0usize;
955
956 callback(
958 ProgressInfo::new()
959 .with_phase("NFP Placement")
960 .with_items(0, total_pieces)
961 .with_elapsed(0),
962 );
963
964 for geom in geometries {
965 geom.validate()?;
966
967 let rotations = geom.rotations();
968 let rotation_angles: Vec<f64> = if rotations.is_empty() {
969 vec![0.0]
970 } else {
971 rotations
972 };
973
974 for instance in 0..geom.quantity() {
975 if self.cancelled.load(Ordering::Relaxed) {
976 result.computation_time_ms = start.elapsed().as_millis() as u64;
977 callback(
978 ProgressInfo::new()
979 .with_phase("Cancelled")
980 .with_items(placed_count, total_pieces)
981 .with_elapsed(result.computation_time_ms)
982 .finished(),
983 );
984 return Ok(result);
985 }
986
987 if self.config.time_limit_ms > 0
989 && start.elapsed().as_millis() as u64 >= self.config.time_limit_ms
990 {
991 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
992 result.utilization = total_placed_area / boundary.measure();
993 result.computation_time_ms = start.elapsed().as_millis() as u64;
994 result.placements = placements;
995 callback(
996 ProgressInfo::new()
997 .with_phase("Time Limit Reached")
998 .with_items(placed_count, total_pieces)
999 .with_elapsed(result.computation_time_ms)
1000 .finished(),
1001 );
1002 return Ok(result);
1003 }
1004
1005 let mut best_placement: Option<(f64, f64, f64)> = None;
1006
1007 for &rotation in &rotation_angles {
1008 let ifp =
1009 match compute_ifp_with_margin(&boundary_polygon, geom, rotation, margin) {
1010 Ok(ifp) => ifp,
1011 Err(_) => continue,
1012 };
1013
1014 if ifp.is_empty() {
1015 continue;
1016 }
1017
1018 let mut nfps: Vec<Nfp> = Vec::new();
1019 for placed in &placed_geometries {
1020 let cache_key = (
1022 placed.geometry.id().as_str(),
1023 geom.id().as_str(),
1024 rotation - placed.rotation,
1025 );
1026
1027 let nfp_at_origin = match self.nfp_cache.get_or_compute(cache_key, || {
1030 let placed_at_origin = placed.geometry.clone();
1031 compute_nfp(&placed_at_origin, geom, rotation - placed.rotation)
1032 }) {
1033 Ok(nfp) => nfp,
1034 Err(_) => continue,
1035 };
1036
1037 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
1039 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
1040 let expanded = self.expand_nfp(&translated_nfp, spacing);
1041 nfps.push(expanded);
1042 }
1043
1044 let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
1045 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
1046
1047 if let Some((x, y)) =
1048 find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
1049 {
1050 let is_better = match best_placement {
1051 None => true,
1052 Some((best_x, best_y, _)) => {
1053 x < best_x - 1e-6 || (x < best_x + 1e-6 && y < best_y - 1e-6)
1054 }
1055 };
1056 if is_better {
1057 best_placement = Some((x, y, rotation));
1058 }
1059 }
1060 }
1061
1062 if let Some((x, y, rotation)) = best_placement {
1063 let geom_aabb = geom.aabb_at_rotation(rotation);
1065 let boundary_aabb = boundary.aabb();
1066
1067 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
1068 x,
1069 y,
1070 geom_aabb,
1071 boundary_aabb,
1072 margin,
1073 ) {
1074 let placement = Placement::new_2d(
1075 geom.id().clone(),
1076 instance,
1077 clamped_x,
1078 clamped_y,
1079 rotation,
1080 );
1081 placements.push(placement);
1082 placed_geometries.push(PlacedGeometry::new(
1083 geom.clone(),
1084 (clamped_x, clamped_y),
1085 rotation,
1086 ));
1087 total_placed_area += geom.measure();
1088 placed_count += 1;
1089
1090 callback(
1092 ProgressInfo::new()
1093 .with_phase("NFP Placement")
1094 .with_items(placed_count, total_pieces)
1095 .with_utilization(total_placed_area / boundary.measure())
1096 .with_elapsed(start.elapsed().as_millis() as u64),
1097 );
1098 } else {
1099 result.unplaced.push(geom.id().clone());
1100 }
1101 } else {
1102 result.unplaced.push(geom.id().clone());
1103 }
1104 }
1105 }
1106
1107 result.placements = placements;
1108 result.boundaries_used = 1;
1109 result.utilization = total_placed_area / boundary.measure();
1110 result.computation_time_ms = start.elapsed().as_millis() as u64;
1111
1112 callback(
1114 ProgressInfo::new()
1115 .with_phase("Complete")
1116 .with_items(placed_count, total_pieces)
1117 .with_utilization(result.utilization)
1118 .with_elapsed(result.computation_time_ms)
1119 .finished(),
1120 );
1121
1122 Ok(result)
1123 }
1124
1125 pub fn solve_multi_strip(
1131 &self,
1132 geometries: &[Geometry2D],
1133 boundary: &Boundary2D,
1134 ) -> Result<SolveResult<f64>> {
1135 boundary.validate()?;
1136 self.cancelled.store(false, Ordering::Relaxed);
1137
1138 let (b_min, b_max) = boundary.aabb();
1139 let strip_width = b_max[0] - b_min[0];
1140
1141 let mut final_result = SolveResult::new();
1142 let mut remaining_geometries: Vec<Geometry2D> = geometries.to_vec();
1143 let mut strip_index = 0;
1144 let max_strips = 100; while !remaining_geometries.is_empty() && strip_index < max_strips {
1147 if self.cancelled.load(Ordering::Relaxed) {
1148 break;
1149 }
1150
1151 let strip_result = match self.config.strategy {
1153 Strategy::BottomLeftFill => self.bottom_left_fill(&remaining_geometries, boundary),
1154 Strategy::NfpGuided => self.nfp_guided_blf(&remaining_geometries, boundary),
1155 Strategy::GeneticAlgorithm => {
1156 self.genetic_algorithm(&remaining_geometries, boundary)
1157 }
1158 Strategy::Brkga => self.brkga(&remaining_geometries, boundary),
1159 Strategy::SimulatedAnnealing => {
1160 self.simulated_annealing(&remaining_geometries, boundary)
1161 }
1162 Strategy::Gdrr => self.gdrr(&remaining_geometries, boundary),
1163 Strategy::Alns => self.alns(&remaining_geometries, boundary),
1164 #[cfg(feature = "milp")]
1165 Strategy::MilpExact => self.milp_exact(&remaining_geometries, boundary),
1166 #[cfg(feature = "milp")]
1167 Strategy::HybridExact => self.hybrid_exact(&remaining_geometries, boundary),
1168 _ => self.nfp_guided_blf(&remaining_geometries, boundary),
1169 }?;
1170
1171 let strip_result =
1173 validate_and_filter_placements(strip_result, &remaining_geometries, boundary);
1174
1175 if strip_result.placements.is_empty() {
1176 final_result.unplaced.extend(strip_result.unplaced);
1178 break;
1179 }
1180
1181 let placed_ids: std::collections::HashSet<_> = strip_result
1183 .placements
1184 .iter()
1185 .map(|p| p.geometry_id.clone())
1186 .collect();
1187
1188 for mut placement in strip_result.placements {
1190 if !placement.position.is_empty() {
1192 placement.position[0] += strip_index as f64 * strip_width;
1193 }
1194 placement.boundary_index = strip_index;
1195 final_result.placements.push(placement);
1196 }
1197
1198 remaining_geometries.retain(|g| !placed_ids.contains(g.id()));
1200
1201 strip_index += 1;
1205 }
1206
1207 final_result.boundaries_used = strip_index;
1208 final_result.deduplicate_unplaced();
1209
1210 let total_boundary_area = boundary.measure() * strip_index as f64;
1212 if total_boundary_area > 0.0 {
1213 let placed_area: f64 = final_result
1214 .placements
1215 .iter()
1216 .filter_map(|p| {
1217 geometries
1218 .iter()
1219 .find(|g| g.id() == &p.geometry_id)
1220 .map(|g| {
1221 use u_nesting_core::geometry::Geometry;
1222 g.measure()
1223 })
1224 })
1225 .sum();
1226 final_result.utilization = placed_area / total_boundary_area;
1227 }
1228
1229 Ok(final_result)
1230 }
1231}
1232
1233fn polygon_centroid(polygon: &[(f64, f64)]) -> (f64, f64) {
1235 if polygon.is_empty() {
1236 return (0.0, 0.0);
1237 }
1238
1239 let sum: (f64, f64) = polygon
1240 .iter()
1241 .fold((0.0, 0.0), |acc, &(x, y)| (acc.0 + x, acc.1 + y));
1242 let n = polygon.len() as f64;
1243 (sum.0 / n, sum.1 / n)
1244}
1245
1246impl Solver for Nester2D {
1247 type Geometry = Geometry2D;
1248 type Boundary = Boundary2D;
1249 type Scalar = f64;
1250
1251 fn solve(
1252 &self,
1253 geometries: &[Self::Geometry],
1254 boundary: &Self::Boundary,
1255 ) -> Result<SolveResult<f64>> {
1256 boundary.validate()?;
1257
1258 self.cancelled.store(false, Ordering::Relaxed);
1260
1261 let initial_result = match self.config.strategy {
1262 Strategy::BottomLeftFill => self.bottom_left_fill(geometries, boundary),
1263 Strategy::NfpGuided => self.nfp_guided_blf(geometries, boundary),
1264 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
1265 Strategy::Brkga => self.brkga(geometries, boundary),
1266 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
1267 Strategy::Gdrr => self.gdrr(geometries, boundary),
1268 Strategy::Alns => self.alns(geometries, boundary),
1269 #[cfg(feature = "milp")]
1270 Strategy::MilpExact => self.milp_exact(geometries, boundary),
1271 #[cfg(feature = "milp")]
1272 Strategy::HybridExact => self.hybrid_exact(geometries, boundary),
1273 _ => {
1274 log::warn!(
1276 "Strategy {:?} not yet implemented, using NfpGuided",
1277 self.config.strategy
1278 );
1279 self.nfp_guided_blf(geometries, boundary)
1280 }
1281 }?;
1282
1283 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1285
1286 result.deduplicate_unplaced();
1288 Ok(result)
1289 }
1290
1291 fn solve_with_progress(
1292 &self,
1293 geometries: &[Self::Geometry],
1294 boundary: &Self::Boundary,
1295 callback: ProgressCallback,
1296 ) -> Result<SolveResult<f64>> {
1297 boundary.validate()?;
1298
1299 self.cancelled.store(false, Ordering::Relaxed);
1301
1302 let initial_result = match self.config.strategy {
1303 Strategy::BottomLeftFill => {
1304 self.bottom_left_fill_with_progress(geometries, boundary, &callback)?
1305 }
1306 Strategy::NfpGuided => {
1307 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1308 }
1309 Strategy::GeneticAlgorithm => {
1310 let mut ga_config = GaConfig::default()
1311 .with_population_size(self.config.population_size)
1312 .with_max_generations(self.config.max_generations)
1313 .with_crossover_rate(self.config.crossover_rate)
1314 .with_mutation_rate(self.config.mutation_rate);
1315
1316 if self.config.time_limit_ms > 0 {
1318 ga_config = ga_config.with_time_limit(std::time::Duration::from_millis(
1319 self.config.time_limit_ms,
1320 ));
1321 }
1322
1323 run_ga_nesting_with_progress(
1324 geometries,
1325 boundary,
1326 &self.config,
1327 ga_config,
1328 self.cancelled.clone(),
1329 callback,
1330 )
1331 }
1332 _ => {
1334 log::warn!(
1335 "Strategy {:?} not yet implemented, using NfpGuided",
1336 self.config.strategy
1337 );
1338 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1339 }
1340 };
1341
1342 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1344
1345 result.deduplicate_unplaced();
1347 Ok(result)
1348 }
1349
1350 fn cancel(&self) {
1351 self.cancelled.store(true, Ordering::Relaxed);
1352 }
1353}
1354
1355#[cfg(test)]
1356mod tests {
1357 use super::*;
1358
1359 #[test]
1360 fn test_simple_nesting() {
1361 let geometries = vec![
1362 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1363 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1364 ];
1365
1366 let boundary = Boundary2D::rectangle(100.0, 50.0);
1367 let nester = Nester2D::default_config();
1368
1369 let result = nester.solve(&geometries, &boundary).unwrap();
1370
1371 assert!(result.utilization > 0.0);
1372 assert!(result.placements.len() <= 5); }
1374
1375 #[test]
1376 fn test_placement_within_bounds() {
1377 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1378
1379 let boundary = Boundary2D::rectangle(50.0, 50.0);
1380 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1381 let nester = Nester2D::new(config);
1382
1383 let result = nester.solve(&geometries, &boundary).unwrap();
1384
1385 assert_eq!(result.placements.len(), 4);
1387 assert!(result.unplaced.is_empty());
1388
1389 for p in &result.placements {
1391 assert!(p.position[0] >= 5.0);
1392 assert!(p.position[1] >= 5.0);
1393 }
1394 }
1395
1396 #[test]
1397 fn test_nfp_guided_basic() {
1398 let geometries = vec![
1399 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1400 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1401 ];
1402
1403 let boundary = Boundary2D::rectangle(100.0, 50.0);
1404 let config = Config::default().with_strategy(Strategy::NfpGuided);
1405 let nester = Nester2D::new(config);
1406
1407 let result = nester.solve(&geometries, &boundary).unwrap();
1408
1409 assert!(result.utilization > 0.0);
1410 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1412 }
1413
1414 #[test]
1415 fn test_nfp_guided_with_spacing() {
1416 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1417
1418 let boundary = Boundary2D::rectangle(50.0, 50.0);
1419 let config = Config::default()
1420 .with_strategy(Strategy::NfpGuided)
1421 .with_margin(2.0)
1422 .with_spacing(3.0);
1423 let nester = Nester2D::new(config);
1424
1425 let result = nester.solve(&geometries, &boundary).unwrap();
1426
1427 assert_eq!(result.placements.len(), 4);
1429 assert!(result.unplaced.is_empty());
1430
1431 assert!(result.utilization > 0.0);
1433 }
1434
1435 #[test]
1436 fn test_nfp_guided_no_overlap() {
1437 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1438
1439 let boundary = Boundary2D::rectangle(100.0, 100.0);
1440 let config = Config::default().with_strategy(Strategy::NfpGuided);
1441 let nester = Nester2D::new(config);
1442
1443 let result = nester.solve(&geometries, &boundary).unwrap();
1444
1445 assert_eq!(result.placements.len(), 3);
1446
1447 for i in 0..result.placements.len() {
1449 for j in (i + 1)..result.placements.len() {
1450 let p1 = &result.placements[i];
1451 let p2 = &result.placements[j];
1452
1453 let r1_min_x = p1.position[0];
1455 let r1_max_x = p1.position[0] + 20.0;
1456 let r1_min_y = p1.position[1];
1457 let r1_max_y = p1.position[1] + 20.0;
1458
1459 let r2_min_x = p2.position[0];
1460 let r2_max_x = p2.position[0] + 20.0;
1461 let r2_min_y = p2.position[1];
1462 let r2_max_y = p2.position[1] + 20.0;
1463
1464 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1466 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1467
1468 assert!(
1469 !(overlaps_x && overlaps_y),
1470 "Placements {} and {} overlap",
1471 i,
1472 j
1473 );
1474 }
1475 }
1476 }
1477
1478 #[test]
1479 fn test_nfp_guided_utilization() {
1480 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
1482
1483 let boundary = Boundary2D::rectangle(100.0, 50.0);
1484 let config = Config::default().with_strategy(Strategy::NfpGuided);
1485 let nester = Nester2D::new(config);
1486
1487 let result = nester.solve(&geometries, &boundary).unwrap();
1488
1489 assert_eq!(result.placements.len(), 4);
1491
1492 assert!(result.utilization > 0.45);
1494 }
1495
1496 #[test]
1497 fn test_polygon_centroid() {
1498 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1500 let (cx, cy) = polygon_centroid(&square);
1501 assert!((cx - 5.0).abs() < 0.01);
1502 assert!((cy - 5.0).abs() < 0.01);
1503
1504 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
1505 let (cx, cy) = polygon_centroid(&triangle);
1506 assert!((cx - 3.0).abs() < 0.01);
1507 assert!((cy - 2.0).abs() < 0.01);
1508 }
1509
1510 #[test]
1511 fn test_ga_strategy_basic() {
1512 let geometries = vec![
1513 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1514 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1515 ];
1516
1517 let boundary = Boundary2D::rectangle(100.0, 50.0);
1518 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1519 let nester = Nester2D::new(config);
1520
1521 let result = nester.solve(&geometries, &boundary).unwrap();
1522
1523 assert!(result.utilization > 0.0);
1524 assert!(!result.placements.is_empty());
1525 assert!(result.generations.is_some());
1527 assert!(result.best_fitness.is_some());
1528 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
1529 }
1530
1531 #[test]
1532 fn test_ga_strategy_all_placed() {
1533 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1535
1536 let boundary = Boundary2D::rectangle(100.0, 100.0);
1537 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1538 let nester = Nester2D::new(config);
1539
1540 let result = nester.solve(&geometries, &boundary).unwrap();
1541
1542 assert_eq!(result.placements.len(), 4);
1544 assert!(result.unplaced.is_empty());
1545 }
1546
1547 #[test]
1548 fn test_brkga_strategy_basic() {
1549 let geometries = vec![
1550 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1551 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1552 ];
1553
1554 let boundary = Boundary2D::rectangle(100.0, 50.0);
1555 let config = Config::default().with_strategy(Strategy::Brkga);
1556 let nester = Nester2D::new(config);
1557
1558 let result = nester.solve(&geometries, &boundary).unwrap();
1559
1560 assert!(result.utilization > 0.0);
1561 assert!(!result.placements.is_empty());
1562 assert!(result.generations.is_some());
1564 assert!(result.best_fitness.is_some());
1565 assert!(result.strategy == Some("BRKGA".to_string()));
1566 }
1567
1568 #[test]
1569 fn test_brkga_strategy_all_placed() {
1570 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1572
1573 let boundary = Boundary2D::rectangle(100.0, 100.0);
1574 let config = Config::default()
1576 .with_strategy(Strategy::Brkga)
1577 .with_time_limit(30000); let nester = Nester2D::new(config);
1579
1580 let result = nester.solve(&geometries, &boundary).unwrap();
1581
1582 assert!(
1585 result.placements.len() >= 3,
1586 "Expected at least 3 placements, got {}",
1587 result.placements.len()
1588 );
1589 }
1590
1591 #[test]
1592 fn test_gdrr_strategy_basic() {
1593 let geometries = vec![
1594 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1595 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1596 ];
1597
1598 let boundary = Boundary2D::rectangle(100.0, 50.0);
1599 let config = Config::default().with_strategy(Strategy::Gdrr);
1600 let nester = Nester2D::new(config);
1601
1602 let result = nester.solve(&geometries, &boundary).unwrap();
1603
1604 assert!(result.utilization > 0.0);
1605 assert!(!result.placements.is_empty());
1606 assert!(result.iterations.is_some());
1608 assert!(result.best_fitness.is_some());
1609 assert!(result.strategy == Some("GDRR".to_string()));
1610 }
1611
1612 #[test]
1613 fn test_gdrr_strategy_all_placed() {
1614 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1616
1617 let boundary = Boundary2D::rectangle(100.0, 100.0);
1618 let config = Config::default().with_strategy(Strategy::Gdrr);
1619 let nester = Nester2D::new(config);
1620
1621 let result = nester.solve(&geometries, &boundary).unwrap();
1622
1623 assert_eq!(result.placements.len(), 4);
1625 assert!(result.unplaced.is_empty());
1626 }
1627
1628 #[test]
1629 fn test_alns_strategy_basic() {
1630 let geometries = vec![
1631 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1632 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1633 ];
1634
1635 let boundary = Boundary2D::rectangle(100.0, 50.0);
1636 let config = Config::default().with_strategy(Strategy::Alns);
1637 let nester = Nester2D::new(config);
1638
1639 let result = nester.solve(&geometries, &boundary).unwrap();
1640
1641 assert!(result.utilization > 0.0);
1642 assert!(!result.placements.is_empty());
1643 assert!(result.iterations.is_some());
1645 assert!(result.best_fitness.is_some());
1646 assert!(result.strategy == Some("ALNS".to_string()));
1647 }
1648
1649 #[test]
1650 fn test_alns_strategy_all_placed() {
1651 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1653
1654 let boundary = Boundary2D::rectangle(100.0, 100.0);
1655 let config = Config::default().with_strategy(Strategy::Alns);
1656 let nester = Nester2D::new(config);
1657
1658 let result = nester.solve(&geometries, &boundary).unwrap();
1659
1660 assert_eq!(result.placements.len(), 4);
1662 assert!(result.unplaced.is_empty());
1663 }
1664
1665 #[test]
1666 fn test_blf_rotation_optimization() {
1667 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
1670 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
1672
1673 let boundary = Boundary2D::rectangle(35.0, 95.0);
1676 let nester = Nester2D::default_config();
1677
1678 let result = nester.solve(&geometries, &boundary).unwrap();
1679
1680 assert_eq!(
1682 result.placements.len(),
1683 3,
1684 "All pieces should be placed with rotation optimization"
1685 );
1686 assert!(result.unplaced.is_empty());
1687 }
1688
1689 #[test]
1690 fn test_blf_selects_best_rotation() {
1691 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
1693 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
1695
1696 let boundary = Boundary2D::rectangle(45.0, 50.0);
1700 let nester = Nester2D::default_config();
1701
1702 let result = nester.solve(&geometries, &boundary).unwrap();
1703
1704 assert_eq!(result.placements.len(), 2);
1705 assert!(result.unplaced.is_empty());
1706 }
1707
1708 #[test]
1709 fn test_progress_callback_blf() {
1710 use std::sync::atomic::{AtomicUsize, Ordering};
1711 use std::sync::Arc;
1712
1713 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1714 let boundary = Boundary2D::rectangle(50.0, 50.0);
1715 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1716 let nester = Nester2D::new(config);
1717
1718 let callback_count = Arc::new(AtomicUsize::new(0));
1719 let callback_count_clone = callback_count.clone();
1720 let last_items_placed = Arc::new(AtomicUsize::new(0));
1721 let last_items_placed_clone = last_items_placed.clone();
1722
1723 let callback: ProgressCallback = Box::new(move |info| {
1724 callback_count_clone.fetch_add(1, Ordering::Relaxed);
1725 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
1726 });
1727
1728 let result = nester
1729 .solve_with_progress(&geometries, &boundary, callback)
1730 .unwrap();
1731
1732 let count = callback_count.load(Ordering::Relaxed);
1734 assert!(
1735 count >= 5,
1736 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
1737 count
1738 );
1739
1740 let final_placed = last_items_placed.load(Ordering::Relaxed);
1742 assert_eq!(final_placed, 4, "Should report 4 items placed");
1743
1744 assert_eq!(result.placements.len(), 4);
1746 }
1747
1748 #[test]
1749 fn test_progress_callback_nfp() {
1750 use std::sync::atomic::{AtomicUsize, Ordering};
1751 use std::sync::Arc;
1752
1753 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
1754 let boundary = Boundary2D::rectangle(50.0, 50.0);
1755 let config = Config::default().with_strategy(Strategy::NfpGuided);
1756 let nester = Nester2D::new(config);
1757
1758 let callback_count = Arc::new(AtomicUsize::new(0));
1759 let callback_count_clone = callback_count.clone();
1760
1761 let callback: ProgressCallback = Box::new(move |info| {
1762 callback_count_clone.fetch_add(1, Ordering::Relaxed);
1763 assert!(info.items_placed <= info.total_items);
1764 });
1765
1766 let result = nester
1767 .solve_with_progress(&geometries, &boundary, callback)
1768 .unwrap();
1769
1770 let count = callback_count.load(Ordering::Relaxed);
1772 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
1773
1774 assert_eq!(result.placements.len(), 2);
1776 }
1777
1778 #[test]
1779 fn test_time_limit_honored() {
1780 let geometries: Vec<Geometry2D> = (0..100)
1782 .map(|i| Geometry2D::rectangle(&format!("R{}", i), 5.0, 5.0))
1783 .collect();
1784 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
1785
1786 let config = Config::default()
1788 .with_strategy(Strategy::BottomLeftFill)
1789 .with_time_limit(1);
1790 let nester = Nester2D::new(config);
1791
1792 let result = nester.solve(&geometries, &boundary).unwrap();
1793
1794 assert!(
1797 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
1799 result.computation_time_ms
1800 );
1801 }
1802
1803 #[test]
1804 fn test_time_limit_zero_unlimited() {
1805 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1807 let boundary = Boundary2D::rectangle(50.0, 50.0);
1808
1809 let config = Config::default()
1810 .with_strategy(Strategy::BottomLeftFill)
1811 .with_time_limit(0); let nester = Nester2D::new(config);
1813
1814 let result = nester.solve(&geometries, &boundary).unwrap();
1815
1816 assert_eq!(result.placements.len(), 4);
1818 }
1819
1820 #[test]
1821 fn test_blf_bounds_clamping() {
1822 let gear_like = Geometry2D::new("gear")
1826 .with_polygon(vec![
1827 (50.0, 5.0), (65.0, 15.0),
1829 (77.0, 18.0),
1830 (80.0, 32.0),
1831 (95.0, 50.0), (80.0, 68.0),
1833 (77.0, 82.0),
1834 (65.0, 85.0),
1835 (50.0, 95.0), (35.0, 85.0),
1837 (23.0, 82.0),
1838 (20.0, 68.0),
1839 (5.0, 50.0), (20.0, 32.0),
1841 (23.0, 18.0),
1842 (35.0, 15.0),
1843 ])
1844 .with_quantity(1);
1845
1846 let boundary = Boundary2D::rectangle(100.0, 100.0);
1848
1849 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1850 let nester = Nester2D::new(config);
1851
1852 let result = nester.solve(&[gear_like.clone()], &boundary).unwrap();
1853
1854 assert_eq!(result.placements.len(), 1);
1855 let placement = &result.placements[0];
1856
1857 let origin_x = placement.position[0];
1859 let origin_y = placement.position[1];
1860
1861 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1863
1864 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
1866
1867 let actual_min_x = origin_x + g_min[0];
1869 let actual_max_x = origin_x + g_max[0];
1870 let actual_min_y = origin_y + g_min[1];
1871 let actual_max_y = origin_y + g_max[1];
1872
1873 assert!(
1875 actual_min_x >= 0.0,
1876 "Left edge {} should be >= 0",
1877 actual_min_x
1878 );
1879 assert!(
1880 actual_max_x <= 100.0,
1881 "Right edge {} should be <= 100",
1882 actual_max_x
1883 );
1884 assert!(
1885 actual_min_y >= 0.0,
1886 "Bottom edge {} should be >= 0",
1887 actual_min_y
1888 );
1889 assert!(
1890 actual_max_y <= 100.0,
1891 "Top edge {} should be <= 100",
1892 actual_max_y
1893 );
1894 }
1895
1896 #[test]
1897 fn test_blf_bounds_clamping_many_pieces() {
1898 let gear_like = Geometry2D::new("gear")
1901 .with_polygon(vec![
1902 (50.0, 5.0),
1903 (65.0, 15.0),
1904 (77.0, 18.0),
1905 (80.0, 32.0),
1906 (95.0, 50.0),
1907 (80.0, 68.0),
1908 (77.0, 82.0),
1909 (65.0, 85.0),
1910 (50.0, 95.0),
1911 (35.0, 85.0),
1912 (23.0, 82.0),
1913 (20.0, 68.0),
1914 (5.0, 50.0),
1915 (20.0, 32.0),
1916 (23.0, 18.0),
1917 (35.0, 15.0),
1918 ])
1919 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
1923
1924 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1925 let nester = Nester2D::new(config);
1926
1927 let result = nester.solve(&[gear_like.clone()], &boundary).unwrap();
1928
1929 for (i, placement) in result.placements.iter().enumerate() {
1931 let origin_x = placement.position[0];
1932 let origin_y = placement.position[1];
1933 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1934
1935 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
1936
1937 let actual_min_x = origin_x + g_min[0];
1938 let actual_max_x = origin_x + g_max[0];
1939 let actual_min_y = origin_y + g_min[1];
1940 let actual_max_y = origin_y + g_max[1];
1941
1942 assert!(
1943 actual_min_x >= -0.01,
1944 "Piece {}: Left edge {} should be >= 0",
1945 i,
1946 actual_min_x
1947 );
1948 assert!(
1949 actual_max_x <= 500.01,
1950 "Piece {}: Right edge {} should be <= 500",
1951 i,
1952 actual_max_x
1953 );
1954 assert!(
1955 actual_min_y >= -0.01,
1956 "Piece {}: Bottom edge {} should be >= 0",
1957 i,
1958 actual_min_y
1959 );
1960 assert!(
1961 actual_max_y <= 500.01,
1962 "Piece {}: Top edge {} should be <= 500",
1963 i,
1964 actual_max_y
1965 );
1966 }
1967 }
1968
1969 #[test]
1970 fn test_blf_bounds_trace() {
1971 let gear = Geometry2D::new("gear").with_polygon(vec![
1973 (50.0, 5.0),
1974 (65.0, 15.0),
1975 (77.0, 18.0),
1976 (80.0, 32.0),
1977 (95.0, 50.0),
1978 (80.0, 68.0),
1979 (77.0, 82.0),
1980 (65.0, 85.0),
1981 (50.0, 95.0),
1982 (35.0, 85.0),
1983 (23.0, 82.0),
1984 (20.0, 68.0),
1985 (5.0, 50.0),
1986 (20.0, 32.0),
1987 (23.0, 18.0),
1988 (35.0, 15.0),
1989 ]);
1990
1991 let (g_min, g_max) = gear.aabb();
1993 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
1994 assert!(
1995 (g_min[0] - 5.0).abs() < 0.01,
1996 "g_min[0] should be 5, got {}",
1997 g_min[0]
1998 );
1999 assert!(
2000 (g_max[0] - 95.0).abs() < 0.01,
2001 "g_max[0] should be 95, got {}",
2002 g_max[0]
2003 );
2004
2005 let b_max_x = 500.0;
2007 let margin = 0.0;
2008 let max_valid_x = b_max_x - margin - g_max[0];
2009 println!(
2010 "max_valid_x = {} - {} - {} = {}",
2011 b_max_x, margin, g_max[0], max_valid_x
2012 );
2013 assert!(
2014 (max_valid_x - 405.0).abs() < 0.01,
2015 "max_valid_x should be 405, got {}",
2016 max_valid_x
2017 );
2018
2019 let boundary = Boundary2D::rectangle(500.0, 500.0);
2021 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2022 let nester = Nester2D::new(config);
2023
2024 let result = nester
2025 .solve(&[gear.clone().with_quantity(1)], &boundary)
2026 .unwrap();
2027
2028 assert_eq!(result.placements.len(), 1);
2029 let p = &result.placements[0];
2030 let origin_x = p.position[0];
2031 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2032
2033 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2034 let actual_max_x = origin_x + g_max_r[0];
2035
2036 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2037 println!(
2038 "At rotation {}: g_min={:?}, g_max={:?}",
2039 rotation, g_min_r, g_max_r
2040 );
2041 println!(
2042 "Actual max x: {} + {} = {}",
2043 origin_x, g_max_r[0], actual_max_x
2044 );
2045
2046 assert!(
2047 actual_max_x <= 500.01,
2048 "Geometry exceeds boundary: max_x={} > 500",
2049 actual_max_x
2050 );
2051 }
2052
2053 #[test]
2054 fn test_blf_bounds_many_pieces_direct() {
2055 let gear = Geometry2D::new("gear")
2057 .with_polygon(vec![
2058 (50.0, 5.0),
2059 (65.0, 15.0),
2060 (77.0, 18.0),
2061 (80.0, 32.0),
2062 (95.0, 50.0),
2063 (80.0, 68.0),
2064 (77.0, 82.0),
2065 (65.0, 85.0),
2066 (50.0, 95.0),
2067 (35.0, 85.0),
2068 (23.0, 82.0),
2069 (20.0, 68.0),
2070 (5.0, 50.0),
2071 (20.0, 32.0),
2072 (23.0, 18.0),
2073 (35.0, 15.0),
2074 ])
2075 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2078 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2079 let nester = Nester2D::new(config);
2080
2081 let result = nester.solve(&[gear.clone()], &boundary).unwrap();
2082
2083 println!("Placed {} pieces", result.placements.len());
2084
2085 for (i, p) in result.placements.iter().enumerate() {
2087 let origin_x = p.position[0];
2088 let origin_y = p.position[1];
2089 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2090
2091 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2092
2093 let actual_min_x = origin_x + g_min_r[0];
2094 let actual_max_x = origin_x + g_max_r[0];
2095 let actual_min_y = origin_y + g_min_r[1];
2096 let actual_max_y = origin_y + g_max_r[1];
2097
2098 println!(
2099 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2100 i,
2101 origin_x,
2102 origin_y,
2103 rotation,
2104 actual_min_x,
2105 actual_max_x,
2106 actual_min_y,
2107 actual_max_y
2108 );
2109
2110 assert!(
2111 actual_max_x <= 500.01,
2112 "Piece {}: Right edge {} > 500",
2113 i,
2114 actual_max_x
2115 );
2116 assert!(
2117 actual_max_y <= 500.01,
2118 "Piece {}: Top edge {} > 500",
2119 i,
2120 actual_max_y
2121 );
2122 }
2123 }
2124
2125 #[test]
2126 fn test_blf_bounds_multi_strip() {
2127 let gear = Geometry2D::new("gear")
2129 .with_polygon(vec![
2130 (50.0, 5.0),
2131 (65.0, 15.0),
2132 (77.0, 18.0),
2133 (80.0, 32.0),
2134 (95.0, 50.0),
2135 (80.0, 68.0),
2136 (77.0, 82.0),
2137 (65.0, 85.0),
2138 (50.0, 95.0),
2139 (35.0, 85.0),
2140 (23.0, 82.0),
2141 (20.0, 68.0),
2142 (5.0, 50.0),
2143 (20.0, 32.0),
2144 (23.0, 18.0),
2145 (35.0, 15.0),
2146 ])
2147 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2150 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2151 let nester = Nester2D::new(config);
2152
2153 let result = nester
2155 .solve_multi_strip(&[gear.clone()], &boundary)
2156 .unwrap();
2157
2158 println!(
2159 "Placed {} pieces across {} strips",
2160 result.placements.len(),
2161 result.boundaries_used
2162 );
2163
2164 let strip_width = 500.0;
2166 for (i, p) in result.placements.iter().enumerate() {
2167 let origin_x = p.position[0];
2168 let origin_y = p.position[1];
2169 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2170 let strip_idx = p.boundary_index;
2171
2172 let local_x = origin_x - (strip_idx as f64 * strip_width);
2174
2175 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2176
2177 let local_max_x = local_x + g_max_r[0];
2178 let local_max_y = origin_y + g_max_r[1];
2179
2180 println!(
2181 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2182 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2183 );
2184
2185 assert!(
2186 local_max_x <= 500.01,
2187 "Piece {}: In strip {}, local right edge {:.1} > 500",
2188 i,
2189 strip_idx,
2190 local_max_x
2191 );
2192 assert!(
2193 local_max_y <= 500.01,
2194 "Piece {}: Top edge {:.1} > 500",
2195 i,
2196 local_max_y
2197 );
2198 }
2199 }
2200
2201 #[test]
2202 fn test_blf_bounds_mixed_shapes() {
2203 let shapes = vec![
2205 Geometry2D::new("shape0")
2207 .with_polygon(vec![
2208 (0.0, 0.0),
2209 (180.0, 0.0),
2210 (195.0, 15.0),
2211 (200.0, 50.0),
2212 (200.0, 150.0),
2213 (195.0, 185.0),
2214 (180.0, 200.0),
2215 (20.0, 200.0),
2216 (5.0, 185.0),
2217 (0.0, 150.0),
2218 (0.0, 50.0),
2219 (5.0, 15.0),
2220 ])
2221 .with_quantity(2),
2222 Geometry2D::new("shape1")
2224 .with_polygon(vec![
2225 (60.0, 0.0),
2226 (85.0, 7.0),
2227 (104.0, 25.0),
2228 (118.0, 50.0),
2229 (120.0, 60.0),
2230 (118.0, 70.0),
2231 (104.0, 95.0),
2232 (85.0, 113.0),
2233 (60.0, 120.0),
2234 (35.0, 113.0),
2235 (16.0, 95.0),
2236 (2.0, 70.0),
2237 (0.0, 60.0),
2238 (2.0, 50.0),
2239 (16.0, 25.0),
2240 (35.0, 7.0),
2241 ])
2242 .with_quantity(4),
2243 Geometry2D::new("shape2")
2245 .with_polygon(vec![
2246 (0.0, 0.0),
2247 (80.0, 0.0),
2248 (80.0, 20.0),
2249 (20.0, 20.0),
2250 (20.0, 80.0),
2251 (0.0, 80.0),
2252 ])
2253 .with_quantity(6),
2254 Geometry2D::new("shape3")
2256 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2257 .with_quantity(6),
2258 Geometry2D::new("shape4")
2260 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2261 .with_quantity(4),
2262 Geometry2D::new("shape5")
2264 .with_polygon(vec![
2265 (15.0, 0.0),
2266 (45.0, 0.0),
2267 (60.0, 26.0),
2268 (45.0, 52.0),
2269 (15.0, 52.0),
2270 (0.0, 26.0),
2271 ])
2272 .with_quantity(8),
2273 Geometry2D::new("shape6")
2275 .with_polygon(vec![
2276 (0.0, 0.0),
2277 (90.0, 0.0),
2278 (90.0, 12.0),
2279 (55.0, 12.0),
2280 (55.0, 60.0),
2281 (35.0, 60.0),
2282 (35.0, 12.0),
2283 (0.0, 12.0),
2284 ])
2285 .with_quantity(4),
2286 Geometry2D::new("shape7")
2288 .with_polygon(vec![
2289 (0.0, 10.0),
2290 (10.0, 0.0),
2291 (70.0, 0.0),
2292 (80.0, 10.0),
2293 (80.0, 70.0),
2294 (70.0, 80.0),
2295 (10.0, 80.0),
2296 (0.0, 70.0),
2297 ])
2298 .with_quantity(3),
2299 Geometry2D::new("shape8_gear")
2301 .with_polygon(vec![
2302 (50.0, 5.0),
2303 (65.0, 15.0),
2304 (77.0, 18.0),
2305 (80.0, 32.0),
2306 (95.0, 50.0),
2307 (80.0, 68.0),
2308 (77.0, 82.0),
2309 (65.0, 85.0),
2310 (50.0, 95.0),
2311 (35.0, 85.0),
2312 (23.0, 82.0),
2313 (20.0, 68.0),
2314 (5.0, 50.0),
2315 (20.0, 32.0),
2316 (23.0, 18.0),
2317 (35.0, 15.0),
2318 ])
2319 .with_quantity(13),
2320 ];
2321
2322 let boundary = Boundary2D::rectangle(500.0, 500.0);
2324 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2325 let nester = Nester2D::new(config);
2326
2327 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2328
2329 println!(
2330 "Placed {} pieces across {} strips",
2331 result.placements.len(),
2332 result.boundaries_used
2333 );
2334
2335 let strip_width = 500.0;
2337 let gear_aabb = shapes[8].aabb();
2338 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2339
2340 let mut violations = Vec::new();
2341 for p in &result.placements {
2342 if p.geometry_id.as_str().starts_with("shape8") {
2343 let origin_x = p.position[0];
2344 let _origin_y = p.position[1];
2345 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2346 let strip_idx = p.boundary_index;
2347 let local_x = origin_x - (strip_idx as f64 * strip_width);
2348
2349 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2350 let local_max_x = local_x + g_max_r[0];
2351
2352 println!(
2353 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2354 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2355 );
2356
2357 if local_max_x > 500.01 {
2358 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2359 }
2360 }
2361 }
2362
2363 assert!(
2364 violations.is_empty(),
2365 "Found {} Gear pieces exceeding boundary: {:?}",
2366 violations.len(),
2367 violations
2368 );
2369 }
2370
2371 #[test]
2372 fn test_blf_bounds_expanded_like_benchmark() {
2373 let shape_defs: Vec<(Vec<(f64, f64)>, usize, Vec<f64>)> = vec![
2377 (
2378 vec![
2379 (0.0, 0.0),
2380 (180.0, 0.0),
2381 (195.0, 15.0),
2382 (200.0, 50.0),
2383 (200.0, 150.0),
2384 (195.0, 185.0),
2385 (180.0, 200.0),
2386 (20.0, 200.0),
2387 (5.0, 185.0),
2388 (0.0, 150.0),
2389 (0.0, 50.0),
2390 (5.0, 15.0),
2391 ],
2392 2,
2393 vec![0.0, 90.0, 180.0, 270.0],
2394 ),
2395 (
2396 vec![
2397 (60.0, 0.0),
2398 (85.0, 7.0),
2399 (104.0, 25.0),
2400 (118.0, 50.0),
2401 (120.0, 60.0),
2402 (118.0, 70.0),
2403 (104.0, 95.0),
2404 (85.0, 113.0),
2405 (60.0, 120.0),
2406 (35.0, 113.0),
2407 (16.0, 95.0),
2408 (2.0, 70.0),
2409 (0.0, 60.0),
2410 (2.0, 50.0),
2411 (16.0, 25.0),
2412 (35.0, 7.0),
2413 ],
2414 4,
2415 vec![0.0, 45.0, 90.0, 135.0],
2416 ),
2417 (
2418 vec![
2419 (0.0, 0.0),
2420 (80.0, 0.0),
2421 (80.0, 20.0),
2422 (20.0, 20.0),
2423 (20.0, 80.0),
2424 (0.0, 80.0),
2425 ],
2426 6,
2427 vec![0.0, 90.0, 180.0, 270.0],
2428 ),
2429 (
2430 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
2431 6,
2432 vec![0.0, 90.0, 180.0, 270.0],
2433 ),
2434 (
2435 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
2436 4,
2437 vec![0.0, 90.0],
2438 ),
2439 (
2440 vec![
2441 (15.0, 0.0),
2442 (45.0, 0.0),
2443 (60.0, 26.0),
2444 (45.0, 52.0),
2445 (15.0, 52.0),
2446 (0.0, 26.0),
2447 ],
2448 8,
2449 vec![0.0, 60.0, 120.0],
2450 ),
2451 (
2452 vec![
2453 (0.0, 0.0),
2454 (90.0, 0.0),
2455 (90.0, 12.0),
2456 (55.0, 12.0),
2457 (55.0, 60.0),
2458 (35.0, 60.0),
2459 (35.0, 12.0),
2460 (0.0, 12.0),
2461 ],
2462 4,
2463 vec![0.0, 90.0, 180.0, 270.0],
2464 ),
2465 (
2466 vec![
2467 (0.0, 10.0),
2468 (10.0, 0.0),
2469 (70.0, 0.0),
2470 (80.0, 10.0),
2471 (80.0, 70.0),
2472 (70.0, 80.0),
2473 (10.0, 80.0),
2474 (0.0, 70.0),
2475 ],
2476 3,
2477 vec![0.0, 90.0],
2478 ),
2479 (
2481 vec![
2482 (50.0, 5.0),
2483 (65.0, 15.0),
2484 (77.0, 18.0),
2485 (80.0, 32.0),
2486 (95.0, 50.0),
2487 (80.0, 68.0),
2488 (77.0, 82.0),
2489 (65.0, 85.0),
2490 (50.0, 95.0),
2491 (35.0, 85.0),
2492 (23.0, 82.0),
2493 (20.0, 68.0),
2494 (5.0, 50.0),
2495 (20.0, 32.0),
2496 (23.0, 18.0),
2497 (35.0, 15.0),
2498 ],
2499 13,
2500 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
2501 ),
2502 ];
2503
2504 let mut geometries = Vec::new();
2506 let mut piece_id = 0;
2507 for (vertices, demand, rotations) in shape_defs.iter() {
2508 for _ in 0..*demand {
2509 let geom = Geometry2D::new(format!("piece_{}", piece_id))
2510 .with_polygon(vertices.clone())
2511 .with_rotations_deg(rotations.clone());
2512 geometries.push(geom);
2513 piece_id += 1;
2514 }
2515 }
2516
2517 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
2519 let (gear_min, gear_max) = gear_geom.aabb();
2520 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
2521
2522 let boundary = Boundary2D::rectangle(500.0, 500.0);
2523 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2524 let nester = Nester2D::new(config);
2525
2526 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
2527
2528 println!(
2529 "Placed {} pieces across {} strips",
2530 result.placements.len(),
2531 result.boundaries_used
2532 );
2533
2534 let strip_width = 500.0;
2536 let mut violations = Vec::new();
2537
2538 for p in &result.placements {
2539 let id_num: usize = p
2540 .geometry_id
2541 .as_str()
2542 .strip_prefix("piece_")
2543 .and_then(|s| s.parse().ok())
2544 .unwrap_or(0);
2545
2546 if id_num >= 37 && id_num <= 49 {
2548 let origin_x = p.position[0];
2549 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2550 let strip_idx = p.boundary_index;
2551 let local_x = origin_x - (strip_idx as f64 * strip_width);
2552
2553 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
2554 let local_max_x = local_x + g_max_r[0];
2555
2556 println!(
2557 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2558 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2559 );
2560
2561 if local_max_x > 500.01 {
2562 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2563 }
2564 }
2565 }
2566
2567 assert!(
2568 violations.is_empty(),
2569 "Found {} Gear pieces exceeding boundary: {:?}",
2570 violations.len(),
2571 violations
2572 );
2573 }
2574
2575 fn aabbs_overlap(
2577 a_min: [f64; 2],
2578 a_max: [f64; 2],
2579 b_min: [f64; 2],
2580 b_max: [f64; 2],
2581 tolerance: f64,
2582 ) -> bool {
2583 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
2585 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
2586 x_overlap && y_overlap
2587 }
2588
2589 #[test]
2591 fn test_all_strategies_boundary_and_overlap() {
2592 use std::collections::HashMap;
2593
2594 let shapes = vec![
2596 Geometry2D::new("shape0")
2597 .with_polygon(vec![
2598 (0.0, 0.0),
2599 (180.0, 0.0),
2600 (195.0, 15.0),
2601 (200.0, 50.0),
2602 (200.0, 150.0),
2603 (195.0, 185.0),
2604 (180.0, 200.0),
2605 (20.0, 200.0),
2606 (5.0, 185.0),
2607 (0.0, 150.0),
2608 (0.0, 50.0),
2609 (5.0, 15.0),
2610 ])
2611 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2612 .with_quantity(2),
2613 Geometry2D::new("shape1_flange")
2614 .with_polygon(vec![
2615 (60.0, 0.0),
2616 (85.0, 7.0),
2617 (104.0, 25.0),
2618 (118.0, 50.0),
2619 (120.0, 60.0),
2620 (118.0, 70.0),
2621 (104.0, 95.0),
2622 (85.0, 113.0),
2623 (60.0, 120.0),
2624 (35.0, 113.0),
2625 (16.0, 95.0),
2626 (2.0, 70.0),
2627 (0.0, 60.0),
2628 (2.0, 50.0),
2629 (16.0, 25.0),
2630 (35.0, 7.0),
2631 ])
2632 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
2633 .with_quantity(4),
2634 Geometry2D::new("shape2_lbracket")
2635 .with_polygon(vec![
2636 (0.0, 0.0),
2637 (80.0, 0.0),
2638 (80.0, 20.0),
2639 (20.0, 20.0),
2640 (20.0, 80.0),
2641 (0.0, 80.0),
2642 ])
2643 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2644 .with_quantity(6),
2645 Geometry2D::new("shape3_triangle")
2646 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2647 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2648 .with_quantity(6),
2649 Geometry2D::new("shape4_rect")
2650 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2651 .with_rotations_deg(vec![0.0, 90.0])
2652 .with_quantity(4),
2653 Geometry2D::new("shape5_hexagon")
2654 .with_polygon(vec![
2655 (15.0, 0.0),
2656 (45.0, 0.0),
2657 (60.0, 26.0),
2658 (45.0, 52.0),
2659 (15.0, 52.0),
2660 (0.0, 26.0),
2661 ])
2662 .with_rotations_deg(vec![0.0, 60.0, 120.0])
2663 .with_quantity(8),
2664 Geometry2D::new("shape6_tstiff")
2665 .with_polygon(vec![
2666 (0.0, 0.0),
2667 (90.0, 0.0),
2668 (90.0, 12.0),
2669 (55.0, 12.0),
2670 (55.0, 60.0),
2671 (35.0, 60.0),
2672 (35.0, 12.0),
2673 (0.0, 12.0),
2674 ])
2675 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2676 .with_quantity(4),
2677 Geometry2D::new("shape7_mount")
2678 .with_polygon(vec![
2679 (0.0, 10.0),
2680 (10.0, 0.0),
2681 (70.0, 0.0),
2682 (80.0, 10.0),
2683 (80.0, 70.0),
2684 (70.0, 80.0),
2685 (10.0, 80.0),
2686 (0.0, 70.0),
2687 ])
2688 .with_rotations_deg(vec![0.0, 90.0])
2689 .with_quantity(3),
2690 Geometry2D::new("shape8_gear")
2691 .with_polygon(vec![
2692 (50.0, 5.0),
2693 (65.0, 15.0),
2694 (77.0, 18.0),
2695 (80.0, 32.0),
2696 (95.0, 50.0),
2697 (80.0, 68.0),
2698 (77.0, 82.0),
2699 (65.0, 85.0),
2700 (50.0, 95.0),
2701 (35.0, 85.0),
2702 (23.0, 82.0),
2703 (20.0, 68.0),
2704 (5.0, 50.0),
2705 (20.0, 32.0),
2706 (23.0, 18.0),
2707 (35.0, 15.0),
2708 ])
2709 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
2710 .with_quantity(13),
2711 ];
2712
2713 let geom_map: HashMap<String, &Geometry2D> =
2715 shapes.iter().map(|g| (g.id().clone(), g)).collect();
2716
2717 let boundary = Boundary2D::rectangle(500.0, 500.0);
2718 let strip_width = 500.0;
2719
2720 let strategies = vec![
2722 Strategy::BottomLeftFill,
2723 Strategy::NfpGuided,
2724 Strategy::GeneticAlgorithm,
2725 Strategy::Brkga,
2726 Strategy::SimulatedAnnealing,
2727 Strategy::Gdrr,
2728 Strategy::Alns,
2729 ];
2730
2731 for strategy in strategies {
2732 println!("\n========== Testing {:?} ==========", strategy);
2733
2734 let config = Config::default()
2735 .with_strategy(strategy)
2736 .with_time_limit(30000); let nester = Nester2D::new(config);
2738
2739 let result = match nester.solve_multi_strip(&shapes, &boundary) {
2740 Ok(r) => r,
2741 Err(e) => {
2742 println!(" Strategy {:?} failed: {}", strategy, e);
2743 continue;
2744 }
2745 };
2746
2747 println!(
2748 " Placed {} pieces across {} strips",
2749 result.placements.len(),
2750 result.boundaries_used
2751 );
2752
2753 let mut boundary_violations = Vec::new();
2755 for p in &result.placements {
2756 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
2758 let full_id = if base_id.starts_with("shape") {
2759 shapes
2761 .iter()
2762 .find(|g| p.geometry_id.starts_with(g.id()))
2763 .map(|g| g.id().as_str())
2764 } else {
2765 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
2766 };
2767
2768 let geom = match full_id.and_then(|id| geom_map.get(id)) {
2769 Some(g) => *g,
2770 None => {
2771 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
2773 Some(g) => g,
2774 None => {
2775 println!(
2776 " WARNING: Could not find geometry for {}",
2777 p.geometry_id
2778 );
2779 continue;
2780 }
2781 }
2782 }
2783 };
2784
2785 let origin_x = p.position[0];
2786 let origin_y = p.position[1];
2787 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2788 let strip_idx = p.boundary_index;
2789
2790 let local_x = origin_x - (strip_idx as f64 * strip_width);
2792
2793 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
2794
2795 let local_min_x = local_x + g_min[0];
2797 let local_max_x = local_x + g_max[0];
2798 let local_min_y = origin_y + g_min[1];
2799 let local_max_y = origin_y + g_max[1];
2800
2801 let tolerance = 0.1;
2803 if local_min_x < -tolerance
2804 || local_max_x > 500.0 + tolerance
2805 || local_min_y < -tolerance
2806 || local_max_y > 500.0 + tolerance
2807 {
2808 boundary_violations.push(format!(
2809 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
2810 p.geometry_id,
2811 strip_idx,
2812 local_min_x,
2813 local_min_y,
2814 local_max_x,
2815 local_max_y
2816 ));
2817 }
2818 }
2819
2820 if !boundary_violations.is_empty() {
2821 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
2822 for v in &boundary_violations {
2823 println!(" - {}", v);
2824 }
2825 }
2826
2827 let mut overlaps = Vec::new();
2829 let placements: Vec<_> = result.placements.iter().collect();
2830
2831 for i in 0..placements.len() {
2832 for j in (i + 1)..placements.len() {
2833 let p1 = placements[i];
2834 let p2 = placements[j];
2835
2836 if p1.boundary_index != p2.boundary_index {
2838 continue;
2839 }
2840
2841 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
2843 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
2844
2845 let (g1, g2) = match (g1, g2) {
2846 (Some(a), Some(b)) => (a, b),
2847 _ => continue,
2848 };
2849
2850 let strip_idx = p1.boundary_index;
2851 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
2852 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
2853
2854 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
2855 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
2856
2857 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
2858 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
2859
2860 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
2861 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
2862 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
2863 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
2864
2865 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
2866 overlaps.push(format!(
2867 "{} and {} in strip {}",
2868 p1.geometry_id, p2.geometry_id, strip_idx
2869 ));
2870 }
2871 }
2872 }
2873
2874 if !overlaps.is_empty() {
2875 println!(" OVERLAPS ({}):", overlaps.len());
2876 for o in overlaps.iter().take(10) {
2877 println!(" - {}", o);
2878 }
2879 if overlaps.len() > 10 {
2880 println!(" ... and {} more", overlaps.len() - 10);
2881 }
2882 }
2883
2884 assert!(
2886 boundary_violations.is_empty(),
2887 "{:?}: Found {} boundary violations",
2888 strategy,
2889 boundary_violations.len()
2890 );
2891
2892 println!(" ✓ All placements within boundary");
2893 println!(" ✓ No AABB overlaps detected");
2894 }
2895 }
2896}