@floless/app 0.42.0 → 0.43.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.42.0" : void 0,
52859
+ define: true ? "0.43.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.42.0" : void 0 });
52869
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.43.0" : void 0 });
52870
52870
  }
52871
52871
 
52872
52872
  // oauth-presets.ts
@@ -143,7 +143,7 @@
143
143
  #lbView.drag{cursor:grabbing}
144
144
  #lbImg{position:absolute;left:50%;top:50%;max-width:86vw;max-height:80vh;transform-origin:center;user-select:none;-webkit-user-drag:none}
145
145
  line.draw{stroke:var(--brand);stroke-width:6;stroke-linecap:round;stroke-dasharray:8 5;opacity:.85;pointer-events:none}
146
- circle.snapmk{fill:none;stroke:#22d3ee;stroke-width:2;vector-effect:non-scaling-stroke;pointer-events:none}
146
+ .snapmk{fill:none;stroke:#22d3ee;stroke-width:2;vector-effect:non-scaling-stroke;pointer-events:none}
147
147
  line.dim{stroke:#22d3ee;stroke-width:2.5;vector-effect:non-scaling-stroke;pointer-events:stroke;cursor:pointer}
148
148
  line.dimwit{stroke:#22d3ee;stroke-width:1;opacity:.85;vector-effect:non-scaling-stroke;pointer-events:none}
149
149
  circle.dimend{fill:#22d3ee;pointer-events:none}
@@ -960,24 +960,34 @@ function rectHit(p0,p1,r){
960
960
  const c=[[r[0],r[1]],[r[2],r[1]],[r[2],r[3]],[r[0],r[3]]];
961
961
  for(let i=0;i<4;i++) if(segSeg(p0,p1,c[i],c[(i+1)%4])) return true;
962
962
  return false;}
963
- // --- snap to existing member/segment endpoints (members lie on grids → snaps to grid intersections); Alt = off ---
964
- const SNAP_PX=9; let snapPts=[], snapSegs=[];
963
+ // --- snap to existing member/segment/dimension endpoints + MIDPOINTS (△); members lie on grids → snaps to grid intersections; Alt = off ---
964
+ const SNAP_PX=9; let snapPts=[], snapSegs=[], snapMids=[], lastSnapKind='end'; // lastSnapKind drives the marker glyph: 'mid' → triangle, else circle
965
+ const _mid=(a,b)=>[(a[0]+b[0])/2,(a[1]+b[1])/2];
965
966
  function buildSnap(except){const ex=except instanceof Set?except:(except!=null?new Set([except]):new Set()); // id or Set of ids to skip
966
- snapPts=[];snapSegs=[];
967
- for(const m of P.members){if(ex.has(m.id))continue;snapPts.push(m.wp[0],m.wp[1]);snapSegs.push([m.wp[0],m.wp[1]]);}
968
- for(const s of P.segments){snapPts.push(s.a,s.b);snapSegs.push([s.a,s.b]);}}
967
+ snapPts=[];snapSegs=[];snapMids=[];
968
+ for(const m of P.members){if(ex.has(m.id))continue;snapPts.push(m.wp[0],m.wp[1]);snapSegs.push([m.wp[0],m.wp[1]]);snapMids.push(_mid(m.wp[0],m.wp[1]));}
969
+ for(const s of P.segments){snapPts.push(s.a,s.b);snapSegs.push([s.a,s.b]);snapMids.push(_mid(s.a,s.b));}
970
+ if(dimsVisible&&Array.isArray(P.dims))for(const d of P.dims){if(ex.has(d.id))continue; // snap to a dimension's VISIBLE (offset) line — its ends, midpoint, and onto-line (dimGeo already gives g.mid)
971
+ const g=dimGeo(d.a,d.b,d.axis,d.off,d.rot);snapPts.push(g.p1,g.p2);snapSegs.push([g.p1,g.p2]);snapMids.push(g.mid);}}
969
972
  // nearest point lying ON another member/segment LINE (perpendicular foot, clamped to the segment) within tol
970
973
  function nearestOnLine(x,y,tol){let bp=null,bd=tol;
971
974
  for(const sg of snapSegs){const pr=projPt([x,y],sg[0],sg[1]);const d=Math.hypot(pr.pt[0]-x,pr.pt[1]-y);if(d<bd){bd=d;bp=pr.pt;}}
972
975
  return bp?{x:bp[0],y:bp[1],d:bd}:null;}
973
- function snap(x,y){const tol=SNAP_PX/zoom;let bp=null,bd=tol;
976
+ function snap(x,y){const tol=SNAP_PX/zoom;lastSnapKind='end';let bp=null,bd=tol;
974
977
  for(const q of snapPts){const d=Math.hypot(q[0]-x,q[1]-y);if(d<bd){bd=d;bp=q;}}
975
- if(bp)return {x:bp[0],y:bp[1],hit:true}; // 1) snap to an endpoint/grid intersection
976
- const ln=nearestOnLine(x,y,tol);if(ln)return {x:ln.x,y:ln.y,hit:true}; // 2) else snap ONTO a line (not only its endpoints)
977
- let sx=x,sy=y,bx=tol,by=tol; // 3) else axis-align to a nearby point
978
+ if(bp)return {x:bp[0],y:bp[1],hit:true,kind:'end'}; // 1) endpoint / grid intersection (highest priority)
979
+ let mp=null,md=tol;for(const q of snapMids){const d=Math.hypot(q[0]-x,q[1]-y);if(d<md){md=d;mp=q;}}
980
+ if(mp){lastSnapKind='mid';return {x:mp[0],y:mp[1],hit:true,kind:'mid'};} // 2) midpoint of a member/segment/dimension line (△)
981
+ const ln=nearestOnLine(x,y,tol);if(ln)return {x:ln.x,y:ln.y,hit:true,kind:'line'}; // 3) else snap ONTO a line (not only its endpoints)
982
+ let sx=x,sy=y,bx=tol,by=tol; // 4) else axis-align to a nearby point
978
983
  for(const q of snapPts){const dx=Math.abs(q[0]-x);if(dx<bx){bx=dx;sx=q[0];}const dy=Math.abs(q[1]-y);if(dy<by){by=dy;sy=q[1];}}
979
- return {x:sx,y:sy,hit:false};}
980
- function snapMark(x,y){let c=document.getElementById('snapMark');if(!c){c=document.createElementNS('http://www.w3.org/2000/svg','circle');c.id='snapMark';c.setAttribute('class','snapmk');svg.appendChild(c);}c.setAttribute('cx',x);c.setAttribute('cy',y);c.setAttribute('r',6/zoom);}
984
+ return {x:sx,y:sy,hit:false,kind:null};}
985
+ // one persistent marker element; kind 'mid' draws an upward triangle, everything else the original circle. Defaults to the last snap()'s kind.
986
+ function snapMark(x,y,kind){kind=kind||lastSnapKind;snapClear();
987
+ const ns='http://www.w3.org/2000/svg';let el;
988
+ if(kind==='mid'){const r=8/zoom;el=document.createElementNS(ns,'path');el.setAttribute('d',`M ${x} ${y-r} L ${x+0.866*r} ${y+0.5*r} L ${x-0.866*r} ${y+0.5*r} Z`);} // apex-up equilateral, centroid ON the snap point
989
+ else{el=document.createElementNS(ns,'circle');el.setAttribute('cx',x);el.setAttribute('cy',y);el.setAttribute('r',6/zoom);}
990
+ el.id='snapMark';el.setAttribute('class','snapmk');svg.appendChild(el);}
981
991
  function snapClear(){const c=document.getElementById('snapMark');if(c)c.remove();}
982
992
  // --- Dimension tool: armed mode + 3-click placement (anchor, anchor, offset). Shares the editor's
983
993
  // buildSnap/snap/snapMark stack. The in-progress preview lives in its own <g> (render() rebuilds
@@ -1112,7 +1122,7 @@ svg.addEventListener('pointerdown',e=>{if(e.button!==0)return;const t=e.target;
1112
1122
  if(csaxisMode){csClick(e);e.preventDefault();return;} // set-local-axes armed → clicks define origin then X-direction
1113
1123
  if(dimMode){dimClick(e);e.preventDefault();return;} // tool armed → all clicks place dimension points
1114
1124
  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
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)
1125
+ if(t.dataset.dimend!=null&&mode==='sel'){const id=t.dataset.dim,end=+t.dataset.dimend;buildSnap(id);drag={type:'dimend',id,end,pre:snapshot()};svg.setPointerCapture(e.pointerId);e.preventDefault();return;} // drag a selected dim's anchor handle → re-measure (snaps; exclude this dim so the end can't snap to its own line/midpoint)
1116
1126
  if(t.dataset.dim&&mode==='sel'){const did=t.dataset.dim;selIds.clear(); // a dim click is member-exclusive (clears any member selection)
1117
1127
  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
1118
1128
  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)
@@ -1156,7 +1166,7 @@ svg.addEventListener('pointermove',e=>{
1156
1166
  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
1157
1167
  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]);
1158
1168
  const d=Math.hypot(q.x-raw.pt[0],q.y-raw.pt[1]);
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;}
1169
+ 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],'end');}else snapClear();}return;} // the cut lands ON the member → always the neutral circle, never a stale midpoint △ (e.g. Alt held = no snap() this frame)
1160
1170
  if(!drag)return;const p=toSvg(e);
