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

View File

@@ -0,0 +1 @@
{"files":{"Cargo.toml":"f9d1f4722c8d76252fdfd8d5d0113db2c60dee654ff2a0619c776b0447c5ef72","LICENSE-APACHE":"7e41d7640541250da5939f8ad2676dd0097742aedfd26d0802f788336791083b","LICENSE-MIT":"4718670de5a73f9e0a68d56c36cd17db8c92587875a9a3cb412c10e8a5dccfb3","README.md":"87ccbde0e9add14ddfd768f9831d5fa718c61d625368dbde65b50dccceca86ea","src/bin_section.rs":"182e92166bb6ed4e05db9e7f5844bace3e3cf472896ea261b8ec22426ce8cee1","src/bin_section/overlaps.rs":"434a8768ecf860cfc7bdbae9a4151ee0d9effd98dc44903d578824ddfe419666","src/box_size_heuristics.rs":"b13044bc8b3fabe41abf8d68511716ef0f50a022b1e2bd1d14a239f7c065ec3d","src/grouped_rects_to_place.rs":"09ce068915c84f6c57b5d47e5b2260e1a553b6eeeab9f592f33d483283e42187","src/lib.rs":"3fdbf10d6ae44396fb5d6b8e012acd81b28751826cb2193adc1f9a75b28a9585","src/packed_location.rs":"5bf04d85135116ad4a489c02becf68383f7e36ce78115a207deccb697148132f","src/rect_to_insert.rs":"8362ede057c183d666da1c1d74d1becd6cfb9ebfa9b01f10f491b9b2bba0b2a3","src/target_bin.rs":"05802f9add89d42fc610347731bd0633a6c8f2e21cc048610a342a3e7f6176f1","src/target_bin/coalesce.rs":"68a0afd4ac45762806bea54f207dc50cbe3cc4034a4894979a94cdf88c1f6a2c","src/target_bin/push_available_bin_section.rs":"6397c3d5edcca1e8461bc1d5b0bb5a0fae4d6ea06e0f524adbb9a6d446914f26","src/width_height_depth.rs":"280fa97416920a10578013251d6ed3db0ccdbaa2c2e81116c03db6e51d2a77a1"},"package":"a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb"}

25
vendor/rectangle-pack/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,25 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies
#
# If you believe there's an error in this file please file an
# issue against the rust-lang/cargo repository. If you're
# editing this file be aware that the upstream Cargo.toml
# will likely look very different (and much more reasonable)
[package]
edition = "2018"
name = "rectangle-pack"
version = "0.4.2"
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
description = "A general purpose, deterministic bin packer designed to conform to any two or three dimensional use case."
keywords = ["texture", "atlas", "bin", "box", "packer"]
license = "MIT/Apache-2.0"
repository = "https://github.com/chinedufn/rectangle-pack"
[features]
default = ["std"]
std = []

201
vendor/rectangle-pack/LICENSE-APACHE vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 Chinedu Francis Nwafili
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

25
vendor/rectangle-pack/LICENSE-MIT vendored Normal file
View File

@@ -0,0 +1,25 @@
Copyright (c) 2021 Chinedu Francis Nwafili
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

275
vendor/rectangle-pack/README.md vendored Normal file
View File

