rig-parts 2.2.0 → 2.4.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/cli.ts +2 -0
- package/docs/AUTHORING.md +291 -6
- package/docs/STABILITY.md +59 -3
- package/package.json +2 -2
- package/src/assemble.ts +124 -30
- package/src/boundarycolour.ts +233 -0
- package/src/build.ts +24 -5
- package/src/check.ts +165 -6
- package/src/config.ts +49 -6
- package/src/editreceipt.ts +194 -0
- package/src/motionseampose.ts +3 -1
- package/src/partsource.ts +91 -0
- package/src/requirements.ts +64 -2
- package/src/rig.ts +31 -8
- package/src/setuppainting.ts +18 -3
package/src/build.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_SEAM_RULE } from './seamrule.ts';
|
|
2
2
|
import { SETUP_PAINTING_LINE, setupPaintingLines } from './setuppainting.ts';
|
|
3
|
+
import { boundaryColourLines } from './boundarycolour.ts';
|
|
3
4
|
import { writeMaterialSources } from './cutmaterialio.ts';
|
|
4
5
|
import { CUT_MATERIAL_LINE, CUT_MATERIAL_SOURCES, materialLines } from './cutmaterial.ts';
|
|
5
6
|
import { ART_JOINT_LINE, artJointLines } from './artjoints.ts';
|
|
@@ -55,7 +56,7 @@ import { createHash } from 'node:crypto';
|
|
|
55
56
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
56
57
|
import { dirname, join, resolve } from 'node:path';
|
|
57
58
|
import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
|
|
58
|
-
import { assemble, type AssembleResult, cutLines, type ExcludedCount, excludedLines, setExclusions, islandLines, setIslandSources, type IslandCount, hiddenLines, SINGLE_RUN_FACE_TAGS, singleRunHeadBox, softOcclusionLine, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeKindsLine, holeLines, measureRecomposite, type ProjectRule, recomposite, recompositeRecord, scenePhrase, type SeamRule, sourceInRig, stageFields, uncoveredLine } from './assemble.ts';
|
|
59
|
+
import { assemble, type AssembleResult, cutLines, type ExcludedCount, excludedLines, setExclusions, islandLines, setIslandSources, type IslandCount, hiddenLines, SINGLE_RUN_FACE_TAGS, singleRunHeadBox, softOcclusionLine, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeKindsLine, holeLines, measurePinholes, measureRecomposite, pinholeProblems, pinholesLine, type ProjectRule, recomposite, recompositeRecord, scenePhrase, type SeamRule, sourceInRig, stageFields, uncoveredLine } from './assemble.ts';
|
|
59
60
|
import { allocationClause, type Reducer, stageBClause, unboundedClause } from './automesh.ts';
|
|
60
61
|
import { residualClause } from './autoenvelope.ts';
|
|
61
62
|
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, prepareComparison, type ReplayCandidate, runComparison, runPreparedComparison, sourceSelfFailure, stimulusMotion, withStimulus } from './automotion.ts';
|
|
@@ -82,9 +83,11 @@ import {
|
|
|
82
83
|
splitSchedule,
|
|
83
84
|
untestedIntervals,
|
|
84
85
|
} from './autoreplay.ts';
|
|
85
|
-
import { besideIdleLine, checkSummaryLine, inputFileRecord, type InputFileRecord, CLOTH_LINE, type ClothLine, clothWarnText, figureOpeningLines, frameRuns, MOTION_SEAM_LINE, type MotionSeamLine, type MotionSeamPlant, motionSeamWarnText, motionJointContactLines, FOLD_LINE, type FoldsLine, LEVEL_LINE, type LevelLine, LIMB_LINE, type LimbLine, STRESS_LINE, SWEEP_LINE, type SweepLine, buildGateLines, causeLines, environmentLine, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, GATE_PROFILE, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, readCheckEnvironment, readWindBand, 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, WIND_BAND_LINE, windBandLines } from './check.ts';
|
|
86
|
+
import { besideIdleLine, checkSummaryLine, inputFileRecord, type InputFileRecord, CLOTH_LINE, type ClothLine, clothWarnText, figureOpeningLines, frameRuns, MOTION_SEAM_LINE, type MotionSeamLine, type MotionSeamPlant, motionSeamRequiredLine, motionSeamWarnText, motionJointContactLines, FOLD_LINE, type FoldsLine, LEVEL_LINE, type LevelLine, LIMB_LINE, type LimbLine, STRESS_LINE, SWEEP_LINE, type SweepLine, buildGateLines, causeLines, environmentLine, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, GATE_PROFILE, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, readCheckEnvironment, readWindBand, 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, WIND_BAND_LINE, windBandLines } from './check.ts';
|
|
86
87
|
import { type Cut, type Extend, type Limb, loadConfigAndAnimations, loadConfigWithBytes, loadEarlyConfig, type Patch, type PlanEntry } from './config.ts';
|
|
87
88
|
import { coreCompileGuideLine, failLine, PartsError, type Problem, refuseIfAny } from './errors.ts';
|
|
89
|
+
import { editedConfigImages, editedRunImages, missingReceipts, readEditorReceipts } from './editreceipt.ts';
|
|
90
|
+
import { untracedPartSources } from './partsource.ts';
|
|
88
91
|
import type { MeshQualityReport, MotionSchedule } from '@rig-c/core/mesh';
|
|
89
92
|
import { encodeGif } from './gif.ts';
|
|
90
93
|
import { CONTROL_SUFFIX, withAnimations } from './motion.ts';
|
|
@@ -98,7 +101,7 @@ import { type Mask, newMask } from './raster/types.ts';
|
|
|
98
101
|
import { type OtherLimbs, splitLines, capOwnershipLines, type SplitCount } from './assemble.ts';
|
|
99
102
|
import type { BoneEntry } from './config.ts';
|
|
100
103
|
import { OPAQUE_ALPHA_ABOVE } from './layers.ts';
|
|
101
|
-
import { readRequirements, type RequirementLine, summaryText } from './requirements.ts';
|
|
104
|
+
import { pinholesRequired, readRequirements, type RequirementLine, summaryText } from './requirements.ts';
|
|
102
105
|
import { type AutoMeshReport, buildRig, DEFAULT_IDLE_KEYS, type FaceAxis, type IdleKeys, type MeshReport, type RigCommand, rigJsonText, type RigOutput, type RigSpec, SETS_FILE, type StrayCleared, type TurnRow } from './rig.ts';
|
|
103
106
|
import { KEYPOINT_NAMES, type KeypointName, type PosePoints } from './skeleton.ts';
|
|
104
107
|
import { AUTHOR_NO_ROLE, isAuthorTag } from './tags.ts';
|
|
@@ -324,13 +327,21 @@ export function frontOrdered<T extends { record: { name: string } }>(items: read
|
|
|
324
327
|
|
|
325
328
|
/** Read, assemble, and write only after every refusal has had its chance. Throws a PartsError on a refusal, having written nothing. */
|
|
326
329
|
export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs, log: Log): AssembleResult {
|
|
327
|
-
const
|
|
330
|
+
const requirementsFile = input.requirements === undefined ? null : readRequirements(input.requirements);
|
|
331
|
+
const materialAsked = requirementsFile?.cut_material !== undefined;
|
|
332
|
+
// Issue #944: the pinhole bar is measured only when the requirements file declares it required.
|
|
333
|
+
const pinholesAsked = requirementsFile !== null && pinholesRequired(requirementsFile);
|
|
328
334
|
// The early door: assemble runs before propose has drafted bones, meshes,
|
|
329
335
|
// regions and motion, so it requires only what it reads (issue #20).
|
|
330
336
|
const cfg = loadEarlyConfig(input.config, 'assemble');
|
|
331
337
|
const seam = input.seam ?? cfg.assemble.seam ?? DEFAULT_SEAM_RULE;
|
|
332
338
|
const src = readSource(input.source);
|
|
333
|
-
const
|
|
339
|
+
const read = readRuns(input.full, input.head);
|
|
340
|
+
// Issue #946: every declared receipt is held to its file, and under `required` every edited image needs one, before anything is assembled.
|
|
341
|
+
const receipts = readEditorReceipts(cfg.assemble.editor_receipts, input.config);
|
|
342
|
+
// Issue #947: under `trace_parts`, a shape drawn in the config through a part's pixels is refused with them.
|
|
343
|
+
refuseIfAny([...receipts.problems, ...missingReceipts(receipts, [...editedRunImages(read), ...editedConfigImages(input.config, cfg.assemble.cuts ?? [], cfg.limbs)]), ...untracedPartSources(cfg)]);
|
|
344
|
+
const runs = applyTakenLayers(read, src, cfg, input.config);
|
|
334
345
|
for (const note of runs.notes) log(`note: ${note}`);
|
|
335
346
|
const fields = stageFields(cfg, runs.single ? { w: src.width, h: src.height } : null);
|
|
336
347
|
const under = readUnders(input.config, fields.cuts);
|
|
@@ -489,6 +500,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
489
500
|
if (invalidFocus.length > 0) { altProblems.push(...invalidFocus); continue; }
|
|
490
501
|
}
|
|
491
502
|
const altRuns = readRuns(alt.full, alt.head ?? null);
|
|
503
|
+
altProblems.push(...missingReceipts(receipts, [...editedRunImages(altRuns, `alternate set "${alt.name}" `), ...editedConfigImages(input.config, alt.cuts ?? [], undefined, `${alt.at}.cuts`)]));
|
|
492
504
|
const hidesSet = new Set(alt.hides ?? []);
|
|
493
505
|
const declaredSlots = alt.parts !== undefined ? alt.parts.filter((p) => !hidesSet.has(p)) : baseSlots.filter((p) => !hidesSet.has(p));
|
|
494
506
|
const coveredSet = new Set(declaredSlots);
|
|
@@ -759,6 +771,10 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
759
771
|
assetIndices.set(file, index);
|
|
760
772
|
}
|
|
761
773
|
});
|
|
774
|
+
// Issue #944: under pinholes.required, every pinhole of the base set's flat parts stack is refused by name, with the
|
|
775
|
+
// alternate sets' refusals, before anything is written.
|
|
776
|
+
const pinholes = pinholesAsked ? measurePinholes(sourceInRig(src, result.parts.rig_size[0], result.parts.rig_size[1]), result.images) : null;
|
|
777
|
+
if (pinholes !== null) altProblems.push(...pinholeProblems(pinholes, input.requirements as string));
|
|
762
778
|
refuseIfAny(altProblems);
|
|
763
779
|
if (altBuilt.length > 0) {
|
|
764
780
|
result.parts.alternates = altBuilt.map((b) => b.record);
|
|
@@ -829,6 +845,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
829
845
|
for (const l of holeLines(result.figures, scene)) log(l);
|
|
830
846
|
if (result.figures.uncoveredErrorPx > 0) log(holeKindsLine(result.holeKinds, scene));
|
|
831
847
|
for (const u of result.uncovered) log(uncoveredLine(u, scene));
|
|
848
|
+
log(pinholesLine(pinholes, altBuilt.map((b) => b.record.name)));
|
|
832
849
|
for (const d of result.drift) log(driftLine(d));
|
|
833
850
|
for (const d of result.softOcclusion ?? []) log(softOcclusionLine(d));
|
|
834
851
|
// Issue #588: per subset set, one line per limb part cut from a layer of more than one island, then (issue #587) one
|
|
@@ -1943,6 +1960,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1943
1960
|
const sweep = fig[SWEEP_LINE];
|
|
1944
1961
|
if (sweep !== undefined) for (const [i, l] of sweepCheckLines(sweep, r.sweepSteps).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1945
1962
|
for (const [i, l] of motionSeamLines(r.motionSeam).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1963
|
+
if (r.motionSeam.required !== undefined) log(` ${motionSeamRequiredLine(r.motionSeam.required)}`);
|
|
1946
1964
|
for (const [i, l] of figureOpeningLines(r.figureOpenings).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1947
1965
|
for (const [i, l] of clothLines(r.cloth).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1948
1966
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
@@ -1961,6 +1979,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1961
1979
|
if (artJoints !== undefined) for (const line of artJointLines(artJoints)) log(` ${line}`);
|
|
1962
1980
|
const setupPainting = fig[SETUP_PAINTING_LINE];
|
|
1963
1981
|
if (setupPainting !== undefined) for (const line of setupPaintingLines(setupPainting)) log(` ${line}`);
|
|
1982
|
+
for (const line of boundaryColourLines(r.boundaryColour)) log(` ${line}`);
|
|
1964
1983
|
if (r.requirements !== null) {
|
|
1965
1984
|
for (const [name, line] of Object.entries(r.requirements.lines)) log(` ${requirementText(name, line)}`);
|
|
1966
1985
|
log(` ${summaryText(r.requirements.summary)}`);
|
package/src/check.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { SETUP_PAINTING_LINE, UNDECLARED_SETUP_PAINTING_REASON, setupPaintingBou
|
|
|
2
2
|
import { readMaterialSources } from './cutmaterialio.ts';
|
|
3
3
|
import { CUT_MATERIAL_LINE, checkJointMaterial, type MaterialLine } from './cutmaterial.ts';
|
|
4
4
|
import { ART_JOINT_LINE, artJointLine, type ArtJointLine } from './artjoints.ts';
|
|
5
|
+
import { BOUNDARY_COLOUR_LINE, BOUNDARY_COLOUR_UNMEASURED_CODE, measureBoundaryColour, type BoundaryColourMeasured } from './boundarycolour.ts';
|
|
5
6
|
import { baseAnchorMoves, type AnchorRegion } from './baseanchor.ts';
|
|
6
7
|
/**
|
|
7
8
|
* The check stage: build a rig through rig-c, gate it under both
|
|
@@ -164,6 +165,7 @@ import {
|
|
|
164
165
|
resolveRequirements,
|
|
165
166
|
rigFacts,
|
|
166
167
|
placedArt,
|
|
168
|
+
seamOverMotionRequired,
|
|
167
169
|
type PosedAttachment,
|
|
168
170
|
resolveSeams,
|
|
169
171
|
type SeamFrame,
|
|
@@ -1927,7 +1929,7 @@ export interface SeamStateBounds {
|
|
|
1927
1929
|
* different API. Importing check, or constructing a root-free reader, loads none.
|
|
1928
1930
|
*/
|
|
1929
1931
|
type OfficialRenderer = typeof import('@rig-c/core/render-shared');
|
|
1930
|
-
function officialRenderer(): OfficialRenderer {
|
|
1932
|
+
export function officialRenderer(): OfficialRenderer {
|
|
1931
1933
|
const require = createRequire(import.meta.url);
|
|
1932
1934
|
const coreRoot = dirname(require.resolve('@rig-c/core/package.json'));
|
|
1933
1935
|
let runtimePresent = false;
|
|
@@ -2736,6 +2738,8 @@ export interface CheckFigures {
|
|
|
2736
2738
|
/** Under --source: the declared observation, or an explicit undeclared SKIP. */
|
|
2737
2739
|
[SETUP_PAINTING_LINE]?: SetupPaintingObservation;
|
|
2738
2740
|
[CUT_MATERIAL_LINE]?: MaterialLine;
|
|
2741
|
+
/** Issue #942: only when requirements declares boundary_colour; undeclared writes no key and prints a SKIP. */
|
|
2742
|
+
[BOUNDARY_COLOUR_LINE]?: BoundaryColourMeasured;
|
|
2739
2743
|
PASS: boolean;
|
|
2740
2744
|
}
|
|
2741
2745
|
|
|
@@ -2982,12 +2986,12 @@ export function barsNotMet(fig: Readonly<Record<string, unknown>>): string[] {
|
|
|
2982
2986
|
isRecord(fig.sets) &&
|
|
2983
2987
|
Object.values(fig.sets).some((s) => isRecord(s) && typeof s.seam_mean === 'number' && typeof s.seam_px_over_40 === 'number' && seamFails(s.seam_mean, s.seam_px_over_40, typeof s.seam_frame_scale === 'number' ? s.seam_frame_scale : 1));
|
|
2984
2988
|
if ((typeof fig.seam_mean === 'number' && typeof fig.seam_px_over_40 === 'number' && seamFails(fig.seam_mean, fig.seam_px_over_40, scale)) || setFailed) out.push('seam');
|
|
2985
|
-
for (const n of [...JUDGEMENT_LINES, FOLD_LINE, LEVEL_LINE, STRESS_LINE, LIMB_LINE, SWEEP_LINE, ART_JOINT_LINE, CUT_MATERIAL_LINE]) {
|
|
2989
|
+
for (const n of [...JUDGEMENT_LINES, FOLD_LINE, LEVEL_LINE, STRESS_LINE, LIMB_LINE, SWEEP_LINE, ART_JOINT_LINE, CUT_MATERIAL_LINE, BOUNDARY_COLOUR_LINE]) {
|
|
2986
2990
|
const l = fig[n];
|
|
2987
2991
|
if (typeof l === 'object' && l !== null && (l as { status?: unknown }).status === 'FAIL') out.push(n);
|
|
2988
2992
|
}
|
|
2989
2993
|
const motion = fig[MOTION_SEAM_LINE];
|
|
2990
|
-
if (isRecord(motion) && isRecord(motion.roots) && motion.roots.status === 'FAIL') out.push(MOTION_SEAM_LINE);
|
|
2994
|
+
if (isRecord(motion) && ((isRecord(motion.roots) && motion.roots.status === 'FAIL') || (isRecord(motion.required) && motion.required.status === 'FAIL'))) out.push(MOTION_SEAM_LINE);
|
|
2991
2995
|
return out;
|
|
2992
2996
|
}
|
|
2993
2997
|
|
|
@@ -3033,6 +3037,8 @@ export interface CheckReport {
|
|
|
3033
3037
|
motionSeam: MotionSeamLine;
|
|
3034
3038
|
/** Issue #933: openings inside the figure over motion on undeclared setup-overlapping layer pairs — a warning, never a bar. */
|
|
3035
3039
|
figureOpenings: FigureOpeningReading;
|
|
3040
|
+
/** Issue #942: the boundary colour-step reading; null when undeclared (the SKIP line is printed, nothing written). */
|
|
3041
|
+
boundaryColour: BoundaryColourMeasured | null;
|
|
3036
3042
|
/** `CLOTH_SETTLES` (issue #501): a warning, never a bar — in `check.json` only when the rig declares a physics constraint and has an idle; its SKIP is printed either way. */
|
|
3037
3043
|
cloth: ClothLine;
|
|
3038
3044
|
/** `WIND_BAND` (issue #590): the steady-wind band each declared seam pair holds, written under `--wind-band` only. */
|
|
@@ -3093,7 +3099,7 @@ export function checkStatuses(fig: CheckFigures): Record<string, CheckStatus> {
|
|
|
3093
3099
|
const bar = (name: string, measured: boolean): CheckStatus => (!measured ? 'SKIP' : notMet.includes(name) ? 'FAIL' : 'PASS');
|
|
3094
3100
|
const out: Record<string, CheckStatus> = { gate: bar('gate', true), loop: bar('loop', fig.loop_max_diff !== null), seam: bar('seam', fig.seam_mean !== null) };
|
|
3095
3101
|
for (const n of JUDGEMENT_LINES) out[n] = fig[n].status;
|
|
3096
|
-
for (const n of [FOLD_LINE, LEVEL_LINE, STRESS_LINE, LIMB_LINE, SWEEP_LINE, MOTION_SEAM_LINE, CLOTH_LINE, ...REPORTED_LINES, 'figure_openings', SOURCE_LINE, WIND_BAND_LINE, ART_JOINT_LINE, CUT_MATERIAL_LINE, SETUP_PAINTING_LINE] as const) {
|
|
3102
|
+
for (const n of [FOLD_LINE, LEVEL_LINE, STRESS_LINE, LIMB_LINE, SWEEP_LINE, MOTION_SEAM_LINE, CLOTH_LINE, ...REPORTED_LINES, 'figure_openings', SOURCE_LINE, WIND_BAND_LINE, ART_JOINT_LINE, CUT_MATERIAL_LINE, SETUP_PAINTING_LINE, BOUNDARY_COLOUR_LINE] as const) {
|
|
3097
3103
|
const l = fig[n];
|
|
3098
3104
|
if (l !== undefined) out[n] = l.status;
|
|
3099
3105
|
}
|
|
@@ -4420,6 +4426,139 @@ export interface MotionSeamLine {
|
|
|
4420
4426
|
worlds?: MotionSeamWorlds;
|
|
4421
4427
|
/** Selected body-root pairs and named zero-band roots; existing internal seams remain warnings. */
|
|
4422
4428
|
roots?: { status: 'PASS' | 'FAIL' | 'SKIP'; mode: 'gap-open+under-ghost'; ghost_owner: 'resolved-draw-order'; pairs: string[]; failed: number; reason?: string; unmeasured?: MotionUnmeasuredRoot[] };
|
|
4429
|
+
/** Issue #941: written only under the requirements file's `seam_over_motion.required: true` ({@link motionSeamRequired}). */
|
|
4430
|
+
required?: MotionSeamRequired;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
/** Issue #941: the refusal's code — background on a frame of a declared pair, or a pair or line not measured, under `seam_over_motion.required`. */
|
|
4434
|
+
export const MOTION_SEAM_REQUIRED_CODE = 'CHECK_SEAM_OVER_MOTION_REQUIRED';
|
|
4435
|
+
|
|
4436
|
+
/** The class a declared pair is named by in the required bar: a proven body root, a limb joint below segment 0, or a declaration (`seam` requirement, `motion_seams`). */
|
|
4437
|
+
export type MotionSeamClass = 'body-root' | 'internal-joint' | 'explicit';
|
|
4438
|
+
|
|
4439
|
+
/** Issue #941: one pair in one animation as the background bar read it — every frame of the set, by rigc's `gapOpen`. */
|
|
4440
|
+
export interface MotionBackgroundRow {
|
|
4441
|
+
pair: string;
|
|
4442
|
+
animation: string;
|
|
4443
|
+
/** Frames in the set: every frame `i / fps`, `i = 0..round(duration * fps)` (frames.json `sampled`, stride 1). */
|
|
4444
|
+
frames: number;
|
|
4445
|
+
status: 'read' | 'skip';
|
|
4446
|
+
drawn: number;
|
|
4447
|
+
banded: number;
|
|
4448
|
+
/** rigc's `gapOpen` hits with a cell: background inside the carried setup-overlap band, alpha 0 in the whole skeleton composite. */
|
|
4449
|
+
hits: Array<MotionSeamHit & { time: number }>;
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
/** Issue #941: `SEAM_OVER_MOTION`'s `required` block. The bar is 0 background cells on every frame read; it never SKIPs. */
|
|
4453
|
+
export interface MotionSeamRequired {
|
|
4454
|
+
status: 'PASS' | 'FAIL';
|
|
4455
|
+
mode: 'gap-open';
|
|
4456
|
+
bar_px: 0;
|
|
4457
|
+
/** Every declared pair, its class, and the frames read over every animation. */
|
|
4458
|
+
pairs: Array<{ pair: string; class: MotionSeamClass; frames: number; background_frames: number; max_px: number }>;
|
|
4459
|
+
/** How many refusals the bar added. */
|
|
4460
|
+
failed: number;
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
/** A declared pair's class: a proven root first, then a limb joint, otherwise its explicit declaration. */
|
|
4464
|
+
export function motionSeamClass(p: MotionSeamPair): MotionSeamClass {
|
|
4465
|
+
if (p.root !== undefined) return 'body-root';
|
|
4466
|
+
return p.declared.some((d) => d.startsWith('config.limbs.')) ? 'internal-joint' : 'explicit';
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
/**
|
|
4470
|
+
* Issue #941: every pair's `gapOpen` over every frame of one `rigc render --seam` run, beside {@link readMotionSeams}
|
|
4471
|
+
* (which keeps its own reading). Refuses (`FRAMES_SIDECAR`), every problem at once, a set that is not every frame at
|
|
4472
|
+
* the rate asked (stride 1, written = sampled), a file that is not the animation's or the rate's, a pair out of the
|
|
4473
|
+
* flag's order, and a `gapOpen` of the wrong shape — unavailable is never a measured zero.
|
|
4474
|
+
*/
|
|
4475
|
+
export function readMotionSeamBackground(dir: string, animations: readonly string[], fps: number, pairs: readonly MotionSeamPair[]): MotionBackgroundRow[] {
|
|
4476
|
+
const problems: Problem[] = [];
|
|
4477
|
+
const bad = (object: string, detail: string): void => { problems.push({ code: 'FRAMES_SIDECAR', object, detail }); };
|
|
4478
|
+
const sidecar = readJson(join(dir, 'frames.json'), problems);
|
|
4479
|
+
const sets = sidecar !== null && Array.isArray(sidecar.sets) ? sidecar.sets.filter(isRecord) : [];
|
|
4480
|
+
const rows: MotionBackgroundRow[] = [];
|
|
4481
|
+
for (const animation of animations) {
|
|
4482
|
+
const set = sets.find((x) => x.animation === animation);
|
|
4483
|
+
if (set === undefined || typeof set.dir !== 'string') { bad(join(dir, 'frames.json'), `names no frame set for animation ${JSON.stringify(animation)}; \`rigc render --seam\` over the build writes one per animation`); continue; }
|
|
4484
|
+
if (set.fps !== fps || set.stride !== 1 || typeof set.sampled !== 'number' || set.written !== set.sampled || !Number.isSafeInteger(set.sampled) || set.sampled < 1) {
|
|
4485
|
+
bad(join(dir, 'frames.json') + ' set ' + JSON.stringify(animation), `is fps ${JSON.stringify(set.fps)}, sampled ${JSON.stringify(set.sampled)}, written ${JSON.stringify(set.written)}, stride ${JSON.stringify(set.stride)}; every frame at ${fps} fps (stride 1, written = sampled) is required — a sample is not every frame`);
|
|
4486
|
+
continue;
|
|
4487
|
+
}
|
|
4488
|
+
const path = join(dir, set.dir, SEAM_READING_FILE);
|
|
4489
|
+
const file = readJson(path, problems);
|
|
4490
|
+
if (file === null) continue;
|
|
4491
|
+
if (file.spec !== SEAM_READING_SPEC || file.animation !== animation || file.fps !== fps) { bad(path, `is ${JSON.stringify(file.spec)} for ${JSON.stringify(file.animation)} at ${JSON.stringify(file.fps)} fps; "${SEAM_READING_SPEC}" for ${JSON.stringify(animation)} at ${fps} fps is required`); continue; }
|
|
4492
|
+
const read = Array.isArray(file.pairs) ? file.pairs : [];
|
|
4493
|
+
if (read.length !== pairs.length) bad(path, `holds ${read.length} pair(s); the ${pairs.length} asked for, in the flag's order, are required`);
|
|
4494
|
+
pairs.forEach((want, i) => {
|
|
4495
|
+
const p = read[i];
|
|
4496
|
+
const at = `${path} pairs[${i}]`;
|
|
4497
|
+
if (!isRecord(p) || p.a !== want.a || p.b !== want.b) { bad(at, `is ${JSON.stringify(isRecord(p) ? `${String(p.a)}|${String(p.b)}` : p)}; "${want.pair}", the flag's entry ${i}, is required`); return; }
|
|
4498
|
+
const open = p.gapOpen;
|
|
4499
|
+
const frames = isRecord(open) && Array.isArray(open.frames) ? open.frames : null;
|
|
4500
|
+
const hits = frames === null ? [] : frames.flatMap((h) => isRecord(h) && Number.isSafeInteger(h.index) && typeof h.index === 'number' && h.index >= 0 && h.index < (set.sampled as number) &&
|
|
4501
|
+
Number.isSafeInteger(h.px) && typeof h.px === 'number' && h.px > 0 && typeof h.cell === 'number' && h.cell > 0 && typeof h.time === 'number' && Number.isFinite(h.time) &&
|
|
4502
|
+
Array.isArray(h.box) && h.box.length === 4 && h.box.every((v) => typeof v === 'number' && Number.isFinite(v))
|
|
4503
|
+
? [{ index: h.index, px: h.px, cell: h.cell, time: h.time, box: [h.box[0], h.box[1], h.box[2], h.box[3]] as [number, number, number, number] }] : []);
|
|
4504
|
+
if ((p.status !== 'read' && p.status !== 'skip') || typeof p.drawn !== 'number' || typeof p.banded !== 'number' || frames === null || hits.length !== frames.length || new Set(hits.map((h) => h.index)).size !== hits.length) {
|
|
4505
|
+
bad(`${at} gapOpen`, 'requires { frames: [{ index, time, px > 0, cell, box }…] } on every pair, one hit per frame, as rigc-seam/1 writes it from rig-c 2.69.0 — unavailable is not zero background');
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
rows.push({ pair: want.pair, animation, frames: set.sampled as number, status: p.status, drawn: p.drawn, banded: p.banded, hits });
|
|
4509
|
+
});
|
|
4510
|
+
}
|
|
4511
|
+
refuseIfAny(problems);
|
|
4512
|
+
return rows;
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
/**
|
|
4516
|
+
* Issue #941: the bar `seam_over_motion.required: true` declares — 0 background cells (rigc's `gapOpen`: gap cells
|
|
4517
|
+
* inside the carried setup-overlap band where the whole skeleton composite has alpha 0) on every frame of every
|
|
4518
|
+
* animation, for every declared pair: body-root, internal-joint and explicit. No floor, no sample. A line or a pair
|
|
4519
|
+
* that measured nothing is refused too, since a SKIP is never a pass. Problems are pushed, one per pair and
|
|
4520
|
+
* animation (with its worst frame) and one per pair or line not measured; the block is returned.
|
|
4521
|
+
*/
|
|
4522
|
+
export function motionSeamRequired(line: MotionSeamLine, rows: readonly MotionBackgroundRow[], requirements: string, problems: Problem[]): MotionSeamRequired {
|
|
4523
|
+
const before = problems.length;
|
|
4524
|
+
const why = `${requirements} declares seam_over_motion.required: true, so 0 background px is required on every frame of every declared seam pair`;
|
|
4525
|
+
const refuse = (object: string, detail: string): void => { problems.push({ code: MOTION_SEAM_REQUIRED_CODE, object, detail }); };
|
|
4526
|
+
if (line.status === 'SKIP' && line.rows.length === 0 && line.pairs.every((p) => p.unread === undefined)) {
|
|
4527
|
+
refuse(MOTION_SEAM_LINE, `is SKIP — ${String(line.reason)}; nothing was measured, which is not a pass: ${why}. Declare the pairs (config.limbs, config.motion_seams, a --requirements seam) and an animation, or drop the declaration`);
|
|
4528
|
+
}
|
|
4529
|
+
const out: MotionSeamRequired['pairs'] = [];
|
|
4530
|
+
for (const p of line.pairs) {
|
|
4531
|
+
const cls = motionSeamClass(p);
|
|
4532
|
+
const label = `${cls} seam "${p.pair}"`;
|
|
4533
|
+
let frames = 0;
|
|
4534
|
+
let open = 0;
|
|
4535
|
+
let max = 0;
|
|
4536
|
+
if (p.unread !== undefined) refuse(label, `is not read — ${p.unread}; ${why}`);
|
|
4537
|
+
else for (const animation of line.animations) {
|
|
4538
|
+
const r = rows.find((x) => x.pair === p.pair && x.animation === animation);
|
|
4539
|
+
if (r === undefined || r.status !== 'read' || r.banded === 0) {
|
|
4540
|
+
refuse(`${label} in "${animation}"`, `${r === undefined ? 'has no returned reading' : `rigc read no band on any of the ${r.drawn} frame(s) both slots draw (status ${r.status})`}; not measured is not a pass: ${why}`);
|
|
4541
|
+
continue;
|
|
4542
|
+
}
|
|
4543
|
+
frames += r.frames;
|
|
4544
|
+
open += r.hits.length;
|
|
4545
|
+
if (r.hits.length === 0) continue;
|
|
4546
|
+
const worst = r.hits.reduce((b, h) => (h.px > b.px || (h.px === b.px && h.index < b.index) ? h : b));
|
|
4547
|
+
max = Math.max(max, worst.px);
|
|
4548
|
+
refuse(`${label} in "${animation}" at frame ${worst.index} (t = ${worst.time}s)`,
|
|
4549
|
+
`background on ${r.hits.length} of ${r.frames} frame(s) read ${frameRuns(r.hits.map((h) => h.index))}: found ${worst.px} px (rigc gapOpen cells of ${worst.cell} world units, world box [${worst.box.join(', ')}]) on the worst frame, 0 required; ${why}`);
|
|
4550
|
+
}
|
|
4551
|
+
out.push({ pair: p.pair, class: cls, frames, background_frames: open, max_px: max });
|
|
4552
|
+
}
|
|
4553
|
+
const failed = problems.length - before;
|
|
4554
|
+
return { status: failed === 0 ? 'PASS' : 'FAIL', mode: 'gap-open', bar_px: 0, pairs: out, failed };
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
/** Issue #941: the console line for the required block. */
|
|
4558
|
+
export function motionSeamRequiredLine(r: MotionSeamRequired): string {
|
|
4559
|
+
const frames = r.pairs.reduce((n, p) => n + p.frames, 0);
|
|
4560
|
+
const classes = (['body-root', 'internal-joint', 'explicit'] as const).map((c) => `${r.pairs.filter((p) => p.class === c).length} ${c}`).join(', ');
|
|
4561
|
+
return `${MOTION_SEAM_LINE} required: ${r.status} — ${r.pairs.length} declared pair(s) (${classes}), ${frames} pair-frame(s) read, every frame of every animation: ${r.failed === 0 ? '0 background px on every frame' : `${r.failed} refusal(s)`}, bar 0 px (rigc gapOpen, no floor; seam_over_motion.required: true)`;
|
|
4423
4562
|
}
|
|
4424
4563
|
|
|
4425
4564
|
/**
|
|
@@ -6285,6 +6424,11 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6285
6424
|
refuseIfAny(setupPaintingBounds(file.setup_painting as SetupPainting, ready.stage.width, ready.stage.height));
|
|
6286
6425
|
if (ready.source === null) refuseIfAny([{ code: 'CHECK_SETUP_PAINTING_SOURCE', object: 'requirements.setup_painting', detail: 'requires --source naming the painting; the assembled composite is not an independent source painting' }]);
|
|
6287
6426
|
});
|
|
6427
|
+
// Issue #942: a declared boundary_colour reads parts.json's pieces and the idle; without either it cannot be taken.
|
|
6428
|
+
if (file.boundary_colour !== undefined) take(() => refuseIfAny([
|
|
6429
|
+
...(ready.parts === null ? [{ code: BOUNDARY_COLOUR_UNMEASURED_CODE, object: 'requirements.boundary_colour', detail: `requires parts.json and its parts/ PNGs, the rig's own pieces the step is measured against: ${ready.noParts ?? 'none was read'}; an untaken reading is never a pass` }] : []),
|
|
6430
|
+
...(ready.idleDuration === null ? [{ code: BOUNDARY_COLOUR_UNMEASURED_CODE, object: 'requirements.boundary_colour', detail: `requires an idle animation, whose every frame is measured: ${ready.noIdle ?? 'none was read'}; an untaken reading is never a pass` }] : []),
|
|
6431
|
+
]));
|
|
6288
6432
|
take(() => refuseIfAny(exceptionProblems(file, ready.parts, ready.noParts)));
|
|
6289
6433
|
// A seam's pairs are read off parts.json's art (issue #111); both resolvers' problems are named in one refusal.
|
|
6290
6434
|
if (file.requirements.some((r) => r.kind === 'seam')) {
|
|
@@ -6710,6 +6854,9 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6710
6854
|
// Read the build as played at the idle's rate; unavailable data is named SKIP.
|
|
6711
6855
|
let motionSeam: MotionSeamLine;
|
|
6712
6856
|
const motionRootProblems: Problem[] = [];
|
|
6857
|
+
// Issue #941: under seam_over_motion.required, the same render's gapOpen on every frame of every declared pair.
|
|
6858
|
+
const seamRequired = seamOverMotionRequired(reqFile);
|
|
6859
|
+
let backgroundRows: MotionBackgroundRow[] = [];
|
|
6713
6860
|
{
|
|
6714
6861
|
const fps = IDLE_FPS;
|
|
6715
6862
|
const seamReqs = reqFile === null || seams === null ? [] : reqFile.requirements.flatMap((r) => (r.kind === 'seam' && seams?.has(r.name) === true ? [{ name: r.name, part: r.part, neighbour: r.neighbour }] : []));
|
|
@@ -6773,6 +6920,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6773
6920
|
if (r.status !== 0) refuseIfAny(rigcFailed(`render --seam ${list} --fps ${fps}`, r, r.out.split('\n').filter((l) => l.includes('FAIL') || l.includes('rigc:'))));
|
|
6774
6921
|
const activeWorldRows: MotionSeamWorldRow[] = [];
|
|
6775
6922
|
motionSeam = motionSeamLine(pairs, animationNames, fps, readMotionSeams(activeDest, animationNames, fps, read, {}, stripped !== null && env === null ? activeWorldRows : undefined, hasRoots ? motionRootDrawOrderReader(activeDest) : undefined), undefined, motionRootProblems, unmeasuredRoots);
|
|
6923
|
+
if (seamRequired) backgroundRows = readMotionSeamBackground(activeDest, animationNames, fps, read);
|
|
6776
6924
|
enrichMotionSeamPoses(activeDest, inp.rig, inp.parts, pairs, motionSeam.rows);
|
|
6777
6925
|
if (stripped !== null) {
|
|
6778
6926
|
const response = environmentResponse(readFileSync(join(buildDir, 'skeleton.json'), 'utf8'));
|
|
@@ -6800,6 +6948,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6800
6948
|
}
|
|
6801
6949
|
}
|
|
6802
6950
|
}
|
|
6951
|
+
if (seamRequired && reqFile !== null) motionSeam.required = motionSeamRequired(motionSeam, backgroundRows, reqFile.path, motionRootProblems);
|
|
6803
6952
|
const declaredPairKeys = new Set(motionSeam.pairs.map((p) => [p.a, p.b].sort().join('|')));
|
|
6804
6953
|
const figureOpenings: FigureOpeningReading =
|
|
6805
6954
|
inp.parts === null
|
|
@@ -7031,6 +7180,11 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
7031
7180
|
}
|
|
7032
7181
|
}
|
|
7033
7182
|
|
|
7183
|
+
// Issue #942: the boundary colour step against the rig's own pieces, on the gated build, only when declared.
|
|
7184
|
+
const boundaryDeclared = reqFile?.boundary_colour;
|
|
7185
|
+
const boundaryColour = boundaryDeclared === undefined || inp.parts === null || inp.idleDuration === null ? null
|
|
7186
|
+
: measureBoundaryColour(officialRenderer(), { build: buildDir, stage: inp.stage, parts: inp.parts, partsDir: inp.partsDir, imagesDir: typeof inp.rig.images === 'string' ? resolve(inp.rigDir, inp.rig.images) : join(inp.rigDir, 'images'), rig: inp.rig, fps: IDLE_FPS }, boundaryDeclared, problems);
|
|
7187
|
+
|
|
7034
7188
|
// 5. the scene's declared requirements (issue #93), only under --requirements
|
|
7035
7189
|
const reqProblems: Problem[] = [];
|
|
7036
7190
|
const reqBlock = reqFile === null || reqRecord === null ? null : { file: reqRecord, ...measureRequirements(inp, reqFile, buildDir, out, rigc, reqProblems, seams ?? new Map(), measured) };
|
|
@@ -7063,7 +7217,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
7063
7217
|
...(stress === null ? {} : { [STRESS_LINE]: stress }),
|
|
7064
7218
|
...(limbs === null ? {} : { [LIMB_LINE]: limbs }),
|
|
7065
7219
|
...(sweep === null ? {} : { [SWEEP_LINE]: sweep }),
|
|
7066
|
-
...(motionSeam.pairs.length === 0 && motionSeam.roots === undefined ? {} : { [MOTION_SEAM_LINE]: motionSeam }),
|
|
7220
|
+
...(motionSeam.pairs.length === 0 && motionSeam.roots === undefined && motionSeam.required === undefined ? {} : { [MOTION_SEAM_LINE]: motionSeam }),
|
|
7067
7221
|
...(stripped === null || idle === null ? {} : { [CLOTH_LINE]: cloth }),
|
|
7068
7222
|
RECOMPOSITE_HOLES: inp.parts === null ? { status: 'SKIP', reason: inp.noParts ?? '' } : holesLine(inp.parts),
|
|
7069
7223
|
...(figureOpenings.status === 'SKIP' ? {} : { figure_openings: figureOpenings }),
|
|
@@ -7074,6 +7228,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
7074
7228
|
...(config === undefined ? {} : { config }),
|
|
7075
7229
|
...(artJoints === null ? {} : { [ART_JOINT_LINE]: artJoints }),
|
|
7076
7230
|
...(setupPainting === null ? {} : { [SETUP_PAINTING_LINE]: setupPainting }),
|
|
7231
|
+
...(boundaryColour === null ? {} : { [BOUNDARY_COLOUR_LINE]: boundaryColour }),
|
|
7077
7232
|
...(cutMaterial === null ? {} : { [CUT_MATERIAL_LINE]: cutMaterial }),
|
|
7078
7233
|
...(reqBlock === null ? {} : { requirements: reqBlock }),
|
|
7079
7234
|
PASS: false,
|
|
@@ -7141,6 +7296,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
7141
7296
|
if (cutMaterial !== null) tally(CUT_MATERIAL_LINE, null);
|
|
7142
7297
|
if (artJoints !== null) tally(ART_JOINT_LINE, artJoints.status === 'SKIP' ? (artJoints.reason ?? 'no declared limit') : null);
|
|
7143
7298
|
if (setupPainting !== null) tally(SETUP_PAINTING_LINE, setupPainting.status === 'SKIP' ? (setupPainting.reason ?? 'no declared limit') : null);
|
|
7299
|
+
if (boundaryColour !== null) tally(BOUNDARY_COLOUR_LINE, null);
|
|
7144
7300
|
// Issue #348: the level bar where it is written; a line whose every foot no frame posed says SKIP and is not measured.
|
|
7145
7301
|
if (level !== null) tally(LEVEL_LINE, level.status === 'SKIP' ? (level.reason ?? '') : null);
|
|
7146
7302
|
// Issue #347: the stress bar is counted where it is written — on every rig with a handwear part and an idle.
|
|
@@ -7149,7 +7305,9 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
7149
7305
|
if (limbs !== null) tally(LIMB_LINE, null);
|
|
7150
7306
|
// Issue #360: the range bar where it is written; a joint it cannot read fails, so it always measures.
|
|
7151
7307
|
if (sweep !== null) tally(SWEEP_LINE, null);
|
|
7152
|
-
|
|
7308
|
+
// Issue #941: the required bar never SKIPs (an unmeasured line or pair is its refusal), so it counts the line measured.
|
|
7309
|
+
if (motionSeam.required !== undefined) tally(MOTION_SEAM_LINE, null);
|
|
7310
|
+
else if (motionSeam.roots !== undefined) tally(MOTION_SEAM_LINE, motionSeam.roots.status === 'SKIP' ? (motionSeam.roots.reason ?? 'no body-root reading') : null);
|
|
7153
7311
|
return {
|
|
7154
7312
|
figures,
|
|
7155
7313
|
gateHtml,
|
|
@@ -7167,6 +7325,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
7167
7325
|
motionSeam,
|
|
7168
7326
|
figureOpenings,
|
|
7169
7327
|
cloth,
|
|
7328
|
+
boundaryColour,
|
|
7170
7329
|
...(sweepSteps.size === 0 ? {} : { sweepSteps }),
|
|
7171
7330
|
...(windBandRead === null ? {} : { windBand: windBandRead }),
|
|
7172
7331
|
};
|
package/src/config.ts
CHANGED
|
@@ -100,6 +100,7 @@ import { GRID, MAX_SIDE } from './contour.ts';
|
|
|
100
100
|
import { type Problem, refuseIfAny } from './errors.ts';
|
|
101
101
|
import { edgeLength } from './length.ts';
|
|
102
102
|
import { setFocusProblems } from './setfocus.ts';
|
|
103
|
+
import { type EditorReceipts, SHA256_PATTERN, SHA256_RULE } from './editreceipt.ts';
|
|
103
104
|
import { isSkeletonName, KEYPOINT_NAMES, type KeypointName, type PosePoints, scaledPoints, SKELETON_NAMES, type SkeletonName } from './skeleton.ts';
|
|
104
105
|
import { acceptedTagNames, AUTHOR_LABEL_RULE, AUTHOR_PREFIX, readLayerTag } from './tags.ts';
|
|
105
106
|
import { BODYSHIFT_BONE, BODY_REAR_SHIFT_BONE, BODY_YAW_DIAL_BONE, EAR_DEPTH_RANGE, FACESHIFT_BONE, FACE_DEFORM_FRACTIONS, FACE_DEFORM_RANGES, FACE_RATED_PITCH_BAND, FACE_RATED_YAW_BAND, type FaceDeformKey, PITCH_DIAL_BONE, REAR_SHIFT_BONE, TURN_BONE, TURN_ROLES, type TurnRole, type TurnSpec, YAW_DIAL_BONE, turnNearest, turnSurfaceBone, turnUnitBone } from './turn.ts';
|
|
@@ -305,6 +306,8 @@ export interface Assemble {
|
|
|
305
306
|
seam?: SeamRule;
|
|
306
307
|
/** Verified named replacements from another See-through run (#866). */
|
|
307
308
|
take?: TakeLayers[];
|
|
309
|
+
/** Issue #946: receipts for images edited in an image editor; absent, no file is read and no byte moves. */
|
|
310
|
+
editor_receipts?: EditorReceipts;
|
|
308
311
|
/** Rig pixels per source pixel. */
|
|
309
312
|
rig_scale: number;
|
|
310
313
|
plan: PlanEntry[];
|
|
@@ -1305,8 +1308,6 @@ export interface CharacterConfig {
|
|
|
1305
1308
|
cloth?: ClothRecipes;
|
|
1306
1309
|
/** The `assemble.rig_scale` (`parts.json`'s `scale_rig_per_source`) `propose` drafted the rig sections at (issue #354): absent on a hand-authored config, held equal to `assemble.rig_scale` when present. */
|
|
1307
1310
|
proposed_rig_scale?: number;
|
|
1308
|
-
/** Alternate painting sets (issue #454): absent, no alternate sets are assembled or emitted. */
|
|
1309
|
-
alternates?: AlternateSetConfig[];
|
|
1310
1311
|
/**
|
|
1311
1312
|
* The requirements file the build's check reads (issue #556), a path relative
|
|
1312
1313
|
* to the config's directory: `build` reads it when no `--requirements` is
|
|
@@ -1625,7 +1626,7 @@ export interface EarlyConfig {
|
|
|
1625
1626
|
depth?: DepthConfig;
|
|
1626
1627
|
key: string;
|
|
1627
1628
|
seethrough: SeeThrough;
|
|
1628
|
-
assemble: { rig_scale: number; seam?: SeamRule; take?: TakeLayers[]; head_face_underfill?: boolean; yield_refused?: boolean; keep_under_occluders?: string[]; extend_painting?: PaintingExtension[] };
|
|
1629
|
+
assemble: { rig_scale: number; seam?: SeamRule; take?: TakeLayers[]; editor_receipts?: EditorReceipts; head_face_underfill?: boolean; yield_refused?: boolean; keep_under_occluders?: string[]; extend_painting?: PaintingExtension[] };
|
|
1629
1630
|
}
|
|
1630
1631
|
|
|
1631
1632
|
/** What plain `assemble` reads: the early config plus the plan `--propose-plan` printed. */
|
|
@@ -1649,7 +1650,14 @@ export interface AssembleConfig {
|
|
|
1649
1650
|
proposed_rig_scale?: number;
|
|
1650
1651
|
}
|
|
1651
1652
|
|
|
1652
|
-
|
|
1653
|
+
/**
|
|
1654
|
+
* The top-level sections `parseConfig` accepts in `config.json` (issue #948),
|
|
1655
|
+
* exported as a runtime tuple so a consumer does not read `src/config.ts` as text.
|
|
1656
|
+
*/
|
|
1657
|
+
export const CONFIG_SECTIONS = ['key', 'generation', 'seethrough', 'assemble', 'limbs', 'bones', 'meshes', 'regions', 'motion', 'constraints', 'stops', 'turn', 'scene', 'cloth', 'proposed_rig_scale', 'requirements', 'motion_seams', 'depth', 'waist'] as const;
|
|
1658
|
+
export type ConfigSection = (typeof CONFIG_SECTIONS)[number];
|
|
1659
|
+
|
|
1660
|
+
const TOP_KEYS = CONFIG_SECTIONS;
|
|
1653
1661
|
|
|
1654
1662
|
/**
|
|
1655
1663
|
* The one partial entry point. It validates exactly what its door requires —
|
|
@@ -1694,7 +1702,7 @@ function parseEarlyConfigFrom(raw: Json, door: EarlyDoor, base: string | null):
|
|
|
1694
1702
|
if (door === 'layers') {
|
|
1695
1703
|
if ('seethrough' in t) checkSeeThrough(c, t.seethrough);
|
|
1696
1704
|
if ('assemble' in t) {
|
|
1697
|
-
const a = c.object('config.assemble', t.assemble, sectionRequired(door, 'assemble'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam']);
|
|
1705
|
+
const a = c.object('config.assemble', t.assemble, sectionRequired(door, 'assemble'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam', 'editor_receipts']);
|
|
1698
1706
|
if (a !== null) {
|
|
1699
1707
|
checkSeam(c, a);
|
|
1700
1708
|
if ('rig_scale' in a) c.number('config.assemble.rig_scale', a.rig_scale, 'positive');
|
|
@@ -1704,6 +1712,7 @@ function parseEarlyConfigFrom(raw: Json, door: EarlyDoor, base: string | null):
|
|
|
1704
1712
|
checkKeepUnderOccluders(c, a, null);
|
|
1705
1713
|
if ('extend_painting' in a) checkPaintingExtensions(c, a.extend_painting);
|
|
1706
1714
|
if ('take' in a) checkTakeLayers(c, a.take);
|
|
1715
|
+
if ('editor_receipts' in a) checkEditorReceipts(c, a.editor_receipts);
|
|
1707
1716
|
if ('yield_refused' in a && typeof a.yield_refused !== 'boolean') {
|
|
1708
1717
|
c.fail('CONFIG_FIELD_TYPE', 'config.assemble.yield_refused', `is ${show(a.yield_refused)}; true or false is required`);
|
|
1709
1718
|
}
|
|
@@ -2461,6 +2470,39 @@ function checkTakeLayers(c: Check, value: Json): void {
|
|
|
2461
2470
|
});
|
|
2462
2471
|
}
|
|
2463
2472
|
|
|
2473
|
+
/**
|
|
2474
|
+
* `config.assemble.editor_receipts` (issue #946): `{receipts, required?, trace_parts?}` (`trace_parts`, issue #947).
|
|
2475
|
+
* Shape refusals are the loader's own; a receipt whose `from_sha256` equals
|
|
2476
|
+
* its `sha256` (no edit), or a `file` receipted twice, is `CONFIG_EDITOR_RECEIPT`.
|
|
2477
|
+
* Whether the file exists and hashes to `sha256` is read at assemble
|
|
2478
|
+
* (`src/editreceipt.ts`), where the file is.
|
|
2479
|
+
*/
|
|
2480
|
+
function checkEditorReceipts(c: Check, value: Json): void {
|
|
2481
|
+
const at = 'config.assemble.editor_receipts';
|
|
2482
|
+
const o = c.object(at, value, ['receipts'], ['required', 'trace_parts']);
|
|
2483
|
+
if (o === null) return;
|
|
2484
|
+
for (const k of ['required', 'trace_parts'] as const) {
|
|
2485
|
+
if (k in o && typeof o[k] !== 'boolean') c.fail('CONFIG_FIELD_TYPE', `${at}.${k}`, `is ${show(o[k])}; true or false is required`);
|
|
2486
|
+
}
|
|
2487
|
+
if (!('receipts' in o) || !c.array(`${at}.receipts`, o.receipts, false)) return;
|
|
2488
|
+
const seen = new Map<string, number>();
|
|
2489
|
+
(o.receipts as Json[]).forEach((v, i) => {
|
|
2490
|
+
const p = `${at}.receipts[${i}]`;
|
|
2491
|
+
const r = c.object(p, v, ['file', 'sha256', 'from_sha256', 'editor'], []);
|
|
2492
|
+
if (r === null) return;
|
|
2493
|
+
if ('file' in r && c.string(`${p}.file`, r.file)) {
|
|
2494
|
+
const file = r.file as string;
|
|
2495
|
+
if (seen.has(file)) c.fail('CONFIG_EDITOR_RECEIPT', `${p}.file`, `names "${file}" again (first at receipts[${seen.get(file)}]); one receipt per file is required`);
|
|
2496
|
+
else seen.set(file, i);
|
|
2497
|
+
}
|
|
2498
|
+
for (const k of ['sha256', 'from_sha256'] as const) {
|
|
2499
|
+
if (k in r && (typeof r[k] !== 'string' || !SHA256_PATTERN.test(r[k] as string))) c.fail('CONFIG_FIELD_TYPE', `${p}.${k}`, `is ${show(r[k])}; ${SHA256_RULE} is required`);
|
|
2500
|
+
}
|
|
2501
|
+
if ('editor' in r) c.string(`${p}.editor`, r.editor);
|
|
2502
|
+
if (typeof r.sha256 === 'string' && r.sha256 === r.from_sha256) c.fail('CONFIG_EDITOR_RECEIPT', `${p}.from_sha256`, `is ${r.from_sha256}, the receipt's own sha256; the bytes the edit started from are required, and an edit that left the bytes unchanged is not an edit`);
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2464
2506
|
function checkSeam(c: Check, a: Record<string, Json>): void {
|
|
2465
2507
|
if ('seam' in a && !(SEAM_RULES as readonly unknown[]).includes(a.seam)) {
|
|
2466
2508
|
c.fail('CONFIG_FIELD_TYPE', 'config.assemble.seam', `is ${show(a.seam)}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
@@ -2469,7 +2511,7 @@ function checkSeam(c: Check, a: Record<string, Json>): void {
|
|
|
2469
2511
|
|
|
2470
2512
|
function checkAssemble(c: Check, v: Json, door: 'assemble' | 'full'): { parts: string[]; patches: string[]; cuts: string[] } {
|
|
2471
2513
|
const required = sectionRequired(door, 'assemble');
|
|
2472
|
-
const a = c.object('config.assemble', v, required.filter((k) => k !== 'plan'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam']);
|
|
2514
|
+
const a = c.object('config.assemble', v, required.filter((k) => k !== 'plan'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam', 'editor_receipts']);
|
|
2473
2515
|
if (a === null) return { parts: [], patches: [], cuts: [] };
|
|
2474
2516
|
checkSeam(c, a);
|
|
2475
2517
|
if (required.includes('plan') && !('plan' in a)) {
|
|
@@ -2507,6 +2549,7 @@ function checkAssemble(c: Check, v: Json, door: 'assemble' | 'full'): { parts: s
|
|
|
2507
2549
|
checkKeepUnderOccluders(c, a, parts);
|
|
2508
2550
|
if ('extend_painting' in a) checkPaintingExtensions(c, a.extend_painting);
|
|
2509
2551
|
if ('take' in a) checkTakeLayers(c, a.take);
|
|
2552
|
+
if ('editor_receipts' in a) checkEditorReceipts(c, a.editor_receipts);
|
|
2510
2553
|
if ('extend_below_crop' in a && c.array(`${p}.extend_below_crop`, a.extend_below_crop, false)) {
|
|
2511
2554
|
(a.extend_below_crop as Json[]).forEach((entry, i) => {
|
|
2512
2555
|
const at = `${p}.extend_below_crop[${i}]`;
|