spine-rigc 2.1.0 → 2.1.1
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/docs/AUTHORING.md +13 -4
- package/package.json +1 -1
- package/src/atlas.ts +192 -23
package/docs/AUTHORING.md
CHANGED
|
@@ -524,10 +524,19 @@ what changes is that two rectangles may overlap where neither footprint is.
|
|
|
524
524
|
are unchanged, except that the `free` search's area bound sums the texels
|
|
525
525
|
the cells own rather than the cells, since overlapping cells can cover more
|
|
526
526
|
than the page (a spilled `polygon` page was refused as fitting no page until
|
|
527
|
-
it did — `PK86`). What it costs in time
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
527
|
+
it did — `PK86`). **What it costs in time**, measured for the pack alone
|
|
528
|
+
(`--page-edges free`, padding 2, page size 2048, both shapes in one
|
|
529
|
+
process, a shared machine, so read the ratios): on a production rig's region
|
|
530
|
+
set (31 meshes, a few of them large, a two-page spill, the `rect` pages to
|
|
531
|
+
the texel) 0.15–0.18 s against `rect`'s 0.06 s — it was 260–304 s before
|
|
532
|
+
issue #1102, because a page search ran a full footprint pass at every width
|
|
533
|
+
and threw each one away; on a generated set of that shape
|
|
534
|
+
(`fixtures/polypack_shapes.ts`, seed 1107) 0.17 s against 0.06 s, from
|
|
535
|
+
4.5 s. Where the footprint pass does move a page the cost is real work and
|
|
536
|
+
stays: on a generated set it packs smaller (seed 1105, 4,882,012 →
|
|
537
|
+
4,467,942 texels over two pages) 0.96–0.99 s against 0.07–0.10 s, about ten
|
|
538
|
+
to fourteen times `rect`, from 127 s. `PK93` holds the first generated set
|
|
539
|
+
as a count of free-list splits, within ten times `rect`'s.
|
|
531
540
|
- **The pixels.** Every cell is drawn whole in packing order, then every
|
|
532
541
|
region's owned texels are drawn again with its own values, so every texel a
|
|
533
542
|
region can sample is its own (`PK79`). A texel nobody owns carries the last
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/atlas.ts
CHANGED
|
@@ -834,7 +834,7 @@ function smallestPageFor(
|
|
|
834
834
|
for (const w of edges) for (const h of edges) candidates.push({ w, h });
|
|
835
835
|
candidates.sort((a, b) => a.w * a.h - b.w * b.h || a.w - b.w);
|
|
836
836
|
for (const candidate of candidates) {
|
|
837
|
-
const attempt = placePass(cells, shapes, candidate.w, candidate.h);
|
|
837
|
+
const attempt = placePass(cells, shapes, candidate.w, candidate.h, Infinity, { wholeOrNothing: true });
|
|
838
838
|
if (attempt.some((r) => r === null)) continue;
|
|
839
839
|
return { width: candidate.w, height: candidate.h, rects: attempt as Rect[] };
|
|
840
840
|
}
|
|
@@ -901,6 +901,21 @@ export interface FreePageSearch {
|
|
|
901
901
|
abandoned: number;
|
|
902
902
|
/** Widths never placed: too narrow for the cells' area, or wide enough that the tallest cell alone loses. */
|
|
903
903
|
skipped: number;
|
|
904
|
+
/** What the passes placed, by kind (`PassTally`). */
|
|
905
|
+
tally: PassTally;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* How `freePageSearch` runs its passes — for the selftest's plants only
|
|
910
|
+
* (`PK94`, `PK95`). Each turns one of issue #1102's savings off: `stopAtMiss:
|
|
911
|
+
* false` runs every footprint pass to its end, `edgeIndex: false` prunes the
|
|
912
|
+
* free list by scanning all of it. The page is the same either way; only what
|
|
913
|
+
* it costs differs, and the plants hold that.
|
|
914
|
+
*/
|
|
915
|
+
export interface FreePageSearchOptions {
|
|
916
|
+
stopAtMiss?: boolean;
|
|
917
|
+
/** `false` scans the whole free list for a piece's containers (`splitFree`), as the prune did before issue #1102. */
|
|
918
|
+
edgeIndex?: boolean;
|
|
904
919
|
}
|
|
905
920
|
|
|
906
921
|
/**
|
|
@@ -910,7 +925,13 @@ export interface FreePageSearch {
|
|
|
910
925
|
* packer uses; the counts are for the control. `shapes`, one per cell, is
|
|
911
926
|
* `shape: 'polygon'`'s placement (`placePass`); absent, the rectangle pass.
|
|
912
927
|
*/
|
|
913
|
-
export function freePageSearch(
|
|
928
|
+
export function freePageSearch(
|
|
929
|
+
cells: Array<{ w: number; h: number }>,
|
|
930
|
+
maxEdge: number,
|
|
931
|
+
shapes?: readonly CellShape[],
|
|
932
|
+
options: FreePageSearchOptions = {},
|
|
933
|
+
): FreePageSearch {
|
|
934
|
+
const wholeOrNothing = options.stopAtMiss ?? true;
|
|
914
935
|
let widest = 0;
|
|
915
936
|
let tallest = 0;
|
|
916
937
|
let cellArea = 0;
|
|
@@ -927,7 +948,7 @@ export function freePageSearch(cells: Array<{ w: number; h: number }>, maxEdge:
|
|
|
927
948
|
cellArea = 0;
|
|
928
949
|
for (const shape of shapes) cellArea += shape.owned;
|
|
929
950
|
}
|
|
930
|
-
const out: FreePageSearch = { page: null, widths: 0, completed: 0, abandoned: 0, skipped: 0 };
|
|
951
|
+
const out: FreePageSearch = { page: null, widths: 0, completed: 0, abandoned: 0, skipped: 0, tally: { rectPlaced: 0, footprintPlaced: 0, bandSplits: 0, containmentTests: 0 } };
|
|
931
952
|
let best: { width: number; height: number; rects: Rect[] } | null = null;
|
|
932
953
|
for (let width = Math.ceil(widest / FREE_EDGE_STEP) * FREE_EDGE_STEP; width <= maxEdge; width += FREE_EDGE_STEP) {
|
|
933
954
|
out.widths++;
|
|
@@ -937,7 +958,7 @@ export function freePageSearch(cells: Array<{ w: number; h: number }>, maxEdge:
|
|
|
937
958
|
continue;
|
|
938
959
|
}
|
|
939
960
|
const maxBottom = best === null ? Infinity : Math.floor(bestArea / width);
|
|
940
|
-
const attempt = placePass(cells, shapes, width, maxEdge, maxBottom);
|
|
961
|
+
const attempt = placePass(cells, shapes, width, maxEdge, maxBottom, { wholeOrNothing, tally: out.tally, edgeIndex: options.edgeIndex ?? true });
|
|
941
962
|
let height = 0;
|
|
942
963
|
for (const r of attempt) if (r !== null) height = Math.max(height, r.y + r.h);
|
|
943
964
|
if (height > maxBottom) {
|
|
@@ -1094,9 +1115,46 @@ function placePass(
|
|
|
1094
1115
|
pageW: number,
|
|
1095
1116
|
pageH: number,
|
|
1096
1117
|
maxBottom = Infinity,
|
|
1118
|
+
pass: PassOptions = {},
|
|
1097
1119
|
): Array<Rect | null> {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1120
|
+
const wholeOrNothing = pass.wholeOrNothing ?? false;
|
|
1121
|
+
const tally = pass.tally;
|
|
1122
|
+
if (shapes === undefined || shapes.every((s) => s.whole)) {
|
|
1123
|
+
const byRect = packOnePage(cells, pageW, pageH, maxBottom);
|
|
1124
|
+
if (tally !== undefined) tally.rectPlaced += placedCount(byRect);
|
|
1125
|
+
return byRect;
|
|
1126
|
+
}
|
|
1127
|
+
// ⭐ `wholeOrNothing` is the page searches' question (issue #1102): they keep
|
|
1128
|
+
// a pass only when it placed every cell, so a pass that has missed one is
|
|
1129
|
+
// already discarded, whatever it does next. Two savings follow, and neither
|
|
1130
|
+
// can change a page the search keeps:
|
|
1131
|
+
//
|
|
1132
|
+
// * the rectangle pass is not run when the cells' own area exceeds the
|
|
1133
|
+
// page — disjoint rectangles cannot all fit, so it could not place every
|
|
1134
|
+
// cell, and the pass it would have lost or won against is discarded either
|
|
1135
|
+
// way (a footprint pass that placed every cell beats a rectangle pass that
|
|
1136
|
+
// did not, on the first key);
|
|
1137
|
+
// * the footprint pass stops at its first miss (`packOnePageByFootprint`'s
|
|
1138
|
+
// `stopAtMiss`), returning the cells after it as not placed.
|
|
1139
|
+
//
|
|
1140
|
+
// What is returned can then differ from the full answer only in which of two
|
|
1141
|
+
// failed passes it is, or in how many cells a failed pass placed — and the
|
|
1142
|
+
// searches read neither. The spill's assignment pass, which keeps the cells
|
|
1143
|
+
// a pass did place, never asks this (`packAtlas`).
|
|
1144
|
+
//
|
|
1145
|
+
// Measured on a replica of a production rig (31 meshes, a two-page `free`
|
|
1146
|
+
// spill at 2048, padding 2), where the pack took 260–304 s against `rect`'s
|
|
1147
|
+
// 0.14–0.24 s: the single-page search ran a full footprint pass at every one of its
|
|
1148
|
+
// 854 widths (54 % of the time), because nothing fits and so no best page
|
|
1149
|
+
// ever bounds a width, and the first page's own search ran one at every width
|
|
1150
|
+
// it did not abandon (46 %). The largest cell's owned box does not start at
|
|
1151
|
+
// its cell's corner, so on an empty page that cell is every footprint pass's
|
|
1152
|
+
// first miss — all 1,709 of them — and every one of those passes was thrown
|
|
1153
|
+
// away. With the early stop the pack takes 0.15–0.7 s by the load, the
|
|
1154
|
+
// same pages.
|
|
1155
|
+
const rectCanFit = !wholeOrNothing || cellAreaOf(cells) <= pageW * pageH;
|
|
1156
|
+
const byRect = rectCanFit ? packOnePage(cells, pageW, pageH, maxBottom) : null;
|
|
1157
|
+
if (tally !== undefined && byRect !== null) tally.rectPlaced += placedCount(byRect);
|
|
1100
1158
|
// ⭐ `polygon` never costs a page anything `rect` would have saved. Both passes
|
|
1101
1159
|
// are run on the same page and the better one is kept: more cells placed,
|
|
1102
1160
|
// then the higher bottom edge, then — on a tie — the rectangle pass. Two
|
|
@@ -1107,15 +1165,56 @@ function placePass(
|
|
|
1107
1165
|
// than `rect` on two of the five gallery rigs that carry a mesh, because a
|
|
1108
1166
|
// greedy pass that places one cell differently places every later one
|
|
1109
1167
|
// differently too.
|
|
1110
|
-
const byFootprint = packOnePageByFootprint(shapes, pageW, pageH, maxBottom);
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
const
|
|
1114
|
-
const
|
|
1168
|
+
const byFootprint = packOnePageByFootprint(shapes, pageW, pageH, maxBottom, wholeOrNothing, pass.edgeIndex ?? true, tally);
|
|
1169
|
+
if (tally !== undefined) tally.footprintPlaced += placedCount(byFootprint);
|
|
1170
|
+
if (byRect === null) return byFootprint;
|
|
1171
|
+
const bottomOf = (placed: Array<Rect | null>): number => placed.reduce((n, r) => (r === null ? n : Math.max(n, r.y + r.h)), 0);
|
|
1172
|
+
const placedRect = placedCount(byRect);
|
|
1173
|
+
const placedFoot = placedCount(byFootprint);
|
|
1115
1174
|
if (placedFoot !== placedRect) return placedFoot > placedRect ? byFootprint : byRect;
|
|
1116
1175
|
return bottomOf(byFootprint) < bottomOf(byRect) ? byFootprint : byRect;
|
|
1117
1176
|
}
|
|
1118
1177
|
|
|
1178
|
+
/** How `placePass` runs — what the page searches ask of it (issue #1102). */
|
|
1179
|
+
interface PassOptions {
|
|
1180
|
+
/** Only a pass that places every cell is kept by the caller. See `placePass`. */
|
|
1181
|
+
wholeOrNothing?: boolean;
|
|
1182
|
+
/** Where to count what the passes placed. */
|
|
1183
|
+
tally?: PassTally;
|
|
1184
|
+
/** `splitFree`'s edge index (default on); off only for the selftest's plant. */
|
|
1185
|
+
edgeIndex?: boolean;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** How many cells a pass placed. */
|
|
1189
|
+
function placedCount(pass: ReadonlyArray<Rect | null>): number {
|
|
1190
|
+
let n = 0;
|
|
1191
|
+
for (const r of pass) if (r !== null) n++;
|
|
1192
|
+
return n;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/** The cells' own area — what disjoint rectangles need of a page at least. */
|
|
1196
|
+
function cellAreaOf(cells: ReadonlyArray<{ w: number; h: number }>): number {
|
|
1197
|
+
let area = 0;
|
|
1198
|
+
for (const cell of cells) area += cell.w * cell.h;
|
|
1199
|
+
return area;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* What a page search's passes placed, by kind — the operation count a search's
|
|
1204
|
+
* cost is held by (`PK93`), since a footprint placement splits the free list
|
|
1205
|
+
* once per band of what the cell owns and a rectangle placement once.
|
|
1206
|
+
*/
|
|
1207
|
+
export interface PassTally {
|
|
1208
|
+
/** Cells placed by rectangle passes (`packOnePage`). */
|
|
1209
|
+
rectPlaced: number;
|
|
1210
|
+
/** Cells placed by footprint passes (`packOnePageByFootprint`). */
|
|
1211
|
+
footprintPlaced: number;
|
|
1212
|
+
/** Free-list splits the footprint passes made — one per band of every cell they placed (`splitFree`). */
|
|
1213
|
+
bandSplits: number;
|
|
1214
|
+
/** Containment tests the footprint passes' prune made (`splitFree`). */
|
|
1215
|
+
containmentTests: number;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1119
1218
|
// ---------------------------------------------------------------------------
|
|
1120
1219
|
// footprints — `shape: 'polygon'` (issue #1099)
|
|
1121
1220
|
// ---------------------------------------------------------------------------
|
|
@@ -1377,44 +1476,100 @@ function shapesMeet(a: CellShape, ax: number, ay: number, b: CellShape, bx: numb
|
|
|
1377
1476
|
* px, hulls near their rectangles, a `free` page about 2023x2046) the pack
|
|
1378
1477
|
* took 18.5 s with the slivers kept and spent 95 % of it in this prune.
|
|
1379
1478
|
*/
|
|
1380
|
-
function splitFree(free: Rect[], put: Rect, minW = 1, minH = 1): void {
|
|
1479
|
+
function splitFree(free: Rect[], put: Rect, minW = 1, minH = 1, edgeIndex = true, tally?: PassTally): void {
|
|
1381
1480
|
const next: Rect[] = [];
|
|
1382
|
-
|
|
1481
|
+
/** Per entry of `next`: `UNTOUCHED`, or which side of `put` the piece was cut from. */
|
|
1482
|
+
const side: number[] = [];
|
|
1383
1483
|
for (const fr of free) {
|
|
1384
1484
|
const overlaps = put.x < fr.x + fr.w && put.x + put.w > fr.x && put.y < fr.y + fr.h && put.y + put.h > fr.y;
|
|
1385
1485
|
if (!overlaps) {
|
|
1386
1486
|
next.push(fr);
|
|
1387
|
-
|
|
1487
|
+
side.push(UNTOUCHED);
|
|
1388
1488
|
continue;
|
|
1389
1489
|
}
|
|
1390
|
-
if (put.x > fr.x)
|
|
1490
|
+
if (put.x > fr.x) {
|
|
1491
|
+
next.push({ x: fr.x, y: fr.y, w: put.x - fr.x, h: fr.h });
|
|
1492
|
+
side.push(LEFT_OF_PUT);
|
|
1493
|
+
}
|
|
1391
1494
|
if (put.x + put.w < fr.x + fr.w) {
|
|
1392
1495
|
next.push({ x: put.x + put.w, y: fr.y, w: fr.x + fr.w - (put.x + put.w), h: fr.h });
|
|
1496
|
+
side.push(RIGHT_OF_PUT);
|
|
1497
|
+
}
|
|
1498
|
+
if (put.y > fr.y) {
|
|
1499
|
+
next.push({ x: fr.x, y: fr.y, w: fr.w, h: put.y - fr.y });
|
|
1500
|
+
side.push(ABOVE_PUT);
|
|
1393
1501
|
}
|
|
1394
|
-
if (put.y > fr.y) next.push({ x: fr.x, y: fr.y, w: fr.w, h: put.y - fr.y });
|
|
1395
1502
|
if (put.y + put.h < fr.y + fr.h) {
|
|
1396
1503
|
next.push({ x: fr.x, y: put.y + put.h, w: fr.w, h: fr.y + fr.h - (put.y + put.h) });
|
|
1504
|
+
side.push(BELOW_PUT);
|
|
1397
1505
|
}
|
|
1398
|
-
while (untouched.length < next.length) untouched.push(false);
|
|
1399
1506
|
}
|
|
1400
1507
|
const contains = (a: Rect, b: Rect): boolean => b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
|
|
1401
1508
|
const usable = (r: Rect): boolean => r.w >= minW && r.h >= minH;
|
|
1509
|
+
// ⭐ The third saving (issue #1102): a piece's containers are looked for only
|
|
1510
|
+
// among the entries that share the edge it was cut along. A piece cut from
|
|
1511
|
+
// the left of `put` ends at `put.x` and keeps its parent's rows, which meet
|
|
1512
|
+
// `put`'s rows (the parent overlapped `put`). A rectangle containing it covers
|
|
1513
|
+
// those rows from the piece's left edge to at least `put.x`; if it reached
|
|
1514
|
+
// past `put.x` it would overlap `put` — and no entry of `next` does (an
|
|
1515
|
+
// untouched rectangle by definition, a piece by construction). So every
|
|
1516
|
+
// container of a left piece ENDS at `put.x`; likewise a right piece's starts
|
|
1517
|
+
// at `put.x + put.w`, an upper piece's ends at `put.y` and a lower piece's
|
|
1518
|
+
// starts at `put.y + put.h`. The test is the full prune's test, asked of the
|
|
1519
|
+
// only entries that can answer it yes, so `contained` is the same boolean and
|
|
1520
|
+
// the list the same list in the same order. Measured where the footprint
|
|
1521
|
+
// pass does work (`fixtures/polypack_shapes.ts`, seed 1105, the searches a
|
|
1522
|
+
// `polygon` pack of it runs): 415,916,651 containment tests by the full scan,
|
|
1523
|
+
// 15,407,179 by the index, 3.2 s → 2.3 s; with the early stop off as well,
|
|
1524
|
+
// 11,928,721,590 → 537,215,014 and 101 s → 14.5 s. `edgeIndex: false` is
|
|
1525
|
+
// that scan, kept so the selftest can hold that the index finds what the
|
|
1526
|
+
// scan finds (`PK94`) and for nothing else.
|
|
1527
|
+
const scan: number[][] = [[], [], [], []];
|
|
1528
|
+
const all: number[] = [];
|
|
1529
|
+
for (let j = 0; j < next.length; j++) {
|
|
1530
|
+
const r = next[j];
|
|
1531
|
+
if (!usable(r)) continue;
|
|
1532
|
+
if (!edgeIndex) {
|
|
1533
|
+
all.push(j);
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (r.x + r.w === put.x) scan[LEFT_OF_PUT].push(j);
|
|
1537
|
+
if (r.x === put.x + put.w) scan[RIGHT_OF_PUT].push(j);
|
|
1538
|
+
if (r.y + r.h === put.y) scan[ABOVE_PUT].push(j);
|
|
1539
|
+
if (r.y === put.y + put.h) scan[BELOW_PUT].push(j);
|
|
1540
|
+
}
|
|
1402
1541
|
free.length = 0;
|
|
1542
|
+
let tests = 0;
|
|
1403
1543
|
for (let i = 0; i < next.length; i++) {
|
|
1404
1544
|
if (!usable(next[i])) continue;
|
|
1405
|
-
if (
|
|
1545
|
+
if (side[i] === UNTOUCHED) {
|
|
1406
1546
|
free.push(next[i]);
|
|
1407
1547
|
continue;
|
|
1408
1548
|
}
|
|
1409
1549
|
let contained = false;
|
|
1410
|
-
for (
|
|
1411
|
-
if (i === j
|
|
1412
|
-
|
|
1550
|
+
for (const j of edgeIndex ? scan[side[i]] : all) {
|
|
1551
|
+
if (i === j) continue;
|
|
1552
|
+
tests++;
|
|
1553
|
+
if (contains(next[j], next[i]) && (j < i || !contains(next[i], next[j]))) {
|
|
1554
|
+
contained = true;
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1413
1557
|
}
|
|
1414
1558
|
if (!contained) free.push(next[i]);
|
|
1415
1559
|
}
|
|
1560
|
+
if (tally !== undefined) {
|
|
1561
|
+
tally.bandSplits++;
|
|
1562
|
+
tally.containmentTests += tests;
|
|
1563
|
+
}
|
|
1416
1564
|
}
|
|
1417
1565
|
|
|
1566
|
+
/** Which side of the placed rectangle a free-list piece was cut from (`splitFree`) — an index into its edge lists. */
|
|
1567
|
+
const LEFT_OF_PUT = 0;
|
|
1568
|
+
const RIGHT_OF_PUT = 1;
|
|
1569
|
+
const ABOVE_PUT = 2;
|
|
1570
|
+
const BELOW_PUT = 3;
|
|
1571
|
+
const UNTOUCHED = -1;
|
|
1572
|
+
|
|
1418
1573
|
/**
|
|
1419
1574
|
* `packOnePage` with the free-space test replaced by the footprint test
|
|
1420
1575
|
* (issue #1099) — the placement `shape: 'polygon'` makes.
|
|
@@ -1455,7 +1610,15 @@ function splitFree(free: Rect[], put: Rect, minW = 1, minH = 1): void {
|
|
|
1455
1610
|
* over the cells in the caller's order, each over a finite free list, every
|
|
1456
1611
|
* choice made by a total order, and no input but the cells and the page.
|
|
1457
1612
|
*/
|
|
1458
|
-
function packOnePageByFootprint(
|
|
1613
|
+
function packOnePageByFootprint(
|
|
1614
|
+
shapes: readonly CellShape[],
|
|
1615
|
+
pageW: number,
|
|
1616
|
+
pageH: number,
|
|
1617
|
+
maxBottom = Infinity,
|
|
1618
|
+
stopAtMiss = false,
|
|
1619
|
+
edgeIndex = true,
|
|
1620
|
+
tally?: PassTally,
|
|
1621
|
+
): Array<Rect | null> {
|
|
1459
1622
|
const free: Rect[] = [{ x: 0, y: 0, w: pageW, h: pageH }];
|
|
1460
1623
|
const placed: Array<Rect | null> = [];
|
|
1461
1624
|
const owned: Array<{ shape: CellShape; x: number; y: number }> = [];
|
|
@@ -1498,6 +1661,12 @@ function packOnePageByFootprint(shapes: readonly CellShape[], pageW: number, pag
|
|
|
1498
1661
|
}
|
|
1499
1662
|
if (best === null) {
|
|
1500
1663
|
placed.push(null);
|
|
1664
|
+
// A caller that keeps only a pass placing every cell has its answer
|
|
1665
|
+
// (`placePass`, `wholeOrNothing`): the rest are not placed.
|
|
1666
|
+
if (stopAtMiss) {
|
|
1667
|
+
while (placed.length < shapes.length) placed.push(null);
|
|
1668
|
+
return placed;
|
|
1669
|
+
}
|
|
1501
1670
|
continue;
|
|
1502
1671
|
}
|
|
1503
1672
|
const put: Rect = { x: best.x - box.x, y: best.y - box.y, w: shape.width, h: shape.height };
|
|
@@ -1521,7 +1690,7 @@ function packOnePageByFootprint(shapes: readonly CellShape[], pageW: number, pag
|
|
|
1521
1690
|
while (placed.length < shapes.length) placed.push(null);
|
|
1522
1691
|
return placed;
|
|
1523
1692
|
}
|
|
1524
|
-
for (const r of shape.rects) splitFree(free, { x: put.x + r.x, y: put.y + r.y, w: r.w, h: r.h }, minW, minH);
|
|
1693
|
+
for (const r of shape.rects) splitFree(free, { x: put.x + r.x, y: put.y + r.y, w: r.w, h: r.h }, minW, minH, edgeIndex, tally);
|
|
1525
1694
|
}
|
|
1526
1695
|
return placed;
|
|
1527
1696
|
}
|