@mark1russell7/packing 0.2.0 → 0.2.1

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
@@ -156,1427 +156,1940 @@ 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);
794
- }
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);
799
- }
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";
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;
807
1192
  }
808
1193
  /**
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.
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.
811
1202
  */
812
- function toPackInput(file) {
813
- 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 }
818
- };
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;
1227
+ }
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));
819
1233
  }
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
1234
  /**
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.
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.
833
1243
  */
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
876
- };
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);
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;
886
1262
  }
887
- return {
888
- x: left,
889
- y: top,
890
- w: right - left,
891
- h: bottom - top
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;
892
1285
  };
893
- }
894
- /**
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.
897
- */
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
909
- });
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
+ }
910
1294
  }
911
- return rows;
912
- }
913
- /**
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.
916
- */
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
931
- });
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
+ }
932
1314
  }
933
- return boxes;
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;
1321
+ return {
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)
1337
+ };
934
1338
  }
935
1339
  /**
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.
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.
938
1342
  */
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;
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
1348
+ };
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
+ };
942
1353
  }
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;
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));
948
1363
  }
949
1364
  /**
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.
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.
956
1369
  *
957
1370
  * @example
958
1371
  * ```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
1372
+ * const moved = pushCascade(dropped, cards, { gap: 8 });
961
1373
  * ```
962
1374
  */
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;
980
- }
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;
987
- }
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
- };
1003
- }
1004
- /**
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.
1007
- *
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.
1013
- */
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;
1020
- };
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);
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
+ }
1034
1398
  }
1035
- if (distances.length === 0) return {
1036
- "pack.stability.moved": 0,
1037
- "pack.stability.mean": 0,
1038
- "pack.stability.q95": 0
1039
- };
1040
- 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)
1044
- };
1399
+ return [...moved];
1045
1400
  }
