arrayview 0.26.0__tar.gz → 0.26.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.
Files changed (212) hide show
  1. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/invocation-consistency/SKILL.md +21 -3
  2. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/context/architecture.md +12 -1
  3. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/add-file-format.md +20 -4
  4. {arrayview-0.26.0 → arrayview-0.26.1}/PKG-INFO +1 -1
  5. {arrayview-0.26.0 → arrayview-0.26.1}/pyproject.toml +1 -1
  6. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/__init__.py +2 -1
  7. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_io.py +17 -2
  8. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_launcher.py +15 -9
  9. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_platform.py +3 -1
  10. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_loading.py +7 -1
  11. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_stdio_server.py +6 -1
  12. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_viewer.html +50 -53
  13. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_api.py +82 -0
  14. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_command_reachability.py +18 -0
  15. arrayview-0.26.1/tests/test_io.py +82 -0
  16. {arrayview-0.26.0 → arrayview-0.26.1}/uv.lock +1 -1
  17. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/frontend-designer/SKILL.md +0 -0
  18. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/modes-consistency/SKILL.md +0 -0
  19. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/SKILL.md +0 -0
  20. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
  21. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
  22. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
  23. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/references/running-code.md +0 -0
  24. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/references/session-management.md +0 -0
  25. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
  26. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
  27. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/references/tracing.md +0 -0
  28. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
  29. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/todo-workflow/SKILL.md +0 -0
  30. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
  31. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
  32. {arrayview-0.26.0 → arrayview-0.26.1}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
  33. {arrayview-0.26.0 → arrayview-0.26.1}/.github/copilot-instructions.md +0 -0
  34. {arrayview-0.26.0 → arrayview-0.26.1}/.github/workflows/docs.yml +0 -0
  35. {arrayview-0.26.0 → arrayview-0.26.1}/.github/workflows/python-publish.yml +0 -0
  36. {arrayview-0.26.0 → arrayview-0.26.1}/.gitignore +0 -0
  37. {arrayview-0.26.0 → arrayview-0.26.1}/.ignore +0 -0
  38. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/AGENTS.md +0 -0
  39. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/ROUTER.md +0 -0
  40. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/SETUP.md +0 -0
  41. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/SYNC.md +0 -0
  42. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/context/conventions.md +0 -0
  43. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/context/decisions.md +0 -0
  44. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/context/frontend.md +0 -0
  45. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/context/lifecycle.md +0 -0
  46. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/context/project-state.md +0 -0
  47. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/context/render-pipeline.md +0 -0
  48. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/context/setup.md +0 -0
  49. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/context/stack.md +0 -0
  50. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/INDEX.md +0 -0
  51. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/README.md +0 -0
  52. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/add-server-endpoint.md +0 -0
  53. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/animation-verify.md +0 -0
  54. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/debug-render.md +0 -0
  55. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/debug-vscode-extension-python.md +0 -0
  56. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/extend-compare-mode.md +0 -0
  57. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/extract-server-route-module.md +0 -0
  58. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/patterns/frontend-change.md +0 -0
  59. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/setup.sh +0 -0
  60. {arrayview-0.26.0 → arrayview-0.26.1}/.mex/sync.sh +0 -0
  61. {arrayview-0.26.0 → arrayview-0.26.1}/.opencode/opencode.json +0 -0
  62. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
  63. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
  64. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
  65. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
  66. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
  67. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
  68. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
  69. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
  70. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
  71. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
  72. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
  73. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
  74. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
  75. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
  76. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
  77. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
  78. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
  79. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
  80. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
  81. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
  82. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
  83. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
  84. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
  85. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
  86. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
  87. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
  88. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
  89. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
  90. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
  91. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
  92. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
  93. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
  94. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
  95. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
  96. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
  97. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
  98. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
  99. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
  100. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
  101. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
  102. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
  103. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
  104. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
  105. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
  106. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
  107. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
  108. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
  109. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
  110. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
  111. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
  112. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
  113. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
  114. {arrayview-0.26.0 → arrayview-0.26.1}/.playwright-cli/theme-dark.yml +0 -0
  115. {arrayview-0.26.0 → arrayview-0.26.1}/.python-version +0 -0
  116. {arrayview-0.26.0 → arrayview-0.26.1}/.vscode/settings.json +0 -0
  117. {arrayview-0.26.0 → arrayview-0.26.1}/AGENTS.md +0 -0
  118. {arrayview-0.26.0 → arrayview-0.26.1}/CONTRIBUTING.md +0 -0
  119. {arrayview-0.26.0 → arrayview-0.26.1}/DESIGN.md +0 -0
  120. {arrayview-0.26.0 → arrayview-0.26.1}/LICENSE +0 -0
  121. {arrayview-0.26.0 → arrayview-0.26.1}/README.md +0 -0
  122. {arrayview-0.26.0 → arrayview-0.26.1}/docs/comparing.md +0 -0
  123. {arrayview-0.26.0 → arrayview-0.26.1}/docs/configuration.md +0 -0
  124. {arrayview-0.26.0 → arrayview-0.26.1}/docs/display.md +0 -0
  125. {arrayview-0.26.0 → arrayview-0.26.1}/docs/index.md +0 -0
  126. {arrayview-0.26.0 → arrayview-0.26.1}/docs/lifecycle.md +0 -0
  127. {arrayview-0.26.0 → arrayview-0.26.1}/docs/loading.md +0 -0
  128. {arrayview-0.26.0 → arrayview-0.26.1}/docs/logo.png +0 -0
  129. {arrayview-0.26.0 → arrayview-0.26.1}/docs/measurement.md +0 -0
  130. {arrayview-0.26.0 → arrayview-0.26.1}/docs/remote.md +0 -0
  131. {arrayview-0.26.0 → arrayview-0.26.1}/docs/stylesheets/extra.css +0 -0
  132. {arrayview-0.26.0 → arrayview-0.26.1}/docs/viewing.md +0 -0
  133. {arrayview-0.26.0 → arrayview-0.26.1}/matlab/arrayview.m +0 -0
  134. {arrayview-0.26.0 → arrayview-0.26.1}/mkdocs.yml +0 -0
  135. {arrayview-0.26.0 → arrayview-0.26.1}/plans/2026-04-14-immersive-animation.md +0 -0
  136. {arrayview-0.26.0 → arrayview-0.26.1}/plans/2026-05-07-unified-colormap-picker.md +0 -0
  137. {arrayview-0.26.0 → arrayview-0.26.1}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
  138. {arrayview-0.26.0 → arrayview-0.26.1}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
  139. {arrayview-0.26.0 → arrayview-0.26.1}/plans/egg-placement-mockup.html +0 -0
  140. {arrayview-0.26.0 → arrayview-0.26.1}/plans/refactoring_plan.md +0 -0
  141. {arrayview-0.26.0 → arrayview-0.26.1}/plans/webview/LOG.md +0 -0
  142. {arrayview-0.26.0 → arrayview-0.26.1}/scripts/demo.py +0 -0
  143. {arrayview-0.26.0 → arrayview-0.26.1}/scripts/release.sh +0 -0
  144. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/ARCHITECTURE.md +0 -0
  145. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/__main__.py +0 -0
  146. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_analysis.py +0 -0
  147. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_app.py +0 -0
  148. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_codex_open.py +0 -0
  149. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_config.py +0 -0
  150. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_diff.py +0 -0
  151. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_icon.png +0 -0
  152. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_imaging.py +0 -0
  153. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_lifecycle.py +0 -0
  154. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_overlays.py +0 -0
  155. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_render.py +0 -0
  156. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_analysis.py +0 -0
  157. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_export.py +0 -0
  158. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_persistence.py +0 -0
  159. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_preload.py +0 -0
  160. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_query.py +0 -0
  161. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_rendering.py +0 -0
  162. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_segmentation.py +0 -0
  163. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_state.py +0 -0
  164. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_vectorfield.py +0 -0
  165. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_routes_websocket.py +0 -0
  166. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_segmentation.py +0 -0
  167. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_server.py +0 -0
  168. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_session.py +0 -0
  169. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_shell.html +0 -0
  170. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_synthetic_mri.py +0 -0
  171. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_torch.py +0 -0
  172. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_vectorfield.py +0 -0
  173. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_vscode.py +0 -0
  174. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_vscode_browser.py +0 -0
  175. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_vscode_extension.py +0 -0
  176. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_vscode_shm.py +0 -0
  177. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/_vscode_signal.py +0 -0
  178. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/arrayview-opener.vsix +0 -0
  179. {arrayview-0.26.0 → arrayview-0.26.1}/src/arrayview/gsap.min.js +0 -0
  180. {arrayview-0.26.0 → arrayview-0.26.1}/tests/capture_v_animation.py +0 -0
  181. {arrayview-0.26.0 → arrayview-0.26.1}/tests/conftest.py +0 -0
  182. {arrayview-0.26.0 → arrayview-0.26.1}/tests/lifecycle_matrix.py +0 -0
  183. {arrayview-0.26.0 → arrayview-0.26.1}/tests/make_vectorfield_test_arrays.py +0 -0
  184. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_backend_shared.py +0 -0
  185. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_browser.py +0 -0
  186. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_cli.py +0 -0
  187. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_colorbar_hover_highlight.py +0 -0
  188. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_config.py +0 -0
  189. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_cross_mode_parametrized.py +0 -0
  190. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_interactions.py +0 -0
  191. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_large_arrays.py +0 -0
  192. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_lifecycle_contract.py +0 -0
  193. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_loading_server.py +0 -0
  194. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_mode_consistency.py +0 -0
  195. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_mode_entry_batching.py +0 -0
  196. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_mode_matrix.py +0 -0
  197. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_mode_roundtrip.py +0 -0
  198. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_nifti_meta.py +0 -0
  199. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_rgb_pixel_art.py +0 -0
  200. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_stdio_server.py +0 -0
  201. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_torch.py +0 -0
  202. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_view_component_integration.py +0 -0
  203. {arrayview-0.26.0 → arrayview-0.26.1}/tests/test_view_component_unit.py +0 -0
  204. {arrayview-0.26.0 → arrayview-0.26.1}/tests/ui_audit.py +0 -0
  205. {arrayview-0.26.0 → arrayview-0.26.1}/tests/v_anim_frames/.gitkeep +0 -0
  206. {arrayview-0.26.0 → arrayview-0.26.1}/tests/visual_smoke.py +0 -0
  207. {arrayview-0.26.0 → arrayview-0.26.1}/vscode-extension/AGENTS.md +0 -0
  208. {arrayview-0.26.0 → arrayview-0.26.1}/vscode-extension/LICENSE +0 -0
  209. {arrayview-0.26.0 → arrayview-0.26.1}/vscode-extension/extension.js +0 -0
  210. {arrayview-0.26.0 → arrayview-0.26.1}/vscode-extension/lifecycle_helpers.js +0 -0
  211. {arrayview-0.26.0 → arrayview-0.26.1}/vscode-extension/package.json +0 -0
  212. {arrayview-0.26.0 → arrayview-0.26.1}/vscode-extension/test_lifecycle_helpers.js +0 -0
