mirror of
https://github.com/Relintai/texture_packer.git
synced 2024-11-12 10:15:16 +01:00
Added the rectpack2D library. (https://github.com/TeamHypersomnia/rectpack2D)
This commit is contained in:
parent
4a4c98fa68
commit
1eaeafb49a
21
rectpack2D/LICENSE
Normal file
21
rectpack2D/LICENSE
Normal file
@ -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.
|
208
rectpack2D/README.md
Normal file
208
rectpack2D/README.md
Normal file
@ -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<rect_xywh> 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.
|
270
rectpack2D/best_bin_finder.h
Normal file
270
rectpack2D/best_bin_finder.h
Normal file
@ -0,0 +1,270 @@
|
||||
#pragma once
|
||||
#include <variant>
|
||||
#include <cassert>
|
||||
#include "rect_structs.h"
|
||||
|
||||
namespace rectpack2D {
|
||||
enum class callback_result {
|
||||
ABORT_PACKING,
|
||||
CONTINUE_PACKING
|
||||
};
|
||||
|
||||
template <class T>
|
||||
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<T>) {
|
||||
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 <class empty_spaces_type, class O>
|
||||
std::variant<total_area_type, rect_wh> 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 <class empty_spaces_type, class O>
|
||||
std::variant<total_area_type, rect_wh> 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<O>(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<total_area_type>(&best_result)) {
|
||||
return *failed;
|
||||
}
|
||||
|
||||
auto best_bin = std::get<rect_wh>(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<rect_wh>(&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<total_area_type>(&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<rect_wh>(&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();
|
||||
}
|
||||
}
|
||||
}
|
70
rectpack2D/empty_space_allocators.h
Normal file
70
rectpack2D/empty_space_allocators.h
Normal file
@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
|
||||
#include "rect_structs.h"
|
||||
|
||||
namespace rectpack2D {
|
||||
class default_empty_spaces {
|
||||
std::vector<space_rect> 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 <int MAX_SPACES>
|
||||
class static_empty_spaces {
|
||||
int count_spaces = 0;
|
||||
std::array<space_rect, MAX_SPACES> 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<int>(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];
|
||||
}
|
||||
};
|
||||
}
|
149
rectpack2D/empty_spaces.h
Normal file
149
rectpack2D/empty_spaces.h
Normal file
@ -0,0 +1,149 @@
|
||||
#pragma once
|
||||
#include "insert_and_split.h"
|
||||
|
||||
namespace rectpack2D {
|
||||
enum class flipping_option {
|
||||
DISABLED,
|
||||
ENABLED
|
||||
};
|
||||
|
||||
class default_empty_spaces;
|
||||
|
||||
template <bool allow_flip, class empty_spaces_provider = default_empty_spaces>
|
||||
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<allow_flip, rect_xywhf, rect_xywh>;
|
||||
|
||||
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 <class F>
|
||||
std::optional<output_rect_type> insert(const rect_wh image_rectangle, F report_candidate_empty_space) {
|
||||
for (int i = static_cast<int>(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<output_rect_type> {
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
153
rectpack2D/finders_interface.h
Normal file
153
rectpack2D/finders_interface.h
Normal file
@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <variant>
|
||||
#include <algorithm>
|
||||
|
||||
#include "insert_and_split.h"
|
||||
#include "empty_spaces.h"
|
||||
#include "empty_space_allocators.h"
|
||||
|
||||
#include "best_bin_finder.h"
|
||||
|
||||
namespace rectpack2D {
|
||||
template <class empty_spaces_type>
|
||||
using output_rect_t = typename empty_spaces_type::output_rect_type;
|
||||
|
||||
template <class F, class G>
|
||||
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 <class F, class G>
|
||||
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<F, G> {
|
||||
max_bin_side,
|
||||
discard_step,
|
||||
std::forward<F>(handle_successful_insertion),
|
||||
std::forward<G>(handle_unsuccessful_insertion),
|
||||
flipping_mode
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
Finds the best packing for the rectangles,
|
||||
just in the order that they were passed.
|
||||
*/
|
||||
|
||||
template <class empty_spaces_type, class F, class G>
|
||||
rect_wh find_best_packing_dont_sort(
|
||||
std::vector<output_rect_t<empty_spaces_type>>& subjects,
|
||||
const finder_input<F, G>& input
|
||||
) {
|
||||
using order_type = std::remove_reference_t<decltype(subjects)>;
|
||||
|
||||
return find_best_packing_impl<empty_spaces_type, order_type>(
|
||||
[&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 <class empty_spaces_type, class F, class G, class Comparator, class... Comparators>
|
||||
rect_wh find_best_packing(
|
||||
std::vector<output_rect_t<empty_spaces_type>>& subjects,
|
||||
const finder_input<F, G>& input,
|
||||
|
||||
Comparator comparator,
|
||||
Comparators... comparators
|
||||
) {
|
||||
using rect_type = output_rect_t<empty_spaces_type>;
|
||||
using order_type = std::vector<rect_type*>;
|
||||
|
||||
constexpr auto count_orders = 1 + sizeof...(Comparators);
|
||||
thread_local std::array<order_type, count_orders> 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<empty_spaces_type, order_type>(
|
||||
[](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 <class empty_spaces_type, class F, class G>
|
||||
rect_wh find_best_packing(
|
||||
std::vector<output_rect_t<empty_spaces_type>>& subjects,
|
||||
const finder_input<F, G>& input
|
||||
) {
|
||||
using rect_type = output_rect_t<empty_spaces_type>;
|
||||
|
||||
return find_best_packing<empty_spaces_type>(
|
||||
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();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
135
rectpack2D/insert_and_split.h
Normal file
135
rectpack2D/insert_and_split.h
Normal file
@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include "rect_structs.h"
|
||||
|
||||
namespace rectpack2D {
|
||||
struct created_splits {
|
||||
int count = 0;
|
||||
std::array<space_rect, 2> spaces;
|
||||
|
||||
static auto failed() {
|
||||
created_splits result;
|
||||
result.count = -1;
|
||||
return result;
|
||||
}
|
||||
|
||||
static auto none() {
|
||||
return created_splits();
|
||||
}
|
||||
|
||||
template <class... Args>
|
||||
created_splits(Args&&... args) : spaces({ std::forward<Args>(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);
|
||||
}
|
||||
}
|
78
rectpack2D/rect_structs.h
Normal file
78
rectpack2D/rect_structs.h
Normal file
@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
#include <utility>
|
||||
|
||||
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 <class R>
|
||||
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;
|
||||
}
|
32
rectpack2D/thoughts.md
Normal file
32
rectpack2D/thoughts.md
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user