1401
+ // src/sha256.ts
1402
+ /** The round constants of SHA-256. */
1403
+ const K = new Uint32Array([
1404
+ 1116352408,
1405
+ 1899447441,
1406
+ 3049323471,
1407
+ 3921009573,
1408
+ 961987163,
1409
+ 1508970993,
1410
+ 2453635748,
1411
+ 2870763221,
1412
+ 3624381080,
1413
+ 310598401,
1414
+ 607225278,
1415
+ 1426881987,
1416
+ 1925078388,
1417
+ 2162078206,
1418
+ 2614888103,
1419
+ 3248222580,
1420
+ 3835390401,
1421
+ 4022224774,
1422
+ 264347078,
1423
+ 604807628,
1424
+ 770255983,
1425
+ 1249150122,
1426
+ 1555081692,
1427
+ 1996064986,
1428
+ 2554220882,
1429
+ 2821834349,
1430
+ 2952996808,
1431
+ 3210313671,
1432
+ 3336571891,
1433
+ 3584528711,
1434
+ 113926993,
1435
+ 338241895,
1436
+ 666307205,
1437
+ 773529912,
1438
+ 1294757372,
1439
+ 1396182291,
1440
+ 1695183700,
1441
+ 1986661051,
1442
+ 2177026350,
1443
+ 2456956037,
1444
+ 2730485921,
1445
+ 2820302411,
1446
+ 3259730800,
1447
+ 3345764771,
1448
+ 3516065817,
1449
+ 3600352804,
1450
+ 4094571909,
1451
+ 275423344,
1452
+ 430227734,
1453
+ 506948616,
1454
+ 659060556,
1455
+ 883997877,
1456
+ 958139571,
1457
+ 1322822218,
1458
+ 1537002063,
1459
+ 1747873779,
1460
+ 1955562222,
1461
+ 2024104815,
1462
+ 2227730452,
1463
+ 2361852424,
1464
+ 2428436474,
1465
+ 2756734187,
1466
+ 3204031479,
1467
+ 3329325298
1468
+ ]);
1469
+ /** The first hash value of SHA-256. */
1470
+ const H0 = [
1471
+ 1779033703,
1472
+ 3144134277,
1473
+ 1013904242,
1474
+ 2773480762,
1475
+ 1359893119,
1476
+ 2600822924,
1477
+ 528734635,
1478
+ 1541459225
1479
+ ];
1480
+ /** This function turns a 32-bit word to the right by `n` bits. */
1481
+ const rotr = (x, n) => x >>> n | x << 32 - n;
1046
1482
  /**
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.
1483
+ * This function gives the UTF-8 bytes of a string. A lone surrogate becomes U+FFFD, as with `TextEncoder`. The
1484
+ * package has no `TextEncoder` type, because its code operates on each runtime with no global types.
1052
1485
  */
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);
1486
+ function utf8(text) {
1487
+ const out = [];
1488
+ for (let i = 0; i < text.length; i++) {
1489
+ let c = text.charCodeAt(i);
1490
+ if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
1491
+ const next = text.charCodeAt(i + 1);
1492
+ if (next >= 56320 && next <= 57343) {
1493
+ c = 65536 + (c - 55296 << 10) + (next - 56320);
1494
+ i++;
1495
+ }
1496
+ }
1497
+ if (c >= 55296 && c <= 57343) c = 65533;
1498
+ if (c < 128) out.push(c);
1499
+ else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
1500
+ else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
1501
+ else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
1502
+ }
1503
+ return Uint8Array.from(out);
1074
1504
  }
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;
1505
+ /** This function gives the SHA-256 of the UTF-8 bytes of a string, as 64 lowercase hex characters. */
1506
+ function sha256Hex(text) {
1507
+ const bytes = utf8(text);
1508
+ const length = bytes.length;
1509
+ const padded = new Uint8Array(((length + 9 + 63) / 64 | 0) * 64);
1510
+ padded.set(bytes);
1511
+ padded[length] = 128;
1512
+ const view = new DataView(padded.buffer);
1513
+ const bits = length * 8;
1514
+ view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
1515
+ view.setUint32(padded.length - 4, bits >>> 0);
1516
+ const hash = [...H0];
1517
+ const w = /* @__PURE__ */ new Uint32Array(64);
1518
+ for (let block = 0; block < padded.length; block += 64) {
1519
+ for (let t = 0; t < 16; t++) w[t] = view.getUint32(block + t * 4);
1520
+ for (let t = 16; t < 64; t++) {
1521
+ const a = w[t - 15];
1522
+ const b = w[t - 2];
1523
+ const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
1524
+ const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
1525
+ w[t] = w[t - 16] + s0 + w[t - 7] + s1 | 0;
1526
+ }
1527
+ let [a, b, c, d, e, f, g, h] = hash;
1528
+ for (let t = 0; t < 64; t++) {
1529
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1530
+ const ch = e & f ^ ~e & g;
1531
+ const t1 = h + s1 + ch + K[t] + w[t] | 0;
1532
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
1533
+ h = g;
1534
+ g = f;
1535
+ f = e;
1536
+ e = d + t1 | 0;
1537
+ d = c;
1538
+ c = b;
1539
+ b = a;
1540
+ a = t1 + t2 | 0;
1541
+ }
1542
+ hash[0] = hash[0] + a | 0;
1543
+ hash[1] = hash[1] + b | 0;
1544
+ hash[2] = hash[2] + c | 0;
1545
+ hash[3] = hash[3] + d | 0;
1546
+ hash[4] = hash[4] + e | 0;
1547
+ hash[5] = hash[5] + f | 0;
1548
+ hash[6] = hash[6] + g | 0;
1549
+ hash[7] = hash[7] + h | 0;
1550
+ }
1551
+ return hash.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
1083
1552
  }
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;
1553
+ // src/layout-input.ts
1554
+ /** This function gives the `fnv1a32` hash of the UTF-8 bytes of a string, as `docs/FORMAT.md` gives it. */
1555
+ function fnv1a32(text) {
1556
+ let h = 2166136261;
1557
+ for (const b of utf8(text)) h = Math.imul(h ^ b, 16777619) >>> 0;
1558
+ return h;
1559
+ }
1560
+ /** This function rounds the numbers of a file to 3 decimals. A half goes up, and the value `-0` becomes 0. */
1561
+ function round3(x) {
1562
+ const v = Math.round(x * 1e3) / 1e3;
1563
+ return Object.is(v, -0) ? 0 : v;
1098
1564
  }
1099
1565
  /**
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`.
1566
+ * This function gives the canonical form of a value: sorted keys, no whitespace and the numbers of `JSON.stringify`
1567
+ * after `round3`. A key with the value `undefined` is not in the form.
1102
1568
  */
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);
1115
- }
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);
1569
+ function canonicalJson(value) {
1570
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
1571
+ 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(",")}}`;
1572
+ return JSON.stringify(typeof value === "number" ? round3(value) : value);
1119
1573
  }
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);
1574
+ /** This function gives the hash of an input file: the first 16 hex characters of the SHA-256 of its canonical form without `hash`. */
1575
+ function hashInput(file) {
1576
+ const { hash: _hash, ...rest } = file;
1577
+ return sha256Hex(canonicalJson(rest)).slice(0, 16);
1123
1578
  }
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;
1579
+ /** 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`. */
1580
+ function countCell(count) {
1581
+ if (count <= 3) return "1";
1582
+ if (count <= 31) return "10";
1583
+ if (count <= 316) return "100";
1584
+ if (count <= 3162) return "1k";
1585
+ return "10k";
1128
1586
  }
