Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RayClicks integration #106

Merged
merged 11 commits into from
Oct 11, 2023
5 changes: 0 additions & 5 deletions docs/source/gui_handles.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,4 @@ connected clients. When a GUI element is added to a client (for example, via
:undoc-members:
:inherited-members:

.. autoapiclass:: viser.GuiEvent
:members:
:undoc-members:
:inherited-members:

<!-- prettier-ignore-end -->
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ URL (default: `http://localhost:8080`).
./client_handles.md
./gui_handles.md
./scene_handles.md
./events.md
./icons.md


Expand Down
10 changes: 5 additions & 5 deletions docs/source/scene_handles.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ connected clients. When a scene node is added to a client (for example, via
:undoc-members:
:inherited-members:

.. autoapiclass:: viser.GlbHandle
:members:
:undoc-members:
:inherited-members:

.. autoapiclass:: viser.Gui3dContainerHandle
:members:
:undoc-members:
Expand Down Expand Up @@ -55,9 +60,4 @@ connected clients. When a scene node is added to a client (for example, via
:undoc-members:
:inherited-members:

.. autoapiclass:: viser.ClickEvent
:members:
:undoc-members:
:inherited-members:

<!-- prettier-ignore-end -->
4 changes: 2 additions & 2 deletions examples/06_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
name="/simple",
vertices=vertices,
faces=faces,
wxyz=tf.SO3.exp(onp.array([onp.pi / 2, 0.0, 0.0])).wxyz,
wxyz=tf.SO3.from_x_radians(onp.pi / 2).wxyz,
position=(0.0, 0.0, 0.0),
)
server.add_mesh_trimesh(
name="/trimesh",
mesh=mesh.smoothed(),
wxyz=tf.SO3.exp(onp.array([onp.pi / 2, 0.0, 0.0])).wxyz,
wxyz=tf.SO3.from_x_radians(onp.pi / 2).wxyz,
position=(0.0, 5.0, 0.0),
)

Expand Down
4 changes: 3 additions & 1 deletion src/viser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from ._gui_handles import GuiTabHandle as GuiTabHandle
from ._icons_enum import Icon as Icon
from ._scene_handles import CameraFrustumHandle as CameraFrustumHandle
from ._scene_handles import ClickEvent as ClickEvent
from ._scene_handles import FrameHandle as FrameHandle
from ._scene_handles import GlbHandle as GlbHandle
from ._scene_handles import Gui3dContainerHandle as Gui3dContainerHandle
Expand All @@ -20,6 +19,8 @@
from ._scene_handles import MeshHandle as MeshHandle
from ._scene_handles import PointCloudHandle as PointCloudHandle
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 TransformControlsHandle as TransformControlsHandle
from ._viser import CameraHandle as CameraHandle
from ._viser import ClientHandle as ClientHandle
Expand All @@ -28,3 +29,4 @@
if not TYPE_CHECKING:
# Backwards compatibility.
GuiHandle = GuiInputHandle
ClickEvent = SceneNodePointerEvent
98 changes: 91 additions & 7 deletions src/viser/_message_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@
import queue
import threading
import time
from typing import TYPE_CHECKING, Dict, Optional, Tuple, TypeVar, Union, cast
from typing import (
TYPE_CHECKING,
Callable,
Dict,
List,
Optional,
Tuple,
TypeVar,
Union,
cast,
)

import imageio.v3 as iio
import numpy as onp
Expand All @@ -27,7 +37,6 @@
from . import _messages, infra, theme
from ._scene_handles import (
CameraFrustumHandle,
ClickEvent,
FrameHandle,
GlbHandle,
Gui3dContainerHandle,
Expand All @@ -36,11 +45,14 @@
MeshHandle,
PointCloudHandle,
SceneNodeHandle,
SceneNodePointerEvent,
ScenePointerEvent,
TransformControlsHandle,
_TransformControlsState,
)

if TYPE_CHECKING:
from ._viser import ClientHandle
from .infra import ClientId


Expand Down Expand Up @@ -138,13 +150,21 @@ def __init__(self, handler: infra.MessageHandler) -> None:
] = {}
self._handle_from_node_name: Dict[str, SceneNodeHandle] = {}

# Callbacks for scene pointer events -- by default don't enable them.
self._scene_pointer_cb: List[Callable[[ScenePointerEvent], None]] = []
self._scene_pointer_enabled = False

handler.register_handler(
_messages.TransformControlsUpdateMessage,
self._handle_transform_controls_updates,
)
handler.register_handler(
_messages.SceneNodeClickedMessage,
self._handle_click_updates,
_messages.SceneNodeClickMessage,
self._handle_node_click_updates,
)
handler.register_handler(
_messages.ScenePointerMessage,
self._handle_scene_pointer_updates,
)

self._atomic_lock = threading.Lock()
Expand Down Expand Up @@ -598,6 +618,25 @@ def _queue_unsafe(self, message: _messages.Message) -> None:
"""Abstract method for sending messages."""
...

def _get_client_handle(self, client_id: ClientId) -> ClientHandle:
"""Private helper for getting a client handle from its ID."""
# Avoid circular imports.
from ._viser import ClientHandle, ViserServer

# Implementation-wise, note that MessageApi is never directly instantiated.
# Instead, it serves as a mixin/base class for either ViserServer, which
# maintains a registry of connected clients, or ClientHandle, which should
# only ever be dealing with its own client_id.
if isinstance(self, ViserServer):
# TODO: there's a potential race condition here when the client disconnects.
# This probably applies to multiple other parts of the code, we should
# revisit all of the cases where we index into connected_clients.
return self._state.connected_clients[client_id]
else:
assert isinstance(self, ClientHandle)
assert client_id == self.client_id
return self

def _handle_transform_controls_updates(
self, client_id: ClientId, message: _messages.TransformControlsUpdateMessage
) -> None:
Expand All @@ -617,17 +656,62 @@ def _handle_transform_controls_updates(
if handle._impl_aux.sync_cb is not None:
handle._impl_aux.sync_cb(client_id, handle)

def _handle_click_updates(
self, client_id: ClientId, message: _messages.SceneNodeClickedMessage
def _handle_node_click_updates(
self, client_id: ClientId, message: _messages.SceneNodeClickMessage
) -> None:
"""Callback for handling click messages."""
handle = self._handle_from_node_name.get(message.name, None)
if handle is None or handle._impl.click_cb is None:
return
for cb in handle._impl.click_cb:
event = ClickEvent(client_id=client_id, target=handle)
event = SceneNodePointerEvent(
client=self._get_client_handle(client_id),
client_id=client_id,
event="click",
target=handle,
ray_origin=message.ray_origin,
ray_direction=message.ray_direction,
)
cb(event) # type: ignore

def _handle_scene_pointer_updates(
self, client_id: ClientId, message: _messages.ScenePointerMessage
):
"""Callback for handling click messages."""
for cb in self._scene_pointer_cb:
event = ScenePointerEvent(
client=self._get_client_handle(client_id),
client_id=client_id,
event=message.event_type,
ray_origin=message.ray_origin,
ray_direction=message.ray_direction,
)
cb(event)

def on_scene_click(
self,
func: Callable[[ScenePointerEvent], None],
) -> Callable[[ScenePointerEvent], None]:
"""Add a callback for scene pointer events."""
self._scene_pointer_cb.append(func)

# Notify client of a new listener. This can help the client determine whether
# or not click events should still be sent; note that we have no way of knowing
# here because both server and client handles manage their own callbacks.
self._queue(_messages.ScenePointerCallbackInfoMessage(count=1))
return func

def remove_scene_click_callback(
self,
func: Callable[[ScenePointerEvent], None],
) -> None:
"""Check for the function handle in the list of callbacks and remove it."""
if func in self._scene_pointer_cb:
self._scene_pointer_cb.remove(func)

# Notify client that the listener has been removed.
self._queue(_messages.ScenePointerCallbackInfoMessage(count=-1))

def add_3d_gui_container(
self,
name: str,
Expand Down
24 changes: 23 additions & 1 deletion src/viser/_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ class ViewerCameraMessage(Message):
up_direction: Tuple[float, float, float]


@dataclasses.dataclass
class ScenePointerMessage(Message):
"""Message for a raycast-like pointer in the scene.
origin is the viewing camera position, in world coordinates.
direction is the vector if a ray is projected from the camera through the clicked pixel,
"""

# Later we can add `double_click`, `move`, `down`, `up`, etc.
event_type: Literal["click"]
ray_origin: Tuple[float, float, float]
ray_direction: Tuple[float, float, float]


@dataclasses.dataclass
class ScenePointerCallbackInfoMessage(Message):
"""Message to enable/disable scene pointer"""

count: int


@dataclasses.dataclass
class CameraFrustumMessage(Message):
"""Variant of CameraMessage used for visualizing camera frustums.
Expand Down Expand Up @@ -277,10 +297,12 @@ class SetSceneNodeClickableMessage(Message):


@dataclasses.dataclass
class SceneNodeClickedMessage(Message):
class SceneNodeClickMessage(Message):
"""Message for clicked objects."""

name: str
ray_origin: Tuple[float, float, float]
ray_direction: Tuple[float, float, float]


@dataclasses.dataclass
Expand Down
27 changes: 23 additions & 4 deletions src/viser/_scene_handles.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Dict,
Generic,
List,
Literal,
Optional,
Tuple,
Type,
Expand All @@ -26,6 +27,17 @@
from ._gui_api import GuiApi
from ._gui_handles import SupportsRemoveProtocol
from ._message_api import ClientId, MessageApi
from ._viser import ClientHandle


@dataclasses.dataclass(frozen=True)
class ScenePointerEvent:
client: ClientHandle
client_id: ClientId
# Later we can add `double_click`, `move`, `down`, `up`, etc
event: Literal["click"]
ray_origin: Tuple[float, float, float]
ray_direction: Tuple[float, float, float]


TSceneNodeHandle = TypeVar("TSceneNodeHandle", bound="SceneNodeHandle")
Expand All @@ -43,7 +55,9 @@ class _SceneNodeHandleState:
)
visible: bool = True
# TODO: we should remove SceneNodeHandle as an argument here.
click_cb: Optional[List[Callable[[ClickEvent[SceneNodeHandle]], None]]] = None
click_cb: Optional[
List[Callable[[SceneNodePointerEvent[SceneNodeHandle]], None]]
] = None


@dataclasses.dataclass
Expand Down Expand Up @@ -121,16 +135,21 @@ def remove(self) -> None:


@dataclasses.dataclass(frozen=True)
class ClickEvent(Generic[TSceneNodeHandle]):
class SceneNodePointerEvent(Generic[TSceneNodeHandle]):
client: ClientHandle
client_id: ClientId
event: Literal["click"]
target: TSceneNodeHandle
ray_origin: Tuple[float, float, float]
ray_direction: Tuple[float, float, float]


@dataclasses.dataclass
class _ClickableSceneNodeHandle(SceneNodeHandle):
def on_click(
self: TSceneNodeHandle, func: Callable[[ClickEvent[TSceneNodeHandle]], None]
) -> Callable[[ClickEvent[TSceneNodeHandle]], None]:
self: TSceneNodeHandle,
func: Callable[[SceneNodePointerEvent[TSceneNodeHandle]], None],
) -> Callable[[SceneNodePointerEvent[TSceneNodeHandle]], None]:
"""Attach a callback for when a scene node is clicked.
TODO:
Expand Down
4 changes: 4 additions & 0 deletions src/viser/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { BlendFunction, KernelSize } from "postprocessing";

import { SynchronizedCameraControls } from "./CameraControls";
import { ScenePointerControls } from "./ScenePointerControls";
import { Box, MantineProvider, MediaQuery } from "@mantine/core";
import React from "react";
import { SceneNodeThreeObject, UseSceneTree } from "./SceneTree";
Expand Down Expand Up @@ -67,6 +68,7 @@ export type ViewerContextContents = {
"ready" | "triggered" | "pause" | "in_progress"
>;
getRenderRequest: React.MutableRefObject<null | GetRenderRequestMessage>;
scenePointerCallbackCount: React.MutableRefObject<number>;
};
export const ViewerContext = React.createContext<null | ViewerContextContents>(
null,
Expand Down Expand Up @@ -108,6 +110,7 @@ function ViewerRoot() {
messageQueueRef: React.useRef([]),
getRenderRequestState: React.useRef("ready"),
getRenderRequest: React.useRef(null),
scenePointerCallbackCount: React.useRef(0),
};

return (
Expand Down Expand Up @@ -180,6 +183,7 @@ function ViewerCanvas({ children }: { children: React.ReactNode }) {
<AdaptiveEvents />
<SceneContextSetter />
<SynchronizedCameraControls />
<ScenePointerControls />
<Selection>
<SceneNodeThreeObject name="" parent={null} />
<EffectComposer enabled={true} autoClear={false}>
Expand Down
Loading
Loading