1use crate::boundary::Boundary3D;
4use crate::brkga_packing::run_brkga_packing;
5use crate::extreme_point::run_ep_packing;
6use crate::ga_packing::run_ga_packing;
7use crate::geometry::Geometry3D;
8use crate::physics::{PhysicsConfig, PhysicsSimulator};
9use crate::sa_packing::run_sa_packing;
10use crate::stability::{PlacedBox, StabilityAnalyzer, StabilityConstraint, StabilityReport};
11use u_nesting_core::brkga::BrkgaConfig;
12use u_nesting_core::ga::GaConfig;
13use u_nesting_core::geom::nalgebra_types::{NaPoint3 as Point3, NaVector3 as Vector3};
14use u_nesting_core::geometry::{Boundary, Geometry};
15use u_nesting_core::sa::SaConfig;
16use u_nesting_core::solver::{Config, ProgressCallback, ProgressInfo, Solver, Strategy};
17use u_nesting_core::{Placement, Result, SolveResult};
18
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::Arc;
21use u_nesting_core::timing::Timer;
22
23type BestFit3D = Option<(usize, f64, f64, f64, f64, f64, f64, f64, f64)>;
26
27pub struct Packer3D {
29 config: Config,
30 cancelled: Arc<AtomicBool>,
31}
32
33impl Packer3D {
34 pub fn new(config: Config) -> Self {
36 Self {
37 config,
38 cancelled: Arc::new(AtomicBool::new(false)),
39 }
40 }
41
42 pub fn default_config() -> Self {
44 Self::new(Config::default())
45 }
46
47 pub fn validate_stability(
51 &self,
52 result: &SolveResult<f64>,
53 geometries: &[Geometry3D],
54 _boundary: &Boundary3D,
55 constraint: StabilityConstraint,
56 ) -> StabilityReport {
57 let placed_boxes = self.placements_to_boxes(result, geometries);
59 let analyzer = StabilityAnalyzer::new(constraint);
60 analyzer.analyze(&placed_boxes, 0.0)
61 }
62
63 pub fn validate_stability_physics(
67 &self,
68 result: &SolveResult<f64>,
69 geometries: &[Geometry3D],
70 boundary: &Boundary3D,
71 ) -> StabilityReport {
72 let placed_boxes = self.placements_to_boxes(result, geometries);
73 let container = Vector3::new(boundary.width(), boundary.depth(), boundary.height());
74
75 let config = PhysicsConfig::default().with_max_time(2.0);
76 let simulator = PhysicsSimulator::new(config);
77 simulator.validate_stability(&placed_boxes, container, 0.0)
78 }
79
80 fn placements_to_boxes(
82 &self,
83 result: &SolveResult<f64>,
84 geometries: &[Geometry3D],
85 ) -> Vec<PlacedBox> {
86 let geom_map: std::collections::HashMap<&str, &Geometry3D> =
87 geometries.iter().map(|g| (g.id().as_str(), g)).collect();
88
89 result
90 .placements
91 .iter()
92 .filter_map(|p| {
93 let geom = geom_map.get(p.geometry_id.as_str())?;
94 let ori_idx = p.rotation_index.unwrap_or(0);
95 let dims = geom.dimensions_for_orientation(ori_idx);
96
97 let mut placed = PlacedBox::new(
98 p.geometry_id.clone(),
99 p.instance,
100 Point3::new(p.position[0], p.position[1], p.position[2]),
101 dims,
102 );
103
104 if let Some(mass) = geom.mass() {
105 placed = placed.with_mass(mass);
106 }
107
108 Some(placed)
109 })
110 .collect()
111 }
112
113 fn layer_packing(
115 &self,
116 geometries: &[Geometry3D],
117 boundary: &Boundary3D,
118 ) -> Result<SolveResult<f64>> {
119 let start = Timer::now();
120 let mut result = SolveResult::new();
121 let mut placements = Vec::new();
122
123 let margin = self.config.margin;
124 let spacing = self.config.spacing;
125
126 let bound_max_x = boundary.width() - margin;
127 let bound_max_y = boundary.depth() - margin;
128 let bound_max_z = boundary.height() - margin;
129
130 let mut current_x = margin;
132 let mut current_y = margin;
133 let mut current_z = margin;
134 let mut row_depth = 0.0_f64;
135 let mut layer_height = 0.0_f64;
136
137 let mut total_placed_volume = 0.0;
138 let mut total_placed_mass = 0.0;
139
140 for geom in geometries {
141 geom.validate()?;
142
143 for instance in 0..geom.quantity() {
144 if self.cancelled.load(Ordering::Relaxed) {
145 result.computation_time_ms = start.elapsed_ms();
146 return Ok(result);
147 }
148
149 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
151 {
152 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
153 result.utilization = total_placed_volume / boundary.measure();
154 result.computation_time_ms = start.elapsed_ms();
155 result.placements = placements;
156 return Ok(result);
157 }
158
159 if let (Some(max_mass), Some(item_mass)) = (boundary.max_mass(), geom.mass()) {
161 if total_placed_mass + item_mass > max_mass {
162 result.unplaced.push(geom.id().clone());
163 continue;
164 }
165 }
166
167 let orientations = geom.allowed_orientations();
169 let mut best_fit: BestFit3D = None;
170 for (ori_idx, _) in orientations.iter().enumerate() {
173 let dims = geom.dimensions_for_orientation(ori_idx);
174 let g_width = dims.x;
175 let g_depth = dims.y;
176 let g_height = dims.z;
177
178 let mut try_x = current_x;
180 let mut try_y = current_y;
181 let mut try_z = current_z;
182 let mut try_row_depth = row_depth;
183 let mut try_layer_height = layer_height;
184
185 if try_x + g_width > bound_max_x {
187 try_x = margin;
188 try_y += row_depth + spacing;
189 try_row_depth = 0.0;
190 }
191
192 if try_y + g_depth > bound_max_y {
194 try_x = margin;
195 try_y = margin;
196 try_z += layer_height + spacing;
197 try_row_depth = 0.0;
198 try_layer_height = 0.0;
199 }
200
201 if try_z + g_height > bound_max_z {
203 continue; }
205
206 let score = try_z * 1000000.0 + try_y * 1000.0 + try_x + g_height * 0.1;
209
210 let is_better = match &best_fit {
211 None => true,
212 Some((_, _, _, bg_height, bx, by, bz, _, _)) => {
213 let best_score = bz * 1000000.0 + by * 1000.0 + bx + bg_height * 0.1;
214 score < best_score
215 }
216 };
217
218 if is_better {
219 best_fit = Some((
220 ori_idx,
221 g_width,
222 g_depth,
223 g_height,
224 try_x,
225 try_y,
226 try_z,
227 try_row_depth,
228 try_layer_height,
229 ));
230 }
231 }
232
233 if let Some((
234 ori_idx,
235 g_width,
236 g_depth,
237 g_height,
238 place_x,
239 place_y,
240 place_z,
241 new_row_depth,
242 new_layer_height,
243 )) = best_fit
244 {
245 let placement = Placement::new_3d(
248 geom.id().clone(),
249 instance,
250 place_x,
251 place_y,
252 place_z,
253 0.0, 0.0,
255 0.0,
256 )
257 .with_rotation_index(ori_idx);
258
259 placements.push(placement);
260 total_placed_volume += geom.measure();
261 if let Some(mass) = geom.mass() {
262 total_placed_mass += mass;
263 }
264
265 current_x = place_x + g_width + spacing;
267 current_y = place_y;
268 current_z = place_z;
269 row_depth = new_row_depth.max(g_depth);
270 layer_height = new_layer_height.max(g_height);
271 } else {
272 result.unplaced.push(geom.id().clone());
273 }
274 }
275 }
276
277 result.placements = placements;
278 result.boundaries_used = 1;
279 result.utilization = total_placed_volume / boundary.measure();
280 result.computation_time_ms = start.elapsed_ms();
281
282 Ok(result)
283 }
284
285 fn genetic_algorithm(
290 &self,
291 geometries: &[Geometry3D],
292 boundary: &Boundary3D,
293 ) -> Result<SolveResult<f64>> {
294 let ga_config = GaConfig::default()
296 .with_population_size(self.config.population_size)
297 .with_max_generations(self.config.max_generations)
298 .with_crossover_rate(self.config.crossover_rate)
299 .with_mutation_rate(self.config.mutation_rate);
300
301 let result = run_ga_packing(
302 geometries,
303 boundary,
304 &self.config,
305 ga_config,
306 self.cancelled.clone(),
307 );
308
309 Ok(result)
310 }
311
312 fn brkga(&self, geometries: &[Geometry3D], boundary: &Boundary3D) -> Result<SolveResult<f64>> {
316 let brkga_config = BrkgaConfig::default()
318 .with_population_size(50)
319 .with_max_generations(100)
320 .with_elite_fraction(0.2)
321 .with_mutant_fraction(0.15)
322 .with_elite_bias(0.7);
323
324 let result = run_brkga_packing(
325 geometries,
326 boundary,
327 &self.config,
328 brkga_config,
329 self.cancelled.clone(),
330 );
331
332 Ok(result)
333 }
334
335 fn simulated_annealing(
340 &self,
341 geometries: &[Geometry3D],
342 boundary: &Boundary3D,
343 ) -> Result<SolveResult<f64>> {
344 let sa_config = SaConfig::default()
346 .with_initial_temp(100.0)
347 .with_final_temp(0.1)
348 .with_cooling_rate(0.95)
349 .with_iterations_per_temp(50)
350 .with_max_iterations(10000);
351
352 let result = run_sa_packing(
353 geometries,
354 boundary,
355 &self.config,
356 sa_config,
357 self.cancelled.clone(),
358 );
359
360 Ok(result)
361 }
362
363 fn extreme_point(
368 &self,
369 geometries: &[Geometry3D],
370 boundary: &Boundary3D,
371 ) -> Result<SolveResult<f64>> {
372 let start = Timer::now();
373
374 let (ep_placements, utilization) = run_ep_packing(
375 geometries,
376 boundary,
377 self.config.margin,
378 self.config.spacing,
379 boundary.max_mass(),
380 );
381
382 let mut placements = Vec::new();
384 for (id, instance, position, _orientation) in ep_placements {
385 let placement = Placement::new_3d(
386 id, instance, position.x, position.y, position.z, 0.0, 0.0, 0.0, );
390 placements.push(placement);
391 }
392
393 let mut placed_ids: std::collections::HashSet<(String, usize)> =
395 std::collections::HashSet::new();
396 for p in &placements {
397 placed_ids.insert((p.geometry_id.clone(), p.instance));
398 }
399
400 let mut unplaced = Vec::new();
401 for geom in geometries {
402 for instance in 0..geom.quantity() {
403 if !placed_ids.contains(&(geom.id().clone(), instance)) {
404 unplaced.push(geom.id().clone());
405 }
406 }
407 }
408
409 let mut result = SolveResult::new();
410 result.placements = placements;
411 result.boundaries_used = 1;
412 result.utilization = utilization;
413 result.unplaced = unplaced;
414 result.computation_time_ms = start.elapsed_ms();
415 result.strategy = Some("ExtremePoint".to_string());
416
417 Ok(result)
418 }
419
420 fn layer_packing_with_progress(
422 &self,
423 geometries: &[Geometry3D],
424 boundary: &Boundary3D,
425 callback: &ProgressCallback,
426 ) -> Result<SolveResult<f64>> {
427 let start = Timer::now();
428 let mut result = SolveResult::new();
429 let mut placements = Vec::new();
430
431 let margin = self.config.margin;
432 let spacing = self.config.spacing;
433
434 let bound_max_x = boundary.width() - margin;
435 let bound_max_y = boundary.depth() - margin;
436 let bound_max_z = boundary.height() - margin;
437
438 let mut current_x = margin;
439 let mut current_y = margin;
440 let mut current_z = margin;
441 let mut row_depth = 0.0_f64;
442 let mut layer_height = 0.0_f64;
443
444 let mut total_placed_volume = 0.0;
445 let mut total_placed_mass = 0.0;
446
447 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
449 let mut placed_count = 0usize;
450
451 callback(
453 ProgressInfo::new()
454 .with_phase("Layer Packing")
455 .with_items(0, total_pieces)
456 .with_elapsed(0),
457 );
458
459 for geom in geometries {
460 geom.validate()?;
461
462 for instance in 0..geom.quantity() {
463 if self.cancelled.load(Ordering::Relaxed) {
464 result.computation_time_ms = start.elapsed_ms();
465 callback(
466 ProgressInfo::new()
467 .with_phase("Cancelled")
468 .with_items(placed_count, total_pieces)
469 .with_elapsed(result.computation_time_ms)
470 .finished(),
471 );
472 return Ok(result);
473 }
474
475 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
477 {
478 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
479 result.utilization = total_placed_volume / boundary.measure();
480 result.computation_time_ms = start.elapsed_ms();
481 result.placements = placements;
482 callback(
483 ProgressInfo::new()
484 .with_phase("Time Limit Reached")
485 .with_items(placed_count, total_pieces)
486 .with_elapsed(result.computation_time_ms)
487 .finished(),
488 );
489 return Ok(result);
490 }
491
492 if let (Some(max_mass), Some(item_mass)) = (boundary.max_mass(), geom.mass()) {
494 if total_placed_mass + item_mass > max_mass {
495 result.unplaced.push(geom.id().clone());
496 continue;
497 }
498 }
499
500 let orientations = geom.allowed_orientations();
502 let mut best_fit: BestFit3D = None;
503
504 for (ori_idx, _) in orientations.iter().enumerate() {
505 let dims = geom.dimensions_for_orientation(ori_idx);
506 let g_width = dims.x;
507 let g_depth = dims.y;
508 let g_height = dims.z;
509
510 let mut try_x = current_x;
511 let mut try_y = current_y;
512 let mut try_z = current_z;
513 let mut try_row_depth = row_depth;
514 let mut try_layer_height = layer_height;
515
516 if try_x + g_width > bound_max_x {
517 try_x = margin;
518 try_y += row_depth + spacing;
519 try_row_depth = 0.0;
520 }
521
522 if try_y + g_depth > bound_max_y {
523 try_x = margin;
524 try_y = margin;
525 try_z += layer_height + spacing;
526 try_row_depth = 0.0;
527 try_layer_height = 0.0;
528 }
529
530 if try_z + g_height > bound_max_z {
531 continue;
532 }
533
534 let score = try_z * 1000000.0 + try_y * 1000.0 + try_x + g_height * 0.1;
535
536 let is_better = match &best_fit {
537 None => true,
538 Some((_, _, _, bg_height, bx, by, bz, _, _)) => {
539 let best_score = bz * 1000000.0 + by * 1000.0 + bx + bg_height * 0.1;
540 score < best_score
541 }
542 };
543
544 if is_better {
545 best_fit = Some((
546 ori_idx,
547 g_width,
548 g_depth,
549 g_height,
550 try_x,
551 try_y,
552 try_z,
553 try_row_depth,
554 try_layer_height,
555 ));
556 }
557 }
558
559 if let Some((
560 ori_idx,
561 g_width,
562 g_depth,
563 g_height,
564 place_x,
565 place_y,
566 place_z,
567 new_row_depth,
568 new_layer_height,
569 )) = best_fit
570 {
571 let placement = Placement::new_3d(
572 geom.id().clone(),
573 instance,
574 place_x,
575 place_y,
576 place_z,
577 0.0,
578 0.0,
579 0.0,
580 )
581 .with_rotation_index(ori_idx);
582
583 placements.push(placement);
584 total_placed_volume += geom.measure();
585 if let Some(mass) = geom.mass() {
586 total_placed_mass += mass;
587 }
588 placed_count += 1;
589
590 current_x = place_x + g_width + spacing;
591 current_y = place_y;
592 current_z = place_z;
593 row_depth = new_row_depth.max(g_depth);
594 layer_height = new_layer_height.max(g_height);
595
596 callback(
598 ProgressInfo::new()
599 .with_phase("Layer Packing")
600 .with_items(placed_count, total_pieces)
601 .with_utilization(total_placed_volume / boundary.measure())
602 .with_elapsed(start.elapsed_ms()),
603 );
604 } else {
605 result.unplaced.push(geom.id().clone());
606 }
607 }
608 }
609
610 result.placements = placements;
611 result.boundaries_used = 1;
612 result.utilization = total_placed_volume / boundary.measure();
613 result.computation_time_ms = start.elapsed_ms();
614
615 callback(
617 ProgressInfo::new()
618 .with_phase("Complete")
619 .with_items(placed_count, total_pieces)
620 .with_utilization(result.utilization)
621 .with_elapsed(result.computation_time_ms)
622 .finished(),
623 );
624
625 Ok(result)
626 }
627}
628
629impl Solver for Packer3D {
630 type Geometry = Geometry3D;
631 type Boundary = Boundary3D;
632 type Scalar = f64;
633
634 fn solve(
635 &self,
636 geometries: &[Self::Geometry],
637 boundary: &Self::Boundary,
638 ) -> Result<SolveResult<f64>> {
639 boundary.validate()?;
640
641 self.cancelled.store(false, Ordering::Relaxed);
643
644 let mut result = match self.config.strategy {
645 Strategy::BottomLeftFill => self.layer_packing(geometries, boundary),
646 Strategy::ExtremePoint => self.extreme_point(geometries, boundary),
647 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
648 Strategy::Brkga => self.brkga(geometries, boundary),
649 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
650 _ => {
651 log::warn!(
653 "Strategy {:?} not yet implemented, using layer packing",
654 self.config.strategy
655 );
656 self.layer_packing(geometries, boundary)
657 }
658 }?;
659
660 result.deduplicate_unplaced();
662 Ok(result)
663 }
664
665 fn solve_with_progress(
666 &self,
667 geometries: &[Self::Geometry],
668 boundary: &Self::Boundary,
669 callback: ProgressCallback,
670 ) -> Result<SolveResult<f64>> {
671 boundary.validate()?;
672
673 self.cancelled.store(false, Ordering::Relaxed);
675
676 let mut result = match self.config.strategy {
677 Strategy::BottomLeftFill | Strategy::ExtremePoint => {
678 self.layer_packing_with_progress(geometries, boundary, &callback)?
679 }
680 _ => {
682 log::warn!(
683 "Strategy {:?} progress not yet implemented, using layer packing",
684 self.config.strategy
685 );
686 self.layer_packing_with_progress(geometries, boundary, &callback)?
687 }
688 };
689
690 result.deduplicate_unplaced();
692 Ok(result)
693 }
694
695 fn cancel(&self) {
696 self.cancelled.store(true, Ordering::Relaxed);
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703
704 #[test]
705 fn test_simple_packing() {
706 let geometries = vec![
707 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(3),
708 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
709 ];
710
711 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
712 let packer = Packer3D::default_config();
713
714 let result = packer.solve(&geometries, &boundary).unwrap();
715
716 assert!(result.utilization > 0.0);
717 assert!(result.placements.len() <= 5);
718 }
719
720 #[test]
721 fn test_mass_constraint() {
722 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)
723 .with_quantity(10)
724 .with_mass(100.0)];
725
726 let boundary = Boundary3D::new(100.0, 80.0, 50.0).with_max_mass(350.0);
727
728 let packer = Packer3D::default_config();
729 let result = packer.solve(&geometries, &boundary).unwrap();
730
731 assert!(result.placements.len() <= 3);
733 }
734
735 #[test]
736 fn test_placement_within_bounds() {
737 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
738
739 let boundary = Boundary3D::new(50.0, 50.0, 50.0);
740 let config = Config::default().with_margin(5.0).with_spacing(2.0);
741 let packer = Packer3D::new(config);
742
743 let result = packer.solve(&geometries, &boundary).unwrap();
744
745 assert_eq!(result.placements.len(), 4);
747 assert!(result.unplaced.is_empty());
748
749 for p in &result.placements {
751 assert!(p.position[0] >= 5.0);
752 assert!(p.position[1] >= 5.0);
753 assert!(p.position[2] >= 5.0);
754 }
755 }
756
757 #[test]
758 fn test_ga_strategy_basic() {
759 let geometries = vec![
760 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
761 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
762 ];
763
764 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
765 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
766 let packer = Packer3D::new(config);
767
768 let result = packer.solve(&geometries, &boundary).unwrap();
769
770 assert!(result.utilization > 0.0);
772 assert!(!result.placements.is_empty());
773 }
774
775 #[test]
776 fn test_ga_strategy_all_placed() {
777 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
779
780 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
781 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
782 let packer = Packer3D::new(config);
783
784 let result = packer.solve(&geometries, &boundary).unwrap();
785
786 assert_eq!(result.placements.len(), 4);
788 assert!(result.unplaced.is_empty());
789 }
790
791 #[test]
792 fn test_ga_strategy_with_orientations() {
793 use crate::geometry::OrientationConstraint;
794
795 let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
797 .with_quantity(2)
798 .with_orientation(OrientationConstraint::Any)];
799
800 let boundary = Boundary3D::new(60.0, 60.0, 60.0);
802 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
803 let packer = Packer3D::new(config);
804
805 let result = packer.solve(&geometries, &boundary).unwrap();
806
807 assert_eq!(result.placements.len(), 2);
809 }
810
811 #[test]
812 fn test_brkga_strategy_basic() {
813 let geometries = vec![
814 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
815 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
816 ];
817
818 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
819 let config = Config::default().with_strategy(Strategy::Brkga);
820 let packer = Packer3D::new(config);
821
822 let result = packer.solve(&geometries, &boundary).unwrap();
823
824 assert!(result.utilization > 0.0);
826 assert!(!result.placements.is_empty());
827 assert_eq!(result.strategy, Some("BRKGA".to_string()));
828 }
829
830 #[test]
831 fn test_brkga_strategy_all_placed() {
832 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
834
835 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
836 let config = Config::default().with_strategy(Strategy::Brkga);
837 let packer = Packer3D::new(config);
838
839 let result = packer.solve(&geometries, &boundary).unwrap();
840
841 assert_eq!(result.placements.len(), 4);
843 assert!(result.unplaced.is_empty());
844 }
845
846 #[test]
847 fn test_ep_strategy_basic() {
848 let geometries = vec![
849 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
850 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
851 ];
852
853 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
854 let config = Config::default().with_strategy(Strategy::ExtremePoint);
855 let packer = Packer3D::new(config);
856
857 let result = packer.solve(&geometries, &boundary).unwrap();
858
859 assert!(result.utilization > 0.0);
861 assert!(!result.placements.is_empty());
862 assert_eq!(result.strategy, Some("ExtremePoint".to_string()));
863 }
864
865 #[test]
866 fn test_ep_strategy_all_placed() {
867 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
869
870 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
871 let config = Config::default().with_strategy(Strategy::ExtremePoint);
872 let packer = Packer3D::new(config);
873
874 let result = packer.solve(&geometries, &boundary).unwrap();
875
876 assert_eq!(result.placements.len(), 4);
878 assert!(result.unplaced.is_empty());
879 }
880
881 #[test]
882 fn test_ep_strategy_with_margin() {
883 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
884
885 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
886 let config = Config::default()
887 .with_strategy(Strategy::ExtremePoint)
888 .with_margin(5.0);
889 let packer = Packer3D::new(config);
890
891 let result = packer.solve(&geometries, &boundary).unwrap();
892
893 for p in &result.placements {
895 assert!(p.position[0] >= 4.9);
896 assert!(p.position[1] >= 4.9);
897 assert!(p.position[2] >= 4.9);
898 }
899 }
900
901 #[test]
902 fn test_ep_strategy_with_orientations() {
903 use crate::geometry::OrientationConstraint;
904
905 let geometries = vec![Geometry3D::new("B1", 80.0, 10.0, 10.0)
907 .with_quantity(2)
908 .with_orientation(OrientationConstraint::Any)];
909
910 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
911 let config = Config::default().with_strategy(Strategy::ExtremePoint);
912 let packer = Packer3D::new(config);
913
914 let result = packer.solve(&geometries, &boundary).unwrap();
915
916 assert_eq!(result.placements.len(), 2);
918 }
919
920 #[test]
921 fn test_layer_packing_orientation_optimization() {
922 use crate::geometry::OrientationConstraint;
923
924 let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
927 .with_quantity(2)
928 .with_orientation(OrientationConstraint::Any)];
929
930 let boundary = Boundary3D::new(45.0, 80.0, 80.0);
932 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
933 let packer = Packer3D::new(config);
934
935 let result = packer.solve(&geometries, &boundary).unwrap();
936
937 assert_eq!(
939 result.placements.len(),
940 2,
941 "Both boxes should be placed by using rotation"
942 );
943 assert!(result.unplaced.is_empty());
944
945 for p in &result.placements {
947 assert!(
948 p.rotation_index.is_some(),
949 "Placement should have rotation_index set"
950 );
951 }
952 }
953
954 #[test]
955 fn test_layer_packing_selects_best_orientation() {
956 use crate::geometry::OrientationConstraint;
957
958 let geometries = vec![Geometry3D::new("B1", 30.0, 20.0, 10.0)
963 .with_quantity(1)
964 .with_orientation(OrientationConstraint::Any)];
965
966 let boundary = Boundary3D::new(35.0, 50.0, 100.0);
967 let packer = Packer3D::default_config();
968
969 let result = packer.solve(&geometries, &boundary).unwrap();
970
971 assert_eq!(result.placements.len(), 1);
972 assert!(result.unplaced.is_empty());
973 }
974}