Files
Mujoco_WASM/python/mujoco/experimental/studio/studio_app_events.py
T
Matija Kecman 4cf4a5665d Configure Copybara export for Dear ImGui and ImPlot Python bindings
Following the export declarations in Dear ImGui and ImPlot METADATA, this change updates MuJoCo's Copybara configuration (copy.bara.sky) to export and transform the Python bindings. `//third_party/dear_imgui/google/py` exports to `python/mujoco/experimental/dear_imgui` and `//third_party/implot/google/py` exports to  `python/mujoco/experimental/implot`.

PiperOrigin-RevId: 925293624
Change-Id: Ie6e32d247a6f7fc24bb36ae7060f2075d8efeb26
2026-06-02 05:32:22 -07:00

593 lines
17 KiB
Python

# Copyright 2026 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Temporary event handling functions for StudioApp."""
# TODO(matijak): These free functions implement the keyboard and mouse event
# handling for Studio. They are separated from the main StudioApp class to keep
# clarify the long-term API and avoid cluttering it with a large amount of
# temporary code. When studio/platform has a proper API for registering key
# bindings and mouse behaviour, the event handling functions will delegate to
# code shared with the C++ studio application.
import mujoco
from mujoco.experimental.studio import sim
from mujoco.experimental.studio import ux
import numpy as np
from mujoco.experimental.dear_imgui import dear_imgui as imgui
def handle_vis_options_keyboard_events(
vis_options: mujoco.MjvOption,
is_freecam_wasd: bool,
) -> bool:
"""Toggles visualization flags based on keyboard shortcuts.
Args:
vis_options: The visualization options to modify.
is_freecam_wasd: If True, keys Q/E/A/D are reserved for camera movement and
will not toggle visualization flags.
Returns:
True if a key was handled, False otherwise.
"""
if imgui.GetIO().WantCaptureKeyboard:
return False
pressed = imgui.IsKeyChordPressed
# Frame and label cycling.
if pressed(imgui.Key.F6):
vis_options.frame = (vis_options.frame + 1) % mujoco.mjtFrame.mjNFRAME.value
elif pressed(imgui.Key.F7):
vis_options.label = (vis_options.label + 1) % mujoco.mjtLabel.mjNLABEL.value
# Visualization flag toggles (single-key shortcuts).
elif pressed(imgui.Key.H):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONVEXHULL] ^= 1
elif pressed(imgui.Key.X):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_TEXTURE] ^= 1
elif pressed(imgui.Key.J):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_JOINT] ^= 1
elif not is_freecam_wasd and pressed(imgui.Key.Q):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CAMERA] ^= 1
elif pressed(imgui.Key.U):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_ACTUATOR] ^= 1
elif pressed(imgui.Key.Comma):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_ACTIVATION] ^= 1
elif pressed(imgui.Key.Z):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_LIGHT] ^= 1
elif pressed(imgui.Key.V):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_TENDON] ^= 1
elif pressed(imgui.Key.Y):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_RANGEFINDER] ^= 1
elif not is_freecam_wasd and pressed(imgui.Key.E):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONSTRAINT] ^= 1
elif pressed(imgui.Key.I):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_INERTIA] ^= 1
elif pressed(imgui.Key.Apostrophe):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_SCLINERTIA] ^= 1
elif pressed(imgui.Key.B):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_PERTFORCE] ^= 1
elif pressed(imgui.Key.O):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_PERTOBJ] ^= 1
elif pressed(imgui.Key.C):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] ^= 1
elif pressed(imgui.Key.N):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_ISLAND] ^= 1
elif pressed(imgui.Key.F):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] ^= 1
elif pressed(imgui.Key.P):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTSPLIT] ^= 1
elif pressed(imgui.Key.T):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_TRANSPARENT] ^= 1
elif not is_freecam_wasd and pressed(imgui.Key.A):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_AUTOCONNECT] ^= 1
elif pressed(imgui.Key.M):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_COM] ^= 1
elif not is_freecam_wasd and pressed(imgui.Key.D):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_STATIC] ^= 1
elif pressed(imgui.Key.Semicolon):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_SKIN] ^= 1
elif pressed(imgui.Key.GraveAccent):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_BODYBVH] ^= 1
elif pressed(imgui.Key.Backslash):
vis_options.flags[mujoco.mjtVisFlag.mjVIS_MESHBVH] ^= 1
# Site group toggles (Shift + 0-5).
elif pressed(int(imgui.Key.N0) | int(imgui.Key.Shift)):
vis_options.sitegroup[0] ^= 1
elif pressed(int(imgui.Key.N1) | int(imgui.Key.Shift)):
vis_options.sitegroup[1] ^= 1
elif pressed(int(imgui.Key.N2) | int(imgui.Key.Shift)):
vis_options.sitegroup[2] ^= 1
elif pressed(int(imgui.Key.N3) | int(imgui.Key.Shift)):
vis_options.sitegroup[3] ^= 1
elif pressed(int(imgui.Key.N4) | int(imgui.Key.Shift)):
vis_options.sitegroup[4] ^= 1
elif pressed(int(imgui.Key.N5) | int(imgui.Key.Shift)):
vis_options.sitegroup[5] ^= 1
# Geom group toggles (0-5).
elif pressed(imgui.Key.N0):
vis_options.geomgroup[0] ^= 1
elif pressed(imgui.Key.N1):
vis_options.geomgroup[1] ^= 1
elif pressed(imgui.Key.N2):
vis_options.geomgroup[2] ^= 1
elif pressed(imgui.Key.N3):
vis_options.geomgroup[3] ^= 1
elif pressed(imgui.Key.N4):
vis_options.geomgroup[4] ^= 1
elif pressed(imgui.Key.N5):
vis_options.geomgroup[5] ^= 1
else:
return False
return True
def handle_step_control_keyboard_events(
model: mujoco.MjModel,
data: mujoco.MjData,
step_control: sim.StepControl,
ux_state: ux.UxState,
) -> bool:
"""Handles keyboard shortcuts for simulation stepping control.
Args:
model: The MuJoCo model.
data: The MuJoCo data.
step_control: The simulation step control object.
ux_state: The UX state object.
Returns:
True if a key was handled, False otherwise.
"""
if imgui.GetIO().WantCaptureKeyboard:
return False
pressed = imgui.IsKeyChordPressed
if pressed(int(imgui.Key.Ctrl) | int(imgui.Key.Space)):
if step_control.get_pause_state() == sim.PauseState.VISCOUS_PAUSED:
step_control.set_pause_state(sim.PauseState.UNPAUSED)
else:
step_control.set_pause_state(sim.PauseState.VISCOUS_PAUSED)
return True
elif pressed(imgui.Key.Space):
pause = step_control.get_pause_state()
if pause in (sim.PauseState.VISCOUS_PAUSED, sim.PauseState.UNPAUSED):
step_control.set_pause_state(sim.PauseState.NORMAL_PAUSED)
else:
step_control.set_pause_state(sim.PauseState.UNPAUSED)
return True
elif pressed(imgui.Key.Backspace):
mujoco.mj_resetData(model, data)
mujoco.mj_forward(model, data)
return True
elif pressed(imgui.Key.Minus):
ux.set_speed_index(step_control, ux_state, ux_state.speed_index + 1)
return True
elif pressed(imgui.Key.Equal):
ux.set_speed_index(step_control, ux_state, ux_state.speed_index - 1)
return True
return False
def handle_camera_select_keyboard_events(
model: mujoco.MjModel,
camera: mujoco.MjvCamera,
ux_state: ux.UxState,
) -> bool:
"""Handles keyboard shortcuts for camera selection.
Args:
model: The MuJoCo model.
camera: The MuJoCo camera object.
ux_state: The UX state object.
Returns:
True if a key was handled, False otherwise.
"""
if imgui.GetIO().WantCaptureKeyboard:
return False
pressed = imgui.IsKeyChordPressed
if pressed(imgui.Key.Escape):
ux_state.camera_index = ux.set_camera(model, camera, ux.TUMBLE_CAMERA_IDX)
return True
elif pressed(imgui.Key.LeftBracket):
ux_state.camera_index = ux.set_camera(
model, camera, ux_state.camera_index - 1
)
return True
elif pressed(imgui.Key.RightBracket):
ux_state.camera_index = ux.set_camera(
model, camera, ux_state.camera_index + 1
)
return True
return False
def handle_freecam_wasd_keyboard_events(
model: mujoco.MjModel,
data: mujoco.MjData,
camera: mujoco.MjvCamera,
cam_speed: float,
) -> tuple[bool, float]:
"""Handles keyboard shortcuts for free camera movement.
Args:
model: The MuJoCo model.
data: The MuJoCo data.
camera: The MuJoCo camera object.
cam_speed: The current camera speed.
Returns:
A tuple of (handled, updated_cam_speed).
"""
if imgui.GetIO().WantCaptureKeyboard:
return False, cam_speed
if (
imgui.IsKeyDown(imgui.Key.W)
or imgui.IsKeyDown(imgui.Key.S)
or imgui.IsKeyDown(imgui.Key.A)
or imgui.IsKeyDown(imgui.Key.D)
or imgui.IsKeyDown(imgui.Key.Q)
or imgui.IsKeyDown(imgui.Key.E)
):
moved = False
# Move (dolly) forward/backward using W and S keys.
if imgui.IsKeyDown(imgui.Key.W):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.TRUCK_DOLLY,
0,
cam_speed,
)
moved = True
elif imgui.IsKeyDown(imgui.Key.S):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.TRUCK_DOLLY,
0,
-cam_speed,
)
moved = True
# Strafe (truck) left/right using A and D keys.
if imgui.IsKeyDown(imgui.Key.A):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.TRUCK_DOLLY,
-cam_speed,
0,
)
moved = True
elif imgui.IsKeyDown(imgui.Key.D):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.TRUCK_DOLLY,
cam_speed,
0,
)
moved = True
# Move (pedestal) up/down using Q and E keys.
if imgui.IsKeyDown(imgui.Key.Q):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.TRUCK_PEDESTAL,
0,
cam_speed,
)
moved = True
elif imgui.IsKeyDown(imgui.Key.E):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.TRUCK_PEDESTAL,
0,
-cam_speed,
)
moved = True
if moved:
cam_speed += 0.001
max_speed = 0.1 if imgui.GetIO().KeyShift else 0.01
if cam_speed > max_speed:
cam_speed = max_speed
else:
cam_speed = 0.001
return True, cam_speed
return False, cam_speed
def handle_keyboard_events(
model: mujoco.MjModel,
data: mujoco.MjData,
camera: mujoco.MjvCamera,
vis_options: mujoco.MjvOption,
step_control: sim.StepControl,
ux_state: ux.UxState,
cam_speed: float,
) -> tuple[bool, float]:
"""Handle keyboard events according to Studio's bindings.
Args:
model: The MuJoCo model.
data: The MuJoCo data.
camera: The MuJoCo camera object.
vis_options: The MuJoCo visualization options.
step_control: The simulation step control object.
ux_state: The UX state object.
cam_speed: The current camera speed.
Returns:
A tuple of (handled, updated_cam_speed).
"""
if imgui.GetIO().WantCaptureKeyboard:
return False, cam_speed
is_freecam_wasd = ux_state.camera_index == ux.FREE_CAMERA_IDX
if handle_step_control_keyboard_events(model, data, step_control, ux_state):
return True, cam_speed
if handle_camera_select_keyboard_events(model, camera, ux_state):
return True, cam_speed
if handle_vis_options_keyboard_events(vis_options, is_freecam_wasd):
return True, cam_speed
if is_freecam_wasd:
handled, cam_speed = handle_freecam_wasd_keyboard_events(
model, data, camera, cam_speed
)
if handled:
return True, cam_speed
return False, cam_speed
def handle_camera_tracking_mouse_events(
model: mujoco.MjModel,
data: mujoco.MjData,
camera: mujoco.MjvCamera,
vis_options: mujoco.MjvOption,
ux_state: ux.UxState,
) -> None:
"""Handles mouse events for camera tracking."""
io = imgui.GetIO()
if imgui.GetIO().WantCaptureMouse:
return
if io.DisplaySize.x <= 0 or io.DisplaySize.y <= 0:
return
mouse_x = io.MousePos.x / io.DisplaySize.x
mouse_y = io.MousePos.y / io.DisplaySize.y
aspect_ratio = io.DisplaySize.x / io.DisplaySize.y
# Right double click.
if imgui.IsMouseDoubleClicked(imgui.MouseButton.Right):
picked = ux.Pick(
model,
data,
camera,
mouse_x,
mouse_y,
aspect_ratio,
vis_options,
)
if picked.body > 0 and io.KeyCtrl:
# Switch camera to tracking mode and track the selected body.
camera.type = int(mujoco.mjtCamera.mjCAMERA_TRACKING)
camera.trackbodyid = picked.body
camera.fixedcamid = -1
ux_state.camera_index = ux.TRACKING_CAMERA_IDX
def handle_mouse_events(
model: mujoco.MjModel,
data: mujoco.MjData,
camera: mujoco.MjvCamera,
vis_options: mujoco.MjvOption,
perturb: mujoco.MjvPerturb,
ux_state: ux.UxState,
) -> None:
"""Handles mouse events."""
io = imgui.GetIO()
if io.WantCaptureMouse:
return
if io.DisplaySize.x <= 0 or io.DisplaySize.y <= 0:
return
mouse_x = io.MousePos.x / io.DisplaySize.x
mouse_y = io.MousePos.y / io.DisplaySize.y
mouse_dx = io.MouseDelta.x / io.DisplaySize.x
mouse_dy = io.MouseDelta.y / io.DisplaySize.y
mouse_scroll = io.MouseWheel / 50.0
is_mouse_moving = mouse_dx != 0.0 or mouse_dy != 0.0
is_any_mouse_down = (
imgui.IsMouseDown(imgui.MouseButton.Left)
or imgui.IsMouseDown(imgui.MouseButton.Right)
or imgui.IsMouseDown(imgui.MouseButton.Middle)
)
is_mouse_dragging = is_mouse_moving and is_any_mouse_down
# If no mouse buttons are down, end any active perturbations.
if not is_any_mouse_down:
perturb.active = 0
# Handle perturbation mouse actions.
if is_mouse_dragging and io.KeyCtrl:
if perturb.select > 0:
action = int(mujoco.mjtMouse.mjMOUSE_NONE)
if imgui.IsMouseDown(imgui.MouseButton.Left):
if io.KeyAlt:
action = int(
mujoco.mjtMouse.mjMOUSE_MOVE_H
if io.KeyShift
else mujoco.mjtMouse.mjMOUSE_MOVE_V
)
else:
action = int(
mujoco.mjtMouse.mjMOUSE_ROTATE_H
if io.KeyShift
else mujoco.mjtMouse.mjMOUSE_ROTATE_V
)
elif imgui.IsMouseDown(imgui.MouseButton.Right):
action = int(
mujoco.mjtMouse.mjMOUSE_MOVE_H
if io.KeyShift
else mujoco.mjtMouse.mjMOUSE_MOVE_V
)
elif imgui.IsMouseDown(imgui.MouseButton.Middle):
action = int(mujoco.mjtMouse.mjMOUSE_ZOOM)
active = int(
mujoco.mjtPertBit.mjPERT_TRANSLATE
if action
in (
int(mujoco.mjtMouse.mjMOUSE_MOVE_V),
int(mujoco.mjtMouse.mjMOUSE_MOVE_H),
)
else mujoco.mjtPertBit.mjPERT_ROTATE
)
if active != perturb.active:
ux.InitPerturb(model, data, camera, perturb, active)
ux.MovePerturb(
model,
data,
camera,
perturb,
action,
mouse_dx,
mouse_dy,
)
elif is_mouse_dragging:
if ux_state.camera_index == ux.FREE_CAMERA_IDX:
if imgui.IsMouseDown(imgui.MouseButton.Left):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.PAN_TILT,
mouse_dx,
mouse_dy,
)
else:
if imgui.IsMouseDown(imgui.MouseButton.Left):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.ORBIT,
mouse_dx,
mouse_dy,
)
elif imgui.IsMouseDown(imgui.MouseButton.Middle):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.ZOOM,
mouse_dx,
mouse_dy,
)
# Right mouse movement is relative to the horizontal and vertical planes.
if imgui.IsMouseDown(imgui.MouseButton.Right) and io.KeyShift:
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.PLANAR_MOVE_H,
mouse_dx,
mouse_dy,
)
elif imgui.IsMouseDown(imgui.MouseButton.Right):
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.PLANAR_MOVE_V,
mouse_dx,
mouse_dy,
)
# Mouse scroll.
if mouse_scroll != 0.0 and ux_state.camera_index != ux.FREE_CAMERA_IDX:
ux.MoveCamera(
model,
data,
camera,
ux.CameraMotion.ZOOM,
0,
-mouse_scroll,
)
aspect_ratio = io.DisplaySize.x / io.DisplaySize.y
# Left double click.
if imgui.IsMouseDoubleClicked(imgui.MouseButton.Left):
picked = ux.Pick(
model,
data,
camera,
mouse_x,
mouse_y,
aspect_ratio,
vis_options,
)
if picked.body >= 0:
perturb.select = picked.body
perturb.flexselect = picked.flex
perturb.skinselect = picked.skin
# Compute the local position of the selected object in the world.
tmp = np.array(picked.point, dtype=np.float64) - data.xpos[picked.body]
xmat = np.array(data.xmat[picked.body], dtype=np.float64).reshape(3, 3)
perturb.localpos = xmat.T @ tmp
else:
perturb.select = 0
perturb.flexselect = -1
perturb.skinselect = -1
handle_camera_tracking_mouse_events(
model, data, camera, vis_options, ux_state
)