arrayview 0.8.0__tar.gz → 0.10.0__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.8.0 → arrayview-0.10.0}/.gitignore +3 -1
- {arrayview-0.8.0 → arrayview-0.10.0}/AGENTS.md +3 -15
- {arrayview-0.8.0 → arrayview-0.10.0}/PKG-INFO +5 -2
- {arrayview-0.8.0 → arrayview-0.10.0}/README.md +4 -1
- {arrayview-0.8.0 → arrayview-0.10.0}/pyproject.toml +1 -1
- arrayview-0.10.0/scripts/release.sh +78 -0
- arrayview-0.10.0/src/arrayview/ARCHITECTURE.md +197 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_app.py +1 -2
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_launcher.py +26 -11
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_render.py +8 -25
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_server.py +293 -46
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_session.py +13 -84
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_shell.html +8 -1
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_stdio_server.py +88 -2
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_viewer.html +1377 -947
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_vscode.py +31 -28
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_api.py +59 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_browser.py +7 -2
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/visual_smoke.py +3 -1
- {arrayview-0.8.0 → arrayview-0.10.0}/uv.lock +1 -1
- {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/.python-version +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/.tmp-vsix/extension/extension.js +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/.tmp-vsix/extension/package.json +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/LICENSE +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/comparing.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/configuration.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/display.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/index.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/loading.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/logo.png +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/measurement.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/remote.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/docs/viewing.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/matlab/arrayview.m +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/mkdocs.yml +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/scripts/demo.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_io.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/conftest.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_cli.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_config.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_interactions.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/test_torch.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/tests/ui_audit.py +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/vscode-extension/LICENSE +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/vscode-extension/extension.js +0 -0
- {arrayview-0.8.0 → arrayview-0.10.0}/vscode-extension/package.json +0 -0
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Interactive viewer for multi-dimensional arrays and medical/scientific volumes. FastAPI server + single-file HTML/JS frontend (`_viewer.html`), displayed via native window, browser, VS Code Simple Browser, or Jupyter iframe.
|
|
1
|
+
Read `src/arrayview/ARCHITECTURE.md` for codebase orientation.
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
| File | Role |
|
|
8
|
-
|------|------|
|
|
9
|
-
| `_launcher.py` | Entry points, process management, display routing |
|
|
10
|
-
| `_server.py` | FastAPI app, REST/WebSocket, HTML templates |
|
|
11
|
-
| `_session.py` | Sessions, state, caches, render thread |
|
|
12
|
-
| `_render.py` | Colormaps, LUTs, slice extraction, RGBA |
|
|
13
|
-
| `_viewer.html` | All frontend (JS/CSS embedded, single file) |
|
|
14
|
-
| `_vscode.py` | Extension management, signal-file IPC |
|
|
15
|
-
| `_stdio_server.py` | Stdio transport for direct webview mode |
|
|
16
|
-
| `_segmentation.py` | nnInteractive segmentation client |
|
|
3
|
+
# ArrayView
|
|
17
4
|
|
|
18
5
|
## Skills
|
|
19
6
|
|
|
@@ -36,6 +23,7 @@ Load the relevant skill before touching the corresponding area.
|
|
|
36
23
|
- Do not regress working display paths when fixing another
|
|
37
24
|
- For visual/animation features, propose 2-3 options BEFORE implementing
|
|
38
25
|
- UI visibility changes go through reconcilers (`_reconcileUI`/`_reconcileLayout`/`_reconcileCompareState`/`_reconcileCbVisibility`), not inline `style.display` or `classList` toggles in mode functions
|
|
26
|
+
- All colorbar state (animation, window/level, hover, drag) flows through `primaryCb` ColorBar instance — never read/write legacy globals. Multiview colorbars sync via `primaryCb`.
|
|
39
27
|
|
|
40
28
|
## Execution
|
|
41
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arrayview
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Fast multi-dimensional array viewer
|
|
5
5
|
Project-URL: Home, https://github.com/oscarvanderheide/arrayview
|
|
6
6
|
Project-URL: Source, https://github.com/oscarvanderheide/arrayview
|
|
@@ -103,7 +103,10 @@ for epoch in range(100):
|
|
|
103
103
|
|
|
104
104
|
## Once open
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
**Navigation:** scroll slices · `h`/`l` cycle dims · `j`/`k` slices · `=`/`-` zoom · drag pan
|
|
107
|
+
**Views:** `v` 3-plane · `z` mosaic · `q` qMRI · `n` compare · `=` immersive
|
|
108
|
+
**Display:** `c`/`C` colormaps · `d`/`D` dynamic range · `f` FFT · `m` complex · `p` projections · `L` log
|
|
109
|
+
**Tools:** `S` segmentation · `u` ruler · `s` screenshot · `?` help
|
|
107
110
|
|
|
108
111
|
|
|
109
112
|
## nnInteractive Segmentation
|
|
@@ -68,7 +68,10 @@ for epoch in range(100):
|
|
|
68
68
|
|
|
69
69
|
## Once open
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
**Navigation:** scroll slices · `h`/`l` cycle dims · `j`/`k` slices · `=`/`-` zoom · drag pan
|
|
72
|
+
**Views:** `v` 3-plane · `z` mosaic · `q` qMRI · `n` compare · `=` immersive
|
|
73
|
+
**Display:** `c`/`C` colormaps · `d`/`D` dynamic range · `f` FFT · `m` complex · `p` projections · `L` log
|
|
74
|
+
**Tools:** `S` segmentation · `u` ruler · `s` screenshot · `?` help
|
|
72
75
|
|
|
73
76
|
|
|
74
77
|
## nnInteractive Segmentation
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# ---------------------------------------------------------------------------
|
|
5
|
+
# release.sh — bump version, commit, tag, push, create GitHub release
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
BUMP="minor"
|
|
9
|
+
DRY_RUN=true
|
|
10
|
+
|
|
11
|
+
usage() {
|
|
12
|
+
cat <<EOF
|
|
13
|
+
Usage: $(basename "$0") [OPTIONS]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--bump {major,minor,patch} Version bump type (default: minor)
|
|
17
|
+
--execute Actually run (default is dry-run)
|
|
18
|
+
-h, --help Show this help
|
|
19
|
+
EOF
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
while [[ $# -gt 0 ]]; do
|
|
23
|
+
case "$1" in
|
|
24
|
+
--bump) BUMP="$2"; shift 2 ;;
|
|
25
|
+
--execute) DRY_RUN=false; shift ;;
|
|
26
|
+
-h|--help) usage; exit 0 ;;
|
|
27
|
+
*) echo "Unknown option: $1"; usage; exit 1 ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
if [[ ! "$BUMP" =~ ^(major|minor|patch)$ ]]; then
|
|
32
|
+
echo "Error: --bump must be major, minor, or patch (got '$BUMP')"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# --- Guard: clean working tree on main ---
|
|
37
|
+
branch=$(git rev-parse --abbrev-ref HEAD)
|
|
38
|
+
if [[ "$branch" != "main" ]]; then
|
|
39
|
+
echo "Error: must be on main (currently on '$branch')"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
44
|
+
echo "Error: working tree is dirty — commit or stash first"
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# --- Bump version ---
|
|
49
|
+
uv version --bump "$BUMP"
|
|
50
|
+
VERSION=$(uv version --short)
|
|
51
|
+
TAG="v${VERSION}"
|
|
52
|
+
|
|
53
|
+
echo "Version bumped to $VERSION (tag: $TAG)"
|
|
54
|
+
|
|
55
|
+
run() {
|
|
56
|
+
echo "+ $*"
|
|
57
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
58
|
+
return
|
|
59
|
+
fi
|
|
60
|
+
"$@"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# --- Commit, tag, push, release ---
|
|
64
|
+
run git add pyproject.toml
|
|
65
|
+
run git commit -m "release: $TAG"
|
|
66
|
+
run git push origin main
|
|
67
|
+
run git tag "$TAG"
|
|
68
|
+
run git push origin "$TAG"
|
|
69
|
+
run gh release create "$TAG" \
|
|
70
|
+
--title "[Pre-release] $TAG" \
|
|
71
|
+
--generate-notes \
|
|
72
|
+
--prerelease
|
|
73
|
+
|
|
74
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
75
|
+
echo ""
|
|
76
|
+
echo "(dry-run — re-run with --execute to apply)"
|
|
77
|
+
git checkout pyproject.toml
|
|
78
|
+
fi
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## System Overview
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
CLI / Python API
|
|
7
|
+
├─ view() / _launcher.py → FastAPI server (_server.py) [network mode]
|
|
8
|
+
│ /ws/{sid} WebSocket, /load register arrays, /seg/* segmentation
|
|
9
|
+
└─ Stdio server (_stdio_server.py) [direct webview mode]
|
|
10
|
+
stdin/stdout JSON+binary, no network — VS Code extension spawns subprocess
|
|
11
|
+
|
|
12
|
+
Server (either mode)
|
|
13
|
+
├─ _session.py Session objects, caches, render thread
|
|
14
|
+
├─ _render.py Slice extraction → RGBA → PNG pipeline
|
|
15
|
+
└─ _io.py File loading (numpy, nifti, zarr, DICOM, …)
|
|
16
|
+
|
|
17
|
+
Frontend (_viewer.html — single self-contained HTML file)
|
|
18
|
+
├─ CSS Dark theme, mode-specific layouts
|
|
19
|
+
├─ Canvas 2D rendering via ImageBitmap / putImageData
|
|
20
|
+
├─ WebSocket Binary slice transport, JSON metadata
|
|
21
|
+
└─ UI Colorbars, eggs, dynamic islands, overlays
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Display Routing
|
|
25
|
+
|
|
26
|
+
| Environment | Default display | Server mode |
|
|
27
|
+
|--------------------|------------------------------------|-------------|
|
|
28
|
+
| Jupyter | Inline iframe | network |
|
|
29
|
+
| VS Code local | Simple Browser (network) | network |
|
|
30
|
+
| VS Code tunnel | Direct webview (stdio) | stdio |
|
|
31
|
+
| Julia | System browser | network |
|
|
32
|
+
| CLI / Python script | Native pywebview | network |
|
|
33
|
+
| SSH terminal (ni) | VS Code ext via TCP relay (prints URL on relay failure) | network |
|
|
34
|
+
|
|
35
|
+
Detection logic lives in `_platform.py`. Display opening logic lives in `_launcher.py` (section: ViewHandle and view() API) and `_vscode.py`.
|
|
36
|
+
|
|
37
|
+
## File Map
|
|
38
|
+
|
|
39
|
+
| File | Lines | Owns |
|
|
40
|
+
|------|------:|------|
|
|
41
|
+
| `__init__.py` | 5 | Public API re-exports: `view`, `arrayview`, `ViewHandle`, `TrainingMonitor`, `view_batch`, `zarr_chunk_preset` |
|
|
42
|
+
| `__main__.py` | 4 | `python -m arrayview` entry point |
|
|
43
|
+
| `_app.py` | 179 | Backward-compat shim — re-exports everything from the split modules |
|
|
44
|
+
| `_config.py` | 121 | `~/.arrayview/config.toml` read/write, valid window modes/env keys |
|
|
45
|
+
| `_io.py` | 253 | Data loading: numpy, NIfTI (lazy nibabel), zarr, DICOM, raw files |
|
|
46
|
+
| `_launcher.py` | 2817 | **Main entry.** CLI parser, `view()` API, `ViewHandle`, server lifecycle, SSH relay, demo arrays, file watching |
|
|
47
|
+
| `_platform.py` | 396 | Environment detection: Jupyter, VS Code, SSH, tunnel, Julia, native-window capability |
|
|
48
|
+
| `_render.py` | 834 | Rendering pipeline: colormap LUTs, slice extraction, RGBA/RGB/mosaic rendering, overlay compositing, preload |
|
|
49
|
+
| `_segmentation.py` | 227 | nnInteractive segmentation client (pure HTTP, no nnInteractive dependency) |
|
|
50
|
+
| `_server.py` | 3258 | FastAPI app, all REST + WebSocket routes, HTML template serving |
|
|
51
|
+
| `_session.py` | 344 | `Session` class, global state (sockets, loops), render thread, prefetch, cache budgets, constants |
|
|
52
|
+
| `_stdio_server.py` | 791 | Stdio transport for VS Code direct webview — JSON stdin, binary stdout |
|
|
53
|
+
| `_torch.py` | 217 | PyTorch integration: `view_batch()`, `TrainingMonitor` (lazy torch import) |
|
|
54
|
+
| `_vscode.py` | 1014 | VS Code extension install/management, signal-file IPC, shared-memory IPC, browser opening |
|
|
55
|
+
| `_viewer.html` | 14480 | **The entire frontend** — CSS + JS in a single file, all viewing modes |
|
|
56
|
+
| `_shell.html` | 174 | Tab-bar shell for native pywebview — wraps viewer iframes, manages multi-tab sessions |
|
|
57
|
+
|
|
58
|
+
## Frontend (_viewer.html)
|
|
59
|
+
|
|
60
|
+
The frontend is a single self-contained HTML file (~15k lines). No build step, no external dependencies. Organized by section separators (`/* ── Section Name ── */` for CSS, `// ── Section Name ──` for JS).
|
|
61
|
+
|
|
62
|
+
### Major Sections
|
|
63
|
+
|
|
64
|
+
**CSS (lines ~7–1500)**
|
|
65
|
+
| Section | What it covers |
|
|
66
|
+
|---------|----------------|
|
|
67
|
+
| Theme Variables and Base Layout | CSS custom properties, dark theme palette, root layout grid |
|
|
68
|
+
| ColorBar and Dynamic Islands | Colorbar positioning, egg badges, info bar, dimension sliders |
|
|
69
|
+
| Generic ColorBar class styles | Shared `.av-colorbar` styles used by the ColorBar JS class |
|
|
70
|
+
| Compare, Overlay, and Prompt Styles | Side-by-side panes, overlay blend, prompt dialogs |
|
|
71
|
+
| Help and Info Overlays | Help shortcut overlay, array-info panel |
|
|
72
|
+
| Immersive, Fullscreen, and Compact Mode | Zen mode hide rules, fullscreen layout, compact overrides |
|
|
73
|
+
|
|
74
|
+
**JavaScript (lines ~1500–14750)**
|
|
75
|
+
| Section | What it covers |
|
|
76
|
+
|---------|----------------|
|
|
77
|
+
| Constants and Transport Setup | WS URL construction, stdio/postMessage transport abstraction |
|
|
78
|
+
| Viewer State Variables | All mutable state: current slice indices, zoom, mode flags |
|
|
79
|
+
| Mode Registry | Mode name → enter/exit function mapping |
|
|
80
|
+
| PanManager | Canvas panning state machine (normal + compare modes) |
|
|
81
|
+
| Canvas Scaling and Layout | `scaleCanvas()`, `mvScaleAllCanvases()`, `compareScaleCanvases()`, `qvScaleAllCanvases()` |
|
|
82
|
+
| Compare Mode | Multi-pane compare infrastructure, drag-to-reorder, sub-modes |
|
|
83
|
+
| Colorbar Rendering and Histogram | Colorbar draw routines, histogram morph, fullscreen overlay colorbar |
|
|
84
|
+
| ColorBar class | Reusable `ColorBar` class (~900 lines) — draw, histogram, window/level, hover |
|
|
85
|
+
| WebSocket and Data Transport | Binary slice receive, request queueing, reconnect |
|
|
86
|
+
| Initialization and Metadata Fetch | `/meta` fetch, loading screen, initial render |
|
|
87
|
+
| Info Bar and Pixel Display | Bottom info bar, hover pixel readout, coordinate display |
|
|
88
|
+
| State Persistence and Restore | URL hash state, `sessionStorage` save/restore |
|
|
89
|
+
| Rendering Pipeline | `updateView()`, play/animate, screenshot capture |
|
|
90
|
+
| ROI and Selection Modes | Rectangle/ellipse ROI drawing, statistics computation |
|
|
91
|
+
| nnInteractive Segmentation | Click-to-segment UI, mask overlay, undo stack |
|
|
92
|
+
| Keyboard Shortcuts | All hotkey bindings — single master switch/case block |
|
|
93
|
+
| Mode Transitions | Compare/multiview/qMRI enter/exit, crosshair animation |
|
|
94
|
+
| Scroll, Zoom, and Pan | Mouse wheel slice scroll, pinch zoom, scroll-to-zoom |
|
|
95
|
+
| Immersive Mode, Cross-Fade, and Visual Effects | Zen mode, fullscreen (K), animated transitions |
|
|
96
|
+
| UI Validation and Reconciliation | Reconcilers that enforce consistent UI state across modes |
|
|
97
|
+
| Colormap Strip and Wipe/Flicker Compare Tools | Colormap preview strip, A/B wipe, flicker, checkerboard |
|
|
98
|
+
| Ruler, Line Profile, and Mini-Map | Distance measurement, 1D intensity profile, overview mini-map |
|
|
99
|
+
| Compact Mode and Touch Input | Compact layout (K), touch gesture handling |
|
|
100
|
+
| File Picker and Session Management | File browser, drag-and-drop, session switching |
|
|
101
|
+
|
|
102
|
+
### Mode Matrix
|
|
103
|
+
|
|
104
|
+
Each viewing mode uses a specific scale function and reconciler set:
|
|
105
|
+
|
|
106
|
+
| Mode | Scale function | Layout | Reconciler notes |
|
|
107
|
+
|------|---------------|--------|------------------|
|
|
108
|
+
| Normal | `scaleCanvas()` | Single canvas, colorbar below | Base reconciler |
|
|
109
|
+
| Immersive (Zen) | `scaleCanvas()` | Canvas fills viewport, UI hidden | Animated enter/exit, dimbar+colorbar drag |
|
|
110
|
+
| Compact | `scaleCanvas()` | Reduced chrome | K-key toggle |
|
|
111
|
+
| Multiview (3-pane oblique) | `mvScaleAllCanvases()` | 3 canvases (axial/coronal/sagittal) | Layout container visibility reconciler |
|
|
112
|
+
| Compare | `compareScaleCanvases()` | 2+ side-by-side panes | Compare sub-mode reconciler (diff/overlay/wipe/flicker/checkerboard) |
|
|
113
|
+
| Diff | `compareScaleCanvases()` | Compare variant — center pane shows A-B | Diff colorbar instances |
|
|
114
|
+
| Registration | `compareScaleCanvases()` | Compare variant — overlay blend | Cross-fade overlay |
|
|
115
|
+
| qMRI | `qvScaleAllCanvases()` | Multi-map quantitative display | Separate scale pipeline |
|
|
116
|
+
|
|
117
|
+
### CSS Architecture
|
|
118
|
+
|
|
119
|
+
- **Dark theme only.** Background `#0c0c0c`, text `#d8d8d8`, accents in yellow (`#f5c842`).
|
|
120
|
+
- **Monospace UI.** Font: system monospace for data, system sans-serif for labels.
|
|
121
|
+
- **No CSS framework.** All custom properties defined in `:root`.
|
|
122
|
+
- **Mode-specific overrides** use `.immersive-mode`, `.compact-mode`, `.multiview-mode` classes on `body`.
|
|
123
|
+
- **`av-loading` class** on `body` during initial load — hides UI until data arrives. Removal triggers layout.
|
|
124
|
+
- **Hard UI invariants** block at line ~157 — structural rules that must never be overridden.
|
|
125
|
+
|
|
126
|
+
## Key Concepts
|
|
127
|
+
|
|
128
|
+
### Sessions
|
|
129
|
+
A `Session` object (`_session.py`) holds one array's data, metadata, and three LRU caches (raw slices, RGBA tiles, mosaics). Sessions are stored in the global `SESSIONS` dict keyed by `sid` (hex UUID). Multiple sessions enable compare mode and the file picker.
|
|
130
|
+
|
|
131
|
+
### Eggs (Mode Badges)
|
|
132
|
+
Pill-shaped badges below the canvas showing active visualization transforms. **Composable** — they stack: `FFT` `LOG` `MAGNITUDE` `PHASE` `REAL` `IMAG` `RGB` `ALPHA` `PROJECTION`. Each egg toggles a transform in the rendering pipeline.
|
|
133
|
+
|
|
134
|
+
**ROI and SEGMENT are NOT eggs.** They are interaction modes that take over canvas input, are mutually incompatible, and each has its own dynamic island UI.
|
|
135
|
+
|
|
136
|
+
### Dynamic Islands
|
|
137
|
+
Floating UI panels that appear/disappear based on context: ROI statistics, segmentation controls, colorbar hover, dimension sliders. Must be tested across all viewing modes (normal, immersive, multiview, compare).
|
|
138
|
+
|
|
139
|
+
### Reconcilers
|
|
140
|
+
Functions in the "UI Validation and Reconciliation" section (~line 13666) that enforce consistent UI state. When mode changes happen, reconcilers update visibility of containers, colorbars, dynamic islands, and compare sub-mode UI. There are four:
|
|
141
|
+
1. **Unified UI reconciler** — master state enforcer
|
|
142
|
+
2. **Layout container visibility reconciler** — show/hide mode-specific containers
|
|
143
|
+
3. **Compare sub-mode state reconciler** — diff/overlay/wipe/flicker/checkerboard state
|
|
144
|
+
4. **CB / island visibility reconciler** — colorbar and dynamic island show/hide
|
|
145
|
+
|
|
146
|
+
### Render Thread
|
|
147
|
+
A dedicated daemon thread (`_session.py`) runs all CPU-heavy rendering off the async event loop. `_render()` posts work to `_RENDER_QUEUE` and returns an awaitable `Future`. The prefetch pool (separate 1-thread executor) warms caches for neighboring slices.
|
|
148
|
+
|
|
149
|
+
## High-Risk Areas
|
|
150
|
+
|
|
151
|
+
### CSS Pitfalls
|
|
152
|
+
- **Canvas buffer vs CSS resolution mismatches.** The canvas element size (CSS pixels) and its buffer size (device pixels) must stay in sync. `scaleCanvas()` handles this — never set canvas dimensions outside a scale function.
|
|
153
|
+
- **Selector targeting wrong wrappers.** Normal mode has one `#canvas-wrapper`; compare mode has per-pane wrappers. CSS rules must scope correctly.
|
|
154
|
+
- **`av-loading` class interactions.** Many elements are hidden while `av-loading` is on `body`. Removing it too early causes layout flash; too late causes blank screen.
|
|
155
|
+
|
|
156
|
+
### Dynamic Islands
|
|
157
|
+
Must verify island positioning and visibility across normal, immersive, multiview, and compare modes. Islands use absolute/fixed positioning that breaks if parent containers change.
|
|
158
|
+
|
|
159
|
+
### Layout Debugging
|
|
160
|
+
When debugging layout issues: identify the root cause (which scale function, which reconciler, which CSS rule) before applying fixes. Symptoms in one mode often originate from shared code affecting all modes.
|
|
161
|
+
|
|
162
|
+
### WebSocket Binary Protocol
|
|
163
|
+
Slice data arrives as raw binary (RGBA bytes). The header format and byte offsets are tightly coupled between `_server.py` (Python) and the WebSocket handler in `_viewer.html` (JS). Changes to one must match the other.
|
|
164
|
+
|
|
165
|
+
### Lazy Imports
|
|
166
|
+
`_launcher.py` deliberately avoids importing numpy, _session, _render, and _io at module level. This saves ~300-350ms on CLI fast paths (when the server is already running). Breaking this lazy-loading pattern degrades startup time for every invocation.
|
|
167
|
+
|
|
168
|
+
## Data Flow
|
|
169
|
+
|
|
170
|
+
### Request Lifecycle: `view()` to Pixels
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
1. view(array) # _launcher.py
|
|
174
|
+
├─ Create Session(data) # _session.py — assigns sid, inits caches
|
|
175
|
+
├─ Start server if not running # _launcher.py → _server.py (uvicorn)
|
|
176
|
+
├─ Register session via /load # HTTP POST to FastAPI
|
|
177
|
+
└─ Open display # _launcher.py → _vscode.py / pywebview / browser
|
|
178
|
+
|
|
179
|
+
2. Browser loads _viewer.html # _server.py serves from package resources
|
|
180
|
+
├─ Establish WebSocket /ws/{sid} # or stdio transport for VS Code direct
|
|
181
|
+
└─ Fetch /meta/{sid} # Session metadata: shape, dtype, colormaps
|
|
182
|
+
|
|
183
|
+
3. User scrolls / interacts
|
|
184
|
+
├─ JS sends slice request # WebSocket binary or JSON
|
|
185
|
+
├─ Server dispatches to render thread
|
|
186
|
+
│ ├─ extract_slice() # _render.py — pull 2D slice from ND array
|
|
187
|
+
│ ├─ apply_complex_mode() # _render.py — FFT, magnitude, phase, etc.
|
|
188
|
+
│ ├─ render_rgba() / render_rgb() # _render.py — apply colormap LUT → RGBA
|
|
189
|
+
│ └─ PNG encode # sent as binary WebSocket frame
|
|
190
|
+
└─ JS receives binary
|
|
191
|
+
├─ Decode PNG → ImageBitmap
|
|
192
|
+
├─ drawImage() to canvas
|
|
193
|
+
└─ Update info bar, colorbar, eggs
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Stdio Transport Variant (VS Code Direct Webview)
|
|
197
|
+
Same pipeline, but `_stdio_server.py` replaces the FastAPI+WebSocket layer. Messages are JSON on stdin, binary responses are length-prefixed on stdout. The VS Code extension bridges between the webview's `postMessage` and the subprocess stdio.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
This module was extracted from _app.py during the modular refactor.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
# ── Imports and Lazy Loading ─────────────────────────────────────
|
|
7
|
+
|
|
6
8
|
import argparse
|
|
7
9
|
import asyncio
|
|
8
10
|
import io
|
|
@@ -125,9 +127,8 @@ def _vprint(*args, **kwargs) -> None:
|
|
|
125
127
|
_session_mod._vprint(*args, **kwargs)
|
|
126
128
|
|
|
127
129
|
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
# ---------------------------------------------------------------------------
|
|
130
|
+
# ── Subprocess GUI Launcher ───────────────────────────────────────
|
|
131
|
+
|
|
131
132
|
_ICON_PNG_PATH: str | None = None
|
|
132
133
|
|
|
133
134
|
|
|
@@ -380,9 +381,7 @@ def _open_webview_cli(
|
|
|
380
381
|
return True
|
|
381
382
|
|
|
382
383
|
|
|
383
|
-
#
|
|
384
|
-
# Server port utilities
|
|
385
|
-
# ---------------------------------------------------------------------------
|
|
384
|
+
# ── Server Port Utilities ─────────────────────────────────────────
|
|
386
385
|
|
|
387
386
|
|
|
388
387
|
def _server_alive(port: int, timeout: float = 0.5) -> bool:
|
|
@@ -479,9 +478,7 @@ def _relay_array_to_server(
|
|
|
479
478
|
)
|
|
480
479
|
|
|
481
480
|
|
|
482
|
-
#
|
|
483
|
-
# Zero-config SSH relay: send array to VS Code extension on SSH client machine
|
|
484
|
-
# ---------------------------------------------------------------------------
|
|
481
|
+
# ── Zero-Config SSH Relay ─────────────────────────────────────────
|
|
485
482
|
|
|
486
483
|
_RELAY_MAGIC = b"AVRELAY1"
|
|
487
484
|
_RELAY_DEFAULT_PORT = 17789
|
|
@@ -633,6 +630,9 @@ async def _serve_background(port: int, stop_when_closed: bool = False):
|
|
|
633
630
|
_OVERLAY_PALETTE = ["ff4444", "44cc44", "4488ff", "ffcc00", "ff44ff", "44ffff"]
|
|
634
631
|
|
|
635
632
|
|
|
633
|
+
# ── ViewHandle and view() API ────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
|
|
636
636
|
class ViewHandle(str):
|
|
637
637
|
"""Returned by :func:`view`. Behaves as a URL string for backward compatibility
|
|
638
638
|
and additionally exposes ``.update(arr)`` to push a new array into the viewer
|
|
@@ -1256,7 +1256,9 @@ def view(
|
|
|
1256
1256
|
if inline:
|
|
1257
1257
|
from IPython.display import IFrame, display as _ipy_display
|
|
1258
1258
|
|
|
1259
|
-
|
|
1259
|
+
# Add inline=1 param so the viewer starts in immersive mode
|
|
1260
|
+
_inline_url = url_viewer + "&inline=1"
|
|
1261
|
+
iframe = IFrame(src=_inline_url, width="100%", height=height)
|
|
1260
1262
|
if n_arrays == 1:
|
|
1261
1263
|
return iframe
|
|
1262
1264
|
# Multi-array inline: display the IFrame and return a uniform tuple of handles.
|
|
@@ -1317,6 +1319,9 @@ def view(
|
|
|
1317
1319
|
return tuple(ViewHandle(url_viewer, s, port) for s in [session.sid] + _compare_sids)
|
|
1318
1320
|
|
|
1319
1321
|
|
|
1322
|
+
# ── Server Lifecycle ──────────────────────────────────────────────
|
|
1323
|
+
|
|
1324
|
+
|
|
1320
1325
|
def _is_script_mode() -> bool:
|
|
1321
1326
|
"""True when running as a plain Python script (not interactive REPL, not Jupyter, not Julia)."""
|
|
1322
1327
|
if _in_jupyter() or _is_julia_env():
|
|
@@ -1531,8 +1536,9 @@ def _view_subprocess(
|
|
|
1531
1536
|
_print_viewer_location(url_viewer)
|
|
1532
1537
|
|
|
1533
1538
|
if inline:
|
|
1539
|
+
_inline_url = url_viewer + "&inline=1"
|
|
1534
1540
|
iframe_html = (
|
|
1535
|
-
f"<iframe src='{
|
|
1541
|
+
f"<iframe src='{_inline_url}' width='100%'"
|
|
1536
1542
|
f" height='{height}' frameborder='0'></iframe>"
|
|
1537
1543
|
)
|
|
1538
1544
|
# IJulia kernel: push HTML through Julia's display stack (routes to Jupyter
|
|
@@ -1727,6 +1733,9 @@ def _serve_daemon(
|
|
|
1727
1733
|
os._exit(0)
|
|
1728
1734
|
|
|
1729
1735
|
|
|
1736
|
+
# ── Demo Array and File Watching ──────────────────────────────────
|
|
1737
|
+
|
|
1738
|
+
|
|
1730
1739
|
def _make_demo_array() -> "np.ndarray":
|
|
1731
1740
|
"""Return a (128, 128, 32, 3) float32 RGB plasma animation.
|
|
1732
1741
|
|
|
@@ -1809,6 +1818,9 @@ def _start_watch_thread(filepath: str, sid: str, port: int) -> None:
|
|
|
1809
1818
|
t.start()
|
|
1810
1819
|
|
|
1811
1820
|
|
|
1821
|
+
# ── Config Subcommand ─────────────────────────────────────────────
|
|
1822
|
+
|
|
1823
|
+
|
|
1812
1824
|
def _handle_config_command(args: list[str]) -> None:
|
|
1813
1825
|
"""Handle 'arrayview config' subcommands."""
|
|
1814
1826
|
from arrayview._config import (
|
|
@@ -1894,6 +1906,9 @@ def _handle_config_command(args: list[str]) -> None:
|
|
|
1894
1906
|
sys.exit(1)
|
|
1895
1907
|
|
|
1896
1908
|
|
|
1909
|
+
# ── CLI Entry Point (arrayview command) ───────────────────────────
|
|
1910
|
+
|
|
1911
|
+
|
|
1897
1912
|
def arrayview():
|
|
1898
1913
|
"""Command Line Interface Entry Point."""
|
|
1899
1914
|
# Handle "arrayview config ..." subcommand before argparse
|
|
@@ -6,7 +6,6 @@ import numpy as np
|
|
|
6
6
|
|
|
7
7
|
from arrayview._session import (
|
|
8
8
|
COLORMAPS,
|
|
9
|
-
DR_PERCENTILES,
|
|
10
9
|
SESSIONS,
|
|
11
10
|
)
|
|
12
11
|
|
|
@@ -105,15 +104,10 @@ def mosaic_shape(batch):
|
|
|
105
104
|
# ---------------------------------------------------------------------------
|
|
106
105
|
|
|
107
106
|
|
|
108
|
-
def _compute_vmin_vmax(session, data, dr, complex_mode=0):
|
|
107
|
+
def _compute_vmin_vmax(session, data, dr=0, complex_mode=0):
|
|
109
108
|
if complex_mode == 1 and np.iscomplexobj(session.data):
|
|
110
109
|
return (-float(np.pi), float(np.pi))
|
|
111
|
-
|
|
112
|
-
vmin, vmax = session.global_stats[dr]
|
|
113
|
-
if vmin != vmax:
|
|
114
|
-
return vmin, vmax
|
|
115
|
-
pct_lo, pct_hi = DR_PERCENTILES[dr % len(DR_PERCENTILES)]
|
|
116
|
-
return float(np.percentile(data, pct_lo)), float(np.percentile(data, pct_hi))
|
|
110
|
+
return float(np.percentile(data, 1)), float(np.percentile(data, 99))
|
|
117
111
|
|
|
118
112
|
|
|
119
113
|
def extract_slice(session, dim_x, dim_y, idx_list):
|
|
@@ -237,18 +231,8 @@ def _prepare_display(
|
|
|
237
231
|
return data, vmin_override, vmax_override
|
|
238
232
|
if log_scale:
|
|
239
233
|
data = np.log1p(np.abs(data)).astype(np.float32)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
vmin = float(np.log1p(abs(raw_vmin)))
|
|
243
|
-
vmax = float(np.log1p(abs(raw_vmax)))
|
|
244
|
-
if vmin == vmax:
|
|
245
|
-
pct_lo, pct_hi = DR_PERCENTILES[dr % len(DR_PERCENTILES)]
|
|
246
|
-
vmin = float(np.percentile(data, pct_lo))
|
|
247
|
-
vmax = float(np.percentile(data, pct_hi))
|
|
248
|
-
else:
|
|
249
|
-
pct_lo, pct_hi = DR_PERCENTILES[dr % len(DR_PERCENTILES)]
|
|
250
|
-
vmin = float(np.percentile(data, pct_lo))
|
|
251
|
-
vmax = float(np.percentile(data, pct_hi))
|
|
234
|
+
vmin = float(np.percentile(data, 1))
|
|
235
|
+
vmax = float(np.percentile(data, 99))
|
|
252
236
|
else:
|
|
253
237
|
vmin, vmax = _compute_vmin_vmax(session, data, dr, complex_mode)
|
|
254
238
|
return data, vmin, vmax
|
|
@@ -698,12 +682,11 @@ def render_mosaic(
|
|
|
698
682
|
frames = [np.log1p(np.abs(f)).astype(np.float32) for f in frames]
|
|
699
683
|
all_data = np.stack(frames)
|
|
700
684
|
|
|
701
|
-
if
|
|
702
|
-
|
|
703
|
-
vmin = float(np.percentile(all_data, pct_lo))
|
|
704
|
-
vmax = float(np.percentile(all_data, pct_hi))
|
|
685
|
+
if complex_mode == 1 and np.iscomplexobj(session.data):
|
|
686
|
+
vmin, vmax = -float(np.pi), float(np.pi)
|
|
705
687
|
else:
|
|
706
|
-
vmin
|
|
688
|
+
vmin = float(np.percentile(all_data, 1))
|
|
689
|
+
vmax = float(np.percentile(all_data, 99))
|
|
707
690
|
|
|
708
691
|
if mosaic_cols is not None:
|
|
709
692
|
cols = mosaic_cols
|