diff --git a/src/experimental/platform/sim/step_control.cc b/src/experimental/platform/sim/step_control.cc index 7dbb9340..1f5b54c3 100644 --- a/src/experimental/platform/sim/step_control.cc +++ b/src/experimental/platform/sim/step_control.cc @@ -83,10 +83,14 @@ void StepControl::SetNoiseParameters(float ctrl_noise_scale, ctrl_noise_rate_ = ctrl_noise_rate; } -void StepControl::SetPauseState(PauseState state, mjModel* m) { +void StepControl::SetPauseState(PauseState state) { pause_state_ = state; } +StepControl::PauseState StepControl::GetPauseState() const { + return pause_state_; +} + StepControl::Status StepControl::Advance(mjModel* m, mjData* d) { if (!m) { return Status::kOk; @@ -184,7 +188,7 @@ StepControl::Status StepControl::Advance(mjModel* m, mjData* d) { for (mjtWarning w : kDivergedWarnings) { if (d->warning[w].number > 0) { // Stop stepping if the simulation diverged. - pause_state_ = PauseState::kNormalPaused; + SetPauseState(PauseState::kNormalPaused); return Status::kDiverged; } } diff --git a/src/experimental/platform/sim/step_control.h b/src/experimental/platform/sim/step_control.h index 0fec3025..1f34a810 100644 --- a/src/experimental/platform/sim/step_control.h +++ b/src/experimental/platform/sim/step_control.h @@ -70,11 +70,10 @@ class StepControl { enum class PauseState { kUnpaused, kNormalPaused, kViscousPaused }; // Sets the pause state of the simulation. - // m must be non-null for viscous pausing. - void SetPauseState(PauseState state, mjModel* m = nullptr); + void SetPauseState(PauseState state); // Gets the current pause state of the simulation. - PauseState GetPauseState() const { return pause_state_; } + PauseState GetPauseState() const; // If the simulation is paused, will perform a single step on the next // Advance() call. diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index a5405cb1..b20993ee 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -15,10 +15,12 @@ #include "experimental/platform/ux/gui.h" #include +#include #include #include #include #include +#include #include #include @@ -26,10 +28,23 @@ #include #include #include "experimental/platform/helpers.h" +#include "experimental/platform/sim/step_control.h" #include "experimental/platform/ux/imgui_widgets.h" #include "experimental/platform/ux/interaction.h" namespace mujoco::platform { +namespace { +struct SpeedStatus { + bool misaligned; + float measured; +}; + +static SpeedStatus IsSpeedMisaligned(const StepControl& step_control) { + const float desired = step_control.GetSpeed(); + const float measured = step_control.GetSpeedMeasured(); + return {std::abs(measured - desired) > 0.1f * desired, measured}; +} +} // namespace static ImVec2 GetFlexElementSize(int num_cols) { const float width = (ImGui::GetContentRegionAvail().x / num_cols) - @@ -317,6 +332,74 @@ ImVec4 ConfigureDockingLayout() { return ImVec4(workspace_x, workspace_y, workspace_w, workspace_h); } +void StepControlGui(const mjModel* model, StepControl* step_control, + int& speed_index) { + platform::ScopedStyle style; + style.Var(ImGuiStyleVar_FrameRounding, 2.f); + + const ImColor yellow(255, 215, 0, 255); + const ImColor green(40, 180, 40, 255); + const float scale = ImGui::GetWindowDpiScale(); + ImVec2 button_size(48.f * scale, 32.f * scale); + + auto make_button = [&](const char* icon, StepControl::PauseState target_state, + ImColor color, const char* tooltip = "", + float hover_alpha = 1.f) { + bool active = step_control->GetPauseState() == target_state; + if (ImGui_ColorButton(icon, active, color, button_size, hover_alpha)) { + step_control->SetPauseState(target_state); + } + if (!std::string_view(tooltip).empty()) { + ImGui::SetItemTooltip("%s", tooltip); + } + }; + + make_button(ICON_FA_PAUSE, StepControl::PauseState::kNormalPaused, yellow, + "Pause"); + ImGui::SameLine(0.f, 0.f); + make_button(ICON_FA_MAGIC, StepControl::PauseState::kViscousPaused, yellow, + "Viscous Pause"); + ImGui::SameLine(0.f, 0.f); + make_button(ICON_FA_PLAY, StepControl::PauseState::kUnpaused, green, "", .6f); + + // Speed selection. + ImGui::SameLine(); + const float pad_y = (button_size.y - ImGui::GetFontSize()) * .5f; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + ImVec2(ImGui::GetStyle().FramePadding.x + 5.f, pad_y)); + + const auto [misaligned, measured] = IsSpeedMisaligned(*step_control); + char speed_preview[64]; + if (misaligned) { + snprintf(speed_preview, sizeof(speed_preview), "%s%s (%-4.1f%%)", + ICON_FA_TACHOMETER, kPercentRealTime[speed_index], measured); + } else { + snprintf(speed_preview, sizeof(speed_preview), "%s%s", ICON_FA_TACHOMETER, + kPercentRealTime[speed_index]); + } + + ImGui::SetNextItemWidth(ImGui::CalcTextSize(speed_preview).x + + ImGui::GetStyle().FramePadding.x * 2.f); + if (ImGui::BeginCombo("##Speed", speed_preview, + ImGuiComboFlags_NoArrowButton)) { + for (int n = 0; n < kPercentRealTime.size(); n++) { + if (ImGui::Selectable(kPercentRealTime[n], (speed_index == n))) { + speed_index = std::clamp(n, 0, kPercentRealTime.size() - 1); + float speed = std::stof(kPercentRealTime[speed_index]); + step_control->SetSpeed(speed); + } + } + ImGui::EndCombo(); + } + + ImGui::PopStyleVar(); + if (misaligned) { + ImGui::SetItemTooltip("%s", "Desired Speed (Measured Speed)"); + } else { + ImGui::SetItemTooltip("%s", "Desired Speed"); + } +} + bool ThemeSelectGui(GuiTheme* theme) { static constexpr const char* ICON_DARKMODE = ICON_FA_CIRCLE; static constexpr const char* ICON_LIGHTMODE = ICON_FA_CIRCLE_O; diff --git a/src/experimental/platform/ux/gui.h b/src/experimental/platform/ux/gui.h index cfa8fd80..938466bb 100644 --- a/src/experimental/platform/ux/gui.h +++ b/src/experimental/platform/ux/gui.h @@ -23,10 +23,12 @@ // by the caller. In most cases, this is already stored in mjModel, mjData, // mjvOption, etc. But, some functions take additional arguments as needed. +#include #include #include #include +#include "experimental/platform/sim/step_control.h" namespace mujoco::platform { @@ -62,6 +64,22 @@ void SetupTheme(GuiTheme theme); // be used to place additional elements (e.g. floating charts). ImVec4 ConfigureDockingLayout(); +// logarithmically spaced real-time slow-down coefficients (percent) +// clang-format off +static constexpr std::array kPercentRealTime = { +"100.0 ", " 80.0 ", " 66.0 ", " 50.0 ", " 40.0 ", " 33.0 ", " 25.0 ", " 20.0 ", " 16.0 ", " 13.0 ", +" 10.0 ", " 8.0 ", " 6.6 ", " 5.0 ", " 4.0 ", " 3.3 ", " 2.5 ", " 2.0 ", " 1.6 ", " 1.3 ", +" 1.0 ", " 0.8 ", " 0.7 ", " 0.5 ", " 0.4 ", " 0.33", " 0.25", " 0.2 ", " 0.16", " 0.13", +" 0.1 ", +}; +// clang-format on + +// UX for controlling the simulation stepping. `speed_index` is an index into +// kPercentRealTime, an array of available speeds (indices in range [0, 30] map +// to real-time percentages in range [100%, 0.1%]). +void StepControlGui(const mjModel* model, StepControl* step_control, + int& speed_index); + // UX for selecting the GUI theme. bool ThemeSelectGui(GuiTheme* theme); diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index b9cf91fb..9937e8ab 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -72,9 +71,6 @@ static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) { // TODO: update selected element! } -static constexpr const char* ICON_PLAY = platform::ICON_FA_PLAY; -static constexpr const char* ICON_PAUSE = platform::ICON_FA_PAUSE; -static constexpr const char* ICON_VISCOUS_PAUSE = platform::ICON_FA_MAGIC; static constexpr const char* ICON_COPY_CAMERA = platform::ICON_FA_COPY; static constexpr const char* ICON_UNLOAD_MODEL = platform::ICON_FA_EJECT; static constexpr const char* ICON_RELOAD_MODEL = platform::ICON_FA_REFRESH; @@ -82,21 +78,10 @@ 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_SPEED = platform::ICON_FA_TACHOMETER; static constexpr const char* ICON_RELOAD_SPEC = platform::ICON_FA_REFRESH; static constexpr const char* ICON_UNDO_SPEC = platform::ICON_FA_UNDO; static constexpr const char* ICON_REDO_SPEC = platform::ICON_FA_REPEAT; -// logarithmically spaced real-time slow-down coefficients (percent) -// clang-format off -static constexpr std::array kPercentRealTime = { -"100.0 ", " 80.0 ", " 66.0 ", " 50.0 ", " 40.0 ", " 33.0 ", " 25.0 ", " 20.0 ", " 16.0 ", " 13.0 ", -" 10.0 ", " 8.0 ", " 6.6 ", " 5.0 ", " 4.0 ", " 3.3 ", " 2.5 ", " 2.0 ", " 1.6 ", " 1.3 ", -" 1.0 ", " 0.8 ", " 0.7 ", " 0.5 ", " 0.4 ", " 0.33", " 0.25", " 0.2 ", " 0.16", " 0.13", -" 0.1 ", -}; -// clang-format on - App::App(Config config) : ini_path_(std::move(config.ini_path)), gfx_mode_(config.gfx_mode) { SwitchGraphicsMode(config.width, config.height, config.gfx_mode); @@ -221,8 +206,8 @@ void App::OnModelLoaded(std::string filename, ModelKind model_kind) { // 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 < kPercentRealTime.size(); ++i) { - const float speed = std::stof(kPercentRealTime[i]); + 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; @@ -613,14 +598,13 @@ void App::HandleKeyboardEvents() { } } else if (ImGui_IsChordJustPressed(ImGuiMod_Ctrl | ImGuiKey_Space)) { if (step_control_.GetPauseState() == PauseState::kViscousPaused) { - step_control_.SetPauseState(PauseState::kUnpaused, model()); + step_control_.SetPauseState(PauseState::kUnpaused); } else { - step_control_.SetPauseState(PauseState::kViscousPaused, model()); - tmp_.viscous_pause_time = ImGui::GetTime(); + step_control_.SetPauseState(PauseState::kViscousPaused); } } else if (ImGui_IsChordJustPressed(ImGuiKey_Space)) { if (step_control_.GetPauseState() == PauseState::kViscousPaused) { - step_control_.SetPauseState(PauseState::kNormalPaused, model()); + step_control_.SetPauseState(PauseState::kNormalPaused); } else if (step_control_.GetPauseState() == PauseState::kUnpaused) { step_control_.SetPauseState(PauseState::kNormalPaused); } else { @@ -817,12 +801,13 @@ void App::SaveSettings() { } void App::SetSpeedIndex(int idx) { - if (idx == tmp_.speed_index || kPercentRealTime.empty()) { + if (idx == tmp_.speed_index || platform::kPercentRealTime.empty()) { return; } - tmp_.speed_index = std::clamp(idx, 0, kPercentRealTime.size() - 1); - float speed = std::stof(kPercentRealTime[tmp_.speed_index]); + tmp_.speed_index = + std::clamp(idx, 0, platform::kPercentRealTime.size() - 1); + float speed = std::stof(platform::kPercentRealTime[tmp_.speed_index]); step_control_.SetSpeed(speed); } @@ -1407,25 +1392,10 @@ void App::HelpGui() { ImGui::Columns(); } -struct SpeedStatus { - bool misaligned; - float measured; -}; - -static SpeedStatus IsSpeedMisaligned( - const platform::StepControl& step_control) { - const float desired = step_control.GetSpeed(); - const float measured = step_control.GetSpeedMeasured(); - return {std::abs(measured - desired) > 0.1f * desired, measured}; -} - void App::ToolBarGui() { if (ImGui::BeginTable("##ToolBarTable", 2)) { platform::ScopedStyle style; const ImColor red(220, 40, 40, 255); - const ImColor green(40, 180, 40, 255); - const ImColor yellow(250, 230, 10, 255); - const int combo_flags = ImGuiComboFlags_NoArrowButton; const float scale = ImGui::GetWindowDpiScale(); const ImVec2 button_size(48.f * scale, 32.f * scale); @@ -1481,82 +1451,10 @@ void App::ToolBarGui() { } ImGui::SetItemTooltip("%s", "Reset"); - // Combined (Normal Pause, Viscous Pause, Play) widget - { - style.Var(ImGuiStyleVar_FrameRounding, 2.0f); - // Normal pause button. - ImGui::SameLine(0, separator_width); - ImColor paused_color = yellow; - bool paused = step_control_.GetPauseState() == PauseState::kNormalPaused; - if (platform::ImGui_ColorButton(ICON_PAUSE, paused, paused_color, - button_size)) { - if (!paused) { - step_control_.SetPauseState(PauseState::kNormalPaused, model()); - } - } - ImGui::SetItemTooltip("%s", "Pause"); - - // Viscous pause button. - ImGui::SameLine(0, 0); - ImColor vpaused_color = green; - float t = 0.f; - bool vpaused = - step_control_.GetPauseState() == PauseState::kViscousPaused; - if (vpaused) { - t = ImGui::GetTime() - tmp_.viscous_pause_time; - t = std::sqrt(std::min(t / 0.75f, 1.0f)); - vpaused_color = ImColor(ImLerp(green.Value, yellow.Value, t)); - } - if (platform::ImGui_ColorButton(ICON_VISCOUS_PAUSE, vpaused, - vpaused_color, button_size, - t < 1.0f ? 1.0f : 0.5f)) { - if (!vpaused) { - step_control_.SetPauseState(PauseState::kViscousPaused, model()); - tmp_.viscous_pause_time = ImGui::GetTime(); - } - } - ImGui::SetItemTooltip("%s", "Viscous Pause"); - - // Play button. - ImGui::SameLine(0, 0); - if (platform::ImGui_ColorButton( - ICON_PLAY, step_control_.GetPauseState() == PauseState::kUnpaused, - green, button_size, 0.6f)) { - step_control_.SetPauseState(PauseState::kUnpaused, model()); - } - } - - // Speed selection. - ImGui::SameLine(); - float pad_y = (button_size.y - ImGui::GetFontSize()) * 0.5f; - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, - ImVec2(ImGui::GetStyle().FramePadding.x + 5.f, pad_y)); - const auto [misaligned, measured] = IsSpeedMisaligned(step_control_); - char speed_preview[64]; - if (misaligned) { - snprintf(speed_preview, sizeof(speed_preview), "%s%s (%-4.1f%%)", - ICON_SPEED, kPercentRealTime[tmp_.speed_index], measured); - } else { - snprintf(speed_preview, sizeof(speed_preview), "%s%s", ICON_SPEED, - kPercentRealTime[tmp_.speed_index]); - } - ImGui::SetNextItemWidth(ImGui::CalcTextSize(speed_preview).x + - ImGui::GetStyle().FramePadding.x * 2); - if (ImGui::BeginCombo("##Speed", speed_preview, combo_flags)) { - for (int n = 0; n < kPercentRealTime.size(); n++) { - if (ImGui::Selectable(kPercentRealTime[n], (tmp_.speed_index == n))) { - SetSpeedIndex(n); - } - } - ImGui::EndCombo(); - } - ImGui::PopStyleVar(); - if (misaligned) { - ImGui::SetItemTooltip("%s", "Desired Speed (Measured Speed)"); - } else { - ImGui::SetItemTooltip("%s", "Desired Speed"); - } + // Combined (Normal Pause, Viscous Pause, Play) widget and Speed selection. + ImGui::SameLine(0, separator_width); + platform::StepControlGui(model(), &step_control_, tmp_.speed_index); ImGui::TableNextColumn(); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + @@ -1709,7 +1607,7 @@ void App::MainMenuGui() { if (step_control_.GetPauseState() != PauseState::kNormalPaused) { step_control_.SetPauseState(PauseState::kNormalPaused); } else { - step_control_.SetPauseState(PauseState::kUnpaused, model()); + step_control_.SetPauseState(PauseState::kUnpaused); } } if (ImGui::MenuItem("Reset", "Backspace")) { diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index 222cdf73..2a1c7326 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -133,9 +133,6 @@ class App { // Controls. bool perturb_active = false; - // Time at which viscous pause was activated, for the green→yellow button - // color animation. - double viscous_pause_time = 0; int speed_index = 0; float cam_speed = 0.0f;