@mark1russell7/packing 0.2.4 → 0.2.6

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.
@@ -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 `shelf`. `skyline` and `shelf` pack around locked rectangles.
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 `packShelf` after the width is set.
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) {
@@ -0,0 +1,103 @@
1
+ // The portfolio of roadmap item k20, part 1 (`docs/PACKER.md`, "The portfolio"). A portfolio packs an input with each
2
+ // member and keeps the best output by the one pick rule `selectBest` of the contract. Nothing default uses it.
3
+
4
+ import { pickMoves, portfolio, type Member, type Portfolio, type PortfolioKey, type PortfolioPick, type SelectLevel } from "@mark1russell7/layout-contract";
5
+ import { checkHard } from "./check.ts";
6
+ import type { PackInput, PackOutput } from "./contract.ts";
7
+ import { outputHash } from "./layout-input.ts";
8
+ import { MEASURE_SPECS, measure, type LayoutMeasures } from "./measures.ts";
9
+ import { packWith, strategy } from "./registry.ts";
10
+
11
+ /** A member of `packPortfolio`: a strategy of the registry with some options. The id names the member in the trace. */
12
+ export interface PortfolioEntry {
13
+ id: string;
14
+ /** The id of a strategy of the registry */
15
+ strategy: string;
16
+ /** The options of the member over the defaults of the strategy */
17
+ options?: unknown;
18
+ }
19
+
20
+ /** The options of `packPortfolio`. The time limit and the member limit are those of `portfolio` of the contract. */
21
+ export interface PackPortfolioOptions {
22
+ /** The id of the portfolio. The default is `portfolio`. */
23
+ id?: string;
24
+ /** The seed of each member. The default is 0, as for `packWith`. */
25
+ seed?: number;
26
+ /** The time limit in milliseconds. Without it, each member runs. A cut member makes the pick not deterministic. */
27
+ budgetMs?: number;
28
+ /** The largest count of members that run */
29
+ maxMembers?: number;
30
+ /** The clock in milliseconds. The default is `performance.now`. */
31
+ now?: () => number;
32
+ }
33
+
34
+ /** The id of the trace stage of `packPortfolio`. Its measure `move.pick` is 1 when the pick is not member 0. */
35
+ export const PORTFOLIO_STAGE = "portfolio";
36
+
37
+ /**
38
+ * The levels of the pick of `packPortfolio` and of the portfolio report of the benchmark, in order. Level 0 is `hard`.
39
+ *
40
+ * - `hard`: the count of hard breaks of `checkHard` with `exactContainer`. Lower is better, with the minimum effect 0.
41
+ * - `pack.overflow.excessArea`: the overflow area above its bound. Lower is better.
42
+ * - `pack.fill`: the fill of the hull. Higher is better.
43
+ */
44
+ export const PORTFOLIO_LEVELS: readonly SelectLevel[] = Object.freeze([
45
+ { id: "hard", spec: { direction: "lower", minEffect: 0 } },
46
+ { id: "pack.overflow.excessArea", spec: MEASURE_SPECS["pack.overflow.excessArea"] },
47
+ { id: "pack.fill", spec: MEASURE_SPECS["pack.fill"] },
48
+ ] as SelectLevel[]);
49
+
50
+ /** This function gives the count of the hard breaks of an output: the sum of `checkHard` with `exactContainer`. */
51
+ export function hardBreaks(input: PackInput, output: PackOutput): number {
52
+ return Object.values(checkHard(input, output, undefined, { exactContainer: true })).reduce((sum, n) => sum + n, 0);
53
+ }
54
+
55
+ /**
56
+ * This function makes an opt-in portfolio of strategies of the registry with `portfolio` of the contract.
57
+ * Its `pick(input)` packs the input with each member and gives the output of the winner, its id and the trace.
58
+ *
59
+ * - A member is a strategy id, or a `PortfolioEntry` with options. The members run in the given order.
60
+ * - The key is the hard breaks, then `pack.overflow.excessArea`, then `pack.fill`. `PORTFOLIO_LEVELS` gives the specs.
61
+ * - The hash of the key is `outputHash`. `selectBest` of the contract picks the winner.
62
+ * - The output of the winner gets the trace stage `portfolio` with `move.pick` of `pickMoves`.
63
+ * - The function throws a `RangeError` for an unknown strategy, an empty list and two members with the same id.
64
+ */
65
+ export function packPortfolio(members: readonly (string | PortfolioEntry)[], options: PackPortfolioOptions = {}): Portfolio<PackInput, PackOutput> {
66
+ const seed = options.seed ?? 0;
67
+ const list: Member<PackInput, PackOutput>[] = members.map((m) => {
68
+ const e = typeof m === "string" ? { id: m, strategy: m } : m;
69
+ strategy(e.strategy);
70
+ return { id: e.id, run: (input: PackInput): PackOutput => packWith(e.strategy, input, e.options, seed) };
71
+ });
72
+ // Each level reads the measures of one output, so the measures of an output are computed one time
73
+ const cache = new WeakMap<PackOutput, LayoutMeasures>();
74
+ const measured = (input: PackInput, output: PackOutput): LayoutMeasures => {
75
+ let m = cache.get(output);
76
+ if (m === undefined) {
77
+ m = measure(input, output);
78
+ cache.set(output, m);
79
+ }
80
+ return m;
81
+ };
82
+ const key: PortfolioKey<PackInput, PackOutput> = {
83
+ hard: hardBreaks,
84
+ levels: PORTFOLIO_LEVELS.slice(1).map((level) => ({
85
+ id: level.id,
86
+ spec: level.spec,
87
+ value: (input: PackInput, output: PackOutput): number => measured(input, output)[level.id as "pack.fill" | "pack.overflow.excessArea"],
88
+ })),
89
+ hash: outputHash,
90
+ };
91
+ const limits = {
92
+ ...(options.budgetMs !== undefined ? { budgetMs: options.budgetMs } : {}),
93
+ ...(options.maxMembers !== undefined ? { maxMembers: options.maxMembers } : {}),
94
+ ...(options.now !== undefined ? { now: options.now } : {}),
95
+ };
96
+ const inner = portfolio(options.id ?? PORTFOLIO_STAGE, list, key, limits);
97
+ const pick = (input: PackInput): PortfolioPick<PackOutput> => {
98
+ const got = inner.pick(input);
99
+ const stage = { id: PORTFOLIO_STAGE, measures: { "move.pick": pickMoves(got.trace).pick } };
100
+ return { ...got, output: { ...got.output, trace: [...(got.output.trace ?? []), stage] } };
101
+ };
102
+ return { id: inner.id, pick, run: (input: PackInput): PackOutput => pick(input).output };
103
+ }
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
- return {
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
  /**