spine-rigc 1.6.2 → 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 +166 -60
- package/docs/AUTHORING.md +25 -16
- 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/atlas.ts +26 -16
- package/src/check.ts +70 -47
- package/src/compile.ts +118 -93
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +102 -15
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +80 -24
- package/src/deformmeasure.ts +383 -157
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +114 -13
- package/src/render.ts +520 -59
- package/src/render_core.ts +178 -21
- package/src/transform.ts +113 -128
- package/src/types.ts +6 -5
- package/src/validate.ts +200 -490
package/src/check.ts
CHANGED
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
BACKGROUND,
|
|
70
70
|
frameGeometry,
|
|
71
71
|
PROTOCOL_FPS,
|
|
72
|
-
|
|
72
|
+
loadCandidate,
|
|
73
73
|
renderFrame,
|
|
74
74
|
sampleAnimation,
|
|
75
75
|
sampleSetupPose,
|
|
@@ -83,11 +83,8 @@ import {
|
|
|
83
83
|
nonFinitePoseOf,
|
|
84
84
|
unframeableSentence,
|
|
85
85
|
UnframeablePoseError,
|
|
86
|
-
|
|
87
|
-
candidatePosers,
|
|
88
|
-
spinePoser,
|
|
86
|
+
refuseUnchosen,
|
|
89
87
|
throughPoser,
|
|
90
|
-
PoserChoiceError,
|
|
91
88
|
SHEET_COLUMNS,
|
|
92
89
|
SHEET_FILE,
|
|
93
90
|
SHEET_GAP,
|
|
@@ -95,9 +92,10 @@ import {
|
|
|
95
92
|
type Frame,
|
|
96
93
|
type FramesSidecar,
|
|
97
94
|
type FrameSet,
|
|
95
|
+
type MakeCorePoser,
|
|
96
|
+
type Posable,
|
|
98
97
|
type PoseOptions,
|
|
99
98
|
type Poser,
|
|
100
|
-
type PoserChoice,
|
|
101
99
|
type PoserName,
|
|
102
100
|
type TextureSubstitution,
|
|
103
101
|
type Viewport,
|
|
@@ -880,8 +878,12 @@ export interface TextureFromReport {
|
|
|
880
878
|
atlas: string;
|
|
881
879
|
/** Every `scale:` its text declares — the line that says a pack is coarser. */
|
|
882
880
|
scales: number[];
|
|
883
|
-
/**
|
|
884
|
-
|
|
881
|
+
/**
|
|
882
|
+
* The same for the CANDIDATE's own atlas, so the two are read side by side —
|
|
883
|
+
* `null` when the candidate has no atlas beside it to read them off (issue
|
|
884
|
+
* #1020), which is not the same claim as declaring none.
|
|
885
|
+
*/
|
|
886
|
+
candidateScales: number[] | null;
|
|
885
887
|
/**
|
|
886
888
|
* Regions the substituting atlas does not have, or the candidate packs rotated
|
|
887
889
|
* so its own corner order cannot be inverted — see `artUvsOf`. Those pieces kept
|
|
@@ -989,7 +991,13 @@ export interface CheckReport {
|
|
|
989
991
|
|
|
990
992
|
export interface CheckOptions {
|
|
991
993
|
skeletonText: string;
|
|
992
|
-
|
|
994
|
+
/**
|
|
995
|
+
* The candidate's atlas, or `null` when there is no file (issue #1020): a
|
|
996
|
+
* rigc build the core poses from a `rigc-compiled/2` document is drawn
|
|
997
|
+
* without one, and anything else is refused naming the file
|
|
998
|
+
* (`CandidateAtlasError`).
|
|
999
|
+
*/
|
|
1000
|
+
atlasText: string | null;
|
|
993
1001
|
/** Where the atlas's page paths resolve from. */
|
|
994
1002
|
atlasDir: string;
|
|
995
1003
|
framesDir: string;
|
|
@@ -1062,22 +1070,11 @@ export interface CheckOptions {
|
|
|
1062
1070
|
* What builds the core poser — `candidatePosers`' own `make`. The suite's
|
|
1063
1071
|
* `CH01` passes a planted copy, and nothing else passes any.
|
|
1064
1072
|
*/
|
|
1065
|
-
makeCorePoser?:
|
|
1073
|
+
makeCorePoser?: MakeCorePoser;
|
|
1066
1074
|
}
|
|
1067
1075
|
|
|
1068
|
-
/**
|
|
1069
|
-
|
|
1070
|
-
* `candidatePosers` over the candidate's files when the caller named them, and
|
|
1071
|
-
* spine-core alone when it handed texts only — there is then no directory to
|
|
1072
|
-
* find a model document in, and the reason says so.
|
|
1073
|
-
*/
|
|
1074
|
-
function checkPoserChoice(data: Parameters<typeof spinePoser>[0], options: CheckOptions): PoserChoice {
|
|
1075
|
-
const paths = options.candidatePaths;
|
|
1076
|
-
if (paths !== undefined) return candidatePosers(data, paths.skeleton, paths.atlas, options.poser, options.makeCorePoser);
|
|
1077
|
-
const why = 'the candidate was handed to check as text, with no path to find a skeleton.model.json beside';
|
|
1078
|
-
if (options.poser === 'core') throw new PoserChoiceError(`--poser core: ${why}`);
|
|
1079
|
-
return { forced: options.poser, core: null, why: options.poser === 'spine' ? '--poser spine' : why, spine: spinePoser(data) };
|
|
1080
|
-
}
|
|
1076
|
+
/** Why a candidate handed to `check` as text alone is posed through spine-core — there is no directory to find a model document in. */
|
|
1077
|
+
const UNPLACED_CANDIDATE = 'the candidate was handed to check as text, with no path to find a skeleton.model.json beside';
|
|
1081
1078
|
|
|
1082
1079
|
// ---------------------------------------------------------------------------
|
|
1083
1080
|
|
|
@@ -1145,14 +1142,27 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1145
1142
|
}
|
|
1146
1143
|
const notes: string[] = [];
|
|
1147
1144
|
|
|
1148
|
-
|
|
1145
|
+
// Which implementation poses the candidate (issue #968), chosen once for the
|
|
1146
|
+
// whole run — every set, the setup pose and the non-finite sentence — so no
|
|
1147
|
+
// two figures in one report can come from two posers: `candidatePaths` over
|
|
1148
|
+
// the candidate's files, as `render` chooses, and spine-core alone when it
|
|
1149
|
+
// was handed texts only. Chosen BEFORE anything is read off the candidate
|
|
1150
|
+
// (issue #1014), so a rigc build the core poses reads its names, its stage,
|
|
1151
|
+
// its bone tree and its pages without loading spine-core.
|
|
1152
|
+
const { choice, facts, pages } = loadCandidate(
|
|
1153
|
+
{ skeletonText: options.skeletonText, atlasText: options.atlasText, atlasDir: options.atlasDir, label: options.labels?.skeleton ?? 'the candidate' },
|
|
1154
|
+
options.candidatePaths ?? null,
|
|
1155
|
+
options.poser,
|
|
1156
|
+
{ ...(options.makeCorePoser === undefined ? {} : { make: options.makeCorePoser }), unplaced: UNPLACED_CANDIDATE },
|
|
1157
|
+
);
|
|
1158
|
+
const posable: Pick<Posable, 'pages'> = { pages };
|
|
1149
1159
|
// A candidate that declares no stage is framed exactly as one that does,
|
|
1150
1160
|
// because no framing here reads a stage: the world box is fitted from what the
|
|
1151
1161
|
// candidate draws. Said on the one candidate where a reader can ask what box
|
|
1152
1162
|
// stood in for the absent one (issue #714) — `SkeletonJson` copies the header
|
|
1153
1163
|
// fields across unconditionally (`SkeletonJson.js:70-73`), so an omitted
|
|
1154
|
-
// extent is `undefined`
|
|
1155
|
-
if (
|
|
1164
|
+
// extent is `undefined` there, not 0, and the header says the same.
|
|
1165
|
+
if (!facts.declaresStage) {
|
|
1156
1166
|
notes.push(
|
|
1157
1167
|
'the candidate declares no stage (no `skeleton.width`/`height`), and nothing stands in for one: its world ' +
|
|
1158
1168
|
'box is fitted from the pixels it draws, as it is for every candidate, so the absence moves no figure below.',
|
|
@@ -1161,24 +1171,27 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1161
1171
|
// The substitution is loaded before anything is posed, because posing has to
|
|
1162
1172
|
// record each piece's original-art UVs for it and that is the one thing about
|
|
1163
1173
|
// this measure that cannot be added afterwards — see `PieceTexture`.
|
|
1174
|
+
//
|
|
1175
|
+
// Read by rigc's own atlas reader when the core poses the candidate, and by
|
|
1176
|
+
// spine-core's when the runtime does (issue #1020, `SubstitutionReader`): a
|
|
1177
|
+
// rigc build's `--texture-from` reads nothing through the runtime, and an
|
|
1178
|
+
// export's reading is the one it always had.
|
|
1164
1179
|
const substitution = options.textureFrom
|
|
1165
|
-
? textureSubstitutionFromText(options.textureFrom.atlasText, options.textureFrom.atlasDir)
|
|
1180
|
+
? textureSubstitutionFromText(options.textureFrom.atlasText, options.textureFrom.atlasDir, choice.core === null ? 'spine' : 'rigc')
|
|
1166
1181
|
: null;
|
|
1167
1182
|
// The skin is refused here rather than deeper in the sampler, for the reason
|
|
1168
1183
|
// every miss in this project is refused where the names are: the skeleton is
|
|
1169
1184
|
// open on this line and the alternatives can be listed.
|
|
1170
|
-
if (options.skin !== undefined && !
|
|
1185
|
+
if (options.skin !== undefined && !facts.skins.includes(options.skin)) {
|
|
1171
1186
|
throw new CheckError(
|
|
1172
1187
|
`the candidate declares no skin ${JSON.stringify(options.skin)}; it declares [${
|
|
1173
|
-
|
|
1188
|
+
facts.skins.join(', ') || 'none'
|
|
1174
1189
|
}]`,
|
|
1175
1190
|
);
|
|
1176
1191
|
}
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
1179
|
-
|
|
1180
|
-
// input that cannot carry it is refused here, before anything is posed.
|
|
1181
|
-
const choice = checkPoserChoice(posable.data, options);
|
|
1192
|
+
// `--poser core` on an input that cannot carry it is refused here, before
|
|
1193
|
+
// anything is posed.
|
|
1194
|
+
refuseUnchosen(choice);
|
|
1182
1195
|
const poseOptions: PoseOptions | undefined =
|
|
1183
1196
|
substitution || options.skin !== undefined
|
|
1184
1197
|
? { ...(substitution ? { texture: true } : {}), ...(options.skin === undefined ? {} : { skin: options.skin }) }
|
|
@@ -1220,7 +1233,7 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1220
1233
|
sets = [
|
|
1221
1234
|
{
|
|
1222
1235
|
dir,
|
|
1223
|
-
animation:
|
|
1236
|
+
animation: facts.animations.length === 0 ? null : animation,
|
|
1224
1237
|
fps,
|
|
1225
1238
|
sampled: disk[disk.length - 1].index + 1,
|
|
1226
1239
|
written: disk.length,
|
|
@@ -1292,8 +1305,8 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1292
1305
|
// poser runs — the model document lists the spec's order, and a refusal that
|
|
1293
1306
|
// listed the names in another order would be a byte of the report that
|
|
1294
1307
|
// depends on the poser.
|
|
1295
|
-
const have =
|
|
1296
|
-
const posing = throughPoser(choice, (poser) => {
|
|
1308
|
+
const have = [...facts.animations];
|
|
1309
|
+
const posing = throughPoser(choice, (poser, roster) => {
|
|
1297
1310
|
const sampled = sets.map((set) => prepareSet(located.root, set, poser, have, options.as, poseOptions));
|
|
1298
1311
|
// 🔒 A pose that is not finite is refused before anything measures it (issue
|
|
1299
1312
|
// #873), in the words `render` and the geometry export use for it. Measured
|
|
@@ -1325,7 +1338,7 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1325
1338
|
posedSets.map((p) => p.frames),
|
|
1326
1339
|
poser.slots,
|
|
1327
1340
|
options.skin,
|
|
1328
|
-
|
|
1341
|
+
roster,
|
|
1329
1342
|
);
|
|
1330
1343
|
if (unframeable !== null) throw new UnframeablePoseError(unframeable);
|
|
1331
1344
|
return sampled;
|
|
@@ -1433,7 +1446,7 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1433
1446
|
throw new CheckError(
|
|
1434
1447
|
`no reference frame could be compared, so there is nothing to frame against${nothingToFrameWhy(
|
|
1435
1448
|
prepared,
|
|
1436
|
-
|
|
1449
|
+
[...facts.animations],
|
|
1437
1450
|
options.as,
|
|
1438
1451
|
located.sidecar !== null,
|
|
1439
1452
|
)}`,
|
|
@@ -1574,8 +1587,8 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1574
1587
|
// `src/chains.ts`. Reading the CANDIDATE's tree is what keeps this on the right
|
|
1575
1588
|
// side of the honesty rule: the reference is still nothing but pixels.
|
|
1576
1589
|
const chains = chainsOf(
|
|
1577
|
-
|
|
1578
|
-
|
|
1590
|
+
facts.bones.map(({ name, parent }) => ({ name, parent })),
|
|
1591
|
+
facts.slots.map(({ name, bone }) => ({ name, bone })),
|
|
1579
1592
|
);
|
|
1580
1593
|
const chainOfSlot = new Map<string, number>();
|
|
1581
1594
|
chains.forEach((chain, index) => {
|
|
@@ -1602,7 +1615,11 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1602
1615
|
);
|
|
1603
1616
|
}
|
|
1604
1617
|
|
|
1605
|
-
|
|
1618
|
+
// `null` when the candidate has no atlas to read them off (issue #1020): the
|
|
1619
|
+
// model document a build is drawn from without one states where each region
|
|
1620
|
+
// sits, not the `scale:` line, so the report says it was not read rather than
|
|
1621
|
+
// that none was declared.
|
|
1622
|
+
const candidateScales = options.atlasText === null ? null : atlasScales(options.atlasText);
|
|
1606
1623
|
if (substitution === null) {
|
|
1607
1624
|
// ⭐ Named at the top of every report rather than only when it is measured.
|
|
1608
1625
|
// Issue #171's finding was not that the floor was mis-measured; it was that
|
|
@@ -1612,7 +1629,13 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1612
1629
|
notes.push(
|
|
1613
1630
|
'part of every MAE below is TEXTURE, not animation, and it is not attributed here. The frames were rendered ' +
|
|
1614
1631
|
"through the reference's own atlas; this candidate samples its own" +
|
|
1615
|
-
`${
|
|
1632
|
+
`${
|
|
1633
|
+
candidateScales === null
|
|
1634
|
+
? ' (its atlas is not beside it, so whether that declares a scale: line is not read — the model document does not state one)'
|
|
1635
|
+
: candidateScales.length > 0
|
|
1636
|
+
? ` (declared at scale: ${[...new Set(candidateScales)].join(', ')})`
|
|
1637
|
+
: ''
|
|
1638
|
+
}` +
|
|
1616
1639
|
', and if the two were packed at different scales every edge of every part is filtered from a different ' +
|
|
1617
1640
|
'source in every frame. That difference is a constant no key can move and is invisible to the content box, ' +
|
|
1618
1641
|
'the fit residual and the whole-pixel refinement. Pass --texture-from <the atlas the frames were rendered ' +
|
|
@@ -2629,7 +2652,7 @@ function nothingToFrameWhy(
|
|
|
2629
2652
|
function checkOneSet(
|
|
2630
2653
|
root: string,
|
|
2631
2654
|
prepared: PreparedSet,
|
|
2632
|
-
posable:
|
|
2655
|
+
posable: Pick<Posable, 'pages'>,
|
|
2633
2656
|
framing: SetFraming,
|
|
2634
2657
|
background: RGBA,
|
|
2635
2658
|
chains: BoneChain[],
|
|
@@ -3310,7 +3333,7 @@ function labelBox(index: number): { width: number; height: number } {
|
|
|
3310
3333
|
function checkAgainstSheet(
|
|
3311
3334
|
root: string,
|
|
3312
3335
|
prepared: PreparedSet,
|
|
3313
|
-
posable:
|
|
3336
|
+
posable: Pick<Posable, 'pages'>,
|
|
3314
3337
|
viewport: Viewport,
|
|
3315
3338
|
background: RGBA,
|
|
3316
3339
|
): { sheet: SheetCheck | null; notes: string[] } {
|
|
@@ -3542,8 +3565,8 @@ export function checkLines(report: CheckReport, opts?: { allFrames?: boolean }):
|
|
|
3542
3565
|
}
|
|
3543
3566
|
if (report.textureFrom) {
|
|
3544
3567
|
const t = report.textureFrom;
|
|
3545
|
-
const scale = (values: number[]): string =>
|
|
3546
|
-
values.length === 0 ? 'no scale: line' : `scale: ${[...new Set(values)].join(', ')}`;
|
|
3568
|
+
const scale = (values: number[] | null): string =>
|
|
3569
|
+
values === null ? 'no atlas beside it to read a scale: line from' : values.length === 0 ? 'no scale: line' : `scale: ${[...new Set(values)].join(', ')}`;
|
|
3547
3570
|
lines.push(
|
|
3548
3571
|
` texture from ${t.atlas} (${scale(t.scales)}) against the candidate's own (${scale(t.candidateScales)})` +
|
|
3549
3572
|
`${t.unmatched.length === 0 ? '' : ` ⚠️ ${t.unmatched.length} region(s) not substituted`}`,
|