1129
- // src/metrics.ts
1130
1587
  /**
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.
1133
- *
1134
- * @example
1135
- * ```ts
1136
- * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
1137
- * ```
1588
+ * This function gives the packing input of a file. The boxes become the items, and the edges go.
1589
+ * A file without a container gets `{ fixed: "none" }`, so the packer picks the size. It is `toPackInput` of the contract.
1138
1590
  */
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
1591
+ function toPackInput(file) {
1592
+ return {
1593
+ container: file.container ?? { fixed: "none" },
1594
+ items: file.boxes,
1595
+ ...file.obstacles === void 0 ? {} : { obstacles: file.obstacles },
1596
+ ...file.gap === void 0 ? {} : { gap: file.gap }
1158
1597
  };
1159
- const box = hullOf(boxes);
1160
- const hull = {
1161
- width: box.w,
1162
- height: box.h
1598
+ }
1599
+ // src/measures.ts
1600
+ const spec = (id, family, unit, direction, min, max, guard) => ({
1601
+ id,
1602
+ family,
1603
+ unit,
1604
+ direction,
1605
+ min,
1606
+ max,
1607
+ ...guard
1608
+ });
1609
+ /**
1610
+ * The facts of each measure, by id. `docs/MEASURES.md` gives the full definitions. This record is the source of the floor
1611
+ * and the tolerance of each guard of the packer.
1612
+ */
1613
+ const MEASURE_SPECS = Object.freeze({
1614
+ "pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity),
1615
+ "pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1),
1616
+ "pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1),
1617
+ "pack.aspect": spec("pack.aspect", "aspect", "ratio", "lower", 1, Infinity),
1618
+ "pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity, {
1619
+ floor: 0,
1620
+ tol: .01
1621
+ }),
1622
+ "pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, {
1623
+ floor: 0,
1624
+ tol: 0
1625
+ }),
1626
+ "pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity),
1627
+ "pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1),
1628
+ "pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, {
1629
+ floor: 0,
1630
+ tol: 0
1631
+ }),
1632
+ "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, {
1633
+ floor: 0,
1634
+ tol: .02
1635
+ }),
1636
+ "pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, {
1637
+ floor: 0,
1638
+ tol: .005
1639
+ }),
1640
+ "pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, {
1641
+ floor: 0,
1642
+ tol: .01
1643
+ }),
1644
+ "pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1),
1645
+ "pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity),
1646
+ "pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity)
1647
+ });
1648
+ /** This function gives the smallest rectangle around the boxes. No box gives a rectangle of size 0 at 0, 0. */
1649
+ function hullOf(boxes) {
1650
+ if (boxes.length === 0) return {
1651
+ x: 0,
1652
+ y: 0,
1653
+ w: 0,
1654
+ h: 0
1163
1655
  };
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);
1656
+ let left = Infinity;
1657
+ let top = Infinity;
1658
+ let right = -Infinity;
1659
+ let bottom = -Infinity;
1660
+ for (const b of boxes) {
1661
+ left = Math.min(left, b.x);
1662
+ top = Math.min(top, b.y);
1663
+ right = Math.max(right, b.x + b.w);
1664
+ bottom = Math.max(bottom, b.y + b.h);
1665
+ }
1170
1666
  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
1667
+ x: left,
1668
+ y: top,
1669
+ w: right - left,
1670
+ h: bottom - top
1178
1671
  };
1179
1672
  }
1180
- // src/strategy.ts
1181
1673
  /**
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.
1674
+ * This function puts the boxes in rows. It sorts the boxes by their top, and keeps the order of the list for equal tops.
1675
+ * A box joins the last row when its top is within `tolerance` of the top of the first box of that row.
1184
1676
  */
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;
1677
+ function rowsOf$1(boxes, tolerance) {
1678
+ const rows = [];
1679
+ for (const b of boxes.toSorted((p, q) => p.y - q.y)) {
1680
+ const row = rows.at(-1);
1681
+ if (row && Math.abs(b.y - row.top) <= tolerance) {
1682
+ row.right = Math.max(row.right, b.x + b.w);
1683
+ row.count += 1;
1684
+ } else rows.push({
1685
+ top: b.y,
1686
+ right: b.x + b.w,
1687
+ count: 1
1688
+ });
1195
1689
  }
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
- };
1690
+ return rows;
1214
1691
  }
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;
1692
+ /**
1693
+ * 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
1694
+ * item after the turn. The list has no item without a placement, and no item with a width or a height of 0.
1695
+ */
1696
+ function boxesOf$1(input, output) {
1697
+ const placements = /* @__PURE__ */ new Map();
1698
+ for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
1699
+ const boxes = [];
1700
+ for (const [index, item] of input.items.entries()) {
1701
+ const p = placements.get(item.id);
1702
+ if (p === void 0 || !(item.w > 0 && item.h > 0)) continue;
1703
+ const turned = p.turn === 90;
1704
+ boxes.push({
1705
+ x: p.x,
1706
+ y: p.y,
1707
+ w: turned ? item.h : item.w,
1708
+ h: turned ? item.w : item.h,
1709
+ rank: item.rank ?? index
1710
+ });
1227
1711
  }
1228
- return true;
1712
+ return boxes;
1229
1713
  }
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
- }
1714
+ /**
1715
+ * This function tells if box `a` comes before box `b` in the reading order. Two boxes whose heights overlap by more than
1716
+ * `RECORD_EPS` are on one line, and the left box comes first. Otherwise the upper box comes first.
1717
+ */
1718
+ function readsBefore(a, b) {
1719
+ 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;
1720
+ return a.y < b.y;
1240
1721
  }
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
- }
1722
+ /** This function sums one measure over the stages of a trace. A trace without the measure gives 0. */
1723
+ function traceSum(output, id) {
1724
+ let sum = 0;
1725
+ for (const stage of output.trace ?? []) sum += stage.measures?.[id] ?? 0;
1726
+ return sum;
1250
1727
  }
