@mark1russell7/packing 0.2.0 → 0.2.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/build/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_TOLERANCE, EPS, RECORD_EPS, boundsOf, boundsOf as boundsOf$1, checkInput, marginSides, regionHitsRect, translate, turnBox, turnPoint, turnPort, turnRegion } from "@mark1russell7/layout-contract";
1
+ import { DEFAULT_TOLERANCE, EPS, RECORD_EPS, boundsOf, boundsOf as boundsOf$1, checkInput, marginSides, rectInRegion, regionHitsRect, translate, turnBox, turnPoint, turnPort, turnRegion } from "@mark1russell7/layout-contract";
2
2
  // src/vector.ts
3
3
  /** A mutable two-dimensional vector. */
4
4
  var Vector = class Vector {
@@ -156,1427 +156,2286 @@ function compareRects(order) {
156
156
  case "input": return () => 0;
157
157
  }
158
158
  }
159
- // src/shelf.ts
159
+ // src/contract.ts
160
+ /** This function gives the margin of an item on each side. A number is the same margin on each side. */
161
+ function marginOf(item) {
162
+ const m = marginSides(item.margin);
163
+ return {
164
+ top: m.top,
165
+ right: m.right,
166
+ bottom: m.bottom,
167
+ left: m.left
168
+ };
169
+ }
160
170
  /**
161
- * This function gives the first x from `start` where a rectangle of width `w` crosses no obstacle in the band from
162
- * `top` to `bottom`.
171
+ * This function gives an item after a turn of 90 degrees clockwise, with the y axis down (decision 1 of
172
+ * `docs/PACKER.md`). The width and the height change places. The top-left corner, the id and the other fields stay.
173
+ * The ports, the shape and the margin turn with the item. The function does not change its argument.
174
+ * It is `turnBox` of the contract.
163
175
  */
164
- function firstFreeX(obstacles, start, w, top, bottom) {
165
- let x = start;
166
- let moved = true;
167
- while (moved) {
168
- moved = false;
169
- for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.left < x + w && x < o.right) {
170
- x = o.right;
171
- moved = true;
172
- }
173
- }
174
- return x;
176
+ function turnItem(item) {
177
+ return turnBox(item);
175
178
  }
176
- /** This function gives the lowest bottom edge of the obstacles in the band from `top` to `bottom`, under `top`. */
177
- function nextTop(obstacles, top, bottom) {
178
- let next = Infinity;
179
- for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.bottom > top) next = Math.min(next, o.bottom);
180
- return next;
179
+ // src/frame.ts
180
+ const clock = globalThis.performance;
181
+ /** This function gives the time in milliseconds. */
182
+ function now() {
183
+ return clock ? clock.now() : Date.now();
184
+ }
185
+ /** This function gives the rectangle of a container. A rectangle shape gives the origin and the sizes of the fixed sides. */
186
+ function containerBounds(input) {
187
+ const { container } = input;
188
+ const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
189
+ const fixedW = container.fixed === "size" || container.fixed === "width";
190
+ const fixedH = container.fixed === "size" || container.fixed === "height";
191
+ const w = shape?.w ?? container.w;
192
+ const h = shape?.h ?? container.h;
193
+ if (fixedW && w === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a width.`);
194
+ if (fixedH && h === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a height.`);
195
+ return {
196
+ x: shape?.x ?? 0,
197
+ y: shape?.y ?? 0,
198
+ w: fixedW ? w : Infinity,
199
+ h: fixedH ? h : Infinity
200
+ };
201
+ }
202
+ /** This function gives the items in the order of their rank. The index in the array is the rank of an item without one. */
203
+ function byRank(items) {
204
+ return items.map((item, index) => ({
205
+ item,
206
+ rank: item.rank ?? index
207
+ })).toSorted((a, b) => a.rank - b.rank).map((e) => e.item);
181
208
  }
182
209
  /**
183
- * This function packs rectangles into shelves: rows that it fills left to right in the sort order, and does not fill
184
- * again. It goes around each `locked` rectangle. It sets the position of each rectangle and gives the outer rectangle.
185
- *
186
- * - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.
187
- * - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.
188
- * - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,
189
- * a new row starts under the current row.
190
- * - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
191
- * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
192
- * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
210
+ * This function makes the frame of an input. A locked item with a position and an obstacle without `move` are locked
211
+ * rectangles. An obstacle with `move` is a free rectangle only when `movable` is `true`.
193
212
  */
