@mark1russell7/packing 0.2.3 → 0.2.4

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/packing",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "description": "Rectangle packing: shelf, guillotine and skyline strategies, push cascades and measures of a layout.",
6
6
  "keywords": [
7
7
  "rectangle-packing",
package/src/adapters.ts CHANGED
@@ -8,6 +8,7 @@ import type { SortOrder } from "./order.ts";
8
8
  import { pack } from "./pack.ts";
9
9
  import { pushCascade } from "./push.ts";
10
10
  import { Rect } from "./rect.ts";
11
+ import type { ShelfFit } from "./shelf.ts";
11
12
  import { packSkyline } from "./skyline.ts";
12
13
  import { spaceOf } from "./space.ts";
13
14
  import { assertSupported, cellOf, UnsupportedCell, type PackContext, type SizeSearch, type Strategy, type Supports } from "./strategy.ts";
@@ -18,6 +19,11 @@ export interface LayoutStrategyOptions {
18
19
  sortOrder: SortOrder;
19
20
  /** The number of widths that the first pass of the size search `log-sweep` tries. */
20
21
  samples: number;
22
+ /**
23
+ * The rule of `shelf` to choose a shelf: `next`, `first` or `best`. Only `shelf` reads it. Without it, `shelf` packs
24
+ * as in 0.2.3. With it, the trace also counts `move.firstFit` and `move.bestFit`.
25
+ */
26
+ fit?: ShelfFit;
21
27
  }
22
28
 
23
29
  /** The options of the strategy `push`. */
@@ -55,15 +61,28 @@ function supportsOf(cells: { [D in keyof Supports]: readonly Supports[D][number]
55
61
  };
56
62
  }
57
63
 
64
+ /** This function gives the fit of the options of a strategy: only `shelf` reads it. */
65
+ function fitOf(id: "rows" | "skyline" | "shelf", options: Partial<LayoutStrategyOptions>): { fit?: ShelfFit } {
66
+ return id === "shelf" && options.fit !== undefined ? { fit: options.fit } : {};
67
+ }
68
+
69
+ /** This function gives the move counts of the trace of a strategy: its own move, and the moves of a shelf fit. */
70
+ function movesOf(id: "rows" | "skyline" | "shelf", fit: { fit?: ShelfFit }): Record<string, number> {
71
+ const out: Record<string, number> = { [MOVES[id]]: moveCount(MOVES[id]) };
72
+ if (fit.fit !== undefined) for (const move of [MOVES.firstFit, MOVES.bestFit]) out[move] = moveCount(move);
73
+ return out;
74
+ }
75
+
58
76
  /** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
59
77
  function packFixedWidth(id: "rows" | "skyline" | "shelf", input: PackInput, options: LayoutStrategyOptions): PackOutput {
60
78
  const start = now();
61
79
  const frame = buildFrame(input);
62
80
  const rects = rectsOf(frame);
81
+ const fit = fitOf(id, options);
63
82
  clearMoves();
64
- if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder, space: spaceOf(input, frame) });
83
+ if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder, space: spaceOf(input, frame), ...fit });
65
84
  const output = readFrame(input, frame, []);
66
- return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length, [MOVES[id]]: moveCount(MOVES[id]) } }] };
85
+ return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length, ...movesOf(id, fit) } }] };
67
86
  }
68
87
 
69
88
  /** This function makes the strategy of one function of `pack`. */
@@ -118,6 +137,7 @@ export const skyline: Strategy<LayoutStrategyOptions> = layoutStrategy("skyline"
118
137
  /**
119
138
  * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
120
139
  * With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
140
+ * The option `fit` selects a shelf rule of roadmap item k21: `next`, `first` or `best`. The default has no `fit`.
121
141
  */
122
142
  export const shelf: Strategy<LayoutStrategyOptions> = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
123
143
  obstacleMove: ["fixed"],
@@ -137,6 +157,7 @@ export const logSweep: SizeSearch = {
137
157
  const o = { ...(inner.defaults as LayoutStrategyOptions), ...(options as Partial<LayoutStrategyOptions> | undefined) };
138
158
  const start = now();
139
159
  const frame = buildFrame(input);
160
+ const fit = fitOf(id, o);
140
161
  const result = packFree(rectsOf(frame), new Rect<number>(), {
141
162
  strategy: id,
142
163
  sortOrder: o.sortOrder,
@@ -144,6 +165,7 @@ export const logSweep: SizeSearch = {
144
165
  samples: o.samples,
145
166
  aspect: input.container.aspect ?? 1,
146
167
  aspectWeight: input.container.aspectWeight ?? 1,
168
+ ...fit,
147
169
  });
148
170
  const ms = now() - start;
149
171
  const runs = result.tried.length + 1;
@@ -152,7 +174,7 @@ export const logSweep: SizeSearch = {
152
174
  ...output,
153
175
  trace: [
154
176
  { id: "log-sweep", ms, measures: { "pack.search.widths": result.tried.length, "pack.search.ms": ms } },
155
- { id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length, [MOVES[id]]: moveCount(MOVES[id]) } },
177
+ { id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length, ...movesOf(id, fit) } },
156
178
  ],
157
179
  };
158
180
  },
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ export { Rect } from "./rect.ts";
3
3
  export { pack, packRows, type PackOptions, type PackLayoutOptions } from "./pack.ts";
4
4
  export { packSkyline, lowestWindow, type SkylineOptions } from "./skyline.ts";
5
5
  export { freeSpace, type FreeSpace, type FreeSpan, type SpaceRect } from "./space.ts";
6
- export { packShelf, type ShelfOptions } from "./shelf.ts";
6
+ export { packShelf, SHELF_FITS, type ShelfFit, type ShelfOptions } from "./shelf.ts";
7
7
  export { packFree, aspectError, type FreeWidthOptions, type FreeWidthResult, type FreeWidthTry } from "./free.ts";
8
8
  export { compareRects, SORT_ORDERS, type SortOrder } from "./order.ts";
9
9
  export { pushCascade, type PushOptions } from "./push.ts";
package/src/measures.ts CHANGED
@@ -51,9 +51,16 @@ export interface LayoutMeasures {
51
51
  "pack.overflow.rate": number;
52
52
  "pack.overflow.area": number;
53
53
  "pack.overflow.forced": number;
54
+ /** The tight lower bound of `pack.overflow.area`. `docs/MEASURES.md` defines it. */
54
55
  "pack.overflow.forcedArea": number;
55
56
  "pack.overflow.excess": number;
57
+ /**
58
+ * This measure is an alias of `pack.overflow.forcedArea`, with the same value.
59
+ *
60
+ * @deprecated Use `pack.overflow.forcedArea`. The alias stays until the next minor version.
61
+ */
56
62
  "pack.overflow.forcedAreaMin": number;
63
+ /** The overflow area above its bound: `pack.overflow.area` less `pack.overflow.forcedArea`. */
57
64
  "pack.overflow.excessArea": number;
58
65
  "pack.search.widths": number;
59
66
  "pack.search.ms": number;
@@ -97,6 +104,8 @@ const REL = Object.freeze({ rel: 0.01 });
97
104
  * - A share of an area from 0 to 1 has 0.005. A guard has its tolerance.
98
105
  * - An area and a ratio of lengths have `{ rel: 0.01 }`: 1% of the larger value.
99
106
  * - The time has no minimum effect, because the times of CI change each time.
107
+ * - `pack.overflow.forcedAreaMin` is a deprecated alias of `pack.overflow.forcedArea`. Its spec is the same, except the id.
108
+ * The alias stays until the next minor version.
100
109
  */
101
110
  export const MEASURE_SPECS: Readonly<Record<MeasureId, MeasureSpec>> = Object.freeze({
102
111
  "pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity, REL),
@@ -113,6 +122,7 @@ export const MEASURE_SPECS: Readonly<Record<MeasureId, MeasureSpec>> = Object.fr
113
122
  "pack.overflow.forced": spec("pack.overflow.forced", "overflow", "count", "lower", 0, Infinity, 0),
114
123
  "pack.overflow.forcedArea": spec("pack.overflow.forcedArea", "overflow", "share", "lower", 0, 1, 0.005),
115
124
  "pack.overflow.excess": spec("pack.overflow.excess", "overflow", "count", "lower", 0, Infinity, 0),
125
+ // The deprecated alias of pack.overflow.forcedArea, until the next minor version
116
126
  "pack.overflow.forcedAreaMin": spec("pack.overflow.forcedAreaMin", "overflow", "share", "lower", 0, 1, 0.005),
117
127
  "pack.overflow.excessArea": spec("pack.overflow.excessArea", "overflow", "share", "lower", 0, 1, 0.005),
118
128
  "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, 0.02, { floor: 0, tol: 0.02 }),
@@ -209,7 +219,8 @@ function traceSum(output: PackOutput, id: string): number {
209
219
  * - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
210
220
  * - The overflow measures count each id once. `pack.overflow.forced` is the lower bound of `overflowBound`.
211
221
  * - `pack.overflow.excess` is the overflow less the forced overflow. A container with no fixed side gives 0 for both.
212
- * - `pack.overflow.forcedAreaMin` is a lower bound of the overflow area. `pack.overflow.excessArea` is the overflow area above it.
222
+ * - `pack.overflow.forcedArea` is a lower bound of the overflow area. `pack.overflow.excessArea` is the overflow area above it.
223
+ * - `pack.overflow.forcedAreaMin` is a deprecated alias of `pack.overflow.forcedArea`, with the same value.
213
224
  *
214
225
  * @example
215
226
  * ```ts
@@ -263,7 +274,7 @@ export function measure(input: PackInput, output: PackOutput, options: MeasureOp
263
274
  }
264
275
  const bound = overflowBound(input);
265
276
  const overflowArea = itemArea > 0 ? unplacedArea / itemArea : 0;
266
- const forcedAreaMin = itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0;
277
+ const forcedArea = itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0;
267
278
 
268
279
  return {
269
280
  "pack.area": hullArea,
@@ -278,10 +289,10 @@ export function measure(input: PackInput, output: PackOutput, options: MeasureOp
278
289
  "pack.overflow.rate": input.items.length > 0 ? unplaced.size / input.items.length : 0,
279
290
  "pack.overflow.area": overflowArea,
280
291
  "pack.overflow.forced": bound.forced,
281
- "pack.overflow.forcedArea": forcedAreaMin,
292
+ "pack.overflow.forcedArea": forcedArea,
282
293
  "pack.overflow.excess": Math.max(0, unplaced.size - bound.forced),
283
- "pack.overflow.forcedAreaMin": forcedAreaMin,
284
- "pack.overflow.excessArea": excessAreaOf(overflowArea, forcedAreaMin),
294
+ "pack.overflow.forcedAreaMin": forcedArea,
295
+ "pack.overflow.excessArea": excessAreaOf(overflowArea, forcedArea),
285
296
  "pack.search.widths": traceSum(output, "pack.search.widths"),
286
297
  "pack.search.ms": traceSum(output, "pack.search.ms"),
287
298
  };
@@ -294,8 +305,8 @@ const AREA_NOISE = 1e-9;
294
305
  * This function gives the overflow area above its lower bound. A value from `-AREA_NOISE` to 0 becomes 0. A value below
295
306
  * `-AREA_NOISE` stays, because it shows a bug of the bound or of the packer.
296
307
  */
297
- function excessAreaOf(overflowArea: number, forcedAreaMin: number): number {
298
- const excess = overflowArea - forcedAreaMin;
308
+ function excessAreaOf(overflowArea: number, forcedArea: number): number {
309
+ const excess = overflowArea - forcedArea;
299
310
  return excess < 0 && excess >= -AREA_NOISE ? 0 : excess;
300
311
  }
301
312
 
package/src/moves.ts CHANGED
@@ -2,16 +2,30 @@
2
2
  // packers count their move here, and the adapters write the count in `trace.measures`. The counts do not change a layout.
3
3
  // The module is internal: the package root does not export it.
4
4
 
5
- /** The id of the characteristic move of each strategy. The trace of each strategy has its id, also with the count 0. */
6
- export const MOVES: Readonly<{ rows: "move.gapFill"; shelf: "move.stack"; skyline: "move.lowerWindow"; push: "move.cascade" }> = Object.freeze({
5
+ /** The id of the characteristic move of each strategy and of each shelf fit. The trace of each strategy has its id, also with the count 0. */
6
+ export const MOVES: Readonly<{
7
+ rows: "move.gapFill";
8
+ shelf: "move.stack";
9
+ skyline: "move.lowerWindow";
10
+ push: "move.cascade";
11
+ firstFit: "move.firstFit";
12
+ bestFit: "move.bestFit";
13
+ }> = Object.freeze({
7
14
  /** A rectangle that the gap fill of `packRow` places in a gap of a row: under a shorter neighbor, or at the row break. */
8
15
  rows: "move.gapFill",
9
- /** A rectangle that `packShelf` puts on an open shelf with a taller rectangle. The strip above it stays empty. */
16
+ /**
17
+ * A rectangle that `packShelf` puts on an open shelf with a taller rectangle. The strip above it stays empty. With a
18
+ * `fit`, the move is a rectangle in the strip above a shorter rectangle of a shelf.
19
+ */
10
20
  shelf: "move.stack",
11
21
  /** A rectangle that the skyline puts in a lower window, not in the leftmost window where it fits. */
12
22
  skyline: "move.lowerWindow",
13
23
  /** A rectangle that a push cascade moves. */
14
24
  push: "move.cascade",
25
+ /** A rectangle that a shelf fit puts on a shelf that is not the open shelf. The open shelf is the last shelf that the packing opened. */
26
+ firstFit: "move.firstFit",
27
+ /** A rectangle that the fit `best` puts on a shelf that is not the first shelf where it fits. */
28
+ bestFit: "move.bestFit",
15
29
  });
16
30
 
17
31
  const counts = new Map<string, number>();
package/src/pack.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { clearMoves, countMove, MOVES } from "./moves.ts";
2
2
  import { compareRects, type SortOrder } from "./order.ts";
3
3
  import { Rect } from "./rect.ts";
4
- import { packRowsInSpace, packShelf } from "./shelf.ts";
4
+ import { packRowsInSpace, packShelf, type ShelfFit } from "./shelf.ts";
5
5
  import { packSkyline } from "./skyline.ts";
6
6
  import type { FreeSpace } from "./space.ts";
7
7
  import { Vector } from "./vector.ts";
@@ -24,6 +24,8 @@ export interface PackLayoutOptions extends PackOptions {
24
24
  * Use `shelf` with the sort order `input` to keep the order of the array around locked rectangles.
25
25
  */
26
26
  strategy?: "rows" | "skyline" | "shelf";
27
+ /** 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
+ fit?: ShelfFit;
27
29
  }
28
30
 
29
31
  /** The depth of the calls of `packInto`. Only the outer gap fill counts its rectangles as moves. */
package/src/shelf.ts CHANGED
@@ -3,6 +3,18 @@ import { compareRects, type SortOrder } from "./order.ts";
3
3
  import type { Rect } from "./rect.ts";
4
4
  import { firstFit, type FreeSpace, type FreeSpan } from "./space.ts";
5
5
 
6
+ /**
7
+ * The rule of a shelf packing to choose the shelf of a rectangle. `packShelf` tells each rule.
8
+ *
9
+ * - `next`: only the open shelf, the last shelf that the packing opened.
10
+ * - `first`: the first shelf from the top where the rectangle fits.
11
+ * - `best`: the shelf with the least height left over above the rectangle.
12
+ */
13
+ export type ShelfFit = "next" | "first" | "best";
14
+
15
+ /** This constant lists each shelf fit. */
16
+ export const SHELF_FITS: readonly ShelfFit[] = ["next", "first", "best"];
17
+
6
18
  /** The options of `packShelf` */
7
19
  export interface ShelfOptions {
8
20
  /** The order of the rectangles before the packing. The default is `height-desc`. */
@@ -12,6 +24,11 @@ export interface ShelfOptions {
12
24
  * outer rectangle and the locked rectangles.
13
25
  */
14
26
  space?: FreeSpace;
27
+ /**
28
+ * The rule to choose a shelf. Without it, `packShelf` packs as in 0.2.3. With it, `packShelf` uses its own shelves in
29
+ * each container shape, and it stacks rectangles in the strips above the shorter rectangles.
30
+ */
31
+ fit?: ShelfFit;
15
32
  }
16
33
 
17
34
  /** An obstacle in the coordinates of the outer rectangle */
@@ -60,8 +77,10 @@ function nextTop(obstacles: readonly Box[], top: number, bottom: number): number
60
77
  * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
61
78
  * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
62
79
  * - With a space that is not plain, `packRowsInSpace` packs the rows into the outline of the space and around its holes.
80
+ * - With `fit`, `packShelvesInSpace` packs the rectangles, in each container shape. A `fit` that is not in `SHELF_FITS` throws a `RangeError`.
63
81
  */
64
82
  export function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOptions = {}): Rect<T> {
83
+ if (options.fit !== undefined) return packShelvesInSpace(rects, outer, options.sortOrder ?? "height-desc", options.fit, options.space);
65
84
  if (options.space !== undefined && !options.space.plain) return packRowsInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
66
85
  const width = outer.size.x;
67
86
  const obstacles: Box[] = [];
@@ -229,3 +248,184 @@ export function packRowsInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: Sort
229
248
  if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
230
249
  return outer;
231
250
  }
251
+
252
+ /** A shelf of `packShelvesInSpace`, in the coordinates of the outer rectangle. */
253
+ interface Shelf {
254
+ /** The top edge of the shelf. */
255
+ top: number;
256
+ /** The height of the shelf: the height of its first rectangle. Only the last shelf grows. */
257
+ height: number;
258
+ /** The x after the last rectangle on the floor of the shelf. The value `Infinity` closes the shelf. */
259
+ cursor: number;
260
+ /** The strips above the rectangles on the floor. A later rectangle stacks in a strip. */
261
+ strips: Strip[];
262
+ }
263
+
264
+ /** The free strip above a rectangle of a shelf: from `x` to `x + w`, and from `bottom` to the top edge of the next shelf. */
265
+ interface Strip {
266
+ x: number;
267
+ w: number;
268
+ bottom: number;
269
+ }
270
+
271
+ /** A place for a rectangle on a shelf. `left` is the height of the shelf that stays free above the rectangle. */
272
+ interface Spot {
273
+ shelf: Shelf;
274
+ x: number;
275
+ y: number;
276
+ left: number;
277
+ /** The strip of a stacked rectangle. A rectangle on the floor has no strip. */
278
+ strip?: Strip;
279
+ /** The value `true` when the rectangle is taller than the last shelf, and the shelf grows. */
280
+ grow?: boolean;
281
+ }
282
+
283
+ /**
284
+ * This function packs rectangles into shelves with a fit: `next`, `first` or `best`. `packShelf` uses it when its
285
+ * options have a `fit`. It sets the position of each rectangle and gives the outer rectangle.
286
+ *
287
+ * - A shelf opens at the bottom edge of the last shelf, at the first x where the rectangle fits in the free space.
288
+ * When no x fits, the shelf steps down to `nextY` of the space, as an empty row of `packRowsInSpace` does.
289
+ * - The first rectangle of a shelf sets its height. A rectangle goes on the floor of a shelf at its cursor, or into
290
+ * the strip above a shorter rectangle of the shelf. The strip is the move `move.stack`.
291
+ * - The fit `next` uses only the open shelf: the last shelf that the packing opened. The fit `first` uses the first
292
+ * shelf from the top where the rectangle fits. The fit `best` uses the place with the least height left over.
293
+ * - In a shelf, the rectangle takes the place with the least height left over. A tie goes to a strip, from left to right.
294
+ * - When no shelf has a place, the last shelf grows for a taller rectangle, if it fits. Else a new shelf opens.
295
+ * - A rectangle wider than the outer rectangle overflows. Without `fixedSize`, it goes on a closed shelf of its own at the left edge.
296
+ * - Each place is in the free space and crosses no locked rectangle. Without a space, the outer rectangle and the
297
+ * locked rectangles give the free space.
298
+ */
299
+ function packShelvesInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder, fit: ShelfFit, space: FreeSpace | undefined): Rect<T> {
300
+ if (!SHELF_FITS.includes(fit)) throw new RangeError(`The shelf fit "${String(fit)}" is not one of ${SHELF_FITS.join(", ")}.`);
301
+ const width = outer.size.x;
302
+ const ox = outer.position.x;
303
+ const oy = outer.position.y;
304
+ const limit = outer.fixedSize ? outer.size.y : Infinity;
305
+ const obstacles: Box[] = [];
306
+ let contentBottom = 0;
307
+ for (const rect of rects) {
308
+ if (!rect.locked) continue;
309
+ rect.wasPacked = true;
310
+ const left = rect.position.x - ox;
311
+ const top = rect.position.y - oy;
312
+ obstacles.push({ left, top, right: left + rect.size.x, bottom: top + rect.size.y });
313
+ contentBottom = Math.max(contentBottom, top + rect.size.y);
314
+ }
315
+ const whole: readonly FreeSpan[] = [{ start: 0, end: Infinity }];
316
+ // The x in the band, in the coordinates of the outer rectangle, or Infinity
317
+ const place = (start: number, w: number, top: number, h: number): number => {
318
+ const spans = space === undefined ? whole : space.band(top + oy, top + h + oy).map((s) => ({ start: s.start - ox, end: s.end - ox }));
319
+ return placeInBand(obstacles, spans, start, w, top, top + h);
320
+ };
321
+ // The next top edge where the free spans change, or Infinity
322
+ const stepDown = (top: number, w: number, h: number): number =>
323
+ Math.min(space === undefined ? Infinity : space.nextY(top + oy, top + h + oy, w) - oy, nextTop(obstacles, top, top + h));
324
+
325
+ // The places of a shelf that the sizes allow, by the height left over. A strip comes before the floor on a tie.
326
+ const spotsOn = (shelf: Shelf, w: number, h: number, grow: boolean): Spot[] => {
327
+ const ceiling = shelf.top + shelf.height;
328
+ const spots: Spot[] = [];
329
+ if (!grow) {
330
+ for (const strip of shelf.strips) {
331
+ if (w <= strip.w && strip.bottom + h <= ceiling) spots.push({ shelf, x: strip.x, y: strip.bottom, left: ceiling - strip.bottom - h, strip });
332
+ }
333
+ }
334
+ const tall = grow ? h > shelf.height && shelf.top + h <= limit : h <= shelf.height;
335
+ if (Number.isFinite(shelf.cursor) && tall) spots.push({ shelf, x: shelf.cursor, y: shelf.top, left: grow ? 0 : shelf.height - h, ...(grow ? { grow } : {}) });
336
+ return spots.toSorted((a, b) => a.left - b.left);
337
+ };
338
+ // The place of a spot in the free space, or undefined. A rectangle on the floor can move right of the cursor.
339
+ const verify = (spot: Spot, w: number, h: number): Spot | undefined => {
340
+ const x = place(spot.x, w, spot.y, h);
341
+ if (spot.strip !== undefined) return x === spot.x ? spot : undefined;
342
+ return x + w <= width ? { ...spot, x } : undefined;
343
+ };
344
+ const firstOn = (shelf: Shelf, w: number, h: number): Spot | undefined => {
345
+ for (const spot of spotsOn(shelf, w, h, false)) {
346
+ const found = verify(spot, w, h);
347
+ if (found !== undefined) return found;
348
+ }
349
+ return undefined;
350
+ };
351
+
352
+ const shelves: Shelf[] = [];
353
+ for (const rect of rects.toSorted(compareRects(order))) {
354
+ if (rect.wasPacked) continue;
355
+ const w = rect.size.x;
356
+ const h = rect.size.y;
357
+ if (!(w > 0 && h > 0)) continue;
358
+ const open = shelves.at(-1);
359
+ const floor = open === undefined ? 0 : open.top + open.height;
360
+
361
+ if (w > width) {
362
+ if (outer.fixedSize) continue;
363
+ // An overflowing rectangle goes on a closed shelf of its own, under the locked rectangles at the left edge
364
+ let top = floor;
365
+ while (firstFreeX(obstacles, 0, w, top, top + h) !== 0) {
366
+ const next = nextTop(obstacles, top, top + h);
367
+ if (!Number.isFinite(next)) break;
368
+ top = next;
369
+ }
370
+ rect.position.set(ox, top + oy);
371
+ rect.wasPacked = true;
372
+ shelves.push({ top, height: h, cursor: Infinity, strips: [] });
373
+ contentBottom = Math.max(contentBottom, top + h);
374
+ continue;
375
+ }
376
+
377
+ // Step 1: a shelf where the rectangle fits without growth
378
+ let spot: Spot | undefined;
379
+ let firstShelf: Shelf | undefined;
380
+ if (fit === "best") {
381
+ for (const s of shelves.flatMap((shelf) => spotsOn(shelf, w, h, false)).toSorted((a, b) => a.left - b.left)) {
382
+ spot = verify(s, w, h);
383
+ if (spot !== undefined) break;
384
+ }
385
+ if (spot !== undefined) firstShelf = shelves.find((shelf) => shelf === spot!.shelf || firstOn(shelf, w, h) !== undefined);
386
+ } else {
387
+ for (const shelf of fit === "next" ? shelves.slice(-1) : shelves) {
388
+ spot = firstOn(shelf, w, h);
389
+ if (spot !== undefined) break;
390
+ }
391
+ }
392
+ // Step 2: the last shelf grows. Step 3: a new shelf opens under the last shelf.
393
+ if (spot === undefined && open !== undefined) {
394
+ const grown = spotsOn(open, w, h, true)[0];
395
+ if (grown !== undefined) spot = verify(grown, w, h);
396
+ }
397
+ if (spot === undefined) {
398
+ let top = floor;
399
+ let x = place(0, w, top, h);
400
+ while (!(x + w <= width) && top + h <= limit) {
401
+ const next = stepDown(top, w, h);
402
+ if (!Number.isFinite(next)) break;
403
+ top = next;
404
+ x = place(0, w, top, h);
405
+ }
406
+ if (!(x + w <= width) || top + h > limit) continue;
407
+ const shelf: Shelf = { top, height: h, cursor: x, strips: [] };
408
+ shelves.push(shelf);
409
+ spot = { shelf, x, y: top, left: 0 };
410
+ } else {
411
+ if (spot.shelf !== open) countMove(MOVES.firstFit);
412
+ if (firstShelf !== undefined && spot.shelf !== firstShelf) countMove(MOVES.bestFit);
413
+ }
414
+
415
+ const { shelf } = spot;
416
+ rect.position.set(spot.x + ox, spot.y + oy);
417
+ rect.wasPacked = true;
418
+ if (spot.strip !== undefined) {
419
+ countMove(MOVES.shelf);
420
+ spot.strip.bottom = spot.y + h;
421
+ } else {
422
+ shelf.cursor = spot.x + w;
423
+ shelf.strips.push({ x: spot.x, w, bottom: spot.y + h });
424
+ if (spot.grow === true) shelf.height = h;
425
+ }
426
+ contentBottom = Math.max(contentBottom, spot.y + h);
427
+ }
428
+
429
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
430
+ return outer;
431
+ }