@mark1russell7/packing 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_TOLERANCE, EPS, RECORD_EPS, boundsOf, boundsOf as boundsOf$1, checkInput, marginSides, regionHitsRect, translate, turnBox, turnPoint, turnPort, turnRegion } from "@mark1russell7/layout-contract";
1
+ import { DEFAULT_TOLERANCE, EPS, RECORD_EPS, boundsOf, boundsOf as boundsOf$1, checkInput, marginSides, rectInRegion, regionHitsRect, translate, turnBox, turnPoint, turnPort, turnRegion } from "@mark1russell7/layout-contract";
2
2
  // src/vector.ts
3
3
  /** A mutable two-dimensional vector. */
4
4
  var Vector = class Vector {
@@ -137,6 +137,32 @@ var Rect = class {
137
137
  return rects.reduce((acc, r) => Math.max(acc, r.bottom()), 0);
138
138
  }
139
139
  };
140
+ // src/moves.ts
141
+ /** The id of the characteristic move of each strategy. The trace of each strategy has its id, also with the count 0. */
142
+ const MOVES = Object.freeze({
143
+ /** A rectangle that the gap fill of `packRow` places in a gap of a row: under a shorter neighbor, or at the row break. */
144
+ rows: "move.gapFill",
145
+ /** A rectangle that `packShelf` puts on an open shelf with a taller rectangle. The strip above it stays empty. */
146
+ shelf: "move.stack",
147
+ /** A rectangle that the skyline puts in a lower window, not in the leftmost window where it fits. */
148
+ skyline: "move.lowerWindow",
149
+ /** A rectangle that a push cascade moves. */
150
+ push: "move.cascade"
151
+ });
152
+ const counts = /* @__PURE__ */ new Map();
153
+ /** This function adds `n` to the count of a move. */
154
+ function countMove(id, n = 1) {
155
+ if (n > 0) counts.set(id, (counts.get(id) ?? 0) + n);
156
+ }
157
+ /** This function sets the count of a move to 0. Without an id, it sets each count to 0. */
158
+ function clearMoves(id) {
159
+ if (id === void 0) counts.clear();
160
+ else counts.delete(id);
161
+ }
162
+ /** This function gives the count of a move since the last `clearMoves`. */
163
+ function moveCount(id) {
164
+ return counts.get(id) ?? 0;
165
+ }
140
166
  // src/order.ts
141
167
  /** This constant lists each sort order. The first one is the default. */
