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 pub fn cancel_handle(&self) -> Arc<AtomicBool> {
257 self.cancelled.clone()
258 }
259
260 pub fn default_config() -> Self {
262 Self::new(Config::default())
263 }
264
265 fn iteration_budget<T: TryFrom<u64> + Copy>(&self, tuned: T) -> T {
268 self.config
269 .max_iterations
270 .map_or(tuned, |n| T::try_from(n).unwrap_or(tuned))
271 }
272
273 fn ga_config(&self) -> GaConfig {
277 let config = GaConfig::default()
278 .with_population_size(self.config.population_size.unwrap_or(SEARCH_POPULATION))
279 .with_max_generations(self.config.max_generations.unwrap_or(SEARCH_GENERATIONS))
280 .with_crossover_rate(self.config.crossover_rate)
281 .with_mutation_rate(self.config.mutation_rate)
282 .with_elite_count(self.config.elite_count);
283 match self.target_fitness() {
284 Some(target) => config.with_target_fitness(target),
285 None => config,
286 }
287 }
288
289 fn brkga_config(&self) -> BrkgaConfig {
292 let config = BrkgaConfig::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_elite_fraction(0.2)
296 .with_mutant_fraction(0.15)
297 .with_elite_bias(0.7);
298 match self.target_fitness() {
299 Some(target) => config.with_target_fitness(target),
300 None => config,
301 }
302 }
303
304 fn gdrr_config(&self, time_limit_ms: u64) -> GdrrConfig {
306 GdrrConfig {
307 seed: self.config.seed,
308 ..GdrrConfig::default()
309 .with_max_iterations(self.iteration_budget(1000))
310 .with_time_limit_ms(time_limit_ms)
311 .with_ruin_ratio(0.1, 0.3) .with_lahc_list_length(30) }
314 }
315
316 fn alns_config(&self, time_limit_ms: u64) -> AlnsConfig {
318 AlnsConfig {
319 seed: self.config.seed,
320 ..AlnsConfig::default()
321 .with_max_iterations(self.iteration_budget(1000))
322 .with_time_limit_ms(time_limit_ms)
323 .with_segment_size(50) .with_scores(33.0, 9.0, 13.0)
325 .with_reaction_factor(0.15) .with_temperature(100.0, 0.999, 0.1) }
328 }
329
330 fn bottom_left_fill(
340 &self,
341 geometries: &[Geometry2D],
342 boundary: &Boundary2D,
343 ) -> Result<SolveResult<f64>> {
344 self.bottom_left_fill_impl(geometries, boundary, None)
345 }
346
347 fn nfp_guided_blf_impl(
350 &self,
351 geometries: &[Geometry2D],
352 boundary: &Boundary2D,
353 callback: Option<&ProgressCallback>,
354 ) -> Result<SolveResult<f64>> {
355 let start = Timer::now();
356 let mut result = SolveResult::new();
357 let mut placements = Vec::new();
358 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
359 let report = |info: ProgressInfo| {
360 if let Some(callback) = callback {
361 callback(info);
362 }
363 };
364 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
365 let mut placed_count = 0usize;
366 report(
367 ProgressInfo::new()
368 .with_phase("NFP Placement")
369 .with_items(0, total_pieces)
370 .with_elapsed(0),
371 );
372
373 let margin = self.config.margin;
374 let spacing = self.config.spacing;
375
376 let boundary_polygon = inset_boundary(boundary, margin);
378
379 let mut total_placed_area = 0.0;
380
381 let sample_step = self.compute_sample_step(geometries);
383
384 for geom in geometries {
385 geom.validate()?;
386
387 let rotation_angles: Vec<f64> = geom.rotations();
390
391 let mirror_candidates = mirror_candidates(geom);
392
393 for instance in 0..geom.quantity() {
394 if let Some(stop) = self.stop_requested(&start) {
396 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
397 result.utilization = total_placed_area / boundary.measure();
398 result.computation_time_ms = start.elapsed_ms();
399 result.placements = placements;
400 result.stop_reason = stop;
401 report(
402 ProgressInfo::new()
403 .with_phase(stop_phase(stop))
404 .with_items(placed_count, total_pieces)
405 .with_elapsed(result.computation_time_ms)
406 .finished(),
407 );
408 return Ok(result);
409 }
410
411 let mut best_placement: Option<(f64, f64, f64, bool)> = None; for &rotation in &rotation_angles {
415 for &mirror in mirror_candidates {
416 let ifp = match compute_ifp_with_margin_and_mirror(
418 &boundary_polygon,
419 geom,
420 rotation,
421 0.0,
423 mirror,
424 ) {
425 Ok(ifp) => ifp,
426 Err(_) => continue,
427 };
428
429 if ifp.is_empty() {
430 continue;
431 }
432
433 let mut nfps: Vec<Nfp> = Vec::new();
435 for placed in &placed_geometries {
436 let cache_key = (
439 placed.geometry.id().as_str(),
440 geom.id().as_str(),
441 rotation - placed.rotation, placed.mirrored,
443 mirror,
444 );
445
446 let nfp_at_origin =
451 match self.nfp_cache.get_or_compute_mirrored(cache_key, || {
452 let placed_at_origin = placed.geometry.clone();
453 compute_nfp_mirrored(
454 &placed_at_origin,
455 geom,
456 rotation - placed.rotation,
457 placed.mirrored,
458 mirror,
459 )
460 .map(|nfp| offset_nfp(&nfp, spacing))
464 }) {
465 Ok(nfp) => nfp,
466 Err(_) => continue,
467 };
468
469 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
472 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
473 nfps.push(translated_nfp);
474 }
475
476 nfps.extend(hole_nfps(boundary, geom, rotation, mirror, margin));
481 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
482 if let Some((x, y)) = find_bottom_left_placement(
483 &ifp,
484 &nfp_refs,
485 sample_step,
486 PackingAxis::of(boundary),
487 ) {
488 let is_better =
490 match best_placement {
491 None => true,
492 Some((best_x, best_y, _, _)) => PackingAxis::of(boundary)
493 .precedes((x, y), (best_x, best_y), 1e-6),
494 };
495 if is_better {
496 best_placement = Some((x, y, rotation, mirror));
497 }
498 }
499 }
500 }
501
502 if let Some((x, y, rotation, mirror)) = best_placement {
504 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
511 let boundary_aabb = boundary.aabb();
512
513 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
514 x,
515 y,
516 geom_aabb,
517 boundary_aabb,
518 margin,
519 ) {
520 let placement = Placement::new_2d(
521 geom.id().clone(),
522 instance,
523 clamped_x,
524 clamped_y,
525 rotation,
526 )
527 .with_mirrored(mirror);
528
529 placements.push(placement);
530 placed_geometries.push(
531 PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation)
532 .with_mirrored(mirror),
533 );
534 total_placed_area += geom.measure();
535 placed_count += 1;
536 report(
537 ProgressInfo::new()
538 .with_phase("NFP Placement")
539 .with_items(placed_count, total_pieces)
540 .with_utilization(total_placed_area / boundary.measure())
541 .with_elapsed(start.elapsed_ms()),
542 );
543 } else {
544 result.unplaced.push(geom.id().clone());
546 }
547 } else {
548 result.unplaced.push(geom.id().clone());
550 }
551 }
552 }
553
554 result.placements = placements;
555 result.boundaries_used = 1;
556 result.utilization = total_placed_area / boundary.measure();
557 result.computation_time_ms = start.elapsed_ms();
558 report(
559 ProgressInfo::new()
560 .with_phase("Complete")
561 .with_items(placed_count, total_pieces)
562 .with_utilization(result.utilization)
563 .with_elapsed(result.computation_time_ms)
564 .finished(),
565 );
566
567 Ok(result)
568 }
569
570 fn compute_sample_step(&self, geometries: &[Geometry2D]) -> f64 {
572 if geometries.is_empty() {
573 return 1.0;
574 }
575
576 let mut min_dim = f64::INFINITY;
578 for geom in geometries {
579 let (g_min, g_max) = geom.aabb();
580 let width = g_max[0] - g_min[0];
581 let height = g_max[1] - g_min[1];
582 min_dim = min_dim.min(width).min(height);
583 }
584
585 (min_dim / 4.0).clamp(0.5, 10.0)
587 }
588
589 fn not_worse_than_baselines(
599 &self,
600 meta: SolveResult<f64>,
601 geometries: &[Geometry2D],
602 boundary: &Boundary2D,
603 greedy: Option<SolveResult<f64>>,
604 ) -> SolveResult<f64> {
605 let baselines = self
606 .bottom_left_fill(geometries, boundary)
607 .ok()
608 .into_iter()
609 .chain(greedy);
610 let quality = |r: &SolveResult<f64>| solution_quality(r, geometries, boundary);
611 meta.floored_at(baselines, |a, b| {
614 let (a, b) = (quality(a), quality(b));
615 a.0 > b.0 || (a.0 == b.0 && a.1 < b.1 - 1e-6)
616 })
617 }
618
619 fn target_fitness(&self) -> Option<f64> {
622 self.config
623 .target_utilization
624 .map(|target| nesting_fitness(1, 1, target))
625 }
626
627 fn search_budget_ms(&self) -> u64 {
637 match self.config.time_limit_ms {
638 0 => u64::MAX / 2,
639 limit => limit,
640 }
641 }
642
643 fn greedy_start(
650 &self,
651 geometries: &[Geometry2D],
652 boundary: &Boundary2D,
653 budget_ms: u64,
654 progress: Option<&ProgressCallback>,
655 ) -> (Option<SolveResult<f64>>, u64) {
656 let started = Timer::now();
657 let greedy = self
658 .with_time_limit(budget_ms)
659 .nfp_guided_blf_impl(geometries, boundary, progress)
660 .ok();
661 (
662 greedy,
663 budget_ms.saturating_sub(started.elapsed_ms()).max(1),
664 )
665 }
666
667 fn genetic_algorithm(
674 &self,
675 geometries: &[Geometry2D],
676 boundary: &Boundary2D,
677 progress: Option<&ProgressCallback>,
678 ) -> Result<SolveResult<f64>> {
679 let (time_limit_ms, greedy) = {
680 let budget = self.search_budget_ms();
681 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
682 (remaining, greedy)
683 };
684
685 let ga_config = self
686 .ga_config()
687 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
688
689 let result = run_ga_nesting(
690 geometries,
691 boundary,
692 &self.config,
693 ga_config,
694 self.cancelled.clone(),
695 greedy.as_ref().map(|g| g.placements.as_slice()),
696 progress,
697 )?;
698
699 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
700 }
701
702 fn brkga(
706 &self,
707 geometries: &[Geometry2D],
708 boundary: &Boundary2D,
709 progress: Option<&ProgressCallback>,
710 ) -> Result<SolveResult<f64>> {
711 let (time_limit_ms, greedy) = {
712 let budget = self.search_budget_ms();
713 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
714 (remaining, greedy)
715 };
716
717 let brkga_config = self
718 .brkga_config()
719 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
720
721 let result = run_brkga_nesting(
722 geometries,
723 boundary,
724 &self.config,
725 brkga_config,
726 self.cancelled.clone(),
727 greedy.as_ref().map(|g| g.placements.as_slice()),
728 progress,
729 )?;
730
731 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
732 }
733
734 fn simulated_annealing(
739 &self,
740 geometries: &[Geometry2D],
741 boundary: &Boundary2D,
742 progress: Option<&ProgressCallback>,
743 ) -> Result<SolveResult<f64>> {
744 let (time_limit_ms, greedy) = {
745 let budget = self.search_budget_ms();
746 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
747 (remaining, greedy)
748 };
749
750 let sa_config = SaConfig::default()
751 .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))
756 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
757 let sa_config = match self.target_fitness() {
758 Some(target) => sa_config.with_target_fitness(target),
759 None => sa_config,
760 };
761
762 let result = run_sa_nesting(
763 geometries,
764 boundary,
765 &self.config,
766 sa_config,
767 self.cancelled.clone(),
768 greedy.as_ref().map(|g| g.placements.as_slice()),
769 progress,
770 )?;
771
772 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
773 }
774
775 fn gdrr(
777 &self,
778 geometries: &[Geometry2D],
779 boundary: &Boundary2D,
780 progress: Option<&ProgressCallback>,
781 ) -> Result<SolveResult<f64>> {
782 let (time_limit, greedy) = {
783 let budget = self.search_budget_ms();
784 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget, progress);
785 (remaining, greedy)
786 };
787 let result = run_gdrr_nesting(
788 geometries,
789 boundary,
790 &self.config,
791 &self.gdrr_config(time_limit),
792 self.cancelled.clone(),
793 progress,
794 )?;
795
796 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
797 }
798
799 fn alns(
801 &self,
802 geometries: &[Geometry2D],
803 boundary: &Boundary2D,
804 progress: Option<&ProgressCallback>,
805 ) -> Result<SolveResult<f64>> {
806 self.alns_within(geometries, boundary, self.search_budget_ms(), progress)
807 }
808
809 fn alns_within(
812 &self,
813 geometries: &[Geometry2D],
814 boundary: &Boundary2D,
815 budget_ms: u64,
816 progress: Option<&ProgressCallback>,
817 ) -> Result<SolveResult<f64>> {
818 let (greedy, time_limit) = self.greedy_start(geometries, boundary, budget_ms, progress);
819 let result = run_alns_nesting(
820 geometries,
821 boundary,
822 &self.config,
823 &self.alns_config(time_limit),
824 self.cancelled.clone(),
825 progress,
826 )?;
827
828 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
829 }
830
831 #[cfg(feature = "milp")]
844 fn milp_exact(
845 &self,
846 geometries: &[Geometry2D],
847 boundary: &Boundary2D,
848 ) -> Result<SolveResult<f64>> {
849 let exact_config = ExactConfig::default()
854 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms))
855 .with_max_items(15)
856 .with_rotation_steps(4)
857 .with_grid_step(milp_grid_step(geometries, boundary));
858
859 run_nfp_cm_nesting(
860 geometries,
861 boundary,
862 &self.config,
863 &exact_config,
864 self.cancelled.clone(),
865 )
866 }
867
868 #[cfg(feature = "milp")]
870 fn hybrid_exact(
871 &self,
872 geometries: &[Geometry2D],
873 boundary: &Boundary2D,
874 ) -> Result<SolveResult<f64>> {
875 let started = Timer::now();
876 let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
878
879 if total_instances <= 15 {
881 let exact_config = ExactConfig::default()
885 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms / 2))
886 .with_max_items(15);
887
888 let exact_result = run_nfp_cm_nesting(
889 geometries,
890 boundary,
891 &self.config,
892 &exact_config,
893 self.cancelled.clone(),
894 )?;
895
896 if !exact_result.placements.is_empty() {
898 return Ok(exact_result);
899 }
900 }
901
902 let budget = match self.config.time_limit_ms {
907 0 => self.search_budget_ms(),
908 limit => limit.saturating_sub(started.elapsed_ms()).max(1),
909 };
910 self.alns_within(geometries, boundary, budget, None)
911 }
912
913 fn bottom_left_fill_impl(
921 &self,
922 geometries: &[Geometry2D],
923 boundary: &Boundary2D,
924 callback: Option<&ProgressCallback>,
925 ) -> Result<SolveResult<f64>> {
926 let start = Timer::now();
927 let mut result = SolveResult::new();
928 let mut placements = Vec::new();
929 let report = |info: ProgressInfo| {
930 if let Some(callback) = callback {
931 callback(info);
932 }
933 };
934
935 let axis = PackingAxis::of(boundary);
936 let to_ca = |x: f64, y: f64| match axis {
938 PackingAxis::Y => (x, y),
939 PackingAxis::X => (y, x),
940 };
941 let from_ca = |across: f64, along: f64| match axis {
942 PackingAxis::Y => (across, along),
943 PackingAxis::X => (along, across),
944 };
945
946 let (b_min, b_max) = boundary.aabb();
947 let margin = self.config.margin;
948 let spacing = self.config.spacing;
949 let (min_across, min_along) = to_ca(b_min[0] + margin, b_min[1] + margin);
950 let (max_across, max_along) = to_ca(b_max[0] - margin, b_max[1] - margin);
951
952 let hole_boxes: Vec<(f64, f64, f64, f64)> = boundary
959 .holes()
960 .iter()
961 .filter(|hole| !hole.is_empty())
962 .map(|hole| {
963 let (mut x0, mut y0) = (f64::INFINITY, f64::INFINITY);
964 let (mut x1, mut y1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
965 for &(x, y) in hole {
966 x0 = x0.min(x);
967 y0 = y0.min(y);
968 x1 = x1.max(x);
969 y1 = y1.max(y);
970 }
971 let (a0, l0) = to_ca(x0 - margin, y0 - margin);
972 let (a1, l1) = to_ca(x1 + margin, y1 + margin);
973 (a0.min(a1), l0.min(l1), a0.max(a1), l0.max(l1))
974 })
975 .collect();
976
977 let blocking_hole_edge = |across: f64, along: f64, ea: f64, el: f64| -> Option<f64> {
981 hole_boxes
982 .iter()
983 .filter(|(a0, l0, a1, l1)| {
984 across < *a1 && across + ea > *a0 && along < *l1 && along + el > *l0
985 })
986 .map(|(_, _, a1, _)| *a1)
987 .fold(None, |far: Option<f64>, a1| {
988 Some(far.map_or(a1, |f| f.max(a1)))
989 })
990 };
991
992 let mut cursor_across = min_across;
993 let mut cursor_along = min_along;
994 let mut row_depth = 0.0_f64;
995 let mut total_placed_area = 0.0;
996 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
997 let mut placed_count = 0usize;
998
999 report(
1000 ProgressInfo::new()
1001 .with_phase("BLF Placement")
1002 .with_items(0, total_pieces)
1003 .with_elapsed(0),
1004 );
1005
1006 for geom in geometries {
1007 geom.validate()?;
1008
1009 let rotation_angles: Vec<f64> = geom.rotations();
1011
1012 for instance in 0..geom.quantity() {
1013 if let Some(stop) = self.stop_requested(&start) {
1015 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
1016 result.utilization = total_placed_area / boundary.measure();
1017 result.computation_time_ms = start.elapsed_ms();
1018 result.placements = placements;
1019 result.stop_reason = stop;
1020 report(
1021 ProgressInfo::new()
1022 .with_phase(stop_phase(stop))
1023 .with_items(placed_count, total_pieces)
1024 .with_elapsed(result.computation_time_ms)
1025 .finished(),
1026 );
1027 return Ok(result);
1028 }
1029
1030 let mut best_fit: Option<BlfCandidate> = None;
1033
1034 for &rotation in &rotation_angles {
1035 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
1036 let (extent_across, extent_along) =
1037 to_ca(g_max[0] - g_min[0], g_max[1] - g_min[1]);
1038
1039 if extent_across > max_across - min_across
1040 || extent_along > max_along - min_along
1041 {
1042 continue;
1043 }
1044
1045 let mut place_across = cursor_across;
1046 let mut place_along = cursor_along;
1047 let mut depth = row_depth;
1050 let mut opened_row = false;
1051 let mut turns = 0usize;
1057 let max_turns = 4 * (hole_boxes.len() + 1) * (hole_boxes.len() + 2) + 16;
1058 let fits = loop {
1059 if place_across + extent_across > max_across {
1060 place_across = min_across;
1061 place_along += depth + spacing;
1062 depth = extent_along;
1063 opened_row = true;
1064 }
1065 if place_along + extent_along > max_along {
1066 break false;
1067 }
1068 match blocking_hole_edge(
1069 place_across,
1070 place_along,
1071 extent_across,
1072 extent_along,
1073 ) {
1074 None => break true,
1075 Some(past) => place_across = past + spacing,
1076 }
1077 turns += 1;
1078 if turns > max_turns {
1079 break false;
1080 }
1081 };
1082 if !fits {
1083 continue;
1084 }
1085
1086 let cost = |c: &BlfCandidate| {
1091 if c.opened_row {
1092 c.place_along - min_along + c.extent_along
1093 } else {
1094 c.place_across - min_across + c.extent_across
1095 }
1096 };
1097 let candidate = BlfCandidate {
1098 rotation,
1099 extent_across,
1100 extent_along,
1101 place_across,
1102 place_along,
1103 opened_row,
1104 };
1105 let better = best_fit
1106 .as_ref()
1107 .is_none_or(|best| cost(&candidate) < cost(best) - 1e-6);
1108 if better {
1109 best_fit = Some(candidate);
1110 }
1111 }
1112
1113 let Some(BlfCandidate {
1114 rotation,
1115 extent_across,
1116 extent_along,
1117 place_across,
1118 place_along,
1119 opened_row,
1120 }) = best_fit
1121 else {
1122 result.unplaced.push(geom.id().clone());
1123 continue;
1124 };
1125 if opened_row {
1126 row_depth = 0.0;
1127 }
1128
1129 let geom_aabb = geom.aabb_at_rotation(rotation);
1130 let (place_x, place_y) = from_ca(place_across, place_along);
1131 let Some((x, y)) = clamp_placement_to_boundary_with_margin(
1132 place_x - geom_aabb.0[0],
1133 place_y - geom_aabb.0[1],
1134 geom_aabb,
1135 (b_min, b_max),
1136 margin,
1137 ) else {
1138 result.unplaced.push(geom.id().clone());
1139 continue;
1140 };
1141
1142 placements.push(Placement::new_2d(
1143 geom.id().clone(),
1144 instance,
1145 x,
1146 y,
1147 rotation,
1148 ));
1149 total_placed_area += geom.measure();
1150 placed_count += 1;
1151
1152 let (actual_across, actual_along) = to_ca(x + geom_aabb.0[0], y + geom_aabb.0[1]);
1154 cursor_across = actual_across + extent_across + spacing;
1155 cursor_along = actual_along;
1156 row_depth = row_depth.max(extent_along);
1157
1158 report(
1159 ProgressInfo::new()
1160 .with_phase("BLF Placement")
1161 .with_items(placed_count, total_pieces)
1162 .with_utilization(total_placed_area / boundary.measure())
1163 .with_elapsed(start.elapsed_ms()),
1164 );
1165 }
1166 }
1167
1168 result.placements = placements;
1169 result.boundaries_used = 1;
1170 result.utilization = total_placed_area / boundary.measure();
1171 result.computation_time_ms = start.elapsed_ms();
1172
1173 report(
1174 ProgressInfo::new()
1175 .with_phase("Complete")
1176 .with_items(placed_count, total_pieces)
1177 .with_utilization(result.utilization)
1178 .with_elapsed(result.computation_time_ms)
1179 .finished(),
1180 );
1181
1182 Ok(result)
1183 }
1184
1185 pub fn validate(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<()> {
1194 boundary.validate()?;
1195 self.validate_geometries(geometries)?;
1196 if !self.config.strategy.is_offered(Dimension::TwoD) {
1197 return Err(self.unoffered_strategy());
1198 }
1199 Ok(())
1200 }
1201
1202 fn unoffered_strategy(&self) -> Error {
1204 let strategy = self.config.strategy;
1205 let why = if strategy == Strategy::ExtremePoint {
1206 "is 3D only".to_string()
1207 } else {
1208 "needs the `milp` feature, which this build does not have".to_string()
1209 };
1210 Error::invalid_option(
1211 "strategy",
1212 format!(
1213 "strategy \"{}\" {why}; 2D nesting offers {}",
1214 strategy.name(),
1215 offered_names(Dimension::TwoD)
1216 ),
1217 )
1218 }
1219
1220 fn validate_geometries(&self, geometries: &[Geometry2D]) -> Result<()> {
1230 use u_nesting_core::geometry::Geometry;
1231 if geometries.is_empty() {
1232 return Err(Error::empty_input("geometries"));
1233 }
1234 self.config.validate()?;
1235 for geom in geometries {
1236 geom.validate()?;
1237 }
1238 u_nesting_core::geometry::ensure_unique_ids(geometries)
1239 }
1240
1241 pub fn solve_multi_strip(
1247 &self,
1248 geometries: &[Geometry2D],
1249 boundary: &Boundary2D,
1250 ) -> Result<SolveResult<f64>> {
1251 self.solve_multi_strip_reporting(geometries, boundary, None)
1252 }
1253
1254 pub fn solve_multi_strip_with_progress(
1260 &self,
1261 geometries: &[Geometry2D],
1262 boundary: &Boundary2D,
1263 callback: ProgressCallback,
1264 ) -> Result<SolveResult<f64>> {
1265 self.solve_multi_strip_reporting(geometries, boundary, Some(&callback))
1266 }
1267
1268 fn solve_multi_strip_reporting(
1269 &self,
1270 geometries: &[Geometry2D],
1271 boundary: &Boundary2D,
1272 progress: Option<&ProgressCallback>,
1273 ) -> Result<SolveResult<f64>> {
1274 self.validate(geometries, boundary)?;
1275 self.cancelled.store(false, Ordering::Relaxed);
1276 let start = Timer::now();
1277
1278 let (b_min, b_max) = boundary.aabb();
1279 let strip_width = b_max[0] - b_min[0];
1280
1281 let sheet_area = boundary.measure();
1282 let mut final_result = SolveResult::new();
1283 let mut stop_reason = StopReason::Completed;
1286 let mut remaining_geometries: Vec<Geometry2D> = geometries.to_vec();
1287 let mut strip_index = 0;
1288 let max_strips = 100; let mut placed_total: std::collections::HashMap<String, usize> =
1293 std::collections::HashMap::new();
1294
1295 while !remaining_geometries.is_empty() && strip_index < max_strips {
1296 if self.cancelled.load(Ordering::Relaxed) {
1297 stop_reason = StopReason::Cancelled;
1298 break;
1299 }
1300
1301 let sheet_nester = match self.config.time_limit_ms {
1306 0 => None,
1307 limit => match limit.saturating_sub(start.elapsed_ms()) {
1308 0 => {
1309 stop_reason = StopReason::TimeLimit;
1310 break;
1311 }
1312 left => {
1313 let remaining_area: f64 = remaining_geometries
1314 .iter()
1315 .map(|g| g.measure() * g.quantity() as f64)
1316 .sum();
1317 let sheets_ahead = (remaining_area / sheet_area).ceil().max(1.0);
1318 Some(self.with_time_limit((left as f64 / sheets_ahead).ceil() as u64))
1319 }
1320 },
1321 };
1322
1323 let strip_result = sheet_nester
1325 .as_ref()
1326 .unwrap_or(self)
1327 .run_strategy_reporting(&remaining_geometries, boundary, progress)?;
1328 stop_reason = strip_result.stop_reason;
1329
1330 let strip_result = validate_and_filter_placements(
1332 strip_result,
1333 &remaining_geometries,
1334 boundary,
1335 self.config.margin,
1336 );
1337
1338 if strip_result.placements.is_empty() {
1339 break;
1342 }
1343
1344 let mut strip_placed: std::collections::HashMap<String, usize> =
1347 std::collections::HashMap::new();
1348
1349 for mut placement in strip_result.placements {
1351 let gid = placement.geometry_id.clone();
1352 let prior = placed_total.get(&gid).copied().unwrap_or(0);
1355 let in_strip = strip_placed.get(&gid).copied().unwrap_or(0);
1356 placement.instance = prior + in_strip;
1357 if !placement.position.is_empty() {
1359 placement.position[0] += strip_index as f64 * strip_width;
1360 }
1361 placement.boundary_index = strip_index;
1362 *strip_placed.entry(gid).or_insert(0) += 1;
1363 final_result.placements.push(placement);
1364 }
1365
1366 for (gid, cnt) in &strip_placed {
1370 *placed_total.entry(gid.clone()).or_insert(0) += cnt;
1371 }
1372 remaining_geometries = remaining_geometries
1373 .into_iter()
1374 .filter_map(|g| {
1375 let placed_here = strip_placed.get(g.id()).copied().unwrap_or(0);
1376 let new_quantity = g.quantity().saturating_sub(placed_here);
1377 if new_quantity == 0 {
1378 None
1379 } else {
1380 Some(g.with_quantity(new_quantity))
1381 }
1382 })
1383 .collect();
1384
1385 strip_index += 1;
1386
1387 if stop_reason == StopReason::Cancelled {
1390 break;
1391 }
1392 }
1393
1394 for g in &remaining_geometries {
1397 final_result.unplaced.push(g.id().clone());
1398 }
1399
1400 final_result.boundaries_used = strip_index;
1401 final_result.stop_reason = stop_reason;
1402 final_result.deduplicate_unplaced();
1403 final_result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1405
1406 let (b_min, b_max) = boundary.aabb();
1408 let strip_height = b_max[1] - b_min[1]; let mut strip_stats_map: std::collections::HashMap<usize, (f64, f64, usize)> =
1412 std::collections::HashMap::new(); for placement in &final_result.placements {
1415 let strip_idx = placement.boundary_index;
1416 if let Some(geom) = geometries.iter().find(|g| g.id() == &placement.geometry_id) {
1418 use u_nesting_core::geometry::Geometry;
1419 let piece_area = geom.measure();
1420 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1421 let (_g_min, g_max) = geom.aabb_at_rotation(rotation);
1422 let local_x = placement.position[0] - (strip_idx as f64 * strip_width);
1425 let right_edge = local_x + g_max[0];
1426
1427 let entry = strip_stats_map.entry(strip_idx).or_insert((0.0, 0.0, 0));
1428 entry.0 = entry.0.max(right_edge); entry.1 += piece_area; entry.2 += 1; }
1432 }
1433
1434 use u_nesting_core::result::StripStats;
1436 let mut strip_stats: Vec<StripStats> = strip_stats_map
1437 .into_iter()
1438 .map(|(idx, (used_length, piece_area, count))| StripStats {
1439 strip_index: idx,
1440 used_length,
1441 piece_area,
1442 piece_count: count,
1443 strip_width, strip_height, })
1446 .collect();
1447 strip_stats.sort_by_key(|s| s.strip_index);
1448
1449 let total_piece_area: f64 = strip_stats.iter().map(|s| s.piece_area).sum();
1452 let total_material_used: f64 = strip_stats
1453 .iter()
1454 .map(|s| s.strip_height * s.used_length)
1455 .sum();
1456
1457 final_result.strip_stats = strip_stats;
1458 final_result.total_piece_area = total_piece_area;
1459 final_result.total_material_used = total_material_used;
1460
1461 if total_material_used > 0.0 {
1462 final_result.utilization = total_piece_area / total_material_used;
1463 }
1464 final_result.computation_time_ms = start.elapsed_ms();
1465
1466 Ok(final_result)
1467 }
1468}
1469
1470impl Nester2D {
1471 fn solve_reporting(
1475 &self,
1476 geometries: &[Geometry2D],
1477 boundary: &Boundary2D,
1478 progress: Option<ProgressCallback>,
1479 ) -> Result<SolveResult<f64>> {
1480 self.validate(geometries, boundary)?;
1481
1482 self.cancelled.store(false, Ordering::Relaxed);
1484
1485 let initial_result =
1486 self.run_strategy_reporting(geometries, boundary, progress.as_ref())?;
1487
1488 let mut result = validate_and_filter_placements(
1490 initial_result,
1491 geometries,
1492 boundary,
1493 self.config.margin,
1494 );
1495
1496 result.record_unplaced(geometries);
1500 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1503 Ok(result)
1504 }
1505}
1506
1507impl Solver for Nester2D {
1508 type Geometry = Geometry2D;
1509 type Boundary = Boundary2D;
1510 type Scalar = f64;
1511
1512 fn solve(
1513 &self,
1514 geometries: &[Self::Geometry],
1515 boundary: &Self::Boundary,
1516 ) -> Result<SolveResult<f64>> {
1517 self.solve_reporting(geometries, boundary, None)
1518 }
1519
1520 fn solve_with_progress(
1521 &self,
1522 geometries: &[Self::Geometry],
1523 boundary: &Self::Boundary,
1524 callback: ProgressCallback,
1525 ) -> Result<SolveResult<f64>> {
1526 self.solve_reporting(geometries, boundary, Some(callback))
1527 }
1528
1529 fn cancel(&self) {
1530 self.cancelled.store(true, Ordering::Relaxed);
1531 }
1532}
1533
1534#[cfg(test)]
1535mod tests {
1536 #[cfg(feature = "milp")]
1537 mod milp_budget {
1538 use super::super::{milp_grid_step, milp_time_limit};
1539 use crate::{Boundary2D, Geometry2D};
1540
1541 #[test]
1544 fn an_unlimited_request_does_not_become_a_minute() {
1545 assert_eq!(milp_time_limit(0), u64::MAX / 2);
1546 }
1547
1548 #[test]
1551 fn a_short_request_is_kept_short() {
1552 assert_eq!(milp_time_limit(1_000), 1_000);
1553 assert_eq!(milp_time_limit(250), 250);
1554 }
1555
1556 #[test]
1560 fn the_grid_step_follows_the_smallest_piece() {
1561 let boundary = Boundary2D::rectangle(400.0, 100.0);
1562 let pieces = [
1563 Geometry2D::rectangle("wide", 80.0, 40.0),
1564 Geometry2D::rectangle("small", 20.0, 60.0),
1565 ];
1566 assert_eq!(milp_grid_step(&pieces, &boundary), 5.0);
1568 }
1569
1570 #[test]
1573 fn a_huge_sheet_keeps_the_step_from_getting_too_fine() {
1574 let boundary = Boundary2D::rectangle(100_000.0, 100.0);
1575 let pieces = [Geometry2D::rectangle("small", 4.0, 4.0)];
1576 assert_eq!(milp_grid_step(&pieces, &boundary), 100_000.0 / 256.0);
1577 }
1578
1579 #[test]
1581 fn the_step_never_goes_below_a_unit() {
1582 let boundary = Boundary2D::rectangle(10.0, 10.0);
1583 let pieces = [Geometry2D::rectangle("tiny", 0.5, 0.5)];
1584 assert_eq!(milp_grid_step(&pieces, &boundary), 1.0);
1585 }
1586 }
1587
1588 use super::*;
1589 use crate::placement_utils::polygon_centroid;
1590
1591 #[test]
1595 fn a_geometry_id_given_twice_is_refused_by_every_entry_point() {
1596 let geometries = [
1597 Geometry2D::rectangle("P", 10.0, 10.0),
1598 Geometry2D::rectangle("Q", 10.0, 10.0),
1599 Geometry2D::rectangle("P", 30.0, 5.0),
1600 ];
1601 let boundary = Boundary2D::rectangle(100.0, 100.0);
1602 let nester = Nester2D::default_config();
1603 let errors = [
1604 nester.solve(&geometries, &boundary).expect_err("solve"),
1605 nester
1606 .solve_multi_strip(&geometries, &boundary)
1607 .expect_err("solve_multi_strip"),
1608 nester
1609 .solve_with_progress(&geometries, &boundary, Box::new(|_| {}))
1610 .expect_err("solve_with_progress"),
1611 ];
1612 for err in errors {
1613 let msg = err.to_string();
1614 assert!(msg.contains("'P'"), "{msg}");
1615 assert!(msg.contains("positions 0 and 2"), "{msg}");
1616 }
1617 }
1618
1619 #[test]
1622 fn an_empty_list_of_geometries_is_refused_by_every_entry_point() {
1623 let boundary = Boundary2D::rectangle(100.0, 100.0);
1624 let nester = Nester2D::default_config();
1625 let errors = [
1626 nester.solve(&[], &boundary).expect_err("solve"),
1627 nester
1628 .solve_multi_strip(&[], &boundary)
1629 .expect_err("solve_multi_strip"),
1630 nester
1631 .solve_with_progress(&[], &boundary, Box::new(|_| {}))
1632 .expect_err("solve_with_progress"),
1633 ];
1634 for err in errors {
1635 assert_eq!(
1636 err,
1637 Error::EmptyInput {
1638 parameter: "geometries".into()
1639 }
1640 );
1641 }
1642 }
1643
1644 #[test]
1645 fn the_seed_reaches_the_searches_that_build_their_own_run() {
1646 let seeded = Nester2D::new(Config::default().with_seed(7));
1650 assert_eq!(seeded.gdrr_config(1000).seed, Some(7));
1651 assert_eq!(seeded.alns_config(1000).seed, Some(7));
1652 let unseeded = Nester2D::default_config();
1653 assert_eq!(unseeded.gdrr_config(1000).seed, None);
1654 assert_eq!(unseeded.alns_config(1000).seed, None);
1655 }
1656
1657 #[test]
1662 fn a_set_budget_is_run_and_an_unset_one_is_the_strategy_s() {
1663 assert_eq!((SEARCH_POPULATION, SEARCH_GENERATIONS), (30, 50));
1664 let unset = Nester2D::default_config();
1665 assert_eq!(unset.ga_config().population_size, SEARCH_POPULATION);
1666 assert_eq!(unset.ga_config().max_generations, SEARCH_GENERATIONS);
1667 assert_eq!(unset.brkga_config().population_size, SEARCH_POPULATION);
1668 assert_eq!(unset.brkga_config().max_generations, SEARCH_GENERATIONS);
1669
1670 let set = Nester2D::new(
1671 Config::default()
1672 .with_population_size(100)
1673 .with_max_generations(200),
1674 );
1675 assert_eq!(set.ga_config().population_size, 100);
1676 assert_eq!(set.ga_config().max_generations, 200);
1677 assert_eq!(set.brkga_config().population_size, 100);
1678 assert_eq!(set.brkga_config().max_generations, 200);
1679
1680 let small = Nester2D::new(Config::default().with_population_size(3));
1681 assert_eq!(small.brkga_config().population_size, 3, "was raised to 4");
1682 assert!(small.brkga_config().validate().is_ok());
1683 }
1684
1685 #[test]
1688 fn the_generation_budget_reaches_the_running_search() {
1689 use std::sync::Mutex;
1690 let reports = |config: Config| {
1691 let seen = Arc::new(Mutex::new(Vec::new()));
1692 let sink = seen.clone();
1693 Nester2D::new(
1694 config
1695 .with_strategy(Strategy::GeneticAlgorithm)
1696 .with_seed(7),
1697 )
1698 .solve_with_progress(
1699 &[Geometry2D::rectangle("R", 10.0, 10.0).with_quantity(4)],
1700 &Boundary2D::rectangle(50.0, 50.0),
1701 Box::new(move |info| {
1703 if info.phase == "Genetic Algorithm" {
1704 sink.lock()
1705 .expect("no report panicked")
1706 .push((info.iteration, info.total_iterations))
1707 }
1708 }),
1709 )
1710 .expect("solve");
1711 let seen = seen.lock().expect("no report panicked").clone();
1712 seen
1713 };
1714 let set = reports(Config::default().with_max_generations(80));
1715 assert!(!set.is_empty(), "the search reports its progress");
1716 assert!(
1717 set.iter().all(|&(at, total)| total == 80 && at <= 80),
1718 "{set:?}"
1719 );
1720 let unset = reports(Config::default());
1721 assert!(!unset.is_empty());
1722 assert!(unset.iter().all(|&(_, total)| total == 50), "{unset:?}");
1723 }
1724
1725 #[test]
1733 fn a_progress_callback_does_not_change_the_solve() {
1734 let geometries = [
1735 Geometry2D::rectangle("A", 30.0, 12.0)
1736 .with_quantity(3)
1737 .with_rotations_deg(vec![0.0, 90.0]),
1738 Geometry2D::rectangle("B", 18.0, 18.0).with_quantity(2),
1739 Geometry2D::new("L")
1740 .with_polygon(vec![
1741 (0.0, 0.0),
1742 (20.0, 0.0),
1743 (20.0, 8.0),
1744 (8.0, 8.0),
1745 (8.0, 20.0),
1746 (0.0, 20.0),
1747 ])
1748 .with_quantity(2)
1749 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0]),
1750 ];
1751 let boundary = Boundary2D::rectangle(60.0, 50.0);
1752 type Laid = (String, usize, Vec<f64>, Vec<f64>, bool);
1754 let layout = |r: &SolveResult<f64>| -> Vec<Laid> {
1755 r.placements
1756 .iter()
1757 .map(|p| {
1758 (
1759 p.geometry_id.clone(),
1760 p.instance,
1761 p.position.clone(),
1762 p.rotation.clone(),
1763 p.mirrored,
1764 )
1765 })
1766 .collect()
1767 };
1768 for strategy in [
1769 Strategy::BottomLeftFill,
1770 Strategy::NfpGuided,
1771 Strategy::GeneticAlgorithm,
1772 Strategy::Brkga,
1773 ] {
1774 for time_limit_ms in [0, 60_000] {
1775 let config = Config::default()
1776 .with_strategy(strategy)
1777 .with_seed(11)
1778 .with_population_size(6)
1779 .with_max_generations(4)
1780 .with_time_limit(time_limit_ms);
1781 let plain = Nester2D::new(config.clone())
1782 .solve(&geometries, &boundary)
1783 .expect("solve");
1784 let reported = Nester2D::new(config)
1785 .solve_with_progress(&geometries, &boundary, Box::new(|_| {}))
1786 .expect("solve with progress");
1787 let case = format!("{strategy:?}, time_limit_ms {time_limit_ms}");
1788 assert!(!plain.placements.is_empty(), "{case}");
1789 assert_eq!(layout(&plain), layout(&reported), "{case}");
1790 assert_eq!(plain.utilization, reported.utilization, "{case}");
1791 assert_eq!(plain.generations, reported.generations, "{case}");
1792 assert_eq!(plain.unplaced, reported.unplaced, "{case}");
1793 }
1794 }
1795 }
1796
1797 #[test]
1798 fn test_simple_nesting() {
1799 let geometries = vec![
1800 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1801 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1802 ];
1803
1804 let boundary = Boundary2D::rectangle(100.0, 50.0);
1805 let nester = Nester2D::default_config();
1806
1807 let result = nester.solve(&geometries, &boundary).unwrap();
1808
1809 assert!(result.utilization > 0.0);
1810 assert!(result.placements.len() <= 5); }
1812
1813 #[test]
1814 fn test_placement_within_bounds() {
1815 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1816
1817 let boundary = Boundary2D::rectangle(50.0, 50.0);
1818 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1819 let nester = Nester2D::new(config);
1820
1821 let result = nester.solve(&geometries, &boundary).unwrap();
1822
1823 assert_eq!(result.placements.len(), 4);
1825 assert!(result.unplaced.is_empty());
1826
1827 for p in &result.placements {
1829 assert!(p.position[0] >= 5.0);
1830 assert!(p.position[1] >= 5.0);
1831 }
1832 }
1833
1834 #[test]
1835 fn test_nfp_guided_basic() {
1836 let geometries = vec![
1837 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1838 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1839 ];
1840
1841 let boundary = Boundary2D::rectangle(100.0, 50.0);
1842 let config = Config::default().with_strategy(Strategy::NfpGuided);
1843 let nester = Nester2D::new(config);
1844
1845 let result = nester.solve(&geometries, &boundary).unwrap();
1846
1847 assert!(result.utilization > 0.0);
1848 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1850 }
1851
1852 #[test]
1853 fn test_nfp_guided_with_spacing() {
1854 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1855
1856 let boundary = Boundary2D::rectangle(50.0, 50.0);
1857 let config = Config::default()
1858 .with_strategy(Strategy::NfpGuided)
1859 .with_margin(2.0)
1860 .with_spacing(3.0);
1861 let nester = Nester2D::new(config);
1862
1863 let result = nester.solve(&geometries, &boundary).unwrap();
1864
1865 assert_eq!(result.placements.len(), 4);
1867 assert!(result.unplaced.is_empty());
1868
1869 assert!(result.utilization > 0.0);
1871 }
1872
1873 #[test]
1874 fn test_nfp_guided_no_overlap() {
1875 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1876
1877 let boundary = Boundary2D::rectangle(100.0, 100.0);
1878 let config = Config::default().with_strategy(Strategy::NfpGuided);
1879 let nester = Nester2D::new(config);
1880
1881 let result = nester.solve(&geometries, &boundary).unwrap();
1882
1883 assert_eq!(result.placements.len(), 3);
1884
1885 for i in 0..result.placements.len() {
1887 for j in (i + 1)..result.placements.len() {
1888 let p1 = &result.placements[i];
1889 let p2 = &result.placements[j];
1890
1891 let r1_min_x = p1.position[0];
1893 let r1_max_x = p1.position[0] + 20.0;
1894 let r1_min_y = p1.position[1];
1895 let r1_max_y = p1.position[1] + 20.0;
1896
1897 let r2_min_x = p2.position[0];
1898 let r2_max_x = p2.position[0] + 20.0;
1899 let r2_min_y = p2.position[1];
1900 let r2_max_y = p2.position[1] + 20.0;
1901
1902 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1904 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1905
1906 assert!(
1907 !(overlaps_x && overlaps_y),
1908 "Placements {} and {} overlap",
1909 i,
1910 j
1911 );
1912 }
1913 }
1914 }
1915
1916 fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
1921 for i in 0..a.len() {
1922 let a1 = a[i];
1923 let a2 = a[(i + 1) % a.len()];
1924 for j in 0..b.len() {
1925 let b1 = b[j];
1926 let b2 = b[(j + 1) % b.len()];
1927 if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
1928 return true;
1929 }
1930 }
1931 }
1932 false
1933 }
1934
1935 #[test]
1936 fn test_mirror_candidates_reflects_allow_flip() {
1937 let plain = Geometry2D::rectangle("R", 10.0, 10.0);
1938 assert_eq!(mirror_candidates(&plain), &[false]);
1939
1940 let flippable = Geometry2D::rectangle("R", 10.0, 10.0).with_flip(true);
1941 assert_eq!(mirror_candidates(&flippable), &[false, true]);
1942 }
1943
1944 #[test]
1964 fn test_mirror_aware_placement_avoids_overlap() {
1965 let spacing = 1.0;
1973 let geom = Geometry2D::l_shape("L", 30.0, 20.0, 20.0, 10.0);
1974 let boundary_polygon = vec![(0.0, 0.0), (65.0, 0.0), (65.0, 45.0), (0.0, 45.0)];
1975 let cache = NfpCache::new();
1976
1977 let ifp1 =
1979 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, false).unwrap();
1980 let (x1, y1) = find_bottom_left_placement(&ifp1, &[], 1.0, PackingAxis::X)
1981 .expect("first piece must fit");
1982 let placed1 = PlacedGeometry::new(geom.clone(), (x1, y1), 0.0).with_mirrored(false);
1983
1984 let ifp2 =
1986 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, true).unwrap();
1987 let nfp_at_origin = cache
1988 .get_or_compute_mirrored(("L", "L", 0.0, false, true), || {
1989 compute_nfp_mirrored(&placed1.geometry, &geom, 0.0, false, true)
1990 })
1991 .unwrap();
1992 let translated_nfp = translate_nfp(&nfp_at_origin, placed1.position);
1993 let expanded_nfp = offset_nfp(&translated_nfp, spacing);
1994 let (x2, y2) = find_bottom_left_placement(&ifp2, &[&expanded_nfp], 1.0, PackingAxis::X)
1995 .expect("mirrored second piece must fit avoiding the first");
1996 let placed2 = PlacedGeometry::new(geom.clone(), (x2, y2), 0.0).with_mirrored(true);
1997
1998 assert_ne!(
1999 (x1, y1),
2000 (x2, y2),
2001 "mirrored placement must actually avoid the first piece's position"
2002 );
2003 assert!(
2004 !polygons_overlap(
2005 &placed1.translated_exterior(),
2006 &placed2.translated_exterior()
2007 ),
2008 "unmirrored piece at {:?} and mirrored piece at {:?} must not overlap",
2009 (x1, y1),
2010 (x2, y2)
2011 );
2012 }
2013
2014 #[test]
2015 fn test_nfp_guided_utilization() {
2016 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
2018
2019 let boundary = Boundary2D::rectangle(100.0, 50.0);
2020 let config = Config::default().with_strategy(Strategy::NfpGuided);
2021 let nester = Nester2D::new(config);
2022
2023 let result = nester.solve(&geometries, &boundary).unwrap();
2024
2025 assert_eq!(result.placements.len(), 4);
2027
2028 assert!(result.utilization > 0.45);
2030 }
2031
2032 #[test]
2033 fn test_polygon_centroid() {
2034 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
2036 let (cx, cy) = polygon_centroid(&square);
2037 assert!((cx - 5.0).abs() < 0.01);
2038 assert!((cy - 5.0).abs() < 0.01);
2039
2040 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
2041 let (cx, cy) = polygon_centroid(&triangle);
2042 assert!((cx - 3.0).abs() < 0.01);
2043 assert!((cy - 2.0).abs() < 0.01);
2044 }
2045
2046 #[test]
2047 fn test_ga_strategy_basic() {
2048 let geometries = vec![
2049 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2050 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2051 ];
2052
2053 let boundary = Boundary2D::rectangle(100.0, 50.0);
2054 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
2055 let nester = Nester2D::new(config);
2056
2057 let result = nester.solve(&geometries, &boundary).unwrap();
2058
2059 assert!(result.utilization > 0.0);
2060 assert!(!result.placements.is_empty());
2061 assert!(result.generations.is_some());
2063 assert!(result.best_fitness.is_some());
2064 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
2065 }
2066
2067 #[test]
2068 fn test_ga_strategy_all_placed() {
2069 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2071
2072 let boundary = Boundary2D::rectangle(100.0, 100.0);
2073 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
2074 let nester = Nester2D::new(config);
2075
2076 let result = nester.solve(&geometries, &boundary).unwrap();
2077
2078 assert_eq!(result.placements.len(), 4);
2080 assert!(result.unplaced.is_empty());
2081 }
2082
2083 #[test]
2084 fn test_brkga_strategy_basic() {
2085 let geometries = vec![
2086 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2087 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2088 ];
2089
2090 let boundary = Boundary2D::rectangle(100.0, 50.0);
2091 let config = Config::default().with_strategy(Strategy::Brkga);
2092 let nester = Nester2D::new(config);
2093
2094 let result = nester.solve(&geometries, &boundary).unwrap();
2095
2096 assert!(result.utilization > 0.0);
2097 assert!(!result.placements.is_empty());
2098 assert!(result.generations.is_some());
2100 assert!(result.best_fitness.is_some());
2101 assert!(result.strategy == Some("BRKGA".to_string()));
2102 }
2103
2104 #[test]
2105 fn test_brkga_strategy_all_placed() {
2106 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2108
2109 let boundary = Boundary2D::rectangle(100.0, 100.0);
2110 let config = Config::default()
2112 .with_strategy(Strategy::Brkga)
2113 .with_time_limit(30000); let nester = Nester2D::new(config);
2115
2116 let result = nester.solve(&geometries, &boundary).unwrap();
2117
2118 assert!(
2121 result.placements.len() >= 3,
2122 "Expected at least 3 placements, got {}",
2123 result.placements.len()
2124 );
2125 }
2126
2127 #[test]
2128 fn test_gdrr_strategy_basic() {
2129 let geometries = vec![
2130 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2131 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2132 ];
2133
2134 let boundary = Boundary2D::rectangle(100.0, 50.0);
2135 let config = Config::default().with_strategy(Strategy::Gdrr);
2136 let nester = Nester2D::new(config);
2137
2138 let result = nester.solve(&geometries, &boundary).unwrap();
2139
2140 assert!(result.utilization > 0.0);
2141 assert!(!result.placements.is_empty());
2142 assert!(result.iterations.is_some());
2144 assert!(result.best_fitness.is_some());
2145 assert!(result.strategy == Some("GDRR".to_string()));
2146 }
2147
2148 #[test]
2149 fn test_gdrr_strategy_all_placed() {
2150 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2152
2153 let boundary = Boundary2D::rectangle(100.0, 100.0);
2154 let config = Config::default().with_strategy(Strategy::Gdrr);
2155 let nester = Nester2D::new(config);
2156
2157 let result = nester.solve(&geometries, &boundary).unwrap();
2158
2159 assert_eq!(result.placements.len(), 4);
2161 assert!(result.unplaced.is_empty());
2162 }
2163
2164 #[test]
2165 fn test_alns_strategy_basic() {
2166 let geometries = vec![
2167 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2168 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2169 ];
2170
2171 let boundary = Boundary2D::rectangle(100.0, 50.0);
2172 let config = Config::default().with_strategy(Strategy::Alns);
2173 let nester = Nester2D::new(config);
2174
2175 let result = nester.solve(&geometries, &boundary).unwrap();
2176
2177 assert!(result.utilization > 0.0);
2178 assert!(!result.placements.is_empty());
2179 assert!(result.iterations.is_some());
2181 assert!(result.best_fitness.is_some());
2182 assert!(result.strategy == Some("ALNS".to_string()));
2183 }
2184
2185 #[test]
2186 fn test_alns_strategy_all_placed() {
2187 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2189
2190 let boundary = Boundary2D::rectangle(100.0, 100.0);
2191 let config = Config::default().with_strategy(Strategy::Alns);
2192 let nester = Nester2D::new(config);
2193
2194 let result = nester.solve(&geometries, &boundary).unwrap();
2195
2196 assert_eq!(result.placements.len(), 4);
2198 assert!(result.unplaced.is_empty());
2199 }
2200
2201 #[test]
2202 fn test_blf_rotation_optimization() {
2203 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
2206 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
2208
2209 let boundary = Boundary2D::rectangle(35.0, 95.0);
2212 let nester = Nester2D::default_config();
2213
2214 let result = nester.solve(&geometries, &boundary).unwrap();
2215
2216 assert_eq!(
2218 result.placements.len(),
2219 3,
2220 "All pieces should be placed with rotation optimization"
2221 );
2222 assert!(result.unplaced.is_empty());
2223 }
2224
2225 #[test]
2226 fn test_blf_selects_best_rotation() {
2227 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
2229 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
2231
2232 let boundary = Boundary2D::rectangle(45.0, 50.0);
2236 let nester = Nester2D::default_config();
2237
2238 let result = nester.solve(&geometries, &boundary).unwrap();
2239
2240 assert_eq!(result.placements.len(), 2);
2241 assert!(result.unplaced.is_empty());
2242 }
2243
2244 #[test]
2245 fn test_progress_callback_blf() {
2246 use std::sync::atomic::{AtomicUsize, Ordering};
2247 use std::sync::Arc;
2248
2249 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2250 let boundary = Boundary2D::rectangle(50.0, 50.0);
2251 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2252 let nester = Nester2D::new(config);
2253
2254 let callback_count = Arc::new(AtomicUsize::new(0));
2255 let callback_count_clone = callback_count.clone();
2256 let last_items_placed = Arc::new(AtomicUsize::new(0));
2257 let last_items_placed_clone = last_items_placed.clone();
2258
2259 let callback: ProgressCallback = Box::new(move |info| {
2260 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2261 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
2262 });
2263
2264 let result = nester
2265 .solve_with_progress(&geometries, &boundary, callback)
2266 .unwrap();
2267
2268 let count = callback_count.load(Ordering::Relaxed);
2270 assert!(
2271 count >= 5,
2272 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
2273 count
2274 );
2275
2276 let final_placed = last_items_placed.load(Ordering::Relaxed);
2278 assert_eq!(final_placed, 4, "Should report 4 items placed");
2279
2280 assert_eq!(result.placements.len(), 4);
2282 }
2283
2284 #[test]
2285 fn test_progress_callback_nfp() {
2286 use std::sync::atomic::{AtomicUsize, Ordering};
2287 use std::sync::Arc;
2288
2289 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
2290 let boundary = Boundary2D::rectangle(50.0, 50.0);
2291 let config = Config::default().with_strategy(Strategy::NfpGuided);
2292 let nester = Nester2D::new(config);
2293
2294 let callback_count = Arc::new(AtomicUsize::new(0));
2295 let callback_count_clone = callback_count.clone();
2296
2297 let callback: ProgressCallback = Box::new(move |info| {
2298 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2299 assert!(info.items_placed <= info.total_items);
2300 });
2301
2302 let result = nester
2303 .solve_with_progress(&geometries, &boundary, callback)
2304 .unwrap();
2305
2306 let count = callback_count.load(Ordering::Relaxed);
2308 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
2309
2310 assert_eq!(result.placements.len(), 2);
2312 }
2313
2314 #[test]
2315 fn test_time_limit_honored() {
2316 let geometries: Vec<Geometry2D> = (0..100)
2318 .map(|i| Geometry2D::rectangle(format!("R{}", i), 5.0, 5.0))
2319 .collect();
2320 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
2321
2322 let config = Config::default()
2324 .with_strategy(Strategy::BottomLeftFill)
2325 .with_time_limit(1);
2326 let nester = Nester2D::new(config);
2327
2328 let result = nester.solve(&geometries, &boundary).unwrap();
2329
2330 assert!(
2333 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
2335 result.computation_time_ms
2336 );
2337 }
2338
2339 #[test]
2340 fn test_time_limit_zero_unlimited() {
2341 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2343 let boundary = Boundary2D::rectangle(50.0, 50.0);
2344
2345 let config = Config::default()
2346 .with_strategy(Strategy::BottomLeftFill)
2347 .with_time_limit(0); let nester = Nester2D::new(config);
2349
2350 let result = nester.solve(&geometries, &boundary).unwrap();
2351
2352 assert_eq!(result.placements.len(), 4);
2354 }
2355
2356 #[test]
2357 fn test_blf_bounds_clamping() {
2358 let gear_like = Geometry2D::new("gear")
2362 .with_polygon(vec![
2363 (50.0, 5.0), (65.0, 15.0),
2365 (77.0, 18.0),
2366 (80.0, 32.0),
2367 (95.0, 50.0), (80.0, 68.0),
2369 (77.0, 82.0),
2370 (65.0, 85.0),
2371 (50.0, 95.0), (35.0, 85.0),
2373 (23.0, 82.0),
2374 (20.0, 68.0),
2375 (5.0, 50.0), (20.0, 32.0),
2377 (23.0, 18.0),
2378 (35.0, 15.0),
2379 ])
2380 .with_quantity(1);
2381
2382 let boundary = Boundary2D::rectangle(100.0, 100.0);
2384
2385 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2386 let nester = Nester2D::new(config);
2387
2388 let result = nester
2389 .solve(std::slice::from_ref(&gear_like), &boundary)
2390 .unwrap();
2391
2392 assert_eq!(result.placements.len(), 1);
2393 let placement = &result.placements[0];
2394
2395 let origin_x = placement.position[0];
2397 let origin_y = placement.position[1];
2398
2399 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2401
2402 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2404
2405 let actual_min_x = origin_x + g_min[0];
2407 let actual_max_x = origin_x + g_max[0];
2408 let actual_min_y = origin_y + g_min[1];
2409 let actual_max_y = origin_y + g_max[1];
2410
2411 assert!(
2413 actual_min_x >= 0.0,
2414 "Left edge {} should be >= 0",
2415 actual_min_x
2416 );
2417 assert!(
2418 actual_max_x <= 100.0,
2419 "Right edge {} should be <= 100",
2420 actual_max_x
2421 );
2422 assert!(
2423 actual_min_y >= 0.0,
2424 "Bottom edge {} should be >= 0",
2425 actual_min_y
2426 );
2427 assert!(
2428 actual_max_y <= 100.0,
2429 "Top edge {} should be <= 100",
2430 actual_max_y
2431 );
2432 }
2433
2434 #[test]
2435 fn test_blf_bounds_clamping_many_pieces() {
2436 let gear_like = Geometry2D::new("gear")
2439 .with_polygon(vec![
2440 (50.0, 5.0),
2441 (65.0, 15.0),
2442 (77.0, 18.0),
2443 (80.0, 32.0),
2444 (95.0, 50.0),
2445 (80.0, 68.0),
2446 (77.0, 82.0),
2447 (65.0, 85.0),
2448 (50.0, 95.0),
2449 (35.0, 85.0),
2450 (23.0, 82.0),
2451 (20.0, 68.0),
2452 (5.0, 50.0),
2453 (20.0, 32.0),
2454 (23.0, 18.0),
2455 (35.0, 15.0),
2456 ])
2457 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
2461
2462 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2463 let nester = Nester2D::new(config);
2464
2465 let result = nester
2466 .solve(std::slice::from_ref(&gear_like), &boundary)
2467 .unwrap();
2468
2469 for (i, placement) in result.placements.iter().enumerate() {
2471 let origin_x = placement.position[0];
2472 let origin_y = placement.position[1];
2473 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2474
2475 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2476
2477 let actual_min_x = origin_x + g_min[0];
2478 let actual_max_x = origin_x + g_max[0];
2479 let actual_min_y = origin_y + g_min[1];
2480 let actual_max_y = origin_y + g_max[1];
2481
2482 assert!(
2483 actual_min_x >= -0.01,
2484 "Piece {}: Left edge {} should be >= 0",
2485 i,
2486 actual_min_x
2487 );
2488 assert!(
2489 actual_max_x <= 500.01,
2490 "Piece {}: Right edge {} should be <= 500",
2491 i,
2492 actual_max_x
2493 );
2494 assert!(
2495 actual_min_y >= -0.01,
2496 "Piece {}: Bottom edge {} should be >= 0",
2497 i,
2498 actual_min_y
2499 );
2500 assert!(
2501 actual_max_y <= 500.01,
2502 "Piece {}: Top edge {} should be <= 500",
2503 i,
2504 actual_max_y
2505 );
2506 }
2507 }
2508
2509 #[test]
2510 fn test_blf_bounds_trace() {
2511 let gear = Geometry2D::new("gear").with_polygon(vec![
2513 (50.0, 5.0),
2514 (65.0, 15.0),
2515 (77.0, 18.0),
2516 (80.0, 32.0),
2517 (95.0, 50.0),
2518 (80.0, 68.0),
2519 (77.0, 82.0),
2520 (65.0, 85.0),
2521 (50.0, 95.0),
2522 (35.0, 85.0),
2523 (23.0, 82.0),
2524 (20.0, 68.0),
2525 (5.0, 50.0),
2526 (20.0, 32.0),
2527 (23.0, 18.0),
2528 (35.0, 15.0),
2529 ]);
2530
2531 let (g_min, g_max) = gear.aabb();
2533 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
2534 assert!(
2535 (g_min[0] - 5.0).abs() < 0.01,
2536 "g_min[0] should be 5, got {}",
2537 g_min[0]
2538 );
2539 assert!(
2540 (g_max[0] - 95.0).abs() < 0.01,
2541 "g_max[0] should be 95, got {}",
2542 g_max[0]
2543 );
2544
2545 let b_max_x = 500.0;
2547 let margin = 0.0;
2548 let max_valid_x = b_max_x - margin - g_max[0];
2549 println!(
2550 "max_valid_x = {} - {} - {} = {}",
2551 b_max_x, margin, g_max[0], max_valid_x
2552 );
2553 assert!(
2554 (max_valid_x - 405.0).abs() < 0.01,
2555 "max_valid_x should be 405, got {}",
2556 max_valid_x
2557 );
2558
2559 let boundary = Boundary2D::rectangle(500.0, 500.0);
2561 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2562 let nester = Nester2D::new(config);
2563
2564 let result = nester
2565 .solve(&[gear.clone().with_quantity(1)], &boundary)
2566 .unwrap();
2567
2568 assert_eq!(result.placements.len(), 1);
2569 let p = &result.placements[0];
2570 let origin_x = p.position[0];
2571 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2572
2573 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2574 let actual_max_x = origin_x + g_max_r[0];
2575
2576 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2577 println!(
2578 "At rotation {}: g_min={:?}, g_max={:?}",
2579 rotation, g_min_r, g_max_r
2580 );
2581 println!(
2582 "Actual max x: {} + {} = {}",
2583 origin_x, g_max_r[0], actual_max_x
2584 );
2585
2586 assert!(
2587 actual_max_x <= 500.01,
2588 "Geometry exceeds boundary: max_x={} > 500",
2589 actual_max_x
2590 );
2591 }
2592
2593 #[test]
2594 fn test_blf_bounds_many_pieces_direct() {
2595 let gear = Geometry2D::new("gear")
2597 .with_polygon(vec![
2598 (50.0, 5.0),
2599 (65.0, 15.0),
2600 (77.0, 18.0),
2601 (80.0, 32.0),
2602 (95.0, 50.0),
2603 (80.0, 68.0),
2604 (77.0, 82.0),
2605 (65.0, 85.0),
2606 (50.0, 95.0),
2607 (35.0, 85.0),
2608 (23.0, 82.0),
2609 (20.0, 68.0),
2610 (5.0, 50.0),
2611 (20.0, 32.0),
2612 (23.0, 18.0),
2613 (35.0, 15.0),
2614 ])
2615 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2618 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2619 let nester = Nester2D::new(config);
2620
2621 let result = nester
2622 .solve(std::slice::from_ref(&gear), &boundary)
2623 .unwrap();
2624
2625 println!("Placed {} pieces", result.placements.len());
2626
2627 for (i, p) in result.placements.iter().enumerate() {
2629 let origin_x = p.position[0];
2630 let origin_y = p.position[1];
2631 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2632
2633 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2634
2635 let actual_min_x = origin_x + g_min_r[0];
2636 let actual_max_x = origin_x + g_max_r[0];
2637 let actual_min_y = origin_y + g_min_r[1];
2638 let actual_max_y = origin_y + g_max_r[1];
2639
2640 println!(
2641 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2642 i,
2643 origin_x,
2644 origin_y,
2645 rotation,
2646 actual_min_x,
2647 actual_max_x,
2648 actual_min_y,
2649 actual_max_y
2650 );
2651
2652 assert!(
2653 actual_max_x <= 500.01,
2654 "Piece {}: Right edge {} > 500",
2655 i,
2656 actual_max_x
2657 );
2658 assert!(
2659 actual_max_y <= 500.01,
2660 "Piece {}: Top edge {} > 500",
2661 i,
2662 actual_max_y
2663 );
2664 }
2665 }
2666
2667 #[test]
2668 fn test_blf_bounds_multi_strip() {
2669 let gear = Geometry2D::new("gear")
2671 .with_polygon(vec![
2672 (50.0, 5.0),
2673 (65.0, 15.0),
2674 (77.0, 18.0),
2675 (80.0, 32.0),
2676 (95.0, 50.0),
2677 (80.0, 68.0),
2678 (77.0, 82.0),
2679 (65.0, 85.0),
2680 (50.0, 95.0),
2681 (35.0, 85.0),
2682 (23.0, 82.0),
2683 (20.0, 68.0),
2684 (5.0, 50.0),
2685 (20.0, 32.0),
2686 (23.0, 18.0),
2687 (35.0, 15.0),
2688 ])
2689 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2692 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2693 let nester = Nester2D::new(config);
2694
2695 let result = nester
2697 .solve_multi_strip(std::slice::from_ref(&gear), &boundary)
2698 .unwrap();
2699
2700 println!(
2701 "Placed {} pieces across {} strips",
2702 result.placements.len(),
2703 result.boundaries_used
2704 );
2705
2706 let strip_width = 500.0;
2708 for (i, p) in result.placements.iter().enumerate() {
2709 let origin_x = p.position[0];
2710 let origin_y = p.position[1];
2711 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2712 let strip_idx = p.boundary_index;
2713
2714 let local_x = origin_x - (strip_idx as f64 * strip_width);
2716
2717 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2718
2719 let local_max_x = local_x + g_max_r[0];
2720 let local_max_y = origin_y + g_max_r[1];
2721
2722 println!(
2723 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2724 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2725 );
2726
2727 assert!(
2728 local_max_x <= 500.01,
2729 "Piece {}: In strip {}, local right edge {:.1} > 500",
2730 i,
2731 strip_idx,
2732 local_max_x
2733 );
2734 assert!(
2735 local_max_y <= 500.01,
2736 "Piece {}: Top edge {:.1} > 500",
2737 i,
2738 local_max_y
2739 );
2740 }
2741 }
2742
2743 #[test]
2744 fn test_blf_bounds_mixed_shapes() {
2745 let shapes = vec![
2747 Geometry2D::new("shape0")
2749 .with_polygon(vec![
2750 (0.0, 0.0),
2751 (180.0, 0.0),
2752 (195.0, 15.0),
2753 (200.0, 50.0),
2754 (200.0, 150.0),
2755 (195.0, 185.0),
2756 (180.0, 200.0),
2757 (20.0, 200.0),
2758 (5.0, 185.0),
2759 (0.0, 150.0),
2760 (0.0, 50.0),
2761 (5.0, 15.0),
2762 ])
2763 .with_quantity(2),
2764 Geometry2D::new("shape1")
2766 .with_polygon(vec![
2767 (60.0, 0.0),
2768 (85.0, 7.0),
2769 (104.0, 25.0),
2770 (118.0, 50.0),
2771 (120.0, 60.0),
2772 (118.0, 70.0),
2773 (104.0, 95.0),
2774 (85.0, 113.0),
2775 (60.0, 120.0),
2776 (35.0, 113.0),
2777 (16.0, 95.0),
2778 (2.0, 70.0),
2779 (0.0, 60.0),
2780 (2.0, 50.0),
2781 (16.0, 25.0),
2782 (35.0, 7.0),
2783 ])
2784 .with_quantity(4),
2785 Geometry2D::new("shape2")
2787 .with_polygon(vec![
2788 (0.0, 0.0),
2789 (80.0, 0.0),
2790 (80.0, 20.0),
2791 (20.0, 20.0),
2792 (20.0, 80.0),
2793 (0.0, 80.0),
2794 ])
2795 .with_quantity(6),
2796 Geometry2D::new("shape3")
2798 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2799 .with_quantity(6),
2800 Geometry2D::new("shape4")
2802 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2803 .with_quantity(4),
2804 Geometry2D::new("shape5")
2806 .with_polygon(vec![
2807 (15.0, 0.0),
2808 (45.0, 0.0),
2809 (60.0, 26.0),
2810 (45.0, 52.0),
2811 (15.0, 52.0),
2812 (0.0, 26.0),
2813 ])
2814 .with_quantity(8),
2815 Geometry2D::new("shape6")
2817 .with_polygon(vec![
2818 (0.0, 0.0),
2819 (90.0, 0.0),
2820 (90.0, 12.0),
2821 (55.0, 12.0),
2822 (55.0, 60.0),
2823 (35.0, 60.0),
2824 (35.0, 12.0),
2825 (0.0, 12.0),
2826 ])
2827 .with_quantity(4),
2828 Geometry2D::new("shape7")
2830 .with_polygon(vec![
2831 (0.0, 10.0),
2832 (10.0, 0.0),
2833 (70.0, 0.0),
2834 (80.0, 10.0),
2835 (80.0, 70.0),
2836 (70.0, 80.0),
2837 (10.0, 80.0),
2838 (0.0, 70.0),
2839 ])
2840 .with_quantity(3),
2841 Geometry2D::new("shape8_gear")
2843 .with_polygon(vec![
2844 (50.0, 5.0),
2845 (65.0, 15.0),
2846 (77.0, 18.0),
2847 (80.0, 32.0),
2848 (95.0, 50.0),
2849 (80.0, 68.0),
2850 (77.0, 82.0),
2851 (65.0, 85.0),
2852 (50.0, 95.0),
2853 (35.0, 85.0),
2854 (23.0, 82.0),
2855 (20.0, 68.0),
2856 (5.0, 50.0),
2857 (20.0, 32.0),
2858 (23.0, 18.0),
2859 (35.0, 15.0),
2860 ])
2861 .with_quantity(13),
2862 ];
2863
2864 let boundary = Boundary2D::rectangle(500.0, 500.0);
2866 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2867 let nester = Nester2D::new(config);
2868
2869 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2870
2871 println!(
2872 "Placed {} pieces across {} strips",
2873 result.placements.len(),
2874 result.boundaries_used
2875 );
2876
2877 let strip_width = 500.0;
2879 let gear_aabb = shapes[8].aabb();
2880 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2881
2882 let mut violations = Vec::new();
2883 for p in &result.placements {
2884 if p.geometry_id.as_str().starts_with("shape8") {
2885 let origin_x = p.position[0];
2886 let _origin_y = p.position[1];
2887 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2888 let strip_idx = p.boundary_index;
2889 let local_x = origin_x - (strip_idx as f64 * strip_width);
2890
2891 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2892 let local_max_x = local_x + g_max_r[0];
2893
2894 println!(
2895 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2896 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2897 );
2898
2899 if local_max_x > 500.01 {
2900 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2901 }
2902 }
2903 }
2904
2905 assert!(
2906 violations.is_empty(),
2907 "Found {} Gear pieces exceeding boundary: {:?}",
2908 violations.len(),
2909 violations
2910 );
2911 }
2912
2913 #[test]
2914 fn test_blf_bounds_expanded_like_benchmark() {
2915 type ShapeDef = (Vec<(f64, f64)>, usize, Vec<f64>);
2919 let shape_defs: Vec<ShapeDef> = vec![
2920 (
2921 vec![
2922 (0.0, 0.0),
2923 (180.0, 0.0),
2924 (195.0, 15.0),
2925 (200.0, 50.0),
2926 (200.0, 150.0),
2927 (195.0, 185.0),
2928 (180.0, 200.0),
2929 (20.0, 200.0),
2930 (5.0, 185.0),
2931 (0.0, 150.0),
2932 (0.0, 50.0),
2933 (5.0, 15.0),
2934 ],
2935 2,
2936 vec![0.0, 90.0, 180.0, 270.0],
2937 ),
2938 (
2939 vec![
2940 (60.0, 0.0),
2941 (85.0, 7.0),
2942 (104.0, 25.0),
2943 (118.0, 50.0),
2944 (120.0, 60.0),
2945 (118.0, 70.0),
2946 (104.0, 95.0),
2947 (85.0, 113.0),
2948 (60.0, 120.0),
2949 (35.0, 113.0),
2950 (16.0, 95.0),
2951 (2.0, 70.0),
2952 (0.0, 60.0),
2953 (2.0, 50.0),
2954 (16.0, 25.0),
2955 (35.0, 7.0),
2956 ],
2957 4,
2958 vec![0.0, 45.0, 90.0, 135.0],
2959 ),
2960 (
2961 vec![
2962 (0.0, 0.0),
2963 (80.0, 0.0),
2964 (80.0, 20.0),
2965 (20.0, 20.0),
2966 (20.0, 80.0),
2967 (0.0, 80.0),
2968 ],
2969 6,
2970 vec![0.0, 90.0, 180.0, 270.0],
2971 ),
2972 (
2973 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
2974 6,
2975 vec![0.0, 90.0, 180.0, 270.0],
2976 ),
2977 (
2978 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
2979 4,
2980 vec![0.0, 90.0],
2981 ),
2982 (
2983 vec![
2984 (15.0, 0.0),
2985 (45.0, 0.0),
2986 (60.0, 26.0),
2987 (45.0, 52.0),
2988 (15.0, 52.0),
2989 (0.0, 26.0),
2990 ],
2991 8,
2992 vec![0.0, 60.0, 120.0],
2993 ),
2994 (
2995 vec![
2996 (0.0, 0.0),
2997 (90.0, 0.0),
2998 (90.0, 12.0),
2999 (55.0, 12.0),
3000 (55.0, 60.0),
3001 (35.0, 60.0),
3002 (35.0, 12.0),
3003 (0.0, 12.0),
3004 ],
3005 4,
3006 vec![0.0, 90.0, 180.0, 270.0],
3007 ),
3008 (
3009 vec![
3010 (0.0, 10.0),
3011 (10.0, 0.0),
3012 (70.0, 0.0),
3013 (80.0, 10.0),
3014 (80.0, 70.0),
3015 (70.0, 80.0),
3016 (10.0, 80.0),
3017 (0.0, 70.0),
3018 ],
3019 3,
3020 vec![0.0, 90.0],
3021 ),
3022 (
3024 vec![
3025 (50.0, 5.0),
3026 (65.0, 15.0),
3027 (77.0, 18.0),
3028 (80.0, 32.0),
3029 (95.0, 50.0),
3030 (80.0, 68.0),
3031 (77.0, 82.0),
3032 (65.0, 85.0),
3033 (50.0, 95.0),
3034 (35.0, 85.0),
3035 (23.0, 82.0),
3036 (20.0, 68.0),
3037 (5.0, 50.0),
3038 (20.0, 32.0),
3039 (23.0, 18.0),
3040 (35.0, 15.0),
3041 ],
3042 13,
3043 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
3044 ),
3045 ];
3046
3047 let mut geometries = Vec::new();
3049 let mut piece_id = 0;
3050 for (vertices, demand, rotations) in shape_defs.iter() {
3051 for _ in 0..*demand {
3052 let geom = Geometry2D::new(format!("piece_{}", piece_id))
3053 .with_polygon(vertices.clone())
3054 .with_rotations_deg(rotations.clone());
3055 geometries.push(geom);
3056 piece_id += 1;
3057 }
3058 }
3059
3060 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
3062 let (gear_min, gear_max) = gear_geom.aabb();
3063 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
3064
3065 let boundary = Boundary2D::rectangle(500.0, 500.0);
3066 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3067 let nester = Nester2D::new(config);
3068
3069 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3070
3071 println!(
3072 "Placed {} pieces across {} strips",
3073 result.placements.len(),
3074 result.boundaries_used
3075 );
3076
3077 let strip_width = 500.0;
3079 let mut violations = Vec::new();
3080
3081 for p in &result.placements {
3082 let id_num: usize = p
3083 .geometry_id
3084 .as_str()
3085 .strip_prefix("piece_")
3086 .and_then(|s| s.parse().ok())
3087 .unwrap_or(0);
3088
3089 if (37..=49).contains(&id_num) {
3091 let origin_x = p.position[0];
3092 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3093 let strip_idx = p.boundary_index;
3094 let local_x = origin_x - (strip_idx as f64 * strip_width);
3095
3096 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
3097 let local_max_x = local_x + g_max_r[0];
3098
3099 println!(
3100 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
3101 p.geometry_id, strip_idx, local_x, rotation, local_max_x
3102 );
3103
3104 if local_max_x > 500.01 {
3105 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
3106 }
3107 }
3108 }
3109
3110 assert!(
3111 violations.is_empty(),
3112 "Found {} Gear pieces exceeding boundary: {:?}",
3113 violations.len(),
3114 violations
3115 );
3116 }
3117
3118 fn aabbs_overlap(
3120 a_min: [f64; 2],
3121 a_max: [f64; 2],
3122 b_min: [f64; 2],
3123 b_max: [f64; 2],
3124 tolerance: f64,
3125 ) -> bool {
3126 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
3128 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
3129 x_overlap && y_overlap
3130 }
3131
3132 #[test]
3134 fn test_all_strategies_boundary_and_overlap() {
3135 use std::collections::HashMap;
3136
3137 let shapes = vec![
3139 Geometry2D::new("shape0")
3140 .with_polygon(vec![
3141 (0.0, 0.0),
3142 (180.0, 0.0),
3143 (195.0, 15.0),
3144 (200.0, 50.0),
3145 (200.0, 150.0),
3146 (195.0, 185.0),
3147 (180.0, 200.0),
3148 (20.0, 200.0),
3149 (5.0, 185.0),
3150 (0.0, 150.0),
3151 (0.0, 50.0),
3152 (5.0, 15.0),
3153 ])
3154 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3155 .with_quantity(2),
3156 Geometry2D::new("shape1_flange")
3157 .with_polygon(vec![
3158 (60.0, 0.0),
3159 (85.0, 7.0),
3160 (104.0, 25.0),
3161 (118.0, 50.0),
3162 (120.0, 60.0),
3163 (118.0, 70.0),
3164 (104.0, 95.0),
3165 (85.0, 113.0),
3166 (60.0, 120.0),
3167 (35.0, 113.0),
3168 (16.0, 95.0),
3169 (2.0, 70.0),
3170 (0.0, 60.0),
3171 (2.0, 50.0),
3172 (16.0, 25.0),
3173 (35.0, 7.0),
3174 ])
3175 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
3176 .with_quantity(4),
3177 Geometry2D::new("shape2_lbracket")
3178 .with_polygon(vec![
3179 (0.0, 0.0),
3180 (80.0, 0.0),
3181 (80.0, 20.0),
3182 (20.0, 20.0),
3183 (20.0, 80.0),
3184 (0.0, 80.0),
3185 ])
3186 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3187 .with_quantity(6),
3188 Geometry2D::new("shape3_triangle")
3189 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
3190 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3191 .with_quantity(6),
3192 Geometry2D::new("shape4_rect")
3193 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
3194 .with_rotations_deg(vec![0.0, 90.0])
3195 .with_quantity(4),
3196 Geometry2D::new("shape5_hexagon")
3197 .with_polygon(vec![
3198 (15.0, 0.0),
3199 (45.0, 0.0),
3200 (60.0, 26.0),
3201 (45.0, 52.0),
3202 (15.0, 52.0),
3203 (0.0, 26.0),
3204 ])
3205 .with_rotations_deg(vec![0.0, 60.0, 120.0])
3206 .with_quantity(8),
3207 Geometry2D::new("shape6_tstiff")
3208 .with_polygon(vec![
3209 (0.0, 0.0),
3210 (90.0, 0.0),
3211 (90.0, 12.0),
3212 (55.0, 12.0),
3213 (55.0, 60.0),
3214 (35.0, 60.0),
3215 (35.0, 12.0),
3216 (0.0, 12.0),
3217 ])
3218 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3219 .with_quantity(4),
3220 Geometry2D::new("shape7_mount")
3221 .with_polygon(vec![
3222 (0.0, 10.0),
3223 (10.0, 0.0),
3224 (70.0, 0.0),
3225 (80.0, 10.0),
3226 (80.0, 70.0),
3227 (70.0, 80.0),
3228 (10.0, 80.0),
3229 (0.0, 70.0),
3230 ])
3231 .with_rotations_deg(vec![0.0, 90.0])
3232 .with_quantity(3),
3233 Geometry2D::new("shape8_gear")
3234 .with_polygon(vec![
3235 (50.0, 5.0),
3236 (65.0, 15.0),
3237 (77.0, 18.0),
3238 (80.0, 32.0),
3239 (95.0, 50.0),
3240 (80.0, 68.0),
3241 (77.0, 82.0),
3242 (65.0, 85.0),
3243 (50.0, 95.0),
3244 (35.0, 85.0),
3245 (23.0, 82.0),
3246 (20.0, 68.0),
3247 (5.0, 50.0),
3248 (20.0, 32.0),
3249 (23.0, 18.0),
3250 (35.0, 15.0),
3251 ])
3252 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
3253 .with_quantity(13),
3254 ];
3255
3256 let geom_map: HashMap<String, &Geometry2D> =
3258 shapes.iter().map(|g| (g.id().clone(), g)).collect();
3259
3260 let boundary = Boundary2D::rectangle(500.0, 500.0);
3261 let strip_width = 500.0;
3262
3263 let strategies = vec![
3265 Strategy::BottomLeftFill,
3266 Strategy::NfpGuided,
3267 Strategy::GeneticAlgorithm,
3268 Strategy::Brkga,
3269 Strategy::SimulatedAnnealing,
3270 Strategy::Gdrr,
3271 Strategy::Alns,
3272 ];
3273
3274 for strategy in strategies {
3275 println!("\n========== Testing {:?} ==========", strategy);
3276
3277 let config = Config::default()
3278 .with_strategy(strategy)
3279 .with_time_limit(30000); let nester = Nester2D::new(config);
3281
3282 let result = match nester.solve_multi_strip(&shapes, &boundary) {
3283 Ok(r) => r,
3284 Err(e) => {
3285 println!(" Strategy {:?} failed: {}", strategy, e);
3286 continue;
3287 }
3288 };
3289
3290 println!(
3291 " Placed {} pieces across {} strips",
3292 result.placements.len(),
3293 result.boundaries_used
3294 );
3295
3296 let mut boundary_violations = Vec::new();
3298 for p in &result.placements {
3299 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
3301 let full_id = if base_id.starts_with("shape") {
3302 shapes
3304 .iter()
3305 .find(|g| p.geometry_id.starts_with(g.id()))
3306 .map(|g| g.id().as_str())
3307 } else {
3308 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
3309 };
3310
3311 let geom = match full_id.and_then(|id| geom_map.get(id)) {
3312 Some(g) => *g,
3313 None => {
3314 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
3316 Some(g) => g,
3317 None => {
3318 println!(
3319 " WARNING: Could not find geometry for {}",
3320 p.geometry_id
3321 );
3322 continue;
3323 }
3324 }
3325 }
3326 };
3327
3328 let origin_x = p.position[0];
3329 let origin_y = p.position[1];
3330 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3331 let strip_idx = p.boundary_index;
3332
3333 let local_x = origin_x - (strip_idx as f64 * strip_width);
3335
3336 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
3337
3338 let local_min_x = local_x + g_min[0];
3340 let local_max_x = local_x + g_max[0];
3341 let local_min_y = origin_y + g_min[1];
3342 let local_max_y = origin_y + g_max[1];
3343
3344 let tolerance = 0.1;
3346 if local_min_x < -tolerance
3347 || local_max_x > 500.0 + tolerance
3348 || local_min_y < -tolerance
3349 || local_max_y > 500.0 + tolerance
3350 {
3351 boundary_violations.push(format!(
3352 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
3353 p.geometry_id,
3354 strip_idx,
3355 local_min_x,
3356 local_min_y,
3357 local_max_x,
3358 local_max_y
3359 ));
3360 }
3361 }
3362
3363 if !boundary_violations.is_empty() {
3364 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
3365 for v in &boundary_violations {
3366 println!(" - {}", v);
3367 }
3368 }
3369
3370 let mut overlaps = Vec::new();
3372 let placements: Vec<_> = result.placements.iter().collect();
3373
3374 for i in 0..placements.len() {
3375 for j in (i + 1)..placements.len() {
3376 let p1 = placements[i];
3377 let p2 = placements[j];
3378
3379 if p1.boundary_index != p2.boundary_index {
3381 continue;
3382 }
3383
3384 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
3386 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
3387
3388 let (g1, g2) = match (g1, g2) {
3389 (Some(a), Some(b)) => (a, b),
3390 _ => continue,
3391 };
3392
3393 let strip_idx = p1.boundary_index;
3394 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
3395 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
3396
3397 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
3398 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
3399
3400 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
3401 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
3402
3403 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
3404 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
3405 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
3406 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
3407
3408 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
3409 overlaps.push(format!(
3410 "{} and {} in strip {}",
3411 p1.geometry_id, p2.geometry_id, strip_idx
3412 ));
3413 }
3414 }
3415 }
3416
3417 if !overlaps.is_empty() {
3418 println!(" OVERLAPS ({}):", overlaps.len());
3419 for o in overlaps.iter().take(10) {
3420 println!(" - {}", o);
3421 }
3422 if overlaps.len() > 10 {
3423 println!(" ... and {} more", overlaps.len() - 10);
3424 }
3425 }
3426
3427 assert!(
3429 boundary_violations.is_empty(),
3430 "{:?}: Found {} boundary violations",
3431 strategy,
3432 boundary_violations.len()
3433 );
3434
3435 println!(" ✓ All placements within boundary");
3436 println!(" ✓ No AABB overlaps detected");
3437 }
3438 }
3439
3440 #[test]
3448 fn test_multi_strip_distributes_all_instances() {
3449 let geometries = vec![Geometry2D::rectangle("part", 100.0, 100.0).with_quantity(20)];
3450 let boundary = Boundary2D::rectangle(300.0, 300.0);
3451 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3452 let nester = Nester2D::new(config);
3453
3454 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3455
3456 assert_eq!(
3458 result.placements.len(),
3459 20,
3460 "all 20 instances must be placed"
3461 );
3462 assert_eq!(
3463 result.boundaries_used, 3,
3464 "20 of 100x100 in 300x300 => 3 sheets"
3465 );
3466 assert!(
3467 result.unplaced.is_empty(),
3468 "nothing should be unplaced, got {:?}",
3469 result.unplaced
3470 );
3471 assert_eq!(result.total_requested, 20);
3473
3474 let mut seen = std::collections::HashSet::new();
3477 for p in &result.placements {
3478 assert!(
3479 seen.insert((p.geometry_id.clone(), p.instance)),
3480 "duplicate (id, instance) = ({}, {}) across sheets",
3481 p.geometry_id,
3482 p.instance
3483 );
3484 }
3485 let mut sheets: Vec<usize> = result.placements.iter().map(|p| p.boundary_index).collect();
3487 sheets.sort_unstable();
3488 sheets.dedup();
3489 assert_eq!(sheets, vec![0, 1, 2]);
3490 }
3491
3492 #[test]
3495 fn test_multi_strip_oversized_reported_unplaced() {
3496 let geometries = vec![
3497 Geometry2D::rectangle("ok", 50.0, 50.0).with_quantity(2),
3498 Geometry2D::rectangle("toobig", 400.0, 400.0).with_quantity(3),
3499 ];
3500 let boundary = Boundary2D::rectangle(300.0, 300.0);
3501 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3502 let nester = Nester2D::new(config);
3503
3504 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3505
3506 assert_eq!(result.total_requested, 5, "2 + 3 instances requested");
3507 assert_eq!(result.placements.len(), 2);
3509 assert!(
3510 result.unplaced.contains(&"toobig".to_string()),
3511 "oversized geometry must surface in unplaced, got {:?}",
3512 result.unplaced
3513 );
3514 }
3515}