5d043d8f72
PiperOrigin-RevId: 936508032 Change-Id: I93806ba3d0569b5ee01e95384f7f835a201b8253
1592 lines
58 KiB
C++
1592 lines
58 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() {
|
|
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 = 36.f * scale * font_scale;
|
|
const float kStatusBarHeight = 32.f * scale * font_scale;
|
|
|
|
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.
|
|
{
|
|
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, (kToolsBarHeight - 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, kToolsBarHeight),
|
|
ImGuiCond_Always);
|
|
ImGui::Begin("ToolBar", nullptr, kFixedFlags);
|
|
ImGui::End();
|
|
}
|
|
|
|
// StatusBar is fixed at the bottom.
|
|
{
|
|
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(const mjModel* model, 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;
|
|
}
|
|
|
|
static 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(const mjModel* model, const mjData* data, float& noise_scale,
|
|
float& 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();
|
|
}
|
|
|
|
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 mjMAX(10, ((max_niter + 9) / 10) * 10);
|
|
}
|
|
|
|
void ConvergenceGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
|
|
int xlim = GetPlotXLimit(data);
|
|
ImPlotFlags flags =
|
|
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
|
|
if (ImPlot::BeginPlot("Convergence (log 10) 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, "%.1f");
|
|
ImPlot::SetupAxisLimits(ImAxis_Y1, -20, 5, 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++) {
|
|
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_log10(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_log10(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_log10(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) {
|
|
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) {
|
|
ImGui::SetWindowFontScale(0.8f);
|
|
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;
|
|
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;
|
|
}
|
|
|
|
int current_col = 0;
|
|
auto advance = [&]() {
|
|
current_col++;
|
|
if (current_col < cols) {
|
|
ImGui::SameLine();
|
|
} else {
|
|
current_col = 0;
|
|
}
|
|
};
|
|
|
|
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
|