@mark1russell7/packing 0.2.2 → 0.2.4

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
@@ -137,6 +137,39 @@ 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 and of each shelf fit. 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
+ /**
146
+ * A rectangle that `packShelf` puts on an open shelf with a taller rectangle. The strip above it stays empty. With a
147
+ * `fit`, the move is a rectangle in the strip above a shorter rectangle of a shelf.
148
+ */
149
+ shelf: "move.stack",
150
+ /** A rectangle that the skyline puts in a lower window, not in the leftmost window where it fits. */
151
+ skyline: "move.lowerWindow",
152
+ /** A rectangle that a push cascade moves. */
153
+ push: "move.cascade",
154
+ /** A rectangle that a shelf fit puts on a shelf that is not the open shelf. The open shelf is the last shelf that the packing opened. */
155
+ firstFit: "move.firstFit",
156
+ /** A rectangle that the fit `best` puts on a shelf that is not the first shelf where it fits. */
157
+ bestFit: "move.bestFit"
158
+ });
159
+ const counts = /* @__PURE__ */ new Map();
160
+ /** This function adds `n` to the count of a move. */
161
+ function countMove(id, n = 1) {
162
+ if (n > 0) counts.set(id, (counts.get(id) ?? 0) + n);
163
+ }
164
+ /** This function sets the count of a move to 0. Without an id, it sets each count to 0. */
165
+ function clearMoves(id) {
166
+ if (id === void 0) counts.clear();
167
+ else counts.delete(id);
168
+ }
169
+ /** This function gives the count of a move since the last `clearMoves`. */
170
+ function moveCount(id) {
171
+ return counts.get(id) ?? 0;
172
+ }
140
173
  // src/order.ts
141
174
  /** This constant lists each sort order. The first one is the default. */
