seqeyes-python 0.2.0__py3-none-any.whl
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.
- seqeyes/__init__.py +32 -0
- seqeyes/_plot.py +221 -0
- seqeyes/_renderer.py +236 -0
- seqeyes/resources/pulseq-bundle.js +2296 -0
- seqeyes/resources/viewer.html +1589 -0
- seqeyes_python-0.2.0.dist-info/METADATA +124 -0
- seqeyes_python-0.2.0.dist-info/RECORD +8 -0
- seqeyes_python-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1589 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
6
|
+
<title>SeqEyes — Pulseq MRI Sequence Viewer</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
9
|
+
CSS — standalone viewer (derived from VS Code webview styles.css)
|
|
10
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
11
|
+
:root{--bg:#fff;--fg:#222;--gr:#ddd;--rf:#e6194b;--rff:rgba(230,25,75,.07);--gx:#3cb44b;--gy:#4363d8;--gz:#f58231;--adc:#42d4f4;--adf:rgba(66,212,244,.14);--tr:#911eb4;--ax:#aaa;--lb:#888;--cr:#ee0000;--bd:#ddd;--trbg:#f8f8f8}
|
|
12
|
+
body.theme-dark{--bg:#1e1e1e;--fg:#ddd;--gr:#3a3a3a;--rf:#ff6b8a;--rff:rgba(255,107,138,.05);--gx:#5cdb5c;--gy:#6b8cff;--gz:#ffb347;--adc:#5ce1f4;--adf:rgba(92,225,244,.10);--tr:#d45cff;--ax:#666;--lb:#999;--cr:#ff4444;--bd:#3a3a3a;--trbg:#252525}
|
|
13
|
+
body.theme-light{--bg:#fff;--fg:#222;--gr:#ddd;--rf:#e6194b;--rff:rgba(230,25,75,.07);--gx:#3cb44b;--gy:#4363d8;--gz:#f58231;--adc:#42d4f4;--adf:rgba(66,212,244,.14);--tr:#911eb4;--ax:#aaa;--lb:#888;--cr:#ee0000;--bd:#ddd;--trbg:#f8f8f8}
|
|
14
|
+
body.theme-onelight{--bg:#fafafa;--fg:#383a42;--gr:#e5e5e6;--rf:#e45649;--rff:rgba(228,86,73,.08);--gx:#50a14f;--gy:#4078f2;--gz:#c18401;--adc:#0184bc;--adf:rgba(1,132,188,.12);--tr:#a626a4;--ax:#a0a1a7;--lb:#696c77;--cr:#e45649;--bd:#e5e5e6;--trbg:#f5f5f5}
|
|
15
|
+
body.theme-onedark{--bg:#282c34;--fg:#abb2bf;--gr:#3e4452;--rf:#e06c75;--rff:rgba(224,108,117,.10);--gx:#98c379;--gy:#61afef;--gz:#e5c07b;--adc:#56b6c2;--adf:rgba(86,182,194,.14);--tr:#c678dd;--ax:#5c6370;--lb:#828997;--cr:#e06c75;--bd:#3e4452;--trbg:#2c313a}
|
|
16
|
+
body.theme-dracula{--bg:#282a36;--fg:#f8f8f2;--gr:#44475a;--rf:#ff5555;--rff:rgba(255,85,85,.10);--gx:#50fa7b;--gy:#8be9fd;--gz:#ffb86c;--adc:#8be9fd;--adf:rgba(139,233,253,.14);--tr:#bd93f9;--ax:#6272a4;--lb:#8890b8;--cr:#ff5555;--bd:#44475a;--trbg:#2e3040}
|
|
17
|
+
body.theme-nord{--bg:#2e3440;--fg:#eceff4;--gr:#434c5e;--rf:#bf616a;--rff:rgba(191,97,106,.10);--gx:#a3be8c;--gy:#81a1c1;--gz:#d08770;--adc:#88c0d0;--adf:rgba(136,192,208,.14);--tr:#b48ead;--ax:#4c566a;--lb:#7b8898;--cr:#bf616a;--bd:#434c5e;--trbg:#353b49}
|
|
18
|
+
body.theme-githublight{--bg:#ffffff;--fg:#24292f;--gr:#d0d7de;--rf:#cf222e;--rff:rgba(207,34,46,.07);--gx:#1a7f37;--gy:#0969da;--gz:#bc4c00;--adc:#0969da;--adf:rgba(9,105,218,.10);--tr:#8250df;--ax:#afb8c1;--lb:#656d76;--cr:#cf222e;--bd:#d0d7de;--trbg:#f6f8fa}
|
|
19
|
+
body.theme-github{--bg:#0d1117;--fg:#c9d1d9;--gr:#21262d;--rf:#ff7b72;--rff:rgba(255,123,114,.08);--gx:#7ee787;--gy:#79c0ff;--gz:#ffa657;--adc:#a5d6ff;--adf:rgba(165,214,255,.12);--tr:#d2a8ff;--ax:#30363d;--lb:#8b949e;--cr:#ff7b72;--bd:#21262d;--trbg:#161b22}
|
|
20
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
21
|
+
body{font:13px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--fg);overflow:hidden;height:100vh;user-select:none;display:flex;flex-direction:column}
|
|
22
|
+
#main{display:flex;flex:1;overflow:hidden}
|
|
23
|
+
#left{display:flex;flex-direction:column;flex:1;overflow:hidden;min-width:0}
|
|
24
|
+
#right{width:0;overflow:hidden;position:relative;border-left:1px solid var(--gr);transition:width .25s}
|
|
25
|
+
#right.open{width:500px}
|
|
26
|
+
#right .handle{position:absolute;left:0;top:0;bottom:0;width:5px;cursor:col-resize;z-index:20}
|
|
27
|
+
#right .handle:hover{background:var(--adc);opacity:.3}
|
|
28
|
+
#right canvas{position:absolute;top:0;left:0;display:block;width:100%;height:100%}
|
|
29
|
+
#kg{z-index:0;pointer-events:none}
|
|
30
|
+
#kc{z-index:1;pointer-events:auto;background:transparent}
|
|
31
|
+
#rc{position:absolute;top:4px;left:4px;z-index:10;display:flex;flex-direction:column;gap:2px}
|
|
32
|
+
#rc button{background:var(--gr);border:1px solid var(--ax);color:var(--fg);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:10px}
|
|
33
|
+
#rc button:hover{opacity:.75}
|
|
34
|
+
#tb{display:flex;align-items:center;gap:6px;padding:5px 10px;background:var(--bg);border-bottom:1px solid var(--gr);font-size:11px;flex-shrink:0;flex-wrap:wrap}
|
|
35
|
+
#tb button{background:var(--gr);border:1px solid var(--ax);color:var(--fg);padding:2px 7px;border-radius:3px;cursor:pointer;font-size:11px}
|
|
36
|
+
#tb button:hover{opacity:.75}
|
|
37
|
+
#tb button:disabled{opacity:.45;cursor:not-allowed}
|
|
38
|
+
#tb select{background:var(--gr);border:1px solid var(--ax);color:var(--fg);padding:2px 4px;border-radius:3px;font-size:11px;cursor:pointer}
|
|
39
|
+
#tb .sep{width:1px;height:16px;background:var(--ax);margin:0 3px}
|
|
40
|
+
#tb .lg{display:flex;gap:7px;row-gap:2px;align-items:center;font-size:11px;flex-wrap:wrap}
|
|
41
|
+
#tb .li{cursor:pointer;display:flex;align-items:center;gap:3px;padding:1px 4px;border-radius:3px;opacity:1;transition:opacity .15s}
|
|
42
|
+
#tb .li.off{opacity:.30}
|
|
43
|
+
#tb .ld{width:10px;height:10px;border-radius:2px;border:1px solid rgba(0,0,0,.15)}
|
|
44
|
+
#tb .cur{font:11px monospace;color:var(--cr);margin-left:auto;min-width:300px;text-align:right;white-space:nowrap}
|
|
45
|
+
#tb .ulbl{font-size:10px;color:var(--lb);margin:0 1px}
|
|
46
|
+
#cc{flex:1;overflow:hidden;position:relative;cursor:crosshair}
|
|
47
|
+
#cc.pan{cursor:grabbing}
|
|
48
|
+
canvas{display:block}
|
|
49
|
+
#tt{position:absolute;pointer-events:none;background:rgba(0,0,0,.88);color:#fff;padding:5px 9px;border-radius:4px;font:11px monospace;white-space:pre;display:none;z-index:100;line-height:1.35;max-width:340px}
|
|
50
|
+
#mmap{height:22px;border-bottom:1px solid var(--gr);background:var(--trbg);flex-shrink:0;cursor:pointer;position:relative}
|
|
51
|
+
#mmap canvas{display:block;width:100%;height:100%}
|
|
52
|
+
#poverlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:500;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}
|
|
53
|
+
#pcard{background:var(--bg);border:1px solid var(--gr);border-radius:10px;padding:24px 32px;min-width:280px;max-width:380px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.18)}
|
|
54
|
+
#pspinner{width:32px;height:32px;border:3px solid var(--gr);border-top-color:var(--adc);border-radius:50%;animation:spin .7s linear infinite;margin:0 auto 12px}
|
|
55
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
56
|
+
#ptext{font-size:12px;color:var(--fg);margin-bottom:10px;line-height:1.4}
|
|
57
|
+
#pbar2{height:5px;background:var(--gr);border-radius:3px;overflow:hidden;margin:0 0 6px}
|
|
58
|
+
#pfill2{height:100%;width:0%;background:var(--adc);border-radius:3px;transition:width .4s ease}
|
|
59
|
+
#ppct{font-size:11px;color:var(--lb);font-family:monospace}
|
|
60
|
+
#mc,#kc,#mmc{touch-action:none}
|
|
61
|
+
body{overscroll-behavior:none}
|
|
62
|
+
body.layout-vertical #main{flex-direction:column}
|
|
63
|
+
body.layout-vertical #left{flex:1;min-height:0}
|
|
64
|
+
body.layout-vertical #right{width:100%!important;height:0;border-left:none;border-top:1px solid var(--gr);transition:height .25s;flex-shrink:0}
|
|
65
|
+
body.layout-vertical #right.open{height:300px;width:100%!important}
|
|
66
|
+
body.layout-vertical #right .handle{left:0;right:0;top:0;width:100%;height:5px;cursor:row-resize;bottom:auto}
|
|
67
|
+
body.layout-vertical #rc{flex-direction:row;gap:4px;top:6px;left:4px}
|
|
68
|
+
#splash{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--bg);z-index:200;flex-direction:column;gap:16px}
|
|
69
|
+
#splash h1{font-size:28px;color:var(--fg);letter-spacing:1px}
|
|
70
|
+
#splash p{color:var(--lb);font-size:14px}
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<!-- ── Toolbar ── -->
|
|
75
|
+
<div id="tb">
|
|
76
|
+
<button id="openBtn" title="Open another .seq file">📂 Open</button>
|
|
77
|
+
<button id="zi" title="Zoom In (scroll wheel)">+</button>
|
|
78
|
+
<button id="zo" title="Zoom Out">−</button>
|
|
79
|
+
<button id="zf" title="Fit All">Fit</button>
|
|
80
|
+
<button id="zr" title="Reset">↺</button><div class="sep"></div>
|
|
81
|
+
<button id="exportKspaceBtn" title="Export ADC k-space trajectory and metadata" disabled>Export</button>
|
|
82
|
+
<button id="pnsBtn" title="Load a Siemens .asc file with PNS hardware parameters">📄 Load ASC</button><div class="sep"></div>
|
|
83
|
+
<span class="ulbl">Theme:</span><select id="theme"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option><option value="dracula">Dracula</option><option value="nord">Nord</option><option value="onelight">One Light</option><option value="onedark">One Dark</option><option value="githublight">GitHub Light</option><option value="github">GitHub</option></select>
|
|
84
|
+
<div class="sep"></div>
|
|
85
|
+
<span class="ulbl">Time:</span><select id="tu"><option value="s">s</option><option value="ms" selected>ms</option><option value="us">µs</option></select>
|
|
86
|
+
<span class="ulbl">Grad:</span><select id="gu"><option value="Hz/m" selected>Hz/m</option><option value="mT/m">mT/m</option><option value="G/cm">G/cm</option></select>
|
|
87
|
+
<div class="sep"></div>
|
|
88
|
+
<label class="li" id="bbt" title="Toggle block boundary lines"><input type="checkbox" id="bbc" style="margin:0;cursor:pointer"><span style="font-size:11px;color:var(--fg)">Blocks</span></label>
|
|
89
|
+
<div class="sep"></div><div class="lg" id="legend"></div>
|
|
90
|
+
<button id="kbtn" title="Toggle K-Space View" style="margin-left:4px">K-Space</button>
|
|
91
|
+
<span class="cur" id="cur">← hover for time</span>
|
|
92
|
+
</div>
|
|
93
|
+
<!-- ── Progress overlay ── -->
|
|
94
|
+
<div id="poverlay" style="display:none">
|
|
95
|
+
<div id="pcard">
|
|
96
|
+
<div id="pspinner"></div>
|
|
97
|
+
<div id="ptext">Loading sequence…</div>
|
|
98
|
+
<div id="pbar2"><div id="pfill2"></div></div>
|
|
99
|
+
<div id="ppct">0%</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<!-- ── Minimap ── -->
|
|
103
|
+
<div id="mmap"><canvas id="mmc"></canvas></div>
|
|
104
|
+
<!-- ── Main split: waveforms | k-space ── -->
|
|
105
|
+
<div id="main">
|
|
106
|
+
<div id="left"><div id="cc"><canvas id="mc"></canvas><div id="tt"></div></div></div>
|
|
107
|
+
<div id="right"><div class="handle" id="khandle"></div><canvas id="kg"></canvas><canvas id="kc"></canvas><div id="rc"><button id="krst" title="Reset view">↺</button><button id="kax" title="Toggle projection">Prj</button><button id="kunit" title="Toggle k-space unit (1/m ↔ rad/m)">Unit: 1/m</button><div style="display:flex;align-items:center;gap:2px;margin-top:4px"><span style="font-size:9px;color:var(--lb)">Size</span><input type="range" id="kdot" min="1" max="12" value="2" style="width:50px;accent-color:var(--adc)" title="ADC marker size"></div></div></div>
|
|
108
|
+
</div>
|
|
109
|
+
<!-- Hidden file inputs for standalone mode -->
|
|
110
|
+
<input type="file" id="fileInput" accept=".seq" style="display:none">
|
|
111
|
+
<input type="file" id="ascFileInput" accept=".asc" style="display:none">
|
|
112
|
+
|
|
113
|
+
<!-- ═══════════════════════════════════════════════════════════════════════
|
|
114
|
+
PULSEQ BUNDLE — injected by Python at build time
|
|
115
|
+
═══════════════════════════════════════════════════════════════════════ -->
|
|
116
|
+
<!-- PULSEQ_BUNDLE_PLACEHOLDER -->
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
120
|
+
SeqEyes Standalone Viewer — Application State & Data Loading
|
|
121
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
122
|
+
(function(){
|
|
123
|
+
"use strict";
|
|
124
|
+
|
|
125
|
+
// ── DOM refs ──
|
|
126
|
+
var cc=document.getElementById('cc'),mc=document.getElementById('mc'),ctx=mc.getContext('2d'),
|
|
127
|
+
tt=document.getElementById('tt'),curEl=document.getElementById('cur'),legend=document.getElementById('legend'),
|
|
128
|
+
tuSel=document.getElementById('tu'),guSel=document.getElementById('gu');
|
|
129
|
+
var exportBtn=document.getElementById('exportKspaceBtn');
|
|
130
|
+
var pnsBtn=document.getElementById('pnsBtn');
|
|
131
|
+
var BL=[],TD=0,GR=1e-5,RR=1e-6,AR=1e-7,BR=1e-5;
|
|
132
|
+
var M={t:8,r:30,b:22,l:92};
|
|
133
|
+
var CH=['RF','φ','Gx','Gy','Gz','ADC','Trig','PNS','M1x','M1y','M1z'];
|
|
134
|
+
var chColors=['var(--rf)','var(--rf)','var(--gx)','var(--gy)','var(--gz)','var(--adc)','var(--tr)','var(--fg)','var(--gx)','var(--gy)','var(--gz)'];
|
|
135
|
+
var chVis=[true,true,true,true,true,true,true,false,false,false,false];
|
|
136
|
+
var gMax=[1,6.28318,1,1,1,0,0,1,0.001,0.001,0.001];
|
|
137
|
+
var ox=0,sc=1;
|
|
138
|
+
var dr=false,dsx=0,dso=0;
|
|
139
|
+
var cursorT=0,cursorActive=false;
|
|
140
|
+
var timeUnit='ms',gradUnit='Hz/m';
|
|
141
|
+
var showBB=false;
|
|
142
|
+
var GAMMA=42576;
|
|
143
|
+
var ampZoom=[1,1,1,1,1,1,1,1,1,1,1];
|
|
144
|
+
var kTraj=null,kAdc=null,kTime=null,kAdcTime=null;
|
|
145
|
+
var m1Data=null,pnsData=null,pnsWindowData=null,pnsWindowPending=null,pnsWindowRequestId=0,pnsBusy=false,m1Busy=false,m1RequestedChannel=8,m1ReferenceMode='rfCenter',m1RestoreChannels=null;
|
|
146
|
+
var pnsHardware=null; // Set when user loads a Siemens .asc file
|
|
147
|
+
var _lastDecodedBlocks=null; // Raw decoded blocks for PNS/M1 recomputation
|
|
148
|
+
var derivedRenderPointCount=0,derivedEnvelopeCurveCount=0,derivedRawCurveCount=0,lastDrawDurationMs=0,viewerDrawCount=0;
|
|
149
|
+
var viewerDrawFrame=0,viewerDrawMinimap=false;
|
|
150
|
+
var seqTiming=null;
|
|
151
|
+
var blockPos=[];
|
|
152
|
+
|
|
153
|
+
function scheduleViewerDraw(includeMinimap){
|
|
154
|
+
viewerDrawMinimap=viewerDrawMinimap||includeMinimap;
|
|
155
|
+
if(viewerDrawFrame)return;
|
|
156
|
+
viewerDrawFrame=requestAnimationFrame(function(){
|
|
157
|
+
viewerDrawFrame=0;draw();
|
|
158
|
+
if(viewerDrawMinimap)drawMinimap();
|
|
159
|
+
viewerDrawMinimap=false;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Unit conversion ──
|
|
164
|
+
function gradConv(v){if(gradUnit==='mT/m')return v/GAMMA;if(gradUnit==='G/cm')return v/(GAMMA*10);return v;}
|
|
165
|
+
function gradUnitStr(){return gradUnit;}
|
|
166
|
+
function timeConv(t){if(timeUnit==='ms')return t*1e3;if(timeUnit==='us')return t*1e6;return t;}
|
|
167
|
+
function timeUnitStr(){return timeUnit;}
|
|
168
|
+
|
|
169
|
+
// ── Legend ──
|
|
170
|
+
function buildLegend(){
|
|
171
|
+
legend.innerHTML='';
|
|
172
|
+
chColors.forEach(function(c,i){
|
|
173
|
+
var d=document.createElement('div');d.className='li'+(chVis[i]?'':' off');d.title='Toggle '+CH[i];
|
|
174
|
+
d.innerHTML='<div class="ld" style="background:'+c+'"></div>'+CH[i];
|
|
175
|
+
if(i>=8&&i<=10&&!m1Data)d.title='Calculate and show '+CH[i];
|
|
176
|
+
else if(i===7&&!pnsData)d.title='Select a PNS ASC file before showing PNS';
|
|
177
|
+
d.onclick=function(){
|
|
178
|
+
if(i>=8&&i<=10&&!m1Data)return;
|
|
179
|
+
if(i===7&&!pnsData)return;
|
|
180
|
+
chVis[i]=!chVis[i];
|
|
181
|
+
buildLegend();computeGlobalMax();draw();
|
|
182
|
+
};
|
|
183
|
+
legend.appendChild(d);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
buildLegend();
|
|
187
|
+
tuSel.onchange=function(){timeUnit=tuSel.value;draw();};
|
|
188
|
+
guSel.onchange=function(){gradUnit=guSel.value;draw();};
|
|
189
|
+
function setExportButtonEnabled(enabled){if(exportBtn)exportBtn.disabled=!enabled;}
|
|
190
|
+
|
|
191
|
+
// ── Global amplitude ranges ──
|
|
192
|
+
function computeGlobalMax(){
|
|
193
|
+
gMax=[0.001,6.28318,0.001,0.001,0.001,1,1,0.001,0.001,0.001,0.001];
|
|
194
|
+
for(var i=0;i<BL.length;i++){var b=BL[i];
|
|
195
|
+
if(b.rf){var a=Math.abs(b.rf.a||0);if(a>gMax[0])gMax[0]=a;}
|
|
196
|
+
if(b.gx&&b.gx.ty!=='none'&&Math.abs(b.gx.a||0)>gMax[2])gMax[2]=Math.abs(b.gx.a);
|
|
197
|
+
if(b.gy&&b.gy.ty!=='none'&&Math.abs(b.gy.a||0)>gMax[3])gMax[3]=Math.abs(b.gy.a);
|
|
198
|
+
if(b.gz&&b.gz.ty!=='none'&&Math.abs(b.gz.a||0)>gMax[4])gMax[4]=Math.abs(b.gz.a);
|
|
199
|
+
}
|
|
200
|
+
if(pnsData)gMax[7]=Math.max(gMax[7],pnsData.x.maxAbs,pnsData.y.maxAbs,pnsData.z.maxAbs,pnsData.n.maxAbs);
|
|
201
|
+
if(m1Data){gMax[8]=Math.max(gMax[8],m1Data.x.maxAbs);gMax[9]=Math.max(gMax[9],m1Data.y.maxAbs);gMax[10]=Math.max(gMax[10],m1Data.z.maxAbs);}
|
|
202
|
+
// Floor at 1 Hz so zero‑RF sequences still have a usable scale
|
|
203
|
+
gMax[0]=Math.max(gMax[0],1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Coordinate mapping ──
|
|
207
|
+
function visChannels(){var v=[];for(var i=0;i<CH.length;i++)if(chVis[i])v.push(i);return v;}
|
|
208
|
+
function cy(vi){var vc=visChannels(),h=(mc.height/(window.devicePixelRatio||1)-M.t-M.b)/Math.max(vc.length,1);return M.t+vi*h+h/2;}
|
|
209
|
+
function cH(){var vc=visChannels();return(mc.height/(window.devicePixelRatio||1)-M.t-M.b)/Math.max(vc.length,1);}
|
|
210
|
+
function t2x(t){return M.l+(t-ox)*sc}
|
|
211
|
+
function x2t(x){return ox+(x-M.l)/sc}
|
|
212
|
+
|
|
213
|
+
// ── Viewport bounds ──
|
|
214
|
+
function plotWidth(){var dpr=window.devicePixelRatio||1,w=mc.width/dpr;return Math.max(1,w-M.l-M.r);}
|
|
215
|
+
function minRasterTime(){var vals=[GR,RR,AR,BR].filter(function(v){return isFinite(v)&&v>0;});return vals.length?Math.min.apply(null,vals):1e-7;}
|
|
216
|
+
function visibleDuration(){return plotWidth()/Math.max(sc,1e-12);}
|
|
217
|
+
function clampView(){
|
|
218
|
+
var pw=plotWidth();
|
|
219
|
+
if(!isFinite(TD)||TD<=0){ox=0;sc=1;return;}
|
|
220
|
+
if(!isFinite(sc)||sc<=0)sc=pw/TD;
|
|
221
|
+
var minDur=Math.min(TD,minRasterTime());
|
|
222
|
+
var dur=visibleDuration();
|
|
223
|
+
if(!isFinite(dur)||dur<=0)dur=TD;
|
|
224
|
+
dur=Math.max(minDur,Math.min(TD,dur));
|
|
225
|
+
sc=pw/dur;
|
|
226
|
+
var maxOx=Math.max(0,TD-dur);
|
|
227
|
+
if(!isFinite(ox))ox=0;
|
|
228
|
+
ox=Math.max(0,Math.min(ox,maxOx));
|
|
229
|
+
}
|
|
230
|
+
function zoomAt(screenX,zf){
|
|
231
|
+
if(!isFinite(zf)||zf<=0)return false;
|
|
232
|
+
var oldSc=sc,oldOx=ox;
|
|
233
|
+
var tm=x2t(screenX);
|
|
234
|
+
sc*=zf;
|
|
235
|
+
var dur=visibleDuration();
|
|
236
|
+
if(!isFinite(dur)||dur<=0)dur=TD||minRasterTime();
|
|
237
|
+
var maxDur=TD||dur,minDur=Math.min(maxDur,minRasterTime());
|
|
238
|
+
dur=Math.max(minDur,Math.min(maxDur,dur));
|
|
239
|
+
sc=plotWidth()/dur;
|
|
240
|
+
ox=tm-(screenX-M.l)/sc;
|
|
241
|
+
clampView();
|
|
242
|
+
return Math.abs(sc-oldSc)>1e-12||Math.abs(ox-oldOx)>1e-12;
|
|
243
|
+
}
|
|
244
|
+
function zoomAtCenter(zf){return zoomAt(M.l+plotWidth()/2,zf);}
|
|
245
|
+
function fit(){var w=mc.width/(window.devicePixelRatio||1);sc=(w-M.l-M.r)/(TD||1e-3);ox=0;clampView();draw();drawMinimap();}
|
|
246
|
+
function fitToFirstTR(){
|
|
247
|
+
if(!seqTiming||seqTiming.trTimeSec<=0){fit();return;}
|
|
248
|
+
var w=mc.width/(window.devicePixelRatio||1);
|
|
249
|
+
var trDur=seqTiming.trTimeSec;
|
|
250
|
+
sc=(w-M.l-M.r)/(trDur*1.2);
|
|
251
|
+
ox=Math.max(0,-trDur*0.1);
|
|
252
|
+
clampView();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Helpers ──
|
|
256
|
+
function nice(r){var ms=[1,2,5,10,20,50,100,200,500,1000,2000,5000,10000,20000,50000];for(var i=0;i<ms.length;i++){var b=Math.pow(10,Math.floor(Math.log10(r)));if(ms[i]*b>=r)return ms[i]*b}return 1000*Math.pow(10,Math.floor(Math.log10(r)))}
|
|
257
|
+
function fmtT(v){if(v>=1)return v.toFixed(3);if(v>=0.001)return v.toFixed(3);return v.toExponential(2)}
|
|
258
|
+
function fmtAmp(v){if(v>=1e6)return(v/1e6).toFixed(1)+'M';if(v>=1e3)return(v/1e3).toFixed(1)+'k';if(v>=1)return v.toFixed(1);return v.toFixed(2)}
|
|
259
|
+
function fmtSignedAmp(v){var av=Math.abs(v),s;if(av>=1e6)s=(av/1e6).toFixed(1)+'M';else if(av>=1e3)s=(av/1e3).toFixed(1)+'k';else if(av>=1)s=av.toFixed(1);else s=av.toFixed(2);return(v<0?'-':'')+s;}
|
|
260
|
+
|
|
261
|
+
function sampleGradAtTime(g,t){
|
|
262
|
+
if(!g||g.ty==='none'||!g.t||!g.w||g.t.length<2||g.w.length<2||!isFinite(t))return 0;
|
|
263
|
+
var n=Math.min(g.t.length,g.w.length);
|
|
264
|
+
if(t<g.t[0]||t>g.t[n-1])return 0;
|
|
265
|
+
var lo=0,hi=n-1;
|
|
266
|
+
while(hi-lo>1){var m=(lo+hi)>>1;if(g.t[m]<=t)lo=m;else hi=m;}
|
|
267
|
+
var dt=g.t[hi]-g.t[lo];
|
|
268
|
+
if(dt<=0)return g.w[lo]||0;
|
|
269
|
+
return g.w[lo]+(g.w[hi]-g.w[lo])*(t-g.t[lo])/dt;
|
|
270
|
+
}
|
|
271
|
+
function fmtG(g,t){var a=gradConv(sampleGradAtTime(g,t));return a.toFixed(1)+' '+gradUnitStr()+' ('+g.ty+')';}
|
|
272
|
+
|
|
273
|
+
// ── Canvas resize ──
|
|
274
|
+
function rs(){
|
|
275
|
+
var dpr=window.devicePixelRatio||1,r=cc.getBoundingClientRect();
|
|
276
|
+
mc.width=r.width*dpr;mc.height=r.height*dpr;
|
|
277
|
+
mc.style.width=r.width+'px';mc.style.height=r.height+'px';
|
|
278
|
+
ctx.setTransform(dpr,0,0,dpr,0,0);
|
|
279
|
+
if(layoutMode==='vertical'||r.width<600){M.l=60;M.r=3;M.t=5;M.b=18;}
|
|
280
|
+
else{M.l=92;M.r=4;M.t=8;M.b=22;}
|
|
281
|
+
var mr=document.getElementById('mmap').getBoundingClientRect();
|
|
282
|
+
mmCanvas.width=mr.width*dpr;mmCanvas.height=mr.height*dpr;
|
|
283
|
+
mmCanvas.style.width=mr.width+'px';mmCanvas.style.height=mr.height+'px';
|
|
284
|
+
mmCtx.setTransform(dpr,0,0,dpr,0,0);
|
|
285
|
+
draw();drawMinimap();
|
|
286
|
+
}
|
|
287
|
+
window.addEventListener('resize',rs);
|
|
288
|
+
new ResizeObserver(rs).observe(cc);
|
|
289
|
+
|
|
290
|
+
// ── Layout ──
|
|
291
|
+
var layoutMode='horizontal';
|
|
292
|
+
function detectLayoutMode(){var was=layoutMode;layoutMode=(window.innerHeight>window.innerWidth)?'vertical':'horizontal';return layoutMode!==was;}
|
|
293
|
+
function applyLayoutMode(){
|
|
294
|
+
document.body.classList.remove('layout-vertical','layout-horizontal');
|
|
295
|
+
document.body.classList.add('layout-'+layoutMode);
|
|
296
|
+
}
|
|
297
|
+
applyLayoutMode();
|
|
298
|
+
var _mainEl=document.getElementById('main');
|
|
299
|
+
if(_mainEl&&typeof ResizeObserver!=='undefined'){
|
|
300
|
+
new ResizeObserver(function(){if(detectLayoutMode()){applyLayoutMode();rs();drawKs();drawMinimap();}}).observe(_mainEl);
|
|
301
|
+
}
|
|
302
|
+
window.addEventListener('resize',function(){if(detectLayoutMode()){applyLayoutMode();rs();drawKs();drawMinimap();}});
|
|
303
|
+
|
|
304
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
305
|
+
Multiresolution Derived Series (from derived-series.js)
|
|
306
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
307
|
+
function createDerivedSeries(time,values,scale){
|
|
308
|
+
var n=Math.min(time?time.length:0,values?values.length:0),maxAbs=0;
|
|
309
|
+
var series={t:time||[],v:values||[],n:n,scale:scale||1,maxAbs:0,levels:[]};
|
|
310
|
+
for(var i=0;i<n;i++)if(isFinite(values[i]))maxAbs=Math.max(maxAbs,Math.abs(values[i]*(scale||1)));
|
|
311
|
+
series.maxAbs=maxAbs;
|
|
312
|
+
if(n<16)return series;
|
|
313
|
+
var bucketSize=8,child=null;
|
|
314
|
+
while(bucketSize<n){
|
|
315
|
+
var count=Math.ceil(n/bucketSize),mins=new Int32Array(count),maxs=new Int32Array(count);
|
|
316
|
+
mins.fill(-1);maxs.fill(-1);
|
|
317
|
+
for(var b=0;b<count;b++){
|
|
318
|
+
var minIndex=-1,maxIndex=-1;
|
|
319
|
+
if(!child){
|
|
320
|
+
var start=b*bucketSize,end=Math.min(n,start+bucketSize);
|
|
321
|
+
for(var j=start;j<end;j++){
|
|
322
|
+
var value=values[j];if(!isFinite(value))continue;
|
|
323
|
+
if(minIndex<0||value<values[minIndex])minIndex=j;
|
|
324
|
+
if(maxIndex<0||value>values[maxIndex])maxIndex=j;
|
|
325
|
+
}
|
|
326
|
+
}else{
|
|
327
|
+
var childStart=b*4,childEnd=Math.min(child.mins.length,childStart+4);
|
|
328
|
+
for(var c=childStart;c<childEnd;c++){
|
|
329
|
+
var candidates=[child.mins[c],child.maxs[c]];
|
|
330
|
+
for(var k=0;k<2;k++){
|
|
331
|
+
var index=candidates[k];if(index<0)continue;
|
|
332
|
+
if(minIndex<0||values[index]<values[minIndex])minIndex=index;
|
|
333
|
+
if(maxIndex<0||values[index]>values[maxIndex])maxIndex=index;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
mins[b]=minIndex;maxs[b]=maxIndex;
|
|
338
|
+
}
|
|
339
|
+
child={bucketSize:bucketSize,mins:mins,maxs:maxs};
|
|
340
|
+
series.levels.push(child);
|
|
341
|
+
if(count<=1)break;
|
|
342
|
+
bucketSize*=4;
|
|
343
|
+
}
|
|
344
|
+
return series;
|
|
345
|
+
}
|
|
346
|
+
function lowerBoundSeries(values,target){var lo=0,hi=values.length;while(lo<hi){var mid=(lo+hi)>>1;if(values[mid]<target)lo=mid+1;else hi=mid}return lo;}
|
|
347
|
+
function upperBoundSeries(values,target){var lo=0,hi=values.length;while(lo<hi){var mid=(lo+hi)>>1;if(values[mid]<=target)lo=mid+1;else hi=mid}return lo;}
|
|
348
|
+
function derivedSeriesWindow(series,viewStart,viewEnd){
|
|
349
|
+
if(!series||series.n<1)return{i0:0,i1:0,count:0};
|
|
350
|
+
var i0=Math.max(0,lowerBoundSeries(series.t,viewStart)-1);
|
|
351
|
+
var i1=Math.min(series.n,upperBoundSeries(series.t,viewEnd)+1);
|
|
352
|
+
return{i0:i0,i1:i1,count:Math.max(0,i1-i0)};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function forEachDerivedPoint(series,vs,ve,maxPoints,fn){
|
|
356
|
+
if(!series||series.n<2)return 0;
|
|
357
|
+
var i0=Math.max(0,lowerBoundSeries(series.t,vs)-1);
|
|
358
|
+
var i1=Math.min(series.n,upperBoundSeries(series.t,ve)+2);
|
|
359
|
+
var range=i1-i0;if(range<=0)return 0;
|
|
360
|
+
if(range<=maxPoints){
|
|
361
|
+
for(var i=i0;i<i1;i++)fn(series.t[i],series.v[i]*series.scale);
|
|
362
|
+
return range;
|
|
363
|
+
}
|
|
364
|
+
var step=Math.max(1,Math.floor(range/maxPoints));
|
|
365
|
+
for(var i=i0;i<i1;i+=step)fn(series.t[i],series.v[i]*series.scale);
|
|
366
|
+
return Math.ceil(range/step);
|
|
367
|
+
}
|
|
368
|
+
function forEachDerivedRange(series,vs,ve,maxRanges,fn){
|
|
369
|
+
if(!series||series.n<2)return 0;
|
|
370
|
+
var i0=Math.max(0,lowerBoundSeries(series.t,vs)-1);
|
|
371
|
+
var i1=Math.min(series.n,upperBoundSeries(series.t,ve)+2);
|
|
372
|
+
var range=i1-i0;if(range<=0)return 0;
|
|
373
|
+
var level=0;while(level<series.levels.length&&series.levels[level].bucketSize*2<range/maxRanges)level++;
|
|
374
|
+
if(level>=series.levels.length){
|
|
375
|
+
for(var i=i0;i<i1;i++)fn(series.t[i],series.v[i]*series.scale,series.v[i]*series.scale);
|
|
376
|
+
return range;
|
|
377
|
+
}
|
|
378
|
+
var lvl=series.levels[level],bs=lvl.bucketSize;
|
|
379
|
+
var b0=Math.floor(i0/bs),b1=Math.min(lvl.mins.length,Math.ceil(i1/bs));
|
|
380
|
+
var count=0;
|
|
381
|
+
for(var b=b0;b<b1;b++){
|
|
382
|
+
var mi=lvl.mins[b],ma=lvl.maxs[b];
|
|
383
|
+
if(mi<0&&ma<0)continue;
|
|
384
|
+
var t=series.t[Math.max(0,Math.min(series.n-1,b*bs+bs/2))];
|
|
385
|
+
var minV=mi>=0?series.v[mi]*series.scale:0;
|
|
386
|
+
var maxV=ma>=0?series.v[ma]*series.scale:0;
|
|
387
|
+
fn(t,minV,maxV);count++;
|
|
388
|
+
}
|
|
389
|
+
return count;
|
|
390
|
+
}
|
|
391
|
+
function sampleDerivedSeriesAtTime(series,t){
|
|
392
|
+
if(!series||!series.t||!series.v||series.n<1||!isFinite(t))return null;
|
|
393
|
+
var n=Math.min(series.n,series.t.length,series.v.length);
|
|
394
|
+
if(n<1||t<series.t[0]||t>series.t[n-1])return null;
|
|
395
|
+
if(n===1||t<=series.t[0])return series.v[0]*series.scale;
|
|
396
|
+
if(t>=series.t[n-1])return series.v[n-1]*series.scale;
|
|
397
|
+
var lo=0,hi=n-1;
|
|
398
|
+
while(hi-lo>1){var m=(lo+hi)>>1;if(series.t[m]<=t)lo=m;else hi=m;}
|
|
399
|
+
var dt=series.t[hi]-series.t[lo];
|
|
400
|
+
if(dt<=0)return series.v[lo]*series.scale;
|
|
401
|
+
return (series.v[lo]+(series.v[hi]-series.v[lo])*(t-series.t[lo])/dt)*series.scale;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
405
|
+
Main Draw Loop (from drawing.js)
|
|
406
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
407
|
+
function draw(){
|
|
408
|
+
var drawStarted=performance.now();derivedRenderPointCount=0;derivedEnvelopeCurveCount=0;derivedRawCurveCount=0;viewerDrawCount++;
|
|
409
|
+
var w=mc.width/(window.devicePixelRatio||1),h=mc.height/(window.devicePixelRatio||1);
|
|
410
|
+
var s=getComputedStyle(document.body);
|
|
411
|
+
ctx.clearRect(0,0,w,h);
|
|
412
|
+
ctx.fillStyle=s.getPropertyValue('--bg').trim();ctx.fillRect(0,0,w,h);
|
|
413
|
+
var visibleRange=visibleDuration();var vs=ox,ve=ox+visibleRange;
|
|
414
|
+
drawGrid(w,h,vs,ve,s);
|
|
415
|
+
drawZeroLines(w,h,s);
|
|
416
|
+
if(showBB)drawBlockBounds(w,h,s);
|
|
417
|
+
drawBlocks(vs,ve,s);
|
|
418
|
+
drawDerivedChannels(vs,ve,s);
|
|
419
|
+
drawAxes(w,h,vs,ve,s);
|
|
420
|
+
drawCursor(w,h,s);
|
|
421
|
+
drawKs();
|
|
422
|
+
lastDrawDurationMs=performance.now()-drawStarted;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function drawCursor(w,h,s){
|
|
426
|
+
if(!cursorActive)return;var cx=t2x(cursorT);
|
|
427
|
+
if(cx<M.l||cx>w-M.r)return;
|
|
428
|
+
ctx.strokeStyle=s.getPropertyValue('--cr').trim();ctx.lineWidth=0.8;ctx.setLineDash([4,3]);
|
|
429
|
+
ctx.beginPath();ctx.moveTo(cx,M.t);ctx.lineTo(cx,h-M.b);ctx.stroke();ctx.setLineDash([]);
|
|
430
|
+
ctx.fillStyle=s.getPropertyValue('--cr').trim();ctx.font='10px monospace';ctx.textAlign='center';
|
|
431
|
+
var label=fmtT(timeConv(cursorT)),tw=ctx.measureText(label).width;
|
|
432
|
+
ctx.fillText(label,Math.max(M.l+tw/2+4,Math.min(cx,w-M.r-tw/2-4)),M.t-1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function drawZeroLines(w,h,s){
|
|
436
|
+
ctx.strokeStyle=s.getPropertyValue('--gr').trim();ctx.lineWidth=0.4;ctx.setLineDash([2,4]);
|
|
437
|
+
var vc=visChannels();for(var vi=0;vi<vc.length;vi++){var i=vc[vi];if((i>=2&&i<=4)||(i>=8&&i<=10)){ctx.beginPath();ctx.moveTo(M.l,cy(vi));ctx.lineTo(w-M.r,cy(vi));ctx.stroke();}}
|
|
438
|
+
ctx.setLineDash([]);
|
|
439
|
+
}
|
|
440
|
+
function drawBlockBounds(w,h,s){
|
|
441
|
+
ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=0.6;ctx.setLineDash([3,6]);
|
|
442
|
+
for(var i=0;i<BL.length;i++){
|
|
443
|
+
var x=t2x(BL[i].s);if(x<M.l||x>w-M.r)continue;
|
|
444
|
+
ctx.beginPath();ctx.moveTo(x,M.t);ctx.lineTo(x,h-M.b);ctx.stroke();
|
|
445
|
+
ctx.fillStyle=s.getPropertyValue('--ax').trim();ctx.font='9px monospace';ctx.textAlign='center';
|
|
446
|
+
ctx.fillText('#'+BL[i].i,x,M.t-2);
|
|
447
|
+
}
|
|
448
|
+
ctx.setLineDash([]);
|
|
449
|
+
}
|
|
450
|
+
function drawGrid(w,h,vs,ve,s){
|
|
451
|
+
ctx.strokeStyle=s.getPropertyValue('--gr').trim();ctx.lineWidth=0.5;
|
|
452
|
+
var st=nice((ve-vs)/8),t=Math.floor(vs/st)*st;
|
|
453
|
+
while(t<=ve){var x=t2x(t);ctx.beginPath();ctx.moveTo(x,M.t);ctx.lineTo(x,h-M.b);ctx.stroke();t+=st;}
|
|
454
|
+
var ch=cH(),vc=visChannels();
|
|
455
|
+
for(var i=0;i<=vc.length;i++){ctx.beginPath();ctx.moveTo(M.l,M.t+i*ch);ctx.lineTo(w-M.r,M.t+i*ch);ctx.stroke();}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function channelRange(ci){
|
|
459
|
+
var z=ampZoom[ci]||1;
|
|
460
|
+
if(ci===0)return Math.max((gMax[0]||1)*z,1e-9);
|
|
461
|
+
if(ci===1)return Math.max(6.28318*z,1e-9);
|
|
462
|
+
if(ci>=2&&ci<=4)return Math.max((gMax[ci]||1)*z,0.001);
|
|
463
|
+
if(ci===7)return Math.max((gMax[7]||1)*z,0.001);
|
|
464
|
+
if(ci>=8&&ci<=10)return Math.max((gMax[ci]||1)*z,1e-12);
|
|
465
|
+
return 1;
|
|
466
|
+
}
|
|
467
|
+
function fmtPhase(v){if(Math.abs(v-6.28318)<1e-3)return'2π';if(Math.abs(v-3.14159)<1e-3)return'π';if(v>=1)return v.toFixed(2);if(v>=0.01)return v.toFixed(3);return v.toExponential(1);}
|
|
468
|
+
function rowClip(vi,ch,fn){
|
|
469
|
+
var w=mc.width/(window.devicePixelRatio||1),top=M.t+vi*ch,bottom=M.t+(vi+1)*ch,rw=Math.max(0,w-M.l-M.r);
|
|
470
|
+
if(rw<=0||bottom<=top)return;
|
|
471
|
+
ctx.save();ctx.beginPath();ctx.rect(M.l,top,rw,bottom-top);ctx.clip();
|
|
472
|
+
try{fn();}finally{ctx.restore();}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function drawAxes(w,h,vs,ve,s){
|
|
476
|
+
var narrow=(typeof layoutMode!=='undefined'&&layoutMode==='vertical')||w<600;
|
|
477
|
+
var axFont=narrow?'9px monospace':'10px monospace';
|
|
478
|
+
var chFont=narrow?'bold 14px monospace':'bold 18px monospace';
|
|
479
|
+
ctx.fillStyle=s.getPropertyValue('--lb').trim();ctx.font=axFont;ctx.textAlign='center';
|
|
480
|
+
ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=1;
|
|
481
|
+
ctx.beginPath();ctx.moveTo(M.l,h-M.b);ctx.lineTo(w-M.r,h-M.b);ctx.stroke();
|
|
482
|
+
var st=nice((ve-vs)/8),t=Math.floor(vs/st)*st;
|
|
483
|
+
while(t<=ve){ctx.fillText(fmtT(timeConv(t)),t2x(t),h-M.b+14);t+=st;}
|
|
484
|
+
ctx.textAlign='right';ctx.fillText('time ('+timeUnitStr()+')',w-4,h-M.b+4);
|
|
485
|
+
var vc=visChannels(),ch=cH();
|
|
486
|
+
for(var vi=0;vi<vc.length;vi++){
|
|
487
|
+
var ci=vc[vi],y0=cy(vi);
|
|
488
|
+
if(narrow){ctx.textAlign='left';ctx.font=chFont;ctx.fillStyle=chColors[ci];ctx.fillText(CH[ci],4,y0+4);}
|
|
489
|
+
else{ctx.textAlign='right';ctx.font=chFont;ctx.fillStyle=chColors[ci];ctx.fillText(CH[ci],M.l-40,y0+4);}
|
|
490
|
+
ctx.fillStyle=s.getPropertyValue('--lb').trim();ctx.font=axFont;
|
|
491
|
+
var lblX=narrow?4:M.l-12;
|
|
492
|
+
ctx.textAlign=narrow?'left':'right';
|
|
493
|
+
if(ci===0){ctx.fillText(fmtAmp(channelRange(0))+'Hz',lblX,M.t+vi*ch+12);ctx.fillText('0',lblX,y0+ch/2-2);}
|
|
494
|
+
else if(ci===1){var ph=channelRange(1);ctx.fillText(fmtPhase(ph),lblX,M.t+vi*ch+12);ctx.fillText(fmtPhase(ph/2),lblX,y0+4);ctx.fillText('0',lblX,y0+ch/2-2);}
|
|
495
|
+
else if(ci>=2&&ci<=4){var d=gradConv(channelRange(ci));ctx.fillText('±'+fmtAmp(d)+gradUnitStr(),lblX,M.t+vi*ch+12);ctx.fillText('0',lblX,y0+4);}
|
|
496
|
+
else if(ci===5){ctx.fillText('on',lblX,M.t+vi*ch+12);}
|
|
497
|
+
else if(ci===6){ctx.fillText('ch',lblX,M.t+vi*ch+12);}
|
|
498
|
+
else if(ci===7){ctx.fillText(fmtAmp(channelRange(7))+'%',lblX,M.t+vi*ch+12);ctx.fillText('0',lblX,M.t+(vi+1)*ch-4);}
|
|
499
|
+
else if(ci>=8&&ci<=10){ctx.fillText('±'+fmtAmp(channelRange(ci))+'s/m',lblX,M.t+vi*ch+12);ctx.fillText('0',lblX,y0+4);}
|
|
500
|
+
ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=0.5;
|
|
501
|
+
ctx.beginPath();ctx.moveTo(M.l-2,M.t+vi*ch);ctx.lineTo(M.l+2,M.t+vi*ch);ctx.stroke();
|
|
502
|
+
ctx.beginPath();ctx.moveTo(M.l-2,y0);ctx.lineTo(M.l+2,y0);ctx.stroke();
|
|
503
|
+
ctx.beginPath();ctx.moveTo(M.l-2,M.t+(vi+1)*ch);ctx.lineTo(M.l+2,M.t+(vi+1)*ch);ctx.stroke();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function drawDerivedChannels(vs,ve,s){
|
|
508
|
+
var ch=cH(),vc=visChannels();
|
|
509
|
+
var viP=-1,viM1x=-1,viM1y=-1,viM1z=-1;
|
|
510
|
+
for(var vi=0;vi<vc.length;vi++){if(vc[vi]===7)viP=vi;if(vc[vi]===8)viM1x=vi;if(vc[vi]===9)viM1y=vi;if(vc[vi]===10)viM1z=vi;}
|
|
511
|
+
var pnsDrawData=pnsSeriesForView(vs,ve);
|
|
512
|
+
if(pnsDrawData&&viP>=0){
|
|
513
|
+
drawPercentSeries(pnsDrawData.x,viP,7,s.getPropertyValue('--gx').trim(),ch,vs,ve);
|
|
514
|
+
drawPercentSeries(pnsDrawData.y,viP,7,s.getPropertyValue('--gy').trim(),ch,vs,ve);
|
|
515
|
+
drawPercentSeries(pnsDrawData.z,viP,7,s.getPropertyValue('--gz').trim(),ch,vs,ve);
|
|
516
|
+
ctx.setLineDash([5,3]);
|
|
517
|
+
drawPercentSeries(pnsDrawData.n,viP,7,s.getPropertyValue('--fg').trim(),ch,vs,ve);
|
|
518
|
+
ctx.setLineDash([]);
|
|
519
|
+
}
|
|
520
|
+
if(m1Data){
|
|
521
|
+
if(viM1x>=0)drawBipolarSeries(m1Data.x,viM1x,8,s.getPropertyValue('--gx').trim(),ch,vs,ve);
|
|
522
|
+
if(viM1y>=0)drawBipolarSeries(m1Data.y,viM1y,9,s.getPropertyValue('--gy').trim(),ch,vs,ve);
|
|
523
|
+
if(viM1z>=0)drawBipolarSeries(m1Data.z,viM1z,10,s.getPropertyValue('--gz').trim(),ch,vs,ve);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function drawPercentSeries(series,vi,ci,c,ch,vs,ve){
|
|
528
|
+
if(!series||series.n<2)return;
|
|
529
|
+
rowClip(vi,ch,function(){
|
|
530
|
+
var maxA=channelRange(ci),base=M.t+(vi+1)*ch-ch*.08,scale=ch*.84/maxA;
|
|
531
|
+
var clipL=M.l,clipR=mc.width/(window.devicePixelRatio||1)-M.r;
|
|
532
|
+
var plotW=Math.max(1,clipR-clipL);
|
|
533
|
+
var rawLimit=Math.max(64,Math.ceil(plotW*2));
|
|
534
|
+
var fineLimit=Math.max(128,Math.ceil(plotW*8));
|
|
535
|
+
var windowRange=derivedSeriesWindow(series,vs,ve);
|
|
536
|
+
var useFinePns=shouldUseFinePnsRendering(windowRange.count,plotW,vs,ve);
|
|
537
|
+
ctx.strokeStyle=c;ctx.lineWidth=1;ctx.beginPath();
|
|
538
|
+
if(windowRange.count<=rawLimit||useFinePns){
|
|
539
|
+
derivedRawCurveCount++;
|
|
540
|
+
var f=1;
|
|
541
|
+
derivedRenderPointCount+=forEachDerivedPoint(series,vs,ve,useFinePns?fineLimit:rawLimit,function(tv,vv){
|
|
542
|
+
var sx=t2x(tv);if(!isFinite(sx)||sx<clipL||sx>clipR){f=1;return}
|
|
543
|
+
var sy=base-vv*scale;if(!isFinite(sy)){f=1;return}
|
|
544
|
+
if(f){ctx.moveTo(sx,sy);f=0}else ctx.lineTo(sx,sy);
|
|
545
|
+
});
|
|
546
|
+
}else{
|
|
547
|
+
derivedEnvelopeCurveCount++;
|
|
548
|
+
var first=1;
|
|
549
|
+
derivedRenderPointCount+=forEachDerivedRange(series,vs,ve,Math.max(32,Math.ceil(clipR-clipL)),function(tv,_minValue,maxValue){
|
|
550
|
+
var sx=t2x(tv),sy=base-maxValue*scale;
|
|
551
|
+
if(!isFinite(sx)||!isFinite(sy)||sx<clipL||sx>clipR){first=1;return}
|
|
552
|
+
if(first){ctx.moveTo(sx,sy);first=0}else ctx.lineTo(sx,sy);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
ctx.stroke();
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function shouldUseFinePnsRendering(count,plotW,vs,ve){
|
|
560
|
+
var dur=ve-vs;
|
|
561
|
+
if(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*10.5)return true;
|
|
562
|
+
return count<=Math.max(256,Math.ceil(plotW*12));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Return pre‑computed PNS data for the current viewport. */
|
|
566
|
+
function pnsSeriesForView(vs,ve){
|
|
567
|
+
if(!pnsData||!pnsData.x||pnsData.x.n<2)return null;
|
|
568
|
+
// Derived series is already created — return as‑is for drawing.
|
|
569
|
+
return {x:pnsData.x, y:pnsData.y, z:pnsData.z, n:pnsData.n};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function drawBipolarSeries(series,vi,ci,c,ch,vs,ve){
|
|
573
|
+
if(!series||series.n<2)return;
|
|
574
|
+
rowClip(vi,ch,function(){
|
|
575
|
+
var maxA=channelRange(ci),y=cy(vi),scale=ch*.4/maxA;
|
|
576
|
+
var clipL=M.l,clipR=mc.width/(window.devicePixelRatio||1)-M.r;
|
|
577
|
+
var rawLimit=Math.max(128,Math.ceil((clipR-clipL)*8));
|
|
578
|
+
if(derivedSeriesWindow(series,vs,ve).count<=rawLimit){
|
|
579
|
+
derivedRawCurveCount++;
|
|
580
|
+
ctx.strokeStyle=c;ctx.lineWidth=1;ctx.beginPath();var f=1;
|
|
581
|
+
derivedRenderPointCount+=forEachDerivedPoint(series,vs,ve,rawLimit,function(tv,vv){
|
|
582
|
+
var sx=t2x(tv);if(!isFinite(sx)||sx<clipL||sx>clipR){f=1;return}
|
|
583
|
+
var sy=y-vv*scale;if(!isFinite(sy)){f=1;return}
|
|
584
|
+
if(f){ctx.moveTo(sx,sy);f=0}else ctx.lineTo(sx,sy);
|
|
585
|
+
});
|
|
586
|
+
ctx.stroke();
|
|
587
|
+
}else{
|
|
588
|
+
derivedEnvelopeCurveCount++;
|
|
589
|
+
var ranges=[];
|
|
590
|
+
var rangeCount=forEachDerivedRange(series,vs,ve,Math.max(32,Math.ceil(clipR-clipL)),function(tv,minValue,maxValue){
|
|
591
|
+
var sx=t2x(tv),upper=y-maxValue*scale,lower=y-minValue*scale;
|
|
592
|
+
if(isFinite(sx)&&isFinite(upper)&&isFinite(lower)&&sx>=clipL&&sx<=clipR)ranges.push([sx,upper,lower]);
|
|
593
|
+
});
|
|
594
|
+
derivedRenderPointCount+=rangeCount*2;
|
|
595
|
+
if(ranges.length){
|
|
596
|
+
ctx.fillStyle=c;ctx.globalAlpha=.14;ctx.beginPath();
|
|
597
|
+
ctx.moveTo(ranges[0][0],ranges[0][1]);
|
|
598
|
+
for(var ri=1;ri<ranges.length;ri++)ctx.lineTo(ranges[ri][0],ranges[ri][1]);
|
|
599
|
+
for(var rj=ranges.length-1;rj>=0;rj--)ctx.lineTo(ranges[rj][0],ranges[rj][2]);
|
|
600
|
+
ctx.closePath();ctx.fill();
|
|
601
|
+
ctx.globalAlpha=.65;ctx.strokeStyle=c;ctx.lineWidth=.8;
|
|
602
|
+
ctx.beginPath();ctx.moveTo(ranges[0][0],(ranges[0][1]+ranges[0][2])*.5);
|
|
603
|
+
for(var rk=1;rk<ranges.length;rk++)ctx.lineTo(ranges[rk][0],(ranges[rk][1]+ranges[rk][2])*.5);
|
|
604
|
+
ctx.stroke();ctx.globalAlpha=1;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function drawBlocks(vs,ve,s){
|
|
611
|
+
var ch=cH(),vc=visChannels();
|
|
612
|
+
for(var bi=0;bi<BL.length;bi++){
|
|
613
|
+
var b=BL[bi];if(b.s+b.d<vs||b.s>ve)continue;
|
|
614
|
+
var viRF=-1,viPh=-1,viGx=-1,viGy=-1,viGz=-1,viADC=-1,viTrig=-1;
|
|
615
|
+
for(var vi=0;vi<vc.length;vi++){if(vc[vi]===0)viRF=vi;if(vc[vi]===1)viPh=vi;if(vc[vi]===2)viGx=vi;if(vc[vi]===3)viGy=vi;if(vc[vi]===4)viGz=vi;if(vc[vi]===5)viADC=vi;if(vc[vi]===6)viTrig=vi;}
|
|
616
|
+
|
|
617
|
+
// RF magnitude
|
|
618
|
+
if(b.rf&&viRF>=0){
|
|
619
|
+
var y=cy(viRF),rc=s.getPropertyValue('--rf').trim(),rff=s.getPropertyValue('--rff').trim();
|
|
620
|
+
var rfx0=t2x(b.rf.s),rfx1=t2x(b.rf.s+b.rf.d);
|
|
621
|
+
rowClip(viRF,ch,function(){
|
|
622
|
+
ctx.fillStyle=rff;ctx.fillRect(rfx0,y-ch*.45,rfx1-rfx0,ch*.9);
|
|
623
|
+
if(b.rf.t&&b.rf.t.length>1){
|
|
624
|
+
ctx.strokeStyle=rc;ctx.lineWidth=1.1;ctx.beginPath();
|
|
625
|
+
for(var i=0,f=1,sc2=ch*.9/channelRange(0);i<b.rf.t.length;i++){
|
|
626
|
+
var sx=t2x(b.rf.t[i]),sy=y+ch*.45-b.rf.m[i]*sc2;
|
|
627
|
+
if(f){ctx.moveTo(sx,sy);f=0}else ctx.lineTo(sx,sy);
|
|
628
|
+
}ctx.stroke();
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
// RF phase
|
|
633
|
+
if(b.rf&&b.rf.p&&viPh>=0){
|
|
634
|
+
var py=cy(viPh);
|
|
635
|
+
rowClip(viPh,ch,function(){
|
|
636
|
+
ctx.strokeStyle=s.getPropertyValue('--rf').trim();ctx.lineWidth=0.8;ctx.beginPath();
|
|
637
|
+
for(var i=0,f=1,psc=ch*.9/channelRange(1);i<b.rf.t.length;i++){
|
|
638
|
+
var sx=t2x(b.rf.t[i]),sy=py+ch*.45-b.rf.p[i]*psc;
|
|
639
|
+
if(f){ctx.moveTo(sx,sy);f=0}else ctx.lineTo(sx,sy);
|
|
640
|
+
}ctx.stroke();
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
// ADC phase curve
|
|
644
|
+
if(b.adc&&viPh>=0&&b.adc.n>1){
|
|
645
|
+
var apy3=cy(viPh);
|
|
646
|
+
var t0=b.adc.s+b.adc.d, fo2=b.adc.fo||0, po2=b.adc.po||0;
|
|
647
|
+
var nAdc=b.adc.n, dw2=b.adc.dw, te2=t0+nAdc*dw2;
|
|
648
|
+
if(te2>vs&&t0<ve){
|
|
649
|
+
rowClip(viPh,ch,function(){
|
|
650
|
+
ctx.strokeStyle=s.getPropertyValue('--adc').trim();ctx.lineWidth=0.8;
|
|
651
|
+
ctx.beginPath();var psc2=ch*.9/channelRange(1);
|
|
652
|
+
var step=Math.max(1,Math.ceil(nAdc/200));
|
|
653
|
+
for(var s2=0,f2=1;s2<=nAdc;s2+=step){
|
|
654
|
+
var tA=(s2<nAdc)?t0+(s2+0.5)*dw2:te2;
|
|
655
|
+
var dtA=tA-t0;
|
|
656
|
+
var phA=((po2+6.283185*fo2*dtA)%6.28318+6.28318)%6.28318;
|
|
657
|
+
var sxA=t2x(tA),syA=apy3+ch*.45-phA*psc2;
|
|
658
|
+
var clipL2=M.l,clipR2=mc.width/(window.devicePixelRatio||1)-M.r;
|
|
659
|
+
if(sxA<clipL2||sxA>clipR2){f2=1;continue}
|
|
660
|
+
if(f2){ctx.moveTo(sxA,syA);f2=0}else ctx.lineTo(sxA,syA);
|
|
661
|
+
}ctx.stroke();
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Gradients
|
|
666
|
+
if(viGx>=0)drawG(b.gx,viGx,2,s.getPropertyValue('--gx').trim(),ch,s,gMax[2]);
|
|
667
|
+
if(viGy>=0)drawG(b.gy,viGy,3,s.getPropertyValue('--gy').trim(),ch,s,gMax[3]);
|
|
668
|
+
if(viGz>=0)drawG(b.gz,viGz,4,s.getPropertyValue('--gz').trim(),ch,s,gMax[4]);
|
|
669
|
+
// ADC
|
|
670
|
+
if(b.adc&&viADC>=0){
|
|
671
|
+
var ay=cy(viADC),af=s.getPropertyValue('--adf').trim(),ac=s.getPropertyValue('--adc').trim();
|
|
672
|
+
var as=b.adc.s+b.adc.d,ae=as+b.adc.n*b.adc.dw;
|
|
673
|
+
if(ae>vs&&as<ve){
|
|
674
|
+
var ax0=t2x(Math.max(as,vs)),ax1=t2x(Math.min(ae,ve));
|
|
675
|
+
ctx.fillStyle=af;ctx.fillRect(ax0,ay-ch*.28,ax1-ax0,ch*.56);
|
|
676
|
+
ctx.strokeStyle=ac;ctx.lineWidth=1;ctx.strokeRect(ax0,ay-ch*.28,ax1-ax0,ch*.56);
|
|
677
|
+
if(ax1-ax0>30){ctx.fillStyle=s.getPropertyValue('--fg').trim();ctx.font='9px monospace';ctx.textAlign='center';ctx.fillText(b.adc.n+'pts',(ax0+ax1)/2,ay+3);}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// Triggers
|
|
681
|
+
if(b.trg&&viTrig>=0){
|
|
682
|
+
var ty=cy(viTrig),tc=s.getPropertyValue('--tr').trim();
|
|
683
|
+
for(var tj=0;tj<b.trg.length;tj++){
|
|
684
|
+
var tg=b.trg[tj],ts=tg.s+tg.d,te=ts+tg.dr;if(te<vs||ts>ve)continue;
|
|
685
|
+
var txm=t2x((ts+te)/2);
|
|
686
|
+
ctx.fillStyle=tc;ctx.beginPath();ctx.moveTo(txm,ty-ch*.05);ctx.lineTo(txm-5,ty+ch*.05);ctx.lineTo(txm+5,ty+ch*.05);ctx.closePath();ctx.fill();
|
|
687
|
+
ctx.fillStyle=tc;ctx.font='8px monospace';ctx.textAlign='center';ctx.fillText('ch'+tg.c,txm,ty+ch*.28);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function drawG(g,vi,ci,c,ch,s,globMax){
|
|
694
|
+
if(!g||g.ty==='none')return;var y=cy(vi);
|
|
695
|
+
if(g.t&&g.t.length>1){
|
|
696
|
+
rowClip(vi,ch,function(){
|
|
697
|
+
var maxA=channelRange(ci);var sc2=ch*.4/maxA;
|
|
698
|
+
ctx.strokeStyle=c;ctx.lineWidth=1;ctx.beginPath();
|
|
699
|
+
var clipL=M.l,clipR=mc.width/(window.devicePixelRatio||1)-M.r;
|
|
700
|
+
for(var i=0,f=1;i<g.t.length;i++){
|
|
701
|
+
var sx=t2x(g.t[i]);if(sx<clipL||sx>clipR){f=1;continue}
|
|
702
|
+
var sy=y-g.w[i]*sc2;if(f){ctx.moveTo(sx,sy);f=0}else ctx.lineTo(sx,sy);
|
|
703
|
+
}ctx.stroke();
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
709
|
+
K-Space Viewer — full WebGL implementation (ported from kspace.js)
|
|
710
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
711
|
+
var kOpen=false, kView="3d";
|
|
712
|
+
var kCx=0, kCy=0, kCz=0, kScl=1;
|
|
713
|
+
var kAutoFit=true;
|
|
714
|
+
var kRotX=-0.5, kRotY=0.7;
|
|
715
|
+
var kDragging=false, kDragPrev=null, kDragBtn=0;
|
|
716
|
+
var kCanvas=document.getElementById("kc"), kCtx=kCanvas.getContext("2d");
|
|
717
|
+
var kDotSize=2, kUnit="cyc";
|
|
718
|
+
|
|
719
|
+
var _tRotX=kRotX, _tRotY=kRotY, _tScl=kScl, _tCx=kCx, _tCy=kCy, _tCz=kCz;
|
|
720
|
+
var _kAnimId=null, _kEasing=0.12;
|
|
721
|
+
|
|
722
|
+
// ── WebGL state ──
|
|
723
|
+
var gl=null, glProgram=null, glBuf=null, glN=0;
|
|
724
|
+
var glAttribPos=-1, glAttribTime=-1;
|
|
725
|
+
var glU_cy=-1,glU_sy=-1,glU_cx=-1,glU_sx=-1,glU_center=-1,glU_scale=-1;
|
|
726
|
+
var glU_halfRes=-1,glU_tMin=-1,glU_tMax=-1,glU_dot=-1,glU_color=-1;
|
|
727
|
+
var _kBxmin=0,_kBxmax=0,_kBymin=0,_kBymax=0,_kBzmin=0,_kBzmax=0,_kBrng=0;
|
|
728
|
+
var _kBoundsDirty=true;
|
|
729
|
+
|
|
730
|
+
function initWebGL(){
|
|
731
|
+
var c=document.getElementById("kg");
|
|
732
|
+
gl=c.getContext("webgl2",{antialias:true,alpha:true,premultipliedAlpha:false})
|
|
733
|
+
||c.getContext("webgl",{antialias:true,alpha:true,premultipliedAlpha:false});
|
|
734
|
+
if(!gl){console.warn("[SeqEyes] WebGL unavailable");return false;}
|
|
735
|
+
gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
|
|
736
|
+
var vs=gl.createShader(gl.VERTEX_SHADER), fs=gl.createShader(gl.FRAGMENT_SHADER);
|
|
737
|
+
gl.shaderSource(vs,'attribute vec3 aPos; attribute float aTime; uniform float uCy,uSy,uCx,uSx,uScale; uniform vec3 uCenter; uniform vec2 uHalfRes; uniform float uTMin,uTMax,uDot; varying float vVis; void main(){ float dx=aPos.x-uCenter.x,dy=aPos.y-uCenter.y,dz=aPos.z-uCenter.z; float rx=dx*uCy-dz*uSy; float rz=dx*uSy+dz*uCy; float ry=dy*uCx-rz*uSx; float sx=rx*uScale/uHalfRes.x; float sy=ry*uScale/uHalfRes.y; vVis=(aTime>=uTMin&&aTime<=uTMax)?1.0:-1.0; gl_Position=vec4(sx,sy,0.0,1.0); gl_PointSize=vVis>0.0?uDot:0.0; }');
|
|
738
|
+
gl.shaderSource(fs,'precision mediump float; uniform vec4 uColor; varying float vVis; void main(){ if(vVis<0.0)discard; float d=length(gl_PointCoord-vec2(0.5)); float a=1.0-smoothstep(0.40,0.50,d); gl_FragColor=vec4(uColor.rgb,uColor.a*a); }');
|
|
739
|
+
gl.compileShader(vs);if(!gl.getShaderParameter(vs,gl.COMPILE_STATUS)){console.warn("[SeqEyes] VS:",gl.getShaderInfoLog(vs));return false;}
|
|
740
|
+
gl.compileShader(fs);if(!gl.getShaderParameter(fs,gl.COMPILE_STATUS)){console.warn("[SeqEyes] FS:",gl.getShaderInfoLog(fs));return false;}
|
|
741
|
+
glProgram=gl.createProgram();
|
|
742
|
+
gl.attachShader(glProgram,vs);gl.attachShader(glProgram,fs);
|
|
743
|
+
gl.linkProgram(glProgram);
|
|
744
|
+
if(!gl.getProgramParameter(glProgram,gl.LINK_STATUS)){console.warn("[SeqEyes] Link:",gl.getProgramInfoLog(glProgram));return false;}
|
|
745
|
+
glAttribPos=gl.getAttribLocation(glProgram,"aPos"); glAttribTime=gl.getAttribLocation(glProgram,"aTime");
|
|
746
|
+
glU_cy=gl.getUniformLocation(glProgram,"uCy");glU_sy=gl.getUniformLocation(glProgram,"uSy");
|
|
747
|
+
glU_cx=gl.getUniformLocation(glProgram,"uCx");glU_sx=gl.getUniformLocation(glProgram,"uSx");
|
|
748
|
+
glU_center=gl.getUniformLocation(glProgram,"uCenter");glU_scale=gl.getUniformLocation(glProgram,"uScale");
|
|
749
|
+
glU_halfRes=gl.getUniformLocation(glProgram,"uHalfRes");glU_dot=gl.getUniformLocation(glProgram,"uDot");
|
|
750
|
+
glU_tMin=gl.getUniformLocation(glProgram,"uTMin");glU_tMax=gl.getUniformLocation(glProgram,"uTMax");
|
|
751
|
+
glU_color=gl.getUniformLocation(glProgram,"uColor");
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function uploadKSpaceGPU(){
|
|
756
|
+
if(!kAdc||!kAdc[0]||kAdc[0].length===0){glN=0;return;}
|
|
757
|
+
if(!gl&&!initWebGL())return;
|
|
758
|
+
var n=kAdc[0].length;
|
|
759
|
+
var data=new Float32Array(n*4);
|
|
760
|
+
var ax=kAdc[0],ay=kAdc[1],az=kAdc[2],at=kAdcTime;
|
|
761
|
+
for(var i=0;i<n;i++){var j=i*4;data[j]=ax[i];data[j+1]=ay[i];data[j+2]=az[i];data[j+3]=at[i];}
|
|
762
|
+
if(glBuf)gl.deleteBuffer(glBuf);
|
|
763
|
+
glBuf=gl.createBuffer();
|
|
764
|
+
gl.bindBuffer(gl.ARRAY_BUFFER,glBuf);
|
|
765
|
+
gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);
|
|
766
|
+
glN=n;
|
|
767
|
+
var xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity,zmin=Infinity,zmax=-Infinity;
|
|
768
|
+
for(var a=0;a<n;a++){var xi=ax[a],yi=ay[a],zi=az[a];if(isFinite(xi)){if(xi<xmin)xmin=xi;if(xi>xmax)xmax=xi;}if(isFinite(yi)){if(yi<ymin)ymin=yi;if(yi>ymax)ymax=yi;}if(isFinite(zi)){if(zi<zmin)zmin=zi;if(zi>zmax)zmax=zi;}}
|
|
769
|
+
_kBxmin=xmin;_kBxmax=xmax;_kBymin=ymin;_kBymax=ymax;_kBzmin=zmin;_kBzmax=zmax;
|
|
770
|
+
_kBrng=Math.max(xmax-xmin,ymax-ymin,zmax-zmin,1e-6);_kBoundsDirty=false;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function kNice(range){var ms=[1,2,5,10,20,50,100,200,500];for(var i=0;i<ms.length;i++){var b=Math.pow(10,Math.floor(Math.log10(range)));if(ms[i]*b>=range/4)return ms[i]*b;}return 1;}
|
|
774
|
+
function _kAutoFitVals(){var rng=_kBrng||1,W=kCanvas.width/(window.devicePixelRatio||1),H=kCanvas.height/(window.devicePixelRatio||1);return{cx:(_kBxmin+_kBxmax)/2,cy:(_kBymin+_kBymax)/2,cz:(_kBzmin+_kBzmax)/2,scl:Math.min(W,H)/(rng*1.15)};}
|
|
775
|
+
function kUnitVal(v){if(kUnit==="rad")return v*6.283185;return v;}
|
|
776
|
+
function kTickVal(v){var u=kUnitVal(v);if(Math.abs(u)>=1000)return (u/1000).toFixed(1)+"k";if(Math.abs(u)>=1)return u.toFixed(1);if(Math.abs(u)>=0.01)return u.toFixed(2);return u.toExponential(1);}
|
|
777
|
+
function parseCSSColor(c){if(!c)return[0.26,0.83,0.96];var m=c.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);if(m)return[parseInt(m[1],16)/255,parseInt(m[2],16)/255,parseInt(m[3],16)/255];m=c.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);if(m)return[+m[1]/255,+m[2]/255,+m[3]/255];return[0.26,0.83,0.96];}
|
|
778
|
+
|
|
779
|
+
function startKSpaceAnim(){
|
|
780
|
+
if(_kAnimId||kDragging)return;
|
|
781
|
+
function tick(){
|
|
782
|
+
var changed=false,eps=0.0005;
|
|
783
|
+
if(Math.abs(_tRotX-kRotX)>eps){kRotX+=(_tRotX-kRotX)*_kEasing;changed=true;}
|
|
784
|
+
if(Math.abs(_tRotY-kRotY)>eps){kRotY+=(_tRotY-kRotY)*_kEasing;changed=true;}
|
|
785
|
+
if(Math.abs(_tScl-kScl)>0.001){kScl+=(_tScl-kScl)*_kEasing;changed=true;}
|
|
786
|
+
if(Math.abs(_tCx-kCx)>0.001){kCx+=(_tCx-kCx)*_kEasing;changed=true;}
|
|
787
|
+
if(Math.abs(_tCy-kCy)>0.001){kCy+=(_tCy-kCy)*_kEasing;changed=true;}
|
|
788
|
+
if(Math.abs(_tCz-kCz)>0.001){kCz+=(_tCz-kCz)*_kEasing;changed=true;}
|
|
789
|
+
if(changed){drawKs();_kAnimId=requestAnimationFrame(tick);}
|
|
790
|
+
else{kRotX=_tRotX;kRotY=_tRotY;kScl=_tScl;kCx=_tCx;kCy=_tCy;kCz=_tCz;_kAnimId=null;}
|
|
791
|
+
}
|
|
792
|
+
_kAnimId=requestAnimationFrame(tick);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function setKSpaceTarget(rx,ry,s,cx,cy,cz,instant){
|
|
796
|
+
if(instant){kRotX=_tRotX=rx;kRotY=_tRotY=ry;kScl=_tScl=s;kCx=_tCx=cx;kCy=_tCy=cy;kCz=_tCz=cz;drawKs();}
|
|
797
|
+
else{_tRotX=rx;_tRotY=ry;_tScl=s;_tCx=cx;_tCy=cy;_tCz=cz;startKSpaceAnim();}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function resizeKc(){
|
|
801
|
+
var r=document.getElementById("right").getBoundingClientRect();
|
|
802
|
+
if(r.width<=0||r.height<=0)return;
|
|
803
|
+
var dpr=window.devicePixelRatio||1;
|
|
804
|
+
var kg=document.getElementById("kg");
|
|
805
|
+
kg.width=r.width*dpr;kg.height=r.height*dpr;
|
|
806
|
+
kg.style.width=r.width+"px";kg.style.height=r.height+"px";
|
|
807
|
+
kCanvas.width=r.width*dpr;kCanvas.height=r.height*dpr;
|
|
808
|
+
kCanvas.style.width=r.width+"px";kCanvas.style.height=r.height+"px";
|
|
809
|
+
kCtx.setTransform(dpr,0,0,dpr,0,0);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function nearestTimeIndex(times,t){
|
|
813
|
+
if(!times||!times.length||!isFinite(t))return -1;
|
|
814
|
+
var lo=0,hi=times.length-1;if(t<=times[0])return 0;if(t>=times[hi])return hi;
|
|
815
|
+
while(lo<hi){var mid=(lo+hi)>>1;if(times[mid]<t)lo=mid+1;else hi=mid;}
|
|
816
|
+
return Math.abs(times[lo]-t)<Math.abs(times[lo-1]-t)?lo:lo-1;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function getKCursorSample(){
|
|
820
|
+
if(!cursorActive||!isFinite(cursorT))return null;
|
|
821
|
+
if(kAdc&&kAdcTime&&kAdc[0]&&kAdcTime.length){
|
|
822
|
+
var idx=nearestTimeIndex(kAdcTime,cursorT);
|
|
823
|
+
if(idx>=0){
|
|
824
|
+
var tol=Math.max(minRasterTime()*0.5,(kAdcTime[Math.min(kAdcTime.length-1,idx+1)]-kAdcTime[Math.max(0,idx-1)])*0.375);
|
|
825
|
+
if(Math.abs(kAdcTime[idx]-cursorT)<=tol){
|
|
826
|
+
return {x:kAdc[0][idx],y:kAdc[1][idx],z:kAdc[2][idx],t:kAdcTime[idx],source:"adc"};
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function fmtKShort(v){var u=kUnitVal(v);if(!isFinite(u))return"--";if(Math.abs(u)>=1000)return(u/1000).toFixed(1)+"k";if(Math.abs(u)>=10)return u.toFixed(1);if(Math.abs(u)>=0.01)return u.toFixed(2);return u.toExponential(1);}
|
|
834
|
+
function formatKCursorReadout(){var s=getKCursorSample();if(!s)return"";return" k="+fmtKShort(s.x)+","+fmtKShort(s.y)+","+fmtKShort(s.z)+" "+ (kUnit==="rad"?"rad/m":"1/m");}
|
|
835
|
+
|
|
836
|
+
function drawKCursorMarker(ctx,proj,cs,W,H){
|
|
837
|
+
var s=getKCursorSample();
|
|
838
|
+
if(!s||!isFinite(s.x)||!isFinite(s.y)||!isFinite(s.z))return;
|
|
839
|
+
var p=proj(s.x,s.y,s.z);
|
|
840
|
+
if(p.x<-12||p.x>W+12||p.y<-12||p.y>H+12)return;
|
|
841
|
+
ctx.save();ctx.lineWidth=2;
|
|
842
|
+
ctx.strokeStyle=cs.getPropertyValue("--cr").trim()||"#ee0000";
|
|
843
|
+
ctx.fillStyle="rgba(255,215,0,0.90)";
|
|
844
|
+
ctx.beginPath();ctx.arc(p.x,p.y,Math.max(5,kDotSize+4),0,6.283);ctx.fill();ctx.stroke();
|
|
845
|
+
ctx.fillStyle=cs.getPropertyValue("--fg").trim();ctx.font="10px monospace";ctx.textAlign="left";
|
|
846
|
+
ctx.fillText(s.source==="adc"?"ADC":"ktraj",p.x+8,p.y-8);ctx.restore();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function drawKs_init(){
|
|
850
|
+
var dpr=window.devicePixelRatio||1;
|
|
851
|
+
var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical');
|
|
852
|
+
var targetW=vertical?document.getElementById("right").getBoundingClientRect().width||500:500;
|
|
853
|
+
var targetH=vertical?300:document.getElementById("right").getBoundingClientRect().height||500;
|
|
854
|
+
if(targetW<=0)targetW=500;if(targetH<=0)targetH=500;
|
|
855
|
+
var kg=document.getElementById("kg");
|
|
856
|
+
kg.width=targetW*dpr;kg.height=targetH*dpr;
|
|
857
|
+
kg.style.width=targetW+"px";kg.style.height=targetH+"px";
|
|
858
|
+
kCanvas.width=targetW*dpr;kCanvas.height=targetH*dpr;
|
|
859
|
+
kCanvas.style.width=targetW+"px";kCanvas.style.height=targetH+"px";
|
|
860
|
+
kCtx.setTransform(dpr,0,0,dpr,0,0);
|
|
861
|
+
drawKs_core(targetW,targetH,dpr);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function drawKs(){resizeKc();var dpr=window.devicePixelRatio||1;drawKs_core(kCanvas.width/dpr,kCanvas.height/dpr,dpr);}
|
|
865
|
+
function drawKsFast(){var dpr=window.devicePixelRatio||1;drawKs_core(kCanvas.width/dpr,kCanvas.height/dpr,dpr);}
|
|
866
|
+
|
|
867
|
+
function drawKs_core(W,H,dpr){
|
|
868
|
+
if(W<=0||H<=0)return;
|
|
869
|
+
var ctx=kCtx,cs=getComputedStyle(document.body);
|
|
870
|
+
ctx.clearRect(0,0,W,H);
|
|
871
|
+
var right=document.getElementById('right');
|
|
872
|
+
if(!kOpen){right.classList.remove('open');return;}
|
|
873
|
+
right.classList.add('open');
|
|
874
|
+
|
|
875
|
+
if(!kAdc||!kAdcTime||!kAdc[0]||kAdc[0].length===0){
|
|
876
|
+
ctx.fillStyle=cs.getPropertyValue('--lb').trim();ctx.font='11px monospace';ctx.textAlign='center';
|
|
877
|
+
ctx.fillText('No ADC events in this sequence',W/2,H/2);return;
|
|
878
|
+
}
|
|
879
|
+
var adcX=kAdc[0],adcY=kAdc[1],adcZ=kAdc[2],nAdc=adcX.length;
|
|
880
|
+
|
|
881
|
+
if(_kBoundsDirty||!isFinite(_kBxmin)){
|
|
882
|
+
var xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity,zmin=Infinity,zmax=-Infinity;
|
|
883
|
+
for(var a=0;a<nAdc;a++){var xi=adcX[a],yi=adcY[a],zi=adcZ[a];if(isFinite(xi)){if(xi<xmin)xmin=xi;if(xi>xmax)xmax=xi;}if(isFinite(yi)){if(yi<ymin)ymin=yi;if(yi>ymax)ymax=yi;}if(isFinite(zi)){if(zi<zmin)zmin=zi;if(zi>zmax)zmax=zi;}}
|
|
884
|
+
_kBxmin=xmin;_kBxmax=xmax;_kBymin=ymin;_kBymax=ymax;_kBzmin=zmin;_kBzmax=zmax;
|
|
885
|
+
_kBrng=Math.max(xmax-xmin,ymax-ymin,zmax-zmin,1e-6);_kBoundsDirty=false;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if(kAutoFit){
|
|
889
|
+
kAutoFit=false;
|
|
890
|
+
var af=_kAutoFitVals();
|
|
891
|
+
setKSpaceTarget(kRotX,kRotY,af.scl,af.cx,af.cy,af.cz,false);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
var visibleRange=visibleDuration(),vs=ox,ve=ox+visibleRange;
|
|
895
|
+
|
|
896
|
+
// ── WebGL scatter (GPU) ──
|
|
897
|
+
if(gl&&glBuf&&glN>0){
|
|
898
|
+
gl.useProgram(glProgram);
|
|
899
|
+
var hW=W*dpr*0.5,hH=H*dpr*0.5;
|
|
900
|
+
gl.viewport(0,0,W*dpr,H*dpr);
|
|
901
|
+
gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);
|
|
902
|
+
gl.uniform1f(glU_cy,Math.cos(kRotY));gl.uniform1f(glU_sy,Math.sin(kRotY));
|
|
903
|
+
gl.uniform1f(glU_cx,Math.cos(kRotX));gl.uniform1f(glU_sx,Math.sin(kRotX));
|
|
904
|
+
gl.uniform3f(glU_center,kCx,kCy,kCz);gl.uniform1f(glU_scale,kScl);
|
|
905
|
+
gl.uniform2f(glU_halfRes,hW,hH);gl.uniform1f(glU_tMin,vs);gl.uniform1f(glU_tMax,ve);
|
|
906
|
+
gl.uniform1f(glU_dot,kDotSize*dpr);
|
|
907
|
+
var ac=cs.getPropertyValue("--adc").trim();var rgb=parseCSSColor(ac);
|
|
908
|
+
gl.uniform4f(glU_color,rgb[0],rgb[1],rgb[2],0.85);
|
|
909
|
+
gl.bindBuffer(gl.ARRAY_BUFFER,glBuf);
|
|
910
|
+
gl.enableVertexAttribArray(glAttribPos);gl.vertexAttribPointer(glAttribPos,3,gl.FLOAT,false,16,0);
|
|
911
|
+
gl.enableVertexAttribArray(glAttribTime);gl.vertexAttribPointer(glAttribTime,1,gl.FLOAT,false,16,12);
|
|
912
|
+
gl.drawArrays(gl.POINTS,0,glN);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ── Canvas 2D axes ──
|
|
916
|
+
var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cxR=Math.cos(kRotX),sxR=Math.sin(kRotX);
|
|
917
|
+
var invDpr=1/dpr;
|
|
918
|
+
function proj(px,py,pz){
|
|
919
|
+
var dx2=px-kCx,dy2=py-kCy,dz2=pz-kCz;
|
|
920
|
+
var rx=dx2*cz-dz2*sz,rz2=dx2*sz+dz2*cz,ry=dy2*cxR-rz2*sxR;
|
|
921
|
+
return {x:W/2+rx*kScl*invDpr,y:H/2-ry*kScl*invDpr};
|
|
922
|
+
}
|
|
923
|
+
var tick=kNice(_kBrng);
|
|
924
|
+
function drawAxis3D(fx,fy,fz,label,col){
|
|
925
|
+
var al=_kBrng*0.65;
|
|
926
|
+
var t1=proj(fx*al,fy*al,fz*al),t2=proj(-fx*al,-fy*al,-fz*al);
|
|
927
|
+
ctx.strokeStyle=col;ctx.lineWidth=1.6;ctx.setLineDash([]);
|
|
928
|
+
ctx.beginPath();ctx.moveTo(t2.x,t2.y);ctx.lineTo(t1.x,t1.y);ctx.stroke();
|
|
929
|
+
ctx.fillStyle=col;ctx.beginPath();ctx.arc(t1.x,t1.y,4,0,6.283);ctx.fill();
|
|
930
|
+
ctx.fillStyle=col;ctx.font="bold 11px monospace";ctx.fillText(label,t1.x+5,t1.y-5);
|
|
931
|
+
ctx.fillStyle=cs.getPropertyValue("--lb").trim();ctx.font="8px monospace";
|
|
932
|
+
for(var v=tick;v<=al;v+=tick){
|
|
933
|
+
var tp=proj(fx*v,fy*v,fz*v),tm=proj(-fx*v,-fy*v,-fz*v);
|
|
934
|
+
ctx.strokeStyle=col;ctx.lineWidth=0.5;
|
|
935
|
+
ctx.beginPath();ctx.moveTo(tp.x-3,tp.y);ctx.lineTo(tp.x+3,tp.y);ctx.stroke();
|
|
936
|
+
ctx.beginPath();ctx.moveTo(tm.x-3,tm.y);ctx.lineTo(tm.x+3,tm.y);ctx.stroke();
|
|
937
|
+
ctx.fillText(kTickVal(v),tp.x+5,tp.y-2);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
drawAxis3D(1,0,0,"kx",cs.getPropertyValue("--gx").trim());
|
|
941
|
+
drawAxis3D(0,1,0,"ky",cs.getPropertyValue("--gy").trim());
|
|
942
|
+
drawAxis3D(0,0,1,"kz",cs.getPropertyValue("--gz").trim());
|
|
943
|
+
var oo=proj(0,0,0);
|
|
944
|
+
ctx.fillStyle=cs.getPropertyValue("--fg").trim();ctx.beginPath();ctx.arc(oo.x,oo.y,4,0,6.283);ctx.fill();
|
|
945
|
+
drawKCursorMarker(ctx,proj,cs,W,H);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// ── K-space panel toggle ──
|
|
949
|
+
document.getElementById("kbtn").onclick=function(){
|
|
950
|
+
if(typeof refreshLayout==='function')refreshLayout();
|
|
951
|
+
kOpen=!kOpen;
|
|
952
|
+
var p=document.getElementById("right");
|
|
953
|
+
if(kOpen){
|
|
954
|
+
p.classList.add("open");
|
|
955
|
+
if(typeof layoutMode!=='undefined'&&layoutMode==='vertical'){
|
|
956
|
+
p.style.setProperty('height','300px','important');p.style.setProperty('width','100%','important');
|
|
957
|
+
}else{p.style.width='500px';}
|
|
958
|
+
this.textContent="K Space ✕";kAutoFit=true;
|
|
959
|
+
}else{
|
|
960
|
+
p.classList.remove("open");
|
|
961
|
+
if(typeof layoutMode!=='undefined'&&layoutMode==='vertical'){
|
|
962
|
+
p.style.setProperty('height','0','important');
|
|
963
|
+
}else{p.style.width='';}
|
|
964
|
+
this.textContent="K Space";
|
|
965
|
+
}
|
|
966
|
+
if(kOpen){requestAnimationFrame(function(){drawKs_init();});}
|
|
967
|
+
else{requestAnimationFrame(function(){resizeKc();drawKs();});}
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
// Camera presets
|
|
971
|
+
document.getElementById("kax").textContent="3D";
|
|
972
|
+
document.getElementById("krst").onclick=function(){
|
|
973
|
+
kView="3d";document.getElementById("kax").textContent="3D";
|
|
974
|
+
var af=_kAutoFitVals();
|
|
975
|
+
setKSpaceTarget(-0.5,0.7,af.scl,af.cx,af.cy,af.cz,false);
|
|
976
|
+
};
|
|
977
|
+
document.getElementById("kax").onclick=function(){
|
|
978
|
+
var views=["3d","xy","xz","yz"];var idx=views.indexOf(kView);
|
|
979
|
+
kView=views[(idx+1)%4];
|
|
980
|
+
var trx=_tRotX,tr=_tRotY;
|
|
981
|
+
if(kView==="xy"){trx=0;tr=0;}else if(kView==="xz"){trx=-Math.PI/2;tr=0;}
|
|
982
|
+
else if(kView==="yz"){trx=0;tr=Math.PI/2;}else{trx=-0.5;tr=0.7;}
|
|
983
|
+
document.getElementById("kax").textContent=kView.toUpperCase();
|
|
984
|
+
var af=_kAutoFitVals();
|
|
985
|
+
setKSpaceTarget(trx,tr,af.scl,af.cx,af.cy,af.cz,false);
|
|
986
|
+
};
|
|
987
|
+
document.getElementById("kunit").onclick=function(){
|
|
988
|
+
kUnit=kUnit==="cyc"?"rad":"cyc";
|
|
989
|
+
this.textContent=kUnit==="cyc"?"Unit: 1/m":"Unit: rad/m";
|
|
990
|
+
drawKs();
|
|
991
|
+
};
|
|
992
|
+
document.getElementById("kdot").oninput=function(){kDotSize=parseInt(this.value);drawKs();};
|
|
993
|
+
|
|
994
|
+
// ── K-space mouse ──
|
|
995
|
+
kCanvas.addEventListener("mousedown",function(e){
|
|
996
|
+
kDragging=true;kDragPrev={x:e.clientX,y:e.clientY};kDragBtn=e.button;
|
|
997
|
+
if(_kAnimId){cancelAnimationFrame(_kAnimId);_kAnimId=null;}
|
|
998
|
+
e.preventDefault();
|
|
999
|
+
});
|
|
1000
|
+
kCanvas.addEventListener("contextmenu",function(e){e.preventDefault();});
|
|
1001
|
+
kCanvas.addEventListener("wheel",function(e){e.preventDefault();
|
|
1002
|
+
var r=kCanvas.getBoundingClientRect(),W=kCanvas.width/(window.devicePixelRatio||1),H=kCanvas.height/(window.devicePixelRatio||1);
|
|
1003
|
+
var mx=(e.clientX-r.left),my=(e.clientY-r.top);
|
|
1004
|
+
var zf=e.deltaY<0?1.25:0.8,dz=(1-1/zf)/kScl;
|
|
1005
|
+
kAutoFit=false;
|
|
1006
|
+
setKSpaceTarget(kRotX,kRotY,kScl*zf,kCx+(mx-W/2)*dz,kCy-(my-H/2)*dz,kCz,false);
|
|
1007
|
+
},{passive:false});
|
|
1008
|
+
window.addEventListener("mousemove",function(e){
|
|
1009
|
+
if(!kDragging||!kDragPrev||!kOpen)return;
|
|
1010
|
+
var dx=e.clientX-kDragPrev.x,dy=e.clientY-kDragPrev.y;
|
|
1011
|
+
kDragPrev={x:e.clientX,y:e.clientY};
|
|
1012
|
+
if(kView!=="3d"){kView="3d";document.getElementById("kax").textContent="3D";}
|
|
1013
|
+
if(kDragBtn===0){kRotY+=dx*0.008;kRotX-=dy*0.008;_tRotY=kRotY;_tRotX=kRotX;}
|
|
1014
|
+
else{
|
|
1015
|
+
var cz1=Math.cos(kRotY),sz1=Math.sin(kRotY),cx1=Math.cos(kRotX),sx1=Math.sin(kRotX);
|
|
1016
|
+
dx/=(window.devicePixelRatio||1)*kScl;dy/=(window.devicePixelRatio||1)*kScl;
|
|
1017
|
+
if(Math.abs(cz1)>0.01){kCy+=dy/cx1;kCx+=(-dx-sz1*sx1*(dy/cx1))/cz1;}
|
|
1018
|
+
else{kCy+=dy/cx1;kCz+=(dx-cz1*sx1*(dy/cx1))/sz1;}
|
|
1019
|
+
_tCy=kCy;_tCx=kCx;_tCz=kCz;kAutoFit=false;
|
|
1020
|
+
}
|
|
1021
|
+
drawKsFast();
|
|
1022
|
+
});
|
|
1023
|
+
window.addEventListener("mouseup",function(){kDragging=false;kDragPrev=null;});
|
|
1024
|
+
|
|
1025
|
+
// ── Resize handle ──
|
|
1026
|
+
var kResizing=false,kResizeStart=0,kResizeW=500,kResizeH=300;
|
|
1027
|
+
document.getElementById("khandle").addEventListener("mousedown",function(e){
|
|
1028
|
+
kResizing=true;
|
|
1029
|
+
if(typeof layoutMode!=='undefined'&&layoutMode==='vertical')kResizeStart=e.clientY;
|
|
1030
|
+
else kResizeStart=e.clientX;
|
|
1031
|
+
e.preventDefault();e.stopPropagation();
|
|
1032
|
+
});
|
|
1033
|
+
window.addEventListener("mousemove",function(e){
|
|
1034
|
+
if(!kResizing)return;
|
|
1035
|
+
var p=document.getElementById("right");
|
|
1036
|
+
var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical');
|
|
1037
|
+
if(vertical){kResizeH=Math.max(120,Math.min(800,kResizeH-(e.clientY-kResizeStart)));kResizeStart=e.clientY;p.style.setProperty('height',kResizeH+'px','important');}
|
|
1038
|
+
else{kResizeW=Math.max(200,Math.min(1200,kResizeW-(e.clientX-kResizeStart)));kResizeStart=e.clientX;p.style.width=kResizeW+"px";p.style.transition="none";}
|
|
1039
|
+
resizeKc();drawKs();
|
|
1040
|
+
});
|
|
1041
|
+
window.addEventListener("mouseup",function(){
|
|
1042
|
+
if(kResizing){kResizing=false;var p=document.getElementById("right");if(typeof layoutMode!=='undefined'&&layoutMode==='vertical'){p.style.setProperty('transition','height .25s','important');}else{p.style.transition='';}}
|
|
1043
|
+
});
|
|
1044
|
+
window.addEventListener("resize",function(){if(kOpen){resizeKc();drawKs();}});
|
|
1045
|
+
|
|
1046
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
1047
|
+
Mouse & Touch Interaction (from interaction.js)
|
|
1048
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
1049
|
+
|
|
1050
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
1051
|
+
Mouse & Touch Interaction (from interaction.js)
|
|
1052
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
1053
|
+
var _touchTooltipTimer=null;
|
|
1054
|
+
|
|
1055
|
+
mc.addEventListener('wheel',function(e){e.preventDefault();
|
|
1056
|
+
var changed=false;
|
|
1057
|
+
if(e.ctrlKey||e.metaKey){
|
|
1058
|
+
var ch=cH(),vc=visChannels(),my=e.clientY-mc.getBoundingClientRect().top;
|
|
1059
|
+
var vi2=Math.floor((my-M.t)/ch);
|
|
1060
|
+
if(vi2>=0&&vi2<vc.length){var ci=vc[vi2],oldZoom=ampZoom[ci];ampZoom[ci]*=e.deltaY<0?1.3:1/1.3;ampZoom[ci]=Math.max(0.1,Math.min(100,ampZoom[ci]));changed=ampZoom[ci]!==oldZoom;}
|
|
1061
|
+
}else{
|
|
1062
|
+
var r=mc.getBoundingClientRect(),mx2=e.clientX-r.left;
|
|
1063
|
+
var zf=e.deltaY<0?1.3:1/1.3;changed=zoomAt(mx2,zf);
|
|
1064
|
+
}
|
|
1065
|
+
if(changed)scheduleViewerDraw(true);},{passive:false});
|
|
1066
|
+
|
|
1067
|
+
mc.addEventListener('mousedown',function(e){if(e.button===0){dr=true;dsx=e.clientX;dso=ox;cc.classList.add('pan')}});
|
|
1068
|
+
window.addEventListener('mousemove',function(e){
|
|
1069
|
+
var r=mc.getBoundingClientRect(),mx2=e.clientX-r.left,my=e.clientY-r.top;cursorT=x2t(mx2);cursorActive=mx2>=M.l&&mx2<=r.width-M.r&&my>=M.t&&my<=r.height-M.b;
|
|
1070
|
+
var kInfo=(typeof formatKCursorReadout==='function')?formatKCursorReadout():'';
|
|
1071
|
+
curEl.textContent=fmtT(timeConv(cursorT))+' '+timeUnitStr()+(kInfo?' | '+kInfo:'');
|
|
1072
|
+
if(dr){ox=dso-(e.clientX-dsx)/sc;clampView();draw();drawMinimap();return}
|
|
1073
|
+
draw();
|
|
1074
|
+
showTooltipAt(e.clientX,e.clientY,cursorT);
|
|
1075
|
+
});
|
|
1076
|
+
window.addEventListener('mouseup',function(){dr=false;cc.classList.remove('pan');drawMinimap();});
|
|
1077
|
+
cc.addEventListener('mouseleave',function(){cursorT=0;cursorActive=false;curEl.textContent='← hover for time';draw();drawMinimap();});
|
|
1078
|
+
|
|
1079
|
+
// Touch
|
|
1080
|
+
var _tchDragging=false,_tchStartX=0,_tchStartO=0,_tchPinch0=0,_tchPinchMid=null,_tchMoved=false;
|
|
1081
|
+
mc.addEventListener('touchstart',function(e){
|
|
1082
|
+
if(e.touches.length===1){_tchDragging=true;_tchMoved=false;_tchStartX=e.touches[0].clientX;_tchStartO=ox;cc.classList.add('pan');
|
|
1083
|
+
var p=getTouchPos(e.touches[0],mc);cursorT=x2t(p.x);cursorActive=p.x>=M.l&&p.x<=mc.width/(window.devicePixelRatio||1)-M.r&&p.y>=M.t&&p.y<=mc.height/(window.devicePixelRatio||1)-M.b;
|
|
1084
|
+
curEl.textContent=fmtT(timeConv(cursorT))+' '+timeUnitStr();
|
|
1085
|
+
}else if(e.touches.length===2){_tchDragging=false;_tchPinch0=getTouchDist(e.touches);_tchPinchMid=getTouchMid(e.touches);}
|
|
1086
|
+
e.preventDefault();
|
|
1087
|
+
},{passive:false});
|
|
1088
|
+
mc.addEventListener('touchmove',function(e){
|
|
1089
|
+
if(e.touches.length===1&&_tchDragging){_tchMoved=true;ox=_tchStartO-(e.touches[0].clientX-_tchStartX)/sc;clampView();
|
|
1090
|
+
var p=getTouchPos(e.touches[0],mc);cursorT=x2t(p.x);cursorActive=p.x>=M.l&&p.x<=mc.width/(window.devicePixelRatio||1)-M.r&&p.y>=M.t&&p.y<=mc.height/(window.devicePixelRatio||1)-M.b;
|
|
1091
|
+
curEl.textContent=fmtT(timeConv(cursorT))+' '+timeUnitStr();scheduleViewerDraw(true);
|
|
1092
|
+
}else if(e.touches.length===2){_tchDragging=false;
|
|
1093
|
+
var d=getTouchDist(e.touches),zf=d/Math.max(_tchPinch0,1);
|
|
1094
|
+
var mid=getTouchMid(e.touches),mx2=mid.x-mc.getBoundingClientRect().left;
|
|
1095
|
+
if(zoomAt(mx2,zf)){_tchPinch0=d;scheduleViewerDraw(true);}
|
|
1096
|
+
}
|
|
1097
|
+
e.preventDefault();
|
|
1098
|
+
},{passive:false});
|
|
1099
|
+
mc.addEventListener('touchend',function(e){
|
|
1100
|
+
if(!_tchMoved&&cursorActive&&e.changedTouches.length===1){showTooltipAt(e.changedTouches[0].clientX,e.changedTouches[0].clientY,cursorT);}
|
|
1101
|
+
_tchDragging=false;cc.classList.remove('pan');drawMinimap();
|
|
1102
|
+
if(e.touches.length===0){_tchPinch0=0;_tchPinchMid=null;}
|
|
1103
|
+
});
|
|
1104
|
+
mc.addEventListener('touchcancel',function(){_tchDragging=false;cc.classList.remove('pan');_tchPinch0=0;_tchPinchMid=null;});
|
|
1105
|
+
|
|
1106
|
+
// Touch utilities
|
|
1107
|
+
function isTouchDevice(){return('ontouchstart' in window)||(navigator.maxTouchPoints>0);}
|
|
1108
|
+
function getTouchPos(touch,el){var r=el.getBoundingClientRect();return {x:touch.clientX-r.left, y:touch.clientY-r.top};}
|
|
1109
|
+
function getTouchMid(touches){return {x:(touches[0].clientX+touches[1].clientX)/2, y:(touches[0].clientY+touches[1].clientY)/2};}
|
|
1110
|
+
function getTouchDist(touches){var dx=touches[0].clientX-touches[1].clientX, dy=touches[0].clientY-touches[1].clientY;return Math.sqrt(dx*dx+dy*dy);}
|
|
1111
|
+
|
|
1112
|
+
// ── Derived‑value tooltip helpers (matching interaction.js) ──
|
|
1113
|
+
function fmtDerivedValue(v,unit){return unit==='%'?v.toFixed(1)+'%':fmtSignedAmp(v)+' '+unit;}
|
|
1114
|
+
function pushDerivedPart(parts,label,series,t,unit){
|
|
1115
|
+
var v=sampleDerivedSeriesAtTime(series,t);if(v===null)return;
|
|
1116
|
+
parts.push(label+'='+fmtDerivedValue(v,unit));
|
|
1117
|
+
}
|
|
1118
|
+
function appendDerivedTooltipLines(lines,ct){
|
|
1119
|
+
if(pnsData&&chVis[7]){
|
|
1120
|
+
var p=[];pushDerivedPart(p,'X',pnsData.x,ct,'%');pushDerivedPart(p,'Y',pnsData.y,ct,'%');pushDerivedPart(p,'Z',pnsData.z,ct,'%');pushDerivedPart(p,'Norm',pnsData.n,ct,'%');
|
|
1121
|
+
if(p.length)lines.push('PNS: '+p.join(' '));
|
|
1122
|
+
}
|
|
1123
|
+
if(m1Data&&(chVis[8]||chVis[9]||chVis[10])){
|
|
1124
|
+
var m=[];if(chVis[8])pushDerivedPart(m,'M1x',m1Data.x,ct,'s/m');if(chVis[9])pushDerivedPart(m,'M1y',m1Data.y,ct,'s/m');if(chVis[10])pushDerivedPart(m,'M1z',m1Data.z,ct,'s/m');
|
|
1125
|
+
if(m.length)lines.push('M1: '+m.join(' '));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Tooltip
|
|
1130
|
+
function showTooltipAt(cx,cy,ct){
|
|
1131
|
+
var ch=cH(),vc=visChannels(),vi2=Math.floor((cy-mc.getBoundingClientRect().top-M.t)/ch);
|
|
1132
|
+
if(vi2>=0&&vi2<vc.length){
|
|
1133
|
+
var found=null;for(var i=0;i<BL.length;i++){if(ct>=BL[i].s&&ct<=BL[i].s+BL[i].d){found=BL[i];break}}
|
|
1134
|
+
var lines=[];
|
|
1135
|
+
if(found){
|
|
1136
|
+
var blockDt=Math.max(0,Math.min(found.d,ct-found.s));
|
|
1137
|
+
lines=['Block #'+found.i,'Time: '+fmtT(timeConv(ct))+' '+timeUnitStr()+' (Δ '+fmtT(timeConv(blockDt))+' / dur: '+fmtT(timeConv(found.d))+' '+timeUnitStr()+')'];
|
|
1138
|
+
if(found.rf){lines.push('RF: '+(found.rf.a||0).toFixed(1)+' Hz fo='+(found.rf.fo||0).toFixed(0)+' Hz φ₀='+((found.rf.po||0)%6.283).toFixed(2)+' rad');}
|
|
1139
|
+
if(found.gx&&found.gx.ty!=='none')lines.push('Gx: '+fmtG(found.gx,ct));
|
|
1140
|
+
if(found.gy&&found.gy.ty!=='none')lines.push('Gy: '+fmtG(found.gy,ct));
|
|
1141
|
+
if(found.gz&&found.gz.ty!=='none')lines.push('Gz: '+fmtG(found.gz,ct));
|
|
1142
|
+
if(found.adc){lines.push('ADC: '+found.adc.n+'pts @'+(found.adc.dw*1e6).toFixed(1)+'µs fo='+(found.adc.fo||0).toFixed(0)+' Hz φ₀='+((found.adc.po||0)%6.283).toFixed(2)+' rad');}
|
|
1143
|
+
if(found.trg)lines.push('Trig: ch'+found.trg.map(function(x){return x.c}).join(',')+' Δ'+found.trg.map(function(x){return fmtT(timeConv(x.dr))+' '+timeUnitStr()}).join(','));
|
|
1144
|
+
}else{lines=['Time: '+fmtT(timeConv(ct))+' '+timeUnitStr()];}
|
|
1145
|
+
appendDerivedTooltipLines(lines,ct);
|
|
1146
|
+
if(lines.length>1||found){tt.textContent=lines.join('\n');tt.style.display='block';placeTooltip(cx,cy);
|
|
1147
|
+
if(_touchTooltipTimer)clearTimeout(_touchTooltipTimer);
|
|
1148
|
+
if(isTouchDevice())_touchTooltipTimer=setTimeout(function(){tt.style.display='none';},3000);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
tt.style.display='none';
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function placeTooltip(cx,cy){
|
|
1156
|
+
var pad=8;tt.style.left='0px';tt.style.top='0px';
|
|
1157
|
+
var r=tt.getBoundingClientRect(),vw=window.innerWidth,vh=window.innerHeight;
|
|
1158
|
+
var left=Math.min(Math.max(pad,cx+15),Math.max(pad,vw-r.width-pad));
|
|
1159
|
+
var top=cy+12;if(top+r.height+pad>vh)top=cy-r.height-12;if(top<pad)top=pad;
|
|
1160
|
+
tt.style.left=left+'px';tt.style.top=top+'px';
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Toolbar buttons
|
|
1164
|
+
document.getElementById('openBtn').onclick=function(){
|
|
1165
|
+
var fi=document.getElementById('fileInput');if(fi)fi.click();
|
|
1166
|
+
};
|
|
1167
|
+
document.getElementById('exportKspaceBtn').onclick=function(){
|
|
1168
|
+
// Export k-space trajectory as text file
|
|
1169
|
+
if(!kAdc||!kAdcTime||!kAdc[0]||kAdc[0].length===0){alert('No k-space data to export.');return;}
|
|
1170
|
+
try{
|
|
1171
|
+
var lines=['# kx (1/m) ky (1/m) kz (1/m) time (s)'];
|
|
1172
|
+
for(var i=0;i<kAdc[0].length;i++){
|
|
1173
|
+
lines.push(kAdc[0][i]+'\t'+kAdc[1][i]+'\t'+kAdc[2][i]+'\t'+kAdcTime[i]);
|
|
1174
|
+
}
|
|
1175
|
+
var blob=new Blob([lines.join('\n')],{type:'text/plain'});
|
|
1176
|
+
var a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='ktraj_adc.txt';a.click();
|
|
1177
|
+
URL.revokeObjectURL(a.href);
|
|
1178
|
+
}catch(e){alert('Export failed: '+e.message);}
|
|
1179
|
+
};
|
|
1180
|
+
document.getElementById('pnsBtn').onclick=function(){
|
|
1181
|
+
// Open ASC file picker — PNS requires real scanner hardware data
|
|
1182
|
+
var ascInput=document.getElementById('ascFileInput');
|
|
1183
|
+
if(ascInput)ascInput.click();
|
|
1184
|
+
};
|
|
1185
|
+
document.getElementById('ascFileInput').onchange=function(){
|
|
1186
|
+
var file=this.files[0];if(!file)return;
|
|
1187
|
+
var reader=new FileReader();
|
|
1188
|
+
reader.onload=function(){
|
|
1189
|
+
try{
|
|
1190
|
+
if(typeof Pulseq==='undefined'||typeof Pulseq.parsePnsHardwareAsc!=='function'){
|
|
1191
|
+
alert('PNS ASC parser not available. Ensure pulseq-bundle.js is loaded.');
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
pnsHardware=Pulseq.parsePnsHardwareAsc(reader.result);
|
|
1195
|
+
if(!pnsHardware||!pnsHardware.valid){alert('Failed to parse PNS hardware from ASC file.');return;}
|
|
1196
|
+
// Update button label
|
|
1197
|
+
if(pnsBtn)pnsBtn.textContent='📄 ASC: '+file.name.replace(/\.asc$/i,'');
|
|
1198
|
+
pnsBtn.title='PNS hardware loaded from '+file.name+' — click to toggle PNS display';
|
|
1199
|
+
// Recompute PNS with the loaded hardware
|
|
1200
|
+
recomputePns();
|
|
1201
|
+
}catch(e){alert('Failed to parse ASC file: '+(e.message||e));}
|
|
1202
|
+
};
|
|
1203
|
+
reader.readAsText(file);
|
|
1204
|
+
};
|
|
1205
|
+
// ── Recompute PNS using current blocks and loaded hardware ──
|
|
1206
|
+
function recomputePns(){
|
|
1207
|
+
if(!pnsHardware||!pnsHardware.valid||!BL.length){alert('Load a sequence and an ASC file first.');return;}
|
|
1208
|
+
// We need the original decoded blocks — store them during loadSequenceText
|
|
1209
|
+
if(!_lastDecodedBlocks){alert('No decoded block data available. Reload the sequence.');return;}
|
|
1210
|
+
try{
|
|
1211
|
+
var pnsResult=Pulseq.calculatePns(_lastDecodedBlocks,GR,pnsHardware);
|
|
1212
|
+
if(pnsResult&&pnsResult.valid&&pnsResult.ok&&pnsResult.timeSec&&pnsResult.timeSec.length>0){
|
|
1213
|
+
pnsData={
|
|
1214
|
+
x:createDerivedSeries(Array.from(pnsResult.timeSec),Array.from(pnsResult.pnsX),100),
|
|
1215
|
+
y:createDerivedSeries(Array.from(pnsResult.timeSec),Array.from(pnsResult.pnsY),100),
|
|
1216
|
+
z:createDerivedSeries(Array.from(pnsResult.timeSec),Array.from(pnsResult.pnsZ),100),
|
|
1217
|
+
n:createDerivedSeries(Array.from(pnsResult.timeSec),Array.from(pnsResult.pnsNorm),100)
|
|
1218
|
+
};
|
|
1219
|
+
chVis[7]=true;computeGlobalMax();buildLegend();draw();
|
|
1220
|
+
}else{alert('PNS calculation: no valid result.');}
|
|
1221
|
+
}catch(e){alert('PNS calculation failed: '+(e.message||e));}
|
|
1222
|
+
}
|
|
1223
|
+
document.getElementById('zi').onclick=function(){if(zoomAtCenter(1.5))scheduleViewerDraw(true);};
|
|
1224
|
+
document.getElementById('zo').onclick=function(){if(zoomAtCenter(1/1.5))scheduleViewerDraw(true);};
|
|
1225
|
+
document.getElementById('zf').onclick=function(){fit();drawMinimap();};
|
|
1226
|
+
document.getElementById('zr').onclick=function(){fit();drawMinimap();};
|
|
1227
|
+
document.getElementById('bbc').onchange=function(){showBB=this.checked;draw();};
|
|
1228
|
+
|
|
1229
|
+
// Theme switcher
|
|
1230
|
+
document.getElementById('theme').onchange=function(){
|
|
1231
|
+
var v=this.value;
|
|
1232
|
+
document.body.className=document.body.className.replace(/theme-\w+/g,'');
|
|
1233
|
+
if(v&&v!=='system')document.body.classList.add('theme-'+v);
|
|
1234
|
+
mmCache=null;draw();drawKs();drawMinimap();
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
// File input for standalone
|
|
1238
|
+
document.getElementById('fileInput').onchange=function(){
|
|
1239
|
+
var file=this.files[0];if(!file)return;
|
|
1240
|
+
var reader=new FileReader();
|
|
1241
|
+
reader.onload=function(){loadSequenceText(reader.result);};
|
|
1242
|
+
reader.readAsText(file);
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
// ── Minimap ──
|
|
1246
|
+
var mmCanvas=document.getElementById('mmc'),mmCtx=mmCanvas.getContext('2d');
|
|
1247
|
+
var mmCache=null, mmCacheW=0, mmCacheDpr=0, mmDrag=false;
|
|
1248
|
+
|
|
1249
|
+
function parseHexRGB(c){if(!c||c[0]!=='#')return[128,128,128];var h=c.substring(1);if(h.length===3)h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2];return[parseInt(h.substring(0,2),16),parseInt(h.substring(2,4),16),parseInt(h.substring(4,6),16)];}
|
|
1250
|
+
function fillBand(d,W,x0,x1,y0,y1,r,g,b,a){
|
|
1251
|
+
for(var y=y0;y<y1;y++){for(var x=x0;x<x1;x++){var i=(y*W+x)*4;var srcA=a/255,dstA=d[i+3]/255;var outA=srcA+dstA*(1-srcA);if(outA<0.001)continue;d[i]=(r*srcA+d[i]*dstA*(1-srcA))/outA;d[i+1]=(g*srcA+d[i+1]*dstA*(1-srcA))/outA;d[i+2]=(b*srcA+d[i+2]*dstA*(1-srcA))/outA;d[i+3]=Math.round(outA*255);}}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function buildMinimapCache(){
|
|
1255
|
+
mmCache=null;if(!TD||TD<=0||!BL.length)return;
|
|
1256
|
+
var dpr=window.devicePixelRatio||1;var W=mmCanvas.width, H=mmCanvas.height;
|
|
1257
|
+
if(W<=0||H<=0)return;
|
|
1258
|
+
var s=getComputedStyle(document.body);var cssW=W/dpr, scM=cssW/TD;
|
|
1259
|
+
var rfRGB=parseHexRGB(s.getPropertyValue('--rf').trim());
|
|
1260
|
+
var gxRGB=parseHexRGB(s.getPropertyValue('--gx').trim());
|
|
1261
|
+
var gyRGB=parseHexRGB(s.getPropertyValue('--gy').trim());
|
|
1262
|
+
var gzRGB=parseHexRGB(s.getPropertyValue('--gz').trim());
|
|
1263
|
+
var adcRGB=parseHexRGB(s.getPropertyValue('--adc').trim());
|
|
1264
|
+
var bgRGB=parseHexRGB(s.getPropertyValue('--trbg').trim());
|
|
1265
|
+
var rfY0=0, rfY1=5*dpr;var gY0=5*dpr, gH=2*dpr;var aY0=11*dpr, aY1=H;
|
|
1266
|
+
var pw=Math.max(1,Math.ceil(cssW));
|
|
1267
|
+
var rfCnt=new Uint8Array(pw), adcCnt=new Uint8Array(pw);
|
|
1268
|
+
var gxCnt=new Uint8Array(pw), gyCnt=new Uint8Array(pw), gzCnt=new Uint8Array(pw);
|
|
1269
|
+
var stride=BL.length>20000?Math.floor(BL.length/15000):1;
|
|
1270
|
+
for(var i=0;i<BL.length;i+=stride){var b=BL[i];var px0=Math.floor(b.s*scM), px1=Math.ceil((b.s+b.d)*scM);if(px0>=pw)continue;px0=Math.max(0,px0);px1=Math.min(pw,px1);
|
|
1271
|
+
if(b.rf) for(var p=px0;p<px1;p++){if(rfCnt[p]<255)rfCnt[p]++;}
|
|
1272
|
+
if(b.gx&&b.gx.ty!=='none') for(var p=px0;p<px1;p++){if(gxCnt[p]<255)gxCnt[p]++;}
|
|
1273
|
+
if(b.gy&&b.gy.ty!=='none') for(var p=px0;p<px1;p++){if(gyCnt[p]<255)gyCnt[p]++;}
|
|
1274
|
+
if(b.gz&&b.gz.ty!=='none') for(var p=px0;p<px1;p++){if(gzCnt[p]<255)gzCnt[p]++;}
|
|
1275
|
+
if(b.adc) for(var p=px0;p<px1;p++){if(adcCnt[p]<255)adcCnt[p]++;}
|
|
1276
|
+
}
|
|
1277
|
+
var imgData=new ImageData(W,H);var d=imgData.data;
|
|
1278
|
+
for(var y=0;y<H;y++){for(var x=0;x<W;x++){var idx=(y*W+x)*4;d[idx]=bgRGB[0];d[idx+1]=bgRGB[1];d[idx+2]=bgRGB[2];d[idx+3]=255;}}
|
|
1279
|
+
for(var px=0;px<pw;px++){var x0=Math.floor(px*dpr), x1=Math.floor((px+1)*dpr);if(x1<=x0)x1=x0+1;
|
|
1280
|
+
if(rfCnt[px]>0){var a=Math.min(140,rfCnt[px]*55);fillBand(d,W,x0,x1,rfY0,rfY1,rfRGB[0],rfRGB[1],rfRGB[2],a);}
|
|
1281
|
+
if(gxCnt[px]>0){var a=Math.min(150,gxCnt[px]*60);fillBand(d,W,x0,x1,gY0,gY0+gH,gxRGB[0],gxRGB[1],gxRGB[2],a);}
|
|
1282
|
+
if(gyCnt[px]>0){var a=Math.min(150,gyCnt[px]*60);fillBand(d,W,x0,x1,gY0+gH,gY0+2*gH,gyRGB[0],gyRGB[1],gyRGB[2],a);}
|
|
1283
|
+
if(gzCnt[px]>0){var a=Math.min(150,gzCnt[px]*60);fillBand(d,W,x0,x1,gY0+2*gH,gY0+3*gH,gzRGB[0],gzRGB[1],gzRGB[2],a);}
|
|
1284
|
+
if(adcCnt[px]>0){var a=Math.min(130,adcCnt[px]*50);fillBand(d,W,x0,x1,aY0,aY1,adcRGB[0],adcRGB[1],adcRGB[2],a);}
|
|
1285
|
+
}
|
|
1286
|
+
var oc=document.createElement('canvas');oc.width=W;oc.height=H;oc.getContext('2d').putImageData(imgData,0,0);
|
|
1287
|
+
mmCache=oc;mmCacheW=W;mmCacheDpr=dpr;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function fmtDur2(s){if(s>=1)return s.toFixed(2)+'s';if(s>=0.001)return(s*1e3).toFixed(1)+'ms';return(s*1e6).toFixed(0)+'µs';}
|
|
1291
|
+
|
|
1292
|
+
function drawMinimap(){
|
|
1293
|
+
var dpr=window.devicePixelRatio||1;var W=mmCanvas.width/dpr, H=mmCanvas.height/dpr;
|
|
1294
|
+
mmCtx.globalAlpha=1;mmCtx.clearRect(0,0,W,H);
|
|
1295
|
+
if(!TD||TD<=0)return;
|
|
1296
|
+
var s=getComputedStyle(document.body);
|
|
1297
|
+
if(!mmCache||mmCacheW!==mmCanvas.width||mmCacheDpr!==dpr){buildMinimapCache();}
|
|
1298
|
+
if(mmCache){mmCtx.drawImage(mmCache,0,0);}else{mmCtx.fillStyle=s.getPropertyValue('--trbg').trim();mmCtx.fillRect(0,0,W,H);}
|
|
1299
|
+
if(!BL.length)return;
|
|
1300
|
+
var scM=W/TD;
|
|
1301
|
+
if(seqTiming&&seqTiming.trTimeSec>0&&seqTiming.trCount>1){mmCtx.strokeStyle=s.getPropertyValue('--fg').trim();mmCtx.globalAlpha=0.10;mmCtx.lineWidth=0.4;mmCtx.beginPath();for(var tr=0;tr<=seqTiming.trCount;tr++){var tx=tr*seqTiming.trTimeSec*scM;if(tx<=W){mmCtx.moveTo(tx,0);mmCtx.lineTo(tx,H);}}mmCtx.stroke();mmCtx.globalAlpha=1;}
|
|
1302
|
+
var mcW=mc.width/dpr;var visibleDur=visibleDuration();var vx0=ox*scM, vx1=(ox+visibleDur)*scM;vx0=Math.max(0,vx0);vx1=Math.min(W,vx1);var vw=Math.max(2,vx1-vx0);
|
|
1303
|
+
mmCtx.fillStyle=s.getPropertyValue('--fg').trim();mmCtx.globalAlpha=0.18;mmCtx.fillRect(vx0,0,vw,H);
|
|
1304
|
+
mmCtx.globalAlpha=0.45;mmCtx.fillStyle=s.getPropertyValue('--adc').trim();if(vx0>0.5)mmCtx.fillRect(vx0,0,1,H);if(vx1<W-0.5)mmCtx.fillRect(vx1-1,0,1,H);mmCtx.globalAlpha=1;
|
|
1305
|
+
var info='';if(seqTiming){if(seqTiming.trTimeSec>0)info+='TR='+fmtDur2(seqTiming.trTimeSec)+(seqTiming.hasExplicitTR?'':'~')+' ';if(seqTiming.teTimeSec>0)info+='TE='+fmtDur2(seqTiming.teTimeSec)+' ';if(seqTiming.trCount>1)info+=seqTiming.trCount+' TRs';}
|
|
1306
|
+
if(info){mmCtx.fillStyle='rgba(0,0,0,0.35)';mmCtx.font='9px monospace';mmCtx.textAlign='left';mmCtx.fillText(info,5,H-3);mmCtx.fillStyle=s.getPropertyValue('--fg').trim();mmCtx.globalAlpha=0.9;mmCtx.fillText(info,4,H-4);mmCtx.globalAlpha=1;}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function scrollMinimapToMouse(e){var r=mmCanvas.getBoundingClientRect(),dpr=window.devicePixelRatio||1;var mx=e.clientX-r.left,W=mmCanvas.width/dpr;var frac=Math.max(0,Math.min(1,mx/W));var vsWidth=visibleDuration();var viewCenter=frac*TD;ox=viewCenter-vsWidth/2;clampView();draw();drawMinimap();}
|
|
1310
|
+
|
|
1311
|
+
mmCanvas.addEventListener('mousedown',function(e){mmDrag=true;scrollMinimapToMouse(e);e.preventDefault();});
|
|
1312
|
+
window.addEventListener('mousemove',function(e){if(!mmDrag)return;scrollMinimapToMouse(e);});
|
|
1313
|
+
window.addEventListener('mouseup',function(){mmDrag=false;});
|
|
1314
|
+
mmCanvas.addEventListener('wheel',function(e){e.preventDefault();var zf=e.deltaY<0?1.3:1/1.3;if(zoomAtCenter(zf))scheduleViewerDraw(true);},{passive:false});
|
|
1315
|
+
|
|
1316
|
+
// ── Theme observer ──
|
|
1317
|
+
new MutationObserver(function(){mmCache=null;draw();drawKs();drawMinimap();}).observe(document.body,{attributes:true,attributeFilter:['class']});
|
|
1318
|
+
|
|
1319
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
1320
|
+
Data Loading — Bridge between Python host and viewer
|
|
1321
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Load raw .seq text, parse it, decode blocks, compute k‑space, and render.
|
|
1325
|
+
* Called either:
|
|
1326
|
+
* - automatically on page load if window.SEQEYES_RAW_B64 is set
|
|
1327
|
+
* - when the user opens a file via the 📂 button
|
|
1328
|
+
*/
|
|
1329
|
+
|
|
1330
|
+
/** Convert a decoded block from Pulseq bundle format to the abbreviated
|
|
1331
|
+
* format expected by the rendering engine (matching VS Code postMessage). */
|
|
1332
|
+
function convertBlock(blk) {
|
|
1333
|
+
var b = {};
|
|
1334
|
+
b.i = blk.index || 0;
|
|
1335
|
+
b.s = blk.startTime || 0;
|
|
1336
|
+
b.d = blk.duration || 0;
|
|
1337
|
+
|
|
1338
|
+
// RF — with phase wrapping matching serializeBlocks in seqEditorProvider.ts
|
|
1339
|
+
if (blk.rf) {
|
|
1340
|
+
var rawP = blk.rf.phase || [];
|
|
1341
|
+
// Wrap phase to [0, 2π) — identical to the VS Code extension
|
|
1342
|
+
var wrappedP = rawP.length ? rawP.map(function(v) { return ((v % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI); }) : [];
|
|
1343
|
+
b.rf = {
|
|
1344
|
+
s: blk.rf.startTime || 0,
|
|
1345
|
+
d: blk.rf.duration || 0,
|
|
1346
|
+
a: blk.rf.amplitude || 0,
|
|
1347
|
+
fo: blk.rf.freqOffset || 0,
|
|
1348
|
+
po: ((blk.rf.phaseOffset || 0) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI),
|
|
1349
|
+
t: blk.rf.timePoints || [],
|
|
1350
|
+
m: blk.rf.magnitude || [],
|
|
1351
|
+
p: wrappedP,
|
|
1352
|
+
u: blk.rf.use || 'u' // 'e'=excitation, 'r'=refocusing, 'i'=inversion, 's'=saturation, 'u'=undefined
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Gradients
|
|
1357
|
+
['gx','gy','gz'].forEach(function(ch) {
|
|
1358
|
+
var g = blk[ch];
|
|
1359
|
+
if (g && g.type && g.type !== 'none') {
|
|
1360
|
+
b[ch] = {
|
|
1361
|
+
ty: g.type,
|
|
1362
|
+
t: g.timePoints || [],
|
|
1363
|
+
w: g.waveform || [],
|
|
1364
|
+
a: g.amplitude || 0
|
|
1365
|
+
};
|
|
1366
|
+
} else {
|
|
1367
|
+
b[ch] = { ty: 'none', t: [], w: [], a: 0 };
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
// ADC
|
|
1372
|
+
if (blk.adc) {
|
|
1373
|
+
b.adc = {
|
|
1374
|
+
s: (blk.adc.startTime || 0) + (blk.adc.delay || 0),
|
|
1375
|
+
d: blk.adc.delay || 0,
|
|
1376
|
+
n: blk.adc.numSamples || 0,
|
|
1377
|
+
dw: blk.adc.dwell || 0,
|
|
1378
|
+
fo: blk.adc.freqOffset || 0,
|
|
1379
|
+
po: blk.adc.phaseOffset || 0
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Triggers
|
|
1384
|
+
if (blk.triggers && blk.triggers.length) {
|
|
1385
|
+
b.trg = blk.triggers.map(function(t) {
|
|
1386
|
+
return {
|
|
1387
|
+
s: t.startTime || 0,
|
|
1388
|
+
d: t.delay || 0,
|
|
1389
|
+
dr: t.duration || 0,
|
|
1390
|
+
c: t.channel || 0
|
|
1391
|
+
};
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
return b;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function loadSequenceText(rawText) {
|
|
1399
|
+
if (!rawText || typeof rawText !== 'string') return;
|
|
1400
|
+
if (typeof Pulseq === 'undefined' || typeof Pulseq.parseSequenceText !== 'function') {
|
|
1401
|
+
console.error('[SeqEyes] Pulseq bundle not loaded — cannot parse .seq text.');
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Show progress
|
|
1406
|
+
var overlay = document.getElementById('poverlay');
|
|
1407
|
+
var fill = document.getElementById('pfill2');
|
|
1408
|
+
var textEl = document.getElementById('ptext');
|
|
1409
|
+
var pct = document.getElementById('ppct');
|
|
1410
|
+
overlay.style.display = 'flex';
|
|
1411
|
+
fill.style.width = '0%';
|
|
1412
|
+
textEl.textContent = 'Parsing sequence…';
|
|
1413
|
+
pct.textContent = '0%';
|
|
1414
|
+
|
|
1415
|
+
// Use requestAnimationFrame to let the UI update before heavy work
|
|
1416
|
+
requestAnimationFrame(function() {
|
|
1417
|
+
try {
|
|
1418
|
+
fill.style.width = '20%'; pct.textContent = '20%';
|
|
1419
|
+
|
|
1420
|
+
// Parse
|
|
1421
|
+
var seq = Pulseq.parseSequenceText(rawText);
|
|
1422
|
+
fill.style.width = '40%'; pct.textContent = '40%';
|
|
1423
|
+
|
|
1424
|
+
// Detect timing
|
|
1425
|
+
if (typeof Pulseq.detectSequenceTiming === 'function') {
|
|
1426
|
+
try { seqTiming = Pulseq.detectSequenceTiming(seq); } catch(e) { seqTiming = null; }
|
|
1427
|
+
}
|
|
1428
|
+
fill.style.width = '50%'; pct.textContent = '50%';
|
|
1429
|
+
|
|
1430
|
+
// Decode blocks
|
|
1431
|
+
var decodedBlocks = Pulseq.decodeAllBlocks(seq);
|
|
1432
|
+
_lastDecodedBlocks = decodedBlocks; // Keep for PNS/M1 recomputation
|
|
1433
|
+
fill.style.width = '70%'; pct.textContent = '70%';
|
|
1434
|
+
|
|
1435
|
+
// Convert to abbreviated format expected by the rendering engine
|
|
1436
|
+
BL = [];
|
|
1437
|
+
TD = 0;
|
|
1438
|
+
for (var i = 0; i < decodedBlocks.length; i++) {
|
|
1439
|
+
var b = convertBlock(decodedBlocks[i]);
|
|
1440
|
+
BL.push(b);
|
|
1441
|
+
TD = Math.max(TD, b.s + b.d);
|
|
1442
|
+
}
|
|
1443
|
+
GR = (seq.rasterTimes && seq.rasterTimes.gradientRaster) || 1e-5;
|
|
1444
|
+
RR = (seq.rasterTimes && seq.rasterTimes.rfRaster) || 1e-6;
|
|
1445
|
+
AR = (seq.rasterTimes && seq.rasterTimes.adcRaster) || 1e-7;
|
|
1446
|
+
BR = (seq.rasterTimes && seq.rasterTimes.blockDurationRaster) || 1e-5;
|
|
1447
|
+
|
|
1448
|
+
// Block positions for minimap
|
|
1449
|
+
blockPos = [];
|
|
1450
|
+
for (var i = 0; i < BL.length; i++) {
|
|
1451
|
+
blockPos.push({ i: BL[i].i, s: BL[i].s, d: BL[i].d });
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Reset k-space, PNS, M1
|
|
1455
|
+
kTraj = null; kAdc = null; kTime = null; kAdcTime = null;
|
|
1456
|
+
m1Data = null; pnsData = null; pnsWindowData = null; pnsWindowPending = null;
|
|
1457
|
+
chVis[7] = false; chVis[8] = false; chVis[9] = false; chVis[10] = false;
|
|
1458
|
+
mmCache = null;
|
|
1459
|
+
|
|
1460
|
+
// Compute k-space (pass the converted BL array — it has .s, .d props;
|
|
1461
|
+
// but Pulseq.calculateKspace needs the ORIGINAL decoded blocks with .startTime etc.)
|
|
1462
|
+
try {
|
|
1463
|
+
if (typeof Pulseq.calculateKspace === 'function') {
|
|
1464
|
+
var ks = Pulseq.calculateKspace(decodedBlocks, GR, TD);
|
|
1465
|
+
if (ks && ks.ktraj_adc && ks.ktraj_adc.length >= 3) {
|
|
1466
|
+
kAdc = [ks.ktraj_adc[0], ks.ktraj_adc[1], ks.ktraj_adc[2]];
|
|
1467
|
+
kAdcTime = ks.t_adc;
|
|
1468
|
+
kTraj = [ks.ktraj[0], ks.ktraj[1], ks.ktraj[2]];
|
|
1469
|
+
kTime = ks.t_ktraj;
|
|
1470
|
+
// Upload to GPU for WebGL rendering
|
|
1471
|
+
if (typeof uploadKSpaceGPU === 'function') uploadKSpaceGPU();
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
} catch(e) { console.warn('[SeqEyes] K-space calculation failed:', e); }
|
|
1475
|
+
|
|
1476
|
+
// ── Compute PNS if hardware is available ──
|
|
1477
|
+
if (pnsHardware && pnsHardware.valid) {
|
|
1478
|
+
try {
|
|
1479
|
+
if (typeof Pulseq.calculatePns === 'function') {
|
|
1480
|
+
var pnsResult = Pulseq.calculatePns(decodedBlocks, GR, pnsHardware);
|
|
1481
|
+
if (pnsResult && pnsResult.valid && pnsResult.ok && pnsResult.timeSec && pnsResult.timeSec.length > 0) {
|
|
1482
|
+
pnsData = {
|
|
1483
|
+
x: createDerivedSeries(Array.from(pnsResult.timeSec), Array.from(pnsResult.pnsX), 100),
|
|
1484
|
+
y: createDerivedSeries(Array.from(pnsResult.timeSec), Array.from(pnsResult.pnsY), 100),
|
|
1485
|
+
z: createDerivedSeries(Array.from(pnsResult.timeSec), Array.from(pnsResult.pnsZ), 100),
|
|
1486
|
+
n: createDerivedSeries(Array.from(pnsResult.timeSec), Array.from(pnsResult.pnsNorm), 100)
|
|
1487
|
+
};
|
|
1488
|
+
chVis[7] = true;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
} catch(e) { console.warn('[SeqEyes] PNS calculation failed:', e); }
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// ── Compute M1 (first gradient moment) ──
|
|
1495
|
+
try {
|
|
1496
|
+
if (typeof Pulseq.calculateM1 === 'function') {
|
|
1497
|
+
var m1Result = Pulseq.calculateM1(decodedBlocks, GR, { referenceMode: 'rfCenter' });
|
|
1498
|
+
if (m1Result && m1Result.valid && m1Result.ok && m1Result.tSec && m1Result.tSec.length > 0) {
|
|
1499
|
+
m1Data = {
|
|
1500
|
+
referenceMode: m1Result.referenceMode || 'rfCenter',
|
|
1501
|
+
x: createDerivedSeries(Array.from(m1Result.tSec), Array.from(m1Result.m1x), 1),
|
|
1502
|
+
y: createDerivedSeries(Array.from(m1Result.tSec), Array.from(m1Result.m1y), 1),
|
|
1503
|
+
z: createDerivedSeries(Array.from(m1Result.tSec), Array.from(m1Result.m1z), 1)
|
|
1504
|
+
};
|
|
1505
|
+
// Show M1x by default when available
|
|
1506
|
+
chVis[8] = true;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
} catch(e) { console.warn('[SeqEyes] M1 calculation failed:', e); }
|
|
1510
|
+
|
|
1511
|
+
fill.style.width = '90%'; pct.textContent = '90%';
|
|
1512
|
+
|
|
1513
|
+
computeGlobalMax();
|
|
1514
|
+
buildLegend();
|
|
1515
|
+
|
|
1516
|
+
// Auto-zoom
|
|
1517
|
+
if (seqTiming && seqTiming.trTimeSec > 0) { fitToFirstTR(); } else { fit(); }
|
|
1518
|
+
|
|
1519
|
+
// ── Apply display options from Python host ──
|
|
1520
|
+
if (window.SEQEYES_SHOW_BLOCKS) { showBB = true; document.getElementById('bbc').checked = true; }
|
|
1521
|
+
if (window.SEQEYES_TIME_DISP) { timeUnit = window.SEQEYES_TIME_DISP; tuSel.value = timeUnit; }
|
|
1522
|
+
if (window.SEQEYES_GRAD_DISP) { gradUnit = window.SEQEYES_GRAD_DISP; guSel.value = gradUnit; }
|
|
1523
|
+
// Apply time_range if specified
|
|
1524
|
+
if (window.SEQEYES_TIME_RANGE && window.SEQEYES_TIME_RANGE.length === 2) {
|
|
1525
|
+
var tr0 = window.SEQEYES_TIME_RANGE[0], tr1 = window.SEQEYES_TIME_RANGE[1];
|
|
1526
|
+
if (isFinite(tr0) && isFinite(tr1) && tr1 > tr0) {
|
|
1527
|
+
var w2 = mc.width / (window.devicePixelRatio || 1);
|
|
1528
|
+
sc = (w2 - M.l - M.r) / (tr1 - tr0);
|
|
1529
|
+
ox = tr0;
|
|
1530
|
+
clampView();
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
draw();
|
|
1535
|
+
drawKs();
|
|
1536
|
+
drawMinimap();
|
|
1537
|
+
setExportButtonEnabled(true);
|
|
1538
|
+
fill.style.width = '100%';
|
|
1539
|
+
pct.textContent = '100%';
|
|
1540
|
+
textEl.textContent = 'Ready';
|
|
1541
|
+
|
|
1542
|
+
setTimeout(function() { overlay.style.display = 'none'; }, 400);
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
console.error('[SeqEyes] Failed to load sequence:', err);
|
|
1545
|
+
textEl.textContent = 'Error: ' + (err.message || 'Unknown error');
|
|
1546
|
+
fill.style.width = '100%';
|
|
1547
|
+
pct.textContent = 'Error';
|
|
1548
|
+
setTimeout(function() { overlay.style.display = 'none'; }, 2000);
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/* SEQEYES_DATA_PLACEHOLDER */
|
|
1554
|
+
|
|
1555
|
+
// ── Auto‑load on page start ──
|
|
1556
|
+
// Check for data injected by the Python host via base64
|
|
1557
|
+
if (window.SEQEYES_RAW_B64) {
|
|
1558
|
+
try {
|
|
1559
|
+
var rawText = atob(window.SEQEYES_RAW_B64);
|
|
1560
|
+
loadSequenceText(rawText);
|
|
1561
|
+
} catch(e) {
|
|
1562
|
+
console.error('[SeqEyes] Failed to decode base64 sequence data:', e);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// ── Drag & drop support ──
|
|
1567
|
+
document.addEventListener('dragover', function(e) { e.preventDefault(); });
|
|
1568
|
+
document.addEventListener('drop', function(e) {
|
|
1569
|
+
e.preventDefault();
|
|
1570
|
+
var file = e.dataTransfer.files[0];
|
|
1571
|
+
if (!file || !file.name.endsWith('.seq')) return;
|
|
1572
|
+
var reader = new FileReader();
|
|
1573
|
+
reader.onload = function() { loadSequenceText(reader.result); };
|
|
1574
|
+
reader.readAsText(file);
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
// ── Defer initial render until after browser layout is complete ──
|
|
1578
|
+
// Without this, the canvas has 0×0 dimensions on first call and nothing draws.
|
|
1579
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
1580
|
+
requestAnimationFrame(function() {
|
|
1581
|
+
requestAnimationFrame(function() { rs(); });
|
|
1582
|
+
});
|
|
1583
|
+
} else {
|
|
1584
|
+
setTimeout(function() { rs(); }, 100);
|
|
1585
|
+
}
|
|
1586
|
+
})();
|
|
1587
|
+
</script>
|
|
1588
|
+
</body>
|
|
1589
|
+
</html>
|