@mark1russell7/packing 0.1.7 → 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
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_TOLERANCE, EPS, RECORD_EPS, boundsOf, boundsOf as boundsOf$1, checkInput, marginSides, regionHitsRect, translate, turnBox, turnPoint, turnPort, turnRegion } from "@mark1russell7/layout-contract";
1
2
  // src/vector.ts
2
3
  /** A mutable two-dimensional vector. */
3
4
  var Vector = class Vector {
@@ -155,1559 +156,1940 @@ function compareRects(order) {
155
156
  case "input": return () => 0;
156
157
  }
157
158
  }
158
- // 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
+ }
159
170
  /**
160
- * This function gives the first x from `start` where a rectangle of width `w` crosses no obstacle in the band from
161
- * `top` to `bottom`.
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.
162
175
  */
163
- function firstFreeX(obstacles, start, w, top, bottom) {
164
- let x = start;
165
- let moved = true;
166
- while (moved) {
167
- moved = false;
168
- for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.left < x + w && x < o.right) {
169
- x = o.right;
170
- moved = true;
171
- }
172
- }
173
- return x;
176
+ function turnItem(item) {
177
+ return turnBox(item);
174
178
  }
175
- /** This function gives the lowest bottom edge of the obstacles in the band from `top` to `bottom`, under `top`. */
176
- function nextTop(obstacles, top, bottom) {
177
- let next = Infinity;
178
- for (const o of obstacles) if (o.top < bottom && top < o.bottom && o.bottom > top) next = Math.min(next, o.bottom);
179
- return next;
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);
180
208
  }
181
209
  /**
182
- * This function packs rectangles into shelves: rows that it fills left to right in the sort order, and does not fill
183
- * again. It goes around each `locked` rectangle. It sets the position of each rectangle and gives the outer rectangle.
184
- *
185
- * - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.
186
- * - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.
187
- * - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,
188
- * a new row starts under the current row.
189
- * - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
190
- * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
191
- * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
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`.
192
212
  */
193
- function packShelf(rects, outer, options = {}) {
194
- const width = outer.size.x;
195
- const obstacles = [];
196
- let contentBottom = 0;
197
- for (const rect of rects) {
198
- if (!rect.locked) continue;
199
- rect.wasPacked = true;
200
- const left = rect.position.x - outer.position.x;
201
- const top = rect.position.y - outer.position.y;
202
- obstacles.push({
203
- left,
204
- top,
205
- right: left + rect.size.x,
206
- bottom: top + rect.size.y
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
207
241
  });
208
- contentBottom = Math.max(contentBottom, top + rect.size.y);
209
242
  }
210
- let rowTop = 0;
211
- let rowBottom = 0;
212
- let cursor = 0;
213
- let rowEmpty = true;
214
- for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
215
- if (rect.wasPacked) continue;
216
- const w = rect.size.x;
217
- const h = rect.size.y;
218
- if (!(w > 0 && h > 0)) continue;
219
- const overflows = w > width;
220
- if (overflows && outer.fixedSize) continue;
221
- if (overflows && !rowEmpty) {
222
- rowTop = rowBottom;
223
- cursor = 0;
224
- rowEmpty = true;
225
- }
226
- let x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
227
- while (!overflows && x + w > width) {
228
- rowTop = rowEmpty ? nextTop(obstacles, rowTop, rowTop + h) : rowBottom;
229
- rowBottom = rowTop;
230
- cursor = 0;
231
- rowEmpty = true;
232
- x = firstFreeX(obstacles, cursor, w, rowTop, rowTop + h);
233
- }
234
- while (overflows && x !== 0) {
235
- rowTop = nextTop(obstacles, rowTop, rowTop + h);
236
- rowBottom = rowTop;
237
- x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
238
- }
239
- if (outer.fixedSize && rowTop + h > outer.size.y) continue;
240
- rect.position.set(x + outer.position.x, rowTop + outer.position.y);
241
- rect.wasPacked = true;
242
- rowEmpty = false;
243
- cursor = x + w;
244
- rowBottom = Math.max(rowBottom, rowTop + h);
245
- contentBottom = Math.max(contentBottom, rowTop + h);
246
- if (overflows) {
247
- rowTop = rowBottom;
248
- cursor = 0;
249
- rowEmpty = true;
250
- }
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
+ });
251
255
  }
252
- 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;
253
274
  return outer;
254
275
  }
255
- // 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
+ }
256
280
  /**
257
- * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
258
- * of the window is the largest value of `skyline[x, x + rw)`. The first `x` with the smallest height wins. A
259
- * 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.
260
283
  */
261
- function lowestWindow(skyline, width, rw) {
262
- const deque = new Int32Array(width);
263
- let head = 0;
264
- let tail = 0;
265
- let bestX = 0;
266
- let bestY = Infinity;
267
- for (let i = 0; i < width; i++) {
268
- while (tail > head && skyline[deque[tail - 1]] <= skyline[i]) tail--;
269
- deque[tail++] = i;
270
- if (deque[head] <= i - rw) head++;
271
- if (i >= rw - 1) {
272
- const max = skyline[deque[head]];
273
- if (max < bestY) {
274
- bestY = max;
275
- bestX = i - rw + 1;
276
- }
277
- }
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);
278
301
  }
279
302
  return {
280
- x: bestX,
281
- y: bestY
303
+ placements,
304
+ overflow,
305
+ size: sizeOf(input, frame, placements, moved),
306
+ moved,
307
+ trace
282
308
  };
283
309
  }
284
310
  /**
285
- * This function packs rectangles into an outer rectangle with a skyline. It puts each rectangle in the lowest place
286
- * where it fits, and it does not change the width of the outer rectangle.
287
- *
288
- * - A `locked` rectangle keeps its position. The skyline starts at its bottom edge across its width, thus the other
289
- * rectangles go under it. The space above it stays empty, because a skyline has no holes.
290
- * - The skyline has a cell for each whole unit of the width. A rectangle that needs more cells than the outer
291
- * rectangle has goes under all other rectangles, at the left edge.
292
- * - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that
293
- * does not fit is not packed: its `wasPacked` stays `false`.
294
- * - 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.
295
313
  */
296
- function packSkyline(rects, outer, options = {}) {
297
- const width = Math.floor(outer.size.x);
298
- if (width <= 0) return outer;
299
- const skyline = new Float64Array(width);
300
- for (const rect of rects) {
301
- if (!rect.locked) continue;
302
- rect.wasPacked = true;
303
- const x0 = Math.max(0, Math.floor(rect.position.x - outer.position.x));
304
- const x1 = Math.min(width, Math.ceil(rect.position.x - outer.position.x + rect.size.x));
305
- const bottom = rect.position.y - outer.position.y + rect.size.y;
306
- for (let i = x0; i < x1; i++) skyline[i] = Math.max(skyline[i], bottom);
307
- 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);
308
323
  }
309
- for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
310
- if (rect.wasPacked) continue;
311
- const rw = Math.ceil(rect.size.x);
312
- const rh = rect.size.y;
313
- if (rw <= 0 || rh <= 0) continue;
314
- let x;
315
- let y;
316
- if (rw > width) {
317
- if (outer.fixedSize) continue;
318
- x = 0;
319
- y = 0;
320
- for (let i = 0; i < width; i++) if (skyline[i] > y) y = skyline[i];
321
- } else {
322
- ({x, y} = lowestWindow(skyline, width, rw));
323
- if (outer.fixedSize && y + rh > outer.size.y) continue;
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);
330
+ }
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
+ };
335
+ }
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
+ };
324
369
  }
325
- rect.position.set(x + outer.position.x, y + outer.position.y);
326
- rect.wasPacked = true;
327
- const top = y + rh;
328
- for (let i = x; i < Math.min(width, x + rw); i++) skyline[i] = top;
329
- if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);
330
370
  }
331
- return outer;
332
371
  }
333
- // src/pack.ts
334
- function packInto(rects, region, offset, order) {
335
- const unpacked = Rect.unpacked(rects);
336
- pack(unpacked, region, { sortOrder: order });
337
- for (const r of Rect.packed(unpacked)) r.translateBy(offset);
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
+ };
338
389
  }
339
- function packRow(rects, outer, order) {
340
- const row = [];
341
- const byHeight = rects.toSorted(compareRects(order));
342
- const fillGaps = order !== "input";
343
- const rowRect = new Rect();
344
- rowRect.size.set(outer.size.x, 0);
345
- for (const rect of byHeight) {
346
- if (rect.wasPacked) continue;
347
- if (rect.size.x > rowRect.size.x) {
348
- if (outer.fixedSize && rect.size.x > outer.size.x) continue;
349
- const outerLeft = new Rect();
350
- outerLeft.position.setTo(rowRect.position);
351
- outerLeft.size.set(outer.size.x - rowRect.position.x, rowRect.size.y);
352
- outerLeft.fixedSize = true;
353
- if (fillGaps) packInto(rects, outerLeft, outer.position, order);
354
- rowRect.position.set(0, rowRect.bottom());
355
- rowRect.size.set(outer.size.x, 0);
356
- row.length = 0;
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
+ };
431
+ }
432
+ /**
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.
435
+ */
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;
357
456
  }
358
- if (rowRect.position.y + rect.size.y > outer.size.y) {
359
- if (outer.fixedSize) continue;
360
- outer.size.y += rect.size.y;
361
- if (!outer.fixedWidth && !outer.size.x) outer.size.x = rect.size.x;
362
- Rect.resetPacked(rects);
363
- return false;
457
+ if (a > s) out.push([s, a]);
458
+ if (e > b) out.push([b, e]);
459
+ }
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];
364
490
  }
365
- row.push(rect);
366
- rect.position.setTo(rowRect.position.add(outer.position));
367
- rowRect.shrinkLeft(rect.size.x);
368
- rect.wasPacked = true;
369
- if (rect.size.y > rowRect.size.y) rowRect.size.y = rect.size.y;
370
- if (fillGaps && row.length > 1) {
371
- const rectInRow = row[row.length - 1];
372
- const outerLeft = new Rect();
373
- outerLeft.position.set(rectInRow.position.x - outer.position.x, rectInRow.bottom() - outer.position.y);
374
- outerLeft.size.set(rectInRow.size.x, rowRect.size.y - rectInRow.size.y);
375
- outerLeft.fixedWidth = true;
376
- outerLeft.fixedSize = true;
377
- packInto(rects, outerLeft, outer.position, order);
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;
378
504
  }
379
505
  }
380
- return true;
381
506
  }
