Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

636
vendor/rectangle-pack/src/bin_section.rs vendored Normal file
View File

@@ -0,0 +1,636 @@
use crate::packed_location::RotatedBy;
use crate::{BoxSizeHeuristicFn, PackedLocation, RectToInsert, WidthHeightDepth};
use core::{
cmp::Ordering,
fmt::{Debug, Display, Error as FmtError, Formatter},
};
mod overlaps;
/// Given two sets of containers, which of these is the more suitable for our packing.
///
/// Useful when we're determining how to split up the remaining volume/area of a box/rectangle.
///
/// For example - we might deem it best to cut the remaining region vertically, or horizontally,
/// or along the Z-axis.
///
/// This decision is based on the more suitable contains heuristic. We determine all 6 possible
/// ways to divide up remaining space, sort them using the more suitable contains heuristic function
/// and choose the best one.
///
/// Ordering::Greater means the first set of containers is better.
/// Ordering::Less means the second set of containers is better.
pub type ComparePotentialContainersFn =
dyn Fn([WidthHeightDepth; 3], [WidthHeightDepth; 3], &BoxSizeHeuristicFn) -> Ordering;
/// Select the container that has the smallest box.
///
/// If there is a tie on the smallest boxes, select whichever also has the second smallest box.
pub fn contains_smallest_box(
mut container1: [WidthHeightDepth; 3],
mut container2: [WidthHeightDepth; 3],
heuristic: &BoxSizeHeuristicFn,
) -> Ordering {
container1.sort_by(|a, b| heuristic(*a).cmp(&heuristic(*b)));
container2.sort_by(|a, b| heuristic(*a).cmp(&heuristic(*b)));
match heuristic(container2[0]).cmp(&heuristic(container1[0])) {
Ordering::Equal => heuristic(container2[1]).cmp(&heuristic(container1[1])),
o => o,
}
}
/// A rectangular section within a target bin that takes up one or more layers
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Ord, PartialOrd)]
pub struct BinSection {
pub(crate) x: u32,
pub(crate) y: u32,
pub(crate) z: u32,
pub(crate) whd: WidthHeightDepth,
}
/// An error while attempting to place a rectangle within a bin section;
#[derive(Debug, Eq, PartialEq)]
#[allow(missing_docs)]
pub enum BinSectionError {
PlacementWiderThanBinSection,
PlacementTallerThanBinSection,
PlacementDeeperThanBinSection,
}
impl Display for BinSectionError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
let err = match self {
BinSectionError::PlacementWiderThanBinSection => {
"Can not place a rectangle inside of a bin that is wider than that rectangle."
}
BinSectionError::PlacementTallerThanBinSection => {
"Can not place a rectangle inside of a bin that is taller than that rectangle."
}
BinSectionError::PlacementDeeperThanBinSection => {
"Can not place a rectangle inside of a bin that is deeper than that rectangle."
}
};
f.write_str(err)
}
}
impl BinSection {
/// Create a new BinSection
pub fn new(x: u32, y: u32, z: u32, whd: WidthHeightDepth) -> Self {
BinSection { x, y, z, whd }
}
// TODO: Delete - just the old API before we had the WidthHeightDepth struct
fn new_spread(x: u32, y: u32, z: u32, width: u32, height: u32, depth: u32) -> Self {
BinSection {
x,
y,
z,
whd: WidthHeightDepth {
width,
height,
depth,
},
}
}
}
impl BinSection {
/// See if a `LayeredRect` can fit inside of this BinSection.
///
/// If it can we return the `BinSection`s that would be created by placing the `LayeredRect`
/// inside of this `BinSection`.
///
/// Consider the diagram below of a smaller box placed into of a larger one.
///
/// The remaining space can be divided into three new sections.
///
/// There are several ways to make this division.
///
/// You could keep all of the space above the smaller box intact and split up the space
/// behind and to the right of it.
///
/// But within that you have a choice between whether the overlapping space goes to right
/// or behind box.
///
/// Or you could keep the space to the right and split the top and behind space.
///
/// etc.
///
/// There are six possible configurations of newly created sections. The configuration to use
/// is decided on based on a a function provided by the consumer.
///
///
/// ```text
/// ┌┬───────────────────┬┐
/// ┌─┘│ ┌─┘│
/// ┌─┘ │ ┌─┘ │
/// ┌─┘ │ ┌─┘ │
/// ┌─┘ │ ┌─┘ │
/// ┌─┘ │ ┌─┘ │
/// ┌─┴──────────┼───────┬─┘ │
/// │ │ │ │
/// │ │ │ │
/// │ ┌┬───┴────┬─┐│ │
/// │ ┌─┘│ ┌─┘ ││ │
/// │ ┌─┘ │ ┌─┘ ││ │
/// │ ┌─┘ │ ┌─┘ ├┼───────────┬┘
/// ├─┴──────┤ ─┘ ││ ┌─┘
/// │ ┌┴─┬───────┬┘│ ┌─┘
/// │ ┌─┘ │ ┌─┘ │ ┌─┘
/// │ ┌─┘ │ ┌─┘ │ ┌─┘
/// │ ┌─┘ │ ┌─┘ │ ┌─┘
/// └─┴────────┴─┴───────┴─┘
/// ```
///
/// # Note
///
/// Written to be readable/maintainable, not to minimize conditional logic, under the
/// (unverified) assumption that a release compilation will inline and dedupe the function
/// calls and conditionals.
pub fn try_place(
&self,
incoming: &RectToInsert,
container_comparison_fn: &ComparePotentialContainersFn,
heuristic_fn: &BoxSizeHeuristicFn,
) -> Result<(PackedLocation, [BinSection; 3]), BinSectionError> {
self.incoming_can_fit(incoming)?;
let mut all_combinations = [
self.depth_largest_height_second_largest_width_smallest(incoming),
self.depth_largest_width_second_largest_height_smallest(incoming),
self.height_largest_depth_second_largest_width_smallest(incoming),
self.height_largest_width_second_largest_depth_smallest(incoming),
self.width_largest_depth_second_largest_height_smallest(incoming),
self.width_largest_height_second_largest_depth_smallest(incoming),
];
all_combinations.sort_by(|a, b| {
container_comparison_fn(
[a[0].whd, a[1].whd, a[2].whd],
[b[0].whd, b[1].whd, b[2].whd],
heuristic_fn,
)
});
let packed_location = PackedLocation {
x: self.x,
y: self.y,
z: self.z,
whd: WidthHeightDepth {
width: incoming.width(),
height: incoming.height(),
depth: incoming.depth(),
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
};
Ok((packed_location, all_combinations[5]))
}
fn incoming_can_fit(&self, incoming: &RectToInsert) -> Result<(), BinSectionError> {
if incoming.width() > self.whd.width {
return Err(BinSectionError::PlacementWiderThanBinSection);
}
if incoming.height() > self.whd.height {
return Err(BinSectionError::PlacementTallerThanBinSection);
}
if incoming.depth() > self.whd.depth {
return Err(BinSectionError::PlacementDeeperThanBinSection);
}
Ok(())
}
fn width_largest_height_second_largest_depth_smallest(
&self,
incoming: &RectToInsert,
) -> [BinSection; 3] {
[
self.empty_space_directly_right(incoming),
self.all_empty_space_above_excluding_behind(incoming),
self.all_empty_space_behind(incoming),
]
}
fn width_largest_depth_second_largest_height_smallest(
&self,
incoming: &RectToInsert,
) -> [BinSection; 3] {
[
self.empty_space_directly_right(incoming),
self.all_empty_space_above(incoming),
self.all_empty_space_behind_excluding_above(incoming),
]
}
fn height_largest_width_second_largest_depth_smallest(
&self,
incoming: &RectToInsert,
) -> [BinSection; 3] {
[
self.all_empty_space_right_excluding_behind(incoming),
self.empty_space_directly_above(incoming),
self.all_empty_space_behind(incoming),
]
}
fn height_largest_depth_second_largest_width_smallest(
&self,
incoming: &RectToInsert,
) -> [BinSection; 3] {
[
self.all_empty_space_right(incoming),
self.empty_space_directly_above(incoming),
self.all_empty_space_behind_excluding_right(incoming),
]
}
fn depth_largest_width_second_largest_height_smallest(
&self,
incoming: &RectToInsert,
) -> [BinSection; 3] {
[
self.all_empty_space_right_excluding_above(incoming),
self.all_empty_space_above(incoming),
self.empty_space_directly_behind(incoming),
]
}
fn depth_largest_height_second_largest_width_smallest(
&self,
incoming: &RectToInsert,
) -> [BinSection; 3] {
[
self.all_empty_space_right(incoming),
self.all_empty_space_above_excluding_right(incoming),
self.empty_space_directly_behind(incoming),
]
}
fn all_empty_space_above(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new_spread(
self.x,
self.y + incoming.height(),
self.z,
self.whd.width,
self.whd.height - incoming.height(),
self.whd.depth,
)
}
fn all_empty_space_right(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new_spread(
self.x + incoming.width(),
self.y,
self.z,
self.whd.width - incoming.width(),
self.whd.height,
self.whd.depth,
)
}
fn all_empty_space_behind(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new_spread(
self.x,
self.y,
self.z + incoming.depth(),
self.whd.width,
self.whd.height,
self.whd.depth - incoming.depth(),
)
}
fn empty_space_directly_above(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new_spread(
self.x,
self.y + incoming.height(),
self.z,
incoming.width(),
self.whd.height - incoming.height(),
incoming.depth(),
)
}
fn empty_space_directly_right(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new_spread(
self.x + incoming.width(),
self.y,
self.z,
self.whd.width - incoming.width(),
incoming.height(),
incoming.depth(),
)
}
fn empty_space_directly_behind(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new(
self.x,
self.y,
self.z + incoming.depth(),
WidthHeightDepth {
width: incoming.width(),
height: incoming.height(),
depth: self.whd.depth - incoming.depth(),
},
)
}
fn all_empty_space_above_excluding_right(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new(
self.x,
self.y + incoming.height(),
self.z,
WidthHeightDepth {
width: incoming.width(),
height: self.whd.height - incoming.height(),
depth: self.whd.depth,
},
)
}
fn all_empty_space_above_excluding_behind(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new(
self.x,
self.y + incoming.height(),
self.z,
WidthHeightDepth {
width: self.whd.width,
height: self.whd.height - incoming.height(),
depth: incoming.depth(),
},
)
}
fn all_empty_space_right_excluding_above(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new(
self.x + incoming.width(),
self.y,
self.z,
WidthHeightDepth {
width: self.whd.width - incoming.width(),
height: incoming.height(),
depth: self.whd.depth,
},
)
}
fn all_empty_space_right_excluding_behind(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new(
self.x + incoming.width(),
self.y,
self.z,
WidthHeightDepth {
width: self.whd.width - incoming.width(),
height: self.whd.height,
depth: incoming.depth(),
},
)
}
fn all_empty_space_behind_excluding_above(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new(
self.x,
self.y,
self.z + incoming.depth(),
WidthHeightDepth {
width: self.whd.width,
height: incoming.height(),
depth: self.whd.depth - incoming.depth(),
},
)
}
fn all_empty_space_behind_excluding_right(&self, incoming: &RectToInsert) -> BinSection {
BinSection::new(
self.x,
self.y,
self.z + incoming.depth(),
WidthHeightDepth {
width: incoming.width(),
height: self.whd.height,
depth: self.whd.depth - incoming.depth(),
},
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{volume_heuristic, RectToInsert};
const BIGGEST: u32 = 50;
const MIDDLE: u32 = 25;
const SMALLEST: u32 = 10;
const FULL: u32 = 100;
/// If we're trying to place a rectangle that is wider than the container we return an error
#[test]
fn error_if_placement_is_wider_than_bin_section() {
let bin_section = bin_section_width_height_depth(5, 20, 1);
let placement = RectToInsert::new(6, 20, 1);
assert_eq!(
bin_section
.try_place(&placement, &contains_smallest_box, &volume_heuristic)
.unwrap_err(),
BinSectionError::PlacementWiderThanBinSection
);
}
/// If we're trying to place a rectangle that is taller than the container we return an error
#[test]
fn error_if_placement_is_taller_than_bin_section() {
let bin_section = bin_section_width_height_depth(5, 20, 1);
let placement = RectToInsert::new(5, 21, 1);
assert_eq!(
bin_section
.try_place(&placement, &contains_smallest_box, &volume_heuristic)
.unwrap_err(),
BinSectionError::PlacementTallerThanBinSection
);
}
/// If we're trying to place a rectangle that is deeper than the container we return an error
#[test]
fn error_if_placement_is_deeper_than_bin_section() {
let bin_section = bin_section_width_height_depth(5, 20, 1);
let placement = RectToInsert::new(5, 20, 2);
assert_eq!(
bin_section
.try_place(&placement, &contains_smallest_box, &volume_heuristic)
.unwrap_err(),
BinSectionError::PlacementDeeperThanBinSection
);
}
fn test_splits(
container_dimensions: u32,
rect_to_place: WidthHeightDepth,
mut expected: [BinSection; 3],
) {
let dim = container_dimensions;
let bin_section = bin_section_width_height_depth(dim, dim, dim);
let whd = rect_to_place;
let placement = RectToInsert::new(whd.width, whd.height, whd.depth);
let mut packed = bin_section
.try_place(&placement, &contains_smallest_box, &volume_heuristic)
.unwrap();
packed.1.sort();
expected.sort();
assert_eq!(packed.1, expected);
}
/// Verify that we choose the correct splits when the placed rectangle is width > height > depth
#[test]
fn width_largest_height_second_largest_depth_smallest() {
let whd = WidthHeightDepth {
width: BIGGEST,
height: MIDDLE,
depth: SMALLEST,
};
test_splits(
FULL,
whd,
[
BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, whd.height, whd.depth),
BinSection::new_spread(0, whd.height, 0, FULL, FULL - whd.height, whd.depth),
BinSection::new_spread(0, 0, whd.depth, FULL, FULL, FULL - whd.depth),
],
);
}
/// Verify that we choose the correct splits when the placed rectangle is width > depth > height
#[test]
fn width_largest_depth_second_largest_height_smallest() {
let whd = WidthHeightDepth {
width: BIGGEST,
height: SMALLEST,
depth: MIDDLE,
};
test_splits(
FULL,
whd,
[
BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, whd.height, whd.depth),
BinSection::new_spread(0, whd.height, 0, FULL, FULL - whd.height, FULL),
BinSection::new_spread(0, 0, whd.depth, FULL, whd.height, FULL - whd.depth),
],
);
}
/// Verify that we choose the correct splits when the placed rectangle is height > width > depth
#[test]
fn height_largest_width_second_largest_depth_smallest() {
let whd = WidthHeightDepth {
width: MIDDLE,
height: BIGGEST,
depth: SMALLEST,
};
test_splits(
FULL,
whd,
[
BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, FULL, whd.depth),
BinSection::new_spread(0, whd.height, 0, whd.width, FULL - whd.height, whd.depth),
BinSection::new_spread(0, 0, whd.depth, FULL, FULL, FULL - whd.depth),
],
);
}
/// Verify that we choose the correct splits when the placed rectangle is height > depth > width
#[test]
fn height_largest_depth_second_largest_width_smallest() {
let whd = WidthHeightDepth {
width: SMALLEST,
height: BIGGEST,
depth: MIDDLE,
};
test_splits(
FULL,
whd,
[
BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, FULL, FULL),
BinSection::new_spread(0, whd.height, 0, whd.width, FULL - whd.height, whd.depth),
BinSection::new_spread(0, 0, whd.depth, whd.width, FULL, FULL - whd.depth),
],
);
}
/// Verify that we choose the correct splits when the placed rectangle is depth > width > height
#[test]
fn depth_largest_width_second_largest_height_smallest() {
let whd = WidthHeightDepth {
width: MIDDLE,
height: SMALLEST,
depth: BIGGEST,
};
test_splits(
FULL,
whd,
[
BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, whd.height, FULL),
BinSection::new_spread(0, whd.height, 0, FULL, FULL - whd.height, FULL),
BinSection::new_spread(0, 0, whd.depth, whd.width, whd.height, FULL - whd.depth),
],
);
}
/// Verify that we choose the correct splits when the placed rectangle is depth > height > width
#[test]
fn depth_largest_height_second_largest_width_smallest() {
let whd = WidthHeightDepth {
width: SMALLEST,
height: MIDDLE,
depth: BIGGEST,
};
test_splits(
FULL,
whd,
[
BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, FULL, FULL),
BinSection::new_spread(0, whd.height, 0, whd.width, FULL - whd.height, FULL),
BinSection::new_spread(0, 0, whd.depth, whd.width, whd.height, FULL - whd.depth),
],
);
}
// #[test]
// fn todo() {
// unimplemented!("Add tests for supporting rotation");
// }
fn bin_section_width_height_depth(width: u32, height: u32, depth: u32) -> BinSection {
BinSection::new(
0,
0,
0,
WidthHeightDepth {
width,
height,
depth,
},
)
}
}

