@mark1russell7/packing 0.1.0 → 0.1.2

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 CHANGED
@@ -33,14 +33,33 @@ pack(rects, outer, { strategy: "skyline", sortOrder: "area-desc" });
33
33
 
34
34
  - `packRows` (the default): a guillotine row packing. It fills the space under the short rectangles of each row.
35
35
  - `packSkyline`: each rectangle goes to the lowest place where it fits. It packs around a `locked` rectangle.
36
+ - `packShelf`: it fills rows left to right in the sort order, and goes around each `locked` rectangle. With the sort order `input`, the rows read in your order.
36
37
 
37
38
  Both functions also work alone, in an outer rectangle with a width that you set. They do not change the width.
38
39
 
39
- A `locked` rectangle keeps its position. `packSkyline` puts the other rectangles under it. `packRows` only keeps its position.
40
+ A `locked` rectangle keeps its position. `packSkyline` and `packShelf` put the other rectangles around it. `packRows` only keeps its position.
40
41
 
41
42
  The sort orders are `height-desc` (the default), `height-asc`, `width-desc`, `area-desc` and `input`. `SORT_ORDERS` lists them, and `compareRects` gives the comparator of one.
42
43
 
43
- The order `input` keeps the order of your array. Use it for a ranked list. `packRows` then fills no gaps, so the rows read in your order: left to right, and top to bottom. `packSkyline` places the rectangles in your order, but a later rectangle can go higher than an earlier one.
44
+ The order `input` keeps the order of your array. Use it for a ranked list. `packRows` then fills no gaps, so the rows read in your order: left to right, and top to bottom. `packSkyline` places the rectangles in your order, but a later rectangle can go higher than an earlier one. For a ranked list with `locked` rectangles, use `packShelf`.
45
+
46
+ ## Free width
47
+
48
+ `packFree` searches the width. It tries many widths with one strategy of `pack`, and keeps the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.
49
+
50
+ ```ts
51
+ import { packFree } from "@mark1russell7/packing";
52
+
53
+ const result = packFree(rects, undefined, { strategy: "skyline", aspect: 16 / 9 });
54
+ result.outer.size; // the chosen size
55
+ result.fill; // the area of the rectangles divided by the area of the box
56
+ result.aspectError; // the distance from the target aspect, on a log scale
57
+ result.tried; // each width that the search tried, with its objective
58
+ ```
59
+
60
+ - `aspect` is the target: the width divided by the height. The default is 1, a square.
61
+ - `aspectWeight` sets the weight of the aspect. With 0, only the area counts.
62
+ - The search is deterministic. The same input gives the same result.
44
63
 
45
64
  ## Pushes and measurements
46
65
 
package/build/index.d.ts CHANGED
@@ -86,8 +86,11 @@ interface PackOptions {
86
86
  }
87
87
  /** The options of `pack` */
88
88
  interface PackLayoutOptions extends PackOptions {
89
- /** The packing: `rows` (the default) or `skyline`. Use `skyline` to pack around locked rectangles. */
90
- strategy?: "rows" | "skyline";
89
+ /**
90
+ * The packing: `rows` (the default), `skyline` or `shelf`. `skyline` and `shelf` pack around locked rectangles.
91
+ * Use `shelf` with the sort order `input` to keep the order of the array around locked rectangles.
92
+ */
93
+ strategy?: "rows" | "skyline" | "shelf";
91
94
  }
92
95
  /**
93
96
  * This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does
@@ -103,7 +106,7 @@ export declare function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options?:
103
106
  * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
104
107
  * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
105
108
  * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
106
- * - `options.strategy` selects `packRows` (the default) or `packSkyline` after the width is set.
109
+ * - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
107
110
  */
108
111
  export declare function pack<T>(rects: Rect<T>[], outer?: Rect<T>, options?: PackLayoutOptions): Rect<T>;
109
112
  /**
@@ -130,6 +133,77 @@ export declare function lowestWindow(skyline: ArrayLike<number>, width: number,
130
133
  export declare function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options?: {
131
134
  sortOrder?: SortOrder;
132
135
  }): Rect<T>;
136
+ /** The options of `packShelf` */
137
+ interface ShelfOptions {
138
+ /** The order of the rectangles before the packing. The default is `height-desc`. */
139
+ sortOrder?: SortOrder;
140
+ }
141
+ /**
142
+ * This function packs rectangles into shelves: rows that it fills left to right in the sort order, and does not fill
143
+ * again. It goes around each `locked` rectangle. It sets the position of each rectangle and gives the outer rectangle.
144
+ *
145
+ * - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.
146
+ * - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.
147
+ * - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,
148
+ * a new row starts under the current row.
149
+ * - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
150
+ * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
151
+ * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
152
+ */
153
+ export declare function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options?: ShelfOptions): Rect<T>;
154
+ /** The options of `packFree` */
155
+ interface FreeWidthOptions extends PackLayoutOptions {
156
+ /** The target aspect: the width divided by the height. The default is 1, a square. */
157
+ aspect?: number;
158
+ /**
159
+ * The weight of the aspect error in the objective. The objective is the area times `1 + aspectWeight * aspectError`.
160
+ * The default is 1. With 0, only the area counts.
161
+ */
162
+ aspectWeight?: number;
163
+ /** The number of widths that the first pass tries. The default is 48. */
164
+ samples?: number;
165
+ }
166
+ /** One width that `packFree` tried, and its result */
167
+ interface FreeWidthTry {
168
+ /** The width of the container for this try */
169
+ width: number;
170
+ /** The width of the content: the right edge of the rectangles */
171
+ contentWidth: number;
172
+ /** The height of the content */
173
+ contentHeight: number;
174
+ /** The objective of this try. Lower is better. */
175
+ objective: number;
176
+ }
177
+ /** The result of `packFree` */
178
+ interface FreeWidthResult<T> {
179
+ /** The outer rectangle, with the size of the content */
180
+ outer: Rect<T>;
181
+ /** The area of the bounding box of the content */
182
+ area: number;
183
+ /** The area of the rectangles divided by the area of the bounding box */
184
+ fill: number;
185
+ /** The aspect of the bounding box: the width divided by the height */
186
+ aspect: number;
187
+ /** The distance from the target aspect: the absolute value of `log(aspect) - log(target)` */
188
+ aspectError: number;
189
+ /** The objective of the chosen width. Lower is better. */
190
+ objective: number;
191
+ /** Each width that the search tried, in the order of the search */
192
+ tried: FreeWidthTry[];
193
+ }
194
+ /** This function gives the distance between an aspect and a target aspect on a log scale. */
195
+ export declare function aspectError(width: number, height: number, target?: number): number;
196
+ /**
197
+ * This function packs rectangles with a free width. It tries many widths with the strategy of `options`, and keeps
198
+ * the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.
199
+ *
200
+ * - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.
201
+ * - The second pass tries the widths between the two neighbours of the best width of the first pass.
202
+ * - The search is deterministic: the same input gives the same widths and the same result.
203
+ * - A `locked` rectangle keeps its position. The width is not less than its right edge.
204
+ * - Each strategy of `pack` works with this function, because it only changes the width.
205
+ */
206
+ export declare function packFree<T>(rects: Rect<T>[], outer?: Rect<T>, options?: FreeWidthOptions): FreeWidthResult<T>;
133
207
  /** The options of `pushCascade`. */
