@mark1russell7/packing 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +24 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +476 -34
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters.ts +7 -4
- package/src/check.ts +14 -3
- package/src/free.ts +3 -0
- package/src/index.ts +1 -1
- package/src/measures.ts +78 -19
- package/src/moves.ts +33 -0
- package/src/overflow.ts +332 -0
- package/src/pack.ts +15 -3
- package/src/shelf.ts +3 -0
- package/src/skyline.ts +34 -7
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_TOLERANCE, EPS, RECORD_EPS, boundsOf, boundsOf as boundsOf$1, checkInput, marginSides, regionHitsRect, translate, turnBox, turnPoint, turnPort, turnRegion } from "@mark1russell7/layout-contract";
|
|
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";
|
|
2
2
|
// src/vector.ts
|
|
3
3
|
/** A mutable two-dimensional vector. */
|
|
4
4
|
var Vector = class Vector {
|
|
@@ -137,6 +137,32 @@ 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. 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
|
+
/** A rectangle that `packShelf` puts on an open shelf with a taller rectangle. The strip above it stays empty. */
|
|
146
|
+
shelf: "move.stack",
|
|
147
|
+
/** A rectangle that the skyline puts in a lower window, not in the leftmost window where it fits. */
|
|
148
|
+
skyline: "move.lowerWindow",
|
|
149
|
+
/** A rectangle that a push cascade moves. */
|
|
150
|
+
push: "move.cascade"
|
|
151
|
+
});
|
|
152
|
+
const counts = /* @__PURE__ */ new Map();
|
|
153
|
+
/** This function adds `n` to the count of a move. */
|
|
154
|
+
function countMove(id, n = 1) {
|
|
155
|
+
if (n > 0) counts.set(id, (counts.get(id) ?? 0) + n);
|
|
156
|
+
}
|
|
157
|
+
/** This function sets the count of a move to 0. Without an id, it sets each count to 0. */
|
|
158
|
+
function clearMoves(id) {
|
|
159
|
+
if (id === void 0) counts.clear();
|
|
160
|
+
else counts.delete(id);
|
|
161
|
+
}
|
|
162
|
+
/** This function gives the count of a move since the last `clearMoves`. */
|
|
163
|
+
function moveCount(id) {
|
|
164
|
+
return counts.get(id) ?? 0;
|
|
165
|
+
}
|
|
140
166
|
// src/order.ts
|
|
141
167
|
/** This constant lists each sort order. The first one is the default. */
|
|
142
168
|
const SORT_ORDERS = [
|
|
@@ -832,6 +858,7 @@ function packShelf(rects, outer, options = {}) {
|
|
|
832
858
|
x = firstFreeX(obstacles, 0, w, rowTop, rowTop + h);
|
|
833
859
|
}
|
|
834
860
|
if (outer.fixedSize && rowTop + h > outer.size.y) continue;
|
|
861
|
+
if (!rowEmpty && rowTop + h < rowBottom) countMove(MOVES.shelf);
|
|
835
862
|
rect.position.set(x + outer.position.x, rowTop + outer.position.y);
|
|
836
863
|
rect.wasPacked = true;
|
|
837
864
|
rowEmpty = false;
|
|
@@ -956,6 +983,14 @@ function packRowsInSpace(rects, outer, order, space) {
|
|
|
956
983
|
* `limit[x, x + rw)`. A second deque keeps that smallest value. No valid window gives the height `Infinity`.
|
|
957
984
|
*/
|
|
958
985
|
function lowestWindow(skyline, width, rw, limit, rh = 0) {
|
|
986
|
+
const { x, y } = findWindow(skyline, width, rw, limit, rh);
|
|
987
|
+
return {
|
|
988
|
+
x,
|
|
989
|
+
y
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
/** This function is `lowestWindow`, and it also gives the x of the leftmost valid window. */
|
|
993
|
+
function findWindow(skyline, width, rw, limit, rh = 0) {
|
|
959
994
|
if (limit !== void 0) return lowestLimitedWindow(skyline, limit, width, rw, rh);
|
|
960
995
|
const deque = new Int32Array(width);
|
|
961
996
|
let head = 0;
|
|
@@ -976,9 +1011,14 @@ function lowestWindow(skyline, width, rw, limit, rh = 0) {
|
|
|
976
1011
|
}
|
|
977
1012
|
return {
|
|
978
1013
|
x: bestX,
|
|
979
|
-
y: bestY
|
|
1014
|
+
y: bestY,
|
|
1015
|
+
first: width >= rw ? 0 : -1
|
|
980
1016
|
};
|
|
981
1017
|
}
|
|
1018
|
+
/** This function counts the move of the skyline: a window that is lower than the leftmost valid window. */
|
|
1019
|
+
function countWindow(w) {
|
|
1020
|
+
if (Number.isFinite(w.y) && w.first >= 0 && w.x !== w.first) countMove(MOVES.skyline);
|
|
1021
|
+
}
|
|
982
1022
|
/** This function is `lowestWindow` with a limit for each cell. */
|
|
983
1023
|
function lowestLimitedWindow(skyline, limit, width, rw, rh) {
|
|
984
1024
|
const high = new Int32Array(width);
|
|
@@ -989,6 +1029,7 @@ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
|
|
|
989
1029
|
let lt = 0;
|
|
990
1030
|
let bestX = 0;
|
|
991
1031
|
let bestY = Infinity;
|
|
1032
|
+
let first = -1;
|
|
992
1033
|
for (let i = 0; i < width; i++) {
|
|
993
1034
|
while (ht > hh && skyline[high[ht - 1]] <= skyline[i]) ht--;
|
|
994
1035
|
high[ht++] = i;
|
|
@@ -998,7 +1039,9 @@ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
|
|
|
998
1039
|
if (low[lh] <= i - rw) lh++;
|
|
999
1040
|
if (i >= rw - 1) {
|
|
1000
1041
|
const max = skyline[high[hh]];
|
|
1001
|
-
|
|
1042
|
+
const valid = max + rh <= limit[low[lh]];
|
|
1043
|
+
if (valid && first < 0) first = i - rw + 1;
|
|
1044
|
+
if (max < bestY && valid) {
|
|
1002
1045
|
bestY = max;
|
|
1003
1046
|
bestX = i - rw + 1;
|
|
1004
1047
|
}
|
|
@@ -1006,7 +1049,8 @@ function lowestLimitedWindow(skyline, limit, width, rw, rh) {
|
|
|
1006
1049
|
}
|
|
1007
1050
|
return {
|
|
1008
1051
|
x: bestX,
|
|
1009
|
-
y: bestY
|
|
1052
|
+
y: bestY,
|
|
1053
|
+
first
|
|
1010
1054
|
};
|
|
1011
1055
|
}
|
|
1012
1056
|
/**
|
|
@@ -1050,8 +1094,10 @@ function packSkyline(rects, outer, options = {}) {
|
|
|
1050
1094
|
y = 0;
|
|
1051
1095
|
for (let i = 0; i < width; i++) if (skyline[i] > y) y = skyline[i];
|
|
1052
1096
|
} else {
|
|
1053
|
-
|
|
1097
|
+
const found = findWindow(skyline, width, rw);
|
|
1098
|
+
({x, y} = found);
|
|
1054
1099
|
if (outer.fixedSize && y + rh > outer.size.y) continue;
|
|
1100
|
+
countWindow(found);
|
|
1055
1101
|
}
|
|
1056
1102
|
rect.position.set(x + outer.position.x, y + outer.position.y);
|
|
1057
1103
|
rect.wasPacked = true;
|
|
@@ -1104,16 +1150,19 @@ function packSkylineInSpace(rects, outer, order, space) {
|
|
|
1104
1150
|
outer.size.y = Math.max(outer.size.y, y + rh);
|
|
1105
1151
|
continue;
|
|
1106
1152
|
}
|
|
1107
|
-
|
|
1153
|
+
let found = findWindow(skyline, width, rw, limit, rh);
|
|
1154
|
+
({x, y} = found);
|
|
1108
1155
|
let sky = skyline;
|
|
1109
1156
|
let lim = limit;
|
|
1110
1157
|
if (!Number.isFinite(y)) {
|
|
1111
1158
|
sky = skyline.slice();
|
|
1112
1159
|
lim = limit.slice();
|
|
1113
1160
|
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
|
-
|
|
1161
|
+
found = findWindow(sky, width, rw, lim, rh);
|
|
1162
|
+
({x, y} = found);
|
|
1115
1163
|
if (!Number.isFinite(y)) continue;
|
|
1116
1164
|
}
|
|
1165
|
+
countWindow(found);
|
|
1117
1166
|
rect.position.set(x + ox, y + oy);
|
|
1118
1167
|
rect.wasPacked = true;
|
|
1119
1168
|
const top = y + rh;
|
|
@@ -1126,10 +1175,19 @@ function packSkylineInSpace(rects, outer, order, space) {
|
|
|
1126
1175
|
return outer;
|
|
1127
1176
|
}
|
|
1128
1177
|
// src/pack.ts
|
|
1178
|
+
/** The depth of the calls of `packInto`. Only the outer gap fill counts its rectangles as moves. */
|
|
1179
|
+
let fillDepth = 0;
|
|
1129
1180
|
function packInto(rects, region, offset, order) {
|
|
1130
1181
|
const unpacked = Rect.unpacked(rects);
|
|
1131
|
-
|
|
1132
|
-
|
|
1182
|
+
fillDepth++;
|
|
1183
|
+
try {
|
|
1184
|
+
pack(unpacked, region, { sortOrder: order });
|
|
1185
|
+
} finally {
|
|
1186
|
+
fillDepth--;
|
|
1187
|
+
}
|
|
1188
|
+
const placed = Rect.packed(unpacked);
|
|
1189
|
+
if (fillDepth === 0) countMove(MOVES.rows, placed.length);
|
|
1190
|
+
for (const r of placed) r.translateBy(offset);
|
|
1133
1191
|
}
|
|
1134
1192
|
function packRow(rects, outer, order) {
|
|
1135
1193
|
const row = [];
|
|
@@ -1187,7 +1245,7 @@ function packRows(rects, outer, options = {}) {
|
|
|
1187
1245
|
const space = options.space;
|
|
1188
1246
|
if (space !== void 0 && (!space.plain || rects.some((r) => r.locked))) return packRowsInSpace(rects, outer, order, space);
|
|
1189
1247
|
for (const r of rects) if (r.locked) r.wasPacked = true;
|
|
1190
|
-
while (!packRow(rects, outer, order));
|
|
1248
|
+
while (!packRow(rects, outer, order)) if (fillDepth === 0) clearMoves(MOVES.rows);
|
|
1191
1249
|
return outer;
|
|
1192
1250
|
}
|
|
1193
1251
|
/**
|
|
@@ -1264,6 +1322,7 @@ function packFree(rects, outer = new Rect(), options = {}) {
|
|
|
1264
1322
|
const maxWidth = Math.max(minWidth, rowWidth);
|
|
1265
1323
|
const tried = [];
|
|
1266
1324
|
const run = (width) => {
|
|
1325
|
+
clearMoves();
|
|
1267
1326
|
Rect.resetPacked(rects);
|
|
1268
1327
|
const box = new Rect();
|
|
1269
1328
|
box.position.setTo(outer.position);
|
|
@@ -1398,6 +1457,323 @@ function pushCascade(placed, rects, options = {}) {
|
|
|
1398
1457
|
}
|
|
1399
1458
|
return [...moved];
|
|
1400
1459
|
}
|
|
1460
|
+
// src/overflow.ts
|
|
1461
|
+
/** This function gives the area of a ring by the shoelace formula. The direction of the ring does not count. */
|
|
1462
|
+
function ringArea(ring) {
|
|
1463
|
+
let sum = 0;
|
|
1464
|
+
for (let i = 0; i < ring.length; i++) {
|
|
1465
|
+
const a = ring[i];
|
|
1466
|
+
const b = ring[(i + 1) % ring.length];
|
|
1467
|
+
sum += a[0] * b[1] - b[0] * a[1];
|
|
1468
|
+
}
|
|
1469
|
+
return Math.abs(sum) / 2;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* This function gives the area of a region that an item cannot use. A polygon loses the area of its holes. Thus a hole
|
|
1473
|
+
* that is out of the polygon makes the area too small, and the free area stays an upper bound.
|
|
1474
|
+
*/
|
|
1475
|
+
function blockedArea(region) {
|
|
1476
|
+
switch (region.kind) {
|
|
1477
|
+
case "rect": return Math.max(0, region.w) * Math.max(0, region.h);
|
|
1478
|
+
case "circle": return Math.PI * region.r ** 2;
|
|
1479
|
+
case "ellipse": return Math.PI * region.rx * region.ry;
|
|
1480
|
+
case "polygon": {
|
|
1481
|
+
let area = ringArea(region.points);
|
|
1482
|
+
for (const hole of region.holes ?? []) area -= ringArea(hole);
|
|
1483
|
+
return Math.max(0, area);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/** This function tells if the insides of two rectangles have a common point. Two rectangles with a common edge do not. */
|
|
1488
|
+
function boxesOverlap(a, b) {
|
|
1489
|
+
return Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > 0 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > 0;
|
|
1490
|
+
}
|
|
1491
|
+
/** This function tells if rectangle `a` is in rectangle `b`. */
|
|
1492
|
+
function boxIn(a, b) {
|
|
1493
|
+
return a.x >= b.x && a.y >= b.y && a.x + a.w <= b.x + b.w && a.y + a.h <= b.y + b.h;
|
|
1494
|
+
}
|
|
1495
|
+
/** This function gives the rectangle made smaller by `d` on each side. */
|
|
1496
|
+
function shrink(b, d) {
|
|
1497
|
+
return {
|
|
1498
|
+
x: b.x + d,
|
|
1499
|
+
y: b.y + d,
|
|
1500
|
+
w: Math.max(0, b.w - 2 * d),
|
|
1501
|
+
h: Math.max(0, b.h - 2 * d)
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
/** This function tells if a point is in a ring, by the even-odd rule. */
|
|
1505
|
+
function pointInRing(x, y, ring) {
|
|
1506
|
+
let inside = false;
|
|
1507
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
1508
|
+
const [xi, yi] = ring[i];
|
|
1509
|
+
const [xj, yj] = ring[j];
|
|
1510
|
+
if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
|
|
1511
|
+
}
|
|
1512
|
+
return inside;
|
|
1513
|
+
}
|
|
1514
|
+
/** This function tells if a segment has a part of a length above 0 in the open rectangle. The Liang-Barsky clip gives it. */
|
|
1515
|
+
function segmentHitsBox(ax, ay, bx, by, r) {
|
|
1516
|
+
let t0 = 0;
|
|
1517
|
+
let t1 = 1;
|
|
1518
|
+
const dx = bx - ax;
|
|
1519
|
+
const dy = by - ay;
|
|
1520
|
+
const clip = (p, q) => {
|
|
1521
|
+
if (p === 0) return q > 0;
|
|
1522
|
+
const t = q / p;
|
|
1523
|
+
if (p < 0) {
|
|
1524
|
+
if (t > t1) return false;
|
|
1525
|
+
if (t > t0) t0 = t;
|
|
1526
|
+
} else {
|
|
1527
|
+
if (t < t0) return false;
|
|
1528
|
+
if (t < t1) t1 = t;
|
|
1529
|
+
}
|
|
1530
|
+
return true;
|
|
1531
|
+
};
|
|
1532
|
+
return clip(-dx, ax - r.x) && clip(dx, r.x + r.w - ax) && clip(-dy, ay - r.y) && clip(dy, r.y + r.h - ay) && t1 > t0;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* This function tells if a rectangle is in a polygon with holes. No edge of a ring crosses the inside of the rectangle.
|
|
1536
|
+
* No vertex is in it, and its center is in the polygon by the even-odd rule.
|
|
1537
|
+
*/
|
|
1538
|
+
function boxInPolygon(b, rings) {
|
|
1539
|
+
for (const ring of rings) for (let i = 0; i < ring.length; i++) {
|
|
1540
|
+
const p = ring[i];
|
|
1541
|
+
const q = ring[(i + 1) % ring.length];
|
|
1542
|
+
if (p[0] > b.x && p[0] < b.x + b.w && p[1] > b.y && p[1] < b.y + b.h) return false;
|
|
1543
|
+
if (segmentHitsBox(p[0], p[1], q[0], q[1], b)) return false;
|
|
1544
|
+
}
|
|
1545
|
+
const cx = b.x + b.w / 2;
|
|
1546
|
+
const cy = b.y + b.h / 2;
|
|
1547
|
+
let inside = false;
|
|
1548
|
+
for (const ring of rings) if (pointInRing(cx, cy, ring)) inside = !inside;
|
|
1549
|
+
return inside;
|
|
1550
|
+
}
|
|
1551
|
+
/** This function tells if a rectangle is in a region of a container. A rectangle with no size is in no region. */
|
|
1552
|
+
function boxInShape(b, shape) {
|
|
1553
|
+
if (!(b.w > 0 && b.h > 0)) return false;
|
|
1554
|
+
const s = shrink(b, 1e-9 * Math.max(1, b.w, b.h));
|
|
1555
|
+
switch (shape.kind) {
|
|
1556
|
+
case "rect": return boxIn(b, {
|
|
1557
|
+
x: shape.x,
|
|
1558
|
+
y: shape.y,
|
|
1559
|
+
w: shape.w,
|
|
1560
|
+
h: shape.h
|
|
1561
|
+
});
|
|
1562
|
+
case "circle":
|
|
1563
|
+
case "ellipse": {
|
|
1564
|
+
const rx = shape.kind === "circle" ? shape.r : shape.rx;
|
|
1565
|
+
const ry = shape.kind === "circle" ? shape.r : shape.ry;
|
|
1566
|
+
const inside = (x, y) => ((x - shape.cx) / rx) ** 2 + ((y - shape.cy) / ry) ** 2 <= 1;
|
|
1567
|
+
return inside(s.x, s.y) && inside(s.x + s.w, s.y) && inside(s.x, s.y + s.h) && inside(s.x + s.w, s.y + s.h);
|
|
1568
|
+
}
|
|
1569
|
+
case "polygon": return boxInPolygon(s, [shape.points, ...shape.holes ?? []]);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* This function gives the area of a container shape. A hole counts only when its bounding box is in the outer ring.
|
|
1574
|
+
* Also, that box has no common point with the box of a hole before it. Thus the area is not too small.
|
|
1575
|
+
*/
|
|
1576
|
+
function shapeArea(shape) {
|
|
1577
|
+
if (shape.kind !== "polygon") return blockedArea(shape);
|
|
1578
|
+
let area = ringArea(shape.points);
|
|
1579
|
+
const kept = [];
|
|
1580
|
+
for (const hole of shape.holes ?? []) {
|
|
1581
|
+
const box = boundsOf({
|
|
1582
|
+
kind: "polygon",
|
|
1583
|
+
points: hole
|
|
1584
|
+
});
|
|
1585
|
+
if (!boxInPolygon(shrink(box, 1e-9 * Math.max(1, box.w, box.h)), [shape.points])) continue;
|
|
1586
|
+
if (kept.some((k) => boxesOverlap(k, box))) continue;
|
|
1587
|
+
kept.push(box);
|
|
1588
|
+
area -= ringArea(hole);
|
|
1589
|
+
}
|
|
1590
|
+
return Math.max(0, area);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* This function gives the area of the union of rectangles, each cut to the rectangle `clip`. A sweep over the x edges
|
|
1594
|
+
* adds the merged y intervals of each slab.
|
|
1595
|
+
*/
|
|
1596
|
+
function unionArea(boxes, clip) {
|
|
1597
|
+
const cut = [];
|
|
1598
|
+
for (const b of boxes) {
|
|
1599
|
+
const x0 = Math.max(b.x, clip.x);
|
|
1600
|
+
const y0 = Math.max(b.y, clip.y);
|
|
1601
|
+
const x1 = Math.min(b.x + b.w, clip.x + clip.w);
|
|
1602
|
+
const y1 = Math.min(b.y + b.h, clip.y + clip.h);
|
|
1603
|
+
if (x1 > x0 && y1 > y0) cut.push({
|
|
1604
|
+
x: x0,
|
|
1605
|
+
y: y0,
|
|
1606
|
+
w: x1 - x0,
|
|
1607
|
+
h: y1 - y0
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
const xs = [...new Set(cut.flatMap((b) => [b.x, b.x + b.w]))].toSorted((a, b) => a - b);
|
|
1611
|
+
let area = 0;
|
|
1612
|
+
for (let i = 0; i + 1 < xs.length; i++) {
|
|
1613
|
+
const left = xs[i];
|
|
1614
|
+
const right = xs[i + 1];
|
|
1615
|
+
const spans = cut.filter((b) => b.x <= left && b.x + b.w >= right).map((b) => [b.y, b.y + b.h]).toSorted((p, q) => p[0] - q[0]);
|
|
1616
|
+
let covered = 0;
|
|
1617
|
+
let top = -Infinity;
|
|
1618
|
+
let bottom = -Infinity;
|
|
1619
|
+
for (const [s, e] of spans) if (s > bottom) {
|
|
1620
|
+
covered += bottom - top > 0 ? bottom - top : 0;
|
|
1621
|
+
top = s;
|
|
1622
|
+
bottom = e;
|
|
1623
|
+
} else bottom = Math.max(bottom, e);
|
|
1624
|
+
covered += bottom - top > 0 ? bottom - top : 0;
|
|
1625
|
+
area += covered * (right - left);
|
|
1626
|
+
}
|
|
1627
|
+
return area;
|
|
1628
|
+
}
|
|
1629
|
+
/** This function gives the rectangle of the container: the bounding box of its shape, or 0, 0, `w`, `h`. */
|
|
1630
|
+
function containerBox(input) {
|
|
1631
|
+
const { container } = input;
|
|
1632
|
+
if (container.shape !== void 0) return boundsOf(container.shape);
|
|
1633
|
+
return {
|
|
1634
|
+
x: 0,
|
|
1635
|
+
y: 0,
|
|
1636
|
+
w: container.w ?? Infinity,
|
|
1637
|
+
h: container.h ?? Infinity
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
/** This function tells if an item is locked at a position. Such an item is not a candidate of the overflow. */
|
|
1641
|
+
const lockedAt = (item) => item.locked === true && item.x !== void 0 && item.y !== void 0;
|
|
1642
|
+
/** This function gives the rectangle of an item at its position: the bounding box of its shape, or its size. */
|
|
1643
|
+
function itemBox(item) {
|
|
1644
|
+
const s = item.shape === void 0 ? {
|
|
1645
|
+
x: 0,
|
|
1646
|
+
y: 0,
|
|
1647
|
+
w: item.w,
|
|
1648
|
+
h: item.h
|
|
1649
|
+
} : boundsOf(item.shape);
|
|
1650
|
+
return {
|
|
1651
|
+
x: (item.x ?? 0) + s.x,
|
|
1652
|
+
y: (item.y ?? 0) + s.y,
|
|
1653
|
+
w: s.w,
|
|
1654
|
+
h: s.h
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
/** This function gives the smallest area of an item: its rectangle, or the area of its shape when that is smaller. */
|
|
1658
|
+
function itemArea(item) {
|
|
1659
|
+
const rect = Math.max(0, item.w) * Math.max(0, item.h);
|
|
1660
|
+
return item.shape === void 0 ? rect : Math.min(rect, blockedArea(item.shape));
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* This function gives an upper bound of the free area of a container with a fixed size. The bound ignores the gap and
|
|
1664
|
+
* the margins, thus it is not too small.
|
|
1665
|
+
*
|
|
1666
|
+
* - The container area is the inner rectangle (the container less the inset). A shape gives the smaller of that and its exact area.
|
|
1667
|
+
* - A rectangle container loses the exact union of the fixed rectangle obstacles and the locked items in the inner rectangle.
|
|
1668
|
+
* - A shape loses only the regions that are fully in it and in the inner rectangle, with no common point between their boxes.
|
|
1669
|
+
* - An obstacle with `move` can go out of the way. Thus the bound does not subtract it.
|
|
1670
|
+
*/
|
|
1671
|
+
function freeAreaBound(input) {
|
|
1672
|
+
const inner = shrink(containerBox(input), input.container.inset ?? 0);
|
|
1673
|
+
if (!(inner.w > 0 && inner.h > 0)) return 0;
|
|
1674
|
+
const blocked = [];
|
|
1675
|
+
for (const o of input.obstacles ?? []) if (o.move === void 0) blocked.push({
|
|
1676
|
+
region: o.region,
|
|
1677
|
+
box: boundsOf(o.region)
|
|
1678
|
+
});
|
|
1679
|
+
for (const item of input.items) {
|
|
1680
|
+
if (!lockedAt(item)) continue;
|
|
1681
|
+
const box = itemBox(item);
|
|
1682
|
+
blocked.push({
|
|
1683
|
+
region: {
|
|
1684
|
+
kind: "rect",
|
|
1685
|
+
...box
|
|
1686
|
+
},
|
|
1687
|
+
box
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
const shape = input.container.shape;
|
|
1691
|
+
const innerArea = inner.w * inner.h;
|
|
1692
|
+
const inInner = (b) => b.w > 0 && b.h > 0 && boxIn(b, inner);
|
|
1693
|
+
if (shape === void 0 || shape.kind === "rect") {
|
|
1694
|
+
const rects = blocked.filter((b) => b.region.kind === "rect");
|
|
1695
|
+
let free = innerArea - unionArea(rects.map((b) => b.box), inner);
|
|
1696
|
+
const kept = [];
|
|
1697
|
+
for (const b of blocked) {
|
|
1698
|
+
if (b.region.kind === "rect" || !inInner(b.box)) continue;
|
|
1699
|
+
if (rects.some((r) => boxesOverlap(r.box, b.box)) || kept.some((k) => boxesOverlap(k, b.box))) continue;
|
|
1700
|
+
kept.push(b.box);
|
|
1701
|
+
free -= blockedArea(b.region);
|
|
1702
|
+
}
|
|
1703
|
+
return Math.max(0, free);
|
|
1704
|
+
}
|
|
1705
|
+
let free = Math.min(innerArea, shapeArea(shape));
|
|
1706
|
+
const kept = [];
|
|
1707
|
+
for (const b of blocked) {
|
|
1708
|
+
if (!inInner(b.box) || !boxInShape(b.box, shape) || kept.some((k) => boxesOverlap(k, b.box))) continue;
|
|
1709
|
+
kept.push(b.box);
|
|
1710
|
+
free -= blockedArea(b.region);
|
|
1711
|
+
}
|
|
1712
|
+
return Math.max(0, free);
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* This function gives a lower bound of the overflow of an input: the items that no packer can place, and their area.
|
|
1716
|
+
*
|
|
1717
|
+
* - The candidates are the items that are not locked at a position.
|
|
1718
|
+
* - A fixed side forces each candidate that is longer than the side less the inset, in each turn that the item permits.
|
|
1719
|
+
* - A fixed size also forces the fewest other candidates that make the rest fit the free area of `freeAreaBound`. These are the largest ones.
|
|
1720
|
+
* - The area of the bound is the area of the long items, plus the part of the area of the rest above the free area.
|
|
1721
|
+
* - The margins and the gap do not count. A slack of the overlap tolerance along the edge of each candidate keeps the bound valid.
|
|
1722
|
+
* - A container with no fixed side gives 0.
|
|
1723
|
+
*/
|
|
1724
|
+
function overflowBound(input) {
|
|
1725
|
+
const { container } = input;
|
|
1726
|
+
const fixedW = container.fixed === "size" || container.fixed === "width";
|
|
1727
|
+
const fixedH = container.fixed === "size" || container.fixed === "height";
|
|
1728
|
+
if (!fixedW && !fixedH) return {
|
|
1729
|
+
forced: 0,
|
|
1730
|
+
area: 0,
|
|
1731
|
+
free: Infinity
|
|
1732
|
+
};
|
|
1733
|
+
const tol = DEFAULT_TOLERANCE.overlap;
|
|
1734
|
+
const bounds = containerBox(input);
|
|
1735
|
+
const inset = container.inset ?? 0;
|
|
1736
|
+
const maxW = fixedW ? bounds.w - 2 * inset + 2 * tol : Infinity;
|
|
1737
|
+
const maxH = fixedH ? bounds.h - 2 * inset + 2 * tol : Infinity;
|
|
1738
|
+
let forced = 0;
|
|
1739
|
+
let area = 0;
|
|
1740
|
+
let slack = 0;
|
|
1741
|
+
const rest = [];
|
|
1742
|
+
for (const item of input.items) {
|
|
1743
|
+
if (lockedAt(item)) continue;
|
|
1744
|
+
const box = itemBox(item);
|
|
1745
|
+
const w = Math.min(item.w, box.w);
|
|
1746
|
+
const h = Math.min(item.h, box.h);
|
|
1747
|
+
const fits = w <= maxW && h <= maxH || item.turnable === true && h <= maxW && w <= maxH;
|
|
1748
|
+
const a = itemArea(item);
|
|
1749
|
+
if (!fits) {
|
|
1750
|
+
forced += 1;
|
|
1751
|
+
area += a;
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
rest.push(a);
|
|
1755
|
+
slack += 2 * tol * (Math.max(0, w) + Math.max(0, h));
|
|
1756
|
+
}
|
|
1757
|
+
if (!(fixedW && fixedH)) return {
|
|
1758
|
+
forced,
|
|
1759
|
+
area,
|
|
1760
|
+
free: Infinity
|
|
1761
|
+
};
|
|
1762
|
+
const free = freeAreaBound(input);
|
|
1763
|
+
const room = free + slack + 2 * tol * (Math.max(0, bounds.w) + Math.max(0, bounds.h));
|
|
1764
|
+
let sum = rest.reduce((s, a) => s + a, 0);
|
|
1765
|
+
area += Math.max(0, sum - room);
|
|
1766
|
+
for (const a of rest.toSorted((p, q) => q - p)) {
|
|
1767
|
+
if (sum <= room || !(a > 0)) break;
|
|
1768
|
+
sum -= a;
|
|
1769
|
+
forced += 1;
|
|
1770
|
+
}
|
|
1771
|
+
return {
|
|
1772
|
+
forced,
|
|
1773
|
+
area,
|
|
1774
|
+
free
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1401
1777
|
// src/sha256.ts
|
|
1402
1778
|
/** The round constants of SHA-256. */
|
|
1403
1779
|
const K = new Uint32Array([
|
|
@@ -1597,53 +1973,68 @@ function toPackInput(file) {
|
|
|
1597
1973
|
};
|
|
1598
1974
|
}
|
|
1599
1975
|
// src/measures.ts
|
|
1600
|
-
const spec = (id, family, unit, direction, min, max, guard) => ({
|
|
1976
|
+
const spec = (id, family, unit, direction, min, max, minEffect, guard) => ({
|
|
1601
1977
|
id,
|
|
1602
1978
|
family,
|
|
1603
1979
|
unit,
|
|
1604
1980
|
direction,
|
|
1605
1981
|
min,
|
|
1606
1982
|
max,
|
|
1983
|
+
...minEffect === void 0 ? {} : { minEffect },
|
|
1607
1984
|
...guard
|
|
1608
1985
|
});
|
|
1986
|
+
/** The minimum effect of an area or a ratio of lengths: 1% of the larger value. */
|
|
1987
|
+
const REL = Object.freeze({ rel: .01 });
|
|
1609
1988
|
/**
|
|
1610
1989
|
* The facts of each measure, by id. `docs/MEASURES.md` gives the full definitions. This record is the source of the floor
|
|
1611
|
-
* and the tolerance of each guard of the packer.
|
|
1990
|
+
* and the tolerance of each guard of the packer, and of the minimum effect of each measure.
|
|
1991
|
+
*
|
|
1992
|
+
* - A count and a share of counts have the minimum effect 0. Thus each change of a count is an effect.
|
|
1993
|
+
* - A share of an area from 0 to 1 has 0.005. A guard has its tolerance.
|
|
1994
|
+
* - An area and a ratio of lengths have `{ rel: 0.01 }`: 1% of the larger value.
|
|
1995
|
+
* - The time has no minimum effect, because the times of CI change each time.
|
|
1612
1996
|
*/
|
|
1613
1997
|
const MEASURE_SPECS = Object.freeze({
|
|
1614
|
-
"pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity),
|
|
1615
|
-
"pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1),
|
|
1616
|
-
"pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1),
|
|
1617
|
-
"pack.aspect": spec("pack.aspect", "aspect", "ratio", "lower", 1, Infinity),
|
|
1618
|
-
"pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity, {
|
|
1998
|
+
"pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity, REL),
|
|
1999
|
+
"pack.fill": spec("pack.fill", "density", "share", "higher", 0, 1, .005),
|
|
2000
|
+
"pack.whitespace": spec("pack.whitespace", "density", "share", "lower", 0, 1, .005),
|
|
2001
|
+
"pack.aspect": spec("pack.aspect", "aspect", "ratio", "lower", 1, Infinity, REL),
|
|
2002
|
+
"pack.aspectError": spec("pack.aspectError", "aspect", "natural log", "lower", 0, Infinity, .01, {
|
|
1619
2003
|
floor: 0,
|
|
1620
2004
|
tol: .01
|
|
1621
2005
|
}),
|
|
1622
|
-
"pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, {
|
|
2006
|
+
"pack.order.inversions": spec("pack.order.inversions", "order", "share", "lower", 0, 1, 0, {
|
|
1623
2007
|
floor: 0,
|
|
1624
2008
|
tol: 0
|
|
1625
2009
|
}),
|
|
1626
|
-
"pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity),
|
|
1627
|
-
"pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1),
|
|
1628
|
-
"pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, {
|
|
2010
|
+
"pack.order.breaks": spec("pack.order.breaks", "order", "count", "lower", 0, Infinity, 0),
|
|
2011
|
+
"pack.raggedness": spec("pack.raggedness", "raggedness", "share", "lower", 0, 1, .005),
|
|
2012
|
+
"pack.overflow": spec("pack.overflow", "overflow", "count", "lower", 0, Infinity, 0, {
|
|
1629
2013
|
floor: 0,
|
|
1630
2014
|
tol: 0
|
|
1631
2015
|
}),
|
|
1632
|
-
"pack.
|
|
2016
|
+
"pack.overflow.rate": spec("pack.overflow.rate", "overflow", "share", "lower", 0, 1, 0),
|
|
2017
|
+
"pack.overflow.area": spec("pack.overflow.area", "overflow", "share", "lower", 0, 1, .005),
|
|
2018
|
+
"pack.overflow.forced": spec("pack.overflow.forced", "overflow", "count", "lower", 0, Infinity, 0),
|
|
2019
|
+
"pack.overflow.forcedArea": spec("pack.overflow.forcedArea", "overflow", "share", "lower", 0, 1, .005),
|
|
2020
|
+
"pack.overflow.excess": spec("pack.overflow.excess", "overflow", "count", "lower", 0, Infinity, 0),
|
|
2021
|
+
"pack.overflow.forcedAreaMin": spec("pack.overflow.forcedAreaMin", "overflow", "share", "lower", 0, 1, .005),
|
|
2022
|
+
"pack.overflow.excessArea": spec("pack.overflow.excessArea", "overflow", "share", "lower", 0, 1, .005),
|
|
2023
|
+
"pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, .02, {
|
|
1633
2024
|
floor: 0,
|
|
1634
2025
|
tol: .02
|
|
1635
2026
|
}),
|
|
1636
|
-
"pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, {
|
|
2027
|
+
"pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, .005, {
|
|
1637
2028
|
floor: 0,
|
|
1638
2029
|
tol: .005
|
|
1639
2030
|
}),
|
|
1640
|
-
"pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, {
|
|
2031
|
+
"pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, .01, {
|
|
1641
2032
|
floor: 0,
|
|
1642
2033
|
tol: .01
|
|
1643
2034
|
}),
|
|
1644
|
-
"pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1),
|
|
1645
|
-
"pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity),
|
|
1646
|
-
"pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity)
|
|
2035
|
+
"pack.deterministic": spec("pack.deterministic", "determinism", "boolean", "higher", 0, 1, 0),
|
|
2036
|
+
"pack.search.widths": spec("pack.search.widths", "speed", "count", "lower", 0, Infinity, 0),
|
|
2037
|
+
"pack.search.ms": spec("pack.search.ms", "speed", "milliseconds", "lower", 0, Infinity, void 0)
|
|
1647
2038
|
});
|
|
1648
2039
|
/** This function gives the smallest rectangle around the boxes. No box gives a rectangle of size 0 at 0, 0. */
|
|
1649
2040
|
function hullOf(boxes) {
|
|
@@ -1732,6 +2123,9 @@ function traceSum(output, id) {
|
|
|
1732
2123
|
* - The boxes are the placed items with a width and a height above 0. The hull is the smallest rectangle around them.
|
|
1733
2124
|
* - An empty layout gives the area 0, the aspect 1 and the aspect error 0. Its fill is 0 and its whitespace is 1.
|
|
1734
2125
|
* - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
|
|
2126
|
+
* - The overflow measures count each id once. `pack.overflow.forced` is the lower bound of `overflowBound`.
|
|
2127
|
+
* - `pack.overflow.excess` is the overflow less the forced overflow. A container with no fixed side gives 0 for both.
|
|
2128
|
+
* - `pack.overflow.forcedAreaMin` is a lower bound of the overflow area. `pack.overflow.excessArea` is the overflow area above it.
|
|
1735
2129
|
*
|
|
1736
2130
|
* @example
|
|
1737
2131
|
* ```ts
|
|
@@ -1766,6 +2160,17 @@ function measure(input, output, options = {}) {
|
|
|
1766
2160
|
}
|
|
1767
2161
|
const rights = rowsOf$1(boxes, options.rowTolerance ?? quantileOrZero(boxes.map((b) => b.h), .5) / 2).map((r) => r.right);
|
|
1768
2162
|
const spread = rights.length > 0 ? Math.max(...rights) - Math.min(...rights) : 0;
|
|
2163
|
+
const unplaced = new Set(output.overflow);
|
|
2164
|
+
let itemArea = 0;
|
|
2165
|
+
let unplacedArea = 0;
|
|
2166
|
+
for (const item of input.items) {
|
|
2167
|
+
const a = Math.max(0, item.w) * Math.max(0, item.h);
|
|
2168
|
+
itemArea += a;
|
|
2169
|
+
if (unplaced.has(item.id)) unplacedArea += a;
|
|
2170
|
+
}
|
|
2171
|
+
const bound = overflowBound(input);
|
|
2172
|
+
const overflowArea = itemArea > 0 ? unplacedArea / itemArea : 0;
|
|
2173
|
+
const forcedAreaMin = itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0;
|
|
1769
2174
|
return {
|
|
1770
2175
|
"pack.area": hullArea,
|
|
1771
2176
|
"pack.fill": fill,
|
|
@@ -1775,12 +2180,27 @@ function measure(input, output, options = {}) {
|
|
|
1775
2180
|
"pack.order.inversions": pairs > 0 ? inversions / pairs : 0,
|
|
1776
2181
|
"pack.order.breaks": breaks,
|
|
1777
2182
|
"pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
|
|
1778
|
-
"pack.overflow":
|
|
2183
|
+
"pack.overflow": unplaced.size,
|
|
2184
|
+
"pack.overflow.rate": input.items.length > 0 ? unplaced.size / input.items.length : 0,
|
|
2185
|
+
"pack.overflow.area": overflowArea,
|
|
2186
|
+
"pack.overflow.forced": bound.forced,
|
|
2187
|
+
"pack.overflow.forcedArea": forcedAreaMin,
|
|
2188
|
+
"pack.overflow.excess": Math.max(0, unplaced.size - bound.forced),
|
|
2189
|
+
"pack.overflow.forcedAreaMin": forcedAreaMin,
|
|
2190
|
+
"pack.overflow.excessArea": excessAreaOf(overflowArea, forcedAreaMin),
|
|
1779
2191
|
"pack.search.widths": traceSum(output, "pack.search.widths"),
|
|
1780
2192
|
"pack.search.ms": traceSum(output, "pack.search.ms")
|
|
1781
2193
|
};
|
|
1782
2194
|
}
|
|
1783
2195
|
/**
|
|
2196
|
+
* This function gives the overflow area above its lower bound. A value from `-AREA_NOISE` to 0 becomes 0. A value below
|
|
2197
|
+
* `-AREA_NOISE` stays, because it shows a bug of the bound or of the packer.
|
|
2198
|
+
*/
|
|
2199
|
+
function excessAreaOf(overflowArea, forcedAreaMin) {
|
|
2200
|
+
const excess = overflowArea - forcedAreaMin;
|
|
2201
|
+
return excess < 0 && excess >= -1e-9 ? 0 : excess;
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
1784
2204
|
* This function gives the stability measures of a change pair: the run on the base input and the run on the changed
|
|
1785
2205
|
* input. The items match by id. The values are not rounded.
|
|
1786
2206
|
*
|
|
@@ -2129,6 +2549,7 @@ function packFixedWidth(id, input, options) {
|
|
|
2129
2549
|
const start = now();
|
|
2130
2550
|
const frame = buildFrame(input);
|
|
2131
2551
|
const rects = rectsOf(frame);
|
|
2552
|
+
clearMoves();
|
|
2132
2553
|
if (frame.w > 0) pack(rects, outerOf(frame), {
|
|
2133
2554
|
strategy: id,
|
|
2134
2555
|
sortOrder: options.sortOrder,
|
|
@@ -2140,7 +2561,10 @@ function packFixedWidth(id, input, options) {
|
|
|
2140
2561
|
trace: [{
|
|
2141
2562
|
id,
|
|
2142
2563
|
ms: now() - start,
|
|
2143
|
-
measures: {
|
|
2564
|
+
measures: {
|
|
2565
|
+
"pack.overflow": output.overflow.length,
|
|
2566
|
+
[MOVES[id]]: moveCount(MOVES[id])
|
|
2567
|
+
}
|
|
2144
2568
|
}]
|
|
2145
2569
|
};
|
|
2146
2570
|
}
|
|
@@ -2246,7 +2670,10 @@ const logSweep = {
|
|
|
2246
2670
|
}, {
|
|
2247
2671
|
id,
|
|
2248
2672
|
ms: ms / runs,
|
|
2249
|
-
measures: {
|
|
2673
|
+
measures: {
|
|
2674
|
+
"pack.overflow": output.overflow.length,
|
|
2675
|
+
[MOVES[id]]: moveCount(MOVES[id])
|
|
2676
|
+
}
|
|
2250
2677
|
}]
|
|
2251
2678
|
};
|
|
2252
2679
|
}
|
|
@@ -2282,11 +2709,12 @@ function pushFixedWidth(input, options, previous) {
|
|
|
2282
2709
|
}
|
|
2283
2710
|
const inPlay = frame.entries.filter((e) => !pending.includes(e)).map((e) => e.rect);
|
|
2284
2711
|
const maxShoves = options.maxShovesPerItem * Math.max(1, inPlay.length);
|
|
2712
|
+
let cascaded = 0;
|
|
2285
2713
|
for (const e of positioned) {
|
|
2286
|
-
pushCascade(e.rect, inPlay, {
|
|
2714
|
+
cascaded += pushCascade(e.rect, inPlay, {
|
|
2287
2715
|
gap: 0,
|
|
2288
2716
|
maxShoves
|
|
2289
|
-
});
|
|
2717
|
+
}).length;
|
|
2290
2718
|
e.rect.locked = true;
|
|
2291
2719
|
}
|
|
2292
2720
|
const { bounds } = frame;
|
|
@@ -2328,7 +2756,10 @@ function pushFixedWidth(input, options, previous) {
|
|
|
2328
2756
|
trace: [{
|
|
2329
2757
|
id: "push",
|
|
2330
2758
|
ms: pushMs,
|
|
2331
|
-
measures: {
|
|
2759
|
+
measures: {
|
|
2760
|
+
"pack.push.moved": moved.length,
|
|
2761
|
+
[MOVES.push]: cascaded
|
|
2762
|
+
}
|
|
2332
2763
|
}, {
|
|
2333
2764
|
id: "skyline",
|
|
2334
2765
|
ms: now() - skylineStart,
|
|
@@ -2488,8 +2919,12 @@ function inner(box, tolerance) {
|
|
|
2488
2919
|
* The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
|
|
2489
2920
|
* The rules read a shape of the container or of an item as its bounding box (decision 5). The bounding box of an item
|
|
2490
2921
|
* shape is relative to the position of the item. The result has no tolerance in it.
|
|
2922
|
+
*
|
|
2923
|
+
* With `options.exactContainer`, a shaped container with the size `fixed` also uses its true outline for `in-container`.
|
|
2924
|
+
* A box then counts when `rectInRegion` of the contract tells that it is not in the shape, as in `packHard`.
|
|
2925
|
+
* For that test, the box grows by the inset and becomes smaller by `tolerance.overlap` on each side. The default is `false`.
|
|
2491
2926
|
*/
|
|
2492
|
-
function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
|
|
2927
|
+
function checkHard(input, output, tolerance = DEFAULT_TOLERANCE, options = {}) {
|
|
2493
2928
|
const tol = tolerance.overlap;
|
|
2494
2929
|
const placements = /* @__PURE__ */ new Map();
|
|
2495
2930
|
for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
|
|
@@ -2554,11 +2989,18 @@ function checkHard(input, output, tolerance = DEFAULT_TOLERANCE) {
|
|
|
2554
2989
|
const inset = container.inset ?? 0;
|
|
2555
2990
|
const fixedW = container.fixed === "size" || container.fixed === "width";
|
|
2556
2991
|
const fixedH = container.fixed === "size" || container.fixed === "height";
|
|
2992
|
+
const exact = options.exactContainer === true && container.fixed === "size" ? container.shape : void 0;
|
|
2557
2993
|
let outside = 0;
|
|
2558
2994
|
for (const box of boxes) {
|
|
2559
2995
|
const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
|
|
2560
2996
|
const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
|
|
2561
2997
|
if (outX || outY) outside += 1;
|
|
2998
|
+
else if (exact !== void 0 && !rectInRegion(exact, inner({
|
|
2999
|
+
l: box.l - inset,
|
|
3000
|
+
t: box.t - inset,
|
|
3001
|
+
r: box.r + inset,
|
|
3002
|
+
b: box.b + inset
|
|
3003
|
+
}, tol))) outside += 1;
|
|
2562
3004
|
}
|
|
2563
3005
|
return {
|
|
2564
3006
|
"no-overlap": overlaps,
|