@rig-c/review 0.8.0 → 0.9.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 -11
- package/docs/DESIGN.md +1 -1
- package/docs/REVIEW_PACKAGE.md +13 -7
- package/package.json +1 -1
- package/src/baseline-reach.ts +177 -0
- package/src/baseline.ts +14 -9
- package/src/cli.ts +2 -2
- package/src/compare-server.ts +1 -1
- package/src/frame-clock.ts +30 -0
- package/src/front.ts +3 -0
- package/src/handoff.ts +59 -7
- package/src/manifest.ts +13 -3
- package/src/open-protocol.ts +4 -3
- package/src/open.ts +12 -3
- package/src/rebuild.ts +198 -21
- package/src/server.ts +8 -1
- package/web/compare.ts +7 -5
- package/web/index.html +1 -1
- package/web/label-placement.ts +46 -0
- package/web/main.ts +73 -10
- package/web/rebuild-client.ts +37 -7
- package/web/rig-overlays.ts +24 -9
- package/web/styles.css +1 -0
package/web/main.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {bindClothPanel,type ClothPanel} from './cloth-panel.ts';
|
|
|
19
19
|
import {retargetAppliedRecord,retargetOutcomeLine,type AppliedRetarget,type RetargetJobView,type RetargetRequestBody} from './rebuild-client.ts';
|
|
20
20
|
import {bindRetargetPanel,type PoseMap,type RetargetPanel} from './retarget-panel.ts';
|
|
21
21
|
import type {RetargetFlags} from '../src/retarget.ts';
|
|
22
|
-
import {appliedRecord,deltaAppliedRecord,deltaOutcomeLine,designatedRegions,handoffText,landmarksRequest,seedLandmarks,makePatch,outcomeLine,patchAppliedRecord,patchOutcomeLine,pickPart,runRebuild,seedFields,stageModel,type AppliedDelta,type AppliedPatch,type AppliedRecord,type DeltaJobView,type DeltaRequestBody,type ConfigPatch,type PatchJobView,type RebuildJobView,type RebuildRefusal,type RebuildRequestBody,type RebuildStatusView} from './rebuild-client.ts';
|
|
22
|
+
import {historyRow,recordFor,refusedText,undoControl,type AppliedHistoryView,type UndoView,type AnyJobView,appliedRecord,deltaAppliedRecord,deltaOutcomeLine,designatedRegions,handoffText,landmarksRequest,seedLandmarks,makePatch,outcomeLine,patchAppliedRecord,patchOutcomeLine,pickPart,runRebuild,seedFields,stageModel,type AppliedDelta,type AppliedPatch,type AppliedRecord,type DeltaJobView,type DeltaRequestBody,type ConfigPatch,type PatchJobView,type RebuildJobView,type RebuildRefusal,type RebuildRequestBody,type RebuildStatusView} from './rebuild-client.ts';
|
|
23
23
|
import {meshEditBinding,meshEditText,type MeshEditDocument} from '../src/mesh-edit.ts';
|
|
24
24
|
import {chestRegionsBinding,cropWorldMapping,type CropWorldMapping} from '../src/chest-regions.ts';
|
|
25
25
|
import {bindWorkspaceUI,selectedInspector,type WorkspaceUI} from './workspace-ui.ts';
|
|
@@ -35,7 +35,7 @@ import type {ReferenceBinding} from '../src/reference.ts';
|
|
|
35
35
|
import type { Checkpoint, Json, Reference, ReviewedSource, ReviewManifest } from '../src/manifest.ts';
|
|
36
36
|
import type { Feedback, ReviewSession } from '../src/session.ts';
|
|
37
37
|
import type { ReportEvent, ReportSeeds } from '../src/report.ts';
|
|
38
|
-
import {
|
|
38
|
+
import { frameAtTime, lastFrame as lastProducerFrame, rigPaneLabel, samplesClock, timeAtFrame, transportClock, type StageClock } from '../src/frame-clock.ts';
|
|
39
39
|
import { feedbackLastFrame, feedbackTargetNames, validateFeedbackSelection, type FeedbackCatalog } from '../src/feedback-validation.ts';
|
|
40
40
|
import { eventCoordinates, eventSeekTime, eventSelection, eventTimingLabel, reportCounts, reportDraftForMarker, targetLabel } from './report-view.ts';
|
|
41
41
|
import type { StripInterval, StripRequest } from '../src/strip.ts';
|
|
@@ -85,6 +85,8 @@ let player: RigPlayer | null = null;
|
|
|
85
85
|
let animation = '';
|
|
86
86
|
let playing = false;
|
|
87
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;
|
|
88
90
|
let duration = 0;
|
|
89
91
|
let lastFrame = 0;
|
|
90
92
|
type CameraTarget='rig'|'reference';
|
|
@@ -434,13 +436,14 @@ function renderFeedbackTargets(preserve = false): void {
|
|
|
434
436
|
}
|
|
435
437
|
|
|
436
438
|
function renderFrameTransport(): void {
|
|
437
|
-
const
|
|
439
|
+
const active = activeTransportClock(), clock = active?.clock ?? null;
|
|
438
440
|
const last = lastProducerFrame(clock), frame = frameAtTime(clock, time);
|
|
441
|
+
text('rig-label', rigPaneLabel({ hasPlayer: !!player, animation: player?.current ?? animation, time, frame }));
|
|
439
442
|
el('frame-controls').hidden = last === null;
|
|
440
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; }
|
|
441
444
|
if (last === null) { text('frame-label', 'F? / ? · frame range unknown'); return; }
|
|
442
445
|
text('frame-label', `F${frame === null ? '?' : frame} / ${last}${frame === null ? ' · Out of range' : ''}`);
|
|
443
|
-
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" : ''));
|
|
444
447
|
input('frame-number').max = String(last);
|
|
445
448
|
if (document.activeElement !== input('frame-number')) input('frame-number').value = frame === null ? '' : String(frame);
|
|
446
449
|
input('frame-scrub').max = String(last); input('frame-scrub').hidden = frame === null;
|
|
@@ -449,10 +452,11 @@ function renderFrameTransport(): void {
|
|
|
449
452
|
el('frame-outside').hidden = frame !== null;
|
|
450
453
|
}
|
|
451
454
|
|
|
452
|
-
function
|
|
455
|
+
function activeTransportClock() {
|
|
453
456
|
const active = player && player.current !== animation ? '' : animation;
|
|
454
|
-
return
|
|
457
|
+
return transportClock({ clock: data.report?.clock, animation: data.report?.animation }, active, !!player, stageClock, stageModelHash);
|
|
455
458
|
}
|
|
459
|
+
function activeFrameClock() { return activeTransportClock()?.clock ?? null; }
|
|
456
460
|
|
|
457
461
|
function seekFrame(frame: number): void {
|
|
458
462
|
const exact = timeAtFrame(activeFrameClock(), frame);
|
|
@@ -926,8 +930,8 @@ function bind(): void {
|
|
|
926
930
|
|
|
927
931
|
async function start(): Promise<void> {
|
|
928
932
|
try {
|
|
929
|
-
let writes: { capabilityRequired: boolean };
|
|
930
|
-
[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')]);
|
|
931
935
|
readOnly = readOnlyReason(access, writes.capabilityRequired);
|
|
932
936
|
submittedBaseline=structuredClone(session);
|
|
933
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;
|
|
@@ -952,7 +956,8 @@ async function start(): Promise<void> {
|
|
|
952
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.');
|
|
953
957
|
requestAnimationFrame(tick);
|
|
954
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();
|
|
955
|
-
|
|
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)});
|
|
956
961
|
} catch (error) { message(`Could not open review package: ${errorText(error)}`, true); text('dirty', 'Load failed'); }
|
|
957
962
|
}
|
|
958
963
|
/** `feedback` is the review dialog, not an Inspector section: a user action opens it; a restored older bookmark that saved it selects nothing. */
|
|
@@ -1266,6 +1271,7 @@ function bindEnvironment():void {
|
|
|
1266
1271
|
button('wind-gust').onclick=()=>engage(()=>wind.triggerGust());
|
|
1267
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.');};
|
|
1268
1273
|
button('physics-reset').onclick=resetPhysics;
|
|
1274
|
+
button('applied-undo').onclick=()=>{void undoApply();};
|
|
1269
1275
|
input('environment-use').onchange=()=>{
|
|
1270
1276
|
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
1277
|
const world=worldFor(animation);
|
|
@@ -1377,6 +1383,8 @@ async function applyRetarget(body:{sequence:string;map:PoseMap;flags:RetargetFla
|
|
|
1377
1383
|
retargetPanel?.result(result);retargetPanel?.status(outcome.text,outcome.error);
|
|
1378
1384
|
if(next.id===null||!('id' in result)){message(outcome.text,true);return result;}
|
|
1379
1385
|
await swapRig(new URL(access.route(next.url),location.href).href,access.route(`/rebuild/${next.id}/`),next.hash,world);
|
|
1386
|
+
// The new animation's own clock (the run's report frameTimes), bound to this build: the transport counter and the rig pane read it.
|
|
1387
|
+
const own=samplesClock(result.frameTimes);stageClock=own?{animation:result.request.record.flags.animation,modelHash:next.hash,clock:own}:null;
|
|
1380
1388
|
appliedOnStage=retargetAppliedRecord(result);stageCheck=null;
|
|
1381
1389
|
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=JSON.stringify(appliedOnStage);draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1382
1390
|
// The new animation plays against the reference: choose it, and show the sequence it was made from beside it.
|
|
@@ -1434,7 +1442,60 @@ function renderHandoff(status:RebuildStatusView):void {
|
|
|
1434
1442
|
if(handoffTimer)clearTimeout(handoffTimer);handoffTimer=null;
|
|
1435
1443
|
if(status.baseline?.status==='running')handoffTimer=setTimeout(()=>{void api<RebuildStatusView>('/api/rebuild').then(next=>{rebuildStatus=next;renderHandoff(next);}).catch(()=>{});},2000);
|
|
1436
1444
|
}
|
|
1437
|
-
function rebuildState():void {chestEditor?.rebuildState({busy:rebuildBusy,applied:!!appliedOnStage});}
|
|
1445
|
+
function rebuildState():void {chestEditor?.rebuildState({busy:rebuildBusy,applied:!!appliedOnStage});renderApplied();if(!rebuildBusy)void refreshApplied();}
|
|
1446
|
+
/** #166: the session's applied records (the server's stack), the last one on the stage when the stage shows it. */
|
|
1447
|
+
let appliedHistory:AppliedHistoryView|null=null;
|
|
1448
|
+
async function refreshApplied():Promise<void> {
|
|
1449
|
+
if(!inspectedModel)return;
|
|
1450
|
+
try{appliedHistory=await api<AppliedHistoryView>('/api/rebuild/history');}catch{return;}
|
|
1451
|
+
renderApplied();
|
|
1452
|
+
}
|
|
1453
|
+
function renderApplied():void {
|
|
1454
|
+
const section=document.getElementById('applied-records');if(!section)return;
|
|
1455
|
+
const h=appliedHistory;section.hidden=!h||h.depth===0;
|
|
1456
|
+
const list=el('applied-list');list.replaceChildren(...(h?.records??[]).map((r,i)=>{const li=document.createElement('li');li.textContent=historyRow(h!,i);li.dataset.buildId=r.id;li.classList.toggle('on-stage',i===h!.records.length-1&&appliedOnStage?.buildId===r.id);return li;}));
|
|
1457
|
+
const control=undoControl(h,appliedOnStage?.buildId??null,rebuildBusy||!!readOnly),undo=button('applied-undo');
|
|
1458
|
+
undo.textContent=control.label;undo.disabled=!control.enabled;undo.title=control.title;
|
|
1459
|
+
}
|
|
1460
|
+
function appliedStatus(text:string,error=false):void {const node=el('applied-status');node.textContent=text;node.classList.toggle('error',error);}
|
|
1461
|
+
/**
|
|
1462
|
+
* #166: Undo last Apply — the last record leaves the server's stack and the stage returns to its predecessor's build
|
|
1463
|
+
* (kept on disk, or re-run from its record when evicted) or to the inspected build, under the same world. The feedback
|
|
1464
|
+
* context then carries the post-undo record (or null), never the reverted one. A refusal leaves the stage as it was.
|
|
1465
|
+
*/
|
|
1466
|
+
async function undoApply():Promise<UndoView|null> {
|
|
1467
|
+
const from=appliedOnStage?.buildId;
|
|
1468
|
+
if(rebuildBusy||readOnly||!from||!inspectedModel)return null;
|
|
1469
|
+
const world=stageWorld();
|
|
1470
|
+
rebuildBusy=true;rebuildState();appliedStatus('Undoing the last Apply…');
|
|
1471
|
+
try{
|
|
1472
|
+
let result=await api<UndoView>('/api/rebuild/undo',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({from})});
|
|
1473
|
+
if(result.status==='running'){
|
|
1474
|
+
// An evicted predecessor re-runs its recorded step; its lines stream like an Apply's.
|
|
1475
|
+
let job=result.job as AnyJobView;const undone=result.undone;let seen=0;
|
|
1476
|
+
while(job.status==='running'){await new Promise(resolve=>setTimeout(resolve,700));job=await api<AnyJobView>(`/api/rebuild/${job.id}`);for(;seen<job.lines.length;seen++)appliedStatus(job.lines[seen]!.trim()||'…');}
|
|
1477
|
+
if(job.status!=='built'){const text=refusedText(job);appliedStatus(text,true);message(text,true);return {status:'refused',error:text,producerLine:job.producerLine};}
|
|
1478
|
+
result={status:'built',undone,job,history:await api<AppliedHistoryView>('/api/rebuild/history')};
|
|
1479
|
+
}
|
|
1480
|
+
if(result.status==='refused'){appliedStatus(result.error,true);message(result.error,true);return result;}
|
|
1481
|
+
let ops:import('../src/config-patch.ts').PatchOp[]|null=null;
|
|
1482
|
+
if(result.status==='inspected'){await swapRig(inspectedModel.url,inspectedModel.prefix,inspectedModel.sha256,world);appliedOnStage=null;stageCheck=null;}
|
|
1483
|
+
else{
|
|
1484
|
+
const job=result.job;if(!job.build)throw new Error('the restored job carries no build');
|
|
1485
|
+
await swapRig(new URL(access.route(job.build.modelUrl),location.href).href,access.route(`/rebuild/${job.id}/`),job.build.modelHash,world);
|
|
1486
|
+
appliedOnStage=recordFor(job);stageCheck=(job as PatchJobView).kind==='patch'?{lines:[...job.lines],check:(job as PatchJobView).check}:null;
|
|
1487
|
+
if((job as PatchJobView).kind==='patch')ops=(job as PatchJobView).request.patch.ops;
|
|
1488
|
+
}
|
|
1489
|
+
// The panels mark the ops the stage's build applied; the pending edits stay as they are.
|
|
1490
|
+
physicsPanel?.applied(ops);idlePanel?.applied(ops);rigFieldsPanel?.applied(ops);bonesPanel?.applied(ops);
|
|
1491
|
+
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=appliedOnStage?JSON.stringify(appliedOnStage):'';draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1492
|
+
appliedHistory=result.history;
|
|
1493
|
+
const text=`Undid ${result.undone.slice(0,12)}… · the stage is ${result.status==='inspected'?`the inspected build (model ${inspectedModel.sha256.slice(0,12)}…)`:`record ${result.history.depth} (model ${stageModelHash?.slice(0,12)}…)`} · ${result.history.depth} applied record(s) left`;
|
|
1494
|
+
appliedStatus(text);message(text);redrawOverlay(true);
|
|
1495
|
+
return result;
|
|
1496
|
+
}catch(error){const text=`UNDO_FAILED: ${errorText(error)}`;appliedStatus(text,true);message(text,true);return {status:'refused',error:text,producerLine:null};}
|
|
1497
|
+
finally{rebuildBusy=false;rebuildState();}
|
|
1498
|
+
}
|
|
1438
1499
|
/** Apply · Rebuild: the rows go to the installed producer; on success its scratch build replaces the drawn rig in place. */
|
|
1439
1500
|
async function applyRebuild():Promise<void> {
|
|
1440
1501
|
if(rebuildBusy)return;
|
|
@@ -1497,6 +1558,8 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1497
1558
|
/** The client's edit entry point for automation and the edit cards to come: apply a patch, return to the inspected build, read the stage. */
|
|
1498
1559
|
(window as unknown as {rigReviewEditor:unknown}).rigReviewEditor={
|
|
1499
1560
|
applyPatch,reset:()=>resetRebuild(),
|
|
1561
|
+
/** #166: undo the last Apply, and the applied records as the server holds them. */
|
|
1562
|
+
undo:()=>undoApply(),history:()=>appliedHistory?JSON.parse(JSON.stringify(appliedHistory)):null,
|
|
1500
1563
|
status:async()=>rebuildStatus??await loadRebuildStatus(),
|
|
1501
1564
|
physics:{rows:()=>physicsPanel?.rows()??[],chains:()=>physicsPanel?.chains()??[],edits:()=>physicsPanel?.edits()??[],set:(name:string,field:Parameters<PhysicsPanel['set']>[1],value:number)=>physicsPanel?.set(name,field,value),ramp:(id:string,field:Parameters<PhysicsPanel['set']>[1],root:number,tip:number)=>physicsPanel?.ramp(id,field,root,tip),select:(name:string)=>physicsPanel?.select(name),apply:()=>applyPhysics(physicsPanel?.edits().map(e=>({op:e.op,path:e.path,value:e.value,name:e.name}))??[]),reset:()=>resetPhysicsBuild()},
|
|
1502
1565
|
/** Inspection only: one slot's posed world geometry on the drawn frame (what the stage drew), for comparing against `rigc render --geometry`. */
|
package/web/rebuild-client.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface RetargetStatusView {available:boolean;reason:string|null;produc
|
|
|
22
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
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
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}
|
|
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}}
|
|
25
|
+
export interface RebuildJobView {id:string;status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;hint?:string;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}}
|
|
26
26
|
export interface RebuildRefusal {status:'refused';error:string;producerLine:string|null}
|
|
27
27
|
/** #99 regions or #105 landmarks: exactly one of `regions` and `bust`. */
|
|
28
28
|
export type RebuildRequestBody={part:string;regions:RegionEntry[];configHash:string}|{part:string;bust:BustSpec;configHash:string};
|
|
@@ -118,15 +118,15 @@ export function parseApplied(text:string):AppliedRecord|AppliedPatch|AppliedDelt
|
|
|
118
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. */
|
|
119
119
|
/** #115: a build job's assemble readout (`AssembledView` in src/rebuild.ts). */
|
|
120
120
|
export type AssembledJobView=import('../src/rebuild.ts').AssembledView;
|
|
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}}
|
|
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;hint?:string;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
122
|
/** #112: `environment` is the exported `rigc-environment/1` schedule Apply's check reads, when "Use for readings" is on. */
|
|
123
123
|
export interface PatchRequestBody {patch:ConfigPatch;environment?:import('../src/environment-schedule.ts').EnvironmentSchedule}
|
|
124
124
|
/** Edit Mesh's delta Apply (#135): `remesh --delta`, the per-file diff and the part's counts; remesh's own gate holds check. */
|
|
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}}
|
|
125
|
+
export interface DeltaJobView {id:string;kind:'delta';op:'remesh';status:'running'|'built'|'refused'|'failed';reused:boolean;lines:string[];producerLine:string|null;hint?:string;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}}
|
|
126
126
|
/** `{part, delta, configHash}`: the server owns every path and flag; the delta is the editor's export as it stands. */
|
|
127
127
|
export interface DeltaRequestBody {part:string;delta:import('../src/mesh-edit.ts').MeshEditDocument;configHash:string}
|
|
128
128
|
/** A retarget Apply's job (#114): clean, retarget, compile; every FAIL line of the step that refused, verbatim; the listed lines. */
|
|
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[];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}}
|
|
130
130
|
/** `{retarget: {sequence, map, flags, configHash}}`: a declared reference id, the map by content and typed flags — no path, no flag string. */
|
|
131
131
|
export interface RetargetRequestBody {retarget:{sequence:string;map:Record<string,unknown>;flags:RetargetFlags;configHash:string}}
|
|
132
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. */
|
|
@@ -172,7 +172,7 @@ export const deltaAppliedRecord=(job:DeltaJobView):AppliedDelta=>({schema:APPLIE
|
|
|
172
172
|
/** A delta job's status line: built (wall clocks, counts, the per-file diff) or the producer's line verbatim. */
|
|
173
173
|
export function deltaOutcomeLine(result:DeltaJobView|RebuildRefusal):{text:string;error:boolean} {
|
|
174
174
|
if(!('id' in result))return {text:result.producerLine??result.error,error:true};
|
|
175
|
-
if(result.status!=='built')return {text:result
|
|
175
|
+
if(result.status!=='built')return {text:refusedText(result),error:true};
|
|
176
176
|
const ops=Array.isArray(result.request.delta.ops)?result.request.delta.ops.length:0;
|
|
177
177
|
const wall=result.reused?'reused the scratch build of this exact delta':`remesh --delta ${seconds(result.wallMs.op)} + compile ${seconds(result.wallMs.compile)}`;
|
|
178
178
|
const counts=result.remesh?` · ${result.remesh.part} v ${result.remesh.before.vertices} t ${result.remesh.before.triangles} → v ${result.remesh.after.vertices} t ${result.remesh.after.triangles}`:'';
|
|
@@ -186,11 +186,13 @@ export function stageModel(result:AnyJobView|RebuildRefusal,current:{url:string;
|
|
|
186
186
|
if('id' in result&&result.status==='built'&&result.build)return {url:result.build.modelUrl,hash:result.build.modelHash,id:result.id};
|
|
187
187
|
return {...current,id:null};
|
|
188
188
|
}
|
|
189
|
+
/** #148: a refused job's line — the producer's line as printed, then review's reading of it when the server added one. */
|
|
190
|
+
export const refusedText=(job:{producerLine:string|null;exitCode:number|null;hint?:string}):string=>`${job.producerLine??`REBUILD_FAILED: exit ${job.exitCode}`}${job.hint?` · review: ${job.hint}`:''}`;
|
|
189
191
|
const seconds=(ms:number|null)=>ms===null?'—':`${(ms/1000).toFixed(1)} s`;
|
|
190
192
|
/** A patch job's status line: built (wall clocks, check's verdict, the per-file diff — #125) or the producer's line verbatim. */
|
|
191
193
|
export function patchOutcomeLine(result:PatchJobView|RebuildRefusal):{text:string;error:boolean} {
|
|
192
194
|
if(!('id' in result))return {text:result.producerLine??result.error,error:true};
|
|
193
|
-
if(result.status!=='built')return {text:result
|
|
195
|
+
if(result.status!=='built')return {text:refusedText(result),error:true};
|
|
194
196
|
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';
|
|
195
197
|
// #115: a build's own wall clock covers assemble, rig and check; the recorded figure is the same number.
|
|
196
198
|
const building=result.op==='build'||(result.op==='cloth'&&result.cloth?.route==='build');
|
|
@@ -201,7 +203,7 @@ export function patchOutcomeLine(result:PatchJobView|RebuildRefusal):{text:strin
|
|
|
201
203
|
/** The status line after a job: built (with the wall clock and whether the scratch build was reused) or the producer's line verbatim. */
|
|
202
204
|
export function outcomeLine(result:RebuildJobView|RebuildRefusal):{text:string;error:boolean} {
|
|
203
205
|
if(!('id' in result))return {text:result.producerLine??result.error,error:true};
|
|
204
|
-
if(result.status!=='built')return {text:result
|
|
206
|
+
if(result.status!=='built')return {text:refusedText(result),error:true};
|
|
205
207
|
const wall=[result.wallMs.remesh,result.wallMs.compile].every(v=>v!==null)?` · remesh ${(result.wallMs.remesh!/1000).toFixed(1)} s + compile ${(result.wallMs.compile!/1000).toFixed(1)} s`:'';
|
|
206
208
|
return {text:`Applied ${result.request.part} with ${result.producer.package} ${result.producer.version}${result.reused?' · reused the scratch build of this exact designation':wall} · Reset returns to the inspected build`,error:false};
|
|
207
209
|
}
|
|
@@ -219,3 +221,31 @@ export function handoffText(status:Pick<RebuildStatusView,'handoff'|'baseline'|'
|
|
|
219
221
|
const base=status.baseline?` · ${status.baseline.wallMs!==null&&status.baseline.status!=='running'?`${status.baseline.line} (${(status.baseline.wallMs/1000).toFixed(1)} s)`:status.baseline.line}`:' · baseline: waits for the producer';
|
|
220
222
|
return {text:`Handoff ${how}: ${h.config.path} sha256 ${h.config.sha256.slice(0,12)}…${parts}${base}`,error:!!status.baseline&&['not-reproduced','refused','failed'].includes(status.baseline.status)};
|
|
221
223
|
}
|
|
224
|
+
|
|
225
|
+
/** #166: `GET /api/rebuild/history` — the session's applied records in order; the last is on the stage; `kept`: its build is on disk. */
|
|
226
|
+
export interface AppliedHistoryView {schema:'rig-review-applied-history/1';depth:number;records:{id:string;kind:'regions'|'patch'|'delta'|'retarget';op:string;what:string;modelHash:string;kept:boolean}[]}
|
|
227
|
+
/** #166: `POST /api/rebuild/undo {from}` — the predecessor's build (kept, or re-run and still running), the inspected build, or a refusal. */
|
|
228
|
+
export type UndoView=
|
|
229
|
+
|{status:'inspected';undone:string;history:AppliedHistoryView}
|
|
230
|
+
|{status:'built';undone:string;job:AnyJobView;history:AppliedHistoryView}
|
|
231
|
+
|{status:'running';undone:string;job:AnyJobView}
|
|
232
|
+
|RebuildRefusal;
|
|
233
|
+
/** #166: the applied record a job carries into `context.applied` (the same shapes each Apply records). */
|
|
234
|
+
export function recordFor(job:AnyJobView):AppliedRecord|AppliedPatch|AppliedDelta|AppliedRetarget {
|
|
235
|
+
const kind=(job as {kind?:string}).kind;
|
|
236
|
+
return kind==='patch'?patchAppliedRecord(job as PatchJobView):kind==='delta'?deltaAppliedRecord(job as DeltaJobView):kind==='retarget'?retargetAppliedRecord(job as RetargetJobView):appliedRecord(job as RebuildJobView);
|
|
237
|
+
}
|
|
238
|
+
/** #166: one record's line in the list: its place, what it applied, the op and the model; the stage's last is marked. */
|
|
239
|
+
export function historyRow(history:AppliedHistoryView,index:number):string {
|
|
240
|
+
const r=history.records[index]!,last=index===history.records.length-1;
|
|
241
|
+
return `${index+1}. ${r.kind} (${r.op}) · ${r.what} · model ${r.modelHash.slice(0,12)}…${last?' · on the stage':r.kept?'':' · build evicted, an undo to it re-runs its step'}`;
|
|
242
|
+
}
|
|
243
|
+
/** #166: the Undo control's label and whether it can run: the depth, and what the stage returns to. */
|
|
244
|
+
export function undoControl(history:AppliedHistoryView|null,stageId:string|null,busy:boolean):{label:string;enabled:boolean;title:string} {
|
|
245
|
+
const depth=history?.depth??0,top=history?.records.at(-1)??null;
|
|
246
|
+
const label=`Undo last Apply (${depth} deep)`;
|
|
247
|
+
if(!top)return {label,enabled:false,title:'No Apply is on the stage'};
|
|
248
|
+
if(stageId!==top.id)return {label,enabled:false,title:'The stage does not show the last Apply (Reset shows the inspected build); undo acts on the last Apply only'};
|
|
249
|
+
const back=depth>1?`record ${depth-1} (model ${history!.records[depth-2]!.modelHash.slice(0,12)}…)`:'the inspected build';
|
|
250
|
+
return {label,enabled:!busy,title:`Return the stage to ${back}; the package is unchanged`};
|
|
251
|
+
}
|
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}
|
|
@@ -221,15 +222,29 @@ export class RigOverlays {
|
|
|
221
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);
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
|
-
if(this.playerAvailable)
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
});
|
|
233
248
|
}
|
|
234
249
|
ctx.globalAlpha=1;
|
|
235
250
|
if(this.playerAvailable&&meshEdit)this.drawMeshEdit(ctx,meshEdit,projected,width,height);
|
package/web/styles.css
CHANGED
|
@@ -262,3 +262,4 @@ footer.status-bar{flex-wrap:nowrap}
|
|
|
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
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
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)}
|
|
265
|
+
.applied-records{padding:12px 15px;border-bottom:1px solid var(--border);background:var(--rv-panel);font-size:11px}.applied-records .row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}.applied-records .eyebrow{margin:0}.applied-records button{padding:4px 9px;font-size:11px}.applied-list{margin:0 0 6px;padding-left:0;list-style:none;display:flex;flex-direction:column;gap:3px}.applied-list li{font:11px var(--font-mono);color:var(--fg-muted);overflow-wrap:anywhere}.applied-list li.on-stage{color:var(--fg-strong)}#applied-status{margin:0}#applied-status.error{color:var(--fail)}
|