@floless/app 0.38.0 → 0.39.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.
@@ -52856,7 +52856,7 @@ function appVersion() {
52856
52856
  return resolveVersion({
52857
52857
  isSea: isSea2(),
52858
52858
  sqVersionXml: readSqVersionXml(),
52859
- define: true ? "0.38.0" : void 0,
52859
+ define: true ? "0.39.0" : void 0,
52860
52860
  pkgVersion: readPkgVersion()
52861
52861
  });
52862
52862
  }
@@ -52866,7 +52866,7 @@ function resolveChannel(s) {
52866
52866
  return "dev";
52867
52867
  }
52868
52868
  function appChannel() {
52869
- return resolveChannel({ isSea: isSea2(), define: true ? "0.38.0" : void 0 });
52869
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.39.0" : void 0 });
52870
52870
  }
52871
52871
 
52872
52872
  // oauth-presets.ts
@@ -146,7 +146,8 @@
146
146
  circle.dimend{fill:#22d3ee;pointer-events:none}
147
147
  rect.dimchip{fill:#111827;stroke:#22d3ee;stroke-width:1;vector-effect:non-scaling-stroke;cursor:pointer}
148
148
  text.dimtx{fill:#e2e8f0;font-weight:600;font-family:system-ui;text-anchor:middle;dominant-baseline:central;pointer-events:none}
149
- line.dim.dimsel{stroke:#f59e0b} rect.dimchip.dimsel{stroke:#f59e0b} circle.dimend.dimsel{fill:#f59e0b}
149
+ line.dim.dimsel{stroke:#f59e0b} line.dimwit.dimsel{stroke:#f59e0b;opacity:1} rect.dimchip.dimsel{stroke:#f59e0b} circle.dimend.dimsel{fill:#f59e0b}
150
+ circle.dimhandle{fill:#22d3ee;stroke:#0b1220;stroke-width:2;cursor:grab}
150
151
  .dimprev{opacity:.85} line.dim.dimprev{stroke-dasharray:7 4}
151
152
  body.dimon line.member,body.dimon line.seg,body.dimon image{cursor:crosshair}
152
153
  body.geo line.seg{opacity:.45;cursor:crosshair} body.geo line.member{cursor:crosshair}
@@ -411,6 +412,7 @@ function main() {
411
412
  let P, X0,Y0,X1,Y1, FT, RB64, EXTX, EXTY, profs; // per-plan, set by setPlan()
412
413
  let mode='sel', drag=null, picking=false, pickKind='profile', pickEnd=null, geoMode=null;
413
414
  let dimMode=false, dimDraft=null, dimAxis='free', selDimIds=new Set(), dimsVisible=true; // Dimension tool: armed flag, in-progress {a,b,axis}, sticky axis, selected dim ids (multi-select), show/hide
415
+ let dimChain=false, dimChainPrev=null, dimSeq=0; // chained "continuous" dimensioning: toggle, the running {point,axis,off}, and a counter for unique ids on rapid clicks
414
416
  let selIds=new Set();
415
417
  let undo=[], redo=[];
416
418
  const byId=id=>P.members.find(m=>m.id===id);
@@ -469,6 +471,7 @@ function apply(json){const d=JSON.parse(json);
469
471
  else{P.members=d.members||[];if(Array.isArray(d.dims))P.dims=d.dims;}
470
472
  selIds=new Set([...selIds].filter(id=>byId(id)));
471
473
  selDimIds.forEach(id=>{if(!(P.dims||[]).some(x=>x.id===id))selDimIds.delete(id);}); // drop any dim selection the undo removed
474
+ dimDraft=null;dimChainPrev=null;dimPrevClear(); // undo/redo changed geometry under the tool → abandon any in-progress dim placement / chain (else the chain head points at a removed segment)
472
475
  scheduleSave();render();sync3D();}
473
476
  function doUndo(){if(!undo.length)return;redo.push(snapshot());apply(undo.pop());}
474
477
  function doRedo(){if(!redo.length)return;undo.push(snapshot());apply(redo.pop());}
@@ -562,7 +565,10 @@ function dimSvg(d,o){o=o||{};const g=dimGeo(d.a,d.b,d.axis,d.off);
562
565
  s+=ang?`<g transform="rotate(${ang.toFixed(2)} ${g.mid[0]} ${g.mid[1]})">${chip}</g>`:chip; // rotate the chip group around the dim-line midpoint
563
566
  return s;}
564
567
  function renderDims(){if(!dimsVisible||!Array.isArray(P.dims))return '';
565
- return P.dims.map(d=>dimSvg(d,{id:d.id,sel:selDimIds.has(d.id)})).join('');}
568
+ let s=P.dims.map(d=>dimSvg(d,{id:d.id,sel:selDimIds.has(d.id)})).join('');
569
+ const HR=epR();for(const d of P.dims)if(selDimIds.has(d.id)) // each selected dim gets draggable anchor handles — drag an end to re-measure (snaps like drawing; Alt off)
570
+ s+=`<circle class=dimhandle data-dim="${esc(d.id)}" data-dimend="0" cx="${d.a[0]}" cy="${d.a[1]}" r="${HR}"/><circle class=dimhandle data-dim="${esc(d.id)}" data-dimend="1" cx="${d.b[0]}" cy="${d.b[1]}" r="${HR}"/>`;
571
+ return s;}
566
572
  function setGeo(){document.body.classList.toggle('geo',!!geoMode);}
567
573
  // nearest end → intersection: extends when the line is past the end, trims when it crosses the member. ponytail: keeps the larger piece on a deep overshoot — undo+drag if that's wrong.
568
574
  // Multi: trim/extend the nearest end of EVERY given member to the (tA,tB) target line; members
@@ -693,7 +699,7 @@ function updDup(){const n=redundantDups().length;
693
699
  function updateBadges(){const R=12/zoom,F=13/zoom,ox=el=>(+el.dataset.fi-(+el.dataset.gn-1)/2)*R*2;
694
700
  svg.querySelectorAll('circle.numbg').forEach(c=>{c.setAttribute('cx',(+c.dataset.bx)+ox(c));c.setAttribute('r',R);});
695
701
  svg.querySelectorAll('text.numtx').forEach(t=>{t.setAttribute('x',(+t.dataset.bx)+ox(t));t.style.fontSize=F+'px';});
696
- svg.querySelectorAll('circle.handle').forEach(h=>h.setAttribute('r',epR()));
702
+ svg.querySelectorAll('circle.handle,circle.dimhandle').forEach(h=>h.setAttribute('r',epR()));
697
703
  const dz=13/zoom;svg.querySelectorAll('text.dimtx').forEach(t=>t.style.fontSize=dz+'px');
698
704
  svg.querySelectorAll('circle.dimend').forEach(c=>c.setAttribute('r',3/zoom));
699
705
  svg.querySelectorAll('rect.dimchip').forEach(r=>{const w=(+r.dataset.w)/zoom,h=(+r.dataset.h)/zoom;
@@ -716,10 +722,13 @@ function panel(){
716
722
  <div class=hint id=dimHint style="margin-top:8px">${esc(dimHintText())}</div>
717
723
  <div class=sect style="margin-top:12px">Direction</div>
718
724
  <div class=seg2><button id=dimAxFree class="${dimAxis==='free'?'on':''}">Free</button><button id=dimAxX class="${dimAxis==='x'?'on':''}">X</button><button id=dimAxY class="${dimAxis==='y'?'on':''}">Y</button></div>
725
+ <div class=sect style="margin-top:12px">Mode</div>
726
+ <div class=seg2><button id=dimChainB class="${dimChain?'on':''}" title="Chain (C) — keep adding dimensions from the last point; Esc/Enter ends the chain">⛓ Chain</button></div>
719
727
  <div class=hint style="margin-top:8px">Snaps to grid intersections, member ends, and onto member lines — same as drawing. <b>Alt</b> turns snap off. <b>Esc</b> cancels.</div>`;
720
728
  document.getElementById('dimAxFree').onclick=()=>{dimSetAxis('free');dimRefreshPrev();panel();};
721
729
  document.getElementById('dimAxX').onclick=()=>{dimSetAxis('x');dimRefreshPrev();panel();};
722
730
  document.getElementById('dimAxY').onclick=()=>{dimSetAxis('y');dimRefreshPrev();panel();};
731
+ document.getElementById('dimChainB').onclick=()=>toggleDimChain();
723
732
  return;}
724
733
  if(mode==='add'){
725
734
  p.innerHTML=`<span class=badge>Add member</span>
@@ -910,20 +919,31 @@ function snapClear(){const c=document.getElementById('snapMark');if(c)c.remove()
910
919
  const SVGNS='http://www.w3.org/2000/svg';
911
920
  function setDimMode(){document.body.classList.toggle('dimon',dimMode);document.getElementById('dimB').classList.toggle('on',dimMode);
912
921
  if(dimMode){if(mode==='add'){mode='sel';setMode();}geoMode=null;setGeo();selIds.clear();selDimIds.clear();dimsVisible=true;updDimToggle();} // arming clears conflicting modes + ensures dims are visible
913
- dimDraft=null;dimPrevClear();}
922
+ dimDraft=null;dimChainPrev=null;dimPrevClear();}
914
923
  function dimSnapAt(e){const q=toSvg(e);let x=q.x,y=q.y;
915
924
  if(!e.altKey){buildSnap(null);const sn=snap(x,y);x=sn.x;y=sn.y;return {x,y,raw:[q.x,q.y],hit:sn.hit};}
916
925
  return {x,y,raw:[q.x,q.y],hit:false};}
917
926
  function dimClick(e){const s=dimSnapAt(e);
927
+ if(dimChainPrev){const a=dimChainPrev.point,b=[s.x,s.y],axis=dimChainPrev.axis,off=dimChainPrev.off; // CHAIN: each click adds the next dim from the previous point, on the same axis + offset line
928
+ const span=axis==='x'?Math.abs(b[0]-a[0]):axis==='y'?Math.abs(b[1]-a[1]):Math.hypot(b[0]-a[0],b[1]-a[1]);
929
+ if(span<1){dimPrev(e);return;} // ignore a zero / sub-pixel click — don't commit a degenerate dim
930
+ edit(()=>{P.dims.push({id:'d'+Date.now()+'_'+(dimSeq++),a,b,axis,off});});
931
+ dimChainPrev={point:b,axis,off};dimPrev(e);return;} // keep going from b; Esc/Enter ends the chain
918
932
  if(!dimDraft){dimDraft={a:[s.x,s.y],b:null,axis:dimAxis};dimPrev(e);return;} // click 1 → first anchor
919
933
  if(!dimDraft.b){dimDraft.b=[s.x,s.y];dimPrev(e);return;} // click 2 → second anchor (value locks)
920
934
  const axis=dimDraft._ax||dimDraft.axis; // commit the axis the preview last showed: _ax is cached every move (incl. the click-2 preview), so a Shift press/release between move and click can't make the commit disagree with what was on screen
921
935
  const off=dimOffFromPointer(dimDraft.a,dimDraft.b,axis,s.raw); // click 3 → offset/side from raw pointer
922
936
  const a=dimDraft.a,b=dimDraft.b;
923
- edit(()=>{P.dims.push({id:'d'+Date.now(),a,b,axis,off});}); // undoable; render() clears the preview
937
+ edit(()=>{P.dims.push({id:'d'+Date.now()+'_'+(dimSeq++),a,b,axis,off});}); // undoable; render() clears the preview
938
+ if(dimChain)dimChainPrev={point:b,axis,off}; // chain mode → continue from b along this same offset line
924
939
  dimDraft=null;}
925
940
  function dimPrevClear(){const g=document.getElementById('dimPrevG');if(g)g.remove();snapClear();}
926
- function dimPrev(e){if(!dimMode||!dimDraft){dimPrevClear();return;}
941
+ function dimPrev(e){if(!dimMode){dimPrevClear();return;}
942
+ {const h=document.getElementById('dimHint');if(h)h.textContent=dimHintText();} // keep the panel hint live (chain / stage state)
943
+ if(dimChainPrev){const s=dimSnapAt(e);s.hit?snapMark(s.x,s.y):snapClear(); // chain: preview the next segment from the last point at the shared axis + offset
944
+ let g=document.getElementById('dimPrevG');if(!g){g=document.createElementNS(SVGNS,'g');g.id='dimPrevG';g.setAttribute('pointer-events','none');svg.appendChild(g);}
945
+ g.innerHTML=dimSvg({a:dimChainPrev.point,b:[s.x,s.y],axis:dimChainPrev.axis,off:dimChainPrev.off},{preview:true});return;}
946
+ if(!dimDraft){const s=dimSnapAt(e);s.hit?snapMark(s.x,s.y):snapClear();const og=document.getElementById('dimPrevG');if(og)og.remove();return;} // before click 1: show the snap marker for the prospective FIRST anchor (no dim drawn yet)
927
947
  let g=document.getElementById('dimPrevG');if(!g){g=document.createElementNS(SVGNS,'g');g.id='dimPrevG';g.setAttribute('pointer-events','none');svg.appendChild(g);}
928
948
  if(!dimDraft.b){const s=dimSnapAt(e);s.hit?snapMark(s.x,s.y):snapClear(); // stage 1: live CONSTRAINED preview from a → the tentative 2nd point
929
949
  const b=[s.x,s.y];dimLastPtr=b;
@@ -936,8 +956,10 @@ function dimPrev(e){if(!dimMode||!dimDraft){dimPrevClear();return;}
936
956
  function dimSetAxis(ax){dimAxis=ax;if(dimDraft)dimDraft.axis=ax;
937
957
  const h=document.getElementById('dimHint');if(h)h.textContent=dimHintText();
938
958
  for(const [id,a] of [['dimAxFree','free'],['dimAxX','x'],['dimAxY','y']]){const b=document.getElementById(id);if(b)b.classList.toggle('on',dimAxis===a);}} // keep the Free/X/Y control in sync whether the axis changed by button OR keyboard (x/y/f)
939
- function dimHintText(){const a={free:'Free (aligned)',x:'X (horizontal)',y:'Y (vertical)'}[dimDraft?dimDraft.axis:dimAxis];
940
- return 'Dimension '+a+'. Click 1: start · Click 2: end · Click 3: place. Shift=lock axis · X/Y/F force · Alt=no snap · Esc=cancel.';}
959
+ function dimHintText(){if(dimChainPrev)return 'Chaining — click the next point to add another dimension from the last one. Enter or Esc ends the chain.';
960
+ const a={free:'Free (aligned)',x:'X (horizontal)',y:'Y (vertical)'}[dimDraft?dimDraft.axis:dimAxis];
961
+ return 'Dimension — '+a+(dimChain?' · Chain ON':'')+'. Click 1: start · Click 2: end · Click 3: place. Shift=lock axis · X/Y/F force · Alt=no snap · Esc=cancel.';}
962
+ function toggleDimChain(){dimChain=!dimChain;if(!dimChain){dimChainPrev=null;dimPrevClear();}render();} // Chain (C): toggle continuous dimensioning; turning it off ends any running chain (render() repaints the panel)
941
963
  let dimLastPtr=null; // last canvas pointer during placement, for axis-key live refresh
942
964
  function dimRefreshPrev(){if(!dimMode||!dimDraft||!dimLastPtr)return;
943
965
  const g=document.getElementById('dimPrevG');if(!g)return;
@@ -952,6 +974,7 @@ function toast(msg){let t=document.getElementById('toast');
952
974
  // --- pointer: select(+ctrl toggle / box) + group-move + endpoint (Shift = ortho) ---
953
975
  svg.addEventListener('pointerdown',e=>{if(e.button!==0)return;const t=e.target;
954
976
  if(dimMode){dimClick(e);e.preventDefault();return;} // tool armed → all clicks place dimension points
977
+ if(t.dataset.dimend!=null&&mode==='sel'){const id=t.dataset.dim,end=+t.dataset.dimend;buildSnap(null);drag={type:'dimend',id,end,pre:snapshot()};svg.setPointerCapture(e.pointerId);e.preventDefault();return;} // drag a selected dim's anchor handle → re-measure (snaps)
955
978
  if(t.dataset.dim&&mode==='sel'){const did=t.dataset.dim;selIds.clear(); // a dim click is member-exclusive (clears any member selection)
956
979
  if(e.ctrlKey||e.metaKey){selDimIds.has(did)?selDimIds.delete(did):selDimIds.add(did);render();return;} // Ctrl+click toggles this dim within the dim selection
957
980
  selDimIds=new Set([did]);const d=P.dims.find(x=>x.id===did); // plain click → exactly this dim (collapses any multi-selection, so Delete removes only it)
@@ -995,6 +1018,7 @@ svg.addEventListener('pointermove',e=>{
995
1018
  const d=Math.hypot(q.x-raw.pt[0],q.y-raw.pt[1]);
996
1019
  if(d<16/zoom&&raw.t>0.03&&raw.t<0.97){let cut=raw.pt;if(!e.altKey){buildSnap(m.id);const sn=snap(raw.pt[0],raw.pt[1]);if(sn.hit){const p2=projPt([sn.x,sn.y],m.wp[0],m.wp[1]);if(p2.t>0.02&&p2.t<0.98)cut=p2.pt;}}snapMark(cut[0],cut[1]);}else snapClear();}return;}
997
1020
  if(!drag)return;const p=toSvg(e);
1021
+ if(drag.type==='dimend'){const d=P.dims.find(x=>x.id===drag.id);if(d){let x=p.x,y=p.y;if(!e.altKey){const sn=snap(x,y);x=sn.x;y=sn.y;sn.hit?snapMark(x,y):snapClear();}else snapClear();if(drag.end===0)d.a=[x,y];else d.b=[x,y];render();}return;} // dragging a dim anchor → snap + re-measure live
998
1022
  if(drag.type==='dim'){const d=P.dims.find(x=>x.id===drag.id);if(d){d.off=dimOffFromPointer(d.a,d.b,d.axis,[p.x,p.y]);render();}return;}
999
1023
  if(drag.type==='draw'){let x=p.x,y=p.y;
1000
1024
  if(e.shiftKey){if(Math.abs(x-drag.x0)>=Math.abs(y-drag.y0))y=drag.y0;else x=drag.x0;snapClear();}
@@ -1060,14 +1084,16 @@ addEventListener('keydown',e=>{
1060
1084
  if(e.key==='Escape'&&confOpen()){closeConf();return;}
1061
1085
  if(e.key==='Home'){e.preventDefault();if(view3d&&window.Steel3DView)window.Steel3DView.frameAll();else fitToWindow();return;}
1062
1086
  if(!view3d&&!inForm&&(((e.key==='z'||e.key==='Z')&&e.altKey)||(e.key===' '&&e.shiftKey))){e.preventDefault();zoomToSelection();return;} // 2D zoom-to-selected (Tekla Shift+Space / Alt+Z); 3D handles its own (steel-3d-view onKey)
1063
- if(e.key==='Escape'&&dimMode){if(dimDraft){dimDraft=null;dimPrevClear();}else{dimMode=false;setDimMode();}render();return;} // 1st Esc drops the in-progress dim, 2nd exits the tool
1087
+ if(e.key==='Enter'&&dimMode&&dimChainPrev){e.preventDefault();dimChainPrev=null;dimPrevClear();render();return;} // Enter ends a running chain
1088
+ if(e.key==='Escape'&&dimMode){if(dimChainPrev){dimChainPrev=null;dimPrevClear();}else if(dimDraft){dimDraft=null;dimPrevClear();}else{dimMode=false;setDimMode();}render();return;} // Esc: end the chain → drop the in-progress dim → exit the tool
1064
1089
  if(e.key==='Escape'){if(geoMode){geoMode=null;setGeo();render();return;}if(mode==='add'){mode='sel';setMode();}picking=false;pickKind='profile';pickEnd=null;selIds.clear();render();return;}
1065
1090
  if((e.key==='Delete'||e.key==='Backspace')&&(selDimIds.size||selIds.size)&&!inForm){e.preventDefault();edit(()=>{ // deletes everything selected — dims and/or members (a marquee can hold both)
1066
1091
  if(selDimIds.size){P.dims=P.dims.filter(d=>!selDimIds.has(d.id));selDimIds.clear();}
1067
1092
  if(selIds.size){P.members=P.members.filter(m=>!selIds.has(m.id));selIds.clear();}});return;}
1068
1093
  if(!inForm&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&e.key.toLowerCase()==='d'){e.preventDefault();dimMode=!dimMode;setDimMode();render();return;} // D — toggle the Dimension tool
1069
1094
  if(dimMode&&!inForm&&!e.ctrlKey&&!e.metaKey&&!e.altKey){const dk=e.key.toLowerCase();
1070
- if(dk==='x'||dk==='y'||dk==='f'){e.preventDefault();dimSetAxis(dk==='f'?'free':dk);dimDraft&&svg.querySelector('#dimPrevG')&&dimRefreshPrev();return;}}
1095
+ if(dk==='x'||dk==='y'||dk==='f'){e.preventDefault();dimSetAxis(dk==='f'?'free':dk);dimDraft&&svg.querySelector('#dimPrevG')&&dimRefreshPrev();return;}
1096
+ if(dk==='c'){e.preventDefault();toggleDimChain();return;}} // C — toggle chained (continuous) dimensioning
1071
1097
  if(!inForm&&selIds.size>=1&&!e.ctrlKey&&!e.metaKey&&!e.altKey){const kk=e.key.toLowerCase();
1072
1098
  if(kk==='e'){e.preventDefault();geoMode=(geoMode==='el'?null:'el');setGeo();render();return;} // Extend/Trim — any selection
1073
1099
  if(kk==='p'){e.preventDefault();edit(()=>{for(const m of selArr())swapMemberEnds(m);});return;} // P — swap start↔end for the whole selection (2D + 3D)
@@ -1488,7 +1514,7 @@ function setPlan(i){C.active=i;P=C.plans[i];
1488
1514
  profs=[...new Set([...P.members.map(m=>m.profile), ...Object.keys(WT)])].sort();
1489
1515
  undo=P.undo||(P.undo=[]);redo=P.redo||(P.redo=[]);
1490
1516
  selIds=new Set();picking=false;pickKind='profile';pickEnd=null;mode='sel';geoMode=null;
1491
- dimMode=false;selDimIds=new Set();setDimMode(); // Dimension tool resets per plan; setDimMode syncs the button/body.dimon classes + clears any draft/preview (dimsVisible persists across plans)
1517
+ dimMode=false;dimChain=false;selDimIds=new Set();setDimMode(); // Dimension tool resets per plan; setDimMode syncs the button/body.dimon classes + clears any draft/preview/chain (dimsVisible persists across plans)
1492
1518
  defaultTOS=(P.default_tos!=null?P.default_tos:198);
1493
1519
  P.default_tos=defaultTOS; // make the default explicit so the 3D scene + bake use the SAME TOS the editor/dots do (contractToScene falls back to 0, not 198 — keeping them in sync stops the end dots floating off the steel)
1494
1520
  if(!P.autofilled){autofillTOS();P.autofilled=true;}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.38.0",
3
+ "version": "0.39.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": {