diff --git a/examples/15_gui_in_scene.py b/examples/15_gui_in_scene.py index 5c963c9e3..9e6cb3014 100644 --- a/examples/15_gui_in_scene.py +++ b/examples/15_gui_in_scene.py @@ -3,10 +3,8 @@ # Asymmetric properties are supported in Pyright, but not yet in mypy. # - https://github.com/python/mypy/issues/3004 # - https://github.com/python/mypy/pull/11643 -"""Camera commands +"""3D GUI Elements -In addition to reads, camera parameters also support writes. These are synced to the -corresponding client automatically. """ import time @@ -31,6 +29,7 @@ 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. @@ -40,20 +39,28 @@ def make_frame(i: int) -> None: # Create a coordinate frame and label. frame = client.add_frame(f"/frame_{i}", wxyz=wxyz, position=position) - client.add_label(f"/frame_{i}/label", text=f"Frame {i}") # Move the camera when we click a frame. @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() - displayed_3d_container = client.add_gui_3d_container(f"/frame_{i}/gui") + # 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") randomize_orientation = client.add_gui_button("Randomize orientation") - close = client.add_gui_button("[Close]") + close = client.add_gui_button("Close GUI") @go_to.on_click def _(_) -> None: @@ -90,10 +97,12 @@ 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/viser/__init__.py b/viser/__init__.py index 27f2f5dbd..37912f52a 100644 --- a/viser/__init__.py +++ b/viser/__init__.py @@ -1,4 +1,3 @@ -from ._gui_api import Gui3dContainerHandle as Gui3dContainerHandle from ._gui_api import GuiFolderHandle as GuiFolderHandle from ._gui_api import GuiTabGroupHandle as GuiTabGroupHandle from ._gui_api import GuiTabHandle as GuiTabHandle @@ -9,6 +8,7 @@ from ._icons_enum import Icon as Icon from ._scene_handles import CameraFrustumHandle as CameraFrustumHandle from ._scene_handles import FrameHandle as FrameHandle +from ._scene_handles import Gui3dContainerHandle as Gui3dContainerHandle from ._scene_handles import ImageHandle as ImageHandle from ._scene_handles import LabelHandle as LabelHandle from ._scene_handles import MeshHandle as MeshHandle diff --git a/viser/_gui_api.py b/viser/_gui_api.py index 5723b2d44..19d866418 100644 --- a/viser/_gui_api.py +++ b/viser/_gui_api.py @@ -111,26 +111,6 @@ def gui_folder(self, label: str) -> GuiFolderHandle: ) return self.add_gui_folder(label) - def add_gui_3d_container( - self, - 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), - ) -> Gui3dContainerHandle: - """Add a 3D gui to the scene.""" - print("here") - container_id = _make_unique_id() - self._get_api()._queue( - _messages.Gui3DMessage( - order=time.time(), - name=name, - container_id=container_id, - ) - ) - assert isinstance(self, MessageApi) - node_handle = SceneNodeHandle._make(self, name, wxyz, position) - return Gui3dContainerHandle(node_handle._impl, self, container_id) - def add_gui_folder(self, label: str) -> GuiFolderHandle: """Add a folder, and return a handle that can be used to populate it.""" folder_container_id = _make_unique_id() @@ -690,37 +670,6 @@ def _sync_with_client(self) -> None: ) -@dataclasses.dataclass -class Gui3dContainerHandle(SceneNodeHandle): - """Use as a context to place GUI elements into a folder.""" - - _gui_api: GuiApi - _container_id: str - _container_id_restore: Optional[str] = None - - def __enter__(self) -> None: - self._container_id_restore = self._gui_api._get_container_id() - self._gui_api._set_container_id(self._container_id) - - def __exit__(self, *args) -> None: - del args - assert self._container_id_restore is not None - self._gui_api._set_container_id(self._container_id_restore) - self._container_id_restore = None - - def remove(self) -> None: - """Permanently remove this GUI container - visualizer.""" - - # Call scene node remove. - super().remove() - - # Clean up contained GUI elements. - self._gui_api._get_api()._queue( - _messages.GuiRemoveContainerChildrenMessage(self._container_id) - ) - - @dataclasses.dataclass class GuiFolderHandle: """Use as a context to place GUI elements into a folder.""" diff --git a/viser/_message_api.py b/viser/_message_api.py index 12da10cb5..1bce7c9ed 100644 --- a/viser/_message_api.py +++ b/viser/_message_api.py @@ -27,6 +27,7 @@ from ._scene_handles import ( CameraFrustumHandle, FrameHandle, + Gui3dContainerHandle, ImageHandle, LabelHandle, MeshHandle, @@ -521,3 +522,28 @@ def _handle_click_updates( return for cb in handle._impl.click_cb: cb(handle) + + def add_3d_gui_container( + self, + 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), + ) -> 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.""" + + # Avoids circular import. + from ._gui_api import GuiApi, _make_unique_id + + container_id = _make_unique_id() + self._queue( + _messages.Gui3DMessage( + order=time.time(), + name=name, + container_id=container_id, + ) + ) + assert isinstance(self, MessageApi) + node_handle = SceneNodeHandle._make(self, name, wxyz, position) + assert isinstance(self, GuiApi) + return Gui3dContainerHandle(node_handle._impl, self, container_id) diff --git a/viser/_scene_handles.py b/viser/_scene_handles.py index c396e326e..d40a31799 100644 --- a/viser/_scene_handles.py +++ b/viser/_scene_handles.py @@ -13,6 +13,7 @@ from . import _messages if TYPE_CHECKING: + from ._gui_api import GuiApi from ._message_api import ClientId, MessageApi @@ -201,3 +202,33 @@ def on_update( """Attach a callback for when the gizmo is moved.""" self._impl_aux.update_cb.append(func) return func + + +@dataclasses.dataclass +class Gui3dContainerHandle(SceneNodeHandle): + """Use as a context to place GUI elements into a 3D GUI container.""" + + _gui_api: GuiApi + _container_id: str + _container_id_restore: Optional[str] = None + + def __enter__(self) -> None: + self._container_id_restore = self._gui_api._get_container_id() + self._gui_api._set_container_id(self._container_id) + + def __exit__(self, *args) -> None: + del args + assert self._container_id_restore is not None + self._gui_api._set_container_id(self._container_id_restore) + self._container_id_restore = None + + def remove(self) -> None: + """Permanently remove this GUI container from the visualizer.""" + + # Call scene node remove. + super().remove() + + # Clean up contained GUI elements. + self._gui_api._get_api()._queue( + _messages.GuiRemoveContainerChildrenMessage(self._container_id) + ) diff --git a/viser/client/src/ControlPanel/Generated.tsx b/viser/client/src/ControlPanel/Generated.tsx index d3b181905..9213839e7 100644 --- a/viser/client/src/ControlPanel/Generated.tsx +++ b/viser/client/src/ControlPanel/Generated.tsx @@ -31,14 +31,10 @@ export default function GeneratedGuiContainer({ containerId: string; viewer?: ViewerContextContents; }) { - if (viewer === undefined) { - viewer = React.useContext(ViewerContext)!; - } - if (viewer === null) { - return null; - } + if (viewer === undefined) viewer = React.useContext(ViewerContext)!; + const guiIdSet = viewer.useGui( - (state) => state.guiIdSetFromContainerId[containerId], + (state) => state.guiIdSetFromContainerId[containerId] ); const guiConfigFromId = viewer.useGui((state) => state.guiConfigFromId); @@ -427,7 +423,7 @@ function VectorInput( precision: number; onChange: (value: number[]) => void; disabled: boolean; - }, + } ) { return (