@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.
@@ -53022,7 +53022,7 @@ function appVersion() {
53022
53022
  return resolveVersion({
53023
53023
  isSea: isSea2(),
53024
53024
  sqVersionXml: readSqVersionXml(),
53025
- define: true ? "0.72.3" : void 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.72.3" : void 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; // non-null = read-only Connection mode (a connection selected connPropRows); null = the member label-picker mode
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||row.classList.contains('dis')||!row.dataset.k)return;if(e.target.tagName==='INPUT')return;togglePropKey(row.dataset.k);}); // conn-mode rows carry no data-k → not toggleable
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){const cs=connSelInfo();if(cs)propPopConn=cs;else if(selArr().length)propPopConn=null;} // a PINNED popup follows the selection across member ↔ connection (an unpinned one re-asserts its mode on each right-click)
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){renderConnProps(el);return;} // read-only Connection view
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 openConnProps(x,y,cs){propPopConn=cs;const el=propPopEl();const sr=el.querySelector('#ppSearch');
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
- // render the connection read-view; re-derives the connection each call so it self-heals if the selection moves
2115
- function renderConnProps(el){
2116
- const cs=connSelInfo();
2117
- if(!cs){ if(!propPopPinned){propPopConn=null;closePropPop(true);return;}
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
- el.querySelector('#ppTitle').textContent='Properties · '+(cs.kind==='base-plate'?'Base plate':'Shear plate');
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
- el.querySelector('#ppScope').textContent='On '+cs.main;
2126
- const all=connPropRows(cs),rows=all.filter(r=>!q||r.label.toLowerCase().includes(q));
2127
- el.querySelector('#ppList').innerHTML=rows.length?rows.map(r=>`<div class=pprow><span class=pn>${esc(r.label)}</span><span class=pv>${esc(r.value)}</span></div>`).join(''):'<div class=ppempty>No properties match your search.</div>';
2128
- el.querySelector('#ppMeta').textContent=rows.length+' of '+all.length+' shown';}
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){const cs=connSelInfo();
2174
- if(cs){openConnProps(e.clientX,e.clientY,cs);return;} // a connection selectedits read-only Properties popup
2175
- if(selArr().length){openPropLabels(e.clientX,e.clientY);return;}} // members selected → the member label-picker popup
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.72.3",
3
+ "version": "0.73.0",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {