diff --git a/src/bin_section.rs b/src/bin_section.rs index b6ad702..8eeadd7 100644 --- a/src/bin_section.rs +++ b/src/bin_section.rs @@ -1,4 +1,4 @@ -use crate::{HeuristicFn, LayeredRect, PackedLocation, RotatedBy, WidthHeightDepth}; +use crate::{BoxSizeHeuristicFn, PackedLocation, RectToInsert, RotatedBy, WidthHeightDepth}; use std::cmp::Ordering; use std::fmt::{Display, Error, Formatter}; @@ -7,7 +7,7 @@ use std::fmt::{Display, Error, Formatter}; /// Ordering::Greater means the first set of containers is better. /// Ordering::Less means the second set of containers is better. pub type MoreSuitableContainersFn = - dyn Fn([WidthHeightDepth; 3], [WidthHeightDepth; 3], &HeuristicFn) -> Ordering; + dyn Fn([WidthHeightDepth; 3], [WidthHeightDepth; 3], &BoxSizeHeuristicFn) -> Ordering; /// Select the container that has the smallest box. /// @@ -15,7 +15,7 @@ pub type MoreSuitableContainersFn = pub fn contains_smallest_box( mut container1: [WidthHeightDepth; 3], mut container2: [WidthHeightDepth; 3], - heuristic: &HeuristicFn, + heuristic: &BoxSizeHeuristicFn, ) -> Ordering { container1.sort_unstable(); container2.sort_unstable(); @@ -144,9 +144,9 @@ impl BinSection { /// calls and conditionals. pub fn try_place( &self, - incoming: &LayeredRect, + incoming: &RectToInsert, container_comparison_fn: &MoreSuitableContainersFn, - heuristic_fn: &HeuristicFn, + heuristic_fn: &BoxSizeHeuristicFn, ) -> Result<(PackedLocation, [BinSection; 3]), BinSectionError> { self.incoming_can_fit(incoming)?; @@ -184,7 +184,7 @@ impl BinSection { Ok((packed_location, all_combinations[5])) } - fn incoming_can_fit(&self, incoming: &LayeredRect) -> Result<(), BinSectionError> { + fn incoming_can_fit(&self, incoming: &RectToInsert) -> Result<(), BinSectionError> { if incoming.width() > self.whd.width { return Err(BinSectionError::PlacementWiderThanBinSection); } @@ -201,7 +201,7 @@ impl BinSection { fn width_largest_height_second_largest_depth_smallest( &self, - incoming: &LayeredRect, + incoming: &RectToInsert, ) -> [BinSection; 3] { [ self.empty_space_directly_right(incoming), @@ -212,7 +212,7 @@ impl BinSection { fn width_largest_depth_second_largest_height_smallest( &self, - incoming: &LayeredRect, + incoming: &RectToInsert, ) -> [BinSection; 3] { [ self.empty_space_directly_right(incoming), @@ -223,7 +223,7 @@ impl BinSection { fn height_largest_width_second_largest_depth_smallest( &self, - incoming: &LayeredRect, + incoming: &RectToInsert, ) -> [BinSection; 3] { [ self.all_empty_space_right_excluding_behind(incoming), @@ -234,7 +234,7 @@ impl BinSection { fn height_largest_depth_second_largest_width_smallest( &self, - incoming: &LayeredRect, + incoming: &RectToInsert, ) -> [BinSection; 3] { [ self.all_empty_space_right(incoming), @@ -245,7 +245,7 @@ impl BinSection { fn depth_largest_width_second_largest_height_smallest( &self, - incoming: &LayeredRect, + incoming: &RectToInsert, ) -> [BinSection; 3] { [ self.all_empty_space_right_excluding_above(incoming), @@ -256,7 +256,7 @@ impl BinSection { fn depth_largest_height_second_largest_width_smallest( &self, - incoming: &LayeredRect, + incoming: &RectToInsert, ) -> [BinSection; 3] { [ self.all_empty_space_right(incoming), @@ -265,7 +265,7 @@ impl BinSection { ] } - fn all_empty_space_above(&self, incoming: &LayeredRect) -> BinSection { + fn all_empty_space_above(&self, incoming: &RectToInsert) -> BinSection { BinSection::new_spread( self.x, self.y + incoming.height(), @@ -276,7 +276,7 @@ impl BinSection { ) } - fn all_empty_space_right(&self, incoming: &LayeredRect) -> BinSection { + fn all_empty_space_right(&self, incoming: &RectToInsert) -> BinSection { BinSection::new_spread( self.x + incoming.width(), self.y, @@ -287,7 +287,7 @@ impl BinSection { ) } - fn all_empty_space_behind(&self, incoming: &LayeredRect) -> BinSection { + fn all_empty_space_behind(&self, incoming: &RectToInsert) -> BinSection { BinSection::new_spread( self.x, self.y, @@ -298,7 +298,7 @@ impl BinSection { ) } - fn empty_space_directly_above(&self, incoming: &LayeredRect) -> BinSection { + fn empty_space_directly_above(&self, incoming: &RectToInsert) -> BinSection { BinSection::new_spread( self.x, self.y + incoming.height(), @@ -309,7 +309,7 @@ impl BinSection { ) } - fn empty_space_directly_right(&self, incoming: &LayeredRect) -> BinSection { + fn empty_space_directly_right(&self, incoming: &RectToInsert) -> BinSection { BinSection::new_spread( self.x + incoming.width(), self.y, @@ -320,7 +320,7 @@ impl BinSection { ) } - fn empty_space_directly_behind(&self, incoming: &LayeredRect) -> BinSection { + fn empty_space_directly_behind(&self, incoming: &RectToInsert) -> BinSection { BinSection::new( self.x, self.y, @@ -333,7 +333,7 @@ impl BinSection { ) } - fn all_empty_space_above_excluding_right(&self, incoming: &LayeredRect) -> BinSection { + fn all_empty_space_above_excluding_right(&self, incoming: &RectToInsert) -> BinSection { BinSection::new( self.x, self.y + incoming.height(), @@ -346,7 +346,7 @@ impl BinSection { ) } - fn all_empty_space_above_excluding_behind(&self, incoming: &LayeredRect) -> BinSection { + fn all_empty_space_above_excluding_behind(&self, incoming: &RectToInsert) -> BinSection { BinSection::new( self.x, self.y + incoming.height(), @@ -359,7 +359,7 @@ impl BinSection { ) } - fn all_empty_space_right_excluding_above(&self, incoming: &LayeredRect) -> BinSection { + fn all_empty_space_right_excluding_above(&self, incoming: &RectToInsert) -> BinSection { BinSection::new( self.x + incoming.width(), self.y, @@ -372,7 +372,7 @@ impl BinSection { ) } - fn all_empty_space_right_excluding_behind(&self, incoming: &LayeredRect) -> BinSection { + fn all_empty_space_right_excluding_behind(&self, incoming: &RectToInsert) -> BinSection { BinSection::new( self.x + incoming.width(), self.y, @@ -385,7 +385,7 @@ impl BinSection { ) } - fn all_empty_space_behind_excluding_above(&self, incoming: &LayeredRect) -> BinSection { + fn all_empty_space_behind_excluding_above(&self, incoming: &RectToInsert) -> BinSection { BinSection::new( self.x, self.y, @@ -398,7 +398,7 @@ impl BinSection { ) } - fn all_empty_space_behind_excluding_right(&self, incoming: &LayeredRect) -> BinSection { + fn all_empty_space_behind_excluding_right(&self, incoming: &RectToInsert) -> BinSection { BinSection::new( self.x, self.y, @@ -415,7 +415,7 @@ impl BinSection { #[cfg(test)] mod tests { use super::*; - use crate::{volume_heuristic, LayeredRect}; + use crate::{volume_heuristic, RectToInsert}; const BIGGEST: u32 = 50; const MIDDLE: u32 = 25; @@ -427,7 +427,7 @@ mod tests { #[test] fn error_if_placement_is_wider_than_bin_section() { let bin_section = bin_section_width_height_depth(5, 20, 1); - let placement = LayeredRect::new(6, 20, 1); + let placement = RectToInsert::new(6, 20, 1); assert_eq!( bin_section @@ -441,7 +441,7 @@ mod tests { #[test] fn error_if_placement_is_taller_than_bin_section() { let bin_section = bin_section_width_height_depth(5, 20, 1); - let placement = LayeredRect::new(5, 21, 1); + let placement = RectToInsert::new(5, 21, 1); assert_eq!( bin_section @@ -455,7 +455,7 @@ mod tests { #[test] fn error_if_placement_is_deeper_than_bin_section() { let bin_section = bin_section_width_height_depth(5, 20, 1); - let placement = LayeredRect::new(5, 20, 2); + let placement = RectToInsert::new(5, 20, 2); assert_eq!( bin_section @@ -475,7 +475,7 @@ mod tests { let whd = rect_to_place; - let placement = LayeredRect::new(whd.width, whd.height, whd.depth); + let placement = RectToInsert::new(whd.width, whd.height, whd.depth); let mut packed = bin_section .try_place(&placement, &contains_smallest_box, &volume_heuristic) diff --git a/src/box_size_heuristics.rs b/src/box_size_heuristics.rs new file mode 100644 index 0000000..592601d --- /dev/null +++ b/src/box_size_heuristics.rs @@ -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 * whd.height * whd.depth) as _ +} diff --git a/src/layered_rect_groups.rs b/src/grouped_rects_to_place.rs similarity index 83% rename from src/layered_rect_groups.rs rename to src/grouped_rects_to_place.rs index b752129..2f175ef 100644 --- a/src/layered_rect_groups.rs +++ b/src/grouped_rects_to_place.rs @@ -1,4 +1,4 @@ -use crate::LayeredRect; +use crate::RectToInsert; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt::Debug; @@ -12,14 +12,14 @@ use std::hash::Hash; /// 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 LayeredRectGroups +pub struct GroupedRectsToPlace where InboundId: Debug + Hash + Eq, GroupId: Debug + Hash + Eq, { pub(crate) inbound_id_to_group_ids: HashMap>>, pub(crate) group_id_to_inbound_ids: HashMap, Vec>, - pub(crate) rects: HashMap, + pub(crate) rects: HashMap, } /// A group of rectangles that need to be placed together @@ -41,7 +41,7 @@ where Grouped(GroupId), } -impl LayeredRectGroups +impl GroupedRectsToPlace where InboundId: Debug + Hash + Clone + Eq, GroupId: Debug + Hash + Clone + Eq, @@ -65,7 +65,7 @@ where &mut self, inbound_id: InboundId, group_ids: Option>, - inbound: LayeredRect, + inbound: RectToInsert, ) { self.rects.insert(inbound_id.clone(), inbound); @@ -107,15 +107,15 @@ where #[cfg(test)] mod tests { use super::*; - use crate::LayeredRect; + 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 inboundID. #[test] fn ungrouped_rectangles_use_their_inbound_id_as_their_group_id() { - let mut lrg: LayeredRectGroups<_, ()> = LayeredRectGroups::new(); + let mut lrg: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); - lrg.push_rect(InboundId::One, None, LayeredRect::new(10, 10, 1)); + lrg.push_rect(InboundId::One, None, RectToInsert::new(10, 10, 1)); assert_eq!( lrg.group_id_to_inbound_ids[&Group::Ungrouped(InboundId::One)], @@ -127,10 +127,10 @@ mod tests { /// map of group id -> inbound rect id #[test] fn group_id_to_inbound_ids() { - let mut lrg = LayeredRectGroups::new(); + let mut lrg = GroupedRectsToPlace::new(); - lrg.push_rect(InboundId::One, Some(vec![0]), LayeredRect::new(10, 10, 1)); - lrg.push_rect(InboundId::Two, Some(vec![0]), LayeredRect::new(10, 10, 1)); + lrg.push_rect(InboundId::One, Some(vec![0]), RectToInsert::new(10, 10, 1)); + lrg.push_rect(InboundId::Two, Some(vec![0]), RectToInsert::new(10, 10, 1)); assert_eq!( lrg.group_id_to_inbound_ids[&Group::Grouped(0)], @@ -141,15 +141,15 @@ mod tests { /// Verify that we store the map of inbound id -> group ids #[test] fn inbound_id_to_group_ids() { - let mut lrg = LayeredRectGroups::new(); + let mut lrg = GroupedRectsToPlace::new(); lrg.push_rect( InboundId::One, Some(vec![0, 1]), - LayeredRect::new(10, 10, 1), + RectToInsert::new(10, 10, 1), ); - lrg.push_rect(InboundId::Two, None, LayeredRect::new(10, 10, 1)); + lrg.push_rect(InboundId::Two, None, RectToInsert::new(10, 10, 1)); assert_eq!( lrg.inbound_id_to_group_ids[&InboundId::One], @@ -165,15 +165,15 @@ mod tests { /// Verify that we store in rectangle associated with its inbound ID #[test] fn store_the_inbound_rectangle() { - let mut lrg = LayeredRectGroups::new(); + let mut lrg = GroupedRectsToPlace::new(); lrg.push_rect( InboundId::One, Some(vec![0, 1]), - LayeredRect::new(10, 10, 1), + RectToInsert::new(10, 10, 1), ); - assert_eq!(lrg.rects[&InboundId::One], LayeredRect::new(10, 10, 1)); + assert_eq!(lrg.rects[&InboundId::One], RectToInsert::new(10, 10, 1)); } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] diff --git a/src/lib.rs b/src/lib.rs index 0591c2a..705c9bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,109 +3,102 @@ #![deny(missing_docs)] -use crate::bin_section::{BinSection, MoreSuitableContainersFn}; -use crate::layered_rect_groups::{Group, LayeredRectGroups}; - use std::collections::HashMap; use std::fmt::{Debug, Display, Error, Formatter}; use std::hash::Hash; pub use crate::bin_section::contains_smallest_box; +use crate::bin_section::{BinSection, MoreSuitableContainersFn}; +use crate::grouped_rects_to_place::{Group, 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; mod bin_section; -mod layered_rect_groups; +mod grouped_rects_to_place; + +mod rect_to_insert; +mod target_bin; +mod width_height_depth; + +mod box_size_heuristics; + +/// Information about successfully packed rectangles. +#[derive(Debug, PartialEq)] +pub struct RectanglePackOk { + packed_locations: HashMap, + // TODO: Other information such as information about how the bins were packed + // (perhaps percentage filled) +} + +impl + RectanglePackOk +{ + /// Indicates where every incoming rectangle was placed + pub fn packed_locations(&self) -> &HashMap { + &self.packed_locations + } +} -pub fn volume_heuristic(whd: WidthHeightDepth) -> u128 { - (whd.width * whd.height * whd.depth) as _ +/// 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, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Ord, PartialOrd)] -#[allow(missing_docs)] -pub struct WidthHeightDepth { - pub width: u32, - pub height: u32, - pub depth: u32, +impl Display for RectanglePackError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + RectanglePackError::NotEnoughBinSpace => { + f.write_str("Not enough space to place all of the rectangles.") + } + } + } } -/// Incoming boxes are places into the smallest hole that will fit them. +/// Determine how to fit a set of incoming rectangles (2d or 3d) into a set of target bins. /// -/// "small" vs. "large" is based on the heuristic function. +/// ## Algorithm /// -/// A larger heuristic means that the box is larger. -pub type HeuristicFn = dyn Fn(WidthHeightDepth) -> u128; - -fn pack_rects< +/// The algorithm was originally inspired by [rectpack2D] and then modified to work in 3D. +/// +/// [rectpack2D]: https://github.com/TeamHypersomnia/rectpack2D +pub fn pack_rects< InboundId: Debug + Hash + PartialEq + Eq + Clone, BinId: Debug + Hash + PartialEq + Eq + Clone, GroupId: Debug + Hash + PartialEq + Eq + Clone, >( - incoming_groups: &LayeredRectGroups, + rects_to_place: &GroupedRectsToPlace, target_bins: HashMap, - box_size_heuristic: &HeuristicFn, + box_size_heuristic: &BoxSizeHeuristicFn, more_suitable_containers_fn: &MoreSuitableContainersFn, ) -> Result, RectanglePackError> { let mut packed_locations = HashMap::new(); - let bin_stats = HashMap::new(); let mut target_bins: Vec<(BinId, TargetBin)> = target_bins.into_iter().collect(); - target_bins.sort_unstable_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, - })) - }); + sort_bins_smallest_to_largest(&mut target_bins, box_size_heuristic); let mut group_id_to_inbound_ids: Vec<(&Group, &Vec)> = - incoming_groups.group_id_to_inbound_ids.iter().collect(); - group_id_to_inbound_ids.sort_unstable_by(|a, b| { - let a_heuristic = - a.1.iter() - .map(|inbound| { - // - let rect = incoming_groups.rects[inbound]; - box_size_heuristic(WidthHeightDepth { - width: rect.width, - height: rect.height, - depth: rect.depth, - }) - }) - .sum(); + 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, + ); - let b_heuristic: u128 = - b.1.iter() - .map(|inbound| { - // - let rect = incoming_groups.rects[inbound]; - box_size_heuristic(WidthHeightDepth { - width: rect.width, - height: rect.height, - depth: rect.depth, - }) - }) - .sum(); - - b_heuristic.cmp(&a_heuristic) - }); - - // FIXME: Split into individual functions for readability ... Too nested 'group: for (_group_id, incomings) in group_id_to_inbound_ids { 'incoming: for incoming_id in incomings.iter() { - 'bin: for (bin_id, bin) in target_bins.iter_mut() { + for (bin_id, bin) in target_bins.iter_mut() { let mut bin_clone = bin.clone(); - bin_clone.remaining_sections.reverse(); - - 'section: for remaining_section in bin_clone.remaining_sections.iter() { - let incoming = incoming_groups.rects[&incoming_id]; + 'section: while let Some(remaining_section) = bin_clone.remaining_sections.pop() { + let rect_to_place = rects_to_place.rects[&incoming_id]; let placement = remaining_section.try_place( - &incoming, + &rect_to_place, more_suitable_containers_fn, box_size_heuristic, ); @@ -114,20 +107,14 @@ fn pack_rects< continue 'section; } - bin.remaining_sections.pop(); - - // TODO: Ignore sections with a volume of 0 let (placement, mut new_sections) = placement.unwrap(); + sort_by_size_largest_to_smallest(&mut new_sections, box_size_heuristic); - new_sections.sort_unstable_by(|a, b| { - box_size_heuristic(b.whd).cmp(&box_size_heuristic(a.whd)) - }); - - for new_section in new_sections.iter() { - bin.remaining_sections.push(*new_section); - } + bin.remove_filled_section(); + bin.add_new_sections(new_sections); packed_locations.insert(incoming_id.clone(), (bin_id.clone(), placement)); + continue 'incoming; } } @@ -136,39 +123,66 @@ fn pack_rects< } } - // for (inbound_id, inbound) in incoming.iter() { - // for (bin_id, bin) in target_bins.iter_mut() { - // for bin_section in bin.remaining_sections.iter_mut() { - // // TODO: Check if inbound can fit into this bin split - if it can then remove the - // // split, place it into the split and create two new splits and push those to - // // the end of the remaining splits (smallest last) - // - // // If we can't then move on to the next split - // } - // } - // - // // If we make it here then no bin was able to fit our inbound rect - return an error - // } - - Ok(RectanglePackOk { - packed_locations, - bin_stats, - }) + Ok(RectanglePackOk { packed_locations }) } -#[derive(Debug, PartialEq)] -struct RectanglePackOk { - packed_locations: HashMap, - bin_stats: HashMap, +fn sort_bins_smallest_to_largest( + bins: &mut Vec<(BinId, TargetBin)>, + box_size_heuristic: &BoxSizeHeuristicFn, +) where + BinId: Debug + Hash + PartialEq + Eq + Clone, +{ + bins.sort_unstable_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, + })) + }); } -#[derive(Debug, PartialEq)] -struct BinStats { - width: u32, - height: u32, - percent_occupied: f32, +fn sort_by_size_largest_to_smallest( + items: &mut [BinSection; 3], + box_size_heuristic: &BoxSizeHeuristicFn, +) { + items.sort_unstable_by(|a, b| box_size_heuristic(b.whd).cmp(&box_size_heuristic(a.whd))); } +fn sort_groups_largest_to_smallest( + group_id_to_inbound_ids: &mut Vec<(&Group, &Vec)>, + incoming_groups: &GroupedRectsToPlace, + box_size_heuristic: &BoxSizeHeuristicFn, +) where + InboundId: Debug + Hash + PartialEq + Eq + Clone, + GroupId: Debug + Hash + PartialEq + Eq + Clone, +{ + group_id_to_inbound_ids.sort_unstable_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) + }); +} + +/// Describes how and where an incoming rectangle was packed into the target bins #[derive(Debug, PartialEq)] pub struct PackedLocation { x: u32, @@ -186,116 +200,22 @@ enum RotatedBy { NinetyDegrees, } -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct LayeredRect { - width: u32, - height: u32, - depth: u32, - allow_global_x_axis_rotation: bool, - allow_global_y_axis_rotation: bool, - allow_global_z_axis_rotation: bool, -} - -impl Into for LayeredRect { - fn into(self) -> WidthHeightDepth { - WidthHeightDepth { - width: self.width(), - height: self.height(), - depth: self.depth(), - } - } -} - -impl LayeredRect { - pub fn new(width: u32, height: u32, depth: u32) -> Self { - LayeredRect { - 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, - } - } -} - -impl LayeredRect { - fn width(&self) -> u32 { - self.width - } - - fn height(&self) -> u32 { - self.height - } - - fn depth(&self) -> u32 { - self.depth - } -} - -/// 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, -} - -impl Display for RectanglePackError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - match self { - RectanglePackError::NotEnoughBinSpace => { - f.write_str("Not enough space to place all of the rectangles.") - } - } - } -} - -#[derive(Debug, Clone)] -struct TargetBin { - max_width: u32, - max_height: u32, - max_depth: u32, - remaining_sections: Vec, -} - -impl TargetBin { - pub fn new(max_width: u32, max_height: u32, max_depth: u32) -> Self { - let remaining_sections = vec![BinSection::new( - 0, - 0, - 0, - WidthHeightDepth { - width: max_width, - height: max_height, - depth: max_depth, - }, - )]; - - TargetBin { - max_width, - max_height, - max_depth, - remaining_sections, - } - } -} - #[cfg(test)] mod tests { - use super::*; - - use crate::{pack_rects, volume_heuristic, LayeredRect, RectanglePackError, TargetBin}; use std::collections::HashMap; + use crate::{pack_rects, volume_heuristic, RectToInsert, RectanglePackError, TargetBin}; + + use super::*; + /// 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 = HashMap::new(); targets.insert(BinId::Three, TargetBin::new(2, 100, 1)); - let mut groups: LayeredRectGroups<_, ()> = LayeredRectGroups::new(); - groups.push_rect(InboundId::One, None, LayeredRect::new(3, 1, 1)); + let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); + groups.push_rect(InboundId::One, None, RectToInsert::new(3, 1, 1)); match pack_rects(&groups, targets, &volume_heuristic, &contains_smallest_box).unwrap_err() { RectanglePackError::NotEnoughBinSpace => {} @@ -306,8 +226,8 @@ mod tests { /// bin. #[test] fn one_inbound_rect_one_bin() { - let mut groups: LayeredRectGroups<_, ()> = LayeredRectGroups::new(); - groups.push_rect(InboundId::One, None, LayeredRect::new(1, 2, 1)); + let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); + groups.push_rect(InboundId::One, None, RectToInsert::new(1, 2, 1)); let mut targets = HashMap::new(); targets.insert(BinId::Three, TargetBin::new(5, 5, 1)); @@ -340,8 +260,8 @@ mod tests { /// 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: LayeredRectGroups<_, ()> = LayeredRectGroups::new(); - groups.push_rect(InboundId::One, None, LayeredRect::new(2, 2, 1)); + let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); + groups.push_rect(InboundId::One, None, RectToInsert::new(2, 2, 1)); let mut targets = HashMap::new(); targets.insert(BinId::Three, TargetBin::new(5, 5, 1)); @@ -375,9 +295,9 @@ mod tests { /// If we have two inbound rects the smallest one should be placed first. #[test] fn places_largest_rectangles_first() { - let mut groups: LayeredRectGroups<_, ()> = LayeredRectGroups::new(); - groups.push_rect(InboundId::One, None, LayeredRect::new(10, 10, 1)); - groups.push_rect(InboundId::Two, None, LayeredRect::new(5, 5, 1)); + let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); + groups.push_rect(InboundId::One, None, RectToInsert::new(10, 10, 1)); + groups.push_rect(InboundId::Two, None, RectToInsert::new(5, 5, 1)); let mut targets = HashMap::new(); targets.insert(BinId::Three, TargetBin::new(20, 20, 2)); @@ -432,9 +352,9 @@ mod tests { /// 2. Second place largest rectangle into the next available bin (i.e. the largest one). #[test] fn two_rects_two_bins() { - let mut groups: LayeredRectGroups<_, ()> = LayeredRectGroups::new(); - groups.push_rect(InboundId::One, None, LayeredRect::new(15, 15, 1)); - groups.push_rect(InboundId::Two, None, LayeredRect::new(20, 20, 1)); + let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); + groups.push_rect(InboundId::One, None, RectToInsert::new(15, 15, 1)); + groups.push_rect(InboundId::Two, None, RectToInsert::new(20, 20, 1)); let mut targets = HashMap::new(); targets.insert(BinId::Three, TargetBin::new(20, 20, 1)); @@ -506,10 +426,10 @@ mod tests { let mut targets = HashMap::new(); targets.insert(BinId::Three, TargetBin::new(100, 100, 1)); - let mut groups: LayeredRectGroups<_, ()> = LayeredRectGroups::new(); + let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); - groups.push_rect(InboundId::One, None, LayeredRect::new(50, 90, 1)); - groups.push_rect(InboundId::Two, None, LayeredRect::new(1, 1, 1)); + groups.push_rect(InboundId::One, None, RectToInsert::new(50, 90, 1)); + groups.push_rect(InboundId::Two, None, RectToInsert::new(1, 1, 1)); let packed = pack_rects(&groups, targets, &volume_heuristic, &contains_smallest_box).unwrap(); @@ -554,10 +474,100 @@ mod tests { ); } + /// 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 = HashMap::new(); + targets.insert(BinId::Three, TargetBin::new(100, 100, 1)); + + let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); + + groups.push_rect(InboundId::One, None, RectToInsert::new(50, 95, 1)); + groups.push_rect(InboundId::Two, None, RectToInsert::new(50, 10, 1)); + groups.push_rect(InboundId::Three, None, RectToInsert::new(20, 3, 1)); + + let packed = + pack_rects(&groups, targets, &volume_heuristic, &contains_smallest_box).unwrap(); + let locations = packed.packed_locations; + + assert_eq!( + locations[&InboundId::One].1, + PackedLocation { + x: 0, + y: 0, + z: 0, + whd: WidthHeightDepth { + width: 50, + height: 95, + depth: 1 + }, + x_axis_rotation: RotatedBy::ZeroDegrees, + y_axis_rotation: RotatedBy::ZeroDegrees, + z_axis_rotation: RotatedBy::ZeroDegrees, + } + ); + assert_eq!( + locations[&InboundId::Two].1, + PackedLocation { + x: 50, + y: 0, + z: 0, + whd: WidthHeightDepth { + width: 50, + height: 10, + depth: 1 + }, + x_axis_rotation: RotatedBy::ZeroDegrees, + y_axis_rotation: RotatedBy::ZeroDegrees, + z_axis_rotation: RotatedBy::ZeroDegrees, + } + ); + assert_eq!( + locations[&InboundId::Three].1, + PackedLocation { + x: 0, + y: 95, + z: 0, + whd: WidthHeightDepth { + width: 20, + height: 3, + depth: 1 + }, + x_axis_rotation: RotatedBy::ZeroDegrees, + y_axis_rotation: RotatedBy::ZeroDegrees, + z_axis_rotation: RotatedBy::ZeroDegrees, + } + ); + } + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] enum InboundId { One, Two, + Three, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] diff --git a/src/rect_to_insert.rs b/src/rect_to_insert.rs new file mode 100644 index 0000000..4057b4f --- /dev/null +++ b/src/rect_to_insert.rs @@ -0,0 +1,51 @@ +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 for RectToInsert { + fn into(self) -> WidthHeightDepth { + WidthHeightDepth { + width: self.width(), + height: self.height(), + depth: self.depth(), + } + } +} + +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 + } +} diff --git a/src/target_bin.rs b/src/target_bin.rs new file mode 100644 index 0000000..4527893 --- /dev/null +++ b/src/target_bin.rs @@ -0,0 +1,49 @@ +use crate::bin_section::BinSection; +use crate::width_height_depth::WidthHeightDepth; + +/// 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) remaining_sections: Vec, +} + +impl TargetBin { + #[allow(missing_docs)] + pub fn new(max_width: u32, max_height: u32, max_depth: u32) -> Self { + let remaining_sections = vec![BinSection::new( + 0, + 0, + 0, + WidthHeightDepth { + width: max_width, + height: max_height, + depth: max_depth, + }, + )]; + + TargetBin { + max_width, + max_height, + max_depth, + remaining_sections, + } + } + + /// Remove the section that was just split by a placed rectangle. + pub fn remove_filled_section(&mut self) { + self.remaining_sections.pop(); + } + + /// 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() { + self.remaining_sections.push(*new_section); + } + } +} diff --git a/src/width_height_depth.rs b/src/width_height_depth.rs new file mode 100644 index 0000000..54cc0c4 --- /dev/null +++ b/src/width_height_depth.rs @@ -0,0 +1,19 @@ +/// 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 { + pub fn new(width: u32, height: u32, depth: u32) -> Self { + WidthHeightDepth { + width, + height, + depth, + } + } +}