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::sync::{Arc, Mutex, MutexGuard, PoisonError};
15use std::time::{Duration, Instant};
16use std::ops::{Deref, DerefMut};
17use std::marker::PhantomData;
18use std::num::NonZero;
19use std::error::Error;
20use std::fmt::Display;
21use std::borrow::Cow;
22
23use bitflags::bitflags;
24
25use crate::prelude::{MjrContext, MjrRectangle, MjtFont, MjtGridPos};
26use crate::winit_gl_base::{RenderBaseGlState, RenderBase};
27use crate::wrappers::mj_data::{MjData, MjtState};
28use crate::{builder_setters, get_mujoco_version};
29use crate::wrappers::mj_primitive::MjtNum;
30use crate::wrappers::mj_visualization::*;
31use crate::wrappers::mj_model::MjModel;
32use crate::vis_common::sync_geoms;
33
34
35#[cfg(feature = "viewer-ui")]
36mod ui;
37
38#[cfg(feature = "viewer-ui")]
40pub use egui;
41
42
43const MJ_VIEWER_DEFAULT_SIZE_PX: (u32, u32) = (1280, 720);
47const DOUBLE_CLICK_WINDOW_MS: u128 = 250;
48const TOUCH_BAR_ZOOM_FACTOR: f64 = 0.1;
49const FPS_SMOOTHING_FACTOR: f64 = 0.1;
50const REALTIME_FACTOR_SMOOTHING_FACTOR: f64 = 0.1;
51const REALTIME_FACTOR_DISPLAY_THRESHOLD: f64 = 0.02;
52
53pub(crate) const EXTRA_SCENE_GEOM_SPACE: usize = 2000;
55
56const HELP_MENU_TITLES: &str = concat!(
57 "Toggle help\n",
58 "Toggle info\n",
59 "Toggle v-sync\n",
60 "Toggle realtime check\n",
61 "Toggle full screen\n",
62 "Free camera\n",
63 "Track camera\n",
64 "Camera orbit\n",
65 "Camera pan\n",
66 "Camera look at\n",
67 "Zoom\n",
68 "Object select\n",
69 "Selection rotate\n",
70 "Selection translate\n",
71 "Exit\n",
72 "Reset simulation\n",
73 "Cycle cameras\n",
74 "Visualization toggles",
75);
76
77const HELP_MENU_VALUES: &str = concat!(
78 "F1\n",
79 "F2\n",
80 "F3\n",
81 "F4\n",
82 "F5\n",
83 "Escape\n",
84 "Control + Alt + double-left click\n",
85 "Left drag\n",
86 "Right [+Shift] drag\n",
87 "Alt + double-left click\n",
88 "Zoom, middle drag\n",
89 "Double-left click\n",
90 "Control + [Shift] + drag\n",
91 "Control + Alt + [Shift] + drag\n",
92 "Control + Q\n",
93 "Backspace\n",
94 "[ ]\n",
95 "See MjViewer docs"
96);
97
98#[derive(Debug)]
99pub enum MjViewerError {
100 EventLoopError(winit::error::EventLoopError),
101 GlutinError(glutin::error::Error)
102}
103
104impl Display for MjViewerError {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match self {
107 Self::EventLoopError(e) => write!(f, "failed to initialize event_loop: {}", e),
108 Self::GlutinError(e) => write!(f, "glutin raised an error: {}", e)
109 }
110 }
111}
112
113impl Error for MjViewerError {
114 fn source(&self) -> Option<&(dyn Error + 'static)> {
115 match self {
116 Self::EventLoopError(e) => Some(e),
117 Self::GlutinError(e) => Some(e)
118 }
119 }
120}
121
122
123#[derive(Debug)]
132pub struct ViewerSharedState<M: Deref<Target = MjModel>>{
133 data_passive_state: Box<[MjtNum]>,
137 data_passive_state_old: Box<[MjtNum]>,
138 data_passive: MjData<M>,
139 pert: MjvPerturb,
140 running: bool,
141 user_scene: MjvScene<M>,
142
143 last_sync_time: Instant,
145 realtime_factor_smooth: f64,
147 data_state_buffer: Box<[MjtNum]>,
149}
150
151impl<M: Deref<Target = MjModel> + Clone> ViewerSharedState<M> {
152 fn new(model: M, max_user_geom: usize) -> Self {
153 let state_size = model.state_size(MjtState::mjSTATE_INTEGRATION as u32) as usize;
155 let data_passive_state = vec![0.0; state_size].into_boxed_slice();
156 let data_passive_state_old = data_passive_state.clone();
157 let data_passive = MjData::new(model.clone());
158 let data_state_buffer = data_passive_state.clone();
159 Self {
160 data_passive_state,
161 data_passive_state_old,
162 data_passive,
163 pert: MjvPerturb::default(),
164 running: true,
165 user_scene: MjvScene::new(model, max_user_geom),
166
167 last_sync_time: Instant::now(),
169 realtime_factor_smooth: 1.0,
170 data_state_buffer
171 }
172 }
173
174 pub fn running(&self) -> bool {
176 self.running
177 }
178
179 pub fn user_scene(&self) -> &MjvScene<M> {
182 &self.user_scene
183 }
184
185 pub fn user_scene_mut(&mut self) -> &mut MjvScene<M> {
188 &mut self.user_scene
189 }
190
191 pub fn sync_data_full(&mut self, data: &mut MjData<M>) {
194 self._sync_data(data, true);
195 }
196
197 pub fn sync_data(&mut self, data: &mut MjData<M>) {
222 self._sync_data(data, false);
223 }
224
225 fn _sync_data(&mut self, data: &mut MjData<M>, full_sync: bool) {
227 let passive_time = self.data_passive.time();
229 let active_time = data.time();
230 if passive_time > 0.0 && active_time > passive_time { let time_elapsed_sim = active_time - passive_time;
232 let elapsed_sync = self.last_sync_time.elapsed();
233 if !elapsed_sync.is_zero() {
234 self.realtime_factor_smooth += REALTIME_FACTOR_SMOOTHING_FACTOR * (
235 time_elapsed_sim / elapsed_sync.as_secs_f64()
236 - self.realtime_factor_smooth
237 );
238 }
239 } else { self.realtime_factor_smooth = 1.0;
241 }
242
243 self.last_sync_time = Instant::now();
244
245 self.data_passive.read_state_into(
247 MjtState::mjSTATE_INTEGRATION as u32,
248 &mut self.data_passive_state
249 );
250 if self.data_passive_state != self.data_passive_state_old {
251 data.read_state_into(MjtState::mjSTATE_INTEGRATION as u32, &mut self.data_state_buffer);
252 for ((new, passive), passive_old) in self.data_state_buffer.iter_mut()
253 .zip(&mut self.data_passive_state)
254 .zip(&mut self.data_passive_state_old)
255 {
256 if *passive_old != *passive {
257 *new = *passive;
258 }
259 }
260
261 data.set_state(&self.data_state_buffer, MjtState::mjSTATE_INTEGRATION as u32);
262 }
263
264 if full_sync {
265 data.copy_to(&mut self.data_passive);
267 } else {
268 data.copy_visual_to(&mut self.data_passive);
270 }
271
272 self.data_passive.read_state_into( MjtState::mjSTATE_INTEGRATION as u32,
277 &mut self.data_passive_state
278 );
279 self.data_passive_state_old.copy_from_slice(&self.data_passive_state);
280
281 self.pert.apply(self.data_passive.model(), data);
283 }
284}
285
286
287#[derive(Debug)]
310pub struct MjViewer<M: Deref<Target = MjModel> + Clone> {
311 scene: MjvScene<M>,
313 context: MjrContext,
314 camera: MjvCamera,
315
316 model: M,
318 opt: MjvOption,
319
320 last_x: f64,
322 last_y: f64,
323 last_bnt_press_time: Instant,
324 rect_view: MjrRectangle,
325 rect_full: MjrRectangle,
326 fps_timer: Instant,
327 fps_smooth: f64,
328
329 adapter: RenderBase,
331 event_loop: EventLoop<()>,
332 modifiers: Modifiers,
333 buttons_pressed: ButtonsPressed,
334 raw_cursor_position: (f64, f64),
335
336 user_scene: MjvScene<M>,
338 shared_state: Arc<Mutex<ViewerSharedState<M>>>,
339
340 #[cfg(feature = "viewer-ui")]
342 ui: ui::ViewerUI<M>,
343
344 status: ViewerStatusBit
345}
346
347impl<M: Deref<Target = MjModel> + Clone> MjViewer<M> {
348 pub fn launch_passive(model: M, max_user_geom: usize) -> Result<Self, MjViewerError> {
356 MjViewerBuilder::new()
357 .max_user_geoms(max_user_geom)
358 .build_passive(model)
359 }
360
361 pub fn builder() -> MjViewerBuilder<M> {
366 MjViewerBuilder::new()
367 }
368
369 pub fn running(&self) -> bool {
371 self.shared_state.lock().unwrap().running()
372 }
373
374 pub fn state(&self) -> &Arc<Mutex<ViewerSharedState<M>>> {
378 &self.shared_state
379 }
380
381 pub fn with_state_lock<F, R>(&self, fun: F) -> Result<R, PoisonError<MutexGuard<'_, ViewerSharedState<M>>>>
401 where F: FnOnce(MutexGuard<ViewerSharedState<M>>) -> R
402 {
403 Ok(fun(self.shared_state.lock()?))
404 }
405
406 #[deprecated(since = "2.2.0", note = "use viewer.state().lock().unwrap().user_scene()")]
419 pub fn user_scene(&self) -> &MjvScene<M>{
420 &self.user_scene
421 }
422
423 #[deprecated(since = "2.2.0", note = "use viewer.state().lock().unwrap().user_scene_mut()")]
436 pub fn user_scene_mut(&mut self) -> &mut MjvScene<M>{
437 &mut self.user_scene
438 }
439
440 #[deprecated(since = "1.3.0", note = "use viewer.state().lock().unwrap().user_scene()")]
441 pub fn user_scn(&self) -> &MjvScene<M> {
442 self.user_scene()
443 }
444
445 #[deprecated(since = "1.3.0", note = "use viewer.state().lock().unwrap().user_scene_mut()")]
446 pub fn user_scn_mut(&mut self) -> &mut MjvScene<M> {
447 self.user_scene_mut()
448 }
449
450 #[cfg(feature = "viewer-ui")]
474 pub fn add_ui_callback<F>(&mut self, callback: F)
475 where
476 F: FnMut(&egui::Context, &mut MjData<M>) + 'static
477 {
478 self.ui.add_ui_callback(callback);
479 }
480
481 #[deprecated(since = "2.2.0", note = "replaced with calls to sync_data and render")]
492 pub fn sync(&mut self, data: &mut MjData<M>) {
493 self.shared_state.lock().unwrap().sync_data(data);
494 self.render();
495 }
496
497 pub fn sync_data_full(&mut self, data: &mut MjData<M>) {
501 self.shared_state.lock().unwrap().sync_data_full(data);
502 }
503
504 pub fn sync_data(&mut self, data: &mut MjData<M>) {
539 self.shared_state.lock().unwrap().sync_data(data);
540 }
541
542 pub fn render(&mut self) {
545 let RenderBaseGlState {
546 gl_context,
547 gl_surface,
548 ..
549 } = self.adapter.state.as_ref().unwrap();
550
551 gl_context.make_current(gl_surface).expect("could not make OpenGL context current");
553
554 self.update_rectangles(self.adapter.state.as_ref().unwrap().window.inner_size().into());
556
557 self.process_events();
559
560 self.update_scene();
562
563 #[cfg(feature = "viewer-ui")]
565 self.process_user_ui();
566
567 self.update_menus();
569
570 self.swap_buffers();
572 }
573
574 fn swap_buffers(&self) {
576 let RenderBaseGlState {
577 gl_context,
578 gl_surface,
579 ..
580 } = self.adapter.state.as_ref().unwrap();
581
582 gl_surface.swap_buffers(gl_context).expect("buffer swap in OpenGL failed");
584 }
585
586 fn update_smooth_fps(&mut self) {
587 let elapsed = self.fps_timer.elapsed();
588
589 let fps = if elapsed.is_zero() {
590 self.fps_smooth
591 } else {
592 1.0 / elapsed.as_secs_f64()
593 };
594
595 self.fps_timer = Instant::now();
596 self.fps_smooth += FPS_SMOOTHING_FACTOR * (fps - self.fps_smooth);
597 }
598
599 fn update_scene(&mut self) {
601 let lock = &mut self.shared_state.lock().unwrap();
603 let ViewerSharedState { data_passive, pert, .. } = lock.deref_mut();
604 self.scene.update(data_passive, &self.opt, pert, &mut self.camera);
605
606 let new_user_scene = lock.user_scene();
609 let old_user_scene = &self.user_scene;
610 if !new_user_scene.geoms().is_empty() && !old_user_scene.geoms().is_empty() {
611 panic!(
612 "Both the new ViewerSharedState::user_scene and the deprecated MjViewer::user_scene are non-empty. \
613 Please update your code to fully use ViewerSharedState::user_scene."
614 );
615 }
616
617 sync_geoms(new_user_scene, &mut self.scene)
619 .expect("could not sync the user scene with the internal scene; this is a bug, please report it.");
620
621 sync_geoms(old_user_scene, &mut self.scene)
623 .expect("could not sync the user scene with the internal scene; this is a bug, please report it.");
624
625 self.scene.render(&self.rect_full, &self.context);
626 }
627
628 fn update_menus(&mut self) {
630 let rectangle_from_ui = self.rect_view;
631 let rectangle_full = self.rect_full;
632
633 if self.status.contains(ViewerStatusBit::HELP) { self.context.overlay(
636 MjtFont::mjFONT_NORMAL, MjtGridPos::mjGRID_TOPLEFT,
637 rectangle_from_ui,
638 HELP_MENU_TITLES,
639 Some(HELP_MENU_VALUES)
640 );
641 }
642
643 let (
645 time,
646 memory_pct,
647 mut total_memory,
648 realtime_factor
649 ) = {
650 let state_lock = self.shared_state.lock().unwrap();
651 let data_lock = &state_lock.data_passive;
652 let memory_total = data_lock.narena().max(1) as f64;
653 (
654 data_lock.time(),
655 100.0 * data_lock.maxuse_arena() as f64 / memory_total, memory_total,
656 state_lock.realtime_factor_smooth
657 )
658 };
659
660 if self.status.contains(ViewerStatusBit::INFO) { self.update_smooth_fps();
662
663 let headers = concat!(
665 "FPS\n",
666 "Time\n",
667 "Memory\n",
668 "Realtime factor"
669 );
670
671 let mut memory_unit = ' ';
673 if total_memory > 1e6 {
674 total_memory /= 1e6;
675 memory_unit = 'M';
676 } else if total_memory > 1e3 {
677 total_memory /= 1e3;
678 memory_unit = 'k';
679 }
680
681 let values = format!(
683 concat!(
684 "{:.1}\n",
685 "{:.1}\n",
686 "{:.1} % out of {:.1} {}\n",
687 "{:.1} %"
688 ),
689 self.fps_smooth,
690 time,
691 memory_pct, total_memory, memory_unit,
692 realtime_factor * 100.0
693 );
694
695 self.context.overlay(
696 MjtFont::mjFONT_NORMAL,
697 MjtGridPos::mjGRID_BOTTOMLEFT,
698 rectangle_from_ui,
699 &headers,
700 Some(&values)
701 );
702 }
703
704 if self.status.contains(ViewerStatusBit::WARN_REALTIME) {
706 if (realtime_factor - 1.0).abs() > REALTIME_FACTOR_DISPLAY_THRESHOLD {
707 self.context.overlay(
708 MjtFont::mjFONT_BIG,
709 MjtGridPos::mjGRID_BOTTOMRIGHT,
710 rectangle_full,
711 &format!("Realtime factor: {:.1} %", realtime_factor * 100.0),
712 None
713 );
714 }
715 }
716 }
717
718 #[cfg(feature = "viewer-ui")]
720 fn process_user_ui(&mut self) {
721 use crate::viewer::ui::UiEvent;
724 let RenderBaseGlState {window, ..} = &self.adapter.state.as_ref().unwrap();
725
726 let inner_size = window.inner_size();
727 let left = self.ui.process(
728 window, &mut self.status,
729 &mut self.scene, &mut self.opt,
730 &mut self.camera, &mut self.shared_state.lock().unwrap().data_passive
731 );
732
733 self.rect_view.left = left as i32;
735 self.rect_view.width = inner_size.width as i32;
736
737 self.ui.reset();
739
740 while let Some(event) = self.ui.drain_events() {
742 use UiEvent::*;
743 match event {
744 Close => self.shared_state.lock().unwrap().running = false,
745 Fullscreen => self.toggle_full_screen(),
746 ResetSimulation => {
747 let mut lock = self.shared_state.lock().unwrap();
748 lock.data_passive.reset();
749 lock.data_passive.forward();
750 },
751 AlignCamera => {
752 self.camera = MjvCamera::new_free(&self.model);
753 },
754 VSyncToggle => {
755 self.update_vsync();
756 }
757 }
758 }
759 }
760
761 fn update_vsync(&self) {
763 let RenderBaseGlState {
764 gl_surface, gl_context, ..
765 } = &self.adapter.state.as_ref().unwrap();
766
767 if self.status.contains(ViewerStatusBit::VSYNC) {
768 gl_surface.set_swap_interval(
769 gl_context, glutin::surface::SwapInterval::Wait(NonZero::new(1).unwrap())
770 ).expect("failed to enable vsync");
771 } else {
772 gl_surface.set_swap_interval(
773 gl_context, glutin::surface::SwapInterval::DontWait
774 ).expect("failed to disable vsync");
775 }
776 }
777
778 fn update_rectangles(&mut self, viewport_size: (i32, i32)) {
781 self.rect_view.width = viewport_size.0;
783 self.rect_view.height = viewport_size.1;
784
785 self.rect_full.width = viewport_size.0;
786 self.rect_full.height = viewport_size.1;
787 }
788
789 fn process_events(&mut self) {
791 self.event_loop.pump_app_events(Some(Duration::ZERO), &mut self.adapter);
792 while let Some(window_event) = self.adapter.queue.pop_front() {
793 #[cfg(feature = "viewer-ui")]
794 {
795 let window: &winit::window::Window = &self.adapter.state.as_ref().unwrap().window;
796 self.ui.handle_events(window, &window_event);
797 }
798
799 match window_event {
800 WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers,
801 WindowEvent::MouseInput {state, button, .. } => {
802 let is_pressed = state == ElementState::Pressed;
803
804 #[cfg(feature = "viewer-ui")]
805 if self.ui.covered() && is_pressed {
806 continue;
807 }
808
809 let index = match button {
810 MouseButton::Left => {
811 self.process_left_click(state);
812 ButtonsPressed::LEFT
813 },
814 MouseButton::Middle => ButtonsPressed::MIDDLE,
815 MouseButton::Right => ButtonsPressed::RIGHT,
816 _ => return
817 };
818
819 self.buttons_pressed.set(index, is_pressed);
820 }
821
822 WindowEvent::CursorMoved { position, .. } => {
823 let PhysicalPosition { x, y } = position;
824
825 #[cfg(feature = "viewer-ui")]
829 if self.ui.dragged() {
830 continue;
831 }
832
833 self.process_cursor_pos(x, y);
834 }
835
836 WindowEvent::KeyboardInput {
838 event: KeyEvent {
839 physical_key: PhysicalKey::Code(KeyCode::KeyQ),
840 state: ElementState::Pressed, ..
841 }, ..
842 } if self.modifiers.state().control_key() => {
843 self.shared_state.lock().unwrap().running = false;
844 }
845
846 WindowEvent::CloseRequested => { self.shared_state.lock().unwrap().running = false }
848
849 WindowEvent::KeyboardInput {
851 event: KeyEvent {
852 physical_key: PhysicalKey::Code(KeyCode::Escape),
853 state: ElementState::Pressed, ..
854 }, ..
855 } => {
856 #[cfg(feature = "viewer-ui")]
857 if self.ui.focused() {
858 continue;
859 }
860 self.camera.free();
861 }
862
863 WindowEvent::KeyboardInput {
865 event: KeyEvent {
866 physical_key: PhysicalKey::Code(KeyCode::F1),
867 state: ElementState::Pressed, ..
868 }, ..
869 } => {
870 self.status.toggle(ViewerStatusBit::HELP);
871 }
872
873 WindowEvent::KeyboardInput {
875 event: KeyEvent {
876 physical_key: PhysicalKey::Code(KeyCode::F2),
877 state: ElementState::Pressed, ..
878 }, ..
879 } => {
880 self.status.toggle(ViewerStatusBit::INFO);
881 }
882
883 WindowEvent::KeyboardInput {
885 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::F3), state: ElementState::Pressed, ..},
886 ..
887 } => {
888 self.status.toggle(ViewerStatusBit::VSYNC);
889 self.update_vsync();
890 }
891
892 WindowEvent::KeyboardInput {
894 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::F4), state: ElementState::Pressed, ..},
895 ..
896 } => {
897 self.status.toggle(ViewerStatusBit::WARN_REALTIME);
898 }
899
900 WindowEvent::KeyboardInput {
902 event: KeyEvent {
903 physical_key: PhysicalKey::Code(KeyCode::F5),
904 state: ElementState::Pressed, ..
905 }, ..
906 } => {
907 self.toggle_full_screen();
908 }
909
910 WindowEvent::KeyboardInput {
912 event: KeyEvent {
913 physical_key: PhysicalKey::Code(KeyCode::Backspace),
914 state: ElementState::Pressed, ..
915 }, ..
916 } => {
917 #[cfg(feature = "viewer-ui")]
918 if self.ui.focused() {
919 continue;
920 }
921 let mut lock = self.shared_state.lock().unwrap();
922 lock.data_passive.reset();
923 lock.data_passive.forward();
924 }
925
926 WindowEvent::KeyboardInput {
928 event: KeyEvent {
929 physical_key: PhysicalKey::Code(KeyCode::BracketRight),
930 state: ElementState::Pressed, ..
931 }, ..
932 } => {
933 self.cycle_camera(1);
934 }
935
936 WindowEvent::KeyboardInput {
938 event: KeyEvent {
939 physical_key: PhysicalKey::Code(KeyCode::BracketLeft),
940 state: ElementState::Pressed, ..
941 }, ..
942 } => {
943 self.cycle_camera(-1);
944 }
945
946 WindowEvent::KeyboardInput {
948 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyC), state: ElementState::Pressed, ..},
949 ..
950 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_CAMERA),
951
952 WindowEvent::KeyboardInput {
953 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyU), state: ElementState::Pressed, ..},
954 ..
955 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_ACTUATOR),
956
957 WindowEvent::KeyboardInput {
958 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyJ), state: ElementState::Pressed, ..},
959 ..
960 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_JOINT),
961
962 WindowEvent::KeyboardInput {
963 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyM), state: ElementState::Pressed, ..},
964 ..
965 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_COM),
966
967 WindowEvent::KeyboardInput {
968 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyH), state: ElementState::Pressed, ..},
969 ..
970 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_CONVEXHULL),
971
972 WindowEvent::KeyboardInput {
973 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyZ), state: ElementState::Pressed, ..},
974 ..
975 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_LIGHT),
976
977 WindowEvent::KeyboardInput {
978 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyT), state: ElementState::Pressed, ..},
979 ..
980 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_TRANSPARENT),
981
982 WindowEvent::KeyboardInput {
983 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyI), state: ElementState::Pressed, ..},
984 ..
985 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_INERTIA),
986
987 WindowEvent::KeyboardInput {
988 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyE), state: ElementState::Pressed, ..},
989 ..
990 } => self.toggle_opt_flag(MjtVisFlag::mjVIS_CONSTRAINT),
991
992 #[cfg(feature = "viewer-ui")]
993 WindowEvent::KeyboardInput {
994 event: KeyEvent {physical_key: PhysicalKey::Code(KeyCode::KeyX), state: ElementState::Pressed, ..},
995 ..
996 } => self.status.toggle(ViewerStatusBit::UI),
997
998 WindowEvent::MouseWheel {delta, ..} => {
1000 #[cfg(feature = "viewer-ui")]
1001 if self.ui.covered() {
1002 continue;
1003 }
1004
1005 let value = match delta {
1006 MouseScrollDelta::LineDelta(_, down) => down as f64,
1007 MouseScrollDelta::PixelDelta(PhysicalPosition {y, ..}) => y * TOUCH_BAR_ZOOM_FACTOR
1008 };
1009 self.process_scroll(value);
1010 }
1011
1012 _ => {} }
1014 }
1015 }
1016
1017 fn toggle_opt_flag(&mut self, flag: MjtVisFlag) {
1019 let index = flag as usize;
1020 self.opt.flags[index] = 1 - self.opt.flags[index];
1021 }
1022
1023 fn cycle_camera(&mut self, direction: i32) {
1025 let n_cam = self.model.ffi().ncam;
1026 if n_cam == 0 { return;
1028 }
1029
1030 self.camera.fix((self.camera.fixedcamid + direction).rem_euclid(n_cam) as u32);
1031 }
1032
1033 fn toggle_full_screen(&mut self) {
1035 let window = &self.adapter.state.as_ref().unwrap().window;
1036 if window.fullscreen().is_some() {
1037 window.set_fullscreen(None);
1038 }
1039 else {
1040 window.set_fullscreen(Some(Fullscreen::Borderless(None)));
1041 }
1042 }
1043
1044 fn process_scroll(&mut self, change: f64) {
1046 self.camera.move_(MjtMouse::mjMOUSE_ZOOM, &self.model, 0.0, -0.05 * change, &self.scene);
1047 }
1048
1049 fn process_cursor_pos(&mut self, x: f64, y: f64) {
1051 self.raw_cursor_position = (x, y);
1052 let dx = x - self.last_x;
1054 let dy = y - self.last_y;
1055 self.last_x = x;
1056 self.last_y = y;
1057 let window = &self.adapter.state.as_ref().unwrap().window;
1058 let modifiers = &self.modifiers.state();
1059 let buttons = &self.buttons_pressed;
1060 let shift = modifiers.shift_key();
1061
1062 let action;
1064 let height = window.outer_size().height as f64;
1065
1066 let mut lock = self.shared_state.lock().unwrap();
1067 let ViewerSharedState {data_passive, pert, ..} = lock.deref_mut();
1068 if buttons.contains(ButtonsPressed::LEFT) {
1069 if pert.active == MjtPertBit::mjPERT_TRANSLATE as i32 {
1070 action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
1071 }
1072 else {
1073 action = if shift {MjtMouse::mjMOUSE_ROTATE_H} else {MjtMouse::mjMOUSE_ROTATE_V};
1074 }
1075 }
1076 else if buttons.contains(ButtonsPressed::RIGHT) {
1077 action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
1078 }
1079 else if buttons.contains(ButtonsPressed::MIDDLE) {
1080 action = MjtMouse::mjMOUSE_ZOOM;
1081 }
1082 else {
1083 return; }
1085
1086 if pert.active == 0 {
1088 self.camera.move_(action, &self.model, dx / height, dy / height, &self.scene);
1089 }
1090 else { pert.move_(&self.model, data_passive, action, dx / height, dy / height, &self.scene);
1092 }
1093 }
1094
1095 fn process_left_click(&mut self, state: ElementState) {
1097 let modifier_state = self.modifiers.state();
1098 let mut lock = self.shared_state.lock().unwrap();
1099 let ViewerSharedState {data_passive, pert, ..} = lock.deref_mut();
1100 match state {
1101 ElementState::Pressed => {
1102 if pert.select > 0 && modifier_state.control_key() {
1104 let type_ = if modifier_state.alt_key() {
1105 MjtPertBit::mjPERT_TRANSLATE
1106 } else {
1107 MjtPertBit::mjPERT_ROTATE
1108 };
1109 pert.start(type_, &self.model, data_passive, &self.scene);
1110 }
1111
1112 if self.last_bnt_press_time.elapsed().as_millis() < DOUBLE_CLICK_WINDOW_MS {
1114 let cp = self.raw_cursor_position;
1115 let x = cp.0;
1116 let y = self.rect_full.height as f64 - cp.1;
1117
1118 let rect = &self.rect_full;
1120 let (body_id, _, flex_id, skin_id, xyz) = self.scene.find_selection(
1121 data_passive, &self.opt,
1122 rect.width as MjtNum / rect.height as MjtNum,
1123 (x - rect.left as MjtNum) / rect.width as MjtNum,
1124 (y - rect.bottom as MjtNum) / rect.height as MjtNum
1125 );
1126
1127 if modifier_state.alt_key() {
1129 if body_id >= 0 {
1130 self.camera.lookat = xyz;
1131 if modifier_state.control_key() {
1132 self.camera.track(body_id as u32);
1133 }
1134 }
1135 }
1136 else {
1137 if body_id >= 0 {
1139 pert.select = body_id;
1140 pert.flexselect = flex_id;
1141 pert.skinselect = skin_id;
1142 pert.active = 0;
1143 pert.update_local_pos(xyz, data_passive);
1144 }
1145 else {
1146 pert.select = 0;
1147 pert.flexselect = -1;
1148 pert.skinselect = -1;
1149 }
1150 }
1151 }
1152 self.last_bnt_press_time = Instant::now();
1153 },
1154 ElementState::Released => {
1155 pert.active = 0;
1157 },
1158 };
1159 }
1160}
1161
1162
1163pub struct MjViewerBuilder<M: Deref<Target = MjModel> + Clone> {
1171 window_name: Cow<'static, str>,
1173 max_user_geoms: usize,
1175 vsync: bool,
1179
1180 warn_non_realtime: bool,
1185
1186 model_type: PhantomData<M>,
1189}
1190
1191impl<M: Deref<Target = MjModel> + Clone> MjViewerBuilder<M> {
1192 builder_setters! {
1193 window_name: S where S: Into<Cow<'static, str>>; "text shown in the title of the window.";
1194 max_user_geoms: usize; "maximum number of geoms that can be drawn by the user in addition to the regular geoms.";
1195 vsync: bool; "enable vertical synchronization by default.";
1196 warn_non_realtime: bool; "enable showing an overlay when the simulation state isn't synced in realtime (deviation larger than 2 %).";
1197 }
1198}
1199
1200impl<M: Deref<Target = MjModel> + Clone> MjViewerBuilder<M> {
1201 pub fn new() -> Self {
1202 Self {
1203 window_name: Cow::Owned(format!("MuJoCo Rust Viewer (MuJoCo {})", get_mujoco_version())),
1204 max_user_geoms: 0, vsync: false, warn_non_realtime: false,
1205 model_type: PhantomData
1206 }
1207 }
1208
1209 pub fn build_passive(&self, model: M) -> Result<MjViewer<M>, MjViewerError> {
1210 let (w, h) = MJ_VIEWER_DEFAULT_SIZE_PX;
1211 let mut event_loop = EventLoop::new().map_err(MjViewerError::EventLoopError)?;
1212 let adapter = RenderBase::new(
1213 w, h,
1214 self.window_name.to_string(),
1215 &mut event_loop,
1216 true );
1218
1219 let RenderBaseGlState {
1221 gl_context,
1222 gl_surface,
1223 #[cfg(feature = "viewer-ui")] window,
1224 ..
1225 } = adapter.state.as_ref().unwrap();
1226 gl_context.make_current(gl_surface).map_err(MjViewerError::GlutinError)?;
1227
1228 if self.vsync {
1230 gl_surface.set_swap_interval(
1231 gl_context,
1232 glutin::surface::SwapInterval::Wait(NonZero::new(1).unwrap())
1233 ).map_err(|e| MjViewerError::GlutinError(e))?;
1234 } else {
1235 gl_surface.set_swap_interval(gl_context, glutin::surface::SwapInterval::DontWait).map_err(
1236 |e| MjViewerError::GlutinError(e)
1237 )?;
1238 }
1239
1240 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
1241
1242 let ngeom = model.ffi().ngeom as usize;
1243 let scene = MjvScene::new(model.clone(), ngeom + self.max_user_geoms + EXTRA_SCENE_GEOM_SPACE);
1244 let context = MjrContext::new(&model);
1245 let camera = MjvCamera::new_free(&model);
1246
1247 let shared_state = Arc::new(Mutex::new(ViewerSharedState::new(model.clone(), self.max_user_geoms)));
1249 let user_scene = MjvScene::new(model.clone(), self.max_user_geoms);
1250
1251 #[cfg(feature = "viewer-ui")]
1253 let ui = ui::ViewerUI::new(model.clone(), &window, &gl_surface.display());
1254 #[cfg(feature = "viewer-ui")]
1255 let mut status = ViewerStatusBit::UI;
1256 #[cfg(not(feature = "viewer-ui"))]
1257 let mut status = ViewerStatusBit::HELP;
1258
1259 status.set(ViewerStatusBit::VSYNC, self.vsync);
1260 status.set(ViewerStatusBit::WARN_REALTIME, self.warn_non_realtime);
1261
1262 Ok(MjViewer {
1263 model,
1264 scene,
1265 context,
1266 camera,
1267 opt: MjvOption::default(),
1268 user_scene, shared_state,
1270 last_x: 0.0,
1271 last_y: 0.0,
1272 last_bnt_press_time: Instant::now(),
1273 fps_timer: Instant::now(),
1274 fps_smooth: 60.0,
1275 rect_view: MjrRectangle::default(),
1276 rect_full: MjrRectangle::default(),
1277 adapter,
1278 event_loop,
1279 modifiers: Modifiers::default(),
1280 buttons_pressed: ButtonsPressed::empty(),
1281 raw_cursor_position: (0.0, 0.0),
1282 #[cfg(feature = "viewer-ui")] ui,
1283 status
1284 })
1285 }
1286}
1287
1288impl<M: Deref<Target = MjModel> + Clone> Default for MjViewerBuilder<M> {
1289 fn default() -> Self {
1290 MjViewerBuilder::new()
1291 }
1292}
1293
1294bitflags! {
1295 #[derive(Debug)]
1296 struct ViewerStatusBit: u8 {
1297 const HELP = 1 << 0;
1298 const VSYNC = 1 << 1;
1299 const INFO = 1 << 2;
1300 const WARN_REALTIME = 1 << 3;
1301 #[cfg(feature = "viewer-ui")] const UI = 1 << 4;
1302 }
1303}
1304
1305bitflags! {
1306 #[derive(Debug)]
1308 struct ButtonsPressed: u8 {
1309 const LEFT = 1 << 0;
1310 const MIDDLE = 1 << 1;
1311 const RIGHT = 1 << 2;
1312 }
1313}