spine-rigc 1.3.0 โ 1.5.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/README.md +1 -1
- package/cli.ts +66 -16
- package/docs/AUTHORING.md +293 -30
- package/package.json +1 -1
- package/src/atlas.ts +173 -21
- package/src/check.ts +22 -0
- package/src/pose.ts +314 -18
- package/src/render.ts +515 -22
package/src/atlas.ts
CHANGED
|
@@ -518,6 +518,43 @@ export interface EmitPage {
|
|
|
518
518
|
* ([`src/validate.ts`](validate.ts)) and `artUvsOf`
|
|
519
519
|
* ([`src/render.ts`](render.ts)), which returns no art UVs for a turned region
|
|
520
520
|
* attachment, so `check`'s texture substitution would report it unmatched.
|
|
521
|
+
*
|
|
522
|
+
* ๐ **Measured again for issue #866, on twelve region sets, and held back again.**
|
|
523
|
+
* The sets are the two painting rigs above and ten production rigs of 20 to 22
|
|
524
|
+
* parts. Each was packed by this file's own search at padding 2 twice: as
|
|
525
|
+
* shipped, and with every cell also tried turned under the same BSSF score
|
|
526
|
+
* (ties go to the unturned cell). The shipped search reproduced the page every
|
|
527
|
+
* one of the twelve atlases was written at. Area change from turning:
|
|
528
|
+
*
|
|
529
|
+
* | set | `pot` | `pot` + turn | `free` | `free` + turn |
|
|
530
|
+
* | --- | --- | --- | --- | --- |
|
|
531
|
+
* | 22-part painting | 1024x2048 | 0.00 % | 1888x697 | โ1.14 % |
|
|
532
|
+
* | 20-part painting | 512x2048 | 0.00 % | 480x1166 | +2.31 % |
|
|
533
|
+
* | P1 | 512x2048 | 0.00 % | 416x1633 | +1.43 % |
|
|
534
|
+
* | P2 | 1024x1024 | 0.00 % | 1184x810 | +0.70 % |
|
|
535
|
+
* | P3 | 512x1024 | 0.00 % | 480x1000 | +2.96 % |
|
|
536
|
+
* | P4 | 1024x2048 | 0.00 % | 864x1346 | โ6.16 % |
|
|
537
|
+
* | P5 | 2048x2048 | 0.00 % | 1344x1768 | โ13.37 % |
|
|
538
|
+
* | P6 | 1024x2048 | 0.00 % | 800x1934 | โ1.62 % |
|
|
539
|
+
* | P7 | 512x2048 | 0.00 % | 1024x928 | โ0.05 % |
|
|
540
|
+
* | P8 | 512x2048 | 0.00 % | 608x1250 | โ4.96 % |
|
|
541
|
+
* | P9 | 1024x2048 | +100.00 % | 1024x2037 | +6.51 % |
|
|
542
|
+
* | P10 | 512x2048 | 0.00 % | 352x1433 | +0.70 % |
|
|
543
|
+
*
|
|
544
|
+
* Under `pot` a turn shrank no page on any set, and on P9 an ungated one doubled
|
|
545
|
+
* it. Under `free` the largest gain, P5's 13.37 %, is inside the search's own
|
|
546
|
+
* noise: the nearest other width on the 32-px grid the search then used moves
|
|
547
|
+
* P5's unturned page by 18.13 %, and searching every width instead of every
|
|
548
|
+
* 32nd finds an unturned P5 page 10.22 % smaller with nothing turned. With every
|
|
549
|
+
* width searched, the largest turn gain on any set is 5.05 % (P5), then 4.96 %
|
|
550
|
+
* (P8) and 4.22 % (P4). That is not a page worth two readers changing, so the
|
|
551
|
+
* packer still never turns a region.
|
|
552
|
+
*
|
|
553
|
+
* โ ๏ธ The `free` column and the 18.13 % are measurements of the search as it was
|
|
554
|
+
* on that day, a 32-px width grid. Issue #872 replaced it with every width (see
|
|
555
|
+
* `FREE_EDGE_STEP`), so today's `free` pages for these sets are the every-width
|
|
556
|
+
* ones the paragraph above compares against, and the 5.05 % is the turn gain
|
|
557
|
+
* measured on the search that ships.
|
|
521
558
|
*/
|
|
522
559
|
export const PACK_NO_ROTATE = 0;
|
|
523
560
|
|
|
@@ -597,7 +634,7 @@ export const DEFAULT_PADDING = 2;
|
|
|
597
634
|
* What a packed page's edges may be โ `--page-edges`. See `packAtlas`, *Page size*.
|
|
598
635
|
*
|
|
599
636
|
* `pot` is the default and the only value until issue #860: both edges are powers
|
|
600
|
-
* of two. `free` lets the width be any
|
|
637
|
+
* of two. `free` lets the width be any whole number of pixels and the height
|
|
601
638
|
* whatever the placement needs. What `free` costs is measured rather than
|
|
602
639
|
* conceded: a region attachment's sampling coordinate stops being exact and moves
|
|
603
640
|
* to `PK05`'s bound of one least significant bit, the bound a mesh already had.
|
|
@@ -605,8 +642,28 @@ export const DEFAULT_PADDING = 2;
|
|
|
605
642
|
export const PAGE_EDGES = ['pot', 'free'] as const;
|
|
606
643
|
export type PageEdges = (typeof PAGE_EDGES)[number];
|
|
607
644
|
export const DEFAULT_PAGE_EDGES: PageEdges = 'pot';
|
|
608
|
-
/**
|
|
609
|
-
|
|
645
|
+
/**
|
|
646
|
+
* The step a `free` page's width is tried on: **1, every width** (issue #872).
|
|
647
|
+
* See `smallestFreePageFor` for the search and `packAtlas`, *Page size*, for
|
|
648
|
+
* what it buys.
|
|
649
|
+
*
|
|
650
|
+
* It was 32 until #872, and the grid was the cost. On twelve region sets (two
|
|
651
|
+
* painting rigs and ten production rigs of 20 to 22 parts) the 32-px grid's page
|
|
652
|
+
* was larger than the every-width page on eleven, by up to 11.38 % (P5: 1344x1768
|
|
653
|
+
* against 1427x1495). Finer fixed steps and coarse-to-fine searches were measured
|
|
654
|
+
* against the every-width page too, and none of them is safe: the area as a
|
|
655
|
+
* function of the width has minima one pixel wide, so a step-4 grid refined
|
|
656
|
+
* over ยฑ3 px of its best four widths still misses one (the fetched
|
|
657
|
+
* `5-squash-and-stretch` atlas: 867x415 against 863x415), and the coarse-to-fine
|
|
658
|
+
* search that was exact on all twelve sets missed by 0.82 % on the first
|
|
659
|
+
* held-out atlas it met. Every width is exact by construction; what keeps it
|
|
660
|
+
* cheap is the two bounds `smallestFreePageFor` prunes with, and they are exact
|
|
661
|
+
* too.
|
|
662
|
+
*
|
|
663
|
+
* Kept as a named constant rather than deleted because it is importable, and a
|
|
664
|
+
* reader of the old value should find the new one rather than a missing name.
|
|
665
|
+
*/
|
|
666
|
+
export const FREE_EDGE_STEP = 1;
|
|
610
667
|
|
|
611
668
|
/** The part a page is packed from: its region name and its pixels. */
|
|
612
669
|
export interface PackInput {
|
|
@@ -720,9 +777,8 @@ function smallestPageFor(
|
|
|
720
777
|
*
|
|
721
778
|
* The rule, which `docs/AUTHORING.md` ยง0.1 states in the same words:
|
|
722
779
|
*
|
|
723
|
-
* * the candidate WIDTHS are
|
|
724
|
-
*
|
|
725
|
-
* only adds anything when it is smaller than the step);
|
|
780
|
+
* * the candidate WIDTHS are every whole width from the widest cell up to
|
|
781
|
+
* `maxEdge` (`FREE_EDGE_STEP` is 1);
|
|
726
782
|
* * at each width the cells are placed by the same MaxRects pass as a `pot`
|
|
727
783
|
* page, on a page `maxEdge` tall, and the HEIGHT is the bottom edge of the
|
|
728
784
|
* lowest cell โ what that placement needs, not rounded;
|
|
@@ -735,26 +791,84 @@ function smallestPageFor(
|
|
|
735
791
|
* in the page height (a shorter page changes which free rectangle scores best),
|
|
736
792
|
* so "the least height that fits" is not a quantity a bisection could find, and a
|
|
737
793
|
* definition that could not be computed exactly would not be deterministic.
|
|
794
|
+
*
|
|
795
|
+
* โญ **Two bounds make every width cheap, and neither can change the answer**
|
|
796
|
+
* (issue #872). Both compare against the best page found so far, and both are
|
|
797
|
+
* strict, so a width that could still TIE the best โ and win on the squarer or
|
|
798
|
+
* narrower key โ is always placed to the end:
|
|
799
|
+
*
|
|
800
|
+
* * a width is not placed at all when the cells' own area exceeds
|
|
801
|
+
* `width x maxEdge` (it cannot fit), or when `width x` the tallest cell
|
|
802
|
+
* already exceeds the best area (no page at that width is shorter than its
|
|
803
|
+
* tallest cell);
|
|
804
|
+
* * a placement is abandoned the moment its lowest cell's bottom edge makes
|
|
805
|
+
* `width x bottom` exceed the best area. The bottom edge only grows as cells
|
|
806
|
+
* are added, so the page that placement would have finished is larger still.
|
|
807
|
+
*
|
|
808
|
+
* Neither changes a placement that runs to the end โ the pass is the `pot` pass,
|
|
809
|
+
* on the same page, and abandoning it only stops it early โ so the winner is the
|
|
810
|
+
* every-width winner, and `PK75` compares the two on every set it builds. What
|
|
811
|
+
* they save, measured on the twelve sets: 391,854 cell placements for an
|
|
812
|
+
* unbounded every-width search, 47,978 bounded, against 12,361 for the retired
|
|
813
|
+
* 32-px grid.
|
|
738
814
|
*/
|
|
739
815
|
function smallestFreePageFor(
|
|
740
816
|
cells: Array<{ w: number; h: number }>,
|
|
741
817
|
maxEdge: number,
|
|
742
818
|
): { width: number; height: number; rects: Rect[] } | null {
|
|
819
|
+
return freePageSearch(cells, maxEdge).page;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** What one `free` page search did: the page it chose, and what each width cost. */
|
|
823
|
+
export interface FreePageSearch {
|
|
824
|
+
page: { width: number; height: number; rects: Array<{ x: number; y: number; w: number; h: number }> } | null;
|
|
825
|
+
/** Candidate widths, from the widest cell to `maxEdge`. */
|
|
826
|
+
widths: number;
|
|
827
|
+
/** Widths whose placement ran to the end. */
|
|
828
|
+
completed: number;
|
|
829
|
+
/** Widths whose placement stopped once its lowest cell made the page larger than the best so far. */
|
|
830
|
+
abandoned: number;
|
|
831
|
+
/** Widths never placed: too narrow for the cells' area, or wide enough that the tallest cell alone loses. */
|
|
832
|
+
skipped: number;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* `smallestFreePageFor`'s search with its bookkeeping โ exported so the
|
|
837
|
+
* selftest can compare its page against an unbounded every-width reference and
|
|
838
|
+
* see that the bounds did prune (`PK75`). The page is the whole of what the
|
|
839
|
+
* packer uses; the counts are for the control.
|
|
840
|
+
*/
|
|
841
|
+
export function freePageSearch(cells: Array<{ w: number; h: number }>, maxEdge: number): FreePageSearch {
|
|
743
842
|
let widest = 0;
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
for (
|
|
747
|
-
|
|
843
|
+
let tallest = 0;
|
|
844
|
+
let cellArea = 0;
|
|
845
|
+
for (const cell of cells) {
|
|
846
|
+
widest = Math.max(widest, cell.w);
|
|
847
|
+
tallest = Math.max(tallest, cell.h);
|
|
848
|
+
cellArea += cell.w * cell.h;
|
|
849
|
+
}
|
|
850
|
+
const out: FreePageSearch = { page: null, widths: 0, completed: 0, abandoned: 0, skipped: 0 };
|
|
748
851
|
let best: { width: number; height: number; rects: Rect[] } | null = null;
|
|
749
|
-
for (
|
|
750
|
-
|
|
852
|
+
for (let width = Math.ceil(widest / FREE_EDGE_STEP) * FREE_EDGE_STEP; width <= maxEdge; width += FREE_EDGE_STEP) {
|
|
853
|
+
out.widths++;
|
|
854
|
+
const bestArea = best === null ? Infinity : best.width * best.height;
|
|
855
|
+
if (cellArea > width * maxEdge || width * tallest > bestArea) {
|
|
856
|
+
out.skipped++;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const maxBottom = best === null ? Infinity : Math.floor(bestArea / width);
|
|
860
|
+
const attempt = packOnePage(cells, width, maxEdge, maxBottom);
|
|
861
|
+
let height = 0;
|
|
862
|
+
for (const r of attempt) if (r !== null) height = Math.max(height, r.y + r.h);
|
|
863
|
+
if (height > maxBottom) {
|
|
864
|
+
out.abandoned++;
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
out.completed++;
|
|
751
868
|
if (attempt.some((r) => r === null)) continue;
|
|
752
869
|
const rects = attempt as Rect[];
|
|
753
|
-
let height = 0;
|
|
754
|
-
for (const r of rects) height = Math.max(height, r.y + r.h);
|
|
755
870
|
if (best !== null) {
|
|
756
871
|
const area = width * height;
|
|
757
|
-
const bestArea = best.width * best.height;
|
|
758
872
|
if (area > bestArea) continue;
|
|
759
873
|
if (area === bestArea) {
|
|
760
874
|
const square = Math.abs(width - height);
|
|
@@ -765,7 +879,28 @@ function smallestFreePageFor(
|
|
|
765
879
|
}
|
|
766
880
|
best = { width, height, rects };
|
|
767
881
|
}
|
|
768
|
-
|
|
882
|
+
out.page = best;
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* One `free` candidate, unbounded: the cells placed at `width` on a page
|
|
888
|
+
* `maxEdge` tall, and the height that placement needs โ or `null` when they do
|
|
889
|
+
* not all fit. It is the pass `freePageSearch` runs at each width with no
|
|
890
|
+
* bound, exported so the selftest can rebuild a reference search from the same
|
|
891
|
+
* placement (`PK74`'s 32-px grid, `PK75`'s every width, `PK76`'s `pot` search).
|
|
892
|
+
*/
|
|
893
|
+
export function placeAtWidth(
|
|
894
|
+
cells: Array<{ w: number; h: number }>,
|
|
895
|
+
width: number,
|
|
896
|
+
height: number,
|
|
897
|
+
): { width: number; height: number; rects: Array<{ x: number; y: number; w: number; h: number }> } | null {
|
|
898
|
+
const attempt = packOnePage(cells, width, height);
|
|
899
|
+
if (attempt.some((r) => r === null)) return null;
|
|
900
|
+
const rects = attempt as Rect[];
|
|
901
|
+
let bottom = 0;
|
|
902
|
+
for (const r of rects) bottom = Math.max(bottom, r.y + r.h);
|
|
903
|
+
return { width, height: bottom, rects };
|
|
769
904
|
}
|
|
770
905
|
|
|
771
906
|
/**
|
|
@@ -782,8 +917,18 @@ function smallestFreePageFor(
|
|
|
782
917
|
* to be pushed. So the tie-break is spelled out and total โ smallest long-side
|
|
783
918
|
* leftover, then topmost, then leftmost โ and the caller sorts its input before
|
|
784
919
|
* calling. Same inputs, byte-identical page.
|
|
920
|
+
*
|
|
921
|
+
* `maxBottom` is the `free` search's abandon bound (see `freePageSearch`): once a
|
|
922
|
+
* placed cell's bottom edge passes it the pass stops, and the cells not placed
|
|
923
|
+
* are `null`. The default, `Infinity`, never stops a pass โ which is every `pot`
|
|
924
|
+
* call, so a `pot` page is placed exactly as it was before the bound existed.
|
|
785
925
|
*/
|
|
786
|
-
function packOnePage(
|
|
926
|
+
function packOnePage(
|
|
927
|
+
cells: Array<{ w: number; h: number }>,
|
|
928
|
+
pageW: number,
|
|
929
|
+
pageH: number,
|
|
930
|
+
maxBottom = Infinity,
|
|
931
|
+
): Array<Rect | null> {
|
|
787
932
|
const free: Rect[] = [{ x: 0, y: 0, w: pageW, h: pageH }];
|
|
788
933
|
const placed: Array<Rect | null> = [];
|
|
789
934
|
|
|
@@ -817,6 +962,10 @@ function packOnePage(cells: Array<{ w: number; h: number }>, pageW: number, page
|
|
|
817
962
|
}
|
|
818
963
|
const put: Rect = { x: best.x, y: best.y, w: cell.w, h: cell.h };
|
|
819
964
|
placed.push(put);
|
|
965
|
+
if (put.y + put.h > maxBottom) {
|
|
966
|
+
while (placed.length < cells.length) placed.push(null);
|
|
967
|
+
return placed;
|
|
968
|
+
}
|
|
820
969
|
|
|
821
970
|
// Split every free rectangle the placement overlaps, then prune.
|
|
822
971
|
const next: Rect[] = [];
|
|
@@ -950,15 +1099,18 @@ function extrudeCell(page: Plate, source: Plate, cellX: number, cellY: number, p
|
|
|
950
1099
|
* Opt-in, and the default stays `pot`: every runtime accepts a power-of-two page
|
|
951
1100
|
* and the editor's own packer writes one by default โ 9 of the 10 atlases in the
|
|
952
1101
|
* fetched `examples/` corpus are power-of-two on both edges. Under `free` the
|
|
953
|
-
* search is `smallestFreePageFor`:
|
|
954
|
-
*
|
|
1102
|
+
* search is `smallestFreePageFor`: every width from the widest cell up (issue
|
|
1103
|
+
* #872; a 32-px grid until then), the height the placement needs, least area
|
|
1104
|
+
* first, then squarer, then narrower, with two exact bounds that keep it cheap. Everything
|
|
955
1105
|
* else is shared โ the MaxRects pass, the packing order, the spill rule (which
|
|
956
1106
|
* parts share a page is still decided at `maxEdge x maxEdge`) and `--page-size`
|
|
957
1107
|
* as the ceiling on both edges, floored to a power of two exactly as under `pot`.
|
|
958
1108
|
*
|
|
959
1109
|
* What it buys and what it costs, observed on two 20- and 22-part painting rigs:
|
|
960
|
-
* the page
|
|
961
|
-
* -
|
|
1110
|
+
* the page goes from 1024x2048 to 967x1338 (2,097,152 to 1,293,846 texels,
|
|
1111
|
+
* -38.3 %) and from 512x2048 to 479x1166 (1,048,576 to 558,514, -46.7 %). Under
|
|
1112
|
+
* the 32-px grid #860 shipped with they were 1888x697 (-37.3 %) and 480x1166
|
|
1113
|
+
* (-46.6 %). The cost is the exactness the paragraph above describes:
|
|
962
1114
|
* `x / pageWidth` is no longer exact, so a REGION attachment's sampling
|
|
963
1115
|
* coordinate joins a mesh's at `PK05`'s bound of one least significant bit
|
|
964
1116
|
* against the loose build. It does not go past it, and no region's bytes change
|
package/src/check.ts
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
PAD,
|
|
81
81
|
FRAMES_SIDECAR,
|
|
82
82
|
FRAMES_SPEC,
|
|
83
|
+
nonFinitePoseOf,
|
|
83
84
|
SHEET_COLUMNS,
|
|
84
85
|
SHEET_FILE,
|
|
85
86
|
SHEET_GAP,
|
|
@@ -1225,6 +1226,27 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1225
1226
|
// to compare it โ and posing twice is both slower and a chance for the framing
|
|
1226
1227
|
// and the comparison to disagree about what they measured.
|
|
1227
1228
|
const prepared = sets.map((set) => prepareSet(located.root, set, posable, options.as, poseOptions));
|
|
1229
|
+
// ๐ A pose that is not finite is refused before anything measures it (issue
|
|
1230
|
+
// #873), in the words `render` and the geometry export use for it. Measured
|
|
1231
|
+
// on a planted bone at x=1e309 before this: an overflowing root read "posed no
|
|
1232
|
+
// drawable attachment", one at -1e309 "drew no pixel", and one at +1e309 or a
|
|
1233
|
+
// rotation at 1e309 exited 0 with MAE 1.00 โ the part missing from every
|
|
1234
|
+
// frame, and nothing said why.
|
|
1235
|
+
const posed = prepared.filter((p) => p.missing === null);
|
|
1236
|
+
const overflow = posed.some((p) =>
|
|
1237
|
+
p.frames.some((frame) => frame.pieces.some((piece) => piece.world.some((value) => !Number.isFinite(value)))),
|
|
1238
|
+
);
|
|
1239
|
+
if (overflow) {
|
|
1240
|
+
const found = nonFinitePoseOf(
|
|
1241
|
+
posable.data,
|
|
1242
|
+
options.skin,
|
|
1243
|
+
posed.map((p) => ({ animation: p.candidateAnimation, fps: p.set.fps })),
|
|
1244
|
+
);
|
|
1245
|
+
if (found === null) {
|
|
1246
|
+
throw new Error('a drawn piece of the candidate is not finite and no bone or vertex of its pose is โ the two were posed differently');
|
|
1247
|
+
}
|
|
1248
|
+
throw new CheckError(`the candidate is posed to a number that is not finite, so no frame of it can be compared: ${found}`);
|
|
1249
|
+
}
|
|
1228
1250
|
const pairs = prepared.flatMap((p) => p.pairs);
|
|
1229
1251
|
if (pairs.length === 0) {
|
|
1230
1252
|
notes.push('no reference frame has a candidate frame at the same index โ nothing below was measured');
|