142
175
  const SORT_ORDERS = [
@@ -749,6 +782,12 @@ function freeSpace(input) {
749
782
  return spaceOf(input, buildFrame(input));
750
783
  }
751
784
  // src/shelf.ts
785
+ /** This constant lists each shelf fit. */
786
+ const SHELF_FITS = [
787
+ "next",
788
+ "first",
789
+ "best"
790
+ ];
752
791
  /**
753
792
  * This function gives the first x from `start` where a rectangle of width `w` crosses no obstacle in the band from
754
793
  * `top` to `bottom`.
@@ -783,8 +822,10 @@ function nextTop(obstacles, top, bottom) {
783
822
  * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
784
823
  * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
785
824
  * - With a space that is not plain, `packRowsInSpace` packs the rows into the outline of the space and around its holes.
825
+ * - With `fit`, `packShelvesInSpace` packs the rectangles, in each container shape. A `fit` that is not in `SHELF_FITS` throws a `RangeError`.
786
826
  */
787
827
  function packShelf(rects, outer, options = {}) {
828
+ if (options.fit !== void 0) return packShelvesInSpace(rects, outer, options.sortOrder ?? "height-desc", options.fit, options.space);
788
829
  if (options.space !== void 0 && !options.space.plain) return packRowsInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
789
830
  const width = outer.size.x;
790
831
  const obstacles = [];
@@ -832,6 +873,7 @@ function packShelf(rects, outer, options = {}) {
832
873
  x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
833
874
  }
834
875
  if (outer.fixedSize && rowTop + h > outer.size.y) continue;
876
+ if (!rowEmpty && rowTop + h < rowBottom) countMove(MOVES.shelf);
835
877
  rect.position.set(x + outer.position.x, rowTop + outer.position.y);
836
878
  rect.wasPacked = true;
837
879
  rowEmpty = false;
@@ -946,6 +988,181 @@ function packRowsInSpace(rects, outer, order, space) {
946
988
  if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
947
989
  return outer;
948
990
  }
991
+ /**
992
+ * This function packs rectangles into shelves with a fit: `next`, `first` or `best`. `packShelf` uses it when its
993
+ * options have a `fit`. It sets the position of each rectangle and gives the outer rectangle.
994
+ *
995
+ * - A shelf opens at the bottom edge of the last shelf, at the first x where the rectangle fits in the free space.
996
+ * When no x fits, the shelf steps down to `nextY` of the space, as an empty row of `packRowsInSpace` does.
997
+ * - The first rectangle of a shelf sets its height. A rectangle goes on the floor of a shelf at its cursor, or into
998
+ * the strip above a shorter rectangle of the shelf. The strip is the move `move.stack`.
999
+ * - The fit `next` uses only the open shelf: the last shelf that the packing opened. The fit `first` uses the first
1000
+ * shelf from the top where the rectangle fits. The fit `best` uses the place with the least height left over.
1001
+ * - In a shelf, the rectangle takes the place with the least height left over. A tie goes to a strip, from left to right.
1002
+ * - When no shelf has a place, the last shelf grows for a taller rectangle, if it fits. Else a new shelf opens.
1003
+ * - A rectangle wider than the outer rectangle overflows. Without `fixedSize`, it goes on a closed shelf of its own at the left edge.
1004
+ * - Each place is in the free space and crosses no locked rectangle. Without a space, the outer rectangle and the
1005
+ * locked rectangles give the free space.
1006
+ */
1007
+ function packShelvesInSpace(rects, outer, order, fit, space) {
1008
+ if (!SHELF_FITS.includes(fit)) throw new RangeError(`The shelf fit "${String(fit)}" is not one of ${SHELF_FITS.join(", ")}.`);
1009
+ const width = outer.size.x;
1010
+ const ox = outer.position.x;
1011
+ const oy = outer.position.y;
1012
+ const limit = outer.fixedSize ? outer.size.y : Infinity;
1013
+ const obstacles = [];
1014
+ let contentBottom = 0;
1015
+ for (const rect of rects) {
1016
+ if (!rect.locked) continue;
1017
+ rect.wasPacked = true;
1018
+ const left = rect.position.x - ox;
1019
+ const top = rect.position.y - oy;
1020
+ obstacles.push({
1021
+ left,
1022
+ top,
1023
+ right: left + rect.size.x,
1024
+ bottom: top + rect.size.y
1025
+ });
1026
+ contentBottom = Math.max(contentBottom, top + rect.size.y);
1027
+ }
1028
+ const whole = [{
1029
+ start: 0,
1030
+ end: Infinity
1031
+ }];
1032
+ const place = (start, w, top, h) => {
1033
+ const spans = space === void 0 ? whole : space.band(top + oy, top + h + oy).map((s) => ({
1034
+ start: s.start - ox,
1035
+ end: s.end - ox
1036
+ }));
1037
+ return placeInBand(obstacles, spans, start, w, top, top + h);
1038
+ };
1039
+ const stepDown = (top, w, h) => Math.min(space === void 0 ? Infinity : space.nextY(top + oy, top + h + oy, w) - oy, nextTop(obstacles, top, top + h));
1040
+ const spotsOn = (shelf, w, h, grow) => {
1041
+ const ceiling = shelf.top + shelf.height;
1042
+ const spots = [];
1043
+ if (!grow) {
1044
+ for (const strip of shelf.strips) if (w <= strip.w && strip.bottom + h <= ceiling) spots.push({
1045
+ shelf,
1046
+ x: strip.x,
1047
+ y: strip.bottom,
1048
+ left: ceiling - strip.bottom - h,
1049
+ strip
1050
+ });
1051
+ }
1052
+ const tall = grow ? h > shelf.height && shelf.top + h <= limit : h <= shelf.height;
1053
+ if (Number.isFinite(shelf.cursor) && tall) spots.push({
1054
+ shelf,
1055
+ x: shelf.cursor,
1056
+ y: shelf.top,
1057
+ left: grow ? 0 : shelf.height - h,
1058
+ ...grow ? { grow } : {}
1059
+ });
1060
+ return spots.toSorted((a, b) => a.left - b.left);
1061
+ };
1062
+ const verify = (spot, w, h) => {
1063
+ const x = place(spot.x, w, spot.y, h);
1064
+ if (spot.strip !== void 0) return x === spot.x ? spot : void 0;
1065
+ return x + w <= width ? {
1066
+ ...spot,
1067
+ x
1068
+ } : void 0;
1069
+ };
1070
+ const firstOn = (shelf, w, h) => {
1071
+ for (const spot of spotsOn(shelf, w, h, false)) {
1072
+ const found = verify(spot, w, h);
1073
+ if (found !== void 0) return found;
1074
+ }
1075
+ };
1076
+ const shelves = [];
1077
+ for (const rect of rects.toSorted(compareRects(order))) {
1078
+ if (rect.wasPacked) continue;
1079
+ const w = rect.size.x;
1080
+ const h = rect.size.y;
1081
+ if (!(w > 0 && h > 0)) continue;
1082
+ const open = shelves.at(-1);
1083
+ const floor = open === void 0 ? 0 : open.top + open.height;
1084
+ if (w > width) {
1085
+ if (outer.fixedSize) continue;
1086
+ let top = floor;
1087
+ while (firstFreeX(obstacles, 0, w, top, top + h) !== 0) {
1088
+ const next = nextTop(obstacles, top, top + h);
1089
+ if (!Number.isFinite(next)) break;
1090
+ top = next;
1091
+ }
1092
+ rect.position.set(ox, top + oy);
1093
+ rect.wasPacked = true;
1094
+ shelves.push({
1095
+ top,
1096
+ height: h,
1097
+ cursor: Infinity,
1098
+ strips: []
1099
+ });
1100
+ contentBottom = Math.max(contentBottom, top + h);
1101
+ continue;
1102
+ }
1103
+ let spot;
1104
+ let firstShelf;
1105
+ if (fit === "best") {
1106
+ for (const s of shelves.flatMap((shelf) => spotsOn(shelf, w, h, false)).toSorted((a, b) => a.left - b.left)) {
1107
+ spot = verify(s, w, h);
1108
+ if (spot !== void 0) break;
1109
+ }
1110
+ if (spot !== void 0) firstShelf = shelves.find((shelf) => shelf === spot.shelf || firstOn(shelf, w, h) !== void 0);
1111
+ } else for (const shelf of fit === "next" ? shelves.slice(-1) : shelves) {
1112
+ spot = firstOn(shelf, w, h);
1113
+ if (spot !== void 0) break;
1114
+ }
1115
+ if (spot === void 0 && open !== void 0) {
1116
+ const grown = spotsOn(open, w, h, true)[0];
1117
+ if (grown !== void 0) spot = verify(grown, w, h);
1118
+ }
1119
+ if (spot === void 0) {
1120
+ let top = floor;
1121
+ let x = place(0, w, top, h);
1122
+ while (!(x + w <= width) && top + h <= limit) {
1123
+ const next = stepDown(top, w, h);
1124
+ if (!Number.isFinite(next)) break;
1125
+ top = next;
1126
+ x = place(0, w, top, h);
1127
+ }
1128
+ if (!(x + w <= width) || top + h > limit) continue;
1129
+ const shelf = {
1130
+ top,
1131
+ height: h,
1132
+ cursor: x,
1133
+ strips: []
1134
+ };
1135
+ shelves.push(shelf);
1136
+ spot = {
1137
+ shelf,
1138
+ x,
1139
+ y: top,
1140
+ left: 0
1141
+ };
1142
+ } else {
1143
+ if (spot.shelf !== open) countMove(MOVES.firstFit);
1144
+ if (firstShelf !== void 0 && spot.shelf !== firstShelf) countMove(MOVES.bestFit);
1145
+ }
1146
+ const { shelf } = spot;
1147
+ rect.position.set(spot.x + ox, spot.y + oy);
1148
+ rect.wasPacked = true;
1149
+ if (spot.strip !== void 0) {
1150
+ countMove(MOVES.shelf);
1151
+ spot.strip.bottom = spot.y + h;
1152
+ } else {
1153
+ shelf.cursor = spot.x + w;
1154
+ shelf.strips.push({
1155
+ x: spot.x,
1156
+ w,
1157
+ bottom: spot.y + h
1158
+ });
1159
+ if (spot.grow === true) shelf.height = h;
1160
+ }
1161
+ contentBottom = Math.max(contentBottom, spot.y + h);
1162
+ }
1163
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
1164
+ return outer;
1165
+ }
949
1166
  // src/skyline.ts
950
1167
  /**
951
1168
  * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
@@ -956,6 +1173,14 @@ function packRowsInSpace(rects, outer, order, space) {
956
1173
  * `limit[x, x + rw)`. A second deque keeps that smallest value. No valid window gives the height `Infinity`.
957
1174
  */
958
1175
  function lowestWindow(skyline, width, rw, limit, rh = 0) {
1176
+ const { x, y } = findWindow(skyline, width, rw, limit, rh);
1177
+ return {
1178
+ x,
1179
+ y
1180
+ };
1181
+ }
1182
+ /** This function is `lowestWindow`, and it also gives the x of the leftmost valid window. */
1183
+ function findWindow(skyline, width, rw, limit, rh = 0) {
959
1184
  if (limit !== void 0) return lowestLimitedWindow(skyline, limit, width, rw, rh);
960
1185
  const deque = new Int32Array(width);
961
1186
  let head = 0;
@@ -976,9 +1201,14 @@ function lowestWindow(skyline, width, rw, limit, rh = 0) {
976
1201
  }
977
1202
  return {
978
1203
  x: bestX,
979
- y: bestY
1204
+ y: bestY,
1205
+ first: width >= rw ? 0 : -1
980
1206
  };
981
1207
  }
1208
+ /** This function counts the move of the skyline: a window that is lower than the leftmost valid window. */
1209
+ function countWindow(w) {
1210
+ if (Number.isFinite(w.y) && w.first >= 0 && w.x !== w.first) countMove(MOVES.skyline);
1211
+ }
982
1212
  /** This function is `lowestWindow` with a limit for each cell. */
983
1213
  function lowestLimitedWindow(skyline, limit, width, rw, rh) {
984
1214
  const high = new Int32Array(width);
@@ -989,6 +1219,7 @@ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
989
1219
  let lt = 0;
990
1220
  let bestX = 0;
991
1221
  let bestY = Infinity;
1222
+ let first = -1;
992
1223
  for (let i = 0; i < width; i++) {
993
1224
  while (ht > hh && skyline[high[ht - 1]] <= skyline[i]) ht--;
994
1225
  high[ht++] = i;
@@ -998,7 +1229,9 @@ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
998
1229
  if (low[lh] <= i - rw) lh++;
999
1230
  if (i >= rw - 1) {
1000
1231
  const max = skyline[high[hh]];
1001
- if (max < bestY && max + rh <= limit[low[lh]]) {
1232
+ const valid = max + rh <= limit[low[lh]];
1233
+ if (valid && first < 0) first = i - rw + 1;
1234
+ if (max < bestY && valid) {
1002
1235
  bestY = max;
1003
1236
  bestX = i - rw + 1;
1004
1237
  }
@@ -1006,7 +1239,8 @@ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
1006
1239
  }
1007
1240
  return {
1008
1241
  x: bestX,
1009
- y: bestY
1242
+ y: bestY,
1243
+ first
1010
1244
  };
1011
1245
  }
1012
1246
  /**
@@ -1050,8 +1284,10 @@ function packSkyline(rects, outer, options = {}) {
1050
1284
  y = 0;
1051
1285
  for (let i = 0; i < width; i++) if (skyline[i] > y) y = skyline[i];
1052
1286
  } else {
1053
- ({x, y} = lowestWindow(skyline, width, rw));
1287
+ const found = findWindow(skyline, width, rw);
1288
+ ({x, y} = found);
1054
1289
  if (outer.fixedSize && y + rh > outer.size.y) continue;
1290
+ countWindow(found);
1055
1291
  }
1056
1292
  rect.position.set(x + outer.position.x, y + outer.position.y);
1057
1293
  rect.wasPacked = true;
@@ -1104,16 +1340,19 @@ function packSkylineInSpace(rects, outer, order, space) {
1104
1340
  outer.size.y = Math.max(outer.size.y, y + rh);
1105
1341
  continue;
1106
1342
  }
1107
- ({x, y} = lowestWindow(skyline, width, rw, limit, rh));
1343
+ let found = findWindow(skyline, width, rw, limit, rh);
1344
+ ({x, y} = found);
1108
1345
  let sky = skyline;
1109
1346
  let lim = limit;
1110
1347
  if (!Number.isFinite(y)) {
1111
1348
  sky = skyline.slice();
1112
1349
  lim = limit.slice();
1113
1350
  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));
1351
+ found = findWindow(sky, width, rw, lim, rh);
1352
+ ({x, y} = found);
1115
1353
  if (!Number.isFinite(y)) continue;
1116
1354
  }
1355
+ countWindow(found);
1117
1356
  rect.position.set(x + ox, y + oy);
1118
1357
  rect.wasPacked = true;
1119
1358
  const top = y + rh;
@@ -1126,10 +1365,19 @@ function packSkylineInSpace(rects, outer, order, space) {
1126
1365
  return outer;
1127
1366
  }
1128
1367
  // src/pack.ts
1368
+ /** The depth of the calls of `packInto`. Only the outer gap fill counts its rectangles as moves. */
1369
+ let fillDepth = 0;
1129
1370
  function packInto(rects, region, offset, order) {
1130
1371
  const unpacked = Rect.unpacked(rects);
1131
- pack(unpacked, region, { sortOrder: order });
1132
- for (const r of Rect.packed(unpacked)) r.translateBy(offset);
1372
+ fillDepth++;
1373
+ try {
1374
+ pack(unpacked, region, { sortOrder: order });
1375
+ } finally {
1376
+ fillDepth--;
1377
+ }
1378
+ const placed = Rect.packed(unpacked);
1379
+ if (fillDepth === 0) countMove(MOVES.rows, placed.length);
1380
+ for (const r of placed) r.translateBy(offset);
1133
1381
  }
1134
1382
  function packRow(rects, outer, order) {
1135
1383
  const row = [];
@@ -1187,7 +1435,7 @@ function packRows(rects, outer, options = {}) {
1187
1435
  const space = options.space;
1188
1436
  if (space !== void 0 && (!space.plain || rects.some((r) => r.locked))) return packRowsInSpace(rects, outer, order, space);
1189
1437
  for (const r of rects) if (r.locked) r.wasPacked = true;
1190
- while (!packRow(rects, outer, order));
1438
+ while (!packRow(rects, outer, order)) if (fillDepth === 0) clearMoves(MOVES.rows);
1191
1439
  return outer;
1192
1440
  }
1193
1441
  /**
@@ -1264,6 +1512,7 @@ function packFree(rects, outer = new Rect(), options = {}) {
1264
1512
  const maxWidth = Math.max(minWidth, rowWidth);
1265
1513
  const tried = [];
1266
1514
  const run = (width) => {
1515
+ clearMoves();
1267
1516
  Rect.resetPacked(rects);
1268
1517
  const box = new Rect();
1269
1518
  box.position.setTo(outer.position);
@@ -1934,6 +2183,8 @@ const REL = Object.freeze({ rel: .01 });
1934
2183
  * - A share of an area from 0 to 1 has 0.005. A guard has its tolerance.
1935
2184
  * - An area and a ratio of lengths have `{ rel: 0.01 }`: 1% of the larger value.
1936
2185
  * - The time has no minimum effect, because the times of CI change each time.
2186
+ * - `pack.overflow.forcedAreaMin` is a deprecated alias of `pack.overflow.forcedArea`. Its spec is the same, except the id.
2187
+ * The alias stays until the next minor version.
1937
2188
  */
1938
2189
  const MEASURE_SPECS = Object.freeze({
1939
2190
  "pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity, REL),
@@ -1959,6 +2210,8 @@ const MEASURE_SPECS = Object.freeze({
1959
2210
  "pack.overflow.forced": spec("pack.overflow.forced", "overflow", "count", "lower", 0, Infinity, 0),
1960
2211
  "pack.overflow.forcedArea": spec("pack.overflow.forcedArea", "overflow", "share", "lower", 0, 1, .005),
1961
2212
  "pack.overflow.excess": spec("pack.overflow.excess", "overflow", "count", "lower", 0, Infinity, 0),
2213
+ "pack.overflow.forcedAreaMin": spec("pack.overflow.forcedAreaMin", "overflow", "share", "lower", 0, 1, .005),
2214
+ "pack.overflow.excessArea": spec("pack.overflow.excessArea", "overflow", "share", "lower", 0, 1, .005),
1962
2215
  "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, .02, {
1963
2216
  floor: 0,
1964
2217
  tol: .02
@@ -2064,6 +2317,8 @@ function traceSum(output, id) {
2064
2317
  * - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
2065
2318
  * - The overflow measures count each id once. `pack.overflow.forced` is the lower bound of `overflowBound`.
2066
2319
  * - `pack.overflow.excess` is the overflow less the forced overflow. A container with no fixed side gives 0 for both.
2320
+ * - `pack.overflow.forcedArea` is a lower bound of the overflow area. `pack.overflow.excessArea` is the overflow area above it.
2321
+ * - `pack.overflow.forcedAreaMin` is a deprecated alias of `pack.overflow.forcedArea`, with the same value.
2067
2322
  *
2068
2323
  * @example
2069
2324
  * ```ts
@@ -2107,6 +2362,8 @@ function measure(input, output, options = {}) {
2107
2362
  if (unplaced.has(item.id)) unplacedArea += a;
2108
2363
  }
2109
2364
  const bound = overflowBound(input);
2365
+ const overflowArea = itemArea > 0 ? unplacedArea / itemArea : 0;
2366
+ const forcedArea = itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0;
2110
2367
  return {
2111
2368
  "pack.area": hullArea,
2112
2369
  "pack.fill": fill,
@@ -2118,15 +2375,25 @@ function measure(input, output, options = {}) {
2118
2375
  "pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
2119
2376
  "pack.overflow": unplaced.size,
2120
2377
  "pack.overflow.rate": input.items.length > 0 ? unplaced.size / input.items.length : 0,
2121
- "pack.overflow.area": itemArea > 0 ? unplacedArea / itemArea : 0,
2378
+ "pack.overflow.area": overflowArea,
2122
2379
  "pack.overflow.forced": bound.forced,
2123
- "pack.overflow.forcedArea": itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0,
2380
+ "pack.overflow.forcedArea": forcedArea,
2124
2381
  "pack.overflow.excess": Math.max(0, unplaced.size - bound.forced),
2382
+ "pack.overflow.forcedAreaMin": forcedArea,
2383
+ "pack.overflow.excessArea": excessAreaOf(overflowArea, forcedArea),
2125
2384
  "pack.search.widths": traceSum(output, "pack.search.widths"),
2126
2385
  "pack.search.ms": traceSum(output, "pack.search.ms")
2127
2386
  };
2128
2387
  }
2129
2388
  /**
2389
+ * This function gives the overflow area above its lower bound. A value from `-AREA_NOISE` to 0 becomes 0. A value below
2390
+ * `-AREA_NOISE` stays, because it shows a bug of the bound or of the packer.
2391
+ */
2392
+ function excessAreaOf(overflowArea, forcedArea) {
2393
+ const excess = overflowArea - forcedArea;
2394
+ return excess < 0 && excess >= -1e-9 ? 0 : excess;
2395
+ }
2396
+ /**
2130
2397
  * This function gives the stability measures of a change pair: the run on the base input and the run on the changed
2131
2398
  * input. The items match by id. The values are not rounded.
2132
2399
  *
@@ -2470,15 +2737,28 @@ function supportsOf(cells) {
2470
2737
  mode: [...cells.mode]
2471
2738
  };
2472
2739
  }
2740
+ /** This function gives the fit of the options of a strategy: only `shelf` reads it. */
2741
+ function fitOf(id, options) {
2742
+ return id === "shelf" && options.fit !== void 0 ? { fit: options.fit } : {};
2743
+ }
2744
+ /** This function gives the move counts of the trace of a strategy: its own move, and the moves of a shelf fit. */
2745
+ function movesOf(id, fit) {
2746
+ const out = { [MOVES[id]]: moveCount(MOVES[id]) };
2747
+ if (fit.fit !== void 0) for (const move of [MOVES.firstFit, MOVES.bestFit]) out[move] = moveCount(move);
2748
+ return out;
2749
+ }
2473
2750
  /** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
2474
2751
  function packFixedWidth(id, input, options) {
2475
2752
  const start = now();
2476
2753
  const frame = buildFrame(input);
2477
2754
  const rects = rectsOf(frame);
2755
+ const fit = fitOf(id, options);
2756
+ clearMoves();
2478
2757
  if (frame.w > 0) pack(rects, outerOf(frame), {
2479
2758
  strategy: id,
2480
2759
  sortOrder: options.sortOrder,
2481
- space: spaceOf(input, frame)
2760
+ space: spaceOf(input, frame),
2761
+ ...fit
2482
2762
  });
2483
2763
  const output = readFrame(input, frame, []);
2484
2764
  return {
@@ -2486,7 +2766,10 @@ function packFixedWidth(id, input, options) {
2486
2766
  trace: [{
2487
2767
  id,
2488
2768
  ms: now() - start,
2489
- measures: { "pack.overflow": output.overflow.length }
2769
+ measures: {
2770
+ "pack.overflow": output.overflow.length,
2771
+ ...movesOf(id, fit)
2772
+ }
2490
2773
  }]
2491
2774
  };
2492
2775
  }
@@ -2544,6 +2827,7 @@ const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
2544
2827
  /**
2545
2828
  * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
2546
2829
  * With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
2830
+ * The option `fit` selects a shelf rule of roadmap item k21: `next`, `first` or `best`. The default has no `fit`.
2547
2831
  */
2548
2832
  const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
2549
2833
  obstacleMove: ["fixed"],
@@ -2569,13 +2853,15 @@ const logSweep = {
2569
2853
  };
2570
2854
  const start = now();
2571
2855
  const frame = buildFrame(input);
2856
+ const fit = fitOf(id, o);
2572
2857
  const result = packFree(rectsOf(frame), new Rect(), {
2573
2858
  strategy: id,
2574
2859
  sortOrder: o.sortOrder,
2575
2860
  space: spaceOf(input, frame),
2576
2861
  samples: o.samples,
2577
2862
  aspect: input.container.aspect ?? 1,
2578
- aspectWeight: input.container.aspectWeight ?? 1
2863
+ aspectWeight: input.container.aspectWeight ?? 1,
2864
+ ...fit
2579
2865
  });
2580
2866
  const ms = now() - start;
2581
2867
  const runs = result.tried.length + 1;
@@ -2592,7 +2878,10 @@ const logSweep = {
2592
2878
  }, {
2593
2879
  id,
2594
2880
  ms: ms / runs,
2595
- measures: { "pack.overflow": output.overflow.length }
2881
+ measures: {
2882
+ "pack.overflow": output.overflow.length,
2883
+ ...movesOf(id, fit)
2884
+ }
2596
2885
  }]
2597
2886
  };
2598
2887
  }
@@ -2628,11 +2917,12 @@ function pushFixedWidth(input, options, previous) {
2628
2917
  }
2629
2918
  const inPlay = frame.entries.filter((e) => !pending.includes(e)).map((e) => e.rect);
2630
2919
  const maxShoves = options.maxShovesPerItem * Math.max(1, inPlay.length);
2920
+ let cascaded = 0;
2631
2921
  for (const e of positioned) {
2632
- pushCascade(e.rect, inPlay, {
2922
+ cascaded += pushCascade(e.rect, inPlay, {
2633
2923
  gap: 0,
2634
2924
  maxShoves
2635
- });
2925
+ }).length;
2636
2926
  e.rect.locked = true;
2637
2927
  }
2638
2928
  const { bounds } = frame;
@@ -2674,7 +2964,10 @@ function pushFixedWidth(input, options, previous) {
2674
2964
  trace: [{
2675
2965
  id: "push",
2676
2966
  ms: pushMs,
2677
- measures: { "pack.push.moved": moved.length }
2967
+ measures: {
2968
+ "pack.push.moved": moved.length,
2969
+ [MOVES.push]: cascaded
2970
+ }
2678
2971
  }, {
2679
2972
  id: "skyline",
2680
2973
  ms: now() - skylineStart,
@@ -3838,6 +4131,6 @@ function corpus(name, specs) {
3838
4131
  inputs
3839
4132
  };
3840
4133
  }
3841
- export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellOf, checkHard, checkInput, compareRects, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, freeSpace, fromRects, gallery, generator, generators, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, measure, measureChange, medianRatio, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, push, pushCascade, quantile, rectGrid, rectScattered, round3, roundMeasures, rows, shelf, skyline, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
4134
+ export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SHELF_FITS, 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 };
3842
4135
 
3843
4136
  //# sourceMappingURL=index.js.map