@floless/app 0.34.2 → 0.35.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/dist/floless-server.cjs +89 -32
- package/dist/schemas/steel.takeoff.v1.schema.json +1 -0
- package/dist/skills/floless-app-steel-takeoff/SKILL.md +6 -3
- package/dist/skills/floless-app-workflows/references/dev-server-and-run-trace.md +4 -2
- package/dist/web/app.css +24 -23
- package/dist/web/app.js +7 -3
- package/dist/web/aware.js +47 -5
- package/dist/web/index.html +22 -9
- package/dist/web/package.json +5 -0
- package/dist/web/panels.js +25 -11
- package/dist/web/steel-3d-core.js +137 -0
- package/dist/web/steel-3d-view.js +422 -0
- package/dist/web/steel-editor.html +140 -6
- package/dist/web/vendor/OrbitControls.js +1963 -0
- package/dist/web/vendor/README.md +15 -0
- package/dist/web/vendor/three.core.js +59732 -0
- package/dist/web/vendor/three.module.js +19552 -0
- package/package.json +1 -1
|
@@ -97,6 +97,9 @@
|
|
|
97
97
|
.mpanel{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:10px;width:min(880px,92vw);max-height:86vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.6)}
|
|
98
98
|
.mhead{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--line)}
|
|
99
99
|
.mhead b{font-size:14px} .mhead button{padding:2px 9px}
|
|
100
|
+
.conf-tgt{display:flex;align-items:center;gap:6px;margin-left:auto;margin-right:12px;font-size:12px;color:var(--mut)}
|
|
101
|
+
#confTarget{width:54px;text-align:right;padding:4px 6px;font:13px system-ui;-moz-appearance:textfield;appearance:textfield}
|
|
102
|
+
#confTarget::-webkit-outer-spin-button,#confTarget::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
|
100
103
|
.mgrid{padding:14px;overflow:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:12px}
|
|
101
104
|
.dcard{border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--bg)}
|
|
102
105
|
.dcard .dprev{height:120px;display:flex;align-items:center;justify-content:center;background:#0b1220;color:var(--mut);font-size:11px;text-align:center;padding:8px;line-height:1.4}
|
|
@@ -133,19 +136,32 @@
|
|
|
133
136
|
#dupStat.has b{color:#fca5a5} #dupB.alert{background:#7f1d1d;border-color:#991b1b} #dupB.alert:hover{background:#991b1b}
|
|
134
137
|
#dupB.ok{background:#166534;border-color:#16a34a;color:#dcfce7}
|
|
135
138
|
header select#planSel{width:auto;min-width:210px;padding:5px 8px;font-weight:600}
|
|
136
|
-
|
|
139
|
+
/* 2D|3D view toggle — segmented, far-left; active segment filled (--panel) with a --brand underline */
|
|
140
|
+
#viewToggle{display:inline-flex;border:1px solid var(--line);border-radius:6px;overflow:hidden;flex:none}
|
|
141
|
+
#viewToggle .seg{padding:5px 11px;margin:0;border:0;border-radius:0;background:transparent;color:var(--mut);font:600 12px system-ui;cursor:pointer}
|
|
142
|
+
#viewToggle .seg+.seg{border-left:1px solid var(--line)}
|
|
143
|
+
#viewToggle .seg:hover{color:var(--text)}
|
|
144
|
+
#viewToggle .seg.on{background:var(--panel);color:var(--text);box-shadow:inset 0 -2px 0 var(--brand)}
|
|
145
|
+
#stagewrap{position:relative}
|
|
146
|
+
#stage3d{position:absolute;inset:0;width:100%;height:100%;display:none;outline:none}
|
|
147
|
+
</style>
|
|
148
|
+
<script type="importmap">{"imports":{"three":"./vendor/three.module.js","three/addons/":"./vendor/"}}</script>
|
|
149
|
+
<script type="module" src="./steel-3d-view.js"></script>
|
|
150
|
+
</head><body>
|
|
137
151
|
<header>
|
|
138
152
|
<b>Steel takeoff</b>
|
|
153
|
+
<div id=viewToggle role=group aria-label="Canvas view"><button id=vt2d class="seg on" aria-pressed=true title="Plan view (2D overlay)">2D</button><button id=vt3d class=seg aria-pressed=false title="3D model view">3D</button></div>
|
|
139
154
|
<select id=planSel title="Switch plan view"></select>
|
|
140
155
|
<span class=stat>Members <b id=mc>0</b></span><span class=stat>Weight <b id=wt>0</b> tons · <b id=wtlb>0</b> lb</span>
|
|
141
156
|
<span class=stat id=rfiStat title="Click to list unresolved members (RFI)">RFI <b id=rc>0</b></span>
|
|
142
157
|
<span class=stat id=dupStat title="Overlapping/duplicate members (same geometry)">Dup <b id=dpc>0</b></span>
|
|
143
|
-
<span class=stat id=confStat title="Confidence report —
|
|
158
|
+
<span class=stat id=confStat title="Confidence report — AI-read score vs. target. Click to review element evidence." style="display:none">Confidence <b id=confPct>—</b><span id=confTgt></span></span>
|
|
144
159
|
<span class=stat id=saveStat title="Edits auto-save in this browser (localStorage)">Saved</span>
|
|
145
160
|
<span class=stat id=srcStat style="display:none"></span><span style="flex:1"></span>
|
|
146
161
|
<button id=undoB title="Undo (Ctrl+Z)">↶</button>
|
|
147
162
|
<button id=redoB title="Redo (Ctrl+Y / Ctrl+Shift+Z)">↷</button>
|
|
148
163
|
<button id=dupB title="Select duplicate (overlapping) members — review then Delete to dedupe">Duplicates</button>
|
|
164
|
+
<button id=mrgB title="Merge collinear segments — joins same-profile, end-to-end runs into one member. Undoable.">Merge collinear</button>
|
|
149
165
|
<button id=mAdd title="Toggle add-member mode">Add member</button>
|
|
150
166
|
<button id=detailsBtn>Details</button>
|
|
151
167
|
<button id=framesBtn>Frames</button>
|
|
@@ -157,6 +173,7 @@
|
|
|
157
173
|
<main>
|
|
158
174
|
<div id=stagewrap>
|
|
159
175
|
<div id=stage><svg id=svg></svg></div>
|
|
176
|
+
<canvas id=stage3d tabindex=0 aria-label="3D model"></canvas>
|
|
160
177
|
<div id=zoombar>
|
|
161
178
|
<button id=zOut title="Zoom out">−</button>
|
|
162
179
|
<input id=zRange type=range min=10 max=400 step=1 value=100>
|
|
@@ -188,7 +205,7 @@
|
|
|
188
205
|
<div class=mpanel><div class=mhead><b>Unresolved members — RFI</b><button id=rfiClose>✕</button></div>
|
|
189
206
|
<div id=rfiGrid style="padding:14px;overflow:auto"></div></div></div>
|
|
190
207
|
<div id=confModal><div class=mbackdrop id=confBackdrop></div>
|
|
191
|
-
<div class=mpanel><div class=mhead><b>Confidence report</b><button id=confClose>✕</button></div>
|
|
208
|
+
<div class=mpanel><div class=mhead><b>Confidence report</b><label class=conf-tgt title="Confidence target for this read (%). Overrides the workflow default; saved with the contract.">Target <input id=confTarget type=number min=0 max=100 step=1> %</label><button id=confClose>✕</button></div>
|
|
192
209
|
<div id=confCats class=conf-cats></div>
|
|
193
210
|
<div id=confFilter class=conf-filter></div>
|
|
194
211
|
<div id=confBody class=conf-body></div></div></div>
|
|
@@ -215,6 +232,8 @@
|
|
|
215
232
|
<script>
|
|
216
233
|
const APP_ID = new URLSearchParams(location.search).get('app') || '';
|
|
217
234
|
let C, PAL, WT;
|
|
235
|
+
let TARGET_CONF = null; // the app's target_confidence input default (%), or null when undeclared — drives the chip's "· target N%" goal
|
|
236
|
+
let view3d = false, view3dReady = false; // 2D|3D toggle state; view3dReady once Steel3DView.init has run
|
|
218
237
|
// Fallback palette (colorFor indexes this array by profile order) when a contract ships no `palette`
|
|
219
238
|
// — e.g. a contract shared/committed with raster_b64 + palette stripped for confidentiality.
|
|
220
239
|
const DEFAULT_PAL=['#60a5fa','#94a3b8','#f59e0b','#34d399','#f472b6','#a78bfa','#fb7185','#22d3ee','#facc15','#4ade80'];
|
|
@@ -242,6 +261,16 @@ async function boot() {
|
|
|
242
261
|
return;
|
|
243
262
|
}
|
|
244
263
|
C = await res.json();
|
|
264
|
+
// The confidence target is a workflow INPUT (target_confidence), not contract data — read its
|
|
265
|
+
// declared default off the app def so the chip can show the goal. Best-effort: on any failure the
|
|
266
|
+
// chip just omits the "· target N%" suffix (no fabricated goal).
|
|
267
|
+
try {
|
|
268
|
+
const ar = await fetch('/api/app/' + encodeURIComponent(APP_ID));
|
|
269
|
+
if (ar.ok) {
|
|
270
|
+
const ti = ((await ar.json())?.app?.inputs || []).find(i => i && i.name === 'target_confidence');
|
|
271
|
+
if (ti && typeof ti.default === 'number') TARGET_CONF = ti.default;
|
|
272
|
+
}
|
|
273
|
+
} catch (_) { /* no target goal shown */ }
|
|
245
274
|
PAL = (Array.isArray(C.palette) && C.palette.length) ? C.palette : DEFAULT_PAL; WT = C.weights || {};
|
|
246
275
|
C.custom_details = C.custom_details || {};
|
|
247
276
|
C.profile_colors = C.profile_colors || {};
|
|
@@ -370,7 +399,7 @@ function setSaved(state,msg){const el=document.getElementById('saveStat');if(!el
|
|
|
370
399
|
else if(state==='err'){el.classList.add('err');el.textContent='Save failed';}
|
|
371
400
|
else el.textContent=msg||'Auto-save on';}
|
|
372
401
|
function persist(){try{localStorage.setItem(LSKEY,JSON.stringify({sig:dataSig(),ts:Date.now(),active:C.active,
|
|
373
|
-
custom_details:C.custom_details, profile_colors:C.profile_colors,
|
|
402
|
+
custom_details:C.custom_details, profile_colors:C.profile_colors, target_confidence:C.target_confidence,
|
|
374
403
|
plans:C.plans.map(p=>({sheet:p.sheet,members:p.members,default_tos:p.default_tos,details:p.details}))}));setSaved('ok');}catch(e){setSaved('err');console.error('local autosave failed',e);}}
|
|
375
404
|
// --- server-side draft save: PUT the FULL contract C — this is the copy Approve bakes.
|
|
376
405
|
// localStorage (persist) stays the instant per-browser draft cache; this is the durable one.
|
|
@@ -399,6 +428,7 @@ function restoreSaved(){try{const raw=localStorage.getItem(LSKEY);if(!raw)return
|
|
|
399
428
|
if(d.sig!==dataSig())return false;const by={};(d.plans||[]).forEach(p=>by[p.sheet]=p);
|
|
400
429
|
if(d.custom_details)C.custom_details=d.custom_details;
|
|
401
430
|
if(d.profile_colors)C.profile_colors=d.profile_colors;
|
|
431
|
+
if(d.target_confidence!=null)C.target_confidence=d.target_confidence;
|
|
402
432
|
C.plans.forEach(p=>{const s=by[p.sheet];if(s){if(Array.isArray(s.members))p.members=s.members;if(s.default_tos!=null)p.default_tos=s.default_tos;if(Array.isArray(s.details))p.details=s.details;p.autofilled=true;}});
|
|
403
433
|
if(d.active!=null)C.active=d.active;return true;}catch(e){console.warn('discarding corrupt local draft',e);return false;}}
|
|
404
434
|
function updUR(){document.getElementById('undoB').disabled=!undo.length;document.getElementById('redoB').disabled=!redo.length;}
|
|
@@ -427,6 +457,46 @@ function redundantDups(){const g={};for(const m of P.members){(g[dupKey(m)]=g[du
|
|
|
427
457
|
const out=[];for(const k in g){const grp=g[k];if(grp.length<2)continue;
|
|
428
458
|
grp.sort((a,b)=>dupScore(b)-dupScore(a));for(let i=1;i<grp.length;i++)out.push(grp[i].id);} // keep [0], rest redundant
|
|
429
459
|
return out;}
|
|
460
|
+
// --- merge collinear chords: same-profile, end-to-end, STRAIGHT beam runs → one member each.
|
|
461
|
+
// The skew read breaks a chord into collinear sub-segments at every rung; this rejoins each run.
|
|
462
|
+
// MIRROR of server/steel-merge.ts (the tested source of truth) — keep the two in lock-step.
|
|
463
|
+
const MRG_END=3, MRG_ANG=3*Math.PI/180, MRG_TOS=0.5;
|
|
464
|
+
function _mUnit(a,b){const dx=b[0]-a[0],dy=b[1]-a[1],L=Math.hypot(dx,dy)||1;return [dx/L,dy/L];}
|
|
465
|
+
function _mElig(m){return (m.role||'beam')==='beam'&&Array.isArray(m.wp)&&m.wp.length>=2&&typeof m.profile==='string'&&m.profile.trim()!=='';}
|
|
466
|
+
function _mShare(a,b){for(const i of[0,1])for(const j of[0,1])if(len(a.wp[i],b.wp[j])<=MRG_END)return{aEnd:i,bEnd:j};return null;}
|
|
467
|
+
function _mStraight(a,b,s){const da=_mUnit(a.wp[0],a.wp[1]),db=_mUnit(b.wp[0],b.wp[1]);
|
|
468
|
+
if(Math.abs(da[0]*db[1]-da[1]*db[0])>Math.sin(MRG_ANG))return false; // not collinear
|
|
469
|
+
const sh=a.wp[s.aEnd],ao=a.wp[1-s.aEnd],bo=b.wp[1-s.bEnd];
|
|
470
|
+
return (ao[0]-sh[0])*(bo[0]-sh[0])+(ao[1]-sh[1])*(bo[1]-sh[1])<0;} // outer points on opposite sides
|
|
471
|
+
function _mElev(a,b,s){const ea=(a.ends&&a.ends[s.aEnd]&&a.ends[s.aEnd].tos!=null)?a.ends[s.aEnd].tos:null;
|
|
472
|
+
const eb=(b.ends&&b.ends[s.bEnd]&&b.ends[s.bEnd].tos!=null)?b.ends[s.bEnd].tos:null;
|
|
473
|
+
if(ea==null&&eb==null)return true;if(ea==null||eb==null)return false;return Math.abs(ea-eb)<=MRG_TOS;}
|
|
474
|
+
function _mAble(a,b){if((a.profile||'').toUpperCase()!==(b.profile||'').toUpperCase())return null;
|
|
475
|
+
const s=_mShare(a,b);if(!s)return null;return _mStraight(a,b,s)&&_mElev(a,b,s)?s:null;}
|
|
476
|
+
function _mAngle(wp){const dx=Math.abs(wp[1][0]-wp[0][0]),dy=Math.abs(wp[1][1]-wp[0][1]),t=Math.tan(MRG_ANG);
|
|
477
|
+
return dy<=dx*t?'H':dx<=dy*t?'V':'D';}
|
|
478
|
+
function mergeCollinearJS(members){
|
|
479
|
+
const idx=members.map((_,i)=>i).filter(i=>_mElig(members[i]));
|
|
480
|
+
const adj=new Map();for(const i of idx)adj.set(i,[]);
|
|
481
|
+
for(let p=0;p<idx.length;p++)for(let q=p+1;q<idx.length;q++){const i=idx[p],j=idx[q];const s=_mAble(members[i],members[j]);
|
|
482
|
+
if(s){adj.get(i).push({j,s});adj.get(j).push({j:i,s:{aEnd:s.bEnd,bEnd:s.aEnd}});}}
|
|
483
|
+
const seen=new Set(),runByAnchor=new Map(),merged=new Set();let mergedAway=0;
|
|
484
|
+
for(const start of idx){if(seen.has(start))continue;const comp=[],stack=[start];seen.add(start);
|
|
485
|
+
while(stack.length){const n=stack.pop();comp.push(n);for(const e of adj.get(n))if(!seen.has(e.j)){seen.add(e.j);stack.push(e.j);}}
|
|
486
|
+
if(comp.length<2)continue;const deg=n=>adj.get(n).length;
|
|
487
|
+
if(comp.some(n=>deg(n)>2))continue;const pe=comp.filter(n=>deg(n)===1);if(pe.length!==2)continue; // branch/loop → skip
|
|
488
|
+
const ordered=[];let prev=-1,cur=pe[0];
|
|
489
|
+
while(cur!==-1){ordered.push(cur);const nx=adj.get(cur).find(e=>e.j!==prev);prev=cur;cur=nx?nx.j:-1;}
|
|
490
|
+
const first=ordered[0],last=ordered[ordered.length-1];
|
|
491
|
+
const fo=1-adj.get(first).find(e=>e.j===ordered[1]).s.aEnd, lo=1-adj.get(last).find(e=>e.j===ordered[ordered.length-2]).s.aEnd;
|
|
492
|
+
const m=JSON.parse(JSON.stringify(members[first])),a=members[first].wp[fo],b=members[last].wp[lo];
|
|
493
|
+
m.wp=[[a[0],a[1]],[b[0],b[1]]];
|
|
494
|
+
if(Array.isArray(members[first].ends)&&Array.isArray(members[last].ends))m.ends=[JSON.parse(JSON.stringify(members[first].ends[fo])),JSON.parse(JSON.stringify(members[last].ends[lo]))];
|
|
495
|
+
m.angle=_mAngle(m.wp);m.mf=ordered.some(n=>members[n].mf===true);m.verified=ordered.every(n=>members[n].verified===true);
|
|
496
|
+
runByAnchor.set(Math.min(...ordered),m);mergedAway+=ordered.length-1;for(const n of ordered)merged.add(n);}
|
|
497
|
+
if(mergedAway===0)return{members,mergedAway:0,runs:0};
|
|
498
|
+
const out=[];for(let i=0;i<members.length;i++){if(runByAnchor.has(i))out.push(runByAnchor.get(i));else if(!merged.has(i))out.push(members[i]);}
|
|
499
|
+
return{members:out,mergedAway,runs:runByAnchor.size};}
|
|
430
500
|
const svg=document.getElementById('svg');
|
|
431
501
|
function toSvg(e){const p=svg.createSVGPoint();p.x=e.clientX;p.y=e.clientY;return p.matrixTransform(svg.getScreenCTM().inverse());}
|
|
432
502
|
// --- zoom + pan ---
|
|
@@ -471,6 +541,7 @@ function render(){
|
|
|
471
541
|
for(const k in grp){const a=grp[k],n=a.length;a.forEach((it,j)=>{const x=it.c[0]+(j-(n-1)/2)*R*2,y=it.c[1];const d=`data-bx="${it.c[0]}" data-fi="${j}" data-gn="${n}"`;
|
|
472
542
|
s+=`<circle class=numbg ${d} cx="${x}" cy="${y}" r="${R}"/><text class=numtx ${d} x="${x}" y="${y}" style="font-size:${F}px">${it.idx+1}</text>`;});}}
|
|
473
543
|
svg.innerHTML=s; document.getElementById('profiles').innerHTML=profs.map(p=>`<option value="${esc(p)}">`).join(''); document.getElementById('details').innerHTML=(P.details||[]).map(d=>`<option value="${esc(d.text)}">`).join(''); stats(); panel(); updUR(); updDup(); updConf();
|
|
544
|
+
if(view3d&&window.Steel3DView)window.Steel3DView.setSelection(selIds); // keep the 3D highlight in sync with the 2D selection
|
|
474
545
|
}
|
|
475
546
|
function updDup(){const n=redundantDups().length;
|
|
476
547
|
document.getElementById('dpc').textContent=n;document.getElementById('dupStat').classList.toggle('has',n>0);
|
|
@@ -689,6 +760,10 @@ document.getElementById('dupB').onclick=()=>{const ids=redundantDups(); // re-
|
|
|
689
760
|
if(!ids.length){const b=document.getElementById('dupB');b.dataset.flash='1';b.classList.add('ok');b.textContent='No duplicates ✓';
|
|
690
761
|
setTimeout(()=>{delete b.dataset.flash;b.classList.remove('ok');updDup();},1500);return;}
|
|
691
762
|
if(mode==='add'){mode='sel';setMode();}geoMode=null;setGeo();selIds=new Set(ids);fitToWindow();render();}; // select the redundant copies → review (numbered) → Delete
|
|
763
|
+
document.getElementById('mrgB').onclick=()=>{const r=mergeCollinearJS(P.members); // conservative; result is undoable (Ctrl+Z)
|
|
764
|
+
if(!r.mergedAway){toast('Nothing to merge');return;}
|
|
765
|
+
edit(()=>{P.members=r.members;selIds=new Set();});
|
|
766
|
+
toast('Merged '+r.mergedAway+' segment'+(r.mergedAway===1?'':'s')+' into '+r.runs+' member'+(r.runs===1?'':'s'));};
|
|
692
767
|
document.getElementById('undoB').onclick=doUndo;
|
|
693
768
|
document.getElementById('redoB').onclick=doRedo;
|
|
694
769
|
addEventListener('keydown',e=>{
|
|
@@ -715,6 +790,46 @@ document.getElementById('zIn').onclick=()=>zoomCentered(zoom*1.25);
|
|
|
715
790
|
document.getElementById('zOut').onclick=()=>zoomCentered(zoom/1.25);
|
|
716
791
|
document.getElementById('zFit').onclick=fitToWindow;
|
|
717
792
|
document.getElementById('zRange').oninput=e=>zoomCentered(+e.target.value/100);
|
|
793
|
+
// --- 2D|3D view toggle. 3D is rendered by Steel3DView (Three.js, loaded as a module → window). It
|
|
794
|
+
// fetches the SAME scene the bake uses (/api/contract/:id/scene) so 2D and 3D show one contract. ---
|
|
795
|
+
const view3dApi={
|
|
796
|
+
// POST the in-progress contract so the model reflects unsaved edits; throws on a bad contract.
|
|
797
|
+
fetchScene:async()=>{const r=await fetch('/api/contract/'+encodeURIComponent(APP_ID)+'/scene',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({contract:C})});const j=await r.json();if(!j.ok)throw new Error(j.error||'scene render failed');return j.scene;}, // endpoint shape: {ok, scene, skipped}
|
|
798
|
+
onSelect:(id,additive)=>{if(!id){selIds=new Set();}else if(additive){selIds.has(id)?selIds.delete(id):selIds.add(id);}else{selIds=new Set([id]);}render();}, // 3D pick → shared selection (Phase B4)
|
|
799
|
+
getMembers:()=>P.members, // raw members (wp) for snap geometry
|
|
800
|
+
ptPerFt:()=>(P&&P.pt_per_ft>0?P.pt_per_ft:1),
|
|
801
|
+
defaultTosMm:()=>(defaultTOS!=null?defaultTOS*25.4:0), // editor stores default T.O.S in inches
|
|
802
|
+
onMoveMember:(id,newWp)=>{edit(()=>{const m=byId(id);if(m)m.wp=newWp;});}, // 3D drag → write wp (undoable, autosaves, syncs 2D/chip/BOM)
|
|
803
|
+
onElevateMember:(id,dIn)=>{edit(()=>{const m=byId(id);if(!m)return;ensureMeta(m); // 3D Alt-drag → raise/lower T.O.S by dIn inches
|
|
804
|
+
if(m.role==='column'){m.col.tos=(m.col.tos!=null?m.col.tos:defaultTOS)+dIn;m.col.bos=(m.col.bos!=null?m.col.bos:0)+dIn;m.col.tosDef=false;} // rigid raise (bos null→0, both shift) so the column isn't stretched
|
|
805
|
+
else m.ends.forEach(e=>{e.tos=(e.tos!=null?e.tos:defaultTOS)+dIn;e.tosDef=false;});});},
|
|
806
|
+
};
|
|
807
|
+
function applyViewState(on){ // flip the toggle + swap the canvases (no 3D side effects)
|
|
808
|
+
view3d=on;
|
|
809
|
+
const t2=document.getElementById('vt2d'),t3=document.getElementById('vt3d');
|
|
810
|
+
t2.classList.toggle('on',!on);t2.setAttribute('aria-pressed',String(!on));
|
|
811
|
+
t3.classList.toggle('on',on);t3.setAttribute('aria-pressed',String(on));
|
|
812
|
+
document.getElementById('stage').style.display=on?'none':'';
|
|
813
|
+
document.getElementById('stage3d').style.display=on?'block':'none';
|
|
814
|
+
document.getElementById('zoombar').style.display=on?'none':''; // zoom slider is 2D-only
|
|
815
|
+
}
|
|
816
|
+
async function setView(on){
|
|
817
|
+
if(on){
|
|
818
|
+
if(!window.Steel3DView){toast('3D view unavailable (renderer failed to load)');return;} // stay in 2D
|
|
819
|
+
try{
|
|
820
|
+
if(!view3dReady){window.Steel3DView.init(document.getElementById('stage3d'),view3dApi);view3dReady=true;}
|
|
821
|
+
applyViewState(true);
|
|
822
|
+
window.Steel3DView.show();
|
|
823
|
+
await window.Steel3DView.rebuild();
|
|
824
|
+
window.Steel3DView.setSelection(selIds);
|
|
825
|
+
}catch(e){ // a failed open must not strand the UI in 3D with a blank canvas
|
|
826
|
+
applyViewState(false);if(window.Steel3DView)window.Steel3DView.hide();
|
|
827
|
+
toast('Could not open 3D view: '+((e&&e.message)||e));
|
|
828
|
+
}
|
|
829
|
+
}else{applyViewState(false);if(window.Steel3DView)window.Steel3DView.hide();}
|
|
830
|
+
}
|
|
831
|
+
document.getElementById('vt2d').onclick=()=>{if(view3d)setView(false);};
|
|
832
|
+
document.getElementById('vt3d').onclick=()=>{if(!view3d)setView(true);};
|
|
718
833
|
// Ctrl+wheel = zoom at cursor
|
|
719
834
|
stage.addEventListener('wheel',e=>{if(e.ctrlKey){e.preventDefault();applyZoom(zoom*Math.exp(-e.deltaY*0.0015),e.clientX,e.clientY);}},{passive:false});
|
|
720
835
|
// middle-button drag = pan
|
|
@@ -874,10 +989,22 @@ function scoreContractJS(){const plans=C.plans||[];const byMember=[],byDetail=[]
|
|
|
874
989
|
const sc=byMember.filter(m=>m.band!=='rfi');const n=sc.reduce((s,m)=>s+m.tons*BANDW[m.band],0),d=sc.reduce((s,m)=>s+m.tons,0);
|
|
875
990
|
return {byMember,byDetail,byCategory:{beams,columns,details,connections:{score:null,note:'Not scored yet (reserved slot)',counts:_cnt0()}},coverage:{members:byMember.length,segments:segs,note:'Approximate in v1 — precise per-segment binding is Slice 2.'},overall:{score:d>0?Math.round(n/d*100):null,tons:d,rfiCount:byMember.filter(m=>m.band==='rfi').length}};}
|
|
876
991
|
function bandColorForPct(p){return p==null?'var(--mut)':p>=95?BANDC.verified:p>=80?BANDC.high:p>=50?BANDC.med:BANDC.low;}
|
|
992
|
+
// Effective target: the user's per-read override (in the contract) wins; else the app's
|
|
993
|
+
// target_confidence input default (read in boot()); else none. Editing the override never touches
|
|
994
|
+
// the .flo, so it can't disarm the Run gate.
|
|
995
|
+
function effectiveTarget(){const t=C&&C.target_confidence;return typeof t==='number'?t:(TARGET_CONF!=null?TARGET_CONF:null);}
|
|
877
996
|
function updConf(){const el=document.getElementById('confStat');if(!el)return;const s=scoreContractJS();
|
|
878
997
|
if(!s.byMember.length){el.style.display='none';return;}el.style.display='';
|
|
879
|
-
const
|
|
880
|
-
|
|
998
|
+
const sc=s.overall.score,col=bandColorForPct(sc),tgt=effectiveTarget();
|
|
999
|
+
const b=document.getElementById('confPct');b.textContent=sc==null?'—':sc+'%';b.style.color=col;
|
|
1000
|
+
// Goal suffix: "· target N%" (muted, static) + "(−G%)" gap in the band color, shown only while below target.
|
|
1001
|
+
const tg=document.getElementById('confTgt');
|
|
1002
|
+
if(tg){
|
|
1003
|
+
if(tgt==null||sc==null)tg.textContent='';
|
|
1004
|
+
else{const gap=tgt-sc;
|
|
1005
|
+
tg.innerHTML=' · <span style="color:var(--mut)">target '+tgt+'%</span>'+(gap>0?' <span style="color:'+col+'">(−'+gap+'%)</span>':'');}
|
|
1006
|
+
}
|
|
1007
|
+
el.title='Confidence '+(sc==null?'—':sc+'%')+(tgt!=null?' vs target '+tgt+'%':'')+' — '+s.overall.tons.toFixed(1)+' t scored · '+s.overall.rfiCount+' RFI. Click for the report.';}
|
|
881
1008
|
// --- the report modal ---
|
|
882
1009
|
let confBand='all',confCat='all',confExpand=null;
|
|
883
1010
|
function openConf(){confExpand=null;renderConf();document.getElementById('confModal').style.display='flex';}
|
|
@@ -886,6 +1013,8 @@ function confOpen(){return document.getElementById('confModal').style.display===
|
|
|
886
1013
|
function _bar(pct){return '<div class=cc-bar><div class=cc-fill style="width:'+(pct||0)+'%;background:'+bandColorForPct(pct)+';'+(pct==null?'opacity:0':'')+'"></div></div>';}
|
|
887
1014
|
function _countsHtml(c){const out=['verified','high','med','low','rfi'].filter(k=>c[k]>0).map(k=>'<span style="color:'+BANDC[k]+'">'+BANDLAB[k][0]+' <b>'+c[k]+'</b></span>').join('');return out||'<span>—</span>';}
|
|
888
1015
|
function renderConf(){const s=scoreContractJS();const cat=s.byCategory;
|
|
1016
|
+
const ct=document.getElementById('confTarget');if(ct&&document.activeElement!==ct){ // value = the per-read OVERRIDE only; the app default shows as a placeholder (empty = using default)
|
|
1017
|
+
ct.value=typeof C.target_confidence==='number'?C.target_confidence:'';ct.placeholder=TARGET_CONF!=null?String(TARGET_CONF):'70';}
|
|
889
1018
|
const card=(key,label,cs,stub)=>'<div class="ccard'+(stub?' stub':' click')+(confCat===key?' on':'')+'" data-cat="'+key+'"'+(stub?' title="not scored yet"':'')+'>'+
|
|
890
1019
|
'<div class=cc-label>'+label+'</div>'+
|
|
891
1020
|
'<div class=cc-score style="color:'+(stub?'var(--mut)':bandColorForPct(cs.score))+'">'+(stub||cs.score==null?'—':cs.score+'%')+'</div>'+
|
|
@@ -924,6 +1053,11 @@ function wireConf(){
|
|
|
924
1053
|
document.querySelectorAll('#confBody tr.confrow').forEach(r=>r.onclick=e=>{if(e.target.closest('button'))return;const id=r.dataset.mid;confExpand=confExpand===id?null:id;renderConf();});
|
|
925
1054
|
document.querySelectorAll('#confBody button[data-loc]').forEach(b=>b.onclick=e=>{e.stopPropagation();const pi=+b.dataset.pi;if(pi!==C.active)setPlan(pi);const m=byId(b.dataset.loc);if(!m)return;selIds=new Set([m.id]);geoMode=null;setGeo();closeConf();render();zoomToMember(m);});}
|
|
926
1055
|
document.getElementById('confStat').onclick=openConf;
|
|
1056
|
+
document.getElementById('confTarget').onchange=e=>{ // per-read target override → contract.target_confidence (never the .flo)
|
|
1057
|
+
const raw=String(e.target.value).trim();
|
|
1058
|
+
if(raw===''){delete C.target_confidence;} // cleared → fall back to the app default
|
|
1059
|
+
else{let v=Math.round(+raw);if(!isFinite(v))return;v=Math.max(0,Math.min(100,v));C.target_confidence=v;e.target.value=v;}
|
|
1060
|
+
scheduleSave();updConf();};
|
|
927
1061
|
document.getElementById('confClose').onclick=closeConf;
|
|
928
1062
|
document.getElementById('confBackdrop').onclick=closeConf;
|
|
929
1063
|
// --- detail preview lightbox (click a card / an end's ⤢ to enlarge) ---
|