Add an Editor panel.

The editor panel allows you to add, remove, and modify spec elements
as well as recompiling and loading the spec.

Also includes utilities for handling vertically split panels.

PiperOrigin-RevId: 875658785
Change-Id: I18b10971d587db5eb1e65a033e1dc042faac7f1d
This commit is contained in:
Haroon Qureshi
2026-02-26 04:50:31 -08:00
committed by Copybara-Service
parent 7232ec3b47
commit a9675fbaa6
7 changed files with 1015 additions and 560 deletions
+1
View File
@@ -246,6 +246,7 @@ ImVec4 ConfigureDockingLayout() {
ImGui::DockBuilderDockWindow("Dockspace", main); ImGui::DockBuilderDockWindow("Dockspace", main);
ImGui::DockBuilderDockWindow("Options", options); ImGui::DockBuilderDockWindow("Options", options);
ImGui::DockBuilderDockWindow("Explorer", inspector); ImGui::DockBuilderDockWindow("Explorer", inspector);
ImGui::DockBuilderDockWindow("Editor", inspector);
ImGui::DockBuilderDockWindow("Inspector", inspector); ImGui::DockBuilderDockWindow("Inspector", inspector);
ImGui::DockBuilderDockWindow("Properties", properties); ImGui::DockBuilderDockWindow("Properties", properties);
ImGui::DockBuilderDockWindow("Stats", stats); ImGui::DockBuilderDockWindow("Stats", stats);
+445 -360
View File
@@ -63,7 +63,7 @@ static int GetElementIndexInSpec(mjsElement* element) {
// Returns a name for the element; either the element has a name, or we // 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 // construct a unique name from the element's id (using mjs_getId) or index
// (using GetElementIndexInSpec). // (using GetElementIndexInSpec).
static std::string ElementName(mjsElement* element) { std::string ElementName(mjsElement* element) {
const mjString* name = mjs_getName(element); const mjString* name = mjs_getName(element);
std::string label = *name; std::string label = *name;
if (label.empty()) { if (label.empty()) {
@@ -77,22 +77,51 @@ static std::string ElementName(mjsElement* element) {
return label; return label;
} }
static void AddDeleteButton(mjsElement* element, static bool AddDeleteButton(mjsElement* element) {
const SpecElementCallbackFn& on_delete) {
if (on_delete) {
// Right-align the delete button. // Right-align the delete button.
const float button_width = ImGui::CalcTextSize(ICON_FA_TRASH_CAN).x + const float button_width = ImGui::CalcTextSize(ICON_FA_TRASH_CAN).x +
ImGui::GetStyle().FramePadding.x * 2.0f; ImGui::GetStyle().FramePadding.x * 2.0f;
ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - button_width); ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - button_width);
if (ImGui::SmallButton(ICON_FA_TRASH_CAN)) { if (ImGui::SmallButton(ICON_FA_TRASH_CAN)) {
on_delete(element); mjs_delete(mjs_getSpec(element), element);
} return true;
} }
return false;
} }
static void SelectableElement(mjsElement* element, static bool AddBodyAddChildButton(mjsElement* element, mjsElement** selected_element) {
// Right-align the add button.
const float button_width = ImGui::CalcTextSize(ICON_FA_TRASH_CAN).x +
ImGui::CalcTextSize(ICON_FA_PLUS).x +
ImGui::GetStyle().FramePadding.x * 4.0f;
ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - button_width);
if (ImGui::SmallButton(ICON_FA_PLUS)) {
ImGui::OpenPopupOnItemClick("BodyAddChild", 0);
}
bool modified = false;
if (ImGui::BeginPopupContextItem("BodyAddChild")) {
mjsBody* body = mjs_asBody(element);
auto option = [&](const char* label, auto fn) {
if (ImGui::Selectable(label)) {
*selected_element = fn()->element;
mjs_setName(*selected_element, ElementName(*selected_element).c_str());
modified = true;
}
};
option("Camera", [&]() { return mjs_addCamera(body, nullptr); });
option("Frame", [&]() { return mjs_addFrame(body, nullptr); });
option("Geom", [&]() { return mjs_addGeom(body, nullptr); });
option("Joint", [&]() { return mjs_addJoint(body, nullptr); });
option("Light", [&]() { return mjs_addLight(body, nullptr); });
option("Site", [&]() { return mjs_addSite(body, nullptr); });
ImGui::EndPopup();
}
return modified;
}
static bool SelectableElement(mjsElement* element,
mjsElement** selected_element, mjsElement** selected_element,
const SpecElementCallbackFn& on_delete) { SpecEditMode mode) {
constexpr ImGuiSelectableFlags flags = ImGuiSelectableFlags_AllowOverlap; constexpr ImGuiSelectableFlags flags = ImGuiSelectableFlags_AllowOverlap;
const std::string name = ElementName(element); const std::string name = ElementName(element);
@@ -100,51 +129,63 @@ static void SelectableElement(mjsElement* element,
if (ImGui::Selectable(name.c_str(), selected, flags)) { if (ImGui::Selectable(name.c_str(), selected, flags)) {
*selected_element = element; *selected_element = element;
} }
if (selected) {
AddDeleteButton(element, on_delete); bool modified = false;
if (selected && mode == SpecEditMode::kEdit) {
if (AddDeleteButton(element)) {
*selected_element = nullptr;
modified = true;
} }
} }
return modified;
}
static void BodyChildrenGui(const char* heading, mjtObj type, static bool BodyChildrenGui(const char* heading, mjtObj type,
mjsElement** element, mjsBody* body, mjsElement** element, mjsBody* body,
const SpecElementCallbackFn& on_delete) { SpecEditMode mode) {
mjsElement* iter = mjs_firstChild(body, type, 0); mjsElement* iter = mjs_firstChild(body, type, 0);
if (!iter) { if (!iter) {
return; return false;
} }
bool modified = false;
constexpr ImGuiTreeNodeFlags tree_flags = constexpr ImGuiTreeNodeFlags tree_flags =
ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_DrawLinesFull; ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_DrawLinesFull;
if (ImGui::TreeNodeEx(heading, tree_flags)) { if (ImGui::TreeNodeEx(heading, tree_flags)) {
while (iter) { while (iter) {
SelectableElement(iter, element, on_delete); mjsElement* next = mjs_nextChild(body, iter, 0);
iter = mjs_nextChild(body, iter, 0); modified |= SelectableElement(iter, element, mode);
iter = next;
} }
ImGui::TreePop(); ImGui::TreePop();
} }
return modified;
} }
static void ElementListGui(const char* heading, mjtObj type, static bool ElementListGui(const char* heading, mjtObj type,
mjsElement** element, mjSpec* spec, mjsElement** element, mjSpec* spec,
const SpecElementCallbackFn& on_delete) { SpecEditMode mode) {
mjsElement* iter = mjs_firstElement(spec, type); mjsElement* iter = mjs_firstElement(spec, type);
if (!iter) { if (!iter) {
return; return false;
} }
bool modified = false;
constexpr ImGuiTreeNodeFlags tree_flags = constexpr ImGuiTreeNodeFlags tree_flags =
ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed;
if (ImGui::TreeNodeEx(heading, tree_flags)) { if (ImGui::TreeNodeEx(heading, tree_flags)) {
while (iter) { while (iter) {
SelectableElement(iter, element, on_delete); mjsElement* next = mjs_nextElement(spec, iter);
iter = mjs_nextElement(spec, iter); modified |= SelectableElement(iter, element, mode);
iter = next;
} }
ImGui::TreePop(); ImGui::TreePop();
} }
return modified;
} }
static void BodyTreeGuiRecursive(mjsElement** element, mjsBody* body, static bool BodyTreeGuiRecursive(mjsElement** element, mjsBody* body,
const SpecElementCallbackFn& on_delete) { SpecEditMode mode) {
const std::string label = ElementName(body->element); const std::string label = ElementName(body->element);
ImGui::PushID(body); ImGui::PushID(body);
@@ -157,52 +198,61 @@ static void BodyTreeGuiRecursive(mjsElement** element, mjsBody* body,
flags |= ImGuiTreeNodeFlags_Selected; flags |= ImGuiTreeNodeFlags_Selected;
} }
bool modified = false;
const bool tree_open = ImGui::TreeNodeEx(label.c_str(), flags); const bool tree_open = ImGui::TreeNodeEx(label.c_str(), flags);
if (ImGui::IsItemClicked()) { if (ImGui::IsItemClicked()) {
*element = body->element; *element = body->element;
} }
if (*element == body->element) { if (*element == body->element && mode == SpecEditMode::kEdit) {
AddDeleteButton(body->element, on_delete); modified |= AddBodyAddChildButton(body->element, element);
modified |= AddDeleteButton(body->element);
} }
if (tree_open) { if (tree_open) {
mjsElement* iter = mjs_firstChild(body, mjOBJ_BODY, 0); mjsElement* iter = mjs_firstChild(body, mjOBJ_BODY, 0);
while (iter) { while (iter) {
BodyTreeGuiRecursive(element, mjs_asBody(iter), on_delete); mjsElement* next = mjs_nextChild(body, iter, 0);
iter = mjs_nextChild(body, iter, 0); modified |= BodyTreeGuiRecursive(element, mjs_asBody(iter), mode);
iter = next;
} }
BodyChildrenGui("Frames", mjOBJ_FRAME, element, body, on_delete); modified |= BodyChildrenGui("Frames", mjOBJ_FRAME, element, body, mode);
BodyChildrenGui("Sites", mjOBJ_SITE, element, body, on_delete); modified |= BodyChildrenGui("Sites", mjOBJ_SITE, element, body, mode);
BodyChildrenGui("Joints", mjOBJ_JOINT, element, body, on_delete); modified |= BodyChildrenGui("Joints", mjOBJ_JOINT, element, body, mode);
BodyChildrenGui("Geoms", mjOBJ_GEOM, element, body, on_delete); modified |= BodyChildrenGui("Geoms", mjOBJ_GEOM, element, body, mode);
BodyChildrenGui("Lights", mjOBJ_LIGHT, element, body, on_delete); modified |= BodyChildrenGui("Lights", mjOBJ_LIGHT, element, body, mode);
BodyChildrenGui("Cameras", mjOBJ_CAMERA, element, body, on_delete); modified |= BodyChildrenGui("Cameras", mjOBJ_CAMERA, element, body, mode);
ImGui::TreePop(); ImGui::TreePop();
} }
ImGui::PopID(); ImGui::PopID();
return modified;
} }
void SpecExplorerGui(mjsElement** element, mjSpec* spec, bool SpecTreeGui(mjsElement** element, mjSpec* spec, SpecEditMode mode) {
const SpecElementCallbackFn& on_delete) { bool modified = false;
const ImGuiTreeNodeFlags flags = const ImGuiTreeNodeFlags flags =
ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed;
ScopedStyle style;
style.Var(ImGuiStyleVar_ItemSpacing, ImVec2(4, 2));
style.Var(ImGuiStyleVar_FramePadding, ImVec2(4, 0));
style.Color(ImGuiCol_Border, ImGuiCol_WindowBg);
if (ImGui::TreeNodeEx("Body Tree", flags)) { if (ImGui::TreeNodeEx("Body Tree", flags)) {
mjsElement* root = mjs_firstElement(spec, mjOBJ_BODY); mjsElement* root = mjs_firstElement(spec, mjOBJ_BODY);
if (root) { if (root) {
mjsBody* body = mjs_asBody(root); mjsBody* body = mjs_asBody(root);
if (body) { if (body) {
BodyTreeGuiRecursive(element, body, on_delete); modified |= BodyTreeGuiRecursive(element, body, mode);
} }
} }
ImGui::TreePop(); ImGui::TreePop();
} }
auto list = [&](const char* heading, mjtObj type) { auto list = [&](const char* heading, mjtObj type) {
ElementListGui(heading, type, element, spec, on_delete); modified |= ElementListGui(heading, type, element, spec, mode);
}; };
ImGui::PushID(spec); ImGui::PushID(spec);
@@ -235,412 +285,447 @@ void SpecExplorerGui(mjsElement** element, mjSpec* spec,
} }
ImGui::PopID(); ImGui::PopID();
return modified;
} }
void ElementSpecGui(const mjSpec* spec, mjsElement* element) { bool ElementSpecGui(mjsElement* element, mjsElement* ref_element,
SpecEditMode mode) {
if (element == nullptr) { if (element == nullptr) {
return; return false;
}
if (ref_element == nullptr) {
ref_element = element;
} }
ImGui_SpecElementTable table; #define FIELD(NAME, TIP) table(#NAME, elem->NAME, ref->NAME, TIP);
#define QFIELD(NAME, ALT, TIP) table(#NAME, #ALT, elem->NAME, ref->NAME, elem->ALT, ref->ALT, TIP);
ImGui_SpecElementTable table(mode == SpecEditMode::kPlay);
switch (element->elemtype) { switch (element->elemtype) {
case mjOBJ_BODY: { case mjOBJ_BODY: {
mjsBody* body = mjs_asBody(element); mjsBody* elem = mjs_asBody(element);
table("childclass", body->childclass, "childclass name"); mjsBody* ref = mjs_asBody(ref_element);
table("pos", body->pos, "frame position"); FIELD(childclass, "childclass name");
table("quat", body->quat, "alt", body->alt, "frame orientation"); FIELD(pos, "frame position");
table("ipos", body->ipos, "inertial frame position"); QFIELD(quat, alt, "frame orientation");
table("iquat", body->iquat, "ialt", body->ialt, "inertial frame orientation"); FIELD(ipos, "inertial frame position");
table("mass", body->mass, "mass"); QFIELD(iquat, ialt, "inertial frame orientation");
table("inertia", body->inertia, "diagonal inertia (in i-frame)"); FIELD(mass, "mass");
table("fullinertia", body->fullinertia, "non-axis-aligned inertia matrix"); FIELD(inertia, "diagonal inertia (in i-frame)");
table("mocap", body->mocap, "is this a mocap body"); FIELD(fullinertia, "non-axis-aligned inertia matrix");
table("gravcomp", body->gravcomp, "gravity compensation"); FIELD(mocap, "is this a mocap body");
table("explicitinertial", body->explicitinertial, "whether to save the body with explicit inertial clause"); FIELD(gravcomp, "gravity compensation");
table("sleep", body->sleep, "sleep policy"); FIELD(explicitinertial, "whether to save the body with explicit inertial clause");
table("info", body->info, "message appended to compiler errors"); FIELD(sleep, "sleep policy");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_JOINT: { case mjOBJ_JOINT: {
mjsJoint* joint = mjs_asJoint(element); mjsJoint* elem = mjs_asJoint(element);
table("pos", joint->pos, "anchor position"); mjsJoint* ref = mjs_asJoint(ref_element);
table("axis", joint->axis, "joint axis"); FIELD(pos, "anchor position");
table("ref", joint->ref, "value at reference configuration: qpos0"); FIELD(axis, "joint axis");
table("align", joint->align, "align free joint with body com (mjtAlignFree)"); FIELD(ref, "value at reference configuration: qpos0");
table("stiffness", joint->stiffness, "stiffness coefficient"); FIELD(align, "align free joint with body com (mjtAlignFree)");
table("springref", joint->springref, "spring reference value: qpos_spring"); FIELD(stiffness, "stiffness coefficient");
table("springdamper", joint->springdamper, "timeconst, dampratio"); FIELD(springref, "spring reference value: qpos_spring");
table("limited", joint->limited, "does joint have limits (mjtLimited)"); FIELD(springdamper, "timeconst, dampratio");
table("range", joint->range, "joint limits"); FIELD(limited, "does joint have limits (mjtLimited)");
table("margin", joint->margin, "margin value for joint limit detection"); FIELD(range, "joint limits");
table("solref_limit", joint->solref_limit, "solver reference: joint limits"); FIELD(margin, "margin value for joint limit detection");
table("solimp_limit", joint->solimp_limit, "solver impedance: joint limits"); FIELD(solref_limit, "solver reference: joint limits");
table("actfrclimited", joint->actfrclimited, "are actuator forces on joint limited (mjtLimited)"); FIELD(solimp_limit, "solver impedance: joint limits");
table("actfrcrange", joint->actfrcrange, "actuator force limits"); FIELD(actfrclimited, "are actuator forces on joint limited (mjtLimited)");
table("armature", joint->armature, "armature inertia (mass for slider)"); FIELD(actfrcrange, "actuator force limits");
table("damping", joint->damping, "damping coefficient"); FIELD(armature, "armature inertia (mass for slider)");
table("frictionloss", joint->frictionloss, "friction loss"); FIELD(damping, "damping coefficient");
table("solref_friction", joint->solref_friction, "solver reference: dof friction"); FIELD(frictionloss, "friction loss");
table("solimp_friction", joint->solimp_friction, "solver impedance: dof friction"); FIELD(solref_friction, "solver reference: dof friction");
table("group", joint->group, "group"); FIELD(solimp_friction, "solver impedance: dof friction");
table("actgravcomp", joint->actgravcomp, "is gravcomp force applied via actuators"); FIELD(group, "group");
table("info", joint->info, "message appended to compiler errors"); FIELD(actgravcomp, "is gravcomp force applied via actuators");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_ACTUATOR: { case mjOBJ_ACTUATOR: {
mjsActuator* actuator = mjs_asActuator(element); mjsActuator* elem = mjs_asActuator(element);
table("gaintype", actuator->gaintype, "gain type"); mjsActuator* ref = mjs_asActuator(ref_element);
table("gainprm", actuator->gainprm, "gain parameters"); FIELD(gaintype, "gain type");
table("biastype", actuator->biastype, "bias type"); FIELD(gainprm, "gain parameters");
table("biasprm", actuator->biasprm, "bias parameters"); FIELD(biastype, "bias type");
table("dyntype", actuator->dyntype, "dynamics type"); FIELD(biasprm, "bias parameters");
table("dynprm", actuator->dynprm, "dynamics parameters"); FIELD(dyntype, "dynamics type");
table("actdim", actuator->actdim, "number of activation variables"); FIELD(dynprm, "dynamics parameters");
table("actearly", actuator->actearly, "apply next activations to qfrc"); FIELD(actdim, "number of activation variables");
table("trntype", actuator->trntype, "transmission type"); FIELD(actearly, "apply next activations to qfrc");
table("gear", actuator->gear, "length and transmitted force scaling"); FIELD(trntype, "transmission type");
table("target", actuator->target, "name of transmission target"); FIELD(gear, "length and transmitted force scaling");
table("refsite", actuator->refsite, "reference site, for site transmission"); FIELD(target, "name of transmission target");
table("slidersite", actuator->slidersite, "site defining cylinder, for slider-crank"); FIELD(refsite, "reference site, for site transmission");
table("cranklength", actuator->cranklength, "crank length, for slider-crank"); FIELD(slidersite, "site defining cylinder, for slider-crank");
table("lengthrange", actuator->lengthrange, "transmission length range"); FIELD(cranklength, "crank length, for slider-crank");
table("inheritrange", actuator->inheritrange, "automatic range setting for position and intvelocity"); FIELD(lengthrange, "transmission length range");
table("ctrllimited", actuator->ctrllimited, "are control limits defined (mjtLimited)"); FIELD(inheritrange, "automatic range setting for position and intvelocity");
table("ctrlrange", actuator->ctrlrange, "control range"); FIELD(ctrllimited, "are control limits defined (mjtLimited)");
table("forcelimited", actuator->forcelimited, "are force limits defined (mjtLimited)"); FIELD(ctrlrange, "control range");
table("forcerange", actuator->forcerange, "force range"); FIELD(forcelimited, "are force limits defined (mjtLimited)");
table("actlimited", actuator->actlimited, "are activation limits defined (mjtLimited)"); FIELD(forcerange, "force range");
table("actrange", actuator->actrange, "activation range"); FIELD(actlimited, "are activation limits defined (mjtLimited)");
table("group", actuator->group, "group"); FIELD(actrange, "activation range");
table("nsample", actuator->nsample, "number of samples in history buffer"); FIELD(group, "group");
table("interp", actuator->interp, "interpolation order (0=ZOH, 1=linear, 2=cubic)"); FIELD(nsample, "number of samples in history buffer");
table("delay", actuator->delay, "delay time in seconds; 0: no delay"); FIELD(interp, "interpolation order (0=ZOH, 1=linear, 2=cubic)");
table("info", actuator->info, "message appended to compiler errors"); FIELD(delay, "delay time in seconds; 0: no delay");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_SENSOR: { case mjOBJ_SENSOR: {
mjsSensor* sensor = mjs_asSensor(element); mjsSensor* elem = mjs_asSensor(element);
table("type", sensor->type, "type of sensor"); mjsSensor* ref = mjs_asSensor(ref_element);
table("objtype", sensor->objtype, "type of sensorized object"); FIELD(type, "type of sensor");
table("objname", sensor->objname, "name of sensorized object"); FIELD(objtype, "type of sensorized object");
table("reftype", sensor->reftype, "type of referenced object"); FIELD(objname, "name of sensorized object");
table("refname", sensor->refname, "name of referenced object"); FIELD(reftype, "type of referenced object");
table("intprm", sensor->intprm, "integer parameters"); FIELD(refname, "name of referenced object");
table("datatype", sensor->datatype, "data type for sensor measurement"); FIELD(intprm, "integer parameters");
table("needstage", sensor->needstage, "compute stage needed to simulate sensor"); FIELD(datatype, "data type for sensor measurement");
table("dim", sensor->dim, "number of scalar outputs"); FIELD(needstage, "compute stage needed to simulate sensor");
table("cutoff", sensor->cutoff, "cutoff for real and positive datatypes"); FIELD(dim, "number of scalar outputs");
table("noise", sensor->noise, "noise stdev"); FIELD(cutoff, "cutoff for real and positive datatypes");
table("nsample", sensor->nsample, "number of samples in history buffer"); FIELD(noise, "noise stdev");
table("interp", sensor->interp, "interpolation order (0=ZOH, 1=linear, 2=cubic)"); FIELD(nsample, "number of samples in history buffer");
table("delay", sensor->delay, "delay time in seconds"); FIELD(interp, "interpolation order (0=ZOH, 1=linear, 2=cubic)");
table("interval", sensor->interval, "[period, time_prev] in seconds"); FIELD(delay, "delay time in seconds");
table("info", sensor->info, "message appended to compiler errors"); FIELD(interval, "[period, time_prev] in seconds");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_SITE: { case mjOBJ_SITE: {
mjsSite* site = mjs_asSite(element); mjsSite* elem = mjs_asSite(element);
table("pos", site->pos, "position"); mjsSite* ref = mjs_asSite(ref_element);
table("quat", site->quat, "alt", site->alt, "orientation"); FIELD(pos, "position");
table("fromto", site->fromto, "alternative for capsule, cylinder, box, ellipsoid"); QFIELD(quat, alt, "orientation");
table("size", site->size, "geom size"); FIELD(fromto, "alternative for capsule, cylinder, box, ellipsoid");
table("type", site->type, "geom type"); FIELD(size, "geom size");
table("material", site->material, "name of material"); FIELD(type, "geom type");
table("group", site->group, "group"); FIELD(material, "name of material");
table("rgba", site->rgba, "rgba when material is omitted"); FIELD(group, "group");
table("info", site->info, "message appended to compiler errors"); FIELD(rgba, "rgba when material is omitted");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_FRAME: { case mjOBJ_FRAME: {
mjsFrame* frame = mjs_asFrame(element); mjsFrame* elem = mjs_asFrame(element);
table("childclass", frame->childclass, "childclass name"); mjsFrame* ref = mjs_asFrame(ref_element);
table("pos", frame->pos, "position"); FIELD(childclass, "childclass name");
table("quat", frame->quat, "alt", frame->alt, "orientation"); FIELD(pos, "position");
table("info", frame->info, "message appended to compiler errors"); QFIELD(quat, alt, "orientation");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_GEOM: { case mjOBJ_GEOM: {
mjsGeom* geom = mjs_asGeom(element); mjsGeom* elem = mjs_asGeom(element);
table("type", geom->type, "geom type"); mjsGeom* ref = mjs_asGeom(ref_element);
table("pos", geom->pos, "position"); FIELD(type, "geom type");
table("quat", geom->quat, "alt", geom->alt, "orientation"); FIELD(pos, "position");
table("fromto", geom->fromto, "alternative for capsule, cylinder, box, ellipsoid"); QFIELD(quat, alt, "orientation");
table("size", geom->size, "type-specific size"); FIELD(fromto, "alternative for capsule, cylinder, box, ellipsoid");
table("contype", geom->contype, "contact type"); FIELD(size, "type-specific size");
table("conaffinity", geom->conaffinity, "contact affinity"); FIELD(contype, "contact type");
table("condim", geom->condim, "contact dimensionality"); FIELD(conaffinity, "contact affinity");
table("priority", geom->priority, "contact priority"); FIELD(condim, "contact dimensionality");
table("friction", geom->friction, "one-sided friction coefficients: slide, roll, spin"); FIELD(priority, "contact priority");
table("solmix", geom->solmix, "solver mixing for contact pairs"); FIELD(friction, "one-sided friction coefficients: slide, roll, spin");
table("solref", geom->solref, "solver reference"); FIELD(solmix, "solver mixing for contact pairs");
table("solimp", geom->solimp, "solver impedance"); FIELD(solref, "solver reference");
table("margin", geom->margin, "margin for contact detection"); FIELD(solimp, "solver impedance");
table("gap", geom->gap, "include in solver if dist < margin-gap"); FIELD(margin, "margin for contact detection");
table("mass", geom->mass, "used to compute density"); FIELD(gap, "include in solver if dist < margin-gap");
table("density", geom->density, "used to compute mass and inertia from volume or surface"); FIELD(mass, "used to compute density");
table("typeinertia", geom->typeinertia, "selects between surface and volume inertia"); FIELD(density, "used to compute mass and inertia from volume or surface");
table("fluid_ellipsoid", geom->fluid_ellipsoid, "whether ellipsoid-fluid model is active"); FIELD(typeinertia, "selects between surface and volume inertia");
table("fluid_coefs", geom->fluid_coefs, "ellipsoid-fluid interaction coefs"); FIELD(fluid_ellipsoid, "whether ellipsoid-fluid model is active");
table("material", geom->material, "name of material"); FIELD(fluid_coefs, "ellipsoid-fluid interaction coefs");
table("rgba", geom->rgba, "rgba when material is omitted"); FIELD(material, "name of material");
table("group", geom->group, "group"); FIELD(rgba, "rgba when material is omitted");
table("hfieldname", geom->hfieldname, "heightfield attached to geom"); FIELD(group, "group");
table("meshname", geom->meshname, "mesh attached to geom"); FIELD(hfieldname, "heightfield attached to geom");
table("fitscale", geom->fitscale, "scale mesh uniformly"); FIELD(meshname, "mesh attached to geom");
table("info", geom->info, "message appended to compiler errors"); FIELD(fitscale, "scale mesh uniformly");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_LIGHT: { case mjOBJ_LIGHT: {
mjsLight* light = mjs_asLight(element); mjsLight* elem = mjs_asLight(element);
table("pos", light->pos, "position"); mjsLight* ref = mjs_asLight(ref_element);
table("dir", light->dir, "direction"); FIELD(pos, "position");
table("mode", light->mode, "tracking mode"); FIELD(dir, "direction");
table("targetbody", light->targetbody, "target body for targeting"); FIELD(mode, "tracking mode");
table("active", light->active, "is light active"); FIELD(targetbody, "target body for targeting");
table("type", light->type, "type of light"); FIELD(active, "is light active");
table("texture", light->texture, "texture name for image lights"); FIELD(type, "type of light");
table("castshadow", light->castshadow, "does light cast shadows"); FIELD(texture, "texture name for image lights");
table("bulbradius", light->bulbradius, "bulb radius, for soft shadows"); FIELD(castshadow, "does light cast shadows");
table("intensity", light->intensity, "intensity, in candelas"); FIELD(bulbradius, "bulb radius, for soft shadows");
table("range", light->range, "range of effectiveness"); FIELD(intensity, "intensity, in candelas");
table("attenuation", light->attenuation, "OpenGL attenuation (quadratic model)"); FIELD(range, "range of effectiveness");
table("cutoff", light->cutoff, "OpenGL cutoff"); FIELD(attenuation, "OpenGL attenuation (quadratic model)");
table("exponent", light->exponent, "OpenGL exponent"); FIELD(cutoff, "OpenGL cutoff");
table("ambient", light->ambient, "ambient color"); FIELD(exponent, "OpenGL exponent");
table("diffuse", light->diffuse, "diffuse color"); FIELD(ambient, "ambient color");
table("specular", light->specular, "specular color"); FIELD(diffuse, "diffuse color");
table("info", light->info, "message appended to compiler errorsx"); FIELD(specular, "specular color");
FIELD(info, "message appended to compiler errorsx");
break; break;
} }
case mjOBJ_CAMERA: { case mjOBJ_CAMERA: {
mjsCamera* camera = mjs_asCamera(element); mjsCamera* elem = mjs_asCamera(element);
table("pos", camera->pos, "position"); mjsCamera* ref = mjs_asCamera(ref_element);
table("quat", camera->quat, "alt", camera->alt, "orientation"); FIELD(pos, "position");
table("mode", camera->mode, "tracking mode"); QFIELD(quat, alt, "orientation");
table("targetbody", camera->targetbody, "target body for tracking/targeting"); FIELD(mode, "tracking mode");
table("proj", camera->proj, "camera projection type"); FIELD(targetbody, "target body for tracking/targeting");
table("resolution", camera->resolution, "resolution (pixel)"); FIELD(proj, "camera projection type");
table("output", camera->output, "bit flags for output type"); FIELD(resolution, "resolution (pixel)");
table("fovy", camera->fovy, "y-field of view"); FIELD(output, "bit flags for output type");
table("ipd", camera->ipd, "inter-pupillary distance"); FIELD(fovy, "y-field of view");
table("intrinsic", camera->intrinsic, "camera intrinsics (length)"); FIELD(ipd, "inter-pupillary distance");
table("sensor_size", camera->sensor_size, "sensor size (length)"); FIELD(intrinsic, "camera intrinsics (length)");
table("focal_length", camera->focal_length, "focal length (length)"); FIELD(sensor_size, "sensor size (length)");
table("focal_pixel", camera->focal_pixel, "focal length (pixel)"); FIELD(focal_length, "focal length (length)");
table("principal_length", camera->principal_length, "principal point (length)"); FIELD(focal_pixel, "focal length (pixel)");
table("principal_pixel", camera->principal_pixel, "principal point (pixel)"); FIELD(principal_length, "principal point (length)");
table("info", camera->info, "message appended to compiler errors"); FIELD(principal_pixel, "principal point (pixel)");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_MESH: { case mjOBJ_MESH: {
mjsMesh* mesh = mjs_asMesh(element); mjsMesh* elem = mjs_asMesh(element);
table("content_type", mesh->content_type, "content type of file"); mjsMesh* ref = mjs_asMesh(ref_element);
table("file", mesh->file, "mesh file"); FIELD(content_type, "content type of file");
table("refpos", mesh->refpos, "reference position"); FIELD(file, "mesh file");
table("refquat", mesh->refquat, "reference orientation"); FIELD(refpos, "reference position");
table("scale", mesh->scale, "rescale mesh"); FIELD(refquat, "reference orientation");
table("inertia", mesh->inertia, "inertia type (convex, legacy, exact, shell)"); FIELD(scale, "rescale mesh");
table("smoothnormal", mesh->smoothnormal, "do not exclude large-angle faces from normals"); FIELD(inertia, "inertia type (convex, legacy, exact, shell)");
table("needsdf", mesh->needsdf, "compute sdf from mesh"); FIELD(smoothnormal, "do not exclude large-angle faces from normals");
table("maxhullvert", mesh->maxhullvert, "maximum vertex count for the convex hull"); FIELD(needsdf, "compute sdf from mesh");
table("material", mesh->material, "name of material"); FIELD(maxhullvert, "maximum vertex count for the convex hull");
table("info", mesh->info, "message appended to compiler errors"); FIELD(material, "name of material");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_HFIELD: { case mjOBJ_HFIELD: {
mjsHField* hfield = mjs_asHField(element); mjsHField* elem = mjs_asHField(element);
table("content_type", hfield->content_type, "content type of file"); mjsHField* ref = mjs_asHField(ref_element);
table("file", hfield->file, "file: (nrow, ncol, [elevation data])"); FIELD(content_type, "content type of file");
table("size", hfield->size, "hfield size (ignore referencing geom size)"); FIELD(file, "file: (nrow, ncol, [elevation data])");
table("nrow", hfield->nrow, "number of rows"); FIELD(size, "hfield size (ignore referencing geom size)");
table("ncol", hfield->ncol, "number of columns"); FIELD(nrow, "number of rows");
table("info", hfield->info, "message appended to compiler errors"); FIELD(ncol, "number of columns");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_SKIN: { case mjOBJ_SKIN: {
mjsSkin* skin = mjs_asSkin(element); mjsSkin* elem = mjs_asSkin(element);
table("file", skin->file, "skin file"); mjsSkin* ref = mjs_asSkin(ref_element);
table("material", skin->material, "name of material used for rendering"); FIELD(file, "skin file");
table("rgba", skin->rgba, "rgba when material is omitted"); FIELD(material, "name of material used for rendering");
table("inflate", skin->inflate, "inflate in normal direction"); FIELD(rgba, "rgba when material is omitted");
table("group", skin->group, "group for visualization"); FIELD(inflate, "inflate in normal direction");
table("info", skin->info, "message appended to compiler errors"); FIELD(group, "group for visualization");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_FLEX: { case mjOBJ_FLEX: {
mjsFlex* flex = mjs_asFlex(element); mjsFlex* elem = mjs_asFlex(element);
table("contype", flex->contype, "contact type"); mjsFlex* ref = mjs_asFlex(ref_element);
table("conaffinity", flex->conaffinity, "contact affinity"); FIELD(contype, "contact type");
table("condim", flex->condim, "contact dimensionality"); FIELD(conaffinity, "contact affinity");
table("priority", flex->priority, "contact priority"); FIELD(condim, "contact dimensionality");
table("friction", flex->friction, "one-sided friction coefficients: slide, roll, spin"); FIELD(priority, "contact priority");
table("solmix", flex->solmix, "solver mixing for contact pairs"); FIELD(friction, "one-sided friction coefficients: slide, roll, spin");
table("solref", flex->solref, "solver reference"); FIELD(solmix, "solver mixing for contact pairs");
table("solimp", flex->solimp, "solver impedance"); FIELD(solref, "solver reference");
table("margin", flex->margin, "margin for contact detection"); FIELD(solimp, "solver impedance");
table("gap", flex->gap, "include in solver if dist<margin-gap"); FIELD(margin, "margin for contact detection");
table("dim", flex->dim, "element dimensionality"); FIELD(gap, "include in solver if dist<margin-gap");
table("radius", flex->radius, "radius around primitive element"); FIELD(dim, "element dimensionality");
table("size", flex->size, "vertex bounding box half sizes in qpos0"); FIELD(radius, "radius around primitive element");
table("internal", flex->internal, "enable internal collisions"); FIELD(size, "vertex bounding box half sizes in qpos0");
table("flatskin", flex->flatskin, "render flex skin with flat shading"); FIELD(internal, "enable internal collisions");
table("selfcollide", flex->selfcollide, "mode for flex self collision"); FIELD(flatskin, "render flex skin with flat shading");
table("vertcollide", flex->vertcollide, "mode for vertex collision"); FIELD(selfcollide, "mode for flex self collision");
table("passive", flex->passive, "mode for passive collisions"); FIELD(vertcollide, "mode for vertex collision");
table("activelayers", flex->activelayers, "number of active element layers in 3D"); FIELD(passive, "mode for passive collisions");
table("group", flex->group, "group for visualization"); FIELD(activelayers, "number of active element layers in 3D");
table("edgestiffness", flex->edgestiffness, "edge stiffness"); FIELD(group, "group for visualization");
table("edgedamping", flex->edgedamping, "edge damping"); FIELD(edgestiffness, "edge stiffness");
table("rgba", flex->rgba, "rgba when material is omitted"); FIELD(edgedamping, "edge damping");
table("material", flex->material, "name of material used for rendering"); FIELD(rgba, "rgba when material is omitted");
table("young", flex->young, "Young's modulus"); FIELD(material, "name of material used for rendering");
table("poisson", flex->poisson, "Poisson's ratio"); FIELD(young, "Young's modulus");
table("damping", flex->damping, "Rayleigh's damping"); FIELD(poisson, "Poisson's ratio");
table("thickness", flex->thickness, "thickness (2D only)"); FIELD(damping, "Rayleigh's damping");
table("elastic2d", flex->elastic2d, "2D passive forces; 0: none, 1: bending, 2: stretching, 3: both"); FIELD(thickness, "thickness (2D only)");
table("info", flex->info, "message appended to compiler errors"); FIELD(elastic2d, "2D passive forces; 0: none, 1: bending, 2: stretching, 3: both");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_TENDON: { case mjOBJ_TENDON: {
mjsTendon* tendon = mjs_asTendon(element); mjsTendon* elem = mjs_asTendon(element);
table("stiffness", tendon->stiffness, "stiffness coefficient"); mjsTendon* ref = mjs_asTendon(ref_element);
table("springlength", tendon->springlength, "spring resting length; {-1, -1}: use qpos_spring"); FIELD(stiffness, "stiffness coefficient");
table("damping", tendon->damping, "damping coefficient"); FIELD(springlength, "spring resting length; {-1, -1}: use qpos_spring");
table("frictionloss", tendon->frictionloss, "friction loss"); FIELD(damping, "damping coefficient");
table("solref_friction", tendon->solref_friction, "solver reference: tendon friction"); FIELD(frictionloss, "friction loss");
table("solimp_friction", tendon->solimp_friction, "solver impedance: tendon friction"); FIELD(solref_friction, "solver reference: tendon friction");
table("armature", tendon->armature, "inertia associated with tendon velocity"); FIELD(solimp_friction, "solver impedance: tendon friction");
table("limited", tendon->limited, "does tendon have limits (mjtLimited)"); FIELD(armature, "inertia associated with tendon velocity");
table("actfrclimited", tendon->actfrclimited, "does tendon have actuator force limits"); FIELD(limited, "does tendon have limits (mjtLimited)");
table("range", tendon->range, "length limits"); FIELD(actfrclimited, "does tendon have actuator force limits");
table("actfrcrange", tendon->actfrcrange, "actuator force limits"); FIELD(range, "length limits");
table("margin", tendon->margin, "margin value for tendon limit detection"); FIELD(actfrcrange, "actuator force limits");
table("solref_limit", tendon->solref_limit, "solver reference: tendon limits"); FIELD(margin, "margin value for tendon limit detection");
table("solimp_limit", tendon->solimp_limit, "solver impedance: tendon limits"); FIELD(solref_limit, "solver reference: tendon limits");
table("material", tendon->material, "name of material for rendering"); FIELD(solimp_limit, "solver impedance: tendon limits");
table("width", tendon->width, "width for rendering"); FIELD(material, "name of material for rendering");
table("rgba", tendon->rgba, "rgba when material is omitted"); FIELD(width, "width for rendering");
table("group", tendon->group, "group"); FIELD(rgba, "rgba when material is omitted");
table("info", tendon->info, "message appended to errors"); FIELD(group, "group");
FIELD(info, "message appended to errors");
break; break;
} }
case mjOBJ_TEXTURE: { case mjOBJ_TEXTURE: {
mjsTexture* texture = mjs_asTexture(element); mjsTexture* elem = mjs_asTexture(element);
table("type", texture->type, "texture type"); mjsTexture* ref = mjs_asTexture(ref_element);
table("colorspace", texture->colorspace, "colorspace"); FIELD(type, "texture type");
table("builtin", texture->builtin, "builtin type (mjtBuiltin)"); FIELD(colorspace, "colorspace");
table("mark", texture->mark, "mark type (mjtMark)"); FIELD(builtin, "builtin type (mjtBuiltin)");
table("rgb1", texture->rgb1, "first color for builtin"); FIELD(mark, "mark type (mjtMark)");
table("rgb2", texture->rgb2, "second color for builtin"); FIELD(rgb1, "first color for builtin");
table("markrgb", texture->markrgb, "mark color"); FIELD(rgb2, "second color for builtin");
table("random", texture->random, "probability of random dots"); FIELD(markrgb, "mark color");
table("height", texture->height, "height in pixels (square for cube and skybox)"); FIELD(random, "probability of random dots");
table("width", texture->width, "width in pixels"); FIELD(height, "height in pixels (square for cube and skybox)");
table("nchannel", texture->nchannel, "number of channels"); FIELD(width, "width in pixels");
table("content_type", texture->content_type, "content type of file"); FIELD(nchannel, "number of channels");
table("file", texture->file, "png file to load; use for all sides of cube"); FIELD(content_type, "content type of file");
table("gridsize", texture->gridsize, "size of grid for composite file; (1,1)-repeat"); FIELD(file, "png file to load; use for all sides of cube");
table("gridlayout", texture->gridlayout, "row-major: L,R,F,B,U,D for faces; . for unused"); FIELD(gridsize, "size of grid for composite file; (1,1)-repeat");
table("cubefiles", texture->cubefiles, "different file for each side of the cube"); FIELD(gridlayout, "row-major: L,R,F,B,U,D for faces; . for unused");
table("hflip", texture->hflip, "horizontal flip"); FIELD(cubefiles, "different file for each side of the cube");
table("vflip", texture->vflip, "vertical flip"); FIELD(hflip, "horizontal flip");
table("info", texture->info, "message appended to compiler errors"); FIELD(vflip, "vertical flip");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_MATERIAL: { case mjOBJ_MATERIAL: {
mjsMaterial* material = mjs_asMaterial(element); mjsMaterial* elem = mjs_asMaterial(element);
table("textures", material->textures, "names of textures (empty: none)"); mjsMaterial* ref = mjs_asMaterial(ref_element);
table("texuniform", material->texuniform, "make texture cube uniform"); FIELD(textures, "names of textures (empty: none)");
table("texrepeat", material->texrepeat, "texture repetition for 2D mapping"); FIELD(texuniform, "make texture cube uniform");
table("emission", material->emission, "emission"); FIELD(texrepeat, "texture repetition for 2D mapping");
table("specular", material->specular, "specular"); FIELD(emission, "emission");
table("shininess", material->shininess, "shininess"); FIELD(specular, "specular");
table("reflectance", material->reflectance, "reflectance"); FIELD(shininess, "shininess");
table("metallic", material->metallic, "metallic"); FIELD(reflectance, "reflectance");
table("roughness", material->roughness, "roughness"); FIELD(metallic, "metallic");
table("rgba", material->rgba, "rgba"); FIELD(roughness, "roughness");
table("info", material->info, "message appended to compiler errors"); FIELD(rgba, "rgba");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_PAIR: { case mjOBJ_PAIR: {
mjsPair* pair = mjs_asPair(element); mjsPair* elem = mjs_asPair(element);
table("geomname1", pair->geomname1, "name of geom 1"); mjsPair* ref = mjs_asPair(ref_element);
table("geomname2", pair->geomname2, "name of geom 2"); FIELD(geomname1, "name of geom 1");
table("condim", pair->condim, "contact dimensionality"); FIELD(geomname2, "name of geom 2");
table("solref", pair->solref, "solver reference, normal direction"); FIELD(condim, "contact dimensionality");
table("solreffriction", pair->solreffriction, "solver reference, frictional directions"); FIELD(solref, "solver reference, normal direction");
table("solimp", pair->solimp, "solver impedance"); FIELD(solreffriction, "solver reference, frictional directions");
table("margin", pair->margin, "margin for contact detection"); FIELD(solimp, "solver impedance");
table("gap", pair->gap, "include in solver if dist<margin-gap"); FIELD(margin, "margin for contact detection");
table("friction", pair->friction, "full contact friction"); FIELD(gap, "include in solver if dist<margin-gap");
table("info", pair->info, "message appended to errors"); FIELD(friction, "full contact friction");
FIELD(info, "message appended to errors");
break; break;
} }
case mjOBJ_EQUALITY: { case mjOBJ_EQUALITY: {
mjsEquality* equality = mjs_asEquality(element); mjsEquality* elem = mjs_asEquality(element);
table("type", equality->type, "constraint type"); mjsEquality* ref = mjs_asEquality(ref_element);
table("data", equality->data, "type-dependent data"); FIELD(type, "constraint type");
table("active", equality->active, "is equality initially active"); FIELD(data, "type-dependent data");
table("name1", equality->name1, "name of object 1"); FIELD(active, "is equality initially active");
table("name2", equality->name2, "name of object 2"); FIELD(name1, "name of object 1");
table("objtype", equality->objtype, "type of both objects"); FIELD(name2, "name of object 2");
table("solref", equality->solref, "solver reference"); FIELD(objtype, "type of both objects");
table("solimp", equality->solimp, "solver impedance"); FIELD(solref, "solver reference");
table("info", equality->info, "message appended to errors"); FIELD(solimp, "solver impedance");
FIELD(info, "message appended to errors");
break; break;
} }
case mjOBJ_EXCLUDE: { case mjOBJ_EXCLUDE: {
mjsExclude* exclude = mjs_asExclude(element); mjsExclude* elem = mjs_asExclude(element);
table("bodyname1", exclude->bodyname1, "name of geom 1"); mjsExclude* ref = mjs_asExclude(ref_element);
table("bodyname2", exclude->bodyname2, "name of geom 2"); FIELD(bodyname1, "name of geom 1");
table("info", exclude->info, "message appended to errors"); FIELD(bodyname2, "name of geom 2");
FIELD(info, "message appended to errors");
break; break;
} }
case mjOBJ_NUMERIC: { case mjOBJ_NUMERIC: {
mjsNumeric* numeric = mjs_asNumeric(element); mjsNumeric* elem = mjs_asNumeric(element);
table("data", numeric->data, "initialization data"); mjsNumeric* ref = mjs_asNumeric(ref_element);
table("size", numeric->size, "array size, can be bigger than data size"); FIELD(data, "initialization data");
table("info", numeric->info, "message appended to errors"); FIELD(size, "array size, can be bigger than data size");
FIELD(info, "message appended to errors");
break; break;
} }
case mjOBJ_TEXT: { case mjOBJ_TEXT: {
mjsText* text = mjs_asText(element); mjsText* elem = mjs_asText(element);
table("data", text->data, "text string"); mjsText* ref = mjs_asText(ref_element);
table("info", text->info, "message appended to compiler errors"); FIELD(data, "text string");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_TUPLE: { case mjOBJ_TUPLE: {
mjsTuple* tuple = mjs_asTuple(element); mjsTuple* elem = mjs_asTuple(element);
table("objtype", tuple->objtype, "object types"); mjsTuple* ref = mjs_asTuple(ref_element);
table("objname", tuple->objname, "object names"); FIELD(objtype, "object types");
table("objprm", tuple->objprm, "object parameters"); FIELD(objname, "object names");
table("info", tuple->info, "message appended to compiler errors"); FIELD(objprm, "object parameters");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_KEY: { case mjOBJ_KEY: {
mjsKey* key = mjs_asKey(element); mjsKey* elem = mjs_asKey(element);
table("time", key->time, "time"); mjsKey* ref = mjs_asKey(ref_element);
table("qpos", key->qpos, "qpos"); FIELD(time, "time");
table("qvel", key->qvel, "qvel"); FIELD(qpos, "qpos");
table("act", key->act, "act"); FIELD(qvel, "qvel");
table("mpos", key->mpos, "mocap pos"); FIELD(act, "act");
table("mquat", key->mquat, "mocap quat"); FIELD(mpos, "mocap pos");
table("ctrl", key->ctrl, "ctrl"); FIELD(mquat, "mocap quat");
table("info", key->info, "message appended to compiler errors"); FIELD(ctrl, "ctrl");
FIELD(info, "message appended to compiler errors");
break; break;
} }
case mjOBJ_PLUGIN: { case mjOBJ_PLUGIN: {
mjsPlugin* plugin = mjs_asPlugin(element); mjsPlugin* elem = mjs_asPlugin(element);
table("name", plugin->name, "instance name"); mjsPlugin* ref = mjs_asPlugin(ref_element);
table("plugin_name", plugin->plugin_name, "plugin name"); FIELD(name, "instance name");
table("active", plugin->active, "is the plugin active"); FIELD(plugin_name, "plugin name");
table("info", plugin->info, "message appended to compiler errors"); FIELD(active, "is the plugin active");
FIELD(info, "message appended to compiler errors");
break; break;
} }
default: default:
// ignore other types // ignore other types
break; break;
} }
#undef FIELD
return table.WasModified();
} }
void ElementModelGui(const mjModel* model, mjsElement* element) { void ElementModelGui(const mjModel* model, mjsElement* element) {
+24 -7
View File
@@ -15,21 +15,38 @@
#ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_SPEC_H_ #ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_SPEC_H_
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_SPEC_H_ #define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_SPEC_H_
#include <functional> #include <string>
#include <mujoco/mujoco.h> #include <mujoco/mujoco.h>
namespace mujoco::platform { namespace mujoco::platform {
using SpecElementCallbackFn = std::function<void(mjsElement*)>; // The mode of the spec visualizer.
enum class SpecEditMode {
kPlay,
kEdit,
};
// UX for displaying the spec as a tree. // Returns the name of the given element.
void SpecExplorerGui(mjsElement** element, mjSpec* spec, std::string ElementName(mjsElement* element);
const SpecElementCallbackFn& on_delete);
// UX for displaying the properties of an mjSpec element. // Displaying the mjSpec as a tree. `element` is the currently selected
void ElementSpecGui(const mjSpec* spec, mjsElement* element); // element and will be updated if a new element is selected. The function
// returns true if the spec was modified in any way.
bool SpecTreeGui(mjsElement** element, mjSpec* spec, SpecEditMode mode);
// Displays the properties of the given element in the table. Returns true if
// any value in the element was changed. The `ref_element` is used to highlight
// when a value differs from a reference element.
bool ElementSpecGui(mjsElement* element, mjsElement* ref_element,
SpecEditMode mode);
// Displays a (read-only) data table of the mjData values that correspond to the
// given element.
void ElementDataGui(const mjData* data, mjsElement* element); void ElementDataGui(const mjData* data, mjsElement* element);
// Displays a (read-only) data table of the mjModel values that correspond to
// the given element.
void ElementModelGui(const mjModel* model, mjsElement* element); void ElementModelGui(const mjModel* model, mjsElement* element);
} // namespace mujoco::platform } // namespace mujoco::platform
+98 -32
View File
@@ -62,12 +62,17 @@ KeyValues ReadIniSection(const std::string& contents,
} }
ImGui_DataPtrTable::ImGui_DataPtrTable(float w1, float w2) { ImGui_DataPtrTable::ImGui_DataPtrTable(float w1, float w2) {
ImGui::BeginTable("##PropertiesTable", 2); ImGui::BeginTable("##PropertiesTable", 2,
ImGuiTableFlags_RowBg);
const float width = ImGui::GetContentRegionAvail().x; const float width = ImGui::GetContentRegionAvail().x;
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width * w1); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width * w1);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width * w2); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width * w2);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f, 0.0f));
}
ImGui_DataPtrTable::~ImGui_DataPtrTable() {
ImGui::PopStyleVar();
ImGui::EndTable();
} }
ImGui_DataPtrTable::~ImGui_DataPtrTable() { ImGui::EndTable(); }
void ImGui_DataPtrTable::SetPrefix(const char* prefix) { void ImGui_DataPtrTable::SetPrefix(const char* prefix) {
prefix_ = strlen(prefix); prefix_ = strlen(prefix);
@@ -206,44 +211,50 @@ void ImGui_DataPtrTable::MakeLabel(const char* label, int index, int total) {
} }
void ImGui_SpecElementTable::operator()(const char* label, mjtByte& val, void ImGui_SpecElementTable::operator()(const char* label, mjtByte& val,
const mjtByte& ref,
const char* tooltip) { const char* tooltip) {
MakeLabel(label); Label(label, tooltip);
ImGui::SetItemTooltip("%s", tooltip); Input(val, ref);
ImGui::Text("%s", val ? "true" : "false");
} }
void ImGui_SpecElementTable::operator()(const char* label, mjtSize& val, void ImGui_SpecElementTable::operator()(const char* label, mjtSize& val,
const mjtSize& ref,
const char* tooltip) { const char* tooltip) {
Scalar(label, val, tooltip); Label(label, tooltip);
Input(val, ref);
} }
void ImGui_SpecElementTable::operator()(const char* label, int& val, void ImGui_SpecElementTable::operator()(const char* label, int& val,
const char* tooltip) { const int& ref, const char* tooltip) {
Scalar(label, val, tooltip); Label(label, tooltip);
Input(val, ref);
} }
void ImGui_SpecElementTable::operator()(const char* label, float& val, void ImGui_SpecElementTable::operator()(const char* label, float& val,
const char* tooltip) { const float& ref, const char* tooltip) {
Scalar(label, val, tooltip); Label(label, tooltip);
Input(val, ref);
} }
void ImGui_SpecElementTable::operator()(const char* label, double& val, void ImGui_SpecElementTable::operator()(const char* label, double& val,
const double& ref,
const char* tooltip) { const char* tooltip) {
Scalar(label, val, tooltip); Label(label, tooltip);
Input(val, ref);
} }
void ImGui_SpecElementTable::operator()(const char* label, std::string* ptr, void ImGui_SpecElementTable::operator()(const char* label, std::string* ptr,
const std::string* ref,
const char* tooltip) { const char* tooltip) {
MakeLabel(label); Label(label, tooltip);
ImGui::SetItemTooltip("%s", tooltip); Input(*ptr, *ref);
ImGui::Text("%s", ptr ? ptr->c_str() : "");
} }
void ImGui_SpecElementTable::operator()(const char* label, void ImGui_SpecElementTable::operator()(const char* label,
std::vector<int>* ptr, std::vector<int>* ptr,
const std::vector<int>* ref,
const char* tooltip) { const char* tooltip) {
MakeLabel(label); Label(label, tooltip);
ImGui::SetItemTooltip("%s", tooltip);
if (ptr == nullptr || ptr->empty()) { if (ptr == nullptr || ptr->empty()) {
ImGui::Text("[empty]"); ImGui::Text("[empty]");
} else { } else {
@@ -253,9 +264,9 @@ void ImGui_SpecElementTable::operator()(const char* label,
void ImGui_SpecElementTable::operator()(const char* label, void ImGui_SpecElementTable::operator()(const char* label,
std::vector<double>* ptr, std::vector<double>* ptr,
const std::vector<double>* ref,
const char* tooltip) { const char* tooltip) {
MakeLabel(label); Label(label, tooltip);
ImGui::SetItemTooltip("%s", tooltip);
if (ptr == nullptr || ptr->empty()) { if (ptr == nullptr || ptr->empty()) {
ImGui::Text("[empty]"); ImGui::Text("[empty]");
} else { } else {
@@ -265,41 +276,49 @@ void ImGui_SpecElementTable::operator()(const char* label,
void ImGui_SpecElementTable::operator()(const char* label, void ImGui_SpecElementTable::operator()(const char* label,
std::vector<std::string>* ptr, std::vector<std::string>* ptr,
const std::vector<std::string>* ref,
const char* tooltip) { const char* tooltip) {
for (int i = 0; i < ptr->size(); ++i) { for (int i = 0; i < ptr->size(); ++i) {
MakeLabel(label, i, ptr->size()); Label(label, tooltip, i, ptr->size());
ImGui::SetItemTooltip("%s", tooltip); Input(ptr->at(i));
ImGui::Text("%s", ptr->at(i).c_str());
} }
} }
void ImGui_SpecElementTable::operator()(const char* name, double (&quat)[4], void ImGui_SpecElementTable::operator()(const char* name, const char* alt_name,
const char* alt, double (&quat)[4],
mjsOrientation& orientation, const double (&ref_quat)[4],
mjsOrientation& alt,
const mjsOrientation& ref_alt,
const char* tooltip) { const char* tooltip) {
auto alt_name = [&](const char* label) { auto aname = [&](const char* label) {
return std::string(alt) + "." + label; return std::string(alt_name) + "." + label;
}; };
switch (orientation.type) { switch (alt.type) {
case mjORIENTATION_QUAT: case mjORIENTATION_QUAT:
(*this)(name, quat, tooltip); (*this)(name, quat, ref_quat, tooltip);
break; break;
case mjORIENTATION_AXISANGLE: case mjORIENTATION_AXISANGLE:
(*this)(alt_name("axisangle").c_str(), orientation.axisangle, tooltip); (*this)(aname("axisangle").c_str(), alt.axisangle, ref_alt.axisangle, tooltip);
break; break;
case mjORIENTATION_XYAXES: case mjORIENTATION_XYAXES:
(*this)(alt_name("xyaxes").c_str(), orientation.xyaxes, tooltip); (*this)(aname("xyaxes").c_str(), alt.xyaxes, ref_alt.xyaxes, tooltip);
break; break;
case mjORIENTATION_ZAXIS: case mjORIENTATION_ZAXIS:
(*this)(alt_name("zaxis").c_str(), orientation.zaxis, tooltip); (*this)(aname("zaxis").c_str(), alt.zaxis, ref_alt.zaxis, tooltip);
break; break;
case mjORIENTATION_EULER: case mjORIENTATION_EULER:
(*this)(alt_name("euler").c_str(), orientation.euler, tooltip); (*this)(aname("euler").c_str(), alt.euler, ref_alt.euler, tooltip);
break; break;
} }
} }
void ImGui_SpecElementTable::Label(const char* label, const char* tooltip,
int i, int n) {
MakeLabel(label, i, n);
ImGui::SetItemTooltip("%s", tooltip);
}
bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max) { bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max) {
float f = *value; float f = *value;
const bool res = ImGui::SliderFloat(name, &f, min, max); const bool res = ImGui::SliderFloat(name, &f, min, max);
@@ -309,6 +328,53 @@ bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max) {
return res; return res;
} }
bool ImGui_BeginHSplit(const char* id, float* height, bool* open) {
const ImVec2 region = ImGui::GetContentRegionAvail();
if (*height < 0) {
*height = region.y / 2;
}
// If the split is closed, use the full region height.
const float h = *open ? *height : region.y;
return ImGui::BeginChild(id, ImVec2(0, h), 0);
}
bool ImGui_HSplit(const char* id, float* height, bool* open) {
// End the parent child.
ImGui::EndChild();
const ImVec2 region = ImGui::GetContentRegionAvail();
if (*open) {
// Just to make the splitter bar centered.
ImGui::InvisibleButton("##empty", ImVec2(10.0f, 8.0f));
// The splitter bar; drag to change the height.
ImGui::SameLine();
ImGui::Button(id, ImVec2(region.x - 30.0f, 8.0f));
if (ImGui::IsItemActive()) {
*height += ImGui::GetIO().MouseDelta.y;
if (*height < 0) *height = 0;
}
// The collapse button.
ImGui::SameLine();
if (ImGui::Button("x", ImVec2(10.0f, 8.0f))) {
*open = false;
}
}
if (*open) {
return ImGui::BeginChild(id, ImVec2(region.x, 0), 1);
} else {
return false;
}
}
void ImGui_EndHSplit(bool open) {
if (open) {
ImGui::EndChild();
}
}
void MaybeSaveToClipboard(const std::string& contents) { void MaybeSaveToClipboard(const std::string& contents) {
if (ImGui::GetIO().SetClipboardTextFn) { if (ImGui::GetIO().SetClipboardTextFn) {
ImGui::GetIO().SetClipboardTextFn(nullptr, contents.c_str()); ImGui::GetIO().SetClipboardTextFn(nullptr, contents.c_str());
+176 -54
View File
@@ -26,6 +26,7 @@
#include <imgui.h> #include <imgui.h>
#include <imgui_internal.h> #include <imgui_internal.h>
#include "third_party/dear_imgui/misc/cpp/imgui_stdlib.h"
#include <mujoco/mujoco.h> #include <mujoco/mujoco.h>
#include "experimental/platform/enum_utils.h" #include "experimental/platform/enum_utils.h"
@@ -38,18 +39,20 @@ static constexpr const char ICON_FA_CAMERA[] = "\xEF\x80\xBD";
static constexpr const char ICON_FA_CARET_LEFT[] = "\xEF\x83\x99"; static constexpr const char ICON_FA_CARET_LEFT[] = "\xEF\x83\x99";
static constexpr const char ICON_FA_CARET_RIGHT[] = "\xEF\x83\x9A"; static constexpr const char ICON_FA_CARET_RIGHT[] = "\xEF\x83\x9A";
static constexpr const char ICON_FA_CHECK_SQUARE_O[] = "\xEF\x81\x9D"; static constexpr const char ICON_FA_CHECK_SQUARE_O[] = "\xEF\x81\x9D";
static constexpr const char ICON_FA_CIRCLE[] = "\xEF\x84\x91";
static constexpr const char ICON_FA_CIRCLE_O[] = "\xEF\x84\x8C"; static constexpr const char ICON_FA_CIRCLE_O[] = "\xEF\x84\x8C";
static constexpr const char ICON_FA_CIRCLE[] = "\xEF\x84\x91";
static constexpr const char ICON_FA_COMMENT[] = "\xEF\x83\xA5"; static constexpr const char ICON_FA_COMMENT[] = "\xEF\x83\xA5";
static constexpr const char ICON_FA_COPY[] = "\xEF\x83\x85"; static constexpr const char ICON_FA_COPY[] = "\xEF\x83\x85";
static constexpr const char ICON_FA_DIAMOND[] = "\xEF\x88\x99"; static constexpr const char ICON_FA_DIAMOND[] = "\xEF\x88\x99";
static constexpr const char ICON_FA_EJECT[] = "\xEF\x81\x92"; static constexpr const char ICON_FA_EJECT[] = "\xEF\x81\x92";
static constexpr const char ICON_FA_FAST_FORWARD[] = "\xEF\x81\x90"; static constexpr const char ICON_FA_FAST_FORWARD[] = "\xEF\x81\x90";
static constexpr const char ICON_FA_MOON[] = "\xEF\x86\x86";
static constexpr const char ICON_FA_MAGIC[] = "\xEF\x83\x90"; static constexpr const char ICON_FA_MAGIC[] = "\xEF\x83\x90";
static constexpr const char ICON_FA_MOON[] = "\xEF\x86\x86";
static constexpr const char ICON_FA_PAUSE[] = "\xEF\x81\x8C"; static constexpr const char ICON_FA_PAUSE[] = "\xEF\x81\x8C";
static constexpr const char ICON_FA_PLAY[] = "\xEF\x81\x8B"; static constexpr const char ICON_FA_PLAY[] = "\xEF\x81\x8B";
static constexpr const char ICON_FA_PLUS[] = "\xEF\x81\xA7";
static constexpr const char ICON_FA_REFRESH[] = "\xEF\x80\xA1"; static constexpr const char ICON_FA_REFRESH[] = "\xEF\x80\xA1";
static constexpr const char ICON_FA_REPEAT[] = "\xEF\x80\x9E";
static constexpr const char ICON_FA_SQUARE_O[] = "\xEF\x87\x9B"; static constexpr const char ICON_FA_SQUARE_O[] = "\xEF\x87\x9B";
static constexpr const char ICON_FA_SUN[] = "\xEF\x86\x85"; static constexpr const char ICON_FA_SUN[] = "\xEF\x86\x85";
static constexpr const char ICON_FA_TACHOMETER[] = "\xEF\x83\xA4"; static constexpr const char ICON_FA_TACHOMETER[] = "\xEF\x83\xA4";
@@ -62,6 +65,9 @@ using KeyValues = std::unordered_map<std::string, std::string>;
template <typename T> template <typename T>
struct dependent_false : std::false_type {}; struct dependent_false : std::false_type {};
template <typename T>
bool ImGui_Checkbox(const char* name, T& value);
// Appends key/value pairs to an Ini file. // Appends key/value pairs to an Ini file.
void AppendIniSection(std::string& ini, const std::string& section, void AppendIniSection(std::string& ini, const std::string& section,
const KeyValues& key_values); const KeyValues& key_values);
@@ -193,93 +199,209 @@ class ImGui_DataPtrTable {
// Helper for displaying mjSpec elements in an ImGui table. // Helper for displaying mjSpec elements in an ImGui table.
class ImGui_SpecElementTable : public ImGui_DataPtrTable { class ImGui_SpecElementTable : public ImGui_DataPtrTable {
public: public:
explicit ImGui_SpecElementTable(bool read_only = true)
: read_only_(read_only) {
}
// Scalar values used by mjSpec elements. // Scalar values used by mjSpec elements.
void operator()(const char* label, mjtByte& val, const char* tooltip); void operator()(const char* label, mjtByte& val, const mjtByte& ref,
void operator()(const char* label, mjtSize& val, const char* tooltip); const char* tooltip);
void operator()(const char* label, int& val, const char* tooltip); void operator()(const char* label, mjtSize& val, const mjtSize& ref,
void operator()(const char* label, float& val, const char* tooltip); const char* tooltip);
void operator()(const char* label, double& val, const char* tooltip); void operator()(const char* label, int& val, const int& ref,
const char* tooltip);
void operator()(const char* label, float& val, const float& ref,
const char* tooltip);
void operator()(const char* label, double& val, const double& ref,
const char* tooltip);
// C++ container values used by mjSpec elements. // C++ container values used by mjSpec elements.
void operator()(const char* label, std::string* ptr, const char* tooltip); void operator()(const char* label, std::string* ptr, const std::string* ref,
const char* tooltip);
void operator()(const char* label, std::vector<int>* ptr, void operator()(const char* label, std::vector<int>* ptr,
const char* tooltip); const std::vector<int>* ref, const char* tooltip);
void operator()(const char* label, std::vector<double>* ptr, void operator()(const char* label, std::vector<double>* ptr,
const char* tooltip); const std::vector<double>* ref, const char* tooltip);
void operator()(const char* label, std::vector<std::string>* ptr, void operator()(const char* label, std::vector<std::string>* ptr,
const char* tooltip); const std::vector<std::string>* ref, const char* tooltip);
// C-style array values used by mjSpec elements. // C-style array values used by mjSpec elements.
template <std::size_t N> template <std::size_t N>
void operator()(const char* label, char (&val)[N], const char* tooltip) { void operator()(const char* label, int (&val)[N], const int (&ref)[N],
MakeLabel(label); const char* tooltip) {
ImGui::SetItemTooltip("%s", tooltip);
ImGui::Text("%s", std::string(val, N).c_str());
}
template <std::size_t N>
void operator()(const char* label, mjtByte (&val)[N], const char* tooltip) {
for (int i = 0; i < N; ++i) { for (int i = 0; i < N; ++i) {
MakeLabel(label, i, N); Label(label, tooltip, i, N);
ImGui::SetItemTooltip("%s", tooltip); Input(val[i], ref[i]);
ImGui::Text("%s", val[i] ? "true" : "false");
} }
} }
template <std::size_t N> template <std::size_t N>
void operator()(const char* label, int (&val)[N], const char* tooltip) { void operator()(const char* label, float (&val)[N], const float (&ref)[N],
Vector(label, val, N, tooltip); const char* tooltip) {
for (int i = 0; i < N; ++i) {
Label(label, tooltip, i, N);
Input(val[i], ref[i]);
}
} }
template <std::size_t N> template <std::size_t N>
void operator()(const char* label, float (&val)[N], const char* tooltip) { void operator()(const char* label, double (&val)[N], const double (&ref)[N],
Vector(label, val, N, tooltip); const char* tooltip) {
for (int i = 0; i < N; ++i) {
Label(label, tooltip, i, N);
Input(val[i], ref[i]);
} }
}
// Special handling for fixed-length character arrays.
template <std::size_t N> template <std::size_t N>
void operator()(const char* label, double (&val)[N], const char* tooltip) { void operator()(const char* label, char (&val)[N], const char (&ref)[N],
Vector(label, val, N, tooltip); const char* tooltip) {
Label(label, tooltip);
char buf[N + 1];
strncpy(buf, val, N);
buf[N] = 0;
if (read_only_) {
ImGui::Text("%s", buf);
} else {
ImGui::PushID(&val);
if (ImGui::InputText("##", buf, N + 1)) {
strncpy(val, buf, N);
modified_ = true;
}
ImGui::PopID();
}
} }
// Special handling for treating enum values as integers. // Special handling for treating enum values as integers.
template <typename T, typename U=std::enable_if_t<std::is_enum_v<T>, T>> template <typename T, typename U=std::enable_if_t<std::is_enum_v<T>, T>>
void operator()(const char* label, T& val, const char* tooltip) { void operator()(const char* label, T& val, const T& ref, const char* tooltip) {
auto v = enum_utils::enum_to_string(val); Label(label, tooltip);
MakeLabel(label); Input(val, ref);
ImGui::SetItemTooltip("%s", tooltip);
ImGui::Text("%s", v.data());
} }
// Special handling for quaternion/orientation pairs. // Special handling for quaternion/orientation pairs.
void operator()(const char* name, double (&quat)[4], const char* alt, void operator()(const char* name, const char* alt_name, double (&quat)[4],
mjsOrientation& orientation, const char* tooltip); const double (&ref_quat)[4], mjsOrientation& alt,
const mjsOrientation& ref_alt, const char* tooltip);
// Returns true if any value in the table was modified.
bool WasModified() const { return modified_; }
private: private:
void Label(const char* label, const char* tooltip, int i = 0, int n = 1);
template <typename T> template <typename T>
void Scalar(const char* label, T& val, const char* tooltip) { void Input(T& val, const T& ref = T()) {
MakeLabel(label); ScopedStyle style;
ImGui::SetItemTooltip("%s", tooltip); if (!read_only_ && val != ref) {
if constexpr (std::is_enum_v<T>) { constexpr ImVec4 kModifiedColor = ImVec4(1.0f, 0.6f, 0.2f, 1.0f);
ImGui::Text("%d", (int)val); style.Color(ImGuiCol_Text, kModifiedColor);
} else if constexpr (std::is_integral_v<T>) { }
ImGui::Text("%d", (int)val);
} else if constexpr (std::is_floating_point_v<T>) { ImGui::PushID(&val);
ImGui::Text("%f", (float)val); ImGui::SetNextItemWidth(-1.0f);
if constexpr (std::is_same_v<T, mjtByte>) {
if (read_only_) {
ImGui::Text("%s", val ? "true" : "false");
} else {
const int flags = ImGuiComboFlags_NoArrowButton;
if (ImGui::BeginCombo("##", val ? "true" : "false", flags)) {
if (ImGui::Selectable("true", val != 0)) {
val = 1;
modified_ = true;
}
if (ImGui::Selectable("false", val == 0)) {
val = 0;
modified_ = true;
}
ImGui::EndCombo();
}
}
} else if constexpr (std::is_enum_v<T>) {
const std::string preview(enum_utils::enum_to_string(val));
if (read_only_) {
ImGui::Text("%s", preview.c_str());
} else {
const int flags = ImGuiComboFlags_NoArrowButton;
if (ImGui::BeginCombo("##", preview.c_str(), flags)) {
for (const auto& [v, n] : enum_utils::entries_v<T>) {
if (ImGui::Selectable(std::string(n).c_str(), val == v)) {
val = v;
modified_ = true;
}
}
ImGui::EndCombo();
}
}
} else if constexpr (std::is_same_v<T, int>) {
if (read_only_) {
ImGui::Text("%d", val);
} else {
if (ImGui::InputInt("##", &val)) {
modified_ = true;
}
}
} else if constexpr (std::is_same_v<T, float>) {
if (read_only_) {
ImGui::Text("%f", val);
} else {
if (ImGui::InputFloat("##", &val)) {
modified_ = true;
}
}
} else if constexpr (std::is_same_v<T, double>) {
if (read_only_) {
ImGui::Text("%f", val);
} else {
if (ImGui::InputDouble("##", &val)) {
modified_ = true;
}
}
} else if constexpr (std::is_same_v<T, int64_t>) {
if (read_only_) {
ImGui::Text("%lld", val);
} else {
if (ImGui::InputScalarN("##", ImGuiDataType_S64, &val, 1)) {
modified_ = true;
}
}
} else if constexpr (std::is_same_v<T, std::string>) {
if (read_only_) {
ImGui::Text("%s", val.c_str());
} else {
if (ImGui::InputText("##", &val)) {
modified_ = true;
}
} }
} }
template <typename T> if (!read_only_) {
void Vector(const char* label, T* ptr, int n, const char* tooltip) { if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
for (int i = 0; i < n; ++i) { val = ref;
MakeLabel(label, i, n); modified_ = true;
ImGui::SetItemTooltip("%s", tooltip);
if constexpr (std::is_enum_v<T>) {
ImGui::Text("%d", (int)ptr[i]);
} else if constexpr (std::is_integral_v<T>) {
ImGui::Text("%d", (int)ptr[i]);
} else if constexpr (std::is_floating_point_v<T>) {
ImGui::Text("%f", (float)ptr[i]);
} }
} }
ImGui::PopID();
} }
bool modified_ = false;
bool read_only_ = false;
}; };
// ImGui horizontal splitter. `height` is the height of the upper pane.
// `open` is whether the bottom pane is open. Usage is:
//
// if (ImGui_BeginHSplit("top", &height, &open)) {
// // Contents of the upper pane.
// if (ImGui_HSplit("bottom", &height, &open)) {
// // Contents of the bottom pane.
// }
// ImGui_EndHSplit(open);
// }
bool ImGui_BeginHSplit(const char* id, float* height, bool* open);
bool ImGui_HSplit(const char* id, float* height, bool* open);
void ImGui_EndHSplit(bool open);
// ImGui Slider that supports both float and double types. // ImGui Slider that supports both float and double types.
bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max); bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max);
+213 -63
View File
@@ -67,6 +67,7 @@ static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) {
perturb.active = 0; perturb.active = 0;
} }
} }
// TODO: update selected element!
} }
static constexpr const char* ICON_PLAY = platform::ICON_FA_PLAY; static constexpr const char* ICON_PLAY = platform::ICON_FA_PLAY;
@@ -86,7 +87,9 @@ static constexpr const char* ICON_PREV_FRAME = platform::ICON_FA_CARET_LEFT;
static constexpr const char* ICON_NEXT_FRAME = platform::ICON_FA_CARET_RIGHT; static constexpr const char* ICON_NEXT_FRAME = platform::ICON_FA_CARET_RIGHT;
static constexpr const char* ICON_CURR_FRAME = platform::ICON_FA_FAST_FORWARD; static constexpr const char* ICON_CURR_FRAME = platform::ICON_FA_FAST_FORWARD;
static constexpr const char* ICON_SPEED = platform::ICON_FA_TACHOMETER; static constexpr const char* ICON_SPEED = platform::ICON_FA_TACHOMETER;
static constexpr const char* ICON_DELETE = platform::ICON_FA_TRASH_CAN; static constexpr const char* ICON_RELOAD_SPEC = platform::ICON_FA_REFRESH;
static constexpr const char* ICON_UNDO_SPEC = platform::ICON_FA_UNDO;
static constexpr const char* ICON_REDO_SPEC = platform::ICON_FA_REPEAT;
// UI labels for mjtLabel. // UI labels for mjtLabel.
static constexpr const char* kLabelNames[] = { static constexpr const char* kLabelNames[] = {
@@ -201,6 +204,9 @@ void App::OnModelLoaded(std::string filename, ModelKind model_kind) {
const int state_size = mj_stateSize(model, mjSTATE_INTEGRATION); const int state_size = mj_stateSize(model, mjSTATE_INTEGRATION);
history_.Init(state_size); history_.Init(state_size);
// Create a copy of the spec for editing.
CopyLoadedSpecForEditing();
if (!preserve_camera_on_load_) { if (!preserve_camera_on_load_) {
const int model_cam = model->vis.global.cameraid; const int model_cam = model->vis.global.cameraid;
if (model_cam >= 0 && model_cam < model->ncam) { if (model_cam >= 0 && model_cam < model->ncam) {
@@ -422,44 +428,54 @@ void App::ProcessPendingLoads() {
}); });
} }
void App::SpecSelectElement(mjsElement* element) { void App::CopyLoadedSpecForEditing() {
tmp_.element = element; if (scratch_spec_ != nullptr) {
if (tmp_.element == nullptr) { mj_deleteSpec(scratch_spec_);
tmp_.element_id = -1; scratch_spec_ = nullptr;
} 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) { scratch_spec_modified_ = false;
if (element == nullptr) { tmp_.curr_edit_element = nullptr;
return;
if (has_spec()) {
scratch_spec_ = mj_copySpec(spec());
auto add_ref_elements = [&](mjtObj type) {
mjsElement* elem = mjs_firstElement(spec(), type);
mjsElement* scratch = mjs_firstElement(scratch_spec_, type);
while (elem != nullptr && scratch != nullptr) {
scratch_to_spec_[scratch] = elem;
spec_to_scratch_[elem] = scratch;
elem = mjs_nextElement(spec(), elem);
scratch = mjs_nextElement(scratch_spec_, scratch);
} }
// 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();
}; };
add_ref_elements(mjOBJ_BODY);
add_ref_elements(mjOBJ_XBODY);
add_ref_elements(mjOBJ_JOINT);
add_ref_elements(mjOBJ_DOF);
add_ref_elements(mjOBJ_GEOM);
add_ref_elements(mjOBJ_SITE);
add_ref_elements(mjOBJ_CAMERA);
add_ref_elements(mjOBJ_LIGHT);
add_ref_elements(mjOBJ_FLEX);
add_ref_elements(mjOBJ_MESH);
add_ref_elements(mjOBJ_SKIN);
add_ref_elements(mjOBJ_HFIELD);
add_ref_elements(mjOBJ_TEXTURE);
add_ref_elements(mjOBJ_MATERIAL);
add_ref_elements(mjOBJ_PAIR);
add_ref_elements(mjOBJ_EXCLUDE);
add_ref_elements(mjOBJ_EQUALITY);
add_ref_elements(mjOBJ_TENDON);
add_ref_elements(mjOBJ_ACTUATOR);
add_ref_elements(mjOBJ_SENSOR);
add_ref_elements(mjOBJ_NUMERIC);
add_ref_elements(mjOBJ_TEXT);
add_ref_elements(mjOBJ_TUPLE);
add_ref_elements(mjOBJ_KEY);
add_ref_elements(mjOBJ_PLUGIN);
}
} }
void App::HandleWindowEvents() { void App::HandleWindowEvents() {
@@ -555,14 +571,12 @@ void App::HandleMouseEvents() {
perturb_.skinselect = picked.skin; perturb_.skinselect = picked.skin;
// Select the corresponding element in the spec. // Select the corresponding element in the spec.
tmp_.element = nullptr; tmp_.curr_element = nullptr;
tmp_.element_id = -1;
if (has_spec()) { if (has_spec()) {
mjsElement* element = mjs_firstElement(spec(), mjOBJ_BODY); mjsElement* element = mjs_firstElement(spec(), mjOBJ_BODY);
while (element) { while (element) {
if (mjs_getId(element) == picked.body) { if (mjs_getId(element) == picked.body) {
tmp_.element = element; tmp_.curr_element = element;
tmp_.element_id = picked.body;
break; break;
} }
element = mjs_nextElement(spec(), element); element = mjs_nextElement(spec(), element);
@@ -665,7 +679,7 @@ void App::HandleKeyboardEvents() {
} else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) {
ResetPhysics(); ResetPhysics();
} else if (ImGui_IsChordJustPressed(ImGuiKey_Delete)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_Delete)) {
SpecDeleteElement(tmp_.element); // TODO: SpecDeleteElement(tmp_.curr_element);
} else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) {
SelectParentPerturb(model(), perturb_); SelectParentPerturb(model(), perturb_);
} else if (ImGui_IsChordJustPressed(ImGuiKey_F1)) { } else if (ImGui_IsChordJustPressed(ImGuiKey_F1)) {
@@ -917,20 +931,16 @@ void App::BuildGui() {
} }
ImGui::End(); ImGui::End();
bool explorer_is_open = false;
if (ImGui::Begin("Explorer", &tmp_.inspector_panel)) { if (ImGui::Begin("Explorer", &tmp_.inspector_panel)) {
explorer_is_open = true;
SpecExplorerGui(); SpecExplorerGui();
} }
ImGui::End(); ImGui::End();
if (explorer_is_open && tmp_.element != nullptr) { if (ImGui::Begin("Editor", &tmp_.inspector_panel)) {
if (ImGui::Begin("Properties")) { SpecEditorGui();
SpecPropertiesGui();
} }
ImGui::End(); ImGui::End();
} }
}
if (tmp_.chart_performance) { if (tmp_.chart_performance) {
ImGui::SetNextWindowPos(chart_pos, ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(chart_pos, ImGuiCond_FirstUseEver);
@@ -1165,56 +1175,196 @@ void App::SpecExplorerGui() {
return; return;
} }
auto on_delete = [this](mjsElement* element) { SpecDeleteElement(element); }; // Initialize the split height.
const ImVec2 region = ImGui::GetContentRegionAvail();
if (tmp_.explorer_split < 0) {
tmp_.explorer_split = region.y * 0.7f;
}
tmp_.explorer_split = std::clamp(tmp_.explorer_split, 20.f, region.y - 40.f);
mjsElement* element = tmp_.element; mjsElement* element = tmp_.curr_element;
platform::SpecExplorerGui(&element, spec(), on_delete); bool open = element != nullptr;
if (element != tmp_.element) { if (platform::ImGui_BeginHSplit("SpecExplorerTree",
SpecSelectElement(element); &tmp_.explorer_split, &open)) {
platform::SpecTreeGui(&element, spec(), SpecEditMode::kPlay);
if (element != tmp_.curr_element) {
tmp_.curr_element = element;
// A different element was selected, so select it for perturb.
if (element) {
open = true;
const int element_id = mjs_getId(element);
if (element->elemtype == mjOBJ_BODY && perturb_.select != element_id) {
mjv_defaultPerturb(&perturb_);
perturb_.select = element_id;
}
} }
} }
void App::SpecPropertiesGui() { if (platform::ImGui_HSplit("SpecExplorerProperties",
&tmp_.explorer_split, &open)) {
platform::ScopedStyle style; platform::ScopedStyle style;
ImGui::Text("%s", mju_type2Str(tmp_.element->elemtype)); ImGui::Text("%s", mju_type2Str(tmp_.curr_element->elemtype));
ImGui::SameLine(); ImGui::SameLine();
ImGui::Text("(%d)", tmp_.element_id); ImGui::Text("(%d)", mjs_getId(tmp_.curr_element));
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 100.0f);
ImGui::SameLine(120);
if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) { if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) {
style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive); style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive);
} }
if (ImGui::SmallButton("S")) { style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
if (ImGui::Button("S", ImVec2(24.0f, 20.0f))) {
tmp_.spec_prop_mode = SpecPropertiesMode::kSpec; tmp_.spec_prop_mode = SpecPropertiesMode::kSpec;
} }
ImGui::SetItemTooltip("Spec"); ImGui::SetItemTooltip("Spec");
style.Reset(); style.Reset();
ImGui::SameLine(); ImGui::SameLine();
if (tmp_.spec_prop_mode == SpecPropertiesMode::kModel) { if (tmp_.spec_prop_mode == SpecPropertiesMode::kModel) {
style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive); style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive);
} }
if (ImGui::SmallButton("M")) { style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
if (ImGui::Button("M", ImVec2(24.0f, 20.0f))) {
tmp_.spec_prop_mode = SpecPropertiesMode::kModel; tmp_.spec_prop_mode = SpecPropertiesMode::kModel;
} }
ImGui::SetItemTooltip("Model"); ImGui::SetItemTooltip("Model");
style.Reset(); style.Reset();
ImGui::SameLine(); ImGui::SameLine();
if (tmp_.spec_prop_mode == SpecPropertiesMode::kData) { if (tmp_.spec_prop_mode == SpecPropertiesMode::kData) {
style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive); style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive);
} }
if (ImGui::SmallButton("D")) { style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
if (ImGui::Button("D", ImVec2(24.0f, 20.0f))) {
tmp_.spec_prop_mode = SpecPropertiesMode::kData; tmp_.spec_prop_mode = SpecPropertiesMode::kData;
} }
ImGui::SetItemTooltip("Data"); ImGui::SetItemTooltip("Data");
style.Reset(); style.Reset();
ImGui::Separator(); ImGui::Separator();
if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) { if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) {
platform::ElementSpecGui(spec(), tmp_.element); platform::ElementSpecGui(element, element, SpecEditMode::kPlay);
} else if (tmp_.spec_prop_mode == SpecPropertiesMode::kModel) { } else if (tmp_.spec_prop_mode == SpecPropertiesMode::kModel) {
platform::ElementModelGui(model(), tmp_.element); platform::ElementModelGui(model(), tmp_.curr_element);
} else { } else {
platform::ElementDataGui(data(), tmp_.element); platform::ElementDataGui(data(), tmp_.curr_element);
}
}
if (!open) {
tmp_.curr_element = nullptr;
}
}
platform::ImGui_EndHSplit(open);
}
void App::SpecEditorGui() {
if (ImGui::BeginChild("SpecEditor", ImVec2(-1, 36))) {
if (ImGui::BeginTable("##SpecEditorHeader", 3)) {
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 70.0f);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 30.0f);
ImGui::TableNextColumn();
if (ImGui::Button(ICON_RELOAD_SPEC)) {
CopyLoadedSpecForEditing();
}
ImGui::BeginDisabled(true);
ImGui::SameLine();
if (ImGui::Button(ICON_UNDO_SPEC)) {
// TODO...
}
ImGui::SameLine();
if (ImGui::Button(ICON_REDO_SPEC)) {
// TODO...
}
ImGui::EndDisabled();
ImGui::TableNextColumn();
ImGui::PushStyleColor(ImGuiCol_Button, ImColor(40, 180, 40, 255).Value);
if (ImGui::Button("Compile and Reload", ImVec2(-1, 0))) {
spec_op_ = [this]() {
model_holder_ = platform::ModelHolder::FromSpec(scratch_spec_);
scratch_spec_ = nullptr;
OnModelLoaded(model_name_, model_kind_);
};
}
ImGui::PopStyleColor();
ImGui::TableNextColumn();
if (ImGui::Button(" + ")) {
ImGui::OpenPopupOnItemClick("SpecAddElement", 0);
}
if (ImGui::BeginPopupContextItem("SpecAddElement")) {
auto option = [&](const char* label, auto fn) {
if (ImGui::Selectable(label)) {
tmp_.curr_edit_element = fn()->element;
mjs_setName(tmp_.curr_edit_element,
platform::ElementName(tmp_.curr_edit_element).c_str());
scratch_spec_modified_ = true;
}
};
option("Actuator", [&]() { return mjs_addActuator(scratch_spec_, 0); });
option("Equality", [&]() { return mjs_addEquality(scratch_spec_, 0); });
option("Exclude", [&]() { return mjs_addExclude(scratch_spec_); });
option("Flex", [&]() { return mjs_addFlex(scratch_spec_); });
option("Height Field", [&]() { return mjs_addHField(scratch_spec_); });
option("Key", [&]() { return mjs_addKey(scratch_spec_); });
option("Material", [&]() { return mjs_addMaterial(scratch_spec_, 0); });
option("Mesh", [&]() { return mjs_addMesh(scratch_spec_, 0); });
option("Numeric", [&]() { return mjs_addNumeric(scratch_spec_); });
option("Pair", [&]() { return mjs_addPair(scratch_spec_, 0); });
option("Sensor", [&]() { return mjs_addSensor(scratch_spec_); });
option("Skin", [&]() { return mjs_addSkin(scratch_spec_); });
option("Tendon", [&]() { return mjs_addTendon(scratch_spec_, 0); });
option("Text", [&]() { return mjs_addText(scratch_spec_); });
option("Texture", [&]() { return mjs_addTexture(scratch_spec_); });
option("Tuple", [&]() { return mjs_addTuple(scratch_spec_); });
ImGui::EndPopup();
}
ImGui::EndTable();
}
}
ImGui::EndChild();
// Initialize the split height.
const ImVec2 region = ImGui::GetContentRegionAvail();
if (tmp_.editor_split < 0) {
tmp_.editor_split = region.y * 0.7f;
}
tmp_.editor_split = std::clamp(tmp_.editor_split, 20.0f, region.y - 40.0f);
bool open = tmp_.curr_edit_element != nullptr;
if (platform::ImGui_BeginHSplit("SpecEditorTree", &tmp_.editor_split,
&open)) {
if (platform::SpecTreeGui(&tmp_.curr_edit_element, scratch_spec_,
SpecEditMode::kEdit)) {
scratch_spec_modified_ = true;
}
open = tmp_.curr_edit_element != nullptr;
if (platform::ImGui_HSplit("SpecEditorProperties", &tmp_.editor_split,
&open)) {
ImGui::Text("%s", mju_type2Str(tmp_.curr_edit_element->elemtype));
ImGui::SameLine();
ImGui::Text("(%d)", mjs_getId(tmp_.curr_edit_element));
ImGui::Separator();
mjsElement* ref = scratch_to_spec_[tmp_.curr_edit_element];
if (platform::ElementSpecGui(tmp_.curr_edit_element, ref,
SpecEditMode::kEdit)) {
scratch_spec_modified_ = true;
}
}
platform::ImGui_EndHSplit(open);
if (!open) {
tmp_.curr_edit_element = nullptr;
}
} }
} }
@@ -1725,12 +1875,12 @@ void App::MainMenuGui() {
} }
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem(tmp_.options_panel ? "Hide Options" : "Show Left UI", if (ImGui::MenuItem(tmp_.options_panel ? "Hide Options" : "Show Options",
"Tab")) { "Tab")) {
tmp_.options_panel = !tmp_.options_panel; tmp_.options_panel = !tmp_.options_panel;
} }
if (ImGui::MenuItem( if (ImGui::MenuItem(
tmp_.inspector_panel ? "Hide Inspector" : "Show Right UI", tmp_.inspector_panel ? "Hide Inspector" : "Show Inspector",
"Shift+Tab")) { "Shift+Tab")) {
tmp_.inspector_panel = !tmp_.inspector_panel; tmp_.inspector_panel = !tmp_.inspector_panel;
} }
+21 -7
View File
@@ -27,6 +27,7 @@
#include <mujoco/mujoco.h> #include <mujoco/mujoco.h>
#include "experimental/platform/gui.h" #include "experimental/platform/gui.h"
#include "experimental/platform/gui_spec.h"
#include "experimental/platform/interaction.h" #include "experimental/platform/interaction.h"
#include "experimental/platform/model_holder.h" #include "experimental/platform/model_holder.h"
#include "experimental/platform/picture_gui.h" #include "experimental/platform/picture_gui.h"
@@ -84,6 +85,8 @@ class App {
void Render(); void Render();
private: private:
using SpecEditMode = platform::SpecEditMode;
// The kind of model that is currently loaded. // The kind of model that is currently loaded.
enum ModelKind { enum ModelKind {
kEmptyModel, kEmptyModel,
@@ -127,6 +130,8 @@ class App {
bool style_editor = false; bool style_editor = false;
bool imgui_demo = false; bool imgui_demo = false;
bool implot_demo = false; bool implot_demo = false;
float editor_split = -1;
float explorer_split = -1;
// Controls. // Controls.
bool perturb_active = false; bool perturb_active = false;
@@ -141,10 +146,10 @@ class App {
std::vector<std::string> camera_names; std::vector<std::string> camera_names;
std::vector<std::string> speed_names; std::vector<std::string> speed_names;
// Spec Properties. // Spec editing.
SpecPropertiesMode spec_prop_mode = SpecPropertiesMode::kSpec; SpecPropertiesMode spec_prop_mode = SpecPropertiesMode::kSpec;
mjsElement* element = nullptr; mjsElement* curr_element = nullptr;
int element_id = -1; mjsElement* curr_edit_element = nullptr;
// State. // State.
int state_sig = 0; int state_sig = 0;
@@ -216,13 +221,11 @@ class App {
void ModelOptionsGui(); void ModelOptionsGui();
void DataInspectorGui(); void DataInspectorGui();
void SpecExplorerGui(); void SpecExplorerGui();
void SpecPropertiesGui(); void SpecEditorGui();
void SpecSelectElement(mjsElement* element);
void SpecDeleteElement(mjsElement* element);
float GetExpectedLabelWidth(); float GetExpectedLabelWidth();
std::vector<const char*> GetCameraNames(); std::vector<const char*> GetCameraNames();
void CopyLoadedSpecForEditing();
mjSpec* spec() { return model_holder_->spec(); } mjSpec* spec() { return model_holder_->spec(); }
mjModel* model() { return model_holder_->model(); } mjModel* model() { return model_holder_->model(); }
@@ -244,6 +247,17 @@ class App {
std::unique_ptr<platform::Window> window_; std::unique_ptr<platform::Window> window_;
std::unique_ptr<platform::Renderer> renderer_; std::unique_ptr<platform::Renderer> renderer_;
std::unique_ptr<platform::ModelHolder> model_holder_; std::unique_ptr<platform::ModelHolder> model_holder_;
// Spec editing. We keep a separate copy of the loaded spec that we can edit.
// Once we're done editing, we will (re)compile the spec and update the
// active model and data.
mjSpec* scratch_spec_ = nullptr;
// Whether or not the scratch spec differs from the loaded spec.
bool scratch_spec_modified_ = false;
// We keep a mapping of the elements between the loaded spec and the scratch
// spec.
std::unordered_map<mjsElement*, mjsElement*> spec_to_scratch_;
std::unordered_map<mjsElement*, mjsElement*> scratch_to_spec_;
std::function<void()> spec_op_; std::function<void()> spec_op_;
platform::StepControl step_control_; platform::StepControl step_control_;