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 // Degenerate bounding box (max < min in at least one dimension). Most often this
133 // means the caller reserved a slot for a chart id that has no geometry
134 logger().warn(
135 "Skipping degenerate bounding box at index {} (chart id with no geometry)",
136 i);
137 box.width = 0;
138 box.height = 0;
139 } else {
140 box.width = safe_cast<Int>(std::ceil((bbox_maxs(i, 0) - bbox_mins(i, 0)) / scale));
141 box.height = safe_cast<Int>(std::ceil((bbox_maxs(i, 1) - bbox_mins(i, 1)) / scale));
142 la_debug_assert(!product_will_overflow<Int>(box.width, box.height));
143 }
144 }
145
146 Eigen::Matrix<Scalar, Eigen::Dynamic, 2, Eigen::RowMajor> centers(num_boxes, 2);
147 std::vector<bool> rotated(num_boxes);
148
149 auto pack = [&](int L, bool trial) -> bool {
150 la_debug_assert(!product_will_overflow<Int>(L, L));
151#ifdef RECTANGLE_BIN_PACK_OSS
152 if (!allow_rotation) {
153 logger().warn(
154 "Disabling rotation is not supported with this version of RectangleBinPack!");
155 }
156 rbp::GuillotineBinPack packer(L, L);
157#else
158 rbp::GuillotineBinPack packer(L, L, allow_rotation);
159#endif
160 Int int_margin = std::max<Int>(2, static_cast<Int>(std::ceil(margin * L)));
161 for (auto i : range(num_boxes)) {
162 auto rect = packer.Insert(
163 boxes[i].width + int_margin,
164 boxes[i].height + int_margin,
165 false, // Perform empty space merging for defragmentation.
166 rbp::GuillotineBinPack::FreeRectChoiceHeuristic::RectBestAreaFit,
167 rbp::GuillotineBinPack::GuillotineSplitHeuristic::SplitMinimizeArea);
168 if (rect.width == 0 || rect.height == 0) {
169 return false;
170 }
171 }
172 const auto& packed_rect = packer.GetUsedRectangles();
173 logger().debug("num packed rectangles {}, expecting {}", packed_rect.size(), num_boxes);
174 if (static_cast<decltype(num_boxes)>(packed_rect.size()) != num_boxes) return false;
175
176 if (!trial) {
177 for (auto i : range(num_boxes)) {
178 const auto& r = packed_rect[i];
179 rotated[i] = r.width != boxes[i].width + int_margin;
180 la_debug_assert(allow_rotation || !rotated[i]);
181 centers(i, 0) = (r.x + r.width * 0.5f) * scale;
182 centers(i, 1) = (r.y + r.height * 0.5f) * scale;
183 }
184 }
185 return true;
186 };
187
188 logger().trace("Minimum canvas size: {}", min_canvas_size);
189 logger().trace("Maximum canvas size: {}", max_canvas_size);
190 // Find max_canvas_size large enough to fit all boxes.
191 while (!pack(max_canvas_size, true)) {
192 min_canvas_size = max_canvas_size;
193 max_canvas_size *= 2;
194 if (MAX_AREA / max_canvas_size <= max_canvas_size) {
195 // Ops, run out of bound.
196 throw PackingFailure("Cannot pack even with canvas at max area!");
197 }
198 }
199 logger().trace("Minimum canvas size: {}", min_canvas_size);
200 logger().trace("Maximum canvas size: {}", max_canvas_size);
201 la_runtime_assert(max_canvas_size > 0);
202 // Binary search for the smallest max_canvas_size that fits.
203 while (min_canvas_size < max_canvas_size) {
204 const auto l = (min_canvas_size + max_canvas_size) / 2;
205 if (l == min_canvas_size) {
206 break;
207 }
208 if (pack(l, true)) {
209 max_canvas_size = l;
210 } else {
211 min_canvas_size = l;
212 }
213 }
214 la_runtime_assert(max_canvas_size > 0);
215 bool r = pack(max_canvas_size, false);
217 logger().trace("Minimum canvas size: {}", min_canvas_size);
218 logger().trace("Maximum canvas size: {}", max_canvas_size);
219
220 return std::make_tuple(centers, rotated, static_cast<Scalar>(max_canvas_size) * scale);
221}
222
223} // 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:175
#define la_debug_assert(...)
Debug assertion check.
Definition assert.h:195
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:51