@rig-c/review 0.7.0 → 0.8.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 +21 -4
- package/docs/DESIGN.md +2 -1
- package/docs/REVIEW_PACKAGE.md +16 -6
- package/package.json +1 -1
- package/src/applied-context.ts +35 -7
- package/src/cli.ts +1 -1
- package/src/cloth-recipe.ts +199 -0
- package/src/environment-check.ts +149 -0
- package/src/environment-schedule.ts +183 -0
- package/src/feedback-export.ts +8 -1
- package/src/front.ts +4 -1
- package/src/handoff.ts +20 -13
- package/src/open.ts +1 -1
- package/src/readings-run.ts +2 -1
- package/src/readings.ts +12 -6
- package/src/rebuild.ts +185 -32
- package/src/retune-check.ts +100 -0
- package/src/server.ts +11 -1
- package/src/session.ts +3 -0
- package/web/cloth-panel.ts +168 -0
- package/web/environment-world.ts +42 -0
- package/web/index.html +1 -1
- package/web/main.ts +112 -9
- package/web/readings-panel.ts +1 -1
- package/web/rebuild-client.ts +13 -8
- package/web/requirements-panel.ts +4 -1
- package/web/rig-overlays.ts +2 -1
- package/web/styles.css +2 -0
package/web/main.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {bindWeightsView,setupPoseMesh,type WeightsView} from './weights-view.ts'
|
|
|
15
15
|
import {modelBoneNames,readSetupPose} from './setup-pose.ts';
|
|
16
16
|
import {bindBonesPanel,type BonesPanel,type BonesCheckView} from './bones-panel.ts';
|
|
17
17
|
import {bindCutsPanel,type CutsPanel} from './cuts-panel.ts';
|
|
18
|
+
import {bindClothPanel,type ClothPanel} from './cloth-panel.ts';
|
|
18
19
|
import {retargetAppliedRecord,retargetOutcomeLine,type AppliedRetarget,type RetargetJobView,type RetargetRequestBody} from './rebuild-client.ts';
|
|
19
20
|
import {bindRetargetPanel,type PoseMap,type RetargetPanel} from './retarget-panel.ts';
|
|
20
21
|
import type {RetargetFlags} from '../src/retarget.ts';
|
|
@@ -42,6 +43,8 @@ import {createWindModel,environmentFor,windReadout,RUNTIME_START_READOUT,joltPac
|
|
|
42
43
|
import {tuningRows,readoutRows,type InspectedCheckView} from '../src/physics-tuning.ts';
|
|
43
44
|
import {bindReadingsPanel,EDGE as READING_EDGE,type ReadingsJobView,type ReadingsPanel,type ReadingsRefusal} from './readings-panel.ts';
|
|
44
45
|
import {READINGS_SCHEMA,type ReadingsContext} from '../src/readings.ts';
|
|
46
|
+
import {environmentCheckRows,scheduleContext,worldExport,type EnvironmentCheckView,type WorldExport} from './environment-world.ts';
|
|
47
|
+
import {barLine,beforeLine} from '../src/physics-tuning.ts';
|
|
45
48
|
import {bindRequirementsPanel,type RequirementsCatalogView,type RequirementsJobView,type RequirementsPanel,type RequirementsRefusal} from './requirements-panel.ts';
|
|
46
49
|
import {patchRoute} from '../src/retune-route.ts';
|
|
47
50
|
import {appliedCheckRefusal,type CheckSummary} from '../src/applied-context.ts';
|
|
@@ -105,6 +108,7 @@ let weightsView:WeightsView|null=null;
|
|
|
105
108
|
/** Setup · Bones (#109): the config's bone anchors on the stage, Check and a whole-rig Apply. */
|
|
106
109
|
let bonesPanel:BonesPanel|null=null;
|
|
107
110
|
let cutsPanel:CutsPanel|null=null;
|
|
111
|
+
let clothPanel:ClothPanel|null=null;
|
|
108
112
|
/** The hash-verified compiled model document the parts catalog was read from (mesh edit reads attachments from it). */
|
|
109
113
|
let partModel:unknown=null;
|
|
110
114
|
/** Apply · Rebuild (#99): the inspected model's URL, the producer status, and the applied record while its scratch build is on the stage. */
|
|
@@ -612,7 +616,7 @@ function tick(now: number): void {
|
|
|
612
616
|
if (player && player.current) { try { stepEnvironment(dt); drawRig(()=>player!.frame(dt)); } catch (error) { pause(); message(errorText(error), true); } }
|
|
613
617
|
if (video && video.readyState >= 1 && time > video.duration) { if (referenceInput.state !== 'out-of-domain') holdVideoReference(video, referenceGeneration); } else if (video && referenceInput.state === 'loaded' && Math.abs(video.currentTime - time) > .15) video.currentTime = time;
|
|
614
618
|
if (input('sync-audio').checked) for (const cue of activeCues()) if ((previous < cue.time && cue.time <= time) || (previous === 0 && cue.time === 0 && dt > 0)) void audition(cue.clip);
|
|
615
|
-
renderSequenceFrame(); renderTransport(); if(time>=duration){if(canRepeatIdle()&&!idleHeld){stopAudio();seek(0);player?.resume();if(video)void video.play().catch(error=>{pause();message(errorText(error),true);});}else pause();} lastFrame = now;
|
|
619
|
+
renderSequenceFrame(); renderTransport(); if(time>=duration){if(canRepeatIdle()&&!idleHeld){stopAudio();if(useWorld())restartWindClock();seek(0);player?.resume();if(video)void video.play().catch(error=>{pause();message(errorText(error),true);});}else pause();} lastFrame = now;
|
|
616
620
|
}
|
|
617
621
|
requestAnimationFrame(tick);
|
|
618
622
|
}
|
|
@@ -783,7 +787,7 @@ function bind(): void {
|
|
|
783
787
|
button('chest-export').onclick = () => { void exportChest(); };
|
|
784
788
|
for (const event of ['input', 'change']) el('chest-regions').addEventListener(event, renderChest);
|
|
785
789
|
// A discarded local draft restores the form after its button's own handler; re-read the tool then.
|
|
786
|
-
el('draft-recovery').addEventListener('click', () => { renderChest(); restoreEnvironment(); physicsPanel?.restore(); bonesPanel?.restore(); cutsPanel?.restore(); idlePanel?.restore(); rigFieldsPanel?.restore(); });
|
|
790
|
+
el('draft-recovery').addEventListener('click', () => { renderChest(); restoreEnvironment(); physicsPanel?.restore(); bonesPanel?.restore(); cutsPanel?.restore(); clothPanel?.restore(); idlePanel?.restore(); rigFieldsPanel?.restore(); });
|
|
787
791
|
for (const id of ['strip-scope', 'strip-reference', 'strip-format']) select(id).onchange = () => stripStatus('Selection changed. Check the ranges, animation and reference video before downloading.');
|
|
788
792
|
for (const item of document.querySelectorAll<HTMLButtonElement>('[data-view]')) item.onclick = () => {setView(item.dataset.view as typeof view);const menu=document.getElementById('view-menu') as HTMLDetailsElement|null;if(menu)menu.open=false;};
|
|
789
793
|
for (const item of document.querySelectorAll<HTMLButtonElement>('[data-proposal]')) item.onclick = () => propose(item.dataset.proposal ?? '');
|
|
@@ -837,6 +841,8 @@ function bind(): void {
|
|
|
837
841
|
view:{...serializableView(numericView()),theme:appearance?.get()??'system',layers:{...layers}},
|
|
838
842
|
axis: axisInspection ? { id: select('axis-select').value, value: Number(input('axis-value').value) } : null,
|
|
839
843
|
environment: feedbackEnvironment(),
|
|
844
|
+
// The exported schedule (#112): the drawn clip's rigc-environment/1 bytes and their sha256, and whether readings and Apply read them.
|
|
845
|
+
environmentSchedule: feedbackSchedule() as unknown as Json,
|
|
840
846
|
// The exact last applied designation and the producer's line, only while that scratch build is what is drawn.
|
|
841
847
|
applied: appliedOnStage ? JSON.parse(JSON.stringify(appliedOnStage)) as Json : null,
|
|
842
848
|
// The readings chosen (#108): the pairs of the last finished run and the builds they ran on, not their results.
|
|
@@ -877,6 +883,10 @@ function bind(): void {
|
|
|
877
883
|
cutsPanel=bindCutsPanel({pane:el('rig-pane'),canvas:el('rig-overlay'),project:()=>player?rigProject():null,mapping:()=>chestMapping,inspector:()=>selectedInspector(),pause,changed:()=>redrawOverlay(false),saved:()=>persistence?.save(),
|
|
878
884
|
apply:applyCuts,reset:resetCutsBuild,token:name=>getComputedStyle(document.documentElement).getPropertyValue(name).trim(),route:path=>access.route(path)});
|
|
879
885
|
viewDisposers.push(()=>cutsPanel?.dispose());
|
|
886
|
+
// #111: the cloth recipe per tag; Apply runs rig-parts propose --cloth --write-config, then builds what it wrote.
|
|
887
|
+
clothPanel=bindClothPanel({project:()=>player?rigProject():null,mapping:()=>chestMapping,inspector:()=>selectedInspector(),pause,changed:()=>redrawOverlay(false),saved:()=>persistence?.save(),
|
|
888
|
+
apply:applyCloth,reset:resetClothBuild,token:name=>getComputedStyle(document.documentElement).getPropertyValue(name).trim()});
|
|
889
|
+
viewDisposers.push(()=>clothPanel?.dispose());
|
|
880
890
|
physicsPanel=bindPhysicsPanel({apply:async ops=>{await applyPhysics(ops);},reset:resetPhysicsBuild,pane:el('rig-pane'),saved:()=>persistence?.save(),
|
|
881
891
|
bonePoints:bones=>{if(!player)return null;const project=rigProject(),pane=el('rig-pane').getBoundingClientRect(),view=el('rig-root').parentElement!.getBoundingClientRect(),out=new Map<string,{x:number;y:number}>();for(const name of bones){try{const b=player.bone(name),p=project(b.x,b.y);out.set(name,{x:p.x+view.left-pane.left,y:p.y+view.top-pane.top});}catch{/* a bone this model lacks is not pickable */}}return out;}});
|
|
882
892
|
viewDisposers.push(()=>physicsPanel?.dispose());
|
|
@@ -941,7 +951,7 @@ async function start(): Promise<void> {
|
|
|
941
951
|
else if (readOnly) message(readOnly, true);
|
|
942
952
|
else message(data.manifest.model ? 'Record review status and structured feedback while preserving sources. Motion is read only.' : 'No rig supplied. Review the available references and checkpoints.');
|
|
943
953
|
requestAnimationFrame(tick);
|
|
944
|
-
const restored=await setupPersistence();await loadRebuildStatus();void loadRequirements();renderChest();restoreEnvironment();physicsPanel?.restore();bonesPanel?.restore();cutsPanel?.restore();idlePanel?.restore();rigFieldsPanel?.restore();meshPanel?.restore();renderIdle();if(!restored&&canRepeatIdle()&&automaticIdlePreview(assets?.animations.map(item=>item.name)??[],animation,first,!!requestedId))void playPreview();
|
|
954
|
+
const restored=await setupPersistence();await loadRebuildStatus();void loadRequirements();renderChest();restoreEnvironment();physicsPanel?.restore();bonesPanel?.restore();cutsPanel?.restore();clothPanel?.restore();idlePanel?.restore();rigFieldsPanel?.restore();meshPanel?.restore();renderIdle();if(!restored&&canRepeatIdle()&&automaticIdlePreview(assets?.animations.map(item=>item.name)??[],animation,first,!!requestedId))void playPreview();
|
|
945
955
|
if(!readOnly)controlDispose=connectControl({sourceHash:data.sourceHash,mode:'single',route:access.route,headers:()=>access.headers(),observe:controlState,apply:applyControl,onStatus:value=>text('control-state',value)});
|
|
946
956
|
} catch (error) { message(`Could not open review package: ${errorText(error)}`, true); text('dirty', 'Load failed'); }
|
|
947
957
|
}
|
|
@@ -963,7 +973,7 @@ function rigProject(){const root=el('rig-root');return (x:number,y:number)=>({x:
|
|
|
963
973
|
function redrawOverlay(fresh:boolean):void {
|
|
964
974
|
if(!overlays||!player)return;
|
|
965
975
|
const project=rigProject();
|
|
966
|
-
const regions=[...chestRegions(),...(meshPanel?.overlayRegions()??[]),...(cutsPanel?.overlayRegions()??[])],circles=[...chestCircles(),...(meshPanel?.overlayCircles()??[])],markers=[...chestMarkers(),...(cutsPanel?.overlayMarkers()??[])];
|
|
976
|
+
const regions=[...chestRegions(),...(meshPanel?.overlayRegions()??[]),...(cutsPanel?.overlayRegions()??[]),...(clothPanel?.overlayRegions()??[])],circles=[...chestCircles(),...(meshPanel?.overlayCircles()??[])],markers=[...chestMarkers(),...(cutsPanel?.overlayMarkers()??[])];
|
|
967
977
|
const value=fresh?overlays.refresh({player,frame:parts?.frame('single'),selectedSlot:parts?.value()?.slot,bones:(data.feedbackCatalog.bones??[]).map(name=>({name})),project,settings:layers,regions,circles,markers,meshEdit:meshEditor?.overlay(),weights:weightsView?.overlay(),anchors:bonesPanel?.overlay()}):overlays.redraw({project,settings:layers,regions,circles,markers,meshEdit:meshEditor?.overlay(),weights:weightsView?.overlay(),anchors:bonesPanel?.overlay()});
|
|
968
978
|
lastOverlay=value;
|
|
969
979
|
text('layer-state',value.warnings.join(' · ')||'Actual last-drawn mesh edges / bone points');idlePanel?.place(duration);rigFieldsPanel?.place();if(!fresh&&value.needsCapture&&!playing&&!seekingControl)drawRig(()=>player!.draw());
|
|
@@ -1160,7 +1170,7 @@ function renderChest():void {
|
|
|
1160
1170
|
el('chest-mapping').classList.toggle('error',!chestMapping.available);
|
|
1161
1171
|
const value=chestEvaluation();text('chest-status',value.ok?`${value.document.mesh.value.contour.regions.map(r=>`${r.name} → ${r.bone}`).join(' · ')} · valid for export`:value.error);
|
|
1162
1172
|
el('chest-status').classList.toggle('error',!value.ok);
|
|
1163
|
-
chestEditor?.sync();meshEditor?.sync();weightsView?.sync();bonesPanel?.sync();cutsPanel?.sync();redrawOverlay(false);
|
|
1173
|
+
chestEditor?.sync();meshEditor?.sync();weightsView?.sync();bonesPanel?.sync();cutsPanel?.sync();clothPanel?.sync();redrawOverlay(false);
|
|
1164
1174
|
}
|
|
1165
1175
|
async function exportChest():Promise<void> {
|
|
1166
1176
|
const value=chestEvaluation();if(!value.ok){message(`Chest regions not exported: ${value.error}`,true);return;}
|
|
@@ -1170,7 +1180,7 @@ async function exportChest():Promise<void> {
|
|
|
1170
1180
|
message('Downloaded chest-regions.json and chest-regions.binding.json. The regions are review evidence, not an approval; the package is unchanged.');
|
|
1171
1181
|
}
|
|
1172
1182
|
/** Animate · Environment. Inspection only: the player is the solver, nothing is written to the model or motion. */
|
|
1173
|
-
const environmentInputs=['wind-base','wind-breeze','wind-gust','wind-repeat','wind-gust-peak','wind-gust-duration','gravity','jolt','drag-trail','physics-reset'];
|
|
1183
|
+
const environmentInputs=['wind-base','wind-breeze','wind-gust','wind-repeat','wind-gust-peak','wind-gust-duration','gravity','jolt','drag-trail','physics-reset','environment-use','environment-export','environment-check'];
|
|
1174
1184
|
function environmentApplied():boolean{return input('environment-applied').checked;}
|
|
1175
1185
|
function gravityValue():number{return Number(input('gravity').value);}
|
|
1176
1186
|
/** Called from `tick` right before `player.frame(dt)`: the gust clock is the stepped time, so a paused player freezes it. */
|
|
@@ -1188,6 +1198,55 @@ function renderEnvironment():void {
|
|
|
1188
1198
|
text('wind-readout',(environmentApplied()?windReadout(reading,gravityValue()):RUNTIME_START_READOUT)+environmentNote);
|
|
1189
1199
|
const gust=button('wind-gust');gust.disabled=reading.gusting;gust.setAttribute('aria-pressed',String(reading.gusting));gust.textContent=reading.gusting?'Gusting…':'Gust';
|
|
1190
1200
|
text('wind-base-label',input('wind-base').value);text('gravity-label',gravityValue().toFixed(2));
|
|
1201
|
+
renderSchedule();
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* #112 · Use for readings: the controls as the exported `rigc-environment/1` schedule (src/environment-schedule.ts). While
|
|
1205
|
+
* on, readings send the drawn clip's schedule, Apply's check reads the checked clip's (`idle` when the rig has one, as
|
|
1206
|
+
* `rig-parts check` renders it), and the wind clock restarts with the clip so the person watches the world rigc reads.
|
|
1207
|
+
*/
|
|
1208
|
+
function useWorld():boolean{return input('environment-use').checked&&environmentApplied();}
|
|
1209
|
+
function clipDuration(name:string):number{return assets?.animations.find(item=>item.name===name)?.duration??data.manifest.animations.find(item=>item.id===name)?.duration??0;}
|
|
1210
|
+
/** The clip `rig-parts check` poses its idle readings over: `idle` when the rig has one, else the drawn clip. */
|
|
1211
|
+
function checkAnimation():string{return assets?.animations.some(item=>item.name==='idle')?'idle':animation;}
|
|
1212
|
+
let worldCache:{key:string;value:WorldExport|{error:string}}|null=null;
|
|
1213
|
+
function worldFor(name:string):WorldExport|{error:string} {
|
|
1214
|
+
const key=JSON.stringify([wind.snapshot(),gravityValue(),environmentApplied(),name,clipDuration(name)]);
|
|
1215
|
+
if(worldCache?.key!==key)worldCache={key,value:worldExport(wind.snapshot(),gravityValue(),environmentApplied(),name,clipDuration(name))};
|
|
1216
|
+
return worldCache.value;
|
|
1217
|
+
}
|
|
1218
|
+
/** A fresh wind clock at the same settings (a repeating gust restarts at 0); pending jolts keep their distance in time. */
|
|
1219
|
+
function restartWindClock():void {const now=wind.read().time;wind.restore(wind.snapshot());joltQueue=joltQueue.map(step=>({...step,at:Math.max(0,step.at-now)}));}
|
|
1220
|
+
let scheduleShown='';
|
|
1221
|
+
function renderSchedule():void {
|
|
1222
|
+
const world=worldFor(animation),check=checkAnimation(),checked=check!==animation?worldFor(check):null;
|
|
1223
|
+
const shown='error' in world?world.error:`${world.summary}${useWorld()?' · used for readings and Apply':''}${checked&&!('error' in checked)&&checked.sha256!==world.sha256?` · check reads ${check}'s: ${checked.summary}`:''}`;
|
|
1224
|
+
if(shown!==scheduleShown){scheduleShown=shown;text('environment-schedule',shown);}
|
|
1225
|
+
}
|
|
1226
|
+
function feedbackSchedule(){if(!player||!environmentApplied())return null;const world=worldFor(animation);return 'error' in world?null:scheduleContext(world,useWorld());}
|
|
1227
|
+
let environmentCheckBusy=false;
|
|
1228
|
+
/** Check under this world: `rig-parts check --environment` on the build on the stage, beside that build's default-world check. */
|
|
1229
|
+
async function runEnvironmentCheck():Promise<EnvironmentCheckView|{status:'refused';error:string}|null> {
|
|
1230
|
+
if(environmentCheckBusy)return null;
|
|
1231
|
+
const out=el('environment-check-lines'),show=(rows:{kind:string;text:string}[])=>{out.replaceChildren(...rows.map(row=>{const line=document.createElement('div');line.className=`env-${row.kind}`;line.textContent=row.text;return line;}));};
|
|
1232
|
+
const name=checkAnimation(),world=worldFor(name);
|
|
1233
|
+
if('error' in world){show([{kind:'error',text:world.error}]);return {status:'refused',error:world.error};}
|
|
1234
|
+
const build=appliedOnStage?'scratch':'inspected',before=stageCheck;
|
|
1235
|
+
environmentCheckBusy=true;button('environment-check').disabled=true;show([{kind:'label',text:`Checking the ${build} build under ${name}'s schedule (${world.schedule.steps.length} step(s), sha256 ${world.sha256.slice(0,16)}) with rig-parts check --environment…`}]);
|
|
1236
|
+
try{
|
|
1237
|
+
let job=await api<EnvironmentCheckView|{status:'refused';error:string}>('/api/environment-check',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({build,schedule:world.schedule})}).catch(error=>({status:'refused' as const,error:errorText(error).replace(/^\d+: /,'')}));
|
|
1238
|
+
if(!('id' in job)){const text=(()=>{try{return (JSON.parse(job.error) as {error?:string}).error??job.error;}catch{return job.error;}})();show([{kind:'error',text}]);return {status:'refused',error:text};}
|
|
1239
|
+
const started=performance.now();
|
|
1240
|
+
while(job.status==='running'){show([{kind:'label',text:`check --environment running on the ${build} build · ${((performance.now()-started)/1000).toFixed(0)} s · ${job.lines.at(-1)?.trim()??''}`}]);await new Promise(resolve=>setTimeout(resolve,1500));job=await api<EnvironmentCheckView>(`/api/environment-check/${job.id}`);}
|
|
1241
|
+
const checked=job;
|
|
1242
|
+
const label=build==='inspected'?'default world (the inspected build\'s own check)':`the scratch build's Apply check${appliedOnStage&&'environment' in appliedOnStage&&appliedOnStage.environment?` (under schedule sha256 ${appliedOnStage.environment.sha256.slice(0,16)})`:' (default world)'}`;
|
|
1243
|
+
// No inspected check exists when the baseline did not reproduce the inspected rig; say why, verbatim (a schedule of wind [1, 0], gravity [0, 1] reads the default world).
|
|
1244
|
+
const baseline=rebuildStatus?.baseline,noDefault=build==='inspected'&&!inspectedCheckView()&&baseline&&baseline.status!=='running'&&baseline.status!=='reproduced';
|
|
1245
|
+
const draw=()=>show(environmentCheckRows(checked,name=>noDefault?`${name}: no default-world check — ${baseline!.line} (check a schedule of wind 1, gravity 1 for the runtime's world)`:build==='inspected'?beforeLine(name,null,inspectedCheckView()):barLine(name,before?.lines??null,before?.check??null),label));
|
|
1246
|
+
draw();if(build==='inspected'&&inspectedCheckView()?.status!=='checked')void followInspectedCheck(draw);
|
|
1247
|
+
return job;
|
|
1248
|
+
}catch(error){show([{kind:'error',text:`ENVIRONMENT_CHECK_FAILED: ${errorText(error)}`}]);return null;}
|
|
1249
|
+
finally{environmentCheckBusy=false;button('environment-check').disabled=!player;}
|
|
1191
1250
|
}
|
|
1192
1251
|
/** Every control change engages the environment; until then the player stands at the runtime start values. */
|
|
1193
1252
|
function engage(change:()=>void):void {
|
|
@@ -1207,6 +1266,15 @@ function bindEnvironment():void {
|
|
|
1207
1266
|
button('wind-gust').onclick=()=>engage(()=>wind.triggerGust());
|
|
1208
1267
|
button('jolt').onclick=()=>{if(!player)return;joltQueue.push(...joltPacket(wind.read().time));if(!playing)message('Jolt queued · it reaches the physics on the next steps; press Play.');};
|
|
1209
1268
|
button('physics-reset').onclick=resetPhysics;
|
|
1269
|
+
input('environment-use').onchange=()=>{
|
|
1270
|
+
if(!input('environment-use').checked){message('Use for readings is off: readings send the standing wind and gravity, Apply\'s check reads the default world.');renderSchedule();return;}
|
|
1271
|
+
const world=worldFor(animation);
|
|
1272
|
+
if('error' in world){input('environment-use').checked=false;message(world.error,true);renderSchedule();return;}
|
|
1273
|
+
// The watched world starts where the schedule does: a fresh wind clock at the clip's first frame.
|
|
1274
|
+
restartWindClock();seek(0);applyEnvironment();message(`Use for readings: ${world.summary} · the wind clock restarts with the clip.`);
|
|
1275
|
+
};
|
|
1276
|
+
button('environment-export').onclick=()=>{const world=worldFor(animation);if('error' in world){message(world.error,true);return;}download('rigc-environment.json',world.text,'application/json');message(`Exported ${world.summary}. The download is the bytes readings, Apply and feedback carry; the package is unchanged.`);};
|
|
1277
|
+
button('environment-check').onclick=()=>{void runEnvironmentCheck();};
|
|
1210
1278
|
input('drag-trail').onchange=()=>{el('rig-viewport').dataset.trail=String(input('drag-trail').checked);};
|
|
1211
1279
|
// Drag to trail: the rig pane's pointer drag becomes `player.moved` before the pan handler sees it; Space still pans.
|
|
1212
1280
|
let space=false,trail:{id:number;x:number;y:number}|null=null;
|
|
@@ -1354,6 +1422,7 @@ async function loadRebuildStatus():Promise<RebuildStatusView|null> {
|
|
|
1354
1422
|
retargetPanel?.load(rebuildStatus.retarget??null,rebuildStatus.reason);
|
|
1355
1423
|
bonesPanel?.load(rebuildStatus.inputs?rebuildStatus.bones??[]:null,rebuildStatus.inputs?null:rebuildStatus.reason??'BONES_UNAVAILABLE: no producer handoff');
|
|
1356
1424
|
rigFieldsPanel?.load(rebuildStatus.inputs?rebuildStatus.fields??{}:null,rebuildStatus.inputs?null:rebuildStatus.reason??'RIG_FIELDS_UNAVAILABLE: no producer handoff');
|
|
1425
|
+
clothPanel?.load(rebuildStatus.inputs?rebuildStatus.cloth??null:null,rebuildStatus.inputs?null:rebuildStatus.reason??'CLOTH_UNAVAILABLE: no producer handoff');
|
|
1357
1426
|
cutsPanel?.load(rebuildStatus.inputs&&rebuildStatus.cuts?rebuildStatus.cuts.config:null,rebuildStatus.bones??[],rebuildStatus.inputs?null:rebuildStatus.reason??'CUTS_UNAVAILABLE: no producer handoff',rebuildStatus.cuts?.build??null);
|
|
1358
1427
|
meshPanel?.load(rebuildStatus.inputs?rebuildStatus.meshes??{}:null,rebuildStatus.inputs?null:rebuildStatus.reason??'MESH_PANEL_UNAVAILABLE: no producer handoff');meshPanel?.select(parts?.value()?.slot??null);
|
|
1359
1428
|
return rebuildStatus;
|
|
@@ -1410,7 +1479,10 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1410
1479
|
const lines:string[]=[];
|
|
1411
1480
|
rebuildBusy=true;rebuildState();chestEditor?.notify(`Applying patch (${patch.op.cmd}, ${patch.ops.length} op(s))…`,false,[]);
|
|
1412
1481
|
try{
|
|
1413
|
-
|
|
1482
|
+
// #112: with "Use for readings" on, Apply's check reads the exported schedule of the clip check renders (its idle).
|
|
1483
|
+
const scheduled=useWorld()?worldFor(checkAnimation()):null;
|
|
1484
|
+
if(scheduled&&'error' in scheduled){chestEditor?.notify(scheduled.error,true);message(scheduled.error,true);return {status:'refused',error:scheduled.error,producerLine:scheduled.error};}
|
|
1485
|
+
const result=await runRebuild<{patch:ConfigPatch;environment?:WorldExport['schedule']},PatchJobView>({patch,...(scheduled?{environment:scheduled.schedule}:{})},{post:value=>api<PatchJobView|RebuildRefusal>('/api/rebuild',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(value)}),get:id=>api<PatchJobView>(`/api/rebuild/${id}`),sleep:ms=>new Promise(resolve=>setTimeout(resolve,ms)),
|
|
1414
1486
|
onLine:line=>{lines.push(line);chestEditor?.notify(line.trim()||'…',false,lines.slice(-6));}});
|
|
1415
1487
|
const outcome=patchOutcomeLine(result),next=stageModel(result,{url:'',hash:stageModelHash??''});
|
|
1416
1488
|
if(next.id===null||!('id' in result)){chestEditor?.notify(outcome.text,true,[...lines.slice(-5),outcome.text]);message(outcome.text,true);return result;}
|
|
@@ -1454,6 +1526,8 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1454
1526
|
const posedTime=player.time;if(steps.length){if(environmentApplied())player.environment(environmentFor(wind.read(),gravityValue()));else player.environment(null);}
|
|
1455
1527
|
return posedTime;},
|
|
1456
1528
|
/** Readings (#108): run (the pickers' choice, or the given readings), the last job, the ticks drawn and the worst lines shown. */
|
|
1529
|
+
/** #112: the exported schedule of a clip (the drawn one by default), Use for readings, and Check under this world. */
|
|
1530
|
+
world:{schedule:(name?:string)=>{const w=worldFor(name??animation);return 'error' in w?w:{text:w.text,sha256:w.sha256,summary:w.summary,form:w.export.form,tolerance:w.export.tolerance};},use:(on:boolean)=>{input('environment-use').checked=on;input('environment-use').dispatchEvent(new Event('change'));return useWorld();},check:()=>runEnvironmentCheck(),lines:()=>[...el('environment-check-lines').children].map(c=>c.textContent??'')},
|
|
1457
1531
|
readings:{run:(readings?:import('../src/readings.ts').ReadingRequest[])=>runReadings(readings?{readings}:undefined),job:()=>readingsJob?JSON.parse(JSON.stringify(readingsJob)):null,ticks:()=>[...document.querySelectorAll<HTMLButtonElement>('#reading-ticks .reading-tick')].map(t=>({build:t.dataset.build,frame:Number(t.dataset.frame),left:t.style.left})),lines:()=>readingsPanel?.shownLines()??[],context:()=>readingsContext()},
|
|
1458
1532
|
/** Idle tracks (#104): the panel's state and ops, its edits, Apply and Reset, the preview, and the inspection-only `sample`. */
|
|
1459
1533
|
idle:{state:()=>idlePanel?.state()??null,ops:()=>idlePanel?.ops()??[],source:()=>idlePanel?.source()??null,keying:()=>idlePanel?.keying()??null,refusals:()=>idlePanel?.refusals()??[],
|
|
@@ -1471,6 +1545,9 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1471
1545
|
cuts:{source:()=>cutsPanel?.source()??null,ops:()=>cutsPanel?.ops()??[],handles:()=>cutsPanel?.handles()??[],active:()=>cutsPanel?.active()??false,setActive:(on:boolean)=>cutsPanel?.setActive(on),
|
|
1472
1546
|
setVertex:(cut:number,k:number,value:[number,number])=>cutsPanel?.setVertex(cut,k,value),setBox:(i:number,value:[number,number,number,number])=>cutsPanel?.setBox(i,value),setRange:(limb:string,k:number,value:[number,number]|null)=>cutsPanel?.setRange(limb,k,value),
|
|
1473
1547
|
apply:()=>cutsPanel?.apply()??Promise.resolve(null),reset:()=>resetCutsBuild()},
|
|
1548
|
+
/** #111: the Cloth block — the selected tag, its form values, the pending ops, the link preview, Apply and Reset. */
|
|
1549
|
+
cloth:{tag:()=>clothPanel?.tag()??'',select:(tag:string)=>clothPanel?.select(tag),values:(tag?:string)=>clothPanel?.values(tag)??{},set:(key:string,text:string,tag?:string)=>clothPanel?.set(key,text,tag),declare:(on:boolean,tag?:string)=>clothPanel?.declare(on,tag),
|
|
1550
|
+
ops:()=>clothPanel?.ops()??[],links:()=>clothPanel?.links()??null,active:()=>clothPanel?.active()??false,setActive:(on:boolean)=>clothPanel?.setActive(on),apply:()=>clothPanel?.apply()??Promise.resolve(null),reset:()=>resetClothBuild(),status:()=>el('cloth-status').textContent,readout:()=>[...document.querySelectorAll('#cloth-readout code')].map(c=>c.textContent)},
|
|
1474
1551
|
bones:{anchors:()=>bonesPanel?.anchors()??[],edits:()=>bonesPanel?.edits()??[],set:(path:string,value:[number,number])=>bonesPanel?.set(path,value),select:(name:string|null)=>bonesPanel?.select(name),
|
|
1475
1552
|
active:()=>bonesPanel?.active()??false,setActive:(on:boolean)=>bonesPanel?.setActive(on),handles:()=>bonesPanel?.handles()??[],check:()=>bonesPanel?.check()??Promise.resolve(null),
|
|
1476
1553
|
apply:()=>applyBones((bonesPanel?.edits()??[]).map(e=>({op:'replace' as const,path:e.path,value:[...e.value],name:e.name}))),reset:()=>resetBonesBuild(),
|
|
@@ -1532,7 +1609,9 @@ async function runReadings(override?:{readings:import('../src/readings.ts').Read
|
|
|
1532
1609
|
const chosen=override??readingsPanel.requests();
|
|
1533
1610
|
if('error' in chosen){readingsPanel.status(chosen.error,true);return {status:'refused',error:chosen.error};}
|
|
1534
1611
|
if(!animation){readingsPanel.status('READINGS_NO_ANIMATION: no animation is drawn',true);return null;}
|
|
1535
|
-
|
|
1612
|
+
// #112: with "Use for readings" on, the exported schedule of the drawn clip goes in place of the standing world.
|
|
1613
|
+
let environment:ReturnType<typeof environmentFor>|WorldExport['schedule']|null=player&&environmentApplied()?environmentFor(wind.read(),gravityValue()):null;
|
|
1614
|
+
if(environment&&useWorld()){const world=worldFor(animation);if('error' in world){readingsPanel.status(world.error,true);return {status:'refused',error:world.error};}environment=world.schedule;}
|
|
1536
1615
|
const body={animation,readings:chosen.readings,environment};
|
|
1537
1616
|
readingsBusy=true;readingsPanel.busy(true);readingsPanel.status(`Running ${chosen.readings.length} reading(s) of ${animation} with rigc render…`,false);
|
|
1538
1617
|
const started=performance.now();
|
|
@@ -1549,7 +1628,8 @@ async function runReadings(override?:{readings:import('../src/readings.ts').Read
|
|
|
1549
1628
|
readingsJob=job;readingsPanel.lines(job);renderReadingTicks(true);
|
|
1550
1629
|
const ticks=job.runs.map(r=>`${r.build} ${r.results.reduce((n,x)=>n+x.frames.length,0)} tick(s)`).join(' · ');
|
|
1551
1630
|
const refused=job.runs.flatMap(r=>r.results).find(r=>r.status!=='read');
|
|
1552
|
-
const
|
|
1631
|
+
const scheduled=job.request.environment&&'spec' in job.request.environment?job.request.environment:null;
|
|
1632
|
+
const env=job.environment.requested?(job.environment.passed?(scheduled?` · the exported schedule (${scheduled.steps.length} step(s)) passed as --environment`:' · wind and gravity passed as --environment'):' · this rigc has no --environment; the environment was not passed'):'';
|
|
1553
1633
|
readingsPanel.status(`${refused?`${refused.refusal} · `:''}Readings of ${job.request.animation}: ${ticks}${job.runs.length<2?' · no scratch build yet (Apply builds one)':''}${env} · ${((performance.now()-started)/1000).toFixed(1)} s`,!!refused);
|
|
1554
1634
|
return job;
|
|
1555
1635
|
}catch(error){readingsPanel.status(`READINGS_FAILED: ${errorText(error)}`,true);return null;}
|
|
@@ -1617,6 +1697,29 @@ async function applyCuts(ops:PatchOp[]):Promise<PatchJobView|RebuildRefusal|null
|
|
|
1617
1697
|
return result;
|
|
1618
1698
|
}finally{cutsPanel?.busy(false);}
|
|
1619
1699
|
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Cloth Apply (#111): the pending recipe ops become one `rig` patch, which the server runs as `rig-parts propose --cloth
|
|
1702
|
+
* --write-config` and then builds the written config; the scratch build reloads in place under the stage's world.
|
|
1703
|
+
*/
|
|
1704
|
+
async function applyCloth(ops:PatchOp[]):Promise<PatchJobView|RebuildRefusal|null> {
|
|
1705
|
+
const status=rebuildStatus??await loadRebuildStatus();
|
|
1706
|
+
const patch=status&&ops.length?makePatch(status,ops,{cmd:'rig'}):null;
|
|
1707
|
+
if(!patch){const reason=!ops.length?'CLOTH_NOTHING_PENDING: no pending ops':status?.reason??'REBUILD_STATUS_UNAVAILABLE';clothPanel?.status(reason,true);return null;}
|
|
1708
|
+
const route=status?.cloth?.route==='build'?'build (assemble → rig → check)':'rig --config, check';
|
|
1709
|
+
clothPanel?.busy(true);clothPanel?.status(`Applying ${ops.length} op(s) with ${patch.producer.name} ${patch.producer.version}: propose --cloth --write-config, then ${route}, compile…`,false);
|
|
1710
|
+
try{
|
|
1711
|
+
const result=await applyPatch(patch,stageWorld());
|
|
1712
|
+
const outcome=patchOutcomeLine(result);clothPanel?.status(outcome.text,outcome.error);
|
|
1713
|
+
if('id' in result&&result.status==='built')redrawOverlay(true);
|
|
1714
|
+
return result;
|
|
1715
|
+
}finally{clothPanel?.busy(false);}
|
|
1716
|
+
}
|
|
1717
|
+
/** Cloth Reset: the inspected build back on the stage (same world), and no pending op. */
|
|
1718
|
+
async function resetClothBuild():Promise<void> {
|
|
1719
|
+
clothPanel?.clear();clothPanel?.shown(null);
|
|
1720
|
+
if(appliedOnStage){clothPanel?.busy(true);try{await resetRebuild(stageWorld());}finally{clothPanel?.busy(false);}}
|
|
1721
|
+
clothPanel?.status('Reset: the inspected build is on the stage; no pending ops.',false);
|
|
1722
|
+
}
|
|
1620
1723
|
/** Cuts Reset: the inspected build back on the stage (same world), and no pending op. */
|
|
1621
1724
|
async function resetCutsBuild():Promise<void> {
|
|
1622
1725
|
cutsPanel?.clear();cutsPanel?.shown(null);
|
package/web/readings-panel.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {readingTicks,ticksOnClip,TICK_EDGE,worstLine,type ReadingBuild,type ReadingRequest,type ReadingResult,type ReadingTick} from '../src/readings.ts';
|
|
8
8
|
|
|
9
|
-
export interface ReadingsJobView {id:string;status:'running'|'done'|'failed';error:string|null;request:{animation:string;readings:ReadingRequest[];environment:
|
|
9
|
+
export interface ReadingsJobView {id:string;status:'running'|'done'|'failed';error:string|null;request:{animation:string;readings:ReadingRequest[];environment:import('../src/readings.ts').ReadingsRequest['environment']};environment:{requested:boolean;flag:boolean;passed:boolean};compiler:{package:string;version:string};runs:{build:ReadingBuild;id:string;modelHash:string;results:ReadingResult[]}[]}
|
|
10
10
|
export interface ReadingsRefusal {status:'refused';error:string}
|
|
11
11
|
export interface ReadingsPanel {
|
|
12
12
|
catalog(slots:readonly string[],bones:readonly string[]):void;
|
package/web/rebuild-client.ts
CHANGED
|
@@ -19,7 +19,9 @@ export type RegionEntry=
|
|
|
19
19
|
export interface HandoffView {how:'declared'|'discovered';via:string;outside:boolean;config:{path:string;sha256:string};parts:string|null;rig:string|null;files:{path:string;sha256:string}[];missing:string[]}
|
|
20
20
|
export interface BaselineView {status:'running'|'reproduced'|'not-reproduced'|'refused'|'failed';firstDifference:string|null;hashes:{baseline:{rigJson:string|null;tree:string};inspected:{rigJson:string|null;tree:string}}|null;line:string;wallMs:number|null}
|
|
21
21
|
export interface RetargetStatusView {available:boolean;reason:string|null;producer:{package:string;version:string}|null;compiler:{package:string;version:string}|null;rig:string|null;sequences:{id:string;label:string;path:string;cleaned:boolean;frames:number;people:string[]}[];bones:string[];animations:string[]}
|
|
22
|
-
export interface RebuildStatusView {retarget?:RetargetStatusView;available:boolean;handoff:HandoffView|null;baseline:BaselineView|null;inputs:{config:string;parts:string|null;rig:string|null;configHash:string}|null;parts:{name:string;regions:unknown[]}[];contour:Record<string,Record<string,unknown>>;meshes?:Record<string,unknown>;constraints?:unknown[]|null;bones?:unknown[]|null;fields?:Record<string,unknown>|null;cuts?:{config:Record<string,unknown>;build:string|null}|null;inspectedCheck?:InspectedCheckView|null;idle?:import('../src/idle-tracks.ts').IdleStatus|null;producer:{package:string;version:string}|null;compiler:{package:string;version:string}|null;reason:string|null}
|
|
22
|
+
export interface RebuildStatusView {retarget?:RetargetStatusView;available:boolean;handoff:HandoffView|null;baseline:BaselineView|null;inputs:{config:string;parts:string|null;rig:string|null;configHash:string}|null;parts:{name:string;regions:unknown[]}[];contour:Record<string,Record<string,unknown>>;meshes?:Record<string,unknown>;constraints?:unknown[]|null;bones?:unknown[]|null;fields?:Record<string,unknown>|null;cuts?:{config:Record<string,unknown>;build:string|null}|null;inspectedCheck?:InspectedCheckView|null;idle?:import('../src/idle-tracks.ts').IdleStatus|null;cloth?:ClothStatusView|null;producer:{package:string;version:string}|null;compiler:{package:string;version:string}|null;reason:string|null}
|
|
23
|
+
/** #111: the cloth recipe form's status — the reader's tags, the recipes as written, each tag's parts and chains, the keypoints, the route and why Apply cannot run. */
|
|
24
|
+
export interface ClothStatusView {targets:string[];targetsRead:boolean;recipes:Record<string,unknown>|null;tags:import('../src/cloth-recipe.ts').ClothTagView[];keypoints:{path:string;sha256:string}|null;route:'build'|'rig';reason:string|null}
|
|
23
25
|
export interface RebuildJobView {id:string;status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;exitCode:number|null;wallMs:{remesh:number|null;compile:number|null};build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{part:string;regions:RegionEntry[];configHash:string;entrySha256:string;bust?:BustSpec};producer:{package:string;version:string};compiler:{package:string;version:string}}
|
|
24
26
|
export interface RebuildRefusal {status:'refused';error:string;producerLine:string|null}
|
|
25
27
|
/** #99 regions or #105 landmarks: exactly one of `regions` and `bust`. */
|
|
@@ -116,8 +118,9 @@ export function parseApplied(text:string):AppliedRecord|AppliedPatch|AppliedDelt
|
|
|
116
118
|
/** A patch Apply's job (#102): the patch and its hash, the op/compile/check wall clocks, check's summary and the rig.json diff. */
|
|
117
119
|
/** #115: a build job's assemble readout (`AssembledView` in src/rebuild.ts). */
|
|
118
120
|
export type AssembledJobView=import('../src/rebuild.ts').AssembledView;
|
|
119
|
-
export interface PatchJobView {id:string;kind:'patch';op?:'rig'|'retune'|'remesh'|'build';assembled?:AssembledJobView;status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;exitCode:number|null;wallMs:{op:number|null;compile:number|null;check:number|null};check:CheckSummary|null;diff:{count:number;paths:string[]}|null;files?:FilesDiff|null;remesh?:import('../src/mesh-entry.ts').RemeshCounts&{modelTriangles:number|null}|null;build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{patch:ConfigPatch;patchSha256:string;configHash:string};producer:{package:string;version:string};compiler:{package:string;version:string}}
|
|
120
|
-
|
|
121
|
+
export interface PatchJobView {id:string;kind:'patch';op?:'rig'|'retune'|'remesh'|'build'|'cloth';assembled?:AssembledJobView;cloth?:import('../src/rebuild.ts').ClothJob;status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;exitCode:number|null;wallMs:{op:number|null;compile:number|null;check:number|null};check:CheckSummary|null;diff:{count:number;paths:string[]}|null;files?:FilesDiff|null;remesh?:import('../src/mesh-entry.ts').RemeshCounts&{modelTriangles:number|null}|null;build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{patch:ConfigPatch;patchSha256:string;configHash:string};producer:{package:string;version:string};compiler:{package:string;version:string};environment?:{sha256:string;steps:number}}
|
|
122
|
+
/** #112: `environment` is the exported `rigc-environment/1` schedule Apply's check reads, when "Use for readings" is on. */
|
|
123
|
+
export interface PatchRequestBody {patch:ConfigPatch;environment?:import('../src/environment-schedule.ts').EnvironmentSchedule}
|
|
121
124
|
/** Edit Mesh's delta Apply (#135): `remesh --delta`, the per-file diff and the part's counts; remesh's own gate holds check. */
|
|
122
125
|
export interface DeltaJobView {id:string;kind:'delta';op:'remesh';status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;exitCode:number|null;wallMs:{op:number|null;compile:number|null};files:FilesDiff|null;remesh:import('../src/mesh-entry.ts').RemeshCounts&{modelTriangles:number|null}|null;build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{part:string;delta:Record<string,unknown>;deltaSha256:string;configHash:string};producer:{package:string;version:string};compiler:{package:string;version:string}}
|
|
123
126
|
/** `{part, delta, configHash}`: the server owns every path and flag; the delta is the editor's export as it stands. */
|
|
@@ -158,11 +161,11 @@ export function makePatch(status:Pick<RebuildStatusView,'inputs'|'producer'>,ops
|
|
|
158
161
|
return {schema:CONFIG_PATCH_SCHEMA,config_sha256:status.inputs.configHash,ops:ops.map(o=>({...o})),producer:{name:status.producer.package,version:status.producer.version},op:{cmd:op.cmd,args:[...(op.args??[])]}};
|
|
159
162
|
}
|
|
160
163
|
/** What the draft keeps and `context.applied` carries for a patch: the patch, its hash, the producer's line and check's summary. */
|
|
161
|
-
export const patchAppliedRecord=(job:PatchJobView):AppliedPatch=>({schema:APPLIED_PATCH_SCHEMA,buildId:job.id,patch:job.request.patch,patchSha256:job.request.patchSha256,configHash:job.request.configHash,producer:job.producer,compiler:job.compiler,line:job.producerLine,check:job.check,diff:job.diff,...(job.files?{files:job.files}:{}),modelHash:job.build!.modelHash,...(job.op?{op:job.op}:{}),...(job.remesh?{mesh:{part:job.remesh.part,before:{...job.remesh.before},after:{...job.remesh.after}}}:{}),...(job.op==='build'?{assembled:assembledRecord(job)}:{})});
|
|
164
|
+
export const patchAppliedRecord=(job:PatchJobView):AppliedPatch=>({schema:APPLIED_PATCH_SCHEMA,buildId:job.id,patch:job.request.patch,patchSha256:job.request.patchSha256,configHash:job.request.configHash,producer:job.producer,compiler:job.compiler,line:job.producerLine,check:job.check,diff:job.diff,...(job.files?{files:job.files}:{}),modelHash:job.build!.modelHash,...(job.op?{op:job.op}:{}),...(job.remesh?{mesh:{part:job.remesh.part,before:{...job.remesh.before},after:{...job.remesh.after}}}:{}),...(job.op==='cloth'&&job.cloth?{cloth:{route:job.cloth.route,keypoints:{...job.cloth.inputs.keypoints},written:{count:job.cloth.written?.count??0,paths:[...(job.cloth.written?.paths??[])]}}}:{}),...(job.op==='build'||(job.op==='cloth'&&job.cloth?.route==='build')?{assembled:assembledRecord(job)}:{}),...(job.environment?{environment:{...job.environment}}:{})});
|
|
162
165
|
/** #115: what a build Apply records: the build's own wall clock, the recomposite figures, the parts it changed and its `seam pair` lines. */
|
|
163
166
|
export function assembledRecord(job:PatchJobView):AppliedAssembled {
|
|
164
167
|
const a=job.assembled,fig=(f:Record<string,number>|null|undefined)=>f?{...f}:null;
|
|
165
|
-
return {wallMs:job.wallMs.op,recomposite:{before:fig(a?.recomposite.before as Record<string,number>|null),after:fig(a?.recomposite.after as Record<string,number>|null)},partsChanged:[...(a?.parts?.changed??[])],seamPairs:(a?.seamPairs??[]).slice(0,64)};
|
|
168
|
+
return {wallMs:job.op==='cloth'?job.cloth?.wallMs.build??null:job.wallMs.op,recomposite:{before:fig(a?.recomposite.before as Record<string,number>|null),after:fig(a?.recomposite.after as Record<string,number>|null)},partsChanged:[...(a?.parts?.changed??[])],seamPairs:(a?.seamPairs??[]).slice(0,64)};
|
|
166
169
|
}
|
|
167
170
|
/** What the draft keeps and `context.applied` carries for a delta: the document and its hash, the producer's line, op "remesh", the diff and the counts. */
|
|
168
171
|
export const deltaAppliedRecord=(job:DeltaJobView):AppliedDelta=>({schema:APPLIED_DELTA_SCHEMA,buildId:job.id,part:job.request.part,delta:JSON.parse(JSON.stringify(job.request.delta)),deltaSha256:job.request.deltaSha256,configHash:job.request.configHash,producer:job.producer,compiler:job.compiler,line:job.producerLine,op:'remesh',files:job.files,...(job.remesh?{mesh:{part:job.remesh.part,before:{...job.remesh.before},after:{...job.remesh.after}}}:{}),modelHash:job.build!.modelHash});
|
|
@@ -188,10 +191,12 @@ const seconds=(ms:number|null)=>ms===null?'—':`${(ms/1000).toFixed(1)} s`;
|
|
|
188
191
|
export function patchOutcomeLine(result:PatchJobView|RebuildRefusal):{text:string;error:boolean} {
|
|
189
192
|
if(!('id' in result))return {text:result.producerLine??result.error,error:true};
|
|
190
193
|
if(result.status!=='built')return {text:result.producerLine??`REBUILD_FAILED: exit ${result.exitCode}`,error:true};
|
|
191
|
-
const check=result.check?(result.check.pass===null?`check refused: ${result.check.line??`exit ${result.check.exitCode}`}`:`check ${result.check.pass?'PASS':'FAIL'}`):'check not run';
|
|
194
|
+
const check=result.check?(result.check.pass===null?`check refused: ${result.check.line??`exit ${result.check.exitCode}`}`:`check ${result.check.pass?'PASS':'FAIL'}${result.check.source==='retune'?' (retune\'s own check of this revision, not run again)':''}`):'check not run';
|
|
192
195
|
// #115: a build's own wall clock covers assemble, rig and check; the recorded figure is the same number.
|
|
193
|
-
const
|
|
194
|
-
|
|
196
|
+
const building=result.op==='build'||(result.op==='cloth'&&result.cloth?.route==='build');
|
|
197
|
+
const propose=result.op==='cloth'&&result.cloth?`propose ${seconds(result.cloth.wallMs.propose)} (wrote a config differing at ${result.cloth.written?.count??'?'} path(s)) + `:'';
|
|
198
|
+
const wall=building?`${result.reused?'reused the scratch build of this exact patch · ':''}${propose}build ${seconds(result.op==='cloth'?result.cloth?.wallMs.build??null:result.wallMs.op)} (assemble → rig → check) + compile ${seconds(result.wallMs.compile)}`:result.reused?'reused the scratch build of this exact patch':`${propose}${result.op==='cloth'?'rig':result.op??result.request.patch.op.cmd} ${seconds(result.op==='cloth'?result.cloth?.wallMs.build??null:result.wallMs.op)} + compile ${seconds(result.wallMs.compile)}${result.check?.source==='retune'?'':` + check ${seconds(result.wallMs.check)}`}`;
|
|
199
|
+
return {text:`Applied patch ${result.request.patchSha256.slice(0,12)}… (${result.request.patch.ops.length} op(s)) with ${result.producer.package} ${result.producer.version} · ${wall} · ${check}${result.environment?` under the exported world (${result.environment.steps} step(s), sha256 ${result.environment.sha256.slice(0,16)})`:''} · ${result.files?filesDiffText(result.files):`rig.json differs at ${result.diff?.count??'?'} path(s)`} · Reset returns to the inspected build`,error:!!result.check&&result.check.pass!==true};
|
|
195
200
|
}
|
|
196
201
|
/** The status line after a job: built (with the wall clock and whether the scratch build was reused) or the producer's line verbatim. */
|
|
197
202
|
export function outcomeLine(result:RebuildJobView|RebuildRefusal):{text:string;error:boolean} {
|
|
@@ -65,6 +65,8 @@ export function selfTargetRefusal(r:{kind:RequirementKind;bone?:string;point?:Bo
|
|
|
65
65
|
if(r.kind==='aim'&&r.target.bone===r.bone)return `REQUIREMENTS_SELF_TARGET: aim of bone "${r.bone}" at its own ${r.target.point} has no line to measure; pick another target bone`;
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
|
+
/** The catalog the form renders from: only a ready rig spec. An unavailable one (no producer handoff) is no catalog, so picking a part with no handoff never renders fields from it (#150). */
|
|
69
|
+
export function readyCatalog<C extends {status:'ready'|'unavailable'}>(next:C|null):C|null {return next&&next.status==='ready'?next:null;}
|
|
68
70
|
/** The target a fresh form proposes: a bone other than the subject (the next one in rig order, else the first other one). */
|
|
69
71
|
export function defaultTargetBone(bones:readonly string[],subject:string):string|null {
|
|
70
72
|
const i=bones.indexOf(subject),next=i>=0?bones.slice(i+1).find(b=>b!==subject):undefined;
|
|
@@ -201,7 +203,8 @@ export function bindRequirementsPanel(deps:RequirementsPanelDeps):RequirementsPa
|
|
|
201
203
|
|
|
202
204
|
const panel:RequirementsPanel={
|
|
203
205
|
load(next,reason){
|
|
204
|
-
catalog
|
|
206
|
+
// Only a ready spec is a catalog: an unavailable one (no producer handoff, #150) leaves the form empty, so selecting a part never renders fields from it.
|
|
207
|
+
catalog=readyCatalog(next);
|
|
205
208
|
if(!next||next.status!=='ready'){state.textContent=reason??next?.reason??'REQUIREMENTS_UNAVAILABLE: no producer handoff';state.classList.add('error');kind.disabled=animation.disabled=true;add.disabled=check.disabled=true;fields.replaceChildren();return;}
|
|
206
209
|
state.textContent=`Rig spec: ${next.bones.length} bones, ${next.slots.length} slots, ${Object.keys(next.meshes).length} mesh slot(s), ${next.constraints.length} ik/transform constraint(s), ${next.animations.length} animation(s)${next.scratch?' · a scratch build exists':''}.`;state.classList.remove('error');
|
|
207
210
|
kind.disabled=animation.disabled=false;
|
package/web/rig-overlays.ts
CHANGED
|
@@ -203,7 +203,8 @@ export class RigOverlays {
|
|
|
203
203
|
const c=region.colors;
|
|
204
204
|
points.forEach((p,i)=>{const size=region.active===i?5:3.5;ctx.fillStyle=region.active===i?c.handle:c.ink;ctx.strokeStyle=region.stroke;ctx.lineWidth=1.5;ctx.fillRect(p!.x-size,p!.y-size,size*2,size*2);ctx.strokeRect(p!.x-size,p!.y-size,size*2,size*2);});
|
|
205
205
|
const top=points.reduce((a,p)=>p!.y<a!.y?p:a,points[0]);
|
|
206
|
-
|
|
206
|
+
// An empty name draws no label (#111: one label per chain set, not per chain).
|
|
207
|
+
if(region.name){ctx.font='600 11px ui-monospace, monospace';const width=ctx.measureText(region.name).width+8;ctx.fillStyle=c.labelBg;ctx.fillRect(top!.x-width/2,top!.y-24,width,15);ctx.fillStyle=region.stroke;ctx.fillText(region.name,top!.x-width/2+4,top!.y-13);}
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
if(this.playerAvailable)for(const circle of circles){
|
package/web/styles.css
CHANGED
|
@@ -260,3 +260,5 @@ footer.status-bar{flex-wrap:nowrap}
|
|
|
260
260
|
.rig-fields-handle{position:absolute;width:14px;height:14px;margin:-7px 0 0 -7px;padding:0;border:2px solid var(--rv-field-still);border-radius:50%;background:var(--rv-field-handle-bg);pointer-events:auto;cursor:grab;touch-action:none}.rig-fields-handle.still{cursor:ns-resize}.rig-fields-handle.inward{border-color:var(--rv-field-inward-line)}.rig-fields-handle.outward{border-color:var(--rv-field-outward-line)}.rig-fields-handle span{position:absolute;left:16px;top:-3px;white-space:nowrap;padding:1px 4px;border-radius:3px;background:var(--rv-field-handle-bg);color:var(--rv-field-ink);font:10px var(--font-mono);pointer-events:none}
|
|
261
261
|
.requirements-fields .row{margin-bottom:6px}.requirements-panel select{width:auto;max-width:170px;padding:4px 6px;font-size:11px}.requirements-panel input[type=text]{flex:1;min-width:0;width:auto;padding:4px 6px;font:11px var(--font-mono)}.requirements-panel input[type=number]{width:70px;padding:4px 6px;font-size:11px}.requirements-list{list-style:none;margin:0 0 9px;padding:0;display:flex;flex-direction:column;gap:8px}.requirements-item{border-left:2px solid var(--border-strong);padding-left:8px;display:flex;flex-direction:column;gap:4px}.requirements-item.pass{border-left-color:var(--pass,var(--link))}.requirements-item.fail{border-left-color:var(--fail)}.requirements-item code{font:11px var(--font-mono);overflow-wrap:anywhere}.requirements-bar input[type=range]{flex:1;min-width:90px}.requirements-bar output{font:11px var(--font-mono);color:var(--fg-strong)}.requirements-refusals{list-style:none;margin:6px 0;padding:0;display:flex;flex-direction:column;gap:3px}.requirements-refusals code{font:11px var(--font-mono);color:var(--fail);overflow-wrap:anywhere}
|
|
262
262
|
.retarget-chains{display:flex;flex-direction:column;gap:5px;margin-bottom:9px}.retarget-chain{display:flex;flex-wrap:wrap;align-items:center;gap:4px}.retarget-chain.off{opacity:.55}.retarget-chain label{min-width:70px}.retarget-chain code,.retarget-translate span{font:11px var(--font-mono);color:var(--fg-muted)}.retarget-translate{display:flex;flex-wrap:wrap;align-items:center;gap:4px}.retarget-json textarea{width:100%;font:11px var(--font-mono)}.retarget-panel .row{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.retarget-log{max-height:160px;overflow:auto;font:10px var(--font-mono);white-space:pre-wrap;color:var(--fg-muted)}
|
|
263
|
+
.environment-check-lines{display:grid;gap:3px;margin:0 0 9px;font:11px var(--font-mono);color:var(--fg-muted);overflow-wrap:anywhere}.environment-check-lines .env-bar{color:var(--fg-strong)}.environment-check-lines .env-label{color:var(--fg-soft)}.environment-check-lines .env-error{color:var(--fail)}
|
|
264
|
+
.cloth-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:6px 10px;margin:0 0 10px;padding:9px 10px;border:1px solid var(--border);border-radius:6px}.cloth-field{display:flex;flex-direction:column;gap:2px;min-width:0;color:var(--fg-muted);font-size:11px}.cloth-field input,.cloth-field select{width:100%;min-width:0;padding:4px 6px;font:11px var(--font-mono)}.cloth-field code{font:10px var(--font-mono);color:var(--fg-soft);overflow-wrap:anywhere}.cloth-field.pending span{color:var(--warn)}.cloth-declared{display:inline-flex;align-items:center;gap:4px}#cloth-panel-state.error,#cloth-status.error{color:var(--fail)!important}#cloth-pending-list .error{color:var(--fail)}
|