Make studio python more like studio C++
- C++ Platform UX: Added a "Copy Camera" button to copy XML camera definitions to clipboard. - C++ Platform UX: Extracted `SetSpeedIndex` and `GetExpectedLabelWidth` into reusable platform helpers. - C++ Platform UX: Fixed combo box widths and removed auto-hide tab bar flag from dockspace. - C++ Studio App: Deduplicated label width and speed index logic by adopting platform helpers. - C++ Studio App: Updated the toolbar layout to use a 2-column table. - Python Bindings: Added pybind definitions for `set_camera_index`, `set_speed_index`, and `camera_to_string`. - Python Studio App: Aligned toolbar UI with C++ using a 2-column table and fixed immediate theme switching. - Python Studio App: Added a Help menu displaying the MuJoCo version and Stats toggle. - Python Events: Added `-`/`=` shortcuts for simulation speed and `Esc`/`[`/`]` shortcuts for camera selection. PiperOrigin-RevId: 922097319 Change-Id: Iec74afe37bf5c7a1ebf1cab4141e47fff5fe9274
This commit is contained in:
committed by
Copybara-Service
parent
79aeceeaa3
commit
c0b8aa9dec
@@ -316,8 +316,7 @@ ImVec4 ConfigureDockingLayout() {
|
|||||||
ImGui::Begin("Dockspace", nullptr, kWorkspaceFlags);
|
ImGui::Begin("Dockspace", nullptr, kWorkspaceFlags);
|
||||||
const ImGuiDockNodeFlags kDockSpaceFlags =
|
const ImGuiDockNodeFlags kDockSpaceFlags =
|
||||||
ImGuiDockNodeFlags_PassthruCentralNode |
|
ImGuiDockNodeFlags_PassthruCentralNode |
|
||||||
ImGuiDockNodeFlags_NoDockingOverCentralNode |
|
ImGuiDockNodeFlags_NoDockingOverCentralNode;
|
||||||
ImGuiDockNodeFlags_AutoHideTabBar;
|
|
||||||
ImGui::DockSpace(root, ImVec2(0.0f, 0.0f), kDockSpaceFlags);
|
ImGui::DockSpace(root, ImVec2(0.0f, 0.0f), kDockSpaceFlags);
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
@@ -426,9 +425,7 @@ void StepControlGui(const mjModel* model, StepControl* step_control,
|
|||||||
ImGuiComboFlags_NoArrowButton)) {
|
ImGuiComboFlags_NoArrowButton)) {
|
||||||
for (int n = 0; n < kPercentRealTime.size(); n++) {
|
for (int n = 0; n < kPercentRealTime.size(); n++) {
|
||||||
if (ImGui::Selectable(kPercentRealTime[n], (speed_index == n))) {
|
if (ImGui::Selectable(kPercentRealTime[n], (speed_index == n))) {
|
||||||
speed_index = std::clamp<int>(n, 0, kPercentRealTime.size() - 1);
|
SetSpeedIndex(step_control, speed_index, n);
|
||||||
float speed = std::stof(kPercentRealTime[speed_index]);
|
|
||||||
step_control->SetSpeed(speed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
@@ -442,6 +439,17 @@ void StepControlGui(const mjModel* model, StepControl* step_control,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size) {
|
||||||
static constexpr const char* ICON_DARKMODE = ICON_FA_CIRCLE;
|
static constexpr const char* ICON_DARKMODE = ICON_FA_CIRCLE;
|
||||||
static constexpr const char* ICON_LIGHTMODE = ICON_FA_CIRCLE_O;
|
static constexpr const char* ICON_LIGHTMODE = ICON_FA_CIRCLE_O;
|
||||||
@@ -471,6 +479,7 @@ bool LabelSelectionGui(mjvOption* opts) {
|
|||||||
const std::string label_preview =
|
const std::string label_preview =
|
||||||
opts->label == 0 ? std::string(ICON_LABEL) + " Label"
|
opts->label == 0 ? std::string(ICON_LABEL) + " Label"
|
||||||
: std::string(ICON_LABEL) + " " + kLabelNames[opts->label];
|
: std::string(ICON_LABEL) + " " + kLabelNames[opts->label];
|
||||||
|
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
|
||||||
if (ImGui::BeginCombo("##Label", label_preview.c_str(),
|
if (ImGui::BeginCombo("##Label", label_preview.c_str(),
|
||||||
ImGuiComboFlags_NoArrowButton)) {
|
ImGuiComboFlags_NoArrowButton)) {
|
||||||
for (int n = 0; n < IM_ARRAYSIZE(kLabelNames); n++) {
|
for (int n = 0; n < IM_ARRAYSIZE(kLabelNames); n++) {
|
||||||
@@ -494,6 +503,7 @@ bool FrameSelectionGui(mjvOption* opts) {
|
|||||||
const std::string frame_preview =
|
const std::string frame_preview =
|
||||||
opts->frame == 0 ? std::string(ICON_FRAME) + " Frame"
|
opts->frame == 0 ? std::string(ICON_FRAME) + " Frame"
|
||||||
: std::string(ICON_FRAME) + " " + kFrameNames[opts->frame];
|
: std::string(ICON_FRAME) + " " + kFrameNames[opts->frame];
|
||||||
|
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
|
||||||
if (ImGui::BeginCombo("##Frame", frame_preview.c_str(),
|
if (ImGui::BeginCombo("##Frame", frame_preview.c_str(),
|
||||||
ImGuiComboFlags_NoArrowButton)) {
|
ImGuiComboFlags_NoArrowButton)) {
|
||||||
for (int n = 0; n < IM_ARRAYSIZE(kFrameNames); n++) {
|
for (int n = 0; n < IM_ARRAYSIZE(kFrameNames); n++) {
|
||||||
@@ -530,6 +540,19 @@ static std::string GetCameraName(const mjModel* model, const mjvCamera& camera,
|
|||||||
bool CameraSelectionGui(const mjModel* model, mjData* data, mjvCamera& camera,
|
bool CameraSelectionGui(const mjModel* model, mjData* data, mjvCamera& camera,
|
||||||
int& index) {
|
int& index) {
|
||||||
static constexpr const char* ICON_CAMERA = ICON_FA_CAMERA;
|
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) {
|
auto select = [&](int type, int idx) {
|
||||||
if (ImGui::Selectable(GetCameraName(model, camera, type).c_str(),
|
if (ImGui::Selectable(GetCameraName(model, camera, type).c_str(),
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ static constexpr std::array<const char*, 31> kPercentRealTime = {
|
|||||||
void StepControlGui(const mjModel* model, StepControl* step_control,
|
void StepControlGui(const mjModel* model, StepControl* step_control,
|
||||||
int& speed_index);
|
int& speed_index);
|
||||||
|
|
||||||
|
// Sets the simulation speed index and updates the StepControl object.
|
||||||
|
void SetSpeedIndex(StepControl* step_control, int& speed_index,
|
||||||
|
int request_idx);
|
||||||
|
|
||||||
// UX for selecting the GUI theme.
|
// UX for selecting the GUI theme.
|
||||||
bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size = ImVec2(0, 0));
|
bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size = ImVec2(0, 0));
|
||||||
|
|
||||||
|
|||||||
@@ -386,6 +386,23 @@ void MaybeSaveToClipboard(const std::string& contents) {
|
|||||||
ImGui::SetClipboardText(contents.c_str());
|
ImGui::SetClipboardText(contents.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float GetExpectedLabelWidth() {
|
||||||
|
static float expected_label_width = 0;
|
||||||
|
if (expected_label_width == 0) {
|
||||||
|
int longest = 0;
|
||||||
|
const char* longest_label = "";
|
||||||
|
for (int i = 0; i < mjNVISFLAG; ++i) {
|
||||||
|
int length = static_cast<int>(std::strlen(mjVISSTRING[i][0]));
|
||||||
|
if (length > longest) {
|
||||||
|
longest_label = mjVISSTRING[i][0];
|
||||||
|
longest = length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expected_label_width = ImGui::CalcTextSize(longest_label).x + 16;
|
||||||
|
}
|
||||||
|
return expected_label_width;
|
||||||
|
}
|
||||||
|
|
||||||
ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size) {
|
ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size) {
|
||||||
ImPlotFlags flags = ImPlotFlags_None;
|
ImPlotFlags flags = ImPlotFlags_None;
|
||||||
if (plot_size.x > 0 && plot_size.y > 0) {
|
if (plot_size.x > 0 && plot_size.y > 0) {
|
||||||
|
|||||||
@@ -617,6 +617,9 @@ inline void EndBoxSection() { ImGui::EndTable(); }
|
|||||||
// Saves the given contents to the clipboard if the clipboard is available.
|
// Saves the given contents to the clipboard if the clipboard is available.
|
||||||
void MaybeSaveToClipboard(const std::string& contents);
|
void MaybeSaveToClipboard(const std::string& contents);
|
||||||
|
|
||||||
|
// Returns the expected width of a UI label based on MuJoCo visualization flags.
|
||||||
|
float GetExpectedLabelWidth();
|
||||||
|
|
||||||
// Returns plot flags with title/legend conditionally hidden when the plot
|
// Returns plot flags with title/legend conditionally hidden when the plot
|
||||||
// area is too small. `plot_size` is the final rendered size of the plot.
|
// area is too small. `plot_size` is the final rendered size of the plot.
|
||||||
ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size);
|
ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size);
|
||||||
|
|||||||
@@ -826,14 +826,7 @@ void App::SaveSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void App::SetSpeedIndex(int idx) {
|
void App::SetSpeedIndex(int idx) {
|
||||||
if (idx == tmp_.speed_index || platform::kPercentRealTime.empty()) {
|
platform::SetSpeedIndex(&step_control_, tmp_.speed_index, idx);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp_.speed_index =
|
|
||||||
std::clamp<int>(idx, 0, platform::kPercentRealTime.size() - 1);
|
|
||||||
float speed = std::stof(platform::kPercentRealTime[tmp_.speed_index]);
|
|
||||||
step_control_.SetSpeed(speed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::MoveCamera(platform::CameraMotion motion, mjtNum reldx,
|
void App::MoveCamera(platform::CameraMotion motion, mjtNum reldx,
|
||||||
@@ -1022,7 +1015,7 @@ void App::BuildGui() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void App::ModelOptionsGui() {
|
void App::ModelOptionsGui() {
|
||||||
const float min_width = GetExpectedLabelWidth();
|
const float min_width = platform::GetExpectedLabelWidth();
|
||||||
const ImGuiChildFlags child_flags =
|
const ImGuiChildFlags child_flags =
|
||||||
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize;
|
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize;
|
||||||
const ImGuiTreeNodeFlags node_flags =
|
const ImGuiTreeNodeFlags node_flags =
|
||||||
@@ -1064,7 +1057,7 @@ void App::DataInspectorGui() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float min_width = GetExpectedLabelWidth();
|
const float min_width = platform::GetExpectedLabelWidth();
|
||||||
const ImGuiChildFlags child_flags =
|
const ImGuiChildFlags child_flags =
|
||||||
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize;
|
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize;
|
||||||
const ImGuiTreeNodeFlags node_flags =
|
const ImGuiTreeNodeFlags node_flags =
|
||||||
@@ -1424,15 +1417,14 @@ void App::ToolBarGui() {
|
|||||||
ImVec2(ImGui::GetStyle().ItemSpacing.x * 2.0f,
|
ImVec2(ImGui::GetStyle().ItemSpacing.x * 2.0f,
|
||||||
ImGui::GetStyle().ItemSpacing.y));
|
ImGui::GetStyle().ItemSpacing.y));
|
||||||
|
|
||||||
const float label_width = GetExpectedLabelWidth();
|
const float label_width = platform::GetExpectedLabelWidth();
|
||||||
const float copy_btn_width = ImGui::CalcTextSize(ICON_COPY_CAMERA).x +
|
const float copy_btn_width = ImGui::GetFrameHeight();
|
||||||
ImGui::GetStyle().FramePadding.x * 2;
|
|
||||||
const float theme_width =
|
const float theme_width =
|
||||||
ImGui::CalcTextSize(platform::ICON_FA_CIRCLE_O).x +
|
ImGui::CalcTextSize(platform::ICON_FA_CIRCLE_O).x +
|
||||||
ImGui::GetStyle().FramePadding.x * 2;
|
ImGui::GetStyle().FramePadding.x * 2;
|
||||||
const float sp = ImGui::GetStyle().ItemSpacing.x;
|
const float sp = ImGui::GetStyle().ItemSpacing.x;
|
||||||
const float right_width = label_width + sp + label_width + sp +
|
const float right_width = copy_btn_width + label_width + sp +
|
||||||
label_width + sp + copy_btn_width + sp +
|
label_width + sp + label_width + sp +
|
||||||
theme_width;
|
theme_width;
|
||||||
const float separator_width = ImGui::GetFrameHeight() * .6f;
|
const float separator_width = ImGui::GetFrameHeight() * .6f;
|
||||||
|
|
||||||
@@ -1467,26 +1459,15 @@ void App::ToolBarGui() {
|
|||||||
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
|
|
||||||
if (ImGui::Button(ICON_COPY_CAMERA, square_size)) {
|
|
||||||
std::string camera_string = platform::CameraToString(data(), &camera_);
|
|
||||||
platform::MaybeSaveToClipboard(camera_string);
|
|
||||||
}
|
|
||||||
ImGui::SetItemTooltip("%s", "Copy Camera");
|
|
||||||
|
|
||||||
ImGui::SameLine(0, 0);
|
|
||||||
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
|
|
||||||
platform::CameraSelectionGui(model(), data(), camera_, ui_.camera_idx);
|
platform::CameraSelectionGui(model(), data(), camera_, ui_.camera_idx);
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
|
|
||||||
platform::LabelSelectionGui(&vis_options_);
|
platform::LabelSelectionGui(&vis_options_);
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
|
|
||||||
platform::FrameSelectionGui(&vis_options_);
|
platform::FrameSelectionGui(&vis_options_);
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::SetNextItemWidth(GetExpectedLabelWidth());
|
|
||||||
if (platform::ThemeSelectGui(&ui_.theme, square_size)) {
|
if (platform::ThemeSelectGui(&ui_.theme, square_size)) {
|
||||||
platform::SetupTheme(ui_.theme);
|
platform::SetupTheme(ui_.theme);
|
||||||
ImGui::GetIO().WantSaveIniSettings = true;
|
ImGui::GetIO().WantSaveIniSettings = true;
|
||||||
@@ -1836,27 +1817,6 @@ void App::FileDialogGui() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float App::GetExpectedLabelWidth() {
|
|
||||||
// Find the longest label which we'll use to set the minimum toggle button
|
|
||||||
// width. This isn't perfect because we may have labels that are longer, but
|
|
||||||
// it's a good enough approximation.
|
|
||||||
if (tmp_.expected_label_width == 0) {
|
|
||||||
int longest = 0;
|
|
||||||
const char* longest_label = "";
|
|
||||||
for (int i = 0; i < mjNVISFLAG; ++i) {
|
|
||||||
int length = static_cast<int>(strlen(mjVISSTRING[i][0]));
|
|
||||||
if (length > longest) {
|
|
||||||
longest_label = mjVISSTRING[i][0];
|
|
||||||
longest = length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Pad the width a bit to account for how the labels will be displayed
|
|
||||||
// (e.g. as button labels or besides checkboxes).
|
|
||||||
tmp_.expected_label_width = ImGui::CalcTextSize(longest_label).x + 16;
|
|
||||||
}
|
|
||||||
return tmp_.expected_label_width;
|
|
||||||
}
|
|
||||||
|
|
||||||
App::UiState::Dict App::UiState::ToDict() const {
|
App::UiState::Dict App::UiState::ToDict() const {
|
||||||
return {
|
return {
|
||||||
{"theme", std::to_string(static_cast<int>(theme))},
|
{"theme", std::to_string(static_cast<int>(theme))},
|
||||||
|
|||||||
@@ -224,8 +224,6 @@ class App {
|
|||||||
void SpecExplorerGui();
|
void SpecExplorerGui();
|
||||||
void SpecEditorGui();
|
void SpecEditorGui();
|
||||||
|
|
||||||
float GetExpectedLabelWidth();
|
|
||||||
|
|
||||||
mjSpec* spec() { return model_holder_->spec(); }
|
mjSpec* spec() { return model_holder_->spec(); }
|
||||||
mjModel* model() { return model_holder_->model(); }
|
mjModel* model() { return model_holder_->model(); }
|
||||||
mjData* data() { return model_holder_->data(); }
|
mjData* data() { return model_holder_->data(); }
|
||||||
|
|||||||
Reference in New Issue
Block a user