142
168
  const SORT_ORDERS = [
@@ -832,6 +858,7 @@ function packShelf(rects, outer, options = {}) {
832
858
  x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
833
859
  }
834
860
  if (outer.fixedSize && rowTop + h > outer.size.y) continue;
861
+ if (!rowEmpty && rowTop + h < rowBottom) countMove(MOVES.shelf);
835
862
  rect.position.set(x + outer.position.x, rowTop + outer.position.y);
836
863
  rect.wasPacked = true;
837
864
  rowEmpty = false;
@@ -956,6 +983,14 @@ function packRowsInSpace(rects, outer, order, space) {
956
983
  * `limit[x, x + rw)`. A second deque keeps that smallest value. No valid window gives the height `Infinity`.
957
984
  */
958
985
  function lowestWindow(skyline, width, rw, limit, rh = 0) {
986
+ const { x, y } = findWindow(skyline, width, rw, limit, rh);
987
+ return {
988
+ x,
989
+ y
990
+ };
991
+ }
992
+ /** This function is `lowestWindow`, and it also gives the x of the leftmost valid window. */
993
+ function findWindow(skyline, width, rw, limit, rh = 0) {
959
994
  if (limit !== void 0) return lowestLimitedWindow(skyline, limit, width, rw, rh);
960
995
  const deque = new Int32Array(width);
961
996
  let head = 0;
@@ -976,9 +1011,14 @@ function lowestWindow(skyline, width, rw, limit, rh = 0) {
976
1011
  }
977
1012
  return {
978
1013
  x: bestX,
979
- y: bestY
1014
+ y: bestY,
1015
+ first: width >= rw ? 0 : -1
980
1016
  };
981
1017
  }
1018
+ /** This function counts the move of the skyline: a window that is lower than the leftmost valid window. */
1019
+ function countWindow(w) {
1020
+ if (Number.isFinite(w.y) && w.first >= 0 && w.x !== w.first) countMove(MOVES.skyline);
1021
+ }
982
1022
  /** This function is `lowestWindow` with a limit for each cell. */
983
1023
  function lowestLimitedWindow(skyline, limit, width, rw, rh) {
984
1024
  const high = new Int32Array(width);
@@ -989,6 +1029,7 @@ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
989
1029
  let lt = 0;
990
1030
  let bestX = 0;
991
1031
  let bestY = Infinity;
1032
+ let first = -1;
992
1033
  for (let i = 0; i < width; i++) {
993
1034
  while (ht > hh && skyline[high[ht - 1]] <= skyline[i]) ht--;
994
1035
  high[ht++] = i;
@@ -998,7 +1039,9 @@ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
998
1039
  if (low[lh] <= i - rw) lh++;
999
1040
  if (i >= rw - 1) {
1000
1041
  const max = skyline[high[hh]];
1001
- if (max < bestY && max + rh <= limit[low[lh]]) {
1042
+ const valid = max + rh <= limit[low[lh]];
1043
+ if (valid && first < 0) first = i - rw + 1;
1044
+ if (max < bestY && valid) {
1002
1045
  bestY = max;
1003
1046
  bestX = i - rw + 1;
1004
1047
  }
@@ -1006,7 +1049,8 @@ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
1006
1049
  }
1007
1050
  return {
1008
1051
  x: bestX,
1009
- y: bestY
1052
+ y: bestY,
1053
+ first
1010
1054
  };
1011
1055
  }
1012
1056
  /**
@@ -1050,8 +1094,10 @@ function packSkyline(rects, outer, options = {}) {
1050
1094
  y = 0;
1051
1095
  for (let i = 0; i < width; i++) if (skyline[i] > y) y = skyline[i];
1052
1096
  } else {
1053
- ({x, y} = lowestWindow(skyline, width, rw));
1097
+ const found = findWindow(skyline, width, rw);
1098
+ ({x, y} = found);
1054
1099
  if (outer.fixedSize && y + rh > outer.size.y) continue;
1100
+ countWindow(found);
1055
1101
  }
1056
1102
  rect.position.set(x + outer.position.x, y + outer.position.y);
1057
1103
  rect.wasPacked = true;
@@ -1104,16 +1150,19 @@ function packSkylineInSpace(rects, outer, order, space) {
1104
1150
  outer.size.y = Math.max(outer.size.y, y + rh);
1105
1151
  continue;
1106
1152
  }
1107
- ({x, y} = lowestWindow(skyline, width, rw, limit, rh));
1153
+ let found = findWindow(skyline, width, rw, limit, rh);
1154
+ ({x, y} = found);
1108
1155
  let sky = skyline;
1109
1156
  let lim = limit;
1110
1157
  if (!Number.isFinite(y)) {
1111
1158
  sky = skyline.slice();
1112
1159
  lim = limit.slice();
1113
1160
  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));
1161
+ found = findWindow(sky, width, rw, lim, rh);
1162
+ ({x, y} = found);
1115
1163
  if (!Number.isFinite(y)) continue;
1116
1164
  }
1165
+ countWindow(found);
1117
1166
  rect.position.set(x + ox, y + oy);
1118
1167
  rect.wasPacked = true;
1119
1168
  const top = y + rh;
@@ -1126,10 +1175,19 @@ function packSkylineInSpace(rects, outer, order, space) {
1126
1175
  return outer;
1127
1176
  }
1128
1177
  // src/pack.ts
1178
+ /** The depth of the calls of `packInto`. Only the outer gap fill counts its rectangles as moves. */
1179
+ let fillDepth = 0;
1129
1180
  function packInto(rects, region, offset, order) {
1130
1181
  const unpacked = Rect.unpacked(rects);
1131
- pack(unpacked, region, { sortOrder: order });
1132
- for (const r of Rect.packed(unpacked)) r.translateBy(offset);
1182
+ fillDepth++;
1183
+ try {
1184
+ pack(unpacked, region, { sortOrder: order });
1185
+ } finally {
1186
+ fillDepth--;
1187
+ }
1188
+ const placed = Rect.packed(unpacked);
1189
+ if (fillDepth === 0) countMove(MOVES.rows, placed.length);
1190
+ for (const r of placed) r.translateBy(offset);
1133
1191
  }
1134
1192
  function packRow(rects, outer, order) {
1135
1193
  const row = [];
@@ -1187,7 +1245,7 @@ function packRows(rects, outer, options = {}) {
1187
1245
  const space = options.space;
1188
1246
  if (space !== void 0 && (!space.plain || rects.some((r) => r.locked))) return packRowsInSpace(rects, outer, order, space);
1189
1247
  for (const r of rects) if (r.locked) r.wasPacked = true;
1190
- while (!packRow(rects, outer, order));
1248
+ while (!packRow(rects, outer, order)) if (fillDepth === 0) clearMoves(MOVES.rows);
1191
1249
  return outer;
1192
1250
  }
1193
1251
  /**
@@ -1264,6 +1322,7 @@ function packFree(rects, outer = new Rect(), options = {}) {
1264
1322
  const maxWidth = Math.max(minWidth, rowWidth);
1265
1323
  const tried = [];
1266
1324
  const run = (width) => {
1325
+ clearMoves();
1267
1326
  Rect.resetPacked(rects);
1268
1327
  const box = new Rect();
1269
1328
  box.position.setTo(outer.position);
@@ -1398,6 +1457,323 @@ function pushCascade(placed, rects, options = {}) {
1398
1457
  }
1399
1458
  return [...moved];
1400
1459
  }
1460
+ // src/overflow.ts
1461
+ /** This function gives the area of a ring by the shoelace formula. The direction of the ring does not count. */
1462
+ function ringArea(ring) {
1463
+ let sum = 0;
1464
+ for (let i = 0; i < ring.length; i++) {
1465
+ const a = ring[i];
1466
+ const b = ring[(i + 1) % ring.length];
1467
+ sum += a[0] * b[1] - b[0] * a[1];
1468
+ }
1469
+ return Math.abs(sum) / 2;
1470
+ }
1471
+ /**
1472
+ * This function gives the area of a region that an item cannot use. A polygon loses the area of its holes. Thus a hole
1473
+ * that is out of the polygon makes the area too small, and the free area stays an upper bound.
1474
+ */
1475
+ function blockedArea(region) {
1476
+ switch (region.kind) {
1477
+ case "rect": return Math.max(0, region.w) * Math.max(0, region.h);
1478
+ case "circle": return Math.PI * region.r ** 2;
1479
+ case "ellipse": return Math.PI * region.rx * region.ry;
1480
+ case "polygon": {
1481
+ let area = ringArea(region.points);
1482
+ for (const hole of region.holes ?? []) area -= ringArea(hole);
1483
+ return Math.max(0, area);
1484
+ }
1485
+ }
1486
+ }
1487
+ /** This function tells if the insides of two rectangles have a common point. Two rectangles with a common edge do not. */
1488
+ function boxesOverlap(a, b) {
1489
+ return Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > 0 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > 0;
1490
+ }
1491
+ /** This function tells if rectangle `a` is in rectangle `b`. */
1492
+ function boxIn(a, b) {
1493
+ return a.x >= b.x && a.y >= b.y && a.x + a.w <= b.x + b.w && a.y + a.h <= b.y + b.h;
1494
+ }
1495
+ /** This function gives the rectangle made smaller by `d` on each side. */
1496
+ function shrink(b, d) {
1497
+ return {
1498
+ x: b.x + d,
1499
+ y: b.y + d,
1500
+ w: Math.max(0, b.w - 2 * d),
1501
+ h: Math.max(0, b.h - 2 * d)
1502
+ };
1503
+ }
1504
+ /** This function tells if a point is in a ring, by the even-odd rule. */
1505
+ function pointInRing(x, y, ring) {
1506
+ let inside = false;
1507
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
1508
+ const [xi, yi] = ring[i];
1509
+ const [xj, yj] = ring[j];
1510
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
1511
+ }
1512
+ return inside;
1513
+ }
1514
+ /** This function tells if a segment has a part of a length above 0 in the open rectangle. The Liang-Barsky clip gives it. */
1515
+ function segmentHitsBox(ax, ay, bx, by, r) {
1516
+ let t0 = 0;
1517
+ let t1 = 1;
1518
+ const dx = bx - ax;
1519
+ const dy = by - ay;
1520
+ const clip = (p, q) => {
1521
+ if (p === 0) return q > 0;
1522
+ const t = q / p;
1523
+ if (p < 0) {
1524
+ if (t > t1) return false;
1525
+ if (t > t0) t0 = t;
1526
+ } else {
1527
+ if (t < t0) return false;
1528
+ if (t < t1) t1 = t;
1529
+ }
1530
+ return true;
1531
+ };
1532
+ return clip(-dx, ax - r.x) && clip(dx, r.x + r.w - ax) && clip(-dy, ay - r.y) && clip(dy, r.y + r.h - ay) && t1 > t0;
1533
+ }
1534
+ /**
1535
+ * This function tells if a rectangle is in a polygon with holes. No edge of a ring crosses the inside of the rectangle.
1536
+ * No vertex is in it, and its center is in the polygon by the even-odd rule.
1537
+ */
1538
+ function boxInPolygon(b, rings) {
1539
+ for (const ring of rings) for (let i = 0; i < ring.length; i++) {
1540
+ const p = ring[i];
1541
+ const q = ring[(i + 1) % ring.length];
1542
+ if (p[0] > b.x && p[0] < b.x + b.w && p[1] > b.y && p[1] < b.y + b.h) return false;
1543
+ if (segmentHitsBox(p[0], p[1], q[0], q[1], b)) return false;
1544
+ }
1545
+ const cx = b.x + b.w / 2;
1546
+ const cy = b.y + b.h / 2;
1547
+ let inside = false;
1548
+ for (const ring of rings) if (pointInRing(cx, cy, ring)) inside = !inside;
1549
+ return inside;
1550
+ }
1551
+ /** This function tells if a rectangle is in a region of a container. A rectangle with no size is in no region. */
1552
+ function boxInShape(b, shape) {
1553
+ if (!(b.w > 0 && b.h > 0)) return false;
1554
+ const s = shrink(b, 1e-9 * Math.max(1, b.w, b.h));
1555
+ switch (shape.kind) {
1556
+ case "rect": return boxIn(b, {
1557
+ x: shape.x,
1558
+ y: shape.y,
1559
+ w: shape.w,
1560
+ h: shape.h
1561
+ });
1562
+ case "circle":
1563
+ case "ellipse": {
1564
+ const rx = shape.kind === "circle" ? shape.r : shape.rx;
1565
+ const ry = shape.kind === "circle" ? shape.r : shape.ry;
1566
+ const inside = (x, y) => ((x - shape.cx) / rx) ** 2 + ((y - shape.cy) / ry) ** 2 <= 1;
1567
+ return inside(s.x, s.y) && inside(s.x + s.w, s.y) && inside(s.x, s.y + s.h) && inside(s.x + s.w, s.y + s.h);
1568
+ }
1569
+ case "polygon": return boxInPolygon(s, [shape.points, ...shape.holes ?? []]);
1570
+ }
1571
+ }
1572
+ /**
1573
+ * This function gives the area of a container shape. A hole counts only when its bounding box is in the outer ring.
1574
+ * Also, that box has no common point with the box of a hole before it. Thus the area is not too small.
1575
+ */
1576
+ function shapeArea(shape) {
1577
+ if (shape.kind !== "polygon") return blockedArea(shape);
1578
+ let area = ringArea(shape.points);
1579
+ const kept = [];
1580
+ for (const hole of shape.holes ?? []) {
1581
+ const box = boundsOf({
1582
+ kind: "polygon",
1583
+ points: hole
1584
+ });
1585
+ if (!boxInPolygon(shrink(box, 1e-9 * Math.max(1, box.w, box.h)), [shape.points])) continue;
1586
+ if (kept.some((k) => boxesOverlap(k, box))) continue;
1587
+ kept.push(box);
1588
+ area -= ringArea(hole);
1589
+ }
1590
+ return Math.max(0, area);
1591
+ }
1592
+ /**
1593
+ * This function gives the area of the union of rectangles, each cut to the rectangle `clip`. A sweep over the x edges
1594
+ * adds the merged y intervals of each slab.
1595
+ */
1596
+ function unionArea(boxes, clip) {
1597
+ const cut = [];
1598
+ for (const b of boxes) {
1599
+ const x0 = Math.max(b.x, clip.x);
1600
+ const y0 = Math.max(b.y, clip.y);
1601
+ const x1 = Math.min(b.x + b.w, clip.x + clip.w);
1602
+ const y1 = Math.min(b.y + b.h, clip.y + clip.h);
1603
+ if (x1 > x0 && y1 > y0) cut.push({
1604
+ x: x0,
1605
+ y: y0,
1606
+ w: x1 - x0,
1607
+ h: y1 - y0
1608
+ });
1609
+ }
1610
+ const xs = [...new Set(cut.flatMap((b) => [b.x, b.x + b.w]))].toSorted((a, b) => a - b);
1611
+ let area = 0;
1612
+ for (let i = 0; i + 1 < xs.length; i++) {
1613
+ const left = xs[i];
1614
+ const right = xs[i + 1];
1615
+ const spans = cut.filter((b) => b.x <= left && b.x + b.w >= right).map((b) => [b.y, b.y + b.h]).toSorted((p, q) => p[0] - q[0]);
1616
+ let covered = 0;
1617
+ let top = -Infinity;
1618
+ let bottom = -Infinity;
1619
+ for (const [s, e] of spans) if (s > bottom) {
1620
+ covered += bottom - top > 0 ? bottom - top : 0;
1621
+ top = s;
1622
+ bottom = e;
1623
+ } else bottom = Math.max(bottom, e);
1624
+ covered += bottom - top > 0 ? bottom - top : 0;
1625
+ area += covered * (right - left);
1626
+ }
1627
+ return area;
1628
+ }
1629
+ /** This function gives the rectangle of the container: the bounding box of its shape, or 0, 0, `w`, `h`. */
1630
+ function containerBox(input) {
1631
+ const { container } = input;
1632
+ if (container.shape !== void 0) return boundsOf(container.shape);
1633
+ return {
1634
+ x: 0,
1635
+ y: 0,
1636
+ w: container.w ?? Infinity,
1637
+ h: container.h ?? Infinity
1638
+ };
1639
+ }
1640
+ /** This function tells if an item is locked at a position. Such an item is not a candidate of the overflow. */
1641
+ const lockedAt = (item) => item.locked === true && item.x !== void 0 && item.y !== void 0;
1642
+ /** This function gives the rectangle of an item at its position: the bounding box of its shape, or its size. */
1643
+ function itemBox(item) {
1644
+ const s = item.shape === void 0 ? {
1645
+ x: 0,
1646
+ y: 0,
1647
+ w: item.w,
1648
+ h: item.h
1649
+ } : boundsOf(item.shape);
1650
+ return {
1651
+ x: (item.x ?? 0) + s.x,
1652
+ y: (item.y ?? 0) + s.y,
1653
+ w: s.w,
1654
+ h: s.h
1655
+ };
1656
+ }
1657
+ /** This function gives the smallest area of an item: its rectangle, or the area of its shape when that is smaller. */
1658
+ function itemArea(item) {
1659
+ const rect = Math.max(0, item.w) * Math.max(0, item.h);
1660
+ return item.shape === void 0 ? rect : Math.min(rect, blockedArea(item.shape));
1661
+ }
1662
+ /**
1663
+ * This function gives an upper bound of the free area of a container with a fixed size. The bound ignores the gap and
1664
+ * the margins, thus it is not too small.
1665
+ *
1666
+ * - The container area is the inner rectangle (the container less the inset). A shape gives the smaller of that and its exact area.
1667
+ * - A rectangle container loses the exact union of the fixed rectangle obstacles and the locked items in the inner rectangle.
1668
+ * - A shape loses only the regions that are fully in it and in the inner rectangle, with no common point between their boxes.
1669
+ * - An obstacle with `move` can go out of the way. Thus the bound does not subtract it.
1670
+ */
1671
+ function freeAreaBound(input) {
1672
+ const inner = shrink(containerBox(input), input.container.inset ?? 0);
1673
+ if (!(inner.w > 0 && inner.h > 0)) return 0;
1674
+ const blocked = [];
1675
+ for (const o of input.obstacles ?? []) if (o.move === void 0) blocked.push({
1676
+ region: o.region,
1677
+ box: boundsOf(o.region)
1678
+ });
1679
+ for (const item of input.items) {
1680
+ if (!lockedAt(item)) continue;
1681
+ const box = itemBox(item);
1682
+ blocked.push({
1683
+ region: {
1684
+ kind: "rect",
1685
+ ...box
1686
+ },
1687
+ box
1688
+ });
1689
+ }
1690
+ const shape = input.container.shape;
1691
+ const innerArea = inner.w * inner.h;
1692
+ const inInner = (b) => b.w > 0 && b.h > 0 && boxIn(b, inner);
1693
+ if (shape === void 0 || shape.kind === "rect") {
1694
+ const rects = blocked.filter((b) => b.region.kind === "rect");
1695
+ let free = innerArea - unionArea(rects.map((b) => b.box), inner);
1696
+ const kept = [];
1697
+ for (const b of blocked) {
1698
+ if (b.region.kind === "rect" || !inInner(b.box)) continue;
1699
+ if (rects.some((r) => boxesOverlap(r.box, b.box)) || kept.some((k) => boxesOverlap(k, b.box))) continue;
1700
+ kept.push(b.box);
1701
+ free -= blockedArea(b.region);
1702
+ }
1703
+ return Math.max(0, free);
1704
+ }
1705
+ let free = Math.min(innerArea, shapeArea(shape));
1706
+ const kept = [];
1707
+ for (const b of blocked) {
1708
+ if (!inInner(b.box) || !boxInShape(b.box, shape) || kept.some((k) => boxesOverlap(k, b.box))) continue;
1709
+ kept.push(b.box);
1710
+ free -= blockedArea(b.region);
1711
+ }
1712
+ return Math.max(0, free);
1713
+ }
1714
+ /**
1715
+ * This function gives a lower bound of the overflow of an input: the items that no packer can place, and their area.
1716
+ *
1717
+ * - The candidates are the items that are not locked at a position.
1718
+ * - A fixed side forces each candidate that is longer than the side less the inset, in each turn that the item permits.
1719
+ * - A fixed size also forces the fewest other candidates that make the rest fit the free area of `freeAreaBound`. These are the largest ones.
1720
+ * - The area of the bound is the area of the long items, plus the part of the area of the rest above the free area.
1721
+ * - The margins and the gap do not count. A slack of the overlap tolerance along the edge of each candidate keeps the bound valid.
1722
+ * - A container with no fixed side gives 0.
1723
+ */
1724
+ function overflowBound(input) {
1725
+ const { container } = input;
1726
+ const fixedW = container.fixed === "size" || container.fixed === "width";
1727
+ const fixedH = container.fixed === "size" || container.fixed === "height";
1728
+ if (!fixedW && !fixedH) return {
1729
+ forced: 0,
1730
+ area: 0,
1731
+ free: Infinity
1732
+ };
1733
+ const tol = DEFAULT_TOLERANCE.overlap;
1734
+ const bounds = containerBox(input);
1735
+ const inset = container.inset ?? 0;
1736
+ const maxW = fixedW ? bounds.w - 2 * inset + 2 * tol : Infinity;
1737
+ const maxH = fixedH ? bounds.h - 2 * inset + 2 * tol : Infinity;
1738
+ let forced = 0;
1739
+ let area = 0;
1740
+ let slack = 0;
1741
+ const rest = [];
1742
+ for (const item of input.items) {
1743
+ if (lockedAt(item)) continue;
1744
+ const box = itemBox(item);
1745
+ const w = Math.min(item.w, box.w);
1746
+ const h = Math.min(item.h, box.h);
1747
+ const fits = w <= maxW && h <= maxH || item.turnable === true && h <= maxW && w <= maxH;
1748
+ const a = itemArea(item);
1749
+ if (!fits) {
1750
+ forced += 1;
1751
+ area += a;
1752
+ continue;
1753
+ }
1754
+ rest.push(a);
1755
+ slack += 2 * tol * (Math.max(0, w) + Math.max(0, h));
1756
+ }
1757
+ if (!(fixedW && fixedH)) return {
1758
+ forced,
1759
+ area,
1760
+ free: Infinity
1761
+ };
1762
+ const free = freeAreaBound(input);
1763
+ const room = free + slack + 2 * tol * (Math.max(0, bounds.w) + Math.max(0, bounds.h));
1764
+ let sum = rest.reduce((s, a) => s + a, 0);
1765
+ area += Math.max(0, sum - room);
1766
+ for (const a of rest.toSorted((p, q) => q - p)) {
1767
+ if (sum <= room || !(a > 0)) break;
1768
+ sum -= a;
1769
+ forced += 1;
1770
+ }
1771
+ return {
1772
+ forced,
1773
+ area,
1774
+ free
1775
+ };
1776
+ }
1401
1777
  // src/sha256.ts
1402
1778
  /** The round constants of SHA-256. */
1403
1779
  const K = new Uint32Array([
@@ -1597,53 +1973,68 @@ function toPackInput(file) {
1597
1973
  };
1598
1974
  }
1599
1975
  // src/measures.ts
1600
- const spec = (id, family, unit, direction, min, max, guard) => ({
1976
+ const spec = (id, family, unit, direction, min, max, minEffect, guard) => ({
1601
1977
  id,
1602
1978
  family,
1603
1979
  unit,
1604
1980
  direction,
1605
1981
  min,
1606
1982
  max,
1983
+ ...minEffect === void 0 ? {} : { minEffect },
1607
1984
  ...guard
1608
1985
  });
1986
+ /** The minimum effect of an area or a ratio of lengths: 1% of the larger value. */
1987
+ const REL = Object.freeze({ rel: .01 });
1609
1988
  /**
1610
1989
  * 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.
1990
+ * and the tolerance of each guard of the packer, and of the minimum effect of each measure.
1991
+ *
1992
+ * - A count and a share of counts have the minimum effect 0. Thus each change of a count is an effect.
1993
+ * - A share of an area from 0 to 1 has 0.005. A guard has its tolerance.
1994
+ * - An area and a ratio of lengths have `{ rel: 0.01 }`: 1% of the larger value.
1995
+ * - The time has no minimum effect, because the times of CI change each time.
1612
1996
  */
1613
1997
  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, {
1998
+ "pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity, REL),
1999
+ "pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1, .005),
2000
+ "pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1, .005),
2001
+ "pack.aspect": spec("pack.aspect", "aspect", "ratio", "lower", 1, Infinity, REL),
2002
+ "pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity, .01, {
1619
2003
  floor: 0,
1620
2004
  tol: .01
1621
2005
  }),
1622
- "pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, {
2006
+ "pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, 0, {
1623
2007
  floor: 0,
1624
2008
  tol: 0
1625
2009
  }),
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, {
2010
+ "pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity, 0),
2011
+ "pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1, .005),
2012
+ "pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, 0, {
1629
2013
  floor: 0,
1630
2014
  tol: 0
1631
2015
  }),
1632
- "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, {
2016
+ "pack.overflow.rate": spec("pack.overflow.rate", "overflow", "share", "lower", 0, 1, 0),
2017
+ "pack.overflow.area": spec("pack.overflow.area", "overflow", "share", "lower", 0, 1, .005),
2018
+ "pack.overflow.forced": spec("pack.overflow.forced", "overflow", "count", "lower", 0, Infinity, 0),
2019
+ "pack.overflow.forcedArea": spec("pack.overflow.forcedArea", "overflow", "share", "lower", 0, 1, .005),
2020
+ "pack.overflow.excess": spec("pack.overflow.excess", "overflow", "count", "lower", 0, Infinity, 0),
2021
+ "pack.overflow.forcedAreaMin": spec("pack.overflow.forcedAreaMin", "overflow", "share", "lower", 0, 1, .005),
2022
+ "pack.overflow.excessArea": spec("pack.overflow.excessArea", "overflow", "share", "lower", 0, 1, .005),
2023
+ "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, .02, {
1633
2024
  floor: 0,
1634
2025
  tol: .02
1635
2026
  }),
1636
- "pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, {
2027
+ "pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, .005, {
1637
2028
  floor: 0,
1638
2029
  tol: .005
1639
2030
  }),
