Add rectpack2D library.

This commit is contained in:
Alex 2022-09-27 16:17:24 +03:00 committed by Jon Evans
parent b6eb5f721a
commit 120da60824
9 changed files with 907 additions and 0 deletions

View File

@ -35,6 +35,7 @@ add_subdirectory( markdown2html )
add_subdirectory( nanodbc )
add_subdirectory( nanosvg )
add_subdirectory( other_math )
add_subdirectory( rectpack2d )
add_subdirectory( rtree )
add_subdirectory( tinyspline_lib )
add_subdirectory( potrace )

12
thirdparty/rectpack2d/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,12 @@
add_library( rectpack2d INTERFACE )
target_include_directories( rectpack2d INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} )
target_sources( rectpack2d INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/rectpack2d/best_bin_finder.h
${CMAKE_CURRENT_SOURCE_DIR}/rectpack2d/empty_space_allocators.h
${CMAKE_CURRENT_SOURCE_DIR}/rectpack2d/empty_spaces.h
${CMAKE_CURRENT_SOURCE_DIR}/rectpack2d/finders_interface.h
${CMAKE_CURRENT_SOURCE_DIR}/rectpack2d/insert_and_split.h
${CMAKE_CURRENT_SOURCE_DIR}/rectpack2d/rect_structs.h
)

21
thirdparty/rectpack2d/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Patryk Czachurski
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.

View File

@ -0,0 +1,286 @@
#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,
int discard_step,
const bin_dimension tried_dimension
) {
auto candidate_bin = starting_bin;
int tries_before_discarding = 0;
if (discard_step <= 0)
{
tries_before_discarding = -discard_step;
discard_step = 1;
}
//std::cout << "best_packing_for_ordering_impl dim: " << int(tried_dimension) << " w: " << starting_bin.w << " h: " << starting_bin.h << std::endl;
int starting_step = 0;
if (tried_dimension == bin_dimension::BOTH) {
candidate_bin.w /= 2;
candidate_bin.h /= 2;
starting_step = candidate_bin.w / 2;
}
else if (tried_dimension == bin_dimension::WIDTH) {
candidate_bin.w /= 2;
starting_step = candidate_bin.w / 2;
}
else {
candidate_bin.h /= 2;
starting_step = candidate_bin.h / 2;
}
for (int step = starting_step; ; step = std::max(1, step / 2)) {
//std::cout << "candidate: " << candidate_bin.w << "x" << candidate_bin.h << std::endl;
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) {
if (tries_before_discarding > 0)
{
tries_before_discarding--;
}
else
{
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();
}
}
}

View 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];
}
};
}

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

View File

@ -0,0 +1,155 @@
#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& orders_ref = orders;
auto make_order = [&f, &orders_ref](auto& predicate) {
std::sort(orders_ref[f].begin(), orders_ref[f].end(), predicate);
++f;
};
make_order(comparator);
(make_order(comparators), ...);
return find_best_packing_impl<empty_spaces_type, order_type>(
[&orders_ref](auto callback){ for (auto& o : orders_ref) { 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();
}
);
}
}

View 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);
}
}

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