Lagrange
Loading...
Searching...
No Matches
pack_boxes.h
1/*
2 * Copyright 2025 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12#pragma once
13
14// clang-format off
15#include <lagrange/utils/warnoff.h>
16#include <RectangleBinPack/GuillotineBinPack.h>
17#include <RectangleBinPack/Rect.h>
18#include <lagrange/utils/warnon.h>
19// clang-format on
20
21#include <lagrange/Logger.h>
22#include <lagrange/utils/assert.h>
23#include <lagrange/utils/range.h>
24#include <lagrange/utils/safe_cast.h>
25
26#include <Eigen/Core>
27
28#include <exception>
29#include <limits>
30#include <string>
31#include <tuple>
32#include <type_traits>
33#include <vector>
34
35namespace lagrange::packing {
36
37template <typename T>
38bool product_will_overflow(T a, T b)
39{
40 static_assert(std::is_integral<T>::value, "T must be an integral type");
41
42 if constexpr (std::is_signed<T>::value) {
43 using Limits = std::numeric_limits<T>;
44
45 if (a > 0) {
46 if (b > 0) {
47 return a > Limits::max() / b;
48 } else if (b < 0) {
49 return b < Limits::min() / a;
50 }
51 } else if (a < 0) {
52 if (b > 0) {
53 return a < Limits::min() / b;
54 } else if (b < 0) {
55 return a < Limits::max() / b;
56 }
57 }
58 return false; // one of them is zero
59 } else {
60 // Unsigned case
61 if (b == 0) return false;
62 return a > std::numeric_limits<T>::max() / b;
63 }
64}
65
66class PackingFailure : public std::runtime_error
67{
68public:
69 PackingFailure(const std::string& what)
70 : std::runtime_error(what)
71 {}
72};
73
87template <typename Scalar>
88std::tuple<Eigen::Matrix<Scalar, Eigen::Dynamic, 2, Eigen::RowMajor>, std::vector<bool>, Scalar>
89pack_boxes(
90 const Eigen::Matrix<Scalar, Eigen::Dynamic, 2, Eigen::RowMajor>& bbox_mins,
91 const Eigen::Matrix<Scalar, Eigen::Dynamic, 2, Eigen::RowMajor>& bbox_maxs,
92 bool allow_rotation,
93 float margin)
94{
95#ifdef RECTANGLE_BIN_PACK_OSS
96 using Int = int;
97#else
98 using Int = ::rbp::Int;
99#endif
100
101 la_runtime_assert(bbox_mins.array().isFinite().all());
102 la_runtime_assert(bbox_maxs.array().isFinite().all());
103 la_runtime_assert(bbox_mins.rows() == bbox_maxs.rows());
104 const auto num_boxes = bbox_mins.rows();
105 if (num_boxes == 0) {
106 return std::make_tuple(
107 Eigen::Matrix<Scalar, Eigen::Dynamic, 2, Eigen::RowMajor>(0, 2),
108 std::vector<bool>(),
109 1);
110 }
111
112 const Scalar max_box_length = (bbox_maxs - bbox_mins).maxCoeff();
113 constexpr Int MAX_AREA = std::numeric_limits<Int>::max();
114 constexpr Int RESOLUTION = 1 << 12;
115 Int max_canvas_size = RESOLUTION;
116 Int min_canvas_size = RESOLUTION;
117
118 // The scale factor normalizes the boxes such that the largest box fits into a
119 // canvas of size max_canvas_size.
120 const Scalar scale =
121 max_box_length > safe_cast<Scalar>(1e-12) ? max_box_length / max_canvas_size : 1;
122 la_runtime_assert(std::isfinite(scale));
123 logger().trace("Scale: {}", scale);
124 la_debug_assert(product_will_overflow<Int>(2, MAX_AREA));
125
126 std::vector<::rbp::RectSize> boxes;
127 boxes.reserve(num_boxes);
128 for (auto i : range(num_boxes)) {
129 boxes.emplace_back();
130 auto& box = boxes.back();
131 if (bbox_maxs(i, 0) < bbox_mins(i, 0) || bbox_maxs(i, 1) < bbox_mins(i, 1)) {
132 // Invalid bounding box.
133 logger().warn("Skipping invalid bounding box (index {})!", i);
134 box.width = 0;
135 box.height = 0;
136 } else {
137 box.width = safe_cast<Int>(std::ceil((bbox_maxs(i, 0) - bbox_mins(i, 0)) / scale));
138 box.height = safe_cast<Int>(std::ceil((bbox_maxs(i, 1) - bbox_mins(i, 1)) / scale));
139 la_debug_assert(!product_will_overflow<Int>(box.width, box.height));
140 }
141 }
142
143 Eigen::Matrix<Scalar, Eigen::Dynamic, 2, Eigen::RowMajor> centers(num_boxes, 2);
144 std::vector<bool> rotated(num_boxes);
145
146 auto pack = [&](int L, bool trial) -> bool {
147 la_debug_assert(!product_will_overflow<Int>(L, L));
148#ifdef RECTANGLE_BIN_PACK_OSS
149 if (!allow_rotation) {
150 logger().warn(
151 "Disabling rotation is not supported with this version of RectangleBinPack!");
152 }
153 rbp::GuillotineBinPack packer(L, L);
154#else
155 rbp::GuillotineBinPack packer(L, L, allow_rotation);
156#endif
157 Int int_margin = std::max<Int>(2, static_cast<Int>(std::ceil(margin * L)));
158 for (auto i : range(num_boxes)) {
159 auto rect = packer.Insert(
160 boxes[i].width + int_margin,
161 boxes[i].height + int_margin,
162 false, // Perform empty space merging for defragmentation.
163 rbp::GuillotineBinPack::FreeRectChoiceHeuristic::RectBestAreaFit,
164 rbp::GuillotineBinPack::GuillotineSplitHeuristic::SplitMinimizeArea);
165 if (rect.width == 0 || rect.height == 0) {
166 return false;
167 }
168 }
169 const auto& packed_rect = packer.GetUsedRectangles();
170 logger().debug("num packed rectangles {}, expecting {}", packed_rect.size(), num_boxes);
171 if (static_cast<decltype(num_boxes)>(packed_rect.size()) != num_boxes) return false;
172
173 if (!trial) {
174 for (auto i : range(num_boxes)) {
175 const auto& r = packed_rect[i];
176 rotated[i] = r.width != boxes[i].width + int_margin;
177 la_debug_assert(allow_rotation || !rotated[i]);
178 centers(i, 0) = (r.x + r.width * 0.5f) * scale;
179 centers(i, 1) = (r.y + r.height * 0.5f) * scale;
180 }
181 }
182 return true;
183 };
184
185 logger().trace("Minimum canvas size: {}", min_canvas_size);
186 logger().trace("Maximum canvas size: {}", max_canvas_size);
187 // Find max_canvas_size large enough to fit all boxes.
188 while (!pack(max_canvas_size, true)) {
189 min_canvas_size = max_canvas_size;
190 max_canvas_size *= 2;
191 if (MAX_AREA / max_canvas_size <= max_canvas_size) {
192 // Ops, run out of bound.
193 throw PackingFailure("Cannot pack even with canvas at max area!");
194 }
195 }
196 logger().trace("Minimum canvas size: {}", min_canvas_size);
197 logger().trace("Maximum canvas size: {}", max_canvas_size);
198 la_runtime_assert(max_canvas_size > 0);
199 // Binary search for the smallest max_canvas_size that fits.
200 while (min_canvas_size < max_canvas_size) {
201 const auto l = (min_canvas_size + max_canvas_size) / 2;
202 if (l == min_canvas_size) {
203 break;
204 }
205 if (pack(l, true)) {
206 max_canvas_size = l;
207 } else {
208 min_canvas_size = l;
209 }
210 }
211 la_runtime_assert(max_canvas_size > 0);
212 bool r = pack(max_canvas_size, false);
214 logger().trace("Minimum canvas size: {}", min_canvas_size);
215 logger().trace("Maximum canvas size: {}", max_canvas_size);
216
217 return std::make_tuple(centers, rotated, static_cast<Scalar>(max_canvas_size) * scale);
218}
219
220} // namespace lagrange::packing
Definition pack_boxes.h:67
LA_CORE_API spdlog::logger & logger()
Retrieves the current logger.
Definition Logger.cpp:40
@ Scalar
Mesh attribute must have exactly 1 channel.
Definition AttributeFwd.h:56
#define la_runtime_assert(...)
Runtime assertion check.
Definition assert.h:174
#define la_debug_assert(...)
Debug assertion check.
Definition assert.h:194
internal::Range< Index > range(Index end)
Returns an iterable object representing the range [0, end).
Definition range.h:176
constexpr auto safe_cast(SourceType value) -> std::enable_if_t<!std::is_same< SourceType, TargetType >::value, TargetType >
Perform safe cast from SourceType to TargetType, where "safe" means:
Definition safe_cast.h:50