spine-rigc 0.9.0 → 0.11.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 +41 -13
- package/cli.ts +349 -14
- package/docs/AUTHORING.md +280 -24
- package/docs/INGEST.md +1117 -0
- package/docs/MOTION.md +16 -0
- package/docs/SPEC_COVERAGE.md +9 -2
- package/package.json +2 -1
- package/src/atlas.ts +871 -0
- package/src/bonedist.ts +632 -0
- package/src/check.ts +5 -2
- package/src/compile.ts +485 -59
- package/src/diff.ts +269 -1
- package/src/mesh.ts +183 -22
- package/src/render.ts +88 -3
- package/src/types.ts +56 -4
- package/src/validate.ts +45 -22
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@ parser and a list of named assertions all come back green.
|
|
|
30
30
|
| You have | You run | You get |
|
|
31
31
|
| --- | --- | --- |
|
|
32
32
|
| part PNGs, a rig spec and a motion spec | `rigc build` | `skeleton.json` + `skeleton.atlas` — or a failure named by rule, and **nothing on disk** |
|
|
33
|
+
| the same, and one texture instead of many | `rigc build --pack` | the parts arranged onto shared atlas pages, written beside the skeleton — losslessly, so the picture is the picture |
|
|
34
|
+
| a pack somebody already made | `rigc build --atlas-in` | the same skeleton, with every part resolved to a region of that atlas — or a named refusal, never a part that silently does not draw |
|
|
33
35
|
| a compiled rig | `rigc render` | every animation as PNG frames, plus one labelled contact sheet of the whole shot |
|
|
34
36
|
| a compiled rig | `rigc preview` | one self-contained `.html` that plays it in Spine's own web player |
|
|
35
37
|
| two to four compiled rigs | `rigc vote` | one ballot page a human picks from, and the answer checked into a ledger |
|
|
@@ -81,10 +83,11 @@ run a list of named assertions, and **write nothing unless all of them are green
|
|
|
81
83
|
|
|
82
84
|
## Install
|
|
83
85
|
|
|
84
|
-
📦 **rigc measures loose PNGs directly
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
📦 **rigc measures loose PNGs directly, and emits one atlas page per image unless
|
|
87
|
+
you ask otherwise.** `rigc build --pack` arranges every part onto shared pages and
|
|
88
|
+
writes them into `--out`; `--atlas-in` builds against a pack somebody else made.
|
|
89
|
+
Both are opt-in and both are narrow — no trimming, no rotation, no scaling — and
|
|
90
|
+
[AUTHORING §0.1–§0.2](docs/AUTHORING.md) states the limits before you hit them.
|
|
88
91
|
|
|
89
92
|
rigc runs on [Bun](https://bun.sh). The package ships its TypeScript sources and
|
|
90
93
|
Bun runs them, so there is no build step and no `dist/` that can drift from the
|
|
@@ -370,6 +373,22 @@ A part that matches nowhere is refused by name, two near-equal placements are re
|
|
|
370
373
|
as both, and nothing it prints is a score. Fields, the coordinate contract and the
|
|
371
374
|
limits: [AUTHORING.md §11](docs/AUTHORING.md).
|
|
372
375
|
|
|
376
|
+
## The gallery — four complete rigs over art that ships with them
|
|
377
|
+
|
|
378
|
+
Each directory in [`gallery/`](https://github.com/firejune/rigc/tree/main/gallery) is
|
|
379
|
+
one rig spec, one motion spec and the PNGs they name, small enough to read in one
|
|
380
|
+
sitting. Each stars a single feature, so *how do I do X* has a working answer rather
|
|
381
|
+
than a field table, and each README carries the frame rate it was authored at, what
|
|
382
|
+
was verified, and what writing it cost. Repository material: a clone and
|
|
383
|
+
`bun install` runs them.
|
|
384
|
+
|
|
385
|
+
| Example | Stars | What it is |
|
|
386
|
+
| --- | --- | --- |
|
|
387
|
+
| [`gallery/walk`](https://github.com/firejune/rigc/tree/main/gallery/walk) | `ik` constraints + **`ik` timelines** | Two two-bone leg chains solved to foot targets — the planted leg nailed down, the swinging one let go at the top of its lift |
|
|
388
|
+
| [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash) | **`deform` timelines** | A ball squashed about its contact point and stretched along its travel, from two affine transforms written out in the README |
|
|
389
|
+
| [`gallery/flex`](https://github.com/firejune/rigc/tree/main/gallery/flex) | **`contour` meshes** | A swallow-tailed banner and a serrated leaf: four meshes traced off their own alpha, waved by bone timelines and rippled by a `deform` |
|
|
390
|
+
| [`gallery/ride`](https://github.com/firejune/rigc/tree/main/gallery/ride) | `path` attachments + **path constraints** | A trolley coasting down a drawn rail and rolling back, driven by a `position` timeline, with `groups` + `stagger` keying the wheels and the ears |
|
|
391
|
+
|
|
373
392
|
## Commands
|
|
374
393
|
|
|
375
394
|
Every command takes its paths explicitly. `rigc <command> --help` prints its flags, and
|
|
@@ -379,6 +398,8 @@ commands take it and what its default is.
|
|
|
379
398
|
| Command | Does |
|
|
380
399
|
| --- | --- |
|
|
381
400
|
| `build --rig … --motion … --out …` | compiles, gates, and **writes only if the gate is green**. `--images <dir>` says where the rig spec's `image` names resolve, `--manifest` adds measured art, and `--copy-images` copies every page PNG into `--out` so the directory is self-contained |
|
|
401
|
+
| `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 |
|
|
402
|
+
| `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 |
|
|
382
403
|
| `validate <dir>` | re-gates artifacts already on disk |
|
|
383
404
|
| `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 |
|
|
384
405
|
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/` |
|
|
@@ -413,6 +434,7 @@ the art. Its shape is under
|
|
|
413
434
|
| --- | --- |
|
|
414
435
|
| 📘 **[docs/AUTHORING.md](docs/AUTHORING.md)** | **the format guide, and the one to read before writing a spec.** Both input files field by field with a complete minimal example each, every field with its Spine meaning, the rules that decide what is emitted, the build → read the report → fix → repeat loop, the map from every named failure to the file that has to change, and the features rigc refuses by name so you do not spend a loop discovering them. It travels **inside the npm package**, at `node_modules/spine-rigc/docs/AUTHORING.md` |
|
|
415
436
|
| 🎞️ **[docs/MOTION.md](docs/MOTION.md)** | **the key-pose recipe.** How to get two poses, what a pair of poses does and does not fix, the in-betweening rules and where each comes from, and how to spread candidates so a ballot informs. Ships in the package too |
|
|
437
|
+
| 📥 **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
|
|
416
438
|
| 🤖 **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** — the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
|
|
417
439
|
| 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
|
|
418
440
|
| 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 36 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
|
|
@@ -424,21 +446,27 @@ rigc is measured against **Spine's own official example projects** — the
|
|
|
424
446
|
`1-weight-and-mass` … `8-follow-through` series as a difficulty ladder, with spineboy
|
|
425
447
|
as the graduation exam.
|
|
426
448
|
|
|
427
|
-
🎓 **The ladder
|
|
428
|
-
spineboy graduation exam cleared
|
|
449
|
+
🎓 **The ladder is complete, 2026-08-28.** All eight numbered rungs and the
|
|
450
|
+
spineboy graduation exam are cleared and hold under the current gate, **v2.3**, every clause PASS or SKIP:
|
|
429
451
|
worst attributable slot drift **5.55 px** against a 6.0 px bar, and **0 of 124**
|
|
430
452
|
frame-change disagreements. Recompiling the same spec in a different session
|
|
431
453
|
reproduced every field of the measurement record **to the digit**. The rungs stay
|
|
432
454
|
in place as regression gates.
|
|
433
455
|
|
|
434
|
-
|
|
435
|
-
`check`'s extent tolerance ([PR #254](https://github.com/firejune/rigc/pull/254))
|
|
436
|
-
changed which box a set is measured in, and rung 7's stored candidate
|
|
456
|
+
🗓️ **One rung's pass was withdrawn and restored on 2026-09-02, and both are dated
|
|
457
|
+
facts.** `check`'s extent tolerance ([PR #254](https://github.com/firejune/rigc/pull/254))
|
|
458
|
+
changed which box a set is measured in, and rung 7's stored candidate failed **G2**
|
|
437
459
|
under it — one of its three slots draws in every set and is attributable in none,
|
|
438
|
-
and no read-down
|
|
439
|
-
|
|
440
|
-
and
|
|
441
|
-
|
|
460
|
+
and no read-down ground survived the framing change. The gate then answered the two
|
|
461
|
+
clause questions that exposed, as **v2.3**: a read-down names the framing of its
|
|
462
|
+
evidence, and a slot whose attributability is **measured** to be capped below the bar
|
|
463
|
+
reads down when everything observable about it is independently verified strict. That
|
|
464
|
+
rung's third attempt clears on those grounds, on the candidate it already had.
|
|
465
|
+
**Rungs 1–6 and 8 and the graduation exam were unaffected throughout**: each reproduces
|
|
466
|
+
its gated figures to the digit, and the 5.55 px and 0-of-124 figures above are among
|
|
467
|
+
them. Both verdicts, and the sweep of every candidate under the new gate, are in
|
|
468
|
+
[docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md)'s *PR #254 instrument re-inspection* and *gate-v2.3
|
|
469
|
+
re-inspection*.
|
|
442
470
|
|
|
443
471
|
⚠️ **What that certifies, stated exactly.** That **the tool, the guide and the
|
|
444
472
|
protocol reach the bar across a bounded series of honest attempts, each residual
|
package/cli.ts
CHANGED
|
@@ -48,10 +48,19 @@ import {
|
|
|
48
48
|
type BallotCandidateInput,
|
|
49
49
|
type BallotInput,
|
|
50
50
|
} from './src/ballot.ts';
|
|
51
|
+
import {
|
|
52
|
+
boneDistance,
|
|
53
|
+
BONEDIST_SPEC,
|
|
54
|
+
boneDistLines,
|
|
55
|
+
BoneDistError,
|
|
56
|
+
IDENTITY_CORRESPONDENCE,
|
|
57
|
+
type BoneDistReport,
|
|
58
|
+
} from './src/bonedist.ts';
|
|
51
59
|
import { checkAgainstFrames, checkLines, CheckError, type CheckOptions, type CheckReport } from './src/check.ts';
|
|
52
60
|
import { compile, CompileError, type CompileOptions } from './src/compile.ts';
|
|
53
|
-
import { diffLines, diffSkeletons, sectionFigures, type DiffReport } from './src/diff.ts';
|
|
61
|
+
import { diffLines, diffSkeletons, reportedFigures, sectionFigures, type DiffReport } from './src/diff.ts';
|
|
54
62
|
import { copyAtlasImages } from './src/emit.ts';
|
|
63
|
+
import { DEFAULT_PADDING, DEFAULT_PAGE_SIZE, packAtlas } from './src/atlas.ts';
|
|
55
64
|
import { parseJsonWithPosition } from './src/json-position.ts';
|
|
56
65
|
import { findRung, RUNG_IDS, type RungSkeleton } from './src/ladder.ts';
|
|
57
66
|
import {
|
|
@@ -152,7 +161,7 @@ function repositoryUrl(): string {
|
|
|
152
161
|
* flag": inferring it would turn `--out --json report.json` — a real typo, a
|
|
153
162
|
* missing value — into a silently accepted switch plus a stray positional.
|
|
154
163
|
*/
|
|
155
|
-
const BOOLEAN_FLAGS = new Set(['all-frames', 'help', 'copy-images', 'again']);
|
|
164
|
+
const BOOLEAN_FLAGS = new Set(['all-frames', 'help', 'copy-images', 'again', 'pack']);
|
|
156
165
|
|
|
157
166
|
/**
|
|
158
167
|
* The flags a command is allowed to spell more than once.
|
|
@@ -276,6 +285,7 @@ function resolveCut(flags: Record<string, string>): { label: string; opts: Compi
|
|
|
276
285
|
};
|
|
277
286
|
if (flags.manifest !== undefined) opts.manifestPath = resolve(flags.manifest);
|
|
278
287
|
if (flags.images !== undefined) opts.imagesDir = resolve(flags.images);
|
|
288
|
+
if (flags['atlas-in'] !== undefined) opts.atlasInPath = resolve(flags['atlas-in']);
|
|
279
289
|
return { label: flags.rig, opts };
|
|
280
290
|
}
|
|
281
291
|
if (flags.cut === undefined) throw new UsageError('give either --cut <name> --cuts <cuts.json>, or --rig/--motion/--out');
|
|
@@ -287,7 +297,13 @@ function resolveCut(flags: Record<string, string>): { label: string; opts: Compi
|
|
|
287
297
|
`unknown cut ${JSON.stringify(flags.cut)} in ${resolve(flags.cuts)}. known: ${Object.keys(table).join(', ') || '(none)'}`,
|
|
288
298
|
);
|
|
289
299
|
}
|
|
290
|
-
|
|
300
|
+
const opts = entryToOptions(dir, flags.cut, entry);
|
|
301
|
+
// `--atlas-in` is not part of the cuts table: a cut names its rig, motion and
|
|
302
|
+
// manifest, and where the pixels are delivered from is a property of the BUILD.
|
|
303
|
+
// Resolved against the working directory, like every other path on the command
|
|
304
|
+
// line, rather than against the table's directory.
|
|
305
|
+
if (flags['atlas-in'] !== undefined) opts.atlasInPath = resolve(flags['atlas-in']);
|
|
306
|
+
return { label: flags.cut, opts };
|
|
291
307
|
}
|
|
292
308
|
|
|
293
309
|
// ---------------------------------------------------------------------------
|
|
@@ -311,15 +327,35 @@ function readProfile(flags: Record<string, string>): ValidateProfile {
|
|
|
311
327
|
return found;
|
|
312
328
|
}
|
|
313
329
|
|
|
314
|
-
|
|
330
|
+
/**
|
|
331
|
+
* An atlas text to gate INSTEAD of the compile's own, with the second, independent
|
|
332
|
+
* emit A18 compares it against.
|
|
333
|
+
*
|
|
334
|
+
* `--pack` is the only caller. A packed build is gated twice on purpose — once as
|
|
335
|
+
* compiled (which is the gate that reads the loose PNGs, so `A06`'s size-vs-file
|
|
336
|
+
* clause still measures the art R5 measures) and once as packed (which is the pair
|
|
337
|
+
* that actually ships). Handing the second pass its texts rather than re-deriving
|
|
338
|
+
* them here keeps `runGate` ignorant of what a pack is.
|
|
339
|
+
*/
|
|
340
|
+
interface AtlasOverride {
|
|
341
|
+
text: string;
|
|
342
|
+
again: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function runGate(
|
|
346
|
+
result: CompileResult,
|
|
347
|
+
opts: CompileOptions,
|
|
348
|
+
profile: ValidateProfile,
|
|
349
|
+
atlas?: AtlasOverride,
|
|
350
|
+
): number {
|
|
315
351
|
// The determinism check compares a second, independent compile.
|
|
316
352
|
const again = compile(opts);
|
|
317
353
|
const report = validate({
|
|
318
354
|
skeletonText: result.skeletonText,
|
|
319
|
-
atlasText: result.atlasText,
|
|
355
|
+
atlasText: atlas ? atlas.text : result.atlasText,
|
|
320
356
|
atlasDir: opts.outDir,
|
|
321
357
|
declaredDurations: result.declaredDurations,
|
|
322
|
-
reEmit: { skeletonText: again.skeletonText, atlasText: again.atlasText },
|
|
358
|
+
reEmit: { skeletonText: again.skeletonText, atlasText: atlas ? atlas.again : again.atlasText },
|
|
323
359
|
rig: result.rig,
|
|
324
360
|
profile,
|
|
325
361
|
});
|
|
@@ -346,15 +382,88 @@ const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> =
|
|
|
346
382
|
authored: 'authored geometry rigc did not build; it assumes nothing about the topology',
|
|
347
383
|
};
|
|
348
384
|
|
|
349
|
-
/**
|
|
385
|
+
/**
|
|
386
|
+
* What a mesh measured about its own fit against the art it names, or nothing
|
|
387
|
+
* for a mesh with no art to measure against.
|
|
388
|
+
*
|
|
389
|
+
* Printed for authored geometry as well as for a `contour` (issue #277): the
|
|
390
|
+
* figure is a measurement between the emitted triangles and the PNG, so it means
|
|
391
|
+
* the same thing whoever drew the vertices, and the silence was the defect —
|
|
392
|
+
* an octagon rim placed on a round part's silhouette clips its own ink outline
|
|
393
|
+
* at 94.31% and used to print nothing at all.
|
|
394
|
+
*
|
|
395
|
+
* The hole is appended only when there is one, so the common line is unchanged.
|
|
396
|
+
* It is the one figure in the report that a hole moves: `coverage` and
|
|
397
|
+
* `overshoot` are both measured against the FILLED silhouette, so spanning an
|
|
398
|
+
* interior hole is neither missing coverage nor reaching past anything, and an
|
|
399
|
+
* unintentional hole — a gap in the art, a stroke that failed to join — bought
|
|
400
|
+
* fill over transparent pixels with nothing anywhere saying so (issue #275).
|
|
401
|
+
*/
|
|
350
402
|
function meshFit(m: CompileResult['meshes'][number]): string {
|
|
351
403
|
if (m.coverage === undefined) return '';
|
|
352
|
-
|
|
404
|
+
const hole = m.holePixels ? `, enclosing ${m.holePixels}px of hole` : '';
|
|
405
|
+
return ` covers ${(m.coverage * 100).toFixed(2)}% of the art, reaching ${m.overshoot?.toFixed(2) ?? '?'}px past it${hole}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The triangle budget a `MESH` line is read against: the rig's, or nothing.
|
|
410
|
+
*
|
|
411
|
+
* 📐 It used to be the literal `80`, which was nobody's budget — the rig quoted
|
|
412
|
+
* in issue #275 declared 64, `A13_MESH_BUDGET` measured against that 64
|
|
413
|
+
* correctly, and the line an author actually reads printed 80. Under the default
|
|
414
|
+
* `--profile spine` `A13` is `PROF`, so the printed number is the only budget
|
|
415
|
+
* figure in the output and it has to be the declared one. A rig that declares
|
|
416
|
+
* none says so in the same words `A13` SKIPs in, rather than being given a wall.
|
|
417
|
+
*/
|
|
418
|
+
function meshBudget(rig: CompileResult['rig']): string {
|
|
419
|
+
return rig.meshTriangleBudget === null ? '(no budget declared)' : `(budget ${rig.meshTriangleBudget})`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Read one non-negative integer flag, or its default.
|
|
424
|
+
*
|
|
425
|
+
* A usage error rather than a `NaN` that reaches the packer: `--padding two`
|
|
426
|
+
* would otherwise place every region at NaN and write a blank page, which is a
|
|
427
|
+
* green build and an empty picture.
|
|
428
|
+
*/
|
|
429
|
+
function readIntFlag(flags: Record<string, string>, name: string, fallback: number): number {
|
|
430
|
+
const raw = flags[name];
|
|
431
|
+
if (raw === undefined) return fallback;
|
|
432
|
+
if (!/^\d+$/.test(raw)) throw new UsageError(`--${name} takes a non-negative integer, got ${JSON.stringify(raw)}`);
|
|
433
|
+
return Number(raw);
|
|
353
434
|
}
|
|
354
435
|
|
|
355
436
|
function cmdBuild(flags: Record<string, string>): void {
|
|
356
437
|
const { label, opts } = resolveCut(flags);
|
|
357
438
|
const profile = readProfile(flags);
|
|
439
|
+
const packing = flags.pack !== undefined;
|
|
440
|
+
// Three combinations are refused rather than silently resolved, because in each
|
|
441
|
+
// one the two flags disagree about a single question and there is no answer
|
|
442
|
+
// that is not a guess about which the caller meant.
|
|
443
|
+
if (packing && opts.atlasInPath !== undefined) {
|
|
444
|
+
throw new UsageError(
|
|
445
|
+
'--pack and --atlas-in are opposite directions through the same door: --pack MAKES an atlas out of the ' +
|
|
446
|
+
'loose parts, --atlas-in resolves the parts against one somebody already made. Pick one',
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (packing && flags['copy-images'] !== undefined) {
|
|
450
|
+
throw new UsageError(
|
|
451
|
+
'--pack already writes self-contained pages into --out (that is what packing is), and --copy-images copies ' +
|
|
452
|
+
'the loose part PNGs, which a packed atlas does not reference. Drop --copy-images',
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (packing && profile === 'spine-html') {
|
|
456
|
+
throw new UsageError(
|
|
457
|
+
"--profile spine-html asserts one part per page (A06's full-page coverage clause), which is rigc's unpacked " +
|
|
458
|
+
'convention and exactly what --pack stops being true. A packed atlas is valid Spine — build it under the ' +
|
|
459
|
+
'default --profile spine',
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (!packing) {
|
|
463
|
+
for (const name of ['page-size', 'padding'] as const) {
|
|
464
|
+
if (flags[name] !== undefined) throw new UsageError(`--${name} only means something with --pack`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
358
467
|
console.log(`rigc build ${label}`);
|
|
359
468
|
// Named explicitly and on their own lines rather than folded into the header
|
|
360
469
|
// above: with two input files, a header that names only one of them (the rig,
|
|
@@ -364,12 +473,26 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
364
473
|
console.log(` .. motion ${opts.motionPath}`);
|
|
365
474
|
const result = compile(opts);
|
|
366
475
|
|
|
476
|
+
if (opts.atlasInPath !== undefined) console.log(` .. atlas-in ${opts.atlasInPath}`);
|
|
367
477
|
console.log(` .. ${result.images.length} part page(s):`);
|
|
368
478
|
for (const img of result.images) {
|
|
369
|
-
|
|
479
|
+
// An imported part says where on the page it came from, because "resolved
|
|
480
|
+
// against a region" is the claim `--atlas-in` makes and a line that only
|
|
481
|
+
// repeated the page filename would look identical for all of them. A page
|
|
482
|
+
// that declares a `scale:` also says so and shows the texels it was read
|
|
483
|
+
// from: the size on the left is the DRAWING's and the rectangle is the
|
|
484
|
+
// pack's, and issue #267 is the report that printed the second as the first.
|
|
485
|
+
const where =
|
|
486
|
+
img.atlas === undefined
|
|
487
|
+
? img.page
|
|
488
|
+
: `${img.page} @ ${img.atlas.x},${img.atlas.y}${img.atlas.degrees ? ` rotate ${img.atlas.degrees}` : ''}` +
|
|
489
|
+
(img.atlasScale === undefined
|
|
490
|
+
? ''
|
|
491
|
+
: ` scale ${img.atlasScale} (${img.atlas.originalWidth}x${img.atlas.originalHeight} texels)`);
|
|
492
|
+
console.log(` .. ${img.region.padEnd(24)} ${img.width}x${img.height} <- ${where}`);
|
|
370
493
|
}
|
|
371
494
|
for (const d of result.droppedStates) {
|
|
372
|
-
console.log(` DROP ${d.slot}/${d.state}: no PNG at ${d.path} (state not emitted)`);
|
|
495
|
+
console.log(` DROP ${d.slot}/${d.state}: ${d.why ?? `no PNG at ${d.path}`} (state not emitted)`);
|
|
373
496
|
}
|
|
374
497
|
// "The optional slots are optional" is a claim about this code path, so this
|
|
375
498
|
// code path says which ones it left out rather than being silently right.
|
|
@@ -379,7 +502,7 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
379
502
|
for (const m of result.meshes) {
|
|
380
503
|
console.log(
|
|
381
504
|
` MESH ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|
|
382
|
-
|
|
505
|
+
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}`,
|
|
383
506
|
);
|
|
384
507
|
}
|
|
385
508
|
for (const ph of result.physics) {
|
|
@@ -414,6 +537,64 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
414
537
|
}
|
|
415
538
|
}
|
|
416
539
|
|
|
540
|
+
// `--pack`: the parts go onto shared pages, which are written here as real
|
|
541
|
+
// PNGs, so `--out` is self-contained by construction. The atlas above stays
|
|
542
|
+
// the one the gate just read — packing changes only the ARRANGEMENT of the
|
|
543
|
+
// bytes, and the sizes in `result.images` are still the ones measured off the
|
|
544
|
+
// loose PNGs (see src/atlas.ts's header).
|
|
545
|
+
if (packing) {
|
|
546
|
+
const packOpts = {
|
|
547
|
+
pageSize: readIntFlag(flags, 'page-size', DEFAULT_PAGE_SIZE),
|
|
548
|
+
padding: readIntFlag(flags, 'padding', DEFAULT_PADDING),
|
|
549
|
+
pageStem: 'skeleton',
|
|
550
|
+
};
|
|
551
|
+
const inputs = result.images.map((img) => ({
|
|
552
|
+
region: img.region,
|
|
553
|
+
absPath: img.absPath,
|
|
554
|
+
width: img.width,
|
|
555
|
+
height: img.height,
|
|
556
|
+
}));
|
|
557
|
+
const packed = packAtlas(inputs, packOpts);
|
|
558
|
+
atlasText = packed.atlasText;
|
|
559
|
+
for (const page of packed.pages) {
|
|
560
|
+
page.plate.writePng(join(opts.outDir, page.name));
|
|
561
|
+
console.log(
|
|
562
|
+
` .. pack: ${page.name} ${page.width}x${page.height}, ` +
|
|
563
|
+
`${packed.placements.filter((p) => packed.pages[p.page].name === page.name).length} region(s), ` +
|
|
564
|
+
`${(page.occupancy * 100).toFixed(1)}% covered, padding ${packed.padding}`,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
for (const place of packed.placements) {
|
|
568
|
+
console.log(
|
|
569
|
+
` .. ${place.region.padEnd(24)} ${place.width}x${place.height} -> ` +
|
|
570
|
+
`${packed.pages[place.page].name} @ ${place.x},${place.y}`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
// The pages are on disk now, so the packed pair can be gated as an artifact
|
|
574
|
+
// rather than trusted as a construction: A17 stats every page, A06 reads its
|
|
575
|
+
// IHDR back, A07 re-reads the text shape, A08 re-joins every attachment onto
|
|
576
|
+
// a region, and A18 compares a second independent compile+pack. Two gates on
|
|
577
|
+
// one build is the cost of shipping a second atlas shape.
|
|
578
|
+
console.log(' .. validate (packed atlas, pages on disk)');
|
|
579
|
+
const packAgain = packAtlas(
|
|
580
|
+
compile(opts).images.map((img) => ({
|
|
581
|
+
region: img.region,
|
|
582
|
+
absPath: img.absPath,
|
|
583
|
+
width: img.width,
|
|
584
|
+
height: img.height,
|
|
585
|
+
})),
|
|
586
|
+
packOpts,
|
|
587
|
+
);
|
|
588
|
+
const packFailures = runGate(result, opts, profile, { text: atlasText, again: packAgain.atlasText });
|
|
589
|
+
if (packFailures > 0) {
|
|
590
|
+
console.error(
|
|
591
|
+
`rigc: ${packFailures} assertion(s) failed on the PACKED atlas — the pages were written to ` +
|
|
592
|
+
`${opts.outDir}, the skeleton/atlas pair was not`,
|
|
593
|
+
);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
417
598
|
writeFileSync(join(opts.outDir, 'skeleton.json'), result.skeletonText);
|
|
418
599
|
writeFileSync(join(opts.outDir, 'skeleton.atlas'), atlasText);
|
|
419
600
|
console.log(`rigc: wrote ${join(opts.outDir, 'skeleton.json')}`);
|
|
@@ -1172,6 +1353,38 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
1172
1353
|
diffs.push({ skeleton, reference: referencePath, report: diff });
|
|
1173
1354
|
}
|
|
1174
1355
|
|
|
1356
|
+
// Stage 3, optional and behind a flag because the correspondence is an INPUT:
|
|
1357
|
+
// a candidate is entitled to its own bone names, so there is nothing sensible
|
|
1358
|
+
// to default to and a derived mapping would be a guess reported as a
|
|
1359
|
+
// measurement (issue #8). Nothing here gates, and without the flag the report
|
|
1360
|
+
// above is unchanged to the byte.
|
|
1361
|
+
const boneDists: Array<{ skeleton: RungSkeleton; report: BoneDistReport }> = [];
|
|
1362
|
+
if (flags.bones !== undefined) {
|
|
1363
|
+
for (const skeleton of rung.skeletons) {
|
|
1364
|
+
const referencePath = join(exportDir, skeleton.file);
|
|
1365
|
+
if (!existsSync(referencePath)) continue;
|
|
1366
|
+
console.log(` ── bonedist vs ${rung.example}/${skeleton.label} (stage 3) ──`);
|
|
1367
|
+
const boneDist = boneDistance({
|
|
1368
|
+
candidateSkeleton: skeletonPath,
|
|
1369
|
+
candidateAtlas: atlasPath,
|
|
1370
|
+
candidateAtlasDir: dirname(atlasPath),
|
|
1371
|
+
referenceSkeleton: referencePath,
|
|
1372
|
+
referenceAtlas: join(exportDir, skeleton.atlas),
|
|
1373
|
+
referenceAtlasDir: exportDir,
|
|
1374
|
+
bones: flags.bones,
|
|
1375
|
+
// Deliberately NOT `flags.fps`. Inside `bench` that flag already means
|
|
1376
|
+
// "the rate this frame set was recorded at, for a set with no sidecar",
|
|
1377
|
+
// and one flag doing two unrelated things in one command is how a
|
|
1378
|
+
// reader ends up quoting a figure measured at a rate they did not ask
|
|
1379
|
+
// for. A run wanting another sampling rate calls `rigc bonedist`, where
|
|
1380
|
+
// `--fps` has exactly one meaning.
|
|
1381
|
+
});
|
|
1382
|
+
for (const line of boneDistLines(boneDist, { allBones: flags['all-bones'] !== undefined })) console.log(` ${line}`);
|
|
1383
|
+
console.log('');
|
|
1384
|
+
boneDists.push({ skeleton, report: boneDist });
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1175
1388
|
// Third, optional and third for a reason: is it the same MOTION? `diff`
|
|
1176
1389
|
// compares structure, and a reversed easing is the same key count and the same
|
|
1177
1390
|
// curve kind — so a row of this ladder carrying only `validate` and `diff`
|
|
@@ -1197,6 +1410,13 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
1197
1410
|
// name-agnostic figure printed beside — issue #21.
|
|
1198
1411
|
const split = d.report.sections.filter((s) => s.nameAgnostic !== undefined);
|
|
1199
1412
|
if (split.length > 0) console.log(` ${''.padEnd(10)} ${split.map(sectionFigures).join(' ')}`);
|
|
1413
|
+
// A third line, for the same reason the second one is not folded into the
|
|
1414
|
+
// first: the reported measures are unobservable by construction, so they
|
|
1415
|
+
// roll into no mean at all and cannot be shown as one. Each is named with
|
|
1416
|
+
// its own figure — a per-section digest would be the mean this block exists
|
|
1417
|
+
// to refuse.
|
|
1418
|
+
const reported = reportedFigures(d.report);
|
|
1419
|
+
if (reported !== null) console.log(` ${''.padEnd(10)} reported: ${reported}`);
|
|
1200
1420
|
}
|
|
1201
1421
|
if (check) {
|
|
1202
1422
|
// The framing goes first because it is upstream of every MAE below it: a
|
|
@@ -1271,6 +1491,19 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
1271
1491
|
console.log(' check not run — pass --frames <dir> to compare against the rendered reference frames.');
|
|
1272
1492
|
console.log(' Without it this report says nothing about whether the ANIMATION is right.');
|
|
1273
1493
|
}
|
|
1494
|
+
if (boneDists.length > 0) {
|
|
1495
|
+
for (const b of boneDists) {
|
|
1496
|
+
const w = b.report.worst;
|
|
1497
|
+
console.log(
|
|
1498
|
+
` ${b.skeleton.label.padEnd(10)} bonedist worst position ${w.position.value.toFixed(6)} skeleton-size(s), ` +
|
|
1499
|
+
`rotation ${w.rotation.value.toFixed(4)}°, scale ${w.scale.value.toFixed(6)}, linear ${w.linear.value.toFixed(6)} ` +
|
|
1500
|
+
`over ${b.report.animations.reduce((n, a) => n + a.compared, 0)} frame(s) × ${b.report.correspondence.pairs} bone pair(s)`,
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
console.log(' bonedist not run — pass --bones <correspondence.json | identity> for the stage-3 per-frame');
|
|
1505
|
+
console.log(' bone world-transform distance. It reports and gates nothing.');
|
|
1506
|
+
}
|
|
1274
1507
|
console.log(' Section figures are means of their own measures. There is no rung score:');
|
|
1275
1508
|
console.log(' a rung is cleared by a person reading the measures, and docs/LADDER.md records it.');
|
|
1276
1509
|
|
|
@@ -1298,6 +1531,12 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
1298
1531
|
...d.report,
|
|
1299
1532
|
})),
|
|
1300
1533
|
check,
|
|
1534
|
+
// Absent rather than null when the flag was not passed: `bonedist: null`
|
|
1535
|
+
// in a stored record would read as "measured, nothing to report", and
|
|
1536
|
+
// that is the opposite of "not measured".
|
|
1537
|
+
...(boneDists.length === 0
|
|
1538
|
+
? {}
|
|
1539
|
+
: { boneDists: boneDists.map((b) => ({ label: b.skeleton.label, role: b.skeleton.role, ...b.report })) }),
|
|
1301
1540
|
});
|
|
1302
1541
|
}
|
|
1303
1542
|
|
|
@@ -1307,6 +1546,39 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
1307
1546
|
}
|
|
1308
1547
|
}
|
|
1309
1548
|
|
|
1549
|
+
/**
|
|
1550
|
+
* bonedist — the ladder's stage 3, run on its own.
|
|
1551
|
+
*
|
|
1552
|
+
* ⚠️ It reads BOTH skeletons, so it is a finish-line instrument like `bench` and
|
|
1553
|
+
* unlike `check`. Every convention behind every figure is printed above the
|
|
1554
|
+
* tables, and there is no score — see [`src/bonedist.ts`](src/bonedist.ts).
|
|
1555
|
+
*/
|
|
1556
|
+
function cmdBoneDist(flags: Record<string, string>): void {
|
|
1557
|
+
if (flags.candidate === undefined) throw new UsageError('bonedist needs --candidate <dir | skeleton.json>');
|
|
1558
|
+
if (flags.reference === undefined) throw new UsageError('bonedist needs --reference <skeleton.json>');
|
|
1559
|
+
if (flags.bones === undefined) {
|
|
1560
|
+
throw new UsageError(
|
|
1561
|
+
`bonedist needs --bones <correspondence.json | ${IDENTITY_CORRESPONDENCE}> — a candidate is entitled to its own bone ` +
|
|
1562
|
+
'names, so the mapping is an input and never a guess; pass `identity` to state that the two use the same names',
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
const candidate = resolveArtifacts(flags.candidate, flags.atlas);
|
|
1566
|
+
const reference = resolveArtifacts(flags.reference, flags['reference-atlas']);
|
|
1567
|
+
const report = boneDistance({
|
|
1568
|
+
candidateSkeleton: candidate.skeletonPath,
|
|
1569
|
+
candidateAtlas: candidate.atlasPath,
|
|
1570
|
+
candidateAtlasDir: dirname(candidate.atlasPath),
|
|
1571
|
+
referenceSkeleton: reference.skeletonPath,
|
|
1572
|
+
referenceAtlas: reference.atlasPath,
|
|
1573
|
+
referenceAtlasDir: dirname(reference.atlasPath),
|
|
1574
|
+
bones: flags.bones,
|
|
1575
|
+
...(flags.fps === undefined ? {} : { fps: Number(flags.fps) }),
|
|
1576
|
+
});
|
|
1577
|
+
console.log('rigc bonedist — per-frame bone world-transform distance (the ladder\'s stage 3)');
|
|
1578
|
+
for (const line of boneDistLines(report, { allBones: flags['all-bones'] !== undefined })) console.log(line);
|
|
1579
|
+
if (flags.json !== undefined) writeJson(flags.json, report);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1310
1582
|
function cmdExplain(flags: Record<string, string>): void {
|
|
1311
1583
|
const { label, opts } = resolveCut(flags);
|
|
1312
1584
|
console.log(`rigc explain ${label}`);
|
|
@@ -1522,7 +1794,8 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
1522
1794
|
for (const kind of new Set(result.meshes.map((m) => m.kind))) console.log(` ${MESH_KIND_NOTES[kind]}`);
|
|
1523
1795
|
for (const m of result.meshes) {
|
|
1524
1796
|
console.log(
|
|
1525
|
-
` ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles
|
|
1797
|
+
` ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|
|
1798
|
+
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}]${meshFit(m)}`,
|
|
1526
1799
|
);
|
|
1527
1800
|
}
|
|
1528
1801
|
}
|
|
@@ -1555,12 +1828,29 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1555
1828
|
'copy-images':
|
|
1556
1829
|
'also copy every referenced page PNG into --out and rewrite the atlas to the copies, so the directory is ' +
|
|
1557
1830
|
'self-contained enough to zip or commit on its own (default: page paths still point at the source art)',
|
|
1831
|
+
pack: 'arrange every part PNG onto shared atlas page(s) written into --out as real PNGs, instead of one page ' +
|
|
1832
|
+
'per part. Lossless: every region is a byte-for-byte copy and nothing is resampled, trimmed or rotated ' +
|
|
1833
|
+
'(default: one part, one page, pointing at the source art)',
|
|
1834
|
+
'page-size': `largest page edge, --pack only (default ${DEFAULT_PAGE_SIZE}); pages are powers of two and the ` +
|
|
1835
|
+
'one written is the smallest that holds the pack, spilling to more pages only when the set will not fit',
|
|
1836
|
+
padding: `gutter each region reserves on every side, --pack only (default ${DEFAULT_PADDING}); it is filled by ` +
|
|
1837
|
+
"extending the region's own edge pixels outwards, which is what stops a neighbour bleeding in",
|
|
1838
|
+
'atlas-in':
|
|
1839
|
+
'resolve every part against the regions of this pre-packed .atlas instead of against loose PNGs — region ' +
|
|
1840
|
+
'geometry (bounds/offsets/rotate) is read from the file and the atlas is re-emitted into --out, re-anchored',
|
|
1558
1841
|
cut: 'look up a named cut in --cuts <cuts.json>, instead of --rig/--motion/--out',
|
|
1559
1842
|
cuts: 'the cuts.json --cut names',
|
|
1560
1843
|
profile:
|
|
1561
1844
|
'which rulebook to check against (default: spine) — spine = valid Spine 4.3 that any runtime plays ' +
|
|
1562
1845
|
"correctly; spine-html = also this project's renderer/archetype policy",
|
|
1563
1846
|
atlas: "the candidate's atlas, when it is not beside the skeleton",
|
|
1847
|
+
reference: 'the reference skeleton to pose beside the candidate — a directory or a skeleton.json path',
|
|
1848
|
+
'reference-atlas': "the reference's atlas, when it is not beside the reference skeleton",
|
|
1849
|
+
bones: `a bone correspondence — { "spec": "${BONEDIST_SPEC}", "bones": { "<candidate bone>": "<reference bone>" }, ` +
|
|
1850
|
+
'"animations"?: { … } } — or `identity` to state that the two skeletons use the same names. An INPUT, never ' +
|
|
1851
|
+
'derived: a candidate is entitled to its own vocabulary, so a mapping worked out here would be a guess reported ' +
|
|
1852
|
+
'as a measurement',
|
|
1853
|
+
'all-bones': 'print every bone pair, not just the worst by position',
|
|
1564
1854
|
'texture-from':
|
|
1565
1855
|
"also measure this run through this atlas's texels, keeping the candidate's own geometry, and report how much " +
|
|
1566
1856
|
'of the MAE is texture resampling rather than the rig — pass the atlas the reference frames were rendered ' +
|
|
@@ -1600,7 +1890,13 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1600
1890
|
cuts: '<path>',
|
|
1601
1891
|
profile: 'spine|spine-html',
|
|
1602
1892
|
atlas: '<path>',
|
|
1893
|
+
'atlas-in': '<file.atlas>',
|
|
1894
|
+
'page-size': '<px>',
|
|
1895
|
+
padding: '<px>',
|
|
1603
1896
|
'texture-from': '<path>',
|
|
1897
|
+
reference: '<dir|skeleton.json>',
|
|
1898
|
+
'reference-atlas': '<path>',
|
|
1899
|
+
bones: `<correspondence.json|${IDENTITY_CORRESPONDENCE}>`,
|
|
1604
1900
|
candidate: '<dir|skeleton.json>',
|
|
1605
1901
|
frames: '<dir>',
|
|
1606
1902
|
fps: '<n>',
|
|
@@ -1646,9 +1942,25 @@ const COMMANDS: CommandDoc[] = [
|
|
|
1646
1942
|
name: 'build',
|
|
1647
1943
|
usage: [
|
|
1648
1944
|
'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|spine-html] [--copy-images]',
|
|
1945
|
+
`rigc build … --pack [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] (parts onto shared pages, written into --out)`,
|
|
1946
|
+
'rigc build … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made)',
|
|
1649
1947
|
'rigc build --cut <name> --cuts <cuts.json>',
|
|
1650
1948
|
],
|
|
1651
|
-
flags: [
|
|
1949
|
+
flags: [
|
|
1950
|
+
'rig',
|
|
1951
|
+
'motion',
|
|
1952
|
+
'out',
|
|
1953
|
+
'manifest',
|
|
1954
|
+
'images',
|
|
1955
|
+
'copy-images',
|
|
1956
|
+
'pack',
|
|
1957
|
+
'page-size',
|
|
1958
|
+
'padding',
|
|
1959
|
+
'atlas-in',
|
|
1960
|
+
'cut',
|
|
1961
|
+
'cuts',
|
|
1962
|
+
'profile',
|
|
1963
|
+
],
|
|
1652
1964
|
},
|
|
1653
1965
|
{
|
|
1654
1966
|
name: 'explain',
|
|
@@ -1676,7 +1988,25 @@ const COMMANDS: CommandDoc[] = [
|
|
|
1676
1988
|
{
|
|
1677
1989
|
name: 'bench',
|
|
1678
1990
|
usage: [`rigc bench <${RUNG_IDS.join(' | ')}> --candidate <dir | skeleton.json> [--frames <dir>] [flags]`],
|
|
1679
|
-
flags: ['candidate', 'atlas', 'frames', 'profile', 'all-frames', 'json'],
|
|
1991
|
+
flags: ['candidate', 'atlas', 'frames', 'profile', 'bones', 'all-frames', 'all-bones', 'json'],
|
|
1992
|
+
overrides: {
|
|
1993
|
+
bones: {
|
|
1994
|
+
meaning:
|
|
1995
|
+
'also run the stage-3 per-frame bone world-transform distance against each of the rung\'s reference ' +
|
|
1996
|
+
`skeletons, with this correspondence (or \`identity\`), at ${PROTOCOL_FPS} fps. Reports; gates nothing — ` +
|
|
1997
|
+
'for another sampling rate call `rigc bonedist` directly, where --fps means only that',
|
|
1998
|
+
},
|
|
1999
|
+
},
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
name: 'bonedist',
|
|
2003
|
+
usage: [
|
|
2004
|
+
`rigc bonedist --candidate <dir | skeleton.json> --reference <dir | skeleton.json> --bones <path | ${IDENTITY_CORRESPONDENCE}> [--fps ${PROTOCOL_FPS}] [--all-bones] [--json <out>]`,
|
|
2005
|
+
],
|
|
2006
|
+
flags: ['candidate', 'atlas', 'reference', 'reference-atlas', 'bones', 'fps', 'all-bones', 'json'],
|
|
2007
|
+
overrides: {
|
|
2008
|
+
fps: { meaning: `the rate both skeletons are sampled at, from t=0 over their own durations (default ${PROTOCOL_FPS})` },
|
|
2009
|
+
},
|
|
1680
2010
|
},
|
|
1681
2011
|
{
|
|
1682
2012
|
name: 'render',
|
|
@@ -1843,6 +2173,7 @@ try {
|
|
|
1843
2173
|
else if (command === 'diff') cmdDiff(flags, positional);
|
|
1844
2174
|
else if (command === 'check') cmdCheck(flags);
|
|
1845
2175
|
else if (command === 'bench') cmdBench(flags, positional);
|
|
2176
|
+
else if (command === 'bonedist') cmdBoneDist(flags);
|
|
1846
2177
|
else if (command === 'render') cmdRender(flags);
|
|
1847
2178
|
else if (command === 'preview') cmdPreview(flags);
|
|
1848
2179
|
else if (command === 'pose') cmdPose(flags);
|
|
@@ -1866,6 +2197,10 @@ try {
|
|
|
1866
2197
|
console.error(`rigc check error: ${err.message}`);
|
|
1867
2198
|
process.exit(1);
|
|
1868
2199
|
}
|
|
2200
|
+
if (err instanceof BoneDistError) {
|
|
2201
|
+
console.error(`rigc bonedist error: ${err.message}`);
|
|
2202
|
+
process.exit(1);
|
|
2203
|
+
}
|
|
1869
2204
|
// Like a usage error in kind — a missing directory, an unreadable frame — but
|
|
1870
2205
|
// its messages name a path and a reason, and reprinting the whole usage under
|
|
1871
2206
|
// them buries that.
|