├── .gitattributes ├── .github └── workflows │ └── cmake-multi-platform.yml ├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── example ├── CMakeLists.txt └── main.cpp ├── images ├── atlas_big.png ├── atlas_big_color.png ├── atlas_small.png ├── atlas_small_color.png ├── atlas_tiny.png ├── atlas_tiny_color.png └── diag01.png ├── src ├── CMakeLists.txt └── rectpack2D │ ├── best_bin_finder.h │ ├── empty_space_allocators.h │ ├── empty_spaces.h │ ├── finders_interface.h │ ├── insert_and_split.h │ └── rect_structs.h └── thoughts.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.github/workflows/cmake-multi-platform.yml: -------------------------------------------------------------------------------- 1 | name: CMake on multiple platforms 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | fail-fast: false 15 | 16 | matrix: 17 | os: [ubuntu-latest, windows-latest] 18 | build_type: [Debug, Release] 19 | c_compiler: [gcc, clang, cl] 20 | include: 21 | - os: windows-latest 22 | c_compiler: cl 23 | cpp_compiler: cl 24 | - os: ubuntu-latest 25 | c_compiler: gcc 26 | cpp_compiler: g++ 27 | - os: ubuntu-latest 28 | c_compiler: clang 29 | cpp_compiler: clang++ 30 | exclude: 31 | - os: windows-latest 32 | c_compiler: gcc 33 | - os: windows-latest 34 | c_compiler: clang 35 | - os: ubuntu-latest 36 | c_compiler: cl 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Set reusable strings 42 | # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. 43 | id: strings 44 | shell: bash 45 | run: | 46 | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" 47 | 48 | - name: Configure CMake 49 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 50 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 51 | run: > 52 | cmake -B ${{ steps.strings.outputs.build-output-dir }} 53 | -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} 54 | -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} 55 | -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} 56 | -DRECTPACK2D_BUILD_EXAMPLE=1 57 | -S ${{ github.workspace }} 58 | 59 | - name: Build 60 | # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). 61 | run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} 62 | 63 | # - name: Test 64 | # working-directory: ${{ steps.strings.outputs.build-output-dir }} 65 | # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). 66 | # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail 67 | # run: ctest --build-config ${{ matrix.build_type }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cmake-build-debug/ 2 | .DS_Store 3 | .idea 4 | *.iml 5 | example/clang_commands.sh 6 | example/main 7 | example/main.cpp.o 8 | example/run.sh 9 | build 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | language: cpp 3 | sudo: required 4 | dist: trusty 5 | addons: 6 | apt: 7 | packages: 8 | - gdb 9 | - apport 10 | - gcc-7 11 | - g++-7 12 | - cmake 13 | - ninja-build 14 | sources: 15 | - ubuntu-toolchain-r-test 16 | 17 | matrix: 18 | include: 19 | - env: c_compiler=gcc-7 cxx_compiler=g++-7 config=Debug 20 | compiler: gcc 21 | - env: c_compiler=gcc-7 cxx_compiler=g++-7 config=Release 22 | compiler: gcc 23 | 24 | before_install: 25 | - ulimit -c 26 | - ulimit -a -S 27 | - ulimit -a -H 28 | - cat /proc/sys/kernel/core_pattern 29 | - cat /etc/default/apport 30 | - service --status-all || true 31 | - initctl list || true 32 | 33 | script: 34 | - ${c_compiler} -v && ${cxx_compiler} -v && cmake --version && ninja --version 35 | - export CC=${c_compiler} 36 | - export CXX=${cxx_compiler} 37 | - cmake/build_example.sh ${config} ${c_compiler} ${cxx_compiler} 38 | - ulimit -c unlimited -S 39 | - pushd build/current 40 | - ninja run -k 0 41 | - ls -l 42 | - popd 43 | - ls -l example 44 | - cmake/print_bt_if_core_found.sh example/core 45 | 46 | notfications: 47 | email: false 48 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20.0) 2 | 3 | project(rectpack2D) 4 | 5 | add_subdirectory(src) 6 | add_library(rectpack2D::rectpack2D ALIAS rectpack2D) 7 | 8 | option(RECTPACK2D_BUILD_EXAMPLE "Build example for rectpack2D" OFF) 9 | 10 | if(RECTPACK2D_BUILD_EXAMPLE) 11 | add_subdirectory(example) 12 | endif() -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 20 6 | }, 7 | "configurePresets": [ 8 | { 9 | "name": "linux_binary_env", 10 | "hidden": true, 11 | "binaryDir": "${sourceDir}/build/${presetName}", 12 | "generator": "Ninja Multi-Config", 13 | "cacheVariables" : { 14 | "RECTPACK2D_BUILD_EXAMPLE" : true 15 | } 16 | }, 17 | { 18 | "name": "windows_binary_env", 19 | "hidden": true, 20 | "binaryDir": "${sourceDir}/build/${presetName}", 21 | "generator": "Visual Studio", 22 | "condition": { 23 | "type": "equals", 24 | "lhs": "${hostSystemName}", 25 | "rhs": "Windows" 26 | }, 27 | "cacheVariables" : { 28 | "RECTPACK2D_BUILD_EXAMPLE" : true 29 | } 30 | }, 31 | { 32 | "inherits": [ "linux_binary_env" ], 33 | "name" : "linux" 34 | }, 35 | { 36 | "inherits": [ "windows_binary_env" ], 37 | "name" : "windows" 38 | } 39 | ], 40 | "buildPresets": [ 41 | { 42 | "name": "linux-debug", 43 | "configurePreset": "linux", 44 | "configuration": "Debug" 45 | }, 46 | { 47 | "name": "linux-release", 48 | "configurePreset": "linux", 49 | "configuration": "Release" 50 | }, 51 | { 52 | "name": "windows-debug", 53 | "configurePreset": "windows", 54 | "configuration": "Debug" 55 | }, 56 | { 57 | "name": "windows-release", 58 | "configurePreset": "windows", 59 | "configuration": "Release" 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Patryk Czachurski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | # rectpack2D 5 | 6 | [![Linux & Windows Build](https://github.com/TeamHypersomnia/rectpack2D/actions/workflows/cmake-multi-platform.yml/badge.svg)](https://github.com/TeamHypersomnia/rectpack2D/actions/workflows/cmake-multi-platform.yml) 7 | 8 | **Used in [Assassin's Creed: Valhalla](https://www.youtube.com/watch?v=2KnjDL4DnwM&t=2382s)!** 9 | 10 | **Used by [Skydio](https://pages.skydio.com/rs/784-TUF-591/images/Open%20Source%20Software%20Notice%20v0.2.html), one of the top drone manufacturers!** 11 | 12 | **[2 scientific references](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C5&q=teamhypersomnia&btnG=)!** 13 | 14 | 15 |
16 | 17 | A header-only 2D rectangle packing library written in modern C++. 18 | This is a refactored and **highly optimized** version of the [original library](https://github.com/TeamHypersomnia/rectpack2D/tree/legacy). 19 | 20 | It was originally developed for the needs of [Hypersomnia](https://github.com/TeamHypersomnia/Hypersomnia), a free and open-source multiplayer shooter. 21 | 22 | ![7](https://user-images.githubusercontent.com/3588717/42707552-d8b1c65e-86da-11e8-9412-54c580bd2696.jpg) 23 | 24 | ## Table of contents 25 | 26 | - [Benchmarks](#benchmarks) 27 | - [Usage](#usage) 28 | - [Building the example](#building-the-example) 29 | * [Windows](#windows) 30 | * [Linux](#linux) 31 | - [Algorithm](#algorithm) 32 | * [Insertion algorithm](#insertion-algorithm) 33 | * [Additional heuristics](#additional-heuristics) 34 | 35 | ## Benchmarks 36 | 37 | Tests were conducted on a ``Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz``. 38 | The binary was built with ``clang 6.0.0``, using an -03 switch. 39 | 40 | ### Arbitrary game sprites: 582 subjects. 41 | 42 | **Runtime: 0.8 ms** 43 | **Wasted pixels: 10982 (0.24% - equivalent of a 105 x 105 square)** 44 | 45 | Output (1896 x 2382): 46 | 47 | ![1](images/atlas_small.png) 48 | 49 | In color: 50 | (black is wasted space) 51 | 52 | ![2](images/atlas_small_color.png) 53 | 54 | ### Arbitrary game sprites + Japanese glyphs: 3264 subjects. 55 | 56 | **Runtime: 4 ms** 57 | **Wasted pixels: 15538 (0.31% - equivalent of a 125 x 125 square)** 58 | 59 | Output (2116 x 2382): 60 | 61 | ![3](images/atlas_big.png) 62 | 63 | In color: 64 | (black is wasted space) 65 | 66 | ![4](images/atlas_big_color.png) 67 | 68 | 69 | ### Japanese glyphs + some GUI sprites: 3122 subjects. 70 | 71 | **Runtime: 3.5 - 7 ms** 72 | **Wasted pixels: 9288 (1.23% - equivalent of a 96 x 96 square)** 73 | 74 | Output (866 x 871): 75 | 76 | ![5](images/atlas_tiny.png) 77 | 78 | In color: 79 | (black is wasted space) 80 | 81 | ![6](images/atlas_tiny_color.png) 82 | 83 | ## Usage 84 | 85 | This is a header-only library. 86 | Just include the ``src/finders_interface.h`` and you should be good to go. 87 | 88 | For an example use, see ``example/main.cpp``. 89 | 90 | ## Building the example 91 | 92 | ### Windows 93 | 94 | From the repository's folder, run: 95 | 96 | ```bash 97 | mkdir build 98 | cd build 99 | cmake -G "Visual Studio 15 2017 Win64" .. 100 | ```` 101 | 102 | Then just build the generated ``.sln`` file using the newest Visual Studio Preview. 103 | 104 | ### Linux 105 | 106 | From the repository's folder, run: 107 | 108 | ```bash 109 | cmake/build_example.sh Release gcc g++ 110 | cd build/current 111 | ninja run 112 | ```` 113 | 114 | Or, if you want to use clang, run: 115 | 116 | ```bash 117 | cmake/build_example.sh Release clang clang++ 118 | cd build/current 119 | ninja run 120 | ```` 121 | 122 | ## Algorithm 123 | 124 | ### Insertion algorithm 125 | 126 | The library started as an implementation of this algorithm: 127 | 128 | http://blackpawn.com/texts/lightmaps/default.html 129 | 130 | The current version somewhat derives from the concept described there - 131 | however, it uses just a **vector of empty spaces, instead of a tree** - this turned out to be a performance breakthrough. 132 | 133 | Given 134 | 135 | ```cpp 136 | struct rect_xywh { 137 | int x; 138 | int y; 139 | int w; 140 | int h; 141 | }; 142 | ```` 143 | 144 | Let us create a vector and call it empty_spaces. 145 | 146 | ```cpp 147 | std::vector empty_spaces; 148 | ```` 149 | 150 | Given a user-specified initial bin, which is a square of some size S, we initialize the first empty space. 151 | 152 | ```cpp 153 | empty_spaces.push_back(rect_xywh(0, 0, S, S)); 154 | ```` 155 | 156 | Now, we'd like to insert the first image rectangle. 157 | 158 | To do this, we iterate the vector of empty spaces **backwards** and look for an empty space into which the image can fit. 159 | For now, we only have the S x S square: let's save the index of this candidate empty space, 160 | which is ``candidate_space_index = 0;`` 161 | 162 | If our image is strictly smaller than the candidate space, we have something like this: 163 | 164 | ![diag01](images/diag01.png) 165 | 166 | The blue is our image rectangle. 167 | We now calculate the gray rectangles labeled as "bigger split" and "smaller split", 168 | and save them like this: 169 | 170 | ```cpp 171 | // Erase the space that we've just inserted to, by swapping and popping. 172 | empty_spaces[candidate_space_index] = empty_spaces.back(); 173 | empty_spaces.pop_back(); 174 | 175 | // Save the resultant splits 176 | empty_spaces.push_back(bigger_split); 177 | empty_spaces.push_back(smaller_split); 178 | ```` 179 | 180 | Notice that we push the smaller split *after* the bigger one. 181 | This is fairly important, because later the candidate images will encounter the smaller splits first, 182 | which will make better use of empty spaces overall. 183 | 184 | #### Corner cases: 185 | 186 | - If the image dimensions equal the dimensions of the candidate empty space (image fits exactly), 187 | - we just delete the space and create no splits. 188 | - If the image fits into the candidate empty space, but exactly one of the image dimensions equals the respective dimension of the candidate empty space (e.g. image = 20x40, candidate space = 30x40) 189 | - we delete the space and create a single split. In this case a 10x40 space. 190 | 191 | To see the complete, modular procedure for calculating the splits (along with the corner cases), 192 | [see this source](src/rectpack2D/insert_and_split.h). 193 | 194 | If the insertion fails, we also try the same procedure for a flipped image. 195 | 196 | ### Additional heuristics 197 | 198 | Now we know how to insert individual images into a bin of a given initial size S. 199 | 200 | 1. However, what S should be passed to the algorithm so that the rectangles end up wasting the least amount of space? 201 | - We perform a binary search. 202 | - We start with the size specified by the library user. Typically, it would be the maximum texture size allowed on a particular GPU. 203 | - If the packing was successful on the given bin size, decrease the size and try to pack again. 204 | - If the packing has failed on the given bin size - because some rectangles could not be further inserted - increase the size and try to pack again. 205 | - The search is aborted if we've successfully inserted into a bin and the dimensions of the next candidate would differ from the previous by less than ``discard_step``. 206 | - This variable exists so that we may easily trade accuracy for a speedup. ``discard_step = 1`` yields the highest accuracy. ``discard_step = 128`` will yield worse packings, but will be a lot faster, etc. 207 | - The search is performed first by decreasing the bin size by both width and height, keeping it in square shape. 208 | - Then we do the same, but only decreasing width. 209 | - Then we do the same, but only decreasing height. 210 | - The last two were a breakthrough in packing tightness. It turns out important to consider non-square bins. 211 | 2. In what order should the rectangles be inserted so that they pack the tightest? 212 | - By default, the library tries 5 decreasing orders: 213 | - By area. 214 | - By perimeter. 215 | - By the bigger side. 216 | - By width. 217 | - By height. 218 | -------------------------------------------------------------------------------- /example/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(rectpack2D-example) 2 | 3 | target_sources( 4 | rectpack2D-example 5 | PUBLIC 6 | main.cpp 7 | ) 8 | 9 | target_link_libraries( 10 | rectpack2D-example 11 | PUBLIC 12 | rectpack2D::rectpack2D 13 | ) 14 | 15 | # Set C++ standard to 17 16 | set_property( 17 | TARGET rectpack2D-example 18 | PROPERTY 19 | CXX_STANDARD 17 20 | ) 21 | 22 | set(RESULT_EXE_WORKING_DIR ${CMAKE_CURRENT_SOURCE_DIR}) 23 | 24 | # Enable LTO 25 | include(CheckIPOSupported) 26 | check_ipo_supported(RESULT IPO_SUPPORTED OUTPUT output) 27 | 28 | if(IPO_SUPPORTED) 29 | set_target_properties( 30 | rectpack2D-example 31 | PROPERTIES 32 | INTERPROCEDURAL_OPTIMIZATION TRUE 33 | ) 34 | endif() 35 | 36 | if(MSVC) 37 | # Set executable working directory to current folder if debugging in vs 38 | set_target_properties( 39 | rectpack2D-example 40 | PROPERTIES 41 | VS_DEBUGGER_WORKING_DIRECTORY ${RESULT_EXE_WORKING_DIR} 42 | ) 43 | 44 | # Set executable as a startup project 45 | set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT rectpack2D-example) 46 | 47 | target_compile_options( 48 | rectpack2D-example 49 | PUBLIC 50 | /permissive- 51 | ) 52 | endif() 53 | 54 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") 55 | target_compile_options( 56 | rectpack2D-example 57 | PUBLIC 58 | -Wall 59 | -Werror 60 | -Wextra 61 | -ftemplate-backtrace-limit=0 62 | ) 63 | endif() 64 | -------------------------------------------------------------------------------- /example/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | /* For description of the algorithm, please see the README.md */ 5 | 6 | using namespace rectpack2D; 7 | 8 | int main() { 9 | constexpr bool allow_flip = true; 10 | const auto runtime_flipping_mode = flipping_option::ENABLED; 11 | 12 | /* 13 | Here, we choose the "empty_spaces" class that the algorithm will use from now on. 14 | 15 | The first template argument is a bool which determines 16 | if the algorithm will try to flip rectangles to better fit them. 17 | 18 | The second argument is optional and specifies an allocator for the empty spaces. 19 | The default one just uses a vector to store the spaces. 20 | You can also pass a "static_empty_spaces<10000>" which will allocate 10000 spaces on the stack, 21 | possibly improving performance. 22 | */ 23 | 24 | using spaces_type = rectpack2D::empty_spaces; 25 | 26 | /* 27 | rect_xywh or rect_xywhf (see src/rect_structs.h), 28 | depending on the value of allow_flip. 29 | */ 30 | 31 | using rect_type = output_rect_t; 32 | 33 | /* 34 | Note: 35 | 36 | The multiple-bin functionality was removed. 37 | This means that it is now up to you what is to be done with unsuccessful insertions. 38 | You may initialize another search when this happens. 39 | */ 40 | 41 | auto report_successful = [](rect_type&) { 42 | return callback_result::CONTINUE_PACKING; 43 | }; 44 | 45 | auto report_unsuccessful = [](rect_type&) { 46 | return callback_result::ABORT_PACKING; 47 | }; 48 | 49 | /* 50 | Initial size for the bin, from which the search begins. 51 | The result can only be smaller - if it cannot, the algorithm will gracefully fail. 52 | */ 53 | 54 | const auto max_side = 1000; 55 | 56 | /* 57 | The search stops when the bin was successfully inserted into, 58 | AND the next candidate bin size differs from the last successful one by *less* then discard_step. 59 | 60 | The best possible granuarity is achieved with discard_step = 1. 61 | If you pass a negative discard_step, the algoritm will search with even more granularity - 62 | E.g. with discard_step = -4, the algoritm will behave as if you passed discard_step = 1, 63 | but it will make as many as 4 attempts to optimize bins down to the single pixel. 64 | 65 | Since discard_step = 0 does not make sense, the algoritm will automatically treat this case 66 | as if it were passed a discard_step = 1. 67 | 68 | For common applications, a discard_step = 1 or even discard_step = 128 69 | should yield really good packings while being very performant. 70 | If you are dealing with very small rectangles specifically, 71 | it might be a good idea to make this value negative. 72 | 73 | See the algorithm section of README for more information. 74 | */ 75 | 76 | const auto discard_step = -4; 77 | 78 | /* 79 | Create some arbitrary rectangles. 80 | Every subsequent call to the packer library will only read the widths and heights that we now specify, 81 | and always overwrite the x and y coordinates with calculated results. 82 | */ 83 | 84 | std::vector rectangles; 85 | 86 | rectangles.emplace_back(rect_xywh(0, 0, 20, 40)); 87 | rectangles.emplace_back(rect_xywh(0, 0, 120, 40)); 88 | rectangles.emplace_back(rect_xywh(0, 0, 85, 59)); 89 | rectangles.emplace_back(rect_xywh(0, 0, 199, 380)); 90 | rectangles.emplace_back(rect_xywh(0, 0, 85, 875)); 91 | 92 | auto report_result = [&rectangles](const rect_wh& result_size) { 93 | std::cout << "Resultant bin: " << result_size.w << " " << result_size.h << std::endl; 94 | 95 | for (const auto& r : rectangles) { 96 | std::cout << r.x << " " << r.y << " " << r.w << " " << r.h << std::endl; 97 | } 98 | }; 99 | 100 | { 101 | /* 102 | Example 1: Find best packing with default orders. 103 | 104 | If you pass no comparators whatsoever, 105 | the standard collection of 6 orders: 106 | by area, by perimeter, by bigger side, by width, by height and by "pathological multiplier" 107 | - will be passed by default. 108 | */ 109 | 110 | const auto result_size = find_best_packing( 111 | rectangles, 112 | make_finder_input( 113 | max_side, 114 | discard_step, 115 | report_successful, 116 | report_unsuccessful, 117 | runtime_flipping_mode 118 | ) 119 | ); 120 | 121 | report_result(result_size); 122 | } 123 | 124 | { 125 | /* Example 2: Find best packing using all provided custom rectangle orders. */ 126 | 127 | using rect_ptr = rect_type*; 128 | 129 | auto my_custom_order_1 = [](const rect_ptr a, const rect_ptr b) { 130 | return a->get_wh().area() > b->get_wh().area(); 131 | }; 132 | 133 | auto my_custom_order_2 = [](const rect_ptr a, const rect_ptr b) { 134 | return a->get_wh().perimeter() < b->get_wh().perimeter(); 135 | }; 136 | 137 | const auto result_size = find_best_packing( 138 | rectangles, 139 | make_finder_input( 140 | max_side, 141 | discard_step, 142 | report_successful, 143 | report_unsuccessful, 144 | runtime_flipping_mode 145 | ), 146 | 147 | my_custom_order_1, 148 | my_custom_order_2 149 | ); 150 | 151 | report_result(result_size); 152 | } 153 | 154 | { 155 | /* Example 3: Find best packing exactly in the order of provided input. */ 156 | 157 | const auto result_size = find_best_packing_dont_sort( 158 | rectangles, 159 | make_finder_input( 160 | max_side, 161 | discard_step, 162 | report_successful, 163 | report_unsuccessful, 164 | runtime_flipping_mode 165 | ) 166 | ); 167 | 168 | report_result(result_size); 169 | } 170 | 171 | { 172 | /* Example 4: Manually perform insertions. This way you can try your own best-bin finding logic. */ 173 | 174 | auto packing_root = spaces_type({ max_side, max_side }); 175 | packing_root.flipping_mode = runtime_flipping_mode; 176 | 177 | for (auto& r : rectangles) { 178 | if (const auto inserted_rectangle = packing_root.insert(std::as_const(r).get_wh())) { 179 | r = *inserted_rectangle; 180 | } 181 | else { 182 | std::cout << "Failed to insert a rectangle." << std::endl; 183 | break; 184 | } 185 | } 186 | 187 | const auto result_size = packing_root.get_rects_aabb(); 188 | 189 | report_result(result_size); 190 | } 191 | 192 | return 0; 193 | } 194 | -------------------------------------------------------------------------------- /images/atlas_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamHypersomnia/rectpack2D/3344bf5f39db1a9fb45fd82bfc85c3a5be045e3b/images/atlas_big.png -------------------------------------------------------------------------------- /images/atlas_big_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamHypersomnia/rectpack2D/3344bf5f39db1a9fb45fd82bfc85c3a5be045e3b/images/atlas_big_color.png -------------------------------------------------------------------------------- /images/atlas_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamHypersomnia/rectpack2D/3344bf5f39db1a9fb45fd82bfc85c3a5be045e3b/images/atlas_small.png -------------------------------------------------------------------------------- /images/atlas_small_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamHypersomnia/rectpack2D/3344bf5f39db1a9fb45fd82bfc85c3a5be045e3b/images/atlas_small_color.png -------------------------------------------------------------------------------- /images/atlas_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamHypersomnia/rectpack2D/3344bf5f39db1a9fb45fd82bfc85c3a5be045e3b/images/atlas_tiny.png -------------------------------------------------------------------------------- /images/atlas_tiny_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamHypersomnia/rectpack2D/3344bf5f39db1a9fb45fd82bfc85c3a5be045e3b/images/atlas_tiny_color.png -------------------------------------------------------------------------------- /images/diag01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamHypersomnia/rectpack2D/3344bf5f39db1a9fb45fd82bfc85c3a5be045e3b/images/diag01.png -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(rectpack2D INTERFACE) 2 | 3 | target_include_directories( 4 | rectpack2D 5 | INTERFACE 6 | ${CMAKE_CURRENT_SOURCE_DIR}/ 7 | ) -------------------------------------------------------------------------------- /src/rectpack2D/best_bin_finder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "rect_structs.h" 5 | 6 | namespace rectpack2D { 7 | enum class callback_result { 8 | ABORT_PACKING, 9 | CONTINUE_PACKING 10 | }; 11 | 12 | template 13 | auto& dereference(T& r) { 14 | /* 15 | This will allow us to pass orderings that consist of pointers, 16 | as well as ones that are just plain objects in a vector. 17 | */ 18 | 19 | if constexpr(std::is_pointer_v) { 20 | return *r; 21 | } 22 | else { 23 | return r; 24 | } 25 | }; 26 | 27 | /* 28 | This function will do a binary search on viable bin sizes, 29 | starting from the biggest one: starting_bin. 30 | 31 | The search stops when the bin was successfully inserted into, 32 | AND the bin size to be tried next differs in size from the last viable one by *less* then discard_step. 33 | 34 | If we could not insert all input rectangles into a bin even as big as the starting_bin - the search fails. 35 | In this case, we return the amount of space (total_area_type) inserted in total. 36 | 37 | If we've found a viable bin that is smaller or equal to starting_bin, the search succeeds. 38 | In this case, we return the viable bin (rect_wh). 39 | */ 40 | 41 | enum class bin_dimension { 42 | BOTH, 43 | WIDTH, 44 | HEIGHT 45 | }; 46 | 47 | template 48 | std::variant best_packing_for_ordering_impl( 49 | empty_spaces_type& root, 50 | O ordering, 51 | const rect_wh starting_bin, 52 | int discard_step, 53 | const bin_dimension tried_dimension 54 | ) { 55 | auto candidate_bin = starting_bin; 56 | int tries_before_discarding = 0; 57 | 58 | if (discard_step <= 0) 59 | { 60 | tries_before_discarding = -discard_step; 61 | discard_step = 1; 62 | } 63 | 64 | //std::cout << "best_packing_for_ordering_impl dim: " << int(tried_dimension) << " w: " << starting_bin.w << " h: " << starting_bin.h << std::endl; 65 | 66 | int starting_step = 0; 67 | 68 | if (tried_dimension == bin_dimension::BOTH) { 69 | candidate_bin.w /= 2; 70 | candidate_bin.h /= 2; 71 | 72 | starting_step = candidate_bin.w / 2; 73 | } 74 | else if (tried_dimension == bin_dimension::WIDTH) { 75 | candidate_bin.w /= 2; 76 | starting_step = candidate_bin.w / 2; 77 | } 78 | else { 79 | candidate_bin.h /= 2; 80 | starting_step = candidate_bin.h / 2; 81 | } 82 | 83 | for (int step = starting_step; ; step = std::max(1, step / 2)) { 84 | //std::cout << "candidate: " << candidate_bin.w << "x" << candidate_bin.h << std::endl; 85 | 86 | root.reset(candidate_bin); 87 | 88 | int total_inserted_area = 0; 89 | 90 | const bool all_inserted = [&]() { 91 | for (const auto& r : ordering) { 92 | const auto& rect = dereference(r); 93 | 94 | if (root.insert(rect.get_wh())) { 95 | total_inserted_area += rect.area(); 96 | } 97 | else { 98 | return false; 99 | } 100 | } 101 | 102 | return true; 103 | }(); 104 | 105 | if (all_inserted) { 106 | /* Attempt was successful. Try with a smaller bin. */ 107 | 108 | if (step <= discard_step) { 109 | if (tries_before_discarding > 0) 110 | { 111 | tries_before_discarding--; 112 | } 113 | else 114 | { 115 | return candidate_bin; 116 | } 117 | } 118 | 119 | if (tried_dimension == bin_dimension::BOTH) { 120 | candidate_bin.w -= step; 121 | candidate_bin.h -= step; 122 | } 123 | else if (tried_dimension == bin_dimension::WIDTH) { 124 | candidate_bin.w -= step; 125 | } 126 | else { 127 | candidate_bin.h -= step; 128 | } 129 | 130 | root.reset(candidate_bin); 131 | } 132 | else { 133 | /* Attempt ended with failure. Try with a bigger bin. */ 134 | 135 | if (tried_dimension == bin_dimension::BOTH) { 136 | candidate_bin.w += step; 137 | candidate_bin.h += step; 138 | 139 | if (candidate_bin.area() > starting_bin.area()) { 140 | return total_inserted_area; 141 | } 142 | } 143 | else if (tried_dimension == bin_dimension::WIDTH) { 144 | candidate_bin.w += step; 145 | 146 | if (candidate_bin.w > starting_bin.w) { 147 | return total_inserted_area; 148 | } 149 | } 150 | else { 151 | candidate_bin.h += step; 152 | 153 | if (candidate_bin.h > starting_bin.h) { 154 | return total_inserted_area; 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | template 162 | std::variant best_packing_for_ordering( 163 | empty_spaces_type& root, 164 | O&& ordering, 165 | const rect_wh starting_bin, 166 | const int discard_step 167 | ) { 168 | const auto try_pack = [&]( 169 | const bin_dimension tried_dimension, 170 | const rect_wh starting_bin 171 | ) { 172 | return best_packing_for_ordering_impl( 173 | root, 174 | std::forward(ordering), 175 | starting_bin, 176 | discard_step, 177 | tried_dimension 178 | ); 179 | }; 180 | 181 | const auto best_result = try_pack(bin_dimension::BOTH, starting_bin); 182 | 183 | if (const auto failed = std::get_if(&best_result)) { 184 | return *failed; 185 | } 186 | 187 | auto best_bin = std::get(best_result); 188 | 189 | auto trial = [&](const bin_dimension tried_dimension) { 190 | const auto trial = try_pack(tried_dimension, best_bin); 191 | 192 | if (const auto better = std::get_if(&trial)) { 193 | best_bin = *better; 194 | } 195 | }; 196 | 197 | trial(bin_dimension::WIDTH); 198 | trial(bin_dimension::HEIGHT); 199 | 200 | return best_bin; 201 | } 202 | 203 | /* 204 | This function will try to find the best bin size among the ones generated by all provided rectangle orders. 205 | Only the best order will have results written to. 206 | 207 | The function reports which of the rectangles did and did not fit in the end. 208 | */ 209 | 210 | template < 211 | class empty_spaces_type, 212 | class OrderType, 213 | class F, 214 | class I 215 | > 216 | rect_wh find_best_packing_impl(F for_each_order, const I input) { 217 | const auto max_bin = rect_wh(input.max_bin_side, input.max_bin_side); 218 | 219 | OrderType* best_order = nullptr; 220 | 221 | int best_total_inserted = -1; 222 | auto best_bin = max_bin; 223 | 224 | /* 225 | The root node is re-used on the TLS. 226 | It is always reset before any packing attempt. 227 | */ 228 | 229 | thread_local empty_spaces_type root = rect_wh(); 230 | root.flipping_mode = input.flipping_mode; 231 | 232 | for_each_order ([&](OrderType& current_order) { 233 | const auto packing = best_packing_for_ordering( 234 | root, 235 | current_order, 236 | max_bin, 237 | input.discard_step 238 | ); 239 | 240 | if (const auto total_inserted = std::get_if(&packing)) { 241 | /* 242 | Track which function inserts the most area in total, 243 | just in case that all orders will fail to fit into the largest allowed bin. 244 | */ 245 | if (best_order == nullptr) { 246 | if (*total_inserted > best_total_inserted) { 247 | best_order = std::addressof(current_order); 248 | best_total_inserted = *total_inserted; 249 | } 250 | } 251 | } 252 | else if (const auto result_bin = std::get_if(&packing)) { 253 | /* Save the function if it performed the best. */ 254 | if (result_bin->area() <= best_bin.area()) { 255 | best_order = std::addressof(current_order); 256 | best_bin = *result_bin; 257 | } 258 | } 259 | }); 260 | 261 | { 262 | assert(best_order != nullptr); 263 | 264 | root.reset(best_bin); 265 | 266 | for (auto& rr : *best_order) { 267 | auto& rect = dereference(rr); 268 | 269 | if (const auto ret = root.insert(rect.get_wh())) { 270 | rect = *ret; 271 | 272 | if (callback_result::ABORT_PACKING == input.handle_successful_insertion(rect)) { 273 | break; 274 | } 275 | } 276 | else { 277 | if (callback_result::ABORT_PACKING == input.handle_unsuccessful_insertion(rect)) { 278 | break; 279 | } 280 | } 281 | } 282 | 283 | return root.get_rects_aabb(); 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/rectpack2D/empty_space_allocators.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | #include "rect_structs.h" 7 | 8 | namespace rectpack2D { 9 | class default_empty_spaces { 10 | std::vector empty_spaces; 11 | 12 | public: 13 | void remove(const int i) { 14 | empty_spaces[i] = empty_spaces.back(); 15 | empty_spaces.pop_back(); 16 | } 17 | 18 | bool add(const space_rect r) { 19 | empty_spaces.emplace_back(r); 20 | return true; 21 | } 22 | 23 | auto get_count() const { 24 | return empty_spaces.size(); 25 | } 26 | 27 | void reset() { 28 | empty_spaces.clear(); 29 | } 30 | 31 | const auto& get(const int i) { 32 | return empty_spaces[i]; 33 | } 34 | }; 35 | 36 | template 37 | class static_empty_spaces { 38 | int count_spaces = 0; 39 | std::array empty_spaces; 40 | 41 | public: 42 | void remove(const int i) { 43 | empty_spaces[i] = empty_spaces[count_spaces - 1]; 44 | --count_spaces; 45 | } 46 | 47 | bool add(const space_rect r) { 48 | if (count_spaces < static_cast(empty_spaces.size())) { 49 | empty_spaces[count_spaces] = r; 50 | ++count_spaces; 51 | 52 | return true; 53 | } 54 | 55 | return false; 56 | } 57 | 58 | auto get_count() const { 59 | return count_spaces; 60 | } 61 | 62 | void reset() { 63 | count_spaces = 0; 64 | } 65 | 66 | const auto& get(const int i) { 67 | return empty_spaces[i]; 68 | } 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/rectpack2D/empty_spaces.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "insert_and_split.h" 3 | 4 | namespace rectpack2D { 5 | enum class flipping_option { 6 | DISABLED, 7 | ENABLED 8 | }; 9 | 10 | class default_empty_spaces; 11 | 12 | template 13 | class empty_spaces { 14 | rect_wh current_aabb; 15 | empty_spaces_provider spaces; 16 | 17 | /* MSVC fix for non-conformant if constexpr implementation */ 18 | 19 | static auto make_output_rect(const int x, const int y, const int w, const int h) { 20 | return rect_xywh(x, y, w, h); 21 | } 22 | 23 | static auto make_output_rect(const int x, const int y, const int w, const int h, const bool flipped) { 24 | return rect_xywhf(x, y, w, h, flipped); 25 | } 26 | 27 | public: 28 | using output_rect_type = std::conditional_t; 29 | 30 | flipping_option flipping_mode = flipping_option::ENABLED; 31 | 32 | empty_spaces(const rect_wh& r) { 33 | reset(r); 34 | } 35 | 36 | void reset(const rect_wh& r) { 37 | current_aabb = {}; 38 | 39 | spaces.reset(); 40 | spaces.add(rect_xywh(0, 0, r.w, r.h)); 41 | } 42 | 43 | template 44 | std::optional insert(const rect_wh image_rectangle, F report_candidate_empty_space) { 45 | for (int i = static_cast(spaces.get_count()) - 1; i >= 0; --i) { 46 | const auto candidate_space = spaces.get(i); 47 | 48 | report_candidate_empty_space(candidate_space); 49 | 50 | auto accept_result = [this, i, image_rectangle, candidate_space]( 51 | const created_splits& splits, 52 | const bool flipping_necessary 53 | ) -> std::optional { 54 | spaces.remove(i); 55 | 56 | for (int s = 0; s < splits.count; ++s) { 57 | if (!spaces.add(splits.spaces[s])) { 58 | return std::nullopt; 59 | } 60 | } 61 | 62 | if constexpr(allow_flip) { 63 | const auto result = make_output_rect( 64 | candidate_space.x, 65 | candidate_space.y, 66 | image_rectangle.w, 67 | image_rectangle.h, 68 | flipping_necessary 69 | ); 70 | 71 | current_aabb.expand_with(result); 72 | return result; 73 | } 74 | else if constexpr(!allow_flip) { 75 | (void)flipping_necessary; 76 | 77 | const auto result = make_output_rect( 78 | candidate_space.x, 79 | candidate_space.y, 80 | image_rectangle.w, 81 | image_rectangle.h 82 | ); 83 | 84 | current_aabb.expand_with(result); 85 | return result; 86 | } 87 | }; 88 | 89 | auto try_to_insert = [&](const rect_wh& img) { 90 | return rectpack2D::insert_and_split(img, candidate_space); 91 | }; 92 | 93 | if constexpr(!allow_flip) { 94 | if (const auto normal = try_to_insert(image_rectangle)) { 95 | return accept_result(normal, false); 96 | } 97 | } 98 | else { 99 | if (flipping_mode == flipping_option::ENABLED) { 100 | const auto normal = try_to_insert(image_rectangle); 101 | const auto flipped = try_to_insert(rect_wh(image_rectangle).flip()); 102 | 103 | /* 104 | If both were successful, 105 | prefer the one that generated less remainder spaces. 106 | */ 107 | 108 | if (normal && flipped) { 109 | if (flipped.better_than(normal)) { 110 | /* Accept the flipped result if it producues less or "better" spaces. */ 111 | 112 | return accept_result(flipped, true); 113 | } 114 | 115 | return accept_result(normal, false); 116 | } 117 | 118 | if (normal) { 119 | return accept_result(normal, false); 120 | } 121 | 122 | if (flipped) { 123 | return accept_result(flipped, true); 124 | } 125 | } 126 | else { 127 | if (const auto normal = try_to_insert(image_rectangle)) { 128 | return accept_result(normal, false); 129 | } 130 | } 131 | } 132 | } 133 | 134 | return std::nullopt; 135 | } 136 | 137 | decltype(auto) insert(const rect_wh& image_rectangle) { 138 | return insert(image_rectangle, [](auto&){ }); 139 | } 140 | 141 | auto get_rects_aabb() const { 142 | return current_aabb; 143 | } 144 | 145 | const auto& get_spaces() const { 146 | return spaces; 147 | } 148 | }; 149 | } 150 | -------------------------------------------------------------------------------- /src/rectpack2D/finders_interface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "insert_and_split.h" 9 | #include "empty_spaces.h" 10 | #include "empty_space_allocators.h" 11 | 12 | #include "best_bin_finder.h" 13 | 14 | namespace rectpack2D { 15 | template 16 | using output_rect_t = typename empty_spaces_type::output_rect_type; 17 | 18 | template 19 | struct finder_input { 20 | const int max_bin_side; 21 | const int discard_step; 22 | F handle_successful_insertion; 23 | G handle_unsuccessful_insertion; 24 | const flipping_option flipping_mode; 25 | }; 26 | 27 | template 28 | auto make_finder_input( 29 | const int max_bin_side, 30 | const int discard_step, 31 | F&& handle_successful_insertion, 32 | G&& handle_unsuccessful_insertion, 33 | const flipping_option flipping_mode 34 | ) { 35 | return finder_input { 36 | max_bin_side, 37 | discard_step, 38 | std::forward(handle_successful_insertion), 39 | std::forward(handle_unsuccessful_insertion), 40 | flipping_mode 41 | }; 42 | }; 43 | 44 | /* 45 | Finds the best packing for the rectangles, 46 | just in the order that they were passed. 47 | */ 48 | 49 | template 50 | rect_wh find_best_packing_dont_sort( 51 | std::vector>& subjects, 52 | const finder_input& input 53 | ) { 54 | using order_type = std::remove_reference_t; 55 | 56 | return find_best_packing_impl( 57 | [&subjects](auto callback) { callback(subjects); }, 58 | input 59 | ); 60 | } 61 | 62 | 63 | /* 64 | Finds the best packing for the rectangles. 65 | Accepts a list of predicates able to compare two input rectangles. 66 | 67 | The function will try to pack the rectangles in all orders generated by the predicates, 68 | and will only write the x, y coordinates of the best packing found among the orders. 69 | */ 70 | 71 | template 72 | rect_wh find_best_packing( 73 | std::vector>& subjects, 74 | const finder_input& input, 75 | 76 | Comparator comparator, 77 | Comparators... comparators 78 | ) { 79 | using rect_type = output_rect_t; 80 | using order_type = std::vector; 81 | 82 | constexpr auto count_orders = 1 + sizeof...(Comparators); 83 | thread_local std::array orders; 84 | 85 | { 86 | /* order[0] will always exist since this overload requires at least one comparator */ 87 | auto& initial_pointers = orders[0]; 88 | initial_pointers.clear(); 89 | 90 | for (auto& s : subjects) { 91 | if (s.area() > 0) { 92 | initial_pointers.emplace_back(std::addressof(s)); 93 | } 94 | } 95 | 96 | for (std::size_t i = 1; i < count_orders; ++i) { 97 | orders[i] = initial_pointers; 98 | } 99 | } 100 | 101 | std::size_t f = 0; 102 | 103 | auto& orders_ref = orders; 104 | 105 | auto make_order = [&f, &orders_ref](auto& predicate) { 106 | std::sort(orders_ref[f].begin(), orders_ref[f].end(), predicate); 107 | ++f; 108 | }; 109 | 110 | make_order(comparator); 111 | (make_order(comparators), ...); 112 | 113 | return find_best_packing_impl( 114 | [&orders_ref](auto callback){ for (auto& o : orders_ref) { callback(o); } }, 115 | input 116 | ); 117 | } 118 | 119 | /* 120 | Finds the best packing for the rectangles. 121 | Provides a list of several sensible comparison predicates. 122 | */ 123 | 124 | template 125 | rect_wh find_best_packing( 126 | std::vector>& subjects, 127 | const finder_input& input 128 | ) { 129 | using rect_type = output_rect_t; 130 | 131 | return find_best_packing( 132 | subjects, 133 | input, 134 | 135 | [](const rect_type* const a, const rect_type* const b) { 136 | return a->area() > b->area(); 137 | }, 138 | [](const rect_type* const a, const rect_type* const b) { 139 | return a->perimeter() > b->perimeter(); 140 | }, 141 | [](const rect_type* const a, const rect_type* const b) { 142 | return std::max(a->w, a->h) > std::max(b->w, b->h); 143 | }, 144 | [](const rect_type* const a, const rect_type* const b) { 145 | return a->w > b->w; 146 | }, 147 | [](const rect_type* const a, const rect_type* const b) { 148 | return a->h > b->h; 149 | } 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/rectpack2D/insert_and_split.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "rect_structs.h" 4 | 5 | namespace rectpack2D { 6 | struct created_splits { 7 | int count = 0; 8 | std::array spaces; 9 | 10 | static auto failed() { 11 | created_splits result; 12 | result.count = -1; 13 | return result; 14 | } 15 | 16 | static auto none() { 17 | return created_splits(); 18 | } 19 | 20 | template 21 | created_splits(Args&&... args) : spaces({ std::forward(args)... }) { 22 | count = sizeof...(Args); 23 | } 24 | 25 | bool better_than(const created_splits& b) const { 26 | return count < b.count; 27 | } 28 | 29 | explicit operator bool() const { 30 | return count != -1; 31 | } 32 | }; 33 | 34 | inline created_splits insert_and_split( 35 | const rect_wh& im, /* Image rectangle */ 36 | const space_rect& sp /* Space rectangle */ 37 | ) { 38 | const auto free_w = sp.w - im.w; 39 | const auto free_h = sp.h - im.h; 40 | 41 | if (free_w < 0 || free_h < 0) { 42 | /* 43 | Image is bigger than the candidate empty space. 44 | We'll need to look further. 45 | */ 46 | 47 | return created_splits::failed(); 48 | } 49 | 50 | if (free_w == 0 && free_h == 0) { 51 | /* 52 | If the image dimensions equal the dimensions of the candidate empty space (image fits exactly), 53 | we will just delete the space and create no splits. 54 | */ 55 | 56 | return created_splits::none(); 57 | } 58 | 59 | /* 60 | If the image fits into the candidate empty space, 61 | but exactly one of the image dimensions equals the respective dimension of the candidate empty space 62 | (e.g. image = 20x40, candidate space = 30x40) 63 | we delete the space and create a single split. In this case a 10x40 space. 64 | */ 65 | 66 | if (free_w > 0 && free_h == 0) { 67 | auto r = sp; 68 | r.x += im.w; 69 | r.w -= im.w; 70 | return created_splits(r); 71 | } 72 | 73 | if (free_w == 0 && free_h > 0) { 74 | auto r = sp; 75 | r.y += im.h; 76 | r.h -= im.h; 77 | return created_splits(r); 78 | } 79 | 80 | /* 81 | Every other option has been exhausted, 82 | so at this point the image must be *strictly* smaller than the empty space, 83 | that is, it is smaller in both width and height. 84 | 85 | Thus, free_w and free_h must be positive. 86 | */ 87 | 88 | /* 89 | Decide which way to split. 90 | 91 | Instead of having two normally-sized spaces, 92 | it is better - though I have no proof of that - to have a one tiny space and a one huge space. 93 | This creates better opportunity for insertion of future rectangles. 94 | 95 | This is why, if we had more of width remaining than we had of height, 96 | we split along the vertical axis, 97 | and if we had more of height remaining than we had of width, 98 | we split along the horizontal axis. 99 | */ 100 | 101 | if (free_w > free_h) { 102 | const auto bigger_split = space_rect( 103 | sp.x + im.w, 104 | sp.y, 105 | free_w, 106 | sp.h 107 | ); 108 | 109 | const auto lesser_split = space_rect( 110 | sp.x, 111 | sp.y + im.h, 112 | im.w, 113 | free_h 114 | ); 115 | 116 | return created_splits(bigger_split, lesser_split); 117 | } 118 | 119 | const auto bigger_split = space_rect( 120 | sp.x, 121 | sp.y + im.h, 122 | sp.w, 123 | free_h 124 | ); 125 | 126 | const auto lesser_split = space_rect( 127 | sp.x + im.w, 128 | sp.y, 129 | free_w, 130 | im.h 131 | ); 132 | 133 | return created_splits(bigger_split, lesser_split); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/rectpack2D/rect_structs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace rectpack2D { 5 | using total_area_type = int; 6 | 7 | struct rect_wh { 8 | rect_wh() : w(0), h(0) {} 9 | rect_wh(const int w, const int h) : w(w), h(h) {} 10 | 11 | int w; 12 | int h; 13 | 14 | auto& flip() { 15 | std::swap(w, h); 16 | return *this; 17 | } 18 | 19 | int max_side() const { 20 | return h > w ? h : w; 21 | } 22 | 23 | int min_side() const { 24 | return h < w ? h : w; 25 | } 26 | 27 | int area() const { return w * h; } 28 | int perimeter() const { return 2 * w + 2 * h; } 29 | 30 | template 31 | void expand_with(const R& r) { 32 | w = std::max(w, r.x + r.w); 33 | h = std::max(h, r.y + r.h); 34 | } 35 | }; 36 | 37 | struct rect_xywh { 38 | int x; 39 | int y; 40 | int w; 41 | int h; 42 | 43 | rect_xywh() : x(0), y(0), w(0), h(0) {} 44 | rect_xywh(const int x, const int y, const int w, const int h) : x(x), y(y), w(w), h(h) {} 45 | 46 | int area() const { return w * h; } 47 | int perimeter() const { return 2 * w + 2 * h; } 48 | 49 | auto get_wh() const { 50 | return rect_wh(w, h); 51 | } 52 | }; 53 | 54 | struct rect_xywhf { 55 | int x; 56 | int y; 57 | int w; 58 | int h; 59 | bool flipped; 60 | 61 | rect_xywhf() : x(0), y(0), w(0), h(0), flipped(false) {} 62 | rect_xywhf(const int x, const int y, const int w, const int h, const bool flipped) : x(x), y(y), w(flipped ? h : w), h(flipped ? w : h), flipped(flipped) {} 63 | rect_xywhf(const rect_xywh& b) : rect_xywhf(b.x, b.y, b.w, b.h, false) {} 64 | 65 | int area() const { return w * h; } 66 | int perimeter() const { return 2 * w + 2 * h; } 67 | 68 | auto get_wh() const { 69 | return rect_wh(w, h); 70 | } 71 | }; 72 | 73 | using space_rect = rect_xywh; 74 | } -------------------------------------------------------------------------------- /thoughts.md: -------------------------------------------------------------------------------- 1 | ## Brute force approaches 2 | 3 | - Rect feasibility = number of empty spaces into which the rect fits 4 | - Rect difficulty = 1 / rect feasibility 5 | - Space feasibility = number of remaining input rectangles that fit into the space 6 | - How to break ties for huge, initial spaces? 7 | - How much one can insert until current rects AABB is expanded. 8 | - The more one can insert, the more feasible the space is. 9 | - In practice, will there be many ties? 10 | - There shouldn't be many not be if the max_size is carefully chosen 11 | - Determine difficulty recursively? 12 | - e.g. sum all successful insertions and successful insertions after each successful insertion... 13 | - ...quickly becomes exponential 14 | - Minimum of all free dimensions generated by all insertion trials? 15 | - Or some coefficient like pathological mult for rectangles? 16 | - Space difficulty = 1 / space feasibility 17 | - Algorithm: until no more spaces or no more rectangles 18 | - Find the most difficult space 19 | - among rects that fit... 20 | - insert the one that generates least spaces or the most difficult space - this means that, into the most difficult space, the most difficult rect will be inserted 21 | - In case of a perfectly fitting rect, this will be chosen 22 | - in case of a partly fitting or a strictly smaller rect, insert the most difficult rect, e.g. one that leaves the most difficult spaces 23 | - however, when splitting due to the rect being strictly smaller, split in the direction that generates a maximally feasible space 24 | - complexity: we iterate every space, number of which will grow linearly as time progresses, and then we iterate each rect 25 | 26 | ## Old thoughts 27 | 28 | - what about just resizing when there is no more space left? 29 | - then iterate over all empty spaces and resize those that are touching the edge 30 | - there might be none like this, though 31 | - then we can ditch iterating orders 32 | - we could then easily make it an on-line algorithm 33 | --------------------------------------------------------------------------------