spine-rigc 2.54.0 → 2.55.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 +1 -1
- package/docs/AUTHORING.md +144 -24
- package/package.json +1 -1
- package/src/assertions/bodies/a23.ts +80 -5
- package/src/cli/core_commands.ts +73 -4
- package/src/cli/shared.ts +11 -1
- package/src/passthrough.ts +575 -0
- package/src/timelines.ts +75 -29
- package/src/types.ts +4 -3
package/src/cli/core_commands.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { MODEL_DOCUMENT_FILE, modelDocument } from '../model.ts';
|
|
|
32
32
|
import { parseMotionSpec } from '../motion.ts';
|
|
33
33
|
import { type InspectCompare, type InspectFrame, InspectError, inspectCandidate, INSPECT_DEFAULT_FPS, legendText, readBonePatterns, readShow, readTimes, readZoom } from '../inspect.ts';
|
|
34
34
|
import { FOLDS_FILE, foldLine, foldsFileOf, foldsOf, foldsText, setupFaultLine } from '../folds.ts';
|
|
35
|
+
import { PASS_THROUGH_FILE, passFrameLine, type PassPair, passSummaryLine, passThroughFileOf, passThroughOf, passThroughText, PassThroughError, readPassPairs } from '../passthrough.ts';
|
|
35
36
|
import { environmentCollisions, environmentFrame, EnvironmentError, environmentResponse, readEnvironmentSchedule, type EnvironmentStep } from '../environment.ts';
|
|
36
37
|
import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
|
|
37
38
|
import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, 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 } from '../render_shared.ts';
|
|
@@ -539,6 +540,22 @@ function environmentRefusal(given: string, steps: readonly EnvironmentStep[], sk
|
|
|
539
540
|
return null;
|
|
540
541
|
}
|
|
541
542
|
|
|
543
|
+
/**
|
|
544
|
+
* `--pass-through <cloth>:<leg>[,…]` read against the skeleton's slots (issue
|
|
545
|
+
* #1413, `../passthrough.ts`), or refused as a usage error before anything is
|
|
546
|
+
* posed: a malformed list, an empty name, a slot the skeleton does not have, a
|
|
547
|
+
* pair named twice — each by its entry. `undefined` without the flag.
|
|
548
|
+
*/
|
|
549
|
+
function readPassThroughFlag(raw: string | undefined, facts: SkeletonFacts): PassPair[] | undefined {
|
|
550
|
+
if (raw === undefined) return undefined;
|
|
551
|
+
try {
|
|
552
|
+
return readPassPairs(raw, facts.slots.map((slot) => slot.name));
|
|
553
|
+
} catch (err) {
|
|
554
|
+
if (err instanceof PassThroughError) throw new UsageError(err.message);
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
542
559
|
/**
|
|
543
560
|
* render — the frame series, drawn by the same rasteriser `check` measures with.
|
|
544
561
|
*
|
|
@@ -580,6 +597,8 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
580
597
|
}
|
|
581
598
|
}
|
|
582
599
|
}
|
|
600
|
+
// The pass-through reading (issue #1413) is its own flag for --folds' reason: without it, every file is the bytes it was.
|
|
601
|
+
const passThrough = flags['pass-through'];
|
|
583
602
|
// The same refusal for the same reason: the reading is of every mesh the pose shows, whatever a picture leaves out.
|
|
584
603
|
if (folds) {
|
|
585
604
|
for (const flag of ['slot', 'hide'] as const) {
|
|
@@ -593,6 +612,19 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
593
612
|
}
|
|
594
613
|
}
|
|
595
614
|
|
|
615
|
+
// And for the pass-through reading: it reads the two slots as the frame draws them, and a subset changes which draw.
|
|
616
|
+
if (passThrough !== undefined) {
|
|
617
|
+
for (const flag of ['slot', 'hide'] as const) {
|
|
618
|
+
if (flags[flag] !== undefined) {
|
|
619
|
+
throw new UsageError(
|
|
620
|
+
`--pass-through takes no --${flag}: it reads the cloth and the leg as the whole rig draws them, which a subset of the ` +
|
|
621
|
+
`drawn slots would change. Run \`rigc render --pass-through\` on the whole rig for the reading, and --${flag} ${flags[flag]} ` +
|
|
622
|
+
'without --pass-through for the pictures — both land on the same grid',
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
596
628
|
console.log('rigc render');
|
|
597
629
|
console.log(` .. skeleton ${skeletonPath}`);
|
|
598
630
|
console.log(` .. atlas ${atlasPath}${atlasText === null ? ` — not there (${ATLAS_ABSENT})` : ''}`);
|
|
@@ -610,6 +642,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
610
642
|
const only = readAnimationFlag(flags, [...facts.animations]);
|
|
611
643
|
const skin = readSkinFlag(flags, [...facts.skins]);
|
|
612
644
|
const subset = readSlotSubsetFlags(flags, facts, skin);
|
|
645
|
+
const passPairs = readPassThroughFlag(passThrough, facts);
|
|
613
646
|
// One object, so the framing and the frames cannot be posed under two
|
|
614
647
|
// different skins — which would frame one shot with another shot's box.
|
|
615
648
|
// Not annotated `PoseOptions`: that name is `src/pose.ts`'s in this file, and
|
|
@@ -669,7 +702,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
669
702
|
// `--geometry` rides on the SAME call (issue #864): the bones and the whole
|
|
670
703
|
// attachments are read off the skeleton at the step that drew each frame, so
|
|
671
704
|
// the export's grid is this frame set's by construction.
|
|
672
|
-
const sampling = geometry ? { ...pose, bones: true, geometry: true } : folds ? { ...pose, geometry: true } : pose;
|
|
705
|
+
const sampling = geometry ? { ...pose, bones: true, geometry: true } : folds || passPairs !== undefined ? { ...pose, geometry: true } : pose;
|
|
673
706
|
const sampled: Map<string, Frame[]> =
|
|
674
707
|
only === undefined
|
|
675
708
|
? sampleAll(poser, fps, sampling)
|
|
@@ -694,9 +727,37 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
694
727
|
foldReadings.set(name, foldsFileOf(animation, fps, skin, foldsOf(poser.rest(skin, shown), read)));
|
|
695
728
|
}
|
|
696
729
|
}
|
|
697
|
-
|
|
730
|
+
// The pass-through reading, off the very pieces the frames are drawn from, in their draw order — one per set, every
|
|
731
|
+
// pair the flag names. A pair whose slot draws on no frame of any set drawn is refused rather than read as 0.
|
|
732
|
+
const passReadings = new Map<string, ReturnType<typeof passThroughFileOf>>();
|
|
733
|
+
if (passPairs !== undefined) {
|
|
734
|
+
const draws = new Set<string>();
|
|
735
|
+
for (const frames of sampled.values()) for (const frame of frames) for (const piece of frame.pieces) draws.add(piece.slot);
|
|
736
|
+
for (const pair of passPairs) {
|
|
737
|
+
for (const slot of [pair.cloth, pair.leg]) {
|
|
738
|
+
if (!draws.has(slot)) {
|
|
739
|
+
throw new UsageError(
|
|
740
|
+
`--pass-through: slot ${JSON.stringify(slot)} draws on no frame of [${[...sampled.keys()].join(', ')}]${skin === undefined ? '' : ` under skin ${JSON.stringify(skin)}`} ` +
|
|
741
|
+
'— it shows no region or mesh there, so it has no drawn outline to read',
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const [name, frames] of sampled) {
|
|
747
|
+
const animation = name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name;
|
|
748
|
+
const shown = frames.map((frame) => frame.attachments ?? []);
|
|
749
|
+
const read = frames.map((frame) => ({ index: frame.index, time: frame.time, pieces: frame.pieces, attachments: frame.attachments ?? [] }));
|
|
750
|
+
try {
|
|
751
|
+
passReadings.set(name, passThroughFileOf(animation, fps, skin, passThroughOf(passPairs, poser.rest(skin, shown), read, pages)));
|
|
752
|
+
} catch (err) {
|
|
753
|
+
if (err instanceof PassThroughError) throw new UsageError(`--pass-through, ${JSON.stringify(name)}: ${err.message}`);
|
|
754
|
+
throw err;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return { viewport, sampled, geometryTexts, foldReadings, passReadings };
|
|
698
759
|
});
|
|
699
|
-
const { viewport, sampled, geometryTexts, foldReadings } = posed.value;
|
|
760
|
+
const { viewport, sampled, geometryTexts, foldReadings, passReadings } = posed.value;
|
|
700
761
|
console.log(` .. poser ${posed.note}`);
|
|
701
762
|
if (environment !== undefined) {
|
|
702
763
|
const response = environmentResponse(skeletonText);
|
|
@@ -733,6 +794,8 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
733
794
|
if (geometryOut !== undefined) writeFileSync(join(dir, GEOMETRY_FILE), geometryOut);
|
|
734
795
|
const foldReading = foldReadings.get(name);
|
|
735
796
|
if (foldReading !== undefined) writeFileSync(join(dir, FOLDS_FILE), foldsText(foldReading));
|
|
797
|
+
const passReading = passReadings.get(name);
|
|
798
|
+
if (passReading !== undefined) writeFileSync(join(dir, PASS_THROUGH_FILE), passThroughText(passReading));
|
|
736
799
|
const duration = frames[frames.length - 1].time;
|
|
737
800
|
sets.push({
|
|
738
801
|
dir: dirName,
|
|
@@ -746,7 +809,9 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
746
809
|
const how = frames.length === 1 ? 'a single pose' : `${duration.toFixed(3)}s`;
|
|
747
810
|
const folding = foldReading?.meshes.filter((m) => m.worst !== null).length ?? 0;
|
|
748
811
|
const foldNote = foldReading === undefined ? '' : ` + ${FOLDS_FILE} (${folding} of ${foldReading.meshes.length} mesh(es) fold)`;
|
|
749
|
-
const
|
|
812
|
+
const passing = passReading?.pairs.filter((p) => p.frames.length > 0).length ?? 0;
|
|
813
|
+
const passNote = passReading === undefined ? '' : ` + ${PASS_THROUGH_FILE} (${passing} of ${passReading.pairs.length} pair(s) pass through)`;
|
|
814
|
+
const extras = `${sheet ? ` + ${SHEET_FILE}` : ''}${geometryOut === undefined ? '' : ` + ${GEOMETRY_FILE}`}${foldNote}${passNote}`;
|
|
750
815
|
console.log(` .. ${name.padEnd(16)} ${frames.length} frame(s), ${how}${extras} -> ${dir}`);
|
|
751
816
|
for (const mesh of foldReading?.meshes ?? []) {
|
|
752
817
|
const key = `${mesh.slot}\u0000${mesh.attachment}`;
|
|
@@ -756,6 +821,10 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
756
821
|
const line = foldLine(foldReading?.animation ?? null, fps, mesh);
|
|
757
822
|
if (line !== null) console.log(` .. folds ${line}`);
|
|
758
823
|
}
|
|
824
|
+
for (const pair of passReading?.pairs ?? []) {
|
|
825
|
+
console.log(` .. pass ${passSummaryLine(passReading?.animation ?? null, fps, pair)}`);
|
|
826
|
+
for (const frame of pair.frames) console.log(` .. pass ${passFrameLine(passReading?.animation ?? null, pair, frame)}`);
|
|
827
|
+
}
|
|
759
828
|
}
|
|
760
829
|
|
|
761
830
|
// The sidecar is what makes this a frame SET rather than a pile of pictures:
|
package/src/cli/shared.ts
CHANGED
|
@@ -38,6 +38,7 @@ 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
42
|
import { parseJsonWithPosition } from '../json-position.ts';
|
|
42
43
|
import { RUNG_IDS } from '../ladder.ts';
|
|
43
44
|
import { MODEL_DOCUMENT_FILE, MODEL_DOCUMENT_SPEC, modelDocument, spineFileSha256 } from '../model.ts';
|
|
@@ -1013,6 +1014,13 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1013
1014
|
'with where they are (the world box of their posed vertices) and how big (their setup-pose area, and its share of the mesh\'s). ' +
|
|
1014
1015
|
'One line per mesh that folds; a triangle degenerate or inverted at the setup pose is said once and never counted. ' +
|
|
1015
1016
|
'A reading, not a gate. Not with --slot/--hide: it reads every mesh the pose shows',
|
|
1017
|
+
'pass-through':
|
|
1018
|
+
`also read where a cloth slot passes through a leg slot, for each <cloth>:<leg> pair, and write ${PASS_THROUGH_FILE} into ` +
|
|
1019
|
+
'each frame directory: the frames where the cloth draws over the leg and its drawn side lies inside the leg\'s drawn ' +
|
|
1020
|
+
`outline by ${PASS_THROUGH_RUN} steps or more — rows read in the cloth's setup pose from its hem's corner up, the run stepped in ` +
|
|
1021
|
+
'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. ' +
|
|
1022
|
+
'The order is the declaration: the first name must draw over the second for a frame to be read. A reading, not a gate. ' +
|
|
1023
|
+
'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',
|
|
1016
1024
|
environment:
|
|
1017
1025
|
'the world the rig\'s physics answers, in time: a file { "spec": "rigc-environment/1", "steps": [{ "t": <seconds>, ' +
|
|
1018
1026
|
'"wind"?: [x, y], "gravity"?: [x, y] }, …] }, x right and y down, each step holding from its t until the next and set ' +
|
|
@@ -1202,6 +1210,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1202
1210
|
'texture-from': '<path>',
|
|
1203
1211
|
poser: 'core|spine',
|
|
1204
1212
|
environment: '<schedule.json>',
|
|
1213
|
+
'pass-through': '<cloth>:<leg>[,<cloth>:<leg>…]',
|
|
1205
1214
|
reference: '<dir|skeleton.json>',
|
|
1206
1215
|
'reference-atlas': '<path>',
|
|
1207
1216
|
bones: `<correspondence.json|${IDENTITY_CORRESPONDENCE}>`,
|
|
@@ -1608,8 +1617,9 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1608
1617
|
'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--skin <name>] [--fps 12] [--max 256] [--geometry] [--folds] [--poser core|spine] [--out render/]',
|
|
1609
1618
|
'rigc render … --slot <name[,name…]> | --hide <name[,name…]> (a subset of the slots, on the whole rig\'s grid)',
|
|
1610
1619
|
'rigc render … --environment <schedule.json> (the wind and gravity the physics answers, in time)',
|
|
1620
|
+
'rigc render … --pass-through <cloth>:<leg>[,…] (the frames where a cloth drawn over a leg passes through it)',
|
|
1611
1621
|
],
|
|
1612
|
-
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'geometry', 'folds', 'environment', 'poser', 'out'],
|
|
1622
|
+
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'geometry', 'folds', 'pass-through', 'environment', 'poser', 'out'],
|
|
1613
1623
|
overrides: {
|
|
1614
1624
|
out: { value: '<dir>', meaning: 'directory to write the frame series into (default `render/`)' },
|
|
1615
1625
|
fps: { meaning: `frames per second to sample the animation at (default ${PROTOCOL_FPS})` },
|