382
- /**
383
- * This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does
384
- * not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps
385
- * its position, but the rows do not go around it. `pack` sets the width first and then uses this function.
386
- */
387
- function packRows(rects, outer, options = {}) {
388
- const order = options.sortOrder ?? "height-desc";
389
- for (const r of rects) if (r.locked) r.wasPacked = true;
390
- while (!packRow(rects, outer, order));
391
- return outer;
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]] : [];
526
+ }
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;
560
+ }
561
+ }
392
562
  }
393
- /**
394
- * This function packs rectangles into an outer rectangle with a guillotine row packing. It sets the position
395
- * of each rectangle and the size of the outer rectangle, and it gives the outer rectangle.
396
- *
397
- * - Without fixed sizes, the outer rectangle starts with a width of 1.5 times the square root of the total area.
398
- * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
399
- * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
400
- * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
401
- * - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
402
- */
403
- function pack(rects, outer = new Rect(), options = {}) {
404
- let totalArea = 0;
405
- let widest = 0;
406
- let minX = rects.length > 0 ? Infinity : 0;
407
- let minY = rects.length > 0 ? Infinity : 0;
408
- for (const r of rects) {
409
- totalArea += r.area();
410
- widest = Math.max(widest, r.size.x);
411
- minX = Math.min(minX, r.size.x);
412
- minY = Math.min(minY, r.size.y);
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);
413
569
  }
414
- const idealWidth = outer.fixedWidth ? outer.size.x : Math.max(widest, Math.min(1.5 * Math.sqrt(totalArea), outer.size.x || Infinity));
415
- const ideal = Vector.of(idealWidth).clamp(new Vector(minX, minY), Vector.Infinity);
416
- if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
417
- if (options.strategy === "skyline") packSkyline(rects, outer, options);
418
- else if (options.strategy === "shelf") packShelf(rects, outer, options);
419
- else packRows(rects, outer, options);
420
- if (!outer.fixedSize) {
421
- const contentRight = Rect.maxRight(rects);
422
- if (!outer.size.x) outer.size.x = contentRight;
423
- else if (!outer.fixedWidth) outer.size.x = Math.min(contentRight, outer.size.x);
424
- outer.size.y = Rect.maxBottom(rects);
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);
425
575
  }
426
- return outer;
576
+ return spans;
427
577
  }
428
- // src/free.ts
429
- /** This function gives the distance between an aspect and a target aspect on a log scale. */
430
- function aspectError(width, height, target = 1) {
431
- if (!(width > 0 && height > 0 && target > 0)) return Infinity;
432
- return Math.abs(Math.log(width / height) - Math.log(target));
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;
618
+ }
619
+ }
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) {
624
+ return {
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
+ })
647
+ };
648
+ }
649
+ /** This function makes a blocker of a region in the frame. */
650
+ function blockerOf(region, g) {
651
+ const box = boundsOf(region);
652
+ return {
653
+ region,
654
+ g,
655
+ c0: box.y,
656
+ c1: box.y + box.h
657
+ };
433
658
  }
434
659
  /**
435
- * This function packs rectangles with a free width. It tries many widths with the strategy of `options`, and keeps
436
- * the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.
437
- *
438
- * - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.
439
- * - The second pass tries the widths between the two neighbours of the best width of the first pass.
440
- * - The search is deterministic: the same input gives the same widths and the same result.
441
- * - A `locked` rectangle keeps its position. The width is not less than its right edge.
442
- * - Each strategy of `pack` works with this function, because it only changes the width.
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.
443
662
  */
444
- function packFree(rects, outer = new Rect(), options = {}) {
445
- const target = options.aspect ?? 1;
446
- const weight = options.aspectWeight ?? 1;
447
- const samples = Math.max(2, Math.floor(options.samples ?? 48));
448
- const ox = outer.position.x;
449
- const oy = outer.position.y;
450
- let itemArea = 0;
451
- let minWidth = 0;
452
- let rowWidth = 0;
453
- for (const r of rects) {
454
- if (r.locked) {
455
- minWidth = Math.max(minWidth, r.right() - ox);
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));
456
677
  continue;
457
678
  }
458
- if (!(r.size.x > 0 && r.size.y > 0)) continue;
459
- itemArea += r.area();
460
- minWidth = Math.max(minWidth, r.size.x);
461
- rowWidth += r.size.x;
462
- }
463
- for (const r of rects) if (r.locked) itemArea += r.area();
464
- const maxWidth = Math.max(minWidth, rowWidth);
465
- const tried = [];
466
- const run = (width) => {
467
- Rect.resetPacked(rects);
468
- const box = new Rect();
469
- box.position.setTo(outer.position);
470
- box.size.set(width, 0);
471
- box.fixedWidth = true;
472
- pack(rects, box, options);
473
- const placed = Rect.packed(rects);
474
- const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);
475
- const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);
476
- const area = contentWidth * contentHeight;
477
- const t = {
478
- width,
479
- contentWidth,
480
- contentHeight,
481
- objective: area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0
482
- };
483
- tried.push(t);
484
- return t;
485
- };
486
- const widths = [];
487
- if (maxWidth > 0) {
488
- const ratio = maxWidth / minWidth;
489
- for (let i = 0; i < samples; i++) {
490
- const w = minWidth * ratio ** (i / (samples - 1));
491
- const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));
492
- if (widths.at(-1) !== rounded) widths.push(rounded);
493
- }
494
- }
495
- let best = void 0;
496
- let bestIndex = 0;
497
- for (const [i, w] of widths.entries()) {
498
- const t = run(w);
499
- if (best === void 0 || t.objective < best.objective) {
500
- best = t;
501
- bestIndex = i;
502
- }
503
- }
504
- if (widths.length > 2) {
505
- const low = widths[Math.max(0, bestIndex - 1)];
506
- const high = widths[Math.min(widths.length - 1, bestIndex + 1)];
507
- const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));
508
- for (let i = 1; i <= steps; i++) {
509
- const w = Math.round(low + (high - low) * i / (steps + 1));
510
- if (widths.includes(w)) continue;
511
- const t = run(w);
512
- if (best === void 0 || t.objective < best.objective) best = t;
513
- }
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
+ });
514
686
  }
515
- const chosen = best === void 0 ? void 0 : run(best.width);
516
- tried.pop();
517
- const w = chosen?.contentWidth ?? 0;
518
- const h = chosen?.contentHeight ?? 0;
519
- if (!outer.fixedSize) outer.size.set(w, h);
520
- const area = w * h;
521
- return {
522
- outer,
523
- area,
524
- fill: area > 0 ? itemArea / area : 0,
525
- aspect: h > 0 ? w / h : 0,
526
- aspectError: aspectError(w, h, target),
527
- objective: chosen?.objective ?? 0,
528
- tried
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
529
696
  };
530
- }
531
- // src/push.ts
532
- /** This function gives the overlap of two rectangles on each axis. A value of 0 or less is no overlap. */
533
- function overlapOf(a, b) {
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
+ }));
534
719
  return {
535
- x: Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x),
536
- y: Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y)
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
+ }
537
742
  };
538
743
  }
539
744
  /**
540
- * This function gives the direction of a first shove. The axis has the smaller overlap. The sign points away from the
541
- * center of the pusher. When that shove crosses 0, the sign is positive.
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.
542
747
  */
