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 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
732
733 if boundary.has_gravity() || boundary.has_stability() {
735 self.enforce_support(&mut result, geometries, boundary);
736 }
737 Ok(result)
738 }
739
740 fn solve_with_progress(
741 &self,
742 geometries: &[Self::Geometry],
743 boundary: &Self::Boundary,
744 callback: ProgressCallback,
745 ) -> Result<SolveResult<f64>> {
746 boundary.validate()?;
747
748 self.cancelled.store(false, Ordering::Relaxed);
750
751 let mut result = match self.config.strategy {
752 Strategy::BottomLeftFill => {
753 self.layer_packing_with_progress(geometries, boundary, &callback)?
754 }
755 Strategy::ExtremePoint => self.extreme_point(geometries, boundary)?,
758 _ => {
760 log::warn!(
761 "Strategy {:?} progress not yet implemented, using layer packing",
762 self.config.strategy
763 );
764 self.layer_packing_with_progress(geometries, boundary, &callback)?
765 }
766 };
767
768 result.deduplicate_unplaced();
770 result.total_requested = geometries.iter().map(|g| g.quantity()).sum();
773
774 if boundary.has_gravity() || boundary.has_stability() {
776 self.enforce_support(&mut result, geometries, boundary);
777 }
778 Ok(result)
779 }
780
781 fn cancel(&self) {
782 self.cancelled.store(true, Ordering::Relaxed);
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789
790 #[test]
791 fn test_simple_packing() {
792 let geometries = vec![
793 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(3),
794 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
795 ];
796
797 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
798 let packer = Packer3D::default_config();
799
800 let result = packer.solve(&geometries, &boundary).unwrap();
801
802 assert!(result.utilization > 0.0);
803 assert!(result.placements.len() <= 5);
804 }
805
806 #[test]
807 fn test_mass_constraint() {
808 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)
809 .with_quantity(10)
810 .with_mass(100.0)];
811
812 let boundary = Boundary3D::new(100.0, 80.0, 50.0).with_max_mass(350.0);
813
814 let packer = Packer3D::default_config();
815 let result = packer.solve(&geometries, &boundary).unwrap();
816
817 assert!(result.placements.len() <= 3);
819 }
820
821 #[test]
822 fn test_placement_within_bounds() {
823 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
824
825 let boundary = Boundary3D::new(50.0, 50.0, 50.0);
826 let config = Config::default().with_margin(5.0).with_spacing(2.0);
827 let packer = Packer3D::new(config);
828
829 let result = packer.solve(&geometries, &boundary).unwrap();
830
831 assert_eq!(result.placements.len(), 4);
833 assert!(result.unplaced.is_empty());
834
835 for p in &result.placements {
837 assert!(p.position[0] >= 5.0);
838 assert!(p.position[1] >= 5.0);
839 assert!(p.position[2] >= 5.0);
840 }
841 }
842
843 #[test]
844 fn test_ga_strategy_basic() {
845 let geometries = vec![
846 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
847 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
848 ];
849
850 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
851 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
852 let packer = Packer3D::new(config);
853
854 let result = packer.solve(&geometries, &boundary).unwrap();
855
856 assert!(result.utilization > 0.0);
858 assert!(!result.placements.is_empty());
859 }
860
861 #[test]
862 fn test_ga_strategy_all_placed() {
863 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
865
866 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
867 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
868 let packer = Packer3D::new(config);
869
870 let result = packer.solve(&geometries, &boundary).unwrap();
871
872 assert_eq!(result.placements.len(), 4);
874 assert!(result.unplaced.is_empty());
875 }
876
877 #[test]
878 #[cfg(feature = "serde")]
879 fn test_3d_response_orientation_in_bounds() {
880 use crate::build_pack3d_response;
881 use crate::geometry::OrientationConstraint;
882
883 let (bw, bd, bh) = (100.0_f64, 100.0_f64, 30.0_f64);
889 let boundary = Boundary3D::new(bw, bd, bh);
890
891 for strategy in [
892 Strategy::ExtremePoint,
893 Strategy::GeneticAlgorithm,
894 Strategy::Brkga,
895 ] {
896 let geometries = vec![Geometry3D::new("tall", 25.0, 25.0, 80.0)
897 .with_quantity(3)
898 .with_orientation(OrientationConstraint::Any)];
899 let config = Config::default().with_strategy(strategy);
900 let packer = Packer3D::new(config);
901 let result = packer.solve(&geometries, &boundary).unwrap();
902 let response = build_pack3d_response(&result, &geometries);
903
904 if strategy == Strategy::ExtremePoint {
906 assert!(
907 !response.placements.is_empty(),
908 "EP should place the tall box by rotating it into the flat container"
909 );
910 }
911
912 let base = geometries[0].dimensions_for_orientation(0); for p in &response.placements {
914 let axes: Vec<usize> = p
915 .orientation
916 .chars()
917 .map(|c| match c {
918 'x' => 0,
919 'y' => 1,
920 'z' => 2,
921 _ => panic!("unexpected orientation label '{}'", p.orientation),
922 })
923 .collect();
924 let (dx, dy, dz) = (base[axes[0]], base[axes[1]], base[axes[2]]);
925 let e = 1e-6;
926 assert!(
927 p.x + dx <= bw + e && p.y + dy <= bd + e && p.z + dz <= bh + e,
928 "{:?} {}#{} out of bounds for reported orientation '{}': \
929 pos({:.1},{:.1},{:.1}) dims({dx:.1},{dy:.1},{dz:.1}) boundary({bw},{bd},{bh})",
930 strategy,
931 p.geometry_id,
932 p.instance,
933 p.orientation,
934 p.x,
935 p.y,
936 p.z,
937 );
938 }
939 }
940 }
941
942 #[test]
943 fn test_ga_strategy_with_orientations() {
944 use crate::geometry::OrientationConstraint;
945
946 let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
948 .with_quantity(2)
949 .with_orientation(OrientationConstraint::Any)];
950
951 let boundary = Boundary3D::new(60.0, 60.0, 60.0);
953 let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
954 let packer = Packer3D::new(config);
955
956 let result = packer.solve(&geometries, &boundary).unwrap();
957
958 assert_eq!(result.placements.len(), 2);
960 }
961
962 #[test]
963 fn test_brkga_strategy_basic() {
964 let geometries = vec![
965 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
966 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
967 ];
968
969 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
970 let config = Config::default().with_strategy(Strategy::Brkga);
971 let packer = Packer3D::new(config);
972
973 let result = packer.solve(&geometries, &boundary).unwrap();
974
975 assert!(result.utilization > 0.0);
977 assert!(!result.placements.is_empty());
978 assert_eq!(result.strategy, Some("BRKGA".to_string()));
979 }
980
981 #[test]
982 fn test_brkga_strategy_all_placed() {
983 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
985
986 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
987 let config = Config::default().with_strategy(Strategy::Brkga);
988 let packer = Packer3D::new(config);
989
990 let result = packer.solve(&geometries, &boundary).unwrap();
991
992 assert_eq!(result.placements.len(), 4);
994 assert!(result.unplaced.is_empty());
995 }
996
997 #[test]
998 fn test_ep_strategy_basic() {
999 let geometries = vec![
1000 Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
1001 Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
1002 ];
1003
1004 let boundary = Boundary3D::new(100.0, 80.0, 50.0);
1005 let config = Config::default().with_strategy(Strategy::ExtremePoint);
1006 let packer = Packer3D::new(config);
1007
1008 let result = packer.solve(&geometries, &boundary).unwrap();
1009
1010 assert!(result.utilization > 0.0);
1012 assert!(!result.placements.is_empty());
1013 assert_eq!(result.strategy, Some("ExtremePoint".to_string()));
1014 }
1015
1016 #[test]
1017 fn test_ep_strategy_all_placed() {
1018 let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
1020
1021 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1022 let config = Config::default().with_strategy(Strategy::ExtremePoint);
1023 let packer = Packer3D::new(config);
1024
1025 let result = packer.solve(&geometries, &boundary).unwrap();
1026
1027 assert_eq!(result.placements.len(), 4);
1029 assert!(result.unplaced.is_empty());
1030 }
1031
1032 #[test]
1033 fn test_ep_strategy_with_margin() {
1034 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
1035
1036 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1037 let config = Config::default()
1038 .with_strategy(Strategy::ExtremePoint)
1039 .with_margin(5.0);
1040 let packer = Packer3D::new(config);
1041
1042 let result = packer.solve(&geometries, &boundary).unwrap();
1043
1044 for p in &result.placements {
1046 assert!(p.position[0] >= 4.9);
1047 assert!(p.position[1] >= 4.9);
1048 assert!(p.position[2] >= 4.9);
1049 }
1050 }
1051
1052 #[test]
1053 fn test_ep_strategy_with_orientations() {
1054 use crate::geometry::OrientationConstraint;
1055
1056 let geometries = vec![Geometry3D::new("B1", 80.0, 10.0, 10.0)
1058 .with_quantity(2)
1059 .with_orientation(OrientationConstraint::Any)];
1060
1061 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1062 let config = Config::default().with_strategy(Strategy::ExtremePoint);
1063 let packer = Packer3D::new(config);
1064
1065 let result = packer.solve(&geometries, &boundary).unwrap();
1066
1067 assert_eq!(result.placements.len(), 2);
1069 }
1070
1071 #[test]
1072 fn test_ep_strategy_perfect_fill_via_solve() {
1073 let geometries = vec![Geometry3D::new("cube", 50.0, 50.0, 50.0).with_quantity(8)];
1077 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1078 let packer = Packer3D::new(Config::default().with_strategy(Strategy::ExtremePoint));
1079
1080 let result = packer.solve(&geometries, &boundary).unwrap();
1081
1082 assert_eq!(result.placements.len(), 8, "EP must place all 8 cubes");
1083 assert!(result.unplaced.is_empty());
1084 }
1085
1086 #[test]
1087 fn test_ep_at_least_as_good_as_blf() {
1088 let boundary = Boundary3D::new(85.0, 85.0, 80.0);
1092 let cases: [(&str, f64, f64, f64, usize); 3] = [
1093 ("big", 40.0, 40.0, 40.0, 4),
1094 ("mid", 30.0, 20.0, 25.0, 6),
1095 ("small", 15.0, 15.0, 30.0, 8),
1096 ];
1097 let geometries: Vec<Geometry3D> = cases
1098 .iter()
1099 .map(|(id, w, d, h, q)| Geometry3D::new(*id, *w, *d, *h).with_quantity(*q))
1100 .collect();
1101
1102 let blf = Packer3D::new(Config::default().with_strategy(Strategy::BottomLeftFill))
1103 .solve(&geometries, &boundary)
1104 .unwrap();
1105 let ep = Packer3D::new(Config::default().with_strategy(Strategy::ExtremePoint))
1106 .solve(&geometries, &boundary)
1107 .unwrap();
1108
1109 assert!(
1110 ep.placements.len() >= blf.placements.len(),
1111 "EP placed {} but BLF placed {} — EP must not regress below BLF",
1112 ep.placements.len(),
1113 blf.placements.len()
1114 );
1115 }
1116
1117 fn floating_pair() -> (Vec<Geometry3D>, SolveResult<f64>) {
1119 let geometries = vec![
1120 Geometry3D::new("a", 20.0, 20.0, 20.0),
1121 Geometry3D::new("b", 20.0, 20.0, 20.0),
1122 ];
1123 let mut result = SolveResult::new();
1124 result.placements.push(
1125 Placement::new_3d("a".to_string(), 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
1126 .with_rotation_index(0),
1127 );
1128 result.placements.push(
1130 Placement::new_3d("b".to_string(), 0, 0.0, 0.0, 50.0, 0.0, 0.0, 0.0)
1131 .with_rotation_index(0),
1132 );
1133 (geometries, result)
1134 }
1135
1136 #[test]
1137 fn test_gravity_removes_floating_box() {
1138 let (geometries, mut result) = floating_pair();
1139 let boundary = Boundary3D::new(100.0, 100.0, 100.0).with_gravity(true);
1140 let packer = Packer3D::default_config();
1141
1142 packer.enforce_support(&mut result, &geometries, &boundary);
1143
1144 let ids: Vec<&str> = result
1145 .placements
1146 .iter()
1147 .map(|p| p.geometry_id.as_str())
1148 .collect();
1149 assert_eq!(ids, vec!["a"], "floating box must be dropped under gravity");
1150 assert!(result.unplaced.iter().any(|id| id == "b"));
1151 }
1152
1153 #[test]
1154 fn test_no_constraint_keeps_floating_box() {
1155 let (geometries, result) = floating_pair();
1159 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
1160 assert!(!boundary.has_gravity() && !boundary.has_stability());
1161 assert_eq!(result.placements.len(), 2);
1163 let _ = geometries;
1164 }
1165
1166 #[test]
1167 fn test_gravity_keeps_floor_boxes_with_margin() {
1168 let geometries = vec![
1173 Geometry3D::new("big", 40.0, 40.0, 40.0).with_quantity(4),
1174 Geometry3D::new("mid", 30.0, 20.0, 25.0).with_quantity(6),
1175 ];
1176 let boundary = Boundary3D::new(100.0, 100.0, 100.0).with_gravity(true);
1177 let packer = Packer3D::new(
1178 Config::default()
1179 .with_margin(5.0)
1180 .with_strategy(Strategy::BottomLeftFill),
1181 );
1182
1183 let result = packer.solve(&geometries, &boundary).unwrap();
1184
1185 assert!(
1186 !result.placements.is_empty(),
1187 "margin>0 + gravity must not drop floor-resting boxes"
1188 );
1189 }
1190
1191 #[test]
1192 fn test_stability_drops_undersupported_box() {
1193 let geometries = vec![
1197 Geometry3D::new("a", 40.0, 40.0, 20.0),
1198 Geometry3D::new("b", 40.0, 40.0, 20.0),
1199 ];
1200 let make = || {
1201 let mut r = SolveResult::new();
1202 r.placements.push(
1203 Placement::new_3d("a".to_string(), 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
1204 .with_rotation_index(0),
1205 );
1206 r.placements.push(
1208 Placement::new_3d("b".to_string(), 0, 35.0, 0.0, 20.0, 0.0, 0.0, 0.0)
1209 .with_rotation_index(0),
1210 );
1211 r
1212 };
1213 let packer = Packer3D::default_config();
1214
1215 let mut grav = make();
1216 packer.enforce_support(
1217 &mut grav,
1218 &geometries,
1219 &Boundary3D::new(100.0, 100.0, 100.0).with_gravity(true),
1220 );
1221 assert_eq!(
1222 grav.placements.len(),
1223 2,
1224 "gravity keeps a box that touches below"
1225 );
1226
1227 let mut stab = make();
1228 packer.enforce_support(
1229 &mut stab,
1230 &geometries,
1231 &Boundary3D::new(100.0, 100.0, 100.0).with_stability(true),
1232 );
1233 assert!(
1234 stab.placements.iter().all(|p| p.geometry_id == "a"),
1235 "stability drops the under-supported box"
1236 );
1237 }
1238
1239 #[test]
1240 fn test_layer_packing_orientation_optimization() {
1241 use crate::geometry::OrientationConstraint;
1242
1243 let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
1246 .with_quantity(2)
1247 .with_orientation(OrientationConstraint::Any)];
1248
1249 let boundary = Boundary3D::new(45.0, 80.0, 80.0);
1251 let config = Config::default().with_strategy(Strategy::BottomLeftFill);
1252 let packer = Packer3D::new(config);
1253
1254 let result = packer.solve(&geometries, &boundary).unwrap();
1255
1256 assert_eq!(
1258 result.placements.len(),
1259 2,
1260 "Both boxes should be placed by using rotation"
1261 );
1262 assert!(result.unplaced.is_empty());
1263
1264 for p in &result.placements {
1266 assert!(
1267 p.rotation_index.is_some(),
1268 "Placement should have rotation_index set"
1269 );
1270 }
1271 }
1272
1273 #[test]
1274 fn test_layer_packing_selects_best_orientation() {
1275 use crate::geometry::OrientationConstraint;
1276
1277 let geometries = vec![Geometry3D::new("B1", 30.0, 20.0, 10.0)
1282 .with_quantity(1)
1283 .with_orientation(OrientationConstraint::Any)];
1284
1285 let boundary = Boundary3D::new(35.0, 50.0, 100.0);
1286 let packer = Packer3D::default_config();
1287
1288 let result = packer.solve(&geometries, &boundary).unwrap();
1289
1290 assert_eq!(result.placements.len(), 1);
1291 assert!(result.unplaced.is_empty());
1292 }
1293}