@rig-c/pose 0.29.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/report.ts +88 -0
- package/src/retarget.ts +108 -68
- package/src/run.ts +64 -46
package/README.md
CHANGED
|
@@ -147,7 +147,10 @@ and what the run cannot decide for you it lists:
|
|
|
147
147
|
- `BODY` — per-frame body yaw read from the camera-normalised shoulder width (`r_shoulder + l_shoulder`) and hip width (`r_hip + l_hip`) projected perpendicular to the trunk against the sequence's standing width (`yaw ≈ acos(w / w_standing)`, [#173](https://github.com/rig-c/pose/issues/173)): its overall and opening (`frames 0–47`) range, every span with `|yaw| ≥ 45°`, and, when `body.yaw` or `body.sets` is declared in the map, its knee-mapped dial keys and every yaw-switched set's entered spans, swap count, and any `hides` slot keyed by another track, written as `body` in `--report`.
|
|
148
148
|
- `TURN` — a line across the body (hips, shoulders, eyes) shorter in the picture than 0.5 of its median length, or reversed, for at least 0.15 s: the body or the head turned. A reading of the pose, made whether or not the map uses the line; it changes no key. The demand for a turned-body drawing, as `END-ON` is for a foreshortened limb.
|
|
149
149
|
|
|
150
|
-
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `spec`, `rigc` (`{ version, entry }`: the rig-c that gated it), `rig`, `animation`, `frames`, `duration`, `cleaned` (the sequence's `cleaned` record, [above](#clean-first-rig-pose-clean)), `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet`, `stance`, `floor` and `raisedByHead` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `posture`, `folds`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `feet` (`planted`, `legLength`, `supports`, `jumps`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; a jump is an inclusive `[first, last]` pair of frames; `lowered` holds `{ frames, was, now, cause? }` runs (`cause`, where `now` is `null`: `released` or `higher`), present only with `--plant-feet` on a sequence not declared in place ([#113](https://github.com/rig-c/pose/issues/113)); `floor` is the world y every support span was planted on and `shifts` holds one `{ side, frames, shift }` per support span in world units, `null` and empty without `--plant-feet`; `blend` is `{ bounds, runs: [{ side, frames, k: { min, max } }], over: [{ side, frames, needed, left }] }`, the supporting leg stretched on the blend frames, `null` without `--plant-feet`; `closure` is `{ drift, frames }`, the hip's x drift closed over an in-place loop, `null` unless the sequence is `"in-place"` and `--plant-feet` is given), `stance` (`tolerance`, `release`, `minSeconds`, `bounds`, `runs`: `{ state, side, frames, maxHeight, maxDepth?, k, absorbed? }` (`state` is `both`, `lifted` or `below`; `maxDepth`, on a `below` run, is the free ankle's largest distance below the supporting one in leg lengths), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances and each row of `REDUCE`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
|
|
150
|
+
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `spec`, `rigc` (`{ version, entry }`: the rig-c that gated it), `rig`, `animation`, `inputs` (consumed rig spec, map and pose files, each with invocation-relative `path` and exact-byte `sha256`; a folder rig resolves to its actual rig.json; these three identities do not claim to bind optional base motion, sets or images), `frames`, `duration`, `frameTimes` (the exact motion-key time for each pose frame; never FPS inferred from duration), `render` (the separate renderer sampling domain, `{ fps, frames }` from frame-set metadata, or `null` when not measured), `cleaned` (the sequence's `cleaned` record, [above](#clean-first-rig-pose-clean)), `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet`, `stance`, `floor` and `raisedByHead` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `posture`, `folds`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `feet` (`planted`, `legLength`, `supports`, `jumps`, `jumpEvents`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; a jump is an inclusive `[first, last]` pair of frames; `lowered` holds `{ frames, was, now, cause? }` runs (`cause`, where `now` is `null`: `released` or `higher`), present only with `--plant-feet` on a sequence not declared in place ([#113](https://github.com/rig-c/pose/issues/113)); `floor` is the world y every support span was planted on and `shifts` holds one `{ side, frames, shift }` per support span in world units, `null` and empty without `--plant-feet`; `blend` is `{ bounds, runs: [{ side, frames, k: { min, max } }], over: [{ side, frames, needed, left }] }`, the supporting leg stretched on the blend frames, `null` without `--plant-feet`; `closure` is `{ drift, frames }`, the hip's x drift closed over an in-place loop, `null` unless the sequence is `"in-place"` and `--plant-feet` is given), `stance` (`tolerance`, `release`, `minSeconds`, `bounds`, `runs`: `{ state, side, frames, maxHeight, maxDepth?, k, absorbed? }` (`state` is `both`, `lifted` or `below`; `maxDepth`, on a `below` run, is the free ankle's largest distance below the supporting one in leg lengths), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances and each row of `REDUCE`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
|
|
151
|
+
|
|
152
|
+
Report event objects with a pose `frame` or inclusive `frames` also carry exact `t` or `times` from `frameTimes`; existing `seconds` remain span lengths, with additive `spanSeconds`. Holds preserve their frame-index `from`/`to`. Existing scan joint/chain identities remain beside map-derived `bone` (a unique mapped target) or `bones` (all actual mapped targets); no joint-name heuristic chooses a target. `feet.jumps` stays an array of original frame pairs; `feet.jumpEvents` adds matching `{ frames, times }` objects. Fold and uncovered rows preserve their discrete `times` and gaps, declare `domain: "render"`, and use the separate root `render` domain, never fabricated nearest pose-frame indices. The run hashes map/pose from the exact buffers passed to their readers, snapshots the resolved rig file before its gate, and rechecks all three before user-output writes. The writer validates coordinates before output writes and never changes motion keys. The switch diagnostic `moved.frame` is a selected-to-keyed pair with signed `frames`/`seconds` displacement; it is preserved as diagnostic data, not reinterpreted as an inclusive interval. An explicitly supplied uniform writer clock requires both positive `fps` and seconds offset `frame0`; canonical pose input does not yet declare FPS, so the retarget producer always writes exact `frameTimes`.
|
|
153
|
+
|
|
151
154
|
|
|
152
155
|
| field | each entry |
|
|
153
156
|
| --- | --- |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Add report coordinates without touching the retarget or any source value. */
|
|
2
|
+
import type { JointMap } from './map.ts';
|
|
3
|
+
import { isObject, Problems, Refusal } from './problems.ts';
|
|
4
|
+
|
|
5
|
+
export type ReportClock={frameTimes:readonly number[]}|{fps:number;frame0:number;frames:number};
|
|
6
|
+
export interface RenderDomain {fps:number;frames:number}
|
|
7
|
+
export interface ReportInput {path:string;sha256:string}
|
|
8
|
+
export interface ReportInputs {rig:ReportInput;map:ReportInput;pose:ReportInput}
|
|
9
|
+
function fail(field:string,text:string):never {throw new Refusal([{code:'REPORT_TIMEBASE',text:`${field}: ${text}`}]);}
|
|
10
|
+
function finite(value:unknown,field:string):number {if(typeof value!=='number'||!Number.isFinite(value)||value<0)return fail(field,`found ${String(value)}; expected finite seconds >= 0`);return value;}
|
|
11
|
+
function integer(value:unknown,field:string):number {const n=finite(value,field);if(!Number.isSafeInteger(n))fail(field,`found ${n}; expected nonnegative safe integer`);return n;}
|
|
12
|
+
export function reportFrameTimes(clock:ReportClock):number[] {
|
|
13
|
+
if('frameTimes' in clock){
|
|
14
|
+
if(!Array.isArray(clock.frameTimes))fail('frameTimes','expected an explicit array');
|
|
15
|
+
const values=clock.frameTimes.map((time,i)=>finite(time,`frameTimes[${i}]`));
|
|
16
|
+
if(!values.length)fail('frameTimes','expected at least one explicit frame');
|
|
17
|
+
for(let i=1;i<values.length;i++)if(values[i]!<=values[i-1]!)fail(`frameTimes[${i}]`,`found ${values[i]}; must follow ${values[i-1]}`);return values;
|
|
18
|
+
}
|
|
19
|
+
const fps=finite(clock.fps,'fps');if(fps===0)fail('fps','expected positive explicit rate');const frame0=finite(clock.frame0,'frame0'),count=integer(clock.frames,'frames');
|
|
20
|
+
if(count===0)fail('frames','expected at least one frame');const times=Array.from({length:count},(_,i)=>frame0+i/fps);
|
|
21
|
+
for(let i=0;i<times.length;i++){finite(times[i],`frame ${i}`);if(i>0&×[i]!<=times[i-1]!)fail(`frame ${i}`,'uniform clock cannot represent distinct frame samples');}return times;
|
|
22
|
+
}
|
|
23
|
+
export function reportRenderDomain(input:unknown,animation:string):RenderDomain {
|
|
24
|
+
if(!isObject(input)||!Array.isArray(input.sets))fail('render.sets','expected renderer frame-set metadata');
|
|
25
|
+
const set=input.sets.find((value:unknown)=>isObject(value)&&value.animation===animation);
|
|
26
|
+
if(!isObject(set))fail('render.animation',`missing ${JSON.stringify(animation)} frame-set metadata`);
|
|
27
|
+
const fps=finite(set.fps,'render.fps'),frames=integer(set.sampled,'render.frames');if(fps===0||frames===0)fail('render','expected positive declared fps and sampled frame count');return {fps,frames};
|
|
28
|
+
}
|
|
29
|
+
function clone(value:unknown):unknown {if(Array.isArray(value))return value.map(clone);if(isObject(value))return Object.fromEntries(Object.entries(value).map(([key,item])=>[key,clone(item)]));return value;}
|
|
30
|
+
|
|
31
|
+
export function enrichReport(input:Record<string,unknown>,map:JointMap,clock:ReportClock,render:RenderDomain|null=null,inputs?:ReportInputs):Record<string,unknown> {
|
|
32
|
+
const times=reportFrameTimes(clock),report=clone(input) as Record<string,unknown>;
|
|
33
|
+
if(render!==null){const fps=finite(render.fps,'render.fps'),frames=integer(render.frames,'render.frames');if(fps===0||frames===0)fail('render','expected positive declared fps and sampled frame count');}
|
|
34
|
+
if(report.frames!==times.length)fail('frames',`report has ${String(report.frames)}, clock has ${times.length}`);
|
|
35
|
+
const problems=new Problems();
|
|
36
|
+
const time=(frame:unknown,where:string):number=>{const n=integer(frame,where);if(n>=times.length)fail(where,`frame ${n} outside ${times.length} declared frames`);return times[n]!;};
|
|
37
|
+
const add=(row:Record<string,unknown>,key:string,value:unknown,where:string)=>{
|
|
38
|
+
if(row[key]!==undefined&&JSON.stringify(row[key])!==JSON.stringify(value))problems.add('REPORT_COORDINATE',`${where}.${key}: supplied ${JSON.stringify(row[key])}, expected ${JSON.stringify(value)}`);
|
|
39
|
+
else row[key]=value;
|
|
40
|
+
};
|
|
41
|
+
// A joint can influence several mapped segments. Preserve all actual matches;
|
|
42
|
+
// only a unique match may occupy the singular bone field.
|
|
43
|
+
const bonesFor=(joint:string)=>{const bones=new Set<string>();if(map.translate?.joint===joint)bones.add(map.translate.bone);for(const chain of map.chains)for(let i=0;i<chain.bones.length;i++)if(chain.joints[i]===joint||chain.joints[i+1]===joint)bones.add(chain.bones[i]!);return [...bones];};
|
|
44
|
+
const pair=(value:unknown,where:string):[number,number]=>{if(!Array.isArray(value)||value.length!==2)fail(where,'expected inclusive [first,last] frame pair');const first=integer(value[0],where+'[0]'),last=integer(value[1],where+'[1]');if(last<first)fail(where,`found [${first},${last}]; inclusive last must be >= first`);return [first,last];};
|
|
45
|
+
const poseEvent=(where:string)=>/^report\.(?:holds|short|endOn|bends|flips|turns|range|artRange|raised|byHip|landings|shear|switches|cleaned\.(?:holds|seams|spikes\.found)|feet\.(?:supports|changes|steps|lowered|tracked|shifts)|stance\.(?:runs|over))\[\d+\]$/.test(where);
|
|
46
|
+
const targets=(row:Record<string,unknown>,where:string)=>{
|
|
47
|
+
if(row.bone!==undefined||row.slot!==undefined)return;
|
|
48
|
+
const joints=typeof row.joint==='string'?[row.joint]:typeof row.point==='string'?[row.point]:Array.isArray(row.joints)?row.joints.filter((v):v is string=>typeof v==='string'):[];
|
|
49
|
+
const chain=typeof row.chain==='string'?map.chains.find(c=>c.name===row.chain):undefined;
|
|
50
|
+
const bones=[...new Set(chain?chain.bones:joints.flatMap(bonesFor))];
|
|
51
|
+
if(bones.length===1)add(row,'bone',bones[0],where);else if(bones.length>1)add(row,'bones',bones,where);
|
|
52
|
+
// Multiple affected targets remain explicit lists, never a guessed winner.
|
|
53
|
+
if(typeof row.point==='string')add(row,'joint',row.point,where);
|
|
54
|
+
};
|
|
55
|
+
const visit=(value:unknown,where:string):void=>{
|
|
56
|
+
if(Array.isArray(value)){value.forEach((row,i)=>visit(row,`${where}[${i}]`));return;}
|
|
57
|
+
if(!isObject(value))return;
|
|
58
|
+
// Preserve uncovered's discrete renderer times; they do not index the pose.
|
|
59
|
+
if(where.startsWith('report.uncovered[')||where.startsWith('report.folds[')){
|
|
60
|
+
if(!render)fail(where,'render-domain event requires declared renderer metadata');
|
|
61
|
+
add(value,'domain','render',where);return;
|
|
62
|
+
}
|
|
63
|
+
let coordinates=false;
|
|
64
|
+
if(poseEvent(where)&&value.frame!==undefined&&typeof value.frame!=='number')fail(where+'.frame','expected nonnegative integer pose frame');
|
|
65
|
+
if(typeof value.frame==='number'){add(value,'t',time(value.frame,where+'.frame'),where);coordinates=true;}
|
|
66
|
+
if((poseEvent(where)&&value.frames!==undefined)||(Array.isArray(value.frames)&&value.frames.length===2&&value.frames.every(frame=>typeof frame==='number'))){const frames=pair(value.frames,where+'.frames');add(value,'times',frames.map((frame,i)=>time(frame,`${where}.frames[${i}]`)),where);coordinates=true;}
|
|
67
|
+
if(where.startsWith('report.holds[')&&typeof value.from==='number'&&typeof value.to==='number'){
|
|
68
|
+
pair([value.from,value.to],where+'.from/to');add(value,'frames',[value.from,value.to],where);add(value,'times',[time(value.from,where+'.from'),time(value.to,where+'.to')],where);coordinates=true;
|
|
69
|
+
}
|
|
70
|
+
if(coordinates){if(typeof value.seconds==='number')add(value,'spanSeconds',value.seconds,where);targets(value,where);}
|
|
71
|
+
// Recurse over the original fields only; added time/target arrays are data.
|
|
72
|
+
for(const [key,item] of Object.entries(value))if(!['t','times','bones','spanSeconds'].includes(key))visit(item,where+'.'+key);
|
|
73
|
+
};
|
|
74
|
+
visit(report,'report');
|
|
75
|
+
if(isObject(report.feet)&&Array.isArray(report.feet.jumps)){
|
|
76
|
+
add(report.feet,'jumpEvents',report.feet.jumps.map((frames:unknown,index:number)=>{
|
|
77
|
+
const interval=pair(frames,`feet.jumps[${index}]`);
|
|
78
|
+
return {frames:[...interval],times:interval.map((frame,i)=>time(frame,`feet.jumps[${index}][${i}]`))};
|
|
79
|
+
}),'report.feet');
|
|
80
|
+
}
|
|
81
|
+
// The actual producer uses frameTimes. Uniform clocks exist only when a
|
|
82
|
+
// caller explicitly supplies fps/frame0, never inferred from samples.
|
|
83
|
+
if('frameTimes' in clock)add(report,'frameTimes',[...times],'report');else{add(report,'fps',clock.fps,'report');add(report,'frame0',clock.frame0,'report');}
|
|
84
|
+
add(report,'render',render,'report');
|
|
85
|
+
if(inputs!==undefined){if(!isObject(inputs))throw new Refusal([{code:'REPORT_INPUTS',text:'inputs: expected rig, map and pose file identities'}]);for(const key of ['rig','map','pose'] as const){const entry=inputs[key];if(!isObject(entry)||typeof entry.path!=='string'||!entry.path||entry.path.startsWith('/')||entry.path.charCodeAt(0)===92||/^[A-Za-z]:/.test(entry.path)||typeof entry.sha256!=='string'||! /^[a-f0-9]{64}$/.test(entry.sha256))throw new Refusal([{code:'REPORT_INPUTS',text:`inputs.${key}: expected invocation-relative consumed file path and exact SHA-256`}]);}add(report,'inputs',clone(inputs),'report');}
|
|
86
|
+
problems.throwIfAny();return report;
|
|
87
|
+
}
|
|
88
|
+
export function writeReport(input:Record<string,unknown>,map:JointMap,clock:ReportClock,render:RenderDomain|null,inputs:ReportInputs):string {if(inputs===undefined)throw new Refusal([{code:'REPORT_INPUTS',text:'inputs: expected rig, map and pose file identities'}]);return JSON.stringify(enrichReport(input,map,clock,render,inputs),null,2)+'\n';}
|
package/src/retarget.ts
CHANGED
|
@@ -1582,9 +1582,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1582
1582
|
}
|
|
1583
1583
|
};
|
|
1584
1584
|
/**
|
|
1585
|
-
* #188: For a `set.symmetric === true` gesture set,
|
|
1586
|
-
* on frame `i`
|
|
1587
|
-
*
|
|
1585
|
+
* #188 / #207: For a `set.symmetric === true` gesture set, solve its two two-bone arm chains
|
|
1586
|
+
* on frame `i` in the chest-local frame so the diamond is symmetric about the torso axis and
|
|
1587
|
+
* torso lean carries the whole diamond without collapsing one arm into the ±20° turn clamp.
|
|
1588
1588
|
*/
|
|
1589
1589
|
const solveSymmetricFrame = (set: (typeof map.alternates)[number], i: number): { cR: Chain; cL: Chain; wR: Pt; wL: Pt; hc: Pt; ed: number } | null => {
|
|
1590
1590
|
if (set.chains.length !== 2) return null;
|
|
@@ -1626,6 +1626,21 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1626
1626
|
ed = d;
|
|
1627
1627
|
}
|
|
1628
1628
|
}
|
|
1629
|
+
let hc0: Pt = [(SR0[0] + SL0[0]) / 2, (SR0[1] + SL0[1]) / 2 + 0.85 * Math.hypot(SL0[0] - SR0[0], SL0[1] - SR0[1])];
|
|
1630
|
+
let ed0 = Math.max(1e-3, 0.30 * Math.hypot(SL0[0] - SR0[0], SL0[1] - SR0[1]));
|
|
1631
|
+
if (gestureHead !== null) {
|
|
1632
|
+
const ha0 = rigPoint(rest, gestureHead.points[0]);
|
|
1633
|
+
const hb0 = rigPoint(rest, gestureHead.points[1]);
|
|
1634
|
+
if (ha0 !== null && hb0 !== null) {
|
|
1635
|
+
const d0 = Math.hypot(hb0[0] - ha0[0], hb0[1] - ha0[1]);
|
|
1636
|
+
if (d0 > 1e-6) {
|
|
1637
|
+
hc0 = [(ha0[0] + hb0[0]) / 2, (ha0[1] + hb0[1]) / 2];
|
|
1638
|
+
ed0 = d0;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const above0 = mappedAbove(bR0);
|
|
1643
|
+
const chestTurn = above0 === null ? 0 : turn(above0, i);
|
|
1629
1644
|
const drawnR0 = set.drawn[bR0]!;
|
|
1630
1645
|
const drawnR1 = set.drawn[bR1]!;
|
|
1631
1646
|
const drawnL0 = set.drawn[bL0]!;
|
|
@@ -1634,19 +1649,39 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1634
1649
|
const turnR1 = set.turn[bR1] ?? [-20, 20];
|
|
1635
1650
|
const turnL0 = set.turn[bL0] ?? [-20, 20];
|
|
1636
1651
|
const turnL1 = set.turn[bL1] ?? [-20, 20];
|
|
1637
|
-
const
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
let
|
|
1642
|
-
|
|
1652
|
+
const erDrawnX = SR0[0] + lR0 * Math.cos((drawnR0 + dR0) / DEG);
|
|
1653
|
+
const elDrawnX = SL0[0] + lL0 * Math.cos((drawnL0 + dL0) / DEG);
|
|
1654
|
+
let aR0_c: number;
|
|
1655
|
+
let aL0_c: number;
|
|
1656
|
+
let aR1_c: number;
|
|
1657
|
+
let aL1_c: number;
|
|
1658
|
+
if (Math.abs(wrap(drawnR0 + drawnL0 - 180)) > 1 && (hc0[0] - erDrawnX > lR1 || elDrawnX - hc0[0] > lL1)) {
|
|
1659
|
+
const eyTarget = hc0[1] - 0.30 * ed0;
|
|
1660
|
+
const sR = Math.max(-1, Math.min(1, (eyTarget - SR0[1]) / Math.max(1e-6, lR0)));
|
|
1661
|
+
const sL = Math.max(-1, Math.min(1, (eyTarget - SL0[1]) / Math.max(1e-6, lL0)));
|
|
1662
|
+
const wantR0 = wrap(180 - Math.asin(sR) * DEG - dR0);
|
|
1663
|
+
const wantL0 = wrap(Math.asin(sL) * DEG - dL0);
|
|
1664
|
+
const offR0_c = Math.min(Math.max(wrap(wantR0 - drawnR0), turnR0[0]), turnR0[1]);
|
|
1665
|
+
const offL0_c = Math.min(Math.max(wrap(wantL0 - drawnL0), turnL0[0]), turnL0[1]);
|
|
1666
|
+
aR0_c = wrap(drawnR0 + offR0_c);
|
|
1667
|
+
aL0_c = wrap(drawnL0 + offL0_c);
|
|
1668
|
+
const offR1_c = Math.min(Math.max(-0.35 * offR0_c, turnR1[0]), turnR1[1]);
|
|
1669
|
+
const offL1_c = Math.min(Math.max(-0.35 * offL0_c, turnL1[0]), turnL1[1]);
|
|
1670
|
+
aR1_c = wrap(drawnR1 + offR1_c);
|
|
1671
|
+
aL1_c = wrap(drawnL1 + offL1_c);
|
|
1672
|
+
} else {
|
|
1673
|
+
const rawR0 = wrap((rawTargets.get(bR0)?.[i] ?? drawnR0) - chestTurn);
|
|
1674
|
+
const rawL0 = wrap((rawTargets.get(bL0)?.[i] ?? drawnL0) - chestTurn);
|
|
1675
|
+
const offR0 = wrap(rawR0 - drawnR0);
|
|
1676
|
+
const offL0 = wrap(rawL0 - drawnL0);
|
|
1677
|
+
const cOpen = Math.max(-4, Math.min(2, 0.15 * 0.5 * (offR0 - offL0)));
|
|
1643
1678
|
const evalDiff = (deg: number) => {
|
|
1644
1679
|
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1645
1680
|
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1646
|
-
const ar = drawnR0 + Math.min(Math.max(
|
|
1647
|
-
const al = drawnL0 + Math.min(Math.max(-
|
|
1648
|
-
const eyr =
|
|
1649
|
-
const eyl =
|
|
1681
|
+
const ar = drawnR0 + Math.min(Math.max(cOpen + dR, turnR0[0]), turnR0[1]);
|
|
1682
|
+
const al = drawnL0 + Math.min(Math.max(-cOpen + dL, turnL0[0]), turnL0[1]);
|
|
1683
|
+
const eyr = SR0[1] + lR0 * Math.sin((ar + dR0) / DEG);
|
|
1684
|
+
const eyl = SL0[1] + lL0 * Math.sin((al + dL0) / DEG);
|
|
1650
1685
|
return eyl - eyr;
|
|
1651
1686
|
};
|
|
1652
1687
|
let lo = -45;
|
|
@@ -1671,68 +1706,73 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1671
1706
|
}
|
|
1672
1707
|
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1673
1708
|
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
const eyR0 =
|
|
1677
|
-
const eyL0 =
|
|
1709
|
+
aR0_c = drawnR0 + Math.min(Math.max(cOpen + dR, turnR0[0]), turnR0[1]);
|
|
1710
|
+
aL0_c = drawnL0 + Math.min(Math.max(-cOpen + dL, turnL0[0]), turnL0[1]);
|
|
1711
|
+
const eyR0 = SR0[1] + lR0 * Math.sin((aR0_c + dR0) / DEG);
|
|
1712
|
+
const eyL0 = SL0[1] + lL0 * Math.sin((aL0_c + dL0) / DEG);
|
|
1678
1713
|
if (Math.abs(eyL0 - eyR0) > 1e-4) {
|
|
1679
|
-
const sL = Math.max(-1, Math.min(1, (eyR0 -
|
|
1714
|
+
const sL = Math.max(-1, Math.min(1, (eyR0 - SL0[1]) / Math.max(1e-6, lL0)));
|
|
1680
1715
|
const candL = wrap(Math.asin(sL) * DEG - dL0);
|
|
1681
1716
|
const tL = wrap(candL - drawnL0);
|
|
1682
|
-
if (tL >= turnL0[0] && tL <= turnL0[1])
|
|
1717
|
+
if (tL >= turnL0[0] && tL <= turnL0[1]) aL0_c = candL;
|
|
1683
1718
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
if (err < bestErr) {
|
|
1720
|
-
bestErr = err;
|
|
1721
|
-
bestA = candA;
|
|
1719
|
+
aR0_c = wrap(aR0_c);
|
|
1720
|
+
aL0_c = wrap(aL0_c);
|
|
1721
|
+
const rR0_c = (aR0_c + dR0) / DEG;
|
|
1722
|
+
const rL0_c = (aL0_c + dL0) / DEG;
|
|
1723
|
+
const exR_c = SR0[0] + lR0 * Math.cos(rR0_c);
|
|
1724
|
+
const eyR_c = SR0[1] + lR0 * Math.sin(rR0_c);
|
|
1725
|
+
const exL_c = SL0[0] + lL0 * Math.cos(rL0_c);
|
|
1726
|
+
const eyL_c = SL0[1] + lL0 * Math.sin(rL0_c);
|
|
1727
|
+
const aimFore0 = (ex: number, ey: number, l1: number, d1: number, drawn1: number, turn1: readonly [number, number], tx: number, ty: number): number => {
|
|
1728
|
+
const want = wrap(Math.atan2(ty - ey, tx - ex) * DEG - d1);
|
|
1729
|
+
const off = Math.min(Math.max(wrap(want - drawn1), turn1[0]), turn1[1]);
|
|
1730
|
+
let a1 = wrap(drawn1 + off);
|
|
1731
|
+
const r1 = (a1 + d1) / DEG;
|
|
1732
|
+
const wx = ex + l1 * Math.cos(r1);
|
|
1733
|
+
const wy = ey + l1 * Math.sin(r1);
|
|
1734
|
+
const xMin = hc0[0] - 0.30 * ed0;
|
|
1735
|
+
const xMax = hc0[0] + 0.30 * ed0;
|
|
1736
|
+
const yMin = hc0[1] + 0.15 * ed0;
|
|
1737
|
+
const yMax = hc0[1] + 0.95 * ed0;
|
|
1738
|
+
if (wx < xMin || wx > xMax || wy < yMin || wy > yMax) {
|
|
1739
|
+
let bestA = a1;
|
|
1740
|
+
let bestErr = Infinity;
|
|
1741
|
+
for (let step = -200; step <= 200; step++) {
|
|
1742
|
+
const candOff = turn1[0] + ((turn1[1] - turn1[0]) * (step + 200)) / 400;
|
|
1743
|
+
const candA = wrap(drawn1 + candOff);
|
|
1744
|
+
const cr = (candA + d1) / DEG;
|
|
1745
|
+
const cwx = ex + l1 * Math.cos(cr);
|
|
1746
|
+
const cwy = ey + l1 * Math.sin(cr);
|
|
1747
|
+
const ox = cwx < xMin ? xMin - cwx : cwx > xMax ? cwx - xMax : 0;
|
|
1748
|
+
const oy = cwy < yMin ? yMin - cwy : cwy > yMax ? cwy - yMax : 0;
|
|
1749
|
+
const err = (ox + oy) * 1000 + Math.hypot(cwx - tx, cwy - ty);
|
|
1750
|
+
if (err < bestErr) {
|
|
1751
|
+
bestErr = err;
|
|
1752
|
+
bestA = candA;
|
|
1753
|
+
}
|
|
1722
1754
|
}
|
|
1755
|
+
a1 = bestA;
|
|
1723
1756
|
}
|
|
1724
|
-
a1
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
const
|
|
1732
|
-
const
|
|
1733
|
-
|
|
1757
|
+
return a1;
|
|
1758
|
+
};
|
|
1759
|
+
aR1_c = aimFore0(exR_c, eyR_c, lR1, dR1, drawnR1, turnR1, hc0[0] - 0.20 * ed0, hc0[1] + 0.65 * ed0);
|
|
1760
|
+
aL1_c = aimFore0(exL_c, eyL_c, lL1, dL1, drawnL1, turnL1, hc0[0] + 0.20 * ed0, hc0[1] + 0.65 * ed0);
|
|
1761
|
+
}
|
|
1762
|
+
const carry = 0.35 * chestTurn;
|
|
1763
|
+
const aR0 = wrap(aR0_c + carry);
|
|
1764
|
+
const aR1 = wrap(aR1_c + carry);
|
|
1765
|
+
const aL0 = wrap(aL0_c + carry);
|
|
1766
|
+
const aL1 = wrap(aL1_c + carry);
|
|
1767
|
+
const rR0 = (aR0 + dR0) / DEG;
|
|
1768
|
+
const rR1 = (aR1 + dR1) / DEG;
|
|
1769
|
+
const rL0 = (aL0 + dL0) / DEG;
|
|
1770
|
+
const rL1 = (aL1 + dL1) / DEG;
|
|
1771
|
+
const wR: Pt = [sr[0] + lR0 * Math.cos(rR0) + lR1 * Math.cos(rR1), sr[1] + lR0 * Math.sin(rR0) + lR1 * Math.sin(rR1)];
|
|
1772
|
+
const wL: Pt = [sl[0] + lL0 * Math.cos(rL0) + lL1 * Math.cos(rL1), sl[1] + lL0 * Math.sin(rL0) + lL1 * Math.sin(rL1)];
|
|
1773
|
+
targets.get(bR0)![i] = aR0;
|
|
1734
1774
|
targets.get(bR1)![i] = aR1;
|
|
1735
|
-
targets.get(bL0)![i] =
|
|
1775
|
+
targets.get(bL0)![i] = aL0;
|
|
1736
1776
|
targets.get(bL1)![i] = aL1;
|
|
1737
1777
|
return { cR, cL, wR, wL, hc, ed };
|
|
1738
1778
|
};
|
package/src/run.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* measure where the rig landed, read the folds (and which no switch covers),
|
|
5
5
|
* and write — only when every check passed.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
9
|
import { tmpdir } from 'node:os';
|
|
9
|
-
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
10
11
|
import { knownSetsOf, readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
11
12
|
import { cleanLine, inferredSource } from './clean.ts';
|
|
12
13
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
@@ -19,6 +20,7 @@ import { landed, measureFeet, measureLanding, measurePosture, measureShear, pose
|
|
|
19
20
|
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
20
21
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
21
22
|
import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
|
|
23
|
+
import { reportRenderDomain, writeReport, type ReportInput } from './report.ts';
|
|
22
24
|
import { bodyLine, faceLine } from './face.ts';
|
|
23
25
|
|
|
24
26
|
export interface RetargetArgs extends RetargetOptions {
|
|
@@ -82,6 +84,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
82
84
|
}
|
|
83
85
|
args.throwIfAny();
|
|
84
86
|
const input = resolveRig(a.rig, a.baseMotion);
|
|
87
|
+
const rigSnapshot = readReportInput(input.rig);
|
|
85
88
|
const base = baseMotionOf(input);
|
|
86
89
|
const animations = base.animations as Record<string, unknown>;
|
|
87
90
|
if (a.name in animations) refuse([{ code: 'MOTION_NAME_FREE', text: `the rig's motion spec already has an animation "${a.name}" (it has ${Object.keys(animations).join(', ')}); name this one with --name` }]);
|
|
@@ -90,8 +93,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
90
93
|
refuse([{ code: 'IDLE_NAME', text: `${a.idle !== undefined ? `--idle "${idleName}"` : '--carry-idle reads the animation "idle", and'} the rig's motion spec ${input.motion === null ? '— there is none, so it has no animations' : `${input.motion} has no animation "${idleName}" (it has ${Object.keys(animations).join(', ') || 'none'})`}; name the rig's idle with --idle` }]);
|
|
91
94
|
}
|
|
92
95
|
const fragment = a.merge === undefined ? [] : readFragment(parseJson(readFileSync(a.merge, 'utf8'), a.merge, 'MERGE_JSON'), a.merge);
|
|
93
|
-
const
|
|
94
|
-
const
|
|
96
|
+
const mapSnapshot = readReportInput(a.map), poseSnapshot = readReportInput(a.pose);
|
|
97
|
+
const map = readMap(mapSnapshot.bytes.toString('utf8'), a.map);
|
|
98
|
+
const poseText = poseSnapshot.bytes.toString('utf8');
|
|
95
99
|
const seq = a.coco ? convertCoco17(poseText, a.pose) : readPoseSequence(poseText, a.pose);
|
|
96
100
|
if (seq.cleaned === undefined) refuse([{ code: 'POSE_NOT_CLEANED', text: notCleaned(a.pose, a.coco) }]);
|
|
97
101
|
const frames = personTrack(seq, a.person, a.pose);
|
|
@@ -305,48 +309,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
305
309
|
for (const u of uncovered) say(` UNCOVERED mesh "${u.attachment}" (slot "${u.slot}"; chains ${u.chains.length === 0 ? 'none' : u.chains.join(', ')}) folds with no switch covering it on ${u.times.length} frame(s): t=${spans(u.times, 1 / fps)}`);
|
|
306
310
|
}
|
|
307
311
|
|
|
308
|
-
if (a.
|
|
309
|
-
|
|
310
|
-
rmSync(a.build, { recursive: true, force: true });
|
|
311
|
-
const kept = buildInto(input.rig, join(scratch, 'with', 'motion.json'), resolve(a.build), a.profile, undefined, a.images);
|
|
312
|
-
if (!kept.green) refuse([{ code: 'RIG_GATE', text: `rigc build into --build ${a.build} is red where the same build was green in scratch (exit ${kept.run.status}); nothing written` }, ...kept.fails.map((x) => ({ code: 'RIGC', text: x }))]);
|
|
313
|
-
}
|
|
314
|
-
writeFileSync(a.out, `${JSON.stringify(motion, null, 2)}\n`);
|
|
315
|
-
if (a.camera !== undefined) {
|
|
316
|
-
const ss = r.figureScale.frames.map((x) => x.s);
|
|
317
|
-
const minS = ss.length === 0 ? 1 : Math.min(...ss);
|
|
318
|
-
const maxS = ss.length === 0 ? 1 : Math.max(...ss);
|
|
319
|
-
writeFileSync(a.camera, `${JSON.stringify({
|
|
320
|
-
spec: 'rig-pose-camera/1',
|
|
321
|
-
rig: input.name,
|
|
322
|
-
animation: a.name,
|
|
323
|
-
frames: r.figureScale.frames.map((f, i) => {
|
|
324
|
-
const cam = seq.camera?.[i];
|
|
325
|
-
const camS = cam?.s ?? 1;
|
|
326
|
-
return {
|
|
327
|
-
frame: f.frame,
|
|
328
|
-
t: f.t,
|
|
329
|
-
s: f.s,
|
|
330
|
-
rawS: f.rawS,
|
|
331
|
-
tx: cam?.tx ?? 0,
|
|
332
|
-
ty: cam?.ty ?? 0,
|
|
333
|
-
videoS: Number((f.s * camS).toFixed(6)),
|
|
334
|
-
};
|
|
335
|
-
}),
|
|
336
|
-
summary: {
|
|
337
|
-
minS,
|
|
338
|
-
maxS,
|
|
339
|
-
maxStep: r.figureScale.maxStep,
|
|
340
|
-
endOverStart: r.figureScale.endOverStart,
|
|
341
|
-
},
|
|
342
|
-
}, null, 2)}\n`);
|
|
343
|
-
}
|
|
344
|
-
if (a.render !== undefined && f.meshes !== null && f.frames !== '') {
|
|
345
|
-
rmSync(a.render, { recursive: true, force: true });
|
|
346
|
-
cpSync(f.frames, a.render, { recursive: true });
|
|
347
|
-
}
|
|
348
|
-
if (a.report !== undefined) {
|
|
349
|
-
writeFileSync(a.report, `${JSON.stringify({
|
|
312
|
+
if (a.report !== undefined) assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
|
|
313
|
+
const reportText = a.report === undefined ? null : writeReport({
|
|
350
314
|
spec: 'rig-pose-report/1',
|
|
351
315
|
rigc,
|
|
352
316
|
rig: input.name,
|
|
@@ -403,8 +367,51 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
403
367
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
404
368
|
merged: fragment.map((tr) => ({ track: trackLabel(tr, groups), keys: tr.keys.length })),
|
|
405
369
|
reduce: reduced === null ? null : { tolerance: a.reduce, hipTolerance: a.reduceHip ?? null, rows: reduced.rows },
|
|
370
|
+
}, map, { frameTimes: r.times }, f.meshes !== null && doc.slots.length > 0 ? reportRenderDomain(JSON.parse(readFileSync(join(f.frames, '..', 'frames.json'), 'utf8')), a.name) : null, {rig:rigSnapshot.identity,map:mapSnapshot.identity,pose:poseSnapshot.identity});
|
|
371
|
+
if (a.build !== undefined) {
|
|
372
|
+
// Built again where it will live, not copied: rigc's page paths are relative to the build (#18).
|
|
373
|
+
rmSync(a.build, { recursive: true, force: true });
|
|
374
|
+
const kept = buildInto(input.rig, join(scratch, 'with', 'motion.json'), resolve(a.build), a.profile, undefined, a.images);
|
|
375
|
+
if (!kept.green) refuse([{ code: 'RIG_GATE', text: `rigc build into --build ${a.build} is red where the same build was green in scratch (exit ${kept.run.status}); nothing written` }, ...kept.fails.map((x) => ({ code: 'RIGC', text: x }))]);
|
|
376
|
+
}
|
|
377
|
+
// The kept build reads the rig again; its bytes must still match the report.
|
|
378
|
+
if (a.report !== undefined) assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
|
|
379
|
+
writeFileSync(a.out, `${JSON.stringify(motion, null, 2)}\n`);
|
|
380
|
+
if (a.camera !== undefined) {
|
|
381
|
+
const ss = r.figureScale.frames.map((x) => x.s);
|
|
382
|
+
const minS = ss.length === 0 ? 1 : Math.min(...ss);
|
|
383
|
+
const maxS = ss.length === 0 ? 1 : Math.max(...ss);
|
|
384
|
+
writeFileSync(a.camera, `${JSON.stringify({
|
|
385
|
+
spec: 'rig-pose-camera/1',
|
|
386
|
+
rig: input.name,
|
|
387
|
+
animation: a.name,
|
|
388
|
+
frames: r.figureScale.frames.map((f, i) => {
|
|
389
|
+
const cam = seq.camera?.[i];
|
|
390
|
+
const camS = cam?.s ?? 1;
|
|
391
|
+
return {
|
|
392
|
+
frame: f.frame,
|
|
393
|
+
t: f.t,
|
|
394
|
+
s: f.s,
|
|
395
|
+
rawS: f.rawS,
|
|
396
|
+
tx: cam?.tx ?? 0,
|
|
397
|
+
ty: cam?.ty ?? 0,
|
|
398
|
+
videoS: Number((f.s * camS).toFixed(6)),
|
|
399
|
+
};
|
|
400
|
+
}),
|
|
401
|
+
summary: {
|
|
402
|
+
minS,
|
|
403
|
+
maxS,
|
|
404
|
+
maxStep: r.figureScale.maxStep,
|
|
405
|
+
endOverStart: r.figureScale.endOverStart,
|
|
406
|
+
},
|
|
406
407
|
}, null, 2)}\n`);
|
|
407
408
|
}
|
|
409
|
+
if (a.render !== undefined && f.meshes !== null && f.frames !== '') {
|
|
410
|
+
rmSync(a.render, { recursive: true, force: true });
|
|
411
|
+
cpSync(f.frames, a.render, { recursive: true });
|
|
412
|
+
}
|
|
413
|
+
if (a.report !== undefined && reportText !== null) writeFileSync(a.report, reportText);
|
|
414
|
+
|
|
408
415
|
if (a.camera !== undefined) say(` wrote ${a.camera} — camera track (${r.figureScale.frames.length} frames)`);
|
|
409
416
|
say(` wrote ${a.out} — animations: ${Object.keys(motion.animations).join(', ')}`);
|
|
410
417
|
return { lines, written: true };
|
|
@@ -794,3 +801,14 @@ export function notCleaned(pose: string, coco: boolean): string {
|
|
|
794
801
|
const what = coco ? `${pose} is COCO-17 (--coco), which carries no "cleaned" record` : `${pose} has no "cleaned" record`;
|
|
795
802
|
return `${what}; retarget reads a sequence rig-pose clean wrote (spikes, occlusion, smoothing; there is no flag to skip it). Run \`rig-pose clean ${coco ? '--coco ' : ''}--pose ${pose} --out ${out}\` (add --loop for one cycle of a gait) and retarget --pose ${out}`;
|
|
796
803
|
}
|
|
804
|
+
|
|
805
|
+
/** File-bound identities are assembled only in this existing IO boundary. */
|
|
806
|
+
export interface ReportInputSnapshot {file:string;bytes:Buffer;identity:ReportInput;size:number;mtimeMs:number}
|
|
807
|
+
export function readReportInput(file:string):ReportInputSnapshot {
|
|
808
|
+
const path=realpathSync(resolve(file)),before=statSync(path),bytes=readFileSync(path),after=statSync(path);
|
|
809
|
+
if(!before.isFile()||before.size!==after.size||before.mtimeMs!==after.mtimeMs)throw new Refusal([{code:'REPORT_INPUTS',text:`${file}: input changed while reading; expected one stable regular file`}]);
|
|
810
|
+
return {file:resolve(file),bytes,identity:{path:relative(process.cwd(),path).split('\\').join('/'),sha256:createHash('sha256').update(bytes).digest('hex')},size:after.size,mtimeMs:after.mtimeMs};
|
|
811
|
+
}
|
|
812
|
+
export function assertReportInputs(inputs:readonly ReportInputSnapshot[]):void {
|
|
813
|
+
for(const expected of inputs){const current=readReportInput(expected.file);if(current.identity.path!==expected.identity.path||current.identity.sha256!==expected.identity.sha256)throw new Refusal([{code:'REPORT_INPUTS_CHANGED',text:`${expected.identity.path}: consumed input changed before output writes; expected SHA-256 ${expected.identity.sha256}, found ${current.identity.sha256}`}]);}
|
|
814
|
+
}
|