543
- function firstDirection(pusher, r, gap) {
544
- const overlap = overlapOf(pusher, r);
545
- if (overlap.x < overlap.y) return {
546
- axis: "x",
547
- 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
548
- };
549
- return {
550
- axis: "y",
551
- 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
552
- };
553
- }
554
- /** This function puts a rectangle past the pusher, along a direction. The rectangle does not go past 0. */
555
- function shove(pusher, r, direction, gap) {
556
- if (direction.axis === "x") {
557
- const x = direction.sign > 0 ? pusher.right() + gap : pusher.position.x - r.size.x - gap;
558
- r.position.set(Math.max(0, x), r.position.y);
559
- return;
560
- }
561
- const y = direction.sign > 0 ? pusher.bottom() + gap : pusher.position.y - r.size.y - gap;
562
- r.position.set(r.position.x, Math.max(0, y));
748
+ function freeSpace(input) {
749
+ return spaceOf(input, buildFrame(input));
563
750
  }
751
+ // src/shelf.ts
564
752
  /**
565
- * This function moves each rectangle that a placed rectangle overlaps, and the rectangles that those overlap in turn.
566
- * A shove takes the axis of the smaller overlap, away from the pusher. A shoved rectangle keeps the direction of its first
567
- * shove when it shoves the next one, so the cascade goes outward. The placed rectangle and each locked rectangle stay.
568
- * The function moves the rectangles in place and gives the rectangles that moved.
569
- *
570
- * @example
571
- * ```ts
572
- * const moved = pushCascade(dropped, cards, { gap: 8 });
573
- * ```
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`.
574
755
  */
575
- function pushCascade(placed, rects, options = {}) {
576
- const gap = options.gap ?? 8;
577
- const maxShoves = options.maxShoves ?? rects.length * 8;
578
- const moved = /* @__PURE__ */ new Set();
579
- const queue = [{ pusher: placed }];
580
- let shoves = 0;
581
- while (shoves < maxShoves) {
582
- const step = queue.shift();
583
- if (!step) break;
584
- const { pusher, direction } = step;
585
- for (const r of rects) {
586
- if (r === pusher || r === placed || r.locked) continue;
587
- const overlap = overlapOf(pusher, r);
588
- if (overlap.x <= 0 || overlap.y <= 0) continue;
589
- const next = direction ?? firstDirection(pusher, r, gap);
590
- shove(pusher, r, next, gap);
591
- moved.add(r);
592
- queue.push({
593
- pusher: r,
594
- direction: next
595
- });
596
- 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;
597
764
  }
598
765
  }
599
- return [...moved];
600
- }
601
- // src/contract.ts
602
- /** The tolerance for the comparison of two numbers in an engine. */
603
- const EPS = 1e-6;
604
- /** The tolerance for the comparison of two numbers of a file. The files round to 3 decimals. */
605
- const RECORD_EPS = .001;
606
- /** The default tolerances of the hard rules. Each record writes its tolerances in its top-level field `tolerance`. */
607
- const DEFAULT_TOLERANCE = Object.freeze({
608
- overlap: .001,
609
- through: .5
610
- });
611
- /** This function gives the margin of an item on each side. A number is the same margin on each side. */
612
- function marginOf(item) {
613
- const m = item.margin ?? 0;
614
- if (typeof m === "number") return {
615
- top: m,
616
- right: m,
617
- bottom: m,
618
- left: m
619
- };
620
- return {
621
- top: m.top,
622
- right: m.right,
623
- bottom: m.bottom,
624
- left: m.left
625
- };
766
+ return x;
626
767
  }
627
- /** The side of a port after a turn of 90 degrees clockwise. */
628
- const TURNED_SIDE = {
629
- top: "right",
630
- right: "bottom",
631
- bottom: "left",
632
- left: "top"
633
- };
634
- /** This function gives the point `px`, `py` of an item of height `h` after a turn of 90 degrees clockwise. */
635
- function turnPoint(px, py, h) {
636
- return [h - py, px];
637
- }
638
- /** This function turns a port of an item of height `h` by 90 degrees clockwise. The side turns from top to right. */
639
- function turnPort(port, h) {
640
- const turned = {
641
- id: port.id,
642
- side: TURNED_SIDE[port.side]
643
- };
644
- if (port.x !== void 0 && port.y !== void 0) {
645
- const [x, y] = turnPoint(port.x, port.y, h);
646
- turned.x = x;
647
- turned.y = y;
648
- } else if (port.x !== void 0) turned.y = port.x;
649
- else if (port.y !== void 0) turned.x = h - port.y;
650
- return turned;
651
- }
652
- /** This function turns a region relative to an item of height `h` by 90 degrees clockwise. */
653
- function turnRegion(region, h) {
654
- switch (region.kind) {
655
- case "rect": return {
656
- kind: "rect",
657
- x: h - region.y - region.h,
658
- y: region.x,
659
- w: region.h,
660
- h: region.w
661
- };
662
- case "circle": {
663
- const [cx, cy] = turnPoint(region.cx, region.cy, h);
664
- return {
665
- kind: "circle",
666
- cx,
667
- cy,
668
- r: region.r
669
- };
670
- }
671
- case "ellipse": {
672
- const [cx, cy] = turnPoint(region.cx, region.cy, h);
673
- return {
674
- kind: "ellipse",
675
- cx,
676
- cy,
677
- rx: region.ry,
678
- ry: region.rx
679
- };
680
- }
681
- case "polygon": {
682
- const turnRing = (ring) => ring.map(([x, y]) => turnPoint(x, y, h));
683
- const turned = {
684
- kind: "polygon",
685
- points: turnRing(region.points)
686
- };
687
- if (region.holes) turned.holes = region.holes.map(turnRing);
688
- return turned;
689
- }
690
- }
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;
691
773
  }
692
774
  /**
693
- * This function gives an item after a turn of 90 degrees clockwise, with the y axis down (decision 1 of
694
- * `docs/PACKER.md`). The width and the height change places. The top-left corner, the id and the other fields stay.
695
- * The ports, the shape and the margin turn with the item. The function does not change its argument.
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.
696
786
  */
697
- function turnItem(item) {
698
- const turned = {
699
- ...item,
700
- w: item.h,
701
- h: item.w
702
- };
703
- if (item.ports) turned.ports = item.ports.map((p) => turnPort(p, item.h));
704
- if (item.shape) turned.shape = turnRegion(item.shape, item.h);
705
- if (item.margin !== void 0 && typeof item.margin !== "number") {
706
- const m = item.margin;
707
- turned.margin = {
708
- top: m.left,
709
- right: m.top,
710
- bottom: m.right,
711
- left: m.bottom
712
- };
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);
713
804
  }
714
- return turned;
715
- }
716
- // src/sha256.ts
717
- /** The round constants of SHA-256. */
718
- const K = new Uint32Array([
719
- 1116352408,
720
- 1899447441,
721
- 3049323471,
722
- 3921009573,
723
- 961987163,
724
- 1508970993,
725
- 2453635748,
726
- 2870763221,
727
- 3624381080,
728
- 310598401,
729
- 607225278,
730
- 1426881987,
731
- 1925078388,
732
- 2162078206,
733
- 2614888103,
734
- 3248222580,
735
- 3835390401,
736
- 4022224774,
737
- 264347078,
738
- 604807628,
739
- 770255983,
740
- 1249150122,
741
- 1555081692,
742
- 1996064986,
743
- 2554220882,
744
- 2821834349,
745
- 2952996808,
746
- 3210313671,
747
- 3336571891,
748
- 3584528711,
749
- 113926993,
750
- 338241895,
751
- 666307205,
752
- 773529912,
753
- 1294757372,
754
- 1396182291,
755
- 1695183700,
756
- 1986661051,
757
- 2177026350,
758
- 2456956037,
759
- 2730485921,
760
- 2820302411,
761
- 3259730800,
762
- 3345764771,
763
- 3516065817,
764
- 3600352804,
765
- 4094571909,
766
- 275423344,
767
- 430227734,
768
- 506948616,
769
- 659060556,
770
- 883997877,
771
- 958139571,
772
- 1322822218,
773
- 1537002063,
774
- 1747873779,
775
- 1955562222,
776
- 2024104815,
777
- 2227730452,
778
- 2361852424,
779
- 2428436474,
780
- 2756734187,
781
- 3204031479,
782
- 3329325298
783
- ]);
784
- /** The first hash value of SHA-256. */
785
- const H0 = [
786
- 1779033703,
787
- 3144134277,
788
- 1013904242,
789
- 2773480762,
790
- 1359893119,
791
- 2600822924,
792
- 528734635,
793
- 1541459225
794
- ];
795
- /** This function turns a 32-bit word to the right by `n` bits. */
796
- const rotr = (x, n) => x >>> n | x << 32 - n;
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
+ }
797
850
  /**
798
- * This function gives the UTF-8 bytes of a string. A lone surrogate becomes U+FFFD, as with `TextEncoder`. The
799
- * 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.
800
853
  */
801
- function utf8(text) {
802
- const out = [];
803
- for (let i = 0; i < text.length; i++) {
804
- let c = text.charCodeAt(i);
805
- if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
806
- const next = text.charCodeAt(i + 1);
807
- if (next >= 56320 && next <= 57343) {
808
- c = 65536 + (c - 55296 << 10) + (next - 56320);
809
- 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;
810
974
  }
811
975
  }
812
- if (c >= 55296 && c <= 57343) c = 65533;
813
- if (c < 128) out.push(c);
814
- else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
815
- else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
816
- else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
817
976
  }
818
- return Uint8Array.from(out);
977
+ return {
978
+ x: bestX,
979
+ y: bestY
980
+ };
819
981
  }
820
- /** This function gives the SHA-256 of the UTF-8 bytes of a string, as 64 lowercase hex characters. */
821
- function sha256Hex(text) {
822
- const bytes = utf8(text);
823
- const length = bytes.length;
824
- const padded = new Uint8Array(((length + 9 + 63) / 64 | 0) * 64);
825
- padded.set(bytes);
826
- padded[length] = 128;
827
- const view = new DataView(padded.buffer);
828
- const bits = length * 8;
829
- view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
830
- view.setUint32(padded.length - 4, bits >>> 0);
831
- const hash = [...H0];
832
- const w = /* @__PURE__ */ new Uint32Array(64);
833
- for (let block = 0; block < padded.length; block += 64) {
834
- for (let t = 0; t < 16; t++) w[t] = view.getUint32(block + t * 4);
835
- for (let t = 16; t < 64; t++) {
836
- const a = w[t - 15];
837
- const b = w[t - 2];
838
- const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
839
- const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
840
- 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
+ }
841
1005
  }
842
- let [a, b, c, d, e, f, g, h] = hash;
843
- for (let t = 0; t < 64; t++) {
844
- const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
845
- const ch = e & f ^ ~e & g;
846
- const t1 = h + s1 + ch + K[t] + w[t] | 0;
847
- const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
848
- h = g;
849
- g = f;
850
- f = e;
851
- e = d + t1 | 0;
852
- d = c;
853
- c = b;
854
- b = a;
855
- 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;
1055
+ }
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);
1061
+ }
1062
+ return outer;
1063
+ }
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;
1127
+ }
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);
856
1173
  }
857
- hash[0] = hash[0] + a | 0;
858
- hash[1] = hash[1] + b | 0;
859
- hash[2] = hash[2] + c | 0;
860
- hash[3] = hash[3] + d | 0;
861
- hash[4] = hash[4] + e | 0;
862
- hash[5] = hash[5] + f | 0;
863
- hash[6] = hash[6] + g | 0;
864
- hash[7] = hash[7] + h | 0;
865
1174
  }
866
- return hash.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
867
- }
868
- // src/layout-input.ts
869
- /** This function gives the `fnv1a32` hash of the UTF-8 bytes of a string, as `docs/FORMAT.md` gives it. */
870
- function fnv1a32(text) {
871
- let h = 2166136261;
872
- for (const b of utf8(text)) h = Math.imul(h ^ b, 16777619) >>> 0;
873
- return h;
874
- }
875
- /** This function rounds the numbers of a file to 3 decimals. A half goes up, and the value `-0` becomes 0. */
876
- function round3(x) {
877
- const v = Math.round(x * 1e3) / 1e3;
878
- return Object.is(v, -0) ? 0 : v;
1175
+ return true;
879
1176
  }
880
1177
  /**
881
- * This function gives the canonical form of a value: sorted keys, no whitespace and the numbers of `JSON.stringify`
882
- * 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.
883
1184
  */
884
- function canonicalJson(value) {
885
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
886
- 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(",")}}`;
887
- return JSON.stringify(typeof value === "number" ? round3(value) : value);
888
- }
889
- /** This function gives the hash of an input file: the first 16 hex characters of the SHA-256 of its canonical form without `hash`. */
890
- function hashInput(file) {
891
- const rest = { ...file };
892
- delete rest.hash;
893
- return sha256Hex(canonicalJson(rest)).slice(0, 16);
894
- }
895
- /** 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`. */
896
- function countCell(count) {
897
- if (count <= 3) return "1";
898
- if (count <= 31) return "10";
899
- if (count <= 316) return "100";
900
- if (count <= 3162) return "1k";
901
- return "10k";
902
- }
903
- /** This function gives the packing input of a file. The boxes become the items, and the edges go. */
904
- function toPackInput(file) {
905
- const input = {
906
- container: file.container,
907
- items: file.boxes
908
- };
909
- if (file.obstacles !== void 0) input.obstacles = file.obstacles;
910
- if (file.gap !== void 0) input.gap = file.gap;
911
- return input;
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;
912
1192
  }
913
- // src/measures.ts
914
- const spec = (id, family, unit, direction, min, max, guard) => ({
915
- id,
916
- family,
917
- unit,
918
- direction,
919
- min,
920
- max,
921
- ...guard
922
- });
923
1193
  /**
924
- * The facts of each measure, by id. `docs/MEASURES.md` gives the full definitions. This record is the source of the floor
925
- * and the tolerance of each guard of the packer.
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.
926
1202
  */