1640
- "pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, {
2031
+ "pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, .01, {
1641
2032
  floor: 0,
1642
2033
  tol: .01
1643
2034
  }),
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)
2035
+ "pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1, 0),
2036
+ "pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity, 0),
2037
+ "pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity, void 0)
1647
2038
  });
1648
2039
  /** This function gives the smallest rectangle around the boxes. No box gives a rectangle of size 0 at 0, 0. */
1649
2040
  function hullOf(boxes) {
@@ -1732,6 +2123,9 @@ function traceSum(output, id) {
1732
2123
  * - The boxes are the placed items with a width and a height above 0. The hull is the smallest rectangle around them.
1733
2124
  * - 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
2125
  * - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
2126
+ * - The overflow measures count each id once. `pack.overflow.forced` is the lower bound of `overflowBound`.
2127
+ * - `pack.overflow.excess` is the overflow less the forced overflow. A container with no fixed side gives 0 for both.
2128
+ * - `pack.overflow.forcedAreaMin` is a lower bound of the overflow area. `pack.overflow.excessArea` is the overflow area above it.
1735
2129
  *
1736
2130
  * @example
1737
2131
  * ```ts
@@ -1766,6 +2160,17 @@ function measure(input, output, options = {}) {
1766
2160
  }
1767
2161
  const rights = rowsOf$1(boxes, options.rowTolerance ?? quantileOrZero(boxes.map((b) => b.h), .5) / 2).map((r) => r.right);
1768
2162
  const spread = rights.length > 0 ? Math.max(...rights) - Math.min(...rights) : 0;
2163
+ const unplaced = new Set(output.overflow);
2164
+ let itemArea = 0;
2165
+ let unplacedArea = 0;
2166
+ for (const item of input.items) {
2167
+ const a = Math.max(0, item.w) * Math.max(0, item.h);
2168
+ itemArea += a;
2169
+ if (unplaced.has(item.id)) unplacedArea += a;
2170
+ }
2171
+ const bound = overflowBound(input);
2172
+ const overflowArea = itemArea > 0 ? unplacedArea / itemArea : 0;
2173
+ const forcedAreaMin = itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0;
1769
2174
  return {
1770
2175
  "pack.area": hullArea,
1771
2176
  "pack.fill": fill,
@@ -1775,12 +2180,27 @@ function measure(input, output, options = {}) {
1775
2180
  "pack.order.inversions": pairs > 0 ? inversions / pairs : 0,
1776
2181
  "pack.order.breaks": breaks,
1777
2182
  "pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
1778
- "pack.overflow": new Set(output.overflow).size,
2183
+ "pack.overflow": unplaced.size,
2184
+ "pack.overflow.rate": input.items.length > 0 ? unplaced.size / input.items.length : 0,
2185
+ "pack.overflow.area": overflowArea,
2186
+ "pack.overflow.forced": bound.forced,
2187
+ "pack.overflow.forcedArea": forcedAreaMin,
2188
+ "pack.overflow.excess": Math.max(0, unplaced.size - bound.forced),
2189
+ "pack.overflow.forcedAreaMin": forcedAreaMin,
2190
+ "pack.overflow.excessArea": excessAreaOf(overflowArea, forcedAreaMin),
1779
2191
  "pack.search.widths": traceSum(output, "pack.search.widths"),
1780
2192
  "pack.search.ms": traceSum(output, "pack.search.ms")
1781
2193
  };
1782
2194
  }
1783
2195
  /**
2196
+ * This function gives the overflow area above its lower bound. A value from `-AREA_NOISE` to 0 becomes 0. A value below
2197
+ * `-AREA_NOISE` stays, because it shows a bug of the bound or of the packer.
2198
+ */
2199
+ function excessAreaOf(overflowArea, forcedAreaMin) {
2200
+ const excess = overflowArea - forcedAreaMin;
2201
+ return excess < 0 && excess >= -1e-9 ? 0 : excess;
2202
+ }
2203
+ /**
1784
2204
  * This function gives the stability measures of a change pair: the run on the base input and the run on the changed
1785
2205
  * input. The items match by id. The values are not rounded.
1786
2206
  *
@@ -2129,6 +2549,7 @@ function packFixedWidth(id, input, options) {
2129
2549
  const start = now();
2130
2550
  const frame = buildFrame(input);
2131
2551
  const rects = rectsOf(frame);
2552
+ clearMoves();
2132
2553
  if (frame.w > 0) pack(rects, outerOf(frame), {
2133
2554
  strategy: id,
2134
2555
  sortOrder: options.sortOrder,
@@ -2140,7 +2561,10 @@ function packFixedWidth(id, input, options) {
2140
2561
  trace: [{
2141
2562
  id,
2142
2563
  ms: now() - start,
2143
- measures: { "pack.overflow": output.overflow.length }
2564
+ measures: {
2565
+ "pack.overflow": output.overflow.length,
2566
+ [MOVES[id]]: moveCount(MOVES[id])
2567
+ }
2144
2568
  }]
2145
2569
  };
2146
2570
  }
@@ -2246,7 +2670,10 @@ const logSweep = {
2246
2670
  }, {
2247
2671
  id,
2248
2672
  ms: ms / runs,
2249
- measures: { "pack.overflow": output.overflow.length }
2673
+ measures: {
2674
+ "pack.overflow": output.overflow.length,
2675
+ [MOVES[id]]: moveCount(MOVES[id])
2676
+ }
2250
2677
  }]
2251
2678
  };
2252
2679
  }
@@ -2282,11 +2709,12 @@ function pushFixedWidth(input, options, previous) {
2282
2709
  }
2283
2710
  const inPlay = frame.entries.filter((e) => !pending.includes(e)).map((e) => e.rect);
2284
2711
  const maxShoves = options.maxShovesPerItem * Math.max(1, inPlay.length);
2712
+ let cascaded = 0;
2285
2713
  for (const e of positioned) {
2286
- pushCascade(e.rect, inPlay, {
2714
+ cascaded += pushCascade(e.rect, inPlay, {
2287
2715
  gap: 0,
2288
2716
  maxShoves
2289
- });
2717
+ }).length;
2290
2718
  e.rect.locked = true;
2291
2719
  }
2292
2720
  const { bounds } = frame;
@@ -2328,7 +2756,10 @@ function pushFixedWidth(input, options, previous) {
2328
2756
  trace: [{
2329
2757
  id: "push",
2330
2758
  ms: pushMs,
2331
- measures: { "pack.push.moved": moved.length }
2759
+ measures: {
2760
+ "pack.push.moved": moved.length,
2761
+ [MOVES.push]: cascaded
2762
+ }
2332
2763
  }, {
2333
2764
  id: "skyline",
2334
2765
  ms: now() - skylineStart,
@@ -2488,8 +2919,12 @@ function inner(box, tolerance) {
2488
2919
  * The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
2489
2920
  * 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
2921
  * shape is relative to the position of the item. The result has no tolerance in it.
2922
+ *
2923
+ * With `options.exactContainer`, a shaped container with the size `fixed` also uses its true outline for `in-container`.
2924
+ * A box then counts when `rectInRegion` of the contract tells that it is not in the shape, as in `packHard`.
2925
+ * For that test, the box grows by the inset and becomes smaller by `tolerance.overlap` on each side. The default is `false`.
2491
2926
  */
2492
- function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2927
+ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE, options = {}) {
2493
2928
  const tol = tolerance.overlap;
2494
2929
  const placements = /* @__PURE__ */ new Map();
2495
2930
  for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
@@ -2554,11 +2989,18 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
2554
2989
  const inset = container.inset ?? 0;
2555
2990
  const fixedW = container.fixed === "size" || container.fixed === "width";
2556
2991
  const fixedH = container.fixed === "size" || container.fixed === "height";
2992
+ const exact = options.exactContainer === true && container.fixed === "size" ? container.shape : void 0;
2557
2993
  let outside = 0;
2558
2994
  for (const box of boxes) {
2559
2995
  const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
2560
2996
  const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
2561
2997
  if (outX || outY) outside += 1;
2998
+ else if (exact !== void 0 && !rectInRegion(exact, inner({
2999
+ l: box.l - inset,
3000
+ t: box.t - inset,
3001
+ r: box.r + inset,
3002
+ b: box.b + inset
3003
+ }, tol))) outside += 1;
2562
3004
  }
2563
3005
  return {
2564
3006
  "no-overlap": overlaps,