Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions src/editor/seqEditorProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Custom Text Editor Provider for Pulseq .seq files.
* Custom readonly editor provider for Pulseq .seq files.
*
* Registered as `seqeyes.sequenceViewer` — opens automatically when the user
* opens a `.seq` file. The provider:
Expand Down Expand Up @@ -84,9 +84,17 @@ export async function exportKspaceToDirectoryForTest(
return await writeKspaceArtifacts(sourceUri, saveUri, packageVersion, defaultStem);
}

class SeqDocument implements vscode.CustomDocument {
constructor(public readonly uri: vscode.Uri) { }

dispose(): void {
// This viewer is read-only and does not hold native resources.
}
}

// ─── Provider class ───────────────────────────────────────────────────────

export class SeqEditorProvider implements vscode.CustomTextEditorProvider {
export class SeqEditorProvider implements vscode.CustomReadonlyEditorProvider<SeqDocument> {

/** Register the provider with VS Code. */
static register(ctx: vscode.ExtensionContext): vscode.Disposable {
Expand All @@ -98,10 +106,18 @@ export class SeqEditorProvider implements vscode.CustomTextEditorProvider {

constructor(private readonly _ctx: vscode.ExtensionContext) { }

// ── resolveCustomTextEditor ──────────────────────────────────────
openCustomDocument(
uri: vscode.Uri,
_openContext: vscode.CustomDocumentOpenContext,
_token: vscode.CancellationToken,
): SeqDocument {
return new SeqDocument(uri);
}

async resolveCustomTextEditor(
doc: vscode.TextDocument,
// ── resolveCustomEditor ──────────────────────────────────────────

async resolveCustomEditor(
doc: SeqDocument,
panel: vscode.WebviewPanel,
_token: vscode.CancellationToken,
): Promise<void> {
Expand All @@ -123,10 +139,9 @@ export class SeqEditorProvider implements vscode.CustomTextEditorProvider {
};

postProgress('start', 0, 'Reading file\u2026');
const text = Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8');

postProgress('parse', 5, 'Parsing Pulseq sequence\u2026');
const seq = parseSequenceText(text);
const seq = await readAndParseSequence(uri, () => {
postProgress('parse', 5, 'Parsing Pulseq sequence\u2026');
});

postProgress('timing', 10, 'Detecting TR/TE timing\u2026');
const timing = detectSequenceTiming(seq);
Expand Down Expand Up @@ -218,9 +233,8 @@ export class SeqEditorProvider implements vscode.CustomTextEditorProvider {
}
};

// ── Initial load: validate, set full UI, show progress, then send data ──
// ── Initial load: set full UI, show progress, then send data ──
try {
parseSequenceText(Buffer.from(await vscode.workspace.fs.readFile(doc.uri)).toString('utf8'));
panel.webview.html = getWebviewContent(0);
// Give the webview a moment to parse its new HTML, then start progress
panel.webview.postMessage({ type: 'progress', phase: 'start', percent: 0, text: 'Preparing\u2026' });
Expand Down Expand Up @@ -491,6 +505,13 @@ function serializeBlocks(blocks: DecodedBlock[]): object[] {
});
}

async function readAndParseSequence(uri: vscode.Uri, didRead: () => void) {
const fileBytes = await vscode.workspace.fs.readFile(uri);
didRead();
const text = Buffer.from(fileBytes.buffer, fileBytes.byteOffset, fileBytes.byteLength).toString('utf8');
return parseSequenceText(text);
}

function serializeGrad(g: DecodedGradWaveform): Record<string, unknown> {
return {
s: g.startTime, d: g.duration,
Expand Down
108 changes: 108 additions & 0 deletions src/editor/webview/assets/derived-series.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,111 @@ function forEachDerivedPoint(series,viewStart,viewEnd,maxPoints,visit){
emitRange(Math.max(i0,lastBucket*size),i1);
return emitted;
}

/* Block-indexed min/max summaries for RF, gradients, and ADC occupancy. */
function createWaveformOverview(blocks){
if(!blocks||!blocks.length)return null;
var level=buildWaveformOverviewLevel(blocks,8),levels=[level];
while(level.count>1){level=mergeWaveformOverviewLevel(level);levels.push(level);}
return{levels:levels,blockCount:blocks.length,pointPrefix:createWaveformPointPrefixes(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),
gyMin:new Float64Array(count),gyMax:new Float64Array(count),gzMin:new Float64Array(count),gzMax:new Float64Array(count),
adcStart:new Float64Array(count),adcEnd:new Float64Array(count)};
var mins=[level.rfMin,level.gxMin,level.gyMin,level.gzMin,level.adcStart];
var maxs=[level.rfMax,level.gxMax,level.gyMax,level.gzMax,level.adcEnd];
for(var m=0;m<mins.length;m++)mins[m].fill(Infinity);
for(var x=0;x<maxs.length;x++)maxs[x].fill(-Infinity);
return level;
}

function createWaveformPointPrefixes(blocks){
var keys=['rf','phase','gx','gy','gz','adc'],prefix={};
for(var ki=0;ki<keys.length;ki++)prefix[keys[ki]]=new Float64Array(blocks.length+1);
for(var i=0;i<blocks.length;i++){
var block=blocks[i],rf=block.rf,adc=block.adc;
prefix.rf[i+1]=prefix.rf[i]+(rf&&rf.t&&rf.m?Math.min(rf.t.length,rf.m.length):0);
var rfPhase=rf&&rf.t&&rf.p?Math.min(rf.t.length,rf.p.length):0,adcPhase=0;
if(adc&&adc.n>1){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;i<values.length;i++){
var value=values[i];if(!isFinite(value))continue;
if(value<minArray[bucket])minArray[bucket]=value;
if(value>maxArray[bucket])maxArray[bucket]=value;
}
}

function buildWaveformOverviewLevel(blocks,bucketSize){
var count=Math.ceil(blocks.length/bucketSize),level=createEmptyWaveformOverviewLevel(count,bucketSize);
for(var bucket=0;bucket<count;bucket++){
var start=bucket*bucketSize,end=Math.min(blocks.length,start+bucketSize);
level.t0[bucket]=blocks[start].s;level.t1[bucket]=blocks[end-1].s+blocks[end-1].d;
for(var bi=start;bi<end;bi++){
var block=blocks[bi],rf=block.rf;
if(rf){
includeOverviewValues(rf.m,level.rfMin,level.rfMax,bucket);if(level.rfMin[bucket]===Infinity)includeOverviewValues([rf.a||0],level.rfMin,level.rfMax,bucket);
}
var gx=block.gx,gy=block.gy,gz=block.gz;
if(gx&&gx.ty!=='none')includeOverviewValues(gx.w,level.gxMin,level.gxMax,bucket);
if(gy&&gy.ty!=='none')includeOverviewValues(gy.w,level.gyMin,level.gyMax,bucket);
if(gz&&gz.ty!=='none')includeOverviewValues(gz.w,level.gzMin,level.gzMax,bucket);
if(block.adc){
var adcStart=block.adc.s+block.adc.d,adcEnd=adcStart+block.adc.n*block.adc.dw;
if(adcStart<level.adcStart[bucket])level.adcStart[bucket]=adcStart;
if(adcEnd>level.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'],['gxMin','gxMax'],['gyMin','gyMax'],['gzMin','gzMax'],['adcStart','adcEnd']];
for(var bucket=0;bucket<count;bucket++){
var start=bucket*4,end=Math.min(child.count,start+4);
level.t0[bucket]=child.t0[start];level.t1[bucket]=child.t1[end-1];
for(var ci=0;ci<channels.length;ci++){
var minKey=channels[ci][0],maxKey=channels[ci][1];
for(var i=start;i<end;i++){
if(child[minKey][i]<level[minKey][bucket])level[minKey][bucket]=child[minKey][i];
if(child[maxKey][i]>level[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 selectWaveformOverview(overview,startBlock,endBlock,maxBuckets){
if(!overview||endBlock<=startBlock)return null;
var selected=overview.levels[overview.levels.length-1];
for(var i=0;i<overview.levels.length;i++){
var candidate=overview.levels[i];
var count=Math.ceil(endBlock/candidate.bucketSize)-Math.floor(startBlock/candidate.bucketSize);
if(count<=maxBuckets){selected=candidate;break;}
}
return{level:selected,first:Math.floor(startBlock/selected.bucketSize),last:Math.min(selected.count,Math.ceil(endBlock/selected.bucketSize))};
}
88 changes: 78 additions & 10 deletions src/editor/webview/assets/drawing.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function draw(){
var visibleRange=visibleDuration();var vs=ox,ve=ox+visibleRange;
drawGrid(w,h,vs,ve,s);
drawZeroLines(w,h,s);
if(showBB)drawBlockBounds(w,h,s);
if(showBB)drawBlockBounds(w,h,vs,ve,s);
drawBlocks(vs,ve,s);
drawDerivedChannels(vs,ve,s);
drawAxes(w,h,vs,ve,s);
Expand Down Expand Up @@ -39,9 +39,10 @@ function drawZeroLines(w,h,s){
var vc=visChannels();for(var vi=0;vi<vc.length;vi++){var i=vc[vi];if((i>=2&&i<=4)||(i>=8&&i<=10)){ctx.beginPath();ctx.moveTo(M.l,cy(vi));ctx.lineTo(w-M.r,cy(vi));ctx.stroke();}}
ctx.setLineDash([]);
}
function drawBlockBounds(w,h,s){
function drawBlockBounds(w,h,vs,ve,s){
ctx.strokeStyle=s.getPropertyValue('--ax').trim();ctx.lineWidth=0.6;ctx.setLineDash([3,6]);
for(var i=0;i<BL.length;i++){
var range=visibleBlockRange(vs,ve);
for(var i=range.start;i<range.end;i++){
var x=t2x(BL[i].s);if(x<M.l||x>w-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';
Expand Down Expand Up @@ -232,13 +233,80 @@ function drawBlocks(vs,ve,s){
adc:s.getPropertyValue('--adc').trim(),adf:s.getPropertyValue('--adf').trim(),
tr:s.getPropertyValue('--tr').trim(),fg:s.getPropertyValue('--fg').trim()
};
if(rows[0]>=0)drawRfBlocks(range.start,range.end,rows[0],ch,colors,vs,ve);
if(rows[1]>=0)drawPhaseBlocks(range.start,range.end,rows[1],ch,colors,vs,ve);
if(rows[2]>=0)drawGradientBlocks(range.start,range.end,'gx',rows[2],2,ch,colors.gx,vs,ve);
if(rows[3]>=0)drawGradientBlocks(range.start,range.end,'gy',rows[3],3,ch,colors.gy,vs,ve);
if(rows[4]>=0)drawGradientBlocks(range.start,range.end,'gz',rows[4],4,ch,colors.gz,vs,ve);
if(rows[5]>=0)drawAdcBlocks(range.start,range.end,rows[5],ch,colors,vs,ve);
if(rows[6]>=0)drawTriggerBlocks(range.start,range.end,rows[6],ch,colors,vs,ve);
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;}
if(rows[0]>=0){if(useOverview('rf'))drawRfOverview(overview,rows[0],ch,colors,vs,ve);else drawRfBlocks(range.start,range.end,rows[0],ch,colors,vs,ve);}
if(rows[1]>=0){if(useOverview('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(useOverview('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(useOverview('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(useOverview('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(useOverview('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 level=summary.level,y=cy(vi),scale=ch*.9/channelRange(0);
rowClip(vi,ch,function(){
ctx.strokeStyle=colors.rf;ctx.lineWidth=1;ctx.beginPath();
for(var i=summary.first;i<summary.last;i++){
if(level.t1[i]<vs||level.t0[i]>ve||level.rfMin[i]===Infinity)continue;
var x=t2x(.5*(level.t0[i]+level.t1[i])),y0=y+ch*.45-level.rfMin[i]*scale,y1=y+ch*.45-level.rfMax[i]*scale;
if(Math.abs(y1-y0)<1){y0-=.5;y1+=.5;}ctx.moveTo(x,y0);ctx.lineTo(x,y1);
}
ctx.stroke();
});
}

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<sampleCount;sampleIndex++){
var ordinal=Math.min(last-1,Math.floor(first+(sampleIndex+.5)*sampleStep)),lo=start,hi=end;
while(lo<hi){var mid=(lo+hi)>>1;if(prefix[mid+1]<=ordinal)lo=mid+1;else hi=mid;}
if(lo>=end)continue;
var block=BL[lo],rf=block.rf,rfCount=rf&&rf.t&&rf.p?Math.min(rf.t.length,rf.p.length):0;
var local=ordinal-prefix[lo],time=NaN,value=NaN,isAdc=false;
if(local<rfCount){time=rf.t[local];value=rf.p[local];}
else if(block.adc&&block.adc.n>1){
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=adcSample<adc.n?adcStart+(adcSample+.5)*adc.dw:adcEnd;
value=((adc.po||0)+6.283185*(adc.fo||0)*(time-adcStart))%6.28318;value=(value+6.28318)%6.28318;isAdc=true;
}
if(!isFinite(time)||!isFinite(value)||time<vs||time>ve)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'],y=cy(vi),scale=ch*.4/channelRange(ci);
rowClip(vi,ch,function(){
ctx.strokeStyle=color;ctx.lineWidth=1;ctx.beginPath();
for(var i=summary.first;i<summary.last;i++){
if(level.t1[i]<vs||level.t0[i]>ve||minValues[i]===Infinity)continue;
var x=t2x(.5*(level.t0[i]+level.t1[i])),y0=y-minValues[i]*scale,y1=y-maxValues[i]*scale;
if(Math.abs(y1-y0)<1){y0-=.5;y1+=.5;}ctx.moveTo(x,y0);ctx.lineTo(x,y1);
}
ctx.stroke();
});
}

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;i<summary.last;i++){
if(level.adcStart[i]===Infinity||level.adcEnd[i]<vs||level.adcStart[i]>ve)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){
Expand Down
13 changes: 11 additions & 2 deletions src/editor/webview/assets/interaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Mouse interaction
═══════════════════════════════════════════════════════════════════════ */

var _touchTooltipTimer=null,_pointerFrame=0,_pendingPointer=null;
var _touchTooltipTimer=null,_pointerFrame=0,_pendingPointer=null,mmDrag=false;

mc.addEventListener('wheel',function(e){e.preventDefault();
var changed=false;
Expand Down Expand Up @@ -137,7 +137,7 @@ function placeTooltip(cx,cy){
function showTooltipAt(cx,cy,ct){
var ch=cH(),vc=visChannels(),vi2=Math.floor((cy-mc.getBoundingClientRect().top-M.t)/ch);
if(vi2>=0&&vi2<vc.length){
var found=null;for(var i=0;i<BL.length;i++){if(ct>=BL[i].s&&ct<=BL[i].s+BL[i].d){found=BL[i];break}}
var found=findBlockAtTime(ct);
var lines=[];
if(found){
var blockDt=Math.max(0,Math.min(found.d,ct-found.s));
Expand All @@ -164,6 +164,15 @@ function showTooltipAt(cx,cy,ct){
tt.style.display='none';
}

function findBlockAtTime(t){
var lo=0,hi=BL.length;
while(lo<hi){var mid=(lo+hi)>>1;if(BL[mid].s+BL[mid].d<t)lo=mid+1;else hi=mid;}
for(var i=lo;i<Math.min(BL.length,lo+2);i++){
if(t>=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'});}
Expand Down
6 changes: 4 additions & 2 deletions src/editor/webview/assets/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ var cc=document.getElementById('cc'),mc=document.getElementById('mc'),ctx=mc.get
tuSel=document.getElementById('tu'),guSel=document.getElementById('gu');
var exportBtn=document.getElementById('exportKspaceBtn');
var pnsBtn=document.getElementById('pnsBtn');
var BL=[],TD=0,GR=1e-5,RR=1e-6,AR=1e-7,BR=1e-5; // blocks, duration, rasters [s]
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)'];
Expand Down Expand Up @@ -206,6 +206,7 @@ window.addEventListener('message',function(e){
}
if(m.type==='sequenceData'){
BL=m.blocks||[];TD=m.totalDuration||0;GR=m.gradRaster||1e-5;
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
Expand Down Expand Up @@ -488,7 +489,8 @@ function drawMinimap(){
mmCtx.strokeStyle=s.getPropertyValue('--fg').trim();
mmCtx.globalAlpha=0.10;mmCtx.lineWidth=0.4;
mmCtx.beginPath();
for(var tr=0;tr<=seqTiming.trCount;tr++){
var 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);}
}
Expand Down
Loading
Loading