seqeyes-python 0.2.9__py3-none-any.whl → 0.2.10__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.
@@ -0,0 +1,2667 @@
1
+ /* SeqEyes WebView Bundle — auto-generated, do not edit */
2
+ "use strict";
3
+ /* ══ state.js ══ */
4
+ /* ═══════════════════════════════════════════════════════════════════════
5
+ Application state & configuration
6
+ ═══════════════════════════════════════════════════════════════════════ */
7
+ var cc=document.getElementById('cc'),mc=document.getElementById('mc'),ctx=mc.getContext('2d'),
8
+ moc=document.getElementById('moc'),moctx=moc.getContext('2d'),
9
+ tt=document.getElementById('tt'),curEl=document.getElementById('cur'),legend=document.getElementById('legend'),
10
+ tuSel=document.getElementById('tu'),guSel=document.getElementById('gu');
11
+ var exportBtn=document.getElementById('exportKspaceBtn');
12
+ var pnsBtn=document.getElementById('pnsBtn');
13
+ var BL=[],waveformOverview=null,TD=0,GR=1e-5,RR=1e-6,AR=1e-7,BR=1e-5; // blocks, duration, rasters [s]
14
+ var M={t:8,r:30,b:22,l:92}; // margins
15
+ var CH=['RF','\u03c6','Gx','Gy','Gz','ADC','Trig','PNS','M1x','M1y','M1z'];
16
+ var chColors=['var(--rf)','var(--rf)','var(--gx)','var(--gy)','var(--gz)','var(--adc)','var(--tr)','var(--fg)','var(--gx)','var(--gy)','var(--gz)'];
17
+ var chVis=[true,true,true,true,true,true,true,false,false,false,false]; // visibility toggles
18
+ var gMax=[1,6.28318,1,1,1,0,0,1,0.001,0.001,0.001]; // global max per channel
19
+ var ox=0,sc=1; // view offset [s] & scale [px/s]
20
+ var dr=false,dsx=0,dso=0; // drag state
21
+ var cursorT=0,cursorActive=false; // mouse time position
22
+ var timeUnit='ms',gradUnit='Hz/m'; // display unit selections
23
+ var showBB=false; // show block boundaries (default off)
24
+ var GAMMA=42576; // Hz/m per mT/m for 1H
25
+
26
+ var ampZoom=[1,1,1,1,1,1,1];
27
+ ampZoom[7]=1;ampZoom[8]=1;ampZoom[9]=1;ampZoom[10]=1;
28
+ /* K‑space data — pre‑computed on the extension side */
29
+ var kTraj=null,kAdc=null,kTime=null,kAdcTime=null;
30
+ var m1Data=null,m1WindowData=null,m1WindowPending=null,m1WindowRequestId=0,pnsData=null,pnsWindowData=null,pnsWindowPending=null,pnsWindowRequestId=0,pnsBusy=false,m1Busy=false,m1RequestedChannel=8,m1ReferenceMode=readM1ReferenceMode(),m1RestoreChannels=null;
31
+ var viewerNotices={};
32
+ var kspaceSafetyWarning=null,kspaceSafetyBusy=false,kspaceSafetyPopupTimer=0;
33
+ var derivedRenderPointCount=0,derivedEnvelopeCurveCount=0,derivedRawCurveCount=0,waveformOverviewActive=false,rfRenderPointCount=0,rfRawCurveCount=0,rfReducedCurveCount=0,rfOverviewBucketCount=0,lastDrawDurationMs=0,viewerDrawCount=0,viewerCursorDrawCount=0;
34
+ var viewerDrawFrame=0,viewerDrawMinimap=false;
35
+ function isMobileSafetyLayout(){return !!(window.matchMedia&&window.matchMedia('(max-width: 768px), (pointer: coarse)').matches);}
36
+ function renderViewerNotices(){
37
+ var el=document.getElementById('viewerNotice');if(!el)return;el.textContent='';
38
+ var keys=Object.keys(viewerNotices),visible=0;
39
+ for(var i=0;i<keys.length;i++){
40
+ if(keys[i]==='kspace'&&kspaceSafetyWarning&&isMobileSafetyLayout())continue;
41
+ if(visible)el.appendChild(document.createTextNode(' '));
42
+ var span=document.createElement('span');span.textContent=viewerNotices[keys[i]];el.appendChild(span);visible++;
43
+ }
44
+ if(kspaceSafetyWarning&&!isMobileSafetyLayout()){
45
+ var action=document.createElement('button');action.type='button';action.className='notice-action';action.textContent=kspaceSafetyBusy?'Calculating…':'Calculate anyway…';action.disabled=kspaceSafetyBusy;
46
+ action.onclick=showKspaceSafetyDialog;el.appendChild(action);visible++;
47
+ }
48
+ el.style.display=visible?'block':'none';
49
+ }
50
+ function setViewerNotice(key,message){
51
+ if(message&&viewerNotices[key]===message)return;
52
+ if(!message&&!Object.prototype.hasOwnProperty.call(viewerNotices,key))return;
53
+ if(message)viewerNotices[key]=message;else delete viewerNotices[key];
54
+ renderViewerNotices();
55
+ }
56
+ function setKspaceSafetyWarning(message){
57
+ clearTimeout(kspaceSafetyPopupTimer);kspaceSafetyWarning=message||null;kspaceSafetyBusy=false;
58
+ setViewerNotice('kspace',message?(message+' Calculating anyway may freeze or crash SeqEyes or its host, exhaust system memory, and cause unsaved work to be lost.'):null);
59
+ if(kspaceSafetyWarning&&isMobileSafetyLayout())kspaceSafetyPopupTimer=setTimeout(showKspaceSafetyDialog,650);
60
+ if(!kspaceSafetyWarning)hideKspaceSafetyDialog();
61
+ }
62
+ function showKspaceSafetyDialog(){
63
+ if(!kspaceSafetyWarning)return false;
64
+ var overlay=document.getElementById('kspaceSafetyOverlay'),text=document.getElementById('kspaceSafetyText'),proceed=document.getElementById('kspaceSafetyProceed');
65
+ if(!overlay||!text||!proceed)return false;
66
+ text.textContent=kspaceSafetyWarning+'\n\nCalculating anyway may freeze or crash SeqEyes or its host, exhaust system memory, and cause unsaved work to be lost. Continue only if you accept this risk.';
67
+ proceed.disabled=kspaceSafetyBusy;proceed.textContent=kspaceSafetyBusy?'Calculating…':'Calculate anyway (dangerous)';
68
+ overlay.style.display='flex';overlay.setAttribute('aria-hidden','false');
69
+ var acknowledge=document.getElementById('kspaceSafetyAcknowledge');if(acknowledge)acknowledge.focus();return true;
70
+ }
71
+ function hideKspaceSafetyDialog(){
72
+ var overlay=document.getElementById('kspaceSafetyOverlay');if(!overlay)return;overlay.style.display='none';overlay.setAttribute('aria-hidden','true');
73
+ }
74
+ function requestDangerousKspaceCalculation(){
75
+ if(!kspaceSafetyWarning||kspaceSafetyBusy)return;kspaceSafetyBusy=true;hideKspaceSafetyDialog();renderViewerNotices();
76
+ if(vscApi)vscApi.postMessage({command:'calculateKspaceUnsafe'});
77
+ }
78
+ function finishDangerousKspaceCalculation(error){
79
+ kspaceSafetyBusy=false;
80
+ if(!error){setViewerNotice('kspaceOverrideFailure',null);setKspaceSafetyWarning(null);return;}
81
+ setViewerNotice('kspaceOverrideFailure','The requested K-space calculation failed: '+error);
82
+ renderViewerNotices();
83
+ }
84
+ document.getElementById('kspaceSafetyAcknowledge').onclick=hideKspaceSafetyDialog;
85
+ document.getElementById('kspaceSafetyProceed').onclick=requestDangerousKspaceCalculation;
86
+ window.addEventListener('resize',renderViewerNotices);
87
+
88
+ function scheduleViewerDraw(includeMinimap){
89
+ viewerDrawMinimap=viewerDrawMinimap||includeMinimap;
90
+ if(viewerDrawFrame)return;
91
+ viewerDrawFrame=requestAnimationFrame(function(){
92
+ viewerDrawFrame=0;draw();
93
+ if(typeof drawKs==='function')drawKs();
94
+ if(viewerDrawMinimap)drawMinimap();
95
+ viewerDrawMinimap=false;
96
+ });
97
+ }
98
+
99
+ /* Timing metadata (TR/TE info for minimap tooltip) */
100
+ var seqTiming=null; // {trTimeSec,trCount,hasExplicitTR,teTimeSec,hasExplicitTE,rfUseGuessed}
101
+
102
+ /* Block positions for minimap: [{i,s,d}, ...] */
103
+ var blockPos=[];
104
+
105
+ /* VS Code API — acquired once, used for postMessage to extension host */
106
+ var vscApi=(typeof acquireVsCodeApi!=='undefined')?acquireVsCodeApi():null;
107
+
108
+ function normalizeM1ReferenceMode(mode){return mode==='observationTime'?'observationTime':'rfCenter';}
109
+ function readM1ReferenceMode(){
110
+ try{localStorage.removeItem('seqeyes.m1ReferenceMode');}catch(_){}
111
+ return 'rfCenter';
112
+ }
113
+ function setM1ReferenceMode(mode){
114
+ var next=normalizeM1ReferenceMode(mode);
115
+ if(next===m1ReferenceMode)return m1ReferenceMode;
116
+ var restore=[!!chVis[8],!!chVis[9],!!chVis[10]];
117
+ var shouldRecalc=!!m1Data&&(chVis[8]||chVis[9]||chVis[10]);
118
+ m1ReferenceMode=next;
119
+ m1Data=null;m1WindowData=null;m1WindowPending=null;chVis[8]=false;chVis[9]=false;chVis[10]=false;
120
+ computeGlobalMax();buildLegend();draw();
121
+ if(shouldRecalc){
122
+ m1RestoreChannels=restore;
123
+ requestM1(restore[0]?8:(restore[1]?9:(restore[2]?10:(m1RequestedChannel||8))));
124
+ }
125
+ return m1ReferenceMode;
126
+ }
127
+ window.SeqEyesDev=window.SeqEyesDev||{};
128
+ window.SeqEyesDev.getM1ReferenceMode=function(){return m1ReferenceMode;};
129
+ window.SeqEyesDev.setM1ReferenceMode=setM1ReferenceMode;
130
+
131
+ /* ── Shared touch utilities (used by kspace.js & interaction.js) ──────── */
132
+ function isTouchDevice(){return('ontouchstart' in window)||(navigator.maxTouchPoints>0);}
133
+ function getTouchPos(touch,el){var r=el.getBoundingClientRect();return {x:touch.clientX-r.left, y:touch.clientY-r.top};}
134
+ function getTouchMid(touches){return {x:(touches[0].clientX+touches[1].clientX)/2, y:(touches[0].clientY+touches[1].clientY)/2};}
135
+ 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);}
136
+
137
+ /* ── Responsive layout ────────────────────────────────────────────────── */
138
+ var layoutMode='horizontal'; // 'horizontal' (kspace right) or 'vertical' (kspace bottom)
139
+ function detectLayoutMode(){
140
+ var was=layoutMode;
141
+ layoutMode=(window.innerHeight>window.innerWidth)?'vertical':'horizontal';
142
+ return layoutMode!==was;
143
+ }
144
+ function applyLayoutMode(){
145
+ document.body.classList.remove('layout-vertical','layout-horizontal');
146
+ document.body.classList.add('layout-'+layoutMode);
147
+
148
+ var main=document.getElementById('main');
149
+ var left=document.getElementById('left');
150
+ var right=document.getElementById('right');
151
+ var handle=document.getElementById('khandle');
152
+ var rc=document.getElementById('rc');
153
+
154
+ if(layoutMode==='vertical'){
155
+ // Direct inline !important styles — beats everything in CSS cascade
156
+ main.style.setProperty('flex-direction','column','important');
157
+ left.style.setProperty('flex','1 1 0%','important');
158
+ left.style.setProperty('min-height','0','important');
159
+ left.style.setProperty('min-width','0','important');
160
+ right.style.setProperty('width','100%','important');
161
+ right.style.setProperty('height','0','important');
162
+ right.style.setProperty('border-left','none','important');
163
+ right.style.setProperty('border-top','1px solid var(--gr)','important');
164
+ right.style.setProperty('transition','height .25s','important');
165
+ right.style.setProperty('flex-shrink','0','important');
166
+ right.style.setProperty('overflow','hidden','important');
167
+ right.style.setProperty('position','relative','important');
168
+ handle.style.setProperty('cursor','row-resize','important');
169
+ handle.style.setProperty('left','0','important');
170
+ handle.style.setProperty('right','0','important');
171
+ handle.style.setProperty('top','0','important');
172
+ handle.style.setProperty('width','100%','important');
173
+ handle.style.setProperty('height','5px','important');
174
+ handle.style.setProperty('bottom','auto','important');
175
+ if(rc){rc.style.setProperty('flex-direction','row','important');}
176
+ // If kspace is open, apply open height inline
177
+ if(typeof kOpen!=='undefined'&&kOpen){
178
+ right.style.setProperty('height','300px','important');
179
+ }
180
+ }else{
181
+ // Remove all inline !important overrides — base CSS takes over
182
+ main.style.cssText='';
183
+ left.style.cssText='';
184
+ right.style.cssText='';
185
+ handle.style.cssText='';
186
+ if(rc)rc.style.cssText='';
187
+ // If kspace is open, restore width
188
+ if(typeof kOpen!=='undefined'&&kOpen){
189
+ right.style.width='500px';
190
+ }
191
+ }
192
+ }
193
+ function refreshLayout(){
194
+ if(detectLayoutMode()){
195
+ applyLayoutMode();
196
+ rs();
197
+ if(typeof drawKs==='function')drawKs();
198
+ if(typeof drawMinimap==='function')drawMinimap();
199
+ }
200
+ }
201
+ applyLayoutMode();
202
+ // Use ResizeObserver on #main for reliable layout detection
203
+ var _mainEl=document.getElementById('main');
204
+ if(_mainEl&&typeof ResizeObserver!=='undefined'){
205
+ new ResizeObserver(function(){refreshLayout();}).observe(_mainEl);
206
+ }
207
+ // Fallback listeners
208
+ window.addEventListener('resize',function(){refreshLayout();});
209
+ if(typeof window.matchMedia==='function'){
210
+ window.matchMedia('(orientation: portrait)').addEventListener('change',function(){refreshLayout();});
211
+ }
212
+
213
+ /* ── Unit conversion helpers ──────────────────────────────────────────── */
214
+ function gradConv(v){if(gradUnit==='mT/m')return v/GAMMA;if(gradUnit==='G/cm')return v/(GAMMA*10);return v;}
215
+ function gradUnitStr(){return gradUnit;}
216
+ function timeConv(t){if(timeUnit==='ms')return t*1e3;if(timeUnit==='us')return t*1e6;return t;}
217
+ function timeUnitStr(){return timeUnit;}
218
+
219
+ /* ── Build legend ─────────────────────────────────────────────────────── */
220
+ function buildLegend(){
221
+ legend.innerHTML='';
222
+ chColors.forEach(function(c,i){
223
+ var d=document.createElement('div');d.className='li'+(chVis[i]?'':' off');d.title='Toggle '+CH[i];
224
+ d.innerHTML='<div class="ld" style="background:'+c+'"></div>'+CH[i];
225
+ if(i>=8&&i<=10&&!m1Data)d.title='Calculate and show '+CH[i];
226
+ else if(i===7&&!pnsData)d.title='Select a PNS ASC file before showing PNS';
227
+ d.onclick=function(){
228
+ if(i>=8&&i<=10&&!m1Data){requestM1(i);return;}
229
+ if(i===7&&!pnsData)return;
230
+ chVis[i]=!chVis[i];
231
+ buildLegend();computeGlobalMax();draw();
232
+ };
233
+ legend.appendChild(d);
234
+ });
235
+ }
236
+ buildLegend();
237
+ tuSel.onchange=function(){timeUnit=tuSel.value;draw();};
238
+ guSel.onchange=function(){gradUnit=guSel.value;draw();};
239
+ function setExportButtonEnabled(enabled){if(exportBtn)exportBtn.disabled=!enabled;}
240
+ function applySerializedKspace(payload){
241
+ kTraj=null;kAdc=null;kTime=null;kAdcTime=null;
242
+ if(!payload)return;
243
+ kTraj=[payload.kx,payload.ky,payload.kz];kTime=payload.tk;
244
+ var n=payload.nAdc||0;
245
+ if(n>0&&payload.axb){
246
+ kAdc=[decodeB64F32(payload.axb,n),decodeB64F32(payload.ayb,n),decodeB64F32(payload.azb,n)];
247
+ kAdcTime=decodeB64F32(payload.tab,n);uploadKSpaceGPU();
248
+ }
249
+ }
250
+
251
+ /* ── Data reception ───────────────────────────────────────────────────── */
252
+ window.addEventListener('message',function(e){
253
+ var m=e.data;
254
+ if(m.type==='progress'){
255
+ var overlay=document.getElementById('poverlay'),fill=document.getElementById('pfill2'),
256
+ text=document.getElementById('ptext'),pct=document.getElementById('ppct');
257
+ if(m.phase==='start'){
258
+ setExportButtonEnabled(false);
259
+ overlay.style.display='flex';fill.style.width='0%';
260
+ text.textContent=m.text||'Loading sequence\u2026';pct.textContent='0%';
261
+ }else if(m.phase==='done'){
262
+ fill.style.width='100%';pct.textContent='100%';
263
+ text.textContent=m.text||'Ready';
264
+ setTimeout(function(){overlay.style.display='none';},500);
265
+ }else{
266
+ fill.style.width=(m.percent||0)+'%';
267
+ pct.textContent=(m.percent||0)+'%';
268
+ text.textContent=m.text||'';
269
+ }
270
+ return;
271
+ }
272
+ if(m.type==='sequenceData'){
273
+ BL=m.blocks||[];TD=m.totalDuration||0;GR=m.gradRaster||1e-5;
274
+ setViewerNotice('sequence',m.notices&&m.notices.length?m.notices.join(' '):null);
275
+ setViewerNotice('kspaceOverrideFailure',null);setKspaceSafetyWarning(m.kspaceSafety||null);
276
+ setViewerNotice('m1',null);setViewerNotice('pns',null);setViewerNotice('pnsThreshold',null);
277
+ waveformOverview=createWaveformOverview(BL);
278
+ RR=m.rfRaster||RR;AR=m.adcRaster||AR;BR=m.blockRaster||BR;
279
+ blockPos=m.blockPositions||[];
280
+ mmCache=null; // invalidate minimap cache on new data
281
+ applySerializedKspace(m.kspace);
282
+ m1Data=null;m1WindowData=null;m1WindowPending=null;pnsData=null;pnsWindowData=null;pnsWindowPending=null;chVis[7]=false;chVis[8]=false;chVis[9]=false;chVis[10]=false;
283
+ // Store timing metadata for minimap tooltip
284
+ if(m.timing) seqTiming=m.timing; else seqTiming=null;
285
+ computeGlobalMax();
286
+ buildLegend();
287
+ // Auto-zoom: if TR is known, zoom to first TR; otherwise fit full sequence
288
+ if(seqTiming&&seqTiming.trTimeSec>0){fitToFirstTR();}else{fit();}
289
+ draw();drawKs();drawMinimap();
290
+ setExportButtonEnabled(true);
291
+ }else if(m.type==='kspaceData'){
292
+ applySerializedKspace(m.kspace);finishDangerousKspaceCalculation(null);drawKs();
293
+ if(typeof kOpen!=='undefined'&&!kOpen)document.getElementById('kbtn').click();
294
+ }else if(m.type==='kspaceError'){
295
+ finishDangerousKspaceCalculation(m.message||'Unknown error.');
296
+ }else if(m.type==='m1Data'){
297
+ m1Busy=false;
298
+ if(m.m1&&m.m1.valid){
299
+ setViewerNotice('m1',null);
300
+ m1Data=createM1SeriesPayload(m.m1);
301
+ m1WindowData=null;m1WindowPending=null;
302
+ if(m1RestoreChannels){
303
+ chVis[8]=!!m1RestoreChannels[0];chVis[9]=!!m1RestoreChannels[1];chVis[10]=!!m1RestoreChannels[2];m1RestoreChannels=null;
304
+ }else chVis[m1RequestedChannel]=true;
305
+ computeGlobalMax();buildLegend();draw();
306
+ if(m.m1.warnings&&m.m1.warnings.length)console.warn('[SeqEyes M1]',m.m1.warnings.join('\n'));
307
+ setViewerNotice('m1Coarse',m.m1.coarse?'Large sequence: showing bounded full-sequence M1. Zoom to 100 TRs or fewer for automatic detail.':null);
308
+ }else{
309
+ setViewerNotice('m1','M1 calculation failed: '+((m.m1&&m.m1.error)||'unknown error')+' Zoom in to inspect waveform detail.');
310
+ }
311
+ }else if(m.type==='m1WindowData'){
312
+ if(m1WindowPending&&m.requestId!==m1WindowPending.requestId)return;
313
+ m1WindowPending=null;
314
+ if(m.m1&&m.m1.valid){m1WindowData=createM1SeriesPayload(m.m1);draw();}
315
+ else if(m.m1&&m.m1.error)setViewerNotice('m1Detail','M1 detail was not calculated: '+m.m1.error);
316
+ }else if(m.type==='m1Error'){
317
+ m1Busy=false;
318
+ setViewerNotice('m1',(m.message||'M1 calculation failed.')+(m.message&&/zoom in/i.test(m.message)?'':' Zoom in to inspect waveform detail.'));
319
+ }else if(m.type==='pnsData'){
320
+ pnsBusy=false;if(pnsBtn)pnsBtn.disabled=false;
321
+ if(m.pns&&m.pns.valid){
322
+ setViewerNotice('pns',null);
323
+ pnsData=createPnsSeriesPayload(m.pns);
324
+ pnsWindowData=null;pnsWindowPending=null;chVis[7]=true;
325
+ computeGlobalMax();buildLegend();draw();
326
+ setViewerNotice('pnsThreshold',m.pns.ok?null:'PNS warning: predicted level reaches or exceeds 100%.');
327
+ setViewerNotice('pnsCoarse',m.pns.coarse?'Large sequence: showing bounded full-sequence PNS. Zoom to 100 TRs or fewer for automatic detail.':null);
328
+ }else{
329
+ setViewerNotice('pns','PNS calculation failed: '+((m.pns&&m.pns.error)||'unknown error')+' Zoom in to inspect waveform detail.');
330
+ }
331
+ }else if(m.type==='pnsWindowData'){
332
+ if(pnsWindowPending&&m.requestId!==pnsWindowPending.requestId)return;
333
+ pnsWindowPending=null;
334
+ if(m.pns&&m.pns.valid){
335
+ pnsWindowData=createPnsSeriesPayload(m.pns);
336
+ draw();
337
+ }
338
+ }else if(m.type==='pnsError'){
339
+ pnsBusy=false;if(pnsBtn)pnsBtn.disabled=false;
340
+ setViewerNotice('pns',(m.message||'PNS calculation failed.')+(m.message&&/zoom in/i.test(m.message)?'':' Zoom in to inspect waveform detail.'));
341
+ }else if(m.type==='pnsSelectionCancelled'){
342
+ pnsBusy=false;if(pnsBtn)pnsBtn.disabled=false;
343
+ }
344
+ });
345
+
346
+ /* ── Base64 → Float32Array decoder ─────────────────────────────────── */
347
+ function decodeB64F32(b64,n){var bin=atob(b64),len=bin.length,b=new Uint8Array(len);for(var i=0;i<len;i++)b[i]=bin.charCodeAt(i);return new Float32Array(b.buffer,0,n);}
348
+
349
+
350
+ /* ── Global amplitude ranges ──────────────────────────────────────────── */
351
+ function computeGlobalMax(){
352
+ gMax=[0.001,6.28318,0.001,0.001,0.001,1,1,0.001,0.001,0.001,0.001];
353
+ for(var i=0;i<BL.length;i++){var b=BL[i];
354
+ if(b.rf){var a=Math.abs(b.rf.a||0);if(a>gMax[0])gMax[0]=a;}
355
+ if(b.gx&&b.gx.ty!=='none'&&Math.abs(b.gx.a||0)>gMax[2])gMax[2]=Math.abs(b.gx.a);
356
+ if(b.gy&&b.gy.ty!=='none'&&Math.abs(b.gy.a||0)>gMax[3])gMax[3]=Math.abs(b.gy.a);
357
+ if(b.gz&&b.gz.ty!=='none'&&Math.abs(b.gz.a||0)>gMax[4])gMax[4]=Math.abs(b.gz.a);
358
+ }
359
+ if(pnsData)gMax[7]=Math.max(gMax[7],pnsData.x.maxAbs,pnsData.y.maxAbs,pnsData.z.maxAbs,pnsData.n.maxAbs);
360
+ 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);}
361
+ gMax[0]=Math.max(gMax[0],100);
362
+ }
363
+
364
+ function createPnsSeriesPayload(pns){
365
+ function series(prefix,timeKey,valueKey){
366
+ return pns.coarse
367
+ ?createEnvelopeSeries(pns[prefix+'0'],pns[prefix+'1'],pns[prefix+'min'],pns[prefix+'max'],pns[prefix+'first'],pns[prefix+'last'],1)
368
+ :createDerivedSeries(pns[timeKey]||pns.t,pns[valueKey],1);
369
+ }
370
+ return{
371
+ coarse:!!pns.coarse,
372
+ startSec:isFinite(pns.startSec)?pns.startSec:null,
373
+ endSec:isFinite(pns.endSec)?pns.endSec:null,
374
+ x:series('x','tx','x'),
375
+ y:series('y','ty','y'),
376
+ z:series('z','tz','z'),
377
+ n:series('n','tn','n')
378
+ };
379
+ }
380
+
381
+ function createM1SeriesPayload(m1){
382
+ function series(prefix,timeKey,valueKey){
383
+ return m1.coarse
384
+ ?createEnvelopeSeries(m1[prefix+'0'],m1[prefix+'1'],m1[prefix+'min'],m1[prefix+'max'],m1[prefix+'first'],m1[prefix+'last'],1)
385
+ :createDerivedSeries(m1[timeKey]||m1.t,m1[valueKey],1);
386
+ }
387
+ return{
388
+ coarse:!!m1.coarse,
389
+ startSec:isFinite(m1.startSec)?m1.startSec:null,
390
+ endSec:isFinite(m1.endSec)?m1.endSec:null,
391
+ referenceMode:m1.referenceMode||m1ReferenceMode,
392
+ warnings:m1.warnings||[],
393
+ x:series('x','tx','x'),
394
+ y:series('y','ty','y'),
395
+ z:series('z','tz','z')
396
+ };
397
+ }
398
+
399
+ function shouldRequestFineM1Window(vs,ve){
400
+ if(!vscApi||!m1Data||!m1Data.coarse||!(chVis[8]||chVis[9]||chVis[10]))return false;
401
+ var dur=ve-vs;
402
+ return !!(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*100.5);
403
+ }
404
+
405
+ function derivedWindowCovers(data,vs,ve){
406
+ return !!(data&&data.startSec!==null&&data.endSec!==null&&data.startSec<=vs+1e-12&&data.endSec>=ve-1e-12);
407
+ }
408
+
409
+ function requestM1Window(vs,ve){
410
+ if(!shouldRequestFineM1Window(vs,ve))return;
411
+ if(derivedWindowCovers(m1WindowData,vs,ve)||derivedWindowCovers(m1WindowPending,vs,ve))return;
412
+ var dur=Math.max(ve-vs,minRasterTime()),pad=Math.max(dur*.5,seqTiming&&seqTiming.trTimeSec?seqTiming.trTimeSec:0);
413
+ var start=Math.max(0,vs-pad),end=Math.min(TD||ve,ve+pad);
414
+ m1WindowPending={startSec:start,endSec:end,requestId:++m1WindowRequestId};
415
+ vscApi.postMessage({command:'calculateM1Window',requestId:m1WindowPending.requestId,startSec:start,endSec:end,maxPoints:120000,referenceMode:m1ReferenceMode});
416
+ }
417
+
418
+ function m1SeriesForView(vs,ve){
419
+ if(!m1Data)return null;
420
+ if(shouldRequestFineM1Window(vs,ve)){
421
+ requestM1Window(vs,ve);
422
+ if(derivedWindowCovers(m1WindowData,vs,ve)){setViewerNotice('m1Detail',null);setViewerNotice('m1Coarse','Large sequence: full sequence uses bounded M1; the current view is detailed.');return m1WindowData;}
423
+ }
424
+ if(m1Data.coarse)setViewerNotice('m1Coarse','Large sequence: showing bounded full-sequence M1. Zoom to 100 TRs or fewer for automatic detail.');
425
+ return m1Data;
426
+ }
427
+
428
+ function shouldRequestFinePnsWindow(vs,ve){
429
+ if(!vscApi||!pnsData||!pnsData.coarse||!chVis[7])return false;
430
+ var dur=ve-vs;
431
+ return !!(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*100.5);
432
+ }
433
+
434
+ function pnsWindowCovers(data,vs,ve){
435
+ return derivedWindowCovers(data,vs,ve);
436
+ }
437
+
438
+ function requestPnsWindow(vs,ve){
439
+ if(!shouldRequestFinePnsWindow(vs,ve))return;
440
+ if(pnsWindowCovers(pnsWindowData,vs,ve)||pnsWindowCovers(pnsWindowPending,vs,ve))return;
441
+ var dur=Math.max(ve-vs,minRasterTime()),pad=Math.max(dur*.5,seqTiming&&seqTiming.trTimeSec?seqTiming.trTimeSec:0);
442
+ var start=Math.max(0,vs-pad),end=Math.min(TD||ve,ve+pad);
443
+ pnsWindowPending={startSec:start,endSec:end,requestId:++pnsWindowRequestId};
444
+ vscApi.postMessage({command:'calculatePnsWindow',requestId:pnsWindowPending.requestId,startSec:start,endSec:end,maxPoints:120000});
445
+ }
446
+
447
+ function pnsSeriesForView(vs,ve){
448
+ if(!pnsData)return null;
449
+ if(shouldRequestFinePnsWindow(vs,ve)){
450
+ requestPnsWindow(vs,ve);
451
+ if(pnsWindowCovers(pnsWindowData,vs,ve)){setViewerNotice('pnsCoarse','Large sequence: full sequence uses bounded PNS; the current view is detailed.');return pnsWindowData;}
452
+ }
453
+ if(pnsData.coarse)setViewerNotice('pnsCoarse','Large sequence: showing bounded full-sequence PNS. Zoom to 100 TRs or fewer for automatic detail.');
454
+ return pnsData;
455
+ }
456
+
457
+ /* ── Minimap ──────────────────────────────────────────────────────────── */
458
+ var mmCanvas=document.getElementById('mmc'),mmCtx=mmCanvas.getContext('2d');
459
+ /* Offscreen cache: static block bands rendered once, then blitted each frame.
460
+ * Invalidated on data load or theme change. */
461
+ var mmCache=null, mmCacheW=0, mmCacheDpr=0;
462
+
463
+ /** (Re)build the offscreen minimap cache using ImageData pixel‑buffer rendering.
464
+ * Avoids O(blocks) canvas API calls — instead does one integer‑array pass
465
+ * per block and a single putImageData() at the end. ~20× faster than fillRect. */
466
+ function buildMinimapCache(){
467
+ mmCache=null;
468
+ if(!TD||TD<=0||!BL.length)return;
469
+ var dpr=window.devicePixelRatio||1;
470
+ var W=mmCanvas.width, H=mmCanvas.height; // physical pixels
471
+ if(W<=0||H<=0)return;
472
+
473
+ var s=getComputedStyle(document.body);
474
+ var cssW=W/dpr, scM=cssW/TD;
475
+
476
+ // Parse theme colours → [r,g,b]
477
+ var rfRGB=parseHexRGB(s.getPropertyValue('--rf').trim());
478
+ var gxRGB=parseHexRGB(s.getPropertyValue('--gx').trim());
479
+ var gyRGB=parseHexRGB(s.getPropertyValue('--gy').trim());
480
+ var gzRGB=parseHexRGB(s.getPropertyValue('--gz').trim());
481
+ var adcRGB=parseHexRGB(s.getPropertyValue('--adc').trim());
482
+ var bgRGB=parseHexRGB(s.getPropertyValue('--trbg').trim());
483
+
484
+ // Band layout in physical pixels
485
+ var rfY0=0, rfY1=5*dpr;
486
+ var gY0=5*dpr, gH=2*dpr;
487
+ var aY0=11*dpr, aY1=H;
488
+
489
+ // ── Step 1: accumulate per‑pixel‑column counters ──
490
+ // For each pixel column we store a hit count per band (integer 0..255).
491
+ var pw=Math.max(1,Math.ceil(cssW));
492
+ var rfCnt=new Uint8Array(pw), adcCnt=new Uint8Array(pw);
493
+ var gxCnt=new Uint8Array(pw), gyCnt=new Uint8Array(pw), gzCnt=new Uint8Array(pw);
494
+
495
+ // Stride: for very large sequences, skip blocks that map to sub‑pixel positions
496
+ var stride=BL.length>20000?Math.floor(BL.length/15000):1;
497
+
498
+ for(var i=0;i<BL.length;i+=stride){
499
+ var b=BL[i];
500
+ var px0=Math.floor(b.s*scM), px1=Math.ceil((b.s+b.d)*scM);
501
+ if(px0>=pw)continue;
502
+ px0=Math.max(0,px0);px1=Math.min(pw,px1);
503
+
504
+ if(b.rf) for(var p=px0;p<px1;p++){if(rfCnt[p]<255)rfCnt[p]++;}
505
+ if(b.gx&&b.gx.ty!=='none') for(var p=px0;p<px1;p++){if(gxCnt[p]<255)gxCnt[p]++;}
506
+ if(b.gy&&b.gy.ty!=='none') for(var p=px0;p<px1;p++){if(gyCnt[p]<255)gyCnt[p]++;}
507
+ if(b.gz&&b.gz.ty!=='none') for(var p=px0;p<px1;p++){if(gzCnt[p]<255)gzCnt[p]++;}
508
+ if(b.adc) for(var p=px0;p<px1;p++){if(adcCnt[p]<255)adcCnt[p]++;}
509
+ }
510
+
511
+ // ── Step 2: build ImageData from counters ──
512
+ var imgData=new ImageData(W,H);
513
+ var d=imgData.data;
514
+ // Fill background first
515
+ for(var y=0;y<H;y++){
516
+ for(var x=0;x<W;x++){
517
+ var idx=(y*W+x)*4;
518
+ d[idx]=bgRGB[0];d[idx+1]=bgRGB[1];d[idx+2]=bgRGB[2];d[idx+3]=255;
519
+ }
520
+ }
521
+ // Overlay each band — alpha proportional to hit count, clamped
522
+ for(var px=0;px<pw;px++){
523
+ var x0=Math.floor(px*dpr), x1=Math.floor((px+1)*dpr);
524
+ if(x1<=x0)x1=x0+1;
525
+
526
+ // RF band
527
+ if(rfCnt[px]>0){
528
+ var a=Math.min(140,rfCnt[px]*55);
529
+ fillBand(d,W,x0,x1,rfY0,rfY1,rfRGB[0],rfRGB[1],rfRGB[2],a);
530
+ }
531
+ // Gx band
532
+ if(gxCnt[px]>0){
533
+ var a=Math.min(150,gxCnt[px]*60);
534
+ fillBand(d,W,x0,x1,gY0,gY0+gH,gxRGB[0],gxRGB[1],gxRGB[2],a);
535
+ }
536
+ // Gy band
537
+ if(gyCnt[px]>0){
538
+ var a=Math.min(150,gyCnt[px]*60);
539
+ fillBand(d,W,x0,x1,gY0+gH,gY0+2*gH,gyRGB[0],gyRGB[1],gyRGB[2],a);
540
+ }
541
+ // Gz band
542
+ if(gzCnt[px]>0){
543
+ var a=Math.min(150,gzCnt[px]*60);
544
+ fillBand(d,W,x0,x1,gY0+2*gH,gY0+3*gH,gzRGB[0],gzRGB[1],gzRGB[2],a);
545
+ }
546
+ // ADC band
547
+ if(adcCnt[px]>0){
548
+ var a=Math.min(130,adcCnt[px]*50);
549
+ fillBand(d,W,x0,x1,aY0,aY1,adcRGB[0],adcRGB[1],adcRGB[2],a);
550
+ }
551
+ }
552
+
553
+ // Write to offscreen canvas
554
+ var oc=document.createElement('canvas');
555
+ oc.width=W;oc.height=H;
556
+ oc.getContext('2d').putImageData(imgData,0,0);
557
+ mmCache=oc;mmCacheW=W;mmCacheDpr=dpr;
558
+ }
559
+
560
+ /** Fill a rectangular band in the ImageData buffer with alpha blending. */
561
+ function fillBand(d,W,x0,x1,y0,y1,r,g,b,a){
562
+ for(var y=y0;y<y1;y++){
563
+ for(var x=x0;x<x1;x++){
564
+ var i=(y*W+x)*4;
565
+ var srcA=a/255, dstA=d[i+3]/255;
566
+ var outA=srcA+dstA*(1-srcA);
567
+ if(outA<0.001)continue;
568
+ d[i] =(r*srcA+d[i] *dstA*(1-srcA))/outA;
569
+ d[i+1]=(g*srcA+d[i+1]*dstA*(1-srcA))/outA;
570
+ d[i+2]=(b*srcA+d[i+2]*dstA*(1-srcA))/outA;
571
+ d[i+3]=Math.round(outA*255);
572
+ }
573
+ }
574
+ }
575
+
576
+ /** Parse a hex colour (#rrggbb or #rgb) to [r,g,b]. */
577
+ function parseHexRGB(c){
578
+ if(!c||c[0]!=='#')return[128,128,128];
579
+ var h=c.substring(1);
580
+ if(h.length===3)h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2];
581
+ return[parseInt(h.substring(0,2),16),parseInt(h.substring(2,4),16),parseInt(h.substring(4,6),16)];
582
+ }
583
+
584
+ /** Fast per‑frame draw: blit cache + dynamic viewport highlight + TR lines + text. */
585
+ function drawMinimap(){
586
+ var dpr=window.devicePixelRatio||1;
587
+ var W=mmCanvas.width/dpr, H=mmCanvas.height/dpr;
588
+ mmCtx.globalAlpha=1; // reset — prevent stale alpha from previous draws
589
+ mmCtx.clearRect(0,0,W,H);
590
+ if(!TD||TD<=0)return;
591
+ var s=getComputedStyle(document.body);
592
+
593
+ // Rebuild cache when needed (data changed, theme changed, or DPR changed)
594
+ if(!mmCache||mmCacheW!==mmCanvas.width||mmCacheDpr!==dpr){
595
+ buildMinimapCache();
596
+ }
597
+
598
+ // ── Blit cached block bands ──
599
+ if(mmCache){
600
+ mmCtx.drawImage(mmCache,0,0);
601
+ }else{
602
+ mmCtx.fillStyle=s.getPropertyValue('--trbg').trim();mmCtx.fillRect(0,0,W,H);
603
+ }
604
+
605
+ if(!BL.length)return;
606
+ var scM=W/TD;
607
+
608
+ // ── TR boundary lines ──
609
+ if(seqTiming&&seqTiming.trTimeSec>0&&seqTiming.trCount>1){
610
+ mmCtx.strokeStyle=s.getPropertyValue('--fg').trim();
611
+ mmCtx.globalAlpha=0.10;mmCtx.lineWidth=0.4;
612
+ mmCtx.beginPath();
613
+ var trStep=Math.max(1,Math.ceil(seqTiming.trCount/Math.max(1,W)));
614
+ for(var tr=0;tr<=seqTiming.trCount;tr+=trStep){
615
+ var tx=tr*seqTiming.trTimeSec*scM;
616
+ if(tx<=W){mmCtx.moveTo(tx,0);mmCtx.lineTo(tx,H);}
617
+ }
618
+ mmCtx.stroke();mmCtx.globalAlpha=1;
619
+ }
620
+
621
+ // ── Viewport highlight ──
622
+ var mcW=mc.width/dpr;
623
+ var visibleDur = plotWidth() / Math.max(sc, 1e-12);
624
+ var vx0=ox*scM, vx1=(ox+visibleDur)*scM;
625
+ vx0=Math.max(0,vx0);vx1=Math.min(W,vx1);
626
+ var vw=Math.max(2,vx1-vx0);
627
+ // Subtle wash
628
+ mmCtx.fillStyle=s.getPropertyValue('--fg').trim();mmCtx.globalAlpha=0.18;
629
+ mmCtx.fillRect(vx0,0,vw,H);
630
+ // Edge markers — only when viewport is not flush against the canvas edge
631
+ mmCtx.globalAlpha=0.45;
632
+ mmCtx.fillStyle=s.getPropertyValue('--adc').trim();
633
+ if(vx0>0.5)mmCtx.fillRect(vx0,0,1,H);
634
+ if(vx1<W-0.5)mmCtx.fillRect(vx1-1,0,1,H);
635
+ mmCtx.globalAlpha=1;
636
+
637
+ // ── Info text ──
638
+ var info='';
639
+ if(seqTiming){
640
+ if(seqTiming.trTimeSec>0)info+='TR='+fmtDur2(seqTiming.trTimeSec)+(seqTiming.hasExplicitTR?'':'~')+' ';
641
+ if(seqTiming.teTimeSec>0)info+='TE='+fmtDur2(seqTiming.teTimeSec)+' ';
642
+ if(seqTiming.trCount>1)info+=seqTiming.trCount+' TRs';
643
+ }
644
+ if(info){
645
+ mmCtx.fillStyle='rgba(0,0,0,0.35)';mmCtx.font='9px monospace';mmCtx.textAlign='left';
646
+ mmCtx.fillText(info,5,H-3);
647
+ mmCtx.fillStyle=s.getPropertyValue('--fg').trim();mmCtx.globalAlpha=0.9;
648
+ mmCtx.fillText(info,4,H-4);
649
+ mmCtx.globalAlpha=1;
650
+ }
651
+ }
652
+
653
+ function fmtDur2(s){
654
+ if(s>=1)return s.toFixed(2)+'s';
655
+ if(s>=0.001)return(s*1e3).toFixed(1)+'ms';
656
+ return(s*1e6).toFixed(0)+'\u00b5s';
657
+ }
658
+
659
+ /* ── Canvas resize ────────────────────────────────────────────────────── */
660
+ function rs(){
661
+ var dpr=window.devicePixelRatio||1,r=cc.getBoundingClientRect();
662
+ mc.width=r.width*dpr;mc.height=r.height*dpr;
663
+ mc.style.width=r.width+'px';mc.style.height=r.height+'px';
664
+ ctx.setTransform(dpr,0,0,dpr,0,0);
665
+ moc.width=r.width*dpr;moc.height=r.height*dpr;
666
+ moc.style.width=r.width+'px';moc.style.height=r.height+'px';
667
+ moctx.setTransform(dpr,0,0,dpr,0,0);
668
+ // Adaptive margins for small/narrow screens
669
+ if(layoutMode==='vertical'||r.width<600){
670
+ M.l=60;M.r=3;M.t=5;M.b=18;
671
+ }else{
672
+ M.l=92;M.r=4;M.t=8;M.b=22;
673
+ }
674
+ // Resize minimap canvas too
675
+ var mr=document.getElementById('mmap').getBoundingClientRect();
676
+ mmCanvas.width=mr.width*dpr;mmCanvas.height=mr.height*dpr;
677
+ mmCanvas.style.width=mr.width+'px';mmCanvas.style.height=mr.height+'px';
678
+ mmCtx.setTransform(dpr,0,0,dpr,0,0);
679
+ draw();drawMinimap();
680
+ }
681
+ window.addEventListener('resize',rs);new ResizeObserver(rs).observe(cc);
682
+
683
+ /* ── Coordinate mapping ───────────────────────────────────────────────── */
684
+ function visChannels(){var v=[];for(var i=0;i<CH.length;i++)if(chVis[i])v.push(i);return v;}
685
+ 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;}
686
+ function cH(){var vc=visChannels();return(mc.height/(window.devicePixelRatio||1)-M.t-M.b)/Math.max(vc.length,1);}
687
+ function t2x(t){return M.l+(t-ox)*sc}
688
+ function x2t(x){return ox+(x-M.l)/sc}
689
+
690
+ /* ── Viewport bounds ───────────────────────────────────────────────────── */
691
+ function plotWidth(){
692
+ var dpr=window.devicePixelRatio||1,w=mc.width/dpr;
693
+ return Math.max(1,w-M.l-M.r);
694
+ }
695
+ function minRasterTime(){
696
+ var vals=[GR,RR,AR,BR].filter(function(v){return isFinite(v)&&v>0;});
697
+ return vals.length?Math.min.apply(null,vals):1e-7;
698
+ }
699
+ function visibleDuration(){return plotWidth()/Math.max(sc,1e-12);}
700
+ function clampView(){
701
+ var pw=plotWidth();
702
+ if(!isFinite(TD)||TD<=0){ox=0;sc=1;return;}
703
+ if(!isFinite(sc)||sc<=0)sc=pw/TD;
704
+ var minDur=Math.min(TD,minRasterTime());
705
+ var dur=visibleDuration();
706
+ if(!isFinite(dur)||dur<=0)dur=TD;
707
+ dur=Math.max(minDur,Math.min(TD,dur));
708
+ sc=pw/dur;
709
+ var maxOx=Math.max(0,TD-dur);
710
+ if(!isFinite(ox))ox=0;
711
+ ox=Math.max(0,Math.min(ox,maxOx));
712
+ }
713
+ function zoomAt(screenX,zf){
714
+ if(!isFinite(zf)||zf<=0)return false;
715
+ var oldSc=sc,oldOx=ox;
716
+ var tm=x2t(screenX);
717
+ sc*=zf;
718
+ var dur=visibleDuration();
719
+ if(!isFinite(dur)||dur<=0)dur=TD||minRasterTime();
720
+ var maxDur=TD||dur,minDur=Math.min(maxDur,minRasterTime());
721
+ dur=Math.max(minDur,Math.min(maxDur,dur));
722
+ sc=plotWidth()/dur;
723
+ ox=tm-(screenX-M.l)/sc;
724
+ clampView();
725
+ var scaleTol=Math.max(1,Math.abs(oldSc))*1e-12,offsetTol=Math.max(1,TD)*1e-12;
726
+ return Math.abs(sc-oldSc)>scaleTol||Math.abs(ox-oldOx)>offsetTol;
727
+ }
728
+ function zoomAtCenter(zf){
729
+ return zoomAt(M.l+plotWidth()/2,zf);
730
+ }
731
+
732
+ /* ── Initial view: zoom to first TR ───────────────────────────────────── */
733
+ function fitToFirstTR(){
734
+ if(!seqTiming||seqTiming.trTimeSec<=0){fit();return;}
735
+ var w=mc.width/(window.devicePixelRatio||1);
736
+ // Show first TR with 10% padding on each side
737
+ var trDur=seqTiming.trTimeSec;
738
+ sc=(w-M.l-M.r)/(trDur*1.2);
739
+ ox=Math.max(0,-trDur*0.1); // small negative offset for left padding
740
+ clampView();
741
+ }
742
+
743
+
744
+ /* ══ derived-series.js ══ */
745
+ /* Multiresolution first/min/max/last summaries for derived waveform rendering. */
746
+ function createDerivedSeries(time,values,scale){
747
+ var n=Math.min(time?time.length:0,values?values.length:0),maxAbs=0;
748
+ var series={t:time||[],v:values||[],n:n,scale:scale||1,maxAbs:0,levels:[]};
749
+ for(var i=0;i<n;i++)if(isFinite(values[i]))maxAbs=Math.max(maxAbs,Math.abs(values[i]*(scale||1)));
750
+ series.maxAbs=maxAbs;
751
+ if(n<16)return series;
752
+
753
+ var bucketSize=8,child=null;
754
+ while(bucketSize<n){
755
+ var count=Math.ceil(n/bucketSize),mins=new Int32Array(count),maxs=new Int32Array(count);
756
+ mins.fill(-1);maxs.fill(-1);
757
+ for(var b=0;b<count;b++){
758
+ var minIndex=-1,maxIndex=-1;
759
+ if(!child){
760
+ var start=b*bucketSize,end=Math.min(n,start+bucketSize);
761
+ for(var j=start;j<end;j++){
762
+ var value=values[j];if(!isFinite(value))continue;
763
+ if(minIndex<0||value<values[minIndex])minIndex=j;
764
+ if(maxIndex<0||value>values[maxIndex])maxIndex=j;
765
+ }
766
+ }else{
767
+ var childStart=b*4,childEnd=Math.min(child.mins.length,childStart+4);
768
+ for(var c=childStart;c<childEnd;c++){
769
+ var candidates=[child.mins[c],child.maxs[c]];
770
+ for(var k=0;k<2;k++){
771
+ var index=candidates[k];if(index<0)continue;
772
+ if(minIndex<0||values[index]<values[minIndex])minIndex=index;
773
+ if(maxIndex<0||values[index]>values[maxIndex])maxIndex=index;
774
+ }
775
+ }
776
+ }
777
+ mins[b]=minIndex;maxs[b]=maxIndex;
778
+ }
779
+ child={bucketSize:bucketSize,mins:mins,maxs:maxs};
780
+ series.levels.push(child);
781
+ if(count<=1)break;
782
+ bucketSize*=4;
783
+ }
784
+ return series;
785
+ }
786
+
787
+ /* Explicit time-bucket envelopes used by bounded full-sequence M1/PNS. */
788
+ function createEnvelopeSeries(startTime,endTime,minValues,maxValues,firstValues,lastValues,scale){
789
+ var n=Math.min(startTime?startTime.length:0,endTime?endTime.length:0,minValues?minValues.length:0,maxValues?maxValues.length:0);
790
+ var series={kind:'envelope',n:n,scale:scale||1,maxAbs:0,levels:[]};
791
+ if(n<1)return series;
792
+ var base={count:n,bucketSize:1,t0:startTime,t1:endTime,min:minValues,max:maxValues,first:firstValues||minValues,last:lastValues||maxValues};
793
+ series.levels.push(base);
794
+ for(var i=0;i<n;i++){
795
+ if(isFinite(minValues[i]))series.maxAbs=Math.max(series.maxAbs,Math.abs(minValues[i]*(scale||1)));
796
+ if(isFinite(maxValues[i]))series.maxAbs=Math.max(series.maxAbs,Math.abs(maxValues[i]*(scale||1)));
797
+ }
798
+ var level=base;
799
+ while(level.count>1){level=mergeEnvelopeLevel(level);series.levels.push(level);}
800
+ return series;
801
+ }
802
+
803
+ function mergeEnvelopeLevel(child){
804
+ var count=Math.ceil(child.count/4),level={count:count,bucketSize:child.bucketSize*4,t0:new Float64Array(count),t1:new Float64Array(count),min:new Float64Array(count),max:new Float64Array(count),first:new Float64Array(count),last:new Float64Array(count)};
805
+ level.min.fill(Infinity);level.max.fill(-Infinity);
806
+ for(var bucket=0;bucket<count;bucket++){
807
+ var start=bucket*4,end=Math.min(child.count,start+4);
808
+ level.t0[bucket]=child.t0[start];level.t1[bucket]=child.t1[end-1];level.first[bucket]=child.first[start];level.last[bucket]=child.last[end-1];
809
+ for(var i=start;i<end;i++){if(child.min[i]<level.min[bucket])level.min[bucket]=child.min[i];if(child.max[i]>level.max[bucket])level.max[bucket]=child.max[i];}
810
+ }
811
+ return level;
812
+ }
813
+
814
+ function envelopeLevelWindow(level,viewStart,viewEnd){
815
+ var i0=Math.max(0,lowerBoundSeries(level.t1,viewStart));
816
+ var i1=Math.min(level.count,upperBoundSeries(level.t0,viewEnd));
817
+ return{i0:i0,i1:i1,count:Math.max(0,i1-i0)};
818
+ }
819
+
820
+ function selectEnvelopeLevel(series,viewStart,viewEnd,maxBuckets){
821
+ if(!series||series.kind!=='envelope'||!series.levels.length)return null;
822
+ var selected=series.levels[series.levels.length-1];
823
+ for(var i=0;i<series.levels.length;i++){
824
+ var candidate=series.levels[i];
825
+ if(envelopeLevelWindow(candidate,viewStart,viewEnd).count<=maxBuckets){selected=candidate;break;}
826
+ }
827
+ return selected;
828
+ }
829
+
830
+ function forEachEnvelopeRange(series,viewStart,viewEnd,maxBuckets,visit){
831
+ var level=selectEnvelopeLevel(series,viewStart,viewEnd,maxBuckets);if(!level)return 0;
832
+ var range=envelopeLevelWindow(level,viewStart,viewEnd),emitted=0;
833
+ for(var i=range.i0;i<range.i1;i++){
834
+ if(!isFinite(level.t0[i])||!isFinite(level.t1[i])||!isFinite(level.min[i])||!isFinite(level.max[i]))continue;
835
+ visit(Math.max(viewStart,level.t0[i]),Math.min(viewEnd,level.t1[i]),level.min[i]*series.scale,level.max[i]*series.scale,level.first[i]*series.scale,level.last[i]*series.scale);
836
+ emitted++;
837
+ }
838
+ return emitted;
839
+ }
840
+
841
+ function sampleEnvelopeRangeAtTime(series,timeSec){
842
+ if(!series||series.kind!=='envelope'||!series.levels.length||!isFinite(timeSec))return null;
843
+ var base=series.levels[0];if(!base||base.count<1)return null;
844
+ var index=Math.max(0,lowerBoundSeries(base.t1,timeSec));
845
+ if(index>=base.count||timeSec<base.t0[index]||timeSec>base.t1[index])return null;
846
+ return{
847
+ startTime:base.t0[index],endTime:base.t1[index],
848
+ min:base.min[index]*series.scale,max:base.max[index]*series.scale,
849
+ first:base.first[index]*series.scale,last:base.last[index]*series.scale
850
+ };
851
+ }
852
+
853
+ function lowerBoundSeries(values,target){
854
+ var lo=0,hi=values.length;
855
+ while(lo<hi){var mid=(lo+hi)>>1;if(values[mid]<target)lo=mid+1;else hi=mid}
856
+ return lo;
857
+ }
858
+ function upperBoundSeries(values,target){
859
+ var lo=0,hi=values.length;
860
+ while(lo<hi){var mid=(lo+hi)>>1;if(values[mid]<=target)lo=mid+1;else hi=mid}
861
+ return lo;
862
+ }
863
+
864
+ function derivedSeriesWindow(series,viewStart,viewEnd){
865
+ if(!series||series.n<1)return{i0:0,i1:0,count:0};
866
+ var i0=Math.max(0,lowerBoundSeries(series.t,viewStart)-1);
867
+ var i1=Math.min(series.n,upperBoundSeries(series.t,viewEnd)+1);
868
+ return{i0:i0,i1:i1,count:Math.max(0,i1-i0)};
869
+ }
870
+
871
+ function forEachDerivedRange(series,viewStart,viewEnd,maxBuckets,visit){
872
+ var windowRange=derivedSeriesWindow(series,viewStart,viewEnd);
873
+ if(windowRange.count<1)return 0;
874
+ var i0=windowRange.i0,i1=windowRange.i1,t=series.t,v=series.v,n=series.n;
875
+ var level=series.levels[series.levels.length-1];
876
+ for(var li=0;li<series.levels.length;li++){
877
+ var candidate=series.levels[li];
878
+ var visibleBuckets=Math.ceil(i1/candidate.bucketSize)-Math.floor(i0/candidate.bucketSize);
879
+ if(visibleBuckets<=maxBuckets){level=candidate;break}
880
+ }
881
+ var emitted=0,size=level.bucketSize;
882
+ var firstBucket=Math.floor(i0/size),lastBucket=Math.floor((i1-1)/size);
883
+ function emitRange(start,end,minIndex,maxIndex){
884
+ if(end<=start)return;
885
+ if(minIndex===undefined||maxIndex===undefined){
886
+ minIndex=-1;maxIndex=-1;
887
+ for(var index=start;index<end;index++){
888
+ if(!isFinite(v[index]))continue;
889
+ if(minIndex<0||v[index]<v[minIndex])minIndex=index;
890
+ if(maxIndex<0||v[index]>v[maxIndex])maxIndex=index;
891
+ }
892
+ }
893
+ if(minIndex<0||maxIndex<0)return;
894
+ var center=0.5*(t[start]+t[end-1]);
895
+ if(!isFinite(center))return;
896
+ visit(center,v[minIndex]*series.scale,v[maxIndex]*series.scale);
897
+ emitted++;
898
+ }
899
+ if(firstBucket===lastBucket){
900
+ emitRange(i0,i1);
901
+ return emitted;
902
+ }
903
+ emitRange(i0,Math.min(i1,(firstBucket+1)*size));
904
+ for(var bucket=firstBucket+1;bucket<lastBucket;bucket++){
905
+ var start=bucket*size,end=Math.min(n,start+size);
906
+ emitRange(start,end,level.mins[bucket],level.maxs[bucket]);
907
+ }
908
+ emitRange(Math.max(i0,lastBucket*size),i1);
909
+ return emitted;
910
+ }
911
+
912
+ function forEachDerivedPoint(series,viewStart,viewEnd,maxPoints,visit){
913
+ if(!series||series.n<1)return 0;
914
+ var t=series.t,v=series.v,n=series.n;
915
+ var windowRange=derivedSeriesWindow(series,viewStart,viewEnd);
916
+ var i0=windowRange.i0,i1=windowRange.i1;
917
+ if(i1<=i0)return 0;
918
+ var count=i1-i0,emitted=0;
919
+ function emit(index){
920
+ if(index<i0||index>=i1||!isFinite(t[index])||!isFinite(v[index]))return;
921
+ visit(t[index],v[index]*series.scale);emitted++;
922
+ }
923
+ if(count<=maxPoints||!series.levels.length){
924
+ for(var raw=i0;raw<i1;raw++)emit(raw);
925
+ return emitted;
926
+ }
927
+
928
+ var maxBuckets=Math.max(1,Math.floor(maxPoints/4)),level=series.levels[series.levels.length-1];
929
+ for(var li=0;li<series.levels.length;li++){
930
+ var candidate=series.levels[li],visibleBuckets=Math.ceil(i1/candidate.bucketSize)-Math.floor(i0/candidate.bucketSize);
931
+ if(visibleBuckets<=maxBuckets){level=candidate;break}
932
+ }
933
+ var size=level.bucketSize,firstBucket=Math.floor(i0/size),lastBucket=Math.floor((i1-1)/size);
934
+ function emitRange(start,end){
935
+ var minIndex=-1,maxIndex=-1;
936
+ for(var index=start;index<end;index++){
937
+ if(!isFinite(v[index]))continue;
938
+ if(minIndex<0||v[index]<v[minIndex])minIndex=index;
939
+ if(maxIndex<0||v[index]>v[maxIndex])maxIndex=index;
940
+ }
941
+ emitOrdered([start,minIndex,maxIndex,end-1]);
942
+ }
943
+ function emitOrdered(indices){
944
+ indices.sort(function(a,b){return a-b});
945
+ var previous=-1;
946
+ for(var index=0;index<indices.length;index++){
947
+ var sample=indices[index];if(sample<0||sample===previous)continue;
948
+ emit(sample);previous=sample;
949
+ }
950
+ }
951
+ if(firstBucket===lastBucket){
952
+ emitRange(i0,i1);
953
+ return emitted;
954
+ }
955
+ var firstEnd=Math.min(i1,(firstBucket+1)*size);
956
+ emitRange(i0,firstEnd);
957
+ for(var bucket=firstBucket+1;bucket<lastBucket;bucket++){
958
+ var start=bucket*size,end=Math.min(n,start+size);
959
+ emitOrdered([start,level.mins[bucket],level.maxs[bucket],end-1]);
960
+ }
961
+ emitRange(Math.max(i0,lastBucket*size),i1);
962
+ return emitted;
963
+ }
964
+
965
+ /* Ordered M4-style reduction for one waveform. Keeps both extrema per bucket. */
966
+ function forEachWaveformPoint(time,values,maxPoints,visit){
967
+ var n=Math.min(time?time.length:0,values?values.length:0);if(n<1||maxPoints<1)return 0;
968
+ if(n<=maxPoints){for(var direct=0;direct<n;direct++)visit(time[direct],values[direct]);return n;}
969
+ var bucketCount=Math.max(1,Math.floor(maxPoints/4)),emitted=0;
970
+ for(var bucket=0;bucket<bucketCount;bucket++){
971
+ var start=Math.floor(bucket*n/bucketCount),end=Math.max(start+1,Math.floor((bucket+1)*n/bucketCount));end=Math.min(n,end);
972
+ var minIndex=start,maxIndex=start;
973
+ for(var i=start+1;i<end;i++){if(values[i]<values[minIndex])minIndex=i;if(values[i]>values[maxIndex])maxIndex=i;}
974
+ var indices=[start,minIndex,maxIndex,end-1].sort(function(a,b){return a-b;}),previous=-1;
975
+ for(var j=0;j<indices.length;j++){var index=indices[j];if(index===previous)continue;visit(time[index],values[index]);previous=index;emitted++;}
976
+ }
977
+ return emitted;
978
+ }
979
+
980
+ function createRfEventOverview(blocks){
981
+ var starts=[],ends=[],peaks=[],peakTimes=[],areas=[],waveforms=[];
982
+ for(var bi=0;bi<blocks.length;bi++){
983
+ var rf=blocks[bi]&&blocks[bi].rf;if(!rf)continue;
984
+ var start=isFinite(rf.s)?rf.s:(rf.t&&rf.t.length?rf.t[0]:NaN),end=isFinite(rf.d)&&isFinite(start)?start+Math.max(0,rf.d):(rf.t&&rf.t.length?rf.t[rf.t.length-1]:NaN);
985
+ if(!isFinite(start)||!isFinite(end))continue;
986
+ var n=Math.min(rf.t?rf.t.length:0,rf.m?rf.m.length:0),peak=isFinite(rf.pk)?Math.abs(rf.pk):0,peakTime=(start+end)*.5,area=isFinite(rf.ar)?Math.max(0,rf.ar):0,samplePeak=-1;
987
+ for(var i=0;i<n;i++)if(isFinite(rf.m[i])&&isFinite(rf.t[i])&&Math.abs(rf.m[i])>samplePeak){samplePeak=Math.abs(rf.m[i]);peakTime=rf.t[i];}
988
+ if(!isFinite(rf.pk))peak=Math.max(0,samplePeak);
989
+ if(!isFinite(rf.ar))for(var sample=1;sample<n;sample++){
990
+ var dt=rf.t[sample]-rf.t[sample-1];if(!isFinite(dt)||dt<=0)continue;
991
+ area+=.5*(Math.abs(rf.m[sample-1]||0)+Math.abs(rf.m[sample]||0))*dt;
992
+ }
993
+ if(n<2&&peak>0&&end>start)area=peak*(end-start);
994
+ starts.push(start);ends.push(end);peaks.push(peak);peakTimes.push(peakTime);areas.push(area);waveforms.push(rf);
995
+ }
996
+ return{count:starts.length,start:new Float64Array(starts),end:new Float64Array(ends),peak:new Float64Array(peaks),peakTime:new Float64Array(peakTimes),area:new Float64Array(areas),waveforms:waveforms};
997
+ }
998
+
999
+ function binRfEvents(series,viewStart,viewEnd,pixelCount,widePixelThreshold){
1000
+ var count=Math.max(1,Math.floor(pixelCount)),peak=new Float64Array(count),peakTime=new Float64Array(count),occupiedStart=new Float64Array(count),occupiedEnd=new Float64Array(count),area=new Float64Array(count),events=new Uint32Array(count),wide=[];
1001
+ if(!series||series.count<1||!isFinite(viewStart)||!isFinite(viewEnd)||viewEnd<=viewStart)return{peak:peak,peakTime:peakTime,occupiedStart:occupiedStart,occupiedEnd:occupiedEnd,area:area,events:events,wide:wide,secondsPerPixel:0};
1002
+ var secondsPerPixel=(viewEnd-viewStart)/count,first=Math.max(0,lowerBoundSeries(series.end,viewStart));
1003
+ for(var i=first;i<series.count;i++){
1004
+ var start=series.start[i],end=series.end[i];if(start>viewEnd)break;if(end<viewStart)continue;
1005
+ var clippedStart=Math.max(viewStart,start),clippedEnd=Math.min(viewEnd,end),duration=Math.max(0,end-start);
1006
+ if(duration/secondsPerPixel>=widePixelThreshold){wide.push(i);continue;}
1007
+ var firstBin=Math.max(0,Math.min(count-1,Math.floor((clippedStart-viewStart)/secondsPerPixel)));
1008
+ var lastBin=Math.max(firstBin,Math.min(count-1,Math.ceil((clippedEnd-viewStart)/secondsPerPixel)-1));
1009
+ for(var bin=firstBin;bin<=lastBin;bin++){
1010
+ var binStart=viewStart+bin*secondsPerPixel,binEnd=binStart+secondsPerPixel;
1011
+ var overlap=Math.max(0,Math.min(clippedEnd,binEnd)-Math.max(clippedStart,binStart));
1012
+ if(duration<=0){if(bin!==firstBin)continue;overlap=secondsPerPixel;}
1013
+ if(overlap<=0)continue;
1014
+ var overlapStart=Math.max(clippedStart,binStart),overlapEnd=Math.min(clippedEnd,binEnd),eventPeak=series.peak[i];
1015
+ if(events[bin]<1){occupiedStart[bin]=overlapStart;occupiedEnd[bin]=overlapEnd;peakTime[bin]=Math.max(overlapStart,Math.min(overlapEnd,series.peakTime[i]));}
1016
+ else{occupiedStart[bin]=Math.min(occupiedStart[bin],overlapStart);occupiedEnd[bin]=Math.max(occupiedEnd[bin],overlapEnd);}
1017
+ if(eventPeak>peak[bin]){peak[bin]=eventPeak;peakTime[bin]=Math.max(overlapStart,Math.min(overlapEnd,series.peakTime[i]));}
1018
+ area[bin]+=series.area[i]*(duration>0?overlap/duration:1);events[bin]++;
1019
+ }
1020
+ }
1021
+ return{peak:peak,peakTime:peakTime,occupiedStart:occupiedStart,occupiedEnd:occupiedEnd,area:area,events:events,wide:wide,secondsPerPixel:secondsPerPixel};
1022
+ }
1023
+
1024
+ /* Block-indexed min/max summaries for RF, gradients, and ADC occupancy. */
1025
+ function createWaveformOverview(blocks){
1026
+ if(!blocks||!blocks.length)return null;
1027
+ var level=buildWaveformOverviewLevel(blocks,1),levels=[level];
1028
+ while(level.count>1){level=mergeWaveformOverviewLevel(level);levels.push(level);}
1029
+ return{levels:levels,blockCount:blocks.length,pointPrefix:createWaveformPointPrefixes(blocks),rfEvents:createRfEventOverview(blocks)};
1030
+ }
1031
+
1032
+ function createEmptyWaveformOverviewLevel(count,bucketSize){
1033
+ var level={count:count,bucketSize:bucketSize,t0:new Float64Array(count),t1:new Float64Array(count),
1034
+ rfMin:new Float64Array(count),rfMax:new Float64Array(count),gxMin:new Float64Array(count),gxMax:new Float64Array(count),
1035
+ rfStart:new Float64Array(count),rfEnd:new Float64Array(count),
1036
+ gyMin:new Float64Array(count),gyMax:new Float64Array(count),gzMin:new Float64Array(count),gzMax:new Float64Array(count),
1037
+ gxStart:new Float64Array(count),gxEnd:new Float64Array(count),gyStart:new Float64Array(count),gyEnd:new Float64Array(count),gzStart:new Float64Array(count),gzEnd:new Float64Array(count),
1038
+ adcStart:new Float64Array(count),adcEnd:new Float64Array(count)};
1039
+ var mins=[level.rfMin,level.rfStart,level.gxMin,level.gyMin,level.gzMin,level.gxStart,level.gyStart,level.gzStart,level.adcStart];
1040
+ var maxs=[level.rfMax,level.rfEnd,level.gxMax,level.gyMax,level.gzMax,level.gxEnd,level.gyEnd,level.gzEnd,level.adcEnd];
1041
+ for(var m=0;m<mins.length;m++)mins[m].fill(Infinity);
1042
+ for(var x=0;x<maxs.length;x++)maxs[x].fill(-Infinity);
1043
+ return level;
1044
+ }
1045
+
1046
+ function createWaveformPointPrefixes(blocks){
1047
+ var keys=['rf','rfEvents','phase','gx','gy','gz','adc'],prefix={};
1048
+ for(var ki=0;ki<keys.length;ki++)prefix[keys[ki]]=new Float64Array(blocks.length+1);
1049
+ for(var i=0;i<blocks.length;i++){
1050
+ var block=blocks[i],rf=block.rf,adc=block.adc;
1051
+ prefix.rf[i+1]=prefix.rf[i]+(rf&&rf.t&&rf.m?Math.min(rf.t.length,rf.m.length):0);
1052
+ prefix.rfEvents[i+1]=prefix.rfEvents[i]+(rf?1:0);
1053
+ var rfPhase=rf&&(rf.pt||rf.t)&&rf.p?Math.min((rf.pt||rf.t).length,rf.p.length):0,adcPhase=0;
1054
+ if(adc&&adc.n>1){var step=Math.max(1,Math.ceil(adc.n/200));adcPhase=Math.floor(adc.n/step)+1;}
1055
+ prefix.phase[i+1]=prefix.phase[i]+rfPhase+adcPhase;
1056
+ prefix.gx[i+1]=prefix.gx[i]+overviewGradientPointCount(block.gx);
1057
+ prefix.gy[i+1]=prefix.gy[i]+overviewGradientPointCount(block.gy);
1058
+ prefix.gz[i+1]=prefix.gz[i]+overviewGradientPointCount(block.gz);
1059
+ prefix.adc[i+1]=prefix.adc[i]+(adc?1:0);
1060
+ }
1061
+ return prefix;
1062
+ }
1063
+
1064
+ function overviewGradientPointCount(gradient){
1065
+ return gradient&&gradient.ty!=='none'&&gradient.t&&gradient.w?Math.min(gradient.t.length,gradient.w.length):0;
1066
+ }
1067
+
1068
+ function includeOverviewValues(values,minArray,maxArray,bucket){
1069
+ if(!values)return;
1070
+ for(var i=0;i<values.length;i++){
1071
+ var value=values[i];if(!isFinite(value))continue;
1072
+ if(value<minArray[bucket])minArray[bucket]=value;
1073
+ if(value>maxArray[bucket])maxArray[bucket]=value;
1074
+ }
1075
+ }
1076
+
1077
+ function includeOverviewRf(rf,level,bucket){
1078
+ if(!rf)return;
1079
+ includeOverviewValues(rf.m,level.rfMin,level.rfMax,bucket);
1080
+ if(level.rfMin[bucket]===Infinity)includeOverviewValues([rf.a||0],level.rfMin,level.rfMax,bucket);
1081
+ var start=isFinite(rf.s)?rf.s:(rf.t&&rf.t.length?rf.t[0]:NaN),end=isFinite(rf.d)&&isFinite(start)?start+rf.d:(rf.t&&rf.t.length?rf.t[rf.t.length-1]:NaN);
1082
+ if(isFinite(start)&&start<level.rfStart[bucket])level.rfStart[bucket]=start;
1083
+ if(isFinite(end)&&end>level.rfEnd[bucket])level.rfEnd[bucket]=end;
1084
+ }
1085
+
1086
+ function includeOverviewGradient(gradient,level,key,bucket){
1087
+ if(!gradient||gradient.ty==='none'||!gradient.t||!gradient.w)return;
1088
+ var n=Math.min(gradient.t.length,gradient.w.length);if(n<2)return;
1089
+ var maxAbs=0;for(var i=0;i<n;i++)if(isFinite(gradient.w[i]))maxAbs=Math.max(maxAbs,Math.abs(gradient.w[i]));
1090
+ var epsilon=Math.max(1e-12,maxAbs*1e-12),minArray=level[key+'Min'],maxArray=level[key+'Max'],startArray=level[key+'Start'],endArray=level[key+'End'];
1091
+ for(var segment=0;segment<n-1;segment++){
1092
+ var t0=gradient.t[segment],t1=gradient.t[segment+1],v0=gradient.w[segment],v1=gradient.w[segment+1];
1093
+ if(!isFinite(t0)||!isFinite(t1)||!isFinite(v0)||!isFinite(v1)||t1<t0)continue;
1094
+ if(Math.abs(v0)<=epsilon&&Math.abs(v1)<=epsilon)continue;
1095
+ if(t0<startArray[bucket])startArray[bucket]=t0;if(t1>endArray[bucket])endArray[bucket]=t1;
1096
+ if(v0<minArray[bucket])minArray[bucket]=v0;if(v1<minArray[bucket])minArray[bucket]=v1;
1097
+ if(v0>maxArray[bucket])maxArray[bucket]=v0;if(v1>maxArray[bucket])maxArray[bucket]=v1;
1098
+ }
1099
+ }
1100
+
1101
+ function buildWaveformOverviewLevel(blocks,bucketSize){
1102
+ var count=Math.ceil(blocks.length/bucketSize),level=createEmptyWaveformOverviewLevel(count,bucketSize);
1103
+ for(var bucket=0;bucket<count;bucket++){
1104
+ var start=bucket*bucketSize,end=Math.min(blocks.length,start+bucketSize);
1105
+ level.t0[bucket]=blocks[start].s;level.t1[bucket]=blocks[end-1].s+blocks[end-1].d;
1106
+ for(var bi=start;bi<end;bi++){
1107
+ var block=blocks[bi],rf=block.rf;
1108
+ if(rf)includeOverviewRf(rf,level,bucket);
1109
+ includeOverviewGradient(block.gx,level,'gx',bucket);includeOverviewGradient(block.gy,level,'gy',bucket);includeOverviewGradient(block.gz,level,'gz',bucket);
1110
+ if(block.adc){
1111
+ var adcStart=block.adc.s+block.adc.d,adcEnd=adcStart+block.adc.n*block.adc.dw;
1112
+ if(adcStart<level.adcStart[bucket])level.adcStart[bucket]=adcStart;
1113
+ if(adcEnd>level.adcEnd[bucket])level.adcEnd[bucket]=adcEnd;
1114
+ }
1115
+ }
1116
+ }
1117
+ return level;
1118
+ }
1119
+
1120
+ function mergeWaveformOverviewLevel(child){
1121
+ var count=Math.ceil(child.count/4),level=createEmptyWaveformOverviewLevel(count,child.bucketSize*4);
1122
+ var channels=[['rfMin','rfMax'],['rfStart','rfEnd'],['gxMin','gxMax'],['gyMin','gyMax'],['gzMin','gzMax'],['gxStart','gxEnd'],['gyStart','gyEnd'],['gzStart','gzEnd'],['adcStart','adcEnd']];
1123
+ for(var bucket=0;bucket<count;bucket++){
1124
+ var start=bucket*4,end=Math.min(child.count,start+4);
1125
+ level.t0[bucket]=child.t0[start];level.t1[bucket]=child.t1[end-1];
1126
+ for(var ci=0;ci<channels.length;ci++){
1127
+ var minKey=channels[ci][0],maxKey=channels[ci][1];
1128
+ for(var i=start;i<end;i++){
1129
+ if(child[minKey][i]<level[minKey][bucket])level[minKey][bucket]=child[minKey][i];
1130
+ if(child[maxKey][i]>level[maxKey][bucket])level[maxKey][bucket]=child[maxKey][i];
1131
+ }
1132
+ }
1133
+ }
1134
+ return level;
1135
+ }
1136
+
1137
+ function waveformVisiblePointCount(overview,key,startBlock,endBlock){
1138
+ if(!overview||!overview.pointPrefix||!overview.pointPrefix[key])return 0;
1139
+ var prefix=overview.pointPrefix[key],start=Math.max(0,Math.min(startBlock,prefix.length-1)),end=Math.max(start,Math.min(endBlock,prefix.length-1));
1140
+ return prefix[end]-prefix[start];
1141
+ }
1142
+
1143
+ function waveformVisibleGradientPointCount(blocks,key,startBlock,endBlock,viewStart,viewEnd){
1144
+ var count=0;
1145
+ for(var blockIndex=startBlock;blockIndex<endBlock;blockIndex++){
1146
+ var gradient=blocks[blockIndex]&&blocks[blockIndex][key];
1147
+ if(!gradient||gradient.ty==='none'||!gradient.t||!gradient.w)continue;
1148
+ var n=Math.min(gradient.t.length,gradient.w.length);
1149
+ if(n<1)continue;
1150
+ var first=lowerBoundSeries(gradient.t,viewStart),last=upperBoundSeries(gradient.t,viewEnd);
1151
+ count+=Math.max(0,Math.min(n,last)-Math.min(n,first));
1152
+ }
1153
+ return count;
1154
+ }
1155
+
1156
+ function selectWaveformOverview(overview,startBlock,endBlock,maxBuckets){
1157
+ if(!overview||endBlock<=startBlock)return null;
1158
+ var selected=overview.levels[overview.levels.length-1];
1159
+ for(var i=0;i<overview.levels.length;i++){
1160
+ var candidate=overview.levels[i];
1161
+ var count=Math.ceil(endBlock/candidate.bucketSize)-Math.floor(startBlock/candidate.bucketSize);
1162
+ if(count<=maxBuckets){selected=candidate;break;}
1163
+ }
1164
+ return{level:selected,first:Math.floor(startBlock/selected.bucketSize),last:Math.min(selected.count,Math.ceil(endBlock/selected.bucketSize))};
1165
+ }
1166
+
1167
+
1168
+ /* ══ drawing.js ══ */
1169
+ /* ═══════════════════════════════════════════════════════════════════════
1170
+ Main draw loop
1171
+ ═══════════════════════════════════════════════════════════════════════ */
1172
+ function draw(){
1173
+ var drawStarted=performance.now();derivedRenderPointCount=0;derivedEnvelopeCurveCount=0;derivedRawCurveCount=0;rfRenderPointCount=0;rfRawCurveCount=0;rfReducedCurveCount=0;rfOverviewBucketCount=0;viewerDrawCount++;
1174
+ var w=mc.width/(window.devicePixelRatio||1),h=mc.height/(window.devicePixelRatio||1);
1175
+ var s=getComputedStyle(document.body);
1176
+ ctx.clearRect(0,0,w,h);
1177
+ ctx.fillStyle=s.getPropertyValue('--bg').trim();ctx.fillRect(0,0,w,h);
1178
+ var visibleRange=visibleDuration();var vs=ox,ve=ox+visibleRange;
1179
+ drawGrid(w,h,vs,ve,s);
1180
+ drawZeroLines(w,h,s);
1181
+ if(showBB)drawBlockBounds(w,h,vs,ve,s);
1182
+ drawBlocks(vs,ve,s);
1183
+ drawDerivedChannels(vs,ve,s);
1184
+ drawAxes(w,h,vs,ve,s);
1185
+ lastDrawDurationMs=performance.now()-drawStarted;
1186
+ drawCursorOverlay();
1187
+ }
1188
+
1189
+ /* ── Vertical cursor ──────────────────────────────────────────────────── */
1190
+ function drawCursorOverlay(){
1191
+ viewerCursorDrawCount++;
1192
+ var dpr=window.devicePixelRatio||1,w=moc.width/dpr,h=moc.height/dpr;
1193
+ moctx.clearRect(0,0,w,h);
1194
+ if(!cursorActive)return;var cx=t2x(cursorT);
1195
+ if(cx<M.l||cx>w-M.r)return;
1196
+ var color=getComputedStyle(document.body).getPropertyValue('--cr').trim();
1197
+ moctx.strokeStyle=color;moctx.lineWidth=0.8;moctx.setLineDash([4,3]);
1198
+ moctx.beginPath();moctx.moveTo(cx,M.t);moctx.lineTo(cx,h-M.b);moctx.stroke();moctx.setLineDash([]);
1199
+ moctx.fillStyle=color;moctx.font='10px monospace';moctx.textAlign='center';
1200
+ var label=fmtT(timeConv(cursorT)),tw=moctx.measureText(label).width;
1201
+ moctx.fillText(label,Math.max(M.l+tw/2+4,Math.min(cx,w-M.r-tw/2-4)),M.t-1);
1202
+ }
1203
+
1204
+ /* ── Zero lines & grid ────────────────────────────────────────────────── */
1205
+ function drawZeroLines(w,h,s){
1206
+ ctx.strokeStyle=s.getPropertyValue('--gr').trim();ctx.lineWidth=0.4;ctx.setLineDash([2,4]);
1207
+ 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();}}
1208
+ ctx.setLineDash([]);
1209
+ }
1210
+ function drawBlockBounds(w,h,vs,ve,s){
1211
+ ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=0.6;ctx.setLineDash([3,6]);
1212
+ var range=visibleBlockRange(vs,ve);
1213
+ for(var i=range.start;i<range.end;i++){
1214
+ var x=t2x(BL[i].s);if(x<M.l||x>w-M.r)continue;
1215
+ ctx.beginPath();ctx.moveTo(x,M.t);ctx.lineTo(x,h-M.b);ctx.stroke();
1216
+ ctx.fillStyle=s.getPropertyValue('--ax').trim();ctx.font='9px monospace';ctx.textAlign='center';
1217
+ ctx.fillText('#'+BL[i].i,x,M.t-2);
1218
+ }
1219
+ ctx.setLineDash([]);
1220
+ }
1221
+ function drawGrid(w,h,vs,ve,s){
1222
+ ctx.strokeStyle=s.getPropertyValue('--gr').trim();ctx.lineWidth=0.5;
1223
+ var st=nice((ve-vs)/8),t=Math.floor(vs/st)*st;
1224
+ while(t<=ve){var x=t2x(t);if(x>=M.l){ctx.beginPath();ctx.moveTo(x,M.t);ctx.lineTo(x,h-M.b);ctx.stroke();}t+=st;}
1225
+ var ch=cH(),vc=visChannels();
1226
+ 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();}
1227
+ }
1228
+
1229
+ function channelRange(ci){
1230
+ var z=ampZoom[ci]||1;
1231
+ if(ci===0)return Math.max((gMax[0]||1)*z,1e-9);
1232
+ if(ci===1)return Math.max(6.28318*z,1e-9);
1233
+ if(ci>=2&&ci<=4)return Math.max((gMax[ci]||1)*z,0.001);
1234
+ if(ci===7)return Math.max((gMax[7]||1)*z,0.001);
1235
+ if(ci>=8&&ci<=10)return Math.max((gMax[ci]||1)*z,1e-12);
1236
+ return 1;
1237
+ }
1238
+ function fmtPhase(v){if(Math.abs(v-6.28318)<1e-3)return'2\u03c0';if(Math.abs(v-3.14159)<1e-3)return'\u03c0';if(v>=1)return v.toFixed(2);if(v>=0.01)return v.toFixed(3);return v.toExponential(1);}
1239
+ function rowClip(vi,ch,fn){
1240
+ 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);
1241
+ if(rw<=0||bottom<=top)return;
1242
+ ctx.save();ctx.beginPath();ctx.rect(M.l,top,rw,bottom-top);ctx.clip();
1243
+ try{fn();}finally{ctx.restore();}
1244
+ }
1245
+
1246
+ /* ── Axes (X time + Y per channel) ────────────────────────────────────── */
1247
+ function drawAxes(w,h,vs,ve,s){
1248
+ var narrow=(typeof layoutMode!=='undefined'&&layoutMode==='vertical')||w<600;
1249
+ var axFont=narrow?'9px monospace':'10px monospace';
1250
+ var chFont=narrow?'bold 14px monospace':'bold 18px monospace';
1251
+ // X-axis
1252
+ ctx.fillStyle=s.getPropertyValue('--lb').trim();ctx.font=axFont;ctx.textAlign='center';
1253
+ ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=1;
1254
+ ctx.beginPath();ctx.moveTo(M.l,h-M.b);ctx.lineTo(w-M.r,h-M.b);ctx.stroke();
1255
+ var st=nice((ve-vs)/8),t=Math.floor(vs/st)*st;
1256
+ while(t<=ve){ctx.fillText(fmtT(timeConv(t)),t2x(t),h-M.b+14);t+=st;}
1257
+ ctx.textAlign='right';
1258
+ ctx.fillText('time ('+timeUnitStr()+')',w-4,h-M.b+4);
1259
+
1260
+ // Y-axes — one per visible channel
1261
+ var vc=visChannels(),ch=cH();
1262
+ for(var vi=0;vi<vc.length;vi++){
1263
+ var ci=vc[vi],y0=cy(vi);
1264
+ // Channel label (bold, coloured) — left-aligned on narrow to avoid off-screen overflow
1265
+ if(narrow){
1266
+ ctx.textAlign='left';ctx.font=chFont;
1267
+ ctx.fillStyle=chColors[ci];ctx.fillText(CH[ci],4,y0+4);
1268
+ }else{
1269
+ ctx.textAlign='right';ctx.font=chFont;
1270
+ ctx.fillStyle=chColors[ci];ctx.fillText(CH[ci],M.l-40,y0+4);
1271
+ }
1272
+ // Tick values — left-aligned on narrow to stay within reduced margin
1273
+ ctx.fillStyle=s.getPropertyValue('--lb').trim();ctx.font=axFont;
1274
+ var lblX=narrow?4:M.l-12;
1275
+ ctx.textAlign=narrow?'left':'right';
1276
+ if(ci===0){ctx.fillText(fmtAmp(channelRange(0))+'Hz',lblX,M.t+vi*ch+12);ctx.fillText('0',lblX,y0+ch/2-2);}
1277
+ 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);}
1278
+ else if(ci>=2&&ci<=4){var d=gradConv(channelRange(ci));ctx.fillText('\u00b1'+fmtAmp(d)+gradUnitStr(),lblX,M.t+vi*ch+12);ctx.fillText('0',lblX,y0+4);}
1279
+ else if(ci===5){ctx.fillText('on',lblX,M.t+vi*ch+12);}
1280
+ else if(ci===6){ctx.fillText('ch',lblX,M.t+vi*ch+12);}
1281
+ 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);}
1282
+ else if(ci>=8&&ci<=10){ctx.fillText('\u00b1'+fmtAmp(channelRange(ci))+'s/m',lblX,M.t+vi*ch+12);ctx.fillText('0',lblX,y0+4);}
1283
+
1284
+ // Small tick marks
1285
+ ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=0.5;
1286
+ ctx.beginPath();ctx.moveTo(M.l-2,M.t+vi*ch);ctx.lineTo(M.l+2,M.t+vi*ch);ctx.stroke();
1287
+ ctx.beginPath();ctx.moveTo(M.l-2,y0);ctx.lineTo(M.l+2,y0);ctx.stroke();
1288
+ ctx.beginPath();ctx.moveTo(M.l-2,M.t+(vi+1)*ch);ctx.lineTo(M.l+2,M.t+(vi+1)*ch);ctx.stroke();
1289
+ }
1290
+ }
1291
+
1292
+ /* ── Derived calculation overlays: PNS and M1 ────────────────────────── */
1293
+ function drawDerivedChannels(vs,ve,s){
1294
+ var ch=cH(),vc=visChannels();
1295
+ var viP=-1,viM1x=-1,viM1y=-1,viM1z=-1;
1296
+ for(var vi=0;vi<vc.length;vi++){
1297
+ if(vc[vi]===7)viP=vi;if(vc[vi]===8)viM1x=vi;if(vc[vi]===9)viM1y=vi;if(vc[vi]===10)viM1z=vi;
1298
+ }
1299
+ var pnsDrawData=pnsSeriesForView(vs,ve);
1300
+ if(pnsDrawData&&viP>=0){
1301
+ drawPercentSeries(pnsDrawData.x,viP,7,s.getPropertyValue('--gx').trim(),ch,vs,ve);
1302
+ drawPercentSeries(pnsDrawData.y,viP,7,s.getPropertyValue('--gy').trim(),ch,vs,ve);
1303
+ drawPercentSeries(pnsDrawData.z,viP,7,s.getPropertyValue('--gz').trim(),ch,vs,ve);
1304
+ ctx.setLineDash([5,3]);
1305
+ drawPercentSeries(pnsDrawData.n,viP,7,s.getPropertyValue('--fg').trim(),ch,vs,ve);
1306
+ ctx.setLineDash([]);
1307
+ }
1308
+ var m1DrawData=m1SeriesForView(vs,ve);
1309
+ if(m1DrawData){
1310
+ if(viM1x>=0)drawBipolarSeries(m1DrawData.x,viM1x,8,s.getPropertyValue('--gx').trim(),ch,vs,ve);
1311
+ if(viM1y>=0)drawBipolarSeries(m1DrawData.y,viM1y,9,s.getPropertyValue('--gy').trim(),ch,vs,ve);
1312
+ if(viM1z>=0)drawBipolarSeries(m1DrawData.z,viM1z,10,s.getPropertyValue('--gz').trim(),ch,vs,ve);
1313
+ }
1314
+ }
1315
+
1316
+ function groupEnvelopeRanges(ranges,maxGapPx){
1317
+ var groups=[],group=[];
1318
+ for(var i=0;i<ranges.length;i++){
1319
+ if(group.length&&ranges[i][0]-group[group.length-1][1]>maxGapPx){groups.push(group);group=[];}
1320
+ group.push(ranges[i]);
1321
+ }
1322
+ if(group.length)groups.push(group);return groups;
1323
+ }
1324
+
1325
+ function drawStepEnvelopeGroups(groups,color,fillAlpha){
1326
+ for(var groupIndex=0;groupIndex<groups.length;groupIndex++){
1327
+ var ranges=groups[groupIndex];if(!ranges.length)continue;
1328
+ ctx.fillStyle=color;ctx.globalAlpha=fillAlpha;ctx.beginPath();ctx.moveTo(ranges[0][0],ranges[0][2]);
1329
+ for(var upperIndex=0;upperIndex<ranges.length;upperIndex++){ctx.lineTo(ranges[upperIndex][0],ranges[upperIndex][2]);ctx.lineTo(ranges[upperIndex][1],ranges[upperIndex][2]);}
1330
+ for(var lowerIndex=ranges.length-1;lowerIndex>=0;lowerIndex--){ctx.lineTo(ranges[lowerIndex][1],ranges[lowerIndex][3]);ctx.lineTo(ranges[lowerIndex][0],ranges[lowerIndex][3]);}
1331
+ ctx.closePath();ctx.fill();
1332
+ ctx.strokeStyle=color;ctx.lineWidth=.8;ctx.globalAlpha=.72;ctx.beginPath();ctx.moveTo(ranges[0][0],ranges[0][2]);
1333
+ for(var upper=0;upper<ranges.length;upper++){ctx.lineTo(ranges[upper][0],ranges[upper][2]);ctx.lineTo(ranges[upper][1],ranges[upper][2]);}ctx.stroke();
1334
+ ctx.globalAlpha=.38;ctx.beginPath();ctx.moveTo(ranges[0][0],ranges[0][3]);
1335
+ for(var lower=0;lower<ranges.length;lower++){ctx.lineTo(ranges[lower][0],ranges[lower][3]);ctx.lineTo(ranges[lower][1],ranges[lower][3]);}ctx.stroke();ctx.globalAlpha=1;
1336
+ }
1337
+ }
1338
+
1339
+ function drawPercentSeries(series,vi,ci,c,ch,vs,ve){
1340
+ if(!series||series.n<(series.kind==='envelope'?1:2))return;
1341
+ rowClip(vi,ch,function(){
1342
+ var maxA=channelRange(ci),base=M.t+(vi+1)*ch-ch*.08,scale=ch*.84/maxA;
1343
+ var clipL=M.l,clipR=mc.width/(window.devicePixelRatio||1)-M.r;
1344
+ var plotW=Math.max(1,clipR-clipL);
1345
+ if(series.kind==='envelope'){
1346
+ derivedEnvelopeCurveCount++;
1347
+ var ranges=[];
1348
+ var count=forEachEnvelopeRange(series,vs,ve,Math.max(32,Math.ceil(plotW)),function(t0,t1,minValue,maxValue){
1349
+ var x0=t2x(t0),x1=t2x(t1),upper=base-maxValue*scale,lower=base-minValue*scale;
1350
+ if(isFinite(x0)&&isFinite(x1)&&isFinite(upper)&&isFinite(lower)&&x1>=clipL&&x0<=clipR)ranges.push([Math.max(clipL,x0),Math.min(clipR,x1),upper,lower]);
1351
+ });
1352
+ derivedRenderPointCount+=count*4;drawStepEnvelopeGroups(groupEnvelopeRanges(ranges,1),c,.12);return;
1353
+ }
1354
+ var rawLimit=Math.max(64,Math.ceil(plotW*2));
1355
+ var fineLimit=Math.max(128,Math.ceil(plotW*8));
1356
+ var windowRange=derivedSeriesWindow(series,vs,ve);
1357
+ var useFinePns=shouldUseFinePnsRendering(windowRange.count,plotW,vs,ve);
1358
+ ctx.strokeStyle=c;ctx.lineWidth=1;ctx.beginPath();
1359
+ if(windowRange.count<=rawLimit||useFinePns){
1360
+ derivedRawCurveCount++;
1361
+ var f=1;
1362
+ derivedRenderPointCount+=forEachDerivedPoint(series,vs,ve,useFinePns?fineLimit:rawLimit,function(tv,vv){
1363
+ var sx=t2x(tv);if(!isFinite(sx)||sx<clipL||sx>clipR){f=1;return}
1364
+ var sy=base-vv*scale;if(!isFinite(sy)){f=1;return}
1365
+ if(f){ctx.moveTo(sx,sy);f=0}else ctx.lineTo(sx,sy);
1366
+ });
1367
+ }else{
1368
+ derivedEnvelopeCurveCount++;
1369
+ var first=1;
1370
+ derivedRenderPointCount+=forEachDerivedRange(series,vs,ve,Math.max(32,Math.ceil(clipR-clipL)),function(tv,_minValue,maxValue){
1371
+ var sx=t2x(tv),sy=base-maxValue*scale;
1372
+ if(!isFinite(sx)||!isFinite(sy)||sx<clipL||sx>clipR){first=1;return}
1373
+ if(first){ctx.moveTo(sx,sy);first=0}else ctx.lineTo(sx,sy);
1374
+ });
1375
+ }
1376
+ ctx.stroke();
1377
+ });
1378
+ }
1379
+
1380
+ function shouldUseFinePnsRendering(count,plotW,vs,ve){
1381
+ var dur=ve-vs;
1382
+ if(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*10.5)return true;
1383
+ return count<=Math.max(256,Math.ceil(plotW*12));
1384
+ }
1385
+
1386
+ function drawBipolarSeries(series,vi,ci,c,ch,vs,ve){
1387
+ if(!series||series.n<(series.kind==='envelope'?1:2))return;
1388
+ rowClip(vi,ch,function(){
1389
+ var maxA=channelRange(ci),y=cy(vi),scale=ch*.4/maxA;
1390
+ var clipL=M.l,clipR=mc.width/(window.devicePixelRatio||1)-M.r;
1391
+ if(series.kind==='envelope'){
1392
+ derivedEnvelopeCurveCount++;
1393
+ var envelopeRanges=[];
1394
+ var envelopeCount=forEachEnvelopeRange(series,vs,ve,Math.max(32,Math.ceil(clipR-clipL)),function(t0,t1,minValue,maxValue){
1395
+ var x0=t2x(t0),x1=t2x(t1),upper=y-maxValue*scale,lower=y-minValue*scale;
1396
+ if(isFinite(x0)&&isFinite(x1)&&isFinite(upper)&&isFinite(lower)&&x1>=clipL&&x0<=clipR)envelopeRanges.push([Math.max(clipL,x0),Math.min(clipR,x1),upper,lower]);
1397
+ });
1398
+ derivedRenderPointCount+=envelopeCount*4;drawStepEnvelopeGroups(groupEnvelopeRanges(envelopeRanges,1),c,.14);return;
1399
+ }
1400
+ var rawLimit=Math.max(128,Math.ceil((clipR-clipL)*8));
1401
+ if(derivedSeriesWindow(series,vs,ve).count<=rawLimit){
1402
+ derivedRawCurveCount++;
1403
+ ctx.strokeStyle=c;ctx.lineWidth=1;ctx.beginPath();var f=1;
1404
+ derivedRenderPointCount+=forEachDerivedPoint(series,vs,ve,rawLimit,function(tv,vv){
1405
+ var sx=t2x(tv);if(!isFinite(sx)||sx<clipL||sx>clipR){f=1;return}
1406
+ var sy=y-vv*scale;if(!isFinite(sy)){f=1;return}
1407
+ if(f){ctx.moveTo(sx,sy);f=0}else ctx.lineTo(sx,sy);
1408
+ });
1409
+ ctx.stroke();
1410
+ }else{
1411
+ derivedEnvelopeCurveCount++;
1412
+ var ranges=[];
1413
+ var rangeCount=forEachDerivedRange(series,vs,ve,Math.max(32,Math.ceil(clipR-clipL)),function(tv,minValue,maxValue){
1414
+ var sx=t2x(tv),upper=y-maxValue*scale,lower=y-minValue*scale;
1415
+ if(isFinite(sx)&&isFinite(upper)&&isFinite(lower)&&sx>=clipL&&sx<=clipR)ranges.push([sx,upper,lower]);
1416
+ });
1417
+ derivedRenderPointCount+=rangeCount*2;
1418
+ if(ranges.length){
1419
+ ctx.fillStyle=c;ctx.globalAlpha=.14;ctx.beginPath();
1420
+ ctx.moveTo(ranges[0][0],ranges[0][1]);
1421
+ for(var ri=1;ri<ranges.length;ri++)ctx.lineTo(ranges[ri][0],ranges[ri][1]);
1422
+ for(var rj=ranges.length-1;rj>=0;rj--)ctx.lineTo(ranges[rj][0],ranges[rj][2]);
1423
+ ctx.closePath();ctx.fill();
1424
+ ctx.globalAlpha=.65;ctx.strokeStyle=c;ctx.lineWidth=.8;
1425
+ ctx.beginPath();ctx.moveTo(ranges[0][0],(ranges[0][1]+ranges[0][2])*.5);
1426
+ for(var rk=1;rk<ranges.length;rk++)ctx.lineTo(ranges[rk][0],(ranges[rk][1]+ranges[rk][2])*.5);
1427
+ ctx.stroke();ctx.globalAlpha=1;
1428
+ }
1429
+ }
1430
+ });
1431
+ }
1432
+
1433
+ /* ═══════════════════════════════════════════════════════════════════════
1434
+ Block waveform rendering
1435
+ ═══════════════════════════════════════════════════════════════════════ */
1436
+ function drawBlocks(vs,ve,s){
1437
+ var range=visibleBlockRange(vs,ve);if(range.start>=range.end)return;
1438
+ var ch=cH(),vc=visChannels(),rows=[-1,-1,-1,-1,-1,-1,-1];
1439
+ for(var vi=0;vi<vc.length;vi++)if(vc[vi]>=0&&vc[vi]<=6)rows[vc[vi]]=vi;
1440
+ var colors={
1441
+ rf:s.getPropertyValue('--rf').trim(),rff:s.getPropertyValue('--rff').trim(),
1442
+ gx:s.getPropertyValue('--gx').trim(),gy:s.getPropertyValue('--gy').trim(),gz:s.getPropertyValue('--gz').trim(),
1443
+ adc:s.getPropertyValue('--adc').trim(),adf:s.getPropertyValue('--adf').trim(),
1444
+ tr:s.getPropertyValue('--tr').trim(),fg:s.getPropertyValue('--fg').trim()
1445
+ };
1446
+ var pixelBudget=Math.max(1,Math.floor(plotWidth()));
1447
+ var overview=selectWaveformOverview(waveformOverview,range.start,range.end,pixelBudget);
1448
+ function useOverview(key){return !!overview&&waveformVisiblePointCount(waveformOverview,key,range.start,range.end)>pixelBudget;}
1449
+ function useGradientOverview(key){return !!overview&&waveformVisibleGradientPointCount(BL,key,range.start,range.end,vs,ve)>pixelBudget*8;}
1450
+ var rfPoints=waveformVisiblePointCount(waveformOverview,'rf',range.start,range.end),rfEvents=waveformVisiblePointCount(waveformOverview,'rfEvents',range.start,range.end);
1451
+ var aggregateRf=!!overview&&rfEvents>pixelBudget*2,reduceRf=aggregateRf||rfPoints>pixelBudget*8;
1452
+ var overviewUse={rf:reduceRf,phase:useOverview('phase'),gx:useGradientOverview('gx'),gy:useGradientOverview('gy'),gz:useGradientOverview('gz'),adc:useOverview('adc')};
1453
+ var dense=overviewUse.rf||overviewUse.phase||overviewUse.gx||overviewUse.gy||overviewUse.gz||overviewUse.adc;
1454
+ waveformOverviewActive=dense;
1455
+ setViewerNotice('dense',dense?'Dense overview mode is active. Zoom in for full waveform detail.':null);
1456
+ if(rows[0]>=0){if(aggregateRf)drawRfOverview(overview,rows[0],ch,colors,vs,ve);else drawRfBlocks(range.start,range.end,rows[0],ch,colors,vs,ve,pixelBudget*8);}
1457
+ if(rows[1]>=0){if(overviewUse.phase)drawPhaseSampled(range.start,range.end,rows[1],ch,colors,vs,ve,pixelBudget);else drawPhaseBlocks(range.start,range.end,rows[1],ch,colors,vs,ve);}
1458
+ if(rows[2]>=0){if(overviewUse.gx)drawGradientOverview(overview,'gx',rows[2],2,ch,colors.gx,vs,ve);else drawGradientBlocks(range.start,range.end,'gx',rows[2],2,ch,colors.gx,vs,ve);}
1459
+ if(rows[3]>=0){if(overviewUse.gy)drawGradientOverview(overview,'gy',rows[3],3,ch,colors.gy,vs,ve);else drawGradientBlocks(range.start,range.end,'gy',rows[3],3,ch,colors.gy,vs,ve);}
1460
+ if(rows[4]>=0){if(overviewUse.gz)drawGradientOverview(overview,'gz',rows[4],4,ch,colors.gz,vs,ve);else drawGradientBlocks(range.start,range.end,'gz',rows[4],4,ch,colors.gz,vs,ve);}
1461
+ if(rows[5]>=0){if(overviewUse.adc)drawAdcOverview(overview,rows[5],ch,colors,vs,ve);else drawAdcBlocks(range.start,range.end,rows[5],ch,colors,vs,ve);}
1462
+ if(rows[6]>=0&&range.end-range.start<=pixelBudget)drawTriggerBlocks(range.start,range.end,rows[6],ch,colors,vs,ve);
1463
+ }
1464
+
1465
+ function drawRfOverview(summary,vi,ch,colors,vs,ve){
1466
+ var y=cy(vi),base=y+ch*.45,scale=ch*.9/channelRange(0),pixelCount=Math.max(1,Math.ceil(plotWidth()));
1467
+ var bins=binRfEvents(waveformOverview.rfEvents,vs,ve,pixelCount,2),series=waveformOverview.rfEvents;
1468
+ rowClip(vi,ch,function(){
1469
+ ctx.fillStyle=colors.rf;ctx.globalAlpha=.28;
1470
+ for(var bin=0;bin<bins.peak.length;bin++){
1471
+ if(bins.events[bin]<1||bins.peak[bin]<=0)continue;
1472
+ var mean=Math.min(bins.peak[bin],bins.area[bin]/bins.secondsPerPixel),x=t2x(vs+(bin+.5)*bins.secondsPerPixel);
1473
+ if(mean>0)ctx.fillRect(x-.5,base-mean*scale,1,Math.max(1,mean*scale));
1474
+ }
1475
+ ctx.globalAlpha=.75;ctx.strokeStyle=colors.rf;ctx.lineWidth=1.1;
1476
+ for(var peakBin=0;peakBin<bins.peak.length;peakBin++){
1477
+ if(bins.events[peakBin]<1||bins.peak[peakBin]<=0)continue;
1478
+ var leftX=t2x(bins.occupiedStart[peakBin]),rightX=t2x(bins.occupiedEnd[peakBin]),peakX=t2x(bins.peakTime[peakBin]);
1479
+ if(rightX-leftX<2){var centerX=(leftX+rightX)*.5;leftX=centerX-1;rightX=centerX+1;}
1480
+ peakX=Math.max(leftX+.25,Math.min(rightX-.25,peakX));
1481
+ ctx.beginPath();ctx.moveTo(leftX,base);ctx.lineTo(peakX,base-bins.peak[peakBin]*scale);ctx.lineTo(rightX,base);ctx.stroke();
1482
+ rfOverviewBucketCount++;rfRenderPointCount+=3;
1483
+ }
1484
+ ctx.globalAlpha=1;
1485
+ for(var wi=0;wi<bins.wide.length;wi++){
1486
+ var rf=series.waveforms[bins.wide[wi]],x0=t2x(Math.max(vs,rf.s)),x1=t2x(Math.min(ve,rf.s+rf.d));
1487
+ ctx.fillStyle=colors.rff;ctx.fillRect(x0,y-ch*.45,Math.max(1,x1-x0),ch*.9);
1488
+ var n=Math.min(rf.t?rf.t.length:0,rf.m?rf.m.length:0),pointBudget=Math.max(8,Math.ceil(Math.max(1,x1-x0)*4)),first=true;
1489
+ ctx.strokeStyle=colors.rf;ctx.lineWidth=1.1;ctx.beginPath();
1490
+ var emitted=rf.bp?appendBlockRfPath(rf,base,scale):forEachWaveformPoint(rf.t,rf.m,pointBudget,function(time,value){var sx=t2x(time),sy=base-value*scale;if(first){ctx.moveTo(sx,sy);first=false;}else ctx.lineTo(sx,sy);});
1491
+ if(emitted>1)ctx.stroke();rfRenderPointCount+=emitted;if(n<=pointBudget)rfRawCurveCount++;else rfReducedCurveCount++;
1492
+ }
1493
+ });
1494
+ }
1495
+
1496
+ function appendBlockRfPath(rf,base,scale){
1497
+ var amplitude=isFinite(rf.pk)?rf.pk:(rf.m&&rf.m.length?Math.abs(rf.m[0]):0),top=base-amplitude*scale,x0=t2x(rf.s),x1=t2x(rf.s+rf.d);
1498
+ ctx.moveTo(x0,base);ctx.lineTo(x0,top);ctx.lineTo(x1,top);ctx.lineTo(x1,base);return 4;
1499
+ }
1500
+
1501
+ function drawPhaseSampled(start,end,vi,ch,colors,vs,ve,maxPoints){
1502
+ var prefix=waveformOverview.pointPrefix.phase,first=prefix[start],last=prefix[end],count=last-first;
1503
+ if(count<=0)return;
1504
+ var y=cy(vi),scale=ch*.9/channelRange(1),sampleCount=Math.min(maxPoints,count),sampleStep=count/sampleCount;
1505
+ rowClip(vi,ch,function(){
1506
+ for(var sampleIndex=0;sampleIndex<sampleCount;sampleIndex++){
1507
+ var ordinal=Math.min(last-1,Math.floor(first+(sampleIndex+.5)*sampleStep)),lo=start,hi=end;
1508
+ while(lo<hi){var mid=(lo+hi)>>1;if(prefix[mid+1]<=ordinal)lo=mid+1;else hi=mid;}
1509
+ if(lo>=end)continue;
1510
+ var block=BL[lo],rf=block.rf,phaseTime=rf&&(rf.pt||rf.t),rfCount=phaseTime&&rf.p?Math.min(phaseTime.length,rf.p.length):0;
1511
+ var local=ordinal-prefix[lo],time=NaN,value=NaN,isAdc=false;
1512
+ if(local<rfCount){time=phaseTime[local];value=rf.p[local];}
1513
+ else if(block.adc&&block.adc.n>1){
1514
+ var adc=block.adc,adcStep=Math.max(1,Math.ceil(adc.n/200)),adcSample=(local-rfCount)*adcStep;
1515
+ var adcStart=adc.s+adc.d,adcEnd=adcStart+adc.n*adc.dw;
1516
+ time=adcSample<adc.n?adcStart+(adcSample+.5)*adc.dw:adcEnd;
1517
+ value=((adc.po||0)+6.283185*(adc.fo||0)*(time-adcStart))%6.28318;value=(value+6.28318)%6.28318;isAdc=true;
1518
+ }
1519
+ if(!isFinite(time)||!isFinite(value)||time<vs||time>ve)continue;
1520
+ ctx.fillStyle=isAdc?colors.adc:colors.rf;
1521
+ ctx.fillRect(t2x(time)-.5,y+ch*.45-value*scale-.5,1,1);
1522
+ }
1523
+ });
1524
+ }
1525
+
1526
+ function drawGradientOverview(summary,key,vi,ci,ch,color,vs,ve){
1527
+ var level=summary.level,minValues=level[key+'Min'],maxValues=level[key+'Max'],startValues=level[key+'Start'],endValues=level[key+'End'],y=cy(vi),scale=ch*.4/channelRange(ci);
1528
+ rowClip(vi,ch,function(){
1529
+ var ranges=[];
1530
+ for(var i=summary.first;i<summary.last;i++){
1531
+ if(endValues[i]<vs||startValues[i]>ve||minValues[i]===Infinity)continue;
1532
+ var x0=t2x(Math.max(vs,startValues[i])),x1=t2x(Math.min(ve,endValues[i]));
1533
+ ranges.push([x0,x1,y-maxValues[i]*scale,y-minValues[i]*scale]);
1534
+ }
1535
+ drawStepEnvelopeGroups(groupEnvelopeRanges(ranges,1),color,.14);
1536
+ });
1537
+ }
1538
+
1539
+ function drawAdcOverview(summary,vi,ch,colors,vs,ve){
1540
+ var level=summary.level,y=cy(vi);
1541
+ rowClip(vi,ch,function(){
1542
+ ctx.fillStyle=colors.adf;ctx.strokeStyle=colors.adc;ctx.lineWidth=1;ctx.beginPath();var hasRect=false;
1543
+ for(var i=summary.first;i<summary.last;i++){
1544
+ if(level.adcStart[i]===Infinity||level.adcEnd[i]<vs||level.adcStart[i]>ve)continue;
1545
+ var x0=t2x(Math.max(vs,level.adcStart[i])),x1=t2x(Math.min(ve,level.adcEnd[i]));
1546
+ ctx.rect(x0,y-ch*.28,Math.max(1,x1-x0),ch*.56);hasRect=true;
1547
+ }
1548
+ if(hasRect){ctx.fill();ctx.stroke();}
1549
+ });
1550
+ }
1551
+
1552
+ function visibleBlockRange(vs,ve){
1553
+ var lo=0,hi=BL.length,mid;
1554
+ while(lo<hi){mid=(lo+hi)>>1;if(BL[mid].s+BL[mid].d<vs)lo=mid+1;else hi=mid;}
1555
+ var start=lo;hi=BL.length;
1556
+ while(lo<hi){mid=(lo+hi)>>1;if(BL[mid].s<=ve)lo=mid+1;else hi=mid;}
1557
+ return{start:Math.max(0,start-1),end:Math.min(BL.length,lo+1)};
1558
+ }
1559
+
1560
+ function drawRfBlocks(start,end,vi,ch,colors,vs,ve,maxPoints){
1561
+ var y=cy(vi),scale=ch*.9/channelRange(0);
1562
+ rowClip(vi,ch,function(){
1563
+ var visibleEvents=[];
1564
+ for(var bi=start;bi<end;bi++){
1565
+ var rf=BL[bi].rf;if(!rf||rf.s+rf.d<vs||rf.s>ve)continue;
1566
+ visibleEvents.push(rf);
1567
+ }
1568
+ var pointBudget=Math.max(4,Math.floor((maxPoints||Infinity)/Math.max(1,visibleEvents.length)));
1569
+ ctx.strokeStyle=colors.rf;ctx.lineWidth=1.1;ctx.beginPath();var hasPath=false;
1570
+ for(var eventIndex=0;eventIndex<visibleEvents.length;eventIndex++){
1571
+ var rf=visibleEvents[eventIndex];
1572
+ var x0=t2x(rf.s),x1=t2x(rf.s+rf.d);
1573
+ ctx.fillStyle=colors.rff;ctx.fillRect(x0,y-ch*.45,x1-x0,ch*.9);
1574
+ if(!rf.t||!rf.m||rf.t.length<2)continue;
1575
+ var n=Math.min(rf.t.length,rf.m.length);
1576
+ var first=true,emitted=rf.bp?appendBlockRfPath(rf,y+ch*.45,scale):forEachWaveformPoint(rf.t,rf.m,pointBudget,function(time,value){
1577
+ var sx=t2x(time),sy=y+ch*.45-value*scale;if(first){ctx.moveTo(sx,sy);first=false;}else ctx.lineTo(sx,sy);
1578
+ });
1579
+ rfRenderPointCount+=emitted;if(n<=pointBudget)rfRawCurveCount++;else rfReducedCurveCount++;hasPath=hasPath||emitted>1;
1580
+ }
1581
+ if(hasPath)ctx.stroke();
1582
+ });
1583
+ }
1584
+
1585
+ function drawPhaseBlocks(start,end,vi,ch,colors,vs,ve){
1586
+ var y=cy(vi),scale=ch*.9/channelRange(1);
1587
+ rowClip(vi,ch,function(){
1588
+ ctx.strokeStyle=colors.rf;ctx.lineWidth=.8;ctx.beginPath();var hasRf=false;
1589
+ for(var bi=start;bi<end;bi++){
1590
+ var rf=BL[bi].rf,phaseTime=rf&&(rf.pt||rf.t);if(!rf||!rf.p||!phaseTime||rf.s+rf.d<vs||rf.s>ve)continue;
1591
+ var n=Math.min(phaseTime.length,rf.p.length);
1592
+ for(var i=0;i<n;i++){
1593
+ var sx=t2x(phaseTime[i]),sy=y+ch*.45-rf.p[i]*scale;
1594
+ if(i===0)ctx.moveTo(sx,sy);else ctx.lineTo(sx,sy);
1595
+ }
1596
+ hasRf=hasRf||n>1;
1597
+ }
1598
+ if(hasRf)ctx.stroke();
1599
+
1600
+ ctx.strokeStyle=colors.adc;ctx.lineWidth=.8;ctx.beginPath();var hasAdc=false;
1601
+ var clipL=M.l,clipR=mc.width/(window.devicePixelRatio||1)-M.r;
1602
+ for(var bj=start;bj<end;bj++){
1603
+ var adc=BL[bj].adc;if(!adc||adc.n<=1)continue;
1604
+ var t0=adc.s+adc.d,nAdc=adc.n,te=t0+nAdc*adc.dw;
1605
+ if(te<=vs||t0>=ve)continue;
1606
+ var step=Math.max(1,Math.ceil(nAdc/200)),first=true,fo=adc.fo||0,po=adc.po||0;
1607
+ for(var sample=0;sample<=nAdc;sample+=step){
1608
+ var t=(sample<nAdc)?t0+(sample+.5)*adc.dw:te;
1609
+ var phase=((po+6.283185*fo*(t-t0))%6.28318+6.28318)%6.28318;
1610
+ var x=t2x(t);if(x<clipL||x>clipR){first=true;continue;}
1611
+ var py=y+ch*.45-phase*scale;
1612
+ if(first){ctx.moveTo(x,py);first=false;}else ctx.lineTo(x,py);
1613
+ hasAdc=true;
1614
+ }
1615
+ }
1616
+ if(hasAdc)ctx.stroke();ctx.setLineDash([]);
1617
+ });
1618
+ }
1619
+
1620
+ function drawGradientBlocks(start,end,key,vi,ci,ch,color,vs,ve){
1621
+ var y=cy(vi),scale=ch*.4/channelRange(ci);
1622
+ rowClip(vi,ch,function(){
1623
+ ctx.strokeStyle=color;ctx.lineWidth=1;ctx.beginPath();var hasPath=false;
1624
+ for(var bi=start;bi<end;bi++){
1625
+ var g=BL[bi][key];if(!g||g.ty==='none'||!g.t||!g.w||g.t.length<2)continue;
1626
+ var n=Math.min(g.t.length,g.w.length);
1627
+ if(n<2)continue;
1628
+ if(g.t[n-1]<vs||g.t[0]>ve)continue;
1629
+ for(var i=0;i<n;i++){
1630
+ var sx=t2x(g.t[i]),sy=y-g.w[i]*scale;
1631
+ if(i===0)ctx.moveTo(sx,sy);else ctx.lineTo(sx,sy);
1632
+ }
1633
+ hasPath=true;
1634
+ }
1635
+ if(hasPath)ctx.stroke();
1636
+ });
1637
+ }
1638
+
1639
+ function drawAdcBlocks(start,end,vi,ch,colors,vs,ve){
1640
+ var y=cy(vi),labels=[];
1641
+ rowClip(vi,ch,function(){
1642
+ ctx.beginPath();var hasRect=false;
1643
+ for(var bi=start;bi<end;bi++){
1644
+ var adc=BL[bi].adc;if(!adc)continue;
1645
+ var eventStart=adc.s+adc.d,eventEnd=eventStart+adc.n*adc.dw;
1646
+ if(eventEnd<=vs||eventStart>=ve)continue;
1647
+ var x0=t2x(Math.max(eventStart,vs)),x1=t2x(Math.min(eventEnd,ve));
1648
+ ctx.rect(x0,y-ch*.28,x1-x0,ch*.56);hasRect=true;
1649
+ if(x1-x0>30)labels.push({text:adc.n+'pts',x:(x0+x1)/2});
1650
+ }
1651
+ if(hasRect){ctx.fillStyle=colors.adf;ctx.fill();ctx.strokeStyle=colors.adc;ctx.lineWidth=1;ctx.stroke();}
1652
+ ctx.fillStyle=colors.fg;ctx.font='9px monospace';ctx.textAlign='center';
1653
+ for(var li=0;li<labels.length;li++)ctx.fillText(labels[li].text,labels[li].x,y+3);
1654
+ });
1655
+ }
1656
+
1657
+ function drawTriggerBlocks(start,end,vi,ch,colors,vs,ve){
1658
+ var y=cy(vi),labels=[];
1659
+ rowClip(vi,ch,function(){
1660
+ ctx.fillStyle=colors.tr;ctx.beginPath();var hasTrigger=false;
1661
+ for(var bi=start;bi<end;bi++){
1662
+ var triggers=BL[bi].trg;if(!triggers)continue;
1663
+ for(var ti=0;ti<triggers.length;ti++){
1664
+ var tg=triggers[ti],eventStart=tg.s+tg.d,eventEnd=eventStart+tg.dr;
1665
+ if(eventEnd<vs||eventStart>ve)continue;
1666
+ var x=t2x((eventStart+eventEnd)/2);
1667
+ ctx.moveTo(x,y-ch*.05);ctx.lineTo(x-5,y+ch*.05);ctx.lineTo(x+5,y+ch*.05);ctx.closePath();
1668
+ labels.push({text:'ch'+tg.c,x:x});hasTrigger=true;
1669
+ }
1670
+ }
1671
+ if(hasTrigger)ctx.fill();ctx.font='8px monospace';ctx.textAlign='center';
1672
+ for(var li=0;li<labels.length;li++)ctx.fillText(labels[li].text,labels[li].x,y+ch*.28);
1673
+ });
1674
+ }
1675
+
1676
+
1677
+ /* ══ kspace.js ══ */
1678
+ var kOpen=false, kView="3d";
1679
+ var kSpaceTrajectoryDrawCount=0,kSpaceOverlayDrawCount=0;
1680
+ var kCx=0, kCy=0, kCz=0, kScl=1; // view center & zoom
1681
+ var kAutoFit=true;
1682
+ var kRotX=-0.5, kRotY=0.7; // default 3D perspective
1683
+ var kDragging=false, kDragPrev=null, kDragBtn=0;
1684
+ var kCanvas=document.getElementById("kc"), kCtx=kCanvas.getContext("2d");
1685
+ var kDotSize=2, kUnit="cyc"; // cyc=1/m, rad=rad/m
1686
+
1687
+ // ── Smooth animation targets ──────────────────────────────────────────
1688
+ var _tRotX=kRotX, _tRotY=kRotY, _tScl=kScl, _tCx=kCx, _tCy=kCy, _tCz=kCz;
1689
+ var _kAnimId=null;
1690
+ var _kEasing=0.12; // higher = snappier, lower = smoother (0.06–0.20)
1691
+
1692
+ function startKSpaceAnim() {
1693
+ if (_kAnimId || kDragging) return;
1694
+ function tick() {
1695
+ var changed = false;
1696
+ var eps = 0.0005;
1697
+ if (Math.abs(_tRotX - kRotX) > eps) { kRotX += (_tRotX - kRotX) * _kEasing; changed = true; }
1698
+ if (Math.abs(_tRotY - kRotY) > eps) { kRotY += (_tRotY - kRotY) * _kEasing; changed = true; }
1699
+ if (Math.abs(_tScl - kScl) > 0.001){ kScl += (_tScl - kScl) * _kEasing; changed = true; }
1700
+ if (Math.abs(_tCx - kCx) > 0.001){ kCx += (_tCx - kCx) * _kEasing; changed = true; }
1701
+ if (Math.abs(_tCy - kCy) > 0.001){ kCy += (_tCy - kCy) * _kEasing; changed = true; }
1702
+ if (Math.abs(_tCz - kCz) > 0.001){ kCz += (_tCz - kCz) * _kEasing; changed = true; }
1703
+ if (changed) {
1704
+ drawKsFast();
1705
+ _kAnimId = requestAnimationFrame(tick);
1706
+ } else {
1707
+ // Snap to exact targets
1708
+ kRotX=_tRotX; kRotY=_tRotY; kScl=_tScl; kCx=_tCx; kCy=_tCy; kCz=_tCz;
1709
+ _kAnimId = null;
1710
+ }
1711
+ }
1712
+ _kAnimId = requestAnimationFrame(tick);
1713
+ }
1714
+
1715
+ function setKSpaceTarget(rx, ry, s, cx, cy, cz, instant) {
1716
+ if (instant) {
1717
+ kRotX=_tRotX=rx; kRotY=_tRotY=ry; kScl=_tScl=s; kCx=_tCx=cx; kCy=_tCy=cy; kCz=_tCz=cz;
1718
+ drawKs();
1719
+ } else {
1720
+ _tRotX=rx; _tRotY=ry; _tScl=s; _tCx=cx; _tCy=cy; _tCz=cz;
1721
+ startKSpaceAnim();
1722
+ }
1723
+ }
1724
+ document.getElementById("kdot").oninput=function(){kDotSize=parseInt(this.value);drawKs();};
1725
+ document.getElementById("kunit").onclick=function(){
1726
+ kUnit=kUnit==="cyc"?"rad":"cyc";this.textContent=kUnit==="cyc"?"Unit: 1/m":"Unit: rad/m";drawKs();
1727
+ };
1728
+
1729
+ /* ── Theme selector (toolbar) ─────────────────────────────────────── */
1730
+ var themeSelect=document.getElementById("theme");
1731
+ var systemThemeQuery=(typeof window.matchMedia==="function")?window.matchMedia("(prefers-color-scheme: dark)"):null;
1732
+ var inVsCode=!!vscApi;
1733
+ function storageGet(k){try{return localStorage.getItem(k);}catch(_){return null;}}
1734
+ function storageSet(k,v){try{localStorage.setItem(k,v);}catch(_){}}
1735
+ function clearThemeClasses(){
1736
+ var b=document.body,rm=[];
1737
+ b.classList.forEach(function(c){if(c.indexOf("theme-")===0)rm.push(c);});
1738
+ rm.forEach(function(c){b.classList.remove(c);});
1739
+ }
1740
+ function redrawAfterThemeChange(){
1741
+ mmCache=null;
1742
+ draw();drawKs();drawMinimap();
1743
+ }
1744
+ function applyThemeChoice(value,persist){
1745
+ if(!value)value="system";
1746
+ clearThemeClasses();
1747
+ if(value!=="system"){
1748
+ document.body.classList.add("theme-"+value);
1749
+ }else if(!inVsCode){
1750
+ document.body.classList.add(systemThemeQuery&&systemThemeQuery.matches?"theme-github":"theme-githublight");
1751
+ }
1752
+ if(themeSelect&&themeSelect.value!==value)themeSelect.value=value;
1753
+ if(persist)storageSet("seqeyes.theme",value);
1754
+ redrawAfterThemeChange();
1755
+ }
1756
+ if(themeSelect){
1757
+ var savedTheme=storageGet("seqeyes.theme")||"system";
1758
+ if(!themeSelect.querySelector('option[value="'+savedTheme+'"]'))savedTheme="system";
1759
+ themeSelect.onchange=function(){applyThemeChoice(this.value,true);};
1760
+ applyThemeChoice(savedTheme,false);
1761
+ }
1762
+ if(systemThemeQuery){
1763
+ var onSystemTheme=function(){if(!themeSelect||themeSelect.value==="system")applyThemeChoice("system",false);};
1764
+ if(systemThemeQuery.addEventListener)systemThemeQuery.addEventListener("change",onSystemTheme);
1765
+ else if(systemThemeQuery.addListener)systemThemeQuery.addListener(onSystemTheme);
1766
+ }
1767
+
1768
+ /* ═══════════════════════════════════════════════════════════════════════
1769
+ WebGL state
1770
+ ═══════════════════════════════════════════════════════════════════════ */
1771
+ var gl=null, glProgram=null, glBuf=null, glN=0;
1772
+ var glAttribPos=-1, glAttribTime=-1;
1773
+ var glU_cy=-1,glU_sy=-1,glU_cx=-1,glU_sx=-1,glU_center=-1,glU_scale=-1;
1774
+ var glU_halfRes=-1,glU_tMin=-1,glU_tMax=-1,glU_dot=-1,glU_color=-1;
1775
+
1776
+ // ── Cached bounds (computed once when data is uploaded) ──────────────
1777
+ var _kBxmin=0,_kBxmax=0,_kBymin=0,_kBymax=0,_kBzmin=0,_kBzmax=0,_kBrng=0;
1778
+ var _kBoundsDirty=true; // set true when new data arrives
1779
+
1780
+ function initWebGL(){
1781
+ var c=document.getElementById("kg");
1782
+ gl=c.getContext("webgl2",{antialias:true,alpha:true,premultipliedAlpha:false})
1783
+ ||c.getContext("webgl",{antialias:true,alpha:true,premultipliedAlpha:false});
1784
+ if(!gl){console.warn("[SeqEyes] WebGL unavailable");return false;}
1785
+ gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
1786
+
1787
+ var vs=gl.createShader(gl.VERTEX_SHADER), fs=gl.createShader(gl.FRAGMENT_SHADER);
1788
+ gl.shaderSource(vs,'\
1789
+ attribute vec3 aPos; attribute float aTime;\
1790
+ uniform float uCy,uSy,uCx,uSx,uScale;\
1791
+ uniform vec3 uCenter; uniform vec2 uHalfRes;\
1792
+ uniform float uTMin,uTMax,uDot;\
1793
+ varying float vVis;\
1794
+ void main(){\
1795
+ float dx=aPos.x-uCenter.x,dy=aPos.y-uCenter.y,dz=aPos.z-uCenter.z;\
1796
+ float rx=dx*uCy-dz*uSy;\
1797
+ float rz=dx*uSy+dz*uCy;\
1798
+ float ry=dy*uCx-rz*uSx;\
1799
+ float sx=rx*uScale/uHalfRes.x;\
1800
+ float sy=ry*uScale/uHalfRes.y;\
1801
+ vVis=(aTime>=uTMin&&aTime<=uTMax)?1.0:-1.0;\
1802
+ gl_Position=vec4(sx,sy,0.0,1.0);\
1803
+ gl_PointSize=vVis>0.0?uDot:0.0;\
1804
+ }');
1805
+ gl.shaderSource(fs,'\
1806
+ precision mediump float;\
1807
+ uniform vec4 uColor; varying float vVis;\
1808
+ void main(){\
1809
+ if(vVis<0.0)discard;\
1810
+ float d=length(gl_PointCoord-vec2(0.5));\
1811
+ float a=1.0-smoothstep(0.40,0.50,d);\
1812
+ gl_FragColor=vec4(uColor.rgb,uColor.a*a);\
1813
+ }');
1814
+ gl.compileShader(vs);if(!gl.getShaderParameter(vs,gl.COMPILE_STATUS)){console.warn("[SeqEyes] VS:",gl.getShaderInfoLog(vs));return false;}
1815
+ gl.compileShader(fs);if(!gl.getShaderParameter(fs,gl.COMPILE_STATUS)){console.warn("[SeqEyes] FS:",gl.getShaderInfoLog(fs));return false;}
1816
+ glProgram=gl.createProgram();
1817
+ gl.attachShader(glProgram,vs);gl.attachShader(glProgram,fs);
1818
+ gl.linkProgram(glProgram);
1819
+ if(!gl.getProgramParameter(glProgram,gl.LINK_STATUS)){console.warn("[SeqEyes] Link:",gl.getProgramInfoLog(glProgram));return false;}
1820
+
1821
+ glAttribPos=gl.getAttribLocation(glProgram,"aPos");
1822
+ glAttribTime=gl.getAttribLocation(glProgram,"aTime");
1823
+ glU_cy=gl.getUniformLocation(glProgram,"uCy");glU_sy=gl.getUniformLocation(glProgram,"uSy");
1824
+ glU_cx=gl.getUniformLocation(glProgram,"uCx");glU_sx=gl.getUniformLocation(glProgram,"uSx");
1825
+ glU_center=gl.getUniformLocation(glProgram,"uCenter");glU_scale=gl.getUniformLocation(glProgram,"uScale");
1826
+ glU_halfRes=gl.getUniformLocation(glProgram,"uHalfRes");glU_dot=gl.getUniformLocation(glProgram,"uDot");
1827
+ glU_tMin=gl.getUniformLocation(glProgram,"uTMin");glU_tMax=gl.getUniformLocation(glProgram,"uTMax");
1828
+ glU_color=gl.getUniformLocation(glProgram,"uColor");
1829
+ return true;
1830
+ }
1831
+
1832
+ /* ── Upload ADC k‑space data to GPU (called after base64 decode) ────── */
1833
+ function uploadKSpaceGPU(){
1834
+ if(!kAdc||!kAdc[0]||kAdc[0].length===0){glN=0;return;}
1835
+ if(!gl&&!initWebGL())return;
1836
+ var n=kAdc[0].length;
1837
+ var data=new Float32Array(n*4);
1838
+ var ax=kAdc[0],ay=kAdc[1],az=kAdc[2],at=kAdcTime;
1839
+ 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];}
1840
+ if(glBuf)gl.deleteBuffer(glBuf);
1841
+ glBuf=gl.createBuffer();
1842
+ gl.bindBuffer(gl.ARRAY_BUFFER,glBuf);
1843
+ gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);
1844
+ glN=n;
1845
+ // Compute bounds once
1846
+ var xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity,zmin=Infinity,zmax=-Infinity;
1847
+ 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;}}
1848
+ _kBxmin=xmin;_kBxmax=xmax;_kBymin=ymin;_kBymax=ymax;_kBzmin=zmin;_kBzmax=zmax;
1849
+ _kBrng=Math.max(xmax-xmin,ymax-ymin,zmax-zmin,1e-6);
1850
+ _kBoundsDirty=false;
1851
+ }
1852
+
1853
+ /* ═══════════════════════════════════════════════════════════════════════
1854
+ Toggle / View cycle / Canvas sizing
1855
+ ═══════════════════════════════════════════════════════════════════════ */
1856
+ document.getElementById("kbtn").onclick=function(){
1857
+ if((!kAdc||!kAdc[0]||!kAdc[0].length)&&typeof showKspaceSafetyDialog==='function'&&showKspaceSafetyDialog())return;
1858
+ // Ensure layout class is in sync before toggling
1859
+ if(typeof refreshLayout==='function')refreshLayout();
1860
+ kOpen=!kOpen;
1861
+ var p=document.getElementById("right");
1862
+ if(kOpen){
1863
+ p.classList.add("open");
1864
+ if(typeof layoutMode!=='undefined'&&layoutMode==='vertical'){
1865
+ // In vertical mode, apply height via inline !important to match applyLayoutMode
1866
+ p.style.setProperty('height','300px','important');
1867
+ p.style.setProperty('width','100%','important');
1868
+ }else{
1869
+ p.style.width='500px';
1870
+ }
1871
+ this.textContent="K Space ✕";kAutoFit=true;
1872
+ }else{
1873
+ p.classList.remove("open");
1874
+ if(typeof layoutMode!=='undefined'&&layoutMode==='vertical'){
1875
+ p.style.setProperty('height','0','important');
1876
+ }else{
1877
+ p.style.width='';
1878
+ }
1879
+ this.textContent="K Space";
1880
+ }
1881
+ if(kOpen){requestAnimationFrame(function(){drawKs_init();});}
1882
+ else{requestAnimationFrame(function(){resizeKc();drawKs();});}
1883
+ };
1884
+ document.getElementById("kax").textContent="3D";
1885
+ document.getElementById("krst").onclick=function(){
1886
+ kView="3d";
1887
+ document.getElementById("kax").textContent="3D";
1888
+ var af=_kAutoFitVals();
1889
+ setKSpaceTarget(-0.5, 0.7, af.scl, af.cx, af.cy, af.cz, false);
1890
+ };
1891
+ // Camera presets: smoothly rotate to look straight down an axis
1892
+ document.getElementById("kax").onclick=function(){
1893
+ var views=["3d","xy","xz","yz"];var idx=views.indexOf(kView);
1894
+ kView=views[(idx+1)%4];
1895
+ var trx=_tRotX, tr=_tRotY;
1896
+ if(kView==="xy"){trx=0; tr=0;}
1897
+ else if(kView==="xz"){trx=-Math.PI/2; tr=0;}
1898
+ else if(kView==="yz"){trx=0; tr=Math.PI/2;}
1899
+ else{trx=-0.5; tr=0.7;} // 3d — default perspective
1900
+ document.getElementById("kax").textContent=kView.toUpperCase();
1901
+ var af=_kAutoFitVals();
1902
+ setKSpaceTarget(trx, tr, af.scl, af.cx, af.cy, af.cz, false);
1903
+ };
1904
+
1905
+ function resizeKc(){
1906
+ var r=document.getElementById("right").getBoundingClientRect();
1907
+ if(r.width<=0||r.height<=0)return;
1908
+ var dpr=window.devicePixelRatio||1;
1909
+ // Resize both canvases
1910
+ var kg=document.getElementById("kg");
1911
+ kg.width=r.width*dpr;kg.height=r.height*dpr;
1912
+ kg.style.width=r.width+"px";kg.style.height=r.height+"px";
1913
+ kCanvas.width=r.width*dpr;kCanvas.height=r.height*dpr;
1914
+ kCanvas.style.width=r.width+"px";kCanvas.style.height=r.height+"px";
1915
+ kCtx.setTransform(dpr,0,0,dpr,0,0);
1916
+ }
1917
+
1918
+ /* ── Convert k-space value to display units ──────────────────────────── */
1919
+ function kUnitVal(v){if(kUnit==="rad")return v*6.283185;return v;}
1920
+ function kUnitStr(){return kUnit==="rad"?"rad/m":"1/m";}
1921
+ 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);}
1922
+
1923
+ /* ── Cursor-linked trajectory sample ─────────────────────────────────── */
1924
+ function nearestTimeIndex(times,t){
1925
+ if(!times||!times.length||!isFinite(t))return -1;
1926
+ var lo=0,hi=times.length-1;
1927
+ if(t<=times[0])return 0;
1928
+ if(t>=times[hi])return hi;
1929
+ while(lo<hi){
1930
+ var mid=(lo+hi)>>1;
1931
+ if(times[mid]<t)lo=mid+1;else hi=mid;
1932
+ }
1933
+ var a=lo,b=lo-1;
1934
+ if(b<0)return a;
1935
+ return Math.abs(times[a]-t)<Math.abs(times[b]-t)?a:b;
1936
+ }
1937
+ function sampleSeriesAtTime(times,series,t){
1938
+ if(!times||!series||!series[0]||!times.length)return null;
1939
+ var n=Math.min(times.length,series[0].length,series[1].length,series[2].length);
1940
+ if(n<=0)return null;
1941
+ var idx=nearestTimeIndex(times,t);
1942
+ if(idx<0)return null;
1943
+ if(t<=times[0]||t>=times[n-1]){
1944
+ idx=Math.max(0,Math.min(n-1,idx));
1945
+ return {x:series[0][idx],y:series[1][idx],z:series[2][idx],t:times[idx],source:"traj"};
1946
+ }
1947
+ var upper=idx;
1948
+ if(times[upper]<t)upper++;
1949
+ upper=Math.max(1,Math.min(n-1,upper));
1950
+ var lower=upper-1,t0=times[lower],t1=times[upper];
1951
+ if(!isFinite(t0)||!isFinite(t1)||Math.abs(t1-t0)<1e-15){
1952
+ return {x:series[0][idx],y:series[1][idx],z:series[2][idx],t:times[idx],source:"traj"};
1953
+ }
1954
+ var a=Math.max(0,Math.min(1,(t-t0)/(t1-t0)));
1955
+ return {
1956
+ x:series[0][lower]+(series[0][upper]-series[0][lower])*a,
1957
+ y:series[1][lower]+(series[1][upper]-series[1][lower])*a,
1958
+ z:series[2][lower]+(series[2][upper]-series[2][lower])*a,
1959
+ t:t,
1960
+ source:"traj"
1961
+ };
1962
+ }
1963
+ function getKCursorSample(){
1964
+ if(!cursorActive||!isFinite(cursorT))return null;
1965
+ if(kAdc&&kAdcTime&&kAdc[0]&&kAdcTime.length){
1966
+ var idx=nearestTimeIndex(kAdcTime,cursorT);
1967
+ if(idx>=0){
1968
+ var prev=idx>0?Math.abs(kAdcTime[idx]-kAdcTime[idx-1]):Infinity;
1969
+ var next=idx+1<kAdcTime.length?Math.abs(kAdcTime[idx+1]-kAdcTime[idx]):Infinity;
1970
+ var localStep=Math.min(prev,next);
1971
+ if(!isFinite(localStep))localStep=minRasterTime();
1972
+ var tol=Math.max(minRasterTime()*0.5,localStep*0.75);
1973
+ if(Math.abs(kAdcTime[idx]-cursorT)<=tol){
1974
+ return {x:kAdc[0][idx],y:kAdc[1][idx],z:kAdc[2][idx],t:kAdcTime[idx],source:"adc"};
1975
+ }
1976
+ }
1977
+ }
1978
+ return sampleSeriesAtTime(kTime,kTraj,cursorT);
1979
+ }
1980
+ function fmtKShort(v){
1981
+ var u=kUnitVal(v);
1982
+ if(!isFinite(u))return "--";
1983
+ if(Math.abs(u)>=1000)return (u/1000).toFixed(1)+"k";
1984
+ if(Math.abs(u)>=10)return u.toFixed(1);
1985
+ if(Math.abs(u)>=0.01)return u.toFixed(2);
1986
+ return u.toExponential(1);
1987
+ }
1988
+ function formatKCursorReadout(){
1989
+ var s=getKCursorSample();
1990
+ if(!s)return "";
1991
+ return "kxyz="+fmtKShort(s.x)+","+fmtKShort(s.y)+","+fmtKShort(s.z)+" "+kUnitStr();
1992
+ }
1993
+ function drawKCursorMarker(ctx,proj,cs,W,H){
1994
+ var s=getKCursorSample();
1995
+ if(!s||!isFinite(s.x)||!isFinite(s.y)||!isFinite(s.z))return;
1996
+ var p=proj(s.x,s.y,s.z);
1997
+ if(p.x<-12||p.x>W+12||p.y<-12||p.y>H+12)return;
1998
+ ctx.save();
1999
+ ctx.lineWidth=2;
2000
+ ctx.strokeStyle=cs.getPropertyValue("--cr").trim()||"#ee0000";
2001
+ ctx.fillStyle="rgba(255,215,0,0.90)";
2002
+ ctx.beginPath();ctx.arc(p.x,p.y,Math.max(5,kDotSize+4),0,6.283);ctx.fill();ctx.stroke();
2003
+ ctx.fillStyle=cs.getPropertyValue("--fg").trim();
2004
+ ctx.font="10px monospace";
2005
+ ctx.textAlign="left";
2006
+ ctx.fillText(s.source==="adc"?"ADC":"ktraj",p.x+8,p.y-8);
2007
+ ctx.restore();
2008
+ }
2009
+
2010
+ /* ── Mouse ───────────────────────────────────────────────────────────── */
2011
+ kCanvas.addEventListener("mousedown",function(e){
2012
+ kDragging=true;kDragPrev={x:e.clientX,y:e.clientY};kDragBtn=e.button;
2013
+ // Cancel any running animation so it doesn't fight the drag
2014
+ if(_kAnimId){cancelAnimationFrame(_kAnimId);_kAnimId=null;}
2015
+ e.preventDefault();
2016
+ });
2017
+ kCanvas.addEventListener("contextmenu",function(e){e.preventDefault();});
2018
+
2019
+ kCanvas.addEventListener("wheel",function(e){e.preventDefault();
2020
+ var r=kCanvas.getBoundingClientRect(),dpr=window.devicePixelRatio||1;
2021
+ var mx=(e.clientX-r.left)/dpr, my=(e.clientY-r.top)/dpr;
2022
+ var zf=e.deltaY<0?1.25:0.8, W=kCanvas.width/dpr, H=kCanvas.height/dpr;
2023
+ var dz=(1-1/zf)*dpr/kScl;
2024
+ kAutoFit=false;
2025
+ setKSpaceTarget(kRotX, kRotY, kScl*zf, kCx+(mx-W/2)*dz, kCy-(my-H/2)*dz, kCz, false);
2026
+ },{passive:false});
2027
+
2028
+ window.addEventListener("mousemove",function(e){
2029
+ if(!kDragging||!kDragPrev||!kOpen)return;
2030
+ var dx=e.clientX-kDragPrev.x, dy=e.clientY-kDragPrev.y;
2031
+ kDragPrev={x:e.clientX,y:e.clientY};
2032
+ if(kView!=="3d"){kView="3d";document.getElementById("kax").textContent="3D";}
2033
+ if(kDragBtn===0){
2034
+ // left drag = instant rotate (no lerp — feels responsive)
2035
+ kRotY+=dx*0.008; kRotX-=dy*0.008;
2036
+ _tRotY=kRotY; _tRotX=kRotX; // sync targets
2037
+ }else{
2038
+ // right/middle drag = instant pan
2039
+ var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cx=Math.cos(kRotX),sx=Math.sin(kRotX);
2040
+ var dpr=window.devicePixelRatio||1;
2041
+ dx/=(dpr*kScl); dy/=(dpr*kScl);
2042
+ if(Math.abs(cz)>0.01){kCy+=dy/cx;kCx+=(-dx-sz*sx*(dy/cx))/cz;}
2043
+ else{kCy+=dy/cx;kCz+=(dx-cz*sx*(dy/cx))/sz;}
2044
+ _tCy=kCy; _tCx=kCx; _tCz=kCz; // sync targets
2045
+ kAutoFit=false;
2046
+ }
2047
+ drawKsFast();
2048
+ });
2049
+ window.addEventListener("mouseup",function(){kDragging=false;kDragPrev=null;});
2050
+
2051
+ /* ── Touch: 3D k-space viewer ─────────────────────────────────────── */
2052
+ var _kTouchActive=false,_kTouchPrev=null,_kTouchPinch0=0,_kTouchMid=null,_kTouchBtn=0;
2053
+ kCanvas.addEventListener("touchstart",function(e){
2054
+ if(_kAnimId){cancelAnimationFrame(_kAnimId);_kAnimId=null;}
2055
+ if(e.touches.length===1){
2056
+ _kTouchActive=true;_kTouchBtn=0;
2057
+ _kTouchPrev={x:e.touches[0].clientX,y:e.touches[0].clientY};
2058
+ }else if(e.touches.length===2){
2059
+ _kTouchActive=true;_kTouchBtn=1;
2060
+ _kTouchPinch0=getTouchDist(e.touches);
2061
+ _kTouchMid=getTouchMid(e.touches);
2062
+ _kTouchPrev={x:_kTouchMid.x,y:_kTouchMid.y};
2063
+ }
2064
+ e.preventDefault();
2065
+ },{passive:false});
2066
+ kCanvas.addEventListener("touchmove",function(e){
2067
+ if(!_kTouchActive||!_kTouchPrev||!kOpen)return;
2068
+ if(kView!=="3d"){kView="3d";document.getElementById("kax").textContent="3D";}
2069
+ if(e.touches.length===1&&_kTouchBtn===0){
2070
+ // 1‑finger rotate
2071
+ var dx=e.touches[0].clientX-_kTouchPrev.x;
2072
+ var dy=e.touches[0].clientY-_kTouchPrev.y;
2073
+ _kTouchPrev={x:e.touches[0].clientX,y:e.touches[0].clientY};
2074
+ kRotY+=dx*0.008; kRotX-=dy*0.008;
2075
+ _tRotY=kRotY; _tRotX=kRotX;
2076
+ drawKsFast();
2077
+ }else if(e.touches.length===2){
2078
+ // 2‑finger pinch-zoom + pan
2079
+ var d=getTouchDist(e.touches);
2080
+ if(_kTouchPinch0>0){
2081
+ var zf=d/_kTouchPinch0;
2082
+ kScl*=zf; kScl=Math.max(0.001,Math.min(1e6,kScl));
2083
+ _tScl=kScl; kAutoFit=false;
2084
+ _kTouchPinch0=d;
2085
+ }
2086
+ var mid=getTouchMid(e.touches);
2087
+ var pdx=(mid.x-_kTouchPrev.x), pdy=(mid.y-_kTouchPrev.y);
2088
+ _kTouchPrev={x:mid.x,y:mid.y};
2089
+ var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cx=Math.cos(kRotX),sx=Math.sin(kRotX);
2090
+ var dpr=window.devicePixelRatio||1;
2091
+ pdx/=(dpr*kScl); pdy/=(dpr*kScl);
2092
+ if(Math.abs(cz)>0.01){kCy+=pdy/cx;kCx+=(-pdx-sz*sx*(pdy/cx))/cz;}
2093
+ else{kCy+=pdy/cx;kCz+=(pdx-cz*sx*(pdy/cx))/sz;}
2094
+ _tCy=kCy; _tCx=kCx; _tCz=kCz; kAutoFit=false;
2095
+ drawKsFast();
2096
+ }
2097
+ e.preventDefault();
2098
+ },{passive:false});
2099
+ kCanvas.addEventListener("touchend",function(e){
2100
+ _kTouchActive=false;_kTouchPrev=null;_kTouchPinch0=0;_kTouchMid=null;
2101
+ if(e.touches.length===1){
2102
+ // Transition from 2‑finger to 1‑finger
2103
+ _kTouchActive=true;_kTouchBtn=0;
2104
+ _kTouchPrev={x:e.touches[0].clientX,y:e.touches[0].clientY};
2105
+ }
2106
+ });
2107
+ kCanvas.addEventListener("touchcancel",function(){
2108
+ _kTouchActive=false;_kTouchPrev=null;_kTouchPinch0=0;_kTouchMid=null;
2109
+ });
2110
+
2111
+ /* ── Resize handle ───────────────────────────────────────────────────── */
2112
+ var kResizing=false, kResizeStart=0, kResizeW=500, kResizeH=300;
2113
+ document.getElementById("khandle").addEventListener("mousedown",function(e){
2114
+ kResizing=true;
2115
+ if(typeof layoutMode!=='undefined'&&layoutMode==='vertical')kResizeStart=e.clientY;
2116
+ else kResizeStart=e.clientX;
2117
+ e.preventDefault();e.stopPropagation();
2118
+ });
2119
+ document.getElementById("khandle").addEventListener("touchstart",function(e){
2120
+ kResizing=true;
2121
+ if(typeof layoutMode!=='undefined'&&layoutMode==='vertical')kResizeStart=e.touches[0].clientY;
2122
+ else kResizeStart=e.touches[0].clientX;
2123
+ e.preventDefault();e.stopPropagation();
2124
+ },{passive:false});
2125
+ window.addEventListener("mousemove",function(e){
2126
+ if(!kResizing)return;
2127
+ var p=document.getElementById("right");
2128
+ var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical');
2129
+ if(vertical){
2130
+ kResizeH=Math.max(120,Math.min(800,kResizeH-(e.clientY-kResizeStart)));
2131
+ kResizeStart=e.clientY;
2132
+ p.style.setProperty('height',kResizeH+'px','important');p.style.setProperty('transition','none','important');
2133
+ }else{
2134
+ kResizeW=Math.max(200,Math.min(1200,kResizeW-(e.clientX-kResizeStart)));
2135
+ kResizeStart=e.clientX;
2136
+ p.style.width=kResizeW+"px";p.style.transition="none";
2137
+ }
2138
+ resizeKc();drawKs();
2139
+ });
2140
+ window.addEventListener("touchmove",function(e){
2141
+ if(!kResizing||e.touches.length!==1)return;
2142
+ var p=document.getElementById("right");
2143
+ var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical');
2144
+ if(vertical){
2145
+ kResizeH=Math.max(120,Math.min(800,kResizeH-(e.touches[0].clientY-kResizeStart)));
2146
+ kResizeStart=e.touches[0].clientY;
2147
+ p.style.setProperty('height',kResizeH+'px','important');p.style.setProperty('transition','none','important');
2148
+ }else{
2149
+ kResizeW=Math.max(200,Math.min(1200,kResizeW-(e.touches[0].clientX-kResizeStart)));
2150
+ kResizeStart=e.touches[0].clientX;
2151
+ p.style.width=kResizeW+"px";p.style.transition="none";
2152
+ }
2153
+ resizeKc();drawKs();
2154
+ },{passive:false});
2155
+ window.addEventListener("mouseup",function(){
2156
+ if(kResizing){
2157
+ kResizing=false;
2158
+ var p=document.getElementById("right");
2159
+ var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical');
2160
+ if(vertical){p.style.setProperty('transition','height .25s','important');}
2161
+ else{p.style.transition='';}
2162
+ }
2163
+ });
2164
+ window.addEventListener("touchend",function(){
2165
+ if(kResizing){
2166
+ kResizing=false;
2167
+ var p=document.getElementById("right");
2168
+ var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical');
2169
+ if(vertical){p.style.setProperty('transition','height .25s','important');}
2170
+ else{p.style.transition='';}
2171
+ }
2172
+ });
2173
+ window.addEventListener("resize",function(){if(kOpen){resizeKc();drawKs();}});
2174
+
2175
+ /* ═══════════════════════════════════════════════════════════════════════
2176
+ Nice tick spacing
2177
+ ═══════════════════════════════════════════════════════════════════════ */
2178
+ 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;}
2179
+
2180
+ function _kAutoFitVals(){
2181
+ var rng=_kBrng||1,W=kCanvas.width/(window.devicePixelRatio||1),H=kCanvas.height/(window.devicePixelRatio||1);
2182
+ return{cx:(_kBxmin+_kBxmax)/2,cy:(_kBymin+_kBymax)/2,cz:(_kBzmin+_kBzmax)/2,scl:Math.min(W,H)/(rng*1.15)};
2183
+ }
2184
+
2185
+ /* ═══════════════════════════════════════════════════════════════════════
2186
+ Drawing (Canvas 2D axes + WebGL scatter)
2187
+ ═══════════════════════════════════════════════════════════════════════ */
2188
+
2189
+ /** First‑open initialisation: sizes canvases to the KNOWN CSS target
2190
+ * dimensions so auto‑fit never sees a mid‑transition partial size. */
2191
+ function drawKs_init(){
2192
+ var dpr=window.devicePixelRatio||1;
2193
+ var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical');
2194
+ var targetW=vertical?document.getElementById("right").getBoundingClientRect().width||500:500;
2195
+ var targetH=vertical?300:document.getElementById("right").getBoundingClientRect().height||500;
2196
+ if(targetW<=0)targetW=500;
2197
+ if(targetH<=0)targetH=500;
2198
+ // Size both canvases directly — no DOM width read
2199
+ var kg=document.getElementById("kg");
2200
+ kg.width=targetW*dpr;kg.height=targetH*dpr;
2201
+ kg.style.width=targetW+"px";kg.style.height=targetH+"px";
2202
+ kCanvas.width=targetW*dpr;kCanvas.height=targetH*dpr;
2203
+ kCanvas.style.width=targetW+"px";kCanvas.style.height=targetH+"px";
2204
+ kCtx.setTransform(dpr,0,0,dpr,0,0);
2205
+ drawKs_core(targetW,targetH,dpr);
2206
+ }
2207
+
2208
+ /** Normal draw: reads current canvas size from DOM. */
2209
+ function drawKs(){
2210
+ resizeKc();
2211
+ var dpr=window.devicePixelRatio||1;
2212
+ var W=kCanvas.width/dpr, H=kCanvas.height/dpr;
2213
+ drawKs_core(W,H,dpr);
2214
+ }
2215
+
2216
+ /** Fast draw: skips canvas resize (use during drag for responsiveness). */
2217
+ function drawKsFast(){
2218
+ var dpr=window.devicePixelRatio||1;
2219
+ var W=kCanvas.width/dpr, H=kCanvas.height/dpr;
2220
+ drawKs_core(W,H,dpr);
2221
+ }
2222
+
2223
+ /** Core rendering — assumes canvases are already sized, W & H in CSS px. */
2224
+ function drawKs_core(W,H,dpr){
2225
+ if(W<=0||H<=0)return;
2226
+
2227
+ var ctx=kCtx,cs=getComputedStyle(document.body);
2228
+ // Clear Canvas 2D (axes layer) — transparent so WebGL shows through
2229
+ ctx.clearRect(0,0,W,H);
2230
+ if(!kOpen)return;
2231
+
2232
+ // ── Check data ──
2233
+ if(!kAdc||!kAdcTime||!kAdc[0]||kAdc[0].length===0){
2234
+ ctx.fillStyle="#f00";ctx.font="11px monospace";ctx.fillText("NO ADC data",10,20);return;
2235
+ }
2236
+ var adcX=kAdc[0],adcY=kAdc[1],adcZ=kAdc[2], nAdc=adcX.length;
2237
+
2238
+ // ── Use cached bounds (computed once in uploadKSpaceGPU) ──────────
2239
+ if (_kBoundsDirty || !isFinite(_kBxmin)) {
2240
+ // Recompute if needed (shouldn't happen normally)
2241
+ var xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity,zmin=Infinity,zmax=-Infinity;
2242
+ 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;}}
2243
+ _kBxmin=xmin;_kBxmax=xmax;_kBymin=ymin;_kBymax=ymax;_kBzmin=zmin;_kBzmax=zmax;
2244
+ _kBrng=Math.max(xmax-xmin,ymax-ymin,zmax-zmin,1e-6);
2245
+ _kBoundsDirty=false;
2246
+ }
2247
+ if(!isFinite(_kBxmin)){ctx.fillStyle="#f00";ctx.font="11px monospace";ctx.fillText("ALL NaN",10,20);return;}
2248
+
2249
+ // ── Auto-fit (initial open only) ──
2250
+ if(kAutoFit){
2251
+ kAutoFit=false;
2252
+ var af=_kAutoFitVals();
2253
+ setKSpaceTarget(kRotX, kRotY, af.scl, af.cx, af.cy, af.cz, false);
2254
+ }
2255
+
2256
+ // ── Time window ──
2257
+ var visibleRange=visibleDuration(),vs=ox,ve=ox+visibleRange;
2258
+
2259
+ // ═══════════════════════════════════════════════════════════════════
2260
+ // WebGL scatter (GPU — renders millions of points at 60 fps)
2261
+ // ═══════════════════════════════════════════════════════════════════
2262
+ if(gl&&glBuf&&glN>0){
2263
+ gl.useProgram(glProgram);
2264
+ var hW=W*dpr*0.5, hH=H*dpr*0.5;
2265
+ gl.viewport(0,0,W*dpr,H*dpr);
2266
+ gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);
2267
+
2268
+ gl.uniform1f(glU_cy,Math.cos(kRotY));gl.uniform1f(glU_sy,Math.sin(kRotY));
2269
+ gl.uniform1f(glU_cx,Math.cos(kRotX));gl.uniform1f(glU_sx,Math.sin(kRotX));
2270
+ gl.uniform3f(glU_center,kCx,kCy,kCz);
2271
+ gl.uniform1f(glU_scale,kScl);
2272
+ gl.uniform2f(glU_halfRes,hW,hH);
2273
+ gl.uniform1f(glU_tMin,vs);gl.uniform1f(glU_tMax,ve);
2274
+ gl.uniform1f(glU_dot,kDotSize*dpr);
2275
+
2276
+ var ac=cs.getPropertyValue("--adc").trim();
2277
+ var rgb=parseCSSColor(ac);
2278
+ gl.uniform4f(glU_color,rgb[0],rgb[1],rgb[2],0.85);
2279
+
2280
+ gl.bindBuffer(gl.ARRAY_BUFFER,glBuf);
2281
+ gl.enableVertexAttribArray(glAttribPos);
2282
+ gl.vertexAttribPointer(glAttribPos,3,gl.FLOAT,false,16,0);
2283
+ gl.enableVertexAttribArray(glAttribTime);
2284
+ gl.vertexAttribPointer(glAttribTime,1,gl.FLOAT,false,16,12);
2285
+
2286
+ kSpaceTrajectoryDrawCount++;
2287
+ gl.drawArrays(gl.POINTS,0,glN);
2288
+ }
2289
+
2290
+ drawKsOverlay(W,H,dpr,cs);
2291
+ }
2292
+
2293
+ /** Redraw only the lightweight 2D axes/cursor layer. The WebGL trajectory
2294
+ * remains untouched when waveform hover changes cursorT. */
2295
+ function drawKsOverlayFast(){
2296
+ if(!kOpen||!kAdc||!kAdcTime||!kAdc[0]||kAdc[0].length===0)return;
2297
+ var dpr=window.devicePixelRatio||1;
2298
+ var W=kCanvas.width/dpr,H=kCanvas.height/dpr;
2299
+ if(W<=0||H<=0)return;
2300
+ drawKsOverlay(W,H,dpr,getComputedStyle(document.body));
2301
+ }
2302
+
2303
+ function drawKsOverlay(W,H,dpr,cs){
2304
+ kSpaceOverlayDrawCount++;
2305
+ var ctx=kCtx;ctx.clearRect(0,0,W,H);
2306
+ if(!kOpen||!isFinite(_kBxmin))return;
2307
+ var rng3=_kBrng;
2308
+ var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cxR=Math.cos(kRotX),sxR=Math.sin(kRotX);
2309
+ var invDpr=1/dpr;
2310
+ function proj(px,py,pz){
2311
+ var dx2=px-kCx, dy2=py-kCy, dz2=pz-kCz;
2312
+ var rx=dx2*cz-dz2*sz;
2313
+ var rz2=dx2*sz+dz2*cz;
2314
+ var ry=dy2*cxR-rz2*sxR;
2315
+ return {x:W/2+rx*kScl*invDpr, y:H/2-ry*kScl*invDpr};
2316
+ }
2317
+ var tick=kNice(rng3);
2318
+ function drawAxis3D(fx,fy,fz,label,col){
2319
+ var al=rng3*0.65;
2320
+ var t1=proj(fx*al,fy*al,fz*al);
2321
+ var t2=proj(-fx*al,-fy*al,-fz*al);
2322
+ ctx.strokeStyle=col;ctx.lineWidth=1.6;ctx.setLineDash([]);
2323
+ ctx.beginPath();ctx.moveTo(t2.x,t2.y);ctx.lineTo(t1.x,t1.y);ctx.stroke();
2324
+ ctx.fillStyle=col;ctx.beginPath();ctx.arc(t1.x,t1.y,4,0,6.283);ctx.fill();
2325
+ ctx.fillStyle=col;ctx.font="bold 11px monospace";ctx.fillText(label,t1.x+5,t1.y-5);
2326
+ ctx.fillStyle=cs.getPropertyValue("--lb").trim();ctx.font="8px monospace";
2327
+ for(var v=tick;v<=al;v+=tick){
2328
+ var tp=proj(fx*v,fy*v,fz*v);
2329
+ var tm=proj(-fx*v,-fy*v,-fz*v);
2330
+ ctx.strokeStyle=col;ctx.lineWidth=0.5;
2331
+ ctx.beginPath();ctx.moveTo(tp.x-3,tp.y);ctx.lineTo(tp.x+3,tp.y);ctx.stroke();
2332
+ ctx.beginPath();ctx.moveTo(tm.x-3,tm.y);ctx.lineTo(tm.x+3,tm.y);ctx.stroke();
2333
+ ctx.fillText(kTickVal(v),tp.x+5,tp.y-2);
2334
+ }
2335
+ }
2336
+ drawAxis3D(1,0,0,"kx",cs.getPropertyValue("--gx").trim());
2337
+ drawAxis3D(0,1,0,"ky",cs.getPropertyValue("--gy").trim());
2338
+ drawAxis3D(0,0,1,"kz",cs.getPropertyValue("--gz").trim());
2339
+ var oo=proj(0,0,0);
2340
+ ctx.fillStyle=cs.getPropertyValue("--fg").trim();ctx.beginPath();ctx.arc(oo.x,oo.y,4,0,6.283);ctx.fill();
2341
+ drawKCursorMarker(ctx,proj,cs,W,H);
2342
+ }
2343
+
2344
+ /* ── Parse CSS hex colour to [r,g,b] 0‑1 ────────────────────────────── */
2345
+ function parseCSSColor(c){
2346
+ if(!c)return[0.26,0.83,0.96];
2347
+ var m=c.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
2348
+ if(m)return[parseInt(m[1],16)/255,parseInt(m[2],16)/255,parseInt(m[3],16)/255];
2349
+ m=c.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
2350
+ if(m)return[+m[1]/255,+m[2]/255,+m[3]/255];
2351
+ return[0.26,0.83,0.96];
2352
+ }
2353
+
2354
+
2355
+ /* ══ interaction.js ══ */
2356
+ /* ═══════════════════════════════════════════════════════════════════════
2357
+ Mouse interaction
2358
+ ═══════════════════════════════════════════════════════════════════════ */
2359
+
2360
+ var _touchTooltipTimer=null,_pointerFrame=0,_pendingPointer=null,mmDrag=false;
2361
+
2362
+ mc.addEventListener('wheel',function(e){e.preventDefault();
2363
+ var changed=false;
2364
+ if(e.ctrlKey||e.metaKey){
2365
+ var ch=cH(),vc=visChannels(),my=e.clientY-mc.getBoundingClientRect().top;
2366
+ var vi2=Math.floor((my-M.t)/ch);
2367
+ 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;}
2368
+ }else{
2369
+ var r=mc.getBoundingClientRect(),mx2=e.clientX-r.left;
2370
+ var zf=e.deltaY<0?1.3:1/1.3;changed=zoomAt(mx2,zf);
2371
+ }
2372
+ if(changed)scheduleViewerDraw(true);},{passive:false});
2373
+ mc.addEventListener('mousedown',function(e){if(e.button===0){dr=true;dsx=e.clientX;dso=ox;cc.classList.add('pan')}});
2374
+ window.addEventListener('mousemove',function(e){
2375
+ // Skip waveform pointer work while user is dragging/rotating k-space.
2376
+ if(typeof kDragging!=='undefined'&&kDragging)return;
2377
+ _pendingPointer={x:e.clientX,y:e.clientY,dragging:dr};
2378
+ if(_pointerFrame)return;
2379
+ _pointerFrame=requestAnimationFrame(flushPointerUpdate);
2380
+ });
2381
+ window.addEventListener('mouseup',function(){dr=false;cc.classList.remove('pan');drawMinimap();});
2382
+ cc.addEventListener('mouseleave',function(){
2383
+ if(dr)return;
2384
+ _pendingPointer=null;if(_pointerFrame){cancelAnimationFrame(_pointerFrame);_pointerFrame=0;}
2385
+ clearViewerCursor();
2386
+ });
2387
+
2388
+ function flushPointerUpdate(){
2389
+ _pointerFrame=0;var p=_pendingPointer;_pendingPointer=null;if(!p)return;
2390
+ var r=mc.getBoundingClientRect();
2391
+ var inside=p.x>=r.left&&p.x<=r.right&&p.y>=r.top&&p.y<=r.bottom;
2392
+ if(!p.dragging&&!inside){if(cursorActive)clearViewerCursor();return;}
2393
+ var mx=p.x-r.left,my=p.y-r.top;cursorT=x2t(mx);
2394
+ cursorActive=mx>=M.l&&mx<=r.width-M.r&&my>=M.t&&my<=r.height-M.b;
2395
+ var kInfo=(typeof formatKCursorReadout==='function')?formatKCursorReadout():'';
2396
+ curEl.textContent=fmtT(timeConv(cursorT))+' '+timeUnitStr()+(kInfo?' | '+kInfo:'');
2397
+ if(p.dragging){
2398
+ ox=dso-(p.x-dsx)/sc;clampView();tt.style.display='none';
2399
+ draw();if(typeof drawKs==='function')drawKs();drawMinimap();return;
2400
+ }
2401
+ drawCursorOverlay();
2402
+ if(typeof drawKsOverlayFast==='function')drawKsOverlayFast();
2403
+ showTooltipAt(p.x,p.y,cursorT);
2404
+ }
2405
+
2406
+ function clearViewerCursor(){
2407
+ cursorT=0;cursorActive=false;curEl.textContent='\u2190 hover for time';tt.style.display='none';
2408
+ drawCursorOverlay();if(typeof drawKsOverlayFast==='function')drawKsOverlayFast();
2409
+ }
2410
+
2411
+ /* ── Touch: waveform viewer ───────────────────────────────────────── */
2412
+ var _tchDragging=false,_tchStartX=0,_tchStartO=0,_tchPinch0=0,_tchPinchMid=null,_tchMoved=false;
2413
+ mc.addEventListener('touchstart',function(e){
2414
+ if(e.touches.length===1){
2415
+ _tchDragging=true;_tchMoved=false;
2416
+ _tchStartX=e.touches[0].clientX;_tchStartO=ox;
2417
+ cc.classList.add('pan');
2418
+ // Update cursor position
2419
+ var p=getTouchPos(e.touches[0],mc);
2420
+ 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;
2421
+ curEl.textContent=fmtT(timeConv(cursorT))+' '+timeUnitStr();
2422
+ }else if(e.touches.length===2){
2423
+ _tchDragging=false;_tchPinch0=getTouchDist(e.touches);_tchPinchMid=getTouchMid(e.touches);
2424
+ }
2425
+ e.preventDefault();
2426
+ },{passive:false});
2427
+ mc.addEventListener('touchmove',function(e){
2428
+ if(e.touches.length===1&&_tchDragging){
2429
+ _tchMoved=true;
2430
+ ox=_tchStartO-(e.touches[0].clientX-_tchStartX)/sc;clampView();
2431
+ var p=getTouchPos(e.touches[0],mc);
2432
+ 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;
2433
+ curEl.textContent=fmtT(timeConv(cursorT))+' '+timeUnitStr();
2434
+ scheduleViewerDraw(true);
2435
+ }else if(e.touches.length===2){
2436
+ _tchDragging=false;
2437
+ var d=getTouchDist(e.touches),zf=d/Math.max(_tchPinch0,1);
2438
+ var mid=getTouchMid(e.touches),mx2=mid.x-mc.getBoundingClientRect().left;
2439
+ if(zoomAt(mx2,zf)){_tchPinch0=d;scheduleViewerDraw(true);}
2440
+ }
2441
+ e.preventDefault();
2442
+ },{passive:false});
2443
+ mc.addEventListener('touchend',function(e){
2444
+ if(!_tchMoved&&cursorActive&&e.changedTouches.length===1){
2445
+ // Tap → show tooltip
2446
+ showTooltipAt(e.changedTouches[0].clientX,e.changedTouches[0].clientY,cursorT);
2447
+ }
2448
+ _tchDragging=false;cc.classList.remove('pan');drawMinimap();
2449
+ if(e.touches.length===0){_tchPinch0=0;_tchPinchMid=null;}
2450
+ });
2451
+ mc.addEventListener('touchcancel',function(){_tchDragging=false;cc.classList.remove('pan');_tchPinch0=0;_tchPinchMid=null;});
2452
+
2453
+ /* ── Tooltip helper (also used by touch tap) ──────────────────────── */
2454
+ function sampleDerivedSeriesAtTime(series,t){
2455
+ if(series&&series.kind==='envelope'){
2456
+ var base=series.levels&&series.levels[0];if(!base||base.count<1||!isFinite(t))return null;
2457
+ var index=Math.max(0,lowerBoundSeries(base.t1,t));if(index>=base.count||t<base.t0[index]||t>base.t1[index])return null;
2458
+ var span=base.t1[index]-base.t0[index],alpha=span>0?(t-base.t0[index])/span:0;
2459
+ return(base.first[index]+(base.last[index]-base.first[index])*alpha)*series.scale;
2460
+ }
2461
+ if(!series||!series.t||!series.v||series.n<1||!isFinite(t))return null;
2462
+ var n=Math.min(series.n,series.t.length,series.v.length);
2463
+ if(n<1||t<series.t[0]||t>series.t[n-1])return null;
2464
+ if(n===1||t<=series.t[0])return series.v[0]*series.scale;
2465
+ if(t>=series.t[n-1])return series.v[n-1]*series.scale;
2466
+ var lo=0,hi=n-1;
2467
+ while(hi-lo>1){var m=(lo+hi)>>1;if(series.t[m]<=t)lo=m;else hi=m;}
2468
+ var dt=series.t[hi]-series.t[lo];
2469
+ if(dt<=0)return series.v[lo]*series.scale;
2470
+ return (series.v[lo]+(series.v[hi]-series.v[lo])*(t-series.t[lo])/dt)*series.scale;
2471
+ }
2472
+ function fmtSignedAmp(v){
2473
+ 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);
2474
+ return(v<0?'-':'')+s;
2475
+ }
2476
+ function fmtDerivedValue(v,unit){return unit==='%'?v.toFixed(1)+'%':fmtSignedAmp(v)+' '+unit;}
2477
+ function pushDerivedPart(parts,label,series,t,unit){
2478
+ var v=sampleDerivedSeriesAtTime(series,t);if(v===null)return;
2479
+ parts.push(label+'='+fmtDerivedValue(v,unit));
2480
+ }
2481
+ function pushM1DerivedPart(parts,label,series,t){
2482
+ if(series&&series.kind==='envelope'){
2483
+ var range=sampleEnvelopeRangeAtTime(series,t);if(!range)return;
2484
+ if(range.min===range.max)parts.push(label+'='+fmtDerivedValue(range.min,'s/m'));
2485
+ else parts.push(label+'\u2208['+fmtSignedAmp(range.min)+', '+fmtSignedAmp(range.max)+'] s/m');
2486
+ return;
2487
+ }
2488
+ pushDerivedPart(parts,label,series,t,'s/m');
2489
+ }
2490
+ function appendDerivedTooltipLines(lines,ct){
2491
+ if(pnsData&&chVis[7]){
2492
+ 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,'%');
2493
+ if(p.length)lines.push('PNS: '+p.join(' '));
2494
+ }
2495
+ var activeM1=m1SeriesForView(ox,ox+visibleDuration());
2496
+ if(activeM1&&(chVis[8]||chVis[9]||chVis[10])){
2497
+ var m=[];if(chVis[8])pushM1DerivedPart(m,'M1x',activeM1.x,ct);if(chVis[9])pushM1DerivedPart(m,'M1y',activeM1.y,ct);if(chVis[10])pushM1DerivedPart(m,'M1z',activeM1.z,ct);
2498
+ if(m.length)lines.push('M1: '+m.join(' '));
2499
+ }
2500
+ }
2501
+ function placeTooltip(cx,cy){
2502
+ var pad=8;tt.style.left='0px';tt.style.top='0px';
2503
+ var r=tt.getBoundingClientRect(),vw=window.innerWidth,vh=window.innerHeight;
2504
+ var left=Math.min(Math.max(pad,cx+15),Math.max(pad,vw-r.width-pad));
2505
+ var top=cy+12;if(top+r.height+pad>vh)top=cy-r.height-12;if(top<pad)top=pad;
2506
+ tt.style.left=left+'px';tt.style.top=top+'px';
2507
+ }
2508
+ function showTooltipAt(cx,cy,ct){
2509
+ var ch=cH(),vc=visChannels(),vi2=Math.floor((cy-mc.getBoundingClientRect().top-M.t)/ch);
2510
+ if(vi2>=0&&vi2<vc.length){
2511
+ var found=findBlockAtTime(ct);
2512
+ var lines=[];
2513
+ if(found){
2514
+ var blockDt=Math.max(0,Math.min(found.d,ct-found.s));
2515
+ lines=['Block #'+found.i,'Time: '+fmtT(timeConv(ct))+' '+timeUnitStr()+' (\u0394 '+fmtT(timeConv(blockDt))+' / dur: '+fmtT(timeConv(found.d))+' '+timeUnitStr()+')'];
2516
+ if(found.rf){lines.push('RF: '+(found.rf.a||0).toFixed(1)+' Hz fo='+(found.rf.fo||0).toFixed(0)+' Hz \u03c6\u2080='+((found.rf.po||0)%6.283).toFixed(2)+' rad');}
2517
+ if(found.gx&&found.gx.ty!=='none')lines.push('Gx: '+fmtG(found.gx,ct));
2518
+ if(found.gy&&found.gy.ty!=='none')lines.push('Gy: '+fmtG(found.gy,ct));
2519
+ if(found.gz&&found.gz.ty!=='none')lines.push('Gz: '+fmtG(found.gz,ct));
2520
+ if(found.adc){lines.push('ADC: '+found.adc.n+'pts @'+(found.adc.dw*1e6).toFixed(1)+'\u00b5s fo='+(found.adc.fo||0).toFixed(0)+' Hz \u03c6\u2080='+((found.adc.po||0)%6.283).toFixed(2)+' rad');}
2521
+ if(found.trg)lines.push('Trig: ch'+found.trg.map(function(x){return x.c}).join(',')+' \u0394'+found.trg.map(function(x){return fmtT(timeConv(x.dr))+' '+timeUnitStr()}).join(','));
2522
+ }else{
2523
+ lines=['Time: '+fmtT(timeConv(ct))+' '+timeUnitStr()];
2524
+ }
2525
+ appendDerivedTooltipLines(lines,ct);
2526
+ if(lines.length>1||found){
2527
+ tt.textContent=lines.join('\n');tt.style.display='block';
2528
+ placeTooltip(cx,cy);
2529
+ // Auto-dismiss on touch devices
2530
+ if(_touchTooltipTimer)clearTimeout(_touchTooltipTimer);
2531
+ if(isTouchDevice())_touchTooltipTimer=setTimeout(function(){tt.style.display='none';},3000);
2532
+ return;
2533
+ }
2534
+ }
2535
+ tt.style.display='none';
2536
+ }
2537
+
2538
+ function findBlockAtTime(t){
2539
+ var lo=0,hi=BL.length;
2540
+ while(lo<hi){var mid=(lo+hi)>>1;if(BL[mid].s+BL[mid].d<t)lo=mid+1;else hi=mid;}
2541
+ for(var i=lo;i<Math.min(BL.length,lo+2);i++){
2542
+ if(t>=BL[i].s&&t<=BL[i].s+BL[i].d)return BL[i];
2543
+ }
2544
+ return null;
2545
+ }
2546
+
2547
+ /* ── Toolbar buttons ──────────────────────────────────────────────────── */
2548
+ document.getElementById('openBtn').onclick=function(){
2549
+ if(vscApi){vscApi.postMessage({command:'openFile'});}
2550
+ else{var fi=document.getElementById('fileInput');if(fi)fi.click();}
2551
+ };
2552
+ document.getElementById('exportKspaceBtn').onclick=function(){
2553
+ if(vscApi){vscApi.postMessage({command:'exportKspace'});}
2554
+ };
2555
+ function requestM1(channel){
2556
+ if(m1Busy)return;
2557
+ if(m1Data){chVis[channel]=!chVis[channel];buildLegend();draw();return;}
2558
+ m1RequestedChannel=channel;
2559
+ m1Busy=true;buildLegend();
2560
+ if(vscApi){vscApi.postMessage({command:'calculateM1',referenceMode:m1ReferenceMode});}
2561
+ }
2562
+ document.getElementById('pnsBtn').onclick=function(){
2563
+ if(pnsBusy)return;
2564
+ pnsBusy=true;if(pnsBtn)pnsBtn.disabled=true;
2565
+ if(vscApi){vscApi.postMessage({command:'openPnsAsc'});}
2566
+ };
2567
+ document.getElementById('zi').onclick=function(){if(zoomAtCenter(1.5))scheduleViewerDraw(true);};
2568
+ document.getElementById('zo').onclick=function(){if(zoomAtCenter(1/1.5))scheduleViewerDraw(true);};
2569
+ document.getElementById('zf').onclick=function(){fit();drawMinimap();};
2570
+ document.getElementById('zr').onclick=function(){fit();drawMinimap();};
2571
+ document.getElementById('bbc').onchange=function(){showBB=this.checked;draw();};
2572
+
2573
+ /* ── Mobile hamburger menu ─────────────────────────────────────────── */
2574
+ var menuBtn=document.getElementById('menuBtn');
2575
+ if(menuBtn){
2576
+ menuBtn.onclick=function(e){var m=document.getElementById('tbMore');m.classList.toggle('open');e.stopPropagation();};
2577
+ document.addEventListener('click',function(e){
2578
+ var m=document.getElementById('tbMore');
2579
+ if(m.classList.contains('open')&&!m.contains(e.target)&&e.target!==menuBtn)m.classList.remove('open');
2580
+ });
2581
+ // Close menu when any option inside it is used
2582
+ document.getElementById('tbMore').addEventListener('click',function(e){
2583
+ if(e.target.tagName==='BUTTON'||e.target.tagName==='SELECT'||e.target.tagName==='INPUT'){
2584
+ this.classList.remove('open');
2585
+ }
2586
+ });
2587
+ }
2588
+
2589
+ /* ── Minimap interaction ──────────────────────────────────────────────── */
2590
+ mmCanvas.addEventListener('mousedown',function(e){
2591
+ mmDrag=true;scrollMinimapToMouse(e);e.preventDefault();
2592
+ });
2593
+ window.addEventListener('mousemove',function(e){
2594
+ if(!mmDrag)return;
2595
+ scrollMinimapToMouse(e);
2596
+ });
2597
+ window.addEventListener('mouseup',function(){mmDrag=false;});
2598
+
2599
+ /* ── Touch: minimap ──────────────────────────────────────────────── */
2600
+ var _mmTouchDrag=false,_mmTouchPinch0=0;
2601
+ mmCanvas.addEventListener('touchstart',function(e){
2602
+ if(e.touches.length===1){_mmTouchDrag=true;scrollMinimapToTouch(e.touches[0]);}
2603
+ else if(e.touches.length===2){_mmTouchPinch0=getTouchDist(e.touches);}
2604
+ e.preventDefault();
2605
+ },{passive:false});
2606
+ mmCanvas.addEventListener('touchmove',function(e){
2607
+ if(e.touches.length===1&&_mmTouchDrag){scrollMinimapToTouch(e.touches[0]);}
2608
+ else if(e.touches.length===2){
2609
+ var d=getTouchDist(e.touches),zf=d/Math.max(_mmTouchPinch0,1);
2610
+ if(zoomAtCenter(zf)){_mmTouchPinch0=d;scheduleViewerDraw(true);}
2611
+ }
2612
+ e.preventDefault();
2613
+ },{passive:false});
2614
+ mmCanvas.addEventListener('touchend',function(){_mmTouchDrag=false;_mmTouchPinch0=0;});
2615
+ mmCanvas.addEventListener('touchcancel',function(){_mmTouchDrag=false;_mmTouchPinch0=0;});
2616
+
2617
+ function scrollMinimapToTouch(touch){
2618
+ var r=mmCanvas.getBoundingClientRect(),dpr=window.devicePixelRatio||1;
2619
+ var mx=touch.clientX-r.left,W=mmCanvas.width/dpr;
2620
+ var frac=Math.max(0,Math.min(1,mx/W));
2621
+ var vsWidth=(mc.width/dpr-M.l-M.r)/sc;
2622
+ var viewCenter=frac*TD;
2623
+ ox=viewCenter-vsWidth/2;
2624
+ clampView();
2625
+ draw();if(typeof drawKs==='function')drawKs();drawMinimap();
2626
+ }
2627
+
2628
+ /* ── Old mouse minimap ────────────────────────────────────────────── */
2629
+ mmCanvas.addEventListener('wheel',function(e){
2630
+ e.preventDefault();
2631
+ var zf=e.deltaY<0?1.3:1/1.3;
2632
+ if(zoomAtCenter(zf))scheduleViewerDraw(true);
2633
+ },{passive:false});
2634
+
2635
+ function scrollMinimapToMouse(e){
2636
+ var r=mmCanvas.getBoundingClientRect(),dpr=window.devicePixelRatio||1;
2637
+ var mx=e.clientX-r.left; // CSS pixels — e.clientX and r.left are both CSS
2638
+ var W=mmCanvas.width/dpr; // physical → CSS pixels
2639
+ var frac=Math.max(0,Math.min(1,mx/W));
2640
+ var vsWidth=visibleDuration();
2641
+ // Align viewport CENTER to mouse, not left edge
2642
+ var viewCenter=frac*TD;
2643
+ ox=viewCenter-vsWidth/2;
2644
+ clampView();
2645
+ draw();if(typeof drawKs==='function')drawKs();drawMinimap();
2646
+ }
2647
+
2648
+ /* ── Helpers ──────────────────────────────────────────────────────────── */
2649
+ function fit(){var w=mc.width/(window.devicePixelRatio||1);sc=(w-M.l-M.r)/(TD||1e-3);ox=0;clampView();draw();if(typeof drawKs==='function')drawKs();drawMinimap();}
2650
+ function nice(r){var ms=[1,2,5,10,20,50,100,200,500,1000,2000,5000,10000,20000,50000];
2651
+ 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)))}
2652
+ function fmtT(v){if(v===0)return'0';if(v>=0.001)return v.toFixed(3);return v.toExponential(2)}
2653
+ 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)}
2654
+ function sampleGradAtTime(g,t){
2655
+ if(!g||g.ty==='none'||!g.t||!g.w||g.t.length<2||g.w.length<2||!isFinite(t))return 0;
2656
+ var n=Math.min(g.t.length,g.w.length);
2657
+ if(t<g.t[0]||t>g.t[n-1])return 0;
2658
+ var lo=0,hi=n-1;
2659
+ while(hi-lo>1){var m=(lo+hi)>>1;if(g.t[m]<=t)lo=m;else hi=m;}
2660
+ var dt=g.t[hi]-g.t[lo];
2661
+ if(dt<=0)return g.w[lo]||0;
2662
+ return g.w[lo]+(g.w[hi]-g.w[lo])*(t-g.t[lo])/dt;
2663
+ }
2664
+ function fmtG(g,t){var a=gradConv(sampleGradAtTime(g,t));return a.toFixed(1)+' '+gradUnitStr()+' ('+g.ty+')';}
2665
+ new MutationObserver(function(){mmCache=null;draw();drawKs();drawMinimap();}).observe(document.body,{attributes:true,attributeFilter:['class']});
2666
+ rs();
2667
+