seqeyes-python 0.3.2__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/PKG-INFO +1 -1
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/pyproject.toml +1 -1
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/src/seqeyes/resources/pulseq-bundle.js +198 -29
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/src/seqeyes/resources/webview-bundle.js +352 -11
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/.gitignore +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/README.md +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/examples/demo.ipynb +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/examples/demo.py +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/src/seqeyes/_plot.py +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/src/seqeyes/_renderer.py +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/src/seqeyes/resources/viewer.html +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/tests/conftest.py +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/tests/debug_full.html +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/tests/integration_test.py +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/tests/test_plot.py +0 -0
- {seqeyes_python-0.3.2 → seqeyes_python-0.3.4}/tests/test_renderer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: seqeyes-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq.
|
|
5
5
|
Project-URL: Homepage, https://github.com/bughht/seqeyes_plugin
|
|
6
6
|
Project-URL: Repository, https://github.com/bughht/seqeyes_plugin
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seqeyes-python"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.4"
|
|
8
8
|
description = "Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -23,7 +23,12 @@ var Pulseq = (() => {
|
|
|
23
23
|
// web/pulseq-browser.ts
|
|
24
24
|
var pulseq_browser_exports = {};
|
|
25
25
|
__export(pulseq_browser_exports, {
|
|
26
|
+
BAND_DETAIL_SAMPLES: () => BAND_DETAIL_SAMPLES,
|
|
27
|
+
EXACT_DETAIL_SAMPLES: () => EXACT_DETAIL_SAMPLES,
|
|
26
28
|
INTERACTIVE_COMPUTE_LIMITS: () => INTERACTIVE_COMPUTE_LIMITS,
|
|
29
|
+
MAX_DETAIL_PTS: () => MAX_DETAIL_PTS,
|
|
30
|
+
MAX_DISPLAY_PTS: () => MAX_DISPLAY_PTS,
|
|
31
|
+
MAX_ENVELOPE_COLUMNS: () => MAX_ENVELOPE_COLUMNS,
|
|
27
32
|
MAX_RF_RESPONSE_BANDS: () => MAX_RF_RESPONSE_BANDS,
|
|
28
33
|
MAX_RF_RESPONSE_FFT_POINTS: () => MAX_RF_RESPONSE_FFT_POINTS,
|
|
29
34
|
MAX_RF_RESPONSE_SAMPLES: () => MAX_RF_RESPONSE_SAMPLES,
|
|
@@ -36,10 +41,13 @@ var Pulseq = (() => {
|
|
|
36
41
|
calculatePns: () => calculatePns,
|
|
37
42
|
calculatePnsCoarse: () => calculatePnsCoarse,
|
|
38
43
|
computeGradSpectrumParity: () => computeGradSpectrumParity,
|
|
44
|
+
computeGradientEnvelope: () => computeGradientEnvelope,
|
|
39
45
|
computeGradientSpectrogram: () => computeGradientSpectrogram,
|
|
40
46
|
computeGradientSpectrumAverage: () => computeGradientSpectrumAverage,
|
|
41
47
|
computeGradientSpectrumSlice: () => computeGradientSpectrumSlice,
|
|
42
48
|
countBandsOutsideRange: () => countBandsOutsideRange,
|
|
49
|
+
countExactDetailSamples: () => countExactDetailSamples,
|
|
50
|
+
countGradientSamples: () => countGradientSamples,
|
|
43
51
|
createSequenceDecodeContext: () => createSequenceDecodeContext,
|
|
44
52
|
decodeAllBlocks: () => decodeAllBlocks,
|
|
45
53
|
decodeBlockRange: () => decodeBlockRange,
|
|
@@ -64,6 +72,8 @@ var Pulseq = (() => {
|
|
|
64
72
|
hasPulseqBinaryMagic: () => hasPulseqBinaryMagic,
|
|
65
73
|
isEmptyAscProfile: () => isEmptyAscProfile,
|
|
66
74
|
kspaceExceedsInteractiveBudget: () => kspaceExceedsInteractiveBudget,
|
|
75
|
+
packGradientEnvelope: () => packGradientEnvelope,
|
|
76
|
+
packSequenceBlockRange: () => packSequenceBlockRange,
|
|
67
77
|
packSequenceBlocks: () => packSequenceBlocks,
|
|
68
78
|
parseAcousticResonancesAsc: () => parseAcousticResonancesAsc,
|
|
69
79
|
parseAscProfile: () => parseAscProfile,
|
|
@@ -74,6 +84,7 @@ var Pulseq = (() => {
|
|
|
74
84
|
parseSequenceText: () => parseSequenceText,
|
|
75
85
|
physicalGradientValueAt: () => physicalGradientValueAt,
|
|
76
86
|
resamplePhysicalGradients: () => resamplePhysicalGradients,
|
|
87
|
+
resolveDetailBlockRange: () => resolveDetailBlockRange,
|
|
77
88
|
resolveSpectrogramParams: () => resolveSpectrogramParams,
|
|
78
89
|
rotateGradient: () => rotateGradient,
|
|
79
90
|
safePnsModel: () => safePnsModel,
|
|
@@ -86,7 +97,7 @@ var Pulseq = (() => {
|
|
|
86
97
|
});
|
|
87
98
|
|
|
88
99
|
// package.json
|
|
89
|
-
var version = "0.3.
|
|
100
|
+
var version = "0.3.4";
|
|
90
101
|
|
|
91
102
|
// src/pulseq/decompressor.ts
|
|
92
103
|
function decompressShape(compressed, numSamples) {
|
|
@@ -2426,40 +2437,55 @@ var Pulseq = (() => {
|
|
|
2426
2437
|
}
|
|
2427
2438
|
|
|
2428
2439
|
// src/pulseq/displayDownsampling.ts
|
|
2429
|
-
function
|
|
2430
|
-
const n = Math.
|
|
2440
|
+
function reduceM4Range(time, values, start, end, maxPoints, emit) {
|
|
2441
|
+
const n = Math.max(0, end - start);
|
|
2431
2442
|
if (n === 0 || maxPoints <= 0) return 0;
|
|
2432
2443
|
if (n <= maxPoints) {
|
|
2433
|
-
let
|
|
2434
|
-
|
|
2435
|
-
emit(time[index], values[index]);
|
|
2436
|
-
kept2++;
|
|
2437
|
-
}
|
|
2438
|
-
return kept2;
|
|
2444
|
+
for (let index = start; index < end; index++) emit(time[index], values[index]);
|
|
2445
|
+
return n;
|
|
2439
2446
|
}
|
|
2440
2447
|
const bucketCount = Math.max(1, Math.floor(maxPoints / 4));
|
|
2441
2448
|
let kept = 0;
|
|
2442
2449
|
for (let bucket = 0; bucket < bucketCount; bucket++) {
|
|
2443
|
-
const
|
|
2444
|
-
const
|
|
2445
|
-
kept += emitBucket(time, values,
|
|
2450
|
+
const bucketStart = start + Math.floor(bucket * n / bucketCount);
|
|
2451
|
+
const bucketEnd = Math.max(bucketStart + 1, start + Math.floor((bucket + 1) * n / bucketCount));
|
|
2452
|
+
kept += emitBucket(time, values, bucketStart, Math.min(end, bucketEnd), emit);
|
|
2446
2453
|
}
|
|
2447
2454
|
return kept;
|
|
2448
2455
|
}
|
|
2449
|
-
function
|
|
2450
|
-
const n = Math.
|
|
2456
|
+
function reduceUniformRange(time, values, start, end, maxPoints, emit) {
|
|
2457
|
+
const n = Math.max(0, end - start);
|
|
2451
2458
|
if (n === 0 || maxPoints <= 0) return 0;
|
|
2452
2459
|
if (n <= maxPoints) {
|
|
2453
|
-
for (let index =
|
|
2460
|
+
for (let index = start; index < end; index++) emit(time[index], values[index]);
|
|
2454
2461
|
return n;
|
|
2455
2462
|
}
|
|
2456
2463
|
const step = n / maxPoints;
|
|
2457
2464
|
for (let index = 0; index < maxPoints; index++) {
|
|
2458
|
-
|
|
2459
|
-
emit(time[source], values[source]);
|
|
2465
|
+
emit(time[start + Math.floor(index * step)], values[start + Math.floor(index * step)]);
|
|
2460
2466
|
}
|
|
2461
2467
|
return maxPoints;
|
|
2462
2468
|
}
|
|
2469
|
+
function clipIndexRange(time, startSec, endSec, length) {
|
|
2470
|
+
const n = Math.max(0, Math.min(length, time.length));
|
|
2471
|
+
if (n === 0) return { start: 0, end: 0 };
|
|
2472
|
+
let lo = 0;
|
|
2473
|
+
let hi = n;
|
|
2474
|
+
while (lo < hi) {
|
|
2475
|
+
const mid = lo + hi >> 1;
|
|
2476
|
+
if (time[mid] < startSec) lo = mid + 1;
|
|
2477
|
+
else hi = mid;
|
|
2478
|
+
}
|
|
2479
|
+
const start = Math.max(0, lo - 1);
|
|
2480
|
+
lo = start;
|
|
2481
|
+
hi = n;
|
|
2482
|
+
while (lo < hi) {
|
|
2483
|
+
const mid = lo + hi >> 1;
|
|
2484
|
+
if (time[mid] <= endSec) lo = mid + 1;
|
|
2485
|
+
else hi = mid;
|
|
2486
|
+
}
|
|
2487
|
+
return { start, end: Math.min(n, lo + 1) };
|
|
2488
|
+
}
|
|
2463
2489
|
function emitBucket(time, values, start, end, emit) {
|
|
2464
2490
|
let minIndex = start;
|
|
2465
2491
|
let maxIndex = start;
|
|
@@ -2486,24 +2512,36 @@ var Pulseq = (() => {
|
|
|
2486
2512
|
// src/editor/blockTransport.ts
|
|
2487
2513
|
var MAX_DISPLAY_PTS = 500;
|
|
2488
2514
|
var MIN_DISPLAY_PTS = 8;
|
|
2515
|
+
var MAX_DETAIL_PTS = 4096;
|
|
2516
|
+
var EXACT_DETAIL_SAMPLES = 5e4;
|
|
2517
|
+
var BAND_DETAIL_SAMPLES = INTERACTIVE_COMPUTE_LIMITS.derivedRasterSamples;
|
|
2518
|
+
var WINDOW_DETAIL_SAMPLE_LIMIT = 2e6;
|
|
2489
2519
|
var TAU2 = 2 * Math.PI;
|
|
2490
2520
|
var EMPTY_PAIR = { o: 0, n: 0 };
|
|
2491
2521
|
var CountingSink = class {
|
|
2492
|
-
constructor(cap) {
|
|
2522
|
+
constructor(cap, window = null) {
|
|
2493
2523
|
__publicField(this, "cap", cap);
|
|
2524
|
+
__publicField(this, "window", window);
|
|
2494
2525
|
__publicField(this, "buildEnvelope", false);
|
|
2495
2526
|
__publicField(this, "total", 0);
|
|
2496
2527
|
}
|
|
2497
2528
|
pair(time, values, useM4) {
|
|
2498
|
-
|
|
2529
|
+
const { start, end } = clipPair(time, values, this.window);
|
|
2530
|
+
this.total += useM4 ? reduceM4Range(time, values, start, end, this.cap, discard) : reduceUniformRange(time, values, start, end, this.cap, discard);
|
|
2499
2531
|
return EMPTY_PAIR;
|
|
2500
2532
|
}
|
|
2501
2533
|
};
|
|
2534
|
+
function clipPair(time, values, window) {
|
|
2535
|
+
const n = Math.min(time.length, values.length);
|
|
2536
|
+
if (!window) return { start: 0, end: n };
|
|
2537
|
+
return clipIndexRange(time, window.startSec, window.endSec, n);
|
|
2538
|
+
}
|
|
2502
2539
|
var WritingSink = class {
|
|
2503
|
-
constructor(cap, times, values) {
|
|
2540
|
+
constructor(cap, times, values, window = null) {
|
|
2504
2541
|
__publicField(this, "cap", cap);
|
|
2505
2542
|
__publicField(this, "times", times);
|
|
2506
2543
|
__publicField(this, "values", values);
|
|
2544
|
+
__publicField(this, "window", window);
|
|
2507
2545
|
__publicField(this, "buildEnvelope", true);
|
|
2508
2546
|
__publicField(this, "cursor", 0);
|
|
2509
2547
|
__publicField(this, "wrapPhase", false);
|
|
@@ -2518,12 +2556,13 @@ var Pulseq = (() => {
|
|
|
2518
2556
|
return this.cursor;
|
|
2519
2557
|
}
|
|
2520
2558
|
pair(time, values, useM4, wrapPhase) {
|
|
2521
|
-
const
|
|
2559
|
+
const origin = this.cursor;
|
|
2560
|
+
const clipped = clipPair(time, values, this.window);
|
|
2522
2561
|
this.wrapPhase = wrapPhase;
|
|
2523
|
-
if (useM4)
|
|
2524
|
-
else
|
|
2562
|
+
if (useM4) reduceM4Range(time, values, clipped.start, clipped.end, this.cap, this.emit);
|
|
2563
|
+
else reduceUniformRange(time, values, clipped.start, clipped.end, this.cap, this.emit);
|
|
2525
2564
|
this.wrapPhase = false;
|
|
2526
|
-
return { o:
|
|
2565
|
+
return { o: origin, n: this.cursor - origin };
|
|
2527
2566
|
}
|
|
2528
2567
|
};
|
|
2529
2568
|
function discard() {
|
|
@@ -2563,11 +2602,52 @@ var Pulseq = (() => {
|
|
|
2563
2602
|
notice: cap < MAX_DISPLAY_PTS ? `Large sequence: waveform detail was reduced to ${cap} points per event (normally ${MAX_DISPLAY_PTS}) to stay inside the display transfer budget.` : null
|
|
2564
2603
|
};
|
|
2565
2604
|
}
|
|
2566
|
-
function
|
|
2567
|
-
const
|
|
2605
|
+
function packSequenceBlockRange(seq, startBlock, endBlock, context = createSequenceDecodeContext(seq), requestedCap = MAX_DISPLAY_PTS, sampleLimit = WINDOW_DETAIL_SAMPLE_LIMIT, window = null) {
|
|
2606
|
+
const start = Math.max(0, Math.min(seq.blocks.length, Math.floor(startBlock)));
|
|
2607
|
+
const end = Math.max(start, Math.min(seq.blocks.length, Math.ceil(endBlock)));
|
|
2608
|
+
const seriesCount = countSequenceWaveformSeriesRange(seq, start, end);
|
|
2609
|
+
const safeSampleLimit = Number.isFinite(sampleLimit) && sampleLimit >= 0 ? Math.floor(sampleLimit) : WINDOW_DETAIL_SAMPLE_LIMIT;
|
|
2610
|
+
const safeRequestedCap = Number.isFinite(requestedCap) && requestedCap > 0 ? Math.floor(requestedCap) : MAX_DISPLAY_PTS;
|
|
2611
|
+
if (seriesCount * MIN_DISPLAY_PTS > safeSampleLimit) {
|
|
2612
|
+
throw new Error(
|
|
2613
|
+
`The waveform detail window needs at least ${seriesCount * MIN_DISPLAY_PTS} samples; zoom in further.`
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
const cap = seriesCount > 0 ? Math.max(
|
|
2617
|
+
MIN_DISPLAY_PTS,
|
|
2618
|
+
Math.min(safeRequestedCap, Math.floor(safeSampleLimit / seriesCount))
|
|
2619
|
+
) : safeRequestedCap;
|
|
2620
|
+
const decoded = decodeBlockRange(seq, start, end, context);
|
|
2621
|
+
const clip = window && window.endSec > window.startSec ? window : null;
|
|
2622
|
+
const exactTotal = countSamples(decoded, Number.MAX_SAFE_INTEGER, clip);
|
|
2623
|
+
if (exactTotal <= Math.min(EXACT_DETAIL_SAMPLES, safeSampleLimit)) {
|
|
2624
|
+
return packBlocksAtCap(decoded, Number.MAX_SAFE_INTEGER, exactTotal, clip);
|
|
2625
|
+
}
|
|
2626
|
+
return packBlocksAtCap(decoded, cap, void 0, clip);
|
|
2627
|
+
}
|
|
2628
|
+
function resolveDetailBlockRange(blockStartTimes, blockCount, startSec, endSec) {
|
|
2629
|
+
const lowerBound = (target) => {
|
|
2630
|
+
let lo = 0;
|
|
2631
|
+
let hi = blockStartTimes.length;
|
|
2632
|
+
while (lo < hi) {
|
|
2633
|
+
const mid = lo + hi >> 1;
|
|
2634
|
+
if (blockStartTimes[mid] < target) lo = mid + 1;
|
|
2635
|
+
else hi = mid;
|
|
2636
|
+
}
|
|
2637
|
+
return lo;
|
|
2638
|
+
};
|
|
2639
|
+
const start = Math.max(0, Math.min(blockCount, lowerBound(startSec) - 1));
|
|
2640
|
+
const end = Math.max(start, Math.min(blockCount, lowerBound(endSec) + 1));
|
|
2641
|
+
return { start, end };
|
|
2642
|
+
}
|
|
2643
|
+
function countExactDetailSamples(blocks, window = null) {
|
|
2644
|
+
return countSamples(blocks, Number.MAX_SAFE_INTEGER, window);
|
|
2645
|
+
}
|
|
2646
|
+
function packBlocksAtCap(blocks, cap, knownTotal, window = null) {
|
|
2647
|
+
const total = knownTotal ?? countSamples(blocks, cap, window);
|
|
2568
2648
|
const times = new Float64Array(total);
|
|
2569
2649
|
const values = new Float32Array(total);
|
|
2570
|
-
const sink = new WritingSink(cap, times, values);
|
|
2650
|
+
const sink = new WritingSink(cap, times, values, window);
|
|
2571
2651
|
const envelope = new Array(blocks.length);
|
|
2572
2652
|
for (let index = 0; index < blocks.length; index++) {
|
|
2573
2653
|
envelope[index] = walkBlock(blocks[index], sink);
|
|
@@ -2611,8 +2691,8 @@ var Pulseq = (() => {
|
|
|
2611
2691
|
if (gradient) gradient.o += delta;
|
|
2612
2692
|
}
|
|
2613
2693
|
}
|
|
2614
|
-
function countSamples(blocks, cap) {
|
|
2615
|
-
const sink = new CountingSink(cap);
|
|
2694
|
+
function countSamples(blocks, cap, window = null) {
|
|
2695
|
+
const sink = new CountingSink(cap, window);
|
|
2616
2696
|
for (const block of blocks) walkBlock(block, sink);
|
|
2617
2697
|
return sink.total;
|
|
2618
2698
|
}
|
|
@@ -2717,6 +2797,95 @@ var Pulseq = (() => {
|
|
|
2717
2797
|
return { peak, area, blockPulse };
|
|
2718
2798
|
}
|
|
2719
2799
|
|
|
2800
|
+
// src/pulseq/gradientEnvelope.ts
|
|
2801
|
+
var GRADIENT_CHANNELS = ["gx", "gy", "gz"];
|
|
2802
|
+
var MAX_ENVELOPE_COLUMNS = 8192;
|
|
2803
|
+
function emptyChannel(columns) {
|
|
2804
|
+
const min = new Float32Array(columns).fill(Infinity);
|
|
2805
|
+
const max = new Float32Array(columns).fill(-Infinity);
|
|
2806
|
+
return { min, max, filled: new Uint8Array(columns) };
|
|
2807
|
+
}
|
|
2808
|
+
function accumulate(channel, grad, startSec, endSec, columns) {
|
|
2809
|
+
const time = grad.timePoints;
|
|
2810
|
+
const values = grad.waveform;
|
|
2811
|
+
const n = Math.min(time.length, values.length);
|
|
2812
|
+
if (n === 0) return;
|
|
2813
|
+
const span = endSec - startSec;
|
|
2814
|
+
if (!(span > 0)) return;
|
|
2815
|
+
const toColumn = (t) => (t - startSec) / span * columns;
|
|
2816
|
+
const put = (column, value) => {
|
|
2817
|
+
if (column < 0 || column >= columns || !Number.isFinite(value)) return;
|
|
2818
|
+
if (value < channel.min[column]) channel.min[column] = value;
|
|
2819
|
+
if (value > channel.max[column]) channel.max[column] = value;
|
|
2820
|
+
channel.filled[column] = 1;
|
|
2821
|
+
};
|
|
2822
|
+
if (n === 1) {
|
|
2823
|
+
put(Math.floor(toColumn(time[0])), values[0]);
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
for (let i = 1; i < n; i++) {
|
|
2827
|
+
const t0 = time[i - 1];
|
|
2828
|
+
const t1 = time[i];
|
|
2829
|
+
if (t1 < startSec || t0 > endSec) continue;
|
|
2830
|
+
const c0 = toColumn(t0);
|
|
2831
|
+
const c1 = toColumn(t1);
|
|
2832
|
+
const lo = Math.max(0, Math.floor(Math.min(c0, c1)));
|
|
2833
|
+
const hi = Math.min(columns - 1, Math.floor(Math.max(c0, c1)));
|
|
2834
|
+
const dc = c1 - c0;
|
|
2835
|
+
const valueAt = (column) => {
|
|
2836
|
+
if (dc === 0) return values[i];
|
|
2837
|
+
const f = (column - c0) / dc;
|
|
2838
|
+
return values[i - 1] + f * (values[i] - values[i - 1]);
|
|
2839
|
+
};
|
|
2840
|
+
for (let column = lo; column <= hi; column++) {
|
|
2841
|
+
const from = Math.max(Math.min(c0, c1), column);
|
|
2842
|
+
const to = Math.min(Math.max(c0, c1), column + 1);
|
|
2843
|
+
if (to < from) continue;
|
|
2844
|
+
put(column, valueAt(from));
|
|
2845
|
+
put(column, valueAt(to));
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function computeGradientEnvelope(blocks, startSec, endSec, columns) {
|
|
2850
|
+
const width = Math.max(1, Math.min(MAX_ENVELOPE_COLUMNS, Math.floor(columns)));
|
|
2851
|
+
const channels = {
|
|
2852
|
+
gx: emptyChannel(width),
|
|
2853
|
+
gy: emptyChannel(width),
|
|
2854
|
+
gz: emptyChannel(width)
|
|
2855
|
+
};
|
|
2856
|
+
for (const block of blocks) {
|
|
2857
|
+
for (const key of GRADIENT_CHANNELS) {
|
|
2858
|
+
const grad = block[key];
|
|
2859
|
+
if (!grad || grad.type === "none") continue;
|
|
2860
|
+
accumulate(channels[key], grad, startSec, endSec, width);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
return { startSec, endSec, columns: width, channels };
|
|
2864
|
+
}
|
|
2865
|
+
function countGradientSamples(blocks) {
|
|
2866
|
+
let total = 0;
|
|
2867
|
+
for (const block of blocks) {
|
|
2868
|
+
for (const key of GRADIENT_CHANNELS) {
|
|
2869
|
+
const grad = block[key];
|
|
2870
|
+
if (grad && grad.type !== "none") total += grad.timePoints.length;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
return total;
|
|
2874
|
+
}
|
|
2875
|
+
function packGradientEnvelope(envelope) {
|
|
2876
|
+
const { columns } = envelope;
|
|
2877
|
+
const out = new Float32Array(columns * 6);
|
|
2878
|
+
let cursor = 0;
|
|
2879
|
+
for (const key of GRADIENT_CHANNELS) {
|
|
2880
|
+
const channel = envelope.channels[key];
|
|
2881
|
+
for (let c = 0; c < columns; c++) out[cursor + c] = channel.filled[c] ? channel.min[c] : NaN;
|
|
2882
|
+
cursor += columns;
|
|
2883
|
+
for (let c = 0; c < columns; c++) out[cursor + c] = channel.filled[c] ? channel.max[c] : NaN;
|
|
2884
|
+
cursor += columns;
|
|
2885
|
+
}
|
|
2886
|
+
return { startSec: envelope.startSec, endSec: envelope.endSec, columns, values: out.buffer };
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2720
2889
|
// src/pulseq/physicalGradients.ts
|
|
2721
2890
|
var GRADIENT_ENDPOINT_TOLERANCE_SEC = 1e-12;
|
|
2722
2891
|
function rotateGradient(block, gx, gy, gz) {
|
|
@@ -113,6 +113,243 @@ function deserializeGradientSound(payload){
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/* ── Viewport waveform detail ─────────────────────────────────────────────
|
|
117
|
+
The initial payload is one bounded overview of the whole sequence: every
|
|
118
|
+
waveform is reduced to at most a few hundred points so a large sequence can
|
|
119
|
+
cross the transport at all. That is the right trade for the first paint and
|
|
120
|
+
for wide views, but it is irreversible — zooming into 0.5 ms of a 48 ms
|
|
121
|
+
readout leaves the renderer connecting a handful of surviving extrema, which
|
|
122
|
+
draws as straight polygonal segments instead of the real waveform.
|
|
123
|
+
|
|
124
|
+
So when the view is zoomed far enough that it holds fewer transported
|
|
125
|
+
samples than it has pixels, the host is asked to decode that block range
|
|
126
|
+
again and reduce it against the visible interval instead of the whole event.
|
|
127
|
+
The reply replaces only the blocks it covers, and only while it covers the
|
|
128
|
+
view; everything outside keeps the overview. Requests are debounced so a
|
|
129
|
+
drag issues one decode at rest rather than one per frame, and each reply
|
|
130
|
+
carries the sequence generation it was packed from so a load that lands
|
|
131
|
+
mid-flight cannot paint the previous sequence's samples. */
|
|
132
|
+
|
|
133
|
+
/** Detail currently held: {blocks,startBlock,endBlock,startSec,endSec,generation}. */
|
|
134
|
+
var waveformDetail = null;
|
|
135
|
+
/**
|
|
136
|
+
* Band held for windows carrying more samples than can be drawn one segment
|
|
137
|
+
* each: {startSec,endSec,columns,values,generation}. `values` is six runs of
|
|
138
|
+
* `columns` floats — gxMin, gxMax, gyMin, gyMax, gzMin, gzMax — NaN where a
|
|
139
|
+
* column holds no gradient, so a gap cannot be read as a zero.
|
|
140
|
+
*/
|
|
141
|
+
var waveformBand = null;
|
|
142
|
+
/** Set for one drawBlocks pass when the band, not samples, describes the view. */
|
|
143
|
+
var activeWaveformBand = null;
|
|
144
|
+
/**
|
|
145
|
+
* Narrowest window the host refused as beyond its decode budget. Anything at
|
|
146
|
+
* least this wide would be refused too, so it is not asked for again; zooming
|
|
147
|
+
* in narrows the request and lifts the block on its own.
|
|
148
|
+
*/
|
|
149
|
+
var waveformDetailRefusedSec = Infinity;
|
|
150
|
+
/** Set for the duration of one drawBlocks pass when detail covers the view. */
|
|
151
|
+
var activeWaveformDetail = null;
|
|
152
|
+
var waveformDetailPending = null;
|
|
153
|
+
var waveformDetailTimer = 0;
|
|
154
|
+
var waveformDetailRequestId = 0;
|
|
155
|
+
/** Bumped by the host on every sequence load; stale replies are dropped. */
|
|
156
|
+
var waveformDetailGeneration = 0;
|
|
157
|
+
/**
|
|
158
|
+
* Installed by each host: VS Code posts a message to the extension, the
|
|
159
|
+
* standalone web app packs the range in this same heap. Receives
|
|
160
|
+
* {requestId, generation, startSec, endSec, columns}.
|
|
161
|
+
*/
|
|
162
|
+
var requestWaveformDetailWindow = null;
|
|
163
|
+
/**
|
|
164
|
+
* Installed by each host to surface a detail failure. This bundle's notice
|
|
165
|
+
* state is not what the standalone web app renders, so the message has to go
|
|
166
|
+
* back out to whichever viewer is actually on screen.
|
|
167
|
+
*/
|
|
168
|
+
var waveformDetailNotice = null;
|
|
169
|
+
|
|
170
|
+
function reportWaveformDetail(message){
|
|
171
|
+
if(typeof waveformDetailNotice==='function')waveformDetailNotice(message);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
var WAVEFORM_DETAIL_DEBOUNCE_MS = 160;
|
|
175
|
+
/**
|
|
176
|
+
* Refetch once the held window is this many times wider than the view.
|
|
177
|
+
*
|
|
178
|
+
* Time coverage alone is not enough: a window fetched for a 5 ms view still
|
|
179
|
+
* covers a 0.5 ms view, so without this the detail would freeze at whatever
|
|
180
|
+
* zoom first requested it and never sharpen as the user keeps zooming in. A
|
|
181
|
+
* fresh request pads to 1.5x the view, comfortably inside this bound, so
|
|
182
|
+
* refetching settles in one step instead of oscillating.
|
|
183
|
+
*/
|
|
184
|
+
var WAVEFORM_DETAIL_REFINE_FACTOR = 2;
|
|
185
|
+
/** Mirrors the host's block ceiling so a hopeless request is never sent. */
|
|
186
|
+
var WAVEFORM_DETAIL_BLOCK_LIMIT = 20000;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Discard detail and in-flight requests; call whenever BL is replaced.
|
|
190
|
+
*
|
|
191
|
+
* VS Code passes the extension host's own sequence generation so both sides
|
|
192
|
+
* agree on which load a reply belongs to; the standalone web app owns the
|
|
193
|
+
* counter itself and just advances it.
|
|
194
|
+
*/
|
|
195
|
+
function resetWaveformDetail(generation){
|
|
196
|
+
waveformDetail=null;activeWaveformDetail=null;waveformDetailPending=null;
|
|
197
|
+
waveformBand=null;activeWaveformBand=null;waveformDetailRefusedSec=Infinity;
|
|
198
|
+
clearTimeout(waveformDetailTimer);waveformDetailTimer=0;
|
|
199
|
+
waveformDetailGeneration=(typeof generation==='number'&&isFinite(generation))
|
|
200
|
+
?generation:waveformDetailGeneration+1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function waveformDetailCovers(detail,vs,ve){
|
|
204
|
+
return !!(detail&&detail.generation===waveformDetailGeneration
|
|
205
|
+
&&detail.startSec<=vs+1e-12&&detail.endSec>=ve-1e-12);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Whether a window is not just valid for this view but sharp enough for it.
|
|
210
|
+
* A window far wider than the view spent most of its point budget off screen.
|
|
211
|
+
*/
|
|
212
|
+
function waveformWindowServes(win,vs,ve){
|
|
213
|
+
if(!waveformDetailCovers(win,vs,ve))return false;
|
|
214
|
+
return (win.endSec-win.startSec)<=(ve-vs)*WAVEFORM_DETAIL_REFINE_FACTOR;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The block the renderer should draw for index `bi`. Detail blocks carry the
|
|
219
|
+
* same identity and timing as their overview counterparts and differ only in
|
|
220
|
+
* waveform resolution, so substituting one is transparent to every caller.
|
|
221
|
+
*
|
|
222
|
+
* `blocks` is passed in rather than read from a global because the standalone
|
|
223
|
+
* web app runs its renderer inside an IIFE with its own `BL`; every function
|
|
224
|
+
* this bundle shares with it has to take the state it operates on.
|
|
225
|
+
*/
|
|
226
|
+
function blockAt(blocks,bi){
|
|
227
|
+
var detail=activeWaveformDetail;
|
|
228
|
+
if(detail&&bi>=detail.startBlock&&bi<detail.endBlock){
|
|
229
|
+
var block=detail.blocks[bi-detail.startBlock];
|
|
230
|
+
if(block)return block;
|
|
231
|
+
}
|
|
232
|
+
return blocks[bi];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Decide whether detail applies to this view and, if it would help, ask for it.
|
|
237
|
+
* Returns the detail to draw from, or null to keep the overview.
|
|
238
|
+
*/
|
|
239
|
+
function waveformDetailForView(vs,ve,pixelBudget,startBlock,endBlock){
|
|
240
|
+
// Draw from any detail that covers the view, even while a sharper window is
|
|
241
|
+
// in flight: coarse detail still beats the whole-sequence overview.
|
|
242
|
+
activeWaveformDetail=waveformDetailCovers(waveformDetail,vs,ve)?waveformDetail:null;
|
|
243
|
+
// Anything not already served by exact samples or a band is being drawn from
|
|
244
|
+
// the reduced transport, which is the representation to get away from — so
|
|
245
|
+
// ask, whatever the point count. Held data is refetched once its window is
|
|
246
|
+
// wider than the view deserves. Loops are prevented by the coverage test and
|
|
247
|
+
// by remembering a refusal, not by withholding the request.
|
|
248
|
+
var wants=activeWaveformDetail
|
|
249
|
+
? !waveformWindowServes(waveformDetail,vs,ve)
|
|
250
|
+
: (waveformDetailCovers(waveformBand,vs,ve)
|
|
251
|
+
? !waveformWindowServes(waveformBand,vs,ve)
|
|
252
|
+
: true);
|
|
253
|
+
if(wants&&endBlock-startBlock<=WAVEFORM_DETAIL_BLOCK_LIMIT)
|
|
254
|
+
scheduleWaveformDetail(vs,ve,pixelBudget);
|
|
255
|
+
return activeWaveformDetail;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The band gradients should be drawn from, or null when exact samples are
|
|
260
|
+
* available or the view is beyond what the host will decode. Kept separate
|
|
261
|
+
* from the sample path so a stale band is never drawn beside fresher samples.
|
|
262
|
+
*/
|
|
263
|
+
function waveformBandForView(vs,ve){
|
|
264
|
+
activeWaveformBand=(!activeWaveformDetail&&waveformDetailCovers(waveformBand,vs,ve))
|
|
265
|
+
?waveformBand:null;
|
|
266
|
+
return activeWaveformBand;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** One column's [min,max] for a channel, or null where it holds nothing. */
|
|
270
|
+
function bandColumn(band,channelIndex,column){
|
|
271
|
+
if(!band||column<0||column>=band.columns)return null;
|
|
272
|
+
var base=channelIndex*2*band.columns;
|
|
273
|
+
var lo=band.values[base+column],hi=band.values[base+band.columns+column];
|
|
274
|
+
if(!(lo===lo)||!(hi===hi))return null; // NaN marks an empty column
|
|
275
|
+
return{lo:lo,hi:hi};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function scheduleWaveformDetail(vs,ve,pixelBudget){
|
|
279
|
+
if(typeof requestWaveformDetailWindow!=='function'||!(ve>vs))return;
|
|
280
|
+
var pad=(ve-vs)*0.25,start=Math.max(0,vs-pad),end=ve+pad;
|
|
281
|
+
if(end-start>=waveformDetailRefusedSec)return;
|
|
282
|
+
// Enough columns that the part inside the view still gets about one per
|
|
283
|
+
// pixel once the padding either side is discounted.
|
|
284
|
+
var columns=Math.max(1,Math.ceil((pixelBudget||1)*((end-start)/(ve-vs))));
|
|
285
|
+
// Same adequacy test as the held detail, so an in-flight wide request cannot
|
|
286
|
+
// suppress the narrower one a deeper zoom now needs.
|
|
287
|
+
if(waveformWindowServes(waveformDetailPending,vs,ve))return;
|
|
288
|
+
clearTimeout(waveformDetailTimer);
|
|
289
|
+
waveformDetailTimer=setTimeout(function(){
|
|
290
|
+
waveformDetailPending={startSec:start,endSec:end,generation:waveformDetailGeneration};
|
|
291
|
+
waveformDetailRequestId++;
|
|
292
|
+
try{
|
|
293
|
+
requestWaveformDetailWindow({
|
|
294
|
+
requestId:waveformDetailRequestId,
|
|
295
|
+
generation:waveformDetailGeneration,
|
|
296
|
+
startSec:start,endSec:end,
|
|
297
|
+
columns:columns
|
|
298
|
+
});
|
|
299
|
+
}catch(err){waveformDetailPending=null;
|
|
300
|
+
reportWaveformDetail('Waveform detail was not calculated: '+(err&&err.message||String(err))+'.');}
|
|
301
|
+
},WAVEFORM_DETAIL_DEBOUNCE_MS);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Accept a detail reply. Ignores stale generations and superseded requests. */
|
|
305
|
+
function applyWaveformDetail(payload){
|
|
306
|
+
if(!payload||payload.generation!==waveformDetailGeneration)return false;
|
|
307
|
+
if(payload.requestId!==waveformDetailRequestId)return false;
|
|
308
|
+
waveformDetailPending=null;
|
|
309
|
+
waveformDetail={
|
|
310
|
+
blocks:payload.blocks,startBlock:payload.startBlock,endBlock:payload.endBlock,
|
|
311
|
+
startSec:payload.startSec,endSec:payload.endSec,generation:payload.generation
|
|
312
|
+
};
|
|
313
|
+
reportWaveformDetail(null);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Accept a band for a window too dense to draw sample by sample. */
|
|
318
|
+
function applyWaveformBand(payload){
|
|
319
|
+
if(!payload||payload.generation!==waveformDetailGeneration)return false;
|
|
320
|
+
if(payload.requestId!==waveformDetailRequestId)return false;
|
|
321
|
+
waveformDetailPending=null;
|
|
322
|
+
waveformDetail=null;activeWaveformDetail=null;
|
|
323
|
+
waveformBand={
|
|
324
|
+
startSec:payload.startSec,endSec:payload.endSec,columns:payload.columns,
|
|
325
|
+
values:payload.values,generation:payload.generation
|
|
326
|
+
};
|
|
327
|
+
reportWaveformDetail(null);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Remember that this window exceeds the decode budget and stop asking. */
|
|
332
|
+
function refuseWaveformDetail(payload){
|
|
333
|
+
if(!payload||payload.generation!==waveformDetailGeneration)return false;
|
|
334
|
+
if(payload.requestId!==waveformDetailRequestId)return false;
|
|
335
|
+
waveformDetailPending=null;
|
|
336
|
+
var width=payload.endSec-payload.startSec;
|
|
337
|
+
if(width>0&&width<waveformDetailRefusedSec)waveformDetailRefusedSec=width;
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Record a failed detail request without discarding the overview. Checks the
|
|
343
|
+
* request id as well as the generation so a superseded failure cannot clear a
|
|
344
|
+
* newer in-flight request or leave a notice about a window nobody is viewing.
|
|
345
|
+
*/
|
|
346
|
+
function failWaveformDetail(payload){
|
|
347
|
+
if(!payload||payload.generation!==waveformDetailGeneration)return;
|
|
348
|
+
if(payload.requestId!==waveformDetailRequestId)return;
|
|
349
|
+
waveformDetailPending=null;
|
|
350
|
+
reportWaveformDetail('Waveform detail was not calculated: '+(payload.message||'unknown error')+'.');
|
|
351
|
+
}
|
|
352
|
+
|
|
116
353
|
|
|
117
354
|
/* ══ state.js ══ */
|
|
118
355
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
@@ -144,7 +381,7 @@ var kTraj=null,kAdc=null,kTime=null,kAdcTime=null;
|
|
|
144
381
|
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;
|
|
145
382
|
var viewerNotices={},viewerNoticesCollapsed=readViewerNoticesCollapsed();
|
|
146
383
|
var kspaceSafetyWarning=null,kspaceSafetyBusy=false,kspaceSafetyPopupTimer=0;
|
|
147
|
-
var derivedRenderPointCount=0,derivedEnvelopeCurveCount=0,derivedRawCurveCount=0,waveformOverviewActive=false,rfRenderPointCount=0,rfRawCurveCount=0,rfReducedCurveCount=0,rfOverviewBucketCount=0,lastDrawDurationMs=0,viewerDrawCount=0,viewerCursorDrawCount=0;
|
|
384
|
+
var derivedRenderPointCount=0,derivedEnvelopeCurveCount=0,derivedRawCurveCount=0,waveformOverviewActive=false,rfRenderPointCount=0,rfRawCurveCount=0,rfReducedCurveCount=0,rfOverviewBucketCount=0,gradViewPointCount=0,lastDrawDurationMs=0,viewerDrawCount=0,viewerCursorDrawCount=0;
|
|
148
385
|
var viewerDrawFrame=0,viewerDrawMinimap=false;
|
|
149
386
|
function isMobileSafetyLayout(){return !!(window.matchMedia&&window.matchMedia('(max-width: 768px), (pointer: coarse)').matches);}
|
|
150
387
|
function viewerNoticeMessages(value){
|
|
@@ -245,6 +482,19 @@ var blockPos=[];
|
|
|
245
482
|
/* VS Code API — acquired once, used for postMessage to extension host */
|
|
246
483
|
var vscApi=(typeof acquireVsCodeApi!=='undefined')?acquireVsCodeApi():null;
|
|
247
484
|
|
|
485
|
+
/* Viewport waveform detail crosses the extension boundary; the standalone web
|
|
486
|
+
app installs its own in-heap implementation instead. */
|
|
487
|
+
waveformDetailNotice=function(message){setViewerNotice('waveformDetail',message);};
|
|
488
|
+
if(vscApi)requestWaveformDetailWindow=function(request){
|
|
489
|
+
vscApi.postMessage({
|
|
490
|
+
command:'requestWaveformDetail',
|
|
491
|
+
requestId:request.requestId,
|
|
492
|
+
sequenceGeneration:request.generation,
|
|
493
|
+
startSec:request.startSec,endSec:request.endSec,
|
|
494
|
+
columns:request.columns
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
|
|
248
498
|
function normalizeM1ReferenceMode(mode){return mode==='observationTime'?'observationTime':'rfCenter';}
|
|
249
499
|
function readM1ReferenceMode(){
|
|
250
500
|
try{localStorage.removeItem('seqeyes.m1ReferenceMode');}catch(_){}
|
|
@@ -402,7 +652,7 @@ function applySerializedKspace(payload){
|
|
|
402
652
|
much as the message: the progress overlay reaches "Ready" either way, so
|
|
403
653
|
leaving stale blocks on screen would read as a successful load. */
|
|
404
654
|
function showSequenceLoadFailure(message){
|
|
405
|
-
BL=[];waveformOverview=null;blockPos=[];mmCache=null;
|
|
655
|
+
BL=[];waveformOverview=null;blockPos=[];mmCache=null;resetWaveformDetail();
|
|
406
656
|
setViewerNotice('sequence',message);
|
|
407
657
|
setExportButtonEnabled(false);
|
|
408
658
|
draw();drawMinimap();
|
|
@@ -430,6 +680,7 @@ window.addEventListener('message',function(e){
|
|
|
430
680
|
return;
|
|
431
681
|
}
|
|
432
682
|
if(m.type==='sequenceData'){
|
|
683
|
+
resetWaveformDetail(m.sequenceGeneration);
|
|
433
684
|
try{
|
|
434
685
|
BL=unpackSequenceBlocks(m.blocks,m.sampleTimes,m.sampleValues,m.sampleCount||0);
|
|
435
686
|
}catch(err){
|
|
@@ -456,6 +707,35 @@ window.addEventListener('message',function(e){
|
|
|
456
707
|
setExportButtonEnabled(true);
|
|
457
708
|
SeqEyesPanel.onSequenceLoaded();
|
|
458
709
|
requestAnimationFrame(function(){refreshLayout();draw();drawKs();drawMinimap();});
|
|
710
|
+
}else if(m.type==='waveformDetailData'){
|
|
711
|
+
var detailBlocks;
|
|
712
|
+
try{
|
|
713
|
+
detailBlocks=unpackSequenceBlocks(m.blocks,m.sampleTimes,m.sampleValues,m.sampleCount||0);
|
|
714
|
+
}catch(err){
|
|
715
|
+
failWaveformDetail({requestId:m.requestId,generation:m.sequenceGeneration,message:(err&&err.message||String(err))});
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if(applyWaveformDetail({
|
|
719
|
+
requestId:m.requestId,generation:m.sequenceGeneration,
|
|
720
|
+
startBlock:m.startBlock,endBlock:m.endBlock,
|
|
721
|
+
startSec:m.startSec,endSec:m.endSec,blocks:detailBlocks
|
|
722
|
+
}))draw();
|
|
723
|
+
}else if(m.type==='waveformBandData'){
|
|
724
|
+
var bandValues=asTypedView(m.values,Float32Array);
|
|
725
|
+
if(!bandValues){
|
|
726
|
+
failWaveformDetail({requestId:m.requestId,generation:m.sequenceGeneration,
|
|
727
|
+
message:'the waveform band did not arrive as binary data'});
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if(applyWaveformBand({
|
|
731
|
+
requestId:m.requestId,generation:m.sequenceGeneration,
|
|
732
|
+
startSec:m.startSec,endSec:m.endSec,columns:m.columns,values:bandValues
|
|
733
|
+
}))draw();
|
|
734
|
+
}else if(m.type==='waveformDetailUnavailable'){
|
|
735
|
+
if(refuseWaveformDetail({requestId:m.requestId,generation:m.sequenceGeneration,
|
|
736
|
+
startSec:m.startSec,endSec:m.endSec}))draw();
|
|
737
|
+
}else if(m.type==='waveformDetailError'){
|
|
738
|
+
failWaveformDetail({requestId:m.requestId,generation:m.sequenceGeneration,message:m.message});
|
|
459
739
|
}else if(m.type==='loadError'){
|
|
460
740
|
showSequenceLoadFailure(m.message||'The sequence could not be loaded.');
|
|
461
741
|
}else if(m.type==='kspaceData'){
|
|
@@ -1424,7 +1704,7 @@ var panelMarkerTimeSec=NaN;
|
|
|
1424
1704
|
Main draw loop
|
|
1425
1705
|
═══════════════════════════════════════════════════════════════════════ */
|
|
1426
1706
|
function draw(){
|
|
1427
|
-
var drawStarted=performance.now();derivedRenderPointCount=0;derivedEnvelopeCurveCount=0;derivedRawCurveCount=0;rfRenderPointCount=0;rfRawCurveCount=0;rfReducedCurveCount=0;rfOverviewBucketCount=0;viewerDrawCount++;
|
|
1707
|
+
var drawStarted=performance.now();derivedRenderPointCount=0;derivedEnvelopeCurveCount=0;derivedRawCurveCount=0;rfRenderPointCount=0;rfRawCurveCount=0;rfReducedCurveCount=0;rfOverviewBucketCount=0;gradViewPointCount=0;viewerDrawCount++;
|
|
1428
1708
|
var w=mc.width/(window.devicePixelRatio||1),h=mc.height/(window.devicePixelRatio||1);
|
|
1429
1709
|
var s=getComputedStyle(document.body);
|
|
1430
1710
|
ctx.clearRect(0,0,w,h);
|
|
@@ -1714,18 +1994,37 @@ function drawBlocks(vs,ve,s){
|
|
|
1714
1994
|
var pixelBudget=Math.max(1,Math.floor(plotWidth()));
|
|
1715
1995
|
var overview=selectWaveformOverview(waveformOverview,range.start,range.end,pixelBudget);
|
|
1716
1996
|
function useOverview(key){return !!overview&&waveformVisiblePointCount(waveformOverview,key,range.start,range.end)>pixelBudget;}
|
|
1717
|
-
|
|
1997
|
+
var gradVisible={gx:waveformVisibleGradientPointCount(BL,'gx',range.start,range.end,vs,ve),
|
|
1998
|
+
gy:waveformVisibleGradientPointCount(BL,'gy',range.start,range.end,vs,ve),
|
|
1999
|
+
gz:waveformVisibleGradientPointCount(BL,'gz',range.start,range.end,vs,ve)};
|
|
2000
|
+
function useGradientOverview(key){return !!overview&&gradVisible[key]>pixelBudget*8;}
|
|
1718
2001
|
var rfPoints=waveformVisiblePointCount(waveformOverview,'rf',range.start,range.end),rfEvents=waveformVisiblePointCount(waveformOverview,'rfEvents',range.start,range.end);
|
|
1719
2002
|
var aggregateRf=!!overview&&rfEvents>pixelBudget*2,reduceRf=aggregateRf||rfPoints>pixelBudget*8;
|
|
1720
2003
|
var overviewUse={rf:reduceRf,phase:useOverview('phase'),gx:useGradientOverview('gx'),gy:useGradientOverview('gy'),gz:useGradientOverview('gz'),adc:useOverview('adc')};
|
|
1721
2004
|
var dense=overviewUse.rf||overviewUse.phase||overviewUse.gx||overviewUse.gy||overviewUse.gz||overviewUse.adc;
|
|
1722
2005
|
waveformOverviewActive=dense;
|
|
1723
2006
|
setViewerNotice('dense',dense?'Dense overview mode is active. Zoom in for full waveform detail.':null);
|
|
2007
|
+
// Raw level draws whatever the transport delivered, so this is where an
|
|
2008
|
+
// over-reduced view has to be refilled with samples clipped to the viewport.
|
|
2009
|
+
// Keyed on the waveform rows specifically: phase or ADC switching to the
|
|
2010
|
+
// overview says nothing about the gradients, which may still be drawing
|
|
2011
|
+
// transported samples and still be the thing that looks wrong.
|
|
2012
|
+
var rawWaveformRows=!(overviewUse.gx&&overviewUse.gy&&overviewUse.gz&&reduceRf);
|
|
2013
|
+
if(rawWaveformRows)waveformDetailForView(vs,ve,pixelBudget,range.start,range.end);
|
|
2014
|
+
else activeWaveformDetail=null;
|
|
1724
2015
|
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);}
|
|
1725
2016
|
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);}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2017
|
+
// Band first: where it covers the view it is the accurate representation, and
|
|
2018
|
+
// the block polyline would be drawing an already-reduced transport instead.
|
|
2019
|
+
var band=waveformBandForView(vs,ve);
|
|
2020
|
+
var gradKeys=['gx','gy','gz'];
|
|
2021
|
+
for(var gi=0;gi<3;gi++){
|
|
2022
|
+
var row=rows[2+gi],key=gradKeys[gi];
|
|
2023
|
+
if(row<0)continue;
|
|
2024
|
+
if(band)drawGradientBand(band,gi,key,row,2+gi,ch,colors[key],vs,ve);
|
|
2025
|
+
else if(overviewUse[key])drawGradientOverview(overview,key,row,2+gi,ch,colors[key],vs,ve);
|
|
2026
|
+
else drawGradientBlocks(range.start,range.end,key,row,2+gi,ch,colors[key],vs,ve);
|
|
2027
|
+
}
|
|
1729
2028
|
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);}
|
|
1730
2029
|
if(rows[6]>=0&&range.end-range.start<=pixelBudget)drawTriggerBlocks(range.start,range.end,rows[6],ch,colors,vs,ve);
|
|
1731
2030
|
}
|
|
@@ -1830,7 +2129,7 @@ function drawRfBlocks(start,end,vi,ch,colors,vs,ve,maxPoints){
|
|
|
1830
2129
|
rowClip(vi,ch,function(){
|
|
1831
2130
|
var visibleEvents=[];
|
|
1832
2131
|
for(var bi=start;bi<end;bi++){
|
|
1833
|
-
var rf=BL
|
|
2132
|
+
var rf=blockAt(BL,bi).rf;if(!rf||rf.s+rf.d<vs||rf.s>ve)continue;
|
|
1834
2133
|
visibleEvents.push(rf);
|
|
1835
2134
|
}
|
|
1836
2135
|
var pointBudget=Math.max(4,Math.floor((maxPoints||Infinity)/Math.max(1,visibleEvents.length)));
|
|
@@ -1855,7 +2154,7 @@ function drawPhaseBlocks(start,end,vi,ch,colors,vs,ve){
|
|
|
1855
2154
|
rowClip(vi,ch,function(){
|
|
1856
2155
|
ctx.strokeStyle=colors.rf;ctx.lineWidth=.8;ctx.beginPath();var hasRf=false;
|
|
1857
2156
|
for(var bi=start;bi<end;bi++){
|
|
1858
|
-
var rf=BL
|
|
2157
|
+
var rf=blockAt(BL,bi).rf,phaseTime=rf&&(rf.pt||rf.t);if(!rf||!rf.p||!phaseTime||rf.s+rf.d<vs||rf.s>ve)continue;
|
|
1859
2158
|
var n=Math.min(phaseTime.length,rf.p.length);
|
|
1860
2159
|
for(var i=0;i<n;i++){
|
|
1861
2160
|
var sx=t2x(phaseTime[i]),sy=y+ch*.45-rf.p[i]*scale;
|
|
@@ -1885,18 +2184,51 @@ function drawPhaseBlocks(start,end,vi,ch,colors,vs,ve){
|
|
|
1885
2184
|
});
|
|
1886
2185
|
}
|
|
1887
2186
|
|
|
2187
|
+
/**
|
|
2188
|
+
* Draw one gradient channel as a min/max band.
|
|
2189
|
+
*
|
|
2190
|
+
* Used where the view holds more samples than pixels: each column is filled
|
|
2191
|
+
* between the extremes the waveform actually reached while crossing it, so the
|
|
2192
|
+
* shape stays truthful without asserting a path between samples that are not
|
|
2193
|
+
* adjacent. Empty columns break the shape rather than being drawn at zero.
|
|
2194
|
+
*/
|
|
2195
|
+
function drawGradientBand(band,channelIndex,key,vi,ci,ch,color,vs,ve){
|
|
2196
|
+
var y=cy(vi),scale=ch*.4/channelRange(ci);
|
|
2197
|
+
var span=band.endSec-band.startSec;if(!(span>0))return;
|
|
2198
|
+
var perColumn=span/band.columns;
|
|
2199
|
+
var first=Math.max(0,Math.floor((vs-band.startSec)/perColumn));
|
|
2200
|
+
var last=Math.min(band.columns-1,Math.ceil((ve-band.startSec)/perColumn));
|
|
2201
|
+
rowClip(vi,ch,function(){
|
|
2202
|
+
ctx.fillStyle=color;ctx.globalAlpha=.55;ctx.beginPath();var drew=false;
|
|
2203
|
+
for(var c=first;c<=last;c++){
|
|
2204
|
+
var col=bandColumn(band,channelIndex,c);if(!col)continue;
|
|
2205
|
+
var x0=t2x(band.startSec+c*perColumn),x1=t2x(band.startSec+(c+1)*perColumn);
|
|
2206
|
+
var w=Math.max(x1-x0,.75);
|
|
2207
|
+
var yTop=y-col.hi*scale,yBottom=y-col.lo*scale;
|
|
2208
|
+
// A flat run would be an invisible zero-height rectangle.
|
|
2209
|
+
ctx.rect(x0,yTop,w,Math.max(yBottom-yTop,.75));drew=true;
|
|
2210
|
+
gradViewPointCount++;
|
|
2211
|
+
}
|
|
2212
|
+
if(drew)ctx.fill();
|
|
2213
|
+
ctx.globalAlpha=1;
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
|
|
1888
2217
|
function drawGradientBlocks(start,end,key,vi,ci,ch,color,vs,ve){
|
|
1889
2218
|
var y=cy(vi),scale=ch*.4/channelRange(ci);
|
|
1890
2219
|
rowClip(vi,ch,function(){
|
|
1891
2220
|
ctx.strokeStyle=color;ctx.lineWidth=1;ctx.beginPath();var hasPath=false;
|
|
1892
2221
|
for(var bi=start;bi<end;bi++){
|
|
1893
|
-
var g=BL
|
|
2222
|
+
var g=blockAt(BL,bi)[key];if(!g||g.ty==='none'||!g.t||!g.w||g.t.length<2)continue;
|
|
1894
2223
|
var n=Math.min(g.t.length,g.w.length);
|
|
1895
2224
|
if(n<2)continue;
|
|
1896
2225
|
if(g.t[n-1]<vs||g.t[0]>ve)continue;
|
|
1897
2226
|
for(var i=0;i<n;i++){
|
|
1898
2227
|
var sx=t2x(g.t[i]),sy=y-g.w[i]*scale;
|
|
1899
2228
|
if(i===0)ctx.moveTo(sx,sy);else ctx.lineTo(sx,sy);
|
|
2229
|
+
// Counted per visible point, not per event: an over-reduced waveform
|
|
2230
|
+
// still draws a whole long event, it just has almost nothing in view.
|
|
2231
|
+
if(g.t[i]>=vs&&g.t[i]<=ve)gradViewPointCount++;
|
|
1900
2232
|
}
|
|
1901
2233
|
hasPath=true;
|
|
1902
2234
|
}
|
|
@@ -4823,7 +5155,16 @@ var SeqEyesPanel = (function () {
|
|
|
4823
5155
|
if (e.key === 'Escape' && isFinite(markerTimeSec)) { setMarkerTime(NaN); syncControls(); }
|
|
4824
5156
|
});
|
|
4825
5157
|
document.addEventListener('visibilitychange', function () {
|
|
4826
|
-
if (document.hidden)
|
|
5158
|
+
if (document.hidden) {
|
|
5159
|
+
// Web Audio can keep rendering in a background tab, but animation frames
|
|
5160
|
+
// cannot. Preserve the audition and suspend only its visual updates.
|
|
5161
|
+
stopPlayheadLoop();
|
|
5162
|
+
return;
|
|
5163
|
+
}
|
|
5164
|
+
if (panelMode !== 'spectrogram') return;
|
|
5165
|
+
if (SeqEyesAudio.isPlaying()) startPlayheadLoop();
|
|
5166
|
+
syncTransport();
|
|
5167
|
+
render();
|
|
4827
5168
|
});
|
|
4828
5169
|
window.addEventListener('pagehide', function () { SeqEyesAudio.dispose(); });
|
|
4829
5170
|
window.addEventListener('resize', function () { if (panelMode === 'spectrogram') resize(); });
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|