Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions python/mujoco/simulate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,11 @@ PYBIND11_MODULE(_simulate, pymodule) {
CallIfNotNull(+[](mujoco::Simulate& sim, int enabled) {
sim.ui1_enable = enabled;
}),
py::call_guard<py::gil_scoped_release>())
.def_property("keyboard_ui", GetIfNotNull(&mujoco::Simulate::keyboard_ui),
CallIfNotNull(+[](mujoco::Simulate& sim, int enabled) {
sim.keyboard_ui = enabled;
}),
py::call_guard<py::gil_scoped_release>());

pymodule.def("set_glfw_dlhandle", [](std::uintptr_t dlhandle) {
Expand Down
74 changes: 59 additions & 15 deletions python/mujoco/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
import weakref

import glfw
import numpy as np

import mujoco
from mujoco import _simulate
import numpy as np

if not glfw._glfw: # pylint: disable=protected-access
raise RuntimeError('GLFW dynamic library handle is not available')
Expand Down Expand Up @@ -116,8 +117,11 @@ def viewport(self):
return None

def set_figures(
self, viewports_figures: Union[Tuple[mujoco.MjrRect, mujoco.MjvFigure],
List[Tuple[mujoco.MjrRect, mujoco.MjvFigure]]]
self,
viewports_figures: Union[
Tuple[mujoco.MjrRect, mujoco.MjvFigure],
List[Tuple[mujoco.MjrRect, mujoco.MjvFigure]],
],
):
"""Overlay figures on the viewer.

Expand All @@ -138,8 +142,15 @@ def clear_figures(self):
if sim is not None:
sim.clear_figures()

def set_texts(self, texts: Union[Tuple[Optional[int], Optional[int], Optional[str], Optional[str]],
List[Tuple[Optional[int], Optional[int], Optional[str], Optional[str]]]]):
def set_texts(
self,
texts: Union[
Tuple[Optional[int], Optional[int], Optional[str], Optional[str]],
List[
Tuple[Optional[int], Optional[int], Optional[str], Optional[str]]
],
],
):
"""Overlay text on the viewer.

Args:
Expand All @@ -158,12 +169,15 @@ def set_texts(self, texts: Union[Tuple[Optional[int], Optional[int], Optional[st
# Convert None values to empty strings
default_font = mujoco.mjtFontScale.mjFONTSCALE_150
default_gridpos = mujoco.mjtGridPos.mjGRID_TOPLEFT
processed_texts = [(
default_font if font is None else font,
default_gridpos if gridpos is None else gridpos,
"" if text1 is None else text1,
"" if text2 is None else text2)
for font, gridpos, text1, text2 in texts]
processed_texts = [
(
default_font if font is None else font,
default_gridpos if gridpos is None else gridpos,
'' if text1 is None else text1,
'' if text2 is None else text2,
)
for font, gridpos, text1, text2 in texts
]

sim.set_texts(processed_texts)

Expand All @@ -173,8 +187,11 @@ def clear_texts(self):
sim.clear_texts()

def set_images(
self, viewports_images: Union[Tuple[mujoco.MjrRect, np.ndarray],
List[Tuple[mujoco.MjrRect, np.ndarray]]]
self,
viewports_images: Union[
Tuple[mujoco.MjrRect, np.ndarray],
List[Tuple[mujoco.MjrRect, np.ndarray]],
],
):
"""Overlay images on the viewer.

Expand All @@ -194,7 +211,10 @@ def set_images(
targ_shape = (viewport.height, viewport.width)
# Check if image is already the correct shape
if image.shape[:2] != targ_shape:
raise ValueError(f"Image shape {image.shape[:2]} does not match target shape {targ_shape}")
raise ValueError(
f'Image shape {image.shape[:2]} does not match target shape'
f' {targ_shape}'
)
flipped = np.flip(image, axis=0)
contiguous = np.ascontiguousarray(flipped)
processed_images.append((viewport, contiguous))
Expand Down Expand Up @@ -268,6 +288,9 @@ def launch_on_ui_thread(
data: mujoco.MjData,
handle_return: Optional['queue.Queue[Handle]'],
key_callback: Optional[KeyCallbackType],
show_left_ui: bool = True,
show_right_ui: bool = True,
handle_keyboard: bool = True,
):
pass

Expand Down Expand Up @@ -455,6 +478,7 @@ def _launch_internal(
key_callback: Optional[KeyCallbackType] = None,
show_left_ui: bool = True,
show_right_ui: bool = True,
handle_keyboard: bool = True,
) -> None:
"""Internal API, so that the public API has more readable type annotations."""
if model is None and data is not None:
Expand Down Expand Up @@ -490,6 +514,7 @@ def _loader(m=model, d=data) -> Tuple[mujoco.MjModel, mujoco.MjData]:

simulate.ui0_enable = show_left_ui
simulate.ui1_enable = show_right_ui
simulate.keyboard_ui = handle_keyboard

# Initialize GLFW if not using mjpython.
if _MJPYTHON is None:
Expand Down Expand Up @@ -535,6 +560,7 @@ def launch(
loader: Optional[LoaderType] = None,
show_left_ui: bool = True,
show_right_ui: bool = True,
handle_keyboard: bool = True,
) -> None:
"""Launches the Simulate GUI."""
_launch_internal(
Expand All @@ -544,6 +570,7 @@ def launch(
loader=loader,
show_left_ui=show_left_ui,
show_right_ui=show_right_ui,
handle_keyboard=handle_keyboard,
)


Expand All @@ -559,8 +586,23 @@ def launch_passive(
key_callback: Optional[KeyCallbackType] = None,
show_left_ui: bool = True,
show_right_ui: bool = True,
handle_keyboard: bool = True,
) -> Handle:
"""Launches a passive Simulate GUI without blocking the running thread."""
"""Launches a passive Simulate GUI without blocking the running thread.

Args:
model: The MuJoCo model.
data: The MuJoCo data.
key_callback: Optional callback for custom key handling.
show_left_ui: Whether to show the left UI panel.
show_right_ui: Whether to show the right UI panel.
handle_keyboard: Whether to handle keyboard shortcuts. Set to False to
disable built-in keyboard shortcuts (e.g., 'W' for wireframe toggle).
This is useful when using keyboard input for robot teleoperation.

Returns:
A Handle for interacting with the viewer.
"""
if not isinstance(model, mujoco.MjModel):
raise ValueError(f'`model` is not a mujoco.MjModel: got {model!r}')
if not isinstance(data, mujoco.MjData):
Expand All @@ -581,6 +623,7 @@ def launch_passive(
key_callback=key_callback,
show_left_ui=show_left_ui,
show_right_ui=show_right_ui,
handle_keyboard=handle_keyboard,
),
)
thread.daemon = True
Expand All @@ -598,6 +641,7 @@ def launch_passive(
key_callback,
show_left_ui,
show_right_ui,
handle_keyboard,
)

return handle_return.get()
Expand Down
2 changes: 1 addition & 1 deletion simulate/simulate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1661,7 +1661,7 @@ void UiEvent(mjuiState* state) {
}

// shortcut not handled by UI
if (state->type==mjEVENT_KEY && state->key!=0) {
if (state->type==mjEVENT_KEY && state->key!=0 && sim->keyboard_ui) {
switch (state->key) {
case ' ': // Mode
if (!sim->is_passive_ && sim->m_) {
Expand Down
1 change: 1 addition & 0 deletions simulate/simulate.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class Simulate {
int font = 0;
int ui0_enable = 1;
int ui1_enable = 1;
int keyboard_ui = 1; // enable keyboard shortcuts
int help = 0;
int info = 0;
int profiler = 0;
Expand Down