@floless/app 0.79.0 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/floless-server.cjs
CHANGED
|
@@ -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.80.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.80.0" : void 0 });
|
|
53107
53107
|
}
|
|
53108
53108
|
|
|
53109
53109
|
// workflow-update.ts
|
|
@@ -98,6 +98,19 @@ export function elevationLevels(members, ptPerFt, defaultTosMm, excludeId) {
|
|
|
98
98
|
return [...set].sort((a, b) => a - b);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Snap a raw elevation (mm) to the nearest level datum { z, label } within `tol`, returning the snapped
|
|
102
|
+
// value + its source, or the raw value when nothing is in range. Nearest wins. Pure — backs the Mode B
|
|
103
|
+
// column-base pick (the levels come from elevationLevels()). Malformed datums are skipped.
|
|
104
|
+
export function snapElevation(z, levels, tol) {
|
|
105
|
+
let best = null, bestD = tol;
|
|
106
|
+
for (const lv of levels || []) {
|
|
107
|
+
if (!lv || typeof lv.z !== 'number' || !isFinite(lv.z)) continue;
|
|
108
|
+
const d = Math.abs(z - lv.z);
|
|
109
|
+
if (d <= bestD) { bestD = d; best = lv; }
|
|
110
|
+
}
|
|
111
|
+
return best ? { z: best.z, snapped: true, label: best.label ?? null } : { z, snapped: false, label: null };
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
// Lower wins when two candidates are both within tolerance. Grid intersections rank with member
|
|
102
115
|
// intersections (columns land on them); grid lines below member centerlines.
|
|
103
116
|
const PRECEDENCE = { vertex: 0, intersection: 1, 'grid-int': 1, midpoint: 2, centerline: 3, 'vertical-axis': 4, 'grid-line': 5, level: 1 };
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import * as THREE from 'three';
|
|
18
18
|
import { OrbitControls } from 'three/addons/OrbitControls.js';
|
|
19
19
|
import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
20
|
-
import { snapCandidates, snapPoint, planPointToWp, memberGeometry, elevationLevels, dim3dGeom, planeBasis, projectToPlane, planeFrom3Points, vecToPlane } from './steel-3d-core.js';
|
|
20
|
+
import { snapCandidates, snapPoint, planPointToWp, memberGeometry, elevationLevels, snapElevation, dim3dGeom, planeBasis, projectToPlane, planeFrom3Points, vecToPlane } from './steel-3d-core.js';
|
|
21
21
|
import { gridGeometry, gridCandidates3d, mmPerPx } from './grid-core.js';
|
|
22
22
|
|
|
23
23
|
let renderer, scene, perspCam, orthoCam, camera, controls, root, api, canvasEl, ro, rafId, grid, raycaster, downXY, lastPick = '(none)';
|
|
@@ -31,6 +31,10 @@ let hoverEp = null, dragEp = null; // {id,end} of the hovered / dragged
|
|
|
31
31
|
let refGroup = null; let refLineOn = false; // optional reference (work) lines between each member's end points
|
|
32
32
|
let insertMode = false; // armed "place a vectored detail" pick (mirrors clipMode)
|
|
33
33
|
let insertPending = null; // {name,url} — the detail the editor queued for the next placement click
|
|
34
|
+
let basePickCol = null; // Mode B: armed "trim column to base" pick — the target column id, or null
|
|
35
|
+
let basePickLevels = null; // [{ z, label }] level datums the base pick snaps its elevation to
|
|
36
|
+
const ELEV_SNAP_PX = 10; // screen-pixel snap radius for the base-pick elevation (world tol via pxToWorldAt, so it's consistent at any zoom)
|
|
37
|
+
const BASE_PICK_HIT_PX = 40; // the base pick only commits when the click is within this many px of the column axis (rejects an accidental empty-canvas click)
|
|
34
38
|
let labelsOnFlag = false; // member mark/id overlay labels toggle (a readability aid)
|
|
35
39
|
let memberLabelHost = null; // fixed-position container for the member labels (positioned each frame)
|
|
36
40
|
const memberLabelPool = []; // reused <div> labels, one per member, positioned in the loop
|
|
@@ -631,6 +635,57 @@ function setInsertMode(on, pending) {
|
|
|
631
635
|
if (api && api.onInsertModeChange) api.onInsertModeChange(insertMode);
|
|
632
636
|
}
|
|
633
637
|
function insertModeOn() { return insertMode; }
|
|
638
|
+
|
|
639
|
+
// Mode B: arm a "trim column to base" pick on `colId`. The level datums are derived from the OTHER members'
|
|
640
|
+
// rendered top/bottom (world mm, so no display→mm conversion), each labelled by its member mark, so the pick
|
|
641
|
+
// snaps the base to a real framing level. A one-shot left-click hands the picked base elevation to the editor
|
|
642
|
+
// via api.onBasePick. Mirrors setInsertMode.
|
|
643
|
+
function setBasePickMode(colId) {
|
|
644
|
+
basePickCol = colId || null;
|
|
645
|
+
basePickLevels = null;
|
|
646
|
+
if (basePickCol) {
|
|
647
|
+
setInsertMode(false); if (api && api.disarmTransform) api.disarmTransform(); if (api && api.disarmAdd) api.disarmAdd(); setClipMode(null);
|
|
648
|
+
root.updateMatrixWorld(true);
|
|
649
|
+
basePickLevels = [];
|
|
650
|
+
const _b = new THREE.Box3();
|
|
651
|
+
for (const [id, m] of meshById) {
|
|
652
|
+
// MEMBER meshes only (their id is a plain mark): a ':' marks a connection part (`joint:part`) or a
|
|
653
|
+
// detail (`det:…`), so those are excluded — the base must snap to framing levels, not the moving
|
|
654
|
+
// base-plate/anchor/weld hardware of the very connection being re-seated.
|
|
655
|
+
if (id === basePickCol || !m.visible || String(id).includes(':')) continue;
|
|
656
|
+
_b.setFromObject(m); if (_b.isEmpty()) continue;
|
|
657
|
+
basePickLevels.push({ z: Math.round(_b.max.z), label: id + ' top' }, { z: Math.round(_b.min.z), label: id + ' base' });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (canvasEl) canvasEl.style.cursor = basePickCol ? 'crosshair' : 'default';
|
|
661
|
+
if (!basePickCol) { if (marker) marker.visible = false; if (readout) readout.style.display = 'none'; }
|
|
662
|
+
if (api && api.onBasePickModeChange) api.onBasePickModeChange(basePickModeOn());
|
|
663
|
+
}
|
|
664
|
+
function basePickModeOn() { return !!basePickCol; }
|
|
665
|
+
// A point along the target column's VERTICAL axis at the cursor, elevation snapped to the level datums.
|
|
666
|
+
// Intersect the pick ray with a vertical plane through the column axis facing the camera — so a click
|
|
667
|
+
// anywhere near the (thin) column reads as an elevation on its reference line. Returns { z, snapped, label, x, y }.
|
|
668
|
+
function basePickPoint(cx, cy) {
|
|
669
|
+
const col = meshById.get(basePickCol); if (!col) return null;
|
|
670
|
+
camera.updateMatrixWorld(); root.updateMatrixWorld(true);
|
|
671
|
+
const box = new THREE.Box3().setFromObject(col);
|
|
672
|
+
const ax = (box.min.x + box.max.x) / 2, ay = (box.min.y + box.max.y) / 2;
|
|
673
|
+
const rect = canvasEl.getBoundingClientRect(); if (!rect.width || !rect.height) return null;
|
|
674
|
+
const ndc = new THREE.Vector2(((cx - rect.left) / rect.width) * 2 - 1, -((cy - rect.top) / rect.height) * 2 + 1);
|
|
675
|
+
raycaster.setFromCamera(ndc, camera);
|
|
676
|
+
const toCam = new THREE.Vector3(camera.position.x - ax, camera.position.y - ay, 0);
|
|
677
|
+
if (toCam.lengthSq() < 1e-6) toCam.set(1, 0, 0);
|
|
678
|
+
toCam.normalize();
|
|
679
|
+
const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(toCam, new THREE.Vector3(ax, ay, 0));
|
|
680
|
+
const g = new THREE.Vector3();
|
|
681
|
+
if (!raycaster.ray.intersectPlane(plane, g)) return null;
|
|
682
|
+
// The plane is infinite, so gate the pick to clicks NEAR the column axis (else an accidental empty-canvas
|
|
683
|
+
// click would commit a trim). g lies on the plane through the axis; its horizontal offset from the axis is
|
|
684
|
+
// how far to the side the cursor is.
|
|
685
|
+
if (Math.hypot(g.x - ax, g.y - ay) > pxToWorldAt(BASE_PICK_HIT_PX, g)) return null;
|
|
686
|
+
const s = snapElevation(g.z, basePickLevels, pxToWorldAt(ELEV_SNAP_PX, g)); // screen-scaled tol so the snap window is ~constant on-screen
|
|
687
|
+
return { z: s.z, snapped: s.snapped, label: s.label, x: ax, y: ay };
|
|
688
|
+
}
|
|
634
689
|
// The placement raycast: a member face under the cursor gives the point + its basis + the member id to
|
|
635
690
|
// anchor to; empty space drops the detail on the ground plane (z=0) with a default vertical orientation.
|
|
636
691
|
function insertPick(cx, cy) {
|
|
@@ -1348,6 +1403,7 @@ function onKey(e) {
|
|
|
1348
1403
|
if (pending && pending.clipDrag && e.key === 'Escape') { e.preventDefault(); const p = pending; if (p.plane) p.clip.point = p.prePoint; else p.clip.box.copy(p.preBox); rebuildClipPlanes(p.clip); applyClips(); renderClipGizmo(); pending = dragging = null; if (controls) controls.enabled = true; if (readout) readout.style.display = 'none'; return; } // Esc mid-drag → undo the handle move
|
|
1349
1404
|
if (wpMode && e.key === 'Escape') { e.preventDefault(); if (wpMode === '3pt' && wpDraft && wpDraft.length) { wpDraft.pop(); updateStatusChip(); } else { wpMode = null; wpDraft = null; marker.visible = false; canvasEl.style.cursor = 'default'; reflectWpBar(); updateStatusChip(); } return; } // Esc steps a 3pt pick back, else disarms the set-plane mode
|
|
1350
1405
|
if (insertMode && e.key === 'Escape') { e.preventDefault(); setInsertMode(false); if (api && api.toast) api.toast('Insert cancelled'); return; } // Esc disarms the detail-placement pick
|
|
1406
|
+
if (basePickCol && e.key === 'Escape') { e.preventDefault(); setBasePickMode(null); if (api && api.toast) api.toast('Trim cancelled'); return; } // Esc disarms the Mode B column-base pick
|
|
1351
1407
|
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
|
|
1352
1408
|
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)
|
|
1353
1409
|
if (e.key === 'Escape' && !dimMode3d && !cmActive() && ascendConn()) { e.preventDefault(); return; } // Esc ascends the connection drill: part → whole → nothing
|
|
@@ -2341,6 +2397,7 @@ function onDown(e) {
|
|
|
2341
2397
|
return; }
|
|
2342
2398
|
if (addActive()) { e.stopPropagation(); controls.enabled = true; drClick(e); return; } // Add-member armed (editor state) → two plane picks draw a member
|
|
2343
2399
|
if (cmActive()) { if (tfClick(e)) { e.stopPropagation(); controls.enabled = true; return; } } // Move/Copy armed (editor state) → picks land on the working plane; an empty-selection click falls through to select
|
|
2400
|
+
if (basePickCol) { e.stopPropagation(); const r = basePickPoint(e.clientX, e.clientY); if (r) { if (api && api.onBasePick) api.onBasePick(r); setBasePickMode(null); } return; } // Mode B: commit + disarm only on a valid on-column pick; an off-column click is ignored (stays armed) — Esc cancels
|
|
2344
2401
|
if (insertMode) { e.stopPropagation(); const r = insertPick(e.clientX, e.clientY); if (r && api && api.onInsertPlace) api.onInsertPlace(r, insertPending); setInsertMode(false); return; } // armed: place the queued detail at the pick, then disarm (one shot)
|
|
2345
2402
|
if (clipMode === 'plane') { e.stopPropagation(); addClipPlaneAtScreen(e.clientX, e.clientY); return; } // armed: left-click a face → place a clip plane (stays armed)
|
|
2346
2403
|
if (clipMode === 'box') { e.stopPropagation(); onClipBoxClick(e); return; } // armed: 2-corner clip-box draw on the floor plane
|
|
@@ -2620,6 +2677,17 @@ function onHoverMove(e) {
|
|
|
2620
2677
|
hoverRAF = requestAnimationFrame(() => {
|
|
2621
2678
|
hoverRAF = 0;
|
|
2622
2679
|
if (!lastHoverXY || pending || dragging || boxSel) return;
|
|
2680
|
+
if (basePickCol) { // Mode B armed: reticle on the column axis + a live elevation readout
|
|
2681
|
+
const p = basePickPoint(lastHoverXY[0], lastHoverXY[1]);
|
|
2682
|
+
if (p) {
|
|
2683
|
+
showMarker([p.x, p.y, p.z], p.snapped ? 'end' : 'dot');
|
|
2684
|
+
canvasEl.style.cursor = p.snapped ? 'none' : 'crosshair';
|
|
2685
|
+
readout._dist.textContent = api.fmtLen ? api.fmtLen(p.z) : (p.z / 304.8).toFixed(2) + ' ft';
|
|
2686
|
+
readout._type.textContent = p.snapped ? ' · ' + (p.label || 'level') : ' ↕';
|
|
2687
|
+
readout.style.left = (lastHoverXY[0] + 14) + 'px'; readout.style.top = (lastHoverXY[1] + 14) + 'px'; readout.style.display = 'block';
|
|
2688
|
+
} else { marker.visible = false; readout.style.display = 'none'; canvasEl.style.cursor = 'crosshair'; }
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2623
2691
|
if (wpMode) { // armed set-plane pick: crosshair (+ snap marker for the 3pt flow)
|
|
2624
2692
|
canvasEl.style.cursor = 'crosshair';
|
|
2625
2693
|
if (wpMode === '3pt') { const r = dimPointAt({ clientX: lastHoverXY[0], clientY: lastHoverXY[1], altKey: dimLastAlt });
|
|
@@ -2805,6 +2873,7 @@ window.Steel3DView = {
|
|
|
2805
2873
|
setProjection, projection, setDisplayMode, mode: () => displayMode, frameAll, frameSelection, applyView,
|
|
2806
2874
|
setRefLine, refLine: () => refLineOn,
|
|
2807
2875
|
setInsertMode, insertMode: insertModeOn, // arm/query the detail-placement pick (Slice 4)
|
|
2876
|
+
setBasePickMode, basePickMode: basePickModeOn, // arm/query the Mode B column-base pick (level-snapped elevation)
|
|
2808
2877
|
selectWholeConn, clearConnSel, ascendConn, connContext, connEnvelopeOn: () => !!connEnvelope, // Connection Components (Slice A): whole-select / drill / ascend + test probes
|
|
2809
2878
|
setLabelsOn, labelsOn: () => labelsOnFlag, // member mark/id label overlay toggle
|
|
2810
2879
|
syncMemberLabels, // editor calls after a mark/id edit to refresh labels
|
|
@@ -941,6 +941,16 @@ function retargetTos(m,srcDef,dstDef){const d=dstDef-srcDef;ensureMeta(m);
|
|
|
941
941
|
// best-effort: framing plans carry TOS at the level UNO — assume the L2 datum +16'-6" (198"); each end's
|
|
942
942
|
// 'default' checkbox links it to this value (auto-updates when changed); uncheck to override.
|
|
943
943
|
let defaultTOS=198, addProfile='';
|
|
944
|
+
let basePickColId=null; // Mode B: the column whose base an armed 3D level-pick retargets
|
|
945
|
+
const MIN_COL_STUB_IN=12; // never trim a column below a 1' stub (keeps bos < tos)
|
|
946
|
+
// Mode B: arm the 3D column-base pick on `colId` (a level-snapped elevation → col.bos). 3D-view only.
|
|
947
|
+
function armBaseTrim(colId){
|
|
948
|
+
if(!view3d){toast('Open the 3D view to pick the column base point');return;}
|
|
949
|
+
if(!(window.Steel3DView&&window.Steel3DView.setBasePickMode))return;
|
|
950
|
+
basePickColId=colId;
|
|
951
|
+
window.Steel3DView.setBasePickMode(colId);
|
|
952
|
+
toast('Click a point along '+colId+' — the base snaps to framing levels · Esc cancels');
|
|
953
|
+
}
|
|
944
954
|
function syncDefaults(){for(const m of P.members){ensureMeta(m);
|
|
945
955
|
if(m.role==='column'){if(m.col.tosDef!==false&&defaultTOS!=null)m.col.tos=defaultTOS;if(m.col.bos==null)m.col.bos=0;}
|
|
946
956
|
else for(const en of m.ends)if(en.tosDef!==false&&defaultTOS!=null)en.tos=defaultTOS;}}
|
|
@@ -1738,12 +1748,14 @@ function panel(){
|
|
|
1738
1748
|
<div class=divrow><hr></div>
|
|
1739
1749
|
<div class="row f" style="gap:6px;flex-wrap:wrap">
|
|
1740
1750
|
<button class=ghostw id=cmpEdit data-tip="Edit this connection's parameters on ${esc(j.main)}">✎ Edit parameters on ${esc(j.main)} →</button>
|
|
1751
|
+
${isBP?`<button class=ghostw id=cmpTrim data-tip="Pick a point on ${esc(j.main)} — trim or extend the column so its base seats there (snaps to framing levels)">⭶ Trim / Extend column to base…</button>`:''}
|
|
1741
1752
|
<button class=ghostw id=cmpAsk data-tip="Record a request for your terminal AI to modify / replace / move this connection">Modify connection…</button>
|
|
1742
1753
|
<button class=danger id=cmpDel data-tip="Remove this whole connection">Delete connection</button>
|
|
1743
1754
|
</div>`;
|
|
1744
1755
|
const toMember=()=>{if(window.Steel3DView&&window.Steel3DView.clearConnSel)window.Steel3DView.clearConnSel();selIds=new Set([j.main]);selDimIds.clear();sel3dDimIds.clear();render();};
|
|
1745
1756
|
{const b=document.getElementById('cmpMember');if(b)b.onclick=toMember;}
|
|
1746
1757
|
{const b=document.getElementById('cmpEdit');if(b)b.onclick=toMember;}
|
|
1758
|
+
{const b=document.getElementById('cmpTrim');if(b)b.onclick=()=>armBaseTrim(j.main);}
|
|
1747
1759
|
{const b=document.getElementById('cmpAsk');if(b)b.onclick=()=>connModifyRequest(j);}
|
|
1748
1760
|
{const b=document.getElementById('cmpDel');if(b)b.onclick=()=>edit(()=>{C.joints=(C.joints||[]).filter(x=>x!==j);selIds.clear();});}
|
|
1749
1761
|
return;
|
|
@@ -1901,7 +1913,7 @@ function panel(){
|
|
|
1901
1913
|
${pFld('plateWidth','Plate width "N"','auto','mm')}${pFld('plateDepth','Plate depth "B"','auto','mm')}${pFld('thickness','Thickness','1"','mm')}${pFld('weldLeg','Weld leg','5/16"','mm')}
|
|
1902
1914
|
<div class=elab style="margin-top:7px;opacity:.7">Anchor kit</div>
|
|
1903
1915
|
${pFld('boltCols','Bolt columns','2','')}${pFld('boltRows','Bolt rows','2','')}${pFld('boltDia','Bolt ⌀','1"','mm')}${pFld('embedment','Embedment','auto','mm')}${pFld('grout','Grout','auto','mm')}
|
|
1904
|
-
<div class="row f"><button class="ghostw" id=bpRemove data-tip="Delete this column's base plate" style="color:#fca5a5;border-color:#7f1d1d">Remove base plate</button></div>`:'';
|
|
1916
|
+
<div class="row f"><button class="ghostw" id=bpTrim data-tip="Pick a point on ${esc(m.id)} — trim or extend the column so its base seats there (snaps to framing levels)">⭶ Trim / Extend column to base…</button><button class="ghostw" id=bpRemove data-tip="Delete this column's base plate" style="color:#fca5a5;border-color:#7f1d1d">Remove base plate</button></div>`:'';
|
|
1905
1917
|
// This BEAM's shear-plate joints — one params block per detailed END (start/end). Mirrors bpSect but
|
|
1906
1918
|
// per-end (a beam can be detailed at both ends), and adds the clearance + web-side + stiffener controls.
|
|
1907
1919
|
const spjs=col?[]:[0,1].map(e=>({e,j:(C.joints||[]).find(j=>j&&j.kind==='shear-plate'&&j.main===m.id&&j.at==='end'+e)})).filter(x=>x.j);
|
|
@@ -1977,7 +1989,8 @@ function panel(){
|
|
|
1977
1989
|
else delete bpj.params[key]; // empty / invalid / ≤0 → drop the override so the engine falls back to its default
|
|
1978
1990
|
bpj.source='user';});};}; // through edit() → the param change is undoable; editing also makes the plate user-owned (survives the auto-detail "Clear")
|
|
1979
1991
|
['plateWidth','plateDepth','thickness','boltDia','weldLeg','embedment','grout'].forEach(k=>wireBp(k,false));['boltCols','boltRows'].forEach(k=>wireBp(k,true));
|
|
1980
|
-
{const rm=document.getElementById('bpRemove');if(rm)rm.onclick=()=>edit(()=>{C.joints=(C.joints||[]).filter(j=>j!==bpj);});}
|
|
1992
|
+
{const rm=document.getElementById('bpRemove');if(rm)rm.onclick=()=>edit(()=>{C.joints=(C.joints||[]).filter(j=>j!==bpj);});}
|
|
1993
|
+
{const tb=document.getElementById('bpTrim');if(tb)tb.onclick=()=>armBaseTrim(m.id);}} // Remove is undoable; Trim arms the 3D base pick
|
|
1981
1994
|
}else{
|
|
1982
1995
|
wireTos('tosA',m.ends[0]);
|
|
1983
1996
|
document.getElementById('ntA').onchange=e=>edit(()=>{m.ends[0].note=e.target.value;});
|
|
@@ -3072,6 +3085,23 @@ const view3dApi={
|
|
|
3072
3085
|
beginClipEdit:()=>pushUndo(snapshot()), // a clip / work-area manipulation → push a pre-edit snapshot so Ctrl+Z/Y restores it
|
|
3073
3086
|
onClipModeChange:(m)=>{const b=document.getElementById('m3dClip');if(b){b.classList.toggle('on',!!m);b.textContent=m?'Clip ✕':'Clip ▾';}}, // armed → button fills brand-blue + becomes a cancel target (✕ = cancel)
|
|
3074
3087
|
onInsertModeChange:(on)=>{const b=document.getElementById('m3dInsert');if(b){b.classList.toggle('on',!!on);b.textContent=on?'✕ Cancel insert':'Insert…';}}, // armed → cancel target
|
|
3088
|
+
onBasePickModeChange:()=>{}, // Mode B armed state shows via the 3D crosshair + elevation readout; nothing else to reflect
|
|
3089
|
+
onBasePick:(p)=>{ // Mode B: retarget the armed column's base to the picked elevation (world mm → inches), ONE undo entry
|
|
3090
|
+
const m=byId(basePickColId); if(!m||m.role!=='column'||!m.col){toast('No column to trim');return;}
|
|
3091
|
+
const tos=(m.col.tos!=null?m.col.tos:defaultTOS); // inches (col.bos/tos are inches; memberGeometry ×25.4 → mm)
|
|
3092
|
+
if(tos==null){toast('Set the column top (TOS) first');return;}
|
|
3093
|
+
const want=p.z/25.4, cap=tos-MIN_COL_STUB_IN;
|
|
3094
|
+
const bosIn=Math.min(want, cap); // world mm → inches; clamp so the column keeps a min stub (bos<tos)
|
|
3095
|
+
const clamped=want>cap+0.02; // the pick was above the min-stub cap → we seated at the cap, not the picked level
|
|
3096
|
+
const cur=(m.col.bos!=null?m.col.bos:0);
|
|
3097
|
+
if(Math.abs(bosIn-cur)<0.02){toast('Column base already at that level');return;}
|
|
3098
|
+
const dir=bosIn<cur?'extended':'trimmed'; // lower base = longer column = extended; higher = trimmed
|
|
3099
|
+
edit(()=>{m.col.bos=bosIn;}); // set BOS only (leave tosDef — the top isn't edited); the base plate re-seats at the new base via expandBasePlate, same undo
|
|
3100
|
+
// Report the ACTUAL result: a snapped level when honored, else the ft-in elevation; call out the clamp so a
|
|
3101
|
+
// pick above the min stub isn't misreported as landing on the snapped level.
|
|
3102
|
+
const where=clamped?(fmtFtIn(bosIn)+' (clamped to a 1′ minimum stub)'):(p.snapped?(p.label||'a level'):fmtFtIn(bosIn));
|
|
3103
|
+
toast('Column '+m.id+' base '+dir+' to '+where+' — base plate re-seated');
|
|
3104
|
+
},
|
|
3075
3105
|
onInsertPlace:(pick,pending)=>{
|
|
3076
3106
|
if(pending&&pending.kind==='connection'&&pending.connection){
|
|
3077
3107
|
const conn=pending.connection;const rc=conn.recipe;
|