arrayview 0.23.0__tar.gz → 0.24.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/architecture.md +13 -1
- {arrayview-0.23.0 → arrayview-0.24.0}/PKG-INFO +1 -1
- {arrayview-0.23.0 → arrayview-0.24.0}/pyproject.toml +1 -1
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_launcher.py +74 -52
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_platform.py +40 -42
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_rendering.py +33 -6
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_websocket.py +23 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_shell.html +20 -20
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_stdio_server.py +81 -28
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_viewer.html +1062 -525
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode_extension.py +1 -1
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode_signal.py +3 -45
- arrayview-0.24.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_api.py +125 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_browser.py +94 -12
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_loading_server.py +1 -1
- {arrayview-0.23.0 → arrayview-0.24.0}/uv.lock +1 -1
- {arrayview-0.23.0 → arrayview-0.24.0}/vscode-extension/extension.js +73 -5
- {arrayview-0.23.0 → arrayview-0.24.0}/vscode-extension/package.json +1 -1
- arrayview-0.23.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/frontend-designer/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/running-code.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/session-management.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/tracing.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/todo-workflow/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.github/copilot-instructions.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.gitignore +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.ignore +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/AGENTS.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/ROUTER.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/SETUP.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/SYNC.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/conventions.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/decisions.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/frontend.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/project-state.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/render-pipeline.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/setup.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/stack.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/INDEX.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/README.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/add-file-format.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/add-server-endpoint.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/animation-verify.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/debug-render.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/debug-vscode-extension-python.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/extend-compare-mode.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/extract-server-route-module.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/frontend-change.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/setup.sh +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.mex/sync.sh +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.opencode/opencode.json +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/theme-dark.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.python-version +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/.vscode/settings.json +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/AGENTS.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/CONTRIBUTING.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/DESIGN.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/LICENSE +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/README.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/comparing.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/configuration.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/display.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/index.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/loading.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/logo.png +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/measurement.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/remote.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/docs/viewing.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/matlab/arrayview.m +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/mkdocs.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/plans/2026-04-14-immersive-animation.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/plans/2026-05-07-unified-colormap-picker.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/plans/egg-placement-mockup.html +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/plans/refactoring_plan.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/scripts/demo.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/scripts/release.sh +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_codex_open.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_diff.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_io.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_loading.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_server.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_session.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode_shm.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/capture_v_animation.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/conftest.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_backend_shared.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_cli.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_command_reachability.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_config.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_interactions.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_mode_entry_batching.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_stdio_server.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_torch.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/ui_audit.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/tests/visual_smoke.py +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/vscode-extension/AGENTS.md +0 -0
- {arrayview-0.23.0 → arrayview-0.24.0}/vscode-extension/LICENSE +0 -0
|
@@ -18,7 +18,7 @@ edges:
|
|
|
18
18
|
condition: when the task involves _viewer.html, modes, reconcilers, or the View Component System
|
|
19
19
|
- target: context/render-pipeline.md
|
|
20
20
|
condition: when the task involves slice extraction, colormaps, caching, or the render thread
|
|
21
|
-
last_updated: 2026-05-
|
|
21
|
+
last_updated: 2026-05-26
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
# Architecture
|
|
@@ -63,6 +63,18 @@ pywebview, or system browser).
|
|
|
63
63
|
- **`_stdio_server.py`** — Alternative to FastAPI for VS Code tunnel (direct webview): JSON on stdin, length-prefixed binary on stdout.
|
|
64
64
|
- **`_viewer.html`** — The entire frontend (~24 100 lines). CSS + JS in one file, no build step. Canvas-based rendering, WebSocket binary protocol, all viewing modes, reconcilers, command registry. See `context/frontend.md`.
|
|
65
65
|
|
|
66
|
+
## Frontend Tool Lifecycle
|
|
67
|
+
|
|
68
|
+
Tool launch, tool activation, and drawer visibility are separate states.
|
|
69
|
+
|
|
70
|
+
- Selecting an actionable tool activates it immediately and opens the right drawer.
|
|
71
|
+
- Closing the right drawer hides controls only; it does not deactivate the active tool.
|
|
72
|
+
- Re-selecting an active tool reopens its drawer when hidden, or hides the drawer when already visible.
|
|
73
|
+
- `Esc` targets visible UI first: drawer, launcher, picker, or help before exiting active tools.
|
|
74
|
+
- Passive tools such as Save open a drawer without entering a mode.
|
|
75
|
+
- Overlay and vector tools show their rendered layer immediately when selected and available.
|
|
76
|
+
- Existing tool output persists after mode exit unless the tool has an explicit destructive action.
|
|
77
|
+
|
|
66
78
|
## Display Routing
|
|
67
79
|
|
|
68
80
|
| Environment | Default display | Server mode |
|
|
@@ -156,6 +156,7 @@ def _vprint(*args, **kwargs) -> None:
|
|
|
156
156
|
# ── Subprocess GUI Launcher ───────────────────────────────────────
|
|
157
157
|
|
|
158
158
|
_ICON_PNG_PATH: str | None = None
|
|
159
|
+
_LOOPBACK_HOST = "localhost"
|
|
159
160
|
|
|
160
161
|
|
|
161
162
|
def _get_icon_png_path() -> str | None:
|
|
@@ -177,6 +178,57 @@ def _get_icon_png_path() -> str | None:
|
|
|
177
178
|
return _ICON_PNG_PATH or None
|
|
178
179
|
|
|
179
180
|
|
|
181
|
+
def _build_inline_shell_html(url: str, shell_port: int) -> str | None:
|
|
182
|
+
"""Return embedded shell HTML for a cold-start native window."""
|
|
183
|
+
try:
|
|
184
|
+
shell_html = _pkg_files("arrayview").joinpath("_shell.html").read_text(
|
|
185
|
+
encoding="utf-8"
|
|
186
|
+
)
|
|
187
|
+
parsed = urllib.parse.urlparse(url)
|
|
188
|
+
inline_query = parsed.query
|
|
189
|
+
shell_html = shell_html.replace(
|
|
190
|
+
"</head>",
|
|
191
|
+
f"<script>"
|
|
192
|
+
f"window.__av_inline=true;"
|
|
193
|
+
f"window.__av_inlineQuery={inline_query!r};"
|
|
194
|
+
f"</script>\n"
|
|
195
|
+
f'<base href="http://{_LOOPBACK_HOST}:{shell_port}/">\n'
|
|
196
|
+
f"</head>",
|
|
197
|
+
1,
|
|
198
|
+
)
|
|
199
|
+
# Fix WebSocket URL — location.host is "" in inline html= mode
|
|
200
|
+
shell_html = shell_html.replace(
|
|
201
|
+
"`${proto}//${location.host}/ws/shell`",
|
|
202
|
+
f"`ws://{_LOOPBACK_HOST}:{shell_port}/ws/shell`",
|
|
203
|
+
)
|
|
204
|
+
return shell_html
|
|
205
|
+
except Exception:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _make_loopback_socket(port: int) -> "socket.socket":
|
|
210
|
+
"""Bind a TCP listener on the same loopback host the viewer URLs use."""
|
|
211
|
+
for family, socktype, proto, _, sockaddr in socket.getaddrinfo(
|
|
212
|
+
_LOOPBACK_HOST,
|
|
213
|
+
port,
|
|
214
|
+
type=socket.SOCK_STREAM,
|
|
215
|
+
):
|
|
216
|
+
sock = None
|
|
217
|
+
try:
|
|
218
|
+
sock = socket.socket(family, socktype, proto)
|
|
219
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
220
|
+
sock.bind(sockaddr)
|
|
221
|
+
sock.listen(128)
|
|
222
|
+
sock.set_inheritable(True)
|
|
223
|
+
return sock
|
|
224
|
+
except OSError:
|
|
225
|
+
try:
|
|
226
|
+
sock.close()
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
raise OSError(f"Could not bind {_LOOPBACK_HOST}:{port}")
|
|
230
|
+
|
|
231
|
+
|
|
180
232
|
def _open_webview(
|
|
181
233
|
url: str,
|
|
182
234
|
win_w: int,
|
|
@@ -199,25 +251,9 @@ def _open_webview(
|
|
|
199
251
|
inline_html_b64 = None
|
|
200
252
|
|
|
201
253
|
if shell_port is not None:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
shell_html = _pkg_files("arrayview").joinpath("_shell.html").read_text(encoding="utf-8")
|
|
205
|
-
# Inject inline-mode flag and hardcoded host (location.host is empty in html= mode)
|
|
206
|
-
shell_html = shell_html.replace(
|
|
207
|
-
"</head>",
|
|
208
|
-
f'<script>window.__av_inline=true;</script>\n'
|
|
209
|
-
f'<base href="http://localhost:{shell_port}/">\n'
|
|
210
|
-
f"</head>",
|
|
211
|
-
1,
|
|
212
|
-
)
|
|
213
|
-
# Fix WebSocket URL — location.host is "" in inline html= mode
|
|
214
|
-
shell_html = shell_html.replace(
|
|
215
|
-
"`${proto}//${location.host}/ws/shell`",
|
|
216
|
-
f"`ws://localhost:{shell_port}/ws/shell`",
|
|
217
|
-
)
|
|
254
|
+
shell_html = _build_inline_shell_html(url, shell_port)
|
|
255
|
+
if shell_html is not None:
|
|
218
256
|
inline_html_b64 = _b64.b64encode(shell_html.encode()).decode()
|
|
219
|
-
except Exception:
|
|
220
|
-
pass # fall back to URL mode
|
|
221
257
|
|
|
222
258
|
if inline_html_b64:
|
|
223
259
|
script_lines = [
|
|
@@ -469,7 +505,7 @@ def _open_webview_cli(
|
|
|
469
505
|
|
|
470
506
|
def _server_alive(port: int, timeout: float = 0.5) -> bool:
|
|
471
507
|
"""Return True only if an ArrayView server is responding on the port."""
|
|
472
|
-
url = f"http://
|
|
508
|
+
url = f"http://{_LOOPBACK_HOST}:{port}/ping"
|
|
473
509
|
try:
|
|
474
510
|
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
475
511
|
if resp.status != 200:
|
|
@@ -482,7 +518,7 @@ def _server_alive(port: int, timeout: float = 0.5) -> bool:
|
|
|
482
518
|
|
|
483
519
|
def _server_pid(port: int) -> int | None:
|
|
484
520
|
"""Return the pid of the responding ArrayView server, or None if unreachable."""
|
|
485
|
-
url = f"http://
|
|
521
|
+
url = f"http://{_LOOPBACK_HOST}:{port}/ping"
|
|
486
522
|
try:
|
|
487
523
|
with urllib.request.urlopen(url, timeout=0.5) as resp:
|
|
488
524
|
if resp.status != 200:
|
|
@@ -497,7 +533,7 @@ def _server_pid(port: int) -> int | None:
|
|
|
497
533
|
|
|
498
534
|
def _server_hostname(port: int, timeout: float = 0.5) -> str | None:
|
|
499
535
|
"""Return the hostname reported by the ArrayView server on ``port``, or None."""
|
|
500
|
-
url = f"http://
|
|
536
|
+
url = f"http://{_LOOPBACK_HOST}:{port}/ping"
|
|
501
537
|
try:
|
|
502
538
|
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
503
539
|
if resp.status != 200:
|
|
@@ -515,7 +551,7 @@ def _relay_array_to_server(
|
|
|
515
551
|
port: int,
|
|
516
552
|
name: str,
|
|
517
553
|
rgb: bool = False,
|
|
518
|
-
relay_host: str =
|
|
554
|
+
relay_host: str = _LOOPBACK_HOST,
|
|
519
555
|
) -> None:
|
|
520
556
|
"""Load *filepath* locally and POST the bytes to an ArrayView relay server.
|
|
521
557
|
|
|
@@ -523,7 +559,7 @@ def _relay_array_to_server(
|
|
|
523
559
|
ArrayView server (e.g. tunnel-remote). The relay server registers the
|
|
524
560
|
session and writes its own VS Code signal file so a viewer tab opens there.
|
|
525
561
|
|
|
526
|
-
``relay_host`` defaults to
|
|
562
|
+
``relay_host`` defaults to localhost; only change it when the relay server
|
|
527
563
|
is genuinely on a different network interface (rare).
|
|
528
564
|
"""
|
|
529
565
|
import base64
|
|
@@ -567,7 +603,7 @@ type _CompareSids = list[str] | tuple[str, ...]
|
|
|
567
603
|
|
|
568
604
|
def _server_json_request(port: int, path: str, payload: dict) -> dict:
|
|
569
605
|
req = urllib.request.Request(
|
|
570
|
-
f"http://
|
|
606
|
+
f"http://{_LOOPBACK_HOST}:{port}{path}",
|
|
571
607
|
data=json.dumps(payload).encode(),
|
|
572
608
|
headers={"Content-Type": "application/json"},
|
|
573
609
|
method="POST",
|
|
@@ -1002,7 +1038,7 @@ def _handle_cli_spawned_daemon(
|
|
|
1002
1038
|
window_mode=window_mode,
|
|
1003
1039
|
floating=floating,
|
|
1004
1040
|
is_remote=is_remote,
|
|
1005
|
-
webview_already_opened=
|
|
1041
|
+
webview_already_opened=early_webview_opened,
|
|
1006
1042
|
)
|
|
1007
1043
|
|
|
1008
1044
|
|
|
@@ -1170,7 +1206,7 @@ def _promote_view_to_vscode_terminal(
|
|
|
1170
1206
|
|
|
1171
1207
|
def _port_in_use(port: int) -> bool:
|
|
1172
1208
|
try:
|
|
1173
|
-
with socket.create_connection((
|
|
1209
|
+
with socket.create_connection((_LOOPBACK_HOST, port), timeout=0.3):
|
|
1174
1210
|
return True
|
|
1175
1211
|
except OSError:
|
|
1176
1212
|
return False
|
|
@@ -1311,25 +1347,17 @@ async def _serve_background(port: int, stop_when_closed: bool = False):
|
|
|
1311
1347
|
_loading_port = None # reset for this server lifetime
|
|
1312
1348
|
_session_mod.SERVER_LOOP = asyncio.get_running_loop()
|
|
1313
1349
|
_session_mod.SERVER_PORT = port
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
#
|
|
1317
|
-
|
|
1318
|
-
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
1319
|
-
sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
|
|
1320
|
-
sock.bind(("127.0.0.1", port))
|
|
1321
|
-
sock.listen(128)
|
|
1322
|
-
sock.set_inheritable(True)
|
|
1350
|
+
# Bind on the same loopback hostname the viewer URLs use. On macOS,
|
|
1351
|
+
# localhost often resolves to ::1 first, so binding only 127.0.0.1 can
|
|
1352
|
+
# leave the native shell stuck on its loading overlay forever.
|
|
1353
|
+
sock = _make_loopback_socket(port)
|
|
1323
1354
|
|
|
1324
1355
|
# Bind the loading-page server on an OS-chosen ephemeral port.
|
|
1325
1356
|
# This uses only stdlib so it starts in microseconds — well before
|
|
1326
1357
|
# uvicorn's heavy imports finish. _loading_port is read by the main
|
|
1327
1358
|
# thread after _server_ready_event fires.
|
|
1328
1359
|
try:
|
|
1329
|
-
_lsock =
|
|
1330
|
-
_lsock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
|
|
1331
|
-
_lsock.bind(("127.0.0.1", 0))
|
|
1332
|
-
_lsock.listen(16)
|
|
1360
|
+
_lsock = _make_loopback_socket(0)
|
|
1333
1361
|
_loading_port = _lsock.getsockname()[1]
|
|
1334
1362
|
threading.Thread(
|
|
1335
1363
|
target=_run_loading_server, args=(_lsock,), daemon=True
|
|
@@ -1361,7 +1389,7 @@ def _with_loading(url: str) -> str:
|
|
|
1361
1389
|
"""
|
|
1362
1390
|
if _loading_port is not None:
|
|
1363
1391
|
encoded = urllib.parse.quote(url, safe="")
|
|
1364
|
-
return f"http://
|
|
1392
|
+
return f"http://{_LOOPBACK_HOST}:{_loading_port}/?target={encoded}"
|
|
1365
1393
|
return url
|
|
1366
1394
|
|
|
1367
1395
|
|
|
@@ -1615,7 +1643,7 @@ class ViewHandle(str):
|
|
|
1615
1643
|
_np.save(buf, arr)
|
|
1616
1644
|
body = buf.getvalue()
|
|
1617
1645
|
request = _req.Request(
|
|
1618
|
-
f"http://
|
|
1646
|
+
f"http://{_LOOPBACK_HOST}:{self._port}/update/{self._sid}",
|
|
1619
1647
|
data=body,
|
|
1620
1648
|
method="POST",
|
|
1621
1649
|
)
|
|
@@ -1625,7 +1653,7 @@ class ViewHandle(str):
|
|
|
1625
1653
|
except Exception as e:
|
|
1626
1654
|
raise RuntimeError(
|
|
1627
1655
|
f"[ArrayView] Failed to update viewer: {e}\n"
|
|
1628
|
-
f" URL: http://
|
|
1656
|
+
f" URL: http://{_LOOPBACK_HOST}:{self._port}/update/{self._sid}\n"
|
|
1629
1657
|
f" Is the ArrayView server still running?"
|
|
1630
1658
|
) from e
|
|
1631
1659
|
|
|
@@ -2507,7 +2535,7 @@ def _serve_empty(port: int) -> None:
|
|
|
2507
2535
|
threading.Thread(
|
|
2508
2536
|
target=lambda: _uvicorn().run(
|
|
2509
2537
|
_server_mod().app,
|
|
2510
|
-
host=
|
|
2538
|
+
host=_LOOPBACK_HOST,
|
|
2511
2539
|
port=port,
|
|
2512
2540
|
log_level="error",
|
|
2513
2541
|
timeout_keep_alive=30,
|
|
@@ -2548,13 +2576,7 @@ def _serve_daemon(
|
|
|
2548
2576
|
_session_mod.PENDING_SESSION_EVENTS[sid] = _pending_event
|
|
2549
2577
|
_session_mod.SERVER_PORT = port
|
|
2550
2578
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
2554
|
-
sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
|
|
2555
|
-
sock.bind(("127.0.0.1", port))
|
|
2556
|
-
sock.listen(128)
|
|
2557
|
-
sock.set_inheritable(True)
|
|
2579
|
+
sock = _make_loopback_socket(port)
|
|
2558
2580
|
|
|
2559
2581
|
def _run_uvicorn_on_socket():
|
|
2560
2582
|
config = _uvicorn().Config(
|
|
@@ -2741,7 +2763,7 @@ def _start_watch_thread(filepath: str, sid: str, port: int) -> None:
|
|
|
2741
2763
|
last_mtime = mtime
|
|
2742
2764
|
try:
|
|
2743
2765
|
req = _urlreq.Request(
|
|
2744
|
-
f"http://
|
|
2766
|
+
f"http://{_LOOPBACK_HOST}:{port}/reload/{sid}",
|
|
2745
2767
|
data=b"",
|
|
2746
2768
|
method="POST",
|
|
2747
2769
|
)
|
|
@@ -3209,7 +3231,7 @@ def arrayview():
|
|
|
3209
3231
|
if ":" in relay_str:
|
|
3210
3232
|
relay_host, relay_port_str = relay_str.rsplit(":", 1)
|
|
3211
3233
|
else:
|
|
3212
|
-
relay_host, relay_port_str =
|
|
3234
|
+
relay_host, relay_port_str = _LOOPBACK_HOST, relay_str
|
|
3213
3235
|
try:
|
|
3214
3236
|
relay_port = int(relay_port_str)
|
|
3215
3237
|
except ValueError:
|
|
@@ -6,6 +6,44 @@ import os
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
def get_ppid(pid: int) -> int:
|
|
11
|
+
"""Return parent PID of *pid*, or -1 if unavailable.
|
|
12
|
+
|
|
13
|
+
Cross-platform: /proc on Linux, ps on macOS, wmic on Windows.
|
|
14
|
+
"""
|
|
15
|
+
if sys.platform == "win32":
|
|
16
|
+
try:
|
|
17
|
+
r = subprocess.run(
|
|
18
|
+
["wmic", "process", "where", f"processid={pid}",
|
|
19
|
+
"get", "parentprocessid"],
|
|
20
|
+
capture_output=True, text=True, timeout=5,
|
|
21
|
+
)
|
|
22
|
+
for line in r.stdout.strip().splitlines():
|
|
23
|
+
val = line.strip()
|
|
24
|
+
if val.isdigit():
|
|
25
|
+
return int(val)
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
return -1
|
|
29
|
+
try:
|
|
30
|
+
with open(f"/proc/{pid}/status") as fh:
|
|
31
|
+
for line in fh:
|
|
32
|
+
if line.startswith("PPid:"):
|
|
33
|
+
return int(line.split()[1])
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
try:
|
|
37
|
+
r = subprocess.run(
|
|
38
|
+
["ps", "-p", str(pid), "-o", "ppid="],
|
|
39
|
+
capture_output=True, text=True, timeout=2,
|
|
40
|
+
)
|
|
41
|
+
return int(r.stdout.strip())
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
return -1
|
|
45
|
+
|
|
46
|
+
|
|
9
47
|
# ---------------------------------------------------------------------------
|
|
10
48
|
# Jupyter
|
|
11
49
|
# ---------------------------------------------------------------------------
|
|
@@ -58,26 +96,6 @@ def _in_matlab() -> bool:
|
|
|
58
96
|
_MATLAB_CACHE = True
|
|
59
97
|
return True
|
|
60
98
|
|
|
61
|
-
def _ppid(pid: int) -> int:
|
|
62
|
-
try:
|
|
63
|
-
with open(f"/proc/{pid}/status") as fh:
|
|
64
|
-
for line in fh:
|
|
65
|
-
if line.startswith("PPid:"):
|
|
66
|
-
return int(line.split()[1])
|
|
67
|
-
except Exception:
|
|
68
|
-
pass
|
|
69
|
-
try:
|
|
70
|
-
r = subprocess.run(
|
|
71
|
-
["ps", "-p", str(pid), "-o", "ppid="],
|
|
72
|
-
capture_output=True,
|
|
73
|
-
text=True,
|
|
74
|
-
timeout=2,
|
|
75
|
-
)
|
|
76
|
-
return int(r.stdout.strip())
|
|
77
|
-
except Exception:
|
|
78
|
-
pass
|
|
79
|
-
return -1
|
|
80
|
-
|
|
81
99
|
def _command_from_pid(pid: int) -> str:
|
|
82
100
|
try:
|
|
83
101
|
with open(f"/proc/{pid}/cmdline", "rb") as fh:
|
|
@@ -103,7 +121,7 @@ def _in_matlab() -> bool:
|
|
|
103
121
|
if "matlab" in cmd:
|
|
104
122
|
_MATLAB_CACHE = True
|
|
105
123
|
return True
|
|
106
|
-
pid =
|
|
124
|
+
pid = get_ppid(pid)
|
|
107
125
|
if pid <= 1:
|
|
108
126
|
break
|
|
109
127
|
|
|
@@ -130,26 +148,6 @@ def _find_vscode_ipc_hook() -> str | None:
|
|
|
130
148
|
if _VSCODE_IPC_HOOK_CACHE != "__unset__":
|
|
131
149
|
return _VSCODE_IPC_HOOK_CACHE
|
|
132
150
|
|
|
133
|
-
def _ppid(pid: int) -> int:
|
|
134
|
-
try:
|
|
135
|
-
with open(f"/proc/{pid}/status") as fh:
|
|
136
|
-
for line in fh:
|
|
137
|
-
if line.startswith("PPid:"):
|
|
138
|
-
return int(line.split()[1])
|
|
139
|
-
except Exception:
|
|
140
|
-
pass
|
|
141
|
-
try:
|
|
142
|
-
r = subprocess.run(
|
|
143
|
-
["ps", "-p", str(pid), "-o", "ppid="],
|
|
144
|
-
capture_output=True,
|
|
145
|
-
text=True,
|
|
146
|
-
timeout=2,
|
|
147
|
-
)
|
|
148
|
-
return int(r.stdout.strip())
|
|
149
|
-
except Exception:
|
|
150
|
-
pass
|
|
151
|
-
return -1
|
|
152
|
-
|
|
153
151
|
def _ipc_from_pid(pid: int) -> str:
|
|
154
152
|
# Linux: /proc/<pid>/environ (null-separated KEY=VALUE pairs)
|
|
155
153
|
try:
|
|
@@ -184,7 +182,7 @@ def _find_vscode_ipc_hook() -> str | None:
|
|
|
184
182
|
# Walk up to 12 ancestor processes
|
|
185
183
|
pid = os.getpid()
|
|
186
184
|
for _ in range(12):
|
|
187
|
-
pid =
|
|
185
|
+
pid = get_ppid(pid)
|
|
188
186
|
if pid <= 1:
|
|
189
187
|
break
|
|
190
188
|
val = _ipc_from_pid(pid)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import io
|
|
2
|
+
import time
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
from fastapi import Depends, Response
|
|
@@ -93,8 +94,11 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
93
94
|
te: float | None = None,
|
|
94
95
|
tr: float | None = None,
|
|
95
96
|
ti: float | None = None,
|
|
97
|
+
perf: bool = False,
|
|
96
98
|
session=Depends(get_session_or_404),
|
|
97
99
|
):
|
|
100
|
+
total_t0 = time.perf_counter()
|
|
101
|
+
render_t0 = total_t0
|
|
98
102
|
idx_tuple = tuple(int(x) for x in indices.split(","))
|
|
99
103
|
if synthetic_mri:
|
|
100
104
|
try:
|
|
@@ -252,17 +256,40 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
|
|
|
252
256
|
vmin_override=vmin_override,
|
|
253
257
|
vmax_override=vmax_override,
|
|
254
258
|
)
|
|
259
|
+
render_ms = (time.perf_counter() - render_t0) * 1000.0
|
|
260
|
+
encode_t0 = time.perf_counter()
|
|
255
261
|
img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
|
|
256
262
|
buf = io.BytesIO()
|
|
257
263
|
img.save(buf, format="JPEG", quality=90)
|
|
264
|
+
payload = buf.getvalue()
|
|
265
|
+
encode_ms = (time.perf_counter() - encode_t0) * 1000.0
|
|
266
|
+
total_ms = (time.perf_counter() - total_t0) * 1000.0
|
|
267
|
+
headers = {
|
|
268
|
+
"Cache-Control": "max-age=300",
|
|
269
|
+
"X-ArrayView-Vmin": str(vmin),
|
|
270
|
+
"X-ArrayView-Vmax": str(vmax),
|
|
271
|
+
}
|
|
272
|
+
if perf:
|
|
273
|
+
payload_bytes = len(payload)
|
|
274
|
+
headers.update(
|
|
275
|
+
{
|
|
276
|
+
"Server-Timing": (
|
|
277
|
+
f"render;dur={render_ms:.3f}, "
|
|
278
|
+
f"encode;dur={encode_ms:.3f}, total;dur={total_ms:.3f}"
|
|
279
|
+
),
|
|
280
|
+
"X-ArrayView-Render-Ms": f"{render_ms:.3f}",
|
|
281
|
+
"X-ArrayView-Encode-Ms": f"{encode_ms:.3f}",
|
|
282
|
+
"X-ArrayView-Total-Ms": f"{total_ms:.3f}",
|
|
283
|
+
"X-ArrayView-Payload-Bytes": str(payload_bytes),
|
|
284
|
+
"X-ArrayView-Raw-Cache-Entries": str(len(session.raw_cache)),
|
|
285
|
+
"X-ArrayView-RGBA-Cache-Entries": str(len(session.rgba_cache)),
|
|
286
|
+
"X-ArrayView-Mosaic-Cache-Entries": str(len(session.mosaic_cache)),
|
|
287
|
+
}
|
|
288
|
+
)
|
|
258
289
|
return Response(
|
|
259
|
-
content=
|
|
290
|
+
content=payload,
|
|
260
291
|
media_type="image/jpeg",
|
|
261
|
-
headers=
|
|
262
|
-
"Cache-Control": "max-age=300",
|
|
263
|
-
"X-ArrayView-Vmin": str(vmin),
|
|
264
|
-
"X-ArrayView-Vmax": str(vmax),
|
|
265
|
-
},
|
|
292
|
+
headers=headers,
|
|
266
293
|
)
|
|
267
294
|
|
|
268
295
|
@app.get("/diff/{sid_a}/{sid_b}")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import time
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
from fastapi import WebSocket
|
|
@@ -159,6 +160,9 @@ def register_websocket_routes(app) -> None:
|
|
|
159
160
|
qmri_dim = int(msg.get("qmri_dim", -1))
|
|
160
161
|
qmri_role = str(msg.get("qmri_role", ""))
|
|
161
162
|
synthetic_mri = str(msg.get("synthetic_mri", ""))
|
|
163
|
+
perf = bool(msg.get("perf", False))
|
|
164
|
+
total_t0 = time.perf_counter()
|
|
165
|
+
render_t0 = total_t0
|
|
162
166
|
|
|
163
167
|
if synthetic_mri:
|
|
164
168
|
te = msg.get("te")
|
|
@@ -331,6 +335,8 @@ def register_websocket_routes(app) -> None:
|
|
|
331
335
|
(h, w),
|
|
332
336
|
)
|
|
333
337
|
|
|
338
|
+
render_ms = (time.perf_counter() - render_t0) * 1000.0
|
|
339
|
+
post_t0 = time.perf_counter()
|
|
334
340
|
header = np.array([seq, w, h], dtype=np.uint32).tobytes()
|
|
335
341
|
vminmax = np.array([vmin, vmax], dtype=np.float32).tobytes()
|
|
336
342
|
if canvas_w and canvas_h and (w > canvas_w or h > canvas_h):
|
|
@@ -362,6 +368,23 @@ def register_websocket_routes(app) -> None:
|
|
|
362
368
|
vf_scale = np.array([vf_result["scale"]], dtype=np.float32).tobytes()
|
|
363
369
|
payload += vf_hdr + vf_scale + arrows.tobytes()
|
|
364
370
|
await ws.send_bytes(payload)
|
|
371
|
+
if perf:
|
|
372
|
+
total_ms = (time.perf_counter() - total_t0) * 1000.0
|
|
373
|
+
await ws.send_json(
|
|
374
|
+
{
|
|
375
|
+
"type": "render_timing",
|
|
376
|
+
"seq": seq,
|
|
377
|
+
"total_ms": total_ms,
|
|
378
|
+
"render_ms": render_ms,
|
|
379
|
+
"post_ms": (time.perf_counter() - post_t0) * 1000.0,
|
|
380
|
+
"payload_bytes": len(payload),
|
|
381
|
+
"width": int(w),
|
|
382
|
+
"height": int(h),
|
|
383
|
+
"raw_cache_entries": len(session.raw_cache),
|
|
384
|
+
"rgba_cache_entries": len(session.rgba_cache),
|
|
385
|
+
"mosaic_cache_entries": len(session.mosaic_cache),
|
|
386
|
+
}
|
|
387
|
+
)
|
|
365
388
|
|
|
366
389
|
if slice_dim >= 0 and not (dim_z >= 0):
|
|
367
390
|
_schedule_prefetch(
|
|
@@ -243,26 +243,26 @@
|
|
|
243
243
|
}
|
|
244
244
|
connectShellWS();
|
|
245
245
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
}
|
|
246
|
+
const initParams = new URLSearchParams(
|
|
247
|
+
window.__av_inline ? (window.__av_inlineQuery || '') : window.location.search
|
|
248
|
+
);
|
|
249
|
+
const initSid = initParams.get('init_sid');
|
|
250
|
+
const initName = initParams.get('init_name');
|
|
251
|
+
const initCompareSid = initParams.get('init_compare_sid');
|
|
252
|
+
const initCompareSids = initParams.get('init_compare_sids');
|
|
253
|
+
if (initSid) {
|
|
254
|
+
let url = `/?sid=${initSid}`;
|
|
255
|
+
if (initCompareSid) url += `&compare_sid=${initCompareSid}&compare_sids=${initCompareSids}`;
|
|
256
|
+
addTab(initSid, initName || 'Array', url);
|
|
257
|
+
} else if (!window.__av_inline) {
|
|
258
|
+
fetch('/sessions').then(r => r.json()).then(sessions => {
|
|
259
|
+
if (sessions.length > 0) {
|
|
260
|
+
sessions.forEach(s => addTab(s.sid, s.name));
|
|
261
|
+
} else {
|
|
262
|
+
// No arrays loaded yet — show the welcome/plasma screen
|
|
263
|
+
addTab('__welcome__', '', '/');
|
|
264
|
+
}
|
|
265
|
+
}).catch(() => { addTab('__welcome__', '', '/'); });
|
|
266
266
|
}
|
|
267
267
|
</script>
|
|
268
268
|
</body>
|