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