@rig-c/review 0.7.1 → 0.8.1
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 +27 -7
- package/docs/DESIGN.md +2 -1
- package/docs/REVIEW_PACKAGE.md +17 -7
- 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/compare-server.ts +1 -1
- package/src/environment-check.ts +149 -0
- package/src/environment-schedule.ts +183 -0
- package/src/feedback-export.ts +8 -1
- package/src/frame-clock.ts +30 -0
- package/src/front.ts +4 -1
- package/src/handoff.ts +20 -13
- package/src/open-protocol.ts +4 -3
- package/src/open.ts +1 -1
- package/src/readings-run.ts +2 -1
- package/src/readings.ts +12 -6
- package/src/rebuild.ts +198 -34
- package/src/retune-check.ts +100 -0
- package/src/server.ts +12 -2
- package/src/session.ts +3 -0
- package/web/cloth-panel.ts +168 -0
- package/web/compare.ts +7 -5
- package/web/environment-world.ts +42 -0
- package/web/index.html +1 -1
- package/web/label-placement.ts +46 -0
- package/web/main.ts +127 -17
- package/web/readings-panel.ts +1 -1
- package/web/rebuild-client.ts +14 -9
- package/web/rig-overlays.ts +26 -10
- 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';
|
|
@@ -34,7 +35,7 @@ import type {ReferenceBinding} from '../src/reference.ts';
|
|
|
34
35
|
import type { Checkpoint, Json, Reference, ReviewedSource, ReviewManifest } from '../src/manifest.ts';
|
|
35
36
|
import type { Feedback, ReviewSession } from '../src/session.ts';
|
|
36
37
|
import type { ReportEvent, ReportSeeds } from '../src/report.ts';
|
|
37
|
-
import {
|
|
38
|
+
import { frameAtTime, lastFrame as lastProducerFrame, rigPaneLabel, samplesClock, timeAtFrame, transportClock, type StageClock } from '../src/frame-clock.ts';
|
|
38
39
|
import { feedbackLastFrame, feedbackTargetNames, validateFeedbackSelection, type FeedbackCatalog } from '../src/feedback-validation.ts';
|
|
39
40
|
import { eventCoordinates, eventSeekTime, eventSelection, eventTimingLabel, reportCounts, reportDraftForMarker, targetLabel } from './report-view.ts';
|
|
40
41
|
import type { StripInterval, StripRequest } from '../src/strip.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';
|
|
@@ -82,6 +85,8 @@ let player: RigPlayer | null = null;
|
|
|
82
85
|
let animation = '';
|
|
83
86
|
let playing = false;
|
|
84
87
|
let time = 0;
|
|
88
|
+
/** The clock a scratch build on the stage states for its own animation (a retarget's report frameTimes, #164). */
|
|
89
|
+
let stageClock: StageClock | null = null;
|
|
85
90
|
let duration = 0;
|
|
86
91
|
let lastFrame = 0;
|
|
87
92
|
type CameraTarget='rig'|'reference';
|
|
@@ -105,6 +110,7 @@ let weightsView:WeightsView|null=null;
|
|
|
105
110
|
/** Setup · Bones (#109): the config's bone anchors on the stage, Check and a whole-rig Apply. */
|
|
106
111
|
let bonesPanel:BonesPanel|null=null;
|
|
107
112
|
let cutsPanel:CutsPanel|null=null;
|
|
113
|
+
let clothPanel:ClothPanel|null=null;
|
|
108
114
|
/** The hash-verified compiled model document the parts catalog was read from (mesh edit reads attachments from it). */
|
|
109
115
|
let partModel:unknown=null;
|
|
110
116
|
/** Apply · Rebuild (#99): the inspected model's URL, the producer status, and the applied record while its scratch build is on the stage. */
|
|
@@ -430,13 +436,14 @@ function renderFeedbackTargets(preserve = false): void {
|
|
|
430
436
|
}
|
|
431
437
|
|
|
432
438
|
function renderFrameTransport(): void {
|
|
433
|
-
const
|
|
439
|
+
const active = activeTransportClock(), clock = active?.clock ?? null;
|
|
434
440
|
const last = lastProducerFrame(clock), frame = frameAtTime(clock, time);
|
|
441
|
+
text('rig-label', rigPaneLabel({ hasPlayer: !!player, animation: player?.current ?? animation, time, frame }));
|
|
435
442
|
el('frame-controls').hidden = last === null;
|
|
436
443
|
if (!clock) { text('frame-label', data.report?.clock ? data.report.animation ? 'Frame unknown · report and current clip do not match' : 'Frame unknown · report animation not supplied' : 'rate unknown'); return; }
|
|
437
444
|
if (last === null) { text('frame-label', 'F? / ? · frame range unknown'); return; }
|
|
438
445
|
text('frame-label', `F${frame === null ? '?' : frame} / ${last}${frame === null ? ' · Out of range' : ''}`);
|
|
439
|
-
text('frame-clock-label', clock.kind === 'uniform' ? `${clock.fps} fps · frame0 ${clock.frame0} s` : `frameTimes · ${clock.frameTimes.length} explicit times`);
|
|
446
|
+
text('frame-clock-label', (clock.kind === 'uniform' ? `${clock.fps} fps · frame0 ${clock.frame0} s` : `frameTimes · ${clock.frameTimes.length} explicit times`) + (active?.source === 'stage' ? " · this build's report" : ''));
|
|
440
447
|
input('frame-number').max = String(last);
|
|
441
448
|
if (document.activeElement !== input('frame-number')) input('frame-number').value = frame === null ? '' : String(frame);
|
|
442
449
|
input('frame-scrub').max = String(last); input('frame-scrub').hidden = frame === null;
|
|
@@ -445,10 +452,11 @@ function renderFrameTransport(): void {
|
|
|
445
452
|
el('frame-outside').hidden = frame !== null;
|
|
446
453
|
}
|
|
447
454
|
|
|
448
|
-
function
|
|
455
|
+
function activeTransportClock() {
|
|
449
456
|
const active = player && player.current !== animation ? '' : animation;
|
|
450
|
-
return
|
|
457
|
+
return transportClock({ clock: data.report?.clock, animation: data.report?.animation }, active, !!player, stageClock, stageModelHash);
|
|
451
458
|
}
|
|
459
|
+
function activeFrameClock() { return activeTransportClock()?.clock ?? null; }
|
|
452
460
|
|
|
453
461
|
function seekFrame(frame: number): void {
|
|
454
462
|
const exact = timeAtFrame(activeFrameClock(), frame);
|
|
@@ -612,7 +620,7 @@ function tick(now: number): void {
|
|
|
612
620
|
if (player && player.current) { try { stepEnvironment(dt); drawRig(()=>player!.frame(dt)); } catch (error) { pause(); message(errorText(error), true); } }
|
|
613
621
|
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
622
|
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;
|
|
623
|
+
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
624
|
}
|
|
617
625
|
requestAnimationFrame(tick);
|
|
618
626
|
}
|
|
@@ -783,7 +791,7 @@ function bind(): void {
|
|
|
783
791
|
button('chest-export').onclick = () => { void exportChest(); };
|
|
784
792
|
for (const event of ['input', 'change']) el('chest-regions').addEventListener(event, renderChest);
|
|
785
793
|
// 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(); });
|
|
794
|
+
el('draft-recovery').addEventListener('click', () => { renderChest(); restoreEnvironment(); physicsPanel?.restore(); bonesPanel?.restore(); cutsPanel?.restore(); clothPanel?.restore(); idlePanel?.restore(); rigFieldsPanel?.restore(); });
|
|
787
795
|
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
796
|
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
797
|
for (const item of document.querySelectorAll<HTMLButtonElement>('[data-proposal]')) item.onclick = () => propose(item.dataset.proposal ?? '');
|
|
@@ -837,6 +845,8 @@ function bind(): void {
|
|
|
837
845
|
view:{...serializableView(numericView()),theme:appearance?.get()??'system',layers:{...layers}},
|
|
838
846
|
axis: axisInspection ? { id: select('axis-select').value, value: Number(input('axis-value').value) } : null,
|
|
839
847
|
environment: feedbackEnvironment(),
|
|
848
|
+
// The exported schedule (#112): the drawn clip's rigc-environment/1 bytes and their sha256, and whether readings and Apply read them.
|
|
849
|
+
environmentSchedule: feedbackSchedule() as unknown as Json,
|
|
840
850
|
// The exact last applied designation and the producer's line, only while that scratch build is what is drawn.
|
|
841
851
|
applied: appliedOnStage ? JSON.parse(JSON.stringify(appliedOnStage)) as Json : null,
|
|
842
852
|
// The readings chosen (#108): the pairs of the last finished run and the builds they ran on, not their results.
|
|
@@ -877,6 +887,10 @@ function bind(): void {
|
|
|
877
887
|
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
888
|
apply:applyCuts,reset:resetCutsBuild,token:name=>getComputedStyle(document.documentElement).getPropertyValue(name).trim(),route:path=>access.route(path)});
|
|
879
889
|
viewDisposers.push(()=>cutsPanel?.dispose());
|
|
890
|
+
// #111: the cloth recipe per tag; Apply runs rig-parts propose --cloth --write-config, then builds what it wrote.
|
|
891
|
+
clothPanel=bindClothPanel({project:()=>player?rigProject():null,mapping:()=>chestMapping,inspector:()=>selectedInspector(),pause,changed:()=>redrawOverlay(false),saved:()=>persistence?.save(),
|
|
892
|
+
apply:applyCloth,reset:resetClothBuild,token:name=>getComputedStyle(document.documentElement).getPropertyValue(name).trim()});
|
|
893
|
+
viewDisposers.push(()=>clothPanel?.dispose());
|
|
880
894
|
physicsPanel=bindPhysicsPanel({apply:async ops=>{await applyPhysics(ops);},reset:resetPhysicsBuild,pane:el('rig-pane'),saved:()=>persistence?.save(),
|
|
881
895
|
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
896
|
viewDisposers.push(()=>physicsPanel?.dispose());
|
|
@@ -916,8 +930,8 @@ function bind(): void {
|
|
|
916
930
|
|
|
917
931
|
async function start(): Promise<void> {
|
|
918
932
|
try {
|
|
919
|
-
let writes: { capabilityRequired: boolean };
|
|
920
|
-
[data, session, writes] = await Promise.all([api<ReviewResponse>('/api/review'), api<ReviewSession>('/api/session'), api<{ capabilityRequired: boolean }>('/api/access')]);
|
|
933
|
+
let writes: { capabilityRequired: boolean; control?: boolean };
|
|
934
|
+
[data, session, writes] = await Promise.all([api<ReviewResponse>('/api/review'), api<ReviewSession>('/api/session'), api<{ capabilityRequired: boolean; control?: boolean }>('/api/access')]);
|
|
921
935
|
readOnly = readOnlyReason(access, writes.capabilityRequired);
|
|
922
936
|
submittedBaseline=structuredClone(session);
|
|
923
937
|
text('package-title', data.manifest.title); text('revision', data.manifest.revision); document.title = `${data.manifest.title} · rig review`; text('source-hash', `source ${data.sourceHash.slice(0, 12)}`); select('review-status').value = session.status; select('feedback-target-kind').value = 'rig'; input('feedback-target-name').value = data.manifest.id;
|
|
@@ -941,8 +955,9 @@ async function start(): Promise<void> {
|
|
|
941
955
|
else if (readOnly) message(readOnly, true);
|
|
942
956
|
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
957
|
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();
|
|
945
|
-
|
|
958
|
+
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();
|
|
959
|
+
// Only an instance that serves agent control is asked for its config: a plain open has no control endpoint (#164).
|
|
960
|
+
if(!readOnly&&writes.control!==false)controlDispose=connectControl({sourceHash:data.sourceHash,mode:'single',route:access.route,headers:()=>access.headers(),observe:controlState,apply:applyControl,onStatus:value=>text('control-state',value)});
|
|
946
961
|
} catch (error) { message(`Could not open review package: ${errorText(error)}`, true); text('dirty', 'Load failed'); }
|
|
947
962
|
}
|
|
948
963
|
/** `feedback` is the review dialog, not an Inspector section: a user action opens it; a restored older bookmark that saved it selects nothing. */
|
|
@@ -963,7 +978,7 @@ function rigProject(){const root=el('rig-root');return (x:number,y:number)=>({x:
|
|
|
963
978
|
function redrawOverlay(fresh:boolean):void {
|
|
964
979
|
if(!overlays||!player)return;
|
|
965
980
|
const project=rigProject();
|
|
966
|
-
const regions=[...chestRegions(),...(meshPanel?.overlayRegions()??[]),...(cutsPanel?.overlayRegions()??[])],circles=[...chestCircles(),...(meshPanel?.overlayCircles()??[])],markers=[...chestMarkers(),...(cutsPanel?.overlayMarkers()??[])];
|
|
981
|
+
const regions=[...chestRegions(),...(meshPanel?.overlayRegions()??[]),...(cutsPanel?.overlayRegions()??[]),...(clothPanel?.overlayRegions()??[])],circles=[...chestCircles(),...(meshPanel?.overlayCircles()??[])],markers=[...chestMarkers(),...(cutsPanel?.overlayMarkers()??[])];
|
|
967
982
|
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
983
|
lastOverlay=value;
|
|
969
984
|
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 +1175,7 @@ function renderChest():void {
|
|
|
1160
1175
|
el('chest-mapping').classList.toggle('error',!chestMapping.available);
|
|
1161
1176
|
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
1177
|
el('chest-status').classList.toggle('error',!value.ok);
|
|
1163
|
-
chestEditor?.sync();meshEditor?.sync();weightsView?.sync();bonesPanel?.sync();cutsPanel?.sync();redrawOverlay(false);
|
|
1178
|
+
chestEditor?.sync();meshEditor?.sync();weightsView?.sync();bonesPanel?.sync();cutsPanel?.sync();clothPanel?.sync();redrawOverlay(false);
|
|
1164
1179
|
}
|
|
1165
1180
|
async function exportChest():Promise<void> {
|
|
1166
1181
|
const value=chestEvaluation();if(!value.ok){message(`Chest regions not exported: ${value.error}`,true);return;}
|
|
@@ -1170,7 +1185,7 @@ async function exportChest():Promise<void> {
|
|
|
1170
1185
|
message('Downloaded chest-regions.json and chest-regions.binding.json. The regions are review evidence, not an approval; the package is unchanged.');
|
|
1171
1186
|
}
|
|
1172
1187
|
/** 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'];
|
|
1188
|
+
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
1189
|
function environmentApplied():boolean{return input('environment-applied').checked;}
|
|
1175
1190
|
function gravityValue():number{return Number(input('gravity').value);}
|
|
1176
1191
|
/** Called from `tick` right before `player.frame(dt)`: the gust clock is the stepped time, so a paused player freezes it. */
|
|
@@ -1188,6 +1203,55 @@ function renderEnvironment():void {
|
|
|
1188
1203
|
text('wind-readout',(environmentApplied()?windReadout(reading,gravityValue()):RUNTIME_START_READOUT)+environmentNote);
|
|
1189
1204
|
const gust=button('wind-gust');gust.disabled=reading.gusting;gust.setAttribute('aria-pressed',String(reading.gusting));gust.textContent=reading.gusting?'Gusting…':'Gust';
|
|
1190
1205
|
text('wind-base-label',input('wind-base').value);text('gravity-label',gravityValue().toFixed(2));
|
|
1206
|
+
renderSchedule();
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* #112 · Use for readings: the controls as the exported `rigc-environment/1` schedule (src/environment-schedule.ts). While
|
|
1210
|
+
* on, readings send the drawn clip's schedule, Apply's check reads the checked clip's (`idle` when the rig has one, as
|
|
1211
|
+
* `rig-parts check` renders it), and the wind clock restarts with the clip so the person watches the world rigc reads.
|
|
1212
|
+
*/
|
|
1213
|
+
function useWorld():boolean{return input('environment-use').checked&&environmentApplied();}
|
|
1214
|
+
function clipDuration(name:string):number{return assets?.animations.find(item=>item.name===name)?.duration??data.manifest.animations.find(item=>item.id===name)?.duration??0;}
|
|
1215
|
+
/** The clip `rig-parts check` poses its idle readings over: `idle` when the rig has one, else the drawn clip. */
|
|
1216
|
+
function checkAnimation():string{return assets?.animations.some(item=>item.name==='idle')?'idle':animation;}
|
|
1217
|
+
let worldCache:{key:string;value:WorldExport|{error:string}}|null=null;
|
|
1218
|
+
function worldFor(name:string):WorldExport|{error:string} {
|
|
1219
|
+
const key=JSON.stringify([wind.snapshot(),gravityValue(),environmentApplied(),name,clipDuration(name)]);
|
|
1220
|
+
if(worldCache?.key!==key)worldCache={key,value:worldExport(wind.snapshot(),gravityValue(),environmentApplied(),name,clipDuration(name))};
|
|
1221
|
+
return worldCache.value;
|
|
1222
|
+
}
|
|
1223
|
+
/** A fresh wind clock at the same settings (a repeating gust restarts at 0); pending jolts keep their distance in time. */
|
|
1224
|
+
function restartWindClock():void {const now=wind.read().time;wind.restore(wind.snapshot());joltQueue=joltQueue.map(step=>({...step,at:Math.max(0,step.at-now)}));}
|
|
1225
|
+
let scheduleShown='';
|
|
1226
|
+
function renderSchedule():void {
|
|
1227
|
+
const world=worldFor(animation),check=checkAnimation(),checked=check!==animation?worldFor(check):null;
|
|
1228
|
+
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}`:''}`;
|
|
1229
|
+
if(shown!==scheduleShown){scheduleShown=shown;text('environment-schedule',shown);}
|
|
1230
|
+
}
|
|
1231
|
+
function feedbackSchedule(){if(!player||!environmentApplied())return null;const world=worldFor(animation);return 'error' in world?null:scheduleContext(world,useWorld());}
|
|
1232
|
+
let environmentCheckBusy=false;
|
|
1233
|
+
/** Check under this world: `rig-parts check --environment` on the build on the stage, beside that build's default-world check. */
|
|
1234
|
+
async function runEnvironmentCheck():Promise<EnvironmentCheckView|{status:'refused';error:string}|null> {
|
|
1235
|
+
if(environmentCheckBusy)return null;
|
|
1236
|
+
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;}));};
|
|
1237
|
+
const name=checkAnimation(),world=worldFor(name);
|
|
1238
|
+
if('error' in world){show([{kind:'error',text:world.error}]);return {status:'refused',error:world.error};}
|
|
1239
|
+
const build=appliedOnStage?'scratch':'inspected',before=stageCheck;
|
|
1240
|
+
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…`}]);
|
|
1241
|
+
try{
|
|
1242
|
+
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+: /,'')}));
|
|
1243
|
+
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};}
|
|
1244
|
+
const started=performance.now();
|
|
1245
|
+
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}`);}
|
|
1246
|
+
const checked=job;
|
|
1247
|
+
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)'}`;
|
|
1248
|
+
// 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).
|
|
1249
|
+
const baseline=rebuildStatus?.baseline,noDefault=build==='inspected'&&!inspectedCheckView()&&baseline&&baseline.status!=='running'&&baseline.status!=='reproduced';
|
|
1250
|
+
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));
|
|
1251
|
+
draw();if(build==='inspected'&&inspectedCheckView()?.status!=='checked')void followInspectedCheck(draw);
|
|
1252
|
+
return job;
|
|
1253
|
+
}catch(error){show([{kind:'error',text:`ENVIRONMENT_CHECK_FAILED: ${errorText(error)}`}]);return null;}
|
|
1254
|
+
finally{environmentCheckBusy=false;button('environment-check').disabled=!player;}
|
|
1191
1255
|
}
|
|
1192
1256
|
/** Every control change engages the environment; until then the player stands at the runtime start values. */
|
|
1193
1257
|
function engage(change:()=>void):void {
|
|
@@ -1207,6 +1271,15 @@ function bindEnvironment():void {
|
|
|
1207
1271
|
button('wind-gust').onclick=()=>engage(()=>wind.triggerGust());
|
|
1208
1272
|
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
1273
|
button('physics-reset').onclick=resetPhysics;
|
|
1274
|
+
input('environment-use').onchange=()=>{
|
|
1275
|
+
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;}
|
|
1276
|
+
const world=worldFor(animation);
|
|
1277
|
+
if('error' in world){input('environment-use').checked=false;message(world.error,true);renderSchedule();return;}
|
|
1278
|
+
// The watched world starts where the schedule does: a fresh wind clock at the clip's first frame.
|
|
1279
|
+
restartWindClock();seek(0);applyEnvironment();message(`Use for readings: ${world.summary} · the wind clock restarts with the clip.`);
|
|
1280
|
+
};
|
|
1281
|
+
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.`);};
|
|
1282
|
+
button('environment-check').onclick=()=>{void runEnvironmentCheck();};
|
|
1210
1283
|
input('drag-trail').onchange=()=>{el('rig-viewport').dataset.trail=String(input('drag-trail').checked);};
|
|
1211
1284
|
// Drag to trail: the rig pane's pointer drag becomes `player.moved` before the pan handler sees it; Space still pans.
|
|
1212
1285
|
let space=false,trail:{id:number;x:number;y:number}|null=null;
|
|
@@ -1309,6 +1382,8 @@ async function applyRetarget(body:{sequence:string;map:PoseMap;flags:RetargetFla
|
|
|
1309
1382
|
retargetPanel?.result(result);retargetPanel?.status(outcome.text,outcome.error);
|
|
1310
1383
|
if(next.id===null||!('id' in result)){message(outcome.text,true);return result;}
|
|
1311
1384
|
await swapRig(new URL(access.route(next.url),location.href).href,access.route(`/rebuild/${next.id}/`),next.hash,world);
|
|
1385
|
+
// The new animation's own clock (the run's report frameTimes), bound to this build: the transport counter and the rig pane read it.
|
|
1386
|
+
const own=samplesClock(result.frameTimes);stageClock=own?{animation:result.request.record.flags.animation,modelHash:next.hash,clock:own}:null;
|
|
1312
1387
|
appliedOnStage=retargetAppliedRecord(result);stageCheck=null;
|
|
1313
1388
|
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=JSON.stringify(appliedOnStage);draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1314
1389
|
// The new animation plays against the reference: choose it, and show the sequence it was made from beside it.
|
|
@@ -1354,6 +1429,7 @@ async function loadRebuildStatus():Promise<RebuildStatusView|null> {
|
|
|
1354
1429
|
retargetPanel?.load(rebuildStatus.retarget??null,rebuildStatus.reason);
|
|
1355
1430
|
bonesPanel?.load(rebuildStatus.inputs?rebuildStatus.bones??[]:null,rebuildStatus.inputs?null:rebuildStatus.reason??'BONES_UNAVAILABLE: no producer handoff');
|
|
1356
1431
|
rigFieldsPanel?.load(rebuildStatus.inputs?rebuildStatus.fields??{}:null,rebuildStatus.inputs?null:rebuildStatus.reason??'RIG_FIELDS_UNAVAILABLE: no producer handoff');
|
|
1432
|
+
clothPanel?.load(rebuildStatus.inputs?rebuildStatus.cloth??null:null,rebuildStatus.inputs?null:rebuildStatus.reason??'CLOTH_UNAVAILABLE: no producer handoff');
|
|
1357
1433
|
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
1434
|
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
1435
|
return rebuildStatus;
|
|
@@ -1410,7 +1486,10 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1410
1486
|
const lines:string[]=[];
|
|
1411
1487
|
rebuildBusy=true;rebuildState();chestEditor?.notify(`Applying patch (${patch.op.cmd}, ${patch.ops.length} op(s))…`,false,[]);
|
|
1412
1488
|
try{
|
|
1413
|
-
|
|
1489
|
+
// #112: with "Use for readings" on, Apply's check reads the exported schedule of the clip check renders (its idle).
|
|
1490
|
+
const scheduled=useWorld()?worldFor(checkAnimation()):null;
|
|
1491
|
+
if(scheduled&&'error' in scheduled){chestEditor?.notify(scheduled.error,true);message(scheduled.error,true);return {status:'refused',error:scheduled.error,producerLine:scheduled.error};}
|
|
1492
|
+
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
1493
|
onLine:line=>{lines.push(line);chestEditor?.notify(line.trim()||'…',false,lines.slice(-6));}});
|
|
1415
1494
|
const outcome=patchOutcomeLine(result),next=stageModel(result,{url:'',hash:stageModelHash??''});
|
|
1416
1495
|
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 +1533,8 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1454
1533
|
const posedTime=player.time;if(steps.length){if(environmentApplied())player.environment(environmentFor(wind.read(),gravityValue()));else player.environment(null);}
|
|
1455
1534
|
return posedTime;},
|
|
1456
1535
|
/** Readings (#108): run (the pickers' choice, or the given readings), the last job, the ticks drawn and the worst lines shown. */
|
|
1536
|
+
/** #112: the exported schedule of a clip (the drawn one by default), Use for readings, and Check under this world. */
|
|
1537
|
+
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
1538
|
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
1539
|
/** Idle tracks (#104): the panel's state and ops, its edits, Apply and Reset, the preview, and the inspection-only `sample`. */
|
|
1459
1540
|
idle:{state:()=>idlePanel?.state()??null,ops:()=>idlePanel?.ops()??[],source:()=>idlePanel?.source()??null,keying:()=>idlePanel?.keying()??null,refusals:()=>idlePanel?.refusals()??[],
|
|
@@ -1471,6 +1552,9 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1471
1552
|
cuts:{source:()=>cutsPanel?.source()??null,ops:()=>cutsPanel?.ops()??[],handles:()=>cutsPanel?.handles()??[],active:()=>cutsPanel?.active()??false,setActive:(on:boolean)=>cutsPanel?.setActive(on),
|
|
1472
1553
|
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
1554
|
apply:()=>cutsPanel?.apply()??Promise.resolve(null),reset:()=>resetCutsBuild()},
|
|
1555
|
+
/** #111: the Cloth block — the selected tag, its form values, the pending ops, the link preview, Apply and Reset. */
|
|
1556
|
+
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),
|
|
1557
|
+
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
1558
|
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
1559
|
active:()=>bonesPanel?.active()??false,setActive:(on:boolean)=>bonesPanel?.setActive(on),handles:()=>bonesPanel?.handles()??[],check:()=>bonesPanel?.check()??Promise.resolve(null),
|
|
1476
1560
|
apply:()=>applyBones((bonesPanel?.edits()??[]).map(e=>({op:'replace' as const,path:e.path,value:[...e.value],name:e.name}))),reset:()=>resetBonesBuild(),
|
|
@@ -1532,7 +1616,9 @@ async function runReadings(override?:{readings:import('../src/readings.ts').Read
|
|
|
1532
1616
|
const chosen=override??readingsPanel.requests();
|
|
1533
1617
|
if('error' in chosen){readingsPanel.status(chosen.error,true);return {status:'refused',error:chosen.error};}
|
|
1534
1618
|
if(!animation){readingsPanel.status('READINGS_NO_ANIMATION: no animation is drawn',true);return null;}
|
|
1535
|
-
|
|
1619
|
+
// #112: with "Use for readings" on, the exported schedule of the drawn clip goes in place of the standing world.
|
|
1620
|
+
let environment:ReturnType<typeof environmentFor>|WorldExport['schedule']|null=player&&environmentApplied()?environmentFor(wind.read(),gravityValue()):null;
|
|
1621
|
+
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
1622
|
const body={animation,readings:chosen.readings,environment};
|
|
1537
1623
|
readingsBusy=true;readingsPanel.busy(true);readingsPanel.status(`Running ${chosen.readings.length} reading(s) of ${animation} with rigc render…`,false);
|
|
1538
1624
|
const started=performance.now();
|
|
@@ -1549,7 +1635,8 @@ async function runReadings(override?:{readings:import('../src/readings.ts').Read
|
|
|
1549
1635
|
readingsJob=job;readingsPanel.lines(job);renderReadingTicks(true);
|
|
1550
1636
|
const ticks=job.runs.map(r=>`${r.build} ${r.results.reduce((n,x)=>n+x.frames.length,0)} tick(s)`).join(' · ');
|
|
1551
1637
|
const refused=job.runs.flatMap(r=>r.results).find(r=>r.status!=='read');
|
|
1552
|
-
const
|
|
1638
|
+
const scheduled=job.request.environment&&'spec' in job.request.environment?job.request.environment:null;
|
|
1639
|
+
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
1640
|
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
1641
|
return job;
|
|
1555
1642
|
}catch(error){readingsPanel.status(`READINGS_FAILED: ${errorText(error)}`,true);return null;}
|
|
@@ -1617,6 +1704,29 @@ async function applyCuts(ops:PatchOp[]):Promise<PatchJobView|RebuildRefusal|null
|
|
|
1617
1704
|
return result;
|
|
1618
1705
|
}finally{cutsPanel?.busy(false);}
|
|
1619
1706
|
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Cloth Apply (#111): the pending recipe ops become one `rig` patch, which the server runs as `rig-parts propose --cloth
|
|
1709
|
+
* --write-config` and then builds the written config; the scratch build reloads in place under the stage's world.
|
|
1710
|
+
*/
|
|
1711
|
+
async function applyCloth(ops:PatchOp[]):Promise<PatchJobView|RebuildRefusal|null> {
|
|
1712
|
+
const status=rebuildStatus??await loadRebuildStatus();
|
|
1713
|
+
const patch=status&&ops.length?makePatch(status,ops,{cmd:'rig'}):null;
|
|
1714
|
+
if(!patch){const reason=!ops.length?'CLOTH_NOTHING_PENDING: no pending ops':status?.reason??'REBUILD_STATUS_UNAVAILABLE';clothPanel?.status(reason,true);return null;}
|
|
1715
|
+
const route=status?.cloth?.route==='build'?'build (assemble → rig → check)':'rig --config, check';
|
|
1716
|
+
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);
|
|
1717
|
+
try{
|
|
1718
|
+
const result=await applyPatch(patch,stageWorld());
|
|
1719
|
+
const outcome=patchOutcomeLine(result);clothPanel?.status(outcome.text,outcome.error);
|
|
1720
|
+
if('id' in result&&result.status==='built')redrawOverlay(true);
|
|
1721
|
+
return result;
|
|
1722
|
+
}finally{clothPanel?.busy(false);}
|
|
1723
|
+
}
|
|
1724
|
+
/** Cloth Reset: the inspected build back on the stage (same world), and no pending op. */
|
|
1725
|
+
async function resetClothBuild():Promise<void> {
|
|
1726
|
+
clothPanel?.clear();clothPanel?.shown(null);
|
|
1727
|
+
if(appliedOnStage){clothPanel?.busy(true);try{await resetRebuild(stageWorld());}finally{clothPanel?.busy(false);}}
|
|
1728
|
+
clothPanel?.status('Reset: the inspected build is on the stage; no pending ops.',false);
|
|
1729
|
+
}
|
|
1620
1730
|
/** Cuts Reset: the inspected build back on the stage (same world), and no pending op. */
|
|
1621
1731
|
async function resetCutsBuild():Promise<void> {
|
|
1622
1732
|
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,14 +118,15 @@ 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. */
|
|
124
127
|
export interface DeltaRequestBody {part:string;delta:import('../src/mesh-edit.ts').MeshEditDocument;configHash:string}
|
|
125
128
|
/** A retarget Apply's job (#114): clean, retarget, compile; every FAIL line of the step that refused, verbatim; the listed lines. */
|
|
126
|
-
export interface RetargetJobView {id:string;kind:'retarget';op:'retarget';status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;refusals:string[];exitCode:number|null;wallMs:{clean:number|null;retarget:number|null;compile:number|null};files:FilesDiff|null;motion:MotionSummary|null;readings:RetargetLine[];build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{record:RetargetRecord;recordSha256:string;configHash:string};producer:{package:string;version:string};compiler:{package:string;version:string}}
|
|
129
|
+
export interface RetargetJobView {id:string;kind:'retarget';op:'retarget';status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;refusals:string[];exitCode:number|null;wallMs:{clean:number|null;retarget:number|null;compile:number|null};files:FilesDiff|null;motion:MotionSummary|null;readings:RetargetLine[];frameTimes?:number[]|null;build:{modelUrl:string;modelHash:string;files:Record<string,string>}|null;request:{record:RetargetRecord;recordSha256:string;configHash:string};producer:{package:string;version:string};compiler:{package:string;version:string}}
|
|
127
130
|
/** `{retarget: {sequence, map, flags, configHash}}`: a declared reference id, the map by content and typed flags — no path, no flag string. */
|
|
128
131
|
export interface RetargetRequestBody {retarget:{sequence:string;map:Record<string,unknown>;flags:RetargetFlags;configHash:string}}
|
|
129
132
|
/** What the draft keeps and `context.applied` carries for a retarget: the record, the producer's line, the motion summary, the diff and the lines. */
|
|
@@ -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} {
|
package/web/rig-overlays.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Inspection graphics from the last successfully drawn public player pose. */
|
|
2
2
|
import {partBounds} from './part-selection.ts';
|
|
3
|
+
import {placeLabels} from './label-placement.ts';
|
|
3
4
|
import type {BoneWorld,PosedFrame,RigPlayer} from '@rig-c/play';
|
|
4
5
|
|
|
5
6
|
export interface OverlayPoint {x:number;y:number}
|
|
@@ -203,7 +204,8 @@ export class RigOverlays {
|
|
|
203
204
|
const c=region.colors;
|
|
204
205
|
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
206
|
const top=points.reduce((a,p)=>p!.y<a!.y?p:a,points[0]);
|
|
206
|
-
|
|
207
|
+
// An empty name draws no label (#111: one label per chain set, not per chain).
|
|
208
|
+
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
209
|
}
|
|
208
210
|
}
|
|
209
211
|
if(this.playerAvailable)for(const circle of circles){
|
|
@@ -220,15 +222,29 @@ export class RigOverlays {
|
|
|
220
222
|
ctx.fillStyle=circle.flash?c.flash:c.labelBg;ctx.fillRect(lx,ly-11,width,15);ctx.fillStyle=circle.flash?c.ink:c.stroke;ctx.fillText(circle.label,lx+4,ly);
|
|
221
223
|
}
|
|
222
224
|
}
|
|
223
|
-
if(this.playerAvailable)
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
if(this.playerAvailable){
|
|
226
|
+
const drawn:{marker:OverlayMarker;at:OverlayPoint}[]=[];
|
|
227
|
+
for(const marker of markers){
|
|
228
|
+
const at=projected(marker.at);if(!at)continue;const c=marker.colors;drawn.push({marker,at});
|
|
229
|
+
ctx.globalAlpha=.9;ctx.setLineDash([3,3]);ctx.strokeStyle=c.stroke;ctx.lineWidth=1.2;
|
|
230
|
+
for(const to of marker.guideTo??[]){const end=projected(to);if(!end)continue;ctx.beginPath();ctx.moveTo(at.x,at.y);ctx.lineTo(end.x,end.y);ctx.stroke();}
|
|
231
|
+
ctx.setLineDash([]);ctx.globalAlpha=1;const size=marker.active?7:5.5;
|
|
232
|
+
ctx.beginPath();ctx.moveTo(at.x,at.y-size);ctx.lineTo(at.x+size,at.y);ctx.lineTo(at.x,at.y+size);ctx.lineTo(at.x-size,at.y);ctx.closePath();
|
|
233
|
+
ctx.fillStyle=c.fill;ctx.fill();ctx.strokeStyle=c.ink;ctx.lineWidth=marker.active?2.4:1.6;ctx.stroke();
|
|
234
|
+
}
|
|
235
|
+
// Labels avoid each other (#147): a moved label keeps a leader to its point; one with no free slot shows on hover only.
|
|
236
|
+
ctx.font='600 11px ui-monospace, monospace';
|
|
237
|
+
const placed=placeLabels(drawn.map(({marker,at})=>({at,width:ctx.measureText(marker.tag).width+8,pinned:marker.active})),{gap:10,rise:3,bounds:{width,height},obstacles:drawn.map(({marker,at})=>{const r=marker.active?8:6.5;return {x:at.x-r,y:at.y-r,w:r*2,h:r*2};})});
|
|
238
|
+
// Leaders first, so no leader crosses another joint's label.
|
|
239
|
+
drawn.forEach(({marker,at},i)=>{
|
|
240
|
+
const {box,visible,moved}=placed[i]!;if(!visible||!moved)return;
|
|
241
|
+
const ex=Math.min(Math.max(at.x,box.x),box.x+box.w),ey=Math.min(Math.max(at.y,box.y),box.y+box.h);
|
|
242
|
+
ctx.globalAlpha=.85;ctx.strokeStyle=marker.colors.stroke;ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(at.x,at.y);ctx.lineTo(ex,ey);ctx.stroke();ctx.globalAlpha=1;
|
|
243
|
+
});
|
|
244
|
+
drawn.forEach(({marker},i)=>{
|
|
245
|
+
const {box,visible}=placed[i]!,c=marker.colors;if(!visible)return;
|
|
246
|
+
ctx.fillStyle=c.labelBg;ctx.fillRect(box.x,box.y,box.w,box.h);ctx.fillStyle=c.stroke;ctx.fillText(marker.tag,box.x+4,box.y+11);
|
|
247
|
+
});
|
|
232
248
|
}
|
|
233
249
|
ctx.globalAlpha=1;
|
|
234
250
|
if(this.playerAvailable&&meshEdit)this.drawMeshEdit(ctx,meshEdit,projected,width,height);
|
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)}
|