From ba989e30baaf95de85ff0dc7ada68935a4d9c476 Mon Sep 17 00:00:00 2001 From: Brent Yi Date: Wed, 22 May 2024 20:22:35 +0100 Subject: [PATCH] Cleanup --- docs/source/examples/00_coordinate_frames.rst | 6 +- docs/source/examples/01_image.rst | 6 +- docs/source/examples/02_gui.rst | 32 +-- docs/source/examples/03_gui_callbacks.rst | 30 +- docs/source/examples/04_camera_poses.rst | 4 +- docs/source/examples/05_camera_commands.rst | 4 +- docs/source/examples/06_mesh.rst | 4 +- .../examples/07_record3d_visualizer.rst | 27 +- docs/source/examples/08_smpl_visualizer.rst | 41 ++- docs/source/examples/09_urdf_visualizer.rst | 9 +- docs/source/examples/10_realsense.rst | 7 +- docs/source/examples/11_colmap_visualizer.rst | 18 +- docs/source/examples/12_click_meshes.rst | 10 +- docs/source/examples/13_theming.rst | 24 +- docs/source/examples/14_markdown.rst | 10 +- docs/source/examples/15_gui_in_scene.rst | 10 +- docs/source/examples/16_modal.rst | 14 +- .../examples/17_background_composite.rst | 4 +- docs/source/examples/18_splines.rst | 4 +- docs/source/examples/19_get_renders.rst | 6 +- docs/source/examples/20_scene_pointer.rst | 34 +-- docs/source/examples/21_set_up_direction.rst | 6 +- docs/source/examples/22_games.rst | 24 +- docs/source/examples/23_plotly.rst | 6 +- examples/07_record3d_visualizer.py | 3 +- examples/08_smpl_visualizer.py | 13 +- examples/09_urdf_visualizer.py | 5 +- examples/10_realsense.py | 3 +- examples/20_scene_pointer.py | 4 +- src/viser/_gui_api.py | 271 ++++++++---------- src/viser/_gui_handles.py | 66 ++--- src/viser/_scene_api.py | 151 ++++------ src/viser/_scene_handles.py | 38 +-- src/viser/_tunnel.py | 22 +- src/viser/_viser.py | 98 +++---- src/viser/extras/_urdf.py | 3 +- 36 files changed, 463 insertions(+), 554 deletions(-) diff --git a/docs/source/examples/00_coordinate_frames.rst b/docs/source/examples/00_coordinate_frames.rst index b0bd33d27..62a11f919 100644 --- a/docs/source/examples/00_coordinate_frames.rst +++ b/docs/source/examples/00_coordinate_frames.rst @@ -25,17 +25,17 @@ relative to /tree. while True: # Add some coordinate frames to the scene. These will be visualized in the viewer. - server.add_frame( + server.scene.add_frame( "/tree", wxyz=(1.0, 0.0, 0.0, 0.0), position=(random.random() * 2.0, 2.0, 0.2), ) - server.add_frame( + server.scene.add_frame( "/tree/branch", wxyz=(1.0, 0.0, 0.0, 0.0), position=(random.random() * 2.0, 2.0, 0.2), ) - leaf = server.add_frame( + leaf = server.scene.add_frame( "/tree/branch/leaf", wxyz=(1.0, 0.0, 0.0, 0.0), position=(random.random() * 2.0, 2.0, 0.2), diff --git a/docs/source/examples/01_image.rst b/docs/source/examples/01_image.rst index 4f3c42698..fae604cd5 100644 --- a/docs/source/examples/01_image.rst +++ b/docs/source/examples/01_image.rst @@ -28,13 +28,13 @@ NeRFs), or images to render as 3D textures. server = viser.ViserServer() # Add a background image. - server.set_background_image( + server.scene.set_background_image( iio.imread(Path(__file__).parent / "assets/Cal_logo.png"), format="png", ) # Add main image. - server.add_image( + server.scene.add_image( "/img", iio.imread(Path(__file__).parent / "assets/Cal_logo.png"), 4.0, @@ -44,7 +44,7 @@ NeRFs), or images to render as 3D textures. position=(2.0, 2.0, 0.0), ) while True: - server.add_image( + server.scene.add_image( "/noise", onp.random.randint( 0, diff --git a/docs/source/examples/02_gui.rst b/docs/source/examples/02_gui.rst index 66ef1f5ae..2d3178eb6 100644 --- a/docs/source/examples/02_gui.rst +++ b/docs/source/examples/02_gui.rst @@ -23,14 +23,14 @@ Examples of basic GUI elements that we can create, read from, and write to. server = viser.ViserServer() # Add some common GUI elements: number inputs, sliders, vectors, checkboxes. - with server.add_gui_folder("Read-only"): - gui_counter = server.add_gui_number( + with server.gui.add_folder("Read-only"): + gui_counter = server.gui.add_number( "Counter", initial_value=0, disabled=True, ) - gui_slider = server.add_gui_slider( + gui_slider = server.gui.add_slider( "Slider", min=0, max=100, @@ -39,36 +39,36 @@ Examples of basic GUI elements that we can create, read from, and write to. disabled=True, ) - with server.add_gui_folder("Editable"): - gui_vector2 = server.add_gui_vector2( + with server.gui.add_folder("Editable"): + gui_vector2 = server.gui.add_vector2( "Position", initial_value=(0.0, 0.0), step=0.1, ) - gui_vector3 = server.add_gui_vector3( + gui_vector3 = server.gui.add_vector3( "Size", initial_value=(1.0, 1.0, 1.0), step=0.25, ) - with server.add_gui_folder("Text toggle"): - gui_checkbox_hide = server.add_gui_checkbox( + with server.gui.add_folder("Text toggle"): + gui_checkbox_hide = server.gui.add_checkbox( "Hide", initial_value=False, ) - gui_text = server.add_gui_text( + gui_text = server.gui.add_text( "Text", initial_value="Hello world", ) - gui_button = server.add_gui_button("Button") - gui_checkbox_disable = server.add_gui_checkbox( + gui_button = server.gui.add_button("Button") + gui_checkbox_disable = server.gui.add_checkbox( "Disable", initial_value=False, ) - gui_rgb = server.add_gui_rgb( + gui_rgb = server.gui.add_rgb( "Color", initial_value=(255, 255, 0), ) - gui_multi_slider = server.add_gui_multi_slider( + gui_multi_slider = server.gui.add_multi_slider( "Multi slider", min=0, max=100, @@ -76,7 +76,7 @@ Examples of basic GUI elements that we can create, read from, and write to. initial_value=(0, 30, 100), marks=((0, "0"), (50, "5"), (70, "7"), 99), ) - gui_slider_positions = server.add_gui_slider( + gui_slider_positions = server.gui.add_slider( "# sliders", min=0, max=10, @@ -84,7 +84,7 @@ Examples of basic GUI elements that we can create, read from, and write to. initial_value=3, marks=((0, "0"), (5, "5"), (7, "7"), 10), ) - gui_upload_button = server.add_gui_upload_button( + gui_upload_button = server.gui.add_upload_button( "Upload", icon=viser.Icon.UPLOAD ) @@ -108,7 +108,7 @@ Examples of basic GUI elements that we can create, read from, and write to. # We can set the position of a scene node with `.position`, and read the value # of a gui element with `.value`. Changes are automatically reflected in # connected clients. - server.add_point_cloud( + server.scene.add_point_cloud( "/point_cloud", points=point_positions * onp.array(gui_vector3.value, dtype=onp.float32), colors=( diff --git a/docs/source/examples/03_gui_callbacks.rst b/docs/source/examples/03_gui_callbacks.rst index d219f9bd8..5eac92096 100644 --- a/docs/source/examples/03_gui_callbacks.rst +++ b/docs/source/examples/03_gui_callbacks.rst @@ -24,14 +24,14 @@ we get updates. def main() -> None: server = viser.ViserServer() - gui_reset_scene = server.add_gui_button("Reset Scene") + gui_reset_scene = server.gui.add_button("Reset Scene") - gui_plane = server.add_gui_dropdown( + gui_plane = server.gui.add_dropdown( "Grid plane", ("xz", "xy", "yx", "yz", "zx", "zy") ) def update_plane() -> None: - server.add_grid( + server.scene.add_grid( "/grid", width=10.0, height=20.0, @@ -42,23 +42,23 @@ we get updates. gui_plane.on_update(lambda _: update_plane()) - with server.add_gui_folder("Control"): - gui_show_frame = server.add_gui_checkbox("Show Frame", initial_value=True) - gui_show_everything = server.add_gui_checkbox( + with server.gui.add_folder("Control"): + gui_show_frame = server.gui.add_checkbox("Show Frame", initial_value=True) + gui_show_everything = server.gui.add_checkbox( "Show Everything", initial_value=True ) - gui_axis = server.add_gui_dropdown("Axis", ("x", "y", "z")) - gui_include_z = server.add_gui_checkbox("Z in dropdown", initial_value=True) + gui_axis = server.gui.add_dropdown("Axis", ("x", "y", "z")) + gui_include_z = server.gui.add_checkbox("Z in dropdown", initial_value=True) @gui_include_z.on_update def _(_) -> None: gui_axis.options = ("x", "y", "z") if gui_include_z.value else ("x", "y") - with server.add_gui_folder("Sliders"): - gui_location = server.add_gui_slider( + with server.gui.add_folder("Sliders"): + gui_location = server.gui.add_slider( "Location", min=-5.0, max=5.0, step=0.05, initial_value=0.0 ) - gui_num_points = server.add_gui_slider( + gui_num_points = server.gui.add_slider( "# Points", min=1000, max=200_000, step=1000, initial_value=10_000 ) @@ -73,7 +73,7 @@ we get updates. else: assert_never(axis) - server.add_frame( + server.scene.add_frame( "/frame", wxyz=(1.0, 0.0, 0.0, 0.0), position=pos, @@ -83,7 +83,7 @@ we get updates. def draw_points() -> None: num_points = gui_num_points.value - server.add_point_cloud( + server.scene.add_point_cloud( "/frame/point_cloud", points=onp.random.normal(size=(num_points, 3)), colors=onp.random.randint(0, 256, size=(num_points, 3)), @@ -93,7 +93,9 @@ we get updates. # Here, we update the point clouds + frames whenever any of the GUI items are updated. gui_show_frame.on_update(lambda _: draw_frame()) gui_show_everything.on_update( - lambda _: server.set_global_scene_node_visibility(gui_show_everything.value) + lambda _: server.scene.set_global_scene_node_visibility( + gui_show_everything.value + ) ) gui_axis.on_update(lambda _: draw_frame()) gui_location.on_update(lambda _: draw_frame()) diff --git a/docs/source/examples/04_camera_poses.rst b/docs/source/examples/04_camera_poses.rst index ac5c9127f..2676e96d7 100644 --- a/docs/source/examples/04_camera_poses.rst +++ b/docs/source/examples/04_camera_poses.rst @@ -18,7 +18,7 @@ Example showing how we can detect new clients and read camera poses from them. import viser server = viser.ViserServer() - server.world_axes.visible = True + server.scene.world_axes.visible = True @server.on_client_connect @@ -31,7 +31,7 @@ Example showing how we can detect new clients and read camera poses from them. print(f"New camera on client {client.client_id}!") # Show the client ID in the GUI. - gui_info = client.add_gui_text("Client ID", initial_value=str(client.client_id)) + gui_info = client.gui.add_text("Client ID", initial_value=str(client.client_id)) gui_info.disabled = True diff --git a/docs/source/examples/05_camera_commands.rst b/docs/source/examples/05_camera_commands.rst index 636c6002e..95ee19a07 100644 --- a/docs/source/examples/05_camera_commands.rst +++ b/docs/source/examples/05_camera_commands.rst @@ -40,8 +40,8 @@ corresponding client automatically. position = rng.uniform(-3.0, 3.0, size=(3,)) # 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}") + frame = client.scene.add_frame(f"/frame_{i}", wxyz=wxyz, position=position) + client.scene.add_label(f"/frame_{i}/label", text=f"Frame {i}") # Move the camera when we click a frame. @frame.on_click diff --git a/docs/source/examples/06_mesh.rst b/docs/source/examples/06_mesh.rst index 918a45f98..15c1e718b 100644 --- a/docs/source/examples/06_mesh.rst +++ b/docs/source/examples/06_mesh.rst @@ -30,14 +30,14 @@ Visualize a mesh. To get the demo data, see ``./assets/download_dragon_mesh.sh`` print(f"Loaded mesh with {vertices.shape} vertices, {faces.shape} faces") server = viser.ViserServer() - server.add_mesh_simple( + server.scene.add_mesh_simple( name="/simple", vertices=vertices, faces=faces, wxyz=tf.SO3.from_x_radians(onp.pi / 2).wxyz, position=(0.0, 0.0, 0.0), ) - server.add_mesh_trimesh( + server.scene.add_mesh_trimesh( name="/trimesh", mesh=mesh.smoothed(), wxyz=tf.SO3.from_x_radians(onp.pi / 2).wxyz, diff --git a/docs/source/examples/07_record3d_visualizer.rst b/docs/source/examples/07_record3d_visualizer.rst index 9b0d85cdf..e7f28be00 100644 --- a/docs/source/examples/07_record3d_visualizer.rst +++ b/docs/source/examples/07_record3d_visualizer.rst @@ -15,7 +15,6 @@ Parse and stream record3d captures. To get the demo data, see ``./assets/downloa import time from pathlib import Path - from typing import List import numpy as onp import tyro @@ -40,8 +39,8 @@ Parse and stream record3d captures. To get the demo data, see ``./assets/downloa num_frames = min(max_frames, loader.num_frames()) # Add playback UI. - with server.add_gui_folder("Playback"): - gui_timestep = server.add_gui_slider( + with server.gui.add_folder("Playback"): + gui_timestep = server.gui.add_slider( "Timestep", min=0, max=num_frames - 1, @@ -49,13 +48,13 @@ Parse and stream record3d captures. To get the demo data, see ``./assets/downloa initial_value=0, disabled=True, ) - gui_next_frame = server.add_gui_button("Next Frame", disabled=True) - gui_prev_frame = server.add_gui_button("Prev Frame", disabled=True) - gui_playing = server.add_gui_checkbox("Playing", True) - gui_framerate = server.add_gui_slider( + gui_next_frame = server.gui.add_button("Next Frame", disabled=True) + gui_prev_frame = server.gui.add_button("Prev Frame", disabled=True) + gui_playing = server.gui.add_checkbox("Playing", True) + gui_framerate = server.gui.add_slider( "FPS", min=1, max=60, step=0.1, initial_value=loader.fps ) - gui_framerate_options = server.add_gui_button_group( + gui_framerate_options = server.gui.add_button_group( "FPS options", ("10", "20", "30", "60") ) @@ -94,22 +93,22 @@ Parse and stream record3d captures. To get the demo data, see ``./assets/downloa server.flush() # Optional! # Load in frames. - server.add_frame( + server.scene.add_frame( "/frames", wxyz=tf.SO3.exp(onp.array([onp.pi / 2.0, 0.0, 0.0])).wxyz, position=(0, 0, 0), show_axes=False, ) - frame_nodes: List[viser.FrameHandle] = [] + frame_nodes: list[viser.FrameHandle] = [] for i in tqdm(range(num_frames)): frame = loader.get_frame(i) position, color = frame.get_point_cloud(downsample_factor) # Add base frame. - frame_nodes.append(server.add_frame(f"/frames/t{i}", show_axes=False)) + frame_nodes.append(server.scene.add_frame(f"/frames/t{i}", show_axes=False)) # Place the point cloud in the frame. - server.add_point_cloud( + server.scene.add_point_cloud( name=f"/frames/t{i}/point_cloud", points=position, colors=color, @@ -120,7 +119,7 @@ Parse and stream record3d captures. To get the demo data, see ``./assets/downloa # Place the frustum. fov = 2 * onp.arctan2(frame.rgb.shape[0] / 2, frame.K[0, 0]) aspect = frame.rgb.shape[1] / frame.rgb.shape[0] - server.add_camera_frustum( + server.scene.add_camera_frustum( f"/frames/t{i}/frustum", fov=fov, aspect=aspect, @@ -131,7 +130,7 @@ Parse and stream record3d captures. To get the demo data, see ``./assets/downloa ) # Add some axes. - server.add_frame( + server.scene.add_frame( f"/frames/t{i}/frustum/axes", axes_length=0.05, axes_radius=0.005, diff --git a/docs/source/examples/08_smpl_visualizer.rst b/docs/source/examples/08_smpl_visualizer.rst index 9f8de6d6f..9883b78c1 100644 --- a/docs/source/examples/08_smpl_visualizer.rst +++ b/docs/source/examples/08_smpl_visualizer.rst @@ -19,7 +19,6 @@ See here for download instructions: import time from dataclasses import dataclass from pathlib import Path - from typing import List, Tuple import numpy as np import numpy as onp @@ -83,8 +82,8 @@ See here for download instructions: def main(model_path: Path) -> None: server = viser.ViserServer() - server.set_up_direction("+y") - server.configure_theme(control_layout="collapsible") + server.scene.set_up_direction("+y") + server.gui.configure_theme(control_layout="collapsible") # Main loop. We'll read pose/shape from the GUI elements, compute the mesh, # and then send the updated mesh in a loop. @@ -111,7 +110,7 @@ See here for download instructions: np.array([x.value for x in gui_elements.gui_joints]) ).as_matrix(), ) - server.add_mesh_simple( + server.scene.add_mesh_simple( "/human", smpl_outputs.vertices, smpl_outputs.faces, @@ -128,11 +127,11 @@ See here for download instructions: class GuiElements: """Structure containing handles for reading from GUI elements.""" - gui_rgb: viser.GuiInputHandle[Tuple[int, int, int]] + gui_rgb: viser.GuiInputHandle[tuple[int, int, int]] gui_wireframe: viser.GuiInputHandle[bool] - gui_betas: List[viser.GuiInputHandle[float]] - gui_joints: List[viser.GuiInputHandle[Tuple[float, float, float]]] - transform_controls: List[viser.TransformControlsHandle] + gui_betas: list[viser.GuiInputHandle[float]] + gui_joints: list[viser.GuiInputHandle[tuple[float, float, float]]] + transform_controls: list[viser.TransformControlsHandle] changed: bool """This flag will be flipped to True whenever the mesh needs to be re-generated.""" @@ -146,16 +145,16 @@ See here for download instructions: ) -> GuiElements: """Make GUI elements for interacting with the model.""" - tab_group = server.add_gui_tab_group() + tab_group = server.gui.add_tab_group() def set_changed(_) -> None: out.changed = True # out is define later! # GUI elements: mesh settings + visibility. with tab_group.add_tab("View", viser.Icon.VIEWFINDER): - gui_rgb = server.add_gui_rgb("Color", initial_value=(90, 200, 255)) - gui_wireframe = server.add_gui_checkbox("Wireframe", initial_value=False) - gui_show_controls = server.add_gui_checkbox("Handles", initial_value=False) + gui_rgb = server.gui.add_rgb("Color", initial_value=(90, 200, 255)) + gui_wireframe = server.gui.add_checkbox("Wireframe", initial_value=False) + gui_show_controls = server.gui.add_checkbox("Handles", initial_value=False) gui_rgb.on_update(set_changed) gui_wireframe.on_update(set_changed) @@ -167,8 +166,8 @@ See here for download instructions: # GUI elements: shape parameters. with tab_group.add_tab("Shape", viser.Icon.BOX): - gui_reset_shape = server.add_gui_button("Reset Shape") - gui_random_shape = server.add_gui_button("Random Shape") + gui_reset_shape = server.gui.add_button("Reset Shape") + gui_random_shape = server.gui.add_button("Random Shape") @gui_reset_shape.on_click def _(_): @@ -182,7 +181,7 @@ See here for download instructions: gui_betas = [] for i in range(num_betas): - beta = server.add_gui_slider( + beta = server.gui.add_slider( f"beta{i}", min=-5.0, max=5.0, step=0.01, initial_value=0.0 ) gui_betas.append(beta) @@ -190,8 +189,8 @@ See here for download instructions: # GUI elements: joint angles. with tab_group.add_tab("Joints", viser.Icon.ANGLE): - gui_reset_joints = server.add_gui_button("Reset Joints") - gui_random_joints = server.add_gui_button("Random Joints") + gui_reset_joints = server.gui.add_button("Reset Joints") + gui_random_joints = server.gui.add_button("Random Joints") @gui_reset_joints.on_click def _(_): @@ -207,9 +206,9 @@ See here for download instructions: quat /= onp.linalg.norm(quat) joint.value = tf.SO3(wxyz=quat).log() - gui_joints: List[viser.GuiInputHandle[Tuple[float, float, float]]] = [] + gui_joints: list[viser.GuiInputHandle[tuple[float, float, float]]] = [] for i in range(num_joints): - gui_joint = server.add_gui_vector3( + gui_joint = server.gui.add_vector3( label=f"Joint {i}", initial_value=(0.0, 0.0, 0.0), step=0.05, @@ -227,7 +226,7 @@ See here for download instructions: set_callback_in_closure(i) # Transform control gizmos on joints. - transform_controls: List[viser.TransformControlsHandle] = [] + transform_controls: list[viser.TransformControlsHandle] = [] prefixed_joint_names = [] # Joint names, but prefixed with parents. for i in range(num_joints): prefixed_joint_name = f"joint_{i}" @@ -236,7 +235,7 @@ See here for download instructions: prefixed_joint_names[parent_idx[i]] + "/" + prefixed_joint_name ) prefixed_joint_names.append(prefixed_joint_name) - controls = server.add_transform_controls( + controls = server.scene.add_transform_controls( f"/smpl/{prefixed_joint_name}", depth_test=False, scale=0.2 * (0.75 ** prefixed_joint_name.count("/")), diff --git a/docs/source/examples/09_urdf_visualizer.rst b/docs/source/examples/09_urdf_visualizer.rst index 5d05b8a75..b8c611cae 100644 --- a/docs/source/examples/09_urdf_visualizer.rst +++ b/docs/source/examples/09_urdf_visualizer.rst @@ -23,7 +23,6 @@ Examples: import time from pathlib import Path - from typing import List import numpy as onp import tyro @@ -39,14 +38,14 @@ Examples: urdf = ViserUrdf(server, urdf_path) # Create joint angle sliders. - gui_joints: List[viser.GuiInputHandle[float]] = [] - initial_angles: List[float] = [] + gui_joints: list[viser.GuiInputHandle[float]] = [] + initial_angles: list[float] = [] for joint_name, (lower, upper) in urdf.get_actuated_joint_limits().items(): lower = lower if lower is not None else -onp.pi upper = upper if upper is not None else onp.pi initial_angle = 0.0 if lower < 0 and upper > 0 else (lower + upper) / 2.0 - slider = server.add_gui_slider( + slider = server.gui.add_slider( label=joint_name, min=lower, max=upper, @@ -61,7 +60,7 @@ Examples: initial_angles.append(initial_angle) # Create joint reset button. - reset_button = server.add_gui_button("Reset") + reset_button = server.gui.add_button("Reset") @reset_button.on_click def _(_): diff --git a/docs/source/examples/10_realsense.rst b/docs/source/examples/10_realsense.rst index 9eb748560..39b180e29 100644 --- a/docs/source/examples/10_realsense.rst +++ b/docs/source/examples/10_realsense.rst @@ -15,7 +15,6 @@ pyrealsense2. import contextlib - from typing import Tuple import numpy as np import numpy.typing as npt @@ -49,7 +48,7 @@ pyrealsense2. def point_cloud_arrays_from_frames( depth_frame, color_frame - ) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.uint8]]: + ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.uint8]]: """Maps realsense frames to two arrays. Returns: @@ -101,7 +100,7 @@ pyrealsense2. def main(): # Start visualization server. - viser_server = viser.ViserServer() + server = viser.ViserServer() with realsense_pipeline() as pipeline: for i in tqdm(range(10000000)): @@ -124,7 +123,7 @@ pyrealsense2. positions = positions @ R.T # Visualize. - viser_server.add_point_cloud( + server.scene.add_point_cloud( "/realsense", points=positions * 10.0, colors=colors, diff --git a/docs/source/examples/11_colmap_visualizer.rst b/docs/source/examples/11_colmap_visualizer.rst index b26d04bc4..565687183 100644 --- a/docs/source/examples/11_colmap_visualizer.rst +++ b/docs/source/examples/11_colmap_visualizer.rst @@ -43,13 +43,13 @@ Visualize COLMAP sparse reconstruction outputs. To get demo data, see ``./assets downsample_factor: Downsample factor for the images. """ server = viser.ViserServer() - server.configure_theme(titlebar_content=None, control_layout="collapsible") + server.gui.configure_theme(titlebar_content=None, control_layout="collapsible") # Load the colmap info. cameras = read_cameras_binary(colmap_path / "cameras.bin") images = read_images_binary(colmap_path / "images.bin") points3d = read_points3d_binary(colmap_path / "points3D.bin") - gui_reset_up = server.add_gui_button( + gui_reset_up = server.gui.add_button( "Reset up direction", hint="Set the camera control 'up' direction to the current camera's 'up'.", ) @@ -62,21 +62,21 @@ Visualize COLMAP sparse reconstruction outputs. To get demo data, see ``./assets [0.0, -1.0, 0.0] ) - gui_points = server.add_gui_slider( + gui_points = server.gui.add_slider( "Max points", min=1, max=len(points3d), step=1, initial_value=min(len(points3d), 50_000), ) - gui_frames = server.add_gui_slider( + gui_frames = server.gui.add_slider( "Max frames", min=1, max=len(images), step=1, initial_value=min(len(images), 100), ) - gui_point_size = server.add_gui_number("Point size", initial_value=0.05) + gui_point_size = server.gui.add_number("Point size", initial_value=0.05) def visualize_colmap() -> None: """Send all COLMAP elements to viser for visualization. This could be optimized @@ -90,7 +90,7 @@ Visualize COLMAP sparse reconstruction outputs. To get demo data, see ``./assets points = points[points_selection] colors = colors[points_selection] - server.add_point_cloud( + server.scene.add_point_cloud( name="/colmap/pcd", points=points, colors=colors, @@ -123,7 +123,7 @@ Visualize COLMAP sparse reconstruction outputs. To get demo data, see ``./assets T_world_camera = tf.SE3.from_rotation_and_translation( tf.SO3(img.qvec), img.tvec ).inverse() - frame = server.add_frame( + frame = server.scene.add_frame( f"/colmap/frame_{img_id}", wxyz=T_world_camera.rotation().wxyz, position=T_world_camera.translation(), @@ -139,7 +139,7 @@ Visualize COLMAP sparse reconstruction outputs. To get demo data, see ``./assets fy = cam.params[1] image = iio.imread(image_filename) image = image[::downsample_factor, ::downsample_factor] - frustum = server.add_camera_frustum( + frustum = server.scene.add_camera_frustum( f"/colmap/frame_{img_id}/frustum", fov=2 * onp.arctan2(H / 2, fy), aspect=W / H, @@ -169,7 +169,7 @@ Visualize COLMAP sparse reconstruction outputs. To get demo data, see ``./assets if need_update: need_update = False - server.reset_scene() + server.scene.reset() visualize_colmap() time.sleep(1e-3) diff --git a/docs/source/examples/12_click_meshes.rst b/docs/source/examples/12_click_meshes.rst index 062270f04..621625a27 100644 --- a/docs/source/examples/12_click_meshes.rst +++ b/docs/source/examples/12_click_meshes.rst @@ -23,14 +23,14 @@ Click on meshes to select them. The index of the last clicked mesh is displayed grid_shape = (4, 5) server = viser.ViserServer() - with server.add_gui_folder("Last clicked"): - x_value = server.add_gui_number( + with server.gui.add_folder("Last clicked"): + x_value = server.gui.add_number( label="x", initial_value=0, disabled=True, hint="x coordinate of the last clicked mesh", ) - y_value = server.add_gui_number( + y_value = server.gui.add_number( label="y", initial_value=0, disabled=True, @@ -56,14 +56,14 @@ Click on meshes to select them. The index of the last clicked mesh is displayed color = colormap(index)[:3] if counter in (0, 1): - handle = server.add_box( + handle = server.scene.add_box( name=f"/sphere_{i}_{j}", position=(i, j, 0.0), color=color, dimensions=(0.5, 0.5, 0.5), ) else: - handle = server.add_icosphere( + handle = server.scene.add_icosphere( name=f"/sphere_{i}_{j}", radius=0.4, color=color, diff --git a/docs/source/examples/13_theming.rst b/docs/source/examples/13_theming.rst index c25d1e1cf..38305ab90 100644 --- a/docs/source/examples/13_theming.rst +++ b/docs/source/examples/13_theming.rst @@ -47,28 +47,28 @@ Viser includes support for light theming. ) titlebar_theme = TitlebarConfig(buttons=buttons, image=image) - server.add_gui_markdown( + server.gui.add_markdown( "Viser includes support for light theming via the `.configure_theme()` method." ) - gui_theme_code = server.add_gui_markdown("no theme applied yet") + gui_theme_code = server.gui.add_markdown("no theme applied yet") # GUI elements for controllable values. - titlebar = server.add_gui_checkbox("Titlebar", initial_value=True) - dark_mode = server.add_gui_checkbox("Dark mode", initial_value=True) - show_logo = server.add_gui_checkbox("Show logo", initial_value=True) - show_share_button = server.add_gui_checkbox("Show share button", initial_value=True) - brand_color = server.add_gui_rgb("Brand color", (230, 180, 30)) - control_layout = server.add_gui_dropdown( + titlebar = server.gui.add_checkbox("Titlebar", initial_value=True) + dark_mode = server.gui.add_checkbox("Dark mode", initial_value=True) + show_logo = server.gui.add_checkbox("Show logo", initial_value=True) + show_share_button = server.gui.add_checkbox("Show share button", initial_value=True) + brand_color = server.gui.add_rgb("Brand color", (230, 180, 30)) + control_layout = server.gui.add_dropdown( "Control layout", ("floating", "fixed", "collapsible") ) - control_width = server.add_gui_dropdown( + control_width = server.gui.add_dropdown( "Control width", ("small", "medium", "large"), initial_value="medium" ) - synchronize = server.add_gui_button("Apply theme", icon=viser.Icon.CHECK) + synchronize = server.gui.add_button("Apply theme", icon=viser.Icon.CHECK) def synchronize_theme() -> None: - server.configure_theme( + server.gui.configure_theme( titlebar_content=titlebar_theme if titlebar.value else None, control_layout=control_layout.value, control_width=control_width.value, @@ -80,7 +80,7 @@ Viser includes support for light theming. gui_theme_code.content = f""" ### Current applied theme ``` - server.configure_theme( + server.gui.configure_theme( titlebar_content={"titlebar_content" if titlebar.value else None}, control_layout="{control_layout.value}", control_width="{control_width.value}", diff --git a/docs/source/examples/14_markdown.rst b/docs/source/examples/14_markdown.rst index 94f37a4b5..de5df24da 100644 --- a/docs/source/examples/14_markdown.rst +++ b/docs/source/examples/14_markdown.rst @@ -19,17 +19,17 @@ Viser GUI has MDX 2 support. import viser server = viser.ViserServer() - server.world_axes.visible = True + server.scene.world_axes.visible = True - markdown_counter = server.add_gui_markdown("Counter: 0") + markdown_counter = server.gui.add_markdown("Counter: 0") here = Path(__file__).absolute().parent - button = server.add_gui_button("Remove blurb") - checkbox = server.add_gui_checkbox("Visibility", initial_value=True) + button = server.gui.add_button("Remove blurb") + checkbox = server.gui.add_checkbox("Visibility", initial_value=True) markdown_source = (here / "./assets/mdx_example.mdx").read_text() - markdown_blurb = server.add_gui_markdown( + markdown_blurb = server.gui.add_markdown( content=markdown_source, image_root=here, ) diff --git a/docs/source/examples/15_gui_in_scene.rst b/docs/source/examples/15_gui_in_scene.rst index 84ef4c83f..cdb145f80 100644 --- a/docs/source/examples/15_gui_in_scene.rst +++ b/docs/source/examples/15_gui_in_scene.rst @@ -44,7 +44,7 @@ performed on them. position = rng.uniform(-3.0, 3.0, size=(3,)) # Create a coordinate frame and label. - frame = client.add_frame(f"/frame_{i}", wxyz=wxyz, position=position) + frame = client.scene.add_frame(f"/frame_{i}", wxyz=wxyz, position=position) # Move the camera when we click a frame. @frame.on_click @@ -55,11 +55,11 @@ performed on them. if displayed_3d_container is not None: displayed_3d_container.remove() - displayed_3d_container = client.add_3d_gui_container(f"/frame_{i}/gui") + displayed_3d_container = client.scene.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 GUI") + go_to = client.gui.add_button("Go to") + randomize_orientation = client.gui.add_button("Randomize orientation") + close = client.gui.add_button("Close GUI") @go_to.on_click def _(_) -> None: diff --git a/docs/source/examples/16_modal.rst b/docs/source/examples/16_modal.rst index 2072ac692..08b6013b3 100644 --- a/docs/source/examples/16_modal.rst +++ b/docs/source/examples/16_modal.rst @@ -23,23 +23,23 @@ Examples of using modals in Viser. @server.on_client_connect def _(client: viser.ClientHandle) -> None: - with client.add_gui_modal("Modal example"): - client.add_gui_markdown( + with client.gui.add_modal("Modal example"): + client.gui.add_markdown( "**The input below determines the title of the modal...**" ) - gui_title = client.add_gui_text( + gui_title = client.gui.add_text( "Title", initial_value="My Modal", ) - modal_button = client.add_gui_button("Show more modals") + modal_button = client.gui.add_button("Show more modals") @modal_button.on_click def _(_) -> None: - with client.add_gui_modal(gui_title.value) as modal: - client.add_gui_markdown("This is content inside the modal!") - client.add_gui_button("Close").on_click(lambda _: modal.close()) + with client.gui.add_modal(gui_title.value) as modal: + client.gui.add_markdown("This is content inside the modal!") + client.gui.add_button("Close").on_click(lambda _: modal.close()) while True: time.sleep(0.15) diff --git a/docs/source/examples/17_background_composite.rst b/docs/source/examples/17_background_composite.rst index 8af08a2a7..dec747943 100644 --- a/docs/source/examples/17_background_composite.rst +++ b/docs/source/examples/17_background_composite.rst @@ -32,12 +32,12 @@ be useful when we want a 2D image to occlude 3D geometry, such as for NeRF rende img[250:750, 250:750, :] = 255 mesh = trimesh.creation.box((0.5, 0.5, 0.5)) - server.add_mesh_trimesh( + server.scene.add_mesh_trimesh( name="/cube", mesh=mesh, position=(0, 0, 0.0), ) - server.set_background_image(img, depth=depth) + server.scene.set_background_image(img, depth=depth) while True: diff --git a/docs/source/examples/18_splines.rst b/docs/source/examples/18_splines.rst index 1ff845edd..74d93b34a 100644 --- a/docs/source/examples/18_splines.rst +++ b/docs/source/examples/18_splines.rst @@ -23,7 +23,7 @@ Make a ball with some random splines. server = viser.ViserServer() for i in range(10): positions = onp.random.normal(size=(30, 3)) * 3.0 - server.add_spline_catmull_rom( + server.scene.add_spline_catmull_rom( f"/catmull_{i}", positions, tension=0.5, @@ -33,7 +33,7 @@ Make a ball with some random splines. ) control_points = onp.random.normal(size=(30 * 2 - 2, 3)) * 3.0 - server.add_spline_cubic_bezier( + server.scene.add_spline_cubic_bezier( f"/cubic_bezier_{i}", positions, control_points, diff --git a/docs/source/examples/19_get_renders.rst b/docs/source/examples/19_get_renders.rst index 5683acf4c..a2be93291 100644 --- a/docs/source/examples/19_get_renders.rst +++ b/docs/source/examples/19_get_renders.rst @@ -23,20 +23,20 @@ Example for getting renders from a client's viewport to the Python API. def main(): server = viser.ViserServer() - button = server.add_gui_button("Render a GIF") + button = server.gui.add_button("Render a GIF") @button.on_click def _(event: viser.GuiEvent) -> None: client = event.client assert client is not None - client.reset_scene() + client.scene.reset() images = [] for i in range(20): positions = onp.random.normal(size=(30, 3)) * 3.0 - client.add_spline_catmull_rom( + client.scene.add_spline_catmull_rom( f"/catmull_{i}", positions, tension=0.5, diff --git a/docs/source/examples/20_scene_pointer.rst b/docs/source/examples/20_scene_pointer.rst index d7fd9ed57..284bb4f6e 100644 --- a/docs/source/examples/20_scene_pointer.rst +++ b/docs/source/examples/20_scene_pointer.rst @@ -18,7 +18,7 @@ To get the demo data, see ``./assets/download_dragon_mesh.sh``. import time from pathlib import Path - from typing import List, cast + from typing import cast import numpy as onp import trimesh @@ -28,21 +28,21 @@ To get the demo data, see ``./assets/download_dragon_mesh.sh``. import viser.transforms as tf server = viser.ViserServer() - server.configure_theme(brand_color=(130, 0, 150)) - server.set_up_direction("+y") + server.gui.configure_theme(brand_color=(130, 0, 150)) + server.scene.set_up_direction("+y") mesh = cast( trimesh.Trimesh, trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj")) ) mesh.apply_scale(0.05) - mesh_handle = server.add_mesh_trimesh( + mesh_handle = server.scene.add_mesh_trimesh( name="/mesh", mesh=mesh, position=(0.0, 0.0, 0.0), ) - hit_pos_handles: List[viser.GlbHandle] = [] + hit_pos_handles: list[viser.GlbHandle] = [] # Buttons + callbacks will operate on a per-client basis, but will modify the global scene! :) @@ -53,13 +53,13 @@ To get the demo data, see ``./assets/download_dragon_mesh.sh``. client.camera.wxyz = onp.array([0.0, 0.0, 0.0, 1.0]) # Tests "click" scenepointerevent. - click_button_handle = client.add_gui_button("Add sphere", icon=viser.Icon.POINTER) + click_button_handle = client.gui.add_button("Add sphere", icon=viser.Icon.POINTER) @click_button_handle.on_click def _(_): click_button_handle.disabled = True - @client.on_scene_pointer(event_type="click") + @client.scene.on_pointer_event(event_type="click") def _(event: viser.ScenePointerEvent) -> None: # Check for intersection with the mesh, using trimesh's ray-mesh intersection. # Note that mesh is in the mesh frame, so we need to transform the ray. @@ -72,7 +72,7 @@ To get the demo data, see ``./assets/download_dragon_mesh.sh``. if len(hit_pos) == 0: return - client.remove_scene_pointer_callback() + client.scene.remove_pointer_callback() # Get the first hit position (based on distance from the ray origin). hit_pos = min(hit_pos, key=lambda x: onp.linalg.norm(x - origin)) @@ -81,25 +81,25 @@ To get the demo data, see ``./assets/download_dragon_mesh.sh``. hit_pos_mesh = trimesh.creation.icosphere(radius=0.1) hit_pos_mesh.vertices += R_world_mesh @ hit_pos hit_pos_mesh.visual.vertex_colors = (0.5, 0.0, 0.7, 1.0) # type: ignore - hit_pos_handle = server.add_mesh_trimesh( + hit_pos_handle = server.scene.add_mesh_trimesh( name=f"/hit_pos_{len(hit_pos_handles)}", mesh=hit_pos_mesh ) hit_pos_handles.append(hit_pos_handle) - @client.on_scene_pointer_removed + @client.scene.on_pointer_callback_removed def _(): click_button_handle.disabled = False # Tests "rect-select" scenepointerevent. - paint_button_handle = client.add_gui_button("Paint mesh", icon=viser.Icon.PAINT) + paint_button_handle = client.gui.add_button("Paint mesh", icon=viser.Icon.PAINT) @paint_button_handle.on_click def _(_): paint_button_handle.disabled = True - @client.on_scene_pointer(event_type="rect-select") + @client.scene.on_pointer_event(event_type="rect-select") def _(message: viser.ScenePointerEvent) -> None: - client.remove_scene_pointer_callback() + client.scene.remove_pointer_callback() global mesh_handle camera = message.client.camera @@ -137,18 +137,18 @@ To get the demo data, see ``./assets/download_dragon_mesh.sh``. mesh.visual.vertex_colors = onp.where( # type: ignore mask, (0.5, 0.0, 0.7, 1.0), (0.9, 0.9, 0.9, 1.0) ) - mesh_handle = server.add_mesh_trimesh( + mesh_handle = server.scene.add_mesh_trimesh( name="/mesh", mesh=mesh, position=(0.0, 0.0, 0.0), ) - @client.on_scene_pointer_removed + @client.scene.on_pointer_callback_removed def _(): paint_button_handle.disabled = False # Button to clear spheres. - clear_button_handle = client.add_gui_button("Clear scene", icon=viser.Icon.X) + clear_button_handle = client.gui.add_button("Clear scene", icon=viser.Icon.X) @clear_button_handle.on_click def _(_): @@ -158,7 +158,7 @@ To get the demo data, see ``./assets/download_dragon_mesh.sh``. handle.remove() hit_pos_handles.clear() mesh.visual.vertex_colors = (0.9, 0.9, 0.9, 1.0) # type: ignore - mesh_handle = server.add_mesh_trimesh( + mesh_handle = server.scene.add_mesh_trimesh( name="/mesh", mesh=mesh, position=(0.0, 0.0, 0.0), diff --git a/docs/source/examples/21_set_up_direction.rst b/docs/source/examples/21_set_up_direction.rst index 70ca29bfe..5669f90fd 100644 --- a/docs/source/examples/21_set_up_direction.rst +++ b/docs/source/examples/21_set_up_direction.rst @@ -20,8 +20,8 @@ Set Up Direction def main() -> None: server = viser.ViserServer() - server.world_axes.visible = True - gui_up = server.add_gui_vector3( + server.scene.world_axes.visible = True + gui_up = server.gui.add_vector3( "Up Direction", initial_value=(0.0, 0.0, 1.0), step=0.01, @@ -29,7 +29,7 @@ Set Up Direction @gui_up.on_update def _(_) -> None: - server.set_up_direction(gui_up.value) + server.scene.set_up_direction(gui_up.value) while True: time.sleep(1.0) diff --git a/docs/source/examples/22_games.rst b/docs/source/examples/22_games.rst index 455224ce5..4c630179a 100644 --- a/docs/source/examples/22_games.rst +++ b/docs/source/examples/22_games.rst @@ -25,11 +25,11 @@ Some two-player games implemented using scene click events. def main() -> None: server = viser.ViserServer() - server.configure_theme(dark_mode=True) + server.gui.configure_theme(dark_mode=True) play_connect_4(server) - server.add_gui_button("Tic-Tac-Toe").on_click(lambda _: play_tic_tac_toe(server)) - server.add_gui_button("Connect 4").on_click(lambda _: play_connect_4(server)) + server.gui.add_button("Tic-Tac-Toe").on_click(lambda _: play_tic_tac_toe(server)) + server.gui.add_button("Connect 4").on_click(lambda _: play_connect_4(server)) while True: time.sleep(10.0) @@ -37,7 +37,7 @@ Some two-player games implemented using scene click events. def play_connect_4(server: viser.ViserServer) -> None: """Play a game of Connect 4.""" - server.reset_scene() + server.scene.reset() num_rows = 6 num_cols = 7 @@ -48,7 +48,7 @@ Some two-player games implemented using scene click events. # Create the board frame. for col in range(num_cols): for row in range(num_rows): - server.add_mesh_trimesh( + server.scene.add_mesh_trimesh( f"/structure/{row}_{col}", trimesh.creation.annulus(0.45, 0.55, 0.125), position=(0.0, col, row), @@ -57,7 +57,7 @@ Some two-player games implemented using scene click events. # Create a sphere to click on for each column. def setup_column(col: int) -> None: - sphere = server.add_icosphere( + sphere = server.scene.add_icosphere( f"/spheres/{col}", radius=0.25, position=(0, col, num_rows - 0.25), @@ -76,7 +76,7 @@ Some two-player games implemented using scene click events. pieces_in_col[col] += 1 cylinder = trimesh.creation.cylinder(radius=0.4, height=0.125) - piece = server.add_mesh_simple( + piece = server.scene.add_mesh_simple( f"/game_pieces/{row}_{col}", cylinder.vertices, cylinder.faces, @@ -97,12 +97,12 @@ Some two-player games implemented using scene click events. def play_tic_tac_toe(server: viser.ViserServer) -> None: """Play a game of tic-tac-toe.""" - server.reset_scene() + server.scene.reset() whose_turn: Literal["x", "o"] = "x" for i in range(4): - server.add_spline_catmull_rom( + server.scene.add_spline_catmull_rom( f"/gridlines/{i}", ((-0.5, -1.5, 0), (-0.5, 1.5, 0)), color=(127, 127, 127), @@ -115,7 +115,7 @@ Some two-player games implemented using scene click events. for scale in onp.linspace(0.01, 1.0, 5): if symbol == "x": for k in range(2): - server.add_box( + server.scene.add_box( f"/symbols/{i}_{j}/{k}", dimensions=(0.7 * scale, 0.125 * scale, 0.125), position=(i, j, 0), @@ -126,7 +126,7 @@ Some two-player games implemented using scene click events. ) elif symbol == "o": mesh = trimesh.creation.annulus(0.25 * scale, 0.35 * scale, 0.125) - server.add_mesh_simple( + server.scene.add_mesh_simple( f"/symbols/{i}_{j}", mesh.vertices, mesh.faces, @@ -140,7 +140,7 @@ Some two-player games implemented using scene click events. def setup_cell(i: int, j: int) -> None: """Create a clickable sphere in a given cell.""" - sphere = server.add_icosphere( + sphere = server.scene.add_icosphere( f"/spheres/{i}_{j}", radius=0.25, position=(i, j, 0), diff --git a/docs/source/examples/23_plotly.rst b/docs/source/examples/23_plotly.rst index dcc3f1062..f5a596d25 100644 --- a/docs/source/examples/23_plotly.rst +++ b/docs/source/examples/23_plotly.rst @@ -48,14 +48,14 @@ Examples of visualizing plotly plots in Viser. # Plot type 1: Line plot. line_plot_time = 0.0 - line_plot = server.add_gui_plotly(figure=create_sinusoidal_wave(line_plot_time)) + line_plot = server.gui.add_plotly(figure=create_sinusoidal_wave(line_plot_time)) # Plot type 2: Image plot. fig = px.imshow(Image.open("assets/Cal_logo.png")) fig.update_layout( margin=dict(l=20, r=20, t=20, b=20), ) - server.add_gui_plotly(figure=fig, aspect=1.0) + server.gui.add_plotly(figure=fig, aspect=1.0) # Plot type 3: 3D Scatter plot. fig = px.scatter_3d( @@ -69,7 +69,7 @@ Examples of visualizing plotly plots in Viser. fig.update_layout( margin=dict(l=20, r=20, t=20, b=20), ) - server.add_gui_plotly(figure=fig, aspect=1.0) + server.gui.add_plotly(figure=fig, aspect=1.0) while True: # Update the line plot. diff --git a/examples/07_record3d_visualizer.py b/examples/07_record3d_visualizer.py index 10332262f..2d869257f 100644 --- a/examples/07_record3d_visualizer.py +++ b/examples/07_record3d_visualizer.py @@ -5,7 +5,6 @@ import time from pathlib import Path -from typing import List import numpy as onp import tyro @@ -90,7 +89,7 @@ def _(_) -> None: position=(0, 0, 0), show_axes=False, ) - frame_nodes: List[viser.FrameHandle] = [] + frame_nodes: list[viser.FrameHandle] = [] for i in tqdm(range(num_frames)): frame = loader.get_frame(i) position, color = frame.get_point_cloud(downsample_factor) diff --git a/examples/08_smpl_visualizer.py b/examples/08_smpl_visualizer.py index 46a5e9ec8..165bb1e50 100644 --- a/examples/08_smpl_visualizer.py +++ b/examples/08_smpl_visualizer.py @@ -14,7 +14,6 @@ import time from dataclasses import dataclass from pathlib import Path -from typing import List, Tuple import numpy as np import numpy as onp @@ -123,11 +122,11 @@ def main(model_path: Path) -> None: class GuiElements: """Structure containing handles for reading from GUI elements.""" - gui_rgb: viser.GuiInputHandle[Tuple[int, int, int]] + gui_rgb: viser.GuiInputHandle[tuple[int, int, int]] gui_wireframe: viser.GuiInputHandle[bool] - gui_betas: List[viser.GuiInputHandle[float]] - gui_joints: List[viser.GuiInputHandle[Tuple[float, float, float]]] - transform_controls: List[viser.TransformControlsHandle] + gui_betas: list[viser.GuiInputHandle[float]] + gui_joints: list[viser.GuiInputHandle[tuple[float, float, float]]] + transform_controls: list[viser.TransformControlsHandle] changed: bool """This flag will be flipped to True whenever the mesh needs to be re-generated.""" @@ -202,7 +201,7 @@ def _(_): quat /= onp.linalg.norm(quat) joint.value = tf.SO3(wxyz=quat).log() - gui_joints: List[viser.GuiInputHandle[Tuple[float, float, float]]] = [] + gui_joints: list[viser.GuiInputHandle[tuple[float, float, float]]] = [] for i in range(num_joints): gui_joint = server.gui.add_vector3( label=f"Joint {i}", @@ -222,7 +221,7 @@ def _(_): set_callback_in_closure(i) # Transform control gizmos on joints. - transform_controls: List[viser.TransformControlsHandle] = [] + transform_controls: list[viser.TransformControlsHandle] = [] prefixed_joint_names = [] # Joint names, but prefixed with parents. for i in range(num_joints): prefixed_joint_name = f"joint_{i}" diff --git a/examples/09_urdf_visualizer.py b/examples/09_urdf_visualizer.py index 91fd0f8eb..664f1c650 100644 --- a/examples/09_urdf_visualizer.py +++ b/examples/09_urdf_visualizer.py @@ -11,7 +11,6 @@ import time from pathlib import Path -from typing import List import numpy as onp import tyro @@ -27,8 +26,8 @@ def main(urdf_path: Path) -> None: urdf = ViserUrdf(server, urdf_path) # Create joint angle sliders. - gui_joints: List[viser.GuiInputHandle[float]] = [] - initial_angles: List[float] = [] + gui_joints: list[viser.GuiInputHandle[float]] = [] + initial_angles: list[float] = [] for joint_name, (lower, upper) in urdf.get_actuated_joint_limits().items(): lower = lower if lower is not None else -onp.pi upper = upper if upper is not None else onp.pi diff --git a/examples/10_realsense.py b/examples/10_realsense.py index b19b38129..a7c426dd4 100644 --- a/examples/10_realsense.py +++ b/examples/10_realsense.py @@ -5,7 +5,6 @@ """ import contextlib -from typing import Tuple import numpy as np import numpy.typing as npt @@ -39,7 +38,7 @@ def realsense_pipeline(fps: int = 30): def point_cloud_arrays_from_frames( depth_frame, color_frame -) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.uint8]]: +) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.uint8]]: """Maps realsense frames to two arrays. Returns: diff --git a/examples/20_scene_pointer.py b/examples/20_scene_pointer.py index e828f5e68..a5d9296ed 100644 --- a/examples/20_scene_pointer.py +++ b/examples/20_scene_pointer.py @@ -8,7 +8,7 @@ import time from pathlib import Path -from typing import List, cast +from typing import cast import numpy as onp import trimesh @@ -32,7 +32,7 @@ position=(0.0, 0.0, 0.0), ) -hit_pos_handles: List[viser.GlbHandle] = [] +hit_pos_handles: list[viser.GlbHandle] = [] # Buttons + callbacks will operate on a per-client basis, but will modify the global scene! :) diff --git a/src/viser/_gui_api.py b/src/viser/_gui_api.py index cd5e68dc0..903308cc3 100644 --- a/src/viser/_gui_api.py +++ b/src/viser/_gui_api.py @@ -9,22 +9,9 @@ import functools import threading import time -import warnings from concurrent.futures import ThreadPoolExecutor from pathlib import Path -from typing import ( - TYPE_CHECKING, - Any, - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, - overload, -) +from typing import TYPE_CHECKING, Any, Sequence, TypeVar, Union, overload import numpy as onp from typing_extensions import ( @@ -76,7 +63,7 @@ T = TypeVar("T") -def _compute_step(x: Optional[float]) -> float: # type: ignore +def _compute_step(x: float | None) -> float: # type: ignore """For number inputs: compute an increment size from some number. Example inputs/outputs: @@ -107,13 +94,13 @@ def _compute_precision_digits(x: float) -> int: @dataclasses.dataclass class _RootGuiContainer: - _children: Dict[str, SupportsRemoveProtocol] + _children: dict[str, SupportsRemoveProtocol] _global_order_counter = 0 -def _apply_default_order(order: Optional[float]) -> float: +def _apply_default_order(order: float | None) -> float: """Apply default ordering logic for GUI elements. If `order` is set to a float, this function is a no-op and returns it back. @@ -128,7 +115,7 @@ def _apply_default_order(order: Optional[float]) -> float: @functools.lru_cache(maxsize=None) -def get_type_hints_cached(cls: Type[Any]) -> Dict[str, Any]: +def get_type_hints_cached(cls: type[Any]) -> dict[str, Any]: return get_type_hints(cls) # type: ignore @@ -175,18 +162,18 @@ def cast_value(tp, value): raise TypeError(f"Cannot cast value {value} to type {tp}") -class FileUploadState(TypedDict): +class _FileUploadState(TypedDict): filename: str mime_type: str part_count: int - parts: Dict[int, bytes] + parts: dict[int, bytes] total_bytes: int transferred_bytes: int lock: threading.Lock class GuiApi: - _target_container_from_thread_id: Dict[int, str] = {} + _target_container_from_thread_id: dict[int, str] = {} """ID of container to put GUI elements into.""" def __init__( @@ -207,11 +194,11 @@ def __init__( ) """Interface for sending and listening to messages.""" - self._gui_handle_from_id: Dict[str, _GuiInputHandle[Any]] = {} - self._container_handle_from_id: Dict[str, GuiContainerProtocol] = { + self._gui_handle_from_id: dict[str, _GuiInputHandle[Any]] = {} + self._container_handle_from_id: dict[str, GuiContainerProtocol] = { "root": _RootGuiContainer({}) } - self._current_file_upload_states: Dict[str, FileUploadState] = {} + self._current_file_upload_states: dict[str, _FileUploadState] = {} # Set to True when plotly.min.js has been sent to client. self._setup_plotly_js: bool = False @@ -361,17 +348,7 @@ def _set_container_id(self, container_id: str) -> None: """Set container ID associated with the current thread.""" self._target_container_from_thread_id[threading.get_ident()] = container_id - if not TYPE_CHECKING: - - def gui_folder(self, label: str) -> GuiFolderHandle: - """Deprecated.""" - warnings.warn( - "gui_folder() is deprecated. Use add_folder() instead!", - stacklevel=2, - ) - return self.add_folder(label) - - def set_gui_panel_label(self, label: Optional[str]) -> None: + def set_panel_label(self, label: str | None) -> None: """Set the main label that appears in the GUI panel. Args: @@ -382,13 +359,13 @@ def set_gui_panel_label(self, label: Optional[str]) -> None: def configure_theme( self, *, - titlebar_content: Optional[theme.TitlebarConfig] = None, + titlebar_content: theme.TitlebarConfig | None = None, control_layout: Literal["floating", "collapsible", "fixed"] = "floating", control_width: Literal["small", "medium", "large"] = "medium", dark_mode: bool = False, show_logo: bool = True, show_share_button: bool = True, - brand_color: Optional[Tuple[int, int, int]] = None, + brand_color: tuple[int, int, int] | None = None, ) -> None: """Configures the visual appearance of the viser front-end. @@ -404,9 +381,9 @@ def configure_theme( brand_color: An optional tuple of integers (RGB) representing the brand color. """ - colors_cast: Optional[ - Tuple[str, str, str, str, str, str, str, str, str, str] - ] = None + colors_cast: tuple[ + str, str, str, str, str, str, str, str, str, str + ] | None = None if brand_color is not None: assert len(brand_color) in (3, 10) @@ -456,7 +433,7 @@ def configure_theme( def add_folder( self, label: str, - order: Optional[float] = None, + order: float | None = None, expand_by_default: bool = True, visible: bool = True, ) -> GuiFolderHandle: @@ -494,7 +471,7 @@ def add_folder( def add_modal( self, title: str, - order: Optional[float] = None, + order: float | None = None, ) -> GuiModalHandle: """Show a modal window, which can be useful for popups and messages, then return a handle that can be used to populate it. @@ -522,7 +499,7 @@ def add_modal( def add_tab_group( self, - order: Optional[float] = None, + order: float | None = None, visible: bool = True, ) -> GuiTabGroupHandle: """Add a tab group. @@ -560,8 +537,8 @@ def add_tab_group( def add_markdown( self, content: str, - image_root: Optional[Path] = None, - order: Optional[float] = None, + image_root: Path | None = None, + order: float | None = None, visible: bool = True, ) -> GuiMarkdownHandle: """Add markdown to the GUI. @@ -603,7 +580,7 @@ def add_plotly( self, figure: go.Figure, aspect: float = 1.0, - order: Optional[float] = None, + order: float | None = None, visible: bool = True, ) -> GuiPlotlyHandle: """Add a Plotly figure to the GUI. Requires the `plotly` package to be @@ -680,27 +657,26 @@ def add_button( label: str, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - color: Optional[ - Literal[ - "dark", - "gray", - "red", - "pink", - "grape", - "violet", - "indigo", - "blue", - "cyan", - "green", - "lime", - "yellow", - "orange", - "teal", - ] - ] = None, - icon: Optional[IconName] = None, - order: Optional[float] = None, + hint: str | None = None, + color: Literal[ + "dark", + "gray", + "red", + "pink", + "grape", + "violet", + "indigo", + "blue", + "cyan", + "green", + "lime", + "yellow", + "orange", + "teal", + ] + | None = None, + icon: IconName | None = None, + order: float | None = None, ) -> GuiButtonHandle: """Add a button to the GUI. The value of this input is set to `True` every time it is clicked; to detect clicks, we can manually set it back to `False`. @@ -745,28 +721,27 @@ def add_upload_button( label: str, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - color: Optional[ - Literal[ - "dark", - "gray", - "red", - "pink", - "grape", - "violet", - "indigo", - "blue", - "cyan", - "green", - "lime", - "yellow", - "orange", - "teal", - ] - ] = None, - icon: Optional[IconName] = None, + hint: str | None = None, + color: Literal[ + "dark", + "gray", + "red", + "pink", + "grape", + "violet", + "indigo", + "blue", + "cyan", + "green", + "lime", + "yellow", + "orange", + "teal", + ] + | None = None, + icon: IconName | None = None, mime_type: str = "*/*", - order: Optional[float] = None, + order: float | None = None, ) -> GuiUploadButtonHandle: """Add a button to the GUI. The value of this input is set to `True` every time it is clicked; to detect clicks, we can manually set it back to `False`. @@ -820,8 +795,8 @@ def add_button_group( options: Sequence[TLiteralString], visible: bool = True, disabled: bool = False, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiButtonGroupHandle[TLiteralString]: ... @@ -832,8 +807,8 @@ def add_button_group( options: Sequence[TString], visible: bool = True, disabled: bool = False, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiButtonGroupHandle[TString]: ... @@ -843,8 +818,8 @@ def add_button_group( options: Sequence[TLiteralString] | Sequence[TString], visible: bool = True, disabled: bool = False, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiButtonGroupHandle[Any]: # Return types are specified in overloads. """Add a button group to the GUI. @@ -885,8 +860,8 @@ def add_checkbox( initial_value: bool, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiInputHandle[bool]: """Add a checkbox to the GUI. @@ -925,8 +900,8 @@ def add_text( initial_value: str, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiInputHandle[str]: """Add a text input to the GUI. @@ -963,13 +938,13 @@ def add_number( self, label: str, initial_value: IntOrFloat, - min: Optional[IntOrFloat] = None, - max: Optional[IntOrFloat] = None, - step: Optional[IntOrFloat] = None, + min: IntOrFloat | None = None, + max: IntOrFloat | None = None, + step: IntOrFloat | None = None, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiInputHandle[IntOrFloat]: """Add a number input to the GUI, with user-specifiable bound and precision parameters. @@ -1031,15 +1006,15 @@ def add_number( def add_vector2( self, label: str, - initial_value: Tuple[float, float] | onp.ndarray, - min: Tuple[float, float] | onp.ndarray | None = None, - max: Tuple[float, float] | onp.ndarray | None = None, - step: Optional[float] = None, + initial_value: tuple[float, float] | onp.ndarray, + min: tuple[float, float] | onp.ndarray | None = None, + max: tuple[float, float] | onp.ndarray | None = None, + step: float | None = None, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, - ) -> GuiInputHandle[Tuple[float, float]]: + hint: str | None = None, + order: float | None = None, + ) -> GuiInputHandle[tuple[float, float]]: """Add a length-2 vector input to the GUI. Args: @@ -1064,7 +1039,7 @@ def add_vector2( order = _apply_default_order(order) if step is None: - possible_steps: List[float] = [] + possible_steps: list[float] = [] possible_steps.extend([_compute_step(x) for x in value]) if min is not None: possible_steps.extend([_compute_step(x) for x in min]) @@ -1093,15 +1068,15 @@ def add_vector2( def add_vector3( self, label: str, - initial_value: Tuple[float, float, float] | onp.ndarray, - min: Tuple[float, float, float] | onp.ndarray | None = None, - max: Tuple[float, float, float] | onp.ndarray | None = None, - step: Optional[float] = None, + initial_value: tuple[float, float, float] | onp.ndarray, + min: tuple[float, float, float] | onp.ndarray | None = None, + max: tuple[float, float, float] | onp.ndarray | None = None, + step: float | None = None, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, - ) -> GuiInputHandle[Tuple[float, float, float]]: + hint: str | None = None, + order: float | None = None, + ) -> GuiInputHandle[tuple[float, float, float]]: """Add a length-3 vector input to the GUI. Args: @@ -1126,7 +1101,7 @@ def add_vector3( order = _apply_default_order(order) if step is None: - possible_steps: List[float] = [] + possible_steps: list[float] = [] possible_steps.extend([_compute_step(x) for x in value]) if min is not None: possible_steps.extend([_compute_step(x) for x in min]) @@ -1158,11 +1133,11 @@ def add_dropdown( self, label: str, options: Sequence[TLiteralString], - initial_value: Optional[TLiteralString] = None, + initial_value: TLiteralString | None = None, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiDropdownHandle[TLiteralString]: ... @@ -1171,11 +1146,11 @@ def add_dropdown( self, label: str, options: Sequence[TString], - initial_value: Optional[TString] = None, + initial_value: TString | None = None, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiDropdownHandle[TString]: ... @@ -1183,11 +1158,11 @@ def add_dropdown( self, label: str, options: Sequence[TLiteralString] | Sequence[TString], - initial_value: Optional[TLiteralString | TString] = None, + initial_value: TLiteralString | TString | None = None, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiDropdownHandle[Any]: # Output type is specified in overloads. """Add a dropdown to the GUI. @@ -1233,11 +1208,11 @@ def add_slider( max: IntOrFloat, step: IntOrFloat, initial_value: IntOrFloat, - marks: Optional[Tuple[IntOrFloat | Tuple[IntOrFloat, str], ...]] = None, + marks: tuple[IntOrFloat | tuple[IntOrFloat, str], ...] | None = None, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, + hint: str | None = None, + order: float | None = None, ) -> GuiInputHandle[IntOrFloat]: """Add a slider to the GUI. Types of the min, max, step, and initial value should match. @@ -1247,7 +1222,7 @@ def add_slider( max: Maximum value of the slider. step: Step size of the slider. initial_value: Initial value of the slider. - marks: Tuple of marks to display below the slider. Each mark should + marks: tuple of marks to display below the slider. Each mark should either be a numerical or a (number, label) tuple, where the label is provided as a string. disabled: Whether the slider is disabled. @@ -1313,15 +1288,15 @@ def add_multi_slider( min: IntOrFloat, max: IntOrFloat, step: IntOrFloat, - initial_value: Tuple[IntOrFloat, ...], - min_range: Optional[IntOrFloat] = None, + initial_value: tuple[IntOrFloat, ...], + min_range: IntOrFloat | None = None, fixed_endpoints: bool = False, - marks: Optional[Tuple[IntOrFloat | Tuple[IntOrFloat, str], ...]] = None, + marks: tuple[IntOrFloat | tuple[IntOrFloat, str], ...] | None = None, disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, - ) -> GuiInputHandle[Tuple[IntOrFloat, ...]]: + hint: str | None = None, + order: float | None = None, + ) -> GuiInputHandle[tuple[IntOrFloat, ...]]: """Add a multi slider to the GUI. Types of the min, max, step, and initial value should match. Args: @@ -1332,7 +1307,7 @@ def add_multi_slider( initial_value: Initial values of the slider. min_range: Optional minimum difference between two values of the slider. fixed_endpoints: Whether the endpoints of the slider are fixed. - marks: Tuple of marks to display below the slider. Each mark should + marks: tuple of marks to display below the slider. Each mark should either be a numerical or a (number, label) tuple, where the label is provided as a string. disabled: Whether the slider is disabled. @@ -1394,12 +1369,12 @@ def add_multi_slider( def add_rgb( self, label: str, - initial_value: Tuple[int, int, int], + initial_value: tuple[int, int, int], disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, - ) -> GuiInputHandle[Tuple[int, int, int]]: + hint: str | None = None, + order: float | None = None, + ) -> GuiInputHandle[tuple[int, int, int]]: """Add an RGB picker to the GUI. Args: @@ -1434,12 +1409,12 @@ def add_rgb( def add_rgba( self, label: str, - initial_value: Tuple[int, int, int, int], + initial_value: tuple[int, int, int, int], disabled: bool = False, visible: bool = True, - hint: Optional[str] = None, - order: Optional[float] = None, - ) -> GuiInputHandle[Tuple[int, int, int, int]]: + hint: str | None = None, + order: float | None = None, + ) -> GuiInputHandle[tuple[int, int, int, int]]: """Add an RGBA picker to the GUI. Args: @@ -1505,7 +1480,7 @@ def _create_gui_input( if not is_button: def sync_other_clients( - client_id: ClientId, updates: Dict[str, Any] + client_id: ClientId, updates: dict[str, Any] ) -> None: message = _messages.GuiUpdateMessage(handle_state.id, updates) message.excluded_self_client = client_id diff --git a/src/viser/_gui_handles.py b/src/viser/_gui_handles.py index 4dd63f229..48bf0387d 100644 --- a/src/viser/_gui_handles.py +++ b/src/viser/_gui_handles.py @@ -7,19 +7,7 @@ import uuid import warnings from pathlib import Path -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Generic, - Iterable, - List, - Optional, - Tuple, - Type, - TypeVar, -) +from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, TypeVar import imageio.v3 as iio import numpy as onp @@ -48,7 +36,7 @@ def _make_unique_id() -> str: class GuiContainerProtocol(Protocol): - _children: Dict[str, SupportsRemoveProtocol] = dataclasses.field( + _children: dict[str, SupportsRemoveProtocol] = dataclasses.field( default_factory=dict ) @@ -63,7 +51,7 @@ class _GuiHandleState(Generic[T]): """Internal API for GUI elements.""" label: str - typ: Type[T] + typ: type[T] gui_api: GuiApi value: T update_timestamp: float @@ -71,13 +59,13 @@ class _GuiHandleState(Generic[T]): container_id: str """Container that this GUI input was placed into.""" - update_cb: List[Callable[[GuiEvent], None]] + update_cb: list[Callable[[GuiEvent], None]] """Registered functions to call when this input is updated.""" is_button: bool """Indicates a button element, which requires special handling.""" - sync_cb: Optional[Callable[[ClientId, Dict[str, Any]], None]] + sync_cb: Callable[[ClientId, dict[str, Any]], None] | None """Callback for synchronizing inputs across clients.""" disabled: bool @@ -85,9 +73,9 @@ class _GuiHandleState(Generic[T]): order: float id: str - hint: Optional[str] + hint: str | None - message_type: Type[Message] + message_type: type[Message] @dataclasses.dataclass @@ -234,9 +222,9 @@ class GuiEvent(Generic[TGuiHandle]): Passed as input to callback functions.""" - client: Optional[ClientHandle] + client: ClientHandle | None """Client that triggered this event.""" - client_id: Optional[int] + client_id: int | None """ID of client that triggered this event.""" target: TGuiHandle """GUI element that was affected.""" @@ -311,10 +299,10 @@ class GuiDropdownHandle(GuiInputHandle[StringType], Generic[StringType]): Lets us get values, set values, and detect updates.""" - _impl_options: Tuple[StringType, ...] + _impl_options: tuple[StringType, ...] @property - def options(self) -> Tuple[StringType, ...]: + def options(self) -> tuple[StringType, ...]: """Options for our dropdown. Synchronized automatically when assigned. For projects that care about typing: the static type of `options` should be @@ -349,9 +337,9 @@ def options(self, options: Iterable[StringType]) -> None: @dataclasses.dataclass(frozen=True) class GuiTabGroupHandle: _tab_group_id: str - _labels: List[str] - _icons_html: List[Optional[str]] - _tabs: List[GuiTabHandle] + _labels: list[str] + _icons_html: list[str | None] + _tabs: list[GuiTabHandle] _gui_api: GuiApi _order: float @@ -360,7 +348,7 @@ def order(self) -> float: """Read-only order value, which dictates the position of the GUI element.""" return self._order - def add_tab(self, label: str, icon: Optional[IconName] = None) -> GuiTabHandle: + def add_tab(self, label: str, icon: IconName | None = None) -> GuiTabHandle: """Add a tab. Returns a handle we can use to add GUI elements to it.""" id = _make_unique_id() @@ -405,8 +393,8 @@ class GuiFolderHandle: _id: str # Used as container ID for children. _order: float _parent_container_id: str # Container ID of parent. - _container_id_restore: Optional[str] = None - _children: Dict[str, SupportsRemoveProtocol] = dataclasses.field( + _container_id_restore: str | None = None + _children: dict[str, SupportsRemoveProtocol] = dataclasses.field( default_factory=dict ) @@ -449,8 +437,8 @@ class GuiModalHandle: _gui_api: GuiApi _id: str # Used as container ID of children. - _container_id_restore: Optional[str] = None - _children: Dict[str, SupportsRemoveProtocol] = dataclasses.field( + _container_id_restore: str | None = None + _children: dict[str, SupportsRemoveProtocol] = dataclasses.field( default_factory=dict ) @@ -484,8 +472,8 @@ class GuiTabHandle: _parent: GuiTabGroupHandle _id: str # Used as container ID of children. - _container_id_restore: Optional[str] = None - _children: Dict[str, SupportsRemoveProtocol] = dataclasses.field( + _container_id_restore: str | None = None + _children: dict[str, SupportsRemoveProtocol] = dataclasses.field( default_factory=dict ) @@ -525,7 +513,7 @@ def remove(self) -> None: child.remove() -def _get_data_url(url: str, image_root: Optional[Path]) -> str: +def _get_data_url(url: str, image_root: Path | None) -> str: if not url.startswith("http") and not image_root: warnings.warn( ( @@ -551,7 +539,7 @@ def _get_data_url(url: str, image_root: Optional[Path]) -> str: return url -def _parse_markdown(markdown: str, image_root: Optional[Path]) -> str: +def _parse_markdown(markdown: str, image_root: Path | None) -> str: markdown = re.sub( r"\!\[([^]]*)\]\(([^]]*)\)", lambda match: ( @@ -571,8 +559,8 @@ class GuiMarkdownHandle: _visible: bool _parent_container_id: str # Parent. _order: float - _image_root: Optional[Path] - _content: Optional[str] + _image_root: Path | None + _content: str | None @property def content(self) -> str: @@ -633,8 +621,8 @@ class GuiPlotlyHandle: _visible: bool _parent_container_id: str # Parent. _order: float - _figure: Optional[go.Figure] - _aspect: Optional[float] + _figure: go.Figure | None + _aspect: float | None @property def figure(self) -> go.Figure: diff --git a/src/viser/_scene_api.py b/src/viser/_scene_api.py index 2bb5db4b4..89b4eed91 100644 --- a/src/viser/_scene_api.py +++ b/src/viser/_scene_api.py @@ -8,22 +8,11 @@ from __future__ import annotations import base64 -import colorsys import io import time import warnings from concurrent.futures import ThreadPoolExecutor -from typing import ( - TYPE_CHECKING, - Callable, - Dict, - Optional, - Tuple, - TypeVar, - Union, - cast, - get_args, -) +from typing import TYPE_CHECKING, Callable, TypeVar, Union, cast, get_args import imageio.v3 as iio import numpy as onp @@ -60,16 +49,6 @@ P = ParamSpec("P") -def _hex_from_hls(h: float, l: float, s: float) -> str: - """Converts HLS values in [0.0, 1.0] to a hex-formatted string, eg 0xffffff.""" - return "#" + "".join( - [ - int(min(255, max(0, channel * 255.0)) + 0.5).to_bytes(1, "little").hex() - for channel in colorsys.hls_to_rgb(h, l, s) - ] - ) - - def _colors_to_uint8(colors: onp.ndarray) -> onpt.NDArray[onp.uint8]: """Convert intensity values to uint8. We assume the range [0,1] for floats, and [0,255] for integers. Accepts any shape.""" @@ -82,7 +61,7 @@ def _colors_to_uint8(colors: onp.ndarray) -> onpt.NDArray[onp.uint8]: RgbTupleOrArray: TypeAlias = Union[ - Tuple[int, int, int], Tuple[float, float, float], onp.ndarray + tuple[int, int, int], tuple[float, float, float], onp.ndarray ] @@ -100,8 +79,8 @@ def _encode_rgb(rgb: RgbTupleOrArray) -> int: def _encode_image_base64( image: onp.ndarray, format: Literal["png", "jpeg"], - jpeg_quality: Optional[int] = None, -) -> Tuple[Literal["image/png", "image/jpeg"], str]: + jpeg_quality: int | None = None, +) -> tuple[Literal["image/png", "image/jpeg"], str]: media_type: Literal["image/png", "image/jpeg"] image = _colors_to_uint8(image) with io.BytesIO() as data_buffer: @@ -172,14 +151,14 @@ def __init__( if isinstance(owner, ViserServer): self.world_axes.visible = False - self._handle_from_transform_controls_name: Dict[ + self._handle_from_transform_controls_name: dict[ str, TransformControlsHandle ] = {} - self._handle_from_node_name: Dict[str, SceneNodeHandle] = {} + self._handle_from_node_name: dict[str, SceneNodeHandle] = {} - self._scene_pointer_cb: Optional[Callable[[ScenePointerEvent], None]] = None + self._scene_pointer_cb: Callable[[ScenePointerEvent], None] | None = None self._scene_pointer_done_cb: Callable[[], None] = lambda: None - self._scene_pointer_event_type: Optional[_messages.ScenePointerEventType] = None + self._scene_pointer_event_type: _messages.ScenePointerEventType | None = None self._websock_interface.register_handler( _messages.TransformControlsUpdateMessage, @@ -199,7 +178,7 @@ def __init__( def set_up_direction( self, direction: Literal["+x", "+y", "+z", "-x", "-y", "-z"] - | Tuple[float, float, float] + | tuple[float, float, float] | onp.ndarray, ) -> None: """Set the global up direction of the scene. By default we follow +Z-up @@ -281,8 +260,8 @@ def add_glb( name: str, glb_data: bytes, scale=1.0, - 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), + 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, ) -> GlbHandle: """Add a general 3D asset via binary glTF (GLB). @@ -313,15 +292,15 @@ def add_glb( def add_spline_catmull_rom( self, name: str, - positions: Tuple[Tuple[float, float, float], ...] | onp.ndarray, + positions: tuple[tuple[float, float, float], ...] | onp.ndarray, curve_type: Literal["centripetal", "chordal", "catmullrom"] = "centripetal", tension: float = 0.5, closed: bool = False, line_width: float = 1, color: RgbTupleOrArray = (20, 20, 20), - segments: Optional[int] = None, - 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), + segments: int | None = None, + 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, ) -> SceneNodeHandle: """Add a spline to the scene using Catmull-Rom interpolation. @@ -368,13 +347,13 @@ def add_spline_catmull_rom( def add_spline_cubic_bezier( self, name: str, - positions: Tuple[Tuple[float, float, float], ...] | onp.ndarray, - control_points: Tuple[Tuple[float, float, float], ...] | onp.ndarray, + positions: tuple[tuple[float, float, float], ...] | onp.ndarray, + control_points: tuple[tuple[float, float, float], ...] | onp.ndarray, line_width: float = 1, color: RgbTupleOrArray = (20, 20, 20), - segments: Optional[int] = None, - 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), + segments: int | None = None, + 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, ) -> SceneNodeHandle: """Add a spline to the scene using Cubic Bezier interpolation. @@ -428,11 +407,11 @@ def add_camera_frustum( aspect: float, scale: float = 0.3, color: RgbTupleOrArray = (20, 20, 20), - image: Optional[onp.ndarray] = None, + image: onp.ndarray | None = None, format: Literal["png", "jpeg"] = "jpeg", - jpeg_quality: Optional[int] = None, - 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), + jpeg_quality: int | None = None, + 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, ) -> CameraFrustumHandle: """Add a camera frustum to the scene for visualization. @@ -491,8 +470,8 @@ def add_frame( axes_length: float = 0.5, axes_radius: float = 0.025, origin_radius: float | None = None, - 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), + 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, ) -> FrameHandle: """Add a coordinate frame to the scene. @@ -536,12 +515,12 @@ def add_frame( def add_batched_axes( self, name: str, - batched_wxyzs: Tuple[Tuple[float, float, float, float], ...] | onp.ndarray, - batched_positions: Tuple[Tuple[float, float, float], ...] | onp.ndarray, + batched_wxyzs: tuple[tuple[float, float, float, float], ...] | onp.ndarray, + batched_positions: tuple[tuple[float, float, float], ...] | onp.ndarray, axes_length: float = 0.5, axes_radius: float = 0.025, - 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), + 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, ) -> BatchedAxesHandle: """Visualize batched sets of coordinate frame axes. @@ -602,8 +581,8 @@ def add_grid( section_color: RgbTupleOrArray = (140, 140, 140), section_thickness: float = 1.0, section_size: float = 1.0, - 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), + 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, ) -> SceneNodeHandle: """Add a 2D grid to the scene. @@ -652,8 +631,8 @@ def add_label( self, name: str, 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), + 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. @@ -678,13 +657,13 @@ def add_point_cloud( self, name: str, points: onp.ndarray, - colors: onp.ndarray | Tuple[float, float, float], + colors: onp.ndarray | tuple[float, float, float], point_size: float = 0.1, point_shape: Literal[ "square", "diamond", "circle", "rounded", "sparkle" ] = "square", - 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), + 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, ) -> PointCloudHandle: """Add a point cloud to the scene. @@ -731,10 +710,6 @@ def add_point_cloud( ) return PointCloudHandle._make(self, name, wxyz, position, visible) - def add_mesh(self, *args, **kwargs) -> MeshHandle: - """Deprecated alias for `add_mesh_simple()`.""" - return self.add_mesh_simple(*args, **kwargs) - def add_mesh_simple( self, name: str, @@ -742,12 +717,12 @@ def add_mesh_simple( faces: onp.ndarray, color: RgbTupleOrArray = (90, 200, 255), wireframe: bool = False, - opacity: Optional[float] = None, + opacity: float | None = None, material: Literal["standard", "toon3", "toon5"] = "standard", flat_shading: bool = False, side: Literal["front", "back", "double"] = "front", - 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), + 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, ) -> MeshHandle: """Add a mesh to the scene. @@ -806,8 +781,8 @@ def add_mesh_trimesh( name: str, mesh: trimesh.Trimesh, scale: float = 1.0, - 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), + 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, ) -> GlbHandle: """Add a trimesh mesh to the scene. Internally calls `self.add_glb()`. @@ -841,9 +816,9 @@ def add_box( self, name: str, color: RgbTupleOrArray, - dimensions: Tuple[float, float, float] | onp.ndarray = (1.0, 1.0, 1.0), - 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), + dimensions: tuple[float, float, float] | onp.ndarray = (1.0, 1.0, 1.0), + 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, ) -> MeshHandle: """Add a box to the scene. @@ -881,8 +856,8 @@ def add_icosphere( radius: float, color: RgbTupleOrArray, subdivisions: int = 3, - 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), + 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, ) -> MeshHandle: """Add an icosphere to the scene. @@ -921,8 +896,8 @@ def set_background_image( self, image: onp.ndarray, format: Literal["png", "jpeg"] = "jpeg", - jpeg_quality: Optional[int] = None, - depth: Optional[onp.ndarray] = None, + jpeg_quality: int | None = None, + depth: onp.ndarray | None = None, ) -> None: """Set a background image for the scene, optionally with depth compositing. @@ -972,9 +947,9 @@ def add_image( render_width: float, render_height: float, format: Literal["png", "jpeg"] = "jpeg", - jpeg_quality: Optional[int] = None, - 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), + jpeg_quality: int | None = None, + 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, ) -> ImageHandle: """Add a 2D image to the scene. @@ -1016,20 +991,20 @@ def add_transform_controls( line_width: float = 2.5, fixed: bool = False, auto_transform: bool = True, - active_axes: Tuple[bool, bool, bool] = (True, True, True), + active_axes: tuple[bool, bool, bool] = (True, True, True), disable_axes: bool = False, disable_sliders: bool = False, disable_rotations: bool = False, - translation_limits: Tuple[ - Tuple[float, float], Tuple[float, float], Tuple[float, float] + translation_limits: tuple[ + tuple[float, float], tuple[float, float], tuple[float, float] ] = ((-1000.0, 1000.0), (-1000.0, 1000.0), (-1000.0, 1000.0)), - rotation_limits: Tuple[ - Tuple[float, float], Tuple[float, float], Tuple[float, float] + rotation_limits: tuple[ + tuple[float, float], tuple[float, float], tuple[float, float] ] = ((-1000.0, 1000.0), (-1000.0, 1000.0), (-1000.0, 1000.0)), depth_test: bool = True, opacity: float = 1.0, - 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), + 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, ) -> TransformControlsHandle: """Add a transform gizmo for interacting with the scene. @@ -1044,7 +1019,7 @@ def add_transform_controls( line_width: Width of the lines used in the gizmo. fixed: Boolean indicating if the gizmo should be fixed in position. auto_transform: Whether the transform should be applied automatically. - active_axes: Tuple of booleans indicating active axes. + active_axes: tuple of booleans indicating active axes. disable_axes: Boolean to disable axes interaction. disable_sliders: Boolean to disable slider interaction. disable_rotations: Boolean to disable rotation interaction. @@ -1119,7 +1094,7 @@ def _get_client_handle(self, client_id: ClientId) -> ClientHandle: # 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._owner._state.connected_clients[client_id] + return self._owner._connected_clients[client_id] else: assert client_id == self._owner.client_id return self._owner @@ -1279,8 +1254,8 @@ def remove_pointer_callback( 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), + 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 diff --git a/src/viser/_scene_handles.py b/src/viser/_scene_handles.py index 35a6f0471..5441b4b13 100644 --- a/src/viser/_scene_handles.py +++ b/src/viser/_scene_handles.py @@ -6,17 +6,7 @@ from __future__ import annotations import dataclasses -from typing import ( - TYPE_CHECKING, - Callable, - Dict, - Generic, - List, - Literal, - Tuple, - Type, - TypeVar, -) +from typing import TYPE_CHECKING, Callable, Generic, Literal, TypeVar import numpy as onp @@ -40,11 +30,11 @@ class ScenePointerEvent: """ID of client that triggered this event.""" event_type: _messages.ScenePointerEventType """Type of event that was triggered. Currently we only support clicks and box selections.""" - ray_origin: Tuple[float, float, float] | None + ray_origin: tuple[float, float, float] | None """Origin of 3D ray corresponding to this click, in world coordinates.""" - ray_direction: Tuple[float, float, float] | None + ray_direction: tuple[float, float, float] | None """Direction of 3D ray corresponding to this click, in world coordinates.""" - screen_pos: List[Tuple[float, float]] + screen_pos: list[tuple[float, float]] """Screen position of the click on the screen (OpenCV image coordinates, 0 to 1). (0, 0) is the upper-left corner, (1, 1) is the bottom-right corner. For a box selection, this includes the min- and max- corners of the box.""" @@ -70,7 +60,7 @@ class _SceneNodeHandleState: ) visible: bool = True # TODO: we should remove SceneNodeHandle as an argument here. - click_cb: List[ + click_cb: list[ Callable[[SceneNodePointerEvent[SceneNodeHandle]], None] ] | None = None @@ -83,11 +73,11 @@ class SceneNodeHandle: @classmethod def _make( - cls: Type[TSceneNodeHandle], + cls: type[TSceneNodeHandle], api: SceneApi, name: str, - wxyz: Tuple[float, float, float, float] | onp.ndarray, - position: Tuple[float, float, float] | onp.ndarray, + wxyz: tuple[float, float, float, float] | onp.ndarray, + position: tuple[float, float, float] | onp.ndarray, visible: bool, ) -> TSceneNodeHandle: out = cls(_SceneNodeHandleState(name, api)) @@ -110,7 +100,7 @@ def wxyz(self) -> onp.ndarray: return self._impl.wxyz @wxyz.setter - def wxyz(self, wxyz: Tuple[float, float, float, float] | onp.ndarray) -> None: + def wxyz(self, wxyz: tuple[float, float, float, float] | onp.ndarray) -> None: from ._scene_api import cast_vector wxyz_cast = cast_vector(wxyz, 4) @@ -127,7 +117,7 @@ def position(self) -> onp.ndarray: return self._impl.position @position.setter - def position(self, position: Tuple[float, float, float] | onp.ndarray) -> None: + def position(self, position: tuple[float, float, float] | onp.ndarray) -> None: from ._scene_api import cast_vector position_cast = cast_vector(position, 3) @@ -169,9 +159,9 @@ class SceneNodePointerEvent(Generic[TSceneNodeHandle]): """Type of event that was triggered. Currently we only support clicks.""" target: TSceneNodeHandle """Scene node that was clicked.""" - ray_origin: Tuple[float, float, float] + ray_origin: tuple[float, float, float] """Origin of 3D ray corresponding to this click, in world coordinates.""" - ray_direction: Tuple[float, float, float] + ray_direction: tuple[float, float, float] """Direction of 3D ray corresponding to this click, in world coordinates.""" @@ -234,7 +224,7 @@ class LabelHandle(SceneNodeHandle): @dataclasses.dataclass class _TransformControlsState: last_updated: float - update_cb: List[Callable[[TransformControlsHandle], None]] + update_cb: list[Callable[[TransformControlsHandle], None]] sync_cb: None | Callable[[ClientId, TransformControlsHandle], None] = None @@ -263,7 +253,7 @@ class Gui3dContainerHandle(SceneNodeHandle): _gui_api: GuiApi _container_id: str _container_id_restore: str | None = None - _children: Dict[str, SupportsRemoveProtocol] = dataclasses.field( + _children: dict[str, SupportsRemoveProtocol] = dataclasses.field( default_factory=dict ) diff --git a/src/viser/_tunnel.py b/src/viser/_tunnel.py index 08ab99c18..cbb68ecaf 100644 --- a/src/viser/_tunnel.py +++ b/src/viser/_tunnel.py @@ -5,7 +5,7 @@ from functools import lru_cache from multiprocessing.managers import DictProxy from pathlib import Path -from typing import Callable, Literal, Optional, Union +from typing import Callable, Literal import rich @@ -39,11 +39,11 @@ def __init__(self, share_domain: str, local_port: int) -> None: "[bold](viser)[/bold] No `if __name__ == '__main__'` check found; creating share URL tunnel in a thread" ) - self._process: Optional[mp.Process] = None - self._thread: Optional[threading.Thread] = None - self._event_loop: Optional[asyncio.AbstractEventLoop] = None + self._process: mp.Process | None = None + self._thread: threading.Thread | None = None + self._event_loop: asyncio.AbstractEventLoop | None = None - self._shared_state: Union[DictProxy, dict] + self._shared_state: DictProxy | dict if self._multiprocess_ok: manager = mp.Manager() self._connect_event = manager.Event() @@ -119,7 +119,7 @@ def wait_job() -> None: ) self._thread.start() - def get_url(self) -> Optional[str]: + def get_url(self) -> str | None: """Get tunnel URL. None if not connected (or connection failed).""" return self._shared_state["url"] @@ -149,11 +149,11 @@ def _() -> None: def _connect_job( connect_event: threading.Event, disconnect_event: threading.Event, - close_event: Optional[asyncio.Event], # Only for threads. + close_event: asyncio.Event | None, # Only for threads. share_domain: str, local_port: int, - shared_state: Union[DictProxy, dict], - event_loop_target: Optional[ViserTunnel], # Only for threads. + shared_state: DictProxy | dict, + event_loop_target: ViserTunnel | None, # Only for threads. ) -> None: event_loop = asyncio.new_event_loop() asyncio.set_event_loop(event_loop) @@ -184,10 +184,10 @@ def _connect_job( async def _make_tunnel( connect_event: threading.Event, disconnect_event: threading.Event, - close_event: Optional[asyncio.Event], + close_event: asyncio.Event | None, share_domain: str, local_port: int, - shared_state: Union[DictProxy, dict], + shared_state: DictProxy | dict, ) -> None: share_domain = "share.viser.studio" diff --git a/src/viser/_viser.py b/src/viser/_viser.py index 0ecc6bf6f..9995d2520 100644 --- a/src/viser/_viser.py +++ b/src/viser/_viser.py @@ -6,7 +6,7 @@ import threading import time from pathlib import Path -from typing import Callable, ContextManager, Dict, List, Optional, Tuple +from typing import Callable, ContextManager import imageio.v3 as iio import numpy as onp @@ -36,12 +36,22 @@ class _CameraHandleState: look_at: npt.NDArray[onp.float64] up_direction: npt.NDArray[onp.float64] update_timestamp: float - camera_cb: List[Callable[[CameraHandle], None]] + camera_cb: list[Callable[[CameraHandle], None]] -@dataclasses.dataclass class CameraHandle: - _state: _CameraHandleState + def __init__(self, client: ClientHandle) -> None: + self._state = _CameraHandleState( + client, + wxyz=onp.zeros(4), + position=onp.zeros(3), + fov=0.0, + aspect=0.0, + look_at=onp.zeros(3), + up_direction=onp.zeros(3), + update_timestamp=0.0, + camera_cb=[], + ) @property def client(self) -> ClientHandle: @@ -59,7 +69,7 @@ def wxyz(self) -> npt.NDArray[onp.float64]: # - https://github.com/python/mypy/issues/3004 # - https://github.com/python/mypy/pull/11643 @wxyz.setter - def wxyz(self, wxyz: Tuple[float, float, float, float] | onp.ndarray) -> None: + def wxyz(self, wxyz: tuple[float, float, float, float] | onp.ndarray) -> None: R_world_camera = tf.SO3(onp.asarray(wxyz)).as_matrix() look_distance = onp.linalg.norm(self.look_at - self.position) @@ -105,7 +115,7 @@ def position(self) -> npt.NDArray[onp.float64]: return self._state.position @position.setter - def position(self, position: Tuple[float, float, float] | onp.ndarray) -> None: + def position(self, position: tuple[float, float, float] | onp.ndarray) -> None: offset = onp.asarray(position) - onp.array(self.position) # type: ignore self._state.position = onp.asarray(position) self.look_at = onp.array(self.look_at) + offset @@ -157,7 +167,7 @@ def look_at(self) -> npt.NDArray[onp.float64]: return self._state.look_at @look_at.setter - def look_at(self, look_at: Tuple[float, float, float] | onp.ndarray) -> None: + def look_at(self, look_at: tuple[float, float, float] | onp.ndarray) -> None: self._state.look_at = onp.asarray(look_at) self._state.update_timestamp = time.time() self._update_wxyz() @@ -173,7 +183,7 @@ def up_direction(self) -> npt.NDArray[onp.float64]: @up_direction.setter def up_direction( - self, up_direction: Tuple[float, float, float] | onp.ndarray + self, up_direction: tuple[float, float, float] | onp.ndarray ) -> None: self._state.up_direction = onp.asarray(up_direction) self._update_wxyz() @@ -206,7 +216,7 @@ def get_render( # Listen for a render reseponse message, which should contain the rendered # image. render_ready_event = threading.Event() - out: Optional[onp.ndarray] = None + out: onp.ndarray | None = None connection = self.client._websock_connection @@ -248,35 +258,22 @@ class ClientHandle: def __init__( self, conn: infra.WebsockClientConnection, server: ViserServer ) -> None: + # Private attributes. self._websock_connection = conn self._viser_server = server + # Public attributes. self.scene: SceneApi = SceneApi( self, thread_executor=server._websock_server._thread_executor ) """Handle for interacting with the 3D scene.""" - self.gui: GuiApi = GuiApi( self, thread_executor=server._websock_server._thread_executor ) """Handle for interacting with the GUI.""" - - self.client_id = conn.client_id + self.client_id: int = conn.client_id """Unique ID for this client.""" - - self.camera = CameraHandle( - _CameraHandleState( - self, - wxyz=onp.zeros(4), - position=onp.zeros(3), - fov=0.0, - aspect=0.0, - look_at=onp.zeros(3), - up_direction=onp.zeros(3), - update_timestamp=0.0, - camera_cb=[], - ) - ) + self.camera: CameraHandle = CameraHandle(self) """Handle for reading from and manipulating the client's viewport camera.""" def flush(self) -> None: @@ -342,18 +339,6 @@ def send_file_download( self.flush() -# We can serialize the state of a ViserServer via a tuple of -# (serialized message, timestamp) pairs. -SerializedServerState = Tuple[Tuple[bytes, float], ...] - - -@dataclasses.dataclass -class _ViserServerState: - connection: infra.WebsockServer - connected_clients: Dict[int, ClientHandle] = dataclasses.field(default_factory=dict) - client_lock: threading.Lock = dataclasses.field(default_factory=threading.Lock) - - class ViserServer: """Viser server class. The primary interface for functionality in `viser`. @@ -371,7 +356,7 @@ def __init__( self, host: str = "0.0.0.0", port: int = 8080, - label: Optional[str] = None, + label: str | None = None, **_deprecated_kwargs, ): # Create server. @@ -386,10 +371,11 @@ def __init__( _client_autobuild.ensure_client_is_built() - state = _ViserServerState(server) - self._state = state - self._client_connect_cb: List[Callable[[ClientHandle], None]] = [] - self._client_disconnect_cb: List[Callable[[ClientHandle], None]] = [] + self._connection = server + self._connected_clients: dict[int, ClientHandle] = {} + self._client_lock = threading.Lock() + self._client_connect_cb: list[Callable[[ClientHandle], None]] = [] + self._client_disconnect_cb: list[Callable[[ClientHandle], None]] = [] # For new clients, register and add a handler for camera messages. @server.on_client_connect @@ -422,8 +408,8 @@ def handle_camera_message( # received. if first: first = False - with self._state.client_lock: - state.connected_clients[conn.client_id] = client + with self._client_lock: + self._connected_clients[conn.client_id] = client for cb in self._client_connect_cb: cb(client) @@ -435,11 +421,11 @@ def handle_camera_message( # Remove clients when they disconnect. @server.on_client_disconnect def _(conn: infra.WebsockClientConnection) -> None: - with self._state.client_lock: - if conn.client_id not in state.connected_clients: + with self._client_lock: + if conn.client_id not in self._connected_clients: return - handle = state.connected_clients.pop(conn.client_id) + handle = self._connected_clients.pop(conn.client_id) for cb in self._client_disconnect_cb: cb(handle) @@ -474,7 +460,7 @@ def _(conn: infra.WebsockClientConnection) -> None: table.add_row("Websocket", ws_url) rich.print(Panel(table, title="[bold]viser[/bold]", expand=False)) - self._share_tunnel: Optional[ViserTunnel] = None + self._share_tunnel: ViserTunnel | None = None # Create share tunnel if requested. # This is deprecated: we should use get_share_url() instead. @@ -483,7 +469,7 @@ def _(conn: infra.WebsockClientConnection) -> None: self.request_share_url() self.scene.reset() - self.gui.set_gui_panel_label(label) + self.gui.set_panel_label(label) def get_host(self) -> str: """Returns the host address of the Viser server. @@ -502,7 +488,7 @@ def get_port(self) -> int: """ return self._websock_server._port - def request_share_url(self, verbose: bool = True) -> Optional[str]: + def request_share_url(self, verbose: bool = True) -> str | None: """Request a share URL for the Viser server, which allows for public access. On the first call, will block until a connecting with the share URL server is established. Afterwards, the URL will be returned directly. @@ -573,22 +559,22 @@ def stop(self) -> None: if self._share_tunnel is not None: self._share_tunnel.close() - def get_clients(self) -> Dict[int, ClientHandle]: + def get_clients(self) -> dict[int, ClientHandle]: """Creates and returns a copy of the mapping from connected client IDs to handles. Returns: Dictionary of clients. """ - with self._state.client_lock: - return self._state.connected_clients.copy() + with self._client_lock: + return self._connected_clients.copy() def on_client_connect( self, cb: Callable[[ClientHandle], None] ) -> Callable[[ClientHandle], None]: """Attach a callback to run for newly connected clients.""" - with self._state.client_lock: - clients = self._state.connected_clients.copy().values() + with self._client_lock: + clients = self._connected_clients.copy().values() self._client_connect_cb.append(cb) # Trigger callback on any already-connected clients. diff --git a/src/viser/extras/_urdf.py b/src/viser/extras/_urdf.py index ab1702851..54dd4f5c0 100644 --- a/src/viser/extras/_urdf.py +++ b/src/viser/extras/_urdf.py @@ -4,9 +4,10 @@ import numpy as onp import trimesh -import viser import yourdfpy +import viser + from .. import transforms as tf