927
- const MEASURE_SPECS = Object.freeze({
928
- "pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity),
929
- "pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1),
930
- "pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1),
931
- "pack.aspect": spec("pack.aspect", "aspect", "ratio", "lower", 1, Infinity),
932
- "pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity, {
933
- floor: 0,
934
- tol: .01
935
- }),
936
- "pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, {
937
- floor: 0,
938
- tol: 0
939
- }),
940
- "pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity),
941
- "pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1),
942
- "pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, {
943
- floor: 0,
944
- tol: 0
945
- }),
946
- "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, {
947
- floor: 0,
948
- tol: .02
949
- }),
950
- "pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, {
951
- floor: 0,
952
- tol: .005
953
- }),
954
- "pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, {
955
- floor: 0,
956
- tol: .01
957
- }),
958
- "pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1),
959
- "pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity),
960
- "pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity)
961
- });
962
- /** This function gives the smallest rectangle around the boxes. No box gives a rectangle of size 0 at 0, 0. */
963
- function hullOf(boxes) {
964
- if (boxes.length === 0) return {
965
- x: 0,
966
- y: 0,
967
- w: 0,
968
- h: 0
969
- };
970
- let left = Infinity;
971
- let top = Infinity;
972
- let right = -Infinity;
973
- let bottom = -Infinity;
974
- for (const b of boxes) {
975
- left = Math.min(left, b.x);
976
- top = Math.min(top, b.y);
977
- right = Math.max(right, b.x + b.w);
978
- bottom = Math.max(bottom, b.y + b.h);
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);
979
1213
  }
980
- return {
981
- x: left,
982
- y: top,
983
- w: right - left,
984
- h: bottom - top
985
- };
986
- }
987
- /**
988
- * This function puts the boxes in rows. It sorts the boxes by their top, and keeps the order of the list for equal tops.
989
- * A box joins the last row when its top is within `tolerance` of the top of the first box of that row.
990
- */
991
- function rowsOf$1(boxes, tolerance) {
992
- const rows = [];
993
- for (const b of boxes.toSorted((p, q) => p.y - q.y)) {
994
- const row = rows.at(-1);
995
- if (row && Math.abs(b.y - row.top) <= tolerance) {
996
- row.right = Math.max(row.right, b.x + b.w);
997
- row.count += 1;
998
- } else rows.push({
999
- top: b.y,
1000
- right: b.x + b.w,
1001
- count: 1
1002
- });
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);
1003
1225
  }
1004
- return rows;
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));
1005
1233
  }
1006
1234
  /**
1007
- * 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
1008
- * item after the turn. The list has no item without a placement, and no item with a width or a height of 0.
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.
1009
1243
  */
1010
- function boxesOf$1(input, output) {
1011
- const placements = /* @__PURE__ */ new Map();
1012
- for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
1013
- const boxes = [];
1014
- for (const [index, item] of input.items.entries()) {
1015
- const p = placements.get(item.id);
1016
- if (p === void 0 || !(item.w > 0 && item.h > 0)) continue;
1017
- const turned = p.turn === 90;
1018
- boxes.push({
1019
- x: p.x,
1020
- y: p.y,
1021
- w: turned ? item.h : item.w,
1022
- h: turned ? item.w : item.h,
1023
- rank: item.rank ?? index
1024
- });
1244
+ function packFree(rects, outer = new Rect(), options = {}) {
1245
+ const target = options.aspect ?? 1;
1246
+ const weight = options.aspectWeight ?? 1;
1247
+ const samples = Math.max(2, Math.floor(options.samples ?? 48));
1248
+ const ox = outer.position.x;
1249
+ const oy = outer.position.y;
1250
+ let itemArea = 0;
1251
+ let minWidth = 0;
1252
+ let rowWidth = 0;
1253
+ for (const r of rects) {
1254
+ if (r.locked) {
1255
+ minWidth = Math.max(minWidth, r.right() - ox);
1256
+ continue;
1257
+ }
1258
+ if (!(r.size.x > 0 && r.size.y > 0)) continue;
1259
+ itemArea += r.area();
1260
+ minWidth = Math.max(minWidth, r.size.x);
1261
+ rowWidth += r.size.x;
1262
+ }
1263
+ for (const r of rects) if (r.locked) itemArea += r.area();
1264
+ const maxWidth = Math.max(minWidth, rowWidth);
1265
+ const tried = [];
1266
+ const run = (width) => {
1267
+ Rect.resetPacked(rects);
1268
+ const box = new Rect();
1269
+ box.position.setTo(outer.position);
1270
+ box.size.set(width, 0);
1271
+ box.fixedWidth = true;
1272
+ pack(rects, box, options);
1273
+ const placed = Rect.packed(rects);
1274
+ const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);
1275
+ const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);
1276
+ const area = contentWidth * contentHeight;
1277
+ const t = {
1278
+ width,
1279
+ contentWidth,
1280
+ contentHeight,
1281
+ objective: area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0
1282
+ };
1283
+ tried.push(t);
1284
+ return t;
1285
+ };
1286
+ const widths = [];
1287
+ if (maxWidth > 0) {
1288
+ const ratio = maxWidth / minWidth;
1289
+ for (let i = 0; i < samples; i++) {
1290
+ const w = minWidth * ratio ** (i / (samples - 1));
1291
+ const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));
1292
+ if (widths.at(-1) !== rounded) widths.push(rounded);
1293
+ }
1294
+ }
1295
+ let best = void 0;
1296
+ let bestIndex = 0;
1297
+ for (const [i, w] of widths.entries()) {
1298
+ const t = run(w);
1299
+ if (best === void 0 || t.objective < best.objective) {
1300
+ best = t;
1301
+ bestIndex = i;
1302
+ }
1303
+ }
1304
+ if (widths.length > 2) {
1305
+ const low = widths[Math.max(0, bestIndex - 1)];
1306
+ const high = widths[Math.min(widths.length - 1, bestIndex + 1)];
1307
+ const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));
1308
+ for (let i = 1; i <= steps; i++) {
1309
+ const w = Math.round(low + (high - low) * i / (steps + 1));
1310
+ if (widths.includes(w)) continue;
1311
+ const t = run(w);
1312
+ if (best === void 0 || t.objective < best.objective) best = t;
1313
+ }
1025
1314
  }
1026
- 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
+ };
1027
1338
  }
1028
1339
  /**
1029
- * This function tells if box `a` comes before box `b` in the reading order. Two boxes whose heights overlap by more than
1030
- * `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.
1031
1342
  */
1032
- function readsBefore(a, b) {
1033
- if (Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > .001) return a.x + RECORD_EPS < b.x;
1034
- 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
+ };
1035
1353
  }
1036
- /** This function sums one measure over the stages of a trace. A trace without the measure gives 0. */
1037
- function traceSum(output, id) {
1038
- let sum = 0;
1039
- for (const stage of output.trace) sum += stage.measures[id] ?? 0;
1040
- 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));
1041
1363
  }
1042
1364
  /**
1043
- * This function gives the measures of one layout, by the ids of `docs/MEASURES.md`. The values are not rounded. A
1044
- * record rounds them with `roundMeasures`.
1045
- *
1046
- * - The boxes are the placed items with a width and a height above 0. The hull is the smallest rectangle around them.
1047
- * - An empty layout gives the area 0, the aspect 1 and the aspect error 0. Its fill is 0 and its whitespace is 1.
1048
- * - 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.
1049
1369
  *
1050
1370
  * @example
1051
1371
  * ```ts
1052
- * const m = measure(input, packWith("shelf", input));
1053
- * m["pack.fill"]; // the area of the boxes divided by the area of the hull
1372
+ * const moved = pushCascade(dropped, cards, { gap: 8 });
1054
1373
  * ```
1055
1374
  */
1056
- function measure(input, output, options = {}) {
1057
- const boxes = boxesOf$1(input, output);
1058
- const hull = hullOf(boxes);
1059
- const hullArea = hull.w * hull.h;
1060
- const boxArea = boxes.reduce((sum, b) => sum + b.w * b.h, 0);
1061
- const fill = hullArea > 0 ? boxArea / hullArea : 0;
1062
- const empty = boxes.length === 0;
1063
- const target = input.container.aspect ?? 1;
1064
- let pairs = 0;
1065
- let inversions = 0;
1066
- for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
1067
- const a = boxes[i];
1068
- const b = boxes[j];
1069
- if (a.rank === b.rank) continue;
1070
- pairs += 1;
1071
- const first = a.rank < b.rank ? a : b;
1072
- if (readsBefore(first === a ? b : a, first)) inversions += 1;
1073
- }
1074
- const byRank = boxes.toSorted((a, b) => a.rank - b.rank);
1075
- let breaks = 0;
1076
- for (let k = 1; k < byRank.length; k++) {
1077
- const prev = byRank[k - 1];
1078
- const next = byRank[k];
1079
- if (prev.rank !== next.rank && readsBefore(next, prev)) breaks += 1;
1080
- }
1081
- const rights = rowsOf$1(boxes, options.rowTolerance ?? quantileOrZero(boxes.map((b) => b.h), .5) / 2).map((r) => r.right);
1082
- const spread = rights.length > 0 ? Math.max(...rights) - Math.min(...rights) : 0;
1083
- return {
1084
- "pack.area": hullArea,
1085
- "pack.fill": fill,
1086
- "pack.whitespace": 1 - fill,
1087
- "pack.aspect": empty ? 1 : Math.max(hull.w / hull.h, hull.h / hull.w),
1088
- "pack.aspectError": empty ? 0 : aspectError(hull.w, hull.h, target),
1089
- "pack.order.inversions": pairs > 0 ? inversions / pairs : 0,
1090
- "pack.order.breaks": breaks,
1091
- "pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
1092
- "pack.overflow": new Set(output.overflow).size,
1093
- "pack.search.widths": traceSum(output, "pack.search.widths"),
1094
- "pack.search.ms": traceSum(output, "pack.search.ms")
1095
- };
1096
- }
1097
- /**
1098
- * This function gives the stability measures of a change pair: the run on the base input and the run on the changed
1099
- * input. The items match by id. The values are not rounded.
1100
- *
1101
- * - An item stays when both inputs have it and both outputs place it. The added and the removed items do not count.
1102
- * - A staying item moved when its `x` or its `y` changed by more than `RECORD_EPS`, or when its turn changed.
1103
- * - The distance of an item is the length of the move of its top-left corner. The unit is the width `W`.
1104
- * - `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.
1105
- * - No staying item gives 0 for each measure.
1106
- */
1107
- function measureChange(baseInput, baseOutput, changeInput, changeOutput) {
1108
- const placed = (input, output) => {
1109
- const ids = new Set(input.items.map((i) => i.id));
1110
- const map = /* @__PURE__ */ new Map();
1111
- for (const p of output.placements) if (ids.has(p.id) && !map.has(p.id)) map.set(p.id, p);
1112
- return map;
1113
- };
1114
- const before = placed(baseInput, baseOutput);
1115
- const after = placed(changeInput, changeOutput);
1116
- const width = Math.max(baseOutput.size.w, changeOutput.size.w);
1117
- const unit = width > 0 ? width : 1;
1118
- const distances = [];
1119
- let moved = 0;
1120
- for (const [id, p] of before) {
1121
- const q = after.get(id);
1122
- if (q === void 0) continue;
1123
- const dx = q.x - p.x;
1124
- const dy = q.y - p.y;
1125
- if (Math.abs(dx) > .001 || Math.abs(dy) > .001 || (p.turn ?? 0) !== (q.turn ?? 0)) moved += 1;
1126
- 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
+ }
1127
1398
  }