1251
1728
  /**
1252
- * This function gives the cell of an input: the values of the packer dimensions that the input tells.
1729
+ * This function gives the measures of one layout, by the ids of `docs/MEASURES.md`. The values are not rounded. A
1730
+ * record rounds them with `roundMeasures`.
1253
1731
  *
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.
1732
+ * - The boxes are the placed items with a width and a height above 0. The hull is the smallest rectangle around them.
1733
+ * - An empty layout gives the area 0, the aspect 1 and the aspect error 0. Its fill is 0 and its whitespace is 1.
1734
+ * - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
1735
+ *
1736
+ * @example
1737
+ * ```ts
1738
+ * const m = measure(input, packWith("shelf", input));
1739
+ * m["pack.fill"]; // the area of the boxes divided by the area of the hull
1740
+ * ```
1259
1741
  */
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;
1742
+ function measure(input, output, options = {}) {
1743
+ const boxes = boxesOf$1(input, output);
1744
+ const hull = hullOf(boxes);
1745
+ const hullArea = hull.w * hull.h;
1746
+ const boxArea = boxes.reduce((sum, b) => sum + b.w * b.h, 0);
1747
+ const fill = hullArea > 0 ? boxArea / hullArea : 0;
1748
+ const empty = boxes.length === 0;
1749
+ const target = input.container.aspect ?? 1;
1750
+ let pairs = 0;
1751
+ let inversions = 0;
1752
+ for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
1753
+ const a = boxes[i];
1754
+ const b = boxes[j];
1755
+ if (a.rank === b.rank) continue;
1756
+ pairs += 1;
1757
+ const first = a.rank < b.rank ? a : b;
1758
+ if (readsBefore(first === a ? b : a, first)) inversions += 1;
1275
1759
  }
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"
1285
- };
1286
- if (obstacleMove !== void 0) cell.obstacleMove = obstacleMove;
1287
- return cell;
1288
- }
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
- };
1760
+ const byRank = boxes.toSorted((a, b) => a.rank - b.rank);
1761
+ let breaks = 0;
1762
+ for (let k = 1; k < byRank.length; k++) {
1763
+ const prev = byRank[k - 1];
1764
+ const next = byRank[k];
1765
+ if (prev.rank !== next.rank && readsBefore(next, prev)) breaks += 1;
1298
1766
  }
