@rig-c/review 0.8.1 → 0.10.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 +15 -8
- package/docs/DESIGN.md +1 -1
- package/docs/REVIEW_PACKAGE.md +22 -8
- package/package.json +1 -1
- package/src/applied-blocks.ts +48 -0
- package/src/applied-context.ts +4 -3
- package/src/baseline-reach.ts +177 -0
- package/src/baseline.ts +14 -9
- package/src/cli.ts +2 -2
- package/src/density-strip.ts +201 -0
- package/src/front.ts +7 -0
- package/src/handoff.ts +59 -7
- package/src/manifest.ts +13 -3
- package/src/open.ts +12 -3
- package/src/rebuild.ts +354 -42
- package/src/retune-check.ts +48 -28
- package/src/server.ts +30 -3
- package/web/cloth-panel.ts +10 -2
- package/web/cuts-panel.ts +15 -4
- package/web/density-strip-panel.ts +89 -0
- package/web/index.html +1 -1
- package/web/main.ts +171 -13
- package/web/rebuild-client.ts +38 -8
- package/web/styles.css +2 -0
package/web/main.ts
CHANGED
|
@@ -6,7 +6,9 @@ import {rendererStatus} from './renderer-status.ts';
|
|
|
6
6
|
import {bindAppearance,bindPan,bindStageLayout,syncStageLayout,bindInspectorTabs,idleAvailable,initialIdleAnimation,automaticIdlePreview,bindLayerShortcuts,describeLayerToggles,type ViewPoint} from './view-controls.ts';
|
|
7
7
|
import {RigOverlays,type OverlayCircle,type OverlayMarker,type OverlayRegion,type OverlaySnapshot} from './rig-overlays.ts';
|
|
8
8
|
import {bindMeshPanel,type MeshPanel} from './mesh-panel.ts';
|
|
9
|
-
import {
|
|
9
|
+
import {bindDensityStrip,type DensityStripPanel,type DensityStripViewLike} from './density-strip-panel.ts';
|
|
10
|
+
import {densityField,densityPath} from '../src/density-strip.ts';
|
|
11
|
+
import {assertMeshPatch,countsText,meshApplyOutcome,meshPatchOps,setEntryValue,triangleAgreement,type MeshEntry} from '../src/mesh-entry.ts';
|
|
10
12
|
import {worstLine} from '../src/readings.ts';
|
|
11
13
|
import {chestOverlayCircles,chestOverlayPolygons,chestRegionRows,chestRowId,evaluateChestForm} from './chest-regions-tool.ts';
|
|
12
14
|
import {bindChestEditor,canvasToCrop,bustLabel,bustSides,LANDMARKS,landmarkInputId,rowCircle,type ChestEditor,type LandmarkKey} from './chest-editor.ts';
|
|
@@ -19,7 +21,7 @@ import {bindClothPanel,type ClothPanel} from './cloth-panel.ts';
|
|
|
19
21
|
import {retargetAppliedRecord,retargetOutcomeLine,type AppliedRetarget,type RetargetJobView,type RetargetRequestBody} from './rebuild-client.ts';
|
|
20
22
|
import {bindRetargetPanel,type PoseMap,type RetargetPanel} from './retarget-panel.ts';
|
|
21
23
|
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';
|
|
24
|
+
import {historyRow,isPatchJob,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
25
|
import {meshEditBinding,meshEditText,type MeshEditDocument} from '../src/mesh-edit.ts';
|
|
24
26
|
import {chestRegionsBinding,cropWorldMapping,type CropWorldMapping} from '../src/chest-regions.ts';
|
|
25
27
|
import {bindWorkspaceUI,selectedInspector,type WorkspaceUI} from './workspace-ui.ts';
|
|
@@ -48,6 +50,7 @@ import {barLine,beforeLine} from '../src/physics-tuning.ts';
|
|
|
48
50
|
import {bindRequirementsPanel,type RequirementsCatalogView,type RequirementsJobView,type RequirementsPanel,type RequirementsRefusal} from './requirements-panel.ts';
|
|
49
51
|
import {patchRoute} from '../src/retune-route.ts';
|
|
50
52
|
import {appliedCheckRefusal,type CheckSummary} from '../src/applied-context.ts';
|
|
53
|
+
import {appliedBlockOps,recordBlock} from '../src/applied-blocks.ts';
|
|
51
54
|
import {bindPhysicsPanel,type PhysicsPanel} from './physics-panel.ts';
|
|
52
55
|
import {bindIdlePanel,type BoneSetup,type IdlePanel} from './idle-panel.ts';
|
|
53
56
|
import {bindRigFieldsPanel,type RigFieldsPanel} from './rig-fields-panel.ts';
|
|
@@ -149,7 +152,7 @@ let idlePanel:IdlePanel|null=null;
|
|
|
149
152
|
let rigFieldsPanel:RigFieldsPanel|null=null,idleAdjust:BoneAdjust|null=null,dialAdjust:BoneAdjust|null=null;
|
|
150
153
|
function setAdjust():void {if(!player)return;const a=idleAdjust,b=dialAdjust;player.adjust=a&&b?bones=>{a(bones);b(bones);}:a??b;if(!playing)drawRig(()=>player!.draw());}
|
|
151
154
|
/** #106: the Setup Mesh panel, and the overlay's last snapshot (bar 1 reads its per-slot triangle count). */
|
|
152
|
-
let meshPanel:MeshPanel|null=null,lastOverlay:OverlaySnapshot|null=null;
|
|
155
|
+
let meshPanel:MeshPanel|null=null,lastOverlay:OverlaySnapshot|null=null,densityStrip:DensityStripPanel|null=null;
|
|
153
156
|
let physicsPanel:PhysicsPanel|null=null,inspectedDocument:unknown=null,inspectedDocumentReason:string|null=null,stageCheck:{lines:string[];check:CheckSummary|null}|null=null;
|
|
154
157
|
/** Animate · Readings (#108): the block, the last finished job (its ticks and worst lines), and the run in flight. */
|
|
155
158
|
let requirementsPanel:RequirementsPanel|null=null,retargetPanel:RetargetPanel|null=null;
|
|
@@ -895,9 +898,11 @@ function bind(): void {
|
|
|
895
898
|
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;}});
|
|
896
899
|
viewDisposers.push(()=>physicsPanel?.dispose());
|
|
897
900
|
meshPanel=bindMeshPanel({pane:el('rig-pane'),project:()=>player?rigProject():null,mapping:()=>chestMapping,bones:()=>data.feedbackCatalog.bones??[],
|
|
898
|
-
apply:async(part,entry)=>{await applyMesh(part,entry);},reset:resetMeshBuild,folds:async()=>{await meshFolds();},changed:()=>redrawOverlay(false),saved:()=>persistence?.save(),
|
|
901
|
+
apply:async(part,entry)=>{await applyMesh(part,entry);},reset:resetMeshBuild,folds:async()=>{await meshFolds();},changed:()=>{redrawOverlay(false);densityStrip?.changed();},saved:()=>persistence?.save(),
|
|
899
902
|
token:name=>getComputedStyle(document.documentElement).getPropertyValue(name).trim()});
|
|
900
903
|
viewDisposers.push(()=>meshPanel?.dispose());
|
|
904
|
+
// #173: the density strip beside the Mesh panel's density field.
|
|
905
|
+
densityStrip=bindDensityStrip({part:()=>meshPanel?.part()??null,entry:()=>meshPanel?.entry()??null,build:async values=>{await buildDensityStrip(values);},load:async i=>{await loadStripRow(i);},back:async()=>{await stripBack();},close:async()=>{await closeDensityStrip();}});
|
|
901
906
|
idlePanel=bindIdlePanel({apply:async(ops,keys)=>{await applyIdle(ops,keys);},reset:resetIdleBuild,pane:el('rig-pane'),saved:()=>persistence?.save(),clock:()=>player?.time??0,
|
|
902
907
|
preview:fn=>{idleAdjust=fn as BoneAdjust|null;setAdjust();},
|
|
903
908
|
bonePoints:bones=>{if(!player)return null;const project=rigProject(),out=new Map<string,{x:number;y:number}>();for(const name of bones){try{const b=player.bone(name);out.set(name,project(b.x,b.y));}catch{/* a bone this model lacks has no handle */}}return out;}});
|
|
@@ -955,7 +960,7 @@ async function start(): Promise<void> {
|
|
|
955
960
|
else if (readOnly) message(readOnly, true);
|
|
956
961
|
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.');
|
|
957
962
|
requestAnimationFrame(tick);
|
|
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();
|
|
963
|
+
const restored=await setupPersistence();await loadRebuildStatus();void loadRequirements();renderChest();restoreEnvironment();physicsPanel?.restore();bonesPanel?.restore();cutsPanel?.restore();clothPanel?.restore();idlePanel?.restore();rigFieldsPanel?.restore();meshPanel?.restore();void refreshDensityStrip();renderIdle();if(!restored&&canRepeatIdle()&&automaticIdlePreview(assets?.animations.map(item=>item.name)??[],animation,first,!!requestedId))void playPreview();
|
|
959
964
|
// Only an instance that serves agent control is asked for its config: a plain open has no control endpoint (#164).
|
|
960
965
|
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)});
|
|
961
966
|
} catch (error) { message(`Could not open review package: ${errorText(error)}`, true); text('dirty', 'Load failed'); }
|
|
@@ -1271,6 +1276,7 @@ function bindEnvironment():void {
|
|
|
1271
1276
|
button('wind-gust').onclick=()=>engage(()=>wind.triggerGust());
|
|
1272
1277
|
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.');};
|
|
1273
1278
|
button('physics-reset').onclick=resetPhysics;
|
|
1279
|
+
button('applied-undo').onclick=()=>{void undoApply();};
|
|
1274
1280
|
input('environment-use').onchange=()=>{
|
|
1275
1281
|
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
1282
|
const world=worldFor(animation);
|
|
@@ -1441,7 +1447,66 @@ function renderHandoff(status:RebuildStatusView):void {
|
|
|
1441
1447
|
if(handoffTimer)clearTimeout(handoffTimer);handoffTimer=null;
|
|
1442
1448
|
if(status.baseline?.status==='running')handoffTimer=setTimeout(()=>{void api<RebuildStatusView>('/api/rebuild').then(next=>{rebuildStatus=next;renderHandoff(next);}).catch(()=>{});},2000);
|
|
1443
1449
|
}
|
|
1444
|
-
function rebuildState():void {chestEditor?.rebuildState({busy:rebuildBusy,applied:!!appliedOnStage});}
|
|
1450
|
+
function rebuildState():void {chestEditor?.rebuildState({busy:rebuildBusy,applied:!!appliedOnStage});renderApplied();if(!rebuildBusy)void refreshApplied();}
|
|
1451
|
+
/** #166: the session's applied records (the server's stack), the last one on the stage when the stage shows it. */
|
|
1452
|
+
let appliedHistory:AppliedHistoryView|null=null;
|
|
1453
|
+
async function refreshApplied():Promise<void> {
|
|
1454
|
+
if(!inspectedModel)return;
|
|
1455
|
+
try{appliedHistory=await api<AppliedHistoryView>('/api/rebuild/history');}catch{return;}
|
|
1456
|
+
renderApplied();
|
|
1457
|
+
}
|
|
1458
|
+
/** #172: the Cuts and Cloth blocks mark as applied only the ops the stage's applied record holds (none on the inspected build). */
|
|
1459
|
+
function renderAppliedBlocks():void {const on=appliedBlockOps(appliedOnStage);cutsPanel?.applied(on.cuts);clothPanel?.applied(on.cloth);}
|
|
1460
|
+
function renderApplied():void {
|
|
1461
|
+
renderAppliedBlocks();
|
|
1462
|
+
const section=document.getElementById('applied-records');if(!section)return;
|
|
1463
|
+
const h=appliedHistory;section.hidden=!h||h.depth===0;
|
|
1464
|
+
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;}));
|
|
1465
|
+
const control=undoControl(h,appliedOnStage?.buildId??null,rebuildBusy||!!readOnly),undo=button('applied-undo');
|
|
1466
|
+
undo.textContent=control.label;undo.disabled=!control.enabled;undo.title=control.title;
|
|
1467
|
+
}
|
|
1468
|
+
function appliedStatus(text:string,error=false):void {const node=el('applied-status');node.textContent=text;node.classList.toggle('error',error);}
|
|
1469
|
+
/**
|
|
1470
|
+
* #166: Undo last Apply — the last record leaves the server's stack and the stage returns to its predecessor's build
|
|
1471
|
+
* (kept on disk, or re-run from its record when evicted) or to the inspected build, under the same world. The feedback
|
|
1472
|
+
* context then carries the post-undo record (or null), never the reverted one. A refusal leaves the stage as it was.
|
|
1473
|
+
*/
|
|
1474
|
+
async function undoApply():Promise<UndoView|null> {
|
|
1475
|
+
const from=appliedOnStage?.buildId;
|
|
1476
|
+
if(rebuildBusy||readOnly||!from||!inspectedModel)return null;
|
|
1477
|
+
const world=stageWorld();
|
|
1478
|
+
rebuildBusy=true;rebuildState();appliedStatus('Undoing the last Apply…');
|
|
1479
|
+
try{
|
|
1480
|
+
let result=await api<UndoView>('/api/rebuild/undo',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({from})});
|
|
1481
|
+
if(result.status==='running'){
|
|
1482
|
+
// An evicted predecessor re-runs its recorded step; its lines stream like an Apply's.
|
|
1483
|
+
let job=result.job as AnyJobView;const undone=result.undone;let seen=0;
|
|
1484
|
+
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()||'…');}
|
|
1485
|
+
if(job.status!=='built'){const text=refusedText(job);appliedStatus(text,true);message(text,true);return {status:'refused',error:text,producerLine:job.producerLine};}
|
|
1486
|
+
result={status:'built',undone,job,history:await api<AppliedHistoryView>('/api/rebuild/history')};
|
|
1487
|
+
}
|
|
1488
|
+
if(result.status==='refused'){appliedStatus(result.error,true);message(result.error,true);return result;}
|
|
1489
|
+
let ops:import('../src/config-patch.ts').PatchOp[]|null=null;
|
|
1490
|
+
if(result.status==='inspected'){await swapRig(inspectedModel.url,inspectedModel.prefix,inspectedModel.sha256,world);appliedOnStage=null;stageCheck=null;}
|
|
1491
|
+
else{
|
|
1492
|
+
const job=result.job;if(!job.build)throw new Error('the restored job carries no build');
|
|
1493
|
+
await swapRig(new URL(access.route(job.build.modelUrl),location.href).href,access.route(`/rebuild/${job.id}/`),job.build.modelHash,world);
|
|
1494
|
+
appliedOnStage=recordFor(job);stageCheck=(job as PatchJobView).kind==='patch'?{lines:[...job.lines],check:(job as PatchJobView).check}:null;
|
|
1495
|
+
if((job as PatchJobView).kind==='patch')ops=(job as PatchJobView).request.patch.ops;
|
|
1496
|
+
}
|
|
1497
|
+
// The panels mark the ops the stage's build applied; the pending edits stay as they are.
|
|
1498
|
+
physicsPanel?.applied(ops);idlePanel?.applied(ops);rigFieldsPanel?.applied(ops);bonesPanel?.applied(ops);
|
|
1499
|
+
// #172: the Cuts and Cloth readouts show the restored build's job when it is theirs, else nothing (never the reverted one).
|
|
1500
|
+
const restored=result.status==='built'&&isPatchJob(result.job)?result.job:null,block=recordBlock(appliedOnStage);
|
|
1501
|
+
cutsPanel?.shown(block==='cuts'?restored:null);clothPanel?.shown(block==='cloth'?restored:null);
|
|
1502
|
+
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=appliedOnStage?JSON.stringify(appliedOnStage):'';draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1503
|
+
appliedHistory=result.history;
|
|
1504
|
+
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`;
|
|
1505
|
+
appliedStatus(text);message(text);redrawOverlay(true);
|
|
1506
|
+
return result;
|
|
1507
|
+
}catch(error){const text=`UNDO_FAILED: ${errorText(error)}`;appliedStatus(text,true);message(text,true);return {status:'refused',error:text,producerLine:null};}
|
|
1508
|
+
finally{rebuildBusy=false;rebuildState();}
|
|
1509
|
+
}
|
|
1445
1510
|
/** Apply · Rebuild: the rows go to the installed producer; on success its scratch build replaces the drawn rig in place. */
|
|
1446
1511
|
async function applyRebuild():Promise<void> {
|
|
1447
1512
|
if(rebuildBusy)return;
|
|
@@ -1504,6 +1569,8 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1504
1569
|
/** 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. */
|
|
1505
1570
|
(window as unknown as {rigReviewEditor:unknown}).rigReviewEditor={
|
|
1506
1571
|
applyPatch,reset:()=>resetRebuild(),
|
|
1572
|
+
/** #166: undo the last Apply, and the applied records as the server holds them. */
|
|
1573
|
+
undo:()=>undoApply(),history:()=>appliedHistory?JSON.parse(JSON.stringify(appliedHistory)):null,
|
|
1507
1574
|
status:async()=>rebuildStatus??await loadRebuildStatus(),
|
|
1508
1575
|
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()},
|
|
1509
1576
|
/** Inspection only: one slot's posed world geometry on the drawn frame (what the stage drew), for comparing against `rigc render --geometry`. */
|
|
@@ -1547,14 +1614,17 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1547
1614
|
mesh:{part:()=>meshPanel?.part()??null,select:(part:string)=>meshPanel?.select(part),entry:()=>meshPanel?.entry()??null,configEntry:()=>meshPanel?.configEntry()??null,set:(path:string[],value:import('../src/config-patch.ts').JsonValue|undefined)=>meshPanel?.set(path,value),setEntry:(entry:MeshEntry)=>meshPanel?.setEntry(entry),
|
|
1548
1615
|
regions:()=>meshPanel?.regions.list()??[],addRegion:(region:import('../src/mesh-entry.ts').MeshRegion)=>meshPanel?.regions.add(region),moveRegion:(region:number,vertex:number,to:[number,number])=>meshPanel?.regions.move(region,vertex,to),drawRegions:(on:boolean)=>meshPanel?.regions.draw(on),placing:()=>meshPanel?.regions.placing()??null,
|
|
1549
1616
|
precheck:()=>meshPanel?.precheck()??[],apply:()=>{const part=meshPanel?.part(),entry=meshPanel?.entry();return part&&entry?applyMesh(part,entry):Promise.resolve(null);},reset:()=>resetMeshBuild(),folds:()=>meshFolds(),
|
|
1550
|
-
overlayTriangles:(slot:string)=>lastOverlay?.meshTriangles[slot]??null,status:()=>el('mesh-status').textContent,counts:()=>[...document.querySelectorAll('#mesh-counts code')].map(c=>c.textContent),readout:()=>[...document.querySelectorAll('#mesh-readout code')].map(c=>c.textContent),pending:()=>input('mesh-pending').value,lastPatch:()=>lastMeshPatch?JSON.parse(JSON.stringify(lastMeshPatch)):null
|
|
1617
|
+
overlayTriangles:(slot:string)=>lastOverlay?.meshTriangles[slot]??null,status:()=>el('mesh-status').textContent,counts:()=>[...document.querySelectorAll('#mesh-counts code')].map(c=>c.textContent),readout:()=>[...document.querySelectorAll('#mesh-readout code')].map(c=>c.textContent),pending:()=>input('mesh-pending').value,lastPatch:()=>lastMeshPatch?JSON.parse(JSON.stringify(lastMeshPatch)):null,
|
|
1618
|
+
/** #173: the density strip — build N values, the view, load a row as pending, back, close. */
|
|
1619
|
+
strip:{build:(values:number[])=>buildDensityStrip(values),view:()=>stripView?JSON.parse(JSON.stringify(stripView)):null,load:(i:number)=>loadStripRow(i),back:()=>stripBack(),close:()=>closeDensityStrip(),pending:()=>stripPending?{index:stripPending.index,candidate:stripPending.candidate}:null,
|
|
1620
|
+
applied:()=>appliedOnStage?JSON.parse(JSON.stringify(appliedOnStage)):null,stageHash:()=>stageModelHash}},
|
|
1551
1621
|
/** #115: the Cuts block — its handles as client pixels (for a real-pointer drag), its ops, Apply and Reset. */
|
|
1552
1622
|
cuts:{source:()=>cutsPanel?.source()??null,ops:()=>cutsPanel?.ops()??[],handles:()=>cutsPanel?.handles()??[],active:()=>cutsPanel?.active()??false,setActive:(on:boolean)=>cutsPanel?.setActive(on),
|
|
1553
1623
|
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),
|
|
1554
|
-
apply:()=>cutsPanel?.apply()??Promise.resolve(null),reset:()=>resetCutsBuild()},
|
|
1624
|
+
apply:()=>cutsPanel?.apply()??Promise.resolve(null),reset:()=>resetCutsBuild(),applied:()=>cutsPanel?.appliedItems()??[]},
|
|
1555
1625
|
/** #111: the Cloth block — the selected tag, its form values, the pending ops, the link preview, Apply and Reset. */
|
|
1556
1626
|
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)},
|
|
1627
|
+
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(),applied:()=>clothPanel?.appliedItems()??[],status:()=>el('cloth-status').textContent,readout:()=>[...document.querySelectorAll('#cloth-readout code')].map(c=>c.textContent)},
|
|
1558
1628
|
bones:{anchors:()=>bonesPanel?.anchors()??[],edits:()=>bonesPanel?.edits()??[],set:(path:string,value:[number,number])=>bonesPanel?.set(path,value),select:(name:string|null)=>bonesPanel?.select(name),
|
|
1559
1629
|
active:()=>bonesPanel?.active()??false,setActive:(on:boolean)=>bonesPanel?.setActive(on),handles:()=>bonesPanel?.handles()??[],check:()=>bonesPanel?.check()??Promise.resolve(null),
|
|
1560
1630
|
apply:()=>applyBones((bonesPanel?.edits()??[]).map(e=>({op:'replace' as const,path:e.path,value:[...e.value],name:e.name}))),reset:()=>resetBonesBuild(),
|
|
@@ -1821,9 +1891,9 @@ function sampleLocals(bone:string,dt:number,n:number):{t:number;rotation:number;
|
|
|
1821
1891
|
return [...rows.values()].sort((a,b)=>a.t-b.t);
|
|
1822
1892
|
}
|
|
1823
1893
|
async function resetRebuild(world:StageWorld|null=null):Promise<void> {
|
|
1824
|
-
if(rebuildBusy
|
|
1894
|
+
if(rebuildBusy||(!appliedOnStage&&!stripPending)||!inspectedModel)return;
|
|
1825
1895
|
rebuildBusy=true;rebuildState();
|
|
1826
|
-
try{await swapRig(inspectedModel.url,inspectedModel.prefix,inspectedModel.sha256,world);appliedOnStage=null;stageCheck=null;chestEditor?.notify('Reset: the inspected build is on the stage. The draft keeps the last applied state.',false);message('Reset to the inspected build.');}
|
|
1896
|
+
try{await swapRig(inspectedModel.url,inspectedModel.prefix,inspectedModel.sha256,world);appliedOnStage=null;stageCheck=null;if(stripPending){stripPending=null;renderStrip();}chestEditor?.notify('Reset: the inspected build is on the stage. The draft keeps the last applied state.',false);message('Reset to the inspected build.');}
|
|
1827
1897
|
catch(error){chestEditor?.notify(`REBUILD_RESET_FAILED: ${errorText(error)}`,true);}
|
|
1828
1898
|
finally{rebuildBusy=false;rebuildState();}
|
|
1829
1899
|
}
|
|
@@ -1831,10 +1901,11 @@ async function resetRebuild(world:StageWorld|null=null):Promise<void> {
|
|
|
1831
1901
|
* Replace the drawn rig in place: the new model loads first (a failure leaves the stage as it was), then the player is
|
|
1832
1902
|
* swapped and the same animation, time, set, camera and environment are restored; playback resumes when it was running.
|
|
1833
1903
|
*/
|
|
1904
|
+
let stageSource:{url:string;prefix:string;hash:string}|null=null;
|
|
1834
1905
|
async function swapRig(modelUrl:string,prefix:string,sha256:string,world:StageWorld|null=null):Promise<void> {
|
|
1835
1906
|
const keepAnimation=animation,keepTime=player?.time??time,wasPlaying=playing,keepSet=select('part-set').disabled?'':select('part-set').value;
|
|
1836
1907
|
const next=await loadModel({modelUrl,setsUrl:null,resolvePage:(name,url)=>{const target=new URL(name,url);if(target.origin!==location.origin||!target.pathname.startsWith(prefix))throw new Error(`Model page escapes its build: ${name}`);return target.href;}});
|
|
1837
|
-
pause();player?.dispose();assets?.dispose();assets=next;player=new RigPlayer(el('rig-root'),assets);stageModelHash=sha256;
|
|
1908
|
+
pause();player?.dispose();assets?.dispose();assets=next;player=new RigPlayer(el('rig-root'),assets);stageModelHash=sha256;stageSource={url:modelUrl,prefix,hash:sha256};
|
|
1838
1909
|
// A scratch build may hold another set of animations (a retarget adds one, #114): the picker lists what this build has.
|
|
1839
1910
|
{const animations=select('animation-select');animations.replaceChildren(...assets.animations.map(item=>option(item.name,data.manifest.animations.find(a=>a.id===item.name)?.label??item.name)));animations.disabled=assets.animations.length===0;}
|
|
1840
1911
|
player.on('event',event=>{text('event-log',`Runtime event: ${event.name} · ${event.time.toFixed(2)} s`);});
|
|
@@ -1873,6 +1944,8 @@ async function applyMesh(part:string,entry:MeshEntry):Promise<PatchJobView|Rebui
|
|
|
1873
1944
|
const result=await applyPatch(patch,world);
|
|
1874
1945
|
const outcome=meshApplyOutcome(result as Parameters<typeof meshApplyOutcome>[0]);meshPanel?.status(outcome.text,outcome.error);
|
|
1875
1946
|
if('id' in result&&result.status==='built'){
|
|
1947
|
+
// #173: a strip row on the stage as pending is now (or was replaced by) an applied record; Back has nothing to return to.
|
|
1948
|
+
if(stripPending){stripPending=null;void refreshDensityStrip();}
|
|
1876
1949
|
// The new triangulation is drawn: the mesh edges go on and the overlay captures the reloaded pose.
|
|
1877
1950
|
if(!layers.mesh){layers.mesh=true;input('layer-mesh').checked=true;}
|
|
1878
1951
|
if(player)drawRig(()=>player!.draw());redrawOverlay(true);
|
|
@@ -1891,9 +1964,94 @@ async function applyMesh(part:string,entry:MeshEntry):Promise<PatchJobView|Rebui
|
|
|
1891
1964
|
/** Mesh Reset: the inspected build back on the stage (same world); the panel already holds the config's entry again. */
|
|
1892
1965
|
async function resetMeshBuild():Promise<void> {
|
|
1893
1966
|
meshPanel?.counts([]);meshPanel?.readout([]);
|
|
1894
|
-
if(appliedOnStage){meshPanel?.busy(true);try{await resetRebuild(stageWorld());}finally{meshPanel?.busy(false);}}
|
|
1967
|
+
if(appliedOnStage||stripPending){meshPanel?.busy(true);try{await resetRebuild(stageWorld());}finally{meshPanel?.busy(false);}}
|
|
1895
1968
|
meshPanel?.status('Reset: the inspected build is on the stage; the pending entry is the config\'s.',false);
|
|
1896
1969
|
}
|
|
1970
|
+
/**
|
|
1971
|
+
* #173 · Density strip. Build: one Mesh-panel patch per value (the pending entry with its density field at that value),
|
|
1972
|
+
* sent together to `POST /api/density-strip` and polled until every candidate settles. Load: a built row's revision on
|
|
1973
|
+
* the stage as pending — the applied record, the undo stack and the feedback context's applied state are left alone
|
|
1974
|
+
* (the context reads null while a pending row is on the stage: nothing applied is on it), and Undo is unavailable until
|
|
1975
|
+
* Back returns the stage to what it showed or the Mesh panel's Apply applies the row (reusing its build). Close removes
|
|
1976
|
+
* the strip's scratch.
|
|
1977
|
+
*/
|
|
1978
|
+
let stripView:DensityStripViewLike|null=null,densityBusy=false;
|
|
1979
|
+
let stripPending:{index:number;id:string;candidate:string;back:{url:string;prefix:string;hash:string;applied:AppliedRecord|AppliedPatch|AppliedDelta|AppliedRetarget|null;check:{lines:string[];check:CheckSummary|null}|null}}|null=null;
|
|
1980
|
+
const densityLine=(text:string,error=false)=>{densityStrip?.status(text,error);if(error)message(text,true);};
|
|
1981
|
+
const renderStrip=()=>densityStrip?.show(stripView,stripPending&&stripView?.id===stripPending.id?stripPending.index:null,densityBusy||rebuildBusy||!!readOnly);
|
|
1982
|
+
async function refreshDensityStrip():Promise<void>{try{const r=await api<{strip:DensityStripViewLike|null}>('/api/density-strip');stripView=r.strip;}catch{/* keep the last view */}renderStrip();}
|
|
1983
|
+
async function buildDensityStrip(values:number[]):Promise<DensityStripViewLike|{error:string}>{
|
|
1984
|
+
if(densityBusy)return {error:'DENSITY_STRIP_BUSY: a strip is still building'};
|
|
1985
|
+
if(readOnly){densityLine(readOnly,true);return {error:readOnly};}
|
|
1986
|
+
const status=rebuildStatus??await loadRebuildStatus(),part=meshPanel?.part()??null,entry=meshPanel?.entry()??null;
|
|
1987
|
+
if(!status||!part||!entry){const t=status?.reason??'MESH_PANEL_NO_PART: select a mesh part';densityLine(t,true);return {error:t};}
|
|
1988
|
+
const problems=meshPanel?.precheck()??[];if(problems.length){const t=`${problems[0]} (review pre-check; nothing sent)`;densityLine(t,true);return {error:t};}
|
|
1989
|
+
let patches:ConfigPatch[];
|
|
1990
|
+
try{const field=densityField(entry,part);patches=values.map(v=>{const patch=makePatch(status,meshPatchOps(part,setEntryValue(entry,densityPath(field),v)),{cmd:'remesh',args:['--part',part]});if(!patch)throw new Error(status.reason??'REBUILD_STATUS_UNAVAILABLE');assertMeshPatch(patch,part);return patch;});}
|
|
1991
|
+
catch(error){const t=errorText(error);densityLine(t,true);return {error:t};}
|
|
1992
|
+
// A pending row goes back first: the strip it came from is about to be replaced.
|
|
1993
|
+
if(stripPending)await stripBack();
|
|
1994
|
+
densityBusy=true;renderStrip();densityLine(`Building a strip of ${values.length} on ${part}: ${values.join(', ')} — remesh, compile and remesh's own check per value…`);
|
|
1995
|
+
const t0=performance.now();
|
|
1996
|
+
try{
|
|
1997
|
+
let view:DensityStripViewLike|{status:string;error:string};
|
|
1998
|
+
try{view=await api<DensityStripViewLike|{status:string;error:string}>('/api/density-strip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({patches})});}
|
|
1999
|
+
catch(error){const t=errorText(error);densityLine(t,true);return {error:t};}
|
|
2000
|
+
if('error' in view){densityLine(view.error,true);return {error:view.error};}
|
|
2001
|
+
stripView=view;renderStrip();
|
|
2002
|
+
while(stripView.status==='running'){await new Promise(resolve=>setTimeout(resolve,1500));stripView=await api<DensityStripViewLike>(`/api/density-strip/${stripView.id}`);renderStrip();
|
|
2003
|
+
const settled=stripView.rows.filter(r=>r.status!=='running').length;densityLine(`Strip ${stripView.id.slice(0,12)}…: ${settled} of ${stripView.rows.length} settled · ${((performance.now()-t0)/1000).toFixed(0)} s`);}
|
|
2004
|
+
const passing=stripView.rows.filter(r=>r.verdict==='PASS').length;
|
|
2005
|
+
densityLine(`Strip of ${stripView.rows.length} on ${stripView.part} settled in ${((stripView.wallMs??0)/1000).toFixed(1)} s (at most ${stripView.concurrency} at once): ${passing} passing, ${stripView.rows.length-passing} refused or failing. Labels only — nothing is applied; Load puts a row on the stage as pending.`);
|
|
2006
|
+
return stripView;
|
|
2007
|
+
}finally{densityBusy=false;renderStrip();}
|
|
2008
|
+
}
|
|
2009
|
+
async function loadStripRow(index:number):Promise<{line:string;ok:boolean}|{error:string}>{
|
|
2010
|
+
const view=stripView,row=view?.rows[index];
|
|
2011
|
+
if(!view||!row)return {error:'DENSITY_STRIP_NOT_FOUND'};
|
|
2012
|
+
if(!row.modelUrl||!row.modelHash){const t=`DENSITY_STRIP_ROW_NOT_LOADABLE: ${row.value} — ${row.applied?'applied already':row.verdict}`;densityLine(t,true);return {error:t};}
|
|
2013
|
+
if(rebuildBusy||densityBusy)return {error:'REBUILD_BUSY'};
|
|
2014
|
+
const back=stripPending?.back??{url:stageSource?.url??inspectedModel!.url,prefix:stageSource?.prefix??inspectedModel!.prefix,hash:stageSource?.hash??inspectedModel!.sha256,applied:appliedOnStage,check:stageCheck};
|
|
2015
|
+
rebuildBusy=true;rebuildState();
|
|
2016
|
+
try{
|
|
2017
|
+
await swapRig(new URL(access.route(row.modelUrl),location.href).href,access.route(`/density-strip/${view.id}/${row.id}/`),row.modelHash,stageWorld());
|
|
2018
|
+
stripPending={index,id:view.id,candidate:row.id,back};
|
|
2019
|
+
// Pending, not applied: no record, no undo entry; the feedback context names no applied state while it is on the stage.
|
|
2020
|
+
appliedOnStage=null;stageCheck=null;
|
|
2021
|
+
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value='';draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
2022
|
+
meshPanel?.setEntry(row.entry as MeshEntry);
|
|
2023
|
+
if(!layers.mesh){layers.mesh=true;input('layer-mesh').checked=true;}
|
|
2024
|
+
if(player)drawRig(()=>player!.draw());redrawOverlay(true);
|
|
2025
|
+
const c=row.counts?{vertices:row.counts.vertices,triangles:row.counts.triangles,hull:0}:null;
|
|
2026
|
+
const agree=triangleAgreement(c?{part:view.part,before:c,after:c}:null,row.modelTriangles,lastOverlay?.meshTriangles[view.part]??null);
|
|
2027
|
+
meshPanel?.counts([{label:'PENDING',text:`${view.field} ${row.value} · ${row.counts?`v ${row.counts.vertices} · t ${row.counts.triangles}`:'no counts'} (remesh.json) — not applied`},{label:agree.ok?'AGREE':'DIFFER',text:agree.line,error:!agree.ok}]);
|
|
2028
|
+
const line=`Pending on the stage: ${view.part} ${view.field} ${row.value} (strip row ${index+1}) — not applied. The Mesh panel's Apply applies it; Back returns the stage. ${agree.line}`;
|
|
2029
|
+
densityLine(line,!agree.ok);
|
|
2030
|
+
return {line:agree.line,ok:agree.ok};
|
|
2031
|
+
}catch(error){const t=`DENSITY_STRIP_LOAD_FAILED: ${errorText(error)}`;densityLine(t,true);return {error:t};}
|
|
2032
|
+
finally{rebuildBusy=false;rebuildState();renderStrip();}
|
|
2033
|
+
}
|
|
2034
|
+
async function stripBack():Promise<void>{
|
|
2035
|
+
const pending=stripPending;if(!pending||rebuildBusy)return;
|
|
2036
|
+
rebuildBusy=true;rebuildState();
|
|
2037
|
+
try{
|
|
2038
|
+
await swapRig(pending.back.url,pending.back.prefix,pending.back.hash,stageWorld());
|
|
2039
|
+
appliedOnStage=pending.back.applied;stageCheck=pending.back.check;stripPending=null;
|
|
2040
|
+
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=appliedOnStage?JSON.stringify(appliedOnStage):'';draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
2041
|
+
meshPanel?.counts([]);redrawOverlay(true);
|
|
2042
|
+
densityLine(`Back: the stage shows ${appliedOnStage?`the last applied record (model ${pending.back.hash.slice(0,12)}…)`:`the inspected build (model ${pending.back.hash.slice(0,12)}…)`}.`);
|
|
2043
|
+
}catch(error){densityLine(`DENSITY_STRIP_BACK_FAILED: ${errorText(error)}`,true);}
|
|
2044
|
+
finally{rebuildBusy=false;rebuildState();renderStrip();}
|
|
2045
|
+
}
|
|
2046
|
+
async function closeDensityStrip():Promise<{status:string;error?:string}>{
|
|
2047
|
+
const view=stripView;if(!view)return {status:'closed'};
|
|
2048
|
+
if(stripPending&&stripPending.id===view.id)await stripBack();
|
|
2049
|
+
try{
|
|
2050
|
+
const r=await api<{status:string;error?:string}>('/api/density-strip/close',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:view.id})});
|
|
2051
|
+
if(r.status!=='closed'){densityLine(r.error??'DENSITY_STRIP_CLOSE_REFUSED',true);return r;}
|
|
2052
|
+
stripView=null;renderStrip();densityLine('Strip closed; its scratch is removed.');return r;
|
|
2053
|
+
}catch(error){const t=errorText(error);densityLine(t,true);return {status:'refused',error:t};}
|
|
2054
|
+
}
|
|
1897
2055
|
/** The folds reading (#108) of the drawn animation, on the inspected build and on the latest scratch build: one line per build. */
|
|
1898
2056
|
async function meshFolds():Promise<string[]> {
|
|
1899
2057
|
meshPanel?.status(`Running the folds reading of ${animation||'(no animation)'}…`,false);
|
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,11 +118,11 @@ 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. */
|
|
@@ -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,22 +186,24 @@ 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
|
|
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'
|
|
195
|
+
if(result.status!=='built')return {text:refusedText(result),error:true};
|
|
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'||result.check.source==='remesh'?` (${result.check.source}'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');
|
|
197
199
|
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)}`}`;
|
|
200
|
+
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'||result.check?.source==='remesh'?'':` + check ${seconds(result.wallMs.check)}`}`;
|
|
199
201
|
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};
|
|
200
202
|
}
|
|
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/styles.css
CHANGED
|
@@ -262,3 +262,5 @@ 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)}
|
|
266
|
+
.density-strip{margin-top:12px;padding-top:10px;border-top:1px solid var(--border)}.density-strip-form label{display:inline-flex;align-items:center;gap:4px}.density-strip-form input[type=number]{width:64px}.density-strip-table{overflow-x:auto;max-width:100%}.density-strip-rows{width:100%;border-collapse:collapse;font:11px var(--font-mono);font-variant-numeric:tabular-nums}.density-strip-rows th,.density-strip-rows td{padding:4px 6px;border-top:1px solid var(--border);text-align:left;vertical-align:top}.density-strip-rows th{color:var(--fg-muted);font-weight:500;white-space:nowrap}.density-strip-rows tr.pending td{background:var(--rv-pressed-bg)}.density-strip-rows tr.refused td{color:var(--fg-muted)}.density-strip-value strong{display:block;color:var(--fg-strong)}.density-strip-label{display:block;margin-top:3px;padding:1px 5px;border:1px solid var(--rv-pressed-border);border-radius:4px;color:var(--fg-strong);white-space:nowrap;font-family:var(--font-sans)}.density-strip-bars div{white-space:nowrap}.density-strip-verdict{min-width:140px;max-width:340px;overflow-wrap:anywhere}.density-strip-verdict.pass{color:var(--pass)}.density-strip-actions{white-space:nowrap}.density-strip-actions button{margin:0 4px 4px 0}
|