1128
- if (distances.length === 0) return {
1129
- "pack.stability.moved": 0,
1130
- "pack.stability.mean": 0,
1131
- "pack.stability.q95": 0
1132
- };
1133
- return {
1134
- "pack.stability.moved": moved / distances.length,
1135
- "pack.stability.mean": distances.reduce((sum, d) => sum + d, 0) / distances.length,
1136
- "pack.stability.q95": quantile(distances, .95)
1137
- };
1399
+ return [...moved];
1138
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;
1139
1482
  /**
1140
- * This function gives the measure `pack.deterministic` of two runs with the same input, options and seed. It gives
1141
- * `true` when the runs have the same placements, overflow, size and moved obstacles.
1142
- *
1143
- * - The placements match by id, so their order in the list does not count. The numbers must be equal.
1144
- * - 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.
1145
1485
  */
1146
- function isDeterministic(first, second) {
1147
- const key = (list) => new Map(list.map((e) => [e.id, e]));
1148
- const samePlacements = (a, b) => {
1149
- if (a.length !== b.length) return false;
1150
- const other = key(b);
1151
- return a.every((p) => {
1152
- const q = other.get(p.id);
1153
- return q !== void 0 && q.x === p.x && q.y === p.y && (q.turn ?? 0) === (p.turn ?? 0);
1154
- });
1155
- };
1156
- const sameMoved = (a, b) => {
1157
- if (a.length !== b.length) return false;
1158
- const other = key(b);
1159
- return a.every((m) => {
1160
- const n = other.get(m.id);
1161
- return n !== void 0 && n.x === m.x && n.y === m.y;
1162
- });
1163
- };
1164
- const overflowA = new Set(first.overflow);
1165
- const overflowB = new Set(second.overflow);
1166
- 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);
1167
1504
  }
1168
- /**
1169
- * This function rounds each number of a record of measures with `round3`, as `docs/FORMAT.md` tells. A half goes up,
1170
- * and `-0` becomes 0. The function keeps the other values, and it does not change its argument.
1171
- */
1172
- function roundMeasures(measures) {
1173
- const out = {};
1174
- for (const [id, value] of Object.entries(measures)) out[id] = typeof value === "number" ? round3(value) : value;
1175
- 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("");
1176
1552
  }
1177
- /**
1178
- * This function gives the paired ratio of a measure on one input: the value of the candidate against the value of the
1179
- * current default. A ratio above 1 is worse, in the direction of the measure.
1180
- *
1181
- * - For `lower`, the ratio is `candidate / current`. For `higher`, it is `current / candidate`.
1182
- * - When both values are 0, the ratio is 1. When only the divisor is 0, the ratio is `Infinity`.
1183
- * - The values are 0 or more. A negative value or `NaN` throws a `RangeError`.
1184
- */
1185
- function pairedRatio(candidate, current, direction) {
1186
- if (!(candidate >= 0 && current >= 0)) throw new RangeError("A paired ratio needs two values of 0 or more.");
1187
- const top = direction === "lower" ? candidate : current;
1188
- const bottom = direction === "lower" ? current : candidate;
1189
- if (bottom === 0) return top === 0 ? 1 : Infinity;
1190
- 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;
1191
1564
  }
1192
1565
  /**
1193
- * This function gives the quantile `q` of a list of numbers, with linear interpolation between the sorted values. The
1194
- * 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.
1195
1568
  */
1196
- function quantile(values, q) {
1197
- if (values.length === 0) throw new RangeError("A quantile needs one value or more.");
1198
- if (!(q >= 0 && q <= 1)) throw new RangeError("A quantile needs a q from 0 to 1.");
1199
- const sorted = values.toSorted((a, b) => a - b);
1200
- const at = (sorted.length - 1) * q;
1201
- const low = Math.floor(at);
1202
- const part = at - low;
1203
- const a = sorted[low];
1204
- if (part === 0) return a;
1205
- const b = sorted[low + 1];
1206
- if (a === b) return a;
1207
- return a + part * (b - a);
1208
- }
1209
- /** This function gives the quantile `q` of a list, and 0 for an empty list. */
1210
- function quantileOrZero(values, q) {
1211
- 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);
1212
1573
  }
1213
- /** This function gives the median of the paired ratios of a corpus: the quantile 0.5. It is not a ratio of two medians. */
1214
- function medianRatio(ratios) {
1215
- 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);
1216
1578
  }
1217
- /** This function gives the share of the paired ratios above a limit. An empty list throws a `RangeError`. */
1218
- function tailShare(ratios, limit) {
1219
- if (ratios.length === 0) throw new RangeError("A tail share needs one ratio or more.");
1220
- 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";
1221
1586
  }
1222
- // src/metrics.ts
1223
1587
  /**
1224
- * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.
1225
- * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.
1226
- *
1227
- * @example
1228
- * ```ts
1229
- * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
1230
- * ```
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.
1231
1590
  */
1232
- function packingMetrics(rects, options = {}) {
1233
- const tolerance = options.rowTolerance ?? 40;
1234
- const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0).map((r) => ({
1235
- x: r.position.x,
1236
- y: r.position.y,
1237
- w: r.size.x,
1238
- h: r.size.y
1239
- }));
1240
- if (boxes.length === 0) return {
1241
- count: 0,
1242
- hull: {
1243
- width: 0,
1244
- height: 0
1245
- },
1246
- hullAspect: 0,
1247
- rows: [],
1248
- raggedness: 0,
1249
- utilization: 0,
1250
- 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 }
1251
1597
  };
1252
- const box = hullOf(boxes);
1253
- const hull = {
1254
- width: box.w,
1255
- 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
1256
1655
  };
1257
- const rows = rowsOf$1(boxes, tolerance);
1258
- const rights = rows.map((row) => row.right);
1259
- const widths = boxes.map((r) => r.w).toSorted((a, b) => a - b);
1260
- const median = widths[Math.floor(widths.length / 2)] ?? 0;
1261
- const widest = widths.at(-1) ?? 0;
1262
- 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
+ }
1263
1666
  return {
1264
- count: boxes.length,
1265
- hull,
1266
- hullAspect: hull.height > 0 ? hull.width / hull.height : 0,
1267
- rows: rows.map((row) => row.count),
1268
- raggedness: Math.max(...rights) - Math.min(...rights),
1269
- utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,
1270
- widestOverMedian: median > 0 ? widest / median : 0
1667
+ x: left,
1668
+ y: top,
1669
+ w: right - left,
1670
+ h: bottom - top
1271
1671
  };
1272
1672
  }
1273
- // src/strategy.ts
1274
1673
  /**
1275
- * The error of a strategy for an input outside its cells. The benchmark records this result. It is not a failure of the
1276
- * 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.
1277
1676
  */
1278
- var UnsupportedCell = class extends Error {
1279
- /** The dimension of the taxonomy. */
1280
- dimension;
1281
- /** The value of the input that the strategy does not support. */
1282
- value;
1283
- constructor(dimension, value) {
1284
- super(`The strategy does not support the value "${value}" of the dimension "${dimension}".`);
1285
- this.name = "UnsupportedCell";
1286
- this.dimension = dimension;
1287
- 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
+ });
1288
1689
  }
1289
- };
1290
- /** This function gives a generator of random numbers in `[0, 1)`: `mulberry32` of `docs/FORMAT.md`. */
1291
- function mulberry32(seed) {
1292
- let s = seed >>> 0;
1293
- return () => {
1294
- s = s + 1831565813 | 0;
1295
- let t = Math.imul(s ^ s >>> 15, 1 | s);
1296
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1297
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
1298
- };
1299
- }
1300
- /** This function makes the context of one packing from a seed. */
1301
- function createContext(seed = 0, more = {}) {
1302
- return {
1303
- ...more,
1304
- seed,
1305
- rng: mulberry32(seed)
1306
- };
1690
+ return rows;
1307
1691
  }
1308
- /** This function tells if a polygon ring is convex. A ring with fewer than 3 points is convex. */
1309
- function isConvex(points) {
1310
- let sign = 0;
1311
- const n = points.length;
1312
- for (let i = 0; i < n; i++) {
1313
- const [ax, ay] = points[i];
1314
- const [bx, by] = points[(i + 1) % n];
1315
- const [cx, cy] = points[(i + 2) % n];
1316
- const cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx);
1317
- if (cross === 0) continue;
1318
- if (sign === 0) sign = Math.sign(cross);
1319
- 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
+ });
1320
1711
  }
1321
- return true;
1712
+ return boxes;
1322
1713
  }
1323
- /** This function gives the value of `containerShape` of one region. */
1324
- function shapeOf$1(region) {
1325
- switch (region.kind) {
1326
- case "rect":
1327
- case "circle":
1328
- case "ellipse": return region.kind;
1329
- case "polygon":
1330
- if (region.holes && region.holes.length > 0) return "holes";
1331
- return isConvex(region.points) ? "convex" : "polygon";
1332
- }
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;
1333
1721
  }
1334
- /** This function gives the value of `itemShape` of one item shape. An ellipse is convex. */
1335
- function itemShapeOf(region) {
1336
- if (region === void 0) return "rect";
1337
- switch (region.kind) {
1338
- case "rect":
1339
- case "circle": return region.kind;
1340
- case "ellipse": return "convex";
1341
- case "polygon": return !region.holes?.length && isConvex(region.points) ? "convex" : "polygon";
1342
- }
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;
1343
1727
  }
1344
1728
  /**
1345
- * 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`.
1346
1731
  *
1347
- * - `containerSize` comes from `fixed`. The value `none` gives `freeArea` when `aspectWeight` is 0, and `freeAspect` if not.
1348
- * - `containerShape` comes from the shape. A rectangle container with an obstacle of another region gives `holes`.
1349
- * - `obstacleMove` is the hardest value of the obstacles and the locked items: `costly`, then `free`, then `fixed`.
1350
- * - `order` is `soft` when an item has a rank, and `free` if not. The input does not tell a hard order.
1351
- * - `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
+ * ```
1352
1741
  */
