mujoco-studio 4.0.0__cp315-cp315-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. mujoco/_render_filament.cp315-win_amd64.pyd +0 -0
  2. mujoco/studio/__init__.py +45 -0
  3. mujoco/studio/_studio.cp315-win_amd64.pyd +0 -0
  4. mujoco/studio/app/__init__.py +33 -0
  5. mujoco/studio/app/inputs.py +553 -0
  6. mujoco/studio/app/plugins.py +778 -0
  7. mujoco/studio/assets/AtkinsonHyperlegibleMono-Regular.ttf +0 -0
  8. mujoco/studio/assets/AtkinsonHyperlegibleNext[wght].ttf +0 -0
  9. mujoco/studio/assets/decor.filamat +0 -0
  10. mujoco/studio/assets/decor_fade.filamat +0 -0
  11. mujoco/studio/assets/fontawesome-webfont.ttf +0 -0
  12. mujoco/studio/assets/haze.filamat +0 -0
  13. mujoco/studio/assets/ibl.ktx +0 -0
  14. mujoco/studio/assets/outline_composite.filamat +0 -0
  15. mujoco/studio/assets/outline_flatten.filamat +0 -0
  16. mujoco/studio/assets/outline_jumpflood.filamat +0 -0
  17. mujoco/studio/assets/pbr.filamat +0 -0
  18. mujoco/studio/assets/pbr_packed.filamat +0 -0
  19. mujoco/studio/assets/pbr_packed_reflect.filamat +0 -0
  20. mujoco/studio/assets/pbr_packed_transparent.filamat +0 -0
  21. mujoco/studio/assets/pbr_reflect.filamat +0 -0
  22. mujoco/studio/assets/pbr_transparent.filamat +0 -0
  23. mujoco/studio/assets/phong_2d.filamat +0 -0
  24. mujoco/studio/assets/phong_2d_fade.filamat +0 -0
  25. mujoco/studio/assets/phong_2d_uv.filamat +0 -0
  26. mujoco/studio/assets/phong_2d_uv_fade.filamat +0 -0
  27. mujoco/studio/assets/phong_2d_uv_reflect.filamat +0 -0
  28. mujoco/studio/assets/phong_color.filamat +0 -0
  29. mujoco/studio/assets/phong_color_fade.filamat +0 -0
  30. mujoco/studio/assets/phong_color_reflect.filamat +0 -0
  31. mujoco/studio/assets/phong_cube.filamat +0 -0
  32. mujoco/studio/assets/phong_cube_fade.filamat +0 -0
  33. mujoco/studio/assets/phong_cube_reflect.filamat +0 -0
  34. mujoco/studio/assets/unlit_depth.filamat +0 -0
  35. mujoco/studio/assets/unlit_segmentation.filamat +0 -0
  36. mujoco/studio/assets/unlit_ui.filamat +0 -0
  37. mujoco/studio/dear_imgui/dear_imgui.cp315-win_amd64.pyd +0 -0
  38. mujoco/studio/endpoints.py +162 -0
  39. mujoco/studio/hot_reload.py +573 -0
  40. mujoco/studio/hot_reload_utils.py +220 -0
  41. mujoco/studio/implot/implot.cp315-win_amd64.pyd +0 -0
  42. mujoco/studio/launch_native.py +97 -0
  43. mujoco/studio/launch_passive.py +85 -0
  44. mujoco/studio/launch_thread.py +132 -0
  45. mujoco/studio/launch_web.py +111 -0
  46. mujoco/studio/messages.py +697 -0
  47. mujoco/studio/native_viewer.py +262 -0
  48. mujoco/studio/parser.py +57 -0
  49. mujoco/studio/plugin.py +225 -0
  50. mujoco/studio/plugin_registry.py +103 -0
  51. mujoco/studio/sim_history.py +217 -0
  52. mujoco/studio/step_control.py +114 -0
  53. mujoco/studio/viewer.py +115 -0
  54. mujoco/studio/viewer_handle.py +538 -0
  55. mujoco/studio/viewer_protocol.py +337 -0
  56. mujoco/studio/viewer_utils.py +80 -0
  57. mujoco/studio/web/__init__.py +32 -0
  58. mujoco/studio/web/client_camera.py +116 -0
  59. mujoco/studio/web/index.html +612 -0
  60. mujoco/studio/web/web_server.py +1477 -0
  61. mujoco/studio/web_viewer.py +471 -0
  62. mujoco_studio-4.0.0.dist-info/METADATA +4742 -0
  63. mujoco_studio-4.0.0.dist-info/RECORD +68 -0
  64. mujoco_studio-4.0.0.dist-info/WHEEL +5 -0
  65. mujoco_studio-4.0.0.dist-info/entry_points.txt +2 -0
  66. mujoco_studio-4.0.0.dist-info/licenses/LICENSE +202 -0
  67. mujoco_studio-4.0.0.dist-info/licenses/LICENSES_THIRD_PARTY.md +4675 -0
  68. mujoco_studio-4.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,45 @@