1767
+ const rights = rowsOf$1(boxes, options.rowTolerance ?? quantileOrZero(boxes.map((b) => b.h), .5) / 2).map((r) => r.right);
1768
+ const spread = rights.length > 0 ? Math.max(...rights) - Math.min(...rights) : 0;
1769
+ return {
1770
+ "pack.area": hullArea,
1771
+ "pack.fill": fill,
1772
+ "pack.whitespace": 1 - fill,
1773
+ "pack.aspect": empty ? 1 : Math.max(hull.w / hull.h, hull.h / hull.w),
1774
+ "pack.aspectError": empty ? 0 : aspectError(hull.w, hull.h, target),
1775
+ "pack.order.inversions": pairs > 0 ? inversions / pairs : 0,
1776
+ "pack.order.breaks": breaks,
1777
+ "pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
1778
+ "pack.overflow": new Set(output.overflow).size,
1779
+ "pack.search.widths": traceSum(output, "pack.search.widths"),
1780
+ "pack.search.ms": traceSum(output, "pack.search.ms")
1781
+ };
1299
1782
  }
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.`);
1783
+ /**
1784
+ * This function gives the stability measures of a change pair: the run on the base input and the run on the changed
1785
+ * input. The items match by id. The values are not rounded.
1786
+ *
1787
+ * - An item stays when both inputs have it and both outputs place it. The added and the removed items do not count.
1788
+ * - A staying item moved when its `x` or its `y` changed by more than `RECORD_EPS`, or when its turn changed.
1789
+ * - The distance of an item is the length of the move of its top-left corner. The unit is the width `W`.
1790
+ * - `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.
1791
+ * - No staying item gives 0 for each measure.
1792
+ */
1793
+ function measureChange(baseInput, baseOutput, changeInput, changeOutput) {
1794
+ const placed = (input, output) => {
1795
+ const ids = new Set(input.items.map((i) => i.id));
1796
+ const map = /* @__PURE__ */ new Map();
1797
+ for (const p of output.placements) if (ids.has(p.id) && !map.has(p.id)) map.set(p.id, p);
1798
+ return map;
1799
+ };
1800
+ const before = placed(baseInput, baseOutput);
1801
+ const after = placed(changeInput, changeOutput);
1802
+ const width = Math.max(baseOutput.size.w, changeOutput.size.w);
1803
+ const unit = width > 0 ? width : 1;
1804
+ const distances = [];
1805
+ let moved = 0;
1806
+ for (const [id, p] of before) {
1807
+ const q = after.get(id);
1808
+ if (q === void 0) continue;
1809
+ const dx = q.x - p.x;
1810
+ const dy = q.y - p.y;
1811
+ if (Math.abs(dx) > RECORD_EPS || Math.abs(dy) > RECORD_EPS || (p.turn ?? 0) !== (q.turn ?? 0)) moved += 1;
1812
+ distances.push(Math.hypot(dx, dy) / unit);
1813
+ }
1814
+ if (distances.length === 0) return {
1815
+ "pack.stability.moved": 0,
1816
+ "pack.stability.mean": 0,
1817
+ "pack.stability.q95": 0
1818
+ };
1325
1819
  return {
1326
- x: shape?.x ?? 0,
1327
- y: shape?.y ?? 0,
1328
- w: fixedW ? w : Infinity,
1329
- h: fixedH ? h : Infinity
1820
+ "pack.stability.moved": moved / distances.length,
1821
+ "pack.stability.mean": distances.reduce((sum, d) => sum + d, 0) / distances.length,
1822
+ "pack.stability.q95": quantile(distances, .95)
1330
1823
  };
1331
1824
  }
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
1825
  /**
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`.
1826
+ * This function gives the measure `pack.deterministic` of two runs with the same input, options and seed. It gives
1827
+ * `true` when the runs have the same placements, overflow, size and moved obstacles.
1828
+ *
1829
+ * - The placements match by id, so their order in the list does not count. The numbers must be equal.
1830
+ * - The trace is not part of the rule, because its times change each time.
1342
1831
  */
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
1832
+ function isDeterministic(first, second) {
1833
+ const key = (list) => new Map(list.map((e) => [e.id, e]));
1834
+ const samePlacements = (a, b) => {
1835
+ if (a.length !== b.length) return false;
1836
+ const other = key(b);
1837
+ return a.every((p) => {
1838
+ const q = other.get(p.id);
1839
+ return q !== void 0 && q.x === p.x && q.y === p.y && (q.turn ?? 0) === (p.turn ?? 0);
1371
1840
  });
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
1841
+ };
1842
+ const sameMoved = (a, b) => {
1843
+ if (a.length !== b.length) return false;
1844
+ const other = key(b);
1845
+ return a.every((m) => {
1846
+ const n = other.get(m.id);
1847
+ return n !== void 0 && n.x === m.x && n.y === m.y;
1384
1848
  });
1385
- }
1386
- return {
1387
- ox,
1388
- oy,
1389
- w,
1390
- h,
1391
- bounds,
1392
- inset,
1393
- entries,
1394
- tooLarge
1395
1849
  };
1850
+ const overflowA = new Set(first.overflow);
1851
+ const overflowB = new Set(second.overflow);
1852
+ 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
1853
  }
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;
1854
+ /**
1855
+ * This function rounds each number of a record of measures with `round3`, as `docs/FORMAT.md` tells. A half goes up,
1856
+ * and `-0` becomes 0. The function keeps the other values, and it does not change its argument.
1857
+ */
1858
+ function roundMeasures(measures) {
1859
+ const out = {};
1860
+ for (const [id, value] of Object.entries(measures)) out[id] = typeof value === "number" ? round3(value) : value;
1861
+ return out;
1405
1862
  }
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);
1863
+ /**
1864
+ * This function gives the paired ratio of a measure on one input: the value of the candidate against the value of the
1865
+ * current default. A ratio above 1 is worse, in the direction of the measure.
1866
+ *
1867
+ * - For `lower`, the ratio is `candidate / current`. For `higher`, it is `current / candidate`.
1868
+ * - When both values are 0, the ratio is 1. When only the divisor is 0, the ratio is `Infinity`.
1869
+ * - The values are 0 or more. A negative value or `NaN` throws a `RangeError`.
1870
+ */
1871
+ function pairedRatio(candidate, current, direction) {
1872
+ if (!(candidate >= 0 && current >= 0)) throw new RangeError("A paired ratio needs two values of 0 or more.");
1873
+ const top = direction === "lower" ? candidate : current;
1874
+ const bottom = direction === "lower" ? current : candidate;
1875
+ if (bottom === 0) return top === 0 ? 1 : Infinity;
1876
+ return top / bottom;
1409
1877
  }