1353
- function cellOf(input) {
1354
- const { container, items } = input;
1355
- const obstacles = input.obstacles ?? [];
1356
- const containerSize = {
1357
- size: "fixed",
1358
- width: "freeHeight",
1359
- height: "freeWidth",
1360
- none: container.aspectWeight === 0 ? "freeArea" : "freeAspect"
1361
- }[container.fixed];
1362
- let containerShape = container.shape ? shapeOf$1(container.shape) : "rect";
1363
- if (containerShape === "rect" && obstacles.some((o) => o.region.kind !== "rect")) containerShape = "holes";
1364
- let obstacleMove = items.some((i) => i.locked === true) ? "fixed" : void 0;
1365
- for (const o of obstacles) {
1366
- const move = o.move === void 0 ? "fixed" : o.move.cost > 0 ? "costly" : "free";
1367
- if (move === "costly" || obstacleMove === void 0 || move === "free" && obstacleMove === "fixed") obstacleMove = move;
1368
- }
1369
- const shapes = new Set(items.map((i) => itemShapeOf(i.shape)));
1370
- const itemShape = shapes.size > 1 ? "mixed" : [...shapes][0] ?? "rect";
1371
- const cell = {
1372
- containerSize,
1373
- containerShape,
1374
- itemShape,
1375
- rotation: items.some((i) => i.turnable === true) ? "quarter" : "none",
1376
- order: items.some((i) => i.rank !== void 0) ? "soft" : "free",
1377
- groups: items.some((i) => i.parent !== void 0) ? "nested" : items.some((i) => i.group !== void 0) ? "flat" : "none"
1378
- };
1379
- if (obstacleMove !== void 0) cell.obstacleMove = obstacleMove;
1380
- return cell;
1381
- }
1382
- /** This function gives the first dimension of a cell that `supports` does not hold, or `undefined` when it holds each one. */
1383
- function unsupportedOf(supports, cell) {
1384
- for (const dimension of Object.keys(supports)) {
1385
- const value = cell[dimension];
1386
- if (value === void 0) continue;
1387
- if (!supports[dimension].includes(value)) return {
1388
- dimension,
1389
- value
1390
- };
1391
- }
1392
- }
1393
- /** This function tells if `supports` holds each value of a cell. A dimension that the cell does not have is held. */
1394
- function supportsCell(supports, cell) {
1395
- return unsupportedOf(supports, cell) === void 0;
1396
- }
1397
- /** This function throws `UnsupportedCell` for the first value of the cell of an input that `supports` does not hold. */
1398
- function assertSupported(supports, input) {
1399
- const miss = unsupportedOf(supports, cellOf(input));
1400
- if (miss) throw new UnsupportedCell(miss.dimension, miss.value);
1401
- }
1402
- // src/frame.ts
1403
- const clock = globalThis.performance;
1404
- /** This function gives the time in milliseconds. */
1405
- function now() {
1406
- return clock ? clock.now() : Date.now();
1407
- }
1408
- /** This function gives the bounding box of a region. */
1409
- function boundsOf(region) {
1410
- switch (region.kind) {
1411
- case "rect": return {
1412
- x: region.x,
1413
- y: region.y,
1414
- w: region.w,
1415
- h: region.h
1416
- };
1417
- case "circle": return {
1418
- x: region.cx - region.r,
1419
- y: region.cy - region.r,
1420
- w: 2 * region.r,
1421
- h: 2 * region.r
1422
- };
1423
- case "ellipse": return {
1424
- x: region.cx - region.rx,
1425
- y: region.cy - region.ry,
1426
- w: 2 * region.rx,
1427
- h: 2 * region.ry
1428
- };
1429
- case "polygon": {
1430
- const xs = region.points.map((p) => p[0]);
1431
- const ys = region.points.map((p) => p[1]);
1432
- const x = Math.min(...xs);
1433
- const y = Math.min(...ys);
1434
- return {
1435
- x,
1436
- y,
1437
- w: Math.max(...xs) - x,
1438
- h: Math.max(...ys) - y
1439
- };
1440
- }
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;
1441
1759
  }
1442
- }
1443
- /** This function gives the rectangle of a container. A rectangle shape gives the origin and the sizes of the fixed sides. */
1444
- function containerBounds(input) {
1445
- const { container } = input;
1446
- const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
1447
- const fixedW = container.fixed === "size" || container.fixed === "width";
1448
- const fixedH = container.fixed === "size" || container.fixed === "height";
1449
- const w = shape?.w ?? container.w;
1450
- const h = shape?.h ?? container.h;
1451
- if (fixedW && w === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a width.`);
1452
- if (fixedH && h === void 0) throw new TypeError(`A container with fixed "${container.fixed}" needs a height.`);
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;
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;
1453
1769
  return {
1454
- x: shape?.x ?? 0,
1455
- y: shape?.y ?? 0,
1456
- w: fixedW ? w : Infinity,
1457
- h: fixedH ? h : Infinity
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")
1458
1781
  };
1459
1782
  }
1460
- /** This function gives the items in the order of their rank. The index in the array is the rank of an item without one. */
1461
- function byRank(items) {
1462
- return items.map((item, index) => ({
1463
- item,
1464
- rank: item.rank ?? index
1465
- })).toSorted((a, b) => a.rank - b.rank).map((e) => e.item);
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
+ };
1819
+ return {
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)
1823
+ };
1466
1824
  }
1467
1825
  /**
1468
- * This function makes the frame of an input. A locked item with a position and an obstacle without `move` are locked
1469
- * 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.
1470
1831
  */
1471
- function buildFrame(input, movable = false) {
1472
- const bounds = containerBounds(input);
1473
- const inset = input.container.inset ?? 0;
1474
- const half = (input.gap ?? 0) / 2;
1475
- const ox = bounds.x + inset - half;
1476
- const oy = bounds.y + inset - half;
1477
- const w = bounds.w - 2 * inset + 2 * half;
1478
- const h = bounds.h - 2 * inset + 2 * half;
1479
- const entries = [];
1480
- const tooLarge = [];
1481
- for (const item of byRank(input.items)) {
1482
- const m = marginOf(item);
1483
- const rect = new Rect();
1484
- rect.size.set(item.w + m.left + m.right + 2 * half, item.h + m.top + m.bottom + 2 * half);
1485
- const dx = m.left + half;
1486
- const dy = m.top + half;
1487
- if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
1488
- rect.locked = true;
1489
- rect.position.set(item.x - dx - ox, item.y - dy - oy);
1490
- } else if (rect.size.x > w + 1e-6 || rect.size.y > h + 1e-6) {
1491
- tooLarge.push(item.id);
1492
- continue;
1493
- }
1494
- entries.push({
1495
- rect,
1496
- item,
1497
- dx,
1498
- 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);
1499
1840
  });
1500
- }
1501
- for (const obstacle of input.obstacles ?? []) {
1502
- const b = boundsOf(obstacle.region);
1503
- const rect = new Rect();
1504
- rect.size.set(b.w + 2 * half, b.h + 2 * half);
1505
- rect.position.set(b.x - half - ox, b.y - half - oy);
1506
- rect.locked = !(movable && obstacle.move !== void 0);
1507
- entries.push({
1508
- rect,
1509
- obstacle,
1510
- dx: half,
1511
- 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;
1512
1848
  });
1513
- }
1514
- return {
1515
- ox,
1516
- oy,
1517
- w,
1518
- h,
1519
- bounds,
1520
- inset,
1521
- entries,
1522
- tooLarge
1523
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);
1524
1853
  }
1525
- /** This function gives the outer rectangle of 0.1 for a frame. A free height starts at 0 and grows. */
1526
- function outerOf(frame) {
1527
- const outer = new Rect();
1528
- const fixedSize = Number.isFinite(frame.h);
1529
- outer.size.set(frame.w, fixedSize ? frame.h : 0);
1530
- outer.fixedWidth = true;
1531
- outer.fixedSize = fixedSize;
1532
- 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;
1533
1862
  }
1534
- /** This function gives the rectangles of a frame, in the order of the entries. */
1535
- function rectsOf(frame) {
1536
- 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;
1537
1877
  }
1538
1878
  /**
1539
- * This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
1540
- * 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`.
1541
1881
  */
1542
- function readFrame(input, frame, trace, moved = []) {
1543
- const placements = [];
1544
- const overflow = [...frame.tooLarge];
1545
- for (const e of frame.entries) {
1546
- const item = e.item;
1547
- if (item === void 0) continue;
1548
- if (item.locked === true && item.x !== void 0 && item.y !== void 0) placements.push({
1549
- id: item.id,
1550
- x: item.x,
1551
- y: item.y
1552
- });
1553
- else if (e.rect.wasPacked) placements.push({
1554
- id: item.id,
1555
- x: e.rect.position.x + e.dx + frame.ox,
1556
- y: e.rect.position.y + e.dy + frame.oy
1557
- });
1558
- else overflow.push(item.id);
1559
- }
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);
1560
1949
  return {
1561
- placements,
1562
- overflow,
1563
- size: sizeOf(input, frame, placements, moved),
1564
- moved,
1565
- 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
1566
1957
  };
1567
1958
  }
1959
+ // src/strategy.ts
1568
1960
  /**
1569
- * This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
1570
- * 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.
1571
1963
  */
1572
- function sizeOf(input, frame, placements, moved) {
1573
- const items = new Map(input.items.map((i) => [i.id, i]));
1574
- let right = 0;
1575
- let bottom = 0;
1576
- for (const p of placements) {
1577
- const item = items.get(p.id);
1578
- const m = marginOf(item);
1579
- right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
1580
- bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
1581
- }
1582
- const movedById = new Map(moved.map((m) => [m.id, m]));
1583
- for (const o of input.obstacles ?? []) {
1584
- const b = boundsOf(o.region);
1585
- const at = movedById.get(o.id) ?? b;
1586
- right = Math.max(right, at.x + b.w - frame.bounds.x);
1587
- 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;
1588
1974
  }
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;
1984
+ };
1985
+ }
1986
+ /** This function makes the context of one packing from a seed. */
1987
+ function createContext(seed = 0, more = {}) {
1589
1988
  return {
1590
- w: Number.isFinite(frame.bounds.w) ? frame.bounds.w : right,
1591
- h: Number.isFinite(frame.bounds.h) ? frame.bounds.h : bottom
1989
+ ...more,
1990
+ seed,
1991
+ rng: mulberry32(seed)
1592
1992
  };
1593
1993
  }
