diff --git a/.firebaserc b/.firebaserc index 304a4ac8a..4c28ddc88 100644 --- a/.firebaserc +++ b/.firebaserc @@ -7,9 +7,12 @@ "hosting": { "app": [ "neuroglancer" + ], + "docs": [ + "neuroglancer-docs" ] } } }, "etags": {} -} \ No newline at end of file +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f03241824..23b4528ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -101,7 +101,7 @@ jobs: # - name: Setup tmate session # uses: mxschmitt/action-tmate@v3 - name: Install Python packaging/test tools - run: python -m pip install --upgrade pip tox nox wheel numpy -r python/requirements-test.txt + run: pip install --upgrade pip tox nox wheel numpy -r python/requirements-test.txt - uses: ./.github/actions/setup-firefox - run: nox -s lint format mypy - name: Check for dirty working directory @@ -178,6 +178,29 @@ jobs: dist/*.whl dist/*.tar.gz + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Setup Graphviz + uses: ts-graphviz/setup-graphviz@b1de5da23ed0a6d14e0aeee8ed52fdd87af2363c # v2.0.2 + with: + macos-skip-brew-update: "true" + - name: Install nox + run: pip install nox + - name: Build docs + run: nox -s docs + - name: Upload docs as artifact + uses: actions/upload-artifact@v4 + with: + name: docs + path: | + dist/docs + publish-package: # Only publish package on push to tag or default branch. if: ${{ github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master') }} @@ -185,6 +208,7 @@ jobs: needs: - "client" - "python-build-package" + - "docs" steps: - uses: actions/checkout@v4 - name: Use Node.js @@ -224,12 +248,26 @@ jobs: with: name: client path: dist/client - - name: Publish to Firebase hosting + - name: Publish client to Firebase hosting + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + firebaseServiceAccount: "${{ secrets.FIREBASE_HOSTING_SERVICE_ACCOUNT_KEY }}" + projectId: neuroglancer-demo + channelId: live + target: app + # Download dist/docs after publishing to PyPI, because PyPI publish + # action fails if dist/docs directory is present. + - uses: actions/download-artifact@v4 + with: + name: docs + path: dist/docs + - name: Publish docs to Firebase hosting uses: FirebaseExtended/action-hosting-deploy@v0 with: firebaseServiceAccount: "${{ secrets.FIREBASE_HOSTING_SERVICE_ACCOUNT_KEY }}" projectId: neuroglancer-demo channelId: live + target: docs ngauth: strategy: diff --git a/.github/workflows/build_docs_preview.yml b/.github/workflows/build_docs_preview.yml new file mode 100644 index 000000000..77523f7f2 --- /dev/null +++ b/.github/workflows/build_docs_preview.yml @@ -0,0 +1,29 @@ +name: Build docs preview + +on: + pull_request: + +jobs: + upload: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Setup Graphviz + uses: ts-graphviz/setup-graphviz@b1de5da23ed0a6d14e0aeee8ed52fdd87af2363c # v2.0.2 + with: + macos-skip-brew-update: "true" + - name: Install nox + run: pip install nox + - name: Build documentation + run: nox -s docs + - name: Upload client as artifact + uses: actions/upload-artifact@v4 + with: + name: docs + path: | + dist/docs/ diff --git a/.github/workflows/deploy_docs_preview.yml b/.github/workflows/deploy_docs_preview.yml new file mode 100644 index 000000000..68207e200 --- /dev/null +++ b/.github/workflows/deploy_docs_preview.yml @@ -0,0 +1,67 @@ +name: Deploy docs preview + +on: + workflow_run: + workflows: ["Build docs preview"] + types: [completed] + +jobs: + deploy: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: "Create commit status" + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const commitId = "${{ github.event.workflow_run.head_commit.id }}"; + await github.rest.repos.createCommitStatus({ + context: "docs-preview", + owner: context.repo.owner, + repo: context.repo.repo, + sha: commitId, + state: "pending", + description: `Creating preview`, + target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + }); + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: docs + path: dist/docs + github-token: "${{ secrets.GITHUB_TOKEN }}" + run-id: "${{ github.event.workflow_run.id }}" + - name: Get PR ID + # https://github.com/orgs/community/discussions/25220#discussioncomment-7532132 + id: pr-id + run: | + PR_ID=$(gh run view -R ${{ github.repository }} ${{ github.event.workflow_run.id }} | grep -oP '#[0-9]+ . ${{ github.event.workflow_run.id }}' | grep -oP '#[0-9]+' | cut -c 2-) + echo "pr-id=${PR_ID}" >> $GITHUB_OUTPUT + env: + GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + - uses: FirebaseExtended/action-hosting-deploy@v0 + id: deploy + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_HOSTING_SERVICE_ACCOUNT_KEY }}" + expires: 30d + channelId: "pr${{ steps.pr-id.outputs.pr-id }}-docs" + projectId: neuroglancer-demo + target: docs + - name: "Update commit status" + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const expires = new Date("${{ steps.deploy.outputs.expire_time }}"); + const commitId = "${{ github.event.workflow_run.head_commit.id }}"; + await github.rest.repos.createCommitStatus({ + context: "docs-preview", + owner: context.repo.owner, + repo: context.repo.repo, + sha: commitId, + state: "success", + target_url: "${{ steps.deploy.outputs.details_url }}", + description: `Preview created, expires at: ${expires.toISOString()}`, + }); diff --git a/.github/workflows/deploy_preview.yml b/.github/workflows/deploy_preview.yml index 545bda17c..bdde334d0 100644 --- a/.github/workflows/deploy_preview.yml +++ b/.github/workflows/deploy_preview.yml @@ -48,6 +48,7 @@ jobs: expires: 30d channelId: "pr${{ steps.pr-id.outputs.pr-id }}" projectId: neuroglancer-demo + target: docs - name: "Update commit status" uses: actions/github-script@v7 with: diff --git a/docs/concepts/coordinate_spaces.dot b/docs/concepts/coordinate_spaces.dot new file mode 100644 index 000000000..f8a45fceb --- /dev/null +++ b/docs/concepts/coordinate_spaces.dot @@ -0,0 +1,218 @@ +digraph coordinate_spaces { + newrank = true; + outputorder = "edgesfirst"; + compound = true; + + node [ + style="rounded" + shape=rect + ] + + edge [ + ] + + graph [ + rankdir = LR + fillcolor = "var(--md-default-fg-color--lightest)" + style="solid,filled" + ] + { + rank=same; + projection_view_space cross_section_view_space global_space + } + + subgraph cluster_layer { + label = i> + + subgraph cluster_datasource { + label = j> + + { rank=same; + + datasource_space [ + label="Coordinate space" + href="#data-source-coordinate-space" + ] + + datasource_transform [ + label = "Coordinate transform" + href="#data-source-coordinate-transform" + ] + + } + style="filled,dashed" + } + + { + rank = same; + + subgraph cluster_layer_space { + label = "Coordinate space" + + layer_global_dims [ + label="Global dimensions" + ] + layer_local_dims [ + label="Local dimensions" + ] + layer_channel_dims [ + label="Channel dimensions" + ] + } + + layer_position [ + label = "Local position" + ] + } + + datasource_space -> datasource_transform + datasource_transform -> layer_local_dims [ + lhead="cluster_layer_space" + ] + style="filled,dashed" + } + + global_space [ + label="Global\ncoordinate\nspace" + ] + + layer_global_dims -> global_space [ + label="Merge" + ] + + subgraph cluster_globalcamera { + label = "Global camera parameters" + + subgraph cluster_globalcamera_cross_section { + + label = "Cross-section" + + global_cross_section_orientation [ + label = "Orientation" + ] + + global_cross_section_scale [ + label = "Scale" + ] + global_cross_section_depth [ + label = "Depth bounds" + ] + } + + global_center_position [ + label = "Center position" + ] + + global_display_dimensions [ + label = "Display dimensions" + ] + + global_relative_display_scales [ + label = "Relative\ndisplay scales" + ] + + subgraph cluster_globalcamera_projection { + + label = "3-d projection" + + global_projection_orientation [ + label = "Orientation" + ] + + global_projection_scale [ + label = "Scale" + ] + + global_projection_depth [ + label = "Depth bounds" + ] + } + } + + subgraph cluster_layergroupviewer { + label = k> + + layergroupviewer_center_position [ + label = "Center position" + ] + + layergroupviewer_display_dimensions [ + label = "Display dimensions" + ] + + layergroupviewer_relative_display_scales [ + label = "Relative\ndisplay scales" + ] + + subgraph cluster_layergroupviewer_cross_section { + label = "Cross-section views" + + layergroupviewer_cross_section_orientation [ + label = "Orientation" + ] + + layergroupviewer_cross_section_scale [ + label = "Scale" + ] + layergroupviewer_cross_section_depth [ + label = "Depth bounds" + ] + cross_section_view_space [ + label = "View\ncoordinate\nspace" + ] + } + + subgraph cluster_layergroupviewer_projection { + label = "3-d projection view" + + layergroupviewer_projection_orientation [ + label = "Orientation" + ] + + layergroupviewer_projection_scale [ + label = "Scale" + ] + + layergroupviewer_projection_depth [ + label = "Depth bounds" + ] + + projection_view_space [ + label = "View\ncoordinate\nspace" + ] + + } + + { layergroupviewer_display_dimensions, + layergroupviewer_relative_display_scales, + layergroupviewer_center_position, + layergroupviewer_cross_section_depth, + layergroupviewer_cross_section_scale, + layergroupviewer_cross_section_orientation } -> cross_section_view_space + + { layergroupviewer_display_dimensions, + layergroupviewer_relative_display_scales, + layergroupviewer_center_position, + layergroupviewer_projection_depth, + layergroupviewer_projection_scale, + layergroupviewer_projection_orientation } -> projection_view_space + style="filled,dashed" + } + + { + edge [ + style=dotted + ] + global_center_position -> layergroupviewer_center_position + global_cross_section_orientation -> layergroupviewer_cross_section_orientation + global_cross_section_scale -> layergroupviewer_cross_section_scale + global_cross_section_depth -> layergroupviewer_cross_section_depth + global_projection_orientation -> layergroupviewer_projection_orientation + global_projection_scale -> layergroupviewer_projection_scale + global_projection_depth -> layergroupviewer_projection_depth + global_display_dimensions -> layergroupviewer_display_dimensions + global_relative_display_scales -> layergroupviewer_relative_display_scales + } + + global_space -> {cross_section_view_space,projection_view_space} +} diff --git a/docs/concepts/coordinate_spaces.rst b/docs/concepts/coordinate_spaces.rst new file mode 100644 index 000000000..7b81b3138 --- /dev/null +++ b/docs/concepts/coordinate_spaces.rst @@ -0,0 +1,272 @@ +.. _coordinate-spaces: + +Coordinate spaces +================= + +A *coordinate space* associates a semantic meaning to *coordinate vectors* +within that space, vectors of real-valued coordinates specifying a coordinate +for each dimension of the coordinate space. At any given time, a coordinate +space has a fixed number of dimensions, called the *rank* of the coordinate +space. + +Commonly, Neuroglancer is used with 3-dimensional data, and hence coordinate +spaces of rank 3, but Neuroglancer supports degenerate coordinate spaces of rank +0, and there is no explicit upper limit on the number of dimensions. + +In addition to rank, coordinate spaces have several other properties: + +- The coordinate space specifies a unique name for each dimension, such as + ``x``, ``y``, or ``z``. Dimension names must match the regular expression + :regexp:`[a-zA-Z][a-zA-Z_0-9]*['^]?`: they must consist of an ASCII letter, + and followed by zero or more ASCII letters, digits, or underscore characters. + The optional suffix of ``'`` or ``^`` indicates a :ref:`local or channel + dimension`, respectively. +- For each dimension, the coordinate space specify either: + + - a *physical unit*, which may include both a base unit and a coefficient, + such as ``4e-9 m``, or may omit a base unit to indicate a unitless + dimension, such as ``1`` or ``1e3``; or + - a *coordinate array* specifying a string label associated with some of the + coordinates (indicating a discrete dimension). +- Coordinate spaces may optionally have a list of associated bounding boxes, + from which lower and upper bounds for each coordinate may be inferred. + +Neuroglancer makes use of a number of interrelated coordinate spaces and +associated positions, orientations, and other coordinate transformation +parameters: + +.. graphviz:: coordinate_spaces.dot + :caption: Coordinate spaces and transforms in Neuroglancer. The labels above + link to the corresponding description below. + +The series of coordinate transformations, starting from the coordinate spaces of +each :ref:`data source`, into a common +:ref:`global coordinate space`, and then into the +coordinate space for each rendered view, is described below. + +.. _data-source-coordinate-space: + +Data source coordinate space +---------------------------- + +The starting point for all coordinate transformations in Neuroglancer is the +data source itself. Each :ref:`data source` added to a +:ref:`layer` has an inherent associated coordinate space. + +.. + Screenshots not yet supported + .. neuroglancer-screenshot:: concepts/data_source_coordinate_space + +The dimension names, physical units or coordinate arrays, and bounds are +determined from the source data automatically; if dimension names or units +cannot be determined, default values are chosen by the data source +implementation. The dimension names and bounds within the source coordinate +space are fixed, but the :ref:`coordinate +transform` controls how the source data +coordinate space maps to the :ref:`layer and global coordinate +spaces`. + +.. _data-source-coordinate-transform: + +Coordinate transform +^^^^^^^^^^^^^^^^^^^^ + +A configurable :wikipedia:`affine` *coordinate +transform*, represented by an :wikipedia:`affine transformation +matrix` and a list of output +dimension names, maps the source coordinate space to the :ref:`layer coordinate +space` and to the :ref:`global coordinate +space`. + +The data source provides a default value for the coordinate transform, +normally an identity matrix. + +The user can configure the coordinate transform in three ways: + +1. The affine transformation matrix scaling and translation coefficients may be + changed directly. Note that the translation coefficients are in the units + specified for the output (layer) dimension. + +2. The names of the output dimensions of the transform may be changed. + Permuting the output dimension names has a similar effect to permuting the + rows of the transformation matrix in the same way, but may be more + convenient. + +3. The source dimension scales/units may be changed, in order to rescale the + input. This is equivalent to applying an appropriate scale transformation to + the affine transformation matrix, but in many cases is more convenient. + + .. note:: + + Changing the units of an output dimension does *not* rescale the data, it + simply changes the unit used to display coordinates. + +The output space of the coordinate transform is a subspace of the :ref:`layer +coordinate space`. If two data sources associated with +a layer both have a coordinate transform with an output dimension named ``x``, +both coordinate transforms are referring to the same dimension ``x``. In +contrast, the names of the *source dimensions* of the coordinate transform are +purely descriptive; if two data sources associated with a layer both have a +source dimension ``x``, there is no direct correspondence between those two +source dimensions. + +.. _layer-coordinate-space: + +Layer coordinate space +---------------------- + +The output coordinate spaces of the :ref:`coordinate +transforms` of each data source in a given +layer are *merged* into a single coordinate space that is called the *layer +coordinate space*: + +- If a layer has just a single data source (most common case), then the layer + coordinate space is simply the output coordinate space of the coordinate + transform. + +- In general, if a layer has more than one data source, the layer coordinate + space consists of the distinct output dimensions of the coordinate transforms + of each of the data sources. + +.. _dimension-kinds: + +Global, local and channel dimensions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A layer coordinate space dimension may be one of three *kinds*, determined based +on the dimension name: + +.. _global-dimensions: + +Global dimensions +~~~~~~~~~~~~~~~~~ + +Global dimensions have names consisting of only ASCII +alphanumeric and underscore characters without a special suffix, e.g. ``x``. + +- The global dimensions of each layer are merged into the :ref:`global + coordinate space`, which specifies the units for + global dimensions. + +- A global dimension ``x`` in one layer refers to the same dimension as a + global dimension ``x`` in another layer. + +- Only global dimensions may be used as :ref:`display + dimensions`. + +.. _local-dimensions: + +Local dimensions +~~~~~~~~~~~~~~~~ + +Local dimensions have names ending in ``'`` (ASCII single quote), e.g. ``c'``. + +- The :ref:`local coordinate space` of each layer + consists of the subset of the layer dimensions that are local dimensions, + and specifies the units for local dimensions. + +- A local dimension ``c'`` in one layer is completely independent from a local + dimension with the same name ``c'`` in another layer. + +- Local dimensions may not be used as :ref:`display + dimensions`. Instead, :ref:`data views` always + display a cross section at a single position along each local dimension; this + position is determined by the :ref:`local position`. + +- A global dimension with a unique name that is not a :ref:`display + dimension` may be used as an alternative to a local + dimension; a local dimension simply avoids the need to assign unique names, + and may be more convenient in some cases. + +.. _channel-dimensions: + +Channel dimensions +~~~~~~~~~~~~~~~~~~ + +:ref:`Image layers` additionally support channel dimensions, +which have names ending ``^`` (ASCII caret), e.g. ``c^``. + +- The :ref:`shader` can access the value at every position + within the channel dimensions when computing the output pixel color. For + example, if there is a single channel dimension with a range of ``[0, 3)``, + the shader can compute the output pixel color based on the data value at + each of the 3 positions. + +- Like :ref:`local dimensions`, a channel dimension ``c^`` + in one layer is completely independent from a channel dimension with the + same name ``c^`` in another layer. + +- A dimension can be used as a channel dimensions only if the data source is + unchunked along that dimension. + +.. _local-coordinate-space: + +Local coordinate space +^^^^^^^^^^^^^^^^^^^^^^ + +The local coordinate space of a layer consists of the local dimensions of the +layer coordinate space. + +By default, dimensions are ordered based on when they are first added, with +dimensions added later ordered after dimensions added earlier, but dimensions +may be explicitly reordered. + +.. _local-position: + +Local position +~~~~~~~~~~~~~~ + +Each layer has an associated *local position*, specifying for each dimension in +the :ref:`local coordinate space` the single slice for +each dimension in the local coordinate space to be displayed in any views of the +layer. + +.. _global-coordinate-space: + +Global coordinate space +----------------------- + +The global coordinate space consists of the global dimensions from the +coordinate spaces of each layer added to the viewer. + +By default, dimensions are ordered based on when they are first added to a layer +coordinate space, with dimensions added later ordered after dimensions added +earlier, but dimensions may be explicitly reordered. + + +.. _global center position: + +Global center position +^^^^^^^^^^^^^^^^^^^^^^ + +The viewer has a single default global position, called the global center +position, which specifies a center coordinate for each dimension in the +:ref:`global coordinate space`. + +.. _global-mouse-position: + +Global mouse position +^^^^^^^^^^^^^^^^^^^^^ + +The position within the :ref:`global-coordinate-space` corresponding to the +current mouse position within a :ref:`data view` is called the +*global mouse position*. + +.. _layer group center position: + +Layer group center position +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Additionally, each layer group sub-viewer has a separate center position in the +:ref:`global coordinate space` which may optionally be +fixed to, or linked by an offset to, the global center position. + +.. _display-dimensions: + +Display dimensions +------------------ + +:ref:`Data views` project data from at most three +:ref:`global dimensions`; these projected dimensions +are called *display dimensions*. From all other global dimensions, only a +cross section of the data is displayed at any given time. diff --git a/docs/concepts/data_source_coordinate_space.screenshot.py b/docs/concepts/data_source_coordinate_space.screenshot.py new file mode 100644 index 000000000..1e7b04c2b --- /dev/null +++ b/docs/concepts/data_source_coordinate_space.screenshot.py @@ -0,0 +1,59 @@ +from selenium.webdriver.common.by import By + +rec.begin( + { + "layers": [ + { + "type": "image", + "source": "precomputed://gs://neuroglancer-public-data/flyem_fib-25/image", + "tab": "source", + "name": "image", + } + ], + "selectedLayer": {"layer": "image", "visible": True}, + } +) + +# rec.highlight_element_with_outline( +# elements=( +# rec.webdriver.driver.find_elements( +# By.CSS_SELECTOR, '.neuroglancer-coordinate-space-transform-source-label') + +# rec.webdriver.driver.find_elements( +# By.CSS_SELECTOR, '.neuroglancer-coordinate-space-transform-input-scale-container')), +# caption='Data source coordinate space', +# direction='left', +# ) + +# rec.highlight_element_with_outline( +# elements=(rec.webdriver.driver.find_elements(By.CSS_SELECTOR, +# '.neuroglancer-coordinate-space-transform-coeff') + +# rec.webdriver.driver.find_elements( +# By.CSS_SELECTOR, '.neuroglancer-coordinate-space-transform-output-label')), +# caption='Coordinate transform', +# direction='left') + + +rec.add_label( + elements=( + rec.webdriver.driver.find_elements( + By.CSS_SELECTOR, ".neuroglancer-coordinate-space-transform-source-label" + ) + + rec.webdriver.driver.find_elements( + By.CSS_SELECTOR, + ".neuroglancer-coordinate-space-transform-input-scale-container", + ) + ), + caption="Data source coordinate space", +) + +rec.add_label( + elements=( + rec.webdriver.driver.find_elements( + By.CSS_SELECTOR, ".neuroglancer-coordinate-space-transform-coeff" + ) + + rec.webdriver.driver.find_elements( + By.CSS_SELECTOR, ".neuroglancer-coordinate-space-transform-output-label" + ) + ), + caption="Coordinate transform", +) diff --git a/docs/concepts/data_source_coordinate_transform.screenshot.py b/docs/concepts/data_source_coordinate_transform.screenshot.py new file mode 100644 index 000000000..7b0301ae0 --- /dev/null +++ b/docs/concepts/data_source_coordinate_transform.screenshot.py @@ -0,0 +1,59 @@ +from selenium.webdriver.common.by import By + +rec.begin( + { + "layers": [ + { + "type": "image", + "source": "precomputed://gs://neuroglancer-public-data/flyem_fib-25/image", + "tab": "source", + "name": "image", + } + ], + "selectedLayer": {"layer": "image", "visible": True}, + } +) + + +rec.add_labels( + [ + ( + rec.webdriver.driver.find_elements( + By.CSS_SELECTOR, + ".neuroglancer-coordinate-space-transform-input-scale-container", + ), + "top", + "Source dimension units", + ), + ( + rec.webdriver.driver.find_elements( + By.CSS_SELECTOR, ".neuroglancer-coordinate-space-transform-coeff" + ), + "bottom", + "Affine transform matrix", + ), + ( + rec.webdriver.driver.find_elements( + By.CSS_SELECTOR, ".neuroglancer-coordinate-space-transform-output-label" + ), + "left", + "Output dimension labels", + ), + ( + rec.webdriver.driver.find_elements( + By.CSS_SELECTOR, + ".neuroglancer-coordinate-space-transform-output-scale-container", + ), + "bottom", + "Output dimension units", + ), + ( + rec.webdriver.driver.find_elements( + By.CSS_SELECTOR, + ".neuroglancer-coordinate-space-transform-output-extend", + ), + "left", + "Extend output space", + ), + ] +) diff --git a/docs/concepts/data_views.rst b/docs/concepts/data_views.rst new file mode 100644 index 000000000..48b18cfbd --- /dev/null +++ b/docs/concepts/data_views.rst @@ -0,0 +1,18 @@ +.. _data-view: + +Data view +========= + +.. _cross-section-view: + +Cross-section view +------------------ + +TODO + +.. _projection-view: + +3-D projection view +------------------- + +TODO diff --git a/docs/concepts/layers.rst b/docs/concepts/layers.rst new file mode 100644 index 000000000..319cf7d9d --- /dev/null +++ b/docs/concepts/layers.rst @@ -0,0 +1,26 @@ +.. _layer: + +Layer +===== + +.. _layer-data-source: + +Layer data source +----------------- + +TODO + +Layer kinds +----------- + +.. _image-layer: + +Image layer +^^^^^^^^^^^ + +.. _image-layer-shader: + +Image layer shader +~~~~~~~~~~~~~~~~~~ + +TODO diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..6a6555598 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,324 @@ +project = "Neuroglancer" +copyright = "2021 The Neuroglancer Authors" +version = "" +release = "" + +import typing +from typing import NamedTuple, Optional +import os +import re +import sys + +import docutils.nodes +import sphinx.application +import sphinx.addnodes +import sphinx.domains.python +import sphinx.environment + + +os.environ["NEUROGLANCER_BUILDING_DOCS"] = "1" + +extensions = [ + "sphinx.ext.extlinks", + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.graphviz", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx_immaterial", + "sphinx_immaterial.apidoc.python.apigen", + "sphinx_immaterial.apidoc.json.domain", + "sphinx_immaterial.graphviz", + "sphinx_immaterial.apidoc.format_signatures", +] + +napoleon_numpy_docstring = False +napoleon_use_admonition_for_examples = True +napoleon_use_admonition_for_notes = True + + +html_title = "Neuroglancer" + +# Don't include "View page source" links, since they aren't very helpful, +# especially for generated pages. +html_show_sourcelink = True +html_copy_source = False + +# Skip unnecessary footer text. +html_show_sphinx = True +html_show_copyright = True + +# Override default of `utf-8-sig` which can cause problems with autosummary due +# to the extra Unicode Byte Order Mark that gets inserted. +source_encoding = "utf-8" + +source_suffix = ".rst" +master_doc = "index" +language = "en" + +html_use_index = False + +intersphinx_mapping = { + "python": ( + "https://docs.python.org/3", + ("intersphinx_inv/python3.inv", None), + ), + "numpy": ( + "https://numpy.org/doc/stable/", + ("intersphinx_inv/numpy.inv", None), + ), + "tensorstore": ( + "https://google.github.io/tensorstore/", + ("intersphinx_inv/tensorstore.inv", None), + ), + # "sphinx_docs": ("https://www.sphinx-doc.org/en/master", None), +} + +html_theme = "sphinx_immaterial" + +html_theme_options = { + "icon": { + "logo": "material/library", + "repo": "fontawesome/brands/github", + }, + "site_url": "https://google.github.io/neuroglancer/", + "repo_url": "https://github.com/google/neuroglancer/", + "edit_uri": "blob/master/docs", + "features": [ + "navigation.expand", + "navigation.tabs", + # "toc.integrate", + "navigation.sections", + # "navigation.instant", + # "header.autohide", + "navigation.top", + "toc.sticky", + "toc.follow", + ], + "toc_title_is_page_title": True, + "palette": [ + { + "media": "(prefers-color-scheme: dark)", + "scheme": "slate", + "primary": "green", + "accent": "light blue", + "toggle": { + "icon": "material/lightbulb", + "name": "Switch to light mode", + }, + }, + { + "media": "(prefers-color-scheme: light)", + "scheme": "default", + "primary": "green", + "accent": "light blue", + "toggle": { + "icon": "material/lightbulb-outline", + "name": "Switch to dark mode", + }, + }, + ], +} + +default_role = "any" + +# Warn about missing references +nitpicky = True + +python_apigen_modules = { + "neuroglancer.viewer_state": "python/api/", + "neuroglancer.viewer_config_state": "python/api/", + "neuroglancer.trackable_state": "python/api/", + "neuroglancer.json_wrappers": "python/api/", + "neuroglancer": "python/api/", +} + +python_apigen_default_groups = [ + (r"class:neuroglancer\.viewer_state\..*", "viewer-state"), + (r"(class|function):neuroglancer\.viewer_config_state\..*", "viewer-config-state"), + (r"class:neuroglancer\.coordinate_space\..*", "coordinate-space"), + (r"class:neuroglancer\.viewer_state\..*Tool", "viewer-state-tools"), + (r"class:neuroglancer\.viewer_state\..*Layer", "viewer-state-layers"), + (r"class:neuroglancer\.viewer\..*", "core"), + (r"class:neuroglancer\.server\..*", "server"), +] + +python_apigen_rst_prolog = """ +.. default-role:: py:obj + +.. default-literal-role:: python + +.. highlight:: python + +""" + + +json_schemas = ["json_schema/*.yml"] + +rst_prolog = """ +.. role:: python(code) + :language: python + :class: highlight + +.. role:: json(code) + :language: json + :class: highlight + +""" + +json_schema_rst_prolog = """ +.. default-role:: json:schema + +.. default-literal-role:: json + +.. highlight:: json +""" + + +graphviz_output_format = "svg" + +extlinks = { + "wikipedia": ("https://en.wikipedia.org/wiki/%s", None), +} + + +python_module_names_to_strip_from_xrefs = [ + "neuroglancer.viewer_state", + "neuroglancer.trackable_state", + "neuroglancer.viewer_config_state", + "neuroglancer", + "collections.abc", + "numbers", + "numpy.typing", + "numpy", +] + +object_description_options = [ + ("py:.*", dict(black_format_style={})), +] + + +# Monkey patch numpy.typing.NDArray +def _monkey_patch_numpy_typing_ndarray(): + import numpy.typing + + T = typing.TypeVar("T") + + class NDArray(typing.Generic[T]): + pass + + NDArray.__module__ = "numpy.typing" + + numpy.typing.NDArray = NDArray + + +_monkey_patch_numpy_typing_ndarray() + + +# Monkey patch Sphinx to generate custom cross references for specific type +# annotations. +# +# The Sphinx Python domain generates a `py:class` cross reference for type +# annotations. However, in some cases in the TensorStore documentation, type +# annotations are used to refer to targets that are not actual Python classes, +# such as `DownsampleMethod`, `DimSelectionLike`, or `NumpyIndexingSpec`. +# Additionally, some types like `numpy.typing.ArrayLike` are `py:data` objects +# and can't be referenced as `py:class`. +class TypeXrefTarget(NamedTuple): + domain: str + reftype: str + target: str + title: str + + +python_type_to_xref_mappings = { + f"numpy.{name}": TypeXrefTarget("py", "obj", f"numpy.{name}", name) + for name in [ + "int64", + "uint64", + "float32", + "float64", + ] +} + +python_type_to_xref_mappings["numpy.typing.NDArray"] = TypeXrefTarget( + "py", "obj", "numpy.typing.NDArray", "NDArray" +) + +python_strip_property_prefix = True + + +_orig_python_type_to_xref = sphinx.domains.python.type_to_xref + + +def _python_type_to_xref( + target: str, + env: sphinx.environment.BuildEnvironment, + *, + suppress_prefix: bool = False, +) -> sphinx.addnodes.pending_xref: + xref_info = python_type_to_xref_mappings.get(target) + if xref_info is not None: + return sphinx.addnodes.pending_xref( + "", + docutils.nodes.Text(xref_info.title), + refdomain=xref_info.domain, + reftype=xref_info.reftype, + reftarget=xref_info.target, + refspecific=False, + refexplicit=True, + refwarn=True, + ) + return _orig_python_type_to_xref(target, env, suppress_prefix=suppress_prefix) + + +sphinx.domains.python.type_to_xref = _python_type_to_xref +sphinx.domains.python._annotations.type_to_xref = _python_type_to_xref + + +python_type_aliases = { + "concurrent.futures._base.Future": "concurrent.futures.Future", +} + + +def _should_document_python_class_base(base: type) -> bool: + if base.__name__.startswith("_"): + return False + if base.__module__ in ("neuroglancer.json_wrappers", "neuroglancer.viewer_base"): + return False + return True + + +PYTHON_MEMBER_SKIP_PATTERNS = re.compile(r"supports_(readonly|validation)") + + +def _autodoc_skip_member( + app: sphinx.application.Sphinx, + what: str, + name: str, + obj, + skip: bool, + options, +): + if PYTHON_MEMBER_SKIP_PATTERNS.fullmatch(name) is not None: + return True + return None + + +def _python_apigen_skip_base( + app: sphinx.application.Sphinx, subclass: type, base: type +): + if base.__module__ == "collections.abc": + return True + return None + + +def setup(app: sphinx.application.Sphinx): + # Ignore certain base classes. + def _autodoc_process_bases(app, name, obj, options, bases): + bases[:] = [base for base in bases if _should_document_python_class_base(base)] + + app.connect("autodoc-process-bases", _autodoc_process_bases) + + app.connect("autodoc-skip-member", _autodoc_skip_member) + app.connect("python-apigen-skip-base", _python_apigen_skip_base) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..bdd78a65f --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,34 @@ + +Neuroglancer +============ + +.. toctree:: + :hidden: + :caption: User Guide + + user-guide/navigation + +.. toctree:: + :hidden: + :caption: Concepts + + concepts/coordinate_spaces + concepts/data_views + concepts/layers + +.. toctree:: + :hidden: + :caption: JSON API + + json/api/index + +.. toctree:: + :hidden: + :caption: Python API + + python/api/index + +Neuroglancer is a WebGL-based viewer for volumetric data. It is capable of +displaying arbitrary (non axis-aligned) cross-sectional views of volumetric +data, as well as 3-D meshes and line-segment based models (skeletons). + diff --git a/docs/intersphinx_inv/numpy.inv b/docs/intersphinx_inv/numpy.inv new file mode 100644 index 000000000..9a95a49ee Binary files /dev/null and b/docs/intersphinx_inv/numpy.inv differ diff --git a/docs/intersphinx_inv/python3.inv b/docs/intersphinx_inv/python3.inv new file mode 100644 index 000000000..adf47f5d3 Binary files /dev/null and b/docs/intersphinx_inv/python3.inv differ diff --git a/docs/intersphinx_inv/tensorstore.inv b/docs/intersphinx_inv/tensorstore.inv new file mode 100644 index 000000000..33fcd83b0 Binary files /dev/null and b/docs/intersphinx_inv/tensorstore.inv differ diff --git a/docs/json/api/index.rst b/docs/json/api/index.rst new file mode 100644 index 000000000..d5c3c7b1b --- /dev/null +++ b/docs/json/api/index.rst @@ -0,0 +1,33 @@ +API Reference +============= + +.. json:schema:: ViewerState + +.. json:schema:: Layer + +.. json:schema:: Position + +.. json:schema:: Orientation + +.. json:schema:: DepthRange + +.. json:schema:: DisplayDimensions + +.. json:schema:: DisplayScale + +.. json:schema:: RelativeDisplayScales + +.. json:schema:: CoordinateSpace + +Layout +------ + +.. json:schema:: DataPanelLayoutType + +.. json:schema:: HierarchicalLayout + +.. json:schema:: StackLayout + +.. json:schema:: LayerGroupViewer + +.. json:schema:: DataPanelLayout diff --git a/docs/json_schema/layer.yml b/docs/json_schema/layer.yml new file mode 100644 index 000000000..aaac47cbd --- /dev/null +++ b/docs/json_schema/layer.yml @@ -0,0 +1,14 @@ +$schema: http://json-schema.org/draft-07/schema# +$id: Layer +title: "Layer within a Neuroglancer instance." +type: object +properties: + type: + type: string + title: "Specifies the layer type." + name: + type: string + title: "Specifies the layer name to show in the UI." + visible: + type: boolean + title: "Indicates whether the layer is displayed in 2d and 3d projections." diff --git a/docs/json_schema/viewer_state.yml b/docs/json_schema/viewer_state.yml new file mode 100644 index 000000000..acaba2fea --- /dev/null +++ b/docs/json_schema/viewer_state.yml @@ -0,0 +1,252 @@ +$schema: http://json-schema.org/draft-07/schema# +$id: ViewerState +title: "Complete state of a Neuroglancer instance." +type: object +properties: + dimensions: + $ref: CoordinateSpace + title: "Global coordinate space." + relativeDisplayScales: + $ref: RelativeDisplayScales + title: "Default additional relative display scale factors for each `global dimension<.dimensions>`." + displayDimensions: + $ref: DisplayDimensions + title: "Default display dimensions for 2d and 3d projections." + position: + $ref: Position + title: "Global position within each `global dimension<.dimensions>`." + crossSectionOrientation: + $ref: Orientation + title: "Default orientation within the `.displayDimensions`." + crossSectionScale: + $ref: DisplayScale + title: "Default display scale (zoom level) for cross-section views." + crossSectionDepth: + $ref: DepthRange + title: "Default depth-of-field for cross-section views." + projectionOrientation: + $ref: Orientation + title: "Default orientation within the `.displayDimensions`." + projectionScale: + $ref: DisplayScale + title: "Default display scale (zoom level) for projection views." + projectionDepth: + $ref: DepthRange + title: "Default depth-of-field for projection views." + layers: + type: array + items: + $ref: Layer + showAxisLines: + type: boolean + title: "Indicates whether to show the red/green/blue axis lines." + default: true + wireFrame: + type: boolean + title: "Indicates whether to enable wireframe rendering mode (for debugging)." + default: false + showScaleBar: + type: boolean + title: "Indicates whether to show scale bars." + default: true + showDefaultAnnotations: + type: boolean + title: "Indicates whether to show bounding boxes of data sources." + default: true + showSlices: + type: boolean + title: "Indicates whether to show cross sections in the 3-d view of `~DataPanelLayoutType.4panel` layouts." + default: true + gpuMemoryLimit: + type: integer + title: "GPU memory limit, in bytes." + systemMemoryLimit: + type: integer + title: "System memory limit, in bytes." + concurrentDownloads: + type: integer + title: "Maximum number of concurrent downloads." + prefetch: + type: boolean + title: "Indicates whether to use adaptive prefetching." + default: true + title: + type: string + title: "Additional text to include in the page title." + layout: + oneOf: + - $ref: DataPanelLayoutType + - $ref: DataPanelLayout + - $ref: HierarchicalLayout + title: "Data panel and layer group layout." +definitions: + CoordinateSpace: + $id: CoordinateSpace + title: "Specifies a coordinate space." + type: object + properties: + "": + type: array + items: + - type: number + - type: string + title: Specifies a dimension name and the corresponding scale/unit. + + DisplayDimensions: + $id: DisplayDimensions + type: array + items: + type: string + minItems: 0 + maxItems: 3 + uniqueItems: true + title: "Specifies the display dimensions for 2d and 3d projections." + description: | + Orientation: + $id: Orientation + title: "Specifies a 3-d orientation as a unit quaternion." + description: | + For the 3-d projection view and for the `DataPanelLayoutType.xy` cross-section + view, with the default orientation of ``[0, 0, 0, 1]`` the first display + dimension (red axis) points right, the second display dimension (green + axis) points down, and the third display dimension (blue axis) points away + from the camera. + type: array + items: + - type: number + - type: number + - type: number + - type: number + default: [0, 0, 0, 1] + DisplayScale: + $id: DisplayScale + title: "Specifies the scale (zoom level) of a cross-section or 3d projection view." + description: | + For cross-section views, the scale is specified in canonical voxels per + screen pixel, and defaults to ``1``. For 3d projection views, the scale is specified in + canonical voxels per viewport height. + type: number + exclusiveMinimum: 0 + RelativeDisplayScales: + $id: RelativeDisplayScales + title: "Specifies additional relative display scale factors for each `global dimension`." + description: | + The length must be equal to the number of `global + dimensions`. Defaults to a vector of all ones. + type: array + items: + type: number + Position: + $id: Position + title: "Specifies the position within a `CoordinateSpace`." + description: | + The length must be equal to the number of dimensions in the coordinate + space. + type: array + items: + type: number + DepthRange: + $id: DepthRange + title: "Specifies the depth-of-field for cross section or 3d projection views." + type: number + exclusiveMinimum: 0 + HierarchicalLayout: + $id: HierarchicalLayout + title: "Specifies a hierarchical grid of layer groups and data views." + type: object + properties: + type: + type: string + title: Indicates the layout type. + flex: + type: number + title: Indicates the relative size of this layout within its parent stack. + required: + - "type" + DataPanelLayoutType: + $id: DataPanelLayoutType + title: "Specifies a layout of 3-d projection and 2-d cross-section views for a layer group." + oneOf: + - const: "4panel" + title: "2x2 grid layout with `.xy`, `.xz`, `.3d`, and `.yz` panels." + description: | + The top left is an `.xy` cross-section view, the top right is an `.xz` + cross-section view, the bottom left is a `.3d` projection view, and the + bottom right is a `.yz` cross-section view. + - const: "xy" + title: "Single cross-section view in the default orientation." + description: | + with the first display dimension (red) + pointing right, the second display dimension (green) pointing down, and + the third display dimension (blue) pointing away from the camera. + - const: "xz" + title: | + Single cross-section view with the first display dimension (red) + pointing right, the third display dimension (blue) pointing down, and + the second display dimension (green) pointing towards the camera. + - const: "yz" + title: | + Single cross-section view with the second display dimension (green) + pointing down, the third display dimension (blue) pointing left, and + the first display dimension (red) pointing away from the camera. + - const: "3d" + title: "Single 3-d projection view." + - const: "xy-3d" + - const: "xz-3d" + - const: "yz-3d" + DataPanelLayout: + $id: DataPanelLayout + title: "Describes the :ref:`data views` to display." + type: object + properties: + type: + $ref: DataPanelLayoutType + title: Indicates the layout type. + orthographicProjection: + type: boolean + title: | + Indicates whether the :ref:`projection views`, if + present, uses orthographic rather than perspective projection. + default: false + required: + - "type" + LayerGroupViewer: + $id: LayerGroupViewer + title: "Specifies a :json:schema:`DataPanelLayout` for a subset of the layers." + allOf: + - $ref: HierarchicalLayout + - type: object + properties: + type: + const: "viewer" + layers: + type: array + items: + type: string + title: "Names of layers included in this sub-viewer." + description: | + Each name must match the name of a layer specified in the + top-level `ViewerState.layers`. + layout: + oneOf: + - $ref: DataPanelLayoutType + - $ref: DataPanelLayout + title: Layout of the data panels for this viewer. + default: "xy" + + StackLayout: + $id: StackLayout + title: "Specifies a row or column of sub-layouts." + allOf: + - $ref: HierarchicalLayout + - type: object + properties: + type: + oneOf: + - const: "row" + - const: "column" + title: Indicates the stack direction. + children: + type: array + items: + $ref: HierarchicalLayout diff --git a/docs/pyproject.toml b/docs/pyproject.toml new file mode 100644 index 000000000..c9184b31a --- /dev/null +++ b/docs/pyproject.toml @@ -0,0 +1,2 @@ +[tool.ruff] +exclude = ["*"] diff --git a/docs/python/api/.gitignore b/docs/python/api/.gitignore new file mode 100644 index 000000000..e15a7a67e --- /dev/null +++ b/docs/python/api/.gitignore @@ -0,0 +1,2 @@ +-index.rst +*.rst diff --git a/docs/python/api/index.rst b/docs/python/api/index.rst new file mode 100644 index 000000000..074fbbec8 --- /dev/null +++ b/docs/python/api/index.rst @@ -0,0 +1,78 @@ +API Reference +============= + +Core +---- + +.. python-apigen-group:: core + +Viewer state +------------ + +.. python-apigen-group:: viewer-state + +Segment sets +^^^^^^^^^^^^ + +.. python-apigen-group:: viewer-state-segments + +URL representation +^^^^^^^^^^^^^^^^^^ + +.. python-apigen-group:: viewer-state-url + +Coordinate space +^^^^^^^^^^^^^^^^ + +.. python-apigen-group:: coordinate-space + +Layers +^^^^^^ + +.. python-apigen-group:: viewer-state-layers + +Tools +^^^^^ + +.. python-apigen-group:: viewer-state-tools + +Viewer Config state +------------------- + +.. python-apigen-group:: viewer-config-state + + +Exposing local data as data source +---------------------------------- + +.. python-apigen-group:: serve-data + +Credentials +----------- + +.. python-apigen-group:: credentials + +Capturing Screenshots +--------------------- + +.. python-apigen-group:: screenshots + +Server +------ + +.. python-apigen-group:: server + + +Utilities +--------- + +JSON Containers +^^^^^^^^^^^^^^^ + +.. python-apigen-group:: json-containers + + +Trackable state +^^^^^^^^^^^^^^^ + +.. python-apigen-group:: trackable-state diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..1cb6b52fc --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,7 @@ +sphinx-immaterial[black,json]>=0.12.0 +sphinx>=7.4.0 +pyyaml +jsonschema +numpy +tensorstore +-r ../python/requirements.txt diff --git a/docs/update_intersphinx_inventories.py b/docs/update_intersphinx_inventories.py new file mode 100644 index 000000000..8ab6fd7a6 --- /dev/null +++ b/docs/update_intersphinx_inventories.py @@ -0,0 +1,42 @@ +# Copyright 2020 The TensorStore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Downloads the intersphinx inventories referenced by `conf.py`. + +This allows the documentation build to be hermetic. +""" + +import os +import requests + +if __name__ == "__main__": + docs_dir = os.path.dirname(os.path.realpath(__file__)) + conf_path = os.path.join(docs_dir, "conf.py") + conf_module = { + "__file__": conf_path, + } + with open(conf_path, "r") as f: + exec(f.read(), conf_module) + intersphinx_mapping = conf_module["intersphinx_mapping"] + for _, (url, (local_path, _)) in intersphinx_mapping.items(): + if not url.endswith("/"): + url += "/" + full_url = url + "objects.inv" + full_path = os.path.join(docs_dir, local_path) + print("Fetching: %r" % (full_url,)) + resp = requests.get(full_url) + resp.raise_for_status() + os.makedirs(os.path.dirname(full_path), exist_ok=True) + with open(full_path, "wb") as f: + f.write(resp.content) + print("Fetched %s -> %s" % (full_url, full_path)) diff --git a/docs/user-guide/navigation.rst b/docs/user-guide/navigation.rst new file mode 100644 index 000000000..7ef701e8e --- /dev/null +++ b/docs/user-guide/navigation.rst @@ -0,0 +1,43 @@ +Navigation +========== + +The position, orientation, and zoom level of :ref:`data views` can +be controlled using :ref:`mouse` and +:ref:`keyboard` bindings as well as UI controls. + +.. _mouse-navigation: + +Mouse navigation +---------------- + +Translating cross-section views in-plane +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can translate a :ref:`cross-section view` in-plane by +left-clicking within the view and dragging the mouse while holding down the left +button. As you drag, the `global center position` changes but the :ref:`global mouse +position` remains fixed. + +.. + Videos not yet supported + .. neuroglancer-video:: user-guide/navigation_mouse_cross_section_translate + +Adjusting the zoom level of cross-section views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can adjust the zoom level of a :ref:`cross-section view` +by holding down :kbd:`Control` and moving the mouse wheel up or down. As you +move the mouse wheel, the `global center position` may change but the +:ref:`global mouse position` remains fixed. + +.. + Videos not yet supported + .. neuroglancer-video:: user-guide/navigation_mouse_cross_section_zoom + + +.. _keyboard-navigation: + +Keyboard navigation +------------------- + +TODO diff --git a/docs/user-guide/navigation_mouse_cross_section_translate.video.py b/docs/user-guide/navigation_mouse_cross_section_translate.video.py new file mode 100644 index 000000000..afd6c3508 --- /dev/null +++ b/docs/user-guide/navigation_mouse_cross_section_translate.video.py @@ -0,0 +1,28 @@ +rec.begin( + { + "dimensions": {"x": [8e-9, "m"], "y": [8e-9, "m"], "z": [8e-9, "m"]}, + "position": [3242.833251953125, 3334.499755859375, 4045.5], + "crossSectionScale": 1, + "projectionOrientation": [ + 0.23160003125667572, + 0.20444951951503754, + -0.017615893855690956, + 0.9509214162826538, + ], + "projectionScale": 512, + "layers": [ + { + "type": "image", + "source": "precomputed://gs://neuroglancer-public-data/flyem_fib-25/image", + "tab": "source", + "name": "image", + } + ], + "selectedLayer": {"layer": "image"}, + "layout": "4panel", + } +) +panel = rec.get_data_panels()[0] +rec.move_to_element_smoothly(panel, 0.1, 0.1) +with rec.mouse_buttons_held(1): + rec.move_to_element_smoothly(panel, 0.8, 0.9, speed=0.1) diff --git a/docs/user-guide/navigation_mouse_cross_section_zoom.video.py b/docs/user-guide/navigation_mouse_cross_section_zoom.video.py new file mode 100644 index 000000000..9d176578b --- /dev/null +++ b/docs/user-guide/navigation_mouse_cross_section_zoom.video.py @@ -0,0 +1,30 @@ +rec.begin( + { + "dimensions": {"x": [8e-9, "m"], "y": [8e-9, "m"], "z": [8e-9, "m"]}, + "position": [3242.833251953125, 3334.499755859375, 4045.5], + "crossSectionScale": 1, + "projectionOrientation": [ + 0.23160003125667572, + 0.20444951951503754, + -0.017615893855690956, + 0.9509214162826538, + ], + "projectionScale": 512, + "layers": [ + { + "type": "image", + "source": "precomputed://gs://neuroglancer-public-data/flyem_fib-25/image", + "tab": "source", + "name": "image", + } + ], + "selectedLayer": {"layer": "image"}, + "layout": "4panel", + } +) +panel = rec.get_data_panels()[0] +rec.move_to_element_smoothly(panel, 0.8, 0.7) +time.sleep(0.5) +with rec.keys_held("leftctrl"): + rec.mouse_wheel_smoothly(-0.3) + rec.mouse_wheel_smoothly(0.3) diff --git a/firebase.json b/firebase.json index ae0bf3ae7..1e4aed1f6 100644 --- a/firebase.json +++ b/firebase.json @@ -2,5 +2,9 @@ "hosting": { "target": "app", "public": "dist/client" + }, + "hosting": { + "target": "docs", + "public": "dist/docs" } } diff --git a/noxfile.py b/noxfile.py index f033de152..147d604f9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,3 +1,5 @@ +import os + import nox nox.options.reuse_existing_virtualenvs = True @@ -20,3 +22,22 @@ def format(session): def mypy(session): session.install("-r", "python/requirements-mypy.txt") session.run("mypy", ".") + + +@nox.session +def docs(session): + session.install("-r", "docs/requirements.txt") + session.run( + "sphinx-build", + "docs", + "dist/docs", + "-E", + "-j", + "auto", + "-T", + "-W", + "--keep-going", + env={ + "PYTHONPATH": os.path.join(os.path.dirname(__file__), "python"), + }, + )