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_temperature(100.0, 0.999, 0.1) }
343 }
344
345 fn bottom_left_fill(
355 &self,
356 geometries: &[Geometry2D],
357 boundary: &Boundary2D,
358 ) -> Result<SolveResult<f64>> {
359 self.bottom_left_fill_impl(geometries, boundary, None)
360 }
361
362 fn nfp_guided_blf_impl(
365 &self,
366 geometries: &[Geometry2D],
367 boundary: &Boundary2D,
368 callback: Option<&ProgressCallback>,
369 ) -> Result<SolveResult<f64>> {
370 let start = Timer::now();
371 let mut result = SolveResult::new();
372 let mut placements = Vec::new();
373 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
374 let report = |info: ProgressInfo| {
375 if let Some(callback) = callback {
376 callback(info);
377 }
378 };
379 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
380 let mut placed_count = 0usize;
381 report(
382 ProgressInfo::new()
383 .with_phase("NFP Placement")
384 .with_items(0, total_pieces)
385 .with_elapsed(0),
386 );
387
388 let margin = self.config.margin;
389 let spacing = self.config.spacing;
390
391 let boundary_polygon = inset_boundary(boundary, margin);
393
394 let mut total_placed_area = 0.0;
395
396 let sample_step = self.compute_sample_step(geometries);
398
399 for geom in geometries {
400 geom.validate()?;
401
402 let rotation_angles: Vec<f64> = geom.rotations();
405
406 let mirror_candidates = mirror_candidates(geom);
407
408 for instance in 0..geom.quantity() {
409 if let Some(stop) = self.stop_requested(&start) {
411 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
412 result.utilization = total_placed_area / boundary.measure();
413 result.computation_time_ms = start.elapsed_ms();
414 result.placements = placements;
415 result.stop_reason = stop;
416 report(
417 ProgressInfo::new()
418 .with_phase(stop_phase(stop))
419 .with_items(placed_count, total_pieces)
420 .with_elapsed(result.computation_time_ms)
421 .finished(),
422 );
423 return Ok(result);
424 }
425
426 let mut best_placement: Option<(f64, f64, f64, bool)> = None; for &rotation in &rotation_angles {
430 for &mirror in mirror_candidates {
431 let ifp = match compute_ifp_with_margin_and_mirror(
433 &boundary_polygon,
434 geom,
435 rotation,
436 0.0,
438 mirror,
439 ) {
440 Ok(ifp) => ifp,
441 Err(_) => continue,
442 };
443
444 if ifp.is_empty() {
445 continue;
446 }
447
448 let mut nfps: Vec<Nfp> = Vec::new();
450 for placed in &placed_geometries {
451 let cache_key = (
454 placed.geometry.id().as_str(),
455 geom.id().as_str(),
456 rotation - placed.rotation, placed.mirrored,
458 mirror,
459 );
460
461 let nfp_at_origin =
466 match self.nfp_cache.get_or_compute_mirrored(cache_key, || {
467 let placed_at_origin = placed.geometry.clone();
468 compute_nfp_mirrored(
469 &placed_at_origin,
470 geom,
471 rotation - placed.rotation,
472 placed.mirrored,
473 mirror,
474 )
475 .map(|nfp| offset_nfp(&nfp, spacing))
479 }) {
480 Ok(nfp) => nfp,
481 Err(_) => continue,
482 };
483
484 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
487 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
488 nfps.push(translated_nfp);
489 }
490
491 nfps.extend(hole_nfps(boundary, geom, rotation, mirror, margin));
496 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
497 if let Some((x, y)) = find_bottom_left_placement(
498 &ifp,
499 &nfp_refs,
500 sample_step,
501 PackingAxis::of(boundary),
502 ) {
503 let is_better =
505 match best_placement {
506 None => true,
507 Some((best_x, best_y, _, _)) => PackingAxis::of(boundary)
508 .precedes((x, y), (best_x, best_y), 1e-6),
509 };
510 if is_better {
511 best_placement = Some((x, y, rotation, mirror));
512 }
513 }
514 }
515 }
516
517 if let Some((x, y, rotation, mirror)) = best_placement {
519 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
526 let boundary_aabb = boundary.aabb();
527
528 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
529 x,
530 y,
531 geom_aabb,
532 boundary_aabb,
533 margin,
534 ) {
535 let placement = Placement::new_2d(
536 geom.id().clone(),
537 instance,
538 clamped_x,
539 clamped_y,
540 rotation,
541 )
542 .with_mirrored(mirror);
543
544 placements.push(placement);
545 placed_geometries.push(
546 PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation)
547 .with_mirrored(mirror),
548 );
549 total_placed_area += geom.measure();
550 placed_count += 1;
551 report(
552 ProgressInfo::new()
553 .with_phase("NFP Placement")
554 .with_items(placed_count, total_pieces)
555 .with_utilization(total_placed_area / boundary.measure())
556 .with_elapsed(start.elapsed_ms()),
557 );
558 } else {
559 result.unplaced.push(geom.id().clone());
561 }
562 } else {
563 result.unplaced.push(geom.id().clone());
565 }
566 }
567 }
568
569 result.placements = placements;
570 result.boundaries_used = 1;
571 result.utilization = total_placed_area / boundary.measure();
572 result.computation_time_ms = start.elapsed_ms();
573 report(
574 ProgressInfo::new()
575 .with_phase("Complete")
576 .with_items(placed_count, total_pieces)
577 .with_utilization(result.utilization)
578 .with_elapsed(result.computation_time_ms)
579 .finished(),
580 );
581
582 Ok(result)
583 }
584
585 fn compute_sample_step(&self, geometries: &[Geometry2D]) -> f64 {
587 if geometries.is_empty() {
588 return 1.0;
589 }
590
591 let mut min_dim = f64::INFINITY;
593 for geom in geometries {
594 let (g_min, g_max) = geom.aabb();
595 let width = g_max[0] - g_min[0];
596 let height = g_max[1] - g_min[1];
597 min_dim = min_dim.min(width).min(height);
598 }
599
600 (min_dim / 4.0).clamp(0.5, 10.0)
602 }
603
604 fn not_worse_than_baselines(
614 &self,
615 meta: SolveResult<f64>,
616 geometries: &[Geometry2D],
617 boundary: &Boundary2D,
618 greedy: Option<SolveResult<f64>>,
619 ) -> SolveResult<f64> {
620 let baselines = self
621 .bottom_left_fill(geometries, boundary)
622 .ok()
623 .into_iter()
624 .chain(greedy);
625 let quality = |r: &SolveResult<f64>| solution_quality(r, geometries, boundary);
626 meta.floored_at(baselines, |a, b| {
629 let (a, b) = (quality(a), quality(b));
630 a.0 > b.0 || (a.0 == b.0 && a.1 < b.1 - 1e-6)
631 })
632 }
633
634 fn target_fitness(&self) -> Option<f64> {
637 self.config
638 .target_utilization
639 .map(|target| nesting_fitness(1, 1, target))
640 }
641
642 fn search_budget_ms(&self) -> u64 {
652 match self.config.time_limit_ms {
653 0 => u64::MAX / 2,
654 limit => limit,
655 }
656 }
657
658 fn greedy_start(
665 &self,
666 geometries: &[Geometry2D],
667 boundary: &Boundary2D,
668 budget_ms: u64,
669 progress: Option<&ProgressCallback>,
670 ) -> (Option<SolveResult<f64>>, u64) {
671 let started = Timer::now();
672 let greedy = self
673 .with_time_limit(budget_ms)
674 .nfp_guided_blf_impl(geometries, boundary, progress)
675 .ok();
676 (
677 greedy,
678 budget_ms.saturating_sub(started.elapsed_ms()).max(1),
679 )
680 }
681
682 fn genetic_algorithm(
689 &self,
690 geometries: &[Geometry2D],
691 boundary: &Boundary2D,
692 progress: Option<&ProgressCallback>,
693 ) -> Result<SolveResult<f64>> {
694 let (time_limit_ms, greedy) = {
695 let budget = self.search_budget_ms();
696 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
697 (remaining, greedy)
698 };
699
700 let ga_config = self
701 .ga_config()
702 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
703
704 let result = run_ga_nesting(
705 geometries,
706 boundary,
707 &self.config,
708 ga_config,
709 self.cancelled.clone(),
710 greedy.as_ref().map(|g| g.placements.as_slice()),
711 progress,
712 )?;
713
714 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
715 }
716
717 fn brkga(
721 &self,
722 geometries: &[Geometry2D],
723 boundary: &Boundary2D,
724 progress: Option<&ProgressCallback>,
725 ) -> Result<SolveResult<f64>> {
726 let (time_limit_ms, greedy) = {
727 let budget = self.search_budget_ms();
728 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
729 (remaining, greedy)
730 };
731
732 let brkga_config = self
733 .brkga_config()
734 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
735
736 let result = run_brkga_nesting(
737 geometries,
738 boundary,
739 &self.config,
740 brkga_config,
741 self.cancelled.clone(),
742 greedy.as_ref().map(|g| g.placements.as_slice()),
743 progress,
744 )?;
745
746 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
747 }
748
749 fn simulated_annealing(
754 &self,
755 geometries: &[Geometry2D],
756 boundary: &Boundary2D,
757 progress: Option<&ProgressCallback>,
758 ) -> Result<SolveResult<f64>> {
759 let (time_limit_ms, greedy) = {
760 let budget = self.search_budget_ms();
761 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
762 (remaining, greedy)
763 };
764
765 let sa_config = SaConfig::default()
766 .with_initial_temp(50.0) .with_final_temp(1.0) .with_cooling_rate(0.9) .with_iterations_per_temp(20) .with_max_iterations(self.iteration_budget(500))
771 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
772 let sa_config = match self.target_fitness() {
773 Some(target) => sa_config.with_target_fitness(target),
774 None => sa_config,
775 };
776
777 let result = run_sa_nesting(
778 geometries,
779 boundary,
780 &self.config,
781 sa_config,
782 self.cancelled.clone(),
783 greedy.as_ref().map(|g| g.placements.as_slice()),
784 progress,
785 )?;
786
787 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
788 }
789
790 fn gdrr(
792 &self,
793 geometries: &[Geometry2D],
794 boundary: &Boundary2D,
795 progress: Option<&ProgressCallback>,
796 ) -> Result<SolveResult<f64>> {
797 let (time_limit, greedy) = {
798 let budget = self.search_budget_ms();
799 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
800 (remaining, greedy)
801 };
802 let result = run_gdrr_nesting(
803 geometries,
804 boundary,
805 &self.config,
806 &self.gdrr_config(time_limit),
807 self.cancelled.clone(),
808 progress,
809 )?;
810
811 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
812 }
813
814 fn alns(
816 &self,
817 geometries: &[Geometry2D],
818 boundary: &Boundary2D,
819 progress: Option<&ProgressCallback>,
820 ) -> Result<SolveResult<f64>> {
821 self.alns_within(geometries, boundary, self.search_budget_ms(), progress)
822 }
823
824 fn alns_within(
827 &self,
828 geometries: &[Geometry2D],
829 boundary: &Boundary2D,
830 budget_ms: u64,
831 progress: Option<&ProgressCallback>,
832 ) -> Result<SolveResult<f64>> {
833 let (greedy, time_limit) = self.greedy_start(geometries, boundary, budget_ms, progress);
834 let result = run_alns_nesting(
835 geometries,
836 boundary,
837 &self.config,
838 &self.alns_config(time_limit),
839 self.cancelled.clone(),
840 progress,
841 )?;
842
843 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
844 }
845
846 #[cfg(feature = "milp")]
859 fn milp_exact(
860 &self,
861 geometries: &[Geometry2D],
862 boundary: &Boundary2D,
863 ) -> Result<SolveResult<f64>> {
864 let exact_config = ExactConfig::default()
869 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms))
870 .with_max_items(15)
871 .with_grid_step(milp_grid_step(geometries, boundary));
872
873 run_nfp_cm_nesting(
874 geometries,
875 boundary,
876 &self.config,
877 &exact_config,
878 self.cancelled.clone(),
879 )
880 }
881
882 #[cfg(feature = "milp")]
884 fn hybrid_exact(
885 &self,
886 geometries: &[Geometry2D],
887 boundary: &Boundary2D,
888 ) -> Result<SolveResult<f64>> {
889 let started = Timer::now();
890 let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
892
893 if total_instances <= 15 {
895 let exact_config = ExactConfig::default()
899 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms / 2))
900 .with_max_items(15)
901 .with_grid_step(milp_grid_step(geometries, boundary));
902
903 let exact_result = run_nfp_cm_nesting(
904 geometries,
905 boundary,
906 &self.config,
907 &exact_config,
908 self.cancelled.clone(),
909 )?;
910
911 if !exact_result.placements.is_empty() && exact_result.unplaced.is_empty() {
918 return Ok(exact_result);
919 }
920 let heuristic =
921 self.alns_within(geometries, boundary, self.fallback_budget(started), None)?;
922 let (exact_placed, exact_length) =
923 solution_quality(&exact_result, geometries, boundary);
924 let (placed, length) = solution_quality(&heuristic, geometries, boundary);
925 let exact_wins =
926 exact_placed > placed || (exact_placed == placed && exact_length < length);
927 return Ok(if exact_wins { exact_result } else { heuristic });
928 }
929
930 self.alns_within(geometries, boundary, self.fallback_budget(started), None)
935 }
936
937 fn fallback_budget(&self, started: Timer) -> u64 {
940 match self.config.time_limit_ms {
941 0 => self.search_budget_ms(),
942 limit => limit.saturating_sub(started.elapsed_ms()).max(1),
943 }
944 }
945
946 fn bottom_left_fill_impl(
954 &self,
955 geometries: &[Geometry2D],
956 boundary: &Boundary2D,
957 callback: Option<&ProgressCallback>,
958 ) -> Result<SolveResult<f64>> {
959 let start = Timer::now();
960 let mut result = SolveResult::new();
961 let mut placements = Vec::new();
962 let report = |info: ProgressInfo| {
963 if let Some(callback) = callback {
964 callback(info);
965 }
966 };
967
968 let axis = PackingAxis::of(boundary);
969 let to_ca = |x: f64, y: f64| match axis {
971 PackingAxis::Y => (x, y),
972 PackingAxis::X => (y, x),
973 };
974 let from_ca = |across: f64, along: f64| match axis {
975 PackingAxis::Y => (across, along),
976 PackingAxis::X => (along, across),
977 };
978
979 let (b_min, b_max) = boundary.aabb();
980 let margin = self.config.margin;
981 let spacing = self.config.spacing;
982 let (min_across, min_along) = to_ca(b_min[0] + margin, b_min[1] + margin);
983 let (max_across, max_along) = to_ca(b_max[0] - margin, b_max[1] - margin);
984
985 let hole_boxes: Vec<(f64, f64, f64, f64)> = boundary
992 .holes()
993 .iter()
994 .filter(|hole| !hole.is_empty())
995 .map(|hole| {
996 let (mut x0, mut y0) = (f64::INFINITY, f64::INFINITY);
997 let (mut x1, mut y1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
998 for &(x, y) in hole {
999 x0 = x0.min(x);
1000 y0 = y0.min(y);
1001 x1 = x1.max(x);
1002 y1 = y1.max(y);
1003 }
1004 let (a0, l0) = to_ca(x0 - margin, y0 - margin);
1005 let (a1, l1) = to_ca(x1 + margin, y1 + margin);
1006 (a0.min(a1), l0.min(l1), a0.max(a1), l0.max(l1))
1007 })
1008 .collect();
1009
1010 let blocking_hole_edge = |across: f64, along: f64, ea: f64, el: f64| -> Option<f64> {
1014 hole_boxes
1015 .iter()
1016 .filter(|(a0, l0, a1, l1)| {
1017 across < *a1 && across + ea > *a0 && along < *l1 && along + el > *l0
1018 })
1019 .map(|(_, _, a1, _)| *a1)
1020 .fold(None, |far: Option<f64>, a1| {
1021 Some(far.map_or(a1, |f| f.max(a1)))
1022 })
1023 };
1024
1025 let mut cursor_across = min_across;
1026 let mut cursor_along = min_along;
1027 let mut row_depth = 0.0_f64;
1028 let mut total_placed_area = 0.0;
1029 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
1030 let mut placed_count = 0usize;
1031
1032 report(
1033 ProgressInfo::new()
1034 .with_phase("BLF Placement")
1035 .with_items(0, total_pieces)
1036 .with_elapsed(0),
1037 );
1038
1039 for geom in geometries {
1040 geom.validate()?;
1041
1042 let rotation_angles: Vec<f64> = geom.rotations();
1044
1045 for instance in 0..geom.quantity() {
1046 if let Some(stop) = self.stop_requested(&start) {
1048 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
1049 result.utilization = total_placed_area / boundary.measure();
1050 result.computation_time_ms = start.elapsed_ms();
1051 result.placements = placements;
1052 result.stop_reason = stop;
1053 report(
1054 ProgressInfo::new()
1055 .with_phase(stop_phase(stop))
1056 .with_items(placed_count, total_pieces)
1057 .with_elapsed(result.computation_time_ms)
1058 .finished(),
1059 );
1060 return Ok(result);
1061 }
1062
1063 let mut best_fit: Option<BlfCandidate> = None;
1066
1067 for &rotation in &rotation_angles {
1068 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
1069 let (extent_across, extent_along) =
1070 to_ca(g_max[0] - g_min[0], g_max[1] - g_min[1]);
1071
1072 if extent_across > max_across - min_across
1073 || extent_along > max_along - min_along
1074 {
1075 continue;
1076 }
1077
1078 let mut place_across = cursor_across;
1079 let mut place_along = cursor_along;
1080 let mut depth = row_depth;
1083 let mut opened_row = false;
1084 let mut turns = 0usize;
1090 let max_turns = 4 * (hole_boxes.len() + 1) * (hole_boxes.len() + 2) + 16;
1091 let fits = loop {
1092 if place_across + extent_across > max_across {
1093 place_across = min_across;
1094 place_along += depth + spacing;
1095 depth = extent_along;
1096 opened_row = true;
1097 }
1098 if place_along + extent_along > max_along {
1099 break false;
1100 }
1101 match blocking_hole_edge(
1102 place_across,
1103 place_along,
1104 extent_across,
1105 extent_along,
1106 ) {
1107 None => break true,
1108 Some(past) => place_across = past + spacing,
1109 }
1110 turns += 1;
1111 if turns > max_turns {
1112 break false;
1113 }
1114 };
1115 if !fits {
1116 continue;
1117 }
1118
1119 let cost = |c: &BlfCandidate| {
1124 if c.opened_row {
1125 c.place_along - min_along + c.extent_along
1126 } else {
1127 c.place_across - min_across + c.extent_across
1128 }
1129 };
1130 let candidate = BlfCandidate {
1131 rotation,
1132 extent_across,
1133 extent_along,
1134 place_across,
1135 place_along,
1136 opened_row,
1137 };
1138 let better = best_fit
1139 .as_ref()
1140 .is_none_or(|best| cost(&candidate) < cost(best) - 1e-6);
1141 if better {
1142 best_fit = Some(candidate);
1143 }
1144 }
1145
1146 let Some(BlfCandidate {
1147 rotation,
1148 extent_across,
1149 extent_along,
1150 place_across,
1151 place_along,
1152 opened_row,
1153 }) = best_fit
1154 else {
1155 result.unplaced.push(geom.id().clone());
1156 continue;
1157 };
1158 if opened_row {
1159 row_depth = 0.0;
1160 }
1161
1162 let geom_aabb = geom.aabb_at_rotation(rotation);
1163 let (place_x, place_y) = from_ca(place_across, place_along);
1164 let Some((x, y)) = clamp_placement_to_boundary_with_margin(
1165 place_x - geom_aabb.0[0],
1166 place_y - geom_aabb.0[1],
1167 geom_aabb,
1168 (b_min, b_max),
1169 margin,
1170 ) else {
1171 result.unplaced.push(geom.id().clone());
1172 continue;
1173 };
1174
1175 placements.push(Placement::new_2d(
1176 geom.id().clone(),
1177 instance,
1178 x,
1179 y,
1180 rotation,
1181 ));
1182 total_placed_area += geom.measure();
1183 placed_count += 1;
1184
1185 let (actual_across, actual_along) = to_ca(x + geom_aabb.0[0], y + geom_aabb.0[1]);
1187 cursor_across = actual_across + extent_across + spacing;
1188 cursor_along = actual_along;
1189 row_depth = row_depth.max(extent_along);
1190
1191 report(
1192 ProgressInfo::new()
1193 .with_phase("BLF Placement")
1194 .with_items(placed_count, total_pieces)
1195 .with_utilization(total_placed_area / boundary.measure())
1196 .with_elapsed(start.elapsed_ms()),
1197 );
1198 }
1199 }
1200
1201 result.placements = placements;
1202 result.boundaries_used = 1;
1203 result.utilization = total_placed_area / boundary.measure();
1204 result.computation_time_ms = start.elapsed_ms();
1205
1206 report(
1207 ProgressInfo::new()
1208 .with_phase("Complete")
1209 .with_items(placed_count, total_pieces)
1210 .with_utilization(result.utilization)
1211 .with_elapsed(result.computation_time_ms)
1212 .finished(),
1213 );
1214
1215 Ok(result)
1216 }
1217
1218 pub fn validate(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<()> {
1227 boundary.validate()?;
1228 self.validate_geometries(geometries)?;
1229 if !self.config.strategy.is_offered(Dimension::TwoD) {
1230 return Err(self.unoffered_strategy());
1231 }
1232 Ok(())
1233 }
1234
1235 fn unoffered_strategy(&self) -> Error {
1237 let strategy = self.config.strategy;
1238 let why = if strategy == Strategy::ExtremePoint {
1239 "is 3D only".to_string()
1240 } else {
1241 "needs the `milp` feature, which this build does not have".to_string()
1242 };
1243 Error::invalid_option(
1244 "strategy",
1245 format!(
1246 "strategy \"{}\" {why}; 2D nesting offers {}",
1247 strategy.name(),
1248 offered_names(Dimension::TwoD)
1249 ),
1250 )
1251 }
1252
1253 fn validate_geometries(&self, geometries: &[Geometry2D]) -> Result<()> {
1263 use u_nesting_core::geometry::Geometry;
1264 if geometries.is_empty() {
1265 return Err(Error::empty_input("geometries"));
1266 }
1267 self.config.validate()?;
1268 for geom in geometries {
1269 geom.validate()?;
1270 }
1271 u_nesting_core::geometry::ensure_unique_ids(geometries)
1272 }
1273
1274 pub fn solve_multi_strip(
1280 &self,
1281 geometries: &[Geometry2D],
1282 boundary: &Boundary2D,
1283 ) -> Result<SolveResult<f64>> {
1284 self.solve_multi_strip_reporting(geometries, boundary, None)
1285 }
1286
1287 pub fn solve_multi_strip_with_progress(
1293 &self,
1294 geometries: &[Geometry2D],
1295 boundary: &Boundary2D,
1296 callback: ProgressCallback,
1297 ) -> Result<SolveResult<f64>> {
1298 self.solve_multi_strip_reporting(geometries, boundary, Some(&callback))
1299 }
1300
1301 fn solve_multi_strip_reporting(
1302 &self,
1303 geometries: &[Geometry2D],
1304 boundary: &Boundary2D,
1305 progress: Option<&ProgressCallback>,
1306 ) -> Result<SolveResult<f64>> {
1307 self.validate(geometries, boundary)?;
1308 self.cancelled.store(false, Ordering::Relaxed);
1309 let start = Timer::now();
1310
1311 let (b_min, b_max) = boundary.aabb();
1312 let strip_width = b_max[0] - b_min[0];
1313
1314 let sheet_area = boundary.measure();
1315 let mut final_result = SolveResult::new();
1316 let mut stop_reason = StopReason::Completed;
1319 let mut remaining_geometries: Vec<Geometry2D> = geometries.to_vec();
1320 let mut strip_index = 0;
1321 let max_strips = 100; let mut placed_total: std::collections::HashMap<String, usize> =
1326 std::collections::HashMap::new();
1327
1328 let mut budget_spent = false;
1332
1333 while !remaining_geometries.is_empty() && strip_index < max_strips {
1334 if self.cancelled.load(Ordering::Relaxed) {
1335 stop_reason = StopReason::Cancelled;
1336 break;
1337 }
1338
1339 let left = match self.config.time_limit_ms {
1344 0 => None,
1345 limit => Some(limit.saturating_sub(start.elapsed_ms())),
1346 };
1347 if left == Some(0) {
1348 budget_spent = true;
1349 }
1350 let (sheet_nester, whole_budget) = match left {
1351 _ if budget_spent => (Some(self.constructive_fill()), false),
1352 None => (None, false),
1353 Some(left) => {
1354 let remaining_area: f64 = remaining_geometries
1355 .iter()
1356 .map(|g| g.measure() * g.quantity() as f64)
1357 .sum();
1358 let sheets_ahead = (remaining_area / sheet_area).ceil().max(1.0);
1359 let share = (left as f64 / sheets_ahead).ceil() as u64;
1360 (Some(self.with_time_limit(share)), share >= left)
1361 }
1362 };
1363
1364 let strip_result = sheet_nester
1366 .as_ref()
1367 .unwrap_or(self)
1368 .run_strategy_reporting(&remaining_geometries, boundary, progress)?;
1369 if whole_budget && strip_result.stop_reason == StopReason::TimeLimit {
1372 budget_spent = true;
1373 }
1374 stop_reason = if budget_spent && strip_result.stop_reason != StopReason::Cancelled {
1375 StopReason::TimeLimit
1376 } else {
1377 strip_result.stop_reason
1378 };
1379
1380 let strip_result = validate_and_filter_placements(
1382 strip_result,
1383 &remaining_geometries,
1384 boundary,
1385 self.config.margin,
1386 );
1387
1388 if strip_result.placements.is_empty() {
1389 break;
1392 }
1393
1394 let mut strip_placed: std::collections::HashMap<String, usize> =
1397 std::collections::HashMap::new();
1398
1399 for mut placement in strip_result.placements {
1401 let gid = placement.geometry_id.clone();
1402 let prior = placed_total.get(&gid).copied().unwrap_or(0);
1405 let in_strip = strip_placed.get(&gid).copied().unwrap_or(0);
1406 placement.instance = prior + in_strip;
1407 if !placement.position.is_empty() {
1409 placement.position[0] += strip_index as f64 * strip_width;
1410 }
1411 placement.boundary_index = strip_index;
1412 *strip_placed.entry(gid).or_insert(0) += 1;
1413 final_result.placements.push(placement);
1414 }
1415
1416 for (gid, cnt) in &strip_placed {
1420 *placed_total.entry(gid.clone()).or_insert(0) += cnt;
1421 }
1422 remaining_geometries = remaining_geometries
1423 .into_iter()
1424 .filter_map(|g| {
1425 let placed_here = strip_placed.get(g.id()).copied().unwrap_or(0);
1426 let new_quantity = g.quantity().saturating_sub(placed_here);
1427 if new_quantity == 0 {
1428 None
1429 } else {
1430 Some(g.with_quantity(new_quantity))
1431 }
1432 })
1433 .collect();
1434
1435 strip_index += 1;
1436
1437 if stop_reason == StopReason::Cancelled {
1441 break;
1442 }
1443 }
1444
1445 for g in &remaining_geometries {
1448 final_result.unplaced.push(g.id().clone());
1449 }
1450
1451 final_result.boundaries_used = strip_index;
1452 final_result.stop_reason = stop_reason;
1453 final_result.deduplicate_unplaced();
1454 final_result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1456
1457 let (b_min, b_max) = boundary.aabb();
1459 let strip_height = b_max[1] - b_min[1]; let mut strip_stats_map: std::collections::HashMap<usize, (f64, f64, usize)> =
1463 std::collections::HashMap::new(); for placement in &final_result.placements {
1466 let strip_idx = placement.boundary_index;
1467 if let Some(geom) = geometries.iter().find(|g| g.id() == &placement.geometry_id) {
1469 use u_nesting_core::geometry::Geometry;
1470 let piece_area = geom.measure();
1471 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1472 let (_g_min, g_max) = geom.aabb_at_rotation(rotation);
1473 let local_x = placement.position[0] - (strip_idx as f64 * strip_width);
1476 let right_edge = local_x + g_max[0];
1477
1478 let entry = strip_stats_map.entry(strip_idx).or_insert((0.0, 0.0, 0));
1479 entry.0 = entry.0.max(right_edge); entry.1 += piece_area; entry.2 += 1; }
1483 }
1484
1485 use u_nesting_core::result::StripStats;
1487 let mut strip_stats: Vec<StripStats> = strip_stats_map
1488 .into_iter()
1489 .map(|(idx, (used_length, piece_area, count))| StripStats {
1490 strip_index: idx,
1491 used_length,
1492 piece_area,
1493 piece_count: count,
1494 strip_width, strip_height, })
1497 .collect();
1498 strip_stats.sort_by_key(|s| s.strip_index);
1499
1500 let total_piece_area: f64 = strip_stats.iter().map(|s| s.piece_area).sum();
1503 let total_material_used: f64 = strip_stats
1504 .iter()
1505 .map(|s| s.strip_height * s.used_length)
1506 .sum();
1507
1508 final_result.strip_stats = strip_stats;
1509 final_result.total_piece_area = total_piece_area;
1510 final_result.total_material_used = total_material_used;
1511
1512 if total_material_used > 0.0 {
1513 final_result.utilization = total_piece_area / total_material_used;
1514 }
1515 final_result.computation_time_ms = start.elapsed_ms();
1516
1517 Ok(final_result)
1518 }
1519}
1520
1521impl Nester2D {
1522 fn solve_reporting(
1526 &self,
1527 geometries: &[Geometry2D],
1528 boundary: &Boundary2D,
1529 progress: Option<ProgressCallback>,
1530 ) -> Result<SolveResult<f64>> {
1531 self.validate(geometries, boundary)?;
1532
1533 self.cancelled.store(false, Ordering::Relaxed);
1535 let start = Timer::now();
1536
1537 let initial_result =
1538 self.run_strategy_reporting(geometries, boundary, progress.as_ref())?;
1539
1540 let mut result = validate_and_filter_placements(
1542 initial_result,
1543 geometries,
1544 boundary,
1545 self.config.margin,
1546 );
1547
1548 result.record_unplaced(geometries);
1552 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1555 result.computation_time_ms = start.elapsed_ms();
1558 Ok(result)
1559 }
1560}
1561
1562impl Solver for Nester2D {
1563 type Geometry = Geometry2D;
1564 type Boundary = Boundary2D;
1565 type Scalar = f64;
1566
1567 fn solve(
1568 &self,
1569 geometries: &[Self::Geometry],
1570 boundary: &Self::Boundary,
1571 ) -> Result<SolveResult<f64>> {
1572 self.solve_reporting(geometries, boundary, None)
1573 }
1574
1575 fn solve_with_progress(
1576 &self,
1577 geometries: &[Self::Geometry],
1578 boundary: &Self::Boundary,
1579 callback: ProgressCallback,
1580 ) -> Result<SolveResult<f64>> {
1581 self.solve_reporting(geometries, boundary, Some(callback))
1582 }
1583
1584 fn cancel(&self) {
1585 self.cancelled.store(true, Ordering::Relaxed);
1586 }
1587}
1588
1589#[cfg(test)]
1590mod tests {
1591 #[cfg(feature = "milp")]
1592 mod milp_budget {
1593 use super::super::{milp_grid_step, milp_time_limit};
1594 use crate::{Boundary2D, Geometry2D};
1595
1596 #[test]
1599 fn an_unlimited_request_does_not_become_a_minute() {
1600 assert_eq!(milp_time_limit(0), u64::MAX / 2);
1601 }
1602
1603 #[test]
1606 fn a_short_request_is_kept_short() {
1607 assert_eq!(milp_time_limit(1_000), 1_000);
1608 assert_eq!(milp_time_limit(250), 250);
1609 }
1610
1611 #[test]
1615 fn the_grid_step_follows_the_smallest_piece() {
1616 let boundary = Boundary2D::rectangle(400.0, 100.0);
1617 let pieces = [
1618 Geometry2D::rectangle("wide", 80.0, 40.0),
1619 Geometry2D::rectangle("small", 20.0, 60.0),
1620 ];
1621 assert_eq!(milp_grid_step(&pieces, &boundary), 5.0);
1623 }
1624
1625 #[test]
1628 fn a_huge_sheet_keeps_the_step_from_getting_too_fine() {
1629 let boundary = Boundary2D::rectangle(100_000.0, 100.0);
1630 let pieces = [Geometry2D::rectangle("small", 4.0, 4.0)];
1631 assert_eq!(milp_grid_step(&pieces, &boundary), 100_000.0 / 256.0);
1632 }
1633
1634 #[test]
1636 fn the_step_never_goes_below_a_unit() {
1637 let boundary = Boundary2D::rectangle(10.0, 10.0);
1638 let pieces = [Geometry2D::rectangle("tiny", 0.5, 0.5)];
1639 assert_eq!(milp_grid_step(&pieces, &boundary), 1.0);
1640 }
1641 }
1642
1643 use super::*;
1644 use crate::placement_utils::polygon_centroid;
1645
1646 #[test]
1650 fn a_geometry_id_given_twice_is_refused_by_every_entry_point() {
1651 let geometries = [
1652 Geometry2D::rectangle("P", 10.0, 10.0),
1653 Geometry2D::rectangle("Q", 10.0, 10.0),
1654 Geometry2D::rectangle("P", 30.0, 5.0),
1655 ];
1656 let boundary = Boundary2D::rectangle(100.0, 100.0);
1657 let nester = Nester2D::default_config();
1658 let errors = [
1659 nester.solve(&geometries, &boundary).expect_err("solve"),
1660 nester
1661 .solve_multi_strip(&geometries, &boundary)
1662 .expect_err("solve_multi_strip"),
1663 nester
1664 .solve_with_progress(&geometries, &boundary, Box::new(|_| {}))
1665 .expect_err("solve_with_progress"),
1666 ];
1667 for err in errors {
1668 let msg = err.to_string();
1669 assert!(msg.contains("'P'"), "{msg}");
1670 assert!(msg.contains("positions 0 and 2"), "{msg}");
1671 }
1672 }
1673
1674 #[test]
1677 fn an_empty_list_of_geometries_is_refused_by_every_entry_point() {
1678 let boundary = Boundary2D::rectangle(100.0, 100.0);
1679 let nester = Nester2D::default_config();
1680 let errors = [
1681 nester.solve(&[], &boundary).expect_err("solve"),
1682 nester
1683 .solve_multi_strip(&[], &boundary)
1684 .expect_err("solve_multi_strip"),
1685 nester
1686 .solve_with_progress(&[], &boundary, Box::new(|_| {}))
1687 .expect_err("solve_with_progress"),
1688 ];
1689 for err in errors {
1690 assert_eq!(
1691 err,
1692 Error::EmptyInput {
1693 parameter: "geometries".into()
1694 }
1695 );
1696 }
1697 }
1698
1699 #[test]
1700 fn the_seed_reaches_the_searches_that_build_their_own_run() {
1701 let seeded = Nester2D::new(Config::default().with_seed(7));
1705 assert_eq!(seeded.gdrr_config(1000).seed, Some(7));
1706 assert_eq!(seeded.alns_config(1000).seed, Some(7));
1707 let unseeded = Nester2D::default_config();
1708 assert_eq!(unseeded.gdrr_config(1000).seed, None);
1709 assert_eq!(unseeded.alns_config(1000).seed, None);
1710 }
1711
1712 #[test]
1717 fn a_set_budget_is_run_and_an_unset_one_is_the_strategy_s() {
1718 assert_eq!((SEARCH_POPULATION, SEARCH_GENERATIONS), (30, 50));
1719 let unset = Nester2D::default_config();
1720 assert_eq!(unset.ga_config().population_size, SEARCH_POPULATION);
1721 assert_eq!(unset.ga_config().max_generations, SEARCH_GENERATIONS);
1722 assert_eq!(unset.brkga_config().population_size, SEARCH_POPULATION);
1723 assert_eq!(unset.brkga_config().max_generations, SEARCH_GENERATIONS);
1724
1725 let set = Nester2D::new(
1726 Config::default()
1727 .with_population_size(100)
1728 .with_max_generations(200),
1729 );
1730 assert_eq!(set.ga_config().population_size, 100);
1731 assert_eq!(set.ga_config().max_generations, 200);
1732 assert_eq!(set.brkga_config().population_size, 100);
1733 assert_eq!(set.brkga_config().max_generations, 200);
1734
1735 let small = Nester2D::new(Config::default().with_population_size(3));
1736 assert_eq!(small.brkga_config().population_size, 3, "was raised to 4");
1737 assert!(small.brkga_config().validate().is_ok());
1738 }
1739
1740 #[test]
1743 fn the_generation_budget_reaches_the_running_search() {
1744 use std::sync::Mutex;
1745 let reports = |config: Config| {
1746 let seen = Arc::new(Mutex::new(Vec::new()));
1747 let sink = seen.clone();
1748 Nester2D::new(
1749 config
1750 .with_strategy(Strategy::GeneticAlgorithm)
1751 .with_seed(7),
1752 )
1753 .solve_with_progress(
1754 &[Geometry2D::rectangle("R", 10.0, 10.0).with_quantity(4)],
1755 &Boundary2D::rectangle(50.0, 50.0),
1756 Box::new(move |info| {
1758 if info.phase == "Genetic Algorithm" {
1759 sink.lock()
1760 .expect("no report panicked")
1761 .push((info.iteration, info.total_iterations))
1762 }
1763 }),
1764 )
1765 .expect("solve");
1766 let seen = seen.lock().expect("no report panicked").clone();
1767 seen
1768 };
1769 let set = reports(Config::default().with_max_generations(80));
1770 assert!(!set.is_empty(), "the search reports its progress");
1771 assert!(
1772 set.iter().all(|&(at, total)| total == 80 && at <= 80),
1773 "{set:?}"
1774 );
1775 let unset = reports(Config::default());
1776 assert!(!unset.is_empty());
1777 assert!(unset.iter().all(|&(_, total)| total == 50), "{unset:?}");
1778 }
1779
1780 #[test]
1788 fn a_progress_callback_does_not_change_the_solve() {
1789 let geometries = [
1790 Geometry2D::rectangle("A", 30.0, 12.0)
1791 .with_quantity(3)
1792 .with_rotations_deg(vec![0.0, 90.0]),
1793 Geometry2D::rectangle("B", 18.0, 18.0).with_quantity(2),
1794 Geometry2D::new("L")
1795 .with_polygon(vec![
1796 (0.0, 0.0),
1797 (20.0, 0.0),
1798 (20.0, 8.0),
1799 (8.0, 8.0),
1800 (8.0, 20.0),
1801 (0.0, 20.0),
1802 ])
1803 .with_quantity(2)
1804 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0]),
1805 ];
1806 let boundary = Boundary2D::rectangle(60.0, 50.0);
1807 type Laid = (String, usize, Vec<f64>, Vec<f64>, bool);
1809 let layout = |r: &SolveResult<f64>| -> Vec<Laid> {
1810 r.placements
1811 .iter()
1812 .map(|p| {
1813 (
1814 p.geometry_id.clone(),
1815 p.instance,
1816 p.position.clone(),
1817 p.rotation.clone(),
1818 p.mirrored,
1819 )
1820 })
1821 .collect()
1822 };
1823 for strategy in [
1824 Strategy::BottomLeftFill,
1825 Strategy::NfpGuided,
1826 Strategy::GeneticAlgorithm,
1827 Strategy::Brkga,
1828 ] {
1829 for time_limit_ms in [0, 60_000] {
1830 let config = Config::default()
1831 .with_strategy(strategy)
1832 .with_seed(11)
1833 .with_population_size(6)
1834 .with_max_generations(4)
1835 .with_time_limit(time_limit_ms);
1836 let plain = Nester2D::new(config.clone())
1837 .solve(&geometries, &boundary)
1838 .expect("solve");
1839 let reported = Nester2D::new(config)
1840 .solve_with_progress(&geometries, &boundary, Box::new(|_| {}))
1841 .expect("solve with progress");
1842 let case = format!("{strategy:?}, time_limit_ms {time_limit_ms}");
1843 assert!(!plain.placements.is_empty(), "{case}");
1844 assert_eq!(layout(&plain), layout(&reported), "{case}");
1845 assert_eq!(plain.utilization, reported.utilization, "{case}");
1846 assert_eq!(plain.generations, reported.generations, "{case}");
1847 assert_eq!(plain.unplaced, reported.unplaced, "{case}");
1848 }
1849 }
1850 }
1851
1852 #[test]
1853 fn test_simple_nesting() {
1854 let geometries = vec![
1855 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1856 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1857 ];
1858
1859 let boundary = Boundary2D::rectangle(100.0, 50.0);
1860 let nester = Nester2D::default_config();
1861
1862 let result = nester.solve(&geometries, &boundary).unwrap();
1863
1864 assert!(result.utilization > 0.0);
1865 assert!(result.placements.len() <= 5); }
1867
1868 #[test]
1869 fn test_placement_within_bounds() {
1870 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1871
1872 let boundary = Boundary2D::rectangle(50.0, 50.0);
1873 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1874 let nester = Nester2D::new(config);
1875
1876 let result = nester.solve(&geometries, &boundary).unwrap();
1877
1878 assert_eq!(result.placements.len(), 4);
1880 assert!(result.unplaced.is_empty());
1881
1882 for p in &result.placements {
1884 assert!(p.position[0] >= 5.0);
1885 assert!(p.position[1] >= 5.0);
1886 }
1887 }
1888
1889 #[test]
1890 fn test_nfp_guided_basic() {
1891 let geometries = vec![
1892 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1893 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1894 ];
1895
1896 let boundary = Boundary2D::rectangle(100.0, 50.0);
1897 let config = Config::default().with_strategy(Strategy::NfpGuided);
1898 let nester = Nester2D::new(config);
1899
1900 let result = nester.solve(&geometries, &boundary).unwrap();
1901
1902 assert!(result.utilization > 0.0);
1903 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1905 }
1906
1907 #[test]
1908 fn test_nfp_guided_with_spacing() {
1909 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1910
1911 let boundary = Boundary2D::rectangle(50.0, 50.0);
1912 let config = Config::default()
1913 .with_strategy(Strategy::NfpGuided)
1914 .with_margin(2.0)
1915 .with_spacing(3.0);
1916 let nester = Nester2D::new(config);
1917
1918 let result = nester.solve(&geometries, &boundary).unwrap();
1919
1920 assert_eq!(result.placements.len(), 4);
1922 assert!(result.unplaced.is_empty());
1923
1924 assert!(result.utilization > 0.0);
1926 }
1927
1928 #[test]
1929 fn test_nfp_guided_no_overlap() {
1930 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1931
1932 let boundary = Boundary2D::rectangle(100.0, 100.0);
1933 let config = Config::default().with_strategy(Strategy::NfpGuided);
1934 let nester = Nester2D::new(config);
1935
1936 let result = nester.solve(&geometries, &boundary).unwrap();
1937
1938 assert_eq!(result.placements.len(), 3);
1939
1940 for i in 0..result.placements.len() {
1942 for j in (i + 1)..result.placements.len() {
1943 let p1 = &result.placements[i];
1944 let p2 = &result.placements[j];
1945
1946 let r1_min_x = p1.position[0];
1948 let r1_max_x = p1.position[0] + 20.0;
1949 let r1_min_y = p1.position[1];
1950 let r1_max_y = p1.position[1] + 20.0;
1951
1952 let r2_min_x = p2.position[0];
1953 let r2_max_x = p2.position[0] + 20.0;
1954 let r2_min_y = p2.position[1];
1955 let r2_max_y = p2.position[1] + 20.0;
1956
1957 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1959 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1960
1961 assert!(
1962 !(overlaps_x && overlaps_y),
1963 "Placements {} and {} overlap",
1964 i,
1965 j
1966 );
1967 }
1968 }
1969 }
1970
1971 fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
1976 for i in 0..a.len() {
1977 let a1 = a[i];
1978 let a2 = a[(i + 1) % a.len()];
1979 for j in 0..b.len() {
1980 let b1 = b[j];
1981 let b2 = b[(j + 1) % b.len()];
1982 if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
1983 return true;
1984 }
1985 }
1986 }
1987 false
1988 }
1989
1990 #[test]
1991 fn test_mirror_candidates_reflects_allow_flip() {
1992 let plain = Geometry2D::rectangle("R", 10.0, 10.0);
1993 assert_eq!(mirror_candidates(&plain), &[false]);
1994
1995 let flippable = Geometry2D::rectangle("R", 10.0, 10.0).with_flip(true);
1996 assert_eq!(mirror_candidates(&flippable), &[false, true]);
1997 }
1998
1999 #[test]
2019 fn test_mirror_aware_placement_avoids_overlap() {
2020 let spacing = 1.0;
2028 let geom = Geometry2D::l_shape("L", 30.0, 20.0, 20.0, 10.0);
2029 let boundary_polygon = vec![(0.0, 0.0), (65.0, 0.0), (65.0, 45.0), (0.0, 45.0)];
2030 let cache = NfpCache::new();
2031
2032 let ifp1 =
2034 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, false).unwrap();
2035 let (x1, y1) = find_bottom_left_placement(&ifp1, &[], 1.0, PackingAxis::X)
2036 .expect("first piece must fit");
2037 let placed1 = PlacedGeometry::new(geom.clone(), (x1, y1), 0.0).with_mirrored(false);
2038
2039 let ifp2 =
2041 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, true).unwrap();
2042 let nfp_at_origin = cache
2043 .get_or_compute_mirrored(("L", "L", 0.0, false, true), || {
2044 compute_nfp_mirrored(&placed1.geometry, &geom, 0.0, false, true)
2045 })
2046 .unwrap();
2047 let translated_nfp = translate_nfp(&nfp_at_origin, placed1.position);
2048 let expanded_nfp = offset_nfp(&translated_nfp, spacing);
2049 let (x2, y2) = find_bottom_left_placement(&ifp2, &[&expanded_nfp], 1.0, PackingAxis::X)
2050 .expect("mirrored second piece must fit avoiding the first");
2051 let placed2 = PlacedGeometry::new(geom.clone(), (x2, y2), 0.0).with_mirrored(true);
2052
2053 assert_ne!(
2054 (x1, y1),
2055 (x2, y2),
2056 "mirrored placement must actually avoid the first piece's position"
2057 );
2058 assert!(
2059 !polygons_overlap(
2060 &placed1.translated_exterior(),
2061 &placed2.translated_exterior()
2062 ),
2063 "unmirrored piece at {:?} and mirrored piece at {:?} must not overlap",
2064 (x1, y1),
2065 (x2, y2)
2066 );
2067 }
2068
2069 #[test]
2070 fn test_nfp_guided_utilization() {
2071 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
2073
2074 let boundary = Boundary2D::rectangle(100.0, 50.0);
2075 let config = Config::default().with_strategy(Strategy::NfpGuided);
2076 let nester = Nester2D::new(config);
2077
2078 let result = nester.solve(&geometries, &boundary).unwrap();
2079
2080 assert_eq!(result.placements.len(), 4);
2082
2083 assert!(result.utilization > 0.45);
2085 }
2086
2087 #[test]
2088 fn test_polygon_centroid() {
2089 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
2091 let (cx, cy) = polygon_centroid(&square);
2092 assert!((cx - 5.0).abs() < 0.01);
2093 assert!((cy - 5.0).abs() < 0.01);
2094
2095 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
2096 let (cx, cy) = polygon_centroid(&triangle);
2097 assert!((cx - 3.0).abs() < 0.01);
2098 assert!((cy - 2.0).abs() < 0.01);
2099 }
2100
2101 #[test]
2102 fn test_ga_strategy_basic() {
2103 let geometries = vec![
2104 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2105 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2106 ];
2107
2108 let boundary = Boundary2D::rectangle(100.0, 50.0);
2109 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
2110 let nester = Nester2D::new(config);
2111
2112 let result = nester.solve(&geometries, &boundary).unwrap();
2113
2114 assert!(result.utilization > 0.0);
2115 assert!(!result.placements.is_empty());
2116 assert!(result.generations.is_some());
2118 assert!(result.best_fitness.is_some());
2119 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
2120 }
2121
2122 #[test]
2123 fn test_ga_strategy_all_placed() {
2124 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2126
2127 let boundary = Boundary2D::rectangle(100.0, 100.0);
2128 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
2129 let nester = Nester2D::new(config);
2130
2131 let result = nester.solve(&geometries, &boundary).unwrap();
2132
2133 assert_eq!(result.placements.len(), 4);
2135 assert!(result.unplaced.is_empty());
2136 }
2137
2138 #[test]
2139 fn test_brkga_strategy_basic() {
2140 let geometries = vec![
2141 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2142 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2143 ];
2144
2145 let boundary = Boundary2D::rectangle(100.0, 50.0);
2146 let config = Config::default().with_strategy(Strategy::Brkga);
2147 let nester = Nester2D::new(config);
2148
2149 let result = nester.solve(&geometries, &boundary).unwrap();
2150
2151 assert!(result.utilization > 0.0);
2152 assert!(!result.placements.is_empty());
2153 assert!(result.generations.is_some());
2155 assert!(result.best_fitness.is_some());
2156 assert!(result.strategy == Some("BRKGA".to_string()));
2157 }
2158
2159 #[test]
2160 fn test_brkga_strategy_all_placed() {
2161 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2163
2164 let boundary = Boundary2D::rectangle(100.0, 100.0);
2165 let config = Config::default()
2167 .with_strategy(Strategy::Brkga)
2168 .with_time_limit(30000); let nester = Nester2D::new(config);
2170
2171 let result = nester.solve(&geometries, &boundary).unwrap();
2172
2173 assert!(
2176 result.placements.len() >= 3,
2177 "Expected at least 3 placements, got {}",
2178 result.placements.len()
2179 );
2180 }
2181
2182 #[test]
2183 fn test_gdrr_strategy_basic() {
2184 let geometries = vec![
2185 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2186 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2187 ];
2188
2189 let boundary = Boundary2D::rectangle(100.0, 50.0);
2190 let config = Config::default().with_strategy(Strategy::Gdrr);
2191 let nester = Nester2D::new(config);
2192
2193 let result = nester.solve(&geometries, &boundary).unwrap();
2194
2195 assert!(result.utilization > 0.0);
2196 assert!(!result.placements.is_empty());
2197 assert!(result.iterations.is_some());
2199 assert!(result.best_fitness.is_some());
2200 assert!(result.strategy == Some("GDRR".to_string()));
2201 }
2202
2203 #[test]
2204 fn test_gdrr_strategy_all_placed() {
2205 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2207
2208 let boundary = Boundary2D::rectangle(100.0, 100.0);
2209 let config = Config::default().with_strategy(Strategy::Gdrr);
2210 let nester = Nester2D::new(config);
2211
2212 let result = nester.solve(&geometries, &boundary).unwrap();
2213
2214 assert_eq!(result.placements.len(), 4);
2216 assert!(result.unplaced.is_empty());
2217 }
2218
2219 #[test]
2220 fn test_alns_strategy_basic() {
2221 let geometries = vec![
2222 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2223 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2224 ];
2225
2226 let boundary = Boundary2D::rectangle(100.0, 50.0);
2227 let config = Config::default().with_strategy(Strategy::Alns);
2228 let nester = Nester2D::new(config);
2229
2230 let result = nester.solve(&geometries, &boundary).unwrap();
2231
2232 assert!(result.utilization > 0.0);
2233 assert!(!result.placements.is_empty());
2234 assert!(result.iterations.is_some());
2236 assert!(result.best_fitness.is_some());
2237 assert!(result.strategy == Some("ALNS".to_string()));
2238 }
2239
2240 #[test]
2241 fn test_alns_strategy_all_placed() {
2242 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2244
2245 let boundary = Boundary2D::rectangle(100.0, 100.0);
2246 let config = Config::default().with_strategy(Strategy::Alns);
2247 let nester = Nester2D::new(config);
2248
2249 let result = nester.solve(&geometries, &boundary).unwrap();
2250
2251 assert_eq!(result.placements.len(), 4);
2253 assert!(result.unplaced.is_empty());
2254 }
2255
2256 #[test]
2257 fn test_blf_rotation_optimization() {
2258 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
2261 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
2263
2264 let boundary = Boundary2D::rectangle(35.0, 95.0);
2267 let nester = Nester2D::default_config();
2268
2269 let result = nester.solve(&geometries, &boundary).unwrap();
2270
2271 assert_eq!(
2273 result.placements.len(),
2274 3,
2275 "All pieces should be placed with rotation optimization"
2276 );
2277 assert!(result.unplaced.is_empty());
2278 }
2279
2280 #[test]
2281 fn test_blf_selects_best_rotation() {
2282 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
2284 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
2286
2287 let boundary = Boundary2D::rectangle(45.0, 50.0);
2291 let nester = Nester2D::default_config();
2292
2293 let result = nester.solve(&geometries, &boundary).unwrap();
2294
2295 assert_eq!(result.placements.len(), 2);
2296 assert!(result.unplaced.is_empty());
2297 }
2298
2299 #[test]
2300 fn test_progress_callback_blf() {
2301 use std::sync::atomic::{AtomicUsize, Ordering};
2302 use std::sync::Arc;
2303
2304 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2305 let boundary = Boundary2D::rectangle(50.0, 50.0);
2306 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2307 let nester = Nester2D::new(config);
2308
2309 let callback_count = Arc::new(AtomicUsize::new(0));
2310 let callback_count_clone = callback_count.clone();
2311 let last_items_placed = Arc::new(AtomicUsize::new(0));
2312 let last_items_placed_clone = last_items_placed.clone();
2313
2314 let callback: ProgressCallback = Box::new(move |info| {
2315 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2316 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
2317 });
2318
2319 let result = nester
2320 .solve_with_progress(&geometries, &boundary, callback)
2321 .unwrap();
2322
2323 let count = callback_count.load(Ordering::Relaxed);
2325 assert!(
2326 count >= 5,
2327 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
2328 count
2329 );
2330
2331 let final_placed = last_items_placed.load(Ordering::Relaxed);
2333 assert_eq!(final_placed, 4, "Should report 4 items placed");
2334
2335 assert_eq!(result.placements.len(), 4);
2337 }
2338
2339 #[test]
2340 fn test_progress_callback_nfp() {
2341 use std::sync::atomic::{AtomicUsize, Ordering};
2342 use std::sync::Arc;
2343
2344 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
2345 let boundary = Boundary2D::rectangle(50.0, 50.0);
2346 let config = Config::default().with_strategy(Strategy::NfpGuided);
2347 let nester = Nester2D::new(config);
2348
2349 let callback_count = Arc::new(AtomicUsize::new(0));
2350 let callback_count_clone = callback_count.clone();
2351
2352 let callback: ProgressCallback = Box::new(move |info| {
2353 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2354 assert!(info.items_placed <= info.total_items);
2355 });
2356
2357 let result = nester
2358 .solve_with_progress(&geometries, &boundary, callback)
2359 .unwrap();
2360
2361 let count = callback_count.load(Ordering::Relaxed);
2363 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
2364
2365 assert_eq!(result.placements.len(), 2);
2367 }
2368
2369 #[test]
2370 fn test_time_limit_honored() {
2371 let geometries: Vec<Geometry2D> = (0..100)
2373 .map(|i| Geometry2D::rectangle(format!("R{}", i), 5.0, 5.0))
2374 .collect();
2375 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
2376
2377 let config = Config::default()
2379 .with_strategy(Strategy::BottomLeftFill)
2380 .with_time_limit(1);
2381 let nester = Nester2D::new(config);
2382
2383 let result = nester.solve(&geometries, &boundary).unwrap();
2384
2385 assert!(
2388 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
2390 result.computation_time_ms
2391 );
2392 }
2393
2394 #[test]
2395 fn test_time_limit_zero_unlimited() {
2396 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2398 let boundary = Boundary2D::rectangle(50.0, 50.0);
2399
2400 let config = Config::default()
2401 .with_strategy(Strategy::BottomLeftFill)
2402 .with_time_limit(0); let nester = Nester2D::new(config);
2404
2405 let result = nester.solve(&geometries, &boundary).unwrap();
2406
2407 assert_eq!(result.placements.len(), 4);
2409 }
2410
2411 #[test]
2412 fn test_blf_bounds_clamping() {
2413 let gear_like = Geometry2D::new("gear")
2417 .with_polygon(vec![
2418 (50.0, 5.0), (65.0, 15.0),
2420 (77.0, 18.0),
2421 (80.0, 32.0),
2422 (95.0, 50.0), (80.0, 68.0),
2424 (77.0, 82.0),
2425 (65.0, 85.0),
2426 (50.0, 95.0), (35.0, 85.0),
2428 (23.0, 82.0),
2429 (20.0, 68.0),
2430 (5.0, 50.0), (20.0, 32.0),
2432 (23.0, 18.0),
2433 (35.0, 15.0),
2434 ])
2435 .with_quantity(1);
2436
2437 let boundary = Boundary2D::rectangle(100.0, 100.0);
2439
2440 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2441 let nester = Nester2D::new(config);
2442
2443 let result = nester
2444 .solve(std::slice::from_ref(&gear_like), &boundary)
2445 .unwrap();
2446
2447 assert_eq!(result.placements.len(), 1);
2448 let placement = &result.placements[0];
2449
2450 let origin_x = placement.position[0];
2452 let origin_y = placement.position[1];
2453
2454 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2456
2457 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2459
2460 let actual_min_x = origin_x + g_min[0];
2462 let actual_max_x = origin_x + g_max[0];
2463 let actual_min_y = origin_y + g_min[1];
2464 let actual_max_y = origin_y + g_max[1];
2465
2466 assert!(
2468 actual_min_x >= 0.0,
2469 "Left edge {} should be >= 0",
2470 actual_min_x
2471 );
2472 assert!(
2473 actual_max_x <= 100.0,
2474 "Right edge {} should be <= 100",
2475 actual_max_x
2476 );
2477 assert!(
2478 actual_min_y >= 0.0,
2479 "Bottom edge {} should be >= 0",
2480 actual_min_y
2481 );
2482 assert!(
2483 actual_max_y <= 100.0,
2484 "Top edge {} should be <= 100",
2485 actual_max_y
2486 );
2487 }
2488
2489 #[test]
2490 fn test_blf_bounds_clamping_many_pieces() {
2491 let gear_like = Geometry2D::new("gear")
2494 .with_polygon(vec![
2495 (50.0, 5.0),
2496 (65.0, 15.0),
2497 (77.0, 18.0),
2498 (80.0, 32.0),
2499 (95.0, 50.0),
2500 (80.0, 68.0),
2501 (77.0, 82.0),
2502 (65.0, 85.0),
2503 (50.0, 95.0),
2504 (35.0, 85.0),
2505 (23.0, 82.0),
2506 (20.0, 68.0),
2507 (5.0, 50.0),
2508 (20.0, 32.0),
2509 (23.0, 18.0),
2510 (35.0, 15.0),
2511 ])
2512 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
2516
2517 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2518 let nester = Nester2D::new(config);
2519
2520 let result = nester
2521 .solve(std::slice::from_ref(&gear_like), &boundary)
2522 .unwrap();
2523
2524 for (i, placement) in result.placements.iter().enumerate() {
2526 let origin_x = placement.position[0];
2527 let origin_y = placement.position[1];
2528 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2529
2530 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2531
2532 let actual_min_x = origin_x + g_min[0];
2533 let actual_max_x = origin_x + g_max[0];
2534 let actual_min_y = origin_y + g_min[1];
2535 let actual_max_y = origin_y + g_max[1];
2536
2537 assert!(
2538 actual_min_x >= -0.01,
2539 "Piece {}: Left edge {} should be >= 0",
2540 i,
2541 actual_min_x
2542 );
2543 assert!(
2544 actual_max_x <= 500.01,
2545 "Piece {}: Right edge {} should be <= 500",
2546 i,
2547 actual_max_x
2548 );
2549 assert!(
2550 actual_min_y >= -0.01,
2551 "Piece {}: Bottom edge {} should be >= 0",
2552 i,
2553 actual_min_y
2554 );
2555 assert!(
2556 actual_max_y <= 500.01,
2557 "Piece {}: Top edge {} should be <= 500",
2558 i,
2559 actual_max_y
2560 );
2561 }
2562 }
2563
2564 #[test]
2565 fn test_blf_bounds_trace() {
2566 let gear = Geometry2D::new("gear").with_polygon(vec![
2568 (50.0, 5.0),
2569 (65.0, 15.0),
2570 (77.0, 18.0),
2571 (80.0, 32.0),
2572 (95.0, 50.0),
2573 (80.0, 68.0),
2574 (77.0, 82.0),
2575 (65.0, 85.0),
2576 (50.0, 95.0),
2577 (35.0, 85.0),
2578 (23.0, 82.0),
2579 (20.0, 68.0),
2580 (5.0, 50.0),
2581 (20.0, 32.0),
2582 (23.0, 18.0),
2583 (35.0, 15.0),
2584 ]);
2585
2586 let (g_min, g_max) = gear.aabb();
2588 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
2589 assert!(
2590 (g_min[0] - 5.0).abs() < 0.01,
2591 "g_min[0] should be 5, got {}",
2592 g_min[0]
2593 );
2594 assert!(
2595 (g_max[0] - 95.0).abs() < 0.01,
2596 "g_max[0] should be 95, got {}",
2597 g_max[0]
2598 );
2599
2600 let b_max_x = 500.0;
2602 let margin = 0.0;
2603 let max_valid_x = b_max_x - margin - g_max[0];
2604 println!(
2605 "max_valid_x = {} - {} - {} = {}",
2606 b_max_x, margin, g_max[0], max_valid_x
2607 );
2608 assert!(
2609 (max_valid_x - 405.0).abs() < 0.01,
2610 "max_valid_x should be 405, got {}",
2611 max_valid_x
2612 );
2613
2614 let boundary = Boundary2D::rectangle(500.0, 500.0);
2616 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2617 let nester = Nester2D::new(config);
2618
2619 let result = nester
2620 .solve(&[gear.clone().with_quantity(1)], &boundary)
2621 .unwrap();
2622
2623 assert_eq!(result.placements.len(), 1);
2624 let p = &result.placements[0];
2625 let origin_x = p.position[0];
2626 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2627
2628 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2629 let actual_max_x = origin_x + g_max_r[0];
2630
2631 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2632 println!(
2633 "At rotation {}: g_min={:?}, g_max={:?}",
2634 rotation, g_min_r, g_max_r
2635 );
2636 println!(
2637 "Actual max x: {} + {} = {}",
2638 origin_x, g_max_r[0], actual_max_x
2639 );
2640
2641 assert!(
2642 actual_max_x <= 500.01,
2643 "Geometry exceeds boundary: max_x={} > 500",
2644 actual_max_x
2645 );
2646 }
2647
2648 #[test]
2649 fn test_blf_bounds_many_pieces_direct() {
2650 let gear = Geometry2D::new("gear")
2652 .with_polygon(vec![
2653 (50.0, 5.0),
2654 (65.0, 15.0),
2655 (77.0, 18.0),
2656 (80.0, 32.0),
2657 (95.0, 50.0),
2658 (80.0, 68.0),
2659 (77.0, 82.0),
2660 (65.0, 85.0),
2661 (50.0, 95.0),
2662 (35.0, 85.0),
2663 (23.0, 82.0),
2664 (20.0, 68.0),
2665 (5.0, 50.0),
2666 (20.0, 32.0),
2667 (23.0, 18.0),
2668 (35.0, 15.0),
2669 ])
2670 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2673 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2674 let nester = Nester2D::new(config);
2675
2676 let result = nester
2677 .solve(std::slice::from_ref(&gear), &boundary)
2678 .unwrap();
2679
2680 println!("Placed {} pieces", result.placements.len());
2681
2682 for (i, p) in result.placements.iter().enumerate() {
2684 let origin_x = p.position[0];
2685 let origin_y = p.position[1];
2686 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2687
2688 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2689
2690 let actual_min_x = origin_x + g_min_r[0];
2691 let actual_max_x = origin_x + g_max_r[0];
2692 let actual_min_y = origin_y + g_min_r[1];
2693 let actual_max_y = origin_y + g_max_r[1];
2694
2695 println!(
2696 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2697 i,
2698 origin_x,
2699 origin_y,
2700 rotation,
2701 actual_min_x,
2702 actual_max_x,
2703 actual_min_y,
2704 actual_max_y
2705 );
2706
2707 assert!(
2708 actual_max_x <= 500.01,
2709 "Piece {}: Right edge {} > 500",
2710 i,
2711 actual_max_x
2712 );
2713 assert!(
2714 actual_max_y <= 500.01,
2715 "Piece {}: Top edge {} > 500",
2716 i,
2717 actual_max_y
2718 );
2719 }
2720 }
2721
2722 #[test]
2723 fn test_blf_bounds_multi_strip() {
2724 let gear = Geometry2D::new("gear")
2726 .with_polygon(vec![
2727 (50.0, 5.0),
2728 (65.0, 15.0),
2729 (77.0, 18.0),
2730 (80.0, 32.0),
2731 (95.0, 50.0),
2732 (80.0, 68.0),
2733 (77.0, 82.0),
2734 (65.0, 85.0),
2735 (50.0, 95.0),
2736 (35.0, 85.0),
2737 (23.0, 82.0),
2738 (20.0, 68.0),
2739 (5.0, 50.0),
2740 (20.0, 32.0),
2741 (23.0, 18.0),
2742 (35.0, 15.0),
2743 ])
2744 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2747 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2748 let nester = Nester2D::new(config);
2749
2750 let result = nester
2752 .solve_multi_strip(std::slice::from_ref(&gear), &boundary)
2753 .unwrap();
2754
2755 println!(
2756 "Placed {} pieces across {} strips",
2757 result.placements.len(),
2758 result.boundaries_used
2759 );
2760
2761 let strip_width = 500.0;
2763 for (i, p) in result.placements.iter().enumerate() {
2764 let origin_x = p.position[0];
2765 let origin_y = p.position[1];
2766 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2767 let strip_idx = p.boundary_index;
2768
2769 let local_x = origin_x - (strip_idx as f64 * strip_width);
2771
2772 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2773
2774 let local_max_x = local_x + g_max_r[0];
2775 let local_max_y = origin_y + g_max_r[1];
2776
2777 println!(
2778 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2779 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2780 );
2781
2782 assert!(
2783 local_max_x <= 500.01,
2784 "Piece {}: In strip {}, local right edge {:.1} > 500",
2785 i,
2786 strip_idx,
2787 local_max_x
2788 );
2789 assert!(
2790 local_max_y <= 500.01,
2791 "Piece {}: Top edge {:.1} > 500",
2792 i,
2793 local_max_y
2794 );
2795 }
2796 }
2797
2798 #[test]
2799 fn test_blf_bounds_mixed_shapes() {
2800 let shapes = vec![
2802 Geometry2D::new("shape0")
2804 .with_polygon(vec![
2805 (0.0, 0.0),
2806 (180.0, 0.0),
2807 (195.0, 15.0),
2808 (200.0, 50.0),
2809 (200.0, 150.0),
2810 (195.0, 185.0),
2811 (180.0, 200.0),
2812 (20.0, 200.0),
2813 (5.0, 185.0),
2814 (0.0, 150.0),
2815 (0.0, 50.0),
2816 (5.0, 15.0),
2817 ])
2818 .with_quantity(2),
2819 Geometry2D::new("shape1")
2821 .with_polygon(vec![
2822 (60.0, 0.0),
2823 (85.0, 7.0),
2824 (104.0, 25.0),
2825 (118.0, 50.0),
2826 (120.0, 60.0),
2827 (118.0, 70.0),
2828 (104.0, 95.0),
2829 (85.0, 113.0),
2830 (60.0, 120.0),
2831 (35.0, 113.0),
2832 (16.0, 95.0),
2833 (2.0, 70.0),
2834 (0.0, 60.0),
2835 (2.0, 50.0),
2836 (16.0, 25.0),
2837 (35.0, 7.0),
2838 ])
2839 .with_quantity(4),
2840 Geometry2D::new("shape2")
2842 .with_polygon(vec![
2843 (0.0, 0.0),
2844 (80.0, 0.0),
2845 (80.0, 20.0),
2846 (20.0, 20.0),
2847 (20.0, 80.0),
2848 (0.0, 80.0),
2849 ])
2850 .with_quantity(6),
2851 Geometry2D::new("shape3")
2853 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2854 .with_quantity(6),
2855 Geometry2D::new("shape4")
2857 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2858 .with_quantity(4),
2859 Geometry2D::new("shape5")
2861 .with_polygon(vec![
2862 (15.0, 0.0),
2863 (45.0, 0.0),
2864 (60.0, 26.0),
2865 (45.0, 52.0),
2866 (15.0, 52.0),
2867 (0.0, 26.0),
2868 ])
2869 .with_quantity(8),
2870 Geometry2D::new("shape6")
2872 .with_polygon(vec![
2873 (0.0, 0.0),
2874 (90.0, 0.0),
2875 (90.0, 12.0),
2876 (55.0, 12.0),
2877 (55.0, 60.0),
2878 (35.0, 60.0),
2879 (35.0, 12.0),
2880 (0.0, 12.0),
2881 ])
2882 .with_quantity(4),
2883 Geometry2D::new("shape7")
2885 .with_polygon(vec![
2886 (0.0, 10.0),
2887 (10.0, 0.0),
2888 (70.0, 0.0),
2889 (80.0, 10.0),
2890 (80.0, 70.0),
2891 (70.0, 80.0),
2892 (10.0, 80.0),
2893 (0.0, 70.0),
2894 ])
2895 .with_quantity(3),
2896 Geometry2D::new("shape8_gear")
2898 .with_polygon(vec![
2899 (50.0, 5.0),
2900 (65.0, 15.0),
2901 (77.0, 18.0),
2902 (80.0, 32.0),
2903 (95.0, 50.0),
2904 (80.0, 68.0),
2905 (77.0, 82.0),
2906 (65.0, 85.0),
2907 (50.0, 95.0),
2908 (35.0, 85.0),
2909 (23.0, 82.0),
2910 (20.0, 68.0),
2911 (5.0, 50.0),
2912 (20.0, 32.0),
2913 (23.0, 18.0),
2914 (35.0, 15.0),
2915 ])
2916 .with_quantity(13),
2917 ];
2918
2919 let boundary = Boundary2D::rectangle(500.0, 500.0);
2921 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2922 let nester = Nester2D::new(config);
2923
2924 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2925
2926 println!(
2927 "Placed {} pieces across {} strips",
2928 result.placements.len(),
2929 result.boundaries_used
2930 );
2931
2932 let strip_width = 500.0;
2934 let gear_aabb = shapes[8].aabb();
2935 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2936
2937 let mut violations = Vec::new();
2938 for p in &result.placements {
2939 if p.geometry_id.as_str().starts_with("shape8") {
2940 let origin_x = p.position[0];
2941 let _origin_y = p.position[1];
2942 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2943 let strip_idx = p.boundary_index;
2944 let local_x = origin_x - (strip_idx as f64 * strip_width);
2945
2946 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2947 let local_max_x = local_x + g_max_r[0];
2948
2949 println!(
2950 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2951 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2952 );
2953
2954 if local_max_x > 500.01 {
2955 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2956 }
2957 }
2958 }
2959
2960 assert!(
2961 violations.is_empty(),
2962 "Found {} Gear pieces exceeding boundary: {:?}",
2963 violations.len(),
2964 violations
2965 );
2966 }
2967
2968 #[test]
2969 fn test_blf_bounds_expanded_like_benchmark() {
2970 type ShapeDef = (Vec<(f64, f64)>, usize, Vec<f64>);
2974 let shape_defs: Vec<ShapeDef> = vec![
2975 (
2976 vec![
2977 (0.0, 0.0),
2978 (180.0, 0.0),
2979 (195.0, 15.0),
2980 (200.0, 50.0),
2981 (200.0, 150.0),
2982 (195.0, 185.0),
2983 (180.0, 200.0),
2984 (20.0, 200.0),
2985 (5.0, 185.0),
2986 (0.0, 150.0),
2987 (0.0, 50.0),
2988 (5.0, 15.0),
2989 ],
2990 2,
2991 vec![0.0, 90.0, 180.0, 270.0],
2992 ),
2993 (
2994 vec![
2995 (60.0, 0.0),
2996 (85.0, 7.0),
2997 (104.0, 25.0),
2998 (118.0, 50.0),
2999 (120.0, 60.0),
3000 (118.0, 70.0),
3001 (104.0, 95.0),
3002 (85.0, 113.0),
3003 (60.0, 120.0),
3004 (35.0, 113.0),
3005 (16.0, 95.0),
3006 (2.0, 70.0),
3007 (0.0, 60.0),
3008 (2.0, 50.0),
3009 (16.0, 25.0),
3010 (35.0, 7.0),
3011 ],
3012 4,
3013 vec![0.0, 45.0, 90.0, 135.0],
3014 ),
3015 (
3016 vec![
3017 (0.0, 0.0),
3018 (80.0, 0.0),
3019 (80.0, 20.0),
3020 (20.0, 20.0),
3021 (20.0, 80.0),
3022 (0.0, 80.0),
3023 ],
3024 6,
3025 vec![0.0, 90.0, 180.0, 270.0],
3026 ),
3027 (
3028 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
3029 6,
3030 vec![0.0, 90.0, 180.0, 270.0],
3031 ),
3032 (
3033 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
3034 4,
3035 vec![0.0, 90.0],
3036 ),
3037 (
3038 vec![
3039 (15.0, 0.0),
3040 (45.0, 0.0),
3041 (60.0, 26.0),
3042 (45.0, 52.0),
3043 (15.0, 52.0),
3044 (0.0, 26.0),
3045 ],
3046 8,
3047 vec![0.0, 60.0, 120.0],
3048 ),
3049 (
3050 vec![
3051 (0.0, 0.0),
3052 (90.0, 0.0),
3053 (90.0, 12.0),
3054 (55.0, 12.0),
3055 (55.0, 60.0),
3056 (35.0, 60.0),
3057 (35.0, 12.0),
3058 (0.0, 12.0),
3059 ],
3060 4,
3061 vec![0.0, 90.0, 180.0, 270.0],
3062 ),
3063 (
3064 vec![
3065 (0.0, 10.0),
3066 (10.0, 0.0),
3067 (70.0, 0.0),
3068 (80.0, 10.0),
3069 (80.0, 70.0),
3070 (70.0, 80.0),
3071 (10.0, 80.0),
3072 (0.0, 70.0),
3073 ],
3074 3,
3075 vec![0.0, 90.0],
3076 ),
3077 (
3079 vec![
3080 (50.0, 5.0),
3081 (65.0, 15.0),
3082 (77.0, 18.0),
3083 (80.0, 32.0),
3084 (95.0, 50.0),
3085 (80.0, 68.0),
3086 (77.0, 82.0),
3087 (65.0, 85.0),
3088 (50.0, 95.0),
3089 (35.0, 85.0),
3090 (23.0, 82.0),
3091 (20.0, 68.0),
3092 (5.0, 50.0),
3093 (20.0, 32.0),
3094 (23.0, 18.0),
3095 (35.0, 15.0),
3096 ],
3097 13,
3098 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
3099 ),
3100 ];
3101
3102 let mut geometries = Vec::new();
3104 let mut piece_id = 0;
3105 for (vertices, demand, rotations) in shape_defs.iter() {
3106 for _ in 0..*demand {
3107 let geom = Geometry2D::new(format!("piece_{}", piece_id))
3108 .with_polygon(vertices.clone())
3109 .with_rotations_deg(rotations.clone());
3110 geometries.push(geom);
3111 piece_id += 1;
3112 }
3113 }
3114
3115 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
3117 let (gear_min, gear_max) = gear_geom.aabb();
3118 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
3119
3120 let boundary = Boundary2D::rectangle(500.0, 500.0);
3121 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3122 let nester = Nester2D::new(config);
3123
3124 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3125
3126 println!(
3127 "Placed {} pieces across {} strips",
3128 result.placements.len(),
3129 result.boundaries_used
3130 );
3131
3132 let strip_width = 500.0;
3134 let mut violations = Vec::new();
3135
3136 for p in &result.placements {
3137 let id_num: usize = p
3138 .geometry_id
3139 .as_str()
3140 .strip_prefix("piece_")
3141 .and_then(|s| s.parse().ok())
3142 .unwrap_or(0);
3143
3144 if (37..=49).contains(&id_num) {
3146 let origin_x = p.position[0];
3147 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3148 let strip_idx = p.boundary_index;
3149 let local_x = origin_x - (strip_idx as f64 * strip_width);
3150
3151 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
3152 let local_max_x = local_x + g_max_r[0];
3153
3154 println!(
3155 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
3156 p.geometry_id, strip_idx, local_x, rotation, local_max_x
3157 );
3158
3159 if local_max_x > 500.01 {
3160 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
3161 }
3162 }
3163 }
3164
3165 assert!(
3166 violations.is_empty(),
3167 "Found {} Gear pieces exceeding boundary: {:?}",
3168 violations.len(),
3169 violations
3170 );
3171 }
3172
3173 fn aabbs_overlap(
3175 a_min: [f64; 2],
3176 a_max: [f64; 2],
3177 b_min: [f64; 2],
3178 b_max: [f64; 2],
3179 tolerance: f64,
3180 ) -> bool {
3181 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
3183 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
3184 x_overlap && y_overlap
3185 }
3186
3187 #[test]
3189 fn test_all_strategies_boundary_and_overlap() {
3190 use std::collections::HashMap;
3191
3192 let shapes = vec![
3194 Geometry2D::new("shape0")
3195 .with_polygon(vec![
3196 (0.0, 0.0),
3197 (180.0, 0.0),
3198 (195.0, 15.0),
3199 (200.0, 50.0),
3200 (200.0, 150.0),
3201 (195.0, 185.0),
3202 (180.0, 200.0),
3203 (20.0, 200.0),
3204 (5.0, 185.0),
3205 (0.0, 150.0),
3206 (0.0, 50.0),
3207 (5.0, 15.0),
3208 ])
3209 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3210 .with_quantity(2),
3211 Geometry2D::new("shape1_flange")
3212 .with_polygon(vec![
3213 (60.0, 0.0),
3214 (85.0, 7.0),
3215 (104.0, 25.0),
3216 (118.0, 50.0),
3217 (120.0, 60.0),
3218 (118.0, 70.0),
3219 (104.0, 95.0),
3220 (85.0, 113.0),
3221 (60.0, 120.0),
3222 (35.0, 113.0),
3223 (16.0, 95.0),
3224 (2.0, 70.0),
3225 (0.0, 60.0),
3226 (2.0, 50.0),
3227 (16.0, 25.0),
3228 (35.0, 7.0),
3229 ])
3230 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
3231 .with_quantity(4),
3232 Geometry2D::new("shape2_lbracket")
3233 .with_polygon(vec![
3234 (0.0, 0.0),
3235 (80.0, 0.0),
3236 (80.0, 20.0),
3237 (20.0, 20.0),
3238 (20.0, 80.0),
3239 (0.0, 80.0),
3240 ])
3241 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3242 .with_quantity(6),
3243 Geometry2D::new("shape3_triangle")
3244 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
3245 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3246 .with_quantity(6),
3247 Geometry2D::new("shape4_rect")
3248 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
3249 .with_rotations_deg(vec![0.0, 90.0])
3250 .with_quantity(4),
3251 Geometry2D::new("shape5_hexagon")
3252 .with_polygon(vec![
3253 (15.0, 0.0),
3254 (45.0, 0.0),
3255 (60.0, 26.0),
3256 (45.0, 52.0),
3257 (15.0, 52.0),
3258 (0.0, 26.0),
3259 ])
3260 .with_rotations_deg(vec![0.0, 60.0, 120.0])
3261 .with_quantity(8),
3262 Geometry2D::new("shape6_tstiff")
3263 .with_polygon(vec![
3264 (0.0, 0.0),
3265 (90.0, 0.0),
3266 (90.0, 12.0),
3267 (55.0, 12.0),
3268 (55.0, 60.0),
3269 (35.0, 60.0),
3270 (35.0, 12.0),
3271 (0.0, 12.0),
3272 ])
3273 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3274 .with_quantity(4),
3275 Geometry2D::new("shape7_mount")
3276 .with_polygon(vec![
3277 (0.0, 10.0),
3278 (10.0, 0.0),
3279 (70.0, 0.0),
3280 (80.0, 10.0),
3281 (80.0, 70.0),
3282 (70.0, 80.0),
3283 (10.0, 80.0),
3284 (0.0, 70.0),
3285 ])
3286 .with_rotations_deg(vec![0.0, 90.0])
3287 .with_quantity(3),
3288 Geometry2D::new("shape8_gear")
3289 .with_polygon(vec![
3290 (50.0, 5.0),
3291 (65.0, 15.0),
3292 (77.0, 18.0),
3293 (80.0, 32.0),
3294 (95.0, 50.0),
3295 (80.0, 68.0),
3296 (77.0, 82.0),
3297 (65.0, 85.0),
3298 (50.0, 95.0),
3299 (35.0, 85.0),
3300 (23.0, 82.0),
3301 (20.0, 68.0),
3302 (5.0, 50.0),
3303 (20.0, 32.0),
3304 (23.0, 18.0),
3305 (35.0, 15.0),
3306 ])
3307 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
3308 .with_quantity(13),
3309 ];
3310
3311 let geom_map: HashMap<String, &Geometry2D> =
3313 shapes.iter().map(|g| (g.id().clone(), g)).collect();
3314
3315 let boundary = Boundary2D::rectangle(500.0, 500.0);
3316 let strip_width = 500.0;
3317
3318 let strategies = vec![
3320 Strategy::BottomLeftFill,
3321 Strategy::NfpGuided,
3322 Strategy::GeneticAlgorithm,
3323 Strategy::Brkga,
3324 Strategy::SimulatedAnnealing,
3325 Strategy::Gdrr,
3326 Strategy::Alns,
3327 ];
3328
3329 for strategy in strategies {
3330 println!("\n========== Testing {:?} ==========", strategy);
3331
3332 let config = Config::default()
3333 .with_strategy(strategy)
3334 .with_time_limit(30000); let nester = Nester2D::new(config);
3336
3337 let result = match nester.solve_multi_strip(&shapes, &boundary) {
3338 Ok(r) => r,
3339 Err(e) => {
3340 println!(" Strategy {:?} failed: {}", strategy, e);
3341 continue;
3342 }
3343 };
3344
3345 println!(
3346 " Placed {} pieces across {} strips",
3347 result.placements.len(),
3348 result.boundaries_used
3349 );
3350
3351 let mut boundary_violations = Vec::new();
3353 for p in &result.placements {
3354 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
3356 let full_id = if base_id.starts_with("shape") {
3357 shapes
3359 .iter()
3360 .find(|g| p.geometry_id.starts_with(g.id()))
3361 .map(|g| g.id().as_str())
3362 } else {
3363 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
3364 };
3365
3366 let geom = match full_id.and_then(|id| geom_map.get(id)) {
3367 Some(g) => *g,
3368 None => {
3369 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
3371 Some(g) => g,
3372 None => {
3373 println!(
3374 " WARNING: Could not find geometry for {}",
3375 p.geometry_id
3376 );
3377 continue;
3378 }
3379 }
3380 }
3381 };
3382
3383 let origin_x = p.position[0];
3384 let origin_y = p.position[1];
3385 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3386 let strip_idx = p.boundary_index;
3387
3388 let local_x = origin_x - (strip_idx as f64 * strip_width);
3390
3391 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
3392
3393 let local_min_x = local_x + g_min[0];
3395 let local_max_x = local_x + g_max[0];
3396 let local_min_y = origin_y + g_min[1];
3397 let local_max_y = origin_y + g_max[1];
3398
3399 let tolerance = 0.1;
3401 if local_min_x < -tolerance
3402 || local_max_x > 500.0 + tolerance
3403 || local_min_y < -tolerance
3404 || local_max_y > 500.0 + tolerance
3405 {
3406 boundary_violations.push(format!(
3407 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
3408 p.geometry_id,
3409 strip_idx,
3410 local_min_x,
3411 local_min_y,
3412 local_max_x,
3413 local_max_y
3414 ));
3415 }
3416 }
3417
3418 if !boundary_violations.is_empty() {
3419 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
3420 for v in &boundary_violations {
3421 println!(" - {}", v);
3422 }
3423 }
3424
3425 let mut overlaps = Vec::new();
3427 let placements: Vec<_> = result.placements.iter().collect();
3428
3429 for i in 0..placements.len() {
3430 for j in (i + 1)..placements.len() {
3431 let p1 = placements[i];
3432 let p2 = placements[j];
3433
3434 if p1.boundary_index != p2.boundary_index {
3436 continue;
3437 }
3438
3439 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
3441 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
3442
3443 let (g1, g2) = match (g1, g2) {
3444 (Some(a), Some(b)) => (a, b),
3445 _ => continue,
3446 };
3447
3448 let strip_idx = p1.boundary_index;
3449 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
3450 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
3451
3452 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
3453 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
3454
3455 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
3456 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
3457
3458 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
3459 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
3460 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
3461 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
3462
3463 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
3464 overlaps.push(format!(
3465 "{} and {} in strip {}",
3466 p1.geometry_id, p2.geometry_id, strip_idx
3467 ));
3468 }
3469 }
3470 }
3471
3472 if !overlaps.is_empty() {
3473 println!(" OVERLAPS ({}):", overlaps.len());
3474 for o in overlaps.iter().take(10) {
3475 println!(" - {}", o);
3476 }
3477 if overlaps.len() > 10 {
3478 println!(" ... and {} more", overlaps.len() - 10);
3479 }
3480 }
3481
3482 assert!(
3484 boundary_violations.is_empty(),
3485 "{:?}: Found {} boundary violations",
3486 strategy,
3487 boundary_violations.len()
3488 );
3489
3490 println!(" ✓ All placements within boundary");
3491 println!(" ✓ No AABB overlaps detected");
3492 }
3493 }
3494
3495 #[test]
3503 fn test_multi_strip_distributes_all_instances() {
3504 let geometries = vec![Geometry2D::rectangle("part", 100.0, 100.0).with_quantity(20)];
3505 let boundary = Boundary2D::rectangle(300.0, 300.0);
3506 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3507 let nester = Nester2D::new(config);
3508
3509 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3510
3511 assert_eq!(
3513 result.placements.len(),
3514 20,
3515 "all 20 instances must be placed"
3516 );
3517 assert_eq!(
3518 result.boundaries_used, 3,
3519 "20 of 100x100 in 300x300 => 3 sheets"
3520 );
3521 assert!(
3522 result.unplaced.is_empty(),
3523 "nothing should be unplaced, got {:?}",
3524 result.unplaced
3525 );
3526 assert_eq!(result.total_requested, 20);
3528
3529 let mut seen = std::collections::HashSet::new();
3532 for p in &result.placements {
3533 assert!(
3534 seen.insert((p.geometry_id.clone(), p.instance)),
3535 "duplicate (id, instance) = ({}, {}) across sheets",
3536 p.geometry_id,
3537 p.instance
3538 );
3539 }
3540 let mut sheets: Vec<usize> = result.placements.iter().map(|p| p.boundary_index).collect();
3542 sheets.sort_unstable();
3543 sheets.dedup();
3544 assert_eq!(sheets, vec![0, 1, 2]);
3545 }
3546
3547 #[test]
3550 fn test_multi_strip_oversized_reported_unplaced() {
3551 let geometries = vec![
3552 Geometry2D::rectangle("ok", 50.0, 50.0).with_quantity(2),
3553 Geometry2D::rectangle("toobig", 400.0, 400.0).with_quantity(3),
3554 ];
3555 let boundary = Boundary2D::rectangle(300.0, 300.0);
3556 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3557 let nester = Nester2D::new(config);
3558
3559 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3560
3561 assert_eq!(result.total_requested, 5, "2 + 3 instances requested");
3562 assert_eq!(result.placements.len(), 2);
3564 assert!(
3565 result.unplaced.contains(&"toobig".to_string()),
3566 "oversized geometry must surface in unplaced, got {:?}",
3567 result.unplaced
3568 );
3569 }
3570}