arrayview 0.12.1__tar.gz → 0.12.2__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.
- {arrayview-0.12.1 → arrayview-0.12.2}/.claude/skills/viewer-ui-checklist/SKILL.md +10 -2
- {arrayview-0.12.1 → arrayview-0.12.2}/AGENTS.md +2 -1
- {arrayview-0.12.1 → arrayview-0.12.2}/PKG-INFO +1 -1
- {arrayview-0.12.1 → arrayview-0.12.2}/pyproject.toml +1 -1
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_stdio_server.py +6 -1
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_viewer.html +279 -97
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_vscode.py +71 -1
- arrayview-0.12.2/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/uv.lock +1 -1
- {arrayview-0.12.1 → arrayview-0.12.2}/vscode-extension/extension.js +28 -0
- arrayview-0.12.1/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/.claude/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/.claude/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/.github/workflows/docs.yml +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/.gitignore +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/.python-version +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/CONTRIBUTING.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/LICENSE +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/README.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/comparing.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/configuration.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/display.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/index.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/loading.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/logo.png +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/measurement.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/remote.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/docs/viewing.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/matlab/arrayview.m +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/mkdocs.yml +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/plans/webview/LOG.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/scripts/demo.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/scripts/release.sh +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/__init__.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/__main__.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_app.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_config.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_icon.png +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_io.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_platform.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_render.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_server.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_session.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_shell.html +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/src/arrayview/_torch.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/conftest.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_api.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_browser.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_cli.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_command_reachability.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_config.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_interactions.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_large_arrays.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/test_torch.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/ui_audit.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/tests/visual_smoke.py +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/vscode-extension/LICENSE +0 -0
- {arrayview-0.12.1 → arrayview-0.12.2}/vscode-extension/package.json +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: viewer-ui-checklist
|
|
3
|
-
description: Use when adding keyboard shortcuts, changing layout, or making any UI change to arrayview. Ensures visual_smoke.py
|
|
3
|
+
description: Use when adding keyboard shortcuts, changing layout, or making any UI change to arrayview. Ensures visual_smoke.py, help overlay, and README stay in sync.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# ArrayView UI Checklist
|
|
7
7
|
|
|
8
8
|
## Rule
|
|
9
9
|
|
|
10
|
-
Every UI change to arrayview MUST be reflected in `tests/visual_smoke.py` before the task is complete.
|
|
10
|
+
Every UI change to arrayview MUST be reflected in `tests/visual_smoke.py`, the help overlay, and (when user-facing) `README.md` before the task is complete.
|
|
11
11
|
|
|
12
12
|
## What counts as a UI change
|
|
13
13
|
|
|
@@ -35,6 +35,14 @@ Every UI change to arrayview MUST be reflected in `tests/visual_smoke.py` before
|
|
|
35
35
|
|
|
36
36
|
4. **Open and review** the new screenshots in `tests/smoke_output/`
|
|
37
37
|
|
|
38
|
+
5. **Update `GUIDE_TABS`** in `_viewer.html` if you added, removed, or changed a keybinding
|
|
39
|
+
- Add/update the entry in the appropriate tab and section
|
|
40
|
+
- Include a `hint` for non-obvious shortcuts
|
|
41
|
+
- Use the `docs-style` skill for formatting rules
|
|
42
|
+
|
|
43
|
+
6. **Update `README.md`** if the change is user-facing (new CLI flag, new API, new mode)
|
|
44
|
+
- Use the `docs-style` skill for formatting rules
|
|
45
|
+
|
|
38
46
|
## Red flags — STOP
|
|
39
47
|
|
|
40
48
|
- "The shortcut is too simple to need a smoke test" → ALL shortcuts need entries
|
|
@@ -15,7 +15,8 @@ Load the relevant skill before touching the corresponding area.
|
|
|
15
15
|
| `frontend-designer` | Styling/layout changes to `_viewer.html` |
|
|
16
16
|
| `vscode-simplebrowser` | Extension, signal-file IPC, `_VSCODE_EXT_VERSION` |
|
|
17
17
|
| `invocation-consistency` | Server startup, display-opening, env detection |
|
|
18
|
-
| `
|
|
18
|
+
| `viewer-ui-checklist` | Any UI change (smoke tests, help overlay, README sync) |
|
|
19
|
+
| `docs-style` | README, help overlay, docstrings (formatting rules) |
|
|
19
20
|
|
|
20
21
|
## Non-Negotiables
|
|
21
22
|
|
|
@@ -637,7 +637,7 @@ def _handle_get_viewer_html(msg: dict) -> None:
|
|
|
637
637
|
"""Return the rendered viewer HTML with template substitutions."""
|
|
638
638
|
from importlib.resources import files as _pkg_files
|
|
639
639
|
|
|
640
|
-
from arrayview._config import get_viewer_colormaps
|
|
640
|
+
from arrayview._config import get_viewer_colormaps, get_viewer_theme
|
|
641
641
|
from arrayview._render import COLORMAP_GRADIENT_STOPS, COMPLEX_MODES, REAL_MODES
|
|
642
642
|
from arrayview._session import COLORMAPS
|
|
643
643
|
|
|
@@ -650,12 +650,17 @@ def _handle_get_viewer_html(msg: dict) -> None:
|
|
|
650
650
|
|
|
651
651
|
query_val = json.dumps(f"?sid={sid}&transport=postMessage") if sid else "null"
|
|
652
652
|
|
|
653
|
+
_theme_names = ["dark", "light", "solarized", "nord"]
|
|
654
|
+
_cfg_theme = get_viewer_theme()
|
|
655
|
+
_default_theme_idx = _theme_names.index(_cfg_theme) if _cfg_theme in _theme_names else 0
|
|
656
|
+
|
|
653
657
|
html = (
|
|
654
658
|
template.replace("__COLORMAPS__", str(_active_colormaps))
|
|
655
659
|
.replace("__COLORMAP_GRADIENT_STOPS__", json.dumps(COLORMAP_GRADIENT_STOPS))
|
|
656
660
|
.replace("__COMPLEX_MODES__", str(COMPLEX_MODES))
|
|
657
661
|
.replace("__REAL_MODES__", str(REAL_MODES))
|
|
658
662
|
.replace("__ARRAYVIEW_QUERY__", query_val)
|
|
663
|
+
.replace("__DEFAULT_THEME_IDX__", str(_default_theme_idx))
|
|
659
664
|
)
|
|
660
665
|
|
|
661
666
|
_write_json({"html": html})
|
|
@@ -535,14 +535,7 @@
|
|
|
535
535
|
width: min(820px, 92vw); height: min(520px, 86vh);
|
|
536
536
|
overflow: hidden;
|
|
537
537
|
}
|
|
538
|
-
.help-
|
|
539
|
-
display: flex; justify-content: space-between; align-items: center;
|
|
540
|
-
margin-bottom: 14px; flex-shrink: 0;
|
|
541
|
-
}
|
|
542
|
-
.help-mode-hint {
|
|
543
|
-
font-size: 11px; color: var(--muted); letter-spacing: 0.04em;
|
|
544
|
-
}
|
|
545
|
-
.help-mode-hint kbd, .help-footer kbd {
|
|
538
|
+
.help-footer kbd {
|
|
546
539
|
display: inline-block; border: 1px solid var(--border);
|
|
547
540
|
border-radius: 4px; padding: 0px 5px;
|
|
548
541
|
font-family: 'SF Mono', ui-monospace, monospace;
|
|
@@ -579,13 +572,23 @@
|
|
|
579
572
|
margin-bottom: 8px; padding-bottom: 4px;
|
|
580
573
|
border-bottom: 1px solid var(--border-subtle);
|
|
581
574
|
}
|
|
575
|
+
.help-section-title:not(:first-child) { margin-top: 16px; }
|
|
576
|
+
.help-subsection-title {
|
|
577
|
+
font-size: 12px; font-weight: 600; color: var(--text);
|
|
578
|
+
margin-top: 18px; margin-bottom: 4px;
|
|
579
|
+
}
|
|
580
|
+
.help-subsection-title:first-child { margin-top: 0; }
|
|
581
|
+
.help-subsection-desc {
|
|
582
|
+
color: var(--muted); font-size: 11.5px;
|
|
583
|
+
line-height: 1.6; margin-bottom: 10px;
|
|
584
|
+
}
|
|
582
585
|
.help-section { margin-bottom: 18px; }
|
|
583
586
|
.help-columns { display: flex; gap: 28px; }
|
|
584
587
|
.help-column { flex: 1; min-width: 0; }
|
|
585
588
|
.help-entry { margin-bottom: 8px; }
|
|
586
589
|
.help-entry-row { display: flex; align-items: baseline; }
|
|
587
590
|
.help-entry-key {
|
|
588
|
-
flex-shrink: 0; width:
|
|
591
|
+
flex-shrink: 0; width: 100px;
|
|
589
592
|
color: var(--help-key); font-weight: 600;
|
|
590
593
|
font-family: 'SF Mono', ui-monospace, 'Cascadia Code', monospace;
|
|
591
594
|
font-size: 12px;
|
|
@@ -593,15 +596,14 @@
|
|
|
593
596
|
.help-entry-action { color: var(--text); font-size: 12.5px; }
|
|
594
597
|
.help-entry-hint {
|
|
595
598
|
color: var(--muted); font-size: 11px;
|
|
596
|
-
margin-left:
|
|
599
|
+
margin-left: 100px; margin-top: 1px;
|
|
597
600
|
}
|
|
598
601
|
.help-footer {
|
|
599
602
|
flex-shrink: 0; border-top: 1px solid var(--border-subtle);
|
|
600
603
|
margin-top: 14px; padding-top: 10px;
|
|
601
|
-
display: flex; justify-content:
|
|
604
|
+
display: flex; justify-content: flex-end; align-items: center;
|
|
602
605
|
font-size: 11px; color: var(--muted); letter-spacing: 0.04em;
|
|
603
606
|
}
|
|
604
|
-
.help-footer-nav kbd { margin-right: 2px; }
|
|
605
607
|
.help-footer-link {
|
|
606
608
|
color: var(--muted); text-decoration: none;
|
|
607
609
|
font-size: 11px; letter-spacing: 0.04em;
|
|
@@ -1501,14 +1503,10 @@
|
|
|
1501
1503
|
</div>
|
|
1502
1504
|
<div id="help-overlay">
|
|
1503
1505
|
<div id="help-box">
|
|
1504
|
-
<div class="help-topbar">
|
|
1505
|
-
<span class="help-mode-hint">press <kbd>?</kbd> to close</span>
|
|
1506
|
-
</div>
|
|
1507
1506
|
<div id="help-tabs"></div>
|
|
1508
1507
|
<div id="help-content"></div>
|
|
1509
1508
|
<div class="help-footer">
|
|
1510
|
-
<
|
|
1511
|
-
<a class="help-footer-link" href="https://github.com/brainhack-school2024/arrayview#keybindings" target="_blank" rel="noopener">Full reference →</a>
|
|
1509
|
+
<a class="help-footer-link" href="https://oscarvanderheide.github.io/arrayview/" target="_blank" rel="noopener">Full reference →</a>
|
|
1512
1510
|
</div>
|
|
1513
1511
|
</div>
|
|
1514
1512
|
</div>
|
|
@@ -1749,7 +1747,8 @@
|
|
|
1749
1747
|
let lebesgueMode = false; // Lebesgue integral: highlight pixels matching hovered bin
|
|
1750
1748
|
let _lebesgueSlice = null; // Float32Array of raw 2-D slice values (row-major)
|
|
1751
1749
|
let _lebesgueVersion = null; // version key for cached _lebesgueSlice
|
|
1752
|
-
let
|
|
1750
|
+
let _lebesgueFetchCtrl = null; // AbortController for in-flight Lebesgue fetch
|
|
1751
|
+
let _hoverPrefetchTimer = null; // debounce for hover prefetch during rapid scrolling
|
|
1753
1752
|
let _lastLebesgueBin = -1; // tracks which histogram bin is currently highlighted
|
|
1754
1753
|
let _lebesgueSliceW = 0, _lebesgueSliceH = 0; // dimensions of cached raw slice
|
|
1755
1754
|
let rgbMode = false;
|
|
@@ -5035,6 +5034,16 @@
|
|
|
5035
5034
|
}
|
|
5036
5035
|
}
|
|
5037
5036
|
|
|
5037
|
+
/** Debounced prefetch — safe to call on every wheel / key event. */
|
|
5038
|
+
function _debouncedHoverPrefetch() {
|
|
5039
|
+
if (!_pixelInfoVisible && !lebesgueMode) return;
|
|
5040
|
+
clearTimeout(_hoverPrefetchTimer);
|
|
5041
|
+
_hoverPrefetchTimer = setTimeout(() => {
|
|
5042
|
+
_fetchLebesgueSlice();
|
|
5043
|
+
if (_pixelInfoVisible) _prefetchHoverSlices();
|
|
5044
|
+
}, 80);
|
|
5045
|
+
}
|
|
5046
|
+
|
|
5038
5047
|
async function _fetchLebesgueSliceForSid(fetchSid, fetchDimX, fetchDimY) {
|
|
5039
5048
|
const params = new URLSearchParams({
|
|
5040
5049
|
dim_x: fetchDimX, dim_y: fetchDimY,
|
|
@@ -5058,32 +5067,37 @@
|
|
|
5058
5067
|
}
|
|
5059
5068
|
|
|
5060
5069
|
async function _fetchLebesgueSlice() {
|
|
5061
|
-
if (!sid || !indices.length
|
|
5062
|
-
|
|
5063
|
-
if (
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
_lebesgueSlice = entry.slice;
|
|
5079
|
-
_lebesgueSliceW = entry.w;
|
|
5080
|
-
_lebesgueSliceH = entry.h;
|
|
5081
|
-
_lebesgueVersion = entry.version;
|
|
5070
|
+
if (!sid || !indices.length) return;
|
|
5071
|
+
// Abort any in-flight fetch so rapid slice changes always fetch the latest
|
|
5072
|
+
if (_lebesgueFetchCtrl) _lebesgueFetchCtrl.abort();
|
|
5073
|
+
const ctrl = new AbortController();
|
|
5074
|
+
_lebesgueFetchCtrl = ctrl;
|
|
5075
|
+
try {
|
|
5076
|
+
if (compareActive) {
|
|
5077
|
+
const sids = getCompareRenderSids();
|
|
5078
|
+
await Promise.all(sids.map(s => _fetchLebesgueSliceForSid(s, dim_x, dim_y)));
|
|
5079
|
+
if (ctrl.signal.aborted) return;
|
|
5080
|
+
const first = _lebesguePerSession[sids[0]];
|
|
5081
|
+
if (first) {
|
|
5082
|
+
_lebesgueSlice = first.slice;
|
|
5083
|
+
_lebesgueSliceW = first.w;
|
|
5084
|
+
_lebesgueSliceH = first.h;
|
|
5085
|
+
_lebesgueVersion = first.version;
|
|
5086
|
+
}
|
|
5082
5087
|
} else {
|
|
5083
|
-
|
|
5088
|
+
const entry = await _fetchLebesgueSliceForSid(sid, dim_x, dim_y);
|
|
5089
|
+
if (ctrl.signal.aborted) return;
|
|
5090
|
+
if (entry) {
|
|
5091
|
+
_lebesgueSlice = entry.slice;
|
|
5092
|
+
_lebesgueSliceW = entry.w;
|
|
5093
|
+
_lebesgueSliceH = entry.h;
|
|
5094
|
+
_lebesgueVersion = entry.version;
|
|
5095
|
+
} else {
|
|
5096
|
+
_lebesgueSlice = null;
|
|
5097
|
+
}
|
|
5084
5098
|
}
|
|
5085
|
-
}
|
|
5086
|
-
|
|
5099
|
+
} catch (_) { /* aborted or network error */ }
|
|
5100
|
+
if (_lebesgueFetchCtrl === ctrl) _lebesgueFetchCtrl = null;
|
|
5087
5101
|
}
|
|
5088
5102
|
|
|
5089
5103
|
function _clearLebesgueOverlay() {
|
|
@@ -7142,14 +7156,18 @@
|
|
|
7142
7156
|
renderInfo();
|
|
7143
7157
|
// Invalidate cached histogram data so next colorbar hover re-fetches
|
|
7144
7158
|
_histDataVersion = null;
|
|
7145
|
-
//
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7159
|
+
// Lebesgue / hover caches are version-keyed, so stale entries are
|
|
7160
|
+
// never served for the wrong slice. Don't eagerly clear — the old
|
|
7161
|
+
// data remains harmless while the async re-fetch is in flight, and
|
|
7162
|
+
// clearing creates a window where hover falls to the slow /pixel path.
|
|
7163
|
+
// Cap cache size to prevent unbounded memory growth.
|
|
7164
|
+
const _hscKeys = Object.keys(_hoverSliceCache);
|
|
7165
|
+
if (_hscKeys.length > 64) _hscKeys.slice(0, _hscKeys.length - 32).forEach(k => delete _hoverSliceCache[k]);
|
|
7166
|
+
const _lpsKeys = Object.keys(_lebesguePerSession);
|
|
7167
|
+
if (_lpsKeys.length > 32) _lpsKeys.slice(0, _lpsKeys.length - 16).forEach(k => delete _lebesguePerSession[k]);
|
|
7149
7168
|
if (lebesgueMode || _pixelInfoVisible) {
|
|
7150
7169
|
if (lebesgueMode) _clearLebesgueOverlay();
|
|
7151
|
-
|
|
7152
|
-
if (_pixelInfoVisible) _prefetchHoverSlices();
|
|
7170
|
+
_debouncedHoverPrefetch();
|
|
7153
7171
|
}
|
|
7154
7172
|
if (compareQmriActive) {
|
|
7155
7173
|
compareQmriViews.forEach(v => compareQmriRender(v));
|
|
@@ -9232,19 +9250,53 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
9232
9250
|
},
|
|
9233
9251
|
'overlay.cycleVisibility': {
|
|
9234
9252
|
title: 'Cycle overlay visibility',
|
|
9235
|
-
when: [
|
|
9253
|
+
when: [],
|
|
9236
9254
|
run: (ctx, e) => {
|
|
9255
|
+
const hasOv = !!overlay_sid;
|
|
9256
|
+
const hasVf = hasVectorfield;
|
|
9257
|
+
if (!hasOv && !hasVf) return;
|
|
9258
|
+
|
|
9259
|
+
// Helper: show or hide vector field arrows
|
|
9260
|
+
const setVfield = (show) => {
|
|
9261
|
+
if (!hasVf) return;
|
|
9262
|
+
vfieldVisible = show;
|
|
9263
|
+
if (!show) {
|
|
9264
|
+
if (_vfieldAbort) { _vfieldAbort.abort(); _vfieldAbort = null; }
|
|
9265
|
+
vfieldCanvas.style.display = 'none';
|
|
9266
|
+
if (multiViewActive) {
|
|
9267
|
+
_mvVfieldAborts.forEach(ac => ac.abort());
|
|
9268
|
+
_mvVfieldAborts.clear();
|
|
9269
|
+
_mvVfieldCache.clear();
|
|
9270
|
+
document.querySelectorAll('.mv-vfield-overlay').forEach(el => el.style.display = 'none');
|
|
9271
|
+
}
|
|
9272
|
+
} else {
|
|
9273
|
+
if (multiViewActive) mvDrawAllVectorOverlays();
|
|
9274
|
+
else drawVectorOverlay();
|
|
9275
|
+
}
|
|
9276
|
+
};
|
|
9277
|
+
|
|
9278
|
+
// Vector field only (no masks): simple toggle
|
|
9279
|
+
if (!hasOv) {
|
|
9280
|
+
setVfield(!vfieldVisible);
|
|
9281
|
+
showStatus(`arrows: ${vfieldVisible ? 'on' : 'off'}`);
|
|
9282
|
+
saveState();
|
|
9283
|
+
return;
|
|
9284
|
+
}
|
|
9285
|
+
|
|
9237
9286
|
// Cycle overlay visibility: all → none → mask0 → mask1 → ... → all
|
|
9238
9287
|
const sids = overlay_sid.split(',');
|
|
9239
9288
|
const nMasks = sids.length;
|
|
9240
9289
|
if (_overlayVisibility === 'all') {
|
|
9241
9290
|
_overlayVisibility = 'none';
|
|
9291
|
+
setVfield(false);
|
|
9242
9292
|
} else if (_overlayVisibility === 'none') {
|
|
9243
9293
|
_overlayVisibility = nMasks > 1 ? 0 : 'all';
|
|
9294
|
+
if (_overlayVisibility === 'all') setVfield(true);
|
|
9244
9295
|
} else {
|
|
9245
9296
|
// Currently showing individual mask
|
|
9246
9297
|
const next = _overlayVisibility + 1;
|
|
9247
9298
|
_overlayVisibility = next >= nMasks ? 'all' : next;
|
|
9299
|
+
if (_overlayVisibility === 'all') setVfield(true);
|
|
9248
9300
|
}
|
|
9249
9301
|
// Status message
|
|
9250
9302
|
if (_overlayVisibility === 'all') {
|
|
@@ -9258,6 +9310,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
9258
9310
|
showStatus(`overlay ${_overlayVisibility + 1}/${nMasks}${colorTag}`);
|
|
9259
9311
|
}
|
|
9260
9312
|
updateView();
|
|
9313
|
+
saveState();
|
|
9261
9314
|
},
|
|
9262
9315
|
},
|
|
9263
9316
|
'overlay.openPicker': {
|
|
@@ -9874,7 +9927,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
9874
9927
|
when: [],
|
|
9875
9928
|
run: (ctx, e) => {
|
|
9876
9929
|
if (!hasVectorfield) {
|
|
9877
|
-
showStatus('
|
|
9930
|
+
showStatus('no vector field loaded');
|
|
9878
9931
|
return;
|
|
9879
9932
|
} // kept inline: emits toast
|
|
9880
9933
|
vfieldVisible = !vfieldVisible;
|
|
@@ -10038,6 +10091,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10038
10091
|
v.rendering = false; v.pending = false; v.seq++;
|
|
10039
10092
|
});
|
|
10040
10093
|
mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
|
|
10094
|
+
_debouncedHoverPrefetch();
|
|
10041
10095
|
showStatus('origin reset');
|
|
10042
10096
|
},
|
|
10043
10097
|
},
|
|
@@ -10185,6 +10239,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10185
10239
|
indices[activeDim] = Math.min(shape[activeDim] - 1, indices[activeDim] + 1);
|
|
10186
10240
|
renderInfo();
|
|
10187
10241
|
mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
|
|
10242
|
+
_debouncedHoverPrefetch();
|
|
10188
10243
|
saveState();
|
|
10189
10244
|
} else if (activeDim === dim_x) {
|
|
10190
10245
|
flip_x = !flip_x;
|
|
@@ -10215,6 +10270,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10215
10270
|
indices[activeDim] = Math.max(0, indices[activeDim] - 1);
|
|
10216
10271
|
renderInfo();
|
|
10217
10272
|
mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
|
|
10273
|
+
_debouncedHoverPrefetch();
|
|
10218
10274
|
saveState();
|
|
10219
10275
|
} else if (activeDim === dim_x) {
|
|
10220
10276
|
flip_x = !flip_x;
|
|
@@ -10337,7 +10393,6 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10337
10393
|
// Order matters: seg.exit (when segMode) takes precedence over slice.cancelJump
|
|
10338
10394
|
{ key: 'Escape', command: 'seg.exit' },
|
|
10339
10395
|
{ key: 'Escape', command: 'slice.cancelJump' },
|
|
10340
|
-
{ key: 'U', command: 'vfield.toggleArrows' },
|
|
10341
10396
|
{ key: 'u', command: 'ruler.toggle' },
|
|
10342
10397
|
];
|
|
10343
10398
|
|
|
@@ -10362,97 +10417,189 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10362
10417
|
const GUIDE_TABS = [
|
|
10363
10418
|
{
|
|
10364
10419
|
id: 'basics', label: 'Basics',
|
|
10365
|
-
desc: 'The essentials for
|
|
10420
|
+
desc: 'The essentials for exploring any array. Move through slices, zoom into details, or play through a volume as an animation. These controls work everywhere.',
|
|
10421
|
+
columns: 2,
|
|
10422
|
+
sections: [
|
|
10423
|
+
{
|
|
10424
|
+
title: 'Slices & Dimensions',
|
|
10425
|
+
entries: [
|
|
10426
|
+
{ key: '← → ↑ ↓', action: 'Step through slices' },
|
|
10427
|
+
{ key: 'x / y', action: 'Change viewing dimension', hint: 'choose which axes map to horizontal and vertical' },
|
|
10428
|
+
{ key: 'z', action: 'Mosaic — tile all slices in a grid' },
|
|
10429
|
+
{ key: '1–9 Enter', action: 'Jump to a specific slice number' },
|
|
10430
|
+
]
|
|
10431
|
+
},
|
|
10432
|
+
{
|
|
10433
|
+
title: 'Playback',
|
|
10434
|
+
entries: [
|
|
10435
|
+
{ key: 'Space', action: 'Play / pause animation' },
|
|
10436
|
+
{ key: '< >', action: 'Slower / faster playback' },
|
|
10437
|
+
]
|
|
10438
|
+
},
|
|
10439
|
+
{
|
|
10440
|
+
title: 'Info & Search',
|
|
10441
|
+
entries: [
|
|
10442
|
+
{ key: 'i', action: 'Toggle hover pixel info', hint: 'shows coordinates and value under cursor' },
|
|
10443
|
+
{ key: 'I', action: 'Info panel', hint: 'array shape, dtype, memory layout' },
|
|
10444
|
+
{ key: '/', action: 'Command palette', hint: 'search all available commands' },
|
|
10445
|
+
]
|
|
10446
|
+
},
|
|
10447
|
+
{
|
|
10448
|
+
title: 'Zoom & Pan', columnBreak: true,
|
|
10449
|
+
entries: [
|
|
10450
|
+
{ key: 'Pinch / ⌘Scroll', action: 'Zoom in / out', hint: 'Ctrl+Scroll on Windows / Linux' },
|
|
10451
|
+
{ key: '0', action: 'Reset zoom to fit' },
|
|
10452
|
+
{ key: 'Drag', action: 'Pan the image (when zoomed in)' },
|
|
10453
|
+
{ key: 'F', action: 'Zen mode — hide UI chrome', hint: 'strips away panels and controls to focus on the image' },
|
|
10454
|
+
]
|
|
10455
|
+
},
|
|
10456
|
+
{
|
|
10457
|
+
title: 'Export',
|
|
10458
|
+
entries: [
|
|
10459
|
+
{ key: 's', action: 'Save screenshot' },
|
|
10460
|
+
{ key: 'g', action: 'Save GIF animation' },
|
|
10461
|
+
{ key: 'e', action: 'Copy shareable URL' },
|
|
10462
|
+
]
|
|
10463
|
+
}
|
|
10464
|
+
]
|
|
10465
|
+
},
|
|
10466
|
+
{
|
|
10467
|
+
id: 'display', label: 'Display',
|
|
10468
|
+
desc: 'Control how your data looks. Adjust colors and the display range to bring out features that aren\u2019t visible with default settings.',
|
|
10366
10469
|
columns: 2,
|
|
10367
10470
|
sections: [
|
|
10368
10471
|
{
|
|
10369
|
-
title: '
|
|
10472
|
+
title: 'Color & Range',
|
|
10473
|
+
entries: [
|
|
10474
|
+
{ key: 'c', action: 'Cycle colormap', hint: 'grayscale, viridis, turbo, and more' },
|
|
10475
|
+
{ key: 'd', action: 'Cycle display range presets', hint: 'clips outliers — press repeatedly to tighten' },
|
|
10476
|
+
{ key: 'L', action: 'Toggle log scale', hint: 'when values span several orders of magnitude' },
|
|
10477
|
+
{ key: 'T', action: 'Cycle theme', hint: 'dark, light, solarized, nord' },
|
|
10478
|
+
{ key: 'b', action: 'Toggle canvas borders' },
|
|
10479
|
+
]
|
|
10480
|
+
},
|
|
10481
|
+
{
|
|
10482
|
+
title: 'Colorbar',
|
|
10370
10483
|
entries: [
|
|
10371
|
-
{ key: '
|
|
10372
|
-
{ key: '
|
|
10373
|
-
{ key: '
|
|
10374
|
-
{ key: '
|
|
10375
|
-
{ key: '
|
|
10376
|
-
{ key: 'space', action: 'play / pause', hint: '< > adjust playback speed' },
|
|
10484
|
+
{ key: 'Scroll', action: 'Narrow or widen the display range' },
|
|
10485
|
+
{ key: 'Drag handles', action: 'Adjust min / max bounds' },
|
|
10486
|
+
{ key: 'Drag bar', action: 'Shift the visible range up or down' },
|
|
10487
|
+
{ key: 'Dbl-click', action: 'Expand histogram' },
|
|
10488
|
+
{ key: 'Dbl-click label', action: 'Type an exact min or max value' },
|
|
10377
10489
|
]
|
|
10378
10490
|
},
|
|
10379
10491
|
{
|
|
10380
|
-
title: '
|
|
10492
|
+
title: 'Transforms', columnBreak: true,
|
|
10381
10493
|
entries: [
|
|
10382
|
-
{ key: '
|
|
10383
|
-
{ key: '
|
|
10384
|
-
{ key: '
|
|
10385
|
-
{ key: 'I', action: 'info panel', hint: 'array shape, dtype, memory layout' },
|
|
10386
|
-
{ key: '/', action: 'command palette', hint: 'search all commands' },
|
|
10494
|
+
{ key: 'f', action: 'FFT — view frequency domain', hint: 'press again to return to spatial domain' },
|
|
10495
|
+
{ key: 'p', action: 'Cycle projections', hint: 'MAX → MIN → MEAN → STD → off' },
|
|
10496
|
+
{ key: 'm', action: 'Complex mode cycle', hint: 'magnitude → phase → real → imaginary' },
|
|
10387
10497
|
]
|
|
10388
10498
|
}
|
|
10389
10499
|
]
|
|
10390
10500
|
},
|
|
10391
10501
|
{
|
|
10392
10502
|
id: 'ortho', label: 'Ortho View',
|
|
10393
|
-
desc: 'Three synchronized panels showing
|
|
10503
|
+
desc: 'Three synchronized panels showing your volume from three directions at once. A crosshair links the views — clicking in one panel updates the other two. Great for exploring 3D data like MRI scans, where you need to see axial, sagittal, and coronal slices together.',
|
|
10394
10504
|
sections: [
|
|
10395
10505
|
{
|
|
10506
|
+
title: 'Controls',
|
|
10396
10507
|
entries: [
|
|
10397
|
-
{ key: '
|
|
10398
|
-
{ key: 'Click', action: 'Set crosshair position', hint: '
|
|
10399
|
-
{ key: '
|
|
10400
|
-
{ key: '
|
|
10401
|
-
{ key: '
|
|
10508
|
+
{ key: 'v', action: 'Enter or exit ortho view' },
|
|
10509
|
+
{ key: 'Click / Drag', action: 'Set crosshair position', hint: 'click any panel to jump all views to that point, or drag to scrub' },
|
|
10510
|
+
{ key: 'Drag lines', action: 'Reposition slice planes', hint: 'each colored line corresponds to a slice in another panel' },
|
|
10511
|
+
{ key: 'p', action: 'Maximum intensity projection (MIP)', hint: '3D volume rendering — drag to rotate' },
|
|
10512
|
+
{ key: 'o', action: 'Reset to origin' },
|
|
10402
10513
|
]
|
|
10403
10514
|
}
|
|
10404
10515
|
]
|
|
10405
10516
|
},
|
|
10406
10517
|
{
|
|
10407
|
-
id: '
|
|
10408
|
-
desc: '
|
|
10518
|
+
id: 'compare', label: 'Compare',
|
|
10519
|
+
desc: 'View two or more arrays side by side, blend them together, or compute a difference map. Useful for checking registration, comparing model outputs, or inspecting before-and-after changes. Open multiple arrays with the file picker first.',
|
|
10409
10520
|
sections: [
|
|
10410
10521
|
{
|
|
10522
|
+
title: 'Layout',
|
|
10411
10523
|
entries: [
|
|
10412
|
-
{ key: '
|
|
10413
|
-
{ key: 'Drag', action: '
|
|
10414
|
-
|
|
10415
|
-
|
|
10524
|
+
{ key: 'G', action: 'Cycle layout', hint: 'horizontal → vertical → grid' },
|
|
10525
|
+
{ key: 'Drag title', action: 'Reorder panes by dragging their title bar' },
|
|
10526
|
+
]
|
|
10527
|
+
},
|
|
10528
|
+
{
|
|
10529
|
+
title: 'Operations (requires exactly 2 arrays)',
|
|
10530
|
+
entries: [
|
|
10531
|
+
{ key: 'X', action: 'Cycle compare operation', hint: 'off → A−B → |A−B| → relative → overlay → wipe → flicker → checkerboard' },
|
|
10532
|
+
{ key: '[ ]', action: 'Adjust operation parameter', hint: 'blend amount, flicker speed, or checkerboard tile size' },
|
|
10416
10533
|
]
|
|
10417
10534
|
}
|
|
10418
10535
|
]
|
|
10419
10536
|
},
|
|
10420
10537
|
{
|
|
10421
|
-
id: '
|
|
10422
|
-
desc: '
|
|
10538
|
+
id: 'tools', label: 'Tools',
|
|
10539
|
+
desc: 'Draw on the image to measure, segment, or crop. Each tool uses click or drag to define a region, and shows results in real time.',
|
|
10423
10540
|
sections: [
|
|
10424
10541
|
{
|
|
10542
|
+
subsection: 'ROI — Region of Interest',
|
|
10543
|
+
subdesc: 'Draw a region to see live statistics (mean, std, min, max). The histogram updates to show only the selected area.',
|
|
10544
|
+
entries: [
|
|
10545
|
+
{ key: 'R', action: 'Toggle ROI mode' },
|
|
10546
|
+
{ key: 'r', action: 'Cycle shape', hint: 'rectangle → circle → freehand → flood fill' },
|
|
10547
|
+
{ key: 'Drag', action: 'Draw ROI region' },
|
|
10548
|
+
{ key: 'Right-click', action: 'Delete ROI under cursor' },
|
|
10549
|
+
{ key: 'Esc', action: 'Clear all and exit' },
|
|
10550
|
+
]
|
|
10551
|
+
},
|
|
10552
|
+
{
|
|
10553
|
+
subsection: 'Ruler',
|
|
10554
|
+
subdesc: 'Click two points to measure pixel distance.',
|
|
10555
|
+
entries: [
|
|
10556
|
+
{ key: 'u', action: 'Toggle ruler mode' },
|
|
10557
|
+
]
|
|
10558
|
+
},
|
|
10559
|
+
{
|
|
10560
|
+
subsection: 'Segmentation',
|
|
10561
|
+
subdesc: 'Interactive segmentation with nnInteractive. Click to add or exclude regions from the label.',
|
|
10425
10562
|
entries: [
|
|
10426
|
-
{ key: 'S', action: '
|
|
10427
|
-
{ key: '
|
|
10428
|
-
{ key: 'Click', action: '
|
|
10429
|
-
{ key: 'Esc', action: 'Exit segmentation
|
|
10563
|
+
{ key: 'S', action: 'Toggle segmentation mode' },
|
|
10564
|
+
{ key: 'Click', action: 'Add region (positive prompt)' },
|
|
10565
|
+
{ key: 'Shift+Click', action: 'Exclude region (negative prompt)' },
|
|
10566
|
+
{ key: 'Esc', action: 'Exit segmentation' },
|
|
10430
10567
|
]
|
|
10431
10568
|
}
|
|
10432
10569
|
]
|
|
10433
10570
|
},
|
|
10434
10571
|
{
|
|
10435
|
-
id: '
|
|
10436
|
-
desc: '
|
|
10572
|
+
id: 'overlays', label: 'Overlays',
|
|
10573
|
+
desc: 'Extra layers displayed on top of your array. Load segmentation masks with --overlay or deformation fields with --vectorfield when launching arrayview. Press O to toggle visibility for all overlays at once.',
|
|
10437
10574
|
sections: [
|
|
10438
10575
|
{
|
|
10576
|
+
subsection: 'Segmentation Masks',
|
|
10577
|
+
subdesc: 'Binary masks rendered as colored layers on the image. Load one or more with --overlay mask1.nii,mask2.nii. Each mask gets its own color.',
|
|
10578
|
+
entries: [
|
|
10579
|
+
{ key: 'O', action: 'Cycle visibility', hint: 'all → none → individual masks (if multiple)' },
|
|
10580
|
+
{ key: '[ ]', action: 'Adjust opacity' },
|
|
10581
|
+
]
|
|
10582
|
+
},
|
|
10583
|
+
{
|
|
10584
|
+
subsection: 'Vector Field',
|
|
10585
|
+
subdesc: 'Deformation or flow fields rendered as arrows. Load with --vectorfield field.nii.',
|
|
10439
10586
|
entries: [
|
|
10440
|
-
{ key: '
|
|
10441
|
-
{ key: '
|
|
10442
|
-
{ key: '
|
|
10587
|
+
{ key: 'O', action: 'Toggle visibility' },
|
|
10588
|
+
{ key: '[ ]', action: 'Adjust arrow density' },
|
|
10589
|
+
{ key: '{ }', action: 'Adjust arrow length' },
|
|
10443
10590
|
]
|
|
10444
10591
|
}
|
|
10445
10592
|
]
|
|
10446
10593
|
},
|
|
10447
10594
|
{
|
|
10448
10595
|
id: 'qmri', label: 'qMRI',
|
|
10449
|
-
desc: '
|
|
10596
|
+
desc: 'For quantitative MRI parameter maps — T1, T2, FA, and others. Voxel values are shown with proper units and the display adjusts to tissue-specific intensity ranges. Use this when viewing parameter maps to get meaningful readouts instead of raw numbers.',
|
|
10450
10597
|
sections: [
|
|
10451
10598
|
{
|
|
10599
|
+
title: 'Controls',
|
|
10452
10600
|
entries: [
|
|
10453
|
-
{ key: 'q', action: '
|
|
10454
|
-
{ key: '
|
|
10455
|
-
{ key: 'R', action: 'Cycle range preset', hint: 'Tissue-specific intensity ranges for each parameter' },
|
|
10601
|
+
{ key: 'q', action: 'Enter / cycle / exit qMRI mode', hint: 'steps through available parameter types' },
|
|
10602
|
+
{ key: 'z', action: 'Toggle qMRI mosaic', hint: 'see all parameter maps side by side in a grid' },
|
|
10456
10603
|
]
|
|
10457
10604
|
}
|
|
10458
10605
|
]
|
|
@@ -10491,10 +10638,37 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10491
10638
|
const wrapper = document.createElement('div');
|
|
10492
10639
|
if (tab.columns === 2) wrapper.className = 'help-columns';
|
|
10493
10640
|
|
|
10641
|
+
let col = null; // current column container
|
|
10494
10642
|
for (const section of tab.sections) {
|
|
10495
|
-
|
|
10496
|
-
if (tab.columns === 2)
|
|
10643
|
+
// Start a new column if 2-column layout and this is the first section or a columnBreak
|
|
10644
|
+
if (tab.columns === 2) {
|
|
10645
|
+
if (!col || section.columnBreak) {
|
|
10646
|
+
col = document.createElement('div');
|
|
10647
|
+
col.className = 'help-column';
|
|
10648
|
+
wrapper.appendChild(col);
|
|
10649
|
+
}
|
|
10650
|
+
} else {
|
|
10651
|
+
if (!col) {
|
|
10652
|
+
col = wrapper; // single-column: append directly to wrapper
|
|
10653
|
+
}
|
|
10654
|
+
}
|
|
10655
|
+
|
|
10656
|
+
// Subsection header (bold title + description paragraph)
|
|
10657
|
+
if (section.subsection) {
|
|
10658
|
+
const subTitle = document.createElement('div');
|
|
10659
|
+
subTitle.className = 'help-subsection-title';
|
|
10660
|
+
subTitle.textContent = section.subsection;
|
|
10661
|
+
col.appendChild(subTitle);
|
|
10497
10662
|
|
|
10663
|
+
if (section.subdesc) {
|
|
10664
|
+
const subDesc = document.createElement('div');
|
|
10665
|
+
subDesc.className = 'help-subsection-desc';
|
|
10666
|
+
subDesc.textContent = section.subdesc;
|
|
10667
|
+
col.appendChild(subDesc);
|
|
10668
|
+
}
|
|
10669
|
+
}
|
|
10670
|
+
|
|
10671
|
+
// Section title (uppercase label with divider)
|
|
10498
10672
|
if (section.title) {
|
|
10499
10673
|
const titleEl = document.createElement('div');
|
|
10500
10674
|
titleEl.className = 'help-section-title';
|
|
@@ -10530,8 +10704,6 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10530
10704
|
|
|
10531
10705
|
col.appendChild(entryEl);
|
|
10532
10706
|
}
|
|
10533
|
-
|
|
10534
|
-
wrapper.appendChild(col);
|
|
10535
10707
|
}
|
|
10536
10708
|
|
|
10537
10709
|
panel.appendChild(wrapper);
|
|
@@ -11577,6 +11749,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
11577
11749
|
activeDim = view.sliceDir;
|
|
11578
11750
|
renderInfo();
|
|
11579
11751
|
mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
|
|
11752
|
+
_debouncedHoverPrefetch();
|
|
11580
11753
|
saveState();
|
|
11581
11754
|
}, { passive: false });
|
|
11582
11755
|
cv.addEventListener('mousedown', (e) => {
|
|
@@ -12319,6 +12492,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
12319
12492
|
indices[current_slice_dim] = Math.max(0, Math.min(shape[current_slice_dim] - 1, indices[current_slice_dim] + delta));
|
|
12320
12493
|
renderInfo();
|
|
12321
12494
|
qmriViews.forEach(v => qvRender(v));
|
|
12495
|
+
_debouncedHoverPrefetch();
|
|
12322
12496
|
saveState();
|
|
12323
12497
|
}, { passive: false });
|
|
12324
12498
|
cv.addEventListener('mousemove', (e) => {
|
|
@@ -12538,6 +12712,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
12538
12712
|
indices[current_slice_dim] = Math.max(0, Math.min(shape[current_slice_dim] - 1, indices[current_slice_dim] + delta));
|
|
12539
12713
|
renderInfo();
|
|
12540
12714
|
compareQmriViews.forEach(v => compareQmriRender(v));
|
|
12715
|
+
_debouncedHoverPrefetch();
|
|
12541
12716
|
saveState();
|
|
12542
12717
|
}, { passive: false });
|
|
12543
12718
|
cv.addEventListener('mousemove', (e) => {
|
|
@@ -12891,6 +13066,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
12891
13066
|
indices[view.sliceDir] = Math.max(0, Math.min(shape[view.sliceDir] - 1, indices[view.sliceDir] + delta));
|
|
12892
13067
|
renderInfo();
|
|
12893
13068
|
compareMvViews.forEach(v => compareMvRender(v));
|
|
13069
|
+
_debouncedHoverPrefetch();
|
|
12894
13070
|
saveState();
|
|
12895
13071
|
}, { passive: false });
|
|
12896
13072
|
cv.addEventListener('mousedown', (e) => {
|
|
@@ -12995,6 +13171,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
12995
13171
|
fy ? shape[view.dimY] - 1 - Math.floor(py) : Math.floor(py)));
|
|
12996
13172
|
renderInfo();
|
|
12997
13173
|
compareMvViews.forEach(v => { compareMvDrawFrame(v); compareMvRender(v); });
|
|
13174
|
+
_debouncedHoverPrefetch();
|
|
12998
13175
|
}
|
|
12999
13176
|
|
|
13000
13177
|
function exitCompareMv() {
|
|
@@ -13641,6 +13818,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
13641
13818
|
renderInfo();
|
|
13642
13819
|
mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
|
|
13643
13820
|
}
|
|
13821
|
+
_debouncedHoverPrefetch();
|
|
13644
13822
|
}
|
|
13645
13823
|
|
|
13646
13824
|
async function mvRenderOblique(v) {
|
|
@@ -14333,7 +14511,8 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
14333
14511
|
else if (compareMvActive) compareMvViews.forEach(v => compareMvRender(v));
|
|
14334
14512
|
else if (multiViewActive) mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
|
|
14335
14513
|
else if (qmriActive) qmriViews.forEach(v => qvRender(v));
|
|
14336
|
-
else updateView();
|
|
14514
|
+
else { updateView(); return; }
|
|
14515
|
+
_debouncedHoverPrefetch();
|
|
14337
14516
|
}
|
|
14338
14517
|
});
|
|
14339
14518
|
document.addEventListener('mouseup', (e) => {
|
|
@@ -14388,7 +14567,10 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
14388
14567
|
qmriViews.forEach(v => qvRender(v));
|
|
14389
14568
|
} else {
|
|
14390
14569
|
updateView();
|
|
14570
|
+
saveState();
|
|
14571
|
+
return;
|
|
14391
14572
|
}
|
|
14573
|
+
_debouncedHoverPrefetch();
|
|
14392
14574
|
saveState();
|
|
14393
14575
|
}, { passive: false });
|
|
14394
14576
|
|
|
@@ -733,11 +733,48 @@ def _write_vscode_signal(payload: dict, delay: float = 0.0, skip_compat: bool =
|
|
|
733
733
|
uses_pid = _reg_data.get("fallbackId", False)
|
|
734
734
|
except Exception:
|
|
735
735
|
uses_pid = env_wid.isdigit()
|
|
736
|
+
|
|
737
|
+
# Guard against stale ARRAYVIEW_WINDOW_ID: when the extension
|
|
738
|
+
# host restarts (e.g. tunnel reconnection), the old process may
|
|
739
|
+
# still be alive but is no longer connected to a VS Code client.
|
|
740
|
+
# Detect this by looking for a newer registration from the same
|
|
741
|
+
# server (same first ppid — the tunnel/server parent process).
|
|
742
|
+
_env_ts = _reg_data.get("ts", 0)
|
|
743
|
+
_env_ppids = _reg_data.get("ppids", [])
|
|
744
|
+
try:
|
|
745
|
+
for _fname in os.listdir(signal_dir):
|
|
746
|
+
if not (_fname.startswith("window-") and _fname.endswith(".json")):
|
|
747
|
+
continue
|
|
748
|
+
_other_wid = _fname[7:-5]
|
|
749
|
+
if _other_wid == env_wid:
|
|
750
|
+
continue
|
|
751
|
+
with open(os.path.join(signal_dir, _fname)) as _f:
|
|
752
|
+
_other = json.load(_f)
|
|
753
|
+
_other_ts = _other.get("ts", 0)
|
|
754
|
+
_other_ppids = _other.get("ppids", [])
|
|
755
|
+
if (
|
|
756
|
+
_other_ts > _env_ts
|
|
757
|
+
and len(_env_ppids) >= 1
|
|
758
|
+
and len(_other_ppids) >= 1
|
|
759
|
+
and _env_ppids[0] == _other_ppids[0]
|
|
760
|
+
):
|
|
761
|
+
_vprint(
|
|
762
|
+
f"[ArrayView] signal: ARRAYVIEW_WINDOW_ID={env_wid} is stale "
|
|
763
|
+
f"(newer registration {_other_wid} found), redirecting",
|
|
764
|
+
flush=True,
|
|
765
|
+
)
|
|
766
|
+
env_wid = _other_wid
|
|
767
|
+
_reg_data = _other
|
|
768
|
+
uses_pid = _other.get("fallbackId", False)
|
|
769
|
+
_env_ts = _other_ts
|
|
770
|
+
except Exception:
|
|
771
|
+
pass
|
|
772
|
+
|
|
736
773
|
_prefix = "pid" if uses_pid else "ipc"
|
|
737
774
|
filenames = (f"open-request-{_prefix}-{env_wid}.json",)
|
|
738
775
|
targeted_via_env = True
|
|
739
776
|
_vprint(
|
|
740
|
-
f"[ArrayView] signal: ARRAYVIEW_WINDOW_ID
|
|
777
|
+
f"[ArrayView] signal: ARRAYVIEW_WINDOW_ID → {filenames[0]}",
|
|
741
778
|
flush=True,
|
|
742
779
|
)
|
|
743
780
|
|
|
@@ -868,6 +905,39 @@ def _write_vscode_signal(payload: dict, delay: float = 0.0, skip_compat: bool =
|
|
|
868
905
|
_uses_pid = _reg_data.get("fallbackId", False)
|
|
869
906
|
except Exception:
|
|
870
907
|
_uses_pid = env_wid.isdigit()
|
|
908
|
+
|
|
909
|
+
# Same stale-env guard as the primary check above.
|
|
910
|
+
_env_ts = _reg_data.get("ts", 0)
|
|
911
|
+
_env_ppids = _reg_data.get("ppids", [])
|
|
912
|
+
try:
|
|
913
|
+
for _fname in os.listdir(signal_dir):
|
|
914
|
+
if not (_fname.startswith("window-") and _fname.endswith(".json")):
|
|
915
|
+
continue
|
|
916
|
+
_other_wid = _fname[7:-5]
|
|
917
|
+
if _other_wid == env_wid:
|
|
918
|
+
continue
|
|
919
|
+
with open(os.path.join(signal_dir, _fname)) as _f:
|
|
920
|
+
_other = json.load(_f)
|
|
921
|
+
_other_ts = _other.get("ts", 0)
|
|
922
|
+
_other_ppids = _other.get("ppids", [])
|
|
923
|
+
if (
|
|
924
|
+
_other_ts > _env_ts
|
|
925
|
+
and len(_env_ppids) >= 1
|
|
926
|
+
and len(_other_ppids) >= 1
|
|
927
|
+
and _env_ppids[0] == _other_ppids[0]
|
|
928
|
+
):
|
|
929
|
+
_vprint(
|
|
930
|
+
f"[ArrayView] signal: remote env {env_wid} is stale "
|
|
931
|
+
f"(newer {_other_wid}), redirecting",
|
|
932
|
+
flush=True,
|
|
933
|
+
)
|
|
934
|
+
env_wid = _other_wid
|
|
935
|
+
_reg_data = _other
|
|
936
|
+
_uses_pid = _other.get("fallbackId", False)
|
|
937
|
+
_env_ts = _other_ts
|
|
938
|
+
except Exception:
|
|
939
|
+
pass
|
|
940
|
+
|
|
871
941
|
_prefix = "pid" if _uses_pid else "ipc"
|
|
872
942
|
_vprint(f"[ArrayView] signal: env window match → {_prefix}-{env_wid}", flush=True)
|
|
873
943
|
filenames = (f"open-request-{_prefix}-{env_wid}.json",)
|
|
Binary file
|
|
@@ -824,6 +824,34 @@ function activate(context) {
|
|
|
824
824
|
|
|
825
825
|
cleanupStaleFiles();
|
|
826
826
|
|
|
827
|
+
// Clean up stale registrations from previous tunnel sessions.
|
|
828
|
+
// When the extension host restarts (e.g. tunnel reconnect), the old
|
|
829
|
+
// process may still be alive but is no longer connected to a VS Code
|
|
830
|
+
// client. Detect by finding registrations with the same first ppid
|
|
831
|
+
// (same server/tunnel) but an older timestamp.
|
|
832
|
+
if (EXT_PPIDS.length >= 1) {
|
|
833
|
+
try {
|
|
834
|
+
const now = Date.now();
|
|
835
|
+
for (const f of fs.readdirSync(SIGNAL_DIR)) {
|
|
836
|
+
if (!f.startsWith('window-') || !f.endsWith('.json')) continue;
|
|
837
|
+
const wid = f.slice(7, -5);
|
|
838
|
+
if (wid === windowId) continue;
|
|
839
|
+
try {
|
|
840
|
+
const data = JSON.parse(fs.readFileSync(path.join(SIGNAL_DIR, f), 'utf8'));
|
|
841
|
+
const sameTunnel = data.ppids && data.ppids.length >= 1 &&
|
|
842
|
+
data.ppids[0] === EXT_PPIDS[0];
|
|
843
|
+
if (sameTunnel && data.ts && data.ts < now - 5000) {
|
|
844
|
+
fs.unlinkSync(path.join(SIGNAL_DIR, f));
|
|
845
|
+
log(`CLEANUP: removed stale same-tunnel registration ${f} (ts=${data.ts})`);
|
|
846
|
+
// Also remove any stale signal files targeting that window
|
|
847
|
+
const prefix = data.fallbackId ? 'pid' : 'ipc';
|
|
848
|
+
try { fs.unlinkSync(path.join(SIGNAL_DIR, `open-request-${prefix}-${wid}.json`)); } catch (_) {}
|
|
849
|
+
}
|
|
850
|
+
} catch (_) {}
|
|
851
|
+
}
|
|
852
|
+
} catch (_) {}
|
|
853
|
+
}
|
|
854
|
+
|
|
827
855
|
void tryOpenSignalFile();
|
|
828
856
|
|
|
829
857
|
const interval = setInterval(() => void tryOpenSignalFile(), 1000);
|
|
Binary file
|
|
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
|
|
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
|
|
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
|
|
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
|