arrayview 0.11.0__tar.gz → 0.12.1__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.11.0 → arrayview-0.12.1}/AGENTS.md +15 -3
- arrayview-0.12.1/CONTRIBUTING.md +93 -0
- arrayview-0.12.1/PKG-INFO +60 -0
- arrayview-0.12.1/README.md +25 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/pyproject.toml +1 -1
- arrayview-0.12.1/scripts/release.sh +138 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/ARCHITECTURE.md +8 -2
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_io.py +65 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_launcher.py +5 -2
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_render.py +6 -2
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_server.py +149 -20
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_session.py +16 -1
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_shell.html +28 -10
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_stdio_server.py +5 -2
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_viewer.html +3240 -2092
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_vscode.py +80 -25
- arrayview-0.12.1/tests/test_command_reachability.py +120 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_mode_roundtrip.py +0 -1
- arrayview-0.12.1/tests/test_nifti_meta.py +59 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/uv.lock +1 -1
- arrayview-0.11.0/.tmp-vsix/extension/extension.js +0 -343
- arrayview-0.11.0/.tmp-vsix/extension/package.json +0 -13
- arrayview-0.11.0/PKG-INFO +0 -137
- arrayview-0.11.0/README.md +0 -102
- arrayview-0.11.0/scripts/release.sh +0 -78
- {arrayview-0.11.0 → arrayview-0.12.1}/.claude/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/.claude/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/.github/workflows/docs.yml +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/.gitignore +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/.python-version +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/LICENSE +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/comparing.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/configuration.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/display.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/index.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/loading.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/logo.png +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/measurement.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/remote.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/docs/viewing.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/matlab/arrayview.m +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/mkdocs.yml +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/plans/webview/LOG.md +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/scripts/demo.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/__init__.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/__main__.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_app.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_config.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_icon.png +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_platform.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/_torch.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/conftest.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_api.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_browser.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_cli.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_config.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_interactions.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_large_arrays.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/test_torch.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/ui_audit.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/tests/visual_smoke.py +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/vscode-extension/LICENSE +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/vscode-extension/extension.js +0 -0
- {arrayview-0.11.0 → arrayview-0.12.1}/vscode-extension/package.json +0 -0
|
@@ -2,6 +2,9 @@ Read `src/arrayview/ARCHITECTURE.md` for codebase orientation.
|
|
|
2
2
|
|
|
3
3
|
# ArrayView
|
|
4
4
|
|
|
5
|
+
I haven't written or read a single line of code in src so when you ask me questions/input,
|
|
6
|
+
keep it simple with some simple examples.
|
|
7
|
+
|
|
5
8
|
## Skills
|
|
6
9
|
|
|
7
10
|
Load the relevant skill before touching the corresponding area.
|
|
@@ -24,6 +27,13 @@ Load the relevant skill before touching the corresponding area.
|
|
|
24
27
|
- For visual/animation features, propose 2-3 options BEFORE implementing
|
|
25
28
|
- UI visibility changes go through reconcilers (`_reconcileUI`/`_reconcileLayout`/`_reconcileCompareState`/`_reconcileCbVisibility`), not inline `style.display` or `classList` toggles in mode functions
|
|
26
29
|
- All colorbar state (animation, window/level, hover, drag) flows through `primaryCb` ColorBar instance — never read/write legacy globals. Multiview colorbars sync via `primaryCb`.
|
|
30
|
+
- Keybinds flow through the command registry (`commands` / `keybinds` in `_viewer.html`), not inline keydown branches. The help overlay auto-generates from command `title` fields — do not hand-edit it.
|
|
31
|
+
|
|
32
|
+
## Contributing
|
|
33
|
+
|
|
34
|
+
Before creating a PR or making user-facing changes, read `CONTRIBUTING.md`. It defines the design
|
|
35
|
+
language, keybinding conventions, overlay/popup patterns, and testing requirements. All PRs that
|
|
36
|
+
touch the viewer must follow it.
|
|
27
37
|
|
|
28
38
|
## Execution
|
|
29
39
|
|
|
@@ -32,9 +42,11 @@ Always use **subagent-driven development** for implementation. Commit completed
|
|
|
32
42
|
## Testing
|
|
33
43
|
|
|
34
44
|
```bash
|
|
35
|
-
uv run pytest tests/test_api.py -v
|
|
36
|
-
uv run pytest tests/test_browser.py -v
|
|
37
|
-
uv run
|
|
45
|
+
uv run pytest tests/test_api.py -v # HTTP API
|
|
46
|
+
uv run pytest tests/test_browser.py -v # Playwright
|
|
47
|
+
uv run pytest tests/test_mode_roundtrip.py -v # mode state round-trip
|
|
48
|
+
uv run pytest tests/test_command_reachability.py -v # command when-clause matrix
|
|
49
|
+
uv run python tests/visual_smoke.py # screenshots
|
|
38
50
|
```
|
|
39
51
|
|
|
40
52
|
After any UI change, use `/ui-consistency-audit` to verify across all modes.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Contributing to arrayview
|
|
2
|
+
|
|
3
|
+
Thanks for your interest. This guide keeps things consistent as more people
|
|
4
|
+
contribute.
|
|
5
|
+
|
|
6
|
+
## Proposing changes
|
|
7
|
+
|
|
8
|
+
For anything user-facing (new shortcut, overlay, layout change), **open an
|
|
9
|
+
issue first**. Include:
|
|
10
|
+
|
|
11
|
+
- What it does, in one sentence
|
|
12
|
+
- Which key triggers it (if any)
|
|
13
|
+
- Which modes it affects (Normal, Multi-view, Compare, Diff, Registration, qMRI)
|
|
14
|
+
- A rough sketch or description of how it looks
|
|
15
|
+
|
|
16
|
+
Bug fixes and internal refactors can go straight to a PR.
|
|
17
|
+
|
|
18
|
+
## Design principles
|
|
19
|
+
|
|
20
|
+
1. **Array fills the screen.** Minimize chrome. UI elements stay hidden or
|
|
21
|
+
dimmed until the user hovers or presses a key, then fade back out.
|
|
22
|
+
|
|
23
|
+
2. **Monospace only.** All text uses the system monospace stack
|
|
24
|
+
(`'SF Mono', ui-monospace, 'Cascadia Code', 'JetBrains Mono', monospace`).
|
|
25
|
+
Never use sans-serif.
|
|
26
|
+
|
|
27
|
+
3. **Colors via CSS custom properties.** Use `var(--surface)`, `var(--text)`,
|
|
28
|
+
`var(--active-dim)`, etc. Never hardcode hex values. The viewer ships four
|
|
29
|
+
themes (dark, light, solarized, nord) and all must work.
|
|
30
|
+
|
|
31
|
+
4. **Yellow for active state.** `--active-dim` (#f5c842 in dark theme) marks
|
|
32
|
+
the currently active element. Don't introduce new accent colors.
|
|
33
|
+
|
|
34
|
+
5. **All six modes.** Every visual feature must be tested across Normal,
|
|
35
|
+
Multi-view (V/v), Compare (B/P), Diff (X), Registration (R), and qMRI (q).
|
|
36
|
+
If your feature only applies to some modes, add explicit mode guards.
|
|
37
|
+
|
|
38
|
+
## Keyboard shortcuts
|
|
39
|
+
|
|
40
|
+
- Check the existing shortcut table (press `?` in the viewer) before picking a
|
|
41
|
+
key. Conflicts will be caught in review but save yourself the round-trip.
|
|
42
|
+
- Single lowercase letters are scarce. Prefer Shift+key or a modifier for new
|
|
43
|
+
features.
|
|
44
|
+
- If a shortcut only makes sense in certain modes, guard it:
|
|
45
|
+
```js
|
|
46
|
+
if (currentMode !== 'compare') return;
|
|
47
|
+
```
|
|
48
|
+
- Document the new shortcut in the help overlay (`#help-overlay` in
|
|
49
|
+
`_viewer.html`).
|
|
50
|
+
|
|
51
|
+
## Popup menus and overlays
|
|
52
|
+
|
|
53
|
+
Several proposals involve popup/context menus. To keep them visually
|
|
54
|
+
consistent:
|
|
55
|
+
|
|
56
|
+
- Background: `var(--surface)`, border: `1px solid var(--border)`,
|
|
57
|
+
border-radius: `var(--radius-lg)`.
|
|
58
|
+
- Dismiss on **Escape** and on clicking outside the popup.
|
|
59
|
+
- No permanent visibility -- show on trigger, hide when done.
|
|
60
|
+
- Keep text small (12-13px) and monospace.
|
|
61
|
+
- Use `var(--active-dim)` for the selected/hovered item, `var(--text)` for
|
|
62
|
+
normal items, `var(--muted)` for secondary info.
|
|
63
|
+
- Animate in with a short opacity+scale transition, not an instant pop.
|
|
64
|
+
|
|
65
|
+
Look at `#uni-picker-box` in `_viewer.html` for a reference implementation.
|
|
66
|
+
|
|
67
|
+
## Testing checklist
|
|
68
|
+
|
|
69
|
+
Before submitting a PR:
|
|
70
|
+
|
|
71
|
+
- [ ] `uv run pytest tests/test_api.py -x` passes
|
|
72
|
+
- [ ] `uv run python tests/visual_smoke.py` passes
|
|
73
|
+
- [ ] If you added UI, update `tests/visual_smoke.py` to cover it
|
|
74
|
+
- [ ] Manually verify in all affected modes (at minimum: Normal + one
|
|
75
|
+
multi-pane mode)
|
|
76
|
+
- [ ] New shortcuts are documented in the help overlay
|
|
77
|
+
|
|
78
|
+
## Dev setup
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone <repo-url>
|
|
82
|
+
cd arrayview
|
|
83
|
+
uv sync
|
|
84
|
+
uv run arrayview tests/ # launch with test data
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Style notes
|
|
88
|
+
|
|
89
|
+
- The frontend lives in a single file: `src/arrayview/_viewer.html`.
|
|
90
|
+
HTML, CSS, and JS are all in there. Keep it that way.
|
|
91
|
+
- Python backend uses `uv` for package management.
|
|
92
|
+
- Commit messages follow conventional commits (`feat:`, `fix:`, `refactor:`,
|
|
93
|
+
etc.).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arrayview
|
|
3
|
+
Version: 0.12.1
|
|
4
|
+
Summary: Fast multi-dimensional array viewer
|
|
5
|
+
Project-URL: Home, https://github.com/oscarvanderheide/arrayview
|
|
6
|
+
Project-URL: Source, https://github.com/oscarvanderheide/arrayview
|
|
7
|
+
Author-email: Oscar <oscarvanderheide@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: array,mri,npy,viewer,visualization
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: fastapi>=0.129.0
|
|
18
|
+
Requires-Dist: h5py>=3.0
|
|
19
|
+
Requires-Dist: matplotlib>=3.9.0
|
|
20
|
+
Requires-Dist: nibabel>=5.3.3
|
|
21
|
+
Requires-Dist: numpy>=2.4.2
|
|
22
|
+
Requires-Dist: pillow>=12.1.1
|
|
23
|
+
Requires-Dist: pyqt5>=5.15; sys_platform == 'linux'
|
|
24
|
+
Requires-Dist: pyqtwebengine>=5.15; sys_platform == 'linux'
|
|
25
|
+
Requires-Dist: python-multipart>=0.0.22
|
|
26
|
+
Requires-Dist: pywebview>=6.1
|
|
27
|
+
Requires-Dist: qmricolors>=0.1.2
|
|
28
|
+
Requires-Dist: qtpy>=2.0; sys_platform == 'linux'
|
|
29
|
+
Requires-Dist: scipy>=1.10
|
|
30
|
+
Requires-Dist: tifffile>=2023.1.1
|
|
31
|
+
Requires-Dist: uvicorn>=0.41.0
|
|
32
|
+
Requires-Dist: websockets>=14.0
|
|
33
|
+
Requires-Dist: zarr>=2.17
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# <img src="docs/logo.png" height="36"> arrayview
|
|
37
|
+
|
|
38
|
+
This is what looking at arrays should feel like.
|
|
39
|
+
|
|
40
|
+
Load up `.npy`, `.nii`, `.h5`, `.mat` and friends. Works locally, in Jupyter, over SSH and VS Code tunnels. Press `?` once you're in.
|
|
41
|
+
|
|
42
|
+
## CLI
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uvx arrayview your_array.npy
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Python
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv add arrayview
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from arrayview import view
|
|
56
|
+
view(arr)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
[docs →](https://oscarvanderheide.github.io/arrayview/)
|
|
60
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# <img src="docs/logo.png" height="36"> arrayview
|
|
2
|
+
|
|
3
|
+
This is what looking at arrays should feel like.
|
|
4
|
+
|
|
5
|
+
Load up `.npy`, `.nii`, `.h5`, `.mat` and friends. Works locally, in Jupyter, over SSH and VS Code tunnels. Press `?` once you're in.
|
|
6
|
+
|
|
7
|
+
## CLI
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uvx arrayview your_array.npy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Python
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add arrayview
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from arrayview import view
|
|
21
|
+
view(arr)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
[docs →](https://oscarvanderheide.github.io/arrayview/)
|
|
25
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
NO_AI=false
|
|
11
|
+
|
|
12
|
+
usage() {
|
|
13
|
+
cat <<EOF
|
|
14
|
+
Usage: $(basename "$0") [OPTIONS]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--bump {major,minor,patch} Version bump type (default: minor)
|
|
18
|
+
--execute Actually run (default is dry-run)
|
|
19
|
+
--no-ai Skip AI release notes, use GitHub's --generate-notes
|
|
20
|
+
-h, --help Show this help
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
while [[ $# -gt 0 ]]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--bump) BUMP="$2"; shift 2 ;;
|
|
27
|
+
--execute) DRY_RUN=false; shift ;;
|
|
28
|
+
--no-ai) NO_AI=true; shift ;;
|
|
29
|
+
-h|--help) usage; exit 0 ;;
|
|
30
|
+
*) echo "Unknown option: $1"; usage; exit 1 ;;
|
|
31
|
+
esac
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
if [[ ! "$BUMP" =~ ^(major|minor|patch)$ ]]; then
|
|
35
|
+
echo "Error: --bump must be major, minor, or patch (got '$BUMP')"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# --- Guard: clean working tree on main ---
|
|
40
|
+
branch=$(git rev-parse --abbrev-ref HEAD)
|
|
41
|
+
if [[ "$branch" != "main" ]]; then
|
|
42
|
+
echo "Error: must be on main (currently on '$branch')"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
47
|
+
echo "Error: working tree is dirty — commit or stash first"
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# --- Bump version ---
|
|
52
|
+
uv version --bump "$BUMP"
|
|
53
|
+
VERSION=$(uv version --short)
|
|
54
|
+
TAG="v${VERSION}"
|
|
55
|
+
|
|
56
|
+
echo "Version bumped to $VERSION (tag: $TAG)"
|
|
57
|
+
|
|
58
|
+
uv lock --quiet
|
|
59
|
+
|
|
60
|
+
run() {
|
|
61
|
+
echo "+ $*"
|
|
62
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
63
|
+
return
|
|
64
|
+
fi
|
|
65
|
+
"$@"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# --- Generate release notes ---
|
|
69
|
+
CLAUDE_BIN="${CLAUDE_BIN:-/Users/oscar/.local/bin/claude}"
|
|
70
|
+
PREV_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "")
|
|
71
|
+
NOTES=""
|
|
72
|
+
|
|
73
|
+
if [[ -n "$PREV_TAG" ]]; then
|
|
74
|
+
COMMITS=$(git log "${PREV_TAG}..HEAD" --oneline)
|
|
75
|
+
else
|
|
76
|
+
COMMITS=$(git log --oneline -20)
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
if [[ "$NO_AI" == false ]] && command -v "$CLAUDE_BIN" &>/dev/null; then
|
|
80
|
+
echo "Generating release notes with Claude..."
|
|
81
|
+
NOTES=$("$CLAUDE_BIN" -p "You are writing release notes for arrayview $TAG (a Python array/image viewer).
|
|
82
|
+
|
|
83
|
+
Here are the commits since the last release ($PREV_TAG):
|
|
84
|
+
|
|
85
|
+
$COMMITS
|
|
86
|
+
|
|
87
|
+
Write concise, user-friendly release notes in this exact format:
|
|
88
|
+
|
|
89
|
+
## What's new in $TAG
|
|
90
|
+
|
|
91
|
+
- **Feature name**: one-sentence description
|
|
92
|
+
|
|
93
|
+
Rules:
|
|
94
|
+
- 5-10 bullet points max — group related commits into one bullet
|
|
95
|
+
- Write for end-users, not developers (no commit hashes, no file names)
|
|
96
|
+
- Use past tense (\"added\", \"fixed\", \"improved\")
|
|
97
|
+
- Skip pure refactors/docs unless they affect user experience
|
|
98
|
+
- Bold the feature name, keep the description to one sentence" 2>/dev/null) || true
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if [[ -z "$NOTES" ]]; then
|
|
102
|
+
if [[ "$NO_AI" == false ]]; then
|
|
103
|
+
echo "AI notes unavailable, falling back to --generate-notes"
|
|
104
|
+
fi
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if [[ "$DRY_RUN" == true && -n "$NOTES" ]]; then
|
|
108
|
+
echo ""
|
|
109
|
+
echo "--- Release notes preview ---"
|
|
110
|
+
echo "$NOTES"
|
|
111
|
+
echo "-----------------------------"
|
|
112
|
+
echo ""
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# --- Commit, tag, push, release ---
|
|
116
|
+
run git add pyproject.toml uv.lock
|
|
117
|
+
run git commit -m "release: $TAG"
|
|
118
|
+
run git push origin main
|
|
119
|
+
run git tag "$TAG"
|
|
120
|
+
run git push origin "$TAG"
|
|
121
|
+
|
|
122
|
+
if [[ -n "$NOTES" ]]; then
|
|
123
|
+
run gh release create "$TAG" \
|
|
124
|
+
--title "$TAG" \
|
|
125
|
+
--notes "$NOTES" \
|
|
126
|
+
--prerelease
|
|
127
|
+
else
|
|
128
|
+
run gh release create "$TAG" \
|
|
129
|
+
--title "$TAG" \
|
|
130
|
+
--generate-notes \
|
|
131
|
+
--prerelease
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
135
|
+
echo ""
|
|
136
|
+
echo "(dry-run — re-run with --execute to apply)"
|
|
137
|
+
git checkout pyproject.toml uv.lock
|
|
138
|
+
fi
|
|
@@ -52,7 +52,7 @@ Detection logic lives in `_platform.py`. Display opening logic lives in `_launch
|
|
|
52
52
|
| `_stdio_server.py` | 791 | Stdio transport for VS Code direct webview — JSON stdin, binary stdout |
|
|
53
53
|
| `_torch.py` | 217 | PyTorch integration: `view_batch()`, `TrainingMonitor` (lazy torch import) |
|
|
54
54
|
| `_vscode.py` | 1014 | VS Code extension install/management, signal-file IPC, shared-memory IPC, browser opening |
|
|
55
|
-
| `_viewer.html` |
|
|
55
|
+
| `_viewer.html` | 15600 | **The entire frontend** — CSS + JS in a single file, all viewing modes |
|
|
56
56
|
| `_shell.html` | 174 | Tab-bar shell for native pywebview — wraps viewer iframes, manages multi-tab sessions |
|
|
57
57
|
|
|
58
58
|
## Frontend (_viewer.html)
|
|
@@ -89,7 +89,7 @@ The frontend is a single self-contained HTML file (~15k lines). No build step, n
|
|
|
89
89
|
| Rendering Pipeline | `updateView()`, play/animate, screenshot capture |
|
|
90
90
|
| ROI and Selection Modes | Rectangle/ellipse ROI drawing, statistics computation |
|
|
91
91
|
| nnInteractive Segmentation | Click-to-segment UI, mask overlay, undo stack |
|
|
92
|
-
| Keyboard Shortcuts |
|
|
92
|
+
| Keyboard Shortcuts | Command registry (`commands` / `keybinds` / `makeContext` / `evalWhen` / `dispatchCommand`) + `/`-triggered command palette. Keydown handler is a thin dispatcher prefix |
|
|
93
93
|
| Mode Transitions | Compare/multiview/qMRI enter/exit, crosshair animation |
|
|
94
94
|
| Scroll, Zoom, and Pan | Mouse wheel slice scroll, pinch zoom, scroll-to-zoom |
|
|
95
95
|
| Immersive Mode, Cross-Fade, and Visual Effects | Zen mode, fullscreen (K), animated transitions |
|
|
@@ -136,6 +136,9 @@ Pill-shaped badges below the canvas showing active visualization transforms. **C
|
|
|
136
136
|
### Dynamic Islands
|
|
137
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
138
|
|
|
139
|
+
### Command Registry
|
|
140
|
+
All keybinds flow through a VS Code-style command registry in `_viewer.html`. Three tables: `commands` (id → `{title, when, run}`), `keybinds` (key+modifiers → command id), and `makeContext(state)` (mode/state flag bag). `dispatchCommand(e)` is wired as a prefix to the keydown handler; on a match it evaluates `when` against the context and runs the command, otherwise falls through. The help overlay is auto-generated from command `title` fields — never hand-edit it. A `/`-triggered command palette fuzzy-searches all commands. Cross-mode enablement is guarded by `tests/test_command_reachability.py`.
|
|
141
|
+
|
|
139
142
|
### Reconcilers
|
|
140
143
|
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
144
|
1. **Unified UI reconciler** — master state enforcer
|
|
@@ -156,6 +159,9 @@ A dedicated daemon thread (`_session.py`) runs all CPU-heavy rendering off the a
|
|
|
156
159
|
### Dynamic Islands
|
|
157
160
|
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
161
|
|
|
162
|
+
### Keybind Changes
|
|
163
|
+
Keybinds live in the `commands` + `keybinds` tables, not in the keydown handler. Adding or changing a keybind means editing those tables and (if needed) extending `makeContext` / `evalWhen`. The help overlay regenerates itself from command `title` fields — do not edit overlay HTML directly.
|
|
164
|
+
|
|
159
165
|
### Layout Debugging
|
|
160
166
|
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
167
|
|
|
@@ -124,6 +124,71 @@ def _select_npz_array(npz, filepath):
|
|
|
124
124
|
print(f" Please enter a number between 1 and {len(entries)}.")
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
def _load_nifti_with_meta(filepath):
|
|
128
|
+
"""Load a NIfTI file, canonical-reorient, return (array, meta).
|
|
129
|
+
|
|
130
|
+
meta is a dict with keys:
|
|
131
|
+
affine : original 4x4 affine (RAS+ mm)
|
|
132
|
+
affine_canonical : 4x4 affine after as_closest_canonical
|
|
133
|
+
voxel_sizes : tuple (sx, sy, sz) in mm, post-reorient
|
|
134
|
+
axis_labels : tuple of 3 strs from {"R","L","A","P","S","I"}
|
|
135
|
+
— positive direction of each canonical axis
|
|
136
|
+
is_oblique : bool — True if rotation part has off-diagonal magnitude > 1e-3
|
|
137
|
+
after normalizing voxel sizes
|
|
138
|
+
"""
|
|
139
|
+
nib = _nib()
|
|
140
|
+
img = nib.load(filepath)
|
|
141
|
+
original_affine = np.asarray(img.affine, dtype=np.float64)
|
|
142
|
+
canon = nib.as_closest_canonical(img)
|
|
143
|
+
affine_canonical = np.asarray(canon.affine, dtype=np.float64)
|
|
144
|
+
|
|
145
|
+
# NOTE: reorient requires materializing axis permutes/flips. .nii.gz is
|
|
146
|
+
# already eager (gzip not seekable), so this is free; .nii loses mmap as a
|
|
147
|
+
# necessary cost to apply the reorient.
|
|
148
|
+
arr = np.asarray(canon.dataobj)
|
|
149
|
+
|
|
150
|
+
rot = affine_canonical[:3, :3]
|
|
151
|
+
voxel_sizes = tuple(float(np.linalg.norm(rot[:, i])) for i in range(3))
|
|
152
|
+
|
|
153
|
+
# Direction of each canonical axis (sign of diagonal after normalizing)
|
|
154
|
+
norm_rot = np.zeros((3, 3))
|
|
155
|
+
for i in range(3):
|
|
156
|
+
if voxel_sizes[i] > 0:
|
|
157
|
+
norm_rot[:, i] = rot[:, i] / voxel_sizes[i]
|
|
158
|
+
pos_labels = ("R", "A", "S")
|
|
159
|
+
neg_labels = ("L", "P", "I")
|
|
160
|
+
axis_labels = tuple(
|
|
161
|
+
pos_labels[i] if norm_rot[i, i] >= 0 else neg_labels[i] for i in range(3)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Oblique = off-diagonal of normalized rotation has |val| > tol
|
|
165
|
+
off_diag_max = 0.0
|
|
166
|
+
for i in range(3):
|
|
167
|
+
for j in range(3):
|
|
168
|
+
if i != j:
|
|
169
|
+
off_diag_max = max(off_diag_max, abs(norm_rot[i, j]))
|
|
170
|
+
is_oblique = bool(off_diag_max > 1e-3)
|
|
171
|
+
|
|
172
|
+
meta = {
|
|
173
|
+
"affine": original_affine,
|
|
174
|
+
"affine_canonical": affine_canonical,
|
|
175
|
+
"voxel_sizes": voxel_sizes,
|
|
176
|
+
"axis_labels": axis_labels,
|
|
177
|
+
"is_oblique": is_oblique,
|
|
178
|
+
}
|
|
179
|
+
return arr, meta
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def load_data_with_meta(filepath):
|
|
183
|
+
"""Like load_data but also returns spatial metadata for NIfTI files.
|
|
184
|
+
|
|
185
|
+
Returns (array, meta_or_None). meta is None for non-NIfTI formats.
|
|
186
|
+
"""
|
|
187
|
+
if filepath.endswith(".nii") or filepath.endswith(".nii.gz"):
|
|
188
|
+
return _load_nifti_with_meta(filepath)
|
|
189
|
+
return load_data(filepath), None
|
|
190
|
+
|
|
191
|
+
|
|
127
192
|
def load_data(filepath):
|
|
128
193
|
if filepath.endswith(".npy"):
|
|
129
194
|
return np.load(filepath, mmap_mode="r")
|
|
@@ -1654,10 +1654,10 @@ def _serve_daemon(
|
|
|
1654
1654
|
).start()
|
|
1655
1655
|
|
|
1656
1656
|
def _load():
|
|
1657
|
-
from arrayview._io import load_data
|
|
1657
|
+
from arrayview._io import load_data, load_data_with_meta
|
|
1658
1658
|
|
|
1659
1659
|
try:
|
|
1660
|
-
data =
|
|
1660
|
+
data, spatial_meta = load_data_with_meta(filepath)
|
|
1661
1661
|
if cleanup:
|
|
1662
1662
|
try:
|
|
1663
1663
|
os.unlink(filepath)
|
|
@@ -1667,6 +1667,9 @@ def _serve_daemon(
|
|
|
1667
1667
|
data, filepath=None if cleanup else filepath, name=name
|
|
1668
1668
|
)
|
|
1669
1669
|
session.sid = sid
|
|
1670
|
+
session.spatial_meta = spatial_meta
|
|
1671
|
+
if spatial_meta is not None:
|
|
1672
|
+
session.original_volume = data
|
|
1670
1673
|
if rgb:
|
|
1671
1674
|
from arrayview._render import _setup_rgb
|
|
1672
1675
|
|
|
@@ -667,10 +667,12 @@ def render_mosaic(
|
|
|
667
667
|
complex_mode=0,
|
|
668
668
|
log_scale=False,
|
|
669
669
|
mosaic_cols=None,
|
|
670
|
+
vmin_override=None,
|
|
671
|
+
vmax_override=None,
|
|
670
672
|
):
|
|
671
673
|
idx_norm = list(idx_tuple)
|
|
672
674
|
idx_norm[dim_z] = 0
|
|
673
|
-
key = (dim_x, dim_y, dim_z, tuple(idx_norm), colormap, dr, complex_mode, log_scale, mosaic_cols)
|
|
675
|
+
key = (dim_x, dim_y, dim_z, tuple(idx_norm), colormap, dr, complex_mode, log_scale, mosaic_cols, vmin_override, vmax_override)
|
|
674
676
|
if key in session.mosaic_cache:
|
|
675
677
|
session.mosaic_cache.move_to_end(key)
|
|
676
678
|
return session.mosaic_cache[key]
|
|
@@ -690,7 +692,9 @@ def render_mosaic(
|
|
|
690
692
|
frames = [np.log1p(np.abs(f)).astype(np.float32) for f in frames]
|
|
691
693
|
all_data = np.stack(frames)
|
|
692
694
|
|
|
693
|
-
if
|
|
695
|
+
if vmin_override is not None and vmax_override is not None:
|
|
696
|
+
vmin, vmax = float(vmin_override), float(vmax_override)
|
|
697
|
+
elif complex_mode == 1 and np.iscomplexobj(session.data):
|
|
694
698
|
vmin, vmax = -float(np.pi), float(np.pi)
|
|
695
699
|
else:
|
|
696
700
|
vmin = float(np.percentile(all_data, 1))
|