arrayview 0.3.0__tar.gz → 0.5.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.5.1/.claude/skills/invocation-consistency/SKILL.md +235 -0
- arrayview-0.5.1/.claude/skills/modes-consistency/SKILL.md +126 -0
- arrayview-0.5.1/.claude/skills/task-workflow/SKILL.md +78 -0
- arrayview-0.5.1/.claude/skills/viewer-ui-checklist/SKILL.md +62 -0
- {arrayview-0.3.0 → arrayview-0.5.1}/.github/workflows/python-publish.yml +4 -4
- {arrayview-0.3.0 → arrayview-0.5.1}/.gitignore +11 -0
- arrayview-0.5.1/.tmp-vsix/extension/extension.js +343 -0
- arrayview-0.5.1/.tmp-vsix/extension/package.json +13 -0
- arrayview-0.5.1/AGENTS.md +219 -0
- arrayview-0.5.1/CHUNK_PLAN.md +179 -0
- arrayview-0.5.1/LOG_LARGE_ARRAYS.md +214 -0
- arrayview-0.5.1/PKG-INFO +411 -0
- arrayview-0.5.1/README.md +376 -0
- arrayview-0.5.1/TODO.md +41 -0
- arrayview-0.5.1/VSCODE_DETECTION.md +82 -0
- arrayview-0.5.1/docs/large-arrays.md +197 -0
- arrayview-0.5.1/docs/superpowers/specs/2026-03-14-todo-batch-design.md +77 -0
- arrayview-0.5.1/plans/speed/LOG.md +219 -0
- arrayview-0.5.1/plans/speed/PLAN.md +266 -0
- arrayview-0.5.1/plans/tunnel-fix/LOG.md +284 -0
- arrayview-0.5.1/plans/tunnel-fix/PLAN.md +148 -0
- arrayview-0.5.1/plans/tunnel-fix-local/LOG.md +65 -0
- arrayview-0.5.1/plans/tunnel-fix-local/PLAN.md +148 -0
- {arrayview-0.3.0 → arrayview-0.5.1}/pyproject.toml +3 -2
- arrayview-0.5.1/scripts/demo.py +61 -0
- arrayview-0.5.1/src/arrayview/__init__.py +5 -0
- arrayview-0.5.1/src/arrayview/_app.py +179 -0
- arrayview-0.5.1/src/arrayview/_icon.png +0 -0
- arrayview-0.5.1/src/arrayview/_io.py +155 -0
- arrayview-0.5.1/src/arrayview/_launcher.py +2048 -0
- arrayview-0.5.1/src/arrayview/_platform.py +374 -0
- arrayview-0.5.1/src/arrayview/_render.py +757 -0
- arrayview-0.5.1/src/arrayview/_server.py +1858 -0
- arrayview-0.5.1/src/arrayview/_session.py +411 -0
- arrayview-0.5.1/src/arrayview/_shell.html +167 -0
- arrayview-0.5.1/src/arrayview/_viewer.html +6837 -0
- arrayview-0.5.1/src/arrayview/_vscode.py +615 -0
- arrayview-0.5.1/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.5.1/tests/make_vectorfield_test_arrays.py +45 -0
- arrayview-0.5.1/tests/test_api.py +1006 -0
- arrayview-0.5.1/tests/test_browser.py +734 -0
- arrayview-0.5.1/tests/test_cli.py +110 -0
- arrayview-0.5.1/tests/test_large_arrays.py +285 -0
- arrayview-0.5.1/tests/test_rgb_pixel_art.py +66 -0
- arrayview-0.5.1/tests/visual_smoke.py +1401 -0
- {arrayview-0.3.0 → arrayview-0.5.1}/uv.lock +12 -1
- arrayview-0.5.1/vscode-extension/LICENSE +21 -0
- arrayview-0.5.1/vscode-extension/extension.js +368 -0
- arrayview-0.5.1/vscode-extension/package.json +23 -0
- arrayview-0.3.0/PKG-INFO +0 -79
- arrayview-0.3.0/README.md +0 -45
- arrayview-0.3.0/src/arrayview/__init__.py +0 -3
- arrayview-0.3.0/src/arrayview/_app.py +0 -1738
- arrayview-0.3.0/src/arrayview/_shell.html +0 -136
- arrayview-0.3.0/src/arrayview/_viewer.html +0 -1179
- arrayview-0.3.0/tests/test_api.py +0 -378
- arrayview-0.3.0/tests/test_browser.py +0 -350
- {arrayview-0.3.0 → arrayview-0.5.1}/.python-version +0 -0
- {arrayview-0.3.0 → arrayview-0.5.1}/LICENSE +0 -0
- {arrayview-0.3.0 → arrayview-0.5.1}/tests/conftest.py +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: invocation-consistency
|
|
3
|
+
description: Use when implementing any server-side, startup, display-opening, or environment-detection feature in arrayview. Ensures the feature works correctly across all six ways arrayview can be launched — CLI, Python script, Jupyter, Julia, VS Code tunnel, and SSH.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ArrayView Invocation Consistency Checklist
|
|
7
|
+
|
|
8
|
+
## Rule
|
|
9
|
+
|
|
10
|
+
Every behavior that depends on *how* arrayview is started (server lifecycle, browser opening, display routing, port forwarding) must be verified across all six invocation paths before it is considered done.
|
|
11
|
+
|
|
12
|
+
## The Six Invocation Paths
|
|
13
|
+
|
|
14
|
+
| Path | Entry point | Key detection | Server model |
|
|
15
|
+
|------|------------|--------------|-------------|
|
|
16
|
+
| **CLI** | `arrayview()` in `_app.py` | — (always CLI path) | `_run_server_subprocess()` or in-process thread |
|
|
17
|
+
| **Python script** | `view(arr)` | `_in_jupyter()` → False | In-process daemon thread |
|
|
18
|
+
| **Jupyter / VS Code interactive** | `view(arr)` | `_in_jupyter()` → True | In-process daemon thread |
|
|
19
|
+
| **Julia (PythonCall)** | `view(arr)` | `_is_julia_env()` → True | Always subprocess (`_view_julia()`) |
|
|
20
|
+
| **VS Code tunnel / SSH remote** | `arrayview()` or `view()` | `_is_vscode_remote()` → True | Subprocess server |
|
|
21
|
+
| **Plain SSH (no VS Code)** | `arrayview()` or `view()` | `SSH_CONNECTION` set, no VS Code remote | In-process or subprocess; prints port-forward hint |
|
|
22
|
+
|
|
23
|
+
## Key Detection Functions (all in `_app.py`)
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
_in_jupyter() # ipykernel present → display inline IFrame
|
|
27
|
+
_in_vscode_terminal() # TERM_PROGRAM=vscode OR VSCODE_IPC_HOOK_CLI → use Simple Browser
|
|
28
|
+
_is_vscode_remote() # tunnel/SSH remote with VS Code server → subprocess + extension routing
|
|
29
|
+
_is_julia_env() # juliacall in sys.modules or julia in sys.executable → subprocess
|
|
30
|
+
_can_native_window() # pywebview available + not remote + display present → native window
|
|
31
|
+
_find_vscode_ipc_hook() # walk process tree for VSCODE_IPC_HOOK_CLI (stripped by uv run)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Feature Categories & Where Each Path Diverges
|
|
35
|
+
|
|
36
|
+
### Server Lifecycle
|
|
37
|
+
|
|
38
|
+
| Path | Server model | Implication |
|
|
39
|
+
|------|-------------|-------------|
|
|
40
|
+
| CLI | Background thread in `main()` → blocks until Ctrl-C | `_shutdown_event` drives cleanup |
|
|
41
|
+
| Python script | `_start_server_thread()` → daemon thread | Dies with caller process |
|
|
42
|
+
| Jupyter | `_start_server_thread()` → daemon thread | Persists across cells; port reuse matters |
|
|
43
|
+
| Julia | `_view_julia()` spawns detached subprocess | Caller (Julia) may not share GIL; subprocess manages its own lifecycle |
|
|
44
|
+
| VS Code remote | Subprocess via `_run_server_subprocess()` | Caller exits; server stays alive independently |
|
|
45
|
+
| Plain SSH | In-process or subprocess | No GUI; user must port-forward manually |
|
|
46
|
+
|
|
47
|
+
When changing server startup or shutdown logic, check whether daemonized threads, subprocess reaping, and port reuse are all handled correctly for each path.
|
|
48
|
+
|
|
49
|
+
### Browser / Display Opening
|
|
50
|
+
|
|
51
|
+
| Path | Opens how | Key function |
|
|
52
|
+
|------|-----------|-------------|
|
|
53
|
+
| CLI local | Native window (`pywebview`) OR system browser | `_open_webview_with_fallback()` → `_open_browser()` |
|
|
54
|
+
| Python script local | Same as CLI local | Same |
|
|
55
|
+
| Jupyter | Inline IFrame via `IPython.display.IFrame` | `view()` returns IFrame object |
|
|
56
|
+
| Julia | System browser via subprocess signal | `_view_julia()` writes signal file |
|
|
57
|
+
| VS Code terminal (local) | VS Code Simple Browser via extension | `_open_via_signal_file()` + `_ensure_vscode_extension()` |
|
|
58
|
+
| VS Code tunnel/remote | VS Code Simple Browser on *client* side | signal file + extension + port auto-forward |
|
|
59
|
+
| Plain SSH | Prints `ssh -L <port>:localhost:<port>` hint | `_open_browser()` fallback |
|
|
60
|
+
|
|
61
|
+
When changing URL construction, port logic, or display routing, trace through `_open_browser()` and its callers for each path — particularly the VS Code tunnel path where the *client's* VS Code picks up the signal file.
|
|
62
|
+
|
|
63
|
+
### VS Code Extension (`arrayview-opener.vsix`)
|
|
64
|
+
|
|
65
|
+
- Extension is auto-installed by `_ensure_vscode_extension()` when `_in_vscode_terminal()` is True
|
|
66
|
+
- Version must match `_VSCODE_EXT_VERSION` in `_app.py` AND `vscode-extension/package.json`
|
|
67
|
+
- After rebuilding the VSIX: update `_VSCODE_EXT_VERSION` in `_app.py`
|
|
68
|
+
- IPC hook may be stripped by `uv run` → `_find_vscode_ipc_hook()` walks parent process env
|
|
69
|
+
- Tunnel path: extension must configure `remote.portsAttributes` to make port public/silent
|
|
70
|
+
|
|
71
|
+
### Port & URL Construction
|
|
72
|
+
|
|
73
|
+
- Always use `localhost` (not `127.0.0.1`) so VS Code port-forwarding works
|
|
74
|
+
- Port default: `8123`; CLI: `--port` flag; `view()`: `port=` kwarg
|
|
75
|
+
- Compare mode URLs include `?compare_sid=...&compare_sids=...`
|
|
76
|
+
- Overlay URLs include `?overlay_sid=...`
|
|
77
|
+
- Mosaic/qMRI state is in the JS (not URL params); no server changes needed for those
|
|
78
|
+
|
|
79
|
+
### Julia-Specific Constraints
|
|
80
|
+
|
|
81
|
+
- GIL conflicts: never run server in-process when `_is_julia_env()` is True
|
|
82
|
+
- `_view_julia()` starts a detached subprocess running `arrayview_server` CLI tool
|
|
83
|
+
- Array data is serialized to a temp `.npy` file and loaded by the subprocess
|
|
84
|
+
- Julia arrays use PythonCall → numpy conversion before any arrayview API call
|
|
85
|
+
- No interactive stdin in subprocesses; all params must be encoded in CLI flags or files
|
|
86
|
+
|
|
87
|
+
### Jupyter-Specific Constraints
|
|
88
|
+
|
|
89
|
+
- `_in_jupyter()` returns True for ipykernel (VS Code notebook, JupyterLab, classic Notebook)
|
|
90
|
+
- Julia's IJulia kernel is NOT ipykernel → `_in_jupyter()` returns False in Julia notebooks (use Julia path instead)
|
|
91
|
+
- `inline=True` → returns `IPython.display.IFrame`; caller must return it from the cell for display
|
|
92
|
+
- Port reuse: calling `view()` multiple times reuses same port and server if already running
|
|
93
|
+
- `window='native'` override still works in Jupyter; viewer opens as separate window
|
|
94
|
+
|
|
95
|
+
## Step-by-Step Checklist
|
|
96
|
+
|
|
97
|
+
When implementing a new feature, ask:
|
|
98
|
+
|
|
99
|
+
1. **Does it touch server startup, teardown, or port selection?**
|
|
100
|
+
- Test CLI (`uv run arrayview file.npy`) — server must start and terminate cleanly
|
|
101
|
+
- Test Python script (`python script.py`) — daemon thread must not orphan
|
|
102
|
+
- Test Jupyter cell — repeated calls must not fail on port-already-in-use
|
|
103
|
+
- Check Julia path — `_view_julia()` must pass any new params via CLI flag or file
|
|
104
|
+
|
|
105
|
+
2. **Does it touch browser/display opening?**
|
|
106
|
+
- Test local native window (macOS) — `pywebview` opens correctly
|
|
107
|
+
- Test local system browser — `open http://localhost:8123` path works
|
|
108
|
+
- Test VS Code terminal — Simple Browser opens via extension signal file
|
|
109
|
+
- Test VS Code tunnel — route reaches client-side Simple Browser, not remote host browser
|
|
110
|
+
- Verify `_ensure_vscode_extension()` installs/updates if VSIX changed
|
|
111
|
+
|
|
112
|
+
3. **Does it touch URL construction or query params?**
|
|
113
|
+
- Verify `localhost` (not `127.0.0.1`)
|
|
114
|
+
- Verify all modes still get the right params (compare, overlay, vectorfield)
|
|
115
|
+
|
|
116
|
+
4. **Does it touch environment detection?**
|
|
117
|
+
- Check the detection order in `view()`: Julia → Jupyter → VS Code remote → VS Code terminal → local
|
|
118
|
+
- Any new detection must not break the fallback chain for paths it shouldn't match
|
|
119
|
+
- `_find_vscode_ipc_hook()` walks up to 12 parent processes — new subprocess wrappers may need the same treatment
|
|
120
|
+
|
|
121
|
+
5. **Does it change the VS Code extension?**
|
|
122
|
+
- Rebuild VSIX: `cd vscode-extension && vsce package -o ../src/arrayview/arrayview-opener.vsix`
|
|
123
|
+
- Bump `_VSCODE_EXT_VERSION` in `_app.py` and `vscode-extension/package.json` together
|
|
124
|
+
- Test install in a fresh VS Code profile
|
|
125
|
+
|
|
126
|
+
## Validation Matrix
|
|
127
|
+
|
|
128
|
+
After implementing, run through this matrix manually or in CI:
|
|
129
|
+
|
|
130
|
+
| Check | CLI | Python script | Jupyter | Julia | VS Code local | VS Code tunnel |
|
|
131
|
+
|-------|-----|--------------|---------|-------|--------------|----------------|
|
|
132
|
+
| Server starts | ✓? | ✓? | ✓? | ✓? | ✓? | ✓? |
|
|
133
|
+
| Array loads & renders | ✓? | ✓? | ✓? | ✓? | ✓? | ✓? |
|
|
134
|
+
| Display opens (window/browser/inline) | ✓? | ✓? | inline IFrame | browser | Simple Browser | Simple Browser on client |
|
|
135
|
+
| Ctrl-C / kernel stop cleans up | ✓? | ✓? | ✓? | ✓? | ✓? | ✓? |
|
|
136
|
+
| Compare mode works (if file-based) | ✓? | N/A | N/A | N/A | ✓? | ✓? |
|
|
137
|
+
|
|
138
|
+
Quick automated checks:
|
|
139
|
+
```bash
|
|
140
|
+
uv run pytest tests/test_api.py -x # API contract
|
|
141
|
+
uv run pytest tests/test_cli.py -x # CLI entry point
|
|
142
|
+
uv run python -c "from arrayview import view; import numpy as np; view(np.zeros((10,10)))"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## tmux and VS Code Terminal Detection
|
|
146
|
+
|
|
147
|
+
tmux is the single most common reason `_find_vscode_ipc_hook()` fails even though the user IS in a VS Code terminal.
|
|
148
|
+
|
|
149
|
+
### Why the ancestor-walk fails inside tmux
|
|
150
|
+
|
|
151
|
+
Normal process tree (no tmux):
|
|
152
|
+
```
|
|
153
|
+
VS Code terminal shell (has VSCODE_IPC_HOOK_CLI)
|
|
154
|
+
└─ uv run python / arrayview ← walks up, finds it ✓
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Process tree inside tmux:
|
|
158
|
+
```
|
|
159
|
+
VS Code terminal shell (has VSCODE_IPC_HOOK_CLI)
|
|
160
|
+
└─ tmux (client process — also has VSCODE_IPC_HOOK_CLI)
|
|
161
|
+
↕ socket IPC (not parent/child)
|
|
162
|
+
tmux-server (independent daemon — does NOT have VSCODE_IPC_HOOK_CLI)
|
|
163
|
+
└─ pane shell
|
|
164
|
+
└─ uv run python / arrayview ← walks up through tmux-server, never finds it ✗
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The arrayview process's parent chain goes through `tmux-server`, which is an independent daemon that was started at some point and does NOT inherit the VS Code terminal's environment.
|
|
168
|
+
|
|
169
|
+
### Why `tmux show-environment` fails
|
|
170
|
+
|
|
171
|
+
`tmux show-environment VSCODE_IPC_HOOK_CLI` queries tmux's *session* environment. tmux only tracks variables listed in its `update-environment` option. The default list is:
|
|
172
|
+
```
|
|
173
|
+
DISPLAY SSH_ASKPASS SSH_AUTH_SOCK SSH_AGENT_PID SSH_CONNECTION WINDOWID XAUTHORITY
|
|
174
|
+
```
|
|
175
|
+
`VSCODE_IPC_HOOK_CLI` is **not** in the default list, so it is never copied into tmux's session environment. The command returns `-VSCODE_IPC_HOOK_CLI` (meaning unset) even when every client has it.
|
|
176
|
+
|
|
177
|
+
### Why `#{client_pid}` alone is unreliable
|
|
178
|
+
|
|
179
|
+
`tmux display-message -p '#{client_pid}'` returns **one** client PID (the "current" client). This fails when:
|
|
180
|
+
- Multiple clients are attached to the session (e.g., shared session, or user has the session open in both VS Code and a regular terminal)
|
|
181
|
+
- A session was created outside VS Code and then attached from VS Code
|
|
182
|
+
|
|
183
|
+
### Correct approach: enumerate ALL clients for the current session
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
# Get current session ID
|
|
187
|
+
session_id = subprocess.run(["tmux", "display-message", "-p", "#{session_id}"], ...).stdout.strip()
|
|
188
|
+
|
|
189
|
+
# List ALL clients for this session
|
|
190
|
+
client_pids = subprocess.run(
|
|
191
|
+
["tmux", "list-clients", "-t", session_id, "-F", "#{client_pid}"], ...
|
|
192
|
+
).stdout.strip().splitlines()
|
|
193
|
+
|
|
194
|
+
# Check each client's environment
|
|
195
|
+
for pid_str in client_pids:
|
|
196
|
+
val = _ipc_from_pid(int(pid_str))
|
|
197
|
+
if val and os.path.exists(val):
|
|
198
|
+
return val # found VSCODE_IPC_HOOK_CLI in one of the VS Code clients ✓
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`list-clients -t <session_id>` scopes to the current session only, so we don't accidentally pick up a `VSCODE_IPC_HOOK_CLI` from a completely different VS Code window on another session.
|
|
202
|
+
|
|
203
|
+
### Detection order in `_find_vscode_ipc_hook()` (when TERM_PROGRAM=tmux)
|
|
204
|
+
|
|
205
|
+
1. `tmux show-environment VSCODE_IPC_HOOK_CLI` — cheap, works if user set `update-environment`
|
|
206
|
+
2. `tmux list-clients -t <session_id> -F '#{client_pid}'` → `ps ewwww` each — robust, handles all cases
|
|
207
|
+
|
|
208
|
+
### What `--diagnose` should show when working
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
"detection": {
|
|
212
|
+
"in_vscode_terminal": true,
|
|
213
|
+
"vscode_ipc_hook_recovered": "/var/folders/.../vscode-ipc-xxx.sock"
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
If `vscode_ipc_hook_recovered` is `null` despite being in VS Code+tmux, the strategies above both failed. Check:
|
|
218
|
+
- Is the tmux session detached (no clients attached)?
|
|
219
|
+
- Is `ps ewwww -p <client_pid>` working on this OS? (Some Linux distros restrict it)
|
|
220
|
+
- Is the IPC socket path valid (does the `.sock` file exist)?
|
|
221
|
+
|
|
222
|
+
### Red flags for tmux detection
|
|
223
|
+
|
|
224
|
+
- "I fixed it with `#{client_pid}`" → only works with one client; use `list-clients` instead
|
|
225
|
+
- "I fixed it with `show-environment`" → only works if user customised `update-environment`; both strategies needed
|
|
226
|
+
- "It works for me but not for the user" → check if they have multiple clients or a session created before VS Code
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
- "I changed how the server starts but only tested CLI" → test all paths
|
|
231
|
+
- "The feature works locally but not in tunnel" → check `_is_vscode_remote()` path and port exposure
|
|
232
|
+
- "I used `127.0.0.1` in the URL" → use `localhost` so VS Code port-forwarding intercepts it
|
|
233
|
+
- "Julia works but I only tested the Python path" → Julia always uses subprocess — test `_view_julia()` explicitly
|
|
234
|
+
- "I bumped the extension version in package.json but not `_VSCODE_EXT_VERSION`" → they must stay in sync
|
|
235
|
+
- "The server starts but leaves an orphan process after Ctrl-C" → check `_shutdown_event`, signal handlers, and subprocess reaping
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modes-consistency
|
|
3
|
+
description: Use when implementing any visual feature in arrayview that touches canvas rendering, zoom, eggs, colorbars, keyboard shortcuts, or layout. Ensures the feature is applied consistently across ALL viewing modes, not just the one being worked on.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ArrayView Modes Consistency Checklist
|
|
7
|
+
|
|
8
|
+
## Rule
|
|
9
|
+
|
|
10
|
+
Every visual feature in `_viewer.html` MUST be implemented for all applicable modes. Implementing it only in one mode and shipping is a bug, not a partial feature.
|
|
11
|
+
|
|
12
|
+
## The Six Modes
|
|
13
|
+
|
|
14
|
+
| Mode | State flag | Scale function | Entry key |
|
|
15
|
+
|------|-----------|---------------|-----------|
|
|
16
|
+
| **Normal** | (default) | `scaleCanvas()` | — |
|
|
17
|
+
| **Multi-view** | `multiViewActive` | `mvScaleAllCanvases()` | V / v |
|
|
18
|
+
| **Compare** | `compareActive` | `compareScaleCanvases()` | B / P |
|
|
19
|
+
| **Diff** | `diffMode > 0` (inside compare) | `compareScaleCanvases()` | X (in compare) |
|
|
20
|
+
| **Registration** | `registrationMode` (inside compare) | `compareScaleCanvases()` | R (in compare) |
|
|
21
|
+
| **qMRI** | `qmriActive` | `qvScaleAllCanvases()` | q |
|
|
22
|
+
|
|
23
|
+
Note: Overlay mode (`overlay_sid` URL param) is composited server-side into the normal frame — check backend rendering too.
|
|
24
|
+
|
|
25
|
+
## Common Feature Categories & Where to Implement
|
|
26
|
+
|
|
27
|
+
### Zoom / Canvas Sizing
|
|
28
|
+
|
|
29
|
+
Every mode has a dedicated scale function. When changing zoom behavior, check **all four**:
|
|
30
|
+
|
|
31
|
+
- `scaleCanvas(w, h)` [Normal] — applies `baseScale * userZoom`, snaps `userZoom` to cap at top
|
|
32
|
+
- `mvScaleAllCanvases()` [Multi-view] — iterates `mvViews`, caps `userZoom` via per-view calculation
|
|
33
|
+
- `compareScaleCanvases()` [Compare / Diff / Registration] — grid layout, caps `userZoom` before applying
|
|
34
|
+
- `qvScaleAllCanvases()` [qMRI] — grid layout for parameter maps
|
|
35
|
+
|
|
36
|
+
**Cap pattern** (already applied in compare): compute `capZoom` from max allowable scale across all panes, then `if (userZoom > capZoom) userZoom = capZoom;` before the sizing loop.
|
|
37
|
+
|
|
38
|
+
### Eggs (Mode Indicator Dots — LOG, complex, mask, overlay badges)
|
|
39
|
+
|
|
40
|
+
All eggs are positioned by `positionEggs()`. The function branches by mode. When changing egg placement rules, check:
|
|
41
|
+
|
|
42
|
+
- Normal branch: uses `#slim-cb-wrap` bounding box if visible, else canvas bottom
|
|
43
|
+
- Multi-view branch: uses `#mv-cb-wrap` bounding box if present, else estimates 36px below panes
|
|
44
|
+
- Compare branch: walks `.compare-canvas-wrap` rects to find the tallest pane bottom
|
|
45
|
+
- qMRI branch: uses `.qv-canvas-wrap` rects + 36px estimate
|
|
46
|
+
|
|
47
|
+
When adding new egg types or changing vertical anchor, update all four branches.
|
|
48
|
+
|
|
49
|
+
### Colorbar / Window-Level Interaction
|
|
50
|
+
|
|
51
|
+
Colorbars are drawn by separate per-mode functions — there is no shared abstraction:
|
|
52
|
+
|
|
53
|
+
| Mode | Colorbar function(s) |
|
|
54
|
+
|------|---------------------|
|
|
55
|
+
| Normal | `drawSlimColorbar(markerFrac)` |
|
|
56
|
+
| Compare | `drawComparePaneCb(idx)` + `drawAllComparePaneCbs()` |
|
|
57
|
+
| Diff (in compare) | `drawDiffPaneCb(vmin, vmax)` |
|
|
58
|
+
| Registration (in compare) | `drawRegBlendCb()` |
|
|
59
|
+
| Multi-view | `drawMvCbs()` (called inside `mvScaleAllCanvases`) |
|
|
60
|
+
| qMRI | Drawn inline per view in `qvRender()` |
|
|
61
|
+
|
|
62
|
+
When adding colorbar interactivity (e.g., drag, scroll), check which colorbars the feature should apply to, and implement it per-element.
|
|
63
|
+
|
|
64
|
+
### Keyboard Shortcuts
|
|
65
|
+
|
|
66
|
+
The `keydown` handler on `#keyboard-sink` dispatches by active mode. New shortcuts must:
|
|
67
|
+
|
|
68
|
+
1. Not conflict with mode-specific keys — check the table in _viewer.html's keyboard section
|
|
69
|
+
2. Have an explicit guard when the shortcut only makes sense in specific modes (e.g., `if (!compareActive) return;`)
|
|
70
|
+
3. Fall through correctly when multiple modes are active (e.g., `registrationMode` requires `compareActive`)
|
|
71
|
+
|
|
72
|
+
### Canvas Elements Present Per Mode
|
|
73
|
+
|
|
74
|
+
| Mode | Canvas elements |
|
|
75
|
+
|------|----------------|
|
|
76
|
+
| Normal | `#canvas` |
|
|
77
|
+
| Multi-view | `.mv-canvas` × 3 (inside `.mv-view`) |
|
|
78
|
+
| Compare | `.compare-canvas` × 2–6 (inside `.compare-canvas-inner`) |
|
|
79
|
+
| Diff | `#compare-diff-canvas` (shown only when `diffMode > 0` and `compareSids.length === 2`) |
|
|
80
|
+
| Registration | 3rd `.compare-canvas` = blended overlay; `compareRegistrationFrame` drives it |
|
|
81
|
+
| qMRI | `.qv-canvas` × 3–6 |
|
|
82
|
+
|
|
83
|
+
Features that attach event listeners to canvas elements (mouse, wheel, etc.) must attach to the correct per-mode set of canvases, not just `document.getElementById('canvas')`.
|
|
84
|
+
|
|
85
|
+
## Step-by-Step Implementation Checklist
|
|
86
|
+
|
|
87
|
+
When implementing a new feature, run through this list:
|
|
88
|
+
|
|
89
|
+
1. **Identify applicable modes**: Does this feature affect canvas sizing? colorbars? eggs? cursor? Yes → all modes.
|
|
90
|
+
|
|
91
|
+
2. **Normal mode** — implement first, verify it works.
|
|
92
|
+
|
|
93
|
+
3. **Compare mode** (includes Diff and Registration) — `compareScaleCanvases()` or the relevant compare-specific functions.
|
|
94
|
+
|
|
95
|
+
4. **Multi-view mode** — `mvScaleAllCanvases()`, or per-view listener attachment if it's an event feature.
|
|
96
|
+
|
|
97
|
+
5. **qMRI mode** — `qvScaleAllCanvases()`, or inline in `qvRender()` if it's a per-pane colorbar feature.
|
|
98
|
+
|
|
99
|
+
6. **Overlay mode (backend)** — if the feature affects how frames are composited, check the `/slice`, `/frame`, `/diff`, and `/mosaic` endpoints in `_app.py`. Overlay compositing happens in `_composite_overlay_mask()` before PNG encoding.
|
|
100
|
+
|
|
101
|
+
7. **Mosaic / z-grid** — when feature involves the diff endpoint (`/diff`), check that `dim_z` is correctly passed and handled server-side (backend `get_diff` must produce a mosaic grid when `dim_z >= 0`).
|
|
102
|
+
|
|
103
|
+
8. **State snapshot** — if the feature introduces a new state variable, add it to `collectStateSnapshot()` and `applyStateSnapshot()` so it is preserved across page reloads and compare mode transitions.
|
|
104
|
+
|
|
105
|
+
## Red Flags — STOP
|
|
106
|
+
|
|
107
|
+
- "I only changed it for normal mode, the others are TODO" → implement all applicable modes now
|
|
108
|
+
- "`compareScaleCanvases` and `scaleCanvas` now behave differently for zoom capping" → pick one pattern and apply to both
|
|
109
|
+
- "I added a canvas listener to `#canvas` and now it doesn't work in compare" → multi-canvas modes have different DOM structures
|
|
110
|
+
- "I only pass `dim_z` to some endpoints but not others" → check every endpoint that renders an image and needs the mosaic path
|
|
111
|
+
|
|
112
|
+
## Quick Sanity Test
|
|
113
|
+
|
|
114
|
+
After implementing, run:
|
|
115
|
+
```
|
|
116
|
+
uv run pytest tests/test_api.py -x
|
|
117
|
+
uv run python tests/visual_smoke.py # then review smoke_output/
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
And manually verify:
|
|
121
|
+
- [ ] Feature works in normal view
|
|
122
|
+
- [ ] Feature works in compare (press B → pick second array)
|
|
123
|
+
- [ ] Feature works in diff (press X while in compare)
|
|
124
|
+
- [ ] Feature works in multi-view (press v)
|
|
125
|
+
- [ ] Feature works in qMRI if applicable (press q — needs array with 3–6 dim of size 3–6)
|
|
126
|
+
- [ ] Eggs remain correctly anchored below canvases in each mode after feature is applied
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: task-workflow
|
|
3
|
+
description: Enforce one-commit-per-TODO-item workflow and required collateral updates (README/help/tests/CHANGELOG) for arrayview feature/fix tasks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ArrayView Task Workflow Skill
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Use this skill whenever implementing or fixing a TODO item. It enforces a disciplined workflow so each TODO becomes a single, self-contained commit (and ideally a single PR) that also updates documentation, help text, and tests.
|
|
11
|
+
|
|
12
|
+
## Rule
|
|
13
|
+
|
|
14
|
+
Every completed TODO item must:
|
|
15
|
+
|
|
16
|
+
- Be implemented in a single commit with a clear commit message (see Commit Message format below).
|
|
17
|
+
- Include or update automated tests that validate the feature/fix where possible.
|
|
18
|
+
- Update `README.md` or the in-app help overlay if usage or shortcuts changed.
|
|
19
|
+
- Update `tests/visual_smoke.py` for UI/layout changes (add a numbered scenario and screenshot capture).
|
|
20
|
+
- Add a short entry to `CHANGELOG.md` or `AGENTS.md` (if CHANGELOG.md doesn't exist, add to `AGENTS.md` under a "Changelog" or the Skills section).
|
|
21
|
+
|
|
22
|
+
If any of these steps cannot be completed (e.g., untestable UI dialog that requires human review), the implementer must document the reason in the PR description and add a smoke-test TODO row in `tests/visual_smoke.py` with `✗ (reason)`.
|
|
23
|
+
|
|
24
|
+
## Commit Message Format
|
|
25
|
+
|
|
26
|
+
- Use a concise, searchable prefix describing the TODO ID or short label, then a clear subject, then an optional body.
|
|
27
|
+
- Recommended: `todo: <short-label>: <one-line-summary>`
|
|
28
|
+
- Example: `todo: picker-two-column: add two-column compare picker and shape-filtering`
|
|
29
|
+
|
|
30
|
+
Include a bullet list in the commit body showing collateral updates, e.g.:
|
|
31
|
+
|
|
32
|
+
- Tests: `tests/test_picker.py` and `tests/visual_smoke.py#NN`
|
|
33
|
+
- Docs: `README.md` updated section "Compare Picker"
|
|
34
|
+
- Changelog: `AGENTS.md` / `CHANGELOG.md`
|
|
35
|
+
|
|
36
|
+
## Checklist (enforced by this skill)
|
|
37
|
+
|
|
38
|
+
For each TODO item, ensure the following before marking the task done:
|
|
39
|
+
|
|
40
|
+
- [ ] Code implements the feature/fix and passes `python -m mccabe`/lint checks (if configured).
|
|
41
|
+
- [ ] Unit tests or API tests added/updated in `tests/`.
|
|
42
|
+
- [ ] If UI changes, `tests/visual_smoke.py` updated with a numbered scenario and `_shot()` call.
|
|
43
|
+
- [ ] README or in-app help overlay (`#help-overlay` content in `src/arrayview/_viewer.html`) updated if usage changed.
|
|
44
|
+
- [ ] `CHANGELOG.md` or `AGENTS.md` updated with a one-line summary.
|
|
45
|
+
- [ ] Commit message follows the format above and lists affected files.
|
|
46
|
+
- [ ] Run the core checks locally:
|
|
47
|
+
- `uv run pytest tests/test_api.py -q`
|
|
48
|
+
- `uv run python tests/visual_smoke.py` (and review `tests/smoke_output/`)
|
|
49
|
+
|
|
50
|
+
## How to use
|
|
51
|
+
|
|
52
|
+
1. Create a branch for the TODO item: `git checkout -b todo/<short-label>`.
|
|
53
|
+
2. Implement the change and add tests and docs as required.
|
|
54
|
+
3. Run tests and smoke script locally until passing (or document failures).
|
|
55
|
+
4. Stage and commit only the files relevant to this TODO with a single commit message as defined above.
|
|
56
|
+
5. Push and open a PR.
|
|
57
|
+
|
|
58
|
+
## Special cases
|
|
59
|
+
|
|
60
|
+
- If a TODO necessarily spans multiple commits (e.g., large refactor), use a temporary feature branch with descriptive commits, then squash into a single commit before merging. The PR must include a description explaining why squashing is required and ensure tests/docs are included in the final squashed commit.
|
|
61
|
+
|
|
62
|
+
- If a fix must touch unrelated files (rare), keep those touches minimal and include an explanation in the commit body.
|
|
63
|
+
|
|
64
|
+
## Automation hints for maintainers
|
|
65
|
+
|
|
66
|
+
- Prefer adding a simple CI check that asserts commits touching `src/arrayview/` are accompanied by changes in `tests/` or `README.md`. This can be implemented as a lightweight script in `.github/workflows/`.
|
|
67
|
+
|
|
68
|
+
- Encourage using `git commit --no-verify -m "..."` only when CI is temporarily failing for reasons unrelated to the task; prefer fixing CI first.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## Red Flags — STOP
|
|
72
|
+
|
|
73
|
+
- "I'll do tests later" — must add tests or justify why not and add smoke test TODO entry.
|
|
74
|
+
- "I'll bundle multiple unrelated TODOs into one commit" — split them or squash locally, but final PR should present one item per commit semantics.
|
|
75
|
+
- "Docs unchanged although usage changed" — update README/help or explain in PR why docs remain unchanged.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# End of skill
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: viewer-ui-checklist
|
|
3
|
+
description: Use when adding keyboard shortcuts, changing layout, or making any UI change to arrayview. Ensures visual_smoke.py stays in sync.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ArrayView UI Checklist
|
|
7
|
+
|
|
8
|
+
## Rule
|
|
9
|
+
|
|
10
|
+
Every UI change to arrayview MUST be reflected in `tests/visual_smoke.py` before the task is complete.
|
|
11
|
+
|
|
12
|
+
## What counts as a UI change
|
|
13
|
+
|
|
14
|
+
- New keyboard shortcut
|
|
15
|
+
- Changed keyboard shortcut behavior
|
|
16
|
+
- New view mode or display mode
|
|
17
|
+
- Layout changes (canvas sizing, colorbar position, overlays)
|
|
18
|
+
- New overlay, dialog, or panel
|
|
19
|
+
|
|
20
|
+
## Steps (mandatory, in order)
|
|
21
|
+
|
|
22
|
+
1. **Update coverage table** at the top of `visual_smoke.py`
|
|
23
|
+
- If shortcut is now testable: change `✗` to `✓ NN` with scenario number
|
|
24
|
+
- If new shortcut: add a row
|
|
25
|
+
- Mark untestable shortcuts with `✗ (reason)`
|
|
26
|
+
|
|
27
|
+
2. **Add or update scenario** in `run_smoke()` with a numbered section comment
|
|
28
|
+
- Follow the existing pattern: `# ── NN: description ──────`
|
|
29
|
+
- Capture at least one screenshot with `_shot(page, "NN_descriptive_name")`
|
|
30
|
+
|
|
31
|
+
3. **Run the smoke test** to verify the new scenario works:
|
|
32
|
+
```
|
|
33
|
+
uv run python tests/visual_smoke.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
4. **Open and review** the new screenshots in `tests/smoke_output/`
|
|
37
|
+
|
|
38
|
+
## Red flags — STOP
|
|
39
|
+
|
|
40
|
+
- "The shortcut is too simple to need a smoke test" → ALL shortcuts need entries
|
|
41
|
+
- "I'll add the test later" → add it in the same task
|
|
42
|
+
- "The coverage table says ✗, that's fine" → only fine if you document WHY (requires dialog, etc.)
|
|
43
|
+
|
|
44
|
+
## Stability check pattern
|
|
45
|
+
|
|
46
|
+
When verifying a key causes no visual jumps:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
def _check_no_jump(page, key, selectors, shot_name):
|
|
50
|
+
before = {s: page.locator(s).bounding_box() for s in selectors}
|
|
51
|
+
_press(page, key)
|
|
52
|
+
after = {s: page.locator(s).bounding_box() for s in selectors}
|
|
53
|
+
_shot(page, shot_name)
|
|
54
|
+
for s in selectors:
|
|
55
|
+
b, a = before[s], after[s]
|
|
56
|
+
if b and a:
|
|
57
|
+
dx = abs(b["x"] - a["x"]); dy = abs(b["y"] - a["y"])
|
|
58
|
+
if dx > 2 or dy > 2:
|
|
59
|
+
print(f" JUMP: {s} moved {dx:.0f}px/{dy:.0f}px after {key}")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Key selectors to check: `#canvas-wrap`, `#slim-cb-wrap`, `#info`
|
|
@@ -20,9 +20,9 @@ jobs:
|
|
|
20
20
|
runs-on: ubuntu-latest
|
|
21
21
|
|
|
22
22
|
steps:
|
|
23
|
-
- uses: actions/checkout@
|
|
23
|
+
- uses: actions/checkout@v6
|
|
24
24
|
|
|
25
|
-
- uses: actions/setup-python@
|
|
25
|
+
- uses: actions/setup-python@v6
|
|
26
26
|
with:
|
|
27
27
|
python-version: "3.x"
|
|
28
28
|
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
python -m build
|
|
34
34
|
|
|
35
35
|
- name: Upload distributions
|
|
36
|
-
uses: actions/upload-artifact@
|
|
36
|
+
uses: actions/upload-artifact@v7
|
|
37
37
|
with:
|
|
38
38
|
name: release-dists
|
|
39
39
|
path: dist/
|
|
@@ -59,7 +59,7 @@ jobs:
|
|
|
59
59
|
|
|
60
60
|
steps:
|
|
61
61
|
- name: Retrieve release distributions
|
|
62
|
-
uses: actions/download-artifact@
|
|
62
|
+
uses: actions/download-artifact@v8
|
|
63
63
|
with:
|
|
64
64
|
name: release-dists
|
|
65
65
|
path: dist/
|