diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 911dd23a4..e543e3949 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -3,6 +3,9 @@ name: docs
on:
push:
branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
jobs:
docs:
@@ -14,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v1
with:
- python-version: 3.8
+ python-version: "3.10"
# Build documentation
- name: Building documentation
@@ -31,3 +34,4 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build
cname: viser.studio
+ if: github.event_name != 'pull_request'
diff --git a/.github/workflows/typescript-compile.yml b/.github/workflows/typescript-compile.yml
index 7192ea21f..fb4beea07 100644
--- a/.github/workflows/typescript-compile.yml
+++ b/.github/workflows/typescript-compile.yml
@@ -14,7 +14,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
- node-version: 19
+ node-version: 20
- name: Run tsc
run: |
cd src/viser/client
diff --git a/docs/requirements.txt b/docs/requirements.txt
index f913bc506..7260ab5aa 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,8 +1,7 @@
-sphinx==5.2.0
-furo==2023.03.27
-docutils==0.17.1
-sphinx-autoapi==2.1.0
-m2r2==0.3.2
+sphinx==7.2.6
+furo==2023.9.10
+docutils==0.20.1
+sphinx-autoapi==3.0.0
+m2r2==0.3.3.post2
git+https://github.com/brentyi/sphinxcontrib-programoutput.git
git+https://github.com/brentyi/ansi.git
-setuptools
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 596c206a3..f116ad8fe 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -212,6 +212,7 @@
# -- Options for autoapi extension --------------------------------------------
autoapi_dirs = ["../../src/viser"]
autoapi_root = "api"
+autoapi_ignore = ["**/client/**/*.py"]
autoapi_options = [
"members",
"inherited-members",
diff --git a/docs/source/examples/15_gui_in_scene.rst b/docs/source/examples/15_gui_in_scene.rst
index d75e1aaa2..a8dc4a549 100644
--- a/docs/source/examples/15_gui_in_scene.rst
+++ b/docs/source/examples/15_gui_in_scene.rst
@@ -37,7 +37,6 @@ performed on them.
rng = onp.random.default_rng(0)
displayed_3d_container: Optional[viser.Gui3dContainerHandle] = None
- displayed_index: Optional[int] = None
def make_frame(i: int) -> None:
# Sample a random orientation + position.
@@ -52,18 +51,11 @@ performed on them.
@frame.on_click
def _(_):
nonlocal displayed_3d_container
- nonlocal displayed_index
# Close previously opened GUI.
if displayed_3d_container is not None:
displayed_3d_container.remove()
- # Don't re-show the same GUI element.
- if displayed_index == i:
- return
-
- displayed_index = i
-
displayed_3d_container = client.add_3d_gui_container(f"/frame_{i}/gui")
with displayed_3d_container:
go_to = client.add_gui_button("Go to")
@@ -105,12 +97,10 @@ performed on them.
@close.on_click
def _(_) -> None:
nonlocal displayed_3d_container
- nonlocal displayed_index
if displayed_3d_container is None:
return
displayed_3d_container.remove()
displayed_3d_container = None
- displayed_index = None
for i in range(num_frames):
make_frame(i)
diff --git a/examples/14_markdown.py b/examples/14_markdown.py
index 3579cbde5..4d85ac6cc 100644
--- a/examples/14_markdown.py
+++ b/examples/14_markdown.py
@@ -11,24 +11,32 @@
server = viser.ViserServer()
server.world_axes.visible = True
+markdown_counter = server.add_gui_markdown("Counter: 0")
-@server.on_client_connect
-def _(client: viser.ClientHandle) -> None:
- here = Path(__file__).absolute().parent
- markdown_source = (here / "./assets/mdx_example.mdx").read_text()
- markdown = client.add_gui_markdown(markdown=markdown_source, image_root=here)
+here = Path(__file__).absolute().parent
- button = client.add_gui_button("Remove Markdown")
- checkbox = client.add_gui_checkbox("Visibility", initial_value=True)
+button = server.add_gui_button("Remove blurb")
+checkbox = server.add_gui_checkbox("Visibility", initial_value=True)
- @button.on_click
- def _(_):
- markdown.remove()
+markdown_source = (here / "./assets/mdx_example.mdx").read_text()
+markdown_blurb = server.add_gui_markdown(
+ content=markdown_source,
+ image_root=here,
+)
- @checkbox.on_update
- def _(_):
- markdown.visible = checkbox.value
+@button.on_click
+def _(_):
+ markdown_blurb.remove()
+
+@checkbox.on_update
+def _(_):
+ markdown_blurb.visible = checkbox.value
+
+
+counter = 0
while True:
- time.sleep(10.0)
+ markdown_counter.content = f"Counter: {counter}"
+ counter += 1
+ time.sleep(0.1)
diff --git a/examples/15_gui_in_scene.py b/examples/15_gui_in_scene.py
index 2ff54f4fd..d57e76db2 100644
--- a/examples/15_gui_in_scene.py
+++ b/examples/15_gui_in_scene.py
@@ -32,7 +32,6 @@ def _(client: viser.ClientHandle) -> None:
rng = onp.random.default_rng(0)
displayed_3d_container: Optional[viser.Gui3dContainerHandle] = None
- displayed_index: Optional[int] = None
def make_frame(i: int) -> None:
# Sample a random orientation + position.
@@ -47,18 +46,11 @@ def make_frame(i: int) -> None:
@frame.on_click
def _(_):
nonlocal displayed_3d_container
- nonlocal displayed_index
# Close previously opened GUI.
if displayed_3d_container is not None:
displayed_3d_container.remove()
- # Don't re-show the same GUI element.
- if displayed_index == i:
- return
-
- displayed_index = i
-
displayed_3d_container = client.add_3d_gui_container(f"/frame_{i}/gui")
with displayed_3d_container:
go_to = client.add_gui_button("Go to")
@@ -100,12 +92,10 @@ def _(_) -> None:
@close.on_click
def _(_) -> None:
nonlocal displayed_3d_container
- nonlocal displayed_index
if displayed_3d_container is None:
return
displayed_3d_container.remove()
displayed_3d_container = None
- displayed_index = None
for i in range(num_frames):
make_frame(i)
diff --git a/examples/17_background_composite.py b/examples/17_background_composite.py
index caa422e9c..c839937ef 100644
--- a/examples/17_background_composite.py
+++ b/examples/17_background_composite.py
@@ -1,4 +1,3 @@
-# mypy: disable-error-code="var-annotated"
"""Depth compositing
In this example, we show how to use a background image with depth compositing. This can
diff --git a/pyproject.toml b/pyproject.toml
index ca29e0cc7..4e115d20a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "viser"
-version = "0.1.5"
+version = "0.1.6"
description = "3D visualization + Python"
readme = "README.md"
license = { text="MIT" }
@@ -67,6 +67,7 @@ python_version = "3.8"
ignore_missing_imports = true
warn_unused_configs = true
exclude="viser/client/.nodeenv"
+disable_error_code="var-annotated" # Common source of mypy + numpy false positives.
[tool.pyright]
exclude = ["./docs/**/*", "./examples/assets/**/*", "./src/viser/client/.nodeenv", "./build"]
diff --git a/src/viser/_gui_api.py b/src/viser/_gui_api.py
index cacc3a027..039f5d127 100644
--- a/src/viser/_gui_api.py
+++ b/src/viser/_gui_api.py
@@ -6,10 +6,8 @@
import abc
import dataclasses
-import re
import threading
import time
-import urllib.parse
import warnings
from pathlib import Path
from typing import (
@@ -24,7 +22,6 @@
overload,
)
-import imageio.v3 as iio
import numpy as onp
from typing_extensions import Literal, LiteralString
@@ -47,7 +44,7 @@
)
from ._icons import base64_from_icon
from ._icons_enum import Icon
-from ._message_api import MessageApi, _encode_image_base64, cast_vector
+from ._message_api import MessageApi, cast_vector
if TYPE_CHECKING:
from .infra import ClientId
@@ -87,38 +84,6 @@ def _compute_precision_digits(x: float) -> int:
return digits
-def _get_data_url(url: str, image_root: Optional[Path]) -> str:
- if not url.startswith("http") and not image_root:
- warnings.warn(
- "No `image_root` provided. All relative paths will be scoped to viser's installation path.",
- stacklevel=2,
- )
- if url.startswith("http"):
- return url
- if image_root is None:
- image_root = Path(__file__).parent
- try:
- image = iio.imread(image_root / url)
- data_uri = _encode_image_base64(image, "png")
- url = urllib.parse.quote(f"{data_uri[1]}")
- return f"data:{data_uri[0]};base64,{url}"
- except (IOError, FileNotFoundError):
- warnings.warn(
- f"Failed to read image {url}, with image_root set to {image_root}.",
- stacklevel=2,
- )
- return url
-
-
-def _parse_markdown(markdown: str, image_root: Optional[Path]) -> str:
- markdown = re.sub(
- r"\!\[([^]]*)\]\(([^]]*)\)",
- lambda match: f"![{match.group(1)}]({_get_data_url(match.group(2), image_root)})",
- markdown,
- )
- return markdown
-
-
@dataclasses.dataclass
class _RootGuiContainer:
_children: Dict[str, SupportsRemoveProtocol]
@@ -277,30 +242,25 @@ def add_gui_tab_group(
def add_gui_markdown(
self,
- markdown: str,
+ content: str,
image_root: Optional[Path] = None,
order: Optional[float] = None,
) -> GuiMarkdownHandle:
"""Add markdown to the GUI."""
- markdown = _parse_markdown(markdown, image_root)
- markdown_id = _make_unique_id()
- order = _apply_default_order(order)
- self._get_api()._queue(
- _messages.GuiAddMarkdownMessage(
- order=order,
- id=markdown_id,
- markdown=markdown,
- container_id=self._get_container_id(),
- )
- )
- return GuiMarkdownHandle(
+ handle = GuiMarkdownHandle(
_gui_api=self,
- _id=markdown_id,
+ _id=_make_unique_id(),
_visible=True,
_container_id=self._get_container_id(),
- _order=order,
+ _order=_apply_default_order(order),
+ _image_root=image_root,
+ _content=None,
)
+ # Assigning content will send a GuiAddMarkdownMessage.
+ handle.content = content
+ return handle
+
def add_gui_button(
self,
label: str,
diff --git a/src/viser/_gui_handles.py b/src/viser/_gui_handles.py
index c3bad52cf..50c5556b4 100644
--- a/src/viser/_gui_handles.py
+++ b/src/viser/_gui_handles.py
@@ -1,9 +1,13 @@
from __future__ import annotations
import dataclasses
+import re
import threading
import time
+import urllib.parse
import uuid
+import warnings
+from pathlib import Path
from typing import (
TYPE_CHECKING,
Callable,
@@ -18,13 +22,16 @@
Union,
)
+import imageio.v3 as iio
import numpy as onp
from typing_extensions import Protocol
from ._icons import base64_from_icon
from ._icons_enum import Icon
+from ._message_api import _encode_image_base64
from ._messages import (
GuiAddDropdownMessage,
+ GuiAddMarkdownMessage,
GuiAddTabGroupMessage,
GuiCloseModalMessage,
GuiRemoveMessage,
@@ -493,6 +500,38 @@ def remove(self) -> None:
child.remove()
+def _get_data_url(url: str, image_root: Optional[Path]) -> str:
+ if not url.startswith("http") and not image_root:
+ warnings.warn(
+ "No `image_root` provided. All relative paths will be scoped to viser's installation path.",
+ stacklevel=2,
+ )
+ if url.startswith("http"):
+ return url
+ if image_root is None:
+ image_root = Path(__file__).parent
+ try:
+ image = iio.imread(image_root / url)
+ data_uri = _encode_image_base64(image, "png")
+ url = urllib.parse.quote(f"{data_uri[1]}")
+ return f"data:{data_uri[0]};base64,{url}"
+ except (IOError, FileNotFoundError):
+ warnings.warn(
+ f"Failed to read image {url}, with image_root set to {image_root}.",
+ stacklevel=2,
+ )
+ return url
+
+
+def _parse_markdown(markdown: str, image_root: Optional[Path]) -> str:
+ markdown = re.sub(
+ r"\!\[([^]]*)\]\(([^]]*)\)",
+ lambda match: f"![{match.group(1)}]({_get_data_url(match.group(2), image_root)})",
+ markdown,
+ )
+ return markdown
+
+
@dataclasses.dataclass
class GuiMarkdownHandle:
"""Use to remove markdown."""
@@ -502,6 +541,26 @@ class GuiMarkdownHandle:
_visible: bool
_container_id: str # Parent.
_order: float
+ _image_root: Optional[Path]
+ _content: Optional[str]
+
+ @property
+ def content(self) -> str:
+ """Current content of this markdown element. Synchronized automatically when assigned."""
+ assert self._content is not None
+ return self._content
+
+ @content.setter
+ def content(self, content: str) -> None:
+ self._content = content
+ self._gui_api._get_api()._queue(
+ GuiAddMarkdownMessage(
+ order=self._order,
+ id=self._id,
+ markdown=_parse_markdown(content, self._image_root),
+ container_id=self._container_id,
+ )
+ )
@property
def order(self) -> float:
diff --git a/src/viser/_message_api.py b/src/viser/_message_api.py
index 192d726a7..271f860dc 100644
--- a/src/viser/_message_api.py
+++ b/src/viser/_message_api.py
@@ -48,7 +48,6 @@
PointCloudHandle,
SceneNodeHandle,
TransformControlsHandle,
- _SupportsVisibility,
_TransformControlsState,
)
@@ -372,10 +371,11 @@ def add_label(
text: str,
wxyz: Tuple[float, float, float, float] | onp.ndarray = (1.0, 0.0, 0.0, 0.0),
position: Tuple[float, float, float] | onp.ndarray = (0.0, 0.0, 0.0),
+ visible: bool = True,
) -> LabelHandle:
"""Add a 2D label to the scene."""
self._queue(_messages.LabelMessage(name, text))
- return LabelHandle._make(self, name, wxyz, position)
+ return LabelHandle._make(self, name, wxyz, position, visible=visible)
def add_point_cloud(
self,
@@ -581,7 +581,7 @@ def sync_cb(client_id: ClientId, state: TransformControlsHandle) -> None:
message_position.excluded_self_client = client_id
self._queue(message_position)
- node_handle = _SupportsVisibility._make(self, name, wxyz, position, visible)
+ node_handle = SceneNodeHandle._make(self, name, wxyz, position, visible)
state_aux = _TransformControlsState(
last_updated=time.time(),
update_cb=[],
@@ -703,6 +703,7 @@ def add_3d_gui_container(
name: str,
wxyz: Tuple[float, float, float, float] | onp.ndarray = (1.0, 0.0, 0.0, 0.0),
position: Tuple[float, float, float] | onp.ndarray = (0.0, 0.0, 0.0),
+ visible: bool = True,
) -> Gui3dContainerHandle:
"""Add a 3D gui container to the scene. The returned container handle can be
used as a context to place GUI elements into the 3D scene."""
@@ -728,5 +729,5 @@ def add_3d_gui_container(
container_id=container_id,
)
)
- node_handle = SceneNodeHandle._make(self, name, wxyz, position)
+ node_handle = SceneNodeHandle._make(self, name, wxyz, position, visible=visible)
return Gui3dContainerHandle(node_handle._impl, gui_api, container_id)
diff --git a/src/viser/_scene_handles.py b/src/viser/_scene_handles.py
index beffa6c03..c900c7642 100644
--- a/src/viser/_scene_handles.py
+++ b/src/viser/_scene_handles.py
@@ -40,7 +40,6 @@ class ScenePointerEvent:
TSceneNodeHandle = TypeVar("TSceneNodeHandle", bound="SceneNodeHandle")
-TSupportsVisibility = TypeVar("TSupportsVisibility", bound="_SupportsVisibility")
@dataclasses.dataclass
@@ -73,12 +72,14 @@ def _make(
name: str,
wxyz: Tuple[float, float, float, float] | onp.ndarray,
position: Tuple[float, float, float] | onp.ndarray,
+ visible: bool,
) -> TSceneNodeHandle:
out = cls(_SceneNodeHandleState(name, api))
api._handle_from_node_name[name] = out
out.wxyz = wxyz
out.position = position
+ out.visible = visible
return out
@property
@@ -115,6 +116,18 @@ def position(self, position: Tuple[float, float, float] | onp.ndarray) -> None:
_messages.SetPositionMessage(self._impl.name, position_cast)
)
+ @property
+ def visible(self) -> bool:
+ """Whether the scene node is visible or not. Synchronized to clients automatically when assigned."""
+ return self._impl.visible
+
+ @visible.setter
+ def visible(self, visible: bool) -> None:
+ self._impl.api._queue(
+ _messages.SetSceneNodeVisibilityMessage(self._impl.name, visible)
+ )
+ self._impl.visible = visible
+
def remove(self) -> None:
"""Remove the node from the scene."""
self._impl.api._queue(_messages.RemoveSceneNodeMessage(self._impl.name))
@@ -127,7 +140,7 @@ class SceneNodePointerEvent(Generic[TSceneNodeHandle]):
@dataclasses.dataclass
-class _SupportsClick(SceneNodeHandle):
+class _ClickableSceneNodeHandle(SceneNodeHandle):
def on_click(
self: TSceneNodeHandle,
func: Callable[[SceneNodePointerEvent[TSceneNodeHandle]], None],
@@ -148,71 +161,38 @@ def on_click(
@dataclasses.dataclass
-class _SupportsVisibility(SceneNodeHandle):
- @classmethod
- def _make(
- cls: Type[TSupportsVisibility],
- api: MessageApi,
- name: str,
- wxyz: Tuple[float, float, float, float] | onp.ndarray,
- position: Tuple[float, float, float] | onp.ndarray,
- visible: bool = True,
- ) -> TSupportsVisibility:
- out = cls(_SceneNodeHandleState(name, api))
- api._handle_from_node_name[name] = out
-
- out.wxyz = wxyz
- out.position = position
- out.visible = visible
-
- return out
-
- @property
- def visible(self) -> bool:
- """Whether the scene node is visible or not. Synchronized to clients automatically when assigned."""
- return self._impl.visible
-
- @visible.setter
- def visible(self, visible: bool) -> None:
- self._impl.api._queue(
- _messages.SetSceneNodeVisibilityMessage(self._impl.name, visible)
- )
- self._impl.visible = visible
-
-
-@dataclasses.dataclass
-class CameraFrustumHandle(_SupportsClick, _SupportsVisibility):
+class CameraFrustumHandle(_ClickableSceneNodeHandle):
"""Handle for camera frustums."""
@dataclasses.dataclass
-class PointCloudHandle(_SupportsVisibility):
+class PointCloudHandle(SceneNodeHandle):
"""Handle for point clouds. Does not support click events."""
@dataclasses.dataclass
-class FrameHandle(_SupportsClick, _SupportsVisibility):
+class FrameHandle(_ClickableSceneNodeHandle):
"""Handle for coordinate frames."""
@dataclasses.dataclass
-class MeshHandle(_SupportsClick, _SupportsVisibility):
+class MeshHandle(_ClickableSceneNodeHandle):
"""Handle for mesh objects."""
@dataclasses.dataclass
-class GlbHandle(_SupportsClick, _SupportsVisibility):
+class GlbHandle(_ClickableSceneNodeHandle):
"""Handle for GLB objects."""
@dataclasses.dataclass
-class ImageHandle(_SupportsClick, _SupportsVisibility):
+class ImageHandle(_ClickableSceneNodeHandle):
"""Handle for 2D images, rendered in 3D."""
@dataclasses.dataclass
class LabelHandle(SceneNodeHandle):
- """Handle for 2D label objects. Does not support click events or visibility toggling."""
+ """Handle for 2D label objects. Does not support click events."""
@dataclasses.dataclass
@@ -223,7 +203,7 @@ class _TransformControlsState:
@dataclasses.dataclass
-class TransformControlsHandle(_SupportsClick, _SupportsVisibility):
+class TransformControlsHandle(_ClickableSceneNodeHandle):
"""Handle for interacting with transform control gizmos."""
_impl_aux: _TransformControlsState
diff --git a/src/viser/_viser.py b/src/viser/_viser.py
index 692661f29..df587330f 100644
--- a/src/viser/_viser.py
+++ b/src/viser/_viser.py
@@ -408,13 +408,15 @@ def _(conn: infra.ClientConnection) -> None:
)
table.add_row("HTTP", http_url)
table.add_row("Websocket", ws_url)
- rich.print(Panel(table, title="[bold]viser[/bold]", expand=False))
# Create share tunnel if requested.
if not share:
self._share_tunnel = None
+ rich.print(Panel(table, title="[bold]viser[/bold]", expand=False))
else:
- rich.print("[bold](viser)[/bold] Share URL requested!")
+ rich.print(
+ "[bold](viser)[/bold] Share URL requested! (expires in 24 hours)"
+ )
self._share_tunnel = _ViserTunnel(port)
@self._share_tunnel.on_connect
@@ -424,9 +426,8 @@ def _() -> None:
if share_url is None:
rich.print("[bold](viser)[/bold] Could not generate share URL")
else:
- rich.print(
- f"[bold](viser)[/bold] Share URL (expires in 24 hours): {share_url}"
- )
+ table.add_row("Share URL", share_url)
+ rich.print(Panel(table, title="[bold]viser[/bold]", expand=False))
self.reset_scene()
self.world_axes = FrameHandle(
diff --git a/src/viser/client/package.json b/src/viser/client/package.json
index 43b20fcf7..748216ecc 100644
--- a/src/viser/client/package.json
+++ b/src/viser/client/package.json
@@ -1,5 +1,5 @@
{
- "name": "viewer",
+ "name": "viser",
"version": "0.1.0",
"private": true,
"dependencies": {
@@ -28,7 +28,7 @@
"isomorphic-ws": "^5.0.0",
"mantine-react-table": "^1.0.0-beta.11",
"msgpackr": "^1.8.5",
- "prettier": "^2.8.7",
+ "prettier": "^3.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.10",
diff --git a/src/viser/client/src/App.tsx b/src/viser/client/src/App.tsx
index 942cb4db6..afcf5e87b 100644
--- a/src/viser/client/src/App.tsx
+++ b/src/viser/client/src/App.tsx
@@ -185,7 +185,7 @@ function ViewerCanvas({ children }: { children: React.ReactNode }) {
-
+
void;
}>(null);
+/** A bottom panel is used to display the controls on mobile devices. */
export default function BottomPanel({
children,
}: {
@@ -77,6 +78,7 @@ BottomPanel.Handle = function BottomPanelHandle({
);
};
+
/** Contents of a panel. */
BottomPanel.Contents = function BottomPanelContents({
children,
diff --git a/src/viser/client/src/ControlPanel/FloatingPanel.tsx b/src/viser/client/src/ControlPanel/FloatingPanel.tsx
index ce9f6796a..aa688e5d1 100644
--- a/src/viser/client/src/ControlPanel/FloatingPanel.tsx
+++ b/src/viser/client/src/ControlPanel/FloatingPanel.tsx
@@ -24,8 +24,7 @@ const FloatingPanelContext = React.createContext;
}>(null);
-/** Root component for control panel. Parents a set of control tabs.
- * This could be refactored+cleaned up a lot! */
+/** A floating panel for displaying controls. */
export default function FloatingPanel({
children,
}: {
@@ -288,7 +287,13 @@ FloatingPanel.Contents = function FloatingPanelContents({
return (
- {children}
+
+ {children}
+
);
diff --git a/src/viser/client/src/ControlPanel/SidebarPanel.tsx b/src/viser/client/src/ControlPanel/SidebarPanel.tsx
index cdcb6b584..c3aebd97b 100644
--- a/src/viser/client/src/ControlPanel/SidebarPanel.tsx
+++ b/src/viser/client/src/ControlPanel/SidebarPanel.tsx
@@ -10,8 +10,7 @@ export const SidebarPanelContext = React.createContext void;
}>(null);
-/** Root component for control panel. Parents a set of control tabs.
- * This could be refactored+cleaned up a lot! */
+/** A fixed or collapsible side panel for displaying controls. */
export default function SidebarPanel({
children,
collapsible,
@@ -73,9 +72,9 @@ export default function SidebarPanel({
}}
>
{children}
diff --git a/src/viser/client/src/Markdown.tsx b/src/viser/client/src/Markdown.tsx
index 9d3ce865b..91826438b 100644
--- a/src/viser/client/src/Markdown.tsx
+++ b/src/viser/client/src/Markdown.tsx
@@ -177,7 +177,7 @@ export default function Markdown(props: { children?: string }) {
} catch {
setChild(Error Parsing Markdown...);
}
- }, []);
+ }, [props.children]);
return child;
}
diff --git a/src/viser/client/src/SceneTree.tsx b/src/viser/client/src/SceneTree.tsx
index 9d482a904..39d37c6f0 100644
--- a/src/viser/client/src/SceneTree.tsx
+++ b/src/viser/client/src/SceneTree.tsx
@@ -26,6 +26,12 @@ export class SceneNode {
public name: string,
public makeObject: MakeObject,
public cleanup?: () => void,
+ /** unmountWhenInvisible is used to unmount components when they
+ * should be hidden.
+ *
+ * https://github.com/pmndrs/drei/issues/1323
+ */
+ public unmountWhenInvisible?: true,
) {
this.children = [];
this.clickable = false;
@@ -49,7 +55,13 @@ function SceneNodeThreeChildren(props: {
{children &&
children.map((child_id) => {
- return ;
+ return (
+
+ );
})}
,
@@ -81,7 +93,10 @@ function SceneNodeLabel(props: { name: string }) {
}
/** Component containing the three.js object and children for a particular scene node. */
-export function SceneNodeThreeObject(props: { name: string }) {
+export function SceneNodeThreeObject(props: {
+ name: string;
+ parent: THREE.Object3D | null;
+}) {
const viewer = React.useContext(ViewerContext)!;
const makeObject = viewer.useSceneTree(
(state) => state.nodeFromName[props.name]?.makeObject,
@@ -89,11 +104,21 @@ export function SceneNodeThreeObject(props: { name: string }) {
const cleanup = viewer.useSceneTree(
(state) => state.nodeFromName[props.name]?.cleanup,
);
+ const unmountWhenInvisible = viewer.useSceneTree(
+ (state) => state.nodeFromName[props.name]?.unmountWhenInvisible,
+ );
+ const [unmount, setUnmount] = React.useState(false);
const clickable =
viewer.useSceneTree((state) => state.nodeFromName[props.name]?.clickable) ??
false;
const [obj, setRef] = React.useState(null);
+ const dragInfo = React.useRef({
+ dragging: false,
+ startClientX: 0,
+ startClientY: 0,
+ });
+
// Create object + children.
//
// For not-fully-understood reasons, wrapping makeObject with useMemo() fixes
@@ -108,9 +133,41 @@ export function SceneNodeThreeObject(props: { name: string }) {
);
+ // Helper for transient visibility checks. Checks the .visible attribute of
+ // both this object and ancestors.
+ //
+ // This is used for (1) suppressing click events and (2) unmounting when
+ // unmountWhenInvisible is true. The latter is used for components.
+ function isDisplayed() {
+ // We avoid checking obj.visible because obj may be unmounted when
+ // unmountWhenInvisible=true.
+ if (viewer.nodeAttributesFromName.current[props.name]?.visibility === false)
+ return false;
+ if (props.parent === null) return true;
+
+ // Check visibility of parents + ancestors.
+ let visible = props.parent.visible;
+ if (visible) {
+ props.parent.traverseAncestors((ancestor) => {
+ visible = visible && ancestor.visible;
+ });
+ }
+ return visible;
+ }
+
// Update attributes on a per-frame basis. Currently does redundant work,
// although this shouldn't be a bottleneck.
useFrame(() => {
+ if (unmountWhenInvisible) {
+ const displayed = isDisplayed();
+ if (displayed && unmount) {
+ setUnmount(false);
+ }
+ if (!displayed && !unmount) {
+ setUnmount(true);
+ }
+ }
+
if (obj === null) return;
const nodeAttributes = viewer.nodeAttributesFromName.current[props.name];
@@ -149,17 +206,10 @@ export function SceneNodeThreeObject(props: { name: string }) {
50,
);
const [hovered, setHovered] = React.useState(false);
- const [dragged, setDragged] = React.useState(false);
useCursor(hovered);
if (!clickable && hovered) setHovered(false);
- // Helper for checking transient visibility checks.
- function isVisible() {
- const nodeAttributes = viewer.nodeAttributesFromName.current[props.name];
- return nodeAttributes?.visibility ?? false;
- }
-
- if (objNode === undefined) {
+ if (objNode === undefined || unmount) {
return <>{children}>;
} else if (clickable) {
return (
@@ -172,31 +222,41 @@ export function SceneNodeThreeObject(props: { name: string }) {
// - onPointerUp, if triggered, sends a click if dragged = false.
// Note: It would be cool to have dragged actions too...
onPointerDown={(e) => {
- if (!isVisible()) return;
+ if (!isDisplayed()) return;
e.stopPropagation();
- setDragged(false);
+ const state = dragInfo.current;
+ state.startClientX = e.clientX;
+ state.startClientY = e.clientY;
+ state.dragging = false;
}}
onPointerMove={(e) => {
- if (!isVisible()) return;
+ if (!isDisplayed()) return;
e.stopPropagation();
- setDragged(true);
+ const state = dragInfo.current;
+ const deltaX = e.clientX - state.startClientX;
+ const deltaY = e.clientY - state.startClientY;
+ // Minimum motion.
+ console.log(deltaX, deltaY);
+ if (Math.abs(deltaX) <= 3 && Math.abs(deltaY) <= 3) return;
+ state.dragging = true;
}}
onPointerUp={(e) => {
- if (!isVisible()) return;
+ if (!isDisplayed()) return;
e.stopPropagation();
- if (dragged) return;
+ const state = dragInfo.current;
+ if (state.dragging) return;
sendClicksThrottled({
type: "SceneNodeClickedMessage",
name: props.name,
});
}}
onPointerOver={(e) => {
- if (!isVisible()) return;
+ if (!isDisplayed()) return;
e.stopPropagation();
setHovered(true);
}}
onPointerOut={() => {
- if (!isVisible()) return;
+ if (!isDisplayed()) return;
setHovered(false);
}}
>
diff --git a/src/viser/client/src/ThreeAssets.tsx b/src/viser/client/src/ThreeAssets.tsx
index 8fbfa6234..69f31c339 100644
--- a/src/viser/client/src/ThreeAssets.tsx
+++ b/src/viser/client/src/ThreeAssets.tsx
@@ -80,7 +80,7 @@ export const GlbAsset = React.forwardRef<
(error) => {
console.log("Error loading GLB!");
console.log(error);
- }
+ },
);
return () => {
@@ -149,7 +149,7 @@ export const CoordinateFrame = React.forwardRef<
}
>(function CoordinateFrame(
{ show_axes = true, axes_length = 0.5, axes_radius = 0.0125 },
- ref
+ ref,
) {
return (
@@ -162,7 +162,7 @@ export const CoordinateFrame = React.forwardRef<
new THREE.Vector3(
axes_radius * 2.5,
axes_radius * 2.5,
- axes_radius * 2.5
+ axes_radius * 2.5,
)
}
/>
@@ -304,7 +304,7 @@ function LineSegmentInstance(props: {
const orientation = new THREE.Quaternion().setFromAxisAngle(
rotationAxis,
- rotationAngle
+ rotationAngle,
);
return (
<>
diff --git a/src/viser/client/src/WebsocketInterface.tsx b/src/viser/client/src/WebsocketInterface.tsx
index 243bb4e1a..34e6ee414 100644
--- a/src/viser/client/src/WebsocketInterface.tsx
+++ b/src/viser/client/src/WebsocketInterface.tsx
@@ -34,7 +34,7 @@ function threeColorBufferFromUint8Buffer(colors: ArrayBuffer) {
return Math.pow((value + 0.055) / 1.055, 2.4);
}
}),
- 3
+ 3,
);
}
@@ -66,7 +66,7 @@ function useMessageHandler() {
addSceneNodeMakeParents(
new SceneNode(parent_name, (ref) => (
- ))
+ )),
);
}
addSceneNode(node);
@@ -110,7 +110,7 @@ function useMessageHandler() {
axes_length={message.axes_length}
axes_radius={message.axes_radius}
/>
- ))
+ )),
);
return;
}
@@ -131,18 +131,18 @@ function useMessageHandler() {
new Float32Array(
message.points.buffer.slice(
message.points.byteOffset,
- message.points.byteOffset + message.points.byteLength
- )
+ message.points.byteOffset + message.points.byteLength,
+ ),
),
- 3
- )
+ 3,
+ ),
);
geometry.computeBoundingSphere();
// Wrap uint8 buffer for colors. Note that we need to set normalized=true.
geometry.setAttribute(
"color",
- threeColorBufferFromUint8Buffer(message.colors)
+ threeColorBufferFromUint8Buffer(message.colors),
);
addSceneNodeMakeParents(
@@ -161,8 +161,8 @@ function useMessageHandler() {
// disposal.
geometry.dispose();
pointCloudMaterial.dispose();
- }
- )
+ },
+ ),
);
return;
}
@@ -198,16 +198,16 @@ function useMessageHandler() {
new Float32Array(
message.vertices.buffer.slice(
message.vertices.byteOffset,
- message.vertices.byteOffset + message.vertices.byteLength
- )
+ message.vertices.byteOffset + message.vertices.byteLength,
+ ),
),
- 3
- )
+ 3,
+ ),
);
if (message.vertex_colors !== null) {
geometry.setAttribute(
"color",
- threeColorBufferFromUint8Buffer(message.vertex_colors)
+ threeColorBufferFromUint8Buffer(message.vertex_colors),
);
}
@@ -216,11 +216,11 @@ function useMessageHandler() {
new Uint32Array(
message.faces.buffer.slice(
message.faces.byteOffset,
- message.faces.byteOffset + message.faces.byteLength
- )
+ message.faces.byteOffset + message.faces.byteLength,
+ ),
),
- 1
- )
+ 1,
+ ),
);
geometry.computeVertexNormals();
geometry.computeBoundingSphere();
@@ -236,8 +236,8 @@ function useMessageHandler() {
// disposal.
geometry.dispose();
material.dispose();
- }
- )
+ },
+ ),
);
return;
}
@@ -247,7 +247,7 @@ function useMessageHandler() {
message.image_media_type !== null &&
message.image_base64_data !== null
? new TextureLoader().load(
- `data:${message.image_media_type};base64,${message.image_base64_data}`
+ `data:${message.image_media_type};base64,${message.image_base64_data}`,
)
: undefined;
@@ -264,8 +264,8 @@ function useMessageHandler() {
image={texture}
/>
),
- () => texture?.dispose()
- )
+ () => texture?.dispose(),
+ ),
);
return;
}
@@ -273,7 +273,7 @@ function useMessageHandler() {
const name = message.name;
const sendDragMessage = makeThrottledMessageSender(
viewer.websocketRef,
- 50
+ 50,
);
addSceneNodeMakeParents(
new SceneNode(message.name, (ref) => (
@@ -314,7 +314,7 @@ function useMessageHandler() {
}}
/>
- ))
+ )),
);
return;
}
@@ -323,12 +323,12 @@ function useMessageHandler() {
const R_threeworld_world = new THREE.Quaternion();
R_threeworld_world.setFromEuler(
- new THREE.Euler(-Math.PI / 2.0, 0.0, 0.0)
+ new THREE.Euler(-Math.PI / 2.0, 0.0, 0.0),
);
const target = new THREE.Vector3(
message.look_at[0],
message.look_at[1],
- message.look_at[2]
+ message.look_at[2],
);
target.applyQuaternion(R_threeworld_world);
cameraControls.setTarget(target.x, target.y, target.z, false);
@@ -339,12 +339,12 @@ function useMessageHandler() {
const cameraControls = viewer.cameraControlRef.current!;
const R_threeworld_world = new THREE.Quaternion();
R_threeworld_world.setFromEuler(
- new THREE.Euler(-Math.PI / 2.0, 0.0, 0.0)
+ new THREE.Euler(-Math.PI / 2.0, 0.0, 0.0),
);
const updir = new THREE.Vector3(
message.position[0],
message.position[1],
- message.position[2]
+ message.position[2],
).applyQuaternion(R_threeworld_world);
camera.up.set(updir.x, updir.y, updir.z);
@@ -359,7 +359,7 @@ function useMessageHandler() {
prevPosition.x,
prevPosition.y,
prevPosition.z,
- false
+ false,
);
return;
}
@@ -370,18 +370,18 @@ function useMessageHandler() {
const position_cmd = new THREE.Vector3(
message.position[0],
message.position[1],
- message.position[2]
+ message.position[2],
);
const R_worldthree_world = new THREE.Quaternion();
R_worldthree_world.setFromEuler(
- new THREE.Euler(-Math.PI / 2.0, 0.0, 0.0)
+ new THREE.Euler(-Math.PI / 2.0, 0.0, 0.0),
);
position_cmd.applyQuaternion(R_worldthree_world);
cameraControls.setPosition(
position_cmd.x,
position_cmd.y,
- position_cmd.z
+ position_cmd.z,
);
return;
}
@@ -390,7 +390,7 @@ function useMessageHandler() {
// tan(fov / 2.0) = 0.5 * film height / focal length
// focal length = 0.5 * film height / tan(fov / 2.0)
camera.setFocalLength(
- (0.5 * camera.getFilmHeight()) / Math.tan(message.fov / 2.0)
+ (0.5 * camera.getFilmHeight()) / Math.tan(message.fov / 2.0),
);
return;
}
@@ -426,7 +426,7 @@ function useMessageHandler() {
if (isTexture(oldBackgroundTexture)) oldBackgroundTexture.dispose();
viewer.useGui.setState({ backgroundAvailable: true });
- }
+ },
);
viewer.backgroundMaterialRef.current!.uniforms.enabled.value = true;
viewer.backgroundMaterialRef.current!.uniforms.hasDepth.value =
@@ -442,7 +442,7 @@ function useMessageHandler() {
viewer.backgroundMaterialRef.current!.uniforms.depthMap.value =
texture;
if (isTexture(oldDepthTexture)) oldDepthTexture.dispose();
- }
+ },
);
}
return;
@@ -470,61 +470,71 @@ function useMessageHandler() {
}
`;
addSceneNodeMakeParents(
- new SceneNode(message.name, (ref) => {
- // We wrap with because Html doesn't implement THREE.Object3D.
- return (
-
-
-
-
-
-
-
- );
- })
+ new SceneNode(
+ message.name,
+ (ref) => {
+ // We wrap with because Html doesn't implement THREE.Object3D.
+ return (
+
+
+
+
+
+
+
+ );
+ },
+ undefined,
+ true,
+ ),
);
return;
}
case "Gui3DMessage": {
addSceneNodeMakeParents(
- new SceneNode(message.name, (ref) => {
- // We wrap with because Html doesn't implement THREE.Object3D.
- return (
-
-
-
- {
- evt.stopPropagation();
- }}
+ new SceneNode(
+ message.name,
+ (ref) => {
+ // We wrap with because Html doesn't implement THREE.Object3D.
+ return (
+
+
+
-
-
-
-
-
- );
- })
+ {
+ evt.stopPropagation();
+ }}
+ >
+
+
+
+
+
+ );
+ },
+ undefined,
+ true,
+ ),
);
return;
}
@@ -559,10 +569,10 @@ function useMessageHandler() {
);
},
- () => texture.dispose()
- )
+ () => texture.dispose(),
+ ),
);
- }
+ },
);
return;
}
@@ -623,7 +633,7 @@ function useMessageHandler() {
scale={message.scale}
/>
);
- })
+ }),
);
return;
}
@@ -642,7 +652,7 @@ function useMessageHandler() {
>
);
- })
+ }),
);
return;
}
@@ -664,7 +674,7 @@ function useMessageHandler() {
))}
);
- })
+ }),
);
return;
}
@@ -729,7 +739,7 @@ export function FrameSynchronizedMessageHandler() {
console.log(
`Sending render; requested aspect ratio was ${targetAspect} (dimensinos: ${targetWidth}/${targetHeight}), copying from aspect ratio ${
sourceWidth / sourceHeight
- } (dimensions: ${sourceWidth}/${sourceHeight}).`
+ } (dimensions: ${sourceWidth}/${sourceHeight}).`,
);
ctx.drawImage(
@@ -741,7 +751,7 @@ export function FrameSynchronizedMessageHandler() {
0,
0,
targetWidth,
- targetHeight
+ targetHeight,
);
viewer.getRenderRequestState.current = "in_progress";
@@ -775,7 +785,7 @@ export function FrameSynchronizedMessageHandler() {
// If a render is requested, note that we don't handle any more messages
// until the render is done.
const requestRenderIndex = messageQueueRef.current.findIndex(
- (message) => message.type === "GetRenderRequestMessage"
+ (message) => message.type === "GetRenderRequestMessage",
);
const numMessages =
requestRenderIndex !== -1
diff --git a/src/viser/client/yarn.lock b/src/viser/client/yarn.lock
index 60ba30b6f..ac9877f56 100644
--- a/src/viser/client/yarn.lock
+++ b/src/viser/client/yarn.lock
@@ -3767,10 +3767,10 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
-prettier@^2.8.7:
- version "2.8.8"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
- integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
+prettier@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
+ integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"