@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.
- package/dist/floless-server.cjs +554 -636
- package/dist/skills/floless-app-steel-takeoff/SKILL.md +15 -1
- package/dist/web/steel-editor.html +69 -11
- package/package.json +1 -1
- package/dist/skills/floless-app-steel-from-drawings/SKILL.md +0 -120
- package/dist/templates/steel-from-drawings.flo +0 -73
- package/dist/templates/steel-to-ifc.flo +0 -294
- package/dist/templates/steel-to-tekla.flo +0 -293
|
@@ -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}
|
|
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" · 1'-0 1/4"" 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)
|
|
907
|
-
: `<div class="sect f">${dotS}Start</div>${tosFld('tosA','TOS',m.ends[0])}${nin('ntA',m.ends[0].note)
|
|
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)
|
|
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
|
-
|
|
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,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"]
|