├── .clang-format
├── .github
└── workflows
│ ├── docs.yml
│ ├── eslint.yml
│ ├── formatting.yaml
│ ├── publish.yml
│ ├── pyright.yml
│ ├── pyright_examples.yml
│ ├── pytest.yml
│ └── typescript-compile.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .prettierignore
├── LICENSE
├── README.md
├── docs
├── .gitignore
├── Makefile
├── README.md
├── requirements.txt
├── source
│ ├── _static
│ │ ├── css
│ │ │ └── custom.css
│ │ └── logo.svg
│ ├── _templates
│ │ └── sidebar
│ │ │ └── brand.html
│ ├── camera_handles.rst
│ ├── client_handles.rst
│ ├── conf.py
│ ├── conventions.rst
│ ├── development.rst
│ ├── embedded_visualizations.rst
│ ├── events.rst
│ ├── examples
│ │ ├── 00_coordinate_frames.rst
│ │ ├── 01_image.rst
│ │ ├── 02_gui.rst
│ │ ├── 03_gui_callbacks.rst
│ │ ├── 04_camera_poses.rst
│ │ ├── 05_camera_commands.rst
│ │ ├── 06_mesh.rst
│ │ ├── 07_record3d_visualizer.rst
│ │ ├── 08_smpl_visualizer.rst
│ │ ├── 09_urdf_visualizer.rst
│ │ ├── 10_realsense.rst
│ │ ├── 11_colmap_visualizer.rst
│ │ ├── 12_click_meshes.rst
│ │ ├── 13_theming.rst
│ │ ├── 14_markdown.rst
│ │ ├── 15_gui_in_scene.rst
│ │ ├── 16_modal.rst
│ │ ├── 17_background_composite.rst
│ │ ├── 18_lines.rst
│ │ ├── 19_get_renders.rst
│ │ ├── 20_scene_pointer.rst
│ │ ├── 21_set_up_direction.rst
│ │ ├── 22_games.rst
│ │ ├── 23_plotly.rst
│ │ ├── 24_plots_as_images.rst
│ │ ├── 25_smpl_visualizer_skinned.rst
│ │ ├── 26_lighting.rst
│ │ └── 27_notifications.rst
│ ├── extras.rst
│ ├── gui_api.rst
│ ├── gui_handles.rst
│ ├── icons.rst
│ ├── index.rst
│ ├── infrastructure.rst
│ ├── scene_api.rst
│ ├── scene_handles.rst
│ ├── server.rst
│ ├── state_serializer.rst
│ └── transforms.rst
└── update_example_docs.py
├── examples
├── 00_coordinate_frames.py
├── 01_image.py
├── 02_gui.py
├── 03_gui_callbacks.py
├── 04_camera_poses.py
├── 05_camera_commands.py
├── 06_mesh.py
├── 07_record3d_visualizer.py
├── 08_smpl_visualizer.py
├── 09_urdf_visualizer.py
├── 10_realsense.py
├── 11_colmap_visualizer.py
├── 12_click_meshes.py
├── 13_theming.py
├── 14_markdown.py
├── 15_gui_in_scene.py
├── 16_modal.py
├── 17_background_composite.py
├── 18_lines.py
├── 19_get_renders.py
├── 20_scene_pointer.py
├── 21_set_up_direction.py
├── 22_games.py
├── 23_plotly.py
├── 24_plots_as_images.py
├── 25_smpl_visualizer_skinned.py
├── 26_lighting.py
├── 27_notifications.py
├── assets
│ ├── .gitignore
│ ├── Cal_logo.png
│ ├── download_colmap_garden.sh
│ ├── download_dragon_mesh.sh
│ ├── download_record3d_dance.sh
│ └── mdx_example.mdx
└── experimental
│ └── gaussian_splats.py
├── examples_dev
├── 00_coordinate_frames.py
├── 01_image.py
├── 02_gui.py
├── 03_gui_callbacks.py
├── 04_camera_poses.py
├── 05_camera_commands.py
├── 06_mesh.py
├── 07_record3d_visualizer.py
├── 08_smpl_visualizer.py
├── 09_urdf_visualizer.py
├── 10_realsense.py
├── 11_colmap_visualizer.py
├── 12_click_meshes.py
├── 13_theming.py
├── 14_markdown.py
├── 15_gui_in_scene.py
├── 16_modal.py
├── 17_background_composite.py
├── 18_lines.py
├── 19_get_renders.py
├── 20_scene_pointer.py
├── 21_set_up_direction.py
├── 22_games.py
├── 23_plotly.py
├── 24_plots_as_images.py
├── 25_smpl_visualizer_skinned.py
├── 26_lighting.py
├── 27_notifications.py
├── 28_meshes_batched.py
└── README.md
├── pyproject.toml
├── src
└── viser
│ ├── __init__.py
│ ├── _assignable_props_api.py
│ ├── _client_autobuild.py
│ ├── _gui_api.py
│ ├── _gui_handles.py
│ ├── _icons.py
│ ├── _icons
│ └── tabler-icons.zip
│ ├── _icons_enum.py
│ ├── _icons_enum.pyi
│ ├── _icons_generate_enum.py
│ ├── _messages.py
│ ├── _notification_handle.py
│ ├── _scene_api.py
│ ├── _scene_handles.py
│ ├── _threadpool_exceptions.py
│ ├── _tunnel.py
│ ├── _viser.py
│ ├── client
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── index.html
│ ├── package.json
│ ├── postcss.config.cjs
│ ├── public
│ │ ├── Inter-VariableFont_slnt,wght.ttf
│ │ ├── hdri
│ │ │ ├── dikhololo_night_1k.hdr
│ │ │ ├── empty_warehouse_01_1k.hdr
│ │ │ ├── forest_slope_1k.hdr
│ │ │ ├── kiara_1_dawn_1k.hdr
│ │ │ ├── lebombo_1k.hdr
│ │ │ ├── potsdamer_platz_1k.hdr
│ │ │ ├── rooitou_park_1k.hdr
│ │ │ ├── st_fagans_interior_1k.hdr
│ │ │ ├── studio_small_03_1k.hdr
│ │ │ └── venice_sunset_1k.hdr
│ │ ├── logo.svg
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── App.css.ts
│ │ ├── App.tsx
│ │ ├── AppTheme.ts
│ │ ├── BrowserWarning.tsx
│ │ ├── CameraControls.tsx
│ │ ├── ControlPanel
│ │ │ ├── BottomPanel.tsx
│ │ │ ├── ControlPanel.tsx
│ │ │ ├── FloatingPanel.tsx
│ │ │ ├── Generated.tsx
│ │ │ ├── GuiComponentContext.tsx
│ │ │ ├── GuiState.tsx
│ │ │ ├── SceneTreeTable.css.ts
│ │ │ ├── SceneTreeTable.tsx
│ │ │ ├── ServerControls.tsx
│ │ │ └── SidebarPanel.tsx
│ │ ├── CsmDirectionalLight.tsx
│ │ ├── FilePlayback.tsx
│ │ ├── HoverContext.ts
│ │ ├── Line.tsx
│ │ ├── MacWindowWrapper.tsx
│ │ ├── Markdown.tsx
│ │ ├── MessageHandler.tsx
│ │ ├── Modal.tsx
│ │ ├── Outlines.tsx
│ │ ├── OutlinesIfHovered.tsx
│ │ ├── SceneTree.tsx
│ │ ├── SceneTreeState.tsx
│ │ ├── SearchParamsUtils.tsx
│ │ ├── ShadowArgs.tsx
│ │ ├── Splatting
│ │ │ ├── GaussianSplats.tsx
│ │ │ ├── GaussianSplatsHelpers.ts
│ │ │ ├── SplatSortWorker.ts
│ │ │ └── WasmSorter
│ │ │ │ ├── Sorter.mjs
│ │ │ │ ├── Sorter.wasm
│ │ │ │ ├── build.sh
│ │ │ │ └── sorter.cpp
│ │ ├── ThreeAssets.tsx
│ │ ├── Titlebar.tsx
│ │ ├── VersionInfo.ts
│ │ ├── ViewerContext.ts
│ │ ├── WebsocketInterface.tsx
│ │ ├── WebsocketMessages.ts
│ │ ├── WebsocketServerWorker.ts
│ │ ├── WebsocketUtils.ts
│ │ ├── WorldTransformUtils.ts
│ │ ├── components
│ │ │ ├── Button.tsx
│ │ │ ├── ButtonGroup.tsx
│ │ │ ├── Checkbox.tsx
│ │ │ ├── ComponentStyles.css.ts
│ │ │ ├── Dropdown.tsx
│ │ │ ├── Folder.css.ts
│ │ │ ├── Folder.tsx
│ │ │ ├── Html.tsx
│ │ │ ├── Image.tsx
│ │ │ ├── Markdown.tsx
│ │ │ ├── MultiSlider.tsx
│ │ │ ├── MultiSliderComponent.css
│ │ │ ├── MultiSliderComponent.tsx
│ │ │ ├── NumberInput.tsx
│ │ │ ├── PlotlyComponent.tsx
│ │ │ ├── ProgressBar.tsx
│ │ │ ├── Rgb.tsx
│ │ │ ├── Rgba.tsx
│ │ │ ├── Slider.tsx
│ │ │ ├── TabGroup.tsx
│ │ │ ├── TextInput.tsx
│ │ │ ├── UploadButton.tsx
│ │ │ ├── Vector2.tsx
│ │ │ ├── Vector3.tsx
│ │ │ ├── colorUtils.ts
│ │ │ └── common.tsx
│ │ ├── csm
│ │ │ ├── CSM.d.ts
│ │ │ ├── CSM.js
│ │ │ ├── CSMFrustum.js
│ │ │ ├── CSMHelper.js
│ │ │ ├── CSMShader.js
│ │ │ └── CSMShadowNode.js
│ │ ├── index.css
│ │ ├── index.tsx
│ │ ├── mesh
│ │ │ ├── BasicMesh.tsx
│ │ │ ├── BatchedGlbAsset.tsx
│ │ │ ├── BatchedMesh.tsx
│ │ │ ├── BatchedMeshBase.tsx
│ │ │ ├── BatchedMeshHoverOutlines.tsx
│ │ │ ├── GlbLoaderUtils.tsx
│ │ │ ├── MeshUtils.tsx
│ │ │ ├── SingleGlbAsset.tsx
│ │ │ └── SkinnedMesh.tsx
│ │ └── react-app-env.d.ts
│ ├── tsconfig.json
│ ├── vite-env.d.ts
│ ├── vite.config.mts
│ └── yarn.lock
│ ├── extras
│ ├── __init__.py
│ ├── _record3d.py
│ ├── _urdf.py
│ └── colmap
│ │ ├── __init__.py
│ │ └── _colmap_utils.py
│ ├── infra
│ ├── __init__.py
│ ├── _async_message_buffer.py
│ ├── _infra.py
│ ├── _messages.py
│ └── _typescript_interface_gen.py
│ ├── py.typed
│ ├── theme
│ ├── __init__.py
│ └── _titlebar.py
│ └── transforms
│ ├── __init__.py
│ ├── _base.py
│ ├── _se2.py
│ ├── _se3.py
│ ├── _so2.py
│ ├── _so3.py
│ ├── hints
│ └── __init__.py
│ └── utils
│ ├── __init__.py
│ └── _utils.py
├── sync_client_server.py
└── tests
├── test_garbage_collection.py
├── test_message_annotations.py
├── test_server_stop.py
├── test_transforms_axioms.py
├── test_transforms_bijective.py
├── test_transforms_ops.py
├── test_version_sync.py
└── utils.py
/.clang-format:
--------------------------------------------------------------------------------
1 | # C++ formatting rules; used for WebAssembly code.
2 | BasedOnStyle: LLVM
3 | AlignAfterOpenBracket: BlockIndent
4 | BinPackArguments: false
5 | BinPackParameters: false
6 | IndentWidth: 4
7 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: docs
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 | release:
9 | types: [created]
10 | workflow_dispatch:
11 |
12 | jobs:
13 | docs:
14 | runs-on: ubuntu-latest
15 | steps:
16 | # Check out source.
17 | - uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 0 # This ensures the entire history is fetched so we can switch branches
20 |
21 | - name: Set up Python
22 | uses: actions/setup-python@v1
23 | with:
24 | python-version: "3.10"
25 |
26 | - name: Set up dependencies
27 | run: |
28 | pip install uv
29 | uv pip install --system -e ".[dev,examples]"
30 | uv pip install --system -r docs/requirements.txt
31 |
32 | # Get version from pyproject.toml.
33 | - name: Get version + subdirectory
34 | run: |
35 | VERSION=$(python -c "import viser; print(viser.__version__)")
36 | echo "VERSION=$VERSION" >> $GITHUB_ENV
37 | echo "DOCS_SUBDIR=versions/$VERSION" >> $GITHUB_ENV
38 |
39 | # Hack to overwrite version.
40 | - name: Set version to 'main' for pushes (this will appear in the doc banner)
41 | run: |
42 | echo "VISER_VERSION_STR_OVERRIDE=main" >> $GITHUB_ENV
43 | if: github.event_name == 'push'
44 |
45 | # Build documentation.
46 | - name: Building documentation
47 | run: |
48 | sphinx-build docs/source docs/build -b dirhtml
49 |
50 | # Get version from pyproject.toml.
51 | - name: Override subdirectory to `main/` for pushes
52 | run: |
53 | echo "DOCS_SUBDIR=main" >> $GITHUB_ENV
54 | if: github.event_name == 'push'
55 |
56 | # Deploy to version-dependent subdirectory.
57 | - name: Deploy to GitHub Pages
58 | uses: peaceiris/actions-gh-pages@v4
59 | with:
60 | github_token: ${{ secrets.GITHUB_TOKEN }}
61 | publish_dir: ./docs/build
62 | destination_dir: ${{ env.DOCS_SUBDIR }}
63 | keep_files: false # This will only erase the destination subdirectory.
64 | cname: viser.studio
65 | if: github.event_name != 'pull_request'
66 |
67 | # We'll maintain an index of all versions under viser.studio/versions.
68 | # This will be useful for dynamically generating lists of possible doc links.
69 | - name: Update versions index.txt
70 | run: |
71 | git checkout . # Revert change to pyproject.toml from earlier...
72 | git checkout gh-pages
73 | git pull
74 | git config --global user.email "yibrenth@gmail.com"
75 | git config --global user.name "Brent Yi"
76 | FILE="versions/index.txt" # Replace with your file path
77 | if ! grep -qx "$VERSION" "$FILE"; then
78 | echo "$VERSION" >> "$FILE"
79 | git add $FILE
80 | git commit -m "Update versions.txt with new version $VERSION"
81 | git push origin gh-pages
82 | fi
83 | env:
84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
85 | VERSION: ${{ env.VERSION }}
86 | if: github.event_name == 'release'
87 |
--------------------------------------------------------------------------------
/.github/workflows/eslint.yml:
--------------------------------------------------------------------------------
1 | name: eslint
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | eslint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Use Node.js
15 | uses: actions/setup-node@v3
16 | with:
17 | node-version: 20
18 | - name: Run eslint
19 | run: |
20 | cd src/viser/client
21 | yarn
22 | yarn eslint .
23 |
--------------------------------------------------------------------------------
/.github/workflows/formatting.yaml:
--------------------------------------------------------------------------------
1 | name: formatting
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | - name: Set up Python 3.12
16 | uses: actions/setup-python@v1
17 | with:
18 | python-version: 3.12
19 | - name: Install dependencies
20 | run: |
21 | pip install uv
22 | uv pip install --system -e ".[dev,examples]"
23 | - name: Run Ruff
24 | run: ruff check docs/ src/ examples/
25 | - name: Run Ruff format
26 | run: ruff format docs/ src/ examples/ --check
27 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # This workflows will upload a Python Package using Poetry when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | deploy:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Set up Python
17 | uses: actions/setup-python@v4
18 | with:
19 | python-version: "3.8"
20 | - name: Install dependencies and build client
21 | run: |
22 | pip install uv
23 | uv pip install --system -e ".[dev]"
24 | uv pip install --system build twine
25 | # Build client files.
26 | python -c "import viser; viser.ViserServer()"
27 | - name: Strip unsupported tags in README
28 | run: |
29 | sed -i '//,//d' README.md
30 | - name: Only bundle client build for PyPI release
31 | run: |
32 | # This should delete everything in src/viser/client except for the
33 | # build folder + original source files. We don't want to package
34 | # .nodeenv, node_modules, etc in the release.
35 | mv src/viser/client/build ./__built_client
36 | rm -rf src/viser/client/*
37 | git checkout src/viser/client
38 | mv ./__built_client src/viser/client/build
39 | - name: Build and publish
40 | env:
41 | PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
42 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
43 | run: |
44 | python -m build
45 | twine upload --username $PYPI_USERNAME --password $PYPI_PASSWORD dist/*
46 |
--------------------------------------------------------------------------------
/.github/workflows/pyright.yml:
--------------------------------------------------------------------------------
1 | name: pyright
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | pyright:
11 | runs-on: ubuntu-22.04
12 | strategy:
13 | matrix:
14 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Set up Python ${{ matrix.python-version }}
19 | uses: actions/setup-python@v1
20 | with:
21 | python-version: ${{ matrix.python-version }}
22 | - name: Install dependencies
23 | run: |
24 | pip install uv
25 | uv pip install --system -e ".[dev,examples]"
26 | - name: Run pyright
27 | run: |
28 | pyright ./src
29 | pyright ./examples_dev
30 |
--------------------------------------------------------------------------------
/.github/workflows/pyright_examples.yml:
--------------------------------------------------------------------------------
1 | # Check that pyright passes on the examples when using the latest release. We
2 | # want to avoid breaking examples.
3 |
4 | name: pyright (examples)
5 |
6 | # Let's only run on pull requests. We'd expect this check to fail when making
7 | # pushes for new releases.
8 | on:
9 | # push:
10 | # branches: [main]
11 | pull_request:
12 | branches: [main]
13 |
14 | jobs:
15 | pyright:
16 | runs-on: ubuntu-22.04
17 | strategy:
18 | matrix:
19 | python-version: ["3.12"]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Set up Python ${{ matrix.python-version }}
24 | uses: actions/setup-python@v1
25 | with:
26 | python-version: ${{ matrix.python-version }}
27 | - name: Install dependencies
28 | run: |
29 | pip install uv
30 | uv pip install --system "viser[dev,examples]"
31 | - name: Run pyright
32 | run: |
33 | pyright examples
34 |
--------------------------------------------------------------------------------
/.github/workflows/pytest.yml:
--------------------------------------------------------------------------------
1 | name: pytest
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | python-version: ["3.8", "3.8", "3.9", "3.10", "3.11", "3.12"]
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Set up Python ${{ matrix.python-version }}
19 | uses: actions/setup-python@v4
20 | with:
21 | python-version: ${{ matrix.python-version }}
22 | - name: Install dependencies
23 | run: |
24 | pip install uv
25 | uv pip install --system -e ".[dev,examples]"
26 | - name: Test with pytest
27 | run: |
28 | pytest
29 |
--------------------------------------------------------------------------------
/.github/workflows/typescript-compile.yml:
--------------------------------------------------------------------------------
1 | name: typescript-compile
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | typescript-compile:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Use Node.js
15 | uses: actions/setup-node@v3
16 | with:
17 | node-version: 20
18 | - name: Run tsc
19 | run: |
20 | cd src/viser/client
21 | yarn
22 | yarn tsc
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.swo
3 | *.pyc
4 | *.egg-info
5 | *.ipynb_checkpoints
6 | __pycache__
7 | .coverage
8 | htmlcov
9 | .mypy_cache
10 | .dmypy.json
11 | .hypothesis
12 | .envrc
13 | .lvimrc
14 | .DS_Store
15 | .envrc
16 | .vite
17 | build
18 | src/viser/client/build
19 | src/viser/client/.nodeenv
20 |
21 | **/.claude/settings.local.json
22 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | default_language_version:
4 | python: python3
5 | repos:
6 | - repo: https://github.com/pre-commit/pre-commit-hooks
7 | rev: v3.2.0
8 | hooks:
9 | - id: trailing-whitespace
10 | - id: end-of-file-fixer
11 | - repo: https://github.com/astral-sh/ruff-pre-commit
12 | # Ruff version.
13 | rev: v0.6.2
14 | hooks:
15 | # Run the linter.
16 | - id: ruff
17 | args: [--fix]
18 | # Run the formatter.
19 | - id: ruff-format
20 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.mjs
2 | build/
3 | csm/
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | viser
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | `viser` is a library for interactive 3D visualization in Python.
16 |
17 | Features include:
18 |
19 | - API for visualizing 3D primitives
20 | - GUI building blocks: buttons, checkboxes, text inputs, sliders, etc.
21 | - Scene interaction tools (clicks, selection, transform gizmos)
22 | - Programmatic camera control and rendering
23 | - An entirely web-based client, for easy use over SSH!
24 |
25 | For usage and API reference, see our documentation.
26 |
27 | ## Installation
28 |
29 | You can install `viser` with `pip`:
30 |
31 | ```bash
32 | pip install viser
33 | ```
34 |
35 | To include example dependencies:
36 |
37 | ```bash
38 | pip install viser[examples]
39 | ```
40 |
41 | After an example script is running, you can connect by navigating to the printed
42 | URL (default: `http://localhost:8080`).
43 |
44 | See also: our [development docs](https://viser.studio/main/development/).
45 |
46 | ## Examples
47 |
48 | **Point cloud visualization**
49 |
50 | https://github.com/nerfstudio-project/viser/assets/6992947/df35c6ee-78a3-43ad-a2c7-1dddf83f7458
51 |
52 | Source: `./examples/07_record3d_visualizer.py`
53 |
54 | **Gaussian splatting visualization**
55 |
56 | https://github.com/nerfstudio-project/viser/assets/6992947/c51b4871-6cc8-4987-8751-2bf186bcb1ae
57 |
58 | Source:
59 | [WangFeng18/3d-gaussian-splatting](https://github.com/WangFeng18/3d-gaussian-splatting)
60 | and
61 | [heheyas/gaussian_splatting_3d](https://github.com/heheyas/gaussian_splatting_3d).
62 |
63 | **SMPLX visualizer**
64 |
65 | https://github.com/nerfstudio-project/viser/assets/6992947/78ba0e09-612d-4678-abf3-beaeeffddb01
66 |
67 | Source: `./example/08_smpl_visualizer.py`
68 |
69 | ## Acknowledgements
70 |
71 | `viser` is heavily inspired by packages like
72 | [Pangolin](https://github.com/stevenlovegrove/Pangolin),
73 | [rviz](https://wiki.ros.org/rviz/),
74 | [meshcat](https://github.com/rdeits/meshcat), and
75 | [Gradio](https://github.com/gradio-app/gradio).
76 | It's made possible by several open-source projects.
77 |
78 | The web client is implemented using [React](https://react.dev/), with:
79 |
80 | - [Vite](https://vitejs.dev/) / [Rollup](https://rollupjs.org/) for bundling
81 | - [three.js](https://threejs.org/) via [react-three-fiber](https://github.com/pmndrs/react-three-fiber) and [drei](https://github.com/pmndrs/drei)
82 | - [Mantine](https://mantine.dev/) for UI components
83 | - [zustand](https://github.com/pmndrs/zustand) for state management
84 | - [vanilla-extract](https://vanilla-extract.style/) for stylesheets
85 |
86 | The Python API communicates via [msgpack](https://msgpack.org/index.html) and [websockets](https://websockets.readthedocs.io/en/stable/index.html).
87 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = viser
8 | SOURCEDIR = source
9 | BUILDDIR = ./build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Viser Documentation
2 |
3 | This directory contains the documentation for Viser.
4 |
5 | ## Building the Documentation
6 |
7 | To build the documentation:
8 |
9 | 1. Install the documentation dependencies:
10 |
11 | ```bash
12 | pip install -r docs/requirements.txt
13 | ```
14 |
15 | 2. Build the documentation:
16 |
17 | ```bash
18 | cd docs
19 | make html
20 | ```
21 |
22 | 3. View the documentation:
23 |
24 | ```bash
25 | # On macOS
26 | open build/html/index.html
27 |
28 | # On Linux
29 | xdg-open build/html/index.html
30 | ```
31 |
32 | ## Contributing Screenshots
33 |
34 | When adding new documentation, screenshots and visual examples significantly improve user understanding.
35 |
36 | We need screenshots for:
37 |
38 | - The Getting Started guide
39 | - GUI element examples
40 | - Scene API visualization examples
41 | - Customization/theming examples
42 |
43 | See [Contributing Visuals](./source/contributing_visuals.md) for guidelines on capturing and adding images to the documentation.
44 |
45 | ## Documentation Structure
46 |
47 | - `source/` - Source files for the documentation
48 | - `_static/` - Static files (CSS, images, etc.)
49 | - `images/` - Screenshots and other images
50 | - `examples/` - Example code with documentation
51 | - `*.md` - Markdown files for documentation pages
52 | - `conf.py` - Sphinx configuration
53 |
54 | ## Auto-Generated Example Documentation
55 |
56 | Example documentation is automatically generated from the examples in the `examples/` directory using the `update_example_docs.py` script. To update the example documentation after making changes to examples:
57 |
58 | ```bash
59 | cd docs
60 | python update_example_docs.py
61 | ```
62 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx==8.0.2
2 | furo==2024.8.6
3 | docutils==0.20.1
4 | toml==0.10.2
5 | git+https://github.com/brentyi/sphinxcontrib-programoutput.git
6 | git+https://github.com/brentyi/ansi.git
7 | git+https://github.com/sphinx-contrib/googleanalytics.git
8 |
--------------------------------------------------------------------------------
/docs/source/_static/css/custom.css:
--------------------------------------------------------------------------------
1 | img.sidebar-logo {
2 | width: 5em;
3 | margin: 1em 0 0 0;
4 | }
5 |
--------------------------------------------------------------------------------
/docs/source/_static/logo.svg:
--------------------------------------------------------------------------------
1 | ../../../src/viser/client/public/logo.svg
--------------------------------------------------------------------------------
/docs/source/_templates/sidebar/brand.html:
--------------------------------------------------------------------------------
1 |
5 | {% block brand_content %} {%- if logo_url %}
6 |
11 | {%- endif %} {%- if theme_light_logo and theme_dark_logo %}
12 |
24 | {%- endif %}
25 |
26 |
27 | {% endblock brand_content %}
28 |
29 |
30 |
31 |
32 |
66 |
75 |
76 | Version: {{ version }}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
96 |
--------------------------------------------------------------------------------
/docs/source/camera_handles.rst:
--------------------------------------------------------------------------------
1 | Camera Handles
2 | ==============
3 |
4 | .. autoclass:: viser.CameraHandle
5 | :members:
6 | :undoc-members:
7 | :inherited-members:
8 |
--------------------------------------------------------------------------------
/docs/source/client_handles.rst:
--------------------------------------------------------------------------------
1 | Client Handles
2 | ==============
3 |
4 | .. autoclass:: viser.ClientHandle
5 | :members:
6 | :undoc-members:
7 | :inherited-members:
8 |
9 | .. autoclass:: viser.NotificationHandle
10 | :members:
11 | :undoc-members:
12 | :inherited-members:
13 |
--------------------------------------------------------------------------------
/docs/source/conventions.rst:
--------------------------------------------------------------------------------
1 | Frame Conventions
2 | =================
3 |
4 | In this note, we describe the coordinate frame conventions used in ``viser``.
5 |
6 | Scene tree naming
7 | -----------------
8 |
9 | Each object that we add to the scene in viser is instantiated as a node in a
10 | scene tree. The structure of this tree is determined by the names assigned to
11 | the nodes.
12 |
13 | If we add a coordinate frame called ``/base_link/shoulder/wrist``, it signifies
14 | three nodes: the ``wrist`` is a child of the ``shoulder`` which is a child of the
15 | ``base_link``.
16 |
17 | If we set the transformation of a given node like ``/base_link/shoulder``, both
18 | it and its child ``/base_link/shoulder/wrist`` will move. Its parent,
19 | ``/base_link``, will be unaffected.
20 |
21 | Poses
22 | -----
23 |
24 | Poses in ``viser`` are defined using a pair of fields:
25 |
26 | - ``wxyz``, a unit quaternion orientation term. This should always be 4D.
27 | - ``position``, a translation term. This should always be 3D.
28 |
29 | These correspond to a transformation from coordinates in the local frame to the
30 | parent frame:
31 |
32 | .. math::
33 |
34 | p_\mathrm{parent} = \begin{bmatrix} R & t \end{bmatrix}\begin{bmatrix}p_\mathrm{local} \\ 1\end{bmatrix}
35 |
36 | where ``wxyz`` is the quaternion form of the :math:`\mathrm{SO}(3)` matrix
37 | :math:`R` and ``position`` is the :math:`\mathbb{R}^3` translation term
38 | :math:`t`.
39 |
40 | World coordinates
41 | -----------------
42 |
43 | In the world coordinate space, +Z points upward by default. This can be
44 | overridden with :func:`viser.SceneApi.set_up_direction()`.
45 |
46 | Cameras
47 | -------
48 |
49 | In ``viser``, all camera parameters exposed to the Python API use the
50 | COLMAP/OpenCV convention:
51 |
52 | - Forward: +Z
53 | - Up: -Y
54 | - Right: +X
55 |
56 | Confusingly, this is different from Nerfstudio, which adopts the OpenGL/Blender
57 | convention:
58 |
59 | - Forward: -Z
60 | - Up: +Y
61 | - Right: +X
62 |
63 | Conversion between the two is a simple 180 degree rotation around the local X-axis.
64 |
--------------------------------------------------------------------------------
/docs/source/events.rst:
--------------------------------------------------------------------------------
1 | Events
2 | ======
3 |
4 | We define a small set of event types, which are passed to callback functions
5 | when events like clicks or GUI updates are triggered.
6 |
7 | .. autoclass:: viser.ScenePointerEvent()
8 |
9 | .. autoclass:: viser.SceneNodePointerEvent()
10 |
11 | .. autoclass:: viser.GuiEvent()
12 |
--------------------------------------------------------------------------------
/docs/source/examples/00_coordinate_frames.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Coordinate frames
5 | ==========================================
6 |
7 |
8 | In this basic example, we visualize a set of coordinate frames.
9 |
10 | Naming for all scene nodes are hierarchical; /tree/branch, for example, is defined
11 | relative to /tree.
12 |
13 |
14 |
15 | .. code-block:: python
16 | :linenos:
17 |
18 |
19 | import random
20 | import time
21 |
22 | import viser
23 |
24 | server = viser.ViserServer()
25 |
26 | while True:
27 | # Add some coordinate frames to the scene. These will be visualized in the viewer.
28 | server.scene.add_frame(
29 | "/tree",
30 | wxyz=(1.0, 0.0, 0.0, 0.0),
31 | position=(random.random() * 2.0, 2.0, 0.2),
32 | )
33 | server.scene.add_frame(
34 | "/tree/branch",
35 | wxyz=(1.0, 0.0, 0.0, 0.0),
36 | position=(random.random() * 2.0, 2.0, 0.2),
37 | )
38 | leaf = server.scene.add_frame(
39 | "/tree/branch/leaf",
40 | wxyz=(1.0, 0.0, 0.0, 0.0),
41 | position=(random.random() * 2.0, 2.0, 0.2),
42 | )
43 | time.sleep(5.0)
44 |
45 | # Remove the leaf node from the scene.
46 | leaf.remove()
47 | time.sleep(0.5)
48 |
--------------------------------------------------------------------------------
/docs/source/examples/01_image.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Images
5 | ==========================================
6 |
7 |
8 | Example for sending images to the viewer.
9 |
10 | We can send backgrond images to display behind the viewer (useful for visualizing
11 | NeRFs), or images to render as 3D textures.
12 |
13 |
14 |
15 | .. code-block:: python
16 | :linenos:
17 |
18 |
19 | import time
20 | from pathlib import Path
21 |
22 | import imageio.v3 as iio
23 | import numpy as np
24 |
25 | import viser
26 |
27 |
28 | def main() -> None:
29 | server = viser.ViserServer()
30 |
31 | # Add a background image.
32 | server.scene.set_background_image(
33 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"),
34 | format="png",
35 | )
36 |
37 | # Add main image.
38 | server.scene.add_image(
39 | "/img",
40 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"),
41 | 4.0,
42 | 4.0,
43 | format="png",
44 | wxyz=(1.0, 0.0, 0.0, 0.0),
45 | position=(2.0, 2.0, 0.0),
46 | )
47 | while True:
48 | server.scene.add_image(
49 | "/noise",
50 | np.random.randint(0, 256, size=(400, 400, 3), dtype=np.uint8),
51 | 4.0,
52 | 4.0,
53 | format="jpeg",
54 | wxyz=(1.0, 0.0, 0.0, 0.0),
55 | position=(2.0, 2.0, -1e-2),
56 | )
57 | time.sleep(0.2)
58 |
59 |
60 | if __name__ == "__main__":
61 | main()
62 |
--------------------------------------------------------------------------------
/docs/source/examples/04_camera_poses.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Camera poses
5 | ==========================================
6 |
7 |
8 | Example showing how we can detect new clients and read camera poses from them.
9 |
10 |
11 |
12 | .. code-block:: python
13 | :linenos:
14 |
15 |
16 | import time
17 |
18 | import viser
19 |
20 | server = viser.ViserServer()
21 | server.scene.world_axes.visible = True
22 |
23 |
24 | @server.on_client_connect
25 | def _(client: viser.ClientHandle) -> None:
26 | print("new client!")
27 |
28 | # This will run whenever we get a new camera!
29 | @client.camera.on_update
30 | def _(_: viser.CameraHandle) -> None:
31 | print(f"New camera on client {client.client_id}!")
32 |
33 | # Show the client ID in the GUI.
34 | gui_info = client.gui.add_text("Client ID", initial_value=str(client.client_id))
35 | gui_info.disabled = True
36 |
37 |
38 | while True:
39 | # Get all currently connected clients.
40 | clients = server.get_clients()
41 | print("Connected client IDs", clients.keys())
42 |
43 | for id, client in clients.items():
44 | print(f"Camera pose for client {id}")
45 | print(f"\twxyz: {client.camera.wxyz}")
46 | print(f"\tposition: {client.camera.position}")
47 | print(f"\tfov: {client.camera.fov}")
48 | print(f"\taspect: {client.camera.aspect}")
49 | print(f"\tlast update: {client.camera.update_timestamp}")
50 |
51 | time.sleep(2.0)
52 |
--------------------------------------------------------------------------------
/docs/source/examples/06_mesh.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Meshes
5 | ==========================================
6 |
7 |
8 | Visualize a mesh. To get the demo data, see ``./assets/download_dragon_mesh.sh``.
9 |
10 |
11 |
12 | .. code-block:: python
13 | :linenos:
14 |
15 |
16 | import time
17 | from pathlib import Path
18 |
19 | import numpy as np
20 | import trimesh
21 |
22 | import viser
23 | import viser.transforms as tf
24 |
25 | mesh = trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj"))
26 | assert isinstance(mesh, trimesh.Trimesh)
27 | mesh.apply_scale(0.05)
28 |
29 | vertices = mesh.vertices
30 | faces = mesh.faces
31 | print(f"Loaded mesh with {vertices.shape} vertices, {faces.shape} faces")
32 |
33 | server = viser.ViserServer()
34 | server.scene.add_mesh_simple(
35 | name="/simple",
36 | vertices=vertices,
37 | faces=faces,
38 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz,
39 | position=(0.0, 0.0, 0.0),
40 | )
41 | server.scene.add_mesh_trimesh(
42 | name="/trimesh",
43 | mesh=mesh,
44 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz,
45 | position=(0.0, 5.0, 0.0),
46 | )
47 | grid = server.scene.add_grid(
48 | "grid",
49 | width=20.0,
50 | height=20.0,
51 | position=np.array([0.0, 0.0, -2.0]),
52 | )
53 |
54 | while True:
55 | time.sleep(10.0)
56 |
--------------------------------------------------------------------------------
/docs/source/examples/12_click_meshes.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Mesh click events
5 | ==========================================
6 |
7 |
8 | Click on meshes to select them. The index of the last clicked mesh is displayed in the GUI.
9 |
10 |
11 |
12 | .. code-block:: python
13 | :linenos:
14 |
15 |
16 | import time
17 |
18 | import matplotlib
19 |
20 | import viser
21 |
22 |
23 | def main() -> None:
24 | grid_shape = (4, 5)
25 | server = viser.ViserServer()
26 |
27 | with server.gui.add_folder("Last clicked"):
28 | x_value = server.gui.add_number(
29 | label="x",
30 | initial_value=0,
31 | disabled=True,
32 | hint="x coordinate of the last clicked mesh",
33 | )
34 | y_value = server.gui.add_number(
35 | label="y",
36 | initial_value=0,
37 | disabled=True,
38 | hint="y coordinate of the last clicked mesh",
39 | )
40 |
41 | def add_swappable_mesh(i: int, j: int) -> None:
42 | """Simple callback that swaps between:
43 | - a gray box
44 | - a colored box
45 | - a colored sphere
46 |
47 | Color is chosen based on the position (i, j) of the mesh in the grid.
48 | """
49 |
50 | colormap = matplotlib.colormaps["tab20"]
51 |
52 | def create_mesh(counter: int) -> None:
53 | if counter == 0:
54 | color = (0.8, 0.8, 0.8)
55 | else:
56 | index = (i * grid_shape[1] + j) / (grid_shape[0] * grid_shape[1])
57 | color = colormap(index)[:3]
58 |
59 | if counter in (0, 1):
60 | handle = server.scene.add_box(
61 | name=f"/sphere_{i}_{j}",
62 | position=(i, j, 0.0),
63 | color=color,
64 | dimensions=(0.5, 0.5, 0.5),
65 | )
66 | else:
67 | handle = server.scene.add_icosphere(
68 | name=f"/sphere_{i}_{j}",
69 | radius=0.4,
70 | color=color,
71 | position=(i, j, 0.0),
72 | )
73 |
74 | @handle.on_click
75 | def _(_) -> None:
76 | x_value.value = i
77 | y_value.value = j
78 |
79 | # The new mesh will replace the old one because the names
80 | # /sphere_{i}_{j} are the same.
81 | create_mesh((counter + 1) % 3)
82 |
83 | create_mesh(0)
84 |
85 | for i in range(grid_shape[0]):
86 | for j in range(grid_shape[1]):
87 | add_swappable_mesh(i, j)
88 |
89 | while True:
90 | time.sleep(10.0)
91 |
92 |
93 | if __name__ == "__main__":
94 | main()
95 |
--------------------------------------------------------------------------------
/docs/source/examples/14_markdown.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Markdown demonstration
5 | ==========================================
6 |
7 |
8 | Viser GUI has MDX 2 support.
9 |
10 |
11 |
12 | .. code-block:: python
13 | :linenos:
14 |
15 |
16 | import time
17 | from pathlib import Path
18 |
19 | import viser
20 |
21 | server = viser.ViserServer()
22 | server.scene.world_axes.visible = True
23 |
24 | markdown_counter = server.gui.add_markdown("Counter: 0")
25 |
26 | here = Path(__file__).absolute().parent
27 |
28 | button = server.gui.add_button("Remove blurb")
29 | checkbox = server.gui.add_checkbox("Visibility", initial_value=True)
30 |
31 | markdown_source = (here / "./assets/mdx_example.mdx").read_text()
32 | markdown_blurb = server.gui.add_markdown(
33 | content=markdown_source,
34 | image_root=here,
35 | )
36 |
37 |
38 | @button.on_click
39 | def _(_):
40 | markdown_blurb.remove()
41 |
42 |
43 | @checkbox.on_update
44 | def _(_):
45 | markdown_blurb.visible = checkbox.value
46 |
47 |
48 | counter = 0
49 | while True:
50 | markdown_counter.content = f"Counter: {counter}"
51 | counter += 1
52 | time.sleep(0.1)
53 |
--------------------------------------------------------------------------------
/docs/source/examples/16_modal.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Modal basics
5 | ==========================================
6 |
7 |
8 | Examples of using modals in Viser.
9 |
10 |
11 |
12 | .. code-block:: python
13 | :linenos:
14 |
15 |
16 | import time
17 |
18 | import viser
19 |
20 |
21 | def main():
22 | server = viser.ViserServer()
23 |
24 | @server.on_client_connect
25 | def _(client: viser.ClientHandle) -> None:
26 | with client.gui.add_modal("Modal example"):
27 | client.gui.add_markdown(
28 | "**The input below determines the title of the modal...**"
29 | )
30 |
31 | gui_title = client.gui.add_text(
32 | "Title",
33 | initial_value="My Modal",
34 | )
35 |
36 | modal_button = client.gui.add_button("Show more modals")
37 |
38 | @modal_button.on_click
39 | def _(_) -> None:
40 | with client.gui.add_modal(gui_title.value) as modal:
41 | client.gui.add_markdown("This is content inside the modal!")
42 | client.gui.add_button("Close").on_click(lambda _: modal.close())
43 |
44 | while True:
45 | time.sleep(0.15)
46 |
47 |
48 | if __name__ == "__main__":
49 | main()
50 |
--------------------------------------------------------------------------------
/docs/source/examples/17_background_composite.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Depth compositing
5 | ==========================================
6 |
7 |
8 | In this example, we show how to use a background image with depth compositing. This can
9 | be useful when we want a 2D image to occlude 3D geometry, such as for NeRF rendering.
10 |
11 |
12 |
13 | .. code-block:: python
14 | :linenos:
15 |
16 |
17 | import time
18 |
19 | import numpy as np
20 | import trimesh
21 | import trimesh.creation
22 |
23 | import viser
24 |
25 | server = viser.ViserServer()
26 |
27 |
28 | img = np.random.randint(0, 255, size=(1000, 1000, 3), dtype=np.uint8)
29 | depth = np.ones((1000, 1000, 1), dtype=np.float32)
30 |
31 | # Make a square middle portal.
32 | depth[250:750, 250:750, :] = 10.0
33 | img[250:750, 250:750, :] = 255
34 |
35 | mesh = trimesh.creation.box((0.5, 0.5, 0.5))
36 | server.scene.add_mesh_trimesh(
37 | name="/cube",
38 | mesh=mesh,
39 | position=(0, 0, 0.0),
40 | )
41 | server.scene.set_background_image(img, depth=depth)
42 |
43 |
44 | while True:
45 | time.sleep(1.0)
46 |
--------------------------------------------------------------------------------
/docs/source/examples/18_lines.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Lines
5 | ==========================================
6 |
7 |
8 | Make a ball with some random line segments and splines.
9 |
10 |
11 |
12 | .. code-block:: python
13 | :linenos:
14 |
15 |
16 | import time
17 |
18 | import numpy as np
19 |
20 | import viser
21 |
22 |
23 | def main() -> None:
24 | server = viser.ViserServer()
25 |
26 | # Line segments.
27 | #
28 | # This will be much faster than creating separate scene objects for
29 | # individual line segments or splines.
30 | N = 2000
31 | points = np.random.normal(size=(N, 2, 3)) * 3.0
32 | colors = np.random.randint(0, 255, size=(N, 2, 3))
33 | server.scene.add_line_segments(
34 | "/line_segments",
35 | points=points,
36 | colors=colors,
37 | line_width=3.0,
38 | )
39 |
40 | # Spline helpers.
41 | #
42 | # If many lines are needed, it'll be more efficient to batch them in
43 | # `add_line_segments()`.
44 | for i in range(10):
45 | points = np.random.normal(size=(30, 3)) * 3.0
46 | server.scene.add_spline_catmull_rom(
47 | f"/catmull/{i}",
48 | positions=points,
49 | tension=0.5,
50 | line_width=3.0,
51 | color=np.random.uniform(size=3),
52 | segments=100,
53 | )
54 |
55 | control_points = np.random.normal(size=(30 * 2 - 2, 3)) * 3.0
56 | server.scene.add_spline_cubic_bezier(
57 | f"/cubic_bezier/{i}",
58 | positions=points,
59 | control_points=control_points,
60 | line_width=3.0,
61 | color=np.random.uniform(size=3),
62 | segments=100,
63 | )
64 |
65 | while True:
66 | time.sleep(10.0)
67 |
68 |
69 | if __name__ == "__main__":
70 | main()
71 |
--------------------------------------------------------------------------------
/docs/source/examples/19_get_renders.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Get renders
5 | ==========================================
6 |
7 |
8 | Example for getting renders from a client's viewport to the Python API.
9 |
10 |
11 |
12 | .. code-block:: python
13 | :linenos:
14 |
15 |
16 | import time
17 |
18 | import imageio.v3 as iio
19 | import numpy as np
20 |
21 | import viser
22 |
23 |
24 | def main():
25 | server = viser.ViserServer()
26 |
27 | button = server.gui.add_button("Render a GIF")
28 |
29 | @button.on_click
30 | def _(event: viser.GuiEvent) -> None:
31 | client = event.client
32 | assert client is not None
33 |
34 | client.scene.reset()
35 |
36 | images = []
37 |
38 | for i in range(20):
39 | positions = np.random.normal(size=(30, 3))
40 | client.scene.add_spline_catmull_rom(
41 | f"/catmull_{i}",
42 | positions,
43 | tension=0.5,
44 | line_width=3.0,
45 | color=np.random.uniform(size=3),
46 | )
47 | images.append(client.get_render(height=720, width=1280))
48 | print("Got image with shape", images[-1].shape)
49 |
50 | print("Generating and sending GIF...")
51 | client.send_file_download(
52 | "image.gif", iio.imwrite("", images, extension=".gif", loop=0)
53 | )
54 | print("Done!")
55 |
56 | while True:
57 | time.sleep(10.0)
58 |
59 |
60 | if __name__ == "__main__":
61 | main()
62 |
--------------------------------------------------------------------------------
/docs/source/examples/21_set_up_direction.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Set up direction
5 | ==========================================
6 |
7 |
8 | ``.set_up_direction()`` can help us set the global up direction.
9 |
10 |
11 |
12 | .. code-block:: python
13 | :linenos:
14 |
15 |
16 | import time
17 |
18 | import viser
19 |
20 |
21 | def main() -> None:
22 | server = viser.ViserServer()
23 | server.scene.world_axes.visible = True
24 | gui_up = server.gui.add_vector3(
25 | "Up Direction",
26 | initial_value=(0.0, 0.0, 1.0),
27 | step=0.01,
28 | )
29 |
30 | @gui_up.on_update
31 | def _(_) -> None:
32 | server.scene.set_up_direction(gui_up.value)
33 |
34 | while True:
35 | time.sleep(1.0)
36 |
37 |
38 | if __name__ == "__main__":
39 | main()
40 |
--------------------------------------------------------------------------------
/docs/source/examples/23_plotly.rst:
--------------------------------------------------------------------------------
1 | .. Comment: this file is automatically generated by `update_example_docs.py`.
2 | It should not be modified manually.
3 |
4 | Plotly
5 | ==========================================
6 |
7 |
8 | Examples of visualizing plotly plots in Viser.
9 |
10 |
11 |
12 | .. code-block:: python
13 | :linenos:
14 |
15 |
16 | import time
17 |
18 | import numpy as np
19 | import plotly.express as px
20 | import plotly.graph_objects as go
21 | from PIL import Image
22 |
23 | import viser
24 |
25 |
26 | def create_sinusoidal_wave(t: float) -> go.Figure:
27 | """Create a sinusoidal wave plot, starting at time t."""
28 | x_data = np.linspace(t, t + 6 * np.pi, 50)
29 | y_data = np.sin(x_data) * 10
30 |
31 | fig = px.line(
32 | x=list(x_data),
33 | y=list(y_data),
34 | labels={"x": "x", "y": "sin(x)"},
35 | title="Sinusoidal Wave",
36 | )
37 |
38 | # this sets the margins to be tight around the title.
39 | fig.layout.title.automargin = True # type: ignore
40 | fig.update_layout(
41 | margin=dict(l=20, r=20, t=20, b=20),
42 | ) # Reduce plot margins.
43 |
44 | return fig
45 |
46 |
47 | def main() -> None:
48 | server = viser.ViserServer()
49 |
50 | # Plot type 1: Line plot.
51 | line_plot_time = 0.0
52 | line_plot = server.gui.add_plotly(figure=create_sinusoidal_wave(line_plot_time))
53 |
54 | # Plot type 2: Image plot.
55 | fig = px.imshow(Image.open("assets/Cal_logo.png"))
56 | fig.update_layout(
57 | margin=dict(l=20, r=20, t=20, b=20),
58 | )
59 | server.gui.add_plotly(figure=fig, aspect=1.0)
60 |
61 | # Plot type 3: 3D Scatter plot.
62 | fig = px.scatter_3d(
63 | px.data.iris(),
64 | x="sepal_length",
65 | y="sepal_width",
66 | z="petal_width",
67 | color="species",
68 | )
69 | fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01))
70 | fig.update_layout(
71 | margin=dict(l=20, r=20, t=20, b=20),
72 | )
73 | server.gui.add_plotly(figure=fig, aspect=1.0)
74 |
75 | while True:
76 | # Update the line plot.
77 | line_plot_time += 0.1
78 | line_plot.figure = create_sinusoidal_wave(line_plot_time)
79 |
80 | time.sleep(0.01)
81 |
82 |
83 | if __name__ == "__main__":
84 | main()
85 |
--------------------------------------------------------------------------------
/docs/source/extras.rst:
--------------------------------------------------------------------------------
1 | Record3D + URDF Helpers
2 | =======================
3 |
4 | .. automodule:: viser.extras
5 |
--------------------------------------------------------------------------------
/docs/source/gui_api.rst:
--------------------------------------------------------------------------------
1 | GUI API
2 | =======
3 |
4 | .. autoclass:: viser.GuiApi
5 | :members:
6 | :undoc-members:
7 | :inherited-members:
8 |
--------------------------------------------------------------------------------
/docs/source/gui_handles.rst:
--------------------------------------------------------------------------------
1 | GUI Handles
2 | ===========
3 |
4 | .. autoclass:: viser.GuiInputHandle()
5 |
6 | .. autoclass:: viser.GuiButtonHandle()
7 |
8 | .. autoclass:: viser.GuiButtonGroupHandle()
9 |
10 | .. autoclass:: viser.GuiDropdownHandle()
11 |
12 | .. autoclass:: viser.GuiFolderHandle()
13 |
14 | .. autoclass:: viser.GuiMarkdownHandle()
15 |
16 | .. autoclass:: viser.GuiHtmlHandle()
17 |
18 | .. autoclass:: viser.GuiPlotlyHandle()
19 |
20 | .. autoclass:: viser.GuiTabGroupHandle()
21 |
22 | .. autoclass:: viser.GuiTabHandle()
23 |
24 | .. autoclass:: viser.GuiCheckboxHandle()
25 |
26 | .. autoclass:: viser.GuiMultiSliderHandle()
27 |
28 | .. autoclass:: viser.GuiNumberHandle()
29 |
30 | .. autoclass:: viser.GuiRgbaHandle()
31 |
32 | .. autoclass:: viser.GuiRgbHandle()
33 |
34 | .. autoclass:: viser.GuiSliderHandle()
35 |
36 | .. autoclass:: viser.GuiTextHandle()
37 |
38 | .. autoclass:: viser.GuiUploadButtonHandle()
39 |
40 | .. autoclass:: viser.UploadedFile()
41 |
42 | .. autoclass:: viser.GuiVector2Handle()
43 |
44 | .. autoclass:: viser.GuiVector3Handle()
45 |
--------------------------------------------------------------------------------
/docs/source/icons.rst:
--------------------------------------------------------------------------------
1 | Icons
2 | =====
3 |
4 | Icons for GUI elements (such as :meth:`GuiApi.add_button()`) can be
5 | specified using the :class:`viser.Icon` enum.
6 |
7 | .. autoclass:: viser.IconName
8 | .. autoclass:: viser.Icon
9 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | viser
2 | =====
3 |
4 | |pyright| |nbsp| |typescript| |nbsp| |versions|
5 |
6 | **viser** is a library for interactive 3D visualization in Python.
7 |
8 | Features include:
9 |
10 | - API for visualizing 3D primitives
11 | - GUI building blocks: buttons, checkboxes, text inputs, sliders, etc.
12 | - Scene interaction tools (clicks, selection, transform gizmos)
13 | - Programmatic camera control and rendering
14 | - An entirely web-based client, for easy use over SSH!
15 |
16 | Installation
17 | -----------
18 |
19 | You can install ``viser`` with ``pip``:
20 |
21 | .. code-block:: bash
22 |
23 | pip install viser
24 |
25 | To include example dependencies:
26 |
27 | .. code-block:: bash
28 |
29 | pip install viser[examples]
30 |
31 | After an example script is running, you can connect by navigating to the printed
32 | URL (default: ``http://localhost:8080``).
33 |
34 | .. toctree::
35 | :caption: Notes
36 | :hidden:
37 | :maxdepth: 1
38 | :titlesonly:
39 |
40 | ./conventions.rst
41 | ./development.rst
42 | ./embedded_visualizations.rst
43 |
44 | .. toctree::
45 | :caption: API (Basics)
46 | :hidden:
47 | :maxdepth: 1
48 | :titlesonly:
49 |
50 | ./server.rst
51 | ./scene_api.rst
52 | ./gui_api.rst
53 | ./state_serializer.rst
54 |
55 |
56 | .. toctree::
57 | :caption: API (Advanced)
58 | :hidden:
59 | :maxdepth: 1
60 | :titlesonly:
61 |
62 | ./client_handles.rst
63 | ./camera_handles.rst
64 | ./gui_handles.rst
65 | ./scene_handles.rst
66 | ./events.rst
67 | ./icons.rst
68 |
69 |
70 | .. toctree::
71 | :caption: API (Auxiliary)
72 | :hidden:
73 | :maxdepth: 1
74 | :titlesonly:
75 |
76 | ./transforms.rst
77 | ./infrastructure.rst
78 | ./extras.rst
79 |
80 | .. toctree::
81 | :caption: Examples
82 | :hidden:
83 | :maxdepth: 1
84 | :titlesonly:
85 | :glob:
86 |
87 | examples/*
88 |
89 |
90 | .. |pyright| image:: https://github.com/nerfstudio-project/viser/actions/workflows/pyright.yml/badge.svg
91 | :alt: Pyright status icon
92 | :target: https://github.com/nerfstudio-project/viser
93 | .. |typescript| image:: https://github.com/nerfstudio-project/viser/actions/workflows/typescript-compile.yml/badge.svg
94 | :alt: TypeScript status icon
95 | :target: https://github.com/nerfstudio-project/viser
96 | .. |versions| image:: https://img.shields.io/pypi/pyversions/viser
97 | :alt: Version icon
98 | :target: https://pypi.org/project/viser/
99 | .. |nbsp| unicode:: 0xA0
100 | :trim:
--------------------------------------------------------------------------------
/docs/source/infrastructure.rst:
--------------------------------------------------------------------------------
1 | Communication
2 | =============
3 |
4 | .. automodule:: viser.infra
5 | :show-inheritance:
6 |
--------------------------------------------------------------------------------
/docs/source/scene_api.rst:
--------------------------------------------------------------------------------
1 | Scene API
2 | =========
3 |
4 | .. autoclass:: viser.SceneApi
5 | :members:
6 | :undoc-members:
7 | :inherited-members:
8 |
--------------------------------------------------------------------------------
/docs/source/scene_handles.rst:
--------------------------------------------------------------------------------
1 | Scene Handles
2 | =============
3 |
4 | A handle is created for each object that is added to the scene. These can be
5 | used to read and set state, as well as detect clicks.
6 |
7 | When a scene node is added to a server (for example, via
8 | :func:`viser.ViserServer.add_frame()`), state is synchronized between all
9 | connected clients. When a scene node is added to a client (for example, via
10 | :func:`viser.ClientHandle.add_frame()`), state is local to a specific client.
11 |
12 | The most common attributes to read and write here are
13 | :attr:`viser.SceneNodeHandle.wxyz` and :attr:`viser.SceneNodeHandle.position`.
14 | Each node type also has type-specific attributes that we can read and write.
15 | Many of these are lower-level than their equivalent arguments in factory
16 | methods like :func:`viser.ViserServer.add_frame()` or
17 | :func:`viser.ViserServer.add_image()`.
18 |
19 | .. autoclass:: viser.SceneNodeHandle
20 |
21 | .. autoclass:: viser.CameraFrustumHandle
22 |
23 | .. autoclass:: viser.FrameHandle
24 |
25 | .. autoclass:: viser.BatchedAxesHandle
26 |
27 | .. autoclass:: viser.GlbHandle
28 |
29 | .. autoclass:: viser.GridHandle
30 |
31 | .. autoclass:: viser.Gui3dContainerHandle
32 |
33 | .. autoclass:: viser.ImageHandle
34 |
35 | .. autoclass:: viser.LabelHandle
36 |
37 | .. autoclass:: viser.MeshHandle
38 |
39 | .. autoclass:: viser.BatchedMeshHandle
40 |
41 | .. autoclass:: viser.BatchedGlbHandle
42 |
43 | .. autoclass:: viser.MeshSkinnedHandle
44 |
45 | .. autoclass:: viser.MeshSkinnedBoneHandle
46 |
47 | .. autoclass:: viser.PointCloudHandle
48 |
49 | .. autoclass:: viser.SplineCatmullRomHandle
50 |
51 | .. autoclass:: viser.SplineCubicBezierHandle
52 |
53 | .. autoclass:: viser.TransformControlsHandle
54 |
55 | .. autoclass:: viser.GaussianSplatHandle
56 |
57 | .. autoclass:: viser.DirectionalLightHandle
58 |
59 | .. autoclass:: viser.AmbientLightHandle
60 |
61 | .. autoclass:: viser.HemisphereLightHandle
62 |
63 | .. autoclass:: viser.PointLightHandle
64 |
65 | .. autoclass:: viser.RectAreaLightHandle
66 |
67 | .. autoclass:: viser.SpotLightHandle
--------------------------------------------------------------------------------
/docs/source/server.rst:
--------------------------------------------------------------------------------
1 | Viser Server
2 | ============
3 |
4 | .. autoclass:: viser.ViserServer
5 |
--------------------------------------------------------------------------------
/docs/source/state_serializer.rst:
--------------------------------------------------------------------------------
1 | State Serializer
2 | ===============
3 |
4 | .. autoclass:: viser.infra.StateSerializer
5 | :members:
6 | :undoc-members:
7 | :inherited-members:
8 |
--------------------------------------------------------------------------------
/docs/source/transforms.rst:
--------------------------------------------------------------------------------
1 | Transforms
2 | ==========
3 |
4 | .. automodule:: viser.transforms
5 | :show-inheritance:
6 |
--------------------------------------------------------------------------------
/docs/update_example_docs.py:
--------------------------------------------------------------------------------
1 | """Helper script for updating the auto-generated examples pages in the documentation."""
2 |
3 | from __future__ import annotations
4 |
5 | import dataclasses
6 | import pathlib
7 | import shutil
8 | from typing import Iterable
9 |
10 | import m2r2
11 | import tyro
12 |
13 |
14 | @dataclasses.dataclass
15 | class ExampleMetadata:
16 | index: str
17 | index_with_zero: str
18 | source: str
19 | title: str
20 | description: str
21 |
22 | @staticmethod
23 | def from_path(path: pathlib.Path) -> ExampleMetadata:
24 | # 01_functions -> 01, _, functions.
25 | index, _, _ = path.stem.partition("_")
26 |
27 | # 01 -> 1.
28 | index_with_zero = index
29 | index = str(int(index))
30 |
31 | print("Parsing", path)
32 | source = path.read_text().strip()
33 | docstring = source.split('"""')[1].strip()
34 |
35 | title, _, description = docstring.partition("\n")
36 |
37 | return ExampleMetadata(
38 | index=index,
39 | index_with_zero=index_with_zero,
40 | source=source.partition('"""')[2].partition('"""')[2].strip(),
41 | title=title,
42 | description=description.strip(),
43 | )
44 |
45 |
46 | def get_example_paths(examples_dir: pathlib.Path) -> Iterable[pathlib.Path]:
47 | return filter(
48 | lambda p: not p.name.startswith("_"), sorted(examples_dir.glob("*.py"))
49 | )
50 |
51 |
52 | REPO_ROOT = pathlib.Path(__file__).absolute().parent.parent
53 |
54 |
55 | def main(
56 | examples_dir: pathlib.Path = REPO_ROOT / "examples",
57 | sphinx_source_dir: pathlib.Path = REPO_ROOT / "docs" / "source",
58 | ) -> None:
59 | example_doc_dir = sphinx_source_dir / "examples"
60 | shutil.rmtree(example_doc_dir)
61 | example_doc_dir.mkdir()
62 |
63 | for path in get_example_paths(examples_dir):
64 | ex = ExampleMetadata.from_path(path)
65 |
66 | relative_dir = path.parent.relative_to(examples_dir)
67 | target_dir = example_doc_dir / relative_dir
68 | target_dir.mkdir(exist_ok=True, parents=True)
69 |
70 | (target_dir / f"{path.stem}.rst").write_text(
71 | "\n".join(
72 | [
73 | (
74 | ".. Comment: this file is automatically generated by"
75 | " `update_example_docs.py`."
76 | ),
77 | " It should not be modified manually.",
78 | "",
79 | f"{ex.title}",
80 | "==========================================",
81 | "",
82 | m2r2.convert(ex.description),
83 | "",
84 | "",
85 | ".. code-block:: python",
86 | " :linenos:",
87 | "",
88 | "",
89 | "\n".join(
90 | f" {line}".rstrip() for line in ex.source.split("\n")
91 | ),
92 | "",
93 | ]
94 | )
95 | )
96 |
97 |
98 | if __name__ == "__main__":
99 | tyro.cli(main, description=__doc__)
100 |
--------------------------------------------------------------------------------
/examples/00_coordinate_frames.py:
--------------------------------------------------------------------------------
1 | """Coordinate frames
2 |
3 | In this basic example, we visualize a set of coordinate frames.
4 |
5 | Naming for all scene nodes are hierarchical; /tree/branch, for example, is defined
6 | relative to /tree.
7 | """
8 |
9 | import random
10 | import time
11 |
12 | import viser
13 |
14 | server = viser.ViserServer()
15 |
16 | while True:
17 | # Add some coordinate frames to the scene. These will be visualized in the viewer.
18 | server.scene.add_frame(
19 | "/tree",
20 | wxyz=(1.0, 0.0, 0.0, 0.0),
21 | position=(random.random() * 2.0, 2.0, 0.2),
22 | )
23 | server.scene.add_frame(
24 | "/tree/branch",
25 | wxyz=(1.0, 0.0, 0.0, 0.0),
26 | position=(random.random() * 2.0, 2.0, 0.2),
27 | )
28 | leaf = server.scene.add_frame(
29 | "/tree/branch/leaf",
30 | wxyz=(1.0, 0.0, 0.0, 0.0),
31 | position=(random.random() * 2.0, 2.0, 0.2),
32 | )
33 | time.sleep(5.0)
34 |
35 | # Remove the leaf node from the scene.
36 | leaf.remove()
37 | time.sleep(0.5)
38 |
--------------------------------------------------------------------------------
/examples/01_image.py:
--------------------------------------------------------------------------------
1 | """Images
2 |
3 | Example for sending images to the viewer.
4 |
5 | We can send backgrond images to display behind the viewer (useful for visualizing
6 | NeRFs), or images to render as 3D textures.
7 | """
8 |
9 | import time
10 | from pathlib import Path
11 |
12 | import imageio.v3 as iio
13 | import numpy as np
14 |
15 | import viser
16 |
17 |
18 | def main() -> None:
19 | server = viser.ViserServer()
20 |
21 | # Add a background image.
22 | server.scene.set_background_image(
23 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"),
24 | format="png",
25 | )
26 |
27 | # Add main image.
28 | server.scene.add_image(
29 | "/img",
30 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"),
31 | 4.0,
32 | 4.0,
33 | format="png",
34 | wxyz=(1.0, 0.0, 0.0, 0.0),
35 | position=(2.0, 2.0, 0.0),
36 | )
37 | while True:
38 | server.scene.add_image(
39 | "/noise",
40 | np.random.randint(0, 256, size=(400, 400, 3), dtype=np.uint8),
41 | 4.0,
42 | 4.0,
43 | format="jpeg",
44 | wxyz=(1.0, 0.0, 0.0, 0.0),
45 | position=(2.0, 2.0, -1e-2),
46 | )
47 | time.sleep(0.2)
48 |
49 |
50 | if __name__ == "__main__":
51 | main()
52 |
--------------------------------------------------------------------------------
/examples/04_camera_poses.py:
--------------------------------------------------------------------------------
1 | """Camera poses
2 |
3 | Example showing how we can detect new clients and read camera poses from them.
4 | """
5 |
6 | import time
7 |
8 | import viser
9 |
10 | server = viser.ViserServer()
11 | server.scene.world_axes.visible = True
12 |
13 |
14 | @server.on_client_connect
15 | def _(client: viser.ClientHandle) -> None:
16 | print("new client!")
17 |
18 | # This will run whenever we get a new camera!
19 | @client.camera.on_update
20 | def _(_: viser.CameraHandle) -> None:
21 | print(f"New camera on client {client.client_id}!")
22 |
23 | # Show the client ID in the GUI.
24 | gui_info = client.gui.add_text("Client ID", initial_value=str(client.client_id))
25 | gui_info.disabled = True
26 |
27 |
28 | while True:
29 | # Get all currently connected clients.
30 | clients = server.get_clients()
31 | print("Connected client IDs", clients.keys())
32 |
33 | for id, client in clients.items():
34 | print(f"Camera pose for client {id}")
35 | print(f"\twxyz: {client.camera.wxyz}")
36 | print(f"\tposition: {client.camera.position}")
37 | print(f"\tfov: {client.camera.fov}")
38 | print(f"\taspect: {client.camera.aspect}")
39 | print(f"\tlast update: {client.camera.update_timestamp}")
40 |
41 | time.sleep(2.0)
42 |
--------------------------------------------------------------------------------
/examples/05_camera_commands.py:
--------------------------------------------------------------------------------
1 | """Camera commands
2 |
3 | In addition to reads, camera parameters also support writes. These are synced to the
4 | corresponding client automatically.
5 | """
6 |
7 | import time
8 |
9 | import numpy as np
10 |
11 | import viser
12 | import viser.transforms as tf
13 |
14 | server = viser.ViserServer()
15 | num_frames = 20
16 |
17 |
18 | @server.on_client_connect
19 | def _(client: viser.ClientHandle) -> None:
20 | """For each client that connects, create GUI elements for adjusting the
21 | near/far clipping planes."""
22 |
23 | client.camera.far = 10.0
24 |
25 | near_slider = client.gui.add_slider(
26 | "Near", min=0.01, max=10.0, step=0.001, initial_value=client.camera.near
27 | )
28 | far_slider = client.gui.add_slider(
29 | "Far", min=1, max=20.0, step=0.001, initial_value=client.camera.far
30 | )
31 |
32 | @near_slider.on_update
33 | def _(_) -> None:
34 | client.camera.near = near_slider.value
35 |
36 | @far_slider.on_update
37 | def _(_) -> None:
38 | client.camera.far = far_slider.value
39 |
40 |
41 | @server.on_client_connect
42 | def _(client: viser.ClientHandle) -> None:
43 | """For each client that connects, we create a set of random frames + a click handler for each frame.
44 |
45 | When a frame is clicked, we move the camera to the corresponding frame.
46 | """
47 |
48 | rng = np.random.default_rng(0)
49 |
50 | def make_frame(i: int) -> None:
51 | # Sample a random orientation + position.
52 | wxyz = rng.normal(size=4)
53 | wxyz /= np.linalg.norm(wxyz)
54 | position = rng.uniform(-3.0, 3.0, size=(3,))
55 |
56 | # Create a coordinate frame and label.
57 | frame = client.scene.add_frame(f"/frame_{i}", wxyz=wxyz, position=position)
58 | client.scene.add_label(f"/frame_{i}/label", text=f"Frame {i}")
59 |
60 | # Move the camera when we click a frame.
61 | @frame.on_click
62 | def _(_):
63 | T_world_current = tf.SE3.from_rotation_and_translation(
64 | tf.SO3(client.camera.wxyz), client.camera.position
65 | )
66 | T_world_target = tf.SE3.from_rotation_and_translation(
67 | tf.SO3(frame.wxyz), frame.position
68 | ) @ tf.SE3.from_translation(np.array([0.0, 0.0, -0.5]))
69 |
70 | T_current_target = T_world_current.inverse() @ T_world_target
71 |
72 | for j in range(20):
73 | T_world_set = T_world_current @ tf.SE3.exp(
74 | T_current_target.log() * j / 19.0
75 | )
76 |
77 | # We can atomically set the orientation and the position of the camera
78 | # together to prevent jitter that might happen if one was set before the
79 | # other.
80 | with client.atomic():
81 | client.camera.wxyz = T_world_set.rotation().wxyz
82 | client.camera.position = T_world_set.translation()
83 |
84 | client.flush() # Optional!
85 | time.sleep(1.0 / 60.0)
86 |
87 | # Mouse interactions should orbit around the frame origin.
88 | client.camera.look_at = frame.position
89 |
90 | for i in range(num_frames):
91 | make_frame(i)
92 |
93 |
94 | while True:
95 | time.sleep(1.0)
96 |
--------------------------------------------------------------------------------
/examples/06_mesh.py:
--------------------------------------------------------------------------------
1 | """Meshes
2 |
3 | Visualize a mesh. To get the demo data, see `./assets/download_dragon_mesh.sh`.
4 | """
5 |
6 | import time
7 | from pathlib import Path
8 |
9 | import numpy as np
10 | import trimesh
11 |
12 | import viser
13 | import viser.transforms as tf
14 |
15 | mesh = trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj"))
16 | assert isinstance(mesh, trimesh.Trimesh)
17 | mesh.apply_scale(0.05)
18 |
19 | vertices = mesh.vertices
20 | faces = mesh.faces
21 | print(f"Loaded mesh with {vertices.shape} vertices, {faces.shape} faces")
22 |
23 | server = viser.ViserServer()
24 | server.scene.add_mesh_simple(
25 | name="/simple",
26 | vertices=vertices,
27 | faces=faces,
28 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz,
29 | position=(0.0, 0.0, 0.0),
30 | )
31 | server.scene.add_mesh_trimesh(
32 | name="/trimesh",
33 | mesh=mesh,
34 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz,
35 | position=(0.0, 5.0, 0.0),
36 | )
37 | grid = server.scene.add_grid(
38 | "grid",
39 | width=20.0,
40 | height=20.0,
41 | position=np.array([0.0, 0.0, -2.0]),
42 | )
43 |
44 | while True:
45 | time.sleep(10.0)
46 |
--------------------------------------------------------------------------------
/examples/12_click_meshes.py:
--------------------------------------------------------------------------------
1 | """Mesh click events
2 |
3 | Click on meshes to select them. The index of the last clicked mesh is displayed in the GUI.
4 | """
5 |
6 | import time
7 |
8 | import matplotlib
9 |
10 | import viser
11 |
12 |
13 | def main() -> None:
14 | grid_shape = (4, 5)
15 | server = viser.ViserServer()
16 |
17 | with server.gui.add_folder("Last clicked"):
18 | x_value = server.gui.add_number(
19 | label="x",
20 | initial_value=0,
21 | disabled=True,
22 | hint="x coordinate of the last clicked mesh",
23 | )
24 | y_value = server.gui.add_number(
25 | label="y",
26 | initial_value=0,
27 | disabled=True,
28 | hint="y coordinate of the last clicked mesh",
29 | )
30 |
31 | def add_swappable_mesh(i: int, j: int) -> None:
32 | """Simple callback that swaps between:
33 | - a gray box
34 | - a colored box
35 | - a colored sphere
36 |
37 | Color is chosen based on the position (i, j) of the mesh in the grid.
38 | """
39 |
40 | colormap = matplotlib.colormaps["tab20"]
41 |
42 | def create_mesh(counter: int) -> None:
43 | if counter == 0:
44 | color = (0.8, 0.8, 0.8)
45 | else:
46 | index = (i * grid_shape[1] + j) / (grid_shape[0] * grid_shape[1])
47 | color = colormap(index)[:3]
48 |
49 | if counter in (0, 1):
50 | handle = server.scene.add_box(
51 | name=f"/sphere_{i}_{j}",
52 | position=(i, j, 0.0),
53 | color=color,
54 | dimensions=(0.5, 0.5, 0.5),
55 | )
56 | else:
57 | handle = server.scene.add_icosphere(
58 | name=f"/sphere_{i}_{j}",
59 | radius=0.4,
60 | color=color,
61 | position=(i, j, 0.0),
62 | )
63 |
64 | @handle.on_click
65 | def _(_) -> None:
66 | x_value.value = i
67 | y_value.value = j
68 |
69 | # The new mesh will replace the old one because the names
70 | # /sphere_{i}_{j} are the same.
71 | create_mesh((counter + 1) % 3)
72 |
73 | create_mesh(0)
74 |
75 | for i in range(grid_shape[0]):
76 | for j in range(grid_shape[1]):
77 | add_swappable_mesh(i, j)
78 |
79 | while True:
80 | time.sleep(10.0)
81 |
82 |
83 | if __name__ == "__main__":
84 | main()
85 |
--------------------------------------------------------------------------------
/examples/14_markdown.py:
--------------------------------------------------------------------------------
1 | """Markdown demonstration
2 |
3 | Viser GUI has MDX 2 support.
4 | """
5 |
6 | import time
7 | from pathlib import Path
8 |
9 | import viser
10 |
11 | server = viser.ViserServer()
12 | server.scene.world_axes.visible = True
13 |
14 | markdown_counter = server.gui.add_markdown("Counter: 0")
15 |
16 | here = Path(__file__).absolute().parent
17 |
18 | button = server.gui.add_button("Remove blurb")
19 | checkbox = server.gui.add_checkbox("Visibility", initial_value=True)
20 |
21 | markdown_source = (here / "./assets/mdx_example.mdx").read_text()
22 | markdown_blurb = server.gui.add_markdown(
23 | content=markdown_source,
24 | image_root=here,
25 | )
26 |
27 |
28 | @button.on_click
29 | def _(_):
30 | markdown_blurb.remove()
31 |
32 |
33 | @checkbox.on_update
34 | def _(_):
35 | markdown_blurb.visible = checkbox.value
36 |
37 |
38 | counter = 0
39 | while True:
40 | markdown_counter.content = f"Counter: {counter}"
41 | counter += 1
42 | time.sleep(0.1)
43 |
--------------------------------------------------------------------------------
/examples/16_modal.py:
--------------------------------------------------------------------------------
1 | """Modal basics
2 |
3 | Examples of using modals in Viser."""
4 |
5 | import time
6 |
7 | import viser
8 |
9 |
10 | def main():
11 | server = viser.ViserServer()
12 |
13 | @server.on_client_connect
14 | def _(client: viser.ClientHandle) -> None:
15 | with client.gui.add_modal("Modal example"):
16 | client.gui.add_markdown(
17 | "**The input below determines the title of the modal...**"
18 | )
19 |
20 | gui_title = client.gui.add_text(
21 | "Title",
22 | initial_value="My Modal",
23 | )
24 |
25 | modal_button = client.gui.add_button("Show more modals")
26 |
27 | @modal_button.on_click
28 | def _(_) -> None:
29 | with client.gui.add_modal(gui_title.value) as modal:
30 | client.gui.add_markdown("This is content inside the modal!")
31 | client.gui.add_button("Close").on_click(lambda _: modal.close())
32 |
33 | while True:
34 | time.sleep(0.15)
35 |
36 |
37 | if __name__ == "__main__":
38 | main()
39 |
--------------------------------------------------------------------------------
/examples/17_background_composite.py:
--------------------------------------------------------------------------------
1 | """Depth compositing
2 |
3 | In this example, we show how to use a background image with depth compositing. This can
4 | be useful when we want a 2D image to occlude 3D geometry, such as for NeRF rendering.
5 | """
6 |
7 | import time
8 |
9 | import numpy as np
10 | import trimesh
11 | import trimesh.creation
12 |
13 | import viser
14 |
15 | server = viser.ViserServer()
16 |
17 |
18 | img = np.random.randint(0, 255, size=(1000, 1000, 3), dtype=np.uint8)
19 | depth = np.ones((1000, 1000, 1), dtype=np.float32)
20 |
21 | # Make a square middle portal.
22 | depth[250:750, 250:750, :] = 10.0
23 | img[250:750, 250:750, :] = 255
24 |
25 | mesh = trimesh.creation.box((0.5, 0.5, 0.5))
26 | server.scene.add_mesh_trimesh(
27 | name="/cube",
28 | mesh=mesh,
29 | position=(0, 0, 0.0),
30 | )
31 | server.scene.set_background_image(img, depth=depth)
32 |
33 |
34 | while True:
35 | time.sleep(1.0)
36 |
--------------------------------------------------------------------------------
/examples/18_lines.py:
--------------------------------------------------------------------------------
1 | """Lines
2 |
3 | Make a ball with some random line segments and splines.
4 | """
5 |
6 | import time
7 |
8 | import numpy as np
9 |
10 | import viser
11 |
12 |
13 | def main() -> None:
14 | server = viser.ViserServer()
15 |
16 | # Line segments.
17 | #
18 | # This will be much faster than creating separate scene objects for
19 | # individual line segments or splines.
20 | N = 2000
21 | points = np.random.normal(size=(N, 2, 3)) * 3.0
22 | colors = np.random.randint(0, 255, size=(N, 2, 3))
23 | server.scene.add_line_segments(
24 | "/line_segments",
25 | points=points,
26 | colors=colors,
27 | line_width=3.0,
28 | )
29 |
30 | # Spline helpers.
31 | #
32 | # If many lines are needed, it'll be more efficient to batch them in
33 | # `add_line_segments()`.
34 | for i in range(10):
35 | points = np.random.normal(size=(30, 3)) * 3.0
36 | server.scene.add_spline_catmull_rom(
37 | f"/catmull/{i}",
38 | positions=points,
39 | tension=0.5,
40 | line_width=3.0,
41 | color=np.random.uniform(size=3),
42 | segments=100,
43 | )
44 |
45 | control_points = np.random.normal(size=(30 * 2 - 2, 3)) * 3.0
46 | server.scene.add_spline_cubic_bezier(
47 | f"/cubic_bezier/{i}",
48 | positions=points,
49 | control_points=control_points,
50 | line_width=3.0,
51 | color=np.random.uniform(size=3),
52 | segments=100,
53 | )
54 |
55 | while True:
56 | time.sleep(10.0)
57 |
58 |
59 | if __name__ == "__main__":
60 | main()
61 |
--------------------------------------------------------------------------------
/examples/19_get_renders.py:
--------------------------------------------------------------------------------
1 | """Get renders
2 |
3 | Example for getting renders from a client's viewport to the Python API."""
4 |
5 | import time
6 |
7 | import imageio.v3 as iio
8 | import numpy as np
9 |
10 | import viser
11 |
12 |
13 | def main():
14 | server = viser.ViserServer()
15 |
16 | button = server.gui.add_button("Render a GIF")
17 |
18 | @button.on_click
19 | def _(event: viser.GuiEvent) -> None:
20 | client = event.client
21 | assert client is not None
22 |
23 | client.scene.reset()
24 |
25 | images = []
26 |
27 | for i in range(20):
28 | positions = np.random.normal(size=(30, 3))
29 | client.scene.add_spline_catmull_rom(
30 | f"/catmull_{i}",
31 | positions,
32 | tension=0.5,
33 | line_width=3.0,
34 | color=np.random.uniform(size=3),
35 | )
36 | images.append(client.get_render(height=720, width=1280))
37 | print("Got image with shape", images[-1].shape)
38 |
39 | print("Generating and sending GIF...")
40 | client.send_file_download(
41 | "image.gif", iio.imwrite("", images, extension=".gif", loop=0)
42 | )
43 | print("Done!")
44 |
45 | while True:
46 | time.sleep(10.0)
47 |
48 |
49 | if __name__ == "__main__":
50 | main()
51 |
--------------------------------------------------------------------------------
/examples/21_set_up_direction.py:
--------------------------------------------------------------------------------
1 | """Set up direction
2 |
3 | `.set_up_direction()` can help us set the global up direction."""
4 |
5 | import time
6 |
7 | import viser
8 |
9 |
10 | def main() -> None:
11 | server = viser.ViserServer()
12 | server.scene.world_axes.visible = True
13 | gui_up = server.gui.add_vector3(
14 | "Up Direction",
15 | initial_value=(0.0, 0.0, 1.0),
16 | step=0.01,
17 | )
18 |
19 | @gui_up.on_update
20 | def _(_) -> None:
21 | server.scene.set_up_direction(gui_up.value)
22 |
23 | while True:
24 | time.sleep(1.0)
25 |
26 |
27 | if __name__ == "__main__":
28 | main()
29 |
--------------------------------------------------------------------------------
/examples/23_plotly.py:
--------------------------------------------------------------------------------
1 | """Plotly
2 |
3 | Examples of visualizing plotly plots in Viser."""
4 |
5 | import time
6 |
7 | import numpy as np
8 | import plotly.express as px
9 | import plotly.graph_objects as go
10 | from PIL import Image
11 |
12 | import viser
13 |
14 |
15 | def create_sinusoidal_wave(t: float) -> go.Figure:
16 | """Create a sinusoidal wave plot, starting at time t."""
17 | x_data = np.linspace(t, t + 6 * np.pi, 50)
18 | y_data = np.sin(x_data) * 10
19 |
20 | fig = px.line(
21 | x=list(x_data),
22 | y=list(y_data),
23 | labels={"x": "x", "y": "sin(x)"},
24 | title="Sinusoidal Wave",
25 | )
26 |
27 | # this sets the margins to be tight around the title.
28 | fig.layout.title.automargin = True # type: ignore
29 | fig.update_layout(
30 | margin=dict(l=20, r=20, t=20, b=20),
31 | ) # Reduce plot margins.
32 |
33 | return fig
34 |
35 |
36 | def main() -> None:
37 | server = viser.ViserServer()
38 |
39 | # Plot type 1: Line plot.
40 | line_plot_time = 0.0
41 | line_plot = server.gui.add_plotly(figure=create_sinusoidal_wave(line_plot_time))
42 |
43 | # Plot type 2: Image plot.
44 | fig = px.imshow(Image.open("assets/Cal_logo.png"))
45 | fig.update_layout(
46 | margin=dict(l=20, r=20, t=20, b=20),
47 | )
48 | server.gui.add_plotly(figure=fig, aspect=1.0)
49 |
50 | # Plot type 3: 3D Scatter plot.
51 | fig = px.scatter_3d(
52 | px.data.iris(),
53 | x="sepal_length",
54 | y="sepal_width",
55 | z="petal_width",
56 | color="species",
57 | )
58 | fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01))
59 | fig.update_layout(
60 | margin=dict(l=20, r=20, t=20, b=20),
61 | )
62 | server.gui.add_plotly(figure=fig, aspect=1.0)
63 |
64 | while True:
65 | # Update the line plot.
66 | line_plot_time += 0.1
67 | line_plot.figure = create_sinusoidal_wave(line_plot_time)
68 |
69 | time.sleep(0.01)
70 |
71 |
72 | if __name__ == "__main__":
73 | main()
74 |
--------------------------------------------------------------------------------
/examples/27_notifications.py:
--------------------------------------------------------------------------------
1 | """Notifications
2 |
3 | Examples of adding notifications per client in Viser."""
4 |
5 | import time
6 |
7 | import viser
8 |
9 |
10 | def main() -> None:
11 | server = viser.ViserServer()
12 |
13 | persistent_notif_button = server.gui.add_button(
14 | "Show persistent notification (default)"
15 | )
16 | timed_notif_button = server.gui.add_button("Show timed notification")
17 | controlled_notif_button = server.gui.add_button("Show controlled notification")
18 | loading_notif_button = server.gui.add_button("Show loading notification")
19 |
20 | remove_controlled_notif = server.gui.add_button("Remove controlled notification")
21 |
22 | @persistent_notif_button.on_click
23 | def _(event: viser.GuiEvent) -> None:
24 | """Show persistent notification when the button is clicked."""
25 | client = event.client
26 | assert client is not None
27 |
28 | client.add_notification(
29 | title="Persistent notification",
30 | body="This can be closed manually and does not disappear on its own!",
31 | loading=False,
32 | with_close_button=True,
33 | auto_close=False,
34 | )
35 |
36 | @timed_notif_button.on_click
37 | def _(event: viser.GuiEvent) -> None:
38 | """Show timed notification when the button is clicked."""
39 | client = event.client
40 | assert client is not None
41 |
42 | client.add_notification(
43 | title="Timed notification",
44 | body="This disappears automatically after 5 seconds!",
45 | loading=False,
46 | with_close_button=True,
47 | auto_close=5000,
48 | )
49 |
50 | @controlled_notif_button.on_click
51 | def _(event: viser.GuiEvent) -> None:
52 | """Show controlled notification when the button is clicked."""
53 | client = event.client
54 | assert client is not None
55 |
56 | controlled_notif = client.add_notification(
57 | title="Controlled notification",
58 | body="This cannot be closed by the user and is controlled in code only!",
59 | loading=False,
60 | with_close_button=False,
61 | auto_close=False,
62 | )
63 |
64 | @remove_controlled_notif.on_click
65 | def _(_) -> None:
66 | """Remove controlled notification."""
67 | controlled_notif.remove()
68 |
69 | @loading_notif_button.on_click
70 | def _(event: viser.GuiEvent) -> None:
71 | """Show loading notification when the button is clicked."""
72 | client = event.client
73 | assert client is not None
74 |
75 | loading_notif = client.add_notification(
76 | title="Loading notification",
77 | body="This indicates that some action is in progress! It will be updated in 3 seconds.",
78 | loading=True,
79 | with_close_button=False,
80 | auto_close=False,
81 | )
82 |
83 | time.sleep(3.0)
84 |
85 | loading_notif.title = "Updated notification"
86 | loading_notif.body = "This notification has been updated!"
87 | loading_notif.loading = False
88 | loading_notif.with_close_button = True
89 | loading_notif.auto_close = 5000
90 | loading_notif.color = "green"
91 |
92 | while True:
93 | time.sleep(1.0)
94 |
95 |
96 | if __name__ == "__main__":
97 | main()
98 |
--------------------------------------------------------------------------------
/examples/assets/.gitignore:
--------------------------------------------------------------------------------
1 | dragon.obj
2 | /record3d_dance/
3 | /colmap_garden/
4 |
--------------------------------------------------------------------------------
/examples/assets/Cal_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/examples/assets/Cal_logo.png
--------------------------------------------------------------------------------
/examples/assets/download_colmap_garden.sh:
--------------------------------------------------------------------------------
1 | # This downloads the COLMAP model for the MIP-NeRF garden dataset
2 | # with the images that are downscaled by a factor of 8.
3 | # The full dataset is available at https://jonbarron.info/mipnerf360/.
4 |
5 | set -e -x
6 |
7 | gdown "https://drive.google.com/uc?id=1wYHdrgwXPHtREdCjItvt4gqRQGISMade"
8 |
9 | mkdir -p colmap_garden
10 | # shellcheck disable=SC2035
11 | unzip *.zip && rm *.zip
--------------------------------------------------------------------------------
/examples/assets/download_dragon_mesh.sh:
--------------------------------------------------------------------------------
1 | set -e -x
2 |
3 | gdown "https://drive.google.com/uc?id=1uRDvoS_l2Or8g8YDDPYV79K6_RfFYBeF"
4 |
--------------------------------------------------------------------------------
/examples/assets/download_record3d_dance.sh:
--------------------------------------------------------------------------------
1 | set -e -x
2 |
3 | gdown "https://drive.google.com/uc?id=1_vd5bK_MhtlfisA6BkK1IgiJNfDbIntq"
4 |
5 | mkdir -p record3d_dance
6 | # shellcheck disable=SC2035
7 | unzip *.r3d -d record3d_dance && rm *.r3d
8 |
--------------------------------------------------------------------------------
/examples/assets/mdx_example.mdx:
--------------------------------------------------------------------------------
1 | ## Markdown in Viser
2 |
3 | ---
4 |
5 | Viser has full support for the GFM markdown spec, including **bold**, _italics_,
6 | ~~strikethrough~~, and many other features.
7 |
8 | Here's a [masked link](https://github.com/nerfstudio-project/viser). Not a fan?
9 | Here's a normal one: https://pypi.org/project/viser/
10 |
11 | Anywhere where you can insert GUI elements, you can also insert `images`,
12 | `blockquotes`, `lists`, `tables`, `task lists`, and `(unstyled) code blocks`.
13 |
14 | In inline code blocks, you can show off colors with color chips: `#FED363`
15 | `hsl(0, 0%, 82%)` `rgb(255, 255, 255)`
16 |
17 | Adding images from a remote origin is simple.
18 |
19 | 
20 |
21 | For local images with relative paths, you can either directly use a
22 | [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)
23 | or set the `image_root` argument to the `Path` object that you'd like your paths
24 | relative to. If no such `image_root` is provided, the file system will be scoped
25 | to the directory that Viser is installed in.
26 |
27 | 
28 |
29 | Tables follow the standard markdown spec:
30 |
31 | | Application | Description |
32 | | ---------------------------------------------------- | -------------------------------------------------- |
33 | | [NS](https://nerf.studio) | A collaboration friendly studio for NeRFs |
34 | | [Viser](https://nerfstudio-project.github.io/viser/) | An interactive 3D visualization toolbox for Python |
35 |
36 | Code blocks, while being not nearly as exciting as some of the things presented,
37 | work as expected. Currently, while you can specify a language and metadata in
38 | your blocks, they will remain unused by the Markdown renderer.
39 |
40 | ```python
41 | """Markdown Demonstration
42 |
43 | Viser GUI has MDX 2 support.
44 | """
45 |
46 | import time
47 | from pathlib import Path
48 |
49 | import viser
50 |
51 | server = viser.ViserServer()
52 | server.world_axes.visible = True
53 |
54 |
55 | @server.on_client_connect
56 | def _(client: viser.ClientHandle) -> None:
57 | with open("./assets/mdx_example.mdx", "r") as mkdn:
58 | markdown = client.gui.add_markdown(
59 | markdown=mkdn.read(), image_root=Path(__file__).parent
60 | )
61 |
62 | button = client.gui.add_button("Remove Markdown")
63 |
64 | @button.on_click
65 | def _(_):
66 | markdown.remove()
67 |
68 |
69 | while True:
70 | time.sleep(10.0)
71 | ```
72 |
73 | As a bonus, MDX is extensible and JS capable. This means that you have the
74 | freedom to do things like:
75 |
76 | This page loaded on {(new Date()).toString()}
77 |
78 | Or:
79 |
80 | > Oh yes, mdx PR would be exciting
81 | >
82 | > — Brent Yi
83 |
84 | **Note**: Be careful when playing with JSX, it's very easy to break markdown.
85 |
86 | So that's MDX in Viser. It has support for:
87 |
88 | - [x] CommonMark and GFM standards
89 | - bold, italics, strikethrough, images, blockquotes, tables, task lists, code
90 | blocks, inline code
91 | - [x] Color chips
92 | - [x] JSX enhanced components
93 |
--------------------------------------------------------------------------------
/examples_dev/00_coordinate_frames.py:
--------------------------------------------------------------------------------
1 | """Coordinate frames
2 |
3 | In this basic example, we visualize a set of coordinate frames.
4 |
5 | Naming for all scene nodes are hierarchical; /tree/branch, for example, is defined
6 | relative to /tree.
7 | """
8 |
9 | import random
10 | import time
11 |
12 | import viser
13 |
14 | server = viser.ViserServer()
15 |
16 | while True:
17 | # Add some coordinate frames to the scene. These will be visualized in the viewer.
18 | server.scene.add_frame(
19 | "/tree",
20 | wxyz=(1.0, 0.0, 0.0, 0.0),
21 | position=(random.random() * 2.0, 2.0, 0.2),
22 | )
23 | server.scene.add_frame(
24 | "/tree/branch",
25 | wxyz=(1.0, 0.0, 0.0, 0.0),
26 | position=(random.random() * 2.0, 2.0, 0.2),
27 | )
28 | leaf = server.scene.add_frame(
29 | "/tree/branch/leaf",
30 | wxyz=(1.0, 0.0, 0.0, 0.0),
31 | position=(random.random() * 2.0, 2.0, 0.2),
32 | )
33 | time.sleep(5.0)
34 |
35 | # Remove the leaf node from the scene.
36 | leaf.remove()
37 | time.sleep(0.5)
38 |
--------------------------------------------------------------------------------
/examples_dev/01_image.py:
--------------------------------------------------------------------------------
1 | """Images
2 |
3 | Example for sending images to the viewer.
4 |
5 | We can send backgrond images to display behind the viewer (useful for visualizing
6 | NeRFs), or images to render as 3D textures.
7 | """
8 |
9 | import time
10 | from pathlib import Path
11 |
12 | import imageio.v3 as iio
13 | import numpy as np
14 |
15 | import viser
16 |
17 |
18 | def main() -> None:
19 | server = viser.ViserServer()
20 |
21 | # Add a background image.
22 | server.scene.set_background_image(
23 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"),
24 | format="png",
25 | )
26 |
27 | # Add main image.
28 | server.scene.add_image(
29 | "/img",
30 | iio.imread(Path(__file__).parent / "assets/Cal_logo.png"),
31 | 4.0,
32 | 4.0,
33 | format="png",
34 | wxyz=(1.0, 0.0, 0.0, 0.0),
35 | position=(2.0, 2.0, 0.0),
36 | )
37 | while True:
38 | server.scene.add_image(
39 | "/noise",
40 | np.random.randint(0, 256, size=(400, 400, 3), dtype=np.uint8),
41 | 4.0,
42 | 4.0,
43 | format="jpeg",
44 | wxyz=(1.0, 0.0, 0.0, 0.0),
45 | position=(2.0, 2.0, -1e-2),
46 | )
47 | time.sleep(0.2)
48 |
49 |
50 | if __name__ == "__main__":
51 | main()
52 |
--------------------------------------------------------------------------------
/examples_dev/04_camera_poses.py:
--------------------------------------------------------------------------------
1 | """Camera poses
2 |
3 | Example showing how we can detect new clients and read camera poses from them.
4 | """
5 |
6 | import time
7 |
8 | import viser
9 |
10 | server = viser.ViserServer()
11 | server.scene.world_axes.visible = True
12 |
13 |
14 | @server.on_client_connect
15 | def _(client: viser.ClientHandle) -> None:
16 | print("new client!")
17 |
18 | # This will run whenever we get a new camera!
19 | @client.camera.on_update
20 | def _(_: viser.CameraHandle) -> None:
21 | print(f"New camera on client {client.client_id}!")
22 |
23 | # Show the client ID in the GUI.
24 | gui_info = client.gui.add_text("Client ID", initial_value=str(client.client_id))
25 | gui_info.disabled = True
26 |
27 |
28 | while True:
29 | # Get all currently connected clients.
30 | clients = server.get_clients()
31 | print("Connected client IDs", clients.keys())
32 |
33 | for id, client in clients.items():
34 | print(f"Camera pose for client {id}")
35 | print(f"\twxyz: {client.camera.wxyz}")
36 | print(f"\tposition: {client.camera.position}")
37 | print(f"\tfov: {client.camera.fov}")
38 | print(f"\taspect: {client.camera.aspect}")
39 | print(f"\tlast update: {client.camera.update_timestamp}")
40 | print(
41 | f"\tcanvas size: {client.camera.image_width}x{client.camera.image_height}"
42 | )
43 |
44 | time.sleep(2.0)
45 |
--------------------------------------------------------------------------------
/examples_dev/05_camera_commands.py:
--------------------------------------------------------------------------------
1 | """Camera commands
2 |
3 | In addition to reads, camera parameters also support writes. These are synced to the
4 | corresponding client automatically.
5 | """
6 |
7 | import time
8 |
9 | import numpy as np
10 |
11 | import viser
12 | import viser.transforms as tf
13 |
14 | server = viser.ViserServer()
15 | num_frames = 20
16 |
17 |
18 | @server.on_client_connect
19 | def _(client: viser.ClientHandle) -> None:
20 | """For each client that connects, create GUI elements for adjusting the
21 | near/far clipping planes."""
22 |
23 | client.camera.far = 10.0
24 |
25 | near_slider = client.gui.add_slider(
26 | "Near", min=0.01, max=10.0, step=0.001, initial_value=client.camera.near
27 | )
28 | far_slider = client.gui.add_slider(
29 | "Far", min=1, max=20.0, step=0.001, initial_value=client.camera.far
30 | )
31 |
32 | @near_slider.on_update
33 | def _(_) -> None:
34 | client.camera.near = near_slider.value
35 |
36 | @far_slider.on_update
37 | def _(_) -> None:
38 | client.camera.far = far_slider.value
39 |
40 |
41 | @server.on_client_connect
42 | def _(client: viser.ClientHandle) -> None:
43 | """For each client that connects, we create a set of random frames + a click handler for each frame.
44 |
45 | When a frame is clicked, we move the camera to the corresponding frame.
46 | """
47 |
48 | rng = np.random.default_rng(0)
49 |
50 | def make_frame(i: int) -> None:
51 | # Sample a random orientation + position.
52 | wxyz = rng.normal(size=4)
53 | wxyz /= np.linalg.norm(wxyz)
54 | position = rng.uniform(-3.0, 3.0, size=(3,))
55 |
56 | # Create a coordinate frame and label.
57 | frame = client.scene.add_frame(f"/frame_{i}", wxyz=wxyz, position=position)
58 | client.scene.add_label(f"/frame_{i}/label", text=f"Frame {i}")
59 |
60 | # Move the camera when we click a frame.
61 | @frame.on_click
62 | def _(_):
63 | T_world_current = tf.SE3.from_rotation_and_translation(
64 | tf.SO3(client.camera.wxyz), client.camera.position
65 | )
66 | T_world_target = tf.SE3.from_rotation_and_translation(
67 | tf.SO3(frame.wxyz), frame.position
68 | ) @ tf.SE3.from_translation(np.array([0.0, 0.0, -0.5]))
69 |
70 | T_current_target = T_world_current.inverse() @ T_world_target
71 |
72 | for j in range(20):
73 | T_world_set = T_world_current @ tf.SE3.exp(
74 | T_current_target.log() * j / 19.0
75 | )
76 |
77 | # We can atomically set the orientation and the position of the camera
78 | # together to prevent jitter that might happen if one was set before the
79 | # other.
80 | with client.atomic():
81 | client.camera.wxyz = T_world_set.rotation().wxyz
82 | client.camera.position = T_world_set.translation()
83 |
84 | client.flush() # Optional!
85 | time.sleep(1.0 / 60.0)
86 |
87 | # Mouse interactions should orbit around the frame origin.
88 | client.camera.look_at = frame.position
89 |
90 | for i in range(num_frames):
91 | make_frame(i)
92 |
93 |
94 | while True:
95 | time.sleep(1.0)
96 |
--------------------------------------------------------------------------------
/examples_dev/06_mesh.py:
--------------------------------------------------------------------------------
1 | """Meshes
2 |
3 | Visualize a mesh. To get the demo data, see `./assets/download_dragon_mesh.sh`.
4 | """
5 |
6 | import time
7 | from pathlib import Path
8 |
9 | import numpy as np
10 | import trimesh
11 |
12 | import viser
13 | import viser.transforms as tf
14 |
15 | mesh = trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj"))
16 | assert isinstance(mesh, trimesh.Trimesh)
17 | mesh.apply_scale(0.05)
18 |
19 | vertices = mesh.vertices
20 | faces = mesh.faces
21 | print(f"Loaded mesh with {vertices.shape} vertices, {faces.shape} faces")
22 |
23 | server = viser.ViserServer()
24 | server.scene.add_mesh_simple(
25 | name="/simple",
26 | vertices=vertices,
27 | faces=faces,
28 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz,
29 | position=(0.0, 0.0, 0.0),
30 | )
31 | server.scene.add_mesh_trimesh(
32 | name="/trimesh",
33 | mesh=mesh,
34 | wxyz=tf.SO3.from_x_radians(np.pi / 2).wxyz,
35 | position=(0.0, 5.0, 0.0),
36 | )
37 | grid = server.scene.add_grid(
38 | "grid",
39 | width=20.0,
40 | height=20.0,
41 | position=np.array([0.0, 0.0, -2.0]),
42 | )
43 |
44 | while True:
45 | time.sleep(10.0)
46 |
--------------------------------------------------------------------------------
/examples_dev/12_click_meshes.py:
--------------------------------------------------------------------------------
1 | """Mesh click events
2 |
3 | Click on meshes to select them. The index of the last clicked mesh is displayed in the GUI.
4 | """
5 |
6 | import time
7 |
8 | import matplotlib
9 |
10 | import viser
11 |
12 |
13 | def main() -> None:
14 | grid_shape = (4, 5)
15 | server = viser.ViserServer()
16 |
17 | with server.gui.add_folder("Last clicked"):
18 | x_value = server.gui.add_number(
19 | label="x",
20 | initial_value=0,
21 | disabled=True,
22 | hint="x coordinate of the last clicked mesh",
23 | )
24 | y_value = server.gui.add_number(
25 | label="y",
26 | initial_value=0,
27 | disabled=True,
28 | hint="y coordinate of the last clicked mesh",
29 | )
30 |
31 | def add_swappable_mesh(i: int, j: int) -> None:
32 | """Simple callback that swaps between:
33 | - a gray box
34 | - a colored box
35 | - a colored sphere
36 |
37 | Color is chosen based on the position (i, j) of the mesh in the grid.
38 | """
39 |
40 | colormap = matplotlib.colormaps["tab20"]
41 |
42 | def create_mesh(counter: int) -> None:
43 | if counter == 0:
44 | color = (0.8, 0.8, 0.8)
45 | else:
46 | index = (i * grid_shape[1] + j) / (grid_shape[0] * grid_shape[1])
47 | color = colormap(index)[:3]
48 |
49 | if counter in (0, 1):
50 | handle = server.scene.add_box(
51 | name=f"/sphere_{i}_{j}",
52 | position=(i, j, 0.0),
53 | color=color,
54 | dimensions=(0.5, 0.5, 0.5),
55 | )
56 | else:
57 | handle = server.scene.add_icosphere(
58 | name=f"/sphere_{i}_{j}",
59 | radius=0.4,
60 | color=color,
61 | position=(i, j, 0.0),
62 | )
63 |
64 | @handle.on_click
65 | def _(_) -> None:
66 | x_value.value = i
67 | y_value.value = j
68 |
69 | # The new mesh will replace the old one because the names
70 | # /sphere_{i}_{j} are the same.
71 | create_mesh((counter + 1) % 3)
72 |
73 | create_mesh(0)
74 |
75 | for i in range(grid_shape[0]):
76 | for j in range(grid_shape[1]):
77 | add_swappable_mesh(i, j)
78 |
79 | while True:
80 | time.sleep(10.0)
81 |
82 |
83 | if __name__ == "__main__":
84 | main()
85 |
--------------------------------------------------------------------------------
/examples_dev/14_markdown.py:
--------------------------------------------------------------------------------
1 | """Markdown demonstration
2 |
3 | Viser GUI has MDX 2 support.
4 | """
5 |
6 | import time
7 | from pathlib import Path
8 |
9 | import viser
10 |
11 | server = viser.ViserServer()
12 | server.scene.world_axes.visible = True
13 |
14 | markdown_counter = server.gui.add_markdown("Counter: 0")
15 |
16 | here = Path(__file__).absolute().parent
17 |
18 | button = server.gui.add_button("Remove blurb")
19 | checkbox = server.gui.add_checkbox("Visibility", initial_value=True)
20 |
21 | markdown_source = (here / "./assets/mdx_example.mdx").read_text()
22 | markdown_blurb = server.gui.add_markdown(
23 | content=markdown_source,
24 | image_root=here,
25 | )
26 |
27 |
28 | @button.on_click
29 | def _(_):
30 | markdown_blurb.remove()
31 |
32 |
33 | @checkbox.on_update
34 | def _(_):
35 | markdown_blurb.visible = checkbox.value
36 |
37 |
38 | counter = 0
39 | while True:
40 | markdown_counter.content = f"Counter: {counter}"
41 | counter += 1
42 | time.sleep(0.1)
43 |
--------------------------------------------------------------------------------
/examples_dev/16_modal.py:
--------------------------------------------------------------------------------
1 | """Modal basics
2 |
3 | Examples of using modals in Viser."""
4 |
5 | import time
6 |
7 | import viser
8 |
9 |
10 | def main():
11 | server = viser.ViserServer()
12 |
13 | @server.on_client_connect
14 | def _(client: viser.ClientHandle) -> None:
15 | with client.gui.add_modal("Modal example"):
16 | client.gui.add_markdown(
17 | "**The input below determines the title of the modal...**"
18 | )
19 |
20 | gui_title = client.gui.add_text(
21 | "Title",
22 | initial_value="My Modal",
23 | )
24 |
25 | modal_button = client.gui.add_button("Show more modals")
26 |
27 | @modal_button.on_click
28 | def _(_) -> None:
29 | with client.gui.add_modal(gui_title.value) as modal:
30 | client.gui.add_markdown("This is content inside the modal!")
31 | client.gui.add_button("Close").on_click(lambda _: modal.close())
32 |
33 | while True:
34 | time.sleep(0.15)
35 |
36 |
37 | if __name__ == "__main__":
38 | main()
39 |
--------------------------------------------------------------------------------
/examples_dev/17_background_composite.py:
--------------------------------------------------------------------------------
1 | """Depth compositing
2 |
3 | In this example, we show how to use a background image with depth compositing. This can
4 | be useful when we want a 2D image to occlude 3D geometry, such as for NeRF rendering.
5 | """
6 |
7 | import time
8 |
9 | import numpy as np
10 | import trimesh
11 | import trimesh.creation
12 |
13 | import viser
14 |
15 | server = viser.ViserServer()
16 |
17 |
18 | img = np.random.randint(0, 255, size=(1000, 1000, 3), dtype=np.uint8)
19 | depth = np.ones((1000, 1000, 1), dtype=np.float32)
20 |
21 | # Make a square middle portal.
22 | depth[250:750, 250:750, :] = 10.0
23 | img[250:750, 250:750, :] = 255
24 |
25 | mesh = trimesh.creation.box((0.5, 0.5, 0.5))
26 | server.scene.add_mesh_trimesh(
27 | name="/cube",
28 | mesh=mesh,
29 | position=(0, 0, 0.0),
30 | )
31 | server.scene.set_background_image(img, depth=depth)
32 |
33 |
34 | while True:
35 | time.sleep(1.0)
36 |
--------------------------------------------------------------------------------
/examples_dev/18_lines.py:
--------------------------------------------------------------------------------
1 | """Lines
2 |
3 | Make a ball with some random line segments and splines.
4 | """
5 |
6 | import time
7 |
8 | import numpy as np
9 |
10 | import viser
11 |
12 |
13 | def main() -> None:
14 | server = viser.ViserServer()
15 |
16 | # Line segments.
17 | #
18 | # This will be much faster than creating separate scene objects for
19 | # individual line segments or splines.
20 | N = 2000
21 | points = np.random.normal(size=(N, 2, 3)) * 3.0
22 | colors = np.random.randint(0, 255, size=(N, 2, 3))
23 | server.scene.add_line_segments(
24 | "/line_segments",
25 | points=points,
26 | colors=colors,
27 | line_width=3.0,
28 | )
29 |
30 | # Spline helpers.
31 | #
32 | # If many lines are needed, it'll be more efficient to batch them in
33 | # `add_line_segments()`.
34 | for i in range(10):
35 | points = np.random.normal(size=(30, 3)) * 3.0
36 | server.scene.add_spline_catmull_rom(
37 | f"/catmull/{i}",
38 | positions=points,
39 | tension=0.5,
40 | line_width=3.0,
41 | color=np.random.uniform(size=3),
42 | segments=100,
43 | )
44 |
45 | control_points = np.random.normal(size=(30 * 2 - 2, 3)) * 3.0
46 | server.scene.add_spline_cubic_bezier(
47 | f"/cubic_bezier/{i}",
48 | positions=points,
49 | control_points=control_points,
50 | line_width=3.0,
51 | color=np.random.uniform(size=3),
52 | segments=100,
53 | )
54 |
55 | while True:
56 | time.sleep(10.0)
57 |
58 |
59 | if __name__ == "__main__":
60 | main()
61 |
--------------------------------------------------------------------------------
/examples_dev/19_get_renders.py:
--------------------------------------------------------------------------------
1 | """Get renders
2 |
3 | Example for getting renders from a client's viewport to the Python API."""
4 |
5 | import time
6 |
7 | import imageio.v3 as iio
8 | import numpy as np
9 |
10 | import viser
11 |
12 |
13 | def main():
14 | server = viser.ViserServer()
15 |
16 | button = server.gui.add_button("Render a GIF")
17 |
18 | @button.on_click
19 | def _(event: viser.GuiEvent) -> None:
20 | client = event.client
21 | assert client is not None
22 |
23 | client.scene.reset()
24 |
25 | images = []
26 |
27 | for i in range(20):
28 | positions = np.random.normal(size=(30, 3))
29 | client.scene.add_spline_catmull_rom(
30 | f"/catmull_{i}",
31 | positions,
32 | tension=0.5,
33 | line_width=3.0,
34 | color=np.random.uniform(size=3),
35 | )
36 | images.append(client.get_render(height=720, width=1280))
37 | print("Got image with shape", images[-1].shape)
38 |
39 | print("Generating and sending GIF...")
40 | client.send_file_download(
41 | "image.gif", iio.imwrite("", images, extension=".gif", loop=0)
42 | )
43 | print("Done!")
44 |
45 | while True:
46 | time.sleep(10.0)
47 |
48 |
49 | if __name__ == "__main__":
50 | main()
51 |
--------------------------------------------------------------------------------
/examples_dev/21_set_up_direction.py:
--------------------------------------------------------------------------------
1 | """Set up direction
2 |
3 | `.set_up_direction()` can help us set the global up direction."""
4 |
5 | import time
6 |
7 | import viser
8 |
9 |
10 | def main() -> None:
11 | server = viser.ViserServer()
12 | server.scene.world_axes.visible = True
13 | gui_up = server.gui.add_vector3(
14 | "Up Direction",
15 | initial_value=(0.0, 0.0, 1.0),
16 | step=0.01,
17 | )
18 |
19 | @gui_up.on_update
20 | def _(_) -> None:
21 | server.scene.set_up_direction(gui_up.value)
22 |
23 | while True:
24 | time.sleep(1.0)
25 |
26 |
27 | if __name__ == "__main__":
28 | main()
29 |
--------------------------------------------------------------------------------
/examples_dev/23_plotly.py:
--------------------------------------------------------------------------------
1 | """Plotly
2 |
3 | Examples of visualizing plotly plots in Viser."""
4 |
5 | import time
6 |
7 | import numpy as np
8 | import plotly.express as px
9 | import plotly.graph_objects as go
10 | from PIL import Image
11 |
12 | import viser
13 |
14 |
15 | def create_sinusoidal_wave(t: float) -> go.Figure:
16 | """Create a sinusoidal wave plot, starting at time t."""
17 | x_data = np.linspace(t, t + 6 * np.pi, 50)
18 | y_data = np.sin(x_data) * 10
19 |
20 | fig = px.line(
21 | x=list(x_data),
22 | y=list(y_data),
23 | labels={"x": "x", "y": "sin(x)"},
24 | title="Sinusoidal Wave",
25 | )
26 |
27 | # this sets the margins to be tight around the title.
28 | fig.layout.title.automargin = True # type: ignore
29 | fig.update_layout(
30 | margin=dict(l=20, r=20, t=20, b=20),
31 | ) # Reduce plot margins.
32 |
33 | return fig
34 |
35 |
36 | def main() -> None:
37 | server = viser.ViserServer()
38 |
39 | # Plot type 1: Line plot.
40 | line_plot_time = 0.0
41 | line_plot = server.gui.add_plotly(figure=create_sinusoidal_wave(line_plot_time))
42 |
43 | # Plot type 2: Image plot.
44 | fig = px.imshow(Image.open("assets/Cal_logo.png"))
45 | fig.update_layout(
46 | margin=dict(l=20, r=20, t=20, b=20),
47 | )
48 | server.gui.add_plotly(figure=fig, aspect=1.0)
49 |
50 | # Plot type 3: 3D Scatter plot.
51 | fig = px.scatter_3d(
52 | px.data.iris(),
53 | x="sepal_length",
54 | y="sepal_width",
55 | z="petal_width",
56 | color="species",
57 | )
58 | fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01))
59 | fig.update_layout(
60 | margin=dict(l=20, r=20, t=20, b=20),
61 | )
62 | server.gui.add_plotly(figure=fig, aspect=1.0)
63 |
64 | while True:
65 | # Update the line plot.
66 | line_plot_time += 0.1
67 | line_plot.figure = create_sinusoidal_wave(line_plot_time)
68 |
69 | time.sleep(0.01)
70 |
71 |
72 | if __name__ == "__main__":
73 | main()
74 |
--------------------------------------------------------------------------------
/examples_dev/27_notifications.py:
--------------------------------------------------------------------------------
1 | """Notifications
2 |
3 | Examples of adding notifications per client in Viser."""
4 |
5 | import time
6 |
7 | import viser
8 |
9 |
10 | def main() -> None:
11 | server = viser.ViserServer()
12 |
13 | persistent_notif_button = server.gui.add_button(
14 | "Show persistent notification (default)"
15 | )
16 | timed_notif_button = server.gui.add_button("Show timed notification")
17 | controlled_notif_button = server.gui.add_button("Show controlled notification")
18 | loading_notif_button = server.gui.add_button("Show loading notification")
19 |
20 | remove_controlled_notif = server.gui.add_button("Remove controlled notification")
21 |
22 | @persistent_notif_button.on_click
23 | def _(event: viser.GuiEvent) -> None:
24 | """Show persistent notification when the button is clicked."""
25 | client = event.client
26 | assert client is not None
27 |
28 | client.add_notification(
29 | title="Persistent notification",
30 | body="This can be closed manually and does not disappear on its own!",
31 | loading=False,
32 | with_close_button=True,
33 | auto_close=False,
34 | )
35 |
36 | @timed_notif_button.on_click
37 | def _(event: viser.GuiEvent) -> None:
38 | """Show timed notification when the button is clicked."""
39 | client = event.client
40 | assert client is not None
41 |
42 | client.add_notification(
43 | title="Timed notification",
44 | body="This disappears automatically after 5 seconds!",
45 | loading=False,
46 | with_close_button=True,
47 | auto_close=5000,
48 | )
49 |
50 | @controlled_notif_button.on_click
51 | def _(event: viser.GuiEvent) -> None:
52 | """Show controlled notification when the button is clicked."""
53 | client = event.client
54 | assert client is not None
55 |
56 | controlled_notif = client.add_notification(
57 | title="Controlled notification",
58 | body="This cannot be closed by the user and is controlled in code only!",
59 | loading=False,
60 | with_close_button=False,
61 | auto_close=False,
62 | )
63 |
64 | @remove_controlled_notif.on_click
65 | def _(_) -> None:
66 | """Remove controlled notification."""
67 | controlled_notif.remove()
68 |
69 | @loading_notif_button.on_click
70 | def _(event: viser.GuiEvent) -> None:
71 | """Show loading notification when the button is clicked."""
72 | client = event.client
73 | assert client is not None
74 |
75 | loading_notif = client.add_notification(
76 | title="Loading notification",
77 | body="This indicates that some action is in progress! It will be updated in 3 seconds.",
78 | loading=True,
79 | with_close_button=False,
80 | auto_close=False,
81 | )
82 |
83 | time.sleep(3.0)
84 |
85 | loading_notif.title = "Updated notification"
86 | loading_notif.body = "This notification has been updated!"
87 | loading_notif.loading = False
88 | loading_notif.with_close_button = True
89 | loading_notif.auto_close = 5000
90 | loading_notif.color = "green"
91 |
92 | while True:
93 | time.sleep(1.0)
94 |
95 |
96 | if __name__ == "__main__":
97 | main()
98 |
--------------------------------------------------------------------------------
/examples_dev/README.md:
--------------------------------------------------------------------------------
1 | # Examples (development)
2 |
3 | This is the development version of examples for `viser`. This should be compatible with:
4 |
5 | - The latest development version of `viser` on GitHub.
6 |
7 | But not necessarily:
8 |
9 | - The latest release of `viser` on PyPI.
10 |
--------------------------------------------------------------------------------
/src/viser/_icons.py:
--------------------------------------------------------------------------------
1 | import zipfile
2 | from functools import lru_cache
3 | from pathlib import Path
4 |
5 | from ._icons_enum import IconName
6 |
7 | ICONS_DIR = Path(__file__).absolute().parent / "_icons"
8 |
9 |
10 | @lru_cache(maxsize=32)
11 | def svg_from_icon(icon_name: IconName) -> str:
12 | """Read an icon and return it as a UTF string; we expect this to be an
13 | tag."""
14 | assert isinstance(icon_name, str)
15 | icons_zipfile = ICONS_DIR / "tabler-icons.zip"
16 |
17 | with zipfile.ZipFile(icons_zipfile) as zip_file:
18 | with zip_file.open(f"{icon_name}.svg") as icon_file:
19 | out = icon_file.read()
20 |
21 | return out.decode("utf-8")
22 |
--------------------------------------------------------------------------------
/src/viser/_icons/tabler-icons.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/_icons/tabler-icons.zip
--------------------------------------------------------------------------------
/src/viser/_notification_handle.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import dataclasses
4 | from typing import Any, Literal
5 |
6 | from typing_extensions import override
7 |
8 | from viser._assignable_props_api import AssignablePropsBase
9 |
10 | from ._messages import NotificationMessage, NotificationProps, RemoveNotificationMessage
11 | from .infra._infra import WebsockClientConnection
12 |
13 |
14 | @dataclasses.dataclass
15 | class _NotificationHandleState:
16 | websock_interface: WebsockClientConnection
17 | uuid: str
18 | props: NotificationProps
19 |
20 |
21 | class NotificationHandle(
22 | NotificationProps, AssignablePropsBase[_NotificationHandleState]
23 | ):
24 | """Handle for a notification in our visualizer."""
25 |
26 | def __init__(self, impl: _NotificationHandleState) -> None:
27 | self._impl = impl
28 |
29 | @override
30 | def _queue_update(self, name: str, value: Any) -> None:
31 | """Queue an update message with the property change."""
32 | # For notifications, we'll just send the whole props object when a
33 | # property is reassigned. Deduplication in the message buffer will
34 | # debounce this when multiple properties are updated in succession.
35 | del name, value
36 | self._sync_with_client("update")
37 |
38 | def _sync_with_client(self, mode: Literal["show", "update"]) -> None:
39 | msg = NotificationMessage(mode, self._impl.uuid, self._impl.props)
40 | self._impl.websock_interface.queue_message(msg)
41 |
42 | def remove(self) -> None:
43 | self._impl.websock_interface.get_message_buffer().remove_from_buffer(
44 | # Don't send outdated GUI updates to new clients.
45 | # This is brittle...
46 | lambda message: getattr(message, "uuid", None) == self._impl.uuid
47 | )
48 | msg = RemoveNotificationMessage(self._impl.uuid)
49 | self._impl.websock_interface.queue_message(msg)
50 |
--------------------------------------------------------------------------------
/src/viser/_threadpool_exceptions.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | import traceback
5 | from concurrent.futures import Future
6 | from typing import Any
7 |
8 |
9 | def print_threadpool_errors(future: Future[Any]) -> None:
10 | """Print errors from a Future in a ThreadPool, should be used with
11 | `add_done_callback`."""
12 | if future.cancelled():
13 | print("Task was cancelled", file=sys.stderr)
14 | return
15 |
16 | exc = future.exception()
17 | if exc is not None:
18 | print("Task failed with exception:", file=sys.stderr)
19 | traceback.print_exception(type(exc), exc, exc.__traceback__)
20 |
--------------------------------------------------------------------------------
/src/viser/client/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | settings: {
3 | react: {
4 | version: "detect", // React version. "detect" automatically picks the version you have installed.
5 | // You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
6 | // It will default to "latest" and warn if missing, and to "detect" in the future
7 | },
8 | },
9 | env: {
10 | browser: true,
11 | es2021: true,
12 | },
13 | extends: [
14 | "eslint:recommended",
15 | "plugin:react/recommended",
16 | "plugin:@typescript-eslint/recommended",
17 | ],
18 | overrides: [],
19 | parser: "@typescript-eslint/parser",
20 | parserOptions: {
21 | ecmaVersion: "latest",
22 | sourceType: "module",
23 | },
24 | plugins: ["react", "@typescript-eslint", "react-refresh"],
25 | ignorePatterns: ["build/", ".eslintrc.js", "src/csm"],
26 | rules: {
27 | // https://github.com/jsx-eslint/eslint-plugin-react/issues/3423
28 | "react/no-unknown-property": "off",
29 | "no-constant-condition": "off",
30 | // Suppress errors for missing 'import React' in files.
31 | "react/react-in-jsx-scope": "off",
32 | "@typescript-eslint/ban-ts-comment": "off",
33 | "@typescript-eslint/no-explicit-any": "off",
34 | "@typescript-eslint/no-non-null-assertion": "off",
35 | "react/prop-types": [
36 | "error",
37 | {
38 | skipUndeclared: true,
39 | },
40 | ],
41 | "react-refresh/only-export-components": "warn",
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/viser/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/src/viser/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 | Viser
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/viser/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "viser",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@mantine/core": "^8.0.0",
7 | "@mantine/form": "^8.0.0",
8 | "@mantine/hooks": "^8.0.0",
9 | "@mantine/notifications": "^8.0.0",
10 | "@mantine/vanilla-extract": "^8.0.0",
11 | "@mdx-js/mdx": "^3.0.1",
12 | "@mdx-js/react": "^3.0.1",
13 | "@msgpack/msgpack": "^3.0.0-beta2",
14 | "@react-three/drei": "^10.0.5",
15 | "@react-three/fiber": "9.1.0",
16 | "@tabler/icons-react": "^3.1.0",
17 | "@three.ez/instanced-mesh": "0.3.4",
18 | "@types/node": "^20.11.30",
19 | "@types/react": "^18.0.33",
20 | "@types/react-dom": "^18.0.11",
21 | "@types/three": "^0.174.0",
22 | "@vanilla-extract/css": "^1.14.1",
23 | "@vitejs/plugin-react": "^4.0.1",
24 | "acorn": "^8.0.0",
25 | "await-lock": "^2.2.2",
26 | "browserslist": "^4.24.4",
27 | "colortranslator": "^4.1.0",
28 | "detect-browser": "^5.3.0",
29 | "fflate": "^0.8.2",
30 | "hold-event": "^1.1.0",
31 | "immer": "^10.0.4",
32 | "its-fine": "^2.0.0",
33 | "postcss": "^8.4.38",
34 | "react": "^19.0.0",
35 | "react-dom": "^19.0.0",
36 | "react-error-boundary": "^4.0.10",
37 | "react-intersection-observer": "^9.13.1",
38 | "react-qr-code": "^2.0.12",
39 | "rehype-color-chips": "^0.1.3",
40 | "remark-gfm": "^4.0.0",
41 | "three": "^0.174.0",
42 | "uuid": "^11.0.5",
43 | "vite": "^5.2.6",
44 | "vite-plugin-svgr": "^4.2.0",
45 | "vite-tsconfig-paths": "^4.2.0",
46 | "zustand": "^4.3.7"
47 | },
48 | "scripts": {
49 | "start": "vite --host",
50 | "build": "tsc && vite build",
51 | "serve": "vite preview"
52 | },
53 | "eslintConfig": {
54 | "extends": [
55 | "react-app"
56 | ]
57 | },
58 | "browserslist": {
59 | "production": [
60 | "last 2 chrome versions",
61 | "last 2 firefox versions",
62 | "last 2 safari versions"
63 | ],
64 | "development": [
65 | "last 1 chrome version",
66 | "last 1 firefox version",
67 | "last 1 safari version"
68 | ]
69 | },
70 | "devDependencies": {
71 | "@types/uuid": "^9.0.8",
72 | "@types/wicg-file-system-access": "^2023.10.5",
73 | "@typescript-eslint/eslint-plugin": "^7.4.0",
74 | "@typescript-eslint/parser": "^7.4.0",
75 | "@vanilla-extract/vite-plugin": "^4.0.6",
76 | "browserslist-to-esbuild": "^2.1.1",
77 | "eslint": "^8.43.0",
78 | "eslint-plugin-react": "^7.32.2",
79 | "eslint-plugin-react-refresh": "^0.4.1",
80 | "typescript": "^5.0.4",
81 | "vite-plugin-eslint": "^1.8.1"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/viser/client/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {},
3 | };
4 |
--------------------------------------------------------------------------------
/src/viser/client/public/Inter-VariableFont_slnt,wght.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/Inter-VariableFont_slnt,wght.ttf
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/dikhololo_night_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/dikhololo_night_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/empty_warehouse_01_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/empty_warehouse_01_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/forest_slope_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/forest_slope_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/kiara_1_dawn_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/kiara_1_dawn_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/lebombo_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/lebombo_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/potsdamer_platz_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/potsdamer_platz_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/rooitou_park_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/rooitou_park_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/st_fagans_interior_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/st_fagans_interior_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/studio_small_03_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/studio_small_03_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/hdri/venice_sunset_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/public/hdri/venice_sunset_1k.hdr
--------------------------------------------------------------------------------
/src/viser/client/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/viser/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Viser",
3 | "name": "Viser",
4 | "icons": [
5 | {
6 | "src": "favicon.svg",
7 | "sizes": "any",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/viser/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/viser/client/src/App.css.ts:
--------------------------------------------------------------------------------
1 | import { globalStyle } from "@vanilla-extract/css";
2 |
3 | globalStyle(".mantine-ScrollArea-scrollbar", {
4 | zIndex: 100,
5 | });
6 |
7 | globalStyle("html", {
8 | fontSize: "92.5%",
9 | "@media": {
10 | "(max-width: 767px)": {
11 | fontSize: "83%",
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/src/viser/client/src/AppTheme.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Checkbox,
3 | ColorInput,
4 | Select,
5 | TextInput,
6 | NumberInput,
7 | Paper,
8 | ActionIcon,
9 | Button,
10 | createTheme,
11 | Textarea,
12 | } from "@mantine/core";
13 | import { themeToVars } from "@mantine/vanilla-extract";
14 |
15 | export const theme = createTheme({
16 | fontFamily: "Inter",
17 | autoContrast: true,
18 | components: {
19 | Checkbox: Checkbox.extend({
20 | defaultProps: {
21 | radius: "xs",
22 | },
23 | }),
24 | ColorInput: ColorInput.extend({
25 | defaultProps: {
26 | radius: "xs",
27 | },
28 | }),
29 | Select: Select.extend({
30 | defaultProps: {
31 | radius: "sm",
32 | },
33 | }),
34 | Textarea: Textarea.extend({
35 | defaultProps: {
36 | radius: "xs",
37 | },
38 | }),
39 | TextInput: TextInput.extend({
40 | defaultProps: {
41 | radius: "xs",
42 | },
43 | }),
44 | NumberInput: NumberInput.extend({
45 | defaultProps: {
46 | radius: "xs",
47 | },
48 | }),
49 | Paper: Paper.extend({
50 | defaultProps: {
51 | radius: "xs",
52 | shadow: "0",
53 | },
54 | }),
55 | ActionIcon: ActionIcon.extend({
56 | defaultProps: {
57 | variant: "subtle",
58 | color: "gray",
59 | radius: "xs",
60 | },
61 | }),
62 | Button: Button.extend({
63 | defaultProps: {
64 | radius: "xs",
65 | styles: {
66 | label: {
67 | fontWeight: 450,
68 | },
69 | },
70 | },
71 | }),
72 | },
73 | });
74 |
75 | export const vars = themeToVars(theme);
76 |
--------------------------------------------------------------------------------
/src/viser/client/src/BrowserWarning.tsx:
--------------------------------------------------------------------------------
1 | import { notifications } from "@mantine/notifications";
2 | import { detect } from "detect-browser";
3 | import { useEffect } from "react";
4 |
5 | export function BrowserWarning() {
6 | useEffect(() => {
7 | const browser = detect();
8 |
9 | // Browser version are based loosely on support for SIMD, OffscreenCanvas.
10 | //
11 | // https://caniuse.com/?search=simd
12 | // https://caniuse.com/?search=OffscreenCanvas
13 | if (browser === null || browser.version === null) {
14 | console.log("Failed to detect browser");
15 | notifications.show({
16 | title: "Could not detect browser version",
17 | message:
18 | "Your browser version could not be detected. It may not be supported.",
19 | autoClose: false,
20 | color: "red",
21 | });
22 | } else {
23 | const version = parseFloat(browser.version);
24 | console.log(`Detected ${browser.name} version ${version}`);
25 | if (
26 | (browser.name === "chrome" && version < 91) ||
27 | (browser.name === "edge" && version < 91) ||
28 | (browser.name === "firefox" && version < 89) ||
29 | (browser.name === "opera" && version < 77) ||
30 | (browser.name === "safari" && version < 16.4)
31 | )
32 | notifications.show({
33 | title: "Unsuppported browser",
34 | message: `Your browser (${
35 | browser.name.slice(0, 1).toUpperCase() + browser.name.slice(1)
36 | }/${
37 | browser.version
38 | }) is outdated, which may cause problems. Consider updating.`,
39 | autoClose: false,
40 | color: "red",
41 | });
42 | }
43 | });
44 | return null;
45 | }
46 |
--------------------------------------------------------------------------------
/src/viser/client/src/ControlPanel/GuiComponentContext.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Messages from "../WebsocketMessages";
3 |
4 | interface GuiComponentContext {
5 | folderDepth: number;
6 | setValue: (id: string, value: NonNullable) => void;
7 | messageSender: (message: Messages.Message) => void;
8 | GuiContainer: React.FC<{ containerUuid: string }>;
9 | }
10 |
11 | export const GuiComponentContext = React.createContext({
12 | folderDepth: 0,
13 | setValue: () => undefined,
14 | messageSender: () => undefined,
15 | GuiContainer: () => {
16 | throw new Error("GuiComponentContext not initialized");
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/viser/client/src/ControlPanel/SceneTreeTable.css.ts:
--------------------------------------------------------------------------------
1 | import { globalStyle, style } from "@vanilla-extract/css";
2 | import { vars } from "../AppTheme";
3 |
4 | export const tableWrapper = style({
5 | borderRadius: vars.radius.xs,
6 | padding: "0.1em 0",
7 | overflowX: "auto",
8 | display: "flex",
9 | flexDirection: "column",
10 | gap: "0",
11 | });
12 |
13 | export const propsWrapper = style({
14 | position: "relative",
15 | borderRadius: vars.radius.xs,
16 | border: "1px solid",
17 | borderColor: vars.colors.defaultBorder,
18 | padding: vars.spacing.xs,
19 | boxSizing: "border-box",
20 | margin: vars.spacing.xs,
21 | marginTop: "0.2em",
22 | marginBottom: "0.2em",
23 | overflowX: "auto",
24 | display: "flex",
25 | flexDirection: "column",
26 | gap: vars.spacing.xs,
27 | });
28 |
29 | export const editIconWrapper = style({
30 | opacity: "0",
31 | });
32 |
33 | export const tableRow = style({
34 | display: "flex",
35 | alignItems: "center",
36 | gap: "0.2em",
37 | padding: "0 0.25em",
38 | lineHeight: "2em",
39 | fontSize: "0.875em",
40 | ":hover": {
41 | [vars.lightSelector]: {
42 | backgroundColor: vars.colors.gray[1],
43 | },
44 | [vars.darkSelector]: {
45 | backgroundColor: vars.colors.dark[6],
46 | },
47 | },
48 | });
49 |
50 | export const tableHierarchyLine = style({
51 | [vars.lightSelector]: {
52 | borderColor: vars.colors.gray[2],
53 | },
54 | [vars.darkSelector]: {
55 | borderColor: vars.colors.dark[5],
56 | },
57 | borderLeft: "0.3em solid",
58 | width: "0.2em",
59 | marginLeft: "0.375em",
60 | height: "2em",
61 | });
62 |
63 | globalStyle(`${tableRow}:hover ${tableHierarchyLine}`, {
64 | [vars.lightSelector]: {
65 | borderColor: vars.colors.gray[3],
66 | },
67 | [vars.darkSelector]: {
68 | borderColor: vars.colors.dark[4],
69 | },
70 | });
71 |
72 | globalStyle(`${tableRow}:hover ${editIconWrapper}`, {
73 | opacity: "1.0",
74 | });
75 |
--------------------------------------------------------------------------------
/src/viser/client/src/HoverContext.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // Extended hover context to include instanceId for instanced meshes and clickable state
4 | export interface HoverState {
5 | isHovered: boolean;
6 | instanceId: number | null;
7 | clickable: boolean;
8 | }
9 |
10 | export const HoverableContext =
11 | React.createContext | null>(null);
12 |
--------------------------------------------------------------------------------
/src/viser/client/src/MacWindowWrapper.tsx:
--------------------------------------------------------------------------------
1 | export function MacWindowWrapper({
2 | children,
3 | title,
4 | width,
5 | height,
6 | }: {
7 | children: React.ReactNode;
8 | title: string;
9 | width: number;
10 | height: number;
11 | }) {
12 | const TITLEBAR_HEIGHT = 36; // px
13 |
14 | return (
15 |
30 | {/* MacOS titlebar */}
31 |
42 | {/* Traffic light buttons */}
43 |
51 |
59 |
67 | {/* Window title */}
68 |
80 | {title}
81 |
82 |
83 | {/* Content */}
84 |
85 | {children}
86 |
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/src/viser/client/src/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { ViewerContext } from "./ViewerContext";
2 | import { GuiModalMessage } from "./WebsocketMessages";
3 | import GeneratedGuiContainer from "./ControlPanel/Generated";
4 | import { Modal } from "@mantine/core";
5 | import { useContext } from "react";
6 |
7 | export function ViserModal() {
8 | const viewer = useContext(ViewerContext)!;
9 |
10 | const modalList = viewer.useGui((state) => state.modals);
11 | const modals = modalList.map((conf, index) => {
12 | return ;
13 | });
14 |
15 | return modals;
16 | }
17 |
18 | function GeneratedModal({
19 | conf,
20 | index,
21 | }: {
22 | conf: GuiModalMessage;
23 | index: number;
24 | }) {
25 | return (
26 | {
30 | // To make memory management easier, we should only close modals from
31 | // the server.
32 | // Otherwise, the client would need to communicate to the server that
33 | // the modal was deleted and contained GUI elements were cleared.
34 | }}
35 | withCloseButton={false}
36 | centered
37 | zIndex={100 + index}
38 | >
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/viser/client/src/OutlinesIfHovered.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useFrame } from "@react-three/fiber";
3 | import { HoverableContext } from "./HoverContext";
4 | import { Outlines } from "./Outlines";
5 | import * as THREE from "three";
6 |
7 | /** Outlines object, which should be placed as a child of all meshes that might
8 | * be clickable. */
9 | export function OutlinesIfHovered(
10 | props: { alwaysMounted?: boolean; creaseAngle?: number } = {
11 | // Can be set to true for objects like meshes which may be slow to mount.
12 | // It seems better to set to False for instanced meshes, there may be some
13 | // drei or fiber-related race conditions...
14 | alwaysMounted: false,
15 | // Some thing just look better with no creasing, like camera frustum objects.
16 | creaseAngle: Math.PI,
17 | },
18 | ) {
19 | const groupRef = React.useRef(null);
20 | const hoveredRef = React.useContext(HoverableContext);
21 | const [mounted, setMounted] = React.useState(true);
22 |
23 | useFrame(() => {
24 | if (hoveredRef === null) return;
25 | if (props.alwaysMounted) {
26 | if (groupRef.current === null) return;
27 | groupRef.current.visible = hoveredRef.current.isHovered;
28 | } else if (hoveredRef.current.isHovered != mounted) {
29 | setMounted(hoveredRef.current.isHovered);
30 | }
31 | });
32 | return hoveredRef === null || !mounted ? null : (
33 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/viser/client/src/SearchParamsUtils.tsx:
--------------------------------------------------------------------------------
1 | /** Utilities for interacting with the URL search parameters.
2 | *
3 | * This lets us specify the websocket server + port from the URL. */
4 |
5 | export const searchParamKey = "websocket";
6 |
7 | export function syncSearchParamServer(server: string) {
8 | const searchParams = new URLSearchParams(window.location.search);
9 | // No need to update the URL bar if the websocket port matches the HTTP port.
10 | // So if we navigate to http://localhost:8081, this should by default connect to ws://localhost:8081.
11 | const isDefaultServer =
12 | window.location.host.includes(
13 | server.replace("ws://", "").replace("/", ""),
14 | ) ||
15 | window.location.host.includes(
16 | server.replace("wss://", "").replace("/", ""),
17 | );
18 | if (isDefaultServer && searchParams.has(searchParamKey)) {
19 | searchParams.delete(searchParamKey);
20 | } else if (!isDefaultServer) {
21 | searchParams.set(searchParamKey, server);
22 | }
23 | window.history.replaceState(
24 | null,
25 | "Viser",
26 | // We could use URLSearchParams.toString() to build this string, but that
27 | // would escape it. We're going to just not escape the string. :)
28 | searchParams.size === 0
29 | ? window.location.href.split("?")[0]
30 | : "?" +
31 | Array.from(searchParams.entries())
32 | .map(([k, v]) => `${k}=${v}`)
33 | .join("&"),
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/viser/client/src/ShadowArgs.tsx:
--------------------------------------------------------------------------------
1 | export const shadowArgs = {
2 | "shadow-camera-near": 0.001,
3 | "shadow-camera-far": 500.0,
4 | "shadow-bias": -0.00001,
5 | "shadow-mapSize-width": 1024,
6 | "shadow-mapSize-height": 1024,
7 | "shadow-radius": 4.0,
8 | };
9 |
--------------------------------------------------------------------------------
/src/viser/client/src/Splatting/SplatSortWorker.ts:
--------------------------------------------------------------------------------
1 | /** Worker for sorting splats.
2 | */
3 |
4 | import MakeSorterModulePromise from "./WasmSorter/Sorter.mjs";
5 |
6 | export type SorterWorkerIncoming =
7 | | {
8 | setBuffer: Uint32Array;
9 | setGroupIndices: Uint32Array;
10 | }
11 | | {
12 | setTz_camera_groups: Float32Array;
13 | }
14 | | { close: true };
15 |
16 | {
17 | let sorter: any = null;
18 | let Tz_camera_groups: Float32Array | null = null;
19 | let sortRunning = false;
20 | const throttledSort = () => {
21 | if (sorter === null || Tz_camera_groups === null) {
22 | setTimeout(throttledSort, 1);
23 | return;
24 | }
25 | if (sortRunning) return;
26 |
27 | sortRunning = true;
28 | const lastView = Tz_camera_groups;
29 |
30 | // Important: we clone the output so we can transfer the buffer to the main
31 | // thread. Compared to relying on postMessage for copying, this reduces
32 | // backlog artifacts.
33 | const sortedIndices = (
34 | sorter.sort(Tz_camera_groups) as Uint32Array
35 | ).slice();
36 |
37 | // @ts-ignore
38 | self.postMessage({ sortedIndices: sortedIndices }, [sortedIndices.buffer]);
39 |
40 | setTimeout(() => {
41 | sortRunning = false;
42 | if (Tz_camera_groups === null) return;
43 | if (
44 | !lastView.every(
45 | // Cast is needed because of closure...
46 | (val, i) => val === (Tz_camera_groups as Float32Array)[i],
47 | )
48 | ) {
49 | throttledSort();
50 | }
51 | }, 0);
52 | };
53 |
54 | const SorterModulePromise = MakeSorterModulePromise();
55 |
56 | self.onmessage = async (e) => {
57 | const data = e.data as SorterWorkerIncoming;
58 | if ("setBuffer" in data) {
59 | // Instantiate sorter with buffers populated.
60 | sorter = new (await SorterModulePromise).Sorter(
61 | data.setBuffer,
62 | data.setGroupIndices,
63 | );
64 | } else if ("setTz_camera_groups" in data) {
65 | // Update object transforms.
66 | Tz_camera_groups = data.setTz_camera_groups;
67 | throttledSort();
68 | } else if ("close" in data) {
69 | // Done!
70 | self.close();
71 | }
72 | };
73 | }
74 |
--------------------------------------------------------------------------------
/src/viser/client/src/Splatting/WasmSorter/Sorter.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/client/src/Splatting/WasmSorter/Sorter.wasm
--------------------------------------------------------------------------------
/src/viser/client/src/Splatting/WasmSorter/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | emcc --bind -O3 sorter.cpp -o Sorter.mjs -s WASM=1 -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['addOnPostRun']" -s ALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=1GB -s STACK_SIZE=2097152 -msimd128;
4 |
--------------------------------------------------------------------------------
/src/viser/client/src/VersionInfo.ts:
--------------------------------------------------------------------------------
1 | // Automatically generated file - do not edit manually.
2 | // This is synchronized with the Python package version in viser/__init__.py.
3 | export const VISER_VERSION = "0.2.23";
4 |
--------------------------------------------------------------------------------
/src/viser/client/src/ViewerContext.ts:
--------------------------------------------------------------------------------
1 | import "@mantine/core/styles.css";
2 | import "@mantine/notifications/styles.css";
3 | import "./App.css";
4 | import "./index.css";
5 |
6 | import { CameraControls } from "@react-three/drei";
7 | import * as THREE from "three";
8 | import React from "react";
9 | import { UseSceneTree } from "./SceneTree";
10 |
11 | import { UseGui } from "./ControlPanel/GuiState";
12 | import { GetRenderRequestMessage, Message } from "./WebsocketMessages";
13 |
14 | // Type definitions for all mutable state.
15 | export type ViewerMutable = {
16 | // Function references.
17 | sendMessage: (message: Message) => void;
18 | sendCamera: (() => void) | null;
19 | resetCameraView: (() => void) | null;
20 |
21 | // DOM/Three.js references.
22 | canvas: HTMLCanvasElement | null;
23 | canvas2d: HTMLCanvasElement | null;
24 | scene: THREE.Scene | null;
25 | camera: THREE.PerspectiveCamera | null;
26 | backgroundMaterial: THREE.ShaderMaterial | null;
27 | cameraControl: CameraControls | null;
28 |
29 | // Scene management.
30 | nodeAttributesFromName: {
31 | [name: string]:
32 | | undefined
33 | | {
34 | poseUpdateState?: "updated" | "needsUpdate" | "waitForMakeObject";
35 | wxyz?: [number, number, number, number];
36 | position?: [number, number, number];
37 | visibility?: boolean; // Visibility state from the server.
38 | overrideVisibility?: boolean; // Override from the GUI.
39 | };
40 | };
41 | nodeRefFromName: {
42 | [name: string]: undefined | THREE.Object3D;
43 | };
44 |
45 | // Message and rendering state.
46 | messageQueue: Message[];
47 | getRenderRequestState: "ready" | "triggered" | "pause" | "in_progress";
48 | getRenderRequest: null | GetRenderRequestMessage;
49 |
50 | // Interaction state.
51 | scenePointerInfo: {
52 | enabled: false | "click" | "rect-select"; // Enable box events.
53 | dragStart: [number, number]; // First mouse position.
54 | dragEnd: [number, number]; // Final mouse position.
55 | isDragging: boolean;
56 | };
57 |
58 | // Skinned mesh state.
59 | skinnedMeshState: {
60 | [name: string]: {
61 | initialized: boolean;
62 | poses: {
63 | wxyz: [number, number, number, number];
64 | position: [number, number, number];
65 | }[];
66 | };
67 | };
68 |
69 | // Global hover state tracking.
70 | hoveredElementsCount: number;
71 | };
72 |
73 | export type ViewerContextContents = {
74 | // Non-mutable state.
75 | messageSource: "websocket" | "file_playback";
76 |
77 | // Zustand state hooks.
78 | useSceneTree: UseSceneTree;
79 | useGui: UseGui;
80 |
81 | // Single reference to all mutable state.
82 | mutable: React.MutableRefObject;
83 | };
84 |
85 | export const ViewerContext = React.createContext(
86 | null,
87 | );
88 |
--------------------------------------------------------------------------------
/src/viser/client/src/WebsocketInterface.tsx:
--------------------------------------------------------------------------------
1 | import WebsocketServerWorker from "./WebsocketServerWorker?worker";
2 | import React, { useContext } from "react";
3 | import { notifications } from "@mantine/notifications";
4 |
5 | import { ViewerContext } from "./ViewerContext";
6 | import { syncSearchParamServer } from "./SearchParamsUtils";
7 | import { WsWorkerIncoming, WsWorkerOutgoing } from "./WebsocketServerWorker";
8 |
9 | /** Component for handling websocket connections. */
10 | export function WebsocketMessageProducer() {
11 | const viewer = useContext(ViewerContext)!;
12 | const viewerMutable = viewer.mutable.current;
13 | const server = viewer.useGui((state) => state.server);
14 | const resetGui = viewer.useGui((state) => state.resetGui);
15 | const resetScene = viewer.useSceneTree((state) => state.resetScene);
16 |
17 | syncSearchParamServer(server);
18 |
19 | React.useEffect(() => {
20 | const worker = new WebsocketServerWorker();
21 |
22 | worker.onmessage = (event) => {
23 | const data: WsWorkerOutgoing = event.data;
24 | if (data.type === "connected") {
25 | resetGui();
26 | resetScene();
27 | viewer.useGui.setState({ websocketConnected: true });
28 | viewerMutable.sendMessage = (message) => {
29 | postToWorker({ type: "send", message: message });
30 | };
31 | } else if (data.type === "closed") {
32 | resetGui();
33 | viewer.useGui.setState({ websocketConnected: false });
34 | viewerMutable.sendMessage = (message) => {
35 | console.log(
36 | `Tried to send ${message.type} but websocket is not connected!`,
37 | );
38 | };
39 |
40 | // Show notification for version mismatch.
41 | if (data.versionMismatch) {
42 | notifications.show({
43 | id: "version-mismatch",
44 | title: "Connection rejected",
45 | message: `${data.closeReason}.`,
46 | color: "red",
47 | autoClose: 5000,
48 | withCloseButton: true,
49 | });
50 | }
51 | } else if (data.type === "message_batch") {
52 | viewerMutable.messageQueue.push(...data.messages);
53 | }
54 | };
55 | function postToWorker(data: WsWorkerIncoming) {
56 | worker.postMessage(data);
57 | }
58 | postToWorker({ type: "set_server", server: server });
59 | return () => {
60 | postToWorker({ type: "close" });
61 | viewerMutable.sendMessage = (message) =>
62 | console.log(
63 | `Tried to send ${message.type} but websocket is not connected!`,
64 | );
65 | viewer.useGui.setState({ websocketConnected: false });
66 | };
67 | }, [server, resetGui, resetScene]);
68 |
69 | return null;
70 | }
71 |
--------------------------------------------------------------------------------
/src/viser/client/src/WebsocketUtils.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as THREE from "three";
3 | import { Message } from "./WebsocketMessages";
4 | import { ViewerContext, ViewerContextContents } from "./ViewerContext";
5 |
6 | /** Easier, hook version of makeThrottledMessageSender. */
7 | export function useThrottledMessageSender(throttleMilliseconds: number) {
8 | const viewer = React.useContext(ViewerContext)!;
9 | return makeThrottledMessageSender(viewer, throttleMilliseconds);
10 | }
11 |
12 | /** Returns a function for sending messages, with automatic throttling. */
13 | export function makeThrottledMessageSender(
14 | viewer: ViewerContextContents,
15 | throttleMilliseconds: number,
16 | ) {
17 | let readyToSend = true;
18 | let stale = false;
19 | let latestMessage: Message | null = null;
20 |
21 | function send(message: Message) {
22 | const viewerMutable = viewer.mutable.current;
23 | if (viewerMutable.sendMessage === null) return;
24 | latestMessage = message;
25 | if (readyToSend) {
26 | viewerMutable.sendMessage(message);
27 | stale = false;
28 | readyToSend = false;
29 |
30 | setTimeout(() => {
31 | readyToSend = true;
32 | if (!stale) return;
33 | latestMessage && send(latestMessage);
34 | }, throttleMilliseconds);
35 | } else {
36 | stale = true;
37 | }
38 | }
39 | return send;
40 | }
41 |
42 | /** Type guard for threejs textures. Meant to be used with `scene.background`. */
43 | export function isTexture(
44 | background:
45 | | THREE.Color
46 | | THREE.Texture
47 | | THREE.CubeTexture
48 | | null
49 | | undefined,
50 | ): background is THREE.Texture {
51 | return (
52 | background !== null &&
53 | background !== undefined &&
54 | (background as THREE.Texture).isTexture !== undefined
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/viser/client/src/WorldTransformUtils.ts:
--------------------------------------------------------------------------------
1 | import { ViewerContextContents } from "./ViewerContext";
2 | import * as THREE from "three";
3 |
4 | /** Helper for computing the transformation between the three.js world and the
5 | * Python-exposed world frames. This is useful for things like switching
6 | * between +Y and +Z up directions for the world frame. */
7 | export function computeT_threeworld_world(viewer: ViewerContextContents) {
8 | const wxyz = viewer.mutable.current.nodeAttributesFromName[""]!.wxyz!;
9 | const position = viewer.mutable.current.nodeAttributesFromName[""]!
10 | .position ?? [0, 0, 0];
11 | return new THREE.Matrix4()
12 | .makeRotationFromQuaternion(
13 | new THREE.Quaternion(wxyz[1], wxyz[2], wxyz[3], wxyz[0]),
14 | )
15 | .setPosition(position[0], position[1], position[2]);
16 | }
17 |
18 | /** Helper for converting a ray from the three.js world frame to the Python
19 | * world frame. Applies the transformation from computeT_threeworld_world.
20 | */
21 | export function rayToViserCoords(
22 | viewer: ViewerContextContents,
23 | ray: THREE.Ray,
24 | ): THREE.Ray {
25 | const T_world_threeworld = computeT_threeworld_world(viewer).invert();
26 |
27 | const origin = ray.origin.clone().applyMatrix4(T_world_threeworld);
28 |
29 | // Compute just the rotation term without new memory allocation; this
30 | // will mutate T_world_threeworld!
31 | const R_world_threeworld = T_world_threeworld.setPosition(0.0, 0.0, 0);
32 | const direction = ray.direction.clone().applyMatrix4(R_world_threeworld);
33 |
34 | return new THREE.Ray(origin, direction);
35 | }
36 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { GuiButtonMessage } from "../WebsocketMessages";
2 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
3 | import { Box } from "@mantine/core";
4 |
5 | import { Button } from "@mantine/core";
6 | import React from "react";
7 | import { htmlIconWrapper } from "./ComponentStyles.css";
8 | import { toMantineColor } from "./colorUtils";
9 |
10 | export default function ButtonComponent({
11 | uuid,
12 | props: { visible, disabled, label, color, _icon_html: icon_html },
13 | }: GuiButtonMessage) {
14 | const { messageSender } = React.useContext(GuiComponentContext)!;
15 | if (!(visible ?? true)) return null;
16 |
17 | return (
18 |
19 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/ButtonGroup.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Button, Flex } from "@mantine/core";
3 | import { ViserInputComponent } from "./common";
4 | import { GuiButtonGroupMessage } from "../WebsocketMessages";
5 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
6 |
7 | export default function ButtonGroupComponent({
8 | uuid,
9 | props: { hint, label, visible, disabled, options },
10 | }: GuiButtonGroupMessage) {
11 | const { messageSender } = React.useContext(GuiComponentContext)!;
12 | if (!visible) return null;
13 | return (
14 |
15 |
16 | {options.map((option, index) => (
17 |
33 | ))}
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ViserInputComponent } from "./common";
3 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
4 | import { GuiCheckboxMessage } from "../WebsocketMessages";
5 | import { Box, Checkbox, Tooltip } from "@mantine/core";
6 |
7 | export default function CheckboxComponent({
8 | uuid,
9 | value,
10 | props: { disabled, visible, hint, label },
11 | }: GuiCheckboxMessage) {
12 | const { setValue } = React.useContext(GuiComponentContext)!;
13 | if (!visible) return null;
14 | let input = (
15 | {
20 | setValue(uuid, value.target.checked);
21 | }}
22 | disabled={disabled}
23 | />
24 | );
25 | if (hint !== null && hint !== undefined) {
26 | // For checkboxes, we want to make sure that the wrapper
27 | // doesn't expand to the full wuuidth of the parent. This will
28 | // de-center the tooltip.
29 | input = (
30 |
39 | {input}
40 |
41 | );
42 | }
43 | return (
44 | {input}
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/ComponentStyles.css.ts:
--------------------------------------------------------------------------------
1 | import { globalStyle, style } from "@vanilla-extract/css";
2 |
3 | export const htmlIconWrapper = style({
4 | height: "1em",
5 | width: "1em",
6 | position: "relative",
7 | });
8 |
9 | globalStyle(`${htmlIconWrapper} svg`, {
10 | height: "auto",
11 | width: "1em",
12 | position: "absolute",
13 | top: "50%",
14 | transform: "translateY(-50%)",
15 | });
16 |
17 | // Class for sliders with default min/max marks. We use this for aestheticn
18 | // its; global styles are used to shift the min/max mark labels to stay closer
19 | // within the bounds of the slider.
20 | export const sliderDefaultMarks = style({});
21 |
22 | globalStyle(
23 | `${sliderDefaultMarks} .mantine-Slider-markWrapper:first-of-type div:nth-of-type(2)`,
24 | {
25 | transform: "translate(-0.1rem, 0.03rem) !important",
26 | },
27 | );
28 |
29 | globalStyle(
30 | `${sliderDefaultMarks} .mantine-Slider-markWrapper:last-of-type div:nth-of-type(2)`,
31 | {
32 | transform: "translate(-85%, 0.03rem) !important",
33 | },
34 | );
35 |
36 | // Style for filled slider marks - use primary color to match the active segment.
37 | globalStyle(".mantine-Slider-mark[data-filled]:not([data-disabled])", {
38 | background: "var(--mantine-primary-color-filled)",
39 | borderColor: "var(--mantine-primary-color-filled)",
40 | });
41 |
42 | // Style for filled slider marks when disabled - use separate rules for light/dark.
43 | globalStyle(".mantine-Slider-mark[data-filled][data-disabled]", {
44 | background: "var(--mantine-color-gray-5)",
45 | borderColor: "var(--mantine-color-gray-5)",
46 | });
47 |
48 | // Dark mode styles for filled marks when disabled.
49 | globalStyle(
50 | '[data-mantine-color-scheme="dark"] .mantine-Slider-mark[data-filled][data-disabled]',
51 | {
52 | background: "var(--mantine-color-dark-3)",
53 | borderColor: "var(--mantine-color-dark-3)",
54 | },
55 | );
56 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
3 | import { ViserInputComponent } from "./common";
4 | import { GuiDropdownMessage } from "../WebsocketMessages";
5 | import { Select } from "@mantine/core";
6 |
7 | export default function DropdownComponent({
8 | uuid,
9 | value,
10 | props: { hint, label, disabled, visible, options },
11 | }: GuiDropdownMessage) {
12 | const { setValue } = React.useContext(GuiComponentContext)!;
13 | if (!visible) return null;
14 | return (
15 |
16 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Folder.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from "@vanilla-extract/css";
2 | import { vars } from "../AppTheme";
3 |
4 | export const folderWrapper = style({
5 | borderWidth: "1px",
6 | position: "relative",
7 | marginLeft: vars.spacing.xs,
8 | marginRight: vars.spacing.xs,
9 | // If there's a GUI element above, we need more margin.
10 | marginTop: vars.spacing.xs,
11 | // If there's a GUI element below, we need more margin.
12 | // Note: 0.5em is the vertical margin below general GUI elements.
13 | marginBottom: "1.2em",
14 | ":last-child": {
15 | marginBottom: "0.5em",
16 | },
17 | paddingBottom: `calc(${vars.spacing.xs} - 0.5em)`,
18 | });
19 |
20 | export const folderLabel = style({
21 | fontSize: "0.875em",
22 | position: "absolute",
23 | padding: "0 0.375em 0 0.375em",
24 | top: 0,
25 | left: "0.375em",
26 | transform: "translateY(-50%)",
27 | userSelect: "none",
28 | fontWeight: 500,
29 | });
30 |
31 | export const folderToggleIcon = style({
32 | width: "0.9em",
33 | height: "0.9em",
34 | strokeWidth: 3,
35 | top: "0.1em",
36 | position: "relative",
37 | marginLeft: "0.25em",
38 | marginRight: "-0.1em",
39 | opacity: 0.5,
40 | });
41 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Folder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useDisclosure } from "@mantine/hooks";
3 | import { GuiFolderMessage } from "../WebsocketMessages";
4 | import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
5 | import { Box, Collapse, Paper } from "@mantine/core";
6 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
7 | import { ViewerContext } from "../ViewerContext";
8 | import { folderLabel, folderToggleIcon, folderWrapper } from "./Folder.css";
9 |
10 | export default function FolderComponent({
11 | uuid,
12 | props: { label, visible, expand_by_default },
13 | }: GuiFolderMessage) {
14 | const viewer = React.useContext(ViewerContext)!;
15 | const [opened, { toggle }] = useDisclosure(expand_by_default);
16 | const guiIdSet = viewer.useGui(
17 | (state) => state.guiUuidSetFromContainerUuid[uuid],
18 | );
19 | const guiContext = React.useContext(GuiComponentContext)!;
20 | const isEmpty = guiIdSet === undefined || Object.keys(guiIdSet).length === 0;
21 |
22 | const ToggleIcon = opened ? IconChevronUp : IconChevronDown;
23 | if (!visible) return null;
24 | return (
25 |
26 |
33 | {label}
34 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Html.tsx:
--------------------------------------------------------------------------------
1 | import { GuiHtmlMessage } from "../WebsocketMessages";
2 |
3 | function HtmlComponent({ props }: GuiHtmlMessage) {
4 | if (!props.visible) return null;
5 | return ;
6 | }
7 |
8 | export default HtmlComponent;
9 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Image.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { GuiImageMessage } from "../WebsocketMessages";
3 | import { Box, Text } from "@mantine/core";
4 |
5 | function ImageComponent({ props }: GuiImageMessage) {
6 | if (!props.visible) return null;
7 |
8 | const [imageUrl, setImageUrl] = useState(null);
9 |
10 | useEffect(() => {
11 | if (props._data === null) {
12 | setImageUrl(null);
13 | } else {
14 | const image_url = URL.createObjectURL(
15 | new Blob([props._data], { type: props.media_type }),
16 | );
17 | setImageUrl(image_url);
18 | return () => {
19 | URL.revokeObjectURL(image_url);
20 | };
21 | }
22 | }, [props._data, props.media_type]);
23 |
24 | return imageUrl === null ? null : (
25 |
26 | {props.label === null ? null : (
27 |
28 | {props.label}
29 |
30 | )}
31 |
38 |
39 | );
40 | }
41 |
42 | export default ImageComponent;
43 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Markdown.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from "@mantine/core";
2 | import Markdown from "../Markdown";
3 | import { ErrorBoundary } from "react-error-boundary";
4 | import { GuiMarkdownMessage } from "../WebsocketMessages";
5 |
6 | export default function MarkdownComponent({
7 | props: { visible, _markdown: markdown },
8 | }: GuiMarkdownMessage) {
9 | if (!visible) return null;
10 | return (
11 |
12 | Markdown Failed to Render
15 | }
16 | >
17 | {markdown}
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/MultiSlider.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { GuiMultiSliderMessage } from "../WebsocketMessages";
3 | import { Box } from "@mantine/core";
4 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
5 | import { ViserInputComponent } from "./common";
6 | import { MultiSlider } from "./MultiSliderComponent";
7 | import { sliderDefaultMarks } from "./ComponentStyles.css";
8 |
9 | export default function MultiSliderComponent({
10 | uuid,
11 | value,
12 | props: {
13 | label,
14 | hint,
15 | visible,
16 | disabled,
17 | min,
18 | max,
19 | precision,
20 | step,
21 | _marks: marks,
22 | fixed_endpoints,
23 | min_range,
24 | },
25 | }: GuiMultiSliderMessage) {
26 | const { setValue } = React.useContext(GuiComponentContext)!;
27 | if (!visible) return null;
28 | const updateValue = (value: number[]) => setValue(uuid, value);
29 | const input = (
30 |
31 |
62 |
63 | );
64 |
65 | return (
66 |
67 | {input}
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/MultiSliderComponent.css:
--------------------------------------------------------------------------------
1 | .multi-slider {
2 | position: relative;
3 | width: 100%;
4 | height: 1rem;
5 | margin-top: -0.5rem;
6 | }
7 |
8 | .multi-slider-track-container {
9 | position: relative;
10 | height: 0.25rem;
11 | cursor: pointer;
12 | margin: 1rem 0;
13 | }
14 |
15 | .multi-slider-track {
16 | position: absolute;
17 | top: 0;
18 | left: 0;
19 | right: 0;
20 | height: 100%;
21 | background-color: var(--mantine-color-gray-3);
22 | border-radius: var(--mantine-radius-xl);
23 | }
24 |
25 | .multi-slider-thumb {
26 | position: absolute;
27 | top: 50%;
28 | transform: translate(-50%, -50%);
29 | width: 0.5rem;
30 | height: 0.75rem;
31 | border-radius: var(--mantine-radius-xs);
32 | cursor: pointer;
33 | transition: transform 0.1s;
34 | z-index: 2;
35 | }
36 |
37 | .multi-slider-thumb:active {
38 | transform: translate(-50%, -50%) scale(1.1);
39 | }
40 |
41 | .multi-slider.disabled {
42 | opacity: 0.6;
43 | cursor: not-allowed;
44 | }
45 |
46 | .multi-slider.disabled .multi-slider-track-container {
47 | cursor: not-allowed;
48 | }
49 |
50 | .multi-slider.disabled .multi-slider-thumb {
51 | cursor: not-allowed;
52 | background-color: var(--mantine-color-gray-5) !important;
53 | }
54 |
55 | .multi-slider-mark-wrapper {
56 | position: absolute;
57 | top: 0px;
58 | transform: translateX(-50%);
59 | height: 100%;
60 | display: flex;
61 | flex-direction: column;
62 | align-items: center;
63 | justify-content: center;
64 | }
65 |
66 | .multi-slider-mark {
67 | width: 0.25rem;
68 | height: 0.25rem;
69 | background-color: var(--mantine-color-gray-3);
70 | border-radius: 50%;
71 | transform: scale(2);
72 | }
73 |
74 | .multi-slider-mark-label {
75 | position: absolute;
76 | top: 100%;
77 | margin-top: 0.05rem;
78 | font-size: 0.6rem;
79 | color: var(--mantine-color-gray-6);
80 | white-space: nowrap;
81 | }
82 |
83 | /* Support for dark mode. */
84 | [data-mantine-color-scheme="dark"] .multi-slider-track {
85 | background-color: var(--mantine-color-dark-4);
86 | }
87 |
88 | [data-mantine-color-scheme="dark"] .multi-slider-thumb {
89 | border-color: var(--mantine-color-dark-7);
90 | }
91 |
92 | [data-mantine-color-scheme="dark"] .multi-slider.disabled .multi-slider-thumb {
93 | background-color: var(--mantine-color-dark-3);
94 | }
95 |
96 | [data-mantine-color-scheme="dark"] .multi-slider-mark {
97 | background-color: var(--mantine-color-dark-4);
98 | }
99 |
100 | [data-mantine-color-scheme="dark"] .multi-slider-mark-label {
101 | color: var(--mantine-color-dark-2);
102 | }
103 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/NumberInput.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
3 | import { GuiNumberMessage } from "../WebsocketMessages";
4 | import { ViserInputComponent } from "./common";
5 | import { NumberInput } from "@mantine/core";
6 |
7 | export default function NumberInputComponent({
8 | uuid,
9 | value,
10 | props: { visible, label, hint, disabled, precision, min, max, step },
11 | }: GuiNumberMessage) {
12 | const { setValue } = React.useContext(GuiComponentContext)!;
13 | if (!visible) return null;
14 | return (
15 |
16 | {
26 | // Ignore empty values.
27 | newValue !== "" && setValue(uuid, newValue);
28 | }}
29 | styles={{
30 | input: {
31 | minHeight: "1.625rem",
32 | height: "1.625rem",
33 | },
34 | controls: {
35 | height: "1.625em",
36 | width: "0.825em",
37 | },
38 | }}
39 | disabled={disabled}
40 | stepHoldDelay={500}
41 | stepHoldInterval={(t) => Math.max(1000 / t ** 2, 25)}
42 | />
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Progress } from "@mantine/core";
2 | import { GuiProgressBarMessage } from "../WebsocketMessages";
3 | import { toMantineColor } from "./colorUtils";
4 |
5 | export default function ProgressBarComponent({
6 | value,
7 | props: { visible, color, animated },
8 | }: GuiProgressBarMessage) {
9 | if (!visible) return null;
10 | return (
11 |
12 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Rgb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ColorInput } from "@mantine/core";
3 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
4 | import { ViserInputComponent } from "./common";
5 | import { GuiRgbMessage } from "../WebsocketMessages";
6 | import { IconColorPicker } from "@tabler/icons-react";
7 | import { rgbToString, parseToRgb, rgbEqual } from "./colorUtils";
8 |
9 | export default function RgbComponent({
10 | uuid,
11 | value,
12 | props: { label, hint, disabled, visible },
13 | }: GuiRgbMessage) {
14 | const { setValue } = React.useContext(GuiComponentContext)!;
15 |
16 | // Local state for the input value.
17 | const [localValue, setLocalValue] = React.useState(rgbToString(value));
18 |
19 | // Update local value when prop value changes.
20 | React.useEffect(() => {
21 | // Only update if the parsed local value differs from the new prop value.
22 | const parsedLocal = parseToRgb(localValue);
23 | if (!parsedLocal || !rgbEqual(parsedLocal, value)) {
24 | setLocalValue(rgbToString(value));
25 | }
26 | }, [value]);
27 |
28 | if (!visible) return null;
29 |
30 | return (
31 |
32 | }
38 | popoverProps={{ zIndex: 1000 }}
39 | styles={{
40 | input: { height: "1.625rem", minHeight: "1.625rem" },
41 | }}
42 | onChange={(v) => {
43 | // Always update local state for responsive typing.
44 | setLocalValue(v);
45 |
46 | // Only process RGB format during onChange (not hex).
47 | if (v.startsWith("rgb(")) {
48 | const parsed = parseToRgb(v);
49 | if (parsed && !rgbEqual(parsed, value)) {
50 | setValue(uuid, parsed);
51 | }
52 | }
53 | }}
54 | onKeyDown={(e) => {
55 | // Handle Enter key for hex color input.
56 | if (e.key === "Enter") {
57 | const parsed = parseToRgb(localValue);
58 | if (parsed) {
59 | setValue(uuid, parsed);
60 | }
61 | }
62 | }}
63 | onBlur={() => {
64 | // Parse any format when input loses focus.
65 | const parsed = parseToRgb(localValue);
66 | if (parsed && !rgbEqual(parsed, value)) {
67 | setValue(uuid, parsed);
68 | }
69 | }}
70 | />
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Rgba.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ColorInput } from "@mantine/core";
3 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
4 | import { ViserInputComponent } from "./common";
5 | import { GuiRgbaMessage } from "../WebsocketMessages";
6 | import { IconColorPicker } from "@tabler/icons-react";
7 | import { rgbaToString, parseToRgba, rgbaEqual } from "./colorUtils";
8 |
9 | export default function RgbaComponent({
10 | uuid,
11 | value,
12 | props: { label, hint, disabled, visible },
13 | }: GuiRgbaMessage) {
14 | const { setValue } = React.useContext(GuiComponentContext)!;
15 |
16 | // Local state for the input value.
17 | const [localValue, setLocalValue] = React.useState(rgbaToString(value));
18 |
19 | // Update local value when prop value changes.
20 | React.useEffect(() => {
21 | // Only update if the parsed local value differs from the new prop value.
22 | const parsedLocal = parseToRgba(localValue);
23 | if (!parsedLocal || !rgbaEqual(parsedLocal, value)) {
24 | setLocalValue(rgbaToString(value));
25 | }
26 | }, [value, localValue]);
27 |
28 | if (!visible) return null;
29 |
30 | return (
31 |
32 | }
38 | popoverProps={{ zIndex: 1000 }}
39 | styles={{
40 | input: { height: "1.625rem", minHeight: "1.625rem" },
41 | }}
42 | onChange={(v) => {
43 | // Always update local state for responsive typing.
44 | setLocalValue(v);
45 |
46 | // Only process RGBA format during onChange (not hex).
47 | if (v.startsWith("rgba(")) {
48 | const parsed = parseToRgba(v);
49 | if (parsed && !rgbaEqual(parsed, value)) {
50 | setValue(uuid, parsed);
51 | }
52 | }
53 | }}
54 | onKeyDown={(e) => {
55 | // Handle Enter key for hex color input.
56 | if (e.key === "Enter") {
57 | const parsed = parseToRgba(localValue);
58 | if (parsed) {
59 | setValue(uuid, parsed);
60 | }
61 | }
62 | }}
63 | onBlur={() => {
64 | // Parse any format when input loses focus.
65 | const parsed = parseToRgba(localValue);
66 | if (parsed && !rgbaEqual(parsed, value)) {
67 | setValue(uuid, parsed);
68 | }
69 | }}
70 | />
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/TabGroup.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { GuiTabGroupMessage } from "../WebsocketMessages";
3 | import { Tabs } from "@mantine/core";
4 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
5 | import { htmlIconWrapper } from "./ComponentStyles.css";
6 |
7 | export default function TabGroupComponent({
8 | props: {
9 | _tab_labels: tab_labels,
10 | _tab_icons_html: tab_icons_html,
11 | _tab_container_ids: tab_container_ids,
12 | visible,
13 | },
14 | }: GuiTabGroupMessage) {
15 | const { GuiContainer } = React.useContext(GuiComponentContext)!;
16 | if (!visible) return null;
17 | return (
18 |
19 |
20 | {tab_labels.map((label, index) => (
21 |
34 | )
35 | }
36 | >
37 | {label}
38 |
39 | ))}
40 |
41 | {tab_container_ids.map((containerUuid, index) => (
42 |
43 |
44 |
45 | ))}
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/TextInput.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { TextInput, Textarea } from "@mantine/core";
3 | import { ViserInputComponent } from "./common";
4 | import { GuiTextMessage } from "../WebsocketMessages";
5 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
6 |
7 | export default function TextInputComponent({
8 | uuid,
9 | value,
10 | props: { hint, label, disabled, visible, multiline },
11 | }: GuiTextMessage) {
12 | const { setValue } = React.useContext(GuiComponentContext)!;
13 | if (!visible) return null;
14 | return (
15 |
16 | {multiline ? (
17 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Vector2.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
3 | import { GuiVector2Message } from "../WebsocketMessages";
4 | import { VectorInput, ViserInputComponent } from "./common";
5 |
6 | export default function Vector2Component({
7 | uuid,
8 | value,
9 | props: { hint, label, visible, disabled, min, max, step, precision },
10 | }: GuiVector2Message) {
11 | const { setValue } = React.useContext(GuiComponentContext)!;
12 | if (!visible) return null;
13 | return (
14 |
15 | setValue(uuid, value)}
20 | min={min}
21 | max={max}
22 | step={step}
23 | precision={precision}
24 | disabled={disabled}
25 | />
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/viser/client/src/components/Vector3.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext";
3 | import { GuiVector3Message } from "../WebsocketMessages";
4 | import { VectorInput, ViserInputComponent } from "./common";
5 |
6 | export default function Vector3Component({
7 | uuid,
8 | value,
9 | props: { hint, label, visible, disabled, min, max, step, precision },
10 | }: GuiVector3Message) {
11 | const { setValue } = React.useContext(GuiComponentContext)!;
12 | if (!visible) return null;
13 | return (
14 |
15 | setValue(uuid, value)}
20 | min={min}
21 | max={max}
22 | step={step}
23 | precision={precision}
24 | disabled={disabled}
25 | />
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/viser/client/src/csm/CSM.d.ts:
--------------------------------------------------------------------------------
1 | import { Camera, DirectionalLight, Material, Object3D, Vector3 } from 'three';
2 |
3 | export interface CSMParameters {
4 | camera: Camera;
5 | parent: Object3D;
6 | cascades?: number;
7 | maxFar?: number;
8 | mode?: 'practical' | 'uniform' | 'logarithmic' | 'custom';
9 | shadowMapSize?: number;
10 | shadowBias?: number;
11 | lightDirection?: Vector3;
12 | lightIntensity?: number;
13 | lightNear?: number;
14 | lightFar?: number;
15 | lightMargin?: number;
16 | customSplitsCallback?: (cascades: number, near: number, far: number, breaks: number[]) => void;
17 | }
18 |
19 | export class CSM {
20 | camera: Camera;
21 | parent: Object3D;
22 | cascades: number;
23 | maxFar: number;
24 | mode: 'practical' | 'uniform' | 'logarithmic' | 'custom';
25 | shadowMapSize: number;
26 | shadowBias: number;
27 | lightDirection: Vector3;
28 | lightIntensity: number;
29 | lightNear: number;
30 | lightFar: number;
31 | lightMargin: number;
32 | customSplitsCallback?: (cascades: number, near: number, far: number, breaks: number[]) => void;
33 | fade: boolean;
34 | lights: DirectionalLight[];
35 |
36 | constructor(data: CSMParameters);
37 |
38 | update(): void;
39 | updateFrustums(): void;
40 | remove(): void;
41 | dispose(): void;
42 | setupMaterial(material: Material): void;
43 | }
--------------------------------------------------------------------------------
/src/viser/client/src/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Inter";
3 | src: url("/Inter-VariableFont_slnt,wght.ttf") format("truetype");
4 | font-weight: 1 100 200 300 400 500 600 700 800 900 1000;
5 | font-style: normal italic;
6 | }
7 |
8 | body,
9 | html {
10 | width: 100%;
11 | height: 100%;
12 | margin: 0;
13 | padding: 0;
14 | overflow: hidden;
15 | }
16 |
17 | body {
18 | font-family:
19 | "Inter",
20 | -apple-system,
21 | BlinkMacSystemFont,
22 | "Segoe UI",
23 | "Roboto",
24 | "Oxygen",
25 | "Ubuntu",
26 | "Cantarell",
27 | "Fira Sans",
28 | "Droid Sans",
29 | "Helvetica Neue",
30 | sans-serif;
31 | -webkit-font-smoothing: antialiased;
32 | -moz-osx-font-smoothing: grayscale;
33 | }
34 |
35 | code {
36 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
37 | monospace;
38 | }
39 |
40 | #root {
41 | width: 100%;
42 | height: 100%;
43 | overflow: hidden;
44 | }
45 |
46 | /* Styling for threejs stats panel. Switches position: fixed to position:
47 | * absolute, to respect parent + for better multi-pane layout. */
48 | .stats-panel {
49 | position: absolute !important;
50 | }
51 |
52 | /* Styling for color chips in markdown */
53 | .gfm-color-chip {
54 | margin-left: 0.125rem;
55 | display: inline-block;
56 | height: 0.625rem;
57 | width: 0.625rem;
58 | border-radius: 9999px;
59 | border: 1px solid gray;
60 | transform: TranslateY(0.07em);
61 | }
62 |
--------------------------------------------------------------------------------
/src/viser/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import { Root } from "./App";
3 | import { enableMapSet } from "immer";
4 |
5 | enableMapSet();
6 |
7 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
8 | ,
9 | );
10 |
--------------------------------------------------------------------------------
/src/viser/client/src/mesh/BasicMesh.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as THREE from "three";
3 | import { createStandardMaterial } from "./MeshUtils";
4 | import { MeshMessage } from "../WebsocketMessages";
5 | import { OutlinesIfHovered } from "../OutlinesIfHovered";
6 |
7 | /**
8 | * Component for rendering basic THREE.js meshes
9 | */
10 | export const BasicMesh = React.forwardRef(
11 | function BasicMesh(message, ref: React.ForwardedRef) {
12 | // Create material based on props.
13 | const material = React.useMemo(() => {
14 | return createStandardMaterial(message.props);
15 | }, [
16 | message.props.material,
17 | message.props.color,
18 | message.props.wireframe,
19 | message.props.opacity,
20 | message.props.flat_shading,
21 | message.props.side,
22 | ]);
23 |
24 | // Setup geometry using memoization.
25 | const geometry = React.useMemo(() => {
26 | const geometry = new THREE.BufferGeometry();
27 | geometry.setAttribute(
28 | "position",
29 | new THREE.BufferAttribute(
30 | new Float32Array(
31 | message.props.vertices.buffer.slice(
32 | message.props.vertices.byteOffset,
33 | message.props.vertices.byteOffset +
34 | message.props.vertices.byteLength,
35 | ),
36 | ),
37 | 3,
38 | ),
39 | );
40 | geometry.setIndex(
41 | new THREE.BufferAttribute(
42 | new Uint32Array(
43 | message.props.faces.buffer.slice(
44 | message.props.faces.byteOffset,
45 | message.props.faces.byteOffset + message.props.faces.byteLength,
46 | ),
47 | ),
48 | 1,
49 | ),
50 | );
51 | geometry.computeVertexNormals();
52 | geometry.computeBoundingSphere();
53 | return geometry;
54 | }, [message.props.vertices.buffer, message.props.faces.buffer]);
55 |
56 | // Clean up resources when component unmounts.
57 | React.useEffect(() => {
58 | return () => {
59 | if (material) material.dispose();
60 | if (geometry) geometry.dispose();
61 | };
62 | }, [material, geometry]);
63 |
64 | return (
65 |
72 |
73 |
74 | );
75 | },
76 | );
77 |
--------------------------------------------------------------------------------
/src/viser/client/src/mesh/BatchedGlbAsset.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import * as THREE from "three";
3 | import { BatchedGlbMessage } from "../WebsocketMessages";
4 | import { useGlbLoader } from "./GlbLoaderUtils";
5 | import { ViewerContext } from "../ViewerContext";
6 | import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
7 | import { BatchedMeshBase } from "./BatchedMeshBase";
8 |
9 | /**
10 | * Component for rendering batched/instanced GLB models
11 | *
12 | * Note: Batched GLB has some limitations:
13 | * - Animations are not supported
14 | * - The hierarchy in the GLB is flattened
15 | * - Each mesh in the GLB is instanced separately
16 | */
17 | export const BatchedGlbAsset = React.forwardRef(
18 | function BatchedGlbAsset(message, ref) {
19 | const viewer = React.useContext(ViewerContext)!;
20 | const clickable =
21 | viewer.useSceneTree(
22 | (state) => state.nodeFromName[message.name]?.clickable,
23 | ) ?? false;
24 |
25 | // Note: We don't support animations for batched meshes.
26 | const { gltf } = useGlbLoader(message.props.glb_data);
27 |
28 | // Extract geometry and materials from the GLB.
29 | const { geometry, material } = useMemo(() => {
30 | if (!gltf) return { geometry: null, material: null };
31 |
32 | // Collect meshes and their transforms from the original scene.
33 | const geometries: THREE.BufferGeometry[] = [];
34 | const materials: THREE.Material[] = [];
35 | gltf.scene.traverse((node) => {
36 | if (node instanceof THREE.Mesh && node.parent) {
37 | // Apply any transforms from the model hierarchy to the geometry.
38 | (node.geometry as THREE.BufferGeometry).applyMatrix4(
39 | node.matrixWorld,
40 | );
41 | geometries.push(node.geometry);
42 | materials.push(node.material);
43 | }
44 | });
45 |
46 | // Merge geometries if needed.
47 | const mergedGeometry =
48 | geometries.length === 1
49 | ? geometries[0].clone()
50 | : mergeGeometries(geometries, true);
51 |
52 | // Use either a single material or an array.
53 | const finalMaterial = materials.length === 1 ? materials[0] : materials;
54 |
55 | return {
56 | geometry: mergedGeometry,
57 | material: finalMaterial,
58 | };
59 | }, [gltf]);
60 |
61 | if (!geometry || !material) return null;
62 |
63 | return (
64 |
65 |
77 |
78 | );
79 | },
80 | );
81 |
--------------------------------------------------------------------------------
/src/viser/client/src/mesh/BatchedMesh.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import * as THREE from "three";
3 | import { createStandardMaterial } from "./MeshUtils";
4 | import { BatchedMeshesMessage } from "../WebsocketMessages";
5 | import { InstancedMesh2 } from "@three.ez/instanced-mesh";
6 | import { ViewerContext } from "../ViewerContext";
7 | import { BatchedMeshBase } from "./BatchedMeshBase";
8 |
9 | /**
10 | * Component for rendering batched/instanced meshes
11 | */
12 | export const BatchedMesh = React.forwardRef<
13 | InstancedMesh2,
14 | BatchedMeshesMessage
15 | >(function BatchedMesh(message, ref) {
16 | const viewer = React.useContext(ViewerContext)!;
17 | const clickable =
18 | viewer.useSceneTree(
19 | (state) => state.nodeFromName[message.name]?.clickable,
20 | ) ?? false;
21 |
22 | // Create a material based on the message props.
23 | const material = useMemo(() => {
24 | // Create the material with properties from the message.
25 | const mat = createStandardMaterial({
26 | material: message.props.material,
27 | color: message.props.color,
28 | wireframe: message.props.wireframe,
29 | opacity: message.props.opacity,
30 | flat_shading: message.props.flat_shading,
31 | side: message.props.side,
32 | });
33 |
34 | // Set additional properties.
35 | if (message.props.opacity !== null && message.props.opacity < 1.0) {
36 | mat.transparent = true;
37 | }
38 |
39 | return mat;
40 | }, [
41 | message.props.material,
42 | message.props.color,
43 | message.props.wireframe,
44 | message.props.opacity,
45 | message.props.flat_shading,
46 | message.props.side,
47 | ]);
48 |
49 | // Setup geometry using memoization.
50 | const geometry = useMemo(() => {
51 | const geometry = new THREE.BufferGeometry();
52 | geometry.setAttribute(
53 | "position",
54 | new THREE.BufferAttribute(
55 | new Float32Array(
56 | message.props.vertices.buffer.slice(
57 | message.props.vertices.byteOffset,
58 | message.props.vertices.byteOffset +
59 | message.props.vertices.byteLength,
60 | ),
61 | ),
62 | 3,
63 | ),
64 | );
65 | geometry.setIndex(
66 | new THREE.BufferAttribute(
67 | new Uint32Array(
68 | message.props.faces.buffer.slice(
69 | message.props.faces.byteOffset,
70 | message.props.faces.byteOffset + message.props.faces.byteLength,
71 | ),
72 | ),
73 | 1,
74 | ),
75 | );
76 | geometry.computeVertexNormals();
77 | geometry.computeBoundingSphere();
78 | return geometry;
79 | }, [message.props.vertices.buffer, message.props.faces.buffer]);
80 |
81 | return (
82 |
94 | );
95 | });
96 |
--------------------------------------------------------------------------------
/src/viser/client/src/mesh/GlbLoaderUtils.tsx:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
3 | import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
4 | import React from "react";
5 | import { disposeMaterial } from "./MeshUtils";
6 |
7 | // We use a CDN for Draco. We could move this locally if we want to use Viser offline.
8 | const dracoLoader = new DRACOLoader();
9 | dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
10 |
11 | /**
12 | * Dispose a 3D object and its resources
13 | */
14 | export function disposeNode(node: any) {
15 | if (node instanceof THREE.Mesh) {
16 | if (node.geometry) {
17 | node.geometry.dispose();
18 | }
19 | if (node.material) {
20 | if (Array.isArray(node.material)) {
21 | node.material.forEach((material) => {
22 | disposeMaterial(material);
23 | });
24 | } else {
25 | disposeMaterial(node.material);
26 | }
27 | }
28 | }
29 | }
30 |
31 | /**
32 | * Custom hook for loading a GLB model
33 | */
34 | export function useGlbLoader(glb_data: Uint8Array) {
35 | // State for loaded model and meshes
36 | const [gltf, setGltf] = React.useState();
37 | const [meshes, setMeshes] = React.useState([]);
38 |
39 | // Animation mixer reference
40 | const mixerRef = React.useRef(null);
41 |
42 | // Load the GLB model
43 | React.useEffect(() => {
44 | const loader = new GLTFLoader();
45 | loader.setDRACOLoader(dracoLoader);
46 | loader.parse(
47 | new Uint8Array(glb_data).buffer,
48 | "",
49 | (gltf) => {
50 | // Setup animations if present
51 | if (gltf.animations && gltf.animations.length) {
52 | mixerRef.current = new THREE.AnimationMixer(gltf.scene);
53 | gltf.animations.forEach((clip) => {
54 | mixerRef.current!.clipAction(clip).play();
55 | });
56 | }
57 |
58 | // Process all meshes in the scene
59 | const meshes: THREE.Mesh[] = [];
60 | gltf?.scene.traverse((obj) => {
61 | if (obj instanceof THREE.Mesh) {
62 | obj.geometry.computeVertexNormals();
63 | obj.geometry.computeBoundingSphere();
64 | meshes.push(obj);
65 | }
66 | });
67 |
68 | setMeshes(meshes);
69 | setGltf(gltf);
70 | },
71 | (error) => {
72 | console.log("Error loading GLB!");
73 | console.log(error);
74 | },
75 | );
76 |
77 | // Cleanup function
78 | return () => {
79 | if (mixerRef.current) mixerRef.current.stopAllAction();
80 |
81 | // Attempt to free resources
82 | if (gltf) {
83 | gltf.scene.traverse(disposeNode);
84 | }
85 | };
86 | }, [glb_data]);
87 |
88 | // Return the loaded model, meshes, and mixer for animation updates
89 | return { gltf, meshes, mixerRef };
90 | }
91 |
--------------------------------------------------------------------------------
/src/viser/client/src/mesh/SingleGlbAsset.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as THREE from "three";
3 | import { GlbMessage } from "../WebsocketMessages";
4 | import { useGlbLoader } from "./GlbLoaderUtils";
5 | import { useFrame, useThree } from "@react-three/fiber";
6 | import { HoverableContext } from "../HoverContext";
7 | import { OutlinesMaterial } from "../Outlines";
8 | import { ViewerContext } from "../ViewerContext";
9 |
10 | /**
11 | * Component for rendering a single GLB model
12 | */
13 | export const SingleGlbAsset = React.forwardRef(
14 | function SingleGlbAsset(message, ref: React.ForwardedRef) {
15 | // Load model without passing shadow settings - we'll apply them in useEffect.
16 | const { gltf, meshes, mixerRef } = useGlbLoader(message.props.glb_data);
17 |
18 | // Apply shadow settings directly to the model.
19 | React.useEffect(() => {
20 | if (!gltf) return;
21 |
22 | gltf.scene.traverse((obj) => {
23 | if (obj instanceof THREE.Mesh) {
24 | obj.castShadow = message.props.cast_shadow;
25 | obj.receiveShadow = message.props.receive_shadow;
26 | }
27 | });
28 | }, [gltf, message.props.cast_shadow, message.props.receive_shadow]);
29 |
30 | // Update animations on each frame if mixer exists.
31 | useFrame((_, delta: number) => {
32 | mixerRef.current?.update(delta);
33 | });
34 |
35 | // Get rendering context for screen size.
36 | const gl = useThree((state) => state.gl);
37 | const contextSize = React.useMemo(
38 | () => gl.getDrawingBufferSize(new THREE.Vector2()),
39 | [gl],
40 | );
41 |
42 | // Hover/clicking.
43 | const outlineMaterial = React.useMemo(() => {
44 | const material = new OutlinesMaterial({
45 | side: THREE.BackSide,
46 | });
47 | material.thickness = 10;
48 | material.color = new THREE.Color(0xfbff00); // Yellow highlight color
49 | material.opacity = 0.8;
50 | material.size = contextSize;
51 | material.transparent = true;
52 | material.screenspace = true; // Use screenspace for consistent thickness
53 | material.toneMapped = true;
54 | return material;
55 | }, [contextSize]);
56 | const outlineRef = React.useRef(null);
57 | const hoveredRef = React.useContext(HoverableContext)!;
58 | const viewer = React.useContext(ViewerContext)!;
59 | useFrame(() => {
60 | if (outlineRef.current === null) return;
61 | outlineRef.current.visible = hoveredRef.current.isHovered;
62 | });
63 | const clickable =
64 | viewer.useSceneTree(
65 | (state) => state.nodeFromName[message.name]?.clickable,
66 | ) ?? false;
67 |
68 | if (!gltf) return null;
69 |
70 | return (
71 |
72 |
73 | {clickable ? (
74 |
75 | {meshes.map((mesh, i) => (
76 |
81 | ))}
82 |
83 | ) : null}
84 |
85 | );
86 | },
87 | );
88 |
--------------------------------------------------------------------------------
/src/viser/client/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/viser/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "types": [
6 | "vite/client",
7 | "vite-plugin-svgr/client",
8 | "node",
9 | "@types/wicg-file-system-access"
10 | ],
11 | "allowJs": true,
12 | "skipLibCheck": true,
13 | "esModuleInterop": true,
14 | "allowSyntheticDefaultImports": true,
15 | "strict": true,
16 | "forceConsistentCasingInFileNames": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "module": "esnext",
19 | "moduleResolution": "node",
20 | "resolveJsonModule": true,
21 | "isolatedModules": true,
22 | "noEmit": true,
23 | "jsx": "react-jsx"
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/src/viser/client/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/viser/client/vite.config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
4 |
5 | import viteTsconfigPaths from "vite-tsconfig-paths";
6 | import svgrPlugin from "vite-plugin-svgr";
7 | import eslint from "vite-plugin-eslint";
8 | import browserslistToEsbuild from "browserslist-to-esbuild";
9 |
10 | // https://vitejs.dev/config/
11 | export default defineConfig({
12 | plugins: [
13 | react(),
14 | eslint({ failOnError: false, failOnWarning: false }),
15 | viteTsconfigPaths(),
16 | svgrPlugin(),
17 | vanillaExtractPlugin(),
18 | ],
19 | server: {
20 | port: 3000,
21 | hmr: { port: 1025 },
22 | },
23 | worker: {
24 | format: "es",
25 | },
26 | build: {
27 | outDir: "build",
28 | target: browserslistToEsbuild(),
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/src/viser/extras/__init__.py:
--------------------------------------------------------------------------------
1 | """Extra utilities. Used for example scripts."""
2 |
3 | from ._record3d import Record3dFrame as Record3dFrame
4 | from ._record3d import Record3dLoader as Record3dLoader
5 | from ._urdf import ViserUrdf as ViserUrdf
6 |
--------------------------------------------------------------------------------
/src/viser/extras/colmap/__init__.py:
--------------------------------------------------------------------------------
1 | """Colmap utilities."""
2 |
3 | from ._colmap_utils import read_cameras_binary as read_cameras_binary
4 | from ._colmap_utils import read_cameras_text as read_cameras_text
5 | from ._colmap_utils import read_images_binary as read_images_binary
6 | from ._colmap_utils import read_images_text as read_images_text
7 | from ._colmap_utils import read_points3d_binary as read_points3d_binary
8 | from ._colmap_utils import read_points3D_text as read_points3D_text
9 |
--------------------------------------------------------------------------------
/src/viser/infra/__init__.py:
--------------------------------------------------------------------------------
1 | """:mod:`viser.infra` provides WebSocket-based communication infrastructure.
2 |
3 | We implement abstractions for:
4 | - Launching a WebSocket+HTTP server on a shared port.
5 | - Registering callbacks for connection events and incoming messages.
6 | - Asynchronous message sending, both broadcasted and to individual clients.
7 | - Defining dataclass-based message types.
8 | - Translating Python message types to TypeScript interfaces.
9 |
10 | These are what `viser` runs on under-the-hood, and generally won't be useful unless
11 | you're building a web-based application from scratch.
12 | """
13 |
14 | from ._infra import ClientId as ClientId
15 | from ._infra import StateSerializer as StateSerializer
16 | from ._infra import WebsockClientConnection as WebsockClientConnection
17 | from ._infra import WebsockMessageHandler as WebsockMessageHandler
18 | from ._infra import WebsockServer as WebsockServer
19 | from ._messages import Message as Message
20 | from ._typescript_interface_gen import (
21 | TypeScriptAnnotationOverride as TypeScriptAnnotationOverride,
22 | )
23 | from ._typescript_interface_gen import (
24 | generate_typescript_interfaces as generate_typescript_interfaces,
25 | )
26 |
--------------------------------------------------------------------------------
/src/viser/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerfstudio-project/viser/fff4cc162e4780afd350d7f924c66df06ee4c500/src/viser/py.typed
--------------------------------------------------------------------------------
/src/viser/theme/__init__.py:
--------------------------------------------------------------------------------
1 | """:mod:`viser.theme` provides interfaces for themeing the viser
2 | frontend from within Python.
3 | """
4 |
5 | from ._titlebar import TitlebarButton as TitlebarButton
6 | from ._titlebar import TitlebarConfig as TitlebarConfig
7 | from ._titlebar import TitlebarImage as TitlebarImage
8 |
--------------------------------------------------------------------------------
/src/viser/theme/_titlebar.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Optional, Tuple, TypedDict
2 |
3 |
4 | class TitlebarButton(TypedDict):
5 | """A link-only button that appears in the Titlebar."""
6 |
7 | text: Optional[str]
8 | icon: Optional[Literal["GitHub", "Description", "Keyboard"]]
9 | href: Optional[str]
10 |
11 |
12 | class TitlebarImage(TypedDict):
13 | """An image that appears on the titlebar."""
14 |
15 | image_url_light: str
16 | image_url_dark: Optional[str]
17 | image_alt: str
18 | href: Optional[str]
19 |
20 |
21 | class TitlebarConfig(TypedDict):
22 | """Configure the content that appears in the titlebar."""
23 |
24 | buttons: Optional[Tuple[TitlebarButton, ...]]
25 | image: Optional[TitlebarImage]
26 |
--------------------------------------------------------------------------------
/src/viser/transforms/__init__.py:
--------------------------------------------------------------------------------
1 | """Lie group interface for rigid transforms, ported from
2 | `jaxlie `_. Used by `viser` internally and
3 | in examples.
4 |
5 | Implements SO(2), SO(3), SE(2), and SE(3) Lie groups. Rotations are parameterized
6 | via S^1 and S^3.
7 | """
8 |
9 | from ._base import MatrixLieGroup as MatrixLieGroup
10 | from ._base import SEBase as SEBase
11 | from ._base import SOBase as SOBase
12 | from ._se2 import SE2 as SE2
13 | from ._se3 import SE3 as SE3
14 | from ._so2 import SO2 as SO2
15 | from ._so3 import SO3 as SO3
16 |
--------------------------------------------------------------------------------
/src/viser/transforms/hints/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 |
3 | import numpy as np
4 | import numpy.typing as npt
5 |
6 | # Type aliases Numpy arrays; primarily for function inputs.
7 |
8 | Scalar = Union[float, npt.NDArray[np.floating]]
9 | """Type alias for `Union[float, Array]`."""
10 |
11 |
12 | __all__ = [
13 | "Scalar",
14 | ]
15 |
--------------------------------------------------------------------------------
/src/viser/transforms/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from ._utils import broadcast_leading_axes as broadcast_leading_axes
2 | from ._utils import get_epsilon as get_epsilon
3 |
--------------------------------------------------------------------------------
/src/viser/transforms/utils/_utils.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Tuple, TypeVar, Union, cast
2 |
3 | import numpy as onp
4 |
5 | if TYPE_CHECKING:
6 | from .._base import MatrixLieGroup
7 |
8 |
9 | T = TypeVar("T", bound="MatrixLieGroup")
10 |
11 |
12 | def get_epsilon(dtype: onp.dtype) -> float:
13 | """Helper for grabbing type-specific precision constants.
14 |
15 | Args:
16 | dtype: Datatype.
17 |
18 | Returns:
19 | Output float.
20 | """
21 | if dtype == onp.float32:
22 | return 1e-5
23 | elif dtype == onp.float64:
24 | return 1e-10
25 | else:
26 | assert False
27 |
28 |
29 | TupleOfBroadcastable = TypeVar(
30 | "TupleOfBroadcastable",
31 | bound="Tuple[Union[MatrixLieGroup, onp.ndarray], ...]",
32 | )
33 |
34 |
35 | def broadcast_leading_axes(inputs: TupleOfBroadcastable) -> TupleOfBroadcastable:
36 | """Broadcast leading axes of arrays. Takes tuples of either:
37 | - an array, which we assume has shape (*, D).
38 | - a Lie group object."""
39 |
40 | from .._base import MatrixLieGroup
41 |
42 | array_inputs = [
43 | (
44 | (x.parameters(), (x.parameters_dim,))
45 | if isinstance(x, MatrixLieGroup)
46 | else (x, x.shape[-1:])
47 | )
48 | for x in inputs
49 | ]
50 | for array, shape_suffix in array_inputs:
51 | assert array.shape[-len(shape_suffix) :] == shape_suffix
52 | batch_axes = onp.broadcast_shapes(
53 | *[array.shape[: -len(suffix)] for array, suffix in array_inputs]
54 | )
55 | broadcasted_arrays = tuple(
56 | onp.broadcast_to(array, batch_axes + shape_suffix)
57 | for (array, shape_suffix) in array_inputs
58 | )
59 | return cast(
60 | TupleOfBroadcastable,
61 | tuple(
62 | array if not isinstance(inp, MatrixLieGroup) else type(inp)(array)
63 | for array, inp in zip(broadcasted_arrays, inputs)
64 | ),
65 | )
66 |
--------------------------------------------------------------------------------
/sync_client_server.py:
--------------------------------------------------------------------------------
1 | """Synchronize TypeScript definitions with Python.
2 |
3 | This script:
4 | 1. Generates TypeScript message interfaces from Python dataclasses
5 | 2. Creates a version info file to keep client and server versions in sync
6 | """
7 |
8 | import pathlib
9 | import subprocess
10 |
11 | import viser
12 | import viser.infra
13 | from viser._messages import Message
14 |
15 | if __name__ == "__main__":
16 | # Generate TypeScript message interfaces
17 | defs = viser.infra.generate_typescript_interfaces(Message)
18 |
19 | # Write message interfaces to file
20 | target_path = pathlib.Path(__file__).parent / pathlib.Path(
21 | "src/viser/client/src/WebsocketMessages.ts"
22 | )
23 | # Create directory if it doesn't exist
24 | target_path.parent.mkdir(parents=True, exist_ok=True)
25 |
26 | # Write to file even if it doesn't exist yet
27 | target_path.write_text(defs)
28 | print(f"{target_path} updated")
29 |
30 | # Generate version information file for client-server version compatibility checks
31 | version_path = pathlib.Path(__file__).parent / pathlib.Path(
32 | "src/viser/client/src/VersionInfo.ts"
33 | )
34 | # Create directory if it doesn't exist
35 | version_path.parent.mkdir(parents=True, exist_ok=True)
36 |
37 | version_content = f"""// Automatically generated file - do not edit manually.
38 | // This is synchronized with the Python package version in viser/__init__.py.
39 | export const VISER_VERSION = "{viser.__version__}";
40 | """
41 | version_path.write_text(version_content)
42 | print(f"Version info generated: {version_path}")
43 |
44 | # Run prettier on both files if it's available
45 | try:
46 | subprocess.run(
47 | args=["npx", "prettier", "-w", str(target_path), str(version_path)],
48 | check=False,
49 | )
50 | except FileNotFoundError:
51 | print("Warning: npx/prettier not found, skipping formatting")
52 |
53 | print("Synchronization complete")
54 |
--------------------------------------------------------------------------------
/tests/test_garbage_collection.py:
--------------------------------------------------------------------------------
1 | import viser
2 | import viser._client_autobuild
3 |
4 |
5 | def test_remove_scene_node() -> None:
6 | """Test that viser's internal message buffer is cleaned up properly when we
7 | remove scene nodes."""
8 |
9 | # def test_server_port_is_freed():
10 | # Mock the client autobuild to avoid building the client.
11 | viser._client_autobuild.ensure_client_is_built = lambda: None
12 |
13 | server = viser.ViserServer()
14 |
15 | internal_message_dict = server._websock_server._broadcast_buffer.message_from_id
16 | orig_len = len(internal_message_dict)
17 |
18 | for i in range(50):
19 | server.scene.add_frame(f"/frame_{i}")
20 |
21 | assert len(internal_message_dict) > orig_len
22 | server.scene.reset()
23 | assert len(internal_message_dict) > orig_len
24 | server._run_garbage_collector(force=True)
25 | assert len(internal_message_dict) == orig_len
26 |
27 |
28 | def test_remove_gui_element() -> None:
29 | """Test that viser's internal message buffer is cleaned up properly when we
30 | remove GUI elements."""
31 |
32 | # def test_server_port_is_freed():
33 | # Mock the client autobuild to avoid building the client.
34 | viser._client_autobuild.ensure_client_is_built = lambda: None
35 |
36 | server = viser.ViserServer()
37 |
38 | internal_message_dict = server._websock_server._broadcast_buffer.message_from_id
39 | orig_len = len(internal_message_dict)
40 |
41 | for i in range(50):
42 | server.gui.add_button(f"Button {i}")
43 |
44 | with server.gui.add_folder("Buttons in folder"):
45 | for i in range(50):
46 | server.gui.add_button(f"Button {i}")
47 |
48 | assert len(internal_message_dict) > orig_len
49 | server.gui.reset()
50 | assert len(internal_message_dict) > orig_len
51 | server._run_garbage_collector(force=True)
52 | assert len(internal_message_dict) == orig_len
53 |
54 |
55 | def test_remove_gui_in_modal() -> None:
56 | """Test that viser's internal message buffer is cleaned up properly when we
57 | remove GUI elements."""
58 |
59 | # def test_server_port_is_freed():
60 | # Mock the client autobuild to avoid building the client.
61 | viser._client_autobuild.ensure_client_is_built = lambda: None
62 |
63 | server = viser.ViserServer()
64 |
65 | internal_message_dict = server._websock_server._broadcast_buffer.message_from_id
66 | orig_len = len(internal_message_dict)
67 |
68 | with server.gui.add_modal("Buttons in folder") as modal:
69 | for i in range(50):
70 | server.gui.add_button(f"Button {i}")
71 |
72 | assert len(internal_message_dict) > orig_len
73 | modal.close()
74 | assert len(internal_message_dict) > orig_len
75 | server._run_garbage_collector(force=True)
76 | assert len(internal_message_dict) == orig_len
77 |
--------------------------------------------------------------------------------
/tests/test_message_annotations.py:
--------------------------------------------------------------------------------
1 | from dataclasses import is_dataclass
2 | from typing import get_type_hints
3 |
4 | from viser.infra._messages import Message
5 |
6 |
7 | def test_get_annotations() -> None:
8 | """Check that we can read the type annotations from all messages.
9 |
10 | This is to guard against use of Python annotations that can't be inspected at runtime.
11 | We could also use `eval_type_backport`: https://github.com/alexmojaki/eval_type_backport
12 | """
13 |
14 | def recursive_get_type_hints(cls: type) -> None:
15 | try:
16 | hints = get_type_hints(cls)
17 | except TypeError as e:
18 | raise TypeError(f"Failed to get type hints for {cls}") from e
19 |
20 | assert hints is not None
21 | for hint in hints.values():
22 | if is_dataclass(hint):
23 | recursive_get_type_hints(hint) # type: ignore
24 |
25 | for cls in Message.get_subclasses():
26 | recursive_get_type_hints(cls)
27 |
--------------------------------------------------------------------------------
/tests/test_server_stop.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import time
3 |
4 | import viser
5 | import viser._client_autobuild
6 |
7 |
8 | def test_server_port_is_freed():
9 | # Mock the client autobuild to avoid building the client.
10 | viser._client_autobuild.ensure_client_is_built = lambda: None
11 |
12 | server = viser.ViserServer()
13 | original_port = server.get_port()
14 |
15 | # Assert that the port is not free.
16 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
17 | result = sock.connect_ex(("localhost", original_port))
18 | assert result == 0
19 | sock.close()
20 | server.stop()
21 |
22 | time.sleep(0.05)
23 |
24 | # Assert that the port is now free.
25 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
26 | result = sock.connect_ex(("localhost", original_port))
27 | assert result != 0
28 |
--------------------------------------------------------------------------------
/tests/test_version_sync.py:
--------------------------------------------------------------------------------
1 | """Test that the client and server versions match and subprotocols work."""
2 |
3 | import re
4 | from pathlib import Path
5 |
6 | import pytest
7 |
8 |
9 | def test_versions_match():
10 | """Verify that the server version in __init__.py matches the client version in VersionInfo.ts."""
11 | # Get project root directory
12 | repo_root = Path(__file__).parent.parent
13 |
14 | # Read Python version from __init__.py
15 | init_path = repo_root / "src" / "viser" / "__init__.py"
16 | assert init_path.exists(), "Could not find __init__.py"
17 |
18 | with open(init_path, "r") as f:
19 | init_content = f.read()
20 |
21 | # Extract the version using regex
22 | py_version_match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', init_content)
23 | assert py_version_match, "Could not find __version__ in __init__.py"
24 | py_version = py_version_match.group(1)
25 |
26 | # Read TypeScript version from VersionInfo.ts
27 | version_info_path = (
28 | repo_root / "src" / "viser" / "client" / "src" / "VersionInfo.ts"
29 | )
30 |
31 | # Skip test if file doesn't exist (might be running in a context where it hasn't been generated)
32 | if not version_info_path.exists():
33 | pytest.skip(f"VersionInfo.ts not found at {version_info_path}")
34 |
35 | with open(version_info_path, "r") as f:
36 | ts_content = f.read()
37 |
38 | # Extract the TypeScript version using regex
39 | ts_version_match = re.search(r'VISER_VERSION\s*=\s*["\']([^"\']+)["\']', ts_content)
40 | assert ts_version_match, "Could not find VISER_VERSION in VersionInfo.ts"
41 | ts_version = ts_version_match.group(1)
42 |
43 | # Verify versions match
44 | assert py_version == ts_version, (
45 | f"Version mismatch: {py_version} in __init__.py does not match "
46 | f"{ts_version} in VersionInfo.ts. Run 'python sync_client_server.py' to update."
47 | )
48 |
--------------------------------------------------------------------------------