Make StepControlGui a reusable component and refactor/simplify the implementation
PiperOrigin-RevId: 900045404 Change-Id: I9976ece042710ddce0a7c5d99a6ac85a0ef06d2f
This commit is contained in:
committed by
Copybara-Service
parent
d913e0b1e9
commit
9289905c9a
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
#include "experimental/platform/ux/gui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
@@ -26,10 +28,23 @@
|
||||
#include <implot.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#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<int>(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;
|
||||
|
||||
@@ -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 <array>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#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<const char*, 31> 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);
|
||||
|
||||
|
||||
+13
-115
@@ -17,7 +17,6 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -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<const char*, 31> 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<int>(idx, 0, kPercentRealTime.size() - 1);
|
||||
float speed = std::stof(kPercentRealTime[tmp_.speed_index]);
|
||||
tmp_.speed_index =
|
||||
std::clamp<int>(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")) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user