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
This commit is contained in:
Haroon Qureshi
2026-02-24 07:57:01 -08:00
committed by Copybara-Service
parent 5fc580a8ba
commit 1d6ff2cece
9 changed files with 1290 additions and 226 deletions
+2
View File
@@ -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
+1 -108
View File
@@ -470,7 +470,7 @@ void StateGui(const mjModel* model, mjData* data, std::vector<mjtNum>& 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
-8
View File
@@ -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_
+847
View File
@@ -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 <string>
#include <string_view>
#include <imgui.h>
#include <mujoco/mjxmacro.h>
#include <mujoco/mujoco.h>
#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 dist<margin-gap
table("dim", flex->dim, 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 dist<margin-gap
table("friction", pair->friction, 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
+37
View File
@@ -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 <functional>
#include <mujoco/mujoco.h>
namespace mujoco::platform {
using SpecElementCallbackFn = std::function<void(mjsElement*)>;
// 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_
+226
View File
@@ -14,9 +14,13 @@
#include "experimental/platform/imgui_widgets.h"
#include <cstdint>
#include <cstring>
#include <sstream>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <vector>
#include <imgui.h>
#include <mujoco/mujoco.h>
@@ -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<std::string>* 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<int>* 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<double>* 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 <typename T>
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 <typename T>
void ImGui_DataTable::Numeric(const char* label, const T* ptr, int n) {
const T* addr = ptr + index_ * n;
using U = std::conditional_t<std::is_floating_point_v<T>, float, int>;
// special treatment for NaNs.
if constexpr (std::is_same_v<U, float>) {
if (*addr != *addr) {
MakeLabel(label);
ImGui::Text("nan");
return;
}
}
constexpr const char* fmt1 =
std::is_floating_point_v<U> ? "%f" : "%d";
constexpr const char* fmt2 =
std::is_floating_point_v<U> ? "%f %f" : "%d %d";
constexpr const char* fmt3 =
std::is_floating_point_v<U> ? "%f %f %f" : "%d %d %d";
constexpr const char* fmt4 =
std::is_floating_point_v<U> ? "%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);
+80
View File
@@ -15,11 +15,13 @@
#ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_IMGUI_WIDGETS_H_
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_IMGUI_WIDGETS_H_
#include <cstdint>
#include <optional>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>
#include <imgui.h>
#include <imgui_internal.h>
@@ -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<int>* ptr, int n);
void operator()(const char* label, const std::vector<double>* ptr, int n);
void operator()(const char* label, const std::vector<std::string>* ptr, int n);
private:
template <typename T>
void Numeric(const char* label, const T* ptr, int n);
template <typename T>
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);
+86 -108
View File
@@ -36,6 +36,7 @@
#include <mujoco/mujoco.h>
#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<void()> 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();
+11 -2
View File
@@ -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<std::string> 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<const char*> 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<std::string> pending_load_;
bool preserve_camera_on_load_ = false;
ModelKind model_kind_ = kEmptyModel;