@floless/app 0.68.0 → 0.69.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 +2 -2
- package/dist/web/app.js +2 -2
- package/dist/web/aware.js +4 -4
- package/dist/web/index.html +2 -2
- package/dist/web/steel-editor.html +204 -129
- package/package.json +2 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -53022,7 +53022,7 @@ function appVersion() {
|
|
|
53022
53022
|
return resolveVersion({
|
|
53023
53023
|
isSea: isSea2(),
|
|
53024
53024
|
sqVersionXml: readSqVersionXml(),
|
|
53025
|
-
define: true ? "0.
|
|
53025
|
+
define: true ? "0.69.0" : void 0,
|
|
53026
53026
|
pkgVersion: readPkgVersion()
|
|
53027
53027
|
});
|
|
53028
53028
|
}
|
|
@@ -53032,7 +53032,7 @@ function resolveChannel(s) {
|
|
|
53032
53032
|
return "dev";
|
|
53033
53033
|
}
|
|
53034
53034
|
function appChannel() {
|
|
53035
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
53035
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.69.0" : void 0 });
|
|
53036
53036
|
}
|
|
53037
53037
|
|
|
53038
53038
|
// workflow-update.ts
|
package/dist/web/app.js
CHANGED
|
@@ -360,8 +360,8 @@ function cardEl(id, ports) {
|
|
|
360
360
|
</div>
|
|
361
361
|
<div class="title">${a.title}</div>
|
|
362
362
|
<div class="subtitle">${a.subtitle}</div>
|
|
363
|
-
${a._runtimeModel ? '<div class="rt-model-badge"
|
|
364
|
-
${a._frozen ? '<div class="frozen-badge"
|
|
363
|
+
${a._runtimeModel ? '<div class="rt-model-badge" data-tip="This node calls a model at run time (vision.extract). Output is content-cached and sits behind an approve gate — review the extraction before any write.">⚡ calls a model at run time</div>' : ''}
|
|
364
|
+
${a._frozen ? '<div class="frozen-badge" data-tip="Frozen — its last result is pinned and Run skips this node. Right-click to unfreeze.">❄ Frozen</div>' : ''}
|
|
365
365
|
<div class="blurb">${a.blurb}</div>
|
|
366
366
|
<div class="footer-row">
|
|
367
367
|
<span class="ports"><span class="ports-num">${ports.in}</span> in <span class="ports-arrow">·</span> <span class="ports-num">${ports.out}</span> out</span>
|
package/dist/web/aware.js
CHANGED
|
@@ -1316,7 +1316,7 @@
|
|
|
1316
1316
|
const v = String(vals[k] || '');
|
|
1317
1317
|
if (!v) return `<span class="ni-pair ni-pair-file ni-pair-file-empty"><span class="ni-key">${escapeHtml(k)}</span><span class="ni-val ni-not-set">not set</span></span>`;
|
|
1318
1318
|
const glyph = /\.pdf$/i.test(v) ? 'pdf' : 'img';
|
|
1319
|
-
return `<span class="ni-pair ni-pair-file"><span class="ni-key">${escapeHtml(k)}</span><span class="ni-file-glyph">${glyph}</span><span class="ni-val ni-file-name"
|
|
1319
|
+
return `<span class="ni-pair ni-pair-file"><span class="ni-key">${escapeHtml(k)}</span><span class="ni-file-glyph">${glyph}</span><span class="ni-val ni-file-name" data-tip="${escapeAttr(v)}">${escapeHtml(baseName(v))}</span></span>`;
|
|
1320
1320
|
}
|
|
1321
1321
|
return `<span class="ni-pair"><span class="ni-key">${escapeHtml(k)}</span><span class="ni-val">${escapeHtml(String(vals[k]))}</span></span>`;
|
|
1322
1322
|
}).join('');
|
|
@@ -2305,7 +2305,7 @@
|
|
|
2305
2305
|
if (f.type === 'current') {
|
|
2306
2306
|
const cext = (/\.([a-z0-9]+)$/i.exec(f.value || '') || [])[1];
|
|
2307
2307
|
const cglyph = String(cext || '').toLowerCase() === 'pdf' ? 'pdf' : 'img';
|
|
2308
|
-
return `<div class="rebake-current"><span class="rebake-current-label">${escapeHtml(f.label || 'Currently loaded')}</span><span class="fm-file-glyph">${cglyph}</span><span class="fm-file-name"
|
|
2308
|
+
return `<div class="rebake-current"><span class="rebake-current-label">${escapeHtml(f.label || 'Currently loaded')}</span><span class="fm-file-glyph">${cglyph}</span><span class="fm-file-name" data-tip="${escapeAttr(f.value)}">${escapeHtml(f.value)}</span></div>`;
|
|
2309
2309
|
}
|
|
2310
2310
|
let ctl;
|
|
2311
2311
|
if (f.type === 'images') {
|
|
@@ -2420,7 +2420,7 @@
|
|
|
2420
2420
|
return;
|
|
2421
2421
|
}
|
|
2422
2422
|
const glyph = st.ext === 'pdf' ? 'pdf' : 'img';
|
|
2423
|
-
drop.innerHTML = `<span class="fm-file-glyph">${glyph}</span><span class="fm-file-name"
|
|
2423
|
+
drop.innerHTML = `<span class="fm-file-glyph">${glyph}</span><span class="fm-file-name" data-tip="${escapeAttr(st.value)}">${escapeHtml(st.name)}</span><span class="fm-file-actions"><button type="button" class="fm-file-replace">Replace</button><button type="button" class="fm-file-clear">Clear</button></span>`;
|
|
2424
2424
|
drop.querySelector('.fm-file-replace').onclick = (e) => { e.stopPropagation(); fileInput.click(); };
|
|
2425
2425
|
drop.querySelector('.fm-file-clear').onclick = (e) => { e.stopPropagation(); st.mode = 'empty'; st.value = ''; st.name = ''; st.ext = ''; render(); };
|
|
2426
2426
|
};
|
|
@@ -2473,7 +2473,7 @@
|
|
|
2473
2473
|
list.hidden = state.length === 0;
|
|
2474
2474
|
list.innerHTML = state.map((s, i) => s.reading
|
|
2475
2475
|
? `<div class="fm-filechip is-reading"><span class="fm-file-glyph">${s.ext === 'pdf' ? 'pdf' : 'img'}</span><span class="fm-filechip-name">Reading…</span></div>`
|
|
2476
|
-
: `<div class="fm-filechip"><span class="fm-file-glyph">${s.ext === 'pdf' ? 'pdf' : 'img'}</span><span class="fm-filechip-name"
|
|
2476
|
+
: `<div class="fm-filechip"><span class="fm-file-glyph">${s.ext === 'pdf' ? 'pdf' : 'img'}</span><span class="fm-filechip-name" data-tip="${escapeAttr(s.name)}">${escapeHtml(s.name)}</span><button type="button" class="fm-filechip-del" data-i="${i}" aria-label="Remove ${escapeAttr(s.name)}">×</button></div>`
|
|
2477
2477
|
).join('');
|
|
2478
2478
|
list.querySelectorAll('.fm-filechip-del').forEach((b) => {
|
|
2479
2479
|
b.onclick = () => {
|
package/dist/web/index.html
CHANGED
|
@@ -375,10 +375,10 @@
|
|
|
375
375
|
(unlike the opaque 3D viewer which uses a data: URL and allow-scripts only). -->
|
|
376
376
|
<div id="contract-editor" class="contract-editor" hidden>
|
|
377
377
|
<div class="contract-editor-bar">
|
|
378
|
-
<button id="contract-editor-close" type="button">✕ Close</button>
|
|
379
378
|
<span id="contract-editor-title" class="contract-editor-title"></span>
|
|
380
379
|
<span style="flex:1"></span>
|
|
381
|
-
<button id="contract-editor-
|
|
380
|
+
<button id="contract-editor-close" type="button" data-tip="Close the editor and go back — your edits are already saved">Close</button>
|
|
381
|
+
<button id="contract-editor-approve" type="button" class="primary" data-tip="Locks in this version of the model so it can be Run. Your edits already auto-save — this is the sign-off, and Run stays disabled until you approve after each change.">✓ Approve</button>
|
|
382
382
|
</div>
|
|
383
383
|
<iframe id="contract-editor-frame" title="Contract editor"></iframe>
|
|
384
384
|
</div>
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font:13px system-ui;height:100vh;display:flex;flex-direction:column}
|
|
13
13
|
header{display:flex;align-items:center;gap:14px;padding:8px 14px;background:var(--panel);border-bottom:1px solid var(--line)}
|
|
14
14
|
header b{font-size:14px} .stat{color:var(--mut)} .stat b{color:var(--text)}
|
|
15
|
+
header .tb-sep{width:1px;height:20px;background:var(--line);flex:0 0 auto;align-self:center} /* thin divider grouping the header action clusters (history | tools | AI | more) */
|
|
16
|
+
#dim3dWrap{gap:8px} /* 3D Dimension cluster in the header — display flips inline-flex/none by view (applyViewState) */
|
|
15
17
|
button{background:#334155;color:var(--text);border:1px solid #475569;border-radius:6px;padding:5px 10px;cursor:pointer;font:13px system-ui}
|
|
16
18
|
button:hover{background:#475569} button.on{background:var(--brand);border-color:var(--brand)}
|
|
17
19
|
button:disabled{opacity:.4;cursor:default;background:#334155}
|
|
@@ -101,6 +103,20 @@
|
|
|
101
103
|
#moreMenu .msnap-hdr .chev{color:var(--mut);font-size:10px;transition:transform .15s}
|
|
102
104
|
#moreMenu .msnap-hdr[aria-expanded=true] .chev{transform:rotate(90deg)}
|
|
103
105
|
#moreMenu .msnap-sect{display:none} #moreMenu .msnap-sect.open{display:block}
|
|
106
|
+
/* Section accordions — every ⋯ section (Snapping, Display, Detailing, …) collapses/expands like Snapping; state persists (wireMoreSections). */
|
|
107
|
+
#moreMenu .msec-hdr{display:flex;align-items:center;justify-content:space-between;font-weight:600;border-top:1px solid var(--line)} /* headers read as headers: semibold + a divider between sections */
|
|
108
|
+
#moreMenu #snapHdr{border-top:0} /* first section — no divider above it */
|
|
109
|
+
#moreMenu .msec-hdr .chev{color:var(--mut);font-size:10px;transition:transform .15s}
|
|
110
|
+
#moreMenu .msec-hdr[aria-expanded=true]{background:rgba(59,130,246,.08);color:var(--brand)} /* the OPEN section's header is brand-tinted so its (indented) items read as "inside" it, not merged with the next section */
|
|
111
|
+
#moreMenu .msec-hdr[aria-expanded=true] .chev{transform:rotate(90deg);color:var(--brand)}
|
|
112
|
+
#moreMenu .msec-hdr[aria-expanded=true]:hover{background:#334155;color:var(--text)} /* keep the normal hover feedback on an open header */
|
|
113
|
+
#moreMenu .msec-body{display:none} #moreMenu .msec-body.open{display:block;padding-bottom:4px}
|
|
114
|
+
#moreMenu .msec-body button,#moreMenu .msec-body .mhint{padding-left:26px} /* indent items so they sit visually UNDER their header */
|
|
115
|
+
/* Insert-detail now lives in the Detailing section; its picker flies out to the LEFT of the ⋯ menu (which hugs the right edge). */
|
|
116
|
+
#moreMenu .m3dwrap.ins-in-menu{display:block}
|
|
117
|
+
#moreMenu #m3dInsert.on{color:var(--brand)}
|
|
118
|
+
#moreMenu #m3dInsertMenu{left:auto;right:calc(100% + 4px);top:0}
|
|
119
|
+
body:not(.v3d) #moreMenu #insWrap{display:none} /* Insert detail places into the 3D scene — hide it in 2D (needs 2 ids to beat the .m3dwrap.ins-in-menu display:block) */
|
|
104
120
|
#moreMenu button.msnap{display:flex;align-items:center;gap:0}
|
|
105
121
|
#moreMenu button.msnap.on{color:var(--text)} /* the switch carries the state — don't also brand the text (reads as an armed tool elsewhere in this menu) */
|
|
106
122
|
#moreMenu .mck,.cmmenu .mck{position:relative;width:26px;height:14px;margin-right:9px;border-radius:7px;border:1px solid var(--line);background:#0b1220;flex:none;transition:background-color .15s,border-color .15s} /* delicate CSS-only slider switch — shared by the ⋯ Snapping rows and the Move/Copy → Drag-to-move/copy toggle */
|
|
@@ -284,12 +300,14 @@
|
|
|
284
300
|
.m3dmenu.open{display:block}
|
|
285
301
|
.m3dmenu button{display:block;width:100%;text-align:left;background:transparent;border:0;border-radius:0;padding:7px 12px;color:var(--text);white-space:nowrap;font-size:12px;box-shadow:none}
|
|
286
302
|
.m3dmenu button:hover{background:#334155}
|
|
303
|
+
.m3dmenu button.on{color:var(--brand)} /* active choice in a radio-style menu (Camera / Display) */
|
|
304
|
+
.m3dmenu button.on::after{content:'✓';float:right;margin-left:16px;color:var(--brand)}
|
|
287
305
|
.m3dmenu button:disabled{opacity:.4;cursor:default;background:transparent}
|
|
288
306
|
.m3dmenu button.mdanger{color:#fca5a5} .m3dmenu button.mdanger:hover{background:#7f1d1d;color:#fecaca}
|
|
289
307
|
.m3dmenu hr{border:0;border-top:1px solid var(--line);margin:4px 0}
|
|
290
308
|
.m3dmenu label{display:flex;align-items:center;gap:7px;padding:7px 12px;color:var(--text);font-size:12px;cursor:pointer;white-space:nowrap}
|
|
291
309
|
.m3dmenu label:hover{background:#334155}
|
|
292
|
-
.m3dmenu label input{margin:0;accent-color:var(--brand);cursor:pointer}
|
|
310
|
+
.m3dmenu label input{margin:0;width:auto;accent-color:var(--brand);cursor:pointer} /* width:auto — don't inherit the global input{width:100%}; keeps the checkbox tight against its label (View / Plane / Work toggles) */
|
|
293
311
|
.m3dmenu .wpprow{display:flex;gap:4px;align-items:center;padding:4px 12px}
|
|
294
312
|
.m3dmenu .wpprow button{width:auto;flex:none;padding:4px 8px;border:1px solid var(--line);border-radius:5px}
|
|
295
313
|
.m3dmenu .wpprow input{width:74px;height:24px;background:var(--bg);color:var(--text);border:1px solid var(--line);border-radius:5px;padding:0 6px;font:12px system-ui}
|
|
@@ -382,22 +400,29 @@
|
|
|
382
400
|
</head><body>
|
|
383
401
|
<header>
|
|
384
402
|
<b>Steel Model</b>
|
|
385
|
-
<div id=viewToggle role=group aria-label="Canvas view"><button id=vt2d class="seg on" aria-pressed=true
|
|
386
|
-
<select id=planSel
|
|
403
|
+
<div id=viewToggle role=group aria-label="Canvas view"><button id=vt2d class="seg on" aria-pressed=true data-tip="Plan view (2D overlay)">2D</button><button id=vt3d class=seg aria-pressed=false data-tip="3D model view">3D</button></div>
|
|
404
|
+
<select id=planSel data-tip="Switch plan view"></select>
|
|
387
405
|
<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>
|
|
388
|
-
<span class=stat id=rfiStat
|
|
389
|
-
<span class=stat id=dupStat
|
|
390
|
-
<span class=stat id=csStat
|
|
391
|
-
<span class=stat id=snapStat style="display:none"
|
|
392
|
-
<span class=stat id=confStat
|
|
393
|
-
<span class=stat id=saveStat
|
|
406
|
+
<span class=stat id=rfiStat data-tip="Click to list members with an unresolved size (RFI — Request for Information)">RFI <b id=rc>0</b></span>
|
|
407
|
+
<span class=stat id=dupStat data-tip="Overlapping/duplicate members (same geometry)">Dup <b id=dpc>0</b></span>
|
|
408
|
+
<span class=stat id=csStat data-tip="Coordinate system — Global by default. Set a local frame from the ⋯ menu for skewed framing.">Axes <b>Global</b></span>
|
|
409
|
+
<span class=stat id=snapStat style="display:none" data-tip="Snap restricted for this operation — right-click the canvas to change; click here or press Esc to clear">Snap <b>—</b></span>
|
|
410
|
+
<span class=stat id=confStat data-tip="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>
|
|
411
|
+
<span class=stat id=saveStat data-tip="Edits auto-save in this browser (localStorage)">Saved</span>
|
|
394
412
|
<span class=stat id=srcStat style="display:none"></span><span style="flex:1"></span>
|
|
395
|
-
<button id=undoB
|
|
396
|
-
<button id=redoB
|
|
397
|
-
<
|
|
398
|
-
<button id=
|
|
413
|
+
<button id=undoB data-tip="Undo (Ctrl+Z)">↶</button>
|
|
414
|
+
<button id=redoB data-tip="Redo (Ctrl+Y / Ctrl+Shift+Z)">↷</button>
|
|
415
|
+
<span class=tb-sep></span>
|
|
416
|
+
<button id=mAdd data-tip="Toggle add-member mode — drag to draw. Shift=ortho, Alt=no snap, right-click=restrict snap to one type">Add member</button>
|
|
417
|
+
<button id=dimB data-tip="Dimension tool (D) — click two snapped points, then a third to place. Default Free (aligned); hold Shift to lock to an axis, X/Y force horizontal/vertical, F free. Right-click the canvas to restrict snapping to one type.">⊢ Dimension</button>
|
|
418
|
+
<!-- 3D Dimension cluster — relocated here from the 3D toolbar so Dimension has ONE home; shown only in 3D (applyViewState). Same ids so steel-3d-view.js reflectDimBar() still drives it. -->
|
|
419
|
+
<span id=dim3dWrap style="display:none;align-items:center;gap:8px">
|
|
420
|
+
<button id=m3dDim data-tip="Measure in 3D — click two snapped points (D); axis Free / X / Y / Z, Alt = vertical; right-click = restrict snap">⊢ Dimension</button>
|
|
421
|
+
<div class=seg-group id=m3dDimAxis style="display:none"><button data-d3axis=free class=on data-tip="Free 3D measurement (F)">Free</button><button data-d3axis=x data-tip="Lock measurement to X (X)">X</button><button data-d3axis=y data-tip="Lock measurement to Y (Y)">Y</button><button data-d3axis=z data-tip="Lock to Z / vertical (Z); Alt = quick vertical">Z</button></div>
|
|
422
|
+
<button id=m3dDimShow data-tip="Show or hide placed 3D dimensions" style="display:none">Hide dims</button>
|
|
423
|
+
</span>
|
|
399
424
|
<div class=cmwrap>
|
|
400
|
-
<button id=xfB aria-haspopup=menu aria-expanded=false
|
|
425
|
+
<button id=xfB aria-haspopup=menu aria-expanded=false data-tip="Move, Copy, array and to-level, plus how left-dragging a member behaves. Move (M) / Copy (C) also arm from the keyboard.">Move / Copy ▾</button>
|
|
401
426
|
<div class=cmmenu id=xfMenu role=menu>
|
|
402
427
|
<div class=mlabel>Move</div>
|
|
403
428
|
<button id=mvTwoB>Move — two points <span class=mkbd>M</span></button>
|
|
@@ -407,52 +432,63 @@
|
|
|
407
432
|
<button id=cpArrB>Copy array…</button>
|
|
408
433
|
<button id=cpLevelB>Copy to level…</button>
|
|
409
434
|
<div class=mlabel>Dragging</div>
|
|
410
|
-
<button id=dragMoveB role=menuitemcheckbox aria-checked=false
|
|
435
|
+
<button id=dragMoveB role=menuitemcheckbox aria-checked=false data-tip="When ON, left-dragging a member moves it and Ctrl+drag copies it. When OFF (default), a drag does neither — a click only selects, so members can't be nudged by accident (the Move and Copy tools still work). Applies to 2D and 3D."><span class=mck aria-hidden=true></span>Drag to move/copy</button>
|
|
411
436
|
</div>
|
|
412
437
|
</div>
|
|
438
|
+
<span class=tb-sep></span>
|
|
413
439
|
<button id=askAiBtn>Ask AI ▸</button>
|
|
440
|
+
<span class=tb-sep></span>
|
|
414
441
|
<div id=moreWrap>
|
|
415
|
-
<button id=moreBtn
|
|
442
|
+
<button id=moreBtn data-tip="More actions" aria-haspopup=menu aria-expanded=false aria-label="More actions">⋯</button>
|
|
416
443
|
<div id=moreMenu role=menu>
|
|
417
|
-
|
|
418
|
-
<
|
|
419
|
-
|
|
420
|
-
<button class="msnap on" data-snap=
|
|
421
|
-
<button class="msnap on" data-snap=
|
|
422
|
-
<button class="msnap on" data-snap=
|
|
423
|
-
<button class="msnap" data-snap=
|
|
444
|
+
<!-- Every section is a collapse/expand accordion (same as Snapping); state persists in localStorage (wireMoreSections). -->
|
|
445
|
+
<button id=snapHdr class="msnap-hdr msec-hdr" data-sec=snap aria-expanded=false aria-controls=snapSect data-tip="Running snaps — click to expand and turn each on/off (also on the snap bar, bottom-right)">Snapping<span class=chev aria-hidden=true>▸</span></button>
|
|
446
|
+
<div id=snapSect class="msnap-sect msec-body">
|
|
447
|
+
<button class="msnap on" data-snap=end role=menuitemcheckbox aria-checked=true data-tip="Snap to member/segment endpoints"><span class=mck aria-hidden=true></span><span class=sg aria-hidden=true>□</span>Endpoint</button>
|
|
448
|
+
<button class="msnap on" data-snap=int role=menuitemcheckbox aria-checked=true data-tip="Snap to where two members/segments cross"><span class=mck aria-hidden=true></span><span class=sg aria-hidden=true>✕</span>Intersection</button>
|
|
449
|
+
<button class="msnap on" data-snap=mid role=menuitemcheckbox aria-checked=true data-tip="Snap to the midpoint of a member/segment"><span class=mck aria-hidden=true></span><span class=sg aria-hidden=true>△</span>Midpoint</button>
|
|
450
|
+
<button class="msnap on" data-snap=line role=menuitemcheckbox aria-checked=true data-tip="Snap to the nearest point on any member/segment line"><span class=mck aria-hidden=true></span><span class=sg aria-hidden=true>⧗</span>Nearest</button>
|
|
451
|
+
<button class="msnap" data-snap=ext role=menuitemcheckbox aria-checked=false data-tip="Snap along the invisible extension of a member/line past its endpoint, with a dashed guide line"><span class=mck aria-hidden=true></span><span class=sg aria-hidden=true>┈</span>Extension</button>
|
|
424
452
|
<div class=mhint>Right-click the canvas any time to force one snap type for a single pick.</div>
|
|
425
453
|
</div>
|
|
454
|
+
<button class=msec-hdr data-sec=display aria-expanded=false data-tip="Show/hide plan layers, and edit grid lines">Display<span class=chev aria-hidden=true>▸</span></button>
|
|
455
|
+
<div class=msec-body>
|
|
456
|
+
<button id=dimToggleB data-tip="Show or hide all placed dimensions on the plan">Hide dimensions</button>
|
|
457
|
+
<button id=calloutToggleB data-tip="Show or hide the clickable callout bubbles (section / elevation / detail references) on the plan">Hide callouts</button>
|
|
458
|
+
<button id=gridToggleB data-tip="Show or hide the grid lines in 2D and 3D">Hide grid</button>
|
|
459
|
+
<button id=gridEditB data-tip="Grid lines — a plan reference with structural bay spacings (n*d repeats a bay). Shows in 2D and 3D; drawing and drags snap to its lines and intersections.">Grid lines…</button>
|
|
460
|
+
</div>
|
|
461
|
+
<button class=msec-hdr data-sec=detailing aria-expanded=false data-tip="Connection details, plates, frames, and inserted detail images">Detailing<span class=chev aria-hidden=true>▸</span></button>
|
|
462
|
+
<div class=msec-body>
|
|
463
|
+
<button id=detailsBtn>Details</button>
|
|
464
|
+
<button id=connBtn data-tip="A lookup table of connection types (moment, shear, pinned, …). Each maps to a design detail and, per platform, a component ID; reference a row from each member end.">Connections</button>
|
|
465
|
+
<div class="m3dwrap ins-in-menu" id=insWrap>
|
|
466
|
+
<button id=m3dInsert data-tip="Insert a 2D detail image into the 3D scene, near a beam (3D view only)">Insert detail…</button>
|
|
467
|
+
<div id=m3dInsertMenu class=m3dmenu role=menu></div>
|
|
468
|
+
<input id=insFile type=file accept="image/*" style="display:none">
|
|
469
|
+
</div>
|
|
470
|
+
<button id=bpBtn data-tip="Auto-detail base plates on every column (a plate + anchor kit + weld, shown in 3D). Tune sizes per the schedule via the AI or per-column params.">Base plates</button>
|
|
471
|
+
<button id=spBtn data-tip="Auto-detail bolted shear (fin) plates on eligible beam ends (a fin plate + bolt group + weld, shown in 3D). Tune sizes per the schedule via the AI or per-end params.">Shear plates</button>
|
|
472
|
+
<button id=framesBtn>Frames</button>
|
|
473
|
+
</div>
|
|
474
|
+
<button class=msec-hdr data-sec=checks aria-expanded=false data-tip="Find and fix modelling problems">Model checks<span class=chev aria-hidden=true>▸</span></button>
|
|
475
|
+
<div class=msec-body>
|
|
476
|
+
<button id=dupB data-tip="Selects overlapping members drawn twice by mistake — review, then press Delete to remove the extra one.">Duplicates</button>
|
|
477
|
+
<button id=revB data-tip="Select beams drawn the wrong way — a mostly-horizontal beam should run left→right, a steep/skew beam bottom→up. Review, then press P to swap their direction.">Reversed beams</button>
|
|
478
|
+
<button id=mrgB data-tip="Joins straight, end-to-end runs of the same profile into one beam. Undoable.">Merge collinear beams</button>
|
|
479
|
+
</div>
|
|
480
|
+
<button class=msec-hdr data-sec=coords aria-expanded=false data-tip="Local vs global drawing axes (for skewed framing)">Coordinate system<span class=chev aria-hidden=true>▸</span></button>
|
|
481
|
+
<div class=msec-body>
|
|
482
|
+
<button id=csSetB data-tip="Define a local X axis by clicking two points (origin, then X-direction). Y follows at 90°. Orthogonal drawing and X/Y dimensions then snap to this frame — for skewed framing.">↺ Set local axes…</button>
|
|
483
|
+
<button id=csResetB data-tip="Return to the global X/Y axes (default)" disabled>Reset to global axes</button>
|
|
484
|
+
</div>
|
|
485
|
+
<button class=msec-hdr data-sec=session aria-expanded=false data-tip="Reload from the server, or export the data">Session<span class=chev aria-hidden=true>▸</span></button>
|
|
486
|
+
<div class=msec-body>
|
|
487
|
+
<button id=reloadB data-tip="Reloads this page and pulls in any changes your terminal AI made since you last opened it.">↻ Reload from server</button>
|
|
488
|
+
<button id=exp>Export contract…</button>
|
|
489
|
+
</div>
|
|
426
490
|
<hr>
|
|
427
|
-
<
|
|
428
|
-
<button id=dimToggleB title="Show or hide all placed dimensions on the plan">Hide dimensions</button>
|
|
429
|
-
<button id=calloutToggleB title="Show or hide the clickable callout bubbles (section / elevation / detail references) on the plan">Hide callouts</button>
|
|
430
|
-
<hr>
|
|
431
|
-
<div class=mlabel>Grid</div>
|
|
432
|
-
<button id=gridEditB title="Grid lines — a plan reference with Tekla-style spacings (n*d repeats a bay). Shows in 2D and 3D; drawing and drags snap to its lines and intersections.">Grid lines…</button>
|
|
433
|
-
<button id=gridToggleB title="Show or hide the grid lines in 2D and 3D">Hide grid</button>
|
|
434
|
-
<hr>
|
|
435
|
-
<div class=mlabel>Coordinate system</div>
|
|
436
|
-
<button id=csSetB title="Define a local X axis by clicking two points (origin, then X-direction). Y follows at 90°. Orthogonal drawing and X/Y dimensions then snap to this frame — for skewed framing.">↺ Set local axes…</button>
|
|
437
|
-
<button id=csResetB title="Return to the global X/Y axes (default)" disabled>Reset to global</button>
|
|
438
|
-
<hr>
|
|
439
|
-
<div class=mlabel>Review</div>
|
|
440
|
-
<button id=dupB title="Select duplicate (overlapping) members — review then Delete to dedupe">Duplicates</button>
|
|
441
|
-
<button id=revB title="Select beams drawn the wrong way — a mostly-horizontal beam should run left→right, a steep/skew beam bottom→up. Review, then press P to swap their direction.">Reversed beams</button>
|
|
442
|
-
<button id=mrgB title="Merge collinear segments — joins same-profile, end-to-end runs into one member. Undoable.">Merge collinear</button>
|
|
443
|
-
<hr>
|
|
444
|
-
<div class=mlabel>Reference</div>
|
|
445
|
-
<button id=detailsBtn>Details</button>
|
|
446
|
-
<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>
|
|
447
|
-
<button id=bpBtn title="Auto-detail base plates on every column (a plate + anchor kit + weld, shown in 3D). Tune sizes per the schedule via the AI or per-column params.">Base plates</button>
|
|
448
|
-
<button id=spBtn title="Auto-detail bolted shear (fin) plates on eligible beam ends (a fin plate + bolt group + weld, shown in 3D). Tune sizes per the schedule via the AI or per-end params.">Shear plates</button>
|
|
449
|
-
<button id=framesBtn>Frames</button>
|
|
450
|
-
<hr>
|
|
451
|
-
<div class=mlabel>Session</div>
|
|
452
|
-
<button id=reloadB title="Reload from server — picks up any AI writebacks">↻ Reload</button>
|
|
453
|
-
<button id=exp>Export contract</button>
|
|
454
|
-
<hr>
|
|
455
|
-
<button id=revertB class=mdanger title="Discard all saved edits and reload the detected contract">Revert</button>
|
|
491
|
+
<button id=revertB class=mdanger data-tip="Throws away every change you've made in this browser and reloads the original AI-read model. This can't be undone.">Revert all edits…</button>
|
|
456
492
|
</div>
|
|
457
493
|
</div>
|
|
458
494
|
</header>
|
|
@@ -461,20 +497,37 @@
|
|
|
461
497
|
<div id=stage><svg id=svg></svg></div>
|
|
462
498
|
<canvas id=stage3d tabindex=0 aria-label="3D model"></canvas>
|
|
463
499
|
<div id=m3dBar role=group aria-label="3D view controls">
|
|
464
|
-
<!-- Camera -->
|
|
465
|
-
<div class=
|
|
500
|
+
<!-- Camera projection — dropdown (like Plane / Work area); the button shows the current mode -->
|
|
501
|
+
<div class=m3dwrap>
|
|
502
|
+
<button id=m3dProjBtn data-tip="Camera projection — perspective, or orthographic (true scale)">Persp ▾</button>
|
|
503
|
+
<div id=m3dProj class=m3dmenu role=menu>
|
|
504
|
+
<button data-proj=persp class=on data-tip="Perspective — natural depth">Perspective</button>
|
|
505
|
+
<button data-proj=ortho data-tip="Orthographic — true scale, no perspective">Orthographic</button>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
466
508
|
<button id=m3dFit data-tip="Fit all to view (Home)">Fit</button>
|
|
509
|
+
<button id=m3dFitSel data-tip="Zoom to selected — frame just the selected members (Alt+Z)">Zoom sel</button>
|
|
467
510
|
<span class=tb-sep></span>
|
|
468
|
-
<!-- Display
|
|
469
|
-
<div class=
|
|
511
|
+
<!-- Display mode — dropdown; the button shows the current mode -->
|
|
512
|
+
<div class=m3dwrap>
|
|
513
|
+
<button id=m3dModeBtn data-tip="Display mode — solid, wireframe, or see-through (X-ray)">Solid ▾</button>
|
|
514
|
+
<div id=m3dMode class=m3dmenu role=menu>
|
|
515
|
+
<button data-mode=solid class=on data-tip="Solid shaded model">Solid</button>
|
|
516
|
+
<button data-mode=wire data-tip="Wireframe — edges only">Wire</button>
|
|
517
|
+
<button data-mode=xray data-tip="See-through — reveal hidden parts">X-ray</button>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
470
520
|
<button id=m3dIso data-tip="Isolate selected — hide everything else (Esc to exit)" style="display:none">Isolate</button>
|
|
471
521
|
<span class=tb-sep></span>
|
|
472
|
-
<!--
|
|
473
|
-
|
|
474
|
-
<
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
522
|
+
<!-- Display toggles: reference lines + mark labels (grouped into a menu, like Plane / Work area).
|
|
523
|
+
The Dimension tool moved to the header so it lives in one place across 2D and 3D. -->
|
|
524
|
+
<div class=m3dwrap>
|
|
525
|
+
<button id=m3dView data-tip="Show or hide reference lines and mark labels on the 3D model">View ▾</button>
|
|
526
|
+
<div id=m3dViewMenu class=m3dmenu role=menu>
|
|
527
|
+
<label data-tip="Show each member's reference line (centreline)"><input type=checkbox id=m3dRef> Ref line</label>
|
|
528
|
+
<label data-tip="Show each member's mark / id label in the 3D view"><input type=checkbox id=m3dLabels> Labels</label>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
478
531
|
<span class=tb-sep></span>
|
|
479
532
|
<!-- Section -->
|
|
480
533
|
<div class=m3dwrap>
|
|
@@ -486,11 +539,6 @@
|
|
|
486
539
|
<button data-clip=clear class=mdanger data-tip="Remove every clip">Clear all clips</button>
|
|
487
540
|
</div>
|
|
488
541
|
</div>
|
|
489
|
-
<div class=m3dwrap>
|
|
490
|
-
<button id=m3dInsert data-tip="Insert a 2D detail image into the 3D scene, near a beam">Insert ▾</button>
|
|
491
|
-
<div id=m3dInsertMenu class=m3dmenu role=menu></div>
|
|
492
|
-
<input id=insFile type=file accept="image/*" style="display:none">
|
|
493
|
-
</div>
|
|
494
542
|
<div class=m3dwrap>
|
|
495
543
|
<button id=m3dWp data-tip="Working plane — every 3D pick (Move/Copy, next: drawing) lands on it. Set it from a face, 3 points, or a principal plane.">◇ Plane ▾</button>
|
|
496
544
|
<div id=m3dWpMenu class=m3dmenu role=menu>
|
|
@@ -521,19 +569,19 @@
|
|
|
521
569
|
<div id=m3dCube data-tip="Click a face for that view · right-drag to orbit"></div>
|
|
522
570
|
<div id=m3dAxes></div>
|
|
523
571
|
<div id=zoombar>
|
|
524
|
-
<button id=zOut
|
|
572
|
+
<button id=zOut data-tip="Zoom out">−</button>
|
|
525
573
|
<input id=zRange type=range min=10 max=400 step=1 value=100>
|
|
526
|
-
<button id=zIn
|
|
574
|
+
<button id=zIn data-tip="Zoom in">+</button>
|
|
527
575
|
<span id=zPct>100%</span>
|
|
528
|
-
<button id=zFit
|
|
576
|
+
<button id=zFit data-tip="Zoom to fit (Home)">Fit</button>
|
|
529
577
|
</div>
|
|
530
578
|
<!-- Quick-access snap toggles (same persistent state as the ⋯ menu → Snapping section; keep the two in sync — same 5 items). -->
|
|
531
579
|
<div id=snapBar role=group aria-label="Snap settings">
|
|
532
|
-
<button data-snap=end
|
|
533
|
-
<button data-snap=int
|
|
534
|
-
<button data-snap=mid
|
|
535
|
-
<button data-snap=line
|
|
536
|
-
<button data-snap=ext
|
|
580
|
+
<button data-snap=end data-tip="Endpoint snap">□</button>
|
|
581
|
+
<button data-snap=int data-tip="Intersection snap">✕</button>
|
|
582
|
+
<button data-snap=mid data-tip="Midpoint snap">△</button>
|
|
583
|
+
<button data-snap=line data-tip="Nearest snap">⧗</button>
|
|
584
|
+
<button data-snap=ext data-tip="Extension snap">┈</button>
|
|
537
585
|
</div>
|
|
538
586
|
</div>
|
|
539
587
|
<aside id=panel></aside>
|
|
@@ -546,7 +594,7 @@
|
|
|
546
594
|
<div id=detailNewForm style="display:none;padding:14px;border-bottom:1px solid var(--line)">
|
|
547
595
|
<label class=elab for=dnName>Detail name</label>
|
|
548
596
|
<input id=dnName placeholder="e.g. 5-S504 or My moment conn." autocomplete=off>
|
|
549
|
-
<div id=dnDrop
|
|
597
|
+
<div id=dnDrop data-tip="Paste or click to choose"><span id=dnDropTxt>Paste a screenshot (Ctrl+V) — or click to choose an image</span><img id=dnPrev alt=""></div>
|
|
550
598
|
<input id=dnFile type=file accept="image/*" hidden>
|
|
551
599
|
<div id=dnBtns><button id=dnCancel class=ghost>Cancel</button><button id=dnAdd>Add detail</button></div>
|
|
552
600
|
<div class=hint id=dnHint>For anything the auto‑detection missed. The image stays on your machine.</div>
|
|
@@ -565,7 +613,7 @@
|
|
|
565
613
|
<div class=mpanel><div class=mhead><b>Unresolved members — RFI</b><button id=rfiClose>✕</button></div>
|
|
566
614
|
<div id=rfiGrid style="padding:14px;overflow:auto"></div></div></div>
|
|
567
615
|
<div id=confModal><div class=mbackdrop id=confBackdrop></div>
|
|
568
|
-
<div class=mpanel><div class=mhead><b>Confidence report</b><label class=conf-tgt
|
|
616
|
+
<div class=mpanel><div class=mhead><b>Confidence report</b><label class=conf-tgt data-tip="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>
|
|
569
617
|
<div id=confCats class=conf-cats></div>
|
|
570
618
|
<div id=confFilter class=conf-filter></div>
|
|
571
619
|
<div id=confBody class=conf-body></div></div></div>
|
|
@@ -705,7 +753,7 @@ function showAiUpdateBanner(message) {
|
|
|
705
753
|
const dismissBtn = document.createElement('button');
|
|
706
754
|
dismissBtn.style.cssText = 'background:transparent;color:#93c5fd;border:1px solid #3b82f6;border-radius:6px;padding:5px 10px;cursor:pointer;font:13px system-ui';
|
|
707
755
|
dismissBtn.textContent = '✕';
|
|
708
|
-
dismissBtn.
|
|
756
|
+
dismissBtn.dataset.tip = 'Dismiss';
|
|
709
757
|
dismissBtn.onclick = () => bar.remove();
|
|
710
758
|
|
|
711
759
|
bar.appendChild(msg);
|
|
@@ -1104,7 +1152,7 @@ function renderGridPanel(p){
|
|
|
1104
1152
|
const nCols=planColumnCount(),hasEls=!!filterElsForPlan();
|
|
1105
1153
|
p.innerHTML=`<span class=badge>Grid lines</span>
|
|
1106
1154
|
<div class=hint style="margin-top:8px">No grid yet. Grid lines are a plan reference — they don't constrain members.</div>
|
|
1107
|
-
<div class=gbtns><button id=gridReadB ${hasEls?'':'disabled'}
|
|
1155
|
+
<div class=gbtns><button id=gridReadB ${hasEls?'':'disabled'} data-tip="${hasEls?'Detect the printed grid lines and bubbles in this sheet’s linework. Undoable.':'Needs this sheet’s filter linework — re-read the drawing with the steel filter step to capture it.'}">From drawing</button><button id=gridAutoB ${nCols<2?'disabled':''} data-tip="${nCols<2?'Needs at least two columns in the model to infer spacing.':'Infer spacings and origin from the column layout. Undoable.'}">Auto from columns</button><button id=gridBlankB data-tip="Start from a 4×4 grid of 30' bays and edit the spacings. Undoable.">Start blank</button></div>
|
|
1108
1156
|
<div class=hint style="margin-top:10px">Spacing uses <b>n*d</b> to repeat (e.g. <b>3*25'</b> = three 25' bays). Tokens: <b>25'-6"</b>, <b>6"</b>, <b>7.5m</b>, bare mm.</div>`;
|
|
1109
1157
|
const rb=document.getElementById('gridReadB');if(rb)rb.onclick=()=>gridReadFromDrawing(null);
|
|
1110
1158
|
const ab=document.getElementById('gridAutoB');if(ab)ab.onclick=()=>{const g=gc.inferGrid(P.members,ppf);if(!g){toast('No columns to infer a grid from');return;}edit(()=>{P.grid=g;});};
|
|
@@ -1127,14 +1175,14 @@ function renderGridPanel(p){
|
|
|
1127
1175
|
<div class=sect style="margin-top:10px">Labels</div>
|
|
1128
1176
|
<div style="display:flex;gap:6px"><input id=gLX class=gin value="${esc(g.labels_x||'')}" placeholder="auto: 1 2 3…" autocomplete=off spellcheck=false><input id=gLY class=gin value="${esc(g.labels_y||'')}" placeholder="auto: A B C…" autocomplete=off spellcheck=false></div>
|
|
1129
1177
|
<div id=gLfb class=gfb></div>
|
|
1130
|
-
${(!g.labels_x&&!g.labels_y)?'<div class=gbtns style="margin-top:6px"><button id=gridLabelsAiB class=ghostw
|
|
1178
|
+
${(!g.labels_x&&!g.labels_y)?'<div class=gbtns style="margin-top:6px"><button id=gridLabelsAiB class=ghostw data-tip="Many drawings outline their bubble text, so the marks can’t be read deterministically. This sends a request to your AI terminal — it reads the printed marks from the source drawing and fills the labels as a contract update.">Ask AI to read the labels ▸</button></div>':''}
|
|
1131
1179
|
<div class=sect style="margin-top:10px">Extension</div>
|
|
1132
1180
|
<input id=gExt class=gin style="width:110px" value="${esc(gc.fmtLen(isFinite(g.ext)?g.ext:1524))}" autocomplete=off spellcheck=false>
|
|
1133
1181
|
<div class=hint style="margin-top:3px">How far every line runs past the outermost, in plan. Drag a bubble <b>along</b> its line to override just that one.</div>
|
|
1134
|
-
${(()=>{const n=Object.keys(g.ends_x||{}).length+Object.keys(g.ends_y||{}).length;return n?`<div class=gfb style="display:flex;align-items:center;gap:8px;margin-top:5px"><span>${n} line${n===1?'':'s'} with a custom extent</span><button id=gridResetExtB class=ghost style="height:22px;padding:0 8px;font-size:11px"
|
|
1135
|
-
<div class=gbtns><button id=gridPickB class="${gridPick?'on':''}"
|
|
1182
|
+
${(()=>{const n=Object.keys(g.ends_x||{}).length+Object.keys(g.ends_y||{}).length;return n?`<div class=gfb style="display:flex;align-items:center;gap:8px;margin-top:5px"><span>${n} line${n===1?'':'s'} with a custom extent</span><button id=gridResetExtB class=ghost style="height:22px;padding:0 8px;font-size:11px" data-tip="Reset every line back to the Extension value above. Undoable.">Reset all</button></div>`:'';})()}
|
|
1183
|
+
<div class=gbtns><button id=gridPickB class="${gridPick?'on':''}" data-tip="Click the grid's 1-A corner on the plan — snaps to member ends. Esc cancels.">Pick origin</button><button id=gridReadB2 ${filterElsForPlan()?'':'disabled'} data-tip="${filterElsForPlan()?'Re-detect the printed grid from this sheet’s linework — keeps your levels and hand-set labels. Undoable.':'Needs this sheet’s filter linework — re-read the drawing with the steel filter step to capture it.'}">From drawing</button><button id=gridAutoB2 ${nCols<2?'disabled':''} data-tip="${nCols<2?'Needs at least two columns in the model to infer spacing.':'Re-infer spacings and origin from the column layout. Undoable.'}">Auto from columns</button></div>
|
|
1136
1184
|
${gridPick?'<div class=hint style="margin-top:6px">Click the grid’s <b>1-A corner</b> on the plan — snaps to member ends (<b>Alt</b> off). <b>Esc</b> cancels.</div>':''}
|
|
1137
|
-
<div class=gbtns style="margin-top:16px"><button id=gridRemoveB class=ghostw style="flex:1"
|
|
1185
|
+
<div class=gbtns style="margin-top:16px"><button id=gridRemoveB class=ghostw style="flex:1" data-tip="Delete all grid lines and levels. Undoable.">Remove grid</button><button id=gridDoneB>Done</button></div>`;
|
|
1138
1186
|
const fbSpacing=(inpId,fbId,style,override,emptyMsg)=>{
|
|
1139
1187
|
const el=document.getElementById(fbId),v=document.getElementById(inpId).value;
|
|
1140
1188
|
const r=gc.parseSpacings(v);
|
|
@@ -1391,7 +1439,7 @@ function panel(){
|
|
|
1391
1439
|
<div class=sect style="margin-top:12px">Direction</div>
|
|
1392
1440
|
<div class=seg2><button id=dimAxFree class="${dimAxis==='free'?'on':''}">Free</button><button id=dimAxX class="${dimAxis==='x'?'on':''}">X</button><button id=dimAxY class="${dimAxis==='y'?'on':''}">Y</button></div>
|
|
1393
1441
|
<div class=sect style="margin-top:12px">Mode</div>
|
|
1394
|
-
<div class=seg2><button id=dimChainB class="${dimChain?'on':''}"
|
|
1442
|
+
<div class=seg2><button id=dimChainB class="${dimChain?'on':''}" data-tip="Chain (C) — keep adding dimensions from the last point; Esc/Enter ends the chain">⛓ Chain</button></div>
|
|
1395
1443
|
<div class=hint style="margin-top:8px">Snaps to grid intersections, member ends, and onto member lines — same as drawing. <b>Alt</b> turns snap off. <b>Esc</b> cancels.</div>`;
|
|
1396
1444
|
document.getElementById('dimAxFree').onclick=()=>{dimSetAxis('free');dimRefreshPrev();panel();};
|
|
1397
1445
|
document.getElementById('dimAxX').onclick=()=>{dimSetAxis('x');dimRefreshPrev();panel();};
|
|
@@ -1402,7 +1450,7 @@ function panel(){
|
|
|
1402
1450
|
p.innerHTML=`<span class=badge>Dimension${selDimIds.size>1?'s · '+selDimIds.size:''}</span>
|
|
1403
1451
|
${one?`<div class=hint style="margin-top:8px">Length <b>${esc(dimValueText(dimGeo(one.a,one.b,one.axis,one.off).px))}</b> · ${({free:'Free (aligned)',x:'X (horizontal)',y:'Y (vertical)'})[one.axis]}</div>`:''}
|
|
1404
1452
|
<div class=sect style="margin-top:12px">Edit</div>
|
|
1405
|
-
<div class=seg2><button id=dimSplitB class="${dimSplitMode?'on':''}"
|
|
1453
|
+
<div class=seg2><button id=dimSplitB class="${dimSplitMode?'on':''}" data-tip="Add split point (S) — click points along the dimension to insert extra dimension points; each click splits the segment under it. Esc ends.">✂ Add split point</button></div>
|
|
1406
1454
|
<div class=hint style="margin-top:8px">${dimSplitMode?'Click points along the dimension to split it — keep clicking to add more. <b>Alt</b> = no snap · <b>Esc</b> ends.':'Drag an end handle to re-measure · <b>Del</b> removes · <b>Esc</b> deselects.'}</div>`;
|
|
1407
1455
|
document.getElementById('dimSplitB').onclick=()=>toggleDimSplit();
|
|
1408
1456
|
return;}
|
|
@@ -1437,7 +1485,7 @@ function panel(){
|
|
|
1437
1485
|
+sec('Orientation')+`<div class=detrow><label class=detf><span>Rotation °</span><input id=detRot inputmode=decimal value="${esc(String(dp.rotZ||0))}" autocomplete=off></label><label class=detf><span>Size</span><input id=detSize inputmode=decimal value="${ftin(dp.size||1000)}" autocomplete=off></label></div>`
|
|
1438
1486
|
+sec('Appearance')+`<div class=detrow style="align-items:center"><span class=detf style="flex:none">Opacity</span><input id=detOpacity type=range min=0 max=100 value="${opPct}"><span id=detOpacityV class=edec style="min-width:36px;text-align:right;font-variant-numeric:tabular-nums">${opPct}%</span></div>`
|
|
1439
1487
|
+`<div class=divrow><hr></div>`
|
|
1440
|
-
+`<div class="row f" style="gap:6px;flex-wrap:wrap"><button class=ghostw id=detAsk
|
|
1488
|
+
+`<div class="row f" style="gap:6px;flex-wrap:wrap"><button class=ghostw id=detAsk data-tip="Record a request for your terminal AI to build/adjust this detail">Ask AI to build this…</button><button class=danger id=detRemove>Remove detail</button></div>`;
|
|
1441
1489
|
p.innerHTML=html;
|
|
1442
1490
|
const find=()=>(C.detail_placements||[]).find(x=>x&&x.id===detId);
|
|
1443
1491
|
const wr=(id,fn)=>{const i=document.getElementById(id);if(i)i.onchange=e=>fn(e.target.value);};
|
|
@@ -1490,12 +1538,12 @@ function panel(){
|
|
|
1490
1538
|
${lbl?`<div class="row" style="margin:3px 0 0;font-size:12px;color:var(--brand);font-variant-numeric:tabular-nums">${esc(lbl)}</div>`:''}
|
|
1491
1539
|
${body}
|
|
1492
1540
|
<div class=divrow><hr></div>
|
|
1493
|
-
<div class="row f"><button class=ghostw id=partEdit
|
|
1541
|
+
<div class="row f"><button class=ghostw id=partEdit data-tip="Select the parent member to edit this connection">✎ Edit on ${esc(j.main)} →</button></div>`;
|
|
1494
1542
|
const eb=document.getElementById('partEdit');if(eb)eb.onclick=()=>{selIds=new Set([j.main]);selDimIds.clear();sel3dDimIds.clear();render();};
|
|
1495
1543
|
return;
|
|
1496
1544
|
}}
|
|
1497
1545
|
const arr=selArr();
|
|
1498
|
-
if(arr.length===0){p.innerHTML='<h3 style="display:flex;justify-content:space-between;align-items:center">Legend'+(Object.keys(C.profile_colors).length?'<button class=ghost id=resetCols style="font-size:10px;padding:1px 6px">reset colours</button>':'')+'</h3><div class=legend>'+profs.filter(pr=>P.members.some(mm=>mm.profile===pr)).map(pr=>`<span><input type=color class=swc data-prof="${esc(pr)}" value="${colorFor(pr)}"
|
|
1546
|
+
if(arr.length===0){p.innerHTML='<h3 style="display:flex;justify-content:space-between;align-items:center">Legend'+(Object.keys(C.profile_colors).length?'<button class=ghost id=resetCols style="font-size:10px;padding:1px 6px">reset colours</button>':'')+'</h3><div class=legend>'+profs.filter(pr=>P.members.some(mm=>mm.profile===pr)).map(pr=>`<span><input type=color class=swc data-prof="${esc(pr)}" value="${colorFor(pr)}" data-tip="Click to recolour ${esc(pr)}">${esc(pr)}</span>`).join('')+'</div><div class=hint style="margin-top:12px">Click a member to edit; <b>Ctrl+click</b> to add/remove; drag an empty area to <b>box-select</b> (Ctrl adds). Drag a selected line to move it (all selected move together) — it snaps onto nearby grid/endpoints; drag an end dot to adjust — also snaps (<b>Alt</b> off). Hold <b>Shift</b> to keep it straight (H/V). <b>Ctrl+D</b> duplicate, <b>Ctrl+Z/Y</b> undo/redo, <b>Del</b> delete, <b>Esc</b> deselect. <b>Ctrl+scroll</b> zoom, <b>middle-drag</b> pan, <b>Home</b> fit. Dashed = RFI (size unresolved, e.g. MF).</div>'+
|
|
1499
1547
|
'<div style="border-top:1px solid var(--line);margin-top:12px;padding-top:12px"><div class=sect>Project defaults</div>'+
|
|
1500
1548
|
'<div class=hint style="margin:0 0 6px;font-size:11px">Level <b>'+esc(fmtFtIn(defaultTOS))+'</b> ('+esc(P.sheet)+'). '+((P.tos_callouts&&P.tos_callouts.length)?'Per-zone T.O. STEEL callouts from the drawing applied to each member; ':'')+'ends ticked <b>default</b> follow the level.</div>'+
|
|
1501
1549
|
'<div class=elab>Default TOS</div><input id=defTos inputmode=decimal placeholder="5 3/4" · 1'-0 1/4"" value="'+esc(fmtFtIn(defaultTOS))+'"><span class=edec>'+esc(fmtDecIn(defaultTOS))+'</span>'+
|
|
@@ -1537,7 +1585,7 @@ function panel(){
|
|
|
1537
1585
|
<div class="row hint" style="margin-top:0">Edits apply to <b>all ${arr.length}</b> selected · a blank <b>Varies</b> field is left unchanged.</div>
|
|
1538
1586
|
<div class=row><label>Profile</label><input id=pf class=combo data-src=profiles placeholder="${VV(profAgg)?'Varies':''}" value="${VV(profAgg)?'':esc(profAgg||'')}" autocomplete=off></div>
|
|
1539
1587
|
<div class="seg2 f"><button id=rBeam class="${allBeam?'on':''}">Beam</button><button id=rCol class="${allCol?'on':''}">Column</button></div>
|
|
1540
|
-
<div class="seg2 mtype" id=mTypeM style="margin-top:6px"
|
|
1588
|
+
<div class="seg2 mtype" id=mTypeM style="margin-top:6px" data-tip="Set member type for all selected — drives legend grouping">${MEMBER_TYPES.map(t=>`<button data-mtype="${t.k}" class="${arr.every(x=>memberTypeOf(x)===t.k)?'on':''}">${t.label}</button>`).join('')}</div>
|
|
1541
1589
|
${(!allBeam&&!allCol)?`<div class=hint style="margin-top:4px">Mixed — ${beams.length} beam${beams.length>1?'s':''}, ${cols.length} column${cols.length>1?'s':''}</div>`:''}
|
|
1542
1590
|
<div class="row hint">Total length <b>${totalL} ft</b>${totalW!=null?` · <b>${totalW.toLocaleString()} lb</b>`:''}${dupSel?` · <span style="color:#fca5a5">${dupSel} duplicate${dupSel>1?'s':''}</span> (overlap a kept member — deleting these dedupes)`:''}</div>
|
|
1543
1591
|
<div class=row><button class=ghostw id=verifyBtn${allVerified?' style="border-color:#166534;color:#86efac"':''}>${allVerified?'✓ All verified — click to unverify':'Mark all verified'}</button></div>
|
|
@@ -1548,7 +1596,7 @@ function panel(){
|
|
|
1548
1596
|
<div class=divrow><hr><span class=sect style="margin:0">Modify geometry</span><hr></div>
|
|
1549
1597
|
<div class=seg2 style="margin-top:0"><button id=geoEL class="${geoMode==='el'?'on':''}">Extend / Trim all</button></div>
|
|
1550
1598
|
<div class=hint style="margin-top:6px">${geoMode==='el'?'Click a <b>target line</b> — the nearest end of <b>every</b> selected member snaps to where it meets that line.':'<b>Extend/Trim</b> (E) every selected member\'s nearest end to one line. <b>Esc</b> cancels.'}</div>
|
|
1551
|
-
<div class=row><button class=ghostw id=swapEnds
|
|
1599
|
+
<div class=row><button class=ghostw id=swapEnds data-tip="Reverse every selected member: swap each one's start and end (yellow ↔ magenta / bottom ↔ top) handles · Shortcut: P">⇄ Swap start ↔ end (all) <span style="opacity:.55">(P)</span></button></div>
|
|
1552
1600
|
<div class="row f"><button class=danger id=del>Delete selected (${arr.length})</button></div>`;
|
|
1553
1601
|
// wiring — every commit applies to the whole selection; a blank field is a no-op (leaves each member's own value).
|
|
1554
1602
|
document.getElementById('pf').onchange=e=>{const v=e.target.value.toUpperCase().replace(/ /g,'');if(!v)return;edit(()=>{for(const m of selArr()){m.profile=v;m.rfi=(_wt(v)==null);}if(!profs.includes(v)){profs.push(v);profs.sort();}});};
|
|
@@ -1581,7 +1629,7 @@ function panel(){
|
|
|
1581
1629
|
const tosFld=(id,label,o)=>{const def=o.tosDef!==false,v=def?defaultTOS:o.tos;
|
|
1582
1630
|
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>`;};
|
|
1583
1631
|
const nin=(id,v)=>`<input id=${id} class="f combo" data-src=conntypes placeholder="connection / note" value="${esc(v||'')}" autocomplete=off>`;
|
|
1584
|
-
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
|
|
1632
|
+
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 data-tip="Open detail ${esc(o.detail)}">⤢</button>`:''}<button id=${id}_pk class="ghost${(picking&&pickKind==='detail'&&pickEnd===o)?' on':''}" data-tip="Pick a detail callout from the drawing">⌖</button></div>`;};
|
|
1585
1633
|
// Per-end connection-library picker (sets o.conn = a connections[] row id). When set, the row's
|
|
1586
1634
|
// type/detail# are the source of truth (shown read-only below); the free-text note/detail dim to a
|
|
1587
1635
|
// fallback. Readonly combo → pick only, no free typing; the connrows source shows every row.
|
|
@@ -1603,7 +1651,7 @@ function panel(){
|
|
|
1603
1651
|
${pFld('plateWidth','Plate width "N"','auto','mm')}${pFld('plateDepth','Plate depth "B"','auto','mm')}${pFld('thickness','Thickness','1"','mm')}${pFld('weldLeg','Weld leg','5/16"','mm')}
|
|
1604
1652
|
<div class=elab style="margin-top:7px;opacity:.7">Anchor kit</div>
|
|
1605
1653
|
${pFld('boltCols','Bolt columns','2','')}${pFld('boltRows','Bolt rows','2','')}${pFld('boltDia','Bolt ⌀','1"','mm')}${pFld('embedment','Embedment','auto','mm')}${pFld('grout','Grout','auto','mm')}
|
|
1606
|
-
<div class="row f"><button class="ghostw" id=bpRemove
|
|
1654
|
+
<div class="row f"><button class="ghostw" id=bpRemove data-tip="Delete this column's base plate" style="color:#fca5a5;border-color:#7f1d1d">Remove base plate</button></div>`:'';
|
|
1607
1655
|
// This BEAM's shear-plate joints — one params block per detailed END (start/end). Mirrors bpSect but
|
|
1608
1656
|
// per-end (a beam can be detailed at both ends), and adds the clearance + web-side + stiffener controls.
|
|
1609
1657
|
const spjs=col?[]:[0,1].map(e=>({e,j:(C.joints||[]).find(j=>j&&j.kind==='shear-plate'&&j.main===m.id&&j.at==='end'+e)})).filter(x=>x.j);
|
|
@@ -1625,16 +1673,16 @@ function panel(){
|
|
|
1625
1673
|
<div class=seg2 style="margin-top:0"><button id="spg_e${e}_325" class="${(j.params&&j.params.boltGrade==='A490')?'':'on'}">A325</button><button id="spg_e${e}_490" class="${(j.params&&j.params.boltGrade==='A490')?'on':''}">A490</button></div>
|
|
1626
1674
|
<div class=hint style="margin:4px 0 0">Bolt length auto-sizes from the grip (AISC) → shown on the bolt callout.</div>
|
|
1627
1675
|
<div class=elabrow><span class=elab>Opposite stiffener</span><label class=defck><input type=checkbox id="spck_e${e}"${st?' checked':''}>add</label></div>
|
|
1628
|
-
<div class="row f"><button class="ghostw" id="sprm_e${e}"
|
|
1676
|
+
<div class="row f"><button class="ghostw" id="sprm_e${e}" data-tip="Delete this end's shear plate" style="color:#fca5a5;border-color:#7f1d1d">Remove shear plate</button></div>`;};
|
|
1629
1677
|
const spSect=spjs.length?`<div class=hint style="margin:8px 0 0">${spAllAuto?'Auto-added — millimetres; empty = engine default. <b>Clearance</b> = the gap (≈½–¾") so the beam clears the support; <b>Web side</b> picks which face of the web the plate laps. Clear all via <b>Clear shear plates</b> in the toolbar (AI-tuned plates are kept).':'Tune this end's shear plate — millimetres; empty = engine default. <b>Clearance</b> = the gap (≈½–¾") so the beam clears the support; <b>Web side</b> picks which face of the web the plate laps.'}</div>${spjs.map(spBlock).join('')}`:'';
|
|
1630
1678
|
const _cps=(!col&&copesByMember[m.id])?copesByMember[m.id]:[]; // auto cope(s) on this beam (from the rendered scene)
|
|
1631
1679
|
const copeSect=_cps.length?`<div class=divrow><hr><span class=sect style="margin:0">Cope (auto)</span><hr></div><div class=hint style="margin:0 0 2px">${_cps.map(esc).join(' · ')} — auto, clash-driven so the beam clears the support.</div>`:'';
|
|
1632
1680
|
p.innerHTML=`<h3>Member ${esc(m.id)}</h3>
|
|
1633
|
-
<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':''}"
|
|
1681
|
+
<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':''}" data-tip="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>
|
|
1634
1682
|
<div class="seg2 f"><button id=rBeam class="${col?'':'on'}">Beam</button><button id=rCol class="${col?'on':''}">Column</button></div>
|
|
1635
|
-
<div class="seg2 mtype" id=mTypeS style="margin-top:6px"
|
|
1683
|
+
<div class="seg2 mtype" id=mTypeS style="margin-top:6px" data-tip="Member type — drives legend grouping (independent of the Beam/Column geometry above)">${MEMBER_TYPES.map(t=>`<button data-mtype="${t.k}" class="${memberTypeOf(m)===t.k?'on':''}">${t.label}</button>`).join('')}</div>
|
|
1636
1684
|
<div class="row hint">Length <b>${L} ft</b> · ${wpf==null?'<span class=pill style="background:#7f1d1d">RFI — size unresolved</span>':'Weight <b>'+(len(m.wp[0],m.wp[1])/FT*wpf).toFixed(0)+' lb</b> · '+wpf+' lb/ft'}</div>
|
|
1637
|
-
<div class=row><button class=ghostw id=verifyBtn${m.verified?' style="border-color:#166534;color:#86efac"':''}
|
|
1685
|
+
<div class=row><button class=ghostw id=verifyBtn${m.verified?' style="border-color:#166534;color:#86efac"':''} data-tip="Mark this member human-confirmed → 100% in the confidence report">${m.verified?'✓ Verified — human-confirmed':'Mark verified'}</button></div>
|
|
1638
1686
|
${mfSug.length?`<div class="row" style="border:1px solid #a855f7;border-radius:6px;padding:7px 8px;background:rgba(168,85,247,.07)"><div class=elab style="color:#c4b5fd;margin:0">Moment-frame girder · ${_lvl==='roof'?'roof':'2nd floor'} (from Frames)</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-top:5px">${mfSug.map(s=>`<button class="ghost mfsug${s===m.profile?' on':''}" data-s="${esc(s)}">${esc(s)}</button>`).join('')}</div></div>`:''}
|
|
1639
1687
|
${elev}
|
|
1640
1688
|
${bpSect}${spSect}${copeSect}
|
|
@@ -1644,7 +1692,7 @@ function panel(){
|
|
|
1644
1692
|
<div class=divrow><hr><span class=sect style="margin:0">Modify geometry</span><hr></div>
|
|
1645
1693
|
<div class=seg2 style="margin-top:0"><button id=geoEL class="${geoMode==='el'?'on':''}">Extend / Trim</button><button id=geoSplit class="${geoMode==='split'?'on':''}">Split</button></div>
|
|
1646
1694
|
<div class=hint style="margin-top:6px">${geoMode==='el'?'Click a <b>target line</b> — another member or a grey segment. The nearest end of this member snaps to where the two lines meet (extends if short, trims if it overshoots).':geoMode==='split'?'Click a <b>point on this member</b> to cut it into two members.':'<b>Extend/Trim</b> (E) an end to meet another line · <b>Split</b> (S) at a point. <b>Esc</b> cancels.'}</div>
|
|
1647
|
-
<div class=row><button class=ghostw id=swapEnds
|
|
1695
|
+
<div class=row><button class=ghostw id=swapEnds data-tip="Reverse the member: swap the start (${col?'bottom':'yellow'}) and end (${col?'top':'magenta'}) handles · Shortcut: P">⇄ Swap start ↔ end <span style="opacity:.55">(P)</span></button></div>
|
|
1648
1696
|
<div class="row f"><button class=danger id=del>Delete member</button></div>`;
|
|
1649
1697
|
document.getElementById('pf').onchange=e=>edit(()=>{const v=e.target.value.toUpperCase().replace(/ /g,'');m.profile=v;m.rfi=(_wt(v)==null);if(v&&!profs.includes(v)){profs.push(v);profs.sort();}});
|
|
1650
1698
|
document.getElementById('pickProf').onclick=()=>{if(picking&&pickKind==='profile'){picking=false;}else{picking=true;pickKind='profile';pickEnd=null;}render();};
|
|
@@ -1876,9 +1924,9 @@ function propPopOpen(){const el=document.getElementById('propPop');return !!(el&
|
|
|
1876
1924
|
function propPopEl(){let el=document.getElementById('propPop');if(el)return el;
|
|
1877
1925
|
el=document.createElement('div');el.id='propPop';el.setAttribute('role','dialog');el.setAttribute('aria-label','Member properties');
|
|
1878
1926
|
el.innerHTML=`<div class=pph><b id=ppTitle>Properties</b><span class=pcount id=ppLabeled></span>`
|
|
1879
|
-
+`<button class=pin id=ppPin
|
|
1880
|
-
+`<button id=ppClear
|
|
1881
|
-
+`<button id=ppClose
|
|
1927
|
+
+`<button class=pin id=ppPin data-tip="Pin — keep open across selections">📌</button>`
|
|
1928
|
+
+`<button id=ppClear data-tip="Uncheck every label">Clear all</button>`
|
|
1929
|
+
+`<button id=ppClose data-tip="Close (Esc)">✕</button></div>`
|
|
1882
1930
|
+`<div class=ppsearch><input id=ppSearch placeholder="Search properties…" autocomplete=off aria-label="Search properties"></div>`
|
|
1883
1931
|
+`<div class=ppmeta id=ppMeta></div><div class=ppscope id=ppScope></div>`
|
|
1884
1932
|
+`<div class=pplist id=ppList></div>`
|
|
@@ -1918,7 +1966,7 @@ function renderPropPop(){const el=document.getElementById('propPop');if(!el||!el
|
|
|
1918
1966
|
if(q&&!def.label.toLowerCase().includes(q))return '';
|
|
1919
1967
|
shown++;const r=propAggRow(def,arr),checked=pl.props.includes(def.key),dis=r.state==='na';
|
|
1920
1968
|
const val=r.state==='val'?esc(r.text):r.state==='varies'?'Varies':'—',vc=r.state==='varies'?' varies':'';
|
|
1921
|
-
return `<div class="pprow${dis?' dis':''}" data-k="${def.key}"${dis?'
|
|
1969
|
+
return `<div class="pprow${dis?' dis':''}" data-k="${def.key}"${dis?' data-tip="Nothing to label — no value in the selection"':''}>`
|
|
1922
1970
|
+`<input type=checkbox ${checked?'checked':''}${dis?' disabled':''} aria-label="${esc(def.label)}">`
|
|
1923
1971
|
+`<span class=pn>${esc(def.label)}</span><span class="pv${vc}">${val}</span></div>`;}).join('');
|
|
1924
1972
|
el.querySelector('#ppList').innerHTML=rows||'<div class=ppempty>No properties match your search.</div>';
|
|
@@ -2284,7 +2332,7 @@ function axisGlyphSvg(o,dir,preview){
|
|
|
2284
2332
|
+tag(lx,'#ef4444','X')+tag(ly,'#22c55e','Y')+`</g>`;}
|
|
2285
2333
|
// header chip + the More-menu "Reset to global" enabled state — reflect the current frame.
|
|
2286
2334
|
function updCS(){const s=document.getElementById('csStat');
|
|
2287
|
-
if(s){const f=P&&P.frame;s.innerHTML='Axes <b>'+(f?(Math.round(Math.atan2(f.u[1],f.u[0])*180/Math.PI)+'° local'):'Global')+'</b>';s.classList.toggle('local',!!f);s.
|
|
2335
|
+
if(s){const f=P&&P.frame;s.innerHTML='Axes <b>'+(f?(Math.round(Math.atan2(f.u[1],f.u[0])*180/Math.PI)+'° local'):'Global')+'</b>';s.classList.toggle('local',!!f);s.dataset.tip=f?'Local coordinate frame active — ortho draw & X/Y dimensions follow it. Reset via the ⋯ menu.':'Global axes (default). Set a local frame from the ⋯ menu for skewed framing.';}
|
|
2288
2336
|
const r=document.getElementById('csResetB');if(r)r.disabled=!(P&&P.frame);}
|
|
2289
2337
|
// themed transient toast (baseline tokens — never a native alert)
|
|
2290
2338
|
function toast(msg){let t=document.getElementById('toast');
|
|
@@ -2567,10 +2615,13 @@ function moreOpen(){return moreMenu.classList.contains('open');}
|
|
|
2567
2615
|
function moreOutside(e){if(!moreMenu.contains(e.target)&&e.target!==moreBtn)closeMore();}
|
|
2568
2616
|
function closeMore(){moreMenu.classList.remove('open');moreBtn.setAttribute('aria-expanded','false');document.removeEventListener('mousedown',moreOutside,true);}
|
|
2569
2617
|
moreBtn.onclick=e=>{e.stopPropagation();if(moreOpen())closeMore();else{moreMenu.classList.add('open');moreBtn.setAttribute('aria-expanded','true');document.addEventListener('mousedown',moreOutside,true);}};
|
|
2570
|
-
moreMenu.addEventListener('click',e=>{if(e.target.closest('button')&&!e.target.closest('.msnap')&&!e.target.closest('.
|
|
2618
|
+
moreMenu.addEventListener('click',e=>{if(e.target.closest('button')&&!e.target.closest('.msnap')&&!e.target.closest('.msec-hdr')&&!e.target.closest('.ins-in-menu'))closeMore();}); // an item's own handler runs (bubble) before this closes the menu; the snap toggles, section headers, and the Insert picker keep the menu open (settings, not one-shot actions)
|
|
2571
2619
|
// "Snapping" is collapsible to save menu space — the header expands the running-snap switches below it
|
|
2572
|
-
|
|
2573
|
-
|
|
2620
|
+
// Every ⋯ section is a collapse/expand accordion (Snapping + Display + Detailing + …); state persists in localStorage.
|
|
2621
|
+
{const SEC_KEY='steelMoreSections';let openSecs;try{openSecs=new Set(JSON.parse(localStorage.getItem(SEC_KEY)||'[]'));}catch(e){openSecs=new Set();}
|
|
2622
|
+
function setSec(hdr,open){const body=hdr.nextElementSibling;if(!body)return;body.classList.toggle('open',open);hdr.setAttribute('aria-expanded',String(open));const k=hdr.dataset.sec;if(k){open?openSecs.add(k):openSecs.delete(k);try{localStorage.setItem(SEC_KEY,JSON.stringify([...openSecs]));}catch(e){}}}
|
|
2623
|
+
document.querySelectorAll('#moreMenu .msec-hdr').forEach(hdr=>{setSec(hdr,openSecs.has(hdr.dataset.sec)); // restore
|
|
2624
|
+
hdr.onclick=e=>{e.stopPropagation();setSec(hdr,!hdr.nextElementSibling.classList.contains('open'));};});}
|
|
2574
2625
|
// --- Running-snaps: one source of truth (snapEnabled) rendered onto TWO surfaces — the ⋯ menu switches
|
|
2575
2626
|
// (aria-checked) + the quick-access snap bar (aria-pressed). toggleSnap() is the only writer; both surfaces
|
|
2576
2627
|
// call it, so they can't drift. The transient right-click override is separate and never writes here. ---
|
|
@@ -2677,7 +2728,7 @@ const view3dApi={
|
|
|
2677
2728
|
onClipsChange:()=>{build3DLegend();}, // a clip added / removed / toggled → rebuild the legend's Clip section
|
|
2678
2729
|
beginClipEdit:()=>pushUndo(snapshot()), // a clip / work-area manipulation → push a pre-edit snapshot so Ctrl+Z/Y restores it
|
|
2679
2730
|
onClipModeChange:(m)=>{const b=document.getElementById('m3dClip');if(b){b.classList.toggle('on',!!m);b.textContent=m?'Clip ✕':'Clip ▾';}}, // armed → button fills brand-blue + becomes a cancel target (✕ = cancel)
|
|
2680
|
-
onInsertModeChange:(on)=>{const b=document.getElementById('m3dInsert');if(b){b.classList.toggle('on',!!on);b.textContent=on?'
|
|
2731
|
+
onInsertModeChange:(on)=>{const b=document.getElementById('m3dInsert');if(b){b.classList.toggle('on',!!on);b.textContent=on?'✕ Cancel insert':'Insert detail…';}}, // armed → cancel target
|
|
2681
2732
|
onInsertPlace:(pick,pending)=>{if(!pending||!pending.name){toast('Pick a detail to insert first');return;} // Slice 4: place the queued detail at the pick, select it, record the create intent
|
|
2682
2733
|
const id='det'+Date.now().toString(36)+Math.floor(Math.random()*1e4).toString(36);const sheet=(P&&P.sheet)||'';
|
|
2683
2734
|
const place={id,detailName:pending.name,sheet,anchorId:pick.anchorId||null,pos:pick.point,u:pick.u,n:pick.n,rotZ:0,size:1000,opacity:1};
|
|
@@ -2814,7 +2865,7 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
2814
2865
|
if(!groups.length){host.style.display='none';return;}
|
|
2815
2866
|
const hint=document.createElement('div');hint.className='lhint';hint.textContent='click hide/show · dbl-click isolate · Ctrl/Shift multi';host.appendChild(hint);
|
|
2816
2867
|
const addRow=(g,indent,draggable)=>{const row=document.createElement('div');row.className='lrow'+(indent?' typed':'');row.dataset.key=g.key;
|
|
2817
|
-
if(draggable){const dh=document.createElement('span');dh.className='drag-handle';dh.textContent='⠿';dh.
|
|
2868
|
+
if(draggable){const dh=document.createElement('span');dh.className='drag-handle';dh.textContent='⠿';dh.dataset.tip='Drag onto another type';['click','dblclick'].forEach(ev=>dh.addEventListener(ev,e=>e.stopPropagation()));row.appendChild(dh);} // handle = the only drag initiator; swallow its own clicks so it never toggles the row
|
|
2818
2869
|
const sw=document.createElement('span');sw.className='lsw';sw.style.background=g.color;
|
|
2819
2870
|
row.append(sw,document.createTextNode(g.label));
|
|
2820
2871
|
row.addEventListener('click',()=>{if(row._dragging)return;clearTimeout(leg3dClickT);leg3dClickT=setTimeout(()=>{window.Steel3DView.toggleGroup(g.key);refresh3DLegend();},220);});
|
|
@@ -2825,7 +2876,7 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
2825
2876
|
// getState()→'on'|'off'|'mixed' drives the master glyph; onToggle() runs the master action (refresh follows).
|
|
2826
2877
|
const buildCatHeader=(cat,label,count,opts)=>{opts=opts||{};const hdr=document.createElement('div');hdr.className='cat-hdr'+(opts.empty?' empty':'')+(opts.sub?' sub':'');hdr.dataset.cat=cat;hdr._getState=opts.getState;
|
|
2827
2878
|
const chev=Object.assign(document.createElement('span'),{className:'cat-chevron',textContent:collapsedCats.has(cat)?'▶':'▼'});
|
|
2828
|
-
const tog=Object.assign(document.createElement('span'),{className:'cat-tog'
|
|
2879
|
+
const tog=Object.assign(document.createElement('span'),{className:'cat-tog'});tog.dataset.tip=opts.toggleTitle||('Show / hide all '+label.toLowerCase());if(opts.empty||!opts.onToggle)tog.style.display='none';
|
|
2829
2880
|
const lab=Object.assign(document.createElement('span'),{className:'cat-label',textContent:label});
|
|
2830
2881
|
const cnt=Object.assign(document.createElement('span'),{className:'cat-count',textContent:'('+count+')'});
|
|
2831
2882
|
hdr.append(chev,tog,lab,cnt);
|
|
@@ -2903,7 +2954,7 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
2903
2954
|
// the grid's colour, not the cyan dim overlays.
|
|
2904
2955
|
if(typeof P!=='undefined'&&P&&P.grid){
|
|
2905
2956
|
host.appendChild(Object.assign(document.createElement('div'),{className:'ldiv'}));
|
|
2906
|
-
const grow=document.createElement('div');grow.className='lrow dim';grow.
|
|
2957
|
+
const grow=document.createElement('div');grow.className='lrow dim';grow.dataset.tip='Show / hide the structural grid (2D + 3D)';
|
|
2907
2958
|
const gsw=document.createElement('span');gsw.className='lsw';gsw.style.borderColor='#64748b';
|
|
2908
2959
|
grow.append(gsw,document.createTextNode('Grid lines'));
|
|
2909
2960
|
grow.classList.toggle('dimoff',!gridOn());
|
|
@@ -2923,10 +2974,10 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
2923
2974
|
else for(const c of clips){
|
|
2924
2975
|
// Three separate zones (no fighting): swatch/label = SELECT (reveals its 3D drag handles), On/Off pill = ENABLE, × = DELETE.
|
|
2925
2976
|
const row=document.createElement('div');row.className='lrow clip typed'+(c.selected?' sel':''); // enable state is shown by the On/Off pill, not by dimming the row
|
|
2926
|
-
const sw=document.createElement('span');sw.className='lsw';sw.style.background=c.kind==='box'?'#93c5fd':'#3b82f6';sw.
|
|
2927
|
-
const lab=document.createElement('span');lab.className='clab';lab.textContent=c.label;lab.
|
|
2928
|
-
const tog=document.createElement('button');tog.className='cpill'+(c.enabled?' on':'');tog.textContent=c.enabled?'On':'Off';tog.
|
|
2929
|
-
const x=document.createElement('span');x.className='lx';x.textContent='×';x.
|
|
2977
|
+
const sw=document.createElement('span');sw.className='lsw';sw.style.background=c.kind==='box'?'#93c5fd':'#3b82f6';sw.dataset.tip='Select — show its drag handles in 3D'; // box = lighter blue, plane = brand blue
|
|
2978
|
+
const lab=document.createElement('span');lab.className='clab';lab.textContent=c.label;lab.dataset.tip='Click to select · double-click to rename';
|
|
2979
|
+
const tog=document.createElement('button');tog.className='cpill'+(c.enabled?' on':'');tog.textContent=c.enabled?'On':'Off';tog.dataset.tip='Enable / disable this clip';
|
|
2980
|
+
const x=document.createElement('span');x.className='lx';x.textContent='×';x.dataset.tip='Delete this clip';
|
|
2930
2981
|
row.append(sw,lab,tog,x);
|
|
2931
2982
|
sw.addEventListener('click',e=>{e.stopPropagation();clipSelect(c.id,e);}); // Ctrl/Shift = multi-select (same as parts/dims)
|
|
2932
2983
|
let clipClickT=null;
|
|
@@ -2960,11 +3011,24 @@ function refresh3DLegend(){if(!window.Steel3DView)return;const st=window.Steel3D
|
|
|
2960
3011
|
document.querySelectorAll('#m3dLegend .cat-hdr').forEach(updateCatTog);} // refresh the type-category master toggles too
|
|
2961
3012
|
let bar3dWired=false;
|
|
2962
3013
|
function seg3dActive(sel,attr,val){document.querySelectorAll(sel+' button').forEach(b=>b.classList.toggle('on',b.getAttribute(attr)===val));}
|
|
3014
|
+
// Reflect the live projection / display mode into the Camera + Display dropdowns: tick the active menu item AND label the trigger button, so the current mode shows without opening the menu.
|
|
3015
|
+
function reflectProj(){const V=window.Steel3DView;if(!V)return;const p=V.projection();seg3dActive('#m3dProj','data-proj',p);const b=document.getElementById('m3dProjBtn');if(b)b.textContent=(p==='ortho'?'Ortho':'Persp')+' ▾';}
|
|
3016
|
+
function reflectMode(){const V=window.Steel3DView;if(!V)return;const m=V.mode();seg3dActive('#m3dMode','data-mode',m);const b=document.getElementById('m3dModeBtn');if(b)b.textContent=(m==='wire'?'Wire':m==='xray'?'X-ray':'Solid')+' ▾';}
|
|
2963
3017
|
function wire3DBar(){if(bar3dWired||!window.Steel3DView)return;bar3dWired=true;
|
|
2964
|
-
|
|
2965
|
-
document.
|
|
3018
|
+
// Camera + Display dropdowns — open/close like Clip/Work/Plane; picking an item sets the mode, ticks it, and relabels the trigger (reflectProj/reflectMode).
|
|
3019
|
+
const projBtn=document.getElementById('m3dProjBtn'),projMenu=document.getElementById('m3dProj');
|
|
3020
|
+
function projMenuOutside(e){if(!projMenu.contains(e.target)&&e.target!==projBtn)projMenuClose();}
|
|
3021
|
+
function projMenuClose(){projMenu.classList.remove('open');document.removeEventListener('mousedown',projMenuOutside,true);}
|
|
3022
|
+
projBtn.onclick=e=>{e.stopPropagation();if(projMenu.classList.contains('open'))projMenuClose();else{projMenu.classList.add('open');document.addEventListener('mousedown',projMenuOutside,true);}};
|
|
3023
|
+
projMenu.querySelectorAll('button').forEach(b=>b.onclick=()=>{window.Steel3DView.setProjection(b.dataset.proj);reflectProj();projMenuClose();});
|
|
3024
|
+
const modeBtn=document.getElementById('m3dModeBtn'),modeMenu=document.getElementById('m3dMode');
|
|
3025
|
+
function modeMenuOutside(e){if(!modeMenu.contains(e.target)&&e.target!==modeBtn)modeMenuClose();}
|
|
3026
|
+
function modeMenuClose(){modeMenu.classList.remove('open');document.removeEventListener('mousedown',modeMenuOutside,true);}
|
|
3027
|
+
modeBtn.onclick=e=>{e.stopPropagation();if(modeMenu.classList.contains('open'))modeMenuClose();else{modeMenu.classList.add('open');document.addEventListener('mousedown',modeMenuOutside,true);}};
|
|
3028
|
+
modeMenu.querySelectorAll('button').forEach(b=>b.onclick=()=>{window.Steel3DView.setDisplayMode(b.dataset.mode);reflectMode();modeMenuClose();});
|
|
2966
3029
|
document.getElementById('m3dFit').onclick=()=>window.Steel3DView.frameAll();
|
|
2967
|
-
document.getElementById('
|
|
3030
|
+
document.getElementById('m3dFitSel').onclick=()=>window.Steel3DView.frameSelection(); // Zoom to selected (Alt+Z already bound in steel-3d-view.js)
|
|
3031
|
+
document.getElementById('m3dRef').onchange=e=>window.Steel3DView.setRefLine(e.target.checked); // View ▾ menu checkbox
|
|
2968
3032
|
const d3=window.Steel3DView;
|
|
2969
3033
|
document.getElementById('m3dDim').onclick=()=>{d3.toggleDimTool();setLastCmd('Dimension',()=>{const b=document.getElementById('m3dDim');if(b&&!b.classList.contains('on'))d3.toggleDimTool();});}; // toggleDimTool()'s reflectDimBar() owns the button .on class + axis/show visibility (single source, no duplicate DOM toggles)
|
|
2970
3034
|
document.querySelectorAll('#m3dDimAxis button').forEach(b=>b.onclick=()=>{d3.setDimAxis(b.dataset.d3axis);seg3dActive('#m3dDimAxis','data-d3axis',b.dataset.d3axis);});
|
|
@@ -2980,8 +3044,16 @@ function wire3DBar(){if(bar3dWired||!window.Steel3DView)return;bar3dWired=true;
|
|
|
2980
3044
|
else{clipMenu.classList.add('open');document.addEventListener('mousedown',clipMenuOutside,true);}};
|
|
2981
3045
|
clipMenu.querySelectorAll('button').forEach(b=>b.onclick=()=>{clipMenuClose();const a=b.dataset.clip;
|
|
2982
3046
|
if(a==='plane'){d3.setClipMode('plane');setLastCmd('Clip plane',()=>d3.setClipMode('plane'));}else if(a==='box'){d3.setClipMode('box');setLastCmd('Clip box',()=>d3.setClipMode('box'));}else if(a==='clear')d3.clearClips();});
|
|
2983
|
-
// Labels: a
|
|
2984
|
-
document.getElementById('m3dLabels').
|
|
3047
|
+
// Labels: a checkbox in the View ▾ menu (mirrors Ref line) — show each member's mark/id in the 3D view.
|
|
3048
|
+
document.getElementById('m3dLabels').onchange=e=>d3.setLabelsOn(e.target.checked);
|
|
3049
|
+
// View menu: groups the Ref line + Labels toggles (same open/close discipline as Clip / Work area / Plane).
|
|
3050
|
+
const viewBtn=document.getElementById('m3dView'),viewMenu=document.getElementById('m3dViewMenu');
|
|
3051
|
+
function viewMenuOutside(e){if(!viewMenu.contains(e.target)&&e.target!==viewBtn)viewMenuClose();}
|
|
3052
|
+
function viewMenuClose(){viewMenu.classList.remove('open');document.removeEventListener('mousedown',viewMenuOutside,true);}
|
|
3053
|
+
viewBtn.onclick=e=>{e.stopPropagation();
|
|
3054
|
+
if(viewMenu.classList.contains('open'))viewMenuClose();
|
|
3055
|
+
else{document.getElementById('m3dRef').checked=!!d3.refLine();document.getElementById('m3dLabels').checked=!!d3.labelsOn(); // reflect live state on open
|
|
3056
|
+
viewMenu.classList.add('open');document.addEventListener('mousedown',viewMenuOutside,true);}};
|
|
2985
3057
|
// Insert a detail: the ▾ menu lists the detail library + an "add image" option; picking one arms a
|
|
2986
3058
|
// placement pick (onInsertPlace drops it). While armed the button is a cancel target (onInsertModeChange).
|
|
2987
3059
|
// The menu is built with DOM nodes (textContent) — the detail names are user text, never innerHTML.
|
|
@@ -2991,10 +3063,10 @@ function wire3DBar(){if(bar3dWired||!window.Steel3DView)return;bar3dWired=true;
|
|
|
2991
3063
|
function insMenuBuild(){const names=Object.keys(C.custom_details||{}).sort((a,b)=>String(a).localeCompare(String(b),undefined,{numeric:true}));
|
|
2992
3064
|
const frag=document.createDocumentFragment();
|
|
2993
3065
|
const lab=document.createElement('div');lab.className='mlabel';lab.textContent='Place a detail';frag.appendChild(lab);
|
|
2994
|
-
if(names.length){for(const n of names){const b=document.createElement('button');b.textContent=n;b.
|
|
3066
|
+
if(names.length){for(const n of names){const b=document.createElement('button');b.textContent=n;b.dataset.tip='Place '+n;b.onclick=()=>{insMenuClose();closeMore();armInsert(n);};frag.appendChild(b);}}
|
|
2995
3067
|
else{const note=document.createElement('div');note.style.cssText='padding:4px 10px;color:var(--mut);font-size:12px';note.textContent='No saved details yet — add one below.';frag.appendChild(note);}
|
|
2996
3068
|
frag.appendChild(document.createElement('hr'));
|
|
2997
|
-
const add=document.createElement('button');add.textContent='+ Add an image…';add.onclick=()=>{insMenuClose();document.getElementById('insFile').click();};frag.appendChild(add);
|
|
3069
|
+
const add=document.createElement('button');add.textContent='+ Add an image…';add.onclick=()=>{insMenuClose();closeMore();document.getElementById('insFile').click();};frag.appendChild(add);
|
|
2998
3070
|
insMenu.replaceChildren(frag);}
|
|
2999
3071
|
insBtn.onclick=e=>{e.stopPropagation();
|
|
3000
3072
|
if(d3.insertMode()){d3.setInsertMode(false);return;} // armed → cancel the pick
|
|
@@ -3041,6 +3113,9 @@ function applyViewState(on){ // flip the toggle + swap the canvases (
|
|
|
3041
3113
|
document.getElementById('zoombar').style.display=on?'none':''; // zoom slider is 2D-only
|
|
3042
3114
|
document.getElementById('planSel').style.display=on?'none':''; // plan selector is 2D-only (3D shows the whole model)
|
|
3043
3115
|
document.getElementById('m3dBar').style.display=on?'flex':'none';
|
|
3116
|
+
document.body.classList.toggle('v3d',on); // 3D-only chrome (e.g. ⋯ → Detailing → Insert detail) keys off this
|
|
3117
|
+
document.getElementById('dimB').style.display=on?'none':''; // 2D Dimension button — 2D only
|
|
3118
|
+
document.getElementById('dim3dWrap').style.display=on?'inline-flex':'none'; // 3D Dimension cluster (moved to header) — 3D only
|
|
3044
3119
|
document.getElementById('m3dCube').style.display=on?'block':'none';
|
|
3045
3120
|
document.getElementById('m3dAxes').style.display=on?'block':'none';
|
|
3046
3121
|
document.getElementById('snapBar').classList.toggle('s3d',on); // in 3D the snap bar shifts clear of the world-axis triad (bottom-right); see #snapBar.s3d
|
|
@@ -3059,7 +3134,7 @@ async function setView(on){
|
|
|
3059
3134
|
await window.Steel3DView.rebuild(true); // fit the camera on entering 3D
|
|
3060
3135
|
window.Steel3DView.setSelection(selIds);
|
|
3061
3136
|
wire3DBar();build3DLegend();
|
|
3062
|
-
|
|
3137
|
+
reflectProj();reflectMode(); // reflect persisted projection + display mode into the Camera/Display dropdown triggers
|
|
3063
3138
|
}catch(e){ // a failed open must not strand the UI in 3D with a blank canvas
|
|
3064
3139
|
applyViewState(false);if(window.Steel3DView)window.Steel3DView.hide();
|
|
3065
3140
|
toast('Could not open 3D view: '+((e&&e.message)||e));
|
|
@@ -3114,7 +3189,7 @@ function openDetails(){const g=document.getElementById('detailsGrid');const uniq
|
|
|
3114
3189
|
g.innerHTML=uniq.length?uniq.map(t=>{const custom=C.custom_details[t]!=null;const sheet=custom?'':(sheetOf(t)||'');const pv=previewFor(t),b=custom?null:bubbleFor(t);
|
|
3115
3190
|
const prev=pv?(b?`<img class=dthumb data-t="${esc(t)}" alt="${esc(t)}">`:`<img src="data:image/jpeg;base64,${pv}" alt="${esc(t)}">`):`<div class=dprev>Preview pending<br>from sheet ${esc(sheet)}</div>`;
|
|
3116
3191
|
const sub=custom?'Custom detail':('Sheet '+esc(sheet)+(b?' · detail '+esc(numOf(t)):''));
|
|
3117
|
-
return `<div class=dcard><div class=dprevwrap${pv?` data-det="${esc(t)}"`:''}>${prev}</div><div class=dmeta><input class=dname data-old="${esc(t)}" value="${esc(t)}"
|
|
3192
|
+
return `<div class=dcard><div class=dprevwrap${pv?` data-det="${esc(t)}"`:''}>${prev}</div><div class=dmeta><input class=dname data-old="${esc(t)}" value="${esc(t)}" data-tip="Rename (updates every plan)" autocomplete=off><div class=ds>${sub}${custom?` · <a href="#" class=ddel data-del="${esc(t)}">delete</a>`:''}</div></div></div>`;}).join('')
|
|
3118
3193
|
:'<div class=hint>No detail callouts detected. Use + New detail to add your own.</div>';
|
|
3119
3194
|
g.querySelectorAll('img.dthumb').forEach(im=>detThumb(im.dataset.t,im));
|
|
3120
3195
|
g.querySelectorAll('.dprevwrap[data-det]').forEach(c=>c.onclick=()=>openPreview(c.dataset.det));
|
|
@@ -3145,7 +3220,7 @@ function updBpBtn(){const b=document.getElementById('bpBtn');if(!b)return;
|
|
|
3145
3220
|
const colSet=new Set(colMemberIds()), n=autoBasePlates().length;
|
|
3146
3221
|
const covered=new Set((C.joints||[]).filter(j=>j&&j.kind==='base-plate'&&colSet.has(j.main)).map(j=>j.main)).size; // only CURRENT columns (ignore joints orphaned by a deleted column)
|
|
3147
3222
|
b.disabled=colSet.size===0; // no columns → nothing to detail (empty-state)
|
|
3148
|
-
b.
|
|
3223
|
+
b.dataset.tip=colSet.size===0?'No columns in this model to detail base plates.':'Auto-detail base plates on every column (a plate + anchor kit + weld, shown in 3D). Tune sizes per the schedule via the AI or per-column params.';
|
|
3149
3224
|
b.textContent=((colSet.size>0&&covered>=colSet.size&&n>0)?'Clear base plates':'Base plates')+(n?' ('+n+')':'');} // label flips once all current columns are covered, so the destructive 2nd click reads clearly
|
|
3150
3225
|
// C.joints is model-global but now rides the snapshot (snapshot()/apply() carry it), so every joint
|
|
3151
3226
|
// mutation goes through edit() — auto-detail, the inspector, and Remove are all undoable (Ctrl+Z/Y).
|
|
@@ -3190,7 +3265,7 @@ function updSpBtn(){const b=document.getElementById('spBtn');if(!b)return;
|
|
|
3190
3265
|
const elig=spEligibleEnds(),n=autoShearPlates().length;
|
|
3191
3266
|
const covered=elig.filter(({beam,endIdx})=>spJointOf(beam.id,endIdx)).length; // eligible ends already detailed (auto OR user)
|
|
3192
3267
|
b.disabled=elig.length===0; // no eligible beam ends → nothing to detail (empty-state, disabled-not-hidden)
|
|
3193
|
-
b.
|
|
3268
|
+
b.dataset.tip=elig.length===0?'No eligible beam ends in this model to detail shear plates.':'Auto-detail bolted shear (fin) plates on eligible beam ends (a fin plate + bolt group + weld, shown in 3D). Tune sizes per the schedule via the AI or per-end params.';
|
|
3194
3269
|
b.textContent=((elig.length>0&&covered>=elig.length&&n>0)?'Clear shear plates':'Shear plates')+(n?' ('+n+')':'');} // label flips to Clear once all ELIGIBLE ends are covered (moment ends are legitimately excluded)
|
|
3195
3270
|
function toggleShearPlates(){
|
|
3196
3271
|
const elig=spEligibleEnds();
|
|
@@ -3214,7 +3289,7 @@ function renderConnTable(){const tb=document.getElementById('connBody');if(!tb)r
|
|
|
3214
3289
|
+`<td><input class="combo ftab-inp" data-src=details data-f=detail value="${esc(r.detail||'')}" placeholder="e.g. 50S504" autocomplete=off></td>`
|
|
3215
3290
|
+`<td><input class=ftab-inp data-f=target value="${esc((r.targets&&r.targets[connPlat])||'')}" placeholder="e.g. 146" autocomplete=off></td>`
|
|
3216
3291
|
+`<td class=csrc>${srcChip(r.source||'user')}</td>`
|
|
3217
|
-
+`<td><button class="danger cdel"
|
|
3292
|
+
+`<td><button class="danger cdel" data-tip="Remove this row">✕</button></td></tr>`).join('');
|
|
3218
3293
|
tb.querySelectorAll('tr[data-id]').forEach(tr=>{const id=tr.dataset.id;const row=connRowById(id);if(!row)return;
|
|
3219
3294
|
tr.querySelector('[data-f=type]').onchange=e=>edit(()=>{row.type=e.target.value.trim();});
|
|
3220
3295
|
tr.querySelector('[data-f=detail]').onchange=e=>edit(()=>{row.detail=e.target.value.trim();});
|
|
@@ -3384,7 +3459,7 @@ function updConf(){const el=document.getElementById('confStat');if(!el)return;co
|
|
|
3384
3459
|
else{const gap=tgt-sc;
|
|
3385
3460
|
tg.innerHTML=' · <span style="color:var(--mut)">target '+tgt+'%</span>'+(gap>0?' <span style="color:'+col+'">(−'+gap+'%)</span>':'');}
|
|
3386
3461
|
}
|
|
3387
|
-
el.
|
|
3462
|
+
el.dataset.tip='Confidence '+(sc==null?'—':sc+'%')+(tgt!=null?' vs target '+tgt+'%':'')+' — '+s.overall.tons.toFixed(1)+' t scored · '+s.overall.rfiCount+' RFI. Click for the report.';}
|
|
3388
3463
|
// --- the report modal ---
|
|
3389
3464
|
let confBand='all',confCat='all',confExpand=null;
|
|
3390
3465
|
function openConf(){confExpand=null;renderConf();document.getElementById('confModal').style.display='flex';}
|
|
@@ -3395,7 +3470,7 @@ function _countsHtml(c){const out=['verified','high','med','low','rfi'].filter(k
|
|
|
3395
3470
|
function renderConf(){const s=scoreContractJS();const cat=s.byCategory;
|
|
3396
3471
|
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)
|
|
3397
3472
|
ct.value=typeof C.target_confidence==='number'?C.target_confidence:'';ct.placeholder=TARGET_CONF!=null?String(TARGET_CONF):'70';}
|
|
3398
|
-
const card=(key,label,cs,stub)=>'<div class="ccard'+(stub?' stub':' click')+(confCat===key?' on':'')+'" data-cat="'+key+'"'+(stub?'
|
|
3473
|
+
const card=(key,label,cs,stub)=>'<div class="ccard'+(stub?' stub':' click')+(confCat===key?' on':'')+'" data-cat="'+key+'"'+(stub?' data-tip="not scored yet"':'')+'>'+
|
|
3399
3474
|
'<div class=cc-label>'+label+'</div>'+
|
|
3400
3475
|
'<div class=cc-score style="color:'+(stub?'var(--mut)':bandColorForPct(cs.score))+'">'+(stub||cs.score==null?'—':cs.score+'%')+'</div>'+
|
|
3401
3476
|
_bar(stub?null:cs.score)+
|
|
@@ -3673,7 +3748,7 @@ if(new URLSearchParams(location.search).get('selftest')==='1'){(function(){
|
|
|
3673
3748
|
if(src.read_at){try{const d=new Date(src.read_at);if(!isNaN(d.getTime()))parts.push(d.toLocaleDateString());}catch{/* skip unparseable date */}}
|
|
3674
3749
|
if(!parts.length)return;
|
|
3675
3750
|
const el=document.getElementById('srcStat');if(!el)return;
|
|
3676
|
-
el.textContent=parts.join(' · ');el.
|
|
3751
|
+
el.textContent=parts.join(' · ');el.dataset.tip=parts.join(' · ');el.style.display=''; // title = full provenance (the chip itself truncates with ellipsis)
|
|
3677
3752
|
})();
|
|
3678
3753
|
// --- Ask AI: send instruction + optional screenshots → POST /api/contract-request ---
|
|
3679
3754
|
// The server records a tweak-contract request; the terminal AI picks it up async and PUTs
|
|
@@ -3725,7 +3800,7 @@ function askAiRenderThumbs() {
|
|
|
3725
3800
|
img.src = snap.dataUrl; // full dataURL — safe, not user text, no textContent needed
|
|
3726
3801
|
img.alt = '';
|
|
3727
3802
|
const rmBtn = document.createElement('button');
|
|
3728
|
-
rmBtn.
|
|
3803
|
+
rmBtn.dataset.tip = 'Remove';
|
|
3729
3804
|
rmBtn.textContent = '✕';
|
|
3730
3805
|
rmBtn.onclick = () => { askAiSnapshots.splice(idx, 1); askAiRenderThumbs(); };
|
|
3731
3806
|
const nameEl = document.createElement('div');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floless/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.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": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"build:exe": "node build/bundle.mjs && node build/make-sea.mjs",
|
|
26
26
|
"verify:skills": "node build/verify-shipped-skills.mjs",
|
|
27
27
|
"verify:flo": "node build/verify-flo-descriptions.mjs",
|
|
28
|
+
"verify:no-title": "node build/verify-no-title.mjs",
|
|
28
29
|
"typecheck": "tsc --noEmit",
|
|
29
30
|
"prepack": "npm run build"
|
|
30
31
|
},
|