@mark1russell7/packing 0.2.3 → 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/README.md +28 -2
- package/build/index.d.ts +146 -31
- package/build/index.d.ts.map +1 -1
- package/build/index.js +725 -42
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters.ts +110 -23
- package/src/frame.ts +37 -10
- package/src/index.ts +13 -2
- package/src/maxrects.ts +367 -0
- package/src/measures.ts +18 -7
- package/src/moves.ts +32 -3
- package/src/pack.ts +17 -5
- package/src/registry.ts +2 -2
- package/src/shelf.ts +200 -0
- package/src/space.ts +36 -1
package/build/index.js
CHANGED
|
@@ -138,16 +138,33 @@ var Rect = class {
|
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
140
|
// src/moves.ts
|
|
141
|
-
/** The id of the characteristic move of each strategy. The trace of each strategy has its id, also with the count 0. */
|
|
141
|
+
/** The id of the characteristic move of each strategy and of each shelf fit. The trace of each strategy has its id, also with the count 0. */
|
|
142
142
|
const MOVES = Object.freeze({
|
|
143
143
|
/** A rectangle that the gap fill of `packRow` places in a gap of a row: under a shorter neighbor, or at the row break. */
|
|
144
144
|
rows: "move.gapFill",
|
|
145
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* A rectangle that `packShelf` puts on an open shelf with a taller rectangle. The strip above it stays empty. With a
|
|
147
|
+
* `fit`, the move is a rectangle in the strip above a shorter rectangle of a shelf.
|
|
148
|
+
*/
|
|
146
149
|
shelf: "move.stack",
|
|
147
150
|
/** A rectangle that the skyline puts in a lower window, not in the leftmost window where it fits. */
|
|
148
151
|
skyline: "move.lowerWindow",
|
|
149
152
|
/** A rectangle that a push cascade moves. */
|
|
150
|
-
push: "move.cascade"
|
|
153
|
+
push: "move.cascade",
|
|
154
|
+
/** A rectangle that a shelf fit puts on a shelf that is not the open shelf. The open shelf is the last shelf that the packing opened. */
|
|
155
|
+
firstFit: "move.firstFit",
|
|
156
|
+
/** A rectangle that the fit `best` puts on a shelf that is not the first shelf where it fits. */
|
|
157
|
+
bestFit: "move.bestFit",
|
|
158
|
+
/** 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"
|
|
151
168
|
});
|
|
152
169
|
const counts = /* @__PURE__ */ new Map();
|
|
153
170
|
/** This function adds `n` to the count of a move. */
|
|
@@ -234,9 +251,10 @@ function byRank(items) {
|
|
|
234
251
|
}
|
|
235
252
|
/**
|
|
236
253
|
* This function makes the frame of an input. A locked item with a position and an obstacle without `move` are locked
|
|
237
|
-
* 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.
|
|
238
256
|
*/
|
|
239
|
-
function buildFrame(input, movable = false) {
|
|
257
|
+
function buildFrame(input, movable = false, turnable = false) {
|
|
240
258
|
const bounds = containerBounds(input);
|
|
241
259
|
const inset = input.container.inset ?? 0;
|
|
242
260
|
const half = (input.gap ?? 0) / 2;
|
|
@@ -255,7 +273,7 @@ function buildFrame(input, movable = false) {
|
|
|
255
273
|
if (item.locked === true && item.x !== void 0 && item.y !== void 0) {
|
|
256
274
|
rect.locked = true;
|
|
257
275
|
rect.position.set(item.x - dx - ox, item.y - dy - oy);
|
|
258
|
-
} 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)) {
|
|
259
277
|
tooLarge.push(item.id);
|
|
260
278
|
continue;
|
|
261
279
|
}
|
|
@@ -304,10 +322,18 @@ function rectsOf(frame) {
|
|
|
304
322
|
return frame.entries.map((e) => e.rect);
|
|
305
323
|
}
|
|
306
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
|
+
/**
|
|
307
332
|
* This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
|
|
308
|
-
* 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`.
|
|
309
335
|
*/
|
|
310
|
-
function readFrame(input, frame, trace, moved = []) {
|
|
336
|
+
function readFrame(input, frame, trace, moved = [], turns) {
|
|
311
337
|
const placements = [];
|
|
312
338
|
const overflow = [...frame.tooLarge];
|
|
313
339
|
for (const e of frame.entries) {
|
|
@@ -318,7 +344,16 @@ function readFrame(input, frame, trace, moved = []) {
|
|
|
318
344
|
x: item.x,
|
|
319
345
|
y: item.y
|
|
320
346
|
});
|
|
321
|
-
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({
|
|
322
357
|
id: item.id,
|
|
323
358
|
x: e.rect.position.x + e.dx + frame.ox,
|
|
324
359
|
y: e.rect.position.y + e.dy + frame.oy
|
|
@@ -328,7 +363,7 @@ function readFrame(input, frame, trace, moved = []) {
|
|
|
328
363
|
return {
|
|
329
364
|
placements,
|
|
330
365
|
overflow,
|
|
331
|
-
size: sizeOf(input, frame, placements, moved),
|
|
366
|
+
size: sizeOf(input, frame, placements, moved, turns),
|
|
332
367
|
moved,
|
|
333
368
|
trace
|
|
334
369
|
};
|
|
@@ -337,12 +372,13 @@ function readFrame(input, frame, trace, moved = []) {
|
|
|
337
372
|
* This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
|
|
338
373
|
* far edge of the items with their margins and the inset, or of the obstacles.
|
|
339
374
|
*/
|
|
340
|
-
function sizeOf(input, frame, placements, moved) {
|
|
375
|
+
function sizeOf(input, frame, placements, moved, turns) {
|
|
341
376
|
const items = new Map(input.items.map((i) => [i.id, i]));
|
|
342
377
|
let right = 0;
|
|
343
378
|
let bottom = 0;
|
|
344
379
|
for (const p of placements) {
|
|
345
|
-
const
|
|
380
|
+
const given = items.get(p.id);
|
|
381
|
+
const item = p.turn === 90 && turns !== void 0 ? turns.turn(given) : given;
|
|
346
382
|
const m = marginOf(item);
|
|
347
383
|
right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
|
|
348
384
|
bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
|
|
@@ -742,7 +778,7 @@ function spaceOf(input, frame) {
|
|
|
742
778
|
start,
|
|
743
779
|
end
|
|
744
780
|
}));
|
|
745
|
-
|
|
781
|
+
const space = {
|
|
746
782
|
w: frame.w,
|
|
747
783
|
h: frame.h,
|
|
748
784
|
plain,
|
|
@@ -766,6 +802,33 @@ function spaceOf(input, frame) {
|
|
|
766
802
|
return nextOf(layer, y0, y1, w);
|
|
767
803
|
}
|
|
768
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);
|
|
769
832
|
}
|
|
770
833
|
/**
|
|
771
834
|
* This function gives the free space of an input, in the coordinates of the frame of the adapters. The origin of the
|
|
@@ -774,7 +837,373 @@ function spaceOf(input, frame) {
|
|
|
774
837
|
function freeSpace(input) {
|
|
775
838
|
return spaceOf(input, buildFrame(input));
|
|
776
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
|
+
}
|
|
777
1200
|
// src/shelf.ts
|
|
1201
|
+
/** This constant lists each shelf fit. */
|
|
1202
|
+
const SHELF_FITS = [
|
|
1203
|
+
"next",
|
|
1204
|
+
"first",
|
|
1205
|
+
"best"
|
|
1206
|
+
];
|
|
778
1207
|
/**
|
|
779
1208
|
* This function gives the first x from `start` where a rectangle of width `w` crosses no obstacle in the band from
|
|
780
1209
|
* `top` to `bottom`.
|
|
@@ -809,8 +1238,10 @@ function nextTop(obstacles, top, bottom) {
|
|
|
809
1238
|
* - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
|
|
810
1239
|
* rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
811
1240
|
* - With a space that is not plain, `packRowsInSpace` packs the rows into the outline of the space and around its holes.
|
|
1241
|
+
* - With `fit`, `packShelvesInSpace` packs the rectangles, in each container shape. A `fit` that is not in `SHELF_FITS` throws a `RangeError`.
|
|
812
1242
|
*/
|
|
813
1243
|
function packShelf(rects, outer, options = {}) {
|
|
1244
|
+
if (options.fit !== void 0) return packShelvesInSpace(rects, outer, options.sortOrder ?? "height-desc", options.fit, options.space);
|
|
814
1245
|
if (options.space !== void 0 && !options.space.plain) return packRowsInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
|
|
815
1246
|
const width = outer.size.x;
|
|
816
1247
|
const obstacles = [];
|
|
@@ -973,6 +1404,181 @@ function packRowsInSpace(rects, outer, order, space) {
|
|
|
973
1404
|
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
|
|
974
1405
|
return outer;
|
|
975
1406
|
}
|
|
1407
|
+
/**
|
|
1408
|
+
* This function packs rectangles into shelves with a fit: `next`, `first` or `best`. `packShelf` uses it when its
|
|
1409
|
+
* options have a `fit`. It sets the position of each rectangle and gives the outer rectangle.
|
|
1410
|
+
*
|
|
1411
|
+
* - A shelf opens at the bottom edge of the last shelf, at the first x where the rectangle fits in the free space.
|
|
1412
|
+
* When no x fits, the shelf steps down to `nextY` of the space, as an empty row of `packRowsInSpace` does.
|
|
1413
|
+
* - The first rectangle of a shelf sets its height. A rectangle goes on the floor of a shelf at its cursor, or into
|
|
1414
|
+
* the strip above a shorter rectangle of the shelf. The strip is the move `move.stack`.
|
|
1415
|
+
* - The fit `next` uses only the open shelf: the last shelf that the packing opened. The fit `first` uses the first
|
|
1416
|
+
* shelf from the top where the rectangle fits. The fit `best` uses the place with the least height left over.
|
|
1417
|
+
* - In a shelf, the rectangle takes the place with the least height left over. A tie goes to a strip, from left to right.
|
|
1418
|
+
* - When no shelf has a place, the last shelf grows for a taller rectangle, if it fits. Else a new shelf opens.
|
|
1419
|
+
* - A rectangle wider than the outer rectangle overflows. Without `fixedSize`, it goes on a closed shelf of its own at the left edge.
|
|
1420
|
+
* - Each place is in the free space and crosses no locked rectangle. Without a space, the outer rectangle and the
|
|
1421
|
+
* locked rectangles give the free space.
|
|
1422
|
+
*/
|
|
1423
|
+
function packShelvesInSpace(rects, outer, order, fit, space) {
|
|
1424
|
+
if (!SHELF_FITS.includes(fit)) throw new RangeError(`The shelf fit "${String(fit)}" is not one of ${SHELF_FITS.join(", ")}.`);
|
|
1425
|
+
const width = outer.size.x;
|
|
1426
|
+
const ox = outer.position.x;
|
|
1427
|
+
const oy = outer.position.y;
|
|
1428
|
+
const limit = outer.fixedSize ? outer.size.y : Infinity;
|
|
1429
|
+
const obstacles = [];
|
|
1430
|
+
let contentBottom = 0;
|
|
1431
|
+
for (const rect of rects) {
|
|
1432
|
+
if (!rect.locked) continue;
|
|
1433
|
+
rect.wasPacked = true;
|
|
1434
|
+
const left = rect.position.x - ox;
|
|
1435
|
+
const top = rect.position.y - oy;
|
|
1436
|
+
obstacles.push({
|
|
1437
|
+
left,
|
|
1438
|
+
top,
|
|
1439
|
+
right: left + rect.size.x,
|
|
1440
|
+
bottom: top + rect.size.y
|
|
1441
|
+
});
|
|
1442
|
+
contentBottom = Math.max(contentBottom, top + rect.size.y);
|
|
1443
|
+
}
|
|
1444
|
+
const whole = [{
|
|
1445
|
+
start: 0,
|
|
1446
|
+
end: Infinity
|
|
1447
|
+
}];
|
|
1448
|
+
const place = (start, w, top, h) => {
|
|
1449
|
+
const spans = space === void 0 ? whole : space.band(top + oy, top + h + oy).map((s) => ({
|
|
1450
|
+
start: s.start - ox,
|
|
1451
|
+
end: s.end - ox
|
|
1452
|
+
}));
|
|
1453
|
+
return placeInBand(obstacles, spans, start, w, top, top + h);
|
|
1454
|
+
};
|
|
1455
|
+
const stepDown = (top, w, h) => Math.min(space === void 0 ? Infinity : space.nextY(top + oy, top + h + oy, w) - oy, nextTop(obstacles, top, top + h));
|
|
1456
|
+
const spotsOn = (shelf, w, h, grow) => {
|
|
1457
|
+
const ceiling = shelf.top + shelf.height;
|
|
1458
|
+
const spots = [];
|
|
1459
|
+
if (!grow) {
|
|
1460
|
+
for (const strip of shelf.strips) if (w <= strip.w && strip.bottom + h <= ceiling) spots.push({
|
|
1461
|
+
shelf,
|
|
1462
|
+
x: strip.x,
|
|
1463
|
+
y: strip.bottom,
|
|
1464
|
+
left: ceiling - strip.bottom - h,
|
|
1465
|
+
strip
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
const tall = grow ? h > shelf.height && shelf.top + h <= limit : h <= shelf.height;
|
|
1469
|
+
if (Number.isFinite(shelf.cursor) && tall) spots.push({
|
|
1470
|
+
shelf,
|
|
1471
|
+
x: shelf.cursor,
|
|
1472
|
+
y: shelf.top,
|
|
1473
|
+
left: grow ? 0 : shelf.height - h,
|
|
1474
|
+
...grow ? { grow } : {}
|
|
1475
|
+
});
|
|
1476
|
+
return spots.toSorted((a, b) => a.left - b.left);
|
|
1477
|
+
};
|
|
1478
|
+
const verify = (spot, w, h) => {
|
|
1479
|
+
const x = place(spot.x, w, spot.y, h);
|
|
1480
|
+
if (spot.strip !== void 0) return x === spot.x ? spot : void 0;
|
|
1481
|
+
return x + w <= width ? {
|
|
1482
|
+
...spot,
|
|
1483
|
+
x
|
|
1484
|
+
} : void 0;
|
|
1485
|
+
};
|
|
1486
|
+
const firstOn = (shelf, w, h) => {
|
|
1487
|
+
for (const spot of spotsOn(shelf, w, h, false)) {
|
|
1488
|
+
const found = verify(spot, w, h);
|
|
1489
|
+
if (found !== void 0) return found;
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
const shelves = [];
|
|
1493
|
+
for (const rect of rects.toSorted(compareRects(order))) {
|
|
1494
|
+
if (rect.wasPacked) continue;
|
|
1495
|
+
const w = rect.size.x;
|
|
1496
|
+
const h = rect.size.y;
|
|
1497
|
+
if (!(w > 0 && h > 0)) continue;
|
|
1498
|
+
const open = shelves.at(-1);
|
|
1499
|
+
const floor = open === void 0 ? 0 : open.top + open.height;
|
|
1500
|
+
if (w > width) {
|
|
1501
|
+
if (outer.fixedSize) continue;
|
|
1502
|
+
let top = floor;
|
|
1503
|
+
while (firstFreeX(obstacles, 0, w, top, top + h) !== 0) {
|
|
1504
|
+
const next = nextTop(obstacles, top, top + h);
|
|
1505
|
+
if (!Number.isFinite(next)) break;
|
|
1506
|
+
top = next;
|
|
1507
|
+
}
|
|
1508
|
+
rect.position.set(ox, top + oy);
|
|
1509
|
+
rect.wasPacked = true;
|
|
1510
|
+
shelves.push({
|
|
1511
|
+
top,
|
|
1512
|
+
height: h,
|
|
1513
|
+
cursor: Infinity,
|
|
1514
|
+
strips: []
|
|
1515
|
+
});
|
|
1516
|
+
contentBottom = Math.max(contentBottom, top + h);
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
let spot;
|
|
1520
|
+
let firstShelf;
|
|
1521
|
+
if (fit === "best") {
|
|
1522
|
+
for (const s of shelves.flatMap((shelf) => spotsOn(shelf, w, h, false)).toSorted((a, b) => a.left - b.left)) {
|
|
1523
|
+
spot = verify(s, w, h);
|
|
1524
|
+
if (spot !== void 0) break;
|
|
1525
|
+
}
|
|
1526
|
+
if (spot !== void 0) firstShelf = shelves.find((shelf) => shelf === spot.shelf || firstOn(shelf, w, h) !== void 0);
|
|
1527
|
+
} else for (const shelf of fit === "next" ? shelves.slice(-1) : shelves) {
|
|
1528
|
+
spot = firstOn(shelf, w, h);
|
|
1529
|
+
if (spot !== void 0) break;
|
|
1530
|
+
}
|
|
1531
|
+
if (spot === void 0 && open !== void 0) {
|
|
1532
|
+
const grown = spotsOn(open, w, h, true)[0];
|
|
1533
|
+
if (grown !== void 0) spot = verify(grown, w, h);
|
|
1534
|
+
}
|
|
1535
|
+
if (spot === void 0) {
|
|
1536
|
+
let top = floor;
|
|
1537
|
+
let x = place(0, w, top, h);
|
|
1538
|
+
while (!(x + w <= width) && top + h <= limit) {
|
|
1539
|
+
const next = stepDown(top, w, h);
|
|
1540
|
+
if (!Number.isFinite(next)) break;
|
|
1541
|
+
top = next;
|
|
1542
|
+
x = place(0, w, top, h);
|
|
1543
|
+
}
|
|
1544
|
+
if (!(x + w <= width) || top + h > limit) continue;
|
|
1545
|
+
const shelf = {
|
|
1546
|
+
top,
|
|
1547
|
+
height: h,
|
|
1548
|
+
cursor: x,
|
|
1549
|
+
strips: []
|
|
1550
|
+
};
|
|
1551
|
+
shelves.push(shelf);
|
|
1552
|
+
spot = {
|
|
1553
|
+
shelf,
|
|
1554
|
+
x,
|
|
1555
|
+
y: top,
|
|
1556
|
+
left: 0
|
|
1557
|
+
};
|
|
1558
|
+
} else {
|
|
1559
|
+
if (spot.shelf !== open) countMove(MOVES.firstFit);
|
|
1560
|
+
if (firstShelf !== void 0 && spot.shelf !== firstShelf) countMove(MOVES.bestFit);
|
|
1561
|
+
}
|
|
1562
|
+
const { shelf } = spot;
|
|
1563
|
+
rect.position.set(spot.x + ox, spot.y + oy);
|
|
1564
|
+
rect.wasPacked = true;
|
|
1565
|
+
if (spot.strip !== void 0) {
|
|
1566
|
+
countMove(MOVES.shelf);
|
|
1567
|
+
spot.strip.bottom = spot.y + h;
|
|
1568
|
+
} else {
|
|
1569
|
+
shelf.cursor = spot.x + w;
|
|
1570
|
+
shelf.strips.push({
|
|
1571
|
+
x: spot.x,
|
|
1572
|
+
w,
|
|
1573
|
+
bottom: spot.y + h
|
|
1574
|
+
});
|
|
1575
|
+
if (spot.grow === true) shelf.height = h;
|
|
1576
|
+
}
|
|
1577
|
+
contentBottom = Math.max(contentBottom, spot.y + h);
|
|
1578
|
+
}
|
|
1579
|
+
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
|
|
1580
|
+
return outer;
|
|
1581
|
+
}
|
|
976
1582
|
// src/skyline.ts
|
|
977
1583
|
/**
|
|
978
1584
|
* This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
|
|
@@ -1256,7 +1862,7 @@ function packRows(rects, outer, options = {}) {
|
|
|
1256
1862
|
* This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
|
|
1257
1863
|
* - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
|
|
1258
1864
|
* - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
1259
|
-
* - `options.strategy` selects `packRows` (the default), `packSkyline` or `
|
|
1865
|
+
* - `options.strategy` selects `packRows` (the default), `packSkyline`, `packShelf` or `packMaxRects` after the width is set.
|
|
1260
1866
|
*/
|
|
1261
1867
|
function pack(rects, outer = new Rect(), options = {}) {
|
|
1262
1868
|
let totalArea = 0;
|
|
@@ -1274,6 +1880,7 @@ function pack(rects, outer = new Rect(), options = {}) {
|
|
|
1274
1880
|
if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
|
|
1275
1881
|
if (options.strategy === "skyline") packSkyline(rects, outer, options);
|
|
1276
1882
|
else if (options.strategy === "shelf") packShelf(rects, outer, options);
|
|
1883
|
+
else if (options.strategy === "maxrects") packMaxRects(rects, outer, options);
|
|
1277
1884
|
else packRows(rects, outer, options);
|
|
1278
1885
|
if (!outer.fixedSize) {
|
|
1279
1886
|
const contentRight = Rect.maxRight(rects);
|
|
@@ -1993,6 +2600,8 @@ const REL = Object.freeze({ rel: .01 });
|
|
|
1993
2600
|
* - A share of an area from 0 to 1 has 0.005. A guard has its tolerance.
|
|
1994
2601
|
* - An area and a ratio of lengths have `{ rel: 0.01 }`: 1% of the larger value.
|
|
1995
2602
|
* - The time has no minimum effect, because the times of CI change each time.
|
|
2603
|
+
* - `pack.overflow.forcedAreaMin` is a deprecated alias of `pack.overflow.forcedArea`. Its spec is the same, except the id.
|
|
2604
|
+
* The alias stays until the next minor version.
|
|
1996
2605
|
*/
|
|
1997
2606
|
const MEASURE_SPECS = Object.freeze({
|
|
1998
2607
|
"pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity, REL),
|
|
@@ -2125,7 +2734,8 @@ function traceSum(output, id) {
|
|
|
2125
2734
|
* - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
|
|
2126
2735
|
* - The overflow measures count each id once. `pack.overflow.forced` is the lower bound of `overflowBound`.
|
|
2127
2736
|
* - `pack.overflow.excess` is the overflow less the forced overflow. A container with no fixed side gives 0 for both.
|
|
2128
|
-
* - `pack.overflow.
|
|
2737
|
+
* - `pack.overflow.forcedArea` is a lower bound of the overflow area. `pack.overflow.excessArea` is the overflow area above it.
|
|
2738
|
+
* - `pack.overflow.forcedAreaMin` is a deprecated alias of `pack.overflow.forcedArea`, with the same value.
|
|
2129
2739
|
*
|
|
2130
2740
|
* @example
|
|
2131
2741
|
* ```ts
|
|
@@ -2170,7 +2780,7 @@ function measure(input, output, options = {}) {
|
|
|
2170
2780
|
}
|
|
2171
2781
|
const bound = overflowBound(input);
|
|
2172
2782
|
const overflowArea = itemArea > 0 ? unplacedArea / itemArea : 0;
|
|
2173
|
-
const
|
|
2783
|
+
const forcedArea = itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0;
|
|
2174
2784
|
return {
|
|
2175
2785
|
"pack.area": hullArea,
|
|
2176
2786
|
"pack.fill": fill,
|
|
@@ -2184,10 +2794,10 @@ function measure(input, output, options = {}) {
|
|
|
2184
2794
|
"pack.overflow.rate": input.items.length > 0 ? unplaced.size / input.items.length : 0,
|
|
2185
2795
|
"pack.overflow.area": overflowArea,
|
|
2186
2796
|
"pack.overflow.forced": bound.forced,
|
|
2187
|
-
"pack.overflow.forcedArea":
|
|
2797
|
+
"pack.overflow.forcedArea": forcedArea,
|
|
2188
2798
|
"pack.overflow.excess": Math.max(0, unplaced.size - bound.forced),
|
|
2189
|
-
"pack.overflow.forcedAreaMin":
|
|
2190
|
-
"pack.overflow.excessArea": excessAreaOf(overflowArea,
|
|
2799
|
+
"pack.overflow.forcedAreaMin": forcedArea,
|
|
2800
|
+
"pack.overflow.excessArea": excessAreaOf(overflowArea, forcedArea),
|
|
2191
2801
|
"pack.search.widths": traceSum(output, "pack.search.widths"),
|
|
2192
2802
|
"pack.search.ms": traceSum(output, "pack.search.ms")
|
|
2193
2803
|
};
|
|
@@ -2196,8 +2806,8 @@ function measure(input, output, options = {}) {
|
|
|
2196
2806
|
* This function gives the overflow area above its lower bound. A value from `-AREA_NOISE` to 0 becomes 0. A value below
|
|
2197
2807
|
* `-AREA_NOISE` stays, because it shows a bug of the bound or of the packer.
|
|
2198
2808
|
*/
|
|
2199
|
-
function excessAreaOf(overflowArea,
|
|
2200
|
-
const excess = overflowArea -
|
|
2809
|
+
function excessAreaOf(overflowArea, forcedArea) {
|
|
2810
|
+
const excess = overflowArea - forcedArea;
|
|
2201
2811
|
return excess < 0 && excess >= -1e-9 ? 0 : excess;
|
|
2202
2812
|
}
|
|
2203
2813
|
/**
|
|
@@ -2544,18 +3154,61 @@ function supportsOf(cells) {
|
|
|
2544
3154
|
mode: [...cells.mode]
|
|
2545
3155
|
};
|
|
2546
3156
|
}
|
|
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
|
+
};
|
|
3175
|
+
}
|
|
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
|
+
}
|
|
3187
|
+
const out = { [MOVES[id]]: moveCount(MOVES[id]) };
|
|
3188
|
+
if (extras.fit !== void 0) for (const move of [MOVES.firstFit, MOVES.bestFit]) out[move] = moveCount(move);
|
|
3189
|
+
return out;
|
|
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
|
+
}
|
|
2547
3198
|
/** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
|
|
2548
|
-
function packFixedWidth(id, input, options) {
|
|
3199
|
+
function packFixedWidth(id, input, options, transposed = false) {
|
|
2549
3200
|
const start = now();
|
|
2550
|
-
const frame = buildFrame(input);
|
|
3201
|
+
const frame = buildFrame(input, false, id === "maxrects" && options.rotate === true);
|
|
2551
3202
|
const rects = rectsOf(frame);
|
|
3203
|
+
const extras = extrasOf(id, options, frame);
|
|
2552
3204
|
clearMoves();
|
|
2553
3205
|
if (frame.w > 0) pack(rects, outerOf(frame), {
|
|
2554
3206
|
strategy: id,
|
|
2555
3207
|
sortOrder: options.sortOrder,
|
|
2556
|
-
space: spaceOf(input, frame)
|
|
3208
|
+
space: spaceOf(input, frame),
|
|
3209
|
+
...extras
|
|
2557
3210
|
});
|
|
2558
|
-
const output = readFrame(input, frame, []);
|
|
3211
|
+
const output = readFrame(input, frame, [], [], turnsOf(extras, transposed));
|
|
2559
3212
|
return {
|
|
2560
3213
|
...output,
|
|
2561
3214
|
trace: [{
|
|
@@ -2563,13 +3216,13 @@ function packFixedWidth(id, input, options) {
|
|
|
2563
3216
|
ms: now() - start,
|
|
2564
3217
|
measures: {
|
|
2565
3218
|
"pack.overflow": output.overflow.length,
|
|
2566
|
-
|
|
3219
|
+
...movesOf(id, extras)
|
|
2567
3220
|
}
|
|
2568
3221
|
}]
|
|
2569
3222
|
};
|
|
2570
3223
|
}
|
|
2571
3224
|
/** This function makes the strategy of one function of `pack`. */
|
|
2572
|
-
function layoutStrategy(id, family, catalog, cells) {
|
|
3225
|
+
function layoutStrategy(id, family, catalog, cells, defaults) {
|
|
2573
3226
|
const self = {
|
|
2574
3227
|
id,
|
|
2575
3228
|
family,
|
|
@@ -2578,10 +3231,7 @@ function layoutStrategy(id, family, catalog, cells) {
|
|
|
2578
3231
|
...LAYOUT_CELLS,
|
|
2579
3232
|
...cells
|
|
2580
3233
|
}),
|
|
2581
|
-
defaults: {
|
|
2582
|
-
sortOrder: "height-desc",
|
|
2583
|
-
samples: 48
|
|
2584
|
-
},
|
|
3234
|
+
defaults: { ...defaults },
|
|
2585
3235
|
deterministic: true,
|
|
2586
3236
|
pack(input, options, context) {
|
|
2587
3237
|
assertSupported(self.supports, input);
|
|
@@ -2590,7 +3240,7 @@ function layoutStrategy(id, family, catalog, cells) {
|
|
|
2590
3240
|
...options
|
|
2591
3241
|
};
|
|
2592
3242
|
switch (input.container.fixed) {
|
|
2593
|
-
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));
|
|
2594
3244
|
case "none": return logSweep.search(input, self, context, o);
|
|
2595
3245
|
default: return packFixedWidth(id, input, o);
|
|
2596
3246
|
}
|
|
@@ -2598,6 +3248,11 @@ function layoutStrategy(id, family, catalog, cells) {
|
|
|
2598
3248
|
};
|
|
2599
3249
|
return self;
|
|
2600
3250
|
}
|
|
3251
|
+
/** The defaults of `rows`, `skyline` and `shelf`. */
|
|
3252
|
+
const LAYOUT_DEFAULTS = {
|
|
3253
|
+
sortOrder: "height-desc",
|
|
3254
|
+
samples: 48
|
|
3255
|
+
};
|
|
2601
3256
|
/**
|
|
2602
3257
|
* The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. With obstacles, locked items or a
|
|
2603
3258
|
* container shape, the rows go around the holes of the free space and lose the gap fill. Turnable items, groups and item
|
|
@@ -2610,7 +3265,7 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
|
|
|
2610
3265
|
"soft",
|
|
2611
3266
|
"hard"
|
|
2612
3267
|
]
|
|
2613
|
-
});
|
|
3268
|
+
}, LAYOUT_DEFAULTS);
|
|
2614
3269
|
/**
|
|
2615
3270
|
* The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
|
|
2616
3271
|
* locked items and the fixed obstacles, in each container shape. Turnable items, groups and item shapes throw `UnsupportedCell`.
|
|
@@ -2618,10 +3273,11 @@ const rows = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
|
|
|
2618
3273
|
const skyline = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
|
|
2619
3274
|
obstacleMove: ["fixed"],
|
|
2620
3275
|
order: ["free", "soft"]
|
|
2621
|
-
});
|
|
3276
|
+
}, LAYOUT_DEFAULTS);
|
|
2622
3277
|
/**
|
|
2623
3278
|
* The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
|
|
2624
3279
|
* With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
|
|
3280
|
+
* The option `fit` selects a shelf rule of roadmap item k21: `next`, `first` or `best`. The default has no `fit`.
|
|
2625
3281
|
*/
|
|
2626
3282
|
const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
|
|
2627
3283
|
obstacleMove: ["fixed"],
|
|
@@ -2630,34 +3286,60 @@ const shelf = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
|
|
|
2630
3286
|
"soft",
|
|
2631
3287
|
"hard"
|
|
2632
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
|
|
2633
3313
|
});
|
|
2634
3314
|
/**
|
|
2635
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
|
|
2636
|
-
* wraps `rows`, `skyline` and `
|
|
3316
|
+
* wraps `rows`, `skyline`, `shelf` and `maxrects`. The objective is the area of the frame times `1 + aspectWeight * aspectError`.
|
|
2637
3317
|
*/
|
|
2638
3318
|
const logSweep = {
|
|
2639
3319
|
id: "log-sweep",
|
|
2640
3320
|
search(input, inner, context, options) {
|
|
2641
3321
|
if (input.container.fixed !== "none") return inner.pack(input, options ?? inner.defaults, context);
|
|
2642
3322
|
const id = inner.id;
|
|
2643
|
-
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);
|
|
2644
3324
|
const o = {
|
|
2645
3325
|
...inner.defaults,
|
|
2646
3326
|
...options
|
|
2647
3327
|
};
|
|
2648
3328
|
const start = now();
|
|
2649
|
-
const frame = buildFrame(input);
|
|
3329
|
+
const frame = buildFrame(input, false, id === "maxrects" && o.rotate === true);
|
|
3330
|
+
const extras = extrasOf(id, o, frame);
|
|
2650
3331
|
const result = packFree(rectsOf(frame), new Rect(), {
|
|
2651
3332
|
strategy: id,
|
|
2652
3333
|
sortOrder: o.sortOrder,
|
|
2653
3334
|
space: spaceOf(input, frame),
|
|
2654
3335
|
samples: o.samples,
|
|
2655
3336
|
aspect: input.container.aspect ?? 1,
|
|
2656
|
-
aspectWeight: input.container.aspectWeight ?? 1
|
|
3337
|
+
aspectWeight: input.container.aspectWeight ?? 1,
|
|
3338
|
+
...extras
|
|
2657
3339
|
});
|
|
2658
3340
|
const ms = now() - start;
|
|
2659
3341
|
const runs = result.tried.length + 1;
|
|
2660
|
-
const output = readFrame(input, frame, []);
|
|
3342
|
+
const output = readFrame(input, frame, [], [], turnsOf(extras, false));
|
|
2661
3343
|
return {
|
|
2662
3344
|
...output,
|
|
2663
3345
|
trace: [{
|
|
@@ -2672,7 +3354,7 @@ const logSweep = {
|
|
|
2672
3354
|
ms: ms / runs,
|
|
2673
3355
|
measures: {
|
|
2674
3356
|
"pack.overflow": output.overflow.length,
|
|
2675
|
-
|
|
3357
|
+
...movesOf(id, extras)
|
|
2676
3358
|
}
|
|
2677
3359
|
}]
|
|
2678
3360
|
};
|
|
@@ -2870,7 +3552,8 @@ const strategies = Object.freeze([
|
|
|
2870
3552
|
rows,
|
|
2871
3553
|
skyline,
|
|
2872
3554
|
shelf,
|
|
2873
|
-
push
|
|
3555
|
+
push,
|
|
3556
|
+
maxrects
|
|
2874
3557
|
]);
|
|
2875
3558
|
/** This function gives the strategy with an id. It throws a `RangeError` for an id that the registry does not have. */
|
|
2876
3559
|
function strategy(id) {
|
|
@@ -3923,6 +4606,6 @@ function corpus(name, specs) {
|
|
|
3923
4606
|
inputs
|
|
3924
4607
|
};
|
|
3925
4608
|
}
|
|
3926
|
-
export { DEFAULT_TOLERANCE, EPS, MEASURE_SPECS, RECORD_EPS, Rect, SORT_ORDERS, UnsupportedCell, Vector, aspectError, assertSupported, canonicalJson, cards, cellOf, checkHard, checkInput, compareRects, corpus, countCell, createContext, dashboard, edgeCases, fnv1a32, freeSpace, fromRects, gallery, generator, generators, hashInput, isDeterministic, logSweep, lowestWindow, marginOf, measure, measureChange, medianRatio, mulberry32, pack, packFree, packRows, packShelf, packSkyline, packWith, packingMetrics, pairedRatio, push, pushCascade, quantile, rectGrid, rectScattered, round3, roundMeasures, rows, shelf, skyline, strategies, strategy, supportsCell, tailShare, toPackInput, toRects, turnItem, turnPoint, turnPort, turnRegion, unsupportedOf, wordCloud };
|
|
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 };
|
|
3927
4610
|
|
|
3928
4611
|
//# sourceMappingURL=index.js.map
|