diff --git a/src/experimental/platform/gui.cc b/src/experimental/platform/gui.cc index 2d29eeca..5d1d20ed 100644 --- a/src/experimental/platform/gui.cc +++ b/src/experimental/platform/gui.cc @@ -246,6 +246,7 @@ ImVec4 ConfigureDockingLayout() { ImGui::DockBuilderDockWindow("Dockspace", main); ImGui::DockBuilderDockWindow("Options", options); ImGui::DockBuilderDockWindow("Explorer", inspector); + ImGui::DockBuilderDockWindow("Editor", inspector); ImGui::DockBuilderDockWindow("Inspector", inspector); ImGui::DockBuilderDockWindow("Properties", properties); ImGui::DockBuilderDockWindow("Stats", stats); diff --git a/src/experimental/platform/gui_spec.cc b/src/experimental/platform/gui_spec.cc index 31158ec8..5784b777 100644 --- a/src/experimental/platform/gui_spec.cc +++ b/src/experimental/platform/gui_spec.cc @@ -63,7 +63,7 @@ static int GetElementIndexInSpec(mjsElement* element) { // 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) { +std::string ElementName(mjsElement* element) { const mjString* name = mjs_getName(element); std::string label = *name; if (label.empty()) { @@ -77,22 +77,51 @@ static std::string ElementName(mjsElement* element) { return label; } -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 bool AddDeleteButton(mjsElement* element) { + // 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)) { + 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, - const SpecElementCallbackFn& on_delete) { + SpecEditMode mode) { constexpr ImGuiSelectableFlags flags = ImGuiSelectableFlags_AllowOverlap; const std::string name = ElementName(element); @@ -100,51 +129,63 @@ static void SelectableElement(mjsElement* element, if (ImGui::Selectable(name.c_str(), selected, flags)) { *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, - const SpecElementCallbackFn& on_delete) { + SpecEditMode mode) { mjsElement* iter = mjs_firstChild(body, type, 0); if (!iter) { - return; + return false; } + bool modified = false; 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); + mjsElement* next = mjs_nextChild(body, iter, 0); + modified |= SelectableElement(iter, element, mode); + iter = next; } ImGui::TreePop(); } + return modified; } -static void ElementListGui(const char* heading, mjtObj type, +static bool ElementListGui(const char* heading, mjtObj type, mjsElement** element, mjSpec* spec, - const SpecElementCallbackFn& on_delete) { + SpecEditMode mode) { mjsElement* iter = mjs_firstElement(spec, type); if (!iter) { - return; + return false; } + bool modified = false; 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); + mjsElement* next = mjs_nextElement(spec, iter); + modified |= SelectableElement(iter, element, mode); + iter = next; } ImGui::TreePop(); } + return modified; } -static void BodyTreeGuiRecursive(mjsElement** element, mjsBody* body, - const SpecElementCallbackFn& on_delete) { +static bool BodyTreeGuiRecursive(mjsElement** element, mjsBody* body, + SpecEditMode mode) { const std::string label = ElementName(body->element); ImGui::PushID(body); @@ -157,52 +198,61 @@ static void BodyTreeGuiRecursive(mjsElement** element, mjsBody* body, flags |= ImGuiTreeNodeFlags_Selected; } + bool modified = false; 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 (*element == body->element && mode == SpecEditMode::kEdit) { + modified |= AddBodyAddChildButton(body->element, element); + modified |= AddDeleteButton(body->element); } 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); + mjsElement* next = mjs_nextChild(body, iter, 0); + modified |= BodyTreeGuiRecursive(element, mjs_asBody(iter), mode); + iter = next; } - 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); + modified |= BodyChildrenGui("Frames", mjOBJ_FRAME, element, body, mode); + modified |= BodyChildrenGui("Sites", mjOBJ_SITE, element, body, mode); + modified |= BodyChildrenGui("Joints", mjOBJ_JOINT, element, body, mode); + modified |= BodyChildrenGui("Geoms", mjOBJ_GEOM, element, body, mode); + modified |= BodyChildrenGui("Lights", mjOBJ_LIGHT, element, body, mode); + modified |= BodyChildrenGui("Cameras", mjOBJ_CAMERA, element, body, mode); ImGui::TreePop(); } ImGui::PopID(); + return modified; } -void SpecExplorerGui(mjsElement** element, mjSpec* spec, - const SpecElementCallbackFn& on_delete) { +bool SpecTreeGui(mjsElement** element, mjSpec* spec, SpecEditMode mode) { + bool modified = false; const ImGuiTreeNodeFlags flags = 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)) { mjsElement* root = mjs_firstElement(spec, mjOBJ_BODY); if (root) { mjsBody* body = mjs_asBody(root); if (body) { - BodyTreeGuiRecursive(element, body, on_delete); + modified |= BodyTreeGuiRecursive(element, body, mode); } } ImGui::TreePop(); } auto list = [&](const char* heading, mjtObj type) { - ElementListGui(heading, type, element, spec, on_delete); + modified |= ElementListGui(heading, type, element, spec, mode); }; ImGui::PushID(spec); @@ -235,412 +285,447 @@ void SpecExplorerGui(mjsElement** element, mjSpec* spec, } ImGui::PopID(); + return modified; } -void ElementSpecGui(const mjSpec* spec, mjsElement* element) { +bool ElementSpecGui(mjsElement* element, mjsElement* ref_element, + SpecEditMode mode) { 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) { case mjOBJ_BODY: { - mjsBody* body = mjs_asBody(element); - table("childclass", body->childclass, "childclass name"); - table("pos", body->pos, "frame position"); - table("quat", body->quat, "alt", body->alt, "frame orientation"); - table("ipos", body->ipos, "inertial frame position"); - table("iquat", body->iquat, "ialt", body->ialt, "inertial frame orientation"); - table("mass", body->mass, "mass"); - table("inertia", body->inertia, "diagonal inertia (in i-frame)"); - table("fullinertia", body->fullinertia, "non-axis-aligned inertia matrix"); - table("mocap", body->mocap, "is this a mocap body"); - table("gravcomp", body->gravcomp, "gravity compensation"); - table("explicitinertial", body->explicitinertial, "whether to save the body with explicit inertial clause"); - table("sleep", body->sleep, "sleep policy"); - table("info", body->info, "message appended to compiler errors"); + mjsBody* elem = mjs_asBody(element); + mjsBody* ref = mjs_asBody(ref_element); + FIELD(childclass, "childclass name"); + FIELD(pos, "frame position"); + QFIELD(quat, alt, "frame orientation"); + FIELD(ipos, "inertial frame position"); + QFIELD(iquat, ialt, "inertial frame orientation"); + FIELD(mass, "mass"); + FIELD(inertia, "diagonal inertia (in i-frame)"); + FIELD(fullinertia, "non-axis-aligned inertia matrix"); + FIELD(mocap, "is this a mocap body"); + FIELD(gravcomp, "gravity compensation"); + FIELD(explicitinertial, "whether to save the body with explicit inertial clause"); + FIELD(sleep, "sleep policy"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_JOINT: { - mjsJoint* joint = mjs_asJoint(element); - table("pos", joint->pos, "anchor position"); - table("axis", joint->axis, "joint axis"); - table("ref", joint->ref, "value at reference configuration: qpos0"); - table("align", joint->align, "align free joint with body com (mjtAlignFree)"); - table("stiffness", joint->stiffness, "stiffness coefficient"); - table("springref", joint->springref, "spring reference value: qpos_spring"); - table("springdamper", joint->springdamper, "timeconst, dampratio"); - table("limited", joint->limited, "does joint have limits (mjtLimited)"); - table("range", joint->range, "joint limits"); - table("margin", joint->margin, "margin value for joint limit detection"); - table("solref_limit", joint->solref_limit, "solver reference: joint limits"); - table("solimp_limit", joint->solimp_limit, "solver impedance: joint limits"); - table("actfrclimited", joint->actfrclimited, "are actuator forces on joint limited (mjtLimited)"); - table("actfrcrange", joint->actfrcrange, "actuator force limits"); - table("armature", joint->armature, "armature inertia (mass for slider)"); - table("damping", joint->damping, "damping coefficient"); - table("frictionloss", joint->frictionloss, "friction loss"); - table("solref_friction", joint->solref_friction, "solver reference: dof friction"); - table("solimp_friction", joint->solimp_friction, "solver impedance: dof friction"); - table("group", joint->group, "group"); - table("actgravcomp", joint->actgravcomp, "is gravcomp force applied via actuators"); - table("info", joint->info, "message appended to compiler errors"); + mjsJoint* elem = mjs_asJoint(element); + mjsJoint* ref = mjs_asJoint(ref_element); + FIELD(pos, "anchor position"); + FIELD(axis, "joint axis"); + FIELD(ref, "value at reference configuration: qpos0"); + FIELD(align, "align free joint with body com (mjtAlignFree)"); + FIELD(stiffness, "stiffness coefficient"); + FIELD(springref, "spring reference value: qpos_spring"); + FIELD(springdamper, "timeconst, dampratio"); + FIELD(limited, "does joint have limits (mjtLimited)"); + FIELD(range, "joint limits"); + FIELD(margin, "margin value for joint limit detection"); + FIELD(solref_limit, "solver reference: joint limits"); + FIELD(solimp_limit, "solver impedance: joint limits"); + FIELD(actfrclimited, "are actuator forces on joint limited (mjtLimited)"); + FIELD(actfrcrange, "actuator force limits"); + FIELD(armature, "armature inertia (mass for slider)"); + FIELD(damping, "damping coefficient"); + FIELD(frictionloss, "friction loss"); + FIELD(solref_friction, "solver reference: dof friction"); + FIELD(solimp_friction, "solver impedance: dof friction"); + FIELD(group, "group"); + FIELD(actgravcomp, "is gravcomp force applied via actuators"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_ACTUATOR: { - mjsActuator* actuator = mjs_asActuator(element); - table("gaintype", actuator->gaintype, "gain type"); - table("gainprm", actuator->gainprm, "gain parameters"); - table("biastype", actuator->biastype, "bias type"); - table("biasprm", actuator->biasprm, "bias parameters"); - table("dyntype", actuator->dyntype, "dynamics type"); - table("dynprm", actuator->dynprm, "dynamics parameters"); - table("actdim", actuator->actdim, "number of activation variables"); - table("actearly", actuator->actearly, "apply next activations to qfrc"); - table("trntype", actuator->trntype, "transmission type"); - table("gear", actuator->gear, "length and transmitted force scaling"); - table("target", actuator->target, "name of transmission target"); - table("refsite", actuator->refsite, "reference site, for site transmission"); - table("slidersite", actuator->slidersite, "site defining cylinder, for slider-crank"); - table("cranklength", actuator->cranklength, "crank length, for slider-crank"); - table("lengthrange", actuator->lengthrange, "transmission length range"); - table("inheritrange", actuator->inheritrange, "automatic range setting for position and intvelocity"); - table("ctrllimited", actuator->ctrllimited, "are control limits defined (mjtLimited)"); - table("ctrlrange", actuator->ctrlrange, "control range"); - table("forcelimited", actuator->forcelimited, "are force limits defined (mjtLimited)"); - table("forcerange", actuator->forcerange, "force range"); - table("actlimited", actuator->actlimited, "are activation limits defined (mjtLimited)"); - table("actrange", actuator->actrange, "activation range"); - table("group", actuator->group, "group"); - table("nsample", actuator->nsample, "number of samples in history buffer"); - table("interp", actuator->interp, "interpolation order (0=ZOH, 1=linear, 2=cubic)"); - table("delay", actuator->delay, "delay time in seconds; 0: no delay"); - table("info", actuator->info, "message appended to compiler errors"); + mjsActuator* elem = mjs_asActuator(element); + mjsActuator* ref = mjs_asActuator(ref_element); + FIELD(gaintype, "gain type"); + FIELD(gainprm, "gain parameters"); + FIELD(biastype, "bias type"); + FIELD(biasprm, "bias parameters"); + FIELD(dyntype, "dynamics type"); + FIELD(dynprm, "dynamics parameters"); + FIELD(actdim, "number of activation variables"); + FIELD(actearly, "apply next activations to qfrc"); + FIELD(trntype, "transmission type"); + FIELD(gear, "length and transmitted force scaling"); + FIELD(target, "name of transmission target"); + FIELD(refsite, "reference site, for site transmission"); + FIELD(slidersite, "site defining cylinder, for slider-crank"); + FIELD(cranklength, "crank length, for slider-crank"); + FIELD(lengthrange, "transmission length range"); + FIELD(inheritrange, "automatic range setting for position and intvelocity"); + FIELD(ctrllimited, "are control limits defined (mjtLimited)"); + FIELD(ctrlrange, "control range"); + FIELD(forcelimited, "are force limits defined (mjtLimited)"); + FIELD(forcerange, "force range"); + FIELD(actlimited, "are activation limits defined (mjtLimited)"); + FIELD(actrange, "activation range"); + FIELD(group, "group"); + FIELD(nsample, "number of samples in history buffer"); + FIELD(interp, "interpolation order (0=ZOH, 1=linear, 2=cubic)"); + FIELD(delay, "delay time in seconds; 0: no delay"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_SENSOR: { - mjsSensor* sensor = mjs_asSensor(element); - table("type", sensor->type, "type of sensor"); - table("objtype", sensor->objtype, "type of sensorized object"); - table("objname", sensor->objname, "name of sensorized object"); - table("reftype", sensor->reftype, "type of referenced object"); - table("refname", sensor->refname, "name of referenced object"); - table("intprm", sensor->intprm, "integer parameters"); - table("datatype", sensor->datatype, "data type for sensor measurement"); - table("needstage", sensor->needstage, "compute stage needed to simulate sensor"); - table("dim", sensor->dim, "number of scalar outputs"); - table("cutoff", sensor->cutoff, "cutoff for real and positive datatypes"); - table("noise", sensor->noise, "noise stdev"); - table("nsample", sensor->nsample, "number of samples in history buffer"); - table("interp", sensor->interp, "interpolation order (0=ZOH, 1=linear, 2=cubic)"); - table("delay", sensor->delay, "delay time in seconds"); - table("interval", sensor->interval, "[period, time_prev] in seconds"); - table("info", sensor->info, "message appended to compiler errors"); + mjsSensor* elem = mjs_asSensor(element); + mjsSensor* ref = mjs_asSensor(ref_element); + FIELD(type, "type of sensor"); + FIELD(objtype, "type of sensorized object"); + FIELD(objname, "name of sensorized object"); + FIELD(reftype, "type of referenced object"); + FIELD(refname, "name of referenced object"); + FIELD(intprm, "integer parameters"); + FIELD(datatype, "data type for sensor measurement"); + FIELD(needstage, "compute stage needed to simulate sensor"); + FIELD(dim, "number of scalar outputs"); + FIELD(cutoff, "cutoff for real and positive datatypes"); + FIELD(noise, "noise stdev"); + FIELD(nsample, "number of samples in history buffer"); + FIELD(interp, "interpolation order (0=ZOH, 1=linear, 2=cubic)"); + FIELD(delay, "delay time in seconds"); + FIELD(interval, "[period, time_prev] in seconds"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_SITE: { - mjsSite* site = mjs_asSite(element); - table("pos", site->pos, "position"); - table("quat", site->quat, "alt", site->alt, "orientation"); - table("fromto", site->fromto, "alternative for capsule, cylinder, box, ellipsoid"); - table("size", site->size, "geom size"); - table("type", site->type, "geom type"); - table("material", site->material, "name of material"); - table("group", site->group, "group"); - table("rgba", site->rgba, "rgba when material is omitted"); - table("info", site->info, "message appended to compiler errors"); + mjsSite* elem = mjs_asSite(element); + mjsSite* ref = mjs_asSite(ref_element); + FIELD(pos, "position"); + QFIELD(quat, alt, "orientation"); + FIELD(fromto, "alternative for capsule, cylinder, box, ellipsoid"); + FIELD(size, "geom size"); + FIELD(type, "geom type"); + FIELD(material, "name of material"); + FIELD(group, "group"); + FIELD(rgba, "rgba when material is omitted"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_FRAME: { - mjsFrame* frame = mjs_asFrame(element); - table("childclass", frame->childclass, "childclass name"); - table("pos", frame->pos, "position"); - table("quat", frame->quat, "alt", frame->alt, "orientation"); - table("info", frame->info, "message appended to compiler errors"); + mjsFrame* elem = mjs_asFrame(element); + mjsFrame* ref = mjs_asFrame(ref_element); + FIELD(childclass, "childclass name"); + FIELD(pos, "position"); + QFIELD(quat, alt, "orientation"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_GEOM: { - mjsGeom* geom = mjs_asGeom(element); - table("type", geom->type, "geom type"); - table("pos", geom->pos, "position"); - table("quat", geom->quat, "alt", geom->alt, "orientation"); - table("fromto", geom->fromto, "alternative for capsule, cylinder, box, ellipsoid"); - table("size", geom->size, "type-specific size"); - table("contype", geom->contype, "contact type"); - table("conaffinity", geom->conaffinity, "contact affinity"); - table("condim", geom->condim, "contact dimensionality"); - table("priority", geom->priority, "contact priority"); - table("friction", geom->friction, "one-sided friction coefficients: slide, roll, spin"); - table("solmix", geom->solmix, "solver mixing for contact pairs"); - table("solref", geom->solref, "solver reference"); - table("solimp", geom->solimp, "solver impedance"); - table("margin", geom->margin, "margin for contact detection"); - table("gap", geom->gap, "include in solver if dist < margin-gap"); - table("mass", geom->mass, "used to compute density"); - table("density", geom->density, "used to compute mass and inertia from volume or surface"); - table("typeinertia", geom->typeinertia, "selects between surface and volume inertia"); - table("fluid_ellipsoid", geom->fluid_ellipsoid, "whether ellipsoid-fluid model is active"); - table("fluid_coefs", geom->fluid_coefs, "ellipsoid-fluid interaction coefs"); - table("material", geom->material, "name of material"); - table("rgba", geom->rgba, "rgba when material is omitted"); - table("group", geom->group, "group"); - table("hfieldname", geom->hfieldname, "heightfield attached to geom"); - table("meshname", geom->meshname, "mesh attached to geom"); - table("fitscale", geom->fitscale, "scale mesh uniformly"); - table("info", geom->info, "message appended to compiler errors"); + mjsGeom* elem = mjs_asGeom(element); + mjsGeom* ref = mjs_asGeom(ref_element); + FIELD(type, "geom type"); + FIELD(pos, "position"); + QFIELD(quat, alt, "orientation"); + FIELD(fromto, "alternative for capsule, cylinder, box, ellipsoid"); + FIELD(size, "type-specific size"); + FIELD(contype, "contact type"); + FIELD(conaffinity, "contact affinity"); + FIELD(condim, "contact dimensionality"); + FIELD(priority, "contact priority"); + FIELD(friction, "one-sided friction coefficients: slide, roll, spin"); + FIELD(solmix, "solver mixing for contact pairs"); + FIELD(solref, "solver reference"); + FIELD(solimp, "solver impedance"); + FIELD(margin, "margin for contact detection"); + FIELD(gap, "include in solver if dist < margin-gap"); + FIELD(mass, "used to compute density"); + FIELD(density, "used to compute mass and inertia from volume or surface"); + FIELD(typeinertia, "selects between surface and volume inertia"); + FIELD(fluid_ellipsoid, "whether ellipsoid-fluid model is active"); + FIELD(fluid_coefs, "ellipsoid-fluid interaction coefs"); + FIELD(material, "name of material"); + FIELD(rgba, "rgba when material is omitted"); + FIELD(group, "group"); + FIELD(hfieldname, "heightfield attached to geom"); + FIELD(meshname, "mesh attached to geom"); + FIELD(fitscale, "scale mesh uniformly"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_LIGHT: { - mjsLight* light = mjs_asLight(element); - table("pos", light->pos, "position"); - table("dir", light->dir, "direction"); - table("mode", light->mode, "tracking mode"); - table("targetbody", light->targetbody, "target body for targeting"); - table("active", light->active, "is light active"); - table("type", light->type, "type of light"); - table("texture", light->texture, "texture name for image lights"); - table("castshadow", light->castshadow, "does light cast shadows"); - table("bulbradius", light->bulbradius, "bulb radius, for soft shadows"); - table("intensity", light->intensity, "intensity, in candelas"); - table("range", light->range, "range of effectiveness"); - table("attenuation", light->attenuation, "OpenGL attenuation (quadratic model)"); - table("cutoff", light->cutoff, "OpenGL cutoff"); - table("exponent", light->exponent, "OpenGL exponent"); - table("ambient", light->ambient, "ambient color"); - table("diffuse", light->diffuse, "diffuse color"); - table("specular", light->specular, "specular color"); - table("info", light->info, "message appended to compiler errorsx"); + mjsLight* elem = mjs_asLight(element); + mjsLight* ref = mjs_asLight(ref_element); + FIELD(pos, "position"); + FIELD(dir, "direction"); + FIELD(mode, "tracking mode"); + FIELD(targetbody, "target body for targeting"); + FIELD(active, "is light active"); + FIELD(type, "type of light"); + FIELD(texture, "texture name for image lights"); + FIELD(castshadow, "does light cast shadows"); + FIELD(bulbradius, "bulb radius, for soft shadows"); + FIELD(intensity, "intensity, in candelas"); + FIELD(range, "range of effectiveness"); + FIELD(attenuation, "OpenGL attenuation (quadratic model)"); + FIELD(cutoff, "OpenGL cutoff"); + FIELD(exponent, "OpenGL exponent"); + FIELD(ambient, "ambient color"); + FIELD(diffuse, "diffuse color"); + FIELD(specular, "specular color"); + FIELD(info, "message appended to compiler errorsx"); break; } case mjOBJ_CAMERA: { - mjsCamera* camera = mjs_asCamera(element); - table("pos", camera->pos, "position"); - table("quat", camera->quat, "alt", camera->alt, "orientation"); - table("mode", camera->mode, "tracking mode"); - table("targetbody", camera->targetbody, "target body for tracking/targeting"); - table("proj", camera->proj, "camera projection type"); - table("resolution", camera->resolution, "resolution (pixel)"); - table("output", camera->output, "bit flags for output type"); - table("fovy", camera->fovy, "y-field of view"); - table("ipd", camera->ipd, "inter-pupillary distance"); - table("intrinsic", camera->intrinsic, "camera intrinsics (length)"); - table("sensor_size", camera->sensor_size, "sensor size (length)"); - table("focal_length", camera->focal_length, "focal length (length)"); - table("focal_pixel", camera->focal_pixel, "focal length (pixel)"); - table("principal_length", camera->principal_length, "principal point (length)"); - table("principal_pixel", camera->principal_pixel, "principal point (pixel)"); - table("info", camera->info, "message appended to compiler errors"); + mjsCamera* elem = mjs_asCamera(element); + mjsCamera* ref = mjs_asCamera(ref_element); + FIELD(pos, "position"); + QFIELD(quat, alt, "orientation"); + FIELD(mode, "tracking mode"); + FIELD(targetbody, "target body for tracking/targeting"); + FIELD(proj, "camera projection type"); + FIELD(resolution, "resolution (pixel)"); + FIELD(output, "bit flags for output type"); + FIELD(fovy, "y-field of view"); + FIELD(ipd, "inter-pupillary distance"); + FIELD(intrinsic, "camera intrinsics (length)"); + FIELD(sensor_size, "sensor size (length)"); + FIELD(focal_length, "focal length (length)"); + FIELD(focal_pixel, "focal length (pixel)"); + FIELD(principal_length, "principal point (length)"); + FIELD(principal_pixel, "principal point (pixel)"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_MESH: { - mjsMesh* mesh = mjs_asMesh(element); - table("content_type", mesh->content_type, "content type of file"); - table("file", mesh->file, "mesh file"); - table("refpos", mesh->refpos, "reference position"); - table("refquat", mesh->refquat, "reference orientation"); - table("scale", mesh->scale, "rescale mesh"); - table("inertia", mesh->inertia, "inertia type (convex, legacy, exact, shell)"); - table("smoothnormal", mesh->smoothnormal, "do not exclude large-angle faces from normals"); - table("needsdf", mesh->needsdf, "compute sdf from mesh"); - table("maxhullvert", mesh->maxhullvert, "maximum vertex count for the convex hull"); - table("material", mesh->material, "name of material"); - table("info", mesh->info, "message appended to compiler errors"); + mjsMesh* elem = mjs_asMesh(element); + mjsMesh* ref = mjs_asMesh(ref_element); + FIELD(content_type, "content type of file"); + FIELD(file, "mesh file"); + FIELD(refpos, "reference position"); + FIELD(refquat, "reference orientation"); + FIELD(scale, "rescale mesh"); + FIELD(inertia, "inertia type (convex, legacy, exact, shell)"); + FIELD(smoothnormal, "do not exclude large-angle faces from normals"); + FIELD(needsdf, "compute sdf from mesh"); + FIELD(maxhullvert, "maximum vertex count for the convex hull"); + FIELD(material, "name of material"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_HFIELD: { - mjsHField* hfield = mjs_asHField(element); - table("content_type", hfield->content_type, "content type of file"); - table("file", hfield->file, "file: (nrow, ncol, [elevation data])"); - table("size", hfield->size, "hfield size (ignore referencing geom size)"); - table("nrow", hfield->nrow, "number of rows"); - table("ncol", hfield->ncol, "number of columns"); - table("info", hfield->info, "message appended to compiler errors"); + mjsHField* elem = mjs_asHField(element); + mjsHField* ref = mjs_asHField(ref_element); + FIELD(content_type, "content type of file"); + FIELD(file, "file: (nrow, ncol, [elevation data])"); + FIELD(size, "hfield size (ignore referencing geom size)"); + FIELD(nrow, "number of rows"); + FIELD(ncol, "number of columns"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_SKIN: { - mjsSkin* skin = mjs_asSkin(element); - table("file", skin->file, "skin file"); - table("material", skin->material, "name of material used for rendering"); - table("rgba", skin->rgba, "rgba when material is omitted"); - table("inflate", skin->inflate, "inflate in normal direction"); - table("group", skin->group, "group for visualization"); - table("info", skin->info, "message appended to compiler errors"); + mjsSkin* elem = mjs_asSkin(element); + mjsSkin* ref = mjs_asSkin(ref_element); + FIELD(file, "skin file"); + FIELD(material, "name of material used for rendering"); + FIELD(rgba, "rgba when material is omitted"); + FIELD(inflate, "inflate in normal direction"); + FIELD(group, "group for visualization"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_FLEX: { - mjsFlex* flex = mjs_asFlex(element); - table("contype", flex->contype, "contact type"); - table("conaffinity", flex->conaffinity, "contact affinity"); - table("condim", flex->condim, "contact dimensionality"); - table("priority", flex->priority, "contact priority"); - table("friction", flex->friction, "one-sided friction coefficients: slide, roll, spin"); - table("solmix", flex->solmix, "solver mixing for contact pairs"); - table("solref", flex->solref, "solver reference"); - table("solimp", flex->solimp, "solver impedance"); - table("margin", flex->margin, "margin for contact detection"); - table("gap", flex->gap, "include in solver if distdim, "element dimensionality"); - table("radius", flex->radius, "radius around primitive element"); - table("size", flex->size, "vertex bounding box half sizes in qpos0"); - table("internal", flex->internal, "enable internal collisions"); - table("flatskin", flex->flatskin, "render flex skin with flat shading"); - table("selfcollide", flex->selfcollide, "mode for flex self collision"); - table("vertcollide", flex->vertcollide, "mode for vertex collision"); - table("passive", flex->passive, "mode for passive collisions"); - table("activelayers", flex->activelayers, "number of active element layers in 3D"); - table("group", flex->group, "group for visualization"); - table("edgestiffness", flex->edgestiffness, "edge stiffness"); - table("edgedamping", flex->edgedamping, "edge damping"); - table("rgba", flex->rgba, "rgba when material is omitted"); - table("material", flex->material, "name of material used for rendering"); - table("young", flex->young, "Young's modulus"); - table("poisson", flex->poisson, "Poisson's ratio"); - table("damping", flex->damping, "Rayleigh's damping"); - table("thickness", flex->thickness, "thickness (2D only)"); - table("elastic2d", flex->elastic2d, "2D passive forces; 0: none, 1: bending, 2: stretching, 3: both"); - table("info", flex->info, "message appended to compiler errors"); + mjsFlex* elem = mjs_asFlex(element); + mjsFlex* ref = mjs_asFlex(ref_element); + FIELD(contype, "contact type"); + FIELD(conaffinity, "contact affinity"); + FIELD(condim, "contact dimensionality"); + FIELD(priority, "contact priority"); + FIELD(friction, "one-sided friction coefficients: slide, roll, spin"); + FIELD(solmix, "solver mixing for contact pairs"); + FIELD(solref, "solver reference"); + FIELD(solimp, "solver impedance"); + FIELD(margin, "margin for contact detection"); + FIELD(gap, "include in solver if diststiffness, "stiffness coefficient"); - table("springlength", tendon->springlength, "spring resting length; {-1, -1}: use qpos_spring"); - table("damping", tendon->damping, "damping coefficient"); - table("frictionloss", tendon->frictionloss, "friction loss"); - table("solref_friction", tendon->solref_friction, "solver reference: tendon friction"); - table("solimp_friction", tendon->solimp_friction, "solver impedance: tendon friction"); - table("armature", tendon->armature, "inertia associated with tendon velocity"); - table("limited", tendon->limited, "does tendon have limits (mjtLimited)"); - table("actfrclimited", tendon->actfrclimited, "does tendon have actuator force limits"); - table("range", tendon->range, "length limits"); - table("actfrcrange", tendon->actfrcrange, "actuator force limits"); - table("margin", tendon->margin, "margin value for tendon limit detection"); - table("solref_limit", tendon->solref_limit, "solver reference: tendon limits"); - table("solimp_limit", tendon->solimp_limit, "solver impedance: tendon limits"); - table("material", tendon->material, "name of material for rendering"); - table("width", tendon->width, "width for rendering"); - table("rgba", tendon->rgba, "rgba when material is omitted"); - table("group", tendon->group, "group"); - table("info", tendon->info, "message appended to errors"); + mjsTendon* elem = mjs_asTendon(element); + mjsTendon* ref = mjs_asTendon(ref_element); + FIELD(stiffness, "stiffness coefficient"); + FIELD(springlength, "spring resting length; {-1, -1}: use qpos_spring"); + FIELD(damping, "damping coefficient"); + FIELD(frictionloss, "friction loss"); + FIELD(solref_friction, "solver reference: tendon friction"); + FIELD(solimp_friction, "solver impedance: tendon friction"); + FIELD(armature, "inertia associated with tendon velocity"); + FIELD(limited, "does tendon have limits (mjtLimited)"); + FIELD(actfrclimited, "does tendon have actuator force limits"); + FIELD(range, "length limits"); + FIELD(actfrcrange, "actuator force limits"); + FIELD(margin, "margin value for tendon limit detection"); + FIELD(solref_limit, "solver reference: tendon limits"); + FIELD(solimp_limit, "solver impedance: tendon limits"); + FIELD(material, "name of material for rendering"); + FIELD(width, "width for rendering"); + FIELD(rgba, "rgba when material is omitted"); + FIELD(group, "group"); + FIELD(info, "message appended to errors"); break; } case mjOBJ_TEXTURE: { - mjsTexture* texture = mjs_asTexture(element); - table("type", texture->type, "texture type"); - table("colorspace", texture->colorspace, "colorspace"); - table("builtin", texture->builtin, "builtin type (mjtBuiltin)"); - table("mark", texture->mark, "mark type (mjtMark)"); - table("rgb1", texture->rgb1, "first color for builtin"); - table("rgb2", texture->rgb2, "second color for builtin"); - table("markrgb", texture->markrgb, "mark color"); - table("random", texture->random, "probability of random dots"); - table("height", texture->height, "height in pixels (square for cube and skybox)"); - table("width", texture->width, "width in pixels"); - table("nchannel", texture->nchannel, "number of channels"); - table("content_type", texture->content_type, "content type of file"); - table("file", texture->file, "png file to load; use for all sides of cube"); - table("gridsize", texture->gridsize, "size of grid for composite file; (1,1)-repeat"); - table("gridlayout", texture->gridlayout, "row-major: L,R,F,B,U,D for faces; . for unused"); - table("cubefiles", texture->cubefiles, "different file for each side of the cube"); - table("hflip", texture->hflip, "horizontal flip"); - table("vflip", texture->vflip, "vertical flip"); - table("info", texture->info, "message appended to compiler errors"); + mjsTexture* elem = mjs_asTexture(element); + mjsTexture* ref = mjs_asTexture(ref_element); + FIELD(type, "texture type"); + FIELD(colorspace, "colorspace"); + FIELD(builtin, "builtin type (mjtBuiltin)"); + FIELD(mark, "mark type (mjtMark)"); + FIELD(rgb1, "first color for builtin"); + FIELD(rgb2, "second color for builtin"); + FIELD(markrgb, "mark color"); + FIELD(random, "probability of random dots"); + FIELD(height, "height in pixels (square for cube and skybox)"); + FIELD(width, "width in pixels"); + FIELD(nchannel, "number of channels"); + FIELD(content_type, "content type of file"); + FIELD(file, "png file to load; use for all sides of cube"); + FIELD(gridsize, "size of grid for composite file; (1,1)-repeat"); + FIELD(gridlayout, "row-major: L,R,F,B,U,D for faces; . for unused"); + FIELD(cubefiles, "different file for each side of the cube"); + FIELD(hflip, "horizontal flip"); + FIELD(vflip, "vertical flip"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_MATERIAL: { - mjsMaterial* material = mjs_asMaterial(element); - table("textures", material->textures, "names of textures (empty: none)"); - table("texuniform", material->texuniform, "make texture cube uniform"); - table("texrepeat", material->texrepeat, "texture repetition for 2D mapping"); - table("emission", material->emission, "emission"); - table("specular", material->specular, "specular"); - table("shininess", material->shininess, "shininess"); - table("reflectance", material->reflectance, "reflectance"); - table("metallic", material->metallic, "metallic"); - table("roughness", material->roughness, "roughness"); - table("rgba", material->rgba, "rgba"); - table("info", material->info, "message appended to compiler errors"); + mjsMaterial* elem = mjs_asMaterial(element); + mjsMaterial* ref = mjs_asMaterial(ref_element); + FIELD(textures, "names of textures (empty: none)"); + FIELD(texuniform, "make texture cube uniform"); + FIELD(texrepeat, "texture repetition for 2D mapping"); + FIELD(emission, "emission"); + FIELD(specular, "specular"); + FIELD(shininess, "shininess"); + FIELD(reflectance, "reflectance"); + FIELD(metallic, "metallic"); + FIELD(roughness, "roughness"); + FIELD(rgba, "rgba"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_PAIR: { - mjsPair* pair = mjs_asPair(element); - table("geomname1", pair->geomname1, "name of geom 1"); - table("geomname2", pair->geomname2, "name of geom 2"); - table("condim", pair->condim, "contact dimensionality"); - table("solref", pair->solref, "solver reference, normal direction"); - table("solreffriction", pair->solreffriction, "solver reference, frictional directions"); - table("solimp", pair->solimp, "solver impedance"); - table("margin", pair->margin, "margin for contact detection"); - table("gap", pair->gap, "include in solver if distfriction, "full contact friction"); - table("info", pair->info, "message appended to errors"); + mjsPair* elem = mjs_asPair(element); + mjsPair* ref = mjs_asPair(ref_element); + FIELD(geomname1, "name of geom 1"); + FIELD(geomname2, "name of geom 2"); + FIELD(condim, "contact dimensionality"); + FIELD(solref, "solver reference, normal direction"); + FIELD(solreffriction, "solver reference, frictional directions"); + FIELD(solimp, "solver impedance"); + FIELD(margin, "margin for contact detection"); + FIELD(gap, "include in solver if disttype, "constraint type"); - table("data", equality->data, "type-dependent data"); - table("active", equality->active, "is equality initially active"); - table("name1", equality->name1, "name of object 1"); - table("name2", equality->name2, "name of object 2"); - table("objtype", equality->objtype, "type of both objects"); - table("solref", equality->solref, "solver reference"); - table("solimp", equality->solimp, "solver impedance"); - table("info", equality->info, "message appended to errors"); + mjsEquality* elem = mjs_asEquality(element); + mjsEquality* ref = mjs_asEquality(ref_element); + FIELD(type, "constraint type"); + FIELD(data, "type-dependent data"); + FIELD(active, "is equality initially active"); + FIELD(name1, "name of object 1"); + FIELD(name2, "name of object 2"); + FIELD(objtype, "type of both objects"); + FIELD(solref, "solver reference"); + FIELD(solimp, "solver impedance"); + FIELD(info, "message appended to errors"); break; } case mjOBJ_EXCLUDE: { - mjsExclude* exclude = mjs_asExclude(element); - table("bodyname1", exclude->bodyname1, "name of geom 1"); - table("bodyname2", exclude->bodyname2, "name of geom 2"); - table("info", exclude->info, "message appended to errors"); + mjsExclude* elem = mjs_asExclude(element); + mjsExclude* ref = mjs_asExclude(ref_element); + FIELD(bodyname1, "name of geom 1"); + FIELD(bodyname2, "name of geom 2"); + FIELD(info, "message appended to errors"); break; } case mjOBJ_NUMERIC: { - mjsNumeric* numeric = mjs_asNumeric(element); - table("data", numeric->data, "initialization data"); - table("size", numeric->size, "array size, can be bigger than data size"); - table("info", numeric->info, "message appended to errors"); + mjsNumeric* elem = mjs_asNumeric(element); + mjsNumeric* ref = mjs_asNumeric(ref_element); + FIELD(data, "initialization data"); + FIELD(size, "array size, can be bigger than data size"); + FIELD(info, "message appended to errors"); break; } case mjOBJ_TEXT: { - mjsText* text = mjs_asText(element); - table("data", text->data, "text string"); - table("info", text->info, "message appended to compiler errors"); + mjsText* elem = mjs_asText(element); + mjsText* ref = mjs_asText(ref_element); + FIELD(data, "text string"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_TUPLE: { - mjsTuple* tuple = mjs_asTuple(element); - table("objtype", tuple->objtype, "object types"); - table("objname", tuple->objname, "object names"); - table("objprm", tuple->objprm, "object parameters"); - table("info", tuple->info, "message appended to compiler errors"); + mjsTuple* elem = mjs_asTuple(element); + mjsTuple* ref = mjs_asTuple(ref_element); + FIELD(objtype, "object types"); + FIELD(objname, "object names"); + FIELD(objprm, "object parameters"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_KEY: { - mjsKey* key = mjs_asKey(element); - table("time", key->time, "time"); - table("qpos", key->qpos, "qpos"); - table("qvel", key->qvel, "qvel"); - table("act", key->act, "act"); - table("mpos", key->mpos, "mocap pos"); - table("mquat", key->mquat, "mocap quat"); - table("ctrl", key->ctrl, "ctrl"); - table("info", key->info, "message appended to compiler errors"); + mjsKey* elem = mjs_asKey(element); + mjsKey* ref = mjs_asKey(ref_element); + FIELD(time, "time"); + FIELD(qpos, "qpos"); + FIELD(qvel, "qvel"); + FIELD(act, "act"); + FIELD(mpos, "mocap pos"); + FIELD(mquat, "mocap quat"); + FIELD(ctrl, "ctrl"); + FIELD(info, "message appended to compiler errors"); break; } case mjOBJ_PLUGIN: { - mjsPlugin* plugin = mjs_asPlugin(element); - table("name", plugin->name, "instance name"); - table("plugin_name", plugin->plugin_name, "plugin name"); - table("active", plugin->active, "is the plugin active"); - table("info", plugin->info, "message appended to compiler errors"); + mjsPlugin* elem = mjs_asPlugin(element); + mjsPlugin* ref = mjs_asPlugin(ref_element); + FIELD(name, "instance name"); + FIELD(plugin_name, "plugin name"); + FIELD(active, "is the plugin active"); + FIELD(info, "message appended to compiler errors"); break; } default: // ignore other types break; } + + #undef FIELD + return table.WasModified(); } void ElementModelGui(const mjModel* model, mjsElement* element) { diff --git a/src/experimental/platform/gui_spec.h b/src/experimental/platform/gui_spec.h index e1094583..44cf22b6 100644 --- a/src/experimental/platform/gui_spec.h +++ b/src/experimental/platform/gui_spec.h @@ -15,21 +15,38 @@ #ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_SPEC_H_ #define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_SPEC_H_ -#include +#include #include namespace mujoco::platform { -using SpecElementCallbackFn = std::function; +// The mode of the spec visualizer. +enum class SpecEditMode { + kPlay, + kEdit, +}; -// UX for displaying the spec as a tree. -void SpecExplorerGui(mjsElement** element, mjSpec* spec, - const SpecElementCallbackFn& on_delete); +// Returns the name of the given element. +std::string ElementName(mjsElement* element); -// UX for displaying the properties of an mjSpec element. -void ElementSpecGui(const mjSpec* spec, mjsElement* element); +// Displaying the mjSpec as a tree. `element` is the currently selected +// 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); + +// Displays a (read-only) data table of the mjModel values that correspond to +// the given element. void ElementModelGui(const mjModel* model, mjsElement* element); } // namespace mujoco::platform diff --git a/src/experimental/platform/imgui_widgets.cc b/src/experimental/platform/imgui_widgets.cc index 30faf4dc..5e828dae 100644 --- a/src/experimental/platform/imgui_widgets.cc +++ b/src/experimental/platform/imgui_widgets.cc @@ -62,12 +62,17 @@ KeyValues ReadIniSection(const std::string& contents, } ImGui_DataPtrTable::ImGui_DataPtrTable(float w1, float w2) { - ImGui::BeginTable("##PropertiesTable", 2); + ImGui::BeginTable("##PropertiesTable", 2, + ImGuiTableFlags_RowBg); const float width = ImGui::GetContentRegionAvail().x; ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width * w1); 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) { 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, + const mjtByte& ref, const char* tooltip) { - MakeLabel(label); - ImGui::SetItemTooltip("%s", tooltip); - ImGui::Text("%s", val ? "true" : "false"); + Label(label, tooltip); + Input(val, ref); } void ImGui_SpecElementTable::operator()(const char* label, mjtSize& val, + const mjtSize& ref, const char* tooltip) { - Scalar(label, val, tooltip); + Label(label, tooltip); + Input(val, ref); } void ImGui_SpecElementTable::operator()(const char* label, int& val, - const char* tooltip) { - Scalar(label, val, tooltip); + const int& ref, const char* tooltip) { + Label(label, tooltip); + Input(val, ref); } void ImGui_SpecElementTable::operator()(const char* label, float& val, - const char* tooltip) { - Scalar(label, val, tooltip); + const float& ref, const char* tooltip) { + Label(label, tooltip); + Input(val, ref); } void ImGui_SpecElementTable::operator()(const char* label, double& val, + const double& ref, const char* tooltip) { - Scalar(label, val, tooltip); + Label(label, tooltip); + Input(val, ref); } void ImGui_SpecElementTable::operator()(const char* label, std::string* ptr, + const std::string* ref, const char* tooltip) { - MakeLabel(label); - ImGui::SetItemTooltip("%s", tooltip); - ImGui::Text("%s", ptr ? ptr->c_str() : ""); + Label(label, tooltip); + Input(*ptr, *ref); } void ImGui_SpecElementTable::operator()(const char* label, std::vector* ptr, + const std::vector* ref, const char* tooltip) { - MakeLabel(label); - ImGui::SetItemTooltip("%s", tooltip); + Label(label, tooltip); if (ptr == nullptr || ptr->empty()) { ImGui::Text("[empty]"); } else { @@ -253,9 +264,9 @@ void ImGui_SpecElementTable::operator()(const char* label, void ImGui_SpecElementTable::operator()(const char* label, std::vector* ptr, + const std::vector* ref, const char* tooltip) { - MakeLabel(label); - ImGui::SetItemTooltip("%s", tooltip); + Label(label, tooltip); if (ptr == nullptr || ptr->empty()) { ImGui::Text("[empty]"); } else { @@ -265,41 +276,49 @@ void ImGui_SpecElementTable::operator()(const char* label, void ImGui_SpecElementTable::operator()(const char* label, std::vector* ptr, + const std::vector* ref, const char* tooltip) { for (int i = 0; i < ptr->size(); ++i) { - MakeLabel(label, i, ptr->size()); - ImGui::SetItemTooltip("%s", tooltip); - ImGui::Text("%s", ptr->at(i).c_str()); + Label(label, tooltip, i, ptr->size()); + Input(ptr->at(i)); } } -void ImGui_SpecElementTable::operator()(const char* name, double (&quat)[4], - const char* alt, - mjsOrientation& orientation, +void ImGui_SpecElementTable::operator()(const char* name, const char* alt_name, + double (&quat)[4], + const double (&ref_quat)[4], + mjsOrientation& alt, + const mjsOrientation& ref_alt, const char* tooltip) { - auto alt_name = [&](const char* label) { - return std::string(alt) + "." + label; + auto aname = [&](const char* label) { + return std::string(alt_name) + "." + label; }; - switch (orientation.type) { + switch (alt.type) { case mjORIENTATION_QUAT: - (*this)(name, quat, tooltip); + (*this)(name, quat, ref_quat, tooltip); break; case mjORIENTATION_AXISANGLE: - (*this)(alt_name("axisangle").c_str(), orientation.axisangle, tooltip); + (*this)(aname("axisangle").c_str(), alt.axisangle, ref_alt.axisangle, tooltip); break; case mjORIENTATION_XYAXES: - (*this)(alt_name("xyaxes").c_str(), orientation.xyaxes, tooltip); + (*this)(aname("xyaxes").c_str(), alt.xyaxes, ref_alt.xyaxes, tooltip); break; case mjORIENTATION_ZAXIS: - (*this)(alt_name("zaxis").c_str(), orientation.zaxis, tooltip); + (*this)(aname("zaxis").c_str(), alt.zaxis, ref_alt.zaxis, tooltip); break; case mjORIENTATION_EULER: - (*this)(alt_name("euler").c_str(), orientation.euler, tooltip); + (*this)(aname("euler").c_str(), alt.euler, ref_alt.euler, tooltip); 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) { float f = *value; 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; } + +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) { if (ImGui::GetIO().SetClipboardTextFn) { ImGui::GetIO().SetClipboardTextFn(nullptr, contents.c_str()); diff --git a/src/experimental/platform/imgui_widgets.h b/src/experimental/platform/imgui_widgets.h index dc8bf077..4355e7b9 100644 --- a/src/experimental/platform/imgui_widgets.h +++ b/src/experimental/platform/imgui_widgets.h @@ -26,6 +26,7 @@ #include #include +#include "third_party/dear_imgui/misc/cpp/imgui_stdlib.h" #include #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_RIGHT[] = "\xEF\x83\x9A"; 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[] = "\xEF\x84\x91"; 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_DIAMOND[] = "\xEF\x88\x99"; 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_MOON[] = "\xEF\x86\x86"; 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_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_REPEAT[] = "\xEF\x80\x9E"; 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_TACHOMETER[] = "\xEF\x83\xA4"; @@ -62,6 +65,9 @@ using KeyValues = std::unordered_map; template struct dependent_false : std::false_type {}; +template +bool ImGui_Checkbox(const char* name, T& value); + // Appends key/value pairs to an Ini file. void AppendIniSection(std::string& ini, const std::string& section, const KeyValues& key_values); @@ -193,93 +199,209 @@ class ImGui_DataPtrTable { // Helper for displaying mjSpec elements in an ImGui table. class ImGui_SpecElementTable : public ImGui_DataPtrTable { public: + explicit ImGui_SpecElementTable(bool read_only = true) + : read_only_(read_only) { + } + // Scalar values used by mjSpec elements. - void operator()(const char* label, mjtByte& val, const char* tooltip); - void operator()(const char* label, mjtSize& val, const char* tooltip); - void operator()(const char* label, int& val, const char* tooltip); - void operator()(const char* label, float& val, const char* tooltip); - void operator()(const char* label, double& val, const char* tooltip); + void operator()(const char* label, mjtByte& val, const mjtByte& ref, + const char* tooltip); + void operator()(const char* label, mjtSize& val, const mjtSize& ref, + 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. - 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* ptr, - const char* tooltip); + const std::vector* ref, const char* tooltip); void operator()(const char* label, std::vector* ptr, - const char* tooltip); + const std::vector* ref, const char* tooltip); void operator()(const char* label, std::vector* ptr, - const char* tooltip); + const std::vector* ref, const char* tooltip); // C-style array values used by mjSpec elements. template - void operator()(const char* label, char (&val)[N], const char* tooltip) { - MakeLabel(label); - ImGui::SetItemTooltip("%s", tooltip); - ImGui::Text("%s", std::string(val, N).c_str()); - } - template - void operator()(const char* label, mjtByte (&val)[N], const char* tooltip) { + void operator()(const char* label, int (&val)[N], const int (&ref)[N], + const char* tooltip) { for (int i = 0; i < N; ++i) { - MakeLabel(label, i, N); - ImGui::SetItemTooltip("%s", tooltip); - ImGui::Text("%s", val[i] ? "true" : "false"); + Label(label, tooltip, i, N); + Input(val[i], ref[i]); } } template - void operator()(const char* label, int (&val)[N], const char* tooltip) { - Vector(label, val, N, tooltip); + void operator()(const char* label, float (&val)[N], const float (&ref)[N], + const char* tooltip) { + for (int i = 0; i < N; ++i) { + Label(label, tooltip, i, N); + Input(val[i], ref[i]); + } } template - void operator()(const char* label, float (&val)[N], const char* tooltip) { - Vector(label, val, N, tooltip); + void operator()(const char* label, double (&val)[N], const double (&ref)[N], + 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 - void operator()(const char* label, double (&val)[N], const char* tooltip) { - Vector(label, val, N, tooltip); + void operator()(const char* label, char (&val)[N], const char (&ref)[N], + 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. template , T>> - void operator()(const char* label, T& val, const char* tooltip) { - auto v = enum_utils::enum_to_string(val); - MakeLabel(label); - ImGui::SetItemTooltip("%s", tooltip); - ImGui::Text("%s", v.data()); + void operator()(const char* label, T& val, const T& ref, const char* tooltip) { + Label(label, tooltip); + Input(val, ref); } // Special handling for quaternion/orientation pairs. - void operator()(const char* name, double (&quat)[4], const char* alt, - mjsOrientation& orientation, const char* tooltip); + void operator()(const char* name, const char* alt_name, double (&quat)[4], + 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: - template - void Scalar(const char* label, T& val, const char* tooltip) { - MakeLabel(label); - ImGui::SetItemTooltip("%s", tooltip); - if constexpr (std::is_enum_v) { - ImGui::Text("%d", (int)val); - } else if constexpr (std::is_integral_v) { - ImGui::Text("%d", (int)val); - } else if constexpr (std::is_floating_point_v) { - ImGui::Text("%f", (float)val); - } - } + void Label(const char* label, const char* tooltip, int i = 0, int n = 1); template - void Vector(const char* label, T* ptr, int n, const char* tooltip) { - for (int i = 0; i < n; ++i) { - MakeLabel(label, i, n); - ImGui::SetItemTooltip("%s", tooltip); - if constexpr (std::is_enum_v) { - ImGui::Text("%d", (int)ptr[i]); - } else if constexpr (std::is_integral_v) { - ImGui::Text("%d", (int)ptr[i]); - } else if constexpr (std::is_floating_point_v) { - ImGui::Text("%f", (float)ptr[i]); + void Input(T& val, const T& ref = T()) { + ScopedStyle style; + if (!read_only_ && val != ref) { + constexpr ImVec4 kModifiedColor = ImVec4(1.0f, 0.6f, 0.2f, 1.0f); + style.Color(ImGuiCol_Text, kModifiedColor); + } + + ImGui::PushID(&val); + ImGui::SetNextItemWidth(-1.0f); + if constexpr (std::is_same_v) { + 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) { + 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) { + if (ImGui::Selectable(std::string(n).c_str(), val == v)) { + val = v; + modified_ = true; + } + } + ImGui::EndCombo(); + } + } + } else if constexpr (std::is_same_v) { + if (read_only_) { + ImGui::Text("%d", val); + } else { + if (ImGui::InputInt("##", &val)) { + modified_ = true; + } + } + } else if constexpr (std::is_same_v) { + if (read_only_) { + ImGui::Text("%f", val); + } else { + if (ImGui::InputFloat("##", &val)) { + modified_ = true; + } + } + } else if constexpr (std::is_same_v) { + if (read_only_) { + ImGui::Text("%f", val); + } else { + if (ImGui::InputDouble("##", &val)) { + modified_ = true; + } + } + } else if constexpr (std::is_same_v) { + if (read_only_) { + ImGui::Text("%lld", val); + } else { + if (ImGui::InputScalarN("##", ImGuiDataType_S64, &val, 1)) { + modified_ = true; + } + } + } else if constexpr (std::is_same_v) { + if (read_only_) { + ImGui::Text("%s", val.c_str()); + } else { + if (ImGui::InputText("##", &val)) { + modified_ = true; + } } } + + if (!read_only_) { + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + val = ref; + modified_ = true; + } + } + + 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. bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max); diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 6d33f5af..66d71d34 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -67,6 +67,7 @@ static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) { perturb.active = 0; } } + // TODO: update selected element! } 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_CURR_FRAME = platform::ICON_FA_FAST_FORWARD; static constexpr const char* ICON_SPEED = platform::ICON_FA_TACHOMETER; -static constexpr const char* ICON_DELETE = platform::ICON_FA_TRASH_CAN; +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. 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); history_.Init(state_size); + // Create a copy of the spec for editing. + CopyLoadedSpecForEditing(); + if (!preserve_camera_on_load_) { const int model_cam = model->vis.global.cameraid; if (model_cam >= 0 && model_cam < model->ncam) { @@ -422,44 +428,54 @@ void App::ProcessPendingLoads() { }); } -void App::SpecSelectElement(mjsElement* element) { - tmp_.element = element; - if (tmp_.element == nullptr) { - tmp_.element_id = -1; - } else { - tmp_.element_id = mjs_getId(tmp_.element); +void App::CopyLoadedSpecForEditing() { + if (scratch_spec_ != nullptr) { + mj_deleteSpec(scratch_spec_); + scratch_spec_ = nullptr; + } - // 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; - } - } -} + scratch_spec_modified_ = false; + tmp_.curr_edit_element = nullptr; -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_); + 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); } - } - 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() { @@ -555,14 +571,12 @@ void App::HandleMouseEvents() { perturb_.skinselect = picked.skin; // Select the corresponding element in the spec. - tmp_.element = nullptr; - tmp_.element_id = -1; + tmp_.curr_element = nullptr; if (has_spec()) { mjsElement* element = mjs_firstElement(spec(), mjOBJ_BODY); while (element) { if (mjs_getId(element) == picked.body) { - tmp_.element = element; - tmp_.element_id = picked.body; + tmp_.curr_element = element; break; } element = mjs_nextElement(spec(), element); @@ -665,7 +679,7 @@ void App::HandleKeyboardEvents() { } else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) { ResetPhysics(); } else if (ImGui_IsChordJustPressed(ImGuiKey_Delete)) { - SpecDeleteElement(tmp_.element); + // TODO: SpecDeleteElement(tmp_.curr_element); } else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) { SelectParentPerturb(model(), perturb_); } else if (ImGui_IsChordJustPressed(ImGuiKey_F1)) { @@ -917,19 +931,15 @@ void App::BuildGui() { } ImGui::End(); - bool explorer_is_open = false; if (ImGui::Begin("Explorer", &tmp_.inspector_panel)) { - explorer_is_open = true; SpecExplorerGui(); } ImGui::End(); - if (explorer_is_open && tmp_.element != nullptr) { - if (ImGui::Begin("Properties")) { - SpecPropertiesGui(); - } - ImGui::End(); + if (ImGui::Begin("Editor", &tmp_.inspector_panel)) { + SpecEditorGui(); } + ImGui::End(); } if (tmp_.chart_performance) { @@ -1165,56 +1175,196 @@ void App::SpecExplorerGui() { return; } - auto on_delete = [this](mjsElement* element) { SpecDeleteElement(element); }; - - mjsElement* element = tmp_.element; - platform::SpecExplorerGui(&element, spec(), on_delete); - if (element != tmp_.element) { - SpecSelectElement(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_.curr_element; + bool open = element != nullptr; + if (platform::ImGui_BeginHSplit("SpecExplorerTree", + &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; + } + } + } + + if (platform::ImGui_HSplit("SpecExplorerProperties", + &tmp_.explorer_split, &open)) { + platform::ScopedStyle style; + + ImGui::Text("%s", mju_type2Str(tmp_.curr_element->elemtype)); + ImGui::SameLine(); + ImGui::Text("(%d)", mjs_getId(tmp_.curr_element)); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 100.0f); + + if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) { + style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive); + } + style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::Button("S", ImVec2(24.0f, 20.0f))) { + 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); + } + style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::Button("M", ImVec2(24.0f, 20.0f))) { + 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); + } + style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::Button("D", ImVec2(24.0f, 20.0f))) { + tmp_.spec_prop_mode = SpecPropertiesMode::kData; + } + ImGui::SetItemTooltip("Data"); + style.Reset(); + ImGui::Separator(); + + if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) { + platform::ElementSpecGui(element, element, SpecEditMode::kPlay); + } else if (tmp_.spec_prop_mode == SpecPropertiesMode::kModel) { + platform::ElementModelGui(model(), tmp_.curr_element); + } else { + platform::ElementDataGui(data(), tmp_.curr_element); + } + } + if (!open) { + tmp_.curr_element = nullptr; + } + } + platform::ImGui_EndHSplit(open); } -void App::SpecPropertiesGui() { - platform::ScopedStyle style; +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::Text("%s", mju_type2Str(tmp_.element->elemtype)); - ImGui::SameLine(); - ImGui::Text("(%d)", tmp_.element_id); + 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::SameLine(120); - if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) { - style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive); + 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(); + } } - if (ImGui::SmallButton("S")) { - tmp_.spec_prop_mode = SpecPropertiesMode::kSpec; + ImGui::EndChild(); + + // Initialize the split height. + const ImVec2 region = ImGui::GetContentRegionAvail(); + if (tmp_.editor_split < 0) { + tmp_.editor_split = region.y * 0.7f; } - 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(); - 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); + 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(); - if (ImGui::MenuItem(tmp_.options_panel ? "Hide Options" : "Show Left UI", + if (ImGui::MenuItem(tmp_.options_panel ? "Hide Options" : "Show Options", "Tab")) { tmp_.options_panel = !tmp_.options_panel; } if (ImGui::MenuItem( - tmp_.inspector_panel ? "Hide Inspector" : "Show Right UI", + tmp_.inspector_panel ? "Hide Inspector" : "Show Inspector", "Shift+Tab")) { tmp_.inspector_panel = !tmp_.inspector_panel; } diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index 66332e70..64e548d0 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -27,6 +27,7 @@ #include #include "experimental/platform/gui.h" +#include "experimental/platform/gui_spec.h" #include "experimental/platform/interaction.h" #include "experimental/platform/model_holder.h" #include "experimental/platform/picture_gui.h" @@ -84,6 +85,8 @@ class App { void Render(); private: + using SpecEditMode = platform::SpecEditMode; + // The kind of model that is currently loaded. enum ModelKind { kEmptyModel, @@ -127,6 +130,8 @@ class App { bool style_editor = false; bool imgui_demo = false; bool implot_demo = false; + float editor_split = -1; + float explorer_split = -1; // Controls. bool perturb_active = false; @@ -141,10 +146,10 @@ class App { std::vector camera_names; std::vector speed_names; - // Spec Properties. + // Spec editing. SpecPropertiesMode spec_prop_mode = SpecPropertiesMode::kSpec; - mjsElement* element = nullptr; - int element_id = -1; + mjsElement* curr_element = nullptr; + mjsElement* curr_edit_element = nullptr; // State. int state_sig = 0; @@ -216,13 +221,11 @@ class App { void ModelOptionsGui(); void DataInspectorGui(); void SpecExplorerGui(); - void SpecPropertiesGui(); - - void SpecSelectElement(mjsElement* element); - void SpecDeleteElement(mjsElement* element); + void SpecEditorGui(); float GetExpectedLabelWidth(); std::vector GetCameraNames(); + void CopyLoadedSpecForEditing(); mjSpec* spec() { return model_holder_->spec(); } mjModel* model() { return model_holder_->model(); } @@ -244,6 +247,17 @@ class App { std::unique_ptr window_; std::unique_ptr renderer_; std::unique_ptr 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 spec_to_scratch_; + std::unordered_map scratch_to_spec_; std::function spec_op_; platform::StepControl step_control_;