use crate::boundary::Boundary3D;
use crate::geometry::Geometry3D;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
use u_nesting_core::geom::nalgebra_types::NaVector3 as Vector3;
use u_nesting_core::geometry::{Boundary, Geometry};
#[derive(Debug, Clone, Copy)]
pub struct ExtremePoint {
pub position: Vector3<f64>,
}
impl ExtremePoint {
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self {
position: Vector3::new(x, y, z),
}
}
pub fn pos(&self) -> (f64, f64, f64) {
(self.position.x, self.position.y, self.position.z)
}
}
impl PartialEq for ExtremePoint {
fn eq(&self, other: &Self) -> bool {
(self.position - other.position).norm() < 1e-9
}
}
impl Eq for ExtremePoint {}
#[derive(Debug, Clone)]
struct OrderedEP(ExtremePoint);
impl PartialEq for OrderedEP {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for OrderedEP {}
impl PartialOrd for OrderedEP {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrderedEP {
fn cmp(&self, other: &Self) -> Ordering {
let z_cmp = other
.0
.position
.z
.partial_cmp(&self.0.position.z)
.unwrap_or(Ordering::Equal);
if z_cmp != Ordering::Equal {
return z_cmp;
}
let y_cmp = other
.0
.position
.y
.partial_cmp(&self.0.position.y)
.unwrap_or(Ordering::Equal);
if y_cmp != Ordering::Equal {
return y_cmp;
}
other
.0
.position
.x
.partial_cmp(&self.0.position.x)
.unwrap_or(Ordering::Equal)
}
}
#[derive(Debug, Clone)]
pub struct PlacedBox {
pub id: String,
pub instance: usize,
pub position: Vector3<f64>,
pub dimensions: Vector3<f64>,
pub mass: Option<f64>,
}
impl PlacedBox {
pub fn max_corner(&self) -> Vector3<f64> {
self.position + self.dimensions
}
pub fn overlaps(&self, other: &PlacedBox) -> bool {
let self_max = self.max_corner();
let other_max = other.max_corner();
let no_overlap_x =
self.position.x >= other_max.x - 1e-9 || other.position.x >= self_max.x - 1e-9;
let no_overlap_y =
self.position.y >= other_max.y - 1e-9 || other.position.y >= self_max.y - 1e-9;
let no_overlap_z =
self.position.z >= other_max.z - 1e-9 || other.position.z >= self_max.z - 1e-9;
!(no_overlap_x || no_overlap_y || no_overlap_z)
}
}
pub struct ExtremePointSet {
points: BinaryHeap<OrderedEP>,
container: Vector3<f64>,
placed: Vec<PlacedBox>,
spacing: f64,
margin: f64,
}
impl ExtremePointSet {
pub fn new(boundary: &Boundary3D, margin: f64, spacing: f64) -> Self {
let container = Vector3::new(boundary.width(), boundary.depth(), boundary.height());
let mut eps = Self {
points: BinaryHeap::new(),
container,
placed: Vec::new(),
spacing,
margin,
};
eps.points
.push(OrderedEP(ExtremePoint::new(margin, margin, margin)));
eps
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn placed_count(&self) -> usize {
self.placed.len()
}
pub fn total_volume(&self) -> f64 {
self.placed
.iter()
.map(|b| b.dimensions.x * b.dimensions.y * b.dimensions.z)
.sum()
}
pub fn total_mass(&self) -> f64 {
self.placed.iter().filter_map(|b| b.mass).sum()
}
pub fn placed_boxes(&self) -> &[PlacedBox] {
&self.placed
}
pub fn try_place(
&mut self,
geom: &Geometry3D,
instance: usize,
orientation: usize,
) -> Option<Vector3<f64>> {
let dims = geom.dimensions_for_orientation(orientation);
let mut candidates: Vec<ExtremePoint> = Vec::new();
while let Some(OrderedEP(ep)) = self.points.pop() {
candidates.push(ep);
}
let best_ep_idx = candidates
.iter()
.position(|ep| self.fits_at(ep.position, dims));
let result = if let Some(idx) = best_ep_idx {
let chosen_ep = candidates.remove(idx);
let placed_box = PlacedBox {
id: geom.id().clone(),
instance,
position: chosen_ep.position,
dimensions: dims,
mass: geom.mass(),
};
self.generate_new_eps(&placed_box);
let position = chosen_ep.position;
self.placed.push(placed_box);
Some(position)
} else {
None
};
for ep in candidates {
self.points.push(OrderedEP(ep));
}
result
}
fn generate_new_eps(&mut self, placed: &PlacedBox) {
let box_max = placed.max_corner();
let container_max = self.container - Vector3::new(self.margin, self.margin, self.margin);
if box_max.x < container_max.x - 1e-9 {
self.add_ep_if_valid(ExtremePoint::new(
box_max.x,
placed.position.y,
placed.position.z,
));
}
if box_max.y < container_max.y - 1e-9 {
self.add_ep_if_valid(ExtremePoint::new(
placed.position.x,
box_max.y,
placed.position.z,
));
}
if box_max.z < container_max.z - 1e-9 {
self.add_ep_if_valid(ExtremePoint::new(
placed.position.x,
placed.position.y,
box_max.z,
));
}
}
fn fits_at(&self, pos: Vector3<f64>, dims: Vector3<f64>) -> bool {
const EPS: f64 = 1e-9;
let max = pos + dims;
let m = self.margin;
if pos.x < m - EPS || pos.y < m - EPS || pos.z < m - EPS {
return false;
}
if max.x > self.container.x - m + EPS
|| max.y > self.container.y - m + EPS
|| max.z > self.container.z - m + EPS
{
return false;
}
let s = self.spacing;
for b in &self.placed {
let bmax = b.max_corner();
let separated = pos.x >= bmax.x + s - EPS
|| max.x <= b.position.x - s + EPS
|| pos.y >= bmax.y + s - EPS
|| max.y <= b.position.y - s + EPS
|| pos.z >= bmax.z + s - EPS
|| max.z <= b.position.z - s + EPS;
if !separated {
return false;
}
}
true
}
fn add_ep_if_valid(&mut self, ep: ExtremePoint) {
let container_max = self.container - Vector3::new(self.margin, self.margin, self.margin);
if ep.position.x >= container_max.x - 1e-9
|| ep.position.y >= container_max.y - 1e-9
|| ep.position.z >= container_max.z - 1e-9
{
return;
}
for placed in &self.placed {
let max = placed.max_corner();
if ep.position.x > placed.position.x + 1e-9
&& ep.position.x < max.x - 1e-9
&& ep.position.y > placed.position.y + 1e-9
&& ep.position.y < max.y - 1e-9
&& ep.position.z > placed.position.z + 1e-9
&& ep.position.z < max.z - 1e-9
{
return;
}
}
self.points.push(OrderedEP(ep));
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum EpSelectionStrategy {
#[default]
BottomLeftBack,
BestFit,
FirstFit,
}
pub type EpPlacement = (String, usize, Vector3<f64>, usize);
pub fn run_ep_packing(
geometries: &[Geometry3D],
boundary: &Boundary3D,
margin: f64,
spacing: f64,
max_mass: Option<f64>,
) -> (Vec<EpPlacement>, f64) {
let mut eps = ExtremePointSet::new(boundary, margin, spacing);
let mut placements = Vec::new();
let mut items: Vec<(usize, usize, f64)> = Vec::new();
for (geom_idx, geom) in geometries.iter().enumerate() {
for instance in 0..geom.quantity() {
items.push((geom_idx, instance, geom.measure()));
}
}
items.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(Ordering::Equal));
for (geom_idx, instance, _) in items {
let geom = &geometries[geom_idx];
if let (Some(max), Some(item_mass)) = (max_mass, geom.mass()) {
if eps.total_mass() + item_mass > max {
continue;
}
}
let mut placed = false;
for orientation in 0..geom.allowed_orientations().len() {
if let Some(position) = eps.try_place(geom, instance, orientation) {
placements.push((geom.id().clone(), instance, position, orientation));
placed = true;
break;
}
}
if !placed {
}
}
let utilization = eps.total_volume() / boundary.measure();
(placements, utilization)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extreme_point_creation() {
let ep = ExtremePoint::new(10.0, 20.0, 30.0);
assert_eq!(ep.pos(), (10.0, 20.0, 30.0));
}
#[test]
fn test_extreme_point_set_initial() {
let boundary = Boundary3D::new(100.0, 100.0, 100.0);
let eps = ExtremePointSet::new(&boundary, 0.0, 0.0);
assert_eq!(eps.len(), 1);
assert!(!eps.is_empty());
}
#[test]
fn test_ep_packing_single_box() {
let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)];
let boundary = Boundary3D::new(100.0, 100.0, 100.0);
let (placements, utilization) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
assert_eq!(placements.len(), 1);
assert!(utilization > 0.0);
}
#[test]
fn test_ep_packing_multiple_boxes() {
let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(8)];
let boundary = Boundary3D::new(100.0, 100.0, 100.0);
let (placements, utilization) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
assert_eq!(placements.len(), 8);
assert!(utilization > 0.05);
}
#[test]
fn test_ep_packing_perfect_fill() {
let geometries = vec![Geometry3D::new("cube", 50.0, 50.0, 50.0).with_quantity(8)];
let boundary = Boundary3D::new(100.0, 100.0, 100.0);
let (placements, utilization) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
assert_eq!(
placements.len(),
8,
"EP must place all 8 perfectly-fitting cubes"
);
assert!(
(utilization - 1.0).abs() < 1e-6,
"expected 100% utilization, got {utilization}"
);
let boxes: Vec<PlacedBox> = placements
.iter()
.map(|(id, instance, pos, _)| PlacedBox {
id: id.clone(),
instance: *instance,
position: *pos,
dimensions: Vector3::new(50.0, 50.0, 50.0),
mass: None,
})
.collect();
for (i, a) in boxes.iter().enumerate() {
let m = a.max_corner();
assert!(m.x <= 100.0 + 1e-6 && m.y <= 100.0 + 1e-6 && m.z <= 100.0 + 1e-6);
for b in &boxes[i + 1..] {
assert!(!a.overlaps(b), "perfect-fill placements must not overlap");
}
}
}
#[test]
fn test_ep_packing_all_placements_in_bounds() {
use crate::geometry::OrientationConstraint::Any;
let boundary = Boundary3D::new(100.0, 100.0, 100.0);
let configs = [
("flat-deep", 60.0, 70.0, 15.0, 4usize), ("tall-thin", 40.0, 10.0, 40.0, 20usize), ("issue-mid", 30.0, 20.0, 25.0, 6usize),
];
for (label, w, d, h, qty) in configs {
let geometries = vec![Geometry3D::new(label, w, d, h)
.with_quantity(qty)
.with_orientation(Any)];
let (placements, _) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
for (id, instance, pos, orientation) in &placements {
let dims = geometries[0].dimensions_for_orientation(*orientation);
let eps = 1e-6;
assert!(
pos.x + dims.x <= boundary.width() + eps
&& pos.y + dims.y <= boundary.depth() + eps
&& pos.z + dims.z <= boundary.height() + eps
&& pos.x >= -eps
&& pos.y >= -eps
&& pos.z >= -eps,
"[{label}] {id}#{instance} out of bounds: pos({:.1},{:.1},{:.1}) \
dims({:.1},{:.1},{:.1}) orientation {orientation} exceeds 100x100x100",
pos.x,
pos.y,
pos.z,
dims.x,
dims.y,
dims.z,
);
}
}
}
#[test]
fn test_ep_packing_with_margin() {
let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
let boundary = Boundary3D::new(100.0, 100.0, 100.0);
let (placements, _) = run_ep_packing(&geometries, &boundary, 5.0, 0.0, None);
if !placements.is_empty() {
let (_, _, pos, _) = &placements[0];
assert!(pos.x >= 4.9);
assert!(pos.y >= 4.9);
assert!(pos.z >= 4.9);
}
}
#[test]
fn test_ep_packing_with_spacing() {
let geometries = vec![Geometry3D::new("B1", 40.0, 40.0, 40.0).with_quantity(4)];
let boundary = Boundary3D::new(100.0, 100.0, 100.0);
let (placements_no_spacing, _) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
let (placements_with_spacing, _) = run_ep_packing(&geometries, &boundary, 0.0, 5.0, None);
assert!(placements_with_spacing.len() <= placements_no_spacing.len());
}
#[test]
fn test_placed_box_overlap() {
let box1 = PlacedBox {
id: "A".to_string(),
instance: 0,
position: Vector3::new(0.0, 0.0, 0.0),
dimensions: Vector3::new(10.0, 10.0, 10.0),
mass: None,
};
let box2_overlap = PlacedBox {
id: "B".to_string(),
instance: 0,
position: Vector3::new(5.0, 5.0, 5.0),
dimensions: Vector3::new(10.0, 10.0, 10.0),
mass: None,
};
let box2_no_overlap = PlacedBox {
id: "C".to_string(),
instance: 0,
position: Vector3::new(15.0, 0.0, 0.0),
dimensions: Vector3::new(10.0, 10.0, 10.0),
mass: None,
};
assert!(box1.overlaps(&box2_overlap));
assert!(!box1.overlaps(&box2_no_overlap));
}
#[test]
fn test_ep_packing_orientations() {
use crate::geometry::OrientationConstraint;
let geometries = vec![Geometry3D::new("B1", 80.0, 10.0, 10.0)
.with_quantity(2)
.with_orientation(OrientationConstraint::Any)];
let boundary = Boundary3D::new(100.0, 100.0, 100.0);
let (placements, _) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
assert_eq!(placements.len(), 2);
}
}