diff --git a/src/viser/__init__.py b/src/viser/__init__.py index 3fdd5522..99a6f38a 100644 --- a/src/viser/__init__.py +++ b/src/viser/__init__.py @@ -33,6 +33,8 @@ from ._scene_handles import SceneNodeHandle as SceneNodeHandle from ._scene_handles import SceneNodePointerEvent as SceneNodePointerEvent from ._scene_handles import ScenePointerEvent as ScenePointerEvent +from ._scene_handles import SplineCatmullRomHandle as SplineCatmullRomHandle +from ._scene_handles import SplineCubicBezierHandle as SplineCubicBezierHandle from ._scene_handles import SpotLightHandle as SpotLightHandle from ._scene_handles import TransformControlsHandle as TransformControlsHandle from ._viser import CameraHandle as CameraHandle diff --git a/src/viser/_messages.py b/src/viser/_messages.py index bafce2f4..6956c63e 100644 --- a/src/viser/_messages.py +++ b/src/viser/_messages.py @@ -96,12 +96,23 @@ class NotificationMessage(Message): mode: Literal["show", "update"] id: str + props: NotificationProps + + +@dataclasses.dataclass +class NotificationProps: title: str + """Title of the notification. For handles, synchronized automatically when assigned.""" body: str + """Body text of the notification. For handles, synchronized automatically when assigned.""" loading: bool + """Whether to show a loading indicator. For handles, synchronized automatically when assigned.""" with_close_button: bool + """Whether to show a close button. For handles, synchronized automatically when assigned.""" auto_close: Union[int, Literal[False]] + """Time in milliseconds after which the notification should auto-close, or False to disable auto-close. For handles, synchronized automatically when assigned.""" color: Optional[Color] + """Color of the notification. For handles, synchronized automatically when assigned.""" @dataclasses.dataclass @@ -970,7 +981,7 @@ def redundancy_key(self) -> str: @dataclasses.dataclass class SceneNodeUpdateMessage(Message): - """Sent client<->server when any property of a GUI component is changed.""" + """Sent client<->server when any property of a scene node is changed.""" name: str updates: Annotated[ diff --git a/src/viser/_notification_handle.py b/src/viser/_notification_handle.py index 0292af82..60a8f379 100644 --- a/src/viser/_notification_handle.py +++ b/src/viser/_notification_handle.py @@ -1,10 +1,9 @@ from __future__ import annotations import dataclasses -from typing import Literal +from typing import TYPE_CHECKING, Any, Literal -from ._gui_api import Color -from ._messages import NotificationMessage, RemoveNotificationMessage +from ._messages import NotificationMessage, NotificationProps, RemoveNotificationMessage from .infra._infra import WebsockClientConnection @@ -12,113 +11,37 @@ class _NotificationHandleState: websock_interface: WebsockClientConnection id: str - title: str - body: str - loading: bool - with_close_button: bool - auto_close: int | Literal[False] - color: Color | None + props: NotificationProps -@dataclasses.dataclass -class NotificationHandle: +class NotificationHandle(NotificationProps): """Handle for a notification in our visualizer.""" - _impl: _NotificationHandleState - - def _sync_with_client(self, first: bool = False) -> None: - m = NotificationMessage( - "show" if first else "update", - self._impl.id, - self._impl.title, - self._impl.body, - self._impl.loading, - self._impl.with_close_button, - self._impl.auto_close, - self._impl.color, - ) - self._impl.websock_interface.queue_message(m) - - @property - def title(self) -> str: - """Title to display on the notification.""" - return self._impl.title - - @title.setter - def title(self, title: str) -> None: - if title == self._impl.title: - return - - self._impl.title = title - self._sync_with_client() - - @property - def body(self) -> str: - """Message to display on the notification body.""" - return self._impl.body - - @body.setter - def body(self, body: str) -> None: - if body == self._impl.body: - return - - self._impl.body = body - self._sync_with_client() - - @property - def loading(self) -> bool: - """Whether the notification shows loading icon.""" - return self._impl.loading - - @loading.setter - def loading(self, loading: bool) -> None: - if loading == self._impl.loading: - return - - self._impl.loading = loading - self._sync_with_client() - - @property - def with_close_button(self) -> bool: - """Whether the notification can be manually closed.""" - return self._impl.with_close_button - - @with_close_button.setter - def with_close_button(self, with_close_button: bool) -> None: - if with_close_button == self._impl.with_close_button: - return - - self._impl.with_close_button = with_close_button - self._sync_with_client() - - @property - def auto_close(self) -> int | Literal[False]: - """Time in ms before the notification automatically closes; - otherwise False such that the notification never closes on its own.""" - return self._impl.auto_close - - @auto_close.setter - def auto_close(self, auto_close: int | Literal[False]) -> None: - if auto_close == self._impl.auto_close: - return + def __init__(self, impl: _NotificationHandleState) -> None: + self._impl = impl - self._impl.auto_close = auto_close - self._sync_with_client() + # Support property-style read/write. Similar to `_OverridableScenePropApi`. + if not TYPE_CHECKING: - @property - def color(self) -> Color | None: - """Color of the notification.""" - return self._impl.color + def __setattr__(self, name: str, value: Any) -> None: + if name in NotificationProps.__annotations__: + setattr(self._impl.props, name, value) + self._sync_with_client("update") + else: + return object.__setattr__(self, name, value) - @color.setter - def color(self, color: Color | None) -> None: - if color == self._impl.color: - return + def __getattr__(self, name: str) -> Any: + if name in NotificationProps.__annotations__: + return getattr(self._impl.props, name) + else: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) - self._impl.color = color - self._sync_with_client() + def _sync_with_client(self, mode: Literal["show", "update"]) -> None: + msg = NotificationMessage(mode, self._impl.id, self._impl.props) + self._impl.websock_interface.queue_message(msg) def remove(self) -> None: - self._impl.websock_interface.queue_message( - RemoveNotificationMessage(self._impl.id) - ) + msg = RemoveNotificationMessage(self._impl.id) + self._impl.websock_interface.queue_message(msg) diff --git a/src/viser/_scene_handles.py b/src/viser/_scene_handles.py index 7cc20e04..70505c68 100644 --- a/src/viser/_scene_handles.py +++ b/src/viser/_scene_handles.py @@ -40,7 +40,7 @@ def colors_to_uint8(colors: onp.ndarray) -> onpt.NDArray[onp.uint8]: return colors -class _OverridablePropSettersAndGetters: +class _OverridablePropScenePropSettersAndGetters: def __setattr__(self, name: str, value: Any) -> None: handle = cast(SceneNodeHandle, self) # Get the value of the T TypeVar. @@ -68,8 +68,8 @@ def __getattr__(self, name: str) -> Any: ) -class _OverridablePropApi( - _OverridablePropSettersAndGetters if not TYPE_CHECKING else object +class _OverridableScenePropApi( + _OverridablePropScenePropSettersAndGetters if not TYPE_CHECKING else object ): """Mixin that allows reading/assigning properties defined in each scene node message.""" @@ -270,7 +270,7 @@ def on_click( class CameraFrustumHandle( _ClickableSceneNodeHandle, _messages.CameraFrustumProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.CameraFrustumProps, ): """Handle for camera frustums.""" @@ -279,7 +279,7 @@ class CameraFrustumHandle( class DirectionalLightHandle( SceneNodeHandle, _messages.DirectionalLightProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.DirectionalLightProps, ): """Handle for directional lights.""" @@ -288,7 +288,7 @@ class DirectionalLightHandle( class AmbientLightHandle( SceneNodeHandle, _messages.AmbientLightProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.AmbientLightProps, ): """Handle for ambient lights.""" @@ -297,7 +297,7 @@ class AmbientLightHandle( class HemisphereLightHandle( SceneNodeHandle, _messages.HemisphereLightProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.HemisphereLightProps, ): """Handle for hemisphere lights.""" @@ -306,7 +306,7 @@ class HemisphereLightHandle( class PointLightHandle( SceneNodeHandle, _messages.PointLightProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.PointLightProps, ): """Handle for point lights.""" @@ -315,7 +315,7 @@ class PointLightHandle( class RectAreaLightHandle( SceneNodeHandle, _messages.RectAreaLightProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.RectAreaLightProps, ): """Handle for rectangular area lights.""" @@ -324,7 +324,7 @@ class RectAreaLightHandle( class SpotLightHandle( SceneNodeHandle, _messages.SpotLightProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.SpotLightProps, ): """Handle for spot lights.""" @@ -333,7 +333,7 @@ class SpotLightHandle( class PointCloudHandle( SceneNodeHandle, _messages.PointCloudProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.PointCloudProps, ): """Handle for point clouds. Does not support click events.""" @@ -342,7 +342,7 @@ class PointCloudHandle( class BatchedAxesHandle( _ClickableSceneNodeHandle, _messages.BatchedAxesProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.BatchedAxesProps, ): """Handle for batched coordinate frames.""" @@ -351,7 +351,7 @@ class BatchedAxesHandle( class FrameHandle( _ClickableSceneNodeHandle, _messages.FrameProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.FrameProps, ): """Handle for coordinate frames.""" @@ -360,7 +360,7 @@ class FrameHandle( class MeshHandle( _ClickableSceneNodeHandle, _messages.MeshProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.MeshProps, ): """Handle for mesh objects.""" @@ -369,7 +369,7 @@ class MeshHandle( class GaussianSplatHandle( _ClickableSceneNodeHandle, _messages.GaussianSplatsProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.GaussianSplatsProps, ): """Handle for Gaussian splatting objects. @@ -381,7 +381,7 @@ class GaussianSplatHandle( class MeshSkinnedHandle( _ClickableSceneNodeHandle, _messages.SkinnedMeshProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.SkinnedMeshProps, ): """Handle for skinned mesh objects.""" @@ -450,7 +450,7 @@ def position(self, position: tuple[float, float, float] | onp.ndarray) -> None: class GridHandle( SceneNodeHandle, _messages.GridProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.GridProps, ): """Handle for grid objects.""" @@ -459,7 +459,7 @@ class GridHandle( class SplineCatmullRomHandle( SceneNodeHandle, _messages.CatmullRomSplineProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.CatmullRomSplineProps, ): """Handle for Catmull-Rom splines.""" @@ -468,7 +468,7 @@ class SplineCatmullRomHandle( class SplineCubicBezierHandle( SceneNodeHandle, _messages.CubicBezierSplineProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.CubicBezierSplineProps, ): """Handle for cubic Bezier splines.""" @@ -477,7 +477,7 @@ class SplineCubicBezierHandle( class GlbHandle( _ClickableSceneNodeHandle, _messages.GlbProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.GlbProps, ): """Handle for GLB objects.""" @@ -486,7 +486,7 @@ class GlbHandle( class ImageHandle( _ClickableSceneNodeHandle, _messages.ImageProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.ImageProps, ): """Handle for 2D images, rendered in 3D.""" @@ -495,7 +495,7 @@ class ImageHandle( class LabelHandle( SceneNodeHandle, _messages.LabelProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.LabelProps, ): """Handle for 2D label objects. Does not support click events.""" @@ -511,7 +511,7 @@ class _TransformControlsState: class TransformControlsHandle( _ClickableSceneNodeHandle, _messages.TransformControlsProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.TransformControlsProps, ): """Handle for interacting with transform control gizmos.""" @@ -535,7 +535,7 @@ def on_update( class Gui3dContainerHandle( SceneNodeHandle, _messages.Gui3DProps, - _OverridablePropApi, + _OverridableScenePropApi, PropClass=_messages.Gui3DProps, ): """Use as a context to place GUI elements into a 3D GUI container.""" diff --git a/src/viser/_viser.py b/src/viser/_viser.py index 5b2d9b94..e56110fc 100644 --- a/src/viser/_viser.py +++ b/src/viser/_viser.py @@ -418,15 +418,17 @@ def add_notification( _NotificationHandleState( websock_interface=self._websock_connection, id=_make_unique_id(), - title=title, - body=body, - loading=loading, - with_close_button=with_close_button, - auto_close=auto_close, - color=color, + props=_messages.NotificationProps( + title=title, + body=body, + loading=loading, + with_close_button=with_close_button, + auto_close=auto_close, + color=color, + ), ) ) - handle._sync_with_client(first=True) + handle._sync_with_client("show") return handle diff --git a/src/viser/client/src/MessageHandler.tsx b/src/viser/client/src/MessageHandler.tsx index 895a38e5..6196f053 100644 --- a/src/viser/client/src/MessageHandler.tsx +++ b/src/viser/client/src/MessageHandler.tsx @@ -133,27 +133,15 @@ function useMessageHandler() { // Add a notification. case "NotificationMessage": { - if (message.mode === "show") { - notifications.show({ - id: message.id, - title: message.title, - message: message.body, - withCloseButton: message.with_close_button, - loading: message.loading, - autoClose: message.auto_close, - color: message.color ?? undefined, - }); - } else if (message.mode === "update") { - notifications.update({ - id: message.id, - title: message.title, - message: message.body, - withCloseButton: message.with_close_button, - loading: message.loading, - autoClose: message.auto_close, - color: message.color ?? undefined, - }); - } + (message.mode === "show" ? notifications.show : notifications.update)({ + id: message.id, + title: message.props.title, + message: message.props.body, + withCloseButton: message.props.with_close_button, + loading: message.props.loading, + autoClose: message.props.auto_close, + color: message.props.color ?? undefined, + }); return; } diff --git a/src/viser/client/src/WebsocketMessages.ts b/src/viser/client/src/WebsocketMessages.ts index 4f1bfb73..a78ba205 100644 --- a/src/viser/client/src/WebsocketMessages.ts +++ b/src/viser/client/src/WebsocketMessages.ts @@ -18,27 +18,29 @@ export interface NotificationMessage { type: "NotificationMessage"; mode: "show" | "update"; id: string; - title: string; - body: string; - loading: boolean; - with_close_button: boolean; - auto_close: number | false; - color: - | "dark" - | "gray" - | "red" - | "pink" - | "grape" - | "violet" - | "indigo" - | "blue" - | "cyan" - | "green" - | "lime" - | "yellow" - | "orange" - | "teal" - | null; + props: { + title: string; + body: string; + loading: boolean; + with_close_button: boolean; + auto_close: number | false; + color: + | "dark" + | "gray" + | "red" + | "pink" + | "grape" + | "violet" + | "indigo" + | "blue" + | "cyan" + | "green" + | "lime" + | "yellow" + | "orange" + | "teal" + | null; + }; } /** Remove a specific notification. * @@ -928,7 +930,7 @@ export interface GuiUpdateMessage { id: string; updates: Partial; } -/** Sent client<->server when any property of a GUI component is changed. +/** Sent client<->server when any property of a scene node is changed. * * (automatically generated) */