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