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