@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/README.md +27 -2
- package/build/index.d.ts +138 -7
- package/build/index.d.ts.map +1 -1
- package/build/index.js +623 -42
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters.ts +97 -32
- package/src/frame.ts +37 -10
- package/src/index.ts +15 -1
- package/src/layout-input.ts +11 -1
- package/src/maxrects.ts +367 -0
- package/src/moves.ts +15 -0
- package/src/pack.ts +14 -4
- package/src/portfolio.ts +103 -0
- package/src/registry.ts +2 -2
- package/src/space.ts +36 -1
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
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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
|
-
/**
|
|
2741
|
-
function
|
|
2742
|
-
|
|
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
|
-
/**
|
|
2745
|
-
function
|
|
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 (
|
|
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
|
|
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
|
-
...
|
|
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,
|
|
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 `
|
|
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
|
|
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
|
-
...
|
|
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,
|
|
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
|