├── src └── viser │ ├── py.typed │ ├── scripts │ ├── __init__.py │ └── dev_checks.py │ ├── client │ ├── vite-env.d.ts │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── components │ │ │ ├── MultiSliderPrimitive │ │ │ │ ├── index.ts │ │ │ │ ├── utils │ │ │ │ │ ├── get-floating-value │ │ │ │ │ │ └── get-gloating-value.ts │ │ │ │ │ ├── get-precision │ │ │ │ │ │ └── get-precision.ts │ │ │ │ │ ├── get-client-position │ │ │ │ │ │ └── get-client-position.ts │ │ │ │ │ ├── get-position │ │ │ │ │ │ └── get-position.ts │ │ │ │ │ └── get-change-value │ │ │ │ │ │ └── get-change-value.ts │ │ │ │ ├── Slider.context.ts │ │ │ │ ├── SliderRoot │ │ │ │ │ └── SliderRoot.tsx │ │ │ │ ├── LICENSE │ │ │ │ ├── Track │ │ │ │ │ └── Track.tsx │ │ │ │ ├── Marks │ │ │ │ │ └── Marks.tsx │ │ │ │ └── Thumb │ │ │ │ │ └── Thumb.tsx │ │ │ ├── ProgressBar.tsx │ │ │ ├── Markdown.tsx │ │ │ ├── Folder.css.ts │ │ │ ├── Vector2.tsx │ │ │ ├── Vector3.tsx │ │ │ ├── ComponentStyles.css.ts │ │ │ ├── TextInput.tsx │ │ │ ├── Rgba.tsx │ │ │ ├── Rgb.tsx │ │ │ ├── ButtonGroup.tsx │ │ │ ├── utils.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── Button.tsx │ │ │ ├── Dropdown.tsx │ │ │ ├── NumberInput.tsx │ │ │ ├── TabGroup.tsx │ │ │ ├── Folder.tsx │ │ │ ├── MultiSlider.tsx │ │ │ ├── Slider.tsx │ │ │ └── PlotlyComponent.tsx │ │ ├── Splatting │ │ │ ├── WasmSorter │ │ │ │ ├── Sorter.wasm │ │ │ │ └── build.sh │ │ │ └── SplatSortWorker.ts │ │ ├── App.css.ts │ │ ├── index.tsx │ │ ├── Utils.ts │ │ ├── ControlPanel │ │ │ ├── SceneTreeTable.css.ts │ │ │ ├── GuiComponentContext.tsx │ │ │ └── BottomPanel.tsx │ │ ├── Modal.tsx │ │ ├── SearchParamsUtils.tsx │ │ ├── index.css │ │ ├── AppTheme.ts │ │ ├── ClickUtils.tsx │ │ ├── WorldTransformUtils.ts │ │ ├── BrowserWarning.tsx │ │ ├── WebsocketFunctions.tsx │ │ ├── WebsocketInterface.tsx │ │ └── WebsocketServerWorker.ts │ ├── public │ │ ├── robots.txt │ │ ├── hdri │ │ │ └── potsdamer_platz_1k.hdr │ │ ├── Inter-VariableFont_slnt,wght.ttf │ │ ├── manifest.json │ │ └── logo.svg │ ├── .gitignore │ ├── postcss.config.cjs │ ├── tsconfig.json │ ├── vite.config.mts │ ├── index.html │ ├── .eslintrc.js │ └── package.json │ ├── transforms │ ├── utils │ │ ├── __init__.py │ │ └── _utils.py │ ├── hints │ │ └── __init__.py │ └── __init__.py │ ├── theme │ ├── __init__.py │ └── _titlebar.py │ ├── extras │ ├── __init__.py │ └── colmap │ │ └── __init__.py │ ├── _icons.py │ ├── infra │ └── __init__.py │ ├── __init__.py │ ├── _icons_generate_enum.py │ └── _notification_handle.py ├── docs ├── .gitignore ├── source │ ├── _static │ │ ├── logo.svg │ │ └── css │ │ │ └── custom.css │ ├── server.md │ ├── extras.md │ ├── transforms.md │ ├── infrastructure.md │ ├── gui_api.md │ ├── scene_api.md │ ├── camera_handles.md │ ├── icons.md │ ├── client_handles.md │ ├── events.md │ ├── gui_handles.md │ ├── examples │ │ ├── 21_set_up_direction.rst │ │ ├── 17_background_composite.rst │ │ ├── 14_markdown.rst │ │ ├── 06_mesh.rst │ │ ├── 18_splines.rst │ │ ├── 00_coordinate_frames.rst │ │ ├── 16_modal.rst │ │ ├── 04_camera_poses.rst │ │ ├── 19_get_renders.rst │ │ ├── 01_image.rst │ │ ├── 23_plotly.rst │ │ ├── 05_camera_commands.rst │ │ ├── 12_click_meshes.rst │ │ ├── 09_urdf_visualizer.rst │ │ └── 24_notification.rst │ ├── scene_handles.md │ ├── conventions.md │ ├── index.md │ ├── _templates │ │ └── sidebar │ │ │ └── brand.html │ └── development.md ├── requirements.txt ├── Makefile └── update_example_docs.py ├── .prettierignore ├── examples ├── assets │ ├── .gitignore │ ├── Cal_logo.png │ ├── download_dragon_mesh.sh │ ├── download_record3d_dance.sh │ ├── download_colmap_garden.sh │ └── mdx_example.mdx ├── .vscode │ └── settings.json ├── 21_set_up_direction.py ├── 17_background_composite.py ├── 14_markdown.py ├── 06_mesh.py ├── 18_splines.py ├── 00_coordinate_frames.py ├── 16_modal.py ├── 04_camera_poses.py ├── 19_get_renders.py ├── 01_image.py ├── 23_plotly.py ├── 05_camera_commands.py ├── 12_click_meshes.py ├── 24_notification.py ├── 13_theming.py ├── 09_urdf_visualizer.py ├── 03_gui_callbacks.py ├── 15_gui_in_scene.py └── 10_realsense.py ├── .clang-format ├── .gitignore ├── .pre-commit-config.yaml ├── sync_message_defs.py ├── README.md └── pyproject.toml /src/viser/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /src/viser/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.mjs 2 | build/ 3 | -------------------------------------------------------------------------------- /docs/source/_static/logo.svg: -------------------------------------------------------------------------------- 1 | ../../../src/viser/client/public/logo.svg -------------------------------------------------------------------------------- /src/viser/client/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/assets/.gitignore: -------------------------------------------------------------------------------- 1 | dragon.obj 2 | /record3d_dance/ 3 | /colmap_garden/ 4 | -------------------------------------------------------------------------------- /src/viser/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | img.sidebar-logo { 2 | width: 5em; 3 | margin: 1em 0 0 0; 4 | } 5 | -------------------------------------------------------------------------------- /examples/assets/Cal_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Junyi42/viser/HEAD/examples/assets/Cal_logo.png -------------------------------------------------------------------------------- /src/viser/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/viser/client/src/components/MultiSliderPrimitive/index.ts: -------------------------------------------------------------------------------- 1 | export { MultiSlider } from "./MultiSlider/MultiSlider"; 2 | -------------------------------------------------------------------------------- /examples/assets/download_dragon_mesh.sh: -------------------------------------------------------------------------------- 1 | set -e -x 2 | 3 | gdown "https://drive.google.com/uc?id=1uRDvoS_l2Or8g8YDDPYV79K6_RfFYBeF" 4 | -------------------------------------------------------------------------------- /src/viser/client/public/hdri/potsdamer_platz_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Junyi42/viser/HEAD/src/viser/client/public/hdri/potsdamer_platz_1k.hdr -------------------------------------------------------------------------------- /src/viser/client/src/Splatting/WasmSorter/Sorter.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Junyi42/viser/HEAD/src/viser/client/src/Splatting/WasmSorter/Sorter.wasm -------------------------------------------------------------------------------- /docs/source/server.md: -------------------------------------------------------------------------------- 1 | # Viser Server 2 | 3 | 4 | 5 | .. autoclass:: viser.ViserServer 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/viser/client/public/Inter-VariableFont_slnt,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Junyi42/viser/HEAD/src/viser/client/public/Inter-VariableFont_slnt,wght.ttf -------------------------------------------------------------------------------- /docs/source/extras.md: -------------------------------------------------------------------------------- 1 | # Record3D + URDF Helpers 2 | 3 | 4 | 5 | .. automodule:: viser.extras 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none" 6 | } 7 | -------------------------------------------------------------------------------- /docs/source/transforms.md: -------------------------------------------------------------------------------- 1 | # Transforms 2 | 3 | 4 | 5 | .. automodule:: viser.transforms 6 | :show-inheritance: 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/source/infrastructure.md: -------------------------------------------------------------------------------- 1 | # Communication 2 | 3 | 4 | 5 | .. automodule:: viser.infra 6 | :show-inheritance: 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/viser/transforms/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from ._utils import broadcast_leading_axes, get_epsilon, register_lie_group 2 | 3 | __all__ = ["get_epsilon", "register_lie_group", "broadcast_leading_axes"] 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /docs/source/gui_api.md: -------------------------------------------------------------------------------- 1 | # GUI API 2 | 3 | 4 | 5 | .. autoclass:: viser.GuiApi 6 | :members: 7 | :undoc-members: 8 | :inherited-members: 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/viser/client/src/components/MultiSliderPrimitive/utils/get-floating-value/get-gloating-value.ts: -------------------------------------------------------------------------------- 1 | export function getFloatingValue(value: number, precision: number) { 2 | return parseFloat(value.toFixed(precision)); 3 | } 4 | -------------------------------------------------------------------------------- /docs/source/scene_api.md: -------------------------------------------------------------------------------- 1 | # Scene API 2 | 3 | 4 | 5 | .. autoclass:: viser.SceneApi 6 | :members: 7 | :undoc-members: 8 | :inherited-members: 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/source/camera_handles.md: -------------------------------------------------------------------------------- 1 | # Camera Handles 2 | 3 | 4 | 5 | .. autoclass:: viser.CameraHandle 6 | :members: 7 | :undoc-members: 8 | :inherited-members: 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/viser/client/src/components/MultiSliderPrimitive/utils/get-precision/get-precision.ts: -------------------------------------------------------------------------------- 1 | export function getPrecision(step: number) { 2 | if (!step) return 0; 3 | const split = step.toString().split("."); 4 | return split.length > 1 ? split[1].length : 0; 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.2.6 2 | furo==2023.9.10 3 | docutils==0.20.1 4 | m2r2==0.3.3.post2 5 | toml==0.10.2 6 | git+https://github.com/brentyi/sphinxcontrib-programoutput.git 7 | git+https://github.com/brentyi/ansi.git 8 | git+https://github.com/sphinx-contrib/googleanalytics.git 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/source/icons.md: -------------------------------------------------------------------------------- 1 | # Icons 2 | 3 | Icons for GUI elements (such as :meth:`GuiApi.add_button()`) can be 4 | specified using the :class:`viser.Icon` enum. 5 | 6 | 7 | 8 | .. autoclass:: viser.IconName 9 | .. autoclass:: viser.Icon 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | record3d_dance -------------------------------------------------------------------------------- /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 ._record3d_customized import Record3dLoader_Customized as Record3dLoader_Customized 6 | from ._urdf import ViserUrdf as ViserUrdf 7 | -------------------------------------------------------------------------------- /src/viser/client/src/components/MultiSliderPrimitive/utils/get-client-position/get-client-position.ts: -------------------------------------------------------------------------------- 1 | export function getClientPosition(event: any) { 2 | if ("TouchEvent" in window && event instanceof window.TouchEvent) { 3 | const touch = event.touches[0]; 4 | return touch.clientX; 5 | } 6 | 7 | return event.clientX; 8 | } 9 | -------------------------------------------------------------------------------- /docs/source/client_handles.md: -------------------------------------------------------------------------------- 1 | # Client Handles 2 | 3 | 4 | 5 | .. autoclass:: viser.ClientHandle 6 | :members: 7 | :undoc-members: 8 | :inherited-members: 9 | 10 | .. autoclass:: viser.NotificationHandle 11 | :members: 12 | :undoc-members: 13 | :inherited-members: 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/viser/transforms/hints/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import numpy as onp 4 | import numpy.typing as onpt 5 | 6 | # Type aliases Numpy arrays; primarily for function inputs. 7 | 8 | Scalar = Union[float, onpt.NDArray[onp.floating]] 9 | """Type alias for `Union[float, Array]`.""" 10 | 11 | 12 | __all__ = [ 13 | "Scalar", 14 | ] 15 | -------------------------------------------------------------------------------- /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/src/components/MultiSliderPrimitive/utils/get-position/get-position.ts: -------------------------------------------------------------------------------- 1 | interface GetPosition { 2 | value: number; 3 | min: number; 4 | max: number; 5 | } 6 | 7 | export function getPosition({ value, min, max }: GetPosition) { 8 | const position = ((value - min) / (max - min)) * 100; 9 | return Math.min(Math.max(position, 0), 100); 10 | } 11 | -------------------------------------------------------------------------------- /docs/source/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | We define a small set of event types, which are passed to callback functions 4 | when events like clicks or GUI updates are triggered. 5 | 6 | 7 | 8 | .. autoclass:: viser.ScenePointerEvent() 9 | 10 | .. autoclass:: viser.SceneNodePointerEvent() 11 | 12 | .. autoclass:: viser.GuiEvent() 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /src/viser/client/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-preset-mantine": {}, 4 | "postcss-simple-vars": { 5 | variables: { 6 | "mantine-breakpoint-xs": "36em", 7 | "mantine-breakpoint-sm": "48em", 8 | "mantine-breakpoint-md": "62em", 9 | "mantine-breakpoint-lg": "75em", 10 | "mantine-breakpoint-xl": "88em", 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/source/gui_handles.md: -------------------------------------------------------------------------------- 1 | # GUI Handles 2 | 3 | 4 | 5 | .. autoclass:: viser.GuiInputHandle() 6 | 7 | .. autoclass:: viser.GuiButtonHandle() 8 | 9 | .. autoclass:: viser.GuiButtonGroupHandle() 10 | 11 | .. autoclass:: viser.GuiDropdownHandle() 12 | 13 | .. autoclass:: viser.GuiFolderHandle() 14 | 15 | .. autoclass:: viser.GuiMarkdownHandle() 16 | 17 | .. autoclass:: viser.GuiPlotlyHandle() 18 | 19 | .. autoclass:: viser.GuiTabGroupHandle() 20 | 21 | .. autoclass:: viser.GuiTabHandle() 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/viser/transforms/__init__.py: -------------------------------------------------------------------------------- 1 | """Lie group interface for rigid transforms, ported from 2 | [jaxlie](https://github.com/brentyi/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/client/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/viser/client/src/components/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Progress } from "@mantine/core"; 2 | import { GuiAddProgressBarMessage } from "../WebsocketMessages"; 3 | 4 | export default function ProgressBarComponent({ 5 | visible, 6 | color, 7 | value, 8 | animated, 9 | }: GuiAddProgressBarMessage) { 10 | if (!visible) return <>; 11 | return ( 12 | 13 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/viser/client/src/Utils.ts: -------------------------------------------------------------------------------- 1 | // Drag Utils 2 | export interface DragEvents { 3 | move: "touchmove" | "mousemove"; 4 | end: "touchend" | "mouseup"; 5 | } 6 | export const touchEvents: DragEvents = { move: "touchmove", end: "touchend" }; 7 | export const mouseEvents: DragEvents = { move: "mousemove", end: "mouseup" }; 8 | 9 | export function isTouchEvent( 10 | event: TouchEvent | MouseEvent, 11 | ): event is TouchEvent { 12 | return event.type === "touchmove"; 13 | } 14 | export function isMouseEvent( 15 | event: TouchEvent | MouseEvent, 16 | ): event is MouseEvent { 17 | return event.type === "mousemove"; 18 | } 19 | -------------------------------------------------------------------------------- /src/viser/client/src/ControlPanel/SceneTreeTable.css.ts: -------------------------------------------------------------------------------- 1 | import { style } from "@vanilla-extract/css"; 2 | import { vars } from "../AppTheme"; 3 | 4 | export const tableWrapper = style({ 5 | border: "1px solid", 6 | borderColor: vars.colors.defaultBorder, 7 | borderRadius: vars.radius.xs, 8 | }); 9 | export const icon = style({ 10 | opacity: 0.5, 11 | height: "1em", 12 | width: "1em", 13 | transform: "translateY(0.1em)", 14 | }); 15 | 16 | export const tableRow = style({ 17 | display: "flex", 18 | alignItems: "center", 19 | gap: "0.4em", 20 | padding: "0 0.25em", 21 | lineHeight: "2.25em", 22 | fontSize: "0.875em", 23 | }); 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/viser/_icons.py: -------------------------------------------------------------------------------- 1 | import tarfile 2 | from pathlib import Path 3 | 4 | from ._icons_enum import IconName 5 | 6 | ICONS_DIR = Path(__file__).absolute().parent / "_icons" 7 | 8 | 9 | def svg_from_icon(icon_name: IconName) -> str: 10 | """Read an icon and return it as a UTF string; we expect this to be an 11 | tag.""" 12 | assert isinstance(icon_name, str) 13 | icons_tarball = ICONS_DIR / "tabler-icons.tar" 14 | 15 | with tarfile.open(icons_tarball) as tar: 16 | icon_file = tar.extractfile(f"{icon_name}.svg") 17 | assert icon_file is not None 18 | out = icon_file.read() 19 | 20 | return out.decode("utf-8") 21 | -------------------------------------------------------------------------------- /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<{ containerId: 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/components/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from "@mantine/core"; 2 | import Markdown from "../Markdown"; 3 | import { ErrorBoundary } from "react-error-boundary"; 4 | import { GuiAddMarkdownMessage } from "../WebsocketMessages"; 5 | 6 | export default function MarkdownComponent({ 7 | visible, 8 | markdown, 9 | }: GuiAddMarkdownMessage) { 10 | if (!visible) return <>; 11 | return ( 12 | 13 | Markdown Failed to Render} 15 | > 16 | {markdown} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sync_message_defs.py: -------------------------------------------------------------------------------- 1 | """Generate typescript message definitions from Python dataclasses.""" 2 | 3 | import pathlib 4 | import subprocess 5 | 6 | import viser.infra 7 | from viser._messages import Message 8 | 9 | if __name__ == "__main__": 10 | # Generate typescript source. 11 | defs = viser.infra.generate_typescript_interfaces(Message) 12 | 13 | # Write to file. 14 | target_path = pathlib.Path(__file__).parent / pathlib.Path( 15 | "src/viser/client/src/WebsocketMessages.tsx" 16 | ) 17 | assert target_path.exists() 18 | target_path.write_text(defs) 19 | print(f"Wrote to {target_path}") 20 | 21 | # Run prettier. 22 | subprocess.run(args=["npx", "prettier", "-w", str(target_path)], check=False) 23 | -------------------------------------------------------------------------------- /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/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/src/components/MultiSliderPrimitive/Slider.context.ts: -------------------------------------------------------------------------------- 1 | import { createSafeContext, GetStylesApi } from "@mantine/core"; 2 | 3 | export type SliderStylesNames = 4 | | "root" 5 | | "label" 6 | | "thumb" 7 | | "trackContainer" 8 | | "track" 9 | | "bar" 10 | | "markWrapper" 11 | | "mark" 12 | | "markLabel"; 13 | 14 | export type SliderCssVariables = { 15 | root: 16 | | "--slider-size" 17 | | "--slider-color" 18 | | "--slider-thumb-size" 19 | | "--slider-radius"; 20 | }; 21 | 22 | interface SliderContextValue { 23 | getStyles: GetStylesApi<{ 24 | stylesNames: SliderStylesNames; 25 | props: any; 26 | ref: any; 27 | vars: any; 28 | variant: any; 29 | }>; 30 | } 31 | 32 | export const [SliderProvider, useSliderContext] = 33 | createSafeContext("SliderProvider was not found in tree"); 34 | -------------------------------------------------------------------------------- /src/viser/client/src/components/MultiSliderPrimitive/utils/get-change-value/get-change-value.ts: -------------------------------------------------------------------------------- 1 | interface GetChangeValue { 2 | value: number; 3 | containerWidth?: number; 4 | min: number; 5 | max: number; 6 | step: number; 7 | precision?: number; 8 | } 9 | 10 | export function getChangeValue({ 11 | value, 12 | containerWidth, 13 | min, 14 | max, 15 | step, 16 | precision, 17 | }: GetChangeValue) { 18 | const left = !containerWidth 19 | ? value 20 | : Math.min(Math.max(value, 0), containerWidth) / containerWidth; 21 | const dx = left * (max - min); 22 | const nextValue = (dx !== 0 ? Math.round(dx / step) * step : 0) + min; 23 | 24 | const nextValueWithinStep = Math.max(nextValue, min); 25 | 26 | if (precision !== undefined) { 27 | return Number(nextValueWithinStep.toFixed(precision)); 28 | } 29 | 30 | return nextValueWithinStep; 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 onp 10 | import trimesh 11 | import trimesh.creation 12 | 13 | import viser 14 | 15 | server = viser.ViserServer() 16 | 17 | 18 | img = onp.random.randint(0, 255, size=(1000, 1000, 3), dtype=onp.uint8) 19 | depth = onp.ones((1000, 1000, 1), dtype=onp.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 | -------------------------------------------------------------------------------- /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 | position: "relative", 6 | marginTop: vars.spacing.xs, 7 | marginLeft: vars.spacing.xs, 8 | marginRight: vars.spacing.xs, 9 | marginBottom: vars.spacing.md, 10 | ":last-child": { 11 | marginBottom: vars.spacing.xs, 12 | }, 13 | }); 14 | 15 | export const folderLabel = style({ 16 | fontSize: "0.875em", 17 | position: "absolute", 18 | padding: "0 0.375em 0 0.375em", 19 | top: 0, 20 | left: "0.375em", 21 | transform: "translateY(-50%)", 22 | userSelect: "none", 23 | fontWeight: 500, 24 | }); 25 | 26 | export const folderToggleIcon = style({ 27 | width: "0.9em", 28 | height: "0.9em", 29 | strokeWidth: 3, 30 | top: "0.1em", 31 | position: "relative", 32 | marginLeft: "0.25em", 33 | marginRight: "-0.1em", 34 | opacity: 0.5, 35 | }); 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/viser/client/src/components/Vector2.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; 3 | import { GuiAddVector2Message } from "../WebsocketMessages"; 4 | import { VectorInput, ViserInputComponent } from "./common"; 5 | 6 | export default function Vector2Component({ 7 | id, 8 | hint, 9 | label, 10 | visible, 11 | disabled, 12 | value, 13 | ...otherProps 14 | }: GuiAddVector2Message) { 15 | const { min, max, step, precision } = otherProps; 16 | const { setValue } = React.useContext(GuiComponentContext)!; 17 | if (!visible) return <>; 18 | return ( 19 | 20 | setValue(id, value)} 25 | min={min} 26 | max={max} 27 | step={step} 28 | precision={precision} 29 | disabled={disabled} 30 | /> 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/viser/client/src/components/Vector3.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; 3 | import { GuiAddVector3Message } from "../WebsocketMessages"; 4 | import { VectorInput, ViserInputComponent } from "./common"; 5 | 6 | export default function Vector3Component({ 7 | id, 8 | hint, 9 | label, 10 | visible, 11 | disabled, 12 | value, 13 | ...otherProps 14 | }: GuiAddVector3Message) { 15 | const { min, max, step, precision } = otherProps; 16 | const { setValue } = React.useContext(GuiComponentContext)!; 17 | if (!visible) return <>; 18 | return ( 19 | 20 | setValue(id, value)} 25 | min={min} 26 | max={max} 27 | step={step} 28 | precision={precision} 29 | disabled={disabled} 30 | /> 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /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 onp 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(onp.pi / 2).wxyz, 29 | position=(0.0, 0.0, 0.0), 30 | ) 31 | server.scene.add_mesh_trimesh( 32 | name="/trimesh", 33 | mesh=mesh.smoothed(), 34 | wxyz=tf.SO3.from_x_radians(onp.pi / 2).wxyz, 35 | position=(0.0, 5.0, 0.0), 36 | ) 37 | 38 | while True: 39 | time.sleep(10.0) 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/18_splines.py: -------------------------------------------------------------------------------- 1 | """Splines 2 | 3 | Make a ball with some random splines. 4 | """ 5 | 6 | import time 7 | 8 | import numpy as onp 9 | 10 | import viser 11 | 12 | 13 | def main() -> None: 14 | server = viser.ViserServer() 15 | for i in range(10): 16 | positions = onp.random.normal(size=(30, 3)) * 3.0 17 | server.scene.add_spline_catmull_rom( 18 | f"/catmull_{i}", 19 | positions, 20 | tension=0.5, 21 | line_width=3.0, 22 | color=onp.random.uniform(size=3), 23 | segments=100, 24 | ) 25 | 26 | control_points = onp.random.normal(size=(30 * 2 - 2, 3)) * 3.0 27 | server.scene.add_spline_cubic_bezier( 28 | f"/cubic_bezier_{i}", 29 | positions, 30 | control_points, 31 | line_width=3.0, 32 | color=onp.random.uniform(size=3), 33 | segments=100, 34 | ) 35 | 36 | while True: 37 | time.sleep(10.0) 38 | 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/viser/client/src/components/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { TextInput } from "@mantine/core"; 3 | import { ViserInputComponent } from "./common"; 4 | import { GuiAddTextMessage } from "../WebsocketMessages"; 5 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; 6 | 7 | export default function TextInputComponent(props: GuiAddTextMessage) { 8 | const { id, hint, label, value, disabled, visible } = props; 9 | const { setValue } = React.useContext(GuiComponentContext)!; 10 | if (!visible) return <>; 11 | return ( 12 | 13 | { 17 | setValue(id, value.target.value); 18 | }} 19 | styles={{ 20 | input: { 21 | minHeight: "1.625rem", 22 | height: "1.625rem", 23 | padding: "0 0.5em", 24 | }, 25 | }} 26 | disabled={disabled} 27 | /> 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 WebsockClientConnection as WebsockClientConnection 16 | from ._infra import WebsockMessageHandler as WebsockMessageHandler 17 | from ._infra import WebsockServer as WebsockServer 18 | from ._messages import Message as Message 19 | from ._typescript_interface_gen import ( 20 | TypeScriptAnnotationOverride as TypeScriptAnnotationOverride, 21 | ) 22 | from ._typescript_interface_gen import ( 23 | generate_typescript_interfaces as generate_typescript_interfaces, 24 | ) 25 | -------------------------------------------------------------------------------- /src/viser/client/src/components/MultiSliderPrimitive/SliderRoot/SliderRoot.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | import { 3 | Box, 4 | BoxProps, 5 | ElementProps, 6 | MantineColor, 7 | MantineRadius, 8 | MantineSize, 9 | } from "@mantine/core"; 10 | import { useSliderContext } from "../Slider.context"; 11 | 12 | export interface SliderRootProps extends BoxProps, ElementProps<"div"> { 13 | size: MantineSize | (string & NonNullable) | number; 14 | children: React.ReactNode; 15 | color: MantineColor | undefined; 16 | disabled: boolean | undefined; 17 | variant?: string; 18 | thumbSize: string | number | undefined; 19 | radius: MantineRadius | undefined; 20 | } 21 | 22 | export const SliderRoot = forwardRef( 23 | ({ size, variant, ...others }: SliderRootProps, ref) => { 24 | const { getStyles } = useSliderContext(); 25 | 26 | return ( 27 | 35 | ); 36 | }, 37 | ); 38 | 39 | SliderRoot.displayName = "@mantine/core/SliderRoot"; 40 | -------------------------------------------------------------------------------- /docs/source/scene_handles.md: -------------------------------------------------------------------------------- 1 | # Scene Handles 2 | 3 | A handle is created for each object that is added to the scene. These can be 4 | used to read and set state, as well as detect clicks. 5 | 6 | When a scene node is added to a server (for example, via 7 | :func:`viser.ViserServer.add_frame()`), state is synchronized between all 8 | connected clients. When a scene node is added to a client (for example, via 9 | :func:`viser.ClientHandle.add_frame()`), state is local to a specific client. 10 | 11 | 12 | 13 | .. autoclass:: viser.SceneNodeHandle 14 | 15 | .. autoclass:: viser.CameraFrustumHandle 16 | 17 | .. autoclass:: viser.FrameHandle 18 | 19 | .. autoclass:: viser.BatchedAxesHandle 20 | 21 | .. autoclass:: viser.GlbHandle 22 | 23 | .. autoclass:: viser.Gui3dContainerHandle 24 | 25 | .. autoclass:: viser.ImageHandle 26 | 27 | .. autoclass:: viser.LabelHandle 28 | 29 | .. autoclass:: viser.MeshHandle 30 | 31 | .. autoclass:: viser.MeshSkinnedHandle 32 | 33 | .. autoclass:: viser.MeshSkinnedBoneHandle 34 | 35 | .. autoclass:: viser.PointCloudHandle 36 | 37 | .. autoclass:: viser.TransformControlsHandle 38 | 39 | .. autoclass:: viser.GaussianSplatHandle 40 | 41 | 42 | -------------------------------------------------------------------------------- /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 { rgbaToHex, hexToRgba } from "./utils"; 5 | import { ViserInputComponent } from "./common"; 6 | import { GuiAddRgbaMessage } from "../WebsocketMessages"; 7 | 8 | export default function RgbaComponent({ 9 | id, 10 | label, 11 | hint, 12 | value, 13 | disabled, 14 | visible, 15 | }: GuiAddRgbaMessage) { 16 | const { setValue } = React.useContext(GuiComponentContext)!; 17 | if (!visible) return <>; 18 | return ( 19 | 20 | setValue(id, hexToRgba(v))} 25 | format="hexa" 26 | // zIndex of dropdown should be >modal zIndex. 27 | // On edge cases: it seems like existing dropdowns are always closed when a new modal is opened. 28 | popoverProps={{ zIndex: 1000 }} 29 | styles={{ 30 | input: { height: "1.625rem", minHeight: "1.625rem" }, 31 | }} 32 | /> 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/viser/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | Viser 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /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 { rgbToHex, hexToRgb } from "./utils"; 5 | import { ViserInputComponent } from "./common"; 6 | import { GuiAddRgbMessage } from "../WebsocketMessages"; 7 | 8 | export default function RgbComponent({ 9 | id, 10 | label, 11 | hint, 12 | value, 13 | disabled, 14 | visible, 15 | }: GuiAddRgbMessage) { 16 | const { setValue } = React.useContext(GuiComponentContext)!; 17 | if (!visible) return <>; 18 | return ( 19 | 20 | setValue(id, hexToRgb(v))} 25 | format="hex" 26 | // zIndex of dropdown should be >modal zIndex. 27 | // On edge cases: it seems like existing dropdowns are always closed when a new modal is opened. 28 | popoverProps={{ zIndex: 1000 }} 29 | styles={{ 30 | input: { height: "1.625rem", minHeight: "1.625rem" }, 31 | // icon: { transform: "scale(0.8)" }, 32 | }} 33 | /> 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/viser/client/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { ViewerContext } from "./App"; 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 | -------------------------------------------------------------------------------- /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/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 onp 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 = onp.random.normal(size=(30, 3)) * 3.0 29 | client.scene.add_spline_catmull_rom( 30 | f"/catmull_{i}", 31 | positions, 32 | tension=0.5, 33 | line_width=3.0, 34 | color=onp.random.uniform(size=3), 35 | ) 36 | images.append(client.camera.get_render(height=720, width=1280)) 37 | 38 | print("Generating and sending GIF...") 39 | client.send_file_download( 40 | "image.gif", iio.imwrite("", images, extension=".gif") 41 | ) 42 | print("Done!") 43 | 44 | while True: 45 | time.sleep(10.0) 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 onp 20 | import trimesh 21 | import trimesh.creation 22 | 23 | import viser 24 | 25 | server = viser.ViserServer() 26 | 27 | 28 | img = onp.random.randint(0, 255, size=(1000, 1000, 3), dtype=onp.uint8) 29 | depth = onp.ones((1000, 1000, 1), dtype=onp.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 | -------------------------------------------------------------------------------- /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 { GuiAddButtonGroupMessage } from "../WebsocketMessages"; 5 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; 6 | 7 | export default function ButtonGroupComponent({ 8 | id, 9 | hint, 10 | label, 11 | visible, 12 | disabled, 13 | options, 14 | }: GuiAddButtonGroupMessage) { 15 | const { messageSender } = React.useContext(GuiComponentContext)!; 16 | if (!visible) return <>; 17 | return ( 18 | 19 | 20 | {options.map((option, index) => ( 21 | 37 | ))} 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/viser/client/src/components/MultiSliderPrimitive/LICENSE: -------------------------------------------------------------------------------- 1 | This component is modified from Mantine v7. 2 | 3 | https://github.com/mantinedev/mantine/tree/e3e3bb834de1f2f75a27dbc757dc0a2fc6a6cba8/packages/%40mantine/core/src/components/Slider 4 | 5 | -- 6 | 7 | MIT License 8 | 9 | Copyright (c) 2021 Vitaly Rtishchev 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | -------------------------------------------------------------------------------- /src/viser/client/src/components/MultiSliderPrimitive/Track/Track.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box } from "@mantine/core"; 3 | import { Marks } from "../Marks/Marks"; 4 | import { useSliderContext } from "../Slider.context"; 5 | 6 | export interface TrackProps { 7 | filled: number; 8 | offset?: number; 9 | marksOffset?: number; 10 | marks: { value: number; label?: React.ReactNode }[] | undefined; 11 | min: number; 12 | max: number; 13 | value: number; 14 | children: React.ReactNode; 15 | disabled: boolean | undefined; 16 | inverted: boolean | undefined; 17 | containerProps?: React.PropsWithRef>; 18 | } 19 | 20 | export function Track({ 21 | children, 22 | disabled, 23 | marksOffset, 24 | inverted, 25 | containerProps, 26 | ...others 27 | }: TrackProps) { 28 | const { getStyles } = useSliderContext(); 29 | 30 | return ( 31 | <> 32 | 37 | 38 | {children} 39 | 40 | 46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | Track.displayName = "@mantine/core/SliderTrack"; 53 | -------------------------------------------------------------------------------- /src/viser/client/src/components/utils.tsx: -------------------------------------------------------------------------------- 1 | // Color conversion helpers. 2 | 3 | export function rgbToHex([r, g, b]: [number, number, number]): string { 4 | const hexR = r.toString(16).padStart(2, "0"); 5 | const hexG = g.toString(16).padStart(2, "0"); 6 | const hexB = b.toString(16).padStart(2, "0"); 7 | return `#${hexR}${hexG}${hexB}`; 8 | } 9 | 10 | export function hexToRgb(hexColor: string): [number, number, number] { 11 | const hex = hexColor.slice(1); // Remove the # in #ffffff. 12 | const r = parseInt(hex.substring(0, 2), 16); 13 | const g = parseInt(hex.substring(2, 4), 16); 14 | const b = parseInt(hex.substring(4, 6), 16); 15 | return [r, g, b]; 16 | } 17 | export function rgbaToHex([r, g, b, a]: [ 18 | number, 19 | number, 20 | number, 21 | number, 22 | ]): string { 23 | const hexR = r.toString(16).padStart(2, "0"); 24 | const hexG = g.toString(16).padStart(2, "0"); 25 | const hexB = b.toString(16).padStart(2, "0"); 26 | const hexA = a.toString(16).padStart(2, "0"); 27 | return `#${hexR}${hexG}${hexB}${hexA}`; 28 | } 29 | 30 | export function hexToRgba(hexColor: string): [number, number, number, number] { 31 | const hex = hexColor.slice(1); // Remove the # in #ffffff. 32 | const r = parseInt(hex.substring(0, 2), 16); 33 | const g = parseInt(hex.substring(2, 4), 16); 34 | const b = parseInt(hex.substring(4, 6), 16); 35 | const a = parseInt(hex.substring(6, 8), 16); 36 | return [r, g, b, a]; 37 | } 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 onp 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 | onp.random.randint( 41 | 0, 42 | 256, 43 | size=(400, 400, 3), 44 | dtype=onp.uint8, 45 | ), 46 | 4.0, 47 | 4.0, 48 | format="jpeg", 49 | wxyz=(1.0, 0.0, 0.0, 0.0), 50 | position=(2.0, 2.0, -1e-2), 51 | ) 52 | time.sleep(0.2) 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ViserInputComponent } from "./common"; 3 | import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; 4 | import { GuiAddCheckboxMessage } from "../WebsocketMessages"; 5 | import { Box, Checkbox, Tooltip } from "@mantine/core"; 6 | 7 | export default function CheckboxComponent({ 8 | id, 9 | disabled, 10 | visible, 11 | hint, 12 | label, 13 | value, 14 | }: GuiAddCheckboxMessage) { 15 | const { setValue } = React.useContext(GuiComponentContext)!; 16 | if (!visible) return <>; 17 | let input = ( 18 | { 23 | setValue(id, value.target.checked); 24 | }} 25 | disabled={disabled} 26 | /> 27 | ); 28 | if (hint !== null && hint !== undefined) { 29 | // For checkboxes, we want to make sure that the wrapper 30 | // doesn't expand to the full width of the parent. This will 31 | // de-center the tooltip. 32 | input = ( 33 | 42 | {input} 43 | 44 | ); 45 | } 46 | return {input}; 47 | } 48 | -------------------------------------------------------------------------------- /src/viser/client/src/components/MultiSliderPrimitive/Marks/Marks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box } from "@mantine/core"; 3 | import { useSliderContext } from "../Slider.context"; 4 | import { getPosition } from "../utils/get-position/get-position"; 5 | 6 | export interface MarksProps { 7 | marks: { value: number; label?: React.ReactNode }[] | undefined; 8 | min: number; 9 | max: number; 10 | value: number; 11 | offset: number | undefined; 12 | disabled: boolean | undefined; 13 | inverted: boolean | undefined; 14 | } 15 | 16 | export function Marks({ 17 | marks, 18 | min, 19 | max, 20 | disabled, 21 | value, // eslint-disable-line 22 | offset, // eslint-disable-line 23 | inverted, // eslint-disable-line 24 | }: MarksProps) { 25 | const { getStyles } = useSliderContext(); 26 | 27 | if (!marks) { 28 | return null; 29 | } 30 | 31 | const items = marks.map((mark, index) => ( 32 | 39 | 46 | {mark.label &&
{mark.label}
} 47 |
48 | )); 49 | 50 | return
{items}
; 51 | } 52 | 53 | Marks.displayName = "@mantine/core/SliderMarks"; 54 | -------------------------------------------------------------------------------- /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 | } from "@mantine/core"; 12 | import { themeToVars } from "@mantine/vanilla-extract"; 13 | 14 | export const theme = createTheme({ 15 | fontFamily: "Inter", 16 | autoContrast: true, 17 | components: { 18 | Checkbox: Checkbox.extend({ 19 | defaultProps: { 20 | radius: "xs", 21 | }, 22 | }), 23 | ColorInput: ColorInput.extend({ 24 | defaultProps: { 25 | radius: "xs", 26 | }, 27 | }), 28 | Select: Select.extend({ 29 | defaultProps: { 30 | radius: "sm", 31 | }, 32 | }), 33 | TextInput: TextInput.extend({ 34 | defaultProps: { 35 | radius: "xs", 36 | }, 37 | }), 38 | NumberInput: NumberInput.extend({ 39 | defaultProps: { 40 | radius: "xs", 41 | }, 42 | }), 43 | Paper: Paper.extend({ 44 | defaultProps: { 45 | radius: "xs", 46 | shadow: "0", 47 | }, 48 | }), 49 | ActionIcon: ActionIcon.extend({ 50 | defaultProps: { 51 | variant: "subtle", 52 | color: "gray", 53 | radius: "xs", 54 | }, 55 | }), 56 | Button: Button.extend({ 57 | defaultProps: { 58 | radius: "xs", 59 | fw: 450, 60 | }, 61 | }), 62 | }, 63 | }); 64 | 65 | export const vars = themeToVars(theme); 66 | -------------------------------------------------------------------------------- /src/viser/client/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { GuiAddButtonMessage } 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 | 9 | export default function ButtonComponent({ 10 | id, 11 | visible, 12 | disabled, 13 | label, 14 | ...otherProps 15 | }: GuiAddButtonMessage) { 16 | const { messageSender } = React.useContext(GuiComponentContext)!; 17 | const { color, icon_html } = otherProps; 18 | if (!(visible ?? true)) return <>; 19 | 20 | return ( 21 | 22 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /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 { GuiAddDropdownMessage } from "../WebsocketMessages"; 5 | import { Select } from "@mantine/core"; 6 | 7 | export default function DropdownComponent({ 8 | id, 9 | hint, 10 | label, 11 | value, 12 | disabled, 13 | visible, 14 | options, 15 | }: GuiAddDropdownMessage) { 16 | const { setValue } = React.useContext(GuiComponentContext)!; 17 | if (!visible) return <>; 18 | return ( 19 | 20 |