@mark1russell7/packing 0.2.4 → 0.2.5
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/README.md +17 -2
- package/build/index.d.ts +87 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +516 -41
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters.ts +97 -32
- package/src/frame.ts +37 -10
- package/src/index.ts +12 -1
- package/src/maxrects.ts +367 -0
- package/src/moves.ts +15 -0
- package/src/pack.ts +14 -4
- package/src/registry.ts +2 -2
- package/src/space.ts +36 -1
package/src/maxrects.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// The MaxRects family of roadmap item k24 (docs/PACKER.md, "The MaxRects family"). The packer keeps a list of the
|
|
2
|
+
// maximal free rectangles. Each box goes to the top-left corner of one free rectangle. A rule chooses the free rectangle.
|
|
3
|
+
// Then the box cuts each free rectangle that it hits into at most four parts, and the list loses each part that is in another.
|
|
4
|
+
|
|
5
|
+
import { countMove, MOVES } from "./moves.ts";
|
|
6
|
+
import { compareRects, type SortOrder } from "./order.ts";
|
|
7
|
+
import type { Rect } from "./rect.ts";
|
|
8
|
+
import { spaceParts, type FreeSpace, type FreeSpan, type SpaceRect } from "./space.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The rules of `maxrects` to choose a free rectangle for a box.
|
|
12
|
+
*
|
|
13
|
+
* - `bssf`: the best short side fit. The smaller leftover side is the smallest.
|
|
14
|
+
* - `blsf`: the best long side fit. The larger leftover side is the smallest.
|
|
15
|
+
* - `baf`: the best area fit. The leftover area is the smallest.
|
|
16
|
+
* - `bl`: the bottom-left rule. The bottom edge of the box is the highest, then the left edge is the leftmost.
|
|
17
|
+
* - `cp`: the contact point rule. The box touches the most length of the edges of the frame, the boxes and the obstacles.
|
|
18
|
+
*/
|
|
19
|
+
export const MAXRECTS_RULES: readonly ["bssf", "blsf", "baf", "bl", "cp"] = Object.freeze(["bssf", "blsf", "baf", "bl", "cp"] as const);
|
|
20
|
+
|
|
21
|
+
/** A rule of `maxrects`: `bssf`, `blsf`, `baf`, `bl` or `cp`. `MAXRECTS_RULES` tells each rule. */
|
|
22
|
+
export type MaxRectsRule = (typeof MAXRECTS_RULES)[number];
|
|
23
|
+
|
|
24
|
+
/** The number of bands that the default band height gives. The default band height is the height to cut divided by this number. */
|
|
25
|
+
export const MAXRECTS_BANDS = 128;
|
|
26
|
+
|
|
27
|
+
/** The options of `packMaxRects` */
|
|
28
|
+
export interface MaxRectsOptions {
|
|
29
|
+
/** The order of the rectangles before the packing. The default is `height-desc`. */
|
|
30
|
+
sortOrder?: SortOrder;
|
|
31
|
+
/**
|
|
32
|
+
* The free space of the container: `freeSpace` of the input. Without it, or when it is plain, the free rectangles are
|
|
33
|
+
* exact: the outer rectangle less the locked rectangles. Another space gives an inner approximation with bands.
|
|
34
|
+
*/
|
|
35
|
+
space?: FreeSpace;
|
|
36
|
+
/** The rule to choose a free rectangle. The default is `bssf`. */
|
|
37
|
+
rule?: MaxRectsRule;
|
|
38
|
+
/**
|
|
39
|
+
* The largest height of a band of the approximation of a space that is not plain. The bands have equal heights. The
|
|
40
|
+
* default is the height to cut divided by `MAXRECTS_BANDS`. A fixed height cuts the height. A free height cuts to the
|
|
41
|
+
* lowest edge of the obstacles that are not rectangles.
|
|
42
|
+
*/
|
|
43
|
+
bandHeight?: number;
|
|
44
|
+
/** The rectangles that can turn by 90 degrees. Without it, no rectangle turns. A locked rectangle does not turn. */
|
|
45
|
+
turnable?: ReadonlySet<unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* The rectangles that the packing turned. Each call turns back the rectangles of this set first, and then it clears the
|
|
48
|
+
* set. Thus a size search can pack the same rectangles many times.
|
|
49
|
+
*/
|
|
50
|
+
turned?: Set<unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A rectangle with its four edges, in the coordinates of the outer rectangle. */
|
|
54
|
+
interface Box {
|
|
55
|
+
x0: number;
|
|
56
|
+
y0: number;
|
|
57
|
+
x1: number;
|
|
58
|
+
y1: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A place for a box: a free rectangle, the turn, and the two scores of the rule. Lower scores are better. */
|
|
62
|
+
interface Choice {
|
|
63
|
+
index: number;
|
|
64
|
+
turn: boolean;
|
|
65
|
+
x: number;
|
|
66
|
+
y: number;
|
|
67
|
+
w: number;
|
|
68
|
+
h: number;
|
|
69
|
+
s1: number;
|
|
70
|
+
s2: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The result of `runMaxRects`: the outer rectangle and the free rectangles at the end. */
|
|
74
|
+
export interface MaxRectsRun<T> {
|
|
75
|
+
outer: Rect<T>;
|
|
76
|
+
/** The free rectangles at the end, in the coordinates of the rectangles. Each one is as wide as the narrowest box and as high as the lowest box. */
|
|
77
|
+
free: SpaceRect[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** This function tells if the box `a` is fully in the box `b`. */
|
|
81
|
+
function inside(a: Box, b: Box): boolean {
|
|
82
|
+
return a.x0 >= b.x0 && a.y0 >= b.y0 && a.x1 <= b.x1 && a.y1 <= b.y1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* This function cuts each free rectangle that the box `b` hits into at most four parts. A part with a side less than
|
|
87
|
+
* `minW` or `minH` goes, because no box fits in it. A part that is in another rectangle of the list goes.
|
|
88
|
+
* The function gives the new list and the number of free rectangles that the box hit.
|
|
89
|
+
*/
|
|
90
|
+
function cut(free: Box[], b: Box, minW: number, minH: number): { free: Box[]; hits: number } {
|
|
91
|
+
const kept: Box[] = [];
|
|
92
|
+
const parts: Box[] = [];
|
|
93
|
+
let hits = 0;
|
|
94
|
+
const add = (x0: number, y0: number, x1: number, y1: number): void => {
|
|
95
|
+
if (x1 - x0 >= minW && y1 - y0 >= minH) parts.push({ x0, y0, x1, y1 });
|
|
96
|
+
};
|
|
97
|
+
for (const f of free) {
|
|
98
|
+
if (b.x0 >= f.x1 || b.x1 <= f.x0 || b.y0 >= f.y1 || b.y1 <= f.y0) {
|
|
99
|
+
kept.push(f);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
hits++;
|
|
103
|
+
if (b.x0 > f.x0) add(f.x0, f.y0, b.x0, f.y1);
|
|
104
|
+
if (b.x1 < f.x1) add(b.x1, f.y0, f.x1, f.y1);
|
|
105
|
+
if (b.y0 > f.y0) add(f.x0, f.y0, f.x1, b.y0);
|
|
106
|
+
if (b.y1 < f.y1) add(f.x0, b.y1, f.x1, f.y1);
|
|
107
|
+
}
|
|
108
|
+
// Each part is in the free rectangle that it comes from. Thus a kept rectangle is never in a part.
|
|
109
|
+
const count = kept.length;
|
|
110
|
+
for (let i = 0; i < parts.length; i++) {
|
|
111
|
+
const p = parts[i]!;
|
|
112
|
+
let drop = false;
|
|
113
|
+
for (let j = 0; j < parts.length && !drop; j++) {
|
|
114
|
+
if (j === i) continue;
|
|
115
|
+
const q = parts[j]!;
|
|
116
|
+
// Of two equal parts, the first part stays
|
|
117
|
+
if (inside(p, q) && (j < i || !inside(q, p))) drop = true;
|
|
118
|
+
}
|
|
119
|
+
for (let k = 0; k < count && !drop; k++) if (inside(p, kept[k]!)) drop = true;
|
|
120
|
+
if (!drop) kept.push(p);
|
|
121
|
+
}
|
|
122
|
+
return { free: kept, hits };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The edges of the boxes on one axis: for each coordinate, the flat list of the ranges on the other axis. */
|
|
126
|
+
type Edges = Map<number, number[]>;
|
|
127
|
+
|
|
128
|
+
/** This function adds a range to the edges at a coordinate. */
|
|
129
|
+
function addEdge(edges: Edges, at: number, a: number, b: number): void {
|
|
130
|
+
const list = edges.get(at);
|
|
131
|
+
if (list === undefined) edges.set(at, [a, b]);
|
|
132
|
+
else list.push(a, b);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** This function gives the length of the common part of the range from `a` to `b` and the edges at a coordinate. */
|
|
136
|
+
function touch(edges: Edges, at: number, a: number, b: number): number {
|
|
137
|
+
const list = edges.get(at);
|
|
138
|
+
if (list === undefined) return 0;
|
|
139
|
+
let sum = 0;
|
|
140
|
+
for (let i = 0; i < list.length; i += 2) sum += Math.max(0, Math.min(b, list[i + 1]!) - Math.max(a, list[i]!));
|
|
141
|
+
return sum;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* This function gives the walls of a space that is not plain: the parts of the bands that are not free. The bands cut the
|
|
146
|
+
* height from 0 to `top`. Two walls of neighbour bands with the same span become one wall.
|
|
147
|
+
*/
|
|
148
|
+
function bandWalls(query: (y0: number, y1: number) => FreeSpan[], w: number, top: number, bandHeight: number, grow: number, end: number): Box[] {
|
|
149
|
+
const walls: Box[] = [];
|
|
150
|
+
const count = Math.max(1, Math.ceil(top / bandHeight));
|
|
151
|
+
let open = new Map<string, Box>();
|
|
152
|
+
for (let k = 0; k < count; k++) {
|
|
153
|
+
const y0 = (top * k) / count;
|
|
154
|
+
const y1 = k === count - 1 ? top : (top * (k + 1)) / count;
|
|
155
|
+
// With a growth of the query less than 0, the band asks for a larger strip. Thus the bands cover each y of the outline.
|
|
156
|
+
const spans = query(Math.max(0, y0 - grow), Math.min(end, y1 + grow));
|
|
157
|
+
const next = new Map<string, Box>();
|
|
158
|
+
let x = 0;
|
|
159
|
+
const block = (a: number, b: number): void => {
|
|
160
|
+
if (!(b > a)) return;
|
|
161
|
+
const key = `${a}|${b}`;
|
|
162
|
+
const wall = open.get(key);
|
|
163
|
+
if (wall !== undefined) {
|
|
164
|
+
wall.y1 = y1;
|
|
165
|
+
open.delete(key);
|
|
166
|
+
next.set(key, wall);
|
|
167
|
+
} else next.set(key, { x0: a, y0, x1: b, y1 });
|
|
168
|
+
};
|
|
169
|
+
for (const s of spans) {
|
|
170
|
+
const start = Math.max(0, Math.min(w, s.start));
|
|
171
|
+
const stop = Math.max(0, Math.min(w, s.end));
|
|
172
|
+
if (!(stop > start)) continue;
|
|
173
|
+
block(x, start);
|
|
174
|
+
x = Math.max(x, stop);
|
|
175
|
+
}
|
|
176
|
+
block(x, w);
|
|
177
|
+
walls.push(...open.values());
|
|
178
|
+
open = next;
|
|
179
|
+
}
|
|
180
|
+
walls.push(...open.values());
|
|
181
|
+
return walls;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* This function packs rectangles with MaxRects, as `packMaxRects` tells. It also gives the free rectangles at the end.
|
|
186
|
+
* The tests read them. The package root does not export this function.
|
|
187
|
+
*/
|
|
188
|
+
export function runMaxRects<T>(rects: Rect<T>[], outer: Rect<T>, options: MaxRectsOptions = {}): MaxRectsRun<T> {
|
|
189
|
+
const rule = options.rule ?? "bssf";
|
|
190
|
+
if (!(MAXRECTS_RULES as readonly string[]).includes(rule)) throw new RangeError(`The rule of maxrects is one of ${MAXRECTS_RULES.join(", ")}, not "${String(rule)}".`);
|
|
191
|
+
const turned = options.turned;
|
|
192
|
+
if (turned !== undefined) {
|
|
193
|
+
for (const r of rects) if (turned.has(r)) r.size.set(r.size.y, r.size.x);
|
|
194
|
+
turned.clear();
|
|
195
|
+
}
|
|
196
|
+
for (const r of rects) if (r.locked) r.wasPacked = true;
|
|
197
|
+
|
|
198
|
+
const W = outer.size.x;
|
|
199
|
+
if (!(W > 0)) return { outer, free: [] };
|
|
200
|
+
const ox = outer.position.x;
|
|
201
|
+
const oy = outer.position.y;
|
|
202
|
+
const fixed = outer.fixedSize;
|
|
203
|
+
const space = options.space;
|
|
204
|
+
const parts = space === undefined ? undefined : spaceParts(space);
|
|
205
|
+
const turnable = options.turnable;
|
|
206
|
+
const sorted = rects.toSorted(compareRects(options.sortOrder ?? "height-desc")).filter((r) => !r.wasPacked && r.size.x > 0 && r.size.y > 0);
|
|
207
|
+
|
|
208
|
+
// The smallest width and height of the boxes. A turnable box counts its short side for the two. A free rectangle with
|
|
209
|
+
// less room has no use, thus the list does not keep it.
|
|
210
|
+
let minW = Infinity;
|
|
211
|
+
let minH = Infinity;
|
|
212
|
+
let heights = 0;
|
|
213
|
+
for (const r of sorted) {
|
|
214
|
+
const turns = turnable?.has(r) === true;
|
|
215
|
+
const short = Math.min(r.size.x, r.size.y);
|
|
216
|
+
minW = Math.min(minW, turns ? short : r.size.x);
|
|
217
|
+
minH = Math.min(minH, turns ? short : r.size.y);
|
|
218
|
+
heights += turns ? Math.max(r.size.x, r.size.y) : r.size.y;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The rectangle holes: the holes of the space, or the locked rectangles. The bands of another space hold its holes.
|
|
222
|
+
const holes: Box[] = [];
|
|
223
|
+
if (parts !== undefined) for (const h of parts.holes) holes.push({ x0: h.x - ox, y0: h.y - oy, x1: h.x + h.w - ox, y1: h.y + h.h - oy });
|
|
224
|
+
else if (space === undefined || space.plain) {
|
|
225
|
+
for (const r of rects) if (r.locked) holes.push({ x0: r.position.x - ox, y0: r.position.y - oy, x1: r.right() - ox, y1: r.bottom() - oy });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A free height gets room below each obstacle for each box. The content gives the height at the end.
|
|
229
|
+
let floor = parts?.bottom ?? 0;
|
|
230
|
+
for (const h of holes) floor = Math.max(floor, h.y1);
|
|
231
|
+
const H = fixed ? outer.size.y : floor + heights;
|
|
232
|
+
const walls: Box[] = [];
|
|
233
|
+
for (const h of holes) {
|
|
234
|
+
const wall = { x0: Math.max(0, h.x0), y0: Math.max(0, h.y0), x1: Math.min(W, h.x1), y1: Math.min(H, h.y1) };
|
|
235
|
+
if (wall.x1 > wall.x0 && wall.y1 > wall.y0) walls.push(wall);
|
|
236
|
+
}
|
|
237
|
+
if (space !== undefined && !space.plain) {
|
|
238
|
+
const top = parts === undefined ? H : fixed ? H : Math.min(H, parts.bottom);
|
|
239
|
+
if (top > 0) {
|
|
240
|
+
const bandHeight = options.bandHeight ?? top / MAXRECTS_BANDS;
|
|
241
|
+
if (!(bandHeight > 0)) throw new RangeError(`The band height of maxrects is more than 0, not ${String(bandHeight)}.`);
|
|
242
|
+
const query = parts === undefined ? (y0: number, y1: number) => space.band(y0 + oy, y1 + oy) : (y0: number, y1: number) => parts.bare(y0 + oy, y1 + oy);
|
|
243
|
+
const shifted = (y0: number, y1: number): FreeSpan[] => query(y0, y1).map((s) => ({ start: s.start - ox, end: s.end - ox }));
|
|
244
|
+
walls.push(...bandWalls(shifted, W, top, bandHeight, parts === undefined ? 0 : Math.max(0, -parts.d), fixed ? H : Infinity));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let free: Box[] = [{ x0: 0, y0: 0, x1: W, y1: H }];
|
|
249
|
+
for (const wall of walls) free = cut(free, wall, minW, minH).free;
|
|
250
|
+
|
|
251
|
+
const cp = rule === "cp";
|
|
252
|
+
const vertical: Edges = new Map();
|
|
253
|
+
const horizontal: Edges = new Map();
|
|
254
|
+
const mark = (b: Box): void => {
|
|
255
|
+
if (!cp) return;
|
|
256
|
+
addEdge(vertical, b.x0, b.y0, b.y1);
|
|
257
|
+
addEdge(vertical, b.x1, b.y0, b.y1);
|
|
258
|
+
addEdge(horizontal, b.y0, b.x0, b.x1);
|
|
259
|
+
addEdge(horizontal, b.y1, b.x0, b.x1);
|
|
260
|
+
};
|
|
261
|
+
for (const wall of walls) mark(wall);
|
|
262
|
+
const contact = (x: number, y: number, w: number, h: number): number => {
|
|
263
|
+
let sum = 0;
|
|
264
|
+
if (x === 0) sum += h;
|
|
265
|
+
if (x + w === W) sum += h;
|
|
266
|
+
if (y === 0) sum += w;
|
|
267
|
+
if (fixed && y + h === H) sum += w;
|
|
268
|
+
return sum + touch(vertical, x, y, y + h) + touch(vertical, x + w, y, y + h) + touch(horizontal, y, x, x + w) + touch(horizontal, y + h, x, x + w);
|
|
269
|
+
};
|
|
270
|
+
// A space that is not plain checks each place, because the bands only approximate the outline
|
|
271
|
+
const check = space !== undefined && !space.plain;
|
|
272
|
+
let bottom = 0;
|
|
273
|
+
for (const wall of walls) bottom = Math.max(bottom, wall.y1);
|
|
274
|
+
|
|
275
|
+
for (const rect of sorted) {
|
|
276
|
+
const w = rect.size.x;
|
|
277
|
+
const h = rect.size.y;
|
|
278
|
+
const turns = turnable?.has(rect) === true && w !== h;
|
|
279
|
+
const banned = new Set<number>();
|
|
280
|
+
let best: Choice | undefined;
|
|
281
|
+
let low: Choice | undefined;
|
|
282
|
+
for (;;) {
|
|
283
|
+
best = undefined;
|
|
284
|
+
low = undefined;
|
|
285
|
+
for (let i = 0; i < free.length; i++) {
|
|
286
|
+
const f = free[i]!;
|
|
287
|
+
for (let t = 0; t < (turns ? 2 : 1); t++) {
|
|
288
|
+
if (banned.has(2 * i + t)) continue;
|
|
289
|
+
const bw = t === 1 ? h : w;
|
|
290
|
+
const bh = t === 1 ? w : h;
|
|
291
|
+
if (f.x0 + bw > f.x1 || f.y0 + bh > f.y1) continue;
|
|
292
|
+
const lw = f.x1 - f.x0 - bw;
|
|
293
|
+
const lh = f.y1 - f.y0 - bh;
|
|
294
|
+
let s1: number;
|
|
295
|
+
let s2: number;
|
|
296
|
+
switch (rule) {
|
|
297
|
+
case "bssf":
|
|
298
|
+
s1 = Math.min(lw, lh);
|
|
299
|
+
s2 = Math.max(lw, lh);
|
|
300
|
+
break;
|
|
301
|
+
case "blsf":
|
|
302
|
+
s1 = Math.max(lw, lh);
|
|
303
|
+
s2 = Math.min(lw, lh);
|
|
304
|
+
break;
|
|
305
|
+
case "baf":
|
|
306
|
+
s1 = (f.x1 - f.x0) * (f.y1 - f.y0) - bw * bh;
|
|
307
|
+
s2 = Math.min(lw, lh);
|
|
308
|
+
break;
|
|
309
|
+
case "bl":
|
|
310
|
+
s1 = f.y0 + bh;
|
|
311
|
+
s2 = f.x0;
|
|
312
|
+
break;
|
|
313
|
+
case "cp":
|
|
314
|
+
s1 = -contact(f.x0, f.y0, bw, bh);
|
|
315
|
+
s2 = f.y0 + bh;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
const c: Choice = { index: i, turn: t === 1, x: f.x0, y: f.y0, w: bw, h: bh, s1, s2 };
|
|
319
|
+
if (best === undefined || s1 < best.s1 || (s1 === best.s1 && (s2 < best.s2 || (s2 === best.s2 && (c.y < best.y || (c.y === best.y && c.x < best.x)))))) best = c;
|
|
320
|
+
const ly = f.y0 + bh;
|
|
321
|
+
if (low === undefined || ly < low.s1 || (ly === low.s1 && f.x0 < low.s2)) low = { ...c, s1: ly, s2: f.x0 };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (best === undefined || !check || space.fits({ x: best.x + ox, y: best.y + oy, w: best.w, h: best.h })) break;
|
|
325
|
+
banned.add(2 * best.index + (best.turn ? 1 : 0));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let place: Box;
|
|
329
|
+
if (best !== undefined) {
|
|
330
|
+
place = { x0: best.x, y0: best.y, x1: best.x + best.w, y1: best.y + best.h };
|
|
331
|
+
if (rule !== "bl" && low !== undefined && (low.x !== best.x || low.y !== best.y || low.turn !== best.turn)) countMove(MOVES[rule]);
|
|
332
|
+
if (best.turn) {
|
|
333
|
+
rect.size.set(best.w, best.h);
|
|
334
|
+
turned?.add(rect);
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
// No free rectangle has room. A fixed size leaves the box out. A free height puts it under all others, at the left edge.
|
|
338
|
+
if (fixed) continue;
|
|
339
|
+
place = { x0: 0, y0: bottom, x1: w, y1: bottom + h };
|
|
340
|
+
}
|
|
341
|
+
rect.position.set(place.x0 + ox, place.y0 + oy);
|
|
342
|
+
rect.wasPacked = true;
|
|
343
|
+
bottom = Math.max(bottom, place.y1);
|
|
344
|
+
const result = cut(free, { x0: place.x0, y0: place.y0, x1: Math.min(W, place.x1), y1: place.y1 }, minW, minH);
|
|
345
|
+
free = result.free;
|
|
346
|
+
if (result.hits >= 2) countMove(MOVES.split);
|
|
347
|
+
mark(place);
|
|
348
|
+
if (!fixed) outer.size.y = Math.max(outer.size.y, place.y1);
|
|
349
|
+
}
|
|
350
|
+
return { outer, free: free.map((f) => ({ x: f.x0 + ox, y: f.y0 + oy, w: f.x1 - f.x0, h: f.y1 - f.y0 })) };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* This function packs rectangles into an outer rectangle with MaxRects of Jylänki (2010). It keeps the list of the maximal
|
|
355
|
+
* free rectangles, and it does not change the width of the outer rectangle.
|
|
356
|
+
*
|
|
357
|
+
* - Each box goes to the top-left corner of the free rectangle that `options.rule` chooses. `MAXRECTS_RULES` tells the rules.
|
|
358
|
+
* - A tie goes to the higher place, then to the place on the left, then to the first free rectangle of the list.
|
|
359
|
+
* - A `locked` rectangle keeps its position, and the free rectangles go around it.
|
|
360
|
+
* - With a space that is not plain, the free rectangles start from bands of the outline. `MaxRectsOptions` tells the bands.
|
|
361
|
+
* - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a box that does not fit
|
|
362
|
+
* is not packed: its `wasPacked` stays `false`.
|
|
363
|
+
* - The default sort order is `height-desc`. The order of the array of the caller stays.
|
|
364
|
+
*/
|
|
365
|
+
export function packMaxRects<T>(rects: Rect<T>[], outer: Rect<T>, options: MaxRectsOptions = {}): Rect<T> {
|
|
366
|
+
return runMaxRects(rects, outer, options).outer;
|
|
367
|
+
}
|
package/src/moves.ts
CHANGED
|
@@ -10,6 +10,11 @@ export const MOVES: Readonly<{
|
|
|
10
10
|
push: "move.cascade";
|
|
11
11
|
firstFit: "move.firstFit";
|
|
12
12
|
bestFit: "move.bestFit";
|
|
13
|
+
bssf: "move.bssf";
|
|
14
|
+
blsf: "move.blsf";
|
|
15
|
+
baf: "move.baf";
|
|
16
|
+
cp: "move.cp";
|
|
17
|
+
split: "move.split";
|
|
13
18
|
}> = Object.freeze({
|
|
14
19
|
/** A rectangle that the gap fill of `packRow` places in a gap of a row: under a shorter neighbor, or at the row break. */
|
|
15
20
|
rows: "move.gapFill",
|
|
@@ -26,6 +31,16 @@ export const MOVES: Readonly<{
|
|
|
26
31
|
firstFit: "move.firstFit",
|
|
27
32
|
/** A rectangle that the fit `best` puts on a shelf that is not the first shelf where it fits. */
|
|
28
33
|
bestFit: "move.bestFit",
|
|
34
|
+
/** A rectangle that the rule `bssf` of `maxrects` puts in another place than the rule `bl` on the same free rectangles. */
|
|
35
|
+
bssf: "move.bssf",
|
|
36
|
+
/** A rectangle that the rule `blsf` of `maxrects` puts in another place than the rule `bl` on the same free rectangles. */
|
|
37
|
+
blsf: "move.blsf",
|
|
38
|
+
/** A rectangle that the rule `baf` of `maxrects` puts in another place than the rule `bl` on the same free rectangles. */
|
|
39
|
+
baf: "move.baf",
|
|
40
|
+
/** A rectangle that the rule `cp` of `maxrects` puts in another place than the rule `bl` on the same free rectangles. */
|
|
41
|
+
cp: "move.cp",
|
|
42
|
+
/** A rectangle of `maxrects` that cuts two free rectangles or more. A shelf or a skyline has no such step. */
|
|
43
|
+
split: "move.split",
|
|
29
44
|
});
|
|
30
45
|
|
|
31
46
|
const counts = new Map<string, number>();
|
package/src/pack.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { packMaxRects, type MaxRectsRule } from "./maxrects.ts";
|
|
1
2
|
import { clearMoves, countMove, MOVES } from "./moves.ts";
|
|
2
3
|
import { compareRects, type SortOrder } from "./order.ts";
|
|
3
4
|
import { Rect } from "./rect.ts";
|
|
@@ -20,12 +21,20 @@ export interface PackOptions {
|
|
|
20
21
|
/** The options of `pack` */
|
|
21
22
|
export interface PackLayoutOptions extends PackOptions {
|
|
22
23
|
/**
|
|
23
|
-
* The packing: `rows` (the default), `skyline` or `
|
|
24
|
-
* Use `shelf` with the sort order `input` to keep the order of the array around locked rectangles.
|
|
24
|
+
* The packing: `rows` (the default), `skyline`, `shelf` or `maxrects`. `skyline`, `shelf` and `maxrects` pack around
|
|
25
|
+
* locked rectangles. Use `shelf` with the sort order `input` to keep the order of the array around locked rectangles.
|
|
25
26
|
*/
|
|
26
|
-
strategy?: "rows" | "skyline" | "shelf";
|
|
27
|
+
strategy?: "rows" | "skyline" | "shelf" | "maxrects";
|
|
27
28
|
/** The rule of `shelf` to choose a shelf. Only `shelf` reads it. `ShelfOptions` tells it. Without it, `shelf` packs as in 0.2.3. */
|
|
28
29
|
fit?: ShelfFit;
|
|
30
|
+
/** The rule of `maxrects` to choose a free rectangle. Only `maxrects` reads it. The default is `bssf`. */
|
|
31
|
+
rule?: MaxRectsRule;
|
|
32
|
+
/** The largest height of a band of `maxrects` in a space that is not plain. Only `maxrects` reads it. `MaxRectsOptions` tells it. */
|
|
33
|
+
bandHeight?: number;
|
|
34
|
+
/** The rectangles that `maxrects` can turn by 90 degrees. Only `maxrects` reads it. */
|
|
35
|
+
turnable?: ReadonlySet<unknown>;
|
|
36
|
+
/** The rectangles that `maxrects` turned. Only `maxrects` reads it. `MaxRectsOptions` tells it. */
|
|
37
|
+
turned?: Set<unknown>;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
/** The depth of the calls of `packInto`. Only the outer gap fill counts its rectangles as moves. */
|
|
@@ -126,7 +135,7 @@ export function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options: PackOptio
|
|
|
126
135
|
* This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
|
|
127
136
|
* - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
|
|
128
137
|
* - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
129
|
-
* - `options.strategy` selects `packRows` (the default), `packSkyline` or `
|
|
138
|
+
* - `options.strategy` selects `packRows` (the default), `packSkyline`, `packShelf` or `packMaxRects` after the width is set.
|
|
130
139
|
*/
|
|
131
140
|
export function pack<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options: PackLayoutOptions = {}): Rect<T> {
|
|
132
141
|
let totalArea = 0;
|
|
@@ -150,6 +159,7 @@ export function pack<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options:
|
|
|
150
159
|
if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
|
|
151
160
|
if (options.strategy === "skyline") packSkyline(rects, outer, options);
|
|
152
161
|
else if (options.strategy === "shelf") packShelf(rects, outer, options);
|
|
162
|
+
else if (options.strategy === "maxrects") packMaxRects(rects, outer, options);
|
|
153
163
|
else packRows(rects, outer, options);
|
|
154
164
|
|
|
155
165
|
if (!outer.fixedSize) {
|
package/src/registry.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { push, rows, shelf, skyline } from "./adapters.ts";
|
|
1
|
+
import { maxrects, push, rows, shelf, skyline } from "./adapters.ts";
|
|
2
2
|
import type { PackInput, PackOutput } from "./contract.ts";
|
|
3
3
|
import { createContext, type Strategy } from "./strategy.ts";
|
|
4
4
|
|
|
5
5
|
/** Each strategy of the package. The benchmark starts each one on each input of the corpus. */
|
|
6
|
-
export const strategies: readonly Strategy[] = Object.freeze([rows, skyline, shelf, push] as Strategy[]);
|
|
6
|
+
export const strategies: readonly Strategy[] = Object.freeze([rows, skyline, shelf, push, maxrects] as Strategy[]);
|
|
7
7
|
|
|
8
8
|
/** This function gives the strategy with an id. It throws a `RangeError` for an id that the registry does not have. */
|
|
9
9
|
export function strategy(id: string): Strategy {
|
package/src/space.ts
CHANGED
|
@@ -346,7 +346,7 @@ export function spaceOf(input: PackInput, frame: Frame): FreeSpace {
|
|
|
346
346
|
const across = transposeLayer(layer);
|
|
347
347
|
|
|
348
348
|
const band = (y0: number, y1: number): FreeSpan[] => spansOf(layer, y0, y1).map(([start, end]) => ({ start, end }));
|
|
349
|
-
|
|
349
|
+
const space: FreeSpace = {
|
|
350
350
|
w: frame.w,
|
|
351
351
|
h: frame.h,
|
|
352
352
|
plain,
|
|
@@ -368,6 +368,41 @@ export function spaceOf(input: PackInput, frame: Frame): FreeSpace {
|
|
|
368
368
|
return nextOf(layer, y0, y1, w);
|
|
369
369
|
},
|
|
370
370
|
};
|
|
371
|
+
const bare: Layer = { ...layer, holes: [] };
|
|
372
|
+
let bottom = 0;
|
|
373
|
+
for (const b of blockers) bottom = Math.max(bottom, b.c1 + Math.max(0, b.g));
|
|
374
|
+
PARTS.set(space, {
|
|
375
|
+
kind: layer.kind,
|
|
376
|
+
d,
|
|
377
|
+
holes: holes.map((r) => ({ x: r.a0, y: r.c0, w: r.a1 - r.a0, h: r.c1 - r.c0 })),
|
|
378
|
+
bottom: Number.isFinite(frame.h) ? frame.h : bottom,
|
|
379
|
+
bare: (y0, y1) => spansOf(bare, y0, y1).map(([start, end]) => ({ start, end })),
|
|
380
|
+
});
|
|
381
|
+
return space;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The parts of a free space of `spaceOf`. The packer `maxrects` reads them to make its first free rectangles. The
|
|
386
|
+
* package root does not export them.
|
|
387
|
+
*/
|
|
388
|
+
export interface SpaceParts {
|
|
389
|
+
/** The kind of the outline: `plain`, `box`, `round` or `rings`. */
|
|
390
|
+
readonly kind: "plain" | "box" | "round" | "rings";
|
|
391
|
+
/** The growth of a query for the outline: the inset less half the gap. */
|
|
392
|
+
readonly d: number;
|
|
393
|
+
/** The rectangle holes: the locked items and the rectangle obstacles, in the coordinates of the frame. */
|
|
394
|
+
readonly holes: readonly SpaceRect[];
|
|
395
|
+
/** The height to cut into bands. A fixed height gives the height. A free height gives the lowest edge of the blockers. */
|
|
396
|
+
readonly bottom: number;
|
|
397
|
+
/** This method gives the free x-spans of the outline and the blockers from `y0` to `y1`, without the rectangle holes. */
|
|
398
|
+
bare(y0: number, y1: number): FreeSpan[];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const PARTS = new WeakMap<FreeSpace, SpaceParts>();
|
|
402
|
+
|
|
403
|
+
/** This function gives the parts of a free space of `spaceOf`, or `undefined` for another free space. */
|
|
404
|
+
export function spaceParts(space: FreeSpace): SpaceParts | undefined {
|
|
405
|
+
return PARTS.get(space);
|
|
371
406
|
}
|
|
372
407
|
|
|
373
408
|
/**
|