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 enforce_support(
91 &self,
92 result: &mut SolveResult<f64>,
93 geometries: &[Geometry3D],
94 boundary: &Boundary3D,
95 ) {
96 let constraint = if boundary.has_stability() {
99 StabilityConstraint::partial_base(0.7)
100 } else {
101 StabilityConstraint::partial_base(1e-6)
102 };
103
104 let floor_z = self.config.margin;
108 let analyzer = StabilityAnalyzer::new(constraint);
109 while !result.placements.is_empty() {
110 let placed_boxes = self.placements_to_boxes(result, geometries);
111 let report = analyzer.analyze(&placed_boxes, floor_z);
112 if report.is_all_stable() {
113 break;
114 }
115 let drop: std::collections::HashSet<(String, usize)> = report
116 .unstable_boxes()
117 .iter()
118 .map(|r| (r.id.clone(), r.instance))
119 .collect();
120 result
121 .placements
122 .retain(|p| !drop.contains(&(p.geometry_id.clone(), p.instance)));
123 for (id, _) in drop {
124 result.unplaced.push(id);
125 }
126 }
127
128 result.deduplicate_unplaced();
129 let boxes = self.placements_to_boxes(result, geometries);
131 let placed_volume: f64 = boxes
132 .iter()
133 .map(|b| b.dimensions.x * b.dimensions.y * b.dimensions.z)
134 .sum();
135 let container_volume = boundary.measure();
136 result.utilization = if container_volume > 0.0 {
137 placed_volume / container_volume
138 } else {
139 0.0
140 };
141 }
142
143 fn placements_to_boxes(
145 &self,
146 result: &SolveResult<f64>,
147 geometries: &[Geometry3D],
148 ) -> Vec<PlacedBox> {
149 let geom_map: std::collections::HashMap<&str, &Geometry3D> =
150 geometries.iter().map(|g| (g.id().as_str(), g)).collect();
151
152 result
153 .placements
154 .iter()
155 .filter_map(|p| {
156 let geom = geom_map.get(p.geometry_id.as_str())?;
157 let ori_idx = p.rotation_index.unwrap_or(0);
158 let dims = geom.dimensions_for_orientation(ori_idx);
159
160 let mut placed = PlacedBox::new(
161 p.geometry_id.clone(),
162 p.instance,
163 Point3::new(p.position[0], p.position[1], p.position[2]),
164 dims,
165 );
166
167 if let Some(mass) = geom.mass() {
168 placed = placed.with_mass(mass);
169 }
170
171 Some(placed)
172 })
173 .collect()
174 }
175
176 fn layer_packing(
178 &self,
179 geometries: &[Geometry3D],
180 boundary: &Boundary3D,
181 ) -> Result<SolveResult<f64>> {
182 let start = Timer::now();
183 let mut result = SolveResult::new();
184 let mut placements = Vec::new();
185
186 let margin = self.config.margin;
187 let spacing = self.config.spacing;
188
189 let bound_max_x = boundary.width() - margin;
190 let bound_max_y = boundary.depth() - margin;
191 let bound_max_z = boundary.height() - margin;
192
193 let mut current_x = margin;
195 let mut current_y = margin;
196 let mut current_z = margin;
197 let mut row_depth = 0.0_f64;
198 let mut layer_height = 0.0_f64;
199
200 let mut total_placed_volume = 0.0;
201 let mut total_placed_mass = 0.0;
202
203 for geom in geometries {
204 geom.validate()?;
205
206 for instance in 0..geom.quantity() {
207 if self.cancelled.load(Ordering::Relaxed) {
208 result.computation_time_ms = start.elapsed_ms();
209 return Ok(result);
210 }
211
212 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
214 {
215 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
216 result.utilization = total_placed_volume / boundary.measure();
217 result.computation_time_ms = start.elapsed_ms();
218 result.placements = placements;
219 return Ok(result);
220 }
221
222 if let (Some(max_mass), Some(item_mass)) = (boundary.max_mass(), geom.mass()) {
224 if total_placed_mass + item_mass > max_mass {
225 result.unplaced.push(geom.id().clone());
226 continue;
227 }
228 }
229
230 let orientations = geom.allowed_orientations();
232 let mut best_fit: BestFit3D = None;
233 for (ori_idx, _) in orientations.iter().enumerate() {
236 let dims = geom.dimensions_for_orientation(ori_idx);
237 let g_width = dims.x;
238 let g_depth = dims.y;
239 let g_height = dims.z;
240
241 let mut try_x = current_x;
243 let mut try_y = current_y;
244 let mut try_z = current_z;
245 let mut try_row_depth = row_depth;
246 let mut try_layer_height = layer_height;
247
248 if try_x + g_width > bound_max_x {
250 try_x = margin;
251 try_y += row_depth + spacing;
252 try_row_depth = 0.0;
253 }
254
255 if try_y + g_depth > bound_max_y {
257 try_x = margin;
258 try_y = margin;
259 try_z += layer_height + spacing;
260 try_row_depth = 0.0;
261 try_layer_height = 0.0;
262 }
263
264 if try_z + g_height > bound_max_z {
266 continue; }
268
269 let score = try_z * 1000000.0 + try_y * 1000.0 + try_x + g_height * 0.1;
272
273 let is_better = match &best_fit {
274 None => true,
275 Some((_, _, _, bg_height, bx, by, bz, _, _)) => {
276 let best_score = bz * 1000000.0 + by * 1000.0 + bx + bg_height * 0.1;
277 score < best_score
278 }
279 };
280
281 if is_better {
282 best_fit = Some((
283 ori_idx,
284 g_width,
285 g_depth,
286 g_height,
287 try_x,
288 try_y,
289 try_z,
290 try_row_depth,
291 try_layer_height,
292 ));
293 }
294 }
295
296 if let Some((
297 ori_idx,
298 g_width,
299 g_depth,
300 g_height,
301 place_x,
302 place_y,
303 place_z,
304 new_row_depth,
305 new_layer_height,
306 )) = best_fit
307 {
308 let placement = Placement::new_3d(
311 geom.id().clone(),
312 instance,
313 place_x,
314 place_y,
315 place_z,
316 0.0, 0.0,
318 0.0,
319 )
320 .with_rotation_index(ori_idx);
321
322 placements.push(placement);
323 total_placed_volume += geom.measure();
324 if let Some(mass) = geom.mass() {
325 total_placed_mass += mass;
326 }
327
328 current_x = place_x + g_width + spacing;
330 current_y = place_y;
331 current_z = place_z;
332 row_depth = new_row_depth.max(g_depth);
333 layer_height = new_layer_height.max(g_height);
334 } else {
335 result.unplaced.push(geom.id().clone());
336 }
337 }
338 }
339
340 result.placements = placements;
341 result.boundaries_used = 1;
342 result.utilization = total_placed_volume / boundary.measure();
343 result.computation_time_ms = start.elapsed_ms();
344
345 Ok(result)
346 }
347
348 fn genetic_algorithm(
353 &self,
354 geometries: &[Geometry3D],
355 boundary: &Boundary3D,
356 ) -> Result<SolveResult<f64>> {
357 let ga_config = GaConfig::default()
359 .with_population_size(self.config.population_size)
360 .with_max_generations(self.config.max_generations)
361 .with_crossover_rate(self.config.crossover_rate)
362 .with_mutation_rate(self.config.mutation_rate);
363
364 let result = run_ga_packing(
365 geometries,
366 boundary,
367 &self.config,
368 ga_config,
369 self.cancelled.clone(),
370 );
371
372 Ok(result)
373 }
374
375 fn brkga(&self, geometries: &[Geometry3D], boundary: &Boundary3D) -> Result<SolveResult<f64>> {
379 let brkga_config = BrkgaConfig::default()
381 .with_population_size(50)
382 .with_max_generations(100)
383 .with_elite_fraction(0.2)
384 .with_mutant_fraction(0.15)
385 .with_elite_bias(0.7);
386
387 let result = run_brkga_packing(
388 geometries,
389 boundary,
390 &self.config,
391 brkga_config,
392 self.cancelled.clone(),
393 );
394
395 Ok(result)
396 }
397
398 fn simulated_annealing(
403 &self,
404 geometries: &[Geometry3D],
405 boundary: &Boundary3D,
406 ) -> Result<SolveResult<f64>> {
407 let sa_config = SaConfig::default()
409 .with_initial_temp(100.0)
410 .with_final_temp(0.1)
411 .with_cooling_rate(0.95)
412 .with_iterations_per_temp(50)
413 .with_max_iterations(10000);
414
415 let result = run_sa_packing(
416 geometries,
417 boundary,
418 &self.config,
419 sa_config,
420 self.cancelled.clone(),
421 );
422
423 Ok(result)
424 }
425
426 fn extreme_point(
431 &self,
432 geometries: &[Geometry3D],
433 boundary: &Boundary3D,
434 ) -> Result<SolveResult<f64>> {
435 let start = Timer::now();
436
437 let (ep_placements, utilization) = run_ep_packing(
438 geometries,
439 boundary,
440 self.config.margin,
441 self.config.spacing,
442 boundary.max_mass(),
443 );
444
445 let mut placements = Vec::new();
450 for (id, instance, position, orientation) in ep_placements {
451 let placement = Placement::new_3d(
452 id, instance, position.x, position.y, position.z, 0.0, 0.0, 0.0, )
456 .with_rotation_index(orientation);
457 placements.push(placement);
458 }
459
460 let mut placed_ids: std::collections::HashSet<(String, usize)> =
462 std::collections::HashSet::new();
463 for p in &placements {
464 placed_ids.insert((p.geometry_id.clone(), p.instance));
465 }
466
467 let mut unplaced = Vec::new();
468 for geom in geometries {
469 for instance in 0..geom.quantity() {
470 if !placed_ids.contains(&(geom.id().clone(), instance)) {
471 unplaced.push(geom.id().clone());
472 }
473 }
474 }
475
476 let mut result = SolveResult::new();
477 result.placements = placements;
478 result.boundaries_used = 1;
479 result.utilization = utilization;
480 result.unplaced = unplaced;
481 result.computation_time_ms = start.elapsed_ms();
482 result.strategy = Some("ExtremePoint".to_string());
483
484 Ok(result)
485 }
486
487 fn layer_packing_with_progress(
489 &self,
490 geometries: &[Geometry3D],
491 boundary: &Boundary3D,
492 callback: &ProgressCallback,
493 ) -> Result<SolveResult<f64>> {
494 let start = Timer::now();
495 let mut result = SolveResult::new();
496 let mut placements = Vec::new();
497
498 let margin = self.config.margin;
499 let spacing = self.config.spacing;
500
501 let bound_max_x = boundary.width() - margin;
502 let bound_max_y = boundary.depth() - margin;
503 let bound_max_z = boundary.height() - margin;
504
505 let mut current_x = margin;
506 let mut current_y = margin;
507 let mut current_z = margin;
508 let mut row_depth = 0.0_f64;
509 let mut layer_height = 0.0_f64;
510
511 let mut total_placed_volume = 0.0;
512 let mut total_placed_mass = 0.0;
513
514 let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
516 let mut placed_count = 0usize;
517
518 callback(
520 ProgressInfo::new()
521 .with_phase("Layer Packing")
522 .with_items(0, total_pieces)
523 .with_elapsed(0),
524 );
525
526 for geom in geometries {
527 geom.validate()?;
528
529 for instance in 0..geom.quantity() {
530 if self.cancelled.load(Ordering::Relaxed) {
531 result.computation_time_ms = start.elapsed_ms();
532 callback(
533 ProgressInfo::new()
534 .with_phase("Cancelled")
535 .with_items(placed_count, total_pieces)
536 .with_elapsed(result.computation_time_ms)
537 .finished(),
538 );
539 return Ok(result);
540 }
541
542 if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
544 {
545 result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
546 result.utilization = total_placed_volume / boundary.measure();
547 result.computation_time_ms = start.elapsed_ms();
548 result.placements = placements;
549 callback(
550 ProgressInfo::new()
551 .with_phase("Time Limit Reached")
552 .with_items(placed_count, total_pieces)
553 .with_elapsed(result.computation_time_ms)
554 .finished(),
555 );
556 return Ok(result);
557 }
558
559 if let (Some(max_mass), Some(item_mass)) = (boundary.max_mass(), geom.mass()) {
561 if total_placed_mass + item_mass > max_mass {
562 result.unplaced.push(geom.id().clone());
563 continue;
564 }
565 }
566
567 let orientations = geom.allowed_orientations();
569 let mut best_fit: BestFit3D = None;
570
571 for (ori_idx, _) in orientations.iter().enumerate() {
572 let dims = geom.dimensions_for_orientation(ori_idx);
573 let g_width = dims.x;
574 let g_depth = dims.y;
575 let g_height = dims.z;
576
577 let mut try_x = current_x;
578 let mut try_y = current_y;
579 let mut try_z = current_z;
580 let mut try_row_depth = row_depth;
581 let mut try_layer_height = layer_height;
582
583 if try_x + g_width > bound_max_x {
584 try_x = margin;
585 try_y += row_depth + spacing;
586 try_row_depth = 0.0;
587 }
588
589 if try_y + g_depth > bound_max_y {
590 try_x = margin;
591 try_y = margin;
592 try_z += layer_height + spacing;
593 try_row_depth = 0.0;
594 try_layer_height = 0.0;
595 }
596
597 if try_z + g_height > bound_max_z {
598 continue;
599 }
600
601 let score = try_z * 1000000.0 + try_y * 1000.0 + try_x + g_height * 0.1;
602
603 let is_better = match &best_fit {
604 None => true,
605 Some((_, _, _, bg_height, bx, by, bz, _, _)) => {
606 let best_score = bz * 1000000.0 + by * 1000.0 + bx + bg_height * 0.1;
607 score < best_score
608 }
609 };
610
611 if is_better {
612 best_fit = Some((
613 ori_idx,
614 g_width,
615 g_depth,
616 g_height,
617 try_x,
618 try_y,
619 try_z,
620 try_row_depth,
621 try_layer_height,
622 ));
623 }
624 }
625
626 if let Some((
627 ori_idx,
628 g_width,
629 g_depth,
630 g_height,
631 place_x,
632 place_y,
633 place_z,
634 new_row_depth,
635 new_layer_height,
636 )) = best_fit
637 {
638 let placement = Placement::new_3d(
639 geom.id().clone(),
640 instance,
641 place_x,
642 place_y,
643 place_z,
644 0.0,
645 0.0,
646 0.0,
647 )
648 .with_rotation_index(ori_idx);
649
650 placements.push(placement);
651 total_placed_volume += geom.measure();
652 if let Some(mass) = geom.mass() {
653 total_placed_mass += mass;
654 }
655 placed_count += 1;
656
657 current_x = place_x + g_width + spacing;
658 current_y = place_y;
659 current_z = place_z;
660 row_depth = new_row_depth.max(g_depth);
661 layer_height = new_layer_height.max(g_height);
662
663 callback(
665 ProgressInfo::new()
666 .with_phase("Layer Packing")
667 .with_items(placed_count, total_pieces)
668 .with_utilization(total_placed_volume / boundary.measure())
669 .with_elapsed(start.elapsed_ms()),
670 );
671 } else {
672 result.unplaced.push(geom.id().clone());
673 }
674 }
675 }
676
677 result.placements = placements;
678 result.boundaries_used = 1;
679 result.utilization = total_placed_volume / boundary.measure();
680 result.computation_time_ms = start.elapsed_ms();
681
682 callback(
684 ProgressInfo::new()
685 .with_phase("Complete")
686 .with_items(placed_count, total_pieces)
687 .with_utilization(result.utilization)
688 .with_elapsed(result.computation_time_ms)
689 .finished(),
690 );
691
692 Ok(result)
693 }
694}
695
696impl Solver for Packer3D {
697 type Geometry = Geometry3D;
698 type Boundary = Boundary3D;
699 type Scalar = f64;
700
701 fn solve(
702 &self,
703 geometries: &[Self::Geometry],
704 boundary: &Self::Boundary,
705 ) -> Result<SolveResult<f64>> {
706 boundary.validate()?;
707
708 self.cancelled.store(false, Ordering::Relaxed);
710
711 let mut result = match self.config.strategy {
712 Strategy::BottomLeftFill => self.layer_packing(geometries, boundary),
713 Strategy::ExtremePoint => self.extreme_point(geometries, boundary),
714 Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
715 Strategy::Brkga => self.brkga(geometries, boundary),
716 Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
717 _ => {
718 log::warn!(
720 "Strategy {:?} not yet implemented, using layer packing",
721 self.config.strategy
722 );
723 self.layer_packing(geometries, boundary)
724 }
725 }?;
726
727 result.deduplicate_unplaced();
729
730 if boundary.has_gravity() || boundary.has_stability() {
732 self.enforce_support(&mut result, geometries, boundary);
733 }
734 Ok(result)
735 }
736
737 fn solve_with_progress(
738 &self,
739 geometries: &[Self::Geometry],
740 boundary: &Self::Boundary,
741 callback: ProgressCallback,
742 ) -> Result<SolveResult<f64>> {
743 boundary.validate()?;
744
745 self.cancelled.store(false, Ordering::Relaxed);
747
748 let mut result = match self.config.strategy {
749 Strategy::BottomLeftFill => {
750 self.layer_packing_with_progress(geometries, boundary, &callback)?
751 }
752 Strategy::ExtremePoint => self.extreme_point(geometries, boundary)?,
755 _ => {
757 log::warn!(
758 "Strategy {:?} progress not yet implemented, using layer packing",
759 self.config.strategy
760 );
761 self.layer_packing_with_progress(geometries, boundary, &callback)?
762 }
763 };
764
765 result.deduplicate_unplaced();
767
768 if boundary.has_gravity() || boundary.has_stability() {
770 self.enforce_support(&mut result, geometries, boundary);
771 }
772 Ok(result)
773 }
774
775 fn cancel(&self) {
776 self.cancelled.store(true, Ordering::Relaxed);
777 }
778}
779
780#[cfg(test)]
781mod tests {
782 use super::*;
783
784 #[test]
785 fn test_simple_packing() {
786 let geometries = vec![
787 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(3),
788 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
789 ];
790
791 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
792 let packer = Packer3D::default_config();
793
794 let result = packer.solve(&geometries, &boundary).unwrap();
795
796 assert!(result.utilization > 0.0);
797 assert!(result.placements.len() <= 5);
798 }
799
800 #[test]
801 fn test_mass_constraint() {
802 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)
803 .with_quantity(10)
804 .with_mass(100.0)];
805
806 let boundary = Boundary3D::new(100.0, 80.0, 50.0).with_max_mass(350.0);
807
808 let packer = Packer3D::default_config();
809 let result = packer.solve(&geometries, &boundary).unwrap();
810
811 assert!(result.placements.len() <= 3);
813 }
814
815 #[test]
816 fn test_placement_within_bounds() {
817 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
818
819 let boundary = Boundary3D::new(50.0, 50.0, 50.0);
820 let config = Config::default().with_margin(5.0).with_spacing(2.0);
821 let packer = Packer3D::new(config);
822
823 let result = packer.solve(&geometries, &boundary).unwrap();
824
825 assert_eq!(result.placements.len(), 4);
827 assert!(result.unplaced.is_empty());
828
829 for p in &result.placements {
831 assert!(p.position[0] >= 5.0);
832 assert!(p.position[1] >= 5.0);
833 assert!(p.position[2] >= 5.0);
834 }
835 }
836
837 #[test]
838 fn test_ga_strategy_basic() {
839 let geometries = vec![
840 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
841 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
842 ];
843
844 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
845 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
846 let packer = Packer3D::new(config);
847
848 let result = packer.solve(&geometries, &boundary).unwrap();
849
850 assert!(result.utilization > 0.0);
852 assert!(!result.placements.is_empty());
853 }
854
855 #[test]
856 fn test_ga_strategy_all_placed() {
857 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
859
860 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
861 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
862 let packer = Packer3D::new(config);
863
864 let result = packer.solve(&geometries, &boundary).unwrap();
865
866 assert_eq!(result.placements.len(), 4);
868 assert!(result.unplaced.is_empty());
869 }
870
871 #[test]
872 #[cfg(feature = "serde")]
873 fn test_3d_response_orientation_in_bounds() {
874 use crate::build_pack3d_response;
875 use crate::geometry::OrientationConstraint;
876
877 let (bw, bd, bh) = (100.0_f64, 100.0_f64, 30.0_f64);
883 let boundary = Boundary3D::new(bw, bd, bh);
884
885 for strategy in [
886 Strategy::ExtremePoint,
887 Strategy::GeneticAlgorithm,
888 Strategy::Brkga,
889 ] {
890 let geometries = vec![Geometry3D::new("tall", 25.0, 25.0, 80.0)
891 .with_quantity(3)
892 .with_orientation(OrientationConstraint::Any)];
893 let config = Config::default().with_strategy(strategy);
894 let packer = Packer3D::new(config);
895 let result = packer.solve(&geometries, &boundary).unwrap();
896 let response = build_pack3d_response(&result, &geometries);
897
898 if strategy == Strategy::ExtremePoint {
900 assert!(
901 !response.placements.is_empty(),
902 "EP should place the tall box by rotating it into the flat container"
903 );
904 }
905
906 let base = geometries[0].dimensions_for_orientation(0); for p in &response.placements {
908 let axes: Vec<usize> = p
909 .orientation
910 .chars()
911 .map(|c| match c {
912 'x' => 0,
913 'y' => 1,
914 'z' => 2,
915 _ => panic!("unexpected orientation label '{}'", p.orientation),
916 })
917 .collect();
918 let (dx, dy, dz) = (base[axes[0]], base[axes[1]], base[axes[2]]);
919 let e = 1e-6;
920 assert!(
921 p.x + dx <= bw + e && p.y + dy <= bd + e && p.z + dz <= bh + e,
922 "{:?} {}#{} out of bounds for reported orientation '{}': \
923 pos({:.1},{:.1},{:.1}) dims({dx:.1},{dy:.1},{dz:.1}) boundary({bw},{bd},{bh})",
924 strategy,
925 p.geometry_id,
926 p.instance,
927 p.orientation,
928 p.x,
929 p.y,
930 p.z,
931 );
932 }
933 }
934 }
935
936 #[test]
937 fn test_ga_strategy_with_orientations() {
938 use crate::geometry::OrientationConstraint;
939
940 let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
942 .with_quantity(2)
943 .with_orientation(OrientationConstraint::Any)];
944
945 let boundary = Boundary3D::new(60.0, 60.0, 60.0);
947 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
948 let packer = Packer3D::new(config);
949
950 let result = packer.solve(&geometries, &boundary).unwrap();
951
952 assert_eq!(result.placements.len(), 2);
954 }
955
956 #[test]
957 fn test_brkga_strategy_basic() {
958 let geometries = vec![
959 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
960 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
961 ];
962
963 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
964 let config = Config::default().with_strategy(Strategy::Brkga);
965 let packer = Packer3D::new(config);
966
967 let result = packer.solve(&geometries, &boundary).unwrap();
968
969 assert!(result.utilization > 0.0);
971 assert!(!result.placements.is_empty());
972 assert_eq!(result.strategy, Some("BRKGA".to_string()));
973 }
974
975 #[test]
976 fn test_brkga_strategy_all_placed() {
977 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
979
980 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
981 let config = Config::default().with_strategy(Strategy::Brkga);
982 let packer = Packer3D::new(config);
983
984 let result = packer.solve(&geometries, &boundary).unwrap();
985
986 assert_eq!(result.placements.len(), 4);
988 assert!(result.unplaced.is_empty());
989 }
990
991 #[test]
992 fn test_ep_strategy_basic() {
993 let geometries = vec![
994 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
995 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
996 ];
997
998 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
999 let config = Config::default().with_strategy(Strategy::ExtremePoint);
1000 let packer = Packer3D::new(config);
1001
1002 let result = packer.solve(&geometries, &boundary).unwrap();
1003
1004 assert!(result.utilization > 0.0);
1006 assert!(!result.placements.is_empty());
1007 assert_eq!(result.strategy, Some("ExtremePoint".to_string()));
1008 }
1009
1010 #[test]
1011 fn test_ep_strategy_all_placed() {
1012 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
1014
1015 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1016 let config = Config::default().with_strategy(Strategy::ExtremePoint);
1017 let packer = Packer3D::new(config);
1018
1019 let result = packer.solve(&geometries, &boundary).unwrap();
1020
1021 assert_eq!(result.placements.len(), 4);
1023 assert!(result.unplaced.is_empty());
1024 }
1025
1026 #[test]
1027 fn test_ep_strategy_with_margin() {
1028 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
1029
1030 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1031 let config = Config::default()
1032 .with_strategy(Strategy::ExtremePoint)
1033 .with_margin(5.0);
1034 let packer = Packer3D::new(config);
1035
1036 let result = packer.solve(&geometries, &boundary).unwrap();
1037
1038 for p in &result.placements {
1040 assert!(p.position[0] >= 4.9);
1041 assert!(p.position[1] >= 4.9);
1042 assert!(p.position[2] >= 4.9);
1043 }
1044 }
1045
1046 #[test]
1047 fn test_ep_strategy_with_orientations() {
1048 use crate::geometry::OrientationConstraint;
1049
1050 let geometries = vec![Geometry3D::new("B1", 80.0, 10.0, 10.0)
1052 .with_quantity(2)
1053 .with_orientation(OrientationConstraint::Any)];
1054
1055 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1056 let config = Config::default().with_strategy(Strategy::ExtremePoint);
1057 let packer = Packer3D::new(config);
1058
1059 let result = packer.solve(&geometries, &boundary).unwrap();
1060
1061 assert_eq!(result.placements.len(), 2);
1063 }
1064
1065 #[test]
1066 fn test_ep_strategy_perfect_fill_via_solve() {
1067 let geometries = vec![Geometry3D::new("cube", 50.0, 50.0, 50.0).with_quantity(8)];
1071 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1072 let packer = Packer3D::new(Config::default().with_strategy(Strategy::ExtremePoint));
1073
1074 let result = packer.solve(&geometries, &boundary).unwrap();
1075
1076 assert_eq!(result.placements.len(), 8, "EP must place all 8 cubes");
1077 assert!(result.unplaced.is_empty());
1078 }
1079
1080 #[test]
1081 fn test_ep_at_least_as_good_as_blf() {
1082 let boundary = Boundary3D::new(85.0, 85.0, 80.0);
1086 let cases: [(&str, f64, f64, f64, usize); 3] = [
1087 ("big", 40.0, 40.0, 40.0, 4),
1088 ("mid", 30.0, 20.0, 25.0, 6),
1089 ("small", 15.0, 15.0, 30.0, 8),
1090 ];
1091 let geometries: Vec<Geometry3D> = cases
1092 .iter()
1093 .map(|(id, w, d, h, q)| Geometry3D::new(*id, *w, *d, *h).with_quantity(*q))
1094 .collect();
1095
1096 let blf = Packer3D::new(Config::default().with_strategy(Strategy::BottomLeftFill))
1097 .solve(&geometries, &boundary)
1098 .unwrap();
1099 let ep = Packer3D::new(Config::default().with_strategy(Strategy::ExtremePoint))
1100 .solve(&geometries, &boundary)
1101 .unwrap();
1102
1103 assert!(
1104 ep.placements.len() >= blf.placements.len(),
1105 "EP placed {} but BLF placed {} — EP must not regress below BLF",
1106 ep.placements.len(),
1107 blf.placements.len()
1108 );
1109 }
1110
1111 fn floating_pair() -> (Vec<Geometry3D>, SolveResult<f64>) {
1113 let geometries = vec![
1114 Geometry3D::new("a", 20.0, 20.0, 20.0),
1115 Geometry3D::new("b", 20.0, 20.0, 20.0),
1116 ];
1117 let mut result = SolveResult::new();
1118 result.placements.push(
1119 Placement::new_3d("a".to_string(), 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
1120 .with_rotation_index(0),
1121 );
1122 result.placements.push(
1124 Placement::new_3d("b".to_string(), 0, 0.0, 0.0, 50.0, 0.0, 0.0, 0.0)
1125 .with_rotation_index(0),
1126 );
1127 (geometries, result)
1128 }
1129
1130 #[test]
1131 fn test_gravity_removes_floating_box() {
1132 let (geometries, mut result) = floating_pair();
1133 let boundary = Boundary3D::new(100.0, 100.0, 100.0).with_gravity(true);
1134 let packer = Packer3D::default_config();
1135
1136 packer.enforce_support(&mut result, &geometries, &boundary);
1137
1138 let ids: Vec<&str> = result
1139 .placements
1140 .iter()
1141 .map(|p| p.geometry_id.as_str())
1142 .collect();
1143 assert_eq!(ids, vec!["a"], "floating box must be dropped under gravity");
1144 assert!(result.unplaced.iter().any(|id| id == "b"));
1145 }
1146
1147 #[test]
1148 fn test_no_constraint_keeps_floating_box() {
1149 let (geometries, result) = floating_pair();
1153 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1154 assert!(!boundary.has_gravity() && !boundary.has_stability());
1155 assert_eq!(result.placements.len(), 2);
1157 let _ = geometries;
1158 }
1159
1160 #[test]
1161 fn test_gravity_keeps_floor_boxes_with_margin() {
1162 let geometries = vec![
1167 Geometry3D::new("big", 40.0, 40.0, 40.0).with_quantity(4),
1168 Geometry3D::new("mid", 30.0, 20.0, 25.0).with_quantity(6),
1169 ];
1170 let boundary = Boundary3D::new(100.0, 100.0, 100.0).with_gravity(true);
1171 let packer = Packer3D::new(
1172 Config::default()
1173 .with_margin(5.0)
1174 .with_strategy(Strategy::BottomLeftFill),
1175 );
1176
1177 let result = packer.solve(&geometries, &boundary).unwrap();
1178
1179 assert!(
1180 !result.placements.is_empty(),
1181 "margin>0 + gravity must not drop floor-resting boxes"
1182 );
1183 }
1184
1185 #[test]
1186 fn test_stability_drops_undersupported_box() {
1187 let geometries = vec![
1191 Geometry3D::new("a", 40.0, 40.0, 20.0),
1192 Geometry3D::new("b", 40.0, 40.0, 20.0),
1193 ];
1194 let make = || {
1195 let mut r = SolveResult::new();
1196 r.placements.push(
1197 Placement::new_3d("a".to_string(), 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
1198 .with_rotation_index(0),
1199 );
1200 r.placements.push(
1202 Placement::new_3d("b".to_string(), 0, 35.0, 0.0, 20.0, 0.0, 0.0, 0.0)
1203 .with_rotation_index(0),
1204 );
1205 r
1206 };
1207 let packer = Packer3D::default_config();
1208
1209 let mut grav = make();
1210 packer.enforce_support(
1211 &mut grav,
1212 &geometries,
1213 &Boundary3D::new(100.0, 100.0, 100.0).with_gravity(true),
1214 );
1215 assert_eq!(
1216 grav.placements.len(),
1217 2,
1218 "gravity keeps a box that touches below"
1219 );
1220
1221 let mut stab = make();
1222 packer.enforce_support(
1223 &mut stab,
1224 &geometries,
1225 &Boundary3D::new(100.0, 100.0, 100.0).with_stability(true),
1226 );
1227 assert!(
1228 stab.placements.iter().all(|p| p.geometry_id == "a"),
1229 "stability drops the under-supported box"
1230 );
1231 }
1232
1233 #[test]
1234 fn test_layer_packing_orientation_optimization() {
1235 use crate::geometry::OrientationConstraint;
1236
1237 let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
1240 .with_quantity(2)
1241 .with_orientation(OrientationConstraint::Any)];
1242
1243 let boundary = Boundary3D::new(45.0, 80.0, 80.0);
1245 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1246 let packer = Packer3D::new(config);
1247
1248 let result = packer.solve(&geometries, &boundary).unwrap();
1249
1250 assert_eq!(
1252 result.placements.len(),
1253 2,
1254 "Both boxes should be placed by using rotation"
1255 );
1256 assert!(result.unplaced.is_empty());
1257
1258 for p in &result.placements {
1260 assert!(
1261 p.rotation_index.is_some(),
1262 "Placement should have rotation_index set"
1263 );
1264 }
1265 }
1266
1267 #[test]
1268 fn test_layer_packing_selects_best_orientation() {
1269 use crate::geometry::OrientationConstraint;
1270
1271 let geometries = vec![Geometry3D::new("B1", 30.0, 20.0, 10.0)
1276 .with_quantity(1)
1277 .with_orientation(OrientationConstraint::Any)];
1278
1279 let boundary = Boundary3D::new(35.0, 50.0, 100.0);
1280 let packer = Packer3D::default_config();
1281
1282 let result = packer.solve(&geometries, &boundary).unwrap();
1283
1284 assert_eq!(result.placements.len(), 1);
1285 assert!(result.unplaced.is_empty());
1286 }
1287}