1161
1171
  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
1162
1172
  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],d.rot);render();}return;}
@@ -1168,11 +1178,12 @@ svg.addEventListener('pointermove',e=>{
1168
1178
  drag.rect.setAttribute('x',x);drag.rect.setAttribute('y',y);drag.rect.setAttribute('width',w);drag.rect.setAttribute('height',h);drag.cur=[x,y,x+w,y+h];return;}
1169
1179
  if(drag.type==='move'){let dx=p.x-drag.start[0],dy=p.y-drag.start[1];
1170
1180
  if(e.shiftKey){[dx,dy]=orthoLock(0,0,dx,dy);snapClear();} // ortho move — lock the delta to the local frame (or screen H/V)
1171
- else if(!e.altKey){let best=null,bd=SNAP_PX/zoom; // shift the group so a moving endpoint lands on a snap target (endpoint OR line)
1181
+ else if(!e.altKey){let best=null,bd=SNAP_PX/zoom; // shift the group so a moving endpoint lands on a snap target (endpoint, midpoint △, OR line)
1172
1182
  for(const it of drag.items)for(const o of [it.o0,it.o1]){const nx=o[0]+dx,ny=o[1]+dy;
1173
- for(const q of snapPts){const d=Math.hypot(q[0]-nx,q[1]-ny);if(d<bd){bd=d;best={q,cx:q[0]-nx,cy:q[1]-ny};}}
1174
- const ln=nearestOnLine(nx,ny,bd);if(ln){bd=ln.d;best={q:[ln.x,ln.y],cx:ln.x-nx,cy:ln.y-ny};}} // also snap onto other lines
1175
- if(best){dx+=best.cx;dy+=best.cy;snapMark(best.q[0],best.q[1]);}else snapClear();}
1183
+ for(const q of snapPts){const d=Math.hypot(q[0]-nx,q[1]-ny);if(d<bd){bd=d;best={q,cx:q[0]-nx,cy:q[1]-ny,kind:'end'};}}
1184
+ for(const q of snapMids){const d=Math.hypot(q[0]-nx,q[1]-ny);if(d<bd){bd=d;best={q,cx:q[0]-nx,cy:q[1]-ny,kind:'mid'};}}
1185
+ const ln=nearestOnLine(nx,ny,bd);if(ln){bd=ln.d;best={q:[ln.x,ln.y],cx:ln.x-nx,cy:ln.y-ny,kind:'line'};}} // also snap onto other lines
1186
+ if(best){dx+=best.cx;dy+=best.cy;snapMark(best.q[0],best.q[1],best.kind);}else snapClear();}
1176
1187
  else snapClear();
1177
1188
  for(const it of drag.items){const m=byId(it.id);if(!m)continue;
1178
1189
  m.wp[0]=[it.o0[0]+dx,it.o0[1]+dy];m.wp[1]=[it.o1[0]+dx,it.o1[1]+dy];updateLine(m);}
@@ -1704,6 +1715,17 @@ if(new URLSearchParams(location.search).get('selftest')==='1'){(function(){
1704
1715
  {const tz={frame:{o:[0,0],u:[0,0]}};sanitizeFrame(tz);ok(tz.frame===null,'sanitizeFrame drops zero-length u');}
1705
1716
  {const tb={frame:{o:[0,0],u:[1]}};sanitizeFrame(tb);ok(tb.frame===null,'sanitizeFrame drops malformed u');}
1706
1717
  P.frame=sf; // restore
1718
+ // 6) snap — midpoint tier (△) + dimension lines as snap targets + self-snap exclusion
1719
+ {const sm=P.members,ss=P.segments,sd=P.dims,sdv=dimsVisible,sz=zoom;
1720
+ zoom=1;P.members=[{id:'tm',wp:[[0,0],[100,0]]}];P.segments=[];P.dims=[];dimsVisible=true;
1721
+ buildSnap(null);
1722
+ const e1=snap(1,1);ok(e1.hit&&e1.kind==='end'&&pnear([e1.x,e1.y],[0,0]),'snap → endpoint wins near a corner');
1723
+ const m1=snap(50,3);ok(m1.hit&&m1.kind==='mid'&&pnear([m1.x,m1.y],[50,0]),'snap → member midpoint (△)');
1724
+ P.dims=[{id:'td',a:[0,0],b:[100,0],axis:'free',off:20,rot:0}]; // visible dim line runs (0,20)→(100,20), mid (50,20)
1725
+ buildSnap(null);
1726
+ const d1=snap(50,18);ok(d1.hit&&d1.kind==='mid'&&pnear([d1.x,d1.y],[50,20]),'snap → dimension-line midpoint (△)');
1727
+ buildSnap('td');const d2=snap(50,18);ok(!d2.hit,'snap → excluded dim cannot self-snap');
1728
+ P.members=sm;P.segments=ss;P.dims=sd;dimsVisible=sdv;zoom=sz;}
1707
1729
  const msg=fails.length?('SELFTEST FAIL: '+fails.join(' | ')):'SELFTEST PASS (local-frame math)';
1708
1730
  console[fails.length?'error':'log'](msg);try{toast(msg);}catch(_){}
1709
1731
  })();}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.42.0",
3
+ "version": "0.43.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": {