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)
462 .max(5000)
463 .min(self.config.time_limit_ms)
464 } else {
465 15000 };
467
468 let ga_config = GaConfig::default()
469 .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)
472 .with_mutation_rate(self.config.mutation_rate)
473 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
474
475 let result = run_ga_nesting(
476 geometries,
477 boundary,
478 &self.config,
479 ga_config,
480 self.cancelled.clone(),
481 );
482
483 Ok(result)
484 }
485
486 fn brkga(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
490 let time_limit_ms = if self.config.time_limit_ms > 0 {
492 (self.config.time_limit_ms / 4)
497 .max(5000)
498 .min(self.config.time_limit_ms)
499 } else {
500 15000 };
502
503 let brkga_config = BrkgaConfig::default()
504 .with_population_size(30) .with_max_generations(50) .with_elite_fraction(0.2)
507 .with_mutant_fraction(0.15)
508 .with_elite_bias(0.7)
509 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
510
511 let result = run_brkga_nesting(
512 geometries,
513 boundary,
514 &self.config,
515 brkga_config,
516 self.cancelled.clone(),
517 );
518
519 Ok(result)
520 }
521
522 fn simulated_annealing(
527 &self,
528 geometries: &[Geometry2D],
529 boundary: &Boundary2D,
530 ) -> Result<SolveResult<f64>> {
531 let time_limit_ms = if self.config.time_limit_ms > 0 {
534 (self.config.time_limit_ms / 4)
539 .max(5000)
540 .min(self.config.time_limit_ms)
541 } else {
542 10000 };
544
545 let sa_config = SaConfig::default()
546 .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));
552
553 let result = run_sa_nesting(
554 geometries,
555 boundary,
556 &self.config,
557 sa_config,
558 self.cancelled.clone(),
559 );
560
561 Ok(result)
562 }
563
564 fn gdrr(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
566 let time_limit = if self.config.time_limit_ms > 0 {
569 (self.config.time_limit_ms / 4)
574 .max(5000)
575 .min(self.config.time_limit_ms)
576 } else {
577 10000 };
579 let gdrr_config = GdrrConfig::default()
580 .with_max_iterations(1000) .with_time_limit_ms(time_limit)
582 .with_ruin_ratio(0.1, 0.3) .with_lahc_list_length(30); let result = run_gdrr_nesting(
586 geometries,
587 boundary,
588 &self.config,
589 &gdrr_config,
590 self.cancelled.clone(),
591 );
592
593 Ok(result)
594 }
595
596 fn alns(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
598 let time_limit = if self.config.time_limit_ms > 0 {
601 (self.config.time_limit_ms / 4)
606 .max(5000)
607 .min(self.config.time_limit_ms)
608 } else {
609 10000 };
611 let alns_config = AlnsConfig::default()
612 .with_max_iterations(1000) .with_time_limit_ms(time_limit)
614 .with_segment_size(50) .with_scores(33.0, 9.0, 13.0)
616 .with_reaction_factor(0.15) .with_temperature(100.0, 0.999, 0.1); let result = run_alns_nesting(
620 geometries,
621 boundary,
622 &self.config,
623 &alns_config,
624 self.cancelled.clone(),
625 );
626
627 Ok(result)
628 }
629
630 #[cfg(feature = "milp")]
632 fn milp_exact(
633 &self,
634 geometries: &[Geometry2D],
635 boundary: &Boundary2D,
636 ) -> Result<SolveResult<f64>> {
637 let exact_config = ExactConfig::default()
638 .with_time_limit_ms(self.config.time_limit_ms.max(60000))
639 .with_max_items(15)
640 .with_rotation_steps(4)
641 .with_grid_step(1.0);
642
643 let result = run_milp_nesting(
644 geometries,
645 boundary,
646 &self.config,
647 &exact_config,
648 self.cancelled.clone(),
649 );
650
651 Ok(result)
652 }
653
654 #[cfg(feature = "milp")]
656 fn hybrid_exact(
657 &self,
658 geometries: &[Geometry2D],
659 boundary: &Boundary2D,
660 ) -> Result<SolveResult<f64>> {
661 let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
663
664 if total_instances <= 15 {
666 let exact_config = ExactConfig::default()
667 .with_time_limit_ms((self.config.time_limit_ms / 2).max(30000))
668 .with_max_items(15);
669
670 let exact_result = run_milp_nesting(
671 geometries,
672 boundary,
673 &self.config,
674 &exact_config,
675 self.cancelled.clone(),
676 );
677
678 if !exact_result.placements.is_empty() {
680 return Ok(exact_result);
681 }
682 }
683
684 self.alns(geometries, boundary)
686 }
687
688 fn bottom_left_fill_with_progress(
690 &self,
691 geometries: &[Geometry2D],
692 boundary: &Boundary2D,
693 callback: &ProgressCallback,
694 ) -> Result<SolveResult<f64>> {
695 let start = Timer::now();
696 let mut result = SolveResult::new();
697 let mut placements = Vec::new();
698
699 let (b_min, b_max) = boundary.aabb();
701 let margin = self.config.margin;
702 let spacing = self.config.spacing;
703
704 let bound_min_x = b_min[0] + margin;
705 let bound_min_y = b_min[1] + margin;
706 let bound_max_x = b_max[0] - margin;
707 let bound_max_y = b_max[1] - margin;
708
709 let strip_width = bound_max_x - bound_min_x;
710 let strip_height = bound_max_y - bound_min_y;
711
712 let mut current_x = bound_min_x;
713 let mut current_y = bound_min_y;
714 let mut row_height = 0.0_f64;
715 let mut total_placed_area = 0.0;
716
717 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
719 let mut placed_count = 0usize;
720
721 callback(
723 ProgressInfo::new()
724 .with_phase("BLF Placement")
725 .with_items(0, total_pieces)
726 .with_elapsed(0),
727 );
728
729 for geom in geometries {
730 geom.validate()?;
731
732 let rotations = geom.rotations();
733 let rotation_angles: Vec<f64> = if rotations.is_empty() {
734 vec![0.0]
735 } else {
736 rotations
737 };
738
739 for instance in 0..geom.quantity() {
740 if self.cancelled.load(Ordering::Relaxed) {
741 result.computation_time_ms = start.elapsed_ms();
742 callback(
743 ProgressInfo::new()
744 .with_phase("Cancelled")
745 .with_items(placed_count, total_pieces)
746 .with_elapsed(result.computation_time_ms)
747 .finished(),
748 );
749 return Ok(result);
750 }
751
752 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
754 {
755 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
756 result.utilization = total_placed_area / boundary.measure();
757 result.computation_time_ms = start.elapsed_ms();
758 result.placements = placements;
759 callback(
760 ProgressInfo::new()
761 .with_phase("Time Limit Reached")
762 .with_items(placed_count, total_pieces)
763 .with_elapsed(result.computation_time_ms)
764 .finished(),
765 );
766 return Ok(result);
767 }
768
769 let mut best_fit: Option<(f64, f64, f64, f64, f64, [f64; 2])> = None;
770
771 for &rotation in &rotation_angles {
772 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
773 let g_width = g_max[0] - g_min[0];
774 let g_height = g_max[1] - g_min[1];
775
776 if g_width > strip_width || g_height > strip_height {
777 continue;
778 }
779
780 let mut place_x = current_x;
781 let mut place_y = current_y;
782
783 if place_x + g_width > bound_max_x {
784 place_x = bound_min_x;
785 place_y += row_height + spacing;
786 }
787
788 if place_y + g_height > bound_max_y {
789 continue;
790 }
791
792 let score = if place_x == bound_min_x && place_y > current_y {
793 place_y - bound_min_y + g_height
794 } else {
795 place_x - bound_min_x + g_width
796 };
797
798 let is_better = match &best_fit {
799 None => true,
800 Some((_, _, _, bx, by, _)) => {
801 let best_score = if *bx == bound_min_x && *by > current_y {
802 by - bound_min_y
803 } else {
804 bx - bound_min_x
805 };
806 score < best_score - 1e-6
807 }
808 };
809
810 if is_better {
811 best_fit = Some((rotation, g_width, g_height, place_x, place_y, g_min));
812 }
813 }
814
815 if let Some((rotation, g_width, g_height, place_x, place_y, g_min)) = best_fit {
816 if place_x == bound_min_x && place_y > current_y {
817 row_height = 0.0;
818 }
819
820 let origin_x = place_x - g_min[0];
822 let origin_y = place_y - g_min[1];
823
824 let geom_aabb = geom.aabb_at_rotation(rotation);
826 let boundary_aabb = (b_min, b_max);
827
828 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
829 origin_x,
830 origin_y,
831 geom_aabb,
832 boundary_aabb,
833 margin,
834 ) {
835 let placement = Placement::new_2d(
836 geom.id().clone(),
837 instance,
838 clamped_x,
839 clamped_y,
840 rotation,
841 );
842
843 placements.push(placement);
844 total_placed_area += geom.measure();
845 placed_count += 1;
846
847 current_x = place_x + g_width + spacing;
848 current_y = place_y;
849 row_height = row_height.max(g_height);
850
851 callback(
853 ProgressInfo::new()
854 .with_phase("BLF Placement")
855 .with_items(placed_count, total_pieces)
856 .with_utilization(total_placed_area / boundary.measure())
857 .with_elapsed(start.elapsed_ms()),
858 );
859 } else {
860 result.unplaced.push(geom.id().clone());
861 }
862 } else {
863 result.unplaced.push(geom.id().clone());
864 }
865 }
866 }
867
868 result.placements = placements;
869 result.boundaries_used = 1;
870 result.utilization = total_placed_area / boundary.measure();
871 result.computation_time_ms = start.elapsed_ms();
872
873 callback(
875 ProgressInfo::new()
876 .with_phase("Complete")
877 .with_items(placed_count, total_pieces)
878 .with_utilization(result.utilization)
879 .with_elapsed(result.computation_time_ms)
880 .finished(),
881 );
882
883 Ok(result)
884 }
885
886 fn nfp_guided_blf_with_progress(
888 &self,
889 geometries: &[Geometry2D],
890 boundary: &Boundary2D,
891 callback: &ProgressCallback,
892 ) -> Result<SolveResult<f64>> {
893 let start = Timer::now();
894 let mut result = SolveResult::new();
895 let mut placements = Vec::new();
896 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
897
898 let margin = self.config.margin;
899 let spacing = self.config.spacing;
900 let boundary_polygon = self.get_boundary_polygon_with_margin(boundary, margin);
901
902 let mut total_placed_area = 0.0;
903 let sample_step = self.compute_sample_step(geometries);
904
905 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
907 let mut placed_count = 0usize;
908
909 callback(
911 ProgressInfo::new()
912 .with_phase("NFP Placement")
913 .with_items(0, total_pieces)
914 .with_elapsed(0),
915 );
916
917 for geom in geometries {
918 geom.validate()?;
919
920 let rotations = geom.rotations();
921 let rotation_angles: Vec<f64> = if rotations.is_empty() {
922 vec![0.0]
923 } else {
924 rotations
925 };
926
927 for instance in 0..geom.quantity() {
928 if self.cancelled.load(Ordering::Relaxed) {
929 result.computation_time_ms = start.elapsed_ms();
930 callback(
931 ProgressInfo::new()
932 .with_phase("Cancelled")
933 .with_items(placed_count, total_pieces)
934 .with_elapsed(result.computation_time_ms)
935 .finished(),
936 );
937 return Ok(result);
938 }
939
940 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
942 {
943 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
944 result.utilization = total_placed_area / boundary.measure();
945 result.computation_time_ms = start.elapsed_ms();
946 result.placements = placements;
947 callback(
948 ProgressInfo::new()
949 .with_phase("Time Limit Reached")
950 .with_items(placed_count, total_pieces)
951 .with_elapsed(result.computation_time_ms)
952 .finished(),
953 );
954 return Ok(result);
955 }
956
957 let mut best_placement: Option<(f64, f64, f64)> = None;
958
959 for &rotation in &rotation_angles {
960 let ifp =
961 match compute_ifp_with_margin(&boundary_polygon, geom, rotation, margin) {
962 Ok(ifp) => ifp,
963 Err(_) => continue,
964 };
965
966 if ifp.is_empty() {
967 continue;
968 }
969
970 let mut nfps: Vec<Nfp> = Vec::new();
971 for placed in &placed_geometries {
972 let cache_key = (
974 placed.geometry.id().as_str(),
975 geom.id().as_str(),
976 rotation - placed.rotation,
977 );
978
979 let nfp_at_origin = match self.nfp_cache.get_or_compute(cache_key, || {
982 let placed_at_origin = placed.geometry.clone();
983 compute_nfp(&placed_at_origin, geom, rotation - placed.rotation)
984 }) {
985 Ok(nfp) => nfp,
986 Err(_) => continue,
987 };
988
989 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
991 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
992 let expanded = self.expand_nfp(&translated_nfp, spacing);
993 nfps.push(expanded);
994 }
995
996 let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
997 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
998
999 if let Some((x, y)) =
1000 find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
1001 {
1002 let is_better = match best_placement {
1003 None => true,
1004 Some((best_x, best_y, _)) => {
1005 x < best_x - 1e-6 || (x < best_x + 1e-6 && y < best_y - 1e-6)
1006 }
1007 };
1008 if is_better {
1009 best_placement = Some((x, y, rotation));
1010 }
1011 }
1012 }
1013
1014 if let Some((x, y, rotation)) = best_placement {
1015 let geom_aabb = geom.aabb_at_rotation(rotation);
1017 let boundary_aabb = boundary.aabb();
1018
1019 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
1020 x,
1021 y,
1022 geom_aabb,
1023 boundary_aabb,
1024 margin,
1025 ) {
1026 let placement = Placement::new_2d(
1027 geom.id().clone(),
1028 instance,
1029 clamped_x,
1030 clamped_y,
1031 rotation,
1032 );
1033 placements.push(placement);
1034 placed_geometries.push(PlacedGeometry::new(
1035 geom.clone(),
1036 (clamped_x, clamped_y),
1037 rotation,
1038 ));
1039 total_placed_area += geom.measure();
1040 placed_count += 1;
1041
1042 callback(
1044 ProgressInfo::new()
1045 .with_phase("NFP Placement")
1046 .with_items(placed_count, total_pieces)
1047 .with_utilization(total_placed_area / boundary.measure())
1048 .with_elapsed(start.elapsed_ms()),
1049 );
1050 } else {
1051 result.unplaced.push(geom.id().clone());
1052 }
1053 } else {
1054 result.unplaced.push(geom.id().clone());
1055 }
1056 }
1057 }
1058
1059 result.placements = placements;
1060 result.boundaries_used = 1;
1061 result.utilization = total_placed_area / boundary.measure();
1062 result.computation_time_ms = start.elapsed_ms();
1063
1064 callback(
1066 ProgressInfo::new()
1067 .with_phase("Complete")
1068 .with_items(placed_count, total_pieces)
1069 .with_utilization(result.utilization)
1070 .with_elapsed(result.computation_time_ms)
1071 .finished(),
1072 );
1073
1074 Ok(result)
1075 }
1076
1077 pub fn solve_multi_strip(
1083 &self,
1084 geometries: &[Geometry2D],
1085 boundary: &Boundary2D,
1086 ) -> Result<SolveResult<f64>> {
1087 boundary.validate()?;
1088 self.cancelled.store(false, Ordering::Relaxed);
1089
1090 let (b_min, b_max) = boundary.aabb();
1091 let strip_width = b_max[0] - b_min[0];
1092
1093 let mut final_result = SolveResult::new();
1094 let mut remaining_geometries: Vec<Geometry2D> = geometries.to_vec();
1095 let mut strip_index = 0;
1096 let max_strips = 100; let mut placed_total: std::collections::HashMap<String, usize> =
1101 std::collections::HashMap::new();
1102
1103 while !remaining_geometries.is_empty() && strip_index < max_strips {
1104 if self.cancelled.load(Ordering::Relaxed) {
1105 break;
1106 }
1107
1108 let strip_result = match self.config.strategy {
1110 Strategy::BottomLeftFill => self.bottom_left_fill(&remaining_geometries, boundary),
1111 Strategy::NfpGuided => self.nfp_guided_blf(&remaining_geometries, boundary),
1112 Strategy::GeneticAlgorithm => {
1113 self.genetic_algorithm(&remaining_geometries, boundary)
1114 }
1115 Strategy::Brkga => self.brkga(&remaining_geometries, boundary),
1116 Strategy::SimulatedAnnealing => {
1117 self.simulated_annealing(&remaining_geometries, boundary)
1118 }
1119 Strategy::Gdrr => self.gdrr(&remaining_geometries, boundary),
1120 Strategy::Alns => self.alns(&remaining_geometries, boundary),
1121 #[cfg(feature = "milp")]
1122 Strategy::MilpExact => self.milp_exact(&remaining_geometries, boundary),
1123 #[cfg(feature = "milp")]
1124 Strategy::HybridExact => self.hybrid_exact(&remaining_geometries, boundary),
1125 _ => self.nfp_guided_blf(&remaining_geometries, boundary),
1126 }?;
1127
1128 let strip_result =
1130 validate_and_filter_placements(strip_result, &remaining_geometries, boundary);
1131
1132 if strip_result.placements.is_empty() {
1133 break;
1136 }
1137
1138 let mut strip_placed: std::collections::HashMap<String, usize> =
1141 std::collections::HashMap::new();
1142
1143 for mut placement in strip_result.placements {
1145 let gid = placement.geometry_id.clone();
1146 let prior = placed_total.get(&gid).copied().unwrap_or(0);
1149 let in_strip = strip_placed.get(&gid).copied().unwrap_or(0);
1150 placement.instance = prior + in_strip;
1151 if !placement.position.is_empty() {
1153 placement.position[0] += strip_index as f64 * strip_width;
1154 }
1155 placement.boundary_index = strip_index;
1156 *strip_placed.entry(gid).or_insert(0) += 1;
1157 final_result.placements.push(placement);
1158 }
1159
1160 for (gid, cnt) in &strip_placed {
1164 *placed_total.entry(gid.clone()).or_insert(0) += cnt;
1165 }
1166 remaining_geometries = remaining_geometries
1167 .into_iter()
1168 .filter_map(|g| {
1169 let placed_here = strip_placed.get(g.id()).copied().unwrap_or(0);
1170 let new_quantity = g.quantity().saturating_sub(placed_here);
1171 if new_quantity == 0 {
1172 None
1173 } else {
1174 Some(g.with_quantity(new_quantity))
1175 }
1176 })
1177 .collect();
1178
1179 strip_index += 1;
1180 }
1181
1182 for g in &remaining_geometries {
1185 final_result.unplaced.push(g.id().clone());
1186 }
1187
1188 final_result.boundaries_used = strip_index;
1189 final_result.deduplicate_unplaced();
1190 final_result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1192
1193 let (b_min, b_max) = boundary.aabb();
1195 let strip_height = b_max[1] - b_min[1]; let mut strip_stats_map: std::collections::HashMap<usize, (f64, f64, usize)> =
1199 std::collections::HashMap::new(); for placement in &final_result.placements {
1202 let strip_idx = placement.boundary_index;
1203 if let Some(geom) = geometries.iter().find(|g| g.id() == &placement.geometry_id) {
1205 use u_nesting_core::geometry::Geometry;
1206 let piece_area = geom.measure();
1207 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1208 let (_g_min, g_max) = geom.aabb_at_rotation(rotation);
1209 let local_x = placement.position[0] - (strip_idx as f64 * strip_width);
1212 let right_edge = local_x + g_max[0];
1213
1214 let entry = strip_stats_map.entry(strip_idx).or_insert((0.0, 0.0, 0));
1215 entry.0 = entry.0.max(right_edge); entry.1 += piece_area; entry.2 += 1; }
1219 }
1220
1221 use u_nesting_core::result::StripStats;
1223 let mut strip_stats: Vec<StripStats> = strip_stats_map
1224 .into_iter()
1225 .map(|(idx, (used_length, piece_area, count))| StripStats {
1226 strip_index: idx,
1227 used_length,
1228 piece_area,
1229 piece_count: count,
1230 strip_width, strip_height, })
1233 .collect();
1234 strip_stats.sort_by_key(|s| s.strip_index);
1235
1236 let total_piece_area: f64 = strip_stats.iter().map(|s| s.piece_area).sum();
1239 let total_material_used: f64 = strip_stats
1240 .iter()
1241 .map(|s| s.strip_height * s.used_length)
1242 .sum();
1243
1244 final_result.strip_stats = strip_stats;
1245 final_result.total_piece_area = total_piece_area;
1246 final_result.total_material_used = total_material_used;
1247
1248 if total_material_used > 0.0 {
1249 final_result.utilization = total_piece_area / total_material_used;
1250 }
1251
1252 Ok(final_result)
1253 }
1254}
1255
1256impl Solver for Nester2D {
1257 type Geometry = Geometry2D;
1258 type Boundary = Boundary2D;
1259 type Scalar = f64;
1260
1261 fn solve(
1262 &self,
1263 geometries: &[Self::Geometry],
1264 boundary: &Self::Boundary,
1265 ) -> Result<SolveResult<f64>> {
1266 boundary.validate()?;
1267
1268 self.cancelled.store(false, Ordering::Relaxed);
1270
1271 let initial_result = match self.config.strategy {
1272 Strategy::BottomLeftFill => self.bottom_left_fill(geometries, boundary),
1273 Strategy::NfpGuided => self.nfp_guided_blf(geometries, boundary),
1274 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
1275 Strategy::Brkga => self.brkga(geometries, boundary),
1276 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
1277 Strategy::Gdrr => self.gdrr(geometries, boundary),
1278 Strategy::Alns => self.alns(geometries, boundary),
1279 #[cfg(feature = "milp")]
1280 Strategy::MilpExact => self.milp_exact(geometries, boundary),
1281 #[cfg(feature = "milp")]
1282 Strategy::HybridExact => self.hybrid_exact(geometries, boundary),
1283 _ => {
1284 log::warn!(
1286 "Strategy {:?} not yet implemented, using NfpGuided",
1287 self.config.strategy
1288 );
1289 self.nfp_guided_blf(geometries, boundary)
1290 }
1291 }?;
1292
1293 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1295
1296 result.deduplicate_unplaced();
1298 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1301 Ok(result)
1302 }
1303
1304 fn solve_with_progress(
1305 &self,
1306 geometries: &[Self::Geometry],
1307 boundary: &Self::Boundary,
1308 callback: ProgressCallback,
1309 ) -> Result<SolveResult<f64>> {
1310 boundary.validate()?;
1311
1312 self.cancelled.store(false, Ordering::Relaxed);
1314
1315 let initial_result = match self.config.strategy {
1316 Strategy::BottomLeftFill => {
1317 self.bottom_left_fill_with_progress(geometries, boundary, &callback)?
1318 }
1319 Strategy::NfpGuided => {
1320 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1321 }
1322 Strategy::GeneticAlgorithm => {
1323 let mut ga_config = GaConfig::default()
1324 .with_population_size(self.config.population_size)
1325 .with_max_generations(self.config.max_generations)
1326 .with_crossover_rate(self.config.crossover_rate)
1327 .with_mutation_rate(self.config.mutation_rate);
1328
1329 if self.config.time_limit_ms > 0 {
1331 ga_config = ga_config.with_time_limit(std::time::Duration::from_millis(
1332 self.config.time_limit_ms,
1333 ));
1334 }
1335
1336 run_ga_nesting_with_progress(
1337 geometries,
1338 boundary,
1339 &self.config,
1340 ga_config,
1341 self.cancelled.clone(),
1342 callback,
1343 )
1344 }
1345 _ => {
1347 log::warn!(
1348 "Strategy {:?} not yet implemented, using NfpGuided",
1349 self.config.strategy
1350 );
1351 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1352 }
1353 };
1354
1355 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1357
1358 result.deduplicate_unplaced();
1360 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1363 Ok(result)
1364 }
1365
1366 fn cancel(&self) {
1367 self.cancelled.store(true, Ordering::Relaxed);
1368 }
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373 use super::*;
1374 use crate::placement_utils::polygon_centroid;
1375
1376 #[test]
1377 fn test_simple_nesting() {
1378 let geometries = vec![
1379 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1380 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1381 ];
1382
1383 let boundary = Boundary2D::rectangle(100.0, 50.0);
1384 let nester = Nester2D::default_config();
1385
1386 let result = nester.solve(&geometries, &boundary).unwrap();
1387
1388 assert!(result.utilization > 0.0);
1389 assert!(result.placements.len() <= 5); }
1391
1392 #[test]
1393 fn test_placement_within_bounds() {
1394 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1395
1396 let boundary = Boundary2D::rectangle(50.0, 50.0);
1397 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1398 let nester = Nester2D::new(config);
1399
1400 let result = nester.solve(&geometries, &boundary).unwrap();
1401
1402 assert_eq!(result.placements.len(), 4);
1404 assert!(result.unplaced.is_empty());
1405
1406 for p in &result.placements {
1408 assert!(p.position[0] >= 5.0);
1409 assert!(p.position[1] >= 5.0);
1410 }
1411 }
1412
1413 #[test]
1414 fn test_nfp_guided_basic() {
1415 let geometries = vec![
1416 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1417 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1418 ];
1419
1420 let boundary = Boundary2D::rectangle(100.0, 50.0);
1421 let config = Config::default().with_strategy(Strategy::NfpGuided);
1422 let nester = Nester2D::new(config);
1423
1424 let result = nester.solve(&geometries, &boundary).unwrap();
1425
1426 assert!(result.utilization > 0.0);
1427 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1429 }
1430
1431 #[test]
1432 fn test_nfp_guided_with_spacing() {
1433 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1434
1435 let boundary = Boundary2D::rectangle(50.0, 50.0);
1436 let config = Config::default()
1437 .with_strategy(Strategy::NfpGuided)
1438 .with_margin(2.0)
1439 .with_spacing(3.0);
1440 let nester = Nester2D::new(config);
1441
1442 let result = nester.solve(&geometries, &boundary).unwrap();
1443
1444 assert_eq!(result.placements.len(), 4);
1446 assert!(result.unplaced.is_empty());
1447
1448 assert!(result.utilization > 0.0);
1450 }
1451
1452 #[test]
1453 fn test_nfp_guided_no_overlap() {
1454 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1455
1456 let boundary = Boundary2D::rectangle(100.0, 100.0);
1457 let config = Config::default().with_strategy(Strategy::NfpGuided);
1458 let nester = Nester2D::new(config);
1459
1460 let result = nester.solve(&geometries, &boundary).unwrap();
1461
1462 assert_eq!(result.placements.len(), 3);
1463
1464 for i in 0..result.placements.len() {
1466 for j in (i + 1)..result.placements.len() {
1467 let p1 = &result.placements[i];
1468 let p2 = &result.placements[j];
1469
1470 let r1_min_x = p1.position[0];
1472 let r1_max_x = p1.position[0] + 20.0;
1473 let r1_min_y = p1.position[1];
1474 let r1_max_y = p1.position[1] + 20.0;
1475
1476 let r2_min_x = p2.position[0];
1477 let r2_max_x = p2.position[0] + 20.0;
1478 let r2_min_y = p2.position[1];
1479 let r2_max_y = p2.position[1] + 20.0;
1480
1481 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1483 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1484
1485 assert!(
1486 !(overlaps_x && overlaps_y),
1487 "Placements {} and {} overlap",
1488 i,
1489 j
1490 );
1491 }
1492 }
1493 }
1494
1495 #[test]
1496 fn test_nfp_guided_utilization() {
1497 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
1499
1500 let boundary = Boundary2D::rectangle(100.0, 50.0);
1501 let config = Config::default().with_strategy(Strategy::NfpGuided);
1502 let nester = Nester2D::new(config);
1503
1504 let result = nester.solve(&geometries, &boundary).unwrap();
1505
1506 assert_eq!(result.placements.len(), 4);
1508
1509 assert!(result.utilization > 0.45);
1511 }
1512
1513 #[test]
1514 fn test_polygon_centroid() {
1515 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1517 let (cx, cy) = polygon_centroid(&square);
1518 assert!((cx - 5.0).abs() < 0.01);
1519 assert!((cy - 5.0).abs() < 0.01);
1520
1521 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
1522 let (cx, cy) = polygon_centroid(&triangle);
1523 assert!((cx - 3.0).abs() < 0.01);
1524 assert!((cy - 2.0).abs() < 0.01);
1525 }
1526
1527 #[test]
1528 fn test_ga_strategy_basic() {
1529 let geometries = vec![
1530 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1531 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1532 ];
1533
1534 let boundary = Boundary2D::rectangle(100.0, 50.0);
1535 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1536 let nester = Nester2D::new(config);
1537
1538 let result = nester.solve(&geometries, &boundary).unwrap();
1539
1540 assert!(result.utilization > 0.0);
1541 assert!(!result.placements.is_empty());
1542 assert!(result.generations.is_some());
1544 assert!(result.best_fitness.is_some());
1545 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
1546 }
1547
1548 #[test]
1549 fn test_ga_strategy_all_placed() {
1550 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1552
1553 let boundary = Boundary2D::rectangle(100.0, 100.0);
1554 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1555 let nester = Nester2D::new(config);
1556
1557 let result = nester.solve(&geometries, &boundary).unwrap();
1558
1559 assert_eq!(result.placements.len(), 4);
1561 assert!(result.unplaced.is_empty());
1562 }
1563
1564 #[test]
1565 fn test_brkga_strategy_basic() {
1566 let geometries = vec![
1567 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1568 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1569 ];
1570
1571 let boundary = Boundary2D::rectangle(100.0, 50.0);
1572 let config = Config::default().with_strategy(Strategy::Brkga);
1573 let nester = Nester2D::new(config);
1574
1575 let result = nester.solve(&geometries, &boundary).unwrap();
1576
1577 assert!(result.utilization > 0.0);
1578 assert!(!result.placements.is_empty());
1579 assert!(result.generations.is_some());
1581 assert!(result.best_fitness.is_some());
1582 assert!(result.strategy == Some("BRKGA".to_string()));
1583 }
1584
1585 #[test]
1586 fn test_brkga_strategy_all_placed() {
1587 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1589
1590 let boundary = Boundary2D::rectangle(100.0, 100.0);
1591 let config = Config::default()
1593 .with_strategy(Strategy::Brkga)
1594 .with_time_limit(30000); let nester = Nester2D::new(config);
1596
1597 let result = nester.solve(&geometries, &boundary).unwrap();
1598
1599 assert!(
1602 result.placements.len() >= 3,
1603 "Expected at least 3 placements, got {}",
1604 result.placements.len()
1605 );
1606 }
1607
1608 #[test]
1609 fn test_gdrr_strategy_basic() {
1610 let geometries = vec![
1611 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1612 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1613 ];
1614
1615 let boundary = Boundary2D::rectangle(100.0, 50.0);
1616 let config = Config::default().with_strategy(Strategy::Gdrr);
1617 let nester = Nester2D::new(config);
1618
1619 let result = nester.solve(&geometries, &boundary).unwrap();
1620
1621 assert!(result.utilization > 0.0);
1622 assert!(!result.placements.is_empty());
1623 assert!(result.iterations.is_some());
1625 assert!(result.best_fitness.is_some());
1626 assert!(result.strategy == Some("GDRR".to_string()));
1627 }
1628
1629 #[test]
1630 fn test_gdrr_strategy_all_placed() {
1631 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1633
1634 let boundary = Boundary2D::rectangle(100.0, 100.0);
1635 let config = Config::default().with_strategy(Strategy::Gdrr);
1636 let nester = Nester2D::new(config);
1637
1638 let result = nester.solve(&geometries, &boundary).unwrap();
1639
1640 assert_eq!(result.placements.len(), 4);
1642 assert!(result.unplaced.is_empty());
1643 }
1644
1645 #[test]
1646 fn test_alns_strategy_basic() {
1647 let geometries = vec![
1648 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1649 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1650 ];
1651
1652 let boundary = Boundary2D::rectangle(100.0, 50.0);
1653 let config = Config::default().with_strategy(Strategy::Alns);
1654 let nester = Nester2D::new(config);
1655
1656 let result = nester.solve(&geometries, &boundary).unwrap();
1657
1658 assert!(result.utilization > 0.0);
1659 assert!(!result.placements.is_empty());
1660 assert!(result.iterations.is_some());
1662 assert!(result.best_fitness.is_some());
1663 assert!(result.strategy == Some("ALNS".to_string()));
1664 }
1665
1666 #[test]
1667 fn test_alns_strategy_all_placed() {
1668 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1670
1671 let boundary = Boundary2D::rectangle(100.0, 100.0);
1672 let config = Config::default().with_strategy(Strategy::Alns);
1673 let nester = Nester2D::new(config);
1674
1675 let result = nester.solve(&geometries, &boundary).unwrap();
1676
1677 assert_eq!(result.placements.len(), 4);
1679 assert!(result.unplaced.is_empty());
1680 }
1681
1682 #[test]
1683 fn test_blf_rotation_optimization() {
1684 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
1687 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
1689
1690 let boundary = Boundary2D::rectangle(35.0, 95.0);
1693 let nester = Nester2D::default_config();
1694
1695 let result = nester.solve(&geometries, &boundary).unwrap();
1696
1697 assert_eq!(
1699 result.placements.len(),
1700 3,
1701 "All pieces should be placed with rotation optimization"
1702 );
1703 assert!(result.unplaced.is_empty());
1704 }
1705
1706 #[test]
1707 fn test_blf_selects_best_rotation() {
1708 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
1710 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
1712
1713 let boundary = Boundary2D::rectangle(45.0, 50.0);
1717 let nester = Nester2D::default_config();
1718
1719 let result = nester.solve(&geometries, &boundary).unwrap();
1720
1721 assert_eq!(result.placements.len(), 2);
1722 assert!(result.unplaced.is_empty());
1723 }
1724
1725 #[test]
1726 fn test_progress_callback_blf() {
1727 use std::sync::atomic::{AtomicUsize, Ordering};
1728 use std::sync::Arc;
1729
1730 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1731 let boundary = Boundary2D::rectangle(50.0, 50.0);
1732 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1733 let nester = Nester2D::new(config);
1734
1735 let callback_count = Arc::new(AtomicUsize::new(0));
1736 let callback_count_clone = callback_count.clone();
1737 let last_items_placed = Arc::new(AtomicUsize::new(0));
1738 let last_items_placed_clone = last_items_placed.clone();
1739
1740 let callback: ProgressCallback = Box::new(move |info| {
1741 callback_count_clone.fetch_add(1, Ordering::Relaxed);
1742 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
1743 });
1744
1745 let result = nester
1746 .solve_with_progress(&geometries, &boundary, callback)
1747 .unwrap();
1748
1749 let count = callback_count.load(Ordering::Relaxed);
1751 assert!(
1752 count >= 5,
1753 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
1754 count
1755 );
1756
1757 let final_placed = last_items_placed.load(Ordering::Relaxed);
1759 assert_eq!(final_placed, 4, "Should report 4 items placed");
1760
1761 assert_eq!(result.placements.len(), 4);
1763 }
1764
1765 #[test]
1766 fn test_progress_callback_nfp() {
1767 use std::sync::atomic::{AtomicUsize, Ordering};
1768 use std::sync::Arc;
1769
1770 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
1771 let boundary = Boundary2D::rectangle(50.0, 50.0);
1772 let config = Config::default().with_strategy(Strategy::NfpGuided);
1773 let nester = Nester2D::new(config);
1774
1775 let callback_count = Arc::new(AtomicUsize::new(0));
1776 let callback_count_clone = callback_count.clone();
1777
1778 let callback: ProgressCallback = Box::new(move |info| {
1779 callback_count_clone.fetch_add(1, Ordering::Relaxed);
1780 assert!(info.items_placed <= info.total_items);
1781 });
1782
1783 let result = nester
1784 .solve_with_progress(&geometries, &boundary, callback)
1785 .unwrap();
1786
1787 let count = callback_count.load(Ordering::Relaxed);
1789 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
1790
1791 assert_eq!(result.placements.len(), 2);
1793 }
1794
1795 #[test]
1796 fn test_time_limit_honored() {
1797 let geometries: Vec<Geometry2D> = (0..100)
1799 .map(|i| Geometry2D::rectangle(format!("R{}", i), 5.0, 5.0))
1800 .collect();
1801 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
1802
1803 let config = Config::default()
1805 .with_strategy(Strategy::BottomLeftFill)
1806 .with_time_limit(1);
1807 let nester = Nester2D::new(config);
1808
1809 let result = nester.solve(&geometries, &boundary).unwrap();
1810
1811 assert!(
1814 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
1816 result.computation_time_ms
1817 );
1818 }
1819
1820 #[test]
1821 fn test_time_limit_zero_unlimited() {
1822 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1824 let boundary = Boundary2D::rectangle(50.0, 50.0);
1825
1826 let config = Config::default()
1827 .with_strategy(Strategy::BottomLeftFill)
1828 .with_time_limit(0); let nester = Nester2D::new(config);
1830
1831 let result = nester.solve(&geometries, &boundary).unwrap();
1832
1833 assert_eq!(result.placements.len(), 4);
1835 }
1836
1837 #[test]
1838 fn test_blf_bounds_clamping() {
1839 let gear_like = Geometry2D::new("gear")
1843 .with_polygon(vec![
1844 (50.0, 5.0), (65.0, 15.0),
1846 (77.0, 18.0),
1847 (80.0, 32.0),
1848 (95.0, 50.0), (80.0, 68.0),
1850 (77.0, 82.0),
1851 (65.0, 85.0),
1852 (50.0, 95.0), (35.0, 85.0),
1854 (23.0, 82.0),
1855 (20.0, 68.0),
1856 (5.0, 50.0), (20.0, 32.0),
1858 (23.0, 18.0),
1859 (35.0, 15.0),
1860 ])
1861 .with_quantity(1);
1862
1863 let boundary = Boundary2D::rectangle(100.0, 100.0);
1865
1866 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1867 let nester = Nester2D::new(config);
1868
1869 let result = nester
1870 .solve(std::slice::from_ref(&gear_like), &boundary)
1871 .unwrap();
1872
1873 assert_eq!(result.placements.len(), 1);
1874 let placement = &result.placements[0];
1875
1876 let origin_x = placement.position[0];
1878 let origin_y = placement.position[1];
1879
1880 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1882
1883 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
1885
1886 let actual_min_x = origin_x + g_min[0];
1888 let actual_max_x = origin_x + g_max[0];
1889 let actual_min_y = origin_y + g_min[1];
1890 let actual_max_y = origin_y + g_max[1];
1891
1892 assert!(
1894 actual_min_x >= 0.0,
1895 "Left edge {} should be >= 0",
1896 actual_min_x
1897 );
1898 assert!(
1899 actual_max_x <= 100.0,
1900 "Right edge {} should be <= 100",
1901 actual_max_x
1902 );
1903 assert!(
1904 actual_min_y >= 0.0,
1905 "Bottom edge {} should be >= 0",
1906 actual_min_y
1907 );
1908 assert!(
1909 actual_max_y <= 100.0,
1910 "Top edge {} should be <= 100",
1911 actual_max_y
1912 );
1913 }
1914
1915 #[test]
1916 fn test_blf_bounds_clamping_many_pieces() {
1917 let gear_like = Geometry2D::new("gear")
1920 .with_polygon(vec![
1921 (50.0, 5.0),
1922 (65.0, 15.0),
1923 (77.0, 18.0),
1924 (80.0, 32.0),
1925 (95.0, 50.0),
1926 (80.0, 68.0),
1927 (77.0, 82.0),
1928 (65.0, 85.0),
1929 (50.0, 95.0),
1930 (35.0, 85.0),
1931 (23.0, 82.0),
1932 (20.0, 68.0),
1933 (5.0, 50.0),
1934 (20.0, 32.0),
1935 (23.0, 18.0),
1936 (35.0, 15.0),
1937 ])
1938 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
1942
1943 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1944 let nester = Nester2D::new(config);
1945
1946 let result = nester
1947 .solve(std::slice::from_ref(&gear_like), &boundary)
1948 .unwrap();
1949
1950 for (i, placement) in result.placements.iter().enumerate() {
1952 let origin_x = placement.position[0];
1953 let origin_y = placement.position[1];
1954 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1955
1956 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
1957
1958 let actual_min_x = origin_x + g_min[0];
1959 let actual_max_x = origin_x + g_max[0];
1960 let actual_min_y = origin_y + g_min[1];
1961 let actual_max_y = origin_y + g_max[1];
1962
1963 assert!(
1964 actual_min_x >= -0.01,
1965 "Piece {}: Left edge {} should be >= 0",
1966 i,
1967 actual_min_x
1968 );
1969 assert!(
1970 actual_max_x <= 500.01,
1971 "Piece {}: Right edge {} should be <= 500",
1972 i,
1973 actual_max_x
1974 );
1975 assert!(
1976 actual_min_y >= -0.01,
1977 "Piece {}: Bottom edge {} should be >= 0",
1978 i,
1979 actual_min_y
1980 );
1981 assert!(
1982 actual_max_y <= 500.01,
1983 "Piece {}: Top edge {} should be <= 500",
1984 i,
1985 actual_max_y
1986 );
1987 }
1988 }
1989
1990 #[test]
1991 fn test_blf_bounds_trace() {
1992 let gear = Geometry2D::new("gear").with_polygon(vec![
1994 (50.0, 5.0),
1995 (65.0, 15.0),
1996 (77.0, 18.0),
1997 (80.0, 32.0),
1998 (95.0, 50.0),
1999 (80.0, 68.0),
2000 (77.0, 82.0),
2001 (65.0, 85.0),
2002 (50.0, 95.0),
2003 (35.0, 85.0),
2004 (23.0, 82.0),
2005 (20.0, 68.0),
2006 (5.0, 50.0),
2007 (20.0, 32.0),
2008 (23.0, 18.0),
2009 (35.0, 15.0),
2010 ]);
2011
2012 let (g_min, g_max) = gear.aabb();
2014 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
2015 assert!(
2016 (g_min[0] - 5.0).abs() < 0.01,
2017 "g_min[0] should be 5, got {}",
2018 g_min[0]
2019 );
2020 assert!(
2021 (g_max[0] - 95.0).abs() < 0.01,
2022 "g_max[0] should be 95, got {}",
2023 g_max[0]
2024 );
2025
2026 let b_max_x = 500.0;
2028 let margin = 0.0;
2029 let max_valid_x = b_max_x - margin - g_max[0];
2030 println!(
2031 "max_valid_x = {} - {} - {} = {}",
2032 b_max_x, margin, g_max[0], max_valid_x
2033 );
2034 assert!(
2035 (max_valid_x - 405.0).abs() < 0.01,
2036 "max_valid_x should be 405, got {}",
2037 max_valid_x
2038 );
2039
2040 let boundary = Boundary2D::rectangle(500.0, 500.0);
2042 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2043 let nester = Nester2D::new(config);
2044
2045 let result = nester
2046 .solve(&[gear.clone().with_quantity(1)], &boundary)
2047 .unwrap();
2048
2049 assert_eq!(result.placements.len(), 1);
2050 let p = &result.placements[0];
2051 let origin_x = p.position[0];
2052 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2053
2054 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2055 let actual_max_x = origin_x + g_max_r[0];
2056
2057 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2058 println!(
2059 "At rotation {}: g_min={:?}, g_max={:?}",
2060 rotation, g_min_r, g_max_r
2061 );
2062 println!(
2063 "Actual max x: {} + {} = {}",
2064 origin_x, g_max_r[0], actual_max_x
2065 );
2066
2067 assert!(
2068 actual_max_x <= 500.01,
2069 "Geometry exceeds boundary: max_x={} > 500",
2070 actual_max_x
2071 );
2072 }
2073
2074 #[test]
2075 fn test_blf_bounds_many_pieces_direct() {
2076 let gear = Geometry2D::new("gear")
2078 .with_polygon(vec![
2079 (50.0, 5.0),
2080 (65.0, 15.0),
2081 (77.0, 18.0),
2082 (80.0, 32.0),
2083 (95.0, 50.0),
2084 (80.0, 68.0),
2085 (77.0, 82.0),
2086 (65.0, 85.0),
2087 (50.0, 95.0),
2088 (35.0, 85.0),
2089 (23.0, 82.0),
2090 (20.0, 68.0),
2091 (5.0, 50.0),
2092 (20.0, 32.0),
2093 (23.0, 18.0),
2094 (35.0, 15.0),
2095 ])
2096 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2099 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2100 let nester = Nester2D::new(config);
2101
2102 let result = nester
2103 .solve(std::slice::from_ref(&gear), &boundary)
2104 .unwrap();
2105
2106 println!("Placed {} pieces", result.placements.len());
2107
2108 for (i, p) in result.placements.iter().enumerate() {
2110 let origin_x = p.position[0];
2111 let origin_y = p.position[1];
2112 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2113
2114 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2115
2116 let actual_min_x = origin_x + g_min_r[0];
2117 let actual_max_x = origin_x + g_max_r[0];
2118 let actual_min_y = origin_y + g_min_r[1];
2119 let actual_max_y = origin_y + g_max_r[1];
2120
2121 println!(
2122 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2123 i,
2124 origin_x,
2125 origin_y,
2126 rotation,
2127 actual_min_x,
2128 actual_max_x,
2129 actual_min_y,
2130 actual_max_y
2131 );
2132
2133 assert!(
2134 actual_max_x <= 500.01,
2135 "Piece {}: Right edge {} > 500",
2136 i,
2137 actual_max_x
2138 );
2139 assert!(
2140 actual_max_y <= 500.01,
2141 "Piece {}: Top edge {} > 500",
2142 i,
2143 actual_max_y
2144 );
2145 }
2146 }
2147
2148 #[test]
2149 fn test_blf_bounds_multi_strip() {
2150 let gear = Geometry2D::new("gear")
2152 .with_polygon(vec![
2153 (50.0, 5.0),
2154 (65.0, 15.0),
2155 (77.0, 18.0),
2156 (80.0, 32.0),
2157 (95.0, 50.0),
2158 (80.0, 68.0),
2159 (77.0, 82.0),
2160 (65.0, 85.0),
2161 (50.0, 95.0),
2162 (35.0, 85.0),
2163 (23.0, 82.0),
2164 (20.0, 68.0),
2165 (5.0, 50.0),
2166 (20.0, 32.0),
2167 (23.0, 18.0),
2168 (35.0, 15.0),
2169 ])
2170 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2173 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2174 let nester = Nester2D::new(config);
2175
2176 let result = nester
2178 .solve_multi_strip(std::slice::from_ref(&gear), &boundary)
2179 .unwrap();
2180
2181 println!(
2182 "Placed {} pieces across {} strips",
2183 result.placements.len(),
2184 result.boundaries_used
2185 );
2186
2187 let strip_width = 500.0;
2189 for (i, p) in result.placements.iter().enumerate() {
2190 let origin_x = p.position[0];
2191 let origin_y = p.position[1];
2192 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2193 let strip_idx = p.boundary_index;
2194
2195 let local_x = origin_x - (strip_idx as f64 * strip_width);
2197
2198 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2199
2200 let local_max_x = local_x + g_max_r[0];
2201 let local_max_y = origin_y + g_max_r[1];
2202
2203 println!(
2204 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2205 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2206 );
2207
2208 assert!(
2209 local_max_x <= 500.01,
2210 "Piece {}: In strip {}, local right edge {:.1} > 500",
2211 i,
2212 strip_idx,
2213 local_max_x
2214 );
2215 assert!(
2216 local_max_y <= 500.01,
2217 "Piece {}: Top edge {:.1} > 500",
2218 i,
2219 local_max_y
2220 );
2221 }
2222 }
2223
2224 #[test]
2225 fn test_blf_bounds_mixed_shapes() {
2226 let shapes = vec![
2228 Geometry2D::new("shape0")
2230 .with_polygon(vec![
2231 (0.0, 0.0),
2232 (180.0, 0.0),
2233 (195.0, 15.0),
2234 (200.0, 50.0),
2235 (200.0, 150.0),
2236 (195.0, 185.0),
2237 (180.0, 200.0),
2238 (20.0, 200.0),
2239 (5.0, 185.0),
2240 (0.0, 150.0),
2241 (0.0, 50.0),
2242 (5.0, 15.0),
2243 ])
2244 .with_quantity(2),
2245 Geometry2D::new("shape1")
2247 .with_polygon(vec![
2248 (60.0, 0.0),
2249 (85.0, 7.0),
2250 (104.0, 25.0),
2251 (118.0, 50.0),
2252 (120.0, 60.0),
2253 (118.0, 70.0),
2254 (104.0, 95.0),
2255 (85.0, 113.0),
2256 (60.0, 120.0),
2257 (35.0, 113.0),
2258 (16.0, 95.0),
2259 (2.0, 70.0),
2260 (0.0, 60.0),
2261 (2.0, 50.0),
2262 (16.0, 25.0),
2263 (35.0, 7.0),
2264 ])
2265 .with_quantity(4),
2266 Geometry2D::new("shape2")
2268 .with_polygon(vec![
2269 (0.0, 0.0),
2270 (80.0, 0.0),
2271 (80.0, 20.0),
2272 (20.0, 20.0),
2273 (20.0, 80.0),
2274 (0.0, 80.0),
2275 ])
2276 .with_quantity(6),
2277 Geometry2D::new("shape3")
2279 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2280 .with_quantity(6),
2281 Geometry2D::new("shape4")
2283 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2284 .with_quantity(4),
2285 Geometry2D::new("shape5")
2287 .with_polygon(vec![
2288 (15.0, 0.0),
2289 (45.0, 0.0),
2290 (60.0, 26.0),
2291 (45.0, 52.0),
2292 (15.0, 52.0),
2293 (0.0, 26.0),
2294 ])
2295 .with_quantity(8),
2296 Geometry2D::new("shape6")
2298 .with_polygon(vec![
2299 (0.0, 0.0),
2300 (90.0, 0.0),
2301 (90.0, 12.0),
2302 (55.0, 12.0),
2303 (55.0, 60.0),
2304 (35.0, 60.0),
2305 (35.0, 12.0),
2306 (0.0, 12.0),
2307 ])
2308 .with_quantity(4),
2309 Geometry2D::new("shape7")
2311 .with_polygon(vec![
2312 (0.0, 10.0),
2313 (10.0, 0.0),
2314 (70.0, 0.0),
2315 (80.0, 10.0),
2316 (80.0, 70.0),
2317 (70.0, 80.0),
2318 (10.0, 80.0),
2319 (0.0, 70.0),
2320 ])
2321 .with_quantity(3),
2322 Geometry2D::new("shape8_gear")
2324 .with_polygon(vec![
2325 (50.0, 5.0),
2326 (65.0, 15.0),
2327 (77.0, 18.0),
2328 (80.0, 32.0),
2329 (95.0, 50.0),
2330 (80.0, 68.0),
2331 (77.0, 82.0),
2332 (65.0, 85.0),
2333 (50.0, 95.0),
2334 (35.0, 85.0),
2335 (23.0, 82.0),
2336 (20.0, 68.0),
2337 (5.0, 50.0),
2338 (20.0, 32.0),
2339 (23.0, 18.0),
2340 (35.0, 15.0),
2341 ])
2342 .with_quantity(13),
2343 ];
2344
2345 let boundary = Boundary2D::rectangle(500.0, 500.0);
2347 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2348 let nester = Nester2D::new(config);
2349
2350 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2351
2352 println!(
2353 "Placed {} pieces across {} strips",
2354 result.placements.len(),
2355 result.boundaries_used
2356 );
2357
2358 let strip_width = 500.0;
2360 let gear_aabb = shapes[8].aabb();
2361 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2362
2363 let mut violations = Vec::new();
2364 for p in &result.placements {
2365 if p.geometry_id.as_str().starts_with("shape8") {
2366 let origin_x = p.position[0];
2367 let _origin_y = p.position[1];
2368 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2369 let strip_idx = p.boundary_index;
2370 let local_x = origin_x - (strip_idx as f64 * strip_width);
2371
2372 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2373 let local_max_x = local_x + g_max_r[0];
2374
2375 println!(
2376 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2377 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2378 );
2379
2380 if local_max_x > 500.01 {
2381 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2382 }
2383 }
2384 }
2385
2386 assert!(
2387 violations.is_empty(),
2388 "Found {} Gear pieces exceeding boundary: {:?}",
2389 violations.len(),
2390 violations
2391 );
2392 }
2393
2394 #[test]
2395 fn test_blf_bounds_expanded_like_benchmark() {
2396 type ShapeDef = (Vec<(f64, f64)>, usize, Vec<f64>);
2400 let shape_defs: Vec<ShapeDef> = vec![
2401 (
2402 vec![
2403 (0.0, 0.0),
2404 (180.0, 0.0),
2405 (195.0, 15.0),
2406 (200.0, 50.0),
2407 (200.0, 150.0),
2408 (195.0, 185.0),
2409 (180.0, 200.0),
2410 (20.0, 200.0),
2411 (5.0, 185.0),
2412 (0.0, 150.0),
2413 (0.0, 50.0),
2414 (5.0, 15.0),
2415 ],
2416 2,
2417 vec![0.0, 90.0, 180.0, 270.0],
2418 ),
2419 (
2420 vec![
2421 (60.0, 0.0),
2422 (85.0, 7.0),
2423 (104.0, 25.0),
2424 (118.0, 50.0),
2425 (120.0, 60.0),
2426 (118.0, 70.0),
2427 (104.0, 95.0),
2428 (85.0, 113.0),
2429 (60.0, 120.0),
2430 (35.0, 113.0),
2431 (16.0, 95.0),
2432 (2.0, 70.0),
2433 (0.0, 60.0),
2434 (2.0, 50.0),
2435 (16.0, 25.0),
2436 (35.0, 7.0),
2437 ],
2438 4,
2439 vec![0.0, 45.0, 90.0, 135.0],
2440 ),
2441 (
2442 vec![
2443 (0.0, 0.0),
2444 (80.0, 0.0),
2445 (80.0, 20.0),
2446 (20.0, 20.0),
2447 (20.0, 80.0),
2448 (0.0, 80.0),
2449 ],
2450 6,
2451 vec![0.0, 90.0, 180.0, 270.0],
2452 ),
2453 (
2454 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
2455 6,
2456 vec![0.0, 90.0, 180.0, 270.0],
2457 ),
2458 (
2459 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
2460 4,
2461 vec![0.0, 90.0],
2462 ),
2463 (
2464 vec![
2465 (15.0, 0.0),
2466 (45.0, 0.0),
2467 (60.0, 26.0),
2468 (45.0, 52.0),
2469 (15.0, 52.0),
2470 (0.0, 26.0),
2471 ],
2472 8,
2473 vec![0.0, 60.0, 120.0],
2474 ),
2475 (
2476 vec![
2477 (0.0, 0.0),
2478 (90.0, 0.0),
2479 (90.0, 12.0),
2480 (55.0, 12.0),
2481 (55.0, 60.0),
2482 (35.0, 60.0),
2483 (35.0, 12.0),
2484 (0.0, 12.0),
2485 ],
2486 4,
2487 vec![0.0, 90.0, 180.0, 270.0],
2488 ),
2489 (
2490 vec![
2491 (0.0, 10.0),
2492 (10.0, 0.0),
2493 (70.0, 0.0),
2494 (80.0, 10.0),
2495 (80.0, 70.0),
2496 (70.0, 80.0),
2497 (10.0, 80.0),
2498 (0.0, 70.0),
2499 ],
2500 3,
2501 vec![0.0, 90.0],
2502 ),
2503 (
2505 vec![
2506 (50.0, 5.0),
2507 (65.0, 15.0),
2508 (77.0, 18.0),
2509 (80.0, 32.0),
2510 (95.0, 50.0),
2511 (80.0, 68.0),
2512 (77.0, 82.0),
2513 (65.0, 85.0),
2514 (50.0, 95.0),
2515 (35.0, 85.0),
2516 (23.0, 82.0),
2517 (20.0, 68.0),
2518 (5.0, 50.0),
2519 (20.0, 32.0),
2520 (23.0, 18.0),
2521 (35.0, 15.0),
2522 ],
2523 13,
2524 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
2525 ),
2526 ];
2527
2528 let mut geometries = Vec::new();
2530 let mut piece_id = 0;
2531 for (vertices, demand, rotations) in shape_defs.iter() {
2532 for _ in 0..*demand {
2533 let geom = Geometry2D::new(format!("piece_{}", piece_id))
2534 .with_polygon(vertices.clone())
2535 .with_rotations_deg(rotations.clone());
2536 geometries.push(geom);
2537 piece_id += 1;
2538 }
2539 }
2540
2541 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
2543 let (gear_min, gear_max) = gear_geom.aabb();
2544 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
2545
2546 let boundary = Boundary2D::rectangle(500.0, 500.0);
2547 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2548 let nester = Nester2D::new(config);
2549
2550 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
2551
2552 println!(
2553 "Placed {} pieces across {} strips",
2554 result.placements.len(),
2555 result.boundaries_used
2556 );
2557
2558 let strip_width = 500.0;
2560 let mut violations = Vec::new();
2561
2562 for p in &result.placements {
2563 let id_num: usize = p
2564 .geometry_id
2565 .as_str()
2566 .strip_prefix("piece_")
2567 .and_then(|s| s.parse().ok())
2568 .unwrap_or(0);
2569
2570 if (37..=49).contains(&id_num) {
2572 let origin_x = p.position[0];
2573 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2574 let strip_idx = p.boundary_index;
2575 let local_x = origin_x - (strip_idx as f64 * strip_width);
2576
2577 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
2578 let local_max_x = local_x + g_max_r[0];
2579
2580 println!(
2581 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2582 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2583 );
2584
2585 if local_max_x > 500.01 {
2586 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2587 }
2588 }
2589 }
2590
2591 assert!(
2592 violations.is_empty(),
2593 "Found {} Gear pieces exceeding boundary: {:?}",
2594 violations.len(),
2595 violations
2596 );
2597 }
2598
2599 fn aabbs_overlap(
2601 a_min: [f64; 2],
2602 a_max: [f64; 2],
2603 b_min: [f64; 2],
2604 b_max: [f64; 2],
2605 tolerance: f64,
2606 ) -> bool {
2607 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
2609 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
2610 x_overlap && y_overlap
2611 }
2612
2613 #[test]
2615 fn test_all_strategies_boundary_and_overlap() {
2616 use std::collections::HashMap;
2617
2618 let shapes = vec![
2620 Geometry2D::new("shape0")
2621 .with_polygon(vec![
2622 (0.0, 0.0),
2623 (180.0, 0.0),
2624 (195.0, 15.0),
2625 (200.0, 50.0),
2626 (200.0, 150.0),
2627 (195.0, 185.0),
2628 (180.0, 200.0),
2629 (20.0, 200.0),
2630 (5.0, 185.0),
2631 (0.0, 150.0),
2632 (0.0, 50.0),
2633 (5.0, 15.0),
2634 ])
2635 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2636 .with_quantity(2),
2637 Geometry2D::new("shape1_flange")
2638 .with_polygon(vec![
2639 (60.0, 0.0),
2640 (85.0, 7.0),
2641 (104.0, 25.0),
2642 (118.0, 50.0),
2643 (120.0, 60.0),
2644 (118.0, 70.0),
2645 (104.0, 95.0),
2646 (85.0, 113.0),
2647 (60.0, 120.0),
2648 (35.0, 113.0),
2649 (16.0, 95.0),
2650 (2.0, 70.0),
2651 (0.0, 60.0),
2652 (2.0, 50.0),
2653 (16.0, 25.0),
2654 (35.0, 7.0),
2655 ])
2656 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
2657 .with_quantity(4),
2658 Geometry2D::new("shape2_lbracket")
2659 .with_polygon(vec![
2660 (0.0, 0.0),
2661 (80.0, 0.0),
2662 (80.0, 20.0),
2663 (20.0, 20.0),
2664 (20.0, 80.0),
2665 (0.0, 80.0),
2666 ])
2667 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2668 .with_quantity(6),
2669 Geometry2D::new("shape3_triangle")
2670 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2671 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2672 .with_quantity(6),
2673 Geometry2D::new("shape4_rect")
2674 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2675 .with_rotations_deg(vec![0.0, 90.0])
2676 .with_quantity(4),
2677 Geometry2D::new("shape5_hexagon")
2678 .with_polygon(vec![
2679 (15.0, 0.0),
2680 (45.0, 0.0),
2681 (60.0, 26.0),
2682 (45.0, 52.0),
2683 (15.0, 52.0),
2684 (0.0, 26.0),
2685 ])
2686 .with_rotations_deg(vec![0.0, 60.0, 120.0])
2687 .with_quantity(8),
2688 Geometry2D::new("shape6_tstiff")
2689 .with_polygon(vec![
2690 (0.0, 0.0),
2691 (90.0, 0.0),
2692 (90.0, 12.0),
2693 (55.0, 12.0),
2694 (55.0, 60.0),
2695 (35.0, 60.0),
2696 (35.0, 12.0),
2697 (0.0, 12.0),
2698 ])
2699 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2700 .with_quantity(4),
2701 Geometry2D::new("shape7_mount")
2702 .with_polygon(vec![
2703 (0.0, 10.0),
2704 (10.0, 0.0),
2705 (70.0, 0.0),
2706 (80.0, 10.0),
2707 (80.0, 70.0),
2708 (70.0, 80.0),
2709 (10.0, 80.0),
2710 (0.0, 70.0),
2711 ])
2712 .with_rotations_deg(vec![0.0, 90.0])
2713 .with_quantity(3),
2714 Geometry2D::new("shape8_gear")
2715 .with_polygon(vec![
2716 (50.0, 5.0),
2717 (65.0, 15.0),
2718 (77.0, 18.0),
2719 (80.0, 32.0),
2720 (95.0, 50.0),
2721 (80.0, 68.0),
2722 (77.0, 82.0),
2723 (65.0, 85.0),
2724 (50.0, 95.0),
2725 (35.0, 85.0),
2726 (23.0, 82.0),
2727 (20.0, 68.0),
2728 (5.0, 50.0),
2729 (20.0, 32.0),
2730 (23.0, 18.0),
2731 (35.0, 15.0),
2732 ])
2733 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
2734 .with_quantity(13),
2735 ];
2736
2737 let geom_map: HashMap<String, &Geometry2D> =
2739 shapes.iter().map(|g| (g.id().clone(), g)).collect();
2740
2741 let boundary = Boundary2D::rectangle(500.0, 500.0);
2742 let strip_width = 500.0;
2743
2744 let strategies = vec![
2746 Strategy::BottomLeftFill,
2747 Strategy::NfpGuided,
2748 Strategy::GeneticAlgorithm,
2749 Strategy::Brkga,
2750 Strategy::SimulatedAnnealing,
2751 Strategy::Gdrr,
2752 Strategy::Alns,
2753 ];
2754
2755 for strategy in strategies {
2756 println!("\n========== Testing {:?} ==========", strategy);
2757
2758 let config = Config::default()
2759 .with_strategy(strategy)
2760 .with_time_limit(30000); let nester = Nester2D::new(config);
2762
2763 let result = match nester.solve_multi_strip(&shapes, &boundary) {
2764 Ok(r) => r,
2765 Err(e) => {
2766 println!(" Strategy {:?} failed: {}", strategy, e);
2767 continue;
2768 }
2769 };
2770
2771 println!(
2772 " Placed {} pieces across {} strips",
2773 result.placements.len(),
2774 result.boundaries_used
2775 );
2776
2777 let mut boundary_violations = Vec::new();
2779 for p in &result.placements {
2780 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
2782 let full_id = if base_id.starts_with("shape") {
2783 shapes
2785 .iter()
2786 .find(|g| p.geometry_id.starts_with(g.id()))
2787 .map(|g| g.id().as_str())
2788 } else {
2789 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
2790 };
2791
2792 let geom = match full_id.and_then(|id| geom_map.get(id)) {
2793 Some(g) => *g,
2794 None => {
2795 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
2797 Some(g) => g,
2798 None => {
2799 println!(
2800 " WARNING: Could not find geometry for {}",
2801 p.geometry_id
2802 );
2803 continue;
2804 }
2805 }
2806 }
2807 };
2808
2809 let origin_x = p.position[0];
2810 let origin_y = p.position[1];
2811 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2812 let strip_idx = p.boundary_index;
2813
2814 let local_x = origin_x - (strip_idx as f64 * strip_width);
2816
2817 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
2818
2819 let local_min_x = local_x + g_min[0];
2821 let local_max_x = local_x + g_max[0];
2822 let local_min_y = origin_y + g_min[1];
2823 let local_max_y = origin_y + g_max[1];
2824
2825 let tolerance = 0.1;
2827 if local_min_x < -tolerance
2828 || local_max_x > 500.0 + tolerance
2829 || local_min_y < -tolerance
2830 || local_max_y > 500.0 + tolerance
2831 {
2832 boundary_violations.push(format!(
2833 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
2834 p.geometry_id,
2835 strip_idx,
2836 local_min_x,
2837 local_min_y,
2838 local_max_x,
2839 local_max_y
2840 ));
2841 }
2842 }
2843
2844 if !boundary_violations.is_empty() {
2845 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
2846 for v in &boundary_violations {
2847 println!(" - {}", v);
2848 }
2849 }
2850
2851 let mut overlaps = Vec::new();
2853 let placements: Vec<_> = result.placements.iter().collect();
2854
2855 for i in 0..placements.len() {
2856 for j in (i + 1)..placements.len() {
2857 let p1 = placements[i];
2858 let p2 = placements[j];
2859
2860 if p1.boundary_index != p2.boundary_index {
2862 continue;
2863 }
2864
2865 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
2867 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
2868
2869 let (g1, g2) = match (g1, g2) {
2870 (Some(a), Some(b)) => (a, b),
2871 _ => continue,
2872 };
2873
2874 let strip_idx = p1.boundary_index;
2875 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
2876 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
2877
2878 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
2879 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
2880
2881 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
2882 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
2883
2884 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
2885 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
2886 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
2887 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
2888
2889 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
2890 overlaps.push(format!(
2891 "{} and {} in strip {}",
2892 p1.geometry_id, p2.geometry_id, strip_idx
2893 ));
2894 }
2895 }
2896 }
2897
2898 if !overlaps.is_empty() {
2899 println!(" OVERLAPS ({}):", overlaps.len());
2900 for o in overlaps.iter().take(10) {
2901 println!(" - {}", o);
2902 }
2903 if overlaps.len() > 10 {
2904 println!(" ... and {} more", overlaps.len() - 10);
2905 }
2906 }
2907
2908 assert!(
2910 boundary_violations.is_empty(),
2911 "{:?}: Found {} boundary violations",
2912 strategy,
2913 boundary_violations.len()
2914 );
2915
2916 println!(" ✓ All placements within boundary");
2917 println!(" ✓ No AABB overlaps detected");
2918 }
2919 }
2920
2921 #[test]
2929 fn test_multi_strip_distributes_all_instances() {
2930 let geometries = vec![Geometry2D::rectangle("part", 100.0, 100.0).with_quantity(20)];
2931 let boundary = Boundary2D::rectangle(300.0, 300.0);
2932 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2933 let nester = Nester2D::new(config);
2934
2935 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
2936
2937 assert_eq!(
2939 result.placements.len(),
2940 20,
2941 "all 20 instances must be placed"
2942 );
2943 assert_eq!(
2944 result.boundaries_used, 3,
2945 "20 of 100x100 in 300x300 => 3 sheets"
2946 );
2947 assert!(
2948 result.unplaced.is_empty(),
2949 "nothing should be unplaced, got {:?}",
2950 result.unplaced
2951 );
2952 assert_eq!(result.total_requested, 20);
2954
2955 let mut seen = std::collections::HashSet::new();
2958 for p in &result.placements {
2959 assert!(
2960 seen.insert((p.geometry_id.clone(), p.instance)),
2961 "duplicate (id, instance) = ({}, {}) across sheets",
2962 p.geometry_id,
2963 p.instance
2964 );
2965 }
2966 let mut sheets: Vec<usize> = result.placements.iter().map(|p| p.boundary_index).collect();
2968 sheets.sort_unstable();
2969 sheets.dedup();
2970 assert_eq!(sheets, vec![0, 1, 2]);
2971 }
2972
2973 #[test]
2976 fn test_multi_strip_oversized_reported_unplaced() {
2977 let geometries = vec![
2978 Geometry2D::rectangle("ok", 50.0, 50.0).with_quantity(2),
2979 Geometry2D::rectangle("toobig", 400.0, 400.0).with_quantity(3),
2980 ];
2981 let boundary = Boundary2D::rectangle(300.0, 300.0);
2982 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2983 let nester = Nester2D::new(config);
2984
2985 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
2986
2987 assert_eq!(result.total_requested, 5, "2 + 3 instances requested");
2988 assert_eq!(result.placements.len(), 2);
2990 assert!(
2991 result.unplaced.contains(&"toobig".to_string()),
2992 "oversized geometry must surface in unplaced, got {:?}",
2993 result.unplaced
2994 );
2995 }
2996}