arrayview 0.17.0__tar.gz → 0.19.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.19.0/.agents/skills/frontend-designer/SKILL.md +127 -0
- arrayview-0.19.0/.agents/skills/todo-workflow/SKILL.md +37 -0
- {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/ui-consistency-audit/SKILL.md +1 -1
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/ROUTER.md +1 -1
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/architecture.md +2 -2
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/project-state.md +3 -3
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/setup.md +3 -3
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/stack.md +2 -2
- {arrayview-0.17.0 → arrayview-0.19.0}/.vscode/settings.json +18 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/PKG-INFO +1 -1
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/display.md +1 -1
- {arrayview-0.17.0 → arrayview-0.19.0}/pyproject.toml +1 -1
- {arrayview-0.17.0 → arrayview-0.19.0}/scripts/release.sh +31 -4
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_config.py +18 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_launcher.py +31 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_server.py +62 -38
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_session.py +1 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_stdio_server.py +60 -13
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_viewer.html +2235 -743
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_api.py +0 -33
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_browser.py +170 -19
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_command_reachability.py +1 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_interactions.py +181 -80
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_mode_consistency.py +4 -3
- arrayview-0.19.0/tests/test_mode_entry_batching.py +63 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_mode_roundtrip.py +20 -6
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/visual_smoke.py +60 -57
- {arrayview-0.17.0 → arrayview-0.19.0}/uv.lock +1 -1
- arrayview-0.17.0/IMMERSIVE_ANIMATION.md +0 -43
- {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.github/copilot-instructions.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.gitignore +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.ignore +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/AGENTS.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/SETUP.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/SYNC.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/conventions.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/decisions.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/frontend.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/render-pipeline.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/INDEX.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/README.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/add-file-format.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/add-server-endpoint.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/debug-render.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/frontend-change.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.opencode/opencode.json +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/.python-version +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/AGENTS.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/CONTRIBUTING.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/LICENSE +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/README.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/comparing.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/configuration.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/index.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/loading.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/logo.png +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/measurement.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/remote.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/docs/viewing.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/matlab/arrayview.m +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/mkdocs.yml +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/plans/2026-04-14-immersive-animation.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/scripts/demo.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_io.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/conftest.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_cli.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_config.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_loading_server.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_torch.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/tests/ui_audit.py +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/vscode-extension/AGENTS.md +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/vscode-extension/LICENSE +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/vscode-extension/extension.js +0 -0
- {arrayview-0.17.0 → arrayview-0.19.0}/vscode-extension/package.json +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-designer
|
|
3
|
+
description: Use when making any styling or layout change to _viewer.html. Ensures new UI is visually consistent with the established design language — dark theme, monospace typography, yellow accents, and minimal chrome.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ArrayView Frontend Design Skill
|
|
7
|
+
|
|
8
|
+
## Design Philosophy
|
|
9
|
+
|
|
10
|
+
Minimal chrome. Let arrays fill the screen. UI elements are dim until needed; the array is always the primary focus.
|
|
11
|
+
|
|
12
|
+
- Controls fade in on hover or keypress, not permanently visible
|
|
13
|
+
- Text is small and monospaced
|
|
14
|
+
- No decorative elements — every pixel either shows data or provides affordance
|
|
15
|
+
- All four themes must look correct; never hardcode colors
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Design Tokens (CSS Custom Properties)
|
|
20
|
+
|
|
21
|
+
All colors come from CSS variables defined on `:root`. Never use raw hex values in new code.
|
|
22
|
+
|
|
23
|
+
| Variable | Dark default | Purpose |
|
|
24
|
+
|----------|-------------|---------|
|
|
25
|
+
| `--bg` | `#0c0c0c` | Page/canvas background |
|
|
26
|
+
| `--surface` | `#141414` | Panel/overlay backgrounds |
|
|
27
|
+
| `--surface-2` | `#1c1c1c` | Input fields, nested surfaces |
|
|
28
|
+
| `--border` | `#2c2c2c` | Borders, dividers |
|
|
29
|
+
| `--text` | `#d8d8d8` | Primary text |
|
|
30
|
+
| `--muted` | `#5a5a5a` | Secondary/dim text, inactive labels |
|
|
31
|
+
| `--highlight` | `#fff` | Maximum contrast text |
|
|
32
|
+
| `--active-dim` | `#f5c842` | **Primary accent** — active state, keyboard keys, clim handles |
|
|
33
|
+
| `--spatial-dim` | `#b48ead` | Spatial (x/y) dimension labels |
|
|
34
|
+
| `--help-key` | `#f5c842` | Keyboard shortcut key labels in help overlay |
|
|
35
|
+
| `--overlay-bg` | `rgba(0,0,0,0.72)` | Modal backdrop |
|
|
36
|
+
| `--blur` | `blur(14px)` | Backdrop blur for overlays |
|
|
37
|
+
| `--radius` | `10px` | Standard border radius |
|
|
38
|
+
| `--radius-lg` | `14px` | Large panel border radius |
|
|
39
|
+
|
|
40
|
+
Four themes exist: `dark` (default), `.light`, `.solarized`, `.nord`. Each redefines all tokens. Test with `T` key.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Typography
|
|
45
|
+
|
|
46
|
+
Single font stack throughout:
|
|
47
|
+
```css
|
|
48
|
+
font-family: 'SF Mono', ui-monospace, 'Cascadia Code', 'JetBrains Mono', monospace;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Sizes used in practice:
|
|
52
|
+
- `10px` — tooltips, micro labels
|
|
53
|
+
- `11px` — colorbar value labels, secondary info
|
|
54
|
+
- `12px` — dim labels, position info
|
|
55
|
+
- `13px` — array name, status bar, picker items
|
|
56
|
+
- `15px` — dim scrubber labels
|
|
57
|
+
- `16px` — mode/view headers
|
|
58
|
+
|
|
59
|
+
Never use a sans-serif font. Never use `font-weight: bold` except via the `.highlight` or `.active-dim` classes.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Layout Principles
|
|
64
|
+
|
|
65
|
+
- `#wrapper`: flexbox column, centers content vertically and horizontally
|
|
66
|
+
- Canvas fills available space; UI chrome is positioned absolutely around it
|
|
67
|
+
- Bottom bar (`#info`): fixed height, monospace, dim text
|
|
68
|
+
- Overlays (`#help-overlay`, `#uni-picker`, `#inline-prompt`): centered modal with `--overlay-bg` backdrop
|
|
69
|
+
- Colorbar (`#slim-cb-wrap`): thin strip below canvas, expands only in Lebesgue mode
|
|
70
|
+
|
|
71
|
+
**Canvas sizing:** canvas width/height are set by JavaScript (`scaleCanvas` and friends), not CSS. Do not set canvas dimensions in CSS.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Component Patterns
|
|
76
|
+
|
|
77
|
+
### Status/info text
|
|
78
|
+
```css
|
|
79
|
+
color: var(--muted); font-size: 12px; /* passive info */
|
|
80
|
+
color: var(--text); font-size: 12px; /* active info */
|
|
81
|
+
color: var(--active-dim); font-weight: bold; /* highlighted state */
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Panel/overlay
|
|
85
|
+
```css
|
|
86
|
+
background: var(--surface);
|
|
87
|
+
border: 1px solid var(--border);
|
|
88
|
+
border-radius: var(--radius-lg);
|
|
89
|
+
padding: 16px 20px;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Modal backdrop
|
|
93
|
+
```css
|
|
94
|
+
background: var(--overlay-bg);
|
|
95
|
+
backdrop-filter: var(--blur);
|
|
96
|
+
-webkit-backdrop-filter: var(--blur);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Keyboard key labels (in help overlay)
|
|
100
|
+
```html
|
|
101
|
+
<span class="highlight">X</span>
|
|
102
|
+
```
|
|
103
|
+
CSS: `color: var(--help-key); font-weight: bold;`
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Do / Don't
|
|
108
|
+
|
|
109
|
+
| Do | Don't |
|
|
110
|
+
|----|-------|
|
|
111
|
+
| Use `var(--active-dim)` for active/selected state | Hardcode `#f5c842` |
|
|
112
|
+
| Keep new controls hidden by default, shown on interaction | Add persistent toolbar buttons |
|
|
113
|
+
| Use `opacity` or `color: var(--muted)` for inactive state | Use `visibility: hidden` (breaks layout) |
|
|
114
|
+
| Use `transition: opacity 0.15s` for hover reveals | Animate position/size (janky on canvas) |
|
|
115
|
+
| Test all four themes with `T` key | Assume dark theme only |
|
|
116
|
+
| Match existing font sizes | Introduce new size values |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Checklist Before Shipping a UI Change
|
|
121
|
+
|
|
122
|
+
- [ ] Tested in all four themes (dark, light, solarized, nord) with `T` key
|
|
123
|
+
- [ ] No hardcoded color values — all via `var(--...)`
|
|
124
|
+
- [ ] Font is monospace, size matches existing scale
|
|
125
|
+
- [ ] New panel/overlay uses `--surface` + `--border` + `--radius-lg`
|
|
126
|
+
- [ ] `viewer-ui-checklist` skill followed (smoke test updated)
|
|
127
|
+
- [ ] `modes-consistency` skill followed if canvas/colorbar is involved
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: todo-workflow
|
|
3
|
+
description: Use when working through TODO items, implementing batches of features or fixes, or when the user gives multiple tasks at once. Enforces commit-per-item, collateral updates, and cross-mode verification.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TODO Workflow
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
When working through multiple tasks (TODO items, feature requests, bug fixes given together), follow these rules for every item. The TODO list lives in `dev/TODO.md`.
|
|
11
|
+
|
|
12
|
+
## Per-Item Rules
|
|
13
|
+
|
|
14
|
+
Each finished item gets:
|
|
15
|
+
1. **Its own commit** — one item, one commit, no batching
|
|
16
|
+
2. **Updated tests** — add/update test coverage for new functionality
|
|
17
|
+
3. **Updated README** — if user-facing behavior changed
|
|
18
|
+
4. **UI audit** — run ui-consistency-audit skill to verify across all modes
|
|
19
|
+
5. **Invocation check** — use invocation-consistency skill if touching server/startup/display-opening
|
|
20
|
+
6. **VS Code check** — use vscode-simplebrowser skill if touching extension install, signal-file IPC, or auto-open logic. This breaks often.
|
|
21
|
+
7. **Lessons learned** — update `dev/lessons_learned.md` with anything important for future sessions
|
|
22
|
+
|
|
23
|
+
## Execution Rules
|
|
24
|
+
|
|
25
|
+
- **Plan first** — write a plan before starting implementation
|
|
26
|
+
- **Subagents** — spawn them for independent items to parallelize work
|
|
27
|
+
- **Branching** — work on `main` unless parallelizing; if branches needed, rebase (no merge commits)
|
|
28
|
+
- **Compact context** — clear/compact between items to stay sharp
|
|
29
|
+
- **Skills** — use and update relevant skills, especially ui-consistency-audit
|
|
30
|
+
|
|
31
|
+
## Design Philosophy
|
|
32
|
+
|
|
33
|
+
The app is feature-rich but minimal — no clutter. Users should discover features and think: *"wait... it already does that?!"* Every feature should feel like a hidden gift, not visual noise.
|
|
34
|
+
|
|
35
|
+
## When User Says They're Going to Sleep
|
|
36
|
+
|
|
37
|
+
Don't ask for confirmation. Make your own decisions on remaining items. They expect to be impressed when they wake up.
|
|
@@ -256,7 +256,7 @@ Rules are enforced through multiple layers. When adding new rules, add enforceme
|
|
|
256
256
|
| **CSS** | `!important` structural constraints | `_viewer.html` CSS (line ~156) | Always — structurally impossible to violate |
|
|
257
257
|
| **Runtime JS** | `_validateUIState()` | `_viewer.html` JS (after `_positionFullscreenChrome`) | After every layout change, gated behind `?debug_ui=1` |
|
|
258
258
|
| **Playwright** | `run_invariant_assertions()` + `run_immersive_exit_assertions()` | `tests/ui_audit.py` | During `ui_audit.py` runs (all tiers) |
|
|
259
|
-
| **Skill** | This document (Section 5b) | `.
|
|
259
|
+
| **Skill** | This document (Section 5b) | `.Codex/skills/ui-consistency-audit/SKILL.md` | When AI agent invokes the skill |
|
|
260
260
|
|
|
261
261
|
**Cross-reference:**
|
|
262
262
|
|
|
@@ -37,7 +37,7 @@ Python package for interactively viewing multi-dimensional arrays (numpy, NIfTI,
|
|
|
37
37
|
|
|
38
38
|
## Commands
|
|
39
39
|
- Test: `uv run pytest tests/`
|
|
40
|
-
- Visual smoke: `uv run
|
|
40
|
+
- Visual smoke: `uv run python tests/visual_smoke.py`
|
|
41
41
|
- CLI: `uvx arrayview <file>`
|
|
42
42
|
- Build: `uv build`
|
|
43
43
|
|
|
@@ -52,7 +52,7 @@ pywebview, or system browser).
|
|
|
52
52
|
- **`_server.py`** — FastAPI app with all REST and WebSocket routes (`/meta/{sid}`, `/load`, `/slice`, `/ws/{sid}`, `/seg/*`, `/reload`, etc.). Dispatches render work to the render thread via `_render()` from `_session.py`.
|
|
53
53
|
- **`_session.py`** — Single source of global mutable state: `SESSIONS`, `SERVER_LOOP`, `VIEWER_SOCKETS`, `VIEWER_SIDS`, `SHELL_SOCKETS`. Owns the render thread (`_RENDER_QUEUE`, `_RENDER_THREAD`), prefetch pool, and the `Session` class with its three LRU caches.
|
|
54
54
|
- **`_render.py`** — Stateless rendering functions: `extract_slice()`, `apply_complex_mode()`, `render_rgba()`, `render_rgb_rgba()`, `render_mosaic()`, `extract_projection()`. Owns colormap LUTs (`LUTS` dict, lazy-initialized by `_init_luts()`).
|
|
55
|
-
- **`_io.py`** — All file-format loading behind `load_data(filepath)`. Lazy nibabel import for NIfTI. Handles `.npy`, `.npz`, `.nii
|
|
55
|
+
- **`_io.py`** — All file-format loading behind `load_data(filepath)`. Lazy nibabel import for NIfTI. Handles `.npy`, `.npz`, `.nii` and `.nii.gz`, `.zarr`, `.zarr.zip`, `.pt` and `.pth`, `.h5` and `.hdf5`, `.tif` and `.tiff`, `.mat`. Extensions registered in `_SUPPORTED_EXTS`.
|
|
56
56
|
- **`_platform.py`** — Environment detection: checks jupyter → vscode → julia → ssh → terminal in priority order. Results cached. Never short-circuit this order.
|
|
57
57
|
- **`_vscode.py`** — VS Code extension install/management, signal-file IPC, shared-memory IPC, webview panel and direct webview opening.
|
|
58
58
|
- **`_stdio_server.py`** — Alternative to FastAPI for VS Code tunnel (direct webview): JSON on stdin, length-prefixed binary on stdout.
|
|
@@ -77,7 +77,7 @@ Detection logic: `_platform.py`. Display opening: `_launcher.py` + `_vscode.py`.
|
|
|
77
77
|
- **nibabel** — NIfTI file loading. Lazy-imported in `_io.py` via `_nib()`. Only loaded for `.nii` / `.nii.gz`.
|
|
78
78
|
- **numpy** — Core array type throughout. The only non-lazy import in the render path.
|
|
79
79
|
- **matplotlib** — Colormap LUT generation only. Lazy, initialized once by `_init_luts()` in `_render.py`.
|
|
80
|
-
- **qmricolors** — Registers the `lipari` and `navia` colormaps. Git dependency (`oscarvanderheide/qmricolors`).
|
|
80
|
+
- **qmricolors** — Registers the `lipari` and `navia` colormaps. Git dependency (`https://github.com/oscarvanderheide/qmricolors.git`).
|
|
81
81
|
- **zarr** — Lazy chunk access for `.zarr` / `.zarr.zip`. Chunk presets via `zarr_chunk_preset()` in `_session.py`.
|
|
82
82
|
- **pywebview** — Native OS window. Lazy, only started when `_can_native_window()` is true.
|
|
83
83
|
|
|
@@ -7,7 +7,7 @@ triggers:
|
|
|
7
7
|
- "recent work"
|
|
8
8
|
- "active feature"
|
|
9
9
|
- "shipped recently"
|
|
10
|
-
last_updated: 2026-04-
|
|
10
|
+
last_updated: 2026-04-24
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Project State
|
|
@@ -21,7 +21,7 @@ last_updated: 2026-04-17
|
|
|
21
21
|
- NIfTI spatial metadata, RAS resampling
|
|
22
22
|
- VS Code extension v0.14.5 — stable window ID via `EnvironmentVariableCollection`; `arrayview.openInFloatingWindow` setting moves new tabs to a floating window; `view(arr, floating=True)` and `arrayview file.npy --floating` open in a floating window per-call regardless of global setting; `!vscode.env.remoteName` guard removed (remote VS Code supports floating windows); floating mode now uses a single persistent shell hub panel (`_shell.html`) so all arrays share one floating window as tabs instead of opening separate windows; fixed: second CLI call now injects tab via `new_tab` postMessage relay (extension -> hub wrapper -> shell iframe) instead of relying on WebSocket notify which wasn't sent in VS Code mode
|
|
23
23
|
- Colorbar refactor: `ColorBar` JS class partially migrated (in progress)
|
|
24
|
-
-
|
|
24
|
+
- Colormap picker: `c` opens an expanded colorbar-island grid without changing the colormap; subsequent `c` taps cycle, hover/hjkl/arrows live-preview, Enter/click commits, Esc cancels, and auto-dismiss pauses while hovered
|
|
25
25
|
- Cold-start loading spinner in VS Code and native shell
|
|
26
26
|
- Plugin shelf (`/` menu) supports multi-select: spacebar toggles plugins, Enter applies selection. Mutual exclusion enforced (ROI ↔ Segmentation, and overlay/vectorfield ↔ everything else). Cursor indicator shows focused tile via yellow background + left accent bar
|
|
27
27
|
- Dynamic island renders sections for all active plugins simultaneously (qMRI pills + ROI shapes/stats separated by divider), replacing the old single-plugin priority chain
|
|
@@ -42,4 +42,4 @@ last_updated: 2026-04-17
|
|
|
42
42
|
## Not Yet Built
|
|
43
43
|
|
|
44
44
|
- Independent split view for mismatched-shape arrays (designed, shelved)
|
|
45
|
-
- Admin/config UI (file-based
|
|
45
|
+
- Admin/config UI (design intent: file-based user config only, no in-app admin panel)
|
|
@@ -13,7 +13,7 @@ edges:
|
|
|
13
13
|
condition: when specific technology versions or library details are needed
|
|
14
14
|
- target: context/architecture.md
|
|
15
15
|
condition: when understanding how components connect during setup
|
|
16
|
-
last_updated: 2026-04-
|
|
16
|
+
last_updated: 2026-04-22
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
# Setup
|
|
@@ -28,7 +28,7 @@ last_updated: 2026-04-15
|
|
|
28
28
|
|
|
29
29
|
1. Clone the repo
|
|
30
30
|
2. `uv sync --all-groups` — installs all dependencies including dev and test groups
|
|
31
|
-
3. `uv run arrayview
|
|
31
|
+
3. `uv run arrayview debug/cig_meeting_examples/BraTS2021_00009_t1.nii.gz` — smoke test: should open the viewer with a sample array
|
|
32
32
|
4. `uv run pytest tests/test_mode_consistency.py` — verify core render consistency passes
|
|
33
33
|
|
|
34
34
|
For browser-based tests (playwright):
|
|
@@ -49,7 +49,7 @@ No `.env` file is needed. All env vars are optional overrides; the server runs w
|
|
|
49
49
|
- `uvx arrayview <file>` — launch from anywhere without activating the venv
|
|
50
50
|
- `uv run pytest tests/<target>` — run a specific test file
|
|
51
51
|
- `uv run pytest tests/test_mode_consistency.py` — mode consistency suite (run after render changes)
|
|
52
|
-
- `uv run
|
|
52
|
+
- `uv run python tests/visual_smoke.py` — browser smoke tests (requires playwright)
|
|
53
53
|
- `uv run pytest -m "not browser"` — all non-browser tests
|
|
54
54
|
- `uv build` — build wheel + sdist in `dist/`
|
|
55
55
|
|
|
@@ -14,7 +14,7 @@ edges:
|
|
|
14
14
|
condition: when understanding how to use a technology in this codebase
|
|
15
15
|
- target: context/architecture.md
|
|
16
16
|
condition: when understanding how a library fits into the overall system
|
|
17
|
-
last_updated: 2026-04-
|
|
17
|
+
last_updated: 2026-04-22
|
|
18
18
|
---
|
|
19
19
|
|
|
20
20
|
# Stack
|
|
@@ -34,7 +34,7 @@ last_updated: 2026-04-15
|
|
|
34
34
|
- **zarr 2.17+** — lazy chunk access for `.zarr` / `.zarr.zip` files; chunk preset utility in `_session.py`.
|
|
35
35
|
- **pillow 12+** — PNG encoding for slice frames sent over WebSocket.
|
|
36
36
|
- **pywebview 6.1+** — native OS window for CLI / script invocations; lazy, only started when `_can_native_window()` is true.
|
|
37
|
-
- **qmricolors** — registers `lipari` and `navia` colormaps into matplotlib; Git dependency (`oscarvanderheide/qmricolors`). Imported inside `_init_luts()`.
|
|
37
|
+
- **qmricolors** — registers `lipari` and `navia` colormaps into matplotlib; Git dependency (`https://github.com/oscarvanderheide/qmricolors.git`). Imported inside `_init_luts()`.
|
|
38
38
|
- **scipy** — `.mat` file loading via `scipy.io.loadmat`; lazy in `_io.py`.
|
|
39
39
|
- **h5py** — `.h5` / `.hdf5` file loading; lazy in `_io.py`.
|
|
40
40
|
- **tifffile** — `.tif` / `.tiff` file loading; lazy in `_io.py`.
|
|
@@ -196,6 +196,24 @@
|
|
|
196
196
|
"label": "ArrayView",
|
|
197
197
|
"onAutoForward": "silent",
|
|
198
198
|
"privacy": "public"
|
|
199
|
+
},
|
|
200
|
+
"8777": {
|
|
201
|
+
"protocol": "http",
|
|
202
|
+
"label": "ArrayView",
|
|
203
|
+
"onAutoForward": "silent",
|
|
204
|
+
"privacy": "public"
|
|
205
|
+
},
|
|
206
|
+
"8778": {
|
|
207
|
+
"protocol": "http",
|
|
208
|
+
"label": "ArrayView",
|
|
209
|
+
"onAutoForward": "silent",
|
|
210
|
+
"privacy": "public"
|
|
211
|
+
},
|
|
212
|
+
"8779": {
|
|
213
|
+
"protocol": "http",
|
|
214
|
+
"label": "ArrayView",
|
|
215
|
+
"onAutoForward": "silent",
|
|
216
|
+
"privacy": "public"
|
|
199
217
|
}
|
|
200
218
|
},
|
|
201
219
|
"search.exclude": {
|
|
@@ -8,6 +8,7 @@ set -euo pipefail
|
|
|
8
8
|
BUMP="minor"
|
|
9
9
|
DRY_RUN=true
|
|
10
10
|
NO_AI=false
|
|
11
|
+
AI_TOOL="claude"
|
|
11
12
|
|
|
12
13
|
usage() {
|
|
13
14
|
cat <<EOF
|
|
@@ -15,6 +16,7 @@ Usage: $(basename "$0") [OPTIONS]
|
|
|
15
16
|
|
|
16
17
|
Options:
|
|
17
18
|
--bump {major,minor,patch} Version bump type (default: minor)
|
|
19
|
+
--ai {claude,codex} AI tool for release notes (default: claude)
|
|
18
20
|
--execute Actually run (default is dry-run)
|
|
19
21
|
--no-ai Skip AI release notes, use GitHub's --generate-notes
|
|
20
22
|
-h, --help Show this help
|
|
@@ -24,6 +26,7 @@ EOF
|
|
|
24
26
|
while [[ $# -gt 0 ]]; do
|
|
25
27
|
case "$1" in
|
|
26
28
|
--bump) BUMP="$2"; shift 2 ;;
|
|
29
|
+
--ai) AI_TOOL="$2"; shift 2 ;;
|
|
27
30
|
--execute) DRY_RUN=false; shift ;;
|
|
28
31
|
--no-ai) NO_AI=true; shift ;;
|
|
29
32
|
-h|--help) usage; exit 0 ;;
|
|
@@ -36,6 +39,11 @@ if [[ ! "$BUMP" =~ ^(major|minor|patch)$ ]]; then
|
|
|
36
39
|
exit 1
|
|
37
40
|
fi
|
|
38
41
|
|
|
42
|
+
if [[ ! "$AI_TOOL" =~ ^(claude|codex)$ ]]; then
|
|
43
|
+
echo "Error: --ai must be claude or codex (got '$AI_TOOL')"
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
39
47
|
# --- Guard: clean working tree on main ---
|
|
40
48
|
branch=$(git rev-parse --abbrev-ref HEAD)
|
|
41
49
|
if [[ "$branch" != "main" ]]; then
|
|
@@ -67,6 +75,7 @@ run() {
|
|
|
67
75
|
|
|
68
76
|
# --- Generate release notes ---
|
|
69
77
|
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
|
|
78
|
+
CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
|
|
70
79
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "")
|
|
71
80
|
NOTES=""
|
|
72
81
|
|
|
@@ -76,9 +85,8 @@ else
|
|
|
76
85
|
COMMITS=$(git log --oneline -20)
|
|
77
86
|
fi
|
|
78
87
|
|
|
79
|
-
if [[ "$NO_AI" == false ]]
|
|
80
|
-
|
|
81
|
-
NOTES=$("$CLAUDE_BIN" -p "You are writing release notes for arrayview $TAG (a Python array/image viewer).
|
|
88
|
+
if [[ "$NO_AI" == false ]]; then
|
|
89
|
+
PROMPT="You are writing release notes for arrayview $TAG (a Python array/image viewer).
|
|
82
90
|
|
|
83
91
|
Here are the commits since the last release ($PREV_TAG):
|
|
84
92
|
|
|
@@ -95,7 +103,26 @@ Rules:
|
|
|
95
103
|
- Write for end-users, not developers (no commit hashes, no file names)
|
|
96
104
|
- Use past tense (\"added\", \"fixed\", \"improved\")
|
|
97
105
|
- Skip pure refactors/docs unless they affect user experience
|
|
98
|
-
- Bold the feature name, keep the description to one sentence"
|
|
106
|
+
- Bold the feature name, keep the description to one sentence"
|
|
107
|
+
|
|
108
|
+
case "$AI_TOOL" in
|
|
109
|
+
claude)
|
|
110
|
+
if command -v "$CLAUDE_BIN" &>/dev/null; then
|
|
111
|
+
echo "Generating release notes with Claude..."
|
|
112
|
+
NOTES=$("$CLAUDE_BIN" -p "$PROMPT" 2>/dev/null) || true
|
|
113
|
+
fi
|
|
114
|
+
;;
|
|
115
|
+
codex)
|
|
116
|
+
if command -v "$CODEX_BIN" &>/dev/null; then
|
|
117
|
+
echo "Generating release notes with Codex..."
|
|
118
|
+
tmpfile=$(mktemp)
|
|
119
|
+
trap 'rm -f "$tmpfile"' EXIT
|
|
120
|
+
if printf '%s\n' "$PROMPT" | "$CODEX_BIN" exec --output-last-message "$tmpfile" - >/dev/null 2>/dev/null; then
|
|
121
|
+
NOTES=$(<"$tmpfile")
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
;;
|
|
125
|
+
esac
|
|
99
126
|
fi
|
|
100
127
|
|
|
101
128
|
if [[ -z "$NOTES" ]]; then
|
|
@@ -56,6 +56,24 @@ def get_viewer_theme() -> str | None:
|
|
|
56
56
|
return None
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def get_viewer_rounded_panes() -> bool | None:
|
|
60
|
+
"""Return user-configured default for rounded panes, or None if not configured."""
|
|
61
|
+
cfg = load_config()
|
|
62
|
+
viewer_cfg = cfg.get("viewer", {})
|
|
63
|
+
if not isinstance(viewer_cfg, dict):
|
|
64
|
+
return None
|
|
65
|
+
val = viewer_cfg.get("rounded_panes")
|
|
66
|
+
if isinstance(val, bool):
|
|
67
|
+
return val
|
|
68
|
+
if isinstance(val, str):
|
|
69
|
+
s = val.strip().lower()
|
|
70
|
+
if s in ("true", "1", "yes", "on"):
|
|
71
|
+
return True
|
|
72
|
+
if s in ("false", "0", "no", "off"):
|
|
73
|
+
return False
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
59
77
|
def get_nninteractive_url() -> str | None:
|
|
60
78
|
"""Return configured nnInteractive server URL, or None.
|
|
61
79
|
|
|
@@ -2504,6 +2504,37 @@ def arrayview():
|
|
|
2504
2504
|
print(f"[ArrayView] Killed process {pid} on port {args.port}")
|
|
2505
2505
|
except ProcessLookupError:
|
|
2506
2506
|
pass
|
|
2507
|
+
deadline = time.time() + 1.0
|
|
2508
|
+
while time.time() < deadline:
|
|
2509
|
+
alive = []
|
|
2510
|
+
for pid in pids:
|
|
2511
|
+
try:
|
|
2512
|
+
os.kill(pid, 0)
|
|
2513
|
+
alive.append(pid)
|
|
2514
|
+
except ProcessLookupError:
|
|
2515
|
+
pass
|
|
2516
|
+
if not alive:
|
|
2517
|
+
break
|
|
2518
|
+
time.sleep(0.05)
|
|
2519
|
+
else:
|
|
2520
|
+
for pid in alive:
|
|
2521
|
+
try:
|
|
2522
|
+
os.kill(pid, _signal.SIGKILL)
|
|
2523
|
+
print(f"[ArrayView] Force-killed process {pid} on port {args.port}")
|
|
2524
|
+
except ProcessLookupError:
|
|
2525
|
+
pass
|
|
2526
|
+
deadline = time.time() + 2.0
|
|
2527
|
+
while time.time() < deadline:
|
|
2528
|
+
alive = []
|
|
2529
|
+
for pid in pids:
|
|
2530
|
+
try:
|
|
2531
|
+
os.kill(pid, 0)
|
|
2532
|
+
alive.append(pid)
|
|
2533
|
+
except ProcessLookupError:
|
|
2534
|
+
pass
|
|
2535
|
+
if not alive:
|
|
2536
|
+
break
|
|
2537
|
+
time.sleep(0.05)
|
|
2507
2538
|
return
|
|
2508
2539
|
|
|
2509
2540
|
# -- --serve: start a persistent empty server and exit --
|
|
@@ -74,7 +74,7 @@ from arrayview._render import (
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
from arrayview._io import load_data, _SUPPORTED_EXTS, _peek_file_shape
|
|
77
|
-
from arrayview._config import get_viewer_colormaps, get_viewer_theme
|
|
77
|
+
from arrayview._config import get_viewer_colormaps, get_viewer_rounded_panes, get_viewer_theme
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
# ── Vector Field Helpers ──────────────────────────────────────────
|
|
@@ -1025,6 +1025,7 @@ def _build_metadata(session) -> dict:
|
|
|
1025
1025
|
"has_vectorfield": session.vfield is not None,
|
|
1026
1026
|
"vfield_n_times": _vfield_n_times(session),
|
|
1027
1027
|
"is_rgb": session.rgb_axis is not None,
|
|
1028
|
+
"has_source_file": bool(getattr(session, "filepath", None)),
|
|
1028
1029
|
}
|
|
1029
1030
|
target_shape = (
|
|
1030
1031
|
session.spatial_shape if session.rgb_axis is not None else session.shape
|
|
@@ -2130,21 +2131,24 @@ def get_volume_histogram(
|
|
|
2130
2131
|
sid: str,
|
|
2131
2132
|
dim_x: int,
|
|
2132
2133
|
dim_y: int,
|
|
2133
|
-
scroll_dim: int,
|
|
2134
|
+
scroll_dim: int = -1,
|
|
2135
|
+
scroll_dims: str = "",
|
|
2134
2136
|
fixed_indices: str = "",
|
|
2135
2137
|
complex_mode: int = 0,
|
|
2136
2138
|
bins: int = 64,
|
|
2137
2139
|
session: "Session" = Depends(get_session_or_404),
|
|
2138
2140
|
):
|
|
2139
|
-
"""Return a histogram sampled across
|
|
2141
|
+
"""Return a histogram sampled across one or more aggregation dims.
|
|
2140
2142
|
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2143
|
+
*scroll_dims* is a comma-separated list of dim indices to aggregate
|
|
2144
|
+
over (in addition to *dim_x* / *dim_y*). When unset, falls back to
|
|
2145
|
+
the legacy single *scroll_dim* param. The sampler enumerates index
|
|
2146
|
+
combinations across all aggregation dims but caps total samples at
|
|
2147
|
+
~16 via stride-subsampling.
|
|
2144
2148
|
|
|
2145
2149
|
*fixed_indices* is a comma-separated list of ``dim:idx`` pairs that
|
|
2146
|
-
pin non-display, non-
|
|
2147
|
-
first parameter map in qMRI mode).
|
|
2150
|
+
pin non-display, non-aggregation dimensions (e.g. ``"3:0"`` to
|
|
2151
|
+
select the first parameter map in qMRI mode).
|
|
2148
2152
|
"""
|
|
2149
2153
|
|
|
2150
2154
|
# Parse fixed indices
|
|
@@ -2155,27 +2159,59 @@ def get_volume_histogram(
|
|
|
2155
2159
|
d, v = pair.split(":", 1)
|
|
2156
2160
|
fixed[int(d)] = int(v)
|
|
2157
2161
|
|
|
2162
|
+
# Resolve aggregation dims: prefer scroll_dims, fall back to scroll_dim.
|
|
2163
|
+
agg_dims: list[int] = []
|
|
2164
|
+
if scroll_dims:
|
|
2165
|
+
for tok in scroll_dims.split(","):
|
|
2166
|
+
tok = tok.strip()
|
|
2167
|
+
if not tok:
|
|
2168
|
+
continue
|
|
2169
|
+
try:
|
|
2170
|
+
d = int(tok)
|
|
2171
|
+
except ValueError:
|
|
2172
|
+
continue
|
|
2173
|
+
if 0 <= d < len(session.shape) and d != dim_x and d != dim_y and d not in agg_dims:
|
|
2174
|
+
agg_dims.append(d)
|
|
2175
|
+
if not agg_dims and scroll_dim >= 0 and scroll_dim != dim_x and scroll_dim != dim_y:
|
|
2176
|
+
agg_dims = [scroll_dim]
|
|
2177
|
+
|
|
2158
2178
|
# Check cache
|
|
2159
|
-
cache_key = (dim_x, dim_y,
|
|
2179
|
+
cache_key = (dim_x, dim_y, tuple(agg_dims), tuple(sorted(fixed.items())), complex_mode)
|
|
2160
2180
|
if not hasattr(session, "_volume_hist_cache"):
|
|
2161
2181
|
session._volume_hist_cache = {}
|
|
2162
2182
|
cached = session._volume_hist_cache.get(cache_key)
|
|
2163
2183
|
if cached is not None and cached.get("_data_version") == session.data_version:
|
|
2164
2184
|
return cached["result"]
|
|
2165
2185
|
|
|
2166
|
-
#
|
|
2167
|
-
|
|
2186
|
+
# Build sample index lists per aggregation dim, stride-subsampled so
|
|
2187
|
+
# their Cartesian product stays around ~16 samples total.
|
|
2168
2188
|
max_samples = 16
|
|
2169
|
-
if
|
|
2170
|
-
|
|
2189
|
+
if not agg_dims:
|
|
2190
|
+
# No aggregation dims — use the current middle slice only.
|
|
2191
|
+
sample_combos = [tuple()]
|
|
2171
2192
|
else:
|
|
2172
|
-
|
|
2173
|
-
|
|
2193
|
+
per_dim_counts = [session.shape[d] for d in agg_dims]
|
|
2194
|
+
# Target roughly equal sample count per dim so the product ≈ max_samples.
|
|
2195
|
+
# Ceil to avoid zero; clamp to dim size.
|
|
2196
|
+
k = len(agg_dims)
|
|
2197
|
+
per_dim_target = max(1, int(round(max_samples ** (1.0 / k))))
|
|
2198
|
+
sample_per_dim: list[list[int]] = []
|
|
2199
|
+
for n in per_dim_counts:
|
|
2200
|
+
m = min(n, per_dim_target)
|
|
2201
|
+
if n <= m:
|
|
2202
|
+
sample_per_dim.append(list(range(n)))
|
|
2203
|
+
else:
|
|
2204
|
+
step = n / m
|
|
2205
|
+
sample_per_dim.append([int(i * step) for i in range(m)])
|
|
2206
|
+
# Cartesian product with a hard cap.
|
|
2207
|
+
import itertools as _it
|
|
2208
|
+
sample_combos = list(_it.islice(_it.product(*sample_per_dim), max_samples))
|
|
2174
2209
|
|
|
2175
2210
|
pixels = []
|
|
2176
|
-
for
|
|
2211
|
+
for combo in sample_combos:
|
|
2177
2212
|
idx_list = [s // 2 for s in session.shape]
|
|
2178
|
-
|
|
2213
|
+
for d, si in zip(agg_dims, combo):
|
|
2214
|
+
idx_list[d] = si
|
|
2179
2215
|
for d, v in fixed.items():
|
|
2180
2216
|
idx_list[d] = v
|
|
2181
2217
|
raw = extract_slice(session, dim_x, dim_y, idx_list)
|
|
@@ -2333,34 +2369,19 @@ def get_lebesgue_slice(
|
|
|
2333
2369
|
)
|
|
2334
2370
|
|
|
2335
2371
|
|
|
2336
|
-
@app.get("/
|
|
2337
|
-
def
|
|
2372
|
+
@app.get("/export_array/{sid}")
|
|
2373
|
+
def export_array(
|
|
2338
2374
|
sid: str,
|
|
2339
|
-
dim_x: int,
|
|
2340
|
-
dim_y: int,
|
|
2341
|
-
indices: str,
|
|
2342
|
-
complex_mode: int = 0,
|
|
2343
2375
|
save_to_downloads: int = 0,
|
|
2344
2376
|
session: "Session" = Depends(get_session_or_404),
|
|
2345
2377
|
):
|
|
2346
|
-
"""Return the
|
|
2347
|
-
|
|
2348
|
-
The slice is the raw floating-point data (before colormap/LUT), with the
|
|
2349
|
-
complex mode applied (mag/phase/real/imag). Used by the N-key shortcut.
|
|
2350
|
-
|
|
2351
|
-
When save_to_downloads=1 (PyWebView), saves the file directly to ~/Downloads
|
|
2352
|
-
instead of returning it as a download response.
|
|
2353
|
-
"""
|
|
2354
|
-
idx_tuple = tuple(int(v) for v in indices.split(","))
|
|
2355
|
-
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
2356
|
-
data = apply_complex_mode(raw, complex_mode)
|
|
2378
|
+
"""Return the full N-D array as a downloadable .npy file (raw data, no transforms)."""
|
|
2379
|
+
data = np.asarray(session.data)
|
|
2357
2380
|
buf = io.BytesIO()
|
|
2358
2381
|
np.save(buf, data)
|
|
2359
2382
|
buf.seek(0)
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
idx_str = "_".join(str(v) for v in idx_tuple)
|
|
2363
|
-
filename = f"{name_stem}_x{dim_x}_y{dim_y}_{idx_str}.npy"
|
|
2383
|
+
name_stem = (session.name or "array").replace(" ", "_").replace("/", "_")
|
|
2384
|
+
filename = f"{name_stem}.npy"
|
|
2364
2385
|
if save_to_downloads:
|
|
2365
2386
|
import pathlib
|
|
2366
2387
|
downloads = pathlib.Path.home() / "Downloads"
|
|
@@ -4236,6 +4257,8 @@ def get_ui(sid: str = None):
|
|
|
4236
4257
|
_theme_names = ["dark", "light", "solarized", "nord"]
|
|
4237
4258
|
_cfg_theme = get_viewer_theme()
|
|
4238
4259
|
_default_theme_idx = _theme_names.index(_cfg_theme) if _cfg_theme in _theme_names else 0
|
|
4260
|
+
_cfg_rounded = get_viewer_rounded_panes()
|
|
4261
|
+
_default_rounded_panes = "true" if _cfg_rounded else "false"
|
|
4239
4262
|
html = (
|
|
4240
4263
|
_VIEWER_HTML_TEMPLATE.replace("__COLORMAPS__", str(_active_colormaps))
|
|
4241
4264
|
.replace("__COLORMAP_GRADIENT_STOPS__", json.dumps(COLORMAP_GRADIENT_STOPS))
|
|
@@ -4243,6 +4266,7 @@ def get_ui(sid: str = None):
|
|
|
4243
4266
|
.replace("__REAL_MODES__", str(REAL_MODES))
|
|
4244
4267
|
.replace("__ARRAYVIEW_QUERY__", query_val)
|
|
4245
4268
|
.replace("__DEFAULT_THEME_IDX__", str(_default_theme_idx))
|
|
4269
|
+
.replace("__DEFAULT_ROUNDED_PANES__", _default_rounded_panes)
|
|
4246
4270
|
.replace("__BODY_CLASS__", "av-loading" if sid else "")
|
|
4247
4271
|
)
|
|
4248
4272
|
headers = {"Cache-Control": "no-store"}
|