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;
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, PackingAxis, 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::{
27 offered_names, Config, Dimension, ProgressCallback, ProgressInfo, Solver, Strategy,
28};
29use u_nesting_core::{Error, Placement, Result, SolveResult, StopReason};
30
31use crate::placement_utils::{hole_nfps, inset_boundary, nesting_fitness, offset_nfp};
32use std::sync::atomic::{AtomicBool, Ordering};
33use std::sync::Arc;
34use u_nesting_core::timing::Timer;
35
36const SEARCH_POPULATION: usize = 30;
41
42const SEARCH_GENERATIONS: u32 = 50;
45
46fn mirror_candidates(geom: &Geometry2D) -> &'static [bool] {
58 if geom.allow_flip() {
59 &[false, true]
60 } else {
61 &[false]
62 }
63}
64
65fn solution_quality(
79 result: &SolveResult<f64>,
80 geometries: &[Geometry2D],
81 boundary: &Boundary2D,
82) -> (usize, f64) {
83 use std::collections::HashMap;
84 let geom_map: HashMap<_, _> = geometries.iter().map(|g| (g.id().clone(), g)).collect();
85
86 let mut min_x = f64::INFINITY;
87 let mut min_y = f64::INFINITY;
88 let mut max_x = f64::NEG_INFINITY;
89 let mut max_y = f64::NEG_INFINITY;
90
91 for p in &result.placements {
92 if let Some(geom) = geom_map.get(&p.geometry_id) {
93 let x = p.position.first().copied().unwrap_or(0.0);
94 let y = p.position.get(1).copied().unwrap_or(0.0);
95 let rot = p.rotation.first().copied().unwrap_or(0.0);
96 let (g_min, g_max) = geom.aabb_at_rotation(rot);
97 min_x = min_x.min(x + g_min[0]);
98 min_y = min_y.min(y + g_min[1]);
99 max_x = max_x.max(x + g_max[0]);
100 max_y = max_y.max(y + g_max[1]);
101 }
102 }
103
104 if result.placements.is_empty() {
105 return (0, f64::INFINITY);
106 }
107
108 let strip_length = match PackingAxis::of(boundary) {
110 PackingAxis::Y => max_y - min_y,
111 PackingAxis::X => max_x - min_x,
112 };
113 (result.placements.len(), strip_length)
114}
115
116pub struct Nester2D {
118 config: Config,
119 cancelled: Arc<AtomicBool>,
120 nfp_cache: NfpCache,
121}
122
123#[cfg(feature = "milp")]
134fn milp_grid_step(geometries: &[Geometry2D], boundary: &Boundary2D) -> f64 {
135 let smallest_side = geometries
136 .iter()
137 .map(|g| {
138 let (g_min, g_max) = g.aabb_at_rotation(0.0);
139 (g_max[0] - g_min[0]).min(g_max[1] - g_min[1])
140 })
141 .fold(f64::INFINITY, f64::min);
142 if !smallest_side.is_finite() || smallest_side <= 0.0 {
143 return 1.0;
144 }
145 let (b_min, b_max) = boundary.aabb();
146 let longest_side = (b_max[0] - b_min[0]).max(b_max[1] - b_min[1]);
147 (smallest_side / 4.0).max(1.0).max(longest_side / 256.0)
150}
151
152#[cfg(feature = "milp")]
158fn milp_time_limit(requested_ms: u64) -> u64 {
159 if requested_ms == 0 {
160 u64::MAX / 2
161 } else {
162 requested_ms
163 }
164}
165
166#[derive(Debug, Clone, Copy)]
169struct BlfCandidate {
170 rotation: f64,
171 extent_across: f64,
172 extent_along: f64,
173 place_across: f64,
174 place_along: f64,
175 opened_row: bool,
177}
178
179fn stop_phase(stop: StopReason) -> &'static str {
181 match stop {
182 StopReason::Cancelled => "Cancelled",
183 _ => "Time Limit Reached",
184 }
185}
186
187impl Nester2D {
188 fn run_strategy_reporting(
194 &self,
195 geometries: &[Geometry2D],
196 boundary: &Boundary2D,
197 progress: Option<&ProgressCallback>,
198 ) -> Result<SolveResult<f64>> {
199 match self.config.strategy {
200 Strategy::BottomLeftFill => self.bottom_left_fill_impl(geometries, boundary, progress),
201 Strategy::NfpGuided => self.nfp_guided_blf_impl(geometries, boundary, progress),
202 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary, progress),
203 Strategy::Brkga => self.brkga(geometries, boundary, progress),
204 Strategy::SimulatedAnnealing => {
205 self.simulated_annealing(geometries, boundary, progress)
206 }
207 Strategy::Gdrr => self.gdrr(geometries, boundary, progress),
208 Strategy::Alns => self.alns(geometries, boundary, progress),
209 #[cfg(feature = "milp")]
210 Strategy::MilpExact => self.milp_exact(geometries, boundary),
211 #[cfg(feature = "milp")]
212 Strategy::HybridExact => self.hybrid_exact(geometries, boundary),
213 Strategy::ExtremePoint => Err(self.unoffered_strategy()),
215 #[cfg(not(feature = "milp"))]
216 Strategy::MilpExact | Strategy::HybridExact => Err(self.unoffered_strategy()),
217 }
218 }
219
220 fn stop_requested(&self, start: &Timer) -> Option<StopReason> {
223 if self.cancelled.load(Ordering::Relaxed) {
224 Some(StopReason::Cancelled)
225 } else if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms {
226 Some(StopReason::TimeLimit)
227 } else {
228 None
229 }
230 }
231
232 pub fn new(config: Config) -> Self {
234 Self {
235 config,
236 cancelled: Arc::new(AtomicBool::new(false)),
237 nfp_cache: NfpCache::new(),
238 }
239 }
240
241 fn with_time_limit(&self, time_limit_ms: u64) -> Self {
244 Self {
245 config: Config {
246 time_limit_ms,
247 ..self.config.clone()
248 },
249 cancelled: self.cancelled.clone(),
250 nfp_cache: NfpCache::new(),
251 }
252 }
253
254 fn constructive_fill(&self) -> Self {
258 Self {
259 config: Config {
260 strategy: Strategy::BottomLeftFill,
261 time_limit_ms: 0,
262 ..self.config.clone()
263 },
264 cancelled: self.cancelled.clone(),
265 nfp_cache: NfpCache::new(),
266 }
267 }
268
269 pub fn cancel_handle(&self) -> Arc<AtomicBool> {
272 self.cancelled.clone()
273 }
274
275 pub fn default_config() -> Self {
277 Self::new(Config::default())
278 }
279
280 fn iteration_budget<T: TryFrom<u64> + Copy>(&self, tuned: T) -> T {
283 self.config
284 .max_iterations
285 .map_or(tuned, |n| T::try_from(n).unwrap_or(tuned))
286 }
287
288 fn ga_config(&self) -> GaConfig {
292 let config = GaConfig::default()
293 .with_population_size(self.config.population_size.unwrap_or(SEARCH_POPULATION))
294 .with_max_generations(self.config.max_generations.unwrap_or(SEARCH_GENERATIONS))
295 .with_crossover_rate(self.config.crossover_rate)
296 .with_mutation_rate(self.config.mutation_rate)
297 .with_elite_count(self.config.elite_count);
298 match self.target_fitness() {
299 Some(target) => config.with_target_fitness(target),
300 None => config,
301 }
302 }
303
304 fn brkga_config(&self) -> BrkgaConfig {
307 let config = BrkgaConfig::default()
308 .with_population_size(self.config.population_size.unwrap_or(SEARCH_POPULATION))
309 .with_max_generations(self.config.max_generations.unwrap_or(SEARCH_GENERATIONS))
310 .with_elite_fraction(0.2)
311 .with_mutant_fraction(0.15)
312 .with_elite_bias(0.7);
313 match self.target_fitness() {
314 Some(target) => config.with_target_fitness(target),
315 None => config,
316 }
317 }
318
319 fn gdrr_config(&self, time_limit_ms: u64) -> GdrrConfig {
321 GdrrConfig {
322 seed: self.config.seed,
323 ..GdrrConfig::default()
324 .with_max_iterations(self.iteration_budget(1000))
325 .with_time_limit_ms(time_limit_ms)
326 .with_ruin_ratio(0.1, 0.3) .with_lahc_list_length(30) }
329 }
330
331 fn alns_config(&self, time_limit_ms: u64) -> AlnsConfig {
333 AlnsConfig {
334 seed: self.config.seed,
335 ..AlnsConfig::default()
336 .with_max_iterations(self.iteration_budget(1000))
337 .with_time_limit_ms(time_limit_ms)
338 .with_segment_size(50) .with_scores(33.0, 9.0, 13.0)
340 .with_reaction_factor(0.15) .with_relative_temperature(1.0, 0.995, 0.03, 5)
348 }
349 }
350
351 fn bottom_left_fill(
361 &self,
362 geometries: &[Geometry2D],
363 boundary: &Boundary2D,
364 ) -> Result<SolveResult<f64>> {
365 self.bottom_left_fill_impl(geometries, boundary, None)
366 }
367
368 fn nfp_guided_blf_impl(
371 &self,
372 geometries: &[Geometry2D],
373 boundary: &Boundary2D,
374 callback: Option<&ProgressCallback>,
375 ) -> Result<SolveResult<f64>> {
376 let start = Timer::now();
377 let mut result = SolveResult::new();
378 let mut placements = Vec::new();
379 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
380 let report = |info: ProgressInfo| {
381 if let Some(callback) = callback {
382 callback(info);
383 }
384 };
385 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
386 let mut placed_count = 0usize;
387 report(
388 ProgressInfo::new()
389 .with_phase("NFP Placement")
390 .with_items(0, total_pieces)
391 .with_elapsed(0),
392 );
393
394 let margin = self.config.margin;
395 let spacing = self.config.spacing;
396
397 let boundary_polygon = inset_boundary(boundary, margin);
399
400 let mut total_placed_area = 0.0;
401
402 let sample_step = self.compute_sample_step(geometries);
404
405 for geom in geometries {
406 geom.validate()?;
407
408 let rotation_angles: Vec<f64> = geom.rotations();
411
412 let mirror_candidates = mirror_candidates(geom);
413
414 for instance in 0..geom.quantity() {
415 if let Some(stop) = self.stop_requested(&start) {
417 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
418 result.utilization = total_placed_area / boundary.measure();
419 result.computation_time_ms = start.elapsed_ms();
420 result.placements = placements;
421 result.stop_reason = stop;
422 report(
423 ProgressInfo::new()
424 .with_phase(stop_phase(stop))
425 .with_items(placed_count, total_pieces)
426 .with_elapsed(result.computation_time_ms)
427 .finished(),
428 );
429 return Ok(result);
430 }
431
432 let mut best_placement: Option<(f64, f64, f64, bool)> = None; for &rotation in &rotation_angles {
436 for &mirror in mirror_candidates {
437 let ifp = match compute_ifp_with_margin_and_mirror(
439 &boundary_polygon,
440 geom,
441 rotation,
442 0.0,
444 mirror,
445 ) {
446 Ok(ifp) => ifp,
447 Err(_) => continue,
448 };
449
450 if ifp.is_empty() {
451 continue;
452 }
453
454 let mut nfps: Vec<Nfp> = Vec::new();
456 for placed in &placed_geometries {
457 let cache_key = (
460 placed.geometry.id().as_str(),
461 geom.id().as_str(),
462 rotation - placed.rotation, placed.mirrored,
464 mirror,
465 );
466
467 let nfp_at_origin =
472 match self.nfp_cache.get_or_compute_mirrored(cache_key, || {
473 let placed_at_origin = placed.geometry.clone();
474 compute_nfp_mirrored(
475 &placed_at_origin,
476 geom,
477 rotation - placed.rotation,
478 placed.mirrored,
479 mirror,
480 )
481 .map(|nfp| offset_nfp(&nfp, spacing))
485 }) {
486 Ok(nfp) => nfp,
487 Err(_) => continue,
488 };
489
490 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
493 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
494 nfps.push(translated_nfp);
495 }
496
497 nfps.extend(hole_nfps(boundary, geom, rotation, mirror, margin));
502 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
503 if let Some((x, y)) = find_bottom_left_placement(
504 &ifp,
505 &nfp_refs,
506 sample_step,
507 PackingAxis::of(boundary),
508 ) {
509 let is_better =
511 match best_placement {
512 None => true,
513 Some((best_x, best_y, _, _)) => PackingAxis::of(boundary)
514 .precedes((x, y), (best_x, best_y), 1e-6),
515 };
516 if is_better {
517 best_placement = Some((x, y, rotation, mirror));
518 }
519 }
520 }
521 }
522
523 if let Some((x, y, rotation, mirror)) = best_placement {
525 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
532 let boundary_aabb = boundary.aabb();
533
534 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
535 x,
536 y,
537 geom_aabb,
538 boundary_aabb,
539 margin,
540 ) {
541 let placement = Placement::new_2d(
542 geom.id().clone(),
543 instance,
544 clamped_x,
545 clamped_y,
546 rotation,
547 )
548 .with_mirrored(mirror);
549
550 placements.push(placement);
551 placed_geometries.push(
552 PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation)
553 .with_mirrored(mirror),
554 );
555 total_placed_area += geom.measure();
556 placed_count += 1;
557 report(
558 ProgressInfo::new()
559 .with_phase("NFP Placement")
560 .with_items(placed_count, total_pieces)
561 .with_utilization(total_placed_area / boundary.measure())
562 .with_elapsed(start.elapsed_ms()),
563 );
564 } else {
565 result.unplaced.push(geom.id().clone());
567 }
568 } else {
569 result.unplaced.push(geom.id().clone());
571 }
572 }
573 }
574
575 result.placements = placements;
576 result.boundaries_used = 1;
577 result.utilization = total_placed_area / boundary.measure();
578 result.computation_time_ms = start.elapsed_ms();
579 report(
580 ProgressInfo::new()
581 .with_phase("Complete")
582 .with_items(placed_count, total_pieces)
583 .with_utilization(result.utilization)
584 .with_elapsed(result.computation_time_ms)
585 .finished(),
586 );
587
588 Ok(result)
589 }
590
591 fn compute_sample_step(&self, geometries: &[Geometry2D]) -> f64 {
593 if geometries.is_empty() {
594 return 1.0;
595 }
596
597 let mut min_dim = f64::INFINITY;
599 for geom in geometries {
600 let (g_min, g_max) = geom.aabb();
601 let width = g_max[0] - g_min[0];
602 let height = g_max[1] - g_min[1];
603 min_dim = min_dim.min(width).min(height);
604 }
605
606 (min_dim / 4.0).clamp(0.5, 10.0)
608 }
609
610 fn not_worse_than_baselines(
620 &self,
621 meta: SolveResult<f64>,
622 geometries: &[Geometry2D],
623 boundary: &Boundary2D,
624 greedy: Option<SolveResult<f64>>,
625 ) -> SolveResult<f64> {
626 let baselines = self
627 .bottom_left_fill(geometries, boundary)
628 .ok()
629 .into_iter()
630 .chain(greedy);
631 let quality = |r: &SolveResult<f64>| solution_quality(r, geometries, boundary);
632 meta.floored_at(baselines, |a, b| {
635 let (a, b) = (quality(a), quality(b));
636 a.0 > b.0 || (a.0 == b.0 && a.1 < b.1 - 1e-6)
637 })
638 }
639
640 fn target_fitness(&self) -> Option<f64> {
643 self.config
644 .target_utilization
645 .map(|target| nesting_fitness(1, 1, target))
646 }
647
648 fn search_budget_ms(&self) -> u64 {
658 match self.config.time_limit_ms {
659 0 => u64::MAX / 2,
660 limit => limit,
661 }
662 }
663
664 fn greedy_start(
671 &self,
672 geometries: &[Geometry2D],
673 boundary: &Boundary2D,
674 budget_ms: u64,
675 progress: Option<&ProgressCallback>,
676 ) -> (Option<SolveResult<f64>>, u64) {
677 let started = Timer::now();
678 let greedy = self
679 .with_time_limit(budget_ms)
680 .nfp_guided_blf_impl(geometries, boundary, progress)
681 .ok();
682 (
683 greedy,
684 budget_ms.saturating_sub(started.elapsed_ms()).max(1),
685 )
686 }
687
688 fn genetic_algorithm(
695 &self,
696 geometries: &[Geometry2D],
697 boundary: &Boundary2D,
698 progress: Option<&ProgressCallback>,
699 ) -> Result<SolveResult<f64>> {
700 let (time_limit_ms, greedy) = {
701 let budget = self.search_budget_ms();
702 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
703 (remaining, greedy)
704 };
705
706 let ga_config = self
707 .ga_config()
708 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
709
710 let result = run_ga_nesting(
711 geometries,
712 boundary,
713 &self.config,
714 ga_config,
715 self.cancelled.clone(),
716 greedy.as_ref().map(|g| g.placements.as_slice()),
717 progress,
718 )?;
719
720 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
721 }
722
723 fn brkga(
727 &self,
728 geometries: &[Geometry2D],
729 boundary: &Boundary2D,
730 progress: Option<&ProgressCallback>,
731 ) -> Result<SolveResult<f64>> {
732 let (time_limit_ms, greedy) = {
733 let budget = self.search_budget_ms();
734 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
735 (remaining, greedy)
736 };
737
738 let brkga_config = self
739 .brkga_config()
740 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
741
742 let result = run_brkga_nesting(
743 geometries,
744 boundary,
745 &self.config,
746 brkga_config,
747 self.cancelled.clone(),
748 greedy.as_ref().map(|g| g.placements.as_slice()),
749 progress,
750 )?;
751
752 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
753 }
754
755 fn simulated_annealing(
760 &self,
761 geometries: &[Geometry2D],
762 boundary: &Boundary2D,
763 progress: Option<&ProgressCallback>,
764 ) -> Result<SolveResult<f64>> {
765 let (time_limit_ms, greedy) = {
766 let budget = self.search_budget_ms();
767 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
768 (remaining, greedy)
769 };
770
771 let sa_config = SaConfig::default()
772 .with_relative_temperature(1.0, 0.03, 5)
779 .with_cooling_rate(0.92)
780 .with_iterations_per_temp(20)
781 .with_max_iterations(self.iteration_budget(500))
782 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
783 let sa_config = match self.target_fitness() {
784 Some(target) => sa_config.with_target_fitness(target),
785 None => sa_config,
786 };
787
788 let result = run_sa_nesting(
789 geometries,
790 boundary,
791 &self.config,
792 sa_config,
793 self.cancelled.clone(),
794 greedy.as_ref().map(|g| g.placements.as_slice()),
795 progress,
796 )?;
797
798 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
799 }
800
801 fn gdrr(
803 &self,
804 geometries: &[Geometry2D],
805 boundary: &Boundary2D,
806 progress: Option<&ProgressCallback>,
807 ) -> Result<SolveResult<f64>> {
808 let (time_limit, greedy) = {
809 let budget = self.search_budget_ms();
810 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
811 (remaining, greedy)
812 };
813 let result = run_gdrr_nesting(
814 geometries,
815 boundary,
816 &self.config,
817 &self.gdrr_config(time_limit),
818 self.cancelled.clone(),
819 progress,
820 )?;
821
822 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
823 }
824
825 fn alns(
827 &self,
828 geometries: &[Geometry2D],
829 boundary: &Boundary2D,
830 progress: Option<&ProgressCallback>,
831 ) -> Result<SolveResult<f64>> {
832 self.alns_within(geometries, boundary, self.search_budget_ms(), progress)
833 }
834
835 fn alns_within(
838 &self,
839 geometries: &[Geometry2D],
840 boundary: &Boundary2D,
841 budget_ms: u64,
842 progress: Option<&ProgressCallback>,
843 ) -> Result<SolveResult<f64>> {
844 let (greedy, time_limit) = self.greedy_start(geometries, boundary, budget_ms, progress);
845 let result = run_alns_nesting(
846 geometries,
847 boundary,
848 &self.config,
849 &self.alns_config(time_limit),
850 self.cancelled.clone(),
851 progress,
852 )?;
853
854 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
855 }
856
857 #[cfg(feature = "milp")]
870 fn milp_exact(
871 &self,
872 geometries: &[Geometry2D],
873 boundary: &Boundary2D,
874 ) -> Result<SolveResult<f64>> {
875 let exact_config = ExactConfig::default()
880 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms))
881 .with_max_items(15)
882 .with_grid_step(milp_grid_step(geometries, boundary));
883
884 run_nfp_cm_nesting(
885 geometries,
886 boundary,
887 &self.config,
888 &exact_config,
889 self.cancelled.clone(),
890 )
891 }
892
893 #[cfg(feature = "milp")]
895 fn hybrid_exact(
896 &self,
897 geometries: &[Geometry2D],
898 boundary: &Boundary2D,
899 ) -> Result<SolveResult<f64>> {
900 let started = Timer::now();
901 let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
903
904 if total_instances <= 15 {
906 let exact_config = ExactConfig::default()
910 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms / 2))
911 .with_max_items(15)
912 .with_grid_step(milp_grid_step(geometries, boundary));
913
914 let exact_result = run_nfp_cm_nesting(
915 geometries,
916 boundary,
917 &self.config,
918 &exact_config,
919 self.cancelled.clone(),
920 )?;
921
922 if !exact_result.placements.is_empty() && exact_result.unplaced.is_empty() {
929 return Ok(exact_result);
930 }
931 let heuristic =
932 self.alns_within(geometries, boundary, self.fallback_budget(started), None)?;
933 let (exact_placed, exact_length) =
934 solution_quality(&exact_result, geometries, boundary);
935 let (placed, length) = solution_quality(&heuristic, geometries, boundary);
936 let exact_wins =
937 exact_placed > placed || (exact_placed == placed && exact_length < length);
938 return Ok(if exact_wins { exact_result } else { heuristic });
939 }
940
941 self.alns_within(geometries, boundary, self.fallback_budget(started), None)
946 }
947
948 #[cfg(feature = "milp")]
951 fn fallback_budget(&self, started: Timer) -> u64 {
952 match self.config.time_limit_ms {
953 0 => self.search_budget_ms(),
954 limit => limit.saturating_sub(started.elapsed_ms()).max(1),
955 }
956 }
957
958 fn bottom_left_fill_impl(
966 &self,
967 geometries: &[Geometry2D],
968 boundary: &Boundary2D,
969 callback: Option<&ProgressCallback>,
970 ) -> Result<SolveResult<f64>> {
971 let start = Timer::now();
972 let mut result = SolveResult::new();
973 let mut placements = Vec::new();
974 let report = |info: ProgressInfo| {
975 if let Some(callback) = callback {
976 callback(info);
977 }
978 };
979
980 let axis = PackingAxis::of(boundary);
981 let to_ca = |x: f64, y: f64| match axis {
983 PackingAxis::Y => (x, y),
984 PackingAxis::X => (y, x),
985 };
986 let from_ca = |across: f64, along: f64| match axis {
987 PackingAxis::Y => (across, along),
988 PackingAxis::X => (along, across),
989 };
990
991 let (b_min, b_max) = boundary.aabb();
992 let margin = self.config.margin;
993 let spacing = self.config.spacing;
994 let (min_across, min_along) = to_ca(b_min[0] + margin, b_min[1] + margin);
995 let (max_across, max_along) = to_ca(b_max[0] - margin, b_max[1] - margin);
996
997 let hole_boxes: Vec<(f64, f64, f64, f64)> = boundary
1004 .holes()
1005 .iter()
1006 .filter(|hole| !hole.is_empty())
1007 .map(|hole| {
1008 let (mut x0, mut y0) = (f64::INFINITY, f64::INFINITY);
1009 let (mut x1, mut y1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
1010 for &(x, y) in hole {
1011 x0 = x0.min(x);
1012 y0 = y0.min(y);
1013 x1 = x1.max(x);
1014 y1 = y1.max(y);
1015 }
1016 let (a0, l0) = to_ca(x0 - margin, y0 - margin);
1017 let (a1, l1) = to_ca(x1 + margin, y1 + margin);
1018 (a0.min(a1), l0.min(l1), a0.max(a1), l0.max(l1))
1019 })
1020 .collect();
1021
1022 let blocking_hole_edge = |across: f64, along: f64, ea: f64, el: f64| -> Option<f64> {
1026 hole_boxes
1027 .iter()
1028 .filter(|(a0, l0, a1, l1)| {
1029 across < *a1 && across + ea > *a0 && along < *l1 && along + el > *l0
1030 })
1031 .map(|(_, _, a1, _)| *a1)
1032 .fold(None, |far: Option<f64>, a1| {
1033 Some(far.map_or(a1, |f| f.max(a1)))
1034 })
1035 };
1036
1037 let mut cursor_across = min_across;
1038 let mut cursor_along = min_along;
1039 let mut row_depth = 0.0_f64;
1040 let mut total_placed_area = 0.0;
1041 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
1042 let mut placed_count = 0usize;
1043
1044 report(
1045 ProgressInfo::new()
1046 .with_phase("BLF Placement")
1047 .with_items(0, total_pieces)
1048 .with_elapsed(0),
1049 );
1050
1051 for geom in geometries {
1052 geom.validate()?;
1053
1054 let rotation_angles: Vec<f64> = geom.rotations();
1056
1057 for instance in 0..geom.quantity() {
1058 if let Some(stop) = self.stop_requested(&start) {
1060 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
1061 result.utilization = total_placed_area / boundary.measure();
1062 result.computation_time_ms = start.elapsed_ms();
1063 result.placements = placements;
1064 result.stop_reason = stop;
1065 report(
1066 ProgressInfo::new()
1067 .with_phase(stop_phase(stop))
1068 .with_items(placed_count, total_pieces)
1069 .with_elapsed(result.computation_time_ms)
1070 .finished(),
1071 );
1072 return Ok(result);
1073 }
1074
1075 let mut best_fit: Option<BlfCandidate> = None;
1078
1079 for &rotation in &rotation_angles {
1080 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
1081 let (extent_across, extent_along) =
1082 to_ca(g_max[0] - g_min[0], g_max[1] - g_min[1]);
1083
1084 if extent_across > max_across - min_across
1085 || extent_along > max_along - min_along
1086 {
1087 continue;
1088 }
1089
1090 let mut place_across = cursor_across;
1091 let mut place_along = cursor_along;
1092 let mut depth = row_depth;
1095 let mut opened_row = false;
1096 let mut turns = 0usize;
1102 let max_turns = 4 * (hole_boxes.len() + 1) * (hole_boxes.len() + 2) + 16;
1103 let fits = loop {
1104 if place_across + extent_across > max_across {
1105 place_across = min_across;
1106 place_along += depth + spacing;
1107 depth = extent_along;
1108 opened_row = true;
1109 }
1110 if place_along + extent_along > max_along {
1111 break false;
1112 }
1113 match blocking_hole_edge(
1114 place_across,
1115 place_along,
1116 extent_across,
1117 extent_along,
1118 ) {
1119 None => break true,
1120 Some(past) => place_across = past + spacing,
1121 }
1122 turns += 1;
1123 if turns > max_turns {
1124 break false;
1125 }
1126 };
1127 if !fits {
1128 continue;
1129 }
1130
1131 let cost = |c: &BlfCandidate| {
1136 if c.opened_row {
1137 c.place_along - min_along + c.extent_along
1138 } else {
1139 c.place_across - min_across + c.extent_across
1140 }
1141 };
1142 let candidate = BlfCandidate {
1143 rotation,
1144 extent_across,
1145 extent_along,
1146 place_across,
1147 place_along,
1148 opened_row,
1149 };
1150 let better = best_fit
1151 .as_ref()
1152 .is_none_or(|best| cost(&candidate) < cost(best) - 1e-6);
1153 if better {
1154 best_fit = Some(candidate);
1155 }
1156 }
1157
1158 let Some(BlfCandidate {
1159 rotation,
1160 extent_across,
1161 extent_along,
1162 place_across,
1163 place_along,
1164 opened_row,
1165 }) = best_fit
1166 else {
1167 result.unplaced.push(geom.id().clone());
1168 continue;
1169 };
1170 if opened_row {
1171 row_depth = 0.0;
1172 }
1173
1174 let geom_aabb = geom.aabb_at_rotation(rotation);
1175 let (place_x, place_y) = from_ca(place_across, place_along);
1176 let Some((x, y)) = clamp_placement_to_boundary_with_margin(
1177 place_x - geom_aabb.0[0],
1178 place_y - geom_aabb.0[1],
1179 geom_aabb,
1180 (b_min, b_max),
1181 margin,
1182 ) else {
1183 result.unplaced.push(geom.id().clone());
1184 continue;
1185 };
1186
1187 placements.push(Placement::new_2d(
1188 geom.id().clone(),
1189 instance,
1190 x,
1191 y,
1192 rotation,
1193 ));
1194 total_placed_area += geom.measure();
1195 placed_count += 1;
1196
1197 let (actual_across, actual_along) = to_ca(x + geom_aabb.0[0], y + geom_aabb.0[1]);
1199 cursor_across = actual_across + extent_across + spacing;
1200 cursor_along = actual_along;
1201 row_depth = row_depth.max(extent_along);
1202
1203 report(
1204 ProgressInfo::new()
1205 .with_phase("BLF Placement")
1206 .with_items(placed_count, total_pieces)
1207 .with_utilization(total_placed_area / boundary.measure())
1208 .with_elapsed(start.elapsed_ms()),
1209 );
1210 }
1211 }
1212
1213 result.placements = placements;
1214 result.boundaries_used = 1;
1215 result.utilization = total_placed_area / boundary.measure();
1216 result.computation_time_ms = start.elapsed_ms();
1217
1218 report(
1219 ProgressInfo::new()
1220 .with_phase("Complete")
1221 .with_items(placed_count, total_pieces)
1222 .with_utilization(result.utilization)
1223 .with_elapsed(result.computation_time_ms)
1224 .finished(),
1225 );
1226
1227 Ok(result)
1228 }
1229
1230 pub fn validate(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<()> {
1239 boundary.validate()?;
1240 self.validate_geometries(geometries)?;
1241 if !self.config.strategy.is_offered(Dimension::TwoD) {
1242 return Err(self.unoffered_strategy());
1243 }
1244 Ok(())
1245 }
1246
1247 fn unoffered_strategy(&self) -> Error {
1249 let strategy = self.config.strategy;
1250 let why = if strategy == Strategy::ExtremePoint {
1251 "is 3D only".to_string()
1252 } else {
1253 "needs the `milp` feature, which this build does not have".to_string()
1254 };
1255 Error::invalid_option(
1256 "strategy",
1257 format!(
1258 "strategy \"{}\" {why}; 2D nesting offers {}",
1259 strategy.name(),
1260 offered_names(Dimension::TwoD)
1261 ),
1262 )
1263 }
1264
1265 fn validate_geometries(&self, geometries: &[Geometry2D]) -> Result<()> {
1275 use u_nesting_core::geometry::Geometry;
1276 if geometries.is_empty() {
1277 return Err(Error::empty_input("geometries"));
1278 }
1279 self.config.validate()?;
1280 for geom in geometries {
1281 geom.validate()?;
1282 }
1283 u_nesting_core::geometry::ensure_unique_ids(geometries)
1284 }
1285
1286 pub fn solve_multi_strip(
1292 &self,
1293 geometries: &[Geometry2D],
1294 boundary: &Boundary2D,
1295 ) -> Result<SolveResult<f64>> {
1296 self.solve_multi_strip_reporting(geometries, boundary, None)
1297 }
1298
1299 pub fn solve_multi_strip_with_progress(
1305 &self,
1306 geometries: &[Geometry2D],
1307 boundary: &Boundary2D,
1308 callback: ProgressCallback,
1309 ) -> Result<SolveResult<f64>> {
1310 self.solve_multi_strip_reporting(geometries, boundary, Some(&callback))
1311 }
1312
1313 fn solve_multi_strip_reporting(
1314 &self,
1315 geometries: &[Geometry2D],
1316 boundary: &Boundary2D,
1317 progress: Option<&ProgressCallback>,
1318 ) -> Result<SolveResult<f64>> {
1319 self.validate(geometries, boundary)?;
1320 self.cancelled.store(false, Ordering::Relaxed);
1321 let start = Timer::now();
1322
1323 let (b_min, b_max) = boundary.aabb();
1324 let strip_width = b_max[0] - b_min[0];
1325
1326 let sheet_area = boundary.measure();
1327 let mut final_result = SolveResult::new();
1328 let mut stop_reason = StopReason::Completed;
1331 let mut remaining_geometries: Vec<Geometry2D> = geometries.to_vec();
1332 let mut strip_index = 0;
1333 let max_strips = 100; let mut placed_total: std::collections::HashMap<String, usize> =
1338 std::collections::HashMap::new();
1339
1340 let mut budget_spent = false;
1344
1345 while !remaining_geometries.is_empty() && strip_index < max_strips {
1346 if self.cancelled.load(Ordering::Relaxed) {
1347 stop_reason = StopReason::Cancelled;
1348 break;
1349 }
1350
1351 let left = match self.config.time_limit_ms {
1356 0 => None,
1357 limit => Some(limit.saturating_sub(start.elapsed_ms())),
1358 };
1359 if left == Some(0) {
1360 budget_spent = true;
1361 }
1362 let (sheet_nester, whole_budget) = match left {
1363 _ if budget_spent => (Some(self.constructive_fill()), false),
1364 None => (None, false),
1365 Some(left) => {
1366 let remaining_area: f64 = remaining_geometries
1367 .iter()
1368 .map(|g| g.measure() * g.quantity() as f64)
1369 .sum();
1370 let sheets_ahead = (remaining_area / sheet_area).ceil().max(1.0);
1371 let share = (left as f64 / sheets_ahead).ceil() as u64;
1372 (Some(self.with_time_limit(share)), share >= left)
1373 }
1374 };
1375
1376 let strip_result = sheet_nester
1378 .as_ref()
1379 .unwrap_or(self)
1380 .run_strategy_reporting(&remaining_geometries, boundary, progress)?;
1381 if whole_budget && strip_result.stop_reason == StopReason::TimeLimit {
1384 budget_spent = true;
1385 }
1386 stop_reason = if budget_spent && strip_result.stop_reason != StopReason::Cancelled {
1387 StopReason::TimeLimit
1388 } else {
1389 strip_result.stop_reason
1390 };
1391
1392 let strip_result = validate_and_filter_placements(
1394 strip_result,
1395 &remaining_geometries,
1396 boundary,
1397 self.config.margin,
1398 );
1399
1400 if strip_result.placements.is_empty() {
1401 break;
1404 }
1405
1406 let mut strip_placed: std::collections::HashMap<String, usize> =
1409 std::collections::HashMap::new();
1410
1411 for mut placement in strip_result.placements {
1413 let gid = placement.geometry_id.clone();
1414 let prior = placed_total.get(&gid).copied().unwrap_or(0);
1417 let in_strip = strip_placed.get(&gid).copied().unwrap_or(0);
1418 placement.instance = prior + in_strip;
1419 if !placement.position.is_empty() {
1421 placement.position[0] += strip_index as f64 * strip_width;
1422 }
1423 placement.boundary_index = strip_index;
1424 *strip_placed.entry(gid).or_insert(0) += 1;
1425 final_result.placements.push(placement);
1426 }
1427
1428 for (gid, cnt) in &strip_placed {
1432 *placed_total.entry(gid.clone()).or_insert(0) += cnt;
1433 }
1434 remaining_geometries = remaining_geometries
1435 .into_iter()
1436 .filter_map(|g| {
1437 let placed_here = strip_placed.get(g.id()).copied().unwrap_or(0);
1438 let new_quantity = g.quantity().saturating_sub(placed_here);
1439 if new_quantity == 0 {
1440 None
1441 } else {
1442 Some(g.with_quantity(new_quantity))
1443 }
1444 })
1445 .collect();
1446
1447 strip_index += 1;
1448
1449 if stop_reason == StopReason::Cancelled {
1453 break;
1454 }
1455 }
1456
1457 for g in &remaining_geometries {
1460 final_result.unplaced.push(g.id().clone());
1461 }
1462
1463 final_result.boundaries_used = strip_index;
1464 final_result.stop_reason = stop_reason;
1465 final_result.deduplicate_unplaced();
1466 final_result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1468
1469 let (b_min, b_max) = boundary.aabb();
1471 let strip_height = b_max[1] - b_min[1]; let mut strip_stats_map: std::collections::HashMap<usize, (f64, f64, usize)> =
1475 std::collections::HashMap::new(); for placement in &final_result.placements {
1478 let strip_idx = placement.boundary_index;
1479 if let Some(geom) = geometries.iter().find(|g| g.id() == &placement.geometry_id) {
1481 use u_nesting_core::geometry::Geometry;
1482 let piece_area = geom.measure();
1483 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1484 let (_g_min, g_max) = geom.aabb_at_rotation(rotation);
1485 let local_x = placement.position[0] - (strip_idx as f64 * strip_width);
1488 let right_edge = local_x + g_max[0];
1489
1490 let entry = strip_stats_map.entry(strip_idx).or_insert((0.0, 0.0, 0));
1491 entry.0 = entry.0.max(right_edge); entry.1 += piece_area; entry.2 += 1; }
1495 }
1496
1497 use u_nesting_core::result::StripStats;
1499 let mut strip_stats: Vec<StripStats> = strip_stats_map
1500 .into_iter()
1501 .map(|(idx, (used_length, piece_area, count))| StripStats {
1502 strip_index: idx,
1503 used_length,
1504 piece_area,
1505 piece_count: count,
1506 strip_width, strip_height, })
1509 .collect();
1510 strip_stats.sort_by_key(|s| s.strip_index);
1511
1512 let total_piece_area: f64 = strip_stats.iter().map(|s| s.piece_area).sum();
1515 let total_material_used: f64 = strip_stats
1516 .iter()
1517 .map(|s| s.strip_height * s.used_length)
1518 .sum();
1519
1520 final_result.strip_stats = strip_stats;
1521 final_result.total_piece_area = total_piece_area;
1522 final_result.total_material_used = total_material_used;
1523
1524 if total_material_used > 0.0 {
1525 final_result.utilization = total_piece_area / total_material_used;
1526 }
1527 final_result.computation_time_ms = start.elapsed_ms();
1528
1529 Ok(final_result)
1530 }
1531}
1532
1533impl Nester2D {
1534 fn solve_reporting(
1538 &self,
1539 geometries: &[Geometry2D],
1540 boundary: &Boundary2D,
1541 progress: Option<ProgressCallback>,
1542 ) -> Result<SolveResult<f64>> {
1543 self.validate(geometries, boundary)?;
1544
1545 self.cancelled.store(false, Ordering::Relaxed);
1547 let start = Timer::now();
1548
1549 let initial_result =
1550 self.run_strategy_reporting(geometries, boundary, progress.as_ref())?;
1551
1552 let mut result = validate_and_filter_placements(
1554 initial_result,
1555 geometries,
1556 boundary,
1557 self.config.margin,
1558 );
1559
1560 result.record_unplaced(geometries);
1564 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1567 result.computation_time_ms = start.elapsed_ms();
1570 Ok(result)
1571 }
1572}
1573
1574impl Solver for Nester2D {
1575 type Geometry = Geometry2D;
1576 type Boundary = Boundary2D;
1577 type Scalar = f64;
1578
1579 fn solve(
1580 &self,
1581 geometries: &[Self::Geometry],
1582 boundary: &Self::Boundary,
1583 ) -> Result<SolveResult<f64>> {
1584 self.solve_reporting(geometries, boundary, None)
1585 }
1586
1587 fn solve_with_progress(
1588 &self,
1589 geometries: &[Self::Geometry],
1590 boundary: &Self::Boundary,
1591 callback: ProgressCallback,
1592 ) -> Result<SolveResult<f64>> {
1593 self.solve_reporting(geometries, boundary, Some(callback))
1594 }
1595
1596 fn cancel(&self) {
1597 self.cancelled.store(true, Ordering::Relaxed);
1598 }
1599}
1600
1601#[cfg(test)]
1602mod tests {
1603 #[cfg(feature = "milp")]
1604 mod milp_budget {
1605 use super::super::{milp_grid_step, milp_time_limit};
1606 use crate::{Boundary2D, Geometry2D};
1607
1608 #[test]
1611 fn an_unlimited_request_does_not_become_a_minute() {
1612 assert_eq!(milp_time_limit(0), u64::MAX / 2);
1613 }
1614
1615 #[test]
1618 fn a_short_request_is_kept_short() {
1619 assert_eq!(milp_time_limit(1_000), 1_000);
1620 assert_eq!(milp_time_limit(250), 250);
1621 }
1622
1623 #[test]
1627 fn the_grid_step_follows_the_smallest_piece() {
1628 let boundary = Boundary2D::rectangle(400.0, 100.0);
1629 let pieces = [
1630 Geometry2D::rectangle("wide", 80.0, 40.0),
1631 Geometry2D::rectangle("small", 20.0, 60.0),
1632 ];
1633 assert_eq!(milp_grid_step(&pieces, &boundary), 5.0);
1635 }
1636
1637 #[test]
1640 fn a_huge_sheet_keeps_the_step_from_getting_too_fine() {
1641 let boundary = Boundary2D::rectangle(100_000.0, 100.0);
1642 let pieces = [Geometry2D::rectangle("small", 4.0, 4.0)];
1643 assert_eq!(milp_grid_step(&pieces, &boundary), 100_000.0 / 256.0);
1644 }
1645
1646 #[test]
1648 fn the_step_never_goes_below_a_unit() {
1649 let boundary = Boundary2D::rectangle(10.0, 10.0);
1650 let pieces = [Geometry2D::rectangle("tiny", 0.5, 0.5)];
1651 assert_eq!(milp_grid_step(&pieces, &boundary), 1.0);
1652 }
1653 }
1654
1655 use super::*;
1656 use crate::placement_utils::polygon_centroid;
1657
1658 #[test]
1662 fn a_geometry_id_given_twice_is_refused_by_every_entry_point() {
1663 let geometries = [
1664 Geometry2D::rectangle("P", 10.0, 10.0),
1665 Geometry2D::rectangle("Q", 10.0, 10.0),
1666 Geometry2D::rectangle("P", 30.0, 5.0),
1667 ];
1668 let boundary = Boundary2D::rectangle(100.0, 100.0);
1669 let nester = Nester2D::default_config();
1670 let errors = [
1671 nester.solve(&geometries, &boundary).expect_err("solve"),
1672 nester
1673 .solve_multi_strip(&geometries, &boundary)
1674 .expect_err("solve_multi_strip"),
1675 nester
1676 .solve_with_progress(&geometries, &boundary, Box::new(|_| {}))
1677 .expect_err("solve_with_progress"),
1678 ];
1679 for err in errors {
1680 let msg = err.to_string();
1681 assert!(msg.contains("'P'"), "{msg}");
1682 assert!(msg.contains("positions 0 and 2"), "{msg}");
1683 }
1684 }
1685
1686 #[test]
1689 fn an_empty_list_of_geometries_is_refused_by_every_entry_point() {
1690 let boundary = Boundary2D::rectangle(100.0, 100.0);
1691 let nester = Nester2D::default_config();
1692 let errors = [
1693 nester.solve(&[], &boundary).expect_err("solve"),
1694 nester
1695 .solve_multi_strip(&[], &boundary)
1696 .expect_err("solve_multi_strip"),
1697 nester
1698 .solve_with_progress(&[], &boundary, Box::new(|_| {}))
1699 .expect_err("solve_with_progress"),
1700 ];
1701 for err in errors {
1702 assert_eq!(
1703 err,
1704 Error::EmptyInput {
1705 parameter: "geometries".into()
1706 }
1707 );
1708 }
1709 }
1710
1711 #[test]
1712 fn the_seed_reaches_the_searches_that_build_their_own_run() {
1713 let seeded = Nester2D::new(Config::default().with_seed(7));
1717 assert_eq!(seeded.gdrr_config(1000).seed, Some(7));
1718 assert_eq!(seeded.alns_config(1000).seed, Some(7));
1719 let unseeded = Nester2D::default_config();
1720 assert_eq!(unseeded.gdrr_config(1000).seed, None);
1721 assert_eq!(unseeded.alns_config(1000).seed, None);
1722 }
1723
1724 #[test]
1729 fn a_set_budget_is_run_and_an_unset_one_is_the_strategy_s() {
1730 assert_eq!((SEARCH_POPULATION, SEARCH_GENERATIONS), (30, 50));
1731 let unset = Nester2D::default_config();
1732 assert_eq!(unset.ga_config().population_size, SEARCH_POPULATION);
1733 assert_eq!(unset.ga_config().max_generations, SEARCH_GENERATIONS);
1734 assert_eq!(unset.brkga_config().population_size, SEARCH_POPULATION);
1735 assert_eq!(unset.brkga_config().max_generations, SEARCH_GENERATIONS);
1736
1737 let set = Nester2D::new(
1738 Config::default()
1739 .with_population_size(100)
1740 .with_max_generations(200),
1741 );
1742 assert_eq!(set.ga_config().population_size, 100);
1743 assert_eq!(set.ga_config().max_generations, 200);
1744 assert_eq!(set.brkga_config().population_size, 100);
1745 assert_eq!(set.brkga_config().max_generations, 200);
1746
1747 let small = Nester2D::new(Config::default().with_population_size(3));
1748 assert_eq!(small.brkga_config().population_size, 3, "was raised to 4");
1749 assert!(small.brkga_config().validate().is_ok());
1750 }
1751
1752 #[test]
1755 fn the_generation_budget_reaches_the_running_search() {
1756 use std::sync::Mutex;
1757 let reports = |config: Config| {
1758 let seen = Arc::new(Mutex::new(Vec::new()));
1759 let sink = seen.clone();
1760 Nester2D::new(
1761 config
1762 .with_strategy(Strategy::GeneticAlgorithm)
1763 .with_seed(7),
1764 )
1765 .solve_with_progress(
1766 &[Geometry2D::rectangle("R", 10.0, 10.0).with_quantity(4)],
1767 &Boundary2D::rectangle(50.0, 50.0),
1768 Box::new(move |info| {
1770 if info.phase == "Genetic Algorithm" {
1771 sink.lock()
1772 .expect("no report panicked")
1773 .push((info.iteration, info.total_iterations))
1774 }
1775 }),
1776 )
1777 .expect("solve");
1778 let seen = seen.lock().expect("no report panicked").clone();
1779 seen
1780 };
1781 let set = reports(Config::default().with_max_generations(80));
1782 assert!(!set.is_empty(), "the search reports its progress");
1783 assert!(
1784 set.iter().all(|&(at, total)| total == 80 && at <= 80),
1785 "{set:?}"
1786 );
1787 let unset = reports(Config::default());
1788 assert!(!unset.is_empty());
1789 assert!(unset.iter().all(|&(_, total)| total == 50), "{unset:?}");
1790 }
1791
1792 #[test]
1800 fn a_progress_callback_does_not_change_the_solve() {
1801 let geometries = [
1802 Geometry2D::rectangle("A", 30.0, 12.0)
1803 .with_quantity(3)
1804 .with_rotations_deg(vec![0.0, 90.0]),
1805 Geometry2D::rectangle("B", 18.0, 18.0).with_quantity(2),
1806 Geometry2D::new("L")
1807 .with_polygon(vec![
1808 (0.0, 0.0),
1809 (20.0, 0.0),
1810 (20.0, 8.0),
1811 (8.0, 8.0),
1812 (8.0, 20.0),
1813 (0.0, 20.0),
1814 ])
1815 .with_quantity(2)
1816 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0]),
1817 ];
1818 let boundary = Boundary2D::rectangle(60.0, 50.0);
1819 type Laid = (String, usize, Vec<f64>, Vec<f64>, bool);
1821 let layout = |r: &SolveResult<f64>| -> Vec<Laid> {
1822 r.placements
1823 .iter()
1824 .map(|p| {
1825 (
1826 p.geometry_id.clone(),
1827 p.instance,
1828 p.position.clone(),
1829 p.rotation.clone(),
1830 p.mirrored,
1831 )
1832 })
1833 .collect()
1834 };
1835 for strategy in [
1836 Strategy::BottomLeftFill,
1837 Strategy::NfpGuided,
1838 Strategy::GeneticAlgorithm,
1839 Strategy::Brkga,
1840 ] {
1841 for time_limit_ms in [0, 60_000] {
1842 let config = Config::default()
1843 .with_strategy(strategy)
1844 .with_seed(11)
1845 .with_population_size(6)
1846 .with_max_generations(4)
1847 .with_time_limit(time_limit_ms);
1848 let plain = Nester2D::new(config.clone())
1849 .solve(&geometries, &boundary)
1850 .expect("solve");
1851 let reported = Nester2D::new(config)
1852 .solve_with_progress(&geometries, &boundary, Box::new(|_| {}))
1853 .expect("solve with progress");
1854 let case = format!("{strategy:?}, time_limit_ms {time_limit_ms}");
1855 assert!(!plain.placements.is_empty(), "{case}");
1856 assert_eq!(layout(&plain), layout(&reported), "{case}");
1857 assert_eq!(plain.utilization, reported.utilization, "{case}");
1858 assert_eq!(plain.generations, reported.generations, "{case}");
1859 assert_eq!(plain.unplaced, reported.unplaced, "{case}");
1860 }
1861 }
1862 }
1863
1864 #[test]
1865 fn test_simple_nesting() {
1866 let geometries = vec![
1867 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1868 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1869 ];
1870
1871 let boundary = Boundary2D::rectangle(100.0, 50.0);
1872 let nester = Nester2D::default_config();
1873
1874 let result = nester.solve(&geometries, &boundary).unwrap();
1875
1876 assert!(result.utilization > 0.0);
1877 assert!(result.placements.len() <= 5); }
1879
1880 #[test]
1881 fn test_placement_within_bounds() {
1882 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1883
1884 let boundary = Boundary2D::rectangle(50.0, 50.0);
1885 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1886 let nester = Nester2D::new(config);
1887
1888 let result = nester.solve(&geometries, &boundary).unwrap();
1889
1890 assert_eq!(result.placements.len(), 4);
1892 assert!(result.unplaced.is_empty());
1893
1894 for p in &result.placements {
1896 assert!(p.position[0] >= 5.0);
1897 assert!(p.position[1] >= 5.0);
1898 }
1899 }
1900
1901 #[test]
1902 fn test_nfp_guided_basic() {
1903 let geometries = vec![
1904 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1905 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1906 ];
1907
1908 let boundary = Boundary2D::rectangle(100.0, 50.0);
1909 let config = Config::default().with_strategy(Strategy::NfpGuided);
1910 let nester = Nester2D::new(config);
1911
1912 let result = nester.solve(&geometries, &boundary).unwrap();
1913
1914 assert!(result.utilization > 0.0);
1915 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1917 }
1918
1919 #[test]
1920 fn test_nfp_guided_with_spacing() {
1921 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1922
1923 let boundary = Boundary2D::rectangle(50.0, 50.0);
1924 let config = Config::default()
1925 .with_strategy(Strategy::NfpGuided)
1926 .with_margin(2.0)
1927 .with_spacing(3.0);
1928 let nester = Nester2D::new(config);
1929
1930 let result = nester.solve(&geometries, &boundary).unwrap();
1931
1932 assert_eq!(result.placements.len(), 4);
1934 assert!(result.unplaced.is_empty());
1935
1936 assert!(result.utilization > 0.0);
1938 }
1939
1940 #[test]
1941 fn test_nfp_guided_no_overlap() {
1942 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1943
1944 let boundary = Boundary2D::rectangle(100.0, 100.0);
1945 let config = Config::default().with_strategy(Strategy::NfpGuided);
1946 let nester = Nester2D::new(config);
1947
1948 let result = nester.solve(&geometries, &boundary).unwrap();
1949
1950 assert_eq!(result.placements.len(), 3);
1951
1952 for i in 0..result.placements.len() {
1954 for j in (i + 1)..result.placements.len() {
1955 let p1 = &result.placements[i];
1956 let p2 = &result.placements[j];
1957
1958 let r1_min_x = p1.position[0];
1960 let r1_max_x = p1.position[0] + 20.0;
1961 let r1_min_y = p1.position[1];
1962 let r1_max_y = p1.position[1] + 20.0;
1963
1964 let r2_min_x = p2.position[0];
1965 let r2_max_x = p2.position[0] + 20.0;
1966 let r2_min_y = p2.position[1];
1967 let r2_max_y = p2.position[1] + 20.0;
1968
1969 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1971 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1972
1973 assert!(
1974 !(overlaps_x && overlaps_y),
1975 "Placements {} and {} overlap",
1976 i,
1977 j
1978 );
1979 }
1980 }
1981 }
1982
1983 fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
1988 for i in 0..a.len() {
1989 let a1 = a[i];
1990 let a2 = a[(i + 1) % a.len()];
1991 for j in 0..b.len() {
1992 let b1 = b[j];
1993 let b2 = b[(j + 1) % b.len()];
1994 if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
1995 return true;
1996 }
1997 }
1998 }
1999 false
2000 }
2001
2002 #[test]
2003 fn test_mirror_candidates_reflects_allow_flip() {
2004 let plain = Geometry2D::rectangle("R", 10.0, 10.0);
2005 assert_eq!(mirror_candidates(&plain), &[false]);
2006
2007 let flippable = Geometry2D::rectangle("R", 10.0, 10.0).with_flip(true);
2008 assert_eq!(mirror_candidates(&flippable), &[false, true]);
2009 }
2010
2011 #[test]
2031 fn test_mirror_aware_placement_avoids_overlap() {
2032 let spacing = 1.0;
2040 let geom = Geometry2D::l_shape("L", 30.0, 20.0, 20.0, 10.0);
2041 let boundary_polygon = vec![(0.0, 0.0), (65.0, 0.0), (65.0, 45.0), (0.0, 45.0)];
2042 let cache = NfpCache::new();
2043
2044 let ifp1 =
2046 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, false).unwrap();
2047 let (x1, y1) = find_bottom_left_placement(&ifp1, &[], 1.0, PackingAxis::X)
2048 .expect("first piece must fit");
2049 let placed1 = PlacedGeometry::new(geom.clone(), (x1, y1), 0.0).with_mirrored(false);
2050
2051 let ifp2 =
2053 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, true).unwrap();
2054 let nfp_at_origin = cache
2055 .get_or_compute_mirrored(("L", "L", 0.0, false, true), || {
2056 compute_nfp_mirrored(&placed1.geometry, &geom, 0.0, false, true)
2057 })
2058 .unwrap();
2059 let translated_nfp = translate_nfp(&nfp_at_origin, placed1.position);
2060 let expanded_nfp = offset_nfp(&translated_nfp, spacing);
2061 let (x2, y2) = find_bottom_left_placement(&ifp2, &[&expanded_nfp], 1.0, PackingAxis::X)
2062 .expect("mirrored second piece must fit avoiding the first");
2063 let placed2 = PlacedGeometry::new(geom.clone(), (x2, y2), 0.0).with_mirrored(true);
2064
2065 assert_ne!(
2066 (x1, y1),
2067 (x2, y2),
2068 "mirrored placement must actually avoid the first piece's position"
2069 );
2070 assert!(
2071 !polygons_overlap(
2072 &placed1.translated_exterior(),
2073 &placed2.translated_exterior()
2074 ),
2075 "unmirrored piece at {:?} and mirrored piece at {:?} must not overlap",
2076 (x1, y1),
2077 (x2, y2)
2078 );
2079 }
2080
2081 #[test]
2082 fn test_nfp_guided_utilization() {
2083 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
2085
2086 let boundary = Boundary2D::rectangle(100.0, 50.0);
2087 let config = Config::default().with_strategy(Strategy::NfpGuided);
2088 let nester = Nester2D::new(config);
2089
2090 let result = nester.solve(&geometries, &boundary).unwrap();
2091
2092 assert_eq!(result.placements.len(), 4);
2094
2095 assert!(result.utilization > 0.45);
2097 }
2098
2099 #[test]
2100 fn test_polygon_centroid() {
2101 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
2103 let (cx, cy) = polygon_centroid(&square);
2104 assert!((cx - 5.0).abs() < 0.01);
2105 assert!((cy - 5.0).abs() < 0.01);
2106
2107 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
2108 let (cx, cy) = polygon_centroid(&triangle);
2109 assert!((cx - 3.0).abs() < 0.01);
2110 assert!((cy - 2.0).abs() < 0.01);
2111 }
2112
2113 #[test]
2114 fn test_ga_strategy_basic() {
2115 let geometries = vec![
2116 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2117 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2118 ];
2119
2120 let boundary = Boundary2D::rectangle(100.0, 50.0);
2121 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
2122 let nester = Nester2D::new(config);
2123
2124 let result = nester.solve(&geometries, &boundary).unwrap();
2125
2126 assert!(result.utilization > 0.0);
2127 assert!(!result.placements.is_empty());
2128 assert!(result.generations.is_some());
2130 assert!(result.best_fitness.is_some());
2131 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
2132 }
2133
2134 #[test]
2135 fn test_ga_strategy_all_placed() {
2136 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2138
2139 let boundary = Boundary2D::rectangle(100.0, 100.0);
2140 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
2141 let nester = Nester2D::new(config);
2142
2143 let result = nester.solve(&geometries, &boundary).unwrap();
2144
2145 assert_eq!(result.placements.len(), 4);
2147 assert!(result.unplaced.is_empty());
2148 }
2149
2150 #[test]
2151 fn test_brkga_strategy_basic() {
2152 let geometries = vec![
2153 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2154 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2155 ];
2156
2157 let boundary = Boundary2D::rectangle(100.0, 50.0);
2158 let config = Config::default().with_strategy(Strategy::Brkga);
2159 let nester = Nester2D::new(config);
2160
2161 let result = nester.solve(&geometries, &boundary).unwrap();
2162
2163 assert!(result.utilization > 0.0);
2164 assert!(!result.placements.is_empty());
2165 assert!(result.generations.is_some());
2167 assert!(result.best_fitness.is_some());
2168 assert!(result.strategy == Some("BRKGA".to_string()));
2169 }
2170
2171 #[test]
2172 fn test_brkga_strategy_all_placed() {
2173 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2175
2176 let boundary = Boundary2D::rectangle(100.0, 100.0);
2177 let config = Config::default()
2179 .with_strategy(Strategy::Brkga)
2180 .with_time_limit(30000); let nester = Nester2D::new(config);
2182
2183 let result = nester.solve(&geometries, &boundary).unwrap();
2184
2185 assert!(
2188 result.placements.len() >= 3,
2189 "Expected at least 3 placements, got {}",
2190 result.placements.len()
2191 );
2192 }
2193
2194 #[test]
2195 fn test_gdrr_strategy_basic() {
2196 let geometries = vec![
2197 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2198 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2199 ];
2200
2201 let boundary = Boundary2D::rectangle(100.0, 50.0);
2202 let config = Config::default().with_strategy(Strategy::Gdrr);
2203 let nester = Nester2D::new(config);
2204
2205 let result = nester.solve(&geometries, &boundary).unwrap();
2206
2207 assert!(result.utilization > 0.0);
2208 assert!(!result.placements.is_empty());
2209 assert!(result.iterations.is_some());
2211 assert!(result.best_fitness.is_some());
2212 assert!(result.strategy == Some("GDRR".to_string()));
2213 }
2214
2215 #[test]
2216 fn test_gdrr_strategy_all_placed() {
2217 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2219
2220 let boundary = Boundary2D::rectangle(100.0, 100.0);
2221 let config = Config::default().with_strategy(Strategy::Gdrr);
2222 let nester = Nester2D::new(config);
2223
2224 let result = nester.solve(&geometries, &boundary).unwrap();
2225
2226 assert_eq!(result.placements.len(), 4);
2228 assert!(result.unplaced.is_empty());
2229 }
2230
2231 #[test]
2232 fn test_alns_strategy_basic() {
2233 let geometries = vec![
2234 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2235 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2236 ];
2237
2238 let boundary = Boundary2D::rectangle(100.0, 50.0);
2239 let config = Config::default().with_strategy(Strategy::Alns);
2240 let nester = Nester2D::new(config);
2241
2242 let result = nester.solve(&geometries, &boundary).unwrap();
2243
2244 assert!(result.utilization > 0.0);
2245 assert!(!result.placements.is_empty());
2246 assert!(result.iterations.is_some());
2248 assert!(result.best_fitness.is_some());
2249 assert!(result.strategy == Some("ALNS".to_string()));
2250 }
2251
2252 #[test]
2253 fn test_alns_strategy_all_placed() {
2254 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2256
2257 let boundary = Boundary2D::rectangle(100.0, 100.0);
2258 let config = Config::default().with_strategy(Strategy::Alns);
2259 let nester = Nester2D::new(config);
2260
2261 let result = nester.solve(&geometries, &boundary).unwrap();
2262
2263 assert_eq!(result.placements.len(), 4);
2265 assert!(result.unplaced.is_empty());
2266 }
2267
2268 #[test]
2269 fn test_blf_rotation_optimization() {
2270 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
2273 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
2275
2276 let boundary = Boundary2D::rectangle(35.0, 95.0);
2279 let nester = Nester2D::default_config();
2280
2281 let result = nester.solve(&geometries, &boundary).unwrap();
2282
2283 assert_eq!(
2285 result.placements.len(),
2286 3,
2287 "All pieces should be placed with rotation optimization"
2288 );
2289 assert!(result.unplaced.is_empty());
2290 }
2291
2292 #[test]
2293 fn test_blf_selects_best_rotation() {
2294 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
2296 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
2298
2299 let boundary = Boundary2D::rectangle(45.0, 50.0);
2303 let nester = Nester2D::default_config();
2304
2305 let result = nester.solve(&geometries, &boundary).unwrap();
2306
2307 assert_eq!(result.placements.len(), 2);
2308 assert!(result.unplaced.is_empty());
2309 }
2310
2311 #[test]
2312 fn test_progress_callback_blf() {
2313 use std::sync::atomic::{AtomicUsize, Ordering};
2314 use std::sync::Arc;
2315
2316 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2317 let boundary = Boundary2D::rectangle(50.0, 50.0);
2318 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2319 let nester = Nester2D::new(config);
2320
2321 let callback_count = Arc::new(AtomicUsize::new(0));
2322 let callback_count_clone = callback_count.clone();
2323 let last_items_placed = Arc::new(AtomicUsize::new(0));
2324 let last_items_placed_clone = last_items_placed.clone();
2325
2326 let callback: ProgressCallback = Box::new(move |info| {
2327 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2328 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
2329 });
2330
2331 let result = nester
2332 .solve_with_progress(&geometries, &boundary, callback)
2333 .unwrap();
2334
2335 let count = callback_count.load(Ordering::Relaxed);
2337 assert!(
2338 count >= 5,
2339 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
2340 count
2341 );
2342
2343 let final_placed = last_items_placed.load(Ordering::Relaxed);
2345 assert_eq!(final_placed, 4, "Should report 4 items placed");
2346
2347 assert_eq!(result.placements.len(), 4);
2349 }
2350
2351 #[test]
2352 fn test_progress_callback_nfp() {
2353 use std::sync::atomic::{AtomicUsize, Ordering};
2354 use std::sync::Arc;
2355
2356 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
2357 let boundary = Boundary2D::rectangle(50.0, 50.0);
2358 let config = Config::default().with_strategy(Strategy::NfpGuided);
2359 let nester = Nester2D::new(config);
2360
2361 let callback_count = Arc::new(AtomicUsize::new(0));
2362 let callback_count_clone = callback_count.clone();
2363
2364 let callback: ProgressCallback = Box::new(move |info| {
2365 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2366 assert!(info.items_placed <= info.total_items);
2367 });
2368
2369 let result = nester
2370 .solve_with_progress(&geometries, &boundary, callback)
2371 .unwrap();
2372
2373 let count = callback_count.load(Ordering::Relaxed);
2375 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
2376
2377 assert_eq!(result.placements.len(), 2);
2379 }
2380
2381 #[test]
2382 fn test_time_limit_honored() {
2383 let geometries: Vec<Geometry2D> = (0..100)
2385 .map(|i| Geometry2D::rectangle(format!("R{}", i), 5.0, 5.0))
2386 .collect();
2387 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
2388
2389 let config = Config::default()
2391 .with_strategy(Strategy::BottomLeftFill)
2392 .with_time_limit(1);
2393 let nester = Nester2D::new(config);
2394
2395 let result = nester.solve(&geometries, &boundary).unwrap();
2396
2397 assert!(
2400 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
2402 result.computation_time_ms
2403 );
2404 }
2405
2406 #[test]
2407 fn test_time_limit_zero_unlimited() {
2408 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2410 let boundary = Boundary2D::rectangle(50.0, 50.0);
2411
2412 let config = Config::default()
2413 .with_strategy(Strategy::BottomLeftFill)
2414 .with_time_limit(0); let nester = Nester2D::new(config);
2416
2417 let result = nester.solve(&geometries, &boundary).unwrap();
2418
2419 assert_eq!(result.placements.len(), 4);
2421 }
2422
2423 #[test]
2424 fn test_blf_bounds_clamping() {
2425 let gear_like = Geometry2D::new("gear")
2429 .with_polygon(vec![
2430 (50.0, 5.0), (65.0, 15.0),
2432 (77.0, 18.0),
2433 (80.0, 32.0),
2434 (95.0, 50.0), (80.0, 68.0),
2436 (77.0, 82.0),
2437 (65.0, 85.0),
2438 (50.0, 95.0), (35.0, 85.0),
2440 (23.0, 82.0),
2441 (20.0, 68.0),
2442 (5.0, 50.0), (20.0, 32.0),
2444 (23.0, 18.0),
2445 (35.0, 15.0),
2446 ])
2447 .with_quantity(1);
2448
2449 let boundary = Boundary2D::rectangle(100.0, 100.0);
2451
2452 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2453 let nester = Nester2D::new(config);
2454
2455 let result = nester
2456 .solve(std::slice::from_ref(&gear_like), &boundary)
2457 .unwrap();
2458
2459 assert_eq!(result.placements.len(), 1);
2460 let placement = &result.placements[0];
2461
2462 let origin_x = placement.position[0];
2464 let origin_y = placement.position[1];
2465
2466 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2468
2469 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2471
2472 let actual_min_x = origin_x + g_min[0];
2474 let actual_max_x = origin_x + g_max[0];
2475 let actual_min_y = origin_y + g_min[1];
2476 let actual_max_y = origin_y + g_max[1];
2477
2478 assert!(
2480 actual_min_x >= 0.0,
2481 "Left edge {} should be >= 0",
2482 actual_min_x
2483 );
2484 assert!(
2485 actual_max_x <= 100.0,
2486 "Right edge {} should be <= 100",
2487 actual_max_x
2488 );
2489 assert!(
2490 actual_min_y >= 0.0,
2491 "Bottom edge {} should be >= 0",
2492 actual_min_y
2493 );
2494 assert!(
2495 actual_max_y <= 100.0,
2496 "Top edge {} should be <= 100",
2497 actual_max_y
2498 );
2499 }
2500
2501 #[test]
2502 fn test_blf_bounds_clamping_many_pieces() {
2503 let gear_like = Geometry2D::new("gear")
2506 .with_polygon(vec![
2507 (50.0, 5.0),
2508 (65.0, 15.0),
2509 (77.0, 18.0),
2510 (80.0, 32.0),
2511 (95.0, 50.0),
2512 (80.0, 68.0),
2513 (77.0, 82.0),
2514 (65.0, 85.0),
2515 (50.0, 95.0),
2516 (35.0, 85.0),
2517 (23.0, 82.0),
2518 (20.0, 68.0),
2519 (5.0, 50.0),
2520 (20.0, 32.0),
2521 (23.0, 18.0),
2522 (35.0, 15.0),
2523 ])
2524 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
2528
2529 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2530 let nester = Nester2D::new(config);
2531
2532 let result = nester
2533 .solve(std::slice::from_ref(&gear_like), &boundary)
2534 .unwrap();
2535
2536 for (i, placement) in result.placements.iter().enumerate() {
2538 let origin_x = placement.position[0];
2539 let origin_y = placement.position[1];
2540 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2541
2542 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2543
2544 let actual_min_x = origin_x + g_min[0];
2545 let actual_max_x = origin_x + g_max[0];
2546 let actual_min_y = origin_y + g_min[1];
2547 let actual_max_y = origin_y + g_max[1];
2548
2549 assert!(
2550 actual_min_x >= -0.01,
2551 "Piece {}: Left edge {} should be >= 0",
2552 i,
2553 actual_min_x
2554 );
2555 assert!(
2556 actual_max_x <= 500.01,
2557 "Piece {}: Right edge {} should be <= 500",
2558 i,
2559 actual_max_x
2560 );
2561 assert!(
2562 actual_min_y >= -0.01,
2563 "Piece {}: Bottom edge {} should be >= 0",
2564 i,
2565 actual_min_y
2566 );
2567 assert!(
2568 actual_max_y <= 500.01,
2569 "Piece {}: Top edge {} should be <= 500",
2570 i,
2571 actual_max_y
2572 );
2573 }
2574 }
2575
2576 #[test]
2577 fn test_blf_bounds_trace() {
2578 let gear = Geometry2D::new("gear").with_polygon(vec![
2580 (50.0, 5.0),
2581 (65.0, 15.0),
2582 (77.0, 18.0),
2583 (80.0, 32.0),
2584 (95.0, 50.0),
2585 (80.0, 68.0),
2586 (77.0, 82.0),
2587 (65.0, 85.0),
2588 (50.0, 95.0),
2589 (35.0, 85.0),
2590 (23.0, 82.0),
2591 (20.0, 68.0),
2592 (5.0, 50.0),
2593 (20.0, 32.0),
2594 (23.0, 18.0),
2595 (35.0, 15.0),
2596 ]);
2597
2598 let (g_min, g_max) = gear.aabb();
2600 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
2601 assert!(
2602 (g_min[0] - 5.0).abs() < 0.01,
2603 "g_min[0] should be 5, got {}",
2604 g_min[0]
2605 );
2606 assert!(
2607 (g_max[0] - 95.0).abs() < 0.01,
2608 "g_max[0] should be 95, got {}",
2609 g_max[0]
2610 );
2611
2612 let b_max_x = 500.0;
2614 let margin = 0.0;
2615 let max_valid_x = b_max_x - margin - g_max[0];
2616 println!(
2617 "max_valid_x = {} - {} - {} = {}",
2618 b_max_x, margin, g_max[0], max_valid_x
2619 );
2620 assert!(
2621 (max_valid_x - 405.0).abs() < 0.01,
2622 "max_valid_x should be 405, got {}",
2623 max_valid_x
2624 );
2625
2626 let boundary = Boundary2D::rectangle(500.0, 500.0);
2628 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2629 let nester = Nester2D::new(config);
2630
2631 let result = nester
2632 .solve(&[gear.clone().with_quantity(1)], &boundary)
2633 .unwrap();
2634
2635 assert_eq!(result.placements.len(), 1);
2636 let p = &result.placements[0];
2637 let origin_x = p.position[0];
2638 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2639
2640 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2641 let actual_max_x = origin_x + g_max_r[0];
2642
2643 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2644 println!(
2645 "At rotation {}: g_min={:?}, g_max={:?}",
2646 rotation, g_min_r, g_max_r
2647 );
2648 println!(
2649 "Actual max x: {} + {} = {}",
2650 origin_x, g_max_r[0], actual_max_x
2651 );
2652
2653 assert!(
2654 actual_max_x <= 500.01,
2655 "Geometry exceeds boundary: max_x={} > 500",
2656 actual_max_x
2657 );
2658 }
2659
2660 #[test]
2661 fn test_blf_bounds_many_pieces_direct() {
2662 let gear = Geometry2D::new("gear")
2664 .with_polygon(vec![
2665 (50.0, 5.0),
2666 (65.0, 15.0),
2667 (77.0, 18.0),
2668 (80.0, 32.0),
2669 (95.0, 50.0),
2670 (80.0, 68.0),
2671 (77.0, 82.0),
2672 (65.0, 85.0),
2673 (50.0, 95.0),
2674 (35.0, 85.0),
2675 (23.0, 82.0),
2676 (20.0, 68.0),
2677 (5.0, 50.0),
2678 (20.0, 32.0),
2679 (23.0, 18.0),
2680 (35.0, 15.0),
2681 ])
2682 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2685 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2686 let nester = Nester2D::new(config);
2687
2688 let result = nester
2689 .solve(std::slice::from_ref(&gear), &boundary)
2690 .unwrap();
2691
2692 println!("Placed {} pieces", result.placements.len());
2693
2694 for (i, p) in result.placements.iter().enumerate() {
2696 let origin_x = p.position[0];
2697 let origin_y = p.position[1];
2698 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2699
2700 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2701
2702 let actual_min_x = origin_x + g_min_r[0];
2703 let actual_max_x = origin_x + g_max_r[0];
2704 let actual_min_y = origin_y + g_min_r[1];
2705 let actual_max_y = origin_y + g_max_r[1];
2706
2707 println!(
2708 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2709 i,
2710 origin_x,
2711 origin_y,
2712 rotation,
2713 actual_min_x,
2714 actual_max_x,
2715 actual_min_y,
2716 actual_max_y
2717 );
2718
2719 assert!(
2720 actual_max_x <= 500.01,
2721 "Piece {}: Right edge {} > 500",
2722 i,
2723 actual_max_x
2724 );
2725 assert!(
2726 actual_max_y <= 500.01,
2727 "Piece {}: Top edge {} > 500",
2728 i,
2729 actual_max_y
2730 );
2731 }
2732 }
2733
2734 #[test]
2735 fn test_blf_bounds_multi_strip() {
2736 let gear = Geometry2D::new("gear")
2738 .with_polygon(vec![
2739 (50.0, 5.0),
2740 (65.0, 15.0),
2741 (77.0, 18.0),
2742 (80.0, 32.0),
2743 (95.0, 50.0),
2744 (80.0, 68.0),
2745 (77.0, 82.0),
2746 (65.0, 85.0),
2747 (50.0, 95.0),
2748 (35.0, 85.0),
2749 (23.0, 82.0),
2750 (20.0, 68.0),
2751 (5.0, 50.0),
2752 (20.0, 32.0),
2753 (23.0, 18.0),
2754 (35.0, 15.0),
2755 ])
2756 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2759 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2760 let nester = Nester2D::new(config);
2761
2762 let result = nester
2764 .solve_multi_strip(std::slice::from_ref(&gear), &boundary)
2765 .unwrap();
2766
2767 println!(
2768 "Placed {} pieces across {} strips",
2769 result.placements.len(),
2770 result.boundaries_used
2771 );
2772
2773 let strip_width = 500.0;
2775 for (i, p) in result.placements.iter().enumerate() {
2776 let origin_x = p.position[0];
2777 let origin_y = p.position[1];
2778 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2779 let strip_idx = p.boundary_index;
2780
2781 let local_x = origin_x - (strip_idx as f64 * strip_width);
2783
2784 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2785
2786 let local_max_x = local_x + g_max_r[0];
2787 let local_max_y = origin_y + g_max_r[1];
2788
2789 println!(
2790 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2791 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2792 );
2793
2794 assert!(
2795 local_max_x <= 500.01,
2796 "Piece {}: In strip {}, local right edge {:.1} > 500",
2797 i,
2798 strip_idx,
2799 local_max_x
2800 );
2801 assert!(
2802 local_max_y <= 500.01,
2803 "Piece {}: Top edge {:.1} > 500",
2804 i,
2805 local_max_y
2806 );
2807 }
2808 }
2809
2810 #[test]
2811 fn test_blf_bounds_mixed_shapes() {
2812 let shapes = vec![
2814 Geometry2D::new("shape0")
2816 .with_polygon(vec![
2817 (0.0, 0.0),
2818 (180.0, 0.0),
2819 (195.0, 15.0),
2820 (200.0, 50.0),
2821 (200.0, 150.0),
2822 (195.0, 185.0),
2823 (180.0, 200.0),
2824 (20.0, 200.0),
2825 (5.0, 185.0),
2826 (0.0, 150.0),
2827 (0.0, 50.0),
2828 (5.0, 15.0),
2829 ])
2830 .with_quantity(2),
2831 Geometry2D::new("shape1")
2833 .with_polygon(vec![
2834 (60.0, 0.0),
2835 (85.0, 7.0),
2836 (104.0, 25.0),
2837 (118.0, 50.0),
2838 (120.0, 60.0),
2839 (118.0, 70.0),
2840 (104.0, 95.0),
2841 (85.0, 113.0),
2842 (60.0, 120.0),
2843 (35.0, 113.0),
2844 (16.0, 95.0),
2845 (2.0, 70.0),
2846 (0.0, 60.0),
2847 (2.0, 50.0),
2848 (16.0, 25.0),
2849 (35.0, 7.0),
2850 ])
2851 .with_quantity(4),
2852 Geometry2D::new("shape2")
2854 .with_polygon(vec![
2855 (0.0, 0.0),
2856 (80.0, 0.0),
2857 (80.0, 20.0),
2858 (20.0, 20.0),
2859 (20.0, 80.0),
2860 (0.0, 80.0),
2861 ])
2862 .with_quantity(6),
2863 Geometry2D::new("shape3")
2865 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2866 .with_quantity(6),
2867 Geometry2D::new("shape4")
2869 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2870 .with_quantity(4),
2871 Geometry2D::new("shape5")
2873 .with_polygon(vec![
2874 (15.0, 0.0),
2875 (45.0, 0.0),
2876 (60.0, 26.0),
2877 (45.0, 52.0),
2878 (15.0, 52.0),
2879 (0.0, 26.0),
2880 ])
2881 .with_quantity(8),
2882 Geometry2D::new("shape6")
2884 .with_polygon(vec![
2885 (0.0, 0.0),
2886 (90.0, 0.0),
2887 (90.0, 12.0),
2888 (55.0, 12.0),
2889 (55.0, 60.0),
2890 (35.0, 60.0),
2891 (35.0, 12.0),
2892 (0.0, 12.0),
2893 ])
2894 .with_quantity(4),
2895 Geometry2D::new("shape7")
2897 .with_polygon(vec![
2898 (0.0, 10.0),
2899 (10.0, 0.0),
2900 (70.0, 0.0),
2901 (80.0, 10.0),
2902 (80.0, 70.0),
2903 (70.0, 80.0),
2904 (10.0, 80.0),
2905 (0.0, 70.0),
2906 ])
2907 .with_quantity(3),
2908 Geometry2D::new("shape8_gear")
2910 .with_polygon(vec![
2911 (50.0, 5.0),
2912 (65.0, 15.0),
2913 (77.0, 18.0),
2914 (80.0, 32.0),
2915 (95.0, 50.0),
2916 (80.0, 68.0),
2917 (77.0, 82.0),
2918 (65.0, 85.0),
2919 (50.0, 95.0),
2920 (35.0, 85.0),
2921 (23.0, 82.0),
2922 (20.0, 68.0),
2923 (5.0, 50.0),
2924 (20.0, 32.0),
2925 (23.0, 18.0),
2926 (35.0, 15.0),
2927 ])
2928 .with_quantity(13),
2929 ];
2930
2931 let boundary = Boundary2D::rectangle(500.0, 500.0);
2933 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2934 let nester = Nester2D::new(config);
2935
2936 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2937
2938 println!(
2939 "Placed {} pieces across {} strips",
2940 result.placements.len(),
2941 result.boundaries_used
2942 );
2943
2944 let strip_width = 500.0;
2946 let gear_aabb = shapes[8].aabb();
2947 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2948
2949 let mut violations = Vec::new();
2950 for p in &result.placements {
2951 if p.geometry_id.as_str().starts_with("shape8") {
2952 let origin_x = p.position[0];
2953 let _origin_y = p.position[1];
2954 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2955 let strip_idx = p.boundary_index;
2956 let local_x = origin_x - (strip_idx as f64 * strip_width);
2957
2958 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2959 let local_max_x = local_x + g_max_r[0];
2960
2961 println!(
2962 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2963 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2964 );
2965
2966 if local_max_x > 500.01 {
2967 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2968 }
2969 }
2970 }
2971
2972 assert!(
2973 violations.is_empty(),
2974 "Found {} Gear pieces exceeding boundary: {:?}",
2975 violations.len(),
2976 violations
2977 );
2978 }
2979
2980 #[test]
2981 fn test_blf_bounds_expanded_like_benchmark() {
2982 type ShapeDef = (Vec<(f64, f64)>, usize, Vec<f64>);
2986 let shape_defs: Vec<ShapeDef> = vec![
2987 (
2988 vec![
2989 (0.0, 0.0),
2990 (180.0, 0.0),
2991 (195.0, 15.0),
2992 (200.0, 50.0),
2993 (200.0, 150.0),
2994 (195.0, 185.0),
2995 (180.0, 200.0),
2996 (20.0, 200.0),
2997 (5.0, 185.0),
2998 (0.0, 150.0),
2999 (0.0, 50.0),
3000 (5.0, 15.0),
3001 ],
3002 2,
3003 vec![0.0, 90.0, 180.0, 270.0],
3004 ),
3005 (
3006 vec![
3007 (60.0, 0.0),
3008 (85.0, 7.0),
3009 (104.0, 25.0),
3010 (118.0, 50.0),
3011 (120.0, 60.0),
3012 (118.0, 70.0),
3013 (104.0, 95.0),
3014 (85.0, 113.0),
3015 (60.0, 120.0),
3016 (35.0, 113.0),
3017 (16.0, 95.0),
3018 (2.0, 70.0),
3019 (0.0, 60.0),
3020 (2.0, 50.0),
3021 (16.0, 25.0),
3022 (35.0, 7.0),
3023 ],
3024 4,
3025 vec![0.0, 45.0, 90.0, 135.0],
3026 ),
3027 (
3028 vec![
3029 (0.0, 0.0),
3030 (80.0, 0.0),
3031 (80.0, 20.0),
3032 (20.0, 20.0),
3033 (20.0, 80.0),
3034 (0.0, 80.0),
3035 ],
3036 6,
3037 vec![0.0, 90.0, 180.0, 270.0],
3038 ),
3039 (
3040 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
3041 6,
3042 vec![0.0, 90.0, 180.0, 270.0],
3043 ),
3044 (
3045 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
3046 4,
3047 vec![0.0, 90.0],
3048 ),
3049 (
3050 vec![
3051 (15.0, 0.0),
3052 (45.0, 0.0),
3053 (60.0, 26.0),
3054 (45.0, 52.0),
3055 (15.0, 52.0),
3056 (0.0, 26.0),
3057 ],
3058 8,
3059 vec![0.0, 60.0, 120.0],
3060 ),
3061 (
3062 vec![
3063 (0.0, 0.0),
3064 (90.0, 0.0),
3065 (90.0, 12.0),
3066 (55.0, 12.0),
3067 (55.0, 60.0),
3068 (35.0, 60.0),
3069 (35.0, 12.0),
3070 (0.0, 12.0),
3071 ],
3072 4,
3073 vec![0.0, 90.0, 180.0, 270.0],
3074 ),
3075 (
3076 vec![
3077 (0.0, 10.0),
3078 (10.0, 0.0),
3079 (70.0, 0.0),
3080 (80.0, 10.0),
3081 (80.0, 70.0),
3082 (70.0, 80.0),
3083 (10.0, 80.0),
3084 (0.0, 70.0),
3085 ],
3086 3,
3087 vec![0.0, 90.0],
3088 ),
3089 (
3091 vec![
3092 (50.0, 5.0),
3093 (65.0, 15.0),
3094 (77.0, 18.0),
3095 (80.0, 32.0),
3096 (95.0, 50.0),
3097 (80.0, 68.0),
3098 (77.0, 82.0),
3099 (65.0, 85.0),
3100 (50.0, 95.0),
3101 (35.0, 85.0),
3102 (23.0, 82.0),
3103 (20.0, 68.0),
3104 (5.0, 50.0),
3105 (20.0, 32.0),
3106 (23.0, 18.0),
3107 (35.0, 15.0),
3108 ],
3109 13,
3110 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
3111 ),
3112 ];
3113
3114 let mut geometries = Vec::new();
3116 let mut piece_id = 0;
3117 for (vertices, demand, rotations) in shape_defs.iter() {
3118 for _ in 0..*demand {
3119 let geom = Geometry2D::new(format!("piece_{}", piece_id))
3120 .with_polygon(vertices.clone())
3121 .with_rotations_deg(rotations.clone());
3122 geometries.push(geom);
3123 piece_id += 1;
3124 }
3125 }
3126
3127 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
3129 let (gear_min, gear_max) = gear_geom.aabb();
3130 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
3131
3132 let boundary = Boundary2D::rectangle(500.0, 500.0);
3133 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3134 let nester = Nester2D::new(config);
3135
3136 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3137
3138 println!(
3139 "Placed {} pieces across {} strips",
3140 result.placements.len(),
3141 result.boundaries_used
3142 );
3143
3144 let strip_width = 500.0;
3146 let mut violations = Vec::new();
3147
3148 for p in &result.placements {
3149 let id_num: usize = p
3150 .geometry_id
3151 .as_str()
3152 .strip_prefix("piece_")
3153 .and_then(|s| s.parse().ok())
3154 .unwrap_or(0);
3155
3156 if (37..=49).contains(&id_num) {
3158 let origin_x = p.position[0];
3159 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3160 let strip_idx = p.boundary_index;
3161 let local_x = origin_x - (strip_idx as f64 * strip_width);
3162
3163 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
3164 let local_max_x = local_x + g_max_r[0];
3165
3166 println!(
3167 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
3168 p.geometry_id, strip_idx, local_x, rotation, local_max_x
3169 );
3170
3171 if local_max_x > 500.01 {
3172 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
3173 }
3174 }
3175 }
3176
3177 assert!(
3178 violations.is_empty(),
3179 "Found {} Gear pieces exceeding boundary: {:?}",
3180 violations.len(),
3181 violations
3182 );
3183 }
3184
3185 fn aabbs_overlap(
3187 a_min: [f64; 2],
3188 a_max: [f64; 2],
3189 b_min: [f64; 2],
3190 b_max: [f64; 2],
3191 tolerance: f64,
3192 ) -> bool {
3193 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
3195 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
3196 x_overlap && y_overlap
3197 }
3198
3199 #[test]
3201 fn test_all_strategies_boundary_and_overlap() {
3202 use std::collections::HashMap;
3203
3204 let shapes = vec![
3206 Geometry2D::new("shape0")
3207 .with_polygon(vec![
3208 (0.0, 0.0),
3209 (180.0, 0.0),
3210 (195.0, 15.0),
3211 (200.0, 50.0),
3212 (200.0, 150.0),
3213 (195.0, 185.0),
3214 (180.0, 200.0),
3215 (20.0, 200.0),
3216 (5.0, 185.0),
3217 (0.0, 150.0),
3218 (0.0, 50.0),
3219 (5.0, 15.0),
3220 ])
3221 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3222 .with_quantity(2),
3223 Geometry2D::new("shape1_flange")
3224 .with_polygon(vec![
3225 (60.0, 0.0),
3226 (85.0, 7.0),
3227 (104.0, 25.0),
3228 (118.0, 50.0),
3229 (120.0, 60.0),
3230 (118.0, 70.0),
3231 (104.0, 95.0),
3232 (85.0, 113.0),
3233 (60.0, 120.0),
3234 (35.0, 113.0),
3235 (16.0, 95.0),
3236 (2.0, 70.0),
3237 (0.0, 60.0),
3238 (2.0, 50.0),
3239 (16.0, 25.0),
3240 (35.0, 7.0),
3241 ])
3242 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
3243 .with_quantity(4),
3244 Geometry2D::new("shape2_lbracket")
3245 .with_polygon(vec![
3246 (0.0, 0.0),
3247 (80.0, 0.0),
3248 (80.0, 20.0),
3249 (20.0, 20.0),
3250 (20.0, 80.0),
3251 (0.0, 80.0),
3252 ])
3253 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3254 .with_quantity(6),
3255 Geometry2D::new("shape3_triangle")
3256 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
3257 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3258 .with_quantity(6),
3259 Geometry2D::new("shape4_rect")
3260 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
3261 .with_rotations_deg(vec![0.0, 90.0])
3262 .with_quantity(4),
3263 Geometry2D::new("shape5_hexagon")
3264 .with_polygon(vec![
3265 (15.0, 0.0),
3266 (45.0, 0.0),
3267 (60.0, 26.0),
3268 (45.0, 52.0),
3269 (15.0, 52.0),
3270 (0.0, 26.0),
3271 ])
3272 .with_rotations_deg(vec![0.0, 60.0, 120.0])
3273 .with_quantity(8),
3274 Geometry2D::new("shape6_tstiff")
3275 .with_polygon(vec![
3276 (0.0, 0.0),
3277 (90.0, 0.0),
3278 (90.0, 12.0),
3279 (55.0, 12.0),
3280 (55.0, 60.0),
3281 (35.0, 60.0),
3282 (35.0, 12.0),
3283 (0.0, 12.0),
3284 ])
3285 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3286 .with_quantity(4),
3287 Geometry2D::new("shape7_mount")
3288 .with_polygon(vec![
3289 (0.0, 10.0),
3290 (10.0, 0.0),
3291 (70.0, 0.0),
3292 (80.0, 10.0),
3293 (80.0, 70.0),
3294 (70.0, 80.0),
3295 (10.0, 80.0),
3296 (0.0, 70.0),
3297 ])
3298 .with_rotations_deg(vec![0.0, 90.0])
3299 .with_quantity(3),
3300 Geometry2D::new("shape8_gear")
3301 .with_polygon(vec![
3302 (50.0, 5.0),
3303 (65.0, 15.0),
3304 (77.0, 18.0),
3305 (80.0, 32.0),
3306 (95.0, 50.0),
3307 (80.0, 68.0),
3308 (77.0, 82.0),
3309 (65.0, 85.0),
3310 (50.0, 95.0),
3311 (35.0, 85.0),
3312 (23.0, 82.0),
3313 (20.0, 68.0),
3314 (5.0, 50.0),
3315 (20.0, 32.0),
3316 (23.0, 18.0),
3317 (35.0, 15.0),
3318 ])
3319 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
3320 .with_quantity(13),
3321 ];
3322
3323 let geom_map: HashMap<String, &Geometry2D> =
3325 shapes.iter().map(|g| (g.id().clone(), g)).collect();
3326
3327 let boundary = Boundary2D::rectangle(500.0, 500.0);
3328 let strip_width = 500.0;
3329
3330 let strategies = vec![
3332 Strategy::BottomLeftFill,
3333 Strategy::NfpGuided,
3334 Strategy::GeneticAlgorithm,
3335 Strategy::Brkga,
3336 Strategy::SimulatedAnnealing,
3337 Strategy::Gdrr,
3338 Strategy::Alns,
3339 ];
3340
3341 for strategy in strategies {
3342 println!("\n========== Testing {:?} ==========", strategy);
3343
3344 let config = Config::default()
3345 .with_strategy(strategy)
3346 .with_time_limit(30000); let nester = Nester2D::new(config);
3348
3349 let result = match nester.solve_multi_strip(&shapes, &boundary) {
3350 Ok(r) => r,
3351 Err(e) => {
3352 println!(" Strategy {:?} failed: {}", strategy, e);
3353 continue;
3354 }
3355 };
3356
3357 println!(
3358 " Placed {} pieces across {} strips",
3359 result.placements.len(),
3360 result.boundaries_used
3361 );
3362
3363 let mut boundary_violations = Vec::new();
3365 for p in &result.placements {
3366 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
3368 let full_id = if base_id.starts_with("shape") {
3369 shapes
3371 .iter()
3372 .find(|g| p.geometry_id.starts_with(g.id()))
3373 .map(|g| g.id().as_str())
3374 } else {
3375 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
3376 };
3377
3378 let geom = match full_id.and_then(|id| geom_map.get(id)) {
3379 Some(g) => *g,
3380 None => {
3381 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
3383 Some(g) => g,
3384 None => {
3385 println!(
3386 " WARNING: Could not find geometry for {}",
3387 p.geometry_id
3388 );
3389 continue;
3390 }
3391 }
3392 }
3393 };
3394
3395 let origin_x = p.position[0];
3396 let origin_y = p.position[1];
3397 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3398 let strip_idx = p.boundary_index;
3399
3400 let local_x = origin_x - (strip_idx as f64 * strip_width);
3402
3403 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
3404
3405 let local_min_x = local_x + g_min[0];
3407 let local_max_x = local_x + g_max[0];
3408 let local_min_y = origin_y + g_min[1];
3409 let local_max_y = origin_y + g_max[1];
3410
3411 let tolerance = 0.1;
3413 if local_min_x < -tolerance
3414 || local_max_x > 500.0 + tolerance
3415 || local_min_y < -tolerance
3416 || local_max_y > 500.0 + tolerance
3417 {
3418 boundary_violations.push(format!(
3419 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
3420 p.geometry_id,
3421 strip_idx,
3422 local_min_x,
3423 local_min_y,
3424 local_max_x,
3425 local_max_y
3426 ));
3427 }
3428 }
3429
3430 if !boundary_violations.is_empty() {
3431 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
3432 for v in &boundary_violations {
3433 println!(" - {}", v);
3434 }
3435 }
3436
3437 let mut overlaps = Vec::new();
3439 let placements: Vec<_> = result.placements.iter().collect();
3440
3441 for i in 0..placements.len() {
3442 for j in (i + 1)..placements.len() {
3443 let p1 = placements[i];
3444 let p2 = placements[j];
3445
3446 if p1.boundary_index != p2.boundary_index {
3448 continue;
3449 }
3450
3451 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
3453 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
3454
3455 let (g1, g2) = match (g1, g2) {
3456 (Some(a), Some(b)) => (a, b),
3457 _ => continue,
3458 };
3459
3460 let strip_idx = p1.boundary_index;
3461 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
3462 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
3463
3464 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
3465 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
3466
3467 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
3468 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
3469
3470 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
3471 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
3472 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
3473 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
3474
3475 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
3476 overlaps.push(format!(
3477 "{} and {} in strip {}",
3478 p1.geometry_id, p2.geometry_id, strip_idx
3479 ));
3480 }
3481 }
3482 }
3483
3484 if !overlaps.is_empty() {
3485 println!(" OVERLAPS ({}):", overlaps.len());
3486 for o in overlaps.iter().take(10) {
3487 println!(" - {}", o);
3488 }
3489 if overlaps.len() > 10 {
3490 println!(" ... and {} more", overlaps.len() - 10);
3491 }
3492 }
3493
3494 assert!(
3496 boundary_violations.is_empty(),
3497 "{:?}: Found {} boundary violations",
3498 strategy,
3499 boundary_violations.len()
3500 );
3501
3502 println!(" ✓ All placements within boundary");
3503 println!(" ✓ No AABB overlaps detected");
3504 }
3505 }
3506
3507 #[test]
3515 fn test_multi_strip_distributes_all_instances() {
3516 let geometries = vec![Geometry2D::rectangle("part", 100.0, 100.0).with_quantity(20)];
3517 let boundary = Boundary2D::rectangle(300.0, 300.0);
3518 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3519 let nester = Nester2D::new(config);
3520
3521 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3522
3523 assert_eq!(
3525 result.placements.len(),
3526 20,
3527 "all 20 instances must be placed"
3528 );
3529 assert_eq!(
3530 result.boundaries_used, 3,
3531 "20 of 100x100 in 300x300 => 3 sheets"
3532 );
3533 assert!(
3534 result.unplaced.is_empty(),
3535 "nothing should be unplaced, got {:?}",
3536 result.unplaced
3537 );
3538 assert_eq!(result.total_requested, 20);
3540
3541 let mut seen = std::collections::HashSet::new();
3544 for p in &result.placements {
3545 assert!(
3546 seen.insert((p.geometry_id.clone(), p.instance)),
3547 "duplicate (id, instance) = ({}, {}) across sheets",
3548 p.geometry_id,
3549 p.instance
3550 );
3551 }
3552 let mut sheets: Vec<usize> = result.placements.iter().map(|p| p.boundary_index).collect();
3554 sheets.sort_unstable();
3555 sheets.dedup();
3556 assert_eq!(sheets, vec![0, 1, 2]);
3557 }
3558
3559 #[test]
3562 fn test_multi_strip_oversized_reported_unplaced() {
3563 let geometries = vec![
3564 Geometry2D::rectangle("ok", 50.0, 50.0).with_quantity(2),
3565 Geometry2D::rectangle("toobig", 400.0, 400.0).with_quantity(3),
3566 ];
3567 let boundary = Boundary2D::rectangle(300.0, 300.0);
3568 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3569 let nester = Nester2D::new(config);
3570
3571 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3572
3573 assert_eq!(result.total_requested, 5, "2 + 3 instances requested");
3574 assert_eq!(result.placements.len(), 2);
3576 assert!(
3577 result.unplaced.contains(&"toobig".to_string()),
3578 "oversized geometry must surface in unplaced, got {:?}",
3579 result.unplaced
3580 );
3581 }
3582}