@@ -0,0 +1,275 @@
# rectangle-pack [![Actions Status](https://github.com/chinedufn/rectangle-pack/workflows/test/badge.svg)](https://github.com/chinedufn/rectangle-pack/actions) [![docs](https://docs.rs/rectangle-pack/badge.svg)](https://docs.rs/rectangle-pack)
> A general purpose, deterministic bin packer designed to conform to any two or three dimensional use case.
`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.
`rectangle-pack` exposes an API that gives the consumer control over how rectangles are packed - allowing them to tailor
the packing to their specific use case.
While `rectangle-pack` was originally designed with texture atlas related use cases in mind - **the library itself has no notions of images and can be used
in any rectangle packing context**.
## Quickstart
```
# In your Cargo.toml
rectangle-pack = "0.4"
```
```rust
//! 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();
```
[Full API Documentation](https://docs.rs/rectangle-pack)
## Background / Initial Motivation
<details>
<summary>
Click to show the initial motivation for the library.
In my application I've switched to dynamically placing textures into atlases at runtime
instead of in how I previously used an asset compilation step, so some of the problems
explained here are now moot.
I still use rectangle-pack to power my runtime texture allocation, though,
along with a handful of other strategies depending on the nature of the
textures that need to be placed into the atlas.
rectangle-pack knows nothing about textures, so you can use it for any form of bin
packing, whether at runtime, during an offline step or any other time you like.
</summary>
I'm working on a game with some of the following texture atlas requirements (as of March 2020):
- I need to be able to guarantee that certain textures are available in the same atlas.
- For example - if I'm rendering terrain using a blend map that maps each channel to a color / metallic-roughness / normal texture
I want all of those textures to be available in the same atlas.
Otherwise in the worst case I might need over a dozen texture uniforms in order to render a single chunk of terrain.
- I want to have control over which channels are used when I'm packing my atlases.
- For example - I need to be able to easily pack my metallic and roughness textures into one channel each, while
packing color and normal channels into three channels.
- This means that my rectangle packer needs to expose configuration on the number of layers/channels available in our target bins.
- I need to be able to ensure that uncommon textures aren't taking up space in commonly used atlases
- For example - if a set of textures is only used in one specific region of the game - they shouldn't take up space in an atlas that contains a texture
that is used for very common game elements.
- This means that the packer needs to cater to some notion of groups or priority so that uncommon textures can be placed separately from common ones.
- This allows us to minimize the number of textures in GPU memory at any time since atlases with uncommon texture atlases can be removed after not being in use for some time.
- Without meeting this requirement - a large texture might be sitting on the GPU wasting space indefinitely since it shares an atlas with very common textures that will never be evicted.
- Note that we might not end up achieving this at the API level. This could potentially be achieved by just having the consumer call the library multiple times using whichever input rectangles they determine to be of
similar priority.
- Or some other solution.
- I need to be able to pack individual bits within a channel. For example - if I have a texture mask that encodes whether or not a fragment is metallic I want to be able to pack that into a single bit,
perhaps within my alpha channel.
- This means that our layers concept needs to support multi-dimensional needs. A layer within a layer.
- For example - In color space one might be thinking of RGBA channels / layers or be thinking about within the Alpha channel having 255 different sub-layers. Or even a smaller number of variable sized sub-layers.
Our API needs to make this simple to represent and pack.
- We don't necessarily need to model things that way internally or even expose a multi-layered notion in the API - we just need to enable those use cases - even if we still think of things as one dimension of layers at the API level.
- In fact .. as I type this .. one dimensions of layers at the API level both internally and externally sounds much simpler. Let the consumer worry about whether a channel is considered one layer (i.e. alpha) or 255 layers (i.e. every bit in the alpha channel).
- I need to be able to allow one texture to be present in multiple atlases.
- For example - say there is a grass texture that is used in every grassy region of the game. Say each of those regions has some textures that are only used in that region and thus relegated to their own
atlas. We want to make sure our grass texture is copied into each of those textures so that one texture can support the needs of that region instead of two.
These requirements are the initial guiding pillars to design the rectangle-pack API.
The API shouldn't know about the specifics of any of these requirements - it should just provide the bare minimum required to make them possible. We're trying to push as much into user-land as possible and leave
`rectangle-pack`s responsibility to not much more than answering:
> Given these rectangles that need to be placed, the maximum sizes of the target bins to place them in and some criteria about how to place and how not to place them,
> where can I put all of these rectangles?
</details>
<p></p>
## no_std
rectangle-pack supports `no_std` by disabling the `std` feature.
```toml
rectangle-pack = {version = "0.4", default-features = false}
```
Disabling the `std` feature does the following.
- `BTreeMap`s are used internally in places where `HashMap`s would have been used.
## Features
- 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.
- Generic API that pushes as much as possible into user-land for maximum flexibility.
- 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.
- Ability to remove placed rectangles and coalesce neighboring free space.
## Future Work
The first version of `rectangle-pack` was designed to meet my own needs.
As such there is functionality that could be useful that was not explored since I did not need it.
Here are some things that could be useful in the future.
### Three-Dimensional Incoming Rectangle Rotation
When attempting to place a Rectangle into the smallest available bin section we might want to rotate the rectangle in order to see which orientation produces the best fit.
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 `ComparePotentialContainersFn` heuristic).
3. Return information to the caller about which axis ended up being rotated.
### Mutually exclusive groups
An example of this is the ability to ensure that certain rectqngle groups are not placed in the same bins.
Perhaps you have two plates (bins) and two groups of cheese (rectangles), one for Alice and one for Bob.
When packing you want to ensure that these groups of cheese each end up in a different bin since Alice and Bob don't like to share.
### Stats on how the bins were packed
Things such as the amount of wasted space - or anything else that would allow the caller to compare the results of different combinations of
target bin sizes and heuristics to see which packed the most efficiently.
---
If you have a use case that isn't supported, go right ahead and open an issue or submit a pull request.
## Packing Algorithm
We started with the algorithm described in [rectpack2D] and then made some adjustments in order to
support our goal of flexibly supporting all use cases.
- The heuristic is provided by the caller instead of having `rectangle-pack` decide on a user provided heuristic.
- When splitting an available section of a bin into two new sections of a bin - we do not decide on how the split should occur arbitrarily.
Instead, we base it on the user provided `more_suitable_containers` heuristic function.
- There is a third dimension.
## In The Wild
Here are some known production users of `rectangle-pack`.
- [Akigi](https://akigi.com) uses `rectangle-pack` to power parts of its runtime texture allocation strategy.
- [Bevy](https://github.com/bevyengine/bevy/blob/9ae56e860468aa3158a702cbcf64e511b84a4b1c/crates/bevy_sprite/Cargo.toml#L29) uses `rectangle-pack`
to create texture atlases.
## Contributing
If you have a use case that isn't supported, a question, a patch, or anything else, go right ahead and open an issue or submit a pull request.
## To Test
To run the test suite.
```sh
# Clone the repository
git clone git@github.com:chinedufn/rectangle-pack.git
cd rectangle-pack
# Run tests
cargo test
```
## See Also
- [rectpack2D]
- Inspired parts of our initial implementation
[rectpack2D]: https://github.com/TeamHypersomnia/rectpack2D

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
}
}