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 nalgebra::{Point3, Vector3};
12use u_nesting_core::brkga::BrkgaConfig;
13use u_nesting_core::ga::GaConfig;
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 std::time::Instant;
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 = Instant::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().as_millis() as u64;
146 return Ok(result);
147 }
148
149 if self.config.time_limit_ms > 0
151 && start.elapsed().as_millis() as u64 >= self.config.time_limit_ms
152 {
153 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
154 result.utilization = total_placed_volume / boundary.measure();
155 result.computation_time_ms = start.elapsed().as_millis() as u64;
156 result.placements = placements;
157 return Ok(result);
158 }
159
160 if let (Some(max_mass), Some(item_mass)) = (boundary.max_mass(), geom.mass()) {
162 if total_placed_mass + item_mass > max_mass {
163 result.unplaced.push(geom.id().clone());
164 continue;
165 }
166 }
167
168 let orientations = geom.allowed_orientations();
170 let mut best_fit: BestFit3D = None;
171 for (ori_idx, _) in orientations.iter().enumerate() {
174 let dims = geom.dimensions_for_orientation(ori_idx);
175 let g_width = dims.x;
176 let g_depth = dims.y;
177 let g_height = dims.z;
178
179 let mut try_x = current_x;
181 let mut try_y = current_y;
182 let mut try_z = current_z;
183 let mut try_row_depth = row_depth;
184 let mut try_layer_height = layer_height;
185
186 if try_x + g_width > bound_max_x {
188 try_x = margin;
189 try_y += row_depth + spacing;
190 try_row_depth = 0.0;
191 }
192
193 if try_y + g_depth > bound_max_y {
195 try_x = margin;
196 try_y = margin;
197 try_z += layer_height + spacing;
198 try_row_depth = 0.0;
199 try_layer_height = 0.0;
200 }
201
202 if try_z + g_height > bound_max_z {
204 continue; }
206
207 let score = try_z * 1000000.0 + try_y * 1000.0 + try_x + g_height * 0.1;
210
211 let is_better = match &best_fit {
212 None => true,
213 Some((_, _, _, _, _, _, bz, _, _)) => {
214 let best_score = bz * 1000000.0
215 + best_fit.as_ref().unwrap().5 * 1000.0
216 + best_fit.as_ref().unwrap().4
217 + best_fit.as_ref().unwrap().3 * 0.1;
218 score < best_score
219 }
220 };
221
222 if is_better {
223 best_fit = Some((
224 ori_idx,
225 g_width,
226 g_depth,
227 g_height,
228 try_x,
229 try_y,
230 try_z,
231 try_row_depth,
232 try_layer_height,
233 ));
234 }
235 }
236
237 if let Some((
238 ori_idx,
239 g_width,
240 g_depth,
241 g_height,
242 place_x,
243 place_y,
244 place_z,
245 new_row_depth,
246 new_layer_height,
247 )) = best_fit
248 {
249 let placement = Placement::new_3d(
252 geom.id().clone(),
253 instance,
254 place_x,
255 place_y,
256 place_z,
257 0.0, 0.0,
259 0.0,
260 )
261 .with_rotation_index(ori_idx);
262
263 placements.push(placement);
264 total_placed_volume += geom.measure();
265 if let Some(mass) = geom.mass() {
266 total_placed_mass += mass;
267 }
268
269 current_x = place_x + g_width + spacing;
271 current_y = place_y;
272 current_z = place_z;
273 row_depth = new_row_depth.max(g_depth);
274 layer_height = new_layer_height.max(g_height);
275 } else {
276 result.unplaced.push(geom.id().clone());
277 }
278 }
279 }
280
281 result.placements = placements;
282 result.boundaries_used = 1;
283 result.utilization = total_placed_volume / boundary.measure();
284 result.computation_time_ms = start.elapsed().as_millis() as u64;
285
286 Ok(result)
287 }
288
289 fn genetic_algorithm(
294 &self,
295 geometries: &[Geometry3D],
296 boundary: &Boundary3D,
297 ) -> Result<SolveResult<f64>> {
298 let ga_config = GaConfig::default()
300 .with_population_size(self.config.population_size)
301 .with_max_generations(self.config.max_generations)
302 .with_crossover_rate(self.config.crossover_rate)
303 .with_mutation_rate(self.config.mutation_rate);
304
305 let result = run_ga_packing(
306 geometries,
307 boundary,
308 &self.config,
309 ga_config,
310 self.cancelled.clone(),
311 );
312
313 Ok(result)
314 }
315
316 fn brkga(&self, geometries: &[Geometry3D], boundary: &Boundary3D) -> Result<SolveResult<f64>> {
320 let brkga_config = BrkgaConfig::default()
322 .with_population_size(50)
323 .with_max_generations(100)
324 .with_elite_fraction(0.2)
325 .with_mutant_fraction(0.15)
326 .with_elite_bias(0.7);
327
328 let result = run_brkga_packing(
329 geometries,
330 boundary,
331 &self.config,
332 brkga_config,
333 self.cancelled.clone(),
334 );
335
336 Ok(result)
337 }
338
339 fn simulated_annealing(
344 &self,
345 geometries: &[Geometry3D],
346 boundary: &Boundary3D,
347 ) -> Result<SolveResult<f64>> {
348 let sa_config = SaConfig::default()
350 .with_initial_temp(100.0)
351 .with_final_temp(0.1)
352 .with_cooling_rate(0.95)
353 .with_iterations_per_temp(50)
354 .with_max_iterations(10000);
355
356 let result = run_sa_packing(
357 geometries,
358 boundary,
359 &self.config,
360 sa_config,
361 self.cancelled.clone(),
362 );
363
364 Ok(result)
365 }
366
367 fn extreme_point(
372 &self,
373 geometries: &[Geometry3D],
374 boundary: &Boundary3D,
375 ) -> Result<SolveResult<f64>> {
376 let start = Instant::now();
377
378 let (ep_placements, utilization) = run_ep_packing(
379 geometries,
380 boundary,
381 self.config.margin,
382 self.config.spacing,
383 boundary.max_mass(),
384 );
385
386 let mut placements = Vec::new();
388 for (id, instance, position, _orientation) in ep_placements {
389 let placement = Placement::new_3d(
390 id, instance, position.x, position.y, position.z, 0.0, 0.0, 0.0, );
394 placements.push(placement);
395 }
396
397 let mut placed_ids: std::collections::HashSet<(String, usize)> =
399 std::collections::HashSet::new();
400 for p in &placements {
401 placed_ids.insert((p.geometry_id.clone(), p.instance));
402 }
403
404 let mut unplaced = Vec::new();
405 for geom in geometries {
406 for instance in 0..geom.quantity() {
407 if !placed_ids.contains(&(geom.id().clone(), instance)) {
408 unplaced.push(geom.id().clone());
409 }
410 }
411 }
412
413 let mut result = SolveResult::new();
414 result.placements = placements;
415 result.boundaries_used = 1;
416 result.utilization = utilization;
417 result.unplaced = unplaced;
418 result.computation_time_ms = start.elapsed().as_millis() as u64;
419 result.strategy = Some("ExtremePoint".to_string());
420
421 Ok(result)
422 }
423
424 fn layer_packing_with_progress(
426 &self,
427 geometries: &[Geometry3D],
428 boundary: &Boundary3D,
429 callback: &ProgressCallback,
430 ) -> Result<SolveResult<f64>> {
431 let start = Instant::now();
432 let mut result = SolveResult::new();
433 let mut placements = Vec::new();
434
435 let margin = self.config.margin;
436 let spacing = self.config.spacing;
437
438 let bound_max_x = boundary.width() - margin;
439 let bound_max_y = boundary.depth() - margin;
440 let bound_max_z = boundary.height() - margin;
441
442 let mut current_x = margin;
443 let mut current_y = margin;
444 let mut current_z = margin;
445 let mut row_depth = 0.0_f64;
446 let mut layer_height = 0.0_f64;
447
448 let mut total_placed_volume = 0.0;
449 let mut total_placed_mass = 0.0;
450
451 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
453 let mut placed_count = 0usize;
454
455 callback(
457 ProgressInfo::new()
458 .with_phase("Layer Packing")
459 .with_items(0, total_pieces)
460 .with_elapsed(0),
461 );
462
463 for geom in geometries {
464 geom.validate()?;
465
466 for instance in 0..geom.quantity() {
467 if self.cancelled.load(Ordering::Relaxed) {
468 result.computation_time_ms = start.elapsed().as_millis() as u64;
469 callback(
470 ProgressInfo::new()
471 .with_phase("Cancelled")
472 .with_items(placed_count, total_pieces)
473 .with_elapsed(result.computation_time_ms)
474 .finished(),
475 );
476 return Ok(result);
477 }
478
479 if self.config.time_limit_ms > 0
481 && start.elapsed().as_millis() as u64 >= self.config.time_limit_ms
482 {
483 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
484 result.utilization = total_placed_volume / boundary.measure();
485 result.computation_time_ms = start.elapsed().as_millis() as u64;
486 result.placements = placements;
487 callback(
488 ProgressInfo::new()
489 .with_phase("Time Limit Reached")
490 .with_items(placed_count, total_pieces)
491 .with_elapsed(result.computation_time_ms)
492 .finished(),
493 );
494 return Ok(result);
495 }
496
497 if let (Some(max_mass), Some(item_mass)) = (boundary.max_mass(), geom.mass()) {
499 if total_placed_mass + item_mass > max_mass {
500 result.unplaced.push(geom.id().clone());
501 continue;
502 }
503 }
504
505 let orientations = geom.allowed_orientations();
507 let mut best_fit: BestFit3D = None;
508
509 for (ori_idx, _) in orientations.iter().enumerate() {
510 let dims = geom.dimensions_for_orientation(ori_idx);
511 let g_width = dims.x;
512 let g_depth = dims.y;
513 let g_height = dims.z;
514
515 let mut try_x = current_x;
516 let mut try_y = current_y;
517 let mut try_z = current_z;
518 let mut try_row_depth = row_depth;
519 let mut try_layer_height = layer_height;
520
521 if try_x + g_width > bound_max_x {
522 try_x = margin;
523 try_y += row_depth + spacing;
524 try_row_depth = 0.0;
525 }
526
527 if try_y + g_depth > bound_max_y {
528 try_x = margin;
529 try_y = margin;
530 try_z += layer_height + spacing;
531 try_row_depth = 0.0;
532 try_layer_height = 0.0;
533 }
534
535 if try_z + g_height > bound_max_z {
536 continue;
537 }
538
539 let score = try_z * 1000000.0 + try_y * 1000.0 + try_x + g_height * 0.1;
540
541 let is_better = match &best_fit {
542 None => true,
543 Some((_, _, _, _, _, _, bz, _, _)) => {
544 let best_score = bz * 1000000.0
545 + best_fit.as_ref().unwrap().5 * 1000.0
546 + best_fit.as_ref().unwrap().4
547 + best_fit.as_ref().unwrap().3 * 0.1;
548 score < best_score
549 }
550 };
551
552 if is_better {
553 best_fit = Some((
554 ori_idx,
555 g_width,
556 g_depth,
557 g_height,
558 try_x,
559 try_y,
560 try_z,
561 try_row_depth,
562 try_layer_height,
563 ));
564 }
565 }
566
567 if let Some((
568 ori_idx,
569 g_width,
570 g_depth,
571 g_height,
572 place_x,
573 place_y,
574 place_z,
575 new_row_depth,
576 new_layer_height,
577 )) = best_fit
578 {
579 let placement = Placement::new_3d(
580 geom.id().clone(),
581 instance,
582 place_x,
583 place_y,
584 place_z,
585 0.0,
586 0.0,
587 0.0,
588 )
589 .with_rotation_index(ori_idx);
590
591 placements.push(placement);
592 total_placed_volume += geom.measure();
593 if let Some(mass) = geom.mass() {
594 total_placed_mass += mass;
595 }
596 placed_count += 1;
597
598 current_x = place_x + g_width + spacing;
599 current_y = place_y;
600 current_z = place_z;
601 row_depth = new_row_depth.max(g_depth);
602 layer_height = new_layer_height.max(g_height);
603
604 callback(
606 ProgressInfo::new()
607 .with_phase("Layer Packing")
608 .with_items(placed_count, total_pieces)
609 .with_utilization(total_placed_volume / boundary.measure())
610 .with_elapsed(start.elapsed().as_millis() as u64),
611 );
612 } else {
613 result.unplaced.push(geom.id().clone());
614 }
615 }
616 }
617
618 result.placements = placements;
619 result.boundaries_used = 1;
620 result.utilization = total_placed_volume / boundary.measure();
621 result.computation_time_ms = start.elapsed().as_millis() as u64;
622
623 callback(
625 ProgressInfo::new()
626 .with_phase("Complete")
627 .with_items(placed_count, total_pieces)
628 .with_utilization(result.utilization)
629 .with_elapsed(result.computation_time_ms)
630 .finished(),
631 );
632
633 Ok(result)
634 }
635}
636
637impl Solver for Packer3D {
638 type Geometry = Geometry3D;
639 type Boundary = Boundary3D;
640 type Scalar = f64;
641
642 fn solve(
643 &self,
644 geometries: &[Self::Geometry],
645 boundary: &Self::Boundary,
646 ) -> Result<SolveResult<f64>> {
647 boundary.validate()?;
648
649 self.cancelled.store(false, Ordering::Relaxed);
651
652 let mut result = match self.config.strategy {
653 Strategy::BottomLeftFill => self.layer_packing(geometries, boundary),
654 Strategy::ExtremePoint => self.extreme_point(geometries, boundary),
655 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
656 Strategy::Brkga => self.brkga(geometries, boundary),
657 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
658 _ => {
659 log::warn!(
661 "Strategy {:?} not yet implemented, using layer packing",
662 self.config.strategy
663 );
664 self.layer_packing(geometries, boundary)
665 }
666 }?;
667
668 result.deduplicate_unplaced();
670 Ok(result)
671 }
672
673 fn solve_with_progress(
674 &self,
675 geometries: &[Self::Geometry],
676 boundary: &Self::Boundary,
677 callback: ProgressCallback,
678 ) -> Result<SolveResult<f64>> {
679 boundary.validate()?;
680
681 self.cancelled.store(false, Ordering::Relaxed);
683
684 let mut result = match self.config.strategy {
685 Strategy::BottomLeftFill | Strategy::ExtremePoint => {
686 self.layer_packing_with_progress(geometries, boundary, &callback)?
687 }
688 _ => {
690 log::warn!(
691 "Strategy {:?} progress not yet implemented, using layer packing",
692 self.config.strategy
693 );
694 self.layer_packing_with_progress(geometries, boundary, &callback)?
695 }
696 };
697
698 result.deduplicate_unplaced();
700 Ok(result)
701 }
702
703 fn cancel(&self) {
704 self.cancelled.store(true, Ordering::Relaxed);
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711
712 #[test]
713 fn test_simple_packing() {
714 let geometries = vec![
715 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(3),
716 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
717 ];
718
719 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
720 let packer = Packer3D::default_config();
721
722 let result = packer.solve(&geometries, &boundary).unwrap();
723
724 assert!(result.utilization > 0.0);
725 assert!(result.placements.len() <= 5);
726 }
727
728 #[test]
729 fn test_mass_constraint() {
730 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)
731 .with_quantity(10)
732 .with_mass(100.0)];
733
734 let boundary = Boundary3D::new(100.0, 80.0, 50.0).with_max_mass(350.0);
735
736 let packer = Packer3D::default_config();
737 let result = packer.solve(&geometries, &boundary).unwrap();
738
739 assert!(result.placements.len() <= 3);
741 }
742
743 #[test]
744 fn test_placement_within_bounds() {
745 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
746
747 let boundary = Boundary3D::new(50.0, 50.0, 50.0);
748 let config = Config::default().with_margin(5.0).with_spacing(2.0);
749 let packer = Packer3D::new(config);
750
751 let result = packer.solve(&geometries, &boundary).unwrap();
752
753 assert_eq!(result.placements.len(), 4);
755 assert!(result.unplaced.is_empty());
756
757 for p in &result.placements {
759 assert!(p.position[0] >= 5.0);
760 assert!(p.position[1] >= 5.0);
761 assert!(p.position[2] >= 5.0);
762 }
763 }
764
765 #[test]
766 fn test_ga_strategy_basic() {
767 let geometries = vec![
768 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
769 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
770 ];
771
772 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
773 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
774 let packer = Packer3D::new(config);
775
776 let result = packer.solve(&geometries, &boundary).unwrap();
777
778 assert!(result.utilization > 0.0);
780 assert!(!result.placements.is_empty());
781 }
782
783 #[test]
784 fn test_ga_strategy_all_placed() {
785 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
787
788 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
789 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
790 let packer = Packer3D::new(config);
791
792 let result = packer.solve(&geometries, &boundary).unwrap();
793
794 assert_eq!(result.placements.len(), 4);
796 assert!(result.unplaced.is_empty());
797 }
798
799 #[test]
800 fn test_ga_strategy_with_orientations() {
801 use crate::geometry::OrientationConstraint;
802
803 let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
805 .with_quantity(2)
806 .with_orientation(OrientationConstraint::Any)];
807
808 let boundary = Boundary3D::new(60.0, 60.0, 60.0);
810 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
811 let packer = Packer3D::new(config);
812
813 let result = packer.solve(&geometries, &boundary).unwrap();
814
815 assert_eq!(result.placements.len(), 2);
817 }
818
819 #[test]
820 fn test_brkga_strategy_basic() {
821 let geometries = vec![
822 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
823 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
824 ];
825
826 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
827 let config = Config::default().with_strategy(Strategy::Brkga);
828 let packer = Packer3D::new(config);
829
830 let result = packer.solve(&geometries, &boundary).unwrap();
831
832 assert!(result.utilization > 0.0);
834 assert!(!result.placements.is_empty());
835 assert_eq!(result.strategy, Some("BRKGA".to_string()));
836 }
837
838 #[test]
839 fn test_brkga_strategy_all_placed() {
840 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
842
843 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
844 let config = Config::default().with_strategy(Strategy::Brkga);
845 let packer = Packer3D::new(config);
846
847 let result = packer.solve(&geometries, &boundary).unwrap();
848
849 assert_eq!(result.placements.len(), 4);
851 assert!(result.unplaced.is_empty());
852 }
853
854 #[test]
855 fn test_ep_strategy_basic() {
856 let geometries = vec![
857 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
858 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
859 ];
860
861 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
862 let config = Config::default().with_strategy(Strategy::ExtremePoint);
863 let packer = Packer3D::new(config);
864
865 let result = packer.solve(&geometries, &boundary).unwrap();
866
867 assert!(result.utilization > 0.0);
869 assert!(!result.placements.is_empty());
870 assert_eq!(result.strategy, Some("ExtremePoint".to_string()));
871 }
872
873 #[test]
874 fn test_ep_strategy_all_placed() {
875 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
877
878 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
879 let config = Config::default().with_strategy(Strategy::ExtremePoint);
880 let packer = Packer3D::new(config);
881
882 let result = packer.solve(&geometries, &boundary).unwrap();
883
884 assert_eq!(result.placements.len(), 4);
886 assert!(result.unplaced.is_empty());
887 }
888
889 #[test]
890 fn test_ep_strategy_with_margin() {
891 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
892
893 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
894 let config = Config::default()
895 .with_strategy(Strategy::ExtremePoint)
896 .with_margin(5.0);
897 let packer = Packer3D::new(config);
898
899 let result = packer.solve(&geometries, &boundary).unwrap();
900
901 for p in &result.placements {
903 assert!(p.position[0] >= 4.9);
904 assert!(p.position[1] >= 4.9);
905 assert!(p.position[2] >= 4.9);
906 }
907 }
908
909 #[test]
910 fn test_ep_strategy_with_orientations() {
911 use crate::geometry::OrientationConstraint;
912
913 let geometries = vec![Geometry3D::new("B1", 80.0, 10.0, 10.0)
915 .with_quantity(2)
916 .with_orientation(OrientationConstraint::Any)];
917
918 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
919 let config = Config::default().with_strategy(Strategy::ExtremePoint);
920 let packer = Packer3D::new(config);
921
922 let result = packer.solve(&geometries, &boundary).unwrap();
923
924 assert_eq!(result.placements.len(), 2);
926 }
927
928 #[test]
929 fn test_layer_packing_orientation_optimization() {
930 use crate::geometry::OrientationConstraint;
931
932 let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
935 .with_quantity(2)
936 .with_orientation(OrientationConstraint::Any)];
937
938 let boundary = Boundary3D::new(45.0, 80.0, 80.0);
940 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
941 let packer = Packer3D::new(config);
942
943 let result = packer.solve(&geometries, &boundary).unwrap();
944
945 assert_eq!(
947 result.placements.len(),
948 2,
949 "Both boxes should be placed by using rotation"
950 );
951 assert!(result.unplaced.is_empty());
952
953 for p in &result.placements {
955 assert!(
956 p.rotation_index.is_some(),
957 "Placement should have rotation_index set"
958 );
959 }
960 }
961
962 #[test]
963 fn test_layer_packing_selects_best_orientation() {
964 use crate::geometry::OrientationConstraint;
965
966 let geometries = vec![Geometry3D::new("B1", 30.0, 20.0, 10.0)
971 .with_quantity(1)
972 .with_orientation(OrientationConstraint::Any)];
973
974 let boundary = Boundary3D::new(35.0, 50.0, 100.0);
975 let packer = Packer3D::default_config();
976
977 let result = packer.solve(&geometries, &boundary).unwrap();
978
979 assert_eq!(result.placements.len(), 1);
980 assert!(result.unplaced.is_empty());
981 }
982}