spine-rigc 1.7.0 → 1.8.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/cli.ts +87 -20
- package/docs/AUTHORING.md +18 -12
- package/package.json +1 -1
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/skin_entries.ts +35 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/kinds.ts +102 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/index.ts +130 -0
- package/src/assertions/model/parse.ts +136 -0
- package/src/assertions/model/skin_entries.ts +58 -0
- package/src/assertions/model/slot_colour.ts +80 -0
- package/src/assertions/reasons.ts +127 -0
- package/src/assertions/values.ts +39 -0
- package/src/check.ts +33 -8
- package/src/compile.ts +59 -19
- package/src/core/index.ts +7 -5
- package/src/core/world.ts +21 -20
- package/src/deformmeasure.ts +377 -154
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/model.ts +5 -2
- package/src/render.ts +195 -47
- package/src/render_core.ts +68 -8
- package/src/transform.ts +79 -30
- package/src/validate.ts +149 -461
package/src/render.ts
CHANGED
|
@@ -83,9 +83,10 @@
|
|
|
83
83
|
* `./render_core.ts`, and this file keeps the runtime for the export's poser,
|
|
84
84
|
* the export's atlas pages and texture substitution — see *which poser* below.
|
|
85
85
|
* Since issue #1014 a rigc build the core poses touches none of it: what a
|
|
86
|
-
* render reads off the candidate besides the pose comes from
|
|
87
|
-
*
|
|
88
|
-
*
|
|
86
|
+
* render reads off the candidate besides the pose comes from its model
|
|
87
|
+
* document and the skeleton's own JSON (`loadCandidate`), and since issue
|
|
88
|
+
* #1020 its pages and a `--texture-from` atlas through rigc's own reader, so
|
|
89
|
+
* spine-core is reached only where it poses an export or a fallback.) The rule that matters is unchanged
|
|
89
90
|
* — `src/compile.ts` must stay independent of the runtime so the compiler and
|
|
90
91
|
* the gate are not checking each other's assumptions — and this file is neither.
|
|
91
92
|
* It also imports `tools/plate.ts` for the PNG codec, which is dependency-free.
|
|
@@ -111,8 +112,19 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
111
112
|
import { Plate, readPlate, type RGBA } from '../tools/plate.ts';
|
|
112
113
|
import { pageFootprint, parseAtlasText } from './atlas.ts';
|
|
113
114
|
import { CoreInputError } from './core/index.ts';
|
|
115
|
+
import { computeUvs } from './core/uvs.ts';
|
|
114
116
|
import { MODEL_DOCUMENT_FILE } from './model.ts';
|
|
115
|
-
import {
|
|
117
|
+
import {
|
|
118
|
+
clipSourceOf,
|
|
119
|
+
coreDocumentFacts,
|
|
120
|
+
corePoser,
|
|
121
|
+
inactiveBoneSnapshot,
|
|
122
|
+
SlotSubsetError,
|
|
123
|
+
subsetOver,
|
|
124
|
+
unposedBones,
|
|
125
|
+
type CoreDocumentFacts,
|
|
126
|
+
type SubsetRoster,
|
|
127
|
+
} from './render_core.ts';
|
|
116
128
|
import { walkTimelines } from './timelines.ts';
|
|
117
129
|
|
|
118
130
|
/** Opaque, and light: both of rung 3's parts are dark slate, so is every ground. */
|
|
@@ -759,6 +771,14 @@ export function posableFromText(skeletonText: string, atlasText: string, atlasDi
|
|
|
759
771
|
// nineteen built corpus rows and the twelve editor exports (the PR of #1014
|
|
760
772
|
// carries the counts). A Spine export, `--poser spine` and a fallback the
|
|
761
773
|
// poser line names load spine-core as before and read every fact off it.
|
|
774
|
+
//
|
|
775
|
+
// Since issue #1020 a build the core poses reads what its model document
|
|
776
|
+
// states off the document (`coreFacts`): the bone tree, the slot list and the
|
|
777
|
+
// subset's roster, and the page images by the names its `pages` section gives
|
|
778
|
+
// — so with a `rigc-compiled/2` document the atlas file is not needed at all.
|
|
779
|
+
// What only `skeleton.json` holds is still read there: the order the
|
|
780
|
+
// animations and skins are listed in (the emitter's, not the model's) and the
|
|
781
|
+
// stage.
|
|
762
782
|
|
|
763
783
|
/** What `render` and `check` read off a candidate's skeleton besides its pose. */
|
|
764
784
|
export interface SkeletonFacts {
|
|
@@ -859,13 +879,35 @@ function skeletonDurations(root: JsonObject): Array<{ name: string; duration: nu
|
|
|
859
879
|
});
|
|
860
880
|
}
|
|
861
881
|
|
|
862
|
-
/** Every page
|
|
863
|
-
function
|
|
882
|
+
/** Every page named, read from `atlasDir` by that name — the images a candidate the core poses is drawn from. */
|
|
883
|
+
function pagesNamed(names: readonly string[], atlasDir: string): Map<string, Plate> {
|
|
864
884
|
const pages = new Map<string, Plate>();
|
|
865
|
-
for (const
|
|
885
|
+
for (const name of names) pages.set(name, readPlate(join(atlasDir, name)));
|
|
866
886
|
return pages;
|
|
867
887
|
}
|
|
868
888
|
|
|
889
|
+
/**
|
|
890
|
+
* A candidate with no atlas beside it that has to be read through one —
|
|
891
|
+
* refused naming the file and why (issue #1020).
|
|
892
|
+
*
|
|
893
|
+
* A rigc build whose model document states where each region sits on its page
|
|
894
|
+
* (`rigc-compiled/2`, the `pages` section) is drawn by the core without its
|
|
895
|
+
* atlas. Everything else reads one: a Spine export, `--poser spine`, a
|
|
896
|
+
* `rigc-compiled/1` document, and a build the core refuses and spine-core
|
|
897
|
+
* draws instead. A class of its own so `cli.ts` refuses it as an invocation
|
|
898
|
+
* (exit 2, nothing written), as it refuses a missing atlas on an export.
|
|
899
|
+
*/
|
|
900
|
+
export class CandidateAtlasError extends Error {}
|
|
901
|
+
|
|
902
|
+
/** The refusal for a candidate that has to be read through an atlas it does not have. */
|
|
903
|
+
function atlasAbsent(atlasPath: string, label: string, why: string): CandidateAtlasError {
|
|
904
|
+
return new CandidateAtlasError(
|
|
905
|
+
`nothing at ${atlasPath}: ${label} is drawn through its atlas (${why}). ` +
|
|
906
|
+
'Only a rigc build whose skeleton.model.json is a rigc-compiled/2 document, posed by the core, is drawn ' +
|
|
907
|
+
'without one — it states where each region sits on its page',
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
869
911
|
/**
|
|
870
912
|
* Posing an input through spine-core when the runtime cannot be reached —
|
|
871
913
|
* refused naming the input and why it needs the runtime (issue #1014).
|
|
@@ -900,9 +942,13 @@ export interface Candidate {
|
|
|
900
942
|
|
|
901
943
|
/**
|
|
902
944
|
* Load a candidate for `render` or `check`, choosing its poser FIRST (issue
|
|
903
|
-
* #1014): a rigc build the core poses reads its facts off its
|
|
904
|
-
*
|
|
905
|
-
*
|
|
945
|
+
* #1014): a rigc build the core poses reads its facts off its model document
|
|
946
|
+
* and its own JSON (`coreFacts`), its page images by the names the
|
|
947
|
+
* document's `pages` gives (a `rigc-compiled/1` document's, by its atlas's)
|
|
948
|
+
* and its skin roster off the document, and spine-core is never loaded for
|
|
949
|
+
* it; `input.atlasText` is `null` where the atlas file is not there, which only
|
|
950
|
+
* a `rigc-compiled/2` document the core poses is drawn without (issue #1020 —
|
|
951
|
+
* anything else is refused, `CandidateAtlasError`); anything else — a Spine export,
|
|
906
952
|
* `--poser spine`, a candidate handed over as text (`paths` null, `unplaced`
|
|
907
953
|
* the reason) — is loaded through spine-core as `posableFromText` always
|
|
908
954
|
* loaded it. The choice's spine-core poser stays unloaded until something
|
|
@@ -913,31 +959,66 @@ export interface Candidate {
|
|
|
913
959
|
* refuse before it.
|
|
914
960
|
*/
|
|
915
961
|
export function loadCandidate(
|
|
916
|
-
input: { skeletonText: string; atlasText: string; atlasDir: string; label: string },
|
|
962
|
+
input: { skeletonText: string; atlasText: string | null; atlasDir: string; label: string },
|
|
917
963
|
paths: { skeleton: string; atlas: string } | null,
|
|
918
964
|
forced: PoserName | undefined,
|
|
919
965
|
options: { make?: MakeCorePoser; unplaced?: string } = {},
|
|
920
966
|
): Candidate {
|
|
921
967
|
let data: SkeletonData | null = null;
|
|
922
968
|
let choice: PoserChoice | null = null;
|
|
969
|
+
// `null` is an atlas file that is not there (issue #1020): a candidate the core draws from its document needs none, and anything read through one is refused naming the file before the runtime is touched.
|
|
970
|
+
const atlasText = (why: string): string => {
|
|
971
|
+
if (input.atlasText === null) throw atlasAbsent(paths?.atlas ?? '(no atlas)', input.label, why);
|
|
972
|
+
return input.atlasText;
|
|
973
|
+
};
|
|
923
974
|
const spineData = (): SkeletonData => {
|
|
924
975
|
if (data === null) {
|
|
925
|
-
|
|
926
|
-
|
|
976
|
+
const why = choice === null || choice.core !== null ? 'a fallback from the core poser' : choice.why;
|
|
977
|
+
const text = atlasText(why);
|
|
978
|
+
requireSpineRuntime(input.label, why);
|
|
979
|
+
data = new SkeletonJson(new AtlasAttachmentLoader(new TextureAtlas(text))).readSkeletonData(JSON.parse(input.skeletonText));
|
|
927
980
|
}
|
|
928
981
|
return data;
|
|
929
982
|
};
|
|
930
|
-
|
|
983
|
+
const chosen =
|
|
931
984
|
paths === null
|
|
932
|
-
? posersOver(forced, null, forced === 'spine' ? '--poser spine' : (options.unplaced ?? 'no path to find a model document beside'), spineData)
|
|
933
|
-
: choosePosers(paths.skeleton, paths.atlas, forced, spineData, options.make ?? corePoser);
|
|
934
|
-
|
|
985
|
+
? { choice: posersOver(forced, null, forced === 'spine' ? '--poser spine' : (options.unplaced ?? 'no path to find a model document beside'), spineData), document: null }
|
|
986
|
+
: choosePosers(paths.skeleton, paths.atlas, input.atlasText, forced, spineData, options.make ?? corePoser);
|
|
987
|
+
choice = chosen.choice;
|
|
988
|
+
if (choice.core !== null && chosen.document !== null) {
|
|
989
|
+
// A rigc-compiled/2 document names its pages; a /1 document is posed only with its atlas beside it, which names them (`choosePosers`).
|
|
990
|
+
const names = chosen.document.pageNames ?? parseAtlasText(atlasText(choice.why)).pages.map((page) => page.name);
|
|
991
|
+
return { choice, facts: coreFacts(input.skeletonText, choice.core, chosen.document), pages: pagesNamed(names, input.atlasDir) };
|
|
992
|
+
}
|
|
993
|
+
const text = atlasText(choice.why);
|
|
935
994
|
requireSpineRuntime(input.label, choice.why);
|
|
936
|
-
const posable = posableFromText(input.skeletonText,
|
|
995
|
+
const posable = posableFromText(input.skeletonText, text, input.atlasDir);
|
|
937
996
|
data = posable.data;
|
|
938
997
|
return { choice, facts: spineFacts(posable.data), pages: posable.pages };
|
|
939
998
|
}
|
|
940
999
|
|
|
1000
|
+
/**
|
|
1001
|
+
* A candidate's facts when the core poses it (issue #1020): what the model
|
|
1002
|
+
* document states, read from it — the bone tree and the slot list (the core
|
|
1003
|
+
* poser's own, which `rosterDifference` held to the Spine file's before the
|
|
1004
|
+
* core was chosen) and the slot subset's roster (`coreDocumentFacts`) — and,
|
|
1005
|
+
* off the skeleton's own JSON, what it does not: the order the animations and
|
|
1006
|
+
* the skins are listed in (the Spine file's — the emitter's order, not the
|
|
1007
|
+
* model's) and whether a stage is declared (the header's `width`/`height`,
|
|
1008
|
+
* which the document has no field for).
|
|
1009
|
+
*/
|
|
1010
|
+
function coreFacts(skeletonText: string, poser: Poser, document: CoreDocumentFacts): SkeletonFacts {
|
|
1011
|
+
const spine = skeletonFacts(skeletonText);
|
|
1012
|
+
return {
|
|
1013
|
+
animations: spine.animations,
|
|
1014
|
+
skins: spine.skins,
|
|
1015
|
+
declaresStage: spine.declaresStage,
|
|
1016
|
+
bones: poser.bones,
|
|
1017
|
+
slots: poser.slots,
|
|
1018
|
+
subset: (opts, skin) => subsetOver(document.subset, opts, skin),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
941
1022
|
// ---------------------------------------------------------------------------
|
|
942
1023
|
// the posing seam (issue #965, step 3a of #380)
|
|
943
1024
|
// ---------------------------------------------------------------------------
|
|
@@ -962,9 +1043,10 @@ export function loadCandidate(
|
|
|
962
1043
|
// The framing samples are not a fourth entry: `framingViewport` is the same
|
|
963
1044
|
// animation entry at `FRAMING_FPS` with the clip off.
|
|
964
1045
|
//
|
|
965
|
-
// 🔒 What stays spine-core's and outside the seam, deliberately:
|
|
966
|
-
// (`posableFromText`'s pages, `substituteTexture`'s region lookup
|
|
967
|
-
//
|
|
1046
|
+
// 🔒 What stays spine-core's and outside the seam, deliberately: an export's
|
|
1047
|
+
// atlas (`posableFromText`'s pages, and `substituteTexture`'s region lookup on
|
|
1048
|
+
// anything spine-core poses — a rigc build the core poses reads both through
|
|
1049
|
+
// rigc's own reader since issue #1020) and `nonFiniteOfPosed`, which `validate.ts` calls on a spine-core
|
|
968
1050
|
// skeleton it stepped itself (A10), so the round trip keeps its spine-core
|
|
969
1051
|
// entry whatever poses the renders.
|
|
970
1052
|
|
|
@@ -1308,13 +1390,16 @@ function posersOver(
|
|
|
1308
1390
|
function choosePosers(
|
|
1309
1391
|
skeletonPath: string,
|
|
1310
1392
|
atlasPath: string,
|
|
1393
|
+
/** The atlas file's text, or `null` when there is no file at `atlasPath` (issue #1020). */
|
|
1394
|
+
atlasText: string | null,
|
|
1311
1395
|
forced: PoserName | undefined,
|
|
1312
1396
|
spineData: () => SkeletonData,
|
|
1313
1397
|
make: MakeCorePoser,
|
|
1314
|
-
): PoserChoice {
|
|
1398
|
+
): { choice: PoserChoice; document: CoreDocumentFacts | null } {
|
|
1315
1399
|
const dir = dirname(resolve(skeletonPath));
|
|
1316
1400
|
const modelPath = join(dir, MODEL_DOCUMENT_FILE);
|
|
1317
1401
|
let core: { poser: Poser; roster: SkinRoster } | null = null;
|
|
1402
|
+
let document: CoreDocumentFacts | null = null;
|
|
1318
1403
|
let why: string;
|
|
1319
1404
|
if (forced === 'spine') why = '--poser spine';
|
|
1320
1405
|
else if (!existsSync(modelPath)) why = `no ${MODEL_DOCUMENT_FILE} beside ${resolve(skeletonPath)} — a Spine export, not a rigc build`;
|
|
@@ -1326,12 +1411,18 @@ function choosePosers(
|
|
|
1326
1411
|
try {
|
|
1327
1412
|
const modelText = readFileSync(modelPath, 'utf8');
|
|
1328
1413
|
const bytes = readFileSync(skeletonPath);
|
|
1329
|
-
|
|
1414
|
+
// No atlas is `''`: a rigc-compiled/2 document draws from its own `pages`, and a /1 document is refused by name (`placementOf`). An atlas that is there is held to the document's `pages` (#1016) and refused, naming the first difference, when it is not the one the build wrote.
|
|
1415
|
+
const candidate = make(modelText, atlasText ?? '', modelPath, { path: resolve(skeletonPath), bytes });
|
|
1330
1416
|
const rosters = skeletonRosters(bytes.toString('utf8'));
|
|
1331
1417
|
const differs = rosterDifference(candidate, rosters);
|
|
1332
1418
|
if (differs === null) {
|
|
1333
|
-
|
|
1334
|
-
|
|
1419
|
+
document = coreDocumentFacts(modelText, modelPath, rosters.skins);
|
|
1420
|
+
core = { poser: candidate, roster: document.roster };
|
|
1421
|
+
// The poser line names where the placement came from when it is not the document's own (issue #1020): a /1 document states none, and the core reads it from the atlas beside it.
|
|
1422
|
+
why =
|
|
1423
|
+
document.pageNames === null
|
|
1424
|
+
? `${modelPath} — a ${document.spec} document, which does not state where each region sits on its page: that is read from ${resolve(atlasPath)}`
|
|
1425
|
+
: modelPath;
|
|
1335
1426
|
} else why = `${modelPath} does not describe ${resolve(skeletonPath)}: ${differs}`;
|
|
1336
1427
|
} catch (err) {
|
|
1337
1428
|
if (!(err instanceof CoreInputError)) throw err;
|
|
@@ -1339,7 +1430,7 @@ function choosePosers(
|
|
|
1339
1430
|
why = `the core refused ${err.message.startsWith(`${modelPath}: `) ? err.message : `${modelPath}: ${err.message}`}`;
|
|
1340
1431
|
}
|
|
1341
1432
|
}
|
|
1342
|
-
return posersOver(forced, core, why, spineData);
|
|
1433
|
+
return { choice: posersOver(forced, core, why, spineData), document: core === null ? null : document };
|
|
1343
1434
|
}
|
|
1344
1435
|
|
|
1345
1436
|
/**
|
|
@@ -1367,7 +1458,7 @@ export function candidatePosers(
|
|
|
1367
1458
|
/** What builds the core poser: `corePoser` — the suite's `RC02` passes a planted copy, and nothing else passes any. */
|
|
1368
1459
|
make: MakeCorePoser = corePoser,
|
|
1369
1460
|
): PoserChoice {
|
|
1370
|
-
return refuseUnchosen(choosePosers(skeletonPath, atlasPath, forced, () => data, make));
|
|
1461
|
+
return refuseUnchosen(choosePosers(skeletonPath, atlasPath, readFileSync(atlasPath, 'utf8'), forced, () => data, make).choice);
|
|
1371
1462
|
}
|
|
1372
1463
|
|
|
1373
1464
|
/**
|
|
@@ -2094,7 +2185,7 @@ function artUvsOf(attachment: MeshAttachment | RegionAttachment, region: Texture
|
|
|
2094
2185
|
* sequence packs several regions under one name and `findRegion` returns only the
|
|
2095
2186
|
* first of them.
|
|
2096
2187
|
*/
|
|
2097
|
-
function regionKey(region:
|
|
2188
|
+
function regionKey(region: { name: string; index: number }): string {
|
|
2098
2189
|
return `${region.name.trim()}#${region.index}`;
|
|
2099
2190
|
}
|
|
2100
2191
|
|
|
@@ -2104,28 +2195,86 @@ function regionKey(region: TextureAtlasRegion): string {
|
|
|
2104
2195
|
*/
|
|
2105
2196
|
export const SUBSTITUTE_PAGE = 'texture-from:';
|
|
2106
2197
|
|
|
2198
|
+
/**
|
|
2199
|
+
* One region of a substituting atlas, as `substituteTexture` reads it: where
|
|
2200
|
+
* it sits on which page, and the mapping from the drawing's own coordinates
|
|
2201
|
+
* onto it (`MeshAttachment.computeUVs`'s job).
|
|
2202
|
+
*/
|
|
2203
|
+
export interface SubstituteRegion {
|
|
2204
|
+
page: { name: string; width: number; height: number };
|
|
2205
|
+
x: number;
|
|
2206
|
+
y: number;
|
|
2207
|
+
width: number;
|
|
2208
|
+
height: number;
|
|
2209
|
+
degrees: number;
|
|
2210
|
+
/** Art-space UVs (`u, v` per vertex) mapped onto this region of its page, in doubles. */
|
|
2211
|
+
pageUvs(art: readonly number[]): number[];
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2107
2214
|
/** An atlas whose texels can stand in for another's — see `substituteTexture`. */
|
|
2108
2215
|
export interface TextureSubstitution {
|
|
2109
2216
|
/** Prefixed page name → the page, ready to merge into a render's page map. */
|
|
2110
2217
|
pages: Map<string, Plate>;
|
|
2111
2218
|
/** The atlas's regions, by the key both sides agree on — see `regionKey`. */
|
|
2112
|
-
regions: Map<string,
|
|
2219
|
+
regions: Map<string, SubstituteRegion>;
|
|
2113
2220
|
/** Every `scale:` the atlas text declares, in the order the pages declare them. */
|
|
2114
2221
|
scales: number[];
|
|
2115
2222
|
}
|
|
2116
2223
|
|
|
2117
|
-
/**
|
|
2118
|
-
|
|
2119
|
-
|
|
2224
|
+
/**
|
|
2225
|
+
* Who reads a substituting atlas (issue #1020): `rigc` — `parseAtlasText` and
|
|
2226
|
+
* `./core/uvs.ts`'s `computeUvs`, for a candidate the core poses, so a rigc
|
|
2227
|
+
* build's `check --texture-from` reads nothing through spine-core — or
|
|
2228
|
+
* `spine`, the runtime's `TextureAtlas` and `MeshAttachment.computeUVs`, for
|
|
2229
|
+
* everything spine-core poses (an export keeps the reading it always had).
|
|
2230
|
+
*
|
|
2231
|
+
* The two were measured to agree: the region list (`TextureAtlas.regions` is
|
|
2232
|
+
* file order, and so is `parseAtlasText`'s, the pairing the selftest holds on
|
|
2233
|
+
* every corpus atlas) and the mapping (`computeUvs`, bit for bit in doubles
|
|
2234
|
+
* over 6,000 calls — its header). The PR of #1020 carries the substituted
|
|
2235
|
+
* frames, pixel for pixel, on the corpus rows that exercise it.
|
|
2236
|
+
*/
|
|
2237
|
+
export type SubstitutionReader = 'rigc' | 'spine';
|
|
2238
|
+
|
|
2239
|
+
/** Load an atlas and its pages as a substitution source, read by `reader` — see `SubstitutionReader`. */
|
|
2240
|
+
export function textureSubstitutionFromText(atlasText: string, atlasDir: string, reader: SubstitutionReader = 'spine'): TextureSubstitution {
|
|
2241
|
+
const regions = new Map<string, SubstituteRegion>();
|
|
2242
|
+
const names: string[] = [];
|
|
2243
|
+
if (reader === 'rigc') {
|
|
2244
|
+
for (const page of parseAtlasText(atlasText).pages) {
|
|
2245
|
+
names.push(page.name);
|
|
2246
|
+
const at = { name: page.name, width: page.width, height: page.height };
|
|
2247
|
+
for (const region of page.regions) {
|
|
2248
|
+
regions.set(regionKey(region), { page: at, x: region.x, y: region.y, width: region.width, height: region.height, degrees: region.degrees, pageUvs: (art) => computeUvs(region, at, art) });
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
} else {
|
|
2252
|
+
const atlas = new TextureAtlas(atlasText);
|
|
2253
|
+
for (const page of atlas.pages) names.push(page.name);
|
|
2254
|
+
for (const region of atlas.regions) {
|
|
2255
|
+
const page = { name: region.page.name, width: region.page.width, height: region.page.height };
|
|
2256
|
+
regions.set(regionKey(region), {
|
|
2257
|
+
page,
|
|
2258
|
+
x: region.x,
|
|
2259
|
+
y: region.y,
|
|
2260
|
+
width: region.width,
|
|
2261
|
+
height: region.height,
|
|
2262
|
+
degrees: region.degrees,
|
|
2263
|
+
pageUvs: (art) => {
|
|
2264
|
+
const uvs = new Array<number>(art.length).fill(0);
|
|
2265
|
+
MeshAttachment.computeUVs(region, [...art], uvs);
|
|
2266
|
+
return uvs;
|
|
2267
|
+
},
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2120
2271
|
const pages = new Map<string, Plate>();
|
|
2121
|
-
for (const
|
|
2122
|
-
if (
|
|
2123
|
-
throw new Error(`atlas page "${
|
|
2272
|
+
for (const name of names) {
|
|
2273
|
+
if (name.startsWith(SUBSTITUTE_PAGE)) {
|
|
2274
|
+
throw new Error(`atlas page "${name}" starts with the reserved prefix ${JSON.stringify(SUBSTITUTE_PAGE)}`);
|
|
2124
2275
|
}
|
|
2125
|
-
pages.set(SUBSTITUTE_PAGE +
|
|
2276
|
+
pages.set(SUBSTITUTE_PAGE + name, readPlate(join(atlasDir, name)));
|
|
2126
2277
|
}
|
|
2127
|
-
const regions = new Map<string, TextureAtlasRegion>();
|
|
2128
|
-
for (const region of atlas.regions) regions.set(regionKey(region), region);
|
|
2129
2278
|
return { pages, regions, scales: atlasScales(atlasText) };
|
|
2130
2279
|
}
|
|
2131
2280
|
|
|
@@ -2171,7 +2320,7 @@ export function atlasScales(atlasText: string): number[] {
|
|
|
2171
2320
|
* rectangle; two of them had it wrong at 270 (issue #579), so it is one function
|
|
2172
2321
|
* now and this is one of its callers.
|
|
2173
2322
|
*/
|
|
2174
|
-
function windowOf(region:
|
|
2323
|
+
function windowOf(region: SubstituteRegion): UvWindow {
|
|
2175
2324
|
const rect = pageFootprint(region);
|
|
2176
2325
|
const page = region.page;
|
|
2177
2326
|
return {
|
|
@@ -2227,17 +2376,16 @@ export function substituteTexture(
|
|
|
2227
2376
|
pieces.push(piece);
|
|
2228
2377
|
continue;
|
|
2229
2378
|
}
|
|
2230
|
-
|
|
2231
|
-
// `
|
|
2232
|
-
//
|
|
2233
|
-
//
|
|
2234
|
-
//
|
|
2235
|
-
|
|
2379
|
+
// The mapping from the drawing's coordinates into a page's, which is where
|
|
2380
|
+
// `rotate:` (all four of them) and the trim offsets are handled: spine-core's
|
|
2381
|
+
// own `MeshAttachment.computeUVs`, or `./core/uvs.ts`'s `computeUvs`, measured
|
|
2382
|
+
// equal to it bit for bit — never a third opinion about the atlas format
|
|
2383
|
+
// (`SubstitutionReader`).
|
|
2384
|
+
const uvs = region.pageUvs(texture.artUvs);
|
|
2236
2385
|
// A clipped piece's source map is re-seated the same way, through the drawing's own coordinates (`Mesh.source`).
|
|
2237
2386
|
if (piece.kind === 'mesh' && piece.source !== undefined) {
|
|
2238
2387
|
if (texture.sourceArtUvs === undefined) throw new Error(`slot "${piece.slot}": a clipped piece carries no original-art UVs for its source triangles`);
|
|
2239
|
-
const sourceUvs =
|
|
2240
|
-
MeshAttachment.computeUVs(region, texture.sourceArtUvs, sourceUvs);
|
|
2388
|
+
const sourceUvs = region.pageUvs(texture.sourceArtUvs);
|
|
2241
2389
|
pieces.push({ ...piece, page: SUBSTITUTE_PAGE + region.page.name, uvs, uvWindow: windowOf(region), source: { world: piece.source.world, uvs: sourceUvs } });
|
|
2242
2390
|
continue;
|
|
2243
2391
|
}
|
package/src/render_core.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* its raw entry to the renderer's shapes.
|
|
11
11
|
*
|
|
12
12
|
* ⭐ Why a module of its own rather than a second block in `src/render.ts`.
|
|
13
|
-
* `render.ts` links spine-core (
|
|
14
|
-
* `spinePoser`), so a core poser written there would share an import list
|
|
13
|
+
* `render.ts` links spine-core (an export's atlas pages and texture
|
|
14
|
+
* substitution, and `spinePoser`), so a core poser written there would share an import list
|
|
15
15
|
* with the runtime it replaces — and "the core poser imports nothing from
|
|
16
16
|
* spine-core" could then only be said, not read off the file. Here it can be:
|
|
17
17
|
* the value imports below are the core, the atlas reader and nothing else, and
|
|
@@ -278,8 +278,12 @@ interface CoreInput {
|
|
|
278
278
|
* The document posed with no skin set — spine-core's initial state, every slot
|
|
279
279
|
* resolved through the default skin alone — or refused by name where that
|
|
280
280
|
* equivalence was not measured (the header's *The skin*).
|
|
281
|
+
*
|
|
282
|
+
* Exported for the model side of the validator (issue #1025), which poses a
|
|
283
|
+
* slot the way `validate()` does — a fresh skeleton, no skin set — and must
|
|
284
|
+
* resolve that state by this rule rather than by a copy of it.
|
|
281
285
|
*/
|
|
282
|
-
function noSkinView(doc: CompiledDocument): CompiledDocument {
|
|
286
|
+
export function noSkinView(doc: CompiledDocument): CompiledDocument {
|
|
283
287
|
if (doc.skins.length === 0) return underSkin(doc, CORE_ALL_SKINS);
|
|
284
288
|
const fallback = doc.skins.find((k) => k.name === CORE_DEFAULT_SKIN);
|
|
285
289
|
if (fallback === undefined) {
|
|
@@ -662,18 +666,74 @@ export function corePoser(modelText: string, atlasText: string, where = 'skeleto
|
|
|
662
666
|
*
|
|
663
667
|
* Measured against the runtime's reading (`skinRosterOf`, a fresh skeleton's
|
|
664
668
|
* `active` under each skin and under none) on every rigc build the tree
|
|
665
|
-
* carries: the same bones, under every skin.
|
|
666
|
-
*
|
|
669
|
+
* carries: the same bones, under every skin. Since issue #1020 it is handed the
|
|
670
|
+
* document `coreDocumentFacts` read for the run's other facts, so the roster
|
|
671
|
+
* costs no reading of its own; a skin's view is still built only when a
|
|
672
|
+
* question is asked of it.
|
|
667
673
|
*/
|
|
668
|
-
|
|
669
|
-
let doc: CompiledDocument | null = null;
|
|
674
|
+
function coreSkinRoster(doc: CompiledDocument, skins: readonly string[]): SkinRoster {
|
|
670
675
|
return {
|
|
671
676
|
skins,
|
|
672
677
|
unposedUnder: (skin) => {
|
|
673
|
-
doc ??= readModel(modelText, where);
|
|
674
678
|
const view = skin === undefined ? noSkinView(doc) : underSkin(doc, skin);
|
|
675
679
|
const active = activeBones(view);
|
|
676
680
|
return unposedBones(view.bones.map((b) => ({ name: b.name, parent: b.parent ?? null, active: active.has(b.name) })));
|
|
677
681
|
},
|
|
678
682
|
};
|
|
679
683
|
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* What `render` and `check` read off a rigc build's model document besides the
|
|
687
|
+
* pose (issue #1020) — so a build the core poses draws with its
|
|
688
|
+
* `skeleton.atlas` gone, and reads off `skeleton.json` only what the document
|
|
689
|
+
* does not state.
|
|
690
|
+
*/
|
|
691
|
+
export interface CoreDocumentFacts {
|
|
692
|
+
/** The document's spec: `rigc-compiled/2` states where each region sits on its page, `rigc-compiled/1` does not. */
|
|
693
|
+
spec: string;
|
|
694
|
+
/**
|
|
695
|
+
* Every page the `pages` section states, by name, in file order — the
|
|
696
|
+
* images the draw samples, read by these names; `null` for a
|
|
697
|
+
* `rigc-compiled/1` document, whose pages only its atlas names.
|
|
698
|
+
*/
|
|
699
|
+
pageNames: readonly string[] | null;
|
|
700
|
+
/** The skin roster behind the core poser (`coreSkinRoster`), over this one reading. */
|
|
701
|
+
roster: SkinRoster;
|
|
702
|
+
/**
|
|
703
|
+
* The slot subset's roster (`subsetOver`): the slots in the document's draw
|
|
704
|
+
* order, its default skin, and the skins the document files a slot's
|
|
705
|
+
* attachments under. A refusal lists those skins, and the order it lists
|
|
706
|
+
* them in is the Spine file's, which the document does not hold
|
|
707
|
+
* (`./core/index.ts`, *Several skins filling one placeholder*) — so they are
|
|
708
|
+
* put in `skins`' order, the Spine file's own list.
|
|
709
|
+
*/
|
|
710
|
+
subset: SubsetRoster;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* The document's facts for `render` and `check` (`CoreDocumentFacts`), read
|
|
715
|
+
* once. `skins` is the Spine file's skin list, in its order — the one fact
|
|
716
|
+
* here the document does not state. Refused by `CoreInputError` where
|
|
717
|
+
* `readModel` refuses the document.
|
|
718
|
+
*/
|
|
719
|
+
export function coreDocumentFacts(modelText: string, where: string, skins: readonly string[]): CoreDocumentFacts {
|
|
720
|
+
const doc = readModel(modelText, where);
|
|
721
|
+
const rank = (name: string): number => {
|
|
722
|
+
const at = skins.indexOf(name);
|
|
723
|
+
return at < 0 ? skins.length : at;
|
|
724
|
+
};
|
|
725
|
+
return {
|
|
726
|
+
spec: doc.spec,
|
|
727
|
+
pageNames: doc.pages === null ? null : doc.pages.map((page) => page.name),
|
|
728
|
+
roster: coreSkinRoster(doc, skins),
|
|
729
|
+
subset: {
|
|
730
|
+
declared: doc.slots.map((s) => s.name),
|
|
731
|
+
carriers: (slot) =>
|
|
732
|
+
doc.skins
|
|
733
|
+
.filter((k) => Object.keys(k.attachments[slot] ?? {}).length > 0)
|
|
734
|
+
.map((k) => k.name)
|
|
735
|
+
.sort((a, b) => rank(a) - rank(b)),
|
|
736
|
+
defaultSkin: doc.skins.some((k) => k.name === CORE_DEFAULT_SKIN) ? CORE_DEFAULT_SKIN : null,
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
}
|
package/src/transform.ts
CHANGED
|
@@ -33,13 +33,38 @@
|
|
|
33
33
|
* ⭐ **`computeWorldTransforms` is the core's evaluator** (`worldTransforms` in
|
|
34
34
|
* [`src/core/world.ts`](core/world.ts), issue #1015), whose every rule — the five
|
|
35
35
|
* inherit modes, the collapsed parent axis, the operation orders — was measured
|
|
36
|
-
* against the runtime's pose dump and is held there by the core suite.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
36
|
+
* against the runtime's pose dump and is held there by the core suite. Since
|
|
37
|
+
* issue #1021 it runs under the runtime's own arithmetic (`RUNTIME_ARITHMETIC`,
|
|
38
|
+
* the evaluator's default), so the matrices every point is bound through are the
|
|
39
|
+
* ones the runtime poses the build with, to the bit. This file keeps what is the
|
|
40
|
+
* compiler's own: the coordinate contract, the refusals by name, the world
|
|
41
|
+
* rotation a region cancels, and the inverses.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ **Why the runtime's arithmetic and not the textbook's** (issue #1021). A
|
|
44
|
+
* point is bound by inverting a setup matrix; the runtime then poses it through
|
|
45
|
+
* its own. The two used to differ in the last bits — `Math.PI` against the
|
|
46
|
+
* runtime's 3.1415927, `[cos, −sin, sin, cos]` against the y column taken at
|
|
47
|
+
* `rotation + 90`, radians to degrees by division against multiplication — and
|
|
48
|
+
* that is not noise below the file's precision: an unrotated root reads
|
|
49
|
+
* `b = −2.3e-8` in the runtime, so a point 256 units above its bone was bound
|
|
50
|
+
* 5.9e-6 off before the float32 spelling was even applied. Measured from inside
|
|
51
|
+
* `compile` (every authored world position `toBoneLocal` was handed) against
|
|
52
|
+
* spine-core's pose of the emitted build, the old arithmetic put `gallery/look`'s
|
|
53
|
+
* 312 bound points up to 8.6e-5 from where they were authored (RMS 5.1e-5) and
|
|
54
|
+
* `gallery/flex`'s 258 up to 2.3e-5; the runtime's puts every one of them at the
|
|
55
|
+
* float32 floor — the distance the float32 spelling of the exact inverse alone
|
|
56
|
+
* accounts for (look 8.1e-6, flex 4.1e-6). `CO24` holds it.
|
|
57
|
+
*
|
|
58
|
+
* 🔸 **The rule has a second half: what is MEASURED reads the exact frame.**
|
|
59
|
+
* The runtime's arithmetic is for what is bound — the matrices a point is
|
|
60
|
+
* inverted through. Anything the compiler measures of the authored rig and
|
|
61
|
+
* reports, compares against a threshold, sorts by or refuses on is a statement
|
|
62
|
+
* about the spec, and reads `computeExactFrameTransforms` (below): a depth turn
|
|
63
|
+
* ceiling, a ring's control angles, a `segments` mesh's segments and falloff.
|
|
64
|
+
* An angle the author stated as 90 has to print as 90.
|
|
40
65
|
*/
|
|
41
66
|
import type { ModelBone } from './model.ts';
|
|
42
|
-
import { modeMatrix, worldTransforms, type CoreWorld, type WorldArithmetic } from './core/world.ts';
|
|
67
|
+
import { modeMatrix, RUNTIME_ARITHMETIC, worldTransforms, type CoreWorld, type WorldArithmetic } from './core/world.ts';
|
|
43
68
|
|
|
44
69
|
/**
|
|
45
70
|
* What `computeWorldTransforms` reads off a bone — a structural type, so both a
|
|
@@ -78,26 +103,6 @@ export interface BoneTransform {
|
|
|
78
103
|
|
|
79
104
|
export class TransformError extends Error {}
|
|
80
105
|
|
|
81
|
-
const DEG = Math.PI / 180;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* The arithmetic every figure rigc emits was computed with, handed to the core's
|
|
85
|
-
* evaluator (issue #1015). It differs from the runtime's in the last bit and in
|
|
86
|
-
* no matrix: degrees to radians with `Math.PI` rather than the runtime's
|
|
87
|
-
* 3.1415927, radians back to degrees by dividing by that factor, and a bone with
|
|
88
|
-
* the default scale and no shear framed as `lb = -sin`, `ld = cos` rather than
|
|
89
|
-
* `cos`/`sin` of `rot + 90°`. One bit can move a float32 spelling in the file, so
|
|
90
|
-
* this is a parameter and not a rewrite: measured with the runtime's arithmetic
|
|
91
|
-
* instead, 2 of the 19 public recipes moved by at most 4e-5. Whether the compiler
|
|
92
|
-
* should bind with the runtime's constant is a question of its own, not settled
|
|
93
|
-
* here.
|
|
94
|
-
*/
|
|
95
|
-
const COMPILER_ARITHMETIC: WorldArithmetic = {
|
|
96
|
-
radiansPerDegree: DEG,
|
|
97
|
-
degreesOf: (radians) => radians / DEG,
|
|
98
|
-
rotationOnlyFrame: true,
|
|
99
|
-
};
|
|
100
|
-
|
|
101
106
|
/**
|
|
102
107
|
* Crop pixels (y down, origin top-left) -> Spine world (y up, origin at the
|
|
103
108
|
* bottom-left of the crop).
|
|
@@ -113,6 +118,20 @@ export function cropToSpineY(cropY: number, cropHeight: number): number {
|
|
|
113
118
|
return cropHeight - cropY;
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The textbook's arithmetic — `Math.PI`, a bone with the default scale and no
|
|
123
|
+
* shear framed as `[cos, −sin, sin, cos]`, radians to degrees by dividing by
|
|
124
|
+
* the degree factor — in which an unrotated bone's frame is the identity
|
|
125
|
+
* exactly. Every point the compiler binds goes through the runtime's instead
|
|
126
|
+
* (the header's ⚠️); this is what `computeExactFrameTransforms` evaluates
|
|
127
|
+
* under, and nothing else.
|
|
128
|
+
*/
|
|
129
|
+
const EXACT_FRAME_ARITHMETIC: WorldArithmetic = {
|
|
130
|
+
radiansPerDegree: Math.PI / 180,
|
|
131
|
+
degreesOf: (radians) => radians / (Math.PI / 180),
|
|
132
|
+
rotationOnlyFrame: true,
|
|
133
|
+
};
|
|
134
|
+
|
|
116
135
|
/**
|
|
117
136
|
* World transform of every bone, in declaration order.
|
|
118
137
|
*
|
|
@@ -120,14 +139,44 @@ export function cropToSpineY(cropY: number, cropHeight: number): number {
|
|
|
120
139
|
* requires (it resolves `parent` by name against the bones already read), so a
|
|
121
140
|
* violation here is a violation there.
|
|
122
141
|
*
|
|
123
|
-
* The matrices and origins are the core's (`worldTransforms`, under
|
|
124
|
-
*
|
|
142
|
+
* The matrices and origins are the core's (`worldTransforms`, under the
|
|
143
|
+
* runtime's arithmetic — the header's ⚠️). What is decided here is what the core does not say:
|
|
125
144
|
* an undeclared parent and an `inherit` no mode answers to are refused by name
|
|
126
145
|
* as `TransformError`, a bone whose `parent` is empty is a root as it always was
|
|
127
146
|
* here, and `worldRotation` is read off the matrix — a root with the default
|
|
128
|
-
* scale and no shear keeps its stated rotation, unwrapped.
|
|
147
|
+
* scale and no shear keeps its stated rotation, unwrapped. The angle is turned
|
|
148
|
+
* into degrees as the runtime turns one (`RUNTIME_DEG`, by multiplying), so
|
|
149
|
+
* the rotation a region writes to cancel it is read back by the runtime as
|
|
150
|
+
* that angle.
|
|
129
151
|
*/
|
|
130
152
|
export function computeWorldTransforms(bones: readonly PosableBone[]): Map<string, BoneTransform> {
|
|
153
|
+
return evaluate(bones, RUNTIME_ARITHMETIC);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The same bones under `EXACT_FRAME_ARITHMETIC`: the frame the compiler
|
|
158
|
+
* MEASURES the authored rig in, and binds nothing through (issue #1021) — a
|
|
159
|
+
* mesh's depth turn ceiling (`sampleMeshDepth` in `compile.ts`), a ring's
|
|
160
|
+
* control angles (`ringControlAngles`, which orders the split and refuses a
|
|
161
|
+
* tie by printing the angle), and a `segments` mesh's segments, the distances
|
|
162
|
+
* its falloff and `minWeight` read and its shared-origin refusal. It differs from
|
|
163
|
+
* `computeWorldTransforms` only in the last bits, and that is exactly why it is
|
|
164
|
+
* kept (issue #1021): the ceiling is a statement about the authored mesh, and
|
|
165
|
+
* its reader treats an axis whose area term is exactly zero as one that cannot
|
|
166
|
+
* fold. The runtime frames an unrotated bone with `b = −2.3e-8`, so in its
|
|
167
|
+
* bind space a sheet whose depth rises linearly in x — which cannot fold a
|
|
168
|
+
* pitch at any angle — reads a pitch ceiling of 89.99997°, and `gallery/look`'s
|
|
169
|
+
* two hair locks gained four such ceilings with their counts (`TC03`, `TC06`
|
|
170
|
+
* went red on it). A control angle read through the runtime's frame printed
|
|
171
|
+
* an authored 90 as 89.99999734089101 in its tie refusal (`RF46`). On the 19
|
|
172
|
+
* recipes the only bytes this keeps from moving are look's ceilings; no
|
|
173
|
+
* emitted position goes through it.
|
|
174
|
+
*/
|
|
175
|
+
export function computeExactFrameTransforms(bones: readonly PosableBone[]): Map<string, BoneTransform> {
|
|
176
|
+
return evaluate(bones, EXACT_FRAME_ARITHMETIC);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function evaluate(bones: readonly PosableBone[], arithmetic: WorldArithmetic): Map<string, BoneTransform> {
|
|
131
180
|
const model: ModelBone[] = [];
|
|
132
181
|
const declared = new Set<string>();
|
|
133
182
|
for (const bone of bones) {
|
|
@@ -149,12 +198,12 @@ export function computeWorldTransforms(bones: readonly PosableBone[]): Map<strin
|
|
|
149
198
|
});
|
|
150
199
|
declared.add(bone.name);
|
|
151
200
|
}
|
|
152
|
-
const worlds = worldTransforms(model, null, modeMatrix,
|
|
201
|
+
const worlds = worldTransforms(model, null, modeMatrix, arithmetic);
|
|
153
202
|
const out = new Map<string, BoneTransform>();
|
|
154
203
|
for (const bone of model) {
|
|
155
204
|
const w = worlds.get(bone.name) as CoreWorld;
|
|
156
205
|
const plain = (bone.scaleX ?? 1) === 1 && (bone.scaleY ?? 1) === 1 && (bone.shearX ?? 0) === 0 && (bone.shearY ?? 0) === 0;
|
|
157
|
-
const worldRotation = bone.parent === undefined && plain ? (bone.rotation ?? 0) : (Math.atan2(w.c, w.a)
|
|
206
|
+
const worldRotation = bone.parent === undefined && plain ? (bone.rotation ?? 0) : (arithmetic.degreesOf(Math.atan2(w.c, w.a)) + 360) % 360;
|
|
158
207
|
out.set(bone.name, { a: w.a, b: w.b, c: w.c, d: w.d, worldX: w.worldX, worldY: w.worldY, worldRotation });
|
|
159
208
|
}
|
|
160
209
|
return out;
|