1410
1878
  /**
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.
1879
+ * This function gives the quantile `q` of a list of numbers, with linear interpolation between the sorted values. The
1880
+ * position in the sorted list is `(n - 1) * q`. An empty list or a `q` outside 0 to 1 throws a `RangeError`.
1413
1881
  */
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
- }
1882
+ function quantile(values, q) {
1883
+ if (values.length === 0) throw new RangeError("A quantile needs one value or more.");
1884
+ if (!(q >= 0 && q <= 1)) throw new RangeError("A quantile needs a q from 0 to 1.");
1885
+ const sorted = values.toSorted((a, b) => a - b);
1886
+ const at = (sorted.length - 1) * q;
1887
+ const low = Math.floor(at);
1888
+ const part = at - low;
1889
+ const a = sorted[low];
1890
+ if (part === 0) return a;
1891
+ const b = sorted[low + 1];
1892
+ if (a === b) return a;
1893
+ return a + part * (b - a);
1894
+ }
1895
+ /** This function gives the quantile `q` of a list, and 0 for an empty list. */
1896
+ function quantileOrZero(values, q) {
1897
+ return values.length === 0 ? 0 : quantile(values, q);
1898
+ }
1899
+ /** This function gives the median of the paired ratios of a corpus: the quantile 0.5. It is not a ratio of two medians. */
1900
+ function medianRatio(ratios) {
1901
+ return quantile(ratios, .5);
1902
+ }
1903
+ /** This function gives the share of the paired ratios above a limit. An empty list throws a `RangeError`. */
1904
+ function tailShare(ratios, limit) {
1905
+ if (ratios.length === 0) throw new RangeError("A tail share needs one ratio or more.");
1906
+ return ratios.filter((r) => r > limit).length / ratios.length;
1907
+ }
1908
+ // src/metrics.ts
1909
+ /**
1910
+ * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.
1911
+ * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.
1912
+ *
1913
+ * @example
1914
+ * ```ts
1915
+ * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
1916
+ * ```
1917
+ */
1918
+ function packingMetrics(rects, options = {}) {
1919
+ const tolerance = options.rowTolerance ?? 40;
1920
+ const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0).map((r) => ({
1921
+ x: r.position.x,
1922
+ y: r.position.y,
1923
+ w: r.size.x,
1924
+ h: r.size.y
1925
+ }));
1926
+ if (boxes.length === 0) return {
1927
+ count: 0,
1928
+ hull: {
1929
+ width: 0,
1930
+ height: 0
1931
+ },
1932
+ hullAspect: 0,
1933
+ rows: [],
1934
+ raggedness: 0,
1935
+ utilization: 0,
1936
+ widestOverMedian: 0
1937
+ };
1938
+ const box = hullOf(boxes);
1939
+ const hull = {
1940
+ width: box.w,
1941
+ height: box.h
1942
+ };
1943
+ const rows = rowsOf$1(boxes, tolerance);
1944
+ const rights = rows.map((row) => row.right);
1945
+ const widths = boxes.map((r) => r.w).toSorted((a, b) => a - b);
1946
+ const median = widths[Math.floor(widths.length / 2)] ?? 0;
1947
+ const widest = widths.at(-1) ?? 0;
1948
+ const area = boxes.reduce((sum, r) => sum + r.w * r.h, 0);
1432
1949
  return {
1433
- placements,
1434
- overflow,
1435
- size: sizeOf(input, frame, placements, moved),
1436
- moved,
1437
- trace
1950
+ count: boxes.length,
1951
+ hull,
1952
+ hullAspect: hull.height > 0 ? hull.width / hull.height : 0,
1953
+ rows: rows.map((row) => row.count),
1954
+ raggedness: Math.max(...rights) - Math.min(...rights),
1955
+ utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,
1956
+ widestOverMedian: median > 0 ? widest / median : 0
1438
1957
  };
1439
1958
  }