1594
- /** This function gives a region with the axes changed: x for y, and w for h. */
1595
- function transposeRegion(region) {
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;
2008
+ }
2009
+ /** This function gives the value of `containerShape` of one region. */
2010
+ function shapeOf$1(region) {
1596
2011
  switch (region.kind) {
1597
- case "rect": return {
1598
- kind: "rect",
1599
- x: region.y,
1600
- y: region.x,
1601
- w: region.h,
1602
- h: region.w
1603
- };
1604
- case "circle": return {
1605
- kind: "circle",
1606
- cx: region.cy,
1607
- cy: region.cx,
1608
- r: region.r
1609
- };
1610
- case "ellipse": return {
1611
- kind: "ellipse",
1612
- cx: region.cy,
1613
- cy: region.cx,
1614
- rx: region.ry,
1615
- ry: region.rx
1616
- };
1617
- case "polygon": {
1618
- const swap = (ring) => ring.map(([x, y]) => [y, x]);
1619
- const turned = {
1620
- kind: "polygon",
1621
- points: swap(region.points)
1622
- };
1623
- if (region.holes) turned.holes = region.holes.map(swap);
1624
- return turned;
1625
- }
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";
1626
2018
  }
1627
2019
  }
1628
- /** This function gives an item with the axes changed. The ports go, because the adapters of 0.1 do not read them. */
1629
- function transposeItem(item) {
1630
- const { ports: _ports, ...rest } = item;
1631
- const turned = {
1632
- ...rest,
1633
- w: item.h,
1634
- h: item.w
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
+ }
2029
+ }
2030
+ /**
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.
2038
+ */
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"
1635
2064
  };
1636
- delete turned.x;
1637
- delete turned.y;
1638
- if (item.y !== void 0) turned.x = item.y;
1639
- if (item.x !== void 0) turned.y = item.x;
1640
- if (item.shape) turned.shape = transposeRegion(item.shape);
1641
- if (item.margin !== void 0 && typeof item.margin !== "number") {
1642
- const m = item.margin;
1643
- turned.margin = {
1644
- top: m.left,
1645
- right: m.bottom,
1646
- bottom: m.right,
1647
- left: m.top
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
1648
2076
  };
1649
2077
  }
1650
- return turned;
1651
2078
  }
1652
- /** This function gives an input with the axes changed. A fixed height becomes a fixed width. */
1653
- function transposeInput(input) {
1654
- const c = input.container;
1655
- const container = { fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed };
1656
- if (c.h !== void 0) container.w = c.h;
1657
- if (c.w !== void 0) container.h = c.w;
1658
- if (c.aspect !== void 0) container.aspect = 1 / c.aspect;
1659
- if (c.aspectWeight !== void 0) container.aspectWeight = c.aspectWeight;
1660
- if (c.shape !== void 0) container.shape = transposeRegion(c.shape);
1661
- if (c.inset !== void 0) container.inset = c.inset;
1662
- const turned = {
1663
- container,
1664
- items: input.items.map(transposeItem)
1665
- };
1666
- if (input.obstacles) turned.obstacles = input.obstacles.map((o) => ({
1667
- ...o,
1668
- region: transposeRegion(o.region)
1669
- }));
1670
- if (input.gap !== void 0) turned.gap = input.gap;
1671
- return turned;
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;
1672
2082
  }
1673
- /** This function gives an output with the axes changed. */
1674
- function transposeOutput(output) {
1675
- return {
1676
- placements: output.placements.map((p) => ({
1677
- ...p,
1678
- x: p.y,
1679
- y: p.x
1680
- })),
1681
- overflow: [...output.overflow],
1682
- size: {
1683
- w: output.size.h,
1684
- h: output.size.w
1685
- },
1686
- moved: output.moved.map((m) => ({
1687
- id: m.id,
1688
- x: m.y,
1689
- y: m.x
1690
- })),
1691
- trace: output.trace
1692
- };
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);
1693
2087
  }
2088
+ // src/adapters.ts
1694
2089
  /**
1695
- * This function packs an input with a fixed height through the turn of the axes. It changes x for y in the input.
1696
- * Then it packs with a fixed width, and changes the axes of the output back.
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.
1697
2092
  */
1698
- function viaAxes(input, previous, run) {
1699
- const start = now();
1700
- const inner = run(transposeInput(input), previous === void 0 ? void 0 : transposeOutput(previous));
1701
- const output = transposeOutput(inner);
1702
- output.trace = [{
1703
- id: "axes",
1704
- ms: now() - start,
1705
- measures: {}
1706
- }, ...inner.trace];
1707
- return output;
1708
- }
1709
- // src/adapters.ts
1710
- /** The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. */
1711
2093
  const LAYOUT_CELLS = {
1712
2094
  containerSize: [
1713
2095
  "fixed",
@@ -1716,7 +2098,14 @@ const LAYOUT_CELLS = {
1716
2098
  "freeAspect",
1717
2099
  "freeArea"
1718
2100
  ],
1719
- containerShape: ["rect"],
2101
+ containerShape: [
2102
+ "rect",
2103
+ "convex",
2104
+ "polygon",
2105
+ "circle",
2106
+ "ellipse",
2107
+ "holes"
2108
+ ],
1720
2109
  itemShape: ["rect"],
1721
2110
  rotation: ["none"],
1722
2111
  groups: ["none"],
@@ -1742,15 +2131,18 @@ function packFixedWidth(id, input, options) {
1742
2131
  const rects = rectsOf(frame);
1743
2132
  if (frame.w > 0) pack(rects, outerOf(frame), {
1744
2133
  strategy: id,
1745
- sortOrder: options.sortOrder
2134
+ sortOrder: options.sortOrder,
2135
+ space: spaceOf(input, frame)
1746
2136
  });
1747
2137
  const output = readFrame(input, frame, []);
1748
- output.trace.push({
1749
- id,
1750
- ms: now() - start,
1751
- measures: { "pack.overflow": output.overflow.length }
1752
- });
1753
- return output;
2138
+ return {
2139
+ ...output,
2140
+ trace: [{
2141
+ id,
2142
+ ms: now() - start,
2143
+ measures: { "pack.overflow": output.overflow.length }
2144
+ }]
2145
+ };
1754
2146
  }
1755
2147
  /** This function makes the strategy of one function of `pack`. */
1756
2148
  function layoutStrategy(id, family, catalog, cells) {
@@ -1783,11 +2175,12 @@ function layoutStrategy(id, family, catalog, cells) {
1783
2175
  return self;
1784
2176
  }
1785
2177
  /**
1786
- * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. It supports no obstacle and
1787
- * 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`.
1788
2181
  */
1789
2182
  const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
1790
- obstacleMove: [],
2183
+ obstacleMove: ["fixed"],
1791
2184
  order: [
1792
2185
  "free",
1793
2186
  "soft",
@@ -1796,7 +2189,7 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
1796
2189
  });
1797
2190
  /**
1798
2191
  * The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
1799
- * 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`.
1800
2193
  */
1801
2194
  const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
1802
2195
  obstacleMove: ["fixed"],
@@ -1804,7 +2197,7 @@ const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
1804
2197
  });
1805
2198
  /**
1806
2199
  * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
1807
- * 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`.
1808
2201
  */
1809
2202
  const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
1810
2203
  obstacleMove: ["fixed"],
@@ -1833,6 +2226,7 @@ const logSweep = {
1833
2226
  const result = packFree(rectsOf(frame), new Rect(), {
1834
2227
  strategy: id,
1835
2228
  sortOrder: o.sortOrder,
2229
+ space: spaceOf(input, frame),
1836
2230
  samples: o.samples,
1837
2231
  aspect: input.container.aspect ?? 1,
1838
2232
  aspectWeight: input.container.aspectWeight ?? 1
@@ -1840,24 +2234,26 @@ const logSweep = {
1840
2234
  const ms = now() - start;
1841
2235
  const runs = result.tried.length + 1;
1842
2236
  const output = readFrame(input, frame, []);
1843
- output.trace.push({
1844
- id: "log-sweep",
1845
- ms,
1846
- measures: {
1847
- "pack.search.widths": result.tried.length,
1848
- "pack.search.ms": ms
1849
- }
1850
- }, {
1851
- id,
1852
- ms: ms / runs,
1853
- measures: { "pack.overflow": output.overflow.length }
1854
- });
1855
- return output;
2237
+ return {
2238
+ ...output,
2239
+ trace: [{
2240
+ id: "log-sweep",
2241
+ ms,
2242
+ measures: {
2243
+ "pack.search.widths": result.tried.length,
2244
+ "pack.search.ms": ms
2245
+ }
2246
+ }, {
2247
+ id,
2248
+ ms: ms / runs,
2249
+ measures: { "pack.overflow": output.overflow.length }
2250
+ }]
2251
+ };
1856
2252
  }
1857
2253
  };
1858
2254
  /** This function tells if the insides of two rectangles overlap by more than `EPS`. */
1859
2255
  function overlaps(a, b) {
1860
- return Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x) > 1e-6 && Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y) > 1e-6;
2256
+ return Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x) > EPS && Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y) > EPS;
1861
2257
  }
1862
2258
  /** This function packs an input with a fixed size or a fixed width with a push cascade. */
