@mark1russell7/packing 0.2.4 → 0.2.5

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
@@ -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);
@@ -2737,30 +3154,61 @@ function supportsOf(cells) {
2737
3154
  mode: [...cells.mode]
2738
3155
  };
2739
3156
  }
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 } : {};
3157
+ /**
3158
+ * This function gives the options of `pack` that a strategy adds: the fit of `shelf`, or the rule, the bands and the turns
3159
+ * of `maxrects`. A turnable item turns only with `rotate`, and a locked item does not turn.
3160
+ */
3161
+ function extrasOf(id, options, frame) {
3162
+ if (id === "shelf") return options.fit !== void 0 ? { fit: options.fit } : {};
3163
+ if (id !== "maxrects") return {};
3164
+ const out = {
3165
+ rule: options.rule ?? "bssf",
3166
+ ...options.bandHeight === void 0 ? {} : { bandHeight: options.bandHeight }
3167
+ };
3168
+ if (options.rotate !== true) return out;
3169
+ const turnable = new Set(frame.entries.filter((e) => e.item?.turnable === true && !e.rect.locked).map((e) => e.rect));
3170
+ return {
3171
+ ...out,
3172
+ turnable,
3173
+ turned: /* @__PURE__ */ new Set()
3174
+ };
2743
3175
  }
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) {
3176
+ /**
3177
+ * 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
3178
+ * `maxrects` has the move of its rule and `move.split`. The rule `bl` has only `move.split`.
3179
+ */
3180
+ function movesOf(id, extras) {
3181
+ if (id === "maxrects") {
3182
+ const rule = extras.rule ?? "bssf";
3183
+ const out = rule === "bl" ? {} : { [MOVES[rule]]: moveCount(MOVES[rule]) };
3184
+ out[MOVES.split] = moveCount(MOVES.split);
3185
+ return out;
3186
+ }
2746
3187
  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);
3188
+ if (extras.fit !== void 0) for (const move of [MOVES.firstFit, MOVES.bestFit]) out[move] = moveCount(move);
2748
3189
  return out;
2749
3190
  }
3191
+ /** This function gives the turns of a packing for `readFrame`, or `undefined` when no rectangle can turn. */
3192
+ function turnsOf(extras, transposed) {
3193
+ return extras.turned === void 0 ? void 0 : {
3194
+ rects: extras.turned,
3195
+ turn: frameTurn(transposed)
3196
+ };
3197
+ }
2750
3198
  /** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
2751
- function packFixedWidth(id, input, options) {
3199
+ function packFixedWidth(id, input, options, transposed = false) {
2752
3200
  const start = now();
2753
- const frame = buildFrame(input);
3201
+ const frame = buildFrame(input, false, id === "maxrects" && options.rotate === true);
2754
3202
  const rects = rectsOf(frame);
2755
- const fit = fitOf(id, options);
3203
+ const extras = extrasOf(id, options, frame);
2756
3204
  clearMoves();
2757
3205
  if (frame.w > 0) pack(rects, outerOf(frame), {
2758
3206
  strategy: id,
2759
3207
  sortOrder: options.sortOrder,
2760
3208
  space: spaceOf(input, frame),
2761
- ...fit
3209
+ ...extras
2762
3210
  });
2763
- const output = readFrame(input, frame, []);
3211
+ const output = readFrame(input, frame, [], [], turnsOf(extras, transposed));
2764
3212
  return {
2765
3213
  ...output,
2766
3214
  trace: [{
@@ -2768,13 +3216,13 @@ function packFixedWidth(id, input, options) {
2768
3216
  ms: now() - start,
2769
3217
  measures: {
2770
3218
  "pack.overflow": output.overflow.length,
2771
- ...movesOf(id, fit)
3219
+ ...movesOf(id, extras)
2772
3220
  }
2773
3221
  }]
2774
3222
  };
2775
3223
  }
2776
3224
  /** This function makes the strategy of one function of `pack`. */
2777
- function layoutStrategy(id, family, catalog, cells) {
3225
+ function layoutStrategy(id, family, catalog, cells, defaults) {
2778
3226
  const self = {
2779
3227
  id,
2780
3228
  family,
@@ -2783,10 +3231,7 @@ function layoutStrategy(id, family, catalog, cells) {
2783
3231
  ...LAYOUT_CELLS,
2784
3232
  ...cells
2785
3233
  }),
2786
- defaults: {
2787
- sortOrder: "height-desc",
2788
- samples: 48
2789
- },
3234
+ defaults: { ...defaults },
2790
3235
  deterministic: true,
2791
3236
  pack(input, options, context) {
2792
3237
  assertSupported(self.supports, input);
@@ -2795,7 +3240,7 @@ function layoutStrategy(id, family, catalog, cells) {
2795
3240
  ...options
2796
3241
  };
2797
3242
  switch (input.container.fixed) {
2798
- case "height": return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o));
3243
+ case "height": return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o, true));
2799
3244
  case "none": return logSweep.search(input, self, context, o);
2800
3245
  default: return packFixedWidth(id, input, o);
2801
3246
  }
@@ -2803,6 +3248,11 @@ function layoutStrategy(id, family, catalog, cells) {
2803
3248
  };
2804
3249
  return self;
2805
3250
  }
