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