├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src ├── bin_section.rs ├── bin_section └── overlaps.rs ├── box_size_heuristics.rs ├── grouped_rects_to_place.rs ├── lib.rs ├── packed_location.rs ├── rect_to_insert.rs ├── target_bin.rs ├── target_bin ├── coalesce.rs └── push_available_bin_section.rs └── width_height_depth.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | # TODO: Introduce caching https://github.com/actions/cache/blob/master/examples.md#rust---cargo 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions-rs/cargo@v1 17 | 18 | - name: Rust Version Info 19 | run: rustc --version && cargo --version 20 | 21 | - name: Run tests 22 | run: RUSTFLAGS="-D warnings" cargo test --workspace 23 | 24 | - name: Run tests with no defaults 25 | run: RUSTFLAGS="-D warnings" cargo test --workspace --no-default-features 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rectangle-pack" 3 | version = "0.4.2" 4 | authors = ["Chinedu Francis Nwafili "] 5 | edition = "2018" 6 | keywords = ["texture", "atlas", "bin", "box", "packer"] 7 | description = "A general purpose, deterministic bin packer designed to conform to any two or three dimensional use case." 8 | license = "MIT/Apache-2.0" 9 | repository = "https://github.com/chinedufn/rectangle-pack" 10 | 11 | [features] 12 | default = ["std"] 13 | std = [] 14 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Chinedu Francis Nwafili 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Chinedu Francis Nwafili 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | > A general purpose, deterministic bin packer designed to conform to any two or three dimensional use case. 4 | 5 | `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. 6 | 7 | `rectangle-pack` exposes an API that gives the consumer control over how rectangles are packed - allowing them to tailor 8 | the packing to their specific use case. 9 | 10 | 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 11 | in any rectangle packing context**. 12 | 13 | ## Quickstart 14 | 15 | ``` 16 | # In your Cargo.toml 17 | rectangle-pack = "0.4" 18 | ``` 19 | 20 | ```rust 21 | //! A basic example of packing rectangles into target bins 22 | 23 | use rectangle_pack::{ 24 | GroupedRectsToPlace, 25 | RectToInsert, 26 | pack_rects, 27 | TargetBin, 28 | volume_heuristic, 29 | contains_smallest_box 30 | }; 31 | use std::collections::BTreeMap; 32 | 33 | // A rectangle ID just needs to meet these trait bounds (ideally also Copy). 34 | // So you could use a String, PathBuf, or any other type that meets these 35 | // trat bounds. You do not have to use a custom enum. 36 | #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)] 37 | enum MyCustomRectId { 38 | RectOne, 39 | RectTwo, 40 | RectThree, 41 | } 42 | 43 | // A target bin ID just needs to meet these trait bounds (ideally also Copy) 44 | // So you could use a u32, &str, or any other type that meets these 45 | // trait bounds. You do not have to use a custom enum. 46 | #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)] 47 | enum MyCustomBinId { 48 | DestinationBinOne, 49 | DestinationBinTwo, 50 | } 51 | 52 | // A placement group just needs to meet these trait bounds (ideally also Copy). 53 | // 54 | // Groups allow you to ensure that a set of rectangles will be placed 55 | // into the same bin. If this isn't possible an error is returned. 56 | // 57 | // Groups are optional. 58 | // 59 | // You could use an i32, &'static str, or any other type that meets these 60 | // trat bounds. You do not have to use a custom enum. 61 | #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)] 62 | enum MyCustomGroupId { 63 | GroupIdOne 64 | } 65 | 66 | let mut rects_to_place = GroupedRectsToPlace::new(); 67 | rects_to_place.push_rect( 68 | MyCustomRectId::RectOne, 69 | Some(vec![MyCustomGroupId::GroupIdOne]), 70 | RectToInsert::new(10, 20, 255) 71 | ); 72 | rects_to_place.push_rect( 73 | MyCustomRectId::RectTwo, 74 | Some(vec![MyCustomGroupId::GroupIdOne]), 75 | RectToInsert::new(5, 50, 255) 76 | ); 77 | rects_to_place.push_rect( 78 | MyCustomRectId::RectThree, 79 | None, 80 | RectToInsert::new(30, 30, 255) 81 | ); 82 | 83 | let mut target_bins = BTreeMap::new(); 84 | target_bins.insert(MyCustomBinId::DestinationBinOne, TargetBin::new(2048, 2048, 255)); 85 | target_bins.insert(MyCustomBinId::DestinationBinTwo, TargetBin::new(4096, 4096, 1020)); 86 | 87 | // Information about where each `MyCustomRectId` was placed 88 | let rectangle_placements = pack_rects( 89 | &rects_to_place, 90 | &mut target_bins, 91 | &volume_heuristic, 92 | &contains_smallest_box 93 | ).unwrap(); 94 | ``` 95 | 96 | [Full API Documentation](https://docs.rs/rectangle-pack) 97 | 98 | ## Background / Initial Motivation 99 | 100 | In my application I've switched to dynamically placing textures into atlases at runtime 101 | instead of in how I previously used an asset compilation step, so some of the problems 102 | explained in the initial motivation details below are now moot. 103 | 104 | I still use rectangle-pack to power my runtime texture allocation, though, 105 | along with a handful of other strategies depending on the nature of the 106 | textures that need to be placed into the atlas. 107 | 108 | rectangle-pack knows nothing about textures, so you can use it for any form of bin 109 | packing, whether at runtime, during an offline step or any other time you like. 110 | 111 |
112 | 113 | Click to show the initial motivation for the library. 114 | 115 | 116 | I'm working on a game with some of the following texture atlas requirements (as of March 2020): 117 | 118 | - I need to be able to guarantee that certain textures are available in the same atlas. 119 | - For example - if I'm rendering terrain using a blend map that maps each channel to a color / metallic-roughness / normal texture 120 | I want all of those textures to be available in the same atlas. 121 | Otherwise in the worst case I might need over a dozen texture uniforms in order to render a single chunk of terrain. 122 | 123 | - I want to have control over which channels are used when I'm packing my atlases. 124 | - For example - I need to be able to easily pack my metallic and roughness textures into one channel each, while 125 | packing color and normal channels into three channels. 126 | - This means that my rectangle packer needs to expose configuration on the number of layers/channels available in our target bins. 127 | 128 | - I need to be able to ensure that uncommon textures aren't taking up space in commonly used atlases 129 | - 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 130 | that is used for very common game elements. 131 | - 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. 132 | - 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. 133 | - 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. 134 | - 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 135 | similar priority. 136 | - Or some other solution. 137 | 138 | - 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, 139 | perhaps within my alpha channel. 140 | - This means that our layers concept needs to support multi-dimensional needs. A layer within a layer. 141 | - 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. 142 | Our API needs to make this simple to represent and pack. 143 | - 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. 144 | - 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). 145 | 146 | - I need to be able to allow one texture to be present in multiple atlases. 147 | - 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 148 | 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. 149 | 150 | These requirements are the initial guiding pillars to design the rectangle-pack API. 151 | 152 | 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 153 | `rectangle-pack`s responsibility to not much more than answering: 154 | 155 | > 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, 156 | > where can I put all of these rectangles? 157 | 158 |
159 |

