@floless/app 0.72.0 → 0.72.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/floless-server.cjs +2 -2
- package/dist/web/steel-3d-view.js +72 -20
- package/dist/web/steel-editor.html +18 -10
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -53022,7 +53022,7 @@ function appVersion() {
|
|
|
53022
53022
|
return resolveVersion({
|
|
53023
53023
|
isSea: isSea2(),
|
|
53024
53024
|
sqVersionXml: readSqVersionXml(),
|
|
53025
|
-
define: true ? "0.72.
|
|
53025
|
+
define: true ? "0.72.2" : void 0,
|
|
53026
53026
|
pkgVersion: readPkgVersion()
|
|
53027
53027
|
});
|
|
53028
53028
|
}
|
|
@@ -53032,7 +53032,7 @@ function resolveChannel(s) {
|
|
|
53032
53032
|
return "dev";
|
|
53033
53033
|
}
|
|
53034
53034
|
function appChannel() {
|
|
53035
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.72.
|
|
53035
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.72.2" : void 0 });
|
|
53036
53036
|
}
|
|
53037
53037
|
|
|
53038
53038
|
// workflow-update.ts
|
|
@@ -848,7 +848,12 @@ function fitCamera(box, dir) {
|
|
|
848
848
|
perspCam.position.copy(c).addScaledVector(d, dist);
|
|
849
849
|
orthoCam.position.copy(c).addScaledVector(d, dist);
|
|
850
850
|
const r = box.getBoundingSphere(new THREE.Sphere()).radius || dist;
|
|
851
|
-
const near = Math.max(dist / 2000, 0.5)
|
|
851
|
+
const near = Math.max(dist / 2000, 0.5); // small near so the wheel can zoom right up to a connection detail without clipping
|
|
852
|
+
// far must always clear the WHOLE model, not just the framed box — else a tight detail-zoom (dbl-click) puts the
|
|
853
|
+
// rest of the model behind the far plane and slices it with a view-perpendicular cut that reads as a skewed
|
|
854
|
+
// section. Size far from the scene bounds seen from the final camera position; the fit box only sets distance/zoom.
|
|
855
|
+
const sceneSph = sceneBox.getBoundingSphere(new THREE.Sphere());
|
|
856
|
+
const far = Math.max(dist + r * 4, perspCam.position.distanceTo(sceneSph.center) + sceneSph.radius) * 1.02;
|
|
852
857
|
perspCam.near = near; perspCam.far = far; perspCam.updateProjectionMatrix();
|
|
853
858
|
orthoCam.near = near; orthoCam.far = far; orthoCam.zoom = 1;
|
|
854
859
|
orthoBaseH = Math.max(extU, extR / aspect) * 1.15; // tight, aspect-preserving box fit (no ortho under-frame)
|
|
@@ -915,13 +920,25 @@ function applyDisplayMode() {
|
|
|
915
920
|
mat.needsUpdate = true;
|
|
916
921
|
}
|
|
917
922
|
}
|
|
923
|
+
// A work area in "show whole parts" mode acts as a spatial filter: a part whose world AABB touches the box shows
|
|
924
|
+
// in FULL (never sliced), a part entirely outside hides. Only evaluated when that mode is active (setFromObject
|
|
925
|
+
// per mesh is cheap here — this runs on toggles, not per frame).
|
|
926
|
+
const _waBox = new THREE.Box3();
|
|
927
|
+
function meshInWorkArea(m) {
|
|
928
|
+
if (!(workArea && workArea.enabled && workArea.whole)) return true;
|
|
929
|
+
_waBox.setFromObject(m); return !_waBox.isEmpty() && workArea.box.intersectsBox(_waBox);
|
|
930
|
+
}
|
|
931
|
+
// A mesh's visibility EXCEPT the work-area whole-parts filter — i.e. only the group/solo/isolate/legend hides.
|
|
932
|
+
// Split out so "Define from selection" can still bound a part that's hidden ONLY by the current work area (the
|
|
933
|
+
// new box replaces that area), and so applyGroupVisibility reads as one line. connHidden = per-part legend hide.
|
|
934
|
+
function visibleIgnoringWorkArea(m) {
|
|
935
|
+
const k = m.userData && m.userData.group;
|
|
936
|
+
const byGroup = !groupHidden.has(k) && (soloGroups.size === 0 || soloGroups.has(k));
|
|
937
|
+
const byIso = isolatedIds === null || isolatedIds.has(m.userData.id); // isolate-selected: only the isolated ids show
|
|
938
|
+
return byGroup && byIso && !connHidden.has(m.userData.id);
|
|
939
|
+
}
|
|
918
940
|
function applyGroupVisibility() {
|
|
919
|
-
for (const m of meshById.values())
|
|
920
|
-
const k = m.userData && m.userData.group;
|
|
921
|
-
const byGroup = !groupHidden.has(k) && (soloGroups.size === 0 || soloGroups.has(k));
|
|
922
|
-
const byIso = isolatedIds === null || isolatedIds.has(m.userData.id); // isolate-selected: only the isolated ids show
|
|
923
|
-
m.visible = byGroup && byIso && !connHidden.has(m.userData.id); // connHidden: per-part legend hide (connection rows split a shared part-kind by connection)
|
|
924
|
-
}
|
|
941
|
+
for (const m of meshById.values()) m.visible = visibleIgnoringWorkArea(m) && meshInWorkArea(m); // meshInWorkArea: work-area whole-parts spatial filter
|
|
925
942
|
}
|
|
926
943
|
function toggleGroup(k) { if (groupHidden.has(k)) groupHidden.delete(k); else groupHidden.add(k); soloGroups.clear(); applyGroupVisibility(); rebuildEndpoints(); refreshOverlayDims(); refreshDims(); }
|
|
927
944
|
// Hide/show a SET of groups in one pass (a legend type-category master toggle) — deterministic (not a flip),
|
|
@@ -961,7 +978,7 @@ const CLIP_PLANE_COLOR = 0x3b82f6, CLIP_BOX_COLOR = 0x93c5fd; // brand blue (p
|
|
|
961
978
|
function applyClips() {
|
|
962
979
|
if (!renderer) return;
|
|
963
980
|
const active = clips.filter((c) => c.enabled).flatMap((c) => c.planes);
|
|
964
|
-
if (workArea && workArea.enabled) active.push(...workArea.planes); // the work area sections the view too
|
|
981
|
+
if (workArea && workArea.enabled && !workArea.whole) active.push(...workArea.planes); // the work area sections the view too — unless it's in "show whole parts" mode (parts are hidden/shown whole via applyGroupVisibility, never sliced)
|
|
965
982
|
renderer.clippingPlanes = active.length ? active : EMPTY_CLIPS;
|
|
966
983
|
}
|
|
967
984
|
// 6 inward planes (keep INSIDE) for an axis-aligned Box3 → a section/clip box.
|
|
@@ -1046,7 +1063,7 @@ function clipState() {
|
|
|
1046
1063
|
clips: clips.map((c) => c.kind === 'box'
|
|
1047
1064
|
? { id: c.id, kind: 'box', enabled: c.enabled, label: c.label, box: { min: c.box.min.toArray(), max: c.box.max.toArray() } }
|
|
1048
1065
|
: { id: c.id, kind: 'plane', enabled: c.enabled, label: c.label, n: c.n.toArray(), point: c.point.toArray() }),
|
|
1049
|
-
workArea: workArea ? { enabled: workArea.enabled, box: { min: workArea.box.min.toArray(), max: workArea.box.max.toArray() } } : null,
|
|
1066
|
+
workArea: workArea ? { enabled: workArea.enabled, whole: !!workArea.whole, box: { min: workArea.box.min.toArray(), max: workArea.box.max.toArray() } } : null,
|
|
1050
1067
|
selected: [...selectedClipIds], seq: clipSeq,
|
|
1051
1068
|
};
|
|
1052
1069
|
}
|
|
@@ -1058,12 +1075,13 @@ function setClipState(s) {
|
|
|
1058
1075
|
: { id: d.id, kind: 'plane', enabled: d.enabled, label: d.label, n: new THREE.Vector3(...d.n), point: new THREE.Vector3(...d.point), planes: [] };
|
|
1059
1076
|
rebuildClipPlanes(c); return c;
|
|
1060
1077
|
});
|
|
1061
|
-
if (s.workArea) { const b = new THREE.Box3(new THREE.Vector3(...s.workArea.box.min), new THREE.Vector3(...s.workArea.box.max)); workArea = { enabled: s.workArea.enabled, box: b, planes: boxToPlanes(b) }; }
|
|
1078
|
+
if (s.workArea) { const b = new THREE.Box3(new THREE.Vector3(...s.workArea.box.min), new THREE.Vector3(...s.workArea.box.max)); workArea = { enabled: s.workArea.enabled, whole: s.workArea.whole !== false, box: b, planes: boxToPlanes(b) }; } // whole !== false → older snapshots (no field) restore as show-whole
|
|
1062
1079
|
else workArea = null;
|
|
1063
1080
|
selectedClipIds = new Set(s.selected || []);
|
|
1064
1081
|
if (typeof s.seq === 'number') clipSeq = Math.max(clipSeq, s.seq);
|
|
1065
|
-
applyClips(); renderClipGizmo(); renderWorkArea();
|
|
1082
|
+
applyClips(); renderClipGizmo(); renderWorkArea(); refreshWorkAreaVis();
|
|
1066
1083
|
if (api && api.onClipsChange) api.onClipsChange();
|
|
1084
|
+
if (api && api.onWorkAreaChange) api.onWorkAreaChange(workAreaState()); // undo/redo of a work-area edit must resync the Work-area button + its sliders, not just the model
|
|
1067
1085
|
}
|
|
1068
1086
|
|
|
1069
1087
|
// ---- clip gizmo: the selected clip's outline + draggable handles (a plane move-arrow, or 6 box face handles) ----
|
|
@@ -1237,29 +1255,41 @@ function renderWorkArea() {
|
|
|
1237
1255
|
workAreaHelper.material.depthTest = false; workAreaHelper.renderOrder = 995; // unclipped overlay pass keeps it visible through any clip
|
|
1238
1256
|
overlayScene.add(workAreaHelper);
|
|
1239
1257
|
}
|
|
1258
|
+
function refreshWorkAreaVis() { applyGroupVisibility(); rebuildEndpoints(); refreshOverlayDims(); refreshDims(); } // whole-parts mode hides/shows whole meshes → recompute visibility + dependent dims/endpoints (same trio the legend toggles use)
|
|
1240
1259
|
function setWorkAreaBox(box) {
|
|
1241
1260
|
if (!box || box.isEmpty()) return false;
|
|
1242
1261
|
if (api && api.beginClipEdit) api.beginClipEdit(); // undoable
|
|
1243
|
-
|
|
1244
|
-
|
|
1262
|
+
const whole = workArea ? workArea.whole : true; // keep the current cut/whole mode across a re-define; a brand-new work area defaults to "show whole parts" (no surprise slicing)
|
|
1263
|
+
workArea = { enabled: true, whole, planes: boxToPlanes(box), box: box.clone() };
|
|
1264
|
+
applyClips(); renderWorkArea(); refreshWorkAreaVis();
|
|
1245
1265
|
if (api && api.onWorkAreaChange) api.onWorkAreaChange(workAreaState());
|
|
1246
1266
|
return true;
|
|
1247
1267
|
}
|
|
1248
1268
|
function workAreaSetAll() { return setWorkAreaBox(sceneBox.clone()); } // "set work area to all objects" = model bounds
|
|
1249
1269
|
function workAreaFromSelection(pad = 150) { // define a new work area around the selection
|
|
1250
|
-
const box = new THREE.Box3(); for (const id of selIds) { const m = meshById.get(id); if (m && m
|
|
1270
|
+
const box = new THREE.Box3(); for (const id of selIds) { const m = meshById.get(id); if (m && visibleIgnoringWorkArea(m)) box.expandByObject(m); } // include a part hidden ONLY by the current work area — the new box supersedes it (a group/isolate-hidden part is still excluded)
|
|
1251
1271
|
if (box.isEmpty()) return false; box.expandByScalar(pad); return setWorkAreaBox(box);
|
|
1252
1272
|
}
|
|
1253
1273
|
function workAreaToggle(on) {
|
|
1254
1274
|
if (!workArea) return on === false ? false : workAreaSetAll(); // first toggle-on with no box → bound the whole model
|
|
1255
1275
|
if (api && api.beginClipEdit) api.beginClipEdit(); // undoable
|
|
1256
1276
|
workArea.enabled = on === undefined ? !workArea.enabled : !!on;
|
|
1257
|
-
applyClips(); renderWorkArea();
|
|
1277
|
+
applyClips(); renderWorkArea(); refreshWorkAreaVis();
|
|
1258
1278
|
if (api && api.onWorkAreaChange) api.onWorkAreaChange(workAreaState());
|
|
1259
1279
|
return workArea.enabled;
|
|
1260
1280
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1281
|
+
// "Show whole parts" toggle: whole → a part touching the box shows in full (nothing sliced); cut → hard planar
|
|
1282
|
+
// section at the box faces (the Tekla "cut parts by work area" behaviour). No-op without a work area.
|
|
1283
|
+
function workAreaSetWhole(on) {
|
|
1284
|
+
if (!workArea) return false;
|
|
1285
|
+
if (api && api.beginClipEdit) api.beginClipEdit(); // undoable
|
|
1286
|
+
workArea.whole = on === undefined ? !workArea.whole : !!on;
|
|
1287
|
+
applyClips(); refreshWorkAreaVis();
|
|
1288
|
+
if (api && api.onWorkAreaChange) api.onWorkAreaChange(workAreaState());
|
|
1289
|
+
return workArea.whole;
|
|
1290
|
+
}
|
|
1291
|
+
function clearWorkArea() { if (api && api.beginClipEdit && workArea) api.beginClipEdit(); workArea = null; applyClips(); renderWorkArea(); refreshWorkAreaVis(); if (api && api.onWorkAreaChange) api.onWorkAreaChange(null); }
|
|
1292
|
+
function workAreaState() { return workArea ? { on: workArea.enabled, whole: !!workArea.whole } : null; }
|
|
1263
1293
|
|
|
1264
1294
|
function frameAll() { fitCamera(sceneBox); }
|
|
1265
1295
|
const VIEWS = { top: [0, 0, 1], bottom: [0, 0, -1], front: [0, -1, 0], back: [0, 1, 0], right: [1, 0, 0], left: [-1, 0, 0], iso: [0.55, -0.8, 0.5] };
|
|
@@ -1469,7 +1499,8 @@ function endpointUnseen(p) {
|
|
|
1469
1499
|
return true;
|
|
1470
1500
|
}
|
|
1471
1501
|
function dimHiddenByIsolation(d) {
|
|
1472
|
-
|
|
1502
|
+
const waHides = !!(workArea && workArea.enabled && workArea.whole); // a whole-parts work area hides meshes too → a placed dim on a hidden part must drop, not float
|
|
1503
|
+
if (isolatedIds === null && soloGroups.size === 0 && groupHidden.size === 0 && !waHides) return false; // nothing hidden → every dim shows
|
|
1473
1504
|
return endpointUnseen(d.a) || endpointUnseen(d.b);
|
|
1474
1505
|
}
|
|
1475
1506
|
function refreshDims() {
|
|
@@ -2454,6 +2485,22 @@ function membersInRect(x0, y0, x1, y1) {
|
|
|
2454
2485
|
}
|
|
2455
2486
|
return out;
|
|
2456
2487
|
}
|
|
2488
|
+
// Connection Components whose CENTRE falls inside a marquee rect (mirrors membersInRect's member-centre
|
|
2489
|
+
// test, applied to each connection's bounding box) — so area-select picks up connections, not just members.
|
|
2490
|
+
function connsInRect(x0, y0, x1, y1) {
|
|
2491
|
+
const rect = canvasEl.getBoundingClientRect();
|
|
2492
|
+
const lo = { x: Math.min(x0, x1), y: Math.min(y0, y1) }, hi = { x: Math.max(x0, x1), y: Math.max(y0, y1) };
|
|
2493
|
+
const conns = new Set();
|
|
2494
|
+
for (const m of meshById.values()) { const c = m.userData && m.userData.conn; if (c && m.visible) conns.add(c); }
|
|
2495
|
+
const out = [];
|
|
2496
|
+
for (const conn of conns) {
|
|
2497
|
+
const b = connBox(conn); if (b.isEmpty()) continue;
|
|
2498
|
+
const w = b.getCenter(new THREE.Vector3()).project(camera); if (w.z > 1) continue;
|
|
2499
|
+
const sx = rect.left + (w.x * 0.5 + 0.5) * rect.width, sy = rect.top + (-w.y * 0.5 + 0.5) * rect.height;
|
|
2500
|
+
if (sx >= lo.x && sx <= hi.x && sy >= lo.y && sy <= hi.y) out.push(conn);
|
|
2501
|
+
}
|
|
2502
|
+
return out;
|
|
2503
|
+
}
|
|
2457
2504
|
|
|
2458
2505
|
function onUp(e) {
|
|
2459
2506
|
if (e.button === 2) rightDownXY = null; // end the click-vs-drag test (rightMoved keeps the verdict for the contextmenu that follows)
|
|
@@ -2461,7 +2508,12 @@ function onUp(e) {
|
|
|
2461
2508
|
if (!renderer || !canvasEl || canvasEl.style.display === 'none') { downXY = null; boxSel = pending = dragging = null; if (controls) controls.enabled = true; return; } // 3D hidden mid-gesture → drop stale gesture state (no resume on re-show)
|
|
2462
2509
|
const bs = boxSel; boxSel = null;
|
|
2463
2510
|
if (bs) { // empty-space gesture: drag = box-select, click = clear selection
|
|
2464
|
-
if (bs.moved) { resetCycle();
|
|
2511
|
+
if (bs.moved) { resetCycle();
|
|
2512
|
+
const memberIds = membersInRect(bs.x, bs.y, e.clientX, e.clientY);
|
|
2513
|
+
const connIds = connsInRect(bs.x, bs.y, e.clientX, e.clientY);
|
|
2514
|
+
if (!memberIds.length && connIds.length === 1) selectWholeConn(connIds[0]); // a lone connection framed → full component select (envelope + inspector), same as a click
|
|
2515
|
+
else { resetConnState(); const ids = [...memberIds, ...connIds.flatMap((c) => connChildIds(c))]; if (api && api.onSelectMany) api.onSelectMany(ids); } // members and/or several connections → a plain multi-select that INCLUDES the connections' parts
|
|
2516
|
+
}
|
|
2465
2517
|
else clickSelect(e.clientX, e.clientY, e.ctrlKey || e.metaKey); // click in empty space → cycle-pick (may land on a derived part) or clear
|
|
2466
2518
|
downXY = null; return;
|
|
2467
2519
|
}
|
|
@@ -2723,7 +2775,7 @@ window.Steel3DView = {
|
|
|
2723
2775
|
toggleGroup, setGroupsHidden, setIdsHidden, connHiddenIds: () => [...connHidden], soloToggle, setSoloGroups, showAllGroups, groupState, getGroups,
|
|
2724
2776
|
setClipMode, clipMode: clipModeOn, addClipBox, toggleClip, removeClip, clearClips, getClips, renameClip, selectClip, setSelectedClips, selectedClips, deleteSelectedClips, clipState, setClipState,
|
|
2725
2777
|
isolateSelected, clearIsolation, isIsolated,
|
|
2726
|
-
workAreaSetAll, workAreaFromSelection, workAreaToggle, clearWorkArea, workAreaState,
|
|
2778
|
+
workAreaSetAll, workAreaFromSelection, workAreaToggle, workAreaSetWhole, clearWorkArea, workAreaState,
|
|
2727
2779
|
armWorkPlanePick, setWorkPlanePrincipal, clearWorkPlane, toggleWorkPlaneVisible, workPlaneInfo,
|
|
2728
2780
|
cmEscape, cmHasBase, cmClear3d, setCmAxis, cmLastClient, cmHudApply,
|
|
2729
2781
|
drClear3d, drEscape: () => { if (drDraft) { drDraft = null; drClear3d(); return true; } return false; },
|
|
@@ -126,10 +126,12 @@
|
|
|
126
126
|
body:not(.v3d) #moreMenu #insWrap{display:none} /* Insert detail places into the 3D scene — hide it in 2D (needs 2 ids to beat the .m3dwrap.ins-in-menu display:block) */
|
|
127
127
|
#moreMenu button.msnap{display:flex;align-items:center;gap:0}
|
|
128
128
|
#moreMenu button.msnap.on{color:var(--text)} /* the switch carries the state — don't also brand the text (reads as an armed tool elsewhere in this menu) */
|
|
129
|
-
#moreMenu .mck,.cmmenu .mck{position:relative;width:26px;height:14px;margin-right:9px;border-radius:7px;border:1px solid var(--line);background:#0b1220;flex:none;transition:background-color .15s,border-color .15s} /* delicate CSS-only slider switch — shared by the ⋯ Snapping rows
|
|
130
|
-
#moreMenu .mck::after,.cmmenu .mck::after{content:'';position:absolute;top:1px;left:1px;width:10px;height:10px;border-radius:50%;background:var(--mut);transition:transform .15s,background-color .15s}
|
|
131
|
-
#moreMenu button.msnap.on .mck,.cmmenu #dragMoveB.on .mck{background:rgba(59,130,246,.28);border-color:var(--brand)}
|
|
132
|
-
#moreMenu button.msnap.on .mck::after,.cmmenu #dragMoveB.on .mck::after{transform:translateX(12px);background:var(--brand)}
|
|
129
|
+
#moreMenu .mck,.cmmenu .mck,.m3dmenu .mck{position:relative;width:26px;height:14px;margin-right:9px;border-radius:7px;border:1px solid var(--line);background:#0b1220;flex:none;transition:background-color .15s,border-color .15s} /* delicate CSS-only slider switch — shared by the ⋯ Snapping rows, the Move/Copy → Drag-to-move/copy toggle, and the Work-area toggles */
|
|
130
|
+
#moreMenu .mck::after,.cmmenu .mck::after,.m3dmenu .mck::after{content:'';position:absolute;top:1px;left:1px;width:10px;height:10px;border-radius:50%;background:var(--mut);transition:transform .15s,background-color .15s}
|
|
131
|
+
#moreMenu button.msnap.on .mck,.cmmenu #dragMoveB.on .mck,.m3dmenu button.wtog.on .mck{background:rgba(59,130,246,.28);border-color:var(--brand)}
|
|
132
|
+
#moreMenu button.msnap.on .mck::after,.cmmenu #dragMoveB.on .mck::after,.m3dmenu button.wtog.on .mck::after{transform:translateX(12px);background:var(--brand)}
|
|
133
|
+
.m3dmenu button.wtog{display:flex;align-items:center;justify-content:flex-start;gap:0}
|
|
134
|
+
.m3dmenu button.wtog.on{color:var(--text)} /* the slider carries the on-state — don't also brand the label text */
|
|
133
135
|
#moreMenu button.msnap .sg{display:inline-block;width:17px;color:#22d3ee;opacity:.5;flex:none;transition:opacity .15s}
|
|
134
136
|
#moreMenu button.msnap.on .sg{opacity:1}
|
|
135
137
|
/* Quick-access snap bar — always-expanded row of glyph toggles, bottom-right of the canvas (both views); reuses the brand-fill "on" toolbar language */
|
|
@@ -308,7 +310,7 @@
|
|
|
308
310
|
.m3dmenu button{display:block;width:100%;text-align:left;background:transparent;border:0;border-radius:0;padding:7px 12px;color:var(--text);white-space:nowrap;font-size:12px;box-shadow:none}
|
|
309
311
|
.m3dmenu button:hover{background:#334155}
|
|
310
312
|
.m3dmenu button.on{color:var(--brand)} /* active choice in a radio-style menu (Camera / Display) */
|
|
311
|
-
.m3dmenu button.on::after{content:'✓';float:right;margin-left:16px;color:var(--brand)}
|
|
313
|
+
.m3dmenu button.on:not(.wtog)::after{content:'✓';float:right;margin-left:16px;color:var(--brand)} /* radio-style tick for Camera/Display choices — NOT the .wtog slider toggles (their .mck knob carries state) */
|
|
312
314
|
.m3dmenu button:disabled{opacity:.4;cursor:default;background:transparent}
|
|
313
315
|
.m3dmenu button.mdanger{color:#fca5a5} .m3dmenu button.mdanger:hover{background:#7f1d1d;color:#fecaca}
|
|
314
316
|
.m3dmenu hr{border:0;border-top:1px solid var(--line);margin:4px 0}
|
|
@@ -569,7 +571,8 @@
|
|
|
569
571
|
<button data-wa=all data-tip="Bound the work area to the whole model">Set to all objects</button>
|
|
570
572
|
<button data-wa=sel data-tip="Bound the work area to the current selection">Define from selection</button>
|
|
571
573
|
<hr>
|
|
572
|
-
<
|
|
574
|
+
<button id=m3dWorkOn class=wtog role=menuitemcheckbox aria-checked=false data-tip="Show or hide the work-area box"><span class=mck aria-hidden=true></span>Show work area</button>
|
|
575
|
+
<button id=m3dWorkWhole class=wtog role=menuitemcheckbox aria-checked=true style=display:none data-tip="When ON, any part that touches the work area is shown in full — nothing gets cut. When OFF, the work area slices parts cleanly at its box faces (a section cut)."><span class=mck aria-hidden=true></span>Show whole parts</button>
|
|
573
576
|
</div>
|
|
574
577
|
</div>
|
|
575
578
|
</div>
|
|
@@ -3091,10 +3094,14 @@ function updateIsolateBtn(){const b=document.getElementById('m3dIso');if(!b||!wi
|
|
|
3091
3094
|
b.classList.toggle('on',iso);
|
|
3092
3095
|
b.textContent=iso?'Show all':'Isolate';
|
|
3093
3096
|
b.setAttribute('data-tip', iso?'Restore all hidden members':'Isolate selected — hide everything else (Esc to exit)');} // themed tooltip, updated with state (no native title)
|
|
3094
|
-
// The Work area button + its
|
|
3095
|
-
|
|
3097
|
+
// The Work area button + its two slider toggles reflect the live work-area state (api.onWorkAreaChange).
|
|
3098
|
+
// "Show work area" = box drawn / sectioning on; "Show whole parts" = touching parts shown whole vs cut at the box.
|
|
3099
|
+
// The whole-parts row only appears once a work area exists (nothing to act on before that — no dimmed dead control).
|
|
3100
|
+
function updateWorkBtn(){const b=document.getElementById('m3dWork'),ck=document.getElementById('m3dWorkOn'),wh=document.getElementById('m3dWorkWhole');if(!b||!window.Steel3DView||!window.Steel3DView.workAreaState)return;
|
|
3096
3101
|
const st=window.Steel3DView.workAreaState(),on=!!(st&&st.on);
|
|
3097
|
-
b.classList.toggle('on',on);
|
|
3102
|
+
b.classList.toggle('on',on);
|
|
3103
|
+
if(ck){ck.classList.toggle('on',on);ck.setAttribute('aria-checked',String(on));}
|
|
3104
|
+
if(wh){wh.style.display=st?'':'none';wh.classList.toggle('on',!!(st&&st.whole));wh.setAttribute('aria-checked',String(!!(st&&st.whole)));}}
|
|
3098
3105
|
// A type category's master toggle reads its profiles' hide state: all visible = ■ on, all hidden = □ off,
|
|
3099
3106
|
// some = ◪ mixed. Colour comes from data-state (CSS); the glyph from the textContent.
|
|
3100
3107
|
function updateCatTog(hdr){const tog=hdr&&hdr.querySelector('.cat-tog');if(!tog||!hdr._getState||tog.style.display==='none')return;
|
|
@@ -3178,7 +3185,8 @@ function wire3DBar(){if(bar3dWired||!window.Steel3DView)return;bar3dWired=true;
|
|
|
3178
3185
|
function workMenuClose(){workMenu.classList.remove('open');document.removeEventListener('mousedown',workMenuOutside,true);}
|
|
3179
3186
|
workBtn.onclick=e=>{e.stopPropagation();if(workMenu.classList.contains('open'))workMenuClose();else{updateWorkBtn();workMenu.classList.add('open');document.addEventListener('mousedown',workMenuOutside,true);}};
|
|
3180
3187
|
workMenu.querySelectorAll('button[data-wa]').forEach(b=>b.onclick=()=>{workMenuClose();const a=b.dataset.wa;if(a==='all')d3.workAreaSetAll();else if(a==='sel'){if(!d3.workAreaFromSelection())d3.workAreaSetAll();}}); // define from selection; fall back to whole model if nothing's selected
|
|
3181
|
-
document.getElementById('m3dWorkOn').
|
|
3188
|
+
document.getElementById('m3dWorkOn').onclick=()=>d3.workAreaToggle(); // flip box on/off (creates one bound to the whole model on first turn-on); onWorkAreaChange refreshes the slider
|
|
3189
|
+
document.getElementById('m3dWorkWhole').onclick=()=>d3.workAreaSetWhole(); // flip show-whole vs cut-at-boundary; stays open so the state flip is visible
|
|
3182
3190
|
// Working plane: ◇ Plane menu (face pick / 3 points / principal+offset / show / reset). While a pick
|
|
3183
3191
|
// mode is armed the button is a cancel target (reflectWpBar renders '✕' — same pattern as Clip).
|
|
3184
3192
|
const wpBtn=document.getElementById('m3dWp'),wpMenu=document.getElementById('m3dWpMenu');
|