1
+ # Copyright 2026 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """MuJoCo Studio package."""
15
+
16
+ import importlib as _importlib
17
+ import importlib.machinery as _machinery
18
+ import sys as _sys
19
+
20
+ from mujoco._version import __version__ as _core_version
21
+
22
+ __version__ = '4.0.0'
23
+ if _core_version != __version__:
24
+ raise ImportError(
25
+ f"Version mismatch between 'mujoco-studio' ({__version__}) and "
26
+ f"'mujoco-core' ({_core_version}). Please upgrade both packages "
27
+ 'together:\n'
28
+ f" pip install --upgrade 'mujoco-studio=={__version__}' "
29
+ f"'mujoco-core=={__version__}'"
30
+ )
31
+ del _core_version
32
+
33
+ try:
34
+ _studio_ext = _importlib.import_module('mujoco.studio._studio')
35
+ except ImportError:
36
+ pass
37
+ else:
38
+ for _name in ('native_viewer_cc', 'sim', 'ux', 'window'):
39
+ _submod = getattr(_studio_ext, _name, None)
40
+ if _submod is not None:
41
+ _submod.__spec__ = _machinery.ModuleSpec(f'{__name__}.{_name}', None)
42
+ globals()[_name] = _submod
43
+ _sys.modules[f'{__name__}.{_name}'] = _submod
44
+ del _name, _submod, _studio_ext
45
+ del _importlib, _machinery, _sys
@@ -0,0 +1,33 @@
1
+ # Copyright 2026 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Built-in MuJoCo Studio application."""
15
+
16
+ # pylint: disable=g-importing-member
17
+ from mujoco.studio.app import inputs
18
+ from mujoco.studio.app import plugins
19
+ from mujoco.studio.app.plugins import App
20
+ from mujoco.studio.app.plugins import Config
21
+ from mujoco.studio.app.plugins import InitEvent
22
+ from mujoco.studio.app.plugins import install_console_hooks
23
+ from mujoco.studio.app.plugins import SIM_PLUGINS
24
+
25
+ __all__ = [
26
+ 'App',
27
+ 'Config',
28
+ 'InitEvent',
29
+ 'SIM_PLUGINS',
30
+ 'inputs',
31
+ 'install_console_hooks',
32
+ 'plugins',
33
+ ]
@@ -0,0 +1,553 @@
1
+ # Copyright 2026 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Temporary input (e.g., keyboard, mouse) handling functions for Studio app."""
15
+
16
+ # TODO(matijak): These free functions implement the keyboard and mouse event
17
+ # handling for Studio. They are separated from the main App class to
18
+ # clarify the long-term API and avoid cluttering it with a large amount of
19
+ # temporary code. When studio/platform has a proper API for registering key
20
+ # bindings and mouse behaviour, the event handling functions will delegate to
21
+ # code shared with the C++ studio application.
22
+
23
+ from typing import Any
24
+
25
+ import mujoco
26
+ from mujoco.studio import sim
27
+ from mujoco.studio import ux
28
+ import numpy as np
29
+
30
+ from mujoco.studio.dear_imgui import dear_imgui as imgui
31
+
32
+
33
+ def handle_vis_options_keyboard_events(
34
+ vis_options: mujoco.MjvOption,
35
+ is_freecam_wasd: bool,
36
+ ) -> bool:
37
+ """Toggles visualization flags based on keyboard shortcuts.
38
+
39
+ Args:
40
+ vis_options: The visualization options to modify.
41
+ is_freecam_wasd: If True, keys Q/E/A/D are reserved for camera movement and
42
+ will not toggle visualization flags.
43
+
44
+ Returns:
45
+ True if a key was handled, False otherwise.
46
+ """
47
+ if imgui.GetIO().WantCaptureKeyboard:
48
+ return False
49
+
50
+ pressed = imgui.IsKeyChordPressed
51
+
52
+ # Frame and label cycling.
53
+ if pressed(imgui.Key.F6):
54
+ vis_options.frame = (vis_options.frame + 1) % mujoco.mjtFrame.mjNFRAME.value
55
+ elif pressed(imgui.Key.F7):
56
+ vis_options.label = (vis_options.label + 1) % mujoco.mjtLabel.mjNLABEL.value
57
+
58
+ # Visualization flag toggles (single-key shortcuts).
59
+ elif pressed(imgui.Key.H):
60
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONVEXHULL] ^= 1
61
+ elif pressed(imgui.Key.X):
62
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_TEXTURE] ^= 1
63
+ elif pressed(imgui.Key.J):
64
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_JOINT] ^= 1
65
+ elif not is_freecam_wasd and pressed(imgui.Key.Q):
66
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_CAMERA] ^= 1
67
+ elif pressed(imgui.Key.U):
68
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_ACTUATOR] ^= 1
69
+ elif pressed(imgui.Key.Comma):
70
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_ACTIVATION] ^= 1
71
+ elif pressed(imgui.Key.Z):
72
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_LIGHT] ^= 1
73
+ elif pressed(imgui.Key.V):
74
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_TENDON] ^= 1
75
+ elif pressed(imgui.Key.Y):
76
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_RANGEFINDER] ^= 1
77
+ elif not is_freecam_wasd and pressed(imgui.Key.E):
78
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONSTRAINT] ^= 1
79
+ elif pressed(imgui.Key.I):
80
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_INERTIA] ^= 1
81
+ elif pressed(imgui.Key.Apostrophe):
82
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_SCLINERTIA] ^= 1
83
+ elif pressed(imgui.Key.B):
84
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_PERTFORCE] ^= 1
85
+ elif pressed(imgui.Key.O):
86
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_PERTOBJ] ^= 1
87
+ elif pressed(imgui.Key.C):
88
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] ^= 1
89
+ elif pressed(imgui.Key.N):
90
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_ISLAND] ^= 1
91
+ elif pressed(imgui.Key.F):
92
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] ^= 1
93
+ elif pressed(imgui.Key.P):
94
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTSPLIT] ^= 1
95
+ elif pressed(imgui.Key.T):
96
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_TRANSPARENT] ^= 1
97
+ elif not is_freecam_wasd and pressed(imgui.Key.A):
98
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_AUTOCONNECT] ^= 1
99
+ elif pressed(imgui.Key.M):
100
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_COM] ^= 1
101
+ elif not is_freecam_wasd and pressed(imgui.Key.D):
102
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_STATIC] ^= 1
103
+ elif pressed(imgui.Key.Semicolon):
104
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_SKIN] ^= 1
105
+ elif pressed(imgui.Key.GraveAccent):
106
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_BODYBVH] ^= 1
107
+ elif pressed(imgui.Key.Backslash):
108
+ vis_options.flags[mujoco.mjtVisFlag.mjVIS_MESHBVH] ^= 1
109
+
110
+ # Site group toggles (Shift + 0-5).
111
+ elif pressed(int(imgui.Key.N0) | int(imgui.Key.Shift)):
112
+ vis_options.sitegroup[0] ^= 1
113
+ elif pressed(int(imgui.Key.N1) | int(imgui.Key.Shift)):
114
+ vis_options.sitegroup[1] ^= 1
115
+ elif pressed(int(imgui.Key.N2) | int(imgui.Key.Shift)):
116
+ vis_options.sitegroup[2] ^= 1
117
+ elif pressed(int(imgui.Key.N3) | int(imgui.Key.Shift)):
118
+ vis_options.sitegroup[3] ^= 1
119
+ elif pressed(int(imgui.Key.N4) | int(imgui.Key.Shift)):
120
+ vis_options.sitegroup[4] ^= 1
121
+ elif pressed(int(imgui.Key.N5) | int(imgui.Key.Shift)):
122
+ vis_options.sitegroup[5] ^= 1
123
+
124
+ # Geom group toggles (0-5).
125
+ elif pressed(imgui.Key.N0):
126
+ vis_options.geomgroup[0] ^= 1
127
+ elif pressed(imgui.Key.N1):
128
+ vis_options.geomgroup[1] ^= 1
129
+ elif pressed(imgui.Key.N2):
130
+ vis_options.geomgroup[2] ^= 1
131
+ elif pressed(imgui.Key.N3):
132
+ vis_options.geomgroup[3] ^= 1
133
+ elif pressed(imgui.Key.N4):
134
+ vis_options.geomgroup[4] ^= 1
135
+ elif pressed(imgui.Key.N5):
136
+ vis_options.geomgroup[5] ^= 1
137
+
138
+ else:
139
+ return False
140
+
141
+ return True
142
+
143
+
144
+ def handle_step_control_keyboard_events(
145
+ step_control: sim.StepControl,
146
+ ux_state: ux.UxState,
147
+ ) -> bool:
148
+ """Handles keyboard shortcuts for simulation stepping control.
149
+
150
+ Args:
151
+ step_control: The simulation step control object.
152
+ ux_state: The UX state object.
153
+
154
+ Returns:
155
+ True if a key was handled, False otherwise.
156
+ """
157
+ if imgui.GetIO().WantCaptureKeyboard:
158
+ return False
159
+
160
+ pressed = imgui.IsKeyChordPressed
161
+
162
+ if pressed(imgui.Key.Space):
163
+ if step_control.get_pause_state() == sim.PauseState.UNPAUSED:
164
+ step_control.set_pause_state(sim.PauseState.NORMAL_PAUSED)
165
+ else:
166
+ step_control.set_pause_state(sim.PauseState.UNPAUSED)
167
+ return True
168
+ elif pressed(imgui.Key.Minus):
169
+ ux_state.speed_index = ux.set_speed_index(
170
+ step_control, ux_state.speed_index, ux_state.speed_index + 1
171
+ )
172
+ return True
173
+ elif pressed(imgui.Key.Equal):
174
+ ux_state.speed_index = ux.set_speed_index(
175
+ step_control, ux_state.speed_index, ux_state.speed_index - 1
176
+ )
177
+ return True
178
+
179
+ return False
180
+
181
+
182
+ def handle_sim_history_keyboard_events(
183
+ step_control: sim.StepControl,
184
+ ux_state: ux.UxState,
185
+ ) -> bool:
186
+ """Handles keyboard shortcuts for scrubbing the simulation history.
187
+
188
+ Left/Right load the previous/next recorded frame while paused; Right at the
189
+ head of the history requests a single step instead. Matches the C++ Studio
190
+ app.
191
+
192
+ Args:
193
+ step_control: The simulation step control object.
194
+ ux_state: The UX state holding the timeline history size and index.
195
+
196
+ Returns:
197
+ True if a key was handled, False otherwise.
198
+ """
199
+ if imgui.GetIO().WantCaptureKeyboard or ux_state.history_size <= 0:
200
+ return False
201
+
202
+ # Scrubbing only makes sense while paused: a running sim would immediately
203
+ # step away from the loaded frame.
204
+ if step_control.get_pause_state() != sim.PauseState.NORMAL_PAUSED:
205
+ return False
206
+
207
+ pressed = imgui.IsKeyChordPressed
208
+
209
+ if pressed(imgui.Key.LeftArrow):
210
+ ux_state.history_index -= 1
211
+ return True
212
+ elif pressed(imgui.Key.RightArrow):
213
+ if ux_state.history_index == 0:
214
+ step_control.request_single_step()
215
+ else:
216
+ ux_state.history_index += 1
217
+ return True
218
+
219
+ return False
220
+
221
+
222
+ def handle_reset_keyboard_events(
223
+ model: mujoco.MjModel, data: mujoco.MjData
224
+ ) -> bool:
225
+ """Handles keyboard shortcuts for simulation reset."""
226
+ if imgui.GetIO().WantCaptureKeyboard:
227
+ return False
228
+
229
+ if imgui.IsKeyChordPressed(imgui.Key.Backspace):
230
+ if model is not None and data is not None:
231
+ mujoco.mj_resetData(model, data)
232
+ mujoco.mj_forward(model, data)
233
+ return True
234
+
235
+ return False
236
+
237
+
238
+ def handle_camera_select_keyboard_events(
239
+ model: mujoco.MjModel,
240
+ camera: mujoco.MjvCamera,
241
+ ux_state: ux.UxState,
242
+ ) -> bool:
243
+ """Handles keyboard shortcuts for camera selection.
244
+
245
+ Args:
246
+ model: The MuJoCo model.
247
+ camera: The MuJoCo camera object.
248
+ ux_state: The UX state object.
249
+
250
+ Returns:
251
+ True if a key was handled, False otherwise.
252
+ """
253
+ if imgui.GetIO().WantCaptureKeyboard:
254
+ return False
255
+
256
+ pressed = imgui.IsKeyChordPressed
257
+
258
+ if pressed(imgui.Key.Escape):
259
+ ux_state.camera_index = ux.set_camera(model, camera, ux.TUMBLE_CAMERA_IDX)
260
+ return True
261
+ elif pressed(imgui.Key.LeftBracket):
262
+ ux_state.camera_index = ux.set_camera(
263
+ model, camera, ux_state.camera_index - 1
264
+ )
265
+ return True
266
+ elif pressed(imgui.Key.RightBracket):
267
+ ux_state.camera_index = ux.set_camera(
268
+ model, camera, ux_state.camera_index + 1
269
+ )
270
+ return True
271
+
272
+ return False
273
+
274
+
275
+ def handle_miscellaneous_keyboard_events(app: Any) -> bool:
276
+ """Handles miscellaneous keyboard shortcuts for toggling panels and windows.
277
+
278
+ Tab/Shift+Tab toggle Options and Inspector panels.
279
+ F2/F3 the Info and Profiler windows.
280
+
281
+ Args:
282
+ app: The viewer application instance, whose config holds the UI state flags.
283
+
284
+ Returns:
285
+ True if a key was handled, False otherwise.
286
+ """
287
+ if imgui.GetIO().WantCaptureKeyboard:
288
+ return False
289
+
290
+ # Shift+Tab has to be tested before Tab, else the plain chord consumes it.
291
+ if imgui.IsKeyChordPressed(int(imgui.Key.Tab) | int(imgui.Key.Shift)):
292
+ app.config.show_inspector = not app.config.show_inspector
293
+ return True
294
+ elif imgui.IsKeyChordPressed(imgui.Key.Tab):
295
+ app.config.show_options = not app.config.show_options
296
+ return True
297
+ elif imgui.IsKeyChordPressed(imgui.Key.F2):
298
+ app.config.show_info = not app.config.show_info
299
+ return True
300
+ elif imgui.IsKeyChordPressed(imgui.Key.F3):
301
+ app.config.show_profiler = not app.config.show_profiler
302
+ return True
303
+
304
+ return False
305
+
306
+
307
+ def handle_freecam_wasd_keyboard_events(
308
+ model: mujoco.MjModel,
309
+ data: mujoco.MjData,
310
+ camera: mujoco.MjvCamera,
311
+ cam_speed: float,
312
+ ) -> tuple[bool, float]:
313
+ """Handles keyboard shortcuts for free camera movement.
314
+
315
+ Flies the camera with W/S, A/D and Q/E using the camera controls shared with
316
+ the C++ Studio app and the web viewer (ux.move_camera_with_keys).
317
+
318
+ Args:
319
+ model: The MuJoCo model.
320
+ data: The MuJoCo data.
321
+ camera: The MuJoCo camera object.
322
+ cam_speed: The current camera speed.
323
+
324
+ Returns:
325
+ A tuple of (handled, updated_cam_speed).
326
+ """
327
+ if imgui.GetIO().WantCaptureKeyboard:
328
+ return False, cam_speed
329
+
330
+ del data # Unused.
331
+ return ux.move_camera_with_keys(model, camera, cam_speed)
332
+
333
+
334
+ def handle_keyboard_events(
335
+ model: mujoco.MjModel,
336
+ data: mujoco.MjData,
337
+ camera: mujoco.MjvCamera,
338
+ vis_options: mujoco.MjvOption,
339
+ step_control: sim.StepControl,
340
+ ux_state: ux.UxState,
341
+ cam_speed: float,
342
+ ) -> tuple[bool, float]:
343
+ """Handle keyboard events according to Studio's bindings.
344
+
345
+ Args:
346
+ model: The MuJoCo model.
347
+ data: The MuJoCo data.
348
+ camera: The MuJoCo camera object.
349
+ vis_options: The MuJoCo visualization options.
350
+ step_control: The simulation step control object.
351
+ ux_state: The UX state object.
352
+ cam_speed: The current camera speed.
353
+
354
+ Returns:
355
+ A tuple of (handled, updated_cam_speed).
356
+ """
357
+ if imgui.GetIO().WantCaptureKeyboard:
358
+ return False, cam_speed
359
+
360
+ is_freecam_wasd = ux_state.camera_index == ux.FREE_CAMERA_IDX
361
+ if handle_step_control_keyboard_events(step_control, ux_state):
362
+ return True, cam_speed
363
+
364
+ if handle_reset_keyboard_events(model, data):
365
+ return True, cam_speed
366
+
367
+ if handle_camera_select_keyboard_events(model, camera, ux_state):
368
+ return True, cam_speed
369
+
370
+ if handle_vis_options_keyboard_events(vis_options, is_freecam_wasd):
371
+ return True, cam_speed
372
+
373
+ if is_freecam_wasd:
374
+ handled, cam_speed = handle_freecam_wasd_keyboard_events(
375
+ model, data, camera, cam_speed
376
+ )
377
+ if handled:
378
+ return True, cam_speed
379
+
380
+ return False, cam_speed
381
+
382
+
383
+ def handle_camera_tracking_mouse_events(
384
+ model: mujoco.MjModel,
385
+ data: mujoco.MjData,
386
+ camera: mujoco.MjvCamera,
387
+ vis_options: mujoco.MjvOption,
388
+ ux_state: ux.UxState,
389
+ ) -> None:
390
+ """Handles mouse events for camera tracking."""
391
+ io = imgui.GetIO()
392
+ if imgui.GetIO().WantCaptureMouse:
393
+ return
394
+
395
+ if io.DisplaySize.x <= 0 or io.DisplaySize.y <= 0:
396
+ return
397
+
398
+ mouse_x = io.MousePos.x / io.DisplaySize.x
399
+ mouse_y = io.MousePos.y / io.DisplaySize.y
400
+ aspect_ratio = io.DisplaySize.x / io.DisplaySize.y
401
+
402
+ # Right double click.
403
+ if imgui.IsMouseDoubleClicked(imgui.MouseButton.Right):
404
+ picked = ux.Pick(
405
+ model,
406
+ data,
407
+ camera,
408
+ mouse_x,
409
+ mouse_y,
410
+ aspect_ratio,
411
+ vis_options,
412
+ )
413
+ camera.lookat[:] = picked.point
414
+ if picked.body > 0 and io.KeyCtrl:
415
+ # Switch camera to tracking mode and track the selected body.
416
+ camera.type = int(mujoco.mjtCamera.mjCAMERA_TRACKING)
417
+ camera.trackbodyid = picked.body
418
+ camera.fixedcamid = -1
419
+ ux_state.camera_index = ux.TRACKING_CAMERA_IDX
420
+
421
+
422
+ def handle_mouse_events(
423
+ model: mujoco.MjModel,
424
+ data: mujoco.MjData,
425
+ camera: mujoco.MjvCamera,
426
+ vis_options: mujoco.MjvOption,
427
+ perturb: mujoco.MjvPerturb,
428
+ ux_state: ux.UxState,
429
+ move_camera: bool = True,
430
+ ) -> None:
431
+ """Handles mouse events.
432
+
433
+ Args:
434
+ model: The MuJoCo model.
435
+ data: The MuJoCo data.
436
+ camera: The MuJoCo camera object.
437
+ vis_options: The MuJoCo visualization options.
438
+ perturb: The MuJoCo perturbation object.
439
+ ux_state: The UX state object.
440
+ move_camera: Whether drags and scrolls move the camera. False when the
441
+ viewer's client moves it itself (Viewer.client_camera); picking and
442
+ perturbation are handled either way.
443
+ """
444
+ io = imgui.GetIO()
445
+ if io.WantCaptureMouse:
446
+ return
447
+
448
+ if io.DisplaySize.x <= 0 or io.DisplaySize.y <= 0:
449
+ return
450
+
451
+ mouse_x = io.MousePos.x / io.DisplaySize.x
452
+ mouse_y = io.MousePos.y / io.DisplaySize.y
453
+ mouse_dx = io.MouseDelta.x / io.DisplaySize.x
454
+ mouse_dy = io.MouseDelta.y / io.DisplaySize.y
455
+
456
+ is_mouse_moving = mouse_dx != 0.0 or mouse_dy != 0.0
457
+ is_any_mouse_down = (
458
+ imgui.IsMouseDown(imgui.MouseButton.Left)
459
+ or imgui.IsMouseDown(imgui.MouseButton.Right)
460
+ or imgui.IsMouseDown(imgui.MouseButton.Middle)
461
+ )
462
+ is_mouse_dragging = is_mouse_moving and is_any_mouse_down
463
+
464
+ # If no mouse buttons are down, end any active perturbations.
465
+ if not is_any_mouse_down:
466
+ perturb.active = 0
467
+
468
+ # Handle perturbation mouse actions.
469
+ if is_mouse_dragging and io.KeyCtrl:
470
+ if perturb.select > 0:
471
+ action = int(mujoco.mjtMouse.mjMOUSE_NONE)
472
+ if imgui.IsMouseDown(imgui.MouseButton.Left):
473
+ if io.KeyAlt:
474
+ action = int(
475
+ mujoco.mjtMouse.mjMOUSE_MOVE_H
476
+ if io.KeyShift
477
+ else mujoco.mjtMouse.mjMOUSE_MOVE_V
478
+ )
479
+ else:
480
+ action = int(
481
+ mujoco.mjtMouse.mjMOUSE_ROTATE_H
482
+ if io.KeyShift
483
+ else mujoco.mjtMouse.mjMOUSE_ROTATE_V
484
+ )
485
+ elif imgui.IsMouseDown(imgui.MouseButton.Right):
486
+ action = int(
487
+ mujoco.mjtMouse.mjMOUSE_MOVE_H
488
+ if io.KeyShift
489
+ else mujoco.mjtMouse.mjMOUSE_MOVE_V
490
+ )
491
+ elif imgui.IsMouseDown(imgui.MouseButton.Middle):
492
+ action = int(mujoco.mjtMouse.mjMOUSE_ZOOM)
493
+
494
+ move_actions = (
495
+ int(mujoco.mjtMouse.mjMOUSE_MOVE_V),
496
+ int(mujoco.mjtMouse.mjMOUSE_MOVE_H),
497
+ )
498
+ active = int(
499
+ mujoco.mjtPertBit.mjPERT_TRANSLATE
500
+ if action in move_actions
501
+ else mujoco.mjtPertBit.mjPERT_ROTATE
502
+ )
503
+ if active != perturb.active:
504
+ ux.InitPerturb(model, data, camera, perturb, active)
505
+ ux.MovePerturb(
506
+ model,
507
+ data,
508
+ camera,
509
+ perturb,
510
+ action,
511
+ mouse_dx,
512
+ mouse_dy,
513
+ )
514
+
515
+ # Camera drags and wheel, with the camera controls shared with the C++ Studio
516
+ # app and the web viewer (which leave Ctrl drags to the perturbation above).
517
+ if move_camera:
518
+ ux.move_camera_with_mouse(
519
+ model,
520
+ camera,
521
+ fly_camera=ux_state.camera_index == ux.FREE_CAMERA_IDX,
522
+ )
523
+
524
+ aspect_ratio = io.DisplaySize.x / io.DisplaySize.y
525
+
526
+ # Left double click.
527
+ if imgui.IsMouseDoubleClicked(imgui.MouseButton.Left):
528
+ picked = ux.Pick(
529
+ model,
530
+ data,
531
+ camera,
532
+ mouse_x,
533
+ mouse_y,
534
+ aspect_ratio,
535
+ vis_options,
536
+ )
537
+ if picked.body >= 0:
538
+ perturb.select = picked.body
539
+ perturb.flexselect = picked.flex
540
+ perturb.skinselect = picked.skin
541
+
542
+ # Compute the local position of the selected object in the world.
543
+ tmp = np.array(picked.point, dtype=np.float64) - data.xpos[picked.body]
544
+ xmat = np.array(data.xmat[picked.body], dtype=np.float64).reshape(3, 3)
545
+ perturb.localpos = xmat.T @ tmp
546
+ else:
547
+ perturb.select = 0
548
+ perturb.flexselect = -1
549
+ perturb.skinselect = -1
550
+
551
+ handle_camera_tracking_mouse_events(
552
+ model, data, camera, vis_options, ux_state
553
+ )