194
- function packShelf(rects, outer, options = {}) {
195
- const width = outer.size.x;
196
- const obstacles = [];
197
- let contentBottom = 0;
198
- for (const rect of rects) {
199
- if (!rect.locked) continue;
200
- rect.wasPacked = true;
201
- const left = rect.position.x - outer.position.x;
202
- const top = rect.position.y - outer.position.y;
203
- obstacles.push({
204
- left,
205
- top,
206
- right: left + rect.size.x,
207
- bottom: top + rect.size.y
213
+ function buildFrame(input, movable = false) {
214
+ const bounds = containerBounds(input);
215
+ const inset = input.container.inset ?? 0;
216
+ const half = (input.gap ?? 0) / 2;
217
+ const ox = bounds.x + inset - half;
218
+ const oy = bounds.y + inset - half;
219
+ const w = bounds.w - 2 * inset + 2 * half;
220
+ const h = bounds.h - 2 * inset + 2 * half;
221
+ const entries = [];
222
+ const tooLarge = [];
223
+ for (const item of byRank(input.items)) {
224
+ const m = marginOf(item);
225
+ const rect = new Rect();
226
+ rect.size.set(item.w + m.left + m.right + 2 * half, item.h + m.top + m.bottom + 2 * half);
227
+ const dx = m.left + half;
228
+ const dy = m.top + half;
229
+ if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
230
+ rect.locked = true;
231
+ rect.position.set(item.x - dx - ox, item.y - dy - oy);
232
+ } else if (rect.size.x > w + EPS || rect.size.y > h + EPS) {
233
+ tooLarge.push(item.id);
234
+ continue;
235
+ }
236
+ entries.push({
237
+ rect,
238
+ item,
239
+ dx,
240
+ dy
208
241
  });
209
- contentBottom = Math.max(contentBottom, top + rect.size.y);
210
242
  }
211
- let rowTop = 0;
212
- let rowBottom = 0;
213
- let cursor = 0;
214
- let rowEmpty = true;
215
- for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
216
- if (rect.wasPacked) continue;
217
- const w = rect.size.x;
218
- const h = rect.size.y;
219
- if (!(w > 0 && h > 0)) continue;
220
- const overflows = w > width;
221
- if (overflows && outer.fixedSize) continue;
222
- if (overflows && !rowEmpty) {
223
- rowTop = rowBottom;
224
- cursor = 0;
225
- rowEmpty = true;
226
- }
227
- let x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
228
- while (!overflows && x + w > width) {
229
- rowTop = rowEmpty ? nextTop(obstacles, rowTop, rowTop + h) : rowBottom;
230
- rowBottom = rowTop;
231
- cursor = 0;
232
- rowEmpty = true;
233
- x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
234
- }
235
- while (overflows && x !== 0) {
236
- rowTop = nextTop(obstacles, rowTop, rowTop + h);
237
- rowBottom = rowTop;
238
- x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
239
- }
240
- if (outer.fixedSize && rowTop + h > outer.size.y) continue;
241
- rect.position.set(x + outer.position.x, rowTop + outer.position.y);
242
- rect.wasPacked = true;
243
- rowEmpty = false;
244
- cursor = x + w;
245
- rowBottom = Math.max(rowBottom, rowTop + h);
246
- contentBottom = Math.max(contentBottom, rowTop + h);
247
- if (overflows) {
248
- rowTop = rowBottom;
249
- cursor = 0;
250
- rowEmpty = true;
251
- }
243
+ for (const obstacle of input.obstacles ?? []) {
244
+ const b = boundsOf(obstacle.region);
245
+ const rect = new Rect();
246
+ rect.size.set(b.w + 2 * half, b.h + 2 * half);
247
+ rect.position.set(b.x - half - ox, b.y - half - oy);
248
+ rect.locked = !(movable && obstacle.move !== void 0);
249
+ entries.push({
250
+ rect,
251
+ obstacle,
252
+ dx: half,
253
+ dy: half
254
+ });
252
255
  }
253
- if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
256
+ return {
257
+ ox,
258
+ oy,
259
+ w,
260
+ h,
261
+ bounds,
262
+ inset,
263
+ entries,
264
+ tooLarge
265
+ };
266
+ }
267
+ /** This function gives the outer rectangle of 0.1 for a frame. A free height starts at 0 and grows. */
268
+ function outerOf(frame) {
269
+ const outer = new Rect();
270
+ const fixedSize = Number.isFinite(frame.h);
271
+ outer.size.set(frame.w, fixedSize ? frame.h : 0);
272
+ outer.fixedWidth = true;
273
+ outer.fixedSize = fixedSize;
254
274
  return outer;
255
275
  }
256
- // src/skyline.ts
276
+ /** This function gives the rectangles of a frame, in the order of the entries. */
277
+ function rectsOf(frame) {
278
+ return frame.entries.map((e) => e.rect);
279
+ }
257
280
  /**
258
- * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
259
- * of the window is the largest value of `skyline[x, x + rw)`. The first `x` with the smallest height wins. A
260
- * monotonic deque makes the search O(width) for one rectangle.
281
+ * This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
282
+ * position exactly. Each other item that the packer did not place goes to the overflow.
261
283
  */
262
- function lowestWindow(skyline, width, rw) {
263
- const deque = new Int32Array(width);
264
- let head = 0;
265
- let tail = 0;
266
- let bestX = 0;
267
- let bestY = Infinity;
268
- for (let i = 0; i < width; i++) {
269
- while (tail > head && skyline[deque[tail - 1]] <= skyline[i]) tail--;
270
- deque[tail++] = i;
271
- if (deque[head] <= i - rw) head++;
272
- if (i >= rw - 1) {
273
- const max = skyline[deque[head]];
274
- if (max < bestY) {
275
- bestY = max;
276
- bestX = i - rw + 1;
277
- }
278
- }
284
+ function readFrame(input, frame, trace, moved = []) {
285
+ const placements = [];
286
+ const overflow = [...frame.tooLarge];
287
+ for (const e of frame.entries) {
288
+ const item = e.item;
289
+ if (item === void 0) continue;
290
+ if (item.locked === true && item.x !== void 0 && item.y !== void 0) placements.push({
291
+ id: item.id,
292
+ x: item.x,
293
+ y: item.y
294
+ });
295
+ else if (e.rect.wasPacked) placements.push({
296
+ id: item.id,
297
+ x: e.rect.position.x + e.dx + frame.ox,
298
+ y: e.rect.position.y + e.dy + frame.oy
299
+ });
300
+ else overflow.push(item.id);
279
301
  }
280
302
  return {
281
- x: bestX,
282
- y: bestY
303
+ placements,
304
+ overflow,
305
+ size: sizeOf(input, frame, placements, moved),
306
+ moved,
307
+ trace
283
308
  };
284
309
  }
285
310
  /**
286
- * This function packs rectangles into an outer rectangle with a skyline. It puts each rectangle in the lowest place
287
- * where it fits, and it does not change the width of the outer rectangle.
288
- *
289
- * - A `locked` rectangle keeps its position. The skyline starts at its bottom edge across its width, thus the other
290
- * rectangles go under it. The space above it stays empty, because a skyline has no holes.
291
- * - The skyline has a cell for each whole unit of the width. A rectangle that needs more cells than the outer
292
- * rectangle has goes under all other rectangles, at the left edge.
293
- * - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that
294
- * does not fit is not packed: its `wasPacked` stays `false`.
295
- * - The default sort order is `height-desc`. The order of the array of the caller stays.
311
+ * This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
312
+ * far edge of the items with their margins and the inset, or of the obstacles.
296
313
  */
297
- function packSkyline(rects, outer, options = {}) {
298
- const width = Math.floor(outer.size.x);
299
- if (width <= 0) return outer;
300
- const skyline = new Float64Array(width);
301
- for (const rect of rects) {
302
- if (!rect.locked) continue;
303
- rect.wasPacked = true;
304
- const x0 = Math.max(0, Math.floor(rect.position.x - outer.position.x));
305
- const x1 = Math.min(width, Math.ceil(rect.position.x - outer.position.x + rect.size.x));
306
- const bottom = rect.position.y - outer.position.y + rect.size.y;
307
- for (let i = x0; i < x1; i++) skyline[i] = Math.max(skyline[i], bottom);
308
- if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, bottom);
314
+ function sizeOf(input, frame, placements, moved) {
315
+ const items = new Map(input.items.map((i) => [i.id, i]));
316
+ let right = 0;
317
+ let bottom = 0;
318
+ for (const p of placements) {
319
+ const item = items.get(p.id);
320
+ const m = marginOf(item);
321
+ right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
322
+ bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
309
323
  }
310
- for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
311
- if (rect.wasPacked) continue;
312
- const rw = Math.ceil(rect.size.x);
313
- const rh = rect.size.y;
314
- if (rw <= 0 || rh <= 0) continue;
315
- let x;
316
- let y;
317
- if (rw > width) {
318
- if (outer.fixedSize) continue;
319
- x = 0;
320
- y = 0;
321
- for (let i = 0; i < width; i++) if (skyline[i] > y) y = skyline[i];
322
- } else {
323
- ({x, y} = lowestWindow(skyline, width, rw));
324
- if (outer.fixedSize && y + rh > outer.size.y) continue;
325
- }
326
- rect.position.set(x + outer.position.x, y + outer.position.y);
327
- rect.wasPacked = true;
328
- const top = y + rh;
329
- for (let i = x; i < Math.min(width, x + rw); i++) skyline[i] = top;
330
- if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);
324
+ const movedById = new Map(moved.map((m) => [m.id, m]));
325
+ for (const o of input.obstacles ?? []) {
326
+ const b = boundsOf(o.region);
327
+ const at = movedById.get(o.id) ?? b;
328
+ right = Math.max(right, at.x + b.w - frame.bounds.x);
329
+ bottom = Math.max(bottom, at.y + b.h - frame.bounds.y);
331
330
  }
332
- return outer;
333
- }
334
- // src/pack.ts
335
- function packInto(rects, region, offset, order) {
336
- const unpacked = Rect.unpacked(rects);
337
- pack(unpacked, region, { sortOrder: order });
338
- for (const r of Rect.packed(unpacked)) r.translateBy(offset);
331
+ return {
332
+ w: Number.isFinite(frame.bounds.w) ? frame.bounds.w : right,
333
+ h: Number.isFinite(frame.bounds.h) ? frame.bounds.h : bottom
334
+ };
339
335
  }
340
- function packRow(rects, outer, order) {
341
- const row = [];
342
- const byHeight = rects.toSorted(compareRects(order));
343
- const fillGaps = order !== "input";
344
- const rowRect = new Rect();
345
- rowRect.size.set(outer.size.x, 0);
346
- for (const rect of byHeight) {
347
- if (rect.wasPacked) continue;
348
- if (rect.size.x > rowRect.size.x) {
349
- if (outer.fixedSize && rect.size.x > outer.size.x) continue;
350
- const outerLeft = new Rect();
351
- outerLeft.position.setTo(rowRect.position);
352
- outerLeft.size.set(outer.size.x - rowRect.position.x, rowRect.size.y);
353
- outerLeft.fixedSize = true;
354
- if (fillGaps) packInto(rects, outerLeft, outer.position, order);
355
- rowRect.position.set(0, rowRect.bottom());
356
- rowRect.size.set(outer.size.x, 0);
357
- row.length = 0;
358
- }
359
- if (rowRect.position.y + rect.size.y > outer.size.y) {
360
- if (outer.fixedSize) continue;
361
- outer.size.y += rect.size.y;
362
- if (!outer.fixedWidth && !outer.size.x) outer.size.x = rect.size.x;
363
- Rect.resetPacked(rects);
364
- return false;
365
- }
366
- row.push(rect);
367
- rect.position.setTo(rowRect.position.add(outer.position));
368
- rowRect.shrinkLeft(rect.size.x);
369
- rect.wasPacked = true;
370
- if (rect.size.y > rowRect.size.y) rowRect.size.y = rect.size.y;
371
- if (fillGaps && row.length > 1) {
372
- const rectInRow = row[row.length - 1];
373
- const outerLeft = new Rect();
374
- outerLeft.position.set(rectInRow.position.x - outer.position.x, rectInRow.bottom() - outer.position.y);
375
- outerLeft.size.set(rectInRow.size.x, rowRect.size.y - rectInRow.size.y);
376
- outerLeft.fixedWidth = true;
377
- outerLeft.fixedSize = true;
378
- packInto(rects, outerLeft, outer.position, order);
336
+ /** This function gives a region with the axes changed: x for y, and w for h. */
337
+ function transposeRegion(region) {
338
+ switch (region.kind) {
339
+ case "rect": return {
340
+ kind: "rect",
341
+ x: region.y,
342
+ y: region.x,
343
+ w: region.h,
344
+ h: region.w
345
+ };
346
+ case "circle": return {
347
+ kind: "circle",
348
+ cx: region.cy,
349
+ cy: region.cx,
350
+ r: region.r
351
+ };
352
+ case "ellipse": return {
353
+ kind: "ellipse",
354
+ cx: region.cy,
355
+ cy: region.cx,
356
+ rx: region.ry,
357
+ ry: region.rx
358
+ };
359
+ case "polygon": {
360
+ const swap = (ring) => ring.map(([x, y]) => [y, x]);
361
+ return region.holes === void 0 ? {
362
+ kind: "polygon",
363
+ points: swap(region.points)
364
+ } : {
365
+ kind: "polygon",
366
+ points: swap(region.points),
367
+ holes: region.holes.map(swap)
368
+ };
379
369
  }
380
370
  }
381
- return true;
382
371
  }
383
- /**
384
- * This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does
385
- * not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps
386
- * its position, but the rows do not go around it. `pack` sets the width first and then uses this function.
387
- */
388
- function packRows(rects, outer, options = {}) {
389
- const order = options.sortOrder ?? "height-desc";
390
- for (const r of rects) if (r.locked) r.wasPacked = true;
391
- while (!packRow(rects, outer, order));
392
- return outer;
372
+ /** This function gives an item with the axes changed. The ports go, because the adapters of 0.1 do not read them. */
373
+ function transposeItem(item) {
374
+ const { ports: _ports, x, y, shape, margin, ...rest } = item;
375
+ return {
376
+ ...rest,
377
+ w: item.h,
378
+ h: item.w,
379
+ ...y === void 0 ? {} : { x: y },
380
+ ...x === void 0 ? {} : { y: x },
381
+ ...shape === void 0 ? {} : { shape: transposeRegion(shape) },
382
+ ...margin === void 0 ? {} : { margin: typeof margin === "number" ? margin : {
383
+ top: margin.left,
384
+ right: margin.bottom,
385
+ bottom: margin.right,
386
+ left: margin.top
387
+ } }
388
+ };
389
+ }
390
+ /** This function gives an input with the axes changed. A fixed height becomes a fixed width. */
391
+ function transposeInput(input) {
392
+ const c = input.container;
393
+ return {
394
+ container: {
395
+ fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed,
396
+ ...c.h === void 0 ? {} : { w: c.h },
397
+ ...c.w === void 0 ? {} : { h: c.w },
398
+ ...c.aspect === void 0 ? {} : { aspect: 1 / c.aspect },
399
+ ...c.aspectWeight === void 0 ? {} : { aspectWeight: c.aspectWeight },
400
+ ...c.shape === void 0 ? {} : { shape: transposeRegion(c.shape) },
401
+ ...c.inset === void 0 ? {} : { inset: c.inset }
402
+ },
403
+ items: input.items.map(transposeItem),
404
+ ...input.obstacles === void 0 ? {} : { obstacles: input.obstacles.map((o) => ({
405
+ ...o,
406
+ region: transposeRegion(o.region)
407
+ })) },
408
+ ...input.gap === void 0 ? {} : { gap: input.gap }
409
+ };
410
+ }
411
+ /** This function gives an output with the axes changed. */
412
+ function transposeOutput(output) {
413
+ return {
414
+ placements: output.placements.map((p) => ({
415
+ ...p,
416
+ x: p.y,
417
+ y: p.x
418
+ })),
419
+ overflow: [...output.overflow],
420
+ size: {
421
+ w: output.size.h,
422
+ h: output.size.w
423
+ },
424
+ moved: output.moved.map((m) => ({
425
+ id: m.id,
426
+ x: m.y,
427
+ y: m.x
428
+ })),
429
+ ...output.trace === void 0 ? {} : { trace: output.trace }
430
+ };
393
431
  }
394
432
  /**
395
- * This function packs rectangles into an outer rectangle with a guillotine row packing. It sets the position
396
- * of each rectangle and the size of the outer rectangle, and it gives the outer rectangle.
397
- *
398
- * - Without fixed sizes, the outer rectangle starts with a width of 1.5 times the square root of the total area.
399
- * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
400
- * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
401
- * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
402
- * - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
433
+ * This function packs an input with a fixed height through the turn of the axes. It changes x for y in the input.
434
+ * Then it packs with a fixed width, and changes the axes of the output back.
403
435
  */
404
- function pack(rects, outer = new Rect(), options = {}) {
405
- let totalArea = 0;
406
- let widest = 0;
407
- let minX = rects.length > 0 ? Infinity : 0;
408
- let minY = rects.length > 0 ? Infinity : 0;
409
- for (const r of rects) {
410
- totalArea += r.area();
411
- widest = Math.max(widest, r.size.x);
412
- minX = Math.min(minX, r.size.x);
413
- minY = Math.min(minY, r.size.y);
436
+ function viaAxes(input, previous, run) {
437
+ const start = now();
438
+ const inner = run(transposeInput(input), previous === void 0 ? void 0 : transposeOutput(previous));
439
+ return {
440
+ ...transposeOutput(inner),
441
+ trace: [{
442
+ id: "axes",
443
+ ms: now() - start,
444
+ measures: {}
445
+ }, ...inner.trace ?? []]
446
+ };
447
+ }
448
+ // src/space.ts
449
+ /** This function removes the closed interval from `a` to `b` from a list of spans. A span of no length goes. */
450
+ function subtract(spans, a, b) {
451
+ const out = [];
452
+ for (const [s, e] of spans) {
453
+ if (b <= s || a >= e) {
454
+ out.push([s, e]);
455
+ continue;
456
+ }
457
+ if (a > s) out.push([s, a]);
458
+ if (e > b) out.push([b, e]);
414
459
  }
415
- const idealWidth = outer.fixedWidth ? outer.size.x : Math.max(widest, Math.min(1.5 * Math.sqrt(totalArea), outer.size.x || Infinity));
416
- const ideal = Vector.of(idealWidth).clamp(new Vector(minX, minY), Vector.Infinity);
417
- if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
418
- if (options.strategy === "skyline") packSkyline(rects, outer, options);
419
- else if (options.strategy === "shelf") packShelf(rects, outer, options);
420
- else packRows(rects, outer, options);
421
- if (!outer.fixedSize) {
422
- const contentRight = Rect.maxRight(rects);
423
- if (!outer.size.x) outer.size.x = contentRight;
424
- else if (!outer.fixedWidth) outer.size.x = Math.min(contentRight, outer.size.x);
425
- outer.size.y = Rect.maxBottom(rects);
460
+ return out;
461
+ }
462
+ /** This function gives the x of a segment at `y`. The ends give their exact x. */
463
+ function xAt(ax, ay, bx, by, y) {
464
+ if (y === ay) return ax;
465
+ if (y === by) return bx;
466
+ return ax + (y - ay) * (bx - ax) / (by - ay);
467
+ }
468
+ /** This function gives the x-range of the part of a segment in the open strip from `y0` to `y1`, or `undefined`. */
469
+ function edgeRange(ax, ay, bx, by, y0, y1) {
470
+ const lo = Math.min(ay, by);
471
+ const hi = Math.max(ay, by);
472
+ if (hi <= y0 || lo >= y1) return void 0;
473
+ if (ay === by) return [Math.min(ax, bx), Math.max(ax, bx)];
474
+ const p = xAt(ax, ay, bx, by, Math.max(y0, lo));
475
+ const q = xAt(ax, ay, bx, by, Math.min(y1, hi));
476
+ return [Math.min(p, q), Math.max(p, q)];
477
+ }
478
+ /** This function gives the x-range of a region in the open strip from `y0` to `y1`, or `undefined` when they have no common point. */
479
+ function regionRange(region, y0, y1) {
480
+ switch (region.kind) {
481
+ case "rect": return region.y < y1 && y0 < region.y + region.h ? [region.x, region.x + region.w] : void 0;
482
+ case "circle":
483
+ case "ellipse": {
484
+ const rx = region.kind === "circle" ? region.r : region.rx;
485
+ const ry = region.kind === "circle" ? region.r : region.ry;
486
+ if (y1 <= region.cy - ry || y0 >= region.cy + ry) return void 0;
487
+ const dy = y0 <= region.cy && region.cy <= y1 ? 0 : Math.min(Math.abs(y0 - region.cy), Math.abs(y1 - region.cy));
488
+ const c = rx * Math.sqrt(Math.max(0, 1 - (dy / ry) ** 2));
489
+ return [region.cx - c, region.cx + c];
490
+ }
491
+ case "polygon": {
492
+ let lo = Infinity;
493
+ let hi = -Infinity;
494
+ const ring = region.points;
495
+ for (let i = 0; i < ring.length; i++) {
496
+ const a = ring[i];
497
+ const b = ring[(i + 1) % ring.length];
498
+ const r = edgeRange(a[0], a[1], b[0], b[1], y0, y1);
499
+ if (r === void 0) continue;
500
+ lo = Math.min(lo, r[0]);
501
+ hi = Math.max(hi, r[1]);
502
+ }
503
+ return lo <= hi ? [lo, hi] : void 0;
504
+ }
426
505
  }
427
- return outer;
428
506
  }
429
- // src/free.ts
430
- /** This function gives the distance between an aspect and a target aspect on a log scale. */
431
- function aspectError(width, height, target = 1) {
432
- if (!(width > 0 && height > 0 && target > 0)) return Infinity;
433
- return Math.abs(Math.log(width / height) - Math.log(target));
434
- }
435
- /**
436
- * This function packs rectangles with a free width. It tries many widths with the strategy of `options`, and keeps
437
- * the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.
438
- *
439
- * - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.
440
- * - The second pass tries the widths between the two neighbours of the best width of the first pass.
441
- * - The search is deterministic: the same input gives the same widths and the same result.
442
- * - A `locked` rectangle keeps its position. The width is not less than its right edge.
443
- * - Each strategy of `pack` works with this function, because it only changes the width.
444
- */
445
- function packFree(rects, outer = new Rect(), options = {}) {
446
- const target = options.aspect ?? 1;
447
- const weight = options.aspectWeight ?? 1;
448
- const samples = Math.max(2, Math.floor(options.samples ?? 48));
449
- const ox = outer.position.x;
450
- const oy = outer.position.y;
451
- let itemArea = 0;
452
- let minWidth = 0;
453
- let rowWidth = 0;
454
- for (const r of rects) {
455
- if (r.locked) {
456
- minWidth = Math.max(minWidth, r.right() - ox);
457
- continue;
507
+ /** This function gives the spans of the outline of a layer for the strip from `y0` to `y1`, before the holes and the blockers. */
508
+ function outline(layer, y0, y1) {
509
+ const { d } = layer;
510
+ switch (layer.kind) {
511
+ case "plain": return [[0, layer.w]];
512
+ case "box": return y0 < 0 || y1 > layer.h ? [] : [[0, layer.w]];
513
+ case "round": {
514
+ const r = layer.region;
515
+ if (r.kind !== "circle" && r.kind !== "ellipse") return [];
516
+ const rx = r.kind === "circle" ? r.r : r.rx;
517
+ const ry = r.kind === "circle" ? r.r : r.ry;
518
+ const top = y0 - d;
519
+ const bottom = y1 + d;
520
+ if (top < r.cy - ry || bottom > r.cy + ry) return [];
521
+ const dy = Math.max(Math.abs(top - r.cy), Math.abs(bottom - r.cy));
522
+ const c = rx * Math.sqrt(Math.max(0, 1 - (dy / ry) ** 2)) - EPS;
523
+ const a = r.cx - c + d;
524
+ const b = r.cx + c - d;
525
+ return b > a ? [[a, b]] : [];
458
526
  }
459
- if (!(r.size.x > 0 && r.size.y > 0)) continue;
460
- itemArea += r.area();
461
- minWidth = Math.max(minWidth, r.size.x);
462
- rowWidth += r.size.x;
463
- }
464
- for (const r of rects) if (r.locked) itemArea += r.area();
465
- const maxWidth = Math.max(minWidth, rowWidth);
466
- const tried = [];
467
- const run = (width) => {
468
- Rect.resetPacked(rects);
469
- const box = new Rect();
470
- box.position.setTo(outer.position);
471
- box.size.set(width, 0);
472
- box.fixedWidth = true;
473
- pack(rects, box, options);
474
- const placed = Rect.packed(rects);
475
- const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);
476
- const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);
477
- const area = contentWidth * contentHeight;
478
- const t = {
479
- width,
480
- contentWidth,
481
- contentHeight,
482
- objective: area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0
483
- };
484
- tried.push(t);
485
- return t;
486
- };
487
- const widths = [];
488
- if (maxWidth > 0) {
489
- const ratio = maxWidth / minWidth;
490
- for (let i = 0; i < samples; i++) {
491
- const w = minWidth * ratio ** (i / (samples - 1));
492
- const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));
493
- if (widths.at(-1) !== rounded) widths.push(rounded);
527
+ case "rings": {
528
+ const top = y0 - d;
529
+ const bottom = y1 + d;
530
+ if (!(bottom > top)) return [];
531
+ const blocked = [];
532
+ for (const ring of layer.rings) for (let i = 0; i < ring.length; i++) {
533
+ const a = ring[i];
534
+ const b = ring[(i + 1) % ring.length];
535
+ const r = edgeRange(a[0], a[1], b[0], b[1], top, bottom);
536
+ if (r !== void 0) blocked.push(r);
537
+ }
538
+ if (blocked.length === 0) return [];
539
+ blocked.sort((p, q) => p[0] - q[0] || p[1] - q[1]);
540
+ const merged = [];
541
+ for (const r of blocked) {
542
+ const last = merged.at(-1);
543
+ if (last !== void 0 && r[0] <= last[1]) last[1] = Math.max(last[1], r[1]);
544
+ else merged.push([r[0], r[1]]);
545
+ }
546
+ const spans = [];
547
+ for (let i = 0; i + 1 < merged.length; i++) {
548
+ const a = merged[i][1];
549
+ const b = merged[i + 1][0];
550
+ if (!(b > a)) continue;
551
+ if (!regionHitsRect(layer.region, {
552
+ x: a,
553
+ y: top,
554
+ w: b - a,
555
+ h: bottom - top
556
+ })) continue;
557
+ if (b - d > a + d) spans.push([a + d, b - d]);
558
+ }
559
+ return spans;
494
560
  }
495
561
  }
496
- let best = void 0;
497
- let bestIndex = 0;
498
- for (const [i, w] of widths.entries()) {
499
- const t = run(w);
500
- if (best === void 0 || t.objective < best.objective) {
501
- best = t;
502
- bestIndex = i;
503
- }
562
+ }
563
+ /** This function gives the free spans of a layer for the strip from `y0` to `y1`. */
564
+ function spansOf(layer, y0, y1) {
565
+ let spans = outline(layer, y0, y1);
566
+ for (const hole of layer.holes) {
567
+ if (spans.length === 0) return spans;
568
+ if (Math.min(y1, hole.c1) - Math.max(y0, hole.c0) > 0) spans = subtract(spans, hole.a0, hole.a1);
504
569
  }
505
- if (widths.length > 2) {
506
- const low = widths[Math.max(0, bestIndex - 1)];
507
- const high = widths[Math.min(widths.length - 1, bestIndex + 1)];
508
- const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));
509
- for (let i = 1; i <= steps; i++) {
510
- const w = Math.round(low + (high - low) * i / (steps + 1));
511
- if (widths.includes(w)) continue;
512
- const t = run(w);
513
- if (best === void 0 || t.objective < best.objective) best = t;
570
+ for (const b of layer.blockers) {
571
+ if (spans.length === 0) return spans;
572
+ if (b.c1 + b.g <= y0 || b.c0 - b.g >= y1) continue;
573
+ const r = regionRange(b.region, y0 - b.g, y1 + b.g);
574
+ if (r !== void 0) spans = subtract(spans, r[0] - b.g - EPS, r[1] + b.g + EPS);
575
+ }
576
+ return spans;
577
+ }
578
+ /** This function gives the first x from `x` where a box of width `w` fits in one of the spans, or `Infinity`. */
579
+ function firstFit(spans, x, w) {
580
+ for (const s of spans) {
581
+ const at = Math.max(x, s.start);
582
+ if (at + w <= s.end) return at;
583
+ }
584
+ return Infinity;
585
+ }
586
+ /** This function gives the next top edge of a layer after `y0` for a strip of the height `y1 - y0`, as `FreeSpace.nextY` tells. */
587
+ function nextOf(layer, y0, y1, w) {
588
+ const height = y1 - y0;
589
+ const { d } = layer;
590
+ let next = Infinity;
591
+ const take = (y) => {
592
+ if (y > y0 + EPS && y < next) next = y;
593
+ };
594
+ for (const hole of layer.holes) if (hole.c0 < y1 && y0 < hole.c1) take(hole.c1);
595
+ for (const b of layer.blockers) if (b.c0 - b.g < y1 && y0 < b.c1 + b.g) take(b.c1 + b.g);
596
+ switch (layer.kind) {
597
+ case "plain": break;
598
+ case "box":
599
+ if (y1 > layer.h) return Infinity;
600
+ break;
601
+ case "rings":
602
+ for (const ring of layer.rings) for (const [, vy] of ring) {
603
+ take(vy + d);
604
+ take(vy - height - d);
605
+ }
606
+ break;
607
+ case "round": {
608
+ const r = layer.region;
609
+ if (r.kind !== "circle" && r.kind !== "ellipse") break;
610
+ const rx = r.kind === "circle" ? r.r : r.rx;
611
+ const ry = r.kind === "circle" ? r.r : r.ry;
612
+ const need = w / 2 + d + EPS;
613
+ if (need >= rx) return Infinity;
614
+ const reach = ry * Math.sqrt(1 - (need / rx) ** 2);
615
+ if (y1 + d > r.cy + reach) return Infinity;
616
+ take(r.cy - reach + d);
617
+ break;
514
618
  }
515
619
  }
516
- const chosen = best === void 0 ? void 0 : run(best.width);
517
- tried.pop();
518
- const w = chosen?.contentWidth ?? 0;
519
- const h = chosen?.contentHeight ?? 0;
520
- if (!outer.fixedSize) outer.size.set(w, h);
521
- const area = w * h;
620
+ return next;
621
+ }
622
+ /** This function gives the layer with the axes changed: the spans go along y, and the query goes along x. */
623
+ function transposeLayer(layer) {
522
624
  return {
523
- outer,
524
- area,
525
- fill: area > 0 ? itemArea / area : 0,
526
- aspect: h > 0 ? w / h : 0,
527
- aspectError: aspectError(w, h, target),
528
- objective: chosen?.objective ?? 0,
529
- tried
625
+ kind: layer.kind,
626
+ w: layer.h,
627
+ h: layer.w,
628
+ d: layer.d,
629
+ ...layer.region === void 0 ? {} : { region: transposeRegion(layer.region) },
630
+ rings: layer.rings.map((ring) => ring.map(([x, y]) => [y, x])),
631
+ holes: layer.holes.map((h) => ({
632
+ a0: h.c0,
633
+ a1: h.c1,
634
+ c0: h.a0,
635
+ c1: h.a1
636
+ })),
637
+ blockers: layer.blockers.map((b) => {
638
+ const region = transposeRegion(b.region);
639
+ const box = boundsOf(region);
640
+ return {
641
+ region,
642
+ g: b.g,
643
+ c0: box.y,
644
+ c1: box.y + box.h
645
+ };
646
+ })
530
647
  };
531
648
  }
532
- // src/push.ts
533
- /** This function gives the overlap of two rectangles on each axis. A value of 0 or less is no overlap. */
534
- function overlapOf(a, b) {
649
+ /** This function makes a blocker of a region in the frame. */
650
+ function blockerOf(region, g) {
651
+ const box = boundsOf(region);
535
652
  return {
536
- x: Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x),
537
- y: Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y)
653
+ region,
654
+ g,
655
+ c0: box.y,
656
+ c1: box.y + box.h
538
657
  };
539
658
  }
540
659
  /**
541
- * This function gives the direction of a first shove. The axis has the smaller overlap. The sign points away from the
542
- * center of the pusher. When that shove crosses 0, the sign is positive.
660
+ * This function gives the free space of an input and its frame. The adapters use it with the frame that they pack.
661
+ * `freeSpace` makes the frame itself.
543
662
  */
544
- function firstDirection(pusher, r, gap) {
545
- const overlap = overlapOf(pusher, r);
546
- if (overlap.x < overlap.y) return {
547
- axis: "x",
548
- sign: r.position.x + r.size.x / 2 < pusher.position.x + pusher.size.x / 2 && pusher.position.x - r.size.x - gap >= 0 ? -1 : 1
663
+ function spaceOf(input, frame) {
664
+ const { container } = input;
665
+ const half = (input.gap ?? 0) / 2;
666
+ const d = frame.inset - half;
667
+ const shape = container.shape === void 0 ? void 0 : translate(container.shape, -frame.ox, -frame.oy);
668
+ const fixedSize = Number.isFinite(frame.w) && Number.isFinite(frame.h);
669
+ const holes = [];
670
+ const blockers = [];
671
+ let rectObstacles = true;
672
+ for (const e of frame.entries) {
673
+ if (!e.rect.locked) continue;
674
+ if (e.obstacle !== void 0 && e.obstacle.region.kind !== "rect") {
675
+ rectObstacles = false;
676
+ blockers.push(blockerOf(translate(e.obstacle.region, -frame.ox, -frame.oy), half));
677
+ continue;
678
+ }
679
+ const r = e.rect;
680
+ holes.push({
681
+ a0: r.position.x,
682
+ a1: r.right(),
683
+ c0: r.position.y,
684
+ c1: r.bottom()
685
+ });
686
+ }
687
+ const plain = (shape === void 0 || shape.kind === "rect") && rectObstacles;
688
+ let layer = {
689
+ kind: plain ? "plain" : "box",
690
+ w: frame.w,
691
+ h: frame.h,
692
+ d,
693
+ rings: [],
694
+ holes,
695
+ blockers
549
696
  };
697
+ if (shape !== void 0 && shape.kind !== "rect") {
698
+ if (fixedSize && shape.kind === "polygon") layer = {
699
+ ...layer,
700
+ kind: "rings",
701
+ region: shape,
702
+ rings: [shape.points, ...shape.holes ?? []]
703
+ };
704
+ else if (fixedSize) layer = {
705
+ ...layer,
706
+ kind: "round",
707
+ region: shape
708
+ };
709
+ else if (shape.kind === "polygon") for (const ring of shape.holes ?? []) blockers.push(blockerOf({
710
+ kind: "polygon",
711
+ points: ring
712
+ }, d));
713
+ }
714
+ const across = transposeLayer(layer);
715
+ const band = (y0, y1) => spansOf(layer, y0, y1).map(([start, end]) => ({
716
+ start,
717
+ end
718
+ }));
550
719
  return {
551
- axis: "y",
552
- sign: r.position.y + r.size.y / 2 < pusher.position.y + pusher.size.y / 2 && pusher.position.y - r.size.y - gap >= 0 ? -1 : 1
720
+ w: frame.w,
721
+ h: frame.h,
722
+ plain,
723
+ band,
724
+ column(x0, x1, from = 0) {
725
+ for (const [start, end] of spansOf(across, x0, x1)) {
726
+ const top = Math.max(start, from);
727
+ if (end > top) return {
728
+ start: top,
729
+ end
730
+ };
731
+ }
732
+ },
733
+ fits(rect) {
734
+ return firstFit(band(rect.y, rect.y + rect.h), rect.x, rect.w) === rect.x;
735
+ },
736
+ fitX(x, w, y0, y1) {
737
+ return firstFit(band(y0, y1), x, w);
738
+ },
739
+ nextY(y0, y1, w) {
740
+ return nextOf(layer, y0, y1, w);
741
+ }
553
742
  };
554
743
  }
555
- /** This function puts a rectangle past the pusher, along a direction. The rectangle does not go past 0. */
556
- function shove(pusher, r, direction, gap) {
557
- if (direction.axis === "x") {
558
- const x = direction.sign > 0 ? pusher.right() + gap : pusher.position.x - r.size.x - gap;
559
- r.position.set(Math.max(0, x), r.position.y);
560
- return;
561
- }
562
- const y = direction.sign > 0 ? pusher.bottom() + gap : pusher.position.y - r.size.y - gap;
563
- r.position.set(r.position.x, Math.max(0, y));
744
+ /**
745
+ * This function gives the free space of an input, in the coordinates of the frame of the adapters. The origin of the
746
+ * frame is the corner of the container, moved in by the inset and out by half the gap. `FreeSpace` tells the methods.
747
+ */
748
+ function freeSpace(input) {
749
+ return spaceOf(input, buildFrame(input));
564
750
  }
751
+ // src/shelf.ts
565
752
  /**
566
- * This function moves each rectangle that a placed rectangle overlaps, and the rectangles that those overlap in turn.
567
- * A shove takes the axis of the smaller overlap, away from the pusher. A shoved rectangle keeps the direction of its first
568
- * shove when it shoves the next one, so the cascade goes outward. The placed rectangle and each locked rectangle stay.
569
- * The function moves the rectangles in place and gives the rectangles that moved.
570
- *
571
- * @example
572
- * ```ts
573
- * const moved = pushCascade(dropped, cards, { gap: 8 });
574
- * ```
753
+ * This function gives the first x from `start` where a rectangle of width `w` crosses no obstacle in the band from
754
+ * `top` to `bottom`.
575
755
  */
576
- function pushCascade(placed, rects, options = {}) {
577
- const gap = options.gap ?? 8;
578
- const maxShoves = options.maxShoves ?? rects.length * 8;
579
- const moved = /* @__PURE__ */ new Set();
580
- const queue = [{ pusher: placed }];
581
- let shoves = 0;
582
- while (shoves < maxShoves) {
583
- const step = queue.shift();
584
- if (!step) break;
585
- const { pusher, direction } = step;
586
- for (const r of rects) {
587
- if (r === pusher || r === placed || r.locked) continue;
588
- const overlap = overlapOf(pusher, r);
589
- if (overlap.x <= 0 || overlap.y <= 0) continue;
590
- const next = direction ?? firstDirection(pusher, r, gap);
591
- shove(pusher, r, next, gap);
592
- moved.add(r);
593
- queue.push({
594
- pusher: r,
595
- direction: next
596
- });
597
- if (++shoves >= maxShoves) break;
756
+ function firstFreeX(obstacles, start, w, top, bottom) {
757
+ let x = start;
758
+ let moved = true;
759
+ while (moved) {
760
+ moved = false;
761
+ for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.left < x + w && x < o.right) {
762
+ x = o.right;
763
+ moved = true;
598
764
  }
599
765
  }
600
- return [...moved];
766
+ return x;
601
767
  }
602
- // src/contract.ts
603
- /** This function gives the margin of an item on each side. A number is the same margin on each side. */
604
- function marginOf(item) {
605
- const m = marginSides(item.margin);
606
- return {
607
- top: m.top,
608
- right: m.right,
609
- bottom: m.bottom,
610
- left: m.left
611
- };
768
+ /** This function gives the lowest bottom edge of the obstacles in the band from `top` to `bottom`, under `top`. */
769
+ function nextTop(obstacles, top, bottom) {
770
+ let next = Infinity;
771
+ for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.bottom > top) next = Math.min(next, o.bottom);
772
+ return next;
612
773
  }
613
774
  /**
614
- * This function gives an item after a turn of 90 degrees clockwise, with the y axis down (decision 1 of
615
- * `docs/PACKER.md`). The width and the height change places. The top-left corner, the id and the other fields stay.
616
- * The ports, the shape and the margin turn with the item. The function does not change its argument.
617
- * It is `turnBox` of the contract.
775
+ * This function packs rectangles into shelves: rows that it fills left to right in the sort order, and does not fill
776
+ * again. It goes around each `locked` rectangle. It sets the position of each rectangle and gives the outer rectangle.
777
+ *
778
+ * - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.
779
+ * - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.
780
+ * - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,
781
+ * a new row starts under the current row.
782
+ * - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
783
+ * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
784
+ * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
785
+ * - With a space that is not plain, `packRowsInSpace` packs the rows into the outline of the space and around its holes.
618
786
  */
619
- function turnItem(item) {
620
- return turnBox(item);
621
- }
622
- // src/sha256.ts
623
- /** The round constants of SHA-256. */
624
- const K = new Uint32Array([
625
- 1116352408,
626
- 1899447441,
627
- 3049323471,
628
- 3921009573,
629
- 961987163,
630
- 1508970993,
631
- 2453635748,
632
- 2870763221,
633
- 3624381080,
634
- 310598401,
635
- 607225278,
636
- 1426881987,
637
- 1925078388,
638
- 2162078206,
639
- 2614888103,
640
- 3248222580,
641
- 3835390401,
642
- 4022224774,
643
- 264347078,
644
- 604807628,
645
- 770255983,
646
- 1249150122,
647
- 1555081692,
648
- 1996064986,
649
- 2554220882,
650
- 2821834349,
651
- 2952996808,
652
- 3210313671,
653
- 3336571891,
654
- 3584528711,
655
- 113926993,
656
- 338241895,
657
- 666307205,
658
- 773529912,
659
- 1294757372,
660
- 1396182291,
661
- 1695183700,
662
- 1986661051,
663
- 2177026350,
664
- 2456956037,
665
- 2730485921,
666
- 2820302411,
667
- 3259730800,
668
- 3345764771,
669
- 3516065817,
670
- 3600352804,
671
- 4094571909,
672
- 275423344,
673
- 430227734,
674
- 506948616,
675
- 659060556,
676
- 883997877,
677
- 958139571,
678
- 1322822218,
679
- 1537002063,
680
- 1747873779,
681
- 1955562222,
682
- 2024104815,
683
- 2227730452,
684
- 2361852424,
685
- 2428436474,
686
- 2756734187,
687
- 3204031479,
688
- 3329325298
689
- ]);
690
- /** The first hash value of SHA-256. */
691
- const H0 = [
692
- 1779033703,
693
- 3144134277,
694
- 1013904242,
695
- 2773480762,
696
- 1359893119,
697
- 2600822924,
698
- 528734635,
699
- 1541459225
700
- ];
701
- /** This function turns a 32-bit word to the right by `n` bits. */
702
- const rotr = (x, n) => x >>> n | x << 32 - n;
787
+ function packShelf(rects, outer, options = {}) {
788
+ if (options.space !== void 0 && !options.space.plain) return packRowsInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
789
+ const width = outer.size.x;
790
+ const obstacles = [];
791
+ let contentBottom = 0;
792
+ for (const rect of rects) {
793
+ if (!rect.locked) continue;
794
+ rect.wasPacked = true;
795
+ const left = rect.position.x - outer.position.x;
796
+ const top = rect.position.y - outer.position.y;
797
+ obstacles.push({
798
+ left,
799
+ top,
800
+ right: left + rect.size.x,
801
+ bottom: top + rect.size.y
802
+ });
803
+ contentBottom = Math.max(contentBottom, top + rect.size.y);
804
+ }
805
+ let rowTop = 0;
806
+ let rowBottom = 0;
807
+ let cursor = 0;
808
+ let rowEmpty = true;
809
+ for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
810
+ if (rect.wasPacked) continue;
811
+ const w = rect.size.x;
812
+ const h = rect.size.y;
813
+ if (!(w > 0 && h > 0)) continue;
814
+ const overflows = w > width;
815
+ if (overflows && outer.fixedSize) continue;
816
+ if (overflows && !rowEmpty) {
817
+ rowTop = rowBottom;
818
+ cursor = 0;
819
+ rowEmpty = true;
820
+ }
821
+ let x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
822
+ while (!overflows && x + w > width) {
823
+ rowTop = rowEmpty ? nextTop(obstacles, rowTop, rowTop + h) : rowBottom;
824
+ rowBottom = rowTop;
825
+ cursor = 0;
826
+ rowEmpty = true;
827
+ x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
828
+ }
829
+ while (overflows && x !== 0) {
830
+ rowTop = nextTop(obstacles, rowTop, rowTop + h);
831
+ rowBottom = rowTop;
832
+ x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
833
+ }
834
+ if (outer.fixedSize && rowTop + h > outer.size.y) continue;
835
+ rect.position.set(x + outer.position.x, rowTop + outer.position.y);
836
+ rect.wasPacked = true;
837
+ rowEmpty = false;
838
+ cursor = x + w;
839
+ rowBottom = Math.max(rowBottom, rowTop + h);
840
+ contentBottom = Math.max(contentBottom, rowTop + h);
841
+ if (overflows) {
842
+ rowTop = rowBottom;
843
+ cursor = 0;
844
+ rowEmpty = true;
845
+ }
846
+ }
847
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
848
+ return outer;
849
+ }
703
850
  /**
704
- * This function gives the UTF-8 bytes of a string. A lone surrogate becomes U+FFFD, as with `TextEncoder`. The
705
- * package has no `TextEncoder` type, because its code operates on each runtime with no global types.
851
+ * This function gives the first x from `start` where a rectangle of width `w` fits. There, it crosses no locked rectangle,
852
+ * and it is in a free span of its band. It takes `firstFreeX` and `firstFit` in turn until they agree. It gives `Infinity` when no x fits.
706
853
  */
707
- function utf8(text) {
708
- const out = [];
709
- for (let i = 0; i < text.length; i++) {
710
- let c = text.charCodeAt(i);
711
- if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
712
- const next = text.charCodeAt(i + 1);
713
- if (next >= 56320 && next <= 57343) {
714
- c = 65536 + (c - 55296 << 10) + (next - 56320);
715
- i++;
854
+ function placeInBand(obstacles, spans, start, w, top, bottom) {
855
+ let x = start;
856
+ for (;;) {
857
+ const free = firstFreeX(obstacles, x, w, top, bottom);
858
+ const fit = firstFit(spans, free, w);
859
+ if (fit === free || !Number.isFinite(fit)) return fit;
860
+ x = fit;
861
+ }
862
+ }
863
+ /**
864
+ * This function packs rectangles into rows inside a free space. `packShelf` and `packRows` use it when the space is not
865
+ * plain. It sets the position of each rectangle and gives the outer rectangle.
866
+ *
867
+ * - A rectangle goes to the first x from the row cursor where it fits in the space and crosses no locked rectangle.
868
+ * - When it does not fit, a new row starts under the current row. An empty row steps down to `nextY` of the space.
869
+ * - When the steps of an empty row find no place, the row goes back to its last top edge. Later rectangles can use it.
870
+ * - A rectangle wider than the outer rectangle overflows. Without `fixedSize`, it goes on its own row at the left edge.
871
+ */
872
+ function packRowsInSpace(rects, outer, order, space) {
873
+ const width = outer.size.x;
874
+ const ox = outer.position.x;
875
+ const oy = outer.position.y;
876
+ const limit = outer.fixedSize ? outer.size.y : Infinity;
877
+ const obstacles = [];
878
+ let contentBottom = 0;
879
+ for (const rect of rects) {
880
+ if (!rect.locked) continue;
881
+ rect.wasPacked = true;
882
+ const left = rect.position.x - ox;
883
+ const top = rect.position.y - oy;
884
+ obstacles.push({
885
+ left,
886
+ top,
887
+ right: left + rect.size.x,
888
+ bottom: top + rect.size.y
889
+ });
890
+ contentBottom = Math.max(contentBottom, top + rect.size.y);
891
+ }
892
+ const place = (start, w, top, h) => {
893
+ const spans = space.band(top + oy, top + h + oy).map((s) => ({
894
+ start: s.start - ox,
895
+ end: s.end - ox
896
+ }));
897
+ return placeInBand(obstacles, spans, start, w, top, top + h);
898
+ };
899
+ let rowTop = 0;
900
+ let rowBottom = 0;
901
+ let cursor = 0;
902
+ let rowEmpty = true;
903
+ for (const rect of rects.toSorted(compareRects(order))) {
904
+ if (rect.wasPacked) continue;
905
+ const w = rect.size.x;
906
+ const h = rect.size.y;
907
+ if (!(w > 0 && h > 0)) continue;
908
+ if (w > width) {
909
+ if (outer.fixedSize) continue;
910
+ const top = rowEmpty ? rowTop : rowBottom;
911
+ rect.position.set(ox, top + oy);
912
+ rect.wasPacked = true;
913
+ rowTop = top + h;
914
+ rowBottom = rowTop;
915
+ cursor = 0;
916
+ rowEmpty = true;
917
+ contentBottom = Math.max(contentBottom, top + h);
918
+ continue;
919
+ }
920
+ let x = place(cursor, w, rowTop, h);
921
+ if (!(x + w <= width) && !rowEmpty) {
922
+ rowTop = rowBottom;
923
+ cursor = 0;
924
+ rowEmpty = true;
925
+ x = place(0, w, rowTop, h);
926
+ }
927
+ let top = rowTop;
928
+ while (!(x + w <= width) && top + h <= limit) {
929
+ const next = Math.min(space.nextY(top + oy, top + h + oy, w) - oy, nextTop(obstacles, top, top + h));
930
+ if (!Number.isFinite(next)) break;
931
+ top = next;
932
+ x = place(0, w, top, h);
933
+ }
934
+ if (!(x + w <= width) || top + h > limit) continue;
935
+ if (top !== rowTop) {
936
+ rowTop = top;
937
+ rowBottom = top;
938
+ }
939
+ rect.position.set(x + ox, rowTop + oy);
940
+ rect.wasPacked = true;
941
+ rowEmpty = false;
942
+ cursor = x + w;
943
+ rowBottom = Math.max(rowBottom, rowTop + h);
944
+ contentBottom = Math.max(contentBottom, rowTop + h);
945
+ }
946
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
947
+ return outer;
948
+ }
949
+ // src/skyline.ts
950
+ /**
951
+ * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
952
+ * of the window is the largest value of `skyline[x, x + rw)`. The first `x` with the smallest height wins. A
953
+ * monotonic deque makes the search O(width) for one rectangle.
954
+ *
955
+ * With `limit`, a window is valid only when its height plus `rh` is not more than the smallest value of
956
+ * `limit[x, x + rw)`. A second deque keeps that smallest value. No valid window gives the height `Infinity`.
957
+ */
958
+ function lowestWindow(skyline, width, rw, limit, rh = 0) {
959
+ if (limit !== void 0) return lowestLimitedWindow(skyline, limit, width, rw, rh);
960
+ const deque = new Int32Array(width);
961
+ let head = 0;
962
+ let tail = 0;
963
+ let bestX = 0;
964
+ let bestY = Infinity;
965
+ for (let i = 0; i < width; i++) {
966
+ while (tail > head && skyline[deque[tail - 1]] <= skyline[i]) tail--;
967
+ deque[tail++] = i;
968
+ if (deque[head] <= i - rw) head++;
969
+ if (i >= rw - 1) {
970
+ const max = skyline[deque[head]];
971
+ if (max < bestY) {
972
+ bestY = max;
973
+ bestX = i - rw + 1;
716
974
  }
717
975
  }
718
- if (c >= 55296 && c <= 57343) c = 65533;
719
- if (c < 128) out.push(c);
720
- else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
721
- else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
722
- else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
723
976
  }
724
- return Uint8Array.from(out);
977
+ return {
978
+ x: bestX,
979
+ y: bestY
980
+ };
725
981
  }
726
- /** This function gives the SHA-256 of the UTF-8 bytes of a string, as 64 lowercase hex characters. */
727
- function sha256Hex(text) {
728
- const bytes = utf8(text);
729
- const length = bytes.length;
730
- const padded = new Uint8Array(((length + 9 + 63) / 64 | 0) * 64);
731
- padded.set(bytes);
732
- padded[length] = 128;
733
- const view = new DataView(padded.buffer);
734
- const bits = length * 8;
735
- view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
736
- view.setUint32(padded.length - 4, bits >>> 0);
737
- const hash = [...H0];
738
- const w = /* @__PURE__ */ new Uint32Array(64);
739
- for (let block = 0; block < padded.length; block += 64) {
740
- for (let t = 0; t < 16; t++) w[t] = view.getUint32(block + t * 4);
741
- for (let t = 16; t < 64; t++) {
742
- const a = w[t - 15];
743
- const b = w[t - 2];
744
- const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
745
- const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
746
- w[t] = w[t - 16] + s0 + w[t - 7] + s1 | 0;
982
+ /** This function is `lowestWindow` with a limit for each cell. */
983
+ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
984
+ const high = new Int32Array(width);
985
+ const low = new Int32Array(width);
986
+ let hh = 0;
987
+ let ht = 0;
988
+ let lh = 0;
989
+ let lt = 0;
990
+ let bestX = 0;
991
+ let bestY = Infinity;
992
+ for (let i = 0; i < width; i++) {
993
+ while (ht > hh && skyline[high[ht - 1]] <= skyline[i]) ht--;
994
+ high[ht++] = i;
995
+ if (high[hh] <= i - rw) hh++;
996
+ while (lt > lh && limit[low[lt - 1]] >= limit[i]) lt--;
997
+ low[lt++] = i;
998
+ if (low[lh] <= i - rw) lh++;
999
+ if (i >= rw - 1) {
1000
+ const max = skyline[high[hh]];
1001
+ if (max < bestY && max + rh <= limit[low[lh]]) {
1002
+ bestY = max;
1003
+ bestX = i - rw + 1;
1004
+ }
747
1005
  }
748
- let [a, b, c, d, e, f, g, h] = hash;
749
- for (let t = 0; t < 64; t++) {
750
- const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
751
- const ch = e & f ^ ~e & g;
752
- const t1 = h + s1 + ch + K[t] + w[t] | 0;
753
- const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
754
- h = g;
755
- g = f;
756
- f = e;
757
- e = d + t1 | 0;
758
- d = c;
759
- c = b;
760
- b = a;
761
- a = t1 + t2 | 0;
1006
+ }
1007
+ return {
1008
+ x: bestX,
1009
+ y: bestY
1010
+ };
1011
+ }
1012
+ /**
1013
+ * This function packs rectangles into an outer rectangle with a skyline. It puts each rectangle in the lowest place
1014
+ * where it fits, and it does not change the width of the outer rectangle.
1015
+ *
1016
+ * - A `locked` rectangle keeps its position. The skyline starts at its bottom edge across its width, thus the other
1017
+ * rectangles go under it. The space above it stays empty, because a skyline has no holes.
1018
+ * - The skyline has a cell for each whole unit of the width. A rectangle that needs more cells than the outer
1019
+ * rectangle has goes under all other rectangles, at the left edge.
1020
+ * - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that
1021
+ * does not fit is not packed: its `wasPacked` stays `false`.
1022
+ * - The default sort order is `height-desc`. The order of the array of the caller stays.
1023
+ * - With a space that is not plain, each cell gets its start and its limit from `column` of the space. The space holds
1024
+ * the obstacles and the locked items, thus the locked rectangles only keep their positions.
1025
+ */
1026
+ function packSkyline(rects, outer, options = {}) {
1027
+ if (options.space !== void 0 && !options.space.plain) return packSkylineInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
1028
+ const width = Math.floor(outer.size.x);
1029
+ if (width <= 0) return outer;
1030
+ const skyline = new Float64Array(width);
1031
+ for (const rect of rects) {
1032
+ if (!rect.locked) continue;
1033
+ rect.wasPacked = true;
1034
+ const x0 = Math.max(0, Math.floor(rect.position.x - outer.position.x));
1035
+ const x1 = Math.min(width, Math.ceil(rect.position.x - outer.position.x + rect.size.x));
1036
+ const bottom = rect.position.y - outer.position.y + rect.size.y;
1037
+ for (let i = x0; i < x1; i++) skyline[i] = Math.max(skyline[i], bottom);
1038
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, bottom);
1039
+ }
1040
+ for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
1041
+ if (rect.wasPacked) continue;
1042
+ const rw = Math.ceil(rect.size.x);
1043
+ const rh = rect.size.y;
1044
+ if (rw <= 0 || rh <= 0) continue;
1045
+ let x;
1046
+ let y;
1047
+ if (rw > width) {
1048
+ if (outer.fixedSize) continue;
1049
+ x = 0;
1050
+ y = 0;
1051
+ for (let i = 0; i < width; i++) if (skyline[i] > y) y = skyline[i];
1052
+ } else {
1053
+ ({x, y} = lowestWindow(skyline, width, rw));
1054
+ if (outer.fixedSize && y + rh > outer.size.y) continue;
762
1055
  }
763
- hash[0] = hash[0] + a | 0;
764
- hash[1] = hash[1] + b | 0;
765
- hash[2] = hash[2] + c | 0;
766
- hash[3] = hash[3] + d | 0;
767
- hash[4] = hash[4] + e | 0;
768
- hash[5] = hash[5] + f | 0;
769
- hash[6] = hash[6] + g | 0;
770
- hash[7] = hash[7] + h | 0;
1056
+ rect.position.set(x + outer.position.x, y + outer.position.y);
1057
+ rect.wasPacked = true;
1058
+ const top = y + rh;
1059
+ for (let i = x; i < Math.min(width, x + rw); i++) skyline[i] = top;
1060
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);
771
1061
  }
772
- return hash.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
1062
+ return outer;
773
1063
  }
774
- // src/layout-input.ts
775
- /** This function gives the `fnv1a32` hash of the UTF-8 bytes of a string, as `docs/FORMAT.md` gives it. */
776
- function fnv1a32(text) {
777
- let h = 2166136261;
778
- for (const b of utf8(text)) h = Math.imul(h ^ b, 16777619) >>> 0;
779
- return h;
1064
+ /**
1065
+ * This function packs rectangles with a skyline in a free space. Each cell has a free range: the skyline is its start,
1066
+ * and `limit` is its end. A cell with less room than the lowest rectangle left goes to its next free range.
1067
+ */
1068
+ function packSkylineInSpace(rects, outer, order, space) {
1069
+ const width = Math.floor(outer.size.x);
1070
+ if (width <= 0) return outer;
1071
+ const ox = outer.position.x;
1072
+ const oy = outer.position.y;
1073
+ const end = outer.fixedSize ? outer.size.y : Infinity;
1074
+ for (const rect of rects) if (rect.locked) rect.wasPacked = true;
1075
+ const rangeOf = (i, from) => {
1076
+ const r = space.column(ox + i, ox + i + 1, from + oy);
1077
+ if (r === void 0) return [Infinity, -Infinity];
1078
+ return [r.start - oy, Math.min(r.end - oy, end)];
1079
+ };
1080
+ const skyline = new Float64Array(width);
1081
+ const limit = new Float64Array(width);
1082
+ for (let i = 0; i < width; i++) [skyline[i], limit[i]] = rangeOf(i, 0);
1083
+ const sorted = rects.toSorted(compareRects(order)).filter((r) => !r.wasPacked && Math.ceil(r.size.x) > 0 && r.size.y > 0);
1084
+ const lowest = new Float64Array(sorted.length + 1).fill(Infinity);
1085
+ for (let k = sorted.length - 1; k >= 0; k--) lowest[k] = Math.min(lowest[k + 1], sorted[k].size.y);
1086
+ const advance = (k) => {
1087
+ const need = lowest[k];
1088
+ if (!Number.isFinite(need)) return;
1089
+ for (let i = 0; i < width; i++) while (Number.isFinite(limit[i]) && limit[i] - skyline[i] < need && limit[i] < end) [skyline[i], limit[i]] = rangeOf(i, limit[i]);
1090
+ };
1091
+ for (const [k, rect] of sorted.entries()) {
1092
+ advance(k);
1093
+ const rw = Math.ceil(rect.size.x);
1094
+ const rh = rect.size.y;
1095
+ let x;
1096
+ let y;
1097
+ if (rw > width) {
1098
+ if (outer.fixedSize) continue;
1099
+ x = 0;
1100
+ y = 0;
1101
+ for (let i = 0; i < width; i++) if (Number.isFinite(skyline[i]) && skyline[i] > y) y = skyline[i];
1102
+ rect.position.set(x + ox, y + oy);
1103
+ rect.wasPacked = true;
1104
+ outer.size.y = Math.max(outer.size.y, y + rh);
1105
+ continue;
1106
+ }
1107
+ ({x, y} = lowestWindow(skyline, width, rw, limit, rh));
1108
+ let sky = skyline;
1109
+ let lim = limit;
1110
+ if (!Number.isFinite(y)) {
1111
+ sky = skyline.slice();
1112
+ lim = limit.slice();
1113
+ for (let i = 0; i < width; i++) while (Number.isFinite(lim[i]) && lim[i] - sky[i] < rh && lim[i] < end) [sky[i], lim[i]] = rangeOf(i, lim[i]);
1114
+ ({x, y} = lowestWindow(sky, width, rw, lim, rh));
1115
+ if (!Number.isFinite(y)) continue;
1116
+ }
1117
+ rect.position.set(x + ox, y + oy);
1118
+ rect.wasPacked = true;
1119
+ const top = y + rh;
1120
+ for (let i = x; i < x + rw; i++) {
1121
+ skyline[i] = top;
1122
+ limit[i] = lim[i];
1123
+ }
1124
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);
1125
+ }
1126
+ return outer;
780
1127
  }
781
- /** This function rounds the numbers of a file to 3 decimals. A half goes up, and the value `-0` becomes 0. */
782
- function round3(x) {
783
- const v = Math.round(x * 1e3) / 1e3;
784
- return Object.is(v, -0) ? 0 : v;
1128
+ // src/pack.ts
1129
+ function packInto(rects, region, offset, order) {
1130
+ const unpacked = Rect.unpacked(rects);
1131
+ pack(unpacked, region, { sortOrder: order });
1132
+ for (const r of Rect.packed(unpacked)) r.translateBy(offset);
1133
+ }
1134
+ function packRow(rects, outer, order) {
1135
+ const row = [];
1136
+ const byHeight = rects.toSorted(compareRects(order));
1137
+ const fillGaps = order !== "input";
1138
+ const rowRect = new Rect();
1139
+ rowRect.size.set(outer.size.x, 0);
1140
+ for (const rect of byHeight) {
1141
+ if (rect.wasPacked) continue;
1142
+ if (rect.size.x > rowRect.size.x) {
1143
+ if (outer.fixedSize && rect.size.x > outer.size.x) continue;
1144
+ const outerLeft = new Rect();
1145
+ outerLeft.position.setTo(rowRect.position);
1146
+ outerLeft.size.set(outer.size.x - rowRect.position.x, rowRect.size.y);
1147
+ outerLeft.fixedSize = true;
1148
+ if (fillGaps) packInto(rects, outerLeft, outer.position, order);
1149
+ rowRect.position.set(0, rowRect.bottom());
1150
+ rowRect.size.set(outer.size.x, 0);
1151
+ row.length = 0;
1152
+ }
1153
+ if (rowRect.position.y + rect.size.y > outer.size.y) {
1154
+ if (outer.fixedSize) continue;
1155
+ outer.size.y += rect.size.y;
1156
+ if (!outer.fixedWidth && !outer.size.x) outer.size.x = rect.size.x;
1157
+ Rect.resetPacked(rects);
1158
+ return false;
1159
+ }
1160
+ row.push(rect);
1161
+ rect.position.setTo(rowRect.position.add(outer.position));
1162
+ rowRect.shrinkLeft(rect.size.x);
1163
+ rect.wasPacked = true;
1164
+ if (rect.size.y > rowRect.size.y) rowRect.size.y = rect.size.y;
1165
+ if (fillGaps && row.length > 1) {
1166
+ const rectInRow = row[row.length - 1];
1167
+ const outerLeft = new Rect();
1168
+ outerLeft.position.set(rectInRow.position.x - outer.position.x, rectInRow.bottom() - outer.position.y);
1169
+ outerLeft.size.set(rectInRow.size.x, rowRect.size.y - rectInRow.size.y);
1170
+ outerLeft.fixedWidth = true;
1171
+ outerLeft.fixedSize = true;
1172
+ packInto(rects, outerLeft, outer.position, order);
1173
+ }
1174
+ }
1175
+ return true;
785
1176
  }
786
1177
  /**
787
- * This function gives the canonical form of a value: sorted keys, no whitespace and the numbers of `JSON.stringify`
788
- * after `round3`. A key with the value `undefined` is not in the form.
1178
+ * This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does
1179
+ * not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps
1180
+ * its position, but the rows do not go around it. `pack` sets the width first and then uses this function.
1181
+ *
1182
+ * With `options.space`, the rows go around the locked rectangles and the holes of the space, inside its outline. A space
1183
+ * that is plain, with no locked rectangle, gives the rows of 0.1.
789
1184
  */
790
- function canonicalJson(value) {
791
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
792
- if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
793
- return JSON.stringify(typeof value === "number" ? round3(value) : value);
1185
+ function packRows(rects, outer, options = {}) {
1186
+ const order = options.sortOrder ?? "height-desc";
1187
+ const space = options.space;
1188
+ if (space !== void 0 && (!space.plain || rects.some((r) => r.locked))) return packRowsInSpace(rects, outer, order, space);
1189
+ for (const r of rects) if (r.locked) r.wasPacked = true;
1190
+ while (!packRow(rects, outer, order));
1191
+ return outer;
794
1192
  }
795
- /** This function gives the hash of an input file: the first 16 hex characters of the SHA-256 of its canonical form without `hash`. */
796
- function hashInput(file) {
797
- const { hash: _hash, ...rest } = file;
798
- return sha256Hex(canonicalJson(rest)).slice(0, 16);
1193
+ /**
1194
+ * This function packs rectangles into an outer rectangle with a guillotine row packing. It sets the position
1195
+ * of each rectangle and the size of the outer rectangle, and it gives the outer rectangle.
1196
+ *
1197
+ * - Without fixed sizes, the outer rectangle starts with a width of 1.5 times the square root of the total area.
1198
+ * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
1199
+ * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
1200
+ * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
1201
+ * - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
1202
+ */
1203
+ function pack(rects, outer = new Rect(), options = {}) {
1204
+ let totalArea = 0;
1205
+ let widest = 0;
1206
+ let minX = rects.length > 0 ? Infinity : 0;
1207
+ let minY = rects.length > 0 ? Infinity : 0;
1208
+ for (const r of rects) {
1209
+ totalArea += r.area();
1210
+ widest = Math.max(widest, r.size.x);
1211
+ minX = Math.min(minX, r.size.x);
1212
+ minY = Math.min(minY, r.size.y);
1213
+ }
1214
+ const idealWidth = outer.fixedWidth ? outer.size.x : Math.max(widest, Math.min(1.5 * Math.sqrt(totalArea), outer.size.x || Infinity));
1215
+ const ideal = Vector.of(idealWidth).clamp(new Vector(minX, minY), Vector.Infinity);
1216
+ if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
1217
+ if (options.strategy === "skyline") packSkyline(rects, outer, options);
1218
+ else if (options.strategy === "shelf") packShelf(rects, outer, options);
1219
+ else packRows(rects, outer, options);
1220
+ if (!outer.fixedSize) {
1221
+ const contentRight = Rect.maxRight(rects);
1222
+ if (!outer.size.x) outer.size.x = contentRight;
1223
+ else if (!outer.fixedWidth) outer.size.x = Math.min(contentRight, outer.size.x);
1224
+ outer.size.y = Rect.maxBottom(rects);
1225
+ }
1226
+ return outer;
799
1227
  }
800
- /** This function gives the scale of a count: the nearest of 1, 10, 100, 1000 and 10000 on a log scale. The count 0 gives `1`. */
801
- function countCell(count) {
802
- if (count <= 3) return "1";
803
- if (count <= 31) return "10";
804
- if (count <= 316) return "100";
805
- if (count <= 3162) return "1k";
806
- return "10k";
1228
+ // src/free.ts
1229
+ /** This function gives the distance between an aspect and a target aspect on a log scale. */
1230
+ function aspectError(width, height, target = 1) {
1231
+ if (!(width > 0 && height > 0 && target > 0)) return Infinity;
1232
+ return Math.abs(Math.log(width / height) - Math.log(target));
807
1233
  }
808
1234
  /**
809
- * This function gives the packing input of a file. The boxes become the items, and the edges go.
810
- * A file without a container gets `{ fixed: "none" }`, so the packer picks the size. It is `toPackInput` of the contract.
1235
+ * This function packs rectangles with a free width. It tries many widths with the strategy of `options`, and keeps
1236
+ * the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.
1237
+ *
1238
+ * - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.
1239
+ * - The second pass tries the widths between the two neighbours of the best width of the first pass.
1240
+ * - The search is deterministic: the same input gives the same widths and the same result.
1241
+ * - A `locked` rectangle keeps its position. The width is not less than its right edge.
1242
+ * - Each strategy of `pack` works with this function, because it only changes the width.
811
1243
  */
812
- function toPackInput(file) {
1244
+ function packFree(rects, outer = new Rect(), options = {}) {
1245
+ const target = options.aspect ?? 1;
1246
+ const weight = options.aspectWeight ?? 1;
1247
+ const samples = Math.max(2, Math.floor(options.samples ?? 48));
1248
+ const ox = outer.position.x;
1249
+ const oy = outer.position.y;
1250
+ let itemArea = 0;
1251
+ let minWidth = 0;
1252
+ let rowWidth = 0;
1253
+ for (const r of rects) {
1254
+ if (r.locked) {
1255
+ minWidth = Math.max(minWidth, r.right() - ox);
1256
+ continue;
1257
+ }
1258
+ if (!(r.size.x > 0 && r.size.y > 0)) continue;
1259
+ itemArea += r.area();
1260
+ minWidth = Math.max(minWidth, r.size.x);
1261
+ rowWidth += r.size.x;
1262
+ }
1263
+ for (const r of rects) if (r.locked) itemArea += r.area();
1264
+ const maxWidth = Math.max(minWidth, rowWidth);
1265
+ const tried = [];
1266
+ const run = (width) => {
1267
+ Rect.resetPacked(rects);
1268
+ const box = new Rect();
1269
+ box.position.setTo(outer.position);
1270
+ box.size.set(width, 0);
1271
+ box.fixedWidth = true;
1272
+ pack(rects, box, options);
1273
+ const placed = Rect.packed(rects);
1274
+ const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);
1275
+ const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);
1276
+ const area = contentWidth * contentHeight;
1277
+ const t = {
1278
+ width,
1279
+ contentWidth,
1280
+ contentHeight,
1281
+ objective: area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0
1282
+ };
1283
+ tried.push(t);
1284
+ return t;
1285
+ };
1286
+ const widths = [];
1287
+ if (maxWidth > 0) {
1288
+ const ratio = maxWidth / minWidth;
1289
+ for (let i = 0; i < samples; i++) {
1290
+ const w = minWidth * ratio ** (i / (samples - 1));
1291
+ const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));
1292
+ if (widths.at(-1) !== rounded) widths.push(rounded);
1293
+ }
1294
+ }
1295
+ let best = void 0;
1296
+ let bestIndex = 0;
1297
+ for (const [i, w] of widths.entries()) {
1298
+ const t = run(w);
1299
+ if (best === void 0 || t.objective < best.objective) {
1300
+ best = t;
1301
+ bestIndex = i;
1302
+ }
1303
+ }
1304
+ if (widths.length > 2) {
1305
+ const low = widths[Math.max(0, bestIndex - 1)];
1306
+ const high = widths[Math.min(widths.length - 1, bestIndex + 1)];
1307
+ const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));
1308
+ for (let i = 1; i <= steps; i++) {
1309
+ const w = Math.round(low + (high - low) * i / (steps + 1));
1310
+ if (widths.includes(w)) continue;
1311
+ const t = run(w);
1312
+ if (best === void 0 || t.objective < best.objective) best = t;
1313
+ }
1314
+ }
1315
+ const chosen = best === void 0 ? void 0 : run(best.width);
1316
+ tried.pop();
1317
+ const w = chosen?.contentWidth ?? 0;
1318
+ const h = chosen?.contentHeight ?? 0;
1319
+ if (!outer.fixedSize) outer.size.set(w, h);
1320
+ const area = w * h;
813
1321
  return {
814
- container: file.container ?? { fixed: "none" },
815
- items: file.boxes,
816
- ...file.obstacles === void 0 ? {} : { obstacles: file.obstacles },
817
- ...file.gap === void 0 ? {} : { gap: file.gap }
1322
+ outer,
1323
+ area,
1324
+ fill: area > 0 ? itemArea / area : 0,
1325
+ aspect: h > 0 ? w / h : 0,
1326
+ aspectError: aspectError(w, h, target),
1327
+ objective: chosen?.objective ?? 0,
1328
+ tried
1329
+ };
1330
+ }
1331
+ // src/push.ts
1332
+ /** This function gives the overlap of two rectangles on each axis. A value of 0 or less is no overlap. */
1333
+ function overlapOf(a, b) {
1334
+ return {
1335
+ x: Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x),
1336
+ y: Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y)
818
1337
  };
819
1338
  }
820
- // src/measures.ts
821
- const spec = (id, family, unit, direction, min, max, guard) => ({
822
- id,
823
- family,
824
- unit,
825
- direction,
826
- min,
827
- max,
828
- ...guard
829
- });
830
1339
  /**
831
- * The facts of each measure, by id. `docs/MEASURES.md` gives the full definitions. This record is the source of the floor
832
- * and the tolerance of each guard of the packer.
1340
+ * This function gives the direction of a first shove. The axis has the smaller overlap. The sign points away from the
1341
+ * center of the pusher. When that shove crosses 0, the sign is positive.
833
1342
  */
834
- const MEASURE_SPECS = Object.freeze({
835
- "pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity),
836
- "pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1),
837
- "pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1),
838
- "pack.aspect": spec("pack.aspect", "aspect", "ratio", "lower", 1, Infinity),
839
- "pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity, {
840
- floor: 0,
841
- tol: .01
842
- }),
843
- "pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, {
844
- floor: 0,
845
- tol: 0
846
- }),
847
- "pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity),
848
- "pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1),
849
- "pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, {
850
- floor: 0,
851
- tol: 0
852
- }),
853
- "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, {
854
- floor: 0,
855
- tol: .02
856
- }),
857
- "pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, {
858
- floor: 0,
859
- tol: .005
860
- }),
861
- "pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, {
862
- floor: 0,
863
- tol: .01
864
- }),
865
- "pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1),
866
- "pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity),
867
- "pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity)
868
- });
869
- /** This function gives the smallest rectangle around the boxes. No box gives a rectangle of size 0 at 0, 0. */
870
- function hullOf(boxes) {
871
- if (boxes.length === 0) return {
872
- x: 0,
873
- y: 0,
874
- w: 0,
875
- h: 0
1343
+ function firstDirection(pusher, r, gap) {
1344
+ const overlap = overlapOf(pusher, r);
1345
+ if (overlap.x < overlap.y) return {
1346
+ axis: "x",
1347
+ sign: r.position.x + r.size.x / 2 < pusher.position.x + pusher.size.x / 2 && pusher.position.x - r.size.x - gap >= 0 ? -1 : 1
876
1348
  };
877
- let left = Infinity;
878
- let top = Infinity;
879
- let right = -Infinity;
880
- let bottom = -Infinity;
881
- for (const b of boxes) {
882
- left = Math.min(left, b.x);
883
- top = Math.min(top, b.y);
884
- right = Math.max(right, b.x + b.w);
885
- bottom = Math.max(bottom, b.y + b.h);
1349
+ return {
1350
+ axis: "y",
1351
+ sign: r.position.y + r.size.y / 2 < pusher.position.y + pusher.size.y / 2 && pusher.position.y - r.size.y - gap >= 0 ? -1 : 1
1352
+ };
1353
+ }
1354
+ /** This function puts a rectangle past the pusher, along a direction. The rectangle does not go past 0. */
1355
+ function shove(pusher, r, direction, gap) {
1356
+ if (direction.axis === "x") {
1357
+ const x = direction.sign > 0 ? pusher.right() + gap : pusher.position.x - r.size.x - gap;
1358
+ r.position.set(Math.max(0, x), r.position.y);
1359
+ return;
1360
+ }
1361
+ const y = direction.sign > 0 ? pusher.bottom() + gap : pusher.position.y - r.size.y - gap;
1362
+ r.position.set(r.position.x, Math.max(0, y));
1363
+ }
1364
+ /**
1365
+ * This function moves each rectangle that a placed rectangle overlaps, and the rectangles that those overlap in turn.
1366
+ * A shove takes the axis of the smaller overlap, away from the pusher. A shoved rectangle keeps the direction of its first
1367
+ * shove when it shoves the next one, so the cascade goes outward. The placed rectangle and each locked rectangle stay.
1368
+ * The function moves the rectangles in place and gives the rectangles that moved.
1369
+ *
1370
+ * @example
1371
+ * ```ts
1372
+ * const moved = pushCascade(dropped, cards, { gap: 8 });
1373
+ * ```
1374
+ */
1375
+ function pushCascade(placed, rects, options = {}) {
1376
+ const gap = options.gap ?? 8;
1377
+ const maxShoves = options.maxShoves ?? rects.length * 8;
1378
+ const moved = /* @__PURE__ */ new Set();
1379
+ const queue = [{ pusher: placed }];
1380
+ let shoves = 0;
1381
+ while (shoves < maxShoves) {
1382
+ const step = queue.shift();
1383
+ if (!step) break;
1384
+ const { pusher, direction } = step;
1385
+ for (const r of rects) {
1386
+ if (r === pusher || r === placed || r.locked) continue;
1387
+ const overlap = overlapOf(pusher, r);
1388
+ if (overlap.x <= 0 || overlap.y <= 0) continue;
1389
+ const next = direction ?? firstDirection(pusher, r, gap);
1390
+ shove(pusher, r, next, gap);
1391
+ moved.add(r);
1392
+ queue.push({
1393
+ pusher: r,
1394
+ direction: next
1395
+ });
1396
+ if (++shoves >= maxShoves) break;
1397
+ }
1398
+ }
1399
+ return [...moved];
1400
+ }
1401
+ // src/overflow.ts
1402
+ /** This function gives the area of a ring by the shoelace formula. The direction of the ring does not count. */
1403
+ function ringArea(ring) {
1404
+ let sum = 0;
1405
+ for (let i = 0; i < ring.length; i++) {
1406
+ const a = ring[i];
1407
+ const b = ring[(i + 1) % ring.length];
1408
+ sum += a[0] * b[1] - b[0] * a[1];
886
1409
  }
1410
+ return Math.abs(sum) / 2;
1411
+ }
1412
+ /**
1413
+ * This function gives the area of a region that an item cannot use. A polygon loses the area of its holes. Thus a hole
1414
+ * that is out of the polygon makes the area too small, and the free area stays an upper bound.
1415
+ */
1416
+ function blockedArea(region) {
1417
+ switch (region.kind) {
1418
+ case "rect": return Math.max(0, region.w) * Math.max(0, region.h);
1419
+ case "circle": return Math.PI * region.r ** 2;
1420
+ case "ellipse": return Math.PI * region.rx * region.ry;
1421
+ case "polygon": {
1422
+ let area = ringArea(region.points);
1423
+ for (const hole of region.holes ?? []) area -= ringArea(hole);
1424
+ return Math.max(0, area);
1425
+ }
1426
+ }
1427
+ }
1428
+ /** This function tells if the insides of two rectangles have a common point. Two rectangles with a common edge do not. */
1429
+ function boxesOverlap(a, b) {
1430
+ return Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > 0 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > 0;
1431
+ }
1432
+ /** This function tells if rectangle `a` is in rectangle `b`. */
1433
+ function boxIn(a, b) {
1434
+ return a.x >= b.x && a.y >= b.y && a.x + a.w <= b.x + b.w && a.y + a.h <= b.y + b.h;
1435
+ }
1436
+ /** This function gives the rectangle made smaller by `d` on each side. */
1437
+ function shrink(b, d) {
887
1438
  return {
888
- x: left,
889
- y: top,
890
- w: right - left,
891
- h: bottom - top
1439
+ x: b.x + d,
1440
+ y: b.y + d,
1441
+ w: Math.max(0, b.w - 2 * d),
1442
+ h: Math.max(0, b.h - 2 * d)
1443
+ };
1444
+ }
1445
+ /** This function tells if a point is in a ring, by the even-odd rule. */
1446
+ function pointInRing(x, y, ring) {
1447
+ let inside = false;
1448
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
1449
+ const [xi, yi] = ring[i];
1450
+ const [xj, yj] = ring[j];
1451
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
1452
+ }
1453
+ return inside;
1454
+ }
1455
+ /** This function tells if a segment has a part of a length above 0 in the open rectangle. The Liang-Barsky clip gives it. */
1456
+ function segmentHitsBox(ax, ay, bx, by, r) {
1457
+ let t0 = 0;
1458
+ let t1 = 1;
1459
+ const dx = bx - ax;
1460
+ const dy = by - ay;
1461
+ const clip = (p, q) => {
1462
+ if (p === 0) return q > 0;
1463
+ const t = q / p;
1464
+ if (p < 0) {
1465
+ if (t > t1) return false;
1466
+ if (t > t0) t0 = t;
1467
+ } else {
1468
+ if (t < t0) return false;
1469
+ if (t < t1) t1 = t;
1470
+ }
1471
+ return true;
892
1472
  };
1473
+ return clip(-dx, ax - r.x) && clip(dx, r.x + r.w - ax) && clip(-dy, ay - r.y) && clip(dy, r.y + r.h - ay) && t1 > t0;
893
1474
  }
894
1475
  /**
895
- * This function puts the boxes in rows. It sorts the boxes by their top, and keeps the order of the list for equal tops.
896
- * A box joins the last row when its top is within `tolerance` of the top of the first box of that row.
1476
+ * This function tells if a rectangle is in a polygon with holes. No edge of a ring crosses the inside of the rectangle.
1477
+ * No vertex is in it, and its center is in the polygon by the even-odd rule.
897
1478
  */
898
- function rowsOf$1(boxes, tolerance) {
899
- const rows = [];
900
- for (const b of boxes.toSorted((p, q) => p.y - q.y)) {
901
- const row = rows.at(-1);
902
- if (row && Math.abs(b.y - row.top) <= tolerance) {
903
- row.right = Math.max(row.right, b.x + b.w);
904
- row.count += 1;
905
- } else rows.push({
906
- top: b.y,
907
- right: b.x + b.w,
908
- count: 1
1479
+ function boxInPolygon(b, rings) {
1480
+ for (const ring of rings) for (let i = 0; i < ring.length; i++) {
1481
+ const p = ring[i];
1482
+ const q = ring[(i + 1) % ring.length];
1483
+ if (p[0] > b.x && p[0] < b.x + b.w && p[1] > b.y && p[1] < b.y + b.h) return false;
1484
+ if (segmentHitsBox(p[0], p[1], q[0], q[1], b)) return false;
1485
+ }
1486
+ const cx = b.x + b.w / 2;
1487
+ const cy = b.y + b.h / 2;
1488
+ let inside = false;
1489
+ for (const ring of rings) if (pointInRing(cx, cy, ring)) inside = !inside;
1490
+ return inside;
1491
+ }
1492
+ /** This function tells if a rectangle is in a region of a container. A rectangle with no size is in no region. */
1493
+ function boxInShape(b, shape) {
1494
+ if (!(b.w > 0 && b.h > 0)) return false;
1495
+ const s = shrink(b, 1e-9 * Math.max(1, b.w, b.h));
1496
+ switch (shape.kind) {
1497
+ case "rect": return boxIn(b, {
1498
+ x: shape.x,
1499
+ y: shape.y,
1500
+ w: shape.w,
1501
+ h: shape.h
909
1502
  });
1503
+ case "circle":
1504
+ case "ellipse": {
1505
+ const rx = shape.kind === "circle" ? shape.r : shape.rx;
1506
+ const ry = shape.kind === "circle" ? shape.r : shape.ry;
1507
+ const inside = (x, y) => ((x - shape.cx) / rx) ** 2 + ((y - shape.cy) / ry) ** 2 <= 1;
1508
+ return inside(s.x, s.y) && inside(s.x + s.w, s.y) && inside(s.x, s.y + s.h) && inside(s.x + s.w, s.y + s.h);
1509
+ }
1510
+ case "polygon": return boxInPolygon(s, [shape.points, ...shape.holes ?? []]);
910
1511
  }
911
- return rows;
912
1512
  }
913
1513
  /**
914
- * This function gives the placed boxes of an output, in the order of the items of the input. A box has the size of its
915
- * item after the turn. The list has no item without a placement, and no item with a width or a height of 0.
1514
+ * This function gives the area of a container shape. A hole counts only when its bounding box is in the outer ring.
1515
+ * Also, that box has no common point with the box of a hole before it. Thus the area is not too small.
916
1516
  */
917
- function boxesOf$1(input, output) {
918
- const placements = /* @__PURE__ */ new Map();
919
- for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
920
- const boxes = [];
921
- for (const [index, item] of input.items.entries()) {
922
- const p = placements.get(item.id);
923
- if (p === void 0 || !(item.w > 0 && item.h > 0)) continue;
924
- const turned = p.turn === 90;
925
- boxes.push({
926
- x: p.x,
927
- y: p.y,
928
- w: turned ? item.h : item.w,
929
- h: turned ? item.w : item.h,
930
- rank: item.rank ?? index
1517
+ function shapeArea(shape) {
1518
+ if (shape.kind !== "polygon") return blockedArea(shape);
1519
+ let area = ringArea(shape.points);
1520
+ const kept = [];
1521
+ for (const hole of shape.holes ?? []) {
1522
+ const box = boundsOf({
1523
+ kind: "polygon",
1524
+ points: hole
931
1525
  });
1526
+ if (!boxInPolygon(shrink(box, 1e-9 * Math.max(1, box.w, box.h)), [shape.points])) continue;
1527
+ if (kept.some((k) => boxesOverlap(k, box))) continue;
1528
+ kept.push(box);
1529
+ area -= ringArea(hole);
932
1530
  }
933
- return boxes;
1531
+ return Math.max(0, area);
934
1532
  }
935
1533
  /**
936
- * This function tells if box `a` comes before box `b` in the reading order. Two boxes whose heights overlap by more than
937
- * `RECORD_EPS` are on one line, and the left box comes first. Otherwise the upper box comes first.
1534
+ * This function gives the area of the union of rectangles, each cut to the rectangle `clip`. A sweep over the x edges
1535
+ * adds the merged y intervals of each slab.
938
1536
  */
939
- function readsBefore(a, b) {
940
- if (Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > RECORD_EPS) return a.x + RECORD_EPS < b.x;
941
- return a.y < b.y;
1537
+ function unionArea(boxes, clip) {
1538
+ const cut = [];
1539
+ for (const b of boxes) {
1540
+ const x0 = Math.max(b.x, clip.x);
1541
+ const y0 = Math.max(b.y, clip.y);
1542
+ const x1 = Math.min(b.x + b.w, clip.x + clip.w);
1543
+ const y1 = Math.min(b.y + b.h, clip.y + clip.h);
1544
+ if (x1 > x0 && y1 > y0) cut.push({
1545
+ x: x0,
1546
+ y: y0,
1547
+ w: x1 - x0,
1548
+ h: y1 - y0
1549
+ });
1550
+ }
1551
+ const xs = [...new Set(cut.flatMap((b) => [b.x, b.x + b.w]))].toSorted((a, b) => a - b);
1552
+ let area = 0;
1553
+ for (let i = 0; i + 1 < xs.length; i++) {
1554
+ const left = xs[i];
1555
+ const right = xs[i + 1];
1556
+ const spans = cut.filter((b) => b.x <= left && b.x + b.w >= right).map((b) => [b.y, b.y + b.h]).toSorted((p, q) => p[0] - q[0]);
1557
+ let covered = 0;
1558
+ let top = -Infinity;
1559
+ let bottom = -Infinity;
1560
+ for (const [s, e] of spans) if (s > bottom) {
1561
+ covered += bottom - top > 0 ? bottom - top : 0;
1562
+ top = s;
1563
+ bottom = e;
1564
+ } else bottom = Math.max(bottom, e);
1565
+ covered += bottom - top > 0 ? bottom - top : 0;
1566
+ area += covered * (right - left);
1567
+ }
1568
+ return area;
1569
+ }
1570
+ /** This function gives the rectangle of the container: the bounding box of its shape, or 0, 0, `w`, `h`. */
1571
+ function containerBox(input) {
1572
+ const { container } = input;
1573
+ if (container.shape !== void 0) return boundsOf(container.shape);
1574
+ return {
1575
+ x: 0,
1576
+ y: 0,
1577
+ w: container.w ?? Infinity,
1578
+ h: container.h ?? Infinity
1579
+ };
942
1580
  }
943
- /** This function sums one measure over the stages of a trace. A trace without the measure gives 0. */
944
- function traceSum(output, id) {
945
- let sum = 0;
946
- for (const stage of output.trace ?? []) sum += stage.measures?.[id] ?? 0;
947
- return sum;
1581
+ /** This function tells if an item is locked at a position. Such an item is not a candidate of the overflow. */
1582
+ const lockedAt = (item) => item.locked === true && item.x !== void 0 && item.y !== void 0;
1583
+ /** This function gives the rectangle of an item at its position: the bounding box of its shape, or its size. */
1584
+ function itemBox(item) {
1585
+ const s = item.shape === void 0 ? {
1586
+ x: 0,
1587
+ y: 0,
1588
+ w: item.w,
1589
+ h: item.h
1590
+ } : boundsOf(item.shape);
1591
+ return {
1592
+ x: (item.x ?? 0) + s.x,
1593
+ y: (item.y ?? 0) + s.y,
1594
+ w: s.w,
1595
+ h: s.h
1596
+ };
1597
+ }
1598
+ /** This function gives the smallest area of an item: its rectangle, or the area of its shape when that is smaller. */
1599
+ function itemArea(item) {
1600
+ const rect = Math.max(0, item.w) * Math.max(0, item.h);
1601
+ return item.shape === void 0 ? rect : Math.min(rect, blockedArea(item.shape));
948
1602
  }
949
1603
  /**
950
- * This function gives the measures of one layout, by the ids of `docs/MEASURES.md`. The values are not rounded. A
951
- * record rounds them with `roundMeasures`.
952
- *
953
- * - The boxes are the placed items with a width and a height above 0. The hull is the smallest rectangle around them.
954
- * - An empty layout gives the area 0, the aspect 1 and the aspect error 0. Its fill is 0 and its whitespace is 1.
955
- * - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
1604
+ * This function gives an upper bound of the free area of a container with a fixed size. The bound ignores the gap and
1605
+ * the margins, thus it is not too small.
956
1606
  *
957
- * @example
958
- * ```ts
959
- * const m = measure(input, packWith("shelf", input));
960
- * m["pack.fill"]; // the area of the boxes divided by the area of the hull
961
- * ```
1607
+ * - The container area is the inner rectangle (the container less the inset). A shape gives the smaller of that and its exact area.
1608
+ * - A rectangle container loses the exact union of the fixed rectangle obstacles and the locked items in the inner rectangle.
1609
+ * - A shape loses only the regions that are fully in it and in the inner rectangle, with no common point between their boxes.
1610
+ * - An obstacle with `move` can go out of the way. Thus the bound does not subtract it.
962
1611
  */
963
- function measure(input, output, options = {}) {
964
- const boxes = boxesOf$1(input, output);
965
- const hull = hullOf(boxes);
966
- const hullArea = hull.w * hull.h;
967
- const boxArea = boxes.reduce((sum, b) => sum + b.w * b.h, 0);
968
- const fill = hullArea > 0 ? boxArea / hullArea : 0;
969
- const empty = boxes.length === 0;
970
- const target = input.container.aspect ?? 1;
971
- let pairs = 0;
972
- let inversions = 0;
973
- for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
974
- const a = boxes[i];
975
- const b = boxes[j];
976
- if (a.rank === b.rank) continue;
977
- pairs += 1;
978
- const first = a.rank < b.rank ? a : b;
979
- if (readsBefore(first === a ? b : a, first)) inversions += 1;
1612
+ function freeAreaBound(input) {
1613
+ const inner = shrink(containerBox(input), input.container.inset ?? 0);
1614
+ if (!(inner.w > 0 && inner.h > 0)) return 0;
1615
+ const blocked = [];
1616
+ for (const o of input.obstacles ?? []) if (o.move === void 0) blocked.push({
1617
+ region: o.region,
1618
+ box: boundsOf(o.region)
1619
+ });
1620
+ for (const item of input.items) {
1621
+ if (!lockedAt(item)) continue;
1622
+ const box = itemBox(item);
1623
+ blocked.push({
1624
+ region: {
1625
+ kind: "rect",
1626
+ ...box
1627
+ },
1628
+ box
1629
+ });
980
1630
  }
981
- const byRank = boxes.toSorted((a, b) => a.rank - b.rank);
982
- let breaks = 0;
983
- for (let k = 1; k < byRank.length; k++) {
984
- const prev = byRank[k - 1];
985
- const next = byRank[k];
986
- if (prev.rank !== next.rank && readsBefore(next, prev)) breaks += 1;
1631
+ const shape = input.container.shape;
1632
+ const innerArea = inner.w * inner.h;
1633
+ const inInner = (b) => b.w > 0 && b.h > 0 && boxIn(b, inner);
1634
+ if (shape === void 0 || shape.kind === "rect") {
1635
+ const rects = blocked.filter((b) => b.region.kind === "rect");
1636
+ let free = innerArea - unionArea(rects.map((b) => b.box), inner);
1637
+ const kept = [];
1638
+ for (const b of blocked) {
1639
+ if (b.region.kind === "rect" || !inInner(b.box)) continue;
1640
+ if (rects.some((r) => boxesOverlap(r.box, b.box)) || kept.some((k) => boxesOverlap(k, b.box))) continue;
1641
+ kept.push(b.box);
1642
+ free -= blockedArea(b.region);
1643
+ }
1644
+ return Math.max(0, free);
987
1645
  }
988
- const rights = rowsOf$1(boxes, options.rowTolerance ?? quantileOrZero(boxes.map((b) => b.h), .5) / 2).map((r) => r.right);
989
- const spread = rights.length > 0 ? Math.max(...rights) - Math.min(...rights) : 0;
990
- return {
991
- "pack.area": hullArea,
992
- "pack.fill": fill,
993
- "pack.whitespace": 1 - fill,
994
- "pack.aspect": empty ? 1 : Math.max(hull.w / hull.h, hull.h / hull.w),
995
- "pack.aspectError": empty ? 0 : aspectError(hull.w, hull.h, target),
996
- "pack.order.inversions": pairs > 0 ? inversions / pairs : 0,
997
- "pack.order.breaks": breaks,
998
- "pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
999
- "pack.overflow": new Set(output.overflow).size,
1000
- "pack.search.widths": traceSum(output, "pack.search.widths"),
1001
- "pack.search.ms": traceSum(output, "pack.search.ms")
1002
- };
1646
+ let free = Math.min(innerArea, shapeArea(shape));
1647
+ const kept = [];
1648
+ for (const b of blocked) {
1649
+ if (!inInner(b.box) || !boxInShape(b.box, shape) || kept.some((k) => boxesOverlap(k, b.box))) continue;
1650
+ kept.push(b.box);
1651
+ free -= blockedArea(b.region);
1652
+ }
1653
+ return Math.max(0, free);
1003
1654
  }
1004
1655
  /**
1005
- * This function gives the stability measures of a change pair: the run on the base input and the run on the changed
1006
- * input. The items match by id. The values are not rounded.
1656
+ * This function gives a lower bound of the overflow of an input: the items that no packer can place, and their area.
1007
1657
  *
1008
- * - An item stays when both inputs have it and both outputs place it. The added and the removed items do not count.
1009
- * - A staying item moved when its `x` or its `y` changed by more than `RECORD_EPS`, or when its turn changed.
1010
- * - The distance of an item is the length of the move of its top-left corner. The unit is the width `W`.
1011
- * - `W` is the larger width of the 2 output sizes. A `W` of 0 becomes 1, so the unit is then the unit of the input.
1012
- * - No staying item gives 0 for each measure.
1658
+ * - The candidates are the items that are not locked at a position.
1659
+ * - A fixed side forces each candidate that is longer than the side less the inset, in each turn that the item permits.
1660
+ * - A fixed size also forces the fewest other candidates that make the rest fit the free area of `freeAreaBound`. These are the largest ones.
1661
+ * - The area of the bound is the area of the long items, plus the part of the area of the rest above the free area.
1662
+ * - The margins and the gap do not count. A slack of the overlap tolerance along the edge of each candidate keeps the bound valid.
1663
+ * - A container with no fixed side gives 0.
1013
1664
  */
1014
- function measureChange(baseInput, baseOutput, changeInput, changeOutput) {
1015
- const placed = (input, output) => {
1016
- const ids = new Set(input.items.map((i) => i.id));
1017
- const map = /* @__PURE__ */ new Map();
1018
- for (const p of output.placements) if (ids.has(p.id) && !map.has(p.id)) map.set(p.id, p);
1019
- return map;
1665
+ function overflowBound(input) {
1666
+ const { container } = input;
1667
+ const fixedW = container.fixed === "size" || container.fixed === "width";
1668
+ const fixedH = container.fixed === "size" || container.fixed === "height";
1669
+ if (!fixedW && !fixedH) return {
1670
+ forced: 0,
1671
+ area: 0,
1672
+ free: Infinity
1020
1673
  };
1021
- const before = placed(baseInput, baseOutput);
1022
- const after = placed(changeInput, changeOutput);
1023
- const width = Math.max(baseOutput.size.w, changeOutput.size.w);
1024
- const unit = width > 0 ? width : 1;
1025
- const distances = [];
1026
- let moved = 0;
1027
- for (const [id, p] of before) {
1028
- const q = after.get(id);
1029
- if (q === void 0) continue;
1030
- const dx = q.x - p.x;
1031
- const dy = q.y - p.y;
1032
- if (Math.abs(dx) > RECORD_EPS || Math.abs(dy) > RECORD_EPS || (p.turn ?? 0) !== (q.turn ?? 0)) moved += 1;
1033
- distances.push(Math.hypot(dx, dy) / unit);
1674
+ const tol = DEFAULT_TOLERANCE.overlap;
1675
+ const bounds = containerBox(input);
1676
+ const inset = container.inset ?? 0;
1677
+ const maxW = fixedW ? bounds.w - 2 * inset + 2 * tol : Infinity;
1678
+ const maxH = fixedH ? bounds.h - 2 * inset + 2 * tol : Infinity;
1679
+ let forced = 0;
1680
+ let area = 0;
1681
+ let slack = 0;
1682
+ const rest = [];
1683
+ for (const item of input.items) {
1684
+ if (lockedAt(item)) continue;
1685
+ const box = itemBox(item);
1686
+ const w = Math.min(item.w, box.w);
1687
+ const h = Math.min(item.h, box.h);
1688
+ const fits = w <= maxW && h <= maxH || item.turnable === true && h <= maxW && w <= maxH;
1689
+ const a = itemArea(item);
1690
+ if (!fits) {
1691
+ forced += 1;
1692
+ area += a;
1693
+ continue;
1694
+ }
1695
+ rest.push(a);
1696
+ slack += 2 * tol * (Math.max(0, w) + Math.max(0, h));
1034
1697
  }
1035
- if (distances.length === 0) return {
1036
- "pack.stability.moved": 0,
1037
- "pack.stability.mean": 0,
1038
- "pack.stability.q95": 0
1698
+ if (!(fixedW && fixedH)) return {
1699
+ forced,
1700
+ area,
1701
+ free: Infinity
1039
1702
  };
1703
+ const free = freeAreaBound(input);
1704
+ const room = free + slack + 2 * tol * (Math.max(0, bounds.w) + Math.max(0, bounds.h));
1705
+ let sum = rest.reduce((s, a) => s + a, 0);
1706
+ area += Math.max(0, sum - room);
1707
+ for (const a of rest.toSorted((p, q) => q - p)) {
1708
+ if (sum <= room || !(a > 0)) break;
1709
+ sum -= a;
1710
+ forced += 1;
1711
+ }
1040
1712
  return {
1041
- "pack.stability.moved": moved / distances.length,
1042
- "pack.stability.mean": distances.reduce((sum, d) => sum + d, 0) / distances.length,
1043
- "pack.stability.q95": quantile(distances, .95)
1713
+ forced,
1714
+ area,
1715
+ free
1044
1716
  };
1045
1717
  }
1718
+ // src/sha256.ts
1719
+ /** The round constants of SHA-256. */
1720
+ const K = new Uint32Array([
1721
+ 1116352408,
1722
+ 1899447441,
1723
+ 3049323471,
1724
+ 3921009573,
1725
+ 961987163,
1726
+ 1508970993,
1727
+ 2453635748,
1728
+ 2870763221,
1729
+ 3624381080,
1730
+ 310598401,
1731
+ 607225278,
1732
+ 1426881987,
1733
+ 1925078388,
1734
+ 2162078206,
1735
+ 2614888103,
1736
+ 3248222580,
1737
+ 3835390401,
1738
+ 4022224774,
1739
+ 264347078,
1740
+ 604807628,
1741
+ 770255983,
1742
+ 1249150122,
1743
+ 1555081692,
1744
+ 1996064986,
1745
+ 2554220882,
1746
+ 2821834349,
1747
+ 2952996808,
1748
+ 3210313671,
1749
+ 3336571891,
1750
+ 3584528711,
1751
+ 113926993,
1752
+ 338241895,
1753
+ 666307205,
1754
+ 773529912,
1755
+ 1294757372,
1756
+ 1396182291,
1757
+ 1695183700,
1758
+ 1986661051,
1759
+ 2177026350,
1760
+ 2456956037,
1761
+ 2730485921,
1762
+ 2820302411,
1763
+ 3259730800,
1764
+ 3345764771,
1765
+ 3516065817,
1766
+ 3600352804,
1767
+ 4094571909,
1768
+ 275423344,
1769
+ 430227734,
1770
+ 506948616,
1771
+ 659060556,
1772
+ 883997877,
1773
+ 958139571,
1774
+ 1322822218,
1775
+ 1537002063,
1776
+ 1747873779,
1777
+ 1955562222,
1778
+ 2024104815,
1779
+ 2227730452,
1780
+ 2361852424,
1781
+ 2428436474,
1782
+ 2756734187,
1783
+ 3204031479,
1784
+ 3329325298
1785
+ ]);
1786
+ /** The first hash value of SHA-256. */
1787
+ const H0 = [
1788
+ 1779033703,
1789
+ 3144134277,
1790
+ 1013904242,
1791
+ 2773480762,
1792
+ 1359893119,
1793
+ 2600822924,
1794
+ 528734635,
1795
+ 1541459225
1796
+ ];
1797
+ /** This function turns a 32-bit word to the right by `n` bits. */
1798
+ const rotr = (x, n) => x >>> n | x << 32 - n;
1046
1799
  /**
1047
- * This function gives the measure `pack.deterministic` of two runs with the same input, options and seed. It gives
1048
- * `true` when the runs have the same placements, overflow, size and moved obstacles.
1049
- *
1050
- * - The placements match by id, so their order in the list does not count. The numbers must be equal.
1051
- * - The trace is not part of the rule, because its times change each time.
1800
+ * This function gives the UTF-8 bytes of a string. A lone surrogate becomes U+FFFD, as with `TextEncoder`. The
1801
+ * package has no `TextEncoder` type, because its code operates on each runtime with no global types.
1052
1802
  */
1053
- function isDeterministic(first, second) {
1054
- const key = (list) => new Map(list.map((e) => [e.id, e]));
1055
- const samePlacements = (a, b) => {
1056
- if (a.length !== b.length) return false;
1057
- const other = key(b);
1058
- return a.every((p) => {
1059
- const q = other.get(p.id);
1060
- return q !== void 0 && q.x === p.x && q.y === p.y && (q.turn ?? 0) === (p.turn ?? 0);
1061
- });
1062
- };
1063
- const sameMoved = (a, b) => {
1064
- if (a.length !== b.length) return false;
1065
- const other = key(b);
1066
- return a.every((m) => {
1067
- const n = other.get(m.id);
1068
- return n !== void 0 && n.x === m.x && n.y === m.y;
1069
- });
1070
- };
1071
- const overflowA = new Set(first.overflow);
1072
- const overflowB = new Set(second.overflow);
1073
- return first.size.w === second.size.w && first.size.h === second.size.h && overflowA.size === overflowB.size && [...overflowA].every((id) => overflowB.has(id)) && samePlacements(first.placements, second.placements) && sameMoved(first.moved, second.moved);
1803
+ function utf8(text) {
1804
+ const out = [];
1805
+ for (let i = 0; i < text.length; i++) {
1806
+ let c = text.charCodeAt(i);
1807
+ if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
1808
+ const next = text.charCodeAt(i + 1);
1809
+ if (next >= 56320 && next <= 57343) {
1810
+ c = 65536 + (c - 55296 << 10) + (next - 56320);
1811
+ i++;
1812
+ }
1813
+ }
1814
+ if (c >= 55296 && c <= 57343) c = 65533;
1815
+ if (c < 128) out.push(c);
1816
+ else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
1817
+ else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
1818
+ else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
1819
+ }
1820
+ return Uint8Array.from(out);
1074
1821
  }
1075
- /**
1076
- * This function rounds each number of a record of measures with `round3`, as `docs/FORMAT.md` tells. A half goes up,
1077
- * and `-0` becomes 0. The function keeps the other values, and it does not change its argument.
1078
- */
1079
- function roundMeasures(measures) {
1080
- const out = {};
1081
- for (const [id, value] of Object.entries(measures)) out[id] = typeof value === "number" ? round3(value) : value;
1082
- return out;
1822
+ /** This function gives the SHA-256 of the UTF-8 bytes of a string, as 64 lowercase hex characters. */
1823
+ function sha256Hex(text) {
1824
+ const bytes = utf8(text);
1825
+ const length = bytes.length;
1826
+ const padded = new Uint8Array(((length + 9 + 63) / 64 | 0) * 64);
1827
+ padded.set(bytes);
1828
+ padded[length] = 128;
1829
+ const view = new DataView(padded.buffer);
1830
+ const bits = length * 8;
1831
+ view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
1832
+ view.setUint32(padded.length - 4, bits >>> 0);
1833
+ const hash = [...H0];
1834
+ const w = /* @__PURE__ */ new Uint32Array(64);
1835
+ for (let block = 0; block < padded.length; block += 64) {
1836
+ for (let t = 0; t < 16; t++) w[t] = view.getUint32(block + t * 4);
1837
+ for (let t = 16; t < 64; t++) {
1838
+ const a = w[t - 15];
1839
+ const b = w[t - 2];
1840
+ const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
1841
+ const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
1842
+ w[t] = w[t - 16] + s0 + w[t - 7] + s1 | 0;
1843
+ }
1844
+ let [a, b, c, d, e, f, g, h] = hash;
1845
+ for (let t = 0; t < 64; t++) {
1846
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1847
+ const ch = e & f ^ ~e & g;
1848
+ const t1 = h + s1 + ch + K[t] + w[t] | 0;
1849
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
1850
+ h = g;
1851
+ g = f;
1852
+ f = e;
1853
+ e = d + t1 | 0;
1854
+ d = c;
1855
+ c = b;
1856
+ b = a;
1857
+ a = t1 + t2 | 0;
1858
+ }
1859
+ hash[0] = hash[0] + a | 0;
1860
+ hash[1] = hash[1] + b | 0;
1861
+ hash[2] = hash[2] + c | 0;
1862
+ hash[3] = hash[3] + d | 0;
1863
+ hash[4] = hash[4] + e | 0;
1864
+ hash[5] = hash[5] + f | 0;
1865
+ hash[6] = hash[6] + g | 0;
1866
+ hash[7] = hash[7] + h | 0;
1867
+ }
1868
+ return hash.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
1083
1869
  }
1084
- /**
1085
- * This function gives the paired ratio of a measure on one input: the value of the candidate against the value of the
1086
- * current default. A ratio above 1 is worse, in the direction of the measure.
1087
- *
1088
- * - For `lower`, the ratio is `candidate / current`. For `higher`, it is `current / candidate`.
1089
- * - When both values are 0, the ratio is 1. When only the divisor is 0, the ratio is `Infinity`.
1090
- * - The values are 0 or more. A negative value or `NaN` throws a `RangeError`.
1091
- */
1092
- function pairedRatio(candidate, current, direction) {
1093
- if (!(candidate >= 0 && current >= 0)) throw new RangeError("A paired ratio needs two values of 0 or more.");
1094
- const top = direction === "lower" ? candidate : current;
1095
- const bottom = direction === "lower" ? current : candidate;
1096
- if (bottom === 0) return top === 0 ? 1 : Infinity;
1097
- return top / bottom;
1870
+ // src/layout-input.ts
1871
+ /** This function gives the `fnv1a32` hash of the UTF-8 bytes of a string, as `docs/FORMAT.md` gives it. */
1872
+ function fnv1a32(text) {
1873
+ let h = 2166136261;
1874
+ for (const b of utf8(text)) h = Math.imul(h ^ b, 16777619) >>> 0;
1875
+ return h;
1876
+ }
1877
+ /** This function rounds the numbers of a file to 3 decimals. A half goes up, and the value `-0` becomes 0. */
1878
+ function round3(x) {
1879
+ const v = Math.round(x * 1e3) / 1e3;
1880
+ return Object.is(v, -0) ? 0 : v;
1098
1881
  }
1099
1882
  /**
1100
- * This function gives the quantile `q` of a list of numbers, with linear interpolation between the sorted values. The
1101
- * position in the sorted list is `(n - 1) * q`. An empty list or a `q` outside 0 to 1 throws a `RangeError`.
1883
+ * This function gives the canonical form of a value: sorted keys, no whitespace and the numbers of `JSON.stringify`
1884
+ * after `round3`. A key with the value `undefined` is not in the form.
1102
1885
  */
1103
- function quantile(values, q) {
1104
- if (values.length === 0) throw new RangeError("A quantile needs one value or more.");
1105
- if (!(q >= 0 && q <= 1)) throw new RangeError("A quantile needs a q from 0 to 1.");
1106
- const sorted = values.toSorted((a, b) => a - b);
1107
- const at = (sorted.length - 1) * q;
1108
- const low = Math.floor(at);
1109
- const part = at - low;
1110
- const a = sorted[low];
1111
- if (part === 0) return a;
1112
- const b = sorted[low + 1];
1113
- if (a === b) return a;
1114
- return a + part * (b - a);
1886
+ function canonicalJson(value) {
1887
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
1888
+ if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
1889
+ return JSON.stringify(typeof value === "number" ? round3(value) : value);
1115
1890
  }
1116
- /** This function gives the quantile `q` of a list, and 0 for an empty list. */
1117
- function quantileOrZero(values, q) {
1118
- return values.length === 0 ? 0 : quantile(values, q);
1891
+ /** This function gives the hash of an input file: the first 16 hex characters of the SHA-256 of its canonical form without `hash`. */
1892
+ function hashInput(file) {
1893
+ const { hash: _hash, ...rest } = file;
1894
+ return sha256Hex(canonicalJson(rest)).slice(0, 16);
1119
1895
  }
1120
- /** This function gives the median of the paired ratios of a corpus: the quantile 0.5. It is not a ratio of two medians. */
1121
- function medianRatio(ratios) {
1122
- return quantile(ratios, .5);
1896
+ /** This function gives the scale of a count: the nearest of 1, 10, 100, 1000 and 10000 on a log scale. The count 0 gives `1`. */
1897
+ function countCell(count) {
1898
+ if (count <= 3) return "1";
1899
+ if (count <= 31) return "10";
1900
+ if (count <= 316) return "100";
1901
+ if (count <= 3162) return "1k";
1902
+ return "10k";
1123
1903
  }
1124
- /** This function gives the share of the paired ratios above a limit. An empty list throws a `RangeError`. */
1125
- function tailShare(ratios, limit) {
1126
- if (ratios.length === 0) throw new RangeError("A tail share needs one ratio or more.");
1127
- return ratios.filter((r) => r > limit).length / ratios.length;
1904
+ /**
1905
+ * This function gives the packing input of a file. The boxes become the items, and the edges go.
1906
+ * A file without a container gets `{ fixed: "none" }`, so the packer picks the size. It is `toPackInput` of the contract.
1907
+ */
1908
+ function toPackInput(file) {
1909
+ return {
1910
+ container: file.container ?? { fixed: "none" },
1911
+ items: file.boxes,
1912
+ ...file.obstacles === void 0 ? {} : { obstacles: file.obstacles },
1913
+ ...file.gap === void 0 ? {} : { gap: file.gap }
1914
+ };
1128
1915
  }
1129
- // src/metrics.ts
1916
+ // src/measures.ts
1917
+ const spec = (id, family, unit, direction, min, max, minEffect, guard) => ({
1918
+ id,
1919
+ family,
1920
+ unit,
1921
+ direction,
1922
+ min,
1923
+ max,
1924
+ ...minEffect === void 0 ? {} : { minEffect },
1925
+ ...guard
1926
+ });
1927
+ /** The minimum effect of an area or a ratio of lengths: 1% of the larger value. */
1928
+ const REL = Object.freeze({ rel: .01 });
1130
1929
  /**
1131
- * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.
1132
- * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.
1930
+ * The facts of each measure, by id. `docs/MEASURES.md` gives the full definitions. This record is the source of the floor
1931
+ * and the tolerance of each guard of the packer, and of the minimum effect of each measure.
1133
1932
  *
1134
- * @example
1135
- * ```ts
1136
- * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
1137
- * ```
1933
+ * - A count and a share of counts have the minimum effect 0. Thus each change of a count is an effect.
1934
+ * - A share of an area from 0 to 1 has 0.005. A guard has its tolerance.
1935
+ * - An area and a ratio of lengths have `{ rel: 0.01 }`: 1% of the larger value.
1936
+ * - The time has no minimum effect, because the times of CI change each time.
1138
1937
  */
1139
- function packingMetrics(rects, options = {}) {
1140
- const tolerance = options.rowTolerance ?? 40;
1141
- const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0).map((r) => ({
1142
- x: r.position.x,
1143
- y: r.position.y,
1144
- w: r.size.x,
1145
- h: r.size.y
1146
- }));
1147
- if (boxes.length === 0) return {
1148
- count: 0,
1149
- hull: {
1150
- width: 0,
1151
- height: 0
1152
- },
1153
- hullAspect: 0,
1154
- rows: [],
1155
- raggedness: 0,
1156
- utilization: 0,
1157
- widestOverMedian: 0
1158
- };
1159
- const box = hullOf(boxes);
1160
- const hull = {
1161
- width: box.w,
1162
- height: box.h
1938
+ const MEASURE_SPECS = Object.freeze({
1939
+ "pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity, REL),
1940
+ "pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1, .005),
1941
+ "pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1, .005),
1942
+ "pack.aspect": spec("pack.aspect", "aspect", "ratio", "lower", 1, Infinity, REL),
1943
+ "pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity, .01, {
1944
+ floor: 0,
1945
+ tol: .01
1946
+ }),
1947
+ "pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, 0, {
1948
+ floor: 0,
1949
+ tol: 0
1950
+ }),
1951
+ "pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity, 0),
1952
+ "pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1, .005),
1953
+ "pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, 0, {
1954
+ floor: 0,
1955
+ tol: 0
1956
+ }),
1957
+ "pack.overflow.rate": spec("pack.overflow.rate", "overflow", "share", "lower", 0, 1, 0),
1958
+ "pack.overflow.area": spec("pack.overflow.area", "overflow", "share", "lower", 0, 1, .005),
1959
+ "pack.overflow.forced": spec("pack.overflow.forced", "overflow", "count", "lower", 0, Infinity, 0),
1960
+ "pack.overflow.forcedArea": spec("pack.overflow.forcedArea", "overflow", "share", "lower", 0, 1, .005),
1961
+ "pack.overflow.excess": spec("pack.overflow.excess", "overflow", "count", "lower", 0, Infinity, 0),
1962
+ "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, .02, {
1963
+ floor: 0,
1964
+ tol: .02
1965
+ }),
1966
+ "pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, .005, {
1967
+ floor: 0,
1968
+ tol: .005
1969
+ }),
1970
+ "pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, .01, {
1971
+ floor: 0,
1972
+ tol: .01
1973
+ }),
1974
+ "pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1, 0),
1975
+ "pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity, 0),
1976
+ "pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity, void 0)
1977
+ });
1978
+ /** This function gives the smallest rectangle around the boxes. No box gives a rectangle of size 0 at 0, 0. */
1979
+ function hullOf(boxes) {
1980
+ if (boxes.length === 0) return {
1981
+ x: 0,
1982
+ y: 0,
1983
+ w: 0,
1984
+ h: 0
1163
1985
  };
1164
- const rows = rowsOf$1(boxes, tolerance);
1165
- const rights = rows.map((row) => row.right);
1166
- const widths = boxes.map((r) => r.w).toSorted((a, b) => a - b);
1167
- const median = widths[Math.floor(widths.length / 2)] ?? 0;
1168
- const widest = widths.at(-1) ?? 0;
1169
- const area = boxes.reduce((sum, r) => sum + r.w * r.h, 0);
1986
+ let left = Infinity;
1987
+ let top = Infinity;
1988
+ let right = -Infinity;
1989
+ let bottom = -Infinity;
1990
+ for (const b of boxes) {
1991
+ left = Math.min(left, b.x);
1992
+ top = Math.min(top, b.y);
1993
+ right = Math.max(right, b.x + b.w);
1994
+ bottom = Math.max(bottom, b.y + b.h);
1995
+ }
1170
1996
  return {
1171
- count: boxes.length,
1172
- hull,
1173
- hullAspect: hull.height > 0 ? hull.width / hull.height : 0,
1174
- rows: rows.map((row) => row.count),
1175
- raggedness: Math.max(...rights) - Math.min(...rights),
1176
- utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,
1177
- widestOverMedian: median > 0 ? widest / median : 0
1997
+ x: left,
1998
+ y: top,
1999
+ w: right - left,
2000
+ h: bottom - top
1178
2001
  };
1179
2002
  }
1180
- // src/strategy.ts
1181
2003
  /**
1182
- * The error of a strategy for an input outside its cells. The benchmark records this result. It is not a failure of the
1183
- * strategy.
2004
+ * This function puts the boxes in rows. It sorts the boxes by their top, and keeps the order of the list for equal tops.
2005
+ * A box joins the last row when its top is within `tolerance` of the top of the first box of that row.
1184
2006
  */
1185
- var UnsupportedCell = class extends Error {
1186
- /** The dimension of the taxonomy. */
1187
- dimension;
1188
- /** The value of the input that the strategy does not support. */
1189
- value;
1190
- constructor(dimension, value) {
1191
- super(`The strategy does not support the value "${value}" of the dimension "${dimension}".`);
1192
- this.name = "UnsupportedCell";
1193
- this.dimension = dimension;
1194
- this.value = value;
2007
+ function rowsOf$1(boxes, tolerance) {
2008
+ const rows = [];
2009
+ for (const b of boxes.toSorted((p, q) => p.y - q.y)) {
2010
+ const row = rows.at(-1);
2011
+ if (row && Math.abs(b.y - row.top) <= tolerance) {
2012
+ row.right = Math.max(row.right, b.x + b.w);
2013
+ row.count += 1;
2014
+ } else rows.push({
2015
+ top: b.y,
2016
+ right: b.x + b.w,
2017
+ count: 1
2018
+ });
1195
2019
  }
1196
- };
1197
- /** This function gives a generator of random numbers in `[0, 1)`: `mulberry32` of `docs/FORMAT.md`. */
1198
- function mulberry32(seed) {
1199
- let s = seed >>> 0;
1200
- return () => {
1201
- s = s + 1831565813 | 0;
1202
- let t = Math.imul(s ^ s >>> 15, 1 | s);
1203
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1204
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
1205
- };
1206
- }
1207
- /** This function makes the context of one packing from a seed. */
1208
- function createContext(seed = 0, more = {}) {
1209
- return {
1210
- ...more,
1211
- seed,
1212
- rng: mulberry32(seed)
1213
- };
2020
+ return rows;
1214
2021
  }
1215
- /** This function tells if a polygon ring is convex. A ring with fewer than 3 points is convex. */
1216
- function isConvex(points) {
1217
- let sign = 0;
1218
- const n = points.length;
1219
- for (let i = 0; i < n; i++) {
1220
- const [ax, ay] = points[i];
1221
- const [bx, by] = points[(i + 1) % n];
1222
- const [cx, cy] = points[(i + 2) % n];
1223
- const cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx);
1224
- if (cross === 0) continue;
1225
- if (sign === 0) sign = Math.sign(cross);
1226
- else if (Math.sign(cross) !== sign) return false;
2022
+ /**
2023
+ * This function gives the placed boxes of an output, in the order of the items of the input. A box has the size of its
2024
+ * item after the turn. The list has no item without a placement, and no item with a width or a height of 0.
2025
+ */
2026
+ function boxesOf$1(input, output) {
2027
+ const placements = /* @__PURE__ */ new Map();
2028
+ for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
2029
+ const boxes = [];
2030
+ for (const [index, item] of input.items.entries()) {
2031
+ const p = placements.get(item.id);
2032
+ if (p === void 0 || !(item.w > 0 && item.h > 0)) continue;
2033
+ const turned = p.turn === 90;
2034
+ boxes.push({
2035
+ x: p.x,
2036
+ y: p.y,
2037
+ w: turned ? item.h : item.w,
2038
+ h: turned ? item.w : item.h,
2039
+ rank: item.rank ?? index
2040
+ });
1227
2041
  }
1228
- return true;
2042
+ return boxes;
1229
2043
  }
1230
- /** This function gives the value of `containerShape` of one region. */
1231
- function shapeOf$1(region) {
1232
- switch (region.kind) {
1233
- case "rect":
1234
- case "circle":
1235
- case "ellipse": return region.kind;
1236
- case "polygon":
1237
- if (region.holes && region.holes.length > 0) return "holes";
1238
- return isConvex(region.points) ? "convex" : "polygon";
1239
- }
2044
+ /**
2045
+ * This function tells if box `a` comes before box `b` in the reading order. Two boxes whose heights overlap by more than
2046
+ * `RECORD_EPS` are on one line, and the left box comes first. Otherwise the upper box comes first.
2047
+ */
2048
+ function readsBefore(a, b) {
2049
+ if (Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > RECORD_EPS) return a.x + RECORD_EPS < b.x;
2050
+ return a.y < b.y;
1240
2051
  }
1241
- /** This function gives the value of `itemShape` of one item shape. An ellipse is convex. */
1242
- function itemShapeOf(region) {
1243
- if (region === void 0) return "rect";
1244
- switch (region.kind) {
1245
- case "rect":
1246
- case "circle": return region.kind;
1247
- case "ellipse": return "convex";
1248
- case "polygon": return !region.holes?.length && isConvex(region.points) ? "convex" : "polygon";
1249
- }
2052
+ /** This function sums one measure over the stages of a trace. A trace without the measure gives 0. */
2053
+ function traceSum(output, id) {
2054
+ let sum = 0;
2055
+ for (const stage of output.trace ?? []) sum += stage.measures?.[id] ?? 0;
2056
+ return sum;
1250
2057
  }
1251
2058
  /**
1252
- * This function gives the cell of an input: the values of the packer dimensions that the input tells.
2059
+ * This function gives the measures of one layout, by the ids of `docs/MEASURES.md`. The values are not rounded. A
2060
+ * record rounds them with `roundMeasures`.
1253
2061
  *
1254
- * - `containerSize` comes from `fixed`. The value `none` gives `freeArea` when `aspectWeight` is 0, and `freeAspect` if not.
1255
- * - `containerShape` comes from the shape. A rectangle container with an obstacle of another region gives `holes`.
1256
- * - `obstacleMove` is the hardest value of the obstacles and the locked items: `costly`, then `free`, then `fixed`.
1257
- * - `order` is `soft` when an item has a rank, and `free` if not. The input does not tell a hard order.
1258
- * - `groups` is `nested` when an item has a parent, `flat` when an item has a group, and `none` if not.
2062
+ * - The boxes are the placed items with a width and a height above 0. The hull is the smallest rectangle around them.
2063
+ * - An empty layout gives the area 0, the aspect 1 and the aspect error 0. Its fill is 0 and its whitespace is 1.
2064
+ * - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
2065
+ * - The overflow measures count each id once. `pack.overflow.forced` is the lower bound of `overflowBound`.
2066
+ * - `pack.overflow.excess` is the overflow less the forced overflow. A container with no fixed side gives 0 for both.
2067
+ *
2068
+ * @example
2069
+ * ```ts
2070
+ * const m = measure(input, packWith("shelf", input));
2071
+ * m["pack.fill"]; // the area of the boxes divided by the area of the hull
2072
+ * ```
1259
2073
  */
1260
- function cellOf(input) {
1261
- const { container, items } = input;
1262
- const obstacles = input.obstacles ?? [];
1263
- const containerSize = {
1264
- size: "fixed",
1265
- width: "freeHeight",
1266
- height: "freeWidth",
1267
- none: container.aspectWeight === 0 ? "freeArea" : "freeAspect"
1268
- }[container.fixed];
1269
- let containerShape = container.shape ? shapeOf$1(container.shape) : "rect";
1270
- if (containerShape === "rect" && obstacles.some((o) => o.region.kind !== "rect")) containerShape = "holes";
1271
- let obstacleMove = items.some((i) => i.locked === true) ? "fixed" : void 0;
1272
- for (const o of obstacles) {
1273
- const move = o.move === void 0 ? "fixed" : o.move.cost > 0 ? "costly" : "free";
1274
- if (move === "costly" || obstacleMove === void 0 || move === "free" && obstacleMove === "fixed") obstacleMove = move;
2074
+ function measure(input, output, options = {}) {
2075
+ const boxes = boxesOf$1(input, output);
2076
+ const hull = hullOf(boxes);
2077
+ const hullArea = hull.w * hull.h;
2078
+ const boxArea = boxes.reduce((sum, b) => sum + b.w * b.h, 0);
2079
+ const fill = hullArea > 0 ? boxArea / hullArea : 0;
2080
+ const empty = boxes.length === 0;
2081
+ const target = input.container.aspect ?? 1;
2082
+ let pairs = 0;
2083
+ let inversions = 0;
2084
+ for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
2085
+ const a = boxes[i];
2086
+ const b = boxes[j];
2087
+ if (a.rank === b.rank) continue;
2088
+ pairs += 1;
2089
+ const first = a.rank < b.rank ? a : b;
2090
+ if (readsBefore(first === a ? b : a, first)) inversions += 1;
1275
2091
  }
1276
- const shapes = new Set(items.map((i) => itemShapeOf(i.shape)));
1277
- const itemShape = shapes.size > 1 ? "mixed" : [...shapes][0] ?? "rect";
1278
- const cell = {
1279
- containerSize,
1280
- containerShape,
1281
- itemShape,
1282
- rotation: items.some((i) => i.turnable === true) ? "quarter" : "none",
1283
- order: items.some((i) => i.rank !== void 0) ? "soft" : "free",
1284
- groups: items.some((i) => i.parent !== void 0) ? "nested" : items.some((i) => i.group !== void 0) ? "flat" : "none"
2092
+ const byRank = boxes.toSorted((a, b) => a.rank - b.rank);
2093
+ let breaks = 0;
2094
+ for (let k = 1; k < byRank.length; k++) {
2095
+ const prev = byRank[k - 1];
2096
+ const next = byRank[k];
2097
+ if (prev.rank !== next.rank && readsBefore(next, prev)) breaks += 1;
2098
+ }
2099
+ const rights = rowsOf$1(boxes, options.rowTolerance ?? quantileOrZero(boxes.map((b) => b.h), .5) / 2).map((r) => r.right);
2100
+ const spread = rights.length > 0 ? Math.max(...rights) - Math.min(...rights) : 0;
2101
+ const unplaced = new Set(output.overflow);
2102
+ let itemArea = 0;
2103
+ let unplacedArea = 0;
2104
+ for (const item of input.items) {
2105
+ const a = Math.max(0, item.w) * Math.max(0, item.h);
2106
+ itemArea += a;
2107
+ if (unplaced.has(item.id)) unplacedArea += a;
2108
+ }
2109
+ const bound = overflowBound(input);
2110
+ return {
2111
+ "pack.area": hullArea,
2112
+ "pack.fill": fill,
2113
+ "pack.whitespace": 1 - fill,
2114
+ "pack.aspect": empty ? 1 : Math.max(hull.w / hull.h, hull.h / hull.w),
2115
+ "pack.aspectError": empty ? 0 : aspectError(hull.w, hull.h, target),
2116
+ "pack.order.inversions": pairs > 0 ? inversions / pairs : 0,
2117
+ "pack.order.breaks": breaks,
2118
+ "pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
2119
+ "pack.overflow": unplaced.size,
2120
+ "pack.overflow.rate": input.items.length > 0 ? unplaced.size / input.items.length : 0,
2121
+ "pack.overflow.area": itemArea > 0 ? unplacedArea / itemArea : 0,
2122
+ "pack.overflow.forced": bound.forced,
2123
+ "pack.overflow.forcedArea": itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0,
2124
+ "pack.overflow.excess": Math.max(0, unplaced.size - bound.forced),
2125
+ "pack.search.widths": traceSum(output, "pack.search.widths"),
2126
+ "pack.search.ms": traceSum(output, "pack.search.ms")
1285
2127
  };
1286
- if (obstacleMove !== void 0) cell.obstacleMove = obstacleMove;
1287
- return cell;
1288
2128
  }
1289
- /** This function gives the first dimension of a cell that `supports` does not hold, or `undefined` when it holds each one. */
1290
- function unsupportedOf(supports, cell) {
1291
- for (const dimension of Object.keys(supports)) {
1292
- const value = cell[dimension];
1293
- if (value === void 0) continue;
1294
- if (!supports[dimension].includes(value)) return {
1295
- dimension,
1296
- value
1297
- };
2129
+ /**
2130
+ * This function gives the stability measures of a change pair: the run on the base input and the run on the changed
2131
+ * input. The items match by id. The values are not rounded.
2132
+ *
2133
+ * - An item stays when both inputs have it and both outputs place it. The added and the removed items do not count.
2134
+ * - A staying item moved when its `x` or its `y` changed by more than `RECORD_EPS`, or when its turn changed.
2135
+ * - The distance of an item is the length of the move of its top-left corner. The unit is the width `W`.
2136
+ * - `W` is the larger width of the 2 output sizes. A `W` of 0 becomes 1, so the unit is then the unit of the input.
2137
+ * - No staying item gives 0 for each measure.
2138
+ */
2139
+ function measureChange(baseInput, baseOutput, changeInput, changeOutput) {
2140
+ const placed = (input, output) => {
2141
+ const ids = new Set(input.items.map((i) => i.id));
2142
+ const map = /* @__PURE__ */ new Map();
2143
+ for (const p of output.placements) if (ids.has(p.id) && !map.has(p.id)) map.set(p.id, p);
2144
+ return map;
2145
+ };
2146
+ const before = placed(baseInput, baseOutput);
2147
+ const after = placed(changeInput, changeOutput);
2148
+ const width = Math.max(baseOutput.size.w, changeOutput.size.w);
2149
+ const unit = width > 0 ? width : 1;
2150
+ const distances = [];
2151
+ let moved = 0;
2152
+ for (const [id, p] of before) {
2153
+ const q = after.get(id);
2154
+ if (q === void 0) continue;
2155
+ const dx = q.x - p.x;
2156
+ const dy = q.y - p.y;
2157
+ if (Math.abs(dx) > RECORD_EPS || Math.abs(dy) > RECORD_EPS || (p.turn ?? 0) !== (q.turn ?? 0)) moved += 1;
2158
+ distances.push(Math.hypot(dx, dy) / unit);
1298
2159
  }
1299
- }
1300
- /** This function tells if `supports` holds each value of a cell. A dimension that the cell does not have is held. */
1301
- function supportsCell(supports, cell) {
1302
- return unsupportedOf(supports, cell) === void 0;
1303
- }
1304
- /** This function throws `UnsupportedCell` for the first value of the cell of an input that `supports` does not hold. */
1305
- function assertSupported(supports, input) {
1306
- const miss = unsupportedOf(supports, cellOf(input));
1307
- if (miss) throw new UnsupportedCell(miss.dimension, miss.value);
1308
- }
1309
- // src/frame.ts
1310
- const clock = globalThis.performance;
1311
- /** This function gives the time in milliseconds. */
1312
- function now() {
1313
- return clock ? clock.now() : Date.now();
1314
- }
1315
- /** This function gives the rectangle of a container. A rectangle shape gives the origin and the sizes of the fixed sides. */
1316
- function containerBounds(input) {
1317
- const { container } = input;
1318
- const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
1319
- const fixedW = container.fixed === "size" || container.fixed === "width";
1320
- const fixedH = container.fixed === "size" || container.fixed === "height";
1321
- const w = shape?.w ?? container.w;
1322
- const h = shape?.h ?? container.h;
1323
- if (fixedW && w === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a width.`);
1324
- if (fixedH && h === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a height.`);
2160
+ if (distances.length === 0) return {
2161
+ "pack.stability.moved": 0,
2162
+ "pack.stability.mean": 0,
2163
+ "pack.stability.q95": 0
2164
+ };
1325
2165
  return {
1326
- x: shape?.x ?? 0,
1327
- y: shape?.y ?? 0,
1328
- w: fixedW ? w : Infinity,
1329
- h: fixedH ? h : Infinity
2166
+ "pack.stability.moved": moved / distances.length,
2167
+ "pack.stability.mean": distances.reduce((sum, d) => sum + d, 0) / distances.length,
2168
+ "pack.stability.q95": quantile(distances, .95)
1330
2169
  };
1331
2170
  }
1332
- /** This function gives the items in the order of their rank. The index in the array is the rank of an item without one. */
1333
- function byRank(items) {
1334
- return items.map((item, index) => ({
1335
- item,
1336
- rank: item.rank ?? index
1337
- })).toSorted((a, b) => a.rank - b.rank).map((e) => e.item);
1338
- }
1339
2171
  /**
1340
- * This function makes the frame of an input. A locked item with a position and an obstacle without `move` are locked
1341
- * rectangles. An obstacle with `move` is a free rectangle only when `movable` is `true`.
2172
+ * This function gives the measure `pack.deterministic` of two runs with the same input, options and seed. It gives
2173
+ * `true` when the runs have the same placements, overflow, size and moved obstacles.
2174
+ *
2175
+ * - The placements match by id, so their order in the list does not count. The numbers must be equal.
2176
+ * - The trace is not part of the rule, because its times change each time.
1342
2177
  */
1343
- function buildFrame(input, movable = false) {
1344
- const bounds = containerBounds(input);
1345
- const inset = input.container.inset ?? 0;
1346
- const half = (input.gap ?? 0) / 2;
1347
- const ox = bounds.x + inset - half;
1348
- const oy = bounds.y + inset - half;
1349
- const w = bounds.w - 2 * inset + 2 * half;
1350
- const h = bounds.h - 2 * inset + 2 * half;
1351
- const entries = [];
1352
- const tooLarge = [];
1353
- for (const item of byRank(input.items)) {
1354
- const m = marginOf(item);
1355
- const rect = new Rect();
1356
- rect.size.set(item.w + m.left + m.right + 2 * half, item.h + m.top + m.bottom + 2 * half);
1357
- const dx = m.left + half;
1358
- const dy = m.top + half;
1359
- if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
1360
- rect.locked = true;
1361
- rect.position.set(item.x - dx - ox, item.y - dy - oy);
1362
- } else if (rect.size.x > w + EPS || rect.size.y > h + EPS) {
1363
- tooLarge.push(item.id);
1364
- continue;
1365
- }
1366
- entries.push({
1367
- rect,
1368
- item,
1369
- dx,
1370
- dy
2178
+ function isDeterministic(first, second) {
2179
+ const key = (list) => new Map(list.map((e) => [e.id, e]));
2180
+ const samePlacements = (a, b) => {
2181
+ if (a.length !== b.length) return false;
2182
+ const other = key(b);
2183
+ return a.every((p) => {
2184
+ const q = other.get(p.id);
2185
+ return q !== void 0 && q.x === p.x && q.y === p.y && (q.turn ?? 0) === (p.turn ?? 0);
1371
2186
  });
1372
- }
1373
- for (const obstacle of input.obstacles ?? []) {
1374
- const b = boundsOf(obstacle.region);
1375
- const rect = new Rect();
1376
- rect.size.set(b.w + 2 * half, b.h + 2 * half);
1377
- rect.position.set(b.x - half - ox, b.y - half - oy);
1378
- rect.locked = !(movable && obstacle.move !== void 0);
1379
- entries.push({
1380
- rect,
1381
- obstacle,
1382
- dx: half,
1383
- dy: half
2187
+ };
2188
+ const sameMoved = (a, b) => {
2189
+ if (a.length !== b.length) return false;
2190
+ const other = key(b);
2191
+ return a.every((m) => {
2192
+ const n = other.get(m.id);
2193
+ return n !== void 0 && n.x === m.x && n.y === m.y;
1384
2194
  });
1385
- }
1386
- return {
1387
- ox,
1388
- oy,
1389
- w,
1390
- h,
1391
- bounds,
1392
- inset,
1393
- entries,
1394
- tooLarge
1395
2195
  };
2196
+ const overflowA = new Set(first.overflow);
2197
+ const overflowB = new Set(second.overflow);
2198
+ return first.size.w === second.size.w && first.size.h === second.size.h && overflowA.size === overflowB.size && [...overflowA].every((id) => overflowB.has(id)) && samePlacements(first.placements, second.placements) && sameMoved(first.moved, second.moved);
1396
2199
  }
1397
- /** This function gives the outer rectangle of 0.1 for a frame. A free height starts at 0 and grows. */
1398
- function outerOf(frame) {
1399
- const outer = new Rect();
1400
- const fixedSize = Number.isFinite(frame.h);
1401
- outer.size.set(frame.w, fixedSize ? frame.h : 0);
1402
- outer.fixedWidth = true;
1403
- outer.fixedSize = fixedSize;
1404
- return outer;
2200
+ /**
2201
+ * This function rounds each number of a record of measures with `round3`, as `docs/FORMAT.md` tells. A half goes up,
2202
+ * and `-0` becomes 0. The function keeps the other values, and it does not change its argument.
2203
+ */
2204
+ function roundMeasures(measures) {
2205
+ const out = {};
2206
+ for (const [id, value] of Object.entries(measures)) out[id] = typeof value === "number" ? round3(value) : value;
2207
+ return out;
1405
2208
  }
1406
- /** This function gives the rectangles of a frame, in the order of the entries. */
1407
- function rectsOf(frame) {
1408
- return frame.entries.map((e) => e.rect);
2209
+ /**
2210
+ * This function gives the paired ratio of a measure on one input: the value of the candidate against the value of the
2211
+ * current default. A ratio above 1 is worse, in the direction of the measure.
2212
+ *
2213
+ * - For `lower`, the ratio is `candidate / current`. For `higher`, it is `current / candidate`.
2214
+ * - When both values are 0, the ratio is 1. When only the divisor is 0, the ratio is `Infinity`.
2215
+ * - The values are 0 or more. A negative value or `NaN` throws a `RangeError`.
2216
+ */
2217
+ function pairedRatio(candidate, current, direction) {
2218
+ if (!(candidate >= 0 && current >= 0)) throw new RangeError("A paired ratio needs two values of 0 or more.");
2219
+ const top = direction === "lower" ? candidate : current;
2220
+ const bottom = direction === "lower" ? current : candidate;
2221
+ if (bottom === 0) return top === 0 ? 1 : Infinity;
2222
+ return top / bottom;
1409
2223
  }
1410
2224
  /**
1411
- * This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
1412
- * position exactly. Each other item that the packer did not place goes to the overflow.
2225
+ * This function gives the quantile `q` of a list of numbers, with linear interpolation between the sorted values. The
2226
+ * position in the sorted list is `(n - 1) * q`. An empty list or a `q` outside 0 to 1 throws a `RangeError`.
1413
2227
  */
1414
- function readFrame(input, frame, trace, moved = []) {
1415
- const placements = [];
1416
- const overflow = [...frame.tooLarge];
1417
- for (const e of frame.entries) {
1418
- const item = e.item;
1419
- if (item === void 0) continue;
1420
- if (item.locked === true && item.x !== void 0 && item.y !== void 0) placements.push({
1421
- id: item.id,
1422
- x: item.x,
1423
- y: item.y
1424
- });
1425
- else if (e.rect.wasPacked) placements.push({
1426
- id: item.id,
1427
- x: e.rect.position.x + e.dx + frame.ox,
1428
- y: e.rect.position.y + e.dy + frame.oy
1429
- });
1430
- else overflow.push(item.id);
1431
- }
2228
+ function quantile(values, q) {
2229
+ if (values.length === 0) throw new RangeError("A quantile needs one value or more.");
2230
+ if (!(q >= 0 && q <= 1)) throw new RangeError("A quantile needs a q from 0 to 1.");
2231
+ const sorted = values.toSorted((a, b) => a - b);
2232
+ const at = (sorted.length - 1) * q;
2233
+ const low = Math.floor(at);
2234
+ const part = at - low;
2235
+ const a = sorted[low];
2236
+ if (part === 0) return a;
2237
+ const b = sorted[low + 1];
2238
+ if (a === b) return a;
2239
+ return a + part * (b - a);
2240
+ }
2241
+ /** This function gives the quantile `q` of a list, and 0 for an empty list. */
2242
+ function quantileOrZero(values, q) {
2243
+ return values.length === 0 ? 0 : quantile(values, q);
2244
+ }
2245
+ /** This function gives the median of the paired ratios of a corpus: the quantile 0.5. It is not a ratio of two medians. */
2246
+ function medianRatio(ratios) {
2247
+ return quantile(ratios, .5);
2248
+ }
2249
+ /** This function gives the share of the paired ratios above a limit. An empty list throws a `RangeError`. */
2250
+ function tailShare(ratios, limit) {
2251
+ if (ratios.length === 0) throw new RangeError("A tail share needs one ratio or more.");
2252
+ return ratios.filter((r) => r > limit).length / ratios.length;
2253
+ }
2254
+ // src/metrics.ts
2255
+ /**
2256
+ * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.
2257
+ * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.
2258
+ *
2259
+ * @example
2260
+ * ```ts
2261
+ * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
2262
+ * ```
2263
+ */
2264
+ function packingMetrics(rects, options = {}) {
2265
+ const tolerance = options.rowTolerance ?? 40;
2266
+ const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0).map((r) => ({
2267
+ x: r.position.x,
2268
+ y: r.position.y,
2269
+ w: r.size.x,
2270
+ h: r.size.y
2271
+ }));
2272
+ if (boxes.length === 0) return {
2273
+ count: 0,
2274
+ hull: {
2275
+ width: 0,
2276
+ height: 0
2277
+ },
2278
+ hullAspect: 0,
2279
+ rows: [],
2280
+ raggedness: 0,
2281
+ utilization: 0,
2282
+ widestOverMedian: 0
2283
+ };
2284
+ const box = hullOf(boxes);
2285
+ const hull = {
2286
+ width: box.w,
2287
+ height: box.h
2288
+ };
2289
+ const rows = rowsOf$1(boxes, tolerance);
2290
+ const rights = rows.map((row) => row.right);
2291
+ const widths = boxes.map((r) => r.w).toSorted((a, b) => a - b);
2292
+ const median = widths[Math.floor(widths.length / 2)] ?? 0;
2293
+ const widest = widths.at(-1) ?? 0;
2294
+ const area = boxes.reduce((sum, r) => sum + r.w * r.h, 0);
1432
2295
  return {
1433
- placements,
1434
- overflow,
1435
- size: sizeOf(input, frame, placements, moved),
1436
- moved,
1437
- trace
2296
+ count: boxes.length,
2297
+ hull,
2298
+ hullAspect: hull.height > 0 ? hull.width / hull.height : 0,
2299
+ rows: rows.map((row) => row.count),
2300
+ raggedness: Math.max(...rights) - Math.min(...rights),
2301
+ utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,
2302
+ widestOverMedian: median > 0 ? widest / median : 0
1438
2303
  };
1439
2304
  }
2305
+ // src/strategy.ts
1440
2306
  /**
1441
- * This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
1442
- * far edge of the items with their margins and the inset, or of the obstacles.
2307
+ * The error of a strategy for an input outside its cells. The benchmark records this result. It is not a failure of the
2308
+ * strategy.
1443
2309
  */
1444
- function sizeOf(input, frame, placements, moved) {
1445
- const items = new Map(input.items.map((i) => [i.id, i]));
1446
- let right = 0;
1447
- let bottom = 0;
1448
- for (const p of placements) {
1449
- const item = items.get(p.id);
1450
- const m = marginOf(item);
1451
- right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
1452
- bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
1453
- }
1454
- const movedById = new Map(moved.map((m) => [m.id, m]));
1455
- for (const o of input.obstacles ?? []) {
1456
- const b = boundsOf(o.region);
1457
- const at = movedById.get(o.id) ?? b;
1458
- right = Math.max(right, at.x + b.w - frame.bounds.x);
1459
- bottom = Math.max(bottom, at.y + b.h - frame.bounds.y);
2310
+ var UnsupportedCell = class extends Error {
2311
+ /** The dimension of the taxonomy. */
2312
+ dimension;
2313
+ /** The value of the input that the strategy does not support. */
2314
+ value;
2315
+ constructor(dimension, value) {
2316
+ super(`The strategy does not support the value "${value}" of the dimension "${dimension}".`);
2317
+ this.name = "UnsupportedCell";
2318
+ this.dimension = dimension;
2319
+ this.value = value;
1460
2320
  }
1461
- return {
1462
- w: Number.isFinite(frame.bounds.w) ? frame.bounds.w : right,
1463
- h: Number.isFinite(frame.bounds.h) ? frame.bounds.h : bottom
2321
+ };
2322
+ /** This function gives a generator of random numbers in `[0, 1)`: `mulberry32` of `docs/FORMAT.md`. */
2323
+ function mulberry32(seed) {
2324
+ let s = seed >>> 0;
2325
+ return () => {
2326
+ s = s + 1831565813 | 0;
2327
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
2328
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2329
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1464
2330
  };
1465
2331
  }
1466
- /** This function gives a region with the axes changed: x for y, and w for h. */
1467
- function transposeRegion(region) {
1468
- switch (region.kind) {
1469
- case "rect": return {
1470
- kind: "rect",
1471
- x: region.y,
1472
- y: region.x,
1473
- w: region.h,
1474
- h: region.w
1475
- };
1476
- case "circle": return {
1477
- kind: "circle",
1478
- cx: region.cy,
1479
- cy: region.cx,
1480
- r: region.r
1481
- };
1482
- case "ellipse": return {
1483
- kind: "ellipse",
1484
- cx: region.cy,
1485
- cy: region.cx,
1486
- rx: region.ry,
1487
- ry: region.rx
1488
- };
1489
- case "polygon": {
1490
- const swap = (ring) => ring.map(([x, y]) => [y, x]);
1491
- return region.holes === void 0 ? {
1492
- kind: "polygon",
1493
- points: swap(region.points)
1494
- } : {
1495
- kind: "polygon",
1496
- points: swap(region.points),
1497
- holes: region.holes.map(swap)
1498
- };
1499
- }
1500
- }
1501
- }
1502
- /** This function gives an item with the axes changed. The ports go, because the adapters of 0.1 do not read them. */
1503
- function transposeItem(item) {
1504
- const { ports: _ports, x, y, shape, margin, ...rest } = item;
2332
+ /** This function makes the context of one packing from a seed. */
2333
+ function createContext(seed = 0, more = {}) {
1505
2334
  return {
1506
- ...rest,
1507
- w: item.h,
1508
- h: item.w,
1509
- ...y === void 0 ? {} : { x: y },
1510
- ...x === void 0 ? {} : { y: x },
1511
- ...shape === void 0 ? {} : { shape: transposeRegion(shape) },
1512
- ...margin === void 0 ? {} : { margin: typeof margin === "number" ? margin : {
1513
- top: margin.left,
1514
- right: margin.bottom,
1515
- bottom: margin.right,
1516
- left: margin.top
1517
- } }
2335
+ ...more,
2336
+ seed,
2337
+ rng: mulberry32(seed)
1518
2338
  };
1519
2339
  }
1520
- /** This function gives an input with the axes changed. A fixed height becomes a fixed width. */
1521
- function transposeInput(input) {
1522
- const c = input.container;
1523
- return {
1524
- container: {
1525
- fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed,
1526
- ...c.h === void 0 ? {} : { w: c.h },
1527
- ...c.w === void 0 ? {} : { h: c.w },
1528
- ...c.aspect === void 0 ? {} : { aspect: 1 / c.aspect },
1529
- ...c.aspectWeight === void 0 ? {} : { aspectWeight: c.aspectWeight },
1530
- ...c.shape === void 0 ? {} : { shape: transposeRegion(c.shape) },
1531
- ...c.inset === void 0 ? {} : { inset: c.inset }
1532
- },
1533
- items: input.items.map(transposeItem),
1534
- ...input.obstacles === void 0 ? {} : { obstacles: input.obstacles.map((o) => ({
1535
- ...o,
1536
- region: transposeRegion(o.region)
1537
- })) },
1538
- ...input.gap === void 0 ? {} : { gap: input.gap }
1539
- };
2340
+ /** This function tells if a polygon ring is convex. A ring with fewer than 3 points is convex. */
2341
+ function isConvex(points) {
2342
+ let sign = 0;
2343
+ const n = points.length;
2344
+ for (let i = 0; i < n; i++) {
2345
+ const [ax, ay] = points[i];
2346
+ const [bx, by] = points[(i + 1) % n];
2347
+ const [cx, cy] = points[(i + 2) % n];
2348
+ const cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx);
2349
+ if (cross === 0) continue;
2350
+ if (sign === 0) sign = Math.sign(cross);
2351
+ else if (Math.sign(cross) !== sign) return false;
2352
+ }
2353
+ return true;
1540
2354
  }
1541
- /** This function gives an output with the axes changed. */
1542
- function transposeOutput(output) {
1543
- return {
1544
- placements: output.placements.map((p) => ({
1545
- ...p,
1546
- x: p.y,
1547
- y: p.x
1548
- })),
1549
- overflow: [...output.overflow],
1550
- size: {
1551
- w: output.size.h,
1552
- h: output.size.w
1553
- },
1554
- moved: output.moved.map((m) => ({
1555
- id: m.id,
1556
- x: m.y,
1557
- y: m.x
1558
- })),
1559
- ...output.trace === void 0 ? {} : { trace: output.trace }
1560
- };
2355
+ /** This function gives the value of `containerShape` of one region. */
2356
+ function shapeOf$1(region) {
2357
+ switch (region.kind) {
2358
+ case "rect":
2359
+ case "circle":
2360
+ case "ellipse": return region.kind;
2361
+ case "polygon":
2362
+ if (region.holes && region.holes.length > 0) return "holes";
2363
+ return isConvex(region.points) ? "convex" : "polygon";
2364
+ }
2365
+ }
2366
+ /** This function gives the value of `itemShape` of one item shape. An ellipse is convex. */
2367
+ function itemShapeOf(region) {
2368
+ if (region === void 0) return "rect";
2369
+ switch (region.kind) {
2370
+ case "rect":
2371
+ case "circle": return region.kind;
2372
+ case "ellipse": return "convex";
2373
+ case "polygon": return !region.holes?.length && isConvex(region.points) ? "convex" : "polygon";
2374
+ }
1561
2375
  }
1562
2376
  /**
1563
- * This function packs an input with a fixed height through the turn of the axes. It changes x for y in the input.
1564
- * Then it packs with a fixed width, and changes the axes of the output back.
2377
+ * This function gives the cell of an input: the values of the packer dimensions that the input tells.
2378
+ *
2379
+ * - `containerSize` comes from `fixed`. The value `none` gives `freeArea` when `aspectWeight` is 0, and `freeAspect` if not.
2380
+ * - `containerShape` comes from the shape. A rectangle container with an obstacle of another region gives `holes`.
2381
+ * - `obstacleMove` is the hardest value of the obstacles and the locked items: `costly`, then `free`, then `fixed`.
2382
+ * - `order` is `soft` when an item has a rank, and `free` if not. The input does not tell a hard order.
2383
+ * - `groups` is `nested` when an item has a parent, `flat` when an item has a group, and `none` if not.
1565
2384
  */
1566
- function viaAxes(input, previous, run) {
1567
- const start = now();
1568
- const inner = run(transposeInput(input), previous === void 0 ? void 0 : transposeOutput(previous));
1569
- return {
1570
- ...transposeOutput(inner),
1571
- trace: [{
1572
- id: "axes",
1573
- ms: now() - start,
1574
- measures: {}
1575
- }, ...inner.trace ?? []]
2385
+ function cellOf(input) {
2386
+ const { container, items } = input;
2387
+ const obstacles = input.obstacles ?? [];
2388
+ const containerSize = {
2389
+ size: "fixed",
2390
+ width: "freeHeight",
2391
+ height: "freeWidth",
2392
+ none: container.aspectWeight === 0 ? "freeArea" : "freeAspect"
2393
+ }[container.fixed];
2394
+ let containerShape = container.shape ? shapeOf$1(container.shape) : "rect";
2395
+ if (containerShape === "rect" && obstacles.some((o) => o.region.kind !== "rect")) containerShape = "holes";
2396
+ let obstacleMove = items.some((i) => i.locked === true) ? "fixed" : void 0;
2397
+ for (const o of obstacles) {
2398
+ const move = o.move === void 0 ? "fixed" : o.move.cost > 0 ? "costly" : "free";
2399
+ if (move === "costly" || obstacleMove === void 0 || move === "free" && obstacleMove === "fixed") obstacleMove = move;
2400
+ }
2401
+ const shapes = new Set(items.map((i) => itemShapeOf(i.shape)));
2402
+ const itemShape = shapes.size > 1 ? "mixed" : [...shapes][0] ?? "rect";
2403
+ const cell = {
2404
+ containerSize,
2405
+ containerShape,
2406
+ itemShape,
2407
+ rotation: items.some((i) => i.turnable === true) ? "quarter" : "none",
2408
+ order: items.some((i) => i.rank !== void 0) ? "soft" : "free",
2409
+ groups: items.some((i) => i.parent !== void 0) ? "nested" : items.some((i) => i.group !== void 0) ? "flat" : "none"
1576
2410
  };
2411
+ if (obstacleMove !== void 0) cell.obstacleMove = obstacleMove;
2412
+ return cell;
2413
+ }
2414
+ /** This function gives the first dimension of a cell that `supports` does not hold, or `undefined` when it holds each one. */
2415
+ function unsupportedOf(supports, cell) {
2416
+ for (const dimension of Object.keys(supports)) {
2417
+ const value = cell[dimension];
2418
+ if (value === void 0) continue;
2419
+ if (!supports[dimension].includes(value)) return {
2420
+ dimension,
2421
+ value
2422
+ };
2423
+ }
2424
+ }
2425
+ /** This function tells if `supports` holds each value of a cell. A dimension that the cell does not have is held. */
2426
+ function supportsCell(supports, cell) {
2427
+ return unsupportedOf(supports, cell) === void 0;
2428
+ }
2429
+ /** This function throws `UnsupportedCell` for the first value of the cell of an input that `supports` does not hold. */
2430
+ function assertSupported(supports, input) {
2431
+ const miss = unsupportedOf(supports, cellOf(input));
2432
+ if (miss) throw new UnsupportedCell(miss.dimension, miss.value);
1577
2433
  }
1578
2434
  // src/adapters.ts
1579
- /** The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. */
2435
+ /**
2436
+ * The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. The container layer of `src/space.ts`
2437
+ * gives them each container shape.
2438
+ */
1580
2439
  const LAYOUT_CELLS = {
1581
2440
  containerSize: [
1582
2441
  "fixed",
@@ -1585,7 +2444,14 @@ const LAYOUT_CELLS = {
1585
2444
  "freeAspect",
1586
2445
  "freeArea"
1587
2446
  ],
1588
- containerShape: ["rect"],
2447
+ containerShape: [
2448
+ "rect",
2449
+ "convex",
2450
+ "polygon",
2451
+ "circle",
2452
+ "ellipse",
2453
+ "holes"
2454
+ ],
1589
2455
  itemShape: ["rect"],
1590
2456
  rotation: ["none"],
1591
2457
  groups: ["none"],
@@ -1611,7 +2477,8 @@ function packFixedWidth(id, input, options) {
1611
2477
  const rects = rectsOf(frame);
1612
2478
  if (frame.w > 0) pack(rects, outerOf(frame), {
1613
2479
  strategy: id,
1614
- sortOrder: options.sortOrder
2480
+ sortOrder: options.sortOrder,
2481
+ space: spaceOf(input, frame)
1615
2482
  });
1616
2483
  const output = readFrame(input, frame, []);
1617
2484
  return {
@@ -1654,11 +2521,12 @@ function layoutStrategy(id, family, catalog, cells) {
1654
2521
  return self;
1655
2522
  }
1656
2523
  /**
1657
- * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. It supports no obstacle and
1658
- * no locked item, because `packRows` does not go around them. Turnable items, groups and shapes throw `UnsupportedCell`.
2524
+ * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. With obstacles, locked items or a
2525
+ * container shape, the rows go around the holes of the free space and lose the gap fill. Turnable items, groups and item
2526
+ * shapes throw `UnsupportedCell`.
1659
2527
  */
1660
2528
  const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
1661
- obstacleMove: [],
2529
+ obstacleMove: ["fixed"],
1662
2530
  order: [
1663
2531
  "free",
1664
2532
  "soft",
@@ -1667,7 +2535,7 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
1667
2535
  });
1668
2536
  /**
1669
2537
  * The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
1670
- * locked items and the fixed obstacles. Turnable items, groups and shapes throw `UnsupportedCell`.
2538
+ * locked items and the fixed obstacles, in each container shape. Turnable items, groups and item shapes throw `UnsupportedCell`.
1671
2539
  */
1672
2540
  const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
1673
2541
  obstacleMove: ["fixed"],
@@ -1675,7 +2543,7 @@ const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
1675
2543
  });
1676
2544
  /**
1677
2545
  * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
1678
- * With the sort order `input`, the rows keep the rank order. Turnable items, groups and shapes throw `UnsupportedCell`.
2546
+ * With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
1679
2547
  */
1680
2548
  const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
1681
2549
  obstacleMove: ["fixed"],
@@ -1704,6 +2572,7 @@ const logSweep = {
1704
2572
  const result = packFree(rectsOf(frame), new Rect(), {
1705
2573
  strategy: id,
1706
2574
  sortOrder: o.sortOrder,
2575
+ space: spaceOf(input, frame),
1707
2576
  samples: o.samples,
1708
2577
  aspect: input.container.aspect ?? 1,
1709
2578
  aspectWeight: input.container.aspectWeight ?? 1
@@ -1965,8 +2834,12 @@ function inner(box, tolerance) {
1965
2834
  * The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
1966
2835
  * The rules read a shape of the container or of an item as its bounding box (decision 5). The bounding box of an item
1967
2836
  * shape is relative to the position of the item. The result has no tolerance in it.
2837
+ *
2838
+ * With `options.exactContainer`, a shaped container with the size `fixed` also uses its true outline for `in-container`.
2839
+ * A box then counts when `rectInRegion` of the contract tells that it is not in the shape, as in `packHard`.
2840
+ * For that test, the box grows by the inset and becomes smaller by `tolerance.overlap` on each side. The default is `false`.
1968
2841
  */
1969
- function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2842
+ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE, options = {}) {
1970
2843
  const tol = tolerance.overlap;
1971
2844
  const placements = /* @__PURE__ */ new Map();
1972
2845
  for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
@@ -2031,11 +2904,18 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2031
2904
  const inset = container.inset ?? 0;
2032
2905
  const fixedW = container.fixed === "size" || container.fixed === "width";
2033
2906
  const fixedH = container.fixed === "size" || container.fixed === "height";
2907
+ const exact = options.exactContainer === true && container.fixed === "size" ? container.shape : void 0;
2034
2908
  let outside = 0;
2035
2909
  for (const box of boxes) {
2036
2910
  const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
2037
2911
  const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
2038
2912
  if (outX || outY) outside += 1;
2913
+ else if (exact !== void 0 && !rectInRegion(exact, inner({
2914
+ l: box.l - inset,
2915
+ t: box.t - inset,
2916
+ r: box.r + inset,
2917
+ b: box.b + inset
2918
+ }, tol))) outside += 1;
2039
2919
  }
2040
2920
  return {
2041
2921
  "no-overlap": overlaps,
@@ -2958,6 +3838,6 @@ function corpus(name, specs) {
2958
3838
  inputs
2959
3839
  };
2960
3840
  }
2961
- export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellOf, checkHard, checkInput, compareRects, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, fromRects, gallery, generator, generators, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, measure, measureChange, medianRatio, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, push, pushCascade, quantile, rectGrid, rectScattered, round3, roundMeasures, rows, shelf, skyline, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
3841
+ export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellOf, checkHard, checkInput, compareRects, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, freeSpace, fromRects, gallery, generator, generators, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, measure, measureChange, medianRatio, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, push, pushCascade, quantile, rectGrid, rectScattered, round3, roundMeasures, rows, shelf, skyline, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
2962
3842
 
2963
3843
  //# sourceMappingURL=index.js.map