@rig-c/parts 1.15.0 → 1.17.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 +4 -0
- package/cli.ts +62 -12
- package/docs/AUTHORING.md +454 -200
- package/docs/STABILITY.md +12 -8
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +11 -2
- package/src/assemble.ts +325 -23
- package/src/autoamplitude.ts +23 -3
- package/src/autoenvelope.ts +17 -5
- package/src/build.ts +81 -8
- package/src/check.ts +632 -11
- package/src/config.ts +185 -8
- package/src/errors.ts +16 -0
- package/src/guide.ts +255 -0
- package/src/limbs.ts +188 -0
- package/src/parts.ts +89 -4
- package/src/propose.ts +137 -107
- package/src/rig.ts +43 -8
- package/src/scene.ts +2 -2
package/src/autoenvelope.ts
CHANGED
|
@@ -58,7 +58,10 @@
|
|
|
58
58
|
* setup by; an unkeyed property is [0, 0], [1, 1] or 0. `pivot` is the
|
|
59
59
|
* bone's setup joint in the part's drawing frame (crop px less the part's
|
|
60
60
|
* padded origin, y down). `setup` is the rig's as written: this package
|
|
61
|
-
* writes no bone scale
|
|
61
|
+
* writes no bone scale or shear, so `{ 1, 1, 0, 0, 'normal' }` — with the
|
|
62
|
+
* bone's own `inherit` where config.bones declares one (issue #348), handed
|
|
63
|
+
* to rig-c as written, which refuses any mode but `normal` by name
|
|
64
|
+
* (`SKINNING_RANGE_UNSUPPORTED`): the residual is then not measurable.
|
|
62
65
|
* A shear key has no field in `BoneMotionRange`, so a bone keyed by one
|
|
63
66
|
* makes the residual not measurable (`ENVELOPE_SHEAR_KEYED`); a property
|
|
64
67
|
* the idle writes and none of the above names is refused the same way.
|
|
@@ -86,7 +89,7 @@ import { edgeLength } from './length.ts';
|
|
|
86
89
|
|
|
87
90
|
/** What the derivation reads off the rig besides the part: the bones (name, parent), each bone's setup joint in crop px, the idle as written, the constraints. */
|
|
88
91
|
export interface EnvelopeBasis {
|
|
89
|
-
bones: ReadonlyArray<{ name: string; parent?: string }>;
|
|
92
|
+
bones: ReadonlyArray<{ name: string; parent?: string; inherit?: string }>;
|
|
90
93
|
joints: ReadonlyMap<string, readonly [number, number]>;
|
|
91
94
|
motion: MotionSpec;
|
|
92
95
|
constraints: readonly ConfigConstraint[];
|
|
@@ -105,8 +108,11 @@ export type EnvelopeDerivation = { envelope: SkinningEnvelope } | { stops: Envel
|
|
|
105
108
|
/** Where a range comes from: the bone's keys, or the kind of the constraint that drives it — which rig-c refuses by name. */
|
|
106
109
|
type RangeSource = 'keys' | 'ik' | 'transform' | 'path' | 'physics';
|
|
107
110
|
|
|
108
|
-
/**
|
|
109
|
-
|
|
111
|
+
/**
|
|
112
|
+
* A `BoneMotionRange` whose `source` may name a constraint kind, and whose `setup.inherit` may name a declared mode
|
|
113
|
+
* (issue #348): rig-c reads both at run time and certifies `'keys'` and `'normal'` alone.
|
|
114
|
+
*/
|
|
115
|
+
type DeclaredRange = Omit<BoneMotionRange, 'source' | 'setup'> & { source: RangeSource; setup: Omit<BoneMotionRange['setup'], 'inherit'> & { inherit: string } };
|
|
110
116
|
|
|
111
117
|
/**
|
|
112
118
|
* rig-c's helper over ranges whose `source` may be a constraint kind. rig-c's type admits only the one value it
|
|
@@ -136,6 +142,12 @@ function trackValues(keys: readonly MotionKey[]): number[] {
|
|
|
136
142
|
|
|
137
143
|
const SETUP: BoneMotionRange['setup'] = { scaleX: 1, scaleY: 1, shearX: 0, shearY: 0, inherit: 'normal' };
|
|
138
144
|
|
|
145
|
+
/** The bone's declared inherit mode as the rig writes it (issue #348), or nothing, so {@link SETUP}'s `normal` stands. */
|
|
146
|
+
function inheritOf(basis: EnvelopeBasis, bone: string): { inherit: string } | Record<string, never> {
|
|
147
|
+
const mode = basis.bones.find((b) => b.name === bone)?.inherit;
|
|
148
|
+
return mode === undefined ? {} : { inherit: mode };
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
/** One bone's declared range off the idle, or the stop that names why it has none. */
|
|
140
152
|
export function boneRange(basis: EnvelopeBasis, bone: string, origin: readonly [number, number], driven: ReadonlyMap<string, RangeSource>): DeclaredRange | EnvelopeStop {
|
|
141
153
|
const joint = basis.joints.get(bone);
|
|
@@ -170,7 +182,7 @@ export function boneRange(basis: EnvelopeBasis, bone: string, origin: readonly [
|
|
|
170
182
|
scaleX,
|
|
171
183
|
scaleY,
|
|
172
184
|
translate: edgeLength(tx, ty),
|
|
173
|
-
setup: { ...SETUP },
|
|
185
|
+
setup: { ...SETUP, ...inheritOf(basis, bone) },
|
|
174
186
|
};
|
|
175
187
|
}
|
|
176
188
|
|
package/src/build.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
44
44
|
import { dirname, join, resolve } from 'node:path';
|
|
45
45
|
import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
|
|
46
|
-
import { assemble, type AssembleResult, cutLines, driftLine, FIGURE_MASK_FILE, figuresLine, holeLines, type ProjectRule, scenePhrase, type SeamRule, stageFields, uncoveredLine } from './assemble.ts';
|
|
46
|
+
import { assemble, type AssembleResult, cutLines, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeLines, type ProjectRule, scenePhrase, type SeamRule, stageFields, uncoveredLine } from './assemble.ts';
|
|
47
47
|
import { allocationClause, type Reducer, stageBClause, unboundedClause } from './automesh.ts';
|
|
48
48
|
import { residualClause } from './autoenvelope.ts';
|
|
49
49
|
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, type ReplayCandidate, runComparison, sourceSelfFailure, stimulusMotion, withStimulus } from './automotion.ts';
|
|
@@ -70,9 +70,9 @@ import {
|
|
|
70
70
|
splitSchedule,
|
|
71
71
|
untestedIntervals,
|
|
72
72
|
} from './autoreplay.ts';
|
|
73
|
-
import { besideIdleLine, FOLD_LINE, type FoldsLine, buildGateLines, causeLines, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, rigcFailed, rigcIdentity, rigcRanClause, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
|
|
74
|
-
import { type Cut, loadConfig, loadConfigAndAnimations, loadEarlyConfig } from './config.ts';
|
|
75
|
-
import { PartsError, type Problem,
|
|
73
|
+
import { besideIdleLine, FOLD_LINE, type FoldsLine, LEVEL_LINE, type LevelLine, LIMB_LINE, type LimbLine, STRESS_LINE, buildGateLines, causeLines, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, rigcFailed, rigcIdentity, rigcRanClause, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
|
|
74
|
+
import { type Cut, type Limb, loadConfig, loadConfigAndAnimations, loadEarlyConfig } from './config.ts';
|
|
75
|
+
import { failLine, PartsError, type Problem, refuseIfAny } from './errors.ts';
|
|
76
76
|
import type { MeshQualityReport, MotionSchedule } from 'rig-c/mesh';
|
|
77
77
|
import { encodeGif } from './gif.ts';
|
|
78
78
|
import { CONTROL_SUFFIX, withAnimations } from './motion.ts';
|
|
@@ -200,6 +200,36 @@ export function readUnders(configPath: string, cuts: readonly Cut[]): Array<Rast
|
|
|
200
200
|
return out;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Each limb segment's `cap` image (issue #349), read from its path relative to
|
|
205
|
+
* the config's directory and keyed `<limb>/<segment index>`. Every one is read
|
|
206
|
+
* before one refusal names them all: no such file
|
|
207
|
+
* (`ASSEMBLE_LIMB_CAP_PRESENT`), or a file that does not decode as a PNG
|
|
208
|
+
* (`ASSEMBLE_LIMB_CAP_DECODES`). The stage holds the image to the cap's box.
|
|
209
|
+
*/
|
|
210
|
+
export function readCaps(configPath: string, limbs: Readonly<Record<string, Limb>>): Map<string, Raster> {
|
|
211
|
+
const problems: Problem[] = [];
|
|
212
|
+
const out = new Map<string, Raster>();
|
|
213
|
+
for (const [l, limb] of Object.entries(limbs)) {
|
|
214
|
+
limb.segments.forEach((sg, k) => {
|
|
215
|
+
if (sg.cap === undefined) return;
|
|
216
|
+
const file = resolve(dirname(resolve(configPath)), sg.cap);
|
|
217
|
+
const object = `config.limbs.${l}.segments[${k}].cap (part "${sg.part}")`;
|
|
218
|
+
if (!existsSync(file) || !statSync(file).isFile()) {
|
|
219
|
+
problems.push({ code: 'ASSEMBLE_LIMB_CAP_PRESENT', object, detail: `names ${file}, which is no such file; a PNG of the cap, its path relative to the config's directory, is required` });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
out.set(`${l}/${k}`, readPng(file));
|
|
224
|
+
} catch (err) {
|
|
225
|
+
problems.push({ code: 'ASSEMBLE_LIMB_CAP_DECODES', object, detail: `${file}: ${(err as Error).message.split('\n')[0]}; a PNG is required` });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
refuseIfAny(problems);
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
203
233
|
/** Read, assemble, and write only after every refusal has had its chance. Throws a PartsError on a refusal, having written nothing. */
|
|
204
234
|
export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs, log: Log): AssembleResult {
|
|
205
235
|
const src = readSource(input.source);
|
|
@@ -208,7 +238,8 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
208
238
|
// regions and motion, so it requires only what it reads (issue #20).
|
|
209
239
|
const fields = stageFields(loadEarlyConfig(input.config, 'assemble'));
|
|
210
240
|
const under = readUnders(input.config, fields.cuts);
|
|
211
|
-
const
|
|
241
|
+
const caps = fields.limbs === undefined ? undefined : readCaps(input.config, fields.limbs);
|
|
242
|
+
const result = assemble({ source: src, full: runs.full, head: runs.head, ...fields, under, ...(caps === undefined ? {} : { caps }), seamRule: input.seam, projectRule: input.project });
|
|
212
243
|
// Emit only after green: every refusal above has already thrown.
|
|
213
244
|
mkdirSync(outs.partsDir, { recursive: true });
|
|
214
245
|
mkdirSync(dirname(outs.partsJson), { recursive: true });
|
|
@@ -234,6 +265,8 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
234
265
|
for (const l of authorPartLines(result.parts.parts)) log(l);
|
|
235
266
|
// One line per plan part an assemble.cuts entry cut, before the fringe lines: the cut is made first. None without cuts.
|
|
236
267
|
for (const l of cutLines(result.cuts)) log(l);
|
|
268
|
+
// Issue #349: one line per part a limb took, and one per joint with its cap's pixels. None without limbs.
|
|
269
|
+
for (const l of limbLines(result.limbs)) log(l);
|
|
237
270
|
for (const l of fringeLines(result.parts.parts)) log(l);
|
|
238
271
|
const total = (k: 'opaque_px' | 'visible_px' | 'occluded_px' | 'source_px_taken' | 'visible_not_projected_px'): number => result.parts.parts.reduce((a, p) => a + (p[k] ?? 0), 0);
|
|
239
272
|
const [op, vis, occ, taken, unproj] = (['opaque_px', 'visible_px', 'occluded_px', 'source_px_taken', 'visible_not_projected_px'] as const).map(total);
|
|
@@ -1046,6 +1079,40 @@ export function foldsLines(line: FoldsLine): string[] {
|
|
|
1046
1079
|
return [head, ...line.folding.map((r) => ` folds ${r.line}${r.declared === null ? '' : ` — declared in may_fold: ${r.declared}`}`)];
|
|
1047
1080
|
}
|
|
1048
1081
|
|
|
1082
|
+
/**
|
|
1083
|
+
* The feet-level line (issue #348) as the console prints it: the status, what was read, the bar and whose it is, then
|
|
1084
|
+
* each foot on a line of its own — its bone, what that hangs from, how many frames posed it, and the worst turn with
|
|
1085
|
+
* the bone that was turned and the bones it came through. A SKIP prints its reason.
|
|
1086
|
+
*/
|
|
1087
|
+
export function levelLines(line: LevelLine): string[] {
|
|
1088
|
+
const physics = line.physics_left_out === 0 ? '' : `, its ${line.physics_left_out} physics constraint(s) left out`;
|
|
1089
|
+
const read = `${line.animations.length} animation(s) read by rigc render --folds --geometry at ${line.fps} fps${physics} (${line.animations.join(', ')})`;
|
|
1090
|
+
const head =
|
|
1091
|
+
line.status === 'SKIP'
|
|
1092
|
+
? `${LEVEL_LINE}: SKIP — ${String(line.reason)}; ${read}`
|
|
1093
|
+
: `${LEVEL_LINE}: ${line.status} — ${read}: each foot's drawing within ±${line.bar_degrees} degrees of its setup pose while a bone above it is turned, ${line.bar}`;
|
|
1094
|
+
return [
|
|
1095
|
+
head,
|
|
1096
|
+
...line.feet.map((f) => {
|
|
1097
|
+
const at = `foot "${f.part}" on "${f.bone}" (hangs from "${f.hangs_from}"): ${f.status}, ${f.frames_posed} posed frame(s)`;
|
|
1098
|
+
if (f.worst === null) return `${at}, so nothing was measured`;
|
|
1099
|
+
const w = f.worst;
|
|
1100
|
+
return `${at}; worst ${w.degrees} degrees in "${w.animation}" frame ${w.frame} (t = ${w.time}s), "${w.turned}" turned ${w.turned_degrees}, through ${w.through.map((x) => `${x.bone} (inherit ${x.inherit})`).join(', ')}`;
|
|
1101
|
+
}),
|
|
1102
|
+
];
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* The limb line (issue #349) as the console prints it: the status and the bar, then each limb part on a line of its
|
|
1107
|
+
* own — its limb, its bone, and on a FAIL every other bone it is bound to.
|
|
1108
|
+
*/
|
|
1109
|
+
export function limbCheckLines(line: LimbLine): string[] {
|
|
1110
|
+
return [
|
|
1111
|
+
`${LIMB_LINE}: ${line.status} — ${line.parts.length} limb part(s), each bound in rig.json to its one bone with weight 1 (a region on it, or a mesh weighted to it alone)`,
|
|
1112
|
+
...line.parts.map((r) => `limb "${r.part}" (${r.limb}) on "${r.bone}": ${r.status}${r.others.length === 0 ? '' : `, also weighted to ${r.others.map((b) => `"${b}"`).join(', ')}`}`),
|
|
1113
|
+
];
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1049
1116
|
/** A requirement's line as the console prints it: its name and status, the reason first when NOT MEASURABLE, then its figures as check.json holds them. */
|
|
1050
1117
|
export function requirementText(name: string, line: RequirementLine): string {
|
|
1051
1118
|
const rest = Object.entries(line).filter(([k]) => k !== 'status' && k !== 'reason');
|
|
@@ -1072,7 +1139,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1072
1139
|
if (r.idle === null) log(` loop: SKIP — ${fig.skipped?.loop ?? ''}`);
|
|
1073
1140
|
else log(` ${loopCheckLine(r.idle, fig.loop_max_diff, fig.loop_physics?.length ?? 0)}`);
|
|
1074
1141
|
if (r.loopPhysics !== null) log(` ${r.loopPhysics}`);
|
|
1075
|
-
const beside = besideIdleLine(r.besideIdle);
|
|
1142
|
+
const beside = besideIdleLine(r.besideIdle, r.figures[LEVEL_LINE] !== undefined);
|
|
1076
1143
|
if (beside !== null) log(` ${beside}`);
|
|
1077
1144
|
if (r.seamViewport === null || fig.seam_mean === null) log(` seam: SKIP — ${fig.skipped?.seam ?? ''}`);
|
|
1078
1145
|
else {
|
|
@@ -1086,6 +1153,12 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1086
1153
|
for (const name of JUDGEMENT_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1087
1154
|
const folds = fig[FOLD_LINE];
|
|
1088
1155
|
if (folds !== undefined) for (const l of foldsLines(folds)) log(` ${l}`);
|
|
1156
|
+
const level = fig[LEVEL_LINE];
|
|
1157
|
+
if (level !== undefined) for (const [i, l] of levelLines(level).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1158
|
+
const stress = fig[STRESS_LINE];
|
|
1159
|
+
if (stress !== undefined) log(` ${judgementLine(STRESS_LINE, stress)}`);
|
|
1160
|
+
const limbs = fig[LIMB_LINE];
|
|
1161
|
+
if (limbs !== undefined) for (const [i, l] of limbCheckLines(limbs).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1089
1162
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1090
1163
|
const vsSource = fig[SOURCE_LINE];
|
|
1091
1164
|
if (vsSource !== undefined) log(` ${judgementLine(SOURCE_LINE, vsSource)}`);
|
|
@@ -1095,7 +1168,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1095
1168
|
}
|
|
1096
1169
|
log(` gate: spine-html ${fig.gate_spine_html_green ? 'green' : 'RED'} (${fig.rigc_entry.entry}${fig.rigc_entry.spine_core === null ? ', rigc\'s own validator' : `, the spine-core ${fig.rigc_entry.spine_core} round trip`})`);
|
|
1097
1170
|
log(` wrote ${r.written.map((w) => join(input.out, w)).join(', ')}, ${join(input.out, 'build')}/${r.idle === null ? '' : `, ${join(input.out, 'idle_frames')}/`}${r.requirements === null ? '' : `, ${join(input.out, REQUIREMENTS_DIR)}/`}${fig.seam_pairs === undefined ? '' : `, ${join(input.out, SEAM_DIR)}/`}${existsSync(join(input.out, STILL_DIR)) ? `, ${join(input.out, STILL_DIR)}/` : ''}`);
|
|
1098
|
-
for (const p of r.problems) log(
|
|
1171
|
+
for (const p of r.problems) log(failLine(p));
|
|
1099
1172
|
const reqNotPass = r.requirements === null ? 0 : r.requirements.summary.declared - r.requirements.summary.pass;
|
|
1100
1173
|
const notMet = r.requirements === null ? `${r.problems.length} bar(s) not met` : `${r.problems.length - reqNotPass} bar(s) not met, ${reqNotPass} declared requirement(s) not PASS`;
|
|
1101
1174
|
log(`${fig.PASS ? 'check: PASS' : `check: FAIL — ${notMet}`}; ${barsSummary(r)}`);
|
|
@@ -1466,7 +1539,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
1466
1539
|
const refused = (stage: BuildStage, err: unknown, check: CheckReport | null = null): BuildResult => {
|
|
1467
1540
|
if (!(err instanceof PartsError)) throw err;
|
|
1468
1541
|
const say = prefixed(stage);
|
|
1469
|
-
for (const p of err.problems) say(
|
|
1542
|
+
for (const p of err.problems) say(failLine(p));
|
|
1470
1543
|
say(`refused: ${err.problems.length} problem(s)`);
|
|
1471
1544
|
log(`build: stopped at ${stage}; no later stage ran`);
|
|
1472
1545
|
return { stoppedAt: stage, check, artifact: [] };
|