spine-rigc 2.11.0 → 2.11.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/README.md +1 -1
- package/docs/AUTHORING.md +14 -0
- package/package.json +1 -1
- package/src/atlas.ts +253 -82
package/README.md
CHANGED
|
@@ -554,7 +554,7 @@ commands take it and what its default is.
|
|
|
554
554
|
| Command | Does |
|
|
555
555
|
| --- | --- |
|
|
556
556
|
| `build --rig … --motion … --out …` | compiles, gates, and **writes only if the gate is green**. `--images <dir>` says where the rig spec's `image` names resolve, `--manifest` adds measured art, and `--copy-images` copies every page PNG into `--out` so the directory is self-contained, with `skeleton.images` pointing at it so the editor's import finds the parts |
|
|
557
|
-
| `build … --pack` | the same build with every part arranged onto **shared** atlas pages, written into `--out` — losslessly, and gated a second time as the pair that ships. `--page-size`, `--padding`, `--page-edges` and `--pack-shape` tune it — `--pack-shape polygon` lets a neighbour sit inside a mesh's rectangle where its hull is not |
|
|
557
|
+
| `build … --pack` | the same build with every part arranged onto **shared** atlas pages, written into `--out` — losslessly, and gated a second time as the pair that ships. `--page-size`, `--padding`, `--page-edges` and `--pack-shape` tune it — `--pack-shape polygon` lets a neighbour sit inside a mesh's rectangle where its hull is not, and with `--page-edges free` costs 12–56 times `rect`'s pack (3–11 s against 0.2–0.3 s on 90–144 parts over two to four pages; past 5 s from about 60 parts, a second or so below 45 on one page — AUTHORING §0.1) |
|
|
558
558
|
| `build … --atlas-in <file.atlas>` | the same build with every part resolved to a **region of an existing pack** instead of a loose PNG; a name the atlas lacks, a size the spec disagrees with or a rectangle off its page is refused by name |
|
|
559
559
|
| `validate <dir>` | re-gates artifacts already on disk |
|
|
560
560
|
| `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all |
|
package/docs/AUTHORING.md
CHANGED
|
@@ -554,6 +554,20 @@ what changes is that two rectangles may overlap where neither footprint is.
|
|
|
554
554
|
1107, which `PK93` holds within 25 times. The cost is the placing the
|
|
555
555
|
second anchor makes possible: before #1102 the same search threw its passes
|
|
556
556
|
away and took 260–304 s on the production set.
|
|
557
|
+
- **Where the cost starts** (issue #1165). Under `free` the page search runs
|
|
558
|
+
a footprint pass at every width below the page it settles on and throws each
|
|
559
|
+
away when a part misses, so the cost follows how full a page is more than how
|
|
560
|
+
many parts there are; `build --pack` packs twice (the second pack is
|
|
561
|
+
`A18`'s). On three production rigs of 90–144 parts over two to four pages the
|
|
562
|
+
free polygon pack alone takes 3.4–10.6 s against `rect`'s 0.17–0.33 s (12 to
|
|
563
|
+
56 times; 10.7–41.8 s before #1165's free-list partition, the same pages),
|
|
564
|
+
and 0.4–0.6 s under `--page-edges pot`. On a generated set fitted to those
|
|
565
|
+
rigs (three seeds a size, 20 to 150 parts) it is past twice `rect` at every
|
|
566
|
+
size, 20 parts on one page included — `rect` there takes under 0.2 s and the
|
|
567
|
+
polygon pack 0.5–1 s — and its median passes 5 s at 60 parts (45 before),
|
|
568
|
+
worst 14.4 s at 75 parts over two pages. So below about 45 parts on one page
|
|
569
|
+
`polygon` with `free` costs a build a second or a few; past that, pick
|
|
570
|
+
`--page-edges pot` or `--pack-shape rect` where build time matters.
|
|
557
571
|
- **The pixels.** Every cell is drawn whole in packing order, then every
|
|
558
572
|
region's owned texels are drawn again with its own values, so every texel a
|
|
559
573
|
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.11.
|
|
3
|
+
"version": "2.11.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
|
@@ -803,6 +803,13 @@ export interface PackOptions {
|
|
|
803
803
|
* the same either way (`PK94`); only the cost differs.
|
|
804
804
|
*/
|
|
805
805
|
stopAtMiss?: boolean;
|
|
806
|
+
/**
|
|
807
|
+
* `false` splits every band of every cell a footprint pass places over the
|
|
808
|
+
* whole free list — issue #1165's partition off (`splitFreeByCell`), for the
|
|
809
|
+
* selftest's plant (`PK106`) only. The pages are the same either way; only
|
|
810
|
+
* the cost differs.
|
|
811
|
+
*/
|
|
812
|
+
partition?: boolean;
|
|
806
813
|
}
|
|
807
814
|
|
|
808
815
|
/** Where one region landed. `x`/`y` are the REGION's own corner, not its cell's. */
|
|
@@ -884,15 +891,16 @@ function smallestPageFor(
|
|
|
884
891
|
anchors: FootprintAnchors = DEFAULT_FOOTPRINT_ANCHORS,
|
|
885
892
|
tally?: PassTally,
|
|
886
893
|
stopAtMiss = true,
|
|
894
|
+
partition = true,
|
|
887
895
|
): { width: number; height: number; rects: Rect[] } | null {
|
|
888
|
-
if (pageEdges === 'free') return smallestFreePageFor(cells, maxEdge, shapes, anchors, tally, stopAtMiss);
|
|
896
|
+
if (pageEdges === 'free') return smallestFreePageFor(cells, maxEdge, shapes, anchors, tally, stopAtMiss, partition);
|
|
889
897
|
const edges: number[] = [];
|
|
890
898
|
for (let e = 1; e <= maxEdge; e *= 2) edges.push(e);
|
|
891
899
|
const candidates: Array<{ w: number; h: number }> = [];
|
|
892
900
|
for (const w of edges) for (const h of edges) candidates.push({ w, h });
|
|
893
901
|
candidates.sort((a, b) => a.w * a.h - b.w * b.h || a.w - b.w);
|
|
894
902
|
for (const candidate of candidates) {
|
|
895
|
-
const attempt = placePass(cells, shapes, candidate.w, candidate.h, Infinity, { wholeOrNothing: stopAtMiss, anchors, tally });
|
|
903
|
+
const attempt = placePass(cells, shapes, candidate.w, candidate.h, Infinity, { wholeOrNothing: stopAtMiss, anchors, tally, partition });
|
|
896
904
|
if (attempt.some((r) => r === null)) continue;
|
|
897
905
|
return { width: candidate.w, height: candidate.h, rects: attempt as Rect[] };
|
|
898
906
|
}
|
|
@@ -947,8 +955,9 @@ function smallestFreePageFor(
|
|
|
947
955
|
anchors: FootprintAnchors = DEFAULT_FOOTPRINT_ANCHORS,
|
|
948
956
|
tally?: PassTally,
|
|
949
957
|
stopAtMiss = true,
|
|
958
|
+
partition = true,
|
|
950
959
|
): { width: number; height: number; rects: Rect[] } | null {
|
|
951
|
-
const search = freePageSearch(cells, maxEdge, shapes, { anchors, stopAtMiss });
|
|
960
|
+
const search = freePageSearch(cells, maxEdge, shapes, { anchors, stopAtMiss, partition });
|
|
952
961
|
if (tally !== undefined) for (const key of Object.keys(tally) as Array<keyof PassTally>) tally[key] += search.tally[key];
|
|
953
962
|
return search.page;
|
|
954
963
|
}
|
|
@@ -970,15 +979,19 @@ export interface FreePageSearch {
|
|
|
970
979
|
|
|
971
980
|
/**
|
|
972
981
|
* 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
|
-
*
|
|
982
|
+
* (`PK94`, `PK95`, `PK106`). Each turns one saving off: `stopAtMiss: false`
|
|
983
|
+
* runs every footprint pass to its end and `edgeIndex: false` splits and
|
|
984
|
+
* prunes the whole free list by scanning all of it (issue #1102's two);
|
|
985
|
+
* `partition: false` splits every band over the whole list but keeps the edge
|
|
986
|
+
* index (issue #1165's). The page is the same either way; only what it costs
|
|
987
|
+
* differs, and the plants hold that.
|
|
977
988
|
*/
|
|
978
989
|
export interface FreePageSearchOptions {
|
|
979
990
|
stopAtMiss?: boolean;
|
|
980
|
-
/** `false`
|
|
991
|
+
/** `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
992
|
edgeIndex?: boolean;
|
|
993
|
+
/** `false` splits every band over the whole free list (`splitFreeByCell`), as before issue #1165 — the plant of `PK106`. */
|
|
994
|
+
partition?: boolean;
|
|
982
995
|
/** Where a candidate may anchor a cell (`FOOTPRINT_ANCHORS`) — `tools/pack_anchor.ts`'s candidate rules; default `box`. */
|
|
983
996
|
anchors?: FootprintAnchors;
|
|
984
997
|
}
|
|
@@ -1023,7 +1036,7 @@ export function freePageSearch(
|
|
|
1023
1036
|
continue;
|
|
1024
1037
|
}
|
|
1025
1038
|
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 });
|
|
1039
|
+
const attempt = placePass(cells, shapes, width, maxEdge, maxBottom, { wholeOrNothing, tally: out.tally, edgeIndex: options.edgeIndex ?? true, anchors: options.anchors, partition: options.partition ?? true });
|
|
1027
1040
|
let height = 0;
|
|
1028
1041
|
for (const r of attempt) if (r !== null) height = Math.max(height, r.y + r.h);
|
|
1029
1042
|
if (height > maxBottom) {
|
|
@@ -1231,13 +1244,13 @@ function placePass(
|
|
|
1231
1244
|
// greedy pass that places one cell differently places every later one
|
|
1232
1245
|
// differently too.
|
|
1233
1246
|
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);
|
|
1247
|
+
let byFootprint = packOnePageByFootprint(shapes, pageW, pageH, maxBottom, wholeOrNothing, pass.edgeIndex ?? true, tally, anchors === 'best-of' ? 'box' : anchors, pass.partition ?? true);
|
|
1235
1248
|
if (anchors === 'best-of') {
|
|
1236
1249
|
// Issue #1104's fourth candidate: both searches on the same page, the
|
|
1237
1250
|
// better kept and the first anchor's on a tie — so a pass is never worse
|
|
1238
1251
|
// than the first anchor's on that page.
|
|
1239
1252
|
if (tally !== undefined) tally.footprintPlaced += placedCount(byFootprint);
|
|
1240
|
-
const byTwo = packOnePageByFootprint(shapes, pageW, pageH, maxBottom, wholeOrNothing, pass.edgeIndex ?? true, tally, 'box-or-cell');
|
|
1253
|
+
const byTwo = packOnePageByFootprint(shapes, pageW, pageH, maxBottom, wholeOrNothing, pass.edgeIndex ?? true, tally, 'box-or-cell', pass.partition ?? true);
|
|
1241
1254
|
const bottom = (placed: Array<Rect | null>): number => placed.reduce((n, r) => (r === null ? n : Math.max(n, r.y + r.h)), 0);
|
|
1242
1255
|
const placedBox = placedCount(byFootprint);
|
|
1243
1256
|
const placedTwo = placedCount(byTwo);
|
|
@@ -1262,6 +1275,8 @@ interface PassOptions {
|
|
|
1262
1275
|
edgeIndex?: boolean;
|
|
1263
1276
|
/** Where a candidate may anchor a cell (`FOOTPRINT_ANCHORS`). */
|
|
1264
1277
|
anchors?: FootprintAnchors;
|
|
1278
|
+
/** `splitFreeByCell`'s partition (default on); off only for the selftest's plant. */
|
|
1279
|
+
partition?: boolean;
|
|
1265
1280
|
}
|
|
1266
1281
|
|
|
1267
1282
|
/** How many cells a pass placed. */
|
|
@@ -1292,6 +1307,12 @@ export interface PassTally {
|
|
|
1292
1307
|
bandSplits: number;
|
|
1293
1308
|
/** Containment tests the footprint passes' prune made (`splitFree`). */
|
|
1294
1309
|
containmentTests: number;
|
|
1310
|
+
/**
|
|
1311
|
+
* Free-list entries the footprint passes' splits read: the list's length at
|
|
1312
|
+
* every band split, and once per placed cell the whole list the partition
|
|
1313
|
+
* reads (`splitFreeByCell`) — the work issue #1165 measured, held by `PK106`.
|
|
1314
|
+
*/
|
|
1315
|
+
entriesRead: number;
|
|
1295
1316
|
/**
|
|
1296
1317
|
* Free rectangles that held a cell's owned box and were refused as its
|
|
1297
1318
|
* first-anchor candidate only because the cell, anchored there, would leave
|
|
@@ -1307,7 +1328,7 @@ export interface PassTally {
|
|
|
1307
1328
|
|
|
1308
1329
|
/** A tally with nothing counted. */
|
|
1309
1330
|
export function emptyTally(): PassTally {
|
|
1310
|
-
return { rectPlaced: 0, footprintPlaced: 0, bandSplits: 0, containmentTests: 0, boxAnchorRefused: 0, missedByAnchor: 0, secondAnchorPlaced: 0 };
|
|
1331
|
+
return { rectPlaced: 0, footprintPlaced: 0, bandSplits: 0, containmentTests: 0, entriesRead: 0, boxAnchorRefused: 0, missedByAnchor: 0, secondAnchorPlaced: 0 };
|
|
1311
1332
|
}
|
|
1312
1333
|
|
|
1313
1334
|
// ---------------------------------------------------------------------------
|
|
@@ -1551,57 +1572,81 @@ function shapesMeet(a: CellShape, ax: number, ay: number, b: CellShape, bx: numb
|
|
|
1551
1572
|
|
|
1552
1573
|
/**
|
|
1553
1574
|
* 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
|
-
*
|
|
1575
|
+
* `packOnePage`'s, over the first `length` entries of `free`, written back in
|
|
1576
|
+
* place; returns the new length. Four savings, none of which changes a
|
|
1577
|
+
* rectangle or the list's order:
|
|
1578
|
+
*
|
|
1579
|
+
* * a free rectangle the placement did not touch is never tested for
|
|
1580
|
+
* containment. Before the split no free rectangle lay inside another (the
|
|
1581
|
+
* prune's own postcondition), and every new piece lies inside the
|
|
1582
|
+
* rectangle it was cut from, so an untouched rectangle inside a new piece
|
|
1583
|
+
* would have lain inside that one — the test `packOnePage` makes for it
|
|
1584
|
+
* always answers "not contained";
|
|
1585
|
+
* * ⭐ (issue #1099, measured on production rigs) a free rectangle narrower
|
|
1586
|
+
* than `minW` or shorter than `minH` is dropped. The caller passes the
|
|
1587
|
+
* least bounding box of every cell the pass places, so such a rectangle can
|
|
1588
|
+
* never be a candidate, and neither can any piece later cut from it (a
|
|
1589
|
+
* piece lies inside the rectangle it was cut from); and a rectangle a
|
|
1590
|
+
* dropped one contains is itself too small. So every rectangle that could
|
|
1591
|
+
* ever be a candidate is kept, in the order the full prune keeps it. What
|
|
1592
|
+
* it removes is the staircase of slivers an irregular hull's bands cut
|
|
1593
|
+
* along its edge: on a production-shaped set (30 regions, 200 to 900 px,
|
|
1594
|
+
* hulls near their rectangles, a `free` page about 2023x2046) the pack took
|
|
1595
|
+
* 18.5 s with the slivers kept and spent 95 % of it in this prune. A piece
|
|
1596
|
+
* that would be dropped is not made at all (issue #1165): it takes part in
|
|
1597
|
+
* no test and is not written back, so it changes no other entry's index
|
|
1598
|
+
* order either;
|
|
1599
|
+
* * the edge index (issue #1102), below;
|
|
1600
|
+
* * (issue #1165) the rebuilt list, each entry's side and the edge lists are
|
|
1601
|
+
* kept between calls (`splitNext` and its neighbours) and the list is
|
|
1602
|
+
* written back over itself by index, never emptied and refilled: on the
|
|
1603
|
+
* production rigs the emptying and the pushes that refilled it were most
|
|
1604
|
+
* of this function's time, and none of it was arithmetic.
|
|
1573
1605
|
*/
|
|
1574
|
-
function splitFree(free: Rect[], put: Rect, minW
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1606
|
+
function splitFree(free: Rect[], length: number, put: Rect, minW: number, minH: number, edgeIndex: boolean, tally?: PassTally): number {
|
|
1607
|
+
const px0 = put.x;
|
|
1608
|
+
const py0 = put.y;
|
|
1609
|
+
const px1 = put.x + put.w;
|
|
1610
|
+
const py1 = put.y + put.h;
|
|
1611
|
+
// A touched entry yields at most four pieces.
|
|
1612
|
+
if (splitSide.length < length * 4) {
|
|
1613
|
+
const capacity = Math.max(64, length * 8);
|
|
1614
|
+
splitSide = new Int8Array(capacity);
|
|
1615
|
+
splitLeft = new Int32Array(capacity);
|
|
1616
|
+
splitRight = new Int32Array(capacity);
|
|
1617
|
+
splitAbove = new Int32Array(capacity);
|
|
1618
|
+
splitBelow = new Int32Array(capacity);
|
|
1619
|
+
}
|
|
1620
|
+
const next = splitNext;
|
|
1621
|
+
const side = splitSide;
|
|
1622
|
+
let n = 0;
|
|
1623
|
+
for (let k = 0; k < length; k++) {
|
|
1624
|
+
const fr = free[k];
|
|
1625
|
+
const fx1 = fr.x + fr.w;
|
|
1626
|
+
const fy1 = fr.y + fr.h;
|
|
1627
|
+
if (!(px0 < fx1 && px1 > fr.x && py0 < fy1 && py1 > fr.y)) {
|
|
1628
|
+
next[n] = fr;
|
|
1629
|
+
side[n++] = UNTOUCHED;
|
|
1583
1630
|
continue;
|
|
1584
1631
|
}
|
|
1585
|
-
if (
|
|
1586
|
-
next
|
|
1587
|
-
side
|
|
1632
|
+
if (px0 > fr.x && px0 - fr.x >= minW && fr.h >= minH) {
|
|
1633
|
+
next[n] = { x: fr.x, y: fr.y, w: px0 - fr.x, h: fr.h };
|
|
1634
|
+
side[n++] = LEFT_OF_PUT;
|
|
1588
1635
|
}
|
|
1589
|
-
if (
|
|
1590
|
-
next
|
|
1591
|
-
side
|
|
1636
|
+
if (px1 < fx1 && fx1 - px1 >= minW && fr.h >= minH) {
|
|
1637
|
+
next[n] = { x: px1, y: fr.y, w: fx1 - px1, h: fr.h };
|
|
1638
|
+
side[n++] = RIGHT_OF_PUT;
|
|
1592
1639
|
}
|
|
1593
|
-
if (
|
|
1594
|
-
next
|
|
1595
|
-
side
|
|
1640
|
+
if (py0 > fr.y && fr.w >= minW && py0 - fr.y >= minH) {
|
|
1641
|
+
next[n] = { x: fr.x, y: fr.y, w: fr.w, h: py0 - fr.y };
|
|
1642
|
+
side[n++] = ABOVE_PUT;
|
|
1596
1643
|
}
|
|
1597
|
-
if (
|
|
1598
|
-
next
|
|
1599
|
-
side
|
|
1644
|
+
if (py1 < fy1 && fr.w >= minW && fy1 - py1 >= minH) {
|
|
1645
|
+
next[n] = { x: fr.x, y: py1, w: fr.w, h: fy1 - py1 };
|
|
1646
|
+
side[n++] = BELOW_PUT;
|
|
1600
1647
|
}
|
|
1601
1648
|
}
|
|
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
|
|
1649
|
+
// ⭐ The edge index (issue #1102): a piece's containers are looked for only
|
|
1605
1650
|
// among the entries that share the edge it was cut along. A piece cut from
|
|
1606
1651
|
// the left of `put` ends at `put.x` and keeps its parent's rows, which meet
|
|
1607
1652
|
// `put`'s rows (the parent overlapped `put`). A rectangle containing it covers
|
|
@@ -1617,45 +1662,165 @@ function splitFree(free: Rect[], put: Rect, minW = 1, minH = 1, edgeIndex = true
|
|
|
1617
1662
|
// `polygon` pack of it runs): 415,916,651 containment tests by the full scan,
|
|
1618
1663
|
// 15,407,179 by the index, 3.2 s → 2.3 s; with the early stop off as well,
|
|
1619
1664
|
// 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
|
-
|
|
1665
|
+
// that scan — every usable entry in `left` — kept so the selftest can hold
|
|
1666
|
+
// that the index finds what the scan finds (`PK94`) and for nothing else.
|
|
1667
|
+
const left = splitLeft;
|
|
1668
|
+
const right = splitRight;
|
|
1669
|
+
const above = splitAbove;
|
|
1670
|
+
const below = splitBelow;
|
|
1671
|
+
let nLeft = 0;
|
|
1672
|
+
let nRight = 0;
|
|
1673
|
+
let nAbove = 0;
|
|
1674
|
+
let nBelow = 0;
|
|
1675
|
+
for (let j = 0; j < n; j++) {
|
|
1625
1676
|
const r = next[j];
|
|
1626
|
-
if (
|
|
1677
|
+
if (r.w < minW || r.h < minH) continue;
|
|
1627
1678
|
if (!edgeIndex) {
|
|
1628
|
-
|
|
1679
|
+
left[nLeft++] = j;
|
|
1629
1680
|
continue;
|
|
1630
1681
|
}
|
|
1631
|
-
if (r.x + r.w ===
|
|
1632
|
-
if (r.x ===
|
|
1633
|
-
if (r.y + r.h ===
|
|
1634
|
-
if (r.y ===
|
|
1682
|
+
if (r.x + r.w === px0) left[nLeft++] = j;
|
|
1683
|
+
if (r.x === px1) right[nRight++] = j;
|
|
1684
|
+
if (r.y + r.h === py0) above[nAbove++] = j;
|
|
1685
|
+
if (r.y === py1) below[nBelow++] = j;
|
|
1635
1686
|
}
|
|
1636
|
-
|
|
1687
|
+
let w = 0;
|
|
1637
1688
|
let tests = 0;
|
|
1638
|
-
for (let i = 0; i <
|
|
1639
|
-
|
|
1640
|
-
if (
|
|
1641
|
-
|
|
1689
|
+
for (let i = 0; i < n; i++) {
|
|
1690
|
+
const piece = next[i];
|
|
1691
|
+
if (piece.w < minW || piece.h < minH) continue;
|
|
1692
|
+
const cut = side[i];
|
|
1693
|
+
if (cut === UNTOUCHED) {
|
|
1694
|
+
free[w++] = piece;
|
|
1642
1695
|
continue;
|
|
1643
1696
|
}
|
|
1697
|
+
const list = !edgeIndex || cut === LEFT_OF_PUT ? left : cut === RIGHT_OF_PUT ? right : cut === ABOVE_PUT ? above : below;
|
|
1698
|
+
const count = !edgeIndex || cut === LEFT_OF_PUT ? nLeft : cut === RIGHT_OF_PUT ? nRight : cut === ABOVE_PUT ? nAbove : nBelow;
|
|
1644
1699
|
let contained = false;
|
|
1645
|
-
for (
|
|
1700
|
+
for (let t = 0; t < count; t++) {
|
|
1701
|
+
const j = list[t];
|
|
1646
1702
|
if (i === j) continue;
|
|
1647
1703
|
tests++;
|
|
1648
|
-
|
|
1704
|
+
const other = next[j];
|
|
1705
|
+
// On a mutual containment (two identical rectangles) the later index
|
|
1706
|
+
// loses, so exactly one survives and it is always the same one.
|
|
1707
|
+
if (rectContains(other, piece) && (j < i || !rectContains(piece, other))) {
|
|
1649
1708
|
contained = true;
|
|
1650
1709
|
break;
|
|
1651
1710
|
}
|
|
1652
1711
|
}
|
|
1653
|
-
if (!contained) free
|
|
1712
|
+
if (!contained) free[w++] = piece;
|
|
1654
1713
|
}
|
|
1655
1714
|
if (tally !== undefined) {
|
|
1656
1715
|
tally.bandSplits++;
|
|
1657
1716
|
tally.containmentTests += tests;
|
|
1717
|
+
tally.entriesRead += length;
|
|
1658
1718
|
}
|
|
1719
|
+
return w;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/** Whether `a` contains `b`. */
|
|
1723
|
+
function rectContains(a: Rect, b: Rect): boolean {
|
|
1724
|
+
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;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* `splitFree`'s working storage, kept between calls so a split allocates
|
|
1729
|
+
* nothing but the pieces it cuts (issue #1165). `splitFree` is never
|
|
1730
|
+
* re-entered and reads no entry past the `n` it wrote this call, so what an
|
|
1731
|
+
* earlier call left behind is never read.
|
|
1732
|
+
*/
|
|
1733
|
+
const splitNext: Rect[] = [];
|
|
1734
|
+
let splitSide = new Int8Array(0);
|
|
1735
|
+
let splitLeft = new Int32Array(0);
|
|
1736
|
+
let splitRight = new Int32Array(0);
|
|
1737
|
+
let splitAbove = new Int32Array(0);
|
|
1738
|
+
let splitBelow = new Int32Array(0);
|
|
1739
|
+
/** `splitFreeByCell`'s near list, kept between calls for the same reason. */
|
|
1740
|
+
const splitNear: Rect[] = [];
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Every band of one placed cell split out of the first `length` entries of
|
|
1744
|
+
* `free` (`splitFree`, once per band, top to bottom); returns the new length.
|
|
1745
|
+
*
|
|
1746
|
+
* ⭐ **The list is split near the cell only** (issue #1165). Before a cell's
|
|
1747
|
+
* bands are split, the list is parted once into the entries that touch the
|
|
1748
|
+
* bands' bounding box — overlapping it or sharing an edge with it — and the
|
|
1749
|
+
* rest; every band is then split over the near part alone, and the far part is
|
|
1750
|
+
* written back first, the near part after it. Nothing the far part holds could
|
|
1751
|
+
* have been read:
|
|
1752
|
+
*
|
|
1753
|
+
* * a far entry does not overlap any band (every band lies inside the box),
|
|
1754
|
+
* so every split leaves it untouched — kept as it is, never tested;
|
|
1755
|
+
* * a far entry contains no piece any band cuts. A piece cut from the left of
|
|
1756
|
+
* a band ends at the band's left edge `x` and keeps a row `y` of the band
|
|
1757
|
+
* (its parent overlapped the band), so a container of it holds the texel
|
|
1758
|
+
* `(x − 1, y)` — inside the box or against its left edge, which an entry
|
|
1759
|
+
* that neither overlaps the box nor shares an edge with it cannot hold.
|
|
1760
|
+
* Likewise for the other three sides. And every piece is cut from a near
|
|
1761
|
+
* entry, so it is in the near part for the next band.
|
|
1762
|
+
*
|
|
1763
|
+
* So every split makes the decisions over the near part that it makes over the
|
|
1764
|
+
* whole list, and the far part comes through every split unchanged. ⚠️ What
|
|
1765
|
+
* changes is the ORDER of the list: the far entries move ahead of the near ones.
|
|
1766
|
+
* That decides nothing, because nothing that reads the list reads its order:
|
|
1767
|
+
*
|
|
1768
|
+
* * a split keeps every untouched entry, and keeps a piece unless another
|
|
1769
|
+
* usable entry contains it, the later index losing only between two equal
|
|
1770
|
+
* rectangles — and which of two equal rectangles survives is invisible,
|
|
1771
|
+
* they are the same rectangle — so the SET of rectangles after a split is
|
|
1772
|
+
* a function of the set before it;
|
|
1773
|
+
* * a candidate is chosen by a total order on its score, its free
|
|
1774
|
+
* rectangle's corner and its anchor (`packOnePageByFootprint`); two free
|
|
1775
|
+
* rectangles that tie on all of those put the cell at the same place.
|
|
1776
|
+
*
|
|
1777
|
+
* So the placements are the ones the whole-list split makes, decision for
|
|
1778
|
+
* decision. What it saves: on three production rigs the near part is 10 to 22 %
|
|
1779
|
+
* of the list (about 90 entries of 940 on the slowest), and each of a cell's
|
|
1780
|
+
* 50 or so bands read the whole list. `partition: false` splits every band over
|
|
1781
|
+
* the whole list in order, as before — kept so the selftest can plant the
|
|
1782
|
+
* work back (`PK106`) and hold that the pages do not move (`PK94`, through
|
|
1783
|
+
* `edgeIndex: false`, which splits the whole list too).
|
|
1784
|
+
*/
|
|
1785
|
+
function splitFreeByCell(
|
|
1786
|
+
free: Rect[],
|
|
1787
|
+
length: number,
|
|
1788
|
+
cellX: number,
|
|
1789
|
+
cellY: number,
|
|
1790
|
+
rects: readonly Rect[],
|
|
1791
|
+
minW: number,
|
|
1792
|
+
minH: number,
|
|
1793
|
+
edgeIndex: boolean,
|
|
1794
|
+
partition: boolean,
|
|
1795
|
+
tally?: PassTally,
|
|
1796
|
+
): number {
|
|
1797
|
+
if (!edgeIndex || !partition) {
|
|
1798
|
+
let n = length;
|
|
1799
|
+
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);
|
|
1800
|
+
return n;
|
|
1801
|
+
}
|
|
1802
|
+
let x0 = Infinity;
|
|
1803
|
+
let y0 = Infinity;
|
|
1804
|
+
let x1 = -Infinity;
|
|
1805
|
+
let y1 = -Infinity;
|
|
1806
|
+
for (const r of rects) {
|
|
1807
|
+
x0 = Math.min(x0, cellX + r.x);
|
|
1808
|
+
y0 = Math.min(y0, cellY + r.y);
|
|
1809
|
+
x1 = Math.max(x1, cellX + r.x + r.w);
|
|
1810
|
+
y1 = Math.max(y1, cellY + r.y + r.h);
|
|
1811
|
+
}
|
|
1812
|
+
const near = splitNear;
|
|
1813
|
+
let nNear = 0;
|
|
1814
|
+
let nFar = 0;
|
|
1815
|
+
for (let k = 0; k < length; k++) {
|
|
1816
|
+
const fr = free[k];
|
|
1817
|
+
if (fr.x <= x1 && fr.x + fr.w >= x0 && fr.y <= y1 && fr.y + fr.h >= y0) near[nNear++] = fr;
|
|
1818
|
+
else free[nFar++] = fr;
|
|
1819
|
+
}
|
|
1820
|
+
if (tally !== undefined) tally.entriesRead += length;
|
|
1821
|
+
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);
|
|
1822
|
+
for (let k = 0; k < nNear; k++) free[nFar + k] = near[k];
|
|
1823
|
+
return nFar + nNear;
|
|
1659
1824
|
}
|
|
1660
1825
|
|
|
1661
1826
|
/** Which side of the placed rectangle a free-list piece was cut from (`splitFree`) — an index into its edge lists. */
|
|
@@ -1738,8 +1903,11 @@ function packOnePageByFootprint(
|
|
|
1738
1903
|
edgeIndex = true,
|
|
1739
1904
|
tally?: PassTally,
|
|
1740
1905
|
anchors: FootprintAnchors = DEFAULT_FOOTPRINT_ANCHORS,
|
|
1906
|
+
partition = true,
|
|
1741
1907
|
): Array<Rect | null> {
|
|
1908
|
+
// The free list is `free`'s first `freeLength` entries (`splitFree`).
|
|
1742
1909
|
const free: Rect[] = [{ x: 0, y: 0, w: pageW, h: pageH }];
|
|
1910
|
+
let freeLength = 1;
|
|
1743
1911
|
const placed: Array<Rect | null> = [];
|
|
1744
1912
|
const owned: Array<{ shape: CellShape; x: number; y: number }> = [];
|
|
1745
1913
|
// The least box any cell of this pass needs — what a free rectangle must
|
|
@@ -1759,7 +1927,8 @@ function packOnePageByFootprint(
|
|
|
1759
1927
|
let held = false;
|
|
1760
1928
|
// The second anchor differs from the first only when the box is inset.
|
|
1761
1929
|
const anchorCount = anchors !== 'box' && (box.x !== 0 || box.y !== 0) ? 2 : 1;
|
|
1762
|
-
for (
|
|
1930
|
+
for (let f = 0; f < freeLength; f++) {
|
|
1931
|
+
const fr = free[f];
|
|
1763
1932
|
if (fr.w < box.w || fr.h < box.h) continue;
|
|
1764
1933
|
held = true;
|
|
1765
1934
|
let firstOffPage = false;
|
|
@@ -1834,7 +2003,7 @@ function packOnePageByFootprint(
|
|
|
1834
2003
|
while (placed.length < shapes.length) placed.push(null);
|
|
1835
2004
|
return placed;
|
|
1836
2005
|
}
|
|
1837
|
-
|
|
2006
|
+
freeLength = splitFreeByCell(free, freeLength, put.x, put.y, shape.rects, minW, minH, edgeIndex, partition, tally);
|
|
1838
2007
|
}
|
|
1839
2008
|
return placed;
|
|
1840
2009
|
}
|
|
@@ -2087,8 +2256,9 @@ function layoutPack(
|
|
|
2087
2256
|
anchors: FootprintAnchors,
|
|
2088
2257
|
tally?: PassTally,
|
|
2089
2258
|
stopAtMiss = true,
|
|
2259
|
+
partition = true,
|
|
2090
2260
|
): PackLayout {
|
|
2091
|
-
const single = smallestPageFor(cells, maxEdge, pageEdges, shapes, anchors, tally, stopAtMiss);
|
|
2261
|
+
const single = smallestPageFor(cells, maxEdge, pageEdges, shapes, anchors, tally, stopAtMiss, partition);
|
|
2092
2262
|
|
|
2093
2263
|
/** page index -> the placements on it, in packing order. */
|
|
2094
2264
|
const perPage: Placement[][] = [];
|
|
@@ -2126,7 +2296,7 @@ function layoutPack(
|
|
|
2126
2296
|
maxEdge,
|
|
2127
2297
|
maxEdge,
|
|
2128
2298
|
Infinity,
|
|
2129
|
-
{ anchors, tally },
|
|
2299
|
+
{ anchors, tally, partition },
|
|
2130
2300
|
);
|
|
2131
2301
|
const onPage: typeof remaining = [];
|
|
2132
2302
|
const leftOver: typeof remaining = [];
|
|
@@ -2157,6 +2327,7 @@ function layoutPack(
|
|
|
2157
2327
|
anchors,
|
|
2158
2328
|
tally,
|
|
2159
2329
|
stopAtMiss,
|
|
2330
|
+
partition,
|
|
2160
2331
|
);
|
|
2161
2332
|
if (shrunk === null) {
|
|
2162
2333
|
// Unreachable for the same reason as the stall above: these cells were
|
|
@@ -2333,11 +2504,11 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
2333
2504
|
// `pot` on two 2048x2048 pages against `rect`'s 2048x2048 + 1024x2048.
|
|
2334
2505
|
// `footprintAnchors` names one rule alone instead (the instrument, the plant).
|
|
2335
2506
|
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) });
|
|
2507
|
+
if (shapes === undefined) candidates.push({ name: 'rect', layout: layoutPack(sorted, cells, undefined, maxEdge, pageEdges, padding, 'box', opts.tally, opts.stopAtMiss ?? true, opts.partition ?? true) });
|
|
2508
|
+
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
2509
|
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) });
|
|
2510
|
+
candidates.push({ name: 'rect', layout: layoutPack(sorted, cells, undefined, maxEdge, pageEdges, padding, 'box', opts.tally, opts.stopAtMiss ?? true, opts.partition ?? true) });
|
|
2511
|
+
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
2512
|
}
|
|
2342
2513
|
const areaOf = (layout: PackLayout): number => layout.pageSizes.reduce((n, p) => n + p.width * p.height, 0);
|
|
2343
2514
|
let chosen = candidates[0];
|