View File

@@ -0,0 +1,86 @@
use crate::bin_section::BinSection;
impl BinSection {
/// Whether or not two bin sections overlap each other.
pub fn overlaps(&self, other: &Self) -> bool {
(self.x >= other.x && self.x <= other.right())
&& (self.y >= other.y && self.y <= other.top())
&& (self.z >= other.z && self.z <= other.back())
}
fn right(&self) -> u32 {
self.x + (self.whd.width - 1)
}
fn top(&self) -> u32 {
self.y + (self.whd.height - 1)
}
fn back(&self) -> u32 {
self.z + (self.whd.depth - 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::width_height_depth::WidthHeightDepth;
/// Verify that the overlaps method works properly.
#[test]
fn overlaps() {
OverlapsTest {
label: "Overlaps X, Y and Z",
section1: BinSection::new(3, 4, 5, WidthHeightDepth::new(1, 1, 1)),
section2: section_2_3_4(),
expected_overlap: true,
}
.test();
OverlapsTest {
label: "Overlaps X only",
section1: BinSection::new(3, 40, 50, WidthHeightDepth::new(1, 1, 1)),
section2: section_2_3_4(),
expected_overlap: false,
}
.test();
OverlapsTest {
label: "Overlaps Y only",
section1: BinSection::new(30, 4, 50, WidthHeightDepth::new(1, 1, 1)),
section2: section_2_3_4(),
expected_overlap: false,
}
.test();
OverlapsTest {
label: "Overlaps Z only",
section1: BinSection::new(30, 40, 5, WidthHeightDepth::new(1, 1, 1)),
section2: section_2_3_4(),
expected_overlap: false,
}
.test();
}
fn section_2_3_4() -> BinSection {
BinSection::new(2, 3, 4, WidthHeightDepth::new(2, 3, 4))
}
struct OverlapsTest {
label: &'static str,
section1: BinSection,
section2: BinSection,
expected_overlap: bool,
}
impl OverlapsTest {
fn test(self) {
assert_eq!(
self.section1.overlaps(&self.section2),
self.expected_overlap,
"{}",
self.label
)
}
}
}

View File

@@ -0,0 +1,13 @@
use crate::WidthHeightDepth;
/// Incoming boxes are places into the smallest hole that will fit them.
///
/// "small" vs. "large" is based on the heuristic function.
///
/// A larger heuristic means that the box is larger.
pub type BoxSizeHeuristicFn = dyn Fn(WidthHeightDepth) -> u128;
/// The volume of the box
pub fn volume_heuristic(whd: WidthHeightDepth) -> u128 {
whd.width as u128 * whd.height as u128 * whd.depth as u128
}

View File

@@ -0,0 +1,202 @@
use crate::RectToInsert;
#[cfg(not(std))]
use alloc::collections::BTreeMap as KeyValMap;
#[cfg(std)]
use std::collections::HashMap as KeyValMap;
use alloc::{
collections::{btree_map::Entry, BTreeMap},
vec::Vec,
};
use core::{fmt::Debug, hash::Hash};
/// Groups of rectangles that need to be placed into bins.
///
/// When placing groups a heuristic is used to determine which groups are the largest.
/// Larger groups are placed first.
///
/// A group's heuristic is computed by calculating the heuristic of all of the rectangles inside
/// the group and then summing them.
#[derive(Debug)]
pub struct GroupedRectsToPlace<RectToPlaceId, GroupId = ()>
where
RectToPlaceId: Debug + Hash + Eq + Ord + PartialOrd,
GroupId: Debug + Hash + Eq + Ord + PartialOrd,
{
// FIXME: inbound_id_to_group_id appears to be unused. If so, remove it. Also remove the
// Hash and Eq constraints on RectToPlaceId if we remove this map
pub(crate) inbound_id_to_group_ids:
KeyValMap<RectToPlaceId, Vec<Group<GroupId, RectToPlaceId>>>,
pub(crate) group_id_to_inbound_ids: BTreeMap<Group<GroupId, RectToPlaceId>, Vec<RectToPlaceId>>,
pub(crate) rects: KeyValMap<RectToPlaceId, RectToInsert>,
}
/// A group of rectangles that need to be placed together
#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub enum Group<GroupId, RectToPlaceId>
where
GroupId: Debug + Hash + Eq + PartialEq + Ord + PartialOrd,
RectToPlaceId: Debug + Ord + PartialOrd,
{
/// An automatically generated (auto incrementing) group identifier for rectangles that were
/// passed in without any associated group ids.
///
/// We still want to treat these lone rectangles as their own "groups" so that we can more
/// easily compare their heuristics against those of other groups.
///
/// If everything is a "group" - comparing groups becomes simpler.
Ungrouped(RectToPlaceId),
/// Wraps a user provided group identifier.
Grouped(GroupId),
}
impl<RectToPlaceId, GroupId> GroupedRectsToPlace<RectToPlaceId, GroupId>
where
RectToPlaceId: Debug + Hash + Clone + Eq + Ord + PartialOrd,
GroupId: Debug + Hash + Clone + Eq + Ord + PartialOrd,
{
/// Create a new `LayeredRectGroups`
pub fn new() -> Self {
Self {
inbound_id_to_group_ids: Default::default(),
group_id_to_inbound_ids: Default::default(),
rects: Default::default(),
}
}
/// Push one or more rectangles
///
/// # Panics
///
/// Panics if a `Some(Vec<GroupId>)` passed in but the length is 0, as this is likely a
/// mistake and `None` should be used instead.
pub fn push_rect(
&mut self,
inbound_id: RectToPlaceId,
group_ids: Option<Vec<GroupId>>,
inbound: RectToInsert,
) {
self.rects.insert(inbound_id.clone(), inbound);
match group_ids {
None => {
self.group_id_to_inbound_ids.insert(
Group::Ungrouped(inbound_id.clone()),
vec![inbound_id.clone()],
);
self.inbound_id_to_group_ids
.insert(inbound_id.clone(), vec![Group::Ungrouped(inbound_id)]);
}
Some(group_ids) => {
self.inbound_id_to_group_ids.insert(
inbound_id.clone(),
group_ids
.clone()
.into_iter()
.map(|gid| Group::Grouped(gid))
.collect(),
);
for group_id in group_ids {
match self.group_id_to_inbound_ids.entry(Group::Grouped(group_id)) {
Entry::Occupied(mut o) => {
o.get_mut().push(inbound_id.clone());
}
Entry::Vacant(v) => {
v.insert(vec![inbound_id.clone()]);
}
};
}
}
};
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::RectToInsert;
/// Verify that if we insert a rectangle that doesn't have a group it is given a group ID based
/// on its RectToPlaceId.
#[test]
fn ungrouped_rectangles_use_their_inbound_id_as_their_group_id() {
let mut lrg: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
lrg.push_rect(RectToPlaceId::One, None, RectToInsert::new(10, 10, 1));
assert_eq!(
lrg.group_id_to_inbound_ids[&Group::Ungrouped(RectToPlaceId::One)],
vec![RectToPlaceId::One]
);
}
/// When multiple different rects from the same group are pushed they should be present in the
/// map of group id -> inbound rect id
#[test]
fn group_id_to_inbound_ids() {
let mut lrg = GroupedRectsToPlace::new();
lrg.push_rect(
RectToPlaceId::One,
Some(vec![0]),
RectToInsert::new(10, 10, 1),
);
lrg.push_rect(
RectToPlaceId::Two,
Some(vec![0]),
RectToInsert::new(10, 10, 1),
);
assert_eq!(
lrg.group_id_to_inbound_ids.get(&Group::Grouped(0)).unwrap(),
&vec![RectToPlaceId::One, RectToPlaceId::Two]
);
}
/// Verify that we store the map of inbound id -> group ids
#[test]
fn inbound_id_to_group_ids() {
let mut lrg = GroupedRectsToPlace::new();
lrg.push_rect(
RectToPlaceId::One,
Some(vec![0, 1]),
RectToInsert::new(10, 10, 1),
);
lrg.push_rect(RectToPlaceId::Two, None, RectToInsert::new(10, 10, 1));
assert_eq!(
lrg.inbound_id_to_group_ids[&RectToPlaceId::One],
vec![Group::Grouped(0), Group::Grouped(1)]
);
assert_eq!(
lrg.inbound_id_to_group_ids[&RectToPlaceId::Two],
vec![Group::Ungrouped(RectToPlaceId::Two)]
);
}
/// Verify that we store in rectangle associated with its inbound ID
#[test]
fn store_the_inbound_rectangle() {
let mut lrg = GroupedRectsToPlace::new();
lrg.push_rect(
RectToPlaceId::One,
Some(vec![0, 1]),
RectToInsert::new(10, 10, 1),
);
assert_eq!(lrg.rects[&RectToPlaceId::One], RectToInsert::new(10, 10, 1));
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
enum RectToPlaceId {
One,
Two,
}
}

849
vendor/rectangle-pack/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,849 @@
//! `rectangle-pack` is a library focused on laying out any number of smaller rectangles
//! (both 2d rectangles and 3d rectangular prisms) inside any number of larger rectangles.
#![cfg_attr(not(std), no_std)]
#![deny(missing_docs)]
#[macro_use]
extern crate alloc;
#[cfg(not(std))]
use alloc::collections::BTreeMap as KeyValMap;
#[cfg(std)]
use std::collections::HashMap as KeyValMap;
use alloc::{collections::BTreeMap, vec::Vec};
use core::{
fmt::{Debug, Display, Error as FmtError, Formatter},
hash::Hash,
};
pub use crate::bin_section::contains_smallest_box;
pub use crate::bin_section::BinSection;
pub use crate::bin_section::ComparePotentialContainersFn;
use crate::grouped_rects_to_place::Group;
pub use crate::grouped_rects_to_place::GroupedRectsToPlace;
pub use crate::target_bin::TargetBin;
use crate::width_height_depth::WidthHeightDepth;
pub use self::box_size_heuristics::{volume_heuristic, BoxSizeHeuristicFn};
pub use self::rect_to_insert::RectToInsert;
pub use crate::packed_location::PackedLocation;
mod bin_section;
mod grouped_rects_to_place;
mod packed_location;
mod rect_to_insert;
mod target_bin;
mod width_height_depth;
mod box_size_heuristics;
/// Determine how to fit a set of incoming rectangles (2d or 3d) into a set of target bins.
///
/// ## Example
///
/// ```
/// //! A basic example of packing rectangles into target bins
///
/// use rectangle_pack::{
/// GroupedRectsToPlace,
/// RectToInsert,
/// pack_rects,
/// TargetBin,
/// volume_heuristic,
/// contains_smallest_box
/// };
/// use std::collections::BTreeMap;
///
/// // A rectangle ID just needs to meet these trait bounds (ideally also Copy).
/// // So you could use a String, PathBuf, or any other type that meets these
/// // trat bounds. You do not have to use a custom enum.
/// #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)]
/// enum MyCustomRectId {
/// RectOne,
/// RectTwo,
/// RectThree,
/// }
///
/// // A target bin ID just needs to meet these trait bounds (ideally also Copy)
/// // So you could use a u32, &str, or any other type that meets these
/// // trat bounds. You do not have to use a custom enum.
/// #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)]
/// enum MyCustomBinId {
/// DestinationBinOne,
/// DestinationBinTwo,
/// }
///
/// // A placement group just needs to meet these trait bounds (ideally also Copy).
/// //
/// // Groups allow you to ensure that a set of rectangles will be placed
/// // into the same bin. If this isn't possible an error is returned.
/// //
/// // Groups are optional.
/// //
/// // You could use an i32, &'static str, or any other type that meets these
/// // trat bounds. You do not have to use a custom enum.
/// #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)]
/// enum MyCustomGroupId {
/// GroupIdOne
/// }
///
/// let mut rects_to_place = GroupedRectsToPlace::new();
/// rects_to_place.push_rect(
/// MyCustomRectId::RectOne,
/// Some(vec![MyCustomGroupId::GroupIdOne]),
/// RectToInsert::new(10, 20, 255)
/// );
/// rects_to_place.push_rect(
/// MyCustomRectId::RectTwo,
/// Some(vec![MyCustomGroupId::GroupIdOne]),
/// RectToInsert::new(5, 50, 255)
/// );
/// rects_to_place.push_rect(
/// MyCustomRectId::RectThree,
/// None,
/// RectToInsert::new(30, 30, 255)
/// );
///
/// let mut target_bins = BTreeMap::new();
/// target_bins.insert(MyCustomBinId::DestinationBinOne, TargetBin::new(2048, 2048, 255));
/// target_bins.insert(MyCustomBinId::DestinationBinTwo, TargetBin::new(4096, 4096, 1020));
///
/// // Information about where each `MyCustomRectId` was placed
/// let rectangle_placements = pack_rects(
/// &rects_to_place,
/// &mut target_bins,
/// &volume_heuristic,
/// &contains_smallest_box
/// ).unwrap();
/// ```
///
/// ## Algorithm
///
/// The algorithm was originally inspired by [rectpack2D] and then modified to work in 3D.
///
/// [rectpack2D]: https://github.com/TeamHypersomnia/rectpack2D
///
/// ## TODO:
///
/// Optimize - plenty of room to remove clones and duplication .. etc
pub fn pack_rects<
RectToPlaceId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
BinId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
GroupId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
>(
rects_to_place: &GroupedRectsToPlace<RectToPlaceId, GroupId>,
target_bins: &mut BTreeMap<BinId, TargetBin>,
box_size_heuristic: &BoxSizeHeuristicFn,
more_suitable_containers_fn: &ComparePotentialContainersFn,
) -> Result<RectanglePackOk<RectToPlaceId, BinId>, RectanglePackError> {
let mut packed_locations = KeyValMap::new();
let mut target_bins: Vec<(&BinId, &mut TargetBin)> = target_bins.iter_mut().collect();
sort_bins_smallest_to_largest(&mut target_bins, box_size_heuristic);
let mut group_id_to_inbound_ids: Vec<(&Group<GroupId, RectToPlaceId>, &Vec<RectToPlaceId>)> =
rects_to_place.group_id_to_inbound_ids.iter().collect();
sort_groups_largest_to_smallest(
&mut group_id_to_inbound_ids,
rects_to_place,
box_size_heuristic,
);
'group: for (_group_id, rects_to_place_ids) in group_id_to_inbound_ids {
for (bin_id, bin) in target_bins.iter_mut() {
if !can_fit_entire_group_into_bin(
bin.clone(),
&rects_to_place_ids[..],
rects_to_place,
box_size_heuristic,
more_suitable_containers_fn,
) {
continue;
}
'incoming: for rect_to_place_id in rects_to_place_ids.iter() {
if bin.available_bin_sections.len() == 0 {
continue;
}
let _bin_clone = bin.clone();
let mut bin_sections = bin.available_bin_sections.clone();
let last_section_idx = bin_sections.len() - 1;
let mut sections_tried = 0;
'section: while let Some(remaining_section) = bin_sections.pop() {
let rect_to_place = rects_to_place.rects[&rect_to_place_id];
let placement = remaining_section.try_place(
&rect_to_place,
more_suitable_containers_fn,
box_size_heuristic,
);
if placement.is_err() {
sections_tried += 1;
continue 'section;
}
let (placement, mut new_sections) = placement.unwrap();
sort_by_size_largest_to_smallest(&mut new_sections, box_size_heuristic);
bin.remove_filled_section(last_section_idx - sections_tried);
bin.add_new_sections(new_sections);
packed_locations.insert(rect_to_place_id.clone(), (bin_id.clone(), placement));
continue 'incoming;
}
}
continue 'group;
}
return Err(RectanglePackError::NotEnoughBinSpace);
}
Ok(RectanglePackOk { packed_locations })
}
// TODO: This is duplicative of the code above
fn can_fit_entire_group_into_bin<RectToPlaceId, GroupId>(
mut bin: TargetBin,
group: &[RectToPlaceId],
rects_to_place: &GroupedRectsToPlace<RectToPlaceId, GroupId>,
box_size_heuristic: &BoxSizeHeuristicFn,
more_suitable_containers_fn: &ComparePotentialContainersFn,
) -> bool
where
RectToPlaceId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
GroupId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
{
'incoming: for rect_to_place_id in group.iter() {
if bin.available_bin_sections.len() == 0 {
return false;
}
let mut bin_sections = bin.available_bin_sections.clone();
let last_section_idx = bin_sections.len() - 1;
let mut sections_tried = 0;
'section: while let Some(remaining_section) = bin_sections.pop() {
let rect_to_place = rects_to_place.rects[&rect_to_place_id];
let placement = remaining_section.try_place(
&rect_to_place,
more_suitable_containers_fn,
box_size_heuristic,
);
if placement.is_err() {
sections_tried += 1;
continue 'section;
}
let (_placement, mut new_sections) = placement.unwrap();
sort_by_size_largest_to_smallest(&mut new_sections, box_size_heuristic);
bin.remove_filled_section(last_section_idx - sections_tried);
bin.add_new_sections(new_sections);
continue 'incoming;
}
return false;
}
true
}
/// Information about successfully packed rectangles.
#[derive(Debug, PartialEq)]
pub struct RectanglePackOk<RectToPlaceId: PartialEq + Eq + Hash, BinId: PartialEq + Eq + Hash> {
packed_locations: KeyValMap<RectToPlaceId, (BinId, PackedLocation)>,
// TODO: Other information such as information about how the bins were packed
// (perhaps percentage filled)
}
impl<RectToPlaceId: PartialEq + Eq + Hash, BinId: PartialEq + Eq + Hash>
RectanglePackOk<RectToPlaceId, BinId>
{
/// Indicates where every incoming rectangle was placed
pub fn packed_locations(&self) -> &KeyValMap<RectToPlaceId, (BinId, PackedLocation)> {
&self.packed_locations
}
}
/// An error while attempting to pack rectangles into bins.
#[derive(Debug, PartialEq)]
pub enum RectanglePackError {
/// The rectangles can't be placed into the bins. More bin space needs to be provided.
NotEnoughBinSpace,
}
#[cfg(std)]
impl std::error::Error for RectanglePackError {}
impl Display for RectanglePackError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
match self {
RectanglePackError::NotEnoughBinSpace => {
f.write_str("Not enough space to place all of the rectangles.")
}
}
}
}
fn sort_bins_smallest_to_largest<BinId>(
bins: &mut Vec<(&BinId, &mut TargetBin)>,
box_size_heuristic: &BoxSizeHeuristicFn,
) where
BinId: Debug + Hash + PartialEq + Eq + Clone,
{
bins.sort_by(|a, b| {
box_size_heuristic(WidthHeightDepth {
width: a.1.max_width,
height: a.1.max_height,
depth: a.1.max_depth,
})
.cmp(&box_size_heuristic(WidthHeightDepth {
width: b.1.max_width,
height: b.1.max_height,
depth: b.1.max_depth,
}))
});
}
fn sort_by_size_largest_to_smallest(
items: &mut [BinSection; 3],
box_size_heuristic: &BoxSizeHeuristicFn,
) {
items.sort_by(|a, b| box_size_heuristic(b.whd).cmp(&box_size_heuristic(a.whd)));
}
fn sort_groups_largest_to_smallest<GroupId, RectToPlaceId>(
group_id_to_inbound_ids: &mut Vec<(&Group<GroupId, RectToPlaceId>, &Vec<RectToPlaceId>)>,
incoming_groups: &GroupedRectsToPlace<RectToPlaceId, GroupId>,
box_size_heuristic: &BoxSizeHeuristicFn,
) where
RectToPlaceId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
GroupId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
{
group_id_to_inbound_ids.sort_by(|a, b| {
let a_heuristic =
a.1.iter()
.map(|inbound| {
let rect = incoming_groups.rects[inbound];
box_size_heuristic(rect.whd)
})
.sum();
let b_heuristic: u128 =
b.1.iter()
.map(|inbound| {
let rect = incoming_groups.rects[inbound];
box_size_heuristic(rect.whd)
})
.sum();
b_heuristic.cmp(&a_heuristic)
});
}
#[cfg(test)]
mod tests {
use crate::{pack_rects, volume_heuristic, RectToInsert, RectanglePackError, TargetBin};
use super::*;
use crate::packed_location::RotatedBy;
/// If the provided rectangles can't fit into the provided bins.
#[test]
fn error_if_the_rectangles_cannot_fit_into_target_bins() {
let mut targets = BTreeMap::new();
targets.insert(BinId::Three, TargetBin::new(2, 100, 1));
let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(3, 1, 1));
match pack_rects(
&groups,
&mut targets,
&volume_heuristic,
&contains_smallest_box,
)
.unwrap_err()
{
RectanglePackError::NotEnoughBinSpace => {}
};
}
/// Rectangles in the same group need to be placed in the same bin.
///
/// Here we create two Rectangles in the same group and create two bins that could fit them
/// individually but cannot fit them together.
///
/// Then we verify that we receive an error for being unable to place the group.
#[test]
fn error_if_cannot_fit_group() {
let mut targets = BTreeMap::new();
targets.insert(BinId::Three, TargetBin::new(100, 100, 1));
targets.insert(BinId::Four, TargetBin::new(100, 100, 1));
let mut groups = GroupedRectsToPlace::new();
groups.push_rect(
RectToPlaceId::One,
Some(vec!["A Group"]),
RectToInsert::new(100, 100, 1),
);
groups.push_rect(
RectToPlaceId::Two,
Some(vec!["A Group"]),
RectToInsert::new(100, 100, 1),
);
match pack_rects(
&groups,
&mut targets,
&volume_heuristic,
&contains_smallest_box,
)
.unwrap_err()
{
RectanglePackError::NotEnoughBinSpace => {}
};
}
/// If we provide a single inbound rectangle and a single bin - it should be placed into that
/// bin.
#[test]
fn one_inbound_rect_one_bin() {
let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(1, 2, 1));
let mut targets = BTreeMap::new();
targets.insert(BinId::Three, TargetBin::new(5, 5, 1));
let packed = pack_rects(
&groups,
&mut targets,
&volume_heuristic,
&contains_smallest_box,
)
.unwrap();
let locations = packed.packed_locations;
assert_eq!(locations.len(), 1);
assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,);
assert_eq!(
locations[&RectToPlaceId::One].1,
PackedLocation {
x: 0,
y: 0,
z: 0,
whd: WidthHeightDepth {
width: 1,
height: 2,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
)
}
/// If we have one inbound rect and two bins, it should be placed into the smallest bin.
#[test]
fn one_inbound_rect_two_bins() {
let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(2, 2, 1));
let mut targets = BTreeMap::new();
targets.insert(BinId::Three, TargetBin::new(5, 5, 1));
targets.insert(BinId::Four, TargetBin::new(5, 5, 2));
let packed = pack_rects(
&groups,
&mut targets,
&volume_heuristic,
&contains_smallest_box,
)
.unwrap();
let locations = packed.packed_locations;
assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,);
assert_eq!(locations.len(), 1);
assert_eq!(
locations[&RectToPlaceId::One].1,
PackedLocation {
x: 0,
y: 0,
z: 0,
whd: WidthHeightDepth {
width: 2,
height: 2,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
)
}
/// If we have two inbound rects the largest one should be placed first.
#[test]
fn places_largest_rectangles_first() {
let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(10, 10, 1));
groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(5, 5, 1));
let mut targets = BTreeMap::new();
targets.insert(BinId::Three, TargetBin::new(20, 20, 2));
let packed = pack_rects(
&groups,
&mut targets,
&volume_heuristic,
&contains_smallest_box,
)
.unwrap();
let locations = packed.packed_locations;
assert_eq!(locations.len(), 2);
assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,);
assert_eq!(locations[&RectToPlaceId::Two].0, BinId::Three,);
assert_eq!(
locations[&RectToPlaceId::One].1,
PackedLocation {
x: 0,
y: 0,
z: 0,
whd: WidthHeightDepth {
width: 10,
height: 10,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
);
assert_eq!(
locations[&RectToPlaceId::Two].1,
PackedLocation {
x: 10,
y: 0,
z: 0,
whd: WidthHeightDepth {
width: 5,
height: 5,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
)
}
/// We have two rectangles and two bins. Each bin has enough space to fit one rectangle.
///
/// 1. First place the largest rectangle into the smallest bin.
///
/// 2. Second place the remaining rectangle into the next available bin (i.e. the largest one).
#[test]
fn two_rects_two_bins() {
let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(15, 15, 1));
groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(20, 20, 1));
let mut targets = BTreeMap::new();
targets.insert(BinId::Three, TargetBin::new(20, 20, 1));
targets.insert(BinId::Four, TargetBin::new(50, 50, 1));
let packed = pack_rects(
&groups,
&mut targets,
&volume_heuristic,
&contains_smallest_box,
)
.unwrap();
let locations = packed.packed_locations;
assert_eq!(locations.len(), 2);
assert_eq!(locations[&RectToPlaceId::One].0, BinId::Four,);
assert_eq!(locations[&RectToPlaceId::Two].0, BinId::Three,);
assert_eq!(
locations[&RectToPlaceId::One].1,
PackedLocation {
x: 0,
y: 0,
z: 0,
whd: WidthHeightDepth {
width: 15,
height: 15,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
);
assert_eq!(
locations[&RectToPlaceId::Two].1,
PackedLocation {
x: 0,
y: 0,
z: 0,
whd: WidthHeightDepth {
width: 20,
height: 20,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
)
}
/// If there are two sections available to fill - the smaller one should be filled first
/// (if possible).
///
/// We test this by creating two incoming rectangles.
///
/// The largest one is placed and creates two new sections - after which the second, smaller one
/// should get placed into the smaller of the two new sections.
///
/// ```text
/// ┌──────────────┬──▲───────────────┐
/// │ Second Rect │ │ │
/// ├──────────────┴──┤ │
/// │ │ │
/// │ First Placed │ │
/// │ Rectangle │ │
/// │ │ │
/// └─────────────────┴───────────────┘
/// ```
#[test]
fn fills_small_sections_before_large_ones() {
let mut targets = BTreeMap::new();
targets.insert(BinId::Three, TargetBin::new(100, 100, 1));
let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(50, 90, 1));
groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(1, 1, 1));
let packed = pack_rects(
&groups,
&mut targets,
&volume_heuristic,
&contains_smallest_box,
)
.unwrap();
let locations = packed.packed_locations;
assert_eq!(locations.len(), 2);
assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,);
assert_eq!(locations[&RectToPlaceId::Two].0, BinId::Three,);
assert_eq!(
locations[&RectToPlaceId::One].1,
PackedLocation {
x: 0,
y: 0,
z: 0,
whd: WidthHeightDepth {
width: 50,
height: 90,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
);
assert_eq!(
locations[&RectToPlaceId::Two].1,
PackedLocation {
x: 0,
y: 90,
z: 0,
whd: WidthHeightDepth {
width: 1,
height: 1,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
);
}
/// Say we have one bin and three rectangles to place within in.
///
/// The first one gets placed and creates two new splits.
///
/// We then attempt to place the second one into the smallest split. It's too big to fit, so
/// we place it into the largest split.
///
/// After that we place the third rectangle into the smallest split.
///
/// Here we verify that that actually occurs and that we didn't throw away that smallest split
/// when the second one couldn't fit in it.
///
/// ```text
/// ┌──────────────┬──────────────┐
/// │ Third │ │
/// ├──────────────┤ │
/// │ │ │
/// │ │ │
/// │ ├──────────────┤
/// │ First │ │
/// │ │ Second │
/// │ │ │
/// └──────────────┴──────────────┘
/// ```
#[test]
fn saves_bin_sections_for_future_use() {
let mut targets = BTreeMap::new();
targets.insert(BinId::Three, TargetBin::new(100, 100, 1));
let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(60, 95, 1));
groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(40, 10, 1));
groups.push_rect(RectToPlaceId::Three, None, RectToInsert::new(60, 3, 1));
let packed = pack_rects(
&groups,
&mut targets,
&volume_heuristic,
&contains_smallest_box,
)
.unwrap();
let locations = packed.packed_locations;
assert_eq!(
locations[&RectToPlaceId::One].1,
PackedLocation {
x: 0,
y: 0,
z: 0,
whd: WidthHeightDepth {
width: 60,
height: 95,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
);
assert_eq!(
locations[&RectToPlaceId::Two].1,
PackedLocation {
x: 60,
y: 0,
z: 0,
whd: WidthHeightDepth {
width: 40,
height: 10,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
);
assert_eq!(
locations[&RectToPlaceId::Three].1,
PackedLocation {
x: 0,
y: 95,
z: 0,
whd: WidthHeightDepth {
width: 60,
height: 3,
depth: 1
},
x_axis_rotation: RotatedBy::ZeroDegrees,
y_axis_rotation: RotatedBy::ZeroDegrees,
z_axis_rotation: RotatedBy::ZeroDegrees,
}
);
}
/// Create a handful of rectangles that need to be placed, with two of them in the same group
/// and the rest ungrouped.
/// Try placing them many times and verify that each time they are placed the exact same way.
#[test]
fn deterministic_packing() {
let mut previous_packed = None;
for _ in 0..5 {
let mut rects_to_place: GroupedRectsToPlace<&'static str, &str> =
GroupedRectsToPlace::new();
let mut target_bins = BTreeMap::new();
for bin_id in 0..5 {
target_bins.insert(bin_id, TargetBin::new(8, 8, 1));
}
let rectangles = vec![
"some-rectangle-0",
"some-rectangle-1",
"some-rectangle-2",
"some-rectangle-3",
"some-rectangle-4",
];
for rect_id in rectangles.iter() {
rects_to_place.push_rect(rect_id, None, RectToInsert::new(4, 4, 1));
}
let packed = pack_rects(
&rects_to_place,
&mut target_bins.clone(),
&volume_heuristic,
&contains_smallest_box,
)
.unwrap();
if let Some(previous_packed) = previous_packed.as_ref() {
assert_eq!(&packed, previous_packed);
}
previous_packed = Some(packed);
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
enum RectToPlaceId {
One,
Two,
Three,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
enum BinId {
Three,
Four,
}
}

View File

@@ -0,0 +1,47 @@
use crate::width_height_depth::WidthHeightDepth;
/// Describes how and where an incoming rectangle was packed into the target bins
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct PackedLocation {
pub(crate) x: u32,
pub(crate) y: u32,
pub(crate) z: u32,
pub(crate) whd: WidthHeightDepth,
pub(crate) x_axis_rotation: RotatedBy,
pub(crate) y_axis_rotation: RotatedBy,
pub(crate) z_axis_rotation: RotatedBy,
}
#[derive(Debug, PartialEq, Copy, Clone)]
#[allow(unused)] // TODO: Implement rotations
pub enum RotatedBy {
ZeroDegrees,
NinetyDegrees,
}
#[allow(missing_docs)]
impl PackedLocation {
pub fn x(&self) -> u32 {
self.x
}
pub fn y(&self) -> u32 {
self.y
}
pub fn z(&self) -> u32 {
self.z
}
pub fn width(&self) -> u32 {
self.whd.width
}
pub fn height(&self) -> u32 {
self.whd.height
}
pub fn depth(&self) -> u32 {
self.whd.depth
}
}

View File

@@ -0,0 +1,52 @@
use crate::width_height_depth::WidthHeightDepth;
/// A rectangle that we want to insert into a target bin
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct RectToInsert {
pub(crate) whd: WidthHeightDepth,
allow_global_x_axis_rotation: bool,
allow_global_y_axis_rotation: bool,
allow_global_z_axis_rotation: bool,
}
impl Into<WidthHeightDepth> for RectToInsert {
fn into(self) -> WidthHeightDepth {
WidthHeightDepth {
width: self.width(),
height: self.height(),
depth: self.depth(),
}
}
}
#[allow(missing_docs)]
impl RectToInsert {
pub fn new(width: u32, height: u32, depth: u32) -> Self {
RectToInsert {
whd: WidthHeightDepth {
width,
height,
depth,
},
// Rotation is not yet supported
allow_global_x_axis_rotation: false,
allow_global_y_axis_rotation: false,
allow_global_z_axis_rotation: false,
}
}
}
#[allow(missing_docs)]
impl RectToInsert {
pub fn width(&self) -> u32 {
self.whd.width
}
pub fn height(&self) -> u32 {
self.whd.height
}
pub fn depth(&self) -> u32 {
self.whd.depth
}
}

60
vendor/rectangle-pack/src/target_bin.rs vendored Normal file
View File

@@ -0,0 +1,60 @@
use crate::bin_section::BinSection;
use crate::width_height_depth::WidthHeightDepth;
use alloc::vec::Vec;
mod coalesce;
mod push_available_bin_section;
/// A bin that we'd like to play our incoming rectangles into
#[derive(Debug, Clone)]
pub struct TargetBin {
pub(crate) max_width: u32,
pub(crate) max_height: u32,
pub(crate) max_depth: u32,
pub(crate) available_bin_sections: Vec<BinSection>,
}
impl TargetBin {
#[allow(missing_docs)]
pub fn new(max_width: u32, max_height: u32, max_depth: u32) -> Self {
let available_bin_sections = vec![BinSection::new(
0,
0,
0,
WidthHeightDepth {
width: max_width,
height: max_height,
depth: max_depth,
},
)];
TargetBin {
max_width,
max_height,
max_depth,
available_bin_sections,
}
}
/// The free [`BinSection`]s within the [`TargetBin`] that rectangles can still be placed into.
pub fn available_bin_sections(&self) -> &Vec<BinSection> {
&self.available_bin_sections
}
/// Remove the section that was just split by a placed rectangle.
pub fn remove_filled_section(&mut self, idx: usize) {
self.available_bin_sections.remove(idx);
}
/// When a section is filled it gets split into three new sections.
/// Here we add those.
///
/// TODO: Ignore sections with a volume of 0
pub fn add_new_sections(&mut self, new_sections: [BinSection; 3]) {
for new_section in new_sections.iter() {
if new_section.whd.volume() > 0 {
self.available_bin_sections.push(*new_section);
}
}
}
}

View File

@@ -0,0 +1,88 @@
use crate::TargetBin;
use core::ops::Range;
impl TargetBin {
/// Over time as you use [`TargetBin.push_available_bin_section`] to return remove packed
/// rectangles from the [`TargetBin`], you may end up with neighboring bin sections that can
/// be combined into a larger bin section.
///
/// Combining bin sections in this was is desirable because a larger bin section allows you to
/// place larger rectangles that might not fit into the smaller bin sections.
///
/// In order to coalesce, or combine a bin section with other bin sections, we need to check
/// every other available bin section to see if they are neighbors.
///
/// This means that fully coalescing the entire list of available bin sections is O(n^2) time
/// complexity, where n is the number of available empty sections.
///
/// # Basic Usage
///
/// ```ignore
/// # use rectangle_pack::TargetBin;
/// let target_bin = my_target_bin();
///
/// for idx in 0..target_bin.available_bin_sections().len() {
/// let len = target_bin.available_bin_sections().len();
/// target_bin.coalesce_available_sections(idx, 0..len);
/// }
///
/// # fn my_target_bin () -> TargetBin {
/// # TargetBin::new(1, 2, 3)
/// # }
/// ```
///
/// # Distributing the Workload
///
/// It is possible that you are developing an application that can in some cases have a lot of
/// heavily fragmented bins that need to be coalesced. If your application has a tight
/// performance budget, such as a real time simulation, you may not want to do all of your
/// coalescing at once.
///
/// This method allows you to split the work over many frames by giving you fine grained control
/// over which bin sections is getting coalesced and which other bin sections it gets tested
/// against.
///
/// So, for example, say you have an application where you want to fully coalesce the entire
/// bin every ten seconds, and you are running at 60 frames per second. You would then
/// distribute the coalescing work such that it would take 600 calls to compare every bin
/// section.
///
/// Here's a basic eample of splitting the work.
///
/// ```ignore
/// # use rectangle_pack::TargetBin;
/// let target_bin = my_target_bin();
///
/// let current_frame: usize = get_current_frame() % 600;
///
/// for idx in 0..target_bin.available_bin_sections().len() {
/// let len = target_bin.available_bin_sections().len();
///
/// let start = len / 600 * current_frame;
/// let end = start + len / 600;
///
/// target_bin.coalesce_available_sections(idx, start..end);
/// }
///
/// # fn my_target_bin () -> TargetBin {
/// # TargetBin::new(1, 2, 3)
/// # }
/// #
/// # fn get_current_frame () -> usize {
/// # 0
/// # }
/// ```
///
/// [`TargetBin.push_available_bin_section`]: #method.push_available_bin_section
// TODO: Write tests, implement then remove the "ignore" from the examples above.
// Tests cases should have a rectangle and then a neighbor (above, below, left, right) and
// verify that they get combined, but only if the comparison indices are correct and only if
// the neighbor has the same width (uf above/below) or height (if left/right).
pub fn coalesce_available_sections(
_bin_section_index: usize,
_compare_to_indices: Range<usize>,
) {
unimplemented!()
}
}

View File

@@ -0,0 +1,166 @@
//! Methods for adding a BinSection back into a TargetBin.
//!
//! Useful in an application that needs to be able to remove packed rectangles from bins.
//! After which the [`TargetBin.coalesce`] method can be used to combine smaller adjacent sections
//! into larger sections.
#![allow(missing_docs)]
use crate::bin_section::BinSection;
use crate::TargetBin;
use core::fmt::{Display, Formatter, Result as FmtResult};
impl TargetBin {
/// Push a [`BinSection`] to the list of remaining [`BinSection`]'s that rectangles can be
/// placed in.
///
/// ## Performance
///
/// This checks that your [`BinSection`] does not overlap any other bin sections. In many
/// cases this will be negligible, however it is important to note that this has a worst case
/// time complexity of `O(Width * Height * Depth)`, where the worst case is tht you have a bin
/// full of `1x1x1` rectangles.
///
/// To skip the validity checks use [`TargetBin.push_available_bin_section_unchecked`].
///
/// [`TargetBin.push_available_bin_section_unchecked`]: #method.push_available_bin_section_unchecked
pub fn push_available_bin_section(
&mut self,
bin_section: BinSection,
) -> Result<(), PushBinSectionError> {
if bin_section.x >= self.max_width
|| bin_section.y >= self.max_height
|| bin_section.z >= self.max_depth
{
return Err(PushBinSectionError::OutOfBounds(bin_section));
}
for available in self.available_bin_sections.iter() {
if available.overlaps(&bin_section) {
return Err(PushBinSectionError::Overlaps {
remaining_section: *available,
new_section: bin_section,
});
}
}
self.push_available_bin_section_unchecked(bin_section);
Ok(())
}
/// Push a [`BinSection`] to the list of remaining [`BinSection`]'s that rectangles can be
/// placed in, without checking whether or not it is valid.
///
/// Use [`TargetBin.push_available_bin_section`] if you want to check that the new bin section
/// does not overlap any existing bin sections nad that it is within the [`TargetBin`]'s bounds.
///
/// [`TargetBin.push_available_bin_section`]: #method.push_available_bin_section
pub fn push_available_bin_section_unchecked(&mut self, bin_section: BinSection) {
self.available_bin_sections.push(bin_section);
}
}
/// An error while attempting to push a [`BinSection`] into the remaining bin sections of a
/// [`TargetBin`].
#[derive(Debug)]
pub enum PushBinSectionError {
/// Attempted to push a [`BinSection`] that is not fully contained by the bin.
OutOfBounds(BinSection),
/// Attempted to push a [`BinSection`] that overlaps another empty bin section.
Overlaps {
/// The section that is already stored as empty within the [`TargetBin`];
remaining_section: BinSection,
/// The section that you were trying to add to the [`TargetBin`];
new_section: BinSection,
},
}
impl Display for PushBinSectionError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
PushBinSectionError::OutOfBounds(oob) => {
f.debug_tuple("BinSection").field(oob).finish()
}
PushBinSectionError::Overlaps {
remaining_section,
new_section,
} => f
.debug_struct("Overlaps")
.field("remaining_section", remaining_section)
.field("new_section", new_section)
.finish(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::width_height_depth::WidthHeightDepth;
/// Verify that if the bin section that we are pushing is outside of the TargetBin's bounds we
/// return an error.
#[test]
fn error_if_bin_section_out_of_bounds() {
let mut bin = empty_bin();
let out_of_bounds = BinSection::new(101, 0, 0, WidthHeightDepth::new(1, 1, 1));
match bin.push_available_bin_section(out_of_bounds).err().unwrap() {
PushBinSectionError::OutOfBounds(err_bin_section) => {
assert_eq!(err_bin_section, out_of_bounds)
}
_ => panic!(),
};
}
/// Verify that if the bin section that we are pushing overlaps another bin section we return
/// an error.
#[test]
fn error_if_bin_section_overlaps_another_remaining_section() {
let mut bin = empty_bin();
let overlaps = BinSection::new(0, 0, 0, WidthHeightDepth::new(1, 1, 1));
match bin.push_available_bin_section(overlaps).err().unwrap() {
PushBinSectionError::Overlaps {
remaining_section: err_remaining_section,
new_section: err_new_section,
} => {
assert_eq!(err_new_section, overlaps);
assert_eq!(
err_remaining_section,
BinSection::new(0, 0, 0, WidthHeightDepth::new(100, 100, 1))
);
}
_ => panic!(),
}
}
/// Verify that we can push a valid bin section.
#[test]
fn push_bin_section() {
let mut bin = full_bin();
let valid_section = BinSection::new(1, 2, 0, WidthHeightDepth::new(1, 1, 1));
assert_eq!(bin.available_bin_sections.len(), 0);
bin.push_available_bin_section(valid_section).unwrap();
assert_eq!(bin.available_bin_sections.len(), 1);
assert_eq!(bin.available_bin_sections[0], valid_section);
}
fn empty_bin() -> TargetBin {
TargetBin::new(100, 100, 1)
}
fn full_bin() -> TargetBin {
let mut bin = TargetBin::new(100, 100, 1);
bin.available_bin_sections.clear();
bin
}
}

View File

@@ -0,0 +1,30 @@
/// Used to represent a volume (or area of the depth is 1)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Ord, PartialOrd)]
#[allow(missing_docs)]
pub struct WidthHeightDepth {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) depth: u32,
}
#[allow(missing_docs)]
impl WidthHeightDepth {
/// # Panics
///
/// Panics if width, height or depth is 0.
pub fn new(width: u32, height: u32, depth: u32) -> Self {
assert_ne!(width, 0);
assert_ne!(height, 0);
assert_ne!(depth, 0);
WidthHeightDepth {
width,
height,
depth,
}
}
pub fn volume(&self) -> u128 {
self.width as u128 * self.height as u128 * self.depth as u128
}
}