spine-rigc 1.9.3 → 2.0.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/.claude-plugin/plugin.json +1 -1
- package/NOTICE.md +37 -21
- package/README.md +38 -20
- package/bin/rigc.cjs +45 -2
- package/cli_core.ts +35 -0
- package/docs/AUTHORING.md +27 -15
- package/package.json +4 -5
- package/skills/rigc/SKILL.md +8 -5
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +53 -3
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +44 -2
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/emitted/index.ts +119 -0
- package/src/assertions/report.ts +64 -0
- package/src/cli/core_commands.ts +82 -7
- package/src/cli/shared.ts +567 -59
- package/src/cli/spine_commands.ts +36 -366
- package/src/validate.ts +16 -86
package/src/cli/shared.ts
CHANGED
|
@@ -14,16 +14,19 @@
|
|
|
14
14
|
* anything it imports: the bodies that reach the runtime are
|
|
15
15
|
* `./spine_commands.ts`'s, which only `cli.ts` imports.
|
|
16
16
|
*/
|
|
17
|
-
import { DEFAULT_PADDING, DEFAULT_PAGE_EDGES, DEFAULT_PAGE_SIZE } from '../atlas.ts';
|
|
17
|
+
import { type AtlasRegion, DEFAULT_PADDING, DEFAULT_PAGE_EDGES, DEFAULT_PAGE_SIZE, packAtlas, PAGE_EDGES, type PageEdges, pageFootprint } from '../atlas.ts';
|
|
18
|
+
import { copyAtlasPages, plannedPageCopies } from '../emit.ts';
|
|
19
|
+
import { CLI_DEFAULT_PROFILE, VALIDATE_PROFILES } from '../assertions/report.ts';
|
|
20
|
+
import type { AssertionProfile } from '../assertions/kinds.ts';
|
|
18
21
|
import { MAX_CANDIDATES, MIN_CANDIDATES } from '../ballot.ts';
|
|
19
22
|
import { BONEDIST_SPEC, IDENTITY_CORRESPONDENCE } from '../correspondence.ts';
|
|
20
23
|
import { ANCHOR_MAX_RESIDUAL, ANCHOR_MAX_UNEXPLAINED, DEFAULT_HINGE_MAX, DEFAULT_HINGE_MIN, DEFAULT_MIN_LEVER_PX, DEFAULT_MIN_VISIBLE, DEFAULT_PASSES } from '../chainfit.ts';
|
|
21
24
|
import { checkAgainstFrames, type CheckOptions, CheckPlates, type CheckReport } from '../check.ts';
|
|
22
|
-
import { CompileError, droppedStateReason, type CompileOptions } from '../compile.ts';
|
|
25
|
+
import { compile, CompileError, droppedStateReason, type CompileOptions } from '../compile.ts';
|
|
23
26
|
import { depthStepLevels, type FoldLimit, type TurnCeiling } from '../depth.ts';
|
|
24
27
|
import { parseJsonWithPosition } from '../json-position.ts';
|
|
25
28
|
import { RUNG_IDS } from '../ladder.ts';
|
|
26
|
-
import { MODEL_DOCUMENT_FILE } from '../model.ts';
|
|
29
|
+
import { MODEL_DOCUMENT_FILE, modelDocument } from '../model.ts';
|
|
27
30
|
import { DEFAULT_MAX_RESIDUAL, DEFAULT_SCALE_MAX, DEFAULT_SCALE_MIN } from '../pose.ts';
|
|
28
31
|
import {
|
|
29
32
|
CandidateAtlasError,
|
|
@@ -45,7 +48,7 @@ import { PoseError } from '../pose.ts';
|
|
|
45
48
|
import { SpineRuntimeError, SPINE_SIDE_ABSENT } from '../spine_side.ts';
|
|
46
49
|
import type { CompileResult, DroppedState } from '../types.ts';
|
|
47
50
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
48
|
-
import { dirname, join, resolve } from 'node:path';
|
|
51
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
/**
|
|
@@ -550,43 +553,96 @@ export function meshBudget(rig: CompileResult['rig']): string {
|
|
|
550
553
|
}
|
|
551
554
|
|
|
552
555
|
/**
|
|
553
|
-
*
|
|
556
|
+
* What a path names as a build — the one statement of it every command that
|
|
557
|
+
* takes a build resolves through (issue #1046): the Spine skeleton, the atlas
|
|
558
|
+
* and the model document, each with whether it is there.
|
|
554
559
|
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
560
|
+
* ⭐ A build is a directory holding `skeleton.json` and `skeleton.atlas`, and
|
|
561
|
+
* `skeleton.model.json` beside them when rigc wrote it — or a `.json`
|
|
562
|
+
* skeleton named directly, a foreign export's. A directory holding no
|
|
563
|
+
* `skeleton.json`, or a path with nothing at it, is refused here, `nothing at
|
|
564
|
+
* <path>`, for every command alike: before this, `validate` read the missing
|
|
565
|
+
* file unguarded and died on an ENOENT and a stack (#1046), and `diff` on a
|
|
566
|
+
* directory on an EISDIR. What a command then needs beyond the skeleton is the
|
|
567
|
+
* command's to say — `spinePairOf` for the atlas, `resolveDrawable` for a
|
|
568
|
+
* rigc build drawn without one.
|
|
561
569
|
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
570
|
+
* Two shapes of target, because rigc's own output and a foreign export are
|
|
571
|
+
* named differently and both have to be gateable. rigc writes
|
|
572
|
+
* `skeleton.json` + `skeleton.atlas` into a directory. Everybody else writes
|
|
573
|
+
* whatever the editor called the project, and the official examples are not
|
|
574
|
+
* even consistent with themselves — `7-anticipation/export/` holds
|
|
575
|
+
* `sack-pro.json`, `spineboy/export/` holds two skeletons and two atlases.
|
|
576
|
+
*
|
|
577
|
+
* ⚠️ When more than one atlas sits beside a named skeleton, the atlas is not
|
|
578
|
+
* chosen: guessing by name would be wrong on the corpus that motivated it —
|
|
579
|
+
* `spineboy-ess` shares a longer prefix with `spineboy-run.atlas` than with
|
|
580
|
+
* the `spineboy.atlas` it actually uses, so the plausible heuristic picks the
|
|
581
|
+
* wrong file and every attachment then resolves against the wrong pixels —
|
|
582
|
+
* silently, which is the exact failure mode this tool exists to remove. The
|
|
583
|
+
* refusal is the atlas's (`atlasRefusal`), said by a command that reads one;
|
|
584
|
+
* `diff`, which reads the skeleton alone, is not refused for it.
|
|
568
585
|
*/
|
|
569
|
-
export
|
|
586
|
+
export interface BuildFiles {
|
|
587
|
+
/** The Spine skeleton — `skeleton.json` in a directory, or the `.json` named — and there, or `resolveBuild` refuses. */
|
|
588
|
+
skeletonPath: string;
|
|
589
|
+
/** The atlas: `--atlas`, else `skeleton.atlas` in a directory, else the one `.atlas` beside a named `.json` (or `skeleton.atlas` there, when there is none). */
|
|
590
|
+
atlasPath: string;
|
|
591
|
+
/** `there` — a file at `atlasPath`; `absent` — none; `ambiguous` — several beside a named `.json` and no `--atlas`. */
|
|
592
|
+
atlas: 'there' | 'absent' | 'ambiguous';
|
|
593
|
+
/** What a command that reads the atlas says where `atlas` is not `there`. */
|
|
594
|
+
atlasRefusal: string;
|
|
595
|
+
/** `skeleton.model.json` beside the skeleton, or `null` where there is none (a Spine export). */
|
|
596
|
+
modelPath: string | null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function resolveBuild(target: string, atlasFlag: string | undefined): BuildFiles {
|
|
570
600
|
const abs = resolve(target);
|
|
571
601
|
if (!existsSync(abs)) throw new UsageError(`nothing at ${abs}`);
|
|
602
|
+
let skeletonPath: string;
|
|
603
|
+
let atlasPath: string;
|
|
604
|
+
let atlas: BuildFiles['atlas'];
|
|
605
|
+
let atlasRefusal: string;
|
|
572
606
|
if (statSync(abs).isDirectory()) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
607
|
+
skeletonPath = join(abs, 'skeleton.json');
|
|
608
|
+
atlasPath = atlasFlag ? resolve(atlasFlag) : join(abs, 'skeleton.atlas');
|
|
609
|
+
atlas = existsSync(atlasPath) ? 'there' : 'absent';
|
|
610
|
+
atlasRefusal = `nothing at ${atlasPath}`;
|
|
611
|
+
} else {
|
|
612
|
+
if (!abs.endsWith('.json')) throw new UsageError(`${abs} is neither a directory nor a .json skeleton`);
|
|
613
|
+
skeletonPath = abs;
|
|
614
|
+
if (atlasFlag) {
|
|
615
|
+
atlasPath = resolve(atlasFlag);
|
|
616
|
+
atlas = existsSync(atlasPath) ? 'there' : 'absent';
|
|
617
|
+
atlasRefusal = `nothing at ${atlasPath}`;
|
|
618
|
+
} else {
|
|
619
|
+
const dir = dirname(abs);
|
|
620
|
+
const atlases = readdirSync(dir)
|
|
621
|
+
.filter((f) => f.endsWith('.atlas'))
|
|
622
|
+
.sort();
|
|
623
|
+
atlasPath = join(dir, atlases.length === 1 ? atlases[0] : 'skeleton.atlas');
|
|
624
|
+
atlas = atlases.length === 1 ? 'there' : atlases.length === 0 ? 'absent' : 'ambiguous';
|
|
625
|
+
atlasRefusal =
|
|
626
|
+
atlases.length === 0
|
|
627
|
+
? `no .atlas beside ${abs}; name one with --atlas <path>`
|
|
628
|
+
: `${atlases.length} atlases beside ${abs} (${atlases.join(', ')}); name the right one with --atlas <path> ` +
|
|
629
|
+
'— guessing by filename is how an attachment quietly resolves against the wrong page';
|
|
630
|
+
}
|
|
577
631
|
}
|
|
578
|
-
if (!
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
632
|
+
if (!existsSync(skeletonPath)) throw new UsageError(`nothing at ${skeletonPath}`);
|
|
633
|
+
const model = join(dirname(skeletonPath), MODEL_DOCUMENT_FILE);
|
|
634
|
+
return { skeletonPath, atlasPath, atlas, atlasRefusal, modelPath: existsSync(model) ? model : null };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* The build's Spine pair, for a command that reads both files — the round
|
|
639
|
+
* trip (`validate`, `bench`), `bonedist`, `preview`, `vote` — refused naming
|
|
640
|
+
* the atlas where it is not there (`atlasRefusal`); the skeleton is
|
|
641
|
+
* `resolveBuild`'s.
|
|
642
|
+
*/
|
|
643
|
+
export function spinePairOf(build: BuildFiles): { skeletonPath: string; atlasPath: string; atlasDir: string } {
|
|
644
|
+
if (build.atlas !== 'there') throw new UsageError(build.atlasRefusal);
|
|
645
|
+
return { skeletonPath: build.skeletonPath, atlasPath: build.atlasPath, atlasDir: dirname(build.atlasPath) };
|
|
590
646
|
}
|
|
591
647
|
|
|
592
648
|
/**
|
|
@@ -701,7 +757,7 @@ export function writeJson(target: string, body: unknown): void {
|
|
|
701
757
|
// behind, and it keeps `--out` meaning one thing per command instead of naming
|
|
702
758
|
// the build directory on the way in and the pictures on the way out.
|
|
703
759
|
|
|
704
|
-
/**
|
|
760
|
+
/** `--candidate`, the one build `preview`, `bench` and `bonedist` read the Spine pair of (`spinePairOf`). */
|
|
705
761
|
export function resolveViewable(flags: Record<string, string>): {
|
|
706
762
|
skeletonPath: string;
|
|
707
763
|
atlasPath: string;
|
|
@@ -710,11 +766,7 @@ export function resolveViewable(flags: Record<string, string>): {
|
|
|
710
766
|
if (flags.candidate === undefined) {
|
|
711
767
|
throw new UsageError('needs --candidate <dir | skeleton.json> — the directory `build --out` wrote');
|
|
712
768
|
}
|
|
713
|
-
|
|
714
|
-
for (const path of [skeletonPath, atlasPath]) {
|
|
715
|
-
if (!existsSync(path)) throw new UsageError(`nothing at ${path}`);
|
|
716
|
-
}
|
|
717
|
-
return { skeletonPath, atlasPath, atlasDir: dirname(atlasPath) };
|
|
769
|
+
return spinePairOf(resolveBuild(flags.candidate, flags.atlas));
|
|
718
770
|
}
|
|
719
771
|
|
|
720
772
|
/** What `render` and `check` say where a rigc build's atlas is not there (issue #1020). */
|
|
@@ -735,22 +787,18 @@ export const ATLAS_ABSENT = 'a build the core poses from a rigc-compiled/2 or /3
|
|
|
735
787
|
* read, as it always was: the core holds it to the document's `pages` and
|
|
736
788
|
* draws through spine-core, saying why, when it is not the one the build
|
|
737
789
|
* wrote (#1016), and `check` reads its `scale:` lines for the texture note.
|
|
738
|
-
* Everywhere else this is `resolveViewable`'s refusal, word for word
|
|
790
|
+
* Everywhere else this is `resolveViewable`'s refusal, word for word — both
|
|
791
|
+
* read the build off `resolveBuild` (issue #1046).
|
|
739
792
|
*/
|
|
740
793
|
export function resolveDrawable(target: string, atlasFlag: string | undefined): { skeletonPath: string; atlasPath: string; atlasText: string | null } {
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const dir = dirname(skeletonPath);
|
|
746
|
-
const atlasPath = join(dir, 'skeleton.atlas');
|
|
747
|
-
const noAtlas = directory ? !existsSync(atlasPath) : abs.endsWith('.json') && !readdirSync(dir).some((f) => f.endsWith('.atlas'));
|
|
748
|
-
if (noAtlas && existsSync(join(dir, MODEL_DOCUMENT_FILE))) {
|
|
749
|
-
if (!existsSync(skeletonPath)) throw new UsageError(`nothing at ${skeletonPath}`);
|
|
794
|
+
const build = resolveBuild(target, atlasFlag);
|
|
795
|
+
const { skeletonPath, atlasPath } = build;
|
|
796
|
+
if (atlasFlag === undefined && build.atlas === 'absent') {
|
|
797
|
+
if (build.modelPath !== null) {
|
|
750
798
|
return { skeletonPath, atlasPath, atlasText: null };
|
|
751
799
|
}
|
|
752
800
|
}
|
|
753
|
-
|
|
801
|
+
if (build.atlas !== 'there') throw new UsageError(build.atlasRefusal);
|
|
754
802
|
return { skeletonPath, atlasPath, atlasText: readFileSync(atlasPath, 'utf8') };
|
|
755
803
|
}
|
|
756
804
|
|
|
@@ -1104,6 +1152,23 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1104
1152
|
dir: '<path>',
|
|
1105
1153
|
};
|
|
1106
1154
|
|
|
1155
|
+
/**
|
|
1156
|
+
* A command whose body differs by entry (issue #1060): the body the entry that
|
|
1157
|
+
* links none of spine-core runs under the same name, as that entry's page
|
|
1158
|
+
* documents it. `runtime.for` still says what the full entry's body runs
|
|
1159
|
+
* through the runtime for, and the full entry's page is the command's own
|
|
1160
|
+
* fields, unchanged; the second entry's page takes `usage`, `flags` (the
|
|
1161
|
+
* command's own where this states none) and `notes` from here
|
|
1162
|
+
* (`entryCommands`). Registered by `./core_commands.ts`'s `CORE_ENTRY_RUNS`,
|
|
1163
|
+
* which only the second entry registers — `RC26` holds each body to the
|
|
1164
|
+
* closure of the module that registers it.
|
|
1165
|
+
*/
|
|
1166
|
+
interface CoreBody {
|
|
1167
|
+
usage: string[];
|
|
1168
|
+
flags?: string[];
|
|
1169
|
+
notes: string[];
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1107
1172
|
interface CommandDoc {
|
|
1108
1173
|
name: string;
|
|
1109
1174
|
/** One or more invocation forms, each already spelling the command name. */
|
|
@@ -1156,7 +1221,7 @@ interface CommandDoc {
|
|
|
1156
1221
|
* import graph of the module whose bodies register the command, and a
|
|
1157
1222
|
* mark that disagrees with the graph is red by name.
|
|
1158
1223
|
*/
|
|
1159
|
-
runtime: false | { for: string };
|
|
1224
|
+
runtime: false | { for: string; core?: CoreBody };
|
|
1160
1225
|
/**
|
|
1161
1226
|
* Whether the command exists for the Spine format — reads, writes, compares
|
|
1162
1227
|
* or embeds Spine skeleton data as the whole of what it does with it — so
|
|
@@ -1169,7 +1234,29 @@ interface CommandDoc {
|
|
|
1169
1234
|
export const COMMANDS: CommandDoc[] = [
|
|
1170
1235
|
{
|
|
1171
1236
|
name: 'build',
|
|
1172
|
-
runtime: {
|
|
1237
|
+
runtime: {
|
|
1238
|
+
for: 'the gate round-trips every build through it before anything is written',
|
|
1239
|
+
core: {
|
|
1240
|
+
usage: [
|
|
1241
|
+
'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|spine-html] [--copy-images] (the same files cli.ts build writes; gated without spine-core — see build --help)',
|
|
1242
|
+
`rigc build … --pack [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] (parts onto shared pages, written into --out)`,
|
|
1243
|
+
'rigc build … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made)',
|
|
1244
|
+
'rigc build --cut <name> --cuts <cuts.json>',
|
|
1245
|
+
],
|
|
1246
|
+
notes: [
|
|
1247
|
+
'this entry\'s build writes what `bun cli.ts build` writes — skeleton.json, skeleton.atlas,',
|
|
1248
|
+
'skeleton.model.json and the pages --pack or --copy-images put in --out — by the same',
|
|
1249
|
+
'body, and only when no assertion fails. Its gate is not the round trip through',
|
|
1250
|
+
'spine-core, which this entry links none of. What runs instead: the model side over',
|
|
1251
|
+
'the document (every assertion moved off the round trip), and the round trip\'s own',
|
|
1252
|
+
'rules restated over the text the emitter wrote — A01, A02, A05, A07, A16, A31, A35,',
|
|
1253
|
+
'and A18 over a second, independent compile\'s skeleton, atlas and document. What does',
|
|
1254
|
+
'not run is A00_ROUNDTRIP_PARSE, spine-core\'s parse: it reports SKIP naming spine-core,',
|
|
1255
|
+
'and `bun cli.ts build` and `rigc validate` run it. The report\'s last line says which',
|
|
1256
|
+
'ran here and which did not.',
|
|
1257
|
+
],
|
|
1258
|
+
},
|
|
1259
|
+
},
|
|
1173
1260
|
spineFormat: true,
|
|
1174
1261
|
usage: [
|
|
1175
1262
|
'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|spine-html] [--copy-images]',
|
|
@@ -1485,9 +1572,9 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1485
1572
|
/** Every command's name, in the order the usage lists them — what the full entry dispatches over. */
|
|
1486
1573
|
export const KNOWN_COMMANDS = COMMANDS.map((c) => c.name);
|
|
1487
1574
|
|
|
1488
|
-
/** `rigc <command> --help`: that command's own usage line(s) and flag table. */
|
|
1489
|
-
export function commandHelp(name: string): string {
|
|
1490
|
-
const doc =
|
|
1575
|
+
/** `rigc <command> --help`: that command's own usage line(s) and flag table, as the entry running it (`docs`) documents it. */
|
|
1576
|
+
export function commandHelp(name: string, docs: readonly CommandDoc[] = COMMANDS): string {
|
|
1577
|
+
const doc = docs.find((c) => c.name === name);
|
|
1491
1578
|
if (!doc) throw new Error(`internal: no help text for command "${name}"`);
|
|
1492
1579
|
const keys = [...doc.flags, 'help'];
|
|
1493
1580
|
const value = (key: string): string | undefined => doc.overrides?.[key]?.value ?? FLAG_VALUES[key];
|
|
@@ -1672,6 +1759,417 @@ export function usageText(docs: readonly CommandDoc[], checkout: string, rules:
|
|
|
1672
1759
|
return lines.join('\n');
|
|
1673
1760
|
}
|
|
1674
1761
|
|
|
1762
|
+
// ---------------------------------------------------------------------------
|
|
1763
|
+
// what both `build` bodies share (issue #1060): the flags, the header and the compile's report
|
|
1764
|
+
// ---------------------------------------------------------------------------
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Read `--profile`, defaulting to `spine` — see `CLI_DEFAULT_PROFILE`.
|
|
1768
|
+
*
|
|
1769
|
+
* An unknown name is a usage error rather than a silent fallback, and that
|
|
1770
|
+
* matters in both directions: a typo used to re-apply the strictest rulebook to
|
|
1771
|
+
* data the caller was trying to exempt, and it would now drop the policy layer
|
|
1772
|
+
* from a caller who typed `--profile spine-htlm` and believes they asked for it.
|
|
1773
|
+
* Neither is something to discover from a green.
|
|
1774
|
+
*/
|
|
1775
|
+
export function readProfile(flags: Record<string, string>): AssertionProfile {
|
|
1776
|
+
const raw = flags.profile;
|
|
1777
|
+
if (raw === undefined) return CLI_DEFAULT_PROFILE;
|
|
1778
|
+
const found = VALIDATE_PROFILES.find((p) => p === raw);
|
|
1779
|
+
if (!found) throw new UsageError(`--profile ${JSON.stringify(raw)}; known profiles: ${VALIDATE_PROFILES.join(', ')}`);
|
|
1780
|
+
return found;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Read one non-negative integer flag, or its default.
|
|
1785
|
+
*
|
|
1786
|
+
* A usage error rather than a `NaN` that reaches the packer: `--padding two`
|
|
1787
|
+
* would otherwise place every region at NaN and write a blank page, which is a
|
|
1788
|
+
* green build and an empty picture.
|
|
1789
|
+
*/
|
|
1790
|
+
export function readIntFlag(flags: Record<string, string>, name: string, fallback: number): number {
|
|
1791
|
+
const raw = flags[name];
|
|
1792
|
+
if (raw === undefined) return fallback;
|
|
1793
|
+
if (!/^\d+$/.test(raw)) throw new UsageError(`--${name} takes a non-negative integer, got ${JSON.stringify(raw)}`);
|
|
1794
|
+
return Number(raw);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/**
|
|
1798
|
+
* Read `--page-edges`, or its default — `pot`.
|
|
1799
|
+
*
|
|
1800
|
+
* An unknown value is a usage error for `readProfile`'s reason: a typo that fell
|
|
1801
|
+
* back to `pot` would hand the caller who asked for the smaller page the bigger
|
|
1802
|
+
* one, green, and say nothing.
|
|
1803
|
+
*/
|
|
1804
|
+
export function readPageEdges(flags: Record<string, string>): PageEdges {
|
|
1805
|
+
const raw = flags['page-edges'];
|
|
1806
|
+
if (raw === undefined) return DEFAULT_PAGE_EDGES;
|
|
1807
|
+
const found = PAGE_EDGES.find((e) => e === raw);
|
|
1808
|
+
if (!found) throw new UsageError(`--page-edges ${JSON.stringify(raw)}; known values: ${PAGE_EDGES.join(', ')}`);
|
|
1809
|
+
return found;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* The rectangle a region occupies **on its page**, for a line that has already
|
|
1814
|
+
* said where the region is — and the empty string where the page rectangle is
|
|
1815
|
+
* the one `bounds:` already states.
|
|
1816
|
+
*
|
|
1817
|
+
* ## The fact no surface an author reads carried (issue #718)
|
|
1818
|
+
*
|
|
1819
|
+
* The atlas line beside this clause prints the DRAWING's size, because that is
|
|
1820
|
+
* what an attachment's width and height mean. A packer that turned the drawing a
|
|
1821
|
+
* quarter to fit it wrote `bounds:` in the drawing's orientation too. So an
|
|
1822
|
+
* author holding the pack and the build report had neither end of the rectangle
|
|
1823
|
+
* they have to cut out of the page to measure a part against a rendered frame —
|
|
1824
|
+
* and the one place rigc printed it was `A06`'s overlap text, reachable only
|
|
1825
|
+
* under `--profile spine-html`. The knowledge was in the tree the whole time:
|
|
1826
|
+
* `pageFootprint` has derived this rectangle for every reader of it since issue
|
|
1827
|
+
* #579, and nothing an author reads said it.
|
|
1828
|
+
*
|
|
1829
|
+
* ⚠️ **The condition is `pageFootprint`'s own answer, not a second reading of
|
|
1830
|
+
* `degrees`.** Re-spelling that predicate here is the exact duplication #579 was
|
|
1831
|
+
* filed on — four readers derived this rectangle and two derived it wrongly — so
|
|
1832
|
+
* the clause asks the function whether its answer differs from the `bounds:`
|
|
1833
|
+
* line, and prints only then.
|
|
1834
|
+
*
|
|
1835
|
+
* 🔸 A consequence worth stating rather than leaving to be discovered: a region
|
|
1836
|
+
* whose KEPT rectangle is square is silent here, because a quarter turn leaves
|
|
1837
|
+
* its footprint the same two numbers and there is nothing the pack does not
|
|
1838
|
+
* already say. The general rule — `bounds` is the unturned size, the footprint
|
|
1839
|
+
* is its transpose at `rotate: 90` and `rotate: 270`, and which way to turn the
|
|
1840
|
+
* rectangle to recover the drawing — belongs to an author's own reading and is
|
|
1841
|
+
* stated in `docs/AUTHORING.md` §0.2, which holds for every region including
|
|
1842
|
+
* that one.
|
|
1843
|
+
*/
|
|
1844
|
+
function pageRectangle(region: AtlasRegion): string {
|
|
1845
|
+
const foot = pageFootprint(region);
|
|
1846
|
+
return foot.width === region.width && foot.height === region.height
|
|
1847
|
+
? ''
|
|
1848
|
+
: `, occupies ${foot.width}x${foot.height}`;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/**
|
|
1852
|
+
* `build`'s invocation, read and refused before anything compiles — the cut,
|
|
1853
|
+
* the profile, and the flag combinations that disagree about one question —
|
|
1854
|
+
* for both bodies of the command (issue #1060): the one that writes the Spine
|
|
1855
|
+
* pair (`./spine_commands.ts`) and the one that writes the model document
|
|
1856
|
+
* alone (`./core_commands.ts`). Moved here unchanged from `cmdBuild`.
|
|
1857
|
+
*/
|
|
1858
|
+
export function readBuildInvocation(flags: Record<string, string>): { label: string; opts: CompileOptions; profile: AssertionProfile; packing: boolean } {
|
|
1859
|
+
const { label, opts } = resolveCut(flags);
|
|
1860
|
+
const profile = readProfile(flags);
|
|
1861
|
+
const packing = flags.pack !== undefined;
|
|
1862
|
+
// Two combinations are refused rather than silently resolved, because in each
|
|
1863
|
+
// one the two flags disagree about a single question and there is no answer
|
|
1864
|
+
// that is not a guess about which the caller meant. (There were three until
|
|
1865
|
+
// issue #266 — see the note below the second.)
|
|
1866
|
+
if (packing && opts.atlasInPath !== undefined) {
|
|
1867
|
+
throw new UsageError(
|
|
1868
|
+
'--pack and --atlas-in are opposite directions through the same door: --pack MAKES an atlas out of the ' +
|
|
1869
|
+
'loose parts, --atlas-in resolves the parts against one somebody already made. Pick one',
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
if (packing && flags['copy-images'] !== undefined) {
|
|
1873
|
+
throw new UsageError(
|
|
1874
|
+
'--pack already writes self-contained pages into --out (that is what packing is), and --copy-images copies ' +
|
|
1875
|
+
'the loose part PNGs, which a packed atlas does not reference. Drop --copy-images',
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
// The copy itself happens after the gate (below). The header has to know NOW,
|
|
1879
|
+
// because the skeleton text the gate reads is the skeleton text that is written
|
|
1880
|
+
// — `skeleton.images` says where the parts will be (issue #370).
|
|
1881
|
+
if (flags['copy-images'] !== undefined) opts.copyImages = true;
|
|
1882
|
+
// `--pack --profile spine-html` used to be the third refusal here, because
|
|
1883
|
+
// A06's coverage clause was "one part per page" flat and a legitimate pack
|
|
1884
|
+
// arrived at the gate reading as a defect. Since issue #266's second follow-up
|
|
1885
|
+
// that clause is "one part per page OR a tiling page", so the combination is
|
|
1886
|
+
// now a build like any other — and it is the only one that puts the renderer's
|
|
1887
|
+
// own rulebook over shared-page sampling.
|
|
1888
|
+
if (!packing) {
|
|
1889
|
+
for (const name of ['page-size', 'padding', 'page-edges'] as const) {
|
|
1890
|
+
if (flags[name] !== undefined) throw new UsageError(`--${name} only means something with --pack`);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return { label, opts, profile, packing };
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/** The lines a build opens on: the cut, then its two input files. */
|
|
1897
|
+
export function printBuildHeader(label: string, opts: CompileOptions): void {
|
|
1898
|
+
console.log(`rigc build ${label}`);
|
|
1899
|
+
// Named explicitly and on their own lines rather than folded into the header
|
|
1900
|
+
// above: with two input files, a header that names only one of them (the rig,
|
|
1901
|
+
// historically) reads as though it were the one at fault whenever the error
|
|
1902
|
+
// that follows actually comes from the other.
|
|
1903
|
+
console.log(` .. rig ${opts.rigPath}`);
|
|
1904
|
+
console.log(` .. motion ${opts.motionPath}`);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/** What a compile measured, as `build` reports it before its gate: the parts, the dropped states, the absent parts, the meshes, the physics. */
|
|
1908
|
+
export function printCompiled(opts: CompileOptions, result: Pick<CompileResult, 'images' | 'droppedStates' | 'absentParts' | 'meshes' | 'rig' | 'physics'>): void {
|
|
1909
|
+
if (opts.atlasInPath !== undefined) console.log(` .. atlas-in ${opts.atlasInPath}`);
|
|
1910
|
+
console.log(` .. ${result.images.length} part page(s):`);
|
|
1911
|
+
for (const img of result.images) {
|
|
1912
|
+
// An imported part says where on the page it came from, because "resolved
|
|
1913
|
+
// against a region" is the claim `--atlas-in` makes and a line that only
|
|
1914
|
+
// repeated the page filename would look identical for all of them. A page
|
|
1915
|
+
// that declares a `scale:` also says so and shows the texels it was read
|
|
1916
|
+
// from: the size on the left is the DRAWING's and the rectangle is the
|
|
1917
|
+
// pack's, and issue #267 is the report that printed the second as the first.
|
|
1918
|
+
//
|
|
1919
|
+
// `pageRectangle` closes the line's last silence (issue #718), and it is
|
|
1920
|
+
// placed LAST rather than beside the turn it follows from, which is where
|
|
1921
|
+
// the card put it. The two clauses collide nowhere else, and the collision
|
|
1922
|
+
// is real: `scale 0.5 (373x106 texels)` is itself a size, so
|
|
1923
|
+
// `rotate 90, occupies 106x373 scale 0.5 (…)` reads as though the footprint
|
|
1924
|
+
// were the scaled quantity. As a trailing clause of the whole location
|
|
1925
|
+
// phrase it is unambiguous with a `scale:` line and identical to the card's
|
|
1926
|
+
// wording without one, which is every pack that has no `scale:` to state.
|
|
1927
|
+
const where =
|
|
1928
|
+
img.atlas === undefined
|
|
1929
|
+
? img.page
|
|
1930
|
+
: `${img.page} @ ${img.atlas.x},${img.atlas.y}${img.atlas.degrees ? ` rotate ${img.atlas.degrees}` : ''}` +
|
|
1931
|
+
(img.atlasScale === undefined
|
|
1932
|
+
? ''
|
|
1933
|
+
: ` scale ${img.atlasScale} (${img.atlas.originalWidth}x${img.atlas.originalHeight} texels)`) +
|
|
1934
|
+
pageRectangle(img.atlas);
|
|
1935
|
+
console.log(` .. ${img.region.padEnd(24)} ${img.width}x${img.height} <- ${where}`);
|
|
1936
|
+
}
|
|
1937
|
+
for (const d of result.droppedStates) console.log(dropLine(d));
|
|
1938
|
+
// "The optional slots are optional" is a claim about this code path, so this
|
|
1939
|
+
// code path says which ones it left out rather than being silently right.
|
|
1940
|
+
for (const a of result.absentParts) {
|
|
1941
|
+
console.log(` ABSENT ${a.slot}: ${a.why} — slot not emitted`);
|
|
1942
|
+
}
|
|
1943
|
+
for (const m of result.meshes) {
|
|
1944
|
+
console.log(
|
|
1945
|
+
` MESH ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|
|
1946
|
+
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}` +
|
|
1947
|
+
meshDepthNote(m) +
|
|
1948
|
+
meshInfluenceNote(m),
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
for (const ph of result.physics) {
|
|
1952
|
+
console.log(
|
|
1953
|
+
` PHYS ${ph.name.padEnd(12)} bone=${ph.bone.padEnd(14)} components=[${ph.components.join(', ')}] ` +
|
|
1954
|
+
`mix=${ph.mix}${ph.drivesMesh ? ' <- drives a mesh: its canvas re-rasterises while the spring settles' : ''}`,
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
/**
|
|
1961
|
+
* An atlas text to gate INSTEAD of the compile's own, with the second, independent
|
|
1962
|
+
* emit A18 compares it against.
|
|
1963
|
+
*
|
|
1964
|
+
* `--pack` is the only caller. A packed build is gated twice on purpose — once as
|
|
1965
|
+
* compiled (which is the gate that reads the loose PNGs, so `A06`'s size-vs-file
|
|
1966
|
+
* clause still measures the art R5 measures) and once as packed (which is the pair
|
|
1967
|
+
* that actually ships). Handing the second pass its texts rather than re-deriving
|
|
1968
|
+
* them here keeps `runGate` ignorant of what a pack is.
|
|
1969
|
+
*/
|
|
1970
|
+
interface AtlasOverride {
|
|
1971
|
+
text: string;
|
|
1972
|
+
again: string;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/**
|
|
1976
|
+
* What one gate of `build` is handed (issue #1060): the compile, the atlas text
|
|
1977
|
+
* it gates (the compile's own, or the packed one), the directory its pages
|
|
1978
|
+
* resolve against, the document `build` writes, and a second, independent
|
|
1979
|
+
* compile's three texts for A18 — the same arguments `cli.ts build` has always
|
|
1980
|
+
* handed `validate()`, so each entry's gate reads one statement of them.
|
|
1981
|
+
*/
|
|
1982
|
+
export interface BuildGateInput {
|
|
1983
|
+
result: CompileResult;
|
|
1984
|
+
atlasText: string;
|
|
1985
|
+
atlasDir: string;
|
|
1986
|
+
modelText: string;
|
|
1987
|
+
reEmit: { skeletonText: string; atlasText: string; modelText: string };
|
|
1988
|
+
profile: AssertionProfile;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/**
|
|
1992
|
+
* The gate a `build` body runs, by entry (issue #1060): `cli.ts`'s is the
|
|
1993
|
+
* round trip through spine-core (`./spine_commands.ts`), `cli_core.ts`'s the
|
|
1994
|
+
* model side and the rules restated over the emitted text
|
|
1995
|
+
* (`./core_commands.ts`). Everything else `build` does — compile, the
|
|
1996
|
+
* document, `--copy-images`, `--pack`, the writes and their order — is
|
|
1997
|
+
* `runBuild`'s, written once, so the two entries write the same build.
|
|
1998
|
+
*/
|
|
1999
|
+
export interface BuildGate {
|
|
2000
|
+
/** The line before the first gate's report, naming what judges it. */
|
|
2001
|
+
heading: (profile: AssertionProfile) => string;
|
|
2002
|
+
/** Run the gate, print its report, return the failure count. */
|
|
2003
|
+
run: (input: BuildGateInput) => number;
|
|
2004
|
+
/** The command a green build ends by naming, for its own `--out`. */
|
|
2005
|
+
look: (outDir: string) => string;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
function runGate(
|
|
2009
|
+
gate: BuildGate,
|
|
2010
|
+
result: CompileResult,
|
|
2011
|
+
modelText: string,
|
|
2012
|
+
opts: CompileOptions,
|
|
2013
|
+
profile: AssertionProfile,
|
|
2014
|
+
/** The atlas text `build` writes for a compile's own — `--copy-images` renames the pages — which the second compile's document is spelled from, as `modelText` was (issue #1016). */
|
|
2015
|
+
written: (atlasText: string) => string,
|
|
2016
|
+
atlas?: AtlasOverride,
|
|
2017
|
+
): number {
|
|
2018
|
+
// The determinism check compares a second, independent compile — its model
|
|
2019
|
+
// document included, which is the text `build` writes beside the pair, and
|
|
2020
|
+
// which states where each region sits in the atlas written with it (`pages`,
|
|
2021
|
+
// issue #1016): so the second document is spelled from the second compile's
|
|
2022
|
+
// atlas as it would be written, or from the second, independent pack.
|
|
2023
|
+
const again = compile(opts);
|
|
2024
|
+
return gate.run({
|
|
2025
|
+
result,
|
|
2026
|
+
atlasText: atlas ? atlas.text : result.atlasText,
|
|
2027
|
+
atlasDir: opts.outDir,
|
|
2028
|
+
modelText,
|
|
2029
|
+
reEmit: { skeletonText: again.skeletonText, atlasText: atlas ? atlas.again : again.atlasText, modelText: modelDocument(again.model, again.skeletonText, atlas ? atlas.again : written(again.atlasText)) },
|
|
2030
|
+
profile,
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* build — moved here unchanged from `./spine_commands.ts` (issue #1060) but
|
|
2036
|
+
* for its gate, which the entry hands in (`BuildGate`): both entries write
|
|
2037
|
+
* the Spine pair, the model document and the pages by this one body.
|
|
2038
|
+
*/
|
|
2039
|
+
export function runBuild(flags: Record<string, string>, gate: BuildGate): void {
|
|
2040
|
+
const { label, opts, profile, packing } = readBuildInvocation(flags);
|
|
2041
|
+
printBuildHeader(label, opts);
|
|
2042
|
+
const result = compile(opts);
|
|
2043
|
+
printCompiled(opts, result);
|
|
2044
|
+
|
|
2045
|
+
// The model's document is spelled before the gate, so the text A18 compares
|
|
2046
|
+
// is the text written, and a model the document cannot carry is refused
|
|
2047
|
+
// before anything is (issue #922). It states where each region sits in the
|
|
2048
|
+
// atlas written beside it (`pages`, issue #1016), so it is spelled from that
|
|
2049
|
+
// atlas: under `--copy-images` the text with the copies' page names, planned
|
|
2050
|
+
// here from the text alone (`plannedPageCopies`) and copied after the gate.
|
|
2051
|
+
// Under `--pack` the pages move again after this gate, and the document
|
|
2052
|
+
// written is the one the packed gate below spells and compares.
|
|
2053
|
+
const copying = flags['copy-images'] !== undefined;
|
|
2054
|
+
const writtenAtlas = (atlasText: string): string => (copying ? plannedPageCopies(atlasText, opts.outDir).atlasText : atlasText);
|
|
2055
|
+
let modelText = modelDocument(result.model, result.skeletonText, writtenAtlas(result.atlasText));
|
|
2056
|
+
console.log(gate.heading(profile));
|
|
2057
|
+
const failures = runGate(gate, result, modelText, opts, profile, writtenAtlas);
|
|
2058
|
+
if (failures > 0) {
|
|
2059
|
+
console.error(`rigc: ${failures} assertion(s) failed — nothing written`);
|
|
2060
|
+
process.exit(1);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
mkdirSync(opts.outDir, { recursive: true });
|
|
2064
|
+
|
|
2065
|
+
// `--copy-images`: `--out` is otherwise NOT self-contained — a page's default
|
|
2066
|
+
// path is relative to the source art (often `../parts/foo.png`), which is
|
|
2067
|
+
// correct for a build sitting beside the project it came from and breaks the
|
|
2068
|
+
// moment the directory is zipped, committed or moved on its own (issue #217).
|
|
2069
|
+
// Opt-in only: the default stays exactly what it has always been.
|
|
2070
|
+
//
|
|
2071
|
+
// What is copied is what the ATLAS names, not what the image list holds: under
|
|
2072
|
+
// `--atlas-in` the two are different lists, and rebuilding the text from the
|
|
2073
|
+
// second wrote a file the pack never contained — zero bytes for a rig that
|
|
2074
|
+
// declares no parts, one fabricated page per part for a rig that does, both of
|
|
2075
|
+
// them green here because the gate above had already read the compile's own
|
|
2076
|
+
// text (issue #693, `src/emit.ts`).
|
|
2077
|
+
let atlasText = result.atlasText;
|
|
2078
|
+
if (flags['copy-images'] !== undefined) {
|
|
2079
|
+
const copied = copyAtlasPages(atlasText, opts.outDir);
|
|
2080
|
+
// The document's pages were spelled from the plan; the copy is held to it before anything of the pair is written.
|
|
2081
|
+
if (copied.atlasText !== writtenAtlas(result.atlasText)) {
|
|
2082
|
+
throw new Error(`internal: --copy-images wrote an atlas whose page names are not the ones ${MODEL_DOCUMENT_FILE} was spelled with — nothing of the pair was written`);
|
|
2083
|
+
}
|
|
2084
|
+
atlasText = copied.atlasText;
|
|
2085
|
+
console.log(` .. copy-images: ${copied.pages.length} page(s) copied into ${opts.outDir}`);
|
|
2086
|
+
for (const p of copied.pages) {
|
|
2087
|
+
const note = p.to === basename(p.from) ? '' : ' (renamed — basename collision)';
|
|
2088
|
+
console.log(` .. ${p.to.padEnd(24)} <- ${p.from} (${p.regions} region(s))${note}`);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// `--pack`: the parts go onto shared pages, which are written here as real
|
|
2093
|
+
// PNGs, so `--out` is self-contained by construction. The atlas above stays
|
|
2094
|
+
// the one the gate just read — packing changes only the ARRANGEMENT of the
|
|
2095
|
+
// bytes, and the sizes in `result.images` are still the ones measured off the
|
|
2096
|
+
// loose PNGs (see src/atlas.ts's header).
|
|
2097
|
+
if (packing) {
|
|
2098
|
+
const packOpts = {
|
|
2099
|
+
pageSize: readIntFlag(flags, 'page-size', DEFAULT_PAGE_SIZE),
|
|
2100
|
+
padding: readIntFlag(flags, 'padding', DEFAULT_PADDING),
|
|
2101
|
+
pageEdges: readPageEdges(flags),
|
|
2102
|
+
pageStem: 'skeleton',
|
|
2103
|
+
};
|
|
2104
|
+
const inputs = result.images.map((img) => ({
|
|
2105
|
+
region: img.region,
|
|
2106
|
+
absPath: img.absPath,
|
|
2107
|
+
width: img.width,
|
|
2108
|
+
height: img.height,
|
|
2109
|
+
}));
|
|
2110
|
+
const packed = packAtlas(inputs, packOpts);
|
|
2111
|
+
atlasText = packed.atlasText;
|
|
2112
|
+
for (const page of packed.pages) {
|
|
2113
|
+
page.plate.writePng(join(opts.outDir, page.name));
|
|
2114
|
+
console.log(
|
|
2115
|
+
` .. pack: ${page.name} ${page.width}x${page.height}, ` +
|
|
2116
|
+
`${packed.placements.filter((p) => packed.pages[p.page].name === page.name).length} region(s), ` +
|
|
2117
|
+
`${(page.occupancy * 100).toFixed(1)}% covered, padding ${packed.padding}` +
|
|
2118
|
+
(packOpts.pageEdges === 'free' ? ', page edges free' : ''),
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
for (const place of packed.placements) {
|
|
2122
|
+
console.log(
|
|
2123
|
+
` .. ${place.region.padEnd(24)} ${place.width}x${place.height} -> ` +
|
|
2124
|
+
`${packed.pages[place.page].name} @ ${place.x},${place.y}`,
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
// The pages are on disk now, so the packed pair can be gated as an artifact
|
|
2128
|
+
// rather than trusted as a construction: A17 stats every page, A06 reads its
|
|
2129
|
+
// IHDR back, A07 re-reads the text shape, A08 re-joins every attachment onto
|
|
2130
|
+
// a region, and A18 compares a second independent compile+pack. Two gates on
|
|
2131
|
+
// one build is the cost of shipping a second atlas shape.
|
|
2132
|
+
console.log(' .. validate (packed atlas, pages on disk)');
|
|
2133
|
+
const packAgain = packAtlas(
|
|
2134
|
+
compile(opts).images.map((img) => ({
|
|
2135
|
+
region: img.region,
|
|
2136
|
+
absPath: img.absPath,
|
|
2137
|
+
width: img.width,
|
|
2138
|
+
height: img.height,
|
|
2139
|
+
})),
|
|
2140
|
+
packOpts,
|
|
2141
|
+
);
|
|
2142
|
+
// The document written is spelled from the packed atlas, and this gate's A18 compares it with a
|
|
2143
|
+
// second compile's spelled from the second, independent pack (issue #1016).
|
|
2144
|
+
modelText = modelDocument(result.model, result.skeletonText, atlasText);
|
|
2145
|
+
const packFailures = runGate(gate, result, modelText, opts, profile, (text) => text, { text: atlasText, again: packAgain.atlasText });
|
|
2146
|
+
if (packFailures > 0) {
|
|
2147
|
+
console.error(
|
|
2148
|
+
`rigc: ${packFailures} assertion(s) failed on the PACKED atlas — the pages were written to ` +
|
|
2149
|
+
`${opts.outDir}, the skeleton/atlas pair was not`,
|
|
2150
|
+
);
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
writeFileSync(join(opts.outDir, 'skeleton.json'), result.skeletonText);
|
|
2156
|
+
writeFileSync(join(opts.outDir, 'skeleton.atlas'), atlasText);
|
|
2157
|
+
// rigc's own record of the compiled rig (`rigc-compiled/3`, issue #922;
|
|
2158
|
+
// its `pages` the atlas just written, issue #1016), written with the pair
|
|
2159
|
+
// and only after the same gate — under `--pack`, the packed one. rigc's own posing core
|
|
2160
|
+
// reads it (`readModel`, `src/core/index.ts`, issue #380's step 2).
|
|
2161
|
+
writeFileSync(join(opts.outDir, MODEL_DOCUMENT_FILE), modelText);
|
|
2162
|
+
console.log(`rigc: wrote ${join(opts.outDir, 'skeleton.json')}`);
|
|
2163
|
+
console.log(`rigc: wrote ${join(opts.outDir, 'skeleton.atlas')}`);
|
|
2164
|
+
console.log(`rigc: wrote ${join(opts.outDir, MODEL_DOCUMENT_FILE)}`);
|
|
2165
|
+
// The next command is part of the message (issue #837). A green build is the
|
|
2166
|
+
// moment somebody wants to see what came out, and the one page rigc writes
|
|
2167
|
+
// for that is `preview` of exactly this directory — so the line names it,
|
|
2168
|
+
// with the path already resolved. Printed only here: a red build wrote
|
|
2169
|
+
// nothing, so there is nothing to look at and no line.
|
|
2170
|
+
console.log(gate.look(opts.outDir));
|
|
2171
|
+
}
|
|
2172
|
+
|
|
1675
2173
|
/**
|
|
1676
2174
|
* One `DROP` line, written once because two outcomes print it.
|
|
1677
2175
|
*
|
|
@@ -1729,9 +2227,19 @@ export interface CliEntry {
|
|
|
1729
2227
|
refusals?: readonly CliRefusal[];
|
|
1730
2228
|
}
|
|
1731
2229
|
|
|
1732
|
-
/**
|
|
2230
|
+
/**
|
|
2231
|
+
* The commands an entry runs, as its page documents them: every documented
|
|
2232
|
+
* one, or — linking nothing of the runtime — those whose `runtime` is `false`
|
|
2233
|
+
* and those with a body of their own there (`runtime.core`, issue #1060),
|
|
2234
|
+
* documented by that body.
|
|
2235
|
+
*/
|
|
1733
2236
|
export function entryCommands(linksRuntime: boolean): CommandDoc[] {
|
|
1734
|
-
|
|
2237
|
+
if (linksRuntime) return COMMANDS;
|
|
2238
|
+
return COMMANDS.flatMap((doc): CommandDoc[] => {
|
|
2239
|
+
if (doc.runtime === false) return [doc];
|
|
2240
|
+
const core = doc.runtime.core;
|
|
2241
|
+
return core === undefined ? [] : [{ ...doc, usage: core.usage, flags: core.flags ?? doc.flags, notes: core.notes }];
|
|
2242
|
+
});
|
|
1735
2243
|
}
|
|
1736
2244
|
|
|
1737
2245
|
/**
|
|
@@ -1785,7 +2293,7 @@ export function runCli(entry: CliEntry, argv: readonly string[]): void {
|
|
|
1785
2293
|
|
|
1786
2294
|
const { flags, lists, positional } = parseArgs([...rest], REPEATABLE_FLAGS[command]);
|
|
1787
2295
|
if (flags.help !== undefined) {
|
|
1788
|
-
console.log(commandHelp(command));
|
|
2296
|
+
console.log(commandHelp(command, docs));
|
|
1789
2297
|
process.exit(0);
|
|
1790
2298
|
}
|
|
1791
2299
|
entry.runs[command]({ flags, lists, positional });
|