From 989b6e63c78a7ad610c3e1d6db4de98be9cc4043 Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Tue, 17 Feb 2026 02:14:06 -0800 Subject: [PATCH] Add ability to delete bodies. PiperOrigin-RevId: 871200603 Change-Id: I08bc786ca8106ef13cfcbe89e8cc42e9ad2c99d5 --- src/experimental/studio/app.cc | 105 ++++++++++++++++++++++----------- src/experimental/studio/app.h | 9 ++- 2 files changed, 77 insertions(+), 37 deletions(-) diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index ac6d8825..8dbfdad8 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -84,6 +84,7 @@ static constexpr const char* ICON_PREV_FRAME = platform::ICON_FA_CARET_LEFT; static constexpr const char* ICON_NEXT_FRAME = platform::ICON_FA_CARET_RIGHT; static constexpr const char* ICON_CURR_FRAME = platform::ICON_FA_FAST_FORWARD; static constexpr const char* ICON_SPEED = platform::ICON_FA_TACHOMETER; +static constexpr const char* ICON_DELETE = platform::ICON_FA_TRASH_CAN; // UI labels for mjtLabel. static constexpr const char* kLabelNames[] = { @@ -132,6 +133,13 @@ void App::ClearModel() { step_error_ = ""; } +void App::Recompile() { + mj_recompile(model_holder_->spec(), model_holder_->vfs(), + model_holder_->model(), model_holder_->data()); + const int state_size = mj_stateSize(model(), mjSTATE_INTEGRATION); + history_.Init(state_size); +} + void App::RequestModelLoad(std::string model_file) { pending_load_ = std::move(model_file); } @@ -365,6 +373,11 @@ void App::ProcessPendingLoads() { } } + if (spec_op_) { + spec_op_(); + spec_op_ = nullptr; + } + // Check plugins to see if we need to load a new model. platform::ForEachModelPlugin([&](platform::ModelPlugin* plugin) { if (plugin->get_model_to_load) { @@ -382,6 +395,19 @@ void App::ProcessPendingLoads() { }); } +void App::SpecDeleteSelectedElement() { + spec_op_ = [this]() { + mjs_delete(spec(), tmp_.element); + if (tmp_.element->elemtype == mjOBJ_BODY && + perturb_.select == tmp_.element_id) { + mjv_defaultPerturb(&perturb_); + } + tmp_.element = nullptr; + tmp_.element_id = -1; + Recompile(); + }; +} + void App::HandleWindowEvents() { const std::string drop_file = window_->GetDropFile(); if (!drop_file.empty()) { @@ -474,15 +500,28 @@ void App::HandleMouseEvents() { perturb_.flexselect = picked.flex; perturb_.skinselect = picked.skin; + // Select the corresponding element in the spec. + tmp_.element = nullptr; + tmp_.element_id = -1; + if (has_spec()) { + mjsElement* element = mjs_firstElement(spec(), mjOBJ_BODY); + while (element) { + if (mjs_getId(element) == picked.body) { + tmp_.element = element; + tmp_.element_id = picked.body; + break; + } + element = mjs_nextElement(spec(), element); + } + } + // Compute the local position of the selected object in the world. mjtNum tmp[3]; mju_sub3(tmp, picked.point, data()->xpos + 3 * picked.body); mju_mulMatTVec(perturb_.localpos, data()->xmat + 9 * picked.body, tmp, 3, 3); } else { - perturb_.select = 0; - perturb_.flexselect = -1; - perturb_.skinselect = -1; + mjv_defaultPerturb(&perturb_); } } @@ -571,6 +610,8 @@ void App::HandleKeyboardEvents() { } } else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) { ResetPhysics(); + } else if (ImGui_IsChordJustPressed(ImGuiKey_Delete)) { + SpecDeleteSelectedElement(); } else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) { SelectParentPerturb(model(), perturb_); } else if (ImGui_IsChordJustPressed(ImGuiKey_F1)) { @@ -1082,7 +1123,8 @@ void App::SpecExplorerGui() { label = "(" + prefix + " " + std::to_string(id) + ")"; } - if (ImGui::Selectable(label.c_str(), false)) { + const bool selected = (tmp_.element == element); + if (ImGui::Selectable(label.c_str(), selected)) { tmp_.element = element; tmp_.element_id = id; } @@ -1092,42 +1134,24 @@ void App::SpecExplorerGui() { }; if (ImGui::TreeNodeEx("Bodies", flags)) { - // We don't use `display_group` here because we do additional selection - // logic tied to the `perturb_` field. - mjsElement* element = mjs_firstElement(spec(), mjOBJ_BODY); - while (element) { - const int id = mjs_getId(element); - - const mjString* name = mjs_getName(element); - std::string label = *name; - if (label.empty()) { - label = "(Body " + std::to_string(id) + ")"; - } - - if (ImGui::Selectable(label.c_str(), (id == perturb_.select), - ImGuiSelectableFlags_AllowDoubleClick)) { - tmp_.element = element; - tmp_.element_id = id; - } - if (ImGui::IsItemHovered() && - ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - perturb_.select = id; - } - - element = mjs_nextElement(spec(), element); - } + display_group(mjOBJ_BODY, "Body"); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Joints", flags)) { display_group(mjOBJ_JOINT, "Joint"); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Sites", flags)) { display_group(mjOBJ_SITE, "Site"); ImGui::TreePop(); } + + // If we selected a body, then select the same body for the perturb object. + if (tmp_.element && tmp_.element->elemtype == mjOBJ_BODY && + perturb_.select != tmp_.element_id) { + mjv_defaultPerturb(&perturb_); + perturb_.select = tmp_.element_id; + } } void App::PropertiesGui() { @@ -1136,22 +1160,31 @@ void App::PropertiesGui() { return; } + if (ImGui::BeginTable("##PropertiesHeader", 2)) { + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 20); + ImGui::TableNextColumn(); + ImGui::Text("%s", mju_type2Str(tmp_.element->elemtype)); + ImGui::TableNextColumn(); + if (tmp_.element->elemtype == mjOBJ_BODY) { + if (ImGui::SmallButton(ICON_DELETE)) { + SpecDeleteSelectedElement(); + } + } + ImGui::EndTable(); + } + ImGui::Separator(); + switch (tmp_.element->elemtype) { case mjOBJ_BODY: - ImGui::Text("Body"); - ImGui::Separator(); platform::BodyPropertiesGui(model(), data(), tmp_.element, tmp_.element_id); break; case mjOBJ_JOINT: - ImGui::Text("Joint"); - ImGui::Separator(); platform::JointPropertiesGui(model(), data(), tmp_.element, tmp_.element_id); break; case mjOBJ_SITE: - ImGui::Text("Site"); - ImGui::Separator(); platform::SitePropertiesGui(model(), data(), tmp_.element, tmp_.element_id); break; diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index d8b8fd23..c6d6d065 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -16,6 +16,7 @@ #define MUJOCO_SRC_EXPERIMENTAL_STUDIO_APP_H_ #include +#include #include #include #include @@ -170,6 +171,9 @@ class App { // Clears the currently loaded model and all associated state. void ClearModel(); + // Recompiles the spec, updating the model and data. + void Recompile(); + // Updates the currently loaded model to the given model. If model is null, // then compile the spec to a model. void OnModelLoaded(std::string filename, ModelKind model_kind); @@ -207,6 +211,8 @@ class App { void SpecExplorerGui(); void PropertiesGui(); + void SpecDeleteSelectedElement(); + float GetExpectedLabelWidth(); std::vector GetCameraNames(); @@ -217,7 +223,6 @@ class App { bool has_model() const { return model_holder_ && model_holder_->model(); } bool has_data() const { return model_holder_ && model_holder_->data(); } - std::string ini_path_; std::string model_name_; // Used if model_kind_ is kModelFromBuffer. std::string model_path_; @@ -229,6 +234,8 @@ class App { std::unique_ptr window_; std::unique_ptr renderer_; std::unique_ptr model_holder_; + std::function spec_op_; + platform::StepControl step_control_; platform::SimProfiler profiler_; platform::SimHistory history_;