From 120da60824cc42ea7877fd8a7ac9e697baef8692 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Sep 2022 16:17:24 +0300 Subject: [PATCH] Add rectpack2D library. --- thirdparty/CMakeLists.txt | 1 + thirdparty/rectpack2d/CMakeLists.txt | 12 + thirdparty/rectpack2d/LICENSE | 21 ++ .../rectpack2d/rectpack2d/best_bin_finder.h | 286 ++++++++++++++++++ .../rectpack2d/empty_space_allocators.h | 70 +++++ .../rectpack2d/rectpack2d/empty_spaces.h | 149 +++++++++ .../rectpack2d/rectpack2d/finders_interface.h | 155 ++++++++++ .../rectpack2d/rectpack2d/insert_and_split.h | 135 +++++++++ .../rectpack2d/rectpack2d/rect_structs.h | 78 +++++ 9 files changed, 907 insertions(+) create mode 100644 thirdparty/rectpack2d/CMakeLists.txt create mode 100644 thirdparty/rectpack2d/LICENSE create mode 100644 thirdparty/rectpack2d/rectpack2d/best_bin_finder.h create mode 100644 thirdparty/rectpack2d/rectpack2d/empty_space_allocators.h create mode 100644 thirdparty/rectpack2d/rectpack2d/empty_spaces.h create mode 100644 thirdparty/rectpack2d/rectpack2d/finders_interface.h create mode 100644 thirdparty/rectpack2d/rectpack2d/insert_and_split.h create mode 100644 thirdparty/rectpack2d/rectpack2d/rect_structs.h diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index e8515ba44e..77a38b2761 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -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 ) diff --git a/thirdparty/rectpack2d/CMakeLists.txt b/thirdparty/rectpack2d/CMakeLists.txt new file mode 100644 index 0000000000..81154fee85 --- /dev/null +++ b/thirdparty/rectpack2d/CMakeLists.txt @@ -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 + ) diff --git a/thirdparty/rectpack2d/LICENSE b/thirdparty/rectpack2d/LICENSE new file mode 100644 index 0000000000..a0971a42fc --- /dev/null +++ b/thirdparty/rectpack2d/LICENSE @@ -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. diff --git a/thirdparty/rectpack2d/rectpack2d/best_bin_finder.h b/thirdparty/rectpack2d/rectpack2d/best_bin_finder.h new file mode 100644 index 0000000000..8e669d548f --- /dev/null +++ b/thirdparty/rectpack2d/rectpack2d/best_bin_finder.h @@ -0,0 +1,286 @@ +#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, + 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 + 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/thirdparty/rectpack2d/rectpack2d/empty_space_allocators.h b/thirdparty/rectpack2d/rectpack2d/empty_space_allocators.h new file mode 100644 index 0000000000..217ef77dd2 --- /dev/null +++ b/thirdparty/rectpack2d/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/thirdparty/rectpack2d/rectpack2d/empty_spaces.h b/thirdparty/rectpack2d/rectpack2d/empty_spaces.h new file mode 100644 index 0000000000..bb0ad79f6d --- /dev/null +++ b/thirdparty/rectpack2d/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/thirdparty/rectpack2d/rectpack2d/finders_interface.h b/thirdparty/rectpack2d/rectpack2d/finders_interface.h new file mode 100644 index 0000000000..c3ef8217db --- /dev/null +++ b/thirdparty/rectpack2d/rectpack2d/finders_interface.h @@ -0,0 +1,155 @@ +#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& 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( + [&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 + 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/thirdparty/rectpack2d/rectpack2d/insert_and_split.h b/thirdparty/rectpack2d/rectpack2d/insert_and_split.h new file mode 100644 index 0000000000..723582fe5f --- /dev/null +++ b/thirdparty/rectpack2d/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/thirdparty/rectpack2d/rectpack2d/rect_structs.h b/thirdparty/rectpack2d/rectpack2d/rect_structs.h new file mode 100644 index 0000000000..8d50696721 --- /dev/null +++ b/thirdparty/rectpack2d/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