// Copyright 2025 DeepMind Technologies Limited // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "experimental/studio/app.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "experimental/platform/hal/graphics_mode.h" #include "experimental/platform/hal/renderer.h" #include "experimental/platform/hal/window.h" #include "experimental/platform/helpers.h" #include "experimental/platform/sim/model_holder.h" #include "experimental/platform/sim/step_control.h" #include "experimental/platform/ux/file_dialog.h" #include "experimental/platform/ux/gui.h" #include "experimental/platform/ux/gui_spec.h" #include "experimental/platform/ux/imgui_widgets.h" #include "experimental/platform/ux/interaction.h" #include "experimental/platform/ux/picture_gui.h" #include "experimental/platform/ux/plugin.h" namespace mujoco::studio { using PauseState = platform::StepControl::PauseState; static void ToggleFlag(mjtByte& flag) { flag = flag ? 0 : 1; } static void ToggleWindow(bool& window) { window = !window; ImGui::GetIO().WantSaveIniSettings = true; } static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) { if (perturb.select > 0) { perturb.select = model->body_parentid[perturb.select]; perturb.flexselect = -1; perturb.skinselect = -1; if (perturb.select <= 0) { perturb.active = 0; } } // TODO: update selected element! } static constexpr const char* ICON_COPY_CAMERA = platform::ICON_FA_COPY; static constexpr const char* ICON_RELOAD_MODEL = platform::ICON_FA_REFRESH; static constexpr const char* ICON_RESET_MODEL = platform::ICON_FA_UNDO; static constexpr const char* ICON_PREV_FRAME = platform::ICON_FA_CARET_LEFT; static constexpr const char* ICON_NEXT_FRAME = platform::ICON_FA_CARET_RIGHT; static constexpr const char* ICON_CURR_FRAME = platform::ICON_FA_FAST_FORWARD; static constexpr const char* ICON_UNDO_SPEC = platform::ICON_FA_UNDO; static constexpr const char* ICON_REDO_SPEC = platform::ICON_FA_REPEAT; App::App(Config config) : app_title_(std::move(config.title)), ini_path_(std::move(config.ini_path)), gfx_mode_(config.gfx_mode) { SwitchGraphicsMode(config.width, config.height, config.gfx_mode); if (config.initial_theme.has_value()) { ui_.theme = *config.initial_theme; } ImPlot::CreateContext(); mjv_defaultPerturb(&perturb_); mjv_defaultCamera(&camera_); mjv_defaultOption(&vis_options_); profiler_.Clear(); } void App::SwitchGraphicsMode(int width, int height, platform::GraphicsMode mode) { renderer_.reset(); window_.reset(); gfx_mode_ = mode; platform::Window::Config window_config; window_config.gfx_mode = gfx_mode_; window_ = std::make_unique(app_title_, width, height, window_config); renderer_ = std::make_unique( window_->GetNativeWindowHandle(), gfx_mode_); // TODO: Figure out why this breaks on some platforms. // LoadSettings(); if (ui_.window_width > 0 && ui_.window_height > 0) { window_->Resize(ui_.window_width, ui_.window_height); } } void App::Recompile() { mj_recompile(model_holder_->spec(), model_holder_->vfs(), model_holder_->model(), model_holder_->data()); const int state_size = mj_stateSize(model(), mjSTATE_INTEGRATION); sim_history_.Init(state_size); } void App::RequestModelLoad(std::string model_file) { pending_load_ = std::move(model_file); } void App::RequestModelReload() { if (model_kind_ == kModelFromFile || (model_kind_ == kEmptyModel && !model_path_.empty())) { pending_load_ = model_path_; preserve_camera_on_load_ = true; } } void App::InitEmptyModel() { model_holder_ = platform::ModelHolder::FromSpec(mj_makeSpec()); OnModelLoaded("", kEmptyModel); spec_editor_.Reset(*spec()); } void App::LoadModelFromFile(const std::string& filepath) { const std::string resolved_file = platform::ResolveFile(filepath, search_paths_); model_holder_ = platform::ModelHolder::FromFile(resolved_file); if (model_holder_->ok()) { OnModelLoaded(filepath, kModelFromFile); spec_editor_.Reset(*spec()); UpdateFilePaths(resolved_file); if (model() && model()->names) { // Assumes the first string in the model is the name of the model itself. window_->SetTitle(app_title_ + " : " + std::string(model()->names)); } else { window_->SetTitle(app_title_ + " : " + std::filesystem::path(filepath).stem().string()); } } else { SetLoadError(std::string(model_holder_->error())); // Keep track of the attempted load in case the user fixes the error and // tries to reload the same file again. model_path_ = resolved_file; } } void App::LoadModelFromBuffer(std::span buffer, std::string_view content_type, std::string_view filename) { model_holder_ = platform::ModelHolder::FromBuffer(buffer, content_type, filename); if (model_holder_->ok()) { OnModelLoaded(std::string(filename), kModelFromFile); } else { SetLoadError(std::string(model_holder_->error())); } spec_editor_.Reset(*spec()); } void App::OnModelLoaded(std::string filename, ModelKind model_kind) { load_error_ = ""; step_error_ = ""; edit_error_ = ""; model_path_ = std::move(filename); if (model_kind_ == kEmptyModel) { step_control_.SetPauseState(PauseState::kUnpaused); } model_kind_ = model_kind; if (model_kind_ == kEmptyModel) { step_control_.SetPauseState(PauseState::kNormalPaused); } // Reset/reinitialize everything that depends on the new mjModel. mjModel* model = model_holder_->model(); renderer_->Init(model); const int state_size = mj_stateSize(model, mjSTATE_INTEGRATION); sim_history_.Init(state_size); if (!preserve_camera_on_load_) { const int model_cam = model->vis.global.cameraid; if (model_cam >= 0 && model_cam < model->ncam) { ui_.camera_idx = platform::SetCamera(model, &camera_, model_cam); } else { mjv_defaultFreeCamera(model, &camera_); } } preserve_camera_on_load_ = false; // Initialize the speed based on the model's default real-time setting. float min_error = FLT_MAX; const float desired = mju_log(100 * model->vis.global.realtime); for (int i = 0; i < platform::kPercentRealTime.size(); ++i) { const float speed = std::stof(platform::kPercentRealTime[i]); const float error = mju_abs(mju_log(speed) - desired); if (error < min_error) { min_error = error; SetSpeedIndex(i); } } platform::ForEachPlugin([&](auto* plugin) { if (plugin->post_model_loaded) { plugin->post_model_loaded(plugin, model_path_.c_str()); } }); tmp_.update_threadpool = true; } void App::UpdateFilePaths(const std::string& resolved_path) { if (!resolved_path.empty()) { const std::filesystem::path path(resolved_path); const std::string base_path = path.parent_path().string() + "/"; const std::string model_name = path.stem().string(); tmp_.last_path[UiTempState::FileDialog_Load] = base_path + model_name + ".xml"; tmp_.last_path[UiTempState::FileDialog_SaveMjb] = base_path + model_name + "_saved.mjb"; tmp_.last_path[UiTempState::FileDialog_SaveXml] = base_path + model_name + "_saved.xml"; tmp_.last_path[UiTempState::FileDialog_PrintModel] = base_path + model_name + "_MJMODEL.TXT"; tmp_.last_path[UiTempState::FileDialog_PrintData] = base_path + model_name + "_MJDATA.TXT"; tmp_.last_path[UiTempState::FileDialog_SaveScreenshot] = base_path + "screenshot.webp"; } } void App::SetLoadError(std::string error) { InitEmptyModel(); load_error_ = std::move(error); step_error_ = ""; edit_error_ = ""; } void App::ResetPhysics() { mj_resetData(model(), data()); mj_forward(model(), data()); step_error_ = ""; edit_error_ = ""; } void App::UpdatePhysics() { if (!has_model()) { return; } if (tmp_.update_threadpool) { mju_threadpool(data(), ui_.nthread); tmp_.update_threadpool = false; } bool stepped = false; platform::ForEachPlugin([&](auto* plugin) { if (plugin->do_update) { if (plugin->do_update(plugin, model(), data())) { stepped = true; } } }); if (!stepped) { if (step_control_.GetPauseState() != PauseState::kNormalPaused) { mju_zero(data()->xfrc_applied, 6 * model()->nbody); mjv_applyPerturbPose(model(), data(), &perturb_, 0); mjv_applyPerturbForce(model(), data(), &perturb_); } else { mjv_applyPerturbPose(model(), data(), &perturb_, 1); } if (has_data()) { for (int i = 0; i < mjNTIMER; i++) { data()->timer[i].duration = 0; data()->timer[i].number = 0; } } platform::StepControl::Status status = step_control_.Advance(model(), data()); if (status == platform::StepControl::Status::kPaused) { // do nothing } else if (status == platform::StepControl::Status::kOk) { stepped = true; // If we are adding to the history we didn't have a divergence error step_error_ = ""; } else if (status == platform::StepControl::Status::kAutoReset) { ResetPhysics(); } else if (status == platform::StepControl::Status::kDiverged) { stepped = true; for (mjtWarning w : platform::StepControl::kDivergedWarnings) { if (data()->warning[w].number > 0) { step_error_ = mju_warningText(w, data()->warning[w].lastinfo); } } } } if (stepped) { profiler_.Update(model(), data()); std::span state = sim_history_.AddToHistory(); if (!state.empty()) { mj_getState(model(), data(), state.data(), mjSTATE_INTEGRATION); } } } void App::LoadHistory(int offset) { std::span state = sim_history_.SetIndex(offset); if (!state.empty()) { // Pause simulation when entering history mode. step_control_.SetPauseState(PauseState::kNormalPaused); // Load the state into the data buffer. mj_setState(model(), data(), state.data(), mjSTATE_INTEGRATION); mj_forward(model(), data()); } } bool App::Update() { const platform::Window::Status status = window_->NewFrame(); HandleWindowEvents(); HandleMouseEvents(); HandleKeyboardEvents(); ProcessPendingLoads(); // Only update the simulation if a popup window is not open. Note that the // simulation itself will only update if it is not paused. if (tmp_.file_dialog == UiTempState::FileDialog_None) { UpdatePhysics(); } return status == platform::Window::Status::kRunning && !tmp_.should_exit; } void App::Render() { const float width = window_->GetWidth(); const float height = window_->GetHeight(); const float scale = window_->GetScale(); if (IsHeadless(window_->GetGraphicsMode())) { pixels_.resize(width * height * 3); } else { pixels_.clear(); } renderer_->Render(model(), data(), &perturb_, &camera_, &vis_options_, width * scale, height * scale, pixels_); window_->EndFrame(); window_->Present(pixels_); if (has_data()) { for (int i = 0; i < mjNTIMER; i++) { data()->timer[i].duration = 0; data()->timer[i].number = 0; } } } void App::ProcessPendingLoads() { // Check to see if we need to load a new model. if (pending_load_.has_value()) { std::string load_data = std::move(pending_load_.value()); pending_load_.reset(); if (load_data.empty()) { InitEmptyModel(); } else { LoadModelFromFile(load_data); } } if (pending_op_) { pending_op_(); pending_op_ = nullptr; } // Allow plugins to edit the spec as well. platform::ForEachPlugin([&](auto* plugin) { if (plugin->pre_compile) { if (plugin->pre_compile(plugin, spec(), model(), data(), &camera_)) { Recompile(); if (plugin->post_compile) { plugin->post_compile(plugin, spec(), model(), data()); } }; } }); // Check plugins to see if we need to load a new model. platform::ForEachPlugin([&](auto* plugin) { if (plugin->get_model_to_load) { char model_name[1000] = ""; char content_type[1000] = ""; int size = 0; const char* buf = plugin->get_model_to_load( plugin, &size, content_type, sizeof(content_type), model_name, sizeof(model_name)); if (buf && size) { const std::byte* bytes = reinterpret_cast(buf); LoadModelFromBuffer({bytes, bytes + size}, content_type, model_name); } } }); } void App::HandleWindowEvents() { const std::string drop_file = window_->GetDropFile(); if (!drop_file.empty()) { RequestModelLoad(drop_file); } } void App::HandleMouseEvents() { auto& io = ImGui::GetIO(); if (io.WantCaptureMouse) { return; } if (!has_model() || !has_data()) { return; } // Normalize mouse positions and movement to display size. const float mouse_x = io.MousePos.x / io.DisplaySize.x; const float mouse_y = io.MousePos.y / io.DisplaySize.y; const float mouse_dx = io.MouseDelta.x / io.DisplaySize.x; const float mouse_dy = io.MouseDelta.y / io.DisplaySize.y; const float mouse_scroll = io.MouseWheel / 50.0f; const bool is_mouse_moving = (mouse_dx != 0.0f || mouse_dy != 0.0f); const bool is_any_mouse_down = ImGui::IsMouseDown(ImGuiMouseButton_Left) || ImGui::IsMouseDown(ImGuiMouseButton_Right) || ImGui::IsMouseDown(ImGuiMouseButton_Middle); const bool is_mouse_dragging = is_mouse_moving && is_any_mouse_down; // If no mouse buttons are down, end any active perturbations. if (!is_any_mouse_down) { perturb_.active = 0; } // Handle perturbation mouse actions. if (is_mouse_dragging && io.KeyCtrl) { if (perturb_.select > 0) { mjtMouse action = mjMOUSE_NONE; if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (io.KeyAlt) { action = io.KeyShift ? mjMOUSE_MOVE_H : mjMOUSE_MOVE_V; } else { action = io.KeyShift ? mjMOUSE_ROTATE_H : mjMOUSE_ROTATE_V; } } else if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) { action = io.KeyShift ? mjMOUSE_MOVE_H : mjMOUSE_MOVE_V; } else if (ImGui::IsMouseDown(ImGuiMouseButton_Middle)) { action = mjMOUSE_ZOOM; } const mjtPertBit active = (action == mjMOUSE_MOVE_V || action == mjMOUSE_MOVE_H) ? mjPERT_TRANSLATE : mjPERT_ROTATE; if (active != perturb_.active) { platform::InitPerturb(model(), data(), &camera_, &perturb_, active); } platform::MovePerturb(model(), data(), &camera_, &perturb_, action, mouse_dx, mouse_dy); } } // Handle camera movement actions. else if (is_mouse_dragging) { if (ui_.camera_idx == platform::kFreeCameraIdx) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { MoveCamera(platform::CameraMotion::PAN_TILT, mouse_dx, mouse_dy); } } else { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { MoveCamera(platform::CameraMotion::ORBIT, mouse_dx, mouse_dy); } else if (ImGui::IsMouseDown(ImGuiMouseButton_Middle)) { MoveCamera(platform::CameraMotion::ZOOM, mouse_dx, mouse_dy); } } // Right mouse movement is relative to the horizontal and vertical planes. if (ImGui::IsMouseDown(ImGuiMouseButton_Right) && io.KeyShift) { MoveCamera(platform::CameraMotion::PLANAR_MOVE_H, mouse_dx, mouse_dy); } else if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) { MoveCamera(platform::CameraMotion::PLANAR_MOVE_V, mouse_dx, mouse_dy); } } // Mouse scroll zooms the camera towards/away from the lookat point. // Ignored by user-centered free cameras which don't have a lookat point. if (mouse_scroll != 0.0f && ui_.camera_idx != platform::kFreeCameraIdx) { MoveCamera(platform::CameraMotion::ZOOM, 0, -mouse_scroll); } // Left double click. if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { platform::PickResult picked = platform::Pick(model(), data(), &camera_, mouse_x, mouse_y, window_->GetAspectRatio(), &vis_options_); if (picked.body >= 0) { perturb_.select = picked.body; perturb_.flexselect = picked.flex; perturb_.skinselect = picked.skin; // Select the corresponding element in the spec. tmp_.curr_element = nullptr; if (has_spec()) { mjsElement* element = mjs_firstElement(spec(), mjOBJ_BODY); while (element) { if (mjs_getId(element) == picked.body) { tmp_.curr_element = element; break; } element = mjs_nextElement(spec(), element); } } // Compute the local position of the selected object in the world. mjtNum tmp[3]; mju_sub3(tmp, picked.point, data()->xpos + 3 * picked.body); mju_mulMatTVec(perturb_.localpos, data()->xmat + 9 * picked.body, tmp, 3, 3); } else { mjv_defaultPerturb(&perturb_); } } // Right double click. if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right)) { platform::PickResult picked = platform::Pick(model(), data(), &camera_, mouse_x, mouse_y, window_->GetAspectRatio(), &vis_options_); mju_copy3(camera_.lookat, picked.point); if (picked.body > 0 && io.KeyCtrl) { // Switch camera to tracking mode and track the selected body. camera_.type = mjCAMERA_TRACKING; camera_.trackbodyid = picked.body; camera_.fixedcamid = -1; ui_.camera_idx = platform::kTrackingCameraIdx; } } } void App::HandleKeyboardEvents() { using platform::ImGui_IsChordJustPressed; if (ImGui::GetIO().WantCaptureKeyboard) { return; } constexpr auto ImGuiMod_CtrlShift = ImGuiMod_Ctrl | ImGuiMod_Shift; bool is_freecam_wasd = ui_.camera_idx == platform::kFreeCameraIdx; // Menu shortcuts. if (ImGui_IsChordJustPressed(ImGuiKey_O | ImGuiMod_Ctrl)) { tmp_.file_dialog = UiTempState::FileDialog_Load; } else if (ImGui_IsChordJustPressed(ImGuiKey_S | ImGuiMod_CtrlShift)) { tmp_.file_dialog = UiTempState::FileDialog_SaveMjb; } else if (ImGui_IsChordJustPressed(ImGuiKey_S | ImGuiMod_Ctrl)) { tmp_.file_dialog = UiTempState::FileDialog_SaveXml; } else if (ImGui_IsChordJustPressed(ImGuiKey_M | ImGuiMod_Ctrl)) { tmp_.file_dialog = UiTempState::FileDialog_PrintModel; } else if (ImGui_IsChordJustPressed(ImGuiKey_D | ImGuiMod_Ctrl)) { tmp_.file_dialog = UiTempState::FileDialog_PrintData; } else if (ImGui_IsChordJustPressed(ImGuiKey_P | ImGuiMod_Ctrl)) { tmp_.file_dialog = UiTempState::FileDialog_SaveScreenshot; } else if (ImGui_IsChordJustPressed(ImGuiKey_C | ImGuiMod_Ctrl)) { std::string keyframe = platform::KeyframeToString(model(), data(), false); platform::MaybeSaveToClipboard(keyframe); } else if (ImGui_IsChordJustPressed(ImGuiKey_L | ImGuiMod_Ctrl)) { RequestModelReload(); } else if (ImGui_IsChordJustPressed(ImGuiKey_Q | ImGuiMod_Ctrl)) { tmp_.should_exit = true; } else if (ImGui_IsChordJustPressed(ImGuiKey_A | ImGuiMod_Ctrl)) { mjv_defaultFreeCamera(model(), &camera_); } else if (ImGui_IsChordJustPressed(ImGuiKey_Tab | ImGuiMod_Shift)) { tmp_.inspector_panel = !tmp_.inspector_panel; } else if (ImGui_IsChordJustPressed(ImGuiKey_Tab)) { tmp_.options_panel = !tmp_.options_panel; } else if (ImGui_IsChordJustPressed(ImGuiKey_Minus | ImGuiMod_Ctrl)) { float old_scale = ui_.font_scale; ui_.font_scale = std::clamp(ui_.font_scale - 0.1f, 0.5f, 3.0f); platform::RescaleDock(ui_.font_scale / old_scale); } else if (ImGui_IsChordJustPressed(ImGuiKey_Equal | ImGuiMod_Ctrl)) { float old_scale = ui_.font_scale; ui_.font_scale = std::clamp(ui_.font_scale + 0.1f, 0.5f, 3.0f); platform::RescaleDock(ui_.font_scale / old_scale); } else if (ImGui_IsChordJustPressed(ImGuiKey_Minus)) { SetSpeedIndex(tmp_.speed_index + 1); } else if (ImGui_IsChordJustPressed(ImGuiKey_Equal)) { SetSpeedIndex(tmp_.speed_index - 1); } else if (ImGui_IsChordJustPressed(ImGuiKey_LeftArrow)) { if (step_control_.GetPauseState() == PauseState::kNormalPaused) { LoadHistory(sim_history_.GetIndex() - 1); } } else if (ImGui_IsChordJustPressed(ImGuiKey_RightArrow)) { if (step_control_.GetPauseState() == PauseState::kNormalPaused) { if (sim_history_.GetIndex() == 0) { step_control_.RequestSingleStep(); } else { LoadHistory(sim_history_.GetIndex() + 1); } } } else if (ImGui_IsChordJustPressed(ImGuiMod_Ctrl | ImGuiKey_Space)) { if (step_control_.GetPauseState() == PauseState::kViscousPaused) { step_control_.SetPauseState(PauseState::kUnpaused); } else { step_control_.SetPauseState(PauseState::kViscousPaused); } } else if (ImGui_IsChordJustPressed(ImGuiKey_Space)) { if (step_control_.GetPauseState() == PauseState::kViscousPaused) { step_control_.SetPauseState(PauseState::kNormalPaused); } else if (step_control_.GetPauseState() == PauseState::kUnpaused) { step_control_.SetPauseState(PauseState::kNormalPaused); } else { step_control_.SetPauseState(PauseState::kUnpaused); } } else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) { ResetPhysics(); } else if (ImGui_IsChordJustPressed(ImGuiKey_Delete)) { // TODO: SpecDeleteElement(tmp_.curr_element); } else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) { SelectParentPerturb(model(), perturb_); } else if (ImGui_IsChordJustPressed(ImGuiKey_F1)) { ToggleWindow(tmp_.help); } else if (ImGui_IsChordJustPressed(ImGuiKey_F2)) { ToggleWindow(tmp_.stats); } else if (ImGui_IsChordJustPressed(ImGuiKey_F3)) { ToggleWindow(tmp_.profiler); } else if (ImGui_IsChordJustPressed(ImGuiKey_F6)) { vis_options_.frame = (vis_options_.frame + 1) % mjNFRAME; } else if (ImGui_IsChordJustPressed(ImGuiKey_F7)) { vis_options_.label = (vis_options_.label + 1) % mjNLABEL; } else if (ImGui_IsChordJustPressed(ImGuiKey_F11)) { tmp_.full_screen = !tmp_.full_screen; } else if (ImGui_IsChordJustPressed(ImGuiKey_H)) { ToggleFlag(vis_options_.flags[mjVIS_CONVEXHULL]); } else if (ImGui_IsChordJustPressed(ImGuiKey_X)) { ToggleFlag(vis_options_.flags[mjVIS_TEXTURE]); } else if (ImGui_IsChordJustPressed(ImGuiKey_J)) { ToggleFlag(vis_options_.flags[mjVIS_JOINT]); } else if (!is_freecam_wasd && ImGui_IsChordJustPressed(ImGuiKey_Q)) { ToggleFlag(vis_options_.flags[mjVIS_CAMERA]); } else if (ImGui_IsChordJustPressed(ImGuiKey_U)) { ToggleFlag(vis_options_.flags[mjVIS_ACTUATOR]); } else if (ImGui_IsChordJustPressed(ImGuiKey_Comma)) { ToggleFlag(vis_options_.flags[mjVIS_ACTIVATION]); } else if (ImGui_IsChordJustPressed(ImGuiKey_Z)) { ToggleFlag(vis_options_.flags[mjVIS_LIGHT]); } else if (ImGui_IsChordJustPressed(ImGuiKey_V)) { ToggleFlag(vis_options_.flags[mjVIS_TENDON]); } else if (ImGui_IsChordJustPressed(ImGuiKey_Y)) { ToggleFlag(vis_options_.flags[mjVIS_RANGEFINDER]); } else if (!is_freecam_wasd && ImGui_IsChordJustPressed(ImGuiKey_E)) { ToggleFlag(vis_options_.flags[mjVIS_CONSTRAINT]); } else if (ImGui_IsChordJustPressed(ImGuiKey_I)) { ToggleFlag(vis_options_.flags[mjVIS_INERTIA]); } else if (ImGui_IsChordJustPressed(ImGuiKey_Apostrophe)) { ToggleFlag(vis_options_.flags[mjVIS_SCLINERTIA]); } else if (ImGui_IsChordJustPressed(ImGuiKey_B)) { ToggleFlag(vis_options_.flags[mjVIS_PERTFORCE]); } else if (ImGui_IsChordJustPressed(ImGuiKey_O)) { ToggleFlag(vis_options_.flags[mjVIS_PERTOBJ]); } else if (ImGui_IsChordJustPressed(ImGuiKey_C)) { ToggleFlag(vis_options_.flags[mjVIS_CONTACTPOINT]); } else if (ImGui_IsChordJustPressed(ImGuiKey_N)) { ToggleFlag(vis_options_.flags[mjVIS_ISLAND]); } else if (ImGui_IsChordJustPressed(ImGuiKey_F)) { ToggleFlag(vis_options_.flags[mjVIS_CONTACTFORCE]); } else if (ImGui_IsChordJustPressed(ImGuiKey_P)) { ToggleFlag(vis_options_.flags[mjVIS_CONTACTSPLIT]); } else if (ImGui_IsChordJustPressed(ImGuiKey_T)) { ToggleFlag(vis_options_.flags[mjVIS_TRANSPARENT]); } else if (!is_freecam_wasd && ImGui_IsChordJustPressed(ImGuiKey_A)) { ToggleFlag(vis_options_.flags[mjVIS_AUTOCONNECT]); } else if (ImGui_IsChordJustPressed(ImGuiKey_M)) { ToggleFlag(vis_options_.flags[mjVIS_COM]); } else if (!is_freecam_wasd && ImGui_IsChordJustPressed(ImGuiKey_D)) { ToggleFlag(vis_options_.flags[mjVIS_STATIC]); } else if (ImGui_IsChordJustPressed(ImGuiKey_Semicolon)) { ToggleFlag(vis_options_.flags[mjVIS_SKIN]); } else if (ImGui_IsChordJustPressed(ImGuiKey_GraveAccent)) { ToggleFlag(vis_options_.flags[mjVIS_BODYBVH]); } else if (ImGui_IsChordJustPressed(ImGuiKey_Backslash)) { ToggleFlag(vis_options_.flags[mjVIS_MESHBVH]); } else if (ImGui_IsChordJustPressed(ImGuiKey_0 | ImGuiMod_Shift)) { ToggleFlag(vis_options_.sitegroup[0]); } else if (ImGui_IsChordJustPressed(ImGuiKey_1 | ImGuiMod_Shift)) { ToggleFlag(vis_options_.sitegroup[1]); } else if (ImGui_IsChordJustPressed(ImGuiKey_2 | ImGuiMod_Shift)) { ToggleFlag(vis_options_.sitegroup[2]); } else if (ImGui_IsChordJustPressed(ImGuiKey_3 | ImGuiMod_Shift)) { ToggleFlag(vis_options_.sitegroup[3]); } else if (ImGui_IsChordJustPressed(ImGuiKey_4 | ImGuiMod_Shift)) { ToggleFlag(vis_options_.sitegroup[4]); } else if (ImGui_IsChordJustPressed(ImGuiKey_5 | ImGuiMod_Shift)) { ToggleFlag(vis_options_.sitegroup[5]); } else if (ImGui_IsChordJustPressed(ImGuiKey_0)) { ToggleFlag(vis_options_.geomgroup[0]); } else if (ImGui_IsChordJustPressed(ImGuiKey_1)) { ToggleFlag(vis_options_.geomgroup[1]); } else if (ImGui_IsChordJustPressed(ImGuiKey_2)) { ToggleFlag(vis_options_.geomgroup[2]); } else if (ImGui_IsChordJustPressed(ImGuiKey_3)) { ToggleFlag(vis_options_.geomgroup[3]); } else if (ImGui_IsChordJustPressed(ImGuiKey_4)) { ToggleFlag(vis_options_.geomgroup[4]); } else if (ImGui_IsChordJustPressed(ImGuiKey_5)) { ToggleFlag(vis_options_.geomgroup[5]); } else if (has_model() && ImGui_IsChordJustPressed(ImGuiKey_Escape)) { ui_.camera_idx = platform::SetCamera(model(), &camera_, platform::kTumbleCameraIdx); } else if (has_model() && ImGui_IsChordJustPressed(ImGuiKey_LeftBracket)) { ui_.camera_idx = platform::SetCamera(model(), &camera_, ui_.camera_idx - 1); } else if (has_model() && ImGui_IsChordJustPressed(ImGuiKey_RightBracket)) { ui_.camera_idx = platform::SetCamera(model(), &camera_, ui_.camera_idx + 1); // WASD camera controls for free camera. } else if (is_freecam_wasd && (ImGui::IsKeyDown(ImGuiKey_W) || ImGui::IsKeyDown(ImGuiKey_S) || ImGui::IsKeyDown(ImGuiKey_A) || ImGui::IsKeyDown(ImGuiKey_D) || ImGui::IsKeyDown(ImGuiKey_Q) || ImGui::IsKeyDown(ImGuiKey_E))) { bool moved = false; // Move (dolly) forward/backward using W and S keys. if (ImGui::IsKeyDown(ImGuiKey_W)) { MoveCamera(platform::CameraMotion::TRUCK_DOLLY, 0, tmp_.cam_speed); moved = true; } else if (ImGui::IsKeyDown(ImGuiKey_S)) { MoveCamera(platform::CameraMotion::TRUCK_DOLLY, 0, -tmp_.cam_speed); moved = true; } // Strafe (truck) left/right using A and D keys. if (ImGui::IsKeyDown(ImGuiKey_A)) { MoveCamera(platform::CameraMotion::TRUCK_DOLLY, -tmp_.cam_speed, 0); moved = true; } else if (ImGui::IsKeyDown(ImGuiKey_D)) { MoveCamera(platform::CameraMotion::TRUCK_DOLLY, tmp_.cam_speed, 0); moved = true; } // Move (pedestal) up/down using Q and E keys. if (ImGui::IsKeyDown(ImGuiKey_Q)) { MoveCamera(platform::CameraMotion::TRUCK_PEDESTAL, 0, tmp_.cam_speed); moved = true; } else if (ImGui::IsKeyDown(ImGuiKey_E)) { MoveCamera(platform::CameraMotion::TRUCK_PEDESTAL, 0, -tmp_.cam_speed); moved = true; } if (moved) { tmp_.cam_speed += 0.001f; const float max_speed = ImGui::GetIO().KeyShift ? 0.1 : 0.01f; if (tmp_.cam_speed > max_speed) { tmp_.cam_speed = max_speed; } } else { tmp_.cam_speed = 0.001f; } } else { platform::ForEachPlugin([&](auto* plugin) { if (plugin->key_chord && plugin->on_key_pressed) { if (ImGui_IsChordJustPressed(plugin->key_chord)) { plugin->on_key_pressed(plugin); } } }); } } void App::LoadSettings() { if (!ini_path_.empty()) { std::string settings = platform::LoadText(ini_path_); if (!settings.empty()) { ui_.FromDict(platform::ReadIniSection(settings, "[Studio][UX]")); ImGui::LoadIniSettingsFromMemory(settings.data(), settings.size()); platform::KeyValues plugin_names = platform::ReadIniSection(settings, "[Studio][Plugins]"); platform::ForEachPlugin([&](auto* plugin) { auto it = plugin_names.find(plugin->name); if (it != plugin_names.end()) { plugin->active = std::stoi(it->second) != 0; } }); } } } void App::SaveSettings() { if (!ini_path_.empty()) { std::string settings = ImGui::SaveIniSettingsToMemory(); if (window_) { ui_.window_width = window_->GetWidth(); ui_.window_height = window_->GetHeight(); } platform::AppendIniSection(settings, "[Studio][UX]", ui_.ToDict()); platform::KeyValues plugin_names; platform::ForEachPlugin([&](auto* plugin) { plugin_names[plugin->name] = std::to_string((int)plugin->active); }); platform::AppendIniSection(settings, "[Studio][Plugins]", plugin_names); platform::SaveText(settings, ini_path_); } } void App::SetSpeedIndex(int idx) { platform::SetSpeedIndex(&step_control_, tmp_.speed_index, idx); } void App::MoveCamera(platform::CameraMotion motion, mjtNum reldx, mjtNum reldy) { platform::MoveCamera(model(), data(), &camera_, motion, reldx, reldy); } void App::BuildGui() { if (tmp_.full_screen) { return; } if (!tmp_.style_editor) { platform::SetupTheme(ui_.theme); } ImGui::GetIO().FontGlobalScale = ui_.font_scale; const ImVec4 workspace_rect = platform::ConfigureDockingLayout(); // Place charts in bottom right corner of the workspace. const ImVec2 chart_size(250, 500); const ImVec2 chart_pos(workspace_rect.x + workspace_rect.z - chart_size.x, workspace_rect.y + workspace_rect.w - chart_size.y); MainMenuGui(); { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); if (ImGui::Begin("ToolBar")) { ImGui::PopStyleVar(); ToolBarGui(); } else { ImGui::PopStyleVar(); } ImGui::End(); } { platform::ScopedStyle style; style.Var(ImGuiStyleVar_CellPadding, ImVec2(0, 0)); style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); style.Var(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); if (ImGui::Begin("StatusBar")) { StatusBarGui(); } ImGui::End(); } if (tmp_.options_panel) { if (ImGui::Begin("Options", &tmp_.options_panel)) { ModelOptionsGui(); } ImGui::End(); } if (tmp_.inspector_panel) { if (ImGui::Begin("Explorer", &tmp_.inspector_panel)) { SpecExplorerGui(); } ImGui::End(); if (ImGui::Begin("Editor", &tmp_.inspector_panel)) { SpecEditorGui(); } ImGui::End(); if (ImGui::Begin("Inspector", &tmp_.inspector_panel)) { DataInspectorGui(); } ImGui::End(); } if (tmp_.profiler) { if (ImGui::Begin("Profiler", &tmp_.profiler, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { platform::ProfilerGui(model(), data(), &profiler_); } ImGui::End(); } if (tmp_.picture_in_picture) { if (ImGui::Begin("Picture-in-Picture", &tmp_.picture_in_picture)) { platform::PipGui(model(), data(), window_->GetAspectRatio(), renderer_.get(), &tmp_.pips); } ImGui::End(); } if (tmp_.help) { platform::ScopedStyle style; style.Var(ImGuiStyleVar_Alpha, 0.8f); ImGui::SetNextWindowPos(ImVec2(workspace_rect.x, workspace_rect.y), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_Appearing); if (ImGui::Begin("Help", &tmp_.help, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 5.0f)); HelpGui(); ImGui::PopStyleVar(); } ImGui::End(); } if (tmp_.stats) { platform::ScopedStyle style; style.Var(ImGuiStyleVar_Alpha, 0.6f); if (ImGui::Begin("Stats", &tmp_.stats)) { const float fps = renderer_->GetFps(); platform::StatsGui( model(), data(), step_control_.GetPauseState() == PauseState::kNormalPaused, fps); } ImGui::End(); } // Display a drag-and-drop message if no model is loaded. if (model_kind_ == kEmptyModel) { #ifndef __EMSCRIPTEN__ const char* text = "Load model file or drag-and-drop model file here."; const float width = window_->GetWidth() * ImGui::GetWindowDpiScale(); const float height = window_->GetHeight() * ImGui::GetWindowDpiScale(); const ImVec2 text_size = ImGui::CalcTextSize(text); ImGui::SetNextWindowPos(ImVec2((width - text_size.x) / 2, height / 2), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(text_size.x + 10, text_size.y + 10), ImGuiCond_Always); const ImGuiWindowFlags kOverlayFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoDocking; if (ImGui::Begin("##Overlay", 0, kOverlayFlags)) { platform::ScopedStyle style; style.Color(ImGuiCol_Text, ImVec4(1, 1, 1, 1)); ImGui::Text("%s", text); } ImGui::End(); #endif // !__EMSCRIPTEN__ } FileDialogGui(); if (tmp_.imgui_demo) { ImGui::ShowDemoWindow(); } if (tmp_.implot_demo) { ImPlot::ShowDemoWindow(); } if (tmp_.style_editor) { if (ImGui::Begin("Style Editor", &tmp_.style_editor)) { ImGui::ShowStyleEditor(); } ImGui::End(); } platform::ForEachPlugin([](auto* plugin) { if (!plugin->update) { return; } if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("Plugins")) { if (ImGui::MenuItem(plugin->name, "", plugin->active)) { plugin->active = !plugin->active; } ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } if (plugin->active) { ImGui::Begin(plugin->name); plugin->update(plugin); ImGui::End(); } }); ImGuiIO& io = ImGui::GetIO(); if (tmp_.first_frame) { LoadSettings(); tmp_.first_frame = false; } if (io.WantSaveIniSettings) { SaveSettings(); io.WantSaveIniSettings = false; } } void App::ModelOptionsGui() { const float min_width = platform::GetExpectedLabelWidth(); const ImGuiChildFlags child_flags = ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize; const ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; ImGui::BeginChild("PhysicsGui", {0, 0}, child_flags); if (ImGui::TreeNodeEx("Physics", node_flags)) { platform::PhysicsGui(model(), min_width); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("RenderingGui", {0, 0}, child_flags); if (ImGui::TreeNodeEx("Rendering", node_flags)) { platform::RenderingGui(model(), &vis_options_, renderer_->GetRenderFlags(), min_width); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("GroupsGui", {0, 0}, child_flags); if (ImGui::TreeNodeEx("Visibility Groups", node_flags)) { platform::GroupsGui(model(), &vis_options_, min_width); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("VisualizationGui", {0, 0}, child_flags); if (ImGui::TreeNodeEx("Visualization", node_flags)) { platform::VisualizationGui(model(), &vis_options_, &camera_, min_width); ImGui::TreePop(); } ImGui::EndChild(); } void App::DataInspectorGui() { if (!has_data()) { ImGui::Text("No mjData loaded."); return; } const float min_width = platform::GetExpectedLabelWidth(); const ImGuiChildFlags child_flags = ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize; const ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; ImGui::BeginChild("JointsGui", {0, 0}, child_flags); if (ImGui::TreeNodeEx("Joints", node_flags)) { platform::JointsGui(model(), data(), &vis_options_); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("ControlsGui", {0, 0}, child_flags); if (ImGui::TreeNodeEx("Controls", node_flags)) { float noise_scale = 0; float noise_rate = 0; step_control_.GetNoiseParameters(noise_scale, noise_rate); platform::NoiseGui(model(), data(), noise_scale, noise_rate); step_control_.SetNoiseParameters(noise_scale, noise_rate); ImGui::Separator(); platform::ControlsGui(model(), data(), &vis_options_); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("SensorGui", {0, 0}, child_flags); if (ImGui::TreeNodeEx("Sensor", node_flags)) { platform::SensorGui(model(), data()); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("WatchGui", {0, 0}, child_flags); if (ImGui::TreeNodeEx("Watch", node_flags)) { platform::WatchGui(model(), data(), ui_.watch_field, sizeof(ui_.watch_field), ui_.watch_index); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("StateGui", {0, 0}, child_flags); if (ImGui::TreeNodeEx("State", node_flags)) { platform::StateGui(model(), data(), tmp_.state, tmp_.state_sig, min_width); ImGui::TreePop(); } ImGui::EndChild(); } void App::SpecExplorerGui() { if (!has_spec()) { ImGui::Text("No mjSpec loaded."); return; } // Initialize the split height. const ImVec2 region = ImGui::GetContentRegionAvail(); if (tmp_.explorer_split < 0) { tmp_.explorer_split = region.y * 0.7f; } tmp_.explorer_split = std::clamp(tmp_.explorer_split, 20.f, region.y - 40.f); mjsElement* element = tmp_.curr_element; bool open = element != nullptr; if (platform::ImGui_BeginHSplit("SpecExplorerTree", &tmp_.explorer_split, &open)) { platform::SpecTreeGui(&element, spec()); if (element != tmp_.curr_element) { tmp_.curr_element = element; // A different element was selected, so select it for perturb. if (element) { open = true; const int element_id = mjs_getId(element); if (element->elemtype == mjOBJ_BODY && perturb_.select != element_id) { mjv_defaultPerturb(&perturb_); perturb_.select = element_id; } } } if (platform::ImGui_HSplit("SpecExplorerProperties", &tmp_.explorer_split, &open)) { ImGui::Text("%s", mju_type2Str(tmp_.curr_element->elemtype)); ImGui::SameLine(); ImGui::Text("(%d)", mjs_getId(tmp_.curr_element)); ImGui::SameLine(ImGui::GetContentRegionAvail().x - 100.0f); auto mode_button = [&](const char* label, const char* tooltip, SpecPropertiesMode mode) { platform::ScopedStyle style; style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); if (tmp_.spec_prop_mode == mode) { style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive); } if (ImGui::Button(label, ImVec2(24.0f, 20.0f))) { tmp_.spec_prop_mode = mode; } ImGui::SetItemTooltip("%s", tooltip); }; mode_button("S", "Spec", SpecPropertiesMode::kSpec); ImGui::SameLine(); mode_button("M", "Model", SpecPropertiesMode::kModel); ImGui::SameLine(); mode_button("D", "Data", SpecPropertiesMode::kData); ImGui::Separator(); if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) { platform::ElementSpecGui(element); } else if (tmp_.spec_prop_mode == SpecPropertiesMode::kModel) { platform::ElementModelGui(model(), tmp_.curr_element); } else { platform::ElementDataGui(data(), tmp_.curr_element); } } if (!open) { tmp_.curr_element = nullptr; } } platform::ImGui_EndHSplit(open); } void App::SpecEditorGui() { if (ImGui::BeginChild("SpecEditor", ImVec2(-1, 36))) { if (ImGui::BeginTable("##SpecEditorHeader", 3)) { ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 50.0f); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 30.0f); ImGui::TableNextColumn(); ImGui::BeginDisabled(!spec_editor_.CanUndo()); if (ImGui::Button(ICON_UNDO_SPEC)) { spec_editor_.Undo(); } ImGui::EndDisabled(); ImGui::SameLine(); ImGui::BeginDisabled(!spec_editor_.CanRedo()); if (ImGui::Button(ICON_REDO_SPEC)) { spec_editor_.Redo(); } ImGui::EndDisabled(); ImGui::TableNextColumn(); ImGui::PushStyleColor(ImGuiCol_Button, ImColor(40, 180, 40, 255).Value); if (ImGui::Button("Compile and Reload", ImVec2(-1, 0))) { pending_op_ = [this]() { auto tmp_holder = spec_editor_.Compile(); if (tmp_holder->ok()) { model_holder_ = std::move(tmp_holder); OnModelLoaded(model_name_, model_kind_); } else { load_error_ = std::move(tmp_holder->error()); } }; } ImGui::PopStyleColor(); ImGui::TableNextColumn(); if (ImGui::Button(" + ")) { ImGui::OpenPopupOnItemClick("SpecAddElement", 0); } if (ImGui::BeginPopupContextItem("SpecAddElement")) { auto option = [&](const char* label, mjtObj type) { if (ImGui::Selectable(label)) { mjsElement* element = spec_editor_.AddElement(type); spec_editor_.SetActiveElement(element); } }; option("Actuator", mjOBJ_ACTUATOR); option("Equality", mjOBJ_EQUALITY); option("Exclude", mjOBJ_EXCLUDE); option("Flex", mjOBJ_FLEX); option("Height Field", mjOBJ_HFIELD); option("Key", mjOBJ_KEY); option("Material", mjOBJ_MATERIAL); option("Mesh", mjOBJ_MESH); option("Numeric", mjOBJ_NUMERIC); option("Pair", mjOBJ_PAIR); option("Sensor", mjOBJ_SENSOR); option("Skin", mjOBJ_SKIN); option("Tendon", mjOBJ_TENDON); option("Text", mjOBJ_TEXT); option("Texture", mjOBJ_TEXTURE); option("Tuple", mjOBJ_TUPLE); ImGui::EndPopup(); } ImGui::EndTable(); } } ImGui::EndChild(); // Initialize the split height. const ImVec2 region = ImGui::GetContentRegionAvail(); if (tmp_.editor_split < 0) { tmp_.editor_split = region.y * 0.7f; } tmp_.editor_split = std::clamp(tmp_.editor_split, 20.0f, region.y - 40.0f); mjsElement* element = spec_editor_.GetActiveElement(); bool open = element != nullptr; if (platform::ImGui_BeginHSplit("SpecEditorTree", &tmp_.editor_split, &open)) { platform::SpecTreeGui(&element, spec_editor_.GetActiveSpec(), &spec_editor_); open = element != nullptr; spec_editor_.SetActiveElement(element); if (platform::ImGui_HSplit("SpecEditorProperties", &tmp_.editor_split, &open)) { ImGui::Text("%s", mju_type2Str(element->elemtype)); ImGui::SameLine(); ImGui::Text("(%d)", mjs_getId(element)); ImGui::Separator(); platform::ElementSpecGui(element, &spec_editor_); } platform::ImGui_EndHSplit(open); if (!open) { spec_editor_.SetActiveElement(nullptr); } } } void App::HelpGui() { const float pad = ImGui::GetStyle().ItemSpacing.x; const float indent = pad * 2; const float col0 = ImGui::CalcTextSize("Toggle Visc Pause").x + pad; const float col1 = ImGui::CalcTextSize("Ctrl+Spc").x + pad + indent; const float col2 = ImGui::CalcTextSize("Center of Mass").x + pad + indent; const float col3 = ImGui::CalcTextSize("M").x + pad + indent; ImGui::Dummy(ImVec2(col0 + col1 + col2 + col3, 0)); ImGui::Columns(4); ImGui::SetColumnWidth(0, col0); ImGui::SetColumnWidth(1, col1); ImGui::SetColumnWidth(2, col2); ImGui::SetColumnWidth(3, col3); ImGui::Text("Help"); ImGui::Text("Stats"); ImGui::Text("Profiler"); ImGui::Text("Cycle Frames"); ImGui::Text("Cycle Labels"); ImGui::Text("Toggle Fullscreen"); ImGui::Text("Free Camera"); ImGui::Text("Toggle Pause"); ImGui::Text("Toggle Visc Pause"); ImGui::Text("Reset Sim"); ImGui::Text("Toggle Left UI"); ImGui::Text("Toggle Right UI"); ImGui::Text("Speed Up"); ImGui::Text("Speed Down"); ImGui::Text("Prev Camera"); ImGui::Text("Next Camera"); ImGui::Text("Step Back"); ImGui::Text("Step Forward"); ImGui::Text("Select Parent"); ImGui::Text("Align Camera"); ImGui::Text("Copy Keyframe"); ImGui::Text("Geom Group"); ImGui::Text("Site Group"); ImGui::NextColumn(); ImGui::Indent(indent); ImGui::Text("F1"); ImGui::Text("F2"); ImGui::Text("F3"); ImGui::Text("F6"); ImGui::Text("F7"); ImGui::Text("F11"); ImGui::Text("Esc"); ImGui::Text("Spc"); ImGui::Text("Ctrl+Spc"); ImGui::Text("Bksp"); ImGui::Text("Tab"); ImGui::Text("Sh+Tab"); ImGui::Text("+"); ImGui::Text("-"); ImGui::Text("["); ImGui::Text("]"); ImGui::Text("Left"); ImGui::Text("Right"); ImGui::Text("PgUp"); ImGui::Text("Ctrl+A"); ImGui::Text("Ctrl+C"); ImGui::Text("0-5"); ImGui::Text("Sh+0-5"); ImGui::NextColumn(); ImGui::Indent(indent); ImGui::Text("Activation"); ImGui::Text("Auto Connect"); ImGui::Text("Body Tree"); ImGui::Text("Mesh Tree"); ImGui::Text("Scale Inertia"); ImGui::Text("Skin"); ImGui::Text("Actuator"); ImGui::Text("Camera"); ImGui::Text("Center of Mass"); ImGui::Text("Contact Force"); ImGui::Text("Contact Point"); ImGui::Text("Contact Split"); ImGui::Text("Convex Hull"); ImGui::Text("Constraint"); ImGui::Text("Island"); ImGui::Text("Joint"); ImGui::Text("Light"); ImGui::Text("Perturb Force"); ImGui::Text("Perturb Object"); ImGui::Text("Range Finder"); ImGui::Text("Static Body"); ImGui::Text("Tendon"); ImGui::Text("Texture"); ImGui::Text("Transparent"); ImGui::NextColumn(); ImGui::Indent(indent); ImGui::Text(","); ImGui::Text("K"); ImGui::Text("`"); ImGui::Text("\\"); ImGui::Text("\""); ImGui::Text(";"); ImGui::Text("U"); ImGui::Text("L"); ImGui::Text("M"); ImGui::Text("F"); ImGui::Text("C"); ImGui::Text("P"); ImGui::Text("H"); ImGui::Text("N"); ImGui::Text("I"); ImGui::Text("J"); ImGui::Text("Z"); ImGui::Text("B"); ImGui::Text("O"); ImGui::Text("Y"); ImGui::Text("G"); ImGui::Text("V"); ImGui::Text("X"); ImGui::Text("T"); ImGui::Columns(); } void App::ToolBarGui() { ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0, 0)); if (ImGui::BeginTable("##ToolBarTable", 2)) { platform::ScopedStyle style; style.Var(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x * 2.0f, ImGui::GetStyle().ItemSpacing.y)); const float label_width = platform::GetExpectedLabelWidth(); const float copy_btn_width = ImGui::GetFrameHeight(); const float theme_width = ImGui::CalcTextSize(platform::ICON_FA_CIRCLE_O).x + ImGui::GetStyle().FramePadding.x * 2; const float sp = ImGui::GetStyle().ItemSpacing.x; const float right_width = copy_btn_width + label_width + sp + label_width + sp + label_width + sp + theme_width; const float separator_width = ImGui::GetFrameHeight() * .6f; ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, right_width); ImGui::TableNextColumn(); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetStyle().WindowPadding.x); const float btn_size = ImGui::GetFrameHeight(); const ImVec2 square_size(btn_size, btn_size); // Reload button. { style.Var(ImGuiStyleVar_FrameRounding, 2.0f); if (ImGui::Button(ICON_RELOAD_MODEL, square_size)) { RequestModelReload(); } ImGui::SetItemTooltip("%s", "Reload"); } // Reset button. ImGui::SameLine(0, 0.5 * separator_width); if (ImGui::Button(ICON_RESET_MODEL, square_size)) { ResetPhysics(); } ImGui::SetItemTooltip("%s", "Reset"); // Combined (Normal Pause, Viscous Pause, Play) widget and Speed selection. ImGui::SameLine(0, separator_width); platform::StepControlGui(model(), &step_control_, tmp_.speed_index); ImGui::SameLine(0, separator_width); ImGui::SetNextItemWidth(120); ImGui::BeginDisabled(std::thread::hardware_concurrency() <= 1); if (ImGui::SliderInt("##NumThreads", &ui_.nthread, 0, 8, "%d threads")) { tmp_.update_threadpool = true; } ImGui::EndDisabled(); ImGui::SetItemTooltip("%s", "Number of threads in threadpool"); ImGui::TableNextColumn(); platform::CameraSelectionGui(model(), data(), camera_, ui_.camera_idx); ImGui::SameLine(); platform::LabelSelectionGui(&vis_options_); ImGui::SameLine(); platform::FrameSelectionGui(&vis_options_); ImGui::SameLine(); if (platform::ThemeSelectGui(&ui_.theme, square_size)) { platform::SetupTheme(ui_.theme); ImGui::GetIO().WantSaveIniSettings = true; } ImGui::EndTable(); } ImGui::PopStyleVar(); } void App::StatusBarGui() { if (ImGui::BeginTable("##StatusBarTable", 2)) { ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 520); ImGui::TableNextColumn(); if (!has_model()) { ImGui::Text("No model loaded"); } else if (step_control_.GetPauseState() == PauseState::kViscousPaused) { ImGui::Text("Viscous Pause"); ImGui::SetItemTooltip("Zero gravity, high viscosity, no spring forces"); } else if (step_control_.GetPauseState() == PauseState::kNormalPaused) { ImGui::Text("Paused"); } else { ImGui::Text("Running"); } if (!step_error_.empty()) { ImGui::SameLine(); ImGui::Text(" | Step Error: %s", step_error_.c_str()); } else if (!load_error_.empty()) { ImGui::SameLine(); ImGui::Text(" | Load Error: %s", load_error_.c_str()); } else if (!edit_error_.empty()) { ImGui::SameLine(); ImGui::Text(" | Edit Error: %s", edit_error_.c_str()); } ImGui::TableNextColumn(); ImGui::Text("%s", " |"); // Frame scrubber. platform::ScopedStyle style; style.Var(ImGuiStyleVar_FrameBorderSize, 0); style.Color(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]); ImGui::SameLine(); if (ImGui::Button(ICON_PREV_FRAME)) { LoadHistory(sim_history_.GetIndex() - 1); } ImGui::SetItemTooltip("%s", "Previous Frame"); style.Reset(); ImGui::SameLine(); ImGui::SetNextItemWidth(450); int index = sim_history_.GetIndex(); if (ImGui::SliderInt("##ScrubIndex", &index, 1 - sim_history_.Size(), 0)) { LoadHistory(index); } style.Var(ImGuiStyleVar_FrameBorderSize, 0); style.Color(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]); ImGui::SameLine(); if (ImGui::Button(ICON_NEXT_FRAME)) { if (sim_history_.GetIndex() == 0) { step_control_.RequestSingleStep(); } else { LoadHistory(sim_history_.GetIndex() + 1); } } ImGui::SetItemTooltip("%s", "Next Frame"); ImGui::SameLine(); if (ImGui::Button(ICON_CURR_FRAME)) { LoadHistory(0); } ImGui::SetItemTooltip("%s", "Current Frame"); ImGui::EndTable(); } } void App::MainMenuGui() { if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { #ifndef __EMSCRIPTEN__ if (ImGui::MenuItem("Open Model File", "Ctrl+O")) { tmp_.file_dialog = UiTempState::FileDialog_Load; } ImGui::Separator(); if (ImGui::MenuItem("Save XML", "Ctrl+S")) { tmp_.file_dialog = UiTempState::FileDialog_SaveXml; } if (ImGui::MenuItem("Save MJB", "Ctrl+Shift+S")) { tmp_.file_dialog = UiTempState::FileDialog_SaveMjb; } if (ImGui::MenuItem("Save Screenshot", "Ctrl+P")) { tmp_.file_dialog = UiTempState::FileDialog_SaveScreenshot; } ImGui::Separator(); if (ImGui::MenuItem("Print Model", "Ctrl+M")) { tmp_.file_dialog = UiTempState::FileDialog_PrintModel; } if (ImGui::MenuItem("Print Data", "Ctrl+D")) { tmp_.file_dialog = UiTempState::FileDialog_PrintData; } ImGui::Separator(); #endif // !__EMSCRIPTEN__ if (ImGui::MenuItem("Unload", "Ctrl+U")) { InitEmptyModel(); } #ifndef __EMSCRIPTEN__ ImGui::Separator(); if (ImGui::MenuItem("Quit", "Ctrl+Q")) { tmp_.should_exit = true; } #endif // !__EMSCRIPTEN__ ImGui::EndMenu(); } if (ImGui::BeginMenu("Simulation")) { if (ImGui::MenuItem( "Pause", "Space", step_control_.GetPauseState() == PauseState::kNormalPaused)) { if (step_control_.GetPauseState() != PauseState::kNormalPaused) { step_control_.SetPauseState(PauseState::kNormalPaused); } else { step_control_.SetPauseState(PauseState::kUnpaused); } } if (ImGui::MenuItem("Reset", "Backspace")) { ResetPhysics(); } if (ImGui::MenuItem("Reload", "Ctrl+L")) { RequestModelReload(); } ImGui::Separator(); if (ImGui::BeginMenu("Keyframes")) { ImGui::SetNextItemWidth(200); ImGui::SliderInt("##Key", &ui_.key_idx, 0, model()->nkey); if (ImGui::MenuItem("Load")) { mj_resetDataKeyframe(model(), data(), ui_.key_idx); mj_forward(model(), data()); } if (ImGui::MenuItem("Save")) { mj_setKeyframe(model(), data(), ui_.key_idx); } if (ImGui::MenuItem("Copy")) { std::string str = platform::KeyframeToString(model(), data(), false); platform::MaybeSaveToClipboard(str); } ImGui::EndMenu(); } ImGui::EndMenu(); } if (ImGui::BeginMenu("View")) { if (ImGui::MenuItem("Save Config")) { SaveSettings(); } if (ImGui::MenuItem("Reset Config")) { platform::SaveText("\n\n", ini_path_); LoadSettings(); } ImGui::Separator(); if (ImGui::MenuItem(tmp_.options_panel ? "Hide Options" : "Show Options", "Tab")) { tmp_.options_panel = !tmp_.options_panel; } if (ImGui::MenuItem( tmp_.inspector_panel ? "Hide Inspector" : "Show Inspector", "Shift+Tab")) { tmp_.inspector_panel = !tmp_.inspector_panel; } if (ImGui::MenuItem("Full Screen", "F11")) { tmp_.full_screen = !tmp_.full_screen; } ImGui::Separator(); if (ImGui::MenuItem("Picture-in-Picture")) { tmp_.picture_in_picture = !tmp_.picture_in_picture; } ImGui::Separator(); #ifdef __linux__ if (ImGui::BeginMenu("Graphics Mode (Experimental)")) { std::optional mode; if (ImGui::MenuItem( "Classic OpenGL", nullptr, gfx_mode_ == platform::GraphicsMode::ClassicOpenGl)) { mode = platform::GraphicsMode::ClassicOpenGl; } if (ImGui::MenuItem( "Classic OpenGL Headless", nullptr, gfx_mode_ == platform::GraphicsMode::ClassicOpenGlHeadless)) { mode = platform::GraphicsMode::ClassicOpenGlHeadless; } if (ImGui::MenuItem( "Filament OpenGL", nullptr, gfx_mode_ == platform::GraphicsMode::FilamentOpenGl)) { mode = platform::GraphicsMode::FilamentOpenGl; } if (ImGui::MenuItem( "Filament OpenGL Headless", nullptr, gfx_mode_ == platform::GraphicsMode::FilamentOpenGlHeadless)) { mode = platform::GraphicsMode::FilamentOpenGlHeadless; } if (ImGui::MenuItem( "Filament OpenGL Software", nullptr, gfx_mode_ == platform::GraphicsMode::FilamentOpenGlSoftware)) { mode = platform::GraphicsMode::FilamentOpenGlSoftware; } if (ImGui::MenuItem( "Filament Vulkan", nullptr, gfx_mode_ == platform::GraphicsMode::FilamentVulkan)) { mode = platform::GraphicsMode::FilamentVulkan; } if (ImGui::MenuItem( "Filament Vulkan Software", nullptr, gfx_mode_ == platform::GraphicsMode::FilamentVulkanSoftware)) { mode = platform::GraphicsMode::FilamentVulkanSoftware; } if (mode.has_value()) { pending_op_ = [=, this]() { const int width = window_->GetWidth(); const int height = window_->GetHeight(); SwitchGraphicsMode(width, height, *mode); // TODO: figure out why ImGui doesn't work unless we do this twice. if (IsClassic(*mode)) { SwitchGraphicsMode(width, height, *mode); } renderer_->Init(model()); }; } ImGui::EndMenu(); } #endif // __linux__ ImGui::EndMenu(); } if (ImGui::BeginMenu("Charts")) { if (ImGui::MenuItem("Profiler", "F3")) { ToggleWindow(tmp_.profiler); } ImGui::EndMenu(); } if (ImGui::BeginMenu("Plugins")) { // Placeholder menu item that will be populated by plugins later on. We // do this now in so that the menu is present at the right place. ImGui::EndMenu(); } if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("Help", "F1", tmp_.help)) { ToggleWindow(tmp_.help); } if (ImGui::MenuItem("Stats", "F2", tmp_.stats)) { ToggleWindow(tmp_.stats); } ImGui::Separator(); if (ImGui::MenuItem("Style Editor", "", tmp_.style_editor)) { tmp_.style_editor = !tmp_.style_editor; } ImGui::Separator(); if (ImGui::MenuItem("ImGui Demo")) { tmp_.imgui_demo = !tmp_.imgui_demo; } if (ImGui::MenuItem("ImPlot Demo")) { tmp_.implot_demo = !tmp_.implot_demo; } ImGui::Separator(); std::string version = "Version " + std::string(mj_versionString()); ImGui::MenuItem(version.c_str()); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } } void App::FileDialogGui() { if (tmp_.file_dialog == UiTempState::FileDialog_None) { return; } ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); if (ImGui::BeginPopupModal("FileDialog", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { const platform::DialogResult res = tmp_.file_dialog == UiTempState::FileDialog_Load ? platform::OpenFileDialog(tmp_.filename) : platform::SaveFileDialog(tmp_.filename); if (res.status == platform::DialogResult::kAccepted) { tmp_.last_path[tmp_.file_dialog] = res.path; switch (tmp_.file_dialog) { case UiTempState::FileDialog_Load: RequestModelLoad(res.path); break; case UiTempState::FileDialog_SaveXml: mj_saveLastXML(res.path.c_str(), model(), nullptr, 0); break; case UiTempState::FileDialog_SaveMjb: mj_saveModel(model(), res.path.c_str(), nullptr, 0); break; case UiTempState::FileDialog_SaveScreenshot: { const int width = window_->GetWidth(); const int height = window_->GetHeight(); std::vector buffer(width * height * 3); renderer_->RenderToTexture(model(), data(), &camera_, width, height, buffer.data()); platform::SaveToWebp(width, height, buffer.data(), res.path); break; } case UiTempState::FileDialog_PrintModel: mj_printModel(model(), res.path.c_str()); break; case UiTempState::FileDialog_PrintData: mj_printData(model(), data(), res.path.c_str()); break; default: break; } } if (res.status != platform::DialogResult::kError) { strncpy(tmp_.filename, res.path.c_str(), res.path.size()); tmp_.filename[res.path.size()] = 0; } if (res.status != platform::DialogResult::kPending) { tmp_.file_dialog = UiTempState::FileDialog_None; ImGui::CloseCurrentPopup(); window_->EnableWindowResizing(); } ImGui::EndPopup(); } if (!ImGui::IsPopupOpen("FileDialog") && tmp_.file_dialog != UiTempState::FileDialog_None) { const std::string& src = tmp_.last_path[tmp_.file_dialog]; strncpy(tmp_.filename, src.c_str(), src.size()); tmp_.filename[src.size()] = 0; ImGui::OpenPopup("FileDialog"); window_->DisableWindowResizing(); } } App::UiState::Dict App::UiState::ToDict() const { return { {"theme", std::to_string(static_cast(theme))}, {"font_scale", std::to_string(font_scale)}, {"window_width", std::to_string(window_width)}, {"window_height", std::to_string(window_height)}, {"nthread", std::to_string(nthread)}, }; } void App::UiState::FromDict(const Dict& dict) { using platform::ReadIniValue; *this = UiState(); theme = ReadIniValue(dict, "theme", theme); window_width = ReadIniValue(dict, "window_width", window_width); window_height = ReadIniValue(dict, "window_height", window_height); font_scale = ReadIniValue(dict, "font_scale", font_scale); nthread = ReadIniValue(dict, "nthread", nthread); } } // namespace mujoco::studio