3251
+ /** The defaults of `rows`, `skyline` and `shelf`. */
3252
+ const LAYOUT_DEFAULTS = {
3253
+ sortOrder: "height-desc",
3254
+ samples: 48
3255
+ };
2806
3256
  /**
2807
3257
  * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. With obstacles, locked items or a
2808
3258
  * container shape, the rows go around the holes of the free space and lose the gap fill. Turnable items, groups and item
@@ -2815,7 +3265,7 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
2815
3265
  "soft",
2816
3266
  "hard"
2817
3267
  ]
2818
- });
3268
+ }, LAYOUT_DEFAULTS);
2819
3269
  /**
2820
3270
  * The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
2821
3271
  * locked items and the fixed obstacles, in each container shape. Turnable items, groups and item shapes throw `UnsupportedCell`.
@@ -2823,7 +3273,7 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
2823
3273
  const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
2824
3274
  obstacleMove: ["fixed"],
2825
3275
  order: ["free", "soft"]
2826
- });
3276
+ }, LAYOUT_DEFAULTS);
2827
3277
  /**
2828
3278
  * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
2829
3279
  * With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
@@ -2836,24 +3286,48 @@ const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
2836
3286
  "soft",
2837
3287
  "hard"
2838
3288
  ]
3289
+ }, LAYOUT_DEFAULTS);
3290
+ /**
3291
+ * The strategy `maxrects` uses `packMaxRects`: the MaxRects packer of roadmap item k24. It keeps the list of the maximal
3292
+ * free rectangles, and the option `rule` chooses the free rectangle for each item. The default rule is `bssf`.
3293
+ *
3294
+ * - The free rectangles go around the locked items and the fixed obstacles. A plain rectangle container gives exact free
3295
+ * rectangles. Another shape gives an inner approximation with horizontal bands.
3296
+ * - With `rotate`, each turnable item that is not locked can turn by 90 degrees. Groups and item shapes throw `UnsupportedCell`.
3297
+ */
3298
+ const maxrects = layoutStrategy("maxrects", "maxrects", [
3299
+ "maxrects-bssf",
3300
+ "maxrects-blsf",
3301
+ "maxrects-baf",
3302
+ "maxrects-bl",
3303
+ "maxrects-cp",
3304
+ "maxrects-locked"
3305
+ ], {
3306
+ obstacleMove: ["fixed"],
3307
+ order: ["free", "soft"],
3308
+ rotation: ["none", "quarter"]
3309
+ }, {
3310
+ ...LAYOUT_DEFAULTS,
3311
+ rule: "bssf",
3312
+ rotate: false
2839
3313
  });
2840
3314
  /**
2841
3315
  * 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`.
3316
+ * wraps `rows`, `skyline`, `shelf` and `maxrects`. The objective is the area of the frame times `1 + aspectWeight * aspectError`.
2843
3317
  */
2844
3318
  const logSweep = {
2845
3319
  id: "log-sweep",
2846
3320
  search(input, inner, context, options) {
2847
3321
  if (input.container.fixed !== "none") return inner.pack(input, options ?? inner.defaults, context);
2848
3322
  const id = inner.id;
2849
- if (id !== "rows" && id !== "skyline" && id !== "shelf") throw new UnsupportedCell("containerSize", cellOf(input).containerSize);
3323
+ if (id !== "rows" && id !== "skyline" && id !== "shelf" && id !== "maxrects") throw new UnsupportedCell("containerSize", cellOf(input).containerSize);
2850
3324
  const o = {
2851
3325
  ...inner.defaults,
2852
3326
  ...options
2853
3327
  };
2854
3328
  const start = now();
2855
- const frame = buildFrame(input);
2856
- const fit = fitOf(id, o);
3329
+ const frame = buildFrame(input, false, id === "maxrects" && o.rotate === true);
3330
+ const extras = extrasOf(id, o, frame);
2857
3331
  const result = packFree(rectsOf(frame), new Rect(), {
2858
3332
  strategy: id,
2859
3333
  sortOrder: o.sortOrder,
@@ -2861,11 +3335,11 @@ const logSweep = {
2861
3335
  samples: o.samples,
2862
3336
  aspect: input.container.aspect ?? 1,
2863
3337
  aspectWeight: input.container.aspectWeight ?? 1,
2864
- ...fit
3338
+ ...extras
2865
3339
  });
2866
3340
  const ms = now() - start;
2867
3341
  const runs = result.tried.length + 1;
2868
- const output = readFrame(input, frame, []);
3342
+ const output = readFrame(input, frame, [], [], turnsOf(extras, false));
2869
3343
  return {
2870
3344
  ...output,
2871
3345
  trace: [{
@@ -2880,7 +3354,7 @@ const logSweep = {
2880
3354
  ms: ms / runs,
2881
3355
  measures: {
2882
3356
  "pack.overflow": output.overflow.length,
2883
- ...movesOf(id, fit)
3357
+ ...movesOf(id, extras)
2884
3358
  }
2885
3359
  }]
2886
3360
  };
@@ -3078,7 +3552,8 @@ const strategies = Object.freeze([
3078
3552
  rows,
3079
3553
  skyline,
3080
3554
  shelf,
3081
- push
3555
+ push,
3556
+ maxrects
3082
3557
  ]);
3083
3558
  /** This function gives the strategy with an id. It throws a `RangeError` for an id that the registry does not have. */
3084
3559
  function strategy(id) {
@@ -4131,6 +4606,6 @@ function corpus(name, specs) {
4131
4606
  inputs
4132
4607
  };
4133
4608
  }
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 };
4609
+ export { DEFAULT_TOLERANCE, EPS, MAXRECTS_BANDS, MAXRECTS_RULES, 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, maxrects, measure, measureChange, medianRatio, mulberry32, pack, packFree, packMaxRects, 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
4610
 
4136
4611
  //# sourceMappingURL=index.js.map