spine-rigc 0.22.1 โ 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -1
- package/cli.ts +205 -11
- package/docs/AUTHORING.md +421 -64
- package/docs/INGEST.md +185 -41
- package/docs/SPEC_COVERAGE.md +26 -8
- package/package.json +1 -1
- package/skills/ingest/SKILL.md +33 -11
- package/src/atlas.ts +57 -13
- package/src/check.ts +83 -1
- package/src/compile.ts +400 -63
- package/src/diff.ts +125 -2
- package/src/ingest.ts +1078 -0
- package/src/render.ts +104 -10
- package/src/rig.ts +92 -8
- package/src/types.ts +53 -7
- package/src/validate.ts +205 -27
- package/tools/editor_roundtrip.ts +172 -21
package/README.md
CHANGED
|
@@ -352,6 +352,16 @@ directories makes the result a frame set like any other โ the world box every
|
|
|
352
352
|
frame is a picture of. `--animation <name>` narrows it to one, `--fps` and
|
|
353
353
|
`--max` change the rate and the frame size.
|
|
354
354
|
|
|
355
|
+
> ๐จ **`--skin <name>` if your rig has more than one.** With no `--skin` no skin
|
|
356
|
+
> is set at all, so every slot resolves through the **default** skin alone โ and
|
|
357
|
+
> a slot whose art lives only in a named skin draws *nothing*. That is not a
|
|
358
|
+
> quirk of `render`: `check` compares `render`'s frames, so a multi-skin rig
|
|
359
|
+
> checked with no skin compares blank against blank and reports a perfect
|
|
360
|
+
> `0.0000` about art nobody drew. `render --skin` records the name in
|
|
361
|
+
> `frames.json`, `check --skin` poses the candidate under it, and a candidate
|
|
362
|
+
> scored against frames rendered under a *different* skin is refused by name
|
|
363
|
+
> rather than measured.
|
|
364
|
+
|
|
355
365
|
**`preview`** writes a single self-contained `.html`: your skeleton, your atlas
|
|
356
366
|
and every page's PNG bytes are embedded in it as data URIs, and it plays them in
|
|
357
367
|
the **official [Spine Web Player](https://esotericsoftware.com/spine-player)**.
|
|
@@ -493,6 +503,7 @@ commands take it and what its default is.
|
|
|
493
503
|
| `build โฆ --pack` | the same build with every part arranged onto **shared** atlas pages, written into `--out` โ losslessly, and gated a second time as the pair that ships. `--page-size` and `--padding` tune it |
|
|
494
504
|
| `build โฆ --atlas-in <file.atlas>` | the same build with every part resolved to a **region of an existing pack** instead of a loose PNG; a name the atlas lacks, a size the spec disagrees with or a rectangle off its page is refused by name |
|
|
495
505
|
| `validate <dir>` | re-gates artifacts already on disk |
|
|
506
|
+
| `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. `--stage x,y,w,h` supplies the one value a skeleton does not hold |
|
|
496
507
|
| `explain --rig โฆ --motion โฆ` | the compiled rig as a table โ every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
|
|
497
508
|
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/` |
|
|
498
509
|
| `preview --candidate <dir>` | one self-contained `.html` that plays it |
|
|
@@ -521,6 +532,55 @@ relative to the `cuts.json` file itself, so the table lives with the project tha
|
|
|
521
532
|
the art. Its shape is under
|
|
522
533
|
[Usage](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md#usage).
|
|
523
534
|
|
|
535
|
+
### Starting from a skeleton you already have
|
|
536
|
+
|
|
537
|
+
`rigc ingest` reads a Spine 4.3 `skeleton.json` and writes the two spec files that
|
|
538
|
+
rebuild it. It is the only command that runs against `build`'s direction, and the
|
|
539
|
+
only one whose contract is an equality rather than a rulebook:
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
rigc ingest hero.json --out specs/ --stage 0,0,1024,768
|
|
543
|
+
rigc build --rig specs/rig.json --motion specs/motion.json --images parts/ --out build/
|
|
544
|
+
rigc diff build/skeleton.json hero.json
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**`build(ingest(x))` is `x`.** Over the eleven rigs this repository builds โ the seven
|
|
548
|
+
gallery examples, the three generated probes and a coverage probe written for the
|
|
549
|
+
purpose โ the rebuilt `skeleton.json` is byte for byte the file the decompiler read,
|
|
550
|
+
and `bun run selftest` holds it there on every run. The atlas is held to a weaker
|
|
551
|
+
claim on purpose, and the weakening is measured rather than assumed: it comes back
|
|
552
|
+
equal as a **multiset of region blocks**, because the order the pages are collected in
|
|
553
|
+
is in no field of the skeleton.
|
|
554
|
+
|
|
555
|
+
**What it reads is skeleton JSON and nothing else** โ no `.spine` project, no binary
|
|
556
|
+
`.skel`, no atlas, no art. So it never invents, and the things it cannot get out of
|
|
557
|
+
the file are **findings** with codes rather than plausible values: a construct the
|
|
558
|
+
spec format cannot hold (`linkedmesh`, `point`, a `sequence` block, an unknown field
|
|
559
|
+
on a bone, slot or constraint) is a blocker, the command exits non-zero, and both
|
|
560
|
+
specs are still written โ a spec plus a list of what is missing from it beats no spec.
|
|
561
|
+
One thing it drops on purpose and says so: a path attachment's `lengths`, which is
|
|
562
|
+
`PathConstraint`'s own measurement and which rigc re-measures.
|
|
563
|
+
|
|
564
|
+
โ ๏ธ **Two values are not in a skeleton at all.**
|
|
565
|
+
|
|
566
|
+
- **The stage.** `skeleton.width`/`height`: rigc always writes one and an editor
|
|
567
|
+
export carries none, so without `--stage x,y,w,h` the missing box is refused by
|
|
568
|
+
name. It is not derivable โ posing the rig gives the *animated* extent, which is a
|
|
569
|
+
different number from the setup box โ and it is the value that costs nothing to get
|
|
570
|
+
wrong, because no measure `diff` reports reads the skeleton header at all.
|
|
571
|
+
- **An animation's duration.** The format has no such field. The largest key time is
|
|
572
|
+
the only derivable answer and it is what a runtime plays to; it is wrong for an
|
|
573
|
+
animation that holds its last pose past its last key, so it is recorded as a finding
|
|
574
|
+
on every animation rather than chosen quietly.
|
|
575
|
+
|
|
576
|
+
Both specs carry a `note` that `ingest` writes itself, saying the file is decompiled
|
|
577
|
+
and naming the skeleton it came from โ because a decompiled spec is indistinguishable
|
|
578
|
+
from an authored one by inspection, every gate here calls it green (it *is* green),
|
|
579
|
+
and no gate can catch a missing note.
|
|
580
|
+
|
|
581
|
+
[docs/INGEST.md](docs/INGEST.md) is the whole page on working from a file you were
|
|
582
|
+
handed; [docs/AUTHORING.md](docs/AUTHORING.md) ยง0.3 is the loop.
|
|
583
|
+
|
|
524
584
|
### The editor round trip โ for a licence holder, never in CI
|
|
525
585
|
|
|
526
586
|
`tools/editor_roundtrip.ts` drives the loop the output's whole premise rests on:
|
|
@@ -534,7 +594,10 @@ bun tools/editor_roundtrip.ts --build build/ --editor /Applications/Spine.app/Co
|
|
|
534
594
|
|
|
535
595
|
It prints the import and export exit codes, the validator's verdict on the
|
|
536
596
|
export, every `diff` measure that moved, `check`'s mean MAE and worst drift per
|
|
537
|
-
animation
|
|
597
|
+
animation **for each skin the build declares** โ one render-and-check block per
|
|
598
|
+
skin, with a per-skin roll-up under them, because a rig's contested art lives in
|
|
599
|
+
its named skins and a single un-skinned check draws none of it โ and a
|
|
600
|
+
field-by-field list of what the editor rewrote. On its first
|
|
538
601
|
run it found three emitter defects โ [#368](https://github.com/firejune/rigc/issues/368),
|
|
539
602
|
[#369](https://github.com/firejune/rigc/issues/369),
|
|
540
603
|
[#370](https://github.com/firejune/rigc/issues/370) โ and then showed that a
|
package/cli.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
type DeformSpan,
|
|
68
68
|
} from './src/deformmeasure.ts';
|
|
69
69
|
import { diffLines, diffSkeletons, reportedFigures, sectionFigures, type DiffReport } from './src/diff.ts';
|
|
70
|
+
import { ingest, type IngestFindingKind, type IngestStage } from './src/ingest.ts';
|
|
70
71
|
import { copyAtlasImages } from './src/emit.ts';
|
|
71
72
|
import { DEFAULT_PADDING, DEFAULT_PAGE_SIZE, packAtlas } from './src/atlas.ts';
|
|
72
73
|
import { parseJsonWithPosition } from './src/json-position.ts';
|
|
@@ -1415,8 +1416,8 @@ function cmdDiff(flags: Record<string, string>, positional: string[]): void {
|
|
|
1415
1416
|
*/
|
|
1416
1417
|
function readCheckFlags(
|
|
1417
1418
|
flags: Record<string, string>,
|
|
1418
|
-
): Pick<CheckOptions, 'fps' | 'viewport' | 'as' | 'framing' | 'textureFrom'> {
|
|
1419
|
-
const out: Pick<CheckOptions, 'fps' | 'viewport' | 'as' | 'framing' | 'textureFrom'> = {};
|
|
1419
|
+
): Pick<CheckOptions, 'fps' | 'viewport' | 'as' | 'framing' | 'textureFrom' | 'skin'> {
|
|
1420
|
+
const out: Pick<CheckOptions, 'fps' | 'viewport' | 'as' | 'framing' | 'textureFrom' | 'skin'> = {};
|
|
1420
1421
|
if (flags.framing !== undefined) {
|
|
1421
1422
|
if (flags.framing !== 'per-shot' && flags.framing !== 'shared') {
|
|
1422
1423
|
throw new UsageError('--framing takes per-shot (the default) or shared');
|
|
@@ -1437,6 +1438,10 @@ function readCheckFlags(
|
|
|
1437
1438
|
out.viewport = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
|
|
1438
1439
|
}
|
|
1439
1440
|
if (flags.as !== undefined) out.as = flags.as;
|
|
1441
|
+
// The name is not checked against the candidate here: `check` owns that
|
|
1442
|
+
// refusal, because it is the side that has the skeleton open and can list the
|
|
1443
|
+
// skins it declares. `render` checks its own for the same reason.
|
|
1444
|
+
if (flags.skin !== undefined) out.skin = flags.skin;
|
|
1440
1445
|
if (flags['texture-from'] !== undefined) {
|
|
1441
1446
|
const path = resolve(flags['texture-from']);
|
|
1442
1447
|
if (!existsSync(path)) {
|
|
@@ -1528,6 +1533,29 @@ function readAnimationFlag(flags: Record<string, string>, available: string[]):
|
|
|
1528
1533
|
return name;
|
|
1529
1534
|
}
|
|
1530
1535
|
|
|
1536
|
+
/**
|
|
1537
|
+
* `--skin`, checked against what the skeleton actually declares.
|
|
1538
|
+
*
|
|
1539
|
+
* โญ Absent is not `default`: it is "set no skin at all", which is what every
|
|
1540
|
+
* render did before issue #571 and what `spine-core` starts a skeleton in. The
|
|
1541
|
+
* distinction is the whole of the default path's byte-identity โ see
|
|
1542
|
+
* `FramesSidecar.skin`.
|
|
1543
|
+
*
|
|
1544
|
+
* The miss is refused by name with the declared names beside it, the way
|
|
1545
|
+
* `--animation` is: a skin name is the one place a typo draws a whole rig's
|
|
1546
|
+
* worth of the wrong art and reports a number about it.
|
|
1547
|
+
*/
|
|
1548
|
+
function readSkinFlag(flags: Record<string, string>, declared: string[]): string | undefined {
|
|
1549
|
+
const name = flags.skin;
|
|
1550
|
+
if (name === undefined) return undefined;
|
|
1551
|
+
if (!declared.includes(name)) {
|
|
1552
|
+
throw new UsageError(
|
|
1553
|
+
`no skin ${JSON.stringify(name)} in this skeleton; it declares [${declared.join(', ') || 'none'}]`,
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
return name;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1531
1559
|
function readPositiveNumber(flags: Record<string, string>, key: string, fallback: number, least: number): number {
|
|
1532
1560
|
const raw = flags[key];
|
|
1533
1561
|
if (raw === undefined) return fallback;
|
|
@@ -1555,11 +1583,20 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1555
1583
|
console.log(` .. atlas ${atlasPath}`);
|
|
1556
1584
|
const { data, pages } = loadPosable(skeletonPath, atlasPath, atlasDir);
|
|
1557
1585
|
const only = readAnimationFlag(flags, data.animations.map((a) => a.name));
|
|
1558
|
-
|
|
1559
|
-
|
|
1586
|
+
const skin = readSkinFlag(flags, data.skins.map((s) => s.name));
|
|
1587
|
+
// One object, so the framing and the frames cannot be posed under two
|
|
1588
|
+
// different skins โ which would frame one shot with another shot's box.
|
|
1589
|
+
// Not annotated `PoseOptions`: that name is `src/pose.ts`'s in this file, and
|
|
1590
|
+
// `src/render.ts` has one of its own. The inferred shape is the render one.
|
|
1591
|
+
const pose = skin === undefined ? undefined : { skin };
|
|
1592
|
+
if (skin !== undefined) console.log(` .. skin ${skin}`);
|
|
1593
|
+
|
|
1594
|
+
const viewport = framingViewport(data, maxSide, pose);
|
|
1560
1595
|
if (!viewport) {
|
|
1561
1596
|
throw new UsageError(
|
|
1562
|
-
`${skeletonPath} posed no drawable attachment in any animation or in its setup pose
|
|
1597
|
+
`${skeletonPath} posed no drawable attachment in any animation or in its setup pose${
|
|
1598
|
+
skin === undefined ? '' : ` under skin ${JSON.stringify(skin)}`
|
|
1599
|
+
} โ there is nothing to draw`,
|
|
1563
1600
|
);
|
|
1564
1601
|
}
|
|
1565
1602
|
|
|
@@ -1567,7 +1604,7 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1567
1604
|
// setup-pose frame under the reserved name. Narrowing to one animation reuses
|
|
1568
1605
|
// the same sampler rather than a second path through it.
|
|
1569
1606
|
const sampled: Map<string, Frame[]> =
|
|
1570
|
-
only === undefined ? sampleAll(data, fps) : new Map([[only, sampleAnimation(data, only, fps)]]);
|
|
1607
|
+
only === undefined ? sampleAll(data, fps, pose) : new Map([[only, sampleAnimation(data, only, fps, pose)]]);
|
|
1571
1608
|
console.log(` .. ${viewport.width}x${viewport.height}px at ${fps} fps, ${sampled.size} set(s) -> ${outRoot}`);
|
|
1572
1609
|
|
|
1573
1610
|
mkdirSync(outRoot, { recursive: true });
|
|
@@ -1609,6 +1646,10 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
1609
1646
|
// render something else into the same grid later.
|
|
1610
1647
|
const sidecar: FramesSidecar = {
|
|
1611
1648
|
spec: FRAMES_SPEC,
|
|
1649
|
+
// Written only when a skin was asked for: absent says "no skin was set",
|
|
1650
|
+
// which is both what this run did and what every frame set written before
|
|
1651
|
+
// #571 did. See `FramesSidecar.skin`.
|
|
1652
|
+
...(skin === undefined ? {} : { skin }),
|
|
1612
1653
|
background: BACKGROUND,
|
|
1613
1654
|
viewport: {
|
|
1614
1655
|
x: viewport.minX,
|
|
@@ -2395,7 +2436,15 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
2395
2436
|
// last one in the repository and issue #307 was about exactly that.
|
|
2396
2437
|
const motion = parseMotionSpec(readJsonFile(opts.motionPath), opts.motionPath);
|
|
2397
2438
|
|
|
2398
|
-
|
|
2439
|
+
// A rig may state that it has no stage at all (issue #578), and the two must
|
|
2440
|
+
// not print alike: `undefined x undefined` is what a template does with an
|
|
2441
|
+
// absence, and it reads like a defect in the tool rather than a claim in the
|
|
2442
|
+
// spec.
|
|
2443
|
+
const stage =
|
|
2444
|
+
result.skeleton.skeleton.width === undefined || result.skeleton.skeleton.height === undefined
|
|
2445
|
+
? 'none declared'
|
|
2446
|
+
: `${result.skeleton.skeleton.width} x ${result.skeleton.skeleton.height}`;
|
|
2447
|
+
console.log(`\nstage ${stage} (spine ${result.skeleton.skeleton.spine})`);
|
|
2399
2448
|
|
|
2400
2449
|
// The crop note describes where the numbers CAME from, and without a manifest
|
|
2401
2450
|
// they came from the rig spec's own literals โ there is no crop to be relative
|
|
@@ -2679,6 +2728,100 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
2679
2728
|
console.log(` default=${motion.mix?.default ?? 0} pairs=${JSON.stringify(motion.mix?.pairs ?? [])}`);
|
|
2680
2729
|
}
|
|
2681
2730
|
|
|
2731
|
+
/**
|
|
2732
|
+
* ingest โ a skeleton back into the two specs that rebuild it.
|
|
2733
|
+
*
|
|
2734
|
+
* The only command that runs against `build`'s direction, and the contract is an
|
|
2735
|
+
* equality rather than a rulebook: `build(ingest(A))` is `A`. Everything it
|
|
2736
|
+
* cannot carry is a **finding** printed here with its code, because those lines
|
|
2737
|
+
* are what tells an author what the rebuilt rig will not have โ they are this
|
|
2738
|
+
* command's whole UI, exactly as the validator's messages are `build`'s.
|
|
2739
|
+
*
|
|
2740
|
+
* โ ๏ธ It writes both specs even when a blocker was found, and then exits
|
|
2741
|
+
* non-zero: a blocker means the rebuild will not be the file that was read, and
|
|
2742
|
+
* the useful thing at that point is the spec plus the list of what is missing
|
|
2743
|
+
* from it. `build`'s "nothing written" rule is not this rule โ that one is about
|
|
2744
|
+
* a **gated artifact** on disk, and these two files are inputs to the gate
|
|
2745
|
+
* rather than output of it.
|
|
2746
|
+
*/
|
|
2747
|
+
function cmdIngest(flags: Record<string, string>, positional: string[]): void {
|
|
2748
|
+
const [source] = positional;
|
|
2749
|
+
if (!source) throw new UsageError('ingest takes one path: <skeleton.json>');
|
|
2750
|
+
const skeletonPath = resolve(source);
|
|
2751
|
+
if (!existsSync(skeletonPath)) throw new UsageError(`nothing at ${skeletonPath}`);
|
|
2752
|
+
// The same sentence `resolveArtifacts` refuses a `.spine` with, for the same
|
|
2753
|
+
// reason (docs/INGEST.md ยง5): this reads Spine 4.3 skeleton JSON and nothing
|
|
2754
|
+
// else โ not a project file, not a binary `.skel`, not the atlas.
|
|
2755
|
+
if (!skeletonPath.endsWith('.json')) {
|
|
2756
|
+
throw new UsageError(
|
|
2757
|
+
`${skeletonPath} is not a .json skeleton โ ingest reads Spine 4.3 skeleton JSON and nothing else: not a ` +
|
|
2758
|
+
'.spine project, not a binary .skel, not an atlas. Re-export as JSON',
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
if (flags.out === undefined) throw new UsageError('ingest needs --out <dir> โ the directory to write rig.json and motion.json into');
|
|
2762
|
+
const art = flags.art ?? 'loose';
|
|
2763
|
+
if (art !== 'loose' && art !== 'none') {
|
|
2764
|
+
throw new UsageError(
|
|
2765
|
+
`--art is ${JSON.stringify(art)}; it is "loose" (name an image per attachment, for \`build --images <dir>\`) ` +
|
|
2766
|
+
'or "none" (state width/height only, for `build --atlas-in <pack>`). A skeleton encodes neither, which is ' +
|
|
2767
|
+
'why this is a flag',
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
let stage: IngestStage | undefined;
|
|
2771
|
+
if (flags.stage !== undefined) {
|
|
2772
|
+
const parts = flags.stage.split(',').map(Number);
|
|
2773
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
|
|
2774
|
+
throw new UsageError(`--stage is ${JSON.stringify(flags.stage)}; give four numbers, x,y,width,height`);
|
|
2775
|
+
}
|
|
2776
|
+
stage = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
|
|
2777
|
+
}
|
|
2778
|
+
const outDir = resolve(flags.out);
|
|
2779
|
+
console.log(`rigc ingest ${skeletonPath}`);
|
|
2780
|
+
console.log(` .. out ${outDir}`);
|
|
2781
|
+
console.log(` .. art ${art}`);
|
|
2782
|
+
|
|
2783
|
+
const result = ingest(readJsonFile(skeletonPath), {
|
|
2784
|
+
name: flags.name ?? basename(skeletonPath, '.json'),
|
|
2785
|
+
art,
|
|
2786
|
+
stage,
|
|
2787
|
+
source: basename(skeletonPath),
|
|
2788
|
+
version: readVersion(),
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
mkdirSync(outDir, { recursive: true });
|
|
2792
|
+
// Indent 2, which is what `compile` writes the skeleton with. One emitter
|
|
2793
|
+
// convention, so a spec and the skeleton it came from read the same way.
|
|
2794
|
+
writeFileSync(join(outDir, 'rig.json'), `${JSON.stringify(result.rig, null, 2)}\n`);
|
|
2795
|
+
writeFileSync(join(outDir, 'motion.json'), `${JSON.stringify(result.motion, null, 2)}\n`);
|
|
2796
|
+
writeFileSync(join(outDir, 'findings.json'), `${JSON.stringify(result.findings, null, 2)}\n`);
|
|
2797
|
+
|
|
2798
|
+
// Grouped by kind rather than printed in discovery order: a blocker is what
|
|
2799
|
+
// decides the exit code, and a reader scanning for one should not have to
|
|
2800
|
+
// read past a hundred DURATION lines to find it.
|
|
2801
|
+
const GUTTER: Record<IngestFindingKind, string> = { blocker: 'BLOCK', judgement: 'JUDGE', lossy: 'LOSS ' };
|
|
2802
|
+
for (const kind of ['blocker', 'judgement', 'lossy'] as const) {
|
|
2803
|
+
for (const finding of result.findings.filter((f) => f.kind === kind)) {
|
|
2804
|
+
console.log(` ${GUTTER[kind]} ${finding.code}: ${finding.where} โ ${finding.detail}`);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
console.log(`rigc: wrote ${join(outDir, 'rig.json')}`);
|
|
2808
|
+
console.log(`rigc: wrote ${join(outDir, 'motion.json')}`);
|
|
2809
|
+
console.log(`rigc: wrote ${join(outDir, 'findings.json')}`);
|
|
2810
|
+
console.log(
|
|
2811
|
+
`rigc: build it with rigc build --rig ${join(outDir, 'rig.json')} --motion ${join(outDir, 'motion.json')} ` +
|
|
2812
|
+
`${art === 'loose' ? '--images <dir>' : '--atlas-in <pack.atlas>'} --out <dir>`,
|
|
2813
|
+
);
|
|
2814
|
+
|
|
2815
|
+
const blockers = result.findings.filter((f) => f.kind === 'blocker');
|
|
2816
|
+
if (blockers.length > 0) {
|
|
2817
|
+
console.error(
|
|
2818
|
+
`rigc: ${blockers.length} blocker(s) โ both specs were written, and a build from them will NOT be the ` +
|
|
2819
|
+
`skeleton that was read (${[...new Set(blockers.map((f) => f.code))].join(', ')})`,
|
|
2820
|
+
);
|
|
2821
|
+
process.exit(1);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2682
2825
|
// ---------------------------------------------------------------------------
|
|
2683
2826
|
// usage / per-command help
|
|
2684
2827
|
// ---------------------------------------------------------------------------
|
|
@@ -2767,11 +2910,24 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
2767
2910
|
`printed (default ${DEFAULT_MIN_LEVER_PX}); below it the bone is refused \`no-bracket\` naming the measured ` +
|
|
2768
2911
|
'lever, because an angle read across a short lever turns a half-pixel anchor error into several degrees',
|
|
2769
2912
|
animation: 'which animation to show; the default is every one for `render` and the first for `preview`',
|
|
2913
|
+
skin:
|
|
2914
|
+
'pose under this skin, by the name the skeleton declares. Without it NO skin is set โ every slot resolves ' +
|
|
2915
|
+
'through the default skin alone, so a slot whose art lives only in a named skin draws nothing. A name the ' +
|
|
2916
|
+
'skeleton does not declare is refused with the ones it does. `render` records the skin in frames.json and ' +
|
|
2917
|
+
'`check` reads it back, so a skin-A candidate is not scored against skin-B frames in silence',
|
|
2770
2918
|
max: 'longest side of a rendered frame, in pixels (default 256)',
|
|
2771
2919
|
record: 'a saved vote to check against its ballot and append to the ledger, instead of writing a ballot',
|
|
2772
2920
|
ballot: `the ballot the --record'd vote answers (default \`${DEFAULT_BALLOT}\`); its embedded manifest is what the vote is checked against`,
|
|
2773
2921
|
ledger: `the append-only JSONL the vote lands in (default \`${DEFAULT_LEDGER}\`)`,
|
|
2774
2922
|
again: 'record a second vote on a ballot the ledger already has; without it, a repeat is refused rather than doubled',
|
|
2923
|
+
name: "the rig spec's own name, which the motion spec's archetype must match (default: the skeleton file's basename)",
|
|
2924
|
+
art: 'how the written spec reaches the art, which a skeleton does not encode: `loose` names an image per ' +
|
|
2925
|
+
'attachment for `build --images <dir>` to measure, `none` states width/height only for `build --atlas-in ' +
|
|
2926
|
+
'<pack>` to resolve (default: loose)',
|
|
2927
|
+
stage:
|
|
2928
|
+
"the setup bounding box โ `skeleton.x,y,width,height`. An editor export carries none and rigc refuses a " +
|
|
2929
|
+
'compile without one; posing the rig gives the ANIMATED extent, which is a different number, so this is the ' +
|
|
2930
|
+
"caller's value and is never derived. Without it the missing stage is reported as a blocker",
|
|
2775
2931
|
help: "show this command's flags and exit",
|
|
2776
2932
|
};
|
|
2777
2933
|
|
|
@@ -2812,10 +2968,14 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
2812
2968
|
'anchor-residual': '<0..1>',
|
|
2813
2969
|
'inward-lever': '<px>',
|
|
2814
2970
|
animation: '<name>',
|
|
2971
|
+
skin: '<name>',
|
|
2815
2972
|
max: '<px>',
|
|
2816
2973
|
record: '<result.json>',
|
|
2817
2974
|
ballot: '<ballot.html>',
|
|
2818
2975
|
ledger: '<votes.jsonl>',
|
|
2976
|
+
name: '<n>',
|
|
2977
|
+
art: 'loose|none',
|
|
2978
|
+
stage: '<x,y,w,h>',
|
|
2819
2979
|
};
|
|
2820
2980
|
|
|
2821
2981
|
interface CommandDoc {
|
|
@@ -2829,7 +2989,8 @@ interface CommandDoc {
|
|
|
2829
2989
|
*
|
|
2830
2990
|
* โ ๏ธ The default above it โ one meaning per flag name, everywhere โ is the rule
|
|
2831
2991
|
* and this is the named exception to it, not a second table. Three flags earn it:
|
|
2832
|
-
* `--out` is a directory of artifacts to `build`, a directory of
|
|
2992
|
+
* `--out` is a directory of artifacts to `build`, a directory of specs to
|
|
2993
|
+
* `ingest`, a directory of pictures to
|
|
2833
2994
|
* `render` and one file to `preview` and `vote`; `--fps` is the rate a frame set
|
|
2834
2995
|
* was RECORDED at to `check`, which reads it off a sidecar, and the rate to
|
|
2835
2996
|
* SAMPLE at to `render`, which is choosing it; `--candidate` is one artifact
|
|
@@ -2878,6 +3039,17 @@ const COMMANDS: CommandDoc[] = [
|
|
|
2878
3039
|
],
|
|
2879
3040
|
flags: ['atlas', 'profile', 'cut', 'cuts', 'rig', 'motion', 'out', 'manifest', 'images'],
|
|
2880
3041
|
},
|
|
3042
|
+
{
|
|
3043
|
+
name: 'ingest',
|
|
3044
|
+
usage: ['rigc ingest <skeleton.json> --out <dir> [--name <n>] [--art loose|none] [--stage x,y,w,h]'],
|
|
3045
|
+
flags: ['out', 'name', 'art', 'stage'],
|
|
3046
|
+
overrides: {
|
|
3047
|
+
out: {
|
|
3048
|
+
value: '<dir>',
|
|
3049
|
+
meaning: 'directory to write rig.json, motion.json and findings.json into โ the two specs that rebuild this skeleton',
|
|
3050
|
+
},
|
|
3051
|
+
},
|
|
3052
|
+
},
|
|
2881
3053
|
{
|
|
2882
3054
|
name: 'diff',
|
|
2883
3055
|
usage: ['rigc diff <candidate.json> <reference.json> [--json <out>]'],
|
|
@@ -2886,7 +3058,16 @@ const COMMANDS: CommandDoc[] = [
|
|
|
2886
3058
|
{
|
|
2887
3059
|
name: 'check',
|
|
2888
3060
|
usage: ['rigc check --candidate <dir | skeleton.json> --frames <dir> [flags]'],
|
|
2889
|
-
flags: ['candidate', 'frames', 'atlas', 'texture-from', 'fps', 'viewport', 'framing', 'as', 'all-frames', 'json'],
|
|
3061
|
+
flags: ['candidate', 'frames', 'atlas', 'texture-from', 'fps', 'viewport', 'framing', 'as', 'skin', 'all-frames', 'json'],
|
|
3062
|
+
overrides: {
|
|
3063
|
+
skin: {
|
|
3064
|
+
meaning:
|
|
3065
|
+
'pose the CANDIDATE under this skin, by the name it declares. Without it no skin is set and the ' +
|
|
3066
|
+
'default skin alone is compared, which for a multi-skin rig is a comparison that can see none of the ' +
|
|
3067
|
+
'contested art. The frames are checked back: a set whose frames.json records a different skin is ' +
|
|
3068
|
+
'REFUSED by name, and one that records none says so in the report rather than pretending to agree',
|
|
3069
|
+
},
|
|
3070
|
+
},
|
|
2890
3071
|
},
|
|
2891
3072
|
{
|
|
2892
3073
|
name: 'bench',
|
|
@@ -2914,9 +3095,9 @@ const COMMANDS: CommandDoc[] = [
|
|
|
2914
3095
|
{
|
|
2915
3096
|
name: 'render',
|
|
2916
3097
|
usage: [
|
|
2917
|
-
'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--fps 12] [--max 256] [--out render/]',
|
|
3098
|
+
'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--skin <name>] [--fps 12] [--max 256] [--out render/]',
|
|
2918
3099
|
],
|
|
2919
|
-
flags: ['candidate', 'atlas', 'animation', 'fps', 'max', 'out'],
|
|
3100
|
+
flags: ['candidate', 'atlas', 'animation', 'skin', 'fps', 'max', 'out'],
|
|
2920
3101
|
overrides: {
|
|
2921
3102
|
out: { value: '<dir>', meaning: 'directory to write the frame series into (default `render/`)' },
|
|
2922
3103
|
fps: { meaning: `frames per second to sample the animation at (default ${PROTOCOL_FPS})` },
|
|
@@ -3070,6 +3251,18 @@ const USAGE = [
|
|
|
3070
3251
|
'draws with rigc\'s own rasteriser; preview embeds the artifact in a page that plays',
|
|
3071
3252
|
'it in the official Spine Web Player, which is also the interop proof.',
|
|
3072
3253
|
'',
|
|
3254
|
+
'ingest runs build backwards: it reads a Spine 4.3 skeleton.json and writes the rig',
|
|
3255
|
+
'spec and motion spec that rebuild it, so an existing skeleton becomes a starting',
|
|
3256
|
+
'point instead of something to retype:',
|
|
3257
|
+
' rigc ingest hero.json --out specs/ --stage 0,0,1024,768 rig.json + motion.json',
|
|
3258
|
+
'The contract is an equality, not a rulebook: build(ingest(x)) is x, byte for byte.',
|
|
3259
|
+
'It reads the skeleton and nothing else โ no .spine project, no binary .skel, no',
|
|
3260
|
+
'atlas โ so two things are the caller\'s and are refused rather than guessed: the',
|
|
3261
|
+
'setup stage (--stage; an export carries none) and how the spec reaches the art',
|
|
3262
|
+
'(--art). Everything the spec format cannot hold is printed as a named finding and',
|
|
3263
|
+
'exits non-zero, with both files still written, because a spec plus a list of what',
|
|
3264
|
+
'is missing from it beats no spec at all.',
|
|
3265
|
+
'',
|
|
3073
3266
|
'pose runs the other way round from everything above: it reads a picture you already',
|
|
3074
3267
|
'have โ one key pose โ and reports where each loose part PNG sits in it (x, y, rotation,',
|
|
3075
3268
|
'scale) so an agent can state those poses in a spec by construction:',
|
|
@@ -3130,6 +3323,7 @@ try {
|
|
|
3130
3323
|
process.exit(0);
|
|
3131
3324
|
}
|
|
3132
3325
|
if (command === 'build') cmdBuild(flags);
|
|
3326
|
+
else if (command === 'ingest') cmdIngest(flags, positional);
|
|
3133
3327
|
else if (command === 'validate') cmdValidate(flags, positional);
|
|
3134
3328
|
else if (command === 'explain') cmdExplain(flags);
|
|
3135
3329
|
else if (command === 'diff') cmdDiff(flags, positional);
|