rig-parts 2.3.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 +41 -3
- 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 +25 -1
- 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[];
|
|
@@ -1623,7 +1626,7 @@ export interface EarlyConfig {
|
|
|
1623
1626
|
depth?: DepthConfig;
|
|
1624
1627
|
key: string;
|
|
1625
1628
|
seethrough: SeeThrough;
|
|
1626
|
-
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[] };
|
|
1627
1630
|
}
|
|
1628
1631
|
|
|
1629
1632
|
/** What plain `assemble` reads: the early config plus the plan `--propose-plan` printed. */
|
|
@@ -1699,7 +1702,7 @@ function parseEarlyConfigFrom(raw: Json, door: EarlyDoor, base: string | null):
|
|
|
1699
1702
|
if (door === 'layers') {
|
|
1700
1703
|
if ('seethrough' in t) checkSeeThrough(c, t.seethrough);
|
|
1701
1704
|
if ('assemble' in t) {
|
|
1702
|
-
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']);
|
|
1703
1706
|
if (a !== null) {
|
|
1704
1707
|
checkSeam(c, a);
|
|
1705
1708
|
if ('rig_scale' in a) c.number('config.assemble.rig_scale', a.rig_scale, 'positive');
|
|
@@ -1709,6 +1712,7 @@ function parseEarlyConfigFrom(raw: Json, door: EarlyDoor, base: string | null):
|
|
|
1709
1712
|
checkKeepUnderOccluders(c, a, null);
|
|
1710
1713
|
if ('extend_painting' in a) checkPaintingExtensions(c, a.extend_painting);
|
|
1711
1714
|
if ('take' in a) checkTakeLayers(c, a.take);
|
|
1715
|
+
if ('editor_receipts' in a) checkEditorReceipts(c, a.editor_receipts);
|
|
1712
1716
|
if ('yield_refused' in a && typeof a.yield_refused !== 'boolean') {
|
|
1713
1717
|
c.fail('CONFIG_FIELD_TYPE', 'config.assemble.yield_refused', `is ${show(a.yield_refused)}; true or false is required`);
|
|
1714
1718
|
}
|
|
@@ -2466,6 +2470,39 @@ function checkTakeLayers(c: Check, value: Json): void {
|
|
|
2466
2470
|
});
|
|
2467
2471
|
}
|
|
2468
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
|
+
|
|
2469
2506
|
function checkSeam(c: Check, a: Record<string, Json>): void {
|
|
2470
2507
|
if ('seam' in a && !(SEAM_RULES as readonly unknown[]).includes(a.seam)) {
|
|
2471
2508
|
c.fail('CONFIG_FIELD_TYPE', 'config.assemble.seam', `is ${show(a.seam)}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
@@ -2474,7 +2511,7 @@ function checkSeam(c: Check, a: Record<string, Json>): void {
|
|
|
2474
2511
|
|
|
2475
2512
|
function checkAssemble(c: Check, v: Json, door: 'assemble' | 'full'): { parts: string[]; patches: string[]; cuts: string[] } {
|
|
2476
2513
|
const required = sectionRequired(door, 'assemble');
|
|
2477
|
-
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']);
|
|
2478
2515
|
if (a === null) return { parts: [], patches: [], cuts: [] };
|
|
2479
2516
|
checkSeam(c, a);
|
|
2480
2517
|
if (required.includes('plan') && !('plan' in a)) {
|
|
@@ -2512,6 +2549,7 @@ function checkAssemble(c: Check, v: Json, door: 'assemble' | 'full'): { parts: s
|
|
|
2512
2549
|
checkKeepUnderOccluders(c, a, parts);
|
|
2513
2550
|
if ('extend_painting' in a) checkPaintingExtensions(c, a.extend_painting);
|
|
2514
2551
|
if ('take' in a) checkTakeLayers(c, a.take);
|
|
2552
|
+
if ('editor_receipts' in a) checkEditorReceipts(c, a.editor_receipts);
|
|
2515
2553
|
if ('extend_below_crop' in a && c.array(`${p}.extend_below_crop`, a.extend_below_crop, false)) {
|
|
2516
2554
|
(a.extend_below_crop as Json[]).forEach((entry, i) => {
|
|
2517
2555
|
const at = `${p}.extend_below_crop[${i}]`;
|