@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.
- package/dist/floless-server.cjs +2 -2
- package/dist/web/steel-editor.html +37 -11
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -52856,7 +52856,7 @@ function appVersion() {
|
|
|
52856
52856
|
return resolveVersion({
|
|
52857
52857
|
isSea: isSea2(),
|
|
52858
52858
|
sqVersionXml: readSqVersionXml(),
|
|
52859
|
-
define: true ? "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.
|
|
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
|
-
|
|
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});});
|
|
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
|
|
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(){
|
|
940
|
-
|
|
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==='
|
|
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;}
|