@rig-c/core 2.59.0 → 2.61.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 +6 -2
- package/docs/AUTHORING.md +187 -3
- package/docs/INGEST.md +1 -0
- package/docs/MOTION.md +1 -0
- package/package.json +1 -1
- package/src/cli/core_commands.ts +178 -7
- package/src/cli/shared.ts +80 -6
- package/src/cli/spine_commands.ts +1 -1
- package/src/compile.ts +59 -5
- package/src/passthrough.ts +3 -3
- package/src/render.ts +16 -0
- package/src/render_core.ts +23 -5
- package/src/render_shared.ts +33 -0
- package/src/seam.ts +576 -0
- package/src/stop.ts +33 -3
package/src/cli/core_commands.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { FOLDS_FILE, foldLine, foldsFileOf, foldsOf, foldsText, setupFaultLine }
|
|
|
38
38
|
import { AngleRangeError, angleRangeLine, type AngleRangeReading, angleRangesOf, type BonePair, readBonePairs } from '../anglerange.ts';
|
|
39
39
|
import { outsideLine, outsideViewport, pinnedRecord, readViewportBox, viewportOfFramesFile, ViewportError } from '../viewportpin.ts';
|
|
40
40
|
import { PASS_THROUGH_FILE, passFrameLine, type PassPair, passSummaryLine, passThroughFileOf, passThroughOf, passThroughText, PassThroughError, readPassPairs } from '../passthrough.ts';
|
|
41
|
+
import { readSeamPairs, SEAM_FILE, seamFileOf, seamLine, SeamError, type SeamPair, seamsOf, seamText } from '../seam.ts';
|
|
41
42
|
import { environmentCollisions, environmentFrame, EnvironmentError, environmentResponse, readEnvironmentSchedule, type EnvironmentStep } from '../environment.ts';
|
|
42
43
|
import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
|
|
43
44
|
import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, frameSetDir, type FrameSet, type FramesSidecar, framingViewport, GEOMETRY_FILE, geometryFileOf, geometryText, loadCandidate, POSER_NAMES, type PoserName, PROTOCOL_FPS, refuseUnchosen, renderFrame, sampleAll, sampleAnimation, SETUP_POSE_DIR, SHEET_FILE, SHEET_TILE, sidecarViewport, type SkeletonFacts, type SlotSubset, SlotSubsetError, throughPoser, type Viewport, viewportFor } from '../render_shared.ts';
|
|
@@ -45,9 +46,10 @@ import { type CompileResult } from '../types.ts';
|
|
|
45
46
|
import { attachmentRegionJoins } from '../region_joins.ts';
|
|
46
47
|
import { cmdRepack } from './repack.ts';
|
|
47
48
|
import { GUIDE_FILE, GUIDE_RULE_TABLES_ELSEWHERE, guideAnswer, refusalLine } from '../guide.ts';
|
|
48
|
-
import { ATLAS_ABSENT, COMMANDS, type CommandRun, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, GuideError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
49
|
+
import { ATLAS_ABSENT, COMMANDS, type CommandRun, DelegateError, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, GuideError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
49
50
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
50
51
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
52
|
+
import { spawnSync } from 'node:child_process';
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> = {
|
|
@@ -561,6 +563,23 @@ function readPassThroughFlag(raw: string | undefined, facts: SkeletonFacts): Pas
|
|
|
561
563
|
}
|
|
562
564
|
}
|
|
563
565
|
|
|
566
|
+
/**
|
|
567
|
+
* `--seam <a>:<b>[,…]` read against the skeleton's slots (issue #1456,
|
|
568
|
+
* `../seam.ts`), or refused as a usage error before anything is posed: a
|
|
569
|
+
* malformed list, an empty name, a slot the skeleton does not have, a slot paired
|
|
570
|
+
* with itself, a pair named twice in either order — each by its entry.
|
|
571
|
+
* `undefined` without the flag.
|
|
572
|
+
*/
|
|
573
|
+
function readSeamFlag(raw: string | undefined, facts: SkeletonFacts): SeamPair[] | undefined {
|
|
574
|
+
if (raw === undefined) return undefined;
|
|
575
|
+
try {
|
|
576
|
+
return readSeamPairs(raw, facts.slots.map((slot) => slot.name));
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if (err instanceof SeamError) throw new UsageError(err.message);
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
564
583
|
/**
|
|
565
584
|
* `--angle-range`'s pairs, read against the skeleton's bones (issue #1414,
|
|
566
585
|
* `../anglerange.ts`), or refused as a usage error before anything is posed: a
|
|
@@ -685,6 +704,21 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
685
704
|
}
|
|
686
705
|
}
|
|
687
706
|
|
|
707
|
+
// The seam reading (issue #1456) is its own flag for --folds' reason, and refused beside a subset for --pass-through's:
|
|
708
|
+
// it reads the two slots as the whole rig draws them.
|
|
709
|
+
const seam = flags.seam;
|
|
710
|
+
if (seam !== undefined) {
|
|
711
|
+
for (const flag of ['slot', 'hide'] as const) {
|
|
712
|
+
if (flags[flag] !== undefined) {
|
|
713
|
+
throw new UsageError(
|
|
714
|
+
`--seam takes no --${flag}: it reads the two slots as the whole rig draws them, which a subset of the drawn slots ` +
|
|
715
|
+
`would change. Run \`rigc render --seam\` on the whole rig for the reading, and --${flag} ${flags[flag]} ` +
|
|
716
|
+
'without --seam for the pictures — both land on the same grid',
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
688
722
|
console.log('rigc render');
|
|
689
723
|
console.log(` .. skeleton ${skeletonPath}`);
|
|
690
724
|
console.log(` .. atlas ${atlasPath}${atlasText === null ? ` — not there (${ATLAS_ABSENT})` : ''}`);
|
|
@@ -702,7 +736,10 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
702
736
|
const only = readAnimationFlag(flags, [...facts.animations]);
|
|
703
737
|
const skin = readSkinFlag(flags, [...facts.skins]);
|
|
704
738
|
const subset = readSlotSubsetFlags(flags, facts, skin);
|
|
739
|
+
// The physics constraints the skeleton declares, off the Spine file both posers read (issue #1467).
|
|
740
|
+
const physicsDeclared = environmentResponse(skeletonText).declared;
|
|
705
741
|
const passPairs = readPassThroughFlag(passThrough, facts);
|
|
742
|
+
const seamPairs = readSeamFlag(seam, facts);
|
|
706
743
|
// The angle reading (issue #1414) reads bones, which a subset of the drawn slots does not change, so it takes --slot/--hide.
|
|
707
744
|
const anglePairs = readAngleRangeFlag(flags['angle-range'], facts);
|
|
708
745
|
// One object, so the framing and the frames cannot be posed under two
|
|
@@ -774,8 +811,15 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
774
811
|
// the export's grid is this frame set's by construction.
|
|
775
812
|
// The bones ride along for --geometry and --angle-range, the whole attachments for --geometry and the two readings off them.
|
|
776
813
|
const wantsBones = geometry || anglePairs !== undefined;
|
|
777
|
-
const wantsGeometry = geometry || folds || passPairs !== undefined;
|
|
778
|
-
|
|
814
|
+
const wantsGeometry = geometry || folds || passPairs !== undefined || seamPairs !== undefined;
|
|
815
|
+
// The physics constraints each frame's step stepped ride along whenever the rig declares one (issue #1467): the
|
|
816
|
+
// `physics:` line and frames.json's `physics` field are read off them.
|
|
817
|
+
const sampling = {
|
|
818
|
+
...pose,
|
|
819
|
+
...(wantsBones ? { bones: true } : {}),
|
|
820
|
+
...(wantsGeometry ? { geometry: true } : {}),
|
|
821
|
+
...(physicsDeclared.length > 0 ? { physics: true } : {}),
|
|
822
|
+
};
|
|
779
823
|
const sampled: Map<string, Frame[]> =
|
|
780
824
|
only === undefined
|
|
781
825
|
? sampleAll(poser, fps, sampling)
|
|
@@ -828,6 +872,34 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
828
872
|
}
|
|
829
873
|
}
|
|
830
874
|
}
|
|
875
|
+
// The seam reading, off the very pieces the frames are drawn from — one per set, every pair the flag names. A pair
|
|
876
|
+
// whose slot draws on no frame of any set drawn is refused, as --pass-through refuses it, rather than read as a SKIP.
|
|
877
|
+
const seamReadings = new Map<string, ReturnType<typeof seamFileOf>>();
|
|
878
|
+
if (seamPairs !== undefined) {
|
|
879
|
+
const draws = new Set<string>();
|
|
880
|
+
for (const frames of sampled.values()) for (const frame of frames) for (const piece of frame.pieces) draws.add(piece.slot);
|
|
881
|
+
for (const pair of seamPairs) {
|
|
882
|
+
for (const slot of [pair.a, pair.b]) {
|
|
883
|
+
if (!draws.has(slot)) {
|
|
884
|
+
throw new UsageError(
|
|
885
|
+
`--seam: slot ${JSON.stringify(slot)} draws on no frame of [${[...sampled.keys()].join(', ')}]${skin === undefined ? '' : ` under skin ${JSON.stringify(skin)}`} ` +
|
|
886
|
+
'— it shows no region or mesh there, so it has no drawing to read a seam of',
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
for (const [name, frames] of sampled) {
|
|
892
|
+
const animation = name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name;
|
|
893
|
+
const shown = frames.map((frame) => frame.attachments ?? []);
|
|
894
|
+
const read = frames.map((frame) => ({ index: frame.index, time: frame.time, pieces: frame.pieces, attachments: frame.attachments ?? [] }));
|
|
895
|
+
try {
|
|
896
|
+
seamReadings.set(name, seamFileOf(animation, fps, skin, seamsOf(seamPairs, poser.rest(skin, shown), read, pages)));
|
|
897
|
+
} catch (err) {
|
|
898
|
+
if (err instanceof SeamError) throw new UsageError(`--seam, ${JSON.stringify(name)}: ${err.message}`);
|
|
899
|
+
throw err;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
831
903
|
// The angle reading, off the very bones the frames were posed with — one per set, every pair the flag names.
|
|
832
904
|
const angleReadings = new Map<string, AngleRangeReading[]>();
|
|
833
905
|
if (anglePairs !== undefined) {
|
|
@@ -841,9 +913,9 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
841
913
|
}
|
|
842
914
|
}
|
|
843
915
|
}
|
|
844
|
-
return { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, angleReadings };
|
|
916
|
+
return { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings };
|
|
845
917
|
});
|
|
846
|
-
const { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, angleReadings } = posed.value;
|
|
918
|
+
const { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings } = posed.value;
|
|
847
919
|
console.log(` .. poser ${posed.note}`);
|
|
848
920
|
if (environment !== undefined) {
|
|
849
921
|
const response = environmentResponse(skeletonText);
|
|
@@ -853,6 +925,8 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
853
925
|
);
|
|
854
926
|
}
|
|
855
927
|
console.log(` .. ${viewport.width}x${viewport.height}px at ${fps} fps, ${sampled.size} set(s) -> ${outRoot}`);
|
|
928
|
+
// Whether physics ran (issue #1467): said once for a rig with nothing to step, and per set below for one with.
|
|
929
|
+
if (physicsDeclared.length === 0) console.log(` .. ${PHYSICS_NONE_LINE}`);
|
|
856
930
|
if (!facts.declaresStage) console.log(` .. ${STAGELESS_FRAMING.render}`);
|
|
857
931
|
// A given box (issue #1414): said with the box it replaced, and every slot it leaves part of out, by set and frame.
|
|
858
932
|
const outside = given === undefined ? [] : outsideViewport(sampled, viewport);
|
|
@@ -893,7 +967,10 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
893
967
|
if (foldReading !== undefined) writeFileSync(join(dir, FOLDS_FILE), foldsText(foldReading));
|
|
894
968
|
const passReading = passReadings.get(name);
|
|
895
969
|
if (passReading !== undefined) writeFileSync(join(dir, PASS_THROUGH_FILE), passThroughText(passReading));
|
|
970
|
+
const seamReading = seamReadings.get(name);
|
|
971
|
+
if (seamReading !== undefined) writeFileSync(join(dir, SEAM_FILE), seamText(seamReading));
|
|
896
972
|
const duration = frames[frames.length - 1].time;
|
|
973
|
+
const physics = physicsDeclared.length === 0 ? undefined : physicsSteppedOf(frames, fps);
|
|
897
974
|
sets.push({
|
|
898
975
|
dir: dirName,
|
|
899
976
|
animation: name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name,
|
|
@@ -902,14 +979,18 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
902
979
|
written: frames.length,
|
|
903
980
|
stride: 1,
|
|
904
981
|
duration,
|
|
982
|
+
...(physics === undefined ? {} : { physics: { stepped: physics.stepped.length, fps } }),
|
|
905
983
|
});
|
|
906
984
|
const how = frames.length === 1 ? 'a single pose' : `${duration.toFixed(3)}s`;
|
|
907
985
|
const folding = foldReading?.meshes.filter((m) => m.worst !== null).length ?? 0;
|
|
908
986
|
const foldNote = foldReading === undefined ? '' : ` + ${FOLDS_FILE} (${folding} of ${foldReading.meshes.length} mesh(es) fold)`;
|
|
909
987
|
const passing = passReading?.pairs.filter((p) => p.frames.length > 0).length ?? 0;
|
|
910
988
|
const passNote = passReading === undefined ? '' : ` + ${PASS_THROUGH_FILE} (${passing} of ${passReading.pairs.length} pair(s) pass through)`;
|
|
911
|
-
const
|
|
989
|
+
const opening = seamReading?.pairs.filter((p) => p.opens.length > 0).length ?? 0;
|
|
990
|
+
const seamNote = seamReading === undefined ? '' : ` + ${SEAM_FILE} (${opening} of ${seamReading.pairs.length} pair(s) open)`;
|
|
991
|
+
const extras = `${sheet ? ` + ${SHEET_FILE}` : ''}${geometryOut === undefined ? '' : ` + ${GEOMETRY_FILE}`}${foldNote}${passNote}${seamNote}`;
|
|
912
992
|
console.log(` .. ${name.padEnd(16)} ${frames.length} frame(s), ${how}${extras} -> ${dir}`);
|
|
993
|
+
if (physics !== undefined) console.log(` .. ${physicsLine(physics.stepped, physicsDeclared, fps, frames.length)}`);
|
|
913
994
|
for (const mesh of foldReading?.meshes ?? []) {
|
|
914
995
|
const key = `${mesh.slot}\u0000${mesh.attachment}`;
|
|
915
996
|
const fault = faultsSaid.has(key) ? null : setupFaultLine(mesh);
|
|
@@ -922,6 +1003,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
922
1003
|
console.log(` .. pass ${passSummaryLine(passReading?.animation ?? null, fps, pair)}`);
|
|
923
1004
|
for (const frame of pair.frames) console.log(` .. pass ${passFrameLine(passReading?.animation ?? null, pair, frame)}`);
|
|
924
1005
|
}
|
|
1006
|
+
for (const pair of seamReading?.pairs ?? []) console.log(` .. seam ${seamLine(seamReading?.animation ?? null, fps, pair)}`);
|
|
925
1007
|
for (const reading of angleReadings.get(name) ?? []) {
|
|
926
1008
|
console.log(` .. angle ${angleRangeLine(name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name, fps, reading)}`);
|
|
927
1009
|
}
|
|
@@ -954,6 +1036,28 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
954
1036
|
console.log(`rigc: wrote ${join(outRoot, FRAMES_SIDECAR)}`);
|
|
955
1037
|
}
|
|
956
1038
|
|
|
1039
|
+
/** `render`'s line for a rig that declares no physics constraint (issue #1467): nothing was stepped because nothing could be. */
|
|
1040
|
+
export const PHYSICS_NONE_LINE = 'physics: none in this rig — it declares no physics constraint, so no frame steps one';
|
|
1041
|
+
|
|
1042
|
+
/** The physics constraints any frame of a set stepped (issue #1467), by name in code-unit order. */
|
|
1043
|
+
export function physicsSteppedOf(frames: readonly Frame[], fps: number): { stepped: string[]; fps: number } {
|
|
1044
|
+
const names = new Set<string>();
|
|
1045
|
+
for (const frame of frames) for (const name of frame.physics ?? []) names.add(name);
|
|
1046
|
+
return { stepped: [...names].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)), fps };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* `render`'s `physics:` line for one set (issue #1467): how many constraints were stepped at the set's rate, and — when
|
|
1051
|
+
* fewer than the rig declares — the ones that took no step, by name, with why a constraint takes none.
|
|
1052
|
+
*/
|
|
1053
|
+
export function physicsLine(stepped: readonly string[], declared: readonly string[], fps: number, frames: number): string {
|
|
1054
|
+
const head = `physics: ${stepped.length} constraint(s) stepped at ${fps} fps`;
|
|
1055
|
+
const idle = declared.filter((name) => !stepped.includes(name));
|
|
1056
|
+
if (idle.length === 0) return head;
|
|
1057
|
+
const why = frames === 1 ? 'a single pose takes no step' : 'inactive, or a mix of 0, on every frame';
|
|
1058
|
+
return `${head} — ${idle.length} of the ${declared.length} declared took none: ${idle.map((n) => JSON.stringify(n)).join(', ')} (${why})`;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
957
1061
|
/** Where `inspect` writes when `--out` is not given, and the name it takes inside a directory `--out` names. */
|
|
958
1062
|
export const DEFAULT_INSPECT_OUT = 'inspect.png';
|
|
959
1063
|
|
|
@@ -1283,7 +1387,7 @@ export function cmdExplain(flags: Record<string, string>): void {
|
|
|
1283
1387
|
const leverFold = flags['lever-fold'] === undefined ? null : Number(flags['lever-fold']);
|
|
1284
1388
|
const leverWhy = leverFold === null ? null : leverFractionWhy(leverFold);
|
|
1285
1389
|
if (leverWhy !== null) throw new UsageError(`--lever-fold ${JSON.stringify(flags['lever-fold'])}: ${leverWhy}`);
|
|
1286
|
-
const { label, opts } = resolveCut(flags);
|
|
1390
|
+
const { label, opts } = resolveCut(flags, 'explain');
|
|
1287
1391
|
console.log(`rigc explain ${label}`);
|
|
1288
1392
|
// See the identical pair of lines in `cmdBuild` for why both paths are named
|
|
1289
1393
|
// here rather than only the one the header's `label` happens to carry.
|
|
@@ -2103,6 +2207,72 @@ export const MODEL_AND_TEXT_GATE: BuildGate = {
|
|
|
2103
2207
|
look: (outDir) => `rigc: look at it: rigc render --candidate ${outDir}`,
|
|
2104
2208
|
};
|
|
2105
2209
|
|
|
2210
|
+
/**
|
|
2211
|
+
* Where a delegated package's `package.json` resolves, or `null` (issue #1457).
|
|
2212
|
+
*
|
|
2213
|
+
* ⚖️ Node's `node_modules` lookup — `<dir>/node_modules/<package>` for the
|
|
2214
|
+
* directory and then each of its ancestors, nearest first — from two places in
|
|
2215
|
+
* this order, the first answer winning:
|
|
2216
|
+
*
|
|
2217
|
+
* 1. the working directory — the project `npm i -D <package>` installed it
|
|
2218
|
+
* into, which is what the refusal tells a caller to run, so a project's
|
|
2219
|
+
* own install is the one its command line reaches;
|
|
2220
|
+
* 2. this package's root — where an install of rig-c sits beside it in the
|
|
2221
|
+
* same `node_modules` and the command was run from somewhere else.
|
|
2222
|
+
*
|
|
2223
|
+
* 🚨 Walked here rather than asked of the runtime's resolver: Bun's resolves a
|
|
2224
|
+
* package it cannot find, from a directory with no `node_modules` above it, by
|
|
2225
|
+
* installing it on demand into its global cache — measured, a directory with
|
|
2226
|
+
* none resolved `@rig-c/review` there — so "not installed" would never be
|
|
2227
|
+
* refused, and asking would reach the network. `NODE_PATH` and the global
|
|
2228
|
+
* folders are not read: what the refusal tells a caller to run never puts a
|
|
2229
|
+
* package there. The bin is read off the `package.json` found, not resolved.
|
|
2230
|
+
*/
|
|
2231
|
+
function resolveDelegate(name: string): string | null {
|
|
2232
|
+
for (const from of [process.cwd(), PACKAGE_ROOT]) {
|
|
2233
|
+
for (let dir = resolve(from); ; dir = dirname(dir)) {
|
|
2234
|
+
const manifest = join(dir, 'node_modules', name, 'package.json');
|
|
2235
|
+
if (existsSync(manifest)) return manifest;
|
|
2236
|
+
if (dirname(dir) === dir) break;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return null;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
/**
|
|
2243
|
+
* A delegating command's body (`CommandDoc.delegate`, issue #1457): resolve the
|
|
2244
|
+
* package's bin and run it under the Bun running this — the bin is a Bun script
|
|
2245
|
+
* — with every argument untouched and stdio inherited, then exit with its code,
|
|
2246
|
+
* or die by its signal the way `bin/rigc.cjs` does, so a caller reads what it
|
|
2247
|
+
* would have read running the bin itself. Not installed, it is refused by name
|
|
2248
|
+
* with the one command that installs it, exit 2, and nothing else is printed.
|
|
2249
|
+
*/
|
|
2250
|
+
function cmdDelegate(command: string, raw: readonly string[]): void {
|
|
2251
|
+
const delegate = COMMANDS.find((c) => c.name === command)?.delegate;
|
|
2252
|
+
if (delegate === undefined) throw new Error(`internal: ${command} names no delegate`);
|
|
2253
|
+
const manifest = resolveDelegate(delegate.package);
|
|
2254
|
+
if (manifest === null) throw new DelegateError(`${delegate.package} is not installed — npm i -D ${delegate.package}`, 2);
|
|
2255
|
+
const bins = (readJsonFile(manifest) as { bin?: unknown }).bin;
|
|
2256
|
+
const relativeBin = typeof bins === 'object' && bins !== null ? (bins as Record<string, unknown>)[delegate.bin] : undefined;
|
|
2257
|
+
const bin = typeof relativeBin === 'string' ? join(dirname(manifest), relativeBin) : null;
|
|
2258
|
+
if (bin === null || !existsSync(bin)) {
|
|
2259
|
+
throw new DelegateError(
|
|
2260
|
+
`${delegate.package} at ${dirname(manifest)} ${bin === null ? `declares no "${delegate.bin}" bin` : `declares the "${delegate.bin}" bin ${String(relativeBin)}, which is not there`} — npm i -D ${delegate.package}`,
|
|
2261
|
+
2,
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
const ran = spawnSync(process.execPath, [bin, ...raw], { stdio: 'inherit' });
|
|
2265
|
+
// ⚠️ The exit and the signal are read before `error`: Bun 1.2.0 — the declared minimum — sets `error` on every
|
|
2266
|
+
// child that exits non-zero, so reading it first refused review's own exit 7 as "could not run". `error` is a
|
|
2267
|
+
// launch that failed only where the child left neither a status nor a signal.
|
|
2268
|
+
if (typeof ran.status === 'number') process.exit(ran.status);
|
|
2269
|
+
if (ran.signal !== null) {
|
|
2270
|
+
process.kill(process.pid, ran.signal);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
throw new DelegateError(`could not run ${bin}: ${ran.error?.message ?? 'it left no exit status and no signal'}`, 1);
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2106
2276
|
/**
|
|
2107
2277
|
* split — one authored mesh's pieces, proposed as one mesh each (issue #1414).
|
|
2108
2278
|
*
|
|
@@ -2182,4 +2352,5 @@ export const CORE_COMMAND_RUNS: Readonly<Record<string, CommandRun>> = {
|
|
|
2182
2352
|
chainfit: ({ flags }) => cmdChainFit(flags),
|
|
2183
2353
|
guide: ({ flags, positional }) => cmdGuide(flags, positional),
|
|
2184
2354
|
skills: ({ flags, positional }) => cmdSkills(flags, positional),
|
|
2355
|
+
review: ({ raw }) => cmdDelegate('review', raw),
|
|
2185
2356
|
};
|
package/src/cli/shared.ts
CHANGED
|
@@ -38,7 +38,8 @@ import { checkAgainstFrames, type CheckOptions, CheckPlates, type CheckReport }
|
|
|
38
38
|
import { compile, CompileError, droppedStateReason, headerBoundsOf, type CompileOptions } from '../compile.ts';
|
|
39
39
|
import { depthStepLevels, type FoldLimit, type TurnCeiling } from '../depth.ts';
|
|
40
40
|
import { FOLDS_FILE } from '../folds.ts';
|
|
41
|
-
import { PASS_THROUGH_FILE, PASS_THROUGH_RUN } from '../passthrough.ts';
|
|
41
|
+
import { DRAWN_ALPHA, PASS_THROUGH_FILE, PASS_THROUGH_RUN } from '../passthrough.ts';
|
|
42
|
+
import { SEAM_FILE } from '../seam.ts';
|
|
42
43
|
import { readViewportBox, ViewportError } from '../viewportpin.ts';
|
|
43
44
|
import { parseJsonWithPosition } from '../json-position.ts';
|
|
44
45
|
import { RUNG_IDS } from '../ladder.ts';
|
|
@@ -93,6 +94,19 @@ export type CutTable = Record<string, CutEntry>;
|
|
|
93
94
|
|
|
94
95
|
export class UsageError extends Error {}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* A usage error that is one required flag missing from a spelled-out cut —
|
|
99
|
+
* printed as its one line and exit 2, without the usage block under it (issue
|
|
100
|
+
* #1466). The catalogue under `--out is required` buried the only line that
|
|
101
|
+
* said what to add, which is `PoseError`'s reason for printing alone. Still a
|
|
102
|
+
* `UsageError`, so every reader that asks whether an invocation was refused
|
|
103
|
+
* reads it as one.
|
|
104
|
+
*/
|
|
105
|
+
export class MissingFlagError extends UsageError {}
|
|
106
|
+
|
|
107
|
+
/** The `<value>` each flag of a spelled-out cut takes, for the line that names a missing one. */
|
|
108
|
+
const CUT_FLAG_VALUES = { rig: '<rig.json>', motion: '<motion.json>', out: '<dir>' } as const;
|
|
109
|
+
|
|
96
110
|
/**
|
|
97
111
|
* `explain` refusing a pair it cannot pose — a usage error in kind, printed
|
|
98
112
|
* without the usage block for `PoseError`'s and `IngestError`'s reason: the
|
|
@@ -263,7 +277,7 @@ function entryToOptions(dir: string, name: string, entry: CutEntry): CompileOpti
|
|
|
263
277
|
* Resolve the cut a command was pointed at, either spelled out on the command
|
|
264
278
|
* line or looked up by name in a cuts.json.
|
|
265
279
|
*/
|
|
266
|
-
export function resolveCut(flags: Record<string, string
|
|
280
|
+
export function resolveCut(flags: Record<string, string>, command: 'build' | 'explain' | 'validate'): { label: string; opts: CompileOptions } {
|
|
267
281
|
const explicit =
|
|
268
282
|
flags.rig !== undefined || flags.manifest !== undefined || flags.motion !== undefined || flags.out !== undefined;
|
|
269
283
|
if (explicit) {
|
|
@@ -271,7 +285,7 @@ export function resolveCut(flags: Record<string, string>): { label: string; opts
|
|
|
271
285
|
throw new UsageError('--rig/--motion/--out and --cut/--cuts are two ways to say the same thing; pick one');
|
|
272
286
|
}
|
|
273
287
|
for (const key of ['rig', 'motion', 'out'] as const) {
|
|
274
|
-
if (flags[key] === undefined) throw new
|
|
288
|
+
if (flags[key] === undefined) throw new MissingFlagError(`${command} needs --${key} ${CUT_FLAG_VALUES[key]} (or --cut <name> --cuts <cuts.json>)`);
|
|
275
289
|
}
|
|
276
290
|
const opts: CompileOptions = {
|
|
277
291
|
rigPath: resolve(flags.rig),
|
|
@@ -955,6 +969,21 @@ export class SkillsInstallError extends Error {}
|
|
|
955
969
|
/** `guide` asked for a rule or a section the guide does not have (issue #1397): a refusal by `GUIDE_RULE_KNOWN` or `GUIDE_SECTION_KNOWN`, whose message is the whole line. Exit 1. */
|
|
956
970
|
export class GuideError extends Error {}
|
|
957
971
|
|
|
972
|
+
/**
|
|
973
|
+
* A delegating command (`CommandDoc.delegate`, issue #1457) that could not hand
|
|
974
|
+
* over: the package it delegates to does not resolve, or resolves without the
|
|
975
|
+
* bin it names. Nothing ran, so the exit is `status` (2) and the message is the
|
|
976
|
+
* whole of what is printed after `rigc <command>: `.
|
|
977
|
+
*/
|
|
978
|
+
export class DelegateError extends Error {
|
|
979
|
+
constructor(
|
|
980
|
+
message: string,
|
|
981
|
+
readonly status: number,
|
|
982
|
+
) {
|
|
983
|
+
super(message);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
958
987
|
// ---------------------------------------------------------------------------
|
|
959
988
|
// usage / per-command help
|
|
960
989
|
// ---------------------------------------------------------------------------
|
|
@@ -1023,6 +1052,13 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1023
1052
|
'world units, one texel of the coarser drawing to a step — each with the depth and the world box. One line per pair, and one per frame that passes through. ' +
|
|
1024
1053
|
'The order is the declaration: the first name must draw over the second for a frame to be read. A reading, not a gate. ' +
|
|
1025
1054
|
'An unknown slot, a slot that draws on no frame, a malformed list and a pair named twice are refused by name. Not with --slot/--hide',
|
|
1055
|
+
seam:
|
|
1056
|
+
`also read, for each <slot>:<slot> pair, where the two drawings stop meeting, and write ${SEAM_FILE} into each frame directory: ` +
|
|
1057
|
+
'per pair, the frames with a gap — background inside the hull of the band the two drawings share at the setup pose, carried to the frame by both — ' +
|
|
1058
|
+
'and the frames with a ghost — the band of one drawing showing where, from its partner\'s point of view, nothing was drawn at the setup pose — each with ' +
|
|
1059
|
+
`its cell count, area and world box. Cells are one texel of the coarser drawing, drawn at ${DRAWN_ALPHA} of 255 or more. One line per pair; a pair whose ` +
|
|
1060
|
+
'drawings share no cell at the setup pose is a SKIP. A reading, not a gate: the exit status never moves. An unknown slot, a slot with itself, ' +
|
|
1061
|
+
'a slot that draws on no frame, a malformed list and a pair named twice in either order are refused by name. Not with --slot/--hide',
|
|
1026
1062
|
'angle-range':
|
|
1027
1063
|
'also read, for each <bone>:<relative-to> pair, the range of the first bone\'s world direction against the second\'s over each set\'s ' +
|
|
1028
1064
|
'frames — its rotationX less the other\'s, followed from frame to frame so a sweep past ±180° reads as itself — and print one line per ' +
|
|
@@ -1234,6 +1270,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1234
1270
|
poser: 'core|spine',
|
|
1235
1271
|
environment: '<schedule.json>',
|
|
1236
1272
|
'pass-through': '<cloth>:<leg>[,<cloth>:<leg>…]',
|
|
1273
|
+
seam: '<slot>:<slot>[,<slot>:<slot>…]',
|
|
1237
1274
|
'angle-range': '<bone>:<relative-to>[,…]',
|
|
1238
1275
|
'viewport-from': '<frames.json|dir>',
|
|
1239
1276
|
reference: '<dir|skeleton.json>',
|
|
@@ -1365,6 +1402,15 @@ interface CommandDoc {
|
|
|
1365
1402
|
* `diff`) can be moved to another side in one place.
|
|
1366
1403
|
*/
|
|
1367
1404
|
spineFormat: boolean;
|
|
1405
|
+
/**
|
|
1406
|
+
* A command whose body is another package's bin (issue #1457): the dispatch
|
|
1407
|
+
* hands it every argument after the name untouched — nothing is parsed as a
|
|
1408
|
+
* flag, `--help` included, so its page is the delegate's own — and the
|
|
1409
|
+
* delegate's exit code is the command's. `flags` is empty for that reason:
|
|
1410
|
+
* rigc documents no flag of a program it does not own. A control that reads
|
|
1411
|
+
* a command's flag table off `--help` reads it off the commands without this.
|
|
1412
|
+
*/
|
|
1413
|
+
delegate?: { package: string; bin: string };
|
|
1368
1414
|
}
|
|
1369
1415
|
|
|
1370
1416
|
export const COMMANDS: CommandDoc[] = [
|
|
@@ -1686,10 +1732,11 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1686
1732
|
'rigc render … --slot <name[,name…]> | --hide <name[,name…]> (a subset of the slots, on the whole rig\'s grid)',
|
|
1687
1733
|
'rigc render … --environment <schedule.json> (the wind and gravity the physics answers, in time)',
|
|
1688
1734
|
'rigc render … --pass-through <cloth>:<leg>[,…] (the frames where a cloth drawn over a leg passes through it)',
|
|
1735
|
+
'rigc render … --seam <slot>:<slot>[,…] (the frames where two slots that meet open a gap or show a ghost)',
|
|
1689
1736
|
'rigc render … --angle-range <bone>:<relative-to>[,…] (how far a bone turns against another, per animation)',
|
|
1690
1737
|
'rigc render … --viewport <x,y,w,h> | --viewport-from <frames.json> (a box given, not fitted: two renders on one pixel grid)',
|
|
1691
1738
|
],
|
|
1692
|
-
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'viewport', 'viewport-from', 'geometry', 'folds', 'pass-through', 'angle-range', 'environment', 'poser', 'out'],
|
|
1739
|
+
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'viewport', 'viewport-from', 'geometry', 'folds', 'pass-through', 'seam', 'angle-range', 'environment', 'poser', 'out'],
|
|
1693
1740
|
overrides: {
|
|
1694
1741
|
out: { value: '<dir>', meaning: 'directory to write the frame series into (default `render/`)' },
|
|
1695
1742
|
fps: {
|
|
@@ -1891,6 +1938,16 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1891
1938
|
'"Install it into your agent").',
|
|
1892
1939
|
],
|
|
1893
1940
|
},
|
|
1941
|
+
{
|
|
1942
|
+
name: 'review',
|
|
1943
|
+
runtime: false,
|
|
1944
|
+
spineFormat: false,
|
|
1945
|
+
delegate: { package: '@rig-c/review', bin: 'rig-review' },
|
|
1946
|
+
usage: [
|
|
1947
|
+
"rigc review <dir> [...] (@rig-c/review's rig-review, installed beside: every argument handed over untouched, its exit code returned — `rigc review open <dir>` prints a loopback URL to give a person, `rigc review --help` is rig-review's own usage)",
|
|
1948
|
+
],
|
|
1949
|
+
flags: [],
|
|
1950
|
+
},
|
|
1894
1951
|
];
|
|
1895
1952
|
|
|
1896
1953
|
/** Every command's name, in the order the usage lists them — what the full entry dispatches over. */
|
|
@@ -2247,7 +2304,7 @@ function pageRectangle(region: AtlasRegion): string {
|
|
|
2247
2304
|
* alone (`./core_commands.ts`). Moved here unchanged from `cmdBuild`.
|
|
2248
2305
|
*/
|
|
2249
2306
|
export function readBuildInvocation(flags: Record<string, string>): { label: string; opts: CompileOptions; profile: AssertionProfile; packing: boolean } {
|
|
2250
|
-
const { label, opts } = resolveCut(flags);
|
|
2307
|
+
const { label, opts } = resolveCut(flags, 'build');
|
|
2251
2308
|
const profile = readProfile(flags);
|
|
2252
2309
|
const packing = flags.pack !== undefined;
|
|
2253
2310
|
// Two combinations are refused rather than silently resolved, because in each
|
|
@@ -2731,6 +2788,8 @@ export interface CommandArgs {
|
|
|
2731
2788
|
flags: Record<string, string>;
|
|
2732
2789
|
lists: Record<string, string[]>;
|
|
2733
2790
|
positional: string[];
|
|
2791
|
+
/** Every argument after the command's name, as given — what a delegating command (`CommandDoc.delegate`) hands over. */
|
|
2792
|
+
raw: readonly string[];
|
|
2734
2793
|
}
|
|
2735
2794
|
|
|
2736
2795
|
/** One command's body, as an entry registers it. */
|
|
@@ -2931,13 +2990,18 @@ export function runCli(entry: CliEntry, argv: readonly string[]): void {
|
|
|
2931
2990
|
if (elsewhere !== undefined) throw commandNeedsRuntime(elsewhere, known, rest);
|
|
2932
2991
|
throw new UsageError(`unknown command: ${command}`);
|
|
2933
2992
|
}
|
|
2993
|
+
// A delegating command reads nothing here: not a flag, not `--help` (issue #1457).
|
|
2994
|
+
if (docs.find((doc) => doc.name === command)?.delegate !== undefined) {
|
|
2995
|
+
entry.runs[command]({ flags: {}, lists: {}, positional: [], raw: rest });
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2934
2998
|
|
|
2935
2999
|
const { flags, lists, positional } = parseArgs([...rest], REPEATABLE_FLAGS[command]);
|
|
2936
3000
|
if (flags.help !== undefined) {
|
|
2937
3001
|
console.log(commandHelp(command, docs));
|
|
2938
3002
|
process.exit(0);
|
|
2939
3003
|
}
|
|
2940
|
-
entry.runs[command]({ flags, lists, positional });
|
|
3004
|
+
entry.runs[command]({ flags, lists, positional, raw: rest });
|
|
2941
3005
|
} catch (err) {
|
|
2942
3006
|
refuse(err, command, USAGE, entry);
|
|
2943
3007
|
}
|
|
@@ -2950,6 +3014,10 @@ export function runCli(entry: CliEntry, argv: readonly string[]): void {
|
|
|
2950
3014
|
* (`CliEntry.refusals`). Anything else is not a refusal and is thrown on.
|
|
2951
3015
|
*/
|
|
2952
3016
|
function refuse(err: unknown, command: string | undefined, USAGE: string, entry: CliEntry): never {
|
|
3017
|
+
if (err instanceof MissingFlagError) {
|
|
3018
|
+
console.error(`rigc: ${err.message}`);
|
|
3019
|
+
process.exit(2);
|
|
3020
|
+
}
|
|
2953
3021
|
if (err instanceof UsageError) {
|
|
2954
3022
|
console.error(`rigc: ${err.message}\n\n${USAGE}`);
|
|
2955
3023
|
process.exit(2);
|
|
@@ -3107,5 +3175,11 @@ function refuse(err: unknown, command: string | undefined, USAGE: string, entry:
|
|
|
3107
3175
|
console.error(err.message);
|
|
3108
3176
|
process.exit(1);
|
|
3109
3177
|
}
|
|
3178
|
+
// A delegate that could not be handed the run (issue #1457): one line naming the package and what to run, nothing else —
|
|
3179
|
+
// the usage under it would bury the one command that fixes it.
|
|
3180
|
+
if (err instanceof DelegateError) {
|
|
3181
|
+
console.error(`rigc ${command}: ${err.message}`);
|
|
3182
|
+
process.exit(err.status);
|
|
3183
|
+
}
|
|
3110
3184
|
throw err;
|
|
3111
3185
|
}
|
|
@@ -76,7 +76,7 @@ export function cmdValidate(flags: Record<string, string>, positional: string[])
|
|
|
76
76
|
// a directory alone cannot supply — and the report says which it had.
|
|
77
77
|
const named = flags.cut !== undefined || flags.rig !== undefined;
|
|
78
78
|
const profile = readProfile(flags);
|
|
79
|
-
const derivedOpts = named ? resolveCut(flags).opts : null;
|
|
79
|
+
const derivedOpts = named ? resolveCut(flags, 'validate').opts : null;
|
|
80
80
|
// Both files there before the first line, from the one statement of a build (issue #1046): a directory without them
|
|
81
81
|
// read the skeleton unguarded and died on an ENOENT and a stack.
|
|
82
82
|
const { skeletonPath, atlasPath } = spinePairOf(resolveBuild(derivedOpts ? derivedOpts.outDir : (positional[0] ?? '.'), flags.atlas));
|
package/src/compile.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* the two emits byte for byte (assertion A18).
|
|
19
19
|
*/
|
|
20
20
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
21
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
22
22
|
import { readPngHeader, readPngInfo } from './png.ts';
|
|
23
23
|
import {
|
|
24
24
|
DEPTH_TONE_IDENTITY,
|
|
@@ -1649,8 +1649,62 @@ function easingCurve(
|
|
|
1649
1649
|
// inputs
|
|
1650
1650
|
// ---------------------------------------------------------------------------
|
|
1651
1651
|
|
|
1652
|
-
|
|
1652
|
+
/**
|
|
1653
|
+
* Which input a JSON path was given as, for the refusal a directory gets: the
|
|
1654
|
+
* flag that named it, the file it wants, and whether a file's contents read as
|
|
1655
|
+
* one (`null` where no parser exists here — the manifest is cast, not parsed).
|
|
1656
|
+
*/
|
|
1657
|
+
interface JsonInputRole {
|
|
1658
|
+
flag: '--rig' | '--motion' | '--manifest';
|
|
1659
|
+
wants: string;
|
|
1660
|
+
reads: ((raw: unknown, where: string) => unknown) | null;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const RIG_INPUT: JsonInputRole = { flag: '--rig', wants: 'the rig spec file', reads: parseRigSpec };
|
|
1664
|
+
const MOTION_INPUT: JsonInputRole = { flag: '--motion', wants: 'the motion spec file', reads: parseMotionSpec };
|
|
1665
|
+
const MANIFEST_INPUT: JsonInputRole = { flag: '--manifest', wants: 'the cut manifest file', reads: null };
|
|
1666
|
+
|
|
1667
|
+
/** The most `.json` names a directory refusal lists before it says how many there are. */
|
|
1668
|
+
const LISTED_JSON_NAMES = 8;
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* The refusal for an input flag that names a directory (issue #1466): what the
|
|
1672
|
+
* flag wants, and what the directory holds — the one file there that reads as
|
|
1673
|
+
* that input, named by its path, or every `.json` there when that is not
|
|
1674
|
+
* exactly one. Nothing is accepted on the strength of it: the caller is told
|
|
1675
|
+
* the path, and the compile still reads only what it was given. The names are
|
|
1676
|
+
* sorted, so the sentence is the same on every machine.
|
|
1677
|
+
*/
|
|
1678
|
+
function directoryInputRefusal(dir: string, role: JsonInputRole): string {
|
|
1679
|
+
const head = `${role.flag} names a directory; it wants ${role.wants} —`;
|
|
1680
|
+
const names = readdirSync(dir)
|
|
1681
|
+
.filter((name) => name.endsWith('.json') && statSync(join(dir, name)).isFile())
|
|
1682
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1683
|
+
if (names.length === 0) return `${head} ${dir} holds no .json file`;
|
|
1684
|
+
const reading = names.filter((name) => {
|
|
1685
|
+
if (role.reads === null) return true;
|
|
1686
|
+
const file = join(dir, name);
|
|
1687
|
+
try {
|
|
1688
|
+
role.reads(parseJsonWithPosition(readFileSync(file, 'utf8')), file);
|
|
1689
|
+
return true;
|
|
1690
|
+
} catch {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
if (reading.length === 1) return `${head} ${join(dir, reading[0])} exists there`;
|
|
1695
|
+
const listed = (list: string[]): string =>
|
|
1696
|
+
list.slice(0, LISTED_JSON_NAMES).join(', ') + (list.length > LISTED_JSON_NAMES ? `, … ${list.length} in all` : '');
|
|
1697
|
+
if (reading.length === 0) {
|
|
1698
|
+
return `${head} ${dir} holds ${names.length} .json file(s) and none reads as one: ${listed(names)}`;
|
|
1699
|
+
}
|
|
1700
|
+
return `${head} ${reading.length} .json files in ${dir} read as one, so name it: ${listed(reading)}`;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function readJson<T>(path: string, role: JsonInputRole): T {
|
|
1653
1704
|
let text: string;
|
|
1705
|
+
if (existsSync(path) && statSync(path).isDirectory()) {
|
|
1706
|
+
throw new CompileError(directoryInputRefusal(path, role), 'CE_COMPILE_JSON_READ_FAILED');
|
|
1707
|
+
}
|
|
1654
1708
|
try {
|
|
1655
1709
|
text = readFileSync(path, 'utf8');
|
|
1656
1710
|
} catch (err) {
|
|
@@ -2309,11 +2363,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2309
2363
|
const manifestPath = opts.manifestPath === undefined ? null : resolve(opts.manifestPath);
|
|
2310
2364
|
const manifestDir = manifestPath === null ? null : dirname(manifestPath);
|
|
2311
2365
|
|
|
2312
|
-
const rig = parseRigSpec(readJson<unknown>(rigPath), rigPath);
|
|
2366
|
+
const rig = parseRigSpec(readJson<unknown>(rigPath, RIG_INPUT), rigPath);
|
|
2313
2367
|
// Parsed, not cast, since issue #307 — `src/motion.ts` states what it proves
|
|
2314
2368
|
// and which refusals it deliberately leaves in this file.
|
|
2315
|
-
const motion = parseMotionSpec(readJson<unknown>(motionPath), motionPath);
|
|
2316
|
-
const manifest = manifestPath === null ? null : readJson<FaceManifest>(manifestPath);
|
|
2369
|
+
const motion = parseMotionSpec(readJson<unknown>(motionPath, MOTION_INPUT), motionPath);
|
|
2370
|
+
const manifest = manifestPath === null ? null : readJson<FaceManifest>(manifestPath, MANIFEST_INPUT);
|
|
2317
2371
|
// The manifest is the record of what was measured, and its numbers become
|
|
2318
2372
|
// bone positions and the stage: a `crop.h` of 1e309 built as `"height": null`
|
|
2319
2373
|
// before issue #881. The rig spec's walk is `parseRigSpec`'s; this is the
|
package/src/passthrough.ts
CHANGED
|
@@ -238,7 +238,7 @@ export function texelSize(piece: Piece, vertices: ArrayLike<number>, page: Plate
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
/** The cells `piece` draws at half opacity or more over `viewport`, row major, 1 where drawn. */
|
|
241
|
-
function drawnCells(piece: Piece, page: Plate, viewport: Viewport): Uint8Array {
|
|
241
|
+
export function drawnCells(piece: Piece, page: Plate, viewport: Viewport): Uint8Array {
|
|
242
242
|
const cells = new Uint8Array(viewport.width * viewport.height);
|
|
243
243
|
rasterisePiece(page, piece, projector(viewport), viewport, (px, py, _r, _g, _b, a) => {
|
|
244
244
|
if (a >= DRAWN_ALPHA) cells[py * viewport.width + px] = 1;
|
|
@@ -251,7 +251,7 @@ function drawnCells(piece: Piece, page: Plate, viewport: Viewport): Uint8Array {
|
|
|
251
251
|
* renderer's own traversal over a one-cell grid centred on the point, so the
|
|
252
252
|
* test is the frame's pixel test and nothing re-derived beside it.
|
|
253
253
|
*/
|
|
254
|
-
function drawsAt(piece: Piece, page: Plate, x: number, y: number, cell: number): boolean {
|
|
254
|
+
export function drawsAt(piece: Piece, page: Plate, x: number, y: number, cell: number): boolean {
|
|
255
255
|
const viewport: Viewport = { minX: x - cell / 2, minY: y - cell / 2, maxX: x + cell / 2, maxY: y + cell / 2, scale: 1 / cell, width: 1, height: 1 };
|
|
256
256
|
let drawn = false;
|
|
257
257
|
rasterisePiece(page, piece, projector(viewport), viewport, (_px, _py, _r, _g, _b, a) => {
|
|
@@ -427,7 +427,7 @@ function runsAt(
|
|
|
427
427
|
const restKey = (slot: string, attachment: string): string => `${slot}\u0000${attachment}`;
|
|
428
428
|
|
|
429
429
|
/** Whether two vertex lists are the same numbers — a piece drawn from its attachment whole, not cut by a clip. */
|
|
430
|
-
function sameVertices(a: ArrayLike<number>, b: ArrayLike<number>): boolean {
|
|
430
|
+
export function sameVertices(a: ArrayLike<number>, b: ArrayLike<number>): boolean {
|
|
431
431
|
if (a.length !== b.length) return false;
|
|
432
432
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
433
433
|
return true;
|
package/src/render.ts
CHANGED
|
@@ -549,9 +549,25 @@ function spinePosed(skeleton: Skeleton): Posed {
|
|
|
549
549
|
pieces: (draw) => drawPieces(skeleton, draw),
|
|
550
550
|
bones: () => boneSnapshots(skeleton),
|
|
551
551
|
attachments: () => attachmentsOf(skeleton),
|
|
552
|
+
physics: () => steppedPhysicsOf(skeleton),
|
|
552
553
|
};
|
|
553
554
|
}
|
|
554
555
|
|
|
556
|
+
/**
|
|
557
|
+
* `Posed.physics` on spine-core (issue #1467): every physics constraint whose
|
|
558
|
+
* `lastTime` reads the skeleton's `time` after a step that moved it — which
|
|
559
|
+
* `PhysicsConstraint.update` sets under `Physics.update` and `Physics.reset`,
|
|
560
|
+
* and which an inactive constraint, a mix of 0 or `Physics.none` leaves where
|
|
561
|
+
* it was. The setup pose and frame 0 sit at time 0 and step none.
|
|
562
|
+
*/
|
|
563
|
+
function steppedPhysicsOf(skeleton: Skeleton): string[] {
|
|
564
|
+
if (!(skeleton.time > 0)) return [];
|
|
565
|
+
return skeleton.physics
|
|
566
|
+
.filter((c) => c.lastTime === skeleton.time)
|
|
567
|
+
.map((c) => c.data.name)
|
|
568
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
569
|
+
}
|
|
570
|
+
|
|
555
571
|
/**
|
|
556
572
|
* A fresh skeleton under `skin`, stepped into its setup pose — the one recipe
|
|
557
573
|
* `sampleSetupPose` and the geometry export's `rest` table both pose from, so
|