spine-rigc 2.11.0 → 2.12.0
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/NOTICE.md +2 -2
- package/README.md +13 -7
- package/docs/AUTHORING.md +290 -6
- package/docs/INGEST.md +18 -1
- package/package.json +1 -1
- package/skills/rigc/SKILL.md +6 -1
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/kinds.ts +1 -0
- package/src/assertions/model/index.ts +7 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/atlas.ts +259 -83
- package/src/cli/core_commands.ts +11 -2
- package/src/cli/repack.ts +408 -0
- package/src/cli/shared.ts +96 -3
- package/src/cli/spine_commands.ts +2 -0
- package/src/compile.ts +119 -3
- package/src/core/index.ts +16 -2
- package/src/ingest.ts +154 -8
- package/src/model.ts +21 -2
- package/src/repack.ts +495 -0
- package/src/rig.ts +80 -2
- package/src/validate.ts +61 -0
package/src/atlas.ts
CHANGED
|
@@ -249,8 +249,13 @@ export function pageFootprint(region: { width: number; height: number; degrees:
|
|
|
249
249
|
* field". Four values maximum, because that is where the runtime stops (`if (i
|
|
250
250
|
* === 4) return 4`) and a fifth would silently mean something here that it does
|
|
251
251
|
* not mean there.
|
|
252
|
+
*
|
|
253
|
+
* Exported for `src/repack.ts` (issue #1169), which reads the keys a page and a
|
|
254
|
+
* region state — the ones this parser drops (`filter`, `format`, `repeat`,
|
|
255
|
+
* `split`, `pad`) included — with this function rather than a second spelling
|
|
256
|
+
* of the format, and holds what it walked to `parseAtlasText`'s regions.
|
|
252
257
|
*/
|
|
253
|
-
function readEntry(line: string | null): { key: string; values: string[] } | null {
|
|
258
|
+
export function readEntry(line: string | null): { key: string; values: string[] } | null {
|
|
254
259
|
if (line === null) return null;
|
|
255
260
|
const trimmed = line.trim();
|
|
256
261
|
if (trimmed.length === 0) return null;
|
|
@@ -803,6 +808,13 @@ export interface PackOptions {
|
|
|
803
808
|
* the same either way (`PK94`); only the cost differs.
|
|
804
809
|
*/
|
|
805
810
|
stopAtMiss?: boolean;
|
|
811
|
+
/**
|
|
812
|
+
* `false` splits every band of every cell a footprint pass places over the
|
|
813
|
+
* whole free list — issue #1165's partition off (`splitFreeByCell`), for the
|
|
814
|
+
* selftest's plant (`PK106`) only. The pages are the same either way; only
|
|
815
|
+
* the cost differs.
|
|
816
|
+
*/
|
|
817
|
+
partition?: boolean;
|
|
806
818
|
}
|
|
807
819
|
|
|
808
820
|
/** Where one region landed. `x`/`y` are the REGION's own corner, not its cell's. */
|
|
@@ -884,15 +896,16 @@ function smallestPageFor(
|
|
|
884
896
|
anchors: FootprintAnchors = DEFAULT_FOOTPRINT_ANCHORS,
|
|
885
897
|
tally?: PassTally,
|
|
886
898
|
stopAtMiss = true,
|
|
899
|
+
partition = true,
|
|
887
900
|
): { width: number; height: number; rects: Rect[] } | null {
|
|
888
|
-
if (pageEdges === 'free') return smallestFreePageFor(cells, maxEdge, shapes, anchors, tally, stopAtMiss);
|
|
901
|
+
if (pageEdges === 'free') return smallestFreePageFor(cells, maxEdge, shapes, anchors, tally, stopAtMiss, partition);
|
|
889
902
|
const edges: number[] = [];
|
|
890
903
|
for (let e = 1; e <= maxEdge; e *= 2) edges.push(e);
|
|
891
904
|
const candidates: Array<{ w: number; h: number }> = [];
|
|
892
905
|
for (const w of edges) for (const h of edges) candidates.push({ w, h });
|
|
893
906
|
candidates.sort((a, b) => a.w * a.h - b.w * b.h || a.w - b.w);
|
|
894
907
|
for (const candidate of candidates) {
|
|
895
|
-
const attempt = placePass(cells, shapes, candidate.w, candidate.h, Infinity, { wholeOrNothing: stopAtMiss, anchors, tally });
|
|
908
|
+
const attempt = placePass(cells, shapes, candidate.w, candidate.h, Infinity, { wholeOrNothing: stopAtMiss, anchors, tally, partition });
|
|
896
909
|
if (attempt.some((r) => r === null)) continue;
|
|
897
910
|
return { width: candidate.w, height: candidate.h, rects: attempt as Rect[] };
|
|
898
911
|
}
|
|
@@ -947,8 +960,9 @@ function smallestFreePageFor(
|
|
|
947
960
|
anchors: FootprintAnchors = DEFAULT_FOOTPRINT_ANCHORS,
|
|
948
961
|
tally?: PassTally,
|
|
949
962
|
stopAtMiss = true,
|
|
963
|
+
partition = true,
|
|
950
964
|
): { width: number; height: number; rects: Rect[] } | null {
|
|
951
|
-
const search = freePageSearch(cells, maxEdge, shapes, { anchors, stopAtMiss });
|
|
965
|
+
const search = freePageSearch(cells, maxEdge, shapes, { anchors, stopAtMiss, partition });
|
|
952
966
|
if (tally !== undefined) for (const key of Object.keys(tally) as Array<keyof PassTally>) tally[key] += search.tally[key];
|
|
953
967
|
return search.page;
|
|
954
968
|
}
|
|
@@ -970,15 +984,19 @@ export interface FreePageSearch {
|
|
|
970
984
|
|
|
971
985
|
/**
|
|
972
986
|
* How `freePageSearch` runs its passes — for the selftest's plants only
|
|
973
|
-
* (`PK94`, `PK95`). Each turns one
|
|
974
|
-
*
|
|
975
|
-
* free list by scanning all of it
|
|
976
|
-
*
|
|
987
|
+
* (`PK94`, `PK95`, `PK106`). Each turns one saving off: `stopAtMiss: false`
|
|
988
|
+
* runs every footprint pass to its end and `edgeIndex: false` splits and
|
|
989
|
+
* prunes the whole free list by scanning all of it (issue #1102's two);
|
|
990
|
+
* `partition: false` splits every band over the whole list but keeps the edge
|
|
991
|
+
* index (issue #1165's). The page is the same either way; only what it costs
|
|
992
|
+
* differs, and the plants hold that.
|
|
977
993
|
*/
|
|
978
994
|
export interface FreePageSearchOptions {
|
|
979
995
|
stopAtMiss?: boolean;
|
|
980
|
-
/** `false`
|
|
996
|
+
/** `false` splits the whole free list and scans all of it for a piece's containers (`splitFree`), as the prune did before issue #1102. */
|
|
981
997
|
edgeIndex?: boolean;
|
|
998
|
+
/** `false` splits every band over the whole free list (`splitFreeByCell`), as before issue #1165 — the plant of `PK106`. */
|
|
999
|
+
partition?: boolean;
|
|
982
1000
|
/** Where a candidate may anchor a cell (`FOOTPRINT_ANCHORS`) — `tools/pack_anchor.ts`'s candidate rules; default `box`. */
|
|
983
1001
|
anchors?: FootprintAnchors;
|
|
984
1002
|
}
|
|
@@ -1023,7 +1041,7 @@ export function freePageSearch(
|
|
|
1023
1041
|
continue;
|
|
1024
1042
|
}
|
|
1025
1043
|
const maxBottom = best === null ? Infinity : Math.floor(bestArea / width);
|
|
1026
|
-
const attempt = placePass(cells, shapes, width, maxEdge, maxBottom, { wholeOrNothing, tally: out.tally, edgeIndex: options.edgeIndex ?? true, anchors: options.anchors });
|
|
1044
|
+
const attempt = placePass(cells, shapes, width, maxEdge, maxBottom, { wholeOrNothing, tally: out.tally, edgeIndex: options.edgeIndex ?? true, anchors: options.anchors, partition: options.partition ?? true });
|
|
1027
1045
|
let height = 0;
|
|
1028
1046
|
for (const r of attempt) if (r !== null) height = Math.max(height, r.y + r.h);
|
|
1029
1047
|
if (height > maxBottom) {
|
|
@@ -1231,13 +1249,13 @@ function placePass(
|
|
|
1231
1249
|
// greedy pass that places one cell differently places every later one
|
|
1232
1250
|
// differently too.
|
|
1233
1251
|
const anchors = pass.anchors ?? DEFAULT_FOOTPRINT_ANCHORS;
|
|
1234
|
-
let byFootprint = packOnePageByFootprint(shapes, pageW, pageH, maxBottom, wholeOrNothing, pass.edgeIndex ?? true, tally, anchors === 'best-of' ? 'box' : anchors);
|
|
1252
|
+
let byFootprint = packOnePageByFootprint(shapes, pageW, pageH, maxBottom, wholeOrNothing, pass.edgeIndex ?? true, tally, anchors === 'best-of' ? 'box' : anchors, pass.partition ?? true);
|
|
1235
1253
|
if (anchors === 'best-of') {
|
|
1236
1254
|
// Issue #1104's fourth candidate: both searches on the same page, the
|
|
1237
1255
|
// better kept and the first anchor's on a tie — so a pass is never worse
|
|
1238
1256
|
// than the first anchor's on that page.
|
|
1239
1257
|
if (tally !== undefined) tally.footprintPlaced += placedCount(byFootprint);
|
|
1240
|
-
const byTwo = packOnePageByFootprint(shapes, pageW, pageH, maxBottom, wholeOrNothing, pass.edgeIndex ?? true, tally, 'box-or-cell');
|
|
1258
|
+
const byTwo = packOnePageByFootprint(shapes, pageW, pageH, maxBottom, wholeOrNothing, pass.edgeIndex ?? true, tally, 'box-or-cell', pass.partition ?? true);
|
|
1241
1259
|
const bottom = (placed: Array<Rect | null>): number => placed.reduce((n, r) => (r === null ? n : Math.max(n, r.y + r.h)), 0);
|
|
1242
1260
|
const placedBox = placedCount(byFootprint);
|
|
1243
1261
|
const placedTwo = placedCount(byTwo);
|
|
@@ -1262,6 +1280,8 @@ interface PassOptions {
|
|
|
1262
1280
|
edgeIndex?: boolean;
|
|
1263
1281
|
/** Where a candidate may anchor a cell (`FOOTPRINT_ANCHORS`). */
|
|
1264
1282
|
anchors?: FootprintAnchors;
|
|
1283
|
+
/** `splitFreeByCell`'s partition (default on); off only for the selftest's plant. */
|
|
1284
|
+
partition?: boolean;
|
|
1265
1285
|
}
|
|
1266
1286
|
|
|
1267
1287
|
/** How many cells a pass placed. */
|
|
@@ -1292,6 +1312,12 @@ export interface PassTally {
|
|
|
1292
1312
|
bandSplits: number;
|
|
1293
1313
|
/** Containment tests the footprint passes' prune made (`splitFree`). */
|
|
1294
1314
|
containmentTests: number;
|
|
1315
|
+
/**
|
|
1316
|
+
* Free-list entries the footprint passes' splits read: the list's length at
|
|
1317
|
+
* every band split, and once per placed cell the whole list the partition
|
|
1318
|
+
* reads (`splitFreeByCell`) — the work issue #1165 measured, held by `PK106`.
|
|
1319
|
+
*/
|
|
1320
|
+
entriesRead: number;
|
|
1295
1321
|
/**
|
|
1296
1322
|
* Free rectangles that held a cell's owned box and were refused as its
|
|
1297
1323
|
* first-anchor candidate only because the cell, anchored there, would leave
|
|
@@ -1307,7 +1333,7 @@ export interface PassTally {
|
|
|
1307
1333
|
|
|
1308
1334
|
/** A tally with nothing counted. */
|
|
1309
1335
|
export function emptyTally(): PassTally {
|
|
1310
|
-
return { rectPlaced: 0, footprintPlaced: 0, bandSplits: 0, containmentTests: 0, boxAnchorRefused: 0, missedByAnchor: 0, secondAnchorPlaced: 0 };
|
|
1336
|
+
return { rectPlaced: 0, footprintPlaced: 0, bandSplits: 0, containmentTests: 0, entriesRead: 0, boxAnchorRefused: 0, missedByAnchor: 0, secondAnchorPlaced: 0 };
|
|
1311
1337
|
}
|
|
1312
1338
|
|
|
1313
1339
|
// ---------------------------------------------------------------------------
|
|
@@ -1551,57 +1577,81 @@ function shapesMeet(a: CellShape, ax: number, ay: number, b: CellShape, bx: numb
|
|
|
1551
1577
|
|
|
1552
1578
|
/**
|
|
1553
1579
|
* MaxRects' split of every free rectangle `put` overlaps, then its prune —
|
|
1554
|
-
* `packOnePage`'s,
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
*
|
|
1560
|
-
*
|
|
1561
|
-
*
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1567
|
-
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
1580
|
+
* `packOnePage`'s, over the first `length` entries of `free`, written back in
|
|
1581
|
+
* place; returns the new length. Four savings, none of which changes a
|
|
1582
|
+
* rectangle or the list's order:
|
|
1583
|
+
*
|
|
1584
|
+
* * a free rectangle the placement did not touch is never tested for
|
|
1585
|
+
* containment. Before the split no free rectangle lay inside another (the
|
|
1586
|
+
* prune's own postcondition), and every new piece lies inside the
|
|
1587
|
+
* rectangle it was cut from, so an untouched rectangle inside a new piece
|
|
1588
|
+
* would have lain inside that one — the test `packOnePage` makes for it
|
|
1589
|
+
* always answers "not contained";
|
|
1590
|
+
* * ⭐ (issue #1099, measured on production rigs) a free rectangle narrower
|
|
1591
|
+
* than `minW` or shorter than `minH` is dropped. The caller passes the
|
|
1592
|
+
* least bounding box of every cell the pass places, so such a rectangle can
|
|
1593
|
+
* never be a candidate, and neither can any piece later cut from it (a
|
|
1594
|
+
* piece lies inside the rectangle it was cut from); and a rectangle a
|
|
1595
|
+
* dropped one contains is itself too small. So every rectangle that could
|
|
1596
|
+
* ever be a candidate is kept, in the order the full prune keeps it. What
|
|
1597
|
+
* it removes is the staircase of slivers an irregular hull's bands cut
|
|
1598
|
+
* along its edge: on a production-shaped set (30 regions, 200 to 900 px,
|
|
1599
|
+
* hulls near their rectangles, a `free` page about 2023x2046) the pack took
|
|
1600
|
+
* 18.5 s with the slivers kept and spent 95 % of it in this prune. A piece
|
|
1601
|
+
* that would be dropped is not made at all (issue #1165): it takes part in
|
|
1602
|
+
* no test and is not written back, so it changes no other entry's index
|
|
1603
|
+
* order either;
|
|
1604
|
+
* * the edge index (issue #1102), below;
|
|
1605
|
+
* * (issue #1165) the rebuilt list, each entry's side and the edge lists are
|
|
1606
|
+
* kept between calls (`splitNext` and its neighbours) and the list is
|
|
1607
|
+
* written back over itself by index, never emptied and refilled: on the
|
|
1608
|
+
* production rigs the emptying and the pushes that refilled it were most
|
|
1609
|
+
* of this function's time, and none of it was arithmetic.
|
|
1573
1610
|
*/
|
|
1574
|
-
function splitFree(free: Rect[], put: Rect, minW
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1611
|
+
function splitFree(free: Rect[], length: number, put: Rect, minW: number, minH: number, edgeIndex: boolean, tally?: PassTally): number {
|
|
1612
|
+
const px0 = put.x;
|
|
1613
|
+
const py0 = put.y;
|
|
1614
|
+
const px1 = put.x + put.w;
|
|
1615
|
+
const py1 = put.y + put.h;
|
|
1616
|
+
// A touched entry yields at most four pieces.
|
|
1617
|
+
if (splitSide.length < length * 4) {
|
|
1618
|
+
const capacity = Math.max(64, length * 8);
|
|
1619
|
+
splitSide = new Int8Array(capacity);
|
|
1620
|
+
splitLeft = new Int32Array(capacity);
|
|
1621
|
+
splitRight = new Int32Array(capacity);
|
|
1622
|
+
splitAbove = new Int32Array(capacity);
|
|
1623
|
+
splitBelow = new Int32Array(capacity);
|
|
1624
|
+
}
|
|
1625
|
+
const next = splitNext;
|
|
1626
|
+
const side = splitSide;
|
|
1627
|
+
let n = 0;
|
|
1628
|
+
for (let k = 0; k < length; k++) {
|
|
1629
|
+
const fr = free[k];
|
|
1630
|
+
const fx1 = fr.x + fr.w;
|
|
1631
|
+
const fy1 = fr.y + fr.h;
|
|
1632
|
+
if (!(px0 < fx1 && px1 > fr.x && py0 < fy1 && py1 > fr.y)) {
|
|
1633
|
+
next[n] = fr;
|
|
1634
|
+
side[n++] = UNTOUCHED;
|
|
1583
1635
|
continue;
|
|
1584
1636
|
}
|
|
1585
|
-
if (
|
|
1586
|
-
next
|
|
1587
|
-
side
|
|
1637
|
+
if (px0 > fr.x && px0 - fr.x >= minW && fr.h >= minH) {
|
|
1638
|
+
next[n] = { x: fr.x, y: fr.y, w: px0 - fr.x, h: fr.h };
|
|
1639
|
+
side[n++] = LEFT_OF_PUT;
|
|
1588
1640
|
}
|
|
1589
|
-
if (
|
|
1590
|
-
next
|
|
1591
|
-
side
|
|
1641
|
+
if (px1 < fx1 && fx1 - px1 >= minW && fr.h >= minH) {
|
|
1642
|
+
next[n] = { x: px1, y: fr.y, w: fx1 - px1, h: fr.h };
|
|
1643
|
+
side[n++] = RIGHT_OF_PUT;
|
|
1592
1644
|
}
|
|
1593
|
-
if (
|
|
1594
|
-
next
|
|
1595
|
-
side
|
|
1645
|
+
if (py0 > fr.y && fr.w >= minW && py0 - fr.y >= minH) {
|
|
1646
|
+
next[n] = { x: fr.x, y: fr.y, w: fr.w, h: py0 - fr.y };
|
|
1647
|
+
side[n++] = ABOVE_PUT;
|
|
1596
1648
|
}
|
|
1597
|
-
if (
|
|
1598
|
-
next
|
|
1599
|
-
side
|
|
1649
|
+
if (py1 < fy1 && fr.w >= minW && fy1 - py1 >= minH) {
|
|
1650
|
+
next[n] = { x: fr.x, y: py1, w: fr.w, h: fy1 - py1 };
|
|
1651
|
+
side[n++] = BELOW_PUT;
|
|
1600
1652
|
}
|
|
1601
1653
|
}
|
|
1602
|
-
|
|
1603
|
-
const usable = (r: Rect): boolean => r.w >= minW && r.h >= minH;
|
|
1604
|
-
// ⭐ The third saving (issue #1102): a piece's containers are looked for only
|
|
1654
|
+
// ⭐ The edge index (issue #1102): a piece's containers are looked for only
|
|
1605
1655
|
// among the entries that share the edge it was cut along. A piece cut from
|
|
1606
1656
|
// the left of `put` ends at `put.x` and keeps its parent's rows, which meet
|
|
1607
1657
|
// `put`'s rows (the parent overlapped `put`). A rectangle containing it covers
|
|
@@ -1617,45 +1667,165 @@ function splitFree(free: Rect[], put: Rect, minW = 1, minH = 1, edgeIndex = true
|
|
|
1617
1667
|
// `polygon` pack of it runs): 415,916,651 containment tests by the full scan,
|
|
1618
1668
|
// 15,407,179 by the index, 3.2 s → 2.3 s; with the early stop off as well,
|
|
1619
1669
|
// 11,928,721,590 → 537,215,014 and 101 s → 14.5 s. `edgeIndex: false` is
|
|
1620
|
-
// that scan
|
|
1621
|
-
// scan finds (`PK94`) and for nothing else.
|
|
1622
|
-
const
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1670
|
+
// that scan — every usable entry in `left` — kept so the selftest can hold
|
|
1671
|
+
// that the index finds what the scan finds (`PK94`) and for nothing else.
|
|
1672
|
+
const left = splitLeft;
|
|
1673
|
+
const right = splitRight;
|
|
1674
|
+
const above = splitAbove;
|
|
1675
|
+
const below = splitBelow;
|
|
1676
|
+
let nLeft = 0;
|
|
1677
|
+
let nRight = 0;
|
|
1678
|
+
let nAbove = 0;
|
|
1679
|
+
let nBelow = 0;
|
|
1680
|
+
for (let j = 0; j < n; j++) {
|
|
1625
1681
|
const r = next[j];
|
|
1626
|
-
if (
|
|
1682
|
+
if (r.w < minW || r.h < minH) continue;
|
|
1627
1683
|
if (!edgeIndex) {
|
|
1628
|
-
|
|
1684
|
+
left[nLeft++] = j;
|
|
1629
1685
|
continue;
|
|
1630
1686
|
}
|
|
1631
|
-
if (r.x + r.w ===
|
|
1632
|
-
if (r.x ===
|
|
1633
|
-
if (r.y + r.h ===
|
|
1634
|
-
if (r.y ===
|
|
1687
|
+
if (r.x + r.w === px0) left[nLeft++] = j;
|
|
1688
|
+
if (r.x === px1) right[nRight++] = j;
|
|
1689
|
+
if (r.y + r.h === py0) above[nAbove++] = j;
|
|
1690
|
+
if (r.y === py1) below[nBelow++] = j;
|
|
1635
1691
|
}
|
|
1636
|
-
|
|
1692
|
+
let w = 0;
|
|
1637
1693
|
let tests = 0;
|
|
1638
|
-
for (let i = 0; i <
|
|
1639
|
-
|
|
1640
|
-
if (
|
|
1641
|
-
|
|
1694
|
+
for (let i = 0; i < n; i++) {
|
|
1695
|
+
const piece = next[i];
|
|
1696
|
+
if (piece.w < minW || piece.h < minH) continue;
|
|
1697
|
+
const cut = side[i];
|
|
1698
|
+
if (cut === UNTOUCHED) {
|
|
1699
|
+
free[w++] = piece;
|
|
1642
1700
|
continue;
|
|
1643
1701
|
}
|
|
1702
|
+
const list = !edgeIndex || cut === LEFT_OF_PUT ? left : cut === RIGHT_OF_PUT ? right : cut === ABOVE_PUT ? above : below;
|
|
1703
|
+
const count = !edgeIndex || cut === LEFT_OF_PUT ? nLeft : cut === RIGHT_OF_PUT ? nRight : cut === ABOVE_PUT ? nAbove : nBelow;
|
|
1644
1704
|
let contained = false;
|
|
1645
|
-
for (
|
|
1705
|
+
for (let t = 0; t < count; t++) {
|
|
1706
|
+
const j = list[t];
|
|
1646
1707
|
if (i === j) continue;
|
|
1647
1708
|
tests++;
|
|
1648
|
-
|
|
1709
|
+
const other = next[j];
|
|
1710
|
+
// On a mutual containment (two identical rectangles) the later index
|
|
1711
|
+
// loses, so exactly one survives and it is always the same one.
|
|
1712
|
+
if (rectContains(other, piece) && (j < i || !rectContains(piece, other))) {
|
|
1649
1713
|
contained = true;
|
|
1650
1714
|
break;
|
|
1651
1715
|
}
|
|
1652
1716
|
}
|
|
1653
|
-
if (!contained) free
|
|
1717
|
+
if (!contained) free[w++] = piece;
|
|
1654
1718
|
}
|
|
1655
1719
|
if (tally !== undefined) {
|
|
1656
1720
|
tally.bandSplits++;
|
|
1657
1721
|
tally.containmentTests += tests;
|
|
1722
|
+
tally.entriesRead += length;
|
|
1723
|
+
}
|
|
1724
|
+
return w;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/** Whether `a` contains `b`. */
|
|
1728
|
+
function rectContains(a: Rect, b: Rect): boolean {
|
|
1729
|
+
return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* `splitFree`'s working storage, kept between calls so a split allocates
|
|
1734
|
+
* nothing but the pieces it cuts (issue #1165). `splitFree` is never
|
|
1735
|
+
* re-entered and reads no entry past the `n` it wrote this call, so what an
|
|
1736
|
+
* earlier call left behind is never read.
|
|
1737
|
+
*/
|
|
1738
|
+
const splitNext: Rect[] = [];
|
|
1739
|
+
let splitSide = new Int8Array(0);
|
|
1740
|
+
let splitLeft = new Int32Array(0);
|
|
1741
|
+
let splitRight = new Int32Array(0);
|
|
1742
|
+
let splitAbove = new Int32Array(0);
|
|
1743
|
+
let splitBelow = new Int32Array(0);
|
|
1744
|
+
/** `splitFreeByCell`'s near list, kept between calls for the same reason. */
|
|
1745
|
+
const splitNear: Rect[] = [];
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* Every band of one placed cell split out of the first `length` entries of
|
|
1749
|
+
* `free` (`splitFree`, once per band, top to bottom); returns the new length.
|
|
1750
|
+
*
|
|
1751
|
+
* ⭐ **The list is split near the cell only** (issue #1165). Before a cell's
|
|
1752
|
+
* bands are split, the list is parted once into the entries that touch the
|
|
1753
|
+
* bands' bounding box — overlapping it or sharing an edge with it — and the
|
|
1754
|
+
* rest; every band is then split over the near part alone, and the far part is
|
|
1755
|
+
* written back first, the near part after it. Nothing the far part holds could
|
|
1756
|
+
* have been read:
|
|
1757
|
+
*
|
|
1758
|
+
* * a far entry does not overlap any band (every band lies inside the box),
|
|
1759
|
+
* so every split leaves it untouched — kept as it is, never tested;
|
|
1760
|
+
* * a far entry contains no piece any band cuts. A piece cut from the left of
|
|
1761
|
+
* a band ends at the band's left edge `x` and keeps a row `y` of the band
|
|
1762
|
+
* (its parent overlapped the band), so a container of it holds the texel
|
|
1763
|
+
* `(x − 1, y)` — inside the box or against its left edge, which an entry
|
|
1764
|
+
* that neither overlaps the box nor shares an edge with it cannot hold.
|
|
1765
|
+
* Likewise for the other three sides. And every piece is cut from a near
|
|
1766
|
+
* entry, so it is in the near part for the next band.
|
|
1767
|
+
*
|
|
1768
|
+
* So every split makes the decisions over the near part that it makes over the
|
|
1769
|
+
* whole list, and the far part comes through every split unchanged. ⚠️ What
|
|
1770
|
+
* changes is the ORDER of the list: the far entries move ahead of the near ones.
|
|
1771
|
+
* That decides nothing, because nothing that reads the list reads its order:
|
|
1772
|
+
*
|
|
1773
|
+
* * a split keeps every untouched entry, and keeps a piece unless another
|
|
1774
|
+
* usable entry contains it, the later index losing only between two equal
|
|
1775
|
+
* rectangles — and which of two equal rectangles survives is invisible,
|
|
1776
|
+
* they are the same rectangle — so the SET of rectangles after a split is
|
|
1777
|
+
* a function of the set before it;
|
|
1778
|
+
* * a candidate is chosen by a total order on its score, its free
|
|
1779
|
+
* rectangle's corner and its anchor (`packOnePageByFootprint`); two free
|
|
1780
|
+
* rectangles that tie on all of those put the cell at the same place.
|
|
1781
|
+
*
|
|
1782
|
+
* So the placements are the ones the whole-list split makes, decision for
|
|
1783
|
+
* decision. What it saves: on three production rigs the near part is 10 to 22 %
|
|
1784
|
+
* of the list (about 90 entries of 940 on the slowest), and each of a cell's
|
|
1785
|
+
* 50 or so bands read the whole list. `partition: false` splits every band over
|
|
1786
|
+
* the whole list in order, as before — kept so the selftest can plant the
|
|
1787
|
+
* work back (`PK106`) and hold that the pages do not move (`PK94`, through
|
|
1788
|
+
* `edgeIndex: false`, which splits the whole list too).
|
|
1789
|
+
*/
|
|
1790
|
+
function splitFreeByCell(
|
|
1791
|
+
free: Rect[],
|
|
1792
|
+
length: number,
|
|
1793
|
+
cellX: number,
|
|
1794
|
+
cellY: number,
|
|
1795
|
+
rects: readonly Rect[],
|
|
1796
|
+
minW: number,
|
|
1797
|
+
minH: number,
|
|
1798
|
+
edgeIndex: boolean,
|
|
1799
|
+
partition: boolean,
|
|
1800
|
+
tally?: PassTally,
|
|
1801
|
+
): number {
|
|
1802
|
+
if (!edgeIndex || !partition) {
|
|
1803
|
+
let n = length;
|
|
1804
|
+
for (const r of rects) n = splitFree(free, n, { x: cellX + r.x, y: cellY + r.y, w: r.w, h: r.h }, minW, minH, edgeIndex, tally);
|
|
1805
|
+
return n;
|
|
1806
|
+
}
|
|
1807
|
+
let x0 = Infinity;
|
|
1808
|
+
let y0 = Infinity;
|
|
1809
|
+
let x1 = -Infinity;
|
|
1810
|
+
let y1 = -Infinity;
|
|
1811
|
+
for (const r of rects) {
|
|
1812
|
+
x0 = Math.min(x0, cellX + r.x);
|
|
1813
|
+
y0 = Math.min(y0, cellY + r.y);
|
|
1814
|
+
x1 = Math.max(x1, cellX + r.x + r.w);
|
|
1815
|
+
y1 = Math.max(y1, cellY + r.y + r.h);
|
|
1658
1816
|
}
|
|
1817
|
+
const near = splitNear;
|
|
1818
|
+
let nNear = 0;
|
|
1819
|
+
let nFar = 0;
|
|
1820
|
+
for (let k = 0; k < length; k++) {
|
|
1821
|
+
const fr = free[k];
|
|
1822
|
+
if (fr.x <= x1 && fr.x + fr.w >= x0 && fr.y <= y1 && fr.y + fr.h >= y0) near[nNear++] = fr;
|
|
1823
|
+
else free[nFar++] = fr;
|
|
1824
|
+
}
|
|
1825
|
+
if (tally !== undefined) tally.entriesRead += length;
|
|
1826
|
+
for (const r of rects) nNear = splitFree(near, nNear, { x: cellX + r.x, y: cellY + r.y, w: r.w, h: r.h }, minW, minH, true, tally);
|
|
1827
|
+
for (let k = 0; k < nNear; k++) free[nFar + k] = near[k];
|
|
1828
|
+
return nFar + nNear;
|
|
1659
1829
|
}
|
|
1660
1830
|
|
|
1661
1831
|
/** Which side of the placed rectangle a free-list piece was cut from (`splitFree`) — an index into its edge lists. */
|
|
@@ -1738,8 +1908,11 @@ function packOnePageByFootprint(
|
|
|
1738
1908
|
edgeIndex = true,
|
|
1739
1909
|
tally?: PassTally,
|
|
1740
1910
|
anchors: FootprintAnchors = DEFAULT_FOOTPRINT_ANCHORS,
|
|
1911
|
+
partition = true,
|
|
1741
1912
|
): Array<Rect | null> {
|
|
1913
|
+
// The free list is `free`'s first `freeLength` entries (`splitFree`).
|
|
1742
1914
|
const free: Rect[] = [{ x: 0, y: 0, w: pageW, h: pageH }];
|
|
1915
|
+
let freeLength = 1;
|
|
1743
1916
|
const placed: Array<Rect | null> = [];
|
|
1744
1917
|
const owned: Array<{ shape: CellShape; x: number; y: number }> = [];
|
|
1745
1918
|
// The least box any cell of this pass needs — what a free rectangle must
|
|
@@ -1759,7 +1932,8 @@ function packOnePageByFootprint(
|
|
|
1759
1932
|
let held = false;
|
|
1760
1933
|
// The second anchor differs from the first only when the box is inset.
|
|
1761
1934
|
const anchorCount = anchors !== 'box' && (box.x !== 0 || box.y !== 0) ? 2 : 1;
|
|
1762
|
-
for (
|
|
1935
|
+
for (let f = 0; f < freeLength; f++) {
|
|
1936
|
+
const fr = free[f];
|
|
1763
1937
|
if (fr.w < box.w || fr.h < box.h) continue;
|
|
1764
1938
|
held = true;
|
|
1765
1939
|
let firstOffPage = false;
|
|
@@ -1834,7 +2008,7 @@ function packOnePageByFootprint(
|
|
|
1834
2008
|
while (placed.length < shapes.length) placed.push(null);
|
|
1835
2009
|
return placed;
|
|
1836
2010
|
}
|
|
1837
|
-
|
|
2011
|
+
freeLength = splitFreeByCell(free, freeLength, put.x, put.y, shape.rects, minW, minH, edgeIndex, partition, tally);
|
|
1838
2012
|
}
|
|
1839
2013
|
return placed;
|
|
1840
2014
|
}
|
|
@@ -2087,8 +2261,9 @@ function layoutPack(
|
|
|
2087
2261
|
anchors: FootprintAnchors,
|
|
2088
2262
|
tally?: PassTally,
|
|
2089
2263
|
stopAtMiss = true,
|
|
2264
|
+
partition = true,
|
|
2090
2265
|
): PackLayout {
|
|
2091
|
-
const single = smallestPageFor(cells, maxEdge, pageEdges, shapes, anchors, tally, stopAtMiss);
|
|
2266
|
+
const single = smallestPageFor(cells, maxEdge, pageEdges, shapes, anchors, tally, stopAtMiss, partition);
|
|
2092
2267
|
|
|
2093
2268
|
/** page index -> the placements on it, in packing order. */
|
|
2094
2269
|
const perPage: Placement[][] = [];
|
|
@@ -2126,7 +2301,7 @@ function layoutPack(
|
|
|
2126
2301
|
maxEdge,
|
|
2127
2302
|
maxEdge,
|
|
2128
2303
|
Infinity,
|
|
2129
|
-
{ anchors, tally },
|
|
2304
|
+
{ anchors, tally, partition },
|
|
2130
2305
|
);
|
|
2131
2306
|
const onPage: typeof remaining = [];
|
|
2132
2307
|
const leftOver: typeof remaining = [];
|
|
@@ -2157,6 +2332,7 @@ function layoutPack(
|
|
|
2157
2332
|
anchors,
|
|
2158
2333
|
tally,
|
|
2159
2334
|
stopAtMiss,
|
|
2335
|
+
partition,
|
|
2160
2336
|
);
|
|
2161
2337
|
if (shrunk === null) {
|
|
2162
2338
|
// Unreachable for the same reason as the stall above: these cells were
|
|
@@ -2333,11 +2509,11 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
2333
2509
|
// `pot` on two 2048x2048 pages against `rect`'s 2048x2048 + 1024x2048.
|
|
2334
2510
|
// `footprintAnchors` names one rule alone instead (the instrument, the plant).
|
|
2335
2511
|
const candidates: Array<{ name: PackCandidate; layout: PackLayout }> = [];
|
|
2336
|
-
if (shapes === undefined) candidates.push({ name: 'rect', layout: layoutPack(sorted, cells, undefined, maxEdge, pageEdges, padding, 'box', opts.tally, opts.stopAtMiss ?? true) });
|
|
2337
|
-
else if (opts.footprintAnchors !== undefined) candidates.push({ name: opts.footprintAnchors, layout: layoutPack(sorted, cells, shapes, maxEdge, pageEdges, padding, opts.footprintAnchors, opts.tally, opts.stopAtMiss ?? true) });
|
|
2512
|
+
if (shapes === undefined) candidates.push({ name: 'rect', layout: layoutPack(sorted, cells, undefined, maxEdge, pageEdges, padding, 'box', opts.tally, opts.stopAtMiss ?? true, opts.partition ?? true) });
|
|
2513
|
+
else if (opts.footprintAnchors !== undefined) candidates.push({ name: opts.footprintAnchors, layout: layoutPack(sorted, cells, shapes, maxEdge, pageEdges, padding, opts.footprintAnchors, opts.tally, opts.stopAtMiss ?? true, opts.partition ?? true) });
|
|
2338
2514
|
else {
|
|
2339
|
-
candidates.push({ name: 'rect', layout: layoutPack(sorted, cells, undefined, maxEdge, pageEdges, padding, 'box', opts.tally, opts.stopAtMiss ?? true) });
|
|
2340
|
-
for (const rule of POLYGON_CANDIDATE_RULES) candidates.push({ name: rule, layout: layoutPack(sorted, cells, shapes, maxEdge, pageEdges, padding, rule, opts.tally, opts.stopAtMiss ?? true) });
|
|
2515
|
+
candidates.push({ name: 'rect', layout: layoutPack(sorted, cells, undefined, maxEdge, pageEdges, padding, 'box', opts.tally, opts.stopAtMiss ?? true, opts.partition ?? true) });
|
|
2516
|
+
for (const rule of POLYGON_CANDIDATE_RULES) candidates.push({ name: rule, layout: layoutPack(sorted, cells, shapes, maxEdge, pageEdges, padding, rule, opts.tally, opts.stopAtMiss ?? true, opts.partition ?? true) });
|
|
2341
2517
|
}
|
|
2342
2518
|
const areaOf = (layout: PackLayout): number => layout.pageSizes.reduce((n, p) => n + p.width * p.height, 0);
|
|
2343
2519
|
let chosen = candidates[0];
|
package/src/cli/core_commands.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
|
|
|
34
34
|
import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, type FrameSet, type FramesSidecar, framingViewport, GEOMETRY_FILE, geometryFileOf, geometryText, loadCandidate, POSER_NAMES, type PoserName, PROTOCOL_FPS, refuseUnchosen, renderFrame, sampleAll, sampleAnimation, SETUP_POSE_DIR, SHEET_FILE, SHEET_TILE, sidecarViewport, type SkeletonFacts, type SlotSubset, SlotSubsetError, throughPoser } from '../render_shared.ts';
|
|
35
35
|
import { type CompileResult } from '../types.ts';
|
|
36
36
|
import { attachmentRegionJoins } from '../region_joins.ts';
|
|
37
|
+
import { cmdRepack } from './repack.ts';
|
|
37
38
|
import { ATLAS_ABSENT, COMMANDS, type CommandRun, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
38
39
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
39
40
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
@@ -1299,6 +1300,7 @@ export function cmdIngest(flags: Record<string, string>, positional: string[]):
|
|
|
1299
1300
|
if (documentStage !== undefined) {
|
|
1300
1301
|
console.log(` .. stage ${documentStage === null ? 'none declared' : `${documentStage.width} x ${documentStage.height}`} — read from the ${MODEL_DOCUMENT_FILE} beside it (its spine.sha256 is this skeleton's); the header's box is the setup-pose bounding box`);
|
|
1301
1302
|
}
|
|
1303
|
+
if (flags['stage-box'] !== undefined) console.log(` .. stage read from the bounding box in slot "${flags['stage-box']}" (--stage-box); the header's box is the setup-pose bounding box`);
|
|
1302
1304
|
let result: { rig: unknown; motion: unknown; findings: IngestFinding[] };
|
|
1303
1305
|
try {
|
|
1304
1306
|
result = ingest(readJsonFile(skeletonPath), {
|
|
@@ -1307,6 +1309,7 @@ export function cmdIngest(flags: Record<string, string>, positional: string[]):
|
|
|
1307
1309
|
art,
|
|
1308
1310
|
images: specImages,
|
|
1309
1311
|
stage,
|
|
1312
|
+
...(flags['stage-box'] === undefined ? {} : { stageBox: flags['stage-box'] }),
|
|
1310
1313
|
source: basename(skeletonPath),
|
|
1311
1314
|
version: readVersion(),
|
|
1312
1315
|
});
|
|
@@ -1552,8 +1555,11 @@ export function cmdSkills(flags: Record<string, string>, positional: string[]):
|
|
|
1552
1555
|
* the pages where the compile measured them — the files the round trip's own
|
|
1553
1556
|
* A17 reads on `cli.ts` — and A18 compares the documents as written, the
|
|
1554
1557
|
* copies' names in them. Under `--pack` the two are one text.
|
|
1558
|
+
*
|
|
1559
|
+
* Exported for the selftest's `RPK` plants (issue #1169), which run `repack`
|
|
1560
|
+
* through this entry's dispatch with a hook between the lift and the build.
|
|
1555
1561
|
*/
|
|
1556
|
-
const MODEL_AND_TEXT_GATE: BuildGate = {
|
|
1562
|
+
export const MODEL_AND_TEXT_GATE: BuildGate = {
|
|
1557
1563
|
heading: (profile) =>
|
|
1558
1564
|
` .. validate (the model side over the document + the round trip's rules restated over the emitted text, profile ${profile}; this entry links no spine-core, so the round trip does not run)`,
|
|
1559
1565
|
run: ({ result, atlasText, atlasDir, modelText, reEmit, profile }) => {
|
|
@@ -1592,10 +1598,13 @@ const MODEL_AND_TEXT_GATE: BuildGate = {
|
|
|
1592
1598
|
* The bodies the second entry runs under a name whose full-entry body is the
|
|
1593
1599
|
* runtime's (`runtime.core` in `COMMANDS`, issue #1060) — registered by
|
|
1594
1600
|
* `cli_core.ts` alone, beside `CORE_COMMAND_RUNS`. `build` is `runBuild` with
|
|
1595
|
-
* this entry's gate: it writes what `cli.ts build` writes.
|
|
1601
|
+
* this entry's gate: it writes what `cli.ts build` writes. `repack` (issue
|
|
1602
|
+
* #1169) is `./repack.ts`'s body with the same gate, for the same reason: it
|
|
1603
|
+
* is `build --pack` run over a build's own lifted regions.
|
|
1596
1604
|
*/
|
|
1597
1605
|
export const CORE_ENTRY_RUNS: Readonly<Record<string, CommandRun>> = {
|
|
1598
1606
|
build: ({ flags }) => runBuild(flags, MODEL_AND_TEXT_GATE),
|
|
1607
|
+
repack: (args) => cmdRepack(args, MODEL_AND_TEXT_GATE),
|
|
1599
1608
|
};
|
|
1600
1609
|
|
|
1601
1610
|
/**
|