@floless/app 0.77.0 → 0.79.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 +49 -9
- package/dist/web/steel-editor.html +201 -49
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -53093,7 +53093,7 @@ function appVersion() {
|
|
|
53093
53093
|
return resolveVersion({
|
|
53094
53094
|
isSea: isSea2(),
|
|
53095
53095
|
sqVersionXml: readSqVersionXml(),
|
|
53096
|
-
define: true ? "0.
|
|
53096
|
+
define: true ? "0.79.0" : void 0,
|
|
53097
53097
|
pkgVersion: readPkgVersion()
|
|
53098
53098
|
});
|
|
53099
53099
|
}
|
|
@@ -53103,7 +53103,7 @@ function resolveChannel(s) {
|
|
|
53103
53103
|
return "dev";
|
|
53104
53104
|
}
|
|
53105
53105
|
function appChannel() {
|
|
53106
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
53106
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.79.0" : void 0 });
|
|
53107
53107
|
}
|
|
53108
53108
|
|
|
53109
53109
|
// workflow-update.ts
|
|
@@ -53990,6 +53990,16 @@ var BASE_PLATE_PARAMS = {
|
|
|
53990
53990
|
edgeDist: [0, 5e3],
|
|
53991
53991
|
weldLeg: [0, 200]
|
|
53992
53992
|
};
|
|
53993
|
+
var SHEAR_PLATE_PARAMS = {
|
|
53994
|
+
plateThickness: [1, 100],
|
|
53995
|
+
plateHeight: [1, 5e3],
|
|
53996
|
+
plateWidth: [1, 2e3],
|
|
53997
|
+
boltDia: [1, 100],
|
|
53998
|
+
boltCols: [1, 10],
|
|
53999
|
+
boltRows: [1, 30],
|
|
54000
|
+
boltPitch: [1, 1e3],
|
|
54001
|
+
edgeDist: [0, 2e3]
|
|
54002
|
+
};
|
|
53993
54003
|
function sanitizeRecipe(raw) {
|
|
53994
54004
|
if (!raw || typeof raw !== "object") return void 0;
|
|
53995
54005
|
const r = raw;
|
|
@@ -54008,6 +54018,16 @@ function sanitizeRecipe(raw) {
|
|
|
54008
54018
|
}
|
|
54009
54019
|
return Object.keys(params).length ? { kind: r.kind, params } : void 0;
|
|
54010
54020
|
}
|
|
54021
|
+
if (r.kind === "shear-plate") {
|
|
54022
|
+
const params = {};
|
|
54023
|
+
for (const [k, [lo, hi]] of Object.entries(SHEAR_PLATE_PARAMS)) {
|
|
54024
|
+
if (k in num3) {
|
|
54025
|
+
if (!(num3[k] >= lo && num3[k] <= hi)) return void 0;
|
|
54026
|
+
params[k] = num3[k];
|
|
54027
|
+
}
|
|
54028
|
+
}
|
|
54029
|
+
return Object.keys(params).length ? { kind: r.kind, params } : void 0;
|
|
54030
|
+
}
|
|
54011
54031
|
return Object.keys(num3).length ? { kind: r.kind, params: num3 } : void 0;
|
|
54012
54032
|
}
|
|
54013
54033
|
async function extractConnection(companionId, ifcPath, id) {
|
|
@@ -60171,23 +60191,38 @@ function weightOf(contract, profile) {
|
|
|
60171
60191
|
var isMfMark = (p) => !!p && /(^|[^A-Z])MF($|[^A-Z])/i.test(p);
|
|
60172
60192
|
var len = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
60173
60193
|
function redundantDupIds(members) {
|
|
60174
|
-
const
|
|
60194
|
+
const foot = (m) => {
|
|
60175
60195
|
if (!m.wp || m.wp.length < 2) return null;
|
|
60176
60196
|
const r = (p) => `${Math.round(p[0] / 3)},${Math.round(p[1] / 3)}`;
|
|
60177
60197
|
const a = r(m.wp[0]), b = r(m.wp[1]);
|
|
60178
60198
|
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
60179
60199
|
};
|
|
60180
|
-
const
|
|
60200
|
+
const byFoot = /* @__PURE__ */ new Map();
|
|
60181
60201
|
for (const m of members) {
|
|
60182
|
-
const k =
|
|
60202
|
+
const k = foot(m);
|
|
60183
60203
|
if (!k) continue;
|
|
60184
|
-
(
|
|
60204
|
+
(byFoot.get(k) ?? byFoot.set(k, []).get(k)).push(m);
|
|
60185
60205
|
}
|
|
60186
60206
|
const out = /* @__PURE__ */ new Set();
|
|
60187
|
-
|
|
60188
|
-
if (grp.length < 2)
|
|
60189
|
-
grp.sort((a, b) => dupRank(b) - dupRank(a));
|
|
60207
|
+
const keepBest = (grp) => {
|
|
60208
|
+
if (grp.length < 2) return;
|
|
60209
|
+
grp.sort((a, b) => dupRank(b) - dupRank(a) || (dupElev(b) !== "na" ? 1 : 0) - (dupElev(a) !== "na" ? 1 : 0));
|
|
60190
60210
|
for (let i = 1; i < grp.length; i++) out.add(grp[i].id);
|
|
60211
|
+
};
|
|
60212
|
+
for (const grp of byFoot.values()) {
|
|
60213
|
+
if (grp.length < 2) continue;
|
|
60214
|
+
const explicit = new Set(grp.map(dupElev).filter((s) => s !== "na"));
|
|
60215
|
+
if (explicit.size <= 1) {
|
|
60216
|
+
keepBest(grp);
|
|
60217
|
+
continue;
|
|
60218
|
+
}
|
|
60219
|
+
const bySig = /* @__PURE__ */ new Map();
|
|
60220
|
+
for (const m of grp) {
|
|
60221
|
+
const s = dupElev(m);
|
|
60222
|
+
if (s === "na") continue;
|
|
60223
|
+
(bySig.get(s) ?? bySig.set(s, []).get(s)).push(m);
|
|
60224
|
+
}
|
|
60225
|
+
for (const sub of bySig.values()) keepBest(sub);
|
|
60191
60226
|
}
|
|
60192
60227
|
return out;
|
|
60193
60228
|
}
|
|
@@ -60197,6 +60232,11 @@ function dupRank(m) {
|
|
|
60197
60232
|
if (m.profile && m.profile.trim() !== "") s += 1;
|
|
60198
60233
|
return s;
|
|
60199
60234
|
}
|
|
60235
|
+
function dupElev(m) {
|
|
60236
|
+
const q = (v) => typeof v === "number" && isFinite(v) ? Math.round(v) : null;
|
|
60237
|
+
const sig = m.role === "column" ? [q(m.col?.tos), q(m.col?.bos)] : [q(m.ends?.[0]?.tos), q(m.ends?.[1]?.tos)];
|
|
60238
|
+
return sig.every((v) => v == null) ? "na" : sig.map((v) => v == null ? "" : v).join(":");
|
|
60239
|
+
}
|
|
60200
60240
|
function elevationAssumed(m) {
|
|
60201
60241
|
if (m.role === "column") return m.col?.tosDef !== false;
|
|
60202
60242
|
const ends = m.ends ?? [];
|
|
@@ -398,20 +398,21 @@
|
|
|
398
398
|
#m3dLegend .lrow.clip .clab{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
|
|
399
399
|
#m3dLegend .lrow.clip .lx{margin-left:0} /* the label's flex:1 already pushes On/Off + × to the right */
|
|
400
400
|
#m3dLegend .lrow.clip.sel{border-left:2px solid var(--brand);background:rgba(59,130,246,.16);padding-left:2px}
|
|
401
|
-
#m3dLegend .lrow.clip.sel .clab{color:var(--text)}
|
|
402
|
-
#m3dLegend .lrow.clip.sel .lsw{box-shadow:0 0 0 1.5px #f8fafc} /* white ring on the selected clip's swatch — mirrors the 3D endpoint ring */
|
|
403
|
-
#m3dLegend .cpill{font-size:9px;line-height:1;padding:2px 6px;border-radius:9px;border:1px solid #475569;background:#334155;color:var(--mut);text-transform:uppercase;letter-spacing:.04em;flex:none;box-shadow:none;cursor:pointer}
|
|
404
|
-
#m3dLegend .cpill.on{background:var(--brand);border-color:var(--brand);color:#fff}
|
|
405
|
-
#m3dLegend .cpill:hover{border-color:#64748b}
|
|
401
|
+
#m3dLegend .lrow.clip.sel .clab{color:var(--text)} /* selection is shown by the row-level .sel styling above (brand left-border + tint); the box is the enable toggle now */
|
|
406
402
|
#m3dLegend{position:absolute;left:12px;bottom:64px;display:none;flex-direction:column;gap:1px;max-height:40%;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:8px 10px;z-index:6;box-shadow:0 4px 14px rgba(0,0,0,.45);font-size:12px}
|
|
407
403
|
#m3dLegend .lhint{color:var(--mut);font-size:10px;margin-bottom:4px;max-width:230px;white-space:normal} /* wrap-guard: the hint never drives the panel wider than the rows, so it can't clip on any font */
|
|
408
|
-
#m3dLegend .lrow{display:flex;align-items:center;gap:7px;cursor:
|
|
404
|
+
#m3dLegend .lrow{display:flex;align-items:center;gap:7px;cursor:default;user-select:none;padding:2px 4px;border-radius:5px;white-space:nowrap} /* only the left-hand box toggles now (it sets cursor:pointer); the row still isolates on dbl-click + right-clicks for the menu */
|
|
409
405
|
#m3dLegend .lrow:hover{background:#33415580}
|
|
410
|
-
#m3dLegend .lrow.off{opacity:.4}
|
|
406
|
+
#m3dLegend .lrow.off{opacity:.4}
|
|
407
|
+
/* hidden/off (or a disabled clip) → the box goes HOLLOW (outline of its own colour); the row also dims. No grayscale — the empty box already signals "hidden". */
|
|
408
|
+
#m3dLegend .lrow.off .lsw,#m3dLegend .lrow.dim.dimoff .lsw{background:transparent;box-shadow:inset 0 0 0 1.6px var(--sw)}
|
|
411
409
|
#m3dLegend .lrow.solo{background:rgba(59,130,246,.12)} /* in the isolated set (Explorer-style multi-select highlight) */
|
|
412
|
-
#m3dLegend .lrow.
|
|
413
|
-
#m3dLegend .lrow.dim
|
|
414
|
-
#m3dLegend .
|
|
410
|
+
#m3dLegend .lrow.lsel{box-shadow:inset 2px 0 0 var(--brand)} /* selected in 3D — brand left-bar (distinct from .solo's isolate tint; the two can coexist) */
|
|
411
|
+
#m3dLegend .lrow.dim .lsw{--sw:#67e8f9} /* dim overlays are cyan — same filled(on)/hollow(off) box as every other row */
|
|
412
|
+
#m3dLegend .lrow.dim.dimoff{opacity:.55} /* off = a normal resting choice, gentler than .off; the box hollows via the shared rule above */
|
|
413
|
+
#m3dLegend .lsw{width:11px;height:11px;border-radius:2px;flex:none;background:var(--sw,#94a3b8);cursor:pointer} /* the visibility box: filled(--sw)=shown; the .off/.dimoff rule above hollows it when hidden */
|
|
414
|
+
#m3dLegend .lsw:hover{box-shadow:0 0 0 2px rgba(255,255,255,.25)}
|
|
415
|
+
#m3dLegend .lrow.off .lsw:hover,#m3dLegend .lrow.dim.dimoff .lsw:hover{box-shadow:inset 0 0 0 1.6px var(--sw),0 0 0 2px rgba(255,255,255,.25)}
|
|
415
416
|
#m3dLegend .lsec{color:#475569;font-size:10px;letter-spacing:.06em;text-transform:uppercase;margin:6px 0 2px;padding:0 4px}
|
|
416
417
|
/* member grouping: By profile / By type toggle + collapsible type categories (Phase 1) */
|
|
417
418
|
#m3dLegend .lmode{display:flex;border:1px solid var(--line);border-radius:6px;overflow:hidden;height:24px;margin-bottom:6px;flex:none}
|
|
@@ -434,6 +435,24 @@
|
|
|
434
435
|
#m3dLegend .lrow.flash{background:rgba(59,130,246,.12)}
|
|
435
436
|
.leg-drag-ghost{position:fixed;pointer-events:none;z-index:70;background:var(--panel);border:1px solid var(--brand);border-radius:5px;padding:3px 8px;display:flex;align-items:center;gap:7px;font:12px system-ui;color:var(--text);width:200px;opacity:.88;box-shadow:0 4px 16px rgba(0,0,0,.6)}
|
|
436
437
|
#m3dLegend .ldiv{height:1px;background:var(--line);margin:5px 2px}
|
|
438
|
+
/* Objects-list SEARCH — narrows the member/connection rows as you type; never the 3D scene, never Dims/Grid/Clip.
|
|
439
|
+
Built from the same tokens as #propPop .ppsearch (no new vocabulary); sits between the mode toggle and the hint. */
|
|
440
|
+
#m3dLegend .lsearch{display:flex;align-items:center;gap:6px;height:26px;margin-bottom:6px;padding:0 8px;background:var(--bg);border:1px solid var(--line);border-radius:6px;flex:none}
|
|
441
|
+
#m3dLegend .lsearch:focus-within{border-color:var(--brand)}
|
|
442
|
+
#m3dLegend .lsico{color:var(--mut);flex:none;display:inline-flex;align-items:center}
|
|
443
|
+
#m3dLegend .lsico svg{display:block}
|
|
444
|
+
#m3dLegend .lsearch input{flex:1;min-width:0;width:auto;height:auto;background:transparent;border:0;outline:none;color:var(--text);font:12px system-ui;padding:0}
|
|
445
|
+
#m3dLegend .lsearch input::placeholder{color:var(--mut)}
|
|
446
|
+
#m3dLegend .lsearch .lsx{color:var(--mut);font-size:14px;line-height:1;padding:0 3px;border-radius:4px;cursor:pointer;flex:none;visibility:hidden} /* clear — reuses the .lrow .lx delete-glyph recipe */
|
|
447
|
+
#m3dLegend .lsearch.has .lsx{visibility:visible}
|
|
448
|
+
#m3dLegend .lsearch .lsx:hover{color:#fecaca;background:#7f1d1d}
|
|
449
|
+
#m3dLegend .lrow.qhide,#m3dLegend .cat-hdr.qhide{display:none} /* filtered OUT by search → gone (distinct from .off = hidden-in-3D, which only dims the swatch) */
|
|
450
|
+
#m3dLegend .lsempty{color:var(--mut);font-size:11px;padding:10px 4px;text-align:center}
|
|
451
|
+
/* Show-all reset bar — panel-local entry to showAllGroups(); shown only when something is hidden/isolated. Clones the
|
|
452
|
+
.lsearch full-width box recipe; brand border on hover only (a one-shot action, not a mode → no solid fill). */
|
|
453
|
+
#m3dLegend .lreset{display:none;align-items:center;justify-content:center;gap:6px;height:26px;margin-bottom:6px;background:var(--bg);border:1px solid var(--line);border-radius:6px;color:var(--text);font:12px system-ui;cursor:pointer;flex:none;user-select:none}
|
|
454
|
+
#m3dLegend .lreset.show{display:flex}
|
|
455
|
+
#m3dLegend .lreset:hover{border-color:var(--brand);background:#1a2740}
|
|
437
456
|
#m3dCube{position:absolute;right:12px;top:56px;width:84px;height:84px;display:none;z-index:6;cursor:pointer;filter:drop-shadow(0 6px 14px rgba(0,0,0,.5))} /* top-right (Revit-style), below the toolbar row */
|
|
438
457
|
/* Tekla-style world-axis triad, bottom-right (where the cube used to sit). Passive readout
|
|
439
458
|
(pointer-events:none) — orientation is the ViewCube's job; this only SHOWS where world X/Y/Z point. */
|
|
@@ -1331,12 +1350,29 @@ function doSplit(m,pt){const pv=snapshot();ensureMeta(m);const base=JSON.parse(J
|
|
|
1331
1350
|
if(c.ends)c.ends=[mk(),base.ends?base.ends[1]:mk()]; // second half keeps the original far end
|
|
1332
1351
|
c.rfi=(_wt(c.profile)==null);P.members.push(c);selIds=new Set([m.id,c.id]);selDimIds.clear();geoMode=null;setGeo();pushUndo(pv);render();}
|
|
1333
1352
|
// --- duplicates: members with coincident geometry (same two work-points, order-independent, ~3px tol) ---
|
|
1334
|
-
function
|
|
1353
|
+
function dupFoot(m){if(!m||!m.wp||m.wp.length<2)return null;const r=p=>Math.round(p[0]/3)+','+Math.round(p[1]/3);const a=r(m.wp[0]),b=r(m.wp[1]);return a<b?a+'|'+b:b+'|'+a;}
|
|
1354
|
+
// Elevation signature. Two members at the SAME footprint but a DIFFERENT explicit top-of-steel are different
|
|
1355
|
+
// objects (a beam stacked over a beam); an UNSET elevation is a wildcard — it dedupes against whatever's at that
|
|
1356
|
+
// footprint (a genuine double-read that lost its callout on one copy). tos is rounded to the inch: real callout
|
|
1357
|
+
// elevations are whole values so sub-inch rounding boundaries don't arise; levels differ by feet, so a coarser
|
|
1358
|
+
// tolerance isn't needed. MIRROR of server/steel-confidence.ts dupElev — keep the two in sync.
|
|
1359
|
+
function dupElev(m){const q=v=>(typeof v==='number'&&isFinite(v))?Math.round(v):null;
|
|
1360
|
+
const sig=(m&&m.role==='column')?[q(m.col&&m.col.tos),q(m.col&&m.col.bos)]:[q(m&&m.ends&&m.ends[0]&&m.ends[0].tos),q(m&&m.ends&&m.ends[1]&&m.ends[1].tos)];
|
|
1361
|
+
return sig.every(v=>v==null)?'na':sig.map(v=>v==null?'':v).join(':');}
|
|
1335
1362
|
function dupScore(m){let s=0;if(_wt(m.profile)!=null)s+=2;if(m.profile&&!/^MF/i.test(m.profile))s+=1;return s;} // keep the most-resolved copy
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1363
|
+
// Coincident-member dedupe → the redundant ids. Group by 2D footprint; within a footprint: ≤1 distinct EXPLICIT
|
|
1364
|
+
// elevation ⇒ all copies of one member ⇒ keep the best (highest rankFn; an elevation-tagged copy wins ties), rest
|
|
1365
|
+
// redundant; ≥2 explicit elevations ⇒ distinct levels ⇒ keep the best per level, unset copies kept (ambiguous).
|
|
1366
|
+
// MIRROR of server/steel-confidence.ts redundantDupIds — keep in sync.
|
|
1367
|
+
function dedupeFootprintIds(members,rankFn){const byFoot={};for(const m of members){const k=dupFoot(m);if(!k)continue;(byFoot[k]=byFoot[k]||[]).push(m);}
|
|
1368
|
+
const out=[],keepBest=grp=>{if(grp.length<2)return;grp.sort((a,b)=>rankFn(b)-rankFn(a)||((dupElev(b)!=='na')-(dupElev(a)!=='na')));for(let i=1;i<grp.length;i++)out.push(grp[i].id);};
|
|
1369
|
+
for(const k in byFoot){const grp=byFoot[k];if(grp.length<2)continue;
|
|
1370
|
+
const exp=new Set(grp.map(dupElev).filter(s=>s!=='na'));
|
|
1371
|
+
if(exp.size<=1){keepBest(grp);continue;}
|
|
1372
|
+
const bySig={};for(const m of grp){const s=dupElev(m);if(s==='na')continue;(bySig[s]=bySig[s]||[]).push(m);}
|
|
1373
|
+
for(const s in bySig)keepBest(bySig[s]);}
|
|
1339
1374
|
return out;}
|
|
1375
|
+
function redundantDups(){return dedupeFootprintIds(P.members,dupScore);}
|
|
1340
1376
|
// --- merge collinear chords: same-profile, end-to-end, STRAIGHT beam runs → one member each.
|
|
1341
1377
|
// The skew read breaks a chord into collinear sub-segments at every rung; this rejoins each run.
|
|
1342
1378
|
// MIRROR of server/steel-merge.ts (the tested source of truth) — keep the two in lock-step.
|
|
@@ -1455,7 +1491,7 @@ function render(){
|
|
|
1455
1491
|
s+=renderPropLabels(); // right-click property-label chips (2D); 3D labels ride the div-overlay pool
|
|
1456
1492
|
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)
|
|
1457
1493
|
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();
|
|
1458
|
-
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)
|
|
1494
|
+
if(view3d&&window.Steel3DView){window.Steel3DView.setSelection(selIds);updateIsolateBtn();if(selIds.size&&window.Steel3DView.selectedClips&&window.Steel3DView.selectedClips().length)window.Steel3DView.setSelectedClips([]);refreshLegendSel();} // keep the 3D highlight + legend selection in sync; selecting a member clears any clip selection (exclusive)
|
|
1459
1495
|
try{updateConnCrumb();}catch(_){} // Connection Component breadcrumb follows the selection (3D-only; hidden at root)
|
|
1460
1496
|
syncPropLabelsAfterRender(); // corner-note + push labels to 3D + refresh the popup rows against the (possibly changed) selection
|
|
1461
1497
|
}
|
|
@@ -3054,6 +3090,23 @@ const view3dApi={
|
|
|
3054
3090
|
toast((had?'Base plate on '+col.id+' replaced with imported “'+(conn.name||'connection')+'”':'Base plate “'+(conn.name||'imported')+'” applied to '+col.id)+' — edit its parameters on the member');
|
|
3055
3091
|
return;
|
|
3056
3092
|
}
|
|
3093
|
+
// Slice E: a RECOGNIZED shear/fin plate dropped onto a BEAM → bake an EDITABLE shear-plate recipe joint on
|
|
3094
|
+
// the nearest end; expandShearPlate re-derives it there from the fitted params. Needs a beam at the pick.
|
|
3095
|
+
const beam=(rc&&rc.kind==='shear-plate'&&pick.anchorId)?P.members.find(m=>m&&m.id===pick.anchorId&&m.role==='beam'):null;
|
|
3096
|
+
if(beam){
|
|
3097
|
+
const id='cx'+Date.now().toString(36)+Math.floor(Math.random()*1e4).toString(36);
|
|
3098
|
+
// Nearest end via the resolved 3D geometry: end0 = beam.from (wp[0]), end1 = beam.to (wp[1]).
|
|
3099
|
+
const g=partsById[beam.id],d3=(a,b)=>a&&b?Math.hypot(a[0]-b[0],a[1]-b[1],a[2]-b[2]):Infinity;
|
|
3100
|
+
const endIdx=(g&&g.from&&g.to&&d3(pick.point,g.to)<d3(pick.point,g.from))?1:0;
|
|
3101
|
+
const joint={id,kind:'shear-plate',main:beam.id,at:'end'+endIdx,params:Object.assign({},rc.params),source:'user'};
|
|
3102
|
+
pendingConnSel=id; // its parts only exist after the 3D rebuild → select the whole connection there
|
|
3103
|
+
// One shear plate per beam END: replace any existing joint on this end (else two overlap and "edit on
|
|
3104
|
+
// member" would target the older joint, not this import).
|
|
3105
|
+
const had=(C.joints||[]).some(x=>x&&x.kind==='shear-plate'&&x.main===beam.id&&x.at==='end'+endIdx);
|
|
3106
|
+
edit(()=>{C.joints=(Array.isArray(C.joints)?C.joints:[]).filter(x=>!(x&&x.kind==='shear-plate'&&x.main===beam.id&&x.at==='end'+endIdx));C.joints.push(joint);selIds=new Set();});
|
|
3107
|
+
toast((had?'Shear plate on '+beam.id+' '+(endIdx?'end':'start')+' replaced with imported “'+(conn.name||'connection')+'”':'Shear plate “'+(conn.name||'imported')+'” applied to '+beam.id+' '+(endIdx?'end':'start'))+' — edit its parameters on the member');
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3057
3110
|
// Slice B: opaque custom mesh — bake at the picked point (joint.place); expandCustom re-expands it into
|
|
3058
3111
|
// the scene as one selectable unit. Unrecognized imports, and a recognized base plate NOT dropped on a
|
|
3059
3112
|
// column, land here (still faithful geometry) with a hint toward the editable path.
|
|
@@ -3062,7 +3115,7 @@ const view3dApi={
|
|
|
3062
3115
|
const joint={id,kind:'custom',name:conn.name||'Imported connection',place:pick.point,geometry:conn.geometry,source:'user'};
|
|
3063
3116
|
if(pick.anchorId)joint.main=pick.anchorId; // snapped to a member face → record it for the inspector's "on member" line
|
|
3064
3117
|
edit(()=>{if(!Array.isArray(C.joints))C.joints=[];C.joints.push(joint);selIds=new Set(conn.geometry.map((g,i)=>id+':'+(g.id||'m'+i)));});
|
|
3065
|
-
toast(rc?('Imported “'+(conn.name||'connection')+'” as geometry — drop it on a column to apply it as an editable base plate')
|
|
3118
|
+
toast(rc?('Imported “'+(conn.name||'connection')+'” as geometry — drop it on a '+(rc.kind==='shear-plate'?'beam end to apply it as an editable shear plate':'column to apply it as an editable base plate'))
|
|
3066
3119
|
:('Connection “'+(conn.name||'imported')+'” placed'+(pick.anchorId?' on '+pick.anchorId:'')+' — select it to move or replace'));
|
|
3067
3120
|
return;
|
|
3068
3121
|
}
|
|
@@ -3120,9 +3173,10 @@ async function detailRequest(intent,place,note){
|
|
|
3120
3173
|
body:JSON.stringify({appId:APP_ID,project:PROJECT||undefined,instruction,intent,target:{sheet:place.sheet||undefined,ids},snapshots:snaps})});
|
|
3121
3174
|
toast(res.ok?(intent==='create'?'Insert queued for your terminal AI session':'Change queued for your terminal AI session'):'Could not queue the request');
|
|
3122
3175
|
}catch(_){toast('Could not queue the request');}}
|
|
3123
|
-
// Build the 3D legend overlay from the live scene groups (per profile).
|
|
3124
|
-
//
|
|
3125
|
-
|
|
3176
|
+
// Build the 3D legend overlay from the live scene groups (per profile). Click the BOX to show/hide (filled =
|
|
3177
|
+
// shown, hollow = hidden); click a row (its name) to SELECT the object[s] in 3D — Ctrl/Cmd adds/removes, Shift
|
|
3178
|
+
// ranges; double-click a row to isolate. leg3dClickT defers the plain row-click so a dbl-click isolates instead.
|
|
3179
|
+
let leg3dClickT=null,legendAnchor=null,legendSelAnchor=null;
|
|
3126
3180
|
// Explorer-style multi-isolate on dbl-click of a legend group row: plain = isolate just this group; Ctrl = toggle
|
|
3127
3181
|
// it in/out of the isolated set; Shift = the contiguous range from the anchor row to this one (in displayed order).
|
|
3128
3182
|
function legendIsolate(k,e){
|
|
@@ -3179,6 +3233,10 @@ function profileKeyOf(m){return (m&&m.profile||'').trim().toUpperCase();} // ma
|
|
|
3179
3233
|
function categoryOfProfile(profKey){for(const m of (P.members||[]))if(profileKeyOf(m)===profKey)return memberTypeOf(m);return 'beam';} // a profile-group's category = the type of its member(s)
|
|
3180
3234
|
let legendMode=(localStorage.getItem('floless.legendMode')==='type')?'type':'profile';
|
|
3181
3235
|
let collapsedCats=new Set((()=>{try{return JSON.parse(localStorage.getItem('floless.legendCollapsed')||'[]');}catch{return [];}})());
|
|
3236
|
+
let legendQuery=''; // transient objects-list search filter (members + connections only) — NOT persisted
|
|
3237
|
+
// While a search is active, object categories (member types + connections) render EXPANDED so a match inside a
|
|
3238
|
+
// manually-collapsed category still surfaces — WITHOUT mutating the persisted collapsedCats.
|
|
3239
|
+
function catForceOpen(cat){return !!legendQuery&&(MEMBER_TYPES.some(t=>t.k===cat)||/^conn-/.test(cat));}
|
|
3182
3240
|
function saveLegendPrefs(){try{localStorage.setItem('floless.legendMode',legendMode);localStorage.setItem('floless.legendCollapsed',JSON.stringify([...collapsedCats]));}catch{}}
|
|
3183
3241
|
// Drag a typed member row onto another type category to retype it. Pointer Events (NOT the HTML5 drag API,
|
|
3184
3242
|
// which paints a white browser ghost on Windows). A 6px threshold tells a drag from the row's click(hide) /
|
|
@@ -3215,21 +3273,27 @@ const DIM_LABEL=Object.fromEntries(DIM_CATS);
|
|
|
3215
3273
|
// edge_clearance/cope_size come off the shear-plate fin plate + cope; base_plate/anchor_depth off the base plate.
|
|
3216
3274
|
const DIM_CONN=[{ct:'base-plate',label:'Base-plate',cats:['base_plate','anchor_depth']},{ct:'shear-plate',label:'Shear-plate',cats:['bolt_pitch','edge_clearance','cope_size']}];
|
|
3217
3275
|
function build3DLegend(){const host=document.getElementById('m3dLegend');if(!host||!window.Steel3DView)return;
|
|
3276
|
+
if(!host._ctxWired){host._ctxWired=true;host.addEventListener('contextmenu',e=>e.preventDefault());} // right-click does nothing now (menu removed) — suppress the native OS menu so it never leaks over the dark theme
|
|
3218
3277
|
const groups=window.Steel3DView.getGroups();host.replaceChildren();
|
|
3219
3278
|
if(!groups.length){host.style.display='none';return;}
|
|
3220
|
-
const hint=document.createElement('div');hint.className='lhint';hint.textContent='click hide
|
|
3279
|
+
const hint=document.createElement('div');hint.className='lhint';hint.textContent='click to select · box = show/hide (selection) · dbl-click = isolate (selection) · Ctrl/Shift to multi-select';host.appendChild(hint);
|
|
3221
3280
|
const addRow=(g,indent,draggable)=>{const row=document.createElement('div');row.className='lrow'+(indent?' typed':'');row.dataset.key=g.key;
|
|
3222
3281
|
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
|
|
3223
|
-
const sw=document.createElement('span');sw.className='lsw';sw.style.
|
|
3282
|
+
const sw=document.createElement('span');sw.className='lsw';sw.style.setProperty('--sw',g.color);sw.setAttribute('role','checkbox');sw.dataset.tip='Show / hide';
|
|
3224
3283
|
row.append(sw,document.createTextNode(g.label));
|
|
3225
|
-
row.
|
|
3226
|
-
|
|
3284
|
+
// Show/hide lives on the BOX; dbl-click a row isolates. When the row is part of a multi-selection, the box toggles
|
|
3285
|
+
// ALL selected together and the dbl-click isolates the whole selection. stopPropagation on the box so a dbl-click
|
|
3286
|
+
// landing on it doesn't also fire row-isolate.
|
|
3287
|
+
sw.addEventListener('click',e=>{e.stopPropagation();legendBoxToggle(row);});
|
|
3288
|
+
sw.addEventListener('dblclick',e=>e.stopPropagation());
|
|
3289
|
+
row.addEventListener('dblclick',e=>{e.preventDefault();clearTimeout(leg3dClickT);if(row.classList.contains('lsel'))legendIsolateSel();else legendIsolate(g.key,e);});
|
|
3290
|
+
row.addEventListener('click',e=>legendRowClick(e,row)); // click the name to SELECT (Ctrl/Cmd add · Shift range); plain click is deferred so a dbl-click isolates instead
|
|
3227
3291
|
if(draggable)wireRowDrag(row,g);
|
|
3228
3292
|
host.appendChild(row);return row;};
|
|
3229
3293
|
// A collapsible legend category: chevron (collapse) + tri-state master on/off (■/□/◪) + label + count.
|
|
3230
3294
|
// getState()→'on'|'off'|'mixed' drives the master glyph; onToggle() runs the master action (refresh follows).
|
|
3231
3295
|
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;
|
|
3232
|
-
const chev=Object.assign(document.createElement('span'),{className:'cat-chevron',textContent:
|
|
3296
|
+
const catOpen=!collapsedCats.has(cat)||catForceOpen(cat);const chev=Object.assign(document.createElement('span'),{className:'cat-chevron',textContent:catOpen?'▼':'▶'});
|
|
3233
3297
|
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';
|
|
3234
3298
|
const lab=Object.assign(document.createElement('span'),{className:'cat-label',textContent:label});
|
|
3235
3299
|
const cnt=Object.assign(document.createElement('span'),{className:'cat-count',textContent:'('+count+')'});
|
|
@@ -3249,12 +3313,28 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
3249
3313
|
mode.appendChild(b);}
|
|
3250
3314
|
host.insertBefore(mode,host.firstChild);
|
|
3251
3315
|
}
|
|
3316
|
+
if(members.length||conns.length){ // SEARCH box — narrows object rows (members + connections); shown whenever there are objects to filter
|
|
3317
|
+
const sb=document.createElement('div');sb.className='lsearch'+(legendQuery?' has':'');
|
|
3318
|
+
const ico=Object.assign(document.createElement('span'),{className:'lsico'});ico.setAttribute('aria-hidden','true');
|
|
3319
|
+
const NS='http://www.w3.org/2000/svg',svg=document.createElementNS(NS,'svg'); // magnifier built via DOM (no innerHTML), stroked with currentColor so it inherits --mut
|
|
3320
|
+
svg.setAttribute('viewBox','0 0 16 16');svg.setAttribute('width','12');svg.setAttribute('height','12');svg.setAttribute('fill','none');svg.setAttribute('stroke','currentColor');svg.setAttribute('stroke-width','1.6');svg.setAttribute('stroke-linecap','round');
|
|
3321
|
+
const cir=document.createElementNS(NS,'circle');cir.setAttribute('cx','7');cir.setAttribute('cy','7');cir.setAttribute('r','4.5');
|
|
3322
|
+
const lin=document.createElementNS(NS,'line');lin.setAttribute('x1','10.6');lin.setAttribute('y1','10.6');lin.setAttribute('x2','14');lin.setAttribute('y2','14');
|
|
3323
|
+
svg.append(cir,lin);ico.append(svg);
|
|
3324
|
+
const inp=document.createElement('input');inp.id='legSearch';inp.type='text';inp.placeholder='Search objects…';inp.autocomplete='off';inp.value=legendQuery;inp.setAttribute('role','searchbox');inp.setAttribute('aria-label','Search objects in the list');
|
|
3325
|
+
const clr=Object.assign(document.createElement('span'),{className:'lsx',textContent:'×'});clr.dataset.tip='Clear';
|
|
3326
|
+
inp.addEventListener('input',()=>onLegendSearchInput(inp.value));
|
|
3327
|
+
inp.addEventListener('keydown',e=>{if(e.key==='Escape'){e.stopPropagation();if(inp.value){inp.value='';onLegendSearchInput('');}else{inp.blur();}}});
|
|
3328
|
+
clr.addEventListener('click',()=>{if(!inp.value&&!legendQuery)return;inp.value='';onLegendSearchInput('');inp.focus();});
|
|
3329
|
+
sb.append(ico,inp,clr);
|
|
3330
|
+
host.insertBefore(sb,hint);
|
|
3331
|
+
}
|
|
3252
3332
|
if(legendMode==='type'&&members.length){ // group the profile-rows under their member-type categories
|
|
3253
3333
|
const byCat=new Map(MEMBER_TYPES.map(t=>[t.k,[]]));
|
|
3254
3334
|
for(const g of members){(byCat.get(categoryOfProfile(g.key))||byCat.get('beam')).push(g);}
|
|
3255
3335
|
for(const {k,label} of MEMBER_TYPES){const gs=byCat.get(k)||[],keys=gs.map(g=>g.key);
|
|
3256
3336
|
host.appendChild(buildCatHeader(k,label,gs.length,{empty:!gs.length,getState:()=>grpState(keys),onToggle:()=>grpToggle(keys),toggleTitle:'Show / hide all '+label.toLowerCase()+'s'}));
|
|
3257
|
-
if(!collapsedCats.has(k))for(const g of gs)addRow(g,true,true);}
|
|
3337
|
+
if(!collapsedCats.has(k)||catForceOpen(k))for(const g of gs)addRow(g,true,true);}
|
|
3258
3338
|
} else for(const g of members)addRow(g);
|
|
3259
3339
|
if(conns.length){ // group connection PARTS under their joint (Phase 2): each part-kind a row, hidden per-id
|
|
3260
3340
|
if(members.length){host.appendChild(Object.assign(document.createElement('div'),{className:'ldiv'}));}
|
|
@@ -3273,10 +3353,11 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
3273
3353
|
getState:()=>{const h=hiddenSet(),n=allIds.filter(id=>h.has(id)).length;return n===0?'on':(n===allIds.length?'off':'mixed');},
|
|
3274
3354
|
onToggle:()=>{const h=hiddenSet();window.Steel3DView.setIdsHidden(allIds,allIds.every(id=>!h.has(id)));}, // all-on → hide all; else show all
|
|
3275
3355
|
toggleTitle:'Show / hide the '+label.toLowerCase()+' connection'}));
|
|
3276
|
-
if(!collapsedCats.has(ck))for(const [grp,ids] of pk){const m=meta.get(grp)||{label:grp,color:'#94a3b8'};
|
|
3356
|
+
if(!collapsedCats.has(ck)||catForceOpen(ck))for(const [grp,ids] of pk){const m=meta.get(grp)||{label:grp,color:'#94a3b8'};
|
|
3277
3357
|
const row=document.createElement('div');row.className='lrow typed';row.dataset.connkey=ck+':'+grp;row._ids=ids;
|
|
3278
|
-
const sw=document.createElement('span');sw.className='lsw';sw.style.
|
|
3279
|
-
|
|
3358
|
+
const sw=document.createElement('span');sw.className='lsw';sw.style.setProperty('--sw',m.color);sw.setAttribute('role','checkbox');sw.dataset.tip='Show / hide';row.append(sw,document.createTextNode(m.label));
|
|
3359
|
+
sw.addEventListener('click',e=>{e.stopPropagation();legendBoxToggle(row);});sw.addEventListener('dblclick',e=>e.stopPropagation()); // box toggles this connection's parts (or the whole selection when this row is selected)
|
|
3360
|
+
row.addEventListener('click',e=>legendRowClick(e,row)); // click the row to select these connection parts (Ctrl/Shift multi)
|
|
3280
3361
|
host.appendChild(row);}
|
|
3281
3362
|
}
|
|
3282
3363
|
}
|
|
@@ -3286,11 +3367,12 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
3286
3367
|
// isolate), and a gentler off-state — off is a normal resting choice here, not a hidden-part warning.
|
|
3287
3368
|
const ov=C.dim_overlays||{};
|
|
3288
3369
|
const addDimRow=(cat,label,sub)=>{const row=document.createElement('div');row.className='lrow dim typed'+(sub?' sub':'');row.dataset.dim=cat;
|
|
3289
|
-
const sw=document.createElement('span');sw.className='lsw';
|
|
3370
|
+
const sw=document.createElement('span');sw.className='lsw';sw.setAttribute('role','checkbox');sw.dataset.tip='Show / hide';sw.setAttribute('aria-checked',String(ov[cat]!==false));
|
|
3290
3371
|
row.append(sw,document.createTextNode(label));
|
|
3291
3372
|
row.classList.toggle('dimoff',ov[cat]===false);
|
|
3292
|
-
// toggle the overlay; persist DIRECTLY (model-global, like dims3d — never via edit(), which would snapshot a per-plan undo)
|
|
3293
|
-
|
|
3373
|
+
// toggle the overlay from the BOX; persist DIRECTLY (model-global, like dims3d — never via edit(), which would snapshot a per-plan undo)
|
|
3374
|
+
sw.addEventListener('click',e=>{e.stopPropagation();const on=C.dim_overlays[cat]!==false;C.dim_overlays[cat]=!on;row.classList.toggle('dimoff',on);sw.setAttribute('aria-checked',String(!on));scheduleSave();refreshOverlayDims3d();});
|
|
3375
|
+
sw.addEventListener('dblclick',e=>e.stopPropagation());
|
|
3294
3376
|
host.appendChild(row);};
|
|
3295
3377
|
if(members.length||conns.length)host.appendChild(Object.assign(document.createElement('div'),{className:'ldiv'}));
|
|
3296
3378
|
const dimState=()=>{const on=DIM_CATS.filter(([k])=>C.dim_overlays[k]!==false).length;return on===0?'off':(on===DIM_CATS.length?'on':'mixed');};
|
|
@@ -3298,7 +3380,7 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
3298
3380
|
const dcState=cats=>{const on=cats.filter(k=>C.dim_overlays[k]!==false).length;return on===0?'off':(on===cats.length?'on':'mixed');};
|
|
3299
3381
|
const dcToggle=cats=>{const anyOn=cats.some(k=>C.dim_overlays[k]!==false);for(const k of cats)C.dim_overlays[k]=!anyOn;scheduleSave();refreshOverlayDims3d();build3DLegend();};
|
|
3300
3382
|
host.appendChild(buildCatHeader('dims','Dimensions',DIM_CATS.length,{getState:dimState,onToggle:dimToggle,toggleTitle:'Show / hide all dimension overlays'}));
|
|
3301
|
-
if(!collapsedCats.has('dims')){host.appendChild(Object.assign(document.createElement('div'),{className:'lhint',textContent:'click
|
|
3383
|
+
if(!collapsedCats.has('dims')){host.appendChild(Object.assign(document.createElement('div'),{className:'lhint',textContent:'click the box to show / hide'}));
|
|
3302
3384
|
for(const dc of DIM_CONN){const ck='dims-'+dc.ct; // middle category: overlays grouped by connection
|
|
3303
3385
|
host.appendChild(buildCatHeader(ck,dc.label,dc.cats.length,{sub:true,getState:()=>dcState(dc.cats),onToggle:()=>dcToggle(dc.cats),toggleTitle:'Show / hide all '+dc.label.toLowerCase()+' dimensions'}));
|
|
3304
3386
|
if(!collapsedCats.has(ck))for(const k of dc.cats)addDimRow(k,DIM_LABEL[k],true);}
|
|
@@ -3309,10 +3391,10 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
3309
3391
|
if(typeof P!=='undefined'&&P&&P.grid){
|
|
3310
3392
|
host.appendChild(Object.assign(document.createElement('div'),{className:'ldiv'}));
|
|
3311
3393
|
const grow=document.createElement('div');grow.className='lrow dim';grow.dataset.tip='Show / hide the structural grid (2D + 3D)';
|
|
3312
|
-
const gsw=document.createElement('span');gsw.className='lsw';gsw.style.
|
|
3394
|
+
const gsw=document.createElement('span');gsw.className='lsw';gsw.style.setProperty('--sw','#64748b');gsw.setAttribute('role','checkbox');gsw.dataset.tip='Show / hide';gsw.setAttribute('aria-checked',String(gridOn()));
|
|
3313
3395
|
grow.append(gsw,document.createTextNode('Grid lines'));
|
|
3314
3396
|
grow.classList.toggle('dimoff',!gridOn());
|
|
3315
|
-
|
|
3397
|
+
gsw.addEventListener('click',e=>{e.stopPropagation();gridSetVisible(!gridOn());});gsw.addEventListener('dblclick',e=>e.stopPropagation());
|
|
3316
3398
|
host.appendChild(grow);
|
|
3317
3399
|
}
|
|
3318
3400
|
// CLIP — the active clip planes/boxes (a third axis: each HIDES geometry beyond it). Click a row to enable/
|
|
@@ -3326,22 +3408,22 @@ function build3DLegend(){const host=document.getElementById('m3dLegend');if(!hos
|
|
|
3326
3408
|
if(collapsedCats.has('clip')){/* collapsed → no rows */}
|
|
3327
3409
|
else if(!clips.length){host.appendChild(Object.assign(document.createElement('div'),{className:'lhint',textContent:'(no clips)'}));}
|
|
3328
3410
|
else for(const c of clips){
|
|
3329
|
-
//
|
|
3330
|
-
const row=document.createElement('div');row.className='lrow clip typed'+(c.selected?' sel':''); //
|
|
3331
|
-
const sw=document.createElement('span');sw.className='lsw';sw.style.
|
|
3411
|
+
// Box = ENABLE / disable (filled = cutting, hollow = off); label = SELECT (click) / RENAME (dbl-click); × = DELETE.
|
|
3412
|
+
const row=document.createElement('div');row.className='lrow clip typed'+(c.selected?' sel':'')+(c.enabled?'':' off'); // disabled → .off hollows the box + dims the row, like a hidden part
|
|
3413
|
+
const sw=document.createElement('span');sw.className='lsw';sw.style.setProperty('--sw',c.kind==='box'?'#93c5fd':'#3b82f6');sw.setAttribute('role','checkbox');sw.setAttribute('aria-checked',String(!!c.enabled));sw.dataset.tip='Enable / disable this clip'; // box = lighter blue, plane = brand blue
|
|
3332
3414
|
const lab=document.createElement('span');lab.className='clab';lab.textContent=c.label;lab.dataset.tip='Click to select · double-click to rename';
|
|
3333
|
-
const tog=document.createElement('button');tog.className='cpill'+(c.enabled?' on':'');tog.textContent=c.enabled?'On':'Off';tog.dataset.tip='Enable / disable this clip';
|
|
3334
3415
|
const x=document.createElement('span');x.className='lx';x.textContent='×';x.dataset.tip='Delete this clip';
|
|
3335
|
-
row.append(sw,lab,
|
|
3336
|
-
sw.addEventListener('click',e=>{e.stopPropagation();
|
|
3416
|
+
row.append(sw,lab,x);
|
|
3417
|
+
sw.addEventListener('click',e=>{e.stopPropagation();window.Steel3DView.toggleClip(c.id);}); // box toggles enable; selecting (which reveals the 3D drag handles) is the label's job
|
|
3418
|
+
sw.addEventListener('dblclick',e=>e.stopPropagation());
|
|
3337
3419
|
let clipClickT=null;
|
|
3338
3420
|
lab.addEventListener('click',e=>{e.stopPropagation();clearTimeout(clipClickT);const ev={ctrlKey:e.ctrlKey,metaKey:e.metaKey,shiftKey:e.shiftKey};clipClickT=setTimeout(()=>clipSelect(c.id,ev),200);}); // deferred so a double-click (rename) can cancel the select
|
|
3339
3421
|
lab.addEventListener('dblclick',e=>{e.stopPropagation();e.preventDefault();clearTimeout(clipClickT);startClipRename(c,lab);});
|
|
3340
|
-
tog.addEventListener('click',e=>{e.stopPropagation();window.Steel3DView.toggleClip(c.id);});
|
|
3341
3422
|
x.addEventListener('click',e=>{e.stopPropagation();window.Steel3DView.removeClip(c.id);});
|
|
3342
3423
|
host.appendChild(row);
|
|
3343
3424
|
}
|
|
3344
|
-
|
|
3425
|
+
{const rst=Object.assign(document.createElement('div'),{className:'lreset',id:'m3dLegendReset',textContent:'Show all'});rst.setAttribute('role','button');rst.dataset.tip='Restore every hidden / isolated object in this panel';rst.addEventListener('click',legendReset);host.insertBefore(rst,host.firstChild);} // Show-all reset — panel's first child (above the mode toggle); visibility set by updateLegendReset (via refresh3DLegend)
|
|
3426
|
+
host.style.display='flex';refresh3DLegend();applyLegendFilter();refreshLegendSel();}
|
|
3345
3427
|
// The contextual Isolate / Show all toolbar button: visible when something's selected OR while isolated (so
|
|
3346
3428
|
// "Show all" stays reachable after the selection is cleared). Updated on selection change + via onIsolateChange.
|
|
3347
3429
|
function updateIsolateBtn(){const b=document.getElementById('m3dIso');if(!b||!window.Steel3DView||!window.Steel3DView.isIsolated)return;
|
|
@@ -3363,10 +3445,81 @@ function updateWorkBtn(){const b=document.getElementById('m3dWork'),ck=document.
|
|
|
3363
3445
|
function updateCatTog(hdr){const tog=hdr&&hdr.querySelector('.cat-tog');if(!tog||!hdr._getState||tog.style.display==='none')return;
|
|
3364
3446
|
const state=hdr._getState();tog.dataset.state=state;tog.textContent=state==='on'?'■':(state==='off'?'□':'◪');}
|
|
3365
3447
|
function refresh3DLegend(){if(!window.Steel3DView)return;const st=window.Steel3DView.groupState(),hidden=new Set(st.hidden),solo=new Set(st.solo);
|
|
3366
|
-
document.querySelectorAll('#m3dLegend .lrow[data-key]').forEach(r=>{const k=r.dataset.key;
|
|
3448
|
+
document.querySelectorAll('#m3dLegend .lrow[data-key]').forEach(r=>{const k=r.dataset.key;const off=hidden.has(k)||(solo.size>0&&!solo.has(k));r.classList.toggle('off',off);r.classList.toggle('solo',solo.size>0&&solo.has(k));const sw=r.querySelector('.lsw');if(sw)sw.setAttribute('aria-checked',String(!off));}); // PROFILE rows (data-key); .off = hidden or outside the isolated set → box hollows; .solo = inside it (Explorer-style highlight)
|
|
3367
3449
|
const ch=new Set(window.Steel3DView.connHiddenIds?window.Steel3DView.connHiddenIds():[]); // per-part connection hide
|
|
3368
|
-
document.querySelectorAll('#m3dLegend .lrow[data-connkey]').forEach(r=>{const ids=r._ids||[];
|
|
3369
|
-
document.querySelectorAll('#m3dLegend .cat-hdr').forEach(updateCatTog);} // refresh the type-category master toggles
|
|
3450
|
+
document.querySelectorAll('#m3dLegend .lrow[data-connkey]').forEach(r=>{const ids=r._ids||[];const off=ids.length>0&&ids.every(id=>ch.has(id));r.classList.toggle('off',off);const sw=r.querySelector('.lsw');if(sw)sw.setAttribute('aria-checked',String(!off));});
|
|
3451
|
+
document.querySelectorAll('#m3dLegend .cat-hdr').forEach(updateCatTog);updateLegendReset();} // refresh the type-category master toggles + the show-all reset bar's visibility
|
|
3452
|
+
// ── Objects-list SELECTION — click a row (its name) to select its object[s] in 3D; Ctrl/Cmd add/remove, Shift range ──
|
|
3453
|
+
// Plain click is deferred (leg3dClickT) so a dbl-click can isolate instead; a modified click selects immediately
|
|
3454
|
+
// (so rapid Ctrl-clicking several rows doesn't lose one to the shared timer). Box clicks stopPropagation, so they never reach here.
|
|
3455
|
+
function legendRowClick(e,row){if(row&&row._dragging)return;const mods={ctrl:e.ctrlKey||e.metaKey,shift:e.shiftKey};clearTimeout(leg3dClickT);if(mods.ctrl||mods.shift)legendSelect(row,mods);else leg3dClickT=setTimeout(()=>legendSelect(row,mods),220);}
|
|
3456
|
+
function legendSelect(row,mods){
|
|
3457
|
+
if(!row)return;const ids=legRowIds(row);if(!ids.length)return;
|
|
3458
|
+
if(mods&&mods.shift&&legendSelAnchor&&document.body.contains(legendSelAnchor)){ // Shift → union every VISIBLE object row from the anchor to here (a search-hidden row can't be range-selected)
|
|
3459
|
+
const rows=[...document.querySelectorAll('#m3dLegend .lrow[data-key]:not(.qhide),#m3dLegend .lrow[data-connkey]:not(.qhide)')];
|
|
3460
|
+
const i0=rows.indexOf(legendSelAnchor),i1=rows.indexOf(row);
|
|
3461
|
+
if(i0>=0&&i1>=0){const next=new Set();rows.slice(Math.min(i0,i1),Math.max(i0,i1)+1).forEach(r=>legRowIds(r).forEach(id=>next.add(id)));selIds=next;selDimIds.clear();sel3dDimIds.clear();render();return;}
|
|
3462
|
+
}
|
|
3463
|
+
if(mods&&mods.ctrl){const next=new Set(selIds);const all=ids.every(id=>next.has(id));ids.forEach(id=>all?next.delete(id):next.add(id));selIds=next;} // Ctrl → toggle this group in/out of the selection
|
|
3464
|
+
else selIds=new Set(ids); // plain → replace the selection
|
|
3465
|
+
legendSelAnchor=row;selDimIds.clear();sel3dDimIds.clear();render();
|
|
3466
|
+
}
|
|
3467
|
+
// A row lights up (.lsel) when EVERY object it represents is selected — so a legend click that selects the whole group shows it. Synced from render()'s 3D block + build3DLegend.
|
|
3468
|
+
function refreshLegendSel(){const host=document.getElementById('m3dLegend');if(!host||host.style.display==='none')return;
|
|
3469
|
+
host.querySelectorAll('.lrow[data-key],.lrow[data-connkey]').forEach(r=>{const ids=legRowIds(r);r.classList.toggle('lsel',ids.length>0&&ids.every(id=>selIds.has(id)));});}
|
|
3470
|
+
// Click a row's BOX → show/hide. If that row is part of the current selection, the box acts on EVERY selected row
|
|
3471
|
+
// (the clicked row's current state drives the direction). Members toggle by group key, connections by part id.
|
|
3472
|
+
function legendBoxToggle(row){if(!row||!window.Steel3DView)return;
|
|
3473
|
+
const rows=row.classList.contains('lsel')?[...document.querySelectorAll('#m3dLegend .lrow.lsel')]:[row];
|
|
3474
|
+
const willHide=!row.classList.contains('off'); // one direction for all: hide if the clicked box was shown, else show
|
|
3475
|
+
const keys=[],ids=[];for(const r of rows){if(r.dataset.connkey)ids.push(...(r._ids||[]));else if(r.dataset.key)keys.push(r.dataset.key);}
|
|
3476
|
+
if(keys.length&&window.Steel3DView.setGroupsHidden)window.Steel3DView.setGroupsHidden(keys,willHide);
|
|
3477
|
+
if(ids.length&&window.Steel3DView.setIdsHidden)window.Steel3DView.setIdsHidden(ids,willHide);
|
|
3478
|
+
refresh3DLegend();}
|
|
3479
|
+
// Dbl-click a SELECTED row → isolate the whole selection (all selected profile groups, Explorer-style solo). Only
|
|
3480
|
+
// reachable from a member row's dbl-click (connection rows have no dbl-click), so the double-clicked member's own
|
|
3481
|
+
// key is always present — no empty-keys path.
|
|
3482
|
+
function legendIsolateSel(){if(!window.Steel3DView)return;
|
|
3483
|
+
const keys=[...document.querySelectorAll('#m3dLegend .lrow.lsel[data-key]')].map(r=>r.dataset.key);
|
|
3484
|
+
if(keys.length){window.Steel3DView.setSoloGroups(keys);refresh3DLegend();}}
|
|
3485
|
+
// "Show all" reset — a panel-local door onto showAllGroups() (which already clears box-hides, solo, isolate AND
|
|
3486
|
+
// per-connection hides in one call, then refreshes). Does NOT clear the search filter — the search box's own × owns that.
|
|
3487
|
+
function legendReset(){if(!window.Steel3DView)return;window.Steel3DView.showAllGroups();if(window.Steel3DView.clearIsolation)window.Steel3DView.clearIsolation();}
|
|
3488
|
+
// Show the reset bar only when 3D visibility is non-default (something hidden / solo'd / isolated). Indifferent to search.
|
|
3489
|
+
function updateLegendReset(){const b=document.getElementById('m3dLegendReset');if(!b||!window.Steel3DView)return;
|
|
3490
|
+
const st=window.Steel3DView.groupState();
|
|
3491
|
+
const filtered=(st.hidden&&st.hidden.length>0)||(st.solo&&st.solo.length>0)||(window.Steel3DView.isIsolated&&window.Steel3DView.isIsolated())||(window.Steel3DView.connHiddenIds&&window.Steel3DView.connHiddenIds().length>0);
|
|
3492
|
+
b.classList.toggle('show',filtered);}
|
|
3493
|
+
// ── Objects-list SEARCH ──────────────────────────────────────────────────────────────────────────────────────
|
|
3494
|
+
// Search narrows the MEMBER + CONNECTION rows only (never Dimensions/Grid/Clip, never the 3D scene). Crossing
|
|
3495
|
+
// empty↔non-empty rebuilds once (so collapsed categories force-expand and their matches can surface); refining
|
|
3496
|
+
// within an active query is a cheap show/hide pass that keeps the input focused.
|
|
3497
|
+
function onLegendSearchInput(q){
|
|
3498
|
+
const prev=legendQuery;legendQuery=q;
|
|
3499
|
+
if((!!prev)!==(!!q)){build3DLegend();const inp=document.getElementById('legSearch');if(inp){inp.focus();try{inp.setSelectionRange(inp.value.length,inp.value.length);}catch(_){}}}
|
|
3500
|
+
else applyLegendFilter();
|
|
3501
|
+
}
|
|
3502
|
+
// Show/hide object rows by label; hide object categories left with no visible child; toggle the "no matches" line.
|
|
3503
|
+
function applyLegendFilter(){
|
|
3504
|
+
const host=document.getElementById('m3dLegend');if(!host)return;
|
|
3505
|
+
const q=(legendQuery||'').trim().toLowerCase();
|
|
3506
|
+
const old=host.querySelector('.lsempty');if(old)old.remove();
|
|
3507
|
+
const rows=[...host.querySelectorAll('.lrow[data-key],.lrow[data-connkey]')];
|
|
3508
|
+
if(!q){rows.forEach(r=>r.classList.remove('qhide'));host.querySelectorAll('.cat-hdr.qhide').forEach(h=>h.classList.remove('qhide'));return;}
|
|
3509
|
+
let any=false;
|
|
3510
|
+
rows.forEach(r=>{const hit=(r.textContent||'').toLowerCase().includes(q);r.classList.toggle('qhide',!hit);if(hit)any=true;});
|
|
3511
|
+
host.querySelectorAll('.cat-hdr').forEach(h=>{const cat=h.dataset.cat||'';
|
|
3512
|
+
if(!(MEMBER_TYPES.some(t=>t.k===cat)||/^conn-/.test(cat)))return; // leave the Dimensions/Clip headers untouched
|
|
3513
|
+
let vis=false;
|
|
3514
|
+
for(let n=h.nextElementSibling;n&&!n.classList.contains('cat-hdr')&&!n.classList.contains('lsec')&&!n.classList.contains('ldiv');n=n.nextElementSibling){
|
|
3515
|
+
if((n.matches('.lrow[data-key]')||n.matches('.lrow[data-connkey]'))&&!n.classList.contains('qhide')){vis=true;break;}}
|
|
3516
|
+
h.classList.toggle('qhide',!vis);});
|
|
3517
|
+
if(!any){const hint=host.querySelector('.lhint');const e=Object.assign(document.createElement('div'),{className:'lsempty',textContent:'No objects match “'+legendQuery.trim()+'”.'});
|
|
3518
|
+
if(hint&&hint.nextSibling)host.insertBefore(e,hint.nextSibling);else host.appendChild(e);}
|
|
3519
|
+
}
|
|
3520
|
+
// Resolve a row to the member/connection ids it represents. A member row's data-key is a profile key; a connection
|
|
3521
|
+
// row carries its part ids on row._ids. Used by legend click-to-select + the box/isolate SELECTION actions below.
|
|
3522
|
+
function legRowIds(row){if(!row)return [];if(row.dataset.connkey)return (row._ids||[]).slice();const k=row.dataset.key;if(!k)return [];return (P.members||[]).filter(m=>profileKeyOf(m)===k).map(m=>m.id);}
|
|
3370
3523
|
let bar3dWired=false;
|
|
3371
3524
|
function seg3dActive(sel,attr,val){document.querySelectorAll(sel+' button').forEach(b=>b.classList.toggle('on',b.getAttribute(attr)===val));}
|
|
3372
3525
|
// 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.
|
|
@@ -3867,10 +4020,9 @@ function _wt(profile){if(!profile)return null;
|
|
|
3867
4020
|
if(WT){const h=Object.entries(WT).find(([k])=>k.toUpperCase()===profile.toUpperCase());if(h&&h[1]!=null)return h[1];}
|
|
3868
4021
|
return _nominalPlf(profile);} // fall back to the lb/ft encoded in a standard designation
|
|
3869
4022
|
const _isMf=p=>!!p&&/(^|[^A-Z])MF($|[^A-Z])/i.test(p);
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
const out=new Set();for(const k in g){const grp=g[k];if(grp.length<2)continue;grp.sort((a,b)=>rank(b)-rank(a));for(let i=1;i<grp.length;i++)out.add(grp[i].id);}return out;}
|
|
4023
|
+
// Elevation-aware coincident dedupe for the browser confidence panel — same footprint+wildcard-elevation logic as
|
|
4024
|
+
// redundantDups / the server, so the in-browser confidence report and the server score agree on stacked members.
|
|
4025
|
+
function _confDupIds(members){return new Set(dedupeFootprintIds(members,m=>{let s=0;if(m.profile&&!_isMf(m.profile))s++;if(m.profile&&m.profile.trim()!=='')s++;return s;}));}
|
|
3874
4026
|
function _elevAssumed(m){if(m.role==='column')return !(m.col&&m.col.tosDef===false);const en=m.ends||[];if(!en.length)return true;return en.some(e=>e.tosDef!==false);}
|
|
3875
4027
|
function _scoreMember(m,dup){const plf=_wt(m.profile);const F=[];
|
|
3876
4028
|
if(plf==null){F.push({key:'profile',label:'Profile',state:'fail',detail:(!m.profile||!m.profile.trim())?'no profile assigned':_isMf(m.profile)?('unresolved mark "'+m.profile+'" — not an AISC size'):('"'+m.profile+'" not in the AISC weight table')});return {band:'rfi',factors:F};}
|