diff --git a/docs/source/examples/03_gui_callbacks.rst b/docs/source/examples/03_gui_callbacks.rst index 927ffdeb4..94d1b09ee 100644 --- a/docs/source/examples/03_gui_callbacks.rst +++ b/docs/source/examples/03_gui_callbacks.rst @@ -44,7 +44,10 @@ we get updates. gui_plane.on_update(lambda _: update_plane()) with server.add_gui_folder("Control", expand_by_default=False): - gui_show = server.add_gui_checkbox("Show Frame", initial_value=True) + gui_show_frame = server.add_gui_checkbox("Show Frame", initial_value=True) + gui_show_everything = server.add_gui_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) @@ -75,7 +78,7 @@ we get updates. "/frame", wxyz=(1.0, 0.0, 0.0, 0.0), position=pos, - show_axes=gui_show.value, + show_axes=gui_show_frame.value, axes_length=5.0, ) @@ -89,7 +92,10 @@ we get updates. # We can (optionally) also attach callbacks! # Here, we update the point clouds + frames whenever any of the GUI items are updated. - gui_show.on_update(lambda _: draw_frame()) + gui_show_frame.on_update(lambda _: draw_frame()) + gui_show_everything.on_update( + lambda _: server.set_global_scene_node_visibility(gui_show_everything.value) + ) gui_axis.on_update(lambda _: draw_frame()) gui_location.on_update(lambda _: draw_frame()) gui_num_points.on_update(lambda _: draw_points()) @@ -97,7 +103,7 @@ we get updates. @gui_reset_scene.on_click def _(_) -> None: """Reset the scene when the reset button is clicked.""" - gui_show.value = True + gui_show_frame.value = True gui_location.value = 0.0 gui_axis.value = "x" gui_num_points.value = 10_000 diff --git a/docs/source/examples/06_mesh.rst b/docs/source/examples/06_mesh.rst index ec01e41d7..2549cf4ad 100644 --- a/docs/source/examples/06_mesh.rst +++ b/docs/source/examples/06_mesh.rst @@ -22,7 +22,7 @@ Visualize a mesh. To get the demo data, see ``./assets/download_dragon_mesh.sh`` import viser import viser.transforms as tf - mesh = trimesh.load_mesh(Path(__file__).parent / "assets/dragon.obj") + mesh = trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj")) assert isinstance(mesh, trimesh.Trimesh) mesh.apply_scale(0.05) diff --git a/docs/source/examples/20_scene_click.rst b/docs/source/examples/20_scene_click.rst index c2a9215bf..859f8e9b3 100644 --- a/docs/source/examples/20_scene_click.rst +++ b/docs/source/examples/20_scene_click.rst @@ -29,7 +29,7 @@ To get the demo data, see ``./assets/download_dragon_mesh.sh``. server = viser.ViserServer() - mesh = trimesh.load_mesh(Path(__file__).parent / "assets/dragon.obj") + mesh = trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj")) assert isinstance(mesh, trimesh.Trimesh) mesh.apply_scale(0.05) diff --git a/pyproject.toml b/pyproject.toml index 101a3cbfb..eabc73e87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ dev = [ "pyright>=1.1.308", "mypy>=1.4.1", - "ruff==0.0.267", + "ruff==0.1.9", "black==23.11.0", "pre-commit==3.3.2", ] @@ -87,6 +87,7 @@ select = [ ignore = [ "E741", # Ambiguous variable name. (l, O, or I) "E501", # Line too long. + "E721", # Do not compare types, use `isinstance()`. "F722", # Forward annotation false positive from jaxtyping. Should be caught by pyright. "F821", # Forward annotation false positive from jaxtyping. Should be caught by pyright. "PLR2004", # Magic value used in comparison. diff --git a/src/viser/_client_autobuild.py b/src/viser/_client_autobuild.py index 9483bf0c1..6fb5a8ff3 100644 --- a/src/viser/_client_autobuild.py +++ b/src/viser/_client_autobuild.py @@ -68,6 +68,7 @@ def ensure_client_is_built() -> None: ), cwd=client_dir, shell=True, + check=False, ) @@ -79,10 +80,13 @@ def _install_sandboxed_node() -> Path: rich.print("[bold](viser)[/bold] nodejs is set up!") return env_dir - subprocess.run([sys.executable, "-m", "nodeenv", "--node=20.4.0", env_dir]) + subprocess.run( + [sys.executable, "-m", "nodeenv", "--node=20.4.0", env_dir], check=False + ) subprocess.run( args=[env_dir / "bin" / "npm", "install", "yarn"], input="y\n".encode(), + check=False, ) assert (env_dir / "bin" / "npx").exists() return env_dir diff --git a/src/viser/_message_api.py b/src/viser/_message_api.py index 0e430d80b..8fdecdf08 100644 --- a/src/viser/_message_api.py +++ b/src/viser/_message_api.py @@ -280,37 +280,34 @@ def rotate_between(before: onp.ndarray, after: onp.ndarray) -> tf.SO3: before = before / onp.linalg.norm(before) after = after / onp.linalg.norm(after) - angle = onp.arccos(onp.dot(before, after)) + angle = onp.arccos(onp.clip(onp.dot(before, after), -1, 1)) axis = onp.cross(before, after) - if onp.allclose(axis, onp.zeros(3)): - axis = onp.cross( - before, - (1.0, 0.0, 0.0) - if onp.abs(before[0]) < onp.abs(before[1]) - else (0.0, 1.0, 0.0), - ) + if onp.allclose(axis, onp.zeros(3), rtol=1e-3, atol=1e-5): + unit_vector = onp.arange(3) == onp.argmin(onp.abs(before)) + axis = onp.cross(before, unit_vector) axis = axis / onp.linalg.norm(axis) + return tf.SO3.exp(angle * axis) - return tf.SO3.exp(-angle * axis) - - R_threeworld_world = rotate_between(default_three_up, direction) + R_threeworld_world = rotate_between(direction, default_three_up) # Rotate the world frame such that: # If we set +Y to up, +X and +Z should face the camera. # If we set +Z to up, +X and +Y should face the camera. - # - # This could be made more efficient... - thetas = onp.arange(360) - sums = [ - onp.sum( - (tf.SO3.from_y_radians(theta) @ R_threeworld_world) - @ onp.array([-1.0, -1.0, -1.0]) + # In App.tsx, the camera is initialized at [-3, 3, -3] in the threejs + # coordinate frame. + desired_fwd = onp.array([-1.0, 0.0, -1.0]) / onp.sqrt(2.0) + current_fwd = R_threeworld_world @ (onp.ones(3) / onp.sqrt(3.0)) + current_fwd = current_fwd * onp.array([1.0, 0.0, 1.0]) + current_fwd = current_fwd / onp.linalg.norm(current_fwd) + R_threeworld_world = ( + tf.SO3.from_y_radians( # Rotate around the null space / up direction. + onp.arctan2( + onp.cross(current_fwd, desired_fwd)[1], + onp.dot(current_fwd, desired_fwd), + ), ) - for theta in thetas - ] - best_theta = thetas[onp.argmax(sums)] - - R_threeworld_world = tf.SO3.from_y_radians(best_theta) @ R_threeworld_world + @ R_threeworld_world + ) if not onp.any(onp.isnan(R_threeworld_world.wxyz)): # Set the orientation of the root node. @@ -685,8 +682,8 @@ def add_point_cloud( assert ( len(points.shape) == 2 and points.shape[-1] == 3 ), "Shape of points should be (N, 3)." - assert colors_cast.shape == points.shape or colors_cast.shape == ( - 3, + assert ( + colors_cast.shape == points.shape == (3,) ), "Shape of colors should be (N, 3) or (3,)." if colors_cast.shape == (3,): diff --git a/sync_message_defs.py b/sync_message_defs.py index 55eae61f6..6a22eba43 100644 --- a/sync_message_defs.py +++ b/sync_message_defs.py @@ -19,4 +19,4 @@ print(f"Wrote to {target_path}") # Run prettier. - subprocess.run(args=["npx", "prettier", "-w", str(target_path)]) + subprocess.run(args=["npx", "prettier", "-w", str(target_path)], check=False)