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