@@ -20,6 +20,15 @@ Every behavior that depends on *how* arrayview is started (server lifecycle, bro
20
20
  | **VS Code terminal** | `arrayview()` or `view()` | VS Code Simple Browser | in-process or subprocess |
21
21
  | **Plain SSH (no VS Code)** | `arrayview()` or `view()` | prints port-forward hint | in-process or subprocess |
22
22
 
23
+ ## Transport Split
24
+
25
+ ArrayView has two backend transports. Invocation-sensitive fixes must account for both:
26
+
27
+ - **Network mode**: FastAPI routes and WebSocket server. File registration goes through `/load` in `_routes_loading.py`; spawned CLI sessions load in `_launcher._serve_daemon()`.
28
+ - **VS Code direct mode**: stdio subprocess. VS Code tunnel launches write a signal file; the extension runs `python -m arrayview --mode stdio <file>`, and file registration goes through `_stdio_server._handle_register()`.
29
+
30
+ Do not assume a `/load` or daemon fix covers VS Code tunnel. Shared behavior such as file-format key selection belongs in `_io.py` helpers and must be used by both transports.
31
+
23
32
  ## Key Detection Functions (all in `_platform.py`)
24
33
 
25
34
  ```python
@@ -86,17 +95,24 @@ for pid_str in client_pids:
86
95
  - Jupyter: repeated calls don't fail on port-already-in-use
87
96
  - Julia: `_view_julia()` passes new params via CLI flag or file
88
97
 
89
- 2. **Browser/display opening?**
98
+ 2. **File/session registration consistent?**
99
+ - FastAPI `/load` path handles the feature
100
+ - CLI spawned daemon path handles the feature
101
+ - VS Code direct stdio `_handle_register()` path handles the feature
102
+ - Direct `--mode stdio <file>` CLI path handles the feature
103
+ - Multi-array/default-key behavior is centralized in `_io.py`
104
+
105
+ 3. **Browser/display opening?**
90
106
  - Local native window (macOS): `pywebview` opens correctly
91
107
  - VS Code terminal: Simple Browser opens via extension signal file
92
108
  - VS Code tunnel: reaches client-side Simple Browser, not remote host
93
109
  - Verify `_ensure_vscode_extension()` installs/updates if VSIX changed
94
110
 
95
- 3. **Environment detection?**
111
+ 4. **Environment detection?**
96
112
  - Check detection order: Julia → Jupyter → VS Code remote → VS Code terminal → local
97
113
  - `_find_vscode_ipc_hook()` walks up to 12 parent processes; new subprocess wrappers may need the same
98
114
 
99
- 4. **VS Code extension changed?**
115
+ 5. **VS Code extension changed?**
100
116
  - Rebuild VSIX: `cd vscode-extension && vsce package -o ../src/arrayview/arrayview-opener.vsix`
101
117
  - Bump `_VSCODE_EXT_VERSION` in `_vscode.py` and `vscode-extension/package.json` together
102
118
 
@@ -111,6 +127,8 @@ uv run python -c "from arrayview import view; import numpy as np; view(np.zeros(
111
127
  ## Red Flags — STOP
112
128
 
113
129
  - "I changed server startup but only tested CLI" → test all paths
130
+ - "I fixed `/load` but not `_stdio_server.py`" → VS Code tunnel can still be broken
131
+ - "I fixed `_serve_daemon()` but `--diagnose` says `is_vscode_remote: true`" → tunnel uses stdio direct mode, not the daemon
114
132
  - "Works locally but not in tunnel" → check `_is_vscode_remote()` path and port exposure
115
133
  - "I used `127.0.0.1` in the URL" → use `localhost`
116
134
  - "I fixed tmux with `#{client_pid}`" → only works with one client; use `list-clients`
@@ -20,7 +20,7 @@ edges:
20
20
  condition: when the task involves _viewer.html, modes, reconcilers, or the View Component System
21
21
  - target: context/render-pipeline.md
22
22
  condition: when the task involves slice extraction, colormaps, caching, or the render thread
23
- last_updated: 2026-06-05
23
+ last_updated: 2026-06-11
24
24
  ---
25
25
 
26
26
  # Architecture
@@ -90,6 +90,17 @@ Tool launch, tool activation, and drawer visibility are separate states.
90
90
 
91
91
  Detection logic: `_platform.py`. Display opening: `_launcher.py` + `_vscode.py`.
92
92
 
93
+ ## File Registration Paths
94
+
95
+ File parsing belongs in `_io.py`, but session registration happens through different transports:
96
+
97
+ - **Network `/load`**: existing FastAPI server path in `_routes_loading.py`.
98
+ - **Spawned CLI daemon**: `_launcher._serve_daemon()` loads before publishing the pending session.
99
+ - **VS Code tunnel direct webview**: `_stdio_server._handle_register()` handles `python -m arrayview --mode stdio <file>` spawned by the extension.
100
+ - **Direct stdio CLI mode**: `_launcher.arrayview()` preloads positional files before starting `run_stdio_server()`.
101
+
102
+ For multi-array formats or formats with non-displayable members, keep key filtering/default selection in `_io.py` helpers and call those helpers from every registration path. A fix in `/load` alone does not cover VS Code tunnel.
103
+
93
104
  ## External Dependencies
94
105
 
95
106
  - **FastAPI + uvicorn** — async HTTP/WebSocket server, lazy-imported in `_launcher.py`. Never call uvicorn directly — use the `_uvicorn()` accessor.
@@ -15,20 +15,26 @@ edges:
15
15
  condition: for the lazy import pattern and Verify Checklist
16
16
  - target: context/render-pipeline.md
17
17
  condition: when the new format returns an unusual dtype that may affect the render pipeline
18
- last_updated: 2026-05-16
18
+ last_updated: 2026-06-11
19
19
  ---
20
20
 
21
21
  # Add File Format
22
22
 
23
23
  ## Context
24
24
 
25
- All file loading goes through `_io.load_data(filepath)` in `src/arrayview/_io.py`. The function dispatches on file extension. New formats must:
25
+ All file parsing goes through `_io.load_data(filepath)` or `_io.load_data_with_meta(filepath)` in `src/arrayview/_io.py`. The function dispatches on file extension. New formats must:
26
26
  1. Register the extension in `_SUPPORTED_EXTS`
27
27
  2. Add a lazy import (if the library is not already imported at module level)
28
28
  3. Add a dispatch branch in `load_data()`
29
29
  4. Return a numpy array (or array-like with `.shape` and `.dtype`)
30
30
 
31
- The server (`_server.py`) and CLI (`_launcher.py`) never import format libraries directly — all loading goes through `_io.load_data()`.
31
+ Transport/session creation is separate from parsing:
32
+
33
+ - FastAPI `/load`: `src/arrayview/_routes_loading.py`
34
+ - CLI spawned daemon and direct stdio CLI mode: `src/arrayview/_launcher.py`
35
+ - VS Code tunnel direct webview: `src/arrayview/_stdio_server.py`
36
+
37
+ The server and CLI must not import format libraries directly. They may call generic `_io` helpers such as `list_array_keys()` or `default_array_key()` when they need to choose a key before creating a session.
32
38
 
33
39
  ## Steps
34
40
 
@@ -74,18 +80,28 @@ The server (`_server.py`) and CLI (`_launcher.py`) never import format libraries
74
80
  return list(_mylib().peek_shape(fpath))
75
81
  ```
76
82
 
83
+ 7. **Keep key/default selection consistent across transports** for multi-array formats (`.npz`, `.mat`, HDF5-like containers):
84
+ - Put filtering and default-key logic in `_io.py`, not in a route or launcher branch.
85
+ - Use the same helper from FastAPI `/load`, CLI daemon/direct paths, and `_stdio_server.py`.
86
+ - If `/load` returns picker keys for multiple arrays, define what non-picker transports do (usually first displayable key).
87
+
77
88
  ## Gotchas
78
89
 
79
90
  - **Do not import the format library at the top of `_io.py`** without checking import cost first. Nibabel is the reference: it is lazy because it costs ~200 ms on first import.
80
91
  - **`.nii.gz` is two extensions** — the dispatch checks the full suffix, not just the last dot. New formats with compound extensions (`.foo.bar`) need special suffix handling in `load_data()`.
81
92
  - **Zarr is not loaded via `load_data()`** in all paths — zarr arrays can also be passed directly to `view()` as array objects. Ensure zarr-specific logic lives in `_session.py` / `_render.py`, not only in the file-load path.
82
- - **No format-specific code in `_server.py` or `_launcher.py`** — if you find yourself adding a format check in either of those modules, move it to `_io.py`.
93
+ - **No format-specific parsing code in `_server.py`, `_launcher.py`, or `_stdio_server.py`** — if you find yourself adding a format import or dtype/shape filter in those modules, move it to `_io.py`.
94
+ - **VS Code tunnel is stdio, not HTTP** — `arrayview file.ext` in a VS Code tunnel writes a signal file; the extension then runs `python -m arrayview --mode stdio file.ext`. A `/load` fix alone does not cover this path.
83
95
 
84
96
  ## Verify
85
97
 
86
98
  - [ ] Extension added to `_SUPPORTED_EXTS`
87
99
  - [ ] New library import is lazy (accessor function pattern) unless it imports in <10 ms
88
100
  - [ ] `load_data("myfile.myext")` returns a numpy array with correct shape and dtype
101
+ - [ ] If the format can contain multiple arrays or non-displayable members, add tests for `list_array_keys()` / `default_array_key()` and direct `load_data(..., key=...)`
102
+ - [ ] FastAPI `/load` works for the file, including picker/default-key behavior
103
+ - [ ] `_stdio_server._handle_register()` works for the file, because VS Code tunnel direct webview uses stdio instead of `/load`
104
+ - [ ] CLI daemon/direct paths use generic `_io` helpers for key selection instead of duplicating format rules
89
105
  - [ ] `view(load_data("myfile.myext"))` opens the viewer without errors
90
106
  - [ ] If full-load format, added to `FULL_LOAD_EXTS`
91
107
  - [ ] `uv run pytest` on `tests/test_mode_consistency.py` still passes (no dtype regressions)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.26.0
3
+ Version: 0.26.1
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.26.0"
7
+ version = "0.26.1"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -1,4 +1,5 @@
1
- __version__ = "0.25.1"
1
+ import importlib.metadata as _metadata
2
+ __version__ = _metadata.version("arrayview")
2
3
 
3
4
  __all__ = [
4
5
  "TrainingMonitor",
@@ -38,6 +38,15 @@ def _fix_mat_complex(arr):
38
38
  return arr
39
39
 
40
40
 
41
+ def _is_viewable_mat_array(value):
42
+ """True for MATLAB arrays ArrayView can display directly."""
43
+ return (
44
+ isinstance(value, np.ndarray)
45
+ and value.ndim >= 1
46
+ and value.dtype.kind in ("b", "i", "u", "f", "c")
47
+ )
48
+
49
+
41
50
  def list_npz_keys(filepath):
42
51
  """Return [{key, shape, dtype}] for each ndarray in an .npz file.
43
52
 
@@ -67,7 +76,7 @@ def list_mat_keys(filepath):
67
76
  for k, v in mat.items():
68
77
  if k.startswith("_"):
69
78
  continue
70
- if isinstance(v, np.ndarray) and v.ndim >= 1 and v.dtype.kind in ("b", "i", "u", "f", "c"):
79
+ if _is_viewable_mat_array(v):
71
80
  keys.append({"key": k, "shape": list(v.shape), "dtype": str(v.dtype)})
72
81
  return keys
73
82
  except NotImplementedError:
@@ -95,6 +104,12 @@ def list_array_keys(filepath):
95
104
  return []
96
105
 
97
106
 
107
+ def default_array_key(filepath):
108
+ """Return the first selectable array key for multi-array formats, if any."""
109
+ keys = list_array_keys(filepath)
110
+ return keys[0]["key"] if keys else None
111
+
112
+
98
113
  def _select_npz_array(npz, filepath):
99
114
  """Load the first array from a multi-array .npz file.
100
115
 
@@ -233,7 +248,7 @@ def load_data(filepath, key=None):
233
248
  arrays = {
234
249
  k: v
235
250
  for k, v in mat.items()
236
- if not k.startswith("_") and isinstance(v, np.ndarray)
251
+ if not k.startswith("_") and _is_viewable_mat_array(v)
237
252
  }
238
253
  if key is not None:
239
254
  return _fix_mat_complex(arrays[key])
@@ -2717,7 +2717,7 @@ def _serve_daemon(
2717
2717
  _array_keys = list_array_keys(filepath)
2718
2718
  except Exception:
2719
2719
  pass
2720
- _load_key = _array_keys[0]["key"] if _array_keys and len(_array_keys) > 1 else None
2720
+ _load_key = _array_keys[0]["key"] if _array_keys else None
2721
2721
  data, spatial_meta = load_data_with_meta(filepath, key=_load_key)
2722
2722
  if cleanup:
2723
2723
  try:
@@ -3187,12 +3187,12 @@ def arrayview():
3187
3187
  )
3188
3188
  print(f"SESSION:{info}", file=sys.stderr)
3189
3189
  elif args.files:
3190
- from arrayview._io import load_data
3190
+ from arrayview._io import default_array_key, load_data
3191
3191
  from arrayview._session import SESSIONS, Session
3192
3192
 
3193
3193
  # First file is the main array
3194
3194
  base_path = args.files[0]
3195
- data = load_data(base_path)
3195
+ data = load_data(base_path, key=default_array_key(base_path))
3196
3196
  session = Session(
3197
3197
  data=data,
3198
3198
  filepath=base_path,
@@ -3205,7 +3205,10 @@ def arrayview():
3205
3205
  if getattr(args, "vectorfield", None):
3206
3206
  from arrayview._vectorfield import _configure_vectorfield
3207
3207
 
3208
- vf_data = load_data(args.vectorfield)
3208
+ vf_data = load_data(
3209
+ args.vectorfield,
3210
+ key=default_array_key(args.vectorfield),
3211
+ )
3209
3212
  _configure_vectorfield(
3210
3213
  session, vf_data,
3211
3214
  getattr(args, "vectorfield_components_dim", None),
@@ -3216,7 +3219,7 @@ def arrayview():
3216
3219
  # Load overlay(s) as separate sessions
3217
3220
  overlay_sids = []
3218
3221
  for ov_path in (getattr(args, "overlay", None) or []):
3219
- ov_data = load_data(ov_path)
3222
+ ov_data = load_data(ov_path, key=default_array_key(ov_path))
3220
3223
  ov_session = Session(
3221
3224
  data=ov_data,
3222
3225
  filepath=ov_path,
@@ -3232,7 +3235,7 @@ def arrayview():
3232
3235
  if getattr(args, "compare", None):
3233
3236
  compare_paths.append(args.compare)
3234
3237
  for cp in compare_paths:
3235
- cmp_data = load_data(cp)
3238
+ cmp_data = load_data(cp, key=default_array_key(cp))
3236
3239
  cmp_session = Session(
3237
3240
  data=cmp_data,
3238
3241
  filepath=cp,
@@ -3572,18 +3575,21 @@ def arrayview():
3572
3575
 
3573
3576
  if args.vectorfield:
3574
3577
  try:
3575
- from arrayview._io import load_data
3578
+ from arrayview._io import default_array_key, load_data
3576
3579
  from arrayview._render import _detect_rgb_axis
3577
3580
  from arrayview._vectorfield import _resolve_vfield_layout
3578
3581
 
3579
- base_data = load_data(base_file)
3582
+ base_data = load_data(base_file, key=default_array_key(base_file))
3580
3583
  image_shape = tuple(int(s) for s in base_data.shape)
3581
3584
  if args.rgb:
3582
3585
  rgb_axis = _detect_rgb_axis(image_shape)
3583
3586
  image_shape = tuple(
3584
3587
  s for i, s in enumerate(image_shape) if i != rgb_axis
3585
3588
  )
3586
- vf_data = load_data(args.vectorfield)
3589
+ vf_data = load_data(
3590
+ args.vectorfield,
3591
+ key=default_array_key(args.vectorfield),
3592
+ )
3587
3593
  layout = _resolve_vfield_layout(
3588
3594
  tuple(int(s) for s in vf_data.shape),
3589
3595
  image_shape,
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import importlib.util
5
6
  import os
6
7
  import subprocess
7
8
  import sys
@@ -414,12 +415,13 @@ def _can_native_window() -> bool:
414
415
  # Plain SSH (no VS Code): the display is on the client machine, not here.
415
416
  if os.environ.get("SSH_CLIENT") or os.environ.get("SSH_CONNECTION"):
416
417
  return False
418
+ if importlib.util.find_spec("webview") is None:
419
+ return False
417
420
  if sys.platform in ("darwin", "win32"):
418
421
  return True
419
422
  # Linux/BSD: need a display server AND pywebview's GUI bindings
420
423
  if not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
421
424
  return False
422
- import importlib.util
423
425
 
424
426
  return (
425
427
  importlib.util.find_spec("qtpy") is not None
@@ -123,8 +123,14 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
123
123
  _array_keys = await asyncio.to_thread(list_array_keys, filepath)
124
124
  if len(_array_keys) > 1 and not _key:
125
125
  return {"array_keys": _array_keys, "filepath": filepath}
126
+ if len(_array_keys) == 1 and not _key:
127
+ _key = _array_keys[0]["key"]
126
128
 
127
- data, spatial_meta = await asyncio.to_thread(load_data_with_meta, filepath, key=_key)
129
+ data, spatial_meta = await asyncio.to_thread(
130
+ load_data_with_meta,
131
+ filepath,
132
+ key=_key,
133
+ )
128
134
  except Exception as e:
129
135
  return {"error": str(e)}
130
136
  session = await asyncio.to_thread(Session, data, filepath=filepath, name=name)
@@ -115,7 +115,12 @@ def _handle_register(msg: dict) -> None:
115
115
  name = msg.get("name") or __import__("os").path.basename(file_path)
116
116
  options = msg.get("options", {})
117
117
 
118
- data, spatial_meta = load_data_with_meta(file_path)
118
+ from ._io import default_array_key
119
+
120
+ data, spatial_meta = load_data_with_meta(
121
+ file_path,
122
+ key=default_array_key(file_path),
123
+ )
119
124
  session = Session(data=data, filepath=file_path, name=name)
120
125
  session.spatial_meta = spatial_meta
121
126
  if spatial_meta is not None:
@@ -285,7 +285,7 @@
285
285
  .dim-size { opacity: 0.28; font-size: 0.68em; font-weight: normal; }
286
286
  .dim-hover-idx { font-size: 0.68em; font-weight: normal; color: var(--dim-hover, #777); opacity: 1; }
287
287
  .active-dim { font-weight: bold; }
288
- .playing-dim { color: var(--playing-dim); font-weight: bold; }
288
+ .playing-dim { font-weight: bold; }
289
289
  .vfield-t-dim { color: var(--vfield-t-dim); }
290
290
  .active-vfield-t { font-weight: bold; }
291
291
  .dim-sep { color: var(--muted); opacity: 0.4; font-weight: normal; }
@@ -466,8 +466,7 @@
466
466
  color: var(--text);
467
467
  -webkit-text-fill-color: var(--text);
468
468
  }
469
- .active-dim .dim-slash:not([style*="background"]),
470
- .playing-dim .dim-slash:not([style*="background"]) {
469
+ .active-dim .dim-slash:not([style*="background"]) {
471
470
  color: var(--active-dim) !important;
472
471
  -webkit-text-fill-color: var(--active-dim) !important;
473
472
  }
@@ -13784,10 +13783,9 @@
13784
13783
  const subtle = style.getPropertyValue('--subtle').trim();
13785
13784
  let fill;
13786
13785
  if (isVfieldT) fill = style.getPropertyValue('--vfield-t-dim').trim();
13787
- else if (playing) fill = style.getPropertyValue('--playing-dim').trim();
13788
13786
  else if (active) fill = style.getPropertyValue('--active-dim').trim();
13789
13787
  else fill = muted;
13790
- const unfilled = (active || playing || isVfieldT) ? `color-mix(in srgb, ${fill} 32%, ${muted})` : subtle;
13788
+ const unfilled = (active || isVfieldT) ? `color-mix(in srgb, ${fill} 32%, ${muted})` : subtle;
13791
13789
  return slashFill(pct, fill, unfilled);
13792
13790
  }
13793
13791
  /* ── JS: Info Bar and Pixel Display ───────────────────────── */
@@ -17577,24 +17575,20 @@
17577
17575
  // Transpose x and y: swap the dimension indices and flips
17578
17576
  if (shape.length < 2) return;
17579
17577
  if (multiViewActive) {
17580
- crossfade(() => {
17581
- mvViews.forEach(v => { [v.dimX, v.dimY] = [v.dimY, v.dimX]; v.seq++; });
17582
- [dim_x, dim_y] = [dim_y, dim_x];
17583
- [flip_x, flip_y] = [flip_y, flip_x];
17584
- flipDims[dim_x] = flip_x; flipDims[dim_y] = flip_y;
17585
- mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
17586
- });
17578
+ mvViews.forEach(v => { [v.dimX, v.dimY] = [v.dimY, v.dimX]; v.seq++; });
17579
+ [dim_x, dim_y] = [dim_y, dim_x];
17580
+ [flip_x, flip_y] = [flip_y, flip_x];
17581
+ flipDims[dim_x] = flip_x; flipDims[dim_y] = flip_y;
17582
+ mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
17587
17583
  showStatus('transposed x/y (all panes)');
17588
17584
  if (typeof _cropReconcileBindings === 'function') _cropReconcileBindings();
17589
17585
  return;
17590
17586
  }
17591
17587
  const wasMosaic = dim_z >= 0;
17592
- crossfade(() => {
17593
- [dim_x, dim_y] = [dim_y, dim_x];
17594
- [flip_x, flip_y] = [flip_y, flip_x];
17595
- if (!wasMosaic) dim_z = -1;
17596
- updateView(); triggerPreload();
17597
- });
17588
+ [dim_x, dim_y] = [dim_y, dim_x];
17589
+ [flip_x, flip_y] = [flip_y, flip_x];
17590
+ if (!wasMosaic) dim_z = -1;
17591
+ updateView(); triggerPreload();
17598
17592
  showStatus(wasMosaic ? 'transposed x/y (mosaic)' : 'transposed x/y');
17599
17593
  if (typeof _cropReconcileBindings === 'function') _cropReconcileBindings();
17600
17594
  },
@@ -17605,34 +17599,32 @@
17605
17599
  run: (ctx, e) => {
17606
17600
  if (_segMode) { _segReset(); return; }
17607
17601
  if (multiViewActive) {
17608
- crossfade(() => {
17609
- const oldDimX = dim_x, oldDimY = dim_y;
17610
- dim_x = oldDimY; dim_y = oldDimX;
17611
- const ofx = flipDims[oldDimX] || false, ofy = flipDims[oldDimY] || false;
17612
- flipDims[dim_x] = ofy; flipDims[dim_y] = !ofx;
17613
- const third = mvDims.find(d => d !== oldDimX && d !== oldDimY);
17614
- mvDims = [dim_y, dim_x, third];
17615
- const newDefs = [
17616
- { dimX: mvDims[1], dimY: mvDims[0], sliceDir: mvDims[2] },
17617
- { dimX: mvDims[2], dimY: mvDims[0], sliceDir: mvDims[1] },
17618
- { dimX: mvDims[2], dimY: mvDims[1], sliceDir: mvDims[0] },
17619
- ];
17620
- mvViews.forEach((v, i) => {
17621
- v.dimX = newDefs[i].dimX; v.dimY = newDefs[i].dimY;
17622
- v.sliceDir = newDefs[i].sliceDir;
17623
- v.seq++;
17624
- });
17625
- mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
17626
- const _axLetters = ['x', 'y', 'z'];
17627
- mvViews.forEach((v, i) => {
17628
- const newX = _axLetters[mvDims.indexOf(v.dimX)] || '?';
17629
- const newY = _axLetters[mvDims.indexOf(v.dimY)] || '?';
17630
- v.axLblX = newX; v.axLblY = newY;
17631
- if (v.axesSvg) {
17632
- v.axesSvg.querySelector('.axes-lbl-x').textContent = newX;
17633
- v.axesSvg.querySelector('.axes-lbl-y').textContent = newY;
17634
- }
17635
- });
17602
+ const oldDimX = dim_x, oldDimY = dim_y;
17603
+ dim_x = oldDimY; dim_y = oldDimX;
17604
+ const ofx = flipDims[oldDimX] || false, ofy = flipDims[oldDimY] || false;
17605
+ flipDims[dim_x] = ofy; flipDims[dim_y] = !ofx;
17606
+ const third = mvDims.find(d => d !== oldDimX && d !== oldDimY);
17607
+ mvDims = [dim_y, dim_x, third];
17608
+ const newDefs = [
17609
+ { dimX: mvDims[1], dimY: mvDims[0], sliceDir: mvDims[2] },
17610
+ { dimX: mvDims[2], dimY: mvDims[0], sliceDir: mvDims[1] },
17611
+ { dimX: mvDims[2], dimY: mvDims[1], sliceDir: mvDims[0] },
17612
+ ];
17613
+ mvViews.forEach((v, i) => {
17614
+ v.dimX = newDefs[i].dimX; v.dimY = newDefs[i].dimY;
17615
+ v.sliceDir = newDefs[i].sliceDir;
17616
+ v.seq++;
17617
+ });
17618
+ mvViews.forEach(v => { mvDrawFrame(v); mvRender(v); });
17619
+ const _axLetters = ['x', 'y', 'z'];
17620
+ mvViews.forEach((v, i) => {
17621
+ const newX = _axLetters[mvDims.indexOf(v.dimX)] || '?';
17622
+ const newY = _axLetters[mvDims.indexOf(v.dimY)] || '?';
17623
+ v.axLblX = newX; v.axLblY = newY;
17624
+ if (v.axesSvg) {
17625
+ v.axesSvg.querySelector('.axes-lbl-x').textContent = newX;
17626
+ v.axesSvg.querySelector('.axes-lbl-y').textContent = newY;
17627
+ }
17636
17628
  });
17637
17629
  showStatus('rotated 90°');
17638
17630
  renderInfo(); saveState();
@@ -17645,11 +17637,9 @@
17645
17637
  if (lastImageData) ctx.putImageData(applyFlips(lastImageData, lastImgW, lastImgH), 0, 0);
17646
17638
  renderInfo(); saveState();
17647
17639
  } else {
17648
- crossfade(() => {
17649
- const [odx, ody, ofx, ofy] = [dim_x, dim_y, flip_x, flip_y];
17650
- dim_x = ody; dim_y = odx; flip_x = ofy; flip_y = !ofx;
17651
- updateView(); triggerPreload();
17652
- });
17640
+ const [odx, ody, ofx, ofy] = [dim_x, dim_y, flip_x, flip_y];
17641
+ dim_x = ody; dim_y = odx; flip_x = ofy; flip_y = !ofx;
17642
+ updateView(); triggerPreload();
17653
17643
  showStatus('rotated 90°'); saveState();
17654
17644
  }
17655
17645
  if (typeof _cropReconcileBindings === 'function') _cropReconcileBindings();
@@ -21058,7 +21048,7 @@
21058
21048
 
21059
21049
  /* ── JS: Keyboard Shortcuts ───────────────────────────────── */
21060
21050
 
21061
- sink.addEventListener('keydown', async (e) => {
21051
+ function _handleViewerShortcutKeydown(e) {
21062
21052
  e.preventDefault();
21063
21053
  e.stopImmediatePropagation();
21064
21054
  if (_cmapPickerOpen) {
@@ -21146,7 +21136,14 @@
21146
21136
  document.getElementById('info-overlay').classList.remove('visible');
21147
21137
  return;
21148
21138
  }
21149
- });
21139
+ }
21140
+
21141
+ sink.addEventListener('keydown', _handleViewerShortcutKeydown);
21142
+ document.addEventListener('keydown', (e) => {
21143
+ if (e.target === sink) return;
21144
+ if (_isEditableActive()) return;
21145
+ _handleViewerShortcutKeydown(e);
21146
+ }, true);
21150
21147
 
21151
21148
  // Hold-`g` finalizer. The picker handles its own commit/cancel
21152
21149
  // through armUp(); this listener just routes the keyup to it. Listen
@@ -131,6 +131,54 @@ class TestLoad:
131
131
  r = client.post("/load", json={"filepath": str(tmp_path / "coolarray.npy")})
132
132
  assert r.json()["name"] == "coolarray.npy"
133
133
 
134
+ def test_load_mat_single_numeric_array_with_metadata_returns_sid(
135
+ self, client, tmp_path
136
+ ):
137
+ import scipy.io
138
+
139
+ path = tmp_path / "single_numeric_with_metadata.mat"
140
+ expected = np.arange(12, dtype=np.float32).reshape(3, 4)
141
+ scipy.io.savemat(
142
+ path,
143
+ {
144
+ "image": expected,
145
+ "metadata": np.array([{"name": "not-displayable"}], dtype=object),
146
+ },
147
+ )
148
+
149
+ r = client.post("/load", json={"filepath": str(path)})
150
+ body = r.json()
151
+ assert "error" not in body
152
+ sid = body["sid"]
153
+
154
+ meta = client.get(f"/metadata/{sid}").json()
155
+ assert meta["shape"] == [3, 4]
156
+ assert "array_keys" not in meta
157
+
158
+ def test_load_mat_multiple_numeric_arrays_returns_picker_keys(
159
+ self, client, tmp_path
160
+ ):
161
+ import scipy.io
162
+
163
+ path = tmp_path / "multiple_numeric.mat"
164
+ scipy.io.savemat(
165
+ path,
166
+ {
167
+ "first": np.zeros((2, 3), dtype=np.float32),
168
+ "second": np.ones((4, 5), dtype=np.uint16),
169
+ "metadata": np.array([{"name": "not-displayable"}], dtype=object),
170
+ },
171
+ )
172
+
173
+ body = client.post("/load", json={"filepath": str(path)}).json()
174
+ assert body == {
175
+ "array_keys": [
176
+ {"key": "first", "shape": [2, 3], "dtype": "float32"},
177
+ {"key": "second", "shape": [4, 5], "dtype": "uint16"},
178
+ ],
179
+ "filepath": str(path),
180
+ }
181
+
134
182
  def test_notify_existing_session_returns_open_state(self, client, sid_2d):
135
183
  r = client.post(
136
184
  f"/notify/{sid_2d}",
@@ -3177,6 +3225,40 @@ class TestPortAndTunnelHelpers:
3177
3225
 
3178
3226
  assert platform._can_native_window() is True
3179
3227
 
3228
+ def test_can_native_window_false_on_linux_without_webview(self, monkeypatch):
3229
+ import arrayview._platform as platform
3230
+
3231
+ monkeypatch.setattr(platform, "_in_vscode_terminal", lambda: False)
3232
+ monkeypatch.setattr(platform, "_is_vscode_remote", lambda: False)
3233
+ monkeypatch.delenv("SSH_CLIENT", raising=False)
3234
+ monkeypatch.delenv("SSH_CONNECTION", raising=False)
3235
+ monkeypatch.setenv("DISPLAY", ":0")
3236
+ monkeypatch.setattr(platform.sys, "platform", "linux")
3237
+ monkeypatch.setattr(
3238
+ platform.importlib.util,
3239
+ "find_spec",
3240
+ lambda name: object() if name == "qtpy" else None,
3241
+ )
3242
+
3243
+ assert platform._can_native_window() is False
3244
+
3245
+ def test_can_native_window_true_on_linux_with_webview_and_qt(self, monkeypatch):
3246
+ import arrayview._platform as platform
3247
+
3248
+ monkeypatch.setattr(platform, "_in_vscode_terminal", lambda: False)
3249
+ monkeypatch.setattr(platform, "_is_vscode_remote", lambda: False)
3250
+ monkeypatch.delenv("SSH_CLIENT", raising=False)
3251
+ monkeypatch.delenv("SSH_CONNECTION", raising=False)
3252
+ monkeypatch.setenv("WAYLAND_DISPLAY", "wayland-0")
3253
+ monkeypatch.setattr(platform.sys, "platform", "linux")
3254
+ monkeypatch.setattr(
3255
+ platform.importlib.util,
3256
+ "find_spec",
3257
+ lambda name: object() if name in {"webview", "qtpy"} else None,
3258
+ )
3259
+
3260
+ assert platform._can_native_window() is True
3261
+
3180
3262
  def test_open_browser_skips_vscode_signal_when_terminal_check_is_false(self, monkeypatch):
3181
3263
  import arrayview._vscode_browser as browser_mod
3182
3264
 
@@ -119,3 +119,21 @@ def test_command_reachable_in_mode(reach_page, mode_id, cmd):
119
119
  assert enabled == expected, (
120
120
  f"{cmd} expected enabled={expected} in mode {mode_id}, got {enabled}"
121
121
  )
122
+
123
+
124
+ def test_colormap_shortcut_works_when_body_has_focus(loaded_viewer, sid_3d):
125
+ page = loaded_viewer(sid_3d)
126
+ _focus(page)
127
+ _wait(page, 300)
128
+ opened = page.evaluate(
129
+ """() => {
130
+ const ev = new KeyboardEvent('keydown', {
131
+ key: 'c',
132
+ bubbles: true,
133
+ cancelable: true,
134
+ });
135
+ document.body.dispatchEvent(ev);
136
+ return _cmapPickerOpen;
137
+ }"""
138
+ )
139
+ assert opened is True