@floless/app 0.72.3 → 0.73.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 +2 -2
- package/dist/web/steel-editor.html +79 -19
- 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.
|
|
53025
|
+
define: true ? "0.73.0" : void 0,
|
|
53026
53026
|
pkgVersion: readPkgVersion()
|
|
53027
53027
|
});
|
|
53028
53028
|
}
|
|
@@ -53032,7 +53032,7 @@ function resolveChannel(s) {
|
|
|
53032
53032
|
return "dev";
|
|
53033
53033
|
}
|
|
53034
53034
|
function appChannel() {
|
|
53035
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
53035
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.73.0" : void 0 });
|
|
53036
53036
|
}
|
|
53037
53037
|
|
|
53038
53038
|
// workflow-update.ts
|
|
@@ -353,6 +353,15 @@
|
|
|
353
353
|
#propPop.connmode .ppfoot{display:none}
|
|
354
354
|
#propPop.connmode #ppClear{display:none}
|
|
355
355
|
#propPop.connmode .pprow{cursor:default;padding-left:14px} /* no checkbox → nudge the name so it doesn't float where the box was */
|
|
356
|
+
/* Selection tree: clickable object rows (connection nodes + parts + member leaves), reusing the legend's
|
|
357
|
+
chevron/swatch/selected-row vocabulary — click selects in the model (Ctrl toggle, Shift range). */
|
|
358
|
+
#propPop .pprow.trow{cursor:pointer;padding-left:6px}
|
|
359
|
+
#propPop .pprow.trow.ptrow{padding-left:24px} /* parts indent under their connection node */
|
|
360
|
+
#propPop .pprow .chev{width:14px;flex:none;color:var(--mut);text-align:center;font-size:9px;cursor:pointer}
|
|
361
|
+
#propPop .pprow .tsw{width:6px;height:6px;flex:none;border-radius:50%;background:var(--mut)} /* a round status dot — deliberately NOT a checkbox (label checkboxes are square inputs) */
|
|
362
|
+
#propPop .pprow.node{font-weight:500}
|
|
363
|
+
#propPop .pprow .cnt{color:var(--mut);font-size:10px;font-variant-numeric:tabular-nums;white-space:nowrap;flex:none}
|
|
364
|
+
#propPop .pprow.selrow{border-left:2px solid var(--brand);background:rgba(59,130,246,.16)}
|
|
356
365
|
/* thin cluster divider in the 3D toolbar */
|
|
357
366
|
#m3dBar .tb-sep{width:1px;height:18px;background:var(--line);flex:0 0 auto;align-self:center}
|
|
358
367
|
/* Themed tooltip — replaces native title= so no OS-default tooltip leaks the dark theme. */
|
|
@@ -1453,6 +1462,37 @@ function connSelInfo(){
|
|
|
1453
1462
|
const whole=childIds.length>0&&childIds.every(id=>selIds.has(id));
|
|
1454
1463
|
return {conn,kind:j.kind,main:j.main,joint:j,childIds,whole,mode:whole?'whole':'part'};
|
|
1455
1464
|
}
|
|
1465
|
+
// ── Selection as a browsable tree (Properties popup): group the flat selIds into connections (each with
|
|
1466
|
+
// its parts) + loose members, so a MIXED selection (a connection + members, several connections) renders
|
|
1467
|
+
// as one tree instead of falling back to member-only. connChildIdsEditor = a connection's selectable parts
|
|
1468
|
+
// (copes excluded — subtractive, not in the 3D mesh set). ---
|
|
1469
|
+
function connChildIdsEditor(conn){return Object.keys(partsById||{}).filter(k=>{const e=partsById[k];return e&&e.conn===conn&&e.kind!=='cut';});}
|
|
1470
|
+
function selTree(){
|
|
1471
|
+
const conns=[],cmap={},members=[];
|
|
1472
|
+
for(const id of selIds){
|
|
1473
|
+
const el=(partsById||{})[id];
|
|
1474
|
+
if(el&&el.conn){ if(!cmap[el.conn]){const j=(C.joints||[]).find(x=>x&&x.id===el.conn);cmap[el.conn]={conn:el.conn,kind:j?j.kind:'',main:j?j.main:'',joint:j,childIds:connChildIdsEditor(el.conn)};conns.push(cmap[el.conn]);} }
|
|
1475
|
+
else if(P.members.some(m=>m.id===id)) members.push(id);
|
|
1476
|
+
}
|
|
1477
|
+
return {conns,members};
|
|
1478
|
+
}
|
|
1479
|
+
// Ctrl/Shift multi-select from a popup tree row → drives the model selection (same semantics as the objects
|
|
1480
|
+
// browser / onSelectDim3d). treeRowIds = the flat, in-display-order LEAF ids (expanded parts + members) for
|
|
1481
|
+
// Shift-range; treeAnchor = the range anchor; treeExpanded = which connection nodes are open (transient).
|
|
1482
|
+
let treeExpanded=new Set(), treeAnchor=null, treeRowIds=[];
|
|
1483
|
+
function treeSelectLeaf(id,mods){
|
|
1484
|
+
let next;
|
|
1485
|
+
if(mods&&mods.shift&&treeAnchor!=null&&treeRowIds.includes(treeAnchor)&&treeRowIds.includes(id)){const i0=treeRowIds.indexOf(treeAnchor),i1=treeRowIds.indexOf(id);next=treeRowIds.slice(Math.min(i0,i1),Math.max(i0,i1)+1);}
|
|
1486
|
+
else if(mods&&(mods.ctrl||mods.meta)){next=new Set(selIds);next.has(id)?next.delete(id):next.add(id);next=[...next];treeAnchor=id;}
|
|
1487
|
+
else{next=[id];treeAnchor=id;}
|
|
1488
|
+
selIds=new Set(next);selDimIds.clear();sel3dDimIds.clear();render();
|
|
1489
|
+
}
|
|
1490
|
+
function treeSelectConn(conn,mods){
|
|
1491
|
+
const cids=connChildIdsEditor(conn);
|
|
1492
|
+
if(mods&&(mods.ctrl||mods.meta)){const next=new Set(selIds);const all=cids.length&&cids.every(id=>next.has(id));cids.forEach(id=>all?next.delete(id):next.add(id));selIds=next;selDimIds.clear();sel3dDimIds.clear();treeAnchor=cids[0]||null;render();}
|
|
1493
|
+
else if(window.Steel3DView&&window.Steel3DView.selectWholeConn){window.Steel3DView.selectWholeConn(conn);treeAnchor=cids[0]||null;} // plain → whole connection (envelope + inspector), same as a 3D click
|
|
1494
|
+
else{selIds=new Set(cids);selDimIds.clear();sel3dDimIds.clear();treeAnchor=cids[0]||null;render();}
|
|
1495
|
+
}
|
|
1456
1496
|
// The floating breadcrumb over the 3D canvas: Model ▸ <Connection> [▸ <Part>]. Segments jump levels via the
|
|
1457
1497
|
// 3D view's own ascend/whole-select so the canvas selection + envelope stay in lockstep. 3D-only; hidden at root.
|
|
1458
1498
|
function updateConnCrumb(){
|
|
@@ -2022,7 +2062,7 @@ function refreshPropLabels3d(){const V=window.Steel3DView;if(!V||!V.setPropLabel
|
|
|
2022
2062
|
|
|
2023
2063
|
// ---- The floating Properties popup ----
|
|
2024
2064
|
let propPopPinned=false;
|
|
2025
|
-
let propPopConn=null; //
|
|
2065
|
+
let propPopConn=null; // truthy = connection/tree mode (selection involves a connection → renderConnTree); null = the member label-picker mode
|
|
2026
2066
|
function propPopOpen(){const el=document.getElementById('propPop');return !!(el&&el.classList.contains('open'));}
|
|
2027
2067
|
function propPopEl(){let el=document.getElementById('propPop');if(el)return el;
|
|
2028
2068
|
el=document.createElement('div');el.id='propPop';el.setAttribute('role','dialog');el.setAttribute('aria-label','Member properties');
|
|
@@ -2037,7 +2077,12 @@ function propPopEl(){let el=document.getElementById('propPop');if(el)return el;
|
|
|
2037
2077
|
+`<label><input type=checkbox id=ppSel>Selected only</label></div>`;
|
|
2038
2078
|
document.body.appendChild(el);
|
|
2039
2079
|
const list=el.querySelector('#ppList');
|
|
2040
|
-
list.addEventListener('click',e=>{const row=e.target.closest('.pprow');if(!row
|
|
2080
|
+
list.addEventListener('click',e=>{const row=e.target.closest('.pprow');if(!row)return;
|
|
2081
|
+
if(e.target.closest('.chev')&&row.dataset.conn){treeExpanded.has(row.dataset.conn)?treeExpanded.delete(row.dataset.conn):treeExpanded.add(row.dataset.conn);renderPropPop();return;} // chevron → expand/collapse the connection node
|
|
2082
|
+
const mods={ctrl:e.ctrlKey||e.metaKey,shift:e.shiftKey};
|
|
2083
|
+
if(row.dataset.conn){treeSelectConn(row.dataset.conn,mods);return;} // connection node → select the whole connection in the model
|
|
2084
|
+
if(row.dataset.tid){treeSelectLeaf(row.dataset.tid,mods);return;} // a part / member row → select it (Ctrl toggle, Shift range)
|
|
2085
|
+
if(row.classList.contains('dis')||!row.dataset.k)return;if(e.target.tagName==='INPUT')return;togglePropKey(row.dataset.k);}); // member label rows carry data-k
|
|
2041
2086
|
list.addEventListener('change',e=>{const cb=e.target;if(cb.tagName!=='INPUT')return;const row=cb.closest('.pprow');if(row&&row.dataset.k)togglePropKey(row.dataset.k,cb.checked);});
|
|
2042
2087
|
el.querySelector('#ppSearch').addEventListener('input',renderPropPop);
|
|
2043
2088
|
el.querySelector('#ppClear').onclick=()=>{C.prop_labels.props=[];refreshPropLabels();};
|
|
@@ -2058,9 +2103,9 @@ function togglePropKey(k,on){const pl=C.prop_labels,has=pl.props.includes(k);if(
|
|
|
2058
2103
|
refreshPropLabels();}
|
|
2059
2104
|
// rebuild the popup contents against the current selection (chrome + rows), preserving row focus
|
|
2060
2105
|
function renderPropPop(){const el=document.getElementById('propPop');if(!el||!el.classList.contains('open'))return;
|
|
2061
|
-
if(propPopPinned){
|
|
2106
|
+
if(propPopPinned){propPopConn=selTree().conns.length?true:(selArr().length?null:propPopConn);} // a PINNED popup follows the selection: any connection involved → tree mode; pure members → label-picker
|
|
2062
2107
|
el.classList.toggle('connmode',!!propPopConn);
|
|
2063
|
-
if(propPopConn){
|
|
2108
|
+
if(propPopConn){renderConnTree(el);return;} // connection / mixed-selection tree view
|
|
2064
2109
|
const arr=selArr(),pl=C.prop_labels,q=(el.querySelector('#ppSearch').value||'').trim().toLowerCase();
|
|
2065
2110
|
el.querySelector('#ppTitle').textContent='Properties ('+arr.length+' selected)';
|
|
2066
2111
|
el.querySelector('#ppLabeled').textContent=pl.props.length?pl.props.length+' labeled':'';
|
|
@@ -2090,7 +2135,7 @@ function openPropLabels(x,y){if(!selArr().length)return;propPopConn=null;const e
|
|
|
2090
2135
|
// `connmode`: the connection's props as read-only name/value rows, label controls hidden. Labeling
|
|
2091
2136
|
// connection props onto the model is a member-keyed pipeline → deferred; this is a VIEW, not a picker. ----
|
|
2092
2137
|
function connPropRows(cs){
|
|
2093
|
-
const j=cs.joint,isBP=cs.kind==='base-plate',pp=j.params||{};
|
|
2138
|
+
const j=cs.joint,isBP=cs.kind==='base-plate',pp=(j&&j.params)||{}; // tolerate a missing joint (orphaned part) rather than throw
|
|
2094
2139
|
const plate=(partsById||{})[cs.conn+':plate']||null;
|
|
2095
2140
|
const dim=mm=>(mm==null?'—':fmtFtIn(Number(mm)/25.4));
|
|
2096
2141
|
const cols=pp.boltCols||(isBP?2:1),rows=pp.boltRows||(isBP?2:3);
|
|
@@ -2106,26 +2151,41 @@ function connPropRows(cs){
|
|
|
2106
2151
|
{label:isBP?'Anchor count':'Bolt count', value:String(cols*rows)},
|
|
2107
2152
|
{label:'Parts', value:String(cs.childIds.length)},
|
|
2108
2153
|
];}
|
|
2109
|
-
function
|
|
2154
|
+
function openConnPop(x,y){propPopConn=true;const el=propPopEl();const sr=el.querySelector('#ppSearch');
|
|
2110
2155
|
sr.value='';el.classList.add('open');renderPropPop();
|
|
2111
2156
|
if(propPopPinned){dockPropPop();}
|
|
2112
2157
|
else{const r=el.getBoundingClientRect();el.style.left=Math.max(4,Math.min(x,innerWidth-r.width-8))+'px';el.style.top=Math.max(4,Math.min(y,innerHeight-r.height-8))+'px';}
|
|
2113
2158
|
setTimeout(()=>sr.focus(),0);}
|
|
2114
|
-
//
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2159
|
+
// Render the connection/tree view. Single connection → its property summary + an interactive parts list;
|
|
2160
|
+
// multiple connections and/or members → a tree of selectable rows (click = select in the model, Ctrl toggle,
|
|
2161
|
+
// Shift range). Re-derived from the live selection each call so it self-heals as the selection moves.
|
|
2162
|
+
function renderConnTree(el){
|
|
2163
|
+
const t=selTree();
|
|
2164
|
+
if(!t.conns.length){ if(!propPopPinned){propPopConn=null;closePropPop(true);return;}
|
|
2118
2165
|
el.querySelector('#ppTitle').textContent='Properties';el.querySelector('#ppLabeled').textContent='';
|
|
2119
2166
|
el.querySelector('#ppScope').textContent='Right-click a connection';el.querySelector('#ppMeta').textContent='';
|
|
2120
2167
|
el.querySelector('#ppList').innerHTML='<div class=ppempty>No connection selected.</div>';return; }
|
|
2121
|
-
propPopConn=cs;
|
|
2122
2168
|
const q=(el.querySelector('#ppSearch').value||'').trim().toLowerCase();
|
|
2123
|
-
|
|
2169
|
+
const single=t.conns.length===1&&!t.members.length;
|
|
2170
|
+
const kindName=k=>k==='base-plate'?'Base plate':k==='shear-plate'?'Shear plate':'Connection';
|
|
2171
|
+
const nSel=t.conns.length+t.members.length, rowMatch=s=>!q||String(s).toLowerCase().includes(q), sw='<span class=tsw></span>';
|
|
2172
|
+
el.querySelector('#ppTitle').textContent=single?('Properties · '+kindName(t.conns[0].kind)):('Properties · '+nSel+' objects');
|
|
2173
|
+
el.querySelector('#ppScope').textContent=single?('On '+t.conns[0].main):(t.conns.length+' connection'+(t.conns.length===1?'':'s')+(t.members.length?(' · '+t.members.length+' member'+(t.members.length===1?'':'s')):''));
|
|
2124
2174
|
el.querySelector('#ppLabeled').textContent='';
|
|
2125
|
-
|
|
2126
|
-
const all=connPropRows(
|
|
2127
|
-
|
|
2128
|
-
|
|
2175
|
+
treeRowIds=[];let html='';
|
|
2176
|
+
if(single){ const all=connPropRows(t.conns[0]).filter(r=>rowMatch(r.label)); // property summary (read-only) …
|
|
2177
|
+
html+=all.map(r=>`<div class=pprow><span class=pn>${esc(r.label)}</span><span class=pv>${esc(r.value)}</span></div>`).join('');
|
|
2178
|
+
html+=`<div class=divrow><hr><span class=sect style="margin:0">Parts (${t.conns[0].childIds.length})</span><hr></div>`; } // … then the interactive parts
|
|
2179
|
+
for(const c of t.conns){
|
|
2180
|
+
const expanded=single||treeExpanded.has(c.conn), wholeSel=c.childIds.length&&c.childIds.every(id=>selIds.has(id));
|
|
2181
|
+
if(!single) html+=`<div class="pprow trow node${wholeSel?' selrow':''}" data-conn="${esc(c.conn)}"><span class=chev>${expanded?'▾':'▸'}</span>${sw}<span class=pn>${esc(kindName(c.kind)+' · '+c.main)}</span><span class=cnt>${c.childIds.length} parts</span></div>`;
|
|
2182
|
+
if(expanded) for(const id of c.childIds){ const e2=(partsById||{})[id],lbl=(e2&&e2.meta&&e2.meta.label)||id.slice(id.indexOf(':')+1); if(!rowMatch(lbl))continue; treeRowIds.push(id);
|
|
2183
|
+
html+=`<div class="pprow trow ptrow${selIds.has(id)?' selrow':''}" data-tid="${esc(id)}">${sw}<span class=pn>${esc(lbl)}</span></div>`; }
|
|
2184
|
+
}
|
|
2185
|
+
for(const id of t.members){ const m=byId(id),lbl=id+(m&&m.profile?(' · '+m.profile):''); if(!rowMatch(lbl))continue; treeRowIds.push(id);
|
|
2186
|
+
html+=`<div class="pprow trow mrow${selIds.has(id)?' selrow':''}" data-tid="${esc(id)}">${sw}<span class=pn>${esc(lbl)}</span></div>`; }
|
|
2187
|
+
el.querySelector('#ppList').innerHTML=html||'<div class=ppempty>No properties match your search.</div>';
|
|
2188
|
+
el.querySelector('#ppMeta').textContent=single?(t.conns[0].childIds.length+' parts'):(nSel+' selected');}
|
|
2129
2189
|
function closePropPop(force){const el=document.getElementById('propPop');if(!el)return;if(propPopPinned&&!force)return;el.classList.remove('open');
|
|
2130
2190
|
const c=document.getElementById(view3d?'stage3d':'stage');if(c)c.focus&&c.focus();}
|
|
2131
2191
|
document.addEventListener('pointerdown',e=>{if(propPopOpen()&&!propPopPinned&&!e.target.closest('#propPop'))closePropPop();},true);
|
|
@@ -2170,9 +2230,9 @@ document.getElementById('stage').addEventListener('contextmenu',e=>{e.preventDef
|
|
|
2170
2230
|
document.getElementById('stage3d').addEventListener('contextmenu',e=>{e.preventDefault();const V=window.Steel3DView;
|
|
2171
2231
|
if(V&&V.rightDragged&&V.rightDragged())return; // that right button was an orbit/pan, not a click — no menu
|
|
2172
2232
|
if(V&&V.dimToolOn&&V.dimToolOn()){openSnapMenu(e.clientX,e.clientY,true);return;} // dim tool armed → snap-override menu (unchanged)
|
|
2173
|
-
if(mode==='sel'&&!cmTool&&!picking){
|
|
2174
|
-
if(
|
|
2175
|
-
if(selArr().length){openPropLabels(e.clientX,e.clientY);return;}}
|
|
2233
|
+
if(mode==='sel'&&!cmTool&&!picking){
|
|
2234
|
+
if(selTree().conns.length){openConnPop(e.clientX,e.clientY);return;} // any connection in the selection → the connection / mixed-selection tree popup
|
|
2235
|
+
if(selArr().length){openPropLabels(e.clientX,e.clientY);return;}} // members only → the member label-picker popup
|
|
2176
2236
|
});
|
|
2177
2237
|
document.getElementById('snapStat').onclick=()=>{snapOnly=null;const V=window.Steel3DView;if(V&&V.setSnapOnly)V.setSnapOnly(null);updSnapStat();};
|
|
2178
2238
|
// --- Dimension tool: armed mode + 3-click placement (anchor, anchor, offset). Shares the editor's
|