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; while !remaining_geometries.is_empty() && strip_index < max_strips {
1099 if self.cancelled.load(Ordering::Relaxed) {
1100 break;
1101 }
1102
1103 let strip_result = match self.config.strategy {
1105 Strategy::BottomLeftFill => self.bottom_left_fill(&remaining_geometries, boundary),
1106 Strategy::NfpGuided => self.nfp_guided_blf(&remaining_geometries, boundary),
1107 Strategy::GeneticAlgorithm => {
1108 self.genetic_algorithm(&remaining_geometries, boundary)
1109 }
1110 Strategy::Brkga => self.brkga(&remaining_geometries, boundary),
1111 Strategy::SimulatedAnnealing => {
1112 self.simulated_annealing(&remaining_geometries, boundary)
1113 }
1114 Strategy::Gdrr => self.gdrr(&remaining_geometries, boundary),
1115 Strategy::Alns => self.alns(&remaining_geometries, boundary),
1116 #[cfg(feature = "milp")]
1117 Strategy::MilpExact => self.milp_exact(&remaining_geometries, boundary),
1118 #[cfg(feature = "milp")]
1119 Strategy::HybridExact => self.hybrid_exact(&remaining_geometries, boundary),
1120 _ => self.nfp_guided_blf(&remaining_geometries, boundary),
1121 }?;
1122
1123 let strip_result =
1125 validate_and_filter_placements(strip_result, &remaining_geometries, boundary);
1126
1127 if strip_result.placements.is_empty() {
1128 final_result.unplaced.extend(strip_result.unplaced);
1130 break;
1131 }
1132
1133 let placed_ids: std::collections::HashSet<_> = strip_result
1135 .placements
1136 .iter()
1137 .map(|p| p.geometry_id.clone())
1138 .collect();
1139
1140 for mut placement in strip_result.placements {
1142 if !placement.position.is_empty() {
1144 placement.position[0] += strip_index as f64 * strip_width;
1145 }
1146 placement.boundary_index = strip_index;
1147 final_result.placements.push(placement);
1148 }
1149
1150 remaining_geometries.retain(|g| !placed_ids.contains(g.id()));
1152
1153 strip_index += 1;
1157 }
1158
1159 final_result.boundaries_used = strip_index;
1160 final_result.deduplicate_unplaced();
1161
1162 let (b_min, b_max) = boundary.aabb();
1164 let strip_height = b_max[1] - b_min[1]; let mut strip_stats_map: std::collections::HashMap<usize, (f64, f64, usize)> =
1168 std::collections::HashMap::new(); for placement in &final_result.placements {
1171 let strip_idx = placement.boundary_index;
1172 if let Some(geom) = geometries.iter().find(|g| g.id() == &placement.geometry_id) {
1174 use u_nesting_core::geometry::Geometry;
1175 let piece_area = geom.measure();
1176 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1177 let (_g_min, g_max) = geom.aabb_at_rotation(rotation);
1178 let local_x = placement.position[0] - (strip_idx as f64 * strip_width);
1181 let right_edge = local_x + g_max[0];
1182
1183 let entry = strip_stats_map.entry(strip_idx).or_insert((0.0, 0.0, 0));
1184 entry.0 = entry.0.max(right_edge); entry.1 += piece_area; entry.2 += 1; }
1188 }
1189
1190 use u_nesting_core::result::StripStats;
1192 let mut strip_stats: Vec<StripStats> = strip_stats_map
1193 .into_iter()
1194 .map(|(idx, (used_length, piece_area, count))| StripStats {
1195 strip_index: idx,
1196 used_length,
1197 piece_area,
1198 piece_count: count,
1199 strip_width, strip_height, })
1202 .collect();
1203 strip_stats.sort_by_key(|s| s.strip_index);
1204
1205 let total_piece_area: f64 = strip_stats.iter().map(|s| s.piece_area).sum();
1208 let total_material_used: f64 = strip_stats
1209 .iter()
1210 .map(|s| s.strip_height * s.used_length)
1211 .sum();
1212
1213 final_result.strip_stats = strip_stats;
1214 final_result.total_piece_area = total_piece_area;
1215 final_result.total_material_used = total_material_used;
1216
1217 if total_material_used > 0.0 {
1218 final_result.utilization = total_piece_area / total_material_used;
1219 }
1220
1221 Ok(final_result)
1222 }
1223}
1224
1225impl Solver for Nester2D {
1226 type Geometry = Geometry2D;
1227 type Boundary = Boundary2D;
1228 type Scalar = f64;
1229
1230 fn solve(
1231 &self,
1232 geometries: &[Self::Geometry],
1233 boundary: &Self::Boundary,
1234 ) -> Result<SolveResult<f64>> {
1235 boundary.validate()?;
1236
1237 self.cancelled.store(false, Ordering::Relaxed);
1239
1240 let initial_result = match self.config.strategy {
1241 Strategy::BottomLeftFill => self.bottom_left_fill(geometries, boundary),
1242 Strategy::NfpGuided => self.nfp_guided_blf(geometries, boundary),
1243 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
1244 Strategy::Brkga => self.brkga(geometries, boundary),
1245 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
1246 Strategy::Gdrr => self.gdrr(geometries, boundary),
1247 Strategy::Alns => self.alns(geometries, boundary),
1248 #[cfg(feature = "milp")]
1249 Strategy::MilpExact => self.milp_exact(geometries, boundary),
1250 #[cfg(feature = "milp")]
1251 Strategy::HybridExact => self.hybrid_exact(geometries, boundary),
1252 _ => {
1253 log::warn!(
1255 "Strategy {:?} not yet implemented, using NfpGuided",
1256 self.config.strategy
1257 );
1258 self.nfp_guided_blf(geometries, boundary)
1259 }
1260 }?;
1261
1262 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1264
1265 result.deduplicate_unplaced();
1267 Ok(result)
1268 }
1269
1270 fn solve_with_progress(
1271 &self,
1272 geometries: &[Self::Geometry],
1273 boundary: &Self::Boundary,
1274 callback: ProgressCallback,
1275 ) -> Result<SolveResult<f64>> {
1276 boundary.validate()?;
1277
1278 self.cancelled.store(false, Ordering::Relaxed);
1280
1281 let initial_result = match self.config.strategy {
1282 Strategy::BottomLeftFill => {
1283 self.bottom_left_fill_with_progress(geometries, boundary, &callback)?
1284 }
1285 Strategy::NfpGuided => {
1286 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1287 }
1288 Strategy::GeneticAlgorithm => {
1289 let mut ga_config = GaConfig::default()
1290 .with_population_size(self.config.population_size)
1291 .with_max_generations(self.config.max_generations)
1292 .with_crossover_rate(self.config.crossover_rate)
1293 .with_mutation_rate(self.config.mutation_rate);
1294
1295 if self.config.time_limit_ms > 0 {
1297 ga_config = ga_config.with_time_limit(std::time::Duration::from_millis(
1298 self.config.time_limit_ms,
1299 ));
1300 }
1301
1302 run_ga_nesting_with_progress(
1303 geometries,
1304 boundary,
1305 &self.config,
1306 ga_config,
1307 self.cancelled.clone(),
1308 callback,
1309 )
1310 }
1311 _ => {
1313 log::warn!(
1314 "Strategy {:?} not yet implemented, using NfpGuided",
1315 self.config.strategy
1316 );
1317 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1318 }
1319 };
1320
1321 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1323
1324 result.deduplicate_unplaced();
1326 Ok(result)
1327 }
1328
1329 fn cancel(&self) {
1330 self.cancelled.store(true, Ordering::Relaxed);
1331 }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use super::*;
1337 use crate::placement_utils::polygon_centroid;
1338
1339 #[test]
1340 fn test_simple_nesting() {
1341 let geometries = vec![
1342 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1343 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1344 ];
1345
1346 let boundary = Boundary2D::rectangle(100.0, 50.0);
1347 let nester = Nester2D::default_config();
1348
1349 let result = nester.solve(&geometries, &boundary).unwrap();
1350
1351 assert!(result.utilization > 0.0);
1352 assert!(result.placements.len() <= 5); }
1354
1355 #[test]
1356 fn test_placement_within_bounds() {
1357 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1358
1359 let boundary = Boundary2D::rectangle(50.0, 50.0);
1360 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1361 let nester = Nester2D::new(config);
1362
1363 let result = nester.solve(&geometries, &boundary).unwrap();
1364
1365 assert_eq!(result.placements.len(), 4);
1367 assert!(result.unplaced.is_empty());
1368
1369 for p in &result.placements {
1371 assert!(p.position[0] >= 5.0);
1372 assert!(p.position[1] >= 5.0);
1373 }
1374 }
1375
1376 #[test]
1377 fn test_nfp_guided_basic() {
1378 let geometries = vec![
1379 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1380 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1381 ];
1382
1383 let boundary = Boundary2D::rectangle(100.0, 50.0);
1384 let config = Config::default().with_strategy(Strategy::NfpGuided);
1385 let nester = Nester2D::new(config);
1386
1387 let result = nester.solve(&geometries, &boundary).unwrap();
1388
1389 assert!(result.utilization > 0.0);
1390 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1392 }
1393
1394 #[test]
1395 fn test_nfp_guided_with_spacing() {
1396 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1397
1398 let boundary = Boundary2D::rectangle(50.0, 50.0);
1399 let config = Config::default()
1400 .with_strategy(Strategy::NfpGuided)
1401 .with_margin(2.0)
1402 .with_spacing(3.0);
1403 let nester = Nester2D::new(config);
1404
1405 let result = nester.solve(&geometries, &boundary).unwrap();
1406
1407 assert_eq!(result.placements.len(), 4);
1409 assert!(result.unplaced.is_empty());
1410
1411 assert!(result.utilization > 0.0);
1413 }
1414
1415 #[test]
1416 fn test_nfp_guided_no_overlap() {
1417 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1418
1419 let boundary = Boundary2D::rectangle(100.0, 100.0);
1420 let config = Config::default().with_strategy(Strategy::NfpGuided);
1421 let nester = Nester2D::new(config);
1422
1423 let result = nester.solve(&geometries, &boundary).unwrap();
1424
1425 assert_eq!(result.placements.len(), 3);
1426
1427 for i in 0..result.placements.len() {
1429 for j in (i + 1)..result.placements.len() {
1430 let p1 = &result.placements[i];
1431 let p2 = &result.placements[j];
1432
1433 let r1_min_x = p1.position[0];
1435 let r1_max_x = p1.position[0] + 20.0;
1436 let r1_min_y = p1.position[1];
1437 let r1_max_y = p1.position[1] + 20.0;
1438
1439 let r2_min_x = p2.position[0];
1440 let r2_max_x = p2.position[0] + 20.0;
1441 let r2_min_y = p2.position[1];
1442 let r2_max_y = p2.position[1] + 20.0;
1443
1444 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1446 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1447
1448 assert!(
1449 !(overlaps_x && overlaps_y),
1450 "Placements {} and {} overlap",
1451 i,
1452 j
1453 );
1454 }
1455 }
1456 }
1457
1458 #[test]
1459 fn test_nfp_guided_utilization() {
1460 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
1462
1463 let boundary = Boundary2D::rectangle(100.0, 50.0);
1464 let config = Config::default().with_strategy(Strategy::NfpGuided);
1465 let nester = Nester2D::new(config);
1466
1467 let result = nester.solve(&geometries, &boundary).unwrap();
1468
1469 assert_eq!(result.placements.len(), 4);
1471
1472 assert!(result.utilization > 0.45);
1474 }
1475
1476 #[test]
1477 fn test_polygon_centroid() {
1478 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1480 let (cx, cy) = polygon_centroid(&square);
1481 assert!((cx - 5.0).abs() < 0.01);
1482 assert!((cy - 5.0).abs() < 0.01);
1483
1484 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
1485 let (cx, cy) = polygon_centroid(&triangle);
1486 assert!((cx - 3.0).abs() < 0.01);
1487 assert!((cy - 2.0).abs() < 0.01);
1488 }
1489
1490 #[test]
1491 fn test_ga_strategy_basic() {
1492 let geometries = vec![
1493 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1494 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1495 ];
1496
1497 let boundary = Boundary2D::rectangle(100.0, 50.0);
1498 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1499 let nester = Nester2D::new(config);
1500
1501 let result = nester.solve(&geometries, &boundary).unwrap();
1502
1503 assert!(result.utilization > 0.0);
1504 assert!(!result.placements.is_empty());
1505 assert!(result.generations.is_some());
1507 assert!(result.best_fitness.is_some());
1508 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
1509 }
1510
1511 #[test]
1512 fn test_ga_strategy_all_placed() {
1513 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1515
1516 let boundary = Boundary2D::rectangle(100.0, 100.0);
1517 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1518 let nester = Nester2D::new(config);
1519
1520 let result = nester.solve(&geometries, &boundary).unwrap();
1521
1522 assert_eq!(result.placements.len(), 4);
1524 assert!(result.unplaced.is_empty());
1525 }
1526
1527 #[test]
1528 fn test_brkga_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::Brkga);
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("BRKGA".to_string()));
1546 }
1547
1548 #[test]
1549 fn test_brkga_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()
1556 .with_strategy(Strategy::Brkga)
1557 .with_time_limit(30000); let nester = Nester2D::new(config);
1559
1560 let result = nester.solve(&geometries, &boundary).unwrap();
1561
1562 assert!(
1565 result.placements.len() >= 3,
1566 "Expected at least 3 placements, got {}",
1567 result.placements.len()
1568 );
1569 }
1570
1571 #[test]
1572 fn test_gdrr_strategy_basic() {
1573 let geometries = vec![
1574 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1575 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1576 ];
1577
1578 let boundary = Boundary2D::rectangle(100.0, 50.0);
1579 let config = Config::default().with_strategy(Strategy::Gdrr);
1580 let nester = Nester2D::new(config);
1581
1582 let result = nester.solve(&geometries, &boundary).unwrap();
1583
1584 assert!(result.utilization > 0.0);
1585 assert!(!result.placements.is_empty());
1586 assert!(result.iterations.is_some());
1588 assert!(result.best_fitness.is_some());
1589 assert!(result.strategy == Some("GDRR".to_string()));
1590 }
1591
1592 #[test]
1593 fn test_gdrr_strategy_all_placed() {
1594 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1596
1597 let boundary = Boundary2D::rectangle(100.0, 100.0);
1598 let config = Config::default().with_strategy(Strategy::Gdrr);
1599 let nester = Nester2D::new(config);
1600
1601 let result = nester.solve(&geometries, &boundary).unwrap();
1602
1603 assert_eq!(result.placements.len(), 4);
1605 assert!(result.unplaced.is_empty());
1606 }
1607
1608 #[test]
1609 fn test_alns_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::Alns);
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("ALNS".to_string()));
1627 }
1628
1629 #[test]
1630 fn test_alns_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::Alns);
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_blf_rotation_optimization() {
1647 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
1650 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
1652
1653 let boundary = Boundary2D::rectangle(35.0, 95.0);
1656 let nester = Nester2D::default_config();
1657
1658 let result = nester.solve(&geometries, &boundary).unwrap();
1659
1660 assert_eq!(
1662 result.placements.len(),
1663 3,
1664 "All pieces should be placed with rotation optimization"
1665 );
1666 assert!(result.unplaced.is_empty());
1667 }
1668
1669 #[test]
1670 fn test_blf_selects_best_rotation() {
1671 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
1673 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
1675
1676 let boundary = Boundary2D::rectangle(45.0, 50.0);
1680 let nester = Nester2D::default_config();
1681
1682 let result = nester.solve(&geometries, &boundary).unwrap();
1683
1684 assert_eq!(result.placements.len(), 2);
1685 assert!(result.unplaced.is_empty());
1686 }
1687
1688 #[test]
1689 fn test_progress_callback_blf() {
1690 use std::sync::atomic::{AtomicUsize, Ordering};
1691 use std::sync::Arc;
1692
1693 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1694 let boundary = Boundary2D::rectangle(50.0, 50.0);
1695 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1696 let nester = Nester2D::new(config);
1697
1698 let callback_count = Arc::new(AtomicUsize::new(0));
1699 let callback_count_clone = callback_count.clone();
1700 let last_items_placed = Arc::new(AtomicUsize::new(0));
1701 let last_items_placed_clone = last_items_placed.clone();
1702
1703 let callback: ProgressCallback = Box::new(move |info| {
1704 callback_count_clone.fetch_add(1, Ordering::Relaxed);
1705 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
1706 });
1707
1708 let result = nester
1709 .solve_with_progress(&geometries, &boundary, callback)
1710 .unwrap();
1711
1712 let count = callback_count.load(Ordering::Relaxed);
1714 assert!(
1715 count >= 5,
1716 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
1717 count
1718 );
1719
1720 let final_placed = last_items_placed.load(Ordering::Relaxed);
1722 assert_eq!(final_placed, 4, "Should report 4 items placed");
1723
1724 assert_eq!(result.placements.len(), 4);
1726 }
1727
1728 #[test]
1729 fn test_progress_callback_nfp() {
1730 use std::sync::atomic::{AtomicUsize, Ordering};
1731 use std::sync::Arc;
1732
1733 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
1734 let boundary = Boundary2D::rectangle(50.0, 50.0);
1735 let config = Config::default().with_strategy(Strategy::NfpGuided);
1736 let nester = Nester2D::new(config);
1737
1738 let callback_count = Arc::new(AtomicUsize::new(0));
1739 let callback_count_clone = callback_count.clone();
1740
1741 let callback: ProgressCallback = Box::new(move |info| {
1742 callback_count_clone.fetch_add(1, Ordering::Relaxed);
1743 assert!(info.items_placed <= info.total_items);
1744 });
1745
1746 let result = nester
1747 .solve_with_progress(&geometries, &boundary, callback)
1748 .unwrap();
1749
1750 let count = callback_count.load(Ordering::Relaxed);
1752 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
1753
1754 assert_eq!(result.placements.len(), 2);
1756 }
1757
1758 #[test]
1759 fn test_time_limit_honored() {
1760 let geometries: Vec<Geometry2D> = (0..100)
1762 .map(|i| Geometry2D::rectangle(format!("R{}", i), 5.0, 5.0))
1763 .collect();
1764 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
1765
1766 let config = Config::default()
1768 .with_strategy(Strategy::BottomLeftFill)
1769 .with_time_limit(1);
1770 let nester = Nester2D::new(config);
1771
1772 let result = nester.solve(&geometries, &boundary).unwrap();
1773
1774 assert!(
1777 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
1779 result.computation_time_ms
1780 );
1781 }
1782
1783 #[test]
1784 fn test_time_limit_zero_unlimited() {
1785 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1787 let boundary = Boundary2D::rectangle(50.0, 50.0);
1788
1789 let config = Config::default()
1790 .with_strategy(Strategy::BottomLeftFill)
1791 .with_time_limit(0); let nester = Nester2D::new(config);
1793
1794 let result = nester.solve(&geometries, &boundary).unwrap();
1795
1796 assert_eq!(result.placements.len(), 4);
1798 }
1799
1800 #[test]
1801 fn test_blf_bounds_clamping() {
1802 let gear_like = Geometry2D::new("gear")
1806 .with_polygon(vec![
1807 (50.0, 5.0), (65.0, 15.0),
1809 (77.0, 18.0),
1810 (80.0, 32.0),
1811 (95.0, 50.0), (80.0, 68.0),
1813 (77.0, 82.0),
1814 (65.0, 85.0),
1815 (50.0, 95.0), (35.0, 85.0),
1817 (23.0, 82.0),
1818 (20.0, 68.0),
1819 (5.0, 50.0), (20.0, 32.0),
1821 (23.0, 18.0),
1822 (35.0, 15.0),
1823 ])
1824 .with_quantity(1);
1825
1826 let boundary = Boundary2D::rectangle(100.0, 100.0);
1828
1829 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1830 let nester = Nester2D::new(config);
1831
1832 let result = nester
1833 .solve(std::slice::from_ref(&gear_like), &boundary)
1834 .unwrap();
1835
1836 assert_eq!(result.placements.len(), 1);
1837 let placement = &result.placements[0];
1838
1839 let origin_x = placement.position[0];
1841 let origin_y = placement.position[1];
1842
1843 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1845
1846 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
1848
1849 let actual_min_x = origin_x + g_min[0];
1851 let actual_max_x = origin_x + g_max[0];
1852 let actual_min_y = origin_y + g_min[1];
1853 let actual_max_y = origin_y + g_max[1];
1854
1855 assert!(
1857 actual_min_x >= 0.0,
1858 "Left edge {} should be >= 0",
1859 actual_min_x
1860 );
1861 assert!(
1862 actual_max_x <= 100.0,
1863 "Right edge {} should be <= 100",
1864 actual_max_x
1865 );
1866 assert!(
1867 actual_min_y >= 0.0,
1868 "Bottom edge {} should be >= 0",
1869 actual_min_y
1870 );
1871 assert!(
1872 actual_max_y <= 100.0,
1873 "Top edge {} should be <= 100",
1874 actual_max_y
1875 );
1876 }
1877
1878 #[test]
1879 fn test_blf_bounds_clamping_many_pieces() {
1880 let gear_like = Geometry2D::new("gear")
1883 .with_polygon(vec![
1884 (50.0, 5.0),
1885 (65.0, 15.0),
1886 (77.0, 18.0),
1887 (80.0, 32.0),
1888 (95.0, 50.0),
1889 (80.0, 68.0),
1890 (77.0, 82.0),
1891 (65.0, 85.0),
1892 (50.0, 95.0),
1893 (35.0, 85.0),
1894 (23.0, 82.0),
1895 (20.0, 68.0),
1896 (5.0, 50.0),
1897 (20.0, 32.0),
1898 (23.0, 18.0),
1899 (35.0, 15.0),
1900 ])
1901 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
1905
1906 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1907 let nester = Nester2D::new(config);
1908
1909 let result = nester
1910 .solve(std::slice::from_ref(&gear_like), &boundary)
1911 .unwrap();
1912
1913 for (i, placement) in result.placements.iter().enumerate() {
1915 let origin_x = placement.position[0];
1916 let origin_y = placement.position[1];
1917 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1918
1919 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
1920
1921 let actual_min_x = origin_x + g_min[0];
1922 let actual_max_x = origin_x + g_max[0];
1923 let actual_min_y = origin_y + g_min[1];
1924 let actual_max_y = origin_y + g_max[1];
1925
1926 assert!(
1927 actual_min_x >= -0.01,
1928 "Piece {}: Left edge {} should be >= 0",
1929 i,
1930 actual_min_x
1931 );
1932 assert!(
1933 actual_max_x <= 500.01,
1934 "Piece {}: Right edge {} should be <= 500",
1935 i,
1936 actual_max_x
1937 );
1938 assert!(
1939 actual_min_y >= -0.01,
1940 "Piece {}: Bottom edge {} should be >= 0",
1941 i,
1942 actual_min_y
1943 );
1944 assert!(
1945 actual_max_y <= 500.01,
1946 "Piece {}: Top edge {} should be <= 500",
1947 i,
1948 actual_max_y
1949 );
1950 }
1951 }
1952
1953 #[test]
1954 fn test_blf_bounds_trace() {
1955 let gear = Geometry2D::new("gear").with_polygon(vec![
1957 (50.0, 5.0),
1958 (65.0, 15.0),
1959 (77.0, 18.0),
1960 (80.0, 32.0),
1961 (95.0, 50.0),
1962 (80.0, 68.0),
1963 (77.0, 82.0),
1964 (65.0, 85.0),
1965 (50.0, 95.0),
1966 (35.0, 85.0),
1967 (23.0, 82.0),
1968 (20.0, 68.0),
1969 (5.0, 50.0),
1970 (20.0, 32.0),
1971 (23.0, 18.0),
1972 (35.0, 15.0),
1973 ]);
1974
1975 let (g_min, g_max) = gear.aabb();
1977 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
1978 assert!(
1979 (g_min[0] - 5.0).abs() < 0.01,
1980 "g_min[0] should be 5, got {}",
1981 g_min[0]
1982 );
1983 assert!(
1984 (g_max[0] - 95.0).abs() < 0.01,
1985 "g_max[0] should be 95, got {}",
1986 g_max[0]
1987 );
1988
1989 let b_max_x = 500.0;
1991 let margin = 0.0;
1992 let max_valid_x = b_max_x - margin - g_max[0];
1993 println!(
1994 "max_valid_x = {} - {} - {} = {}",
1995 b_max_x, margin, g_max[0], max_valid_x
1996 );
1997 assert!(
1998 (max_valid_x - 405.0).abs() < 0.01,
1999 "max_valid_x should be 405, got {}",
2000 max_valid_x
2001 );
2002
2003 let boundary = Boundary2D::rectangle(500.0, 500.0);
2005 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2006 let nester = Nester2D::new(config);
2007
2008 let result = nester
2009 .solve(&[gear.clone().with_quantity(1)], &boundary)
2010 .unwrap();
2011
2012 assert_eq!(result.placements.len(), 1);
2013 let p = &result.placements[0];
2014 let origin_x = p.position[0];
2015 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2016
2017 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2018 let actual_max_x = origin_x + g_max_r[0];
2019
2020 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2021 println!(
2022 "At rotation {}: g_min={:?}, g_max={:?}",
2023 rotation, g_min_r, g_max_r
2024 );
2025 println!(
2026 "Actual max x: {} + {} = {}",
2027 origin_x, g_max_r[0], actual_max_x
2028 );
2029
2030 assert!(
2031 actual_max_x <= 500.01,
2032 "Geometry exceeds boundary: max_x={} > 500",
2033 actual_max_x
2034 );
2035 }
2036
2037 #[test]
2038 fn test_blf_bounds_many_pieces_direct() {
2039 let gear = Geometry2D::new("gear")
2041 .with_polygon(vec![
2042 (50.0, 5.0),
2043 (65.0, 15.0),
2044 (77.0, 18.0),
2045 (80.0, 32.0),
2046 (95.0, 50.0),
2047 (80.0, 68.0),
2048 (77.0, 82.0),
2049 (65.0, 85.0),
2050 (50.0, 95.0),
2051 (35.0, 85.0),
2052 (23.0, 82.0),
2053 (20.0, 68.0),
2054 (5.0, 50.0),
2055 (20.0, 32.0),
2056 (23.0, 18.0),
2057 (35.0, 15.0),
2058 ])
2059 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2062 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2063 let nester = Nester2D::new(config);
2064
2065 let result = nester
2066 .solve(std::slice::from_ref(&gear), &boundary)
2067 .unwrap();
2068
2069 println!("Placed {} pieces", result.placements.len());
2070
2071 for (i, p) in result.placements.iter().enumerate() {
2073 let origin_x = p.position[0];
2074 let origin_y = p.position[1];
2075 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2076
2077 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2078
2079 let actual_min_x = origin_x + g_min_r[0];
2080 let actual_max_x = origin_x + g_max_r[0];
2081 let actual_min_y = origin_y + g_min_r[1];
2082 let actual_max_y = origin_y + g_max_r[1];
2083
2084 println!(
2085 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2086 i,
2087 origin_x,
2088 origin_y,
2089 rotation,
2090 actual_min_x,
2091 actual_max_x,
2092 actual_min_y,
2093 actual_max_y
2094 );
2095
2096 assert!(
2097 actual_max_x <= 500.01,
2098 "Piece {}: Right edge {} > 500",
2099 i,
2100 actual_max_x
2101 );
2102 assert!(
2103 actual_max_y <= 500.01,
2104 "Piece {}: Top edge {} > 500",
2105 i,
2106 actual_max_y
2107 );
2108 }
2109 }
2110
2111 #[test]
2112 fn test_blf_bounds_multi_strip() {
2113 let gear = Geometry2D::new("gear")
2115 .with_polygon(vec![
2116 (50.0, 5.0),
2117 (65.0, 15.0),
2118 (77.0, 18.0),
2119 (80.0, 32.0),
2120 (95.0, 50.0),
2121 (80.0, 68.0),
2122 (77.0, 82.0),
2123 (65.0, 85.0),
2124 (50.0, 95.0),
2125 (35.0, 85.0),
2126 (23.0, 82.0),
2127 (20.0, 68.0),
2128 (5.0, 50.0),
2129 (20.0, 32.0),
2130 (23.0, 18.0),
2131 (35.0, 15.0),
2132 ])
2133 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2136 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2137 let nester = Nester2D::new(config);
2138
2139 let result = nester
2141 .solve_multi_strip(std::slice::from_ref(&gear), &boundary)
2142 .unwrap();
2143
2144 println!(
2145 "Placed {} pieces across {} strips",
2146 result.placements.len(),
2147 result.boundaries_used
2148 );
2149
2150 let strip_width = 500.0;
2152 for (i, p) in result.placements.iter().enumerate() {
2153 let origin_x = p.position[0];
2154 let origin_y = p.position[1];
2155 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2156 let strip_idx = p.boundary_index;
2157
2158 let local_x = origin_x - (strip_idx as f64 * strip_width);
2160
2161 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2162
2163 let local_max_x = local_x + g_max_r[0];
2164 let local_max_y = origin_y + g_max_r[1];
2165
2166 println!(
2167 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2168 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2169 );
2170
2171 assert!(
2172 local_max_x <= 500.01,
2173 "Piece {}: In strip {}, local right edge {:.1} > 500",
2174 i,
2175 strip_idx,
2176 local_max_x
2177 );
2178 assert!(
2179 local_max_y <= 500.01,
2180 "Piece {}: Top edge {:.1} > 500",
2181 i,
2182 local_max_y
2183 );
2184 }
2185 }
2186
2187 #[test]
2188 fn test_blf_bounds_mixed_shapes() {
2189 let shapes = vec![
2191 Geometry2D::new("shape0")
2193 .with_polygon(vec![
2194 (0.0, 0.0),
2195 (180.0, 0.0),
2196 (195.0, 15.0),
2197 (200.0, 50.0),
2198 (200.0, 150.0),
2199 (195.0, 185.0),
2200 (180.0, 200.0),
2201 (20.0, 200.0),
2202 (5.0, 185.0),
2203 (0.0, 150.0),
2204 (0.0, 50.0),
2205 (5.0, 15.0),
2206 ])
2207 .with_quantity(2),
2208 Geometry2D::new("shape1")
2210 .with_polygon(vec![
2211 (60.0, 0.0),
2212 (85.0, 7.0),
2213 (104.0, 25.0),
2214 (118.0, 50.0),
2215 (120.0, 60.0),
2216 (118.0, 70.0),
2217 (104.0, 95.0),
2218 (85.0, 113.0),
2219 (60.0, 120.0),
2220 (35.0, 113.0),
2221 (16.0, 95.0),
2222 (2.0, 70.0),
2223 (0.0, 60.0),
2224 (2.0, 50.0),
2225 (16.0, 25.0),
2226 (35.0, 7.0),
2227 ])
2228 .with_quantity(4),
2229 Geometry2D::new("shape2")
2231 .with_polygon(vec![
2232 (0.0, 0.0),
2233 (80.0, 0.0),
2234 (80.0, 20.0),
2235 (20.0, 20.0),
2236 (20.0, 80.0),
2237 (0.0, 80.0),
2238 ])
2239 .with_quantity(6),
2240 Geometry2D::new("shape3")
2242 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2243 .with_quantity(6),
2244 Geometry2D::new("shape4")
2246 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2247 .with_quantity(4),
2248 Geometry2D::new("shape5")
2250 .with_polygon(vec![
2251 (15.0, 0.0),
2252 (45.0, 0.0),
2253 (60.0, 26.0),
2254 (45.0, 52.0),
2255 (15.0, 52.0),
2256 (0.0, 26.0),
2257 ])
2258 .with_quantity(8),
2259 Geometry2D::new("shape6")
2261 .with_polygon(vec![
2262 (0.0, 0.0),
2263 (90.0, 0.0),
2264 (90.0, 12.0),
2265 (55.0, 12.0),
2266 (55.0, 60.0),
2267 (35.0, 60.0),
2268 (35.0, 12.0),
2269 (0.0, 12.0),
2270 ])
2271 .with_quantity(4),
2272 Geometry2D::new("shape7")
2274 .with_polygon(vec![
2275 (0.0, 10.0),
2276 (10.0, 0.0),
2277 (70.0, 0.0),
2278 (80.0, 10.0),
2279 (80.0, 70.0),
2280 (70.0, 80.0),
2281 (10.0, 80.0),
2282 (0.0, 70.0),
2283 ])
2284 .with_quantity(3),
2285 Geometry2D::new("shape8_gear")
2287 .with_polygon(vec![
2288 (50.0, 5.0),
2289 (65.0, 15.0),
2290 (77.0, 18.0),
2291 (80.0, 32.0),
2292 (95.0, 50.0),
2293 (80.0, 68.0),
2294 (77.0, 82.0),
2295 (65.0, 85.0),
2296 (50.0, 95.0),
2297 (35.0, 85.0),
2298 (23.0, 82.0),
2299 (20.0, 68.0),
2300 (5.0, 50.0),
2301 (20.0, 32.0),
2302 (23.0, 18.0),
2303 (35.0, 15.0),
2304 ])
2305 .with_quantity(13),
2306 ];
2307
2308 let boundary = Boundary2D::rectangle(500.0, 500.0);
2310 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2311 let nester = Nester2D::new(config);
2312
2313 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2314
2315 println!(
2316 "Placed {} pieces across {} strips",
2317 result.placements.len(),
2318 result.boundaries_used
2319 );
2320
2321 let strip_width = 500.0;
2323 let gear_aabb = shapes[8].aabb();
2324 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2325
2326 let mut violations = Vec::new();
2327 for p in &result.placements {
2328 if p.geometry_id.as_str().starts_with("shape8") {
2329 let origin_x = p.position[0];
2330 let _origin_y = p.position[1];
2331 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2332 let strip_idx = p.boundary_index;
2333 let local_x = origin_x - (strip_idx as f64 * strip_width);
2334
2335 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2336 let local_max_x = local_x + g_max_r[0];
2337
2338 println!(
2339 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2340 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2341 );
2342
2343 if local_max_x > 500.01 {
2344 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2345 }
2346 }
2347 }
2348
2349 assert!(
2350 violations.is_empty(),
2351 "Found {} Gear pieces exceeding boundary: {:?}",
2352 violations.len(),
2353 violations
2354 );
2355 }
2356
2357 #[test]
2358 fn test_blf_bounds_expanded_like_benchmark() {
2359 type ShapeDef = (Vec<(f64, f64)>, usize, Vec<f64>);
2363 let shape_defs: Vec<ShapeDef> = vec![
2364 (
2365 vec![
2366 (0.0, 0.0),
2367 (180.0, 0.0),
2368 (195.0, 15.0),
2369 (200.0, 50.0),
2370 (200.0, 150.0),
2371 (195.0, 185.0),
2372 (180.0, 200.0),
2373 (20.0, 200.0),
2374 (5.0, 185.0),
2375 (0.0, 150.0),
2376 (0.0, 50.0),
2377 (5.0, 15.0),
2378 ],
2379 2,
2380 vec![0.0, 90.0, 180.0, 270.0],
2381 ),
2382 (
2383 vec![
2384 (60.0, 0.0),
2385 (85.0, 7.0),
2386 (104.0, 25.0),
2387 (118.0, 50.0),
2388 (120.0, 60.0),
2389 (118.0, 70.0),
2390 (104.0, 95.0),
2391 (85.0, 113.0),
2392 (60.0, 120.0),
2393 (35.0, 113.0),
2394 (16.0, 95.0),
2395 (2.0, 70.0),
2396 (0.0, 60.0),
2397 (2.0, 50.0),
2398 (16.0, 25.0),
2399 (35.0, 7.0),
2400 ],
2401 4,
2402 vec![0.0, 45.0, 90.0, 135.0],
2403 ),
2404 (
2405 vec![
2406 (0.0, 0.0),
2407 (80.0, 0.0),
2408 (80.0, 20.0),
2409 (20.0, 20.0),
2410 (20.0, 80.0),
2411 (0.0, 80.0),
2412 ],
2413 6,
2414 vec![0.0, 90.0, 180.0, 270.0],
2415 ),
2416 (
2417 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
2418 6,
2419 vec![0.0, 90.0, 180.0, 270.0],
2420 ),
2421 (
2422 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
2423 4,
2424 vec![0.0, 90.0],
2425 ),
2426 (
2427 vec![
2428 (15.0, 0.0),
2429 (45.0, 0.0),
2430 (60.0, 26.0),
2431 (45.0, 52.0),
2432 (15.0, 52.0),
2433 (0.0, 26.0),
2434 ],
2435 8,
2436 vec![0.0, 60.0, 120.0],
2437 ),
2438 (
2439 vec![
2440 (0.0, 0.0),
2441 (90.0, 0.0),
2442 (90.0, 12.0),
2443 (55.0, 12.0),
2444 (55.0, 60.0),
2445 (35.0, 60.0),
2446 (35.0, 12.0),
2447 (0.0, 12.0),
2448 ],
2449 4,
2450 vec![0.0, 90.0, 180.0, 270.0],
2451 ),
2452 (
2453 vec![
2454 (0.0, 10.0),
2455 (10.0, 0.0),
2456 (70.0, 0.0),
2457 (80.0, 10.0),
2458 (80.0, 70.0),
2459 (70.0, 80.0),
2460 (10.0, 80.0),
2461 (0.0, 70.0),
2462 ],
2463 3,
2464 vec![0.0, 90.0],
2465 ),
2466 (
2468 vec![
2469 (50.0, 5.0),
2470 (65.0, 15.0),
2471 (77.0, 18.0),
2472 (80.0, 32.0),
2473 (95.0, 50.0),
2474 (80.0, 68.0),
2475 (77.0, 82.0),
2476 (65.0, 85.0),
2477 (50.0, 95.0),
2478 (35.0, 85.0),
2479 (23.0, 82.0),
2480 (20.0, 68.0),
2481 (5.0, 50.0),
2482 (20.0, 32.0),
2483 (23.0, 18.0),
2484 (35.0, 15.0),
2485 ],
2486 13,
2487 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
2488 ),
2489 ];
2490
2491 let mut geometries = Vec::new();
2493 let mut piece_id = 0;
2494 for (vertices, demand, rotations) in shape_defs.iter() {
2495 for _ in 0..*demand {
2496 let geom = Geometry2D::new(format!("piece_{}", piece_id))
2497 .with_polygon(vertices.clone())
2498 .with_rotations_deg(rotations.clone());
2499 geometries.push(geom);
2500 piece_id += 1;
2501 }
2502 }
2503
2504 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
2506 let (gear_min, gear_max) = gear_geom.aabb();
2507 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
2508
2509 let boundary = Boundary2D::rectangle(500.0, 500.0);
2510 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2511 let nester = Nester2D::new(config);
2512
2513 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
2514
2515 println!(
2516 "Placed {} pieces across {} strips",
2517 result.placements.len(),
2518 result.boundaries_used
2519 );
2520
2521 let strip_width = 500.0;
2523 let mut violations = Vec::new();
2524
2525 for p in &result.placements {
2526 let id_num: usize = p
2527 .geometry_id
2528 .as_str()
2529 .strip_prefix("piece_")
2530 .and_then(|s| s.parse().ok())
2531 .unwrap_or(0);
2532
2533 if (37..=49).contains(&id_num) {
2535 let origin_x = p.position[0];
2536 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2537 let strip_idx = p.boundary_index;
2538 let local_x = origin_x - (strip_idx as f64 * strip_width);
2539
2540 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
2541 let local_max_x = local_x + g_max_r[0];
2542
2543 println!(
2544 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2545 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2546 );
2547
2548 if local_max_x > 500.01 {
2549 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2550 }
2551 }
2552 }
2553
2554 assert!(
2555 violations.is_empty(),
2556 "Found {} Gear pieces exceeding boundary: {:?}",
2557 violations.len(),
2558 violations
2559 );
2560 }
2561
2562 fn aabbs_overlap(
2564 a_min: [f64; 2],
2565 a_max: [f64; 2],
2566 b_min: [f64; 2],
2567 b_max: [f64; 2],
2568 tolerance: f64,
2569 ) -> bool {
2570 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
2572 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
2573 x_overlap && y_overlap
2574 }
2575
2576 #[test]
2578 fn test_all_strategies_boundary_and_overlap() {
2579 use std::collections::HashMap;
2580
2581 let shapes = vec![
2583 Geometry2D::new("shape0")
2584 .with_polygon(vec![
2585 (0.0, 0.0),
2586 (180.0, 0.0),
2587 (195.0, 15.0),
2588 (200.0, 50.0),
2589 (200.0, 150.0),
2590 (195.0, 185.0),
2591 (180.0, 200.0),
2592 (20.0, 200.0),
2593 (5.0, 185.0),
2594 (0.0, 150.0),
2595 (0.0, 50.0),
2596 (5.0, 15.0),
2597 ])
2598 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2599 .with_quantity(2),
2600 Geometry2D::new("shape1_flange")
2601 .with_polygon(vec![
2602 (60.0, 0.0),
2603 (85.0, 7.0),
2604 (104.0, 25.0),
2605 (118.0, 50.0),
2606 (120.0, 60.0),
2607 (118.0, 70.0),
2608 (104.0, 95.0),
2609 (85.0, 113.0),
2610 (60.0, 120.0),
2611 (35.0, 113.0),
2612 (16.0, 95.0),
2613 (2.0, 70.0),
2614 (0.0, 60.0),
2615 (2.0, 50.0),
2616 (16.0, 25.0),
2617 (35.0, 7.0),
2618 ])
2619 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
2620 .with_quantity(4),
2621 Geometry2D::new("shape2_lbracket")
2622 .with_polygon(vec![
2623 (0.0, 0.0),
2624 (80.0, 0.0),
2625 (80.0, 20.0),
2626 (20.0, 20.0),
2627 (20.0, 80.0),
2628 (0.0, 80.0),
2629 ])
2630 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2631 .with_quantity(6),
2632 Geometry2D::new("shape3_triangle")
2633 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2634 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2635 .with_quantity(6),
2636 Geometry2D::new("shape4_rect")
2637 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2638 .with_rotations_deg(vec![0.0, 90.0])
2639 .with_quantity(4),
2640 Geometry2D::new("shape5_hexagon")
2641 .with_polygon(vec![
2642 (15.0, 0.0),
2643 (45.0, 0.0),
2644 (60.0, 26.0),
2645 (45.0, 52.0),
2646 (15.0, 52.0),
2647 (0.0, 26.0),
2648 ])
2649 .with_rotations_deg(vec![0.0, 60.0, 120.0])
2650 .with_quantity(8),
2651 Geometry2D::new("shape6_tstiff")
2652 .with_polygon(vec![
2653 (0.0, 0.0),
2654 (90.0, 0.0),
2655 (90.0, 12.0),
2656 (55.0, 12.0),
2657 (55.0, 60.0),
2658 (35.0, 60.0),
2659 (35.0, 12.0),
2660 (0.0, 12.0),
2661 ])
2662 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2663 .with_quantity(4),
2664 Geometry2D::new("shape7_mount")
2665 .with_polygon(vec![
2666 (0.0, 10.0),
2667 (10.0, 0.0),
2668 (70.0, 0.0),
2669 (80.0, 10.0),
2670 (80.0, 70.0),
2671 (70.0, 80.0),
2672 (10.0, 80.0),
2673 (0.0, 70.0),
2674 ])
2675 .with_rotations_deg(vec![0.0, 90.0])
2676 .with_quantity(3),
2677 Geometry2D::new("shape8_gear")
2678 .with_polygon(vec![
2679 (50.0, 5.0),
2680 (65.0, 15.0),
2681 (77.0, 18.0),
2682 (80.0, 32.0),
2683 (95.0, 50.0),
2684 (80.0, 68.0),
2685 (77.0, 82.0),
2686 (65.0, 85.0),
2687 (50.0, 95.0),
2688 (35.0, 85.0),
2689 (23.0, 82.0),
2690 (20.0, 68.0),
2691 (5.0, 50.0),
2692 (20.0, 32.0),
2693 (23.0, 18.0),
2694 (35.0, 15.0),
2695 ])
2696 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
2697 .with_quantity(13),
2698 ];
2699
2700 let geom_map: HashMap<String, &Geometry2D> =
2702 shapes.iter().map(|g| (g.id().clone(), g)).collect();
2703
2704 let boundary = Boundary2D::rectangle(500.0, 500.0);
2705 let strip_width = 500.0;
2706
2707 let strategies = vec![
2709 Strategy::BottomLeftFill,
2710 Strategy::NfpGuided,
2711 Strategy::GeneticAlgorithm,
2712 Strategy::Brkga,
2713 Strategy::SimulatedAnnealing,
2714 Strategy::Gdrr,
2715 Strategy::Alns,
2716 ];
2717
2718 for strategy in strategies {
2719 println!("\n========== Testing {:?} ==========", strategy);
2720
2721 let config = Config::default()
2722 .with_strategy(strategy)
2723 .with_time_limit(30000); let nester = Nester2D::new(config);
2725
2726 let result = match nester.solve_multi_strip(&shapes, &boundary) {
2727 Ok(r) => r,
2728 Err(e) => {
2729 println!(" Strategy {:?} failed: {}", strategy, e);
2730 continue;
2731 }
2732 };
2733
2734 println!(
2735 " Placed {} pieces across {} strips",
2736 result.placements.len(),
2737 result.boundaries_used
2738 );
2739
2740 let mut boundary_violations = Vec::new();
2742 for p in &result.placements {
2743 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
2745 let full_id = if base_id.starts_with("shape") {
2746 shapes
2748 .iter()
2749 .find(|g| p.geometry_id.starts_with(g.id()))
2750 .map(|g| g.id().as_str())
2751 } else {
2752 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
2753 };
2754
2755 let geom = match full_id.and_then(|id| geom_map.get(id)) {
2756 Some(g) => *g,
2757 None => {
2758 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
2760 Some(g) => g,
2761 None => {
2762 println!(
2763 " WARNING: Could not find geometry for {}",
2764 p.geometry_id
2765 );
2766 continue;
2767 }
2768 }
2769 }
2770 };
2771
2772 let origin_x = p.position[0];
2773 let origin_y = p.position[1];
2774 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2775 let strip_idx = p.boundary_index;
2776
2777 let local_x = origin_x - (strip_idx as f64 * strip_width);
2779
2780 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
2781
2782 let local_min_x = local_x + g_min[0];
2784 let local_max_x = local_x + g_max[0];
2785 let local_min_y = origin_y + g_min[1];
2786 let local_max_y = origin_y + g_max[1];
2787
2788 let tolerance = 0.1;
2790 if local_min_x < -tolerance
2791 || local_max_x > 500.0 + tolerance
2792 || local_min_y < -tolerance
2793 || local_max_y > 500.0 + tolerance
2794 {
2795 boundary_violations.push(format!(
2796 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
2797 p.geometry_id,
2798 strip_idx,
2799 local_min_x,
2800 local_min_y,
2801 local_max_x,
2802 local_max_y
2803 ));
2804 }
2805 }
2806
2807 if !boundary_violations.is_empty() {
2808 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
2809 for v in &boundary_violations {
2810 println!(" - {}", v);
2811 }
2812 }
2813
2814 let mut overlaps = Vec::new();
2816 let placements: Vec<_> = result.placements.iter().collect();
2817
2818 for i in 0..placements.len() {
2819 for j in (i + 1)..placements.len() {
2820 let p1 = placements[i];
2821 let p2 = placements[j];
2822
2823 if p1.boundary_index != p2.boundary_index {
2825 continue;
2826 }
2827
2828 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
2830 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
2831
2832 let (g1, g2) = match (g1, g2) {
2833 (Some(a), Some(b)) => (a, b),
2834 _ => continue,
2835 };
2836
2837 let strip_idx = p1.boundary_index;
2838 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
2839 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
2840
2841 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
2842 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
2843
2844 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
2845 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
2846
2847 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
2848 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
2849 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
2850 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
2851
2852 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
2853 overlaps.push(format!(
2854 "{} and {} in strip {}",
2855 p1.geometry_id, p2.geometry_id, strip_idx
2856 ));
2857 }
2858 }
2859 }
2860
2861 if !overlaps.is_empty() {
2862 println!(" OVERLAPS ({}):", overlaps.len());
2863 for o in overlaps.iter().take(10) {
2864 println!(" - {}", o);
2865 }
2866 if overlaps.len() > 10 {
2867 println!(" ... and {} more", overlaps.len() - 10);
2868 }
2869 }
2870
2871 assert!(
2873 boundary_violations.is_empty(),
2874 "{:?}: Found {} boundary violations",
2875 strategy,
2876 boundary_violations.len()
2877 );
2878
2879 println!(" ✓ All placements within boundary");
2880 println!(" ✓ No AABB overlaps detected");
2881 }
2882 }
2883}