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
|
+
|