@floless/app 0.40.0 → 0.41.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.40.0" : void 0,
52859
+ define: true ? "0.41.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.40.0" : void 0 });
52869
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.41.0" : void 0 });
52870
52870
  }
52871
52871
 
52872
52872
  // oauth-presets.ts
@@ -151,6 +151,7 @@
151
151
  text.dimtx{fill:#e2e8f0;font-weight:600;font-family:system-ui;text-anchor:middle;dominant-baseline:central;pointer-events:none}
152
152
  line.dim.dimsel{stroke:#f59e0b} line.dimwit.dimsel{stroke:#f59e0b;opacity:1} rect.dimchip.dimsel{stroke:#f59e0b} circle.dimend.dimsel{fill:#f59e0b}
153
153
  circle.dimhandle{fill:#22d3ee;stroke:#0b1220;stroke-width:2;cursor:grab}
154
+ body.dimsplit line.member,body.dimsplit line.seg,body.dimsplit line.dim,body.dimsplit line.dimwit,body.dimsplit image{cursor:crosshair}
154
155
  .dimprev{opacity:.85} line.dim.dimprev{stroke-dasharray:7 4}
155
156
  body.dimon line.member,body.dimon line.seg,body.dimon image{cursor:crosshair}
156
157
  body.csaxison #svg,body.csaxison line.member,body.csaxison line.seg,body.csaxison image{cursor:crosshair}
@@ -424,6 +425,7 @@ let mode='sel', drag=null, picking=false, pickKind='profile', pickEnd=null, geoM
424
425
  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
425
426
  let csaxisMode=false, csDraft=null; // Local coordinate system "set axes" tool: armed flag + in-progress origin [x,y] (null until click 1). P.frame={o,u} holds the committed local frame (null = global).
426
427
  let dimChain=false, dimChainPrev=null, dimSeq=0; // chained "continuous" dimensioning: toggle, the running {point,axis,off,rot}, and a counter for unique ids on rapid clicks
428
+ let dimSplitMode=false; // "add split point" mode on a selected dim — each click inserts a point and splits the dim segment under it into two
427
429
  let selIds=new Set();
428
430
  let undo=[], redo=[];
429
431
  const byId=id=>P.members.find(m=>m.id===id);
@@ -770,6 +772,7 @@ const VARIES=Symbol('varies');
770
772
  function agg(list,get){if(!list.length)return undefined;const f=get(list[0]);for(let i=1;i<list.length;i++)if(get(list[i])!==f)return VARIES;return f;}
771
773
  function panel(){
772
774
  const p=document.getElementById('panel');
775
+ if(!selDimIds.size||!dimsVisible)dimSplitMode=false;document.body.classList.toggle('dimsplit',dimSplitMode); // split mode is meaningless without a (visible) dim selected — also disarms when dims are hidden
773
776
  if(csaxisMode){p.innerHTML=`<span class=badge>Set local axes</span>
774
777
  <div class=hint id=csHint style="margin-top:8px">${csDraft?'Click the <b>second</b> point — the local-X direction. Y follows at 90°.':'Click the <b>origin</b> point to start.'}</div>
775
778
  <div class=hint style="margin-top:8px">Two clicks define the frame; snaps to member ends &amp; grid (<b>Alt</b> off). <b>Esc</b> cancels.</div>`;return;}
@@ -785,6 +788,14 @@ function panel(){
785
788
  document.getElementById('dimAxY').onclick=()=>{dimSetAxis('y');dimRefreshPrev();panel();};
786
789
  document.getElementById('dimChainB').onclick=()=>toggleDimChain();
787
790
  return;}
791
+ if(selDimIds.size&&!selIds.size&&!dimMode&&mode==='sel'){const one=selDimIds.size===1?P.dims.find(x=>x.id===[...selDimIds][0]):null;
792
+ p.innerHTML=`<span class=badge>Dimension${selDimIds.size>1?'s · '+selDimIds.size:''}</span>
793
+ ${one?`<div class=hint style="margin-top:8px">Length <b>${esc(dimValueText(dimGeo(one.a,one.b,one.axis,one.off).px))}</b> · ${({free:'Free (aligned)',x:'X (horizontal)',y:'Y (vertical)'})[one.axis]}</div>`:''}
794
+ <div class=sect style="margin-top:12px">Edit</div>
795
+ <div class=seg2><button id=dimSplitB class="${dimSplitMode?'on':''}" title="Add split point (S) — click points along the dimension to insert extra dimension points; each click splits the segment under it. Esc ends.">✂ Add split point</button></div>
796
+ <div class=hint style="margin-top:8px">${dimSplitMode?'Click points along the dimension to split it — keep clicking to add more. <b>Alt</b> = no snap · <b>Esc</b> ends.':'Drag an end handle to re-measure · <b>Del</b> removes · <b>Esc</b> deselects.'}</div>`;
797
+ document.getElementById('dimSplitB').onclick=()=>toggleDimSplit();
798
+ return;}
788
799
  if(mode==='add'){
789
800
  p.innerHTML=`<span class=badge>Add member</span>
790
801
  <div class=sect>Current profile</div>
@@ -858,7 +869,7 @@ function panel(){
858
869
  document.getElementById('verifyBtn').onclick=()=>edit(()=>{const v=!allVerified;for(const m of selArr())m.verified=v;});
859
870
  {const _gel=document.getElementById('geoEL');if(_gel)_gel.onclick=()=>{geoMode=(geoMode==='el'?null:'el');setGeo();render();};}
860
871
  {const _sw=document.getElementById('swapEnds');if(_sw)_sw.onclick=()=>edit(()=>{for(const m of selArr())swapMemberEnds(m);});} // reverse start↔end for the whole selection (2D + 3D via render+sync3D)
861
- document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(m=>!selIds.has(m.id));selIds.clear();});
872
+ document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(m=>!selIds.has(m.id));selIds.clear();if(selDimIds.size){P.dims=P.dims.filter(d=>!selDimIds.has(d.id));selDimIds.clear();}}); // a mixed marquee selection also holds dims — remove them too (matches keyboard Delete)
862
873
  {const sp=v=>edit(()=>{for(const m of selArr())if(m.role!=='column')m.position=v;});
863
874
  const _pt=document.getElementById('posTop'),_pm=document.getElementById('posMid'),_pb=document.getElementById('posBot');
864
875
  if(_pt)_pt.onclick=()=>sp('top');if(_pm)_pm.onclick=()=>sp('middle');if(_pb)_pb.onclick=()=>sp('bottom');}
@@ -934,7 +945,7 @@ function panel(){
934
945
  wireDet('dtB',m.ends[1]);
935
946
  document.getElementById('matchEnds').onclick=()=>edit(()=>{m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail};});
936
947
  }
937
- document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(x=>x.id!==m.id);selIds.clear();});
948
+ document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(x=>x.id!==m.id);selIds.clear();if(selDimIds.size){P.dims=P.dims.filter(d=>!selDimIds.has(d.id));selDimIds.clear();}}); // also drop any dims a mixed marquee selected
938
949
  {const vb=document.getElementById('verifyBtn');if(vb)vb.onclick=()=>edit(()=>{m.verified=!m.verified;});}
939
950
  }
940
951
  function addFromSeg(sid){const sg=P.segments.find(s=>s.id===sid);if(!sg)return;const prof=addProfile;const id='m'+Date.now();const pv=snapshot();
@@ -1018,6 +1029,26 @@ function dimHintText(){if(dimChainPrev)return 'Chaining — click the next point
1018
1029
  const a={free:'Free (aligned)',x:loc?'X (local)':'X (horizontal)',y:loc?'Y (local)':'Y (vertical)'}[dimDraft?dimDraft.axis:dimAxis];
1019
1030
  return 'Dimension — '+a+(dimChain?' · Chain ON':'')+(loc?' · local axes':'')+'. Click 1: start · Click 2: end · Click 3: place. Shift=lock axis · X/Y/F force · Alt=no snap · Esc=cancel.';}
1020
1031
  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)
1032
+ function toggleDimSplit(){dimSplitMode=!dimSplitMode;snapClear();render();} // S / "Add split point": arm splitting on the selected dim(s); each click then inserts a point
1033
+ // Split the SELECTED dim nearest the click into two at a point ON its baseline (same axis + offset).
1034
+ // Picks the closest selected dim within a screen tolerance whose interior the click projects into; the
1035
+ // split point is the perpendicular foot, so a free (oblique) dim never kinks and the total is preserved.
1036
+ // Keeps both halves selected so you can keep adding points. No-op (with a hint) if the click isn't on a dim.
1037
+ function dimSplitAt(m){
1038
+ const tol=16/zoom;let best=null;
1039
+ for(const d of P.dims){if(!selDimIds.has(d.id))continue;
1040
+ const pr=projPt(m,d.a,d.b); // foot on the a→b baseline + parameter t (clamped 0..1)
1041
+ if(pr.t<=0.02||pr.t>=0.98)continue; // not interior → can't split here
1042
+ const dist=Math.hypot(m[0]-pr.pt[0],m[1]-pr.pt[1]); // how far the click is from this dim's line
1043
+ if(dist<=tol&&(!best||dist<best.dist))best={d,pt:pr.pt,dist};}
1044
+ if(!best){toast('Click on the dimension (between its ends) to split it');return;}
1045
+ const d=best.d,mid=best.pt; // split point ON the baseline (no kink)
1046
+ const id1='d'+Date.now()+'_'+(dimSeq++),id2='d'+Date.now()+'_'+(dimSeq++);
1047
+ const d1={id:id1,a:d.a.slice(),b:mid.slice(),axis:d.axis,off:d.off,rot:d.rot},d2={id:id2,a:mid.slice(),b:d.b.slice(),axis:d.axis,off:d.off,rot:d.rot}; // halves inherit the parent's baked local-frame rotation (undefined = global)
1048
+ const pv=snapshot();P.dims=P.dims.filter(x=>x.id!==d.id);P.dims.push(d1,d2);
1049
+ selDimIds.delete(d.id);selDimIds.add(id1);selDimIds.add(id2); // both halves stay selected → keep adding points
1050
+ pushUndo(pv);render();sync3D();
1051
+ }
1021
1052
  let dimLastPtr=null; // last canvas pointer during placement, for axis-key live refresh
1022
1053
  function dimRefreshPrev(){if(!dimMode||!dimDraft||!dimLastPtr)return;
1023
1054
  const g=document.getElementById('dimPrevG');if(!g)return;
@@ -1080,6 +1111,7 @@ function toast(msg){let t=document.getElementById('toast');
1080
1111
  svg.addEventListener('pointerdown',e=>{if(e.button!==0)return;const t=e.target;
1081
1112
  if(csaxisMode){csClick(e);e.preventDefault();return;} // set-local-axes armed → clicks define origin then X-direction
1082
1113
  if(dimMode){dimClick(e);e.preventDefault();return;} // tool armed → all clicks place dimension points
1114
+ if(dimSplitMode&&selDimIds.size&&dimsVisible&&mode==='sel'){let q=toSvg(e),x=q.x,y=q.y;if(!e.altKey){buildSnap(null);const sn=snap(x,y);x=sn.x;y=sn.y;}snapClear();dimSplitAt([x,y]);e.preventDefault();return;} // split mode → each click inserts a point into the selected dim under it
1083
1115
  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)
1084
1116
  if(t.dataset.dim&&mode==='sel'){const did=t.dataset.dim;selIds.clear(); // a dim click is member-exclusive (clears any member selection)
1085
1117
  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
@@ -1121,6 +1153,7 @@ svg.addEventListener('pointerdown',e=>{if(e.button!==0)return;const t=e.target;
1121
1153
  svg.addEventListener('pointermove',e=>{
1122
1154
  if(csaxisMode){csPrev(e);return;}
1123
1155
  if(dimMode){dimPrev(e);return;}
1156
+ if(dimSplitMode&&dimsVisible){let q=toSvg(e),x=q.x,y=q.y;if(!e.altKey){buildSnap(null);const sn=snap(x,y);sn.hit?snapMark(sn.x,sn.y):snapClear();}else snapClear();return;} // split mode → show the snap marker for the prospective split point
1124
1157
  if(geoMode==='split'&&selIds.size===1&&!drag){const m=byId([...selIds][0]);if(m){const q=toSvg(e),raw=projPt([q.x,q.y],m.wp[0],m.wp[1]);
1125
1158
  const d=Math.hypot(q.x-raw.pt[0],q.y-raw.pt[1]);
1126
1159
  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;}
@@ -1196,6 +1229,7 @@ addEventListener('keydown',e=>{
1196
1229
  if(e.key==='Escape'&&csaxisMode){if(csDraft){csDraft=null;csPrevClear();}else{csaxisMode=false;setCsMode();}render();return;} // 1st Esc drops the in-progress origin, 2nd exits set-axes
1197
1230
  if(e.key==='Enter'&&dimMode&&dimChainPrev){e.preventDefault();dimChainPrev=null;dimPrevClear();render();return;} // Enter ends a running chain
1198
1231
  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
1232
+ if(e.key==='Escape'&&selDimIds.size){if(dimSplitMode){dimSplitMode=false;snapClear();render();}else{selDimIds.clear();render();}return;} // Esc on a selected dim: end split mode → else deselect
1199
1233
  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;}
1200
1234
  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)
1201
1235
  if(selDimIds.size){P.dims=P.dims.filter(d=>!selDimIds.has(d.id));selDimIds.clear();}
@@ -1204,10 +1238,11 @@ addEventListener('keydown',e=>{
1204
1238
  if(dimMode&&!inForm&&!e.ctrlKey&&!e.metaKey&&!e.altKey){const dk=e.key.toLowerCase();
1205
1239
  if(dk==='x'||dk==='y'||dk==='f'){e.preventDefault();dimSetAxis(dk==='f'?'free':dk);dimDraft&&svg.querySelector('#dimPrevG')&&dimRefreshPrev();return;}
1206
1240
  if(dk==='c'){e.preventDefault();toggleDimChain();return;}} // C — toggle chained (continuous) dimensioning
1241
+ if(!inForm&&selDimIds.size>=1&&!selIds.size&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&e.key.toLowerCase()==='s'){e.preventDefault();toggleDimSplit();return;} // S — toggle "add split point" on the selected dimension(s)
1207
1242
  if(!inForm&&selIds.size>=1&&!e.ctrlKey&&!e.metaKey&&!e.altKey){const kk=e.key.toLowerCase();
1208
1243
  if(kk==='e'){e.preventDefault();geoMode=(geoMode==='el'?null:'el');setGeo();render();return;} // Extend/Trim — any selection
1209
1244
  if(kk==='p'){e.preventDefault();edit(()=>{for(const m of selArr())swapMemberEnds(m);});return;} // P — swap start↔end for the whole selection (2D + 3D)
1210
- if(kk==='s'&&selIds.size===1){e.preventDefault();geoMode=(geoMode==='split'?null:'split');setGeo();render();return;}} // Split — single only
1245
+ if(kk==='s'&&selIds.size===1&&!selDimIds.size){e.preventDefault();geoMode=(geoMode==='split'?null:'split');setGeo();render();return;}} // member Split — single member only, and not while dims are also selected (mixed → ambiguous, so neither split fires)
1211
1246
  if(!(e.ctrlKey||e.metaKey))return;const k=e.key.toLowerCase();
1212
1247
  if(k==='z'&&!e.shiftKey){e.preventDefault();doUndo();}
1213
1248
  else if(k==='y'||(k==='z'&&e.shiftKey)){e.preventDefault();doRedo();}
@@ -1625,7 +1660,7 @@ function setPlan(i){C.active=i;P=C.plans[i];
1625
1660
  profs=[...new Set([...P.members.map(m=>m.profile), ...Object.keys(WT)])].sort();
1626
1661
  undo=P.undo||(P.undo=[]);redo=P.redo||(P.redo=[]);
1627
1662
  selIds=new Set();picking=false;pickKind='profile';pickEnd=null;mode='sel';geoMode=null;
1628
- dimMode=false;dimChain=false;selDimIds=new Set();setDimMode(); // Dimension tool resets per plan (incl. chain); setDimMode syncs the button/body.dimon classes + clears any draft/preview/chain (dimsVisible persists across plans)
1663
+ dimMode=false;dimChain=false;dimSplitMode=false;selDimIds=new Set();setDimMode(); // Dimension tool resets per plan (incl. chain + split); setDimMode syncs the button/body.dimon classes + clears any draft/preview/chain (dimsVisible persists across plans)
1629
1664
  csaxisMode=false;setCsMode(); // set-axes tool resets per plan; P.frame itself is per-plan data (persisted), so it stays
1630
1665
  defaultTOS=(P.default_tos!=null?P.default_tos:198);
1631
1666
  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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.40.0",
3
+ "version": "0.41.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": {