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/cli.ts
CHANGED
|
@@ -82,7 +82,8 @@ import { writeCheckPictures } from './src/checkpics.ts';
|
|
|
82
82
|
import { compile, CompileError, droppedStateReason, relativeImagesPath, type CompileOptions } from './src/compile.ts';
|
|
83
83
|
import { MODEL_DOCUMENT_FILE, modelDocument } from './src/model.ts';
|
|
84
84
|
import { deformReportBlock } from './src/deformreport.ts';
|
|
85
|
-
import { surveyOfBuild } from './src/deformmeasure.ts';
|
|
85
|
+
import { surveyOfBuild, type DeformSurvey } from './src/deformmeasure.ts';
|
|
86
|
+
import { CoreInputError } from './src/core/index.ts';
|
|
86
87
|
import {
|
|
87
88
|
diffLines,
|
|
88
89
|
diffSkeletons,
|
|
@@ -93,7 +94,7 @@ import {
|
|
|
93
94
|
} from './src/diff.ts';
|
|
94
95
|
import { ingest, IngestError, IngestSpecRefused, INGEST_GUTTERS, type IngestFinding, type IngestStage } from './src/ingest.ts';
|
|
95
96
|
import { NotAPngError } from './src/png.ts';
|
|
96
|
-
import { copyAtlasPages } from './src/emit.ts';
|
|
97
|
+
import { copyAtlasPages, plannedPageCopies } from './src/emit.ts';
|
|
97
98
|
import {
|
|
98
99
|
DEFAULT_PADDING,
|
|
99
100
|
DEFAULT_PAGE_EDGES,
|
|
@@ -141,10 +142,9 @@ import {
|
|
|
141
142
|
GEOMETRY_FILE,
|
|
142
143
|
GeometryError,
|
|
143
144
|
UnframeablePoseError,
|
|
144
|
-
skinRosterOf,
|
|
145
145
|
geometryFileOf,
|
|
146
146
|
geometryText,
|
|
147
|
-
|
|
147
|
+
loadCandidate,
|
|
148
148
|
PROTOCOL_FPS,
|
|
149
149
|
renderFrame,
|
|
150
150
|
sampleAll,
|
|
@@ -154,17 +154,17 @@ import {
|
|
|
154
154
|
SHEET_TILE,
|
|
155
155
|
sidecarViewport,
|
|
156
156
|
SlotSubsetError,
|
|
157
|
-
slotSubsetOf,
|
|
158
|
-
candidatePosers,
|
|
159
157
|
POSER_NAMES,
|
|
160
158
|
PoserChoiceError,
|
|
159
|
+
refuseUnchosen,
|
|
160
|
+
CandidateAtlasError,
|
|
161
|
+
SpineRuntimeError,
|
|
161
162
|
throughPoser,
|
|
162
163
|
type Frame,
|
|
163
164
|
type FramesSidecar,
|
|
164
165
|
type FrameSet,
|
|
165
|
-
type Posable,
|
|
166
|
-
type PoserChoice,
|
|
167
166
|
type PoserName,
|
|
167
|
+
type SkeletonFacts,
|
|
168
168
|
type SlotSubset,
|
|
169
169
|
} from './src/render.ts';
|
|
170
170
|
import {
|
|
@@ -456,10 +456,15 @@ function runGate(
|
|
|
456
456
|
modelText: string,
|
|
457
457
|
opts: CompileOptions,
|
|
458
458
|
profile: ValidateProfile,
|
|
459
|
+
/** The atlas text `build` writes for a compile's own — `--copy-images` renames the pages — which the second compile's document is spelled from, as `modelText` was (issue #1016). */
|
|
460
|
+
written: (atlasText: string) => string,
|
|
459
461
|
atlas?: AtlasOverride,
|
|
460
462
|
): number {
|
|
461
463
|
// The determinism check compares a second, independent compile — its model
|
|
462
|
-
// document included, which is the text `build` writes beside the pair
|
|
464
|
+
// document included, which is the text `build` writes beside the pair, and
|
|
465
|
+
// which states where each region sits in the atlas written with it (`pages`,
|
|
466
|
+
// issue #1016): so the second document is spelled from the second compile's
|
|
467
|
+
// atlas as it would be written, or from the second, independent pack.
|
|
463
468
|
const again = compile(opts);
|
|
464
469
|
const report = validate({
|
|
465
470
|
skeletonText: result.skeletonText,
|
|
@@ -467,7 +472,7 @@ function runGate(
|
|
|
467
472
|
atlasDir: opts.outDir,
|
|
468
473
|
declaredDurations: result.declaredDurations,
|
|
469
474
|
modelText,
|
|
470
|
-
reEmit: { skeletonText: again.skeletonText, atlasText: atlas ? atlas.again : again.atlasText, modelText: modelDocument(again.model, again.skeletonText) },
|
|
475
|
+
reEmit: { skeletonText: again.skeletonText, atlasText: atlas ? atlas.again : again.atlasText, modelText: modelDocument(again.model, again.skeletonText, atlas ? atlas.again : written(again.atlasText)) },
|
|
471
476
|
rig: result.rig,
|
|
472
477
|
profile,
|
|
473
478
|
});
|
|
@@ -860,11 +865,20 @@ function memberReportLines(result: CompileResult): string[] {
|
|
|
860
865
|
* an author who came here asking whether their deform broke the coverage
|
|
861
866
|
* deserves the answer rather than a silence. What does move is the stretch.
|
|
862
867
|
*/
|
|
863
|
-
function deformReportLines(result: CompileResult, exempt: ReadonlySet<string
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
|
|
868
|
+
function deformReportLines(result: CompileResult, exempt: ReadonlySet<string>, poser: PoserName | undefined, label: string): string[] {
|
|
869
|
+
// Read and posed through rigc's own core over the model document the build carries (issues #969, #1019) —
|
|
870
|
+
// spine-core is not touched — and through spine-core under `--poser spine` or when the core refuses the
|
|
871
|
+
// document, which the block then names; `tools/survey_hashes.ts` holds the two to one block. The block
|
|
872
|
+
// itself is `deformReportBlock` (`src/deformreport.ts`), so a control renders it off either reader's survey.
|
|
873
|
+
const input = { skeletonText: result.skeletonText, atlasText: result.atlasText, modelText: modelDocument(result.model, result.skeletonText, result.atlasText), label: `the deform survey of ${label}` };
|
|
874
|
+
let survey: DeformSurvey;
|
|
875
|
+
try {
|
|
876
|
+
survey = surveyOfBuild(input, new Set(), poser === undefined ? 'auto' : poser === 'core' ? 'model' : 'spine-core');
|
|
877
|
+
} catch (err) {
|
|
878
|
+
// `--poser core` on a build the core refuses: a refusal of the invocation, as `render` and `check` give it.
|
|
879
|
+
if (poser === 'core' && err instanceof CoreInputError) throw new ExplainError(`--poser core: the core refused to pose the deform survey's model document — ${err.message}`);
|
|
880
|
+
throw err;
|
|
881
|
+
}
|
|
868
882
|
return deformReportBlock(survey, result.deformTransforms, exempt);
|
|
869
883
|
}
|
|
870
884
|
|
|
@@ -1113,10 +1127,17 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1113
1127
|
|
|
1114
1128
|
// The model's document is spelled before the gate, so the text A18 compares
|
|
1115
1129
|
// is the text written, and a model the document cannot carry is refused
|
|
1116
|
-
// before anything is (issue #922).
|
|
1117
|
-
|
|
1130
|
+
// before anything is (issue #922). It states where each region sits in the
|
|
1131
|
+
// atlas written beside it (`pages`, issue #1016), so it is spelled from that
|
|
1132
|
+
// atlas: under `--copy-images` the text with the copies' page names, planned
|
|
1133
|
+
// here from the text alone (`plannedPageCopies`) and copied after the gate.
|
|
1134
|
+
// Under `--pack` the pages move again after this gate, and the document
|
|
1135
|
+
// written is the one the packed gate below spells and compares.
|
|
1136
|
+
const copying = flags['copy-images'] !== undefined;
|
|
1137
|
+
const writtenAtlas = (atlasText: string): string => (copying ? plannedPageCopies(atlasText, opts.outDir).atlasText : atlasText);
|
|
1138
|
+
let modelText = modelDocument(result.model, result.skeletonText, writtenAtlas(result.atlasText));
|
|
1118
1139
|
console.log(` .. validate (spine-core round trip + machine assertions, profile ${profile})`);
|
|
1119
|
-
const failures = runGate(result, modelText, opts, profile);
|
|
1140
|
+
const failures = runGate(result, modelText, opts, profile, writtenAtlas);
|
|
1120
1141
|
if (failures > 0) {
|
|
1121
1142
|
console.error(`rigc: ${failures} assertion(s) failed — nothing written`);
|
|
1122
1143
|
process.exit(1);
|
|
@@ -1139,6 +1160,10 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1139
1160
|
let atlasText = result.atlasText;
|
|
1140
1161
|
if (flags['copy-images'] !== undefined) {
|
|
1141
1162
|
const copied = copyAtlasPages(atlasText, opts.outDir);
|
|
1163
|
+
// The document's pages were spelled from the plan; the copy is held to it before anything of the pair is written.
|
|
1164
|
+
if (copied.atlasText !== writtenAtlas(result.atlasText)) {
|
|
1165
|
+
throw new Error(`internal: --copy-images wrote an atlas whose page names are not the ones ${MODEL_DOCUMENT_FILE} was spelled with — nothing of the pair was written`);
|
|
1166
|
+
}
|
|
1142
1167
|
atlasText = copied.atlasText;
|
|
1143
1168
|
console.log(` .. copy-images: ${copied.pages.length} page(s) copied into ${opts.outDir}`);
|
|
1144
1169
|
for (const p of copied.pages) {
|
|
@@ -1197,7 +1222,10 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1197
1222
|
})),
|
|
1198
1223
|
packOpts,
|
|
1199
1224
|
);
|
|
1200
|
-
|
|
1225
|
+
// The document written is spelled from the packed atlas, and this gate's A18 compares it with a
|
|
1226
|
+
// second compile's spelled from the second, independent pack (issue #1016).
|
|
1227
|
+
modelText = modelDocument(result.model, result.skeletonText, atlasText);
|
|
1228
|
+
const packFailures = runGate(result, modelText, opts, profile, (text) => text, { text: atlasText, again: packAgain.atlasText });
|
|
1201
1229
|
if (packFailures > 0) {
|
|
1202
1230
|
console.error(
|
|
1203
1231
|
`rigc: ${packFailures} assertion(s) failed on the PACKED atlas — the pages were written to ` +
|
|
@@ -1209,8 +1237,9 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1209
1237
|
|
|
1210
1238
|
writeFileSync(join(opts.outDir, 'skeleton.json'), result.skeletonText);
|
|
1211
1239
|
writeFileSync(join(opts.outDir, 'skeleton.atlas'), atlasText);
|
|
1212
|
-
// rigc's own record of the compiled rig (`rigc-compiled/
|
|
1213
|
-
//
|
|
1240
|
+
// rigc's own record of the compiled rig (`rigc-compiled/2`, issue #922;
|
|
1241
|
+
// its `pages` the atlas just written, issue #1016), written with the pair
|
|
1242
|
+
// and only after the same gate — under `--pack`, the packed one. rigc's own posing core
|
|
1214
1243
|
// reads it (`readModel`, `src/core/index.ts`, issue #380's step 2).
|
|
1215
1244
|
writeFileSync(join(opts.outDir, MODEL_DOCUMENT_FILE), modelText);
|
|
1216
1245
|
console.log(`rigc: wrote ${join(opts.outDir, 'skeleton.json')}`);
|
|
@@ -1445,16 +1474,16 @@ function runCheck(
|
|
|
1445
1474
|
flags: Record<string, string>,
|
|
1446
1475
|
plates?: CheckPlates,
|
|
1447
1476
|
): CheckReport {
|
|
1448
|
-
const { skeletonPath, atlasPath } =
|
|
1477
|
+
const { skeletonPath, atlasPath, atlasText } = resolveDrawable(candidate, atlasFlag);
|
|
1449
1478
|
// The poser is `render`'s choice, over the same two files (issue #968): the
|
|
1450
1479
|
// paths are what `candidatePosers` looks beside for `skeleton.model.json`.
|
|
1451
1480
|
const poser = readPoserFlag(flags);
|
|
1452
1481
|
return checkAgainstFrames({
|
|
1453
1482
|
skeletonText: readFileSync(skeletonPath, 'utf8'),
|
|
1454
|
-
atlasText
|
|
1483
|
+
atlasText,
|
|
1455
1484
|
atlasDir: dirname(atlasPath),
|
|
1456
1485
|
framesDir,
|
|
1457
|
-
labels: { skeleton: skeletonPath, atlas: atlasPath },
|
|
1486
|
+
labels: { skeleton: skeletonPath, atlas: atlasText === null ? `${atlasPath} — not there (${ATLAS_ABSENT})` : atlasPath },
|
|
1458
1487
|
candidatePaths: { skeleton: skeletonPath, atlas: atlasPath },
|
|
1459
1488
|
...(poser === undefined ? {} : { poser }),
|
|
1460
1489
|
...readCheckFlags(flags),
|
|
@@ -1558,6 +1587,43 @@ function resolveViewable(flags: Record<string, string>): {
|
|
|
1558
1587
|
return { skeletonPath, atlasPath, atlasDir: dirname(atlasPath) };
|
|
1559
1588
|
}
|
|
1560
1589
|
|
|
1590
|
+
/** What `render` and `check` say where a rigc build's atlas is not there (issue #1020). */
|
|
1591
|
+
const ATLAS_ABSENT = 'a build the core poses from a rigc-compiled/2 document needs none';
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* The candidate `render` and `check` draw, with its atlas text — or `null`
|
|
1595
|
+
* where the atlas file is not there and need not be (issue #1020).
|
|
1596
|
+
*
|
|
1597
|
+
* ⭐ A rigc build is drawn by the core from its `skeleton.model.json`, and a
|
|
1598
|
+
* `rigc-compiled/2` document states where each region sits on its page, so
|
|
1599
|
+
* the build's `skeleton.atlas` is not needed to draw it. Its absence is let
|
|
1600
|
+
* through here only where it can be that case — no `--atlas` named, a model
|
|
1601
|
+
* document beside the skeleton, and no atlas beside it at all — and the poser
|
|
1602
|
+
* choice decides the rest: anything that is read through an atlas after all
|
|
1603
|
+
* (a `rigc-compiled/1` document, a build the core refuses) is refused there
|
|
1604
|
+
* naming the file and why (`CandidateAtlasError`). An atlas that IS there is
|
|
1605
|
+
* read, as it always was: the core holds it to the document's `pages` and
|
|
1606
|
+
* draws through spine-core, saying why, when it is not the one the build
|
|
1607
|
+
* wrote (#1016), and `check` reads its `scale:` lines for the texture note.
|
|
1608
|
+
* Everywhere else this is `resolveViewable`'s refusal, word for word.
|
|
1609
|
+
*/
|
|
1610
|
+
function resolveDrawable(target: string, atlasFlag: string | undefined): { skeletonPath: string; atlasPath: string; atlasText: string | null } {
|
|
1611
|
+
const abs = resolve(target);
|
|
1612
|
+
if (atlasFlag === undefined && existsSync(abs)) {
|
|
1613
|
+
const directory = statSync(abs).isDirectory();
|
|
1614
|
+
const skeletonPath = directory ? join(abs, 'skeleton.json') : abs;
|
|
1615
|
+
const dir = dirname(skeletonPath);
|
|
1616
|
+
const atlasPath = join(dir, 'skeleton.atlas');
|
|
1617
|
+
const noAtlas = directory ? !existsSync(atlasPath) : abs.endsWith('.json') && !readdirSync(dir).some((f) => f.endsWith('.atlas'));
|
|
1618
|
+
if (noAtlas && existsSync(join(dir, MODEL_DOCUMENT_FILE))) {
|
|
1619
|
+
if (!existsSync(skeletonPath)) throw new UsageError(`nothing at ${skeletonPath}`);
|
|
1620
|
+
return { skeletonPath, atlasPath, atlasText: null };
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const { skeletonPath, atlasPath } = resolveViewable({ candidate: target, ...(atlasFlag === undefined ? {} : { atlas: atlasFlag }) });
|
|
1624
|
+
return { skeletonPath, atlasPath, atlasText: readFileSync(atlasPath, 'utf8') };
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1561
1627
|
/** `--animation`, checked against what the skeleton actually carries. */
|
|
1562
1628
|
function readAnimationFlag(flags: Record<string, string>, available: string[]): string | undefined {
|
|
1563
1629
|
const name = flags.animation;
|
|
@@ -1603,13 +1669,13 @@ function readSkinFlag(flags: Record<string, string>, declared: string[]): string
|
|
|
1603
1669
|
*/
|
|
1604
1670
|
function readSlotSubsetFlags(
|
|
1605
1671
|
flags: Record<string, string>,
|
|
1606
|
-
|
|
1672
|
+
facts: SkeletonFacts,
|
|
1607
1673
|
skin: string | undefined,
|
|
1608
1674
|
): SlotSubset | undefined {
|
|
1609
1675
|
const list = (raw: string | undefined): string[] | undefined =>
|
|
1610
1676
|
raw === undefined ? undefined : raw.split(',').map((name) => name.trim()).filter((name) => name !== '');
|
|
1611
1677
|
try {
|
|
1612
|
-
return
|
|
1678
|
+
return facts.subset({ slots: list(flags.slot), hidden: list(flags.hide) }, skin);
|
|
1613
1679
|
} catch (err) {
|
|
1614
1680
|
if (err instanceof SlotSubsetError) throw new UsageError(err.message);
|
|
1615
1681
|
throw err;
|
|
@@ -1617,16 +1683,12 @@ function readSlotSubsetFlags(
|
|
|
1617
1683
|
}
|
|
1618
1684
|
|
|
1619
1685
|
/**
|
|
1620
|
-
* `--poser
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1686
|
+
* `--poser` as spelled, checked against the posers there are — shared by
|
|
1687
|
+
* `render` and `check`. On `render` it is read where its refusal has always
|
|
1688
|
+
* stood, after the candidate's own flags; the candidate is loaded before that
|
|
1689
|
+
* with the spelling as given (`posersAsked`), so a rigc build the core poses
|
|
1690
|
+
* is chosen without loading spine-core (issue #1014).
|
|
1624
1691
|
*/
|
|
1625
|
-
function readPoserChoice(flags: Record<string, string>, data: Posable['data'], skeletonPath: string, atlasPath: string): PoserChoice {
|
|
1626
|
-
return candidatePosers(data, skeletonPath, atlasPath, readPoserFlag(flags));
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
/** `--poser` as spelled, checked against the posers there are — shared by `render` and `check`. */
|
|
1630
1692
|
function readPoserFlag(flags: Record<string, string>): PoserName | undefined {
|
|
1631
1693
|
const raw = flags.poser;
|
|
1632
1694
|
const forced = POSER_NAMES.find((name) => name === raw);
|
|
@@ -1636,6 +1698,11 @@ function readPoserFlag(flags: Record<string, string>): PoserName | undefined {
|
|
|
1636
1698
|
return forced;
|
|
1637
1699
|
}
|
|
1638
1700
|
|
|
1701
|
+
/** `--poser` as the candidate is loaded with: a known spelling, or the input's own choice — the spelling is refused later, by `readPoserFlag`. */
|
|
1702
|
+
function posersAsked(flags: Record<string, string>): PoserName | undefined {
|
|
1703
|
+
return POSER_NAMES.find((name) => name === flags.poser);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1639
1706
|
/** A resolved subset as the one field it is spelled as, in `PoseOptions` and in `frames.json` alike. */
|
|
1640
1707
|
function subsetFields(subset: SlotSubset | undefined): { slots?: string[]; hidden?: string[] } {
|
|
1641
1708
|
if (subset === undefined) return {};
|
|
@@ -1653,8 +1720,10 @@ function readPositiveNumber(flags: Record<string, string>, key: string, fallback
|
|
|
1653
1720
|
/**
|
|
1654
1721
|
* Does this skeleton declare a setup stage — a numeric width AND height?
|
|
1655
1722
|
*
|
|
1656
|
-
* Read off the
|
|
1657
|
-
*
|
|
1723
|
+
* Read off the file's header for `preview`, which never loads one; `render`
|
|
1724
|
+
* reads the same statement off its candidate (`SkeletonFacts.declaresStage`:
|
|
1725
|
+
* the header again on a rigc build the core poses, the loaded `SkeletonData`
|
|
1726
|
+
* on an export). Both are the same two fields, because
|
|
1658
1727
|
* `SkeletonJson` copies them across unconditionally (`SkeletonJson.js:70-73`),
|
|
1659
1728
|
* so a header that omits them leaves `undefined` on a field typed `number`.
|
|
1660
1729
|
*/
|
|
@@ -1699,7 +1768,11 @@ const STAGELESS_FRAMING = {
|
|
|
1699
1768
|
* two rates — land on one pixel grid and stay comparable.
|
|
1700
1769
|
*/
|
|
1701
1770
|
function cmdRender(flags: Record<string, string>): void {
|
|
1702
|
-
|
|
1771
|
+
if (flags.candidate === undefined) {
|
|
1772
|
+
throw new UsageError('needs --candidate <dir | skeleton.json> — the directory `build --out` wrote');
|
|
1773
|
+
}
|
|
1774
|
+
const { skeletonPath, atlasPath, atlasText } = resolveDrawable(flags.candidate, flags.atlas);
|
|
1775
|
+
const atlasDir = dirname(atlasPath);
|
|
1703
1776
|
const fps = readPositiveNumber(flags, 'fps', PROTOCOL_FPS, 1);
|
|
1704
1777
|
const maxSide = readPositiveNumber(flags, 'max', 256, 16);
|
|
1705
1778
|
const outRoot = resolve(flags.out ?? 'render');
|
|
@@ -1724,11 +1797,20 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1724
1797
|
|
|
1725
1798
|
console.log('rigc render');
|
|
1726
1799
|
console.log(` .. skeleton ${skeletonPath}`);
|
|
1727
|
-
console.log(` .. atlas ${atlasPath}`);
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1800
|
+
console.log(` .. atlas ${atlasPath}${atlasText === null ? ` — not there (${ATLAS_ABSENT})` : ''}`);
|
|
1801
|
+
// Which implementation of the posing seam draws this (issue #968), chosen
|
|
1802
|
+
// before anything is read off the candidate (issue #1014): a rigc build the
|
|
1803
|
+
// core poses — `skeleton.model.json` beside the pair — reads its names, its
|
|
1804
|
+
// subset roster and its pages without loading spine-core at all, and with a
|
|
1805
|
+
// rigc-compiled/2 document without its atlas either (issue #1020).
|
|
1806
|
+
const { choice, facts, pages } = loadCandidate(
|
|
1807
|
+
{ skeletonText: readFileSync(skeletonPath, 'utf8'), atlasText, atlasDir, label: skeletonPath },
|
|
1808
|
+
{ skeleton: skeletonPath, atlas: atlasPath },
|
|
1809
|
+
posersAsked(flags),
|
|
1810
|
+
);
|
|
1811
|
+
const only = readAnimationFlag(flags, [...facts.animations]);
|
|
1812
|
+
const skin = readSkinFlag(flags, [...facts.skins]);
|
|
1813
|
+
const subset = readSlotSubsetFlags(flags, facts, skin);
|
|
1732
1814
|
// One object, so the framing and the frames cannot be posed under two
|
|
1733
1815
|
// different skins — which would frame one shot with another shot's box.
|
|
1734
1816
|
// Not annotated `PoseOptions`: that name is `src/pose.ts`'s in this file, and
|
|
@@ -1742,23 +1824,25 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1742
1824
|
if (skin !== undefined) console.log(` .. skin ${skin}`);
|
|
1743
1825
|
if (subset !== undefined) console.log(` .. ${subset.mode.padEnd(8)} ${subset.names.join(', ')}`);
|
|
1744
1826
|
|
|
1745
|
-
//
|
|
1746
|
-
//
|
|
1747
|
-
//
|
|
1748
|
-
//
|
|
1749
|
-
|
|
1827
|
+
// Rigc's own core when the candidate is a rigc build, and spine-core
|
|
1828
|
+
// otherwise, or where the core refuses the input by name. Never silently:
|
|
1829
|
+
// the `poser` line below says which, and why. `--poser` is refused here, where
|
|
1830
|
+
// it always was: a spelling there is no poser for, then `core` on an input
|
|
1831
|
+
// that cannot carry it.
|
|
1832
|
+
readPoserFlag(flags);
|
|
1833
|
+
refuseUnchosen(choice);
|
|
1750
1834
|
|
|
1751
1835
|
// Everything is posed before anything is written, so a core refusal partway
|
|
1752
1836
|
// re-poses the whole input on spine-core rather than leaving half of a frame
|
|
1753
1837
|
// set drawn by each.
|
|
1754
|
-
const posed = throughPoser(choice, (poser) => {
|
|
1838
|
+
const posed = throughPoser(choice, (poser, roster) => {
|
|
1755
1839
|
// `null` is a skeleton that posed no vertex at all. One that posed a vertex
|
|
1756
1840
|
// it cannot frame — Infinity or NaN — is thrown from the framing as a
|
|
1757
1841
|
// `GeometryError` naming the number (issue #873), and one whose every vertex
|
|
1758
1842
|
// sits at one point — every drawn bone unposed by the skin, or collapsed — as
|
|
1759
1843
|
// an `UnframeablePoseError` (issue #997); neither reaches this. The roster is what
|
|
1760
1844
|
// lets the second name the skins that pose a bone, whichever poser draws.
|
|
1761
|
-
const viewport = framingViewport(poser, maxSide, pose,
|
|
1845
|
+
const viewport = framingViewport(poser, maxSide, pose, roster);
|
|
1762
1846
|
if (!viewport) {
|
|
1763
1847
|
throw new UsageError(
|
|
1764
1848
|
`${skeletonPath} posed no drawable attachment in any animation or in its setup pose${
|
|
@@ -1784,7 +1868,7 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1784
1868
|
const geometryTexts = new Map<string, string>();
|
|
1785
1869
|
if (geometry) {
|
|
1786
1870
|
for (const [name, frames] of sampled) {
|
|
1787
|
-
const animation = name === SETUP_POSE_DIR &&
|
|
1871
|
+
const animation = name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name;
|
|
1788
1872
|
geometryTexts.set(name, geometryText(geometryFileOf(poser, animation, fps, frames, viewport, skin)));
|
|
1789
1873
|
}
|
|
1790
1874
|
}
|
|
@@ -1793,7 +1877,7 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1793
1877
|
const { viewport, sampled, geometryTexts } = posed.value;
|
|
1794
1878
|
console.log(` .. poser ${posed.note}`);
|
|
1795
1879
|
console.log(` .. ${viewport.width}x${viewport.height}px at ${fps} fps, ${sampled.size} set(s) -> ${outRoot}`);
|
|
1796
|
-
if (!
|
|
1880
|
+
if (!facts.declaresStage) console.log(` .. ${STAGELESS_FRAMING.render}`);
|
|
1797
1881
|
|
|
1798
1882
|
mkdirSync(outRoot, { recursive: true });
|
|
1799
1883
|
const sets: FrameSet[] = [];
|
|
@@ -1819,7 +1903,7 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1819
1903
|
const duration = frames[frames.length - 1].time;
|
|
1820
1904
|
sets.push({
|
|
1821
1905
|
dir: dirName,
|
|
1822
|
-
animation: name === SETUP_POSE_DIR &&
|
|
1906
|
+
animation: name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name,
|
|
1823
1907
|
fps,
|
|
1824
1908
|
sampled: frames.length,
|
|
1825
1909
|
written: frames.length,
|
|
@@ -2716,9 +2800,12 @@ function cmdBoneDist(flags: Record<string, string>): void {
|
|
|
2716
2800
|
/**
|
|
2717
2801
|
* Refuse a compiled pair whose art `explain` cannot pose through, by name.
|
|
2718
2802
|
*
|
|
2719
|
-
* 🚨 `explain` poses the rig — `deformReportLines`
|
|
2720
|
-
*
|
|
2721
|
-
*
|
|
2803
|
+
* 🚨 `explain` poses the rig — `deformReportLines` measures what each deform key
|
|
2804
|
+
* did — and spine-core's reading of the pair resolves EVERY attachment against
|
|
2805
|
+
* the atlas, whether or not anything deforms it. (Since issue #1019 a build's
|
|
2806
|
+
* survey is read off its model document and posed by the core, which resolves
|
|
2807
|
+
* nothing against the atlas; spine-core reads it under `--poser spine` and on a
|
|
2808
|
+
* fallback, and this refusal is unchanged and comes before either.) On the specs
|
|
2722
2809
|
* `ingest --art none` writes there is nothing to resolve against: the entries
|
|
2723
2810
|
* state a size and name no `image`, so the compile atlases nothing, and the load
|
|
2724
2811
|
* threw the runtime's own `Region not found in atlas: rear-upper-arm (attachment:
|
|
@@ -2789,6 +2876,8 @@ function refuseUnposableArt(result: CompileResult, opts: CompileOptions): void {
|
|
|
2789
2876
|
}
|
|
2790
2877
|
|
|
2791
2878
|
function cmdExplain(flags: Record<string, string>): void {
|
|
2879
|
+
// `--poser` is refused by its spelling before anything compiles (issue #1019): it chooses the deform survey's reader and poser.
|
|
2880
|
+
const poser = readPoserFlag(flags);
|
|
2792
2881
|
const { label, opts } = resolveCut(flags);
|
|
2793
2882
|
console.log(`rigc explain ${label}`);
|
|
2794
2883
|
// See the identical pair of lines in `cmdBuild` for why both paths are named
|
|
@@ -3044,7 +3133,7 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
3044
3133
|
// The `DEFORM` block goes after the timelines and before the constraints,
|
|
3045
3134
|
// because it is a measurement OF the deform timelines printed above — the keys
|
|
3046
3135
|
// it names are the keys the reader has just read, by the same index.
|
|
3047
|
-
for (const line of deformReportLines(result, new Set(result.rig.deformMayFold))) console.log(line);
|
|
3136
|
+
for (const line of deformReportLines(result, new Set(result.rig.deformMayFold), poser, label)) console.log(line);
|
|
3048
3137
|
|
|
3049
3138
|
if (result.physics.length) {
|
|
3050
3139
|
console.log('\nphysics constraints (4.3 top-level `constraints` array, type per entry)');
|
|
@@ -3560,7 +3649,9 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
3560
3649
|
"`render` and `check`: which implementation poses the frames (on `check`, the candidate's) — `core` (rigc's own, reading the skeleton.model.json a " +
|
|
3561
3650
|
'build writes beside the pair) or `spine` (spine-core). Default: `core` when that document and the atlas sit ' +
|
|
3562
3651
|
"beside the skeleton and the skeleton is the one the document records (spine.sha256), `spine` otherwise and wherever the core refuses the input by name; the `poser` " +
|
|
3563
|
-
'line of the render or the check report says which and why. `--poser core` on an input that cannot carry it is refused by name'
|
|
3652
|
+
'line of the render or the check report says which and why. `--poser core` on an input that cannot carry it is refused by name. ' +
|
|
3653
|
+
"`explain`: which reads and poses the DEFORM block's survey — `core` (the model document the compile writes, spine-core untouched) or " +
|
|
3654
|
+
'`spine` (the Spine skeleton parsed and posed by spine-core); default `core`, and `spine` where the core refuses the document, which the block names',
|
|
3564
3655
|
'texture-from':
|
|
3565
3656
|
"also measure this run through this atlas's texels, keeping the candidate's own geometry, and report how much " +
|
|
3566
3657
|
'of the MAE is texture resampling rather than the rig — pass the atlas the reference frames were rendered ' +
|
|
@@ -3775,11 +3866,11 @@ const COMMANDS: CommandDoc[] = [
|
|
|
3775
3866
|
{
|
|
3776
3867
|
name: 'explain',
|
|
3777
3868
|
usage: [
|
|
3778
|
-
'rigc explain --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] (it never gates, and writes nothing)',
|
|
3869
|
+
'rigc explain --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--poser core|spine] (it never gates, and writes nothing)',
|
|
3779
3870
|
'rigc explain … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made, as build does)',
|
|
3780
3871
|
'rigc explain --cut <name> --cuts <cuts.json>',
|
|
3781
3872
|
],
|
|
3782
|
-
flags: ['rig', 'motion', 'out', 'manifest', 'images', 'atlas-in', 'cut', 'cuts'],
|
|
3873
|
+
flags: ['rig', 'motion', 'out', 'manifest', 'images', 'atlas-in', 'cut', 'cuts', 'poser'],
|
|
3783
3874
|
notes: [
|
|
3784
3875
|
'this line said "the same arguments as build, minus --profile" and was false in both',
|
|
3785
3876
|
'directions (issue #697): --atlas-in was not listed here, so the one flag that lets this',
|
|
@@ -4281,6 +4372,21 @@ try {
|
|
|
4281
4372
|
console.error(`rigc: ${err.message}`);
|
|
4282
4373
|
process.exit(1);
|
|
4283
4374
|
}
|
|
4375
|
+
// An input posed through spine-core — a Spine export, `--poser spine`, a
|
|
4376
|
+
// fallback — on a run where the runtime cannot be used (issue #1014). The
|
|
4377
|
+
// sentence names the input and why it needs the runtime; exit 1, like a file
|
|
4378
|
+
// that is not a PNG: the invocation was fine and the run could not pose it.
|
|
4379
|
+
// A candidate with no atlas beside it that has to be read through one (issue
|
|
4380
|
+
// #1020): a refusal of the invocation like a missing atlas on an export, exit
|
|
4381
|
+
// 2 and nothing written, and the message names the file and why it is needed.
|
|
4382
|
+
if (err instanceof CandidateAtlasError) {
|
|
4383
|
+
console.error(`rigc ${command}: ${err.message}`);
|
|
4384
|
+
process.exit(2);
|
|
4385
|
+
}
|
|
4386
|
+
if (err instanceof SpineRuntimeError) {
|
|
4387
|
+
console.error(`rigc ${command}: ${err.message}`);
|
|
4388
|
+
process.exit(1);
|
|
4389
|
+
}
|
|
4284
4390
|
// An install refused before its first write (issue #831): the invocation was
|
|
4285
4391
|
// fine and an entry on disk was not what this command would write, so exit 1
|
|
4286
4392
|
// like a file that is not a PNG. The message names every such entry, what is
|
package/docs/AUTHORING.md
CHANGED
|
@@ -195,7 +195,7 @@ What the flags mean:
|
|
|
195
195
|
| --- | --- |
|
|
196
196
|
| `--rig` | the rig spec — skeleton structure |
|
|
197
197
|
| `--motion` | the motion spec — time |
|
|
198
|
-
| `--out` | directory for `skeleton.json` + `skeleton.atlas`, and on `build` `skeleton.model.json` beside them; atlas page paths and `skeleton.images` are written relative to it. `skeleton.model.json` is rigc's own record of the compiled rig (`rigc-compiled/
|
|
198
|
+
| `--out` | directory for `skeleton.json` + `skeleton.atlas`, and on `build` `skeleton.model.json` beside them; atlas page paths and `skeleton.images` are written relative to it. `skeleton.model.json` is rigc's own record of the compiled rig (`rigc-compiled/2`: the bones, slots, skins, constraints, events and animations the Spine files were emitted from, in the spec's order, and the rest of what posing them takes; then `pages`, every page of the `skeleton.atlas` written beside it and where each region sits on it — after `--pack` the packed pages, after `--copy-images` the copies' names), written after the same gate as the pair and ending in `spine.sha256`, the digest of the `skeleton.json` written beside it. A `rigc-compiled/1` document (rigc 1.6) has no `pages` and is still read: the core then takes the placement from the atlas beside it, the `poser` line says so, and with no atlas there `render` and `check` refuse it naming the file. A `rigc-compiled/2` build is rendered and checked with its `skeleton.atlas` gone (`--candidate` below). It is what rigc's own posing core (`src/core/`) poses: `render` and `check` — and `bench --frames`, which folds `check` in — pose a build through it, and `--poser` below says when they do not. On `check` it is a directory of **pictures** instead: one per frame the table lists (every compared frame under `--all-frames`), reference · candidate · difference · overlay at the comparison grid's native size, as `<dir>/<set>/f####.png` beside a `frames.json` that says what they are pictures of. Each `<dir>/<set>/` is cleared first; a file at `<dir>`, or a directory that is or holds `--frames`, is refused by name — **§9.2.1** |
|
|
199
199
|
| `--copy-images` | `build` only: also copies every page **the emitted atlas names** into `--out` and rewrites the atlas to the copies, so the directory is self-contained enough to zip or commit on its own, and points `skeleton.images` at `--out` itself so the editor's import finds the parts beside the skeleton (§3.1 says why it is spelled `../<out>/` and not `./`). Under `--atlas-in` those pages are the pack's, not one per part (**§0.2**). Without it, page paths point at the source art |
|
|
200
200
|
| `--pack` | `build` only: arrange every part onto **shared** atlas page(s), written into `--out` as real PNGs, instead of one page per part. Lossless — nothing is resampled, trimmed or rotated. The default is one page per part — **§0.1** |
|
|
201
201
|
| `--page-size` | `build --pack` only: the largest page edge (default `2048`). A ceiling, not the size: page edges are powers of two and the one written is the smallest that holds the pack — **§0.1** |
|
|
@@ -207,10 +207,10 @@ What the flags mean:
|
|
|
207
207
|
| `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
|
|
208
208
|
| `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
|
|
209
209
|
| `--profile` | `spine` = the 34 validity rules (**the default**) · `spine-html` = all 49, opt-in |
|
|
210
|
-
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
210
|
+
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. With the atlas gone, `check`'s texture note says whether it declares a `scale:` line is not read (the document does not state one), and `check.json`'s `textureFrom.candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
211
211
|
| `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
|
|
212
212
|
| `--geometry` | `render` only: also write a `geometry.json` into each frame directory — per frame, every bone's world transform and every slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry and topology, on the frames' own grid and viewport. Every other file is byte for byte what the render writes without it. Refused with `--slot`/`--hide`, because the geometry is the whole pose whatever is drawn — **§8.2** |
|
|
213
|
-
| `--poser` | `render` and `
|
|
213
|
+
| `--poser` | `render`, `check` and `explain`: `core` or `spine` — on `render` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core. Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `the "default" skin names` (no `--skin`, and the default skin names a skin-required bone or constraint, each named) · `and no "default" one` (no `--skin`, and skins with none named `default` — spine-core draws nothing there either, so the render stops at `there is nothing to draw`, exit 2, and only `--poser core` prints this refusal) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; the order its animations and skins are listed in, and its stage, off its own `skeleton.json`, because the document states neither; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`). On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1) |
|
|
214
214
|
| `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
|
|
215
215
|
| `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
|
|
216
216
|
| `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
|
|
@@ -231,7 +231,9 @@ What the flags mean:
|
|
|
231
231
|
`render` also takes `--fps <n>` (the rate it samples at, default 12 — the same
|
|
232
232
|
protocol rate the reference frames use) and `--max <px>` (the long side of a
|
|
233
233
|
frame, default 256). A clipping attachment is applied as the runtime applies it —
|
|
234
|
-
spine-core's own `SkeletonClipping`, in spine-webgl's call order
|
|
234
|
+
spine-core's own `SkeletonClipping`, in spine-webgl's call order, on a Spine export,
|
|
235
|
+
and the core's clipper, drawing the same pixels, on a rigc build (the `--poser`
|
|
236
|
+
row) — so a slot from
|
|
235
237
|
the clip's through its `end` slot draws only what falls inside the polygon, in
|
|
236
238
|
`render`, `check` and every other picture rigc's own rasteriser draws (`preview`
|
|
237
239
|
plays in Spine's player, which always clipped); the framing box still counts
|
|
@@ -931,12 +933,16 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
|
|
|
931
933
|
|
|
932
934
|
⭐ **It reads what `ingest` writes, given the pack** — `--atlas-in <pack.atlas>`,
|
|
933
935
|
with `build`'s meaning (§0.2). That matters more here than it looks: printing the
|
|
934
|
-
`DEFORM` block means **posing** the rig,
|
|
935
|
-
against the atlas whether or not anything deforms it, and a spec
|
|
936
|
-
`ingest --art none` states sizes and names no image. So the
|
|
937
|
-
`build --atlas-in` gates green is readable here through the same
|
|
938
|
-
without it the pair is refused by name at exit 2 rather than posed
|
|
939
|
-
(§5.1). ⚠️
|
|
936
|
+
`DEFORM` block means **posing** the rig, spine-core's pose resolves *every*
|
|
937
|
+
attachment against the atlas whether or not anything deforms it, and a spec
|
|
938
|
+
written by `ingest --art none` states sizes and names no image. So the
|
|
939
|
+
size-only pair `build --atlas-in` gates green is readable here through the same
|
|
940
|
+
flag, and without it the pair is refused by name at exit 2 rather than posed
|
|
941
|
+
(§5.1). ⚠️ Since issue #1019 the block's survey on a build is read off the
|
|
942
|
+
model document and posed by rigc's own core, which resolves nothing against the
|
|
943
|
+
atlas (`--poser` in the flag table); the refusal is unchanged and still comes
|
|
944
|
+
first, so a size-only pair needs `--atlas-in` here whichever reader the survey
|
|
945
|
+
takes. ⚠️ `--profile`,
|
|
940
946
|
`--pack`, `--page-size`, `--padding`, `--page-edges` and `--copy-images` are
|
|
941
947
|
`build`'s and are not here: five of them decide what is *written*, and this
|
|
942
948
|
command writes nothing.
|
|
@@ -5751,9 +5757,11 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
5751
5757
|
| `skin "S" slot "X" attachment "P": "name" is V, which is not a string. An attachment's name is the runtime's `Attachment.name` …` | §3.4 — write the name as a string, or leave the key out and the attachment is named by its placeholder. It is written verbatim and `path` defaults to it, so a number there would reach the atlas as a region name nobody spelled |
|
|
5752
5758
|
|
|
5753
5759
|
⚠️ **One refusal in this section is not a `CompileError`, and it is `explain`'s.**
|
|
5754
|
-
`explain` prints the `DEFORM` block by **posing** the rig, and
|
|
5755
|
-
attachment against the atlas — so it needs the art `build` needs,
|
|
5756
|
-
two ways (§0.2).
|
|
5760
|
+
`explain` prints the `DEFORM` block by **posing** the rig, and spine-core's pose
|
|
5761
|
+
resolves every attachment against the atlas — so it needs the art `build` needs,
|
|
5762
|
+
reached the same two ways (§0.2). (Since issue #1019 a build's survey is read and
|
|
5763
|
+
posed through rigc's own core by default, which reads no atlas; the refusal below
|
|
5764
|
+
is unchanged and comes before either.) A pair whose art it cannot resolve is refused **before a line of
|
|
5757
5765
|
the report**, at **exit 2**, in rigc's own sentence:
|
|
5758
5766
|
|
|
5759
5767
|
```
|
|
@@ -6481,8 +6489,9 @@ Inside `setup` and each frame:
|
|
|
6481
6489
|
scale and shear all live in `a b c d`.
|
|
6482
6490
|
- `attachments` — every slot showing a region or a mesh, in the frame's **draw
|
|
6483
6491
|
order**: `{ slot, attachment, vertices, color }`. `vertices` are `x, y` per vertex
|
|
6484
|
-
in world units after skinning and deform
|
|
6485
|
-
|
|
6492
|
+
in world units after skinning and deform, over the **whole** attachment — on a
|
|
6493
|
+
Spine export spine-core's own `computeWorldVertices`, and on a rigc build the
|
|
6494
|
+
core's, the same numbers to the bit (the `--poser` row). A region's four are its
|
|
6486
6495
|
corners in the order bottom-left, top-left, top-right, bottom-right; a mesh's are
|
|
6487
6496
|
in the attachment's own order, so vertex `i` is the same vertex in every frame and
|
|
6488
6497
|
in `rest`. `color` is the slot colour times the attachment colour, straight RGBA
|
|
@@ -6501,7 +6510,7 @@ are not numbered like them — so `vertices` are the attachment's own, and the f
|
|
|
6501
6510
|
show what the clip left of them. Bounding boxes, points, paths and clipping polygons
|
|
6502
6511
|
draw nothing and are not listed. Numbers are printed the way every JSON file this
|
|
6503
6512
|
tool writes prints them — the shortest decimal that reads back as the same double —
|
|
6504
|
-
so a vertex in the file *is* the
|
|
6513
|
+
so a vertex in the file *is* the poser's vertex, not a rounding of it; a pose that
|
|
6505
6514
|
is not finite is refused rather than written as `null`, naming where — the setup pose,
|
|
6506
6515
|
`animation "a" frame i at F fps (t=…s)`, or the rest table — and then the bone and
|
|
6507
6516
|
its world-transform field, or the slot, the attachment and the vertex, with the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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, round-trips it through spine-core and named assertions, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A03, the body (issue #1025, step 4c of #380): every region attachment has a
|
|
3
|
+
* finite, positive width and height (case 6c).
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the region
|
|
6
|
+
* attachments are a fact (`../facts/skin_entries.ts`) rather than the list
|
|
7
|
+
* `validate()` filled from spine-core's loaded skins, so the same body runs over
|
|
8
|
+
* the model document. The `check` call — and so where the verdict sits in the
|
|
9
|
+
* report — stays in `validate()`.
|
|
10
|
+
*/
|
|
11
|
+
import type { Verdicts } from '../harness.ts';
|
|
12
|
+
import type { SkinEntryFacts } from '../facts/skin_entries.ts';
|
|
13
|
+
import { SKIP_NO_REGION_ATTACHMENT } from '../reasons.ts';
|
|
14
|
+
|
|
15
|
+
export function a03RegionWidthHeightFinite({ fail, skip }: Verdicts, { regionAttachments }: SkinEntryFacts): void {
|
|
16
|
+
// ⟨subject⟩_⟨property⟩: with no region there is no size to find non-finite,
|
|
17
|
+
// and a loop over nothing used to report that as held (#580).
|
|
18
|
+
if (regionAttachments.length === 0) return skip('A03_REGION_WIDTH_HEIGHT_FINITE', SKIP_NO_REGION_ATTACHMENT);
|
|
19
|
+
for (const att of regionAttachments) {
|
|
20
|
+
if (!Number.isFinite(att.width) || !Number.isFinite(att.height)) {
|
|
21
|
+
fail('A03_REGION_WIDTH_HEIGHT_FINITE', `region "${att.name}" loaded w=${att.width} h=${att.height}`);
|
|
22
|
+
}
|
|
23
|
+
if (att.width <= 0 || att.height <= 0) {
|
|
24
|
+
fail('A03_REGION_WIDTH_HEIGHT_FINITE', `region "${att.name}" has a non-positive size`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|