@rig-c/review 0.10.4 → 0.11.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 +30 -5
- package/docs/DESIGN.md +5 -5
- package/docs/REVIEW_PACKAGE.md +7 -3
- package/package.json +2 -2
- package/src/setup-mesh.ts +5 -40
- package/src/weights.ts +5 -6
- package/web/idle-panel.ts +7 -8
- package/web/index.html +1 -1
- package/web/main.ts +17 -15
- package/web/mesh-editor.ts +11 -10
- package/web/physics-panel.ts +33 -10
- package/web/preview.ts +52 -0
- package/web/setup-pose.ts +17 -16
- package/web/weights-view.ts +12 -12
package/web/mesh-editor.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mesh edit (Setup): a view-bar toggle, an editor bar of sub-modes (Modify / Create / Delete / Edges) and direct
|
|
3
3
|
* manipulation of the selected slot's actual mesh on the rig pane. The mesh is the slot's attachment in the compiled
|
|
4
|
-
* model document at the **setup pose** (#100):
|
|
5
|
-
*
|
|
4
|
+
* model document at the **setup pose** (#100): Play's public readSetupMesh coordinates, topology and hull
|
|
5
|
+
* (`web/setup-pose.ts`, shared with Setup · Weights), drawn over the stage with the stage's own camera — the
|
|
6
6
|
* stage itself stays paused at the pose it held, under the veil. Everything is a typed delta (`src/mesh-edit.ts`,
|
|
7
7
|
* `basis: "setup"`) previewed over the original triangulation; nothing is retriangulated or written into the package
|
|
8
8
|
* from here. Apply · Rebuild (#135) hands the exported document to the host, which sends it through the shared runner as
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* inspected build, and the local draft keeps the applied delta (edits over the revision are not written to it).
|
|
12
12
|
*/
|
|
13
13
|
import type {CropWorldMapping} from '../src/chest-regions.ts';
|
|
14
|
-
import {
|
|
14
|
+
import {readStoredSetupMesh,setupModel} from '../src/setup-mesh.ts';
|
|
15
15
|
import {
|
|
16
16
|
MESH_EDIT_CODES,MESH_EDIT_NO_MESH,MESH_EDIT_PREVIEW_NOTE,MESH_EDIT_RIG_UNAVAILABLE,MESH_EDIT_SELECT_SLOT,MeshEditSession,affineOf,applyAffine,applyMeshOps,attachmentHashText,draftOps,hitMesh,
|
|
17
17
|
invertAffine,meshEditDocument,parseMeshDraft,screenDeltaToCrop,verticesInBox,writeMeshDraft,type MeshEditDocument,type MeshEditResult,type MeshOp,type MeshSource,
|
|
@@ -56,7 +56,7 @@ export function cropFromWorld(mapping:Extract<CropWorldMapping,{available:true}>
|
|
|
56
56
|
type SourceResult={ok:true;source:Omit<MeshSource,'hash'>;entry:unknown;world:number[]}|{ok:false;code:string};
|
|
57
57
|
/**
|
|
58
58
|
* The selected slot's mesh in setup space: its attachment's entry in the hash-verified compiled model document (the
|
|
59
|
-
* attachment the setup
|
|
59
|
+
* attachment the public setup identity resolves), its public readSetupMesh vertices (`world`, flat x, y)
|
|
60
60
|
* and mapped to crop px. Refuses, in order: no model document (`MESH_EDIT_MODEL_UNAVAILABLE`), whole rig, no setup pose,
|
|
61
61
|
* a non-mesh or linked attachment, no crop mapping.
|
|
62
62
|
*/
|
|
@@ -66,17 +66,18 @@ export function meshEditSource(input:{slot:string|null;model:unknown;pose:(slot:
|
|
|
66
66
|
let pose:SetupPose|null;
|
|
67
67
|
try{pose=input.pose(slot);}catch(error){return {ok:false,code:`${MESH_EDIT_RIG_UNAVAILABLE}: ${error instanceof Error?error.message:String(error)}`};}
|
|
68
68
|
if(!pose)return {ok:false,code:`${MESH_EDIT_RIG_UNAVAILABLE}: no rig loaded`};
|
|
69
|
-
const read=
|
|
69
|
+
const read=readStoredSetupMesh(input.model,slot,pose.identity,MESH_EDIT_CODES);if(!read.ok)return !pose.identity&&pose.reason?{ok:false,code:`${read.code} (${pose.reason})`}:read;
|
|
70
70
|
const mesh=read.mesh,noMesh=(why:string)=>({ok:false as const,code:`${MESH_EDIT_NO_MESH} ${slot}: ${why}`});
|
|
71
71
|
if(mesh.linked)return noMesh(`${mesh.skin} / ${mesh.attachment} is a linked mesh of ${mesh.source.slot} / ${mesh.source.attachment}; edit its source mesh`);
|
|
72
|
-
if(mesh.hull===null)return noMesh('the model entry stores no hull');
|
|
73
72
|
if(!input.mapping.available)return {ok:false,code:`${input.mapping.reason}: mesh edit needs the crop mapping`};
|
|
74
|
-
let
|
|
75
|
-
try{
|
|
76
|
-
if(!
|
|
73
|
+
let geometry:ReturnType<SetupPose['readMesh']>;
|
|
74
|
+
try{geometry=pose.readMesh();}catch(error){return {ok:false,code:`${MESH_EDIT_RIG_UNAVAILABLE}: ${error instanceof Error?error.message:String(error)}`};}
|
|
75
|
+
if(!geometry||geometry.slot!==slot||geometry.skin!==mesh.skin||geometry.attachment!==mesh.attachment||geometry.vertices.length!==mesh.vertices.length*2)return {ok:false,code:`MESH_EDIT_SOURCE_MISMATCH: ${slot} setup geometry does not match its stored identity or vertex count`};
|
|
76
|
+
if(geometry.hull===null)return noMesh('the model entry stores no hull');
|
|
77
|
+
const world=geometry.vertices;
|
|
77
78
|
const toCrop=cropFromWorld(input.mapping);if(!toCrop)return {ok:false,code:'MESH_EDIT_MAPPING_SINGULAR'};
|
|
78
79
|
const vertices:Vec[]=[];for(let i=0;i<world.length;i+=2)vertices.push(toCrop(world[i]!,world[i+1]!));
|
|
79
|
-
return {ok:true,source:{slot,skin:mesh.skin,attachment:mesh.attachment,vertices,triangles:
|
|
80
|
+
return {ok:true,source:{slot,skin:mesh.skin,attachment:mesh.attachment,vertices,triangles:geometry.triangles,hull:geometry.hull},entry:mesh.entry,world};
|
|
80
81
|
}
|
|
81
82
|
/** Apply · Rebuild with an empty delta: nothing is sent. */
|
|
82
83
|
export const MESH_EDIT_NOTHING_PENDING='MESH_EDIT_NOTHING_PENDING: the delta holds no op; nothing is sent';
|
package/web/physics-panel.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {applyRamp,componentValue,draftEdits,draftText,nearestChain,patchOps,physicsChains,rowControls,setEdit,TUNING_FIELDS,COMPONENT_FIELDS,type PendingEdit,type PhysicsChain,type PhysicsField,type TuningRow,type TuningTable} from '../src/physics-tuning.ts';
|
|
8
8
|
import type {PatchOp} from '../src/config-patch.ts';
|
|
9
|
+
import type {PhysicsPreviewRead} from './preview.ts';
|
|
9
10
|
|
|
10
11
|
export interface PhysicsPanelDeps {
|
|
11
12
|
apply:(ops:PatchOp[])=>Promise<void>;
|
|
@@ -15,6 +16,9 @@ export interface PhysicsPanelDeps {
|
|
|
15
16
|
pane:HTMLElement;
|
|
16
17
|
/** The draft record changed (the panel already wrote `#physics-pending`). */
|
|
17
18
|
saved:()=>void;
|
|
19
|
+
/** Pending six-field overrides only; the caller uses Play's public live API. */
|
|
20
|
+
preview:(edits:readonly PendingEdit[])=>PhysicsPreviewRead[];
|
|
21
|
+
previewRead:()=>PhysicsPreviewRead[];
|
|
18
22
|
}
|
|
19
23
|
export interface PhysicsPanel {
|
|
20
24
|
load(table:TuningTable,bones:readonly {name:string;parent?:string|null}[]):void;
|
|
@@ -23,6 +27,7 @@ export interface PhysicsPanel {
|
|
|
23
27
|
ramp(chainId:string,field:PhysicsField,root:number,tip:number):void;
|
|
24
28
|
select(name:string):void;
|
|
25
29
|
restore():void;clear():void;
|
|
30
|
+
clearPreview():void;refreshPreview():void;
|
|
26
31
|
applied(ops:readonly PatchOp[]|null):void;
|
|
27
32
|
status(text:string,error:boolean):void;
|
|
28
33
|
readout(lines:{name:string;before:string;after:string}[]):void;
|
|
@@ -38,15 +43,31 @@ export function bindPhysicsPanel(deps:PhysicsPanelDeps):PhysicsPanel {
|
|
|
38
43
|
const state=node('physics-panel-state'),rowsArea=node('physics-rows'),editor=node('physics-row-editor'),locked=node('physics-locked'),pending=node('physics-pending-list');
|
|
39
44
|
const chainSelect=node<HTMLSelectElement>('physics-chain'),pick=node<HTMLButtonElement>('physics-chain-pick'),rampField=node<HTMLSelectElement>('physics-ramp-field'),rampRoot=node<HTMLInputElement>('physics-ramp-root'),rampTip=node<HTMLInputElement>('physics-ramp-tip'),rampButton=node<HTMLButtonElement>('physics-ramp');
|
|
40
45
|
const applyButton=node<HTMLButtonElement>('physics-apply'),resetButton=node<HTMLButtonElement>('physics-reset-build'),status=node('physics-apply-status'),readoutArea=node('physics-readout'),draft=node<HTMLTextAreaElement>('physics-pending');
|
|
46
|
+
const previewBox=document.querySelector<HTMLInputElement>('#physics-panel [data-physics-preview]')!,previewClear=node<HTMLButtonElement>('physics-preview-clear'),previewLabel=node('physics-preview-label');
|
|
41
47
|
let table:TuningRow[]=[],chains:PhysicsChain[]=[],edits=new Map<string,PendingEdit>(),selected:string|null=null,appliedOps:string[]=[],busy=false,picking=false;
|
|
42
48
|
rampField.replaceChildren(...[...TUNING_FIELDS,...COMPONENT_FIELDS].map(f=>make('option',{value:f,textContent:f})));
|
|
43
49
|
rampField.value='damping';
|
|
44
50
|
|
|
45
51
|
const save=()=>{draft.value=draftText(edits);draft.dispatchEvent(new Event('input',{bubbles:true}));deps.saved();};
|
|
46
|
-
|
|
52
|
+
let previewActive=false;
|
|
53
|
+
const change=(next:Map<string,PendingEdit>,live=false)=>{edits=next;save();drivePreview();if(live){renderRows();renderPending();}else render();};
|
|
47
54
|
const tryChange=(make:()=>Map<string,PendingEdit>)=>{try{change(make());}catch(error){panelStatus(error instanceof Error?error.message:String(error),true);}};
|
|
48
55
|
function panelStatus(text:string,error:boolean){status.textContent=text;status.title=text;status.classList.toggle('error',error);}
|
|
49
56
|
const row=(name:string)=>table.find(r=>r.name===name&&r.configIndex!==null)??table.find(r=>r.name===name);
|
|
57
|
+
function showPreview(reads:PhysicsPreviewRead[]){
|
|
58
|
+
const message=reads.length?`preview — Apply decides · ${reads.map(({name,read})=>`${name}: pending ${JSON.stringify(read.override)} · last evaluated ${JSON.stringify(read.effective)}`).join(' · ')}`:'Preview off · mass and x/y/rotate require Apply. Clear resumes authored values on the next evaluation; integrated history is kept.';
|
|
59
|
+
if(message!==previewLabel.textContent){previewLabel.textContent=message;previewLabel.title=message;}
|
|
60
|
+
previewLabel.classList.remove('error');
|
|
61
|
+
}
|
|
62
|
+
function drivePreview(){
|
|
63
|
+
try{
|
|
64
|
+
const reads=deps.preview(previewBox.checked&&!busy?[...edits.values()].filter(e=>!appliedOps.includes(JSON.stringify({op:e.op,path:e.path,value:e.value,name:e.name}))):[]);
|
|
65
|
+
previewActive=reads.length>0;showPreview(reads);
|
|
66
|
+
}catch(error){previewActive=false;previewLabel.textContent=error instanceof Error?error.message:String(error);previewLabel.title=previewLabel.textContent;previewLabel.classList.add('error');}
|
|
67
|
+
}
|
|
68
|
+
function clearPreview(){deps.preview([]);previewActive=false;showPreview([]);}
|
|
69
|
+
previewBox.onchange=()=>drivePreview();
|
|
70
|
+
previewClear.onclick=()=>{previewBox.checked=false;clearPreview();};
|
|
50
71
|
|
|
51
72
|
function renderRows(){
|
|
52
73
|
const editable=table.filter(r=>r.editable);
|
|
@@ -77,8 +98,8 @@ export function bindPhysicsPanel(deps:PhysicsPanelDeps):PhysicsPanel {
|
|
|
77
98
|
const label=`${r.name} ${control.field}`;
|
|
78
99
|
const range=make('input',{type:'range',min:String(control.min),max:String(control.max),step:String(control.step),value:String(control.value),disabled:busy});range.setAttribute('aria-label',label);range.dataset.field=control.field;
|
|
79
100
|
const number=make('input',{type:'number',step:'any',value:String(control.value),disabled:busy,inputMode:'decimal'});number.setAttribute('aria-label',`${label} value`);number.dataset.field=control.field;
|
|
80
|
-
const commit=(raw:string)=>{const value=Number(raw);if(raw.trim()===''||!Number.isFinite(value)){panelStatus(`PHYSICS_VALUE_INVALID: ${r.name}.${control.field} must be a finite number`,true);return;}
|
|
81
|
-
range.onchange=()=>commit(range.value);range.oninput=()=>{number.value=range.value;};number.onchange=()=>commit(number.value);
|
|
101
|
+
const commit=(raw:string,live=false)=>{const value=Number(raw);if(raw.trim()===''||!Number.isFinite(value)){panelStatus(`PHYSICS_VALUE_INVALID: ${r.name}.${control.field} must be a finite number`,true);return;}try{change(setEdit(edits,r,control.field,value),live);}catch(error){panelStatus(error instanceof Error?error.message:String(error),true);}};
|
|
102
|
+
range.onchange=()=>commit(range.value);range.oninput=()=>{number.value=range.value;commit(range.value,true);};number.onchange=()=>commit(number.value);
|
|
82
103
|
list.append(make('label',{className:control.pending?'physics-control pending':'physics-control'},make('span',{textContent:control.field}),range,number,make('output',{textContent:control.pending?`was ${shown(control.base)}`:''})));
|
|
83
104
|
}else{
|
|
84
105
|
const box=make('input',{type:'checkbox',checked:control.on,disabled:busy});box.setAttribute('aria-label',`${r.name} ${control.field} component`);box.dataset.field=control.field;
|
|
@@ -105,7 +126,7 @@ export function bindPhysicsPanel(deps:PhysicsPanelDeps):PhysicsPanel {
|
|
|
105
126
|
if(chains.some(c=>c.id===keep))chainSelect.value=keep;
|
|
106
127
|
for(const control of [chainSelect,rampField,rampRoot,rampTip,rampButton,pick])control.disabled=busy||!chains.length;
|
|
107
128
|
}
|
|
108
|
-
function render(){renderRows();renderEditor();renderPending();renderChains();}
|
|
129
|
+
function render(){renderRows();renderEditor();renderPending();renderChains();previewBox.disabled=busy;previewClear.disabled=busy;}
|
|
109
130
|
|
|
110
131
|
chainSelect.onchange=()=>{const chain=chains.find(c=>c.id===chainSelect.value);if(chain){selected=chain.links[0]!.name;seedRamp(chain);render();}};
|
|
111
132
|
rampField.onchange=()=>{const chain=chains.find(c=>c.id===chainSelect.value);if(chain)seedRamp(chain);};
|
|
@@ -123,29 +144,31 @@ export function bindPhysicsPanel(deps:PhysicsPanelDeps):PhysicsPanel {
|
|
|
123
144
|
chainSelect.value=chain.id;chainSelect.dispatchEvent(new Event('change'));panelStatus(`Picked ${chain.label}.`,false);
|
|
124
145
|
};
|
|
125
146
|
deps.pane.addEventListener('pointerdown',onPick,{capture:true});
|
|
126
|
-
applyButton.onclick=()=>{if(!edits.size||busy)return;void deps.apply(patchOps(edits));};
|
|
147
|
+
applyButton.onclick=()=>{if(!edits.size||busy)return;clearPreview();void deps.apply(patchOps(edits));};
|
|
127
148
|
resetButton.onclick=()=>{if(busy)return;void deps.reset();};
|
|
128
149
|
|
|
129
150
|
const panel:PhysicsPanel={
|
|
130
151
|
load(value,bones){
|
|
131
|
-
if(!value.available){table=[];chains=[];state.textContent=value.reason;state.classList.add('error');render();return;}
|
|
152
|
+
if(!value.available){clearPreview();table=[];chains=[];edits=new Map();state.textContent=value.reason;state.classList.add('error');render();return;}
|
|
132
153
|
table=value.rows;chains=physicsChains(table,bones);
|
|
133
154
|
const editable=table.filter(r=>r.editable).length;
|
|
134
155
|
state.classList.remove('error');state.textContent=`${editable} physics constraints editable from the handoff config · ${table.length-editable} not editable (reasons below) · ${chains.length} chains from bone parentage`;
|
|
135
|
-
edits=draftEdits(draft.value,table);render();
|
|
156
|
+
edits=draftEdits(draft.value,table);if(previewActive)drivePreview();render();
|
|
136
157
|
},
|
|
137
158
|
rows:()=>table.map(r=>({...r,values:{...r.values}})),chains:()=>chains.map(c=>({...c,links:[...c.links]})),edits:()=>[...edits.values()].map(e=>({...e})),
|
|
138
159
|
set(name,field,value){const r=row(name);if(!r)throw new Error(`PHYSICS_ROW_UNKNOWN: ${name}`);change(setEdit(edits,r,field,value));},
|
|
139
160
|
ramp(chainId,field,root,tip){const chain=chains.find(c=>c.id===chainId);if(!chain)throw new Error(`PHYSICS_CHAIN_UNKNOWN: ${chainId}`);change(applyRamp(edits,chain,field,root,tip));},
|
|
140
161
|
select(name){selected=name;chainSelect.value='';render();},
|
|
141
|
-
restore(){edits=draftEdits(draft.value,table);render();},
|
|
162
|
+
restore(){clearPreview();edits=draftEdits(draft.value,table);render();},
|
|
142
163
|
clear(){change(new Map());},
|
|
164
|
+
clearPreview,
|
|
165
|
+
refreshPreview(){if(previewActive)try{showPreview(deps.previewRead());}catch(error){previewLabel.textContent=error instanceof Error?error.message:String(error);previewLabel.classList.add('error');}},
|
|
143
166
|
applied(ops){appliedOps=(ops??[]).map(o=>JSON.stringify({op:o.op,path:o.path,value:o.value,name:o.name}));renderPending();},
|
|
144
167
|
status:panelStatus,
|
|
145
168
|
readout(lines){readoutArea.replaceChildren(...lines.flatMap(l=>[make('div',{className:'physics-readout-line'},make('span',{className:'eyebrow',textContent:'BEFORE'}),make('code',{textContent:l.before,title:l.before})),make('div',{className:'physics-readout-line'},make('span',{className:'eyebrow',textContent:'AFTER'}),make('code',{textContent:l.after,title:l.after}))]));},
|
|
146
169
|
busy(value){busy=value;render();},
|
|
147
|
-
dispose(){deps.pane.removeEventListener('pointerdown',onPick,{capture:true});},
|
|
170
|
+
dispose(){clearPreview();deps.pane.removeEventListener('pointerdown',onPick,{capture:true});},
|
|
148
171
|
};
|
|
149
|
-
render();
|
|
172
|
+
showPreview([]);render();
|
|
150
173
|
return panel;
|
|
151
174
|
}
|
package/web/preview.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Review's preview adapters. All posing, parameter validation and history belong to published Play. */
|
|
2
|
+
import type {BoneAdjust,PhysicsParameterPatch,PhysicsParameterRead,RigPlayer} from '@rig-c/play';
|
|
3
|
+
import {previewLocal,SETUP_DEFAULT,type LocalChannel,type PreviewPlan} from '../src/idle-tracks.ts';
|
|
4
|
+
import type {PendingEdit} from '../src/physics-tuning.ts';
|
|
5
|
+
|
|
6
|
+
export interface BoneSetup {names:readonly string[];get(bone:string):Partial<Record<LocalChannel,number>>|undefined}
|
|
7
|
+
|
|
8
|
+
/** Preserve the core's evaluation context through the idle + turn composition. */
|
|
9
|
+
export function composeAdjust(a:BoneAdjust|null,b:BoneAdjust|null):BoneAdjust|null {
|
|
10
|
+
return a&&b?(bones,ctx)=>{a(bones,ctx);b(bones,ctx);}:a??b;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Uses the existing idle preview conversion; no player clock or seek compensation. */
|
|
14
|
+
export function idlePreviewAdjust(plan:PreviewPlan,setup:BoneSetup):BoneAdjust|null {
|
|
15
|
+
const writes=plan.writes.map(w=>({w,setup:setup.get(w.bone)?.[w.channel]??SETUP_DEFAULT[w.channel]}));
|
|
16
|
+
return writes.length?(bones,ctx)=>{
|
|
17
|
+
if(ctx.trackTime===null)return;
|
|
18
|
+
for(const {w,setup:s} of writes)bones.bone(w.bone)[w.channel]=previewLocal(w,s,ctx.trackTime);
|
|
19
|
+
}:null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const LIVE_PHYSICS_FIELDS=['strength','damping','inertia','wind','gravity','mix'] as const;
|
|
23
|
+
type PhysicsPlayer=Pick<RigPlayer,'setPhysicsParameters'|'readPhysicsParameters'|'clearPhysicsParameters'>;
|
|
24
|
+
export interface PhysicsPreviewRead {name:string;read:PhysicsParameterRead}
|
|
25
|
+
|
|
26
|
+
/** Owns only this panel's pending overrides. Set/read/clear never evaluate, seek or reset physics. */
|
|
27
|
+
export class PhysicsPreview {
|
|
28
|
+
private player:PhysicsPlayer|null=null;
|
|
29
|
+
private patches=new Map<string,PhysicsParameterPatch>();
|
|
30
|
+
sync(player:PhysicsPlayer|null,edits:readonly PendingEdit[]):PhysicsPreviewRead[] {
|
|
31
|
+
const next=new Map<string,PhysicsParameterPatch>();
|
|
32
|
+
for(const e of edits)if((LIVE_PHYSICS_FIELDS as readonly string[]).includes(e.field)){
|
|
33
|
+
next.set(e.name,{...next.get(e.name),[e.field]:e.value});
|
|
34
|
+
}
|
|
35
|
+
if(this.player!==player){this.clear();this.player=player;}
|
|
36
|
+
if(!player){if(next.size)throw new Error('PHYSICS_PREVIEW_UNAVAILABLE: no live player');return [];}
|
|
37
|
+
for(const [name,patch] of this.patches){
|
|
38
|
+
const wanted=next.get(name);
|
|
39
|
+
// Play merges patches: removing a field must clear the old patch before setting the remaining fields.
|
|
40
|
+
if(!wanted||Object.keys(patch).some(k=>!(k in wanted))){player.clearPhysicsParameters(name);this.patches.delete(name);}
|
|
41
|
+
}
|
|
42
|
+
for(const [name,patch] of next)if(JSON.stringify(this.patches.get(name))!==JSON.stringify(patch)){
|
|
43
|
+
player.setPhysicsParameters(name,patch);this.patches.set(name,patch);
|
|
44
|
+
}
|
|
45
|
+
return this.read();
|
|
46
|
+
}
|
|
47
|
+
read():PhysicsPreviewRead[] {return this.player?[...this.patches.keys()].map(name=>({name,read:this.player!.readPhysicsParameters(name)})):[];}
|
|
48
|
+
clear():void {
|
|
49
|
+
for(const name of this.patches.keys())this.player?.clearPhysicsParameters(name);
|
|
50
|
+
this.patches.clear();this.player=null;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/web/setup-pose.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* before this returns, so every bone the caller may ask for is read here.
|
|
2
|
+
* Setup identity through the published player, followed by named geometry through Play's public readSetupMesh.
|
|
3
|
+
* The short-lived player reads identity only: setup sliders can select an attachment different from slot.setup.
|
|
4
|
+
* It never plays or reads bone matrices, and never touches the stage player.
|
|
6
5
|
*/
|
|
7
|
-
import {RigPlayer,type ModelAssets,type RenderMode} from '@rig-c/play';
|
|
8
|
-
import type {WorldTransform} from '../src/setup-mesh.ts';
|
|
6
|
+
import {RigPlayer,readSetupMesh,type ModelAssets,type RenderMode,type SetupMesh,type SlotIdentityReason} from '@rig-c/play';
|
|
9
7
|
|
|
10
8
|
export interface SetupPose {
|
|
11
|
-
/**
|
|
9
|
+
/** Exact supplying table/key, or the public null identity (never guessed from declarations). */
|
|
12
10
|
identity:{skin:string;attachment:string}|null;
|
|
13
|
-
/**
|
|
14
|
-
|
|
11
|
+
/** Active skin selection, distinct from the supplying table in identity.skin. Null means a fresh no-skin player. */
|
|
12
|
+
skin:string|null;
|
|
13
|
+
reason:SlotIdentityReason|null;
|
|
14
|
+
/** Detached geometry from the public API; lazy so callers can name non-mesh/linked refusals first. */
|
|
15
|
+
readMesh:()=>SetupMesh|null;
|
|
15
16
|
}
|
|
16
|
-
/** Reads
|
|
17
|
-
export function readSetupPose(assets:ModelAssets,slot:string,
|
|
18
|
-
const player=new RigPlayer(root,assets,{mode,slots:[slot]});
|
|
17
|
+
/** Reads setup identity without advancing time. Throws public setup/identity/geometry refusals unchanged. */
|
|
18
|
+
export function readSetupPose(assets:ModelAssets,slot:string,root:HTMLElement,mode:RenderMode='dom',skin?:string):SetupPose {
|
|
19
|
+
const player=new RigPlayer(root,assets,{mode,slots:[slot],...(skin===undefined?{}:{skin})});
|
|
19
20
|
try{
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const snapshot=player.slotIdentity(),row=snapshot.slots.find(r=>r.slot===slot);
|
|
22
|
+
const identity=row&&row.attachment!==null&&row.skin!==null?{skin:row.skin,attachment:row.attachment}:null;
|
|
23
|
+
return {identity,skin:snapshot.skin,reason:row?.reason??null,
|
|
24
|
+
readMesh:()=>identity?readSetupMesh(assets,{slot,attachment:identity.attachment,skin:snapshot.skin}):null};
|
|
24
25
|
}finally{player.dispose();}
|
|
25
26
|
}
|
|
26
27
|
/** The bone names a compiled model document declares, in stored order. */
|
package/web/weights-view.ts
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* coloured by that bone's stored weight on the theme's ramp (0 → 1), or switch to max influence and every vertex takes
|
|
4
4
|
* its dominant bone's colour; hovering a vertex reads its stored value. Weights come from the compiled model document
|
|
5
5
|
* (`src/weights.ts` over the shared reader `src/setup-mesh.ts`); vertex positions come from the published player's
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* readSetupMesh API (`web/setup-pose.ts`, shared with Edit Mesh). A never-played player reads setup identity only.
|
|
7
|
+
* Nothing is edited, applied, recorded or sent.
|
|
8
8
|
*/
|
|
9
|
-
import type {ModelAssets,RenderMode} from '@rig-c/play';
|
|
10
|
-
import {placeSetupMesh} from '../src/setup-mesh.ts';
|
|
9
|
+
import type {ModelAssets,RenderMode,SetupMesh as PlaySetupMesh} from '@rig-c/play';
|
|
11
10
|
import {WEIGHTS_SELECT_SLOT,dominantInfluence,meanColor,meshWeights,rampColor,weightOf,weightText,weightsModel,type MeshWeights} from '../src/weights.ts';
|
|
12
11
|
import {modelBoneNames,readSetupPose} from './setup-pose.ts';
|
|
13
12
|
import type {OverlayPoint,OverlayProject,OverlayWeights} from './rig-overlays.ts';
|
|
@@ -20,19 +19,20 @@ export const WEIGHTS_SOURCE_MISMATCH='WEIGHTS_SOURCE_MISMATCH';
|
|
|
20
19
|
export const WEIGHTS_RIG_UNAVAILABLE='WEIGHTS_RIG_UNAVAILABLE';
|
|
21
20
|
|
|
22
21
|
/** The selected slot's mesh at the setup pose: world vertices (one per stored vertex), triangles and identity. */
|
|
23
|
-
export interface SetupMesh {
|
|
22
|
+
export interface SetupMesh extends PlaySetupMesh {identity:{skin:string;attachment:string}|null}
|
|
24
23
|
export type SetupMeshResult={ok:true;mesh:SetupMesh;weights:MeshWeights}|{ok:false;code:string};
|
|
25
24
|
/**
|
|
26
|
-
* The
|
|
27
|
-
* the
|
|
25
|
+
* The public setup identity names the target. Play owns its setup coordinates, topology, hull and deform.
|
|
26
|
+
* The active skin selection is passed independently of the identity's supplying table; the stage is not touched.
|
|
28
27
|
*/
|
|
29
|
-
export function setupPoseMesh(assets:ModelAssets,model:unknown,slot:string,root:HTMLElement,mode:RenderMode='dom'):SetupMeshResult {
|
|
28
|
+
export function setupPoseMesh(assets:ModelAssets,model:unknown,slot:string,root:HTMLElement,mode:RenderMode='dom',skin?:string):SetupMeshResult {
|
|
30
29
|
const available=weightsModel(model);if(!available.ok)return available;
|
|
31
30
|
try{
|
|
32
|
-
const pose=readSetupPose(assets,slot,
|
|
33
|
-
const result=meshWeights(model,slot,pose.identity);if(!result.ok)return result;
|
|
34
|
-
const
|
|
35
|
-
|
|
31
|
+
const pose=readSetupPose(assets,slot,root,mode,skin);
|
|
32
|
+
const result=meshWeights(model,slot,pose.identity);if(!result.ok)return !pose.identity&&pose.reason?{ok:false,code:`${result.code} (${pose.reason})`}:result;
|
|
33
|
+
const mesh=pose.readMesh();
|
|
34
|
+
if(!mesh||mesh.slot!==slot||mesh.skin!==result.weights.skin||mesh.attachment!==result.weights.attachment||mesh.vertices.length!==result.weights.vertices.length*2)return {ok:false,code:`${WEIGHTS_SOURCE_MISMATCH}: ${slot} setup geometry does not match its stored identity or vertex count`};
|
|
35
|
+
return {ok:true,weights:result.weights,mesh:{...mesh,identity:pose.identity}};
|
|
36
36
|
}catch(error){return {ok:false,code:`${WEIGHTS_RIG_UNAVAILABLE}: ${error instanceof Error?error.message:String(error)}`};}
|
|
37
37
|
}
|
|
38
38
|
|