160 | 161 | ## no_std 162 | 163 | rectangle-pack supports `no_std` by disabling the `std` feature. 164 | 165 | ```toml 166 | rectangle-pack = {version = "0.4", default-features = false} 167 | ``` 168 | 169 | Disabling the `std` feature does the following. 170 | 171 | - `BTreeMap`s are used internally in places where `HashMap`s would have been used. 172 | 173 | ## Features 174 | 175 | - Place any number of 2d / 3d rectangles into any number of 2d / 3d target bins. 176 | - Supports three dimensional rectangles through a width + height + depth based API. 177 | 178 | - Generic API that pushes as much as possible into user-land for maximum flexibility. 179 | 180 | - 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. 181 | 182 | - Supports two dimensional rectangles (depth = 1). 183 | 184 | - User provided heuristics to grant full control over the packing algorithm. 185 | 186 | - Zero dependencies, making it easier to embed it inside of a more use case specific library without introducing bloat. 187 | 188 | - Deterministic packing. 189 | - Packing of the same inputs using the same heuristics and the same sized target bins will always lead to the same layout. 190 | - 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. 191 | 192 | - Ability to remove placed rectangles and coalesce neighboring free space. 193 | 194 | ## Future Work 195 | 196 | The first version of `rectangle-pack` was designed to meet my own needs. 197 | 198 | As such there is functionality that could be useful that was not explored since I did not need it. 199 | 200 | Here are some things that could be useful in the future. 201 | 202 | ### Three-Dimensional Incoming Rectangle Rotation 203 | 204 | 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. 205 | 206 | This could be accomplished by: 207 | 208 | 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`. 209 | 210 | 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). 211 | 212 | 3. Return information to the caller about which axis ended up being rotated. 213 | 214 | ### Mutually exclusive groups 215 | 216 | An example of this is the ability to ensure that certain rectqngle groups are not placed in the same bins. 217 | 218 | Perhaps you have two plates (bins) and two groups of cheese (rectangles), one for Alice and one for Bob. 219 | 220 | 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. 221 | 222 | ### Stats on how the bins were packed 223 | 224 | Things such as the amount of wasted space - or anything else that would allow the caller to compare the results of different combinations of 225 | target bin sizes and heuristics to see which packed the most efficiently. 226 | 227 | --- 228 | 229 | If you have a use case that isn't supported, go right ahead and open an issue or submit a pull request. 230 | 231 | ## Packing Algorithm 232 | 233 | We started with the algorithm described in [rectpack2D] and then made some adjustments in order to 234 | support our goal of flexibly supporting all use cases. 235 | 236 | 237 | - The heuristic is provided by the caller instead of having `rectangle-pack` decide on a user provided heuristic. 238 | 239 | - 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. 240 | Instead, we base it on the user provided `more_suitable_containers` heuristic function. 241 | 242 | - There is a third dimension. 243 | 244 | ## In The Wild 245 | 246 | Here are some known production users of `rectangle-pack`. 247 | 248 | - [Akigi](https://akigi.com) uses `rectangle-pack` to power parts of its runtime texture allocation strategy. 249 | 250 | - [Bevy](https://github.com/bevyengine/bevy/blob/9ae56e860468aa3158a702cbcf64e511b84a4b1c/crates/bevy_sprite/Cargo.toml#L29) uses `rectangle-pack` 251 | to create texture atlases. 252 | 253 | ## Contributing 254 | 255 | 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. 256 | 257 | ## To Test 258 | 259 | To run the test suite. 260 | 261 | ```sh 262 | # Clone the repository 263 | git clone git@github.com:chinedufn/rectangle-pack.git 264 | cd rectangle-pack 265 | 266 | # Run tests 267 | cargo test 268 | ``` 269 | 270 | ## See Also 271 | 272 | - [rectpack2D] 273 | - Inspired parts of our initial implementation 274 | 275 | [rectpack2D]: https://github.com/TeamHypersomnia/rectpack2D 276 | -------------------------------------------------------------------------------- /src/bin_section.rs: -------------------------------------------------------------------------------- 1 | use crate::packed_location::RotatedBy; 2 | use crate::{BoxSizeHeuristicFn, PackedLocation, RectToInsert, WidthHeightDepth}; 3 | 4 | use core::{ 5 | cmp::Ordering, 6 | fmt::{Debug, Display, Error as FmtError, Formatter}, 7 | }; 8 | 9 | mod overlaps; 10 | 11 | /// Given two sets of containers, which of these is the more suitable for our packing. 12 | /// 13 | /// Useful when we're determining how to split up the remaining volume/area of a box/rectangle. 14 | /// 15 | /// For example - we might deem it best to cut the remaining region vertically, or horizontally, 16 | /// or along the Z-axis. 17 | /// 18 | /// This decision is based on the more suitable contains heuristic. We determine all 6 possible 19 | /// ways to divide up remaining space, sort them using the more suitable contains heuristic function 20 | /// and choose the best one. 21 | /// 22 | /// Ordering::Greater means the first set of containers is better. 23 | /// Ordering::Less means the second set of containers is better. 24 | pub type ComparePotentialContainersFn = 25 | dyn Fn([WidthHeightDepth; 3], [WidthHeightDepth; 3], &BoxSizeHeuristicFn) -> Ordering; 26 | 27 | /// Select the container that has the smallest box. 28 | /// 29 | /// If there is a tie on the smallest boxes, select whichever also has the second smallest box. 30 | pub fn contains_smallest_box( 31 | mut container1: [WidthHeightDepth; 3], 32 | mut container2: [WidthHeightDepth; 3], 33 | heuristic: &BoxSizeHeuristicFn, 34 | ) -> Ordering { 35 | container1.sort_by(|a, b| heuristic(*a).cmp(&heuristic(*b))); 36 | container2.sort_by(|a, b| heuristic(*a).cmp(&heuristic(*b))); 37 | 38 | match heuristic(container2[0]).cmp(&heuristic(container1[0])) { 39 | Ordering::Equal => heuristic(container2[1]).cmp(&heuristic(container1[1])), 40 | o => o, 41 | } 42 | } 43 | 44 | /// A rectangular section within a target bin that takes up one or more layers 45 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Ord, PartialOrd)] 46 | pub struct BinSection { 47 | pub(crate) x: u32, 48 | pub(crate) y: u32, 49 | pub(crate) z: u32, 50 | pub(crate) whd: WidthHeightDepth, 51 | } 52 | 53 | /// An error while attempting to place a rectangle within a bin section; 54 | #[derive(Debug, Eq, PartialEq)] 55 | #[allow(missing_docs)] 56 | pub enum BinSectionError { 57 | PlacementWiderThanBinSection, 58 | PlacementTallerThanBinSection, 59 | PlacementDeeperThanBinSection, 60 | } 61 | 62 | impl Display for BinSectionError { 63 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { 64 | let err = match self { 65 | BinSectionError::PlacementWiderThanBinSection => { 66 | "Can not place a rectangle inside of a bin that is wider than that rectangle." 67 | } 68 | BinSectionError::PlacementTallerThanBinSection => { 69 | "Can not place a rectangle inside of a bin that is taller than that rectangle." 70 | } 71 | BinSectionError::PlacementDeeperThanBinSection => { 72 | "Can not place a rectangle inside of a bin that is deeper than that rectangle." 73 | } 74 | }; 75 | 76 | f.write_str(err) 77 | } 78 | } 79 | 80 | impl BinSection { 81 | /// Create a new BinSection 82 | pub fn new(x: u32, y: u32, z: u32, whd: WidthHeightDepth) -> Self { 83 | BinSection { x, y, z, whd } 84 | } 85 | 86 | // TODO: Delete - just the old API before we had the WidthHeightDepth struct 87 | fn new_spread(x: u32, y: u32, z: u32, width: u32, height: u32, depth: u32) -> Self { 88 | BinSection { 89 | x, 90 | y, 91 | z, 92 | whd: WidthHeightDepth { 93 | width, 94 | height, 95 | depth, 96 | }, 97 | } 98 | } 99 | } 100 | 101 | impl BinSection { 102 | /// See if a `LayeredRect` can fit inside of this BinSection. 103 | /// 104 | /// If it can we return the `BinSection`s that would be created by placing the `LayeredRect` 105 | /// inside of this `BinSection`. 106 | /// 107 | /// Consider the diagram below of a smaller box placed into of a larger one. 108 | /// 109 | /// The remaining space can be divided into three new sections. 110 | /// 111 | /// There are several ways to make this division. 112 | /// 113 | /// You could keep all of the space above the smaller box intact and split up the space 114 | /// behind and to the right of it. 115 | /// 116 | /// But within that you have a choice between whether the overlapping space goes to right 117 | /// or behind box. 118 | /// 119 | /// Or you could keep the space to the right and split the top and behind space. 120 | /// 121 | /// etc. 122 | /// 123 | /// There are six possible configurations of newly created sections. The configuration to use 124 | /// is decided on based on a a function provided by the consumer. 125 | /// 126 | /// 127 | /// ```text 128 | /// ┌┬───────────────────┬┐ 129 | /// ┌─┘│ ┌─┘│ 130 | /// ┌─┘ │ ┌─┘ │ 131 | /// ┌─┘ │ ┌─┘ │ 132 | /// ┌─┘ │ ┌─┘ │ 133 | /// ┌─┘ │ ┌─┘ │ 134 | /// ┌─┴──────────┼───────┬─┘ │ 135 | /// │ │ │ │ 136 | /// │ │ │ │ 137 | /// │ ┌┬───┴────┬─┐│ │ 138 | /// │ ┌─┘│ ┌─┘ ││ │ 139 | /// │ ┌─┘ │ ┌─┘ ││ │ 140 | /// │ ┌─┘ │ ┌─┘ ├┼───────────┬┘ 141 | /// ├─┴──────┤ ─┘ ││ ┌─┘ 142 | /// │ ┌┴─┬───────┬┘│ ┌─┘ 143 | /// │ ┌─┘ │ ┌─┘ │ ┌─┘ 144 | /// │ ┌─┘ │ ┌─┘ │ ┌─┘ 145 | /// │ ┌─┘ │ ┌─┘ │ ┌─┘ 146 | /// └─┴────────┴─┴───────┴─┘ 147 | /// ``` 148 | /// 149 | /// # Note 150 | /// 151 | /// Written to be readable/maintainable, not to minimize conditional logic, under the 152 | /// (unverified) assumption that a release compilation will inline and dedupe the function 153 | /// calls and conditionals. 154 | pub fn try_place( 155 | &self, 156 | incoming: &RectToInsert, 157 | container_comparison_fn: &ComparePotentialContainersFn, 158 | heuristic_fn: &BoxSizeHeuristicFn, 159 | ) -> Result<(PackedLocation, [BinSection; 3]), BinSectionError> { 160 | self.incoming_can_fit(incoming)?; 161 | 162 | let mut all_combinations = [ 163 | self.depth_largest_height_second_largest_width_smallest(incoming), 164 | self.depth_largest_width_second_largest_height_smallest(incoming), 165 | self.height_largest_depth_second_largest_width_smallest(incoming), 166 | self.height_largest_width_second_largest_depth_smallest(incoming), 167 | self.width_largest_depth_second_largest_height_smallest(incoming), 168 | self.width_largest_height_second_largest_depth_smallest(incoming), 169 | ]; 170 | 171 | all_combinations.sort_by(|a, b| { 172 | container_comparison_fn( 173 | [a[0].whd, a[1].whd, a[2].whd], 174 | [b[0].whd, b[1].whd, b[2].whd], 175 | heuristic_fn, 176 | ) 177 | }); 178 | 179 | let packed_location = PackedLocation { 180 | x: self.x, 181 | y: self.y, 182 | z: self.z, 183 | whd: WidthHeightDepth { 184 | width: incoming.width(), 185 | height: incoming.height(), 186 | depth: incoming.depth(), 187 | }, 188 | x_axis_rotation: RotatedBy::ZeroDegrees, 189 | y_axis_rotation: RotatedBy::ZeroDegrees, 190 | z_axis_rotation: RotatedBy::ZeroDegrees, 191 | }; 192 | 193 | Ok((packed_location, all_combinations[5])) 194 | } 195 | 196 | fn incoming_can_fit(&self, incoming: &RectToInsert) -> Result<(), BinSectionError> { 197 | if incoming.width() > self.whd.width { 198 | return Err(BinSectionError::PlacementWiderThanBinSection); 199 | } 200 | if incoming.height() > self.whd.height { 201 | return Err(BinSectionError::PlacementTallerThanBinSection); 202 | } 203 | 204 | if incoming.depth() > self.whd.depth { 205 | return Err(BinSectionError::PlacementDeeperThanBinSection); 206 | } 207 | 208 | Ok(()) 209 | } 210 | 211 | fn width_largest_height_second_largest_depth_smallest( 212 | &self, 213 | incoming: &RectToInsert, 214 | ) -> [BinSection; 3] { 215 | [ 216 | self.empty_space_directly_right(incoming), 217 | self.all_empty_space_above_excluding_behind(incoming), 218 | self.all_empty_space_behind(incoming), 219 | ] 220 | } 221 | 222 | fn width_largest_depth_second_largest_height_smallest( 223 | &self, 224 | incoming: &RectToInsert, 225 | ) -> [BinSection; 3] { 226 | [ 227 | self.empty_space_directly_right(incoming), 228 | self.all_empty_space_above(incoming), 229 | self.all_empty_space_behind_excluding_above(incoming), 230 | ] 231 | } 232 | 233 | fn height_largest_width_second_largest_depth_smallest( 234 | &self, 235 | incoming: &RectToInsert, 236 | ) -> [BinSection; 3] { 237 | [ 238 | self.all_empty_space_right_excluding_behind(incoming), 239 | self.empty_space_directly_above(incoming), 240 | self.all_empty_space_behind(incoming), 241 | ] 242 | } 243 | 244 | fn height_largest_depth_second_largest_width_smallest( 245 | &self, 246 | incoming: &RectToInsert, 247 | ) -> [BinSection; 3] { 248 | [ 249 | self.all_empty_space_right(incoming), 250 | self.empty_space_directly_above(incoming), 251 | self.all_empty_space_behind_excluding_right(incoming), 252 | ] 253 | } 254 | 255 | fn depth_largest_width_second_largest_height_smallest( 256 | &self, 257 | incoming: &RectToInsert, 258 | ) -> [BinSection; 3] { 259 | [ 260 | self.all_empty_space_right_excluding_above(incoming), 261 | self.all_empty_space_above(incoming), 262 | self.empty_space_directly_behind(incoming), 263 | ] 264 | } 265 | 266 | fn depth_largest_height_second_largest_width_smallest( 267 | &self, 268 | incoming: &RectToInsert, 269 | ) -> [BinSection; 3] { 270 | [ 271 | self.all_empty_space_right(incoming), 272 | self.all_empty_space_above_excluding_right(incoming), 273 | self.empty_space_directly_behind(incoming), 274 | ] 275 | } 276 | 277 | fn all_empty_space_above(&self, incoming: &RectToInsert) -> BinSection { 278 | BinSection::new_spread( 279 | self.x, 280 | self.y + incoming.height(), 281 | self.z, 282 | self.whd.width, 283 | self.whd.height - incoming.height(), 284 | self.whd.depth, 285 | ) 286 | } 287 | 288 | fn all_empty_space_right(&self, incoming: &RectToInsert) -> BinSection { 289 | BinSection::new_spread( 290 | self.x + incoming.width(), 291 | self.y, 292 | self.z, 293 | self.whd.width - incoming.width(), 294 | self.whd.height, 295 | self.whd.depth, 296 | ) 297 | } 298 | 299 | fn all_empty_space_behind(&self, incoming: &RectToInsert) -> BinSection { 300 | BinSection::new_spread( 301 | self.x, 302 | self.y, 303 | self.z + incoming.depth(), 304 | self.whd.width, 305 | self.whd.height, 306 | self.whd.depth - incoming.depth(), 307 | ) 308 | } 309 | 310 | fn empty_space_directly_above(&self, incoming: &RectToInsert) -> BinSection { 311 | BinSection::new_spread( 312 | self.x, 313 | self.y + incoming.height(), 314 | self.z, 315 | incoming.width(), 316 | self.whd.height - incoming.height(), 317 | incoming.depth(), 318 | ) 319 | } 320 | 321 | fn empty_space_directly_right(&self, incoming: &RectToInsert) -> BinSection { 322 | BinSection::new_spread( 323 | self.x + incoming.width(), 324 | self.y, 325 | self.z, 326 | self.whd.width - incoming.width(), 327 | incoming.height(), 328 | incoming.depth(), 329 | ) 330 | } 331 | 332 | fn empty_space_directly_behind(&self, incoming: &RectToInsert) -> BinSection { 333 | BinSection::new( 334 | self.x, 335 | self.y, 336 | self.z + incoming.depth(), 337 | WidthHeightDepth { 338 | width: incoming.width(), 339 | height: incoming.height(), 340 | depth: self.whd.depth - incoming.depth(), 341 | }, 342 | ) 343 | } 344 | 345 | fn all_empty_space_above_excluding_right(&self, incoming: &RectToInsert) -> BinSection { 346 | BinSection::new( 347 | self.x, 348 | self.y + incoming.height(), 349 | self.z, 350 | WidthHeightDepth { 351 | width: incoming.width(), 352 | height: self.whd.height - incoming.height(), 353 | depth: self.whd.depth, 354 | }, 355 | ) 356 | } 357 | 358 | fn all_empty_space_above_excluding_behind(&self, incoming: &RectToInsert) -> BinSection { 359 | BinSection::new( 360 | self.x, 361 | self.y + incoming.height(), 362 | self.z, 363 | WidthHeightDepth { 364 | width: self.whd.width, 365 | height: self.whd.height - incoming.height(), 366 | depth: incoming.depth(), 367 | }, 368 | ) 369 | } 370 | 371 | fn all_empty_space_right_excluding_above(&self, incoming: &RectToInsert) -> BinSection { 372 | BinSection::new( 373 | self.x + incoming.width(), 374 | self.y, 375 | self.z, 376 | WidthHeightDepth { 377 | width: self.whd.width - incoming.width(), 378 | height: incoming.height(), 379 | depth: self.whd.depth, 380 | }, 381 | ) 382 | } 383 | 384 | fn all_empty_space_right_excluding_behind(&self, incoming: &RectToInsert) -> BinSection { 385 | BinSection::new( 386 | self.x + incoming.width(), 387 | self.y, 388 | self.z, 389 | WidthHeightDepth { 390 | width: self.whd.width - incoming.width(), 391 | height: self.whd.height, 392 | depth: incoming.depth(), 393 | }, 394 | ) 395 | } 396 | 397 | fn all_empty_space_behind_excluding_above(&self, incoming: &RectToInsert) -> BinSection { 398 | BinSection::new( 399 | self.x, 400 | self.y, 401 | self.z + incoming.depth(), 402 | WidthHeightDepth { 403 | width: self.whd.width, 404 | height: incoming.height(), 405 | depth: self.whd.depth - incoming.depth(), 406 | }, 407 | ) 408 | } 409 | 410 | fn all_empty_space_behind_excluding_right(&self, incoming: &RectToInsert) -> BinSection { 411 | BinSection::new( 412 | self.x, 413 | self.y, 414 | self.z + incoming.depth(), 415 | WidthHeightDepth { 416 | width: incoming.width(), 417 | height: self.whd.height, 418 | depth: self.whd.depth - incoming.depth(), 419 | }, 420 | ) 421 | } 422 | } 423 | 424 | #[cfg(test)] 425 | mod tests { 426 | use super::*; 427 | use crate::{volume_heuristic, RectToInsert}; 428 | 429 | const BIGGEST: u32 = 50; 430 | const MIDDLE: u32 = 25; 431 | const SMALLEST: u32 = 10; 432 | 433 | const FULL: u32 = 100; 434 | 435 | /// If we're trying to place a rectangle that is wider than the container we return an error 436 | #[test] 437 | fn error_if_placement_is_wider_than_bin_section() { 438 | let bin_section = bin_section_width_height_depth(5, 20, 1); 439 | let placement = RectToInsert::new(6, 20, 1); 440 | 441 | assert_eq!( 442 | bin_section 443 | .try_place(&placement, &contains_smallest_box, &volume_heuristic) 444 | .unwrap_err(), 445 | BinSectionError::PlacementWiderThanBinSection 446 | ); 447 | } 448 | 449 | /// If we're trying to place a rectangle that is taller than the container we return an error 450 | #[test] 451 | fn error_if_placement_is_taller_than_bin_section() { 452 | let bin_section = bin_section_width_height_depth(5, 20, 1); 453 | let placement = RectToInsert::new(5, 21, 1); 454 | 455 | assert_eq!( 456 | bin_section 457 | .try_place(&placement, &contains_smallest_box, &volume_heuristic) 458 | .unwrap_err(), 459 | BinSectionError::PlacementTallerThanBinSection 460 | ); 461 | } 462 | 463 | /// If we're trying to place a rectangle that is deeper than the container we return an error 464 | #[test] 465 | fn error_if_placement_is_deeper_than_bin_section() { 466 | let bin_section = bin_section_width_height_depth(5, 20, 1); 467 | let placement = RectToInsert::new(5, 20, 2); 468 | 469 | assert_eq!( 470 | bin_section 471 | .try_place(&placement, &contains_smallest_box, &volume_heuristic) 472 | .unwrap_err(), 473 | BinSectionError::PlacementDeeperThanBinSection 474 | ); 475 | } 476 | 477 | fn test_splits( 478 | container_dimensions: u32, 479 | rect_to_place: WidthHeightDepth, 480 | mut expected: [BinSection; 3], 481 | ) { 482 | let dim = container_dimensions; 483 | let bin_section = bin_section_width_height_depth(dim, dim, dim); 484 | 485 | let whd = rect_to_place; 486 | 487 | let placement = RectToInsert::new(whd.width, whd.height, whd.depth); 488 | 489 | let mut packed = bin_section 490 | .try_place(&placement, &contains_smallest_box, &volume_heuristic) 491 | .unwrap(); 492 | 493 | packed.1.sort(); 494 | expected.sort(); 495 | 496 | assert_eq!(packed.1, expected); 497 | } 498 | 499 | /// Verify that we choose the correct splits when the placed rectangle is width > height > depth 500 | #[test] 501 | fn width_largest_height_second_largest_depth_smallest() { 502 | let whd = WidthHeightDepth { 503 | width: BIGGEST, 504 | height: MIDDLE, 505 | depth: SMALLEST, 506 | }; 507 | 508 | test_splits( 509 | FULL, 510 | whd, 511 | [ 512 | BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, whd.height, whd.depth), 513 | BinSection::new_spread(0, whd.height, 0, FULL, FULL - whd.height, whd.depth), 514 | BinSection::new_spread(0, 0, whd.depth, FULL, FULL, FULL - whd.depth), 515 | ], 516 | ); 517 | } 518 | 519 | /// Verify that we choose the correct splits when the placed rectangle is width > depth > height 520 | #[test] 521 | fn width_largest_depth_second_largest_height_smallest() { 522 | let whd = WidthHeightDepth { 523 | width: BIGGEST, 524 | height: SMALLEST, 525 | depth: MIDDLE, 526 | }; 527 | 528 | test_splits( 529 | FULL, 530 | whd, 531 | [ 532 | BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, whd.height, whd.depth), 533 | BinSection::new_spread(0, whd.height, 0, FULL, FULL - whd.height, FULL), 534 | BinSection::new_spread(0, 0, whd.depth, FULL, whd.height, FULL - whd.depth), 535 | ], 536 | ); 537 | } 538 | 539 | /// Verify that we choose the correct splits when the placed rectangle is height > width > depth 540 | #[test] 541 | fn height_largest_width_second_largest_depth_smallest() { 542 | let whd = WidthHeightDepth { 543 | width: MIDDLE, 544 | height: BIGGEST, 545 | depth: SMALLEST, 546 | }; 547 | 548 | test_splits( 549 | FULL, 550 | whd, 551 | [ 552 | BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, FULL, whd.depth), 553 | BinSection::new_spread(0, whd.height, 0, whd.width, FULL - whd.height, whd.depth), 554 | BinSection::new_spread(0, 0, whd.depth, FULL, FULL, FULL - whd.depth), 555 | ], 556 | ); 557 | } 558 | 559 | /// Verify that we choose the correct splits when the placed rectangle is height > depth > width 560 | #[test] 561 | fn height_largest_depth_second_largest_width_smallest() { 562 | let whd = WidthHeightDepth { 563 | width: SMALLEST, 564 | height: BIGGEST, 565 | depth: MIDDLE, 566 | }; 567 | 568 | test_splits( 569 | FULL, 570 | whd, 571 | [ 572 | BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, FULL, FULL), 573 | BinSection::new_spread(0, whd.height, 0, whd.width, FULL - whd.height, whd.depth), 574 | BinSection::new_spread(0, 0, whd.depth, whd.width, FULL, FULL - whd.depth), 575 | ], 576 | ); 577 | } 578 | 579 | /// Verify that we choose the correct splits when the placed rectangle is depth > width > height 580 | #[test] 581 | fn depth_largest_width_second_largest_height_smallest() { 582 | let whd = WidthHeightDepth { 583 | width: MIDDLE, 584 | height: SMALLEST, 585 | depth: BIGGEST, 586 | }; 587 | 588 | test_splits( 589 | FULL, 590 | whd, 591 | [ 592 | BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, whd.height, FULL), 593 | BinSection::new_spread(0, whd.height, 0, FULL, FULL - whd.height, FULL), 594 | BinSection::new_spread(0, 0, whd.depth, whd.width, whd.height, FULL - whd.depth), 595 | ], 596 | ); 597 | } 598 | 599 | /// Verify that we choose the correct splits when the placed rectangle is depth > height > width 600 | #[test] 601 | fn depth_largest_height_second_largest_width_smallest() { 602 | let whd = WidthHeightDepth { 603 | width: SMALLEST, 604 | height: MIDDLE, 605 | depth: BIGGEST, 606 | }; 607 | 608 | test_splits( 609 | FULL, 610 | whd, 611 | [ 612 | BinSection::new_spread(whd.width, 0, 0, FULL - whd.width, FULL, FULL), 613 | BinSection::new_spread(0, whd.height, 0, whd.width, FULL - whd.height, FULL), 614 | BinSection::new_spread(0, 0, whd.depth, whd.width, whd.height, FULL - whd.depth), 615 | ], 616 | ); 617 | } 618 | 619 | // #[test] 620 | // fn todo() { 621 | // unimplemented!("Add tests for supporting rotation"); 622 | // } 623 | 624 | fn bin_section_width_height_depth(width: u32, height: u32, depth: u32) -> BinSection { 625 | BinSection::new( 626 | 0, 627 | 0, 628 | 0, 629 | WidthHeightDepth { 630 | width, 631 | height, 632 | depth, 633 | }, 634 | ) 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /src/bin_section/overlaps.rs: -------------------------------------------------------------------------------- 1 | use crate::bin_section::BinSection; 2 | 3 | impl BinSection { 4 | /// Whether or not two bin sections overlap each other. 5 | pub fn overlaps(&self, other: &Self) -> bool { 6 | (self.x >= other.x && self.x <= other.right()) 7 | && (self.y >= other.y && self.y <= other.top()) 8 | && (self.z >= other.z && self.z <= other.back()) 9 | } 10 | 11 | fn right(&self) -> u32 { 12 | self.x + (self.whd.width - 1) 13 | } 14 | 15 | fn top(&self) -> u32 { 16 | self.y + (self.whd.height - 1) 17 | } 18 | 19 | fn back(&self) -> u32 { 20 | self.z + (self.whd.depth - 1) 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use super::*; 27 | use crate::width_height_depth::WidthHeightDepth; 28 | 29 | /// Verify that the overlaps method works properly. 30 | #[test] 31 | fn overlaps() { 32 | OverlapsTest { 33 | label: "Overlaps X, Y and Z", 34 | section1: BinSection::new(3, 4, 5, WidthHeightDepth::new(1, 1, 1)), 35 | section2: section_2_3_4(), 36 | expected_overlap: true, 37 | } 38 | .test(); 39 | 40 | OverlapsTest { 41 | label: "Overlaps X only", 42 | section1: BinSection::new(3, 40, 50, WidthHeightDepth::new(1, 1, 1)), 43 | section2: section_2_3_4(), 44 | expected_overlap: false, 45 | } 46 | .test(); 47 | 48 | OverlapsTest { 49 | label: "Overlaps Y only", 50 | section1: BinSection::new(30, 4, 50, WidthHeightDepth::new(1, 1, 1)), 51 | section2: section_2_3_4(), 52 | expected_overlap: false, 53 | } 54 | .test(); 55 | 56 | OverlapsTest { 57 | label: "Overlaps Z only", 58 | section1: BinSection::new(30, 40, 5, WidthHeightDepth::new(1, 1, 1)), 59 | section2: section_2_3_4(), 60 | expected_overlap: false, 61 | } 62 | .test(); 63 | } 64 | 65 | fn section_2_3_4() -> BinSection { 66 | BinSection::new(2, 3, 4, WidthHeightDepth::new(2, 3, 4)) 67 | } 68 | 69 | struct OverlapsTest { 70 | label: &'static str, 71 | section1: BinSection, 72 | section2: BinSection, 73 | expected_overlap: bool, 74 | } 75 | 76 | impl OverlapsTest { 77 | fn test(self) { 78 | assert_eq!( 79 | self.section1.overlaps(&self.section2), 80 | self.expected_overlap, 81 | "{}", 82 | self.label 83 | ) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/box_size_heuristics.rs: -------------------------------------------------------------------------------- 1 | use crate::WidthHeightDepth; 2 | 3 | /// Incoming boxes are places into the smallest hole that will fit them. 4 | /// 5 | /// "small" vs. "large" is based on the heuristic function. 6 | /// 7 | /// A larger heuristic means that the box is larger. 8 | pub type BoxSizeHeuristicFn = dyn Fn(WidthHeightDepth) -> u128; 9 | 10 | /// The volume of the box 11 | pub fn volume_heuristic(whd: WidthHeightDepth) -> u128 { 12 | whd.width as u128 * whd.height as u128 * whd.depth as u128 13 | } 14 | -------------------------------------------------------------------------------- /src/grouped_rects_to_place.rs: -------------------------------------------------------------------------------- 1 | use crate::RectToInsert; 2 | 3 | #[cfg(not(std))] 4 | use alloc::collections::BTreeMap as KeyValMap; 5 | #[cfg(std)] 6 | use std::collections::HashMap as KeyValMap; 7 | 8 | use alloc::{ 9 | collections::{btree_map::Entry, BTreeMap}, 10 | vec::Vec, 11 | }; 12 | use core::{fmt::Debug, hash::Hash}; 13 | 14 | /// Groups of rectangles that need to be placed into bins. 15 | /// 16 | /// When placing groups a heuristic is used to determine which groups are the largest. 17 | /// Larger groups are placed first. 18 | /// 19 | /// A group's heuristic is computed by calculating the heuristic of all of the rectangles inside 20 | /// the group and then summing them. 21 | #[derive(Debug)] 22 | pub struct GroupedRectsToPlace 23 | where 24 | RectToPlaceId: Debug + Hash + Eq + Ord + PartialOrd, 25 | GroupId: Debug + Hash + Eq + Ord + PartialOrd, 26 | { 27 | // FIXME: inbound_id_to_group_id appears to be unused. If so, remove it. Also remove the 28 | // Hash and Eq constraints on RectToPlaceId if we remove this map 29 | pub(crate) inbound_id_to_group_ids: 30 | KeyValMap>>, 31 | pub(crate) group_id_to_inbound_ids: BTreeMap, Vec>, 32 | pub(crate) rects: KeyValMap, 33 | } 34 | 35 | /// A group of rectangles that need to be placed together 36 | #[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] 37 | pub enum Group 38 | where 39 | GroupId: Debug + Hash + Eq + PartialEq + Ord + PartialOrd, 40 | RectToPlaceId: Debug + Ord + PartialOrd, 41 | { 42 | /// An automatically generated (auto incrementing) group identifier for rectangles that were 43 | /// passed in without any associated group ids. 44 | /// 45 | /// We still want to treat these lone rectangles as their own "groups" so that we can more 46 | /// easily compare their heuristics against those of other groups. 47 | /// 48 | /// If everything is a "group" - comparing groups becomes simpler. 49 | Ungrouped(RectToPlaceId), 50 | /// Wraps a user provided group identifier. 51 | Grouped(GroupId), 52 | } 53 | 54 | impl GroupedRectsToPlace 55 | where 56 | RectToPlaceId: Debug + Hash + Clone + Eq + Ord + PartialOrd, 57 | GroupId: Debug + Hash + Clone + Eq + Ord + PartialOrd, 58 | { 59 | /// Create a new `LayeredRectGroups` 60 | pub fn new() -> Self { 61 | Self { 62 | inbound_id_to_group_ids: Default::default(), 63 | group_id_to_inbound_ids: Default::default(), 64 | rects: Default::default(), 65 | } 66 | } 67 | 68 | /// Push one or more rectangles 69 | /// 70 | /// # Panics 71 | /// 72 | /// Panics if a `Some(Vec)` passed in but the length is 0, as this is likely a 73 | /// mistake and `None` should be used instead. 74 | pub fn push_rect( 75 | &mut self, 76 | inbound_id: RectToPlaceId, 77 | group_ids: Option>, 78 | inbound: RectToInsert, 79 | ) { 80 | self.rects.insert(inbound_id.clone(), inbound); 81 | 82 | match group_ids { 83 | None => { 84 | self.group_id_to_inbound_ids.insert( 85 | Group::Ungrouped(inbound_id.clone()), 86 | vec![inbound_id.clone()], 87 | ); 88 | 89 | self.inbound_id_to_group_ids 90 | .insert(inbound_id.clone(), vec![Group::Ungrouped(inbound_id)]); 91 | } 92 | Some(group_ids) => { 93 | self.inbound_id_to_group_ids.insert( 94 | inbound_id.clone(), 95 | group_ids 96 | .clone() 97 | .into_iter() 98 | .map(|gid| Group::Grouped(gid)) 99 | .collect(), 100 | ); 101 | 102 | for group_id in group_ids { 103 | match self.group_id_to_inbound_ids.entry(Group::Grouped(group_id)) { 104 | Entry::Occupied(mut o) => { 105 | o.get_mut().push(inbound_id.clone()); 106 | } 107 | Entry::Vacant(v) => { 108 | v.insert(vec![inbound_id.clone()]); 109 | } 110 | }; 111 | } 112 | } 113 | }; 114 | } 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use super::*; 120 | use crate::RectToInsert; 121 | 122 | /// Verify that if we insert a rectangle that doesn't have a group it is given a group ID based 123 | /// on its RectToPlaceId. 124 | #[test] 125 | fn ungrouped_rectangles_use_their_inbound_id_as_their_group_id() { 126 | let mut lrg: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); 127 | 128 | lrg.push_rect(RectToPlaceId::One, None, RectToInsert::new(10, 10, 1)); 129 | 130 | assert_eq!( 131 | lrg.group_id_to_inbound_ids[&Group::Ungrouped(RectToPlaceId::One)], 132 | vec![RectToPlaceId::One] 133 | ); 134 | } 135 | 136 | /// When multiple different rects from the same group are pushed they should be present in the 137 | /// map of group id -> inbound rect id 138 | #[test] 139 | fn group_id_to_inbound_ids() { 140 | let mut lrg = GroupedRectsToPlace::new(); 141 | 142 | lrg.push_rect( 143 | RectToPlaceId::One, 144 | Some(vec![0]), 145 | RectToInsert::new(10, 10, 1), 146 | ); 147 | lrg.push_rect( 148 | RectToPlaceId::Two, 149 | Some(vec![0]), 150 | RectToInsert::new(10, 10, 1), 151 | ); 152 | 153 | assert_eq!( 154 | lrg.group_id_to_inbound_ids.get(&Group::Grouped(0)).unwrap(), 155 | &vec![RectToPlaceId::One, RectToPlaceId::Two] 156 | ); 157 | } 158 | 159 | /// Verify that we store the map of inbound id -> group ids 160 | #[test] 161 | fn inbound_id_to_group_ids() { 162 | let mut lrg = GroupedRectsToPlace::new(); 163 | 164 | lrg.push_rect( 165 | RectToPlaceId::One, 166 | Some(vec![0, 1]), 167 | RectToInsert::new(10, 10, 1), 168 | ); 169 | 170 | lrg.push_rect(RectToPlaceId::Two, None, RectToInsert::new(10, 10, 1)); 171 | 172 | assert_eq!( 173 | lrg.inbound_id_to_group_ids[&RectToPlaceId::One], 174 | vec![Group::Grouped(0), Group::Grouped(1)] 175 | ); 176 | 177 | assert_eq!( 178 | lrg.inbound_id_to_group_ids[&RectToPlaceId::Two], 179 | vec![Group::Ungrouped(RectToPlaceId::Two)] 180 | ); 181 | } 182 | 183 | /// Verify that we store in rectangle associated with its inbound ID 184 | #[test] 185 | fn store_the_inbound_rectangle() { 186 | let mut lrg = GroupedRectsToPlace::new(); 187 | 188 | lrg.push_rect( 189 | RectToPlaceId::One, 190 | Some(vec![0, 1]), 191 | RectToInsert::new(10, 10, 1), 192 | ); 193 | 194 | assert_eq!(lrg.rects[&RectToPlaceId::One], RectToInsert::new(10, 10, 1)); 195 | } 196 | 197 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] 198 | enum RectToPlaceId { 199 | One, 200 | Two, 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `rectangle-pack` is a library focused on laying out any number of smaller rectangles 2 | //! (both 2d rectangles and 3d rectangular prisms) inside any number of larger rectangles. 3 | #![cfg_attr(not(std), no_std)] 4 | #![deny(missing_docs)] 5 | 6 | #[macro_use] 7 | extern crate alloc; 8 | 9 | #[cfg(not(std))] 10 | use alloc::collections::BTreeMap as KeyValMap; 11 | #[cfg(std)] 12 | use std::collections::HashMap as KeyValMap; 13 | 14 | use alloc::{collections::BTreeMap, vec::Vec}; 15 | 16 | use core::{ 17 | fmt::{Debug, Display, Error as FmtError, Formatter}, 18 | hash::Hash, 19 | }; 20 | 21 | pub use crate::bin_section::contains_smallest_box; 22 | pub use crate::bin_section::BinSection; 23 | pub use crate::bin_section::ComparePotentialContainersFn; 24 | use crate::grouped_rects_to_place::Group; 25 | pub use crate::grouped_rects_to_place::GroupedRectsToPlace; 26 | pub use crate::target_bin::TargetBin; 27 | use crate::width_height_depth::WidthHeightDepth; 28 | 29 | pub use self::box_size_heuristics::{volume_heuristic, BoxSizeHeuristicFn}; 30 | pub use self::rect_to_insert::RectToInsert; 31 | pub use crate::packed_location::PackedLocation; 32 | 33 | mod bin_section; 34 | mod grouped_rects_to_place; 35 | 36 | mod packed_location; 37 | mod rect_to_insert; 38 | mod target_bin; 39 | mod width_height_depth; 40 | 41 | mod box_size_heuristics; 42 | 43 | /// Determine how to fit a set of incoming rectangles (2d or 3d) into a set of target bins. 44 | /// 45 | /// ## Example 46 | /// 47 | /// ``` 48 | /// //! A basic example of packing rectangles into target bins 49 | /// 50 | /// use rectangle_pack::{ 51 | /// GroupedRectsToPlace, 52 | /// RectToInsert, 53 | /// pack_rects, 54 | /// TargetBin, 55 | /// volume_heuristic, 56 | /// contains_smallest_box 57 | /// }; 58 | /// use std::collections::BTreeMap; 59 | /// 60 | /// // A rectangle ID just needs to meet these trait bounds (ideally also Copy). 61 | /// // So you could use a String, PathBuf, or any other type that meets these 62 | /// // trat bounds. You do not have to use a custom enum. 63 | /// #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)] 64 | /// enum MyCustomRectId { 65 | /// RectOne, 66 | /// RectTwo, 67 | /// RectThree, 68 | /// } 69 | /// 70 | /// // A target bin ID just needs to meet these trait bounds (ideally also Copy) 71 | /// // So you could use a u32, &str, or any other type that meets these 72 | /// // trat bounds. You do not have to use a custom enum. 73 | /// #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)] 74 | /// enum MyCustomBinId { 75 | /// DestinationBinOne, 76 | /// DestinationBinTwo, 77 | /// } 78 | /// 79 | /// // A placement group just needs to meet these trait bounds (ideally also Copy). 80 | /// // 81 | /// // Groups allow you to ensure that a set of rectangles will be placed 82 | /// // into the same bin. If this isn't possible an error is returned. 83 | /// // 84 | /// // Groups are optional. 85 | /// // 86 | /// // You could use an i32, &'static str, or any other type that meets these 87 | /// // trat bounds. You do not have to use a custom enum. 88 | /// #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)] 89 | /// enum MyCustomGroupId { 90 | /// GroupIdOne 91 | /// } 92 | /// 93 | /// let mut rects_to_place = GroupedRectsToPlace::new(); 94 | /// rects_to_place.push_rect( 95 | /// MyCustomRectId::RectOne, 96 | /// Some(vec![MyCustomGroupId::GroupIdOne]), 97 | /// RectToInsert::new(10, 20, 255) 98 | /// ); 99 | /// rects_to_place.push_rect( 100 | /// MyCustomRectId::RectTwo, 101 | /// Some(vec![MyCustomGroupId::GroupIdOne]), 102 | /// RectToInsert::new(5, 50, 255) 103 | /// ); 104 | /// rects_to_place.push_rect( 105 | /// MyCustomRectId::RectThree, 106 | /// None, 107 | /// RectToInsert::new(30, 30, 255) 108 | /// ); 109 | /// 110 | /// let mut target_bins = BTreeMap::new(); 111 | /// target_bins.insert(MyCustomBinId::DestinationBinOne, TargetBin::new(2048, 2048, 255)); 112 | /// target_bins.insert(MyCustomBinId::DestinationBinTwo, TargetBin::new(4096, 4096, 1020)); 113 | /// 114 | /// // Information about where each `MyCustomRectId` was placed 115 | /// let rectangle_placements = pack_rects( 116 | /// &rects_to_place, 117 | /// &mut target_bins, 118 | /// &volume_heuristic, 119 | /// &contains_smallest_box 120 | /// ).unwrap(); 121 | /// ``` 122 | /// 123 | /// ## Algorithm 124 | /// 125 | /// The algorithm was originally inspired by [rectpack2D] and then modified to work in 3D. 126 | /// 127 | /// [rectpack2D]: https://github.com/TeamHypersomnia/rectpack2D 128 | /// 129 | /// ## TODO: 130 | /// 131 | /// Optimize - plenty of room to remove clones and duplication .. etc 132 | pub fn pack_rects< 133 | RectToPlaceId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd, 134 | BinId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd, 135 | GroupId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd, 136 | >( 137 | rects_to_place: &GroupedRectsToPlace, 138 | target_bins: &mut BTreeMap, 139 | box_size_heuristic: &BoxSizeHeuristicFn, 140 | more_suitable_containers_fn: &ComparePotentialContainersFn, 141 | ) -> Result, RectanglePackError> { 142 | let mut packed_locations = KeyValMap::new(); 143 | 144 | let mut target_bins: Vec<(&BinId, &mut TargetBin)> = target_bins.iter_mut().collect(); 145 | sort_bins_smallest_to_largest(&mut target_bins, box_size_heuristic); 146 | 147 | let mut group_id_to_inbound_ids: Vec<(&Group, &Vec)> = 148 | rects_to_place.group_id_to_inbound_ids.iter().collect(); 149 | sort_groups_largest_to_smallest( 150 | &mut group_id_to_inbound_ids, 151 | rects_to_place, 152 | box_size_heuristic, 153 | ); 154 | 155 | 'group: for (_group_id, rects_to_place_ids) in group_id_to_inbound_ids { 156 | for (bin_id, bin) in target_bins.iter_mut() { 157 | if !can_fit_entire_group_into_bin( 158 | bin.clone(), 159 | &rects_to_place_ids[..], 160 | rects_to_place, 161 | box_size_heuristic, 162 | more_suitable_containers_fn, 163 | ) { 164 | continue; 165 | } 166 | 167 | 'incoming: for rect_to_place_id in rects_to_place_ids.iter() { 168 | if bin.available_bin_sections.len() == 0 { 169 | continue; 170 | } 171 | 172 | let _bin_clone = bin.clone(); 173 | 174 | let mut bin_sections = bin.available_bin_sections.clone(); 175 | 176 | let last_section_idx = bin_sections.len() - 1; 177 | let mut sections_tried = 0; 178 | 179 | 'section: while let Some(remaining_section) = bin_sections.pop() { 180 | let rect_to_place = rects_to_place.rects[&rect_to_place_id]; 181 | 182 | let placement = remaining_section.try_place( 183 | &rect_to_place, 184 | more_suitable_containers_fn, 185 | box_size_heuristic, 186 | ); 187 | 188 | if placement.is_err() { 189 | sections_tried += 1; 190 | continue 'section; 191 | } 192 | 193 | let (placement, mut new_sections) = placement.unwrap(); 194 | sort_by_size_largest_to_smallest(&mut new_sections, box_size_heuristic); 195 | 196 | bin.remove_filled_section(last_section_idx - sections_tried); 197 | bin.add_new_sections(new_sections); 198 | 199 | packed_locations.insert(rect_to_place_id.clone(), (bin_id.clone(), placement)); 200 | 201 | continue 'incoming; 202 | } 203 | } 204 | 205 | continue 'group; 206 | } 207 | return Err(RectanglePackError::NotEnoughBinSpace); 208 | } 209 | 210 | Ok(RectanglePackOk { packed_locations }) 211 | } 212 | 213 | // TODO: This is duplicative of the code above 214 | fn can_fit_entire_group_into_bin( 215 | mut bin: TargetBin, 216 | group: &[RectToPlaceId], 217 | rects_to_place: &GroupedRectsToPlace, 218 | 219 | box_size_heuristic: &BoxSizeHeuristicFn, 220 | more_suitable_containers_fn: &ComparePotentialContainersFn, 221 | ) -> bool 222 | where 223 | RectToPlaceId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd, 224 | GroupId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd, 225 | { 226 | 'incoming: for rect_to_place_id in group.iter() { 227 | if bin.available_bin_sections.len() == 0 { 228 | return false; 229 | } 230 | 231 | let mut bin_sections = bin.available_bin_sections.clone(); 232 | 233 | let last_section_idx = bin_sections.len() - 1; 234 | let mut sections_tried = 0; 235 | 236 | 'section: while let Some(remaining_section) = bin_sections.pop() { 237 | let rect_to_place = rects_to_place.rects[&rect_to_place_id]; 238 | 239 | let placement = remaining_section.try_place( 240 | &rect_to_place, 241 | more_suitable_containers_fn, 242 | box_size_heuristic, 243 | ); 244 | 245 | if placement.is_err() { 246 | sections_tried += 1; 247 | continue 'section; 248 | } 249 | 250 | let (_placement, mut new_sections) = placement.unwrap(); 251 | sort_by_size_largest_to_smallest(&mut new_sections, box_size_heuristic); 252 | 253 | bin.remove_filled_section(last_section_idx - sections_tried); 254 | bin.add_new_sections(new_sections); 255 | 256 | continue 'incoming; 257 | } 258 | 259 | return false; 260 | } 261 | 262 | true 263 | } 264 | 265 | /// Information about successfully packed rectangles. 266 | #[derive(Debug, PartialEq)] 267 | pub struct RectanglePackOk { 268 | packed_locations: KeyValMap, 269 | // TODO: Other information such as information about how the bins were packed 270 | // (perhaps percentage filled) 271 | } 272 | 273 | impl 274 | RectanglePackOk 275 | { 276 | /// Indicates where every incoming rectangle was placed 277 | pub fn packed_locations(&self) -> &KeyValMap { 278 | &self.packed_locations 279 | } 280 | } 281 | 282 | /// An error while attempting to pack rectangles into bins. 283 | #[derive(Debug, PartialEq)] 284 | pub enum RectanglePackError { 285 | /// The rectangles can't be placed into the bins. More bin space needs to be provided. 286 | NotEnoughBinSpace, 287 | } 288 | 289 | #[cfg(std)] 290 | impl std::error::Error for RectanglePackError {} 291 | 292 | impl Display for RectanglePackError { 293 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { 294 | match self { 295 | RectanglePackError::NotEnoughBinSpace => { 296 | f.write_str("Not enough space to place all of the rectangles.") 297 | } 298 | } 299 | } 300 | } 301 | 302 | fn sort_bins_smallest_to_largest( 303 | bins: &mut Vec<(&BinId, &mut TargetBin)>, 304 | box_size_heuristic: &BoxSizeHeuristicFn, 305 | ) where 306 | BinId: Debug + Hash + PartialEq + Eq + Clone, 307 | { 308 | bins.sort_by(|a, b| { 309 | box_size_heuristic(WidthHeightDepth { 310 | width: a.1.max_width, 311 | height: a.1.max_height, 312 | depth: a.1.max_depth, 313 | }) 314 | .cmp(&box_size_heuristic(WidthHeightDepth { 315 | width: b.1.max_width, 316 | height: b.1.max_height, 317 | depth: b.1.max_depth, 318 | })) 319 | }); 320 | } 321 | 322 | fn sort_by_size_largest_to_smallest( 323 | items: &mut [BinSection; 3], 324 | box_size_heuristic: &BoxSizeHeuristicFn, 325 | ) { 326 | items.sort_by(|a, b| box_size_heuristic(b.whd).cmp(&box_size_heuristic(a.whd))); 327 | } 328 | 329 | fn sort_groups_largest_to_smallest( 330 | group_id_to_inbound_ids: &mut Vec<(&Group, &Vec)>, 331 | incoming_groups: &GroupedRectsToPlace, 332 | box_size_heuristic: &BoxSizeHeuristicFn, 333 | ) where 334 | RectToPlaceId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd, 335 | GroupId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd, 336 | { 337 | group_id_to_inbound_ids.sort_by(|a, b| { 338 | let a_heuristic = 339 | a.1.iter() 340 | .map(|inbound| { 341 | let rect = incoming_groups.rects[inbound]; 342 | box_size_heuristic(rect.whd) 343 | }) 344 | .sum(); 345 | 346 | let b_heuristic: u128 = 347 | b.1.iter() 348 | .map(|inbound| { 349 | let rect = incoming_groups.rects[inbound]; 350 | box_size_heuristic(rect.whd) 351 | }) 352 | .sum(); 353 | 354 | b_heuristic.cmp(&a_heuristic) 355 | }); 356 | } 357 | 358 | #[cfg(test)] 359 | mod tests { 360 | use crate::{pack_rects, volume_heuristic, RectToInsert, RectanglePackError, TargetBin}; 361 | 362 | use super::*; 363 | use crate::packed_location::RotatedBy; 364 | 365 | /// If the provided rectangles can't fit into the provided bins. 366 | #[test] 367 | fn error_if_the_rectangles_cannot_fit_into_target_bins() { 368 | let mut targets = BTreeMap::new(); 369 | targets.insert(BinId::Three, TargetBin::new(2, 100, 1)); 370 | 371 | let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); 372 | groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(3, 1, 1)); 373 | 374 | match pack_rects( 375 | &groups, 376 | &mut targets, 377 | &volume_heuristic, 378 | &contains_smallest_box, 379 | ) 380 | .unwrap_err() 381 | { 382 | RectanglePackError::NotEnoughBinSpace => {} 383 | }; 384 | } 385 | 386 | /// Rectangles in the same group need to be placed in the same bin. 387 | /// 388 | /// Here we create two Rectangles in the same group and create two bins that could fit them 389 | /// individually but cannot fit them together. 390 | /// 391 | /// Then we verify that we receive an error for being unable to place the group. 392 | #[test] 393 | fn error_if_cannot_fit_group() { 394 | let mut targets = BTreeMap::new(); 395 | targets.insert(BinId::Three, TargetBin::new(100, 100, 1)); 396 | targets.insert(BinId::Four, TargetBin::new(100, 100, 1)); 397 | 398 | let mut groups = GroupedRectsToPlace::new(); 399 | groups.push_rect( 400 | RectToPlaceId::One, 401 | Some(vec!["A Group"]), 402 | RectToInsert::new(100, 100, 1), 403 | ); 404 | groups.push_rect( 405 | RectToPlaceId::Two, 406 | Some(vec!["A Group"]), 407 | RectToInsert::new(100, 100, 1), 408 | ); 409 | 410 | match pack_rects( 411 | &groups, 412 | &mut targets, 413 | &volume_heuristic, 414 | &contains_smallest_box, 415 | ) 416 | .unwrap_err() 417 | { 418 | RectanglePackError::NotEnoughBinSpace => {} 419 | }; 420 | } 421 | 422 | /// If we provide a single inbound rectangle and a single bin - it should be placed into that 423 | /// bin. 424 | #[test] 425 | fn one_inbound_rect_one_bin() { 426 | let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); 427 | groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(1, 2, 1)); 428 | 429 | let mut targets = BTreeMap::new(); 430 | targets.insert(BinId::Three, TargetBin::new(5, 5, 1)); 431 | 432 | let packed = pack_rects( 433 | &groups, 434 | &mut targets, 435 | &volume_heuristic, 436 | &contains_smallest_box, 437 | ) 438 | .unwrap(); 439 | let locations = packed.packed_locations; 440 | 441 | assert_eq!(locations.len(), 1); 442 | 443 | assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,); 444 | assert_eq!( 445 | locations[&RectToPlaceId::One].1, 446 | PackedLocation { 447 | x: 0, 448 | y: 0, 449 | z: 0, 450 | whd: WidthHeightDepth { 451 | width: 1, 452 | height: 2, 453 | depth: 1 454 | }, 455 | x_axis_rotation: RotatedBy::ZeroDegrees, 456 | y_axis_rotation: RotatedBy::ZeroDegrees, 457 | z_axis_rotation: RotatedBy::ZeroDegrees, 458 | } 459 | ) 460 | } 461 | 462 | /// If we have one inbound rect and two bins, it should be placed into the smallest bin. 463 | #[test] 464 | fn one_inbound_rect_two_bins() { 465 | let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); 466 | groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(2, 2, 1)); 467 | 468 | let mut targets = BTreeMap::new(); 469 | targets.insert(BinId::Three, TargetBin::new(5, 5, 1)); 470 | targets.insert(BinId::Four, TargetBin::new(5, 5, 2)); 471 | 472 | let packed = pack_rects( 473 | &groups, 474 | &mut targets, 475 | &volume_heuristic, 476 | &contains_smallest_box, 477 | ) 478 | .unwrap(); 479 | let locations = packed.packed_locations; 480 | 481 | assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,); 482 | 483 | assert_eq!(locations.len(), 1); 484 | assert_eq!( 485 | locations[&RectToPlaceId::One].1, 486 | PackedLocation { 487 | x: 0, 488 | y: 0, 489 | z: 0, 490 | whd: WidthHeightDepth { 491 | width: 2, 492 | height: 2, 493 | depth: 1 494 | }, 495 | x_axis_rotation: RotatedBy::ZeroDegrees, 496 | y_axis_rotation: RotatedBy::ZeroDegrees, 497 | z_axis_rotation: RotatedBy::ZeroDegrees, 498 | } 499 | ) 500 | } 501 | 502 | /// If we have two inbound rects the largest one should be placed first. 503 | #[test] 504 | fn places_largest_rectangles_first() { 505 | let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); 506 | groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(10, 10, 1)); 507 | groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(5, 5, 1)); 508 | 509 | let mut targets = BTreeMap::new(); 510 | targets.insert(BinId::Three, TargetBin::new(20, 20, 2)); 511 | 512 | let packed = pack_rects( 513 | &groups, 514 | &mut targets, 515 | &volume_heuristic, 516 | &contains_smallest_box, 517 | ) 518 | .unwrap(); 519 | let locations = packed.packed_locations; 520 | 521 | assert_eq!(locations.len(), 2); 522 | 523 | assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,); 524 | assert_eq!(locations[&RectToPlaceId::Two].0, BinId::Three,); 525 | 526 | assert_eq!( 527 | locations[&RectToPlaceId::One].1, 528 | PackedLocation { 529 | x: 0, 530 | y: 0, 531 | z: 0, 532 | whd: WidthHeightDepth { 533 | width: 10, 534 | height: 10, 535 | depth: 1 536 | }, 537 | x_axis_rotation: RotatedBy::ZeroDegrees, 538 | y_axis_rotation: RotatedBy::ZeroDegrees, 539 | z_axis_rotation: RotatedBy::ZeroDegrees, 540 | } 541 | ); 542 | assert_eq!( 543 | locations[&RectToPlaceId::Two].1, 544 | PackedLocation { 545 | x: 10, 546 | y: 0, 547 | z: 0, 548 | whd: WidthHeightDepth { 549 | width: 5, 550 | height: 5, 551 | depth: 1 552 | }, 553 | x_axis_rotation: RotatedBy::ZeroDegrees, 554 | y_axis_rotation: RotatedBy::ZeroDegrees, 555 | z_axis_rotation: RotatedBy::ZeroDegrees, 556 | } 557 | ) 558 | } 559 | 560 | /// We have two rectangles and two bins. Each bin has enough space to fit one rectangle. 561 | /// 562 | /// 1. First place the largest rectangle into the smallest bin. 563 | /// 564 | /// 2. Second place the remaining rectangle into the next available bin (i.e. the largest one). 565 | #[test] 566 | fn two_rects_two_bins() { 567 | let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); 568 | groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(15, 15, 1)); 569 | groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(20, 20, 1)); 570 | 571 | let mut targets = BTreeMap::new(); 572 | targets.insert(BinId::Three, TargetBin::new(20, 20, 1)); 573 | targets.insert(BinId::Four, TargetBin::new(50, 50, 1)); 574 | 575 | let packed = pack_rects( 576 | &groups, 577 | &mut targets, 578 | &volume_heuristic, 579 | &contains_smallest_box, 580 | ) 581 | .unwrap(); 582 | let locations = packed.packed_locations; 583 | 584 | assert_eq!(locations.len(), 2); 585 | 586 | assert_eq!(locations[&RectToPlaceId::One].0, BinId::Four,); 587 | assert_eq!(locations[&RectToPlaceId::Two].0, BinId::Three,); 588 | 589 | assert_eq!( 590 | locations[&RectToPlaceId::One].1, 591 | PackedLocation { 592 | x: 0, 593 | y: 0, 594 | z: 0, 595 | whd: WidthHeightDepth { 596 | width: 15, 597 | height: 15, 598 | depth: 1 599 | }, 600 | x_axis_rotation: RotatedBy::ZeroDegrees, 601 | y_axis_rotation: RotatedBy::ZeroDegrees, 602 | z_axis_rotation: RotatedBy::ZeroDegrees, 603 | } 604 | ); 605 | assert_eq!( 606 | locations[&RectToPlaceId::Two].1, 607 | PackedLocation { 608 | x: 0, 609 | y: 0, 610 | z: 0, 611 | whd: WidthHeightDepth { 612 | width: 20, 613 | height: 20, 614 | depth: 1 615 | }, 616 | x_axis_rotation: RotatedBy::ZeroDegrees, 617 | y_axis_rotation: RotatedBy::ZeroDegrees, 618 | z_axis_rotation: RotatedBy::ZeroDegrees, 619 | } 620 | ) 621 | } 622 | 623 | /// If there are two sections available to fill - the smaller one should be filled first 624 | /// (if possible). 625 | /// 626 | /// We test this by creating two incoming rectangles. 627 | /// 628 | /// The largest one is placed and creates two new sections - after which the second, smaller one 629 | /// should get placed into the smaller of the two new sections. 630 | /// 631 | /// ```text 632 | /// ┌──────────────┬──▲───────────────┐ 633 | /// │ Second Rect │ │ │ 634 | /// ├──────────────┴──┤ │ 635 | /// │ │ │ 636 | /// │ First Placed │ │ 637 | /// │ Rectangle │ │ 638 | /// │ │ │ 639 | /// └─────────────────┴───────────────┘ 640 | /// ``` 641 | #[test] 642 | fn fills_small_sections_before_large_ones() { 643 | let mut targets = BTreeMap::new(); 644 | targets.insert(BinId::Three, TargetBin::new(100, 100, 1)); 645 | 646 | let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); 647 | 648 | groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(50, 90, 1)); 649 | groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(1, 1, 1)); 650 | 651 | let packed = pack_rects( 652 | &groups, 653 | &mut targets, 654 | &volume_heuristic, 655 | &contains_smallest_box, 656 | ) 657 | .unwrap(); 658 | let locations = packed.packed_locations; 659 | 660 | assert_eq!(locations.len(), 2); 661 | 662 | assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,); 663 | assert_eq!(locations[&RectToPlaceId::Two].0, BinId::Three,); 664 | 665 | assert_eq!( 666 | locations[&RectToPlaceId::One].1, 667 | PackedLocation { 668 | x: 0, 669 | y: 0, 670 | z: 0, 671 | whd: WidthHeightDepth { 672 | width: 50, 673 | height: 90, 674 | depth: 1 675 | }, 676 | x_axis_rotation: RotatedBy::ZeroDegrees, 677 | y_axis_rotation: RotatedBy::ZeroDegrees, 678 | z_axis_rotation: RotatedBy::ZeroDegrees, 679 | } 680 | ); 681 | assert_eq!( 682 | locations[&RectToPlaceId::Two].1, 683 | PackedLocation { 684 | x: 0, 685 | y: 90, 686 | z: 0, 687 | whd: WidthHeightDepth { 688 | width: 1, 689 | height: 1, 690 | depth: 1 691 | }, 692 | x_axis_rotation: RotatedBy::ZeroDegrees, 693 | y_axis_rotation: RotatedBy::ZeroDegrees, 694 | z_axis_rotation: RotatedBy::ZeroDegrees, 695 | } 696 | ); 697 | } 698 | 699 | /// Say we have one bin and three rectangles to place within in. 700 | /// 701 | /// The first one gets placed and creates two new splits. 702 | /// 703 | /// We then attempt to place the second one into the smallest split. It's too big to fit, so 704 | /// we place it into the largest split. 705 | /// 706 | /// After that we place the third rectangle into the smallest split. 707 | /// 708 | /// Here we verify that that actually occurs and that we didn't throw away that smallest split 709 | /// when the second one couldn't fit in it. 710 | /// 711 | /// ```text 712 | /// ┌──────────────┬──────────────┐ 713 | /// │ Third │ │ 714 | /// ├──────────────┤ │ 715 | /// │ │ │ 716 | /// │ │ │ 717 | /// │ ├──────────────┤ 718 | /// │ First │ │ 719 | /// │ │ Second │ 720 | /// │ │ │ 721 | /// └──────────────┴──────────────┘ 722 | /// ``` 723 | #[test] 724 | fn saves_bin_sections_for_future_use() { 725 | let mut targets = BTreeMap::new(); 726 | targets.insert(BinId::Three, TargetBin::new(100, 100, 1)); 727 | 728 | let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new(); 729 | 730 | groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(60, 95, 1)); 731 | groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(40, 10, 1)); 732 | groups.push_rect(RectToPlaceId::Three, None, RectToInsert::new(60, 3, 1)); 733 | 734 | let packed = pack_rects( 735 | &groups, 736 | &mut targets, 737 | &volume_heuristic, 738 | &contains_smallest_box, 739 | ) 740 | .unwrap(); 741 | let locations = packed.packed_locations; 742 | 743 | assert_eq!( 744 | locations[&RectToPlaceId::One].1, 745 | PackedLocation { 746 | x: 0, 747 | y: 0, 748 | z: 0, 749 | whd: WidthHeightDepth { 750 | width: 60, 751 | height: 95, 752 | depth: 1 753 | }, 754 | x_axis_rotation: RotatedBy::ZeroDegrees, 755 | y_axis_rotation: RotatedBy::ZeroDegrees, 756 | z_axis_rotation: RotatedBy::ZeroDegrees, 757 | } 758 | ); 759 | assert_eq!( 760 | locations[&RectToPlaceId::Two].1, 761 | PackedLocation { 762 | x: 60, 763 | y: 0, 764 | z: 0, 765 | whd: WidthHeightDepth { 766 | width: 40, 767 | height: 10, 768 | depth: 1 769 | }, 770 | x_axis_rotation: RotatedBy::ZeroDegrees, 771 | y_axis_rotation: RotatedBy::ZeroDegrees, 772 | z_axis_rotation: RotatedBy::ZeroDegrees, 773 | } 774 | ); 775 | assert_eq!( 776 | locations[&RectToPlaceId::Three].1, 777 | PackedLocation { 778 | x: 0, 779 | y: 95, 780 | z: 0, 781 | whd: WidthHeightDepth { 782 | width: 60, 783 | height: 3, 784 | depth: 1 785 | }, 786 | x_axis_rotation: RotatedBy::ZeroDegrees, 787 | y_axis_rotation: RotatedBy::ZeroDegrees, 788 | z_axis_rotation: RotatedBy::ZeroDegrees, 789 | } 790 | ); 791 | } 792 | 793 | /// Create a handful of rectangles that need to be placed, with two of them in the same group 794 | /// and the rest ungrouped. 795 | /// Try placing them many times and verify that each time they are placed the exact same way. 796 | #[test] 797 | fn deterministic_packing() { 798 | let mut previous_packed = None; 799 | 800 | for _ in 0..5 { 801 | let mut rects_to_place: GroupedRectsToPlace<&'static str, &str> = 802 | GroupedRectsToPlace::new(); 803 | 804 | let mut target_bins = BTreeMap::new(); 805 | for bin_id in 0..5 { 806 | target_bins.insert(bin_id, TargetBin::new(8, 8, 1)); 807 | } 808 | 809 | let rectangles = vec![ 810 | "some-rectangle-0", 811 | "some-rectangle-1", 812 | "some-rectangle-2", 813 | "some-rectangle-3", 814 | "some-rectangle-4", 815 | ]; 816 | 817 | for rect_id in rectangles.iter() { 818 | rects_to_place.push_rect(rect_id, None, RectToInsert::new(4, 4, 1)); 819 | } 820 | 821 | let packed = pack_rects( 822 | &rects_to_place, 823 | &mut target_bins.clone(), 824 | &volume_heuristic, 825 | &contains_smallest_box, 826 | ) 827 | .unwrap(); 828 | 829 | if let Some(previous_packed) = previous_packed.as_ref() { 830 | assert_eq!(&packed, previous_packed); 831 | } 832 | 833 | previous_packed = Some(packed); 834 | } 835 | } 836 | 837 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] 838 | enum RectToPlaceId { 839 | One, 840 | Two, 841 | Three, 842 | } 843 | 844 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] 845 | enum BinId { 846 | Three, 847 | Four, 848 | } 849 | } 850 | -------------------------------------------------------------------------------- /src/packed_location.rs: -------------------------------------------------------------------------------- 1 | use crate::width_height_depth::WidthHeightDepth; 2 | 3 | /// Describes how and where an incoming rectangle was packed into the target bins 4 | #[derive(Debug, PartialEq, Copy, Clone)] 5 | pub struct PackedLocation { 6 | pub(crate) x: u32, 7 | pub(crate) y: u32, 8 | pub(crate) z: u32, 9 | pub(crate) whd: WidthHeightDepth, 10 | pub(crate) x_axis_rotation: RotatedBy, 11 | pub(crate) y_axis_rotation: RotatedBy, 12 | pub(crate) z_axis_rotation: RotatedBy, 13 | } 14 | 15 | #[derive(Debug, PartialEq, Copy, Clone)] 16 | #[allow(unused)] // TODO: Implement rotations 17 | pub enum RotatedBy { 18 | ZeroDegrees, 19 | NinetyDegrees, 20 | } 21 | 22 | #[allow(missing_docs)] 23 | impl PackedLocation { 24 | pub fn x(&self) -> u32 { 25 | self.x 26 | } 27 | 28 | pub fn y(&self) -> u32 { 29 | self.y 30 | } 31 | 32 | pub fn z(&self) -> u32 { 33 | self.z 34 | } 35 | 36 | pub fn width(&self) -> u32 { 37 | self.whd.width 38 | } 39 | 40 | pub fn height(&self) -> u32 { 41 | self.whd.height 42 | } 43 | 44 | pub fn depth(&self) -> u32 { 45 | self.whd.depth 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/rect_to_insert.rs: -------------------------------------------------------------------------------- 1 | use crate::width_height_depth::WidthHeightDepth; 2 | 3 | /// A rectangle that we want to insert into a target bin 4 | #[derive(Debug, Copy, Clone, PartialEq)] 5 | pub struct RectToInsert { 6 | pub(crate) whd: WidthHeightDepth, 7 | allow_global_x_axis_rotation: bool, 8 | allow_global_y_axis_rotation: bool, 9 | allow_global_z_axis_rotation: bool, 10 | } 11 | 12 | impl Into for RectToInsert { 13 | fn into(self) -> WidthHeightDepth { 14 | WidthHeightDepth { 15 | width: self.width(), 16 | height: self.height(), 17 | depth: self.depth(), 18 | } 19 | } 20 | } 21 | 22 | #[allow(missing_docs)] 23 | impl RectToInsert { 24 | pub fn new(width: u32, height: u32, depth: u32) -> Self { 25 | RectToInsert { 26 | whd: WidthHeightDepth { 27 | width, 28 | height, 29 | depth, 30 | }, 31 | // Rotation is not yet supported 32 | allow_global_x_axis_rotation: false, 33 | allow_global_y_axis_rotation: false, 34 | allow_global_z_axis_rotation: false, 35 | } 36 | } 37 | } 38 | 39 | #[allow(missing_docs)] 40 | impl RectToInsert { 41 | pub fn width(&self) -> u32 { 42 | self.whd.width 43 | } 44 | 45 | pub fn height(&self) -> u32 { 46 | self.whd.height 47 | } 48 | 49 | pub fn depth(&self) -> u32 { 50 | self.whd.depth 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/target_bin.rs: -------------------------------------------------------------------------------- 1 | use crate::bin_section::BinSection; 2 | use crate::width_height_depth::WidthHeightDepth; 3 | use alloc::vec::Vec; 4 | 5 | mod coalesce; 6 | mod push_available_bin_section; 7 | 8 | /// A bin that we'd like to play our incoming rectangles into 9 | #[derive(Debug, Clone)] 10 | pub struct TargetBin { 11 | pub(crate) max_width: u32, 12 | pub(crate) max_height: u32, 13 | pub(crate) max_depth: u32, 14 | pub(crate) available_bin_sections: Vec, 15 | } 16 | 17 | impl TargetBin { 18 | #[allow(missing_docs)] 19 | pub fn new(max_width: u32, max_height: u32, max_depth: u32) -> Self { 20 | let available_bin_sections = vec![BinSection::new( 21 | 0, 22 | 0, 23 | 0, 24 | WidthHeightDepth { 25 | width: max_width, 26 | height: max_height, 27 | depth: max_depth, 28 | }, 29 | )]; 30 | 31 | TargetBin { 32 | max_width, 33 | max_height, 34 | max_depth, 35 | available_bin_sections, 36 | } 37 | } 38 | 39 | /// The free [`BinSection`]s within the [`TargetBin`] that rectangles can still be placed into. 40 | pub fn available_bin_sections(&self) -> &Vec { 41 | &self.available_bin_sections 42 | } 43 | 44 | /// Remove the section that was just split by a placed rectangle. 45 | pub fn remove_filled_section(&mut self, idx: usize) { 46 | self.available_bin_sections.remove(idx); 47 | } 48 | 49 | /// When a section is filled it gets split into three new sections. 50 | /// Here we add those. 51 | /// 52 | /// TODO: Ignore sections with a volume of 0 53 | pub fn add_new_sections(&mut self, new_sections: [BinSection; 3]) { 54 | for new_section in new_sections.iter() { 55 | if new_section.whd.volume() > 0 { 56 | self.available_bin_sections.push(*new_section); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/target_bin/coalesce.rs: -------------------------------------------------------------------------------- 1 | use crate::TargetBin; 2 | 3 | use core::ops::Range; 4 | 5 | impl TargetBin { 6 | /// Over time as you use [`TargetBin.push_available_bin_section`] to return remove packed 7 | /// rectangles from the [`TargetBin`], you may end up with neighboring bin sections that can 8 | /// be combined into a larger bin section. 9 | /// 10 | /// Combining bin sections in this was is desirable because a larger bin section allows you to 11 | /// place larger rectangles that might not fit into the smaller bin sections. 12 | /// 13 | /// In order to coalesce, or combine a bin section with other bin sections, we need to check 14 | /// every other available bin section to see if they are neighbors. 15 | /// 16 | /// This means that fully coalescing the entire list of available bin sections is O(n^2) time 17 | /// complexity, where n is the number of available empty sections. 18 | /// 19 | /// # Basic Usage 20 | /// 21 | /// ```ignore 22 | /// # use rectangle_pack::TargetBin; 23 | /// let target_bin = my_target_bin(); 24 | /// 25 | /// for idx in 0..target_bin.available_bin_sections().len() { 26 | /// let len = target_bin.available_bin_sections().len(); 27 | /// target_bin.coalesce_available_sections(idx, 0..len); 28 | /// } 29 | /// 30 | /// # fn my_target_bin () -> TargetBin { 31 | /// # TargetBin::new(1, 2, 3) 32 | /// # } 33 | /// ``` 34 | /// 35 | /// # Distributing the Workload 36 | /// 37 | /// It is possible that you are developing an application that can in some cases have a lot of 38 | /// heavily fragmented bins that need to be coalesced. If your application has a tight 39 | /// performance budget, such as a real time simulation, you may not want to do all of your 40 | /// coalescing at once. 41 | /// 42 | /// This method allows you to split the work over many frames by giving you fine grained control 43 | /// over which bin sections is getting coalesced and which other bin sections it gets tested 44 | /// against. 45 | /// 46 | /// So, for example, say you have an application where you want to fully coalesce the entire 47 | /// bin every ten seconds, and you are running at 60 frames per second. You would then 48 | /// distribute the coalescing work such that it would take 600 calls to compare every bin 49 | /// section. 50 | /// 51 | /// Here's a basic eample of splitting the work. 52 | /// 53 | /// ```ignore 54 | /// # use rectangle_pack::TargetBin; 55 | /// let target_bin = my_target_bin(); 56 | /// 57 | /// let current_frame: usize = get_current_frame() % 600; 58 | /// 59 | /// for idx in 0..target_bin.available_bin_sections().len() { 60 | /// let len = target_bin.available_bin_sections().len(); 61 | /// 62 | /// let start = len / 600 * current_frame; 63 | /// let end = start + len / 600; 64 | /// 65 | /// target_bin.coalesce_available_sections(idx, start..end); 66 | /// } 67 | /// 68 | /// # fn my_target_bin () -> TargetBin { 69 | /// # TargetBin::new(1, 2, 3) 70 | /// # } 71 | /// # 72 | /// # fn get_current_frame () -> usize { 73 | /// # 0 74 | /// # } 75 | /// ``` 76 | /// 77 | /// [`TargetBin.push_available_bin_section`]: #method.push_available_bin_section 78 | // TODO: Write tests, implement then remove the "ignore" from the examples above. 79 | // Tests cases should have a rectangle and then a neighbor (above, below, left, right) and 80 | // verify that they get combined, but only if the comparison indices are correct and only if 81 | // the neighbor has the same width (uf above/below) or height (if left/right). 82 | pub fn coalesce_available_sections( 83 | _bin_section_index: usize, 84 | _compare_to_indices: Range, 85 | ) { 86 | unimplemented!() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/target_bin/push_available_bin_section.rs: -------------------------------------------------------------------------------- 1 | //! Methods for adding a BinSection back into a TargetBin. 2 | //! 3 | //! Useful in an application that needs to be able to remove packed rectangles from bins. 4 | //! After which the [`TargetBin.coalesce`] method can be used to combine smaller adjacent sections 5 | //! into larger sections. 6 | 7 | #![allow(missing_docs)] 8 | 9 | use crate::bin_section::BinSection; 10 | use crate::TargetBin; 11 | use core::fmt::{Display, Formatter, Result as FmtResult}; 12 | 13 | impl TargetBin { 14 | /// Push a [`BinSection`] to the list of remaining [`BinSection`]'s that rectangles can be 15 | /// placed in. 16 | /// 17 | /// ## Performance 18 | /// 19 | /// This checks that your [`BinSection`] does not overlap any other bin sections. In many 20 | /// cases this will be negligible, however it is important to note that this has a worst case 21 | /// time complexity of `O(Width * Height * Depth)`, where the worst case is tht you have a bin 22 | /// full of `1x1x1` rectangles. 23 | /// 24 | /// To skip the validity checks use [`TargetBin.push_available_bin_section_unchecked`]. 25 | /// 26 | /// [`TargetBin.push_available_bin_section_unchecked`]: #method.push_available_bin_section_unchecked 27 | pub fn push_available_bin_section( 28 | &mut self, 29 | bin_section: BinSection, 30 | ) -> Result<(), PushBinSectionError> { 31 | if bin_section.x >= self.max_width 32 | || bin_section.y >= self.max_height 33 | || bin_section.z >= self.max_depth 34 | { 35 | return Err(PushBinSectionError::OutOfBounds(bin_section)); 36 | } 37 | 38 | for available in self.available_bin_sections.iter() { 39 | if available.overlaps(&bin_section) { 40 | return Err(PushBinSectionError::Overlaps { 41 | remaining_section: *available, 42 | new_section: bin_section, 43 | }); 44 | } 45 | } 46 | 47 | self.push_available_bin_section_unchecked(bin_section); 48 | 49 | Ok(()) 50 | } 51 | 52 | /// Push a [`BinSection`] to the list of remaining [`BinSection`]'s that rectangles can be 53 | /// placed in, without checking whether or not it is valid. 54 | /// 55 | /// Use [`TargetBin.push_available_bin_section`] if you want to check that the new bin section 56 | /// does not overlap any existing bin sections nad that it is within the [`TargetBin`]'s bounds. 57 | /// 58 | /// [`TargetBin.push_available_bin_section`]: #method.push_available_bin_section 59 | pub fn push_available_bin_section_unchecked(&mut self, bin_section: BinSection) { 60 | self.available_bin_sections.push(bin_section); 61 | } 62 | } 63 | 64 | /// An error while attempting to push a [`BinSection`] into the remaining bin sections of a 65 | /// [`TargetBin`]. 66 | #[derive(Debug)] 67 | pub enum PushBinSectionError { 68 | /// Attempted to push a [`BinSection`] that is not fully contained by the bin. 69 | OutOfBounds(BinSection), 70 | /// Attempted to push a [`BinSection`] that overlaps another empty bin section. 71 | Overlaps { 72 | /// The section that is already stored as empty within the [`TargetBin`]; 73 | remaining_section: BinSection, 74 | /// The section that you were trying to add to the [`TargetBin`]; 75 | new_section: BinSection, 76 | }, 77 | } 78 | 79 | impl Display for PushBinSectionError { 80 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 81 | match self { 82 | PushBinSectionError::OutOfBounds(oob) => { 83 | f.debug_tuple("BinSection").field(oob).finish() 84 | } 85 | PushBinSectionError::Overlaps { 86 | remaining_section, 87 | new_section, 88 | } => f 89 | .debug_struct("Overlaps") 90 | .field("remaining_section", remaining_section) 91 | .field("new_section", new_section) 92 | .finish(), 93 | } 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | use crate::width_height_depth::WidthHeightDepth; 101 | 102 | /// Verify that if the bin section that we are pushing is outside of the TargetBin's bounds we 103 | /// return an error. 104 | #[test] 105 | fn error_if_bin_section_out_of_bounds() { 106 | let mut bin = empty_bin(); 107 | 108 | let out_of_bounds = BinSection::new(101, 0, 0, WidthHeightDepth::new(1, 1, 1)); 109 | 110 | match bin.push_available_bin_section(out_of_bounds).err().unwrap() { 111 | PushBinSectionError::OutOfBounds(err_bin_section) => { 112 | assert_eq!(err_bin_section, out_of_bounds) 113 | } 114 | _ => panic!(), 115 | }; 116 | } 117 | 118 | /// Verify that if the bin section that we are pushing overlaps another bin section we return 119 | /// an error. 120 | #[test] 121 | fn error_if_bin_section_overlaps_another_remaining_section() { 122 | let mut bin = empty_bin(); 123 | 124 | let overlaps = BinSection::new(0, 0, 0, WidthHeightDepth::new(1, 1, 1)); 125 | 126 | match bin.push_available_bin_section(overlaps).err().unwrap() { 127 | PushBinSectionError::Overlaps { 128 | remaining_section: err_remaining_section, 129 | new_section: err_new_section, 130 | } => { 131 | assert_eq!(err_new_section, overlaps); 132 | assert_eq!( 133 | err_remaining_section, 134 | BinSection::new(0, 0, 0, WidthHeightDepth::new(100, 100, 1)) 135 | ); 136 | } 137 | _ => panic!(), 138 | } 139 | } 140 | 141 | /// Verify that we can push a valid bin section. 142 | #[test] 143 | fn push_bin_section() { 144 | let mut bin = full_bin(); 145 | 146 | let valid_section = BinSection::new(1, 2, 0, WidthHeightDepth::new(1, 1, 1)); 147 | 148 | assert_eq!(bin.available_bin_sections.len(), 0); 149 | bin.push_available_bin_section(valid_section).unwrap(); 150 | assert_eq!(bin.available_bin_sections.len(), 1); 151 | 152 | assert_eq!(bin.available_bin_sections[0], valid_section); 153 | } 154 | 155 | fn empty_bin() -> TargetBin { 156 | TargetBin::new(100, 100, 1) 157 | } 158 | 159 | fn full_bin() -> TargetBin { 160 | let mut bin = TargetBin::new(100, 100, 1); 161 | 162 | bin.available_bin_sections.clear(); 163 | 164 | bin 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/width_height_depth.rs: -------------------------------------------------------------------------------- 1 | /// Used to represent a volume (or area of the depth is 1) 2 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Ord, PartialOrd)] 3 | #[allow(missing_docs)] 4 | pub struct WidthHeightDepth { 5 | pub(crate) width: u32, 6 | pub(crate) height: u32, 7 | pub(crate) depth: u32, 8 | } 9 | 10 | #[allow(missing_docs)] 11 | impl WidthHeightDepth { 12 | /// # Panics 13 | /// 14 | /// Panics if width, height or depth is 0. 15 | pub fn new(width: u32, height: u32, depth: u32) -> Self { 16 | assert_ne!(width, 0); 17 | assert_ne!(height, 0); 18 | assert_ne!(depth, 0); 19 | 20 | WidthHeightDepth { 21 | width, 22 | height, 23 | depth, 24 | } 25 | } 26 | 27 | pub fn volume(&self) -> u128 { 28 | self.width as u128 * self.height as u128 * self.depth as u128 29 | } 30 | } 31 | --------------------------------------------------------------------------------