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::{Config, ProgressCallback, ProgressInfo, Solver, Strategy};
27use u_nesting_core::{Error, Placement, Result, SolveResult};
28
29use crate::placement_utils::{hole_nfps, inset_boundary, nesting_fitness, 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 self.run_strategy_reporting(geometries, boundary, None)
188 }
189
190 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 => {
201 self.bottom_left_fill_impl(geometries, boundary, progress.as_ref())
202 }
203 Strategy::NfpGuided => {
204 self.nfp_guided_blf_impl(geometries, boundary, progress.as_ref())
205 }
206 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary, progress),
207 Strategy::Brkga => self.brkga(geometries, boundary),
208 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
209 Strategy::Gdrr => self.gdrr(geometries, boundary),
210 Strategy::Alns => self.alns(geometries, boundary),
211 #[cfg(feature = "milp")]
212 Strategy::MilpExact => self.milp_exact(geometries, boundary),
213 #[cfg(feature = "milp")]
214 Strategy::HybridExact => self.hybrid_exact(geometries, boundary),
215 Strategy::ExtremePoint => Err(Error::invalid_option(
216 "strategy",
217 "strategy ExtremePoint is 3D only; 2D nesting offers BottomLeftFill, \
218 NfpGuided, GeneticAlgorithm, Brkga, SimulatedAnnealing, Gdrr and Alns"
219 .into(),
220 )),
221 #[cfg(not(feature = "milp"))]
222 Strategy::MilpExact | Strategy::HybridExact => Err(Error::invalid_option(
223 "strategy",
224 format!(
225 "strategy {:?} needs the `milp` feature, which this build does not have",
226 self.config.strategy
227 ),
228 )),
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 pub fn default_config() -> Self {
243 Self::new(Config::default())
244 }
245
246 fn ga_config(&self) -> GaConfig {
250 let config = GaConfig::default()
251 .with_population_size(self.config.population_size.unwrap_or(SEARCH_POPULATION))
252 .with_max_generations(self.config.max_generations.unwrap_or(SEARCH_GENERATIONS))
253 .with_crossover_rate(self.config.crossover_rate)
254 .with_mutation_rate(self.config.mutation_rate)
255 .with_elite_count(self.config.elite_count);
256 match self.target_fitness() {
257 Some(target) => config.with_target_fitness(target),
258 None => config,
259 }
260 }
261
262 fn brkga_config(&self) -> BrkgaConfig {
265 let config = BrkgaConfig::default()
266 .with_population_size(self.config.population_size.unwrap_or(SEARCH_POPULATION))
267 .with_max_generations(self.config.max_generations.unwrap_or(SEARCH_GENERATIONS))
268 .with_elite_fraction(0.2)
269 .with_mutant_fraction(0.15)
270 .with_elite_bias(0.7);
271 match self.target_fitness() {
272 Some(target) => config.with_target_fitness(target),
273 None => config,
274 }
275 }
276
277 fn gdrr_config(&self, time_limit_ms: u64) -> GdrrConfig {
279 GdrrConfig {
280 seed: self.config.seed,
281 ..GdrrConfig::default()
282 .with_max_iterations(1000) .with_time_limit_ms(time_limit_ms)
284 .with_ruin_ratio(0.1, 0.3) .with_lahc_list_length(30) }
287 }
288
289 fn alns_config(&self, time_limit_ms: u64) -> AlnsConfig {
291 AlnsConfig {
292 seed: self.config.seed,
293 ..AlnsConfig::default()
294 .with_max_iterations(1000) .with_time_limit_ms(time_limit_ms)
296 .with_segment_size(50) .with_scores(33.0, 9.0, 13.0)
298 .with_reaction_factor(0.15) .with_temperature(100.0, 0.999, 0.1) }
301 }
302
303 fn bottom_left_fill(
313 &self,
314 geometries: &[Geometry2D],
315 boundary: &Boundary2D,
316 ) -> Result<SolveResult<f64>> {
317 self.bottom_left_fill_impl(geometries, boundary, None)
318 }
319
320 fn nfp_guided_blf(
325 &self,
326 geometries: &[Geometry2D],
327 boundary: &Boundary2D,
328 ) -> Result<SolveResult<f64>> {
329 self.nfp_guided_blf_impl(geometries, boundary, None)
330 }
331
332 fn nfp_guided_blf_impl(
335 &self,
336 geometries: &[Geometry2D],
337 boundary: &Boundary2D,
338 callback: Option<&ProgressCallback>,
339 ) -> Result<SolveResult<f64>> {
340 let start = Timer::now();
341 let mut result = SolveResult::new();
342 let mut placements = Vec::new();
343 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
344 let report = |info: ProgressInfo| {
345 if let Some(callback) = callback {
346 callback(info);
347 }
348 };
349 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
350 let mut placed_count = 0usize;
351 report(
352 ProgressInfo::new()
353 .with_phase("NFP Placement")
354 .with_items(0, total_pieces)
355 .with_elapsed(0),
356 );
357
358 let margin = self.config.margin;
359 let spacing = self.config.spacing;
360
361 let boundary_polygon = inset_boundary(boundary, margin);
363
364 let mut total_placed_area = 0.0;
365
366 let sample_step = self.compute_sample_step(geometries);
368
369 for geom in geometries {
370 geom.validate()?;
371
372 let rotation_angles: Vec<f64> = geom.rotations();
375
376 let mirror_candidates = mirror_candidates(geom);
377
378 for instance in 0..geom.quantity() {
379 if self.cancelled.load(Ordering::Relaxed) {
380 result.computation_time_ms = start.elapsed_ms();
381 report(
382 ProgressInfo::new()
383 .with_phase("Cancelled")
384 .with_items(placed_count, total_pieces)
385 .with_elapsed(result.computation_time_ms)
386 .finished(),
387 );
388 return Ok(result);
389 }
390
391 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
393 {
394 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
395 result.utilization = total_placed_area / boundary.measure();
396 result.computation_time_ms = start.elapsed_ms();
397 result.placements = placements;
398 report(
399 ProgressInfo::new()
400 .with_phase("Time Limit Reached")
401 .with_items(placed_count, total_pieces)
402 .with_elapsed(result.computation_time_ms)
403 .finished(),
404 );
405 return Ok(result);
406 }
407
408 let mut best_placement: Option<(f64, f64, f64, bool)> = None; for &rotation in &rotation_angles {
412 for &mirror in mirror_candidates {
413 let ifp = match compute_ifp_with_margin_and_mirror(
415 &boundary_polygon,
416 geom,
417 rotation,
418 0.0,
420 mirror,
421 ) {
422 Ok(ifp) => ifp,
423 Err(_) => continue,
424 };
425
426 if ifp.is_empty() {
427 continue;
428 }
429
430 let mut nfps: Vec<Nfp> = Vec::new();
432 for placed in &placed_geometries {
433 let cache_key = (
436 placed.geometry.id().as_str(),
437 geom.id().as_str(),
438 rotation - placed.rotation, placed.mirrored,
440 mirror,
441 );
442
443 let nfp_at_origin =
448 match self.nfp_cache.get_or_compute_mirrored(cache_key, || {
449 let placed_at_origin = placed.geometry.clone();
450 compute_nfp_mirrored(
451 &placed_at_origin,
452 geom,
453 rotation - placed.rotation,
454 placed.mirrored,
455 mirror,
456 )
457 .map(|nfp| offset_nfp(&nfp, spacing))
461 }) {
462 Ok(nfp) => nfp,
463 Err(_) => continue,
464 };
465
466 let rotated_nfp = rotate_nfp(&nfp_at_origin, placed.rotation);
469 let translated_nfp = translate_nfp(&rotated_nfp, placed.position);
470 nfps.push(translated_nfp);
471 }
472
473 nfps.extend(hole_nfps(boundary, geom, rotation, mirror, margin));
478 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
479 if let Some((x, y)) = find_bottom_left_placement(
480 &ifp,
481 &nfp_refs,
482 sample_step,
483 PackingAxis::of(boundary),
484 ) {
485 let is_better =
487 match best_placement {
488 None => true,
489 Some((best_x, best_y, _, _)) => PackingAxis::of(boundary)
490 .precedes((x, y), (best_x, best_y), 1e-6),
491 };
492 if is_better {
493 best_placement = Some((x, y, rotation, mirror));
494 }
495 }
496 }
497 }
498
499 if let Some((x, y, rotation, mirror)) = best_placement {
501 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
508 let boundary_aabb = boundary.aabb();
509
510 if let Some((clamped_x, clamped_y)) = clamp_placement_to_boundary_with_margin(
511 x,
512 y,
513 geom_aabb,
514 boundary_aabb,
515 margin,
516 ) {
517 let placement = Placement::new_2d(
518 geom.id().clone(),
519 instance,
520 clamped_x,
521 clamped_y,
522 rotation,
523 )
524 .with_mirrored(mirror);
525
526 placements.push(placement);
527 placed_geometries.push(
528 PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation)
529 .with_mirrored(mirror),
530 );
531 total_placed_area += geom.measure();
532 placed_count += 1;
533 report(
534 ProgressInfo::new()
535 .with_phase("NFP Placement")
536 .with_items(placed_count, total_pieces)
537 .with_utilization(total_placed_area / boundary.measure())
538 .with_elapsed(start.elapsed_ms()),
539 );
540 } else {
541 result.unplaced.push(geom.id().clone());
543 }
544 } else {
545 result.unplaced.push(geom.id().clone());
547 }
548 }
549 }
550
551 result.placements = placements;
552 result.boundaries_used = 1;
553 result.utilization = total_placed_area / boundary.measure();
554 result.computation_time_ms = start.elapsed_ms();
555 report(
556 ProgressInfo::new()
557 .with_phase("Complete")
558 .with_items(placed_count, total_pieces)
559 .with_utilization(result.utilization)
560 .with_elapsed(result.computation_time_ms)
561 .finished(),
562 );
563
564 Ok(result)
565 }
566
567 fn compute_sample_step(&self, geometries: &[Geometry2D]) -> f64 {
569 if geometries.is_empty() {
570 return 1.0;
571 }
572
573 let mut min_dim = f64::INFINITY;
575 for geom in geometries {
576 let (g_min, g_max) = geom.aabb();
577 let width = g_max[0] - g_min[0];
578 let height = g_max[1] - g_min[1];
579 min_dim = min_dim.min(width).min(height);
580 }
581
582 (min_dim / 4.0).clamp(0.5, 10.0)
584 }
585
586 fn not_worse_than_baselines(
596 &self,
597 meta: SolveResult<f64>,
598 geometries: &[Geometry2D],
599 boundary: &Boundary2D,
600 greedy: Option<SolveResult<f64>>,
601 ) -> SolveResult<f64> {
602 let baselines = self
603 .bottom_left_fill(geometries, boundary)
604 .ok()
605 .into_iter()
606 .chain(greedy);
607 let quality = |r: &SolveResult<f64>| solution_quality(r, geometries, boundary);
608 meta.floored_at(baselines, |a, b| {
611 let (a, b) = (quality(a), quality(b));
612 a.0 > b.0 || (a.0 == b.0 && a.1 < b.1 - 1e-6)
613 })
614 }
615
616 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, default_ms: u64) -> u64 {
628 if self.config.time_limit_ms > 0 {
629 (self.config.time_limit_ms / 4)
630 .max(5000)
631 .min(self.config.time_limit_ms)
632 } else {
633 default_ms
634 }
635 }
636
637 fn greedy_start(
644 &self,
645 geometries: &[Geometry2D],
646 boundary: &Boundary2D,
647 budget_ms: u64,
648 ) -> (Option<SolveResult<f64>>, u64) {
649 let started = Timer::now();
650 let greedy = Nester2D {
651 config: Config {
652 time_limit_ms: budget_ms,
653 ..self.config.clone()
654 },
655 cancelled: self.cancelled.clone(),
656 nfp_cache: NfpCache::new(),
657 }
658 .nfp_guided_blf(geometries, boundary)
659 .ok();
660 (
661 greedy,
662 budget_ms.saturating_sub(started.elapsed_ms()).max(1),
663 )
664 }
665
666 fn genetic_algorithm(
673 &self,
674 geometries: &[Geometry2D],
675 boundary: &Boundary2D,
676 progress: Option<ProgressCallback>,
677 ) -> Result<SolveResult<f64>> {
678 let (time_limit_ms, greedy) = {
679 let budget = self.search_budget_ms(15000);
680 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget);
681 (remaining, greedy)
682 };
683
684 let ga_config = self
685 .ga_config()
686 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
687
688 let result = run_ga_nesting(
689 geometries,
690 boundary,
691 &self.config,
692 ga_config,
693 self.cancelled.clone(),
694 greedy.as_ref().map(|g| g.placements.as_slice()),
695 progress,
696 )?;
697
698 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
699 }
700
701 fn brkga(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
705 let (time_limit_ms, greedy) = {
706 let budget = self.search_budget_ms(15000);
707 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget);
708 (remaining, greedy)
709 };
710
711 let brkga_config = self
712 .brkga_config()
713 .with_time_limit(std::time::Duration::from_millis(time_limit_ms));
714
715 let result = run_brkga_nesting(
716 geometries,
717 boundary,
718 &self.config,
719 brkga_config,
720 self.cancelled.clone(),
721 greedy.as_ref().map(|g| g.placements.as_slice()),
722 )?;
723
724 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
725 }
726
727 fn simulated_annealing(
732 &self,
733 geometries: &[Geometry2D],
734 boundary: &Boundary2D,
735 ) -> Result<SolveResult<f64>> {
736 let (time_limit_ms, greedy) = {
737 let budget = self.search_budget_ms(10000);
738 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget);
739 (remaining, greedy)
740 };
741
742 let sa_config = SaConfig::default()
743 .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));
749 let sa_config = match self.target_fitness() {
750 Some(target) => sa_config.with_target_fitness(target),
751 None => sa_config,
752 };
753
754 let result = run_sa_nesting(
755 geometries,
756 boundary,
757 &self.config,
758 sa_config,
759 self.cancelled.clone(),
760 greedy.as_ref().map(|g| g.placements.as_slice()),
761 )?;
762
763 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
764 }
765
766 fn gdrr(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
768 let (time_limit, greedy) = {
769 let budget = self.search_budget_ms(10000);
770 let (greedy, remaining) = self.greedy_start(geometries, boundary, budget);
771 (remaining, greedy)
772 };
773 let result = run_gdrr_nesting(
774 geometries,
775 boundary,
776 &self.config,
777 &self.gdrr_config(time_limit),
778 self.cancelled.clone(),
779 )?;
780
781 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
782 }
783
784 fn alns(&self, geometries: &[Geometry2D], boundary: &Boundary2D) -> Result<SolveResult<f64>> {
786 self.alns_within(geometries, boundary, self.search_budget_ms(10000))
787 }
788
789 fn alns_within(
792 &self,
793 geometries: &[Geometry2D],
794 boundary: &Boundary2D,
795 budget_ms: u64,
796 ) -> Result<SolveResult<f64>> {
797 let (greedy, time_limit) = self.greedy_start(geometries, boundary, budget_ms);
798 let result = run_alns_nesting(
799 geometries,
800 boundary,
801 &self.config,
802 &self.alns_config(time_limit),
803 self.cancelled.clone(),
804 )?;
805
806 Ok(self.not_worse_than_baselines(result, geometries, boundary, greedy))
807 }
808
809 #[cfg(feature = "milp")]
822 fn milp_exact(
823 &self,
824 geometries: &[Geometry2D],
825 boundary: &Boundary2D,
826 ) -> Result<SolveResult<f64>> {
827 let exact_config = ExactConfig::default()
832 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms))
833 .with_max_items(15)
834 .with_rotation_steps(4)
835 .with_grid_step(milp_grid_step(geometries, boundary));
836
837 run_nfp_cm_nesting(
838 geometries,
839 boundary,
840 &self.config,
841 &exact_config,
842 self.cancelled.clone(),
843 )
844 }
845
846 #[cfg(feature = "milp")]
848 fn hybrid_exact(
849 &self,
850 geometries: &[Geometry2D],
851 boundary: &Boundary2D,
852 ) -> Result<SolveResult<f64>> {
853 let started = Timer::now();
854 let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
856
857 if total_instances <= 15 {
859 let exact_config = ExactConfig::default()
863 .with_time_limit_ms(milp_time_limit(self.config.time_limit_ms / 2))
864 .with_max_items(15);
865
866 let exact_result = run_nfp_cm_nesting(
867 geometries,
868 boundary,
869 &self.config,
870 &exact_config,
871 self.cancelled.clone(),
872 )?;
873
874 if !exact_result.placements.is_empty() {
876 return Ok(exact_result);
877 }
878 }
879
880 let budget = match self.config.time_limit_ms {
885 0 => self.search_budget_ms(10000),
886 limit => self
887 .search_budget_ms(10000)
888 .min(limit.saturating_sub(started.elapsed_ms()).max(1)),
889 };
890 self.alns_within(geometries, boundary, budget)
891 }
892
893 fn bottom_left_fill_impl(
901 &self,
902 geometries: &[Geometry2D],
903 boundary: &Boundary2D,
904 callback: Option<&ProgressCallback>,
905 ) -> Result<SolveResult<f64>> {
906 let start = Timer::now();
907 let mut result = SolveResult::new();
908 let mut placements = Vec::new();
909 let report = |info: ProgressInfo| {
910 if let Some(callback) = callback {
911 callback(info);
912 }
913 };
914
915 let axis = PackingAxis::of(boundary);
916 let to_ca = |x: f64, y: f64| match axis {
918 PackingAxis::Y => (x, y),
919 PackingAxis::X => (y, x),
920 };
921 let from_ca = |across: f64, along: f64| match axis {
922 PackingAxis::Y => (across, along),
923 PackingAxis::X => (along, across),
924 };
925
926 let (b_min, b_max) = boundary.aabb();
927 let margin = self.config.margin;
928 let spacing = self.config.spacing;
929 let (min_across, min_along) = to_ca(b_min[0] + margin, b_min[1] + margin);
930 let (max_across, max_along) = to_ca(b_max[0] - margin, b_max[1] - margin);
931
932 let hole_boxes: Vec<(f64, f64, f64, f64)> = boundary
939 .holes()
940 .iter()
941 .filter(|hole| !hole.is_empty())
942 .map(|hole| {
943 let (mut x0, mut y0) = (f64::INFINITY, f64::INFINITY);
944 let (mut x1, mut y1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
945 for &(x, y) in hole {
946 x0 = x0.min(x);
947 y0 = y0.min(y);
948 x1 = x1.max(x);
949 y1 = y1.max(y);
950 }
951 let (a0, l0) = to_ca(x0 - margin, y0 - margin);
952 let (a1, l1) = to_ca(x1 + margin, y1 + margin);
953 (a0.min(a1), l0.min(l1), a0.max(a1), l0.max(l1))
954 })
955 .collect();
956
957 let blocking_hole_edge = |across: f64, along: f64, ea: f64, el: f64| -> Option<f64> {
961 hole_boxes
962 .iter()
963 .filter(|(a0, l0, a1, l1)| {
964 across < *a1 && across + ea > *a0 && along < *l1 && along + el > *l0
965 })
966 .map(|(_, _, a1, _)| *a1)
967 .fold(None, |far: Option<f64>, a1| {
968 Some(far.map_or(a1, |f| f.max(a1)))
969 })
970 };
971
972 let mut cursor_across = min_across;
973 let mut cursor_along = min_along;
974 let mut row_depth = 0.0_f64;
975 let mut total_placed_area = 0.0;
976 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
977 let mut placed_count = 0usize;
978
979 report(
980 ProgressInfo::new()
981 .with_phase("BLF Placement")
982 .with_items(0, total_pieces)
983 .with_elapsed(0),
984 );
985
986 for geom in geometries {
987 geom.validate()?;
988
989 let rotation_angles: Vec<f64> = geom.rotations();
991
992 for instance in 0..geom.quantity() {
993 if self.cancelled.load(Ordering::Relaxed) {
994 result.computation_time_ms = start.elapsed_ms();
995 report(
996 ProgressInfo::new()
997 .with_phase("Cancelled")
998 .with_items(placed_count, total_pieces)
999 .with_elapsed(result.computation_time_ms)
1000 .finished(),
1001 );
1002 return Ok(result);
1003 }
1004
1005 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
1007 {
1008 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
1009 result.utilization = total_placed_area / boundary.measure();
1010 result.computation_time_ms = start.elapsed_ms();
1011 result.placements = placements;
1012 report(
1013 ProgressInfo::new()
1014 .with_phase("Time Limit Reached")
1015 .with_items(placed_count, total_pieces)
1016 .with_elapsed(result.computation_time_ms)
1017 .finished(),
1018 );
1019 return Ok(result);
1020 }
1021
1022 let mut best_fit: Option<BlfCandidate> = None;
1025
1026 for &rotation in &rotation_angles {
1027 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
1028 let (extent_across, extent_along) =
1029 to_ca(g_max[0] - g_min[0], g_max[1] - g_min[1]);
1030
1031 if extent_across > max_across - min_across
1032 || extent_along > max_along - min_along
1033 {
1034 continue;
1035 }
1036
1037 let mut place_across = cursor_across;
1038 let mut place_along = cursor_along;
1039 let mut depth = row_depth;
1042 let mut opened_row = false;
1043 let mut turns = 0usize;
1049 let max_turns = 4 * (hole_boxes.len() + 1) * (hole_boxes.len() + 2) + 16;
1050 let fits = loop {
1051 if place_across + extent_across > max_across {
1052 place_across = min_across;
1053 place_along += depth + spacing;
1054 depth = extent_along;
1055 opened_row = true;
1056 }
1057 if place_along + extent_along > max_along {
1058 break false;
1059 }
1060 match blocking_hole_edge(
1061 place_across,
1062 place_along,
1063 extent_across,
1064 extent_along,
1065 ) {
1066 None => break true,
1067 Some(past) => place_across = past + spacing,
1068 }
1069 turns += 1;
1070 if turns > max_turns {
1071 break false;
1072 }
1073 };
1074 if !fits {
1075 continue;
1076 }
1077
1078 let cost = |c: &BlfCandidate| {
1083 if c.opened_row {
1084 c.place_along - min_along + c.extent_along
1085 } else {
1086 c.place_across - min_across + c.extent_across
1087 }
1088 };
1089 let candidate = BlfCandidate {
1090 rotation,
1091 extent_across,
1092 extent_along,
1093 place_across,
1094 place_along,
1095 opened_row,
1096 };
1097 let better = best_fit
1098 .as_ref()
1099 .is_none_or(|best| cost(&candidate) < cost(best) - 1e-6);
1100 if better {
1101 best_fit = Some(candidate);
1102 }
1103 }
1104
1105 let Some(BlfCandidate {
1106 rotation,
1107 extent_across,
1108 extent_along,
1109 place_across,
1110 place_along,
1111 opened_row,
1112 }) = best_fit
1113 else {
1114 result.unplaced.push(geom.id().clone());
1115 continue;
1116 };
1117 if opened_row {
1118 row_depth = 0.0;
1119 }
1120
1121 let geom_aabb = geom.aabb_at_rotation(rotation);
1122 let (place_x, place_y) = from_ca(place_across, place_along);
1123 let Some((x, y)) = clamp_placement_to_boundary_with_margin(
1124 place_x - geom_aabb.0[0],
1125 place_y - geom_aabb.0[1],
1126 geom_aabb,
1127 (b_min, b_max),
1128 margin,
1129 ) else {
1130 result.unplaced.push(geom.id().clone());
1131 continue;
1132 };
1133
1134 placements.push(Placement::new_2d(
1135 geom.id().clone(),
1136 instance,
1137 x,
1138 y,
1139 rotation,
1140 ));
1141 total_placed_area += geom.measure();
1142 placed_count += 1;
1143
1144 let (actual_across, actual_along) = to_ca(x + geom_aabb.0[0], y + geom_aabb.0[1]);
1146 cursor_across = actual_across + extent_across + spacing;
1147 cursor_along = actual_along;
1148 row_depth = row_depth.max(extent_along);
1149
1150 report(
1151 ProgressInfo::new()
1152 .with_phase("BLF Placement")
1153 .with_items(placed_count, total_pieces)
1154 .with_utilization(total_placed_area / boundary.measure())
1155 .with_elapsed(start.elapsed_ms()),
1156 );
1157 }
1158 }
1159
1160 result.placements = placements;
1161 result.boundaries_used = 1;
1162 result.utilization = total_placed_area / boundary.measure();
1163 result.computation_time_ms = start.elapsed_ms();
1164
1165 report(
1166 ProgressInfo::new()
1167 .with_phase("Complete")
1168 .with_items(placed_count, total_pieces)
1169 .with_utilization(result.utilization)
1170 .with_elapsed(result.computation_time_ms)
1171 .finished(),
1172 );
1173
1174 Ok(result)
1175 }
1176
1177 fn validate_geometries(&self, geometries: &[Geometry2D]) -> Result<()> {
1191 use u_nesting_core::geometry::Geometry;
1192 if geometries.is_empty() {
1193 return Err(Error::empty_input("geometries"));
1194 }
1195 self.config.validate()?;
1196 for geom in geometries {
1197 geom.validate()?;
1198 }
1199 u_nesting_core::geometry::ensure_unique_ids(geometries)
1200 }
1201
1202 pub fn solve_multi_strip(
1204 &self,
1205 geometries: &[Geometry2D],
1206 boundary: &Boundary2D,
1207 ) -> Result<SolveResult<f64>> {
1208 boundary.validate()?;
1209 self.validate_geometries(geometries)?;
1210 self.cancelled.store(false, Ordering::Relaxed);
1211
1212 let (b_min, b_max) = boundary.aabb();
1213 let strip_width = b_max[0] - b_min[0];
1214
1215 let mut final_result = SolveResult::new();
1216 let mut remaining_geometries: Vec<Geometry2D> = geometries.to_vec();
1217 let mut strip_index = 0;
1218 let max_strips = 100; let mut placed_total: std::collections::HashMap<String, usize> =
1223 std::collections::HashMap::new();
1224
1225 while !remaining_geometries.is_empty() && strip_index < max_strips {
1226 if self.cancelled.load(Ordering::Relaxed) {
1227 break;
1228 }
1229
1230 let strip_result = self.run_strategy(&remaining_geometries, boundary)?;
1232
1233 let strip_result = validate_and_filter_placements(
1235 strip_result,
1236 &remaining_geometries,
1237 boundary,
1238 self.config.margin,
1239 );
1240
1241 if strip_result.placements.is_empty() {
1242 break;
1245 }
1246
1247 let mut strip_placed: std::collections::HashMap<String, usize> =
1250 std::collections::HashMap::new();
1251
1252 for mut placement in strip_result.placements {
1254 let gid = placement.geometry_id.clone();
1255 let prior = placed_total.get(&gid).copied().unwrap_or(0);
1258 let in_strip = strip_placed.get(&gid).copied().unwrap_or(0);
1259 placement.instance = prior + in_strip;
1260 if !placement.position.is_empty() {
1262 placement.position[0] += strip_index as f64 * strip_width;
1263 }
1264 placement.boundary_index = strip_index;
1265 *strip_placed.entry(gid).or_insert(0) += 1;
1266 final_result.placements.push(placement);
1267 }
1268
1269 for (gid, cnt) in &strip_placed {
1273 *placed_total.entry(gid.clone()).or_insert(0) += cnt;
1274 }
1275 remaining_geometries = remaining_geometries
1276 .into_iter()
1277 .filter_map(|g| {
1278 let placed_here = strip_placed.get(g.id()).copied().unwrap_or(0);
1279 let new_quantity = g.quantity().saturating_sub(placed_here);
1280 if new_quantity == 0 {
1281 None
1282 } else {
1283 Some(g.with_quantity(new_quantity))
1284 }
1285 })
1286 .collect();
1287
1288 strip_index += 1;
1289 }
1290
1291 for g in &remaining_geometries {
1294 final_result.unplaced.push(g.id().clone());
1295 }
1296
1297 final_result.boundaries_used = strip_index;
1298 final_result.deduplicate_unplaced();
1299 final_result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1301
1302 let (b_min, b_max) = boundary.aabb();
1304 let strip_height = b_max[1] - b_min[1]; let mut strip_stats_map: std::collections::HashMap<usize, (f64, f64, usize)> =
1308 std::collections::HashMap::new(); for placement in &final_result.placements {
1311 let strip_idx = placement.boundary_index;
1312 if let Some(geom) = geometries.iter().find(|g| g.id() == &placement.geometry_id) {
1314 use u_nesting_core::geometry::Geometry;
1315 let piece_area = geom.measure();
1316 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
1317 let (_g_min, g_max) = geom.aabb_at_rotation(rotation);
1318 let local_x = placement.position[0] - (strip_idx as f64 * strip_width);
1321 let right_edge = local_x + g_max[0];
1322
1323 let entry = strip_stats_map.entry(strip_idx).or_insert((0.0, 0.0, 0));
1324 entry.0 = entry.0.max(right_edge); entry.1 += piece_area; entry.2 += 1; }
1328 }
1329
1330 use u_nesting_core::result::StripStats;
1332 let mut strip_stats: Vec<StripStats> = strip_stats_map
1333 .into_iter()
1334 .map(|(idx, (used_length, piece_area, count))| StripStats {
1335 strip_index: idx,
1336 used_length,
1337 piece_area,
1338 piece_count: count,
1339 strip_width, strip_height, })
1342 .collect();
1343 strip_stats.sort_by_key(|s| s.strip_index);
1344
1345 let total_piece_area: f64 = strip_stats.iter().map(|s| s.piece_area).sum();
1348 let total_material_used: f64 = strip_stats
1349 .iter()
1350 .map(|s| s.strip_height * s.used_length)
1351 .sum();
1352
1353 final_result.strip_stats = strip_stats;
1354 final_result.total_piece_area = total_piece_area;
1355 final_result.total_material_used = total_material_used;
1356
1357 if total_material_used > 0.0 {
1358 final_result.utilization = total_piece_area / total_material_used;
1359 }
1360
1361 Ok(final_result)
1362 }
1363}
1364
1365impl Nester2D {
1366 fn solve_reporting(
1370 &self,
1371 geometries: &[Geometry2D],
1372 boundary: &Boundary2D,
1373 progress: Option<ProgressCallback>,
1374 ) -> Result<SolveResult<f64>> {
1375 boundary.validate()?;
1376 self.validate_geometries(geometries)?;
1377
1378 self.cancelled.store(false, Ordering::Relaxed);
1380
1381 let initial_result = self.run_strategy_reporting(geometries, boundary, progress)?;
1382
1383 let mut result = validate_and_filter_placements(
1385 initial_result,
1386 geometries,
1387 boundary,
1388 self.config.margin,
1389 );
1390
1391 result.deduplicate_unplaced();
1393 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
1396 Ok(result)
1397 }
1398}
1399
1400impl Solver for Nester2D {
1401 type Geometry = Geometry2D;
1402 type Boundary = Boundary2D;
1403 type Scalar = f64;
1404
1405 fn solve(
1406 &self,
1407 geometries: &[Self::Geometry],
1408 boundary: &Self::Boundary,
1409 ) -> Result<SolveResult<f64>> {
1410 self.solve_reporting(geometries, boundary, None)
1411 }
1412
1413 fn solve_with_progress(
1414 &self,
1415 geometries: &[Self::Geometry],
1416 boundary: &Self::Boundary,
1417 callback: ProgressCallback,
1418 ) -> Result<SolveResult<f64>> {
1419 self.solve_reporting(geometries, boundary, Some(callback))
1420 }
1421
1422 fn cancel(&self) {
1423 self.cancelled.store(true, Ordering::Relaxed);
1424 }
1425}
1426
1427#[cfg(test)]
1428mod tests {
1429 #[cfg(feature = "milp")]
1430 mod milp_budget {
1431 use super::super::{milp_grid_step, milp_time_limit};
1432 use crate::{Boundary2D, Geometry2D};
1433
1434 #[test]
1437 fn an_unlimited_request_does_not_become_a_minute() {
1438 assert_eq!(milp_time_limit(0), u64::MAX / 2);
1439 }
1440
1441 #[test]
1444 fn a_short_request_is_kept_short() {
1445 assert_eq!(milp_time_limit(1_000), 1_000);
1446 assert_eq!(milp_time_limit(250), 250);
1447 }
1448
1449 #[test]
1453 fn the_grid_step_follows_the_smallest_piece() {
1454 let boundary = Boundary2D::rectangle(400.0, 100.0);
1455 let pieces = [
1456 Geometry2D::rectangle("wide", 80.0, 40.0),
1457 Geometry2D::rectangle("small", 20.0, 60.0),
1458 ];
1459 assert_eq!(milp_grid_step(&pieces, &boundary), 5.0);
1461 }
1462
1463 #[test]
1466 fn a_huge_sheet_keeps_the_step_from_getting_too_fine() {
1467 let boundary = Boundary2D::rectangle(100_000.0, 100.0);
1468 let pieces = [Geometry2D::rectangle("small", 4.0, 4.0)];
1469 assert_eq!(milp_grid_step(&pieces, &boundary), 100_000.0 / 256.0);
1470 }
1471
1472 #[test]
1474 fn the_step_never_goes_below_a_unit() {
1475 let boundary = Boundary2D::rectangle(10.0, 10.0);
1476 let pieces = [Geometry2D::rectangle("tiny", 0.5, 0.5)];
1477 assert_eq!(milp_grid_step(&pieces, &boundary), 1.0);
1478 }
1479 }
1480
1481 use super::*;
1482 use crate::placement_utils::polygon_centroid;
1483
1484 #[test]
1488 fn a_geometry_id_given_twice_is_refused_by_every_entry_point() {
1489 let geometries = [
1490 Geometry2D::rectangle("P", 10.0, 10.0),
1491 Geometry2D::rectangle("Q", 10.0, 10.0),
1492 Geometry2D::rectangle("P", 30.0, 5.0),
1493 ];
1494 let boundary = Boundary2D::rectangle(100.0, 100.0);
1495 let nester = Nester2D::default_config();
1496 let errors = [
1497 nester.solve(&geometries, &boundary).expect_err("solve"),
1498 nester
1499 .solve_multi_strip(&geometries, &boundary)
1500 .expect_err("solve_multi_strip"),
1501 nester
1502 .solve_with_progress(&geometries, &boundary, Box::new(|_| {}))
1503 .expect_err("solve_with_progress"),
1504 ];
1505 for err in errors {
1506 let msg = err.to_string();
1507 assert!(msg.contains("'P'"), "{msg}");
1508 assert!(msg.contains("positions 0 and 2"), "{msg}");
1509 }
1510 }
1511
1512 #[test]
1515 fn an_empty_list_of_geometries_is_refused_by_every_entry_point() {
1516 let boundary = Boundary2D::rectangle(100.0, 100.0);
1517 let nester = Nester2D::default_config();
1518 let errors = [
1519 nester.solve(&[], &boundary).expect_err("solve"),
1520 nester
1521 .solve_multi_strip(&[], &boundary)
1522 .expect_err("solve_multi_strip"),
1523 nester
1524 .solve_with_progress(&[], &boundary, Box::new(|_| {}))
1525 .expect_err("solve_with_progress"),
1526 ];
1527 for err in errors {
1528 assert_eq!(
1529 err,
1530 Error::EmptyInput {
1531 parameter: "geometries".into()
1532 }
1533 );
1534 }
1535 }
1536
1537 #[test]
1538 fn the_seed_reaches_the_searches_that_build_their_own_run() {
1539 let seeded = Nester2D::new(Config::default().with_seed(7));
1543 assert_eq!(seeded.gdrr_config(1000).seed, Some(7));
1544 assert_eq!(seeded.alns_config(1000).seed, Some(7));
1545 let unseeded = Nester2D::default_config();
1546 assert_eq!(unseeded.gdrr_config(1000).seed, None);
1547 assert_eq!(unseeded.alns_config(1000).seed, None);
1548 }
1549
1550 #[test]
1555 fn a_set_budget_is_run_and_an_unset_one_is_the_strategy_s() {
1556 assert_eq!((SEARCH_POPULATION, SEARCH_GENERATIONS), (30, 50));
1557 let unset = Nester2D::default_config();
1558 assert_eq!(unset.ga_config().population_size, SEARCH_POPULATION);
1559 assert_eq!(unset.ga_config().max_generations, SEARCH_GENERATIONS);
1560 assert_eq!(unset.brkga_config().population_size, SEARCH_POPULATION);
1561 assert_eq!(unset.brkga_config().max_generations, SEARCH_GENERATIONS);
1562
1563 let set = Nester2D::new(
1564 Config::default()
1565 .with_population_size(100)
1566 .with_max_generations(200),
1567 );
1568 assert_eq!(set.ga_config().population_size, 100);
1569 assert_eq!(set.ga_config().max_generations, 200);
1570 assert_eq!(set.brkga_config().population_size, 100);
1571 assert_eq!(set.brkga_config().max_generations, 200);
1572
1573 let small = Nester2D::new(Config::default().with_population_size(3));
1574 assert_eq!(small.brkga_config().population_size, 3, "was raised to 4");
1575 assert!(small.brkga_config().validate().is_ok());
1576 }
1577
1578 #[test]
1581 fn the_generation_budget_reaches_the_running_search() {
1582 use std::sync::Mutex;
1583 let reports = |config: Config| {
1584 let seen = Arc::new(Mutex::new(Vec::new()));
1585 let sink = seen.clone();
1586 Nester2D::new(
1587 config
1588 .with_strategy(Strategy::GeneticAlgorithm)
1589 .with_seed(7),
1590 )
1591 .solve_with_progress(
1592 &[Geometry2D::rectangle("R", 10.0, 10.0).with_quantity(4)],
1593 &Boundary2D::rectangle(50.0, 50.0),
1594 Box::new(move |info| {
1595 sink.lock()
1596 .expect("no report panicked")
1597 .push((info.iteration, info.total_iterations))
1598 }),
1599 )
1600 .expect("solve");
1601 let seen = seen.lock().expect("no report panicked").clone();
1602 seen
1603 };
1604 let set = reports(Config::default().with_max_generations(80));
1605 assert!(!set.is_empty(), "the search reports its progress");
1606 assert!(
1607 set.iter().all(|&(at, total)| total == 80 && at <= 80),
1608 "{set:?}"
1609 );
1610 let unset = reports(Config::default());
1611 assert!(!unset.is_empty());
1612 assert!(unset.iter().all(|&(_, total)| total == 50), "{unset:?}");
1613 }
1614
1615 #[test]
1623 fn a_progress_callback_does_not_change_the_solve() {
1624 let geometries = [
1625 Geometry2D::rectangle("A", 30.0, 12.0)
1626 .with_quantity(3)
1627 .with_rotations_deg(vec![0.0, 90.0]),
1628 Geometry2D::rectangle("B", 18.0, 18.0).with_quantity(2),
1629 Geometry2D::new("L")
1630 .with_polygon(vec![
1631 (0.0, 0.0),
1632 (20.0, 0.0),
1633 (20.0, 8.0),
1634 (8.0, 8.0),
1635 (8.0, 20.0),
1636 (0.0, 20.0),
1637 ])
1638 .with_quantity(2)
1639 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0]),
1640 ];
1641 let boundary = Boundary2D::rectangle(60.0, 50.0);
1642 type Laid = (String, usize, Vec<f64>, Vec<f64>, bool);
1644 let layout = |r: &SolveResult<f64>| -> Vec<Laid> {
1645 r.placements
1646 .iter()
1647 .map(|p| {
1648 (
1649 p.geometry_id.clone(),
1650 p.instance,
1651 p.position.clone(),
1652 p.rotation.clone(),
1653 p.mirrored,
1654 )
1655 })
1656 .collect()
1657 };
1658 for strategy in [
1659 Strategy::BottomLeftFill,
1660 Strategy::NfpGuided,
1661 Strategy::GeneticAlgorithm,
1662 Strategy::Brkga,
1663 ] {
1664 for time_limit_ms in [0, 60_000] {
1665 let config = Config::default()
1666 .with_strategy(strategy)
1667 .with_seed(11)
1668 .with_population_size(6)
1669 .with_max_generations(4)
1670 .with_time_limit(time_limit_ms);
1671 let plain = Nester2D::new(config.clone())
1672 .solve(&geometries, &boundary)
1673 .expect("solve");
1674 let reported = Nester2D::new(config)
1675 .solve_with_progress(&geometries, &boundary, Box::new(|_| {}))
1676 .expect("solve with progress");
1677 let case = format!("{strategy:?}, time_limit_ms {time_limit_ms}");
1678 assert!(!plain.placements.is_empty(), "{case}");
1679 assert_eq!(layout(&plain), layout(&reported), "{case}");
1680 assert_eq!(plain.utilization, reported.utilization, "{case}");
1681 assert_eq!(plain.generations, reported.generations, "{case}");
1682 assert_eq!(plain.unplaced, reported.unplaced, "{case}");
1683 }
1684 }
1685 }
1686
1687 #[test]
1688 fn test_simple_nesting() {
1689 let geometries = vec![
1690 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(3),
1691 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1692 ];
1693
1694 let boundary = Boundary2D::rectangle(100.0, 50.0);
1695 let nester = Nester2D::default_config();
1696
1697 let result = nester.solve(&geometries, &boundary).unwrap();
1698
1699 assert!(result.utilization > 0.0);
1700 assert!(result.placements.len() <= 5); }
1702
1703 #[test]
1704 fn test_placement_within_bounds() {
1705 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1706
1707 let boundary = Boundary2D::rectangle(50.0, 50.0);
1708 let config = Config::default().with_margin(5.0).with_spacing(2.0);
1709 let nester = Nester2D::new(config);
1710
1711 let result = nester.solve(&geometries, &boundary).unwrap();
1712
1713 assert_eq!(result.placements.len(), 4);
1715 assert!(result.unplaced.is_empty());
1716
1717 for p in &result.placements {
1719 assert!(p.position[0] >= 5.0);
1720 assert!(p.position[1] >= 5.0);
1721 }
1722 }
1723
1724 #[test]
1725 fn test_nfp_guided_basic() {
1726 let geometries = vec![
1727 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1728 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(1),
1729 ];
1730
1731 let boundary = Boundary2D::rectangle(100.0, 50.0);
1732 let config = Config::default().with_strategy(Strategy::NfpGuided);
1733 let nester = Nester2D::new(config);
1734
1735 let result = nester.solve(&geometries, &boundary).unwrap();
1736
1737 assert!(result.utilization > 0.0);
1738 assert_eq!(result.placements.len(), 3); assert!(result.unplaced.is_empty());
1740 }
1741
1742 #[test]
1743 fn test_nfp_guided_with_spacing() {
1744 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
1745
1746 let boundary = Boundary2D::rectangle(50.0, 50.0);
1747 let config = Config::default()
1748 .with_strategy(Strategy::NfpGuided)
1749 .with_margin(2.0)
1750 .with_spacing(3.0);
1751 let nester = Nester2D::new(config);
1752
1753 let result = nester.solve(&geometries, &boundary).unwrap();
1754
1755 assert_eq!(result.placements.len(), 4);
1757 assert!(result.unplaced.is_empty());
1758
1759 assert!(result.utilization > 0.0);
1761 }
1762
1763 #[test]
1764 fn test_nfp_guided_no_overlap() {
1765 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(3)];
1766
1767 let boundary = Boundary2D::rectangle(100.0, 100.0);
1768 let config = Config::default().with_strategy(Strategy::NfpGuided);
1769 let nester = Nester2D::new(config);
1770
1771 let result = nester.solve(&geometries, &boundary).unwrap();
1772
1773 assert_eq!(result.placements.len(), 3);
1774
1775 for i in 0..result.placements.len() {
1777 for j in (i + 1)..result.placements.len() {
1778 let p1 = &result.placements[i];
1779 let p2 = &result.placements[j];
1780
1781 let r1_min_x = p1.position[0];
1783 let r1_max_x = p1.position[0] + 20.0;
1784 let r1_min_y = p1.position[1];
1785 let r1_max_y = p1.position[1] + 20.0;
1786
1787 let r2_min_x = p2.position[0];
1788 let r2_max_x = p2.position[0] + 20.0;
1789 let r2_min_y = p2.position[1];
1790 let r2_max_y = p2.position[1] + 20.0;
1791
1792 let overlaps_x = r1_min_x < r2_max_x - 0.01 && r1_max_x > r2_min_x + 0.01;
1794 let overlaps_y = r1_min_y < r2_max_y - 0.01 && r1_max_y > r2_min_y + 0.01;
1795
1796 assert!(
1797 !(overlaps_x && overlaps_y),
1798 "Placements {} and {} overlap",
1799 i,
1800 j
1801 );
1802 }
1803 }
1804 }
1805
1806 fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
1811 for i in 0..a.len() {
1812 let a1 = a[i];
1813 let a2 = a[(i + 1) % a.len()];
1814 for j in 0..b.len() {
1815 let b1 = b[j];
1816 let b2 = b[(j + 1) % b.len()];
1817 if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
1818 return true;
1819 }
1820 }
1821 }
1822 false
1823 }
1824
1825 #[test]
1826 fn test_mirror_candidates_reflects_allow_flip() {
1827 let plain = Geometry2D::rectangle("R", 10.0, 10.0);
1828 assert_eq!(mirror_candidates(&plain), &[false]);
1829
1830 let flippable = Geometry2D::rectangle("R", 10.0, 10.0).with_flip(true);
1831 assert_eq!(mirror_candidates(&flippable), &[false, true]);
1832 }
1833
1834 #[test]
1854 fn test_mirror_aware_placement_avoids_overlap() {
1855 let spacing = 1.0;
1863 let geom = Geometry2D::l_shape("L", 30.0, 20.0, 20.0, 10.0);
1864 let boundary_polygon = vec![(0.0, 0.0), (65.0, 0.0), (65.0, 45.0), (0.0, 45.0)];
1865 let cache = NfpCache::new();
1866
1867 let ifp1 =
1869 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, false).unwrap();
1870 let (x1, y1) = find_bottom_left_placement(&ifp1, &[], 1.0, PackingAxis::X)
1871 .expect("first piece must fit");
1872 let placed1 = PlacedGeometry::new(geom.clone(), (x1, y1), 0.0).with_mirrored(false);
1873
1874 let ifp2 =
1876 compute_ifp_with_margin_and_mirror(&boundary_polygon, &geom, 0.0, 0.0, true).unwrap();
1877 let nfp_at_origin = cache
1878 .get_or_compute_mirrored(("L", "L", 0.0, false, true), || {
1879 compute_nfp_mirrored(&placed1.geometry, &geom, 0.0, false, true)
1880 })
1881 .unwrap();
1882 let translated_nfp = translate_nfp(&nfp_at_origin, placed1.position);
1883 let expanded_nfp = offset_nfp(&translated_nfp, spacing);
1884 let (x2, y2) = find_bottom_left_placement(&ifp2, &[&expanded_nfp], 1.0, PackingAxis::X)
1885 .expect("mirrored second piece must fit avoiding the first");
1886 let placed2 = PlacedGeometry::new(geom.clone(), (x2, y2), 0.0).with_mirrored(true);
1887
1888 assert_ne!(
1889 (x1, y1),
1890 (x2, y2),
1891 "mirrored placement must actually avoid the first piece's position"
1892 );
1893 assert!(
1894 !polygons_overlap(
1895 &placed1.translated_exterior(),
1896 &placed2.translated_exterior()
1897 ),
1898 "unmirrored piece at {:?} and mirrored piece at {:?} must not overlap",
1899 (x1, y1),
1900 (x2, y2)
1901 );
1902 }
1903
1904 #[test]
1905 fn test_nfp_guided_utilization() {
1906 let geometries = vec![Geometry2D::rectangle("R1", 25.0, 25.0).with_quantity(4)];
1908
1909 let boundary = Boundary2D::rectangle(100.0, 50.0);
1910 let config = Config::default().with_strategy(Strategy::NfpGuided);
1911 let nester = Nester2D::new(config);
1912
1913 let result = nester.solve(&geometries, &boundary).unwrap();
1914
1915 assert_eq!(result.placements.len(), 4);
1917
1918 assert!(result.utilization > 0.45);
1920 }
1921
1922 #[test]
1923 fn test_polygon_centroid() {
1924 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1926 let (cx, cy) = polygon_centroid(&square);
1927 assert!((cx - 5.0).abs() < 0.01);
1928 assert!((cy - 5.0).abs() < 0.01);
1929
1930 let triangle = vec![(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
1931 let (cx, cy) = polygon_centroid(&triangle);
1932 assert!((cx - 3.0).abs() < 0.01);
1933 assert!((cy - 2.0).abs() < 0.01);
1934 }
1935
1936 #[test]
1937 fn test_ga_strategy_basic() {
1938 let geometries = vec![
1939 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1940 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1941 ];
1942
1943 let boundary = Boundary2D::rectangle(100.0, 50.0);
1944 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1945 let nester = Nester2D::new(config);
1946
1947 let result = nester.solve(&geometries, &boundary).unwrap();
1948
1949 assert!(result.utilization > 0.0);
1950 assert!(!result.placements.is_empty());
1951 assert!(result.generations.is_some());
1953 assert!(result.best_fitness.is_some());
1954 assert!(result.strategy == Some("GeneticAlgorithm".to_string()));
1955 }
1956
1957 #[test]
1958 fn test_ga_strategy_all_placed() {
1959 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1961
1962 let boundary = Boundary2D::rectangle(100.0, 100.0);
1963 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
1964 let nester = Nester2D::new(config);
1965
1966 let result = nester.solve(&geometries, &boundary).unwrap();
1967
1968 assert_eq!(result.placements.len(), 4);
1970 assert!(result.unplaced.is_empty());
1971 }
1972
1973 #[test]
1974 fn test_brkga_strategy_basic() {
1975 let geometries = vec![
1976 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
1977 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
1978 ];
1979
1980 let boundary = Boundary2D::rectangle(100.0, 50.0);
1981 let config = Config::default().with_strategy(Strategy::Brkga);
1982 let nester = Nester2D::new(config);
1983
1984 let result = nester.solve(&geometries, &boundary).unwrap();
1985
1986 assert!(result.utilization > 0.0);
1987 assert!(!result.placements.is_empty());
1988 assert!(result.generations.is_some());
1990 assert!(result.best_fitness.is_some());
1991 assert!(result.strategy == Some("BRKGA".to_string()));
1992 }
1993
1994 #[test]
1995 fn test_brkga_strategy_all_placed() {
1996 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
1998
1999 let boundary = Boundary2D::rectangle(100.0, 100.0);
2000 let config = Config::default()
2002 .with_strategy(Strategy::Brkga)
2003 .with_time_limit(30000); let nester = Nester2D::new(config);
2005
2006 let result = nester.solve(&geometries, &boundary).unwrap();
2007
2008 assert!(
2011 result.placements.len() >= 3,
2012 "Expected at least 3 placements, got {}",
2013 result.placements.len()
2014 );
2015 }
2016
2017 #[test]
2018 fn test_gdrr_strategy_basic() {
2019 let geometries = vec![
2020 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2021 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2022 ];
2023
2024 let boundary = Boundary2D::rectangle(100.0, 50.0);
2025 let config = Config::default().with_strategy(Strategy::Gdrr);
2026 let nester = Nester2D::new(config);
2027
2028 let result = nester.solve(&geometries, &boundary).unwrap();
2029
2030 assert!(result.utilization > 0.0);
2031 assert!(!result.placements.is_empty());
2032 assert!(result.iterations.is_some());
2034 assert!(result.best_fitness.is_some());
2035 assert!(result.strategy == Some("GDRR".to_string()));
2036 }
2037
2038 #[test]
2039 fn test_gdrr_strategy_all_placed() {
2040 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2042
2043 let boundary = Boundary2D::rectangle(100.0, 100.0);
2044 let config = Config::default().with_strategy(Strategy::Gdrr);
2045 let nester = Nester2D::new(config);
2046
2047 let result = nester.solve(&geometries, &boundary).unwrap();
2048
2049 assert_eq!(result.placements.len(), 4);
2051 assert!(result.unplaced.is_empty());
2052 }
2053
2054 #[test]
2055 fn test_alns_strategy_basic() {
2056 let geometries = vec![
2057 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
2058 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
2059 ];
2060
2061 let boundary = Boundary2D::rectangle(100.0, 50.0);
2062 let config = Config::default().with_strategy(Strategy::Alns);
2063 let nester = Nester2D::new(config);
2064
2065 let result = nester.solve(&geometries, &boundary).unwrap();
2066
2067 assert!(result.utilization > 0.0);
2068 assert!(!result.placements.is_empty());
2069 assert!(result.iterations.is_some());
2071 assert!(result.best_fitness.is_some());
2072 assert!(result.strategy == Some("ALNS".to_string()));
2073 }
2074
2075 #[test]
2076 fn test_alns_strategy_all_placed() {
2077 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
2079
2080 let boundary = Boundary2D::rectangle(100.0, 100.0);
2081 let config = Config::default().with_strategy(Strategy::Alns);
2082 let nester = Nester2D::new(config);
2083
2084 let result = nester.solve(&geometries, &boundary).unwrap();
2085
2086 assert_eq!(result.placements.len(), 4);
2088 assert!(result.unplaced.is_empty());
2089 }
2090
2091 #[test]
2092 fn test_blf_rotation_optimization() {
2093 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
2096 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(3)];
2098
2099 let boundary = Boundary2D::rectangle(35.0, 95.0);
2102 let nester = Nester2D::default_config();
2103
2104 let result = nester.solve(&geometries, &boundary).unwrap();
2105
2106 assert_eq!(
2108 result.placements.len(),
2109 3,
2110 "All pieces should be placed with rotation optimization"
2111 );
2112 assert!(result.unplaced.is_empty());
2113 }
2114
2115 #[test]
2116 fn test_blf_selects_best_rotation() {
2117 let geometries = vec![Geometry2D::rectangle("R1", 40.0, 10.0)
2119 .with_rotations(vec![0.0, std::f64::consts::FRAC_PI_2]) .with_quantity(2)];
2121
2122 let boundary = Boundary2D::rectangle(45.0, 50.0);
2126 let nester = Nester2D::default_config();
2127
2128 let result = nester.solve(&geometries, &boundary).unwrap();
2129
2130 assert_eq!(result.placements.len(), 2);
2131 assert!(result.unplaced.is_empty());
2132 }
2133
2134 #[test]
2135 fn test_progress_callback_blf() {
2136 use std::sync::atomic::{AtomicUsize, Ordering};
2137 use std::sync::Arc;
2138
2139 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2140 let boundary = Boundary2D::rectangle(50.0, 50.0);
2141 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2142 let nester = Nester2D::new(config);
2143
2144 let callback_count = Arc::new(AtomicUsize::new(0));
2145 let callback_count_clone = callback_count.clone();
2146 let last_items_placed = Arc::new(AtomicUsize::new(0));
2147 let last_items_placed_clone = last_items_placed.clone();
2148
2149 let callback: ProgressCallback = Box::new(move |info| {
2150 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2151 last_items_placed_clone.store(info.items_placed, Ordering::Relaxed);
2152 });
2153
2154 let result = nester
2155 .solve_with_progress(&geometries, &boundary, callback)
2156 .unwrap();
2157
2158 let count = callback_count.load(Ordering::Relaxed);
2160 assert!(
2161 count >= 5,
2162 "Expected at least 5 callbacks (1 initial + 4 pieces + 1 final), got {}",
2163 count
2164 );
2165
2166 let final_placed = last_items_placed.load(Ordering::Relaxed);
2168 assert_eq!(final_placed, 4, "Should report 4 items placed");
2169
2170 assert_eq!(result.placements.len(), 4);
2172 }
2173
2174 #[test]
2175 fn test_progress_callback_nfp() {
2176 use std::sync::atomic::{AtomicUsize, Ordering};
2177 use std::sync::Arc;
2178
2179 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
2180 let boundary = Boundary2D::rectangle(50.0, 50.0);
2181 let config = Config::default().with_strategy(Strategy::NfpGuided);
2182 let nester = Nester2D::new(config);
2183
2184 let callback_count = Arc::new(AtomicUsize::new(0));
2185 let callback_count_clone = callback_count.clone();
2186
2187 let callback: ProgressCallback = Box::new(move |info| {
2188 callback_count_clone.fetch_add(1, Ordering::Relaxed);
2189 assert!(info.items_placed <= info.total_items);
2190 });
2191
2192 let result = nester
2193 .solve_with_progress(&geometries, &boundary, callback)
2194 .unwrap();
2195
2196 let count = callback_count.load(Ordering::Relaxed);
2198 assert!(count >= 3, "Expected at least 3 callbacks, got {}", count);
2199
2200 assert_eq!(result.placements.len(), 2);
2202 }
2203
2204 #[test]
2205 fn test_time_limit_honored() {
2206 let geometries: Vec<Geometry2D> = (0..100)
2208 .map(|i| Geometry2D::rectangle(format!("R{}", i), 5.0, 5.0))
2209 .collect();
2210 let boundary = Boundary2D::rectangle(1000.0, 1000.0);
2211
2212 let config = Config::default()
2214 .with_strategy(Strategy::BottomLeftFill)
2215 .with_time_limit(1);
2216 let nester = Nester2D::new(config);
2217
2218 let result = nester.solve(&geometries, &boundary).unwrap();
2219
2220 assert!(
2223 result.computation_time_ms <= 100, "Computation took too long: {}ms (expected <= 100ms with 1ms limit)",
2225 result.computation_time_ms
2226 );
2227 }
2228
2229 #[test]
2230 fn test_time_limit_zero_unlimited() {
2231 let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(4)];
2233 let boundary = Boundary2D::rectangle(50.0, 50.0);
2234
2235 let config = Config::default()
2236 .with_strategy(Strategy::BottomLeftFill)
2237 .with_time_limit(0); let nester = Nester2D::new(config);
2239
2240 let result = nester.solve(&geometries, &boundary).unwrap();
2241
2242 assert_eq!(result.placements.len(), 4);
2244 }
2245
2246 #[test]
2247 fn test_blf_bounds_clamping() {
2248 let gear_like = Geometry2D::new("gear")
2252 .with_polygon(vec![
2253 (50.0, 5.0), (65.0, 15.0),
2255 (77.0, 18.0),
2256 (80.0, 32.0),
2257 (95.0, 50.0), (80.0, 68.0),
2259 (77.0, 82.0),
2260 (65.0, 85.0),
2261 (50.0, 95.0), (35.0, 85.0),
2263 (23.0, 82.0),
2264 (20.0, 68.0),
2265 (5.0, 50.0), (20.0, 32.0),
2267 (23.0, 18.0),
2268 (35.0, 15.0),
2269 ])
2270 .with_quantity(1);
2271
2272 let boundary = Boundary2D::rectangle(100.0, 100.0);
2274
2275 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2276 let nester = Nester2D::new(config);
2277
2278 let result = nester
2279 .solve(std::slice::from_ref(&gear_like), &boundary)
2280 .unwrap();
2281
2282 assert_eq!(result.placements.len(), 1);
2283 let placement = &result.placements[0];
2284
2285 let origin_x = placement.position[0];
2287 let origin_y = placement.position[1];
2288
2289 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2291
2292 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2294
2295 let actual_min_x = origin_x + g_min[0];
2297 let actual_max_x = origin_x + g_max[0];
2298 let actual_min_y = origin_y + g_min[1];
2299 let actual_max_y = origin_y + g_max[1];
2300
2301 assert!(
2303 actual_min_x >= 0.0,
2304 "Left edge {} should be >= 0",
2305 actual_min_x
2306 );
2307 assert!(
2308 actual_max_x <= 100.0,
2309 "Right edge {} should be <= 100",
2310 actual_max_x
2311 );
2312 assert!(
2313 actual_min_y >= 0.0,
2314 "Bottom edge {} should be >= 0",
2315 actual_min_y
2316 );
2317 assert!(
2318 actual_max_y <= 100.0,
2319 "Top edge {} should be <= 100",
2320 actual_max_y
2321 );
2322 }
2323
2324 #[test]
2325 fn test_blf_bounds_clamping_many_pieces() {
2326 let gear_like = Geometry2D::new("gear")
2329 .with_polygon(vec![
2330 (50.0, 5.0),
2331 (65.0, 15.0),
2332 (77.0, 18.0),
2333 (80.0, 32.0),
2334 (95.0, 50.0),
2335 (80.0, 68.0),
2336 (77.0, 82.0),
2337 (65.0, 85.0),
2338 (50.0, 95.0),
2339 (35.0, 85.0),
2340 (23.0, 82.0),
2341 (20.0, 68.0),
2342 (5.0, 50.0),
2343 (20.0, 32.0),
2344 (23.0, 18.0),
2345 (35.0, 15.0),
2346 ])
2347 .with_quantity(13); let boundary = Boundary2D::rectangle(500.0, 500.0);
2351
2352 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2353 let nester = Nester2D::new(config);
2354
2355 let result = nester
2356 .solve(std::slice::from_ref(&gear_like), &boundary)
2357 .unwrap();
2358
2359 for (i, placement) in result.placements.iter().enumerate() {
2361 let origin_x = placement.position[0];
2362 let origin_y = placement.position[1];
2363 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
2364
2365 let (g_min, g_max) = gear_like.aabb_at_rotation(rotation);
2366
2367 let actual_min_x = origin_x + g_min[0];
2368 let actual_max_x = origin_x + g_max[0];
2369 let actual_min_y = origin_y + g_min[1];
2370 let actual_max_y = origin_y + g_max[1];
2371
2372 assert!(
2373 actual_min_x >= -0.01,
2374 "Piece {}: Left edge {} should be >= 0",
2375 i,
2376 actual_min_x
2377 );
2378 assert!(
2379 actual_max_x <= 500.01,
2380 "Piece {}: Right edge {} should be <= 500",
2381 i,
2382 actual_max_x
2383 );
2384 assert!(
2385 actual_min_y >= -0.01,
2386 "Piece {}: Bottom edge {} should be >= 0",
2387 i,
2388 actual_min_y
2389 );
2390 assert!(
2391 actual_max_y <= 500.01,
2392 "Piece {}: Top edge {} should be <= 500",
2393 i,
2394 actual_max_y
2395 );
2396 }
2397 }
2398
2399 #[test]
2400 fn test_blf_bounds_trace() {
2401 let gear = Geometry2D::new("gear").with_polygon(vec![
2403 (50.0, 5.0),
2404 (65.0, 15.0),
2405 (77.0, 18.0),
2406 (80.0, 32.0),
2407 (95.0, 50.0),
2408 (80.0, 68.0),
2409 (77.0, 82.0),
2410 (65.0, 85.0),
2411 (50.0, 95.0),
2412 (35.0, 85.0),
2413 (23.0, 82.0),
2414 (20.0, 68.0),
2415 (5.0, 50.0),
2416 (20.0, 32.0),
2417 (23.0, 18.0),
2418 (35.0, 15.0),
2419 ]);
2420
2421 let (g_min, g_max) = gear.aabb();
2423 println!("Gear AABB: min={:?}, max={:?}", g_min, g_max);
2424 assert!(
2425 (g_min[0] - 5.0).abs() < 0.01,
2426 "g_min[0] should be 5, got {}",
2427 g_min[0]
2428 );
2429 assert!(
2430 (g_max[0] - 95.0).abs() < 0.01,
2431 "g_max[0] should be 95, got {}",
2432 g_max[0]
2433 );
2434
2435 let b_max_x = 500.0;
2437 let margin = 0.0;
2438 let max_valid_x = b_max_x - margin - g_max[0];
2439 println!(
2440 "max_valid_x = {} - {} - {} = {}",
2441 b_max_x, margin, g_max[0], max_valid_x
2442 );
2443 assert!(
2444 (max_valid_x - 405.0).abs() < 0.01,
2445 "max_valid_x should be 405, got {}",
2446 max_valid_x
2447 );
2448
2449 let boundary = Boundary2D::rectangle(500.0, 500.0);
2451 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2452 let nester = Nester2D::new(config);
2453
2454 let result = nester
2455 .solve(&[gear.clone().with_quantity(1)], &boundary)
2456 .unwrap();
2457
2458 assert_eq!(result.placements.len(), 1);
2459 let p = &result.placements[0];
2460 let origin_x = p.position[0];
2461 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2462
2463 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2464 let actual_max_x = origin_x + g_max_r[0];
2465
2466 println!("Placement: origin_x={}, rotation={}", origin_x, rotation);
2467 println!(
2468 "At rotation {}: g_min={:?}, g_max={:?}",
2469 rotation, g_min_r, g_max_r
2470 );
2471 println!(
2472 "Actual max x: {} + {} = {}",
2473 origin_x, g_max_r[0], actual_max_x
2474 );
2475
2476 assert!(
2477 actual_max_x <= 500.01,
2478 "Geometry exceeds boundary: max_x={} > 500",
2479 actual_max_x
2480 );
2481 }
2482
2483 #[test]
2484 fn test_blf_bounds_many_pieces_direct() {
2485 let gear = Geometry2D::new("gear")
2487 .with_polygon(vec![
2488 (50.0, 5.0),
2489 (65.0, 15.0),
2490 (77.0, 18.0),
2491 (80.0, 32.0),
2492 (95.0, 50.0),
2493 (80.0, 68.0),
2494 (77.0, 82.0),
2495 (65.0, 85.0),
2496 (50.0, 95.0),
2497 (35.0, 85.0),
2498 (23.0, 82.0),
2499 (20.0, 68.0),
2500 (5.0, 50.0),
2501 (20.0, 32.0),
2502 (23.0, 18.0),
2503 (35.0, 15.0),
2504 ])
2505 .with_quantity(25); let boundary = Boundary2D::rectangle(500.0, 500.0);
2508 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2509 let nester = Nester2D::new(config);
2510
2511 let result = nester
2512 .solve(std::slice::from_ref(&gear), &boundary)
2513 .unwrap();
2514
2515 println!("Placed {} pieces", result.placements.len());
2516
2517 for (i, p) in result.placements.iter().enumerate() {
2519 let origin_x = p.position[0];
2520 let origin_y = p.position[1];
2521 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2522
2523 let (g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2524
2525 let actual_min_x = origin_x + g_min_r[0];
2526 let actual_max_x = origin_x + g_max_r[0];
2527 let actual_min_y = origin_y + g_min_r[1];
2528 let actual_max_y = origin_y + g_max_r[1];
2529
2530 println!(
2531 "Piece {}: origin=({:.1}, {:.1}), rot={:.2}, bounds=[{:.1},{:.1}]x[{:.1},{:.1}]",
2532 i,
2533 origin_x,
2534 origin_y,
2535 rotation,
2536 actual_min_x,
2537 actual_max_x,
2538 actual_min_y,
2539 actual_max_y
2540 );
2541
2542 assert!(
2543 actual_max_x <= 500.01,
2544 "Piece {}: Right edge {} > 500",
2545 i,
2546 actual_max_x
2547 );
2548 assert!(
2549 actual_max_y <= 500.01,
2550 "Piece {}: Top edge {} > 500",
2551 i,
2552 actual_max_y
2553 );
2554 }
2555 }
2556
2557 #[test]
2558 fn test_blf_bounds_multi_strip() {
2559 let gear = Geometry2D::new("gear")
2561 .with_polygon(vec![
2562 (50.0, 5.0),
2563 (65.0, 15.0),
2564 (77.0, 18.0),
2565 (80.0, 32.0),
2566 (95.0, 50.0),
2567 (80.0, 68.0),
2568 (77.0, 82.0),
2569 (65.0, 85.0),
2570 (50.0, 95.0),
2571 (35.0, 85.0),
2572 (23.0, 82.0),
2573 (20.0, 68.0),
2574 (5.0, 50.0),
2575 (20.0, 32.0),
2576 (23.0, 18.0),
2577 (35.0, 15.0),
2578 ])
2579 .with_quantity(50); let boundary = Boundary2D::rectangle(500.0, 500.0);
2582 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2583 let nester = Nester2D::new(config);
2584
2585 let result = nester
2587 .solve_multi_strip(std::slice::from_ref(&gear), &boundary)
2588 .unwrap();
2589
2590 println!(
2591 "Placed {} pieces across {} strips",
2592 result.placements.len(),
2593 result.boundaries_used
2594 );
2595
2596 let strip_width = 500.0;
2598 for (i, p) in result.placements.iter().enumerate() {
2599 let origin_x = p.position[0];
2600 let origin_y = p.position[1];
2601 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2602 let strip_idx = p.boundary_index;
2603
2604 let local_x = origin_x - (strip_idx as f64 * strip_width);
2606
2607 let (_g_min_r, g_max_r) = gear.aabb_at_rotation(rotation);
2608
2609 let local_max_x = local_x + g_max_r[0];
2610 let local_max_y = origin_y + g_max_r[1];
2611
2612 println!(
2613 "Piece {}: strip={}, origin=({:.1}, {:.1}), local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2614 i, strip_idx, origin_x, origin_y, local_x, rotation, local_max_x
2615 );
2616
2617 assert!(
2618 local_max_x <= 500.01,
2619 "Piece {}: In strip {}, local right edge {:.1} > 500",
2620 i,
2621 strip_idx,
2622 local_max_x
2623 );
2624 assert!(
2625 local_max_y <= 500.01,
2626 "Piece {}: Top edge {:.1} > 500",
2627 i,
2628 local_max_y
2629 );
2630 }
2631 }
2632
2633 #[test]
2634 fn test_blf_bounds_mixed_shapes() {
2635 let shapes = vec![
2637 Geometry2D::new("shape0")
2639 .with_polygon(vec![
2640 (0.0, 0.0),
2641 (180.0, 0.0),
2642 (195.0, 15.0),
2643 (200.0, 50.0),
2644 (200.0, 150.0),
2645 (195.0, 185.0),
2646 (180.0, 200.0),
2647 (20.0, 200.0),
2648 (5.0, 185.0),
2649 (0.0, 150.0),
2650 (0.0, 50.0),
2651 (5.0, 15.0),
2652 ])
2653 .with_quantity(2),
2654 Geometry2D::new("shape1")
2656 .with_polygon(vec![
2657 (60.0, 0.0),
2658 (85.0, 7.0),
2659 (104.0, 25.0),
2660 (118.0, 50.0),
2661 (120.0, 60.0),
2662 (118.0, 70.0),
2663 (104.0, 95.0),
2664 (85.0, 113.0),
2665 (60.0, 120.0),
2666 (35.0, 113.0),
2667 (16.0, 95.0),
2668 (2.0, 70.0),
2669 (0.0, 60.0),
2670 (2.0, 50.0),
2671 (16.0, 25.0),
2672 (35.0, 7.0),
2673 ])
2674 .with_quantity(4),
2675 Geometry2D::new("shape2")
2677 .with_polygon(vec![
2678 (0.0, 0.0),
2679 (80.0, 0.0),
2680 (80.0, 20.0),
2681 (20.0, 20.0),
2682 (20.0, 80.0),
2683 (0.0, 80.0),
2684 ])
2685 .with_quantity(6),
2686 Geometry2D::new("shape3")
2688 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
2689 .with_quantity(6),
2690 Geometry2D::new("shape4")
2692 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
2693 .with_quantity(4),
2694 Geometry2D::new("shape5")
2696 .with_polygon(vec![
2697 (15.0, 0.0),
2698 (45.0, 0.0),
2699 (60.0, 26.0),
2700 (45.0, 52.0),
2701 (15.0, 52.0),
2702 (0.0, 26.0),
2703 ])
2704 .with_quantity(8),
2705 Geometry2D::new("shape6")
2707 .with_polygon(vec![
2708 (0.0, 0.0),
2709 (90.0, 0.0),
2710 (90.0, 12.0),
2711 (55.0, 12.0),
2712 (55.0, 60.0),
2713 (35.0, 60.0),
2714 (35.0, 12.0),
2715 (0.0, 12.0),
2716 ])
2717 .with_quantity(4),
2718 Geometry2D::new("shape7")
2720 .with_polygon(vec![
2721 (0.0, 10.0),
2722 (10.0, 0.0),
2723 (70.0, 0.0),
2724 (80.0, 10.0),
2725 (80.0, 70.0),
2726 (70.0, 80.0),
2727 (10.0, 80.0),
2728 (0.0, 70.0),
2729 ])
2730 .with_quantity(3),
2731 Geometry2D::new("shape8_gear")
2733 .with_polygon(vec![
2734 (50.0, 5.0),
2735 (65.0, 15.0),
2736 (77.0, 18.0),
2737 (80.0, 32.0),
2738 (95.0, 50.0),
2739 (80.0, 68.0),
2740 (77.0, 82.0),
2741 (65.0, 85.0),
2742 (50.0, 95.0),
2743 (35.0, 85.0),
2744 (23.0, 82.0),
2745 (20.0, 68.0),
2746 (5.0, 50.0),
2747 (20.0, 32.0),
2748 (23.0, 18.0),
2749 (35.0, 15.0),
2750 ])
2751 .with_quantity(13),
2752 ];
2753
2754 let boundary = Boundary2D::rectangle(500.0, 500.0);
2756 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2757 let nester = Nester2D::new(config);
2758
2759 let result = nester.solve_multi_strip(&shapes, &boundary).unwrap();
2760
2761 println!(
2762 "Placed {} pieces across {} strips",
2763 result.placements.len(),
2764 result.boundaries_used
2765 );
2766
2767 let strip_width = 500.0;
2769 let gear_aabb = shapes[8].aabb();
2770 println!("Gear AABB: min={:?}, max={:?}", gear_aabb.0, gear_aabb.1);
2771
2772 let mut violations = Vec::new();
2773 for p in &result.placements {
2774 if p.geometry_id.as_str().starts_with("shape8") {
2775 let origin_x = p.position[0];
2776 let _origin_y = p.position[1];
2777 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2778 let strip_idx = p.boundary_index;
2779 let local_x = origin_x - (strip_idx as f64 * strip_width);
2780
2781 let (_g_min_r, g_max_r) = shapes[8].aabb_at_rotation(rotation);
2782 let local_max_x = local_x + g_max_r[0];
2783
2784 println!(
2785 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2786 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2787 );
2788
2789 if local_max_x > 500.01 {
2790 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2791 }
2792 }
2793 }
2794
2795 assert!(
2796 violations.is_empty(),
2797 "Found {} Gear pieces exceeding boundary: {:?}",
2798 violations.len(),
2799 violations
2800 );
2801 }
2802
2803 #[test]
2804 fn test_blf_bounds_expanded_like_benchmark() {
2805 type ShapeDef = (Vec<(f64, f64)>, usize, Vec<f64>);
2809 let shape_defs: Vec<ShapeDef> = vec![
2810 (
2811 vec![
2812 (0.0, 0.0),
2813 (180.0, 0.0),
2814 (195.0, 15.0),
2815 (200.0, 50.0),
2816 (200.0, 150.0),
2817 (195.0, 185.0),
2818 (180.0, 200.0),
2819 (20.0, 200.0),
2820 (5.0, 185.0),
2821 (0.0, 150.0),
2822 (0.0, 50.0),
2823 (5.0, 15.0),
2824 ],
2825 2,
2826 vec![0.0, 90.0, 180.0, 270.0],
2827 ),
2828 (
2829 vec![
2830 (60.0, 0.0),
2831 (85.0, 7.0),
2832 (104.0, 25.0),
2833 (118.0, 50.0),
2834 (120.0, 60.0),
2835 (118.0, 70.0),
2836 (104.0, 95.0),
2837 (85.0, 113.0),
2838 (60.0, 120.0),
2839 (35.0, 113.0),
2840 (16.0, 95.0),
2841 (2.0, 70.0),
2842 (0.0, 60.0),
2843 (2.0, 50.0),
2844 (16.0, 25.0),
2845 (35.0, 7.0),
2846 ],
2847 4,
2848 vec![0.0, 45.0, 90.0, 135.0],
2849 ),
2850 (
2851 vec![
2852 (0.0, 0.0),
2853 (80.0, 0.0),
2854 (80.0, 20.0),
2855 (20.0, 20.0),
2856 (20.0, 80.0),
2857 (0.0, 80.0),
2858 ],
2859 6,
2860 vec![0.0, 90.0, 180.0, 270.0],
2861 ),
2862 (
2863 vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)],
2864 6,
2865 vec![0.0, 90.0, 180.0, 270.0],
2866 ),
2867 (
2868 vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)],
2869 4,
2870 vec![0.0, 90.0],
2871 ),
2872 (
2873 vec![
2874 (15.0, 0.0),
2875 (45.0, 0.0),
2876 (60.0, 26.0),
2877 (45.0, 52.0),
2878 (15.0, 52.0),
2879 (0.0, 26.0),
2880 ],
2881 8,
2882 vec![0.0, 60.0, 120.0],
2883 ),
2884 (
2885 vec![
2886 (0.0, 0.0),
2887 (90.0, 0.0),
2888 (90.0, 12.0),
2889 (55.0, 12.0),
2890 (55.0, 60.0),
2891 (35.0, 60.0),
2892 (35.0, 12.0),
2893 (0.0, 12.0),
2894 ],
2895 4,
2896 vec![0.0, 90.0, 180.0, 270.0],
2897 ),
2898 (
2899 vec![
2900 (0.0, 10.0),
2901 (10.0, 0.0),
2902 (70.0, 0.0),
2903 (80.0, 10.0),
2904 (80.0, 70.0),
2905 (70.0, 80.0),
2906 (10.0, 80.0),
2907 (0.0, 70.0),
2908 ],
2909 3,
2910 vec![0.0, 90.0],
2911 ),
2912 (
2914 vec![
2915 (50.0, 5.0),
2916 (65.0, 15.0),
2917 (77.0, 18.0),
2918 (80.0, 32.0),
2919 (95.0, 50.0),
2920 (80.0, 68.0),
2921 (77.0, 82.0),
2922 (65.0, 85.0),
2923 (50.0, 95.0),
2924 (35.0, 85.0),
2925 (23.0, 82.0),
2926 (20.0, 68.0),
2927 (5.0, 50.0),
2928 (20.0, 32.0),
2929 (23.0, 18.0),
2930 (35.0, 15.0),
2931 ],
2932 13,
2933 vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0],
2934 ),
2935 ];
2936
2937 let mut geometries = Vec::new();
2939 let mut piece_id = 0;
2940 for (vertices, demand, rotations) in shape_defs.iter() {
2941 for _ in 0..*demand {
2942 let geom = Geometry2D::new(format!("piece_{}", piece_id))
2943 .with_polygon(vertices.clone())
2944 .with_rotations_deg(rotations.clone());
2945 geometries.push(geom);
2946 piece_id += 1;
2947 }
2948 }
2949
2950 let gear_geom = Geometry2D::new("gear_check").with_polygon(shape_defs[8].0.clone());
2952 let (gear_min, gear_max) = gear_geom.aabb();
2953 println!("Gear AABB: min={:?}, max={:?}", gear_min, gear_max);
2954
2955 let boundary = Boundary2D::rectangle(500.0, 500.0);
2956 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
2957 let nester = Nester2D::new(config);
2958
2959 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
2960
2961 println!(
2962 "Placed {} pieces across {} strips",
2963 result.placements.len(),
2964 result.boundaries_used
2965 );
2966
2967 let strip_width = 500.0;
2969 let mut violations = Vec::new();
2970
2971 for p in &result.placements {
2972 let id_num: usize = p
2973 .geometry_id
2974 .as_str()
2975 .strip_prefix("piece_")
2976 .and_then(|s| s.parse().ok())
2977 .unwrap_or(0);
2978
2979 if (37..=49).contains(&id_num) {
2981 let origin_x = p.position[0];
2982 let rotation = p.rotation.first().copied().unwrap_or(0.0);
2983 let strip_idx = p.boundary_index;
2984 let local_x = origin_x - (strip_idx as f64 * strip_width);
2985
2986 let (_, g_max_r) = gear_geom.aabb_at_rotation(rotation);
2987 let local_max_x = local_x + g_max_r[0];
2988
2989 println!(
2990 "{}: strip={}, local_x={:.1}, rot={:.2}, local_max_x={:.1}",
2991 p.geometry_id, strip_idx, local_x, rotation, local_max_x
2992 );
2993
2994 if local_max_x > 500.01 {
2995 violations.push((p.geometry_id.clone(), strip_idx, local_x, local_max_x));
2996 }
2997 }
2998 }
2999
3000 assert!(
3001 violations.is_empty(),
3002 "Found {} Gear pieces exceeding boundary: {:?}",
3003 violations.len(),
3004 violations
3005 );
3006 }
3007
3008 fn aabbs_overlap(
3010 a_min: [f64; 2],
3011 a_max: [f64; 2],
3012 b_min: [f64; 2],
3013 b_max: [f64; 2],
3014 tolerance: f64,
3015 ) -> bool {
3016 let x_overlap = a_min[0] < b_max[0] - tolerance && a_max[0] > b_min[0] + tolerance;
3018 let y_overlap = a_min[1] < b_max[1] - tolerance && a_max[1] > b_min[1] + tolerance;
3019 x_overlap && y_overlap
3020 }
3021
3022 #[test]
3024 fn test_all_strategies_boundary_and_overlap() {
3025 use std::collections::HashMap;
3026
3027 let shapes = vec![
3029 Geometry2D::new("shape0")
3030 .with_polygon(vec![
3031 (0.0, 0.0),
3032 (180.0, 0.0),
3033 (195.0, 15.0),
3034 (200.0, 50.0),
3035 (200.0, 150.0),
3036 (195.0, 185.0),
3037 (180.0, 200.0),
3038 (20.0, 200.0),
3039 (5.0, 185.0),
3040 (0.0, 150.0),
3041 (0.0, 50.0),
3042 (5.0, 15.0),
3043 ])
3044 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3045 .with_quantity(2),
3046 Geometry2D::new("shape1_flange")
3047 .with_polygon(vec![
3048 (60.0, 0.0),
3049 (85.0, 7.0),
3050 (104.0, 25.0),
3051 (118.0, 50.0),
3052 (120.0, 60.0),
3053 (118.0, 70.0),
3054 (104.0, 95.0),
3055 (85.0, 113.0),
3056 (60.0, 120.0),
3057 (35.0, 113.0),
3058 (16.0, 95.0),
3059 (2.0, 70.0),
3060 (0.0, 60.0),
3061 (2.0, 50.0),
3062 (16.0, 25.0),
3063 (35.0, 7.0),
3064 ])
3065 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0])
3066 .with_quantity(4),
3067 Geometry2D::new("shape2_lbracket")
3068 .with_polygon(vec![
3069 (0.0, 0.0),
3070 (80.0, 0.0),
3071 (80.0, 20.0),
3072 (20.0, 20.0),
3073 (20.0, 80.0),
3074 (0.0, 80.0),
3075 ])
3076 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3077 .with_quantity(6),
3078 Geometry2D::new("shape3_triangle")
3079 .with_polygon(vec![(0.0, 0.0), (70.0, 0.0), (0.0, 70.0)])
3080 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3081 .with_quantity(6),
3082 Geometry2D::new("shape4_rect")
3083 .with_polygon(vec![(0.0, 0.0), (120.0, 0.0), (120.0, 60.0), (0.0, 60.0)])
3084 .with_rotations_deg(vec![0.0, 90.0])
3085 .with_quantity(4),
3086 Geometry2D::new("shape5_hexagon")
3087 .with_polygon(vec![
3088 (15.0, 0.0),
3089 (45.0, 0.0),
3090 (60.0, 26.0),
3091 (45.0, 52.0),
3092 (15.0, 52.0),
3093 (0.0, 26.0),
3094 ])
3095 .with_rotations_deg(vec![0.0, 60.0, 120.0])
3096 .with_quantity(8),
3097 Geometry2D::new("shape6_tstiff")
3098 .with_polygon(vec![
3099 (0.0, 0.0),
3100 (90.0, 0.0),
3101 (90.0, 12.0),
3102 (55.0, 12.0),
3103 (55.0, 60.0),
3104 (35.0, 60.0),
3105 (35.0, 12.0),
3106 (0.0, 12.0),
3107 ])
3108 .with_rotations_deg(vec![0.0, 90.0, 180.0, 270.0])
3109 .with_quantity(4),
3110 Geometry2D::new("shape7_mount")
3111 .with_polygon(vec![
3112 (0.0, 10.0),
3113 (10.0, 0.0),
3114 (70.0, 0.0),
3115 (80.0, 10.0),
3116 (80.0, 70.0),
3117 (70.0, 80.0),
3118 (10.0, 80.0),
3119 (0.0, 70.0),
3120 ])
3121 .with_rotations_deg(vec![0.0, 90.0])
3122 .with_quantity(3),
3123 Geometry2D::new("shape8_gear")
3124 .with_polygon(vec![
3125 (50.0, 5.0),
3126 (65.0, 15.0),
3127 (77.0, 18.0),
3128 (80.0, 32.0),
3129 (95.0, 50.0),
3130 (80.0, 68.0),
3131 (77.0, 82.0),
3132 (65.0, 85.0),
3133 (50.0, 95.0),
3134 (35.0, 85.0),
3135 (23.0, 82.0),
3136 (20.0, 68.0),
3137 (5.0, 50.0),
3138 (20.0, 32.0),
3139 (23.0, 18.0),
3140 (35.0, 15.0),
3141 ])
3142 .with_rotations_deg(vec![0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0])
3143 .with_quantity(13),
3144 ];
3145
3146 let geom_map: HashMap<String, &Geometry2D> =
3148 shapes.iter().map(|g| (g.id().clone(), g)).collect();
3149
3150 let boundary = Boundary2D::rectangle(500.0, 500.0);
3151 let strip_width = 500.0;
3152
3153 let strategies = vec![
3155 Strategy::BottomLeftFill,
3156 Strategy::NfpGuided,
3157 Strategy::GeneticAlgorithm,
3158 Strategy::Brkga,
3159 Strategy::SimulatedAnnealing,
3160 Strategy::Gdrr,
3161 Strategy::Alns,
3162 ];
3163
3164 for strategy in strategies {
3165 println!("\n========== Testing {:?} ==========", strategy);
3166
3167 let config = Config::default()
3168 .with_strategy(strategy)
3169 .with_time_limit(30000); let nester = Nester2D::new(config);
3171
3172 let result = match nester.solve_multi_strip(&shapes, &boundary) {
3173 Ok(r) => r,
3174 Err(e) => {
3175 println!(" Strategy {:?} failed: {}", strategy, e);
3176 continue;
3177 }
3178 };
3179
3180 println!(
3181 " Placed {} pieces across {} strips",
3182 result.placements.len(),
3183 result.boundaries_used
3184 );
3185
3186 let mut boundary_violations = Vec::new();
3188 for p in &result.placements {
3189 let base_id = p.geometry_id.split('_').next().unwrap_or(&p.geometry_id);
3191 let full_id = if base_id.starts_with("shape") {
3192 shapes
3194 .iter()
3195 .find(|g| p.geometry_id.starts_with(g.id()))
3196 .map(|g| g.id().as_str())
3197 } else {
3198 geom_map.get(&p.geometry_id).map(|g| g.id().as_str())
3199 };
3200
3201 let geom = match full_id.and_then(|id| geom_map.get(id)) {
3202 Some(g) => *g,
3203 None => {
3204 match shapes.iter().find(|g| p.geometry_id.starts_with(g.id())) {
3206 Some(g) => g,
3207 None => {
3208 println!(
3209 " WARNING: Could not find geometry for {}",
3210 p.geometry_id
3211 );
3212 continue;
3213 }
3214 }
3215 }
3216 };
3217
3218 let origin_x = p.position[0];
3219 let origin_y = p.position[1];
3220 let rotation = p.rotation.first().copied().unwrap_or(0.0);
3221 let strip_idx = p.boundary_index;
3222
3223 let local_x = origin_x - (strip_idx as f64 * strip_width);
3225
3226 let (g_min, g_max) = geom.aabb_at_rotation(rotation);
3227
3228 let local_min_x = local_x + g_min[0];
3230 let local_max_x = local_x + g_max[0];
3231 let local_min_y = origin_y + g_min[1];
3232 let local_max_y = origin_y + g_max[1];
3233
3234 let tolerance = 0.1;
3236 if local_min_x < -tolerance
3237 || local_max_x > 500.0 + tolerance
3238 || local_min_y < -tolerance
3239 || local_max_y > 500.0 + tolerance
3240 {
3241 boundary_violations.push(format!(
3242 "{} in strip {}: bounds ({:.1}, {:.1}) to ({:.1}, {:.1})",
3243 p.geometry_id,
3244 strip_idx,
3245 local_min_x,
3246 local_min_y,
3247 local_max_x,
3248 local_max_y
3249 ));
3250 }
3251 }
3252
3253 if !boundary_violations.is_empty() {
3254 println!(" BOUNDARY VIOLATIONS ({}):", boundary_violations.len());
3255 for v in &boundary_violations {
3256 println!(" - {}", v);
3257 }
3258 }
3259
3260 let mut overlaps = Vec::new();
3262 let placements: Vec<_> = result.placements.iter().collect();
3263
3264 for i in 0..placements.len() {
3265 for j in (i + 1)..placements.len() {
3266 let p1 = placements[i];
3267 let p2 = placements[j];
3268
3269 if p1.boundary_index != p2.boundary_index {
3271 continue;
3272 }
3273
3274 let g1 = shapes.iter().find(|g| p1.geometry_id.starts_with(g.id()));
3276 let g2 = shapes.iter().find(|g| p2.geometry_id.starts_with(g.id()));
3277
3278 let (g1, g2) = match (g1, g2) {
3279 (Some(a), Some(b)) => (a, b),
3280 _ => continue,
3281 };
3282
3283 let strip_idx = p1.boundary_index;
3284 let local_x1 = p1.position[0] - (strip_idx as f64 * strip_width);
3285 let local_x2 = p2.position[0] - (strip_idx as f64 * strip_width);
3286
3287 let rot1 = p1.rotation.first().copied().unwrap_or(0.0);
3288 let rot2 = p2.rotation.first().copied().unwrap_or(0.0);
3289
3290 let (g1_min, g1_max) = g1.aabb_at_rotation(rot1);
3291 let (g2_min, g2_max) = g2.aabb_at_rotation(rot2);
3292
3293 let a_min = [local_x1 + g1_min[0], p1.position[1] + g1_min[1]];
3294 let a_max = [local_x1 + g1_max[0], p1.position[1] + g1_max[1]];
3295 let b_min = [local_x2 + g2_min[0], p2.position[1] + g2_min[1]];
3296 let b_max = [local_x2 + g2_max[0], p2.position[1] + g2_max[1]];
3297
3298 if aabbs_overlap(a_min, a_max, b_min, b_max, 1.0) {
3299 overlaps.push(format!(
3300 "{} and {} in strip {}",
3301 p1.geometry_id, p2.geometry_id, strip_idx
3302 ));
3303 }
3304 }
3305 }
3306
3307 if !overlaps.is_empty() {
3308 println!(" OVERLAPS ({}):", overlaps.len());
3309 for o in overlaps.iter().take(10) {
3310 println!(" - {}", o);
3311 }
3312 if overlaps.len() > 10 {
3313 println!(" ... and {} more", overlaps.len() - 10);
3314 }
3315 }
3316
3317 assert!(
3319 boundary_violations.is_empty(),
3320 "{:?}: Found {} boundary violations",
3321 strategy,
3322 boundary_violations.len()
3323 );
3324
3325 println!(" ✓ All placements within boundary");
3326 println!(" ✓ No AABB overlaps detected");
3327 }
3328 }
3329
3330 #[test]
3338 fn test_multi_strip_distributes_all_instances() {
3339 let geometries = vec![Geometry2D::rectangle("part", 100.0, 100.0).with_quantity(20)];
3340 let boundary = Boundary2D::rectangle(300.0, 300.0);
3341 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3342 let nester = Nester2D::new(config);
3343
3344 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3345
3346 assert_eq!(
3348 result.placements.len(),
3349 20,
3350 "all 20 instances must be placed"
3351 );
3352 assert_eq!(
3353 result.boundaries_used, 3,
3354 "20 of 100x100 in 300x300 => 3 sheets"
3355 );
3356 assert!(
3357 result.unplaced.is_empty(),
3358 "nothing should be unplaced, got {:?}",
3359 result.unplaced
3360 );
3361 assert_eq!(result.total_requested, 20);
3363
3364 let mut seen = std::collections::HashSet::new();
3367 for p in &result.placements {
3368 assert!(
3369 seen.insert((p.geometry_id.clone(), p.instance)),
3370 "duplicate (id, instance) = ({}, {}) across sheets",
3371 p.geometry_id,
3372 p.instance
3373 );
3374 }
3375 let mut sheets: Vec<usize> = result.placements.iter().map(|p| p.boundary_index).collect();
3377 sheets.sort_unstable();
3378 sheets.dedup();
3379 assert_eq!(sheets, vec![0, 1, 2]);
3380 }
3381
3382 #[test]
3385 fn test_multi_strip_oversized_reported_unplaced() {
3386 let geometries = vec![
3387 Geometry2D::rectangle("ok", 50.0, 50.0).with_quantity(2),
3388 Geometry2D::rectangle("toobig", 400.0, 400.0).with_quantity(3),
3389 ];
3390 let boundary = Boundary2D::rectangle(300.0, 300.0);
3391 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
3392 let nester = Nester2D::new(config);
3393
3394 let result = nester.solve_multi_strip(&geometries, &boundary).unwrap();
3395
3396 assert_eq!(result.total_requested, 5, "2 + 3 instances requested");
3397 assert_eq!(result.placements.len(), 2);
3399 assert!(
3400 result.unplaced.contains(&"toobig".to_string()),
3401 "oversized geometry must surface in unplaced, got {:?}",
3402 result.unplaced
3403 );
3404 }
3405}