@flighthq/binpack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/packRectangles.d.ts +3 -0
- package/dist/packRectangles.d.ts.map +1 -0
- package/dist/packRectangles.js +269 -0
- package/dist/packRectangles.js.map +1 -0
- package/package.json +38 -0
- package/src/packRectangles.test.ts +189 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packRectangles.d.ts","sourceRoot":"","sources":["../src/packRectangles.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAmB,UAAU,EAAe,MAAM,iBAAiB,CAAC;AAiBnH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,QAAQ,CAAC,iBAAiB,CAAC,EAAE,EAC7C,OAAO,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,GACjC,UAAU,CAuDZ"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { intersectsRectangle } from '@flighthq/geometry';
|
|
2
|
+
// Places a set of rectangles without overlap into a bin using the MaxRects algorithm with the
|
|
3
|
+
// Best-Short-Side-Fit (BSSF) heuristic, and reports each placement, the used bin extent, and any ids
|
|
4
|
+
// that did not fit.
|
|
5
|
+
//
|
|
6
|
+
// The result is deterministic: the same `rects` (by value) and `options` always produce an identical
|
|
7
|
+
// `PackResult`. Inputs are sorted by descending area, then descending height, then width, then id
|
|
8
|
+
// before placement — a total order with no reliance on sort stability — and no `Math.random`/`Date`
|
|
9
|
+
// is consulted.
|
|
10
|
+
//
|
|
11
|
+
// Padding and border are honored geometrically: every pair of placements is at least `padding` apart
|
|
12
|
+
// and every placement is at least `border` from the reported bin edge. When `allowRotation` is set, a
|
|
13
|
+
// rectangle may be turned 90° (reported via `rotated` and swapped `width`/`height`) if that fits
|
|
14
|
+
// better. A `growable` bin starts small and grows toward `maxWidth`/`maxHeight`; a fixed bin overflows
|
|
15
|
+
// into `unpacked`. The reported `width`/`height` is the tight used extent, then adjusted for `square`
|
|
16
|
+
// and `powerOfTwo` when those are set.
|
|
17
|
+
export function packRectangles(rects, options) {
|
|
18
|
+
const padding = options?.padding ?? 0;
|
|
19
|
+
const border = options?.border ?? 0;
|
|
20
|
+
const allowRotation = options?.allowRotation ?? false;
|
|
21
|
+
const growable = options?.growable ?? true;
|
|
22
|
+
const powerOfTwo = options?.powerOfTwo ?? false;
|
|
23
|
+
const square = options?.square ?? false;
|
|
24
|
+
const maxWidth = options?.maxWidth ?? DEFAULT_MAX_EXTENT;
|
|
25
|
+
const maxHeight = options?.maxHeight ?? DEFAULT_MAX_EXTENT;
|
|
26
|
+
if (rects.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
placements: [],
|
|
29
|
+
width: finalizeExtent(0, powerOfTwo),
|
|
30
|
+
height: finalizeExtent(0, powerOfTwo),
|
|
31
|
+
unpacked: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const sorted = sortRectanglesForPacking(rects);
|
|
35
|
+
// The smallest bin each dimension must reach for a rectangle to fit at all, accounting for the
|
|
36
|
+
// border on both sides. A rectangle wider than `maxWidth` (and, if rotatable, taller than
|
|
37
|
+
// `maxHeight` in its other orientation) can never be placed and lands in `unpacked`.
|
|
38
|
+
let needWidth = 2 * border;
|
|
39
|
+
let needHeight = 2 * border;
|
|
40
|
+
let totalArea = 0;
|
|
41
|
+
for (const rect of sorted) {
|
|
42
|
+
const shortSide = Math.min(rect.width, rect.height);
|
|
43
|
+
const requiredWidth = (allowRotation ? shortSide : rect.width) + 2 * border;
|
|
44
|
+
const requiredHeight = (allowRotation ? shortSide : rect.height) + 2 * border;
|
|
45
|
+
needWidth = Math.max(needWidth, requiredWidth);
|
|
46
|
+
needHeight = Math.max(needHeight, requiredHeight);
|
|
47
|
+
totalArea += (rect.width + padding) * (rect.height + padding);
|
|
48
|
+
}
|
|
49
|
+
// A fixed bin is exactly the cap; a growable bin starts at a square seed sized to the total area (but
|
|
50
|
+
// at least large enough to hold the largest single rectangle) and grows from there.
|
|
51
|
+
const seed = Math.ceil(Math.sqrt(totalArea)) + 2 * border;
|
|
52
|
+
let binWidth = growable ? Math.min(Math.max(seed, needWidth), maxWidth) : maxWidth;
|
|
53
|
+
let binHeight = growable ? Math.min(Math.max(seed, needHeight), maxHeight) : maxHeight;
|
|
54
|
+
let attempt = packIntoBin(sorted, binWidth, binHeight, padding, border, allowRotation);
|
|
55
|
+
while (attempt.unpacked.length > 0 && growable && (binWidth < maxWidth || binHeight < maxHeight)) {
|
|
56
|
+
if (binWidth <= binHeight && binWidth < maxWidth) {
|
|
57
|
+
binWidth = Math.min(binWidth * 2, maxWidth);
|
|
58
|
+
}
|
|
59
|
+
else if (binHeight < maxHeight) {
|
|
60
|
+
binHeight = Math.min(binHeight * 2, maxHeight);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
binWidth = Math.min(binWidth * 2, maxWidth);
|
|
64
|
+
}
|
|
65
|
+
attempt = packIntoBin(sorted, binWidth, binHeight, padding, border, allowRotation);
|
|
66
|
+
}
|
|
67
|
+
return finalizeResult(attempt, border, powerOfTwo, square);
|
|
68
|
+
}
|
|
69
|
+
// Rounds `value` up to the next power of two (>= 1). Used only for the final `powerOfTwo` adjustment.
|
|
70
|
+
function ceilToPowerOfTwo(value) {
|
|
71
|
+
if (value <= 1)
|
|
72
|
+
return 1;
|
|
73
|
+
let result = 1;
|
|
74
|
+
while (result < value)
|
|
75
|
+
result *= 2;
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
// Turns the raw placements from `packIntoBin` into the public `PackResult`, shifting effective-space
|
|
79
|
+
// coordinates back into bin space by `border` and finalizing the reported extent for `square` /
|
|
80
|
+
// `powerOfTwo`.
|
|
81
|
+
function finalizeResult(packed, border, powerOfTwo, square) {
|
|
82
|
+
let contentRight = 0;
|
|
83
|
+
let contentBottom = 0;
|
|
84
|
+
for (const placement of packed.placements) {
|
|
85
|
+
contentRight = Math.max(contentRight, placement.x + placement.width);
|
|
86
|
+
contentBottom = Math.max(contentBottom, placement.y + placement.height);
|
|
87
|
+
}
|
|
88
|
+
let width = packed.placements.length > 0 ? contentRight + border : 0;
|
|
89
|
+
let height = packed.placements.length > 0 ? contentBottom + border : 0;
|
|
90
|
+
if (square) {
|
|
91
|
+
width = Math.max(width, height);
|
|
92
|
+
height = width;
|
|
93
|
+
}
|
|
94
|
+
width = finalizeExtent(width, powerOfTwo);
|
|
95
|
+
height = finalizeExtent(height, powerOfTwo);
|
|
96
|
+
if (square) {
|
|
97
|
+
width = Math.max(width, height);
|
|
98
|
+
height = width;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
placements: packed.placements.map((placement) => ({ ...placement })),
|
|
102
|
+
width,
|
|
103
|
+
height,
|
|
104
|
+
unpacked: [...packed.unpacked],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Applies the `powerOfTwo` rounding (if set) to a single extent value.
|
|
108
|
+
function finalizeExtent(value, powerOfTwo) {
|
|
109
|
+
return powerOfTwo && value > 0 ? ceilToPowerOfTwo(value) : value;
|
|
110
|
+
}
|
|
111
|
+
// Best-Short-Side-Fit search: over every free rectangle (and, when rotation is allowed, both
|
|
112
|
+
// orientations of the piece), pick the fit that leaves the smallest leftover short side, breaking
|
|
113
|
+
// ties by smallest leftover long side, then by top-most / left-most free rectangle, then by the
|
|
114
|
+
// unrotated orientation. Returns the chosen placement, or `null` when the piece fits nowhere.
|
|
115
|
+
//
|
|
116
|
+
// `pieceWidth`/`pieceHeight` are the effective footprint (rectangle size plus the trailing padding
|
|
117
|
+
// gutter) in the unrotated orientation.
|
|
118
|
+
function findBestPlacement(free, pieceWidth, pieceHeight, allowRotation) {
|
|
119
|
+
let best = null;
|
|
120
|
+
let bestShort = Number.POSITIVE_INFINITY;
|
|
121
|
+
let bestLong = Number.POSITIVE_INFINITY;
|
|
122
|
+
for (const node of free) {
|
|
123
|
+
// Two candidate orientations: unrotated, then (optionally) the 90° turn.
|
|
124
|
+
for (let rotated = 0; rotated <= (allowRotation ? 1 : 0); rotated++) {
|
|
125
|
+
const width = rotated ? pieceHeight : pieceWidth;
|
|
126
|
+
const height = rotated ? pieceWidth : pieceHeight;
|
|
127
|
+
if (width > node.width || height > node.height)
|
|
128
|
+
continue;
|
|
129
|
+
const leftoverHorizontal = node.width - width;
|
|
130
|
+
const leftoverVertical = node.height - height;
|
|
131
|
+
const shortSide = Math.min(leftoverHorizontal, leftoverVertical);
|
|
132
|
+
const longSide = Math.max(leftoverHorizontal, leftoverVertical);
|
|
133
|
+
if (best === null || shortSide < bestShort || (shortSide === bestShort && longSide < bestLong)) {
|
|
134
|
+
best = { x: node.x, y: node.y, footprintWidth: width, footprintHeight: height, rotated: rotated === 1 };
|
|
135
|
+
bestShort = shortSide;
|
|
136
|
+
bestLong = longSide;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return best;
|
|
141
|
+
}
|
|
142
|
+
// True when the whole of `inner` lies within `outer` — the containment test used to prune redundant
|
|
143
|
+
// free rectangles after a split.
|
|
144
|
+
function isFreeRectangleContained(inner, outer) {
|
|
145
|
+
return (inner.x >= outer.x &&
|
|
146
|
+
inner.y >= outer.y &&
|
|
147
|
+
inner.x + inner.width <= outer.x + outer.width &&
|
|
148
|
+
inner.y + inner.height <= outer.y + outer.height);
|
|
149
|
+
}
|
|
150
|
+
// Runs one MaxRects pass at a fixed bin size. Every rectangle is inflated to an "effective" footprint
|
|
151
|
+
// of `(width + padding) x (height + padding)` — the trailing gutter is what guarantees at least
|
|
152
|
+
// `padding` between neighbors — and packed into the effective usable region
|
|
153
|
+
// `(binWidth - 2*border + padding) x (binHeight - 2*border + padding)`. This construction makes the
|
|
154
|
+
// actual placement satisfy `x >= border` and `x + width <= binWidth - border` (likewise vertically),
|
|
155
|
+
// so `border` is respected on every side while the last row/column's gutter costs no real space.
|
|
156
|
+
function packIntoBin(sorted, binWidth, binHeight, padding, border, allowRotation) {
|
|
157
|
+
const usableWidth = binWidth - 2 * border + padding;
|
|
158
|
+
const usableHeight = binHeight - 2 * border + padding;
|
|
159
|
+
const placements = [];
|
|
160
|
+
const unpacked = [];
|
|
161
|
+
if (usableWidth <= 0 || usableHeight <= 0) {
|
|
162
|
+
for (const rect of sorted)
|
|
163
|
+
unpacked.push(rect.id);
|
|
164
|
+
return { placements, unpacked };
|
|
165
|
+
}
|
|
166
|
+
const free = [{ x: 0, y: 0, width: usableWidth, height: usableHeight }];
|
|
167
|
+
for (const rect of sorted) {
|
|
168
|
+
const pieceWidth = rect.width + padding;
|
|
169
|
+
const pieceHeight = rect.height + padding;
|
|
170
|
+
const placement = findBestPlacement(free, pieceWidth, pieceHeight, allowRotation);
|
|
171
|
+
if (placement === null) {
|
|
172
|
+
unpacked.push(rect.id);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
placements.push({
|
|
176
|
+
id: rect.id,
|
|
177
|
+
x: placement.x + border,
|
|
178
|
+
y: placement.y + border,
|
|
179
|
+
width: placement.rotated ? rect.height : rect.width,
|
|
180
|
+
height: placement.rotated ? rect.width : rect.height,
|
|
181
|
+
rotated: placement.rotated,
|
|
182
|
+
});
|
|
183
|
+
splitFreeRectangles(free, placement);
|
|
184
|
+
pruneFreeRectangles(free);
|
|
185
|
+
}
|
|
186
|
+
return { placements, unpacked };
|
|
187
|
+
}
|
|
188
|
+
// Removes any free rectangle fully contained within another. MaxRects splitting can leave redundant
|
|
189
|
+
// free rectangles nested inside larger ones; pruning them keeps the search cheap and the free list
|
|
190
|
+
// non-redundant.
|
|
191
|
+
function pruneFreeRectangles(free) {
|
|
192
|
+
for (let i = 0; i < free.length; i++) {
|
|
193
|
+
for (let j = i + 1; j < free.length; j++) {
|
|
194
|
+
if (isFreeRectangleContained(free[i], free[j])) {
|
|
195
|
+
free.splice(i, 1);
|
|
196
|
+
i--;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
if (isFreeRectangleContained(free[j], free[i])) {
|
|
200
|
+
free.splice(j, 1);
|
|
201
|
+
j--;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Deterministic total order over the input rectangles: descending area, then descending height, then
|
|
207
|
+
// descending width, then ascending id (numbers before strings; numbers numerically, strings
|
|
208
|
+
// lexicographically). Returns a new array; the input is not mutated.
|
|
209
|
+
function sortRectanglesForPacking(rects) {
|
|
210
|
+
return [...rects].sort((a, b) => {
|
|
211
|
+
const areaA = a.width * a.height;
|
|
212
|
+
const areaB = b.width * b.height;
|
|
213
|
+
if (areaA !== areaB)
|
|
214
|
+
return areaB - areaA;
|
|
215
|
+
if (a.height !== b.height)
|
|
216
|
+
return b.height - a.height;
|
|
217
|
+
if (a.width !== b.width)
|
|
218
|
+
return b.width - a.width;
|
|
219
|
+
return compareRectangleId(a.id, b.id);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// MaxRects splitting: for every free rectangle overlapping the just-placed footprint, replace it with
|
|
223
|
+
// the (up to four) sub-rectangles covering the parts of it the footprint does not cover. The footprint
|
|
224
|
+
// is the effective size (rectangle plus padding gutter), so the reserved padding is carved out of the
|
|
225
|
+
// free space here.
|
|
226
|
+
function splitFreeRectangles(free, placement) {
|
|
227
|
+
const usedX = placement.x;
|
|
228
|
+
const usedY = placement.y;
|
|
229
|
+
const usedRight = placement.x + placement.footprintWidth;
|
|
230
|
+
const usedBottom = placement.y + placement.footprintHeight;
|
|
231
|
+
const used = {
|
|
232
|
+
x: usedX,
|
|
233
|
+
y: usedY,
|
|
234
|
+
width: placement.footprintWidth,
|
|
235
|
+
height: placement.footprintHeight,
|
|
236
|
+
};
|
|
237
|
+
for (let i = free.length - 1; i >= 0; i--) {
|
|
238
|
+
const node = free[i];
|
|
239
|
+
if (!intersectsRectangle(node, used))
|
|
240
|
+
continue;
|
|
241
|
+
const nodeRight = node.x + node.width;
|
|
242
|
+
const nodeBottom = node.y + node.height;
|
|
243
|
+
free.splice(i, 1);
|
|
244
|
+
if (usedX > node.x)
|
|
245
|
+
free.push({ x: node.x, y: node.y, width: usedX - node.x, height: node.height });
|
|
246
|
+
if (usedRight < nodeRight)
|
|
247
|
+
free.push({ x: usedRight, y: node.y, width: nodeRight - usedRight, height: node.height });
|
|
248
|
+
if (usedY > node.y)
|
|
249
|
+
free.push({ x: node.x, y: node.y, width: node.width, height: usedY - node.y });
|
|
250
|
+
if (usedBottom < nodeBottom) {
|
|
251
|
+
free.push({ x: node.x, y: usedBottom, width: node.width, height: nodeBottom - usedBottom });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Orders two rectangle ids for the deterministic input sort: numbers before strings, numbers
|
|
256
|
+
// ascending, strings lexicographically.
|
|
257
|
+
function compareRectangleId(a, b) {
|
|
258
|
+
const aNumber = typeof a === 'number';
|
|
259
|
+
const bNumber = typeof b === 'number';
|
|
260
|
+
if (aNumber && bNumber)
|
|
261
|
+
return a - b;
|
|
262
|
+
if (aNumber !== bNumber)
|
|
263
|
+
return aNumber ? -1 : 1;
|
|
264
|
+
return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
|
|
265
|
+
}
|
|
266
|
+
// The default width/height cap when `maxWidth`/`maxHeight` are unset — large enough to be effectively
|
|
267
|
+
// unbounded for typical inputs while keeping growth from running away.
|
|
268
|
+
const DEFAULT_MAX_EXTENT = 16384;
|
|
269
|
+
//# sourceMappingURL=packRectangles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packRectangles.js","sourceRoot":"","sources":["../src/packRectangles.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/binpack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/geometry": "0.1.0",
|
|
31
|
+
"@flighthq/types": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"description": "2D rectangle bin-packing — MaxRects placement with padding/border, optional power-of-two/square/rotation, and a fixed or growable bin; plain-data rectangles in, placements + used extent + unpacked out",
|
|
37
|
+
"sideEffects": false
|
|
38
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { PackableRectangle, PackedRectangle } from '@flighthq/types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { packRectangles } from './packRectangles';
|
|
5
|
+
|
|
6
|
+
describe('packRectangles', () => {
|
|
7
|
+
it('places ~20 varied rectangles with no pairwise overlap, inside the bin, at their input size', () => {
|
|
8
|
+
const rects: PackableRectangle[] = [];
|
|
9
|
+
for (let i = 0; i < 20; i++) {
|
|
10
|
+
rects.push({ id: i, width: 8 + ((i * 7) % 40), height: 6 + ((i * 13) % 34) });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const result = packRectangles(rects, { padding: 1, border: 2 });
|
|
14
|
+
expect(result.unpacked).toEqual([]);
|
|
15
|
+
expect(result.placements).toHaveLength(20);
|
|
16
|
+
|
|
17
|
+
const sizeById = new Map(rects.map((rect) => [rect.id, rect] as const));
|
|
18
|
+
for (const placement of result.placements) {
|
|
19
|
+
const input = sizeById.get(placement.id)!;
|
|
20
|
+
const expectedWidth = placement.rotated ? input.height : input.width;
|
|
21
|
+
const expectedHeight = placement.rotated ? input.width : input.height;
|
|
22
|
+
expect(placement.width).toBe(expectedWidth);
|
|
23
|
+
expect(placement.height).toBe(expectedHeight);
|
|
24
|
+
|
|
25
|
+
expect(placement.x).toBeGreaterThanOrEqual(2);
|
|
26
|
+
expect(placement.y).toBeGreaterThanOrEqual(2);
|
|
27
|
+
expect(placement.x + placement.width).toBeLessThanOrEqual(result.width - 2);
|
|
28
|
+
expect(placement.y + placement.height).toBeLessThanOrEqual(result.height - 2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < result.placements.length; i++) {
|
|
32
|
+
for (let j = i + 1; j < result.placements.length; j++) {
|
|
33
|
+
expect(rectanglesOverlap(result.placements[i], result.placements[j])).toBe(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('leaves nothing unpacked when everything fits in a growable bin', () => {
|
|
39
|
+
const rects: PackableRectangle[] = [
|
|
40
|
+
{ id: 'a', width: 30, height: 20 },
|
|
41
|
+
{ id: 'b', width: 40, height: 40 },
|
|
42
|
+
{ id: 'c', width: 10, height: 60 },
|
|
43
|
+
{ id: 'd', width: 25, height: 25 },
|
|
44
|
+
{ id: 'e', width: 50, height: 15 },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const result = packRectangles(rects);
|
|
48
|
+
expect(result.unpacked).toEqual([]);
|
|
49
|
+
expect(result.placements).toHaveLength(5);
|
|
50
|
+
for (let i = 0; i < result.placements.length; i++) {
|
|
51
|
+
for (let j = i + 1; j < result.placements.length; j++) {
|
|
52
|
+
expect(rectanglesOverlap(result.placements[i], result.placements[j])).toBe(false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('reports overflow ids in a fixed bin and keeps the placed rectangles non-overlapping', () => {
|
|
58
|
+
const rects: PackableRectangle[] = [];
|
|
59
|
+
for (let i = 0; i < 12; i++) {
|
|
60
|
+
rects.push({ id: i, width: 20, height: 20 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = packRectangles(rects, { growable: false, maxWidth: 44, maxHeight: 44 });
|
|
64
|
+
expect(result.unpacked.length).toBeGreaterThan(0);
|
|
65
|
+
expect(result.placements.length + result.unpacked.length).toBe(12);
|
|
66
|
+
for (let i = 0; i < result.placements.length; i++) {
|
|
67
|
+
for (let j = i + 1; j < result.placements.length; j++) {
|
|
68
|
+
expect(rectanglesOverlap(result.placements[i], result.placements[j])).toBe(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('respects padding between neighbors and border at the bin edge', () => {
|
|
74
|
+
const rects: PackableRectangle[] = [
|
|
75
|
+
{ id: 'a', width: 10, height: 10 },
|
|
76
|
+
{ id: 'b', width: 10, height: 10 },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const result = packRectangles(rects, { padding: 2, border: 4 });
|
|
80
|
+
expect(result.unpacked).toEqual([]);
|
|
81
|
+
|
|
82
|
+
const a = result.placements.find((p) => p.id === 'a')!;
|
|
83
|
+
const b = result.placements.find((p) => p.id === 'b')!;
|
|
84
|
+
expect(a).toEqual({ id: 'a', x: 4, y: 4, width: 10, height: 10, rotated: false });
|
|
85
|
+
expect(b).toEqual({ id: 'b', x: 16, y: 4, width: 10, height: 10, rotated: false });
|
|
86
|
+
|
|
87
|
+
// Gap between the two neighbors is exactly the padding.
|
|
88
|
+
expect(b.x - (a.x + a.width)).toBe(2);
|
|
89
|
+
// Every placement is at least `border` from every bin edge.
|
|
90
|
+
for (const placement of result.placements) {
|
|
91
|
+
expect(placement.x).toBeGreaterThanOrEqual(4);
|
|
92
|
+
expect(placement.y).toBeGreaterThanOrEqual(4);
|
|
93
|
+
expect(placement.x + placement.width).toBeLessThanOrEqual(result.width - 4);
|
|
94
|
+
expect(placement.y + placement.height).toBeLessThanOrEqual(result.height - 4);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('reports power-of-two and square extents that still contain every placement', () => {
|
|
99
|
+
const rects: PackableRectangle[] = [
|
|
100
|
+
{ id: 'a', width: 30, height: 20 },
|
|
101
|
+
{ id: 'b', width: 17, height: 41 },
|
|
102
|
+
{ id: 'c', width: 25, height: 9 },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const result = packRectangles(rects, { powerOfTwo: true, square: true });
|
|
106
|
+
expect(isPowerOfTwo(result.width)).toBe(true);
|
|
107
|
+
expect(isPowerOfTwo(result.height)).toBe(true);
|
|
108
|
+
expect(result.width).toBe(result.height);
|
|
109
|
+
|
|
110
|
+
for (const placement of result.placements) {
|
|
111
|
+
expect(placement.x + placement.width).toBeLessThanOrEqual(result.width);
|
|
112
|
+
expect(placement.y + placement.height).toBeLessThanOrEqual(result.height);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rotates a rectangle when rotation is required to fit a fixed bin', () => {
|
|
117
|
+
const rects: PackableRectangle[] = [{ id: 'tall', width: 8, height: 20 }];
|
|
118
|
+
const options = { growable: false as const, maxWidth: 20, maxHeight: 8 };
|
|
119
|
+
|
|
120
|
+
const withRotation = packRectangles(rects, { ...options, allowRotation: true });
|
|
121
|
+
expect(withRotation.unpacked).toEqual([]);
|
|
122
|
+
expect(withRotation.placements).toHaveLength(1);
|
|
123
|
+
const placed = withRotation.placements[0];
|
|
124
|
+
expect(placed.rotated).toBe(true);
|
|
125
|
+
expect(placed.width).toBe(20);
|
|
126
|
+
expect(placed.height).toBe(8);
|
|
127
|
+
|
|
128
|
+
const withoutRotation = packRectangles(rects, { ...options, allowRotation: false });
|
|
129
|
+
expect(withoutRotation.placements).toEqual([]);
|
|
130
|
+
expect(withoutRotation.unpacked).toEqual(['tall']);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('grows to fit a rectangle that would need rotation in a fixed bin', () => {
|
|
134
|
+
const rects: PackableRectangle[] = [{ id: 'tall', width: 8, height: 20 }];
|
|
135
|
+
const result = packRectangles(rects, { allowRotation: false });
|
|
136
|
+
expect(result.unpacked).toEqual([]);
|
|
137
|
+
expect(result.placements[0].rotated).toBe(false);
|
|
138
|
+
expect(result.placements[0].width).toBe(8);
|
|
139
|
+
expect(result.placements[0].height).toBe(20);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('produces a deep-equal result for the same input packed twice', () => {
|
|
143
|
+
const rects: PackableRectangle[] = [];
|
|
144
|
+
for (let i = 0; i < 15; i++) {
|
|
145
|
+
rects.push({ id: `r${i}`, width: 5 + ((i * 11) % 30), height: 5 + ((i * 17) % 28) });
|
|
146
|
+
}
|
|
147
|
+
const options = { padding: 1, border: 3, allowRotation: true };
|
|
148
|
+
|
|
149
|
+
const first = packRectangles(rects, options);
|
|
150
|
+
const second = packRectangles(rects, options);
|
|
151
|
+
expect(second).toEqual(first);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns an empty result with a zero-size bin for empty input', () => {
|
|
155
|
+
const result = packRectangles([]);
|
|
156
|
+
expect(result).toEqual({ placements: [], width: 0, height: 0, unpacked: [] });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('packs a single rectangle at the border corner', () => {
|
|
160
|
+
const result = packRectangles([{ id: 'only', width: 12, height: 7 }], { border: 3 });
|
|
161
|
+
expect(result.unpacked).toEqual([]);
|
|
162
|
+
expect(result.placements).toEqual([{ id: 'only', x: 3, y: 3, width: 12, height: 7, rotated: false }]);
|
|
163
|
+
expect(result.width).toBe(18);
|
|
164
|
+
expect(result.height).toBe(13);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('reports a rectangle larger than a fixed bin as unpacked', () => {
|
|
168
|
+
const result = packRectangles([{ id: 'big', width: 500, height: 500 }], {
|
|
169
|
+
growable: false,
|
|
170
|
+
maxWidth: 64,
|
|
171
|
+
maxHeight: 64,
|
|
172
|
+
});
|
|
173
|
+
expect(result.placements).toEqual([]);
|
|
174
|
+
expect(result.unpacked).toEqual(['big']);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('reports a rectangle larger than the growth cap as unpacked', () => {
|
|
178
|
+
const result = packRectangles([{ id: 'huge', width: 200, height: 10 }], { maxWidth: 64, maxHeight: 64 });
|
|
179
|
+
expect(result.unpacked).toEqual(['huge']);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
function isPowerOfTwo(value: number): boolean {
|
|
184
|
+
return value > 0 && (value & (value - 1)) === 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function rectanglesOverlap(a: Readonly<PackedRectangle>, b: Readonly<PackedRectangle>): boolean {
|
|
188
|
+
return !(a.x + a.width <= b.x || b.x + b.width <= a.x || a.y + a.height <= b.y || b.y + b.height <= a.y);
|
|
189
|
+
}
|