From 1eaeafb49af7bc7c0fedccf78e3caaf20ce11ce0 Mon Sep 17 00:00:00 2001 From: Relintai Date: Sun, 20 Oct 2019 21:23:33 +0200 Subject: [PATCH] Added the rectpack2D library. (https://github.com/TeamHypersomnia/rectpack2D) --- rectpack2D/LICENSE | 21 +++ rectpack2D/README.md | 208 +++++++++++++++++++++ rectpack2D/best_bin_finder.h | 270 ++++++++++++++++++++++++++++ rectpack2D/empty_space_allocators.h | 70 ++++++++ rectpack2D/empty_spaces.h | 149 +++++++++++++++ rectpack2D/finders_interface.h | 153 ++++++++++++++++ rectpack2D/insert_and_split.h | 135 ++++++++++++++ rectpack2D/rect_structs.h | 78 ++++++++ rectpack2D/thoughts.md | 32 ++++ 9 files changed, 1116 insertions(+) create mode 100644 rectpack2D/LICENSE create mode 100644 rectpack2D/README.md create mode 100644 rectpack2D/best_bin_finder.h create mode 100644 rectpack2D/empty_space_allocators.h create mode 100644 rectpack2D/empty_spaces.h create mode 100644 rectpack2D/finders_interface.h create mode 100644 rectpack2D/insert_and_split.h create mode 100644 rectpack2D/rect_structs.h create mode 100644 rectpack2D/thoughts.md diff --git a/rectpack2D/LICENSE b/rectpack2D/LICENSE new file mode 100644 index 0000000..aa933d1 --- /dev/null +++ b/rectpack2D/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Team Hypersomnia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/rectpack2D/README.md b/rectpack2D/README.md new file mode 100644 index 0000000..404bf71 --- /dev/null +++ b/rectpack2D/README.md @@ -0,0 +1,208 @@ +If you are looking for the old version of rectpack2D, it can still be found in [a legacy branch](https://github.com/TeamHypersomnia/rectpack2D/tree/legacy). + +# rectpack2D + +[![Build Status](https://travis-ci.org/TeamHypersomnia/rectpack2D.svg?branch=master)](https://travis-ci.org/TeamHypersomnia/rectpack2D) +[![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/aojyt3r6ysvadkgl?svg=true)](https://ci.appveyor.com/project/geneotech/rectpack2D) + +Table of contents: + +- [Benchmarks](#benchmarks) +- [Usage](#usage) +- [Building the example](#building-the-example) + * [Windows](#windows) + * [Linux](#linux) +- [Algorithm](#algorithm) + * [Insertion algorithm](#insertion-algorithm) + * [Additional heuristics](#additional-heuristics) + +Rectangle packing library (no longer tiny!). +This is a refactored and **highly optimized** branch of the original library which is easier to use and customize. + +![7](https://user-images.githubusercontent.com/3588717/42707552-d8b1c65e-86da-11e8-9412-54c580bd2696.jpg) + +## Benchmarks + +Tests were conducted on a ``Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz``. +The binary was built with ``clang 6.0.0``, using an -03 switch. + +### Arbitrary game sprites: 582 subjects. + +**Runtime: 0.8 ms** +**Wasted pixels: 10982 (0.24% - equivalent of a 105 x 105 square)** + +Output (1896 x 2382): + +![1](images/atlas_small.png) + +In color: +(black is wasted space) + +![2](images/atlas_small_color.png) + +### Arbitrary game sprites + Japanese glyphs: 3264 subjects. + +**Runtime: 4 ms** +**Wasted pixels: 15538 (0.31% - equivalent of a 125 x 125 square)** + +Output (2116 x 2382): + +![3](images/atlas_big.png) + +In color: +(black is wasted space) + +![4](images/atlas_big_color.png) + + +### Japanese glyphs + some GUI sprites: 3122 subjects. + +**Runtime: 3.5 - 7 ms** +**Wasted pixels: 9288 (1.23% - equivalent of a 96 x 96 square)** + +Output (866 x 871): + +![5](images/atlas_tiny.png) + +In color: +(black is wasted space) + +![6](images/atlas_tiny_color.png) + +## Usage + +This is a header-only library. +Just include the ``src/finders_interface.h`` and you should be good to go. + +For an example use, see ``example/main.cpp``. + +## Building the example + +### Windows + +From the repository's folder, run: + +```bash +mkdir build +cd build +cmake -G "Visual Studio 15 2017 Win64" .. +```` + +Then just build the generated ``.sln`` file using the newest Visual Studio Preview. + +### Linux + +From the repository's folder, run: + +```bash +cmake/build_example.sh Release gcc g++ +cd build/current +ninja run +```` + +Or, if you want to use clang, run: + +```bash +cmake/build_example.sh Release clang clang++ +cd build/current +ninja run +```` + +## Algorithm + +### Insertion algorithm + +The library started as an implementation of this algorithm: + +http://blackpawn.com/texts/lightmaps/default.html + +The current version somewhat derives from the concept described there - +however, it uses just a **vector of empty spaces, instead of a tree** - this turned out to be a performance breakthrough. + +Given + +```cpp +struct rect_xywh { + int x; + int y; + int w; + int h; +}; +```` + +Let us create a vector and call it empty_spaces. + +```cpp +std::vector empty_spaces; +```` + +Given a user-specified initial bin, which is a square of some size S, we initialize the first empty space. + +```cpp +empty_spaces.push_back(rect_xywh(0, 0, S, S)); +```` + +Now, we'd like to insert the first image rectangle. + +To do this, we iterate the vector of empty spaces **backwards** and look for an empty space into which the image can fit. +For now, we only have the S x S square: let's save the index of this candidate empty space, +which is ``candidate_space_index = 0;`` + +If our image is strictly smaller than the candidate space, we have something like this: + +![diag01](images/diag01.png) + +The blue is our image rectangle. +We now calculate the gray rectangles labeled as "bigger split" and "smaller split", +and save them like this: + +```cpp +// Erase the space that we've just inserted to, by swapping and popping. +empty_spaces[candidate_space_index] = empty_spaces.back(); +empty_spaces.pop_back(); + +// Save the resultant splits +empty_spaces.push_back(bigger_split); +empty_spaces.push_back(smaller_split); +```` + +Notice that we push the smaller split *after* the bigger one. +This is fairly important, because later the candidate images will encounter the smaller splits first, +which will make better use of empty spaces overall. + +#### Corner cases: + +- If the image dimensions equal the dimensions of the candidate empty space (image fits exactly), + - we just delete the space and create no splits. +- 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) + - we delete the space and create a single split. In this case a 10x40 space. + +To see the complete, modular procedure for calculating the splits (along with the corner cases), +[see this source](src/insert_and_split.h). + +If the insertion fails, we also try the same procedure for a flipped image. + +### Additional heuristics + +Now we know how to insert individual images into a bin of a given initial size S. + +1. However, what S should be passed to the algorithm so that the rectangles end up wasting the least amount of space? + - We perform a binary search. + - We start with the size specified by the library user. Typically, it would be the maximum texture size allowed on a particular GPU. + - If the packing was successful on the given bin size, decrease the size and try to pack again. + - 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. + - 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``. + - 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. + - The search is performed first by decreasing the bin size by both width and height, keeping it in square shape. + - Then we do the same, but only decreasing width. + - Then we do the same, but only decreasing height. + - The last two were a breakthrough in packing tightness. It turns out important to consider non-square bins. +2. In what order should the rectangles be inserted so that they pack the tightest? + - By default, the library tries 6 decreasing orders: + - By area. + - By perimeter. + - By the bigger side. + - By width. + - By height. + - By 'a pathological multiplier": ``max(w, h) / min(w, h) * w * h`` + - This makes some giant, irregular mutants always go first, which is good, because it doesn't shred our empty spaces to a lot of useless pieces. diff --git a/rectpack2D/best_bin_finder.h b/rectpack2D/best_bin_finder.h new file mode 100644 index 0000000..d14c92b --- /dev/null +++ b/rectpack2D/best_bin_finder.h @@ -0,0 +1,270 @@ +#pragma once +#include +#include +#include "rect_structs.h" + +namespace rectpack2D { + enum class callback_result { + ABORT_PACKING, + CONTINUE_PACKING + }; + + template + auto& dereference(T& r) { + /* + This will allow us to pass orderings that consist of pointers, + as well as ones that are just plain objects in a vector. + */ + + if constexpr(std::is_pointer_v) { + return *r; + } + else { + return r; + } + }; + + /* + This function will do a binary search on viable bin sizes, + starting from the biggest one: starting_bin. + + The search stops when the bin was successfully inserted into, + AND the bin size to be tried next differs in size from the last viable one by *less* then discard_step. + + If we could not insert all input rectangles into a bin even as big as the starting_bin - the search fails. + In this case, we return the amount of space (total_area_type) inserted in total. + + If we've found a viable bin that is smaller or equal to starting_bin, the search succeeds. + In this case, we return the viable bin (rect_wh). + */ + + enum class bin_dimension { + BOTH, + WIDTH, + HEIGHT + }; + + template + std::variant best_packing_for_ordering_impl( + empty_spaces_type& root, + O ordering, + const rect_wh starting_bin, + const int discard_step, + const bin_dimension tried_dimension + ) { + auto candidate_bin = starting_bin; + + int starting_step = 0; + + if (tried_dimension == bin_dimension::BOTH) { + starting_step = starting_bin.w / 2; + + candidate_bin.w /= 2; + candidate_bin.h /= 2; + } + else if (tried_dimension == bin_dimension::WIDTH) { + starting_step = starting_bin.w / 2; + + candidate_bin.w /= 2; + } + else { + starting_step = starting_bin.h / 2; + + candidate_bin.h /= 2; + } + + for (int step = starting_step; ; step = std::max(1, step / 2)) { + root.reset(candidate_bin); + + int total_inserted_area = 0; + + const bool all_inserted = [&]() { + for (const auto& r : ordering) { + const auto& rect = dereference(r); + + if (root.insert(rect.get_wh())) { + total_inserted_area += rect.area(); + } + else { + return false; + } + } + + return true; + }(); + + if (all_inserted) { + /* Attempt was successful. Try with a smaller bin. */ + + if (step <= discard_step) { + return candidate_bin; + } + + if (tried_dimension == bin_dimension::BOTH) { + candidate_bin.w -= step; + candidate_bin.h -= step; + } + else if (tried_dimension == bin_dimension::WIDTH) { + candidate_bin.w -= step; + } + else { + candidate_bin.h -= step; + } + + root.reset(candidate_bin); + } + else { + /* Attempt ended with failure. Try with a bigger bin. */ + + if (tried_dimension == bin_dimension::BOTH) { + candidate_bin.w += step; + candidate_bin.h += step; + + if (candidate_bin.area() > starting_bin.area()) { + return total_inserted_area; + } + } + else if (tried_dimension == bin_dimension::WIDTH) { + candidate_bin.w += step; + + if (candidate_bin.w > starting_bin.w) { + return total_inserted_area; + } + } + else { + candidate_bin.h += step; + + if (candidate_bin.h > starting_bin.h) { + return total_inserted_area; + } + } + } + } + } + + template + std::variant best_packing_for_ordering( + empty_spaces_type& root, + O&& ordering, + const rect_wh starting_bin, + const int discard_step + ) { + const auto try_pack = [&]( + const bin_dimension tried_dimension, + const rect_wh starting_bin + ) { + return best_packing_for_ordering_impl( + root, + std::forward(ordering), + starting_bin, + discard_step, + tried_dimension + ); + }; + + const auto best_result = try_pack(bin_dimension::BOTH, starting_bin); + + if (const auto failed = std::get_if(&best_result)) { + return *failed; + } + + auto best_bin = std::get(best_result); + + auto trial = [&](const bin_dimension tried_dimension) { + const auto trial = try_pack(tried_dimension, best_bin); + + if (const auto better = std::get_if(&trial)) { + best_bin = *better; + } + }; + + trial(bin_dimension::WIDTH); + trial(bin_dimension::HEIGHT); + + return best_bin; + } + + /* + This function will try to find the best bin size among the ones generated by all provided rectangle orders. + Only the best order will have results written to. + + The function reports which of the rectangles did and did not fit in the end. + */ + + template < + class empty_spaces_type, + class OrderType, + class F, + class I + > + rect_wh find_best_packing_impl(F for_each_order, const I input) { + const auto max_bin = rect_wh(input.max_bin_side, input.max_bin_side); + + OrderType* best_order = nullptr; + + int best_total_inserted = -1; + auto best_bin = max_bin; + + /* + The root node is re-used on the TLS. + It is always reset before any packing attempt. + */ + + thread_local empty_spaces_type root = rect_wh(); + root.flipping_mode = input.flipping_mode; + + for_each_order ([&](OrderType& current_order) { + const auto packing = best_packing_for_ordering( + root, + current_order, + max_bin, + input.discard_step + ); + + if (const auto total_inserted = std::get_if(&packing)) { + /* + Track which function inserts the most area in total, + just in case that all orders will fail to fit into the largest allowed bin. + */ + if (best_order == nullptr) { + if (*total_inserted > best_total_inserted) { + best_order = std::addressof(current_order); + best_total_inserted = *total_inserted; + } + } + } + else if (const auto result_bin = std::get_if(&packing)) { + /* Save the function if it performed the best. */ + if (result_bin->area() <= best_bin.area()) { + best_order = std::addressof(current_order); + best_bin = *result_bin; + } + } + }); + + { + assert(best_order != nullptr); + + root.reset(best_bin); + + for (auto& rr : *best_order) { + auto& rect = dereference(rr); + + if (const auto ret = root.insert(rect.get_wh())) { + rect = *ret; + + if (callback_result::ABORT_PACKING == input.handle_successful_insertion(rect)) { + break; + } + } + else { + if (callback_result::ABORT_PACKING == input.handle_unsuccessful_insertion(rect)) { + break; + } + } + } + + return root.get_rects_aabb(); + } + } +} diff --git a/rectpack2D/empty_space_allocators.h b/rectpack2D/empty_space_allocators.h new file mode 100644 index 0000000..217ef77 --- /dev/null +++ b/rectpack2D/empty_space_allocators.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include + +#include "rect_structs.h" + +namespace rectpack2D { + class default_empty_spaces { + std::vector empty_spaces; + + public: + void remove(const int i) { + empty_spaces[i] = empty_spaces.back(); + empty_spaces.pop_back(); + } + + bool add(const space_rect r) { + empty_spaces.emplace_back(r); + return true; + } + + auto get_count() const { + return empty_spaces.size(); + } + + void reset() { + empty_spaces.clear(); + } + + const auto& get(const int i) { + return empty_spaces[i]; + } + }; + + template + class static_empty_spaces { + int count_spaces = 0; + std::array empty_spaces; + + public: + void remove(const int i) { + empty_spaces[i] = empty_spaces[count_spaces - 1]; + --count_spaces; + } + + bool add(const space_rect r) { + if (count_spaces < static_cast(empty_spaces.size())) { + empty_spaces[count_spaces] = r; + ++count_spaces; + + return true; + } + + return false; + } + + auto get_count() const { + return count_spaces; + } + + void reset() { + count_spaces = 0; + } + + const auto& get(const int i) { + return empty_spaces[i]; + } + }; +} diff --git a/rectpack2D/empty_spaces.h b/rectpack2D/empty_spaces.h new file mode 100644 index 0000000..bb0ad79 --- /dev/null +++ b/rectpack2D/empty_spaces.h @@ -0,0 +1,149 @@ +#pragma once +#include "insert_and_split.h" + +namespace rectpack2D { + enum class flipping_option { + DISABLED, + ENABLED + }; + + class default_empty_spaces; + + template + class empty_spaces { + rect_wh current_aabb; + empty_spaces_provider spaces; + + /* MSVC fix for non-conformant if constexpr implementation */ + + static auto make_output_rect(const int x, const int y, const int w, const int h) { + return rect_xywh(x, y, w, h); + } + + static auto make_output_rect(const int x, const int y, const int w, const int h, const bool flipped) { + return rect_xywhf(x, y, w, h, flipped); + } + + public: + using output_rect_type = std::conditional_t; + + flipping_option flipping_mode = flipping_option::ENABLED; + + empty_spaces(const rect_wh& r) { + reset(r); + } + + void reset(const rect_wh& r) { + current_aabb = {}; + + spaces.reset(); + spaces.add(rect_xywh(0, 0, r.w, r.h)); + } + + template + std::optional insert(const rect_wh image_rectangle, F report_candidate_empty_space) { + for (int i = static_cast(spaces.get_count()) - 1; i >= 0; --i) { + const auto candidate_space = spaces.get(i); + + report_candidate_empty_space(candidate_space); + + auto accept_result = [this, i, image_rectangle, candidate_space]( + const created_splits& splits, + const bool flipping_necessary + ) -> std::optional { + spaces.remove(i); + + for (int s = 0; s < splits.count; ++s) { + if (!spaces.add(splits.spaces[s])) { + return std::nullopt; + } + } + + if constexpr(allow_flip) { + const auto result = make_output_rect( + candidate_space.x, + candidate_space.y, + image_rectangle.w, + image_rectangle.h, + flipping_necessary + ); + + current_aabb.expand_with(result); + return result; + } + else if constexpr(!allow_flip) { + (void)flipping_necessary; + + const auto result = make_output_rect( + candidate_space.x, + candidate_space.y, + image_rectangle.w, + image_rectangle.h + ); + + current_aabb.expand_with(result); + return result; + } + }; + + auto try_to_insert = [&](const rect_wh& img) { + return rectpack2D::insert_and_split(img, candidate_space); + }; + + if constexpr(!allow_flip) { + if (const auto normal = try_to_insert(image_rectangle)) { + return accept_result(normal, false); + } + } + else { + if (flipping_mode == flipping_option::ENABLED) { + const auto normal = try_to_insert(image_rectangle); + const auto flipped = try_to_insert(rect_wh(image_rectangle).flip()); + + /* + If both were successful, + prefer the one that generated less remainder spaces. + */ + + if (normal && flipped) { + if (flipped.better_than(normal)) { + /* Accept the flipped result if it producues less or "better" spaces. */ + + return accept_result(flipped, true); + } + + return accept_result(normal, false); + } + + if (normal) { + return accept_result(normal, false); + } + + if (flipped) { + return accept_result(flipped, true); + } + } + else { + if (const auto normal = try_to_insert(image_rectangle)) { + return accept_result(normal, false); + } + } + } + } + + return std::nullopt; + } + + decltype(auto) insert(const rect_wh& image_rectangle) { + return insert(image_rectangle, [](auto&){ }); + } + + auto get_rects_aabb() const { + return current_aabb; + } + + const auto& get_spaces() const { + return spaces; + } + }; +} diff --git a/rectpack2D/finders_interface.h b/rectpack2D/finders_interface.h new file mode 100644 index 0000000..95991e9 --- /dev/null +++ b/rectpack2D/finders_interface.h @@ -0,0 +1,153 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "insert_and_split.h" +#include "empty_spaces.h" +#include "empty_space_allocators.h" + +#include "best_bin_finder.h" + +namespace rectpack2D { + template + using output_rect_t = typename empty_spaces_type::output_rect_type; + + template + struct finder_input { + const int max_bin_side; + const int discard_step; + F handle_successful_insertion; + G handle_unsuccessful_insertion; + const flipping_option flipping_mode; + }; + + template + auto make_finder_input( + const int max_bin_side, + const int discard_step, + F&& handle_successful_insertion, + G&& handle_unsuccessful_insertion, + const flipping_option flipping_mode + ) { + return finder_input { + max_bin_side, + discard_step, + std::forward(handle_successful_insertion), + std::forward(handle_unsuccessful_insertion), + flipping_mode + }; + }; + + /* + Finds the best packing for the rectangles, + just in the order that they were passed. + */ + + template + rect_wh find_best_packing_dont_sort( + std::vector>& subjects, + const finder_input& input + ) { + using order_type = std::remove_reference_t; + + return find_best_packing_impl( + [&subjects](auto callback) { callback(subjects); }, + input + ); + } + + + /* + Finds the best packing for the rectangles. + Accepts a list of predicates able to compare two input rectangles. + + The function will try to pack the rectangles in all orders generated by the predicates, + and will only write the x, y coordinates of the best packing found among the orders. + */ + + template + rect_wh find_best_packing( + std::vector>& subjects, + const finder_input& input, + + Comparator comparator, + Comparators... comparators + ) { + using rect_type = output_rect_t; + using order_type = std::vector; + + constexpr auto count_orders = 1 + sizeof...(Comparators); + thread_local std::array orders; + + { + /* order[0] will always exist since this overload requires at least one comparator */ + auto& initial_pointers = orders[0]; + initial_pointers.clear(); + + for (auto& s : subjects) { + if (s.area() > 0) { + initial_pointers.emplace_back(std::addressof(s)); + } + } + + for (std::size_t i = 1; i < count_orders; ++i) { + orders[i] = initial_pointers; + } + } + + std::size_t f = 0; + + auto make_order = [&f](auto& predicate) { + std::sort(orders[f].begin(), orders[f].end(), predicate); + ++f; + }; + + make_order(comparator); + (make_order(comparators), ...); + + return find_best_packing_impl( + [](auto callback){ for (auto& o : orders) { callback(o); } }, + input + ); + } + + /* + Finds the best packing for the rectangles. + Provides a list of several sensible comparison predicates. + */ + + template + rect_wh find_best_packing( + std::vector>& subjects, + const finder_input& input + ) { + using rect_type = output_rect_t; + + return find_best_packing( + subjects, + input, + + [](const rect_type* const a, const rect_type* const b) { + return a->area() > b->area(); + }, + [](const rect_type* const a, const rect_type* const b) { + return a->perimeter() > b->perimeter(); + }, + [](const rect_type* const a, const rect_type* const b) { + return std::max(a->w, a->h) > std::max(b->w, b->h); + }, + [](const rect_type* const a, const rect_type* const b) { + return a->w > b->w; + }, + [](const rect_type* const a, const rect_type* const b) { + return a->h > b->h; + }, + [](const rect_type* const a, const rect_type* const b) { + return a->get_wh().pathological_mult() > b->get_wh().pathological_mult(); + } + ); + } +} diff --git a/rectpack2D/insert_and_split.h b/rectpack2D/insert_and_split.h new file mode 100644 index 0000000..723582f --- /dev/null +++ b/rectpack2D/insert_and_split.h @@ -0,0 +1,135 @@ +#pragma once +#include +#include "rect_structs.h" + +namespace rectpack2D { + struct created_splits { + int count = 0; + std::array spaces; + + static auto failed() { + created_splits result; + result.count = -1; + return result; + } + + static auto none() { + return created_splits(); + } + + template + created_splits(Args&&... args) : spaces({ std::forward(args)... }) { + count = sizeof...(Args); + } + + bool better_than(const created_splits& b) const { + return count < b.count; + } + + explicit operator bool() const { + return count != -1; + } + }; + + inline created_splits insert_and_split( + const rect_wh& im, /* Image rectangle */ + const space_rect& sp /* Space rectangle */ + ) { + const auto free_w = sp.w - im.w; + const auto free_h = sp.h - im.h; + + if (free_w < 0 || free_h < 0) { + /* + Image is bigger than the candidate empty space. + We'll need to look further. + */ + + return created_splits::failed(); + } + + if (free_w == 0 && free_h == 0) { + /* + If the image dimensions equal the dimensions of the candidate empty space (image fits exactly), + we will just delete the space and create no splits. + */ + + return created_splits::none(); + } + + /* + 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) + we delete the space and create a single split. In this case a 10x40 space. + */ + + if (free_w > 0 && free_h == 0) { + auto r = sp; + r.x += im.w; + r.w -= im.w; + return created_splits(r); + } + + if (free_w == 0 && free_h > 0) { + auto r = sp; + r.y += im.h; + r.h -= im.h; + return created_splits(r); + } + + /* + Every other option has been exhausted, + so at this point the image must be *strictly* smaller than the empty space, + that is, it is smaller in both width and height. + + Thus, free_w and free_h must be positive. + */ + + /* + Decide which way to split. + + Instead of having two normally-sized spaces, + it is better - though I have no proof of that - to have a one tiny space and a one huge space. + This creates better opportunity for insertion of future rectangles. + + This is why, if we had more of width remaining than we had of height, + we split along the vertical axis, + and if we had more of height remaining than we had of width, + we split along the horizontal axis. + */ + + if (free_w > free_h) { + const auto bigger_split = space_rect( + sp.x + im.w, + sp.y, + free_w, + sp.h + ); + + const auto lesser_split = space_rect( + sp.x, + sp.y + im.h, + im.w, + free_h + ); + + return created_splits(bigger_split, lesser_split); + } + + const auto bigger_split = space_rect( + sp.x, + sp.y + im.h, + sp.w, + free_h + ); + + const auto lesser_split = space_rect( + sp.x + im.w, + sp.y, + free_w, + im.h + ); + + return created_splits(bigger_split, lesser_split); + } +} diff --git a/rectpack2D/rect_structs.h b/rectpack2D/rect_structs.h new file mode 100644 index 0000000..8d50696 --- /dev/null +++ b/rectpack2D/rect_structs.h @@ -0,0 +1,78 @@ +#pragma once +#include + +namespace rectpack2D { + using total_area_type = int; + + struct rect_wh { + rect_wh() : w(0), h(0) {} + rect_wh(const int w, const int h) : w(w), h(h) {} + + int w; + int h; + + auto& flip() { + std::swap(w, h); + return *this; + } + + int max_side() const { + return h > w ? h : w; + } + + int min_side() const { + return h < w ? h : w; + } + + int area() const { return w * h; } + int perimeter() const { return 2 * w + 2 * h; } + + float pathological_mult() const { + return float(max_side()) / min_side() * area(); + } + + template + void expand_with(const R& r) { + w = std::max(w, r.x + r.w); + h = std::max(h, r.y + r.h); + } + }; + + struct rect_xywh { + int x; + int y; + int w; + int h; + + rect_xywh() : x(0), y(0), w(0), h(0) {} + rect_xywh(const int x, const int y, const int w, const int h) : x(x), y(y), w(w), h(h) {} + + int area() const { return w * h; } + int perimeter() const { return 2 * w + 2 * h; } + + auto get_wh() const { + return rect_wh(w, h); + } + }; + + struct rect_xywhf { + int x; + int y; + int w; + int h; + bool flipped; + + rect_xywhf() : x(0), y(0), w(0), h(0), flipped(false) {} + 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) {} + rect_xywhf(const rect_xywh& b) : rect_xywhf(b.x, b.y, b.w, b.h, false) {} + + int area() const { return w * h; } + int perimeter() const { return 2 * w + 2 * h; } + + auto get_wh() const { + return rect_wh(w, h); + } + }; + + using space_rect = rect_xywh; +} \ No newline at end of file diff --git a/rectpack2D/thoughts.md b/rectpack2D/thoughts.md new file mode 100644 index 0000000..3c1a630 --- /dev/null +++ b/rectpack2D/thoughts.md @@ -0,0 +1,32 @@ +## Brute force approaches + +- Rect feasibility = number of empty spaces into which the rect fits +- Rect difficulty = 1 / rect feasibility +- Space feasibility = number of remaining input rectangles that fit into the space + - How to break ties for huge, initial spaces? + - How much one can insert until current rects AABB is expanded. + - The more one can insert, the more feasible the space is. + - In practice, will there be many ties? + - There shouldn't be many not be if the max_size is carefully chosen + - Determine difficulty recursively? + - e.g. sum all successful insertions and successful insertions after each successful insertion... + - ...quickly becomes exponential + - Minimum of all free dimensions generated by all insertion trials? + - Or some coefficient like pathological mult for rectangles? +- Space difficulty = 1 / space feasibility +- Algorithm: until no more spaces or no more rectangles + - Find the most difficult space + - among rects that fit... + - 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 + - In case of a perfectly fitting rect, this will be chosen + - 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 + - however, when splitting due to the rect being strictly smaller, split in the direction that generates a maximally feasible space +- complexity: we iterate every space, number of which will grow linearly as time progresses, and then we iterate each rect + +## Old thoughts + +- what about just resizing when there is no more space left? + - then iterate over all empty spaces and resize those that are touching the edge + - there might be none like this, though + - then we can ditch iterating orders + - we could then easily make it an on-line algorithm