use super::model::{
Bin3D, BinLayout3D, BoxDemand3D, ItemInstance3D, MAX_BIN_COUNT_3D, MAX_DIMENSION_3D,
Placement3D, Rotation3D, SolverMetrics3D, ThreeDSolution,
};
#[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FreeCuboid3D {
pub(crate) x: u32,
pub(crate) y: u32,
pub(crate) z: u32,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) depth: u32,
}
#[allow(dead_code)] impl FreeCuboid3D {
pub(crate) fn volume(self) -> u64 {
volume_u64(self.width, self.height, self.depth)
}
pub(crate) fn fits(self, w: u32, h: u32, d: u32) -> bool {
w <= self.width && h <= self.height && d <= self.depth
}
pub(crate) fn intersects(self, other: Self) -> bool {
let self_x_end = self.x.saturating_add(self.width);
let self_y_end = self.y.saturating_add(self.height);
let self_z_end = self.z.saturating_add(self.depth);
let other_x_end = other.x.saturating_add(other.width);
let other_y_end = other.y.saturating_add(other.height);
let other_z_end = other.z.saturating_add(other.depth);
self.x < other_x_end
&& self_x_end > other.x
&& self.y < other_y_end
&& self_y_end > other.y
&& self.z < other_z_end
&& self_z_end > other.z
}
pub(crate) fn contains(self, other: Self) -> bool {
let self_x_end = self.x.saturating_add(self.width);
let self_y_end = self.y.saturating_add(self.height);
let self_z_end = self.z.saturating_add(self.depth);
let other_x_end = other.x.saturating_add(other.width);
let other_y_end = other.y.saturating_add(other.height);
let other_z_end = other.z.saturating_add(other.depth);
self.x <= other.x
&& self.y <= other.y
&& self.z <= other.z
&& self_x_end >= other_x_end
&& self_y_end >= other_y_end
&& self_z_end >= other_z_end
}
}
#[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct ExtremePoint3D {
pub(crate) x: u32,
pub(crate) y: u32,
pub(crate) z: u32,
}
#[allow(dead_code)] pub(crate) fn placements_overlap(a: &Placement3D, b: &Placement3D) -> bool {
let a_x_end = a.x.saturating_add(a.width);
let a_y_end = a.y.saturating_add(a.height);
let a_z_end = a.z.saturating_add(a.depth);
let b_x_end = b.x.saturating_add(b.width);
let b_y_end = b.y.saturating_add(b.height);
let b_z_end = b.z.saturating_add(b.depth);
a.x < b_x_end
&& a_x_end > b.x
&& a.y < b_y_end
&& a_y_end > b.y
&& a.z < b_z_end
&& a_z_end > b.z
}
#[allow(dead_code)] #[allow(clippy::too_many_arguments)] pub(crate) fn placement_feasible(
x: u32,
y: u32,
z: u32,
w: u32,
h: u32,
d: u32,
bin_w: u32,
bin_h: u32,
bin_d: u32,
placed: &[Placement3D],
) -> bool {
if x.saturating_add(w) > bin_w || y.saturating_add(h) > bin_h || z.saturating_add(d) > bin_d {
return false;
}
let candidate = Placement3D {
name: String::new(),
x,
y,
z,
width: w,
height: h,
depth: d,
rotation: Rotation3D::Xyz,
};
placed.iter().all(|other| !placements_overlap(&candidate, other))
}
#[allow(dead_code)] pub(crate) fn check_bin_count_cap(current: usize) -> crate::Result<()> {
if current >= MAX_BIN_COUNT_3D {
return Err(crate::BinPackingError::Unsupported(format!(
"3D bin count cap exceeded: opened {current} bins, MAX_BIN_COUNT_3D = {MAX_BIN_COUNT_3D}"
)));
}
Ok(())
}
#[allow(dead_code)] pub(crate) fn volume_u64(width: u32, height: u32, depth: u32) -> u64 {
debug_assert!(width <= MAX_DIMENSION_3D, "width {width} exceeds MAX_DIMENSION_3D");
debug_assert!(height <= MAX_DIMENSION_3D, "height {height} exceeds MAX_DIMENSION_3D");
debug_assert!(depth <= MAX_DIMENSION_3D, "depth {depth} exceeds MAX_DIMENSION_3D");
u64::from(width) * u64::from(height) * u64::from(depth)
}
#[allow(dead_code)] pub(crate) fn surface_area_u64(a: u32, b: u32) -> u64 {
debug_assert!(a <= MAX_DIMENSION_3D, "a {a} exceeds MAX_DIMENSION_3D");
debug_assert!(b <= MAX_DIMENSION_3D, "b {b} exceeds MAX_DIMENSION_3D");
u64::from(a) * u64::from(b)
}
#[allow(dead_code)] pub(crate) fn layout_volume_breakdown(layout: &BinLayout3D) -> (u64, u64) {
let bin_volume = volume_u64(layout.width, layout.height, layout.depth);
let used = layout
.placements
.iter()
.map(|placement| volume_u64(placement.width, placement.height, placement.depth))
.sum::<u64>();
debug_assert!(used <= bin_volume, "layout used volume exceeds bin volume");
(used, bin_volume.saturating_sub(used))
}
#[allow(dead_code)] pub(crate) type BinPlacements = (usize, Vec<Placement3D>);
#[allow(dead_code)] pub(crate) fn build_solution(
algorithm: impl Into<String>,
bins: &[Bin3D],
bin_placements: Vec<BinPlacements>,
unplaced: Vec<ItemInstance3D>,
metrics: SolverMetrics3D,
guillotine: bool,
) -> crate::Result<ThreeDSolution> {
if bin_placements.len() > MAX_BIN_COUNT_3D {
return Err(crate::BinPackingError::Unsupported(format!(
"3D bin count cap exceeded: solution would consume {} bins, MAX_BIN_COUNT_3D = {MAX_BIN_COUNT_3D}",
bin_placements.len(),
)));
}
let mut layouts = bin_placements
.into_iter()
.map(|(bin_index, placements)| {
let bin = &bins[bin_index];
let used_volume = placements
.iter()
.map(|placement| volume_u64(placement.width, placement.height, placement.depth))
.sum::<u64>();
let bin_volume = volume_u64(bin.width, bin.height, bin.depth);
debug_assert!(used_volume <= bin_volume, "used > bin volume in `{}`", bin.name);
BinLayout3D {
bin_name: bin.name.clone(),
width: bin.width,
height: bin.height,
depth: bin.depth,
cost: bin.cost,
placements,
used_volume,
waste_volume: bin_volume.saturating_sub(used_volume),
}
})
.collect::<Vec<_>>();
layouts.sort_by(|a, b| {
b.used_volume.cmp(&a.used_volume).then_with(|| a.bin_name.cmp(&b.bin_name))
});
let bin_count = layouts.len();
let total_waste_volume = layouts.iter().map(|layout| layout.waste_volume).sum();
let total_cost = layouts.iter().map(|layout| layout.cost).sum();
let mut unplaced_demands: Vec<BoxDemand3D> = unplaced
.into_iter()
.map(|item| BoxDemand3D {
name: item.name,
width: item.width,
height: item.height,
depth: item.depth,
quantity: 1,
allowed_rotations: item.allowed_rotations,
})
.collect();
unplaced_demands.sort_by(|left, right| {
let left_volume = volume_u64(left.width, left.height, left.depth);
let right_volume = volume_u64(right.width, right.height, right.depth);
right_volume.cmp(&left_volume)
});
Ok(ThreeDSolution {
algorithm: algorithm.into(),
exact: false,
lower_bound: None,
guillotine,
bin_count,
total_waste_volume,
total_cost,
layouts,
bin_requirements: Vec::new(),
unplaced: unplaced_demands,
metrics,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::three_d::model::Rotation3D;
fn placement(x: u32, y: u32, z: u32, w: u32, h: u32, d: u32) -> Placement3D {
Placement3D {
name: "x".into(),
x,
y,
z,
width: w,
height: h,
depth: d,
rotation: Rotation3D::Xyz,
}
}
#[test]
fn placements_overlap_detects_corner_intersection() {
let a = placement(0, 0, 0, 2, 2, 2);
let b = placement(1, 1, 1, 2, 2, 2);
assert!(placements_overlap(&a, &b));
}
#[test]
fn placements_overlap_allows_edge_touch() {
let a = placement(0, 0, 0, 2, 2, 2);
let b = placement(2, 0, 0, 2, 2, 2);
assert!(!placements_overlap(&a, &b));
}
#[test]
fn placement_feasible_rejects_off_bin() {
let placed: Vec<Placement3D> = vec![];
assert!(!placement_feasible(8, 0, 0, 4, 4, 4, 10, 10, 10, &placed));
}
#[test]
fn placement_feasible_rejects_overlap() {
let placed = vec![placement(0, 0, 0, 5, 5, 5)];
assert!(!placement_feasible(2, 2, 2, 4, 4, 4, 10, 10, 10, &placed));
}
#[test]
fn placement_feasible_accepts_clear_space() {
let placed = vec![placement(0, 0, 0, 5, 5, 5)];
assert!(placement_feasible(5, 0, 0, 4, 4, 4, 10, 10, 10, &placed));
}
#[test]
fn free_cuboid_intersects_and_contains() {
let a = FreeCuboid3D { x: 0, y: 0, z: 0, width: 10, height: 10, depth: 10 };
let b = FreeCuboid3D { x: 5, y: 5, z: 5, width: 4, height: 4, depth: 4 };
assert!(a.intersects(b));
assert!(a.contains(b));
assert!(!b.contains(a));
}
#[test]
fn free_cuboid_volume_and_fits_helpers() {
let cuboid = FreeCuboid3D { x: 0, y: 0, z: 0, width: 3, height: 4, depth: 5 };
assert_eq!(cuboid.volume(), 60);
assert!(cuboid.fits(3, 4, 5));
assert!(cuboid.fits(1, 1, 1));
assert!(!cuboid.fits(4, 4, 5));
}
#[test]
fn layout_volume_breakdown_reports_used_and_waste() {
let layout = BinLayout3D {
bin_name: "b".into(),
width: 10,
height: 10,
depth: 10,
cost: 1.0,
placements: vec![placement(0, 0, 0, 5, 5, 5), placement(5, 0, 0, 5, 5, 5)],
used_volume: 0,
waste_volume: 0,
};
let (used, waste) = layout_volume_breakdown(&layout);
assert_eq!(used, 250);
assert_eq!(waste, 750);
}
#[test]
fn surface_area_u64_widens_before_multiplying() {
assert_eq!(surface_area_u64(1 << 15, 1 << 15), 1u64 << 30);
}
#[test]
fn check_bin_count_cap_accepts_one_below_limit() {
assert!(check_bin_count_cap(MAX_BIN_COUNT_3D - 1).is_ok());
}
#[test]
fn check_bin_count_cap_rejects_at_limit_with_unsupported() {
let err = check_bin_count_cap(MAX_BIN_COUNT_3D).expect_err("at-limit");
let crate::BinPackingError::Unsupported(message) = err else {
panic!("expected Unsupported, got something else");
};
assert!(
message.contains("3D bin count cap exceeded"),
"stable error message contract: {message}"
);
}
#[test]
fn extreme_point_3d_is_constructible() {
let ep = ExtremePoint3D { x: 1, y: 2, z: 3 };
assert_eq!((ep.x, ep.y, ep.z), (1, 2, 3));
}
}