@rig-c/parts 1.16.0 → 1.18.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 +8 -3
- package/cli.ts +191 -24
- package/docs/AUTHORING.md +381 -21
- package/docs/MIGRATION.md +52 -0
- package/docs/PAINTING.md +172 -0
- package/docs/STABILITY.md +14 -9
- package/package.json +2 -1
- package/skills/rig-parts/SKILL.md +31 -2
- package/src/assemble.ts +642 -41
- package/src/autoamplitude.ts +23 -3
- package/src/autoenvelope.ts +17 -5
- package/src/build.ts +109 -13
- package/src/check.ts +632 -11
- package/src/config.ts +185 -8
- package/src/inputs.ts +11 -1
- package/src/layers.ts +231 -18
- package/src/limbs.ts +188 -0
- package/src/paintcheck.ts +250 -0
- package/src/parts.ts +167 -6
- package/src/propose.ts +148 -108
- package/src/rig.ts +43 -8
- package/src/tags.ts +47 -0
package/src/autoamplitude.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* undeclared, which would leave both rows `not-measurable` (§8: "a pair no
|
|
23
23
|
* track declares makes both rows not-measurable"). A track moves bone `b`
|
|
24
24
|
* when a bone it keys is `b` or an ancestor of `b`: Spine bones inherit
|
|
25
|
-
* their parent's rotation and scale (the rig writes no inherit mode), so a
|
|
25
|
+
* their parent's rotation and scale (where the rig writes no inherit mode), so a
|
|
26
26
|
* pair the track moves together — both under one keyed bone — or not at all
|
|
27
27
|
* keeps its relative linear part, θ 0 (§8: "a bone that does not move is
|
|
28
28
|
* declared with θ 0"); a pair it moves on one side only gets the track's θ.
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
* the author left it out — rig-c 2.29.0 then measures `MQ_DEFORM_LOAD`, which
|
|
58
58
|
* never read G, and reads `MQ_ALLOCATION_CONTRAST` `not-measurable` naming
|
|
59
59
|
* the gradation. No value is derived or defaulted here.
|
|
60
|
+
* - **A declared inherit mode** (issue #348): a bone between a keyed bone and
|
|
61
|
+
* a bound one — the bound bone itself included — whose `inherit` is not
|
|
62
|
+
* `normal` takes less than the key's rotation or scale, by its mode, so the
|
|
63
|
+
* track's θ for that pair is not composed here: the term stops, naming the
|
|
64
|
+
* bone and its mode, and the amplitude is not sent.
|
|
60
65
|
* - **Constraints** move bones by what they solve, not by a key: a rig with
|
|
61
66
|
* any constraint is refused by name, because no declared amplitude bounds
|
|
62
67
|
* what a constraint does.
|
|
@@ -73,7 +78,7 @@ import type { MotionKey, MotionSpec } from './motion.ts';
|
|
|
73
78
|
* — `rig.json`'s bones, `motion.json`, the config's constraints — so both build the one input `reductionKey` matches.
|
|
74
79
|
*/
|
|
75
80
|
export interface AmplitudeBasis {
|
|
76
|
-
bones: ReadonlyArray<{ name: string; parent?: string }>;
|
|
81
|
+
bones: ReadonlyArray<{ name: string; parent?: string; inherit?: string }>;
|
|
77
82
|
motion: MotionSpec;
|
|
78
83
|
constraints: number;
|
|
79
84
|
}
|
|
@@ -133,7 +138,7 @@ export type AmplitudeDerivation = { amplitude: MotionAmplitude } | { stops: Ampl
|
|
|
133
138
|
* reason to stop (rig-c 2.29.0 admits null).
|
|
134
139
|
*/
|
|
135
140
|
export function deriveMotionAmplitude(args: {
|
|
136
|
-
bones: ReadonlyArray<{ name: string; parent?: string }>;
|
|
141
|
+
bones: ReadonlyArray<{ name: string; parent?: string; inherit?: string }>;
|
|
137
142
|
motion: MotionSpec;
|
|
138
143
|
constraints: number;
|
|
139
144
|
bound: readonly string[];
|
|
@@ -142,6 +147,13 @@ export function deriveMotionAmplitude(args: {
|
|
|
142
147
|
}): AmplitudeDerivation {
|
|
143
148
|
const stops: AmplitudeStop[] = [];
|
|
144
149
|
const parent = new Map(args.bones.map((b) => [b.name, b.parent ?? null]));
|
|
150
|
+
const mode = new Map(args.bones.filter((b) => b.inherit !== undefined && b.inherit !== 'normal').map((b) => [b.name, b.inherit as string]));
|
|
151
|
+
/** The bones from `bone` up to `k`, `k` left out, that declare an inherit mode other than normal (issue #348). */
|
|
152
|
+
const declared = (bone: string, k: string): string[] => {
|
|
153
|
+
const out: string[] = [];
|
|
154
|
+
for (let b: string | null = bone; b !== null && b !== k; b = parent.get(b) ?? null) if (mode.has(b)) out.push(b);
|
|
155
|
+
return out;
|
|
156
|
+
};
|
|
145
157
|
const under = (bone: string, k: string): boolean => {
|
|
146
158
|
for (let b: string | null = bone; b !== null; b = parent.get(b) ?? null) if (b === k) return true;
|
|
147
159
|
return false;
|
|
@@ -159,6 +171,14 @@ export function deriveMotionAmplitude(args: {
|
|
|
159
171
|
const name = `${t.bone ?? t.group ?? '?'}.${t.property}#${i}`;
|
|
160
172
|
const moverOf = (b: string): string[] => keyed.filter((k) => under(b, k));
|
|
161
173
|
if (!bound.some((b) => moverOf(b).length > 0)) return;
|
|
174
|
+
const through = [...new Set(bound.flatMap((b) => moverOf(b).flatMap((k) => declared(b, k))))];
|
|
175
|
+
if (through.length > 0) {
|
|
176
|
+
stops.push({
|
|
177
|
+
term: `tracks[${name}].pairs[].theta`,
|
|
178
|
+
detail: `the track keys [${keyed.join(', ')}], and ${through.map((b) => `"${b}" declares inherit "${mode.get(b)}"`).join(', ')} below it, so what the key does to the bones under it is not the key's own rotation or scale; θ is composed here only through bones that inherit normally, and none is invented`,
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
162
182
|
const th = trackTheta(t.property, t.keys);
|
|
163
183
|
if ('refused' in th) {
|
|
164
184
|
stops.push({ term: `tracks[${name}].pairs[].theta`, detail: th.refused });
|
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, hiddenLines, SINGLE_RUN_FACE_TAGS, 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,15 +70,15 @@ 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';
|
|
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
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';
|
|
79
79
|
import type { PaletteError } from './palette.ts';
|
|
80
80
|
import { type LayerSet, readLayers } from './layers.ts';
|
|
81
|
-
import { type PartRecord, readParts, tagOf, writeParts } from './parts.ts';
|
|
81
|
+
import { type PartRecord, readParts, type SingleRunPad, tagOf, writeParts } from './parts.ts';
|
|
82
82
|
import { encodePngBytes, readPng, writePng } from './raster/png.ts';
|
|
83
83
|
import type { Raster } from './raster/types.ts';
|
|
84
84
|
import { readRequirements, type RequirementLine, summaryText } from './requirements.ts';
|
|
@@ -106,8 +106,12 @@ export function readSource(path: string): Raster {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Read both runs, collecting the refusals of both before throwing. With no
|
|
111
|
+
* head run (`head` null, issue #359) the full run is read once and stands for
|
|
112
|
+
* both, and `single` says so: the stage then holds it to `checkSingleRun`.
|
|
113
|
+
*/
|
|
114
|
+
export function readRuns(full: string, head: string | null): { full: LayerSet; head: LayerSet; single: boolean } {
|
|
111
115
|
const problems: Problem[] = [];
|
|
112
116
|
const one = (path: string): LayerSet | null => {
|
|
113
117
|
try {
|
|
@@ -121,15 +125,16 @@ export function readRuns(full: string, head: string): { full: LayerSet; head: La
|
|
|
121
125
|
}
|
|
122
126
|
};
|
|
123
127
|
const f = one(full);
|
|
124
|
-
const h = one(head);
|
|
128
|
+
const h = head === null ? f : one(head);
|
|
125
129
|
if (problems.length > 0) throw new PartsError(problems);
|
|
126
|
-
return { full: f as LayerSet, head: h as LayerSet };
|
|
130
|
+
return { full: f as LayerSet, head: h as LayerSet, single: head === null };
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
export interface AssembleStageInput {
|
|
130
134
|
source: string;
|
|
131
135
|
full: string;
|
|
132
|
-
head
|
|
136
|
+
/** The head run, or null for one run that holds the face (issue #359). */
|
|
137
|
+
head: string | null;
|
|
133
138
|
config: string;
|
|
134
139
|
seam: SeamRule;
|
|
135
140
|
project: ProjectRule;
|
|
@@ -200,15 +205,46 @@ export function readUnders(configPath: string, cuts: readonly Cut[]): Array<Rast
|
|
|
200
205
|
return out;
|
|
201
206
|
}
|
|
202
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Each limb segment's `cap` image (issue #349), read from its path relative to
|
|
210
|
+
* the config's directory and keyed `<limb>/<segment index>`. Every one is read
|
|
211
|
+
* before one refusal names them all: no such file
|
|
212
|
+
* (`ASSEMBLE_LIMB_CAP_PRESENT`), or a file that does not decode as a PNG
|
|
213
|
+
* (`ASSEMBLE_LIMB_CAP_DECODES`). The stage holds the image to the cap's box.
|
|
214
|
+
*/
|
|
215
|
+
export function readCaps(configPath: string, limbs: Readonly<Record<string, Limb>>): Map<string, Raster> {
|
|
216
|
+
const problems: Problem[] = [];
|
|
217
|
+
const out = new Map<string, Raster>();
|
|
218
|
+
for (const [l, limb] of Object.entries(limbs)) {
|
|
219
|
+
limb.segments.forEach((sg, k) => {
|
|
220
|
+
if (sg.cap === undefined) return;
|
|
221
|
+
const file = resolve(dirname(resolve(configPath)), sg.cap);
|
|
222
|
+
const object = `config.limbs.${l}.segments[${k}].cap (part "${sg.part}")`;
|
|
223
|
+
if (!existsSync(file) || !statSync(file).isFile()) {
|
|
224
|
+
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` });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
out.set(`${l}/${k}`, readPng(file));
|
|
229
|
+
} catch (err) {
|
|
230
|
+
problems.push({ code: 'ASSEMBLE_LIMB_CAP_DECODES', object, detail: `${file}: ${(err as Error).message.split('\n')[0]}; a PNG is required` });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
refuseIfAny(problems);
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
203
238
|
/** Read, assemble, and write only after every refusal has had its chance. Throws a PartsError on a refusal, having written nothing. */
|
|
204
239
|
export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs, log: Log): AssembleResult {
|
|
205
240
|
const src = readSource(input.source);
|
|
206
241
|
const runs = readRuns(input.full, input.head);
|
|
207
242
|
// The early door: assemble runs before propose has drafted bones, meshes,
|
|
208
243
|
// regions and motion, so it requires only what it reads (issue #20).
|
|
209
|
-
const fields = stageFields(loadEarlyConfig(input.config, 'assemble'));
|
|
244
|
+
const fields = stageFields(loadEarlyConfig(input.config, 'assemble'), runs.single ? { w: src.width, h: src.height } : null);
|
|
210
245
|
const under = readUnders(input.config, fields.cuts);
|
|
211
|
-
const
|
|
246
|
+
const caps = fields.limbs === undefined ? undefined : readCaps(input.config, fields.limbs);
|
|
247
|
+
const result = assemble({ source: src, full: runs.full, head: runs.head, ...fields, under, ...(caps === undefined ? {} : { caps }), seamRule: input.seam, projectRule: input.project });
|
|
212
248
|
// Emit only after green: every refusal above has already thrown.
|
|
213
249
|
mkdirSync(outs.partsDir, { recursive: true });
|
|
214
250
|
mkdirSync(dirname(outs.partsJson), { recursive: true });
|
|
@@ -223,6 +259,11 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
223
259
|
if (figurePath !== null) writePng(figurePath, result.figure as Raster);
|
|
224
260
|
const [W, H] = result.parts.rig_size;
|
|
225
261
|
log(`rig-parts assemble: ${result.images.length} part(s) on a ${W}x${H} rig (${result.parts.scale_rig_per_source} rig px per source px), seam rule ${result.seamRule}, projection rule ${result.projectRule}`);
|
|
262
|
+
// Issue #359: one run, said, so a reader of the lines knows no head run was placed.
|
|
263
|
+
if (runs.single) log(` one run: ${input.full} holds the face (${SINGLE_RUN_FACE_TAGS.join(', ')}), so no head run is read; every part comes from the full run`);
|
|
264
|
+
// Issue #366: a file that is not the resolution square was padded to it; said, with where parts.json records it. None for a square file.
|
|
265
|
+
const pad = result.parts.single_run_pad;
|
|
266
|
+
if (pad !== undefined) log(singleRunPadLine(input.full, pad));
|
|
226
267
|
for (const p of result.parts.parts) {
|
|
227
268
|
log(
|
|
228
269
|
` ${p.name.padEnd(11)} ${p.from.padEnd(16)} ${`${p.w}x${p.h}`.padEnd(9)} @${String(p.x).padStart(4)},${String(p.y).padStart(4)} ` +
|
|
@@ -234,7 +275,11 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
234
275
|
for (const l of authorPartLines(result.parts.parts)) log(l);
|
|
235
276
|
// One line per plan part an assemble.cuts entry cut, before the fringe lines: the cut is made first. None without cuts.
|
|
236
277
|
for (const l of cutLines(result.cuts)) log(l);
|
|
278
|
+
// Issue #349: one line per part a limb took, and one per joint with its cap's pixels. None without limbs.
|
|
279
|
+
for (const l of limbLines(result.limbs)) log(l);
|
|
237
280
|
for (const l of fringeLines(result.parts.parts)) log(l);
|
|
281
|
+
// Issue #359: one line per hidden-art layer and what read it. None without one.
|
|
282
|
+
for (const l of hiddenLines(result.hidden)) log(l);
|
|
238
283
|
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
284
|
const [op, vis, occ, taken, unproj] = (['opaque_px', 'visible_px', 'occluded_px', 'source_px_taken', 'visible_not_projected_px'] as const).map(total);
|
|
240
285
|
const pct = (n: number): string => `${((100 * n) / op).toFixed(1)}%`;
|
|
@@ -253,6 +298,16 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
253
298
|
return result;
|
|
254
299
|
}
|
|
255
300
|
|
|
301
|
+
/**
|
|
302
|
+
* The line `assemble` prints under `one run:` when the file was padded (issue
|
|
303
|
+
* #366): its size, the square and the four margins, and that `parts.json`'s
|
|
304
|
+
* `single_run_pad` records them.
|
|
305
|
+
*/
|
|
306
|
+
export function singleRunPadLine(file: string, pad: SingleRunPad): string {
|
|
307
|
+
const [w, h] = pad.file_size;
|
|
308
|
+
return ` padded: ${file} is ${w}x${h}, centred on the ${pad.side}x${pad.side} run square (left ${pad.left}, right ${pad.side - w - pad.left}, top ${pad.top}, bottom ${pad.side - h - pad.top}); parts.json single_run_pad records it, and every part's box is in the file's pixels times scale_rig_per_source`;
|
|
309
|
+
}
|
|
310
|
+
|
|
256
311
|
/**
|
|
257
312
|
* The scale the per-part line prints `drift=` with (issue #314): `refused_drift_px` counts the part's projection
|
|
258
313
|
* candidates (`projected_core_px`, the `src=` column's second figure) refused because See-through's colour sits more
|
|
@@ -1046,6 +1101,40 @@ export function foldsLines(line: FoldsLine): string[] {
|
|
|
1046
1101
|
return [head, ...line.folding.map((r) => ` folds ${r.line}${r.declared === null ? '' : ` — declared in may_fold: ${r.declared}`}`)];
|
|
1047
1102
|
}
|
|
1048
1103
|
|
|
1104
|
+
/**
|
|
1105
|
+
* The feet-level line (issue #348) as the console prints it: the status, what was read, the bar and whose it is, then
|
|
1106
|
+
* each foot on a line of its own — its bone, what that hangs from, how many frames posed it, and the worst turn with
|
|
1107
|
+
* the bone that was turned and the bones it came through. A SKIP prints its reason.
|
|
1108
|
+
*/
|
|
1109
|
+
export function levelLines(line: LevelLine): string[] {
|
|
1110
|
+
const physics = line.physics_left_out === 0 ? '' : `, its ${line.physics_left_out} physics constraint(s) left out`;
|
|
1111
|
+
const read = `${line.animations.length} animation(s) read by rigc render --folds --geometry at ${line.fps} fps${physics} (${line.animations.join(', ')})`;
|
|
1112
|
+
const head =
|
|
1113
|
+
line.status === 'SKIP'
|
|
1114
|
+
? `${LEVEL_LINE}: SKIP — ${String(line.reason)}; ${read}`
|
|
1115
|
+
: `${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}`;
|
|
1116
|
+
return [
|
|
1117
|
+
head,
|
|
1118
|
+
...line.feet.map((f) => {
|
|
1119
|
+
const at = `foot "${f.part}" on "${f.bone}" (hangs from "${f.hangs_from}"): ${f.status}, ${f.frames_posed} posed frame(s)`;
|
|
1120
|
+
if (f.worst === null) return `${at}, so nothing was measured`;
|
|
1121
|
+
const w = f.worst;
|
|
1122
|
+
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(', ')}`;
|
|
1123
|
+
}),
|
|
1124
|
+
];
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* The limb line (issue #349) as the console prints it: the status and the bar, then each limb part on a line of its
|
|
1129
|
+
* own — its limb, its bone, and on a FAIL every other bone it is bound to.
|
|
1130
|
+
*/
|
|
1131
|
+
export function limbCheckLines(line: LimbLine): string[] {
|
|
1132
|
+
return [
|
|
1133
|
+
`${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)`,
|
|
1134
|
+
...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(', ')}`}`),
|
|
1135
|
+
];
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1049
1138
|
/** 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
1139
|
export function requirementText(name: string, line: RequirementLine): string {
|
|
1051
1140
|
const rest = Object.entries(line).filter(([k]) => k !== 'status' && k !== 'reason');
|
|
@@ -1072,7 +1161,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1072
1161
|
if (r.idle === null) log(` loop: SKIP — ${fig.skipped?.loop ?? ''}`);
|
|
1073
1162
|
else log(` ${loopCheckLine(r.idle, fig.loop_max_diff, fig.loop_physics?.length ?? 0)}`);
|
|
1074
1163
|
if (r.loopPhysics !== null) log(` ${r.loopPhysics}`);
|
|
1075
|
-
const beside = besideIdleLine(r.besideIdle);
|
|
1164
|
+
const beside = besideIdleLine(r.besideIdle, r.figures[LEVEL_LINE] !== undefined);
|
|
1076
1165
|
if (beside !== null) log(` ${beside}`);
|
|
1077
1166
|
if (r.seamViewport === null || fig.seam_mean === null) log(` seam: SKIP — ${fig.skipped?.seam ?? ''}`);
|
|
1078
1167
|
else {
|
|
@@ -1086,6 +1175,12 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1086
1175
|
for (const name of JUDGEMENT_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1087
1176
|
const folds = fig[FOLD_LINE];
|
|
1088
1177
|
if (folds !== undefined) for (const l of foldsLines(folds)) log(` ${l}`);
|
|
1178
|
+
const level = fig[LEVEL_LINE];
|
|
1179
|
+
if (level !== undefined) for (const [i, l] of levelLines(level).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1180
|
+
const stress = fig[STRESS_LINE];
|
|
1181
|
+
if (stress !== undefined) log(` ${judgementLine(STRESS_LINE, stress)}`);
|
|
1182
|
+
const limbs = fig[LIMB_LINE];
|
|
1183
|
+
if (limbs !== undefined) for (const [i, l] of limbCheckLines(limbs).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1089
1184
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1090
1185
|
const vsSource = fig[SOURCE_LINE];
|
|
1091
1186
|
if (vsSource !== undefined) log(` ${judgementLine(SOURCE_LINE, vsSource)}`);
|
|
@@ -1348,7 +1443,8 @@ export interface BuildInput {
|
|
|
1348
1443
|
config: string;
|
|
1349
1444
|
source: string;
|
|
1350
1445
|
full: string;
|
|
1351
|
-
head
|
|
1446
|
+
/** The head run, or null for one run that holds the face (issue #359). */
|
|
1447
|
+
head: string | null;
|
|
1352
1448
|
out: string;
|
|
1353
1449
|
seam: SeamRule;
|
|
1354
1450
|
project: ProjectRule;
|