1use glfw::{Action, Context, Glfw, GlfwReceiver, Key, Modifiers, MouseButton, PWindow, WindowEvent};
2use bitflags::bitflags;
3
4use std::time::Instant;
5
6use crate::{get_mujoco_version, mujoco_c::*};
7
8#[cfg(feature = "cpp-viewer")]
9use std::ffi::CString;
10
11use crate::prelude::{MjrContext, MjrRectangle, MjtFont, MjtGridPos};
12use crate::wrappers::mj_visualization::*;
13use crate::wrappers::mj_model::MjModel;
14use crate::wrappers::mj_data::MjData;
15
16const MJ_VIEWER_DEFAULT_SIZE_PX: (u32, u32) = (1280, 720);
20const MJ_VIEWER_EXTRA_SCENE_GEOM_SPACE: usize = 100;
22const DOUBLE_CLICK_WINDOW_MS: u128 = 250;
23
24const HELP_MENU_TITLES: &str = concat!(
25 "Toggle help\n",
26 "Free camera\n",
27 "Track camera\n",
28 "Camera orbit\n",
29 "Camera pan\n",
30 "Camera look at\n",
31 "Zoom\n",
32 "Object select\n",
33 "Selection rotate\n",
34 "Selection translate\n",
35 "Exit"
36);
37
38const HELP_MENU_VALUES: &str = concat!(
39 "F1\n",
40 "Escape\n",
41 "Control + Alt + double-left click\n",
42 "Left drag\n",
43 "Right [+Shift] drag\n",
44 "Alt + double-left click\n",
45 "Zoom, middle drag\n",
46 "Double-left click\n",
47 "Control + [Shift] + drag\n",
48 "Control + Alt + [Shift] + drag\n",
49 "Control + Q"
50);
51
52#[derive(Debug)]
53pub enum MjViewerError {
54 GlfwInitError (glfw::InitError),
55 WindowCreationError
56}
57
58#[derive(Debug)]
79pub struct MjViewer<'m> {
80 scene: MjvScene<'m>,
82 context: MjrContext,
83 camera: MjvCamera,
84
85 model: &'m MjModel,
87 pert: MjvPerturb,
88
89 last_x: mjtNum,
91 last_y: mjtNum,
92 last_bnt_press_time: Instant,
93 rect_view: MjrRectangle,
94 rect_full: MjrRectangle,
95 status_flags: ViewerStatusBits, glfw: Glfw,
99 window: PWindow,
100 events: GlfwReceiver<(f64, WindowEvent)>,
101
102 user_scn: MjvScene<'m>
104}
105
106impl<'m> MjViewer<'m> {
107 pub fn launch_passive(model: &'m MjModel, scene_max_geom: usize) -> Result<Self, MjViewerError> {
112 let mut glfw = glfw::init_no_callbacks()
113 .map_err(|err| MjViewerError::GlfwInitError(err))?;
114
115 let (w, h) = MJ_VIEWER_DEFAULT_SIZE_PX;
116 let title = format!("MuJoCo Rust Viewer (MuJoCo {})", get_mujoco_version());
117
118 let (mut window, events) = match glfw.create_window(
119 w, h, &title,
120 glfw::WindowMode::Windowed
121 ) {
122 Some(x) => Ok(x),
123 None => Err(MjViewerError::WindowCreationError)
124 }?;
125
126 window.make_current();
128 window.set_all_polling(true);
129 glfw.set_swap_interval(glfw::SwapInterval::None);
130
131 let scene = MjvScene::new(model, model.ffi().ngeom as usize + scene_max_geom + MJ_VIEWER_EXTRA_SCENE_GEOM_SPACE);
132 let user_scn = MjvScene::new(model, scene_max_geom);
133 let context= MjrContext::new(model);
134 let camera = MjvCamera::new(0, MjtCamera::mjCAMERA_FREE, model);
135 let pert = MjvPerturb::default();
136 Ok(Self {
137 scene,
138 context,
139 camera,
140 model,
141 pert,
142 glfw,
143 window,
144 events,
145 user_scn,
146 last_x: 0.0,
147 last_y: 0.0,
148 status_flags: ViewerStatusBits::HELP_MENU,
149 last_bnt_press_time: Instant::now(),
150 rect_view: MjrRectangle::default(),
151 rect_full: MjrRectangle::default(),
152 })
153 }
154
155 pub fn running(&self) -> bool {
157 !self.window.should_close()
158 }
159
160 pub fn user_scn(&self) -> &MjvScene<'m>{
162 &self.user_scn
163 }
164
165 pub fn user_scn_mut(&mut self) -> &mut MjvScene<'m>{
167 &mut self.user_scn
168 }
169
170 pub fn sync(&mut self, data: &mut MjData) {
173 self.process_events(data);
175
176 self.update_scene(data);
178
179 self.update_menus();
181
182 self.window.swap_buffers();
184
185 self.pert.apply(self.model, data);
187 }
188
189 fn update_scene(&mut self, data: &mut MjData) {
191 self.update_rectangles(self.window.get_framebuffer_size());
193
194 let opt = MjvOption::default();
196 self.scene.update(data, &opt, &self.pert, &mut self.camera);
197
198 let ffi = unsafe { self.scene.ffi_mut() };
200 assert!(
201 ffi.ngeom + self.user_scn.ffi().ngeom <= ffi.maxgeom,
202 "not enough space available in the internal scene; this is a bug, please report it."
203 );
204
205 unsafe { std::ptr::copy_nonoverlapping(
207 self.user_scn.ffi_mut().geoms,
208 ffi.geoms.add(ffi.ngeom as usize),
209 self.user_scn.ffi().ngeom as usize
210 ) };
211 ffi.ngeom += self.user_scn.ffi().ngeom;
212 self.scene.render(&self.rect_full, &self.context);
213 }
214
215 fn update_menus(&mut self) {
217 let mut rectangle = self.rect_view;
218 rectangle.width = rectangle.width - rectangle.width / 4;
219
220 if self.status_flags.contains(ViewerStatusBits::HELP_MENU) { self.context.overlay(
224 MjtFont::mjFONT_NORMAL, MjtGridPos::mjGRID_TOPLEFT,
225 rectangle,
226 HELP_MENU_TITLES,
227 Some(HELP_MENU_VALUES)
228 );
229 }
230 }
231
232 fn update_rectangles(&mut self, viewport_size: (i32, i32)) {
235 self.rect_view.width = viewport_size.0;
237 self.rect_view.height = viewport_size.1;
238
239 self.rect_full.width = viewport_size.0;
240 self.rect_full.height = viewport_size.1;
241 }
242
243 fn process_events(&mut self, data: &mut MjData) {
245 self.glfw.poll_events();
246 while let Some((_, event)) = self.events.receive() {
247 match event {
248 WindowEvent::Key(Key::Q, _, Action::Press, Modifiers::Control) => {
249 self.window.set_should_close(true);
250 break; },
252 WindowEvent::Key(Key::Escape, _, Action::Press, _) => {
253 self.camera.free();
254 },
255
256 WindowEvent::Key(Key::F1, _, Action::Press, _) => {
257 self.status_flags.toggle(ViewerStatusBits::HELP_MENU);
258 }
259
260 WindowEvent::Scroll(_, change) => {
261 self.process_scroll(change);
262 }
263 WindowEvent::CursorPos(x, y) => {
264 self.process_cursor_pos(x, y, data);
265 },
266
267 WindowEvent::MouseButton(MouseButton::Left, action, modifiers) => {
269 self.process_left_click(data, &action, &modifiers);
270 }
271 _ => {} }
273 }
274 }
275
276 fn process_scroll(&mut self, change: f64) {
278 self.camera.move_(MjtMouse::mjMOUSE_ZOOM, self.model, 0.0, -0.05 * change, &self.scene);
279 }
280
281 fn process_cursor_pos(&mut self, x: f64, y: f64, data: &mut MjData) {
283 let dx = x - self.last_x;
285 let dy = y - self.last_y;
286 self.last_x = x;
287 self.last_y = y;
288
289 let action;
291 let height = self.window.get_size().1 as mjtNum;
292
293 let shift = self.window.get_key(Key::LeftShift) == Action::Press;
294
295 if self.status_flags.contains(ViewerStatusBits::LEFT_CLICK) {
296 if self.pert.active == MjtPertBit::mjPERT_TRANSLATE as i32 {
297 action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
298 }
299 else {
300 action = if shift {MjtMouse::mjMOUSE_ROTATE_H} else {MjtMouse::mjMOUSE_ROTATE_V};
301 }
302 }
303 else if self.window.get_mouse_button(MouseButton::Right) == Action::Press {
304 action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
305 }
306 else if self.window.get_mouse_button(MouseButton::Middle) == Action::Press {
307 action = MjtMouse::mjMOUSE_ZOOM;
308 }
309 else {
310 return; }
312
313 if self.pert.active == 0 {
315 self.camera.move_(action, self.model, dx / height, dy / height, &self.scene);
316 }
317 else { self.pert.move_(self.model, data, action, dx / height, dy / height, &self.scene);
319 }
320 }
321
322 fn process_left_click(&mut self, data: &mut MjData, action: &Action, modifiers: &Modifiers) {
324 match action {
325 Action::Press => {
326 if self.pert.select > 0 && modifiers.contains(Modifiers::Control) {
328 let type_ = if modifiers.contains(Modifiers::Alt) {
329 MjtPertBit::mjPERT_TRANSLATE
330 } else {
331 MjtPertBit::mjPERT_ROTATE
332 };
333 self.pert.start(type_, &self.model, data, &self.scene);
334 }
335
336 if !self.status_flags.contains(ViewerStatusBits::LEFT_CLICK) && self.last_bnt_press_time.elapsed().as_millis() < DOUBLE_CLICK_WINDOW_MS {
338 let (mut x, mut y) = self.window.get_cursor_pos();
339
340 let buffer_ratio = self.window.get_framebuffer_size().0 as mjtNum / self.window.get_size().0 as mjtNum;
342 x *= buffer_ratio;
343 y *= buffer_ratio;
344 y = self.rect_full.height as mjtNum - y; let rect: &mjrRect_ = &self.rect_view;
348 let (body_id, _, flex_id, skin_id, xyz) = self.scene.find_selection(
349 data, &MjvOption::default(),
350 rect.width as mjtNum / rect.height as mjtNum,
351 (x - rect.left as mjtNum) / rect.width as mjtNum,
352 (y - rect.bottom as mjtNum) / rect.height as mjtNum
353 );
354
355 if modifiers.contains(Modifiers::Alt) {
357 if body_id >= 0 {
358 self.camera.lookat = xyz;
359 if modifiers.contains(Modifiers::Control) {
360 self.camera.track(body_id as u32);
361 }
362 }
363 }
364 else {
365 if body_id >= 0 {
367 self.pert.select = body_id;
368 self.pert.flexselect = flex_id;
369 self.pert.skinselect = skin_id;
370 self.pert.active = 0;
371 self.pert.update_local_pos(xyz, data);
372 }
373 else {
374 self.pert.select = 0;
375 self.pert.flexselect = -1;
376 self.pert.skinselect = -1;
377 }
378 }
379 }
380 self.last_bnt_press_time = Instant::now();
381 self.status_flags.set(ViewerStatusBits::LEFT_CLICK, true);
382 },
383 Action::Release => {
384 self.pert.active = 0;
386 self.status_flags.remove(ViewerStatusBits::LEFT_CLICK);
387 },
388 Action::Repeat => {}
389 };
390 }
391}
392
393
394
395bitflags! {
396 #[derive(Debug)]
399 struct ViewerStatusBits: u8 {
400 const LEFT_CLICK = 1 << 0;
401 const HELP_MENU = 1 << 1;
402 }
403}
404
405#[cfg(feature = "cpp-viewer")]
410pub struct MjViewerCpp<'m> {
411 sim: *mut mujoco_Simulate,
412 running: bool,
413
414 _cam: Box<MjvCamera>,
417 _opt: Box<MjvOption>,
418 _pert: Box<MjvPerturb>,
419 _user_scn: Box<MjvScene<'m>>,
420 _glfw: glfw::Glfw
421}
422
423#[cfg(feature = "cpp-viewer")]
424impl<'m> MjViewerCpp<'m> {
425 #[inline]
426 pub fn running(&self) -> bool {
427 self.running
428 }
429
430 #[inline]
431 pub fn user_scn_mut(&mut self) -> &mut MjvScene<'m> {
432 &mut self._user_scn
433 }
434
435 pub fn launch_passive(model: &'m MjModel, data: &MjData, scene_max_ngeom: usize) -> Self {
436 let mut _glfw = glfw::init(glfw::fail_on_errors).unwrap();
437
438 let mut _cam = Box::new(MjvCamera::default());
440 let mut _opt: Box<MjvOption> = Box::new(MjvOption::default());
441 let mut _pert = Box::new(MjvPerturb::default());
442 let mut _user_scn = Box::new(MjvScene::new(&model, scene_max_ngeom));
443 let sim;
444 let c_filename = CString::new("file.xml").unwrap();
445 unsafe {
446 sim = new_simulate(&mut *_cam, &mut *_opt, &mut *_pert, _user_scn.ffi_mut(), true);
447 (*sim).RenderInit();
448 (*sim).Load(model.__raw(), data.__raw(), c_filename.as_ptr());
449 (*sim).RenderStep(true);
450 }
451
452 Self {sim, running: true, _cam, _opt, _pert, _glfw, _user_scn}
453 }
454
455 pub fn __raw(&self) -> *mut mujoco_Simulate {
457 self.sim
458 }
459
460 pub fn render(&mut self, update_timer: bool) {
467 unsafe {
468 assert!(self.running, "render called after viewer has been closed!");
469 self.running = (*self.sim).RenderStep(update_timer);
470 }
471 }
472
473 pub fn sync(&mut self) {
474 unsafe {
475 (*self.sim).Sync(false);
476 }
477 }
478}
479
480#[cfg(feature = "cpp-viewer")]
481impl Drop for MjViewerCpp<'_> {
482 fn drop(&mut self) {
483 unsafe {
484 (*self.sim).RenderCleanup();
485 free_simulate(self.sim);
486 }
487 }
488}