arrayview 0.31.0__tar.gz → 0.31.2__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 (231) hide show
  1. {arrayview-0.31.0 → arrayview-0.31.2}/PKG-INFO +1 -1
  2. {arrayview-0.31.0 → arrayview-0.31.2}/pyproject.toml +1 -1
  3. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_analysis.py +9 -2
  4. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_instance_registry.py +6 -0
  5. arrayview-0.31.2/src/arrayview/_overlays.py +150 -0
  6. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_render.py +37 -19
  7. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_rendering.py +13 -1
  8. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_websocket.py +27 -1
  9. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_session.py +96 -0
  10. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vscode_browser.py +2 -1
  11. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_api.py +84 -0
  12. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_lifecycle_contract.py +6 -0
  13. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_nifti_series.py +111 -0
  14. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_vscode_ack_protocol.py +7 -0
  15. {arrayview-0.31.0 → arrayview-0.31.2}/uv.lock +1 -1
  16. {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/test_lifecycle_helpers.js +8 -6
  17. arrayview-0.31.0/src/arrayview/_overlays.py +0 -74
  18. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/frontend-designer/SKILL.md +0 -0
  19. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/invocation-consistency/SKILL.md +0 -0
  20. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/modes-consistency/SKILL.md +0 -0
  21. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/SKILL.md +0 -0
  22. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
  23. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
  24. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
  25. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/running-code.md +0 -0
  26. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/session-management.md +0 -0
  27. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
  28. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
  29. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/tracing.md +0 -0
  30. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
  31. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/todo-workflow/SKILL.md +0 -0
  32. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
  33. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
  34. {arrayview-0.31.0 → arrayview-0.31.2}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
  35. {arrayview-0.31.0 → arrayview-0.31.2}/.github/copilot-instructions.md +0 -0
  36. {arrayview-0.31.0 → arrayview-0.31.2}/.github/workflows/docs.yml +0 -0
  37. {arrayview-0.31.0 → arrayview-0.31.2}/.github/workflows/launch-contracts.yml +0 -0
  38. {arrayview-0.31.0 → arrayview-0.31.2}/.github/workflows/python-publish.yml +0 -0
  39. {arrayview-0.31.0 → arrayview-0.31.2}/.gitignore +0 -0
  40. {arrayview-0.31.0 → arrayview-0.31.2}/.ignore +0 -0
  41. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/AGENTS.md +0 -0
  42. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/ROUTER.md +0 -0
  43. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/SETUP.md +0 -0
  44. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/SYNC.md +0 -0
  45. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/architecture.md +0 -0
  46. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/conventions.md +0 -0
  47. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/decisions.md +0 -0
  48. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/frontend.md +0 -0
  49. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/lifecycle.md +0 -0
  50. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/project-state.md +0 -0
  51. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/render-pipeline.md +0 -0
  52. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/setup.md +0 -0
  53. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/context/stack.md +0 -0
  54. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/INDEX.md +0 -0
  55. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/README.md +0 -0
  56. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/add-file-format.md +0 -0
  57. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/add-server-endpoint.md +0 -0
  58. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/animation-verify.md +0 -0
  59. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/debug-render.md +0 -0
  60. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/debug-vscode-extension-python.md +0 -0
  61. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/extend-compare-mode.md +0 -0
  62. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/extract-server-route-module.md +0 -0
  63. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/patterns/frontend-change.md +0 -0
  64. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/setup.sh +0 -0
  65. {arrayview-0.31.0 → arrayview-0.31.2}/.mex/sync.sh +0 -0
  66. {arrayview-0.31.0 → arrayview-0.31.2}/.opencode/opencode.json +0 -0
  67. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
  68. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
  69. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
  70. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
  71. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
  72. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
  73. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
  74. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
  75. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
  76. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
  77. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
  78. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
  79. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
  80. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
  81. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
  82. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
  83. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
  84. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
  85. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
  86. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
  87. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
  88. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
  89. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
  90. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
  91. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
  92. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
  93. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
  94. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
  95. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
  96. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
  97. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
  98. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
  99. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
  100. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
  101. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
  102. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
  103. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
  104. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
  105. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
  106. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
  107. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
  108. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
  109. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
  110. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
  111. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
  112. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
  113. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
  114. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
  115. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
  116. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
  117. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
  118. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
  119. {arrayview-0.31.0 → arrayview-0.31.2}/.playwright-cli/theme-dark.yml +0 -0
  120. {arrayview-0.31.0 → arrayview-0.31.2}/.python-version +0 -0
  121. {arrayview-0.31.0 → arrayview-0.31.2}/.vscode/settings.json +0 -0
  122. {arrayview-0.31.0 → arrayview-0.31.2}/AGENTS.md +0 -0
  123. {arrayview-0.31.0 → arrayview-0.31.2}/CONTRIBUTING.md +0 -0
  124. {arrayview-0.31.0 → arrayview-0.31.2}/DESIGN.md +0 -0
  125. {arrayview-0.31.0 → arrayview-0.31.2}/LICENSE +0 -0
  126. {arrayview-0.31.0 → arrayview-0.31.2}/README.md +0 -0
  127. {arrayview-0.31.0 → arrayview-0.31.2}/docs/comparing.md +0 -0
  128. {arrayview-0.31.0 → arrayview-0.31.2}/docs/configuration.md +0 -0
  129. {arrayview-0.31.0 → arrayview-0.31.2}/docs/display.md +0 -0
  130. {arrayview-0.31.0 → arrayview-0.31.2}/docs/foreign-hosts.md +0 -0
  131. {arrayview-0.31.0 → arrayview-0.31.2}/docs/index.md +0 -0
  132. {arrayview-0.31.0 → arrayview-0.31.2}/docs/launch-routing-refactor.md +0 -0
  133. {arrayview-0.31.0 → arrayview-0.31.2}/docs/lifecycle.md +0 -0
  134. {arrayview-0.31.0 → arrayview-0.31.2}/docs/loading.md +0 -0
  135. {arrayview-0.31.0 → arrayview-0.31.2}/docs/logo.png +0 -0
  136. {arrayview-0.31.0 → arrayview-0.31.2}/docs/measurement.md +0 -0
  137. {arrayview-0.31.0 → arrayview-0.31.2}/docs/remote.md +0 -0
  138. {arrayview-0.31.0 → arrayview-0.31.2}/docs/stylesheets/extra.css +0 -0
  139. {arrayview-0.31.0 → arrayview-0.31.2}/docs/viewing.md +0 -0
  140. {arrayview-0.31.0 → arrayview-0.31.2}/docs/vscode-window-routing-plan.md +0 -0
  141. {arrayview-0.31.0 → arrayview-0.31.2}/experiments/ws_tunnel_diag/ws_tunnel_diag.py +0 -0
  142. {arrayview-0.31.0 → arrayview-0.31.2}/matlab/arrayview.m +0 -0
  143. {arrayview-0.31.0 → arrayview-0.31.2}/mkdocs.yml +0 -0
  144. {arrayview-0.31.0 → arrayview-0.31.2}/plans/2026-04-14-immersive-animation.md +0 -0
  145. {arrayview-0.31.0 → arrayview-0.31.2}/plans/2026-05-07-unified-colormap-picker.md +0 -0
  146. {arrayview-0.31.0 → arrayview-0.31.2}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
  147. {arrayview-0.31.0 → arrayview-0.31.2}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
  148. {arrayview-0.31.0 → arrayview-0.31.2}/plans/egg-placement-mockup.html +0 -0
  149. {arrayview-0.31.0 → arrayview-0.31.2}/plans/refactoring_plan.md +0 -0
  150. {arrayview-0.31.0 → arrayview-0.31.2}/plans/roi-analysis-requirements.md +0 -0
  151. {arrayview-0.31.0 → arrayview-0.31.2}/plans/webview/LOG.md +0 -0
  152. {arrayview-0.31.0 → arrayview-0.31.2}/scripts/demo.py +0 -0
  153. {arrayview-0.31.0 → arrayview-0.31.2}/scripts/release.sh +0 -0
  154. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/ARCHITECTURE.md +0 -0
  155. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/__init__.py +0 -0
  156. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/__main__.py +0 -0
  157. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_app.py +0 -0
  158. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_codex_open.py +0 -0
  159. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_config.py +0 -0
  160. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_diff.py +0 -0
  161. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_icon.png +0 -0
  162. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_imaging.py +0 -0
  163. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_io.py +0 -0
  164. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_launch_plan.py +0 -0
  165. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_launcher.py +0 -0
  166. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_lifecycle.py +0 -0
  167. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_platform.py +0 -0
  168. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_analysis.py +0 -0
  169. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_export.py +0 -0
  170. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_loading.py +0 -0
  171. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_persistence.py +0 -0
  172. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_preload.py +0 -0
  173. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_query.py +0 -0
  174. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_segmentation.py +0 -0
  175. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_state.py +0 -0
  176. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_routes_vectorfield.py +0 -0
  177. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_segmentation.py +0 -0
  178. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_server.py +0 -0
  179. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_session_spec.py +0 -0
  180. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_shell.html +0 -0
  181. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_synthetic_mri.py +0 -0
  182. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_torch.py +0 -0
  183. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vectorfield.py +0 -0
  184. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_viewer.html +0 -0
  185. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vscode.py +0 -0
  186. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vscode_extension.py +0 -0
  187. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/_vscode_signal.py +0 -0
  188. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/arrayview-opener.vsix +0 -0
  189. {arrayview-0.31.0 → arrayview-0.31.2}/src/arrayview/gsap.min.js +0 -0
  190. {arrayview-0.31.0 → arrayview-0.31.2}/tests/capture_v_animation.py +0 -0
  191. {arrayview-0.31.0 → arrayview-0.31.2}/tests/conftest.py +0 -0
  192. {arrayview-0.31.0 → arrayview-0.31.2}/tests/lifecycle_matrix.py +0 -0
  193. {arrayview-0.31.0 → arrayview-0.31.2}/tests/make_vectorfield_test_arrays.py +0 -0
  194. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_backend_shared.py +0 -0
  195. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_browser.py +0 -0
  196. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_cli.py +0 -0
  197. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_colorbar_hover_highlight.py +0 -0
  198. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_command_reachability.py +0 -0
  199. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_config.py +0 -0
  200. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_cross_mode_parametrized.py +0 -0
  201. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_display_opener_results.py +0 -0
  202. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_doctor_commands.py +0 -0
  203. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_instance_registry.py +0 -0
  204. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_interactions.py +0 -0
  205. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_invocation_contract_matrix.py +0 -0
  206. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_io.py +0 -0
  207. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_large_arrays.py +0 -0
  208. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_launch_diagnostics.py +0 -0
  209. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_launch_plan.py +0 -0
  210. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_loading_server.py +0 -0
  211. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_mode_consistency.py +0 -0
  212. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_mode_matrix.py +0 -0
  213. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_mode_roundtrip.py +0 -0
  214. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_nifti_meta.py +0 -0
  215. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_rgb_pixel_art.py +0 -0
  216. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_safe_server_stop.py +0 -0
  217. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_server_identity_status.py +0 -0
  218. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_session_spec.py +0 -0
  219. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_torch.py +0 -0
  220. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_view_component_integration.py +0 -0
  221. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_view_component_unit.py +0 -0
  222. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_view_handle_lifecycle.py +0 -0
  223. {arrayview-0.31.0 → arrayview-0.31.2}/tests/test_view_launch_execution.py +0 -0
  224. {arrayview-0.31.0 → arrayview-0.31.2}/tests/ui_audit.py +0 -0
  225. {arrayview-0.31.0 → arrayview-0.31.2}/tests/v_anim_frames/.gitkeep +0 -0
  226. {arrayview-0.31.0 → arrayview-0.31.2}/tests/visual_smoke.py +0 -0
  227. {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/AGENTS.md +0 -0
  228. {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/LICENSE +0 -0
  229. {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/extension.js +0 -0
  230. {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/lifecycle_helpers.js +0 -0
  231. {arrayview-0.31.0 → arrayview-0.31.2}/vscode-extension/package.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.31.0
3
+ Version: 0.31.2
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.31.0"
7
+ version = "0.31.2"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -34,7 +34,15 @@ def _build_metadata(session) -> dict:
34
34
  "is_rgb": session.rgb_axis is not None,
35
35
  "has_source_file": bool(getattr(session, "filepath", None)),
36
36
  }
37
- default_dims = _session_mod._startup_dims_for_data(session.data, target_shape)
37
+ collection_spatial_ndim = getattr(session, "collection_spatial_ndim", None)
38
+ default_shape = target_shape
39
+ if collection_spatial_ndim is not None:
40
+ spatial_ndim = int(collection_spatial_ndim)
41
+ if 2 <= spatial_ndim <= len(target_shape):
42
+ # Collection axes are appended after the image dimensions. They
43
+ # select a case; they must not influence the startup plane.
44
+ default_shape = target_shape[:spatial_ndim]
45
+ default_dims = _session_mod._startup_dims_for_data(session.data, default_shape)
38
46
  if default_dims is not None:
39
47
  meta["default_dims"] = [int(default_dims[0]), int(default_dims[1])]
40
48
  if getattr(session, "spatial_meta", None) is not None:
@@ -49,7 +57,6 @@ def _build_metadata(session) -> dict:
49
57
  )
50
58
  if getattr(session, "array_keys", None):
51
59
  meta["array_keys"] = session.array_keys
52
- collection_spatial_ndim = getattr(session, "collection_spatial_ndim", None)
53
60
  if collection_spatial_ndim is not None:
54
61
  meta["collection_spatial_ndim"] = int(collection_spatial_ndim)
55
62
  if getattr(session.data, "_av_ragged", False):
@@ -115,11 +115,17 @@ def _windows_process_start(pid: int) -> str | None:
115
115
  handle = kernel32.OpenProcess(0x1000, False, pid)
116
116
  if not handle:
117
117
  return None
118
+ still_active = 259
119
+ exit_code = ctypes.c_ulong()
118
120
  creation = ctypes.c_ulonglong()
119
121
  exit_time = ctypes.c_ulonglong()
120
122
  kernel = ctypes.c_ulonglong()
121
123
  user = ctypes.c_ulonglong()
122
124
  try:
125
+ if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
126
+ return None
127
+ if exit_code.value != still_active:
128
+ return None
123
129
  ok = kernel32.GetProcessTimes(handle, ctypes.byref(creation),
124
130
  ctypes.byref(exit_time), ctypes.byref(kernel),
125
131
  ctypes.byref(user))
@@ -0,0 +1,150 @@
1
+ """Shared overlay compositing helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+ from arrayview._render import (
8
+ _build_mosaic_grid,
9
+ _composite_overlay_mask,
10
+ _extract_overlay_mask,
11
+ _overlay_is_label_map,
12
+ )
13
+
14
+
15
+ def _parse_hex_color(hex_str: str) -> np.ndarray | None:
16
+ """Parse a 6-char hex RGB color."""
17
+ h = hex_str.strip().lstrip("#")
18
+ if len(h) != 6:
19
+ return None
20
+ try:
21
+ return np.array(
22
+ [int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)],
23
+ dtype=np.uint8,
24
+ )
25
+ except ValueError:
26
+ return None
27
+
28
+
29
+ def _composite_overlays(
30
+ rgba: np.ndarray,
31
+ overlay_sid_str: str | None,
32
+ overlay_colors_str: str | None,
33
+ overlay_alpha: float,
34
+ overlay_alphas_str: str | None,
35
+ overlay_outline: bool,
36
+ dim_x: int,
37
+ dim_y: int,
38
+ idx_tuple: tuple[int, ...],
39
+ shape_hw: tuple[int, int],
40
+ base_shape: tuple[int, ...] | None = None,
41
+ ) -> np.ndarray:
42
+ """Composite one or more overlay masks onto RGBA pixels."""
43
+ if not overlay_sid_str:
44
+ return rgba
45
+ sids = [s.strip() for s in overlay_sid_str.split(",") if s.strip()]
46
+ colors_raw = (
47
+ [c.strip() for c in overlay_colors_str.split(",")] if overlay_colors_str else []
48
+ )
49
+ alphas_raw = (
50
+ [a.strip() for a in overlay_alphas_str.split(",")] if overlay_alphas_str else []
51
+ )
52
+ for i, sid in enumerate(sids):
53
+ color = _parse_hex_color(colors_raw[i]) if i < len(colors_raw) else None
54
+ alpha = overlay_alpha
55
+ if i < len(alphas_raw):
56
+ try:
57
+ alpha = float(alphas_raw[i])
58
+ except ValueError:
59
+ alpha = overlay_alpha
60
+ alpha = max(0.0, min(1.0, alpha))
61
+ ov_raw = _extract_overlay_mask(
62
+ sid,
63
+ dim_x,
64
+ dim_y,
65
+ idx_tuple,
66
+ expected_shape=shape_hw,
67
+ base_shape=base_shape,
68
+ )
69
+ rgba = _composite_overlay_mask(
70
+ rgba,
71
+ ov_raw,
72
+ alpha=alpha,
73
+ is_label=_overlay_is_label_map(sid, ov_raw),
74
+ override_color=color,
75
+ outline_only=overlay_outline,
76
+ )
77
+ return rgba
78
+
79
+
80
+ def _composite_mosaic_overlays(
81
+ rgba: np.ndarray,
82
+ overlay_sid_str: str | None,
83
+ overlay_colors_str: str | None,
84
+ overlay_alpha: float,
85
+ overlay_alphas_str: str | None,
86
+ overlay_outline: bool,
87
+ dim_x: int,
88
+ dim_y: int,
89
+ dim_z: int,
90
+ idx_tuple: tuple[int, ...],
91
+ base_shape: tuple[int, ...],
92
+ mosaic_cols: int | None = None,
93
+ ) -> np.ndarray:
94
+ """Composite each overlay as a mosaic using the base mosaic's frame indices.
95
+
96
+ Missing overlay axes are resolved per frame by ``_extract_overlay_mask``.
97
+ That lets a mask omit, for example, an echo axis without materialising a
98
+ repeated full-volume copy.
99
+ """
100
+ if not overlay_sid_str:
101
+ return rgba
102
+ sids = [sid.strip() for sid in overlay_sid_str.split(",") if sid.strip()]
103
+ colors_raw = (
104
+ [color.strip() for color in overlay_colors_str.split(",")]
105
+ if overlay_colors_str
106
+ else []
107
+ )
108
+ alphas_raw = (
109
+ [alpha.strip() for alpha in overlay_alphas_str.split(",")]
110
+ if overlay_alphas_str
111
+ else []
112
+ )
113
+ tile_shape = (int(base_shape[dim_y]), int(base_shape[dim_x]))
114
+ n_frames = int(base_shape[dim_z])
115
+
116
+ for i, sid in enumerate(sids):
117
+ color = _parse_hex_color(colors_raw[i]) if i < len(colors_raw) else None
118
+ try:
119
+ alpha = float(alphas_raw[i]) if i < len(alphas_raw) else overlay_alpha
120
+ except ValueError:
121
+ alpha = overlay_alpha
122
+ alpha = max(0.0, min(1.0, alpha))
123
+
124
+ frames = []
125
+ for frame in range(n_frames):
126
+ frame_idx = list(idx_tuple)
127
+ frame_idx[dim_z] = frame
128
+ ov_raw = _extract_overlay_mask(
129
+ sid,
130
+ dim_x,
131
+ dim_y,
132
+ tuple(frame_idx),
133
+ expected_shape=tile_shape,
134
+ base_shape=base_shape,
135
+ )
136
+ frames.append(
137
+ ov_raw if ov_raw is not None else np.zeros(tile_shape, dtype=np.float32)
138
+ )
139
+ grid, _, _ = _build_mosaic_grid(frames, n_frames, cols=mosaic_cols)
140
+ # Gaps are transparent, not NaN label values or heatmap samples.
141
+ grid = np.nan_to_num(grid, nan=0.0)
142
+ rgba = _composite_overlay_mask(
143
+ rgba,
144
+ grid,
145
+ alpha=alpha,
146
+ is_label=_overlay_is_label_map(sid, grid),
147
+ override_color=color,
148
+ outline_only=overlay_outline,
149
+ )
150
+ return rgba
@@ -1,5 +1,6 @@
1
1
  """Rendering pipeline: colormaps, LUTs, slice extraction, RGBA, mosaic, RGB, preload."""
2
2
 
3
+ import itertools
3
4
  import threading
4
5
  import time
5
6
 
@@ -527,6 +528,7 @@ def _extract_overlay_mask(
527
528
  dim_y: int,
528
529
  idx_tuple: tuple[int, ...],
529
530
  expected_shape: tuple[int, int],
531
+ base_shape: tuple[int, ...] | None = None,
530
532
  ) -> np.ndarray | None:
531
533
  """Return the raw overlay slice (float32 H×W) for compositing, or None.
532
534
 
@@ -538,27 +540,43 @@ def _extract_overlay_mask(
538
540
  if ov_session is None:
539
541
  return None
540
542
 
541
- ov_ndim = ov_session.data.ndim
542
- if dim_x >= ov_ndim or dim_y >= ov_ndim:
543
+ ov_shape = tuple(int(size) for size in ov_session.shape)
544
+ main_shape = tuple(int(size) for size in (base_shape or ov_shape))
545
+ if len(ov_shape) > len(main_shape):
543
546
  return None
544
547
 
545
- idx_candidates: list[tuple[int, ...]] = [
546
- tuple(int(idx_tuple[i]) if i < len(idx_tuple) else 0 for i in range(ov_ndim))
547
- ]
548
- if len(idx_tuple) >= ov_ndim:
549
- trailing = tuple(int(v) for v in idx_tuple[-ov_ndim:])
550
- if trailing != idx_candidates[0]:
551
- idx_candidates.append(trailing)
552
-
553
- for ov_idx in idx_candidates:
554
- try:
555
- ov_raw = extract_slice(ov_session, dim_x, dim_y, list(ov_idx))
556
- except Exception:
557
- continue
558
- if ov_raw.shape != expected_shape:
559
- continue
560
- if np.any(np.isfinite(ov_raw) & (ov_raw != 0)):
561
- return ov_raw.astype(np.float32)
548
+ # An overlay may omit one or more non-spatial dimensions from the base.
549
+ # Match its axes to equal-sized base axes, then index only those axes. This
550
+ # broadcasts the overlay over every omitted axis without allocating a tiled
551
+ # copy (important for masks next to multi-echo or multi-channel data).
552
+ map_key = (main_shape, dim_x, dim_y)
553
+ axis_maps = getattr(ov_session, "_overlay_axis_maps", None)
554
+ if axis_maps is None:
555
+ axis_maps = {}
556
+ ov_session._overlay_axis_maps = axis_maps
557
+ if map_key not in axis_maps:
558
+ axis_maps[map_key] = next(
559
+ (
560
+ base_axes
561
+ for base_axes in itertools.combinations(range(len(main_shape)), len(ov_shape))
562
+ if dim_x in base_axes
563
+ and dim_y in base_axes
564
+ and tuple(main_shape[axis] for axis in base_axes) == ov_shape
565
+ ),
566
+ None,
567
+ )
568
+ base_axes = axis_maps[map_key]
569
+ if base_axes is None:
570
+ return None
571
+ ov_dim_x = base_axes.index(dim_x)
572
+ ov_dim_y = base_axes.index(dim_y)
573
+ ov_idx = [int(idx_tuple[axis]) for axis in base_axes]
574
+ try:
575
+ ov_raw = extract_slice(ov_session, ov_dim_x, ov_dim_y, ov_idx)
576
+ except Exception:
577
+ return None
578
+ if ov_raw.shape == expected_shape:
579
+ return ov_raw.astype(np.float32)
562
580
  return None
563
581
 
564
582
 
@@ -6,7 +6,7 @@ from fastapi import Depends, Response
6
6
  from fastapi.responses import JSONResponse
7
7
 
8
8
  from arrayview._diff import _compute_diff, _diff_histogram, _render_diff_rgba
9
- from arrayview._overlays import _composite_overlays
9
+ from arrayview._overlays import _composite_mosaic_overlays, _composite_overlays
10
10
  from arrayview._render import (
11
11
  LUTS,
12
12
  _build_mosaic_grid,
@@ -178,6 +178,11 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
178
178
  vmin_override=vmin_override,
179
179
  vmax_override=vmax_override,
180
180
  )
181
+ rgba = _composite_mosaic_overlays(
182
+ rgba, overlay_sid, overlay_colors, overlay_alpha, overlay_alphas,
183
+ overlay_outline, dim_x, dim_y, dim_z, idx_tuple, session.shape,
184
+ mosaic_cols,
185
+ )
181
186
  if vmin_override is not None and vmax_override is not None:
182
187
  vmin, vmax = float(vmin_override), float(vmax_override)
183
188
  else:
@@ -251,6 +256,7 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
251
256
  dim_y,
252
257
  idx_tuple,
253
258
  rgba.shape[:2],
259
+ session.shape,
254
260
  )
255
261
  _, vmin, vmax = _prepare_display(
256
262
  session,
@@ -263,6 +269,12 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
263
269
  )
264
270
  if qmri_role:
265
271
  vmin, vmax = _qmri_adjust_vmin_vmax(vmin, vmax, qmri_role)
272
+ if dim_z >= 0 and qmri_role:
273
+ rgba = _composite_mosaic_overlays(
274
+ rgba, overlay_sid, overlay_colors, overlay_alpha, overlay_alphas,
275
+ overlay_outline, dim_x, dim_y, dim_z, idx_tuple, session.shape,
276
+ mosaic_cols,
277
+ )
266
278
  render_ms = (time.perf_counter() - render_t0) * 1000.0
267
279
  encode_t0 = time.perf_counter()
268
280
  img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
@@ -8,7 +8,7 @@ from starlette.websockets import WebSocketDisconnect
8
8
  import arrayview._session as _session_mod
9
9
  from arrayview._analysis import _build_metadata
10
10
  from arrayview._lifecycle import release_session
11
- from arrayview._overlays import _composite_overlays
11
+ from arrayview._overlays import _composite_mosaic_overlays, _composite_overlays
12
12
  from arrayview._render import (
13
13
  LUTS,
14
14
  _prepare_display,
@@ -25,6 +25,7 @@ from arrayview._session import (
25
25
  SESSIONS,
26
26
  SHELL_SOCKETS,
27
27
  _render,
28
+ _schedule_overlay_prefetch,
28
29
  _schedule_prefetch,
29
30
  _vprint,
30
31
  wait_for_session_ready,
@@ -231,6 +232,12 @@ def register_websocket_routes(app) -> None:
231
232
  vmax_override=vmax_override,
232
233
  ),
233
234
  )
235
+ rgba = _composite_mosaic_overlays(
236
+ rgba, msg.get("overlay_sid"), msg.get("overlay_colors"),
237
+ float(msg.get("overlay_alpha", 0.45)), msg.get("overlay_alphas"),
238
+ bool(msg.get("overlay_outline", False)), dim_x, dim_y, dim_z,
239
+ idx_tuple, session.shape, mosaic_cols,
240
+ )
234
241
  h, w = rgba.shape[:2]
235
242
  raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
236
243
  _, vmin, vmax = _prepare_display(
@@ -340,6 +347,15 @@ def register_websocket_routes(app) -> None:
340
347
  dim_y,
341
348
  idx_tuple,
342
349
  (h, w),
350
+ session.shape,
351
+ )
352
+
353
+ if dim_z >= 0 and qmri_role:
354
+ rgba = _composite_mosaic_overlays(
355
+ rgba, msg.get("overlay_sid"), msg.get("overlay_colors"),
356
+ float(msg.get("overlay_alpha", 0.45)), msg.get("overlay_alphas"),
357
+ bool(msg.get("overlay_outline", False)), dim_x, dim_y, dim_z,
358
+ idx_tuple, session.shape, mosaic_cols,
343
359
  )
344
360
 
345
361
  render_ms = (time.perf_counter() - render_t0) * 1000.0
@@ -414,6 +430,16 @@ def register_websocket_routes(app) -> None:
414
430
  collection_axis,
415
431
  collection_direction,
416
432
  )
433
+ _schedule_overlay_prefetch(
434
+ session,
435
+ msg.get("overlay_sid"),
436
+ msg.get("overlay_alphas"),
437
+ dim_x,
438
+ dim_y,
439
+ list(idx_tuple),
440
+ collection_axis,
441
+ collection_direction,
442
+ )
417
443
  previous_indices = idx_tuple
418
444
  except Exception as _ws_exc:
419
445
  import traceback
@@ -101,6 +101,7 @@ def _ensure_render_thread() -> None:
101
101
  # Neighbor prefetch thread pool (Phase 3)
102
102
  # ---------------------------------------------------------------------------
103
103
  _PREFETCH_POOL = None
104
+ _OVERLAY_PREFETCH_POOL = None
104
105
  _PREFETCH_LOCK = threading.Lock()
105
106
 
106
107
 
@@ -116,6 +117,19 @@ def _get_prefetch_pool():
116
117
  return _PREFETCH_POOL
117
118
 
118
119
 
120
+ def _get_overlay_prefetch_pool():
121
+ """Return the small pool reserved for collection overlay lookahead."""
122
+ global _OVERLAY_PREFETCH_POOL
123
+ with _PREFETCH_LOCK:
124
+ if _OVERLAY_PREFETCH_POOL is None or _OVERLAY_PREFETCH_POOL._shutdown:
125
+ import concurrent.futures
126
+
127
+ _OVERLAY_PREFETCH_POOL = concurrent.futures.ThreadPoolExecutor(
128
+ max_workers=2, thread_name_prefix="arrayview-overlay-prefetch"
129
+ )
130
+ return _OVERLAY_PREFETCH_POOL
131
+
132
+
119
133
  def _schedule_prefetch(session, dim_x, dim_y, idx_list, slice_dim, direction):
120
134
  """Warm raw_cache for the next PREFETCH_NEIGHBORS slices in *direction*.
121
135
 
@@ -170,6 +184,85 @@ def _schedule_prefetch(session, dim_x, dim_y, idx_list, slice_dim, direction):
170
184
  pass # pool shutting down
171
185
 
172
186
 
187
+ def _schedule_overlay_prefetch(
188
+ session,
189
+ overlay_sid_str,
190
+ overlay_alphas_str,
191
+ dim_x,
192
+ dim_y,
193
+ idx_list,
194
+ collection_axis,
195
+ direction,
196
+ ):
197
+ """Warm the next collection volume for visible overlays.
198
+
199
+ Overlay work uses its own bounded pool so a long mask decompression cannot
200
+ hold up the main image's single-worker prefetch lane. New navigation
201
+ supersedes overlay jobs that have not started loading a volume yet.
202
+ """
203
+ if not overlay_sid_str:
204
+ return
205
+
206
+ sids = [sid.strip() for sid in overlay_sid_str.split(",") if sid.strip()]
207
+ alphas = [a.strip() for a in (overlay_alphas_str or "").split(",")]
208
+ visible_sids = []
209
+ for i, sid in enumerate(sids):
210
+ try:
211
+ alpha = float(alphas[i]) if i < len(alphas) and alphas[i] else 1.0
212
+ except ValueError:
213
+ alpha = 1.0
214
+ if alpha > 0.0:
215
+ visible_sids.append(sid)
216
+ if not visible_sids:
217
+ return
218
+
219
+ target = idx_list[collection_axis] + direction
220
+ if not 0 <= target < session.shape[collection_axis]:
221
+ return
222
+
223
+ request_key = (collection_axis, target, tuple(visible_sids))
224
+ if request_key == getattr(session, "_overlay_prefetch_request", None):
225
+ return
226
+ session._overlay_prefetch_request = request_key
227
+ generation = getattr(session, "_overlay_prefetch_generation", 0) + 1
228
+ session._overlay_prefetch_generation = generation
229
+
230
+ def _warm(overlay_sid):
231
+ if generation != getattr(session, "_overlay_prefetch_generation", 0):
232
+ return
233
+ overlay = SESSIONS.get(overlay_sid)
234
+ if overlay is None or collection_axis >= len(overlay.shape):
235
+ return
236
+ overlay_stack_axes = tuple(getattr(overlay.data, "_stack_axes", ()))
237
+ if collection_axis not in overlay_stack_axes or target >= overlay.shape[collection_axis]:
238
+ return
239
+ overlay_idx = list(idx_list[: len(overlay.shape)])
240
+ overlay_idx[collection_axis] = target
241
+ # Sparse --overlay-dir collections represent a missing patient with a
242
+ # None filepath. Avoid allocating a fresh full-size zero volume merely
243
+ # to prefetch an overlay that will be shown empty.
244
+ file_matrix = getattr(overlay.data, "_file_matrix", None)
245
+ if file_matrix is not None:
246
+ modality = 0
247
+ if len(overlay_stack_axes) > 1:
248
+ modality = overlay_idx[overlay_stack_axes[1]]
249
+ if not file_matrix[target][modality]:
250
+ return
251
+ try:
252
+ from arrayview._render import extract_slice
253
+
254
+ extract_slice(overlay, dim_x, dim_y, overlay_idx)
255
+ except Exception:
256
+ pass
257
+
258
+ try:
259
+ pool = _get_overlay_prefetch_pool()
260
+ for overlay_sid in visible_sids:
261
+ pool.submit(_warm, overlay_sid)
262
+ except RuntimeError:
263
+ pass # pool shutting down
264
+
265
+
173
266
  async def _render(loop: asyncio.AbstractEventLoop, func) -> object:
174
267
  """Await *func()* in the render thread without using concurrent.futures."""
175
268
  _ensure_render_thread()
@@ -476,9 +569,12 @@ __all__ = [
476
569
  "_render",
477
570
  # Prefetch
478
571
  "_PREFETCH_POOL",
572
+ "_OVERLAY_PREFETCH_POOL",
479
573
  "_PREFETCH_LOCK",
480
574
  "_get_prefetch_pool",
575
+ "_get_overlay_prefetch_pool",
481
576
  "_schedule_prefetch",
577
+ "_schedule_overlay_prefetch",
482
578
  # Session
483
579
  "Session",
484
580
  "SESSIONS",
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import json
7
+ import platform
7
8
  import subprocess
8
9
  import sys
9
10
  import threading
@@ -219,7 +220,7 @@ def _open_browser(
219
220
  port_hint = int(url.split(":")[2].split("/")[0].split("?")[0])
220
221
  except Exception:
221
222
  port_hint = parsed_port
222
- hostname = os.uname().nodename or "<this-host>"
223
+ hostname = platform.node() or "<this-host>"
223
224
  print(
224
225
  f"[ArrayView] Plain SSH session detected.\n"
225
226
  f"\n"
@@ -100,6 +100,14 @@ class TestHealth:
100
100
 
101
101
  assert body["default_dims"] == [0, 1]
102
102
 
103
+ def test_metadata_prefers_trailing_plane_when_it_has_the_two_largest_axes(self, client, tmp_path):
104
+ path = tmp_path / "startup_dims_volume.npy"
105
+ np.save(path, np.zeros((48, 224, 224), dtype=np.float32))
106
+
107
+ sid = client.post("/load", json={"filepath": str(path)}).json()["sid"]
108
+
109
+ assert client.get(f"/metadata/{sid}").json()["default_dims"] == [1, 2]
110
+
103
111
  def test_sessions_lists_registered_sid(self, client, sid_2d):
104
112
  r = client.get("/sessions")
105
113
  assert r.status_code == 200
@@ -4593,6 +4601,82 @@ class TestCropPersistence:
4593
4601
 
4594
4602
 
4595
4603
  class TestMultipleOverlays:
4604
+ def test_http_mosaic_repeats_overlay_across_missing_dimension(self, client, tmp_path):
4605
+ base_path = tmp_path / "mosaic-base.npy"
4606
+ np.save(base_path, np.zeros((4, 3, 6, 6), dtype=np.float32))
4607
+ base_sid = client.post("/load", json={"filepath": str(base_path)}).json()["sid"]
4608
+ overlay_data = np.zeros((4, 6, 6), dtype=np.uint8)
4609
+ overlay_data[2, 2:4, 2:4] = 1
4610
+ overlay_path = tmp_path / "mosaic-overlay.npy"
4611
+ np.save(overlay_path, overlay_data)
4612
+ overlay_sid = client.post("/load", json={"filepath": str(overlay_path)}).json()["sid"]
4613
+
4614
+ response = client.get(
4615
+ f"/slice/{base_sid}",
4616
+ params={
4617
+ "dim_x": 3,
4618
+ "dim_y": 2,
4619
+ "dim_z": 1,
4620
+ "indices": "2,0,3,3",
4621
+ "overlay_sid": overlay_sid,
4622
+ "overlay_colors": "ff0000",
4623
+ "overlay_alpha": 1.0,
4624
+ },
4625
+ )
4626
+ image = np.asarray(Image.open(io.BytesIO(response.content)))
4627
+ for x0 in (0, 8, 16):
4628
+ tile = image[:, x0 : x0 + 6]
4629
+ assert np.max(tile[:, :, 0].astype(int) - tile[:, :, 1].astype(int)) > 20
4630
+
4631
+ def test_mosaic_overlay_broadcasts_over_missing_mosaic_dimension(self):
4632
+ from arrayview._overlays import _composite_mosaic_overlays
4633
+ from arrayview._render import mosaic_shape
4634
+ from arrayview._session import SESSIONS, Session
4635
+
4636
+ base = Session(np.zeros((4, 3, 6, 6), dtype=np.float32))
4637
+ overlay_data = np.zeros((4, 6, 6), dtype=np.uint8)
4638
+ overlay_data[2, 3, 4] = 1
4639
+ overlay = Session(overlay_data)
4640
+ SESSIONS[overlay.sid] = overlay
4641
+ try:
4642
+ rows, cols = mosaic_shape(3)
4643
+ rgba = np.zeros((rows * 6 + (rows - 1) * 2, cols * 6 + (cols - 1) * 2, 4), dtype=np.uint8)
4644
+ result = _composite_mosaic_overlays(
4645
+ rgba, overlay.sid, "ff0000", 1.0, None, False,
4646
+ dim_x=3, dim_y=2, dim_z=1, idx_tuple=(2, 0, 3, 3),
4647
+ base_shape=base.shape,
4648
+ )
4649
+ for frame in range(3):
4650
+ row, col = divmod(frame, cols)
4651
+ assert result[row * 8 + 3, col * 8 + 4, 0] == 255
4652
+ finally:
4653
+ SESSIONS.pop(overlay.sid, None)
4654
+
4655
+ def test_overlay_broadcasts_over_missing_base_dimension(self):
4656
+ from arrayview._render import _extract_overlay_mask
4657
+ from arrayview._session import SESSIONS, Session
4658
+
4659
+ base = Session(np.zeros((48, 3, 224, 224), dtype=np.float32))
4660
+ overlay_data = np.zeros((48, 224, 224), dtype=np.uint8)
4661
+ overlay_data[12, 100, 150] = 1
4662
+ overlay = Session(overlay_data)
4663
+ SESSIONS[overlay.sid] = overlay
4664
+ try:
4665
+ for channel in range(3):
4666
+ result = _extract_overlay_mask(
4667
+ overlay.sid,
4668
+ dim_x=2,
4669
+ dim_y=3,
4670
+ idx_tuple=(12, channel, 112, 112),
4671
+ expected_shape=(224, 224),
4672
+ base_shape=base.shape,
4673
+ )
4674
+ assert result is not None
4675
+ assert result[150, 100] == 1
4676
+ assert overlay.data is overlay_data
4677
+ finally:
4678
+ SESSIONS.pop(overlay.sid, None)
4679
+
4596
4680
  def test_single_overlay_with_color(self, client, sid_2d):
4597
4681
  """A single binary mask overlay with explicit hex color renders without error."""
4598
4682
  from arrayview._app import SESSIONS, Session