134
208
  interface PushOptions {
135
209
  /** The space between a shoved rectangle and the rectangle that shoves it. The default is 8. */
@@ -184,5 +258,5 @@ interface MetricsOptions {
184
258
  * ```
185
259
  */
186
260
  export declare function packingMetrics(rects: readonly Rect[], options?: MetricsOptions): PackingMetrics;
187
- export type { MetricsOptions, PackLayoutOptions, PackOptions, PackingMetrics, PushOptions, SortOrder };
261
+ export type { FreeWidthOptions, FreeWidthResult, FreeWidthTry, MetricsOptions, PackLayoutOptions, PackOptions, PackingMetrics, PushOptions, ShelfOptions, SortOrder };
188
262
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/vector.ts","../src/rect.ts","../src/order.ts","../src/pack.ts","../src/skyline.ts","../src/push.ts","../src/metrics.ts"],"mappings":";qBACa;;EAIX,YAAY,YAAO;;MAMf;MACA,EAAE;;MAEF;MACA,EAAE;;EAGN,IAAI,WAAW;;EAMf,MAAM,GAAG;;EAMT,IAAI,OAAO,SAAS;;EAKpB;;EAKA,SAAS;;EAKT,MAAM,KAAK,QAAQ,KAAK,SAAS;;SAQ1B,GAAG,iBAAiB;;SAKpB,IAAI,SAAS,WAAW;;SASxB,IAAI,SAAS,WAAW;;aASpB,YAAY;;;qBCxEZ,KAAK;;;EAKhB,KAAK;;EAEL;;EAEA;;EAEA;;;;;EAKA;;MAGI,YAAY;;MAEZ,QAAQ;;EAGZ;;EAEA;;EAGA,WAAW;;EAOX,YAAY,QAAQ;;EAMpB;;SAGO,OAAO,GAAG,OAAO,KAAK,OAAO,KAAK;;SAKlC,SAAS,GAAG,OAAO,KAAK,OAAO,KAAK;;SAKpC,YAAY,GAAG,OAAO,KAAK;;SAK3B,SAAS,GAAG,OAAO,KAAK;;SAKxB,UAAU,GAAG,OAAO,KAAK;;;;;;KC/DtB;;qBAGC,sBAAsB;;wBAGnB,aAAa,OAAO,aAAa,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK;;UCNvD;;EAEf,YAAY;;;UAIG,0BAA0B;;EAEzC;;;;;;;wBAqEc,SAAS,GAAG,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,UAAS,cAAmB,KAAK;;;;;;;;;;;wBAiB/E,KAAK,GAAG,OAAO,KAAK,MAAM,QAAO,KAAK,IAAiB,UAAS,oBAAyB,KAAK;;;;;;wBC5F9F,aAAa,SAAS,mBAAmB,eAAe;EAAe;EAAW;;;;;;;;;;;;;;wBAiClF,YAAY,GAAG,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI;EAAW,YAAY;IAAmB,KAAK;;UCtC/F;;EAEf;;EAEA;;;;;;;;;;;;;wBAqDc,YAAY,GAAG,QAAQ,KAAK,IAAI,gBAAgB,KAAK,MAAM,UAAS,cAAmB,KAAK;;UCzD3F;;EAEf;;EAEA;IAAQ;IAAe;;;EAEvB;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIe;;EAEf;;;;;;;;;;;wBAYc,eAAe,gBAAgB,QAAQ,UAAS,iBAAsB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/vector.ts","../src/rect.ts","../src/order.ts","../src/pack.ts","../src/skyline.ts","../src/shelf.ts","../src/free.ts","../src/push.ts","../src/metrics.ts"],"mappings":";qBACa;;EAIX,YAAY,YAAO;;MAMf;MACA,EAAE;;MAEF;MACA,EAAE;;EAGN,IAAI,WAAW;;EAMf,MAAM,GAAG;;EAMT,IAAI,OAAO,SAAS;;EAKpB;;EAKA,SAAS;;EAKT,MAAM,KAAK,QAAQ,KAAK,SAAS;;SAQ1B,GAAG,iBAAiB;;SAKpB,IAAI,SAAS,WAAW;;SASxB,IAAI,SAAS,WAAW;;aASpB,YAAY;;;qBCxEZ,KAAK;;;EAKhB,KAAK;;EAEL;;EAEA;;EAEA;;;;;EAKA;;MAGI,YAAY;;MAEZ,QAAQ;;EAGZ;;EAEA;;EAGA,WAAW;;EAOX,YAAY,QAAQ;;EAMpB;;SAGO,OAAO,GAAG,OAAO,KAAK,OAAO,KAAK;;SAKlC,SAAS,GAAG,OAAO,KAAK,OAAO,KAAK;;SAKpC,YAAY,GAAG,OAAO,KAAK;;SAK3B,SAAS,GAAG,OAAO,KAAK;;SAKxB,UAAU,GAAG,OAAO,KAAK;;;;;;KC/DtB;;qBAGC,sBAAsB;;wBAGnB,aAAa,OAAO,aAAa,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK;;UCLvD;;EAEf,YAAY;;;UAIG,0BAA0B;;;;;EAKzC;;;;;;;wBAqEc,SAAS,GAAG,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,UAAS,cAAmB,KAAK;;;;;;;;;;;wBAiB/E,KAAK,GAAG,OAAO,KAAK,MAAM,QAAO,KAAK,IAAiB,UAAS,oBAAyB,KAAK;;;;;;wBChG9F,aAAa,SAAS,mBAAmB,eAAe;EAAe;EAAW;;;;;;;;;;;;;;wBAiClF,YAAY,GAAG,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI;EAAW,YAAY;IAAmB,KAAK;;UCrC/F;;EAEf,YAAY;;;;;;;;;;;;;;wBAiDE,UAAU,GAAG,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,UAAS,eAAoB,KAAK;;UCnDhF,yBAAyB;;EAExC;;;;;EAKA;;EAEA;;;UAIe;;EAEf;;EAEA;;EAEA;;EAEA;;;UAIe,gBAAgB;;EAE/B,OAAO,KAAK;;EAEZ;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,OAAO;;;wBAIO,YAAY,eAAe,gBAAgB;;;;;;;;;;;wBAe3C,SAAS,GAAG,OAAO,KAAK,MAAM,QAAO,KAAK,IAAiB,UAAS,mBAAwB,gBAAgB;;UC3D3G;;EAEf;;EAEA;;;;;;;;;;;;;wBAqDc,YAAY,GAAG,QAAQ,KAAK,IAAI,gBAAgB,KAAK,MAAM,UAAS,cAAmB,KAAK;;UCzD3F;;EAEf;;EAEA;IAAQ;IAAe;;;EAEvB;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIe;;EAEf;;;;;;;;;;;wBAYc,eAAe,gBAAgB,QAAQ,UAAS,iBAAsB"}
package/build/index.js CHANGED
@@ -155,6 +155,103 @@ function compareRects(order) {
155
155
  case "input": return () => 0;
156
156
  }
157
157
  }
158
+ // src/shelf.ts
159
+ /**
160
+ * This function gives the first x from `start` where a rectangle of width `w` crosses no obstacle in the band from
161
+ * `top` to `bottom`.
162
+ */
163
+ function firstFreeX(obstacles, start, w, top, bottom) {
164
+ let x = start;
165
+ let moved = true;
166
+ while (moved) {
167
+ moved = false;
168
+ for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.left < x + w && x < o.right) {
169
+ x = o.right;
170
+ moved = true;
171
+ }
172
+ }
173
+ return x;
174
+ }
175
+ /** This function gives the lowest bottom edge of the obstacles in the band from `top` to `bottom`, under `top`. */
176
+ function nextTop(obstacles, top, bottom) {
177
+ let next = Infinity;
178
+ for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.bottom > top) next = Math.min(next, o.bottom);
179
+ return next;
180
+ }
181
+ /**
182
+ * This function packs rectangles into shelves: rows that it fills left to right in the sort order, and does not fill
183
+ * again. It goes around each `locked` rectangle. It sets the position of each rectangle and gives the outer rectangle.
184
+ *
185
+ * - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.
186
+ * - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.
187
+ * - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,
188
+ * a new row starts under the current row.
189
+ * - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
190
+ * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
191
+ * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
192
+ */
193
+ function packShelf(rects, outer, options = {}) {
194
+ const width = outer.size.x;
195
+ const obstacles = [];
196
+ let contentBottom = 0;
197
+ for (const rect of rects) {
198
+ if (!rect.locked) continue;
199
+ rect.wasPacked = true;
200
+ const left = rect.position.x - outer.position.x;
201
+ const top = rect.position.y - outer.position.y;
202
+ obstacles.push({
203
+ left,
204
+ top,
205
+ right: left + rect.size.x,
206
+ bottom: top + rect.size.y
207
+ });
208
+ contentBottom = Math.max(contentBottom, top + rect.size.y);
209
+ }
210
+ let rowTop = 0;
211
+ let rowBottom = 0;
212
+ let cursor = 0;
213
+ let rowEmpty = true;
214
+ for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
215
+ if (rect.wasPacked) continue;
216
+ const w = rect.size.x;
217
+ const h = rect.size.y;
218
+ if (!(w > 0 && h > 0)) continue;
219
+ const overflows = w > width;
220
+ if (overflows && outer.fixedSize) continue;
221
+ if (overflows && !rowEmpty) {
222
+ rowTop = rowBottom;
223
+ cursor = 0;
224
+ rowEmpty = true;
225
+ }
226
+ let x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
227
+ while (!overflows && x + w > width) {
228
+ rowTop = rowEmpty ? nextTop(obstacles, rowTop, rowTop + h) : rowBottom;
229
+ rowBottom = rowTop;
230
+ cursor = 0;
231
+ rowEmpty = true;
232
+ x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
233
+ }
234
+ while (overflows && x !== 0) {
235
+ rowTop = nextTop(obstacles, rowTop, rowTop + h);
236
+ rowBottom = rowTop;
237
+ x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
238
+ }
239
+ if (outer.fixedSize && rowTop + h > outer.size.y) continue;
240
+ rect.position.set(x + outer.position.x, rowTop + outer.position.y);
241
+ rect.wasPacked = true;
242
+ rowEmpty = false;
243
+ cursor = x + w;
244
+ rowBottom = Math.max(rowBottom, rowTop + h);
245
+ contentBottom = Math.max(contentBottom, rowTop + h);
246
+ if (overflows) {
247
+ rowTop = rowBottom;
248
+ cursor = 0;
249
+ rowEmpty = true;
250
+ }
251
+ }
252
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
253
+ return outer;
254
+ }
158
255
  // src/skyline.ts
159
256
  /**
160
257
  * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
@@ -301,7 +398,7 @@ function packRows(rects, outer, options = {}) {
301
398
  * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
302
399
  * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
303
400
  * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
304
- * - `options.strategy` selects `packRows` (the default) or `packSkyline` after the width is set.
401
+ * - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
305
402
  */
306
403
  function pack(rects, outer = new Rect(), options = {}) {
307
404
  let totalArea = 0;
@@ -318,6 +415,7 @@ function pack(rects, outer = new Rect(), options = {}) {
318
415
  const ideal = Vector.of(idealWidth).clamp(new Vector(minX, minY), Vector.Infinity);
319
416
  if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
320
417
  if (options.strategy === "skyline") packSkyline(rects, outer, options);
418
+ else if (options.strategy === "shelf") packShelf(rects, outer, options);
321
419
  else packRows(rects, outer, options);
322
420
  if (!outer.fixedSize) {
323
421
  const contentRight = Rect.maxRight(rects);
@@ -327,6 +425,109 @@ function pack(rects, outer = new Rect(), options = {}) {
327
425
  }
328
426
  return outer;
329
427
  }
428
+ // src/free.ts
429
+ /** This function gives the distance between an aspect and a target aspect on a log scale. */
430
+ function aspectError(width, height, target = 1) {
431
+ if (!(width > 0 && height > 0 && target > 0)) return Infinity;
432
+ return Math.abs(Math.log(width / height) - Math.log(target));
433
+ }
434
+ /**
435
+ * This function packs rectangles with a free width. It tries many widths with the strategy of `options`, and keeps
436
+ * the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.
437
+ *
438
+ * - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.
439
+ * - The second pass tries the widths between the two neighbours of the best width of the first pass.
440
+ * - The search is deterministic: the same input gives the same widths and the same result.
441
+ * - A `locked` rectangle keeps its position. The width is not less than its right edge.
442
+ * - Each strategy of `pack` works with this function, because it only changes the width.
443
+ */
444
+ function packFree(rects, outer = new Rect(), options = {}) {
445
+ const target = options.aspect ?? 1;
446
+ const weight = options.aspectWeight ?? 1;
447
+ const samples = Math.max(2, Math.floor(options.samples ?? 48));
448
+ const ox = outer.position.x;
449
+ const oy = outer.position.y;
450
+ let itemArea = 0;
451
+ let minWidth = 0;
452
+ let rowWidth = 0;
453
+ for (const r of rects) {
454
+ if (r.locked) {
455
+ minWidth = Math.max(minWidth, r.right() - ox);
456
+ continue;
457
+ }
458
+ if (!(r.size.x > 0 && r.size.y > 0)) continue;
459
+ itemArea += r.area();
460
+ minWidth = Math.max(minWidth, r.size.x);
461
+ rowWidth += r.size.x;
462
+ }
463
+ for (const r of rects) if (r.locked) itemArea += r.area();
464
+ const maxWidth = Math.max(minWidth, rowWidth);
465
+ const tried = [];
466
+ const run = (width) => {
467
+ Rect.resetPacked(rects);
468
+ const box = new Rect();
469
+ box.position.setTo(outer.position);
470
+ box.size.set(width, 0);
471
+ box.fixedWidth = true;
472
+ pack(rects, box, options);
473
+ const placed = Rect.packed(rects);
474
+ const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);
475
+ const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);
476
+ const area = contentWidth * contentHeight;
477
+ const t = {
478
+ width,
479
+ contentWidth,
480
+ contentHeight,
481
+ objective: area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0
482
+ };
483
+ tried.push(t);
484
+ return t;
485
+ };
486
+ const widths = [];
487
+ if (maxWidth > 0) {
488
+ const ratio = maxWidth / minWidth;
489
+ for (let i = 0; i < samples; i++) {
490
+ const w = minWidth * ratio ** (i / (samples - 1));
491
+ const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));
492
+ if (widths.at(-1) !== rounded) widths.push(rounded);
493
+ }
494
+ }
495
+ let best = void 0;
496
+ let bestIndex = 0;
497
+ for (const [i, w] of widths.entries()) {
498
+ const t = run(w);
499
+ if (best === void 0 || t.objective < best.objective) {
500
+ best = t;
501
+ bestIndex = i;
502
+ }
503
+ }
504
+ if (widths.length > 2) {
505
+ const low = widths[Math.max(0, bestIndex - 1)];
506
+ const high = widths[Math.min(widths.length - 1, bestIndex + 1)];
507
+ const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));
508
+ for (let i = 1; i <= steps; i++) {
509
+ const w = Math.round(low + (high - low) * i / (steps + 1));
510
+ if (widths.includes(w)) continue;
511
+ const t = run(w);
512
+ if (best === void 0 || t.objective < best.objective) best = t;
513
+ }
514
+ }
515
+ const chosen = best === void 0 ? void 0 : run(best.width);
516
+ tried.pop();
517
+ const w = chosen?.contentWidth ?? 0;
518
+ const h = chosen?.contentHeight ?? 0;
519
+ if (!outer.fixedSize) outer.size.set(w, h);
520
+ const area = w * h;
521
+ return {
522
+ outer,
523
+ area,
524
+ fill: area > 0 ? itemArea / area : 0,
525
+ aspect: h > 0 ? w / h : 0,
526
+ aspectError: aspectError(w, h, target),
527
+ objective: chosen?.objective ?? 0,
528
+ tried
529
+ };
530
+ }
330
531
  // src/push.ts
331
532
  /** This function gives the overlap of two rectangles on each axis. A value of 0 or less is no overlap. */
332
533
  function overlapOf(a, b) {
@@ -457,6 +658,6 @@ function packingMetrics(rects, options = {}) {
457
658
  widestOverMedian: median > 0 ? widest / median : 0
458
659
  };
459
660
  }
460
- export { Rect, SORT_ORDERS, Vector, compareRects, lowestWindow, pack, packRows, packSkyline, packingMetrics, pushCascade };
661
+ export { Rect, SORT_ORDERS, Vector, aspectError, compareRects, lowestWindow, pack, packFree, packRows, packShelf, packSkyline, packingMetrics, pushCascade };
461
662
 
462
663
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/vector.ts","../src/rect.ts","../src/order.ts","../src/skyline.ts","../src/pack.ts","../src/push.ts","../src/metrics.ts"],"sourcesContent":["/** A mutable two-dimensional vector. */\nexport class Vector {\n #x: number;\n #y: number;\n\n constructor(x = 0, y = 0) {\n this.#x = x;\n this.#y = y;\n }\n\n /** The first component: a width or a left position. */\n get x(): number { return this.#x; }\n set x(v: number) { this.#x = v; }\n /** The second component: a height or a top position. */\n get y(): number { return this.#y; }\n set y(v: number) { this.#y = v; }\n\n /** This method sets both components. */\n set(x: number, y: number): void {\n this.#x = x;\n this.#y = y;\n }\n\n /** This method copies the components of another vector. */\n setTo(v: Vector): void {\n this.#x = v.x;\n this.#y = v.y;\n }\n\n /** This method gives a new vector: the sum of this vector and another. */\n add(other: Vector): Vector {\n return new Vector(this.#x + other.x, this.#y + other.y);\n }\n\n /** This method gives the product of the components: the area of a size. */\n area(): number {\n return this.#x * this.#y;\n }\n\n /** This method gives a new vector with the same components. */\n clone(): Vector {\n return new Vector(this.#x, this.#y);\n }\n\n /** This method gives a new vector with each component between the components of `min` and `max`. */\n clamp(min: Vector, max: Vector): Vector {\n return new Vector(\n Math.max(min.x, Math.min(max.x, this.#x)),\n Math.max(min.y, Math.min(max.y, this.#y)),\n );\n }\n\n /** This function gives a vector with the same value in both components. */\n static of(scalar: number): Vector {\n return new Vector(scalar, scalar);\n }\n\n /** This function gives the smallest value of each component, or a zero vector for an empty list. */\n static min(vectors: Vector[]): Vector {\n if (!vectors.length) return new Vector();\n return new Vector(\n Math.min(...vectors.map((v) => v.x)),\n Math.min(...vectors.map((v) => v.y)),\n );\n }\n\n /** This function gives the largest value of each component, or a zero vector for an empty list. */\n static max(vectors: Vector[]): Vector {\n if (!vectors.length) return new Vector();\n return new Vector(\n Math.max(...vectors.map((v) => v.x)),\n Math.max(...vectors.map((v) => v.y)),\n );\n }\n\n /** This getter gives a new infinite vector each time. A shared value is not safe, because a vector is mutable. */\n static get Infinity(): Vector {\n return new Vector(Infinity, Infinity);\n }\n}\n","import { Vector } from \"./vector.ts\";\n\n/** A rectangle of the packer. `id` identifies the item that it measures. The packer sets `position` and `wasPacked`. */\n// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- the parameter types the id of the caller\nexport class Rect<T = unknown> {\n readonly #position = new Vector();\n readonly #size = new Vector();\n\n /** The identity of the item that the rectangle measures. */\n id?: T;\n /** The packer sets this field to `true` when it places the rectangle. */\n wasPacked = false;\n /** For an outer rectangle: the packer keeps its width. */\n fixedWidth = false;\n /** For an outer rectangle: the packer keeps its width and its height, and it skips a rectangle that does not fit. */\n fixedSize = false;\n /**\n * The packer keeps the position of a locked rectangle. `packSkyline` packs the other rectangles around it.\n * `packRows` only keeps its position, and other rectangles can overlap it.\n */\n locked = false;\n\n /** The top left corner. */\n get position(): Vector { return this.#position; }\n /** The width and the height. */\n get size(): Vector { return this.#size; }\n\n /** This method gives the right edge. */\n right(): number { return this.#position.x + this.#size.x; }\n /** This method gives the bottom edge. */\n bottom(): number { return this.#position.y + this.#size.y; }\n\n /** This method moves the left edge to the right by an amount, and the right edge stays. */\n shrinkLeft(amount: number): this {\n this.#position.x += amount;\n this.#size.x -= amount;\n return this;\n }\n\n /** This method moves the rectangle by an offset. */\n translateBy(offset: Vector): this {\n this.#position.set(this.#position.x + offset.x, this.#position.y + offset.y);\n return this;\n }\n\n /** This method gives the area of the size. */\n area(): number { return this.#size.area(); }\n\n /** This function gives the rectangles that the packer placed. */\n static packed<U>(rects: Rect<U>[]): Rect<U>[] {\n return rects.filter((r) => r.wasPacked);\n }\n\n /** This function gives the rectangles that the packer did not place. */\n static unpacked<U>(rects: Rect<U>[]): Rect<U>[] {\n return rects.filter((r) => !r.wasPacked);\n }\n\n /** This function marks each rectangle as not placed. A locked rectangle stays placed. */\n static resetPacked<U>(rects: Rect<U>[]): void {\n for (const r of rects) r.wasPacked = r.locked;\n }\n\n /** This function gives the largest right edge, or 0 for an empty list. */\n static maxRight<U>(rects: Rect<U>[]): number {\n return rects.reduce((acc, r) => Math.max(acc, r.right()), 0);\n }\n\n /** This function gives the largest bottom edge, or 0 for an empty list. */\n static maxBottom<U>(rects: Rect<U>[]): number {\n return rects.reduce((acc, r) => Math.max(acc, r.bottom()), 0);\n }\n}\n","import type { Rect } from \"./rect.ts\";\n\n/**\n * An order of the rectangles before the packing. The order changes the layout much. `input` keeps the order of\n * the array of the caller: the row packing then fills no gaps, so its rows read in that order.\n */\nexport type SortOrder = \"height-desc\" | \"height-asc\" | \"width-desc\" | \"area-desc\" | \"input\";\n\n/** This constant lists each sort order. The first one is the default. */\nexport const SORT_ORDERS: readonly SortOrder[] = [\"height-desc\", \"height-asc\", \"width-desc\", \"area-desc\", \"input\"];\n\n/** This function gives the comparator of a sort order. The sort is stable, so `input` keeps the order. */\nexport function compareRects(order: SortOrder): <T>(a: Rect<T>, b: Rect<T>) => number {\n switch (order) {\n case \"height-desc\":\n return (a, b) => b.size.y - a.size.y;\n case \"height-asc\":\n return (a, b) => a.size.y - b.size.y;\n case \"width-desc\":\n return (a, b) => b.size.x - a.size.x;\n case \"area-desc\":\n return (a, b) => b.area() - a.area();\n case \"input\":\n return () => 0;\n }\n}\n","import { compareRects, type SortOrder } from \"./order.ts\";\nimport type { Rect } from \"./rect.ts\";\n\n/**\n * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height\n * of the window is the largest value of `skyline[x, x + rw)`. The first `x` with the smallest height wins. A\n * monotonic deque makes the search O(width) for one rectangle.\n */\nexport function lowestWindow(skyline: ArrayLike<number>, width: number, rw: number): { x: number; y: number } {\n const deque = new Int32Array(width);\n let head = 0;\n let tail = 0;\n let bestX = 0;\n let bestY = Infinity;\n for (let i = 0; i < width; i++) {\n while (tail > head && skyline[deque[tail - 1]!]! <= skyline[i]!) tail--;\n deque[tail++] = i;\n if (deque[head]! <= i - rw) head++;\n if (i >= rw - 1) {\n const max = skyline[deque[head]!]!;\n if (max < bestY) {\n bestY = max;\n bestX = i - rw + 1;\n }\n }\n }\n return { x: bestX, y: bestY };\n}\n\n/**\n * This function packs rectangles into an outer rectangle with a skyline. It puts each rectangle in the lowest place\n * where it fits, and it does not change the width of the outer rectangle.\n *\n * - A `locked` rectangle keeps its position. The skyline starts at its bottom edge across its width, thus the other\n * rectangles go under it. The space above it stays empty, because a skyline has no holes.\n * - The skyline has a cell for each whole unit of the width. A rectangle that needs more cells than the outer\n * rectangle has goes under all other rectangles, at the left edge.\n * - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that\n * does not fit is not packed: its `wasPacked` stays `false`.\n * - The default sort order is `height-desc`. The order of the array of the caller stays.\n */\nexport function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options: { sortOrder?: SortOrder } = {}): Rect<T> {\n // a cell for each whole unit of the width: a rectangle never goes past the right edge\n const width = Math.floor(outer.size.x);\n if (width <= 0) return outer;\n const skyline = new Float64Array(width);\n\n for (const rect of rects) {\n if (!rect.locked) continue;\n rect.wasPacked = true;\n const x0 = Math.max(0, Math.floor(rect.position.x - outer.position.x));\n const x1 = Math.min(width, Math.ceil(rect.position.x - outer.position.x + rect.size.x));\n const bottom = rect.position.y - outer.position.y + rect.size.y;\n for (let i = x0; i < x1; i++) skyline[i] = Math.max(skyline[i]!, bottom);\n if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, bottom);\n }\n\n for (const rect of rects.toSorted(compareRects(options.sortOrder ?? \"height-desc\"))) {\n if (rect.wasPacked) continue;\n const rw = Math.ceil(rect.size.x);\n const rh = rect.size.y;\n if (rw <= 0 || rh <= 0) continue;\n\n let x: number;\n let y: number;\n if (rw > width) {\n if (outer.fixedSize) continue;\n x = 0;\n y = 0;\n for (let i = 0; i < width; i++) if (skyline[i]! > y) y = skyline[i]!;\n } else {\n ({ x, y } = lowestWindow(skyline, width, rw));\n if (outer.fixedSize && y + rh > outer.size.y) continue;\n }\n\n rect.position.set(x + outer.position.x, y + outer.position.y);\n rect.wasPacked = true;\n const top = y + rh;\n for (let i = x; i < Math.min(width, x + rw); i++) skyline[i] = top;\n if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);\n }\n return outer;\n}\n","import { compareRects, type SortOrder } from \"./order.ts\";\nimport { Rect } from \"./rect.ts\";\nimport { packSkyline } from \"./skyline.ts\";\nimport { Vector } from \"./vector.ts\";\n\n/** The options of `pack` and `packRows` */\nexport interface PackOptions {\n /** The order of the rectangles before the packing. The default is `height-desc`. */\n sortOrder?: SortOrder;\n}\n\n/** The options of `pack` */\nexport interface PackLayoutOptions extends PackOptions {\n /** The packing: `rows` (the default) or `skyline`. Use `skyline` to pack around locked rectangles. */\n strategy?: \"rows\" | \"skyline\";\n}\n\nfunction packInto<T>(rects: Rect<T>[], region: Rect<T>, offset: Vector, order: SortOrder): void {\n const unpacked = Rect.unpacked(rects);\n pack(unpacked, region, { sortOrder: order });\n for (const r of Rect.packed(unpacked)) r.translateBy(offset);\n}\n\nfunction packRow<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder): boolean {\n const row: Rect<T>[] = [];\n // toSorted gives a sorted copy, thus the order of the array of the caller stays\n const byHeight = rects.toSorted(compareRects(order));\n // A gap fill puts a later rect before an earlier one, thus the order `input` fills no gaps\n const fillGaps = order !== \"input\";\n\n const rowRect = new Rect<T>();\n rowRect.size.set(outer.size.x, 0);\n\n for (const rect of byHeight) {\n if (rect.wasPacked) continue;\n\n if (rect.size.x > rowRect.size.x) {\n if (outer.fixedSize && rect.size.x > outer.size.x) continue;\n\n const outerLeft = new Rect<T>();\n outerLeft.position.setTo(rowRect.position);\n outerLeft.size.set(outer.size.x - rowRect.position.x, rowRect.size.y);\n outerLeft.fixedSize = true;\n if (fillGaps) packInto(rects, outerLeft, outer.position, order);\n\n rowRect.position.set(0, rowRect.bottom());\n rowRect.size.set(outer.size.x, 0);\n row.length = 0;\n }\n\n if (rowRect.position.y + rect.size.y > outer.size.y) {\n if (outer.fixedSize) continue;\n outer.size.y += rect.size.y;\n if (!outer.fixedWidth && !outer.size.x) outer.size.x = rect.size.x;\n Rect.resetPacked(rects);\n return false;\n }\n\n row.push(rect);\n rect.position.setTo(rowRect.position.add(outer.position));\n rowRect.shrinkLeft(rect.size.x);\n rect.wasPacked = true;\n\n if (rect.size.y > rowRect.size.y) rowRect.size.y = rect.size.y;\n\n if (fillGaps && row.length > 1) {\n const rectInRow = row[row.length - 1]!;\n const outerLeft = new Rect<T>();\n outerLeft.position.set(rectInRow.position.x, rectInRow.bottom());\n outerLeft.size.set(rectInRow.size.x, rowRect.size.y - rectInRow.size.y);\n outerLeft.fixedWidth = true;\n outerLeft.fixedSize = true;\n packInto(rects, outerLeft, outer.position, order);\n }\n }\n return true;\n}\n\n/**\n * This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does\n * not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps\n * its position, but the rows do not go around it. `pack` sets the width first and then uses this function.\n */\nexport function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options: PackOptions = {}): Rect<T> {\n const order = options.sortOrder ?? \"height-desc\";\n for (const r of rects) if (r.locked) r.wasPacked = true;\n while (!packRow(rects, outer, order));\n return outer;\n}\n\n/**\n * This function packs rectangles into an outer rectangle with a guillotine row packing. It sets the position\n * of each rectangle and the size of the outer rectangle, and it gives the outer rectangle.\n *\n * - Without fixed sizes, the outer rectangle starts with a width of 1.5 times the square root of the total area.\n * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.\n * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.\n * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.\n * - `options.strategy` selects `packRows` (the default) or `packSkyline` after the width is set.\n */\nexport function pack<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options: PackLayoutOptions = {}): Rect<T> {\n let totalArea = 0;\n let widest = 0;\n let minX = rects.length > 0 ? Infinity : 0;\n let minY = rects.length > 0 ? Infinity : 0;\n for (const r of rects) {\n totalArea += r.area();\n widest = Math.max(widest, r.size.x);\n minX = Math.min(minX, r.size.x);\n minY = Math.min(minY, r.size.y);\n }\n\n const idealWidth = outer.fixedWidth\n ? outer.size.x\n : Math.max(widest, Math.min(1.5 * Math.sqrt(totalArea), outer.size.x || Infinity));\n\n const ideal = Vector.of(idealWidth).clamp(new Vector(minX, minY), Vector.Infinity);\n\n // A fixed width stays, also when a rectangle is wider: that rectangle overflows\n if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);\n if (options.strategy === \"skyline\") packSkyline(rects, outer, options);\n else packRows(rects, outer, options);\n\n if (!outer.fixedSize) {\n const contentRight = Rect.maxRight(rects);\n if (!outer.size.x) outer.size.x = contentRight;\n else if (!outer.fixedWidth) outer.size.x = Math.min(contentRight, outer.size.x);\n outer.size.y = Rect.maxBottom(rects);\n }\n\n return outer;\n}\n","import type { Rect } from \"./rect.ts\";\n\n/** The options of `pushCascade`. */\nexport interface PushOptions {\n /** The space between a shoved rectangle and the rectangle that shoves it. The default is 8. */\n gap?: number;\n /** The most shoves. The default is eight for each rectangle. The cascade stops at this limit. */\n maxShoves?: number;\n}\n\n/** A direction of a shove: an axis and a sign. */\ninterface Direction {\n readonly axis: \"x\" | \"y\";\n readonly sign: 1 | -1;\n}\n\n/** This function gives the overlap of two rectangles on each axis. A value of 0 or less is no overlap. */\nfunction overlapOf(a: Rect, b: Rect): { x: number; y: number } {\n return {\n x: Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x),\n y: Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y),\n };\n}\n\n/**\n * This function gives the direction of a first shove. The axis has the smaller overlap. The sign points away from the\n * center of the pusher. When that shove crosses 0, the sign is positive.\n */\nfunction firstDirection(pusher: Rect, r: Rect, gap: number): Direction {\n const overlap = overlapOf(pusher, r);\n if (overlap.x < overlap.y) {\n const back = r.position.x + r.size.x / 2 < pusher.position.x + pusher.size.x / 2 && pusher.position.x - r.size.x - gap >= 0;\n return { axis: \"x\", sign: back ? -1 : 1 };\n }\n const back = r.position.y + r.size.y / 2 < pusher.position.y + pusher.size.y / 2 && pusher.position.y - r.size.y - gap >= 0;\n return { axis: \"y\", sign: back ? -1 : 1 };\n}\n\n/** This function puts a rectangle past the pusher, along a direction. The rectangle does not go past 0. */\nfunction shove(pusher: Rect, r: Rect, direction: Direction, gap: number): void {\n if (direction.axis === \"x\") {\n const x = direction.sign > 0 ? pusher.right() + gap : pusher.position.x - r.size.x - gap;\n r.position.set(Math.max(0, x), r.position.y);\n return;\n }\n const y = direction.sign > 0 ? pusher.bottom() + gap : pusher.position.y - r.size.y - gap;\n r.position.set(r.position.x, Math.max(0, y));\n}\n\n/**\n * This function moves each rectangle that a placed rectangle overlaps, and the rectangles that those overlap in turn.\n * A shove takes the axis of the smaller overlap, away from the pusher. A shoved rectangle keeps the direction of its first\n * shove when it shoves the next one, so the cascade goes outward. The placed rectangle and each locked rectangle stay.\n * The function moves the rectangles in place and gives the rectangles that moved.\n *\n * @example\n * ```ts\n * const moved = pushCascade(dropped, cards, { gap: 8 });\n * ```\n */\nexport function pushCascade<T>(placed: Rect<T>, rects: readonly Rect<T>[], options: PushOptions = {}): Rect<T>[] {\n const gap = options.gap ?? 8;\n const maxShoves = options.maxShoves ?? rects.length * 8;\n const moved = new Set<Rect<T>>();\n const queue: { pusher: Rect<T>; direction?: Direction }[] = [{ pusher: placed }];\n let shoves = 0;\n while (shoves < maxShoves) {\n const step = queue.shift();\n if (!step) break;\n const { pusher, direction } = step;\n for (const r of rects) {\n if (r === pusher || r === placed || r.locked) continue;\n const overlap = overlapOf(pusher, r);\n if (overlap.x <= 0 || overlap.y <= 0) continue;\n const next = direction ?? firstDirection(pusher, r, gap);\n shove(pusher, r, next, gap);\n moved.add(r);\n queue.push({ pusher: r, direction: next });\n if (++shoves >= maxShoves) break;\n }\n }\n return [...moved];\n}\n","import type { Rect } from \"./rect.ts\";\n\n/** The measurements of a packing. */\nexport interface PackingMetrics {\n /** The number of rectangles. */\n count: number;\n /** The size of the hull: the smallest box around the rectangles. */\n hull: { width: number; height: number };\n /** The width of the hull divided by its height. A hull without height gives 0. */\n hullAspect: number;\n /** The number of rectangles in each row, from the top. */\n rows: number[];\n /** The largest right edge of a row less the smallest right edge of a row. A straight right side gives 0. */\n raggedness: number;\n /** The part of the hull that the rectangles cover: their area divided by the area of the hull. */\n utilization: number;\n /** The width of the widest rectangle divided by the median width. */\n widestOverMedian: number;\n}\n\n/** The options of `packingMetrics`. */\nexport interface MetricsOptions {\n /** A rectangle joins a row when its top is this close to the top of the first rectangle of the row. The default is 40. */\n rowTolerance?: number;\n}\n\n/**\n * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.\n * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.\n *\n * @example\n * ```ts\n * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));\n * ```\n */\nexport function packingMetrics(rects: readonly Rect[], options: MetricsOptions = {}): PackingMetrics {\n const tolerance = options.rowTolerance ?? 40;\n const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0);\n if (boxes.length === 0) return { count: 0, hull: { width: 0, height: 0 }, hullAspect: 0, rows: [], raggedness: 0, utilization: 0, widestOverMedian: 0 };\n\n const left = Math.min(...boxes.map((r) => r.position.x));\n const top = Math.min(...boxes.map((r) => r.position.y));\n const right = Math.max(...boxes.map((r) => r.right()));\n const bottom = Math.max(...boxes.map((r) => r.bottom()));\n const hull = { width: right - left, height: bottom - top };\n\n // The rows: the rectangles by their top, and a row takes each rectangle near the top of its first rectangle\n const rows: { top: number; right: number; count: number }[] = [];\n for (const r of boxes.toSorted((a, b) => a.position.y - b.position.y)) {\n const row = rows.at(-1);\n if (row && Math.abs(r.position.y - row.top) <= tolerance) {\n row.right = Math.max(row.right, r.right());\n row.count += 1;\n } else {\n rows.push({ top: r.position.y, right: r.right(), count: 1 });\n }\n }\n const rights = rows.map((row) => row.right);\n const widths = boxes.map((r) => r.size.x).toSorted((a, b) => a - b);\n const median = widths[Math.floor(widths.length / 2)] ?? 0;\n const widest = widths.at(-1) ?? 0;\n const area = boxes.reduce((sum, r) => sum + r.area(), 0);\n return {\n count: boxes.length,\n hull,\n hullAspect: hull.height > 0 ? hull.width / hull.height : 0,\n rows: rows.map((row) => row.count),\n raggedness: Math.max(...rights) - Math.min(...rights),\n utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,\n widestOverMedian: median > 0 ? widest / median : 0,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/vector.ts","../src/rect.ts","../src/order.ts","../src/shelf.ts","../src/skyline.ts","../src/pack.ts","../src/free.ts","../src/push.ts","../src/metrics.ts"],"sourcesContent":["/** A mutable two-dimensional vector. */\nexport class Vector {\n #x: number;\n #y: number;\n\n constructor(x = 0, y = 0) {\n this.#x = x;\n this.#y = y;\n }\n\n /** The first component: a width or a left position. */\n get x(): number { return this.#x; }\n set x(v: number) { this.#x = v; }\n /** The second component: a height or a top position. */\n get y(): number { return this.#y; }\n set y(v: number) { this.#y = v; }\n\n /** This method sets both components. */\n set(x: number, y: number): void {\n this.#x = x;\n this.#y = y;\n }\n\n /** This method copies the components of another vector. */\n setTo(v: Vector): void {\n this.#x = v.x;\n this.#y = v.y;\n }\n\n /** This method gives a new vector: the sum of this vector and another. */\n add(other: Vector): Vector {\n return new Vector(this.#x + other.x, this.#y + other.y);\n }\n\n /** This method gives the product of the components: the area of a size. */\n area(): number {\n return this.#x * this.#y;\n }\n\n /** This method gives a new vector with the same components. */\n clone(): Vector {\n return new Vector(this.#x, this.#y);\n }\n\n /** This method gives a new vector with each component between the components of `min` and `max`. */\n clamp(min: Vector, max: Vector): Vector {\n return new Vector(\n Math.max(min.x, Math.min(max.x, this.#x)),\n Math.max(min.y, Math.min(max.y, this.#y)),\n );\n }\n\n /** This function gives a vector with the same value in both components. */\n static of(scalar: number): Vector {\n return new Vector(scalar, scalar);\n }\n\n /** This function gives the smallest value of each component, or a zero vector for an empty list. */\n static min(vectors: Vector[]): Vector {\n if (!vectors.length) return new Vector();\n return new Vector(\n Math.min(...vectors.map((v) => v.x)),\n Math.min(...vectors.map((v) => v.y)),\n );\n }\n\n /** This function gives the largest value of each component, or a zero vector for an empty list. */\n static max(vectors: Vector[]): Vector {\n if (!vectors.length) return new Vector();\n return new Vector(\n Math.max(...vectors.map((v) => v.x)),\n Math.max(...vectors.map((v) => v.y)),\n );\n }\n\n /** This getter gives a new infinite vector each time. A shared value is not safe, because a vector is mutable. */\n static get Infinity(): Vector {\n return new Vector(Infinity, Infinity);\n }\n}\n","import { Vector } from \"./vector.ts\";\n\n/** A rectangle of the packer. `id` identifies the item that it measures. The packer sets `position` and `wasPacked`. */\n// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- the parameter types the id of the caller\nexport class Rect<T = unknown> {\n readonly #position = new Vector();\n readonly #size = new Vector();\n\n /** The identity of the item that the rectangle measures. */\n id?: T;\n /** The packer sets this field to `true` when it places the rectangle. */\n wasPacked = false;\n /** For an outer rectangle: the packer keeps its width. */\n fixedWidth = false;\n /** For an outer rectangle: the packer keeps its width and its height, and it skips a rectangle that does not fit. */\n fixedSize = false;\n /**\n * The packer keeps the position of a locked rectangle. `packSkyline` packs the other rectangles around it.\n * `packRows` only keeps its position, and other rectangles can overlap it.\n */\n locked = false;\n\n /** The top left corner. */\n get position(): Vector { return this.#position; }\n /** The width and the height. */\n get size(): Vector { return this.#size; }\n\n /** This method gives the right edge. */\n right(): number { return this.#position.x + this.#size.x; }\n /** This method gives the bottom edge. */\n bottom(): number { return this.#position.y + this.#size.y; }\n\n /** This method moves the left edge to the right by an amount, and the right edge stays. */\n shrinkLeft(amount: number): this {\n this.#position.x += amount;\n this.#size.x -= amount;\n return this;\n }\n\n /** This method moves the rectangle by an offset. */\n translateBy(offset: Vector): this {\n this.#position.set(this.#position.x + offset.x, this.#position.y + offset.y);\n return this;\n }\n\n /** This method gives the area of the size. */\n area(): number { return this.#size.area(); }\n\n /** This function gives the rectangles that the packer placed. */\n static packed<U>(rects: Rect<U>[]): Rect<U>[] {\n return rects.filter((r) => r.wasPacked);\n }\n\n /** This function gives the rectangles that the packer did not place. */\n static unpacked<U>(rects: Rect<U>[]): Rect<U>[] {\n return rects.filter((r) => !r.wasPacked);\n }\n\n /** This function marks each rectangle as not placed. A locked rectangle stays placed. */\n static resetPacked<U>(rects: Rect<U>[]): void {\n for (const r of rects) r.wasPacked = r.locked;\n }\n\n /** This function gives the largest right edge, or 0 for an empty list. */\n static maxRight<U>(rects: Rect<U>[]): number {\n return rects.reduce((acc, r) => Math.max(acc, r.right()), 0);\n }\n\n /** This function gives the largest bottom edge, or 0 for an empty list. */\n static maxBottom<U>(rects: Rect<U>[]): number {\n return rects.reduce((acc, r) => Math.max(acc, r.bottom()), 0);\n }\n}\n","import type { Rect } from \"./rect.ts\";\n\n/**\n * An order of the rectangles before the packing. The order changes the layout much. `input` keeps the order of\n * the array of the caller: the row packing then fills no gaps, so its rows read in that order.\n */\nexport type SortOrder = \"height-desc\" | \"height-asc\" | \"width-desc\" | \"area-desc\" | \"input\";\n\n/** This constant lists each sort order. The first one is the default. */\nexport const SORT_ORDERS: readonly SortOrder[] = [\"height-desc\", \"height-asc\", \"width-desc\", \"area-desc\", \"input\"];\n\n/** This function gives the comparator of a sort order. The sort is stable, so `input` keeps the order. */\nexport function compareRects(order: SortOrder): <T>(a: Rect<T>, b: Rect<T>) => number {\n switch (order) {\n case \"height-desc\":\n return (a, b) => b.size.y - a.size.y;\n case \"height-asc\":\n return (a, b) => a.size.y - b.size.y;\n case \"width-desc\":\n return (a, b) => b.size.x - a.size.x;\n case \"area-desc\":\n return (a, b) => b.area() - a.area();\n case \"input\":\n return () => 0;\n }\n}\n","import { compareRects, type SortOrder } from \"./order.ts\";\nimport type { Rect } from \"./rect.ts\";\n\n/** The options of `packShelf` */\nexport interface ShelfOptions {\n /** The order of the rectangles before the packing. The default is `height-desc`. */\n sortOrder?: SortOrder;\n}\n\n/** An obstacle in the coordinates of the outer rectangle */\ninterface Box {\n left: number;\n top: number;\n right: number;\n bottom: number;\n}\n\n/**\n * This function gives the first x from `start` where a rectangle of width `w` crosses no obstacle in the band from\n * `top` to `bottom`.\n */\nfunction firstFreeX(obstacles: readonly Box[], start: number, w: number, top: number, bottom: number): number {\n let x = start;\n let moved = true;\n while (moved) {\n moved = false;\n for (const o of obstacles) {\n if (o.top < bottom && top < o.bottom && o.left < x + w && x < o.right) {\n x = o.right;\n moved = true;\n }\n }\n }\n return x;\n}\n\n/** This function gives the lowest bottom edge of the obstacles in the band from `top` to `bottom`, under `top`. */\nfunction nextTop(obstacles: readonly Box[], top: number, bottom: number): number {\n let next = Infinity;\n for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.bottom > top) next = Math.min(next, o.bottom);\n return next;\n}\n\n/**\n * This function packs rectangles into shelves: rows that it fills left to right in the sort order, and does not fill\n * again. It goes around each `locked` rectangle. It sets the position of each rectangle and gives the outer rectangle.\n *\n * - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.\n * - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.\n * - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,\n * a new row starts under the current row.\n * - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.\n * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a\n * rectangle that does not fit is not packed: its `wasPacked` stays `false`.\n */\nexport function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOptions = {}): Rect<T> {\n const width = outer.size.x;\n const obstacles: Box[] = [];\n let contentBottom = 0;\n for (const rect of rects) {\n if (!rect.locked) continue;\n rect.wasPacked = true;\n const left = rect.position.x - outer.position.x;\n const top = rect.position.y - outer.position.y;\n obstacles.push({ left, top, right: left + rect.size.x, bottom: top + rect.size.y });\n contentBottom = Math.max(contentBottom, top + rect.size.y);\n }\n\n let rowTop = 0;\n let rowBottom = 0;\n let cursor = 0;\n let rowEmpty = true;\n\n for (const rect of rects.toSorted(compareRects(options.sortOrder ?? \"height-desc\"))) {\n if (rect.wasPacked) continue;\n const w = rect.size.x;\n const h = rect.size.y;\n if (!(w > 0 && h > 0)) continue;\n const overflows = w > width;\n if (overflows && outer.fixedSize) continue;\n\n // An overflowing rectangle starts a row of its own\n if (overflows && !rowEmpty) {\n rowTop = rowBottom;\n cursor = 0;\n rowEmpty = true;\n }\n\n let x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);\n while (!overflows && x + w > width) {\n // Start a new row under the current one, or under the obstacles that block an empty row\n rowTop = rowEmpty ? nextTop(obstacles, rowTop, rowTop + h) : rowBottom;\n rowBottom = rowTop;\n cursor = 0;\n rowEmpty = true;\n x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);\n }\n while (overflows && x !== 0) {\n rowTop = nextTop(obstacles, rowTop, rowTop + h);\n rowBottom = rowTop;\n x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);\n }\n\n if (outer.fixedSize && rowTop + h > outer.size.y) continue;\n\n rect.position.set(x + outer.position.x, rowTop + outer.position.y);\n rect.wasPacked = true;\n rowEmpty = false;\n cursor = x + w;\n rowBottom = Math.max(rowBottom, rowTop + h);\n contentBottom = Math.max(contentBottom, rowTop + h);\n if (overflows) {\n rowTop = rowBottom;\n cursor = 0;\n rowEmpty = true;\n }\n }\n\n if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);\n return outer;\n}\n","import { compareRects, type SortOrder } from \"./order.ts\";\nimport type { Rect } from \"./rect.ts\";\n\n/**\n * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height\n * of the window is the largest value of `skyline[x, x + rw)`. The first `x` with the smallest height wins. A\n * monotonic deque makes the search O(width) for one rectangle.\n */\nexport function lowestWindow(skyline: ArrayLike<number>, width: number, rw: number): { x: number; y: number } {\n const deque = new Int32Array(width);\n let head = 0;\n let tail = 0;\n let bestX = 0;\n let bestY = Infinity;\n for (let i = 0; i < width; i++) {\n while (tail > head && skyline[deque[tail - 1]!]! <= skyline[i]!) tail--;\n deque[tail++] = i;\n if (deque[head]! <= i - rw) head++;\n if (i >= rw - 1) {\n const max = skyline[deque[head]!]!;\n if (max < bestY) {\n bestY = max;\n bestX = i - rw + 1;\n }\n }\n }\n return { x: bestX, y: bestY };\n}\n\n/**\n * This function packs rectangles into an outer rectangle with a skyline. It puts each rectangle in the lowest place\n * where it fits, and it does not change the width of the outer rectangle.\n *\n * - A `locked` rectangle keeps its position. The skyline starts at its bottom edge across its width, thus the other\n * rectangles go under it. The space above it stays empty, because a skyline has no holes.\n * - The skyline has a cell for each whole unit of the width. A rectangle that needs more cells than the outer\n * rectangle has goes under all other rectangles, at the left edge.\n * - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that\n * does not fit is not packed: its `wasPacked` stays `false`.\n * - The default sort order is `height-desc`. The order of the array of the caller stays.\n */\nexport function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options: { sortOrder?: SortOrder } = {}): Rect<T> {\n // a cell for each whole unit of the width: a rectangle never goes past the right edge\n const width = Math.floor(outer.size.x);\n if (width <= 0) return outer;\n const skyline = new Float64Array(width);\n\n for (const rect of rects) {\n if (!rect.locked) continue;\n rect.wasPacked = true;\n const x0 = Math.max(0, Math.floor(rect.position.x - outer.position.x));\n const x1 = Math.min(width, Math.ceil(rect.position.x - outer.position.x + rect.size.x));\n const bottom = rect.position.y - outer.position.y + rect.size.y;\n for (let i = x0; i < x1; i++) skyline[i] = Math.max(skyline[i]!, bottom);\n if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, bottom);\n }\n\n for (const rect of rects.toSorted(compareRects(options.sortOrder ?? \"height-desc\"))) {\n if (rect.wasPacked) continue;\n const rw = Math.ceil(rect.size.x);\n const rh = rect.size.y;\n if (rw <= 0 || rh <= 0) continue;\n\n let x: number;\n let y: number;\n if (rw > width) {\n if (outer.fixedSize) continue;\n x = 0;\n y = 0;\n for (let i = 0; i < width; i++) if (skyline[i]! > y) y = skyline[i]!;\n } else {\n ({ x, y } = lowestWindow(skyline, width, rw));\n if (outer.fixedSize && y + rh > outer.size.y) continue;\n }\n\n rect.position.set(x + outer.position.x, y + outer.position.y);\n rect.wasPacked = true;\n const top = y + rh;\n for (let i = x; i < Math.min(width, x + rw); i++) skyline[i] = top;\n if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);\n }\n return outer;\n}\n","import { compareRects, type SortOrder } from \"./order.ts\";\nimport { Rect } from \"./rect.ts\";\nimport { packShelf } from \"./shelf.ts\";\nimport { packSkyline } from \"./skyline.ts\";\nimport { Vector } from \"./vector.ts\";\n\n/** The options of `pack` and `packRows` */\nexport interface PackOptions {\n /** The order of the rectangles before the packing. The default is `height-desc`. */\n sortOrder?: SortOrder;\n}\n\n/** The options of `pack` */\nexport interface PackLayoutOptions extends PackOptions {\n /**\n * The packing: `rows` (the default), `skyline` or `shelf`. `skyline` and `shelf` pack around locked rectangles.\n * Use `shelf` with the sort order `input` to keep the order of the array around locked rectangles.\n */\n strategy?: \"rows\" | \"skyline\" | \"shelf\";\n}\n\nfunction packInto<T>(rects: Rect<T>[], region: Rect<T>, offset: Vector, order: SortOrder): void {\n const unpacked = Rect.unpacked(rects);\n pack(unpacked, region, { sortOrder: order });\n for (const r of Rect.packed(unpacked)) r.translateBy(offset);\n}\n\nfunction packRow<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder): boolean {\n const row: Rect<T>[] = [];\n // toSorted gives a sorted copy, thus the order of the array of the caller stays\n const byHeight = rects.toSorted(compareRects(order));\n // A gap fill puts a later rect before an earlier one, thus the order `input` fills no gaps\n const fillGaps = order !== \"input\";\n\n const rowRect = new Rect<T>();\n rowRect.size.set(outer.size.x, 0);\n\n for (const rect of byHeight) {\n if (rect.wasPacked) continue;\n\n if (rect.size.x > rowRect.size.x) {\n if (outer.fixedSize && rect.size.x > outer.size.x) continue;\n\n const outerLeft = new Rect<T>();\n outerLeft.position.setTo(rowRect.position);\n outerLeft.size.set(outer.size.x - rowRect.position.x, rowRect.size.y);\n outerLeft.fixedSize = true;\n if (fillGaps) packInto(rects, outerLeft, outer.position, order);\n\n rowRect.position.set(0, rowRect.bottom());\n rowRect.size.set(outer.size.x, 0);\n row.length = 0;\n }\n\n if (rowRect.position.y + rect.size.y > outer.size.y) {\n if (outer.fixedSize) continue;\n outer.size.y += rect.size.y;\n if (!outer.fixedWidth && !outer.size.x) outer.size.x = rect.size.x;\n Rect.resetPacked(rects);\n return false;\n }\n\n row.push(rect);\n rect.position.setTo(rowRect.position.add(outer.position));\n rowRect.shrinkLeft(rect.size.x);\n rect.wasPacked = true;\n\n if (rect.size.y > rowRect.size.y) rowRect.size.y = rect.size.y;\n\n if (fillGaps && row.length > 1) {\n const rectInRow = row[row.length - 1]!;\n const outerLeft = new Rect<T>();\n outerLeft.position.set(rectInRow.position.x, rectInRow.bottom());\n outerLeft.size.set(rectInRow.size.x, rowRect.size.y - rectInRow.size.y);\n outerLeft.fixedWidth = true;\n outerLeft.fixedSize = true;\n packInto(rects, outerLeft, outer.position, order);\n }\n }\n return true;\n}\n\n/**\n * This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does\n * not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps\n * its position, but the rows do not go around it. `pack` sets the width first and then uses this function.\n */\nexport function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options: PackOptions = {}): Rect<T> {\n const order = options.sortOrder ?? \"height-desc\";\n for (const r of rects) if (r.locked) r.wasPacked = true;\n while (!packRow(rects, outer, order));\n return outer;\n}\n\n/**\n * This function packs rectangles into an outer rectangle with a guillotine row packing. It sets the position\n * of each rectangle and the size of the outer rectangle, and it gives the outer rectangle.\n *\n * - Without fixed sizes, the outer rectangle starts with a width of 1.5 times the square root of the total area.\n * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.\n * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.\n * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.\n * - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.\n */\nexport function pack<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options: PackLayoutOptions = {}): Rect<T> {\n let totalArea = 0;\n let widest = 0;\n let minX = rects.length > 0 ? Infinity : 0;\n let minY = rects.length > 0 ? Infinity : 0;\n for (const r of rects) {\n totalArea += r.area();\n widest = Math.max(widest, r.size.x);\n minX = Math.min(minX, r.size.x);\n minY = Math.min(minY, r.size.y);\n }\n\n const idealWidth = outer.fixedWidth\n ? outer.size.x\n : Math.max(widest, Math.min(1.5 * Math.sqrt(totalArea), outer.size.x || Infinity));\n\n const ideal = Vector.of(idealWidth).clamp(new Vector(minX, minY), Vector.Infinity);\n\n // A fixed width stays, also when a rectangle is wider: that rectangle overflows\n if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);\n if (options.strategy === \"skyline\") packSkyline(rects, outer, options);\n else if (options.strategy === \"shelf\") packShelf(rects, outer, options);\n else packRows(rects, outer, options);\n\n if (!outer.fixedSize) {\n const contentRight = Rect.maxRight(rects);\n if (!outer.size.x) outer.size.x = contentRight;\n else if (!outer.fixedWidth) outer.size.x = Math.min(contentRight, outer.size.x);\n outer.size.y = Rect.maxBottom(rects);\n }\n\n return outer;\n}\n","import { pack, type PackLayoutOptions } from \"./pack.ts\";\nimport { Rect } from \"./rect.ts\";\n\n/** The options of `packFree` */\nexport interface FreeWidthOptions extends PackLayoutOptions {\n /** The target aspect: the width divided by the height. The default is 1, a square. */\n aspect?: number;\n /**\n * The weight of the aspect error in the objective. The objective is the area times `1 + aspectWeight * aspectError`.\n * The default is 1. With 0, only the area counts.\n */\n aspectWeight?: number;\n /** The number of widths that the first pass tries. The default is 48. */\n samples?: number;\n}\n\n/** One width that `packFree` tried, and its result */\nexport interface FreeWidthTry {\n /** The width of the container for this try */\n width: number;\n /** The width of the content: the right edge of the rectangles */\n contentWidth: number;\n /** The height of the content */\n contentHeight: number;\n /** The objective of this try. Lower is better. */\n objective: number;\n}\n\n/** The result of `packFree` */\nexport interface FreeWidthResult<T> {\n /** The outer rectangle, with the size of the content */\n outer: Rect<T>;\n /** The area of the bounding box of the content */\n area: number;\n /** The area of the rectangles divided by the area of the bounding box */\n fill: number;\n /** The aspect of the bounding box: the width divided by the height */\n aspect: number;\n /** The distance from the target aspect: the absolute value of `log(aspect) - log(target)` */\n aspectError: number;\n /** The objective of the chosen width. Lower is better. */\n objective: number;\n /** Each width that the search tried, in the order of the search */\n tried: FreeWidthTry[];\n}\n\n/** This function gives the distance between an aspect and a target aspect on a log scale. */\nexport function aspectError(width: number, height: number, target = 1): number {\n if (!(width > 0 && height > 0 && target > 0)) return Infinity;\n return Math.abs(Math.log(width / height) - Math.log(target));\n}\n\n/**\n * This function packs rectangles with a free width. It tries many widths with the strategy of `options`, and keeps\n * the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.\n *\n * - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.\n * - The second pass tries the widths between the two neighbours of the best width of the first pass.\n * - The search is deterministic: the same input gives the same widths and the same result.\n * - A `locked` rectangle keeps its position. The width is not less than its right edge.\n * - Each strategy of `pack` works with this function, because it only changes the width.\n */\nexport function packFree<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options: FreeWidthOptions = {}): FreeWidthResult<T> {\n const target = options.aspect ?? 1;\n const weight = options.aspectWeight ?? 1;\n const samples = Math.max(2, Math.floor(options.samples ?? 48));\n const ox = outer.position.x;\n const oy = outer.position.y;\n\n let itemArea = 0;\n let minWidth = 0;\n let rowWidth = 0;\n for (const r of rects) {\n if (r.locked) {\n minWidth = Math.max(minWidth, r.right() - ox);\n continue;\n }\n if (!(r.size.x > 0 && r.size.y > 0)) continue;\n itemArea += r.area();\n minWidth = Math.max(minWidth, r.size.x);\n rowWidth += r.size.x;\n }\n for (const r of rects) if (r.locked) itemArea += r.area();\n const maxWidth = Math.max(minWidth, rowWidth);\n\n const tried: FreeWidthTry[] = [];\n const run = (width: number): FreeWidthTry => {\n Rect.resetPacked(rects);\n const box = new Rect<T>();\n box.position.setTo(outer.position);\n box.size.set(width, 0);\n box.fixedWidth = true;\n pack(rects, box, options);\n const placed = Rect.packed(rects);\n const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);\n const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);\n const area = contentWidth * contentHeight;\n const objective = area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0;\n const t = { width, contentWidth, contentHeight, objective };\n tried.push(t);\n return t;\n };\n\n // The first pass: widths on a log scale. Each width is a whole number when the sizes are whole numbers.\n const widths: number[] = [];\n if (maxWidth > 0) {\n const ratio = maxWidth / minWidth;\n for (let i = 0; i < samples; i++) {\n const w = minWidth * ratio ** (i / (samples - 1));\n const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));\n if (widths.at(-1) !== rounded) widths.push(rounded);\n }\n }\n let best: FreeWidthTry | undefined = undefined;\n let bestIndex = 0;\n for (const [i, w] of widths.entries()) {\n const t = run(w);\n if (best === undefined || t.objective < best.objective) {\n best = t;\n bestIndex = i;\n }\n }\n\n // The second pass: the widths between the two neighbours of the best width\n if (widths.length > 2) {\n const low = widths[Math.max(0, bestIndex - 1)]!;\n const high = widths[Math.min(widths.length - 1, bestIndex + 1)]!;\n const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));\n for (let i = 1; i <= steps; i++) {\n const w = Math.round(low + ((high - low) * i) / (steps + 1));\n if (widths.includes(w)) continue;\n const t = run(w);\n if (best === undefined || t.objective < best.objective) best = t;\n }\n }\n\n // The last run packs with the best width, so the positions of the rectangles are those of the best width\n const chosen = best === undefined ? undefined : run(best.width);\n tried.pop();\n const w = chosen?.contentWidth ?? 0;\n const h = chosen?.contentHeight ?? 0;\n if (!outer.fixedSize) outer.size.set(w, h);\n const area = w * h;\n return {\n outer,\n area,\n fill: area > 0 ? itemArea / area : 0,\n aspect: h > 0 ? w / h : 0,\n aspectError: aspectError(w, h, target),\n objective: chosen?.objective ?? 0,\n tried,\n };\n}\n","import type { Rect } from \"./rect.ts\";\n\n/** The options of `pushCascade`. */\nexport interface PushOptions {\n /** The space between a shoved rectangle and the rectangle that shoves it. The default is 8. */\n gap?: number;\n /** The most shoves. The default is eight for each rectangle. The cascade stops at this limit. */\n maxShoves?: number;\n}\n\n/** A direction of a shove: an axis and a sign. */\ninterface Direction {\n readonly axis: \"x\" | \"y\";\n readonly sign: 1 | -1;\n}\n\n/** This function gives the overlap of two rectangles on each axis. A value of 0 or less is no overlap. */\nfunction overlapOf(a: Rect, b: Rect): { x: number; y: number } {\n return {\n x: Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x),\n y: Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y),\n };\n}\n\n/**\n * This function gives the direction of a first shove. The axis has the smaller overlap. The sign points away from the\n * center of the pusher. When that shove crosses 0, the sign is positive.\n */\nfunction firstDirection(pusher: Rect, r: Rect, gap: number): Direction {\n const overlap = overlapOf(pusher, r);\n if (overlap.x < overlap.y) {\n const back = r.position.x + r.size.x / 2 < pusher.position.x + pusher.size.x / 2 && pusher.position.x - r.size.x - gap >= 0;\n return { axis: \"x\", sign: back ? -1 : 1 };\n }\n const back = r.position.y + r.size.y / 2 < pusher.position.y + pusher.size.y / 2 && pusher.position.y - r.size.y - gap >= 0;\n return { axis: \"y\", sign: back ? -1 : 1 };\n}\n\n/** This function puts a rectangle past the pusher, along a direction. The rectangle does not go past 0. */\nfunction shove(pusher: Rect, r: Rect, direction: Direction, gap: number): void {\n if (direction.axis === \"x\") {\n const x = direction.sign > 0 ? pusher.right() + gap : pusher.position.x - r.size.x - gap;\n r.position.set(Math.max(0, x), r.position.y);\n return;\n }\n const y = direction.sign > 0 ? pusher.bottom() + gap : pusher.position.y - r.size.y - gap;\n r.position.set(r.position.x, Math.max(0, y));\n}\n\n/**\n * This function moves each rectangle that a placed rectangle overlaps, and the rectangles that those overlap in turn.\n * A shove takes the axis of the smaller overlap, away from the pusher. A shoved rectangle keeps the direction of its first\n * shove when it shoves the next one, so the cascade goes outward. The placed rectangle and each locked rectangle stay.\n * The function moves the rectangles in place and gives the rectangles that moved.\n *\n * @example\n * ```ts\n * const moved = pushCascade(dropped, cards, { gap: 8 });\n * ```\n */\nexport function pushCascade<T>(placed: Rect<T>, rects: readonly Rect<T>[], options: PushOptions = {}): Rect<T>[] {\n const gap = options.gap ?? 8;\n const maxShoves = options.maxShoves ?? rects.length * 8;\n const moved = new Set<Rect<T>>();\n const queue: { pusher: Rect<T>; direction?: Direction }[] = [{ pusher: placed }];\n let shoves = 0;\n while (shoves < maxShoves) {\n const step = queue.shift();\n if (!step) break;\n const { pusher, direction } = step;\n for (const r of rects) {\n if (r === pusher || r === placed || r.locked) continue;\n const overlap = overlapOf(pusher, r);\n if (overlap.x <= 0 || overlap.y <= 0) continue;\n const next = direction ?? firstDirection(pusher, r, gap);\n shove(pusher, r, next, gap);\n moved.add(r);\n queue.push({ pusher: r, direction: next });\n if (++shoves >= maxShoves) break;\n }\n }\n return [...moved];\n}\n","import type { Rect } from \"./rect.ts\";\n\n/** The measurements of a packing. */\nexport interface PackingMetrics {\n /** The number of rectangles. */\n count: number;\n /** The size of the hull: the smallest box around the rectangles. */\n hull: { width: number; height: number };\n /** The width of the hull divided by its height. A hull without height gives 0. */\n hullAspect: number;\n /** The number of rectangles in each row, from the top. */\n rows: number[];\n /** The largest right edge of a row less the smallest right edge of a row. A straight right side gives 0. */\n raggedness: number;\n /** The part of the hull that the rectangles cover: their area divided by the area of the hull. */\n utilization: number;\n /** The width of the widest rectangle divided by the median width. */\n widestOverMedian: number;\n}\n\n/** The options of `packingMetrics`. */\nexport interface MetricsOptions {\n /** A rectangle joins a row when its top is this close to the top of the first rectangle of the row. The default is 40. */\n rowTolerance?: number;\n}\n\n/**\n * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.\n * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.\n *\n * @example\n * ```ts\n * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));\n * ```\n */\nexport function packingMetrics(rects: readonly Rect[], options: MetricsOptions = {}): PackingMetrics {\n const tolerance = options.rowTolerance ?? 40;\n const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0);\n if (boxes.length === 0) return { count: 0, hull: { width: 0, height: 0 }, hullAspect: 0, rows: [], raggedness: 0, utilization: 0, widestOverMedian: 0 };\n\n const left = Math.min(...boxes.map((r) => r.position.x));\n const top = Math.min(...boxes.map((r) => r.position.y));\n const right = Math.max(...boxes.map((r) => r.right()));\n const bottom = Math.max(...boxes.map((r) => r.bottom()));\n const hull = { width: right - left, height: bottom - top };\n\n // The rows: the rectangles by their top, and a row takes each rectangle near the top of its first rectangle\n const rows: { top: number; right: number; count: number }[] = [];\n for (const r of boxes.toSorted((a, b) => a.position.y - b.position.y)) {\n const row = rows.at(-1);\n if (row && Math.abs(r.position.y - row.top) <= tolerance) {\n row.right = Math.max(row.right, r.right());\n row.count += 1;\n } else {\n rows.push({ top: r.position.y, right: r.right(), count: 1 });\n }\n }\n const rights = rows.map((row) => row.right);\n const widths = boxes.map((r) => r.size.x).toSorted((a, b) => a - b);\n const median = widths[Math.floor(widths.length / 2)] ?? 0;\n const widest = widths.at(-1) ?? 0;\n const area = boxes.reduce((sum, r) => sum + r.area(), 0);\n return {\n count: boxes.length,\n hull,\n hullAspect: hull.height > 0 ? hull.width / hull.height : 0,\n rows: rows.map((row) => row.count),\n raggedness: Math.max(...rights) - Math.min(...rights),\n utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,\n widestOverMedian: median > 0 ? widest / median : 0,\n };\n}\n"],"mappings":"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"}
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.1.0",
4
+ "version": "0.1.2",
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/free.ts ADDED
@@ -0,0 +1,153 @@
1
+ import { pack, type PackLayoutOptions } from "./pack.ts";
2
+ import { Rect } from "./rect.ts";
3
+
4
+ /** The options of `packFree` */
5
+ export interface FreeWidthOptions extends PackLayoutOptions {
6
+ /** The target aspect: the width divided by the height. The default is 1, a square. */
7
+ aspect?: number;
8
+ /**
9
+ * The weight of the aspect error in the objective. The objective is the area times `1 + aspectWeight * aspectError`.
10
+ * The default is 1. With 0, only the area counts.
11
+ */
12
+ aspectWeight?: number;
13
+ /** The number of widths that the first pass tries. The default is 48. */
14
+ samples?: number;
15
+ }
16
+
17
+ /** One width that `packFree` tried, and its result */
18
+ export interface FreeWidthTry {
19
+ /** The width of the container for this try */
20
+ width: number;
21
+ /** The width of the content: the right edge of the rectangles */
22
+ contentWidth: number;
23
+ /** The height of the content */
24
+ contentHeight: number;
25
+ /** The objective of this try. Lower is better. */
26
+ objective: number;
27
+ }
28
+
29
+ /** The result of `packFree` */
30
+ export interface FreeWidthResult<T> {
31
+ /** The outer rectangle, with the size of the content */
32
+ outer: Rect<T>;
33
+ /** The area of the bounding box of the content */
34
+ area: number;
35
+ /** The area of the rectangles divided by the area of the bounding box */
36
+ fill: number;
37
+ /** The aspect of the bounding box: the width divided by the height */
38
+ aspect: number;
39
+ /** The distance from the target aspect: the absolute value of `log(aspect) - log(target)` */
40
+ aspectError: number;
41
+ /** The objective of the chosen width. Lower is better. */
42
+ objective: number;
43
+ /** Each width that the search tried, in the order of the search */
44
+ tried: FreeWidthTry[];
45
+ }
46
+
47
+ /** This function gives the distance between an aspect and a target aspect on a log scale. */
48
+ export function aspectError(width: number, height: number, target = 1): number {
49
+ if (!(width > 0 && height > 0 && target > 0)) return Infinity;
50
+ return Math.abs(Math.log(width / height) - Math.log(target));
51
+ }
52
+
53
+ /**
54
+ * This function packs rectangles with a free width. It tries many widths with the strategy of `options`, and keeps
55
+ * the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.
56
+ *
57
+ * - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.
58
+ * - The second pass tries the widths between the two neighbours of the best width of the first pass.
59
+ * - The search is deterministic: the same input gives the same widths and the same result.
60
+ * - A `locked` rectangle keeps its position. The width is not less than its right edge.
61
+ * - Each strategy of `pack` works with this function, because it only changes the width.
62
+ */
63
+ export function packFree<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options: FreeWidthOptions = {}): FreeWidthResult<T> {
64
+ const target = options.aspect ?? 1;
65
+ const weight = options.aspectWeight ?? 1;
66
+ const samples = Math.max(2, Math.floor(options.samples ?? 48));
67
+ const ox = outer.position.x;
68
+ const oy = outer.position.y;
69
+
70
+ let itemArea = 0;
71
+ let minWidth = 0;
72
+ let rowWidth = 0;
73
+ for (const r of rects) {
74
+ if (r.locked) {
75
+ minWidth = Math.max(minWidth, r.right() - ox);
76
+ continue;
77
+ }
78
+ if (!(r.size.x > 0 && r.size.y > 0)) continue;
79
+ itemArea += r.area();
80
+ minWidth = Math.max(minWidth, r.size.x);
81
+ rowWidth += r.size.x;
82
+ }
83
+ for (const r of rects) if (r.locked) itemArea += r.area();
84
+ const maxWidth = Math.max(minWidth, rowWidth);
85
+
86
+ const tried: FreeWidthTry[] = [];
87
+ const run = (width: number): FreeWidthTry => {
88
+ Rect.resetPacked(rects);
89
+ const box = new Rect<T>();
90
+ box.position.setTo(outer.position);
91
+ box.size.set(width, 0);
92
+ box.fixedWidth = true;
93
+ pack(rects, box, options);
94
+ const placed = Rect.packed(rects);
95
+ const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);
96
+ const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);
97
+ const area = contentWidth * contentHeight;
98
+ const objective = area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0;
99
+ const t = { width, contentWidth, contentHeight, objective };
100
+ tried.push(t);
101
+ return t;
102
+ };
103
+
104
+ // The first pass: widths on a log scale. Each width is a whole number when the sizes are whole numbers.
105
+ const widths: number[] = [];
106
+ if (maxWidth > 0) {
107
+ const ratio = maxWidth / minWidth;
108
+ for (let i = 0; i < samples; i++) {
109
+ const w = minWidth * ratio ** (i / (samples - 1));
110
+ const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));
111
+ if (widths.at(-1) !== rounded) widths.push(rounded);
112
+ }
113
+ }
114
+ let best: FreeWidthTry | undefined = undefined;
115
+ let bestIndex = 0;
116
+ for (const [i, w] of widths.entries()) {
117
+ const t = run(w);
118
+ if (best === undefined || t.objective < best.objective) {
119
+ best = t;
120
+ bestIndex = i;
121
+ }
122
+ }
123
+
124
+ // The second pass: the widths between the two neighbours of the best width
125
+ if (widths.length > 2) {
126
+ const low = widths[Math.max(0, bestIndex - 1)]!;
127
+ const high = widths[Math.min(widths.length - 1, bestIndex + 1)]!;
128
+ const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));
129
+ for (let i = 1; i <= steps; i++) {
130
+ const w = Math.round(low + ((high - low) * i) / (steps + 1));
131
+ if (widths.includes(w)) continue;
132
+ const t = run(w);
133
+ if (best === undefined || t.objective < best.objective) best = t;
134
+ }
135
+ }
136
+
137
+ // The last run packs with the best width, so the positions of the rectangles are those of the best width
138
+ const chosen = best === undefined ? undefined : run(best.width);
139
+ tried.pop();
140
+ const w = chosen?.contentWidth ?? 0;
141
+ const h = chosen?.contentHeight ?? 0;
142
+ if (!outer.fixedSize) outer.size.set(w, h);
143
+ const area = w * h;
144
+ return {
145
+ outer,
146
+ area,
147
+ fill: area > 0 ? itemArea / area : 0,
148
+ aspect: h > 0 ? w / h : 0,
149
+ aspectError: aspectError(w, h, target),
150
+ objective: chosen?.objective ?? 0,
151
+ tried,
152
+ };
153
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ export { Vector } from "./vector.ts";
2
2
  export { Rect } from "./rect.ts";
3
3
  export { pack, packRows, type PackOptions, type PackLayoutOptions } from "./pack.ts";
4
4
  export { packSkyline, lowestWindow } from "./skyline.ts";
5
+ export { packShelf, type ShelfOptions } from "./shelf.ts";
6
+ export { packFree, aspectError, type FreeWidthOptions, type FreeWidthResult, type FreeWidthTry } from "./free.ts";
5
7
  export { compareRects, SORT_ORDERS, type SortOrder } from "./order.ts";
6
8
  export { pushCascade, type PushOptions } from "./push.ts";
7
9
  export { packingMetrics, type MetricsOptions, type PackingMetrics } from "./metrics.ts";
package/src/pack.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { compareRects, type SortOrder } from "./order.ts";
2
2
  import { Rect } from "./rect.ts";
3
+ import { packShelf } from "./shelf.ts";
3
4
  import { packSkyline } from "./skyline.ts";
4
5
  import { Vector } from "./vector.ts";
5
6
 
@@ -11,8 +12,11 @@ export interface PackOptions {
11
12
 
12
13
  /** The options of `pack` */
13
14
  export interface PackLayoutOptions extends PackOptions {
14
- /** The packing: `rows` (the default) or `skyline`. Use `skyline` to pack around locked rectangles. */
15
- strategy?: "rows" | "skyline";
15
+ /**
16
+ * The packing: `rows` (the default), `skyline` or `shelf`. `skyline` and `shelf` pack around locked rectangles.
17
+ * Use `shelf` with the sort order `input` to keep the order of the array around locked rectangles.
18
+ */
19
+ strategy?: "rows" | "skyline" | "shelf";
16
20
  }
17
21
 
18
22
  function packInto<T>(rects: Rect<T>[], region: Rect<T>, offset: Vector, order: SortOrder): void {
@@ -96,7 +100,7 @@ export function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options: PackOptio
96
100
  * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
97
101
  * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
98
102
  * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
99
- * - `options.strategy` selects `packRows` (the default) or `packSkyline` after the width is set.
103
+ * - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
100
104
  */
101
105
  export function pack<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options: PackLayoutOptions = {}): Rect<T> {
102
106
  let totalArea = 0;
@@ -119,6 +123,7 @@ export function pack<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options:
119
123
  // A fixed width stays, also when a rectangle is wider: that rectangle overflows
120
124
  if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
121
125
  if (options.strategy === "skyline") packSkyline(rects, outer, options);
126
+ else if (options.strategy === "shelf") packShelf(rects, outer, options);
122
127
  else packRows(rects, outer, options);
123
128
 
124
129
  if (!outer.fixedSize) {
package/src/shelf.ts ADDED
@@ -0,0 +1,121 @@
1
+ import { compareRects, type SortOrder } from "./order.ts";
2
+ import type { Rect } from "./rect.ts";
3
+
4
+ /** The options of `packShelf` */
5
+ export interface ShelfOptions {
6
+ /** The order of the rectangles before the packing. The default is `height-desc`. */
7
+ sortOrder?: SortOrder;
8
+ }
9
+
10
+ /** An obstacle in the coordinates of the outer rectangle */
11
+ interface Box {
12
+ left: number;
13
+ top: number;
14
+ right: number;
15
+ bottom: number;
16
+ }
17
+
18
+ /**
19
+ * This function gives the first x from `start` where a rectangle of width `w` crosses no obstacle in the band from
20
+ * `top` to `bottom`.
21
+ */
22
+ function firstFreeX(obstacles: readonly Box[], start: number, w: number, top: number, bottom: number): number {
23
+ let x = start;
24
+ let moved = true;
25
+ while (moved) {
26
+ moved = false;
27
+ for (const o of obstacles) {
28
+ if (o.top < bottom && top < o.bottom && o.left < x + w && x < o.right) {
29
+ x = o.right;
30
+ moved = true;
31
+ }
32
+ }
33
+ }
34
+ return x;
35
+ }
36
+
37
+ /** This function gives the lowest bottom edge of the obstacles in the band from `top` to `bottom`, under `top`. */
38
+ function nextTop(obstacles: readonly Box[], top: number, bottom: number): number {
39
+ let next = Infinity;
40
+ for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.bottom > top) next = Math.min(next, o.bottom);
41
+ return next;
42
+ }
43
+
44
+ /**
45
+ * This function packs rectangles into shelves: rows that it fills left to right in the sort order, and does not fill
46
+ * again. It goes around each `locked` rectangle. It sets the position of each rectangle and gives the outer rectangle.
47
+ *
48
+ * - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.
49
+ * - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.
50
+ * - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,
51
+ * a new row starts under the current row.
52
+ * - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
53
+ * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
54
+ * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
55
+ */
56
+ export function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOptions = {}): Rect<T> {
57
+ const width = outer.size.x;
58
+ const obstacles: Box[] = [];
59
+ let contentBottom = 0;
60
+ for (const rect of rects) {
61
+ if (!rect.locked) continue;
62
+ rect.wasPacked = true;
63
+ const left = rect.position.x - outer.position.x;
64
+ const top = rect.position.y - outer.position.y;
65
+ obstacles.push({ left, top, right: left + rect.size.x, bottom: top + rect.size.y });
66
+ contentBottom = Math.max(contentBottom, top + rect.size.y);
67
+ }
68
+
69
+ let rowTop = 0;
70
+ let rowBottom = 0;
71
+ let cursor = 0;
72
+ let rowEmpty = true;
73
+
74
+ for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
75
+ if (rect.wasPacked) continue;
76
+ const w = rect.size.x;
77
+ const h = rect.size.y;
78
+ if (!(w > 0 && h > 0)) continue;
79
+ const overflows = w > width;
80
+ if (overflows && outer.fixedSize) continue;
81
+
82
+ // An overflowing rectangle starts a row of its own
83
+ if (overflows && !rowEmpty) {
84
+ rowTop = rowBottom;
85
+ cursor = 0;
86
+ rowEmpty = true;
87
+ }
88
+
89
+ let x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
90
+ while (!overflows && x + w > width) {
91
+ // Start a new row under the current one, or under the obstacles that block an empty row
92
+ rowTop = rowEmpty ? nextTop(obstacles, rowTop, rowTop + h) : rowBottom;
93
+ rowBottom = rowTop;
94
+ cursor = 0;
95
+ rowEmpty = true;
96
+ x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
97
+ }
98
+ while (overflows && x !== 0) {
99
+ rowTop = nextTop(obstacles, rowTop, rowTop + h);
100
+ rowBottom = rowTop;
101
+ x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
102
+ }
103
+
104
+ if (outer.fixedSize && rowTop + h > outer.size.y) continue;
105
+
106
+ rect.position.set(x + outer.position.x, rowTop + outer.position.y);
107
+ rect.wasPacked = true;
108
+ rowEmpty = false;
109
+ cursor = x + w;
110
+ rowBottom = Math.max(rowBottom, rowTop + h);
111
+ contentBottom = Math.max(contentBottom, rowTop + h);
112
+ if (overflows) {
113
+ rowTop = rowBottom;
114
+ cursor = 0;
115
+ rowEmpty = true;
116
+ }
117
+ }
118
+
119
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
120
+ return outer;
121
+ }