Skip to content

Commit

Permalink
Deterministic packing
Browse files Browse the repository at this point in the history
v0.2.0
  • Loading branch information
chinedufn committed Jul 12, 2020
1 parent cedac4c commit fcba139
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 62 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rectangle-pack"
version = "0.1.6"
version = "0.2.0"
authors = ["Chinedu Francis Nwafili <[email protected]>"]
edition = "2018"
keywords = ["texture", "atlas", "bin", "box", "packer"]
Expand Down
32 changes: 22 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,22 @@ use rectangle_pack::{
volume_heuristic,
contains_smallest_box
};
use std::collections::HashMap;
use std::collections::BTreeMap;

// A rectangle ID just needs to meet these trait bounds (ideally also Copy)
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
// 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)
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
// 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,
Expand All @@ -51,7 +55,10 @@ enum MyCustomBinId {
// into the same bin. If this isn't possible an error is returned.
//
// Groups are optional.
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
//
// 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
}
Expand All @@ -73,7 +80,7 @@ rects_to_place.push_rect(
RectToInsert::new(30, 30, 255)
);

let mut target_bins = HashMap::new();
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));

Expand Down Expand Up @@ -134,18 +141,23 @@ The API shouldn't know about the specifics of any of these requirements - it sho
## Features

- Minimalist, generic API that pushes as much as possible into user-land.
- Place any number of 2d / 3d rectangles into any number of 2d / 3d target bins.
- Supports three dimensional rectangles through a width + height + depth based API.

- Arbitrarily grouping rectangles to ensure that they are placed in the same bin(s).
- Generic API that pushes as much as possible into user-land for maximum flexibility.

- Supports three dimensional rectangles through a width + height + depth based API.
- Group rectangles using generic group id's when you need to ensure that certain rectangles will always end up sharing a bin with each other.

- Supports two dimensional rectangles (depth = 1).

- User provided heuristics to grant full control over the packing algorithm.

- Zero dependencies, making it easier to embed it inside of a more use case specific library without introducing bloat.

- Deterministic packing.
- Packing of the same inputs using the same heuristics and the same sized target bins will always lead to the same layout.
- This is useful anywhere that reproducible builds are useful, such as when generating a texture atlas that is meant to be cached based on the hash of the contents.

## Future Work

The first version of `rectangle-pack` was designed to meet my own needs.
Expand All @@ -162,7 +174,7 @@ This could be accomplished by:

1. The API exposes three booleans for every incoming rectangles, `allow_global_x_axis_rotation`, `allow_global_y_axis_rotation`, `allow_global_z_axis_rotation`.

2. Let's say all three are enabled. When attempting to place the rectangle/box we should attempt it in all 6 possible orientations and then select the best placement (based on the `MoreSuitableContainersFn` heuristic).
2. Let's say all three are enabled. When attempting to place the rectangle/box we should attempt it in all 6 possible orientations and then select the best placement (based on the `ComparePotentialContainersFn` heuristic).

3. Return information to the caller about which axis ended up being rotated.

Expand Down
12 changes: 6 additions & 6 deletions src/bin_section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::fmt::{Display, Formatter};
///
/// Ordering::Greater means the first set of containers is better.
/// Ordering::Less means the second set of containers is better.
pub type MoreSuitableContainersFn =
pub type ComparePotentialContainersFn =
dyn Fn([WidthHeightDepth; 3], [WidthHeightDepth; 3], &BoxSizeHeuristicFn) -> Ordering;

/// Select the container that has the smallest box.
Expand All @@ -27,8 +27,8 @@ pub fn contains_smallest_box(
mut container2: [WidthHeightDepth; 3],
heuristic: &BoxSizeHeuristicFn,
) -> Ordering {
container1.sort_unstable_by(|a, b| heuristic(*a).cmp(&heuristic(*b)));
container2.sort_unstable_by(|a, b| heuristic(*a).cmp(&heuristic(*b)));
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])),
Expand Down Expand Up @@ -78,7 +78,7 @@ impl BinSection {
BinSection { x, y, z, whd }
}

// TODO: Delete - just the old API
// 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,
Expand Down Expand Up @@ -149,7 +149,7 @@ impl BinSection {
pub fn try_place(
&self,
incoming: &RectToInsert,
container_comparison_fn: &MoreSuitableContainersFn,
container_comparison_fn: &ComparePotentialContainersFn,
heuristic_fn: &BoxSizeHeuristicFn,
) -> Result<(PackedLocation, [BinSection; 3]), BinSectionError> {
self.incoming_can_fit(incoming)?;
Expand All @@ -163,7 +163,7 @@ impl BinSection {
self.width_largest_height_second_largest_depth_smallest(incoming),
];

all_combinations.sort_unstable_by(|a, b| {
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],
Expand Down
28 changes: 15 additions & 13 deletions src/grouped_rects_to_place.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::RectToInsert;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, HashMap};
use std::fmt::Debug;
use std::hash::Hash;

Expand All @@ -14,20 +14,22 @@ use std::hash::Hash;
#[derive(Debug)]
pub struct GroupedRectsToPlace<RectToPlaceId, GroupId = ()>
where
RectToPlaceId: Debug + Hash + Eq,
GroupId: Debug + Hash + Eq,
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: HashMap<RectToPlaceId, Vec<Group<GroupId, RectToPlaceId>>>,
pub(crate) group_id_to_inbound_ids: HashMap<Group<GroupId, RectToPlaceId>, Vec<RectToPlaceId>>,
pub(crate) group_id_to_inbound_ids: BTreeMap<Group<GroupId, RectToPlaceId>, Vec<RectToPlaceId>>,
pub(crate) rects: HashMap<RectToPlaceId, RectToInsert>,
}

/// A group of rectangles that need to be placed together
#[derive(Debug, Hash, Eq, PartialEq)]
#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub enum Group<GroupId, RectToPlaceId>
where
GroupId: Debug + Hash + Eq + PartialEq,
RectToPlaceId: Debug,
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.
Expand All @@ -43,8 +45,8 @@ where

impl<RectToPlaceId, GroupId> GroupedRectsToPlace<RectToPlaceId, GroupId>
where
RectToPlaceId: Debug + Hash + Clone + Eq,
GroupId: Debug + Hash + Clone + Eq,
RectToPlaceId: Debug + Hash + Clone + Eq + Ord + PartialOrd,
GroupId: Debug + Hash + Clone + Eq + Ord + PartialOrd,
{
/// Create a new `LayeredRectGroups`
pub fn new() -> Self {
Expand Down Expand Up @@ -141,8 +143,8 @@ mod tests {
);

assert_eq!(
lrg.group_id_to_inbound_ids[&Group::Grouped(0)],
vec![RectToPlaceId::One, RectToPlaceId::Two]
lrg.group_id_to_inbound_ids.get(&Group::Grouped(0)).unwrap(),
&vec![RectToPlaceId::One, RectToPlaceId::Two]
);
}

Expand Down Expand Up @@ -184,7 +186,7 @@ mod tests {
assert_eq!(lrg.rects[&RectToPlaceId::One], RectToInsert::new(10, 10, 1));
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
enum RectToPlaceId {
One,
Two,
Expand Down
Loading

0 comments on commit fcba139

Please sign in to comment.