1use serde::{Deserialize, Serialize};
4
5use crate::{BinPackingError, Result};
6
7pub const MAX_DIMENSION_3D: u32 = 1 << 15;
12
13pub const MAX_BIN_COUNT_3D: usize = 1 << 15;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
19#[serde(rename_all = "snake_case")]
20pub enum ThreeDAlgorithm {
21 #[default]
23 Auto,
24 ExtremePoints,
26 ExtremePointsResidualSpace,
28 ExtremePointsFreeVolume,
30 ExtremePointsBottomLeftBack,
32 ExtremePointsContactPoint,
34 ExtremePointsEuclidean,
36 #[serde(rename = "guillotine_3d")]
41 Guillotine3D,
42 #[serde(rename = "guillotine_3d_best_short_side_fit")]
44 Guillotine3DBestShortSideFit,
45 #[serde(rename = "guillotine_3d_best_long_side_fit")]
47 Guillotine3DBestLongSideFit,
48 #[serde(rename = "guillotine_3d_shorter_leftover_axis")]
50 Guillotine3DShorterLeftoverAxis,
51 #[serde(rename = "guillotine_3d_longer_leftover_axis")]
53 Guillotine3DLongerLeftoverAxis,
54 #[serde(rename = "guillotine_3d_min_volume_split")]
56 Guillotine3DMinVolumeSplit,
57 #[serde(rename = "guillotine_3d_max_volume_split")]
59 Guillotine3DMaxVolumeSplit,
60 LayerBuilding,
62 LayerBuildingMaxRects,
64 LayerBuildingSkyline,
66 LayerBuildingGuillotine,
68 LayerBuildingShelf,
70 WallBuilding,
72 ColumnBuilding,
74 DeepestBottomLeft,
76 DeepestBottomLeftFill,
78 FirstFitDecreasingVolume,
80 BestFitDecreasingVolume,
82 MultiStart,
84 Grasp,
86 LocalSearch,
88 BranchAndBound,
90}
91
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub struct Bin3D {
95 pub name: String,
97 pub width: u32,
99 pub height: u32,
101 pub depth: u32,
103 #[serde(default = "default_bin_cost")]
105 pub cost: f64,
106 #[serde(default)]
108 pub quantity: Option<usize>,
109}
110
111fn default_bin_cost() -> f64 {
112 1.0
113}
114
115fn default_rotation_mask() -> RotationMask3D {
116 RotationMask3D::ALL
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct BoxDemand3D {
122 pub name: String,
124 pub width: u32,
126 pub height: u32,
128 pub depth: u32,
130 pub quantity: usize,
132 #[serde(default = "default_rotation_mask")]
135 pub allowed_rotations: RotationMask3D,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum Rotation3D {
148 Xyz,
150 Xzy,
152 Yxz,
154 Yzx,
156 Zxy,
158 Zyx,
160}
161
162impl Rotation3D {
163 pub fn apply(self, width: u32, height: u32, depth: u32) -> (u32, u32, u32) {
166 match self {
167 Self::Xyz => (width, height, depth),
168 Self::Xzy => (width, depth, height),
169 Self::Yxz => (height, width, depth),
170 Self::Yzx => (height, depth, width),
171 Self::Zxy => (depth, width, height),
172 Self::Zyx => (depth, height, width),
173 }
174 }
175
176 pub(crate) fn bit(self) -> u8 {
178 match self {
179 Self::Xyz => 0,
180 Self::Xzy => 1,
181 Self::Yxz => 2,
182 Self::Yzx => 3,
183 Self::Zxy => 4,
184 Self::Zyx => 5,
185 }
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
195pub struct RotationMask3D(u8);
196
197impl RotationMask3D {
198 pub const XYZ: Self = Self(1 << 0);
200 pub const XZY: Self = Self(1 << 1);
202 pub const YXZ: Self = Self(1 << 2);
204 pub const YZX: Self = Self(1 << 3);
206 pub const ZXY: Self = Self(1 << 4);
208 pub const ZYX: Self = Self(1 << 5);
210 pub const ALL: Self = Self(0b00111111);
212 pub const UPRIGHT: Self = Self(0b00100001);
215 pub const NONE: Self = Self(0);
217
218 pub fn contains(self, rotation: Rotation3D) -> bool {
220 (self.0 & (1 << rotation.bit())) != 0
221 }
222
223 pub fn is_empty(self) -> bool {
225 self.0 == 0
226 }
227
228 pub fn iter(self) -> impl Iterator<Item = Rotation3D> {
231 const ALL: [Rotation3D; 6] = [
232 Rotation3D::Xyz,
233 Rotation3D::Xzy,
234 Rotation3D::Yxz,
235 Rotation3D::Yzx,
236 Rotation3D::Zxy,
237 Rotation3D::Zyx,
238 ];
239 ALL.into_iter().filter(move |rot| self.contains(*rot))
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct Placement3D {
246 pub name: String,
248 pub x: u32,
250 pub y: u32,
252 pub z: u32,
254 pub width: u32,
256 pub height: u32,
258 pub depth: u32,
260 pub rotation: Rotation3D,
262}
263
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266pub struct BinLayout3D {
267 pub bin_name: String,
269 pub width: u32,
271 pub height: u32,
273 pub depth: u32,
275 pub cost: f64,
277 pub placements: Vec<Placement3D>,
279 pub used_volume: u64,
281 pub waste_volume: u64,
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
287pub struct BinRequirement3D {
288 pub bin_name: String,
290 pub bin_width: u32,
292 pub bin_height: u32,
294 pub bin_depth: u32,
296 pub cost: f64,
298 pub available_quantity: Option<usize>,
300 pub used_quantity: usize,
302 pub required_quantity: usize,
304 pub additional_quantity_needed: usize,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
310pub struct SolverMetrics3D {
311 pub iterations: usize,
322 pub explored_states: usize,
324 pub extreme_points_generated: usize,
327 pub branch_and_bound_nodes: usize,
330 pub notes: Vec<String>,
332}
333
334#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336pub struct ThreeDSolution {
337 pub algorithm: String,
342 pub exact: bool,
347 pub lower_bound: Option<f64>,
351 pub guillotine: bool,
355 pub bin_count: usize,
357 pub total_waste_volume: u64,
359 pub total_cost: f64,
361 pub layouts: Vec<BinLayout3D>,
363 #[serde(default)]
368 pub bin_requirements: Vec<BinRequirement3D>,
369 pub unplaced: Vec<BoxDemand3D>,
373 pub metrics: SolverMetrics3D,
375}
376
377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
379pub struct ThreeDProblem {
380 pub bins: Vec<Bin3D>,
382 pub demands: Vec<BoxDemand3D>,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct ThreeDOptions {
389 #[serde(default)]
391 pub algorithm: ThreeDAlgorithm,
392 #[serde(default = "default_multistart_runs")]
394 pub multistart_runs: usize,
395 #[serde(default = "default_improvement_rounds")]
397 pub improvement_rounds: usize,
398 #[serde(default = "default_beam_width")]
400 pub beam_width: usize,
401 #[serde(default = "default_auto_exact_max_types")]
403 pub auto_exact_max_types: usize,
404 #[serde(default = "default_auto_exact_max_quantity")]
406 pub auto_exact_max_quantity: usize,
407 #[serde(default = "default_branch_and_bound_node_limit")]
409 pub branch_and_bound_node_limit: usize,
410 #[serde(default)]
412 pub guillotine_required: bool,
413 #[serde(default)]
415 pub seed: Option<u64>,
416}
417
418impl Default for ThreeDOptions {
419 fn default() -> Self {
420 Self {
421 algorithm: ThreeDAlgorithm::Auto,
422 multistart_runs: default_multistart_runs(),
423 improvement_rounds: default_improvement_rounds(),
424 beam_width: default_beam_width(),
425 auto_exact_max_types: default_auto_exact_max_types(),
426 auto_exact_max_quantity: default_auto_exact_max_quantity(),
427 branch_and_bound_node_limit: default_branch_and_bound_node_limit(),
428 guillotine_required: false,
429 seed: None,
430 }
431 }
432}
433
434impl ThreeDSolution {
435 #[allow(dead_code)]
444 pub(crate) fn is_better_than(&self, other: &Self) -> bool {
445 (
446 self.unplaced.len(),
447 self.bin_count,
448 self.total_waste_volume,
449 OrderedFloat3D(self.total_cost),
450 ) < (
451 other.unplaced.len(),
452 other.bin_count,
453 other.total_waste_volume,
454 OrderedFloat3D(other.total_cost),
455 )
456 }
457}
458
459#[allow(dead_code)]
463#[derive(Debug, Clone, Copy, PartialEq)]
464struct OrderedFloat3D(f64);
465impl Eq for OrderedFloat3D {}
466impl PartialOrd for OrderedFloat3D {
467 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
468 Some(self.cmp(other))
469 }
470}
471impl Ord for OrderedFloat3D {
472 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
473 self.0.total_cmp(&other.0)
474 }
475}
476
477fn default_multistart_runs() -> usize {
478 12
479}
480fn default_improvement_rounds() -> usize {
481 24
482}
483fn default_beam_width() -> usize {
484 8
485}
486fn default_auto_exact_max_types() -> usize {
487 8
488}
489fn default_auto_exact_max_quantity() -> usize {
490 32
491}
492fn default_branch_and_bound_node_limit() -> usize {
493 1_000_000
494}
495
496#[allow(dead_code)]
500#[derive(Debug, Clone, PartialEq, Eq)]
501pub(crate) struct ItemInstance3D {
502 pub(crate) demand_index: usize,
503 pub(crate) name: String,
504 pub(crate) width: u32,
505 pub(crate) height: u32,
506 pub(crate) depth: u32,
507 pub(crate) allowed_rotations: RotationMask3D,
508}
509
510impl ItemInstance3D {
511 #[allow(dead_code)]
516 pub(crate) fn orientations(&self) -> impl Iterator<Item = (Rotation3D, u32, u32, u32)> + '_ {
517 let mut seen: Vec<(u32, u32, u32)> = Vec::with_capacity(6);
518 self.allowed_rotations.iter().filter_map(move |rotation| {
519 let extents = rotation.apply(self.width, self.height, self.depth);
520 if seen.contains(&extents) {
521 None
522 } else {
523 seen.push(extents);
524 Some((rotation, extents.0, extents.1, extents.2))
525 }
526 })
527 }
528}
529
530impl ThreeDProblem {
531 pub(crate) fn validate(&self) -> Result<()> {
533 if self.bins.is_empty() {
534 return Err(BinPackingError::InvalidInput(
535 "at least one bin entry is required".to_string(),
536 ));
537 }
538 if self.demands.is_empty() {
539 return Err(BinPackingError::InvalidInput(
540 "at least one box demand entry is required".to_string(),
541 ));
542 }
543
544 for bin in &self.bins {
545 if bin.width == 0 || bin.height == 0 || bin.depth == 0 {
546 return Err(BinPackingError::InvalidInput(format!(
547 "bin `{}` must have positive width, height, and depth",
548 bin.name
549 )));
550 }
551 if bin.width > MAX_DIMENSION_3D
552 || bin.height > MAX_DIMENSION_3D
553 || bin.depth > MAX_DIMENSION_3D
554 {
555 return Err(BinPackingError::InvalidInput(format!(
556 "bin `{}` dimensions exceed the supported maximum of {}",
557 bin.name, MAX_DIMENSION_3D
558 )));
559 }
560 if !bin.cost.is_finite() || bin.cost < 0.0 {
561 return Err(BinPackingError::InvalidInput(format!(
562 "bin `{}` must have a finite non-negative cost",
563 bin.name
564 )));
565 }
566 if let Some(quantity) = bin.quantity
567 && quantity == 0
568 {
569 return Err(BinPackingError::InvalidInput(format!(
570 "bin `{}` quantity, if set, must be positive",
571 bin.name
572 )));
573 }
574 }
575
576 for demand in &self.demands {
577 if demand.width == 0 || demand.height == 0 || demand.depth == 0 {
578 return Err(BinPackingError::InvalidInput(format!(
579 "demand `{}` must have positive width, height, and depth",
580 demand.name
581 )));
582 }
583 if demand.width > MAX_DIMENSION_3D
584 || demand.height > MAX_DIMENSION_3D
585 || demand.depth > MAX_DIMENSION_3D
586 {
587 return Err(BinPackingError::InvalidInput(format!(
588 "demand `{}` dimensions exceed the supported maximum of {}",
589 demand.name, MAX_DIMENSION_3D
590 )));
591 }
592 if demand.quantity == 0 {
593 return Err(BinPackingError::InvalidInput(format!(
594 "demand `{}` must have positive quantity",
595 demand.name
596 )));
597 }
598 if demand.allowed_rotations.is_empty() {
599 return Err(BinPackingError::InvalidInput(format!(
600 "demand `{}` must allow at least one rotation",
601 demand.name
602 )));
603 }
604 }
605
606 Ok(())
607 }
608
609 pub(crate) fn ensure_feasible_demands(&self) -> Result<()> {
611 for demand in &self.demands {
612 let feasible = self.bins.iter().any(|bin| {
613 demand.allowed_rotations.iter().any(|rotation| {
614 let (x, y, z) = rotation.apply(demand.width, demand.height, demand.depth);
615 bin.width >= x && bin.height >= y && bin.depth >= z
616 })
617 });
618 if !feasible {
619 return Err(BinPackingError::Infeasible3D {
620 item: demand.name.clone(),
621 width: demand.width,
622 height: demand.height,
623 depth: demand.depth,
624 });
625 }
626 }
627 Ok(())
628 }
629
630 #[allow(dead_code)]
633 pub(crate) fn expanded_items(&self) -> Vec<ItemInstance3D> {
634 let mut items = Vec::new();
635 for (index, demand) in self.demands.iter().enumerate() {
636 for _ in 0..demand.quantity {
637 items.push(ItemInstance3D {
638 demand_index: index,
639 name: demand.name.clone(),
640 width: demand.width,
641 height: demand.height,
642 depth: demand.depth,
643 allowed_rotations: demand.allowed_rotations,
644 });
645 }
646 }
647 items
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use serde_json::json;
655
656 #[test]
657 fn rotation3d_serializes_as_snake_case() {
658 let value = serde_json::to_value(Rotation3D::Zxy).expect("serialize");
659 assert_eq!(value, json!("zxy"));
660 }
661
662 #[test]
663 fn algorithm_serializes_with_explicit_renames_for_digit_variants() {
664 let cases = [
667 (ThreeDAlgorithm::Auto, "auto"),
668 (ThreeDAlgorithm::ExtremePoints, "extreme_points"),
669 (ThreeDAlgorithm::Guillotine3D, "guillotine_3d"),
670 (ThreeDAlgorithm::Guillotine3DBestShortSideFit, "guillotine_3d_best_short_side_fit"),
671 (ThreeDAlgorithm::LayerBuildingMaxRects, "layer_building_max_rects"),
672 (ThreeDAlgorithm::DeepestBottomLeftFill, "deepest_bottom_left_fill"),
673 (ThreeDAlgorithm::BranchAndBound, "branch_and_bound"),
674 ];
675 for (variant, expected) in cases {
676 let value = serde_json::to_value(variant).expect("serialize");
677 assert_eq!(value, json!(expected), "{:?}", variant);
678 let parsed: ThreeDAlgorithm =
679 serde_json::from_value(json!(expected)).expect("deserialize");
680 assert_eq!(parsed, variant);
681 }
682 }
683
684 #[test]
685 fn rotation_mask_all_contains_every_rotation() {
686 let mask = RotationMask3D::ALL;
687 for rot in [
688 Rotation3D::Xyz,
689 Rotation3D::Xzy,
690 Rotation3D::Yxz,
691 Rotation3D::Yzx,
692 Rotation3D::Zxy,
693 Rotation3D::Zyx,
694 ] {
695 assert!(mask.contains(rot), "ALL should contain {:?}", rot);
696 }
697 }
698
699 #[test]
700 fn rotation_mask_upright_only_keeps_y_axis() {
701 let upright = RotationMask3D::UPRIGHT;
702 assert!(upright.contains(Rotation3D::Xyz));
703 assert!(upright.contains(Rotation3D::Zyx));
704 assert!(!upright.contains(Rotation3D::Xzy));
705 assert!(!upright.contains(Rotation3D::Yxz));
706 assert!(!upright.contains(Rotation3D::Yzx));
707 assert!(!upright.contains(Rotation3D::Zxy));
708 }
709
710 #[test]
711 fn rotation_mask_none_is_empty() {
712 let none = RotationMask3D::NONE;
713 for rot in [
714 Rotation3D::Xyz,
715 Rotation3D::Xzy,
716 Rotation3D::Yxz,
717 Rotation3D::Yzx,
718 Rotation3D::Zxy,
719 Rotation3D::Zyx,
720 ] {
721 assert!(!none.contains(rot));
722 }
723 }
724
725 #[test]
726 fn rotation3d_apply_zxy_maps_input_dims_correctly() {
727 let (x, y, z) = Rotation3D::Zxy.apply(3, 5, 7);
730 assert_eq!((x, y, z), (7, 3, 5));
731 }
732
733 #[test]
734 fn rotation3d_apply_xyz_is_identity() {
735 let (x, y, z) = Rotation3D::Xyz.apply(3, 5, 7);
736 assert_eq!((x, y, z), (3, 5, 7));
737 }
738
739 #[test]
740 fn rotation3d_apply_zyx_swaps_x_and_z() {
741 let (x, y, z) = Rotation3D::Zyx.apply(3, 5, 7);
742 assert_eq!((x, y, z), (7, 5, 3));
743 }
744
745 #[test]
746 fn validate_rejects_empty_bins() {
747 let problem = ThreeDProblem { bins: vec![], demands: vec![sample_demand("a", 1, 1, 1, 1)] };
748 let err = problem.validate().expect_err("should reject");
749 assert!(
750 matches!(&err, crate::BinPackingError::InvalidInput(msg) if msg.contains("bin")),
751 "unexpected error: {err:?}",
752 );
753 }
754
755 #[test]
756 fn validate_rejects_empty_demands() {
757 let problem = ThreeDProblem { bins: vec![sample_bin("b", 10, 10, 10)], demands: vec![] };
758 let err = problem.validate().expect_err("should reject");
759 assert!(matches!(err, crate::BinPackingError::InvalidInput(_)));
760 }
761
762 #[test]
763 fn validate_rejects_zero_dimension_bin() {
764 let problem = ThreeDProblem {
765 bins: vec![sample_bin("b", 0, 10, 10)],
766 demands: vec![sample_demand("a", 1, 1, 1, 1)],
767 };
768 assert!(matches!(problem.validate(), Err(crate::BinPackingError::InvalidInput(_))));
769 }
770
771 #[test]
772 fn validate_rejects_oversized_dimension() {
773 let oversize = MAX_DIMENSION_3D + 1;
774 let problem = ThreeDProblem {
775 bins: vec![sample_bin("b", oversize, 10, 10)],
776 demands: vec![sample_demand("a", 1, 1, 1, 1)],
777 };
778 assert!(matches!(problem.validate(), Err(crate::BinPackingError::InvalidInput(_))));
779 }
780
781 #[test]
782 fn validate_rejects_non_finite_cost() {
783 let mut bin = sample_bin("b", 10, 10, 10);
784 bin.cost = f64::NAN;
785 let problem =
786 ThreeDProblem { bins: vec![bin], demands: vec![sample_demand("a", 1, 1, 1, 1)] };
787 assert!(matches!(problem.validate(), Err(crate::BinPackingError::InvalidInput(_))));
788 }
789
790 #[test]
791 fn validate_rejects_negative_cost() {
792 let mut bin = sample_bin("b", 10, 10, 10);
793 bin.cost = -1.0;
794 let problem =
795 ThreeDProblem { bins: vec![bin], demands: vec![sample_demand("a", 1, 1, 1, 1)] };
796 assert!(matches!(problem.validate(), Err(crate::BinPackingError::InvalidInput(_))));
797 }
798
799 #[test]
800 fn validate_rejects_zero_quantity_demand() {
801 let problem = ThreeDProblem {
802 bins: vec![sample_bin("b", 10, 10, 10)],
803 demands: vec![sample_demand("a", 1, 1, 1, 0)],
804 };
805 assert!(matches!(problem.validate(), Err(crate::BinPackingError::InvalidInput(_))));
806 }
807
808 #[test]
809 fn validate_rejects_empty_rotation_mask() {
810 let mut demand = sample_demand("a", 1, 1, 1, 1);
811 demand.allowed_rotations = RotationMask3D::NONE;
812 let problem =
813 ThreeDProblem { bins: vec![sample_bin("b", 10, 10, 10)], demands: vec![demand] };
814 assert!(matches!(problem.validate(), Err(crate::BinPackingError::InvalidInput(_))));
815 }
816
817 #[test]
818 fn validate_accepts_well_formed_problem() {
819 let problem = ThreeDProblem {
820 bins: vec![sample_bin("b", 10, 10, 10)],
821 demands: vec![sample_demand("a", 2, 3, 4, 1)],
822 };
823 problem.validate().expect("should accept");
824 }
825
826 #[test]
827 fn ensure_feasible_demands_rejects_oversize_item() {
828 let problem = ThreeDProblem {
829 bins: vec![sample_bin("b", 5, 5, 5)],
830 demands: vec![sample_demand("a", 6, 6, 6, 1)],
831 };
832 problem.validate().expect("validate ok");
833 let err = problem.ensure_feasible_demands().expect_err("should reject");
834 assert!(
835 matches!(&err, crate::BinPackingError::Infeasible3D { item, .. } if item == "a"),
836 "unexpected error: {err:?}",
837 );
838 }
839
840 #[test]
841 fn ensure_feasible_demands_uses_rotation() {
842 let problem = ThreeDProblem {
844 bins: vec![sample_bin("b", 1, 6, 1)],
845 demands: vec![sample_demand("a", 6, 1, 1, 1)],
846 };
847 problem.validate().expect("validate ok");
848 problem.ensure_feasible_demands().expect("should accept via rotation");
849 }
850
851 #[test]
852 fn expanded_items_yields_one_per_quantity() {
853 let problem = ThreeDProblem {
854 bins: vec![sample_bin("b", 10, 10, 10)],
855 demands: vec![sample_demand("a", 2, 3, 4, 5), sample_demand("c", 1, 1, 1, 2)],
856 };
857 let items = problem.expanded_items();
858 assert_eq!(items.len(), 7);
859 assert_eq!(items.iter().filter(|item| item.name == "a").count(), 5);
860 assert_eq!(items.iter().filter(|item| item.name == "c").count(), 2);
861 }
862
863 #[test]
868 fn three_d_is_better_than_tie_breaks_on_each_key() {
869 let base = sample_solution(0, 1, 0, 1.0);
870
871 let more_unplaced =
873 ThreeDSolution { unplaced: vec![sample_demand("u", 1, 1, 1, 1)], ..base.clone() };
874 assert!(base.is_better_than(&more_unplaced));
875 assert!(!more_unplaced.is_better_than(&base));
876
877 let more_bins = ThreeDSolution { bin_count: 2, ..base.clone() };
879 assert!(base.is_better_than(&more_bins));
880
881 let more_waste = ThreeDSolution { total_waste_volume: 100, ..base.clone() };
883 assert!(base.is_better_than(&more_waste));
884
885 let more_cost = ThreeDSolution { total_cost: base.total_cost + 1.0, ..base.clone() };
887 assert!(base.is_better_than(&more_cost));
888
889 assert!(!base.is_better_than(&base));
891 }
892
893 fn sample_solution(
894 unplaced_count: usize,
895 bin_count: usize,
896 waste: u64,
897 cost: f64,
898 ) -> ThreeDSolution {
899 ThreeDSolution {
900 algorithm: "test".to_string(),
901 exact: false,
902 lower_bound: None,
903 guillotine: false,
904 bin_count,
905 total_waste_volume: waste,
906 total_cost: cost,
907 layouts: Vec::new(),
908 bin_requirements: Vec::new(),
909 unplaced: (0..unplaced_count)
910 .map(|i| sample_demand(&format!("u{i}"), 1, 1, 1, 1))
911 .collect(),
912 metrics: SolverMetrics3D::default(),
913 }
914 }
915
916 fn sample_bin(name: &str, w: u32, h: u32, d: u32) -> Bin3D {
917 Bin3D { name: name.to_string(), width: w, height: h, depth: d, cost: 1.0, quantity: None }
918 }
919
920 fn sample_demand(name: &str, w: u32, h: u32, d: u32, qty: usize) -> BoxDemand3D {
921 BoxDemand3D {
922 name: name.to_string(),
923 width: w,
924 height: h,
925 depth: d,
926 quantity: qty,
927 allowed_rotations: RotationMask3D::ALL,
928 }
929 }
930}