1959
+ // src/strategy.ts
1440
1960
  /**
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.
1961
+ * The error of a strategy for an input outside its cells. The benchmark records this result. It is not a failure of the
1962
+ * strategy.
1443
1963
  */
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);
1964
+ var UnsupportedCell = class extends Error {
1965
+ /** The dimension of the taxonomy. */
1966
+ dimension;
1967
+ /** The value of the input that the strategy does not support. */
1968
+ value;
1969
+ constructor(dimension, value) {
1970
+ super(`The strategy does not support the value "${value}" of the dimension "${dimension}".`);
1971
+ this.name = "UnsupportedCell";
1972
+ this.dimension = dimension;
1973
+ this.value = value;
1460
1974
  }
1461
- return {
1462
- w: Number.isFinite(frame.bounds.w) ? frame.bounds.w : right,
1463
- h: Number.isFinite(frame.bounds.h) ? frame.bounds.h : bottom
1975
+ };
1976
+ /** This function gives a generator of random numbers in `[0, 1)`: `mulberry32` of `docs/FORMAT.md`. */
1977
+ function mulberry32(seed) {
1978
+ let s = seed >>> 0;
1979
+ return () => {
1980
+ s = s + 1831565813 | 0;
1981
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
1982
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1983
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1464
1984
  };
1465
1985
  }
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;
1986
+ /** This function makes the context of one packing from a seed. */
1987
+ function createContext(seed = 0, more = {}) {
1505
1988
  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
- } }
1989
+ ...more,
1990
+ seed,
1991
+ rng: mulberry32(seed)
1518
1992
  };
1519
1993
  }
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
- };
1994
+ /** This function tells if a polygon ring is convex. A ring with fewer than 3 points is convex. */
1995
+ function isConvex(points) {
1996
+ let sign = 0;
1997
+ const n = points.length;
1998
+ for (let i = 0; i < n; i++) {
1999
+ const [ax, ay] = points[i];
2000
+ const [bx, by] = points[(i + 1) % n];
2001
+ const [cx, cy] = points[(i + 2) % n];
2002
+ const cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx);
2003
+ if (cross === 0) continue;
2004
+ if (sign === 0) sign = Math.sign(cross);
2005
+ else if (Math.sign(cross) !== sign) return false;
2006
+ }
2007
+ return true;
1540
2008
  }
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
- };
2009
+ /** This function gives the value of `containerShape` of one region. */
2010
+ function shapeOf$1(region) {
2011
+ switch (region.kind) {
2012
+ case "rect":
2013
+ case "circle":
2014
+ case "ellipse": return region.kind;
2015
+ case "polygon":
2016
+ if (region.holes && region.holes.length > 0) return "holes";
2017
+ return isConvex(region.points) ? "convex" : "polygon";
2018
+ }
2019
+ }
2020
+ /** This function gives the value of `itemShape` of one item shape. An ellipse is convex. */
2021
+ function itemShapeOf(region) {
2022
+ if (region === void 0) return "rect";
2023
+ switch (region.kind) {
2024
+ case "rect":
2025
+ case "circle": return region.kind;
2026
+ case "ellipse": return "convex";
2027
+ case "polygon": return !region.holes?.length && isConvex(region.points) ? "convex" : "polygon";
2028
+ }
1561
2029
  }
1562
2030
  /**
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.
2031
+ * This function gives the cell of an input: the values of the packer dimensions that the input tells.
2032
+ *
2033
+ * - `containerSize` comes from `fixed`. The value `none` gives `freeArea` when `aspectWeight` is 0, and `freeAspect` if not.
2034
+ * - `containerShape` comes from the shape. A rectangle container with an obstacle of another region gives `holes`.
2035
+ * - `obstacleMove` is the hardest value of the obstacles and the locked items: `costly`, then `free`, then `fixed`.
2036
+ * - `order` is `soft` when an item has a rank, and `free` if not. The input does not tell a hard order.
2037
+ * - `groups` is `nested` when an item has a parent, `flat` when an item has a group, and `none` if not.
1565
2038
  */
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 ?? []]
2039
+ function cellOf(input) {
2040
+ const { container, items } = input;
2041
+ const obstacles = input.obstacles ?? [];
2042
+ const containerSize = {
2043
+ size: "fixed",
2044
+ width: "freeHeight",
2045
+ height: "freeWidth",
2046
+ none: container.aspectWeight === 0 ? "freeArea" : "freeAspect"
2047
+ }[container.fixed];
2048
+ let containerShape = container.shape ? shapeOf$1(container.shape) : "rect";
2049
+ if (containerShape === "rect" && obstacles.some((o) => o.region.kind !== "rect")) containerShape = "holes";
2050
+ let obstacleMove = items.some((i) => i.locked === true) ? "fixed" : void 0;
2051
+ for (const o of obstacles) {
2052
+ const move = o.move === void 0 ? "fixed" : o.move.cost > 0 ? "costly" : "free";
2053
+ if (move === "costly" || obstacleMove === void 0 || move === "free" && obstacleMove === "fixed") obstacleMove = move;
2054
+ }
2055
+ const shapes = new Set(items.map((i) => itemShapeOf(i.shape)));
2056
+ const itemShape = shapes.size > 1 ? "mixed" : [...shapes][0] ?? "rect";
2057
+ const cell = {
2058
+ containerSize,
2059
+ containerShape,
2060
+ itemShape,
2061
+ rotation: items.some((i) => i.turnable === true) ? "quarter" : "none",
2062
+ order: items.some((i) => i.rank !== void 0) ? "soft" : "free",
2063
+ groups: items.some((i) => i.parent !== void 0) ? "nested" : items.some((i) => i.group !== void 0) ? "flat" : "none"
1576
2064
  };
2065
+ if (obstacleMove !== void 0) cell.obstacleMove = obstacleMove;
2066
+ return cell;
2067
+ }
2068
+ /** This function gives the first dimension of a cell that `supports` does not hold, or `undefined` when it holds each one. */
2069
+ function unsupportedOf(supports, cell) {
2070
+ for (const dimension of Object.keys(supports)) {
2071
+ const value = cell[dimension];
2072
+ if (value === void 0) continue;
2073
+ if (!supports[dimension].includes(value)) return {
2074
+ dimension,
2075
+ value
2076
+ };
2077
+ }
2078
+ }
2079
+ /** This function tells if `supports` holds each value of a cell. A dimension that the cell does not have is held. */
2080
+ function supportsCell(supports, cell) {
2081
+ return unsupportedOf(supports, cell) === void 0;
2082
+ }
2083
+ /** This function throws `UnsupportedCell` for the first value of the cell of an input that `supports` does not hold. */
2084
+ function assertSupported(supports, input) {
2085
+ const miss = unsupportedOf(supports, cellOf(input));
2086
+ if (miss) throw new UnsupportedCell(miss.dimension, miss.value);
1577
2087
  }
