@rig-c/review 0.10.1 → 0.10.2
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 +5 -3
- package/docs/LIVE_FIXTURE.md +93 -0
- package/docs/REVIEW_PACKAGE.md +8 -3
- package/package.json +2 -1
- package/src/animation-selection.ts +2 -0
- package/src/applied-blocks.ts +14 -0
- package/src/baseline-reach.ts +12 -1
- package/src/bones-check.ts +3 -5
- package/src/cloth-recipe.ts +12 -8
- package/src/environment-check.ts +3 -5
- package/src/environment-schedule.ts +3 -1
- package/src/manifest.ts +6 -2
- package/src/open.ts +5 -3
- package/src/readings-run.ts +3 -5
- package/src/readings.ts +8 -0
- package/src/rebuild.ts +126 -72
- package/src/requirements-run.ts +3 -5
- package/src/requirements.ts +6 -2
- package/src/state-dir.ts +48 -0
- package/web/environment-world.ts +15 -0
- package/web/label-placement.ts +4 -0
- package/web/main.ts +51 -27
- package/web/rebuild-client.ts +27 -0
- package/web/retarget-panel.ts +7 -2
package/web/main.ts
CHANGED
|
@@ -18,10 +18,10 @@ import {modelBoneNames,readSetupPose} from './setup-pose.ts';
|
|
|
18
18
|
import {bindBonesPanel,type BonesPanel,type BonesCheckView} from './bones-panel.ts';
|
|
19
19
|
import {bindCutsPanel,type CutsPanel} from './cuts-panel.ts';
|
|
20
20
|
import {bindClothPanel,type ClothPanel} from './cloth-panel.ts';
|
|
21
|
-
import {retargetAppliedRecord,retargetOutcomeLine,type AppliedRetarget,type RetargetJobView,type RetargetRequestBody} from './rebuild-client.ts';
|
|
21
|
+
import {retargetAppliedRecord,retargetOutcomeLine,stripPendingAfter,type StageChange,type AppliedRetarget,type RetargetJobView,type RetargetRequestBody} from './rebuild-client.ts';
|
|
22
22
|
import {bindRetargetPanel,type PoseMap,type RetargetPanel} from './retarget-panel.ts';
|
|
23
23
|
import type {RetargetFlags} from '../src/retarget.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';
|
|
24
|
+
import {historyRow,isPatchJob,recordFor,refusedText,retargetStage,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';
|
|
25
25
|
import {meshEditBinding,meshEditText,type MeshEditDocument} from '../src/mesh-edit.ts';
|
|
26
26
|
import {chestRegionsBinding,cropWorldMapping,type CropWorldMapping} from '../src/chest-regions.ts';
|
|
27
27
|
import {bindWorkspaceUI,selectedInspector,type WorkspaceUI} from './workspace-ui.ts';
|
|
@@ -31,26 +31,26 @@ import {pageInstanceAccess,readOnlyReason} from './instance-access.ts';
|
|
|
31
31
|
import {controlComparison,type ControlCommand,type ControlInspection,type ControlView} from '../src/control.ts';
|
|
32
32
|
import {singleEligibility,videoReferenceAt,type InputState,type SingleLayout} from '../src/comparison-eligibility.ts';
|
|
33
33
|
import {connectInspectionPersistence,serializableView,numericCamera,restoredZoom,restoreTransaction,captureInputs,restoreInputs,type InspectionBookmark,type InspectionBinding,type PersistenceConnection} from './inspection-persistence.ts';
|
|
34
|
-
import {initialPlayerAnimation} from '../src/animation-selection.ts';
|
|
34
|
+
import {initialPlayerAnimation,keptAnimation} from '../src/animation-selection.ts';
|
|
35
35
|
import {initialReviewSelection} from '../src/open-selection.ts';
|
|
36
36
|
import type {ReferenceBinding} from '../src/reference.ts';
|
|
37
37
|
import type { Checkpoint, Json, Reference, ReviewedSource, ReviewManifest } from '../src/manifest.ts';
|
|
38
38
|
import type { Feedback, ReviewSession } from '../src/session.ts';
|
|
39
39
|
import type { ReportEvent, ReportSeeds } from '../src/report.ts';
|
|
40
|
-
import { frameAtTime, lastFrame as lastProducerFrame, rigPaneLabel,
|
|
40
|
+
import { frameAtTime, lastFrame as lastProducerFrame, rigPaneLabel, timeAtFrame, transportClock, type StageClock } from '../src/frame-clock.ts';
|
|
41
41
|
import { feedbackLastFrame, feedbackTargetNames, validateFeedbackSelection, type FeedbackCatalog } from '../src/feedback-validation.ts';
|
|
42
42
|
import { eventCoordinates, eventSeekTime, eventSelection, eventTimingLabel, reportCounts, reportDraftForMarker, targetLabel } from './report-view.ts';
|
|
43
43
|
import type { StripInterval, StripRequest } from '../src/strip.ts';
|
|
44
44
|
import {createWindModel,environmentFor,windReadout,RUNTIME_START_READOUT,joltPacket,dueJolts,dragMoved,loadModelDocument,environmentContext,environmentJson,type JoltStep,type WindSnapshot,type WindState} from '../src/environment.ts';
|
|
45
45
|
import {tuningRows,readoutRows,type InspectedCheckView} from '../src/physics-tuning.ts';
|
|
46
|
-
import {bindReadingsPanel,
|
|
47
|
-
import {READINGS_SCHEMA,type ReadingsContext} from '../src/readings.ts';
|
|
48
|
-
import {environmentCheckRows,scheduleContext,worldExport,type EnvironmentCheckView,type WorldExport} from './environment-world.ts';
|
|
46
|
+
import {bindReadingsPanel,type ReadingsJobView,type ReadingsPanel,type ReadingsRefusal} from './readings-panel.ts';
|
|
47
|
+
import {READINGS_SCHEMA,readingSeekTime,type ReadingsContext} from '../src/readings.ts';
|
|
48
|
+
import {restartsWindClock,type ClipMove,environmentCheckRows,scheduleContext,worldExport,type EnvironmentCheckView,type WorldExport} from './environment-world.ts';
|
|
49
49
|
import {barLine,beforeLine} from '../src/physics-tuning.ts';
|
|
50
50
|
import {bindRequirementsPanel,type RequirementsCatalogView,type RequirementsJobView,type RequirementsPanel,type RequirementsRefusal} from './requirements-panel.ts';
|
|
51
51
|
import {patchRoute} from '../src/retune-route.ts';
|
|
52
52
|
import {appliedCheckRefusal,type CheckSummary} from '../src/applied-context.ts';
|
|
53
|
-
import {appliedBlockOps,recordBlock} from '../src/applied-blocks.ts';
|
|
53
|
+
import {appliedBlockOps,recordBlock,staleReadouts} from '../src/applied-blocks.ts';
|
|
54
54
|
import {bindPhysicsPanel,type PhysicsPanel} from './physics-panel.ts';
|
|
55
55
|
import {bindIdlePanel,type BoneSetup,type IdlePanel} from './idle-panel.ts';
|
|
56
56
|
import {bindRigFieldsPanel,type RigFieldsPanel} from './rig-fields-panel.ts';
|
|
@@ -475,18 +475,21 @@ function drawRig(action:()=>void):void {
|
|
|
475
475
|
catch(error){overlays?.clear();text('renderer-state',rendererStatus(null,'failed').label);throw error;}
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
function chooseAnimation(name: string): void {
|
|
478
|
+
function chooseAnimation(name: string, move: ClipMove = { kind: 'choose' }): void {
|
|
479
479
|
pause(); animation = name; axisInspection = false; el('angle-panel').hidden = true;
|
|
480
480
|
const runtimeAnimation = assets?.animations.find(item => item.name === name);
|
|
481
|
-
duration =
|
|
481
|
+
duration = clipDuration(name);
|
|
482
482
|
select('animation-select').value = name;
|
|
483
|
+
// #191: the wind restarts before the clip's first frame is drawn, so the paused stage shows the schedule's t=0 world.
|
|
484
|
+
time = 0; clipMoved(move);
|
|
483
485
|
if (player && runtimeAnimation) { drawRig(()=>{player!.play(name,{loop:false});player!.pause();player!.draw();}); }
|
|
484
486
|
else if (player && name){text('renderer-state',rendererStatus(null,'failed').label);message(`Animation "${name}" is not present in the rig.`, true);}
|
|
485
|
-
|
|
487
|
+
renderTransport(); renderCues();renderIdle();
|
|
486
488
|
}
|
|
487
489
|
|
|
488
|
-
function seek(value: number): void {
|
|
490
|
+
function seek(value: number, move?: ClipMove): void {
|
|
489
491
|
time = duration > 0 ? Math.min(Math.max(0, value), duration) : Math.max(0, value);
|
|
492
|
+
clipMoved(move ?? { kind: 'seek', to: time });
|
|
490
493
|
if (player && player.current) { try { player.seek(time); reposePreview(); drawRig(()=>player!.draw()); } catch (error) {text('renderer-state',rendererStatus(null,'failed').label); message(errorText(error), true); } }
|
|
491
494
|
if (video && video.readyState >= 1) holdVideoReference(video, referenceGeneration);
|
|
492
495
|
renderSequenceFrame(); renderTransport();
|
|
@@ -611,8 +614,9 @@ function seekReportEvent(): void {
|
|
|
611
614
|
/** A reading tick or worst line: its frame's time (frame / fps of the reading's set) in the reading's animation. */
|
|
612
615
|
function seekReading(clip: string, frame: number, exact: number | null, label: string): void {
|
|
613
616
|
if (exact === null) { message(`${label}: F${frame} has no time (the reading's set states no fps); no seek was performed.`, true); return; }
|
|
614
|
-
// The clip's last sample can sit a rounding hair past the runtime duration; that tick is the clip's end
|
|
615
|
-
|
|
617
|
+
// The clip's last sample can sit a rounding hair past the runtime duration; that tick is the clip's end — the end of
|
|
618
|
+
// the reading's clip, the one the seek switches to (#186), not the clip drawn now.
|
|
619
|
+
const at = readingSeekTime(exact, clipDuration(clip));
|
|
616
620
|
if (seekMarker(clip, at, 'Reading')) message(`Reading · ${label} → F${frame} · ${exact.toFixed(4)} s`);
|
|
617
621
|
}
|
|
618
622
|
|
|
@@ -623,7 +627,7 @@ function tick(now: number): void {
|
|
|
623
627
|
if (player && player.current) { try { stepEnvironment(dt); drawRig(()=>player!.frame(dt)); } catch (error) { pause(); message(errorText(error), true); } }
|
|
624
628
|
if (video && video.readyState >= 1 && time > video.duration) { if (referenceInput.state !== 'out-of-domain') holdVideoReference(video, referenceGeneration); } else if (video && referenceInput.state === 'loaded' && Math.abs(video.currentTime - time) > .15) video.currentTime = time;
|
|
625
629
|
if (input('sync-audio').checked) for (const cue of activeCues()) if ((previous < cue.time && cue.time <= time) || (previous === 0 && cue.time === 0 && dt > 0)) void audition(cue.clip);
|
|
626
|
-
renderSequenceFrame(); renderTransport(); if(time>=duration){if(canRepeatIdle()&&!idleHeld){stopAudio();
|
|
630
|
+
renderSequenceFrame(); renderTransport(); if(time>=duration){if(canRepeatIdle()&&!idleHeld){stopAudio();seek(0,{kind:'replay'});player?.resume();if(video)void video.play().catch(error=>{pause();message(errorText(error),true);});}else pause();} lastFrame = now;
|
|
627
631
|
}
|
|
628
632
|
requestAnimationFrame(tick);
|
|
629
633
|
}
|
|
@@ -992,7 +996,7 @@ function canRepeatIdle():boolean{return !!player&&idleAvailable(assets?.animatio
|
|
|
992
996
|
function renderIdle():void {text('idle-state',canRepeatIdle()?'Actual idle · repeat enabled':animation==='idle'?'Idle repeat paused':'Non-idle inspection · no automatic playback');}
|
|
993
997
|
async function playPreview():Promise<void> {
|
|
994
998
|
if((!player&&!video&&!sequence)||duration<=0)return;
|
|
995
|
-
const intent=++playbackIntent;desiredPlay=true;idleHeld=false;if(time>=duration)seek(0);
|
|
999
|
+
const intent=++playbackIntent;desiredPlay=true;idleHeld=false;if(time>=duration)seek(0,{kind:'replay'});
|
|
996
1000
|
if(video&&referenceInput.state==='loaded'){try{await video.play();}catch(error){pause();message(`Playback is paused: ${errorText(error)}. Use Play to retry.`,true);return;}}
|
|
997
1001
|
if(intent!==playbackIntent||!desiredPlay){if(!desiredPlay)video?.pause();return;}playing=true;lastFrame=0;player?.resume();button('play').textContent='Ⅱ';button('play').setAttribute('aria-label','Pause');
|
|
998
1002
|
}
|
|
@@ -1022,6 +1026,8 @@ async function applyControl(command:ControlCommand,signal:AbortSignal):Promise<v
|
|
|
1022
1026
|
}
|
|
1023
1027
|
async function inspectControlTime(target:number,signal:AbortSignal):Promise<void> {
|
|
1024
1028
|
if(signal.aborted)throw new Error('CONTROL_SUPERSEDED');
|
|
1029
|
+
// #191: a control seek is a clip move like the transport's: a seek to 0 restarts the wind clock while the exported world is in use.
|
|
1030
|
+
clipMoved({kind:'seek',to:target});
|
|
1025
1031
|
const media=video;
|
|
1026
1032
|
if(!media){if(player){player.seek(target);reposePreview();drawRig(()=>player!.draw());}time=target;renderSequenceFrame();renderTransport();return;}
|
|
1027
1033
|
if(media.readyState<1||target>media.duration)throw new Error('CONTROL_REFERENCE_UNAVAILABLE');
|
|
@@ -1227,6 +1233,8 @@ function worldFor(name:string):WorldExport|{error:string} {
|
|
|
1227
1233
|
}
|
|
1228
1234
|
/** A fresh wind clock at the same settings (a repeating gust restarts at 0); pending jolts keep their distance in time. */
|
|
1229
1235
|
function restartWindClock():void {const now=wind.read().time;wind.restore(wind.snapshot());joltQueue=joltQueue.map(step=>({...step,at:Math.max(0,step.at-now)}));}
|
|
1236
|
+
/** #183: every clip restart (`restartsWindClock`) restarts the wind clock while the schedule is watched, not only the checkbox. */
|
|
1237
|
+
function clipMoved(move:ClipMove):void {if(player&&restartsWindClock(move,useWorld())){restartWindClock();applyEnvironment();}}
|
|
1230
1238
|
let scheduleShown='';
|
|
1231
1239
|
function renderSchedule():void {
|
|
1232
1240
|
const world=worldFor(animation),check=checkAnimation(),checked=check!==animation?worldFor(check):null;
|
|
@@ -1389,8 +1397,8 @@ async function applyRetarget(body:{sequence:string;map:PoseMap;flags:RetargetFla
|
|
|
1389
1397
|
if(next.id===null||!('id' in result)){message(outcome.text,true);return result;}
|
|
1390
1398
|
await swapRig(new URL(access.route(next.url),location.href).href,access.route(`/rebuild/${next.id}/`),next.hash,world);
|
|
1391
1399
|
// The new animation's own clock (the run's report frameTimes), bound to this build: the transport counter and the rig pane read it.
|
|
1392
|
-
|
|
1393
|
-
appliedOnStage=retargetAppliedRecord(result);stageCheck=null;
|
|
1400
|
+
stageClock=retargetStage(result,next.hash)?.clock??null;
|
|
1401
|
+
appliedOnStage=retargetAppliedRecord(result);stageCheck=null;stageChanged({kind:'apply',via:'retarget'});
|
|
1394
1402
|
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=JSON.stringify(appliedOnStage);draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1395
1403
|
// The new animation plays against the reference: choose it, and show the sequence it was made from beside it.
|
|
1396
1404
|
const name=result.request.record.flags.animation;if(assets?.animations.some(clip=>clip.name===name))chooseAnimation(name);
|
|
@@ -1418,7 +1426,7 @@ async function applyMeshDelta(doc:MeshEditDocument,line:(text:string)=>void):Pro
|
|
|
1418
1426
|
const outcome=deltaOutcomeLine(result),next=stageModel(result,{url:'',hash:stageModelHash??''});
|
|
1419
1427
|
if(next.id===null||!('id' in result)){message(outcome.text,true);return {built:false,text:outcome.text,error:true};}
|
|
1420
1428
|
await swapRig(new URL(access.route(next.url),location.href).href,access.route(`/rebuild/${next.id}/`),next.hash,world);
|
|
1421
|
-
appliedOnStage=deltaAppliedRecord(result);stageCheck=null;
|
|
1429
|
+
appliedOnStage=deltaAppliedRecord(result);stageCheck=null;stageChanged({kind:'apply',via:'delta'});
|
|
1422
1430
|
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=JSON.stringify(appliedOnStage);draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1423
1431
|
message(outcome.text);
|
|
1424
1432
|
return {built:true,text:outcome.text,error:false};
|
|
@@ -1456,7 +1464,13 @@ async function refreshApplied():Promise<void> {
|
|
|
1456
1464
|
renderApplied();
|
|
1457
1465
|
}
|
|
1458
1466
|
/** #172: the Cuts and Cloth blocks mark as applied only the ops the stage's applied record holds (none on the inspected build). */
|
|
1459
|
-
|
|
1467
|
+
let readoutStage:AppliedRecord|AppliedPatch|AppliedDelta|AppliedRetarget|null=null;
|
|
1468
|
+
function renderAppliedBlocks():void {
|
|
1469
|
+
const on=appliedBlockOps(appliedOnStage);cutsPanel?.applied(on.cuts);clothPanel?.applied(on.cloth);
|
|
1470
|
+
// #186: when the stage record changes, a Cuts / Cloth readout the new record is not that block's names an older build.
|
|
1471
|
+
for(const block of staleReadouts(readoutStage,appliedOnStage))(block==='cuts'?cutsPanel:clothPanel)?.shown(null);
|
|
1472
|
+
readoutStage=appliedOnStage;
|
|
1473
|
+
}
|
|
1460
1474
|
function renderApplied():void {
|
|
1461
1475
|
renderAppliedBlocks();
|
|
1462
1476
|
const section=document.getElementById('applied-records');if(!section)return;
|
|
@@ -1493,7 +1507,11 @@ async function undoApply():Promise<UndoView|null> {
|
|
|
1493
1507
|
await swapRig(new URL(access.route(job.build.modelUrl),location.href).href,access.route(`/rebuild/${job.id}/`),job.build.modelHash,world);
|
|
1494
1508
|
appliedOnStage=recordFor(job);stageCheck=(job as PatchJobView).kind==='patch'?{lines:[...job.lines],check:(job as PatchJobView).check}:null;
|
|
1495
1509
|
if((job as PatchJobView).kind==='patch')ops=(job as PatchJobView).request.patch.ops;
|
|
1510
|
+
// #186: a restored retarget build gets what its Apply set — its own clock, then its animation selected.
|
|
1511
|
+
const retarget=retargetStage(job,job.build.modelHash);stageClock=retarget?.clock??null;
|
|
1512
|
+
if(retarget&&assets?.animations.some(clip=>clip.name===retarget.animation))chooseAnimation(retarget.animation);
|
|
1496
1513
|
}
|
|
1514
|
+
stageChanged({kind:'undo'});
|
|
1497
1515
|
// The panels mark the ops the stage's build applied; the pending edits stay as they are.
|
|
1498
1516
|
physicsPanel?.applied(ops);idlePanel?.applied(ops);rigFieldsPanel?.applied(ops);bonesPanel?.applied(ops);
|
|
1499
1517
|
// #172: the Cuts and Cloth readouts show the restored build's job when it is theirs, else nothing (never the reverted one).
|
|
@@ -1531,7 +1549,7 @@ async function applyRebuild():Promise<void> {
|
|
|
1531
1549
|
// A refusal stays readable in full: the bar's status line is one line, so the log ends with the producer's line verbatim.
|
|
1532
1550
|
if(!('id' in result)||result.status!=='built'||!result.build){chestEditor?.notify(outcome.text,true,[...lines.slice(-5),outcome.text]);message(outcome.text,true);return;}
|
|
1533
1551
|
await swapRig(new URL(access.route(result.build.modelUrl),location.href).href,access.route(`/rebuild/${result.id}/`),result.build.modelHash);
|
|
1534
|
-
appliedOnStage=appliedRecord(result);stageCheck=null;
|
|
1552
|
+
appliedOnStage=appliedRecord(result);stageCheck=null;stageChanged({kind:'apply',via:'regions'});
|
|
1535
1553
|
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=JSON.stringify(appliedOnStage);draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1536
1554
|
// The log closes on success so nothing floats over the rebuilt stage; a refusal keeps it open.
|
|
1537
1555
|
chestEditor?.notify(outcome.text,false,[]);message(outcome.text);
|
|
@@ -1559,7 +1577,7 @@ async function applyPatch(patch:ConfigPatch,world:StageWorld|null=null):Promise<
|
|
|
1559
1577
|
const outcome=patchOutcomeLine(result),next=stageModel(result,{url:'',hash:stageModelHash??''});
|
|
1560
1578
|
if(next.id===null||!('id' in result)){chestEditor?.notify(outcome.text,true,[...lines.slice(-5),outcome.text]);message(outcome.text,true);return result;}
|
|
1561
1579
|
await swapRig(new URL(access.route(next.url),location.href).href,access.route(`/rebuild/${next.id}/`),next.hash,world);
|
|
1562
|
-
appliedOnStage=patchAppliedRecord(result);stageCheck={lines:[...result.lines],check:result.check};
|
|
1580
|
+
appliedOnStage=patchAppliedRecord(result);stageCheck={lines:[...result.lines],check:result.check};stageChanged({kind:'apply',via:'patch'});
|
|
1563
1581
|
const draft=el<HTMLTextAreaElement>('rebuild-applied');draft.value=JSON.stringify(appliedOnStage);draft.dispatchEvent(new Event('input',{bubbles:true}));
|
|
1564
1582
|
chestEditor?.notify(outcome.text,outcome.error,[]);message(outcome.text,outcome.error);
|
|
1565
1583
|
return result;
|
|
@@ -1893,7 +1911,7 @@ function sampleLocals(bone:string,dt:number,n:number):{t:number;rotation:number;
|
|
|
1893
1911
|
async function resetRebuild(world:StageWorld|null=null):Promise<void> {
|
|
1894
1912
|
if(rebuildBusy||(!appliedOnStage&&!stripPending)||!inspectedModel)return;
|
|
1895
1913
|
rebuildBusy=true;rebuildState();
|
|
1896
|
-
try{await swapRig(inspectedModel.url,inspectedModel.prefix,inspectedModel.sha256,world);appliedOnStage=null;stageCheck=null;
|
|
1914
|
+
try{await swapRig(inspectedModel.url,inspectedModel.prefix,inspectedModel.sha256,world);appliedOnStage=null;stageCheck=null;stageChanged({kind:'reset'});chestEditor?.notify('Reset: the inspected build is on the stage. The draft keeps the last applied state.',false);message('Reset to the inspected build.');}
|
|
1897
1915
|
catch(error){chestEditor?.notify(`REBUILD_RESET_FAILED: ${errorText(error)}`,true);}
|
|
1898
1916
|
finally{rebuildBusy=false;rebuildState();}
|
|
1899
1917
|
}
|
|
@@ -1915,7 +1933,12 @@ async function swapRig(modelUrl:string,prefix:string,sha256:string,world:StageWo
|
|
|
1915
1933
|
// whether the controls are engaged as they stood when Apply was pressed; then the seek re-poses under that world.
|
|
1916
1934
|
if(world){wind.resume(world.wind);joltQueue=world.jolts.map(step=>({...step}));input('environment-applied').checked=world.applied;input('gravity').value=String(world.gravity);syncEnvironmentInputs();}
|
|
1917
1935
|
if(environmentApplied())player.environment(environmentFor(wind.read(),gravityValue()));
|
|
1918
|
-
|
|
1936
|
+
// #186: a model without the staged animation (Reset after a retarget, a rebuild without that clip) clears it, so the
|
|
1937
|
+
// rig pane and the transport name the setup pose drawn, not the removed clip.
|
|
1938
|
+
if(keptAnimation(keepAnimation,assets.animations.map(clip=>clip.name))){chooseAnimation(keepAnimation,{kind:'reload'});seek(keepTime,{kind:'reload'});}
|
|
1939
|
+
// #197: the picker was just refilled (which selects its first option), so it is cleared whenever no clip is kept — even
|
|
1940
|
+
// when none was staged — and nothing claims a clip the stage is not drawing.
|
|
1941
|
+
else{drawRig(()=>player!.draw());animation='';duration=0;time=0;select('animation-select').value='';renderTransport();renderCues();renderIdle();}
|
|
1919
1942
|
if(environmentApplied())applyEnvironment();
|
|
1920
1943
|
updateChestMapping();fitCamera();
|
|
1921
1944
|
// The preview (#104) drives the new player once its bone setups are read (an idle Apply cleared it first).
|
|
@@ -1944,8 +1967,7 @@ async function applyMesh(part:string,entry:MeshEntry):Promise<PatchJobView|Rebui
|
|
|
1944
1967
|
const result=await applyPatch(patch,world);
|
|
1945
1968
|
const outcome=meshApplyOutcome(result as Parameters<typeof meshApplyOutcome>[0]);meshPanel?.status(outcome.text,outcome.error);
|
|
1946
1969
|
if('id' in result&&result.status==='built'){
|
|
1947
|
-
// #173
|
|
1948
|
-
if(stripPending){stripPending=null;void refreshDensityStrip();}
|
|
1970
|
+
// #173 / #183: the pending strip row (if any) was cleared by applyPatch's landing, as after every Apply.
|
|
1949
1971
|
// The new triangulation is drawn: the mesh edges go on and the overlay captures the reloaded pose.
|
|
1950
1972
|
if(!layers.mesh){layers.mesh=true;input('layer-mesh').checked=true;}
|
|
1951
1973
|
if(player)drawRig(()=>player!.draw());redrawOverlay(true);
|
|
@@ -1972,11 +1994,13 @@ async function resetMeshBuild():Promise<void> {
|
|
|
1972
1994
|
* sent together to `POST /api/density-strip` and polled until every candidate settles. Load: a built row's revision on
|
|
1973
1995
|
* the stage as pending — the applied record, the undo stack and the feedback context's applied state are left alone
|
|
1974
1996
|
* (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
|
|
1997
|
+
* Back returns the stage to what it showed or an Apply lands (the Mesh panel's reuses the row's build; any Apply, Reset or Undo clears the pending row, #183). Close removes
|
|
1976
1998
|
* the strip's scratch.
|
|
1977
1999
|
*/
|
|
1978
2000
|
let stripView:DensityStripViewLike|null=null,densityBusy=false;
|
|
1979
2001
|
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;
|
|
2002
|
+
/** #183: after every landed Apply, Reset or Undo the pending row and Back go (`stripPendingAfter`); the strip re-reads (a row Applied is marked). */
|
|
2003
|
+
function stageChanged(change:StageChange<NonNullable<typeof stripPending>>):void {const next=stripPendingAfter(stripPending,change);if(next===stripPending)return;stripPending=next;void refreshDensityStrip();}
|
|
1980
2004
|
const densityLine=(text:string,error=false)=>{densityStrip?.status(text,error);if(error)message(text,true);};
|
|
1981
2005
|
const renderStrip=()=>densityStrip?.show(stripView,stripPending&&stripView?.id===stripPending.id?stripPending.index:null,densityBusy||rebuildBusy||!!readOnly);
|
|
1982
2006
|
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();}
|
package/web/rebuild-client.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {MotionSummary,RetargetFlags,RetargetLine,RetargetRecord} from '../s
|
|
|
11
11
|
export type {ConfigPatch,PatchOp,AppliedPatch,AppliedDelta,AppliedRetarget,CheckSummary,BustSpec};
|
|
12
12
|
import {chestContourFields,chestRegionRows,chestRowId,type ChestFieldReader} from './chest-regions-tool.ts';
|
|
13
13
|
import type {InspectedCheckView} from '../src/physics-tuning.ts';
|
|
14
|
+
import {samplesClock,type StageClock} from '../src/frame-clock.ts';
|
|
14
15
|
|
|
15
16
|
export type RegionEntry=
|
|
16
17
|
|{name:string;shape:'circle';cx:number;cy:number;r:number;bone:string;spacing:number;band:number}
|
|
@@ -235,6 +236,16 @@ export function recordFor(job:AnyJobView):AppliedRecord|AppliedPatch|AppliedDelt
|
|
|
235
236
|
const kind=(job as {kind?:string}).kind;
|
|
236
237
|
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
|
}
|
|
239
|
+
/**
|
|
240
|
+
* What a retarget build on the stage sets beside its model (#164, #186): its new animation, to select, and that
|
|
241
|
+
* animation's own clock (the run's report frameTimes) bound to the build's model hash. The same for an Apply and for an
|
|
242
|
+
* Undo that restores a retarget build; null for any other job (it restores no retarget selection or clock).
|
|
243
|
+
*/
|
|
244
|
+
export function retargetStage(job:AnyJobView,modelHash:string):{animation:string;clock:StageClock|null}|null {
|
|
245
|
+
if((job as {kind?:string}).kind!=='retarget')return null;
|
|
246
|
+
const r=job as RetargetJobView,animation=r.request.record.flags.animation,own=samplesClock(r.frameTimes);
|
|
247
|
+
return {animation,clock:own?{animation,modelHash,clock:own}:null};
|
|
248
|
+
}
|
|
238
249
|
/** #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
250
|
export function historyRow(history:AppliedHistoryView,index:number):string {
|
|
240
251
|
const r=history.records[index]!,last=index===history.records.length-1;
|
|
@@ -249,3 +260,19 @@ export function undoControl(history:AppliedHistoryView|null,stageId:string|null,
|
|
|
249
260
|
const back=depth>1?`record ${depth-1} (model ${history!.records[depth-2]!.modelHash.slice(0,12)}…)`:'the inspected build';
|
|
250
261
|
return {label,enabled:!busy,title:`Return the stage to ${back}; the package is unchanged`};
|
|
251
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* #183: a density-strip row pending on the stage after a stage change. Every landed Apply — whichever block sent it
|
|
265
|
+
* (regions, any patch block, Edit Mesh's delta, retarget) — and every Reset and Undo replaces the stage the row was
|
|
266
|
+
* loaded over and moves the server's stack, so the row stops being pending and Back has nothing to return to (returning
|
|
267
|
+
* would restore a stale stage over the applied record). A refused or failed step leaves the stage, and the row, as they
|
|
268
|
+
* were; only Load puts a row on the stage.
|
|
269
|
+
*/
|
|
270
|
+
export type StageChange<P>=
|
|
271
|
+
|{kind:'apply';via:'regions'|'mesh'|'physics'|'idle'|'cuts'|'cloth'|'rig-fields'|'bones'|'patch'|'delta'|'retarget'}
|
|
272
|
+
|{kind:'reset'}|{kind:'undo'}|{kind:'refused'}
|
|
273
|
+
|{kind:'load';row:P};
|
|
274
|
+
export function stripPendingAfter<P>(pending:P|null,change:StageChange<P>):P|null {
|
|
275
|
+
if(change.kind==='load')return change.row;
|
|
276
|
+
if(change.kind==='refused')return pending;
|
|
277
|
+
return null;
|
|
278
|
+
}
|
package/web/retarget-panel.ts
CHANGED
|
@@ -132,7 +132,12 @@ export function bindRetargetPanel(deps:RetargetPanelDeps):RetargetPanel {
|
|
|
132
132
|
template.onclick=()=>setMap(templateMap());
|
|
133
133
|
file.onchange=()=>{const f=file.files?.[0];if(!f)return;void f.text().then(text=>{const read=readMap(text);if('error' in read)say(read.error,true);else{setMap(read.map);say(`Map loaded from ${f.name} (by content; nothing is sent until Apply).`,false);}file.value='';});};
|
|
134
134
|
useJson.onclick=()=>{const read=readMap(json.value);if('error' in read)say(read.error,true);else{setMap(read.map);say('Map taken from the JSON field.',false);}};
|
|
135
|
-
sequence
|
|
135
|
+
// #186: `clean first` follows the selected sequence whenever the selection changes — on for a raw one (the producer
|
|
136
|
+
// refuses an uncleaned sequence, POSE_NOT_CLEANED), off for one already cleaned. A reload that keeps the selection keeps
|
|
137
|
+
// the reviewer's own choice.
|
|
138
|
+
let cleanFor:string|null=null;
|
|
139
|
+
function deriveClean(){const id=sequence.value;if(id===cleanFor)return;cleanFor=id;const s=status?.sequences.find(x=>x.id===id);if(s)clean.checked=!s.cleaned;}
|
|
140
|
+
sequence.onchange=()=>{deriveClean();people();syncApply();};
|
|
136
141
|
reset.onclick=()=>deps.reset();
|
|
137
142
|
|
|
138
143
|
function flags():RetargetFlags|{error:string}{
|
|
@@ -176,7 +181,7 @@ export function bindRetargetPanel(deps:RetargetPanelDeps):RetargetPanel {
|
|
|
176
181
|
status=next;const prior=sequence.value;
|
|
177
182
|
sequence.replaceChildren(...(next?.sequences??[]).map(s=>make('option',{value:s.id,textContent:`${s.label} · ${s.frames} frame(s) · ${s.cleaned?'cleaned':'raw'}`})));
|
|
178
183
|
if(prior&&next?.sequences.some(s=>s.id===prior))sequence.value=prior;
|
|
179
|
-
|
|
184
|
+
deriveClean();
|
|
180
185
|
people();if(!entries.length)setMap(templateMap());else render();
|
|
181
186
|
say(next?.available?`${next.producer?.package} ${next.producer?.version} with ${next.compiler?.package} ${next.compiler?.version} on the handoff rig${next.rig?` (${next.rig})`:''} · ${next.bones.length} bone(s), animations ${next.animations.join(', ')||'none'}`:(next?.reason??reason??'RETARGET_UNAVAILABLE'),!next?.available);
|
|
182
187
|
syncApply();
|