@floless/app 0.69.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 +366 -283
- package/dist/templates/dummy-report.flo +27 -9
- package/dist/web/aware.js +44 -3
- package/dist/web/index.html +2 -0
- package/dist/web/panels.js +3 -2
- package/dist/web/steel-3d-view.js +83 -6
- package/dist/web/steel-editor.html +93 -3
- package/package.json +1 -1
|
@@ -4,10 +4,11 @@ display-name: Dummy Report
|
|
|
4
4
|
description: |
|
|
5
5
|
A self-contained test workflow that produces a styled HTML report — no Tekla
|
|
6
6
|
model required. Neither node touches `model`, so it runs on the host bridge with
|
|
7
|
-
nothing open. Use it to preview the in-app HTML Viewer
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
nothing open. Use it to preview the in-app HTML Viewer, the "Made with FloLess"
|
|
8
|
+
report badge, and the viewer's "Open file" / "Reveal in folder" buttons end to
|
|
9
|
+
end without a real model. Two real tekla/exec nodes: a title input → a report
|
|
10
|
+
node that builds the HTML inline, saves a copy to a file in your temp folder, and
|
|
11
|
+
is the terminal the viewer renders.
|
|
11
12
|
exposes-as-agent: false
|
|
12
13
|
inputs:
|
|
13
14
|
title:
|
|
@@ -47,11 +48,14 @@ nodes:
|
|
|
47
48
|
args:
|
|
48
49
|
title: "{{ title-input.result.title }}"
|
|
49
50
|
code: |
|
|
50
|
-
// Dummy report — builds a styled HTML report inline
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
51
|
+
// Dummy report — builds a styled HTML report inline, saves a copy to a file
|
|
52
|
+
// in your temp folder, and returns the report as this terminal node's `html`
|
|
53
|
+
// (so the floless.app HTML Viewer renders it on double-click) plus that file's
|
|
54
|
+
// path as `saved_path` (so the viewer's "Open file" / "Reveal in folder"
|
|
55
|
+
// buttons have a real file to act on). Touches NO `model`, so it runs with no
|
|
56
|
+
// Tekla open. Manual HTML-escape (no System.Net available).
|
|
54
57
|
using System;
|
|
58
|
+
using System.IO;
|
|
55
59
|
using System.Text;
|
|
56
60
|
|
|
57
61
|
string title = "FloLess Test Report";
|
|
@@ -99,6 +103,20 @@ nodes:
|
|
|
99
103
|
sb.Append("<div class=\"ft\">This is a dummy report for testing the FloLess HTML viewer.</div>");
|
|
100
104
|
sb.Append("</div></body></html>");
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
string html = sb.ToString();
|
|
107
|
+
|
|
108
|
+
// Also SAVE the report to a real file on disk so the viewer's "Open file" /
|
|
109
|
+
// "Reveal in folder" buttons have something to act on. Best-effort: a write
|
|
110
|
+
// failure just means no saved file (the report still renders inline).
|
|
111
|
+
string savedPath = null;
|
|
112
|
+
try
|
|
113
|
+
{
|
|
114
|
+
var outPath = Path.Combine(Path.GetTempPath(), "floless-dummy-report.html");
|
|
115
|
+
File.WriteAllText(outPath, html);
|
|
116
|
+
savedPath = outPath;
|
|
117
|
+
}
|
|
118
|
+
catch { savedPath = null; }
|
|
119
|
+
|
|
120
|
+
return new { ok = true, title, html, saved_path = savedPath };
|
|
103
121
|
connections:
|
|
104
122
|
- { from: title-input, to: report }
|
package/dist/web/aware.js
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
const $reportSub = document.getElementById('report-sub');
|
|
25
25
|
const $reportOpen = document.getElementById('report-open');
|
|
26
26
|
const $reportShare = document.getElementById('report-share');
|
|
27
|
+
const $reportOpenFile = document.getElementById('report-open-file');
|
|
28
|
+
const $reportRevealFile = document.getElementById('report-reveal-file');
|
|
27
29
|
const $reportClose = document.getElementById('report-close');
|
|
28
30
|
const $reportStop = document.getElementById('report-stop');
|
|
29
31
|
const $stopRunBtn = document.getElementById('stop-run-btn');
|
|
@@ -1699,12 +1701,26 @@
|
|
|
1699
1701
|
// never a disabled affordance with no path forward).
|
|
1700
1702
|
// The last HTML painted (static or 3D) — so ↗ Open works for both surfaces.
|
|
1701
1703
|
let lastPaintedHtml = '';
|
|
1704
|
+
// The workflow-written output file the current report reported (its run's
|
|
1705
|
+
// `saved_path`), or '' when the run wrote none. Drives the ⧉ Open file / ▤ Reveal
|
|
1706
|
+
// buttons — both hide together when there's no file (#209). Hidden is the default:
|
|
1707
|
+
// paintReport clears it every paint, and only a run/cached-show with a real path
|
|
1708
|
+
// re-shows it — so a trigger/3D/other paint can never leak a previous file's path.
|
|
1709
|
+
let currentSavedPath = '';
|
|
1710
|
+
function showSavedPathActions(path) {
|
|
1711
|
+
currentSavedPath = typeof path === 'string' ? path : '';
|
|
1712
|
+
const has = !!currentSavedPath;
|
|
1713
|
+
$reportOpenFile.hidden = !has;
|
|
1714
|
+
$reportRevealFile.hidden = !has;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1702
1717
|
// Paint a report into the viewer. A STATIC report uses the scriptless srcdoc frame; an
|
|
1703
1718
|
// INTERACTIVE 3D doc (viewer-3d) uses the script-enabled frame via a data: URL (opaque
|
|
1704
1719
|
// origin, cannot touch the app page) and keeps a "Loading 3D view…" overlay until the doc
|
|
1705
1720
|
// posts `viewer-ready`. Exactly one frame is shown at a time.
|
|
1706
1721
|
function paintReport(html, interactive) {
|
|
1707
1722
|
lastPaintedHtml = html || '';
|
|
1723
|
+
showSavedPathActions(''); // default off; the caller re-shows if this report saved a file
|
|
1708
1724
|
if (interactive) {
|
|
1709
1725
|
$reportFrame.hidden = true; $reportFrame.srcdoc = '';
|
|
1710
1726
|
$reportFrame3d.hidden = false;
|
|
@@ -1727,6 +1743,7 @@
|
|
|
1727
1743
|
$reportFrame.srcdoc = ''; $reportFrame.hidden = false;
|
|
1728
1744
|
$reportFrame3d.hidden = true; $reportFrame3d.removeAttribute('src');
|
|
1729
1745
|
lastPaintedHtml = '';
|
|
1746
|
+
showSavedPathActions(''); // no report shown → no saved-file actions
|
|
1730
1747
|
}
|
|
1731
1748
|
|
|
1732
1749
|
// Double-click the HTML Viewer node → LOAD the last report (no run; running is
|
|
@@ -1748,6 +1765,7 @@
|
|
|
1748
1765
|
$reportSub.textContent = `Last report${sfx} — rendered from the run's output, never composed by the UI.`;
|
|
1749
1766
|
}
|
|
1750
1767
|
paintReport(cached.html, cached.interactive);
|
|
1768
|
+
showSavedPathActions(cached.savedPath); // re-show Open file / Reveal if this run saved one
|
|
1751
1769
|
} else {
|
|
1752
1770
|
clearReportFrames();
|
|
1753
1771
|
$reportShare.hidden = true; // nothing to share — hide, don't disable
|
|
@@ -2109,8 +2127,12 @@
|
|
|
2109
2127
|
$reportSub.innerHTML = `Live 3D view from <code>${escapeHtml(nodeId)}</code>${inputBadge ? ' · ' + escapeHtml(inputBadge) : ''} — drag to orbit, scroll to zoom, click an element to inspect.`;
|
|
2110
2128
|
}
|
|
2111
2129
|
paintReport(html, interactive);
|
|
2130
|
+
// If a node wrote an output file, the run reports its path — surface the
|
|
2131
|
+
// ⧉ Open file / ▤ Reveal buttons and cache it so a later double-click restores them (#209).
|
|
2132
|
+
const savedPath = res && typeof res.savedPath === 'string' ? res.savedPath : '';
|
|
2133
|
+
showSavedPathActions(savedPath);
|
|
2112
2134
|
markFirstRunDone(app.id); // a real run rendered → this workflow is no longer "new" (app captured pre-await; a mid-run switch must not mark the wrong workflow)
|
|
2113
|
-
lastReportByApp.set(currentId, { nodeId, label: inputBadge, html, interactive });
|
|
2135
|
+
lastReportByApp.set(currentId, { nodeId, label: inputBadge, html, interactive, savedPath });
|
|
2114
2136
|
$reportModal.dataset.html = '1';
|
|
2115
2137
|
appendNarration(`Rendered the ${interactive ? '3D view' : 'report'} from <strong>${escapeHtml(nodeId)}</strong>${inputBadge ? ' (' + escapeHtml(inputBadge) + ')' : ''} — live run against the model.`);
|
|
2116
2138
|
} catch (e) {
|
|
@@ -2664,6 +2686,22 @@
|
|
|
2664
2686
|
});
|
|
2665
2687
|
// The header ■ Stop run — the always-reachable twin of the overlay Stop (#39).
|
|
2666
2688
|
if ($stopRunBtn) $stopRunBtn.onclick = () => stopRun();
|
|
2689
|
+
// ⧉ Open file / ▤ Reveal in folder — act on the run's saved output file via the OS.
|
|
2690
|
+
// The sandboxed report can't (file:// is blocked from an http page), so the local
|
|
2691
|
+
// host shells it (#209). Fire-and-forget: a 400 (file moved/deleted, or bad type)
|
|
2692
|
+
// surfaces as a toast, never a console error.
|
|
2693
|
+
async function savedFileAction(endpoint, failMsg) {
|
|
2694
|
+
if (!currentSavedPath) return; // button is hidden in this state; belt-and-braces
|
|
2695
|
+
try {
|
|
2696
|
+
await api(endpoint, { method: 'POST', body: JSON.stringify({ path: currentSavedPath }) });
|
|
2697
|
+
} catch (e) {
|
|
2698
|
+
const detail = e && e.body && e.body.error;
|
|
2699
|
+
showToast(detail ? `${failMsg} — ${detail}` : failMsg, 'warn');
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
$reportOpenFile.onclick = () => savedFileAction('/api/open-file', 'Couldn’t open the file');
|
|
2703
|
+
$reportRevealFile.onclick = () => savedFileAction('/api/reveal', 'Couldn’t reveal the file');
|
|
2704
|
+
|
|
2667
2705
|
$reportOpen.onclick = () => {
|
|
2668
2706
|
const html = lastPaintedHtml || $reportFrame.srcdoc;
|
|
2669
2707
|
if (!html) return;
|
|
@@ -6392,12 +6430,15 @@
|
|
|
6392
6430
|
window.flolessBridge = {
|
|
6393
6431
|
api,
|
|
6394
6432
|
// Open report HTML in the existing sandboxed HTML Viewer (same modal + srcdoc
|
|
6395
|
-
// iframe the canvas report node uses — no new sandbox surface).
|
|
6396
|
-
|
|
6433
|
+
// iframe the canvas report node uses — no new sandbox surface). `savedPath` is
|
|
6434
|
+
// optional: pass the run's saved_path so a dashboard-panel run surfaces the same
|
|
6435
|
+
// ⧉ Open file / ▤ Reveal buttons as a canvas run (#209); omit → they stay hidden.
|
|
6436
|
+
showHtmlReport(title, html, savedPath) {
|
|
6397
6437
|
$reportTitle.textContent = title;
|
|
6398
6438
|
$reportSub.textContent = "Rendered from the run's output — never composed by the UI.";
|
|
6399
6439
|
showModal($reportModal);
|
|
6400
6440
|
paintReport(html);
|
|
6441
|
+
showSavedPathActions(savedPath);
|
|
6401
6442
|
},
|
|
6402
6443
|
// Refresh the footer requests counter after the Customize box queues a request.
|
|
6403
6444
|
loadRequests,
|
package/dist/web/index.html
CHANGED
|
@@ -395,6 +395,8 @@
|
|
|
395
395
|
<div class="report-actions">
|
|
396
396
|
<button id="report-stop" class="ghost report-stop" hidden data-tip="Stop the live session — the report stops updating on each change">■ Stop run</button>
|
|
397
397
|
<button id="report-share" class="ghost" hidden data-tip="Share this report as a link anyone can view — hosted on floless.io">↗ Share</button>
|
|
398
|
+
<button id="report-open-file" class="ghost" hidden data-tip="Open the saved file in its default app">⧉ Open file</button>
|
|
399
|
+
<button id="report-reveal-file" class="ghost" hidden data-tip="Show the saved file in its folder">▤ Reveal in folder</button>
|
|
398
400
|
<button id="report-open" class="ghost" data-tip="Open the report in a new browser tab">↗ Open</button>
|
|
399
401
|
<button id="report-close" data-tip="Close">×</button>
|
|
400
402
|
</div>
|
package/dist/web/panels.js
CHANGED
|
@@ -398,7 +398,8 @@
|
|
|
398
398
|
const payload = payloadFor(source);
|
|
399
399
|
const html = payload && typeof payload === 'object' && typeof payload.html === 'string' ? payload.html : null;
|
|
400
400
|
if (!html) { showToast('No report yet — run the workflow first.', 'info'); return; }
|
|
401
|
-
|
|
401
|
+
const savedPath = payload && typeof payload.saved_path === 'string' ? payload.saved_path : null;
|
|
402
|
+
if (bridge.showHtmlReport) bridge.showHtmlReport(title || 'Report', html, savedPath);
|
|
402
403
|
}
|
|
403
404
|
|
|
404
405
|
async function runAction(btn) {
|
|
@@ -415,7 +416,7 @@
|
|
|
415
416
|
return;
|
|
416
417
|
}
|
|
417
418
|
const html = res.report && res.report.html;
|
|
418
|
-
if (html && bridge.showHtmlReport) bridge.showHtmlReport(`${appId} · report`, html);
|
|
419
|
+
if (html && bridge.showHtmlReport) bridge.showHtmlReport(`${appId} · report`, html, res.savedPath);
|
|
419
420
|
else showToast(`Run complete · ${appId}`, 'ok');
|
|
420
421
|
} catch (err) {
|
|
421
422
|
const cred = err && err.body && err.body.credentialIssue;
|
|
@@ -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();
|