@rig-c/review 0.9.0 → 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 +1 -1
- package/docs/REVIEW_PACKAGE.md +11 -3
- package/package.json +1 -1
- package/src/applied-blocks.ts +48 -0
- package/src/applied-context.ts +4 -3
- package/src/density-strip.ts +201 -0
- package/src/front.ts +4 -0
- package/src/rebuild.ts +174 -28
- package/src/retune-check.ts +48 -28
- package/src/server.ts +23 -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 +114 -12
- package/web/rebuild-client.ts +2 -2
- package/web/styles.css +1 -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 {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';
|
|
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'); }
|
|
@@ -1450,7 +1455,10 @@ async function refreshApplied():Promise<void> {
|
|
|
1450
1455
|
try{appliedHistory=await api<AppliedHistoryView>('/api/rebuild/history');}catch{return;}
|
|
1451
1456
|
renderApplied();
|
|
1452
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);}
|
|
1453
1460
|
function renderApplied():void {
|
|
1461
|
+
renderAppliedBlocks();
|
|
1454
1462
|
const section=document.getElementById('applied-records');if(!section)return;
|
|
1455
1463
|
const h=appliedHistory;section.hidden=!h||h.depth===0;
|
|
1456
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;}));
|
|
@@ -1488,6 +1496,9 @@ async function undoApply():Promise<UndoView|null> {
|
|
|
1488
1496
|
}
|
|
1489
1497
|
// The panels mark the ops the stage's build applied; the pending edits stay as they are.
|
|
1490
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);
|
|
1491
1502
|
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=appliedOnStage?JSON.stringify(appliedOnStage):'';draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1492
1503
|
appliedHistory=result.history;
|
|
1493
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`;
|
|
@@ -1603,14 +1614,17 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1603
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),
|
|
1604
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,
|
|
1605
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(),
|
|
1606
|
-
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}},
|
|
1607
1621
|
/** #115: the Cuts block — its handles as client pixels (for a real-pointer drag), its ops, Apply and Reset. */
|
|
1608
1622
|
cuts:{source:()=>cutsPanel?.source()??null,ops:()=>cutsPanel?.ops()??[],handles:()=>cutsPanel?.handles()??[],active:()=>cutsPanel?.active()??false,setActive:(on:boolean)=>cutsPanel?.setActive(on),
|
|
1609
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),
|
|
1610
|
-
apply:()=>cutsPanel?.apply()??Promise.resolve(null),reset:()=>resetCutsBuild()},
|
|
1624
|
+
apply:()=>cutsPanel?.apply()??Promise.resolve(null),reset:()=>resetCutsBuild(),applied:()=>cutsPanel?.appliedItems()??[]},
|
|
1611
1625
|
/** #111: the Cloth block — the selected tag, its form values, the pending ops, the link preview, Apply and Reset. */
|
|
1612
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),
|
|
1613
|
-
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)},
|
|
1614
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),
|
|
1615
1629
|
active:()=>bonesPanel?.active()??false,setActive:(on:boolean)=>bonesPanel?.setActive(on),handles:()=>bonesPanel?.handles()??[],check:()=>bonesPanel?.check()??Promise.resolve(null),
|
|
1616
1630
|
apply:()=>applyBones((bonesPanel?.edits()??[]).map(e=>({op:'replace' as const,path:e.path,value:[...e.value],name:e.name}))),reset:()=>resetBonesBuild(),
|
|
@@ -1877,9 +1891,9 @@ function sampleLocals(bone:string,dt:number,n:number):{t:number;rotation:number;
|
|
|
1877
1891
|
return [...rows.values()].sort((a,b)=>a.t-b.t);
|
|
1878
1892
|
}
|
|
1879
1893
|
async function resetRebuild(world:StageWorld|null=null):Promise<void> {
|
|
1880
|
-
if(rebuildBusy
|
|
1894
|
+
if(rebuildBusy||(!appliedOnStage&&!stripPending)||!inspectedModel)return;
|
|
1881
1895
|
rebuildBusy=true;rebuildState();
|
|
1882
|
-
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.');}
|
|
1883
1897
|
catch(error){chestEditor?.notify(`REBUILD_RESET_FAILED: ${errorText(error)}`,true);}
|
|
1884
1898
|
finally{rebuildBusy=false;rebuildState();}
|
|
1885
1899
|
}
|
|
@@ -1887,10 +1901,11 @@ async function resetRebuild(world:StageWorld|null=null):Promise<void> {
|
|
|
1887
1901
|
* Replace the drawn rig in place: the new model loads first (a failure leaves the stage as it was), then the player is
|
|
1888
1902
|
* swapped and the same animation, time, set, camera and environment are restored; playback resumes when it was running.
|
|
1889
1903
|
*/
|
|
1904
|
+
let stageSource:{url:string;prefix:string;hash:string}|null=null;
|
|
1890
1905
|
async function swapRig(modelUrl:string,prefix:string,sha256:string,world:StageWorld|null=null):Promise<void> {
|
|
1891
1906
|
const keepAnimation=animation,keepTime=player?.time??time,wasPlaying=playing,keepSet=select('part-set').disabled?'':select('part-set').value;
|
|
1892
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;}});
|
|
1893
|
-
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};
|
|
1894
1909
|
// A scratch build may hold another set of animations (a retarget adds one, #114): the picker lists what this build has.
|
|
1895
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;}
|
|
1896
1911
|
player.on('event',event=>{text('event-log',`Runtime event: ${event.name} · ${event.time.toFixed(2)} s`);});
|
|
@@ -1929,6 +1944,8 @@ async function applyMesh(part:string,entry:MeshEntry):Promise<PatchJobView|Rebui
|
|
|
1929
1944
|
const result=await applyPatch(patch,world);
|
|
1930
1945
|
const outcome=meshApplyOutcome(result as Parameters<typeof meshApplyOutcome>[0]);meshPanel?.status(outcome.text,outcome.error);
|
|
1931
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();}
|
|
1932
1949
|
// The new triangulation is drawn: the mesh edges go on and the overlay captures the reloaded pose.
|
|
1933
1950
|
if(!layers.mesh){layers.mesh=true;input('layer-mesh').checked=true;}
|
|
1934
1951
|
if(player)drawRig(()=>player!.draw());redrawOverlay(true);
|
|
@@ -1947,9 +1964,94 @@ async function applyMesh(part:string,entry:MeshEntry):Promise<PatchJobView|Rebui
|
|
|
1947
1964
|
/** Mesh Reset: the inspected build back on the stage (same world); the panel already holds the config's entry again. */
|
|
1948
1965
|
async function resetMeshBuild():Promise<void> {
|
|
1949
1966
|
meshPanel?.counts([]);meshPanel?.readout([]);
|
|
1950
|
-
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);}}
|
|
1951
1968
|
meshPanel?.status('Reset: the inspected build is on the stage; the pending entry is the config\'s.',false);
|
|
1952
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
|
+
}
|
|
1953
2055
|
/** The folds reading (#108) of the drawn animation, on the inspected build and on the latest scratch build: one line per build. */
|
|
1954
2056
|
async function meshFolds():Promise<string[]> {
|
|
1955
2057
|
meshPanel?.status(`Running the folds reading of ${animation||'(no animation)'}…`,false);
|
package/web/rebuild-client.ts
CHANGED
|
@@ -193,11 +193,11 @@ const seconds=(ms:number|null)=>ms===null?'—':`${(ms/1000).toFixed(1)} s`;
|
|
|
193
193
|
export function patchOutcomeLine(result:PatchJobView|RebuildRefusal):{text:string;error:boolean} {
|
|
194
194
|
if(!('id' in result))return {text:result.producerLine??result.error,error:true};
|
|
195
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'
|
|
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';
|
|
197
197
|
// #115: a build's own wall clock covers assemble, rig and check; the recorded figure is the same number.
|
|
198
198
|
const building=result.op==='build'||(result.op==='cloth'&&result.cloth?.route==='build');
|
|
199
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)) + `:'';
|
|
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'?'':` + 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)}`}`;
|
|
201
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};
|
|
202
202
|
}
|
|
203
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. */
|
package/web/styles.css
CHANGED
|
@@ -263,3 +263,4 @@ footer.status-bar{flex-wrap:nowrap}
|
|
|
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
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}
|