From 1d6ff2cecec1290f0610818482476113ee3787ae Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Tue, 24 Feb 2026 07:57:01 -0800 Subject: [PATCH] Display Bodies as a tree in Spec Explorer. Display all other elements under "Elements" or "Assets" group. Show properties for mjSpec, mjModel, and mjData for selected elements. PiperOrigin-RevId: 874614195 Change-Id: I128c2bec844983ac5d35a3a0b1e58b8a5b0109cb --- src/experimental/platform/CMakeLists.txt | 2 + src/experimental/platform/gui.cc | 109 +-- src/experimental/platform/gui.h | 8 - src/experimental/platform/gui_spec.cc | 847 +++++++++++++++++++++ src/experimental/platform/gui_spec.h | 37 + src/experimental/platform/imgui_widgets.cc | 226 ++++++ src/experimental/platform/imgui_widgets.h | 80 ++ src/experimental/studio/app.cc | 194 +++-- src/experimental/studio/app.h | 13 +- 9 files changed, 1290 insertions(+), 226 deletions(-) create mode 100644 src/experimental/platform/gui_spec.cc create mode 100644 src/experimental/platform/gui_spec.h diff --git a/src/experimental/platform/CMakeLists.txt b/src/experimental/platform/CMakeLists.txt index d4a58d23..8ba7aa6f 100644 --- a/src/experimental/platform/CMakeLists.txt +++ b/src/experimental/platform/CMakeLists.txt @@ -42,6 +42,8 @@ target_sources(${MUJOCO_PLATFORM_TARGET_NAME} file_dialog.h gui.cc gui.h + gui_spec.cc + gui_spec.h helpers.cc helpers.h imgui_widgets.cc diff --git a/src/experimental/platform/gui.cc b/src/experimental/platform/gui.cc index 5cfc9f2b..2d29eeca 100644 --- a/src/experimental/platform/gui.cc +++ b/src/experimental/platform/gui.cc @@ -470,7 +470,7 @@ void StateGui(const mjModel* model, mjData* data, std::vector& state, ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 20))) { ImGui::TableSetupColumn("Index"); ImGui::TableSetupColumn("Name"); - ImGui::TableSetupColumn("Value"); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); @@ -1107,111 +1107,4 @@ void StatsGui(const mjModel* model, const mjData* data, bool paused, ImGui::Columns(); } -void BodyPropertiesGui(const mjModel* model, const mjData* data, - mjsElement* element, int id) { - const mjsBody* body = mjs_asBody(element); - - ImGui::Columns(2); - ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.4f); - ImGui::SetColumnWidth(1, ImGui::GetWindowWidth() * 0.6f); - - std::string name = *mjs_getName(body->element); - if (name.empty()) { - name = "(Body " + std::to_string(id) + ")"; - } - - ImGui::Columns(2); - ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.3f); - ImGui::SetColumnWidth(1, ImGui::GetWindowWidth() * 0.7f); - - ImGui::Text("Name"); - ImGui::Text("xpos[0]"); - ImGui::Text("xpos[1]"); - ImGui::Text("xpos[2]"); - ImGui::Text("xquat[0]"); - ImGui::Text("xquat[1]"); - ImGui::Text("xquat[2]"); - ImGui::Text("xquat[3]"); - ImGui::Text("mass"); - - ImGui::NextColumn(); - ImGui::Text("%s", name.c_str()); - ImGui::Text("%f", data->xpos[3*id+0]); - ImGui::Text("%f", data->xpos[3*id+1]); - ImGui::Text("%f", data->xpos[3*id+2]); - ImGui::Text("%f", data->xquat[4*id+0]); - ImGui::Text("%f", data->xquat[4*id+1]); - ImGui::Text("%f", data->xquat[4*id+2]); - ImGui::Text("%f", data->xquat[4*id+3]); - ImGui::Text("%f", model->body_mass[id]); -} - -void JointPropertiesGui(const mjModel* model, const mjData* data, - mjsElement* element, int id) { - const mjsJoint* joint = mjs_asJoint(element); - - ImGui::Columns(2); - ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.4f); - ImGui::SetColumnWidth(1, ImGui::GetWindowWidth() * 0.6f); - - std::string name = *mjs_getName(joint->element); - if (name.empty()) { - name = "(Joint " + std::to_string(id) + ")"; - } - - ImGui::Columns(2); - ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.3f); - ImGui::SetColumnWidth(1, ImGui::GetWindowWidth() * 0.7f); - ImGui::Text("Name"); - - ImGui::NextColumn(); - ImGui::Text("%s", name.c_str()); -} - -void SitePropertiesGui(const mjModel* model, const mjData* data, - mjsElement* element, int id) { - const mjsSite* site = mjs_asSite(element); - - ImGui::Columns(2); - ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.4f); - ImGui::SetColumnWidth(1, ImGui::GetWindowWidth() * 0.6f); - - std::string name = *mjs_getName(site->element); - if (name.empty()) { - name = "(Joint " + std::to_string(id) + ")"; - } - - ImGui::Columns(2); - ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.3f); - ImGui::SetColumnWidth(1, ImGui::GetWindowWidth() * 0.7f); - ImGui::Text("Name"); - ImGui::Text("site_xpos[0]"); - ImGui::Text("site_xpos[1]"); - ImGui::Text("site_xpos[2]"); - ImGui::Text("site_xmat[0]"); - ImGui::Text("site_xmat[1]"); - ImGui::Text("site_xmat[2]"); - ImGui::Text("site_xmat[3]"); - ImGui::Text("site_xmat[4]"); - ImGui::Text("site_xmat[5]"); - ImGui::Text("site_xmat[6]"); - ImGui::Text("site_xmat[7]"); - ImGui::Text("site_xmat[8]"); - - ImGui::NextColumn(); - ImGui::Text("%s", name.c_str()); - ImGui::Text("%f", data->site_xpos[3*id+0]); - ImGui::Text("%f", data->site_xpos[3*id+1]); - ImGui::Text("%f", data->site_xpos[3*id+2]); - ImGui::Text("%f", data->site_xmat[4*id+0]); - ImGui::Text("%f", data->site_xmat[4*id+1]); - ImGui::Text("%f", data->site_xmat[4*id+2]); - ImGui::Text("%f", data->site_xmat[4*id+3]); - ImGui::Text("%f", data->site_xmat[4*id+4]); - ImGui::Text("%f", data->site_xmat[4*id+5]); - ImGui::Text("%f", data->site_xmat[4*id+6]); - ImGui::Text("%f", data->site_xmat[4*id+7]); - ImGui::Text("%f", data->site_xmat[4*id+8]); -} - } // namespace mujoco::platform diff --git a/src/experimental/platform/gui.h b/src/experimental/platform/gui.h index 2341023e..7dbdae09 100644 --- a/src/experimental/platform/gui.h +++ b/src/experimental/platform/gui.h @@ -116,14 +116,6 @@ void CountsGui(const mjModel* model, mjData* data); // FPS needs to be tracked by the caller and passed here to be displayed. void StatsGui(const mjModel* model, const mjData* data, bool paused, float fps); -// UX for displaying properties of various mjSpec elements. -void BodyPropertiesGui(const mjModel* model, const mjData* data, - mjsElement* element, int id); -void JointPropertiesGui(const mjModel* model, const mjData* data, - mjsElement* element, int id); -void SitePropertiesGui(const mjModel* model, const mjData* data, - mjsElement* element, int id); - } // namespace mujoco::platform #endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_H_ diff --git a/src/experimental/platform/gui_spec.cc b/src/experimental/platform/gui_spec.cc new file mode 100644 index 00000000..eeb74581 --- /dev/null +++ b/src/experimental/platform/gui_spec.cc @@ -0,0 +1,847 @@ +// Copyright 2026 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/gui_spec.h" + +#include +#include + +#include +#include +#include +#include "experimental/platform/imgui_widgets.h" + +// Define the mujoco X macros to add fields to the ImGui_DataTable. +// We limit the fields to the ones with a matching element by comparing the +// array size field (e.g. nbody) with the MATCH constexpr value. +#define X(TYPE, NAME, NELEM, SIZE) \ + if constexpr (#NELEM == MATCH) table(#NAME, ptr->NAME, SIZE); + +// Simple wrapper around MJMODEL_POINTERS that prepares values we need for the +// X macro above. +#define MJMODEL_POINTERS_X(M) \ + constexpr std::string_view MATCH(#M); \ + const auto* ptr = model; \ + MJMODEL_POINTERS + +// Simple wrapper around MJDATA_POINTERS that prepares values we need for the +// X macro above. +#define MJDATA_POINTERS_X(M) \ + constexpr std::string_view MATCH(#M); \ + const auto* ptr = data; \ + MJDATA_POINTERS + +namespace mujoco::platform { + +// Returns the index of the element in the spec. This is different from +// mjs_getId which returns the runtime ID of an element. +static int GetElementIndexInSpec(mjsElement* element) { + int n = 0; + mjSpec* spec = mjs_getSpec(element); + mjsElement* iter = mjs_firstElement(spec, element->elemtype); + while (iter) { + if (iter == element) { + return n; + } + iter = mjs_nextElement(spec, iter); + ++n; + } + return -1; +} + +// Returns a name for the element; either the element has a name, or we +// construct a unique name from the element's id (using mjs_getId) or index +// (using GetElementIndexInSpec). +static std::string ElementName(mjsElement* element) { + const mjString* name = mjs_getName(element); + std::string label = *name; + if (label.empty()) { + int id = mjs_getId(element); + if (id == -1) { + id = GetElementIndexInSpec(element); + } + const char* type_name = mju_type2Str(element->elemtype); + label = "(" + std::string(type_name) + " " + std::to_string(id) + ")"; + } + return label; +} + +static void QuatOrOrientation(ImGui_DataTable& table, const double quat[4], + const mjsOrientation& orientation, + const char* quat_name, const char* alt_name) { + auto alt = + [&](const char* label) { return std::string(alt_name) + "." + label; }; + + switch (orientation.type) { + case mjORIENTATION_QUAT: + table(quat_name, quat, 4); + break; + case mjORIENTATION_AXISANGLE: + table(alt("axisangle").c_str(), orientation.axisangle, 4); + break; + case mjORIENTATION_XYAXES: + table(alt("xyaxes").c_str(), orientation.xyaxes, 6); + break; + case mjORIENTATION_ZAXIS: + table(alt("zaxis").c_str(), orientation.zaxis, 3); + break; + case mjORIENTATION_EULER: + table(alt("euler").c_str(), orientation.euler, 3); + break; + } +} + +static void AddDeleteButton(mjsElement* element, + const SpecElementCallbackFn& on_delete) { + if (on_delete) { + // Right-align the delete button. + const float button_width = ImGui::CalcTextSize(ICON_FA_TRASH_CAN).x + + ImGui::GetStyle().FramePadding.x * 2.0f; + ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - button_width); + if (ImGui::SmallButton(ICON_FA_TRASH_CAN)) { + on_delete(element); + } + } +} + +static void SelectableElement(mjsElement* element, + mjsElement** selected_element, + const SpecElementCallbackFn& on_delete) { + constexpr ImGuiSelectableFlags flags = ImGuiSelectableFlags_AllowOverlap; + + const std::string name = ElementName(element); + const bool selected = (element == *selected_element); + if (ImGui::Selectable(name.c_str(), selected, flags)) { + *selected_element = element; + } + if (selected) { + AddDeleteButton(element, on_delete); + } +} + +static void BodyChildrenGui(const char* heading, mjtObj type, + mjsElement** element, mjsBody* body, + const SpecElementCallbackFn& on_delete) { + mjsElement* iter = mjs_firstChild(body, type, 0); + if (!iter) { + return; + } + + constexpr ImGuiTreeNodeFlags tree_flags = + ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_DrawLinesFull; + if (ImGui::TreeNodeEx(heading, tree_flags)) { + while (iter) { + SelectableElement(iter, element, on_delete); + iter = mjs_nextChild(body, iter, 0); + } + ImGui::TreePop(); + } +} + +static void ElementListGui(const char* heading, mjtObj type, + mjsElement** element, mjSpec* spec, + const SpecElementCallbackFn& on_delete) { + mjsElement* iter = mjs_firstElement(spec, type); + if (!iter) { + return; + } + + constexpr ImGuiTreeNodeFlags tree_flags = + ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; + if (ImGui::TreeNodeEx(heading, tree_flags)) { + while (iter) { + SelectableElement(iter, element, on_delete); + iter = mjs_nextElement(spec, iter); + } + ImGui::TreePop(); + } +} + +static void BodyTreeGuiRecursive(mjsElement** element, mjsBody* body, + const SpecElementCallbackFn& on_delete) { + const std::string label = ElementName(body->element); + + ImGui::PushID(body); + + ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed | + ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_AllowOverlap; + + if (*element == body->element) { + flags |= ImGuiTreeNodeFlags_Selected; + } + + const bool tree_open = ImGui::TreeNodeEx(label.c_str(), flags); + if (ImGui::IsItemClicked()) { + *element = body->element; + } + if (*element == body->element) { + AddDeleteButton(body->element, on_delete); + } + + if (tree_open) { + mjsElement* iter = mjs_firstChild(body, mjOBJ_BODY, 0); + while (iter) { + BodyTreeGuiRecursive(element, mjs_asBody(iter), on_delete); + iter = mjs_nextChild(body, iter, 0); + } + + BodyChildrenGui("Frames", mjOBJ_FRAME, element, body, on_delete); + BodyChildrenGui("Sites", mjOBJ_SITE, element, body, on_delete); + BodyChildrenGui("Joints", mjOBJ_JOINT, element, body, on_delete); + BodyChildrenGui("Geoms", mjOBJ_GEOM, element, body, on_delete); + BodyChildrenGui("Lights", mjOBJ_LIGHT, element, body, on_delete); + BodyChildrenGui("Cameras", mjOBJ_CAMERA, element, body, on_delete); + + ImGui::TreePop(); + } + + ImGui::PopID(); +} + +void SpecExplorerGui(mjsElement** element, mjSpec* spec, + const SpecElementCallbackFn& on_delete) { + const ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; + + if (ImGui::TreeNodeEx("Body Tree", flags)) { + mjsElement* root = mjs_firstElement(spec, mjOBJ_BODY); + if (root) { + mjsBody* body = mjs_asBody(root); + if (body) { + BodyTreeGuiRecursive(element, body, on_delete); + } + } + ImGui::TreePop(); + } + + auto list = [&](const char* heading, mjtObj type) { + ElementListGui(heading, type, element, spec, on_delete); + }; + + ImGui::PushID(spec); + + // Non-tree elements. + if (ImGui::TreeNodeEx("Elements", flags)) { + list("Actuators", mjOBJ_ACTUATOR); + list("Sensors", mjOBJ_SENSOR); + list("Flexes", mjOBJ_FLEX); + list("Tendons", mjOBJ_TENDON); + list("Pair", mjOBJ_PAIR); + list("Exclude", mjOBJ_EXCLUDE); + list("Equality", mjOBJ_EQUALITY); + list("Numeric", mjOBJ_NUMERIC); + list("Text", mjOBJ_TEXT); + list("Tuple", mjOBJ_TUPLE); + list("Key", mjOBJ_KEY); + list("Default", mjOBJ_DEFAULT); + ImGui::TreePop(); + } + + // Assets. + if (ImGui::TreeNodeEx("Assets", flags)) { + list("Meshes", mjOBJ_MESH); + list("Height Fields", mjOBJ_HFIELD); + list("Skins", mjOBJ_SKIN); + list("Textures", mjOBJ_TEXTURE); + list("Materials", mjOBJ_MATERIAL); + ImGui::TreePop(); + } + + ImGui::PopID(); +} + +void ElementSpecGui(const mjSpec* spec, mjsElement* element) { + if (element == nullptr) { + return; + } + + ImGui_DataTable table; + table("Name", ElementName(element).c_str(), 1); + + switch (element->elemtype) { + case mjOBJ_BODY: { + const mjsBody* body = mjs_asBody(element); + table("childclass", body->childclass, 1); // childclass name + table("pos", body->pos, 3); // frame position + QuatOrOrientation(table, body->quat, body->alt, "quat", "alt"); // frame orientation + table("ipos", body->ipos, 3); // inertial frame position + QuatOrOrientation(table, body->iquat, body->ialt, "iquat", "ialt"); // inertial frame orientation + table("mass", body->mass, 1); // mass + table("inertia", body->inertia, 3); // diagonal inertia (in i-frame) + table("fullinertia", body->fullinertia, 6); // non-axis-aligned inertia matrix + table("mocap", body->mocap, 1); // is this a mocap body + table("gravcomp", body->gravcomp, 1); // gravity compensation + table("explicitinertial", body->explicitinertial, 1); // whether to save the body with explicit inertial clause + table("sleep", body->sleep, 1); // sleep policy + table("info", body->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_JOINT: { + mjsJoint* joint = mjs_asJoint(element); + table("pos", joint->pos, 3); // anchor position + table("axis", joint->axis, 3); // joint axis + table("ref", joint->ref, 1); // value at reference configuration: qpos0 + table("align", joint->align, 1); // align free joint with body com (mjtAlignFree) + table("stiffness", joint->stiffness, 1); // stiffness coefficient + table("springref", joint->springref, 1); // spring reference value: qpos_spring + table("springdamper", joint->springdamper, 2); // timeconst, dampratio + table("limited", joint->limited, 1); // does joint have limits (mjtLimited) + table("range", joint->range, 2); // joint limits + table("margin", joint->margin, 1); // margin value for joint limit detection + table("solref_limit", joint->solref_limit, mjNREF); // solver reference: joint limits + table("solimp_limit", joint->solimp_limit, mjNIMP); // solver impedance: joint limits + table("actfrclimited", joint->actfrclimited, 1); // are actuator forces on joint limited (mjtLimited) + table("actfrcrange", joint->actfrcrange, 2); // actuator force limits + table("armature", joint->armature, 1); // armature inertia (mass for slider) + table("damping", joint->damping, 1); // damping coefficient + table("frictionloss", joint->frictionloss, 1); // friction loss + table("solref_friction", joint->solref_friction, mjNREF); // solver reference: dof friction + table("solimp_friction", joint->solimp_friction, mjNIMP); // solver impedance: dof friction + table("group", joint->group, 1); // group + table("actgravcomp", joint->actgravcomp, 1); // is gravcomp force applied via actuators + table("info", joint->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_ACTUATOR: { + mjsActuator* actuator = mjs_asActuator(element); + table("gaintype", actuator->gaintype, 1); // gain type + table("gainprm", actuator->gainprm, mjNGAIN); // gain parameters + table("biastype", actuator->biastype, 1); // bias type + table("biasprm", actuator->biasprm, mjNGAIN); // bias parameters + table("dyntype", actuator->dyntype, 1); // dynamics type + table("dynprm", actuator->dynprm, mjNDYN); // dynamics parameters + table("actdim", actuator->actdim, 1); // number of activation variables + table("actearly", actuator->actearly, 1); // apply next activations to qfrc + table("trntype", actuator->trntype, 1); // transmission type + table("gear", actuator->gear, 6); // length and transmitted force scaling + table("target", actuator->target, 1); // name of transmission target + table("refsite", actuator->refsite, 1); // reference site, for site transmission + table("slidersite", actuator->slidersite, 1); // site defining cylinder, for slider-crank + table("cranklength", actuator->cranklength, 1); // crank length, for slider-crank + table("lengthrange", actuator->lengthrange, 2); // transmission length range + table("inheritrange", actuator->inheritrange, 1); // automatic range setting for position and intvelocity + table("ctrllimited", actuator->ctrllimited, 1); // are control limits defined (mjtLimited) + table("ctrlrange", actuator->ctrlrange, 2); // control range + table("forcelimited", actuator->forcelimited, 1); // are force limits defined (mjtLimited) + table("forcerange", actuator->forcerange, 2); // force range + table("actlimited", actuator->actlimited, 1); // are activation limits defined (mjtLimited) + table("actrange", actuator->actrange, 2); // activation range + table("group", actuator->group, 1); // group + table("nsample", actuator->nsample, 1); // number of samples in history buffer + table("interp", actuator->interp, 1); // interpolation order (0=ZOH, 1=linear, 2=cubic) + table("delay", actuator->delay, 1); // delay time in seconds; 0: no delay + table("info", actuator->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_SENSOR: { + mjsSensor* sensor = mjs_asSensor(element); + table("type", sensor->type, 1); // type of sensor + table("objtype", sensor->objtype, 1); // type of sensorized object + table("objname", sensor->objname, 1); // name of sensorized object + table("reftype", sensor->reftype, 1); // type of referenced object + table("refname", sensor->refname, 1); // name of referenced object + table("intprm", sensor->intprm, mjNSENS); // integer parameters + table("datatype", sensor->datatype, 1); // data type for sensor measurement + table("needstage", sensor->needstage, 1); // compute stage needed to simulate sensor + table("dim", sensor->dim, 1); // number of scalar outputs + table("cutoff", sensor->cutoff, 1); // cutoff for real and positive datatypes + table("noise", sensor->noise, 1); // noise stdev + table("nsample", sensor->nsample, 1); // number of samples in history buffer + table("interp", sensor->interp, 1); // interpolation order (0=ZOH, 1=linear, 2=cubic) + table("delay", sensor->delay, 1); // delay time in seconds + table("interval", sensor->interval, 2); // [period, time_prev] in seconds + table("info", sensor->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_SITE: { + mjsSite* site = mjs_asSite(element); + table("pos", site->pos, 3); // position + QuatOrOrientation(table, site->quat, site->alt, "quat", "alt"); // orientation + table("fromto", site->fromto, 6); // alternative for capsule, cylinder, box, ellipsoid + table("size", site->size, 3); // geom size + table("type", site->type, 1); // geom type + table("material", site->material, 1); // name of material + table("group", site->group, 1); // group + table("rgba", site->rgba, 4); // rgba when material is omitted + table("info", site->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_FRAME: { + mjsFrame* frame = mjs_asFrame(element); + table("childclass", frame->childclass, 1); // childclass name + table("pos", frame->pos, 3); // position + QuatOrOrientation(table, frame->quat, frame->alt, "quat", "alt"); // orientation + table("info", frame->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_GEOM: { + mjsGeom* geom = mjs_asGeom(element); + table("type", geom->type, 1); // geom type + table("pos", geom->pos, 3); // position + QuatOrOrientation(table, geom->quat, geom->alt, "quat", "alt"); // orientation + table("fromto", geom->fromto, 6); // alternative for capsule, cylinder, box, ellipsoid + table("size", geom->size, 3); // type-specific size + table("contype", geom->contype, 1); // contact type + table("conaffinity", geom->conaffinity, 1); // contact affinity + table("condim", geom->condim, 1); // contact dimensionality + table("priority", geom->priority, 1); // contact priority + table("friction", geom->friction, 3); // one-sided friction coefficients: slide, roll, spin + table("solmix", geom->solmix, 1); // solver mixing for contact pairs + table("solref", geom->solref, mjNREF); // solver reference + table("solimp", geom->solimp, mjNIMP); // solver impedance + table("margin", geom->margin, 1); // margin for contact detection + table("gap", geom->gap, 1); // include in solver if dist < margin-gap + table("mass", geom->mass, 1); // used to compute density + table("density", geom->density, 1); // used to compute mass and inertia from volume or surface + table("typeinertia", geom->typeinertia, 1); // selects between surface and volume inertia + table("fluid_ellipsoid", geom->fluid_ellipsoid, 1); // whether ellipsoid-fluid model is active + table("fluid_coefs", geom->fluid_coefs, 5); // ellipsoid-fluid interaction coefs + table("material", geom->material, 1); // name of material + table("rgba", geom->rgba, 4); // rgba when material is omitted + table("group", geom->group, 1); // group + table("hfieldname", geom->hfieldname, 1); // heightfield attached to geom + table("meshname", geom->meshname, 1); // mesh attached to geom + table("fitscale", geom->fitscale, 1); // scale mesh uniformly + table("info", geom->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_LIGHT: { + mjsLight* light = mjs_asLight(element); + table("pos", light->pos, 3); // position + table("dir", light->dir, 3); // direction + table("mode", light->mode, 1); // tracking mode + table("targetbody", light->targetbody, 1); // target body for targeting + table("active", light->active, 1); // is light active + table("type", light->type, 1); // type of light + table("texture", light->texture, 1); // texture name for image lights + table("castshadow", light->castshadow, 1); // does light cast shadows + table("bulbradius", light->bulbradius, 1); // bulb radius, for soft shadows + table("intensity", light->intensity, 1); // intensity, in candelas + table("range", light->range, 1); // range of effectiveness + table("attenuation", light->attenuation, 3); // OpenGL attenuation (quadratic model) + table("cutoff", light->cutoff, 1); // OpenGL cutoff + table("exponent", light->exponent, 1); // OpenGL exponent + table("ambient", light->ambient, 3); // ambient color + table("diffuse", light->diffuse, 3); // diffuse color + table("specular", light->specular, 3); // specular color + table("info", light->info, 1); // message appended to compiler errorsx + break; + } + case mjOBJ_CAMERA: { + mjsCamera* camera = mjs_asCamera(element); + table("pos", camera->pos, 3); // position + QuatOrOrientation(table, camera->quat, camera->alt, "quat", "alt"); // orientation + table("mode", camera->mode, 1); // tracking mode + table("targetbody", camera->targetbody, 1); // target body for tracking/targeting + table("proj", camera->proj, 1); // camera projection type + table("resolution", camera->resolution, 2); // resolution (pixel) + table("output", camera->output, 1); // bit flags for output type + table("fovy", camera->fovy, 1); // y-field of view + table("ipd", camera->ipd, 1); // inter-pupillary distance + table("intrinsic", camera->intrinsic, 4); // camera intrinsics (length) + table("sensor_size", camera->sensor_size, 2); // sensor size (length) + table("focal_length", camera->focal_length, 2); // focal length (length) + table("focal_pixel", camera->focal_pixel, 2); // focal length (pixel) + table("principal_length", camera->principal_length, 2); // principal point (length) + table("principal_pixel", camera->principal_pixel, 2); // principal point (pixel) + table("info", camera->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_MESH: { + mjsMesh* mesh = mjs_asMesh(element); + table("content_type", mesh->content_type, 1); // content type of file + table("file", mesh->file, 1); // mesh file + table("refpos", mesh->refpos, 3); // reference position + table("refquat", mesh->refquat, 4); // reference orientation + table("scale", mesh->scale, 3); // rescale mesh + table("inertia", mesh->inertia, 1); // inertia type (convex, legacy, exact, shell) + table("smoothnormal", mesh->smoothnormal, 1); // do not exclude large-angle faces from normals + table("needsdf", mesh->needsdf, 1); // compute sdf from mesh + table("maxhullvert", mesh->maxhullvert, 1); // maximum vertex count for the convex hull + table("material", mesh->material, 1); // name of material + table("info", mesh->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_HFIELD: { + mjsHField* hfield = mjs_asHField(element); + table("content_type", hfield->content_type, 1); // content type of file + table("file", hfield->file, 1); // file: (nrow, ncol, [elevation data]) + table("size", hfield->size, 4); // hfield size (ignore referencing geom size) + table("nrow", hfield->nrow, 1); // number of rows + table("ncol", hfield->ncol, 1); // number of columns + table("info", hfield->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_SKIN: { + mjsSkin* skin = mjs_asSkin(element); + table("file", skin->file, 1); // skin file + table("material", skin->material, 1); // name of material used for rendering + table("rgba", skin->rgba, 4); // rgba when material is omitted + table("inflate", skin->inflate, 1); // inflate in normal direction + table("group", skin->group, 1); // group for visualization + table("info", skin->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_FLEX: { + mjsFlex* flex = mjs_asFlex(element); + table("contype", flex->contype, 1); // contact type + table("conaffinity", flex->conaffinity, 1); // contact affinity + table("condim", flex->condim, 1); // contact dimensionality + table("priority", flex->priority, 1); // contact priority + table("friction", flex->friction, 3); // one-sided friction coefficients: slide, roll, spin + table("solmix", flex->solmix, 1); // solver mixing for contact pairs + table("solref", flex->solref, mjNREF); // solver reference + table("solimp", flex->solimp, mjNIMP); // solver impedance + table("margin", flex->margin, 1); // margin for contact detection + table("gap", flex->gap, 1); // include in solver if distdim, 1); // element dimensionality + table("radius", flex->radius, 1); // radius around primitive element + table("size", flex->size, 3); // vertex bounding box half sizes in qpos0 + table("internal", flex->internal, 1); // enable internal collisions + table("flatskin", flex->flatskin, 1); // render flex skin with flat shading + table("selfcollide", flex->selfcollide, 1); // mode for flex self collision + table("vertcollide", flex->vertcollide, 1); // mode for vertex collision + table("passive", flex->passive, 1); // mode for passive collisions + table("activelayers", flex->activelayers, 1); // number of active element layers in 3D + table("group", flex->group, 1); // group for visualization + table("edgestiffness", flex->edgestiffness, 1); // edge stiffness + table("edgedamping", flex->edgedamping, 1); // edge damping + table("rgba", flex->rgba, 4); // rgba when material is omitted + table("material", flex->material, 1); // name of material used for rendering + table("young", flex->young, 1); // Young's modulus + table("poisson", flex->poisson, 1); // Poisson's ratio + table("damping", flex->damping, 1); // Rayleigh's damping + table("thickness", flex->thickness, 1); // thickness (2D only) + table("elastic2d", flex->elastic2d, 1); // 2D passive forces; 0: none, 1: bending, 2: stretching, 3: both + table("info", flex->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_TENDON: { + mjsTendon* tendon = mjs_asTendon(element); + table("stiffness", tendon->stiffness, 1); // stiffness coefficient + table("springlength", tendon->springlength, 2); // spring resting length; {-1, -1}: use qpos_spring + table("damping", tendon->damping, 1); // damping coefficient + table("frictionloss", tendon->frictionloss, 1); // friction loss + table("solref_friction", tendon->solref_friction, mjNREF); // solver reference: tendon friction + table("solimp_friction", tendon->solimp_friction, mjNIMP); // solver impedance: tendon friction + table("armature", tendon->armature, 1); // inertia associated with tendon velocity + table("limited", tendon->limited, 1); // does tendon have limits (mjtLimited) + table("actfrclimited", tendon->actfrclimited, 1); // does tendon have actuator force limits + table("range", tendon->range, 2); // length limits + table("actfrcrange", tendon->actfrcrange, 2); // actuator force limits + table("margin", tendon->margin, 1); // margin value for tendon limit detection + table("solref_limit", tendon->solref_limit, mjNREF); // solver reference: tendon limits + table("solimp_limit", tendon->solimp_limit, mjNIMP); // solver impedance: tendon limits + table("material", tendon->material, 1); // name of material for rendering + table("width", tendon->width, 1); // width for rendering + table("rgba", tendon->rgba, 4); // rgba when material is omitted + table("group", tendon->group, 1); // group + table("info", tendon->info, 1); // message appended to errors + break; + } + case mjOBJ_TEXTURE: { + mjsTexture* texture = mjs_asTexture(element); + table("type", texture->type, 1); // texture type + table("colorspace", texture->colorspace, 1); // colorspace + table("builtin", texture->builtin, 1); // builtin type (mjtBuiltin) + table("mark", texture->mark, 1); // mark type (mjtMark) + table("rgb1", texture->rgb1, 3); // first color for builtin + table("rgb2", texture->rgb2, 3); // second color for builtin + table("markrgb", texture->markrgb, 3); // mark color + table("random", texture->random, 1); // probability of random dots + table("height", texture->height, 1); // height in pixels (square for cube and skybox) + table("width", texture->width, 1); // width in pixels + table("nchannel", texture->nchannel, 1); // number of channels + table("content_type", texture->content_type, 1); // content type of file + table("file", texture->file, 1); // png file to load; use for all sides of cube + table("gridsize", texture->gridsize, 2); // size of grid for composite file; (1,1)-repeat + // TODO: table("gridlayout", texture->gridlayout, 12); // row-major: L,R,F,B,U,D for faces; . for unused + table("cubefiles", texture->cubefiles, 1); // different file for each side of the cube + table("hflip", texture->hflip, 1); // horizontal flip + table("vflip", texture->vflip, 1); // vertical flip + table("info", texture->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_MATERIAL: { + mjsMaterial* material = mjs_asMaterial(element); + table("textures", material->textures, 1); // names of textures (empty: none) + table("texuniform", material->texuniform, 1); // make texture cube uniform + table("texrepeat", material->texrepeat, 2); // texture repetition for 2D mapping + table("emission", material->emission, 1); // emission + table("specular", material->specular, 1); // specular + table("shininess", material->shininess, 1); // shininess + table("reflectance", material->reflectance, 1); // reflectance + table("metallic", material->metallic, 1); // metallic + table("roughness", material->roughness, 1); // roughness + table("rgba", material->rgba, 4); // rgba + table("info", material->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_PAIR: { + mjsPair* pair = mjs_asPair(element); + table("geomname1", pair->geomname1, 1); // name of geom 1 + table("geomname2", pair->geomname2, 1); // name of geom 2 + table("condim", pair->condim, 1); // contact dimensionality + table("solref", pair->solref, mjNREF); // solver reference, normal direction + table("solreffriction", pair->solreffriction, mjNREF); // solver reference, frictional directions + table("solimp", pair->solimp, mjNIMP); // solver impedance + table("margin", pair->margin, 1); // margin for contact detection + table("gap", pair->gap, 1); // include in solver if distfriction, 5); // full contact friction + table("info", pair->info, 1); // message appended to errors + break; + } + case mjOBJ_EQUALITY: { + mjsEquality* equality = mjs_asEquality(element); + table("type", equality->type, 1); // constraint type + table("data", equality->data, mjNEQDATA); // type-dependent data + table("active", equality->active, 1); // is equality initially active + table("name1", equality->name1, 1); // name of object 1 + table("name2", equality->name2, 1); // name of object 2 + table("objtype", equality->objtype, 1); // type of both objects + table("solref", equality->solref, mjNREF); // solver reference + table("solimp", equality->solimp, mjNIMP); // solver impedance + table("info", equality->info, 1); // message appended to errors + break; + } + case mjOBJ_EXCLUDE: { + mjsExclude* exclude = mjs_asExclude(element); + table("bodyname1", exclude->bodyname1, 1); // name of geom 1 + table("bodyname2", exclude->bodyname2, 1); // name of geom 2 + table("info", exclude->info, 1); // message appended to errors + break; + } + case mjOBJ_NUMERIC: { + mjsNumeric* numeric = mjs_asNumeric(element); + table("data", numeric->data, 1); // initialization data + table("size", numeric->size, 1); // array size, can be bigger than data size + table("info", numeric->info, 1); // message appended to errors + break; + } + case mjOBJ_TEXT: { + mjsText* text = mjs_asText(element); + table("data", text->data, 1); // text string + table("info", text->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_TUPLE: { + mjsTuple* tuple = mjs_asTuple(element); + table("objtype", tuple->objtype, 1); // object types + table("objname", tuple->objname, 1); // object names + table("objprm", tuple->objprm, 1); // object parameters + table("info", tuple->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_KEY: { + mjsKey* key = mjs_asKey(element); + table("time", key->time, 1); // time + table("qpos", key->qpos, 1); // qpos + table("qvel", key->qvel, 1); // qvel + table("act", key->act, 1); // act + table("mpos", key->mpos, 1); // mocap pos + table("mquat", key->mquat, 1); // mocap quat + table("ctrl", key->ctrl, 1); // ctrl + table("info", key->info, 1); // message appended to compiler errors + break; + } + case mjOBJ_PLUGIN: { + mjsPlugin* plugin = mjs_asPlugin(element); + table("name", plugin->name, 1); // instance name + table("plugin_name", plugin->plugin_name, 1); // plugin name + table("active", plugin->active, 1); // is the plugin active + table("info", plugin->info, 1); // message appended to compiler errors + break; + } + default: + // ignore other types + break; + } +} + +void ElementModelGui(const mjModel* model, mjsElement* element) { + if (element == nullptr) { + return; + } + + ImGui_DataTable table; + table("Name", ElementName(element).c_str(), 1); + table.SetArrayIndex(mjs_getId(element)); + + MJMODEL_POINTERS_PREAMBLE(model); + switch (element->elemtype) { + case mjOBJ_BODY: { + table.SetPrefix("body_"); + MJMODEL_POINTERS_X(nbody) + break; + } + case mjOBJ_JOINT: { + table.SetPrefix("jnt_"); + MJMODEL_POINTERS_X(njnt) + break; + } + case mjOBJ_ACTUATOR: { + table.SetPrefix("actuator_"); + MJMODEL_POINTERS_X(nu) + break; + } + case mjOBJ_SENSOR: { + table.SetPrefix("sensor_"); + MJMODEL_POINTERS_X(nsensor) + break; + } + case mjOBJ_SITE: { + table.SetPrefix("site_"); + MJMODEL_POINTERS_X(nsite) + break; + } + case mjOBJ_GEOM: { + table.SetPrefix("geom_"); + MJMODEL_POINTERS_X(ngeom) + break; + } + case mjOBJ_LIGHT: { + table.SetPrefix("light_"); + MJMODEL_POINTERS_X(nlight) + break; + } + case mjOBJ_CAMERA: { + table.SetPrefix("cam_"); + MJMODEL_POINTERS_X(ncam) + break; + } + case mjOBJ_MESH: { + table.SetPrefix("mesh_"); + MJMODEL_POINTERS_X(nmesh) + break; + } + case mjOBJ_HFIELD: { + table.SetPrefix("hfield_"); + MJMODEL_POINTERS_X(nhfield) + break; + } + case mjOBJ_SKIN: { + table.SetPrefix("skin_"); + MJMODEL_POINTERS_X(nskin) + break; + } + case mjOBJ_FLEX: { + table.SetPrefix("flex_"); + MJMODEL_POINTERS_X(nflex) + break; + } + case mjOBJ_TENDON: { + table.SetPrefix("tendon_"); + MJMODEL_POINTERS_X(ntendon) + break; + } + case mjOBJ_TEXTURE: { + table.SetPrefix("tex_"); + MJMODEL_POINTERS_X(ntex) + break; + } + case mjOBJ_MATERIAL: { + table.SetPrefix("mat_"); + MJMODEL_POINTERS_X(nmat) + break; + } + default: + // ignore other types + break; + } +} + +void ElementDataGui(const mjData* data, mjsElement* element) { + if (element == nullptr) { + return; + } + + ImGui_DataTable table; + table("Name", ElementName(element).c_str(), 1); + table.SetArrayIndex(mjs_getId(element)); + + switch (element->elemtype) { + case mjOBJ_BODY: { + MJDATA_POINTERS_X(nbody) + break; + } + case mjOBJ_JOINT: { + MJDATA_POINTERS_X(njnt) + break; + } + case mjOBJ_SITE: { + table.SetPrefix("site_"); + MJDATA_POINTERS_X(nsite) + break; + } + case mjOBJ_GEOM: { + table.SetPrefix("geom_"); + MJDATA_POINTERS_X(ngeom) + break; + } + case mjOBJ_CAMERA: { + table.SetPrefix("cam_"); + MJDATA_POINTERS_X(ncam) + break; + } + case mjOBJ_LIGHT: { + table.SetPrefix("light_"); + MJDATA_POINTERS_X(nlight) + break; + } + case mjOBJ_SENSOR: { + table.SetPrefix("sensor_"); + MJDATA_POINTERS_X(nsensor) + break; + } + case mjOBJ_MESH: { + table.SetPrefix("mesh_"); + MJDATA_POINTERS_X(nmesh) + break; + } + case mjOBJ_HFIELD: { + table.SetPrefix("hfield_"); + MJDATA_POINTERS_X(nhfield) + break; + } + case mjOBJ_SKIN: { + table.SetPrefix("skin_"); + MJDATA_POINTERS_X(nskin) + break; + } + case mjOBJ_FLEX: { + table.SetPrefix("flex_"); + MJDATA_POINTERS_X(nflex) + break; + } + case mjOBJ_TENDON: { + table.SetPrefix("ten_"); + MJDATA_POINTERS_X(ntendon) + break; + } + case mjOBJ_TEXTURE: { + table.SetPrefix("tex_"); + MJDATA_POINTERS_X(ntex) + break; + } + case mjOBJ_MATERIAL: { + table.SetPrefix("mat_"); + MJDATA_POINTERS_X(nmat) + break; + } + default: + break; + } +} +} // namespace mujoco::platform diff --git a/src/experimental/platform/gui_spec.h b/src/experimental/platform/gui_spec.h new file mode 100644 index 00000000..e1094583 --- /dev/null +++ b/src/experimental/platform/gui_spec.h @@ -0,0 +1,37 @@ +// Copyright 2026 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. + +#ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_SPEC_H_ +#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_SPEC_H_ + +#include + +#include + +namespace mujoco::platform { + +using SpecElementCallbackFn = std::function; + +// UX for displaying the spec as a tree. +void SpecExplorerGui(mjsElement** element, mjSpec* spec, + const SpecElementCallbackFn& on_delete); + +// UX for displaying the properties of an mjSpec element. +void ElementSpecGui(const mjSpec* spec, mjsElement* element); +void ElementDataGui(const mjData* data, mjsElement* element); +void ElementModelGui(const mjModel* model, mjsElement* element); + +} // namespace mujoco::platform + +#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_SPEC_H_ diff --git a/src/experimental/platform/imgui_widgets.cc b/src/experimental/platform/imgui_widgets.cc index 10a7b374..def513e9 100644 --- a/src/experimental/platform/imgui_widgets.cc +++ b/src/experimental/platform/imgui_widgets.cc @@ -14,9 +14,13 @@ #include "experimental/platform/imgui_widgets.h" +#include +#include #include #include +#include #include +#include #include #include @@ -57,6 +61,228 @@ KeyValues ReadIniSection(const std::string& contents, return key_values; } +ImGui_DataTable::ImGui_DataTable(float w1, float w2) { + ImGui::BeginTable("##PropertiesTable", 2); + const float width = ImGui::GetContentRegionAvail().x; + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width * w1); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width * w2); +} +ImGui_DataTable::~ImGui_DataTable() { ImGui::EndTable(); } + +void ImGui_DataTable::SetArrayIndex(int index) { index_ = index; } + +void ImGui_DataTable::SetPrefix(const char* prefix) { + prefix_ = strlen(prefix); +} + +void ImGui_DataTable::operator()(const char* label, const uintptr_t* ptr, + int n) { + for (int i = 0; i < n; ++i) { + MakeLabel(label, i, n); + ImGui::Text("(%s)", &ptr[index_ + i] ? "[ptr]" : "null"); + } +} + +void ImGui_DataTable::operator()(const char* label, const char* ptr, int n) { + if (n == 1) { + MakeLabel(label); + ImGui::Text("%s", &ptr[index_]); + } else { + mju_error("char cannot be converted to a vector"); + } +} + +void ImGui_DataTable::operator()(const char* label, const mjtByte* ptr, int n) { + for (int i = 0; i < n; ++i) { + MakeLabel(label, i, n); + ImGui::Text("%s", ptr[index_ + i] ? "true" : "false"); + } +} + +void ImGui_DataTable::operator()(const char* label, const mjtByte& val, int n) { + MakeLabel(label, 0, 1); + ImGui::Text("%s", val ? "true" : "false"); +} + +void ImGui_DataTable::operator()(const char* label, const mjtSize* ptr, int n) { + Numeric(label, ptr, n); +} + +void ImGui_DataTable::operator()(const char* label, const int* ptr, int n) { + Numeric(label, ptr, n); +} + +void ImGui_DataTable::operator()(const char* label, const float* ptr, int n) { + Numeric(label, ptr, n); +} + +void ImGui_DataTable::operator()(const char* label, const double* ptr, int n) { + Numeric(label, ptr, n); +} + +void ImGui_DataTable::operator()(const char* label, const mjtSize& val, int n) { + Scalar(label, val, n); +} + +void ImGui_DataTable::operator()(const char* label, const int& val, int n) { + Scalar(label, val, n); +} + +void ImGui_DataTable::operator()(const char* label, const float& val, int n) { + Scalar(label, val, n); +} + +void ImGui_DataTable::operator()(const char* label, const double& val, int n) { + Scalar(label, val, n); +} + +void ImGui_DataTable::operator()(const char* label, const std::string* ptr, + int n) { + for (int i = 0; i < n; ++i) { + MakeLabel(label, i, n); + ImGui::Text("%s", ptr[i].c_str()); + } +} + +void ImGui_DataTable::operator()(const char* label, + const std::vector* ptr, int n) { + if (n == 1) { + for (int i = 0; i < ptr->size(); ++i) { + MakeLabel(label, i, ptr->size()); + ImGui::Text("%s", ptr->at(i).c_str()); + } + } else { + mju_error("data type is vector; cannot also be an array"); + } +} + +void ImGui_DataTable::operator()(const char* label, const std::vector* ptr, + int n) { + if (n == 1) { + const int size = ptr->size(); + if (size == 0) { + (*this)(label, "[empty]", 1); + } else { + std::string tmp = "[" + std::to_string(size) + " values]"; + (*this)(label, tmp.c_str(), 1); + } + } else { + mju_error("data type is vector; cannot also be an array"); + } +} + +void ImGui_DataTable::operator()(const char* label, + const std::vector* ptr, int n) { + if (n == 1) { + const int size = ptr->size(); + if (size == 0) { + (*this)(label, "[empty]", 1); + } else { + std::string tmp = "[" + std::to_string(size) + " values]"; + (*this)(label, tmp.c_str(), 1); + } + } else { + mju_error("data type is vector; cannot also be an array"); + } +} + +template +void ImGui_DataTable::Scalar(const char* label, const T& value, int n) { + if (n == 1) { + Numeric(label, &value, n); + } else { + mju_error("scalar cannot be converted to a vector"); + } +} + +template +void ImGui_DataTable::Numeric(const char* label, const T* ptr, int n) { + const T* addr = ptr + index_ * n; + + using U = std::conditional_t, float, int>; + + // special treatment for NaNs. + if constexpr (std::is_same_v) { + if (*addr != *addr) { + MakeLabel(label); + ImGui::Text("nan"); + return; + } + } + + constexpr const char* fmt1 = + std::is_floating_point_v ? "%f" : "%d"; + constexpr const char* fmt2 = + std::is_floating_point_v ? "%f %f" : "%d %d"; + constexpr const char* fmt3 = + std::is_floating_point_v ? "%f %f %f" : "%d %d %d"; + constexpr const char* fmt4 = + std::is_floating_point_v ? "%f %f %f %f" : "%d %d %d %d"; + + auto text1 = [&](int offset) { + ImGui::Text(fmt1, (U)(addr[offset])); + }; + auto text2 = [&](int offset) { + ImGui::Text(fmt2, (U)(addr[offset + 0]), (U)(addr[offset + 1])); + }; + auto text3 = [&](int offset) { + ImGui::Text(fmt3, (U)(addr[offset + 0]), (U)(addr[offset + 1]), + (U)(addr[offset + 2])); + }; + auto text4 = [&](int offset) { + ImGui::Text(fmt4, (U)(addr[offset + 0]), (U)(addr[offset + 1]), + (U)(addr[offset + 2]), (U)(addr[offset + 3])); + }; + + if (n == 1) { + MakeLabel(label); + text1(0); + } else if (n == 2) { + MakeLabel(label); + text2(0); + } else if (n == 3) { + MakeLabel(label); + text3(0); + } else if (n == 4) { + MakeLabel(label); + text4(0); + } else if (n == 6) { + MakeLabel(label); + text3(0); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + text3(3); + } else if (n == 9) { + MakeLabel(label); + text3(0); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + text3(3); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + text3(6); + } else { + for (int i = 0; i < n; ++i) { + MakeLabel(label, i, n); + text1(i); + } + } +} + +void ImGui_DataTable::MakeLabel(const char* label, int index, int total) { + if (total == 1) { + ImGui::TableNextColumn(); + ImGui::Text("%s", &label[prefix_]); + ImGui::TableNextColumn(); + } else { + const std::string tmp = + std::string(&label[prefix_]) + "[" + std::to_string(index) + "]"; + ImGui::TableNextColumn(); + ImGui::Text("%s", tmp.c_str()); + ImGui::TableNextColumn(); + } +} + bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max) { float f = *value; const bool res = ImGui::SliderFloat(name, &f, min, max); diff --git a/src/experimental/platform/imgui_widgets.h b/src/experimental/platform/imgui_widgets.h index 29f5005f..7fd64ca8 100644 --- a/src/experimental/platform/imgui_widgets.h +++ b/src/experimental/platform/imgui_widgets.h @@ -15,11 +15,13 @@ #ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_IMGUI_WIDGETS_H_ #define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_IMGUI_WIDGETS_H_ +#include #include #include #include #include #include +#include #include #include @@ -112,6 +114,12 @@ struct ScopedStyle { return *this; } + ScopedStyle& Color(ImGuiCol col, ImGuiCol col2) { + ImGui::PushStyleColor(col, CurrentColor(col2)); + ++num_colors; + return *this; + } + ScopedStyle& Var(ImGuiStyleVar var, float value) { ImGui::PushStyleVar(var, value); ++num_vars; @@ -124,6 +132,10 @@ struct ScopedStyle { return *this; } + ImVec4 CurrentColor(ImGuiCol col) { + return ImGui::GetStyle().Colors[col]; + } + void Reset() { ImGui::PopStyleVar(num_vars); ImGui::PopStyleColor(num_colors); @@ -135,6 +147,74 @@ struct ScopedStyle { int num_vars = 0; }; +// Helper for displaying rows of key/value pairs in an ImGui table. +// +// Designed specifically to be used to display mjSpec, mjModel, and mjData +// values. +// +// To add a value to the table, call the operator() function with the label, +// the value, and (optionally) the dimensionality of the value (e.g. for vectors +// and matrices). To support generic code, even scalar values should be passed +// to operator() with n = 1. +class ImGui_DataTable { + public: + // Starts the table (i.e. ImGui::BeginTable()) with two columns of the + // specified widths. + ImGui_DataTable(float w1 = 0.25f, float w2 = 0.75f); + + // Ends the table (e.g. ImGui::EndTable(). + ~ImGui_DataTable(); + + ImGui_DataTable(const ImGui_DataTable& other) = delete; + ImGui_DataTable& operator=(const ImGui_DataTable& other) = delete; + + // Sets the offset into an array of values (e.g. for pointers in mjModel and + // mjData). This is only used for the display functions that take a pointer. + void SetArrayIndex(int index); + + // Sets the prefix that will be removed from all labels. Note: that we simply + // remove the first N characters of the label without actually comparing + // against this prefix. + void SetPrefix(const char* prefix); + + // Displays a labelled value in the table. + void operator()(const char* label, const uintptr_t* ptr, int n); + void operator()(const char* label, const char* ptr, int n); + void operator()(const char* label, const mjtByte* ptr, int n); + void operator()(const char* label, const mjtSize* ptr, int n); + void operator()(const char* label, const int* ptr, int n); + void operator()(const char* label, const float* ptr, int n); + void operator()(const char* label, const double* ptr, int n); + + // Displays a single scalar value in the table. Assumes n == 1. This should + // only be used for mjSpec objects and, therefore, will ignore the array index + // if set. + void operator()(const char* label, const mjtByte& val, int n); + void operator()(const char* label, const mjtSize& val, int n); + void operator()(const char* label, const int& val, int n); + void operator()(const char* label, const float& val, int n); + void operator()(const char* label, const double& val, int n); + + // Overloads for C++ container types. Assumes its only used for mjSpec objects + // and, therefore, will ignore the array index if set. + void operator()(const char* label, const std::string* ptr, int n); + void operator()(const char* label, const std::vector* ptr, int n); + void operator()(const char* label, const std::vector* ptr, int n); + void operator()(const char* label, const std::vector* ptr, int n); + + private: + template + void Numeric(const char* label, const T* ptr, int n); + + template + void Scalar(const char* label, const T& value, int n); + + void MakeLabel(const char* label, int index = 0, int total = 1); + + int prefix_ = 0; + int index_ = 0; +}; + // ImGui Slider that supports both float and double types. bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max); diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 352e8c8a..bade8680 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -36,6 +36,7 @@ #include #include "experimental/platform/file_dialog.h" #include "experimental/platform/gui.h" +#include "experimental/platform/gui_spec.h" #include "experimental/platform/helpers.h" #include "experimental/platform/imgui_widgets.h" #include "experimental/platform/interaction.h" @@ -132,6 +133,7 @@ void App::ClearModel() { tmp_ = UiTempState(); load_error_ = ""; step_error_ = ""; + edit_error_ = ""; } void App::Recompile() { @@ -257,6 +259,7 @@ void App::ResetPhysics() { mj_resetData(model(), data()); mj_forward(model(), data()); step_error_ = ""; + edit_error_ = ""; } void App::UpdatePhysics() { @@ -419,15 +422,42 @@ 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; +void App::SpecSelectElement(mjsElement* element) { + tmp_.element = element; + if (tmp_.element == nullptr) { tmp_.element_id = -1; + } else { + tmp_.element_id = mjs_getId(tmp_.element); + + // If we selected a body, then select the same body for perturb. + if (tmp_.element->elemtype == mjOBJ_BODY && + perturb_.select != tmp_.element_id) { + mjv_defaultPerturb(&perturb_); + perturb_.select = tmp_.element_id; + } + } +} + +void App::SpecDeleteElement(mjsElement* element) { + if (element == nullptr) { + return; + } + // Only bodies can be deleted for now... + if (element->elemtype != mjOBJ_BODY) { + edit_error_ = "WARNING: Only bodies can be deleted (for now...)"; + return; + } + spec_op_ = [this, element]() { + mjs_delete(spec(), element); + if (tmp_.element == element) { + tmp_.element = nullptr; + tmp_.element_id = -1; + } + if (element->elemtype == mjOBJ_BODY) { + if (perturb_.select == mjs_getId(element)) { + mjv_defaultPerturb(&perturb_); + } + } Recompile(); }; } @@ -635,7 +665,7 @@ void App::HandleKeyboardEvents() { } else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) { ResetPhysics(); } else if (ImGui_IsChordJustPressed(ImGuiKey_Delete)) { - SpecDeleteSelectedElement(); + SpecDeleteElement(tmp_.element); } else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) { SelectParentPerturb(model(), perturb_); } else if (ImGui_IsChordJustPressed(ImGuiKey_F1)) { @@ -896,7 +926,7 @@ void App::BuildGui() { if (explorer_is_open && tmp_.element != nullptr) { if (ImGui::Begin("Properties")) { - PropertiesGui(); + SpecPropertiesGui(); } ImGui::End(); } @@ -1129,117 +1159,62 @@ void App::DataInspectorGui() { ImGui::EndChild(); } -void DisplayElementTree(mjsElement* element) { - const mjString* name = mjs_getName(element); - if (name->empty()) { - ImGui::Text("(unnamed)"); - } else { - ImGui::Text("%s", name->c_str()); - } -} - void App::SpecExplorerGui() { if (!has_spec()) { ImGui::Text("No mjSpec loaded."); return; } - const ImGuiTreeNodeFlags flags = - ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; + auto on_delete = [this](mjsElement* element) { SpecDeleteElement(element); }; - auto display_group = [this](mjtObj type, const std::string& prefix, - std::function delete_callback = {}) { - mjsElement* element = mjs_firstElement(spec(), type); - while (element) { - const int id = mjs_getId(element); - - const mjString* name = mjs_getName(element); - std::string label = *name; - if (label.empty()) { - label = "(" + prefix + " " + std::to_string(id) + ")"; - } - - const bool selected = (tmp_.element == element); - if (ImGui::Selectable(label.c_str(), selected, - ImGuiSelectableFlags_AllowOverlap)) { - tmp_.element = element; - tmp_.element_id = id; - } - - if (selected && delete_callback) { - // Right-align the delete button. - const float button_width = ImGui::CalcTextSize(ICON_DELETE).x + - ImGui::GetStyle().FramePadding.x * 2.0f; - ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - button_width); - if (ImGui::SmallButton(ICON_DELETE)) { - delete_callback(); - } - } - - element = mjs_nextElement(spec(), element); - } - }; - - if (ImGui::TreeNodeEx("Bodies", flags)) { - display_group(mjOBJ_BODY, "Body", [this] { SpecDeleteSelectedElement(); }); - 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; + mjsElement* element = tmp_.element; + platform::SpecExplorerGui(&element, spec(), on_delete); + if (element != tmp_.element) { + SpecSelectElement(element); } } -void App::PropertiesGui() { - if (tmp_.element == nullptr) { - ImGui::Text("No element selected."); - return; - } +void App::SpecPropertiesGui() { + platform::ScopedStyle style; - 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::Text("%s", mju_type2Str(tmp_.element->elemtype)); + ImGui::SameLine(); + ImGui::Text("(%d)", tmp_.element_id); + + ImGui::SameLine(120); + if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) { + style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive); } + if (ImGui::SmallButton("S")) { + tmp_.spec_prop_mode = SpecPropertiesMode::kSpec; + } + ImGui::SetItemTooltip("Spec"); + style.Reset(); + ImGui::SameLine(); + if (tmp_.spec_prop_mode == SpecPropertiesMode::kModel) { + style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive); + } + if (ImGui::SmallButton("M")) { + tmp_.spec_prop_mode = SpecPropertiesMode::kModel; + } + ImGui::SetItemTooltip("Model"); + style.Reset(); + ImGui::SameLine(); + if (tmp_.spec_prop_mode == SpecPropertiesMode::kData) { + style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive); + } + if (ImGui::SmallButton("D")) { + tmp_.spec_prop_mode = SpecPropertiesMode::kData; + } + ImGui::SetItemTooltip("Data"); + style.Reset(); ImGui::Separator(); - - switch (tmp_.element->elemtype) { - case mjOBJ_BODY: - platform::BodyPropertiesGui(model(), data(), tmp_.element, - tmp_.element_id); - break; - case mjOBJ_JOINT: - platform::JointPropertiesGui(model(), data(), tmp_.element, - tmp_.element_id); - break; - case mjOBJ_SITE: - platform::SitePropertiesGui(model(), data(), tmp_.element, - tmp_.element_id); - break; - default: - // ignore other types - break; + if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) { + platform::ElementSpecGui(spec(), tmp_.element); + } else if (tmp_.spec_prop_mode == SpecPropertiesMode::kModel) { + platform::ElementModelGui(model(), tmp_.element); + } else { + platform::ElementDataGui(data(), tmp_.element); } } @@ -1617,6 +1592,9 @@ void App::StatusBarGui() { } else if (!load_error_.empty()) { ImGui::SameLine(); ImGui::Text(" | Load Error: %s", load_error_.c_str()); + } else if (!edit_error_.empty()) { + ImGui::SameLine(); + ImGui::Text(" | Edit Error: %s", edit_error_.c_str()); } ImGui::TableNextColumn(); diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index d2cce3a5..66332e70 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -91,6 +91,12 @@ class App { kModelFromBuffer, }; + enum class SpecPropertiesMode { + kSpec, + kModel, + kData, + }; + // UI state that is persisted across application runs struct UiState { char watch_field[1000] = "qpos"; @@ -136,6 +142,7 @@ class App { std::vector speed_names; // Spec Properties. + SpecPropertiesMode spec_prop_mode = SpecPropertiesMode::kSpec; mjsElement* element = nullptr; int element_id = -1; @@ -209,9 +216,10 @@ class App { void ModelOptionsGui(); void DataInspectorGui(); void SpecExplorerGui(); - void PropertiesGui(); + void SpecPropertiesGui(); - void SpecDeleteSelectedElement(); + void SpecSelectElement(mjsElement* element); + void SpecDeleteElement(mjsElement* element); float GetExpectedLabelWidth(); std::vector GetCameraNames(); @@ -228,6 +236,7 @@ class App { std::string model_path_; std::string load_error_; std::string step_error_; + std::string edit_error_; std::optional pending_load_; bool preserve_camera_on_load_ = false; ModelKind model_kind_ = kEmptyModel;