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;
10use crate::nfp::{
11 compute_ifp_with_margin_and_mirror, compute_nfp_mirrored, find_bottom_left_placement,
12 rotate_nfp, translate_nfp, Nfp, NfpCache, PlacedGeometry,
13};
14#[cfg(feature = "milp")]
15use crate::nfp_cm_solver::run_nfp_cm_nesting;
16use crate::sa_nesting::run_sa_nesting;
17use crate::validate_and_filter_placements;
18use u_nesting_core::alns::AlnsConfig;
19use u_nesting_core::brkga::BrkgaConfig;
20#[cfg(feature = "milp")]
21use u_nesting_core::exact::ExactConfig;
22use u_nesting_core::ga::GaConfig;
23use u_nesting_core::gdrr::GdrrConfig;
24use u_nesting_core::geometry::{Boundary, Geometry};
25use u_nesting_core::sa::SaConfig;
26use u_nesting_core::solver::{Config, ProgressCallback, ProgressInfo, Solver, Strategy};
27use u_nesting_core::{Placement, Result, SolveResult};
28
29use crate::placement_utils::{expand_nfp, shrink_ifp};
30use std::sync::atomic::{AtomicBool, Ordering};
31use std::sync::Arc;
32use u_nesting_core::timing::Timer;
33
34fn mirror_candidates(geom: &Geometry2D) -> &'static [bool] {
46 if geom.allow_flip() {
47 &[false, true]
48 } else {
49 &[false]
50 }
51}
52
53fn solution_quality(
67 result: &SolveResult<f64>,
68 geometries: &[Geometry2D],
69 boundary: &Boundary2D,
70) -> (usize, f64) {
71 use std::collections::HashMap;
72 let geom_map: HashMap<_, _> = geometries.iter().map(|g| (g.id().clone(), g)).collect();
73
74 let mut min_x = f64::INFINITY;
75 let mut min_y = f64::INFINITY;
76 let mut max_x = f64::NEG_INFINITY;
77 let mut max_y = f64::NEG_INFINITY;
78
79 for p in &result.placements {
80 if let Some(geom) = geom_map.get(&p.geometry_id) {
81 let x = p.position.first().copied().unwrap_or(0.0);
82 let y = p.position.get(1).copied().unwrap_or(0.0);
83 let rot = p.rotation.first().copied().unwrap_or(0.0);
84 let (g_min, g_max) = geom.aabb_at_rotation(rot);
85 min_x = min_x.min(x + g_min[0]);
86 min_y = min_y.min(y + g_min[1]);
87 max_x = max_x.max(x + g_max[0]);
88 max_y = max_y.max(y + g_max[1]);
89 }
90 }
91
92 if result.placements.is_empty() {
93 return (0, f64::INFINITY);
94 }
95
96 let (b_min, b_max) = boundary.aabb();
98 let bound_w = b_max[0] - b_min[0];
99 let bound_h = b_max[1] - b_min[1];
100 let strip_length = if bound_h >= bound_w {
101 max_y - min_y
102 } else {
103 max_x - min_x
104 };
105 (result.placements.len(), strip_length)
106}
107
108pub struct Nester2D {
110 config: Config,
111 cancelled: Arc<AtomicBool>,
112 #[allow(dead_code)] nfp_cache: NfpCache,
114}
115
116impl Nester2D {
117 pub fn new(config: Config) -> Self {
119 Self {
120 config,
121 cancelled: Arc::new(AtomicBool::new(false)),
122 nfp_cache: NfpCache::new(),
123 }
124 }
125
126 pub fn default_config() -> Self {
128 Self::new(Config::default())
129 }
130
131 fn bottom_left_fill(
141 &self,
142 geometries: &[Geometry2D],
143 boundary: &Boundary2D,
144 ) -> Result<SolveResult<f64>> {
145 let start = Timer::now();
146 let mut result = SolveResult::new();
147 let mut placements = Vec::new();
148
149 let (b_min, b_max) = boundary.aabb();
151 let margin = self.config.margin;
152 let spacing = self.config.spacing;
153
154 let bound_min_x = b_min[0] + margin;
155 let bound_min_y = b_min[1] + margin;
156 let bound_max_x = b_max[0] - margin;
157 let bound_max_y = b_max[1] - margin;
158
159 let strip_width = bound_max_x - bound_min_x;
160 let strip_height = bound_max_y - bound_min_y;
161
162 let mut current_x = bound_min_x;
164 let mut current_y = bound_min_y;
165 let mut row_height = 0.0_f64;
166
167 let mut total_placed_area = 0.0;
168
169 for geom in geometries {
170 geom.validate()?;
171
172 let rotations = geom.rotations();
174 let rotation_angles: Vec<f64> = if rotations.is_empty() {
175 vec![0.0]
176 } else {
177 rotations
178 };
179
180 for instance in 0..geom.quantity() {
181 if self.cancelled.load(Ordering::Relaxed) {
182 result.computation_time_ms = start.elapsed_ms();
183 return Ok(result);
184 }
185
186 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
188 {
189 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
190 result.utilization = total_placed_area / boundary.measure();
191 result.computation_time_ms = start.elapsed_ms();
192 result.placements = placements;
193 return Ok(result);
194 }
195
196 let mut best_fit: Option<(f64, f64, f64, f64, f64, [f64; 2])> = None; for &rotation in &rotation_angles {
200 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
201 let g_width = g_max[0] - g_min[0];
202 let g_height = g_max[1] - g_min[1];
203
204 if g_width > strip_width || g_height > strip_height {
206 continue;
207 }
208
209 let mut place_x = current_x;
211 let mut place_y = current_y;
212
213 if place_x + g_width > bound_max_x {
215 place_x = bound_min_x;
217 place_y += row_height + spacing;
218 }
219
220 if place_y + g_height > bound_max_y {
222 continue; }
224
225 let score = if place_x == bound_min_x && place_y > current_y {
228 place_y - bound_min_y + g_height
230 } else {
231 place_x - bound_min_x + g_width
233 };
234
235 let is_better = match &best_fit {
236 None => true,
237 Some((_, _, _, _, _, _)) => {
238 let best_score = if let Some((_, _, _, bx, by, _)) = best_fit {
240 if bx == bound_min_x && by > current_y {
241 by - bound_min_y + g_height
242 } else {
243 bx - bound_min_x + g_width
244 }
245 } else {
246 f64::INFINITY
247 };
248 score < best_score - 1e-6
249 }
250 };
251
252 if is_better {
253 best_fit = Some((rotation, g_width, g_height, place_x, place_y, g_min));
254 }
255 }
256
257 if let Some((rotation, g_width, g_height, place_x, place_y, g_min)) = best_fit {
259 if place_x == bound_min_x && place_y > current_y {
261 row_height = 0.0;
262 }
263
264 let origin_x = place_x - g_min[0];
266 let origin_y = place_y - g_min[1];
267
268 let geom_aabb = geom.aabb_at_rotation(rotation);
270 let boundary_aabb = (b_min, b_max);
271
272 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
273 origin_x,
274 origin_y,
275 geom_aabb,
276 boundary_aabb,
277 margin,
278 ) {
279 let placement = Placement::new_2d(
280 geom.id().clone(),
281 instance,
282 clamped_x,
283 clamped_y,
284 rotation,
285 );
286
287 placements.push(placement);
288 total_placed_area += geom.measure();
289
290 let actual_place_x = clamped_x + g_min[0];
293 let actual_place_y = clamped_y + g_min[1];
294 current_x = actual_place_x + g_width + spacing;
295 current_y = actual_place_y;
296 row_height = row_height.max(g_height);
297 }
298 } else {
299 result.unplaced.push(geom.id().clone());
301 }
302 }
303 }
304
305 result.placements = placements;
306 result.boundaries_used = 1;
307 result.utilization = total_placed_area / boundary.measure();
308 result.computation_time_ms = start.elapsed_ms();
309
310 Ok(result)
311 }
312
313 fn nfp_guided_blf(
318 &self,
319 geometries: &[Geometry2D],
320 boundary: &Boundary2D,
321 ) -> Result<SolveResult<f64>> {
322 let start = Timer::now();
323 let mut result = SolveResult::new();
324 let mut placements = Vec::new();
325 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
326
327 let margin = self.config.margin;
328 let spacing = self.config.spacing;
329
330 let boundary_polygon = self.get_boundary_polygon_with_margin(boundary, margin);
332
333 let mut total_placed_area = 0.0;
334
335 let sample_step = self.compute_sample_step(geometries);
337
338 for geom in geometries {
339 geom.validate()?;
340
341 let rotations = geom.rotations();
343 let rotation_angles: Vec<f64> = if rotations.is_empty() {
344 vec![0.0]
345 } else {
346 rotations
347 };
348
349 let mirror_candidates = mirror_candidates(geom);
350
351 for instance in 0..geom.quantity() {
352 if self.cancelled.load(Ordering::Relaxed) {
353 result.computation_time_ms = start.elapsed_ms();
354 return Ok(result);
355 }
356
357 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
359 {
360 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
361 result.utilization = total_placed_area / boundary.measure();
362 result.computation_time_ms = start.elapsed_ms();
363 result.placements = placements;
364 return Ok(result);
365 }
366
367 let mut best_placement: Option<(f64, f64, f64, bool)> = None; for &rotation in &rotation_angles {
371 for &mirror in mirror_candidates {
372 let ifp = match compute_ifp_with_margin_and_mirror(
374 &boundary_polygon,
375 geom,
376 rotation,
377 margin,
378 mirror,
379 ) {
380 Ok(ifp) => ifp,
381 Err(_) => continue,
382 };
383
384 if ifp.is_empty() {
385 continue;
386 }
387
388 let mut nfps: Vec<Nfp> = Vec::new();
390 for placed in &placed_geometries {
391 let cache_key = (
394 placed.geometry.id().as_str(),
395 geom.id().as_str(),
396 rotation - placed.rotation, placed.mirrored,
398 mirror,
399 );
400
401 let nfp_at_origin =
406 match self.nfp_cache.get_or_compute_mirrored(cache_key, || {
407 let placed_at_origin = placed.geometry.clone();
408 compute_nfp_mirrored(
409 &placed_at_origin,
410 geom,
411 rotation - placed.rotation,
412 placed.mirrored,
413 mirror,
414 )
415 }) {
416 Ok(nfp) => nfp,
417 Err(_) => continue,
418 };
419
420 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
423 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
424 let expanded = self.expand_nfp(&translated_nfp, spacing);
425 nfps.push(expanded);
426 }
427
428 let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
430
431 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
433 if let Some((x, y)) =
434 find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
435 {
436 let is_better = match best_placement {
438 None => true,
439 Some((best_x, best_y, _, _)) => {
440 x < best_x - 1e-6 || (x < best_x + 1e-6 && y < best_y - 1e-6)
441 }
442 };
443 if is_better {
444 best_placement = Some((x, y, rotation, mirror));
445 }
446 }
447 }
448 }
449
450 if let Some((x, y, rotation, mirror)) = best_placement {
452 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
459 let boundary_aabb = boundary.aabb();
460
461 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
462 x,
463 y,
464 geom_aabb,
465 boundary_aabb,
466 margin,
467 ) {
468 let placement = Placement::new_2d(
469 geom.id().clone(),
470 instance,
471 clamped_x,
472 clamped_y,
473 rotation,
474 )
475 .with_mirrored(mirror);
476
477 placements.push(placement);
478 placed_geometries.push(
479 PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation)
480 .with_mirrored(mirror),
481 );
482 total_placed_area += geom.measure();
483 } else {
484 result.unplaced.push(geom.id().clone());
486 }
487 } else {
488 result.unplaced.push(geom.id().clone());
490 }
491 }
492 }
493
494 result.placements = placements;
495 result.boundaries_used = 1;
496 result.utilization = total_placed_area / boundary.measure();
497 result.computation_time_ms = start.elapsed_ms();
498
499 Ok(result)
500 }
501
502 fn get_boundary_polygon_with_margin(
504 &self,
505 boundary: &Boundary2D,
506 margin: f64,
507 ) -> Vec<(f64, f64)> {
508 let (b_min, b_max) = boundary.aabb();
509
510 vec![
512 (b_min[0] + margin, b_min[1] + margin),
513 (b_max[0] - margin, b_min[1] + margin),
514 (b_max[0] - margin, b_max[1] - margin),
515 (b_min[0] + margin, b_max[1] - margin),
516 ]
517 }
518
519 fn compute_sample_step(&self, geometries: &[Geometry2D]) -> f64 {
521 if geometries.is_empty() {
522 return 1.0;
523 }
524
525 let mut min_dim = f64::INFINITY;
527 for geom in geometries {
528 let (g_min, g_max) = geom.aabb();
529 let width = g_max[0] - g_min[0];
530 let height = g_max[1] - g_min[1];
531 min_dim = min_dim.min(width).min(height);
532 }
533
534 (min_dim / 4.0).clamp(0.5, 10.0)
536 }
537
538 fn expand_nfp(&self, nfp: &Nfp, spacing: f64) -> Nfp {
540 expand_nfp(nfp, spacing)
541 }
542
543 fn shrink_ifp(&self, ifp: &Nfp, spacing: f64) -> Nfp {
545 shrink_ifp(ifp, spacing)
546 }
547
548 fn not_worse_than_blf(
557 &self,
558 meta: SolveResult<f64>,
559 geometries: &[Geometry2D],
560 boundary: &Boundary2D,
561 ) -> SolveResult<f64> {
562 let blf = match self.bottom_left_fill(geometries, boundary) {
563 Ok(b) => b,
564 Err(_) => return meta,
565 };
566 let (meta_placed, meta_len) = solution_quality(&meta, geometries, boundary);
567 let (blf_placed, blf_len) = solution_quality(&blf, geometries, boundary);
568 if blf_placed > meta_placed || (blf_placed == meta_placed && blf_len < meta_len - 1e-6) {
571 let mut floored = blf;
577 floored.strategy = meta.strategy;
578 floored.generations = meta.generations;
579 floored.best_fitness = meta.best_fitness;
580 floored.fitness_history = meta.fitness_history;
581 floored.target_reached = meta.target_reached;
582 floored
583 } else {
584 meta
585 }
586 }
587
588 fn genetic_algorithm(
593 &self,
594 geometries: &[Geometry2D],
595 boundary: &Boundary2D,
596 ) -> Result<SolveResult<f64>> {
597 let time_limit_ms = if self.config.time_limit_ms > 0 {
599 (self.config.time_limit_ms / 4)
604 .max(5000)
605 .min(self.config.time_limit_ms)
606 } else {
607 15000 };
609
610 let ga_config = GaConfig::default()
611 .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)
614 .with_mutation_rate(self.config.mutation_rate)
615 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
616
617 let result = run_ga_nesting(
618 geometries,
619 boundary,
620 &self.config,
621 ga_config,
622 self.cancelled.clone(),
623 );
624
625 Ok(self.not_worse_than_blf(result, geometries, boundary))
626 }
627
628 fn brkga(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
632 let time_limit_ms = if self.config.time_limit_ms > 0 {
634 (self.config.time_limit_ms / 4)
639 .max(5000)
640 .min(self.config.time_limit_ms)
641 } else {
642 15000 };
644
645 let brkga_config = BrkgaConfig::default()
646 .with_population_size(self.config.population_size.min(30))
650 .with_max_generations(50) .with_elite_fraction(0.2)
652 .with_mutant_fraction(0.15)
653 .with_elite_bias(0.7)
654 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
655
656 let result = run_brkga_nesting(
657 geometries,
658 boundary,
659 &self.config,
660 brkga_config,
661 self.cancelled.clone(),
662 );
663
664 Ok(self.not_worse_than_blf(result, geometries, boundary))
665 }
666
667 fn simulated_annealing(
672 &self,
673 geometries: &[Geometry2D],
674 boundary: &Boundary2D,
675 ) -> Result<SolveResult<f64>> {
676 let time_limit_ms = if self.config.time_limit_ms > 0 {
679 (self.config.time_limit_ms / 4)
684 .max(5000)
685 .min(self.config.time_limit_ms)
686 } else {
687 10000 };
689
690 let sa_config = SaConfig::default()
691 .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));
697
698 let result = run_sa_nesting(
699 geometries,
700 boundary,
701 &self.config,
702 sa_config,
703 self.cancelled.clone(),
704 );
705
706 Ok(self.not_worse_than_blf(result, geometries, boundary))
707 }
708
709 fn gdrr(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
711 let time_limit = if self.config.time_limit_ms > 0 {
714 (self.config.time_limit_ms / 4)
719 .max(5000)
720 .min(self.config.time_limit_ms)
721 } else {
722 10000 };
724 let gdrr_config = GdrrConfig::default()
725 .with_max_iterations(1000) .with_time_limit_ms(time_limit)
727 .with_ruin_ratio(0.1, 0.3) .with_lahc_list_length(30); let result = run_gdrr_nesting(
731 geometries,
732 boundary,
733 &self.config,
734 &gdrr_config,
735 self.cancelled.clone(),
736 );
737
738 Ok(result)
739 }
740
741 fn alns(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
743 let time_limit = if self.config.time_limit_ms > 0 {
746 (self.config.time_limit_ms / 4)
751 .max(5000)
752 .min(self.config.time_limit_ms)
753 } else {
754 10000 };
756 let alns_config = AlnsConfig::default()
757 .with_max_iterations(1000) .with_time_limit_ms(time_limit)
759 .with_segment_size(50) .with_scores(33.0, 9.0, 13.0)
761 .with_reaction_factor(0.15) .with_temperature(100.0, 0.999, 0.1); let result = run_alns_nesting(
765 geometries,
766 boundary,
767 &self.config,
768 &alns_config,
769 self.cancelled.clone(),
770 );
771
772 Ok(result)
773 }
774
775 #[cfg(feature = "milp")]
788 fn milp_exact(
789 &self,
790 geometries: &[Geometry2D],
791 boundary: &Boundary2D,
792 ) -> Result<SolveResult<f64>> {
793 let exact_config = ExactConfig::default()
794 .with_time_limit_ms(self.config.time_limit_ms.max(60000))
795 .with_max_items(15)
796 .with_rotation_steps(4)
797 .with_grid_step(1.0);
798
799 let result = run_nfp_cm_nesting(
800 geometries,
801 boundary,
802 &self.config,
803 &exact_config,
804 self.cancelled.clone(),
805 );
806
807 Ok(result)
808 }
809
810 #[cfg(feature = "milp")]
812 fn hybrid_exact(
813 &self,
814 geometries: &[Geometry2D],
815 boundary: &Boundary2D,
816 ) -> Result<SolveResult<f64>> {
817 let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
819
820 if total_instances <= 15 {
822 let exact_config = ExactConfig::default()
823 .with_time_limit_ms((self.config.time_limit_ms / 2).max(30000))
824 .with_max_items(15);
825
826 let exact_result = run_nfp_cm_nesting(
827 geometries,
828 boundary,
829 &self.config,
830 &exact_config,
831 self.cancelled.clone(),
832 );
833
834 if !exact_result.placements.is_empty() {
836 return Ok(exact_result);
837 }
838 }
839
840 self.alns(geometries, boundary)
842 }
843
844 fn bottom_left_fill_with_progress(
849 &self,
850 geometries: &[Geometry2D],
851 boundary: &Boundary2D,
852 callback: &ProgressCallback,
853 ) -> Result<SolveResult<f64>> {
854 let start = Timer::now();
855 let mut result = SolveResult::new();
856 let mut placements = Vec::new();
857
858 let (b_min, b_max) = boundary.aabb();
860 let margin = self.config.margin;
861 let spacing = self.config.spacing;
862
863 let bound_min_x = b_min[0] + margin;
864 let bound_min_y = b_min[1] + margin;
865 let bound_max_x = b_max[0] - margin;
866 let bound_max_y = b_max[1] - margin;
867
868 let strip_width = bound_max_x - bound_min_x;
869 let strip_height = bound_max_y - bound_min_y;
870
871 let mut current_x = bound_min_x;
872 let mut current_y = bound_min_y;
873 let mut row_height = 0.0_f64;
874 let mut total_placed_area = 0.0;
875
876 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
878 let mut placed_count = 0usize;
879
880 callback(
882 ProgressInfo::new()
883 .with_phase("BLF Placement")
884 .with_items(0, total_pieces)
885 .with_elapsed(0),
886 );
887
888 for geom in geometries {
889 geom.validate()?;
890
891 let rotations = geom.rotations();
892 let rotation_angles: Vec<f64> = if rotations.is_empty() {
893 vec![0.0]
894 } else {
895 rotations
896 };
897
898 for instance in 0..geom.quantity() {
899 if self.cancelled.load(Ordering::Relaxed) {
900 result.computation_time_ms = start.elapsed_ms();
901 callback(
902 ProgressInfo::new()
903 .with_phase("Cancelled")
904 .with_items(placed_count, total_pieces)
905 .with_elapsed(result.computation_time_ms)
906 .finished(),
907 );
908 return Ok(result);
909 }
910
911 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
913 {
914 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
915 result.utilization = total_placed_area / boundary.measure();
916 result.computation_time_ms = start.elapsed_ms();
917 result.placements = placements;
918 callback(
919 ProgressInfo::new()
920 .with_phase("Time Limit Reached")
921 .with_items(placed_count, total_pieces)
922 .with_elapsed(result.computation_time_ms)
923 .finished(),
924 );
925 return Ok(result);
926 }
927
928 let mut best_fit: Option<(f64, f64, f64, f64, f64, [f64; 2])> = None;
929
930 for &rotation in &rotation_angles {
931 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
932 let g_width = g_max[0] - g_min[0];
933 let g_height = g_max[1] - g_min[1];
934
935 if g_width > strip_width || g_height > strip_height {
936 continue;
937 }
938
939 let mut place_x = current_x;
940 let mut place_y = current_y;
941
942 if place_x + g_width > bound_max_x {
943 place_x = bound_min_x;
944 place_y += row_height + spacing;
945 }
946
947 if place_y + g_height > bound_max_y {
948 continue;
949 }
950
951 let score = if place_x == bound_min_x && place_y > current_y {
952 place_y - bound_min_y + g_height
953 } else {
954 place_x - bound_min_x + g_width
955 };
956
957 let is_better = match &best_fit {
958 None => true,
959 Some((_, _, _, bx, by, _)) => {
960 let best_score = if *bx == bound_min_x && *by > current_y {
961 by - bound_min_y
962 } else {
963 bx - bound_min_x
964 };
965 score < best_score - 1e-6
966 }
967 };
968
969 if is_better {
970 best_fit = Some((rotation, g_width, g_height, place_x, place_y, g_min));
971 }
972 }
973
974 if let Some((rotation, g_width, g_height, place_x, place_y, g_min)) = best_fit {
975 if place_x == bound_min_x && place_y > current_y {
976 row_height = 0.0;
977 }
978
979 let origin_x = place_x - g_min[0];
981 let origin_y = place_y - g_min[1];
982
983 let geom_aabb = geom.aabb_at_rotation(rotation);
985 let boundary_aabb = (b_min, b_max);
986
987 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
988 origin_x,
989 origin_y,
990 geom_aabb,
991 boundary_aabb,
992 margin,
993 ) {
994 let placement = Placement::new_2d(
995 geom.id().clone(),
996 instance,
997 clamped_x,
998 clamped_y,
999 rotation,
1000 );
1001
1002 placements.push(placement);
1003 total_placed_area += geom.measure();
1004 placed_count += 1;
1005
1006 current_x = place_x + g_width + spacing;
1007 current_y = place_y;
1008 row_height = row_height.max(g_height);
1009
1010 callback(
1012 ProgressInfo::new()
1013 .with_phase("BLF Placement")
1014 .with_items(placed_count, total_pieces)
1015 .with_utilization(total_placed_area / boundary.measure())
1016 .with_elapsed(start.elapsed_ms()),
1017 );
1018 } else {
1019 result.unplaced.push(geom.id().clone());
1020 }
1021 } else {
1022 result.unplaced.push(geom.id().clone());
1023 }
1024 }
1025 }
1026
1027 result.placements = placements;
1028 result.boundaries_used = 1;
1029 result.utilization = total_placed_area / boundary.measure();
1030 result.computation_time_ms = start.elapsed_ms();
1031
1032 callback(
1034 ProgressInfo::new()
1035 .with_phase("Complete")
1036 .with_items(placed_count, total_pieces)
1037 .with_utilization(result.utilization)
1038 .with_elapsed(result.computation_time_ms)
1039 .finished(),
1040 );
1041
1042 Ok(result)
1043 }
1044
1045 fn nfp_guided_blf_with_progress(
1047 &self,
1048 geometries: &[Geometry2D],
1049 boundary: &Boundary2D,
1050 callback: &ProgressCallback,
1051 ) -> Result<SolveResult<f64>> {
1052 let start = Timer::now();
1053 let mut result = SolveResult::new();
1054 let mut placements = Vec::new();
1055 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
1056
1057 let margin = self.config.margin;
1058 let spacing = self.config.spacing;
1059 let boundary_polygon = self.get_boundary_polygon_with_margin(boundary, margin);
1060
1061 let mut total_placed_area = 0.0;
1062 let sample_step = self.compute_sample_step(geometries);
1063
1064 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
1066 let mut placed_count = 0usize;
1067
1068 callback(
1070 ProgressInfo::new()
1071 .with_phase("NFP Placement")
1072 .with_items(0, total_pieces)
1073 .with_elapsed(0),
1074 );
1075
1076 for geom in geometries {
1077 geom.validate()?;
1078
1079 let rotations = geom.rotations();
1080 let rotation_angles: Vec<f64> = if rotations.is_empty() {
1081 vec![0.0]
1082 } else {
1083 rotations
1084 };
1085
1086 let mirror_candidates = mirror_candidates(geom);
1087
1088 for instance in 0..geom.quantity() {
1089 if self.cancelled.load(Ordering::Relaxed) {
1090 result.computation_time_ms = start.elapsed_ms();
1091 callback(
1092 ProgressInfo::new()
1093 .with_phase("Cancelled")
1094 .with_items(placed_count, total_pieces)
1095 .with_elapsed(result.computation_time_ms)
1096 .finished(),
1097 );
1098 return Ok(result);
1099 }
1100
1101 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
1103 {
1104 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
1105 result.utilization = total_placed_area / boundary.measure();
1106 result.computation_time_ms = start.elapsed_ms();
1107 result.placements = placements;
1108 callback(
1109 ProgressInfo::new()
1110 .with_phase("Time Limit Reached")
1111 .with_items(placed_count, total_pieces)
1112 .with_elapsed(result.computation_time_ms)
1113 .finished(),
1114 );
1115 return Ok(result);
1116 }
1117
1118 let mut best_placement: Option<(f64, f64, f64, bool)> = None;
1119
1120 for &rotation in &rotation_angles {
1121 for &mirror in mirror_candidates {
1122 let ifp = match compute_ifp_with_margin_and_mirror(
1123 &boundary_polygon,
1124 geom,
1125 rotation,
1126 margin,
1127 mirror,
1128 ) {
1129 Ok(ifp) => ifp,
1130 Err(_) => continue,
1131 };
1132
1133 if ifp.is_empty() {
1134 continue;
1135 }
1136
1137 let mut nfps: Vec<Nfp> = Vec::new();
1138 for placed in &placed_geometries {
1139 let cache_key = (
1141 placed.geometry.id().as_str(),
1142 geom.id().as_str(),
1143 rotation - placed.rotation,
1144 placed.mirrored,
1145 mirror,
1146 );
1147
1148 let nfp_at_origin =
1151 match self.nfp_cache.get_or_compute_mirrored(cache_key, || {
1152 let placed_at_origin = placed.geometry.clone();
1153 compute_nfp_mirrored(
1154 &placed_at_origin,
1155 geom,
1156 rotation - placed.rotation,
1157 placed.mirrored,
1158 mirror,
1159 )
1160 }) {
1161 Ok(nfp) => nfp,
1162 Err(_) => continue,
1163 };
1164
1165 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
1167 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
1168 let expanded = self.expand_nfp(&translated_nfp, spacing);
1169 nfps.push(expanded);
1170 }
1171
1172 let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
1173 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
1174
1175 if let Some((x, y)) =
1176 find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
1177 {
1178 let is_better = match best_placement {
1179 None => true,
1180 Some((best_x, best_y, _, _)) => {
1181 x < best_x - 1e-6 || (x < best_x + 1e-6 && y < best_y - 1e-6)
1182 }
1183 };
1184 if is_better {
1185 best_placement = Some((x, y, rotation, mirror));
1186 }
1187 }
1188 }
1189 }
1190
1191 if let Some((x, y, rotation, mirror)) = best_placement {
1192 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
1195 let boundary_aabb = boundary.aabb();
1196
1197 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
1198 x,
1199 y,
1200 geom_aabb,
1201 boundary_aabb,
1202 margin,
1203 ) {
1204 let placement = Placement::new_2d(
1205 geom.id().clone(),
1206 instance,
1207 clamped_x,
1208 clamped_y,
1209 rotation,
1210 )
1211 .with_mirrored(mirror);
1212 placements.push(placement);
1213 placed_geometries.push(
1214 PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation)
1215 .with_mirrored(mirror),
1216 );
1217 total_placed_area += geom.measure();
1218 placed_count += 1;
1219
1220 callback(
1222 ProgressInfo::new()
1223 .with_phase("NFP Placement")
1224 .with_items(placed_count, total_pieces)
1225 .with_utilization(total_placed_area / boundary.measure())
1226 .with_elapsed(start.elapsed_ms()),
1227 );
1228 } else {
1229 result.unplaced.push(geom.id().clone());
1230 }
1231 } else {
1232 result.unplaced.push(geom.id().clone());
1233 }
1234 }
1235 }
1236
1237 result.placements = placements;
1238 result.boundaries_used = 1;
1239 result.utilization = total_placed_area / boundary.measure();
1240 result.computation_time_ms = start.elapsed_ms();
1241
1242 callback(
1244 ProgressInfo::new()
1245 .with_phase("Complete")
1246 .with_items(placed_count, total_pieces)
1247 .with_utilization(result.utilization)
1248 .with_elapsed(result.computation_time_ms)
1249 .finished(),
1250 );
1251
1252 Ok(result)
1253 }
1254
1255 fn validate_geometries(&self, geometries: &[Geometry2D]) -> Result<()> {
1266 use u_nesting_core::geometry::Geometry;
1267 for geom in geometries {
1268 geom.validate()?;
1269 }
1270 Ok(())
1271 }
1272
1273 pub fn solve_multi_strip(
1275 &self,
1276 geometries: &[Geometry2D],
1277 boundary: &Boundary2D,
1278 ) -> Result<SolveResult<f64>> {
1279 boundary.validate()?;
1280 self.validate_geometries(geometries)?;
1281 self.cancelled.store(false, Ordering::Relaxed);
1282
1283 let (b_min, b_max) = boundary.aabb();
1284 let strip_width = b_max[0] - b_min[0];
1285
1286 let mut final_result = SolveResult::new();
1287 let mut remaining_geometries: Vec<Geometry2D> = geometries.to_vec();
1288 let mut strip_index = 0;
1289 let max_strips = 100; let mut placed_total: std::collections::HashMap<String, usize> =
1294 std::collections::HashMap::new();
1295
1296 while !remaining_geometries.is_empty() && strip_index < max_strips {
1297 if self.cancelled.load(Ordering::Relaxed) {
1298 break;
1299 }
1300
1301 let strip_result = match self.config.strategy {
1303 Strategy::BottomLeftFill => self.bottom_left_fill(&remaining_geometries, boundary),
1304 Strategy::NfpGuided => self.nfp_guided_blf(&remaining_geometries, boundary),
1305 Strategy::GeneticAlgorithm => {
1306 self.genetic_algorithm(&remaining_geometries, boundary)
1307 }
1308 Strategy::Brkga => self.brkga(&remaining_geometries, boundary),
1309 Strategy::SimulatedAnnealing => {
1310 self.simulated_annealing(&remaining_geometries, boundary)
1311 }
1312 Strategy::Gdrr => self.gdrr(&remaining_geometries, boundary),
1313 Strategy::Alns => self.alns(&remaining_geometries, boundary),
1314 #[cfg(feature = "milp")]
1315 Strategy::MilpExact => self.milp_exact(&remaining_geometries, boundary),
1316 #[cfg(feature = "milp")]
1317 Strategy::HybridExact => self.hybrid_exact(&remaining_geometries, boundary),
1318 _ => self.nfp_guided_blf(&remaining_geometries, boundary),
1319 }?;
1320
1321 let strip_result =
1323 validate_and_filter_placements(strip_result, &remaining_geometries, boundary);
1324
1325 if strip_result.placements.is_empty() {
1326 break;
1329 }
1330
1331 let mut strip_placed: std::collections::HashMap<String, usize> =
1334 std::collections::HashMap::new();
1335
1336 for mut placement in strip_result.placements {
1338 let gid = placement.geometry_id.clone();
1339 let prior = placed_total.get(&gid).copied().unwrap_or(0);
1342 let in_strip = strip_placed.get(&gid).copied().unwrap_or(0);
1343 placement.instance = prior + in_strip;
1344 if !placement.position.is_empty() {
1346 placement.position[0] += strip_index as f64 * strip_width;
1347 }
1348 placement.boundary_index = strip_index;
1349 *strip_placed.entry(gid).or_insert(0) += 1;
1350 final_result.placements.push(placement);
1351 }
1352
1353 for (gid, cnt) in &strip_placed {
1357 *placed_total.entry(gid.clone()).or_insert(0) += cnt;
1358 }
1359 remaining_geometries = remaining_geometries
1360 .into_iter()
1361 .filter_map(|g| {
1362 let placed_here = strip_placed.get(g.id()).copied().unwrap_or(0);
1363 let new_quantity = g.quantity().saturating_sub(placed_here);
1364 if new_quantity == 0 {
1365 None
1366 } else {
1367 Some(g.with_quantity(new_quantity))
1368 }
1369 })
1370 .collect();
1371
1372 strip_index += 1;
1373 }
1374
1375 for g in &remaining_geometries {
1378 final_result.unplaced.push(g.id().clone());
1379 }
1380
1381 final_result.boundaries_used = strip_index;
1382 final_result.deduplicate_unplaced();
1383 final_result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1385
1386 let (b_min, b_max) = boundary.aabb();
1388 let strip_height = b_max[1] - b_min[1]; let mut strip_stats_map: std::collections::HashMap<usize, (f64, f64, usize)> =
1392 std::collections::HashMap::new(); for placement in &final_result.placements {
1395 let strip_idx = placement.boundary_index;
1396 if let Some(geom) = geometries.iter().find(|g| g.id() == &placement.geometry_id) {
1398 use u_nesting_core::geometry::Geometry;
1399 let piece_area = geom.measure();
1400 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1401 let (_g_min, g_max) = geom.aabb_at_rotation(rotation);
1402 let local_x = placement.position[0] - (strip_idx as f64 * strip_width);
1405 let right_edge = local_x + g_max[0];
1406
1407 let entry = strip_stats_map.entry(strip_idx).or_insert((0.0, 0.0, 0));
1408 entry.0 = entry.0.max(right_edge); entry.1 += piece_area; entry.2 += 1; }
1412 }
1413
1414 use u_nesting_core::result::StripStats;
1416 let mut strip_stats: Vec<StripStats> = strip_stats_map
1417 .into_iter()
1418 .map(|(idx, (used_length, piece_area, count))| StripStats {
1419 strip_index: idx,
1420 used_length,
1421 piece_area,
1422 piece_count: count,
1423 strip_width, strip_height, })
1426 .collect();
1427 strip_stats.sort_by_key(|s| s.strip_index);
1428
1429 let total_piece_area: f64 = strip_stats.iter().map(|s| s.piece_area).sum();
1432 let total_material_used: f64 = strip_stats
1433 .iter()
1434 .map(|s| s.strip_height * s.used_length)
1435 .sum();
1436
1437 final_result.strip_stats = strip_stats;
1438 final_result.total_piece_area = total_piece_area;
1439 final_result.total_material_used = total_material_used;
1440
1441 if total_material_used > 0.0 {
1442 final_result.utilization = total_piece_area / total_material_used;
1443 }
1444
1445 Ok(final_result)
1446 }
1447}
1448
1449impl Solver for Nester2D {
1450 type Geometry = Geometry2D;
1451 type Boundary = Boundary2D;
1452 type Scalar = f64;
1453
1454 fn solve(
1455 &self,
1456 geometries: &[Self::Geometry],
1457 boundary: &Self::Boundary,
1458 ) -> Result<SolveResult<f64>> {
1459 boundary.validate()?;
1460 self.validate_geometries(geometries)?;
1461
1462 self.cancelled.store(false, Ordering::Relaxed);
1464
1465 let initial_result = match self.config.strategy {
1466 Strategy::BottomLeftFill => self.bottom_left_fill(geometries, boundary),
1467 Strategy::NfpGuided => self.nfp_guided_blf(geometries, boundary),
1468 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
1469 Strategy::Brkga => self.brkga(geometries, boundary),
1470 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
1471 Strategy::Gdrr => self.gdrr(geometries, boundary),
1472 Strategy::Alns => self.alns(geometries, boundary),
1473 #[cfg(feature = "milp")]
1474 Strategy::MilpExact => self.milp_exact(geometries, boundary),
1475 #[cfg(feature = "milp")]
1476 Strategy::HybridExact => self.hybrid_exact(geometries, boundary),
1477 _ => {
1478 log::warn!(
1480 "Strategy {:?} not yet implemented, using NfpGuided",
1481 self.config.strategy
1482 );
1483 self.nfp_guided_blf(geometries, boundary)
1484 }
1485 }?;
1486
1487 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1489
1490 result.deduplicate_unplaced();
1492 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1495 Ok(result)
1496 }
1497
1498 fn solve_with_progress(
1499 &self,
1500 geometries: &[Self::Geometry],
1501 boundary: &Self::Boundary,
1502 callback: ProgressCallback,
1503 ) -> Result<SolveResult<f64>> {
1504 boundary.validate()?;
1505 self.validate_geometries(geometries)?;
1506
1507 self.cancelled.store(false, Ordering::Relaxed);
1509
1510 let initial_result = match self.config.strategy {
1511 Strategy::BottomLeftFill => {
1512 self.bottom_left_fill_with_progress(geometries, boundary, &callback)?
1513 }
1514 Strategy::NfpGuided => {
1515 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1516 }
1517 Strategy::GeneticAlgorithm => {
1518 let mut ga_config = GaConfig::default()
1524 .with_population_size(self.config.population_size.min(30))
1525 .with_max_generations(self.config.max_generations.min(50))
1526 .with_crossover_rate(self.config.crossover_rate)
1527 .with_mutation_rate(self.config.mutation_rate);
1528
1529 if self.config.time_limit_ms > 0 {
1531 ga_config = ga_config.with_time_limit(std::time::Duration::from_millis(
1532 self.config.time_limit_ms,
1533 ));
1534 }
1535
1536 let ga_result = run_ga_nesting_with_progress(
1537 geometries,
1538 boundary,
1539 &self.config,
1540 ga_config,
1541 self.cancelled.clone(),
1542 callback,
1543 );
1544 self.not_worse_than_blf(ga_result, geometries, boundary)
1549 }
1550 _ => {
1552 log::warn!(
1553 "Strategy {:?} not yet implemented, using NfpGuided",
1554 self.config.strategy
1555 );
1556 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1557 }
1558 };
1559
1560 let mut result = validate_and_filter_placements(initial_result, geometries, boundary);
1562
1563 result.deduplicate_unplaced();
1565 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1568 Ok(result)
1569 }
1570
1571 fn cancel(&self) {
1572 self.cancelled.store(true, Ordering::Relaxed);
1573 }
1574}
1575
1576#[cfg(test)]
1577mod tests {
1578 use super::*;
1579 use crate::placement_utils::polygon_centroid;
1580
1581 #[test]
1582 fn test_simple_nesting() {
1583 let geometries = vec![
1584 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1585 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1586 ];
1587
1588 let boundary = Boundary2D::rectangle(100.0, 50.0);
1589 let nester = Nester2D::default_config();
1590
1591 let result = nester.solve(&geometries, &boundary).unwrap();
1592
1593 assert!(result.utilization > 0.0);
1594 assert!(result.placements.len() <= 5); }
1596
1597 #[test]
1598 fn test_placement_within_bounds() {
1599 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1600
1601 let boundary = Boundary2D::rectangle(50.0, 50.0);
1602 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1603 let nester = Nester2D::new(config);
1604
1605 let result = nester.solve(&geometries, &boundary).unwrap();
1606
1607 assert_eq!(result.placements.len(), 4);
1609 assert!(result.unplaced.is_empty());
1610
1611 for p in &result.placements {
1613 assert!(p.position[0] >= 5.0);
1614 assert!(p.position[1] >= 5.0);
1615 }
1616 }
1617
1618 #[test]
1619 fn test_nfp_guided_basic() {
1620 let geometries = vec![
1621 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1622 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1623 ];
1624
1625 let boundary = Boundary2D::rectangle(100.0, 50.0);
1626 let config = Config::default().with_strategy(Strategy::NfpGuided);
1627 let nester = Nester2D::new(config);
1628
1629 let result = nester.solve(&geometries, &boundary).unwrap();
1630
1631 assert!(result.utilization > 0.0);
1632 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1634 }
1635
1636 #[test]
1637 fn test_nfp_guided_with_spacing() {
1638 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1639
1640 let boundary = Boundary2D::rectangle(50.0, 50.0);
1641 let config = Config::default()
1642 .with_strategy(Strategy::NfpGuided)
1643 .with_margin(2.0)
1644 .with_spacing(3.0);
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 assert!(result.utilization > 0.0);
1655 }
1656
1657 #[test]
1658 fn test_nfp_guided_no_overlap() {
1659 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1660
1661 let boundary = Boundary2D::rectangle(100.0, 100.0);
1662 let config = Config::default().with_strategy(Strategy::NfpGuided);
1663 let nester = Nester2D::new(config);
1664
1665 let result = nester.solve(&geometries, &boundary).unwrap();
1666
1667 assert_eq!(result.placements.len(), 3);
1668
1669 for i in 0..result.placements.len() {
1671 for j in (i + 1)..result.placements.len() {
1672 let p1 = &result.placements[i];
1673 let p2 = &result.placements[j];
1674
1675 let r1_min_x = p1.position[0];
1677 let r1_max_x = p1.position[0] + 20.0;
1678 let r1_min_y = p1.position[1];
1679 let r1_max_y = p1.position[1] + 20.0;
1680
1681 let r2_min_x = p2.position[0];
1682 let r2_max_x = p2.position[0] + 20.0;
1683 let r2_min_y = p2.position[1];
1684 let r2_max_y = p2.position[1] + 20.0;
1685
1686 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1688 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1689
1690 assert!(
1691 !(overlaps_x && overlaps_y),
1692 "Placements {} and {} overlap",
1693 i,
1694 j
1695 );
1696 }
1697 }
1698 }
1699
1700 fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
1705 for i in 0..a.len() {
1706 let a1 = a[i];
1707 let a2 = a[(i + 1) % a.len()];
1708 for j in 0..b.len() {
1709 let b1 = b[j];
1710 let b2 = b[(j + 1) % b.len()];
1711 if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
1712 return true;
1713 }
1714 }
1715 }
1716 false
1717 }
1718
1719 #[test]
1720 fn test_mirror_candidates_reflects_allow_flip() {
1721 let plain = Geometry2D::rectangle("R", 10.0, 10.0);
1722 assert_eq!(mirror_candidates(&plain), &[false]);
1723
1724 let flippable = Geometry2D::rectangle("R", 10.0, 10.0).with_flip(true);
1725 assert_eq!(mirror_candidates(&flippable), &[false, true]);
1726 }
1727
1728 #[test]
1748 fn test_mirror_aware_placement_avoids_overlap() {
1749 let spacing = 1.0;
1757 let geom = Geometry2D::l_shape("L", 30.0, 20.0, 20.0, 10.0);
1758 let boundary_polygon = vec![(0.0, 0.0), (65.0, 0.0), (65.0, 45.0), (0.0, 45.0)];
1759 let cache = NfpCache::new();
1760
1761 let ifp1 =
1763 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, false).unwrap();
1764 let ifp1_shrunk = shrink_ifp(&ifp1, spacing);
1765 let (x1, y1) =
1766 find_bottom_left_placement(&ifp1_shrunk, &[], 1.0).expect("first piece must fit");
1767 let placed1 = PlacedGeometry::new(geom.clone(), (x1, y1), 0.0).with_mirrored(false);
1768
1769 let ifp2 =
1771 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, true).unwrap();
1772 let ifp2_shrunk = shrink_ifp(&ifp2, spacing);
1773 let nfp_at_origin = cache
1774 .get_or_compute_mirrored(("L", "L", 0.0, false, true), || {
1775 compute_nfp_mirrored(&placed1.geometry, &geom, 0.0, false, true)
1776 })
1777 .unwrap();
1778 let translated_nfp = translate_nfp(&nfp_at_origin, placed1.position);
1779 let expanded_nfp = expand_nfp(&translated_nfp, spacing);
1780 let (x2, y2) = find_bottom_left_placement(&ifp2_shrunk, &[&expanded_nfp], 1.0)
1781 .expect("mirrored second piece must fit avoiding the first");
1782 let placed2 = PlacedGeometry::new(geom.clone(), (x2, y2), 0.0).with_mirrored(true);
1783
1784 assert_ne!(
1785 (x1, y1),
1786 (x2, y2),
1787 "mirrored placement must actually avoid the first piece's position"
1788 );
1789 assert!(
1790 !polygons_overlap(
1791 &placed1.translated_exterior(),
1792 &placed2.translated_exterior()
1793 ),
1794 "unmirrored piece at {:?} and mirrored piece at {:?} must not overlap",
1795 (x1, y1),
1796 (x2, y2)
1797 );
1798 }
1799
1800 #[test]
1801 fn test_nfp_guided_utilization() {
1802 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
1804
1805 let boundary = Boundary2D::rectangle(100.0, 50.0);
1806 let config = Config::default().with_strategy(Strategy::NfpGuided);
1807 let nester = Nester2D::new(config);
1808
1809 let result = nester.solve(&geometries, &boundary).unwrap();
1810
1811 assert_eq!(result.placements.len(), 4);
1813
1814 assert!(result.utilization > 0.45);
1816 }
1817
1818 #[test]
1819 fn test_polygon_centroid() {
1820 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1822 let (cx, cy) = polygon_centroid(&square);
1823 assert!((cx - 5.0).abs() < 0.01);
1824 assert!((cy - 5.0).abs() < 0.01);
1825
1826 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
1827 let (cx, cy) = polygon_centroid(&triangle);
1828 assert!((cx - 3.0).abs() < 0.01);
1829 assert!((cy - 2.0).abs() < 0.01);
1830 }
1831
1832 #[test]
1833 fn test_ga_strategy_basic() {
1834 let geometries = vec![
1835 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1836 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1837 ];
1838
1839 let boundary = Boundary2D::rectangle(100.0, 50.0);
1840 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1841 let nester = Nester2D::new(config);
1842
1843 let result = nester.solve(&geometries, &boundary).unwrap();
1844
1845 assert!(result.utilization > 0.0);
1846 assert!(!result.placements.is_empty());
1847 assert!(result.generations.is_some());
1849 assert!(result.best_fitness.is_some());
1850 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
1851 }
1852
1853 #[test]
1854 fn test_ga_strategy_all_placed() {
1855 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1857
1858 let boundary = Boundary2D::rectangle(100.0, 100.0);
1859 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1860 let nester = Nester2D::new(config);
1861
1862 let result = nester.solve(&geometries, &boundary).unwrap();
1863
1864 assert_eq!(result.placements.len(), 4);
1866 assert!(result.unplaced.is_empty());
1867 }
1868
1869 #[test]
1870 fn test_brkga_strategy_basic() {
1871 let geometries = vec![
1872 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1873 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1874 ];
1875
1876 let boundary = Boundary2D::rectangle(100.0, 50.0);
1877 let config = Config::default().with_strategy(Strategy::Brkga);
1878 let nester = Nester2D::new(config);
1879
1880 let result = nester.solve(&geometries, &boundary).unwrap();
1881
1882 assert!(result.utilization > 0.0);
1883 assert!(!result.placements.is_empty());
1884 assert!(result.generations.is_some());
1886 assert!(result.best_fitness.is_some());
1887 assert!(result.strategy == Some("BRKGA".to_string()));
1888 }
1889
1890 #[test]
1891 fn test_brkga_strategy_all_placed() {
1892 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1894
1895 let boundary = Boundary2D::rectangle(100.0, 100.0);
1896 let config = Config::default()
1898 .with_strategy(Strategy::Brkga)
1899 .with_time_limit(30000); let nester = Nester2D::new(config);
1901
1902 let result = nester.solve(&geometries, &boundary).unwrap();
1903
1904 assert!(
1907 result.placements.len() >= 3,
1908 "Expected at least 3 placements, got {}",
1909 result.placements.len()
1910 );
1911 }
1912
1913 #[test]
1914 fn test_gdrr_strategy_basic() {
1915 let geometries = vec![
1916 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1917 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1918 ];
1919
1920 let boundary = Boundary2D::rectangle(100.0, 50.0);
1921 let config = Config::default().with_strategy(Strategy::Gdrr);
1922 let nester = Nester2D::new(config);
1923
1924 let result = nester.solve(&geometries, &boundary).unwrap();
1925
1926 assert!(result.utilization > 0.0);
1927 assert!(!result.placements.is_empty());
1928 assert!(result.iterations.is_some());
1930 assert!(result.best_fitness.is_some());
1931 assert!(result.strategy == Some("GDRR".to_string()));
1932 }
1933
1934 #[test]
1935 fn test_gdrr_strategy_all_placed() {
1936 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1938
1939 let boundary = Boundary2D::rectangle(100.0, 100.0);
1940 let config = Config::default().with_strategy(Strategy::Gdrr);
1941 let nester = Nester2D::new(config);
1942
1943 let result = nester.solve(&geometries, &boundary).unwrap();
1944
1945 assert_eq!(result.placements.len(), 4);
1947 assert!(result.unplaced.is_empty());
1948 }
1949
1950 #[test]
1951 fn test_alns_strategy_basic() {
1952 let geometries = vec![
1953 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1954 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1955 ];
1956
1957 let boundary = Boundary2D::rectangle(100.0, 50.0);
1958 let config = Config::default().with_strategy(Strategy::Alns);
1959 let nester = Nester2D::new(config);
1960
1961 let result = nester.solve(&geometries, &boundary).unwrap();
1962
1963 assert!(result.utilization > 0.0);
1964 assert!(!result.placements.is_empty());
1965 assert!(result.iterations.is_some());
1967 assert!(result.best_fitness.is_some());
1968 assert!(result.strategy == Some("ALNS".to_string()));
1969 }
1970
1971 #[test]
1972 fn test_alns_strategy_all_placed() {
1973 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1975
1976 let boundary = Boundary2D::rectangle(100.0, 100.0);
1977 let config = Config::default().with_strategy(Strategy::Alns);
1978 let nester = Nester2D::new(config);
1979
1980 let result = nester.solve(&geometries, &boundary).unwrap();
1981
1982 assert_eq!(result.placements.len(), 4);
1984 assert!(result.unplaced.is_empty());
1985 }
1986
1987 #[test]
1988 fn test_blf_rotation_optimization() {
1989 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
1992 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
1994
1995 let boundary = Boundary2D::rectangle(35.0, 95.0);
1998 let nester = Nester2D::default_config();
1999
2000 let result = nester.solve(&geometries, &boundary).unwrap();
2001
2002 assert_eq!(
2004 result.placements.len(),
2005 3,
2006 "All pieces should be placed with rotation optimization"
2007 );
2008 assert!(result.unplaced.is_empty());
2009 }
2010
2011 #[test]
2012 fn test_blf_selects_best_rotation() {
2013 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
2015 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
2017
2018 let boundary = Boundary2D::rectangle(45.0, 50.0);
2022 let nester = Nester2D::default_config();
2023
2024 let result = nester.solve(&geometries, &boundary).unwrap();
2025
2026 assert_eq!(result.placements.len(), 2);
2027 assert!(result.unplaced.is_empty());
2028 }
2029
2030 #[test]
2031 fn test_progress_callback_blf() {
2032 use std::sync::atomic::{AtomicUsize, Ordering};
2033 use std::sync::Arc;
2034
2035 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2036 let boundary = Boundary2D::rectangle(50.0, 50.0);
2037 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2038 let nester = Nester2D::new(config);
2039
2040 let callback_count = Arc::new(AtomicUsize::new(0));
2041 let callback_count_clone = callback_count.clone();
2042 let last_items_placed = Arc::new(AtomicUsize::new(0));
2043 let last_items_placed_clone = last_items_placed.clone();
2044
2045 let callback: ProgressCallback = Box::new(move |info| {
2046 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2047 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
2048 });
2049
2050 let result = nester
2051 .solve_with_progress(&geometries, &boundary, callback)
2052 .unwrap();
2053
2054 let count = callback_count.load(Ordering::Relaxed);
2056 assert!(
2057 count >= 5,
2058 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
2059 count
2060 );
2061
2062 let final_placed = last_items_placed.load(Ordering::Relaxed);
2064 assert_eq!(final_placed, 4, "Should report 4 items placed");
2065
2066 assert_eq!(result.placements.len(), 4);
2068 }
2069
2070 #[test]
2071 fn test_progress_callback_nfp() {
2072 use std::sync::atomic::{AtomicUsize, Ordering};
2073 use std::sync::Arc;
2074
2075 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
2076 let boundary = Boundary2D::rectangle(50.0, 50.0);
2077 let config = Config::default().with_strategy(Strategy::NfpGuided);
2078 let nester = Nester2D::new(config);
2079
2080 let callback_count = Arc::new(AtomicUsize::new(0));
2081 let callback_count_clone = callback_count.clone();
2082
2083 let callback: ProgressCallback = Box::new(move |info| {
2084 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2085 assert!(info.items_placed <= info.total_items);
2086 });
2087
2088 let result = nester
2089 .solve_with_progress(&geometries, &boundary, callback)
2090 .unwrap();
2091
2092 let count = callback_count.load(Ordering::Relaxed);
2094 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
2095
2096 assert_eq!(result.placements.len(), 2);
2098 }
2099
2100 #[test]
2101 fn test_time_limit_honored() {
2102 let geometries: Vec<Geometry2D> = (0..100)
2104 .map(|i| Geometry2D::rectangle(format!("R{}", i), 5.0, 5.0))
2105 .collect();
2106 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
2107
2108 let config = Config::default()
2110 .with_strategy(Strategy::BottomLeftFill)
2111 .with_time_limit(1);
2112 let nester = Nester2D::new(config);
2113
2114 let result = nester.solve(&geometries, &boundary).unwrap();
2115
2116 assert!(
2119 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
2121 result.computation_time_ms
2122 );
2123 }
2124
2125 #[test]
2126 fn test_time_limit_zero_unlimited() {
2127 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2129 let boundary = Boundary2D::rectangle(50.0, 50.0);
2130
2131 let config = Config::default()
2132 .with_strategy(Strategy::BottomLeftFill)
2133 .with_time_limit(0); let nester = Nester2D::new(config);
2135
2136 let result = nester.solve(&geometries, &boundary).unwrap();
2137
2138 assert_eq!(result.placements.len(), 4);
2140 }
2141
2142 #[test]
2143 fn test_blf_bounds_clamping() {
2144 let gear_like = Geometry2D::new("gear")
2148 .with_polygon(vec![
2149 (50.0, 5.0), (65.0, 15.0),
2151 (77.0, 18.0),
2152 (80.0, 32.0),
2153 (95.0, 50.0), (80.0, 68.0),
2155 (77.0, 82.0),
2156 (65.0, 85.0),
2157 (50.0, 95.0), (35.0, 85.0),
2159 (23.0, 82.0),
2160 (20.0, 68.0),
2161 (5.0, 50.0), (20.0, 32.0),
2163 (23.0, 18.0),
2164 (35.0, 15.0),
2165 ])
2166 .with_quantity(1);
2167
2168 let boundary = Boundary2D::rectangle(100.0, 100.0);
2170
2171 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2172 let nester = Nester2D::new(config);
2173
2174 let result = nester
2175 .solve(std::slice::from_ref(&gear_like), &boundary)
2176 .unwrap();
2177
2178 assert_eq!(result.placements.len(), 1);
2179 let placement = &result.placements[0];
2180
2181 let origin_x = placement.position[0];
2183 let origin_y = placement.position[1];
2184
2185 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2187
2188 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2190
2191 let actual_min_x = origin_x + g_min[0];
2193 let actual_max_x = origin_x + g_max[0];
2194 let actual_min_y = origin_y + g_min[1];
2195 let actual_max_y = origin_y + g_max[1];
2196
2197 assert!(
2199 actual_min_x >= 0.0,
2200 "Left edge {} should be >= 0",
2201 actual_min_x
2202 );
2203 assert!(
2204 actual_max_x <= 100.0,
2205 "Right edge {} should be <= 100",
2206 actual_max_x
2207 );
2208 assert!(
2209 actual_min_y >= 0.0,
2210 "Bottom edge {} should be >= 0",
2211 actual_min_y
2212 );
2213 assert!(
2214 actual_max_y <= 100.0,
2215 "Top edge {} should be <= 100",
2216 actual_max_y
2217 );
2218 }
2219
2220 #[test]
2221 fn test_blf_bounds_clamping_many_pieces() {
2222 let gear_like = Geometry2D::new("gear")
2225 .with_polygon(vec![
2226 (50.0, 5.0),
2227 (65.0, 15.0),
2228 (77.0, 18.0),
2229 (80.0, 32.0),
2230 (95.0, 50.0),
2231 (80.0, 68.0),
2232 (77.0, 82.0),
2233 (65.0, 85.0),
2234 (50.0, 95.0),
2235 (35.0, 85.0),
2236 (23.0, 82.0),
2237 (20.0, 68.0),
2238 (5.0, 50.0),
2239 (20.0, 32.0),
2240 (23.0, 18.0),
2241 (35.0, 15.0),
2242 ])
2243 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
2247
2248 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2249 let nester = Nester2D::new(config);
2250
2251 let result = nester
2252 .solve(std::slice::from_ref(&gear_like), &boundary)
2253 .unwrap();
2254
2255 for (i, placement) in result.placements.iter().enumerate() {
2257 let origin_x = placement.position[0];
2258 let origin_y = placement.position[1];
2259 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2260
2261 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2262
2263 let actual_min_x = origin_x + g_min[0];
2264 let actual_max_x = origin_x + g_max[0];
2265 let actual_min_y = origin_y + g_min[1];
2266 let actual_max_y = origin_y + g_max[1];
2267
2268 assert!(
2269 actual_min_x >= -0.01,
2270 "Piece {}: Left edge {} should be >= 0",
2271 i,
2272 actual_min_x
2273 );
2274 assert!(
2275 actual_max_x <= 500.01,
2276 "Piece {}: Right edge {} should be <= 500",
2277 i,
2278 actual_max_x
2279 );
2280 assert!(
2281 actual_min_y >= -0.01,
2282 "Piece {}: Bottom edge {} should be >= 0",
2283 i,
2284 actual_min_y
2285 );
2286 assert!(
2287 actual_max_y <= 500.01,
2288 "Piece {}: Top edge {} should be <= 500",
2289 i,
2290 actual_max_y
2291 );
2292 }
2293 }
2294
2295 #[test]
2296 fn test_blf_bounds_trace() {
2297 let gear = Geometry2D::new("gear").with_polygon(vec![
2299 (50.0, 5.0),
2300 (65.0, 15.0),
2301 (77.0, 18.0),
2302 (80.0, 32.0),
2303 (95.0, 50.0),
2304 (80.0, 68.0),
2305 (77.0, 82.0),
2306 (65.0, 85.0),
2307 (50.0, 95.0),
2308 (35.0, 85.0),
2309 (23.0, 82.0),
2310 (20.0, 68.0),
2311 (5.0, 50.0),
2312 (20.0, 32.0),
2313 (23.0, 18.0),
2314 (35.0, 15.0),
2315 ]);
2316
2317 let (g_min, g_max) = gear.aabb();
2319 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
2320 assert!(
2321 (g_min[0] - 5.0).abs() < 0.01,
2322 "g_min[0] should be 5, got {}",
2323 g_min[0]
2324 );
2325 assert!(
2326 (g_max[0] - 95.0).abs() < 0.01,
2327 "g_max[0] should be 95, got {}",
2328 g_max[0]
2329 );
2330
2331 let b_max_x = 500.0;
2333 let margin = 0.0;
2334 let max_valid_x = b_max_x - margin - g_max[0];
2335 println!(
2336 "max_valid_x = {} - {} - {} = {}",
2337 b_max_x, margin, g_max[0], max_valid_x
2338 );
2339 assert!(
2340 (max_valid_x - 405.0).abs() < 0.01,
2341 "max_valid_x should be 405, got {}",
2342 max_valid_x
2343 );
2344
2345 let boundary = Boundary2D::rectangle(500.0, 500.0);
2347 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2348 let nester = Nester2D::new(config);
2349
2350 let result = nester
2351 .solve(&[gear.clone().with_quantity(1)], &boundary)
2352 .unwrap();
2353
2354 assert_eq!(result.placements.len(), 1);
2355 let p = &result.placements[0];
2356 let origin_x = p.position[0];
2357 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2358
2359 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2360 let actual_max_x = origin_x + g_max_r[0];
2361
2362 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2363 println!(
2364 "At rotation {}: g_min={:?}, g_max={:?}",
2365 rotation, g_min_r, g_max_r
2366 );
2367 println!(
2368 "Actual max x: {} + {} = {}",
2369 origin_x, g_max_r[0], actual_max_x
2370 );
2371
2372 assert!(
2373 actual_max_x <= 500.01,
2374 "Geometry exceeds boundary: max_x={} > 500",
2375 actual_max_x
2376 );
2377 }
2378
2379 #[test]
2380 fn test_blf_bounds_many_pieces_direct() {
2381 let gear = Geometry2D::new("gear")
2383 .with_polygon(vec![
2384 (50.0, 5.0),
2385 (65.0, 15.0),
2386 (77.0, 18.0),
2387 (80.0, 32.0),
2388 (95.0, 50.0),
2389 (80.0, 68.0),
2390 (77.0, 82.0),
2391 (65.0, 85.0),
2392 (50.0, 95.0),
2393 (35.0, 85.0),
2394 (23.0, 82.0),
2395 (20.0, 68.0),
2396 (5.0, 50.0),
2397 (20.0, 32.0),
2398 (23.0, 18.0),
2399 (35.0, 15.0),
2400 ])
2401 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2404 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2405 let nester = Nester2D::new(config);
2406
2407 let result = nester
2408 .solve(std::slice::from_ref(&gear), &boundary)
2409 .unwrap();
2410
2411 println!("Placed {} pieces", result.placements.len());
2412
2413 for (i, p) in result.placements.iter().enumerate() {
2415 let origin_x = p.position[0];
2416 let origin_y = p.position[1];
2417 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2418
2419 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2420
2421 let actual_min_x = origin_x + g_min_r[0];
2422 let actual_max_x = origin_x + g_max_r[0];
2423 let actual_min_y = origin_y + g_min_r[1];
2424 let actual_max_y = origin_y + g_max_r[1];
2425
2426 println!(
2427 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2428 i,
2429 origin_x,
2430 origin_y,
2431 rotation,
2432 actual_min_x,
2433 actual_max_x,
2434 actual_min_y,
2435 actual_max_y
2436 );
2437
2438 assert!(
2439 actual_max_x <= 500.01,
2440 "Piece {}: Right edge {} > 500",
2441 i,
2442 actual_max_x
2443 );
2444 assert!(
2445 actual_max_y <= 500.01,
2446 "Piece {}: Top edge {} > 500",
2447 i,
2448 actual_max_y
2449 );
2450 }
2451 }
2452
2453 #[test]
2454 fn test_blf_bounds_multi_strip() {
2455 let gear = Geometry2D::new("gear")
2457 .with_polygon(vec![
2458 (50.0, 5.0),
2459 (65.0, 15.0),
2460 (77.0, 18.0),
2461 (80.0, 32.0),
2462 (95.0, 50.0),
2463 (80.0, 68.0),
2464 (77.0, 82.0),
2465 (65.0, 85.0),
2466 (50.0, 95.0),
2467 (35.0, 85.0),
2468 (23.0, 82.0),
2469 (20.0, 68.0),
2470 (5.0, 50.0),
2471 (20.0, 32.0),
2472 (23.0, 18.0),
2473 (35.0, 15.0),
2474 ])
2475 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2478 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2479 let nester = Nester2D::new(config);
2480
2481 let result = nester
2483 .solve_multi_strip(std::slice::from_ref(&gear), &boundary)
2484 .unwrap();
2485
2486 println!(
2487 "Placed {} pieces across {} strips",
2488 result.placements.len(),
2489 result.boundaries_used
2490 );
2491
2492 let strip_width = 500.0;
2494 for (i, p) in result.placements.iter().enumerate() {
2495 let origin_x = p.position[0];
2496 let origin_y = p.position[1];
2497 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2498 let strip_idx = p.boundary_index;
2499
2500 let local_x = origin_x - (strip_idx as f64 * strip_width);
2502
2503 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2504
2505 let local_max_x = local_x + g_max_r[0];
2506 let local_max_y = origin_y + g_max_r[1];
2507
2508 println!(
2509 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2510 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2511 );
2512
2513 assert!(
2514 local_max_x <= 500.01,
2515 "Piece {}: In strip {}, local right edge {:.1} > 500",
2516 i,
2517 strip_idx,
2518 local_max_x
2519 );
2520 assert!(
2521 local_max_y <= 500.01,
2522 "Piece {}: Top edge {:.1} > 500",
2523 i,
2524 local_max_y
2525 );
2526 }
2527 }
2528
2529 #[test]
2530 fn test_blf_bounds_mixed_shapes() {
2531 let shapes = vec![
2533 Geometry2D::new("shape0")
2535 .with_polygon(vec![
2536 (0.0, 0.0),
2537 (180.0, 0.0),
2538 (195.0, 15.0),
2539 (200.0, 50.0),
2540 (200.0, 150.0),
2541 (195.0, 185.0),
2542 (180.0, 200.0),
2543 (20.0, 200.0),
2544 (5.0, 185.0),
2545 (0.0, 150.0),
2546 (0.0, 50.0),
2547 (5.0, 15.0),
2548 ])
2549 .with_quantity(2),
2550 Geometry2D::new("shape1")
2552 .with_polygon(vec![
2553 (60.0, 0.0),
2554 (85.0, 7.0),
2555 (104.0, 25.0),
2556 (118.0, 50.0),
2557 (120.0, 60.0),
2558 (118.0, 70.0),
2559 (104.0, 95.0),
2560 (85.0, 113.0),
2561 (60.0, 120.0),
2562 (35.0, 113.0),
2563 (16.0, 95.0),
2564 (2.0, 70.0),
2565 (0.0, 60.0),
2566 (2.0, 50.0),
2567 (16.0, 25.0),
2568 (35.0, 7.0),
2569 ])
2570 .with_quantity(4),
2571 Geometry2D::new("shape2")
2573 .with_polygon(vec![
2574 (0.0, 0.0),
2575 (80.0, 0.0),
2576 (80.0, 20.0),
2577 (20.0, 20.0),
2578 (20.0, 80.0),
2579 (0.0, 80.0),
2580 ])
2581 .with_quantity(6),
2582 Geometry2D::new("shape3")
2584 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2585 .with_quantity(6),
2586 Geometry2D::new("shape4")
2588 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2589 .with_quantity(4),
2590 Geometry2D::new("shape5")
2592 .with_polygon(vec![
2593 (15.0, 0.0),
2594 (45.0, 0.0),
2595 (60.0, 26.0),
2596 (45.0, 52.0),
2597 (15.0, 52.0),
2598 (0.0, 26.0),
2599 ])
2600 .with_quantity(8),
2601 Geometry2D::new("shape6")
2603 .with_polygon(vec![
2604 (0.0, 0.0),
2605 (90.0, 0.0),
2606 (90.0, 12.0),
2607 (55.0, 12.0),
2608 (55.0, 60.0),
2609 (35.0, 60.0),
2610 (35.0, 12.0),
2611 (0.0, 12.0),
2612 ])
2613 .with_quantity(4),
2614 Geometry2D::new("shape7")
2616 .with_polygon(vec![
2617 (0.0, 10.0),
2618 (10.0, 0.0),
2619 (70.0, 0.0),
2620 (80.0, 10.0),
2621 (80.0, 70.0),
2622 (70.0, 80.0),
2623 (10.0, 80.0),
2624 (0.0, 70.0),
2625 ])
2626 .with_quantity(3),
2627 Geometry2D::new("shape8_gear")
2629 .with_polygon(vec![
2630 (50.0, 5.0),
2631 (65.0, 15.0),
2632 (77.0, 18.0),
2633 (80.0, 32.0),
2634 (95.0, 50.0),
2635 (80.0, 68.0),
2636 (77.0, 82.0),
2637 (65.0, 85.0),
2638 (50.0, 95.0),
2639 (35.0, 85.0),
2640 (23.0, 82.0),
2641 (20.0, 68.0),
2642 (5.0, 50.0),
2643 (20.0, 32.0),
2644 (23.0, 18.0),
2645 (35.0, 15.0),
2646 ])
2647 .with_quantity(13),
2648 ];
2649
2650 let boundary = Boundary2D::rectangle(500.0, 500.0);
2652 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2653 let nester = Nester2D::new(config);
2654
2655 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2656
2657 println!(
2658 "Placed {} pieces across {} strips",
2659 result.placements.len(),
2660 result.boundaries_used
2661 );
2662
2663 let strip_width = 500.0;
2665 let gear_aabb = shapes[8].aabb();
2666 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2667
2668 let mut violations = Vec::new();
2669 for p in &result.placements {
2670 if p.geometry_id.as_str().starts_with("shape8") {
2671 let origin_x = p.position[0];
2672 let _origin_y = p.position[1];
2673 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2674 let strip_idx = p.boundary_index;
2675 let local_x = origin_x - (strip_idx as f64 * strip_width);
2676
2677 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2678 let local_max_x = local_x + g_max_r[0];
2679
2680 println!(
2681 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2682 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2683 );
2684
2685 if local_max_x > 500.01 {
2686 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2687 }
2688 }
2689 }
2690
2691 assert!(
2692 violations.is_empty(),
2693 "Found {} Gear pieces exceeding boundary: {:?}",
2694 violations.len(),
2695 violations
2696 );
2697 }
2698
2699 #[test]
2700 fn test_blf_bounds_expanded_like_benchmark() {
2701 type ShapeDef = (Vec<(f64, f64)>, usize, Vec<f64>);
2705 let shape_defs: Vec<ShapeDef> = vec![
2706 (
2707 vec![
2708 (0.0, 0.0),
2709 (180.0, 0.0),
2710 (195.0, 15.0),
2711 (200.0, 50.0),
2712 (200.0, 150.0),
2713 (195.0, 185.0),
2714 (180.0, 200.0),
2715 (20.0, 200.0),
2716 (5.0, 185.0),
2717 (0.0, 150.0),
2718 (0.0, 50.0),
2719 (5.0, 15.0),
2720 ],
2721 2,
2722 vec![0.0, 90.0, 180.0, 270.0],
2723 ),
2724 (
2725 vec![
2726 (60.0, 0.0),
2727 (85.0, 7.0),
2728 (104.0, 25.0),
2729 (118.0, 50.0),
2730 (120.0, 60.0),
2731 (118.0, 70.0),
2732 (104.0, 95.0),
2733 (85.0, 113.0),
2734 (60.0, 120.0),
2735 (35.0, 113.0),
2736 (16.0, 95.0),
2737 (2.0, 70.0),
2738 (0.0, 60.0),
2739 (2.0, 50.0),
2740 (16.0, 25.0),
2741 (35.0, 7.0),
2742 ],
2743 4,
2744 vec![0.0, 45.0, 90.0, 135.0],
2745 ),
2746 (
2747 vec![
2748 (0.0, 0.0),
2749 (80.0, 0.0),
2750 (80.0, 20.0),
2751 (20.0, 20.0),
2752 (20.0, 80.0),
2753 (0.0, 80.0),
2754 ],
2755 6,
2756 vec![0.0, 90.0, 180.0, 270.0],
2757 ),
2758 (
2759 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
2760 6,
2761 vec![0.0, 90.0, 180.0, 270.0],
2762 ),
2763 (
2764 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
2765 4,
2766 vec![0.0, 90.0],
2767 ),
2768 (
2769 vec![
2770 (15.0, 0.0),
2771 (45.0, 0.0),
2772 (60.0, 26.0),
2773 (45.0, 52.0),
2774 (15.0, 52.0),
2775 (0.0, 26.0),
2776 ],
2777 8,
2778 vec![0.0, 60.0, 120.0],
2779 ),
2780 (
2781 vec![
2782 (0.0, 0.0),
2783 (90.0, 0.0),
2784 (90.0, 12.0),
2785 (55.0, 12.0),
2786 (55.0, 60.0),
2787 (35.0, 60.0),
2788 (35.0, 12.0),
2789 (0.0, 12.0),
2790 ],
2791 4,
2792 vec![0.0, 90.0, 180.0, 270.0],
2793 ),
2794 (
2795 vec![
2796 (0.0, 10.0),
2797 (10.0, 0.0),
2798 (70.0, 0.0),
2799 (80.0, 10.0),
2800 (80.0, 70.0),
2801 (70.0, 80.0),
2802 (10.0, 80.0),
2803 (0.0, 70.0),
2804 ],
2805 3,
2806 vec![0.0, 90.0],
2807 ),
2808 (
2810 vec![
2811 (50.0, 5.0),
2812 (65.0, 15.0),
2813 (77.0, 18.0),
2814 (80.0, 32.0),
2815 (95.0, 50.0),
2816 (80.0, 68.0),
2817 (77.0, 82.0),
2818 (65.0, 85.0),
2819 (50.0, 95.0),
2820 (35.0, 85.0),
2821 (23.0, 82.0),
2822 (20.0, 68.0),
2823 (5.0, 50.0),
2824 (20.0, 32.0),
2825 (23.0, 18.0),
2826 (35.0, 15.0),
2827 ],
2828 13,
2829 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
2830 ),
2831 ];
2832
2833 let mut geometries = Vec::new();
2835 let mut piece_id = 0;
2836 for (vertices, demand, rotations) in shape_defs.iter() {
2837 for _ in 0..*demand {
2838 let geom = Geometry2D::new(format!("piece_{}", piece_id))
2839 .with_polygon(vertices.clone())
2840 .with_rotations_deg(rotations.clone());
2841 geometries.push(geom);
2842 piece_id += 1;
2843 }
2844 }
2845
2846 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
2848 let (gear_min, gear_max) = gear_geom.aabb();
2849 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
2850
2851 let boundary = Boundary2D::rectangle(500.0, 500.0);
2852 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2853 let nester = Nester2D::new(config);
2854
2855 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
2856
2857 println!(
2858 "Placed {} pieces across {} strips",
2859 result.placements.len(),
2860 result.boundaries_used
2861 );
2862
2863 let strip_width = 500.0;
2865 let mut violations = Vec::new();
2866
2867 for p in &result.placements {
2868 let id_num: usize = p
2869 .geometry_id
2870 .as_str()
2871 .strip_prefix("piece_")
2872 .and_then(|s| s.parse().ok())
2873 .unwrap_or(0);
2874
2875 if (37..=49).contains(&id_num) {
2877 let origin_x = p.position[0];
2878 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2879 let strip_idx = p.boundary_index;
2880 let local_x = origin_x - (strip_idx as f64 * strip_width);
2881
2882 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
2883 let local_max_x = local_x + g_max_r[0];
2884
2885 println!(
2886 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2887 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2888 );
2889
2890 if local_max_x > 500.01 {
2891 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2892 }
2893 }
2894 }
2895
2896 assert!(
2897 violations.is_empty(),
2898 "Found {} Gear pieces exceeding boundary: {:?}",
2899 violations.len(),
2900 violations
2901 );
2902 }
2903
2904 fn aabbs_overlap(
2906 a_min: [f64; 2],
2907 a_max: [f64; 2],
2908 b_min: [f64; 2],
2909 b_max: [f64; 2],
2910 tolerance: f64,
2911 ) -> bool {
2912 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
2914 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
2915 x_overlap && y_overlap
2916 }
2917
2918 #[test]
2920 fn test_all_strategies_boundary_and_overlap() {
2921 use std::collections::HashMap;
2922
2923 let shapes = vec![
2925 Geometry2D::new("shape0")
2926 .with_polygon(vec![
2927 (0.0, 0.0),
2928 (180.0, 0.0),
2929 (195.0, 15.0),
2930 (200.0, 50.0),
2931 (200.0, 150.0),
2932 (195.0, 185.0),
2933 (180.0, 200.0),
2934 (20.0, 200.0),
2935 (5.0, 185.0),
2936 (0.0, 150.0),
2937 (0.0, 50.0),
2938 (5.0, 15.0),
2939 ])
2940 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2941 .with_quantity(2),
2942 Geometry2D::new("shape1_flange")
2943 .with_polygon(vec![
2944 (60.0, 0.0),
2945 (85.0, 7.0),
2946 (104.0, 25.0),
2947 (118.0, 50.0),
2948 (120.0, 60.0),
2949 (118.0, 70.0),
2950 (104.0, 95.0),
2951 (85.0, 113.0),
2952 (60.0, 120.0),
2953 (35.0, 113.0),
2954 (16.0, 95.0),
2955 (2.0, 70.0),
2956 (0.0, 60.0),
2957 (2.0, 50.0),
2958 (16.0, 25.0),
2959 (35.0, 7.0),
2960 ])
2961 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
2962 .with_quantity(4),
2963 Geometry2D::new("shape2_lbracket")
2964 .with_polygon(vec![
2965 (0.0, 0.0),
2966 (80.0, 0.0),
2967 (80.0, 20.0),
2968 (20.0, 20.0),
2969 (20.0, 80.0),
2970 (0.0, 80.0),
2971 ])
2972 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2973 .with_quantity(6),
2974 Geometry2D::new("shape3_triangle")
2975 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2976 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
2977 .with_quantity(6),
2978 Geometry2D::new("shape4_rect")
2979 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2980 .with_rotations_deg(vec![0.0, 90.0])
2981 .with_quantity(4),
2982 Geometry2D::new("shape5_hexagon")
2983 .with_polygon(vec![
2984 (15.0, 0.0),
2985 (45.0, 0.0),
2986 (60.0, 26.0),
2987 (45.0, 52.0),
2988 (15.0, 52.0),
2989 (0.0, 26.0),
2990 ])
2991 .with_rotations_deg(vec![0.0, 60.0, 120.0])
2992 .with_quantity(8),
2993 Geometry2D::new("shape6_tstiff")
2994 .with_polygon(vec![
2995 (0.0, 0.0),
2996 (90.0, 0.0),
2997 (90.0, 12.0),
2998 (55.0, 12.0),
2999 (55.0, 60.0),
3000 (35.0, 60.0),
3001 (35.0, 12.0),
3002 (0.0, 12.0),
3003 ])
3004 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3005 .with_quantity(4),
3006 Geometry2D::new("shape7_mount")
3007 .with_polygon(vec![
3008 (0.0, 10.0),
3009 (10.0, 0.0),
3010 (70.0, 0.0),
3011 (80.0, 10.0),
3012 (80.0, 70.0),
3013 (70.0, 80.0),
3014 (10.0, 80.0),
3015 (0.0, 70.0),
3016 ])
3017 .with_rotations_deg(vec![0.0, 90.0])
3018 .with_quantity(3),
3019 Geometry2D::new("shape8_gear")
3020 .with_polygon(vec![
3021 (50.0, 5.0),
3022 (65.0, 15.0),
3023 (77.0, 18.0),
3024 (80.0, 32.0),
3025 (95.0, 50.0),
3026 (80.0, 68.0),
3027 (77.0, 82.0),
3028 (65.0, 85.0),
3029 (50.0, 95.0),
3030 (35.0, 85.0),
3031 (23.0, 82.0),
3032 (20.0, 68.0),
3033 (5.0, 50.0),
3034 (20.0, 32.0),
3035 (23.0, 18.0),
3036 (35.0, 15.0),
3037 ])
3038 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
3039 .with_quantity(13),
3040 ];
3041
3042 let geom_map: HashMap<String, &Geometry2D> =
3044 shapes.iter().map(|g| (g.id().clone(), g)).collect();
3045
3046 let boundary = Boundary2D::rectangle(500.0, 500.0);
3047 let strip_width = 500.0;
3048
3049 let strategies = vec![
3051 Strategy::BottomLeftFill,
3052 Strategy::NfpGuided,
3053 Strategy::GeneticAlgorithm,
3054 Strategy::Brkga,
3055 Strategy::SimulatedAnnealing,
3056 Strategy::Gdrr,
3057 Strategy::Alns,
3058 ];
3059
3060 for strategy in strategies {
3061 println!("\n========== Testing {:?} ==========", strategy);
3062
3063 let config = Config::default()
3064 .with_strategy(strategy)
3065 .with_time_limit(30000); let nester = Nester2D::new(config);
3067
3068 let result = match nester.solve_multi_strip(&shapes, &boundary) {
3069 Ok(r) => r,
3070 Err(e) => {
3071 println!(" Strategy {:?} failed: {}", strategy, e);
3072 continue;
3073 }
3074 };
3075
3076 println!(
3077 " Placed {} pieces across {} strips",
3078 result.placements.len(),
3079 result.boundaries_used
3080 );
3081
3082 let mut boundary_violations = Vec::new();
3084 for p in &result.placements {
3085 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
3087 let full_id = if base_id.starts_with("shape") {
3088 shapes
3090 .iter()
3091 .find(|g| p.geometry_id.starts_with(g.id()))
3092 .map(|g| g.id().as_str())
3093 } else {
3094 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
3095 };
3096
3097 let geom = match full_id.and_then(|id| geom_map.get(id)) {
3098 Some(g) => *g,
3099 None => {
3100 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
3102 Some(g) => g,
3103 None => {
3104 println!(
3105 " WARNING: Could not find geometry for {}",
3106 p.geometry_id
3107 );
3108 continue;
3109 }
3110 }
3111 }
3112 };
3113
3114 let origin_x = p.position[0];
3115 let origin_y = p.position[1];
3116 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3117 let strip_idx = p.boundary_index;
3118
3119 let local_x = origin_x - (strip_idx as f64 * strip_width);
3121
3122 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
3123
3124 let local_min_x = local_x + g_min[0];
3126 let local_max_x = local_x + g_max[0];
3127 let local_min_y = origin_y + g_min[1];
3128 let local_max_y = origin_y + g_max[1];
3129
3130 let tolerance = 0.1;
3132 if local_min_x < -tolerance
3133 || local_max_x > 500.0 + tolerance
3134 || local_min_y < -tolerance
3135 || local_max_y > 500.0 + tolerance
3136 {
3137 boundary_violations.push(format!(
3138 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
3139 p.geometry_id,
3140 strip_idx,
3141 local_min_x,
3142 local_min_y,
3143 local_max_x,
3144 local_max_y
3145 ));
3146 }
3147 }
3148
3149 if !boundary_violations.is_empty() {
3150 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
3151 for v in &boundary_violations {
3152 println!(" - {}", v);
3153 }
3154 }
3155
3156 let mut overlaps = Vec::new();
3158 let placements: Vec<_> = result.placements.iter().collect();
3159
3160 for i in 0..placements.len() {
3161 for j in (i + 1)..placements.len() {
3162 let p1 = placements[i];
3163 let p2 = placements[j];
3164
3165 if p1.boundary_index != p2.boundary_index {
3167 continue;
3168 }
3169
3170 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
3172 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
3173
3174 let (g1, g2) = match (g1, g2) {
3175 (Some(a), Some(b)) => (a, b),
3176 _ => continue,
3177 };
3178
3179 let strip_idx = p1.boundary_index;
3180 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
3181 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
3182
3183 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
3184 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
3185
3186 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
3187 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
3188
3189 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
3190 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
3191 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
3192 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
3193
3194 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
3195 overlaps.push(format!(
3196 "{} and {} in strip {}",
3197 p1.geometry_id, p2.geometry_id, strip_idx
3198 ));
3199 }
3200 }
3201 }
3202
3203 if !overlaps.is_empty() {
3204 println!(" OVERLAPS ({}):", overlaps.len());
3205 for o in overlaps.iter().take(10) {
3206 println!(" - {}", o);
3207 }
3208 if overlaps.len() > 10 {
3209 println!(" ... and {} more", overlaps.len() - 10);
3210 }
3211 }
3212
3213 assert!(
3215 boundary_violations.is_empty(),
3216 "{:?}: Found {} boundary violations",
3217 strategy,
3218 boundary_violations.len()
3219 );
3220
3221 println!(" ✓ All placements within boundary");
3222 println!(" ✓ No AABB overlaps detected");
3223 }
3224 }
3225
3226 #[test]
3234 fn test_multi_strip_distributes_all_instances() {
3235 let geometries = vec![Geometry2D::rectangle("part", 100.0, 100.0).with_quantity(20)];
3236 let boundary = Boundary2D::rectangle(300.0, 300.0);
3237 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3238 let nester = Nester2D::new(config);
3239
3240 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3241
3242 assert_eq!(
3244 result.placements.len(),
3245 20,
3246 "all 20 instances must be placed"
3247 );
3248 assert_eq!(
3249 result.boundaries_used, 3,
3250 "20 of 100x100 in 300x300 => 3 sheets"
3251 );
3252 assert!(
3253 result.unplaced.is_empty(),
3254 "nothing should be unplaced, got {:?}",
3255 result.unplaced
3256 );
3257 assert_eq!(result.total_requested, 20);
3259
3260 let mut seen = std::collections::HashSet::new();
3263 for p in &result.placements {
3264 assert!(
3265 seen.insert((p.geometry_id.clone(), p.instance)),
3266 "duplicate (id, instance) = ({}, {}) across sheets",
3267 p.geometry_id,
3268 p.instance
3269 );
3270 }
3271 let mut sheets: Vec<usize> = result.placements.iter().map(|p| p.boundary_index).collect();
3273 sheets.sort_unstable();
3274 sheets.dedup();
3275 assert_eq!(sheets, vec![0, 1, 2]);
3276 }
3277
3278 #[test]
3281 fn test_multi_strip_oversized_reported_unplaced() {
3282 let geometries = vec![
3283 Geometry2D::rectangle("ok", 50.0, 50.0).with_quantity(2),
3284 Geometry2D::rectangle("toobig", 400.0, 400.0).with_quantity(3),
3285 ];
3286 let boundary = Boundary2D::rectangle(300.0, 300.0);
3287 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3288 let nester = Nester2D::new(config);
3289
3290 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3291
3292 assert_eq!(result.total_requested, 5, "2 + 3 instances requested");
3293 assert_eq!(result.placements.len(), 2);
3295 assert!(
3296 result.unplaced.contains(&"toobig".to_string()),
3297 "oversized geometry must surface in unplaced, got {:?}",
3298 result.unplaced
3299 );
3300 }
3301}