spine-rigc 0.22.2 โ 0.24.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 +80 -1
- package/cli.ts +263 -13
- package/docs/AUTHORING.md +554 -75
- package/docs/INGEST.md +238 -44
- package/docs/SPEC_COVERAGE.md +14 -3
- package/package.json +1 -1
- package/skills/ingest/SKILL.md +33 -11
- package/src/atlas.ts +135 -16
- package/src/check.ts +83 -1
- package/src/compile.ts +402 -74
- package/src/deformmeasure.ts +322 -151
- package/src/diff.ts +125 -2
- package/src/ingest.ts +1137 -0
- package/src/render.ts +113 -17
- package/src/rig.ts +92 -8
- package/src/timelines.ts +163 -0
- package/src/types.ts +74 -9
- package/src/validate.ts +765 -92
- package/tools/editor_roundtrip.ts +247 -36
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, and `--images <dir>` writes the spec's own images directory โ the opposite direction from `build --images`, which overrides it โ so the rebuild carries no flag at all |
|
|
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,71 @@ 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 --images parts/
|
|
543
|
+
rigc build --rig specs/rig.json --motion specs/motion.json --out build/
|
|
544
|
+
rigc diff build/skeleton.json hero.json
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
`--images parts/` is what makes the second line carry no flag: it writes the spec's
|
|
548
|
+
own `images` directory, spelled from `--out`, so the specs are self-contained from
|
|
549
|
+
there on. Leave it off and the `image` names resolve against `specs/` itself, which
|
|
550
|
+
holds no art โ every rebuild then has to repeat `build --images parts/`.
|
|
551
|
+
|
|
552
|
+
**`build(ingest(x))` is `x`.** Over the eleven rigs this repository builds โ the seven
|
|
553
|
+
gallery examples, the three generated probes and a coverage probe written for the
|
|
554
|
+
purpose โ the rebuilt `skeleton.json` is byte for byte the file the decompiler read,
|
|
555
|
+
and `bun run selftest` holds it there on every run. The atlas is held to a weaker
|
|
556
|
+
claim on purpose, and the weakening is measured rather than assumed: it comes back
|
|
557
|
+
equal as a **multiset of region blocks**, because the order the pages are collected in
|
|
558
|
+
is in no field of the skeleton.
|
|
559
|
+
|
|
560
|
+
**And over twelve skeletons nobody here wrote.** The same run ingests every editor
|
|
561
|
+
export in the fetched example corpus, rebuilds it through the pack beside it and
|
|
562
|
+
`diff`s the result against the source: **12 of 12, no blockers, 1.000 on every measure
|
|
563
|
+
the report carries.** Byte identity is not the claim there and the reason is the input
|
|
564
|
+
rather than the round trip โ an editor header carries `hash` and `audio`, which the rig
|
|
565
|
+
spec has no field for โ so the contract is the structural one `diff` measures.
|
|
566
|
+
[INGEST.md ยง2.3](docs/INGEST.md) has the three kinds of difference that remain, with
|
|
567
|
+
what each is worth.
|
|
568
|
+
|
|
569
|
+
**What it reads is skeleton JSON and nothing else** โ no `.spine` project, no binary
|
|
570
|
+
`.skel`, no atlas, no art. So it never invents, and the things it cannot get out of
|
|
571
|
+
the file are **findings** with codes rather than plausible values: a construct the
|
|
572
|
+
spec format cannot hold (`linkedmesh`, `point`, a `sequence` block, an unknown field
|
|
573
|
+
on a bone, slot or constraint) is a blocker, the command exits non-zero, and both
|
|
574
|
+
specs are still written โ a spec plus a list of what is missing from it beats no spec.
|
|
575
|
+
One thing it drops on purpose and says so: a path attachment's `lengths`, which is
|
|
576
|
+
`PathConstraint`'s own measurement and which rigc re-measures.
|
|
577
|
+
|
|
578
|
+
โ ๏ธ **Two values are not in a skeleton at all.**
|
|
579
|
+
|
|
580
|
+
- **The stage.** `skeleton.width`/`height`: rigc always writes one, and a skeleton that
|
|
581
|
+
carries none is refused by name unless `--stage x,y,w,h` supplies it. It is not
|
|
582
|
+
derivable โ posing the rig gives the *animated* extent, which is a different number
|
|
583
|
+
from the setup box. โ ๏ธ This said *"an editor export carries none"* until #594 measured
|
|
584
|
+
it: **all twelve exports in the example corpus carry a stage** and none of them needs
|
|
585
|
+
the flag. It is still the value that costs least to get wrong, because `diff` reports
|
|
586
|
+
the box and gates nothing on it.
|
|
587
|
+
- **An animation's duration.** The format has no such field. The largest key time is
|
|
588
|
+
the only derivable answer and it is what a runtime plays to; it is wrong for an
|
|
589
|
+
animation that holds its last pose past its last key, so it is recorded as a finding
|
|
590
|
+
on every animation rather than chosen quietly.
|
|
591
|
+
|
|
592
|
+
Both specs carry a `note` that `ingest` writes itself, saying the file is decompiled
|
|
593
|
+
and naming the skeleton it came from โ because a decompiled spec is indistinguishable
|
|
594
|
+
from an authored one by inspection, every gate here calls it green (it *is* green),
|
|
595
|
+
and no gate can catch a missing note.
|
|
596
|
+
|
|
597
|
+
[docs/INGEST.md](docs/INGEST.md) is the whole page on working from a file you were
|
|
598
|
+
handed; [docs/AUTHORING.md](docs/AUTHORING.md) ยง0.3 is the loop.
|
|
599
|
+
|
|
524
600
|
### The editor round trip โ for a licence holder, never in CI
|
|
525
601
|
|
|
526
602
|
`tools/editor_roundtrip.ts` drives the loop the output's whole premise rests on:
|
|
@@ -534,7 +610,10 @@ bun tools/editor_roundtrip.ts --build build/ --editor /Applications/Spine.app/Co
|
|
|
534
610
|
|
|
535
611
|
It prints the import and export exit codes, the validator's verdict on the
|
|
536
612
|
export, every `diff` measure that moved, `check`'s mean MAE and worst drift per
|
|
537
|
-
animation
|
|
613
|
+
animation **for each skin the build declares** โ one render-and-check block per
|
|
614
|
+
skin, with a per-skin roll-up under them, because a rig's contested art lives in
|
|
615
|
+
its named skins and a single un-skinned check draws none of it โ and a
|
|
616
|
+
field-by-field list of what the editor rewrote. On its first
|
|
538
617
|
run it found three emitter defects โ [#368](https://github.com/firejune/rigc/issues/368),
|
|
539
618
|
[#369](https://github.com/firejune/rigc/issues/369),
|
|
540
619
|
[#370](https://github.com/firejune/rigc/issues/370) โ and then showed that a
|
package/cli.ts
CHANGED
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
type BoneDistReport,
|
|
58
58
|
} from './src/bonedist.ts';
|
|
59
59
|
import { checkAgainstFrames, checkLines, CheckError, type CheckOptions, type CheckReport } from './src/check.ts';
|
|
60
|
-
import { compile, CompileError, type CompileOptions } from './src/compile.ts';
|
|
60
|
+
import { compile, CompileError, relativeImagesPath, type CompileOptions } from './src/compile.ts';
|
|
61
61
|
import {
|
|
62
62
|
skeletonDataFromText,
|
|
63
63
|
surveyDeformKeys,
|
|
@@ -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,135 @@ 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
|
+
/**
|
|
2780
|
+
* The rig spec's own `images`, spelled from `--out` the way `build` spells
|
|
2781
|
+
* `skeleton.images` from its own output directory โ the SAME function, so the
|
|
2782
|
+
* two conventions cannot drift (issue #595). Without it the field is left out,
|
|
2783
|
+
* an `image` name resolves against the spec's own directory, and every rebuild
|
|
2784
|
+
* of the spec has to carry `build --images <dir>`.
|
|
2785
|
+
*/
|
|
2786
|
+
let specImages: string | undefined;
|
|
2787
|
+
if (flags.images !== undefined) {
|
|
2788
|
+
// Refused rather than ignored, for the reason `chainfit` refuses `--atlas`:
|
|
2789
|
+
// a flag that silently does nothing is worse than one that says why it
|
|
2790
|
+
// cannot. Measured โ an `--art none` spec rebuilt with `--images` naming a
|
|
2791
|
+
// directory that does not exist is byte-identical to one rebuilt without
|
|
2792
|
+
// it, because no attachment carries an `image` for that directory to be the
|
|
2793
|
+
// base of.
|
|
2794
|
+
if (art === 'none') {
|
|
2795
|
+
throw new UsageError(
|
|
2796
|
+
'--images <dir> and --art none contradict: `none` writes width/height and no `image` at all, so the rig ' +
|
|
2797
|
+
"spec's `images` directory would be the base of nothing and no rebuild would read it. Use --art loose to " +
|
|
2798
|
+
'name an image per attachment, or drop --images โ an `--art none` spec rebuilds with `build --atlas-in ' +
|
|
2799
|
+
'<pack.atlas>`',
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
// Not checked for existence, deliberately: `ingest` reads the skeleton and
|
|
2803
|
+
// nothing else, so the parts may well be extracted AFTER the specs are
|
|
2804
|
+
// written, and refusing a directory this command never opens would refuse a
|
|
2805
|
+
// legitimate order of work. `build` is where a missing PNG is named.
|
|
2806
|
+
specImages = relativeImagesPath(outDir, resolve(flags.images));
|
|
2807
|
+
}
|
|
2808
|
+
console.log(`rigc ingest ${skeletonPath}`);
|
|
2809
|
+
console.log(` .. out ${outDir}`);
|
|
2810
|
+
console.log(` .. art ${art}`);
|
|
2811
|
+
if (specImages !== undefined) console.log(` .. images ${specImages} (the rig spec's own, from ${outDir})`);
|
|
2812
|
+
|
|
2813
|
+
const result = ingest(readJsonFile(skeletonPath), {
|
|
2814
|
+
name: flags.name ?? basename(skeletonPath, '.json'),
|
|
2815
|
+
art,
|
|
2816
|
+
images: specImages,
|
|
2817
|
+
stage,
|
|
2818
|
+
source: basename(skeletonPath),
|
|
2819
|
+
version: readVersion(),
|
|
2820
|
+
});
|
|
2821
|
+
|
|
2822
|
+
mkdirSync(outDir, { recursive: true });
|
|
2823
|
+
// Indent 2, which is what `compile` writes the skeleton with. One emitter
|
|
2824
|
+
// convention, so a spec and the skeleton it came from read the same way.
|
|
2825
|
+
writeFileSync(join(outDir, 'rig.json'), `${JSON.stringify(result.rig, null, 2)}\n`);
|
|
2826
|
+
writeFileSync(join(outDir, 'motion.json'), `${JSON.stringify(result.motion, null, 2)}\n`);
|
|
2827
|
+
writeFileSync(join(outDir, 'findings.json'), `${JSON.stringify(result.findings, null, 2)}\n`);
|
|
2828
|
+
|
|
2829
|
+
// Grouped by kind rather than printed in discovery order: a blocker is what
|
|
2830
|
+
// decides the exit code, and a reader scanning for one should not have to
|
|
2831
|
+
// read past a hundred DURATION lines to find it.
|
|
2832
|
+
const GUTTER: Record<IngestFindingKind, string> = { blocker: 'BLOCK', judgement: 'JUDGE', lossy: 'LOSS ' };
|
|
2833
|
+
for (const kind of ['blocker', 'judgement', 'lossy'] as const) {
|
|
2834
|
+
for (const finding of result.findings.filter((f) => f.kind === kind)) {
|
|
2835
|
+
console.log(` ${GUTTER[kind]} ${finding.code}: ${finding.where} โ ${finding.detail}`);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
console.log(`rigc: wrote ${join(outDir, 'rig.json')}`);
|
|
2839
|
+
console.log(`rigc: wrote ${join(outDir, 'motion.json')}`);
|
|
2840
|
+
console.log(`rigc: wrote ${join(outDir, 'findings.json')}`);
|
|
2841
|
+
// The hint is the command the caller will actually run, so it drops `--images`
|
|
2842
|
+
// exactly when the spec now carries the directory itself โ a hint that asks for
|
|
2843
|
+
// a flag the spec made unnecessary is the defect issue #595 is about, printed.
|
|
2844
|
+
const artFlag = art === 'none' ? ' --atlas-in <pack.atlas>' : specImages === undefined ? ' --images <dir>' : '';
|
|
2845
|
+
console.log(
|
|
2846
|
+
`rigc: build it with rigc build --rig ${join(outDir, 'rig.json')} --motion ${join(outDir, 'motion.json')}` +
|
|
2847
|
+
`${artFlag} --out <dir>`,
|
|
2848
|
+
);
|
|
2849
|
+
|
|
2850
|
+
const blockers = result.findings.filter((f) => f.kind === 'blocker');
|
|
2851
|
+
if (blockers.length > 0) {
|
|
2852
|
+
console.error(
|
|
2853
|
+
`rigc: ${blockers.length} blocker(s) โ both specs were written, and a build from them will NOT be the ` +
|
|
2854
|
+
`skeleton that was read (${[...new Set(blockers.map((f) => f.code))].join(', ')})`,
|
|
2855
|
+
);
|
|
2856
|
+
process.exit(1);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2682
2860
|
// ---------------------------------------------------------------------------
|
|
2683
2861
|
// usage / per-command help
|
|
2684
2862
|
// ---------------------------------------------------------------------------
|
|
@@ -2767,11 +2945,25 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
2767
2945
|
`printed (default ${DEFAULT_MIN_LEVER_PX}); below it the bone is refused \`no-bracket\` naming the measured ` +
|
|
2768
2946
|
'lever, because an angle read across a short lever turns a half-pixel anchor error into several degrees',
|
|
2769
2947
|
animation: 'which animation to show; the default is every one for `render` and the first for `preview`',
|
|
2948
|
+
skin:
|
|
2949
|
+
'pose under this skin, by the name the skeleton declares. Without it NO skin is set โ every slot resolves ' +
|
|
2950
|
+
'through the default skin alone, so a slot whose art lives only in a named skin draws nothing. A name the ' +
|
|
2951
|
+
'skeleton does not declare is refused with the ones it does. `render` records the skin in frames.json and ' +
|
|
2952
|
+
'`check` reads it back, so a skin-A candidate is not scored against skin-B frames in silence',
|
|
2770
2953
|
max: 'longest side of a rendered frame, in pixels (default 256)',
|
|
2771
2954
|
record: 'a saved vote to check against its ballot and append to the ledger, instead of writing a ballot',
|
|
2772
2955
|
ballot: `the ballot the --record'd vote answers (default \`${DEFAULT_BALLOT}\`); its embedded manifest is what the vote is checked against`,
|
|
2773
2956
|
ledger: `the append-only JSONL the vote lands in (default \`${DEFAULT_LEDGER}\`)`,
|
|
2774
2957
|
again: 'record a second vote on a ballot the ledger already has; without it, a repeat is refused rather than doubled',
|
|
2958
|
+
name: "the rig spec's own name, which the motion spec's archetype must match (default: the skeleton file's basename)",
|
|
2959
|
+
art: 'how the written spec reaches the art, which a skeleton does not encode: `loose` names an image per ' +
|
|
2960
|
+
"attachment, measured out of the rig spec's own images directory (--images writes it; without it, `build " +
|
|
2961
|
+
'--images <dir>` on every rebuild), `none` states width/height only for `build --atlas-in <pack>` to ' +
|
|
2962
|
+
'resolve (default: loose)',
|
|
2963
|
+
stage:
|
|
2964
|
+
"the setup bounding box โ `skeleton.x,y,width,height`. An editor export carries none and rigc refuses a " +
|
|
2965
|
+
'compile without one; posing the rig gives the ANIMATED extent, which is a different number, so this is the ' +
|
|
2966
|
+
"caller's value and is never derived. Without it the missing stage is reported as a blocker",
|
|
2775
2967
|
help: "show this command's flags and exit",
|
|
2776
2968
|
};
|
|
2777
2969
|
|
|
@@ -2812,10 +3004,14 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
2812
3004
|
'anchor-residual': '<0..1>',
|
|
2813
3005
|
'inward-lever': '<px>',
|
|
2814
3006
|
animation: '<name>',
|
|
3007
|
+
skin: '<name>',
|
|
2815
3008
|
max: '<px>',
|
|
2816
3009
|
record: '<result.json>',
|
|
2817
3010
|
ballot: '<ballot.html>',
|
|
2818
3011
|
ledger: '<votes.jsonl>',
|
|
3012
|
+
name: '<n>',
|
|
3013
|
+
art: 'loose|none',
|
|
3014
|
+
stage: '<x,y,w,h>',
|
|
2819
3015
|
};
|
|
2820
3016
|
|
|
2821
3017
|
interface CommandDoc {
|
|
@@ -2828,14 +3024,25 @@ interface CommandDoc {
|
|
|
2828
3024
|
* Per-command wording for a flag whose value or meaning genuinely differs here.
|
|
2829
3025
|
*
|
|
2830
3026
|
* โ ๏ธ The default above it โ one meaning per flag name, everywhere โ is the rule
|
|
2831
|
-
* and this is the named exception to it, not a second table.
|
|
2832
|
-
*
|
|
3027
|
+
* and this is the named exception to it, not a second table. What earns an entry
|
|
3028
|
+
* is the criterion rather than a headcount: the flag is **shared with another
|
|
3029
|
+
* command**, and it means something different in this one. Examples, and not an
|
|
3030
|
+
* inventory: `--out` is a directory of artifacts to `build`, a directory of specs
|
|
3031
|
+
* to `ingest`, a directory of pictures to
|
|
2833
3032
|
* `render` and one file to `preview` and `vote`; `--fps` is the rate a frame set
|
|
2834
3033
|
* was RECORDED at to `check`, which reads it off a sidecar, and the rate to
|
|
2835
3034
|
* SAMPLE at to `render`, which is choosing it; `--candidate` is one artifact
|
|
2836
3035
|
* everywhere except `vote`, which is the one command that takes several and is
|
|
2837
3036
|
* the reason there is a ballot at all. Writing any of them as one sentence
|
|
2838
3037
|
* covering every command would leave every command's own help less true.
|
|
3038
|
+
*
|
|
3039
|
+
* โ The other side of the criterion, which is the one that keeps this from
|
|
3040
|
+
* becoming the second table it says it is not: a flag no other command takes has
|
|
3041
|
+
* nothing to differ FROM, so its wording belongs in `FLAG_MEANINGS` /
|
|
3042
|
+
* `FLAG_VALUES` above and an entry here for it buys only a second place to look.
|
|
3043
|
+
* Both halves are read off `--help` by `CLI71` in `selftest.ts`, which is why
|
|
3044
|
+
* this sentence no longer counts anything: it said *"three"* where #605 counted
|
|
3045
|
+
* nine, and nothing had ever compared the two.
|
|
2839
3046
|
*/
|
|
2840
3047
|
overrides?: Record<string, { value?: string; meaning?: string }>;
|
|
2841
3048
|
}
|
|
@@ -2878,6 +3085,25 @@ const COMMANDS: CommandDoc[] = [
|
|
|
2878
3085
|
],
|
|
2879
3086
|
flags: ['atlas', 'profile', 'cut', 'cuts', 'rig', 'motion', 'out', 'manifest', 'images'],
|
|
2880
3087
|
},
|
|
3088
|
+
{
|
|
3089
|
+
name: 'ingest',
|
|
3090
|
+
usage: ['rigc ingest <skeleton.json> --out <dir> [--name <n>] [--art loose|none] [--images <dir>] [--stage x,y,w,h]'],
|
|
3091
|
+
flags: ['out', 'name', 'art', 'images', 'stage'],
|
|
3092
|
+
overrides: {
|
|
3093
|
+
out: {
|
|
3094
|
+
value: '<dir>',
|
|
3095
|
+
meaning: 'directory to write rig.json, motion.json and findings.json into โ the two specs that rebuild this skeleton',
|
|
3096
|
+
},
|
|
3097
|
+
images: {
|
|
3098
|
+
value: '<dir>',
|
|
3099
|
+
meaning:
|
|
3100
|
+
"WRITE the rig spec's own images directory, spelled relative to --out, so the rebuild is a plain `build " +
|
|
3101
|
+
'--rig โฆ --motion โฆ --out โฆ` with no flag. โ ๏ธ The opposite direction from `build --images`, which ' +
|
|
3102
|
+
'OVERRIDES that field: this one fills it in. Without it the field is left out and every `image` resolves ' +
|
|
3103
|
+
'against --out itself. Refused together with --art none, which writes no `image` for it to be the base of',
|
|
3104
|
+
},
|
|
3105
|
+
},
|
|
3106
|
+
},
|
|
2881
3107
|
{
|
|
2882
3108
|
name: 'diff',
|
|
2883
3109
|
usage: ['rigc diff <candidate.json> <reference.json> [--json <out>]'],
|
|
@@ -2886,7 +3112,16 @@ const COMMANDS: CommandDoc[] = [
|
|
|
2886
3112
|
{
|
|
2887
3113
|
name: 'check',
|
|
2888
3114
|
usage: ['rigc check --candidate <dir | skeleton.json> --frames <dir> [flags]'],
|
|
2889
|
-
flags: ['candidate', 'frames', 'atlas', 'texture-from', 'fps', 'viewport', 'framing', 'as', 'all-frames', 'json'],
|
|
3115
|
+
flags: ['candidate', 'frames', 'atlas', 'texture-from', 'fps', 'viewport', 'framing', 'as', 'skin', 'all-frames', 'json'],
|
|
3116
|
+
overrides: {
|
|
3117
|
+
skin: {
|
|
3118
|
+
meaning:
|
|
3119
|
+
'pose the CANDIDATE under this skin, by the name it declares. Without it no skin is set and the ' +
|
|
3120
|
+
'default skin alone is compared, which for a multi-skin rig is a comparison that can see none of the ' +
|
|
3121
|
+
'contested art. The frames are checked back: a set whose frames.json records a different skin is ' +
|
|
3122
|
+
'REFUSED by name, and one that records none says so in the report rather than pretending to agree',
|
|
3123
|
+
},
|
|
3124
|
+
},
|
|
2890
3125
|
},
|
|
2891
3126
|
{
|
|
2892
3127
|
name: 'bench',
|
|
@@ -2914,9 +3149,9 @@ const COMMANDS: CommandDoc[] = [
|
|
|
2914
3149
|
{
|
|
2915
3150
|
name: 'render',
|
|
2916
3151
|
usage: [
|
|
2917
|
-
'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--fps 12] [--max 256] [--out render/]',
|
|
3152
|
+
'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--skin <name>] [--fps 12] [--max 256] [--out render/]',
|
|
2918
3153
|
],
|
|
2919
|
-
flags: ['candidate', 'atlas', 'animation', 'fps', 'max', 'out'],
|
|
3154
|
+
flags: ['candidate', 'atlas', 'animation', 'skin', 'fps', 'max', 'out'],
|
|
2920
3155
|
overrides: {
|
|
2921
3156
|
out: { value: '<dir>', meaning: 'directory to write the frame series into (default `render/`)' },
|
|
2922
3157
|
fps: { meaning: `frames per second to sample the animation at (default ${PROTOCOL_FPS})` },
|
|
@@ -3070,6 +3305,20 @@ const USAGE = [
|
|
|
3070
3305
|
'draws with rigc\'s own rasteriser; preview embeds the artifact in a page that plays',
|
|
3071
3306
|
'it in the official Spine Web Player, which is also the interop proof.',
|
|
3072
3307
|
'',
|
|
3308
|
+
'ingest runs build backwards: it reads a Spine 4.3 skeleton.json and writes the rig',
|
|
3309
|
+
'spec and motion spec that rebuild it, so an existing skeleton becomes a starting',
|
|
3310
|
+
'point instead of something to retype:',
|
|
3311
|
+
' rigc ingest hero.json --out specs/ --stage 0,0,1024,768 rig.json + motion.json',
|
|
3312
|
+
'The contract is an equality, not a rulebook: build(ingest(x)) is x, byte for byte.',
|
|
3313
|
+
'It reads the skeleton and nothing else โ no .spine project, no binary .skel, no',
|
|
3314
|
+
'atlas โ so two things are the caller\'s and are refused rather than guessed: the',
|
|
3315
|
+
'setup stage (--stage; an export carries none) and how the spec reaches the art',
|
|
3316
|
+
'(--art). --images <dir> is the third and the only optional one: it WRITES the rig',
|
|
3317
|
+
'spec\'s own images directory, relative to --out, so the rebuild needs no flag.',
|
|
3318
|
+
'Everything the spec format cannot hold is printed as a named finding and',
|
|
3319
|
+
'exits non-zero, with both files still written, because a spec plus a list of what',
|
|
3320
|
+
'is missing from it beats no spec at all.',
|
|
3321
|
+
'',
|
|
3073
3322
|
'pose runs the other way round from everything above: it reads a picture you already',
|
|
3074
3323
|
'have โ one key pose โ and reports where each loose part PNG sits in it (x, y, rotation,',
|
|
3075
3324
|
'scale) so an agent can state those poses in a spec by construction:',
|
|
@@ -3130,6 +3379,7 @@ try {
|
|
|
3130
3379
|
process.exit(0);
|
|
3131
3380
|
}
|
|
3132
3381
|
if (command === 'build') cmdBuild(flags);
|
|
3382
|
+
else if (command === 'ingest') cmdIngest(flags, positional);
|
|
3133
3383
|
else if (command === 'validate') cmdValidate(flags, positional);
|
|
3134
3384
|
else if (command === 'explain') cmdExplain(flags);
|
|
3135
3385
|
else if (command === 'diff') cmdDiff(flags, positional);
|