spine-rigc 0.4.0 → 0.6.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/NOTICE.md +21 -0
- package/README.md +336 -223
- package/bin/rigc.cjs +40 -0
- package/cli.ts +558 -49
- package/docs/AUTHORING.md +538 -23
- package/docs/PROMPTING.md +106 -0
- package/docs/SPEC_COVERAGE.md +16 -14
- package/package.json +4 -2
- package/src/compile.ts +189 -142
- package/src/emit.ts +88 -0
- package/src/json-position.ts +253 -0
- package/src/png.ts +72 -7
- package/src/preview.ts +243 -0
- package/src/render.ts +58 -0
- package/src/types.ts +8 -0
- package/src/validate.ts +58 -13
- package/tools/plate.ts +164 -21
package/cli.ts
CHANGED
|
@@ -31,13 +31,35 @@
|
|
|
31
31
|
* Its paths resolve against the cuts.json file itself, so the table travels
|
|
32
32
|
* with the project that owns the art rather than with this repository.
|
|
33
33
|
*/
|
|
34
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
35
|
-
import { dirname, join, resolve } from 'node:path';
|
|
34
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
35
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
36
36
|
import { checkAgainstFrames, checkLines, CheckError, type CheckOptions, type CheckReport } from './src/check.ts';
|
|
37
37
|
import { compile, CompileError, type CompileOptions } from './src/compile.ts';
|
|
38
38
|
import { diffLines, diffSkeletons, sectionFigures, type DiffReport } from './src/diff.ts';
|
|
39
|
+
import { copyAtlasImages } from './src/emit.ts';
|
|
40
|
+
import { parseJsonWithPosition } from './src/json-position.ts';
|
|
39
41
|
import { findRung, RUNG_IDS, type RungSkeleton } from './src/ladder.ts';
|
|
40
|
-
import {
|
|
42
|
+
import { buildPreview, PLAYER_LINE, type PreviewPage } from './src/preview.ts';
|
|
43
|
+
import {
|
|
44
|
+
atlasPageNames,
|
|
45
|
+
BACKGROUND,
|
|
46
|
+
contactSheet,
|
|
47
|
+
FRAMES_SIDECAR,
|
|
48
|
+
FRAMES_SPEC,
|
|
49
|
+
framingViewport,
|
|
50
|
+
loadPosable,
|
|
51
|
+
PROTOCOL_FPS,
|
|
52
|
+
renderFrame,
|
|
53
|
+
sampleAll,
|
|
54
|
+
sampleAnimation,
|
|
55
|
+
SETUP_POSE_DIR,
|
|
56
|
+
SHEET_FILE,
|
|
57
|
+
SHEET_TILE,
|
|
58
|
+
type Frame,
|
|
59
|
+
type FramesSidecar,
|
|
60
|
+
type FrameSet,
|
|
61
|
+
} from './src/render.ts';
|
|
62
|
+
import { CLI_DEFAULT_PROFILE, reportLines, validate, VALIDATE_PROFILES, type ValidateProfile } from './src/validate.ts';
|
|
41
63
|
import type { CompileResult, MotionSpec } from './src/types.ts';
|
|
42
64
|
|
|
43
65
|
/**
|
|
@@ -61,6 +83,40 @@ export type CutTable = Record<string, CutEntry>;
|
|
|
61
83
|
|
|
62
84
|
class UsageError extends Error {}
|
|
63
85
|
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// package metadata — the installed version and repository, for `--version`
|
|
88
|
+
// and for naming a remedy `bench` can only give from a repo checkout.
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
interface PackageMeta {
|
|
92
|
+
version?: string;
|
|
93
|
+
repository?: string | { url?: string };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let packageMeta: PackageMeta | null | undefined;
|
|
97
|
+
|
|
98
|
+
/** `package.json` sits next to this file both in the repo and once installed. */
|
|
99
|
+
function readPackageMeta(): PackageMeta | null {
|
|
100
|
+
if (packageMeta === undefined) {
|
|
101
|
+
try {
|
|
102
|
+
packageMeta = JSON.parse(readFileSync(join(import.meta.dir, 'package.json'), 'utf8')) as PackageMeta;
|
|
103
|
+
} catch {
|
|
104
|
+
packageMeta = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return packageMeta;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readVersion(): string {
|
|
111
|
+
return readPackageMeta()?.version ?? 'unknown';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function repositoryUrl(): string {
|
|
115
|
+
const repo = readPackageMeta()?.repository;
|
|
116
|
+
const url = typeof repo === 'string' ? repo : repo?.url;
|
|
117
|
+
return (url ?? 'https://github.com/firejune/rigc').replace(/^git\+/, '').replace(/\.git$/, '');
|
|
118
|
+
}
|
|
119
|
+
|
|
64
120
|
// ---------------------------------------------------------------------------
|
|
65
121
|
// argument parsing
|
|
66
122
|
// ---------------------------------------------------------------------------
|
|
@@ -72,7 +128,7 @@ class UsageError extends Error {}
|
|
|
72
128
|
* flag": inferring it would turn `--out --json report.json` — a real typo, a
|
|
73
129
|
* missing value — into a silently accepted switch plus a stray positional.
|
|
74
130
|
*/
|
|
75
|
-
const BOOLEAN_FLAGS = new Set(['all-frames']);
|
|
131
|
+
const BOOLEAN_FLAGS = new Set(['all-frames', 'help', 'copy-images']);
|
|
76
132
|
|
|
77
133
|
/** `--flag value` pairs plus the leftover positionals, in order. */
|
|
78
134
|
function parseArgs(argv: string[]): { flags: Record<string, string>; positional: string[] } {
|
|
@@ -99,6 +155,21 @@ function parseArgs(argv: string[]): { flags: Record<string, string>; positional:
|
|
|
99
155
|
return { flags, positional };
|
|
100
156
|
}
|
|
101
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Read and parse a JSON file the caller named on the command line — a cuts
|
|
160
|
+
* table, a candidate or reference skeleton to `diff`. A parse failure names the
|
|
161
|
+
* file and, best-effort, where inside it the syntax broke (see
|
|
162
|
+
* `parseJsonWithPosition`); left as a raw `JSON.parse`, it would surface as an
|
|
163
|
+
* unhandled `SyntaxError` with a stack trace instead of a usage error.
|
|
164
|
+
*/
|
|
165
|
+
function readJsonFile(path: string): unknown {
|
|
166
|
+
try {
|
|
167
|
+
return parseJsonWithPosition(readFileSync(path, 'utf8'));
|
|
168
|
+
} catch (err) {
|
|
169
|
+
throw new UsageError(`cannot read ${path}: ${(err as Error).message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
102
173
|
/**
|
|
103
174
|
* Read a cuts.json and resolve its three paths against the file's own
|
|
104
175
|
* directory. Anchoring on the table rather than on the process cwd is what lets
|
|
@@ -107,7 +178,7 @@ function parseArgs(argv: string[]): { flags: Record<string, string>; positional:
|
|
|
107
178
|
function readCutTable(cutsPath: string): { dir: string; table: CutTable } {
|
|
108
179
|
const abs = resolve(cutsPath);
|
|
109
180
|
if (!existsSync(abs)) throw new UsageError(`no cuts file at ${abs}`);
|
|
110
|
-
const parsed
|
|
181
|
+
const parsed = readJsonFile(abs);
|
|
111
182
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
112
183
|
throw new UsageError(`${abs}: expected an object of cut name -> { manifest, motion, out }`);
|
|
113
184
|
}
|
|
@@ -168,14 +239,17 @@ function resolveCut(flags: Record<string, string>): { label: string; opts: Compi
|
|
|
168
239
|
// ---------------------------------------------------------------------------
|
|
169
240
|
|
|
170
241
|
/**
|
|
171
|
-
* Read `--profile`, defaulting to
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
242
|
+
* Read `--profile`, defaulting to `spine` — see `CLI_DEFAULT_PROFILE`.
|
|
243
|
+
*
|
|
244
|
+
* An unknown name is a usage error rather than a silent fallback, and that
|
|
245
|
+
* matters in both directions: a typo used to re-apply the strictest rulebook to
|
|
246
|
+
* data the caller was trying to exempt, and it would now drop the policy layer
|
|
247
|
+
* from a caller who typed `--profile spine-htlm` and believes they asked for it.
|
|
248
|
+
* Neither is something to discover from a green.
|
|
175
249
|
*/
|
|
176
250
|
function readProfile(flags: Record<string, string>): ValidateProfile {
|
|
177
251
|
const raw = flags.profile;
|
|
178
|
-
if (raw === undefined) return
|
|
252
|
+
if (raw === undefined) return CLI_DEFAULT_PROFILE;
|
|
179
253
|
const found = VALIDATE_PROFILES.find((p) => p === raw);
|
|
180
254
|
if (!found) throw new UsageError(`--profile ${JSON.stringify(raw)}; known profiles: ${VALIDATE_PROFILES.join(', ')}`);
|
|
181
255
|
return found;
|
|
@@ -206,6 +280,12 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
206
280
|
const { label, opts } = resolveCut(flags);
|
|
207
281
|
const profile = readProfile(flags);
|
|
208
282
|
console.log(`rigc build ${label}`);
|
|
283
|
+
// Named explicitly and on their own lines rather than folded into the header
|
|
284
|
+
// above: with two input files, a header that names only one of them (the rig,
|
|
285
|
+
// historically) reads as though it were the one at fault whenever the error
|
|
286
|
+
// that follows actually comes from the other.
|
|
287
|
+
console.log(` .. rig ${opts.rigPath}`);
|
|
288
|
+
console.log(` .. motion ${opts.motionPath}`);
|
|
209
289
|
const result = compile(opts);
|
|
210
290
|
|
|
211
291
|
console.log(` .. ${result.images.length} part page(s):`);
|
|
@@ -241,8 +321,25 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
241
321
|
}
|
|
242
322
|
|
|
243
323
|
mkdirSync(opts.outDir, { recursive: true });
|
|
324
|
+
|
|
325
|
+
// `--copy-images`: `--out` is otherwise NOT self-contained — a page's default
|
|
326
|
+
// path is relative to the source art (often `../parts/foo.png`), which is
|
|
327
|
+
// correct for a build sitting beside the project it came from and breaks the
|
|
328
|
+
// moment the directory is zipped, committed or moved on its own (issue #217).
|
|
329
|
+
// Opt-in only: the default stays exactly what it has always been.
|
|
330
|
+
let atlasText = result.atlasText;
|
|
331
|
+
if (flags['copy-images'] !== undefined) {
|
|
332
|
+
const copied = copyAtlasImages(result.images, opts.outDir);
|
|
333
|
+
atlasText = copied.atlasText;
|
|
334
|
+
console.log(` .. copy-images: ${copied.pages.length} page(s) copied into ${opts.outDir}`);
|
|
335
|
+
for (const p of copied.pages) {
|
|
336
|
+
const note = p.to === basename(p.from) ? '' : ` (renamed from ${basename(p.from)} — basename collision)`;
|
|
337
|
+
console.log(` .. ${p.region.padEnd(24)} <- ${p.to}${note}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
244
341
|
writeFileSync(join(opts.outDir, 'skeleton.json'), result.skeletonText);
|
|
245
|
-
writeFileSync(join(opts.outDir, 'skeleton.atlas'),
|
|
342
|
+
writeFileSync(join(opts.outDir, 'skeleton.atlas'), atlasText);
|
|
246
343
|
console.log(`rigc: wrote ${join(opts.outDir, 'skeleton.json')}`);
|
|
247
344
|
console.log(`rigc: wrote ${join(opts.outDir, 'skeleton.atlas')}`);
|
|
248
345
|
}
|
|
@@ -325,10 +422,7 @@ function cmdDiff(flags: Record<string, string>, positional: string[]): void {
|
|
|
325
422
|
for (const path of [candidatePath, referencePath]) {
|
|
326
423
|
if (!existsSync(path)) throw new UsageError(`nothing at ${path}`);
|
|
327
424
|
}
|
|
328
|
-
const report = diffSkeletons(
|
|
329
|
-
JSON.parse(readFileSync(candidatePath, 'utf8')),
|
|
330
|
-
JSON.parse(readFileSync(referencePath, 'utf8')),
|
|
331
|
-
);
|
|
425
|
+
const report = diffSkeletons(readJsonFile(candidatePath), readJsonFile(referencePath));
|
|
332
426
|
console.log('rigc diff');
|
|
333
427
|
for (const line of diffLines(report, { candidate: candidatePath, reference: referencePath })) console.log(line);
|
|
334
428
|
if (flags.json !== undefined) {
|
|
@@ -411,14 +505,235 @@ function writeJson(target: string, body: unknown): void {
|
|
|
411
505
|
console.log(`rigc: wrote ${out}`);
|
|
412
506
|
}
|
|
413
507
|
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// seeing the result — render and preview
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
//
|
|
512
|
+
// ⭐ Why two commands exist for one question. `validate` says the artifact is
|
|
513
|
+
// valid, `check` says how close it is to reference frames — and a first user has
|
|
514
|
+
// neither a reference nor any way to look at what they built. A rig whose head
|
|
515
|
+
// sits visibly off its torso passes the gate, loads in `spine-core` and steps
|
|
516
|
+
// cleanly, because the offsets are the ones the spec asked for. The only remedy
|
|
517
|
+
// is looking (issue #216).
|
|
518
|
+
//
|
|
519
|
+
// `render` looks with OUR rasteriser: PNGs on disk, no browser, no network, and
|
|
520
|
+
// the same frame geometry `check` compares against — so its output is a frame set
|
|
521
|
+
// like any other, sidecar included. `preview` looks with ESOTERIC'S, in one HTML
|
|
522
|
+
// file, which is the stronger statement of the two: a rig that plays there has
|
|
523
|
+
// been played by the reference implementation rather than by ours (issue #151).
|
|
524
|
+
//
|
|
525
|
+
// Both take a COMPILED artifact rather than a rig and motion spec. That is what
|
|
526
|
+
// `check`, `bench` and `validate` all take, it is what `build --out` leaves
|
|
527
|
+
// behind, and it keeps `--out` meaning one thing per command instead of naming
|
|
528
|
+
// the build directory on the way in and the pictures on the way out.
|
|
529
|
+
|
|
530
|
+
/** Both commands' shared front door: which artifact, and what is in it. */
|
|
531
|
+
function resolveViewable(flags: Record<string, string>): {
|
|
532
|
+
skeletonPath: string;
|
|
533
|
+
atlasPath: string;
|
|
534
|
+
atlasDir: string;
|
|
535
|
+
} {
|
|
536
|
+
if (flags.candidate === undefined) {
|
|
537
|
+
throw new UsageError('needs --candidate <dir | skeleton.json> — the directory `build --out` wrote');
|
|
538
|
+
}
|
|
539
|
+
const { skeletonPath, atlasPath } = resolveArtifacts(flags.candidate, flags.atlas);
|
|
540
|
+
for (const path of [skeletonPath, atlasPath]) {
|
|
541
|
+
if (!existsSync(path)) throw new UsageError(`nothing at ${path}`);
|
|
542
|
+
}
|
|
543
|
+
return { skeletonPath, atlasPath, atlasDir: dirname(atlasPath) };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** `--animation`, checked against what the skeleton actually carries. */
|
|
547
|
+
function readAnimationFlag(flags: Record<string, string>, available: string[]): string | undefined {
|
|
548
|
+
const name = flags.animation;
|
|
549
|
+
if (name === undefined) return undefined;
|
|
550
|
+
if (!available.includes(name)) {
|
|
551
|
+
throw new UsageError(
|
|
552
|
+
`no animation ${JSON.stringify(name)} in this skeleton; it has [${available.join(', ') || 'none'}]`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
return name;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function readPositiveNumber(flags: Record<string, string>, key: string, fallback: number, least: number): number {
|
|
559
|
+
const raw = flags[key];
|
|
560
|
+
if (raw === undefined) return fallback;
|
|
561
|
+
const value = Number(raw);
|
|
562
|
+
if (!Number.isFinite(value) || value < least) throw new UsageError(`--${key} must be a number of at least ${least}`);
|
|
563
|
+
return value;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* render — the frame series, drawn by the same rasteriser `check` measures with.
|
|
568
|
+
*
|
|
569
|
+
* The framing is measured across EVERY animation at `FRAMING_FPS` and not across
|
|
570
|
+
* the one being written, which is `src/render.ts`'s own invariant: the viewport is
|
|
571
|
+
* a property of the shot, so two animations of one rig — and the same animation at
|
|
572
|
+
* two rates — land on one pixel grid and stay comparable.
|
|
573
|
+
*/
|
|
574
|
+
function cmdRender(flags: Record<string, string>): void {
|
|
575
|
+
const { skeletonPath, atlasPath, atlasDir } = resolveViewable(flags);
|
|
576
|
+
const fps = readPositiveNumber(flags, 'fps', PROTOCOL_FPS, 1);
|
|
577
|
+
const maxSide = readPositiveNumber(flags, 'max', 256, 16);
|
|
578
|
+
const outRoot = resolve(flags.out ?? 'render');
|
|
579
|
+
|
|
580
|
+
console.log('rigc render');
|
|
581
|
+
console.log(` .. skeleton ${skeletonPath}`);
|
|
582
|
+
console.log(` .. atlas ${atlasPath}`);
|
|
583
|
+
const { data, pages } = loadPosable(skeletonPath, atlasPath, atlasDir);
|
|
584
|
+
const only = readAnimationFlag(flags, data.animations.map((a) => a.name));
|
|
585
|
+
|
|
586
|
+
const viewport = framingViewport(data, maxSide);
|
|
587
|
+
if (!viewport) {
|
|
588
|
+
throw new UsageError(
|
|
589
|
+
`${skeletonPath} posed no drawable attachment in any animation or in its setup pose — there is nothing to draw`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// `sampleAll` covers the skeleton with no animation at all, which files its one
|
|
594
|
+
// setup-pose frame under the reserved name. Narrowing to one animation reuses
|
|
595
|
+
// the same sampler rather than a second path through it.
|
|
596
|
+
const sampled: Map<string, Frame[]> =
|
|
597
|
+
only === undefined ? sampleAll(data, fps) : new Map([[only, sampleAnimation(data, only, fps)]]);
|
|
598
|
+
console.log(` .. ${viewport.width}x${viewport.height}px at ${fps} fps, ${sampled.size} set(s) -> ${outRoot}`);
|
|
599
|
+
|
|
600
|
+
mkdirSync(outRoot, { recursive: true });
|
|
601
|
+
const sets: FrameSet[] = [];
|
|
602
|
+
for (const [name, frames] of sampled) {
|
|
603
|
+
// Same naming as a reference render: the protocol rate says nothing, any
|
|
604
|
+
// other rate says itself, so two rates of one animation sit side by side.
|
|
605
|
+
const dirName = fps === PROTOCOL_FPS ? name : `${name}@${fps}fps`;
|
|
606
|
+
const dir = join(outRoot, dirName);
|
|
607
|
+
// Cleared rather than written over: a shorter animation would otherwise leave
|
|
608
|
+
// the tail of a longer previous run on disk, and stale frames in a frame set
|
|
609
|
+
// are indistinguishable from real ones.
|
|
610
|
+
if (existsSync(dir)) rmSync(dir, { recursive: true });
|
|
611
|
+
mkdirSync(dir, { recursive: true });
|
|
612
|
+
for (let i = 0; i < frames.length; i++) {
|
|
613
|
+
renderFrame(frames[i], pages, viewport, BACKGROUND).writePng(join(dir, `f${String(i).padStart(4, '0')}.png`));
|
|
614
|
+
}
|
|
615
|
+
// One frame has nothing to compare itself against, so it gets no sheet — it
|
|
616
|
+
// would be the same picture with a border and a "0" on it.
|
|
617
|
+
const sheet = frames.length > 1;
|
|
618
|
+
if (sheet) contactSheet(frames, pages, viewport, SHEET_TILE).writePng(join(dir, SHEET_FILE));
|
|
619
|
+
const duration = frames[frames.length - 1].time;
|
|
620
|
+
sets.push({
|
|
621
|
+
dir: dirName,
|
|
622
|
+
animation: name === SETUP_POSE_DIR && data.animations.length === 0 ? null : name,
|
|
623
|
+
fps,
|
|
624
|
+
sampled: frames.length,
|
|
625
|
+
written: frames.length,
|
|
626
|
+
stride: 1,
|
|
627
|
+
duration,
|
|
628
|
+
});
|
|
629
|
+
const how = frames.length === 1 ? 'a single pose' : `${duration.toFixed(3)}s`;
|
|
630
|
+
console.log(` .. ${name.padEnd(16)} ${frames.length} frame(s), ${how}${sheet ? ` + ${SHEET_FILE}` : ''} -> ${dir}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// The sidecar is what makes this a frame SET rather than a pile of pictures:
|
|
634
|
+
// the world box every frame is a picture of, so a distance measured in pixels
|
|
635
|
+
// converts back to the units the rig is authored in — and so `rigc check` can
|
|
636
|
+
// render something else into the same grid later.
|
|
637
|
+
const sidecar: FramesSidecar = {
|
|
638
|
+
spec: FRAMES_SPEC,
|
|
639
|
+
background: BACKGROUND,
|
|
640
|
+
viewport: {
|
|
641
|
+
x: viewport.minX,
|
|
642
|
+
y: viewport.minY,
|
|
643
|
+
width: viewport.maxX - viewport.minX,
|
|
644
|
+
height: viewport.maxY - viewport.minY,
|
|
645
|
+
scale: viewport.scale,
|
|
646
|
+
pixelWidth: viewport.width,
|
|
647
|
+
pixelHeight: viewport.height,
|
|
648
|
+
},
|
|
649
|
+
sets: [...sets].sort((a, b) => a.dir.localeCompare(b.dir)),
|
|
650
|
+
};
|
|
651
|
+
writeFileSync(join(outRoot, FRAMES_SIDECAR), `${JSON.stringify(sidecar, null, 2)}\n`);
|
|
652
|
+
console.log(`rigc: wrote ${join(outRoot, FRAMES_SIDECAR)}`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** The animation names an emitted skeleton carries, in the order it lists them. */
|
|
656
|
+
function skeletonAnimationNames(skeletonText: string, path: string): string[] {
|
|
657
|
+
let parsed: unknown;
|
|
658
|
+
try {
|
|
659
|
+
parsed = parseJsonWithPosition(skeletonText);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
throw new UsageError(`cannot read ${path}: ${(err as Error).message}`);
|
|
662
|
+
}
|
|
663
|
+
if (typeof parsed !== 'object' || parsed === null) throw new UsageError(`${path} is not a skeleton object`);
|
|
664
|
+
const animations = (parsed as { animations?: unknown }).animations;
|
|
665
|
+
if (animations === undefined) return [];
|
|
666
|
+
if (typeof animations !== 'object' || animations === null || Array.isArray(animations)) {
|
|
667
|
+
throw new UsageError(`${path} has an "animations" field that is not an object`);
|
|
668
|
+
}
|
|
669
|
+
return Object.keys(animations);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* preview — the artifact playing in Esoteric's own web player, as one file.
|
|
674
|
+
*
|
|
675
|
+
* ⚠️ Nothing is rasterised here and nothing is decoded. The pages go into the
|
|
676
|
+
* page as the bytes they are on disk, so a preview works for any PNG a BROWSER
|
|
677
|
+
* can draw rather than for the ones our own decoder reads — which is the right
|
|
678
|
+
* direction for the command whose whole job is "just show me".
|
|
679
|
+
*/
|
|
680
|
+
function cmdPreview(flags: Record<string, string>): void {
|
|
681
|
+
const { skeletonPath, atlasPath, atlasDir } = resolveViewable(flags);
|
|
682
|
+
const skeletonText = readFileSync(skeletonPath, 'utf8');
|
|
683
|
+
const atlasText = readFileSync(atlasPath, 'utf8');
|
|
684
|
+
const animations = skeletonAnimationNames(skeletonText, skeletonPath);
|
|
685
|
+
const chosen = readAnimationFlag(flags, animations);
|
|
686
|
+
|
|
687
|
+
// A directory for --out is taken as "put the default name in here", because
|
|
688
|
+
// `--out render/` is what the sibling command means by the same flag and a
|
|
689
|
+
// preview written OVER a directory is not a recoverable mistake.
|
|
690
|
+
const target = resolve(flags.out ?? 'preview.html');
|
|
691
|
+
const out = existsSync(target) && statSync(target).isDirectory() ? join(target, 'preview.html') : target;
|
|
692
|
+
|
|
693
|
+
console.log('rigc preview');
|
|
694
|
+
console.log(` .. skeleton ${skeletonPath}`);
|
|
695
|
+
console.log(` .. atlas ${atlasPath}`);
|
|
696
|
+
|
|
697
|
+
const pages: PreviewPage[] = atlasPageNames(atlasText).map((name) => {
|
|
698
|
+
const path = join(atlasDir, name);
|
|
699
|
+
if (!existsSync(path)) {
|
|
700
|
+
throw new UsageError(
|
|
701
|
+
`the atlas declares page "${name}", which resolves to ${path} and is not there — ` +
|
|
702
|
+
'a page a preview cannot embed is a page the player could not have loaded either',
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
return { name, bytes: readFileSync(path) };
|
|
706
|
+
});
|
|
707
|
+
for (const page of pages) {
|
|
708
|
+
console.log(` .. page ${page.name.padEnd(28)} ${(page.bytes.length / 1024).toFixed(1)} KiB`);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const html = buildPreview({
|
|
712
|
+
skeletonText,
|
|
713
|
+
atlasText,
|
|
714
|
+
pages,
|
|
715
|
+
animation: chosen ?? animations[0] ?? null,
|
|
716
|
+
animations,
|
|
717
|
+
label: skeletonPath,
|
|
718
|
+
version: readVersion(),
|
|
719
|
+
});
|
|
720
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
721
|
+
writeFileSync(out, html);
|
|
722
|
+
console.log(
|
|
723
|
+
` .. embedded ${pages.length} page(s) + the skeleton and atlas as data URIs; ` +
|
|
724
|
+
`the player itself loads from unpkg (@${PLAYER_LINE}), so the first open needs a network`,
|
|
725
|
+
);
|
|
726
|
+
console.log(`rigc: wrote ${out} (${(html.length / 1024).toFixed(1)} KiB — open it in a browser)`);
|
|
727
|
+
}
|
|
728
|
+
|
|
414
729
|
/**
|
|
415
730
|
* bench — run one rung of the benchmark ladder against a candidate rig.
|
|
416
731
|
*
|
|
417
732
|
* Two questions, asked in this order and never merged:
|
|
418
733
|
*
|
|
419
734
|
* 1. Is the candidate valid Spine at all? That is `validate --profile spine`,
|
|
420
|
-
* and it is the only part with a pass/fail.
|
|
421
|
-
*
|
|
735
|
+
* and it is the only part with a pass/fail. The profile is pinned here, not
|
|
736
|
+
* inherited from the CLI default: the thing being reproduced is an editor
|
|
422
737
|
* export, and holding it to this project's renderer policy would fail rungs
|
|
423
738
|
* for reasons the rung is not about.
|
|
424
739
|
* 2. How close is it, structurally, to the reference? That is `diff`, and it
|
|
@@ -438,13 +753,22 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
438
753
|
if (!rung) throw new UsageError(`unknown rung ${JSON.stringify(rungId)}; known: ${RUNG_IDS.join(', ')}`);
|
|
439
754
|
if (flags.candidate === undefined) throw new UsageError('bench needs --candidate <dir | skeleton.json>');
|
|
440
755
|
|
|
441
|
-
// bench judges a reproduction of editor output, so `spine` is
|
|
442
|
-
|
|
756
|
+
// bench judges a reproduction of editor output, so `spine` is PINNED here
|
|
757
|
+
// rather than inherited. It reads the same as the CLI default today (#221) and
|
|
758
|
+
// is kept as its own statement anyway: the ladder's stage-1 gate is defined by
|
|
759
|
+
// `docs/GATE.md` as `validate --profile spine`, and a bench run must go on
|
|
760
|
+
// meaning that whatever a later release decides the default should be.
|
|
761
|
+
const profile: ValidateProfile = flags.profile === undefined ? 'spine' : readProfile(flags);
|
|
443
762
|
const exportDir = resolve(import.meta.dir, 'examples', rung.example, 'export');
|
|
444
763
|
if (!existsSync(exportDir)) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
764
|
+
// `bun run fetch-examples` runs `scripts/fetch-examples.sh`, and `scripts/`
|
|
765
|
+
// is not in package.json's `files` — an npm install has no such script to
|
|
766
|
+
// run. Its presence is what tells the two contexts apart, so the remedy
|
|
767
|
+
// named here is one that actually exists in whichever context this is.
|
|
768
|
+
const remedy = existsSync(resolve(import.meta.dir, 'scripts', 'fetch-examples.sh'))
|
|
769
|
+
? 'run `bun run fetch-examples` first'
|
|
770
|
+
: `bench needs a checkout of ${repositoryUrl()} — its \`fetch-examples\` script is not part of the installed package`;
|
|
771
|
+
throw new UsageError(`no example corpus at ${exportDir} — ${remedy} (examples/ is gitignored, not shipped)`);
|
|
448
772
|
}
|
|
449
773
|
|
|
450
774
|
const { skeletonPath, atlasPath } = resolveArtifacts(flags.candidate, flags.atlas);
|
|
@@ -615,10 +939,14 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
615
939
|
|
|
616
940
|
function cmdExplain(flags: Record<string, string>): void {
|
|
617
941
|
const { label, opts } = resolveCut(flags);
|
|
942
|
+
console.log(`rigc explain ${label}`);
|
|
943
|
+
// See the identical pair of lines in `cmdBuild` for why both paths are named
|
|
944
|
+
// here rather than only the one the header's `label` happens to carry.
|
|
945
|
+
console.log(` .. rig ${opts.rigPath}`);
|
|
946
|
+
console.log(` .. motion ${opts.motionPath}`);
|
|
618
947
|
const result = compile(opts);
|
|
619
|
-
const motion =
|
|
948
|
+
const motion = readJsonFile(opts.motionPath) as MotionSpec;
|
|
620
949
|
|
|
621
|
-
console.log(`rigc explain ${label}`);
|
|
622
950
|
console.log(`\nstage ${result.skeleton.skeleton.width} x ${result.skeleton.skeleton.height} (spine ${result.skeleton.skeleton.spine})`);
|
|
623
951
|
|
|
624
952
|
// The crop note describes where the numbers CAME from, and without a manifest
|
|
@@ -725,35 +1053,198 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
725
1053
|
console.log(` default=${motion.mix?.default ?? 0} pairs=${JSON.stringify(motion.mix?.pairs ?? [])}`);
|
|
726
1054
|
}
|
|
727
1055
|
|
|
1056
|
+
// ---------------------------------------------------------------------------
|
|
1057
|
+
// usage / per-command help
|
|
1058
|
+
// ---------------------------------------------------------------------------
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* One meaning per flag name, shared by every command that takes it — the
|
|
1062
|
+
* single place this project states what a flag means. AUTHORING.md §0 quotes
|
|
1063
|
+
* this table for `build`'s `--rig`/`--motion`/`--out`/`--images`/`--manifest`/
|
|
1064
|
+
* `--profile`; if the two ever disagree, this is the one the code runs.
|
|
1065
|
+
*/
|
|
1066
|
+
const FLAG_MEANINGS: Record<string, string> = {
|
|
1067
|
+
rig: 'the rig spec — skeleton structure',
|
|
1068
|
+
motion: 'the motion spec — time',
|
|
1069
|
+
out: 'directory for skeleton.json + skeleton.atlas; atlas page paths are written relative to it',
|
|
1070
|
+
images: "override the rig spec's own images directory (relative to your working directory)",
|
|
1071
|
+
manifest: 'a cut manifest, for a rig with measured art behind it; a foreign skeleton has none',
|
|
1072
|
+
'copy-images':
|
|
1073
|
+
'also copy every referenced page PNG into --out and rewrite the atlas to the copies, so the directory is ' +
|
|
1074
|
+
'self-contained enough to zip or commit on its own (default: page paths still point at the source art)',
|
|
1075
|
+
cut: 'look up a named cut in --cuts <cuts.json>, instead of --rig/--motion/--out',
|
|
1076
|
+
cuts: 'the cuts.json --cut names',
|
|
1077
|
+
profile:
|
|
1078
|
+
'which rulebook to check against (default: spine) — spine = valid Spine 4.3 that any runtime plays ' +
|
|
1079
|
+
"correctly; spine-html = also this project's renderer/archetype policy",
|
|
1080
|
+
atlas: "the candidate's atlas, when it is not beside the skeleton",
|
|
1081
|
+
candidate: 'a compiled skeleton: a directory holding skeleton.json + skeleton.atlas, or a skeleton.json path',
|
|
1082
|
+
frames: 'a rendered reference frame set (a skeleton root, or one animation directory)',
|
|
1083
|
+
fps: 'frame rate, only for a frame set with no frames.json sidecar',
|
|
1084
|
+
viewport: "pin the candidate's world box, y up, instead of fitting it",
|
|
1085
|
+
framing: 'fit each frame set on its own (default) or once across all of them',
|
|
1086
|
+
as: 'the candidate animation to play, when it is named differently from the frame set',
|
|
1087
|
+
'all-frames': 'print every frame, not just the worst by MAE',
|
|
1088
|
+
json: 'also write the whole report to this path',
|
|
1089
|
+
animation: 'which animation to show; the default is every one for `render` and the first for `preview`',
|
|
1090
|
+
max: 'longest side of a rendered frame, in pixels (default 256)',
|
|
1091
|
+
help: "show this command's flags and exit",
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
/** The `<value>` a flag takes, for its column in a command's flag table. Absent for a boolean switch. */
|
|
1095
|
+
const FLAG_VALUES: Record<string, string> = {
|
|
1096
|
+
rig: '<path>',
|
|
1097
|
+
motion: '<path>',
|
|
1098
|
+
out: '<dir>',
|
|
1099
|
+
images: '<dir>',
|
|
1100
|
+
manifest: '<path>',
|
|
1101
|
+
cut: '<name>',
|
|
1102
|
+
cuts: '<path>',
|
|
1103
|
+
profile: 'spine|spine-html',
|
|
1104
|
+
atlas: '<path>',
|
|
1105
|
+
candidate: '<dir|skeleton.json>',
|
|
1106
|
+
frames: '<dir>',
|
|
1107
|
+
fps: '<n>',
|
|
1108
|
+
viewport: '<x,y,w,h>',
|
|
1109
|
+
framing: 'per-shot|shared',
|
|
1110
|
+
as: '<name>',
|
|
1111
|
+
json: '<out>',
|
|
1112
|
+
animation: '<name>',
|
|
1113
|
+
max: '<px>',
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
interface CommandDoc {
|
|
1117
|
+
name: string;
|
|
1118
|
+
/** One or more invocation forms, each already spelling the command name. */
|
|
1119
|
+
usage: string[];
|
|
1120
|
+
/** Flag names (into FLAG_MEANINGS/FLAG_VALUES), in display order. `--help` is appended automatically. */
|
|
1121
|
+
flags: string[];
|
|
1122
|
+
/**
|
|
1123
|
+
* Per-command wording for a flag whose value or meaning genuinely differs here.
|
|
1124
|
+
*
|
|
1125
|
+
* ⚠️ The default above it — one meaning per flag name, everywhere — is the rule
|
|
1126
|
+
* and this is the named exception to it, not a second table. Two flags earn it:
|
|
1127
|
+
* `--out` is a directory of artifacts to `build`, a directory of pictures to
|
|
1128
|
+
* `render` and one file to `preview`; `--fps` is the rate a frame set was
|
|
1129
|
+
* RECORDED at to `check`, which reads it off a sidecar, and the rate to SAMPLE
|
|
1130
|
+
* at to `render`, which is choosing it. Writing either as one sentence covering
|
|
1131
|
+
* every command would leave every command's own help less true than it is now.
|
|
1132
|
+
*/
|
|
1133
|
+
overrides?: Record<string, { value?: string; meaning?: string }>;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const COMMANDS: CommandDoc[] = [
|
|
1137
|
+
{
|
|
1138
|
+
name: 'build',
|
|
1139
|
+
usage: [
|
|
1140
|
+
'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|spine-html] [--copy-images]',
|
|
1141
|
+
'rigc build --cut <name> --cuts <cuts.json>',
|
|
1142
|
+
],
|
|
1143
|
+
flags: ['rig', 'motion', 'out', 'manifest', 'images', 'copy-images', 'cut', 'cuts', 'profile'],
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
name: 'explain',
|
|
1147
|
+
usage: ['rigc explain (same arguments as build, minus --profile — it never gates)'],
|
|
1148
|
+
flags: ['rig', 'motion', 'out', 'manifest', 'images', 'cut', 'cuts'],
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
name: 'validate',
|
|
1152
|
+
usage: [
|
|
1153
|
+
'rigc validate <dir | skeleton.json> [--atlas <path>] [--profile spine|spine-html]',
|
|
1154
|
+
'rigc validate --cut <name> --cuts <cuts.json> (also re-derives declared durations)',
|
|
1155
|
+
],
|
|
1156
|
+
flags: ['atlas', 'profile', 'cut', 'cuts', 'rig', 'motion', 'out', 'manifest', 'images'],
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
name: 'diff',
|
|
1160
|
+
usage: ['rigc diff <candidate.json> <reference.json> [--json <out>]'],
|
|
1161
|
+
flags: ['json'],
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
name: 'check',
|
|
1165
|
+
usage: ['rigc check --candidate <dir | skeleton.json> --frames <dir> [flags]'],
|
|
1166
|
+
flags: ['candidate', 'frames', 'atlas', 'fps', 'viewport', 'framing', 'as', 'all-frames', 'json'],
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
name: 'bench',
|
|
1170
|
+
usage: [`rigc bench <${RUNG_IDS.join(' | ')}> --candidate <dir | skeleton.json> [--frames <dir>] [flags]`],
|
|
1171
|
+
flags: ['candidate', 'atlas', 'frames', 'profile', 'all-frames', 'json'],
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
name: 'render',
|
|
1175
|
+
usage: [
|
|
1176
|
+
'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--fps 12] [--max 256] [--out render/]',
|
|
1177
|
+
],
|
|
1178
|
+
flags: ['candidate', 'atlas', 'animation', 'fps', 'max', 'out'],
|
|
1179
|
+
overrides: {
|
|
1180
|
+
out: { value: '<dir>', meaning: 'directory to write the frame series into (default `render/`)' },
|
|
1181
|
+
fps: { meaning: `frames per second to sample the animation at (default ${PROTOCOL_FPS})` },
|
|
1182
|
+
},
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
name: 'preview',
|
|
1186
|
+
usage: ['rigc preview --candidate <dir | skeleton.json> [--animation <name>] [--out preview.html]'],
|
|
1187
|
+
flags: ['candidate', 'atlas', 'animation', 'out'],
|
|
1188
|
+
overrides: {
|
|
1189
|
+
out: {
|
|
1190
|
+
value: '<file>',
|
|
1191
|
+
meaning: 'the .html file to write (default `preview.html`); a directory means "the default name in here"',
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
];
|
|
1196
|
+
|
|
1197
|
+
const KNOWN_COMMANDS = COMMANDS.map((c) => c.name);
|
|
1198
|
+
|
|
1199
|
+
/** `rigc <command> --help`: that command's own usage line(s) and flag table. */
|
|
1200
|
+
function commandHelp(name: string): string {
|
|
1201
|
+
const doc = COMMANDS.find((c) => c.name === name);
|
|
1202
|
+
if (!doc) throw new Error(`internal: no help text for command "${name}"`);
|
|
1203
|
+
const keys = [...doc.flags, 'help'];
|
|
1204
|
+
const value = (key: string): string | undefined => doc.overrides?.[key]?.value ?? FLAG_VALUES[key];
|
|
1205
|
+
const meaning = (key: string): string => doc.overrides?.[key]?.meaning ?? FLAG_MEANINGS[key];
|
|
1206
|
+
const labels = keys.map((key) => `--${key}${value(key) ? ` ${value(key)}` : ''}`);
|
|
1207
|
+
const width = Math.max(...labels.map((l) => l.length)) + 2;
|
|
1208
|
+
return ['usage:', ...doc.usage.map((u) => ` ${u}`), '', 'flags:', ...keys.map((key, i) => ` ${labels[i].padEnd(width)}${meaning(key)}`)].join(
|
|
1209
|
+
'\n',
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
728
1213
|
const USAGE = [
|
|
1214
|
+
'rigc — the rig compiler',
|
|
1215
|
+
'',
|
|
1216
|
+
'(from a source checkout: `bun cli.ts <command>` is the same as `rigc <command>`)',
|
|
1217
|
+
'',
|
|
729
1218
|
'usage:',
|
|
730
|
-
|
|
731
|
-
' bun cli.ts build --cut <name> --cuts <cuts.json>',
|
|
732
|
-
' bun cli.ts explain (same arguments as build)',
|
|
733
|
-
' bun cli.ts validate <dir | skeleton.json> [--atlas <path>]',
|
|
734
|
-
' bun cli.ts diff <candidate.json> <reference.json> [--json <out>]',
|
|
735
|
-
' bun cli.ts check --candidate <dir | skeleton.json> --frames <dir>',
|
|
736
|
-
` bun cli.ts bench <${RUNG_IDS.join(' | ')}> --candidate <dir | skeleton.json> [--frames <dir>]`,
|
|
1219
|
+
...COMMANDS.flatMap((c) => c.usage.map((u) => ` ${u}`)),
|
|
737
1220
|
'',
|
|
738
|
-
'
|
|
1221
|
+
' rigc <command> --help that command\'s own flag table',
|
|
1222
|
+
' rigc --version print the installed version (-v works too)',
|
|
1223
|
+
'',
|
|
1224
|
+
'build, validate and bench take --profile spine|spine-html:',
|
|
739
1225
|
' spine is this valid Spine 4.3 that any runtime plays correctly?',
|
|
740
|
-
'
|
|
1226
|
+
' THE DEFAULT — 20 rules, and the question the output answers when',
|
|
1227
|
+
' you import it into the Spine editor.',
|
|
1228
|
+
' spine-html the above, plus this project\'s renderer and archetype policy:',
|
|
1229
|
+
' all 34 rules, opt-in. Those extra 14 fire on real, correct,',
|
|
1230
|
+
' editor-produced Spine data, so they are somebody\'s policy rather',
|
|
1231
|
+
' than anybody\'s validity.',
|
|
1232
|
+
'',
|
|
1233
|
+
'Every report names the profile that judged it and lists, on PROF lines, the',
|
|
1234
|
+
'rules that profile left out.',
|
|
741
1235
|
'',
|
|
742
1236
|
'check renders the candidate onto the reference frames\' own pixel grid, fitting it',
|
|
743
1237
|
'there by its own drawn pixels, and compares. It reads the frames and never the',
|
|
744
1238
|
'reference skeleton, so it belongs INSIDE an authoring loop — the validator cannot',
|
|
745
|
-
'see a wrong animation and this can
|
|
746
|
-
'
|
|
747
|
-
'
|
|
748
|
-
' --
|
|
749
|
-
'
|
|
750
|
-
'
|
|
751
|
-
'
|
|
752
|
-
'
|
|
753
|
-
'
|
|
754
|
-
' --as <name> the candidate animation to play, when it is named differently',
|
|
755
|
-
' --all-frames print every frame, not just the worst by MAE',
|
|
756
|
-
' --json <out> the whole per-frame, per-slot report',
|
|
1239
|
+
'see a wrong animation and this can. See `rigc check --help` for its flags.',
|
|
1240
|
+
'',
|
|
1241
|
+
'render and preview are how you LOOK at a build, and they need no reference at all:',
|
|
1242
|
+
' rigc render --candidate <the dir build --out wrote> PNG frames + a contact sheet',
|
|
1243
|
+
' rigc preview --candidate <the same dir> one .html file that plays it',
|
|
1244
|
+
'A rig with its head off its torso passes the gate and steps cleanly — the offsets',
|
|
1245
|
+
'are the ones you asked for — so looking is the only thing that catches it. render',
|
|
1246
|
+
'draws with rigc\'s own rasteriser; preview embeds the artifact in a page that plays',
|
|
1247
|
+
'it in the official Spine Web Player, which is also the interop proof.',
|
|
757
1248
|
'',
|
|
758
1249
|
'a cuts.json is { "<name>": { "rig": "...", "motion": "...", "out": "...",',
|
|
759
1250
|
' "manifest": "..." (optional) } }, with every path',
|
|
@@ -762,17 +1253,35 @@ const USAGE = [
|
|
|
762
1253
|
|
|
763
1254
|
const [command, ...rest] = process.argv.slice(2);
|
|
764
1255
|
try {
|
|
1256
|
+
if (command === undefined) {
|
|
1257
|
+
console.error(USAGE);
|
|
1258
|
+
process.exit(2);
|
|
1259
|
+
}
|
|
1260
|
+
if (command === '--version' || command === '-v') {
|
|
1261
|
+
console.log(readVersion());
|
|
1262
|
+
process.exit(0);
|
|
1263
|
+
}
|
|
1264
|
+
if (command === '--help' || command === '-h') {
|
|
1265
|
+
console.log(USAGE);
|
|
1266
|
+
process.exit(0);
|
|
1267
|
+
}
|
|
1268
|
+
if (!KNOWN_COMMANDS.includes(command)) {
|
|
1269
|
+
throw new UsageError(`unknown command: ${command}`);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
765
1272
|
const { flags, positional } = parseArgs(rest);
|
|
1273
|
+
if (flags.help !== undefined) {
|
|
1274
|
+
console.log(commandHelp(command));
|
|
1275
|
+
process.exit(0);
|
|
1276
|
+
}
|
|
766
1277
|
if (command === 'build') cmdBuild(flags);
|
|
767
1278
|
else if (command === 'validate') cmdValidate(flags, positional);
|
|
768
1279
|
else if (command === 'explain') cmdExplain(flags);
|
|
769
1280
|
else if (command === 'diff') cmdDiff(flags, positional);
|
|
770
1281
|
else if (command === 'check') cmdCheck(flags);
|
|
771
1282
|
else if (command === 'bench') cmdBench(flags, positional);
|
|
772
|
-
else
|
|
773
|
-
|
|
774
|
-
process.exit(2);
|
|
775
|
-
}
|
|
1283
|
+
else if (command === 'render') cmdRender(flags);
|
|
1284
|
+
else if (command === 'preview') cmdPreview(flags);
|
|
776
1285
|
} catch (err) {
|
|
777
1286
|
if (err instanceof UsageError) {
|
|
778
1287
|
console.error(`rigc: ${err.message}\n\n${USAGE}`);
|