Files
Mujoco_WASM/src/experimental/platform/ux/gui.cc
T
Matija Kecman 58d6910afa Refactor StepControl and GUI to return change status.
StepControl methods SetSpeed, SetNoiseParameters, and SetPauseState now return a boolean indicating whether the state was actually changed. The GUI functions NoiseGui and StepControlGui also return a boolean reflecting if any parameters were modified. Noise parameters are now directly managed by StepControl rather than being stored in UxState. The StepControlEvent message has been expanded to include all step control parameters. Keyboard event handling has been slightly refactored to better align with event-based updates.

PiperOrigin-RevId: 940656342
Change-Id: I3701623a2fdcfc4c084b5db0ae19048669040de4
2026-06-30 14:21:00 -07:00

1619 lines
59 KiB
C++

// 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/platform/ux/gui.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <limits>
#include <string>
#include <string_view>
#include <vector>
#include <imgui.h>
#include <imgui_internal.h>
#include <implot.h>
#include <mujoco/mujoco.h>
#include "experimental/platform/helpers.h"
#include "experimental/platform/sim/sim_profiler.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 = (GetStableAvailWidth() / num_cols) -
ImGui::GetStyle().FramePadding.x * 2;
return ImVec2(width, 0);
}
bool SectionHeader(const char* label, ImGuiTreeNodeFlags flags, float arrow_scale) {
const bool is_framed = (flags & ImGuiTreeNodeFlags_Framed) != 0;
ImGuiContext& g = *GImGui;
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems) return false;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
// Control the bar height via FramePadding.y.
// Main (framed): a bit of padding. Sub (unframed): less padding to be visually thinner.
const float pad_y = (is_framed ? 3.0f : 2.0f) * style.FontScaleDpi;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
ImVec2(style.FramePadding.x, pad_y));
// Push transparent text color to hide the default arrow and label rendered by
// TreeNodeBehavior.
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 0, 0));
// Record cursor position before the widget so we can compute the arrow position.
const ImVec2 cursor_before = ImGui::GetCursorScreenPos();
// Use TreeNodeBehavior (the same function CollapsingHeader uses internally).
// This gives us proper click-to-toggle and open/close state management.
// We do NOT add _Leaf here so toggling works, and we don't add _NoTreePushOnOpen
// so the callers' TreePop() still works.
//
// For unframed nodes, add _FramePadding so TreeNodeBehavior uses FramePadding.y
// for layout (making arrow Y position predictable = cursor.y + pad_y).
// Also add _SpanAvailWidth for consistent full-width hit testing.
ImGuiTreeNodeFlags effective_flags = flags;
if (!is_framed) {
effective_flags |= ImGuiTreeNodeFlags_FramePadding |
ImGuiTreeNodeFlags_SpanAvailWidth;
}
bool is_open = ImGui::TreeNodeBehavior(id, effective_flags, label);
// Restore style state before manual rendering (so we get the correct text color).
ImGui::PopStyleColor();
ImGui::PopStyleVar();
// --- Render custom arrow and text manually ---
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImU32 text_col = ImGui::GetColorU32(ImGuiCol_Text);
// Match position offsets used by TreeNodeBehavior.
// Arrow coordinate: (cursor_before.x + FramePadding.x, cursor_before.y + pad_y)
const float pad_x = style.FramePadding.x;
const float arrow_x = cursor_before.x + pad_x;
const float arrow_y = cursor_before.y + pad_y;
const float full_arrow_size = g.FontSize;
// Draw the custom smaller arrow, centered within the same region.
const float custom_size = full_arrow_size * arrow_scale;
const float offset = (full_arrow_size - custom_size) * 0.5f;
const ImGuiDir arrow_dir = is_open ? ImGuiDir_Down : ImGuiDir_Right;
ImGui::RenderArrow(dl, ImVec2(arrow_x + offset, arrow_y + offset),
text_col, arrow_dir, arrow_scale);
// Render text manually.
// text_offset_x = FontSize + padding.x * (display_frame ? 3 : 2)
const float text_offset_x = full_arrow_size + pad_x * (is_framed ? 3.0f : 2.0f);
ImGui::RenderText(ImVec2(cursor_before.x + text_offset_x, cursor_before.y + pad_y), label);
return is_open;
}
void SetupTheme(GuiTheme theme) {
ImGuiStyle& s = ImGui::GetStyle();
ImVec4* c = s.Colors;
if (theme == GuiTheme::kDark) {
ImGui::StyleColorsDark(&s);
// Cool slate backgrounds with subtle blue undertone.
c[ImGuiCol_Text] = ImVec4(0.92, 0.93, 0.95, 1.00);
c[ImGuiCol_TextDisabled] = ImVec4(0.45, 0.47, 0.50, 1.00);
c[ImGuiCol_WindowBg] = ImVec4(0.16, 0.17, 0.19, 1.00);
c[ImGuiCol_ChildBg] = ImVec4(0.16, 0.17, 0.19, 1.00);
c[ImGuiCol_PopupBg] = ImVec4(0.24, 0.25, 0.28, 1.00);
c[ImGuiCol_Border] = ImVec4(0.10, 0.10, 0.12, 0.40);
c[ImGuiCol_BorderShadow] = ImVec4(0.00, 0.00, 0.00, 0.00);
// Input fields: subtle inset, no transparency.
c[ImGuiCol_FrameBg] = ImVec4(0.22, 0.23, 0.26, 1.00);
c[ImGuiCol_FrameBgHovered] = ImVec4(0.26, 0.27, 0.30, 1.00);
c[ImGuiCol_FrameBgActive] = ImVec4(0.30, 0.31, 0.35, 1.00);
// Title bars: darkest layer.
c[ImGuiCol_TitleBg] = ImVec4(0.12, 0.13, 0.15, 1.00);
c[ImGuiCol_TitleBgActive] = ImVec4(0.14, 0.15, 0.17, 1.00);
c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.12, 0.13, 0.15, 0.90);
c[ImGuiCol_MenuBarBg] = ImVec4(0.12, 0.13, 0.15, 1.00);
// Scrollbars: transparent background, visible thumb only.
c[ImGuiCol_ScrollbarBg] = ImVec4(0.00, 0.00, 0.00, 0.00);
c[ImGuiCol_ScrollbarGrab] = ImVec4(0.34, 0.35, 0.38, 1.00);
c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.42, 0.43, 0.46, 1.00);
c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.55, 0.56, 0.60, 1.00);
// Interactive elements.
c[ImGuiCol_CheckMark] = ImVec4(0.35, 0.50, 0.72, 1.00);
c[ImGuiCol_SliderGrab] = ImVec4(0.28, 0.40, 0.58, 1.00);
c[ImGuiCol_SliderGrabActive] = ImVec4(0.35, 0.50, 0.72, 1.00);
// Buttons: slightly lighter than frame background (0.22) to provide subtle but visible contrast.
c[ImGuiCol_Button] = ImVec4(0.28, 0.29, 0.33, 1.00);
c[ImGuiCol_ButtonHovered] = ImVec4(0.34, 0.36, 0.40, 1.00);
c[ImGuiCol_ButtonActive] = ImVec4(0.32, 0.45, 0.66, 1.00);
// Headers and tree nodes: muted warm-slate accent.
c[ImGuiCol_Header] = ImVec4(0.22, 0.23, 0.26, 1.00);
c[ImGuiCol_HeaderHovered] = ImVec4(0.32, 0.36, 0.42, 0.60);
c[ImGuiCol_HeaderActive] = ImVec4(0.36, 0.41, 0.48, 0.85);
// Separators: crisp dividers.
c[ImGuiCol_Separator] = ImVec4(0.08, 0.08, 0.10, 0.50);
c[ImGuiCol_SeparatorHovered] = ImVec4(0.36, 0.41, 0.48, 0.50);
c[ImGuiCol_SeparatorActive] = ImVec4(0.36, 0.41, 0.48, 0.80);
c[ImGuiCol_ResizeGrip] = ImVec4(0.36, 0.41, 0.48, 0.25);
c[ImGuiCol_ResizeGripHovered] = ImVec4(0.36, 0.41, 0.48, 0.67);
c[ImGuiCol_ResizeGripActive] = ImVec4(0.36, 0.41, 0.48, 0.95);
c[ImGuiCol_PlotLines] = ImVec4(0.35, 0.50, 0.72, 1.00);
c[ImGuiCol_PlotLinesHovered] = ImVec4(1.00, 0.43, 0.35, 1.00);
c[ImGuiCol_PlotHistogram] = ImVec4(0.35, 0.50, 0.72, 1.00);
c[ImGuiCol_PlotHistogramHovered] = ImVec4(0.36, 0.41, 0.48, 1.00);
c[ImGuiCol_TextSelectedBg] = ImVec4(0.32, 0.36, 0.42, 0.50);
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.00, 0.00, 0.00, 0.50);
c[ImGuiCol_DragDropTarget] = ImVec4(0.35, 0.50, 0.72, 0.90);
c[ImGuiCol_NavCursor] = ImVec4(0.36, 0.41, 0.48, 1.00);
c[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00, 1.00, 1.00, 0.70);
c[ImGuiCol_NavWindowingDimBg] = ImVec4(0.00, 0.00, 0.00, 0.30);
// Tabs.
c[ImGuiCol_DockingEmptyBg] = ImVec4(0.14, 0.15, 0.17, 1.00);
c[ImGuiCol_Tab] = ImVec4(0.16, 0.17, 0.19, 1.00);
c[ImGuiCol_TabHovered] = ImVec4(0.32, 0.36, 0.42, 0.60);
c[ImGuiCol_TabSelected] = ImVec4(0.22, 0.23, 0.26, 1.00);
c[ImGuiCol_TabDimmed] = ImVec4(0.14, 0.15, 0.17, 1.00);
c[ImGuiCol_TabDimmedSelected] = ImVec4(0.20, 0.21, 0.24, 1.00);
c[ImGuiCol_DockingPreview] = ImVec4(0.36, 0.41, 0.48, 0.40);
} else if (theme == GuiTheme::kLight) {
ImGui::StyleColorsLight(&s);
// Clean, warm-white palette with blue accent.
ImVec4 text = ImVec4(0.14, 0.15, 0.18, 1.00);
ImVec4 text_dim = ImVec4(0.45, 0.47, 0.50, 1.00);
ImVec4 bg = ImVec4(0.96, 0.96, 0.97, 1.00);
ImVec4 surface = ImVec4(1.00, 1.00, 1.00, 1.00);
ImVec4 border = ImVec4(0.82, 0.83, 0.85, 1.00);
ImVec4 input = ImVec4(0.93, 0.93, 0.95, 1.00);
ImVec4 accent = ImVec4(0.38, 0.60, 0.90, 1.00);
ImVec4 accent_hover = ImVec4(0.38, 0.60, 0.90, 0.18);
ImVec4 accent_dim = ImVec4(0.38, 0.60, 0.90, 0.10);
ImVec4 header = ImVec4(0.90, 0.91, 0.92, 1.00);
ImVec4 grab = ImVec4(0.70, 0.71, 0.73, 1.00);
c[ImGuiCol_Text] = text;
c[ImGuiCol_TextDisabled] = text_dim;
c[ImGuiCol_WindowBg] = bg;
c[ImGuiCol_ChildBg] = bg;
c[ImGuiCol_PopupBg] = ImVec4(0.94, 0.94, 0.95, 1.00);
c[ImGuiCol_Border] = border;
c[ImGuiCol_BorderShadow] = ImVec4(0.00, 0.00, 0.00, 0.00);
c[ImGuiCol_FrameBg] = input;
c[ImGuiCol_FrameBgHovered] = accent_hover;
c[ImGuiCol_FrameBgActive] = ImVec4(0.38, 0.60, 0.90, 0.30);
c[ImGuiCol_TitleBg] = header;
c[ImGuiCol_TitleBgActive] = ImVec4(0.88, 0.89, 0.90, 1.00);
c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.92, 0.93, 0.94, 0.90);
c[ImGuiCol_MenuBarBg] = header;
c[ImGuiCol_ScrollbarBg] = ImVec4(0.00, 0.00, 0.00, 0.00);
c[ImGuiCol_ScrollbarGrab] = grab;
c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.56, 0.57, 0.60, 1.00);
c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.42, 0.43, 0.46, 1.00);
c[ImGuiCol_CheckMark] = accent;
c[ImGuiCol_SliderGrab] = accent;
c[ImGuiCol_SliderGrabActive] = ImVec4(0.30, 0.52, 0.82, 1.00);
// Buttons: slightly darker than frame background (0.93) to provide subtle but visible contrast.
c[ImGuiCol_Button] = ImVec4(0.86, 0.87, 0.89, 1.00);
c[ImGuiCol_ButtonHovered] = accent_hover;
c[ImGuiCol_ButtonActive] = accent;
c[ImGuiCol_Header] = header;
c[ImGuiCol_HeaderHovered] = accent_hover;
c[ImGuiCol_HeaderActive] = ImVec4(0.38, 0.60, 0.90, 0.30);
c[ImGuiCol_Separator] = border;
c[ImGuiCol_SeparatorHovered] = accent;
c[ImGuiCol_SeparatorActive] = accent;
c[ImGuiCol_ResizeGrip] = accent_dim;
c[ImGuiCol_ResizeGripHovered] = accent_hover;
c[ImGuiCol_ResizeGripActive] = accent;
c[ImGuiCol_Tab] = bg;
c[ImGuiCol_TabHovered] = accent_hover;
c[ImGuiCol_TabSelected] = surface;
c[ImGuiCol_TabDimmed] = bg;
c[ImGuiCol_TabDimmedSelected] = ImVec4(0.97, 0.97, 0.98, 1.00);
c[ImGuiCol_DockingEmptyBg] = header;
c[ImGuiCol_DockingPreview] = ImVec4(0.22, 0.47, 0.82, 0.30);
c[ImGuiCol_TextSelectedBg] = accent_hover;
c[ImGuiCol_DragDropTarget] = accent;
c[ImGuiCol_NavCursor] = accent;
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.00, 0.00, 0.00, 0.30);
} else {
ImGui::StyleColorsDark(&s);
ImVec4 black = ImVec4(0.00, 0.00, 0.00, 1.0);
ImVec4 window = ImVec4(0.25, 0.25, 0.25, 1.0);
ImVec4 font_active = ImVec4(1.00, 1.00, 1.00, 1.0);
ImVec4 font_inactive = ImVec4(0.50, 0.50, 0.50, 1.0);
ImVec4 thumb = ImVec4(0.12, 0.12, 0.12, 1.0);
ImVec4 section = ImVec4(0.40, 0.15, 0.15, 1.0);
ImVec4 button = ImVec4(0.60, 0.40, 0.40, 1.0);
ImVec4 check = ImVec4(0.40, 0.40, 0.70, 1.0);
ImVec4 frame = ImVec4(0.40, 0.30, 0.40, 1.0);
ImVec4 slider = ImVec4(0.60, 0.40, 0.60, 1.0);
c[ImGuiCol_WindowBg] = window;
c[ImGuiCol_ChildBg] = black;
c[ImGuiCol_PopupBg] = window;
c[ImGuiCol_Text] = font_active;
c[ImGuiCol_TextDisabled] = font_inactive;
c[ImGuiCol_CheckMark] = font_active;
c[ImGuiCol_Header] = section;
c[ImGuiCol_HeaderHovered] = section;
c[ImGuiCol_HeaderActive] = section;
c[ImGuiCol_TitleBgActive] = window;
c[ImGuiCol_ScrollbarBg] = window;
c[ImGuiCol_ScrollbarGrab] = thumb;
c[ImGuiCol_ScrollbarGrabHovered] = thumb;
c[ImGuiCol_ScrollbarGrabActive] = thumb;
c[ImGuiCol_FrameBg] = frame;
c[ImGuiCol_FrameBgHovered] = frame;
c[ImGuiCol_FrameBgActive] = frame;
c[ImGuiCol_SliderGrab] = slider;
c[ImGuiCol_SliderGrabActive] = slider;
c[ImGuiCol_Button] = window;
c[ImGuiCol_ButtonHovered] = button;
c[ImGuiCol_ButtonActive] = button;
c[ImGuiCol_Tab] = window;
c[ImGuiCol_TabHovered] = check;
c[ImGuiCol_TabSelected] = check;
c[ImGuiCol_TabDimmed] = window;
c[ImGuiCol_TabDimmedSelected] = check;
}
float scale = s.FontScaleDpi;
int hspacing = 3;
int vspacing = 2;
float rounding = 3.0f;
s.DisplaySafeAreaPadding = ImVec2(0, 0);
s.WindowPadding = ImTrunc(ImVec2(6.0f * scale, 6.0f * scale));
s.FramePadding = ImTrunc(ImVec2(hspacing * scale, 4.0f * scale));
s.ItemSpacing = ImTrunc(ImVec2(hspacing * scale, vspacing * scale));
s.ItemInnerSpacing = ImTrunc(ImVec2(hspacing * scale, vspacing * scale));
s.WindowRounding = ImTrunc(rounding * scale);
s.FrameRounding = ImTrunc(rounding * scale);
s.TabRounding = ImTrunc(rounding * scale);
s.ScrollbarRounding = ImTrunc(rounding * scale);
s.ChildRounding = ImTrunc(rounding * scale);
s.GrabRounding = ImTrunc(rounding * scale);
s.PopupRounding = ImTrunc(rounding * scale);
s.WindowBorderSize = 0.0f;
s.FrameBorderSize = 1.0f;
s.PopupBorderSize = 1.0f;
s.IndentSpacing = ImTrunc(10.0f * scale);
s.ScrollbarSize = ImTrunc(10.0f * scale);
s.GrabMinSize = ImTrunc(5.0f * scale);
s.WindowMenuButtonPosition = ImGuiDir_None;
s.TabCloseButtonMinWidthSelected = 0.0f;
s.DockingNodeHasCloseButton = false;
}
void RescaleDock(float ratio) {
if (ratio == 1) return;
ImGuiID root = ImGui::GetID("Root");
ImGuiDockNode* root_node = ImGui::DockBuilderGetNode(root);
if (root_node) {
struct ScaleNodes {
static void Apply(ImGuiDockNode* node, float r) {
node->SizeRef.x *= r;
if (node->ChildNodes[0]) Apply(node->ChildNodes[0], r);
if (node->ChildNodes[1]) Apply(node->ChildNodes[1], r);
}
};
ScaleNodes::Apply(root_node, ratio);
}
}
ImVec4 ConfigureDockingLayout(bool show_toolbar, bool show_status_bar) {
ImGuiViewport* viewport = ImGui::GetMainViewport();
const float scale = ImGui::GetWindowDpiScale();
const float font_scale = ImGui::GetIO().FontGlobalScale;
const float kOptionsRelWidth = 0.15f;
const float kInspectorRelWidth = 0.22f;
const float kPropertiesRelHeight = 0.3f;
const float kToolsBarHeight =
show_toolbar ? 36.f * scale * font_scale : 0.0f;
const float kStatusBarHeight =
show_status_bar ? 32.f * scale * font_scale : 0.0f;
const ImVec2 dockspace_pos{viewport->WorkPos.x,
viewport->WorkPos.y + kToolsBarHeight};
const ImVec2 dockspace_size{
viewport->WorkSize.x,
viewport->WorkSize.y - kToolsBarHeight - kStatusBarHeight};
ImGuiID root = ImGui::GetID("Root");
const bool first_time = (ImGui::DockBuilderGetNode(root) == nullptr);
if (first_time) {
ImGui::DockBuilderRemoveNode(root);
ImGui::DockBuilderAddNode(root, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(root, dockspace_size);
// Slice up the main dock space.
ImGuiID main = root;
ImGuiID options = 0;
ImGui::DockBuilderSplitNode(main, ImGuiDir_Left, kOptionsRelWidth, &options,
&main);
ImGuiID inspector = 0;
ImGui::DockBuilderSplitNode(main, ImGuiDir_Right, kInspectorRelWidth,
&inspector, &main);
ImGuiID properties = 0;
ImGui::DockBuilderSplitNode(inspector, ImGuiDir_Down, kPropertiesRelHeight,
&properties, &inspector);
ImGuiID profiler = 0;
ImGui::DockBuilderSplitNode(main, ImGuiDir_Right, 0.42f, &profiler, &main);
ImGui::DockBuilderDockWindow("Dockspace", main);
ImGui::DockBuilderDockWindow("Options", options);
ImGui::DockBuilderDockWindow("Explorer", inspector);
ImGui::DockBuilderDockWindow("Editor", inspector);
ImGui::DockBuilderDockWindow("Inspector", inspector);
ImGui::DockBuilderDockWindow("Properties", properties);
ImGui::DockBuilderDockWindow("Profiler", profiler);
ImGui::DockBuilderFinish(root);
}
// Create a dummy window filling the entire workspace in which we can perform
// docking.
ImGui::SetNextWindowPos(dockspace_pos);
ImGui::SetNextWindowSize(dockspace_size);
ImGui::SetNextWindowViewport(viewport->ID);
const ImGuiWindowFlags kWorkspaceFlags =
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground;
const ImGuiWindowFlags kFixedFlags =
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoDocking;
// Main workspace area in which we can dock other windows.
{
platform::ScopedStyle style;
style.Var(ImGuiStyleVar_WindowRounding, 0.0f);
style.Var(ImGuiStyleVar_WindowBorderSize, 0.0f);
style.Var(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin("Dockspace", nullptr, kWorkspaceFlags);
const ImGuiDockNodeFlags kDockSpaceFlags =
ImGuiDockNodeFlags_PassthruCentralNode |
ImGuiDockNodeFlags_NoDockingOverCentralNode;
ImGui::DockSpace(root, ImVec2(0.0f, 0.0f), kDockSpaceFlags);
ImGui::End();
}
// Toolbar is fixed at the top.
if (show_toolbar) {
platform::ScopedStyle style;
style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f);
style.Var(ImGuiStyleVar_WindowRounding, 0.0f);
style.Var(ImGuiStyleVar_WindowMinSize, ImVec2(1, 1));
const float toolbar_vpad =
std::max(0.f, (36.f * scale * font_scale - ImGui::GetFrameHeight()) * 0.5f);
style.Var(ImGuiStyleVar_WindowPadding, ImVec2(4 * scale, toolbar_vpad));
ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, 36.f * scale * font_scale),
ImGuiCond_Always);
ImGui::Begin("ToolBar", nullptr, kFixedFlags);
ImGui::End();
}
// StatusBar is fixed at the bottom.
if (show_status_bar) {
platform::ScopedStyle style;
style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f);
style.Var(ImGuiStyleVar_WindowRounding, 0.0f);
style.Var(ImGuiStyleVar_WindowMinSize, ImVec2(1, 1));
ImGui::SetNextWindowPos(ImVec2(0, viewport->Size.y - kStatusBarHeight),
ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kStatusBarHeight),
ImGuiCond_Always);
ImGui::Begin("StatusBar", nullptr, kFixedFlags);
ImGui::End();
}
ImGuiDockNode* central = ImGui::DockBuilderGetCentralNode(root);
if (central) {
return ImVec4(central->Pos.x, central->Pos.y,
central->Size.x, central->Size.y);
}
const int settings_width = dockspace_size.x * kOptionsRelWidth;
const int inspector_width = dockspace_size.x * kInspectorRelWidth;
const float workspace_x = dockspace_pos.x + settings_width;
const float workspace_y = dockspace_pos.y;
const float workspace_w = dockspace_size.x - settings_width - inspector_width;
const float workspace_h = dockspace_size.y;
return ImVec4(workspace_x, workspace_y, workspace_w, workspace_h);
}
void StepControlGui(StepControl* step_control, int& speed_index) {
platform::ScopedStyle style;
bool is_dark = ImGui::GetStyle().Colors[ImGuiCol_WindowBg].x < 0.5f;
const ImColor yellow = is_dark ? ImColor(158, 115, 18, 255) : ImColor(255, 215, 0, 255);
const ImColor green = is_dark ? ImColor(40, 125, 60, 255) : ImColor(40, 180, 40, 255);
auto make_button = [&](const char* icon, StepControl::PauseState target_state,
ImColor color, ImDrawFlags corners,
const char* tooltip = "",
float hover_alpha = 1.f, float width_scale = 1.f) {
ImVec2 size(0, 0);
if (width_scale != 1.f) {
const ImGuiStyle& s = ImGui::GetStyle();
const float w = ImGui::CalcTextSize(icon).x + s.FramePadding.x * 2;
size.x = w * width_scale;
}
bool active = step_control->GetPauseState() == target_state;
if (ImGui_ColorButtonEx(icon, active, color, corners, 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,
ImDrawFlags_RoundCornersLeft, "Pause", .3f, 1.6f);
ImGui::SameLine(0.f, 0.f);
make_button(ICON_FA_MAGIC, StepControl::PauseState::kViscousPaused, yellow,
ImDrawFlags_RoundCornersNone, "Viscous Pause", .3f, 1.3f);
ImGui::SameLine(0.f, 0.f);
make_button(ICON_FA_PLAY, StepControl::PauseState::kUnpaused, green,
ImDrawFlags_RoundCornersRight, "", .3f, 1.6f);
// Speed selection.
style.Reset();
ImGui::SameLine(0, ImGui::GetFrameHeight() * .6f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
ImVec2(ImGui::GetStyle().FramePadding.x +
5.f * ImGui::GetStyle().FontScaleDpi,
ImGui::GetStyle().FramePadding.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))) {
SetSpeedIndex(step_control, speed_index, n);
}
}
ImGui::EndCombo();
}
ImGui::PopStyleVar();
if (misaligned) {
ImGui::SetItemTooltip("%s", "Desired Speed (Measured Speed)");
} else {
ImGui::SetItemTooltip("%s", "Desired Speed");
}
}
void SetSpeedIndex(StepControl* step_control, int& speed_index,
int request_idx) {
if (!step_control || request_idx == speed_index || kPercentRealTime.empty()) {
return;
}
speed_index = std::clamp<int>(request_idx, 0, kPercentRealTime.size() - 1);
float speed = std::stof(kPercentRealTime[speed_index]);
step_control->SetSpeed(speed);
}
bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size) {
static constexpr const char* ICON_DARKMODE = ICON_FA_CIRCLE;
static constexpr const char* ICON_LIGHTMODE = ICON_FA_CIRCLE_O;
static constexpr const char* ICON_CLASSICMODE = ICON_FA_ADJUST;
const char* theme_icons[] = {ICON_LIGHTMODE, ICON_DARKMODE, ICON_CLASSICMODE};
int theme_idx = static_cast<int>(*theme);
if (ImGui::Button(theme_icons[theme_idx], size)) {
theme_idx = (theme_idx + 1) % IM_ARRAYSIZE(theme_icons);
*theme = static_cast<GuiTheme>(theme_idx);
return true;
}
ImGui::SetItemTooltip("%s", "Toggle theme");
return false;
}
bool LabelSelectionGui(mjvOption* opts) {
static constexpr const char* ICON_LABEL = ICON_FA_COMMENT;
static constexpr const char* kLabelNames[] = {
"None", "Body", "Joint", "Geom", "Site", "Camera",
"Light", "Tendon", "Actuator", "Constraint", "Flex", "Skin",
"Selection", "Sel Pnt", "Contact", "Force", "Island"};
bool changed = false;
const std::string label_preview =
opts->label == 0 ? std::string(ICON_LABEL) + " Label"
: std::string(ICON_LABEL) + " " + kLabelNames[opts->label];
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
if (ImGui::BeginCombo("##Label", label_preview.c_str(),
ImGuiComboFlags_NoArrowButton)) {
for (int n = 0; n < IM_ARRAYSIZE(kLabelNames); n++) {
if (ImGui::Selectable(kLabelNames[n], (opts->label == n))) {
changed = true;
opts->label = n;
}
}
ImGui::EndCombo();
}
ImGui::SetItemTooltip("%s", "Label");
return changed;
}
bool FrameSelectionGui(mjvOption* opts) {
static constexpr const char* ICON_FRAME = ICON_FA_ARROWS;
static constexpr const char* kFrameNames[] = {
"None", "Body", "Geom", "Site", "Camera", "Light", "Contact", "World"};
bool changed = false;
const std::string frame_preview =
opts->frame == 0 ? std::string(ICON_FRAME) + " Frame"
: std::string(ICON_FRAME) + " " + kFrameNames[opts->frame];
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
if (ImGui::BeginCombo("##Frame", frame_preview.c_str(),
ImGuiComboFlags_NoArrowButton)) {
for (int n = 0; n < IM_ARRAYSIZE(kFrameNames); n++) {
if (ImGui::Selectable(kFrameNames[n], (opts->frame == n))) {
opts->frame = n;
changed = true;
}
}
ImGui::EndCombo();
}
ImGui::SetItemTooltip("%s", "Frame");
return changed;
}
std::string GetCameraName(const mjModel* model, const mjvCamera& camera,
int index) {
static constexpr char kCameraTumbleName[] = "Free: tumble";
static constexpr char kCameraWasdName[] = "Free: wasd";
static constexpr char kCameraUnnamedName[] = "Unnamed";
if (index == kTumbleCameraIdx) {
return kCameraTumbleName;
} else if (index == kFreeCameraIdx) {
return kCameraWasdName;
} else if (index == kTrackingCameraIdx) {
return "Tracking (" + std::to_string(camera.trackbodyid) + ")";
} else if (model->names[model->name_camadr[index]]) {
return std::string(model->names + model->name_camadr[index]);
} else {
return kCameraUnnamedName;
}
}
bool CameraSelectionGui(const mjModel* model, mjData* data, mjvCamera& camera,
int& index) {
static constexpr const char* ICON_CAMERA = ICON_FA_CAMERA;
static constexpr const char* ICON_COPY_CAMERA = ICON_FA_COPY;
// Copy camera button.
const float btn_size = ImGui::GetFrameHeight();
const ImVec2 square_size(btn_size, btn_size);
if (ImGui::Button(ICON_COPY_CAMERA, square_size)) {
std::string camera_string = CameraToString(data, &camera);
MaybeSaveToClipboard(camera_string);
}
ImGui::SetItemTooltip("%s", "Copy Camera");
ImGui::SameLine(0, 0);
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
auto select = [&](int type, int idx) {
if (ImGui::Selectable(GetCameraName(model, camera, type).c_str(),
(type == idx))) {
return true;
}
return false;
};
bool changed = false;
const std::string preview =
std::string(ICON_CAMERA) + " " + GetCameraName(model, camera, index);
if (ImGui::BeginCombo("##Camera", preview.c_str(),
ImGuiComboFlags_NoArrowButton)) {
if (select(kTumbleCameraIdx, index)) {
index = SetCamera(model, &camera, kTumbleCameraIdx);
changed = true;
}
if (select(kFreeCameraIdx, index)) {
index = SetCamera(model, &camera, kFreeCameraIdx);
changed = true;
}
if (select(kTrackingCameraIdx, index)) {
index = SetCamera(model, &camera, kTrackingCameraIdx);
changed = true;
}
for (int cam = 0; cam < model->ncam; cam++) {
if (select(cam, index)) {
index = SetCamera(model, &camera, cam);
changed = true;
}
}
ImGui::EndCombo();
}
ImGui::SetItemTooltip("%s", "Camera");
return changed;
}
void SensorGui(const mjModel* model, const mjData* data) {
if (model->nsensor == 0) {
return;
}
ImPlot::PushStyleVar(ImPlotStyleVar_FitPadding, ImVec2(0.1f, 0.1f));
if (ImPlot::BeginPlot("Sensors", ImVec2(-1, 0),
ImPlotFlags_NoLegend | ImPlotFlags_NoMouseText)) {
ImPlot::SetupLegend(ImPlotLocation_NorthEast, ImPlotLegendFlags_None);
ImPlot::SetupAxis(ImAxis_X1, "sensor",
ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_NoLabel);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, 5, ImPlotCond_Once);
ImPlot::SetupAxis(ImAxis_Y1, "value",
ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_NoLabel);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.1f");
ImPlot::SetupAxisLimits(ImAxis_Y1, -100, 100, ImPlotCond_Once);
ImPlot::SetupFinish();
// The values to be plotted.
std::vector<ImPlotPoint> sensor_values;
// The x-value of the bar to be plotted. Multiple bars will belong to the
// same sensor (i.e. the sensor_dim), but each group of bars will be appear
// in sequence along the x-axis.
float x_value = 0.f;
// The index of the sensor being plotted, based on sensor_type.
int sensor_index = 0;
// Function that plots the current group of sensor bars.
auto plot_lines = [](int sensor_idx, const ImPlotPoint* values, int count) {
constexpr float bar_weight = 5.0f;
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, bar_weight);
std::string sensor_label = "Sensor " + std::to_string(sensor_idx);
ImPlot::PlotLine(sensor_label.c_str(), &values->x, &values->y, count,
ImPlotLineFlags_Segments, 0, 2 * sizeof(double));
};
for (int n = 0; n < model->nsensor; n++) {
if (n > 0 && model->sensor_type[n] != model->sensor_type[n - 1]) {
plot_lines(sensor_index, sensor_values.data(), sensor_values.size());
sensor_values.clear();
++sensor_index;
}
const int adr = model->sensor_adr[n];
const int dim = model->sensor_dim[n];
const mjtNum cutoff =
(model->sensor_cutoff[n] > 0 ? model->sensor_cutoff[n] : 1);
for (int i = 0; i < dim; ++i) {
sensor_values.push_back({x_value, 0});
sensor_values.push_back({x_value, data->sensordata[adr + i] / cutoff});
x_value += 1.f;
}
}
// Ensure the last group of sensors is plotted.
plot_lines(sensor_index, sensor_values.data(), sensor_values.size());
ImPlot::EndPlot();
}
ImPlot::PopStyleVar();
}
void StateGui(const mjModel* model, mjData* data, std::vector<mjtNum>& state,
int& state_sig, float min_width) {
const float available_width =
GetStableAvailWidth() - ImGui::GetTreeNodeToLabelSpacing();
const int num_cols = std::clamp(
static_cast<int>(std::floor(available_width / min_width)), 1, 4);
const ImVec2 size = GetFlexElementSize(num_cols);
ImGui::Unindent(0.5f * ImGui::GetTreeNodeToLabelSpacing());
// State component names and tooltips.
static constexpr const char* name_and_tooltip[mjNSTATE][2] = {
{"TIME", "Time"},
{"QPOS", "Position"},
{"QVEL", "Velocity"},
{"ACT", "Actuator activation"},
{"HISTORY", "History buffers (control, sensor)"},
{"WARMSTART", "Acceleration used for warmstart"},
{"CTRL", "Control"},
{"QFRC_APPLIED", "Applied generalized force"},
{"XFRC_APPLIED", "Applied Cartesian force/torque"},
{"EQ_ACTIVE", "Enable/disable constraints"},
{"MOCAP_POS", "Positions of mocap bodies"},
{"MOCAP_QUAT", "Orientations of mocap bodies"},
{"USERDATA", "User data"},
{"PLUGIN", "Plugin state"},
};
int prev_state_sig = state_sig;
// State component checkboxes.
if (ImGui::BeginTable("##StateSignature", num_cols)) {
for (int i = 0; i < mjNSTATE; ++i) {
ImGui::TableNextColumn();
bool checked = state_sig & (1 << i);
ImGui::Checkbox(name_and_tooltip[i][0], &checked);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", name_and_tooltip[i][1]);
}
state_sig = checked ? (state_sig | (1 << i)) : (state_sig & ~(1 << i));
}
ImGui::EndTable();
}
// Buttons to select commonly used state signatures.
if (ImGui::BeginTable("##CommonSignatures", num_cols)) {
ImGui::TableNextColumn();
if (ImGui::Button("Physics", size)) {
state_sig = (state_sig == mjSTATE_PHYSICS) ? 0 : mjSTATE_PHYSICS;
}
ImGui::TableNextColumn();
if (ImGui::Button("Full Physics", size)) {
state_sig = (state_sig == mjSTATE_FULLPHYSICS) ? 0 : mjSTATE_FULLPHYSICS;
}
ImGui::TableNextColumn();
if (ImGui::Button("User", size)) {
state_sig = (state_sig == mjSTATE_USER) ? 0 : mjSTATE_USER;
}
ImGui::TableNextColumn();
if (ImGui::Button("Integration", size)) {
state_sig = (state_sig == mjSTATE_INTEGRATION) ? 0 : mjSTATE_INTEGRATION;
}
ImGui::EndTable();
}
if (state_sig != prev_state_sig) {
const int size = mj_stateSize(model, state_sig);
state.resize(size);
}
if (state.empty()) {
// The state size is 0, let the user know why.
ImGui::Separator();
ImGui::BeginDisabled();
ImGui::TextWrapped(
state_sig == 0
? "No state components are selected."
: "Selected state components do not exist in the model.");
ImGui::EndDisabled();
} else {
mj_getState(model, data, state.data(), state_sig);
bool changed = false;
if (ImGui::BeginTable(
"State", 3,
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter |
ImGuiTableFlags_BordersV | ImGuiTableFlags_Resizable |
ImGuiTableFlags_ScrollY,
ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 20))) {
ImGui::TableSetupColumn("Index");
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
ImGuiListClipper clipper;
clipper.Begin(state.size());
while (clipper.Step()) {
int global = 0;
for (int i = 0; i < mjNSTATE; ++i) {
if (state_sig & (1 << i)) {
for (int local = 0; local < mj_stateSize(model, (1 << i));
++local, ++global) {
if (global < clipper.DisplayStart) {
continue;
}
if (global >= clipper.DisplayEnd) {
break;
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", global);
ImGui::TableNextColumn();
ImGui::Text("%s[%d]", name_and_tooltip[i][0], local);
ImGui::TableNextColumn();
float value = state[global];
ImGui::PushItemWidth(-std::numeric_limits<float>::min());
ImGui::PushID(global);
if (ImGui::DragFloat("##value", &value, 0.01f, 0, 0, "%.3f")) {
changed = true;
}
ImGui::PopID();
ImGui::PopItemWidth();
state[global] = value;
}
}
}
}
ImGui::EndTable();
}
if (changed) {
mj_setState(model, data, state.data(), state_sig);
}
}
ImGui::Indent(0.5f * ImGui::GetTreeNodeToLabelSpacing());
}
void WatchGui(const mjModel* model, const mjData* data, char* field_name,
int field_len, int& field_index) {
const float item_width = ImGui::GetWindowWidth() * .6f;
ImGui::PushItemWidth(item_width);
ImGui::InputText("Field", field_name, field_len);
ImGui::InputInt("Index", &field_index);
const mjtNum* value = static_cast<const mjtNum*>(
GetValue(model, data, field_name, field_index));
ScopedStyle style;
style.Color(ImGuiCol_FrameBg, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]);
if (value) {
char buf[100];
int size = std::snprintf(buf, sizeof(buf), "%0.3f", *value);
ImGui::InputText("Value", buf, size, ImGuiInputTextFlags_ReadOnly);
} else {
ImGui::BeginDisabled();
style.Color(ImGuiCol_Text, ImColor(255, 0, 0, 255));
char buf[] = "Invalid field/index!";
ImGui::InputText("Value", buf, sizeof(buf), ImGuiInputTextFlags_ReadOnly);
ImGui::EndDisabled();
}
ImGui::PopItemWidth();
}
void PhysicsGui(mjModel* model, float min_width) {
const float available_width =
GetStableAvailWidth() - ImGui::GetTreeNodeToLabelSpacing();
const int num_cols = std::clamp(
static_cast<int>(std::floor(available_width / min_width)), 1, 6);
const float item_width = ImGui::GetWindowWidth() * .6f;
ImGui::PushItemWidth(item_width);
auto& opt = model->opt;
const char* opts0[] = {"Euler", "RK4", "implicit", "implicitfast"};
ImGui::Combo("Integrator", &opt.integrator, opts0, IM_ARRAYSIZE(opts0));
const char* opts1[] = {"Pyramidal", "Elliptic"};
ImGui::Combo("Cone", &opt.cone, opts1, IM_ARRAYSIZE(opts1));
const char* opts2[] = {"Dense", "Sparse", "Auto"};
ImGui::Combo("Jacobian", &opt.jacobian, opts2, IM_ARRAYSIZE(opts2));
const char* opts3[] = {"PGS", "CG", "Newton"};
ImGui::Combo("Solver", &opt.solver, opts3, IM_ARRAYSIZE(opts3));
if (SectionHeader("Algorithmic Parameters",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui_Input("Timestep", &opt.timestep, {0, 1, 0.01, 0.1});
ImGui_Input("Iterations", &opt.iterations, {0, 1000, 1, 10});
ImGui_Input("Tolerance", &opt.tolerance, {0, 1, 1e-7, 1e-6});
ImGui_Input("LS Iter", &opt.ls_iterations, {0, 100, 1, 0.1});
ImGui_Input("LS Tol", &opt.ls_tolerance, {0, 0.1, 0.01, 0.1});
ImGui_Input("Noslip Iter", &opt.noslip_iterations, {0, 1000, 1, 100});
ImGui_Input("Noslip Tol", &opt.noslip_tolerance, {0, 1, 0.01, 0.1});
ImGui_Input("CCD Iter", &opt.ccd_iterations, {0, 1000, 1, 100});
ImGui_Input("CCD Tol", &opt.ccd_tolerance, {0, 1, 0.01, 0.1});
ImGui_Input("Sleep Tol", &opt.sleep_tolerance, {0, 1, 0.01, 0.1});
ImGui_Input("SDF Iter", &opt.sdf_iterations, {1, 20, 1, 10});
ImGui_Input("SDF Init", &opt.sdf_initpoints, {1, 100, 1, 10});
ImGui::TreePop();
}
if (SectionHeader("Physical Parameters")) {
ImGui_InputN("Gravity", opt.gravity, 3);
ImGui_InputN("Wind", opt.wind, 3);
ImGui_InputN("Magnetic", opt.magnetic, 3);
ImGui_Input("Density", &opt.density, {.min = 0.0});
ImGui_Input("Viscosity", &opt.viscosity, {.min = 0.0});
ImGui_Input("Imp Ratio", &opt.impratio, {.min = 0.0});
ImGui::TreePop();
};
if (SectionHeader("Flags", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::BeginTable("##PhysicsFlagsTable", num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < mjNDISABLE; ++i) {
ImGui::TableNextColumn();
int flipped = ~opt.disableflags;
ImGui_BitToggle(mjDISABLESTRING[i], &flipped, 1 << i, size);
opt.disableflags = ~flipped;
}
for (int i = 0; i < mjNENABLE; ++i) {
ImGui::TableNextColumn();
ImGui_BitToggle(mjENABLESTRING[i], &opt.enableflags, 1 << i, size);
}
ImGui::EndTable();
}
ImGui::TreePop();
}
if (SectionHeader("Contact Override")) {
ImGui_Input("Margin", &opt.o_margin, {.min = 0.0});
ImGui_InputN("Sol Imp", opt.o_solimp, 5, {.format = "%0.3f"});
ImGui_InputN("Sol Ref", opt.o_solref, 2, {.format = "%0.3f"});
ImGui_InputN("Friction", opt.o_friction, 5, {.format = "%.3f"});
ImGui::TreePop();
}
if (SectionHeader("Actuator Groups")) {
if (ImGui::BeginTable("##ActuatorGroupsTable", num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < 6; ++i) {
char label[64];
std::snprintf(label, sizeof(label), "Act Group %d", i);
ImGui::TableNextColumn();
int flipped = ~opt.disableactuator;
ImGui_BitToggle(label, &flipped, 1 << i, size);
opt.disableactuator = ~flipped;
}
ImGui::EndTable();
}
ImGui::TreePop();
}
ImGui::PopItemWidth();
}
void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera,
float min_width) {
auto& vis = model->vis;
auto& stat = model->stat;
const float item_width = ImGui::GetWindowWidth() * .6f;
ImGui::PushItemWidth(item_width);
ImGui::SliderInt("Tree depth", &vis_options->bvh_depth, 0, 20);
ImGui::SliderInt("Flex layer", &vis_options->flex_layer, 0, 10);
if (SectionHeader("Headlight")) {
ImGui_SwitchToggle("Active", &vis.headlight.active);
ImGui::ColorEdit3("Ambient", vis.headlight.ambient);
ImGui::ColorEdit3("Diffuse", vis.headlight.diffuse);
ImGui::ColorEdit3("Specular", vis.headlight.specular);
ImGui::TreePop();
}
if (SectionHeader("Free Camera")) {
ImGui_SwitchToggle("Orthographic", &vis.global.orthographic);
ImGui_Input("FOV", &vis.global.fovy, {.format = "%0.2f"});
ImGui_InputN("Center", stat.center, 3, {.format = "%0.2f"});
ImGui_Input("Azimuth", &vis.global.azimuth, {.format = "%0.2f"});
ImGui_Input("Elevation", &vis.global.elevation, {.format = "%0.2f"});
if (ImGui::Button("Align")) {
mjv_defaultFreeCamera(model, camera);
}
ImGui::TreePop();
}
if (SectionHeader("Global")) {
ImGui_Input("Extent", &stat.extent);
const char* opts[] = {"Box", "Ellipsoid"};
ImGui::SliderInt("Inertia", &vis.global.ellipsoidinertia, 0, 1,
opts[vis.global.ellipsoidinertia]);
ImGui_ButtonToggle("BVH active", &vis.global.bvactive);
ImGui::TreePop();
}
if (SectionHeader("Mapping")) {
ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.3f);
ImGui_Input("Stiffness", &vis.map.stiffness);
ImGui_Input("Rot stiffness", &vis.map.stiffnessrot);
ImGui_Input("Force", &vis.map.force);
ImGui_Input("Torque", &vis.map.torque);
ImGui_Input("Alpha", &vis.map.alpha);
ImGui_Input("Fog start", &vis.map.fogstart);
ImGui_Input("Fog end", &vis.map.fogend);
ImGui_Input("Z near", &vis.map.znear);
ImGui_Input("Z far", &vis.map.zfar);
ImGui_Input("Haze", &vis.map.haze);
ImGui_Input("Shadow clip", &vis.map.shadowclip);
ImGui_Input("Shadow scale", &vis.map.shadowscale);
ImGui::PopItemWidth();
ImGui::TreePop();
}
if (SectionHeader("Scale")) {
ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.3f);
ImGui_Input("All (meansize)", &stat.meansize, {.format = "%0.3f"});
ImGui_Input("Force width", &vis.scale.forcewidth);
ImGui_Input("Contact width", &vis.scale.contactwidth);
ImGui_Input("Contact height", &vis.scale.contactheight);
ImGui_Input("Connect", &vis.scale.connect);
ImGui_Input("Com", &vis.scale.com);
ImGui_Input("Camera", &vis.scale.camera);
ImGui_Input("Light", &vis.scale.light);
ImGui_Input("Select point", &vis.scale.selectpoint);
ImGui_Input("Joint length", &vis.scale.jointlength);
ImGui_Input("Joint width", &vis.scale.jointwidth);
ImGui_Input("Actuator length", &vis.scale.actuatorlength);
ImGui_Input("Actuator width", &vis.scale.actuatorwidth);
ImGui_Input("Frame length", &vis.scale.framelength);
ImGui_Input("Frame width", &vis.scale.framewidth);
ImGui_Input("Constraint", &vis.scale.constraint);
ImGui_Input("Slider-crank", &vis.scale.slidercrank);
ImGui::PopItemWidth();
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Colors")) {
ImGui::ColorEdit4("Fog", vis.rgba.fog);
ImGui::ColorEdit4("Haze", vis.rgba.haze);
ImGui::ColorEdit4("Force", vis.rgba.force);
ImGui::ColorEdit4("Inertia", vis.rgba.inertia);
ImGui::ColorEdit4("Joint", vis.rgba.joint);
ImGui::ColorEdit4("Actuator", vis.rgba.actuator);
ImGui::ColorEdit4("Act. Negative", vis.rgba.actuatornegative);
ImGui::ColorEdit4("Act. Positive", vis.rgba.actuatorpositive);
ImGui::ColorEdit4("Center of Mass", vis.rgba.com);
ImGui::ColorEdit4("Camera", vis.rgba.camera);
ImGui::ColorEdit4("Light", vis.rgba.light);
ImGui::ColorEdit4("Select Point", vis.rgba.selectpoint);
ImGui::ColorEdit4("Auto Connect", vis.rgba.connect);
ImGui::ColorEdit4("Contact Point", vis.rgba.contactpoint);
ImGui::ColorEdit4("Contact Force", vis.rgba.contactforce);
ImGui::ColorEdit4("Contact Friction", vis.rgba.contactfriction);
ImGui::ColorEdit4("Contact Torque", vis.rgba.contacttorque);
ImGui::ColorEdit4("Contact Gap", vis.rgba.contactgap);
ImGui::ColorEdit4("Range Finder", vis.rgba.rangefinder);
ImGui::ColorEdit4("Constraint", vis.rgba.constraint);
ImGui::ColorEdit4("Slider Crank", vis.rgba.slidercrank);
ImGui::ColorEdit4("Crank Broken", vis.rgba.crankbroken);
ImGui::ColorEdit4("Frustum", vis.rgba.frustum);
ImGui::ColorEdit4("Bounding Vol.", vis.rgba.bv);
ImGui::ColorEdit4("BV Active", vis.rgba.bvactive);
ImGui::TreePop();
}
ImGui::PopItemWidth();
}
void RenderingGui(const mjModel* model, mjvOption* vis_options,
mjtByte* render_flags, float min_width) {
const float available_width =
GetStableAvailWidth() - ImGui::GetTreeNodeToLabelSpacing();
const int num_cols = std::clamp(
static_cast<int>(std::floor(available_width / min_width)), 1, 6);
if (ImGui::TreeNodeEx("Model Elements", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::BeginTable("##ModelElementsTable", num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < mjNVISFLAG; ++i) {
ImGui::TableNextColumn();
ImGui_ButtonToggle(mjVISSTRING[i][0], &vis_options->flags[i], size);
}
ImGui::EndTable();
}
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Render Flags", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::BeginTable("##RenderFlagsTable", num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < mjNRNDFLAG; ++i) {
ImGui::TableNextColumn();
ImGui_ButtonToggle(mjRNDSTRING[i][0], &render_flags[i], size);
}
ImGui::EndTable();
}
ImGui::TreePop();
}
}
void GroupsGui(const mjModel* model, mjvOption* vis_options, float min_width) {
const float available_width = GetStableAvailWidth();
// We limit the number of columns to 1, 2, 3, or 6 depending on how much
// space the window has available.
int num_cols = std::clamp(
static_cast<int>(std::floor(available_width / min_width)), 1, 6);
if (num_cols == 4 || num_cols == 5) {
num_cols = 3;
}
auto GroupGui = [&](const char* name, mjtByte* group) {
if (ImGui::TreeNodeEx(name, ImGuiTreeNodeFlags_DefaultOpen)) {
char label[64];
std::snprintf(label, sizeof(label), "##%s", name);
if (ImGui::BeginTable(label, num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < 6; ++i) {
ImGui::TableNextColumn();
std::snprintf(label, sizeof(label), "%s %d", name, i);
ImGui_ButtonToggle(label, &group[i], size);
}
ImGui::EndTable();
}
ImGui::TreePop();
}
};
GroupGui("Geoms", vis_options->geomgroup);
GroupGui("Sites", vis_options->sitegroup);
GroupGui("Joints", vis_options->jointgroup);
GroupGui("Tendons", vis_options->tendongroup);
GroupGui("Actuators", vis_options->actuatorgroup);
GroupGui("Flexes", vis_options->flexgroup);
GroupGui("Skins", vis_options->skingroup);
}
void NoiseGui(StepControl* step_control) {
float noise_scale, noise_rate;
step_control->GetNoiseParameters(noise_scale, noise_rate);
const float item_width = ImGui::GetWindowWidth() * .6f;
ImGui::PushItemWidth(item_width);
ImGui::SliderFloat("Noise scale", &noise_scale, 0, 1);
ImGui::SliderFloat("Noise rate", &noise_rate, 0, 4);
ImGui::PopItemWidth();
step_control->SetNoiseParameters(noise_scale, noise_rate);
}
void JointsGui(const mjModel* model, const mjData* data,
const mjvOption* vis_options) {
const float item_width = ImGui::GetWindowWidth() * .6f;
ImGui::PushItemWidth(item_width);
char name[100];
for (int i = 0; i < model->njnt; ++i) {
if (model->jnt_type[i] != mjJNT_HINGE &&
model->jnt_type[i] != mjJNT_SLIDE) {
continue;
}
const int group = std::clamp(model->jnt_group[i], 0, mjNGROUP - 1);
if (!vis_options->jointgroup[group]) {
continue;
}
const char* jnt_name = model->names + model->name_jntadr[i];
if (*jnt_name) {
std::snprintf(name, sizeof(name), "%s", jnt_name);
} else {
std::snprintf(name, sizeof(name), "joint %d", i);
}
double min = -1.0;
double max = 1.0;
if (model->jnt_limited[i]) {
min = model->jnt_range[2 * i + 0];
max = model->jnt_range[2 * i + 1];
} else if (model->jnt_type[i] == mjJNT_SLIDE) {
min = -1.0;
max = 1.0;
} else {
min = -3.1416;
max = 3.1416;
}
const int data_adr = model->jnt_qposadr[i];
ImGui_Slider(name, &data->qpos[data_adr], min, max);
}
ImGui::PopItemWidth();
}
void ControlsGui(const mjModel* model, const mjData* data,
const mjvOption* vis_options) {
const float item_width = ImGui::GetWindowWidth() * .6f;
ImGui::PushItemWidth(item_width);
if (ImGui::Button("Clear All")) {
mju_zero(data->ctrl, model->nu);
}
char name[100];
for (int i = 0; i < model->nu; i++) {
int group = std::clamp(model->actuator_group[i], 0, mjNGROUP - 1);
if (!vis_options->actuatorgroup[group]) {
continue;
}
if (group >= 0 && group <= 30 &&
model->opt.disableactuator & (1 << group)) {
continue;
}
const char* ctrl_name = model->names + model->name_actuatoradr[i];
if (*ctrl_name) {
std::snprintf(name, sizeof(name), "%s", ctrl_name);
} else {
std::snprintf(name, sizeof(name), "control %d", i);
}
double min = -1.0;
double max = 1.0;
if (model->actuator_ctrllimited[i]) {
min = model->actuator_ctrlrange[2 * i + 0];
max = model->actuator_ctrlrange[2 * i + 1];
}
ImGui_Slider(name, &data->ctrl[i], min, max);
}
ImGui::PopItemWidth();
}
static int GetPlotXLimit(const mjData* data) {
int max_niter = 0;
const int nisland0 =
data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0;
for (int k = 0; k < nisland0; k++) {
max_niter = mjMAX(max_niter, data->solver_niter[k]);
}
return max_niter <= 10 ? 10 : ((max_niter + 59) / 60) * 60;
}
void ConvergenceGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
ScopedStyle style;
style.Font(ScopedFont::kMono);
int xlim = GetPlotXLimit(data);
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
if (ImPlot::BeginPlot("Convergence vs iter", plot_size, flags)) {
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f);
ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, xlim, ImPlotCond_Always);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0e");
ImPlot::SetupAxisLimits(ImAxis_Y1, 1e-15, 1e0, ImPlotCond_Always);
ImPlot::SetupAxisScale(ImAxis_Y1, ImPlotScale_Log10);
const double ticks[] = {1e-15, 1e-12, 1e-9, 1e-6, 1e-3, 1e0};
ImPlot::SetupAxisTicks(ImAxis_Y1, ticks, 6);
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish();
const int nisland =
data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0;
for (int k = 0; k < nisland; k++) {
mjSolverStat* stats = data->solver + k * mjNSOLVER;
const int npoints =
mjMIN(mjMIN(data->solver_niter[k], mjNSOLVER), mjMAXLINEPNT);
ImPlot::PlotLineG(
"improvement",
+[](int i, void* user_data) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = mju_max(mjMINVAL, stats[i].improvement);
return ImPlotPoint{x, y};
},
stats, npoints);
if (model->opt.solver == mjSOL_PGS) {
continue;
}
ImPlot::PlotLineG(
"gradient",
+[](int i, void* user_data) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = mju_max(mjMINVAL, stats[i].gradient);
return ImPlotPoint{x, y};
},
stats, npoints);
ImPlot::PlotLineG(
"lineslope",
+[](int i, void* user_data) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = mju_max(mjMINVAL, stats[i].lineslope);
return ImPlotPoint{x, y};
},
stats, npoints);
}
ImPlot::PopStyleVar();
ImPlot::EndPlot();
}
}
void CountsGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
ScopedStyle style;
style.Font(ScopedFont::kMono);
int xlim = GetPlotXLimit(data);
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
if (ImPlot::BeginPlot("Counts vs iter", plot_size, flags)) {
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f);
ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, xlim, ImPlotCond_Always);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0f");
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 80, ImPlotCond_Always);
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish();
const int nisland =
data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0;
for (int k = 0; k < nisland; k++) {
const int npoints =
mjMIN(mjMIN(data->solver_niter[k], mjNSOLVER), mjMAXLINEPNT);
mjSolverStat* stats = data->solver + k * mjNSOLVER;
int nefc = nisland == 1 ? data->nefc : data->island_nefc[k];
ImPlot::PlotLineG(
"total",
+[](int i, void* user_data) {
const float x = static_cast<float>(i);
const float y = *(static_cast<int*>(user_data));
return ImPlotPoint{x, y};
},
&nefc, npoints);
ImPlot::PlotLineG(
"active",
+[](int i, void* user_data) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = stats[i].nactive;
return ImPlotPoint{x, y};
},
stats, npoints);
ImPlot::PlotLineG(
"changed",
+[](int i, void* user_data) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = stats[i].nchange;
return ImPlotPoint{x, y};
},
stats, npoints);
if (model->opt.solver == mjSOL_PGS) {
continue;
}
ImPlot::PlotLineG(
"evals",
+[](int i, void* user_data) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = stats[i].neval;
return ImPlotPoint{x, y};
},
stats, npoints);
if (model->opt.solver == mjSOL_CG) {
continue;
}
ImPlot::PlotLineG(
"updates",
+[](int i, void* user_data) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = stats[i].nupdate;
return ImPlotPoint{x, y};
},
stats, npoints);
}
ImPlot::PopStyleVar();
ImPlot::EndPlot();
}
}
void InfoGui(const mjModel* model, const mjData* data, bool paused,
float fps) {
const int num_islands = std::clamp(data->nisland, 1, mjNISLAND);
// compute solver error (maximum over islands)
mjtNum solver_err = 0;
int solver_iter = 0;
for (int i = 0; i < num_islands; i++) {
solver_iter += data->solver_niter[i];
mjtNum solerr_i = 0;
if (data->solver_niter[i]) {
const int ind = mjMIN(data->solver_niter[i], mjNSOLVER) - 1;
const mjSolverStat* stat = data->solver + i * mjNSOLVER + ind;
solerr_i = mju_min(stat->improvement, stat->gradient);
if (solerr_i == 0) {
solerr_i = mju_max(stat->improvement, stat->gradient);
}
}
solver_err = mju_max(solver_err, solerr_i);
}
solver_err = mju_log10(mju_max(mjMINVAL, solver_err));
auto type = paused ? mjTIMER_FORWARD : mjTIMER_STEP;
auto cpu = data->timer[type].duration / mjMAX(1, data->timer[type].number);
auto mempct = 100 * data->maxuse_arena / (double)(data->narena);
auto memlimit = mju_writeNumBytes(data->narena);
ImGui::Columns(2);
ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.4f);
ImGui::SetColumnWidth(1, ImGui::GetWindowWidth() * 0.6f);
ImGui::Text("Time");
ImGui::Text("Size");
ImGui::Text("CPU");
ImGui::Text("Solver");
ImGui::Text("FPS");
ImGui::Text("Memory");
if (model->opt.enableflags & mjENBL_ENERGY) {
ImGui::Text("Energy");
}
if (model->opt.enableflags & mjENBL_FWDINV) {
ImGui::Text("FwdInv");
}
if (!(model->opt.disableflags & mjDSBL_ISLAND)) {
ImGui::Text("Islands");
}
ImGui::NextColumn();
ImGui::Text("%-9.3f", data->time);
ImGui::Text("%d (%d con)", data->nefc, data->ncon);
ImGui::Text("%.3f", cpu);
ImGui::Text("%.1f (%d it)", solver_err, solver_iter);
ImGui::Text("%0.1f", fps);
ImGui::Text("%.1f%% of %s", mempct, memlimit);
if (model->opt.enableflags & mjENBL_ENERGY) {
ImGui::Text("%.3f", data->energy[0] + data->energy[1]);
}
if (model->opt.enableflags & mjENBL_FWDINV) {
ImGui::Text("%.1f %.1f",
mju_log10(mju_max(mjMINVAL, data->solver_fwdinv[0])),
mju_log10(mju_max(mjMINVAL, data->solver_fwdinv[1])));
}
if (!(model->opt.disableflags & mjDSBL_ISLAND)) {
ImGui::Text("%d", data->nisland);
}
ImGui::Columns();
}
void ProfilerGui(const mjModel* model, mjData* data, SimProfiler* profiler, bool show_iter) {
ImVec2 avail = ImGui::GetContentRegionAvail();
const float pad = ImGui::GetStyle().ItemSpacing.x;
const float aspect = avail.y > 0 ? avail.x / avail.y : 1.0f;
ImVec2 plot_size;
int cols;
int current_col = 0;
auto advance = [&]() {
current_col++;
if (current_col < cols) {
ImGui::SameLine();
} else {
current_col = 0;
}
};
if (!show_iter) {
if (aspect < 0.8f) {
plot_size.x = avail.x;
plot_size.y = (avail.y - pad) * 0.5f;
cols = 1;
} else {
plot_size.x = (avail.x - pad) * 0.5f;
plot_size.y = avail.y;
cols = 2;
}
profiler->DimensionsGraph(plot_size);
advance();
profiler->CpuTimeGraph(plot_size);
} else {
if (aspect < 0.8f) {
plot_size.x = avail.x;
plot_size.y = (avail.y - pad * 3.0f) * 0.25f;
cols = 1;
} else if (aspect < 1.8f) {
plot_size.x = (avail.x - pad) * 0.5f;
plot_size.y = (avail.y - pad) * 0.5f;
cols = 2;
} else {
plot_size.x = (avail.x - pad * 3.0f) * 0.25f;
plot_size.y = avail.y;
cols = 4;
}
if (cols == 2) {
// In 2x2 layout, vertically stack charts with the same x-axis.
CountsGui(model, data, plot_size);
advance();
profiler->DimensionsGraph(plot_size);
advance();
ConvergenceGui(model, data, plot_size);
advance();
profiler->CpuTimeGraph(plot_size);
} else {
CountsGui(model, data, plot_size);
advance();
ConvergenceGui(model, data, plot_size);
advance();
profiler->DimensionsGraph(plot_size);
advance();
profiler->CpuTimeGraph(plot_size);
}
}
}
} // namespace mujoco::platform