@rig-c/review 0.9.0 → 0.10.1
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 +188 -36
- 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/cloth-panel.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import {ClothFieldError,fieldText,recipeFromFields,recipeOps,RECIPE_FIELDS,type ClothTagView,type RecipeField} from '../src/cloth-recipe.ts';
|
|
14
14
|
import type {PatchOp} from '../src/config-patch.ts';
|
|
15
|
+
import {itemApplied} from '../src/applied-blocks.ts';
|
|
15
16
|
import type {CropWorldMapping} from '../src/chest-regions.ts';
|
|
16
17
|
import type {OverlayPoint,OverlayProject,OverlayRegion} from './rig-overlays.ts';
|
|
17
18
|
import type {ClothStatusView,PatchJobView,RebuildRefusal} from './rebuild-client.ts';
|
|
@@ -33,6 +34,10 @@ export interface ClothPanel {
|
|
|
33
34
|
links():{tag:string;inspected:ClothTagView|null;drafted:ClothTagView|null};
|
|
34
35
|
apply():Promise<PatchJobView|RebuildRefusal|null>;
|
|
35
36
|
shown(job:PatchJobView|RebuildRefusal|null):void;
|
|
37
|
+
/** #172: the ops of the stage's applied record this block owns (`appliedBlockOps`); a pending op among them is marked applied. */
|
|
38
|
+
applied(ops:readonly PatchOp[]|null):void;
|
|
39
|
+
/** #172: the pending ops marked applied, by their op line. */
|
|
40
|
+
appliedItems():string[];
|
|
36
41
|
status(text:string,error:boolean):void;busy(value:boolean):void;
|
|
37
42
|
restore():void;clear():void;sync():void;dispose():void;
|
|
38
43
|
}
|
|
@@ -43,7 +48,7 @@ const isObject=(v:unknown):v is Record<string,unknown>=>!!v&&typeof v==='object'
|
|
|
43
48
|
export function bindClothPanel(deps:ClothPanelDeps):ClothPanel {
|
|
44
49
|
const state=node('cloth-panel-state'),toggle=node<HTMLButtonElement>('cloth-links'),picker=node<HTMLSelectElement>('cloth-tag'),declared=node<HTMLInputElement>('cloth-declared'),fields=node('cloth-fields'),pending=node('cloth-pending-list');
|
|
45
50
|
const applyButton=node<HTMLButtonElement>('cloth-apply'),resetButton=node<HTMLButtonElement>('cloth-reset'),statusOut=node('cloth-status'),readout=node('cloth-readout'),draft=node<HTMLTextAreaElement>('cloth-pending');
|
|
46
|
-
let status:ClothStatusView|null=null,reason:string|null=null,selected='',active=false,busy=false;
|
|
51
|
+
let status:ClothStatusView|null=null,reason:string|null=null,selected='',active=false,busy=false,appliedOps:PatchOp[]=[];
|
|
47
52
|
/** Per touched tag: every form field's text, and whether the tag's recipe is declared. */
|
|
48
53
|
let edits=new Map<string,{declared:boolean;values:Record<string,string>}>();
|
|
49
54
|
let drafted:ClothTagView[]=[];
|
|
@@ -78,7 +83,8 @@ export function bindClothPanel(deps:ClothPanelDeps):ClothPanel {
|
|
|
78
83
|
function renderPending(){
|
|
79
84
|
let ops:PatchOp[]=[],error:string|null=null;
|
|
80
85
|
try{ops=allOps();}catch(err){if(err instanceof ClothFieldError)error=err.message;else throw err;}
|
|
81
|
-
|
|
86
|
+
// #172: an op is marked applied only while the stage's applied record holds it.
|
|
87
|
+
const items=ops.map(o=>{const on=itemApplied([o],appliedOps);return make('li',{className:on?'applied':''},make('code',{textContent:`${o.op} ${o.path}`}),`${o.op==='remove'?'':` → ${JSON.stringify(o.value)}`}${on?' · applied':''}`);});
|
|
82
88
|
const revert=[...edits.keys()].filter(touched).map(tag=>{const b=make('button',{type:'button',textContent:`Revert ${tag}`,disabled:busy});b.onclick=()=>{edits.delete(tag);change();};return make('li',{},b);});
|
|
83
89
|
pending.replaceChildren(...(error?[make('li',{className:'error',textContent:error})]:[]),...(items.length?items:[make('li',{className:'hint',textContent:'No pending ops.'})]),...revert);
|
|
84
90
|
applyButton.disabled=busy||!items.length||!!error||!!reason||!!status?.reason;resetButton.disabled=busy;
|
|
@@ -156,6 +162,8 @@ export function bindClothPanel(deps:ClothPanelDeps):ClothPanel {
|
|
|
156
162
|
drafted=job.status==='built'&&c?c.links:[];
|
|
157
163
|
readout.replaceChildren(...lines);deps.changed();
|
|
158
164
|
},
|
|
165
|
+
applied(ops){appliedOps=(ops??[]).map(o=>({...o}));renderPending();},
|
|
166
|
+
appliedItems(){return [...pending.querySelectorAll('li.applied code')].map(c=>c.textContent??'');},
|
|
159
167
|
status:panelStatus,
|
|
160
168
|
busy(value){busy=value;render();},
|
|
161
169
|
restore(){restore();render();deps.changed();},
|
package/web/cuts-panel.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import {anchorAt,boneAnchors,configBones,setAnchor,type BoneAnchor,type BoneEdit} from '../src/bone-anchors.ts';
|
|
13
13
|
import type {PatchOp} from '../src/config-patch.ts';
|
|
14
|
+
import {itemApplied} from '../src/applied-blocks.ts';
|
|
14
15
|
import type {CropWorldMapping} from '../src/chest-regions.ts';
|
|
15
16
|
import {
|
|
16
17
|
boxAt,CutEditError,cutOps,cutsDraft,cutsDraftText,cutsSource,dragPoint,rangeArc,rangeAt,setBox,setRange,setVertex,vertexAt,
|
|
@@ -42,6 +43,10 @@ export interface CutsPanel {
|
|
|
42
43
|
overlayRegions():OverlayRegion[];overlayMarkers():OverlayMarker[];
|
|
43
44
|
apply():Promise<PatchJobView|RebuildRefusal|null>;
|
|
44
45
|
shown(job:PatchJobView|RebuildRefusal|null):void;
|
|
46
|
+
/** #172: the ops of the stage's applied record this block owns (`appliedBlockOps`); a pending op among them is marked applied. */
|
|
47
|
+
applied(ops:readonly PatchOp[]|null):void;
|
|
48
|
+
/** #172: the pending items marked applied, by their op line. */
|
|
49
|
+
appliedItems():string[];
|
|
45
50
|
status(text:string,error:boolean):void;busy(value:boolean):void;
|
|
46
51
|
restore():void;clear():void;sync():void;dispose():void;
|
|
47
52
|
}
|
|
@@ -62,7 +67,7 @@ export function bindCutsPanel(deps:CutsPanelDeps):CutsPanel {
|
|
|
62
67
|
const addForm=node('cuts-add');
|
|
63
68
|
let config:unknown=null,source:CutsSource={cuts:[],patches:[],segments:[],plan:[],skipped:[]},anchors:BoneAnchor[]=[],buildReason:string|null=null,loaded=false;
|
|
64
69
|
let edits=new Map<string,CutEdit>(),joints=new Map<string,BoneEdit>(),adds:CutAdd[]=[];
|
|
65
|
-
let selected='',active=false,busy=false,space=false,hover:string|null=null;
|
|
70
|
+
let selected='',active=false,busy=false,space=false,hover:string|null=null,appliedOps:PatchOp[]=[];
|
|
66
71
|
let drag:{id:string;pointer:number;start:RigPoint;from:RigPoint}|null=null;
|
|
67
72
|
let placing:{kind:'cut'|'patch';points:RigPoint[]}|null=null;
|
|
68
73
|
|
|
@@ -121,12 +126,16 @@ export function bindCutsPanel(deps:CutsPanelDeps):CutsPanel {
|
|
|
121
126
|
}
|
|
122
127
|
function renderPending(){
|
|
123
128
|
const items:HTMLElement[]=[];
|
|
129
|
+
// #172: an item is marked applied only while the stage's applied record holds every op it sends.
|
|
130
|
+
const opsOf=(build:()=>PatchOp[])=>{try{return build();}catch(error){if(error instanceof CutEditError)return [];throw error;}};
|
|
131
|
+
const item=(sent:PatchOp[],code:HTMLElement,text:string,revert:HTMLElement)=>{const on=itemApplied(sent,appliedOps);return make('li',{className:on?'applied':''},code,text+(on?' · applied':''),revert);};
|
|
124
132
|
for(const e of edits.values()){const revert=make('button',{type:'button',textContent:'Revert',disabled:busy});revert.onclick=()=>{edits.delete(e.path);change();};
|
|
125
|
-
items.push(
|
|
133
|
+
items.push(item(opsOf(()=>cutOps(new Map([[e.path,e]]),[],config)),make('code',{textContent:`${e.kind==='range'&&e.value===null?'remove':e.kind==='range'&&e.from===null?'add':'replace'} ${e.path}`}),` ${e.from?fmt(e.from):'—'} → ${e.value?fmt(e.value):'—'} · ${e.name}`,revert));}
|
|
126
134
|
for(const e of joints.values()){const revert=make('button',{type:'button',textContent:'Revert',disabled:busy});revert.onclick=()=>{joints.delete(e.path);change();};
|
|
127
|
-
items.push(
|
|
135
|
+
items.push(item([{op:'replace',path:e.path,value:[...e.value],name:e.name}],make('code',{textContent:`replace ${e.path}`}),` ${fmt(e.from)} → ${fmt(e.value)} · ${e.name} (a joint is its bone's origin)`,revert));}
|
|
128
136
|
adds.forEach((a,i)=>{const revert=make('button',{type:'button',textContent:'Revert',disabled:busy});revert.onclick=()=>{adds.splice(i,1);change();};
|
|
129
|
-
|
|
137
|
+
const sent=opsOf(()=>{const before=cutOps(new Map(),adds.slice(0,i),config).length;return cutOps(new Map(),adds.slice(0,i+1),config).slice(before);});
|
|
138
|
+
items.push(item(sent,make('code',{textContent:`add /assemble/${a.kind==='cut'?'cuts':'patches'}/-`}),` ${a.kind==='cut'?`cut ${a.into} ← ${a.from}, ${a.polygon.length} point(s)`:`patch ${a.name} ${fmt(a.box)}`} · regions → ${a.bone}`,revert));});
|
|
130
139
|
pending.replaceChildren(...(items.length?items:[make('li',{className:'hint',textContent:'No pending ops.'})]));
|
|
131
140
|
applyButton.disabled=busy||!items.length||!!buildReason;resetButton.disabled=busy;
|
|
132
141
|
toggle.disabled=!loaded;toggle.setAttribute('aria-pressed',String(active));
|
|
@@ -254,6 +263,8 @@ export function bindCutsPanel(deps:CutsPanelDeps):CutsPanel {
|
|
|
254
263
|
const map=a?.recomposite.errorMap;
|
|
255
264
|
readout.replaceChildren(...lines,...(map&&job.status==='built'?[make('figure',{className:'cuts-error-map'},make('img',{src:deps.route(map.url),alt:'Recomposite error map of the scratch build: red is painting no part holds, blue a part more than 40 off'}),make('figcaption',{textContent:`recomposite_error_rig.png · sha256 ${map.sha256.slice(0,12)}…`}))]:[]));
|
|
256
265
|
},
|
|
266
|
+
applied(ops){appliedOps=(ops??[]).map(o=>({...o}));renderPending();},
|
|
267
|
+
appliedItems(){return [...pending.querySelectorAll('li.applied code')].map(c=>c.textContent??'');},
|
|
257
268
|
status:panelStatus,
|
|
258
269
|
busy(value){busy=value;render();},
|
|
259
270
|
restore(){restore();render();deps.changed();},
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh panel · density strip (#173), page side: "N values from A to B" next to the selected part's density field
|
|
3
|
+
* (`grid` on a lattice entry, `contour.spacing` on a contour entry), Build strip, and the table — one row per
|
|
4
|
+
* candidate with `remesh.json`'s counts, the folds count, the part's TEXTURE_STRETCH severity and worst frame, every bar
|
|
5
|
+
* the part is measured on as a ratio against its floor or ceiling, and the verdict (PASS, or the producer's refusal
|
|
6
|
+
* verbatim). The two labels name rows; the table has no Apply. Load puts a built row on the stage as pending (the
|
|
7
|
+
* Mesh panel's Apply then applies it, reusing its build); Refine between builds the next strip inside two rows.
|
|
8
|
+
*/
|
|
9
|
+
import {barText,densityField,densityPath,refineValues,stripValues,type DensityField,type StripRow} from '../src/density-strip.ts';
|
|
10
|
+
import {entryValue,type MeshEntry} from '../src/mesh-entry.ts';
|
|
11
|
+
|
|
12
|
+
export interface DensityStripViewLike {
|
|
13
|
+
id:string;status:'running'|'done';part:string;field:DensityField;concurrency:number;wallMs:number|null;rows:StripRow[];labels:{fewest:number|null;widest:number|null};
|
|
14
|
+
}
|
|
15
|
+
export interface DensityStripHost {
|
|
16
|
+
part():string|null;
|
|
17
|
+
entry():MeshEntry|null;
|
|
18
|
+
build(values:number[]):Promise<void>;
|
|
19
|
+
load(index:number):Promise<void>;
|
|
20
|
+
back():Promise<void>;
|
|
21
|
+
close():Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface DensityStripPanel {
|
|
24
|
+
/** The selected part or its pending entry changed: the field line and the default range follow it. */
|
|
25
|
+
changed():void;
|
|
26
|
+
show(view:DensityStripViewLike|null,pending:number|null,busy:boolean):void;
|
|
27
|
+
status(text:string,error:boolean):void;
|
|
28
|
+
}
|
|
29
|
+
const node=<T extends HTMLElement>(id:string)=>{const found=document.getElementById(id);if(!found)throw new Error(`Missing UI element: ${id}`);return found as T;};
|
|
30
|
+
const make=<K extends keyof HTMLElementTagNameMap>(tag:K,props:Record<string,unknown>={},...children:(Node|string)[]):HTMLElementTagNameMap[K]=>{const item=document.createElement(tag);Object.assign(item,props);item.append(...children);return item;};
|
|
31
|
+
const fieldLabel=(f:DensityField)=>f==='grid'?'grid':'contour.spacing';
|
|
32
|
+
|
|
33
|
+
export function bindDensityStrip(host:DensityStripHost):DensityStripPanel {
|
|
34
|
+
const fieldLine=node('density-strip-field'),n=node<HTMLInputElement>('density-strip-n'),from=node<HTMLInputElement>('density-strip-from'),to=node<HTMLInputElement>('density-strip-to');
|
|
35
|
+
const buildButton=node<HTMLButtonElement>('density-strip-build'),backButton=node<HTMLButtonElement>('density-strip-back'),closeButton=node<HTMLButtonElement>('density-strip-close');
|
|
36
|
+
const statusOut=node('density-strip-status'),table=node('density-strip-table');
|
|
37
|
+
let view:DensityStripViewLike|null=null,pending:number|null=null,busy=false,lastPart:string|null=null;
|
|
38
|
+
const status=(text:string,error:boolean)=>{statusOut.textContent=text;statusOut.title=text;statusOut.classList.toggle('error',error);};
|
|
39
|
+
/** The selected part's density field and its pending value, or the reason a strip cannot run on it. */
|
|
40
|
+
const current=():{part:string;field:DensityField;value:number}|{error:string}=>{
|
|
41
|
+
const part=host.part(),entry=host.entry();
|
|
42
|
+
if(!part||!entry)return {error:'Select a mesh part (grid or contour entry) to build a density strip.'};
|
|
43
|
+
try{const field=densityField(entry,part),value=entryValue(entry,densityPath(field));return typeof value==='number'?{part,field,value}:{error:`meshes.${part}.${fieldLabel(field)} is not a number`};}
|
|
44
|
+
catch(error){return {error:error instanceof Error?error.message:String(error)};}
|
|
45
|
+
};
|
|
46
|
+
function changed(){
|
|
47
|
+
const c=current();
|
|
48
|
+
if('error' in c){fieldLine.textContent=c.error;buildButton.disabled=true;lastPart=null;return;}
|
|
49
|
+
fieldLine.textContent=`${c.part} · ${fieldLabel(c.field)} (pending ${c.value}) · each value runs as its own replace /meshes/${c.part} patch through remesh, at most ${view?.concurrency??'cores/2'} at once`;
|
|
50
|
+
if(lastPart!==c.part){from.value=String(c.value);to.value=String(c.field==='grid'?Math.round(c.value*3):Math.round(c.value*2*100)/100);lastPart=c.part;}
|
|
51
|
+
buildButton.disabled=busy||view?.status==='running';
|
|
52
|
+
}
|
|
53
|
+
buildButton.onclick=()=>{
|
|
54
|
+
const c=current();if('error' in c){status(c.error,true);return;}
|
|
55
|
+
try{void host.build(stripValues(Number(from.value),Number(to.value),Number(n.value),c.field==='grid'));}catch(error){status(error instanceof Error?error.message:String(error),true);}
|
|
56
|
+
};
|
|
57
|
+
backButton.onclick=()=>{void host.back();};
|
|
58
|
+
closeButton.onclick=()=>{void host.close();};
|
|
59
|
+
const cell=(text:string,title=text,cls='')=>make('td',{textContent:text,title,className:cls});
|
|
60
|
+
function show(next:DensityStripViewLike|null,pendingIndex:number|null,isBusy:boolean){
|
|
61
|
+
view=next;pending=pendingIndex;busy=isBusy;changed();
|
|
62
|
+
backButton.disabled=pending===null||busy;closeButton.disabled=!view||view.status==='running'||busy;
|
|
63
|
+
if(!view){table.replaceChildren();return;}
|
|
64
|
+
const v=view,integer=v.field==='grid',head=make('tr',{},...[fieldLabel(v.field),'v / t (remesh.json)','folds','TEXTURE_STRETCH','bars (ratio vs floor / ceiling)','verdict',''].map(h=>make('th',{textContent:h,scope:'col'})));
|
|
65
|
+
const rows=v.rows.map((r,i)=>{
|
|
66
|
+
const labels:string[]=[];if(v.labels.fewest===i)labels.push('fewest triangles passing');if(v.labels.widest===i)labels.push('widest margin among ties');
|
|
67
|
+
const valueCell=make('td',{className:'density-strip-value'},make('strong',{textContent:String(r.value)}),...labels.map(l=>make('span',{className:'density-strip-label',textContent:l})));
|
|
68
|
+
const counts=r.counts?`v ${r.counts.vertices} · t ${r.counts.triangles}`:'—';
|
|
69
|
+
const stretch=r.stretch?`${r.stretch.severity}${r.stretch.frame?` @ ${r.stretch.frame}`:''}`:'—';
|
|
70
|
+
const bars=r.bars.length?make('td',{className:'density-strip-bars'},...r.bars.map(b=>make('div',{textContent:barText(b),title:`${b.bar} ${b.status}`,className:b.margin<1?'error':''}))):cell('—');
|
|
71
|
+
const verdictCell=cell(r.verdict,r.verdict,`density-strip-verdict${r.verdict==='PASS'?' pass':r.status==='running'?'':' error'}`);
|
|
72
|
+
const load=make('button',{type:'button',textContent:pending===i?'On stage':r.applied?'Applied':'Load',disabled:busy||!r.modelUrl||pending===i||v.status==='running'});
|
|
73
|
+
load.title=r.modelUrl?'Load this revision on the stage as pending — nothing is applied; the Mesh panel\'s Apply applies it':r.status==='running'?'Still building':r.applied?'Applied: it is an applied record now':'Refused: there is no revision to load';
|
|
74
|
+
load.setAttribute('aria-label',`Load ${fieldLabel(v.field)} ${r.value}`);load.onclick=()=>{void host.load(i);};
|
|
75
|
+
const actions=make('td',{className:'density-strip-actions'},load);
|
|
76
|
+
if(i<v.rows.length-1){
|
|
77
|
+
const refine=make('button',{type:'button',textContent:'Refine ↓',disabled:busy||v.status==='running'});
|
|
78
|
+
refine.title=`Build the next strip between ${r.value} and ${v.rows[i+1]!.value}`;refine.setAttribute('aria-label',`Refine between ${r.value} and ${v.rows[i+1]!.value}`);
|
|
79
|
+
refine.onclick=()=>{try{void host.build(refineValues(r.value,v.rows[i+1]!.value,Number(n.value)||5,integer));}catch(error){status(error instanceof Error?error.message:String(error),true);}};
|
|
80
|
+
actions.append(refine);
|
|
81
|
+
}
|
|
82
|
+
const tr=make('tr',{className:[pending===i?'pending':'',r.status==='refused'||r.status==='failed'?'refused':''].filter(Boolean).join(' ')},valueCell,cell(counts),cell(r.folds===null?'—':String(r.folds)),cell(stretch),bars,verdictCell,actions);
|
|
83
|
+
tr.dataset.candidate=r.id;return tr;
|
|
84
|
+
});
|
|
85
|
+
table.replaceChildren(make('table',{className:'density-strip-rows'},make('thead',{},head),make('tbody',{},...rows)));
|
|
86
|
+
}
|
|
87
|
+
changed();
|
|
88
|
+
return {changed,show,status};
|
|
89
|
+
}
|