@mark1russell7/packing 0.2.4 → 0.2.6

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, rectInRegion, regionHitsRect, translate, turnBox, turnPoint, turnPort, turnRegion } from "@mark1russell7/layout-contract";
1
+ import { DEFAULT_TOLERANCE, EPS, RECORD_EPS, boundsOf, boundsOf as boundsOf$1, checkInput, marginSides, pickMoves, portfolio, 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 {
@@ -154,7 +154,17 @@ const MOVES = Object.freeze({
154
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
155
  firstFit: "move.firstFit",
156
156
  /** A rectangle that the fit `best` puts on a shelf that is not the first shelf where it fits. */
157
- bestFit: "move.bestFit"
157
+ bestFit: "move.bestFit",
158
+ /** A rectangle that the rule `bssf` of `maxrects` puts in another place than the rule `bl` on the same free rectangles. */
159
+ bssf: "move.bssf",
160
+ /** A rectangle that the rule `blsf` of `maxrects` puts in another place than the rule `bl` on the same free rectangles. */
161
+ blsf: "move.blsf",
162
+ /** A rectangle that the rule `baf` of `maxrects` puts in another place than the rule `bl` on the same free rectangles. */
163
+ baf: "move.baf",
164
+ /** A rectangle that the rule `cp` of `maxrects` puts in another place than the rule `bl` on the same free rectangles. */
165
+ cp: "move.cp",
166
+ /** A rectangle of `maxrects` that cuts two free rectangles or more. A shelf or a skyline has no such step. */
167
+ split: "move.split"
158
168
  });
159
169
  const counts = /* @__PURE__ */ new Map();
160
170
  /** This function adds `n` to the count of a move. */
@@ -241,9 +251,10 @@ function byRank(items) {
241
251
  }
242
252
  /**
243
253
  * This function makes the frame of an input. A locked item with a position and an obstacle without `move` are locked
244
- * rectangles. An obstacle with `move` is a free rectangle only when `movable` is `true`.
254
+ * rectangles. An obstacle with `move` is a free rectangle only when `movable` is `true`. With `turnable`, a turnable item
255
+ * is too large only when it is too large in the two directions.
245
256
  */
246
- function buildFrame(input, movable = false) {
257
+ function buildFrame(input, movable = false, turnable = false) {
247
258
  const bounds = containerBounds(input);
248
259
  const inset = input.container.inset ?? 0;
249
260
  const half = (input.gap ?? 0) / 2;
@@ -262,7 +273,7 @@ function buildFrame(input, movable = false) {
262
273
  if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
263
274
  rect.locked = true;
264
275
  rect.position.set(item.x - dx - ox, item.y - dy - oy);
265
- } else if (rect.size.x > w + EPS || rect.size.y > h + EPS) {
276
+ } else if ((rect.size.x > w + EPS || rect.size.y > h + EPS) && !(turnable && item.turnable === true && rect.size.y <= w + EPS && rect.size.x <= h + EPS)) {
266
277
  tooLarge.push(item.id);
267
278
  continue;
268
279
  }
@@ -311,10 +322,18 @@ function rectsOf(frame) {
311
322
  return frame.entries.map((e) => e.rect);
312
323
  }
313
324
  /**
325
+ * This function gives the turn of an item for a frame. A frame of `viaAxes` has the axes changed. The turn of the input
326
+ * goes through that change, thus the item turns the other way in the frame.
327
+ */
328
+ function frameTurn(transposed) {
329
+ return transposed ? (item) => transposeItem(turnItem(transposeItem(item))) : turnItem;
330
+ }
331
+ /**
314
332
  * This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
315
- * position exactly. Each other item that the packer did not place goes to the overflow.
333
+ * position exactly. Each other item that the packer did not place goes to the overflow. With `turns`, each rectangle
334
+ * that turned gives a placement with `turn: 90`.
316
335
  */
317
- function readFrame(input, frame, trace, moved = []) {
336
+ function readFrame(input, frame, trace, moved = [], turns) {
318
337
  const placements = [];
319
338
  const overflow = [...frame.tooLarge];
320
339
  for (const e of frame.entries) {
@@ -325,7 +344,16 @@ function readFrame(input, frame, trace, moved = []) {
325
344
  x: item.x,
326
345
  y: item.y
327
346
  });
328
- else if (e.rect.wasPacked) placements.push({
347
+ else if (e.rect.wasPacked && turns?.rects.has(e.rect) === true) {
348
+ const half = e.dx - marginOf(item).left;
349
+ const m = marginOf(turns.turn(item));
350
+ placements.push({
351
+ id: item.id,
352
+ x: e.rect.position.x + m.left + half + frame.ox,
353
+ y: e.rect.position.y + m.top + half + frame.oy,
354
+ turn: 90
355
+ });
356
+ } else if (e.rect.wasPacked) placements.push({
329
357
  id: item.id,
330
358
  x: e.rect.position.x + e.dx + frame.ox,
331
359
  y: e.rect.position.y + e.dy + frame.oy
@@ -335,7 +363,7 @@ function readFrame(input, frame, trace, moved = []) {
335
363
  return {
336
364
  placements,
337
365
  overflow,
338
- size: sizeOf(input, frame, placements, moved),
366
+ size: sizeOf(input, frame, placements, moved, turns),
339
367
  moved,
340
368
  trace
341
369
  };
@@ -344,12 +372,13 @@ function readFrame(input, frame, trace, moved = []) {
344
372
  * This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
345
373
  * far edge of the items with their margins and the inset, or of the obstacles.
346
374
  */
347
- function sizeOf(input, frame, placements, moved) {
375
+ function sizeOf(input, frame, placements, moved, turns) {
348
376
  const items = new Map(input.items.map((i) => [i.id, i]));
349
377
  let right = 0;
350
378
  let bottom = 0;
351
379
  for (const p of placements) {
352
- const item = items.get(p.id);
380
+ const given = items.get(p.id);
381
+ const item = p.turn === 90 && turns !== void 0 ? turns.turn(given) : given;
353
382
  const m = marginOf(item);
354
383
  right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
355
384
  bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
@@ -749,7 +778,7 @@ function spaceOf(input, frame) {
749
778
  start,
750
779
  end
751
780
  }));
752
- return {
781
+ const space = {
753
782
  w: frame.w,
754
783
  h: frame.h,
755
784
  plain,
@@ -773,6 +802,33 @@ function spaceOf(input, frame) {
773
802
  return nextOf(layer, y0, y1, w);
774
803
  }
775
804
  };
805
+ const bare = {
806
+ ...layer,
807
+ holes: []
808
+ };
809
+ let bottom = 0;
810
+ for (const b of blockers) bottom = Math.max(bottom, b.c1 + Math.max(0, b.g));
811
+ PARTS.set(space, {
812
+ kind: layer.kind,
813
+ d,
814
+ holes: holes.map((r) => ({
815
+ x: r.a0,
816
+ y: r.c0,
817
+ w: r.a1 - r.a0,
818
+ h: r.c1 - r.c0
819
+ })),
820
+ bottom: Number.isFinite(frame.h) ? frame.h : bottom,
821
+ bare: (y0, y1) => spansOf(bare, y0, y1).map(([start, end]) => ({
822
+ start,
823
+ end
824
+ }))
825
+ });
826
+ return space;
827
+ }
828
+ const PARTS = /* @__PURE__ */ new WeakMap();
829
+ /** This function gives the parts of a free space of `spaceOf`, or `undefined` for another free space. */
830
+ function spaceParts(space) {
831
+ return PARTS.get(space);
776
832
  }
777
833
  /**
778
834
  * This function gives the free space of an input, in the coordinates of the frame of the adapters. The origin of the
@@ -781,6 +837,366 @@ function spaceOf(input, frame) {
781
837
  function freeSpace(input) {
782
838
  return spaceOf(input, buildFrame(input));
783
839
  }
840
+ // src/maxrects.ts
841
+ /**
842
+ * The rules of `maxrects` to choose a free rectangle for a box.
843
+ *
844
+ * - `bssf`: the best short side fit. The smaller leftover side is the smallest.
845
+ * - `blsf`: the best long side fit. The larger leftover side is the smallest.
846
+ * - `baf`: the best area fit. The leftover area is the smallest.
847
+ * - `bl`: the bottom-left rule. The bottom edge of the box is the highest, then the left edge is the leftmost.
848
+ * - `cp`: the contact point rule. The box touches the most length of the edges of the frame, the boxes and the obstacles.
849
+ */
850
+ const MAXRECTS_RULES = Object.freeze([
851
+ "bssf",
852
+ "blsf",
853
+ "baf",
854
+ "bl",
855
+ "cp"
856
+ ]);
857
+ /** The number of bands that the default band height gives. The default band height is the height to cut divided by this number. */
858
+ const MAXRECTS_BANDS = 128;
859
+ /** This function tells if the box `a` is fully in the box `b`. */
860
+ function inside(a, b) {
861
+ return a.x0 >= b.x0 && a.y0 >= b.y0 && a.x1 <= b.x1 && a.y1 <= b.y1;
862
+ }
863
+ /**
864
+ * This function cuts each free rectangle that the box `b` hits into at most four parts. A part with a side less than
865
+ * `minW` or `minH` goes, because no box fits in it. A part that is in another rectangle of the list goes.
866
+ * The function gives the new list and the number of free rectangles that the box hit.
867
+ */
868
+ function cut(free, b, minW, minH) {
869
+ const kept = [];
870
+ const parts = [];
871
+ let hits = 0;
872
+ const add = (x0, y0, x1, y1) => {
873
+ if (x1 - x0 >= minW && y1 - y0 >= minH) parts.push({
874
+ x0,
875
+ y0,
876
+ x1,
877
+ y1
878
+ });
879
+ };
880
+ for (const f of free) {
881
+ if (b.x0 >= f.x1 || b.x1 <= f.x0 || b.y0 >= f.y1 || b.y1 <= f.y0) {
882
+ kept.push(f);
883
+ continue;
884
+ }
885
+ hits++;
886
+ if (b.x0 > f.x0) add(f.x0, f.y0, b.x0, f.y1);
887
+ if (b.x1 < f.x1) add(b.x1, f.y0, f.x1, f.y1);
888
+ if (b.y0 > f.y0) add(f.x0, f.y0, f.x1, b.y0);
889
+ if (b.y1 < f.y1) add(f.x0, b.y1, f.x1, f.y1);
890
+ }
891
+ const count = kept.length;
892
+ for (let i = 0; i < parts.length; i++) {
893
+ const p = parts[i];
894
+ let drop = false;
895
+ for (let j = 0; j < parts.length && !drop; j++) {
896
+ if (j === i) continue;
897
+ const q = parts[j];
898
+ if (inside(p, q) && (j < i || !inside(q, p))) drop = true;
899
+ }
900
+ for (let k = 0; k < count && !drop; k++) if (inside(p, kept[k])) drop = true;
901
+ if (!drop) kept.push(p);
902
+ }
903
+ return {
904
+ free: kept,
905
+ hits
906
+ };
907
+ }
908
+ /** This function adds a range to the edges at a coordinate. */
909
+ function addEdge(edges, at, a, b) {
910
+ const list = edges.get(at);
911
+ if (list === void 0) edges.set(at, [a, b]);
912
+ else list.push(a, b);
913
+ }
914
+ /** This function gives the length of the common part of the range from `a` to `b` and the edges at a coordinate. */
915
+ function touch(edges, at, a, b) {
916
+ const list = edges.get(at);
917
+ if (list === void 0) return 0;
918
+ let sum = 0;
919
+ for (let i = 0; i < list.length; i += 2) sum += Math.max(0, Math.min(b, list[i + 1]) - Math.max(a, list[i]));
920
+ return sum;
921
+ }
922
+ /**
923
+ * This function gives the walls of a space that is not plain: the parts of the bands that are not free. The bands cut the
924
+ * height from 0 to `top`. Two walls of neighbour bands with the same span become one wall.
925
+ */
926
+ function bandWalls(query, w, top, bandHeight, grow, end) {
927
+ const walls = [];
928
+ const count = Math.max(1, Math.ceil(top / bandHeight));
929
+ let open = /* @__PURE__ */ new Map();
930
+ for (let k = 0; k < count; k++) {
931
+ const y0 = top * k / count;
932
+ const y1 = k === count - 1 ? top : top * (k + 1) / count;
933
+ const spans = query(Math.max(0, y0 - grow), Math.min(end, y1 + grow));
934
+ const next = /* @__PURE__ */ new Map();
935
+ let x = 0;
936
+ const block = (a, b) => {
937
+ if (!(b > a)) return;
938
+ const key = `${a}|${b}`;
939
+ const wall = open.get(key);
940
+ if (wall !== void 0) {
941
+ wall.y1 = y1;
942
+ open.delete(key);
943
+ next.set(key, wall);
944
+ } else next.set(key, {
945
+ x0: a,
946
+ y0,
947
+ x1: b,
948
+ y1
949
+ });
950
+ };
951
+ for (const s of spans) {
952
+ const start = Math.max(0, Math.min(w, s.start));
953
+ const stop = Math.max(0, Math.min(w, s.end));
954
+ if (!(stop > start)) continue;
955
+ block(x, start);
956
+ x = Math.max(x, stop);
957
+ }
958
+ block(x, w);
959
+ walls.push(...open.values());
960
+ open = next;
961
+ }
962
+ walls.push(...open.values());
963
+ return walls;
964
+ }
965
+ /**
966
+ * This function packs rectangles with MaxRects, as `packMaxRects` tells. It also gives the free rectangles at the end.
967
+ * The tests read them. The package root does not export this function.
968
+ */
969
+ function runMaxRects(rects, outer, options = {}) {
970
+ const rule = options.rule ?? "bssf";
971
+ if (!MAXRECTS_RULES.includes(rule)) throw new RangeError(`The rule of maxrects is one of ${MAXRECTS_RULES.join(", ")}, not "${String(rule)}".`);
972
+ const turned = options.turned;
973
+ if (turned !== void 0) {
974
+ for (const r of rects) if (turned.has(r)) r.size.set(r.size.y, r.size.x);
975
+ turned.clear();
976
+ }
977
+ for (const r of rects) if (r.locked) r.wasPacked = true;
978
+ const W = outer.size.x;
979
+ if (!(W > 0)) return {
980
+ outer,
981
+ free: []
982
+ };
983
+ const ox = outer.position.x;
984
+ const oy = outer.position.y;
985
+ const fixed = outer.fixedSize;
986
+ const space = options.space;
987
+ const parts = space === void 0 ? void 0 : spaceParts(space);
988
+ const turnable = options.turnable;
989
+ const sorted = rects.toSorted(compareRects(options.sortOrder ?? "height-desc")).filter((r) => !r.wasPacked && r.size.x > 0 && r.size.y > 0);
990
+ let minW = Infinity;
991
+ let minH = Infinity;
992
+ let heights = 0;
993
+ for (const r of sorted) {
994
+ const turns = turnable?.has(r) === true;
995
+ const short = Math.min(r.size.x, r.size.y);
996
+ minW = Math.min(minW, turns ? short : r.size.x);
997
+ minH = Math.min(minH, turns ? short : r.size.y);
998
+ heights += turns ? Math.max(r.size.x, r.size.y) : r.size.y;
999
+ }
1000
+ const holes = [];
1001
+ if (parts !== void 0) for (const h of parts.holes) holes.push({
1002
+ x0: h.x - ox,
1003
+ y0: h.y - oy,
1004
+ x1: h.x + h.w - ox,
1005
+ y1: h.y + h.h - oy
1006
+ });
1007
+ else if (space === void 0 || space.plain) {
1008
+ for (const r of rects) if (r.locked) holes.push({
1009
+ x0: r.position.x - ox,
1010
+ y0: r.position.y - oy,
1011
+ x1: r.right() - ox,
1012
+ y1: r.bottom() - oy
1013
+ });
1014
+ }
1015
+ let floor = parts?.bottom ?? 0;
1016
+ for (const h of holes) floor = Math.max(floor, h.y1);
1017
+ const H = fixed ? outer.size.y : floor + heights;
1018
+ const walls = [];
1019
+ for (const h of holes) {
1020
+ const wall = {
1021
+ x0: Math.max(0, h.x0),
1022
+ y0: Math.max(0, h.y0),
1023
+ x1: Math.min(W, h.x1),
1024
+ y1: Math.min(H, h.y1)
1025
+ };
1026
+ if (wall.x1 > wall.x0 && wall.y1 > wall.y0) walls.push(wall);
1027
+ }
1028
+ if (space !== void 0 && !space.plain) {
1029
+ const top = parts === void 0 ? H : fixed ? H : Math.min(H, parts.bottom);
1030
+ if (top > 0) {
1031
+ const bandHeight = options.bandHeight ?? top / 128;
1032
+ if (!(bandHeight > 0)) throw new RangeError(`The band height of maxrects is more than 0, not ${String(bandHeight)}.`);
1033
+ const query = parts === void 0 ? (y0, y1) => space.band(y0 + oy, y1 + oy) : (y0, y1) => parts.bare(y0 + oy, y1 + oy);
1034
+ const shifted = (y0, y1) => query(y0, y1).map((s) => ({
1035
+ start: s.start - ox,
1036
+ end: s.end - ox
1037
+ }));
1038
+ walls.push(...bandWalls(shifted, W, top, bandHeight, parts === void 0 ? 0 : Math.max(0, -parts.d), fixed ? H : Infinity));
1039
+ }
1040
+ }
1041
+ let free = [{
1042
+ x0: 0,
1043
+ y0: 0,
1044
+ x1: W,
1045
+ y1: H
1046
+ }];
1047
+ for (const wall of walls) free = cut(free, wall, minW, minH).free;
1048
+ const cp = rule === "cp";
1049
+ const vertical = /* @__PURE__ */ new Map();
1050
+ const horizontal = /* @__PURE__ */ new Map();
1051
+ const mark = (b) => {
1052
+ if (!cp) return;
1053
+ addEdge(vertical, b.x0, b.y0, b.y1);
1054
+ addEdge(vertical, b.x1, b.y0, b.y1);
1055
+ addEdge(horizontal, b.y0, b.x0, b.x1);
1056
+ addEdge(horizontal, b.y1, b.x0, b.x1);
1057
+ };
1058
+ for (const wall of walls) mark(wall);
1059
+ const contact = (x, y, w, h) => {
1060
+ let sum = 0;
1061
+ if (x === 0) sum += h;
1062
+ if (x + w === W) sum += h;
1063
+ if (y === 0) sum += w;
1064
+ if (fixed && y + h === H) sum += w;
1065
+ return sum + touch(vertical, x, y, y + h) + touch(vertical, x + w, y, y + h) + touch(horizontal, y, x, x + w) + touch(horizontal, y + h, x, x + w);
1066
+ };
1067
+ const check = space !== void 0 && !space.plain;
1068
+ let bottom = 0;
1069
+ for (const wall of walls) bottom = Math.max(bottom, wall.y1);
1070
+ for (const rect of sorted) {
1071
+ const w = rect.size.x;
1072
+ const h = rect.size.y;
1073
+ const turns = turnable?.has(rect) === true && w !== h;
1074
+ const banned = /* @__PURE__ */ new Set();
1075
+ let best;
1076
+ let low;
1077
+ for (;;) {
1078
+ best = void 0;
1079
+ low = void 0;
1080
+ for (let i = 0; i < free.length; i++) {
1081
+ const f = free[i];
1082
+ for (let t = 0; t < (turns ? 2 : 1); t++) {
1083
+ if (banned.has(2 * i + t)) continue;
1084
+ const bw = t === 1 ? h : w;
1085
+ const bh = t === 1 ? w : h;
1086
+ if (f.x0 + bw > f.x1 || f.y0 + bh > f.y1) continue;
1087
+ const lw = f.x1 - f.x0 - bw;
1088
+ const lh = f.y1 - f.y0 - bh;
1089
+ let s1;
1090
+ let s2;
1091
+ switch (rule) {
1092
+ case "bssf":
1093
+ s1 = Math.min(lw, lh);
1094
+ s2 = Math.max(lw, lh);
1095
+ break;
1096
+ case "blsf":
1097
+ s1 = Math.max(lw, lh);
1098
+ s2 = Math.min(lw, lh);
1099
+ break;
1100
+ case "baf":
1101
+ s1 = (f.x1 - f.x0) * (f.y1 - f.y0) - bw * bh;
1102
+ s2 = Math.min(lw, lh);
1103
+ break;
1104
+ case "bl":
1105
+ s1 = f.y0 + bh;
1106
+ s2 = f.x0;
1107
+ break;
1108
+ case "cp":
1109
+ s1 = -contact(f.x0, f.y0, bw, bh);
1110
+ s2 = f.y0 + bh;
1111
+ }
1112
+ const c = {
1113
+ index: i,
1114
+ turn: t === 1,
1115
+ x: f.x0,
1116
+ y: f.y0,
1117
+ w: bw,
1118
+ h: bh,
1119
+ s1,
1120
+ s2
1121
+ };
1122
+ if (best === void 0 || s1 < best.s1 || s1 === best.s1 && (s2 < best.s2 || s2 === best.s2 && (c.y < best.y || c.y === best.y && c.x < best.x))) best = c;
1123
+ const ly = f.y0 + bh;
1124
+ if (low === void 0 || ly < low.s1 || ly === low.s1 && f.x0 < low.s2) low = {
1125
+ ...c,
1126
+ s1: ly,
1127
+ s2: f.x0
1128
+ };
1129
+ }
1130
+ }
1131
+ if (best === void 0 || !check || space.fits({
1132
+ x: best.x + ox,
1133
+ y: best.y + oy,
1134
+ w: best.w,
1135
+ h: best.h
1136
+ })) break;
1137
+ banned.add(2 * best.index + (best.turn ? 1 : 0));
1138
+ }
1139
+ let place;
1140
+ if (best !== void 0) {
1141
+ place = {
1142
+ x0: best.x,
1143
+ y0: best.y,
1144
+ x1: best.x + best.w,
1145
+ y1: best.y + best.h
1146
+ };
1147
+ if (rule !== "bl" && low !== void 0 && (low.x !== best.x || low.y !== best.y || low.turn !== best.turn)) countMove(MOVES[rule]);
1148
+ if (best.turn) {
1149
+ rect.size.set(best.w, best.h);
1150
+ turned?.add(rect);
1151
+ }
1152
+ } else {
1153
+ if (fixed) continue;
1154
+ place = {
1155
+ x0: 0,
1156
+ y0: bottom,
1157
+ x1: w,
1158
+ y1: bottom + h
1159
+ };
1160
+ }
1161
+ rect.position.set(place.x0 + ox, place.y0 + oy);
1162
+ rect.wasPacked = true;
1163
+ bottom = Math.max(bottom, place.y1);
1164
+ const result = cut(free, {
1165
+ x0: place.x0,
1166
+ y0: place.y0,
1167
+ x1: Math.min(W, place.x1),
1168
+ y1: place.y1
1169
+ }, minW, minH);
1170
+ free = result.free;
1171
+ if (result.hits >= 2) countMove(MOVES.split);
1172
+ mark(place);
1173
+ if (!fixed) outer.size.y = Math.max(outer.size.y, place.y1);
1174
+ }
1175
+ return {
1176
+ outer,
1177
+ free: free.map((f) => ({
1178
+ x: f.x0 + ox,
1179
+ y: f.y0 + oy,
1180
+ w: f.x1 - f.x0,
1181
+ h: f.y1 - f.y0
1182
+ }))
1183
+ };
1184
+ }
1185
+ /**
1186
+ * This function packs rectangles into an outer rectangle with MaxRects of Jylänki (2010). It keeps the list of the maximal
1187
+ * free rectangles, and it does not change the width of the outer rectangle.
1188
+ *
1189
+ * - Each box goes to the top-left corner of the free rectangle that `options.rule` chooses. `MAXRECTS_RULES` tells the rules.
1190
+ * - A tie goes to the higher place, then to the place on the left, then to the first free rectangle of the list.
1191
+ * - A `locked` rectangle keeps its position, and the free rectangles go around it.
1192
+ * - With a space that is not plain, the free rectangles start from bands of the outline. `MaxRectsOptions` tells the bands.
1193
+ * - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a box that does not fit
1194
+ * is not packed: its `wasPacked` stays `false`.
1195
+ * - The default sort order is `height-desc`. The order of the array of the caller stays.
1196
+ */
1197
+ function packMaxRects(rects, outer, options = {}) {
1198
+ return runMaxRects(rects, outer, options).outer;
1199
+ }
784
1200
  // src/shelf.ts
785
1201
  /** This constant lists each shelf fit. */
786
1202
  const SHELF_FITS = [
@@ -1446,7 +1862,7 @@ function packRows(rects, outer, options = {}) {
1446
1862
  * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
1447
1863
  * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
1448
1864
  * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
1449
- * - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
1865
+ * - `options.strategy` selects `packRows` (the default), `packSkyline`, `packShelf` or `packMaxRects` after the width is set.
1450
1866
  */
1451
1867
  function pack(rects, outer = new Rect(), options = {}) {
1452
1868
  let totalArea = 0;
@@ -1464,6 +1880,7 @@ function pack(rects, outer = new Rect(), options = {}) {
1464
1880
  if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
1465
1881
  if (options.strategy === "skyline") packSkyline(rects, outer, options);
1466
1882
  else if (options.strategy === "shelf") packShelf(rects, outer, options);
1883
+ else if (options.strategy === "maxrects") packMaxRects(rects, outer, options);
1467
1884
  else packRows(rects, outer, options);
1468
1885
  if (!outer.fixedSize) {
1469
1886
  const contentRight = Rect.maxRight(rects);
@@ -2142,6 +2559,14 @@ function hashInput(file) {
2142
2559
  const { hash: _hash, ...rest } = file;
2143
2560
  return sha256Hex(canonicalJson(rest)).slice(0, 16);
2144
2561
  }
2562
+ /**
2563
+ * This function gives the `outputHash` of an output. It is the first 16 hex characters of the SHA-256 of the canonical form of
2564
+ * its placements, sorted by id. The canonical form rounds each number with `round3`. Thus the order of the list does not count.
2565
+ * The records of the benchmark and the key of `packPortfolio` use it. It is not `outputHash` of the contract, which uses a grid.
2566
+ */
2567
+ function outputHash(output) {
2568
+ return sha256Hex(canonicalJson(output.placements.toSorted((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0))).slice(0, 16);
2569
+ }
2145
2570
  /** 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`. */
2146
2571
  function countCell(count) {
2147
2572
  if (count <= 3) return "1";
@@ -2737,30 +3162,61 @@ function supportsOf(cells) {
2737
3162
  mode: [...cells.mode]
2738
3163
  };
2739
3164
  }
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 } : {};
3165
+ /**
3166
+ * This function gives the options of `pack` that a strategy adds: the fit of `shelf`, or the rule, the bands and the turns
3167
+ * of `maxrects`. A turnable item turns only with `rotate`, and a locked item does not turn.
3168
+ */
3169
+ function extrasOf(id, options, frame) {
3170
+ if (id === "shelf") return options.fit !== void 0 ? { fit: options.fit } : {};
3171
+ if (id !== "maxrects") return {};
3172
+ const out = {
3173
+ rule: options.rule ?? "bssf",
3174
+ ...options.bandHeight === void 0 ? {} : { bandHeight: options.bandHeight }
3175
+ };
3176
+ if (options.rotate !== true) return out;
3177
+ const turnable = new Set(frame.entries.filter((e) => e.item?.turnable === true && !e.rect.locked).map((e) => e.rect));
3178
+ return {
3179
+ ...out,
3180
+ turnable,
3181
+ turned: /* @__PURE__ */ new Set()
3182
+ };
2743
3183
  }
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) {
3184
+ /**
3185
+ * This function gives the move counts of the trace of a strategy: its own move, and the moves of a shelf fit. The trace of
3186
+ * `maxrects` has the move of its rule and `move.split`. The rule `bl` has only `move.split`.
3187
+ */
3188
+ function movesOf(id, extras) {
3189
+ if (id === "maxrects") {
3190
+ const rule = extras.rule ?? "bssf";
3191
+ const out = rule === "bl" ? {} : { [MOVES[rule]]: moveCount(MOVES[rule]) };
3192
+ out[MOVES.split] = moveCount(MOVES.split);
3193
+ return out;
3194
+ }
2746
3195
  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);
3196
+ if (extras.fit !== void 0) for (const move of [MOVES.firstFit, MOVES.bestFit]) out[move] = moveCount(move);
2748
3197
  return out;
2749
3198
  }
3199
+ /** This function gives the turns of a packing for `readFrame`, or `undefined` when no rectangle can turn. */
3200
+ function turnsOf(extras, transposed) {
3201
+ return extras.turned === void 0 ? void 0 : {
3202
+ rects: extras.turned,
3203
+ turn: frameTurn(transposed)
3204
+ };
3205
+ }
2750
3206
  /** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
2751
- function packFixedWidth(id, input, options) {
3207
+ function packFixedWidth(id, input, options, transposed = false) {
2752
3208
  const start = now();
2753
- const frame = buildFrame(input);
3209
+ const frame = buildFrame(input, false, id === "maxrects" && options.rotate === true);
2754
3210
  const rects = rectsOf(frame);
2755
- const fit = fitOf(id, options);
3211
+ const extras = extrasOf(id, options, frame);
2756
3212
  clearMoves();
2757
3213
  if (frame.w > 0) pack(rects, outerOf(frame), {
2758
3214
  strategy: id,
2759
3215
  sortOrder: options.sortOrder,
2760
3216
  space: spaceOf(input, frame),
2761
- ...fit
3217
+ ...extras
2762
3218
  });
2763
- const output = readFrame(input, frame, []);
3219
+ const output = readFrame(input, frame, [], [], turnsOf(extras, transposed));
2764
3220
  return {
2765
3221
  ...output,
2766
3222
  trace: [{
@@ -2768,13 +3224,13 @@ function packFixedWidth(id, input, options) {
2768
3224
  ms: now() - start,
2769
3225
  measures: {
2770
3226
  "pack.overflow": output.overflow.length,
2771
- ...movesOf(id, fit)
3227
+ ...movesOf(id, extras)
2772
3228
  }
2773
3229
  }]
2774
3230
  };
2775
3231
  }
2776
3232
  /** This function makes the strategy of one function of `pack`. */
2777
- function layoutStrategy(id, family, catalog, cells) {
3233
+ function layoutStrategy(id, family, catalog, cells, defaults) {
2778
3234
  const self = {
2779
3235
  id,
2780
3236
  family,
@@ -2783,10 +3239,7 @@ function layoutStrategy(id, family, catalog, cells) {
2783
3239
  ...LAYOUT_CELLS,
2784
3240
  ...cells
2785
3241
  }),
2786
- defaults: {
2787
- sortOrder: "height-desc",
2788
- samples: 48
2789
- },
3242
+ defaults: { ...defaults },
2790
3243
  deterministic: true,
2791
3244
  pack(input, options, context) {
2792
3245
  assertSupported(self.supports, input);
@@ -2795,7 +3248,7 @@ function layoutStrategy(id, family, catalog, cells) {
2795
3248
  ...options
2796
3249
  };
2797
3250
  switch (input.container.fixed) {
2798
- case "height": return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o));
3251
+ case "height": return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o, true));
2799
3252
  case "none": return logSweep.search(input, self, context, o);
2800
3253
  default: return packFixedWidth(id, input, o);
2801
3254
  }
@@ -2803,6 +3256,11 @@ function layoutStrategy(id, family, catalog, cells) {
2803
3256
  };
2804
3257
  return self;
2805
3258
  }
3259
+ /** The defaults of `rows`, `skyline` and `shelf`. */
3260
+ const LAYOUT_DEFAULTS = {
3261
+ sortOrder: "height-desc",
3262
+ samples: 48
3263
+ };
2806
3264
  /**
2807
3265
  * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. With obstacles, locked items or a
2808
3266
  * container shape, the rows go around the holes of the free space and lose the gap fill. Turnable items, groups and item
@@ -2815,7 +3273,7 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
2815
3273
  "soft",
2816
3274
  "hard"
2817
3275
  ]
2818
- });
3276
+ }, LAYOUT_DEFAULTS);
2819
3277
  /**
2820
3278
  * The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
2821
3279
  * locked items and the fixed obstacles, in each container shape. Turnable items, groups and item shapes throw `UnsupportedCell`.
@@ -2823,7 +3281,7 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
2823
3281
  const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
2824
3282
  obstacleMove: ["fixed"],
2825
3283
  order: ["free", "soft"]
2826
- });
3284
+ }, LAYOUT_DEFAULTS);
2827
3285
  /**
2828
3286
  * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
2829
3287
  * With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
@@ -2836,24 +3294,48 @@ const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
2836
3294
  "soft",
2837
3295
  "hard"
2838
3296
  ]
3297
+ }, LAYOUT_DEFAULTS);
3298
+ /**
3299
+ * The strategy `maxrects` uses `packMaxRects`: the MaxRects packer of roadmap item k24. It keeps the list of the maximal
3300
+ * free rectangles, and the option `rule` chooses the free rectangle for each item. The default rule is `bssf`.
3301
+ *
3302
+ * - The free rectangles go around the locked items and the fixed obstacles. A plain rectangle container gives exact free
3303
+ * rectangles. Another shape gives an inner approximation with horizontal bands.
3304
+ * - With `rotate`, each turnable item that is not locked can turn by 90 degrees. Groups and item shapes throw `UnsupportedCell`.
3305
+ */
3306
+ const maxrects = layoutStrategy("maxrects", "maxrects", [
3307
+ "maxrects-bssf",
3308
+ "maxrects-blsf",
3309
+ "maxrects-baf",
3310
+ "maxrects-bl",
3311
+ "maxrects-cp",
3312
+ "maxrects-locked"
3313
+ ], {
3314
+ obstacleMove: ["fixed"],
3315
+ order: ["free", "soft"],
3316
+ rotation: ["none", "quarter"]
3317
+ }, {
3318
+ ...LAYOUT_DEFAULTS,
3319
+ rule: "bssf",
3320
+ rotate: false
2839
3321
  });
2840
3322
  /**
2841
3323
  * The size search `log-sweep` uses `packFree` of 0.1.2: widths on a log scale, then the widths near the best width. It
2842
- * wraps `rows`, `skyline` and `shelf`. The objective is the area of the frame times `1 + aspectWeight * aspectError`.
3324
+ * wraps `rows`, `skyline`, `shelf` and `maxrects`. The objective is the area of the frame times `1 + aspectWeight * aspectError`.
2843
3325
  */
2844
3326
  const logSweep = {
2845
3327
  id: "log-sweep",
2846
3328
  search(input, inner, context, options) {
2847
3329
  if (input.container.fixed !== "none") return inner.pack(input, options ?? inner.defaults, context);
2848
3330
  const id = inner.id;
2849
- if (id !== "rows" && id !== "skyline" && id !== "shelf") throw new UnsupportedCell("containerSize", cellOf(input).containerSize);
3331
+ if (id !== "rows" && id !== "skyline" && id !== "shelf" && id !== "maxrects") throw new UnsupportedCell("containerSize", cellOf(input).containerSize);
2850
3332
  const o = {
2851
3333
  ...inner.defaults,
2852
3334
  ...options
2853
3335
  };
2854
3336
  const start = now();
2855
- const frame = buildFrame(input);
2856
- const fit = fitOf(id, o);
3337
+ const frame = buildFrame(input, false, id === "maxrects" && o.rotate === true);
3338
+ const extras = extrasOf(id, o, frame);
2857
3339
  const result = packFree(rectsOf(frame), new Rect(), {
2858
3340
  strategy: id,
2859
3341
  sortOrder: o.sortOrder,
@@ -2861,11 +3343,11 @@ const logSweep = {
2861
3343
  samples: o.samples,
2862
3344
  aspect: input.container.aspect ?? 1,
2863
3345
  aspectWeight: input.container.aspectWeight ?? 1,
2864
- ...fit
3346
+ ...extras
2865
3347
  });
2866
3348
  const ms = now() - start;
2867
3349
  const runs = result.tried.length + 1;
2868
- const output = readFrame(input, frame, []);
3350
+ const output = readFrame(input, frame, [], [], turnsOf(extras, false));
2869
3351
  return {
2870
3352
  ...output,
2871
3353
  trace: [{
@@ -2880,7 +3362,7 @@ const logSweep = {
2880
3362
  ms: ms / runs,
2881
3363
  measures: {
2882
3364
  "pack.overflow": output.overflow.length,
2883
- ...movesOf(id, fit)
3365
+ ...movesOf(id, extras)
2884
3366
  }
2885
3367
  }]
2886
3368
  };
@@ -3078,7 +3560,8 @@ const strategies = Object.freeze([
3078
3560
  rows,
3079
3561
  skyline,
3080
3562
  shelf,
3081
- push
3563
+ push,
3564
+ maxrects
3082
3565
  ]);
3083
3566
  /** This function gives the strategy with an id. It throws a `RangeError` for an id that the registry does not have. */
3084
3567
  function strategy(id) {
@@ -3217,6 +3700,104 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE, options = {}) {
3217
3700
  complete
3218
3701
  };
3219
3702
  }
3703
+ // src/portfolio.ts
3704
+ /** The id of the trace stage of `packPortfolio`. Its measure `move.pick` is 1 when the pick is not member 0. */
3705
+ const PORTFOLIO_STAGE = "portfolio";
3706
+ /**
3707
+ * The levels of the pick of `packPortfolio` and of the portfolio report of the benchmark, in order. Level 0 is `hard`.
3708
+ *
3709
+ * - `hard`: the count of hard breaks of `checkHard` with `exactContainer`. Lower is better, with the minimum effect 0.
3710
+ * - `pack.overflow.excessArea`: the overflow area above its bound. Lower is better.
3711
+ * - `pack.fill`: the fill of the hull. Higher is better.
3712
+ */
3713
+ const PORTFOLIO_LEVELS = Object.freeze([
3714
+ {
3715
+ id: "hard",
3716
+ spec: {
3717
+ direction: "lower",
3718
+ minEffect: 0
3719
+ }
3720
+ },
3721
+ {
3722
+ id: "pack.overflow.excessArea",
3723
+ spec: MEASURE_SPECS["pack.overflow.excessArea"]
3724
+ },
3725
+ {
3726
+ id: "pack.fill",
3727
+ spec: MEASURE_SPECS["pack.fill"]
3728
+ }
3729
+ ]);
3730
+ /** This function gives the count of the hard breaks of an output: the sum of `checkHard` with `exactContainer`. */
3731
+ function hardBreaks(input, output) {
3732
+ return Object.values(checkHard(input, output, void 0, { exactContainer: true })).reduce((sum, n) => sum + n, 0);
3733
+ }
3734
+ /**
3735
+ * This function makes an opt-in portfolio of strategies of the registry with `portfolio` of the contract.
3736
+ * Its `pick(input)` packs the input with each member and gives the output of the winner, its id and the trace.
3737
+ *
3738
+ * - A member is a strategy id, or a `PortfolioEntry` with options. The members run in the given order.
3739
+ * - The key is the hard breaks, then `pack.overflow.excessArea`, then `pack.fill`. `PORTFOLIO_LEVELS` gives the specs.
3740
+ * - The hash of the key is `outputHash`. `selectBest` of the contract picks the winner.
3741
+ * - The output of the winner gets the trace stage `portfolio` with `move.pick` of `pickMoves`.
3742
+ * - The function throws a `RangeError` for an unknown strategy, an empty list and two members with the same id.
3743
+ */
3744
+ function packPortfolio(members, options = {}) {
3745
+ const seed = options.seed ?? 0;
3746
+ const list = members.map((m) => {
3747
+ const e = typeof m === "string" ? {
3748
+ id: m,
3749
+ strategy: m
3750
+ } : m;
3751
+ strategy(e.strategy);
3752
+ return {
3753
+ id: e.id,
3754
+ run: (input) => packWith(e.strategy, input, e.options, seed)
3755
+ };
3756
+ });
3757
+ const cache = /* @__PURE__ */ new WeakMap();
3758
+ const measured = (input, output) => {
3759
+ let m = cache.get(output);
3760
+ if (m === void 0) {
3761
+ m = measure(input, output);
3762
+ cache.set(output, m);
3763
+ }
3764
+ return m;
3765
+ };
3766
+ const key = {
3767
+ hard: hardBreaks,
3768
+ levels: PORTFOLIO_LEVELS.slice(1).map((level) => ({
3769
+ id: level.id,
3770
+ spec: level.spec,
3771
+ value: (input, output) => measured(input, output)[level.id]
3772
+ })),
3773
+ hash: outputHash
3774
+ };
3775
+ const limits = {
3776
+ ...options.budgetMs !== void 0 ? { budgetMs: options.budgetMs } : {},
3777
+ ...options.maxMembers !== void 0 ? { maxMembers: options.maxMembers } : {},
3778
+ ...options.now !== void 0 ? { now: options.now } : {}
3779
+ };
3780
+ const inner = portfolio(options.id ?? "portfolio", list, key, limits);
3781
+ const pick = (input) => {
3782
+ const got = inner.pick(input);
3783
+ const stage = {
3784
+ id: PORTFOLIO_STAGE,
3785
+ measures: { "move.pick": pickMoves(got.trace).pick }
3786
+ };
3787
+ return {
3788
+ ...got,
3789
+ output: {
3790
+ ...got.output,
3791
+ trace: [...got.output.trace ?? [], stage]
3792
+ }
3793
+ };
3794
+ };
3795
+ return {
3796
+ id: inner.id,
3797
+ pick,
3798
+ run: (input) => pick(input).output
3799
+ };
3800
+ }
3220
3801
  // src/generators.ts
3221
3802
  /** The largest count of boxes that a generator makes. */
3222
3803
  const MAX_COUNT = 1e5;
@@ -4131,6 +4712,6 @@ function corpus(name, specs) {
4131
4712
  inputs
4132
4713
  };
4133
4714
  }
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 };
4715
+ export { DEFAULT_TOLERANCE, EPS, MAXRECTS_BANDS, MAXRECTS_RULES, MEASURE_SPECS, PORTFOLIO_LEVELS, PORTFOLIO_STAGE, 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, hardBreaks, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, maxrects, measure, measureChange, medianRatio, mulberry32, outputHash, pack, packFree, packMaxRects, packPortfolio, 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 };
4135
4716
 
4136
4717
  //# sourceMappingURL=index.js.map