diff --git a/matlab/pack_toolbox.m b/matlab/pack_toolbox.m index 4668b8f..9f9375c 100644 --- a/matlab/pack_toolbox.m +++ b/matlab/pack_toolbox.m @@ -37,7 +37,8 @@ fullfile('matlab', 'toolboxInfo.xml') fullfile('matlab', 'examples', 'demo.m') fullfile('web', 'index.html') - fullfile('web', 'derived-series.js') + fullfile('web', 'webview-bundle.js') + fullfile('web', 'styles.css') fullfile('web', 'pulseq-bundle.js') fullfile('web', 'logo.png') fullfile('web', 'logo.svg') diff --git a/matlab/seqeyes.m b/matlab/seqeyes.m index 5d862e2..60aadbd 100644 --- a/matlab/seqeyes.m +++ b/matlab/seqeyes.m @@ -222,7 +222,8 @@ function safeRemoveDir(folderPath) mkdir(tempDir); [tmplDir, ~, ~] = fileparts(templatePath); copyfile(fullfile(tmplDir, 'pulseq-bundle.js'), fullfile(tempDir, 'pulseq-bundle.js')); - copyfile(fullfile(tmplDir, 'derived-series.js'), fullfile(tempDir, 'derived-series.js')); + copyfile(fullfile(tmplDir, 'webview-bundle.js'), fullfile(tempDir, 'webview-bundle.js')); + copyfile(fullfile(tmplDir, 'styles.css'), fullfile(tempDir, 'styles.css')); % Copy logo assets if they exist (silently skip if missing) if isfile(fullfile(tmplDir, 'logo.png')) copyfile(fullfile(tmplDir, 'logo.png'), fullfile(tempDir, 'logo.png')); diff --git a/package.json b/package.json index 28f4142..a6508bf 100644 --- a/package.json +++ b/package.json @@ -130,9 +130,9 @@ }, "scripts": { "vscode:prepublish": "npm run compile", - "compile": "tsc -p ./ && npm run copy-assets", - "watch": "npm run copy-assets && tsc -watch -p ./", - "copy-assets": "node -e \"var s='src/editor/webview/assets',d='out/editor/webview/assets',fs=require('fs'),p=require('path');fs.mkdirSync(d,{recursive:true});fs.readdirSync(s).forEach(function(f){fs.copyFileSync(p.join(s,f),p.join(d,f))});console.log('Assets copied to out/');\"", + "compile": "node scripts/build-extension.mjs && node scripts/build-webview.mjs", + "watch": "node scripts/build-webview.mjs && node scripts/build-extension.mjs --watch", + "typecheck": "tsc -p ./ --noEmit", "lint": "eslint src web test --ext ts", "test": "vitest run", "test:browser": "playwright test", @@ -141,12 +141,12 @@ "perf:browser": "npm run build:web && playwright test --config playwright.perf.config.ts", "perf": "npm run perf:node && npm run perf:browser", "test:vscode": "npm run compile && node test/vscode/run-tests.cjs", - "package:vsix": "vsce package --out seqeyes-web.vsix", + "package:vsix": "npm run compile && vsce package --out seqeyes-web.vsix", "test:package": "npm run package:vsix && node test/vscode/package-smoke.cjs seqeyes-web.vsix", - "check": "npm run compile && npm run lint && npm test && npm run build:web", + "check": "npm run lint && npm test && npm run build:web && npm run compile", "export:kspace": "node out/cli/exportKspace.js", - "package": "vsce package", - "build:web": "npx esbuild web/pulseq-browser.ts --bundle --format=iife --global-name=Pulseq --outfile=web/pulseq-bundle.js --target=es2020 && node -e \"const fs=require('fs');fs.copyFileSync('images/logo.png','web/logo.png');fs.copyFileSync('web/pulseq-bundle.js','python/src/seqeyes/resources/pulseq-bundle.js')\"", + "package": "npm run compile && vsce package", + "build:web": "node scripts/build-webview.mjs && npx esbuild web/pulseq-browser.ts --bundle --format=iife --global-name=Pulseq --outfile=web/pulseq-bundle.js --target=es2020 && node -e \"const fs=require('fs');fs.copyFileSync('images/logo.png','web/logo.png');fs.copyFileSync('web/pulseq-bundle.js','python/src/seqeyes/resources/pulseq-bundle.js');fs.copyFileSync('web/webview-bundle.js','python/src/seqeyes/resources/webview-bundle.js')\"", "predeploy": "npm run build:web" }, "devDependencies": { diff --git a/python/src/seqeyes/resources/webview-bundle.js b/python/src/seqeyes/resources/webview-bundle.js new file mode 100644 index 0000000..eb4e8c9 --- /dev/null +++ b/python/src/seqeyes/resources/webview-bundle.js @@ -0,0 +1,2667 @@ +/* SeqEyes WebView Bundle — auto-generated, do not edit */ +"use strict"; +/* ══ state.js ══ */ +/* ═══════════════════════════════════════════════════════════════════════ + Application state & configuration + ═══════════════════════════════════════════════════════════════════════ */ +var cc=document.getElementById('cc'),mc=document.getElementById('mc'),ctx=mc.getContext('2d'), + moc=document.getElementById('moc'),moctx=moc.getContext('2d'), + tt=document.getElementById('tt'),curEl=document.getElementById('cur'),legend=document.getElementById('legend'), + tuSel=document.getElementById('tu'),guSel=document.getElementById('gu'); +var exportBtn=document.getElementById('exportKspaceBtn'); +var pnsBtn=document.getElementById('pnsBtn'); +var BL=[],waveformOverview=null,TD=0,GR=1e-5,RR=1e-6,AR=1e-7,BR=1e-5; // blocks, duration, rasters [s] +var M={t:8,r:30,b:22,l:92}; // margins +var CH=['RF','\u03c6','Gx','Gy','Gz','ADC','Trig','PNS','M1x','M1y','M1z']; +var chColors=['var(--rf)','var(--rf)','var(--gx)','var(--gy)','var(--gz)','var(--adc)','var(--tr)','var(--fg)','var(--gx)','var(--gy)','var(--gz)']; +var chVis=[true,true,true,true,true,true,true,false,false,false,false]; // visibility toggles +var gMax=[1,6.28318,1,1,1,0,0,1,0.001,0.001,0.001]; // global max per channel +var ox=0,sc=1; // view offset [s] & scale [px/s] +var dr=false,dsx=0,dso=0; // drag state +var cursorT=0,cursorActive=false; // mouse time position +var timeUnit='ms',gradUnit='Hz/m'; // display unit selections +var showBB=false; // show block boundaries (default off) +var GAMMA=42576; // Hz/m per mT/m for 1H + +var ampZoom=[1,1,1,1,1,1,1]; +ampZoom[7]=1;ampZoom[8]=1;ampZoom[9]=1;ampZoom[10]=1; +/* K‑space data — pre‑computed on the extension side */ +var kTraj=null,kAdc=null,kTime=null,kAdcTime=null; +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; +var viewerNotices={}; +var kspaceSafetyWarning=null,kspaceSafetyBusy=false,kspaceSafetyPopupTimer=0; +var derivedRenderPointCount=0,derivedEnvelopeCurveCount=0,derivedRawCurveCount=0,waveformOverviewActive=false,rfRenderPointCount=0,rfRawCurveCount=0,rfReducedCurveCount=0,rfOverviewBucketCount=0,lastDrawDurationMs=0,viewerDrawCount=0,viewerCursorDrawCount=0; +var viewerDrawFrame=0,viewerDrawMinimap=false; +function isMobileSafetyLayout(){return !!(window.matchMedia&&window.matchMedia('(max-width: 768px), (pointer: coarse)').matches);} +function renderViewerNotices(){ + var el=document.getElementById('viewerNotice');if(!el)return;el.textContent=''; + var keys=Object.keys(viewerNotices),visible=0; + for(var i=0;i0);} +function getTouchPos(touch,el){var r=el.getBoundingClientRect();return {x:touch.clientX-r.left, y:touch.clientY-r.top};} +function getTouchMid(touches){return {x:(touches[0].clientX+touches[1].clientX)/2, y:(touches[0].clientY+touches[1].clientY)/2};} +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);} + +/* ── Responsive layout ────────────────────────────────────────────────── */ +var layoutMode='horizontal'; // 'horizontal' (kspace right) or 'vertical' (kspace bottom) +function detectLayoutMode(){ + var was=layoutMode; + layoutMode=(window.innerHeight>window.innerWidth)?'vertical':'horizontal'; + return layoutMode!==was; +} +function applyLayoutMode(){ + document.body.classList.remove('layout-vertical','layout-horizontal'); + document.body.classList.add('layout-'+layoutMode); + + var main=document.getElementById('main'); + var left=document.getElementById('left'); + var right=document.getElementById('right'); + var handle=document.getElementById('khandle'); + var rc=document.getElementById('rc'); + + if(layoutMode==='vertical'){ + // Direct inline !important styles — beats everything in CSS cascade + main.style.setProperty('flex-direction','column','important'); + left.style.setProperty('flex','1 1 0%','important'); + left.style.setProperty('min-height','0','important'); + left.style.setProperty('min-width','0','important'); + right.style.setProperty('width','100%','important'); + right.style.setProperty('height','0','important'); + right.style.setProperty('border-left','none','important'); + right.style.setProperty('border-top','1px solid var(--gr)','important'); + right.style.setProperty('transition','height .25s','important'); + right.style.setProperty('flex-shrink','0','important'); + right.style.setProperty('overflow','hidden','important'); + right.style.setProperty('position','relative','important'); + handle.style.setProperty('cursor','row-resize','important'); + handle.style.setProperty('left','0','important'); + handle.style.setProperty('right','0','important'); + handle.style.setProperty('top','0','important'); + handle.style.setProperty('width','100%','important'); + handle.style.setProperty('height','5px','important'); + handle.style.setProperty('bottom','auto','important'); + if(rc){rc.style.setProperty('flex-direction','row','important');} + // If kspace is open, apply open height inline + if(typeof kOpen!=='undefined'&&kOpen){ + right.style.setProperty('height','300px','important'); + } + }else{ + // Remove all inline !important overrides — base CSS takes over + main.style.cssText=''; + left.style.cssText=''; + right.style.cssText=''; + handle.style.cssText=''; + if(rc)rc.style.cssText=''; + // If kspace is open, restore width + if(typeof kOpen!=='undefined'&&kOpen){ + right.style.width='500px'; + } + } +} +function refreshLayout(){ + if(detectLayoutMode()){ + applyLayoutMode(); + rs(); + if(typeof drawKs==='function')drawKs(); + if(typeof drawMinimap==='function')drawMinimap(); + } +} +applyLayoutMode(); +// Use ResizeObserver on #main for reliable layout detection +var _mainEl=document.getElementById('main'); +if(_mainEl&&typeof ResizeObserver!=='undefined'){ + new ResizeObserver(function(){refreshLayout();}).observe(_mainEl); +} +// Fallback listeners +window.addEventListener('resize',function(){refreshLayout();}); +if(typeof window.matchMedia==='function'){ + window.matchMedia('(orientation: portrait)').addEventListener('change',function(){refreshLayout();}); +} + +/* ── Unit conversion helpers ──────────────────────────────────────────── */ +function gradConv(v){if(gradUnit==='mT/m')return v/GAMMA;if(gradUnit==='G/cm')return v/(GAMMA*10);return v;} +function gradUnitStr(){return gradUnit;} +function timeConv(t){if(timeUnit==='ms')return t*1e3;if(timeUnit==='us')return t*1e6;return t;} +function timeUnitStr(){return timeUnit;} + +/* ── Build legend ─────────────────────────────────────────────────────── */ +function buildLegend(){ + legend.innerHTML=''; + chColors.forEach(function(c,i){ + var d=document.createElement('div');d.className='li'+(chVis[i]?'':' off');d.title='Toggle '+CH[i]; + d.innerHTML='
'+CH[i]; + if(i>=8&&i<=10&&!m1Data)d.title='Calculate and show '+CH[i]; + else if(i===7&&!pnsData)d.title='Select a PNS ASC file before showing PNS'; + d.onclick=function(){ + if(i>=8&&i<=10&&!m1Data){requestM1(i);return;} + if(i===7&&!pnsData)return; + chVis[i]=!chVis[i]; + buildLegend();computeGlobalMax();draw(); + }; + legend.appendChild(d); + }); +} +buildLegend(); +tuSel.onchange=function(){timeUnit=tuSel.value;draw();}; +guSel.onchange=function(){gradUnit=guSel.value;draw();}; +function setExportButtonEnabled(enabled){if(exportBtn)exportBtn.disabled=!enabled;} +function applySerializedKspace(payload){ + kTraj=null;kAdc=null;kTime=null;kAdcTime=null; + if(!payload)return; + kTraj=[payload.kx,payload.ky,payload.kz];kTime=payload.tk; + var n=payload.nAdc||0; + if(n>0&&payload.axb){ + kAdc=[decodeB64F32(payload.axb,n),decodeB64F32(payload.ayb,n),decodeB64F32(payload.azb,n)]; + kAdcTime=decodeB64F32(payload.tab,n);uploadKSpaceGPU(); + } +} + +/* ── Data reception ───────────────────────────────────────────────────── */ +window.addEventListener('message',function(e){ + var m=e.data; + if(m.type==='progress'){ + var overlay=document.getElementById('poverlay'),fill=document.getElementById('pfill2'), + text=document.getElementById('ptext'),pct=document.getElementById('ppct'); + if(m.phase==='start'){ + setExportButtonEnabled(false); + overlay.style.display='flex';fill.style.width='0%'; + text.textContent=m.text||'Loading sequence\u2026';pct.textContent='0%'; + }else if(m.phase==='done'){ + fill.style.width='100%';pct.textContent='100%'; + text.textContent=m.text||'Ready'; + setTimeout(function(){overlay.style.display='none';},500); + }else{ + fill.style.width=(m.percent||0)+'%'; + pct.textContent=(m.percent||0)+'%'; + text.textContent=m.text||''; + } + return; + } + if(m.type==='sequenceData'){ + BL=m.blocks||[];TD=m.totalDuration||0;GR=m.gradRaster||1e-5; + setViewerNotice('sequence',m.notices&&m.notices.length?m.notices.join(' '):null); + setViewerNotice('kspaceOverrideFailure',null);setKspaceSafetyWarning(m.kspaceSafety||null); + setViewerNotice('m1',null);setViewerNotice('pns',null);setViewerNotice('pnsThreshold',null); + waveformOverview=createWaveformOverview(BL); + RR=m.rfRaster||RR;AR=m.adcRaster||AR;BR=m.blockRaster||BR; + blockPos=m.blockPositions||[]; + mmCache=null; // invalidate minimap cache on new data + applySerializedKspace(m.kspace); + m1Data=null;m1WindowData=null;m1WindowPending=null;pnsData=null;pnsWindowData=null;pnsWindowPending=null;chVis[7]=false;chVis[8]=false;chVis[9]=false;chVis[10]=false; + // Store timing metadata for minimap tooltip + if(m.timing) seqTiming=m.timing; else seqTiming=null; + computeGlobalMax(); + buildLegend(); + // Auto-zoom: if TR is known, zoom to first TR; otherwise fit full sequence + if(seqTiming&&seqTiming.trTimeSec>0){fitToFirstTR();}else{fit();} + draw();drawKs();drawMinimap(); + setExportButtonEnabled(true); + }else if(m.type==='kspaceData'){ + applySerializedKspace(m.kspace);finishDangerousKspaceCalculation(null);drawKs(); + if(typeof kOpen!=='undefined'&&!kOpen)document.getElementById('kbtn').click(); + }else if(m.type==='kspaceError'){ + finishDangerousKspaceCalculation(m.message||'Unknown error.'); + }else if(m.type==='m1Data'){ + m1Busy=false; + if(m.m1&&m.m1.valid){ + setViewerNotice('m1',null); + m1Data=createM1SeriesPayload(m.m1); + m1WindowData=null;m1WindowPending=null; + if(m1RestoreChannels){ + chVis[8]=!!m1RestoreChannels[0];chVis[9]=!!m1RestoreChannels[1];chVis[10]=!!m1RestoreChannels[2];m1RestoreChannels=null; + }else chVis[m1RequestedChannel]=true; + computeGlobalMax();buildLegend();draw(); + if(m.m1.warnings&&m.m1.warnings.length)console.warn('[SeqEyes M1]',m.m1.warnings.join('\n')); + setViewerNotice('m1Coarse',m.m1.coarse?'Large sequence: showing bounded full-sequence M1. Zoom to 100 TRs or fewer for automatic detail.':null); + }else{ + setViewerNotice('m1','M1 calculation failed: '+((m.m1&&m.m1.error)||'unknown error')+' Zoom in to inspect waveform detail.'); + } + }else if(m.type==='m1WindowData'){ + if(m1WindowPending&&m.requestId!==m1WindowPending.requestId)return; + m1WindowPending=null; + if(m.m1&&m.m1.valid){m1WindowData=createM1SeriesPayload(m.m1);draw();} + else if(m.m1&&m.m1.error)setViewerNotice('m1Detail','M1 detail was not calculated: '+m.m1.error); + }else if(m.type==='m1Error'){ + m1Busy=false; + setViewerNotice('m1',(m.message||'M1 calculation failed.')+(m.message&&/zoom in/i.test(m.message)?'':' Zoom in to inspect waveform detail.')); + }else if(m.type==='pnsData'){ + pnsBusy=false;if(pnsBtn)pnsBtn.disabled=false; + if(m.pns&&m.pns.valid){ + setViewerNotice('pns',null); + pnsData=createPnsSeriesPayload(m.pns); + pnsWindowData=null;pnsWindowPending=null;chVis[7]=true; + computeGlobalMax();buildLegend();draw(); + setViewerNotice('pnsThreshold',m.pns.ok?null:'PNS warning: predicted level reaches or exceeds 100%.'); + setViewerNotice('pnsCoarse',m.pns.coarse?'Large sequence: showing bounded full-sequence PNS. Zoom to 100 TRs or fewer for automatic detail.':null); + }else{ + setViewerNotice('pns','PNS calculation failed: '+((m.pns&&m.pns.error)||'unknown error')+' Zoom in to inspect waveform detail.'); + } + }else if(m.type==='pnsWindowData'){ + if(pnsWindowPending&&m.requestId!==pnsWindowPending.requestId)return; + pnsWindowPending=null; + if(m.pns&&m.pns.valid){ + pnsWindowData=createPnsSeriesPayload(m.pns); + draw(); + } + }else if(m.type==='pnsError'){ + pnsBusy=false;if(pnsBtn)pnsBtn.disabled=false; + setViewerNotice('pns',(m.message||'PNS calculation failed.')+(m.message&&/zoom in/i.test(m.message)?'':' Zoom in to inspect waveform detail.')); + }else if(m.type==='pnsSelectionCancelled'){ + pnsBusy=false;if(pnsBtn)pnsBtn.disabled=false; + } +}); + +/* ── Base64 → Float32Array decoder ─────────────────────────────────── */ +function decodeB64F32(b64,n){var bin=atob(b64),len=bin.length,b=new Uint8Array(len);for(var i=0;igMax[0])gMax[0]=a;} + if(b.gx&&b.gx.ty!=='none'&&Math.abs(b.gx.a||0)>gMax[2])gMax[2]=Math.abs(b.gx.a); + if(b.gy&&b.gy.ty!=='none'&&Math.abs(b.gy.a||0)>gMax[3])gMax[3]=Math.abs(b.gy.a); + if(b.gz&&b.gz.ty!=='none'&&Math.abs(b.gz.a||0)>gMax[4])gMax[4]=Math.abs(b.gz.a); + } + if(pnsData)gMax[7]=Math.max(gMax[7],pnsData.x.maxAbs,pnsData.y.maxAbs,pnsData.z.maxAbs,pnsData.n.maxAbs); + 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);} + gMax[0]=Math.max(gMax[0],100); +} + +function createPnsSeriesPayload(pns){ + function series(prefix,timeKey,valueKey){ + return pns.coarse + ?createEnvelopeSeries(pns[prefix+'0'],pns[prefix+'1'],pns[prefix+'min'],pns[prefix+'max'],pns[prefix+'first'],pns[prefix+'last'],1) + :createDerivedSeries(pns[timeKey]||pns.t,pns[valueKey],1); + } + return{ + coarse:!!pns.coarse, + startSec:isFinite(pns.startSec)?pns.startSec:null, + endSec:isFinite(pns.endSec)?pns.endSec:null, + x:series('x','tx','x'), + y:series('y','ty','y'), + z:series('z','tz','z'), + n:series('n','tn','n') + }; +} + +function createM1SeriesPayload(m1){ + function series(prefix,timeKey,valueKey){ + return m1.coarse + ?createEnvelopeSeries(m1[prefix+'0'],m1[prefix+'1'],m1[prefix+'min'],m1[prefix+'max'],m1[prefix+'first'],m1[prefix+'last'],1) + :createDerivedSeries(m1[timeKey]||m1.t,m1[valueKey],1); + } + return{ + coarse:!!m1.coarse, + startSec:isFinite(m1.startSec)?m1.startSec:null, + endSec:isFinite(m1.endSec)?m1.endSec:null, + referenceMode:m1.referenceMode||m1ReferenceMode, + warnings:m1.warnings||[], + x:series('x','tx','x'), + y:series('y','ty','y'), + z:series('z','tz','z') + }; +} + +function shouldRequestFineM1Window(vs,ve){ + if(!vscApi||!m1Data||!m1Data.coarse||!(chVis[8]||chVis[9]||chVis[10]))return false; + var dur=ve-vs; + return !!(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*100.5); +} + +function derivedWindowCovers(data,vs,ve){ + return !!(data&&data.startSec!==null&&data.endSec!==null&&data.startSec<=vs+1e-12&&data.endSec>=ve-1e-12); +} + +function requestM1Window(vs,ve){ + if(!shouldRequestFineM1Window(vs,ve))return; + if(derivedWindowCovers(m1WindowData,vs,ve)||derivedWindowCovers(m1WindowPending,vs,ve))return; + var dur=Math.max(ve-vs,minRasterTime()),pad=Math.max(dur*.5,seqTiming&&seqTiming.trTimeSec?seqTiming.trTimeSec:0); + var start=Math.max(0,vs-pad),end=Math.min(TD||ve,ve+pad); + m1WindowPending={startSec:start,endSec:end,requestId:++m1WindowRequestId}; + vscApi.postMessage({command:'calculateM1Window',requestId:m1WindowPending.requestId,startSec:start,endSec:end,maxPoints:120000,referenceMode:m1ReferenceMode}); +} + +function m1SeriesForView(vs,ve){ + if(!m1Data)return null; + if(shouldRequestFineM1Window(vs,ve)){ + requestM1Window(vs,ve); + if(derivedWindowCovers(m1WindowData,vs,ve)){setViewerNotice('m1Detail',null);setViewerNotice('m1Coarse','Large sequence: full sequence uses bounded M1; the current view is detailed.');return m1WindowData;} + } + if(m1Data.coarse)setViewerNotice('m1Coarse','Large sequence: showing bounded full-sequence M1. Zoom to 100 TRs or fewer for automatic detail.'); + return m1Data; +} + +function shouldRequestFinePnsWindow(vs,ve){ + if(!vscApi||!pnsData||!pnsData.coarse||!chVis[7])return false; + var dur=ve-vs; + return !!(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*100.5); +} + +function pnsWindowCovers(data,vs,ve){ + return derivedWindowCovers(data,vs,ve); +} + +function requestPnsWindow(vs,ve){ + if(!shouldRequestFinePnsWindow(vs,ve))return; + if(pnsWindowCovers(pnsWindowData,vs,ve)||pnsWindowCovers(pnsWindowPending,vs,ve))return; + var dur=Math.max(ve-vs,minRasterTime()),pad=Math.max(dur*.5,seqTiming&&seqTiming.trTimeSec?seqTiming.trTimeSec:0); + var start=Math.max(0,vs-pad),end=Math.min(TD||ve,ve+pad); + pnsWindowPending={startSec:start,endSec:end,requestId:++pnsWindowRequestId}; + vscApi.postMessage({command:'calculatePnsWindow',requestId:pnsWindowPending.requestId,startSec:start,endSec:end,maxPoints:120000}); +} + +function pnsSeriesForView(vs,ve){ + if(!pnsData)return null; + if(shouldRequestFinePnsWindow(vs,ve)){ + requestPnsWindow(vs,ve); + if(pnsWindowCovers(pnsWindowData,vs,ve)){setViewerNotice('pnsCoarse','Large sequence: full sequence uses bounded PNS; the current view is detailed.');return pnsWindowData;} + } + if(pnsData.coarse)setViewerNotice('pnsCoarse','Large sequence: showing bounded full-sequence PNS. Zoom to 100 TRs or fewer for automatic detail.'); + return pnsData; +} + +/* ── Minimap ──────────────────────────────────────────────────────────── */ +var mmCanvas=document.getElementById('mmc'),mmCtx=mmCanvas.getContext('2d'); +/* Offscreen cache: static block bands rendered once, then blitted each frame. + * Invalidated on data load or theme change. */ +var mmCache=null, mmCacheW=0, mmCacheDpr=0; + +/** (Re)build the offscreen minimap cache using ImageData pixel‑buffer rendering. + * Avoids O(blocks) canvas API calls — instead does one integer‑array pass + * per block and a single putImageData() at the end. ~20× faster than fillRect. */ +function buildMinimapCache(){ + mmCache=null; + if(!TD||TD<=0||!BL.length)return; + var dpr=window.devicePixelRatio||1; + var W=mmCanvas.width, H=mmCanvas.height; // physical pixels + if(W<=0||H<=0)return; + + var s=getComputedStyle(document.body); + var cssW=W/dpr, scM=cssW/TD; + + // Parse theme colours → [r,g,b] + var rfRGB=parseHexRGB(s.getPropertyValue('--rf').trim()); + var gxRGB=parseHexRGB(s.getPropertyValue('--gx').trim()); + var gyRGB=parseHexRGB(s.getPropertyValue('--gy').trim()); + var gzRGB=parseHexRGB(s.getPropertyValue('--gz').trim()); + var adcRGB=parseHexRGB(s.getPropertyValue('--adc').trim()); + var bgRGB=parseHexRGB(s.getPropertyValue('--trbg').trim()); + + // Band layout in physical pixels + var rfY0=0, rfY1=5*dpr; + var gY0=5*dpr, gH=2*dpr; + var aY0=11*dpr, aY1=H; + + // ── Step 1: accumulate per‑pixel‑column counters ── + // For each pixel column we store a hit count per band (integer 0..255). + var pw=Math.max(1,Math.ceil(cssW)); + var rfCnt=new Uint8Array(pw), adcCnt=new Uint8Array(pw); + var gxCnt=new Uint8Array(pw), gyCnt=new Uint8Array(pw), gzCnt=new Uint8Array(pw); + + // Stride: for very large sequences, skip blocks that map to sub‑pixel positions + var stride=BL.length>20000?Math.floor(BL.length/15000):1; + + for(var i=0;i=pw)continue; + px0=Math.max(0,px0);px1=Math.min(pw,px1); + + if(b.rf) for(var p=px0;p0){ + var a=Math.min(140,rfCnt[px]*55); + fillBand(d,W,x0,x1,rfY0,rfY1,rfRGB[0],rfRGB[1],rfRGB[2],a); + } + // Gx band + if(gxCnt[px]>0){ + var a=Math.min(150,gxCnt[px]*60); + fillBand(d,W,x0,x1,gY0,gY0+gH,gxRGB[0],gxRGB[1],gxRGB[2],a); + } + // Gy band + if(gyCnt[px]>0){ + var a=Math.min(150,gyCnt[px]*60); + fillBand(d,W,x0,x1,gY0+gH,gY0+2*gH,gyRGB[0],gyRGB[1],gyRGB[2],a); + } + // Gz band + if(gzCnt[px]>0){ + var a=Math.min(150,gzCnt[px]*60); + fillBand(d,W,x0,x1,gY0+2*gH,gY0+3*gH,gzRGB[0],gzRGB[1],gzRGB[2],a); + } + // ADC band + if(adcCnt[px]>0){ + var a=Math.min(130,adcCnt[px]*50); + fillBand(d,W,x0,x1,aY0,aY1,adcRGB[0],adcRGB[1],adcRGB[2],a); + } + } + + // Write to offscreen canvas + var oc=document.createElement('canvas'); + oc.width=W;oc.height=H; + oc.getContext('2d').putImageData(imgData,0,0); + mmCache=oc;mmCacheW=W;mmCacheDpr=dpr; +} + +/** Fill a rectangular band in the ImageData buffer with alpha blending. */ +function fillBand(d,W,x0,x1,y0,y1,r,g,b,a){ + for(var y=y0;y0&&seqTiming.trCount>1){ + mmCtx.strokeStyle=s.getPropertyValue('--fg').trim(); + mmCtx.globalAlpha=0.10;mmCtx.lineWidth=0.4; + mmCtx.beginPath(); + var trStep=Math.max(1,Math.ceil(seqTiming.trCount/Math.max(1,W))); + for(var tr=0;tr<=seqTiming.trCount;tr+=trStep){ + var tx=tr*seqTiming.trTimeSec*scM; + if(tx<=W){mmCtx.moveTo(tx,0);mmCtx.lineTo(tx,H);} + } + mmCtx.stroke();mmCtx.globalAlpha=1; + } + + // ── Viewport highlight ── + var mcW=mc.width/dpr; + var visibleDur = plotWidth() / Math.max(sc, 1e-12); + var vx0=ox*scM, vx1=(ox+visibleDur)*scM; + vx0=Math.max(0,vx0);vx1=Math.min(W,vx1); + var vw=Math.max(2,vx1-vx0); + // Subtle wash + mmCtx.fillStyle=s.getPropertyValue('--fg').trim();mmCtx.globalAlpha=0.18; + mmCtx.fillRect(vx0,0,vw,H); + // Edge markers — only when viewport is not flush against the canvas edge + mmCtx.globalAlpha=0.45; + mmCtx.fillStyle=s.getPropertyValue('--adc').trim(); + if(vx0>0.5)mmCtx.fillRect(vx0,0,1,H); + if(vx10)info+='TR='+fmtDur2(seqTiming.trTimeSec)+(seqTiming.hasExplicitTR?'':'~')+' '; + if(seqTiming.teTimeSec>0)info+='TE='+fmtDur2(seqTiming.teTimeSec)+' '; + if(seqTiming.trCount>1)info+=seqTiming.trCount+' TRs'; + } + if(info){ + mmCtx.fillStyle='rgba(0,0,0,0.35)';mmCtx.font='9px monospace';mmCtx.textAlign='left'; + mmCtx.fillText(info,5,H-3); + mmCtx.fillStyle=s.getPropertyValue('--fg').trim();mmCtx.globalAlpha=0.9; + mmCtx.fillText(info,4,H-4); + mmCtx.globalAlpha=1; + } +} + +function fmtDur2(s){ + if(s>=1)return s.toFixed(2)+'s'; + if(s>=0.001)return(s*1e3).toFixed(1)+'ms'; + return(s*1e6).toFixed(0)+'\u00b5s'; +} + +/* ── Canvas resize ────────────────────────────────────────────────────── */ +function rs(){ + var dpr=window.devicePixelRatio||1,r=cc.getBoundingClientRect(); + mc.width=r.width*dpr;mc.height=r.height*dpr; + mc.style.width=r.width+'px';mc.style.height=r.height+'px'; + ctx.setTransform(dpr,0,0,dpr,0,0); + moc.width=r.width*dpr;moc.height=r.height*dpr; + moc.style.width=r.width+'px';moc.style.height=r.height+'px'; + moctx.setTransform(dpr,0,0,dpr,0,0); + // Adaptive margins for small/narrow screens + if(layoutMode==='vertical'||r.width<600){ + M.l=60;M.r=3;M.t=5;M.b=18; + }else{ + M.l=92;M.r=4;M.t=8;M.b=22; + } + // Resize minimap canvas too + var mr=document.getElementById('mmap').getBoundingClientRect(); + mmCanvas.width=mr.width*dpr;mmCanvas.height=mr.height*dpr; + mmCanvas.style.width=mr.width+'px';mmCanvas.style.height=mr.height+'px'; + mmCtx.setTransform(dpr,0,0,dpr,0,0); + draw();drawMinimap(); +} +window.addEventListener('resize',rs);new ResizeObserver(rs).observe(cc); + +/* ── Coordinate mapping ───────────────────────────────────────────────── */ +function visChannels(){var v=[];for(var i=0;i0;}); + return vals.length?Math.min.apply(null,vals):1e-7; +} +function visibleDuration(){return plotWidth()/Math.max(sc,1e-12);} +function clampView(){ + var pw=plotWidth(); + if(!isFinite(TD)||TD<=0){ox=0;sc=1;return;} + if(!isFinite(sc)||sc<=0)sc=pw/TD; + var minDur=Math.min(TD,minRasterTime()); + var dur=visibleDuration(); + if(!isFinite(dur)||dur<=0)dur=TD; + dur=Math.max(minDur,Math.min(TD,dur)); + sc=pw/dur; + var maxOx=Math.max(0,TD-dur); + if(!isFinite(ox))ox=0; + ox=Math.max(0,Math.min(ox,maxOx)); +} +function zoomAt(screenX,zf){ + if(!isFinite(zf)||zf<=0)return false; + var oldSc=sc,oldOx=ox; + var tm=x2t(screenX); + sc*=zf; + var dur=visibleDuration(); + if(!isFinite(dur)||dur<=0)dur=TD||minRasterTime(); + var maxDur=TD||dur,minDur=Math.min(maxDur,minRasterTime()); + dur=Math.max(minDur,Math.min(maxDur,dur)); + sc=plotWidth()/dur; + ox=tm-(screenX-M.l)/sc; + clampView(); + var scaleTol=Math.max(1,Math.abs(oldSc))*1e-12,offsetTol=Math.max(1,TD)*1e-12; + return Math.abs(sc-oldSc)>scaleTol||Math.abs(ox-oldOx)>offsetTol; +} +function zoomAtCenter(zf){ + return zoomAt(M.l+plotWidth()/2,zf); +} + +/* ── Initial view: zoom to first TR ───────────────────────────────────── */ +function fitToFirstTR(){ + if(!seqTiming||seqTiming.trTimeSec<=0){fit();return;} + var w=mc.width/(window.devicePixelRatio||1); + // Show first TR with 10% padding on each side + var trDur=seqTiming.trTimeSec; + sc=(w-M.l-M.r)/(trDur*1.2); + ox=Math.max(0,-trDur*0.1); // small negative offset for left padding + clampView(); +} + + +/* ══ derived-series.js ══ */ +/* Multiresolution first/min/max/last summaries for derived waveform rendering. */ +function createDerivedSeries(time,values,scale){ + var n=Math.min(time?time.length:0,values?values.length:0),maxAbs=0; + var series={t:time||[],v:values||[],n:n,scale:scale||1,maxAbs:0,levels:[]}; + for(var i=0;ivalues[maxIndex])maxIndex=j; + } + }else{ + var childStart=b*4,childEnd=Math.min(child.mins.length,childStart+4); + for(var c=childStart;cvalues[maxIndex])maxIndex=index; + } + } + } + mins[b]=minIndex;maxs[b]=maxIndex; + } + child={bucketSize:bucketSize,mins:mins,maxs:maxs}; + series.levels.push(child); + if(count<=1)break; + bucketSize*=4; + } + return series; +} + +/* Explicit time-bucket envelopes used by bounded full-sequence M1/PNS. */ +function createEnvelopeSeries(startTime,endTime,minValues,maxValues,firstValues,lastValues,scale){ + var n=Math.min(startTime?startTime.length:0,endTime?endTime.length:0,minValues?minValues.length:0,maxValues?maxValues.length:0); + var series={kind:'envelope',n:n,scale:scale||1,maxAbs:0,levels:[]}; + if(n<1)return series; + var base={count:n,bucketSize:1,t0:startTime,t1:endTime,min:minValues,max:maxValues,first:firstValues||minValues,last:lastValues||maxValues}; + series.levels.push(base); + for(var i=0;i1){level=mergeEnvelopeLevel(level);series.levels.push(level);} + return series; +} + +function mergeEnvelopeLevel(child){ + 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)}; + level.min.fill(Infinity);level.max.fill(-Infinity); + for(var bucket=0;bucketlevel.max[bucket])level.max[bucket]=child.max[i];} + } + return level; +} + +function envelopeLevelWindow(level,viewStart,viewEnd){ + var i0=Math.max(0,lowerBoundSeries(level.t1,viewStart)); + var i1=Math.min(level.count,upperBoundSeries(level.t0,viewEnd)); + return{i0:i0,i1:i1,count:Math.max(0,i1-i0)}; +} + +function selectEnvelopeLevel(series,viewStart,viewEnd,maxBuckets){ + if(!series||series.kind!=='envelope'||!series.levels.length)return null; + var selected=series.levels[series.levels.length-1]; + for(var i=0;i=base.count||timeSecbase.t1[index])return null; + return{ + startTime:base.t0[index],endTime:base.t1[index], + min:base.min[index]*series.scale,max:base.max[index]*series.scale, + first:base.first[index]*series.scale,last:base.last[index]*series.scale + }; +} + +function lowerBoundSeries(values,target){ + var lo=0,hi=values.length; + while(lo>1;if(values[mid]>1;if(values[mid]<=target)lo=mid+1;else hi=mid} + return lo; +} + +function derivedSeriesWindow(series,viewStart,viewEnd){ + if(!series||series.n<1)return{i0:0,i1:0,count:0}; + var i0=Math.max(0,lowerBoundSeries(series.t,viewStart)-1); + var i1=Math.min(series.n,upperBoundSeries(series.t,viewEnd)+1); + return{i0:i0,i1:i1,count:Math.max(0,i1-i0)}; +} + +function forEachDerivedRange(series,viewStart,viewEnd,maxBuckets,visit){ + var windowRange=derivedSeriesWindow(series,viewStart,viewEnd); + if(windowRange.count<1)return 0; + var i0=windowRange.i0,i1=windowRange.i1,t=series.t,v=series.v,n=series.n; + var level=series.levels[series.levels.length-1]; + for(var li=0;liv[maxIndex])maxIndex=index; + } + } + if(minIndex<0||maxIndex<0)return; + var center=0.5*(t[start]+t[end-1]); + if(!isFinite(center))return; + visit(center,v[minIndex]*series.scale,v[maxIndex]*series.scale); + emitted++; + } + if(firstBucket===lastBucket){ + emitRange(i0,i1); + return emitted; + } + emitRange(i0,Math.min(i1,(firstBucket+1)*size)); + for(var bucket=firstBucket+1;bucket=i1||!isFinite(t[index])||!isFinite(v[index]))return; + visit(t[index],v[index]*series.scale);emitted++; + } + if(count<=maxPoints||!series.levels.length){ + for(var raw=i0;rawv[maxIndex])maxIndex=index; + } + emitOrdered([start,minIndex,maxIndex,end-1]); + } + function emitOrdered(indices){ + indices.sort(function(a,b){return a-b}); + var previous=-1; + for(var index=0;indexvalues[maxIndex])maxIndex=i;} + var indices=[start,minIndex,maxIndex,end-1].sort(function(a,b){return a-b;}),previous=-1; + for(var j=0;jsamplePeak){samplePeak=Math.abs(rf.m[i]);peakTime=rf.t[i];} + if(!isFinite(rf.pk))peak=Math.max(0,samplePeak); + if(!isFinite(rf.ar))for(var sample=1;sample0&&end>start)area=peak*(end-start); + starts.push(start);ends.push(end);peaks.push(peak);peakTimes.push(peakTime);areas.push(area);waveforms.push(rf); + } + 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}; +} + +function binRfEvents(series,viewStart,viewEnd,pixelCount,widePixelThreshold){ + 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=[]; + 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}; + var secondsPerPixel=(viewEnd-viewStart)/count,first=Math.max(0,lowerBoundSeries(series.end,viewStart)); + for(var i=first;iviewEnd)break;if(end=widePixelThreshold){wide.push(i);continue;} + var firstBin=Math.max(0,Math.min(count-1,Math.floor((clippedStart-viewStart)/secondsPerPixel))); + var lastBin=Math.max(firstBin,Math.min(count-1,Math.ceil((clippedEnd-viewStart)/secondsPerPixel)-1)); + for(var bin=firstBin;bin<=lastBin;bin++){ + var binStart=viewStart+bin*secondsPerPixel,binEnd=binStart+secondsPerPixel; + var overlap=Math.max(0,Math.min(clippedEnd,binEnd)-Math.max(clippedStart,binStart)); + if(duration<=0){if(bin!==firstBin)continue;overlap=secondsPerPixel;} + if(overlap<=0)continue; + var overlapStart=Math.max(clippedStart,binStart),overlapEnd=Math.min(clippedEnd,binEnd),eventPeak=series.peak[i]; + if(events[bin]<1){occupiedStart[bin]=overlapStart;occupiedEnd[bin]=overlapEnd;peakTime[bin]=Math.max(overlapStart,Math.min(overlapEnd,series.peakTime[i]));} + else{occupiedStart[bin]=Math.min(occupiedStart[bin],overlapStart);occupiedEnd[bin]=Math.max(occupiedEnd[bin],overlapEnd);} + if(eventPeak>peak[bin]){peak[bin]=eventPeak;peakTime[bin]=Math.max(overlapStart,Math.min(overlapEnd,series.peakTime[i]));} + area[bin]+=series.area[i]*(duration>0?overlap/duration:1);events[bin]++; + } + } + return{peak:peak,peakTime:peakTime,occupiedStart:occupiedStart,occupiedEnd:occupiedEnd,area:area,events:events,wide:wide,secondsPerPixel:secondsPerPixel}; +} + +/* Block-indexed min/max summaries for RF, gradients, and ADC occupancy. */ +function createWaveformOverview(blocks){ + if(!blocks||!blocks.length)return null; + var level=buildWaveformOverviewLevel(blocks,1),levels=[level]; + while(level.count>1){level=mergeWaveformOverviewLevel(level);levels.push(level);} + return{levels:levels,blockCount:blocks.length,pointPrefix:createWaveformPointPrefixes(blocks),rfEvents:createRfEventOverview(blocks)}; +} + +function createEmptyWaveformOverviewLevel(count,bucketSize){ + var level={count:count,bucketSize:bucketSize,t0:new Float64Array(count),t1:new Float64Array(count), + rfMin:new Float64Array(count),rfMax:new Float64Array(count),gxMin:new Float64Array(count),gxMax:new Float64Array(count), + rfStart:new Float64Array(count),rfEnd:new Float64Array(count), + gyMin:new Float64Array(count),gyMax:new Float64Array(count),gzMin:new Float64Array(count),gzMax:new Float64Array(count), + gxStart:new Float64Array(count),gxEnd:new Float64Array(count),gyStart:new Float64Array(count),gyEnd:new Float64Array(count),gzStart:new Float64Array(count),gzEnd:new Float64Array(count), + adcStart:new Float64Array(count),adcEnd:new Float64Array(count)}; + var mins=[level.rfMin,level.rfStart,level.gxMin,level.gyMin,level.gzMin,level.gxStart,level.gyStart,level.gzStart,level.adcStart]; + var maxs=[level.rfMax,level.rfEnd,level.gxMax,level.gyMax,level.gzMax,level.gxEnd,level.gyEnd,level.gzEnd,level.adcEnd]; + for(var m=0;m1){var step=Math.max(1,Math.ceil(adc.n/200));adcPhase=Math.floor(adc.n/step)+1;} + prefix.phase[i+1]=prefix.phase[i]+rfPhase+adcPhase; + prefix.gx[i+1]=prefix.gx[i]+overviewGradientPointCount(block.gx); + prefix.gy[i+1]=prefix.gy[i]+overviewGradientPointCount(block.gy); + prefix.gz[i+1]=prefix.gz[i]+overviewGradientPointCount(block.gz); + prefix.adc[i+1]=prefix.adc[i]+(adc?1:0); + } + return prefix; +} + +function overviewGradientPointCount(gradient){ + return gradient&&gradient.ty!=='none'&&gradient.t&&gradient.w?Math.min(gradient.t.length,gradient.w.length):0; +} + +function includeOverviewValues(values,minArray,maxArray,bucket){ + if(!values)return; + for(var i=0;imaxArray[bucket])maxArray[bucket]=value; + } +} + +function includeOverviewRf(rf,level,bucket){ + if(!rf)return; + includeOverviewValues(rf.m,level.rfMin,level.rfMax,bucket); + if(level.rfMin[bucket]===Infinity)includeOverviewValues([rf.a||0],level.rfMin,level.rfMax,bucket); + 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); + if(isFinite(start)&&startlevel.rfEnd[bucket])level.rfEnd[bucket]=end; +} + +function includeOverviewGradient(gradient,level,key,bucket){ + if(!gradient||gradient.ty==='none'||!gradient.t||!gradient.w)return; + var n=Math.min(gradient.t.length,gradient.w.length);if(n<2)return; + var maxAbs=0;for(var i=0;iendArray[bucket])endArray[bucket]=t1; + if(v0maxArray[bucket])maxArray[bucket]=v0;if(v1>maxArray[bucket])maxArray[bucket]=v1; + } +} + +function buildWaveformOverviewLevel(blocks,bucketSize){ + var count=Math.ceil(blocks.length/bucketSize),level=createEmptyWaveformOverviewLevel(count,bucketSize); + for(var bucket=0;bucketlevel.adcEnd[bucket])level.adcEnd[bucket]=adcEnd; + } + } + } + return level; +} + +function mergeWaveformOverviewLevel(child){ + var count=Math.ceil(child.count/4),level=createEmptyWaveformOverviewLevel(count,child.bucketSize*4); + var channels=[['rfMin','rfMax'],['rfStart','rfEnd'],['gxMin','gxMax'],['gyMin','gyMax'],['gzMin','gzMax'],['gxStart','gxEnd'],['gyStart','gyEnd'],['gzStart','gzEnd'],['adcStart','adcEnd']]; + for(var bucket=0;bucketlevel[maxKey][bucket])level[maxKey][bucket]=child[maxKey][i]; + } + } + } + return level; +} + +function waveformVisiblePointCount(overview,key,startBlock,endBlock){ + if(!overview||!overview.pointPrefix||!overview.pointPrefix[key])return 0; + 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)); + return prefix[end]-prefix[start]; +} + +function waveformVisibleGradientPointCount(blocks,key,startBlock,endBlock,viewStart,viewEnd){ + var count=0; + for(var blockIndex=startBlock;blockIndexw-M.r)return; + var color=getComputedStyle(document.body).getPropertyValue('--cr').trim(); + moctx.strokeStyle=color;moctx.lineWidth=0.8;moctx.setLineDash([4,3]); + moctx.beginPath();moctx.moveTo(cx,M.t);moctx.lineTo(cx,h-M.b);moctx.stroke();moctx.setLineDash([]); + moctx.fillStyle=color;moctx.font='10px monospace';moctx.textAlign='center'; + var label=fmtT(timeConv(cursorT)),tw=moctx.measureText(label).width; + moctx.fillText(label,Math.max(M.l+tw/2+4,Math.min(cx,w-M.r-tw/2-4)),M.t-1); +} + +/* ── Zero lines & grid ────────────────────────────────────────────────── */ +function drawZeroLines(w,h,s){ + ctx.strokeStyle=s.getPropertyValue('--gr').trim();ctx.lineWidth=0.4;ctx.setLineDash([2,4]); + var vc=visChannels();for(var vi=0;vi=2&&i<=4)||(i>=8&&i<=10)){ctx.beginPath();ctx.moveTo(M.l,cy(vi));ctx.lineTo(w-M.r,cy(vi));ctx.stroke();}} + ctx.setLineDash([]); +} +function drawBlockBounds(w,h,vs,ve,s){ + ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=0.6;ctx.setLineDash([3,6]); + var range=visibleBlockRange(vs,ve); + for(var i=range.start;iw-M.r)continue; + ctx.beginPath();ctx.moveTo(x,M.t);ctx.lineTo(x,h-M.b);ctx.stroke(); + ctx.fillStyle=s.getPropertyValue('--ax').trim();ctx.font='9px monospace';ctx.textAlign='center'; + ctx.fillText('#'+BL[i].i,x,M.t-2); + } + ctx.setLineDash([]); +} +function drawGrid(w,h,vs,ve,s){ + ctx.strokeStyle=s.getPropertyValue('--gr').trim();ctx.lineWidth=0.5; + var st=nice((ve-vs)/8),t=Math.floor(vs/st)*st; + 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;} + var ch=cH(),vc=visChannels(); + 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();} +} + +function channelRange(ci){ + var z=ampZoom[ci]||1; + if(ci===0)return Math.max((gMax[0]||1)*z,1e-9); + if(ci===1)return Math.max(6.28318*z,1e-9); + if(ci>=2&&ci<=4)return Math.max((gMax[ci]||1)*z,0.001); + if(ci===7)return Math.max((gMax[7]||1)*z,0.001); + if(ci>=8&&ci<=10)return Math.max((gMax[ci]||1)*z,1e-12); + return 1; +} +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);} +function rowClip(vi,ch,fn){ + 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); + if(rw<=0||bottom<=top)return; + ctx.save();ctx.beginPath();ctx.rect(M.l,top,rw,bottom-top);ctx.clip(); + try{fn();}finally{ctx.restore();} +} + +/* ── Axes (X time + Y per channel) ────────────────────────────────────── */ +function drawAxes(w,h,vs,ve,s){ + var narrow=(typeof layoutMode!=='undefined'&&layoutMode==='vertical')||w<600; + var axFont=narrow?'9px monospace':'10px monospace'; + var chFont=narrow?'bold 14px monospace':'bold 18px monospace'; + // X-axis + ctx.fillStyle=s.getPropertyValue('--lb').trim();ctx.font=axFont;ctx.textAlign='center'; + ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=1; + ctx.beginPath();ctx.moveTo(M.l,h-M.b);ctx.lineTo(w-M.r,h-M.b);ctx.stroke(); + var st=nice((ve-vs)/8),t=Math.floor(vs/st)*st; + while(t<=ve){ctx.fillText(fmtT(timeConv(t)),t2x(t),h-M.b+14);t+=st;} + ctx.textAlign='right'; + ctx.fillText('time ('+timeUnitStr()+')',w-4,h-M.b+4); + + // Y-axes — one per visible channel + var vc=visChannels(),ch=cH(); + for(var vi=0;vi=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);} + else if(ci===5){ctx.fillText('on',lblX,M.t+vi*ch+12);} + else if(ci===6){ctx.fillText('ch',lblX,M.t+vi*ch+12);} + 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);} + 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);} + + // Small tick marks + ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=0.5; + ctx.beginPath();ctx.moveTo(M.l-2,M.t+vi*ch);ctx.lineTo(M.l+2,M.t+vi*ch);ctx.stroke(); + ctx.beginPath();ctx.moveTo(M.l-2,y0);ctx.lineTo(M.l+2,y0);ctx.stroke(); + ctx.beginPath();ctx.moveTo(M.l-2,M.t+(vi+1)*ch);ctx.lineTo(M.l+2,M.t+(vi+1)*ch);ctx.stroke(); + } +} + +/* ── Derived calculation overlays: PNS and M1 ────────────────────────── */ +function drawDerivedChannels(vs,ve,s){ + var ch=cH(),vc=visChannels(); + var viP=-1,viM1x=-1,viM1y=-1,viM1z=-1; + for(var vi=0;vi=0){ + drawPercentSeries(pnsDrawData.x,viP,7,s.getPropertyValue('--gx').trim(),ch,vs,ve); + drawPercentSeries(pnsDrawData.y,viP,7,s.getPropertyValue('--gy').trim(),ch,vs,ve); + drawPercentSeries(pnsDrawData.z,viP,7,s.getPropertyValue('--gz').trim(),ch,vs,ve); + ctx.setLineDash([5,3]); + drawPercentSeries(pnsDrawData.n,viP,7,s.getPropertyValue('--fg').trim(),ch,vs,ve); + ctx.setLineDash([]); + } + var m1DrawData=m1SeriesForView(vs,ve); + if(m1DrawData){ + if(viM1x>=0)drawBipolarSeries(m1DrawData.x,viM1x,8,s.getPropertyValue('--gx').trim(),ch,vs,ve); + if(viM1y>=0)drawBipolarSeries(m1DrawData.y,viM1y,9,s.getPropertyValue('--gy').trim(),ch,vs,ve); + if(viM1z>=0)drawBipolarSeries(m1DrawData.z,viM1z,10,s.getPropertyValue('--gz').trim(),ch,vs,ve); + } +} + +function groupEnvelopeRanges(ranges,maxGapPx){ + var groups=[],group=[]; + for(var i=0;imaxGapPx){groups.push(group);group=[];} + group.push(ranges[i]); + } + if(group.length)groups.push(group);return groups; +} + +function drawStepEnvelopeGroups(groups,color,fillAlpha){ + for(var groupIndex=0;groupIndex=0;lowerIndex--){ctx.lineTo(ranges[lowerIndex][1],ranges[lowerIndex][3]);ctx.lineTo(ranges[lowerIndex][0],ranges[lowerIndex][3]);} + ctx.closePath();ctx.fill(); + ctx.strokeStyle=color;ctx.lineWidth=.8;ctx.globalAlpha=.72;ctx.beginPath();ctx.moveTo(ranges[0][0],ranges[0][2]); + for(var upper=0;upper=clipL&&x0<=clipR)ranges.push([Math.max(clipL,x0),Math.min(clipR,x1),upper,lower]); + }); + derivedRenderPointCount+=count*4;drawStepEnvelopeGroups(groupEnvelopeRanges(ranges,1),c,.12);return; + } + var rawLimit=Math.max(64,Math.ceil(plotW*2)); + var fineLimit=Math.max(128,Math.ceil(plotW*8)); + var windowRange=derivedSeriesWindow(series,vs,ve); + var useFinePns=shouldUseFinePnsRendering(windowRange.count,plotW,vs,ve); + ctx.strokeStyle=c;ctx.lineWidth=1;ctx.beginPath(); + if(windowRange.count<=rawLimit||useFinePns){ + derivedRawCurveCount++; + var f=1; + derivedRenderPointCount+=forEachDerivedPoint(series,vs,ve,useFinePns?fineLimit:rawLimit,function(tv,vv){ + var sx=t2x(tv);if(!isFinite(sx)||sxclipR){f=1;return} + var sy=base-vv*scale;if(!isFinite(sy)){f=1;return} + if(f){ctx.moveTo(sx,sy);f=0}else ctx.lineTo(sx,sy); + }); + }else{ + derivedEnvelopeCurveCount++; + var first=1; + derivedRenderPointCount+=forEachDerivedRange(series,vs,ve,Math.max(32,Math.ceil(clipR-clipL)),function(tv,_minValue,maxValue){ + var sx=t2x(tv),sy=base-maxValue*scale; + if(!isFinite(sx)||!isFinite(sy)||sxclipR){first=1;return} + if(first){ctx.moveTo(sx,sy);first=0}else ctx.lineTo(sx,sy); + }); + } + ctx.stroke(); + }); +} + +function shouldUseFinePnsRendering(count,plotW,vs,ve){ + var dur=ve-vs; + if(seqTiming&&seqTiming.trTimeSec>0&&dur<=seqTiming.trTimeSec*10.5)return true; + return count<=Math.max(256,Math.ceil(plotW*12)); +} + +function drawBipolarSeries(series,vi,ci,c,ch,vs,ve){ + if(!series||series.n<(series.kind==='envelope'?1:2))return; + rowClip(vi,ch,function(){ + var maxA=channelRange(ci),y=cy(vi),scale=ch*.4/maxA; + var clipL=M.l,clipR=mc.width/(window.devicePixelRatio||1)-M.r; + if(series.kind==='envelope'){ + derivedEnvelopeCurveCount++; + var envelopeRanges=[]; + var envelopeCount=forEachEnvelopeRange(series,vs,ve,Math.max(32,Math.ceil(clipR-clipL)),function(t0,t1,minValue,maxValue){ + var x0=t2x(t0),x1=t2x(t1),upper=y-maxValue*scale,lower=y-minValue*scale; + 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]); + }); + derivedRenderPointCount+=envelopeCount*4;drawStepEnvelopeGroups(groupEnvelopeRanges(envelopeRanges,1),c,.14);return; + } + var rawLimit=Math.max(128,Math.ceil((clipR-clipL)*8)); + if(derivedSeriesWindow(series,vs,ve).count<=rawLimit){ + derivedRawCurveCount++; + ctx.strokeStyle=c;ctx.lineWidth=1;ctx.beginPath();var f=1; + derivedRenderPointCount+=forEachDerivedPoint(series,vs,ve,rawLimit,function(tv,vv){ + var sx=t2x(tv);if(!isFinite(sx)||sxclipR){f=1;return} + var sy=y-vv*scale;if(!isFinite(sy)){f=1;return} + if(f){ctx.moveTo(sx,sy);f=0}else ctx.lineTo(sx,sy); + }); + ctx.stroke(); + }else{ + derivedEnvelopeCurveCount++; + var ranges=[]; + var rangeCount=forEachDerivedRange(series,vs,ve,Math.max(32,Math.ceil(clipR-clipL)),function(tv,minValue,maxValue){ + var sx=t2x(tv),upper=y-maxValue*scale,lower=y-minValue*scale; + if(isFinite(sx)&&isFinite(upper)&&isFinite(lower)&&sx>=clipL&&sx<=clipR)ranges.push([sx,upper,lower]); + }); + derivedRenderPointCount+=rangeCount*2; + if(ranges.length){ + ctx.fillStyle=c;ctx.globalAlpha=.14;ctx.beginPath(); + ctx.moveTo(ranges[0][0],ranges[0][1]); + for(var ri=1;ri=0;rj--)ctx.lineTo(ranges[rj][0],ranges[rj][2]); + ctx.closePath();ctx.fill(); + ctx.globalAlpha=.65;ctx.strokeStyle=c;ctx.lineWidth=.8; + ctx.beginPath();ctx.moveTo(ranges[0][0],(ranges[0][1]+ranges[0][2])*.5); + for(var rk=1;rk=range.end)return; + var ch=cH(),vc=visChannels(),rows=[-1,-1,-1,-1,-1,-1,-1]; + for(var vi=0;vi=0&&vc[vi]<=6)rows[vc[vi]]=vi; + var colors={ + rf:s.getPropertyValue('--rf').trim(),rff:s.getPropertyValue('--rff').trim(), + gx:s.getPropertyValue('--gx').trim(),gy:s.getPropertyValue('--gy').trim(),gz:s.getPropertyValue('--gz').trim(), + adc:s.getPropertyValue('--adc').trim(),adf:s.getPropertyValue('--adf').trim(), + tr:s.getPropertyValue('--tr').trim(),fg:s.getPropertyValue('--fg').trim() + }; + var pixelBudget=Math.max(1,Math.floor(plotWidth())); + var overview=selectWaveformOverview(waveformOverview,range.start,range.end,pixelBudget); + function useOverview(key){return !!overview&&waveformVisiblePointCount(waveformOverview,key,range.start,range.end)>pixelBudget;} + function useGradientOverview(key){return !!overview&&waveformVisibleGradientPointCount(BL,key,range.start,range.end,vs,ve)>pixelBudget*8;} + var rfPoints=waveformVisiblePointCount(waveformOverview,'rf',range.start,range.end),rfEvents=waveformVisiblePointCount(waveformOverview,'rfEvents',range.start,range.end); + var aggregateRf=!!overview&&rfEvents>pixelBudget*2,reduceRf=aggregateRf||rfPoints>pixelBudget*8; + var overviewUse={rf:reduceRf,phase:useOverview('phase'),gx:useGradientOverview('gx'),gy:useGradientOverview('gy'),gz:useGradientOverview('gz'),adc:useOverview('adc')}; + var dense=overviewUse.rf||overviewUse.phase||overviewUse.gx||overviewUse.gy||overviewUse.gz||overviewUse.adc; + waveformOverviewActive=dense; + setViewerNotice('dense',dense?'Dense overview mode is active. Zoom in for full waveform detail.':null); + 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);} + 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);} + 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);} + 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);} + 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);} + 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);} + if(rows[6]>=0&&range.end-range.start<=pixelBudget)drawTriggerBlocks(range.start,range.end,rows[6],ch,colors,vs,ve); +} + +function drawRfOverview(summary,vi,ch,colors,vs,ve){ + var y=cy(vi),base=y+ch*.45,scale=ch*.9/channelRange(0),pixelCount=Math.max(1,Math.ceil(plotWidth())); + var bins=binRfEvents(waveformOverview.rfEvents,vs,ve,pixelCount,2),series=waveformOverview.rfEvents; + rowClip(vi,ch,function(){ + ctx.fillStyle=colors.rf;ctx.globalAlpha=.28; + for(var bin=0;bin0)ctx.fillRect(x-.5,base-mean*scale,1,Math.max(1,mean*scale)); + } + ctx.globalAlpha=.75;ctx.strokeStyle=colors.rf;ctx.lineWidth=1.1; + for(var peakBin=0;peakBin1)ctx.stroke();rfRenderPointCount+=emitted;if(n<=pointBudget)rfRawCurveCount++;else rfReducedCurveCount++; + } + }); +} + +function appendBlockRfPath(rf,base,scale){ + 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); + ctx.moveTo(x0,base);ctx.lineTo(x0,top);ctx.lineTo(x1,top);ctx.lineTo(x1,base);return 4; +} + +function drawPhaseSampled(start,end,vi,ch,colors,vs,ve,maxPoints){ + var prefix=waveformOverview.pointPrefix.phase,first=prefix[start],last=prefix[end],count=last-first; + if(count<=0)return; + var y=cy(vi),scale=ch*.9/channelRange(1),sampleCount=Math.min(maxPoints,count),sampleStep=count/sampleCount; + rowClip(vi,ch,function(){ + for(var sampleIndex=0;sampleIndex>1;if(prefix[mid+1]<=ordinal)lo=mid+1;else hi=mid;} + if(lo>=end)continue; + 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; + var local=ordinal-prefix[lo],time=NaN,value=NaN,isAdc=false; + if(local1){ + var adc=block.adc,adcStep=Math.max(1,Math.ceil(adc.n/200)),adcSample=(local-rfCount)*adcStep; + var adcStart=adc.s+adc.d,adcEnd=adcStart+adc.n*adc.dw; + time=adcSampleve)continue; + ctx.fillStyle=isAdc?colors.adc:colors.rf; + ctx.fillRect(t2x(time)-.5,y+ch*.45-value*scale-.5,1,1); + } + }); +} + +function drawGradientOverview(summary,key,vi,ci,ch,color,vs,ve){ + 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); + rowClip(vi,ch,function(){ + var ranges=[]; + for(var i=summary.first;ive||minValues[i]===Infinity)continue; + var x0=t2x(Math.max(vs,startValues[i])),x1=t2x(Math.min(ve,endValues[i])); + ranges.push([x0,x1,y-maxValues[i]*scale,y-minValues[i]*scale]); + } + drawStepEnvelopeGroups(groupEnvelopeRanges(ranges,1),color,.14); + }); +} + +function drawAdcOverview(summary,vi,ch,colors,vs,ve){ + var level=summary.level,y=cy(vi); + rowClip(vi,ch,function(){ + ctx.fillStyle=colors.adf;ctx.strokeStyle=colors.adc;ctx.lineWidth=1;ctx.beginPath();var hasRect=false; + for(var i=summary.first;ive)continue; + var x0=t2x(Math.max(vs,level.adcStart[i])),x1=t2x(Math.min(ve,level.adcEnd[i])); + ctx.rect(x0,y-ch*.28,Math.max(1,x1-x0),ch*.56);hasRect=true; + } + if(hasRect){ctx.fill();ctx.stroke();} + }); +} + +function visibleBlockRange(vs,ve){ + var lo=0,hi=BL.length,mid; + while(lo>1;if(BL[mid].s+BL[mid].d>1;if(BL[mid].s<=ve)lo=mid+1;else hi=mid;} + return{start:Math.max(0,start-1),end:Math.min(BL.length,lo+1)}; +} + +function drawRfBlocks(start,end,vi,ch,colors,vs,ve,maxPoints){ + var y=cy(vi),scale=ch*.9/channelRange(0); + rowClip(vi,ch,function(){ + var visibleEvents=[]; + for(var bi=start;bive)continue; + visibleEvents.push(rf); + } + var pointBudget=Math.max(4,Math.floor((maxPoints||Infinity)/Math.max(1,visibleEvents.length))); + ctx.strokeStyle=colors.rf;ctx.lineWidth=1.1;ctx.beginPath();var hasPath=false; + for(var eventIndex=0;eventIndex1; + } + if(hasPath)ctx.stroke(); + }); +} + +function drawPhaseBlocks(start,end,vi,ch,colors,vs,ve){ + var y=cy(vi),scale=ch*.9/channelRange(1); + rowClip(vi,ch,function(){ + ctx.strokeStyle=colors.rf;ctx.lineWidth=.8;ctx.beginPath();var hasRf=false; + for(var bi=start;bive)continue; + var n=Math.min(phaseTime.length,rf.p.length); + for(var i=0;i1; + } + if(hasRf)ctx.stroke(); + + ctx.strokeStyle=colors.adc;ctx.lineWidth=.8;ctx.beginPath();var hasAdc=false; + var clipL=M.l,clipR=mc.width/(window.devicePixelRatio||1)-M.r; + for(var bj=start;bj=ve)continue; + var step=Math.max(1,Math.ceil(nAdc/200)),first=true,fo=adc.fo||0,po=adc.po||0; + for(var sample=0;sample<=nAdc;sample+=step){ + var t=(sampleclipR){first=true;continue;} + var py=y+ch*.45-phase*scale; + if(first){ctx.moveTo(x,py);first=false;}else ctx.lineTo(x,py); + hasAdc=true; + } + } + if(hasAdc)ctx.stroke();ctx.setLineDash([]); + }); +} + +function drawGradientBlocks(start,end,key,vi,ci,ch,color,vs,ve){ + var y=cy(vi),scale=ch*.4/channelRange(ci); + rowClip(vi,ch,function(){ + ctx.strokeStyle=color;ctx.lineWidth=1;ctx.beginPath();var hasPath=false; + for(var bi=start;bive)continue; + for(var i=0;i=ve)continue; + var x0=t2x(Math.max(eventStart,vs)),x1=t2x(Math.min(eventEnd,ve)); + ctx.rect(x0,y-ch*.28,x1-x0,ch*.56);hasRect=true; + if(x1-x0>30)labels.push({text:adc.n+'pts',x:(x0+x1)/2}); + } + if(hasRect){ctx.fillStyle=colors.adf;ctx.fill();ctx.strokeStyle=colors.adc;ctx.lineWidth=1;ctx.stroke();} + ctx.fillStyle=colors.fg;ctx.font='9px monospace';ctx.textAlign='center'; + for(var li=0;live)continue; + var x=t2x((eventStart+eventEnd)/2); + ctx.moveTo(x,y-ch*.05);ctx.lineTo(x-5,y+ch*.05);ctx.lineTo(x+5,y+ch*.05);ctx.closePath(); + labels.push({text:'ch'+tg.c,x:x});hasTrigger=true; + } + } + if(hasTrigger)ctx.fill();ctx.font='8px monospace';ctx.textAlign='center'; + for(var li=0;li eps) { kRotX += (_tRotX - kRotX) * _kEasing; changed = true; } + if (Math.abs(_tRotY - kRotY) > eps) { kRotY += (_tRotY - kRotY) * _kEasing; changed = true; } + if (Math.abs(_tScl - kScl) > 0.001){ kScl += (_tScl - kScl) * _kEasing; changed = true; } + if (Math.abs(_tCx - kCx) > 0.001){ kCx += (_tCx - kCx) * _kEasing; changed = true; } + if (Math.abs(_tCy - kCy) > 0.001){ kCy += (_tCy - kCy) * _kEasing; changed = true; } + if (Math.abs(_tCz - kCz) > 0.001){ kCz += (_tCz - kCz) * _kEasing; changed = true; } + if (changed) { + drawKsFast(); + _kAnimId = requestAnimationFrame(tick); + } else { + // Snap to exact targets + kRotX=_tRotX; kRotY=_tRotY; kScl=_tScl; kCx=_tCx; kCy=_tCy; kCz=_tCz; + _kAnimId = null; + } + } + _kAnimId = requestAnimationFrame(tick); +} + +function setKSpaceTarget(rx, ry, s, cx, cy, cz, instant) { + if (instant) { + kRotX=_tRotX=rx; kRotY=_tRotY=ry; kScl=_tScl=s; kCx=_tCx=cx; kCy=_tCy=cy; kCz=_tCz=cz; + drawKs(); + } else { + _tRotX=rx; _tRotY=ry; _tScl=s; _tCx=cx; _tCy=cy; _tCz=cz; + startKSpaceAnim(); + } +} +document.getElementById("kdot").oninput=function(){kDotSize=parseInt(this.value);drawKs();}; +document.getElementById("kunit").onclick=function(){ + kUnit=kUnit==="cyc"?"rad":"cyc";this.textContent=kUnit==="cyc"?"Unit: 1/m":"Unit: rad/m";drawKs(); +}; + +/* ── Theme selector (toolbar) ─────────────────────────────────────── */ +var themeSelect=document.getElementById("theme"); +var systemThemeQuery=(typeof window.matchMedia==="function")?window.matchMedia("(prefers-color-scheme: dark)"):null; +var inVsCode=!!vscApi; +function storageGet(k){try{return localStorage.getItem(k);}catch(_){return null;}} +function storageSet(k,v){try{localStorage.setItem(k,v);}catch(_){}} +function clearThemeClasses(){ + var b=document.body,rm=[]; + b.classList.forEach(function(c){if(c.indexOf("theme-")===0)rm.push(c);}); + rm.forEach(function(c){b.classList.remove(c);}); +} +function redrawAfterThemeChange(){ + mmCache=null; + draw();drawKs();drawMinimap(); +} +function applyThemeChoice(value,persist){ + if(!value)value="system"; + clearThemeClasses(); + if(value!=="system"){ + document.body.classList.add("theme-"+value); + }else if(!inVsCode){ + document.body.classList.add(systemThemeQuery&&systemThemeQuery.matches?"theme-github":"theme-githublight"); + } + if(themeSelect&&themeSelect.value!==value)themeSelect.value=value; + if(persist)storageSet("seqeyes.theme",value); + redrawAfterThemeChange(); +} +if(themeSelect){ + var savedTheme=storageGet("seqeyes.theme")||"system"; + if(!themeSelect.querySelector('option[value="'+savedTheme+'"]'))savedTheme="system"; + themeSelect.onchange=function(){applyThemeChoice(this.value,true);}; + applyThemeChoice(savedTheme,false); +} +if(systemThemeQuery){ + var onSystemTheme=function(){if(!themeSelect||themeSelect.value==="system")applyThemeChoice("system",false);}; + if(systemThemeQuery.addEventListener)systemThemeQuery.addEventListener("change",onSystemTheme); + else if(systemThemeQuery.addListener)systemThemeQuery.addListener(onSystemTheme); +} + +/* ═══════════════════════════════════════════════════════════════════════ + WebGL state + ═══════════════════════════════════════════════════════════════════════ */ +var gl=null, glProgram=null, glBuf=null, glN=0; +var glAttribPos=-1, glAttribTime=-1; +var glU_cy=-1,glU_sy=-1,glU_cx=-1,glU_sx=-1,glU_center=-1,glU_scale=-1; +var glU_halfRes=-1,glU_tMin=-1,glU_tMax=-1,glU_dot=-1,glU_color=-1; + +// ── Cached bounds (computed once when data is uploaded) ────────────── +var _kBxmin=0,_kBxmax=0,_kBymin=0,_kBymax=0,_kBzmin=0,_kBzmax=0,_kBrng=0; +var _kBoundsDirty=true; // set true when new data arrives + +function initWebGL(){ + var c=document.getElementById("kg"); + gl=c.getContext("webgl2",{antialias:true,alpha:true,premultipliedAlpha:false}) + ||c.getContext("webgl",{antialias:true,alpha:true,premultipliedAlpha:false}); + if(!gl){console.warn("[SeqEyes] WebGL unavailable");return false;} + gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA); + + var vs=gl.createShader(gl.VERTEX_SHADER), fs=gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(vs,'\ + attribute vec3 aPos; attribute float aTime;\ + uniform float uCy,uSy,uCx,uSx,uScale;\ + uniform vec3 uCenter; uniform vec2 uHalfRes;\ + uniform float uTMin,uTMax,uDot;\ + varying float vVis;\ + void main(){\ + float dx=aPos.x-uCenter.x,dy=aPos.y-uCenter.y,dz=aPos.z-uCenter.z;\ + float rx=dx*uCy-dz*uSy;\ + float rz=dx*uSy+dz*uCy;\ + float ry=dy*uCx-rz*uSx;\ + float sx=rx*uScale/uHalfRes.x;\ + float sy=ry*uScale/uHalfRes.y;\ + vVis=(aTime>=uTMin&&aTime<=uTMax)?1.0:-1.0;\ + gl_Position=vec4(sx,sy,0.0,1.0);\ + gl_PointSize=vVis>0.0?uDot:0.0;\ + }'); + gl.shaderSource(fs,'\ + precision mediump float;\ + uniform vec4 uColor; varying float vVis;\ + void main(){\ + if(vVis<0.0)discard;\ + float d=length(gl_PointCoord-vec2(0.5));\ + float a=1.0-smoothstep(0.40,0.50,d);\ + gl_FragColor=vec4(uColor.rgb,uColor.a*a);\ + }'); + gl.compileShader(vs);if(!gl.getShaderParameter(vs,gl.COMPILE_STATUS)){console.warn("[SeqEyes] VS:",gl.getShaderInfoLog(vs));return false;} + gl.compileShader(fs);if(!gl.getShaderParameter(fs,gl.COMPILE_STATUS)){console.warn("[SeqEyes] FS:",gl.getShaderInfoLog(fs));return false;} + glProgram=gl.createProgram(); + gl.attachShader(glProgram,vs);gl.attachShader(glProgram,fs); + gl.linkProgram(glProgram); + if(!gl.getProgramParameter(glProgram,gl.LINK_STATUS)){console.warn("[SeqEyes] Link:",gl.getProgramInfoLog(glProgram));return false;} + + glAttribPos=gl.getAttribLocation(glProgram,"aPos"); + glAttribTime=gl.getAttribLocation(glProgram,"aTime"); + glU_cy=gl.getUniformLocation(glProgram,"uCy");glU_sy=gl.getUniformLocation(glProgram,"uSy"); + glU_cx=gl.getUniformLocation(glProgram,"uCx");glU_sx=gl.getUniformLocation(glProgram,"uSx"); + glU_center=gl.getUniformLocation(glProgram,"uCenter");glU_scale=gl.getUniformLocation(glProgram,"uScale"); + glU_halfRes=gl.getUniformLocation(glProgram,"uHalfRes");glU_dot=gl.getUniformLocation(glProgram,"uDot"); + glU_tMin=gl.getUniformLocation(glProgram,"uTMin");glU_tMax=gl.getUniformLocation(glProgram,"uTMax"); + glU_color=gl.getUniformLocation(glProgram,"uColor"); + return true; +} + +/* ── Upload ADC k‑space data to GPU (called after base64 decode) ────── */ +function uploadKSpaceGPU(){ + if(!kAdc||!kAdc[0]||kAdc[0].length===0){glN=0;return;} + if(!gl&&!initWebGL())return; + var n=kAdc[0].length; + var data=new Float32Array(n*4); + var ax=kAdc[0],ay=kAdc[1],az=kAdc[2],at=kAdcTime; + for(var i=0;ixmax)xmax=xi;}if(isFinite(yi)){if(yiymax)ymax=yi;}if(isFinite(zi)){if(zizmax)zmax=zi;}} + _kBxmin=xmin;_kBxmax=xmax;_kBymin=ymin;_kBymax=ymax;_kBzmin=zmin;_kBzmax=zmax; + _kBrng=Math.max(xmax-xmin,ymax-ymin,zmax-zmin,1e-6); + _kBoundsDirty=false; +} + +/* ═══════════════════════════════════════════════════════════════════════ + Toggle / View cycle / Canvas sizing + ═══════════════════════════════════════════════════════════════════════ */ +document.getElementById("kbtn").onclick=function(){ + if((!kAdc||!kAdc[0]||!kAdc[0].length)&&typeof showKspaceSafetyDialog==='function'&&showKspaceSafetyDialog())return; + // Ensure layout class is in sync before toggling + if(typeof refreshLayout==='function')refreshLayout(); + kOpen=!kOpen; + var p=document.getElementById("right"); + if(kOpen){ + p.classList.add("open"); + if(typeof layoutMode!=='undefined'&&layoutMode==='vertical'){ + // In vertical mode, apply height via inline !important to match applyLayoutMode + p.style.setProperty('height','300px','important'); + p.style.setProperty('width','100%','important'); + }else{ + p.style.width='500px'; + } + this.textContent="K Space ✕";kAutoFit=true; + }else{ + p.classList.remove("open"); + if(typeof layoutMode!=='undefined'&&layoutMode==='vertical'){ + p.style.setProperty('height','0','important'); + }else{ + p.style.width=''; + } + this.textContent="K Space"; + } + if(kOpen){requestAnimationFrame(function(){drawKs_init();});} + else{requestAnimationFrame(function(){resizeKc();drawKs();});} +}; +document.getElementById("kax").textContent="3D"; +document.getElementById("krst").onclick=function(){ + kView="3d"; + document.getElementById("kax").textContent="3D"; + var af=_kAutoFitVals(); + setKSpaceTarget(-0.5, 0.7, af.scl, af.cx, af.cy, af.cz, false); +}; +// Camera presets: smoothly rotate to look straight down an axis +document.getElementById("kax").onclick=function(){ + var views=["3d","xy","xz","yz"];var idx=views.indexOf(kView); + kView=views[(idx+1)%4]; + var trx=_tRotX, tr=_tRotY; + if(kView==="xy"){trx=0; tr=0;} + else if(kView==="xz"){trx=-Math.PI/2; tr=0;} + else if(kView==="yz"){trx=0; tr=Math.PI/2;} + else{trx=-0.5; tr=0.7;} // 3d — default perspective + document.getElementById("kax").textContent=kView.toUpperCase(); + var af=_kAutoFitVals(); + setKSpaceTarget(trx, tr, af.scl, af.cx, af.cy, af.cz, false); +}; + +function resizeKc(){ + var r=document.getElementById("right").getBoundingClientRect(); + if(r.width<=0||r.height<=0)return; + var dpr=window.devicePixelRatio||1; + // Resize both canvases + var kg=document.getElementById("kg"); + kg.width=r.width*dpr;kg.height=r.height*dpr; + kg.style.width=r.width+"px";kg.style.height=r.height+"px"; + kCanvas.width=r.width*dpr;kCanvas.height=r.height*dpr; + kCanvas.style.width=r.width+"px";kCanvas.style.height=r.height+"px"; + kCtx.setTransform(dpr,0,0,dpr,0,0); +} + +/* ── Convert k-space value to display units ──────────────────────────── */ +function kUnitVal(v){if(kUnit==="rad")return v*6.283185;return v;} +function kUnitStr(){return kUnit==="rad"?"rad/m":"1/m";} +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);} + +/* ── Cursor-linked trajectory sample ─────────────────────────────────── */ +function nearestTimeIndex(times,t){ + if(!times||!times.length||!isFinite(t))return -1; + var lo=0,hi=times.length-1; + if(t<=times[0])return 0; + if(t>=times[hi])return hi; + while(lo>1; + if(times[mid]=times[n-1]){ + idx=Math.max(0,Math.min(n-1,idx)); + return {x:series[0][idx],y:series[1][idx],z:series[2][idx],t:times[idx],source:"traj"}; + } + var upper=idx; + if(times[upper]=0){ + var prev=idx>0?Math.abs(kAdcTime[idx]-kAdcTime[idx-1]):Infinity; + var next=idx+1=1000)return (u/1000).toFixed(1)+"k"; + if(Math.abs(u)>=10)return u.toFixed(1); + if(Math.abs(u)>=0.01)return u.toFixed(2); + return u.toExponential(1); +} +function formatKCursorReadout(){ + var s=getKCursorSample(); + if(!s)return ""; + return "kxyz="+fmtKShort(s.x)+","+fmtKShort(s.y)+","+fmtKShort(s.z)+" "+kUnitStr(); +} +function drawKCursorMarker(ctx,proj,cs,W,H){ + var s=getKCursorSample(); + if(!s||!isFinite(s.x)||!isFinite(s.y)||!isFinite(s.z))return; + var p=proj(s.x,s.y,s.z); + if(p.x<-12||p.x>W+12||p.y<-12||p.y>H+12)return; + ctx.save(); + ctx.lineWidth=2; + ctx.strokeStyle=cs.getPropertyValue("--cr").trim()||"#ee0000"; + ctx.fillStyle="rgba(255,215,0,0.90)"; + ctx.beginPath();ctx.arc(p.x,p.y,Math.max(5,kDotSize+4),0,6.283);ctx.fill();ctx.stroke(); + ctx.fillStyle=cs.getPropertyValue("--fg").trim(); + ctx.font="10px monospace"; + ctx.textAlign="left"; + ctx.fillText(s.source==="adc"?"ADC":"ktraj",p.x+8,p.y-8); + ctx.restore(); +} + +/* ── Mouse ───────────────────────────────────────────────────────────── */ +kCanvas.addEventListener("mousedown",function(e){ + kDragging=true;kDragPrev={x:e.clientX,y:e.clientY};kDragBtn=e.button; + // Cancel any running animation so it doesn't fight the drag + if(_kAnimId){cancelAnimationFrame(_kAnimId);_kAnimId=null;} + e.preventDefault(); +}); +kCanvas.addEventListener("contextmenu",function(e){e.preventDefault();}); + +kCanvas.addEventListener("wheel",function(e){e.preventDefault(); + var r=kCanvas.getBoundingClientRect(),dpr=window.devicePixelRatio||1; + var mx=(e.clientX-r.left)/dpr, my=(e.clientY-r.top)/dpr; + var zf=e.deltaY<0?1.25:0.8, W=kCanvas.width/dpr, H=kCanvas.height/dpr; + var dz=(1-1/zf)*dpr/kScl; + kAutoFit=false; + setKSpaceTarget(kRotX, kRotY, kScl*zf, kCx+(mx-W/2)*dz, kCy-(my-H/2)*dz, kCz, false); +},{passive:false}); + +window.addEventListener("mousemove",function(e){ + if(!kDragging||!kDragPrev||!kOpen)return; + var dx=e.clientX-kDragPrev.x, dy=e.clientY-kDragPrev.y; + kDragPrev={x:e.clientX,y:e.clientY}; + if(kView!=="3d"){kView="3d";document.getElementById("kax").textContent="3D";} + if(kDragBtn===0){ + // left drag = instant rotate (no lerp — feels responsive) + kRotY+=dx*0.008; kRotX-=dy*0.008; + _tRotY=kRotY; _tRotX=kRotX; // sync targets + }else{ + // right/middle drag = instant pan + var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cx=Math.cos(kRotX),sx=Math.sin(kRotX); + var dpr=window.devicePixelRatio||1; + dx/=(dpr*kScl); dy/=(dpr*kScl); + if(Math.abs(cz)>0.01){kCy+=dy/cx;kCx+=(-dx-sz*sx*(dy/cx))/cz;} + else{kCy+=dy/cx;kCz+=(dx-cz*sx*(dy/cx))/sz;} + _tCy=kCy; _tCx=kCx; _tCz=kCz; // sync targets + kAutoFit=false; + } + drawKsFast(); +}); +window.addEventListener("mouseup",function(){kDragging=false;kDragPrev=null;}); + +/* ── Touch: 3D k-space viewer ─────────────────────────────────────── */ +var _kTouchActive=false,_kTouchPrev=null,_kTouchPinch0=0,_kTouchMid=null,_kTouchBtn=0; +kCanvas.addEventListener("touchstart",function(e){ + if(_kAnimId){cancelAnimationFrame(_kAnimId);_kAnimId=null;} + if(e.touches.length===1){ + _kTouchActive=true;_kTouchBtn=0; + _kTouchPrev={x:e.touches[0].clientX,y:e.touches[0].clientY}; + }else if(e.touches.length===2){ + _kTouchActive=true;_kTouchBtn=1; + _kTouchPinch0=getTouchDist(e.touches); + _kTouchMid=getTouchMid(e.touches); + _kTouchPrev={x:_kTouchMid.x,y:_kTouchMid.y}; + } + e.preventDefault(); +},{passive:false}); +kCanvas.addEventListener("touchmove",function(e){ + if(!_kTouchActive||!_kTouchPrev||!kOpen)return; + if(kView!=="3d"){kView="3d";document.getElementById("kax").textContent="3D";} + if(e.touches.length===1&&_kTouchBtn===0){ + // 1‑finger rotate + var dx=e.touches[0].clientX-_kTouchPrev.x; + var dy=e.touches[0].clientY-_kTouchPrev.y; + _kTouchPrev={x:e.touches[0].clientX,y:e.touches[0].clientY}; + kRotY+=dx*0.008; kRotX-=dy*0.008; + _tRotY=kRotY; _tRotX=kRotX; + drawKsFast(); + }else if(e.touches.length===2){ + // 2‑finger pinch-zoom + pan + var d=getTouchDist(e.touches); + if(_kTouchPinch0>0){ + var zf=d/_kTouchPinch0; + kScl*=zf; kScl=Math.max(0.001,Math.min(1e6,kScl)); + _tScl=kScl; kAutoFit=false; + _kTouchPinch0=d; + } + var mid=getTouchMid(e.touches); + var pdx=(mid.x-_kTouchPrev.x), pdy=(mid.y-_kTouchPrev.y); + _kTouchPrev={x:mid.x,y:mid.y}; + var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cx=Math.cos(kRotX),sx=Math.sin(kRotX); + var dpr=window.devicePixelRatio||1; + pdx/=(dpr*kScl); pdy/=(dpr*kScl); + if(Math.abs(cz)>0.01){kCy+=pdy/cx;kCx+=(-pdx-sz*sx*(pdy/cx))/cz;} + else{kCy+=pdy/cx;kCz+=(pdx-cz*sx*(pdy/cx))/sz;} + _tCy=kCy; _tCx=kCx; _tCz=kCz; kAutoFit=false; + drawKsFast(); + } + e.preventDefault(); +},{passive:false}); +kCanvas.addEventListener("touchend",function(e){ + _kTouchActive=false;_kTouchPrev=null;_kTouchPinch0=0;_kTouchMid=null; + if(e.touches.length===1){ + // Transition from 2‑finger to 1‑finger + _kTouchActive=true;_kTouchBtn=0; + _kTouchPrev={x:e.touches[0].clientX,y:e.touches[0].clientY}; + } +}); +kCanvas.addEventListener("touchcancel",function(){ + _kTouchActive=false;_kTouchPrev=null;_kTouchPinch0=0;_kTouchMid=null; +}); + +/* ── Resize handle ───────────────────────────────────────────────────── */ +var kResizing=false, kResizeStart=0, kResizeW=500, kResizeH=300; +document.getElementById("khandle").addEventListener("mousedown",function(e){ + kResizing=true; + if(typeof layoutMode!=='undefined'&&layoutMode==='vertical')kResizeStart=e.clientY; + else kResizeStart=e.clientX; + e.preventDefault();e.stopPropagation(); +}); +document.getElementById("khandle").addEventListener("touchstart",function(e){ + kResizing=true; + if(typeof layoutMode!=='undefined'&&layoutMode==='vertical')kResizeStart=e.touches[0].clientY; + else kResizeStart=e.touches[0].clientX; + e.preventDefault();e.stopPropagation(); +},{passive:false}); +window.addEventListener("mousemove",function(e){ + if(!kResizing)return; + var p=document.getElementById("right"); + var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical'); + if(vertical){ + kResizeH=Math.max(120,Math.min(800,kResizeH-(e.clientY-kResizeStart))); + kResizeStart=e.clientY; + p.style.setProperty('height',kResizeH+'px','important');p.style.setProperty('transition','none','important'); + }else{ + kResizeW=Math.max(200,Math.min(1200,kResizeW-(e.clientX-kResizeStart))); + kResizeStart=e.clientX; + p.style.width=kResizeW+"px";p.style.transition="none"; + } + resizeKc();drawKs(); +}); +window.addEventListener("touchmove",function(e){ + if(!kResizing||e.touches.length!==1)return; + var p=document.getElementById("right"); + var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical'); + if(vertical){ + kResizeH=Math.max(120,Math.min(800,kResizeH-(e.touches[0].clientY-kResizeStart))); + kResizeStart=e.touches[0].clientY; + p.style.setProperty('height',kResizeH+'px','important');p.style.setProperty('transition','none','important'); + }else{ + kResizeW=Math.max(200,Math.min(1200,kResizeW-(e.touches[0].clientX-kResizeStart))); + kResizeStart=e.touches[0].clientX; + p.style.width=kResizeW+"px";p.style.transition="none"; + } + resizeKc();drawKs(); +},{passive:false}); +window.addEventListener("mouseup",function(){ + if(kResizing){ + kResizing=false; + var p=document.getElementById("right"); + var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical'); + if(vertical){p.style.setProperty('transition','height .25s','important');} + else{p.style.transition='';} + } +}); +window.addEventListener("touchend",function(){ + if(kResizing){ + kResizing=false; + var p=document.getElementById("right"); + var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical'); + if(vertical){p.style.setProperty('transition','height .25s','important');} + else{p.style.transition='';} + } +}); +window.addEventListener("resize",function(){if(kOpen){resizeKc();drawKs();}}); + +/* ═══════════════════════════════════════════════════════════════════════ + Nice tick spacing + ═══════════════════════════════════════════════════════════════════════ */ +function kNice(range){var ms=[1,2,5,10,20,50,100,200,500];for(var i=0;i=range/4)return ms[i]*b;}return 1;} + +function _kAutoFitVals(){ + var rng=_kBrng||1,W=kCanvas.width/(window.devicePixelRatio||1),H=kCanvas.height/(window.devicePixelRatio||1); + return{cx:(_kBxmin+_kBxmax)/2,cy:(_kBymin+_kBymax)/2,cz:(_kBzmin+_kBzmax)/2,scl:Math.min(W,H)/(rng*1.15)}; +} + +/* ═══════════════════════════════════════════════════════════════════════ + Drawing (Canvas 2D axes + WebGL scatter) + ═══════════════════════════════════════════════════════════════════════ */ + +/** First‑open initialisation: sizes canvases to the KNOWN CSS target + * dimensions so auto‑fit never sees a mid‑transition partial size. */ +function drawKs_init(){ + var dpr=window.devicePixelRatio||1; + var vertical=(typeof layoutMode!=='undefined'&&layoutMode==='vertical'); + var targetW=vertical?document.getElementById("right").getBoundingClientRect().width||500:500; + var targetH=vertical?300:document.getElementById("right").getBoundingClientRect().height||500; + if(targetW<=0)targetW=500; + if(targetH<=0)targetH=500; + // Size both canvases directly — no DOM width read + var kg=document.getElementById("kg"); + kg.width=targetW*dpr;kg.height=targetH*dpr; + kg.style.width=targetW+"px";kg.style.height=targetH+"px"; + kCanvas.width=targetW*dpr;kCanvas.height=targetH*dpr; + kCanvas.style.width=targetW+"px";kCanvas.style.height=targetH+"px"; + kCtx.setTransform(dpr,0,0,dpr,0,0); + drawKs_core(targetW,targetH,dpr); +} + +/** Normal draw: reads current canvas size from DOM. */ +function drawKs(){ + resizeKc(); + var dpr=window.devicePixelRatio||1; + var W=kCanvas.width/dpr, H=kCanvas.height/dpr; + drawKs_core(W,H,dpr); +} + +/** Fast draw: skips canvas resize (use during drag for responsiveness). */ +function drawKsFast(){ + var dpr=window.devicePixelRatio||1; + var W=kCanvas.width/dpr, H=kCanvas.height/dpr; + drawKs_core(W,H,dpr); +} + +/** Core rendering — assumes canvases are already sized, W & H in CSS px. */ +function drawKs_core(W,H,dpr){ + if(W<=0||H<=0)return; + + var ctx=kCtx,cs=getComputedStyle(document.body); + // Clear Canvas 2D (axes layer) — transparent so WebGL shows through + ctx.clearRect(0,0,W,H); + if(!kOpen)return; + + // ── Check data ── + if(!kAdc||!kAdcTime||!kAdc[0]||kAdc[0].length===0){ + ctx.fillStyle="#f00";ctx.font="11px monospace";ctx.fillText("NO ADC data",10,20);return; + } + var adcX=kAdc[0],adcY=kAdc[1],adcZ=kAdc[2], nAdc=adcX.length; + + // ── Use cached bounds (computed once in uploadKSpaceGPU) ────────── + if (_kBoundsDirty || !isFinite(_kBxmin)) { + // Recompute if needed (shouldn't happen normally) + var xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity,zmin=Infinity,zmax=-Infinity; + for(var a=0;axmax)xmax=xi;}if(isFinite(yi)){if(yiymax)ymax=yi;}if(isFinite(zi)){if(zizmax)zmax=zi;}} + _kBxmin=xmin;_kBxmax=xmax;_kBymin=ymin;_kBymax=ymax;_kBzmin=zmin;_kBzmax=zmax; + _kBrng=Math.max(xmax-xmin,ymax-ymin,zmax-zmin,1e-6); + _kBoundsDirty=false; + } + if(!isFinite(_kBxmin)){ctx.fillStyle="#f00";ctx.font="11px monospace";ctx.fillText("ALL NaN",10,20);return;} + + // ── Auto-fit (initial open only) ── + if(kAutoFit){ + kAutoFit=false; + var af=_kAutoFitVals(); + setKSpaceTarget(kRotX, kRotY, af.scl, af.cx, af.cy, af.cz, false); + } + + // ── Time window ── + var visibleRange=visibleDuration(),vs=ox,ve=ox+visibleRange; + + // ═══════════════════════════════════════════════════════════════════ + // WebGL scatter (GPU — renders millions of points at 60 fps) + // ═══════════════════════════════════════════════════════════════════ + if(gl&&glBuf&&glN>0){ + gl.useProgram(glProgram); + var hW=W*dpr*0.5, hH=H*dpr*0.5; + gl.viewport(0,0,W*dpr,H*dpr); + gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT); + + gl.uniform1f(glU_cy,Math.cos(kRotY));gl.uniform1f(glU_sy,Math.sin(kRotY)); + gl.uniform1f(glU_cx,Math.cos(kRotX));gl.uniform1f(glU_sx,Math.sin(kRotX)); + gl.uniform3f(glU_center,kCx,kCy,kCz); + gl.uniform1f(glU_scale,kScl); + gl.uniform2f(glU_halfRes,hW,hH); + gl.uniform1f(glU_tMin,vs);gl.uniform1f(glU_tMax,ve); + gl.uniform1f(glU_dot,kDotSize*dpr); + + var ac=cs.getPropertyValue("--adc").trim(); + var rgb=parseCSSColor(ac); + gl.uniform4f(glU_color,rgb[0],rgb[1],rgb[2],0.85); + + gl.bindBuffer(gl.ARRAY_BUFFER,glBuf); + gl.enableVertexAttribArray(glAttribPos); + gl.vertexAttribPointer(glAttribPos,3,gl.FLOAT,false,16,0); + gl.enableVertexAttribArray(glAttribTime); + gl.vertexAttribPointer(glAttribTime,1,gl.FLOAT,false,16,12); + + kSpaceTrajectoryDrawCount++; + gl.drawArrays(gl.POINTS,0,glN); + } + + drawKsOverlay(W,H,dpr,cs); +} + +/** Redraw only the lightweight 2D axes/cursor layer. The WebGL trajectory + * remains untouched when waveform hover changes cursorT. */ +function drawKsOverlayFast(){ + if(!kOpen||!kAdc||!kAdcTime||!kAdc[0]||kAdc[0].length===0)return; + var dpr=window.devicePixelRatio||1; + var W=kCanvas.width/dpr,H=kCanvas.height/dpr; + if(W<=0||H<=0)return; + drawKsOverlay(W,H,dpr,getComputedStyle(document.body)); +} + +function drawKsOverlay(W,H,dpr,cs){ + kSpaceOverlayDrawCount++; + var ctx=kCtx;ctx.clearRect(0,0,W,H); + if(!kOpen||!isFinite(_kBxmin))return; + var rng3=_kBrng; + var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cxR=Math.cos(kRotX),sxR=Math.sin(kRotX); + var invDpr=1/dpr; + function proj(px,py,pz){ + var dx2=px-kCx, dy2=py-kCy, dz2=pz-kCz; + var rx=dx2*cz-dz2*sz; + var rz2=dx2*sz+dz2*cz; + var ry=dy2*cxR-rz2*sxR; + return {x:W/2+rx*kScl*invDpr, y:H/2-ry*kScl*invDpr}; + } + var tick=kNice(rng3); + function drawAxis3D(fx,fy,fz,label,col){ + var al=rng3*0.65; + var t1=proj(fx*al,fy*al,fz*al); + var t2=proj(-fx*al,-fy*al,-fz*al); + ctx.strokeStyle=col;ctx.lineWidth=1.6;ctx.setLineDash([]); + ctx.beginPath();ctx.moveTo(t2.x,t2.y);ctx.lineTo(t1.x,t1.y);ctx.stroke(); + ctx.fillStyle=col;ctx.beginPath();ctx.arc(t1.x,t1.y,4,0,6.283);ctx.fill(); + ctx.fillStyle=col;ctx.font="bold 11px monospace";ctx.fillText(label,t1.x+5,t1.y-5); + ctx.fillStyle=cs.getPropertyValue("--lb").trim();ctx.font="8px monospace"; + for(var v=tick;v<=al;v+=tick){ + var tp=proj(fx*v,fy*v,fz*v); + var tm=proj(-fx*v,-fy*v,-fz*v); + ctx.strokeStyle=col;ctx.lineWidth=0.5; + ctx.beginPath();ctx.moveTo(tp.x-3,tp.y);ctx.lineTo(tp.x+3,tp.y);ctx.stroke(); + ctx.beginPath();ctx.moveTo(tm.x-3,tm.y);ctx.lineTo(tm.x+3,tm.y);ctx.stroke(); + ctx.fillText(kTickVal(v),tp.x+5,tp.y-2); + } + } + drawAxis3D(1,0,0,"kx",cs.getPropertyValue("--gx").trim()); + drawAxis3D(0,1,0,"ky",cs.getPropertyValue("--gy").trim()); + drawAxis3D(0,0,1,"kz",cs.getPropertyValue("--gz").trim()); + var oo=proj(0,0,0); + ctx.fillStyle=cs.getPropertyValue("--fg").trim();ctx.beginPath();ctx.arc(oo.x,oo.y,4,0,6.283);ctx.fill(); + drawKCursorMarker(ctx,proj,cs,W,H); +} + +/* ── Parse CSS hex colour to [r,g,b] 0‑1 ────────────────────────────── */ +function parseCSSColor(c){ + if(!c)return[0.26,0.83,0.96]; + var m=c.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); + if(m)return[parseInt(m[1],16)/255,parseInt(m[2],16)/255,parseInt(m[3],16)/255]; + m=c.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/); + if(m)return[+m[1]/255,+m[2]/255,+m[3]/255]; + return[0.26,0.83,0.96]; +} + + +/* ══ interaction.js ══ */ +/* ═══════════════════════════════════════════════════════════════════════ + Mouse interaction + ═══════════════════════════════════════════════════════════════════════ */ + +var _touchTooltipTimer=null,_pointerFrame=0,_pendingPointer=null,mmDrag=false; + +mc.addEventListener('wheel',function(e){e.preventDefault(); + var changed=false; + if(e.ctrlKey||e.metaKey){ + var ch=cH(),vc=visChannels(),my=e.clientY-mc.getBoundingClientRect().top; + var vi2=Math.floor((my-M.t)/ch); + if(vi2>=0&&vi2=r.left&&p.x<=r.right&&p.y>=r.top&&p.y<=r.bottom; + if(!p.dragging&&!inside){if(cursorActive)clearViewerCursor();return;} + var mx=p.x-r.left,my=p.y-r.top;cursorT=x2t(mx); + cursorActive=mx>=M.l&&mx<=r.width-M.r&&my>=M.t&&my<=r.height-M.b; + var kInfo=(typeof formatKCursorReadout==='function')?formatKCursorReadout():''; + curEl.textContent=fmtT(timeConv(cursorT))+' '+timeUnitStr()+(kInfo?' | '+kInfo:''); + if(p.dragging){ + ox=dso-(p.x-dsx)/sc;clampView();tt.style.display='none'; + draw();if(typeof drawKs==='function')drawKs();drawMinimap();return; + } + drawCursorOverlay(); + if(typeof drawKsOverlayFast==='function')drawKsOverlayFast(); + showTooltipAt(p.x,p.y,cursorT); +} + +function clearViewerCursor(){ + cursorT=0;cursorActive=false;curEl.textContent='\u2190 hover for time';tt.style.display='none'; + drawCursorOverlay();if(typeof drawKsOverlayFast==='function')drawKsOverlayFast(); +} + +/* ── Touch: waveform viewer ───────────────────────────────────────── */ +var _tchDragging=false,_tchStartX=0,_tchStartO=0,_tchPinch0=0,_tchPinchMid=null,_tchMoved=false; +mc.addEventListener('touchstart',function(e){ + if(e.touches.length===1){ + _tchDragging=true;_tchMoved=false; + _tchStartX=e.touches[0].clientX;_tchStartO=ox; + cc.classList.add('pan'); + // Update cursor position + var p=getTouchPos(e.touches[0],mc); + cursorT=x2t(p.x);cursorActive=p.x>=M.l&&p.x<=mc.width/(window.devicePixelRatio||1)-M.r&&p.y>=M.t&&p.y<=mc.height/(window.devicePixelRatio||1)-M.b; + curEl.textContent=fmtT(timeConv(cursorT))+' '+timeUnitStr(); + }else if(e.touches.length===2){ + _tchDragging=false;_tchPinch0=getTouchDist(e.touches);_tchPinchMid=getTouchMid(e.touches); + } + e.preventDefault(); +},{passive:false}); +mc.addEventListener('touchmove',function(e){ + if(e.touches.length===1&&_tchDragging){ + _tchMoved=true; + ox=_tchStartO-(e.touches[0].clientX-_tchStartX)/sc;clampView(); + var p=getTouchPos(e.touches[0],mc); + cursorT=x2t(p.x);cursorActive=p.x>=M.l&&p.x<=mc.width/(window.devicePixelRatio||1)-M.r&&p.y>=M.t&&p.y<=mc.height/(window.devicePixelRatio||1)-M.b; + curEl.textContent=fmtT(timeConv(cursorT))+' '+timeUnitStr(); + scheduleViewerDraw(true); + }else if(e.touches.length===2){ + _tchDragging=false; + var d=getTouchDist(e.touches),zf=d/Math.max(_tchPinch0,1); + var mid=getTouchMid(e.touches),mx2=mid.x-mc.getBoundingClientRect().left; + if(zoomAt(mx2,zf)){_tchPinch0=d;scheduleViewerDraw(true);} + } + e.preventDefault(); +},{passive:false}); +mc.addEventListener('touchend',function(e){ + if(!_tchMoved&&cursorActive&&e.changedTouches.length===1){ + // Tap → show tooltip + showTooltipAt(e.changedTouches[0].clientX,e.changedTouches[0].clientY,cursorT); + } + _tchDragging=false;cc.classList.remove('pan');drawMinimap(); + if(e.touches.length===0){_tchPinch0=0;_tchPinchMid=null;} +}); +mc.addEventListener('touchcancel',function(){_tchDragging=false;cc.classList.remove('pan');_tchPinch0=0;_tchPinchMid=null;}); + +/* ── Tooltip helper (also used by touch tap) ──────────────────────── */ +function sampleDerivedSeriesAtTime(series,t){ + if(series&&series.kind==='envelope'){ + var base=series.levels&&series.levels[0];if(!base||base.count<1||!isFinite(t))return null; + var index=Math.max(0,lowerBoundSeries(base.t1,t));if(index>=base.count||tbase.t1[index])return null; + var span=base.t1[index]-base.t0[index],alpha=span>0?(t-base.t0[index])/span:0; + return(base.first[index]+(base.last[index]-base.first[index])*alpha)*series.scale; + } + if(!series||!series.t||!series.v||series.n<1||!isFinite(t))return null; + var n=Math.min(series.n,series.t.length,series.v.length); + if(n<1||tseries.t[n-1])return null; + if(n===1||t<=series.t[0])return series.v[0]*series.scale; + if(t>=series.t[n-1])return series.v[n-1]*series.scale; + var lo=0,hi=n-1; + while(hi-lo>1){var m=(lo+hi)>>1;if(series.t[m]<=t)lo=m;else hi=m;} + var dt=series.t[hi]-series.t[lo]; + if(dt<=0)return series.v[lo]*series.scale; + return (series.v[lo]+(series.v[hi]-series.v[lo])*(t-series.t[lo])/dt)*series.scale; +} +function fmtSignedAmp(v){ + var av=Math.abs(v),s;if(av>=1e6)s=(av/1e6).toFixed(1)+'M';else if(av>=1e3)s=(av/1e3).toFixed(1)+'k';else if(av>=1)s=av.toFixed(1);else s=av.toFixed(2); + return(v<0?'-':'')+s; +} +function fmtDerivedValue(v,unit){return unit==='%'?v.toFixed(1)+'%':fmtSignedAmp(v)+' '+unit;} +function pushDerivedPart(parts,label,series,t,unit){ + var v=sampleDerivedSeriesAtTime(series,t);if(v===null)return; + parts.push(label+'='+fmtDerivedValue(v,unit)); +} +function pushM1DerivedPart(parts,label,series,t){ + if(series&&series.kind==='envelope'){ + var range=sampleEnvelopeRangeAtTime(series,t);if(!range)return; + if(range.min===range.max)parts.push(label+'='+fmtDerivedValue(range.min,'s/m')); + else parts.push(label+'\u2208['+fmtSignedAmp(range.min)+', '+fmtSignedAmp(range.max)+'] s/m'); + return; + } + pushDerivedPart(parts,label,series,t,'s/m'); +} +function appendDerivedTooltipLines(lines,ct){ + if(pnsData&&chVis[7]){ + 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,'%'); + if(p.length)lines.push('PNS: '+p.join(' ')); + } + var activeM1=m1SeriesForView(ox,ox+visibleDuration()); + if(activeM1&&(chVis[8]||chVis[9]||chVis[10])){ + 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); + if(m.length)lines.push('M1: '+m.join(' ')); + } +} +function placeTooltip(cx,cy){ + var pad=8;tt.style.left='0px';tt.style.top='0px'; + var r=tt.getBoundingClientRect(),vw=window.innerWidth,vh=window.innerHeight; + var left=Math.min(Math.max(pad,cx+15),Math.max(pad,vw-r.width-pad)); + var top=cy+12;if(top+r.height+pad>vh)top=cy-r.height-12;if(top=0&&vi21||found){ + tt.textContent=lines.join('\n');tt.style.display='block'; + placeTooltip(cx,cy); + // Auto-dismiss on touch devices + if(_touchTooltipTimer)clearTimeout(_touchTooltipTimer); + if(isTouchDevice())_touchTooltipTimer=setTimeout(function(){tt.style.display='none';},3000); + return; + } + } + tt.style.display='none'; +} + +function findBlockAtTime(t){ + var lo=0,hi=BL.length; + while(lo>1;if(BL[mid].s+BL[mid].d=BL[i].s&&t<=BL[i].s+BL[i].d)return BL[i]; + } + return null; +} + +/* ── Toolbar buttons ──────────────────────────────────────────────────── */ +document.getElementById('openBtn').onclick=function(){ + if(vscApi){vscApi.postMessage({command:'openFile'});} + else{var fi=document.getElementById('fileInput');if(fi)fi.click();} +}; +document.getElementById('exportKspaceBtn').onclick=function(){ + if(vscApi){vscApi.postMessage({command:'exportKspace'});} +}; +function requestM1(channel){ + if(m1Busy)return; + if(m1Data){chVis[channel]=!chVis[channel];buildLegend();draw();return;} + m1RequestedChannel=channel; + m1Busy=true;buildLegend(); + if(vscApi){vscApi.postMessage({command:'calculateM1',referenceMode:m1ReferenceMode});} +} +document.getElementById('pnsBtn').onclick=function(){ + if(pnsBusy)return; + pnsBusy=true;if(pnsBtn)pnsBtn.disabled=true; + if(vscApi){vscApi.postMessage({command:'openPnsAsc'});} +}; +document.getElementById('zi').onclick=function(){if(zoomAtCenter(1.5))scheduleViewerDraw(true);}; +document.getElementById('zo').onclick=function(){if(zoomAtCenter(1/1.5))scheduleViewerDraw(true);}; +document.getElementById('zf').onclick=function(){fit();drawMinimap();}; +document.getElementById('zr').onclick=function(){fit();drawMinimap();}; +document.getElementById('bbc').onchange=function(){showBB=this.checked;draw();}; + +/* ── Mobile hamburger menu ─────────────────────────────────────────── */ +var menuBtn=document.getElementById('menuBtn'); +if(menuBtn){ + menuBtn.onclick=function(e){var m=document.getElementById('tbMore');m.classList.toggle('open');e.stopPropagation();}; + document.addEventListener('click',function(e){ + var m=document.getElementById('tbMore'); + if(m.classList.contains('open')&&!m.contains(e.target)&&e.target!==menuBtn)m.classList.remove('open'); + }); + // Close menu when any option inside it is used + document.getElementById('tbMore').addEventListener('click',function(e){ + if(e.target.tagName==='BUTTON'||e.target.tagName==='SELECT'||e.target.tagName==='INPUT'){ + this.classList.remove('open'); + } + }); +} + +/* ── Minimap interaction ──────────────────────────────────────────────── */ +mmCanvas.addEventListener('mousedown',function(e){ + mmDrag=true;scrollMinimapToMouse(e);e.preventDefault(); +}); +window.addEventListener('mousemove',function(e){ + if(!mmDrag)return; + scrollMinimapToMouse(e); +}); +window.addEventListener('mouseup',function(){mmDrag=false;}); + +/* ── Touch: minimap ──────────────────────────────────────────────── */ +var _mmTouchDrag=false,_mmTouchPinch0=0; +mmCanvas.addEventListener('touchstart',function(e){ + if(e.touches.length===1){_mmTouchDrag=true;scrollMinimapToTouch(e.touches[0]);} + else if(e.touches.length===2){_mmTouchPinch0=getTouchDist(e.touches);} + e.preventDefault(); +},{passive:false}); +mmCanvas.addEventListener('touchmove',function(e){ + if(e.touches.length===1&&_mmTouchDrag){scrollMinimapToTouch(e.touches[0]);} + else if(e.touches.length===2){ + var d=getTouchDist(e.touches),zf=d/Math.max(_mmTouchPinch0,1); + if(zoomAtCenter(zf)){_mmTouchPinch0=d;scheduleViewerDraw(true);} + } + e.preventDefault(); +},{passive:false}); +mmCanvas.addEventListener('touchend',function(){_mmTouchDrag=false;_mmTouchPinch0=0;}); +mmCanvas.addEventListener('touchcancel',function(){_mmTouchDrag=false;_mmTouchPinch0=0;}); + +function scrollMinimapToTouch(touch){ + var r=mmCanvas.getBoundingClientRect(),dpr=window.devicePixelRatio||1; + var mx=touch.clientX-r.left,W=mmCanvas.width/dpr; + var frac=Math.max(0,Math.min(1,mx/W)); + var vsWidth=(mc.width/dpr-M.l-M.r)/sc; + var viewCenter=frac*TD; + ox=viewCenter-vsWidth/2; + clampView(); + draw();if(typeof drawKs==='function')drawKs();drawMinimap(); +} + +/* ── Old mouse minimap ────────────────────────────────────────────── */ +mmCanvas.addEventListener('wheel',function(e){ + e.preventDefault(); + var zf=e.deltaY<0?1.3:1/1.3; + if(zoomAtCenter(zf))scheduleViewerDraw(true); +},{passive:false}); + +function scrollMinimapToMouse(e){ + var r=mmCanvas.getBoundingClientRect(),dpr=window.devicePixelRatio||1; + var mx=e.clientX-r.left; // CSS pixels — e.clientX and r.left are both CSS + var W=mmCanvas.width/dpr; // physical → CSS pixels + var frac=Math.max(0,Math.min(1,mx/W)); + var vsWidth=visibleDuration(); + // Align viewport CENTER to mouse, not left edge + var viewCenter=frac*TD; + ox=viewCenter-vsWidth/2; + clampView(); + draw();if(typeof drawKs==='function')drawKs();drawMinimap(); +} + +/* ── Helpers ──────────────────────────────────────────────────────────── */ +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();} +function nice(r){var ms=[1,2,5,10,20,50,100,200,500,1000,2000,5000,10000,20000,50000]; + for(var i=0;i=r)return ms[i]*b}return 1000*Math.pow(10,Math.floor(Math.log10(r)))} +function fmtT(v){if(v===0)return'0';if(v>=0.001)return v.toFixed(3);return v.toExponential(2)} +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)} +function sampleGradAtTime(g,t){ + if(!g||g.ty==='none'||!g.t||!g.w||g.t.length<2||g.w.length<2||!isFinite(t))return 0; + var n=Math.min(g.t.length,g.w.length); + if(tg.t[n-1])return 0; + var lo=0,hi=n-1; + while(hi-lo>1){var m=(lo+hi)>>1;if(g.t[m]<=t)lo=m;else hi=m;} + var dt=g.t[hi]-g.t[lo]; + if(dt<=0)return g.w[lo]||0; + return g.w[lo]+(g.w[hi]-g.w[lo])*(t-g.t[lo])/dt; +} +function fmtG(g,t){var a=gradConv(sampleGradAtTime(g,t));return a.toFixed(1)+' '+gradUnitStr()+' ('+g.ty+')';} +new MutationObserver(function(){mmCache=null;draw();drawKs();drawMinimap();}).observe(document.body,{attributes:true,attributeFilter:['class']}); +rs(); + diff --git a/scripts/build-extension.mjs b/scripts/build-extension.mjs new file mode 100644 index 0000000..21b5d57 --- /dev/null +++ b/scripts/build-extension.mjs @@ -0,0 +1,80 @@ +/** + * build-extension.mjs — Build the VS Code extension with esbuild. + * + * Replaces `tsc -p ./` for the extension build. Bundles src/extension.ts + * and its dependencies into out/extension.js (CommonJS, targeting Node 18). + * The vscode module is externalised (provided by the VS Code host). + * + * The webview assets are NOT bundled here — they are built separately by + * build-webview.mjs and copied into out/editor/webview/assets/. + * + * Usage: + * node scripts/build-extension.mjs # one-shot build + * node scripts/build-extension.mjs --watch # incremental watch mode + */ + +import * as esbuild from 'esbuild'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); + +const watch = process.argv.includes('--watch'); + +const common = { + bundle: true, + platform: 'node', + target: 'node18', + format: 'cjs', + external: ['vscode'], + sourcemap: true, + logLevel: 'info', +}; + +const extensionEntry = path.join(ROOT, 'src', 'extension.ts'); +const extensionOutfile = path.join(ROOT, 'out', 'extension.js'); + +const cliEntry = path.join(ROOT, 'src', 'cli', 'exportKspace.ts'); +const cliOutfile = path.join(ROOT, 'out', 'cli', 'exportKspace.js'); + +async function build() { + if (watch) { + // Watch mode: use esbuild context for incremental rebuilds + const extCtx = await esbuild.context({ + ...common, + entryPoints: [extensionEntry], + outfile: extensionOutfile, + }); + await extCtx.watch(); + console.log('👁 Watching src/extension.ts …'); + + const cliCtx = await esbuild.context({ + ...common, + entryPoints: [cliEntry], + outfile: cliOutfile, + }); + await cliCtx.watch(); + console.log('👁 Watching src/cli/exportKspace.ts …'); + console.log('(Press Ctrl+C to stop watching)'); + } else { + await esbuild.build({ + ...common, + entryPoints: [extensionEntry], + outfile: extensionOutfile, + }); + console.log('✓ out/extension.js (VS Code extension)'); + + await esbuild.build({ + ...common, + entryPoints: [cliEntry], + outfile: cliOutfile, + }); + console.log('✓ out/cli/exportKspace.js'); + } +} + +build().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/build-webview.mjs b/scripts/build-webview.mjs new file mode 100644 index 0000000..a789ac4 --- /dev/null +++ b/scripts/build-webview.mjs @@ -0,0 +1,63 @@ +/** + * build-webview.mjs — Bundle the shared SeqEyes webview rendering assets. + * + * Concatenates the five webview JS files (state, derived-series, drawing, + * kspace, interaction) into a single self-contained script. The files + * communicate via shared globals so they are concatenated without IIFE + * wrapping — they run in the global scope. The VS Code extension wraps + * them in an IIFE when inlining into the webview; the standalone web app + * loads them directly. + * + * Usage: node scripts/build-webview.mjs + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); + +const ASSETS_DIR = path.join(ROOT, 'src', 'editor', 'webview', 'assets'); + +const files = [ + 'state.js', + 'derived-series.js', + 'drawing.js', + 'kspace.js', + 'interaction.js', +]; + +const parts = files.map(f => { + const content = fs.readFileSync(path.join(ASSETS_DIR, f), 'utf-8'); + return `/* ══ ${f} ══ */\n${content}`; +}); + +const bundle = `/* SeqEyes WebView Bundle — auto-generated, do not edit */ +"use strict"; +${parts.join('\n\n')} +`; + +// Write to web/ (for standalone web app) +const webDir = path.join(ROOT, 'web'); +fs.mkdirSync(webDir, { recursive: true }); +fs.writeFileSync(path.join(webDir, 'webview-bundle.js'), bundle, 'utf-8'); +console.log('✓ web/webview-bundle.js'); + +// Copy to out/ (for VS Code extension — the copy-assets step will use this) +const outDir = path.join(ROOT, 'out', 'editor', 'webview', 'assets'); +fs.mkdirSync(outDir, { recursive: true }); +fs.writeFileSync(path.join(outDir, 'webview-bundle.js'), bundle, 'utf-8'); +console.log('✓ out/editor/webview/assets/webview-bundle.js'); + +// Also copy styles.css to both destinations +const cssContent = fs.readFileSync(path.join(ASSETS_DIR, 'styles.css'), 'utf-8'); +fs.writeFileSync(path.join(webDir, 'styles.css'), cssContent, 'utf-8'); +console.log('✓ web/styles.css'); +fs.writeFileSync(path.join(outDir, 'styles.css'), cssContent, 'utf-8'); +console.log('✓ out/editor/webview/assets/styles.css'); + +// Copy template.html to out/ +const htmlContent = fs.readFileSync(path.join(ASSETS_DIR, 'template.html'), 'utf-8'); +fs.writeFileSync(path.join(outDir, 'template.html'), htmlContent, 'utf-8'); +console.log('✓ out/editor/webview/assets/template.html'); diff --git a/src/editor/webviewContent.ts b/src/editor/webviewContent.ts index 7a1e9e7..2106e7f 100644 --- a/src/editor/webviewContent.ts +++ b/src/editor/webviewContent.ts @@ -1,21 +1,12 @@ /** * webviewContent.ts — SeqEyes webview HTML assembler. * - * Reads CSS, HTML, and JS from real files under webview/assets/ so you get - * full syntax highlighting, IntelliSense, and formatting in each language. + * Reads the pre-built webview bundle (produced by scripts/build-webview.mjs) + * plus CSS and HTML template from the out/ directory. The webview JS is + * wrapped in an IIFE so it runs isolated from the VS Code webview host. * - * Asset files (copied to out/ during build): - * webview/assets/styles.css → @@ -166,9 +99,9 @@

SeqEyes

- + - +