@floless/app 0.70.0 → 0.71.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
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.71.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.71.0" : void 0 });
|
|
53036
53036
|
}
|
|
53037
53037
|
|
|
53038
53038
|
// workflow-update.ts
|
|
@@ -54698,6 +54698,8 @@ function expandJoints(joints, memberGeo) {
|
|
|
54698
54698
|
return;
|
|
54699
54699
|
}
|
|
54700
54700
|
for (const part of expandBasePlate(j, col)) {
|
|
54701
|
+
part.conn = j.id;
|
|
54702
|
+
part.connKind = j.kind;
|
|
54701
54703
|
elements.push(part);
|
|
54702
54704
|
usedGroups.add(part.group);
|
|
54703
54705
|
}
|
|
@@ -54719,6 +54721,8 @@ function expandJoints(joints, memberGeo) {
|
|
|
54719
54721
|
return;
|
|
54720
54722
|
}
|
|
54721
54723
|
for (const part of parts) {
|
|
54724
|
+
part.conn = j.id;
|
|
54725
|
+
part.connKind = j.kind;
|
|
54722
54726
|
elements.push(part);
|
|
54723
54727
|
if (part.kind !== "cut") usedGroups.add(part.group);
|
|
54724
54728
|
}
|
|
@@ -526,6 +526,7 @@ function applyCopes(mesh, cuts) {
|
|
|
526
526
|
|
|
527
527
|
function buildFromScene(sc) {
|
|
528
528
|
clearRoot();
|
|
529
|
+
resetConnState(); // a fresh scene rebuilds selection from scratch — drop any stale connection envelope/context
|
|
529
530
|
for (const mat of baseMat.values()) mat.dispose(); // shared per-profile materials from the prior build
|
|
530
531
|
groupColor.clear(); baseMat.clear();
|
|
531
532
|
sceneGroups = (sc.groups || []).map((g) => ({ key: g.key, label: g.label, color: g.color || '#94a3b8' }));
|
|
@@ -547,6 +548,7 @@ function buildFromScene(sc) {
|
|
|
547
548
|
if (memberCuts && memberCuts.length) applyCopes(mesh, memberCuts); // notch a coped member end
|
|
548
549
|
mesh.userData.id = el.id; mesh.userData.group = el.group; mesh.userData.profile = el.meta && el.meta.profile;
|
|
549
550
|
mesh.userData.derived = !!(el.kind && el.kind !== 'box'); // connection parts: rendered, not member-editable
|
|
551
|
+
mesh.userData.conn = el.conn || null; mesh.userData.connKind = el.connKind || null; // Connection Component membership (Slice A) — the whole-select/drill handle
|
|
550
552
|
root.add(mesh); meshById.set(el.id, mesh);
|
|
551
553
|
box.expandByObject(mesh);
|
|
552
554
|
}
|
|
@@ -1302,6 +1304,7 @@ function onKey(e) {
|
|
|
1302
1304
|
if (insertMode && e.key === 'Escape') { e.preventDefault(); setInsertMode(false); if (api && api.toast) api.toast('Insert cancelled'); return; } // Esc disarms the detail-placement pick
|
|
1303
1305
|
if (clipMode && e.key === 'Escape') { e.preventDefault(); if (clipMode === 'box' && clipBoxDraft) { if (clipBoxDraft.b) clipBoxDraft.b = null; else clipBoxDraft = null; setClipPreview(null); updateStatusChip(); } else setClipMode(null); return; } // Esc steps back: height→footprint→cancel, else disarms the pick
|
|
1304
1306
|
if (isolatedIds && e.key === 'Escape' && !dimMode3d) { e.preventDefault(); clearIsolation(); return; } // Esc exits isolate-selected (the dim tool's own Esc wins while it's armed)
|
|
1307
|
+
if (e.key === 'Escape' && !dimMode3d && !cmActive() && ascendConn()) { e.preventDefault(); return; } // Esc ascends the connection drill: part → whole → nothing
|
|
1305
1308
|
if ((e.key === ' ' && e.shiftKey) || ((e.key === 'z' || e.key === 'Z') && e.altKey)) { e.preventDefault(); frameSelection(); return; } // zoom-selected (Tekla Shift+Space / viewer Alt+Z)
|
|
1306
1309
|
const k = e.key.toLowerCase();
|
|
1307
1310
|
// Don't touch the dim tool while a member gesture (drag / box-select) owns the shared marker/readout —
|
|
@@ -1354,6 +1357,16 @@ function onDblClick(e) {
|
|
|
1354
1357
|
const hits = raycaster.intersectObjects([...meshById.values()].filter((m) => m.visible), false); // incl. connection parts
|
|
1355
1358
|
if (!hits.length) return; // empty space → no-op (Fit / Home fit-all; avoids an accidental camera teleport)
|
|
1356
1359
|
const p = hits[0].point, mesh = hits[0].object;
|
|
1360
|
+
// Connection drill-down (Slice A): double-clicking a part of a connection we're NOT already inside ENTERS
|
|
1361
|
+
// that connection (selects the part under the cursor) and frames it. A part of the connection we're
|
|
1362
|
+
// already in, or a bare member, falls through to the classic zoom-to-part below (non-breaking).
|
|
1363
|
+
const dblConn = mesh.userData && mesh.userData.conn;
|
|
1364
|
+
if (dblConn && ctxConn !== dblConn) {
|
|
1365
|
+
enterConn(dblConn, mesh.userData.id);
|
|
1366
|
+
const cb = connBox(dblConn);
|
|
1367
|
+
if (!cb.isEmpty()) { const vDir = camera.position.clone().sub(controls.target).normalize(); fitCamera(cb, vDir.lengthSq() > 0.5 ? vDir : undefined); }
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1357
1370
|
if (mesh.geometry && !mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
|
|
1358
1371
|
const s = mesh.geometry && mesh.geometry.boundingBox ? mesh.geometry.boundingBox.getSize(new THREE.Vector3()) : V(400, 400, 400);
|
|
1359
1372
|
const sect = Math.max(40, Math.min(s.x, s.y, s.z)); // the part's smallest extent ≈ a section / plate scale
|
|
@@ -1378,6 +1391,7 @@ function setSelection(ids) {
|
|
|
1378
1391
|
}
|
|
1379
1392
|
applyDisplayMode(); // selection swapped the materials → re-apply wire/xray
|
|
1380
1393
|
selIds = new Set(set);
|
|
1394
|
+
reconcileConnState(set); // any selection path (2D click, box-select, keyboard) must not leave a stale connection envelope/drill
|
|
1381
1395
|
rebuildEndpoints(); // endpoint dots follow the selection (+ any hover)
|
|
1382
1396
|
updateStatusChip();
|
|
1383
1397
|
}
|
|
@@ -1817,14 +1831,19 @@ const CYCLE_TOL_PX = 8;
|
|
|
1817
1831
|
function resetCycle() { cycleAnchor = null; cycleIds = []; cycleIdx = 0; }
|
|
1818
1832
|
function clickSelect(cx, cy, ctrl) {
|
|
1819
1833
|
let hits = []; try { hits = pickAllAt(cx, cy); } catch { hits = []; }
|
|
1820
|
-
if (!hits.length) { resetCycle();
|
|
1821
|
-
if (ctrl) { resetCycle(); if (api && api.onSelect) api.onSelect(hits[0], true); return; } // additive toggles the nearest
|
|
1834
|
+
if (!hits.length) { resetCycle(); clearConnSel(); return; } // empty → deselect (clears any connection too)
|
|
1835
|
+
if (ctrl) { resetCycle(); resetConnState(); if (api && api.onSelect) api.onSelect(hits[0], true); return; } // additive toggles the nearest RAW part (leaves connection mode)
|
|
1822
1836
|
const same = cycleAnchor && Math.hypot(cx - cycleAnchor[0], cy - cycleAnchor[1]) <= CYCLE_TOL_PX
|
|
1823
1837
|
&& cycleIds.length === hits.length && cycleIds.every((v, i) => v === hits[i]);
|
|
1824
1838
|
if (same) cycleIdx = (cycleIdx + 1) % hits.length; else { cycleIds = hits; cycleIdx = 0; cycleAnchor = [cx, cy]; }
|
|
1825
|
-
const pick = hits[cycleIdx],
|
|
1826
|
-
if (
|
|
1827
|
-
|
|
1839
|
+
const pick = hits[cycleIdx], conn = connOf(pick);
|
|
1840
|
+
if (!conn) { resetConnState(); if (api && api.onSelect) api.onSelect(pick, false); return; } // a bare member → normal single select
|
|
1841
|
+
if (ctxConn === conn) { // drilled INTO this connection → clicks land on its parts (bolt array or a single part)
|
|
1842
|
+
const grp = boltGroupOf(pick);
|
|
1843
|
+
if (grp.length > 1) { if (api && api.onSelectMany) api.onSelectMany(grp); } else if (api && api.onSelect) api.onSelect(pick, false);
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
selectWholeConn(conn); // at root (or over a different connection) → select the WHOLE connection
|
|
1828
1847
|
}
|
|
1829
1848
|
// A bolt/head/nut id → all bolt-group part ids of the same joint (the connection's bolt array); else just [id].
|
|
1830
1849
|
function boltGroupOf(id) {
|
|
@@ -1834,6 +1853,63 @@ function boltGroupOf(id) {
|
|
|
1834
1853
|
const ids = [...meshById.keys()].filter((k) => { const c = k.indexOf(':'); return c >= 0 && k.slice(0, c) === jid && /^(bolt|head|nut)/.test(k.slice(c + 1)); });
|
|
1835
1854
|
return ids.length ? ids : [id];
|
|
1836
1855
|
}
|
|
1856
|
+
|
|
1857
|
+
// ── Connection Components (Slice A): select/drill a whole connection (base-plate / shear-plate) as ONE
|
|
1858
|
+
// unit. `selConn` = the connection currently whole-selected at root; `ctxConn` = the connection we've
|
|
1859
|
+
// DRILLED INTO (double-click) so subsequent clicks land on its individual parts. Both derive from the
|
|
1860
|
+
// `conn` tag every ConnPart carries (buildFromScene stashes el.conn on userData). A bare member (no conn)
|
|
1861
|
+
// clears both. The host editor re-derives its breadcrumb + component inspector from the selection ids each
|
|
1862
|
+
// render() — no view→editor callback needed; reconcileConnState() (from setSelection) keeps this honest.
|
|
1863
|
+
let selConn = null, ctxConn = null;
|
|
1864
|
+
function connOf(id) { const m = id && meshById.get(id); return m && m.userData ? (m.userData.conn || null) : null; }
|
|
1865
|
+
function connChildIds(conn) { const out = []; for (const [id, m] of meshById) { if (m.userData && m.userData.conn === conn) out.push(id); } return out; } // every rendered part of this connection
|
|
1866
|
+
function connBox(conn) { const b = new THREE.Box3(); for (const m of meshById.values()) { if (m.userData && m.userData.conn === conn && m.visible) b.expandByObject(m); } return b; }
|
|
1867
|
+
// The dashed brand-blue envelope = the single "this is a group" cue for a whole-connection selection.
|
|
1868
|
+
let connEnvelope = null;
|
|
1869
|
+
function clearConnEnvelope() { if (connEnvelope) { if (overlayScene) overlayScene.remove(connEnvelope); connEnvelope.geometry.dispose(); connEnvelope.material.dispose(); connEnvelope = null; } }
|
|
1870
|
+
function renderConnEnvelope(conn) {
|
|
1871
|
+
clearConnEnvelope();
|
|
1872
|
+
if (!conn || !overlayScene) return;
|
|
1873
|
+
const b = connBox(conn); if (b.isEmpty()) return;
|
|
1874
|
+
b.expandByScalar(Math.max(6, b.getSize(new THREE.Vector3()).length() * 0.02)); // a little breathing room around the parts
|
|
1875
|
+
connEnvelope = new THREE.Box3Helper(b, new THREE.Color(SELECT_EMISSIVE)); // --brand
|
|
1876
|
+
connEnvelope.material.depthTest = false; connEnvelope.material.transparent = true; connEnvelope.material.opacity = 0.6; connEnvelope.renderOrder = 996;
|
|
1877
|
+
overlayScene.add(connEnvelope);
|
|
1878
|
+
}
|
|
1879
|
+
function resetConnState() { selConn = null; ctxConn = null; clearConnEnvelope(); } // internal reset, no callbacks
|
|
1880
|
+
// Select the WHOLE connection at root (single-click a part, or a breadcrumb click). Clears any drill context.
|
|
1881
|
+
function selectWholeConn(conn) {
|
|
1882
|
+
if (!conn || !connChildIds(conn).length) return clearConnSel();
|
|
1883
|
+
selConn = conn; ctxConn = null;
|
|
1884
|
+
renderConnEnvelope(conn);
|
|
1885
|
+
if (api && api.onSelectMany) api.onSelectMany(connChildIds(conn));
|
|
1886
|
+
}
|
|
1887
|
+
// Clear any connection selection/drill (back to bare Model root — deselects).
|
|
1888
|
+
function clearConnSel() { resetConnState(); if (api && api.onSelect) api.onSelect(null, false); }
|
|
1889
|
+
// Enter a connection (double-click) and select the part under the cursor — the drill-in step.
|
|
1890
|
+
function enterConn(conn, partId) {
|
|
1891
|
+
ctxConn = conn; selConn = conn; clearConnEnvelope(); // inside → the part-level highlight carries; no whole envelope
|
|
1892
|
+
const grp = boltGroupOf(partId);
|
|
1893
|
+
if (grp.length > 1) { if (api && api.onSelectMany) api.onSelectMany(grp); }
|
|
1894
|
+
else if (api && api.onSelect) api.onSelect(partId, false);
|
|
1895
|
+
}
|
|
1896
|
+
// Ascend one level: drilled part → whole connection → nothing. Returns true if it consumed the gesture.
|
|
1897
|
+
function ascendConn() {
|
|
1898
|
+
if (ctxConn) { selectWholeConn(ctxConn); return true; } // part → whole
|
|
1899
|
+
if (selConn) { clearConnSel(); return true; } // whole → nothing
|
|
1900
|
+
return false;
|
|
1901
|
+
}
|
|
1902
|
+
// Keep the connection state honest against ANY selection change — not just the 3D click paths but a 2D
|
|
1903
|
+
// member click, box-select, keyboard, or Delete that route through setSelection(). Whole: the full child
|
|
1904
|
+
// set must still be selected, else drop the stale envelope; drilled: the selection must stay WITHIN the
|
|
1905
|
+
// connection, else exit the drill. Callback-free (resetConnState) so it can't recurse through render().
|
|
1906
|
+
function reconcileConnState(set) {
|
|
1907
|
+
if (!selConn) return;
|
|
1908
|
+
const kids = connChildIds(selConn);
|
|
1909
|
+
if (ctxConn) { if (!set.size || ![...set].every((id) => kids.includes(id))) resetConnState(); } // drilled: any pick outside the connection → exit
|
|
1910
|
+
else if (!kids.length || !kids.every((k) => set.has(k))) resetConnState(); // whole: must remain the full set, else drop the envelope
|
|
1911
|
+
}
|
|
1912
|
+
function connContext() { return { selConn, ctxConn }; } // test/editor read
|
|
1837
1913
|
// The (currently shown) end-node dot nearest the cursor within a screen tolerance → { id, end } or
|
|
1838
1914
|
// null. Screen-space (not a raycast) so the small dots are easy to grab at any zoom. Dots win over
|
|
1839
1915
|
// the member body, letting you grab one end to stretch it.
|
|
@@ -2575,7 +2651,7 @@ function dispose() {
|
|
|
2575
2651
|
gridTexCache.clear();
|
|
2576
2652
|
clearRoot();
|
|
2577
2653
|
if (workAreaHelper) { if (overlayScene) overlayScene.remove(workAreaHelper); workAreaHelper.geometry.dispose(); workAreaHelper.material.dispose(); workAreaHelper = null; }
|
|
2578
|
-
clearClipGizmo(); setClipPreview(null); overlayScene = null;
|
|
2654
|
+
clearConnEnvelope(); clearClipGizmo(); setClipPreview(null); overlayScene = null;
|
|
2579
2655
|
clips = []; workArea = null; clipMode = null; selectedClipIds.clear(); clipBoxDraft = null; // clips live on the renderer; drop them with the renderer
|
|
2580
2656
|
if (renderer) renderer.dispose();
|
|
2581
2657
|
renderer = scene = camera = perspCam = orthoCam = controls = root = api = canvasEl = ro = null; built = false;
|
|
@@ -2637,6 +2713,7 @@ window.Steel3DView = {
|
|
|
2637
2713
|
setProjection, projection, setDisplayMode, mode: () => displayMode, frameAll, frameSelection, applyView,
|
|
2638
2714
|
setRefLine, refLine: () => refLineOn,
|
|
2639
2715
|
setInsertMode, insertMode: insertModeOn, // arm/query the detail-placement pick (Slice 4)
|
|
2716
|
+
selectWholeConn, clearConnSel, ascendConn, connContext, connEnvelopeOn: () => !!connEnvelope, // Connection Components (Slice A): whole-select / drill / ascend + test probes
|
|
2640
2717
|
setLabelsOn, labelsOn: () => labelsOnFlag, // member mark/id label overlay toggle
|
|
2641
2718
|
syncMemberLabels, // editor calls after a mark/id edit to refresh labels
|
|
2642
2719
|
setPropLabels, // right-click property labels: editor pushes { labels:[{id,lines}], placement }
|
|
@@ -33,6 +33,13 @@
|
|
|
33
33
|
.detf input{width:100%}
|
|
34
34
|
#detOpacity{accent-color:var(--brand);flex:1;min-width:0}
|
|
35
35
|
#zoombar #zPct{min-width:40px;text-align:right;color:var(--mut);font-variant-numeric:tabular-nums}
|
|
36
|
+
/* Connection Component breadcrumb (Slice A) — a floating chip over the 3D canvas, same recipe as #zoombar. */
|
|
37
|
+
#connCrumb{position:absolute;left:50%;top:48px;transform:translateX(-50%);display:none;align-items:center;gap:1px;max-width:min(72%,560px);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:4px 10px;box-shadow:0 4px 14px rgba(0,0,0,.45);z-index:58;font-size:12px;white-space:nowrap;overflow:hidden} /* below #m3dBar (top:12,h~29,z:59); z:58 keeps it clickable above the dim-label chips (57) */
|
|
38
|
+
#connCrumb .seg{color:var(--mut);cursor:pointer;padding:1px 4px;border-radius:4px;background:none;border:0;font:inherit;max-width:260px;overflow:hidden;text-overflow:ellipsis}
|
|
39
|
+
#connCrumb .seg:hover{color:var(--text);text-decoration:underline}
|
|
40
|
+
#connCrumb .seg.cur{color:var(--brand);font-weight:600;cursor:default;text-decoration:none}
|
|
41
|
+
#connCrumb .sep{color:var(--mut);opacity:.7;padding:0 2px}
|
|
42
|
+
.pilllink{background:none;border:0;color:var(--brand);cursor:pointer;font:inherit;padding:0;text-decoration:underline}
|
|
36
43
|
aside{width:240px;flex:none;background:var(--panel);border-left:1px solid var(--line);padding:12px;overflow:auto}
|
|
37
44
|
aside h3{margin:0 0 8px;font-size:12px;color:var(--mut);text-transform:uppercase;letter-spacing:.05em}
|
|
38
45
|
select,input{background:#0f172a;color:var(--text);border:1px solid #475569;border-radius:6px;padding:6px;width:100%;font:13px system-ui}
|
|
@@ -496,6 +503,7 @@
|
|
|
496
503
|
<div id=stagewrap>
|
|
497
504
|
<div id=stage><svg id=svg></svg></div>
|
|
498
505
|
<canvas id=stage3d tabindex=0 aria-label="3D model"></canvas>
|
|
506
|
+
<div id=connCrumb role=navigation aria-label="Connection breadcrumb"></div>
|
|
499
507
|
<div id=m3dBar role=group aria-label="3D view controls">
|
|
500
508
|
<!-- Camera projection — dropdown (like Plane / Work area); the button shows the current mode -->
|
|
501
509
|
<div class=m3dwrap>
|
|
@@ -1377,6 +1385,7 @@ function render(){
|
|
|
1377
1385
|
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)
|
|
1378
1386
|
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(); updBpBtn(); updSpBtn(); updGridToggle();
|
|
1379
1387
|
if(view3d&&window.Steel3DView){window.Steel3DView.setSelection(selIds);updateIsolateBtn();if(selIds.size&&window.Steel3DView.selectedClips&&window.Steel3DView.selectedClips().length)window.Steel3DView.setSelectedClips([]);} // keep the 3D highlight in sync; selecting a member clears any clip selection (exclusive)
|
|
1388
|
+
try{updateConnCrumb();}catch(_){} // Connection Component breadcrumb follows the selection (3D-only; hidden at root)
|
|
1380
1389
|
syncPropLabelsAfterRender(); // corner-note + push labels to 3D + refresh the popup rows against the (possibly changed) selection
|
|
1381
1390
|
}
|
|
1382
1391
|
function updDup(){const n=redundantDups().length;
|
|
@@ -1417,6 +1426,54 @@ function stats(){
|
|
|
1417
1426
|
// "Varies" placeholders + the indeterminate "default" checkbox in the multi-edit panel. get() must return a primitive.
|
|
1418
1427
|
const VARIES=Symbol('varies');
|
|
1419
1428
|
function agg(list,get){if(!list.length)return undefined;const f=get(list[0]);for(let i=1;i<list.length;i++)if(get(list[i])!==f)return VARIES;return f;}
|
|
1429
|
+
// ── Connection Components (Slice A). Derive the current connection-selection state from selIds + the
|
|
1430
|
+
// resolved scene parts (partsById carries each part's `conn` tag). Returns {conn,kind,main,joint,childIds,
|
|
1431
|
+
// whole,mode} or null when the selection isn't one connection's parts. `whole` = every selectable part of
|
|
1432
|
+
// the connection is selected (copes are subtractive → not rendered/selectable, so excluded). Robust: no
|
|
1433
|
+
// dependence on cross-view callback timing — every render() re-derives it.
|
|
1434
|
+
function connSelInfo(){
|
|
1435
|
+
const ids=[...selIds]; if(!ids.length) return null;
|
|
1436
|
+
let conn=null;
|
|
1437
|
+
for(const id of ids){ const el=(partsById||{})[id]; const c=el&&el.conn; if(!c) return null; if(conn==null) conn=c; else if(conn!==c) return null; }
|
|
1438
|
+
if(!conn) return null;
|
|
1439
|
+
const j=(C.joints||[]).find(x=>x&&x.id===conn); if(!j) return null;
|
|
1440
|
+
const childIds=Object.keys(partsById||{}).filter(id=>{const el=partsById[id];return el&&el.conn===conn&&el.kind!=='cut';});
|
|
1441
|
+
const whole=childIds.length>0&&childIds.every(id=>selIds.has(id));
|
|
1442
|
+
return {conn,kind:j.kind,main:j.main,joint:j,childIds,whole,mode:whole?'whole':'part'};
|
|
1443
|
+
}
|
|
1444
|
+
// The floating breadcrumb over the 3D canvas: Model ▸ <Connection> [▸ <Part>]. Segments jump levels via the
|
|
1445
|
+
// 3D view's own ascend/whole-select so the canvas selection + envelope stay in lockstep. 3D-only; hidden at root.
|
|
1446
|
+
function updateConnCrumb(){
|
|
1447
|
+
const el=document.getElementById('connCrumb'); if(!el) return;
|
|
1448
|
+
const cs=view3d?connSelInfo():null;
|
|
1449
|
+
if(!cs){ el.style.display='none'; el.innerHTML=''; return; }
|
|
1450
|
+
const name=(cs.kind==='base-plate'?'Base plate':cs.kind==='shear-plate'?'Shear plate':'Connection')+' · '+cs.main;
|
|
1451
|
+
let html='<button class=seg data-lvl=root data-tip="Back to the model (deselect)">Model</button><span class=sep>▸</span>';
|
|
1452
|
+
if(cs.whole){ html+='<span class="seg cur">'+esc(name)+'</span>'; }
|
|
1453
|
+
else{
|
|
1454
|
+
html+='<button class=seg data-lvl=whole data-tip="Select the whole connection">'+esc(name)+'</button><span class=sep>▸</span>';
|
|
1455
|
+
const partId=[...selIds].find(id=>/:bolt\d+$/.test(id))||[...selIds][0];
|
|
1456
|
+
const pel=(partsById||{})[partId]; const plbl=(pel&&pel.meta&&pel.meta.label)||'Part';
|
|
1457
|
+
html+='<span class="seg cur">'+esc(plbl)+'</span>';
|
|
1458
|
+
}
|
|
1459
|
+
el.innerHTML=html; el.style.display='flex';
|
|
1460
|
+
{const b=el.querySelector('[data-lvl=root]'); if(b)b.onclick=()=>{ if(window.Steel3DView&&window.Steel3DView.clearConnSel)window.Steel3DView.clearConnSel(); else{selIds=new Set();render();} };}
|
|
1461
|
+
{const b=el.querySelector('[data-lvl=whole]'); if(b)b.onclick=()=>{ if(window.Steel3DView&&window.Steel3DView.selectWholeConn)window.Steel3DView.selectWholeConn(cs.conn); };}
|
|
1462
|
+
}
|
|
1463
|
+
// Route a "modify this connection" ask through the Request relay (intent+target). A recipe connection's
|
|
1464
|
+
// geometry is member-derived, so move/replace/adjust go to the terminal AI (the UI relays intent) — unlike
|
|
1465
|
+
// Delete, which is a direct, deterministic contract edit.
|
|
1466
|
+
async function connModifyRequest(j){
|
|
1467
|
+
if(!j) return;
|
|
1468
|
+
try{await window.flushContract();}catch(_){}
|
|
1469
|
+
try{persist();}catch(_){}
|
|
1470
|
+
const kindName=j.kind==='base-plate'?'base plate':j.kind==='shear-plate'?'shear plate':'connection';
|
|
1471
|
+
const instruction='Modify the '+kindName+' connection "'+j.id+'" on member '+j.main+' (sheet '+((P&&P.sheet)||'?')+') — adjust, replace or move it per my request.';
|
|
1472
|
+
try{const res=await fetch('/api/contract-request',{method:'POST',headers:{'content-type':'application/json'},
|
|
1473
|
+
body:JSON.stringify({appId:APP_ID,instruction,intent:'modify',target:{sheet:(P&&P.sheet)||undefined,ids:[j.id,j.main]}})});
|
|
1474
|
+
toast(res.ok?'Change queued for your terminal AI session':'Could not queue the request');
|
|
1475
|
+
}catch(_){toast('Could not queue the request');}
|
|
1476
|
+
}
|
|
1420
1477
|
function panel(){
|
|
1421
1478
|
const p=document.getElementById('panel');
|
|
1422
1479
|
if(!selDimIds.size||!dimsVisible)dimSplitMode=false;document.body.classList.toggle('dimsplit',dimSplitMode); // split mode is meaningless without a (visible) dim selected — also disarms when dims are hidden
|
|
@@ -1499,6 +1556,38 @@ function panel(){
|
|
|
1499
1556
|
{const rm=document.getElementById('detRemove');if(rm)rm.onclick=()=>edit(()=>{C.detail_placements=(C.detail_placements||[]).filter(x=>x&&x.id!==detId);selIds.clear();});}
|
|
1500
1557
|
return;
|
|
1501
1558
|
}}
|
|
1559
|
+
// A WHOLE connection selected (Slice A) — the Component inspector: type badge + editability chip +
|
|
1560
|
+
// on-member link + part count + a read-only param summary, then Delete (direct contract edit) /
|
|
1561
|
+
// Modify (relay) / Edit-on-member. Precedes the single-part branch below (which handles the DRILLED case).
|
|
1562
|
+
{const cs=connSelInfo();
|
|
1563
|
+
if(cs&&cs.whole){
|
|
1564
|
+
const j=cs.joint,isBP=j.kind==='base-plate',pp=j.params||{};
|
|
1565
|
+
const plate=(partsById||{})[cs.conn+':plate']||null;
|
|
1566
|
+
const dim=(n)=>(n==null?'<span style="color:var(--mut)">auto</span>':esc(fmtFtIn(Number(n)/25.4)));
|
|
1567
|
+
const kv=(l,val)=>`<div style="display:flex;justify-content:space-between;gap:8px;font-size:12px;margin:3px 0"><span style="color:var(--mut)">${esc(l)}</span><span style="font-variant-numeric:tabular-nums">${val}</span></div>`;
|
|
1568
|
+
const sec=t=>`<div class=divrow><hr><span class=sect style="margin:0">${esc(t)}</span><hr></div>`;
|
|
1569
|
+
const sz=plate?dim(plate.width)+' × '+dim(plate.depth):'<span style="color:var(--mut)">auto</span>';
|
|
1570
|
+
let body='';
|
|
1571
|
+
if(isBP)body=sec('Plate')+kv('Size',sz)+kv('Thickness',plate?dim(plate.thickness):dim(pp.thickness))+sec('Anchors')+kv('Grid (cols × rows)',esc(`${pp.boltCols||2} × ${pp.boltRows||2}`))+kv('Diameter',pp.boltDia?dim(pp.boltDia):dim(24));
|
|
1572
|
+
else body=sec('Plate')+kv('Size',sz)+kv('Thickness',plate?dim(plate.thickness):dim(pp.plateThickness))+sec('Bolts')+kv('Grid (cols × rows)',esc(`${pp.boltCols||1} × ${pp.boltRows||3}`))+kv('Diameter',pp.boltDia?dim(pp.boltDia):dim(20))+sec('Weld')+kv('Leg',pp.weldLeg?dim(pp.weldLeg):dim(6));
|
|
1573
|
+
p.innerHTML=`<span class=badge>${isBP?'Base plate':'Shear plate'}</span>
|
|
1574
|
+
<div class=row style="margin:0 0 6px"><span class=chip style="border-color:var(--brand);color:#bfdbfe">Parametric — editable</span></div>
|
|
1575
|
+
<div class="row hint" style="margin:0 0 2px">On <button class=pilllink id=cmpMember data-tip="Select ${esc(j.main)}">${esc(j.main)}</button> · ${cs.childIds.length} parts</div>
|
|
1576
|
+
<div class="row hint" style="margin:0 0 6px;font-size:11px">Double-click to enter and pick a part · <b>Esc</b> steps back.</div>
|
|
1577
|
+
${body}
|
|
1578
|
+
<div class=divrow><hr></div>
|
|
1579
|
+
<div class="row f" style="gap:6px;flex-wrap:wrap">
|
|
1580
|
+
<button class=ghostw id=cmpEdit data-tip="Edit this connection's parameters on ${esc(j.main)}">✎ Edit parameters on ${esc(j.main)} →</button>
|
|
1581
|
+
<button class=ghostw id=cmpAsk data-tip="Record a request for your terminal AI to modify / replace / move this connection">Modify connection…</button>
|
|
1582
|
+
<button class=danger id=cmpDel data-tip="Remove this whole connection">Delete connection</button>
|
|
1583
|
+
</div>`;
|
|
1584
|
+
const toMember=()=>{if(window.Steel3DView&&window.Steel3DView.clearConnSel)window.Steel3DView.clearConnSel();selIds=new Set([j.main]);selDimIds.clear();sel3dDimIds.clear();render();};
|
|
1585
|
+
{const b=document.getElementById('cmpMember');if(b)b.onclick=toMember;}
|
|
1586
|
+
{const b=document.getElementById('cmpEdit');if(b)b.onclick=toMember;}
|
|
1587
|
+
{const b=document.getElementById('cmpAsk');if(b)b.onclick=()=>connModifyRequest(j);}
|
|
1588
|
+
{const b=document.getElementById('cmpDel');if(b)b.onclick=()=>edit(()=>{C.joints=(C.joints||[]).filter(x=>x!==j);selIds.clear();});}
|
|
1589
|
+
return;
|
|
1590
|
+
}}
|
|
1502
1591
|
// A derived CONNECTION PART selected in 3D (plate / bolt / weld / cope / stiffener) — show its details
|
|
1503
1592
|
// read-only (parts have no own state; their params live on the parent joint) + a jump to that member.
|
|
1504
1593
|
{const selList=[...selIds];
|
|
@@ -1523,7 +1612,7 @@ function panel(){
|
|
|
1523
1612
|
const sec=t=>`<div class=divrow><hr><span class=sect style="margin:0">${t}</span><hr></div>`;
|
|
1524
1613
|
let body='';
|
|
1525
1614
|
if(pk==='plate'&&j.kind==='shear-plate')body=sec('Plate')+kv('Width',dim(el&&el.width))+kv('Height',dim(el&&el.depth))+kv('Thickness',dim(el&&el.thickness))+kv('Weld leg',v('weldLeg','mm'))+kv('Clearance',v('clearance','mm'));
|
|
1526
|
-
else if(pk==='bolt')body=sec('Bolts')+kv('Grid (cols × rows)'
|
|
1615
|
+
else if(pk==='bolt')body=sec('Bolts')+kv('Grid (cols × rows)',esc(`${pp.boltCols||1} × ${pp.boltRows||3}`))+kv('Diameter',v('boltDia','mm'))+kv('Grade',pp.boltGrade?esc(pp.boltGrade):'A325'+dft)+kv('Pitch',v('boltPitch','mm'))+kv('Length','<span style="color:var(--mut)">auto (from grip)</span>');
|
|
1527
1616
|
else if(pk==='weld')body=sec('Weld')+kv('Leg',v('weldLeg','mm'));
|
|
1528
1617
|
else if(pk==='cope')body=sec('Cope')+kv('Length',dim(el&&el.width))+kv('Depth',dim(el&&el.depth))+kv('Re-entrant radius',v('copeRadius','mm'));
|
|
1529
1618
|
else if(pk==='stiff')body=sec('Stiffener')+`<div class=hint style="margin:0">Opposite-side web stiffener on the support.</div>`;
|
|
@@ -1538,8 +1627,9 @@ function panel(){
|
|
|
1538
1627
|
${lbl?`<div class="row" style="margin:3px 0 0;font-size:12px;color:var(--brand);font-variant-numeric:tabular-nums">${esc(lbl)}</div>`:''}
|
|
1539
1628
|
${body}
|
|
1540
1629
|
<div class=divrow><hr></div>
|
|
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>`;
|
|
1542
|
-
const
|
|
1630
|
+
<div class="row f" style="gap:6px;flex-wrap:wrap"><button class=ghostw id=partBack data-tip="Back to the whole connection (Esc)">◂ Connection</button><button class=ghostw id=partEdit data-tip="Select the parent member to edit this connection">✎ Edit on ${esc(j.main)} →</button></div>`;
|
|
1631
|
+
{const bb=document.getElementById('partBack');if(bb)bb.onclick=()=>{if(window.Steel3DView&&window.Steel3DView.ascendConn)window.Steel3DView.ascendConn();};}
|
|
1632
|
+
const eb=document.getElementById('partEdit');if(eb)eb.onclick=()=>{if(window.Steel3DView&&window.Steel3DView.clearConnSel)window.Steel3DView.clearConnSel();selIds=new Set([j.main]);selDimIds.clear();sel3dDimIds.clear();render();};
|
|
1543
1633
|
return;
|
|
1544
1634
|
}}
|
|
1545
1635
|
const arr=selArr();
|