1578
2088
  // src/adapters.ts
1579
- /** The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. */
2089
+ /**
2090
+ * The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. The container layer of `src/space.ts`
2091
+ * gives them each container shape.
2092
+ */
1580
2093
  const LAYOUT_CELLS = {
1581
2094
  containerSize: [
1582
2095
  "fixed",
@@ -1585,7 +2098,14 @@ const LAYOUT_CELLS = {
1585
2098
  "freeAspect",
1586
2099
  "freeArea"
1587
2100
  ],
1588
- containerShape: ["rect"],
2101
+ containerShape: [
2102
+ "rect",
2103
+ "convex",
2104
+ "polygon",
2105
+ "circle",
2106
+ "ellipse",
2107
+ "holes"
2108
+ ],
1589
2109
  itemShape: ["rect"],
1590
2110
  rotation: ["none"],
1591
2111
  groups: ["none"],
@@ -1611,7 +2131,8 @@ function packFixedWidth(id, input, options) {
1611
2131
  const rects = rectsOf(frame);
1612
2132
  if (frame.w > 0) pack(rects, outerOf(frame), {
1613
2133
  strategy: id,
1614
- sortOrder: options.sortOrder
2134
+ sortOrder: options.sortOrder,
2135
+ space: spaceOf(input, frame)
1615
2136
  });
1616
2137
  const output = readFrame(input, frame, []);
1617
2138
  return {
@@ -1654,11 +2175,12 @@ function layoutStrategy(id, family, catalog, cells) {
1654
2175
  return self;
1655
2176
  }
1656
2177
  /**
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`.
2178
+ * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. With obstacles, locked items or a
2179
+ * container shape, the rows go around the holes of the free space and lose the gap fill. Turnable items, groups and item
2180
+ * shapes throw `UnsupportedCell`.
1659
2181
  */
1660
2182
  const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
1661
- obstacleMove: [],
2183
+ obstacleMove: ["fixed"],
1662
2184
  order: [
1663
2185
  "free",
1664
2186
  "soft",
@@ -1667,7 +2189,7 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
1667
2189
  });
1668
2190
  /**
1669
2191
  * 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`.
2192
+ * locked items and the fixed obstacles, in each container shape. Turnable items, groups and item shapes throw `UnsupportedCell`.
1671
2193
  */
1672
2194
  const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
1673
2195
  obstacleMove: ["fixed"],
@@ -1675,7 +2197,7 @@ const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
1675
2197
  });
1676
2198
  /**
1677
2199
  * 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`.
2200
+ * With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
1679
2201
  */
1680
2202
  const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
1681
2203
  obstacleMove: ["fixed"],
@@ -1704,6 +2226,7 @@ const logSweep = {
1704
2226
  const result = packFree(rectsOf(frame), new Rect(), {
1705
2227
  strategy: id,
1706
2228
  sortOrder: o.sortOrder,
2229
+ space: spaceOf(input, frame),
1707
2230
  samples: o.samples,
1708
2231
  aspect: input.container.aspect ?? 1,
1709
2232
  aspectWeight: input.container.aspectWeight ?? 1
@@ -2958,6 +3481,6 @@ function corpus(name, specs) {
2958
3481
  inputs
2959
3482
  };
2960
3483
  }
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 };
3484
+ 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
3485
 
2963
3486
  //# sourceMappingURL=index.js.map