Cleanup Studio toolbar

PiperOrigin-RevId: 872618794
Change-Id: I2e8f99dc47937416c8516ed1df7821369211b12f
This commit is contained in:
Matija Kecman
2026-02-19 16:33:52 -08:00
committed by Copybara-Service
parent 8ba68cee3e
commit ca0552c83b
2 changed files with 140 additions and 228 deletions
@@ -28,11 +28,14 @@
namespace mujoco::platform {
// FontAwesome icon codes.
static constexpr const char ICON_FA_ADJUST[] = "\xEF\x81\x82";
static constexpr const char ICON_FA_ARROWS[] = "\xEF\x81\x87";
static constexpr const char ICON_FA_CAMERA[] = "\xEF\x80\xBD";
static constexpr const char ICON_FA_CARET_LEFT[] = "\xEF\x83\x99";
static constexpr const char ICON_FA_CARET_RIGHT[] = "\xEF\x83\x9A";
static constexpr const char ICON_FA_CHECK_SQUARE_O[] = "\xEF\x81\x9D";
static constexpr const char ICON_FA_CIRCLE[] = "\xEF\x84\x91";
static constexpr const char ICON_FA_CIRCLE_O[] = "\xEF\x84\x8C";
static constexpr const char ICON_FA_COMMENT[] = "\xEF\x83\xA5";
static constexpr const char ICON_FA_COPY[] = "\xEF\x83\x85";
static constexpr const char ICON_FA_DIAMOND[] = "\xEF\x88\x99";
+137 -228
View File
@@ -24,7 +24,6 @@
#include <cstring>
#include <filesystem>
#include <memory>
#include <random>
#include <span>
#include <string>
#include <string_view>
@@ -78,14 +77,13 @@ static constexpr const char* ICON_LABEL = platform::ICON_FA_COMMENT;
static constexpr const char* ICON_RESET_MODEL = platform::ICON_FA_UNDO;
static constexpr const char* ICON_FRAME = platform::ICON_FA_ARROWS;
static constexpr const char* ICON_CAMERA = platform::ICON_FA_CAMERA;
static constexpr const char* ICON_DARKMODE = platform::ICON_FA_MOON;
static constexpr const char* ICON_LIGHTMODE = platform::ICON_FA_SUN;
static constexpr const char* ICON_CLASSICMODE = platform::ICON_FA_DIAMOND;
static constexpr const char* ICON_DARKMODE = platform::ICON_FA_CIRCLE;
static constexpr const char* ICON_LIGHTMODE = platform::ICON_FA_CIRCLE_O;
static constexpr const char* ICON_CLASSICMODE = platform::ICON_FA_ADJUST;
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_DELETE = platform::ICON_FA_TRASH_CAN;
// UI labels for mjtLabel.
static constexpr const char* kLabelNames[] = {
@@ -107,8 +105,7 @@ static constexpr std::array<const char*, 31> kPercentRealTime = {
};
// clang-format on
App::App(Config config)
: rng_(std::random_device()()), ini_path_(std::move(config.ini_path)) {
App::App(Config config) : ini_path_(std::move(config.ini_path)) {
platform::Window::Config window_config;
window_config.renderer_backend = platform::Renderer::GetBackend();
window_config.offscreen_mode = config.offscreen_mode;
@@ -135,13 +132,6 @@ void App::ClearModel() {
step_error_ = "";
}
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);
history_.Init(state_size);
}
void App::RequestModelLoad(std::string model_file) {
pending_load_ = std::move(model_file);
}
@@ -149,7 +139,6 @@ void App::RequestModelLoad(std::string model_file) {
void App::RequestModelReload() {
if (model_kind_ == kModelFromFile) {
pending_load_ = model_path_;
preserve_camera_on_load_ = true;
}
}
@@ -200,16 +189,6 @@ void App::OnModelLoaded(std::string filename, ModelKind model_kind) {
const int state_size = mj_stateSize(model, mjSTATE_INTEGRATION);
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);
@@ -386,11 +365,6 @@ void App::ProcessPendingLoads() {
}
}
if (spec_op_) {
spec_op_();
spec_op_ = nullptr;
}
// Check plugins to see if we need to load a new model.
platform::ForEachModelPlugin([&](platform::ModelPlugin* plugin) {
if (plugin->get_model_to_load) {
@@ -408,19 +382,6 @@ void App::ProcessPendingLoads() {
});
}
void App::SpecDeleteSelectedElement() {
spec_op_ = [this]() {
mjs_delete(spec(), tmp_.element);
if (tmp_.element->elemtype == mjOBJ_BODY &&
perturb_.select == tmp_.element_id) {
mjv_defaultPerturb(&perturb_);
}
tmp_.element = nullptr;
tmp_.element_id = -1;
Recompile();
};
}
void App::HandleWindowEvents() {
const std::string drop_file = window_->GetDropFile();
if (!drop_file.empty()) {
@@ -513,28 +474,15 @@ void App::HandleMouseEvents() {
perturb_.flexselect = picked.flex;
perturb_.skinselect = picked.skin;
// Select the corresponding element in the spec.
tmp_.element = nullptr;
tmp_.element_id = -1;
if (has_spec()) {
mjsElement* element = mjs_firstElement(spec(), mjOBJ_BODY);
while (element) {
if (mjs_getId(element) == picked.body) {
tmp_.element = element;
tmp_.element_id = picked.body;
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_);
perturb_.select = 0;
perturb_.flexselect = -1;
perturb_.skinselect = -1;
}
}
@@ -623,8 +571,6 @@ void App::HandleKeyboardEvents() {
}
} else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) {
ResetPhysics();
} else if (ImGui_IsChordJustPressed(ImGuiKey_Delete)) {
SpecDeleteSelectedElement();
} else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) {
SelectParentPerturb(model(), perturb_);
} else if (ImGui_IsChordJustPressed(ImGuiKey_F1)) {
@@ -715,61 +661,6 @@ void App::HandleKeyboardEvents() {
ToggleFlag(vis_options_.geomgroup[4]);
} else if (ImGui_IsChordJustPressed(ImGuiKey_5)) {
ToggleFlag(vis_options_.geomgroup[5]);
} else if (ImGui_IsChordJustPressed(ImGuiKey_Enter | ImGuiMode_CtrlShift)) {
if (has_spec()) {
spec_op_ = [this]() {
mjsBody* world = mjs_findBody(spec(), "world");
if (!world) return;
mjsBody* body = mjs_addBody(world, nullptr);
if (!body) return;
mjsJoint* joint = mjs_addJoint(body, nullptr);
if (!joint) return;
mjsGeom* geom = mjs_addGeom(body, nullptr);
if (!geom) return;
// Set body position slightly in front of the camera.
mjtNum pos[3];
mjtNum dir[3];
mjtNum up[3];
mjv_cameraFrame(pos, dir, up, nullptr, data(), &camera_);
static int counter = 0;
std::string name = "projectile" + std::to_string(counter++);
mjs_setName(body->element, name.c_str());
body->mass = 10.0;
body->pos[0] = pos[0] + dir[0] * 0.2;
body->pos[1] = pos[1] + dir[1] * 0.2;
body->pos[2] = pos[2] + dir[2] * 0.2;
geom->type = mjGEOM_BOX;
geom->size[0] = 0.13365;
geom->size[1] = 0.13365;
geom->size[2] = 0.13365;
geom->rgba[0] = std::uniform_real_distribution<float>(0.3f, 1.0f)(rng_);
geom->rgba[1] = std::uniform_real_distribution<float>(0.3f, 1.0f)(rng_);
geom->rgba[2] = std::uniform_real_distribution<float>(0.3f, 1.0f)(rng_);
geom->rgba[3] = 1.0;
joint->type = mjJNT_FREE;
Recompile();
// Give the newly added body a velocity in the direction of the camera.
int bodyid = mj_name2id(model(), mjOBJ_BODY, name.c_str());
if (bodyid >= 0) {
int jntid = model()->body_jntadr[bodyid];
if (jntid >= 0 && model()->jnt_type[jntid] == mjJNT_FREE) {
int qveladr = model()->jnt_dofadr[jntid];
if (qveladr >= 0) {
mjtNum speed = 10.0; // Magnitude of the initial velocity.
data()->qvel[qveladr + 0] = dir[0] * speed + up[0];
data()->qvel[qveladr + 1] = dir[1] * speed + up[1];
data()->qvel[qveladr + 2] = dir[2] * speed + up[2];
}
}
}
};
}
} else if (has_model()) {
if (ImGui_IsChordJustPressed(ImGuiKey_Escape)) {
ui_.camera_idx =
@@ -891,10 +782,16 @@ void App::BuildGui() {
MainMenuGui();
if (ImGui::Begin("ToolBar")) {
ToolBarGui();
{
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
if (ImGui::Begin("ToolBar")) {
ImGui::PopStyleVar();
ToolBarGui();
} else {
ImGui::PopStyleVar();
}
ImGui::End();
}
ImGui::End();
{
platform::ScopedStyle style;
@@ -1191,8 +1088,7 @@ void App::SpecExplorerGui() {
label = "(" + prefix + " " + std::to_string(id) + ")";
}
const bool selected = (tmp_.element == element);
if (ImGui::Selectable(label.c_str(), selected)) {
if (ImGui::Selectable(label.c_str(), false)) {
tmp_.element = element;
tmp_.element_id = id;
}
@@ -1202,24 +1098,42 @@ void App::SpecExplorerGui() {
};
if (ImGui::TreeNodeEx("Bodies", flags)) {
display_group(mjOBJ_BODY, "Body");
// We don't use `display_group` here because we do additional selection
// logic tied to the `perturb_` field.
mjsElement* element = mjs_firstElement(spec(), mjOBJ_BODY);
while (element) {
const int id = mjs_getId(element);
const mjString* name = mjs_getName(element);
std::string label = *name;
if (label.empty()) {
label = "(Body " + std::to_string(id) + ")";
}
if (ImGui::Selectable(label.c_str(), (id == perturb_.select),
ImGuiSelectableFlags_AllowDoubleClick)) {
tmp_.element = element;
tmp_.element_id = id;
}
if (ImGui::IsItemHovered() &&
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
perturb_.select = id;
}
element = mjs_nextElement(spec(), element);
}
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Joints", flags)) {
display_group(mjOBJ_JOINT, "Joint");
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Sites", flags)) {
display_group(mjOBJ_SITE, "Site");
ImGui::TreePop();
}
// If we selected a body, then select the same body for the perturb object.
if (tmp_.element && tmp_.element->elemtype == mjOBJ_BODY &&
perturb_.select != tmp_.element_id) {
mjv_defaultPerturb(&perturb_);
perturb_.select = tmp_.element_id;
}
}
void App::PropertiesGui() {
@@ -1228,31 +1142,22 @@ void App::PropertiesGui() {
return;
}
if (ImGui::BeginTable("##PropertiesHeader", 2)) {
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 20);
ImGui::TableNextColumn();
ImGui::Text("%s", mju_type2Str(tmp_.element->elemtype));
ImGui::TableNextColumn();
if (tmp_.element->elemtype == mjOBJ_BODY) {
if (ImGui::SmallButton(ICON_DELETE)) {
SpecDeleteSelectedElement();
}
}
ImGui::EndTable();
}
ImGui::Separator();
switch (tmp_.element->elemtype) {
case mjOBJ_BODY:
ImGui::Text("Body");
ImGui::Separator();
platform::BodyPropertiesGui(model(), data(), tmp_.element,
tmp_.element_id);
break;
case mjOBJ_JOINT:
ImGui::Text("Joint");
ImGui::Separator();
platform::JointPropertiesGui(model(), data(), tmp_.element,
tmp_.element_id);
break;
case mjOBJ_SITE:
ImGui::Text("Site");
ImGui::Separator();
platform::SitePropertiesGui(model(), data(), tmp_.element,
tmp_.element_id);
break;
@@ -1375,6 +1280,18 @@ 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;
@@ -1384,22 +1301,30 @@ void App::ToolBarGui() {
const int combo_flags = ImGuiComboFlags_NoArrowButton;
const float scale = ImGui::GetWindowDpiScale();
const float right_width = 520.f * scale;
const ImVec2 button_size(48.f * scale, 32.f * scale);
const ImVec2 play_button_size(80.f * scale, 32.f * scale);
const float label_width = GetExpectedLabelWidth();
const float copy_btn_width = ImGui::CalcTextSize(ICON_COPY_CAMERA).x +
ImGui::GetStyle().FramePadding.x * 2;
const float theme_width = ImGui::CalcTextSize(ICON_LIGHTMODE).x +
ImGui::GetStyle().FramePadding.x * 2;
const float sp = ImGui::GetStyle().ItemSpacing.x;
const float right_width = label_width + sp + label_width + sp +
label_width + sp + copy_btn_width + sp +
theme_width;
const float separator_width = .2f * button_size.x;
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, right_width);
ImGui::TableNextColumn();
ImGui::Text("%s", " ");
// Combined (Unload, Reload) widget
{
style.Var(ImGuiStyleVar_FrameRounding, 2.0f);
// Unload button.
ImGui::SameLine();
{
const ImColor a = red;
const ImColor h(a.Value.x, a.Value.y, a.Value.z, a.Value.w * 0.6f);
@@ -1421,25 +1346,19 @@ void App::ToolBarGui() {
ImGui::SetItemTooltip("%s", "Reload");
}
ImGui::SameLine(0, 0);
ImGui::Text(" ");
// Reset button.
ImGui::SameLine();
ImGui::SameLine(0, separator_width);
if (ImGui::Button(ICON_RESET_MODEL, button_size)) {
ResetPhysics();
}
ImGui::SetItemTooltip("%s", "Reset");
ImGui::SameLine(0, 0);
ImGui::Text(" ");
// Combined (Normal Pause, Viscous Pause, Play) widget
{
style.Var(ImGuiStyleVar_FrameRounding, 2.0f);
// Normal pause button.
ImGui::SameLine();
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,
@@ -1480,18 +1399,23 @@ void App::ToolBarGui() {
}
}
ImGui::SameLine();
ImGui::Text("%s", " |");
// Speed selection.
ImGui::SameLine();
ImGui::Text("%s", ICON_SPEED);
ImGui::SetItemTooltip("%s", "Playback Speed");
ImGui::SameLine();
ImGui::SetNextItemWidth(50.0f * scale);
if (ImGui::BeginCombo("##Speed", kPercentRealTime[tmp_.speed_index],
combo_flags)) {
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);
@@ -1499,17 +1423,30 @@ void App::ToolBarGui() {
}
ImGui::EndCombo();
}
ImGui::SetItemTooltip("%s", "Playback Speed");
ImGui::PopStyleVar();
if (misaligned) {
ImGui::SetItemTooltip("%s", "Desired Speed (Measured Speed)");
} else {
ImGui::SetItemTooltip("%s", "Desired Speed");
}
ImGui::TableNextColumn();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() +
(button_size.y - ImGui::GetFrameHeight()) * 0.5f);
// Camera selection.
std::vector<const char*> cameras = GetCameraNames();
ImGui::TableNextColumn();
ImGui::Text("%s", ICON_CAMERA);
ImGui::SetItemTooltip("%s", "Camera");
ImGui::SameLine();
if (ImGui::Button(ICON_COPY_CAMERA)) {
std::string camera_string = platform::CameraToString(data(), &camera_);
platform::MaybeSaveToClipboard(camera_string);
}
ImGui::SetItemTooltip("%s", "Copy Camera");
ImGui::SameLine(0, 0);
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
int camera_idx = ui_.camera_idx - platform::kTumbleCameraIdx;
if (ImGui::BeginCombo("##Camera", cameras[camera_idx], combo_flags)) {
std::vector<const char*> cameras = GetCameraNames();
std::string camera_preview =
std::string(ICON_CAMERA) + " " + cameras[camera_idx];
if (ImGui::BeginCombo("##Camera", camera_preview.c_str(), combo_flags)) {
for (int n = 0; n < cameras.size(); n++) {
if (ImGui::Selectable(cameras[n], (camera_idx == n))) {
ui_.camera_idx = platform::SetCamera(model(), &camera_,
@@ -1519,25 +1456,13 @@ void App::ToolBarGui() {
ImGui::EndCombo();
}
ImGui::SetItemTooltip("%s", "Camera");
ImGui::SameLine();
if (ImGui::Button(ICON_COPY_CAMERA)) {
std::string camera_string = platform::CameraToString(data(), &camera_);
platform::MaybeSaveToClipboard(camera_string);
}
ImGui::SetItemTooltip("%s", "Copy Camera");
ImGui::SameLine();
ImGui::Text("%s", " |");
// Label selection.
ImGui::SameLine();
ImGui::Text("%s", ICON_LABEL);
ImGui::SetItemTooltip("%s", "Label");
ImGui::SameLine();
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
if (ImGui::BeginCombo("##Label", kLabelNames[vis_options_.label],
combo_flags)) {
std::string label_preview =
std::string(ICON_LABEL) + " " + kLabelNames[vis_options_.label];
if (ImGui::BeginCombo("##Label", label_preview.c_str(), combo_flags)) {
for (int n = 0; n < IM_ARRAYSIZE(kLabelNames); n++) {
if (ImGui::Selectable(kLabelNames[n], (vis_options_.label == n))) {
vis_options_.label = n;
@@ -1547,18 +1472,12 @@ void App::ToolBarGui() {
}
ImGui::SetItemTooltip("%s", "Label");
ImGui::SameLine();
ImGui::Text("%s", " |");
// Frame selection.
ImGui::SameLine();
ImGui::Text("%s", ICON_FRAME);
ImGui::SetItemTooltip("%s", "Frame");
ImGui::SameLine();
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
if (ImGui::BeginCombo("##Frame", kFrameNames[vis_options_.frame],
combo_flags)) {
std::string frame_preview =
std::string(ICON_FRAME) + " " + kFrameNames[vis_options_.frame];
if (ImGui::BeginCombo("##Frame", frame_preview.c_str(), combo_flags)) {
for (int n = 0; n < IM_ARRAYSIZE(kFrameNames); n++) {
if (ImGui::Selectable(kFrameNames[n], (vis_options_.frame == n))) {
vis_options_.frame = n;
@@ -1568,31 +1487,31 @@ void App::ToolBarGui() {
}
ImGui::SetItemTooltip("%s", "Frame");
// Theme selection.
ImGui::SameLine();
ImGui::Text("%s", " |");
// Style selection.
ImGui::SameLine();
switch (ui_.theme) {
case platform::GuiTheme::kLight:
if (ImGui::Button(ICON_LIGHTMODE)) {
SetupTheme(platform::GuiTheme::kDark);
const char* theme_icons[] = {ICON_LIGHTMODE, ICON_DARKMODE,
ICON_CLASSICMODE};
const char* theme_tooltips[] = {"Light Mode", "Dark Mode", "Classic Mode"};
const platform::GuiTheme theme_values[] = {
platform::GuiTheme::kLight,
platform::GuiTheme::kDark,
platform::GuiTheme::kClassic,
};
int theme_idx = static_cast<int>(ui_.theme);
ImGui::SetNextItemWidth(ImGui::CalcTextSize(theme_icons[0]).x +
ImGui::GetStyle().FramePadding.x * 2);
if (ImGui::BeginCombo("##Theme", theme_icons[theme_idx], combo_flags)) {
for (int n = 0; n < IM_ARRAYSIZE(theme_icons); n++) {
if (ImGui::Selectable(theme_icons[n], (theme_idx == n))) {
SetupTheme(theme_values[n]);
}
ImGui::SetItemTooltip("%s", "Switch to Dark Mode");
break;
case platform::GuiTheme::kDark:
if (ImGui::Button(ICON_DARKMODE)) {
SetupTheme(platform::GuiTheme::kClassic);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", theme_tooltips[n]);
}
ImGui::SetItemTooltip("%s", "Switch to Classic Mode");
break;
case platform::GuiTheme::kClassic:
if (ImGui::Button(ICON_CLASSICMODE)) {
SetupTheme(platform::GuiTheme::kLight);
}
ImGui::SetItemTooltip("%s", "Switch to Light Mode");
break;
}
ImGui::EndCombo();
}
ImGui::SetItemTooltip("%s", "Theme");
ImGui::EndTable();
}
@@ -1613,17 +1532,7 @@ void App::StatusBarGui() {
} else if (step_control_.GetPauseState() == PauseState::kNormalPaused) {
ImGui::Text("Paused");
} else {
const float desired_realtime = step_control_.GetSpeed();
const float measured_realtime = step_control_.GetSpeedMeasured();
const float realtime_offset =
mju_abs(measured_realtime - desired_realtime);
const bool misaligned = realtime_offset > 0.1 * desired_realtime;
if (misaligned) {
ImGui::Text("Running: %g%% (%-4.1f%%)", desired_realtime,
measured_realtime);
} else {
ImGui::Text("Running: %g%%", desired_realtime);
}
ImGui::Text("Running");
}
if (!step_error_.empty()) {