1use crate::alns_nesting::run_alns_nesting;
4use crate::boundary::Boundary2D;
5use crate::brkga_nesting::run_brkga_nesting;
6use crate::clamp_placement_to_boundary_with_margin;
7use crate::ga_nesting::{run_ga_nesting, run_ga_nesting_with_progress};
8use crate::gdrr_nesting::run_gdrr_nesting;
9use crate::geometry::Geometry2D;
10use crate::nfp::{
11 compute_ifp_with_margin_and_mirror, compute_nfp_mirrored, find_bottom_left_placement,
12 rotate_nfp, translate_nfp, Nfp, NfpCache, 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::{Config, ProgressCallback, ProgressInfo, Solver, Strategy};
27use u_nesting_core::{Error, Placement, Result, SolveResult};
28
29use crate::placement_utils::{hole_nfps, inset_boundary, offset_nfp};
30use std::sync::atomic::{AtomicBool, Ordering};
31use std::sync::Arc;
32use u_nesting_core::timing::Timer;
33
34const SEARCH_POPULATION: usize = 30;
39
40const SEARCH_GENERATIONS: u32 = 50;
43
44fn mirror_candidates(geom: &Geometry2D) -> &'static [bool] {
56 if geom.allow_flip() {
57 &[false, true]
58 } else {
59 &[false]
60 }
61}
62
63fn solution_quality(
77 result: &SolveResult<f64>,
78 geometries: &[Geometry2D],
79 boundary: &Boundary2D,
80) -> (usize, f64) {
81 use std::collections::HashMap;
82 let geom_map: HashMap<_, _> = geometries.iter().map(|g| (g.id().clone(), g)).collect();
83
84 let mut min_x = f64::INFINITY;
85 let mut min_y = f64::INFINITY;
86 let mut max_x = f64::NEG_INFINITY;
87 let mut max_y = f64::NEG_INFINITY;
88
89 for p in &result.placements {
90 if let Some(geom) = geom_map.get(&p.geometry_id) {
91 let x = p.position.first().copied().unwrap_or(0.0);
92 let y = p.position.get(1).copied().unwrap_or(0.0);
93 let rot = p.rotation.first().copied().unwrap_or(0.0);
94 let (g_min, g_max) = geom.aabb_at_rotation(rot);
95 min_x = min_x.min(x + g_min[0]);
96 min_y = min_y.min(y + g_min[1]);
97 max_x = max_x.max(x + g_max[0]);
98 max_y = max_y.max(y + g_max[1]);
99 }
100 }
101
102 if result.placements.is_empty() {
103 return (0, f64::INFINITY);
104 }
105
106 let strip_length = match PackingAxis::of(boundary) {
108 PackingAxis::Y => max_y - min_y,
109 PackingAxis::X => max_x - min_x,
110 };
111 (result.placements.len(), strip_length)
112}
113
114pub struct Nester2D {
116 config: Config,
117 cancelled: Arc<AtomicBool>,
118 #[allow(dead_code)] nfp_cache: NfpCache,
120}
121
122#[cfg(feature = "milp")]
133fn milp_grid_step(geometries: &[Geometry2D], boundary: &Boundary2D) -> f64 {
134 let smallest_side = geometries
135 .iter()
136 .map(|g| {
137 let (g_min, g_max) = g.aabb_at_rotation(0.0);
138 (g_max[0] - g_min[0]).min(g_max[1] - g_min[1])
139 })
140 .fold(f64::INFINITY, f64::min);
141 if !smallest_side.is_finite() || smallest_side <= 0.0 {
142 return 1.0;
143 }
144 let (b_min, b_max) = boundary.aabb();
145 let longest_side = (b_max[0] - b_min[0]).max(b_max[1] - b_min[1]);
146 (smallest_side / 4.0).max(1.0).max(longest_side / 256.0)
149}
150
151#[cfg(feature = "milp")]
157fn milp_time_limit(requested_ms: u64) -> u64 {
158 if requested_ms == 0 {
159 u64::MAX / 2
160 } else {
161 requested_ms
162 }
163}
164
165#[derive(Debug, Clone, Copy)]
168struct BlfCandidate {
169 rotation: f64,
170 extent_across: f64,
171 extent_along: f64,
172 place_across: f64,
173 place_along: f64,
174 opened_row: bool,
176}
177
178impl Nester2D {
179 fn run_strategy(
183 &self,
184 geometries: &[Geometry2D],
185 boundary: &Boundary2D,
186 ) -> Result<SolveResult<f64>> {
187 match self.config.strategy {
188 Strategy::BottomLeftFill => self.bottom_left_fill(geometries, boundary),
189 Strategy::NfpGuided => self.nfp_guided_blf(geometries, boundary),
190 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
191 Strategy::Brkga => self.brkga(geometries, boundary),
192 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
193 Strategy::Gdrr => self.gdrr(geometries, boundary),
194 Strategy::Alns => self.alns(geometries, boundary),
195 #[cfg(feature = "milp")]
196 Strategy::MilpExact => self.milp_exact(geometries, boundary),
197 #[cfg(feature = "milp")]
198 Strategy::HybridExact => self.hybrid_exact(geometries, boundary),
199 Strategy::ExtremePoint => Err(Error::invalid_option(
200 "strategy",
201 "strategy ExtremePoint is 3D only; 2D nesting offers BottomLeftFill, \
202 NfpGuided, GeneticAlgorithm, Brkga, SimulatedAnnealing, Gdrr and Alns"
203 .into(),
204 )),
205 #[cfg(not(feature = "milp"))]
206 Strategy::MilpExact | Strategy::HybridExact => Err(Error::invalid_option(
207 "strategy",
208 format!(
209 "strategy {:?} needs the `milp` feature, which this build does not have",
210 self.config.strategy
211 ),
212 )),
213 }
214 }
215
216 pub fn new(config: Config) -> Self {
218 Self {
219 config,
220 cancelled: Arc::new(AtomicBool::new(false)),
221 nfp_cache: NfpCache::new(),
222 }
223 }
224
225 pub fn default_config() -> Self {
227 Self::new(Config::default())
228 }
229
230 fn ga_config(&self) -> GaConfig {
234 GaConfig::default()
235 .with_population_size(self.config.population_size.unwrap_or(SEARCH_POPULATION))
236 .with_max_generations(self.config.max_generations.unwrap_or(SEARCH_GENERATIONS))
237 .with_crossover_rate(self.config.crossover_rate)
238 .with_mutation_rate(self.config.mutation_rate)
239 .with_elite_count(self.config.elite_count)
240 }
241
242 fn brkga_config(&self) -> BrkgaConfig {
245 BrkgaConfig::default()
246 .with_population_size(self.config.population_size.unwrap_or(SEARCH_POPULATION))
247 .with_max_generations(self.config.max_generations.unwrap_or(SEARCH_GENERATIONS))
248 .with_elite_fraction(0.2)
249 .with_mutant_fraction(0.15)
250 .with_elite_bias(0.7)
251 }
252
253 fn gdrr_config(&self, time_limit_ms: u64) -> GdrrConfig {
255 GdrrConfig {
256 seed: self.config.seed,
257 ..GdrrConfig::default()
258 .with_max_iterations(1000) .with_time_limit_ms(time_limit_ms)
260 .with_ruin_ratio(0.1, 0.3) .with_lahc_list_length(30) }
263 }
264
265 fn alns_config(&self, time_limit_ms: u64) -> AlnsConfig {
267 AlnsConfig {
268 seed: self.config.seed,
269 ..AlnsConfig::default()
270 .with_max_iterations(1000) .with_time_limit_ms(time_limit_ms)
272 .with_segment_size(50) .with_scores(33.0, 9.0, 13.0)
274 .with_reaction_factor(0.15) .with_temperature(100.0, 0.999, 0.1) }
277 }
278
279 fn bottom_left_fill(
289 &self,
290 geometries: &[Geometry2D],
291 boundary: &Boundary2D,
292 ) -> Result<SolveResult<f64>> {
293 self.bottom_left_fill_impl(geometries, boundary, None)
294 }
295
296 fn nfp_guided_blf(
301 &self,
302 geometries: &[Geometry2D],
303 boundary: &Boundary2D,
304 ) -> Result<SolveResult<f64>> {
305 let start = Timer::now();
306 let mut result = SolveResult::new();
307 let mut placements = Vec::new();
308 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
309
310 let margin = self.config.margin;
311 let spacing = self.config.spacing;
312
313 let boundary_polygon = inset_boundary(boundary, margin);
315
316 let mut total_placed_area = 0.0;
317
318 let sample_step = self.compute_sample_step(geometries);
320
321 for geom in geometries {
322 geom.validate()?;
323
324 let rotation_angles: Vec<f64> = geom.rotations();
327
328 let mirror_candidates = mirror_candidates(geom);
329
330 for instance in 0..geom.quantity() {
331 if self.cancelled.load(Ordering::Relaxed) {
332 result.computation_time_ms = start.elapsed_ms();
333 return Ok(result);
334 }
335
336 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
338 {
339 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
340 result.utilization = total_placed_area / boundary.measure();
341 result.computation_time_ms = start.elapsed_ms();
342 result.placements = placements;
343 return Ok(result);
344 }
345
346 let mut best_placement: Option<(f64, f64, f64, bool)> = None; for &rotation in &rotation_angles {
350 for &mirror in mirror_candidates {
351 let ifp = match compute_ifp_with_margin_and_mirror(
353 &boundary_polygon,
354 geom,
355 rotation,
356 0.0,
358 mirror,
359 ) {
360 Ok(ifp) => ifp,
361 Err(_) => continue,
362 };
363
364 if ifp.is_empty() {
365 continue;
366 }
367
368 let mut nfps: Vec<Nfp> = Vec::new();
370 for placed in &placed_geometries {
371 let cache_key = (
374 placed.geometry.id().as_str(),
375 geom.id().as_str(),
376 rotation - placed.rotation, placed.mirrored,
378 mirror,
379 );
380
381 let nfp_at_origin =
386 match self.nfp_cache.get_or_compute_mirrored(cache_key, || {
387 let placed_at_origin = placed.geometry.clone();
388 compute_nfp_mirrored(
389 &placed_at_origin,
390 geom,
391 rotation - placed.rotation,
392 placed.mirrored,
393 mirror,
394 )
395 .map(|nfp| offset_nfp(&nfp, spacing))
399 }) {
400 Ok(nfp) => nfp,
401 Err(_) => continue,
402 };
403
404 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
407 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
408 nfps.push(translated_nfp);
409 }
410
411 nfps.extend(hole_nfps(boundary, geom, rotation, mirror, margin));
416 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
417 if let Some((x, y)) = find_bottom_left_placement(
418 &ifp,
419 &nfp_refs,
420 sample_step,
421 PackingAxis::of(boundary),
422 ) {
423 let is_better =
425 match best_placement {
426 None => true,
427 Some((best_x, best_y, _, _)) => PackingAxis::of(boundary)
428 .precedes((x, y), (best_x, best_y), 1e-6),
429 };
430 if is_better {
431 best_placement = Some((x, y, rotation, mirror));
432 }
433 }
434 }
435 }
436
437 if let Some((x, y, rotation, mirror)) = best_placement {
439 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
446 let boundary_aabb = boundary.aabb();
447
448 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
449 x,
450 y,
451 geom_aabb,
452 boundary_aabb,
453 margin,
454 ) {
455 let placement = Placement::new_2d(
456 geom.id().clone(),
457 instance,
458 clamped_x,
459 clamped_y,
460 rotation,
461 )
462 .with_mirrored(mirror);
463
464 placements.push(placement);
465 placed_geometries.push(
466 PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation)
467 .with_mirrored(mirror),
468 );
469 total_placed_area += geom.measure();
470 } else {
471 result.unplaced.push(geom.id().clone());
473 }
474 } else {
475 result.unplaced.push(geom.id().clone());
477 }
478 }
479 }
480
481 result.placements = placements;
482 result.boundaries_used = 1;
483 result.utilization = total_placed_area / boundary.measure();
484 result.computation_time_ms = start.elapsed_ms();
485
486 Ok(result)
487 }
488
489 fn compute_sample_step(&self, geometries: &[Geometry2D]) -> f64 {
491 if geometries.is_empty() {
492 return 1.0;
493 }
494
495 let mut min_dim = f64::INFINITY;
497 for geom in geometries {
498 let (g_min, g_max) = geom.aabb();
499 let width = g_max[0] - g_min[0];
500 let height = g_max[1] - g_min[1];
501 min_dim = min_dim.min(width).min(height);
502 }
503
504 (min_dim / 4.0).clamp(0.5, 10.0)
506 }
507
508 fn not_worse_than_baselines(
518 &self,
519 meta: SolveResult<f64>,
520 geometries: &[Geometry2D],
521 boundary: &Boundary2D,
522 greedy: Option<SolveResult<f64>>,
523 ) -> SolveResult<f64> {
524 let baselines = self
525 .bottom_left_fill(geometries, boundary)
526 .ok()
527 .into_iter()
528 .chain(greedy);
529 let quality = |r: &SolveResult<f64>| solution_quality(r, geometries, boundary);
530 meta.floored_at(baselines, |a, b| {
533 let (a, b) = (quality(a), quality(b));
534 a.0 > b.0 || (a.0 == b.0 && a.1 < b.1 - 1e-6)
535 })
536 }
537
538 fn search_budget_ms(&self, default_ms: u64) -> u64 {
542 if self.config.time_limit_ms > 0 {
543 (self.config.time_limit_ms / 4)
544 .max(5000)
545 .min(self.config.time_limit_ms)
546 } else {
547 default_ms
548 }
549 }
550
551 fn greedy_start(
558 &self,
559 geometries: &[Geometry2D],
560 boundary: &Boundary2D,
561 budget_ms: u64,
562 ) -> (Option<SolveResult<f64>>, u64) {
563 let started = Timer::now();
564 let greedy = Nester2D {
565 config: Config {
566 time_limit_ms: budget_ms,
567 ..self.config.clone()
568 },
569 cancelled: self.cancelled.clone(),
570 nfp_cache: NfpCache::new(),
571 }
572 .nfp_guided_blf(geometries, boundary)
573 .ok();
574 (
575 greedy,
576 budget_ms.saturating_sub(started.elapsed_ms()).max(1),
577 )
578 }
579
580 fn genetic_algorithm(
585 &self,
586 geometries: &[Geometry2D],
587 boundary: &Boundary2D,
588 ) -> Result<SolveResult<f64>> {
589 let (time_limit_ms, greedy) = {
590 let budget = self.search_budget_ms(15000);
591 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget);
592 (remaining, greedy)
593 };
594
595 let ga_config = self
596 .ga_config()
597 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
598
599 let result = run_ga_nesting(
600 geometries,
601 boundary,
602 &self.config,
603 ga_config,
604 self.cancelled.clone(),
605 greedy.as_ref().map(|g| g.placements.as_slice()),
606 )?;
607
608 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
609 }
610
611 fn brkga(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
615 let (time_limit_ms, greedy) = {
616 let budget = self.search_budget_ms(15000);
617 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget);
618 (remaining, greedy)
619 };
620
621 let brkga_config = self
622 .brkga_config()
623 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
624
625 let result = run_brkga_nesting(
626 geometries,
627 boundary,
628 &self.config,
629 brkga_config,
630 self.cancelled.clone(),
631 greedy.as_ref().map(|g| g.placements.as_slice()),
632 )?;
633
634 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
635 }
636
637 fn simulated_annealing(
642 &self,
643 geometries: &[Geometry2D],
644 boundary: &Boundary2D,
645 ) -> Result<SolveResult<f64>> {
646 let (time_limit_ms, greedy) = {
647 let budget = self.search_budget_ms(10000);
648 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget);
649 (remaining, greedy)
650 };
651
652 let sa_config = SaConfig::default()
653 .with_initial_temp(50.0) .with_final_temp(1.0) .with_cooling_rate(0.9) .with_iterations_per_temp(20) .with_max_iterations(500) .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
659
660 let result = run_sa_nesting(
661 geometries,
662 boundary,
663 &self.config,
664 sa_config,
665 self.cancelled.clone(),
666 greedy.as_ref().map(|g| g.placements.as_slice()),
667 )?;
668
669 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
670 }
671
672 fn gdrr(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
674 let (time_limit, greedy) = {
675 let budget = self.search_budget_ms(10000);
676 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget);
677 (remaining, greedy)
678 };
679 let result = run_gdrr_nesting(
680 geometries,
681 boundary,
682 &self.config,
683 &self.gdrr_config(time_limit),
684 self.cancelled.clone(),
685 )?;
686
687 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
688 }
689
690 fn alns(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
692 let (time_limit, greedy) = {
693 let budget = self.search_budget_ms(10000);
694 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget);
695 (remaining, greedy)
696 };
697 let result = run_alns_nesting(
698 geometries,
699 boundary,
700 &self.config,
701 &self.alns_config(time_limit),
702 self.cancelled.clone(),
703 )?;
704
705 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
706 }
707
708 #[cfg(feature = "milp")]
721 fn milp_exact(
722 &self,
723 geometries: &[Geometry2D],
724 boundary: &Boundary2D,
725 ) -> Result<SolveResult<f64>> {
726 let exact_config = ExactConfig::default()
731 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms))
732 .with_max_items(15)
733 .with_rotation_steps(4)
734 .with_grid_step(milp_grid_step(geometries, boundary));
735
736 run_nfp_cm_nesting(
737 geometries,
738 boundary,
739 &self.config,
740 &exact_config,
741 self.cancelled.clone(),
742 )
743 }
744
745 #[cfg(feature = "milp")]
747 fn hybrid_exact(
748 &self,
749 geometries: &[Geometry2D],
750 boundary: &Boundary2D,
751 ) -> Result<SolveResult<f64>> {
752 let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
754
755 if total_instances <= 15 {
757 let exact_config = ExactConfig::default()
761 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms / 2))
762 .with_max_items(15);
763
764 let exact_result = run_nfp_cm_nesting(
765 geometries,
766 boundary,
767 &self.config,
768 &exact_config,
769 self.cancelled.clone(),
770 )?;
771
772 if !exact_result.placements.is_empty() {
774 return Ok(exact_result);
775 }
776 }
777
778 self.alns(geometries, boundary)
780 }
781
782 fn bottom_left_fill_with_progress(
787 &self,
788 geometries: &[Geometry2D],
789 boundary: &Boundary2D,
790 callback: &ProgressCallback,
791 ) -> Result<SolveResult<f64>> {
792 self.bottom_left_fill_impl(geometries, boundary, Some(callback))
793 }
794
795 fn bottom_left_fill_impl(
803 &self,
804 geometries: &[Geometry2D],
805 boundary: &Boundary2D,
806 callback: Option<&ProgressCallback>,
807 ) -> Result<SolveResult<f64>> {
808 let start = Timer::now();
809 let mut result = SolveResult::new();
810 let mut placements = Vec::new();
811 let report = |info: ProgressInfo| {
812 if let Some(callback) = callback {
813 callback(info);
814 }
815 };
816
817 let axis = PackingAxis::of(boundary);
818 let to_ca = |x: f64, y: f64| match axis {
820 PackingAxis::Y => (x, y),
821 PackingAxis::X => (y, x),
822 };
823 let from_ca = |across: f64, along: f64| match axis {
824 PackingAxis::Y => (across, along),
825 PackingAxis::X => (along, across),
826 };
827
828 let (b_min, b_max) = boundary.aabb();
829 let margin = self.config.margin;
830 let spacing = self.config.spacing;
831 let (min_across, min_along) = to_ca(b_min[0] + margin, b_min[1] + margin);
832 let (max_across, max_along) = to_ca(b_max[0] - margin, b_max[1] - margin);
833
834 let hole_boxes: Vec<(f64, f64, f64, f64)> = boundary
841 .holes()
842 .iter()
843 .filter(|hole| !hole.is_empty())
844 .map(|hole| {
845 let (mut x0, mut y0) = (f64::INFINITY, f64::INFINITY);
846 let (mut x1, mut y1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
847 for &(x, y) in hole {
848 x0 = x0.min(x);
849 y0 = y0.min(y);
850 x1 = x1.max(x);
851 y1 = y1.max(y);
852 }
853 let (a0, l0) = to_ca(x0 - margin, y0 - margin);
854 let (a1, l1) = to_ca(x1 + margin, y1 + margin);
855 (a0.min(a1), l0.min(l1), a0.max(a1), l0.max(l1))
856 })
857 .collect();
858
859 let blocking_hole_edge = |across: f64, along: f64, ea: f64, el: f64| -> Option<f64> {
863 hole_boxes
864 .iter()
865 .filter(|(a0, l0, a1, l1)| {
866 across < *a1 && across + ea > *a0 && along < *l1 && along + el > *l0
867 })
868 .map(|(_, _, a1, _)| *a1)
869 .fold(None, |far: Option<f64>, a1| {
870 Some(far.map_or(a1, |f| f.max(a1)))
871 })
872 };
873
874 let mut cursor_across = min_across;
875 let mut cursor_along = min_along;
876 let mut row_depth = 0.0_f64;
877 let mut total_placed_area = 0.0;
878 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
879 let mut placed_count = 0usize;
880
881 report(
882 ProgressInfo::new()
883 .with_phase("BLF Placement")
884 .with_items(0, total_pieces)
885 .with_elapsed(0),
886 );
887
888 for geom in geometries {
889 geom.validate()?;
890
891 let rotation_angles: Vec<f64> = geom.rotations();
893
894 for instance in 0..geom.quantity() {
895 if self.cancelled.load(Ordering::Relaxed) {
896 result.computation_time_ms = start.elapsed_ms();
897 report(
898 ProgressInfo::new()
899 .with_phase("Cancelled")
900 .with_items(placed_count, total_pieces)
901 .with_elapsed(result.computation_time_ms)
902 .finished(),
903 );
904 return Ok(result);
905 }
906
907 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
909 {
910 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
911 result.utilization = total_placed_area / boundary.measure();
912 result.computation_time_ms = start.elapsed_ms();
913 result.placements = placements;
914 report(
915 ProgressInfo::new()
916 .with_phase("Time Limit Reached")
917 .with_items(placed_count, total_pieces)
918 .with_elapsed(result.computation_time_ms)
919 .finished(),
920 );
921 return Ok(result);
922 }
923
924 let mut best_fit: Option<BlfCandidate> = None;
927
928 for &rotation in &rotation_angles {
929 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
930 let (extent_across, extent_along) =
931 to_ca(g_max[0] - g_min[0], g_max[1] - g_min[1]);
932
933 if extent_across > max_across - min_across
934 || extent_along > max_along - min_along
935 {
936 continue;
937 }
938
939 let mut place_across = cursor_across;
940 let mut place_along = cursor_along;
941 let mut depth = row_depth;
944 let mut opened_row = false;
945 let mut turns = 0usize;
951 let max_turns = 4 * (hole_boxes.len() + 1) * (hole_boxes.len() + 2) + 16;
952 let fits = loop {
953 if place_across + extent_across > max_across {
954 place_across = min_across;
955 place_along += depth + spacing;
956 depth = extent_along;
957 opened_row = true;
958 }
959 if place_along + extent_along > max_along {
960 break false;
961 }
962 match blocking_hole_edge(
963 place_across,
964 place_along,
965 extent_across,
966 extent_along,
967 ) {
968 None => break true,
969 Some(past) => place_across = past + spacing,
970 }
971 turns += 1;
972 if turns > max_turns {
973 break false;
974 }
975 };
976 if !fits {
977 continue;
978 }
979
980 let cost = |c: &BlfCandidate| {
985 if c.opened_row {
986 c.place_along - min_along + c.extent_along
987 } else {
988 c.place_across - min_across + c.extent_across
989 }
990 };
991 let candidate = BlfCandidate {
992 rotation,
993 extent_across,
994 extent_along,
995 place_across,
996 place_along,
997 opened_row,
998 };
999 let better = best_fit
1000 .as_ref()
1001 .is_none_or(|best| cost(&candidate) < cost(best) - 1e-6);
1002 if better {
1003 best_fit = Some(candidate);
1004 }
1005 }
1006
1007 let Some(BlfCandidate {
1008 rotation,
1009 extent_across,
1010 extent_along,
1011 place_across,
1012 place_along,
1013 opened_row,
1014 }) = best_fit
1015 else {
1016 result.unplaced.push(geom.id().clone());
1017 continue;
1018 };
1019 if opened_row {
1020 row_depth = 0.0;
1021 }
1022
1023 let geom_aabb = geom.aabb_at_rotation(rotation);
1024 let (place_x, place_y) = from_ca(place_across, place_along);
1025 let Some((x, y)) = clamp_placement_to_boundary_with_margin(
1026 place_x - geom_aabb.0[0],
1027 place_y - geom_aabb.0[1],
1028 geom_aabb,
1029 (b_min, b_max),
1030 margin,
1031 ) else {
1032 result.unplaced.push(geom.id().clone());
1033 continue;
1034 };
1035
1036 placements.push(Placement::new_2d(
1037 geom.id().clone(),
1038 instance,
1039 x,
1040 y,
1041 rotation,
1042 ));
1043 total_placed_area += geom.measure();
1044 placed_count += 1;
1045
1046 let (actual_across, actual_along) = to_ca(x + geom_aabb.0[0], y + geom_aabb.0[1]);
1048 cursor_across = actual_across + extent_across + spacing;
1049 cursor_along = actual_along;
1050 row_depth = row_depth.max(extent_along);
1051
1052 report(
1053 ProgressInfo::new()
1054 .with_phase("BLF Placement")
1055 .with_items(placed_count, total_pieces)
1056 .with_utilization(total_placed_area / boundary.measure())
1057 .with_elapsed(start.elapsed_ms()),
1058 );
1059 }
1060 }
1061
1062 result.placements = placements;
1063 result.boundaries_used = 1;
1064 result.utilization = total_placed_area / boundary.measure();
1065 result.computation_time_ms = start.elapsed_ms();
1066
1067 report(
1068 ProgressInfo::new()
1069 .with_phase("Complete")
1070 .with_items(placed_count, total_pieces)
1071 .with_utilization(result.utilization)
1072 .with_elapsed(result.computation_time_ms)
1073 .finished(),
1074 );
1075
1076 Ok(result)
1077 }
1078
1079 fn nfp_guided_blf_with_progress(
1081 &self,
1082 geometries: &[Geometry2D],
1083 boundary: &Boundary2D,
1084 callback: &ProgressCallback,
1085 ) -> Result<SolveResult<f64>> {
1086 let start = Timer::now();
1087 let mut result = SolveResult::new();
1088 let mut placements = Vec::new();
1089 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
1090
1091 let margin = self.config.margin;
1092 let spacing = self.config.spacing;
1093 let boundary_polygon = inset_boundary(boundary, margin);
1094
1095 let mut total_placed_area = 0.0;
1096 let sample_step = self.compute_sample_step(geometries);
1097
1098 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
1100 let mut placed_count = 0usize;
1101
1102 callback(
1104 ProgressInfo::new()
1105 .with_phase("NFP Placement")
1106 .with_items(0, total_pieces)
1107 .with_elapsed(0),
1108 );
1109
1110 for geom in geometries {
1111 geom.validate()?;
1112
1113 let rotation_angles: Vec<f64> = geom.rotations();
1115
1116 let mirror_candidates = mirror_candidates(geom);
1117
1118 for instance in 0..geom.quantity() {
1119 if self.cancelled.load(Ordering::Relaxed) {
1120 result.computation_time_ms = start.elapsed_ms();
1121 callback(
1122 ProgressInfo::new()
1123 .with_phase("Cancelled")
1124 .with_items(placed_count, total_pieces)
1125 .with_elapsed(result.computation_time_ms)
1126 .finished(),
1127 );
1128 return Ok(result);
1129 }
1130
1131 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
1133 {
1134 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
1135 result.utilization = total_placed_area / boundary.measure();
1136 result.computation_time_ms = start.elapsed_ms();
1137 result.placements = placements;
1138 callback(
1139 ProgressInfo::new()
1140 .with_phase("Time Limit Reached")
1141 .with_items(placed_count, total_pieces)
1142 .with_elapsed(result.computation_time_ms)
1143 .finished(),
1144 );
1145 return Ok(result);
1146 }
1147
1148 let mut best_placement: Option<(f64, f64, f64, bool)> = None;
1149
1150 for &rotation in &rotation_angles {
1151 for &mirror in mirror_candidates {
1152 let ifp = match compute_ifp_with_margin_and_mirror(
1153 &boundary_polygon,
1154 geom,
1155 rotation,
1156 0.0,
1158 mirror,
1159 ) {
1160 Ok(ifp) => ifp,
1161 Err(_) => continue,
1162 };
1163
1164 if ifp.is_empty() {
1165 continue;
1166 }
1167
1168 let mut nfps: Vec<Nfp> = Vec::new();
1169 for placed in &placed_geometries {
1170 let cache_key = (
1172 placed.geometry.id().as_str(),
1173 geom.id().as_str(),
1174 rotation - placed.rotation,
1175 placed.mirrored,
1176 mirror,
1177 );
1178
1179 let nfp_at_origin =
1182 match self.nfp_cache.get_or_compute_mirrored(cache_key, || {
1183 let placed_at_origin = placed.geometry.clone();
1184 compute_nfp_mirrored(
1185 &placed_at_origin,
1186 geom,
1187 rotation - placed.rotation,
1188 placed.mirrored,
1189 mirror,
1190 )
1191 .map(|nfp| offset_nfp(&nfp, spacing))
1195 }) {
1196 Ok(nfp) => nfp,
1197 Err(_) => continue,
1198 };
1199
1200 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
1202 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
1203 nfps.push(translated_nfp);
1204 }
1205
1206 nfps.extend(hole_nfps(boundary, geom, rotation, mirror, margin));
1209 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
1210
1211 if let Some((x, y)) = find_bottom_left_placement(
1212 &ifp,
1213 &nfp_refs,
1214 sample_step,
1215 PackingAxis::of(boundary),
1216 ) {
1217 let is_better =
1218 match best_placement {
1219 None => true,
1220 Some((best_x, best_y, _, _)) => PackingAxis::of(boundary)
1221 .precedes((x, y), (best_x, best_y), 1e-6),
1222 };
1223 if is_better {
1224 best_placement = Some((x, y, rotation, mirror));
1225 }
1226 }
1227 }
1228 }
1229
1230 if let Some((x, y, rotation, mirror)) = best_placement {
1231 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
1234 let boundary_aabb = boundary.aabb();
1235
1236 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
1237 x,
1238 y,
1239 geom_aabb,
1240 boundary_aabb,
1241 margin,
1242 ) {
1243 let placement = Placement::new_2d(
1244 geom.id().clone(),
1245 instance,
1246 clamped_x,
1247 clamped_y,
1248 rotation,
1249 )
1250 .with_mirrored(mirror);
1251 placements.push(placement);
1252 placed_geometries.push(
1253 PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation)
1254 .with_mirrored(mirror),
1255 );
1256 total_placed_area += geom.measure();
1257 placed_count += 1;
1258
1259 callback(
1261 ProgressInfo::new()
1262 .with_phase("NFP Placement")
1263 .with_items(placed_count, total_pieces)
1264 .with_utilization(total_placed_area / boundary.measure())
1265 .with_elapsed(start.elapsed_ms()),
1266 );
1267 } else {
1268 result.unplaced.push(geom.id().clone());
1269 }
1270 } else {
1271 result.unplaced.push(geom.id().clone());
1272 }
1273 }
1274 }
1275
1276 result.placements = placements;
1277 result.boundaries_used = 1;
1278 result.utilization = total_placed_area / boundary.measure();
1279 result.computation_time_ms = start.elapsed_ms();
1280
1281 callback(
1283 ProgressInfo::new()
1284 .with_phase("Complete")
1285 .with_items(placed_count, total_pieces)
1286 .with_utilization(result.utilization)
1287 .with_elapsed(result.computation_time_ms)
1288 .finished(),
1289 );
1290
1291 Ok(result)
1292 }
1293
1294 fn validate_geometries(&self, geometries: &[Geometry2D]) -> Result<()> {
1308 use u_nesting_core::geometry::Geometry;
1309 if geometries.is_empty() {
1310 return Err(Error::empty_input("geometries"));
1311 }
1312 self.config.validate()?;
1313 for geom in geometries {
1314 geom.validate()?;
1315 }
1316 u_nesting_core::geometry::ensure_unique_ids(geometries)
1317 }
1318
1319 pub fn solve_multi_strip(
1321 &self,
1322 geometries: &[Geometry2D],
1323 boundary: &Boundary2D,
1324 ) -> Result<SolveResult<f64>> {
1325 boundary.validate()?;
1326 self.validate_geometries(geometries)?;
1327 self.cancelled.store(false, Ordering::Relaxed);
1328
1329 let (b_min, b_max) = boundary.aabb();
1330 let strip_width = b_max[0] - b_min[0];
1331
1332 let mut final_result = SolveResult::new();
1333 let mut remaining_geometries: Vec<Geometry2D> = geometries.to_vec();
1334 let mut strip_index = 0;
1335 let max_strips = 100; let mut placed_total: std::collections::HashMap<String, usize> =
1340 std::collections::HashMap::new();
1341
1342 while !remaining_geometries.is_empty() && strip_index < max_strips {
1343 if self.cancelled.load(Ordering::Relaxed) {
1344 break;
1345 }
1346
1347 let strip_result = self.run_strategy(&remaining_geometries, boundary)?;
1349
1350 let strip_result = validate_and_filter_placements(
1352 strip_result,
1353 &remaining_geometries,
1354 boundary,
1355 self.config.margin,
1356 );
1357
1358 if strip_result.placements.is_empty() {
1359 break;
1362 }
1363
1364 let mut strip_placed: std::collections::HashMap<String, usize> =
1367 std::collections::HashMap::new();
1368
1369 for mut placement in strip_result.placements {
1371 let gid = placement.geometry_id.clone();
1372 let prior = placed_total.get(&gid).copied().unwrap_or(0);
1375 let in_strip = strip_placed.get(&gid).copied().unwrap_or(0);
1376 placement.instance = prior + in_strip;
1377 if !placement.position.is_empty() {
1379 placement.position[0] += strip_index as f64 * strip_width;
1380 }
1381 placement.boundary_index = strip_index;
1382 *strip_placed.entry(gid).or_insert(0) += 1;
1383 final_result.placements.push(placement);
1384 }
1385
1386 for (gid, cnt) in &strip_placed {
1390 *placed_total.entry(gid.clone()).or_insert(0) += cnt;
1391 }
1392 remaining_geometries = remaining_geometries
1393 .into_iter()
1394 .filter_map(|g| {
1395 let placed_here = strip_placed.get(g.id()).copied().unwrap_or(0);
1396 let new_quantity = g.quantity().saturating_sub(placed_here);
1397 if new_quantity == 0 {
1398 None
1399 } else {
1400 Some(g.with_quantity(new_quantity))
1401 }
1402 })
1403 .collect();
1404
1405 strip_index += 1;
1406 }
1407
1408 for g in &remaining_geometries {
1411 final_result.unplaced.push(g.id().clone());
1412 }
1413
1414 final_result.boundaries_used = strip_index;
1415 final_result.deduplicate_unplaced();
1416 final_result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1418
1419 let (b_min, b_max) = boundary.aabb();
1421 let strip_height = b_max[1] - b_min[1]; let mut strip_stats_map: std::collections::HashMap<usize, (f64, f64, usize)> =
1425 std::collections::HashMap::new(); for placement in &final_result.placements {
1428 let strip_idx = placement.boundary_index;
1429 if let Some(geom) = geometries.iter().find(|g| g.id() == &placement.geometry_id) {
1431 use u_nesting_core::geometry::Geometry;
1432 let piece_area = geom.measure();
1433 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1434 let (_g_min, g_max) = geom.aabb_at_rotation(rotation);
1435 let local_x = placement.position[0] - (strip_idx as f64 * strip_width);
1438 let right_edge = local_x + g_max[0];
1439
1440 let entry = strip_stats_map.entry(strip_idx).or_insert((0.0, 0.0, 0));
1441 entry.0 = entry.0.max(right_edge); entry.1 += piece_area; entry.2 += 1; }
1445 }
1446
1447 use u_nesting_core::result::StripStats;
1449 let mut strip_stats: Vec<StripStats> = strip_stats_map
1450 .into_iter()
1451 .map(|(idx, (used_length, piece_area, count))| StripStats {
1452 strip_index: idx,
1453 used_length,
1454 piece_area,
1455 piece_count: count,
1456 strip_width, strip_height, })
1459 .collect();
1460 strip_stats.sort_by_key(|s| s.strip_index);
1461
1462 let total_piece_area: f64 = strip_stats.iter().map(|s| s.piece_area).sum();
1465 let total_material_used: f64 = strip_stats
1466 .iter()
1467 .map(|s| s.strip_height * s.used_length)
1468 .sum();
1469
1470 final_result.strip_stats = strip_stats;
1471 final_result.total_piece_area = total_piece_area;
1472 final_result.total_material_used = total_material_used;
1473
1474 if total_material_used > 0.0 {
1475 final_result.utilization = total_piece_area / total_material_used;
1476 }
1477
1478 Ok(final_result)
1479 }
1480}
1481
1482impl Solver for Nester2D {
1483 type Geometry = Geometry2D;
1484 type Boundary = Boundary2D;
1485 type Scalar = f64;
1486
1487 fn solve(
1488 &self,
1489 geometries: &[Self::Geometry],
1490 boundary: &Self::Boundary,
1491 ) -> Result<SolveResult<f64>> {
1492 boundary.validate()?;
1493 self.validate_geometries(geometries)?;
1494
1495 self.cancelled.store(false, Ordering::Relaxed);
1497
1498 let initial_result = self.run_strategy(geometries, boundary)?;
1499
1500 let mut result = validate_and_filter_placements(
1502 initial_result,
1503 geometries,
1504 boundary,
1505 self.config.margin,
1506 );
1507
1508 result.deduplicate_unplaced();
1510 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1513 Ok(result)
1514 }
1515
1516 fn solve_with_progress(
1517 &self,
1518 geometries: &[Self::Geometry],
1519 boundary: &Self::Boundary,
1520 callback: ProgressCallback,
1521 ) -> Result<SolveResult<f64>> {
1522 boundary.validate()?;
1523 self.validate_geometries(geometries)?;
1524
1525 self.cancelled.store(false, Ordering::Relaxed);
1527
1528 let initial_result = match self.config.strategy {
1529 Strategy::BottomLeftFill => {
1530 self.bottom_left_fill_with_progress(geometries, boundary, &callback)?
1531 }
1532 Strategy::NfpGuided => {
1533 self.nfp_guided_blf_with_progress(geometries, boundary, &callback)?
1534 }
1535 Strategy::GeneticAlgorithm => {
1536 let (greedy, remaining) =
1539 self.greedy_start(geometries, boundary, self.config.time_limit_ms);
1540 let mut ga_config = self.ga_config();
1541
1542 if self.config.time_limit_ms > 0 {
1544 ga_config =
1545 ga_config.with_time_limit(std::time::Duration::from_millis(remaining));
1546 }
1547
1548 let ga_result = run_ga_nesting_with_progress(
1549 geometries,
1550 boundary,
1551 &self.config,
1552 ga_config,
1553 self.cancelled.clone(),
1554 callback,
1555 greedy.as_ref().map(|g| g.placements.as_slice()),
1556 )?;
1557 self.not_worse_than_baselines(ga_result, geometries, boundary, greedy)
1562 }
1563 _ => self.run_strategy(geometries, boundary)?,
1567 };
1568
1569 let mut result = validate_and_filter_placements(
1571 initial_result,
1572 geometries,
1573 boundary,
1574 self.config.margin,
1575 );
1576
1577 result.deduplicate_unplaced();
1579 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1582 Ok(result)
1583 }
1584
1585 fn cancel(&self) {
1586 self.cancelled.store(true, Ordering::Relaxed);
1587 }
1588}
1589
1590#[cfg(test)]
1591mod tests {
1592 #[cfg(feature = "milp")]
1593 mod milp_budget {
1594 use super::super::{milp_grid_step, milp_time_limit};
1595 use crate::{Boundary2D, Geometry2D};
1596
1597 #[test]
1600 fn an_unlimited_request_does_not_become_a_minute() {
1601 assert_eq!(milp_time_limit(0), u64::MAX / 2);
1602 }
1603
1604 #[test]
1607 fn a_short_request_is_kept_short() {
1608 assert_eq!(milp_time_limit(1_000), 1_000);
1609 assert_eq!(milp_time_limit(250), 250);
1610 }
1611
1612 #[test]
1616 fn the_grid_step_follows_the_smallest_piece() {
1617 let boundary = Boundary2D::rectangle(400.0, 100.0);
1618 let pieces = [
1619 Geometry2D::rectangle("wide", 80.0, 40.0),
1620 Geometry2D::rectangle("small", 20.0, 60.0),
1621 ];
1622 assert_eq!(milp_grid_step(&pieces, &boundary), 5.0);
1624 }
1625
1626 #[test]
1629 fn a_huge_sheet_keeps_the_step_from_getting_too_fine() {
1630 let boundary = Boundary2D::rectangle(100_000.0, 100.0);
1631 let pieces = [Geometry2D::rectangle("small", 4.0, 4.0)];
1632 assert_eq!(milp_grid_step(&pieces, &boundary), 100_000.0 / 256.0);
1633 }
1634
1635 #[test]
1637 fn the_step_never_goes_below_a_unit() {
1638 let boundary = Boundary2D::rectangle(10.0, 10.0);
1639 let pieces = [Geometry2D::rectangle("tiny", 0.5, 0.5)];
1640 assert_eq!(milp_grid_step(&pieces, &boundary), 1.0);
1641 }
1642 }
1643
1644 use super::*;
1645 use crate::placement_utils::polygon_centroid;
1646
1647 #[test]
1651 fn a_geometry_id_given_twice_is_refused_by_every_entry_point() {
1652 let geometries = [
1653 Geometry2D::rectangle("P", 10.0, 10.0),
1654 Geometry2D::rectangle("Q", 10.0, 10.0),
1655 Geometry2D::rectangle("P", 30.0, 5.0),
1656 ];
1657 let boundary = Boundary2D::rectangle(100.0, 100.0);
1658 let nester = Nester2D::default_config();
1659 let errors = [
1660 nester.solve(&geometries, &boundary).expect_err("solve"),
1661 nester
1662 .solve_multi_strip(&geometries, &boundary)
1663 .expect_err("solve_multi_strip"),
1664 nester
1665 .solve_with_progress(&geometries, &boundary, Box::new(|_| {}))
1666 .expect_err("solve_with_progress"),
1667 ];
1668 for err in errors {
1669 let msg = err.to_string();
1670 assert!(msg.contains("'P'"), "{msg}");
1671 assert!(msg.contains("positions 0 and 2"), "{msg}");
1672 }
1673 }
1674
1675 #[test]
1678 fn an_empty_list_of_geometries_is_refused_by_every_entry_point() {
1679 let boundary = Boundary2D::rectangle(100.0, 100.0);
1680 let nester = Nester2D::default_config();
1681 let errors = [
1682 nester.solve(&[], &boundary).expect_err("solve"),
1683 nester
1684 .solve_multi_strip(&[], &boundary)
1685 .expect_err("solve_multi_strip"),
1686 nester
1687 .solve_with_progress(&[], &boundary, Box::new(|_| {}))
1688 .expect_err("solve_with_progress"),
1689 ];
1690 for err in errors {
1691 assert_eq!(
1692 err,
1693 Error::EmptyInput {
1694 parameter: "geometries".into()
1695 }
1696 );
1697 }
1698 }
1699
1700 #[test]
1701 fn the_seed_reaches_the_searches_that_build_their_own_run() {
1702 let seeded = Nester2D::new(Config::default().with_seed(7));
1706 assert_eq!(seeded.gdrr_config(1000).seed, Some(7));
1707 assert_eq!(seeded.alns_config(1000).seed, Some(7));
1708 let unseeded = Nester2D::default_config();
1709 assert_eq!(unseeded.gdrr_config(1000).seed, None);
1710 assert_eq!(unseeded.alns_config(1000).seed, None);
1711 }
1712
1713 #[test]
1718 fn a_set_budget_is_run_and_an_unset_one_is_the_strategy_s() {
1719 assert_eq!((SEARCH_POPULATION, SEARCH_GENERATIONS), (30, 50));
1720 let unset = Nester2D::default_config();
1721 assert_eq!(unset.ga_config().population_size, SEARCH_POPULATION);
1722 assert_eq!(unset.ga_config().max_generations, SEARCH_GENERATIONS);
1723 assert_eq!(unset.brkga_config().population_size, SEARCH_POPULATION);
1724 assert_eq!(unset.brkga_config().max_generations, SEARCH_GENERATIONS);
1725
1726 let set = Nester2D::new(
1727 Config::default()
1728 .with_population_size(100)
1729 .with_max_generations(200),
1730 );
1731 assert_eq!(set.ga_config().population_size, 100);
1732 assert_eq!(set.ga_config().max_generations, 200);
1733 assert_eq!(set.brkga_config().population_size, 100);
1734 assert_eq!(set.brkga_config().max_generations, 200);
1735
1736 let small = Nester2D::new(Config::default().with_population_size(3));
1737 assert_eq!(small.brkga_config().population_size, 3, "was raised to 4");
1738 assert!(small.brkga_config().validate().is_ok());
1739 }
1740
1741 #[test]
1744 fn the_generation_budget_reaches_the_running_search() {
1745 use std::sync::Mutex;
1746 let reports = |config: Config| {
1747 let seen = Arc::new(Mutex::new(Vec::new()));
1748 let sink = seen.clone();
1749 Nester2D::new(
1750 config
1751 .with_strategy(Strategy::GeneticAlgorithm)
1752 .with_seed(7),
1753 )
1754 .solve_with_progress(
1755 &[Geometry2D::rectangle("R", 10.0, 10.0).with_quantity(4)],
1756 &Boundary2D::rectangle(50.0, 50.0),
1757 Box::new(move |info| {
1758 sink.lock()
1759 .expect("no report panicked")
1760 .push((info.iteration, info.total_iterations))
1761 }),
1762 )
1763 .expect("solve");
1764 let seen = seen.lock().expect("no report panicked").clone();
1765 seen
1766 };
1767 let set = reports(Config::default().with_max_generations(80));
1768 assert!(!set.is_empty(), "the search reports its progress");
1769 assert!(
1770 set.iter().all(|&(at, total)| total == 80 && at <= 80),
1771 "{set:?}"
1772 );
1773 let unset = reports(Config::default());
1774 assert!(!unset.is_empty());
1775 assert!(unset.iter().all(|&(_, total)| total == 50), "{unset:?}");
1776 }
1777
1778 #[test]
1779 fn test_simple_nesting() {
1780 let geometries = vec![
1781 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1782 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1783 ];
1784
1785 let boundary = Boundary2D::rectangle(100.0, 50.0);
1786 let nester = Nester2D::default_config();
1787
1788 let result = nester.solve(&geometries, &boundary).unwrap();
1789
1790 assert!(result.utilization > 0.0);
1791 assert!(result.placements.len() <= 5); }
1793
1794 #[test]
1795 fn test_placement_within_bounds() {
1796 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1797
1798 let boundary = Boundary2D::rectangle(50.0, 50.0);
1799 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1800 let nester = Nester2D::new(config);
1801
1802 let result = nester.solve(&geometries, &boundary).unwrap();
1803
1804 assert_eq!(result.placements.len(), 4);
1806 assert!(result.unplaced.is_empty());
1807
1808 for p in &result.placements {
1810 assert!(p.position[0] >= 5.0);
1811 assert!(p.position[1] >= 5.0);
1812 }
1813 }
1814
1815 #[test]
1816 fn test_nfp_guided_basic() {
1817 let geometries = vec![
1818 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1819 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1820 ];
1821
1822 let boundary = Boundary2D::rectangle(100.0, 50.0);
1823 let config = Config::default().with_strategy(Strategy::NfpGuided);
1824 let nester = Nester2D::new(config);
1825
1826 let result = nester.solve(&geometries, &boundary).unwrap();
1827
1828 assert!(result.utilization > 0.0);
1829 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1831 }
1832
1833 #[test]
1834 fn test_nfp_guided_with_spacing() {
1835 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1836
1837 let boundary = Boundary2D::rectangle(50.0, 50.0);
1838 let config = Config::default()
1839 .with_strategy(Strategy::NfpGuided)
1840 .with_margin(2.0)
1841 .with_spacing(3.0);
1842 let nester = Nester2D::new(config);
1843
1844 let result = nester.solve(&geometries, &boundary).unwrap();
1845
1846 assert_eq!(result.placements.len(), 4);
1848 assert!(result.unplaced.is_empty());
1849
1850 assert!(result.utilization > 0.0);
1852 }
1853
1854 #[test]
1855 fn test_nfp_guided_no_overlap() {
1856 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1857
1858 let boundary = Boundary2D::rectangle(100.0, 100.0);
1859 let config = Config::default().with_strategy(Strategy::NfpGuided);
1860 let nester = Nester2D::new(config);
1861
1862 let result = nester.solve(&geometries, &boundary).unwrap();
1863
1864 assert_eq!(result.placements.len(), 3);
1865
1866 for i in 0..result.placements.len() {
1868 for j in (i + 1)..result.placements.len() {
1869 let p1 = &result.placements[i];
1870 let p2 = &result.placements[j];
1871
1872 let r1_min_x = p1.position[0];
1874 let r1_max_x = p1.position[0] + 20.0;
1875 let r1_min_y = p1.position[1];
1876 let r1_max_y = p1.position[1] + 20.0;
1877
1878 let r2_min_x = p2.position[0];
1879 let r2_max_x = p2.position[0] + 20.0;
1880 let r2_min_y = p2.position[1];
1881 let r2_max_y = p2.position[1] + 20.0;
1882
1883 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1885 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1886
1887 assert!(
1888 !(overlaps_x && overlaps_y),
1889 "Placements {} and {} overlap",
1890 i,
1891 j
1892 );
1893 }
1894 }
1895 }
1896
1897 fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
1902 for i in 0..a.len() {
1903 let a1 = a[i];
1904 let a2 = a[(i + 1) % a.len()];
1905 for j in 0..b.len() {
1906 let b1 = b[j];
1907 let b2 = b[(j + 1) % b.len()];
1908 if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
1909 return true;
1910 }
1911 }
1912 }
1913 false
1914 }
1915
1916 #[test]
1917 fn test_mirror_candidates_reflects_allow_flip() {
1918 let plain = Geometry2D::rectangle("R", 10.0, 10.0);
1919 assert_eq!(mirror_candidates(&plain), &[false]);
1920
1921 let flippable = Geometry2D::rectangle("R", 10.0, 10.0).with_flip(true);
1922 assert_eq!(mirror_candidates(&flippable), &[false, true]);
1923 }
1924
1925 #[test]
1945 fn test_mirror_aware_placement_avoids_overlap() {
1946 let spacing = 1.0;
1954 let geom = Geometry2D::l_shape("L", 30.0, 20.0, 20.0, 10.0);
1955 let boundary_polygon = vec![(0.0, 0.0), (65.0, 0.0), (65.0, 45.0), (0.0, 45.0)];
1956 let cache = NfpCache::new();
1957
1958 let ifp1 =
1960 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, false).unwrap();
1961 let (x1, y1) = find_bottom_left_placement(&ifp1, &[], 1.0, PackingAxis::X)
1962 .expect("first piece must fit");
1963 let placed1 = PlacedGeometry::new(geom.clone(), (x1, y1), 0.0).with_mirrored(false);
1964
1965 let ifp2 =
1967 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, true).unwrap();
1968 let nfp_at_origin = cache
1969 .get_or_compute_mirrored(("L", "L", 0.0, false, true), || {
1970 compute_nfp_mirrored(&placed1.geometry, &geom, 0.0, false, true)
1971 })
1972 .unwrap();
1973 let translated_nfp = translate_nfp(&nfp_at_origin, placed1.position);
1974 let expanded_nfp = offset_nfp(&translated_nfp, spacing);
1975 let (x2, y2) = find_bottom_left_placement(&ifp2, &[&expanded_nfp], 1.0, PackingAxis::X)
1976 .expect("mirrored second piece must fit avoiding the first");
1977 let placed2 = PlacedGeometry::new(geom.clone(), (x2, y2), 0.0).with_mirrored(true);
1978
1979 assert_ne!(
1980 (x1, y1),
1981 (x2, y2),
1982 "mirrored placement must actually avoid the first piece's position"
1983 );
1984 assert!(
1985 !polygons_overlap(
1986 &placed1.translated_exterior(),
1987 &placed2.translated_exterior()
1988 ),
1989 "unmirrored piece at {:?} and mirrored piece at {:?} must not overlap",
1990 (x1, y1),
1991 (x2, y2)
1992 );
1993 }
1994
1995 #[test]
1996 fn test_nfp_guided_utilization() {
1997 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
1999
2000 let boundary = Boundary2D::rectangle(100.0, 50.0);
2001 let config = Config::default().with_strategy(Strategy::NfpGuided);
2002 let nester = Nester2D::new(config);
2003
2004 let result = nester.solve(&geometries, &boundary).unwrap();
2005
2006 assert_eq!(result.placements.len(), 4);
2008
2009 assert!(result.utilization > 0.45);
2011 }
2012
2013 #[test]
2014 fn test_polygon_centroid() {
2015 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
2017 let (cx, cy) = polygon_centroid(&square);
2018 assert!((cx - 5.0).abs() < 0.01);
2019 assert!((cy - 5.0).abs() < 0.01);
2020
2021 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
2022 let (cx, cy) = polygon_centroid(&triangle);
2023 assert!((cx - 3.0).abs() < 0.01);
2024 assert!((cy - 2.0).abs() < 0.01);
2025 }
2026
2027 #[test]
2028 fn test_ga_strategy_basic() {
2029 let geometries = vec![
2030 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2031 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2032 ];
2033
2034 let boundary = Boundary2D::rectangle(100.0, 50.0);
2035 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
2036 let nester = Nester2D::new(config);
2037
2038 let result = nester.solve(&geometries, &boundary).unwrap();
2039
2040 assert!(result.utilization > 0.0);
2041 assert!(!result.placements.is_empty());
2042 assert!(result.generations.is_some());
2044 assert!(result.best_fitness.is_some());
2045 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
2046 }
2047
2048 #[test]
2049 fn test_ga_strategy_all_placed() {
2050 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2052
2053 let boundary = Boundary2D::rectangle(100.0, 100.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_eq!(result.placements.len(), 4);
2061 assert!(result.unplaced.is_empty());
2062 }
2063
2064 #[test]
2065 fn test_brkga_strategy_basic() {
2066 let geometries = vec![
2067 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2068 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2069 ];
2070
2071 let boundary = Boundary2D::rectangle(100.0, 50.0);
2072 let config = Config::default().with_strategy(Strategy::Brkga);
2073 let nester = Nester2D::new(config);
2074
2075 let result = nester.solve(&geometries, &boundary).unwrap();
2076
2077 assert!(result.utilization > 0.0);
2078 assert!(!result.placements.is_empty());
2079 assert!(result.generations.is_some());
2081 assert!(result.best_fitness.is_some());
2082 assert!(result.strategy == Some("BRKGA".to_string()));
2083 }
2084
2085 #[test]
2086 fn test_brkga_strategy_all_placed() {
2087 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2089
2090 let boundary = Boundary2D::rectangle(100.0, 100.0);
2091 let config = Config::default()
2093 .with_strategy(Strategy::Brkga)
2094 .with_time_limit(30000); let nester = Nester2D::new(config);
2096
2097 let result = nester.solve(&geometries, &boundary).unwrap();
2098
2099 assert!(
2102 result.placements.len() >= 3,
2103 "Expected at least 3 placements, got {}",
2104 result.placements.len()
2105 );
2106 }
2107
2108 #[test]
2109 fn test_gdrr_strategy_basic() {
2110 let geometries = vec![
2111 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2112 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2113 ];
2114
2115 let boundary = Boundary2D::rectangle(100.0, 50.0);
2116 let config = Config::default().with_strategy(Strategy::Gdrr);
2117 let nester = Nester2D::new(config);
2118
2119 let result = nester.solve(&geometries, &boundary).unwrap();
2120
2121 assert!(result.utilization > 0.0);
2122 assert!(!result.placements.is_empty());
2123 assert!(result.iterations.is_some());
2125 assert!(result.best_fitness.is_some());
2126 assert!(result.strategy == Some("GDRR".to_string()));
2127 }
2128
2129 #[test]
2130 fn test_gdrr_strategy_all_placed() {
2131 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2133
2134 let boundary = Boundary2D::rectangle(100.0, 100.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_eq!(result.placements.len(), 4);
2142 assert!(result.unplaced.is_empty());
2143 }
2144
2145 #[test]
2146 fn test_alns_strategy_basic() {
2147 let geometries = vec![
2148 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2149 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2150 ];
2151
2152 let boundary = Boundary2D::rectangle(100.0, 50.0);
2153 let config = Config::default().with_strategy(Strategy::Alns);
2154 let nester = Nester2D::new(config);
2155
2156 let result = nester.solve(&geometries, &boundary).unwrap();
2157
2158 assert!(result.utilization > 0.0);
2159 assert!(!result.placements.is_empty());
2160 assert!(result.iterations.is_some());
2162 assert!(result.best_fitness.is_some());
2163 assert!(result.strategy == Some("ALNS".to_string()));
2164 }
2165
2166 #[test]
2167 fn test_alns_strategy_all_placed() {
2168 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2170
2171 let boundary = Boundary2D::rectangle(100.0, 100.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_eq!(result.placements.len(), 4);
2179 assert!(result.unplaced.is_empty());
2180 }
2181
2182 #[test]
2183 fn test_blf_rotation_optimization() {
2184 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
2187 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
2189
2190 let boundary = Boundary2D::rectangle(35.0, 95.0);
2193 let nester = Nester2D::default_config();
2194
2195 let result = nester.solve(&geometries, &boundary).unwrap();
2196
2197 assert_eq!(
2199 result.placements.len(),
2200 3,
2201 "All pieces should be placed with rotation optimization"
2202 );
2203 assert!(result.unplaced.is_empty());
2204 }
2205
2206 #[test]
2207 fn test_blf_selects_best_rotation() {
2208 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
2210 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
2212
2213 let boundary = Boundary2D::rectangle(45.0, 50.0);
2217 let nester = Nester2D::default_config();
2218
2219 let result = nester.solve(&geometries, &boundary).unwrap();
2220
2221 assert_eq!(result.placements.len(), 2);
2222 assert!(result.unplaced.is_empty());
2223 }
2224
2225 #[test]
2226 fn test_progress_callback_blf() {
2227 use std::sync::atomic::{AtomicUsize, Ordering};
2228 use std::sync::Arc;
2229
2230 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2231 let boundary = Boundary2D::rectangle(50.0, 50.0);
2232 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2233 let nester = Nester2D::new(config);
2234
2235 let callback_count = Arc::new(AtomicUsize::new(0));
2236 let callback_count_clone = callback_count.clone();
2237 let last_items_placed = Arc::new(AtomicUsize::new(0));
2238 let last_items_placed_clone = last_items_placed.clone();
2239
2240 let callback: ProgressCallback = Box::new(move |info| {
2241 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2242 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
2243 });
2244
2245 let result = nester
2246 .solve_with_progress(&geometries, &boundary, callback)
2247 .unwrap();
2248
2249 let count = callback_count.load(Ordering::Relaxed);
2251 assert!(
2252 count >= 5,
2253 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
2254 count
2255 );
2256
2257 let final_placed = last_items_placed.load(Ordering::Relaxed);
2259 assert_eq!(final_placed, 4, "Should report 4 items placed");
2260
2261 assert_eq!(result.placements.len(), 4);
2263 }
2264
2265 #[test]
2266 fn test_progress_callback_nfp() {
2267 use std::sync::atomic::{AtomicUsize, Ordering};
2268 use std::sync::Arc;
2269
2270 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
2271 let boundary = Boundary2D::rectangle(50.0, 50.0);
2272 let config = Config::default().with_strategy(Strategy::NfpGuided);
2273 let nester = Nester2D::new(config);
2274
2275 let callback_count = Arc::new(AtomicUsize::new(0));
2276 let callback_count_clone = callback_count.clone();
2277
2278 let callback: ProgressCallback = Box::new(move |info| {
2279 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2280 assert!(info.items_placed <= info.total_items);
2281 });
2282
2283 let result = nester
2284 .solve_with_progress(&geometries, &boundary, callback)
2285 .unwrap();
2286
2287 let count = callback_count.load(Ordering::Relaxed);
2289 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
2290
2291 assert_eq!(result.placements.len(), 2);
2293 }
2294
2295 #[test]
2296 fn test_time_limit_honored() {
2297 let geometries: Vec<Geometry2D> = (0..100)
2299 .map(|i| Geometry2D::rectangle(format!("R{}", i), 5.0, 5.0))
2300 .collect();
2301 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
2302
2303 let config = Config::default()
2305 .with_strategy(Strategy::BottomLeftFill)
2306 .with_time_limit(1);
2307 let nester = Nester2D::new(config);
2308
2309 let result = nester.solve(&geometries, &boundary).unwrap();
2310
2311 assert!(
2314 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
2316 result.computation_time_ms
2317 );
2318 }
2319
2320 #[test]
2321 fn test_time_limit_zero_unlimited() {
2322 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2324 let boundary = Boundary2D::rectangle(50.0, 50.0);
2325
2326 let config = Config::default()
2327 .with_strategy(Strategy::BottomLeftFill)
2328 .with_time_limit(0); let nester = Nester2D::new(config);
2330
2331 let result = nester.solve(&geometries, &boundary).unwrap();
2332
2333 assert_eq!(result.placements.len(), 4);
2335 }
2336
2337 #[test]
2338 fn test_blf_bounds_clamping() {
2339 let gear_like = Geometry2D::new("gear")
2343 .with_polygon(vec![
2344 (50.0, 5.0), (65.0, 15.0),
2346 (77.0, 18.0),
2347 (80.0, 32.0),
2348 (95.0, 50.0), (80.0, 68.0),
2350 (77.0, 82.0),
2351 (65.0, 85.0),
2352 (50.0, 95.0), (35.0, 85.0),
2354 (23.0, 82.0),
2355 (20.0, 68.0),
2356 (5.0, 50.0), (20.0, 32.0),
2358 (23.0, 18.0),
2359 (35.0, 15.0),
2360 ])
2361 .with_quantity(1);
2362
2363 let boundary = Boundary2D::rectangle(100.0, 100.0);
2365
2366 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2367 let nester = Nester2D::new(config);
2368
2369 let result = nester
2370 .solve(std::slice::from_ref(&gear_like), &boundary)
2371 .unwrap();
2372
2373 assert_eq!(result.placements.len(), 1);
2374 let placement = &result.placements[0];
2375
2376 let origin_x = placement.position[0];
2378 let origin_y = placement.position[1];
2379
2380 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2382
2383 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2385
2386 let actual_min_x = origin_x + g_min[0];
2388 let actual_max_x = origin_x + g_max[0];
2389 let actual_min_y = origin_y + g_min[1];
2390 let actual_max_y = origin_y + g_max[1];
2391
2392 assert!(
2394 actual_min_x >= 0.0,
2395 "Left edge {} should be >= 0",
2396 actual_min_x
2397 );
2398 assert!(
2399 actual_max_x <= 100.0,
2400 "Right edge {} should be <= 100",
2401 actual_max_x
2402 );
2403 assert!(
2404 actual_min_y >= 0.0,
2405 "Bottom edge {} should be >= 0",
2406 actual_min_y
2407 );
2408 assert!(
2409 actual_max_y <= 100.0,
2410 "Top edge {} should be <= 100",
2411 actual_max_y
2412 );
2413 }
2414
2415 #[test]
2416 fn test_blf_bounds_clamping_many_pieces() {
2417 let gear_like = Geometry2D::new("gear")
2420 .with_polygon(vec![
2421 (50.0, 5.0),
2422 (65.0, 15.0),
2423 (77.0, 18.0),
2424 (80.0, 32.0),
2425 (95.0, 50.0),
2426 (80.0, 68.0),
2427 (77.0, 82.0),
2428 (65.0, 85.0),
2429 (50.0, 95.0),
2430 (35.0, 85.0),
2431 (23.0, 82.0),
2432 (20.0, 68.0),
2433 (5.0, 50.0),
2434 (20.0, 32.0),
2435 (23.0, 18.0),
2436 (35.0, 15.0),
2437 ])
2438 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
2442
2443 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2444 let nester = Nester2D::new(config);
2445
2446 let result = nester
2447 .solve(std::slice::from_ref(&gear_like), &boundary)
2448 .unwrap();
2449
2450 for (i, placement) in result.placements.iter().enumerate() {
2452 let origin_x = placement.position[0];
2453 let origin_y = placement.position[1];
2454 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2455
2456 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2457
2458 let actual_min_x = origin_x + g_min[0];
2459 let actual_max_x = origin_x + g_max[0];
2460 let actual_min_y = origin_y + g_min[1];
2461 let actual_max_y = origin_y + g_max[1];
2462
2463 assert!(
2464 actual_min_x >= -0.01,
2465 "Piece {}: Left edge {} should be >= 0",
2466 i,
2467 actual_min_x
2468 );
2469 assert!(
2470 actual_max_x <= 500.01,
2471 "Piece {}: Right edge {} should be <= 500",
2472 i,
2473 actual_max_x
2474 );
2475 assert!(
2476 actual_min_y >= -0.01,
2477 "Piece {}: Bottom edge {} should be >= 0",
2478 i,
2479 actual_min_y
2480 );
2481 assert!(
2482 actual_max_y <= 500.01,
2483 "Piece {}: Top edge {} should be <= 500",
2484 i,
2485 actual_max_y
2486 );
2487 }
2488 }
2489
2490 #[test]
2491 fn test_blf_bounds_trace() {
2492 let gear = Geometry2D::new("gear").with_polygon(vec![
2494 (50.0, 5.0),
2495 (65.0, 15.0),
2496 (77.0, 18.0),
2497 (80.0, 32.0),
2498 (95.0, 50.0),
2499 (80.0, 68.0),
2500 (77.0, 82.0),
2501 (65.0, 85.0),
2502 (50.0, 95.0),
2503 (35.0, 85.0),
2504 (23.0, 82.0),
2505 (20.0, 68.0),
2506 (5.0, 50.0),
2507 (20.0, 32.0),
2508 (23.0, 18.0),
2509 (35.0, 15.0),
2510 ]);
2511
2512 let (g_min, g_max) = gear.aabb();
2514 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
2515 assert!(
2516 (g_min[0] - 5.0).abs() < 0.01,
2517 "g_min[0] should be 5, got {}",
2518 g_min[0]
2519 );
2520 assert!(
2521 (g_max[0] - 95.0).abs() < 0.01,
2522 "g_max[0] should be 95, got {}",
2523 g_max[0]
2524 );
2525
2526 let b_max_x = 500.0;
2528 let margin = 0.0;
2529 let max_valid_x = b_max_x - margin - g_max[0];
2530 println!(
2531 "max_valid_x = {} - {} - {} = {}",
2532 b_max_x, margin, g_max[0], max_valid_x
2533 );
2534 assert!(
2535 (max_valid_x - 405.0).abs() < 0.01,
2536 "max_valid_x should be 405, got {}",
2537 max_valid_x
2538 );
2539
2540 let boundary = Boundary2D::rectangle(500.0, 500.0);
2542 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2543 let nester = Nester2D::new(config);
2544
2545 let result = nester
2546 .solve(&[gear.clone().with_quantity(1)], &boundary)
2547 .unwrap();
2548
2549 assert_eq!(result.placements.len(), 1);
2550 let p = &result.placements[0];
2551 let origin_x = p.position[0];
2552 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2553
2554 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2555 let actual_max_x = origin_x + g_max_r[0];
2556
2557 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2558 println!(
2559 "At rotation {}: g_min={:?}, g_max={:?}",
2560 rotation, g_min_r, g_max_r
2561 );
2562 println!(
2563 "Actual max x: {} + {} = {}",
2564 origin_x, g_max_r[0], actual_max_x
2565 );
2566
2567 assert!(
2568 actual_max_x <= 500.01,
2569 "Geometry exceeds boundary: max_x={} > 500",
2570 actual_max_x
2571 );
2572 }
2573
2574 #[test]
2575 fn test_blf_bounds_many_pieces_direct() {
2576 let gear = Geometry2D::new("gear")
2578 .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 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2599 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2600 let nester = Nester2D::new(config);
2601
2602 let result = nester
2603 .solve(std::slice::from_ref(&gear), &boundary)
2604 .unwrap();
2605
2606 println!("Placed {} pieces", result.placements.len());
2607
2608 for (i, p) in result.placements.iter().enumerate() {
2610 let origin_x = p.position[0];
2611 let origin_y = p.position[1];
2612 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2613
2614 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2615
2616 let actual_min_x = origin_x + g_min_r[0];
2617 let actual_max_x = origin_x + g_max_r[0];
2618 let actual_min_y = origin_y + g_min_r[1];
2619 let actual_max_y = origin_y + g_max_r[1];
2620
2621 println!(
2622 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2623 i,
2624 origin_x,
2625 origin_y,
2626 rotation,
2627 actual_min_x,
2628 actual_max_x,
2629 actual_min_y,
2630 actual_max_y
2631 );
2632
2633 assert!(
2634 actual_max_x <= 500.01,
2635 "Piece {}: Right edge {} > 500",
2636 i,
2637 actual_max_x
2638 );
2639 assert!(
2640 actual_max_y <= 500.01,
2641 "Piece {}: Top edge {} > 500",
2642 i,
2643 actual_max_y
2644 );
2645 }
2646 }
2647
2648 #[test]
2649 fn test_blf_bounds_multi_strip() {
2650 let gear = Geometry2D::new("gear")
2652 .with_polygon(vec![
2653 (50.0, 5.0),
2654 (65.0, 15.0),
2655 (77.0, 18.0),
2656 (80.0, 32.0),
2657 (95.0, 50.0),
2658 (80.0, 68.0),
2659 (77.0, 82.0),
2660 (65.0, 85.0),
2661 (50.0, 95.0),
2662 (35.0, 85.0),
2663 (23.0, 82.0),
2664 (20.0, 68.0),
2665 (5.0, 50.0),
2666 (20.0, 32.0),
2667 (23.0, 18.0),
2668 (35.0, 15.0),
2669 ])
2670 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2673 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2674 let nester = Nester2D::new(config);
2675
2676 let result = nester
2678 .solve_multi_strip(std::slice::from_ref(&gear), &boundary)
2679 .unwrap();
2680
2681 println!(
2682 "Placed {} pieces across {} strips",
2683 result.placements.len(),
2684 result.boundaries_used
2685 );
2686
2687 let strip_width = 500.0;
2689 for (i, p) in result.placements.iter().enumerate() {
2690 let origin_x = p.position[0];
2691 let origin_y = p.position[1];
2692 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2693 let strip_idx = p.boundary_index;
2694
2695 let local_x = origin_x - (strip_idx as f64 * strip_width);
2697
2698 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2699
2700 let local_max_x = local_x + g_max_r[0];
2701 let local_max_y = origin_y + g_max_r[1];
2702
2703 println!(
2704 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2705 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2706 );
2707
2708 assert!(
2709 local_max_x <= 500.01,
2710 "Piece {}: In strip {}, local right edge {:.1} > 500",
2711 i,
2712 strip_idx,
2713 local_max_x
2714 );
2715 assert!(
2716 local_max_y <= 500.01,
2717 "Piece {}: Top edge {:.1} > 500",
2718 i,
2719 local_max_y
2720 );
2721 }
2722 }
2723
2724 #[test]
2725 fn test_blf_bounds_mixed_shapes() {
2726 let shapes = vec![
2728 Geometry2D::new("shape0")
2730 .with_polygon(vec![
2731 (0.0, 0.0),
2732 (180.0, 0.0),
2733 (195.0, 15.0),
2734 (200.0, 50.0),
2735 (200.0, 150.0),
2736 (195.0, 185.0),
2737 (180.0, 200.0),
2738 (20.0, 200.0),
2739 (5.0, 185.0),
2740 (0.0, 150.0),
2741 (0.0, 50.0),
2742 (5.0, 15.0),
2743 ])
2744 .with_quantity(2),
2745 Geometry2D::new("shape1")
2747 .with_polygon(vec![
2748 (60.0, 0.0),
2749 (85.0, 7.0),
2750 (104.0, 25.0),
2751 (118.0, 50.0),
2752 (120.0, 60.0),
2753 (118.0, 70.0),
2754 (104.0, 95.0),
2755 (85.0, 113.0),
2756 (60.0, 120.0),
2757 (35.0, 113.0),
2758 (16.0, 95.0),
2759 (2.0, 70.0),
2760 (0.0, 60.0),
2761 (2.0, 50.0),
2762 (16.0, 25.0),
2763 (35.0, 7.0),
2764 ])
2765 .with_quantity(4),
2766 Geometry2D::new("shape2")
2768 .with_polygon(vec![
2769 (0.0, 0.0),
2770 (80.0, 0.0),
2771 (80.0, 20.0),
2772 (20.0, 20.0),
2773 (20.0, 80.0),
2774 (0.0, 80.0),
2775 ])
2776 .with_quantity(6),
2777 Geometry2D::new("shape3")
2779 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2780 .with_quantity(6),
2781 Geometry2D::new("shape4")
2783 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2784 .with_quantity(4),
2785 Geometry2D::new("shape5")
2787 .with_polygon(vec![
2788 (15.0, 0.0),
2789 (45.0, 0.0),
2790 (60.0, 26.0),
2791 (45.0, 52.0),
2792 (15.0, 52.0),
2793 (0.0, 26.0),
2794 ])
2795 .with_quantity(8),
2796 Geometry2D::new("shape6")
2798 .with_polygon(vec![
2799 (0.0, 0.0),
2800 (90.0, 0.0),
2801 (90.0, 12.0),
2802 (55.0, 12.0),
2803 (55.0, 60.0),
2804 (35.0, 60.0),
2805 (35.0, 12.0),
2806 (0.0, 12.0),
2807 ])
2808 .with_quantity(4),
2809 Geometry2D::new("shape7")
2811 .with_polygon(vec![
2812 (0.0, 10.0),
2813 (10.0, 0.0),
2814 (70.0, 0.0),
2815 (80.0, 10.0),
2816 (80.0, 70.0),
2817 (70.0, 80.0),
2818 (10.0, 80.0),
2819 (0.0, 70.0),
2820 ])
2821 .with_quantity(3),
2822 Geometry2D::new("shape8_gear")
2824 .with_polygon(vec![
2825 (50.0, 5.0),
2826 (65.0, 15.0),
2827 (77.0, 18.0),
2828 (80.0, 32.0),
2829 (95.0, 50.0),
2830 (80.0, 68.0),
2831 (77.0, 82.0),
2832 (65.0, 85.0),
2833 (50.0, 95.0),
2834 (35.0, 85.0),
2835 (23.0, 82.0),
2836 (20.0, 68.0),
2837 (5.0, 50.0),
2838 (20.0, 32.0),
2839 (23.0, 18.0),
2840 (35.0, 15.0),
2841 ])
2842 .with_quantity(13),
2843 ];
2844
2845 let boundary = Boundary2D::rectangle(500.0, 500.0);
2847 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2848 let nester = Nester2D::new(config);
2849
2850 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2851
2852 println!(
2853 "Placed {} pieces across {} strips",
2854 result.placements.len(),
2855 result.boundaries_used
2856 );
2857
2858 let strip_width = 500.0;
2860 let gear_aabb = shapes[8].aabb();
2861 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2862
2863 let mut violations = Vec::new();
2864 for p in &result.placements {
2865 if p.geometry_id.as_str().starts_with("shape8") {
2866 let origin_x = p.position[0];
2867 let _origin_y = p.position[1];
2868 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2869 let strip_idx = p.boundary_index;
2870 let local_x = origin_x - (strip_idx as f64 * strip_width);
2871
2872 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2873 let local_max_x = local_x + g_max_r[0];
2874
2875 println!(
2876 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2877 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2878 );
2879
2880 if local_max_x > 500.01 {
2881 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2882 }
2883 }
2884 }
2885
2886 assert!(
2887 violations.is_empty(),
2888 "Found {} Gear pieces exceeding boundary: {:?}",
2889 violations.len(),
2890 violations
2891 );
2892 }
2893
2894 #[test]
2895 fn test_blf_bounds_expanded_like_benchmark() {
2896 type ShapeDef = (Vec<(f64, f64)>, usize, Vec<f64>);
2900 let shape_defs: Vec<ShapeDef> = vec![
2901 (
2902 vec![
2903 (0.0, 0.0),
2904 (180.0, 0.0),
2905 (195.0, 15.0),
2906 (200.0, 50.0),
2907 (200.0, 150.0),
2908 (195.0, 185.0),
2909 (180.0, 200.0),
2910 (20.0, 200.0),
2911 (5.0, 185.0),
2912 (0.0, 150.0),
2913 (0.0, 50.0),
2914 (5.0, 15.0),
2915 ],
2916 2,
2917 vec![0.0, 90.0, 180.0, 270.0],
2918 ),
2919 (
2920 vec![
2921 (60.0, 0.0),
2922 (85.0, 7.0),
2923 (104.0, 25.0),
2924 (118.0, 50.0),
2925 (120.0, 60.0),
2926 (118.0, 70.0),
2927 (104.0, 95.0),
2928 (85.0, 113.0),
2929 (60.0, 120.0),
2930 (35.0, 113.0),
2931 (16.0, 95.0),
2932 (2.0, 70.0),
2933 (0.0, 60.0),
2934 (2.0, 50.0),
2935 (16.0, 25.0),
2936 (35.0, 7.0),
2937 ],
2938 4,
2939 vec![0.0, 45.0, 90.0, 135.0],
2940 ),
2941 (
2942 vec![
2943 (0.0, 0.0),
2944 (80.0, 0.0),
2945 (80.0, 20.0),
2946 (20.0, 20.0),
2947 (20.0, 80.0),
2948 (0.0, 80.0),
2949 ],
2950 6,
2951 vec![0.0, 90.0, 180.0, 270.0],
2952 ),
2953 (
2954 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
2955 6,
2956 vec![0.0, 90.0, 180.0, 270.0],
2957 ),
2958 (
2959 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
2960 4,
2961 vec![0.0, 90.0],
2962 ),
2963 (
2964 vec![
2965 (15.0, 0.0),
2966 (45.0, 0.0),
2967 (60.0, 26.0),
2968 (45.0, 52.0),
2969 (15.0, 52.0),
2970 (0.0, 26.0),
2971 ],
2972 8,
2973 vec![0.0, 60.0, 120.0],
2974 ),
2975 (
2976 vec![
2977 (0.0, 0.0),
2978 (90.0, 0.0),
2979 (90.0, 12.0),
2980 (55.0, 12.0),
2981 (55.0, 60.0),
2982 (35.0, 60.0),
2983 (35.0, 12.0),
2984 (0.0, 12.0),
2985 ],
2986 4,
2987 vec![0.0, 90.0, 180.0, 270.0],
2988 ),
2989 (
2990 vec![
2991 (0.0, 10.0),
2992 (10.0, 0.0),
2993 (70.0, 0.0),
2994 (80.0, 10.0),
2995 (80.0, 70.0),
2996 (70.0, 80.0),
2997 (10.0, 80.0),
2998 (0.0, 70.0),
2999 ],
3000 3,
3001 vec![0.0, 90.0],
3002 ),
3003 (
3005 vec![
3006 (50.0, 5.0),
3007 (65.0, 15.0),
3008 (77.0, 18.0),
3009 (80.0, 32.0),
3010 (95.0, 50.0),
3011 (80.0, 68.0),
3012 (77.0, 82.0),
3013 (65.0, 85.0),
3014 (50.0, 95.0),
3015 (35.0, 85.0),
3016 (23.0, 82.0),
3017 (20.0, 68.0),
3018 (5.0, 50.0),
3019 (20.0, 32.0),
3020 (23.0, 18.0),
3021 (35.0, 15.0),
3022 ],
3023 13,
3024 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
3025 ),
3026 ];
3027
3028 let mut geometries = Vec::new();
3030 let mut piece_id = 0;
3031 for (vertices, demand, rotations) in shape_defs.iter() {
3032 for _ in 0..*demand {
3033 let geom = Geometry2D::new(format!("piece_{}", piece_id))
3034 .with_polygon(vertices.clone())
3035 .with_rotations_deg(rotations.clone());
3036 geometries.push(geom);
3037 piece_id += 1;
3038 }
3039 }
3040
3041 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
3043 let (gear_min, gear_max) = gear_geom.aabb();
3044 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
3045
3046 let boundary = Boundary2D::rectangle(500.0, 500.0);
3047 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3048 let nester = Nester2D::new(config);
3049
3050 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3051
3052 println!(
3053 "Placed {} pieces across {} strips",
3054 result.placements.len(),
3055 result.boundaries_used
3056 );
3057
3058 let strip_width = 500.0;
3060 let mut violations = Vec::new();
3061
3062 for p in &result.placements {
3063 let id_num: usize = p
3064 .geometry_id
3065 .as_str()
3066 .strip_prefix("piece_")
3067 .and_then(|s| s.parse().ok())
3068 .unwrap_or(0);
3069
3070 if (37..=49).contains(&id_num) {
3072 let origin_x = p.position[0];
3073 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3074 let strip_idx = p.boundary_index;
3075 let local_x = origin_x - (strip_idx as f64 * strip_width);
3076
3077 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
3078 let local_max_x = local_x + g_max_r[0];
3079
3080 println!(
3081 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
3082 p.geometry_id, strip_idx, local_x, rotation, local_max_x
3083 );
3084
3085 if local_max_x > 500.01 {
3086 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
3087 }
3088 }
3089 }
3090
3091 assert!(
3092 violations.is_empty(),
3093 "Found {} Gear pieces exceeding boundary: {:?}",
3094 violations.len(),
3095 violations
3096 );
3097 }
3098
3099 fn aabbs_overlap(
3101 a_min: [f64; 2],
3102 a_max: [f64; 2],
3103 b_min: [f64; 2],
3104 b_max: [f64; 2],
3105 tolerance: f64,
3106 ) -> bool {
3107 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
3109 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
3110 x_overlap && y_overlap
3111 }
3112
3113 #[test]
3115 fn test_all_strategies_boundary_and_overlap() {
3116 use std::collections::HashMap;
3117
3118 let shapes = vec![
3120 Geometry2D::new("shape0")
3121 .with_polygon(vec![
3122 (0.0, 0.0),
3123 (180.0, 0.0),
3124 (195.0, 15.0),
3125 (200.0, 50.0),
3126 (200.0, 150.0),
3127 (195.0, 185.0),
3128 (180.0, 200.0),
3129 (20.0, 200.0),
3130 (5.0, 185.0),
3131 (0.0, 150.0),
3132 (0.0, 50.0),
3133 (5.0, 15.0),
3134 ])
3135 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3136 .with_quantity(2),
3137 Geometry2D::new("shape1_flange")
3138 .with_polygon(vec![
3139 (60.0, 0.0),
3140 (85.0, 7.0),
3141 (104.0, 25.0),
3142 (118.0, 50.0),
3143 (120.0, 60.0),
3144 (118.0, 70.0),
3145 (104.0, 95.0),
3146 (85.0, 113.0),
3147 (60.0, 120.0),
3148 (35.0, 113.0),
3149 (16.0, 95.0),
3150 (2.0, 70.0),
3151 (0.0, 60.0),
3152 (2.0, 50.0),
3153 (16.0, 25.0),
3154 (35.0, 7.0),
3155 ])
3156 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
3157 .with_quantity(4),
3158 Geometry2D::new("shape2_lbracket")
3159 .with_polygon(vec![
3160 (0.0, 0.0),
3161 (80.0, 0.0),
3162 (80.0, 20.0),
3163 (20.0, 20.0),
3164 (20.0, 80.0),
3165 (0.0, 80.0),
3166 ])
3167 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3168 .with_quantity(6),
3169 Geometry2D::new("shape3_triangle")
3170 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
3171 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3172 .with_quantity(6),
3173 Geometry2D::new("shape4_rect")
3174 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
3175 .with_rotations_deg(vec![0.0, 90.0])
3176 .with_quantity(4),
3177 Geometry2D::new("shape5_hexagon")
3178 .with_polygon(vec![
3179 (15.0, 0.0),
3180 (45.0, 0.0),
3181 (60.0, 26.0),
3182 (45.0, 52.0),
3183 (15.0, 52.0),
3184 (0.0, 26.0),
3185 ])
3186 .with_rotations_deg(vec![0.0, 60.0, 120.0])
3187 .with_quantity(8),
3188 Geometry2D::new("shape6_tstiff")
3189 .with_polygon(vec![
3190 (0.0, 0.0),
3191 (90.0, 0.0),
3192 (90.0, 12.0),
3193 (55.0, 12.0),
3194 (55.0, 60.0),
3195 (35.0, 60.0),
3196 (35.0, 12.0),
3197 (0.0, 12.0),
3198 ])
3199 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3200 .with_quantity(4),
3201 Geometry2D::new("shape7_mount")
3202 .with_polygon(vec![
3203 (0.0, 10.0),
3204 (10.0, 0.0),
3205 (70.0, 0.0),
3206 (80.0, 10.0),
3207 (80.0, 70.0),
3208 (70.0, 80.0),
3209 (10.0, 80.0),
3210 (0.0, 70.0),
3211 ])
3212 .with_rotations_deg(vec![0.0, 90.0])
3213 .with_quantity(3),
3214 Geometry2D::new("shape8_gear")
3215 .with_polygon(vec![
3216 (50.0, 5.0),
3217 (65.0, 15.0),
3218 (77.0, 18.0),
3219 (80.0, 32.0),
3220 (95.0, 50.0),
3221 (80.0, 68.0),
3222 (77.0, 82.0),
3223 (65.0, 85.0),
3224 (50.0, 95.0),
3225 (35.0, 85.0),
3226 (23.0, 82.0),
3227 (20.0, 68.0),
3228 (5.0, 50.0),
3229 (20.0, 32.0),
3230 (23.0, 18.0),
3231 (35.0, 15.0),
3232 ])
3233 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
3234 .with_quantity(13),
3235 ];
3236
3237 let geom_map: HashMap<String, &Geometry2D> =
3239 shapes.iter().map(|g| (g.id().clone(), g)).collect();
3240
3241 let boundary = Boundary2D::rectangle(500.0, 500.0);
3242 let strip_width = 500.0;
3243
3244 let strategies = vec![
3246 Strategy::BottomLeftFill,
3247 Strategy::NfpGuided,
3248 Strategy::GeneticAlgorithm,
3249 Strategy::Brkga,
3250 Strategy::SimulatedAnnealing,
3251 Strategy::Gdrr,
3252 Strategy::Alns,
3253 ];
3254
3255 for strategy in strategies {
3256 println!("\n========== Testing {:?} ==========", strategy);
3257
3258 let config = Config::default()
3259 .with_strategy(strategy)
3260 .with_time_limit(30000); let nester = Nester2D::new(config);
3262
3263 let result = match nester.solve_multi_strip(&shapes, &boundary) {
3264 Ok(r) => r,
3265 Err(e) => {
3266 println!(" Strategy {:?} failed: {}", strategy, e);
3267 continue;
3268 }
3269 };
3270
3271 println!(
3272 " Placed {} pieces across {} strips",
3273 result.placements.len(),
3274 result.boundaries_used
3275 );
3276
3277 let mut boundary_violations = Vec::new();
3279 for p in &result.placements {
3280 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
3282 let full_id = if base_id.starts_with("shape") {
3283 shapes
3285 .iter()
3286 .find(|g| p.geometry_id.starts_with(g.id()))
3287 .map(|g| g.id().as_str())
3288 } else {
3289 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
3290 };
3291
3292 let geom = match full_id.and_then(|id| geom_map.get(id)) {
3293 Some(g) => *g,
3294 None => {
3295 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
3297 Some(g) => g,
3298 None => {
3299 println!(
3300 " WARNING: Could not find geometry for {}",
3301 p.geometry_id
3302 );
3303 continue;
3304 }
3305 }
3306 }
3307 };
3308
3309 let origin_x = p.position[0];
3310 let origin_y = p.position[1];
3311 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3312 let strip_idx = p.boundary_index;
3313
3314 let local_x = origin_x - (strip_idx as f64 * strip_width);
3316
3317 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
3318
3319 let local_min_x = local_x + g_min[0];
3321 let local_max_x = local_x + g_max[0];
3322 let local_min_y = origin_y + g_min[1];
3323 let local_max_y = origin_y + g_max[1];
3324
3325 let tolerance = 0.1;
3327 if local_min_x < -tolerance
3328 || local_max_x > 500.0 + tolerance
3329 || local_min_y < -tolerance
3330 || local_max_y > 500.0 + tolerance
3331 {
3332 boundary_violations.push(format!(
3333 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
3334 p.geometry_id,
3335 strip_idx,
3336 local_min_x,
3337 local_min_y,
3338 local_max_x,
3339 local_max_y
3340 ));
3341 }
3342 }
3343
3344 if !boundary_violations.is_empty() {
3345 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
3346 for v in &boundary_violations {
3347 println!(" - {}", v);
3348 }
3349 }
3350
3351 let mut overlaps = Vec::new();
3353 let placements: Vec<_> = result.placements.iter().collect();
3354
3355 for i in 0..placements.len() {
3356 for j in (i + 1)..placements.len() {
3357 let p1 = placements[i];
3358 let p2 = placements[j];
3359
3360 if p1.boundary_index != p2.boundary_index {
3362 continue;
3363 }
3364
3365 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
3367 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
3368
3369 let (g1, g2) = match (g1, g2) {
3370 (Some(a), Some(b)) => (a, b),
3371 _ => continue,
3372 };
3373
3374 let strip_idx = p1.boundary_index;
3375 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
3376 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
3377
3378 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
3379 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
3380
3381 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
3382 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
3383
3384 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
3385 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
3386 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
3387 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
3388
3389 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
3390 overlaps.push(format!(
3391 "{} and {} in strip {}",
3392 p1.geometry_id, p2.geometry_id, strip_idx
3393 ));
3394 }
3395 }
3396 }
3397
3398 if !overlaps.is_empty() {
3399 println!(" OVERLAPS ({}):", overlaps.len());
3400 for o in overlaps.iter().take(10) {
3401 println!(" - {}", o);
3402 }
3403 if overlaps.len() > 10 {
3404 println!(" ... and {} more", overlaps.len() - 10);
3405 }
3406 }
3407
3408 assert!(
3410 boundary_violations.is_empty(),
3411 "{:?}: Found {} boundary violations",
3412 strategy,
3413 boundary_violations.len()
3414 );
3415
3416 println!(" ✓ All placements within boundary");
3417 println!(" ✓ No AABB overlaps detected");
3418 }
3419 }
3420
3421 #[test]
3429 fn test_multi_strip_distributes_all_instances() {
3430 let geometries = vec![Geometry2D::rectangle("part", 100.0, 100.0).with_quantity(20)];
3431 let boundary = Boundary2D::rectangle(300.0, 300.0);
3432 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3433 let nester = Nester2D::new(config);
3434
3435 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3436
3437 assert_eq!(
3439 result.placements.len(),
3440 20,
3441 "all 20 instances must be placed"
3442 );
3443 assert_eq!(
3444 result.boundaries_used, 3,
3445 "20 of 100x100 in 300x300 => 3 sheets"
3446 );
3447 assert!(
3448 result.unplaced.is_empty(),
3449 "nothing should be unplaced, got {:?}",
3450 result.unplaced
3451 );
3452 assert_eq!(result.total_requested, 20);
3454
3455 let mut seen = std::collections::HashSet::new();
3458 for p in &result.placements {
3459 assert!(
3460 seen.insert((p.geometry_id.clone(), p.instance)),
3461 "duplicate (id, instance) = ({}, {}) across sheets",
3462 p.geometry_id,
3463 p.instance
3464 );
3465 }
3466 let mut sheets: Vec<usize> = result.placements.iter().map(|p| p.boundary_index).collect();
3468 sheets.sort_unstable();
3469 sheets.dedup();
3470 assert_eq!(sheets, vec![0, 1, 2]);
3471 }
3472
3473 #[test]
3476 fn test_multi_strip_oversized_reported_unplaced() {
3477 let geometries = vec![
3478 Geometry2D::rectangle("ok", 50.0, 50.0).with_quantity(2),
3479 Geometry2D::rectangle("toobig", 400.0, 400.0).with_quantity(3),
3480 ];
3481 let boundary = Boundary2D::rectangle(300.0, 300.0);
3482 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3483 let nester = Nester2D::new(config);
3484
3485 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3486
3487 assert_eq!(result.total_requested, 5, "2 + 3 instances requested");
3488 assert_eq!(result.placements.len(), 2);
3490 assert!(
3491 result.unplaced.contains(&"toobig".to_string()),
3492 "oversized geometry must surface in unplaced, got {:?}",
3493 result.unplaced
3494 );
3495 }
3496}