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