@floless/app 0.48.0 → 0.49.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.
@@ -223,6 +223,9 @@ Important optional fields:
223
223
  "weights": { "W16X26": 26, "HSS6X6X3/8": 27.48 }, // profile → plf; null for unresolved
224
224
  "moment_frames": [ … ], // from Frame Elevation sheets
225
225
  "detail_bubbles": { "S-504": { "5": [0.32, 0.71] } }, // sheet → { number → [fx,fy] }
226
+ "connections": [ // vendor-neutral connection library
227
+ { "id": "cx-m1", "type": "moment", "detail": "5-S504", "targets": { "tekla": "146" }, "source": "drawing" }
228
+ ],
226
229
  "plans": [
227
230
  {
228
231
  "sheet": "S-202",
@@ -245,7 +248,7 @@ Important optional fields:
245
248
  "rfi": false,
246
249
  "mf": false,
247
250
  "ends": [
248
- { "tos": 198, "note": "moment", "tosDef": false, "detail": "5-S504" },
251
+ { "tos": 198, "note": "moment", "tosDef": false, "detail": "5-S504", "conn": "cx-m1" },
249
252
  { "tos": 198, "note": "shear", "tosDef": true, "detail": "" }
250
253
  ]
251
254
  },
@@ -282,6 +285,17 @@ Key field rules:
282
285
  moment-frame member (persistent; survives AISC resolution).
283
286
  - `raster_b64` is a base64 JPEG of the framing area (title block excluded). It is
284
287
  **machine-local** and **never committed**.
288
+ - **Connection library (`connections[]`) + per-end refs.** A vendor-neutral top-level
289
+ `connections: [{ id, type, detail, targets:{ tekla:"146", … }, source }]` maps a connection
290
+ TYPE → the firm's design detail# → per-platform component ids. A member end references a row by id
291
+ via `ends[].conn` (or `col.conn` for a column); the legacy `note`/`detail` stay as the fallback
292
+ when `conn` is empty. `source` is `drawing` (read from a callout — best evidence), `company-rules`
293
+ (the firm's standard), or `user`. Prefer a `conn` reference over free text when the connection is
294
+ known — the editor's per-end picker and the Connections confidence card both key on it.
295
+ - **Company-rules pre-fill (§5.5).** If a firm-standard knowledge file is present (e.g.
296
+ `company-rules.md`: "moment → 50S504 → Tekla 146"), read it and emit those rows in `connections[]`
297
+ with `source: "company-rules"`, then PUT. The user reviews/edits them in the editor's
298
+ **Connections** library panel and assigns a row per member end. No new agent verb — it's contract data.
285
299
 
286
300
  ### 5. Write to the contract store
287
301
 
@@ -68,7 +68,7 @@
68
68
  rect.dethot{cursor:pointer;fill:rgba(168,85,247,.16);stroke:#a855f7;stroke-width:1;stroke-dasharray:3 3;vector-effect:non-scaling-stroke}
69
69
  rect.dethot:hover{fill:rgba(168,85,247,.34)}
70
70
  text.dettx{fill:#ddd6fe;font:bold 11px system-ui;text-anchor:middle;pointer-events:none}
71
- #detailsModal,#framesModal,#rfiModal,#confModal,#askAiModal{position:fixed;inset:0;z-index:20;display:none;align-items:center;justify-content:center}
71
+ #detailsModal,#framesModal,#rfiModal,#confModal,#askAiModal,#connModal{position:fixed;inset:0;z-index:20;display:none;align-items:center;justify-content:center}
72
72
  #askAiDrop:hover{border-color:var(--brand);color:var(--text)} #askAiDrop.has{border-style:solid;border-color:var(--brand)}
73
73
  .aithumb{position:relative;display:inline-flex} .aithumb img{height:60px;border-radius:4px;border:1px solid var(--line);background:#fff;display:block}
74
74
  .aithumb button{position:absolute;top:-5px;right:-5px;width:16px;height:16px;padding:0;border-radius:8px;font-size:10px;line-height:16px;text-align:center;background:#7f1d1d;border-color:#991b1b;color:#fecaca}
@@ -93,7 +93,11 @@
93
93
  .chip{display:inline-block;padding:1px 6px;border-radius:4px;font-size:10px;border:1px solid var(--line);color:var(--mut);margin:0 3px 3px 0}
94
94
  .chip.assumed,.chip.weak{border-color:#713f12;color:#fbbf24} .chip.fail{border-color:#7f1d1d;color:#fca5a5}
95
95
  tr.confrow{cursor:pointer} .conf-expand{color:var(--mut);font-size:12px;line-height:1.6;background:#0b1220}
96
- .ftab td .combo{padding:4px 6px} .ftab .rea{color:var(--mut);font-size:12px} .rfichip{display:inline-block;min-width:18px;height:18px;line-height:18px;text-align:center;background:#7f1d1d;color:#fecaca;border-radius:9px;font-size:11px;padding:0 5px}
96
+ .ftab td .combo{padding:4px 6px} .ftab .rea{color:var(--mut);font-size:12px}
97
+ .ftab-inp{background:#0f172a;color:var(--text);border:1px solid #475569;border-radius:4px;padding:4px 6px;width:100%;font:13px system-ui}
98
+ .ftab-inp:focus{outline:none;border-color:var(--brand)}
99
+ .ftab .cdel{padding:3px 7px;font-size:11px} .ftab .csrc{white-space:nowrap}
100
+ .connset{opacity:.55} .rfichip{display:inline-block;min-width:18px;height:18px;line-height:18px;text-align:center;background:#7f1d1d;color:#fecaca;border-radius:9px;font-size:11px;padding:0 5px}
97
101
  .mbackdrop{position:absolute;inset:0;background:rgba(2,8,23,.62)}
98
102
  .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)}
99
103
  .mhead{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--line)}
@@ -223,6 +227,7 @@
223
227
  <hr>
224
228
  <div class=mlabel>Reference</div>
225
229
  <button id=detailsBtn>Details</button>
230
+ <button id=connBtn title="Connection library — map each connection type to its design detail# and per-platform component id; reference a row from each member end">Connections</button>
226
231
  <button id=framesBtn>Frames</button>
227
232
  <hr>
228
233
  <div class=mlabel>Session</div>
@@ -272,6 +277,12 @@
272
277
  <div class=hint id=dnHint>For anything the auto‑detection missed. The image stays on your machine.</div>
273
278
  </div>
274
279
  <div class=mgrid id=detailsGrid></div></div></div>
280
+ <div id=connModal><div class=mbackdrop id=connBackdrop></div>
281
+ <div class=mpanel><div class=mhead><b>Connection library</b><div style="display:flex;gap:8px;align-items:center"><button id=connNew>+ New row</button><button id=connClose>✕</button></div></div>
282
+ <div class=conf-filter><span class=glab>Platform</span><div class=grp id=connPlatGrp><button class=on data-plat=tekla>Tekla</button><button data-plat=sds2>SDS2</button><button data-plat=revit>Revit</button></div><span class=hint style="margin:0">The design detail# is platform-neutral; the component id is per-platform.</span></div>
283
+ <div style="overflow:auto;flex:1;padding:14px;scrollbar-width:thin">
284
+ <table class=ftab id=connTable><thead><tr><th style="width:24%">Type</th><th style="width:24%">Detail #</th><th id=connCompHead style="width:24%">Tekla id</th><th style="width:18%">Source</th><th style="width:10%"></th></tr></thead><tbody id=connBody></tbody></table>
285
+ </div></div></div>
275
286
  <div id=framesModal><div class=mbackdrop id=framesBackdrop></div>
276
287
  <div class=mpanel><div class=mhead><b>Moment frame schedule</b><button id=framesClose>✕</button></div>
277
288
  <div id=framesGrid style="padding:14px;overflow:auto"></div></div></div>
@@ -748,7 +759,7 @@ function render(){
748
759
  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>`;});}}
749
760
  s+=renderDims();
750
761
  if(P.frame)s+=axisGlyphSvg(P.frame.o,P.frame.u,false); // local-axes glyph at the origin (only when a frame is set; removed on reset)
751
- 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(); updCS();
762
+ 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(); updCS(); updConnBtn();
752
763
  if(view3d&&window.Steel3DView)window.Steel3DView.setSelection(selIds); // keep the 3D highlight in sync with the 2D selection
753
764
  }
754
765
  function updDup(){const n=redundantDups().length;
@@ -901,12 +912,20 @@ function panel(){
901
912
  return `<div class=elabrow><span class=elab>${label}</span><label class=defck><input type=checkbox id=${id}_ck ${def?'checked':''}>default</label></div><input id=${id} inputmode=decimal placeholder="5 3/4&quot; · 1&#39;-0 1/4&quot;" value="${esc(fmtFtIn(v))}"${def?' disabled':''}><span class=edec>${esc(fmtDecIn(v))}</span>`;};
902
913
  const nin=(id,v)=>`<input id=${id} class="f combo" data-src=conntypes placeholder="connection / note" value="${esc(v||'')}" autocomplete=off>`;
903
914
  const dFld=(id,o)=>{const hasPrev=o.detail&&previewFor(o.detail);return `<div class=elab>Connection detail</div><div style="display:flex;gap:6px"><input id=${id} class=combo data-src=details placeholder="e.g. 5-S504" value="${esc(o.detail||'')}" style="flex:1" autocomplete=off>${hasPrev?`<button id=${id}_open class=ghost title="Open detail ${esc(o.detail)}">⤢</button>`:''}<button id=${id}_pk class="ghost${(picking&&pickKind==='detail'&&pickEnd===o)?' on':''}" title="Pick a detail callout from the drawing">⌖</button></div>`;};
915
+ // Per-end connection-library picker (sets o.conn = a connections[] row id). When set, the row's
916
+ // type/detail# are the source of truth (shown read-only below); the free-text note/detail dim to a
917
+ // fallback. Readonly combo → pick only, no free typing; the connrows source shows every row.
918
+ const cFld=(id,o)=>{const row=o.conn?connRowById(o.conn):null;
919
+ const cap=row?`<span class=edec style="text-align:left">${esc(row.type||'?')}${row.detail?' · '+esc(row.detail):''} — ${esc(row.source||'user')}</span>`
920
+ :(o.conn?'<span class=edec style="text-align:left;color:#fca5a5">references a missing row — re-pick</span>':'');
921
+ return `<div class=elab>Connection</div><input id=${id} class=combo data-src=connrows readonly placeholder="— none — (pick from the library)" value="${esc(row?connLabel(row):'')}" autocomplete=off>${cap}`;};
922
+ const fb=(o,html)=>`<div class="${o.conn?'connset':''}">${html}</div>`; // dim the note/detail fallback when a library row is referenced
904
923
  const dotS='<span class=epdot style="background:#facc15"></span>',dotE='<span class=epdot style="background:#f472b6"></span>'; // start=yellow, end=magenta — matches the canvas end markers
905
924
  const elev = col
906
- ? `<div class="sect f">Elevation</div>${ftFld('bos',dotS+'Bottom (BOS)',m.col.bos)}${tosFld('tosC',dotE+'Top (TOS)',m.col)}${nin('ntC',m.col.note)}${dFld('dtC',m.col)}`
907
- : `<div class="sect f">${dotS}Start</div>${tosFld('tosA','TOS',m.ends[0])}${nin('ntA',m.ends[0].note)}${dFld('dtA',m.ends[0])}
925
+ ? `<div class="sect f">Elevation</div>${ftFld('bos',dotS+'Bottom (BOS)',m.col.bos)}${tosFld('tosC',dotE+'Top (TOS)',m.col)}${cFld('cnC',m.col)}${fb(m.col,nin('ntC',m.col.note)+dFld('dtC',m.col))}`
926
+ : `<div class="sect f">${dotS}Start</div>${tosFld('tosA','TOS',m.ends[0])}${cFld('cnA',m.ends[0])}${fb(m.ends[0],nin('ntA',m.ends[0].note)+dFld('dtA',m.ends[0]))}
908
927
  <div class=divrow><hr><button class=ghost id=matchEnds>Match → both ends</button><hr></div>
909
- <div class=sect>${dotE}End</div>${tosFld('tosB','TOS',m.ends[1])}${nin('ntB',m.ends[1].note)}${dFld('dtB',m.ends[1])}`;
928
+ <div class=sect>${dotE}End</div>${tosFld('tosB','TOS',m.ends[1])}${cFld('cnB',m.ends[1])}${fb(m.ends[1],nin('ntB',m.ends[1].note)+dFld('dtB',m.ends[1]))}`;
910
929
  p.innerHTML=`<h3>Member ${esc(m.id)}</h3>
911
930
  <div class=row><label>Profile</label><div style="display:flex;gap:6px"><input id=pf class=combo data-src=profiles value="${esc(m.profile)}" style="flex:1" autocomplete=off><button id=pickProf class="ghost${(picking&&pickKind==='profile')?' on':''}" title="Pick profile by clicking a label in the drawing">⌖ pick</button></div>${(picking&&pickKind==='profile')?'<div class="hint" style="margin-top:4px;font-style:italic;color:var(--brand)">Click a profile label in the drawing…</div>':(picking&&pickKind==='detail')?'<div class="hint" style="margin-top:4px;font-style:italic;color:#a855f7">Click a detail callout in the drawing…</div>':''}</div>
912
931
  <div class="seg2 f"><button id=rBeam class="${col?'':'on'}">Beam</button><button id=rCol class="${col?'on':''}">Column</button></div>
@@ -939,18 +958,19 @@ function panel(){
939
958
  const wireDet=(id,o)=>{const i=document.getElementById(id);if(i)i.onchange=e=>edit(()=>{o.detail=e.target.value.trim();});
940
959
  const pk=document.getElementById(id+'_pk');if(pk)pk.onclick=()=>{if(picking&&pickKind==='detail'&&pickEnd===o){picking=false;pickEnd=null;}else{picking=true;pickKind='detail';pickEnd=o;}render();};
941
960
  const op=document.getElementById(id+'_open');if(op)op.onclick=()=>openPreview(o.detail);};
961
+ const wireConn=(id,o)=>{const i=document.getElementById(id);if(!i)return;i.onchange=e=>edit(()=>{const v=(e.target.value||'').trim();o.conn=(!v||v==='— none —')?'':v.split(' · ')[0];});};
942
962
  if(col){
943
963
  document.getElementById('bos').onchange=e=>edit(()=>{m.col.bos=parseLen(e.target.value);});
944
964
  wireTos('tosC',m.col);
945
965
  document.getElementById('ntC').onchange=e=>edit(()=>{m.col.note=e.target.value;});
946
- wireDet('dtC',m.col);
966
+ wireDet('dtC',m.col);wireConn('cnC',m.col);
947
967
  }else{
948
968
  wireTos('tosA',m.ends[0]);
949
969
  document.getElementById('ntA').onchange=e=>edit(()=>{m.ends[0].note=e.target.value;});
950
- wireDet('dtA',m.ends[0]);
970
+ wireDet('dtA',m.ends[0]);wireConn('cnA',m.ends[0]);
951
971
  wireTos('tosB',m.ends[1]);
952
972
  document.getElementById('ntB').onchange=e=>edit(()=>{m.ends[1].note=e.target.value;});
953
- wireDet('dtB',m.ends[1]);
973
+ wireDet('dtB',m.ends[1]);wireConn('cnB',m.ends[1]);
954
974
  document.getElementById('matchEnds').onclick=()=>edit(()=>{m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail,conn:m.ends[0].conn,connVerified:m.ends[0].connVerified};});
955
975
  }
956
976
  document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(x=>x.id!==m.id);selIds.clear();if(selDimIds.size){P.dims=P.dims.filter(d=>!selDimIds.has(d.id));selDimIds.clear();}}); // also drop any dims a mixed marquee selected
@@ -1240,6 +1260,7 @@ addEventListener('keydown',e=>{
1240
1260
  if(e.key==='Escape'&&lightboxOpen()){closeLightbox();return;}
1241
1261
  if(e.key==='Escape'&&askAiIsOpen()){askAiClose();return;}
1242
1262
  if(e.key==='Escape'&&detailsOpen()){closeDetails();return;}
1263
+ if(e.key==='Escape'&&connLibOpen()){closeConnLib();return;}
1243
1264
  if(e.key==='Escape'&&framesOpen()){closeFrames();return;}
1244
1265
  if(e.key==='Escape'&&rfiOpen()){closeRFI();return;}
1245
1266
  if(e.key==='Escape'&&confOpen()){closeConf();return;}
@@ -1428,6 +1449,37 @@ function detailsOpen(){return document.getElementById('detailsModal').style.disp
1428
1449
  document.getElementById('detailsBtn').onclick=openDetails;
1429
1450
  document.getElementById('detailsClose').onclick=closeDetails;
1430
1451
  document.getElementById('detailsBackdrop').onclick=closeDetails;
1452
+ // --- connection library: edit connections[] (type → design detail# → per-platform component id) ---
1453
+ // The library is the vendor-neutral source; member ends reference a row via ends[].conn / col.conn.
1454
+ // connPlat scopes the editable "component id" column to one platform; the detail# is platform-neutral.
1455
+ let connPlat='tekla';const PLAT_LABEL={tekla:'Tekla',sds2:'SDS2',revit:'Revit'};
1456
+ function openConnLib(){if(!Array.isArray(C.connections))C.connections=[];renderConnTable();document.getElementById('connModal').style.display='flex';}
1457
+ function closeConnLib(){document.getElementById('connModal').style.display='none';}
1458
+ function connLibOpen(){return document.getElementById('connModal').style.display==='flex';}
1459
+ function updConnBtn(){const b=document.getElementById('connBtn');if(b)b.textContent='Connections'+((C.connections||[]).length?' ('+C.connections.length+')':'');}
1460
+ function renderConnTable(){const tb=document.getElementById('connBody');if(!tb)return;
1461
+ document.getElementById('connCompHead').textContent=(PLAT_LABEL[connPlat]||connPlat)+' id';
1462
+ const rows=C.connections||[];
1463
+ if(!rows.length){tb.innerHTML='<tr><td colspan=5 style="text-align:center;padding:32px;color:var(--mut);font-size:12px">No connections defined — click + New row to add the first.</td></tr>';updConnBtn();return;}
1464
+ const srcChip=s=>s==='drawing'?'<span class="chip assumed">drawing</span>':s==='company-rules'?'<span class=chip>company-rules</span>':'<span class=chip style="border-color:#1d4ed8;color:#93c5fd">user</span>';
1465
+ tb.innerHTML=rows.map(r=>`<tr data-id="${esc(r.id)}">`
1466
+ +`<td><input class="combo ftab-inp" data-src=conntypes data-f=type value="${esc(r.type||'')}" placeholder="moment" autocomplete=off></td>`
1467
+ +`<td><input class="combo ftab-inp" data-src=details data-f=detail value="${esc(r.detail||'')}" placeholder="e.g. 50S504" autocomplete=off></td>`
1468
+ +`<td><input class=ftab-inp data-f=target value="${esc((r.targets&&r.targets[connPlat])||'')}" placeholder="e.g. 146" autocomplete=off></td>`
1469
+ +`<td class=csrc>${srcChip(r.source||'user')}</td>`
1470
+ +`<td><button class="danger cdel" title="Remove this row">✕</button></td></tr>`).join('');
1471
+ tb.querySelectorAll('tr[data-id]').forEach(tr=>{const id=tr.dataset.id;const row=connRowById(id);if(!row)return;
1472
+ tr.querySelector('[data-f=type]').onchange=e=>edit(()=>{row.type=e.target.value.trim();});
1473
+ tr.querySelector('[data-f=detail]').onchange=e=>edit(()=>{row.detail=e.target.value.trim();});
1474
+ tr.querySelector('[data-f=target]').onchange=e=>edit(()=>{if(!row.targets)row.targets={};const v=e.target.value.trim();if(v)row.targets[connPlat]=v;else delete row.targets[connPlat];});
1475
+ tr.querySelector('.cdel').onclick=()=>{edit(()=>{C.connections=C.connections.filter(x=>x.id!==id);
1476
+ for(const pl of (C.plans||[]))for(const m of (pl.members||[])){if(m.col&&m.col.conn===id)m.col.conn='';for(const en of (m.ends||[]))if(en&&en.conn===id)en.conn='';}});renderConnTable();};});
1477
+ updConnBtn();}
1478
+ document.getElementById('connBtn').onclick=openConnLib;
1479
+ document.getElementById('connClose').onclick=closeConnLib;
1480
+ document.getElementById('connBackdrop').onclick=closeConnLib;
1481
+ document.getElementById('connNew').onclick=()=>{edit(()=>{if(!Array.isArray(C.connections))C.connections=[];C.connections.push({id:'conn'+Date.now(),type:'moment',detail:'',targets:{[connPlat]:''},source:'user'});});renderConnTable();const tb=document.getElementById('connBody');if(tb.lastElementChild)tb.lastElementChild.scrollIntoView({block:'nearest'});};
1482
+ document.getElementById('connPlatGrp').querySelectorAll('button').forEach(b=>b.onclick=()=>{connPlat=b.dataset.plat;document.getElementById('connPlatGrp').querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b));renderConnTable();});
1431
1483
  // --- create your own detail (name + screenshot) — for anything the auto-detect missed ---
1432
1484
  function readImageCompressed(file,cb){const r=new FileReader();r.onload=()=>{const im=new Image();im.onload=()=>{
1433
1485
  const max=1400;let w=im.naturalWidth,h=im.naturalHeight;const sc=Math.min(1,max/Math.max(w,h));w=Math.max(1,Math.round(w*sc));h=Math.max(1,Math.round(h*sc));
@@ -1678,12 +1730,18 @@ lbView.addEventListener('dblclick',lbReset);
1678
1730
  const COMBO_SRC={
1679
1731
  profiles:()=>profs,
1680
1732
  conntypes:()=>['moment','drag','shear','braced','pinned','fixed'],
1681
- details:()=>{const s=new Set();(P.details||[]).forEach(d=>s.add(d.text));Object.keys(C.custom_details).forEach(n=>s.add(n));return [...s].sort((a,b)=>String(a).localeCompare(String(b),undefined,{numeric:true}));}
1733
+ details:()=>{const s=new Set();(P.details||[]).forEach(d=>s.add(d.text));Object.keys(C.custom_details).forEach(n=>s.add(n));return [...s].sort((a,b)=>String(a).localeCompare(String(b),undefined,{numeric:true}));},
1734
+ connrows:()=>['— none —',...(C.connections||[]).map(r=>connLabel(r))]
1682
1735
  };
1736
+ // A connection-library row's one-line label for the per-end picker. The id is the prefix (parsed
1737
+ // back on select); type/detail follow for the human. Kept here so COMBO_SRC + cFld + wireConn agree.
1738
+ function connLabel(r){return r.id+' · '+(r.type||'?')+(r.detail?' · '+r.detail:'');}
1739
+ function connRowById(id){return (C.connections||[]).find(r=>r&&r.id===id)||null;}
1683
1740
  const comboPop=document.createElement('div');comboPop.id='comboPop';document.body.appendChild(comboPop);
1684
1741
  let comboInput=null,comboIdx=-1;
1685
1742
  function showCombo(inp){const src=COMBO_SRC[inp.dataset.src];if(!src)return;comboInput=inp;
1686
- const q=(inp.value||'').trim().toUpperCase();const opts=src().filter(o=>String(o).toUpperCase().includes(q));
1743
+ // connrows is a row PICKER (readonly), not a type-to-filter field — always offer every row.
1744
+ const q=inp.dataset.src==='connrows'?'':(inp.value||'').trim().toUpperCase();const opts=src().filter(o=>String(o).toUpperCase().includes(q));
1687
1745
  if(!opts.length){comboPop.style.display='none';return;}
1688
1746
  comboPop.innerHTML=opts.map(o=>`<div class=opt data-v="${esc(o)}">${esc(o)}</div>`).join('');
1689
1747
  const r=inp.getBoundingClientRect();comboPop.style.left=Math.round(r.left)+'px';comboPop.style.top=Math.round(r.bottom+2)+'px';comboPop.style.minWidth=Math.round(r.width)+'px';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {
@@ -1,120 +0,0 @@
1
- ---
2
- name: floless-app-steel-from-drawings
3
- description: This skill should be used to turn a structural steel drawing into an interactive 3D model in floless.app — the "Steel from Drawings" reader. Triggers on a pending floless `rebake` request for the `steel-from-drawings` app (queued from its "Re-read & re-bake ▸" button), or asks like "read this framing plan into 3D", "turn this steel drawing into a model", "bake my drawing into steel-from-drawings". It teaches the host AI to READ a steel drawing (PDF/image) with its own vision, extract a schematic 3D scene (grids, members, sizes, takeoff) in the viewer-3d scene schema, bake it INLINE into the app's `view` node, recompile, and ask the user to approve — all at COMPOSE time (no model in the run path, no API key). Also bakes the same scene into the `steel-to-ifc` companion (universal IFC file) and the `steel-to-tekla` companion (native Tekla parts via the Open API, needs Tekla open). Triggers also on "export this to IFC", "bake to Tekla/SDS2/Revit", "bake into Tekla".
4
- metadata:
5
- version: 0.1.0
6
- ---
7
-
8
- # Steel from Drawings — read a steel drawing into an interactive 3D model
9
-
10
- ## What this is
11
-
12
- `steel-from-drawings` is a FloLess app that renders an interactive, **licence-free 3D model** of a
13
- steel frame (the `viewer-3d` agent) from a scene that **you, the terminal AI, bake from a drawing**.
14
- The drawing is read **at compose time** with your own vision — there is **no model in the run path
15
- and no AI API key**: the brain is you, in the terminal (the FloLess thin-UI contract). This is the
16
- **bake** strategy (`floless-app-rebake` is the generic version; this skill is the steel-specific one,
17
- and **for `steel-from-drawings` it supersedes the generic re-bake** — you bake the scene INLINE into
18
- the `.flo`'s `view` node, not into `config.yaml`).
19
-
20
- **Bake-out companions.** Two sibling apps consume the SAME scene: **`steel-to-ifc`** writes a
21
- universal **IFC** file (open in Tekla, SDS2, Revit, Navisworks; no Tekla needed), and
22
- **`steel-to-tekla`** creates **native Tekla members** directly via the Open API (needs Tekla open).
23
- When you bake a scene, bake the *identical* scene into whichever of these are installed (loop steps
24
- 6–7) so every output matches the 3D the user confirmed. The whole loop: **read the drawing → see it
25
- in 3D (`steel-from-drawings`) → bake it out (an IFC file and/or native Tekla parts) → finish in your tool.**
26
-
27
- > **Pasted a request?** If the user pastes a message beginning with a `[floless-request type=rebake
28
- > id=…]` marker, that's a request copied from the FloLess Dashboard — resolve it via
29
- > `GET /api/requests` (match the id), don't run the pasted line verbatim, and `DELETE` it when done.
30
-
31
- ## The loop
32
-
33
- 1. **Find the drawing.** From a `rebake` request: `GET http://localhost:<port>/api/requests` → the
34
- entry with `type:"rebake"` and `appId:"steel-from-drawings"` carries `snapshots: ["<abs path>"]`.
35
- (Or the user just attaches/points at a drawing in prose — same procedure, skip the lookup.)
36
- 2. **Read the drawing with your own vision.** Open the file (PDF or image) and READ it. If it's a
37
- large multi-sheet PDF, focus on one framing plan / GA sheet (say which). This is the compose-time
38
- extraction — your vision is the reader, not any API.
39
- 3. **Extract a schematic 3D scene** in the schema below (§ Scene schema), following § Reading a steel
40
- drawing. Aim for a complete, plausible stick model — the user confirms/nudges it in 3D.
41
- 4. **Bake it INLINE into the app.** Replace the entire `scene:` mapping under the `view` node's
42
- `config:` in the app's `.flo` with your extracted scene — in BOTH the installed copy
43
- `~/.aware/apps/steel-from-drawings/steel-from-drawings.flo` AND the editable
44
- `demos/steel-from-drawings/steel-from-drawings.flo` if present. Change ONLY the `scene:` block;
45
- leave `inputs`, `requires`, the node id/agent/command, and everything else identical.
46
- 5. **Recompile.** `aware app compile ~/.aware/apps/steel-from-drawings` → rewrites the `.lock`. The
47
- source-hash changes, so FloLess's Run gate disarms until the user approves. Copy the fresh `.lock`
48
- to `demos/steel-from-drawings/` if you keep the editable copy in sync.
49
- 6. **Bake the SAME scene into `steel-to-ifc` (the IFC bake-out), if it's installed.** Check
50
- `~/.aware/apps/steel-to-ifc/`. If present, replace the `scene:` mapping under that app's `bake-ifc`
51
- node `config` (the `ifc` agent's `scene` input — NOT `config.args`; the app now calls the host-free
52
- `ifc` AWARE agent, not an exec) with the IDENTICAL scene (the installed copy and `demos/steel-to-ifc/`
53
- if present), then `aware app compile ~/.aware/apps/steel-to-ifc`. This keeps the exported IFC matching
54
- the 3D the user confirmed. (`steel-to-ifc` writes a real IFC file — IfcColumn/IfcBeam on the grid —
55
- openable in Tekla, SDS2, Revit or Navisworks; it never touches a Tekla model, so no Tekla needed.)
56
- 7. **Bake the SAME scene into `steel-to-tekla` (native Tekla parts), if it's installed.** Check
57
- `~/.aware/apps/steel-to-tekla/`. If present, replace the `scene:` under that app's `bake-tekla` node
58
- `config` (the `tekla` `bake-scene` verb's `scene` input — NOT `config.args`; the app now calls the
59
- first-class `bake-scene` verb, not an exec) with the IDENTICAL scene (installed + `demos/steel-to-tekla/`), set `config.version`
60
- to the user's RUNNING Tekla version (e.g. "2026.0" — the Open API connects to a version-specific
61
- channel, so a wrong version silently finds no model), then `aware app compile
62
- ~/.aware/apps/steel-to-tekla`. It creates a real Tekla member per element via the Open API (mode:
63
- write, snapshot — Undo reverts the whole bake); needs Tekla OPEN with a model. Profiles fall back
64
- to a sized rectangular section when the environment lacks the drawing's catalogue profile.
65
- 8. **Hand the approval to the user.** Tell them what you read (e.g. "read 6 columns + 8 beams on grids
66
- A–C / 1–2, heights assumed 4.5 m — review in Inspect → Code, then Compile/Approve, then ▶ Run to
67
- see the 3D; run **steel-to-ifc** for an IFC file or **steel-to-tekla** for native Tekla parts").
68
- The human eyeballing the read in 3D **is the safety gate**. Never auto-run.
69
- 9. **Clear the request.** `DELETE http://localhost:<port>/api/requests/<id>` once done (if from one).
70
-
71
- ## Scene schema (the bake target — exactly what `viewer-3d` renders)
72
-
73
- ```yaml
74
- scene:
75
- meta: { name: "<sheet title / drawing no.>", units: mm, up: z }
76
- groups: # one per group you use; drives legend + colour
77
- - { key: column, label: Columns, color: "#60a5fa" }
78
- - { key: beam, label: Beams, color: "#94a3b8" }
79
- # rafter #38bdf8, brace #818cf8 — add others as needed
80
- elements: # one per member, kind: box (a from→to bar)
81
- - { id: COL-A1, group: column, kind: box, from: [x,y,z], to: [x,y,z],
82
- section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", mark: "C1", grid: "A-1" } }
83
- grids: # grid labels placed just outside the framing
84
- - { label: A, at: [x,y,0] }
85
- panels: # generic side tables; build a Takeoff
86
- - title: Takeoff
87
- note: "<assumptions, e.g. heights assumed — plan view, H=4.5 m>"
88
- columns: [Section, "No.", Length]
89
- rows: [["HSS6x6x3/8", 6, "27.0 m"]]
90
- ```
91
-
92
- `from`/`to` are `[x,y,z]` in mm. `meta` is free-form (shown in the click-to-inspect panel) — put
93
- `profile`, `mark`, `grid` there. `section.w`/`d` are visual thickness only.
94
-
95
- ## Reading a steel drawing (the method)
96
-
97
- 1. **Grid.** Find the grid (A,B,C… one way, 1,2,3… the other, plus half-grids). Read the dimension
98
- strings to place grids in mm; if a spacing is unreadable, assume a regular ~6000 mm bay and say so.
99
- 2. **Sheet type & heights.** A PLAN view (top-down) shows no heights — pick one eave/storey height H
100
- (a level/height note if present, else ~4500 mm) and note it; columns run z:0→H, beams/rafters sit
101
- at z:H. An ELEVATION/SECTION gives real heights — read them.
102
- 3. **Members.** One `elements` entry per member, size label exactly as drawn. Columns → `column`,
103
- `from:[gx,gy,0] to:[gx,gy,H]`. Beams/rafters → `beam`/`rafter`, along the grid at z:H. Braces →
104
- `brace`, the diagonal. Put the size in `meta.profile`, the mark in `meta.mark`, the grid ref in
105
- `meta.grid`; approximate `section.w`/`d` from the profile (W12≈300×310, HSS6x6≈150×150, L≈90×90).
106
- 4. **Takeoff.** One row per distinct profile (count + total length in m). Assumptions in the note.
107
- 5. **meta.name** = the sheet title / drawing number. Never invent grids or members the drawing does
108
- not show; if a sheet has no readable steel framing, bake an empty `elements` with a note saying so.
109
-
110
- ## Guardrails
111
-
112
- - **Compose-time only.** The drawing is read HERE, by you, then baked. Never add a runtime node that
113
- reads the drawing — `aware app validate` rejects it and it breaks determinism.
114
- - **Scene, not logic.** Re-bake only the `view.scene` block. A different output (new columns, a
115
- schedule) is a workflow change — use `floless-app-workflows`, not this.
116
- - **Schematic + honest.** ~90% is the goal; the user tidies the last 10% in 3D. State your
117
- assumptions (heights, assumed bays) in the takeoff note. Don't fabricate sizes the drawing lacks.
118
- - **Thin-UI contract.** The browser only recorded intent + the drawing; the brain is you, in the
119
- terminal. The UI never reads the drawing and never bakes.
120
- - **Resolve the port** from the running floless.app the same way the other floless-app skills do.
@@ -1,73 +0,0 @@
1
- app: steel-from-drawings
2
- version: 0.1.0
3
- display-name: Steel from Drawings
4
- description: |
5
- Turn a structural steel drawing into an interactive, licence-free 3D model — no Tekla, no
6
- modelling, no AI API key. Your terminal AI (Claude Code / Codex) reads the drawing once and
7
- bakes a schematic 3D scene into this app; the viewer-3d agent renders it as an interactive
8
- document anyone can orbit, click to inspect, and take off (counts + length by section) — so an
9
- estimator, PM, fabricator or client can understand and price the steel before opening a seat.
10
-
11
- The reading is a COMPOSE-TIME act — the FloLess way: the app is the prompt in your terminal, so
12
- the brain is your AI terminal, never this server, and there is never a model in the run path.
13
- Attach a drawing and use "Re-read & re-bake ▸" (or just ask your terminal AI to read it). The AI
14
- reads the grids, members and sizes and bakes them straight into the `view` node's `scene` below
15
- (the viewer-3d scene schema), recompiles, and asks you to approve; the deterministic run then
16
- only renders the approved scene. Ships with a small example scene so it renders out of the box —
17
- the `floless-app-steel-from-drawings` skill teaches your terminal AI how to read your own drawing in.
18
- exposes-as-agent: false
19
- inputs:
20
- # BAKED at compose time: the terminal AI reads this drawing and writes the `view.scene` block
21
- # below (the run never reads the drawing). read-strategy: bake surfaces "Re-read & re-bake ▸" so
22
- # you can swap the drawing and have your AI re-read it. No runtime input node.
23
- drawing:
24
- type: image
25
- widget: file
26
- accept: [pdf, png, jpg]
27
- read-strategy: bake
28
- description: A structural steel drawing (framing plan / GA / elevation) — baked into the scene below; swap it to re-read & re-bake.
29
- requires:
30
- - viewer-3d@0.1.x
31
- layout: linear
32
- nodes:
33
- # The viewer-3d render node IS the terminal. Ships with a small EXAMPLE scene (a sample 3x2 bay
34
- # frame, schematic) so the app renders out of the box. Re-bake — or the
35
- # floless-app-steel-from-drawings skill — replaces this whole `scene` with a reading of YOUR drawing.
36
- - id: view
37
- agent: viewer-3d
38
- command: render
39
- config:
40
- scene:
41
- meta: { name: "Example — steel framing (sample, not your drawing)", units: mm, up: z }
42
- groups:
43
- - { key: column, label: "Columns (HSS6x6x3/8)", color: "#60a5fa" }
44
- - { key: beam, label: "Beams (W10x33)", color: "#94a3b8" }
45
- elements:
46
- # 6 columns (z 0 -> 4500) on a 3 x 2 grid
47
- - { id: COL-1A, group: column, kind: box, from: [0,0,0], to: [0,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "1-A" } }
48
- - { id: COL-2A, group: column, kind: box, from: [6000,0,0], to: [6000,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "2-A" } }
49
- - { id: COL-3A, group: column, kind: box, from: [12000,0,0], to: [12000,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "3-A" } }
50
- - { id: COL-1B, group: column, kind: box, from: [0,6000,0], to: [0,6000,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "1-B" } }
51
- - { id: COL-2B, group: column, kind: box, from: [6000,6000,0], to: [6000,6000,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "2-B" } }
52
- - { id: COL-3B, group: column, kind: box, from: [12000,6000,0],to: [12000,6000,4500],section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "3-B" } }
53
- # roof beams at z 4500 (W10x33): along each row, then tie beams along each grid line
54
- - { id: BM-12A, group: beam, kind: box, from: [0,0,4500], to: [6000,0,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "A" } }
55
- - { id: BM-23A, group: beam, kind: box, from: [6000,0,4500], to: [12000,0,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "A" } }
56
- - { id: BM-12B, group: beam, kind: box, from: [0,6000,4500], to: [6000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "B" } }
57
- - { id: BM-23B, group: beam, kind: box, from: [6000,6000,4500],to: [12000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "B" } }
58
- - { id: BM-1AB, group: beam, kind: box, from: [0,0,4500], to: [0,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "1" } }
59
- - { id: BM-2AB, group: beam, kind: box, from: [6000,0,4500], to: [6000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "2" } }
60
- - { id: BM-3AB, group: beam, kind: box, from: [12000,0,4500], to: [12000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "3" } }
61
- grids:
62
- - { label: "1", at: [0,-3000,0] }
63
- - { label: "2", at: [6000,-3000,0] }
64
- - { label: "3", at: [12000,-3000,0] }
65
- - { label: "A", at: [-3000,0,0] }
66
- - { label: "B", at: [-3000,6000,0] }
67
- panels:
68
- - title: Takeoff
69
- note: "EXAMPLE — not your drawing. A sample steel frame on a 1-3 x A-B grid; sizes & heights are illustrative. Use 'Re-read & re-bake ▸' (or ask your terminal AI) to read your own drawing."
70
- columns: [Section, "No.", Length]
71
- rows:
72
- - ["HSS6x6x3/8", 6, "27.0 m"]
73
- - ["W10x33", 7, "42.0 m"]