1863
2259
  function pushFixedWidth(input, options, previous) {
@@ -1899,9 +2295,9 @@ function pushFixedWidth(input, options, previous) {
1899
2295
  const startAt = starts.get(e);
1900
2296
  const x = e.rect.position.x + e.dx + frame.ox;
1901
2297
  const y = e.rect.position.y + e.dy + frame.oy;
1902
- const b = boundsOf(e.obstacle.region);
1903
- if (x < bounds.x - 1e-6 || y < bounds.y - 1e-6 || x + b.w > bounds.x + bounds.w + 1e-6 || y + b.h > bounds.y + bounds.h + 1e-6 || lockedItems.some((l) => overlaps(l.rect, e.rect))) e.rect.position.set(startAt.x, startAt.y);
1904
- else if (Math.abs(e.rect.position.x - startAt.x) > 1e-6 || Math.abs(e.rect.position.y - startAt.y) > 1e-6) moved.push({
2298
+ const b = boundsOf$1(e.obstacle.region);
2299
+ if (x < bounds.x - EPS || y < bounds.y - EPS || x + b.w > bounds.x + bounds.w + EPS || y + b.h > bounds.y + bounds.h + EPS || lockedItems.some((l) => overlaps(l.rect, e.rect))) e.rect.position.set(startAt.x, startAt.y);
2300
+ else if (Math.abs(e.rect.position.x - startAt.x) > EPS || Math.abs(e.rect.position.y - startAt.y) > EPS) moved.push({
1905
2301
  id: e.obstacle.id,
1906
2302
  x,
1907
2303
  y
@@ -1911,7 +2307,7 @@ function pushFixedWidth(input, options, previous) {
1911
2307
  const kept = [...obstacles, ...lockedItems].map((e) => e.rect);
1912
2308
  for (const e of positioned) {
1913
2309
  const r = e.rect;
1914
- if (r.position.x >= -1e-6 && r.position.y >= -1e-6 && r.right() <= frame.w + 1e-6 && r.bottom() <= frame.h + 1e-6 && !kept.some((k) => overlaps(k, r))) kept.push(r);
2310
+ if (r.position.x >= -EPS && r.position.y >= -EPS && r.right() <= frame.w + EPS && r.bottom() <= frame.h + EPS && !kept.some((k) => overlaps(k, r))) kept.push(r);
1915
2311
  else pending.push(e);
1916
2312
  }
1917
2313
  const skylineStart = now();
@@ -1927,19 +2323,21 @@ function pushFixedWidth(input, options, previous) {
1927
2323
  packSkyline([...pending.map((e) => e.rect), ...kept], outerOf(frame), { sortOrder: options.sortOrder });
1928
2324
  }
1929
2325
  const output = readFrame(input, frame, [], moved);
1930
- output.trace.push({
1931
- id: "push",
1932
- ms: pushMs,
1933
- measures: { "pack.push.moved": moved.length }
1934
- }, {
1935
- id: "skyline",
1936
- ms: now() - skylineStart,
1937
- measures: {
1938
- "pack.push.replaced": pending.length,
1939
- "pack.overflow": output.overflow.length
1940
- }
1941
- });
1942
- return output;
2326
+ return {
2327
+ ...output,
2328
+ trace: [{
2329
+ id: "push",
2330
+ ms: pushMs,
2331
+ measures: { "pack.push.moved": moved.length }
2332
+ }, {
2333
+ id: "skyline",
2334
+ ms: now() - skylineStart,
2335
+ measures: {
2336
+ "pack.push.replaced": pending.length,
2337
+ "pack.overflow": output.overflow.length
2338
+ }
2339
+ }]
2340
+ };
1943
2341
  }
1944
2342
  /**
1945
2343
  * The strategy `push` uses `pushCascade`. Each item starts at its `x` and `y`, or at its place in `context.previous`.
@@ -1989,31 +2387,29 @@ const push = {
1989
2387
  */
1990
2388
  function fromRects(rects, outer) {
1991
2389
  const fixed = outer.fixedSize ? "size" : outer.fixedWidth ? "width" : "none";
1992
- const container = { fixed };
1993
- if (fixed !== "none") container.w = outer.size.x;
1994
- if (fixed === "size") container.h = outer.size.y;
1995
- if (outer.position.x !== 0 || outer.position.y !== 0) container.shape = {
1996
- kind: "rect",
1997
- x: outer.position.x,
1998
- y: outer.position.y,
1999
- w: outer.size.x,
2000
- h: outer.size.y
2001
- };
2002
2390
  return {
2003
- container,
2004
- items: rects.map((r, i) => {
2005
- const item = {
2006
- id: String(i),
2007
- w: r.size.x,
2008
- h: r.size.y
2009
- };
2010
- if (r.locked) {
2011
- item.locked = true;
2012
- item.x = r.position.x;
2013
- item.y = r.position.y;
2014
- }
2015
- return item;
2016
- })
2391
+ container: {
2392
+ fixed,
2393
+ ...fixed === "none" ? {} : { w: outer.size.x },
2394
+ ...fixed === "size" ? { h: outer.size.y } : {},
2395
+ ...outer.position.x !== 0 || outer.position.y !== 0 ? { shape: {
2396
+ kind: "rect",
2397
+ x: outer.position.x,
2398
+ y: outer.position.y,
2399
+ w: outer.size.x,
2400
+ h: outer.size.y
2401
+ } } : {}
2402
+ },
2403
+ items: rects.map((r, i) => ({
2404
+ id: String(i),
2405
+ w: r.size.x,
2406
+ h: r.size.y,
2407
+ ...r.locked ? {
2408
+ locked: true,
2409
+ x: r.position.x,
2410
+ y: r.position.y
2411
+ } : {}
2412
+ }))
2017
2413
  };
2018
2414
  }
2019
2415
  /**
@@ -2068,6 +2464,15 @@ function packWith(id, input, options, seed = 0) {
2068
2464
  function overlap(a, b, tolerance) {
2069
2465
  return Math.min(a.r, b.r) - Math.max(a.l, b.l) > tolerance && Math.min(a.b, b.b) - Math.max(a.t, b.t) > tolerance;
2070
2466
  }
2467
+ /** This function gives the box made smaller by a tolerance on each side, as a rectangle. */
2468
+ function inner(box, tolerance) {
2469
+ return {
2470
+ x: box.l + tolerance,
2471
+ y: box.t + tolerance,
2472
+ w: box.r - box.l - 2 * tolerance,
2473
+ h: box.b - box.t - 2 * tolerance
2474
+ };
2475
+ }
2071
2476
  /**
2072
2477
  * This function counts the breaks of the 4 hard rules of the packer for an output.
2073
2478
  *
@@ -2078,38 +2483,36 @@ function overlap(a, b, tolerance) {
2078
2483
  * - `complete`: each item with no placement that is not in the overflow. A locked item breaks `locked-kept` in its place.
2079
2484
  *
2080
2485
  * A locked item with no placement stays at its input position for `no-overlap` and `in-container`. A locked item with no
2081
- * `x` or no `y` in the input is not locked for these rules.
2486
+ * `x` or no `y` in the input is a bad input. The rules skip it, as `packHard` of the contract does. `checkInput` finds it.
2082
2487
  *
2083
- * The rules read a shape of the container, of an obstacle or of an item as its bounding box (decision 5). The bounding box
2084
- * of an item shape is relative to the position of the item. The result has no tolerance in it.
2488
+ * The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
2489
+ * The rules read a shape of the container or of an item as its bounding box (decision 5). The bounding box of an item
2490
+ * shape is relative to the position of the item. The result has no tolerance in it.
2085
2491
  */
2086
2492
  function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2087
2493
  const tol = tolerance.overlap;
2088
2494
  const placements = /* @__PURE__ */ new Map();
2089
2495
  for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
2090
2496
  const overflow = new Set(output.overflow);
2091
- const breaks = {
2092
- "no-overlap": 0,
2093
- "in-container": 0,
2094
- "locked-kept": 0,
2095
- complete: 0
2096
- };
2497
+ let lockedKept = 0;
2498
+ let complete = 0;
2097
2499
  const boxes = [];
2098
2500
  for (const item of input.items) {
2099
- const lockedAt = item.locked === true && item.x !== void 0 && item.y !== void 0 ? {
2501
+ if (item.locked === true && (item.x === void 0 || item.y === void 0)) continue;
2502
+ const lockedAt = item.locked === true ? {
2100
2503
  x: item.x,
2101
2504
  y: item.y
2102
2505
  } : void 0;
2103
2506
  let p = placements.get(item.id);
2104
2507
  if (p === void 0) {
2105
2508
  if (lockedAt === void 0) {
2106
- if (!overflow.has(item.id)) breaks.complete += 1;
2509
+ if (!overflow.has(item.id)) complete += 1;
2107
2510
  continue;
2108
2511
  }
2109
- breaks["locked-kept"] += 1;
2512
+ lockedKept += 1;
2110
2513
  p = lockedAt;
2111
2514
  } else if (lockedAt !== void 0) {
2112
- if (Math.abs(p.x - lockedAt.x) > .001 || Math.abs(p.y - lockedAt.y) > .001) breaks["locked-kept"] += 1;
2515
+ if (Math.abs(p.x - lockedAt.x) > RECORD_EPS || Math.abs(p.y - lockedAt.y) > RECORD_EPS) lockedKept += 1;
2113
2516
  }
2114
2517
  const placed = p.turn === 90 ? turnItem(item) : item;
2115
2518
  const m = marginOf(placed);
@@ -2130,18 +2533,17 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2130
2533
  }
2131
2534
  const moved = new Map(output.moved.map((m) => [m.id, m]));
2132
2535
  const obstacles = (input.obstacles ?? []).map((o) => {
2536
+ const at = moved.get(o.id);
2537
+ if (at === void 0) return o.region;
2133
2538
  const b = boundsOf(o.region);
2134
- const at = moved.get(o.id) ?? b;
2135
- return {
2136
- l: at.x,
2137
- t: at.y,
2138
- r: at.x + b.w,
2139
- b: at.y + b.h
2140
- };
2539
+ return translate(o.region, at.x - b.x, at.y - b.y);
2141
2540
  });
2541
+ let overlaps = 0;
2142
2542
  for (const [i, a] of boxes.entries()) {
2143
- for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol)) breaks["no-overlap"] += 1;
2144
- for (const o of obstacles) if (overlap(a, o, tol)) breaks["no-overlap"] += 1;
2543
+ for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j], tol)) overlaps += 1;
2544
+ if (obstacles.length === 0) continue;
2545
+ const rect = inner(a, tol);
2546
+ for (const region of obstacles) if (regionHitsRect(region, rect)) overlaps += 1;
2145
2547
  }
2146
2548
  const { container } = input;
2147
2549
  const shape = container.shape === void 0 ? void 0 : boundsOf(container.shape);
@@ -2152,12 +2554,18 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2152
2554
  const inset = container.inset ?? 0;
2153
2555
  const fixedW = container.fixed === "size" || container.fixed === "width";
2154
2556
  const fixedH = container.fixed === "size" || container.fixed === "height";
2557
+ let outside = 0;
2155
2558
  for (const box of boxes) {
2156
2559
  const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
2157
2560
  const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
2158
- if (outX || outY) breaks["in-container"] += 1;
2561
+ if (outX || outY) outside += 1;
2159
2562
  }
2160
- return breaks;
2563
+ return {
2564
+ "no-overlap": overlaps,
2565
+ "in-container": outside,
2566
+ "locked-kept": lockedKept,
2567
+ complete
2568
+ };
2161
2569
  }
2162
2570
  // src/generators.ts
2163
2571
  /** The largest count of boxes that a generator makes. */
@@ -2378,8 +2786,10 @@ function containerOf(size, w, h, shape) {
2378
2786
  aspectWeight: 0
2379
2787
  }
2380
2788
  }[size];
2381
- if (shape !== void 0) container.shape = shape;
2382
- return container;
2789
+ return shape === void 0 ? container : {
2790
+ ...container,
2791
+ shape
2792
+ };
2383
2793
  }
2384
2794
  /** This function gives the 4 corners of a rectangle, clockwise from the top left. */
2385
2795
  const corners = (x, y, w, h) => [
@@ -3071,6 +3481,6 @@ function corpus(name, specs) {
3071
3481
  inputs
3072
3482
  };
3073
3483
  }
3074
- export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellOf, checkHard, 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 };
3075
3485
 
3076
3486
  //# sourceMappingURL=index.js.map