1#[cfg(feature = "viewer-ui")] use glutin::display::GetGlDisplay;
4use glutin::prelude::PossiblyCurrentGlContext;
5use glutin::surface::GlSurface;
6
7use winit::event::{ElementState, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, WindowEvent};
8use winit::platform::pump_events::EventLoopExtPumpEvents;
9use winit::keyboard::{KeyCode, PhysicalKey};
10use winit::event_loop::EventLoop;
11use winit::dpi::PhysicalPosition;
12use winit::window::Fullscreen;
13
14use std::time::{Duration, Instant};
15use std::error::Error;
16use std::fmt::Display;
17use std::ops::Deref;
18
19use bitflags::bitflags;
20
21use crate::prelude::{MjrContext, MjrRectangle, MjtFont, MjtGridPos};
22use crate::render_base::{GlState, RenderBase, sync_geoms};
23use crate::wrappers::mj_primitive::MjtNum;
24use crate::wrappers::mj_visualization::*;
25use crate::wrappers::mj_model::MjModel;
26use crate::wrappers::mj_data::MjData;
27use crate::get_mujoco_version;
28
29
30#[cfg(feature = "viewer-ui")]
31mod ui;
32
33
34const MJ_VIEWER_DEFAULT_SIZE_PX: (u32, u32) = (1280, 720);
38const DOUBLE_CLICK_WINDOW_MS: u128 = 250;
39const TOUCH_BAR_ZOOM_FACTOR: f64 = 0.1;
40
41pub(crate) const EXTRA_SCENE_GEOM_SPACE: usize = 2000;
43
44const HELP_MENU_TITLES: &str = concat!(
45 "Toggle help\n",
46 "Toggle full screen\n",
47 "Free camera\n",
48 "Track camera\n",
49 "Camera orbit\n",
50 "Camera pan\n",
51 "Camera look at\n",
52 "Zoom\n",
53 "Object select\n",
54 "Selection rotate\n",
55 "Selection translate\n",
56 "Exit\n",
57 "Reset simulation\n",
58 "Cycle cameras\n",
59 "Visualization toggles",
60);
61
62const HELP_MENU_VALUES: &str = concat!(
63 "F1\n",
64 "F5\n",
65 "Escape\n",
66 "Control + Alt + double-left click\n",
67 "Left drag\n",
68 "Right [+Shift] drag\n",
69 "Alt + double-left click\n",
70 "Zoom, middle drag\n",
71 "Double-left click\n",
72 "Control + [Shift] + drag\n",
73 "Control + Alt + [Shift] + drag\n",
74 "Control + Q\n",
75 "Backspace\n",
76 "[ ]\n",
77 "See MjViewer docs"
78);
79
80#[derive(Debug)]
81pub enum MjViewerError {
82 EventLoopError(winit::error::EventLoopError),
83 GlutinError(glutin::error::Error)
84}
85
86impl Display for MjViewerError {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 Self::EventLoopError(e) => write!(f, "failed to initialize event_loop: {}", e),
90 Self::GlutinError(e) => write!(f, "glutin raised an error: {}", e)
91 }
92 }
93}
94
95impl Error for MjViewerError {
96 fn source(&self) -> Option<&(dyn Error + 'static)> {
97 match self {
98 Self::EventLoopError(e) => Some(e),
99 Self::GlutinError(e) => Some(e)
100 }
101 }
102}
103
104#[derive(Debug)]
127pub struct MjViewer<M: Deref<Target = MjModel> + Clone> {
128 scene: MjvScene<M>,
130 context: MjrContext,
131 camera: MjvCamera,
132
133 model: M,
135 pert: MjvPerturb,
136 opt: MjvOption,
137
138 last_x: f64,
140 last_y: f64,
141 last_bnt_press_time: Instant,
142 rect_view: MjrRectangle,
143 rect_full: MjrRectangle,
144
145 adapter: RenderBase,
147 event_loop: EventLoop<()>,
148 modifiers: Modifiers,
149 buttons_pressed: ButtonsPressed,
150 raw_cursor_position: (f64, f64),
151
152 user_scene: MjvScene<M>,
154
155 #[cfg(feature = "viewer-ui")]
157 ui: ui::ViewerUI<M>,
158
159 status: ViewerStatusBit
160}
161
162impl<M: Deref<Target = MjModel> + Clone> MjViewer<M> {
163 pub fn launch_passive(model: M, max_user_geom: usize) -> Result<Self, MjViewerError> {
168 let (w, h) = MJ_VIEWER_DEFAULT_SIZE_PX;
169 let mut event_loop = EventLoop::new().map_err(MjViewerError::EventLoopError)?;
170 let adapter = RenderBase::new(
171 w, h,
172 format!("MuJoCo Rust Viewer (MuJoCo {})", get_mujoco_version()),
173 &mut event_loop,
174 true );
176
177 let GlState {
179 gl_context,
180 gl_surface,
181 #[cfg(feature = "viewer-ui")] window,
182 ..
183 } = adapter.state.as_ref().unwrap();
184 gl_context.make_current(gl_surface).map_err(MjViewerError::GlutinError)?;
185 gl_surface.set_swap_interval(gl_context, glutin::surface::SwapInterval::DontWait).map_err(
186 |e| MjViewerError::GlutinError(e)
187 )?;
188 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
189
190 let ngeom = model.ffi().ngeom as usize;
191 let scene = MjvScene::new(model.clone(), ngeom + max_user_geom + EXTRA_SCENE_GEOM_SPACE);
192 let user_scene = MjvScene::new(model.clone(), max_user_geom);
193 let context = MjrContext::new(&model);
194 let camera = MjvCamera::new_free(&model);
195
196 #[cfg(feature = "viewer-ui")]
197 let ui = ui::ViewerUI::new(model.clone(), &window, &gl_surface.display());
198
199 Ok(Self {
200 model,
201 scene,
202 context,
203 camera,
204 pert: MjvPerturb::default(),
205 opt: MjvOption::default(),
206 user_scene,
207 last_x: 0.0,
208 last_y: 0.0,
209 last_bnt_press_time: Instant::now(),
210 rect_view: MjrRectangle::default(),
211 rect_full: MjrRectangle::default(),
212 adapter,
213 event_loop,
214 modifiers: Modifiers::default(),
215 buttons_pressed: ButtonsPressed::empty(),
216 raw_cursor_position: (0.0, 0.0),
217 #[cfg(feature = "viewer-ui")] ui,
218 #[cfg(feature = "viewer-ui")] status: ViewerStatusBit::UI,
219 #[cfg(not(feature = "viewer-ui"))] status: ViewerStatusBit::HELP,
220 })
221 }
222
223 pub fn running(&self) -> bool {
225 self.adapter.running
226 }
227
228 pub fn user_scene(&self) -> &MjvScene<M>{
231 &self.user_scene
232 }
233
234 pub fn user_scene_mut(&mut self) -> &mut MjvScene<M>{
237 &mut self.user_scene
238 }
239
240 #[deprecated(since = "1.3.0", note = "use user_scene")]
241 pub fn user_scn(&self) -> &MjvScene<M> {
242 self.user_scene()
243 }
244
245 #[deprecated(since = "1.3.0", note = "use user_scene_mut")]
246 pub fn user_scn_mut(&mut self) -> &mut MjvScene<M> {
247 self.user_scene_mut()
248 }
249
250 pub fn sync(&mut self, data: &mut MjData<M>) {
253 let GlState {
254 gl_context,
255 gl_surface,
256 ..
257 } = self.adapter.state.as_mut().unwrap();
258
259 gl_context.make_current(gl_surface).expect("could not make OpenGL context current");
261
262 self.update_rectangles(self.adapter.state.as_ref().unwrap().window.inner_size().into());
264
265 self.process_events(data);
267
268
269 self.update_scene(data);
271
272 #[cfg(feature = "viewer-ui")]
274 self.process_user_ui(data);
275
276 self.update_menus();
278
279 self.render();
281
282 self.pert.apply(&self.model, data);
284 }
285
286 fn render(&mut self) {
288 let GlState {
290 gl_context,
291 gl_surface, ..
292 } = self.adapter.state.as_mut().unwrap();
293
294 gl_surface.swap_buffers(gl_context).expect("buffer swap in OpenGL failed");
295 }
296
297 fn update_scene(&mut self, data: &mut MjData<M>) {
299 let model_data_ptr = unsafe { data.model().__raw() };
300 let bound_model_ptr = unsafe { self.model.__raw() };
301 assert_eq!(model_data_ptr, bound_model_ptr, "'data' must be created from the same model as the viewer.");
302
303 self.scene.update(data, &self.opt, &self.pert, &mut self.camera);
305
306 sync_geoms(&self.user_scene, &mut self.scene)
308 .expect("could not sync the user scene with the internal scene; this is a bug, please report it.");
309
310 self.scene.render(&self.rect_full, &self.context);
311 }
312
313 fn update_menus(&mut self) {
315 let mut rectangle = self.rect_view;
316 rectangle.width = rectangle.width - rectangle.width / 4;
317
318 if self.status.contains(ViewerStatusBit::HELP) { self.context.overlay(
321 MjtFont::mjFONT_NORMAL, MjtGridPos::mjGRID_TOPLEFT,
322 rectangle,
323 HELP_MENU_TITLES,
324 Some(HELP_MENU_VALUES)
325 );
326 }
327 }
328
329
330 #[cfg(feature = "viewer-ui")]
332 fn process_user_ui(&mut self, data: &mut MjData<M>) {
333 use crate::viewer::ui::UiEvent;
336 let GlState { window, .. } = &self.adapter.state.as_ref().unwrap();
337 let inner_size = window.inner_size();
338 let left = self.ui.process(
339 window, &mut self.status,
340 &mut self.scene, &mut self.opt,
341 &mut self.camera, data
342 );
343
344 self.rect_view.left = left as i32;
346 self.rect_view.width = inner_size.width as i32;
347
348 self.ui.reset();
350
351 while let Some(event) = self.ui.drain_events() {
353 use UiEvent::*;
354 match event {
355 Close => self.adapter.running = false,
356 Fullscreen => self.toggle_full_screen(),
357 ResetSimulation => {
358 data.reset();
359 data.forward();
360 },
361 AlignCamera => {
362 self.camera = MjvCamera::new_free(&self.model);
363 }
364 }
365 }
366 }
367
368 fn update_rectangles(&mut self, viewport_size: (i32, i32)) {
371 self.rect_view.width = viewport_size.0;
373 self.rect_view.height = viewport_size.1;
374
375 self.rect_full.width = viewport_size.0;
376 self.rect_full.height = viewport_size.1;
377 }
378
379 fn process_events(&mut self, data: &mut MjData<M>) {
381 self.event_loop.pump_app_events(Some(Duration::ZERO), &mut self.adapter);
382 while let Some(window_event) = self.adapter.queue.pop_front() {
383 #[cfg(feature = "viewer-ui")]
384 {
385 let window: &winit::window::Window = &self.adapter.state.as_ref().unwrap().window;
386 self.ui.handle_events(window, &window_event);
387 }
388
389 match window_event {
390 WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers,
391 WindowEvent::MouseInput {state, button, .. } => {
392 let is_pressed = state == ElementState::Pressed;
393
394 #[cfg(feature = "viewer-ui")]
395 if self.ui.covered() && is_pressed {
396 continue;
397 }
398
399 let index = match button {
400 MouseButton::Left => {
401 self.process_left_click(data, state);
402 ButtonsPressed::LEFT
403 },
404 MouseButton::Middle => ButtonsPressed::MIDDLE,
405 MouseButton::Right => ButtonsPressed::RIGHT,
406 _ => return
407 };
408
409 self.buttons_pressed.set(index, is_pressed);
410 }
411
412 WindowEvent::CursorMoved { position, .. } => {
413 let PhysicalPosition { x, y } = position;
414
415 #[cfg(feature = "viewer-ui")]
419 if self.ui.dragged() {
420 continue;
421 }
422
423 self.process_cursor_pos(x, y, data);
424 }
425
426 WindowEvent::KeyboardInput {
428 event: KeyEvent {
429 physical_key: PhysicalKey::Code(KeyCode::KeyQ),
430 state: ElementState::Pressed, ..
431 }, ..
432 } if self.modifiers.state().control_key() => {
433 self.adapter.running = false;
434 }
435
436 WindowEvent::KeyboardInput {
438 event: KeyEvent {
439 physical_key: PhysicalKey::Code(KeyCode::Escape),
440 state: ElementState::Pressed, ..
441 }, ..
442 } => {
443 #[cfg(feature = "viewer-ui")]
444 if self.ui.focused() {
445 continue;
446 }
447 self.camera.free();
448 }
449
450 WindowEvent::KeyboardInput {
452 event: KeyEvent {
453 physical_key: PhysicalKey::Code(KeyCode::F1),
454 state: ElementState::Pressed, ..
455 }, ..
456 } => {
457 self.status.toggle(ViewerStatusBit::HELP);
458 }
459
460 WindowEvent::KeyboardInput {
462 event: KeyEvent {
463 physical_key: PhysicalKey::Code(KeyCode::F5),
464 state: ElementState::Pressed, ..
465 }, ..
466 } => {
467 self.toggle_full_screen();
468 }
469
470 WindowEvent::KeyboardInput {
472 event: KeyEvent {
473 physical_key: PhysicalKey::Code(KeyCode::Backspace),
474 state: ElementState::Pressed, ..
475 }, ..
476 } => {
477 #[cfg(feature = "viewer-ui")]
478 if self.ui.focused() {
479 continue;
480 }
481
482 data.reset();
483 data.forward();
484 }
485
486 WindowEvent::KeyboardInput {
488 event: KeyEvent {
489 physical_key: PhysicalKey::Code(KeyCode::BracketRight),
490 state: ElementState::Pressed, ..
491 }, ..
492 } => {
493 self.cycle_camera(1);
494 }
495
496 WindowEvent::KeyboardInput {
498 event: KeyEvent {
499 physical_key: PhysicalKey::Code(KeyCode::BracketLeft),
500 state: ElementState::Pressed, ..
501 }, ..
502 } => {
503 self.cycle_camera(-1);
504 }
505
506 WindowEvent::KeyboardInput {
508 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyC), state: ElementState::Pressed, ..},
509 ..
510 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_CAMERA),
511
512 WindowEvent::KeyboardInput {
513 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyU), state: ElementState::Pressed, ..},
514 ..
515 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_ACTUATOR),
516
517 WindowEvent::KeyboardInput {
518 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyJ), state: ElementState::Pressed, ..},
519 ..
520 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_JOINT),
521
522 WindowEvent::KeyboardInput {
523 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyM), state: ElementState::Pressed, ..},
524 ..
525 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_COM),
526
527 WindowEvent::KeyboardInput {
528 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyH), state: ElementState::Pressed, ..},
529 ..
530 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_CONVEXHULL),
531
532 WindowEvent::KeyboardInput {
533 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyZ), state: ElementState::Pressed, ..},
534 ..
535 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_LIGHT),
536
537 WindowEvent::KeyboardInput {
538 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyT), state: ElementState::Pressed, ..},
539 ..
540 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_TRANSPARENT),
541
542 WindowEvent::KeyboardInput {
543 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyI), state: ElementState::Pressed, ..},
544 ..
545 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_INERTIA),
546
547 WindowEvent::KeyboardInput {
548 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyE), state: ElementState::Pressed, ..},
549 ..
550 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_CONSTRAINT),
551
552 #[cfg(feature = "viewer-ui")]
553 WindowEvent::KeyboardInput {
554 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyX), state: ElementState::Pressed, ..},
555 ..
556 } => self.status.toggle(ViewerStatusBit::UI),
557
558 WindowEvent::MouseWheel {delta, ..} => {
560 #[cfg(feature = "viewer-ui")]
561 if self.ui.covered() {
562 continue;
563 }
564
565 let value = match delta {
566 MouseScrollDelta::LineDelta(_, down) => down as f64,
567 MouseScrollDelta::PixelDelta(PhysicalPosition {y, ..}) => y * TOUCH_BAR_ZOOM_FACTOR
568 };
569 self.process_scroll(value);
570 }
571
572 _ => {} }
574 }
575 }
576
577 fn toggle_opt_flag(&mut self, flag: MjtVisFlag) {
579 let index = flag as usize;
580 self.opt.flags[index] = 1 - self.opt.flags[index];
581 }
582
583 fn cycle_camera(&mut self, direction: i32) {
585 let n_cam = self.model.ffi().ncam;
586 if n_cam == 0 { return;
588 }
589
590 self.camera.fix((self.camera.fixedcamid + direction).rem_euclid(n_cam) as u32);
591 }
592
593 fn toggle_full_screen(&mut self) {
595 let window = &self.adapter.state.as_ref().unwrap().window;
596 if window.fullscreen().is_some() {
597 window.set_fullscreen(None);
598 }
599 else {
600 window.set_fullscreen(Some(Fullscreen::Borderless(None)));
601 }
602 }
603
604 fn process_scroll(&mut self, change: f64) {
606 self.camera.move_(MjtMouse::mjMOUSE_ZOOM, &self.model, 0.0, -0.05 * change, &self.scene);
607 }
608
609 fn process_cursor_pos(&mut self, x: f64, y: f64, data: &mut MjData<M>) {
611 self.raw_cursor_position = (x, y);
612 let dx = x - self.last_x;
614 let dy = y - self.last_y;
615 self.last_x = x;
616 self.last_y = y;
617 let window = &self.adapter.state.as_ref().unwrap().window;
618 let modifiers = &self.modifiers.state();
619 let buttons = &self.buttons_pressed;
620 let shift = modifiers.shift_key();
621
622 let action;
624 let height = window.outer_size().height as f64;
625
626 if buttons.contains(ButtonsPressed::LEFT) {
627 if self.pert.active == MjtPertBit::mjPERT_TRANSLATE as i32 {
628 action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
629 }
630 else {
631 action = if shift {MjtMouse::mjMOUSE_ROTATE_H} else {MjtMouse::mjMOUSE_ROTATE_V};
632 }
633 }
634 else if buttons.contains(ButtonsPressed::RIGHT) {
635 action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
636 }
637 else if buttons.contains(ButtonsPressed::MIDDLE) {
638 action = MjtMouse::mjMOUSE_ZOOM;
639 }
640 else {
641 return; }
643
644 if self.pert.active == 0 {
646 self.camera.move_(action, &self.model, dx / height, dy / height, &self.scene);
647 }
648 else { self.pert.move_(&self.model, data, action, dx / height, dy / height, &self.scene);
650 }
651 }
652
653 fn process_left_click(&mut self, data: &mut MjData<M>, state: ElementState) {
655 let modifier_state = self.modifiers.state();
656 match state {
657 ElementState::Pressed => {
658 if self.pert.select > 0 && modifier_state.control_key() {
660 let type_ = if modifier_state.alt_key() {
661 MjtPertBit::mjPERT_TRANSLATE
662 } else {
663 MjtPertBit::mjPERT_ROTATE
664 };
665 self.pert.start(type_, &self.model, data, &self.scene);
666 }
667
668 if self.last_bnt_press_time.elapsed().as_millis() < DOUBLE_CLICK_WINDOW_MS {
670 let cp = self.raw_cursor_position;
671 let x = cp.0;
672 let y = self.rect_full.height as f64 - cp.1;
673
674 let rect = &self.rect_full;
676 let (body_id, _, flex_id, skin_id, xyz) = self.scene.find_selection(
677 data, &self.opt,
678 rect.width as MjtNum / rect.height as MjtNum,
679 (x - rect.left as MjtNum) / rect.width as MjtNum,
680 (y - rect.bottom as MjtNum) / rect.height as MjtNum
681 );
682
683 if modifier_state.alt_key() {
685 if body_id >= 0 {
686 self.camera.lookat = xyz;
687 if modifier_state.control_key() {
688 self.camera.track(body_id as u32);
689 }
690 }
691 }
692 else {
693 if body_id >= 0 {
695 self.pert.select = body_id;
696 self.pert.flexselect = flex_id;
697 self.pert.skinselect = skin_id;
698 self.pert.active = 0;
699 self.pert.update_local_pos(xyz, data);
700 }
701 else {
702 self.pert.select = 0;
703 self.pert.flexselect = -1;
704 self.pert.skinselect = -1;
705 }
706 }
707 }
708 self.last_bnt_press_time = Instant::now();
709 },
710 ElementState::Released => {
711 self.pert.active = 0;
713 },
714 };
715 }
716}
717
718bitflags! {
719 #[derive(Debug)]
720 struct ViewerStatusBit: u8 {
721 const HELP = 1 << 0;
722 #[cfg(feature = "viewer-ui")]
723 const UI = 1 << 1;
724 }
725}
726
727bitflags! {
728 #[derive(Debug)]
730 struct ButtonsPressed: u8 {
731 const LEFT = 1 << 0;
732 const MIDDLE = 1 << 1;
733 const RIGHT = 1 << 2;
734 }
735}