diff --git a/doc/changelog.rst b/doc/changelog.rst index 6449c0fd..55c9a1d1 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -25,10 +25,41 @@ Bug fixes which affects the behaviour of the :ref:`implicit and implicitfast integrators`. - Fixes to :ref:`mj_ray`, in line with geom visualisation conventions: - - Planes and height-fields respect the `geom_group` and `flg_static` arguments. Before this change, rays would + - Planes and height-fields respect the ``geom_group`` and ``flg_static`` arguments. Before this change, rays would intersect planes and height-fields unconditionally. - ``flg_static`` now apllies to all static geoms, not just those which are direct children of the world body. +.. youtube:: hqIMTNGaLF4 + :align: right + :width: 240px + +Plugins +^^^^^^^ + +- Added touch-grid sensor plugin. See `documentation `_ + for details, and associated `touch_grid.xml `_ + example model. The plugin includes `in-scene visualisation `_. + +Simulate +^^^^^^^^ + +.. youtube:: mXVPbppGk5I + :align: right + :width: 240px + +- Added Visualization tab to simulate UI, corresponding to elements of the :ref:`visual` MJCF element. After + modifying values in the GUI, a saved XML will contain the new values. The modifyable members of + :ref:`mjStatistic` (:ref:`extent`, :ref:`meansize` and + :ref:`center`) are computed by the compiler and therefore do not have defaults. In order for these + attributes to appear in the saved XML, a value must be specified in the loaded XML. + +.. image:: images/changelog/simulate_text_width.png + :align: right + :width: 380px + :alt: Before / After + +- Increased text width for UI elements in the default spacing. [before / after]: + General ^^^^^^^ @@ -65,17 +96,6 @@ General - Removed ``pstack`` and ``parena`` from the output of ``mj_printData``, since these are implementation details of the ``mjData`` allocators that are affected by diagnostic paddings in instrumented builds. -.. youtube:: hqIMTNGaLF4 - :align: right - :width: 240px - -Plugins -^^^^^^^ - -- Added touch-grid sensor plugin. See `documentation `_ - for details, and associated `touch_grid.xml `_ - example model. The plugin includes `in-scene visualisation `_. - Version 2.3.5 (April 25, 2023) ------------------------------ diff --git a/doc/images/changelog/simulate_text_width.png b/doc/images/changelog/simulate_text_width.png new file mode 100644 index 00000000..ee74d60c Binary files /dev/null and b/doc/images/changelog/simulate_text_width.png differ diff --git a/doc/includes/references.h b/doc/includes/references.h index 71e26e7f..ed4166b5 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1402,6 +1402,7 @@ typedef enum mjtItem_ { // UI item type mjITEM_SLIDERNUM, // slider, mjtNum value mjITEM_EDITINT, // editable array, int values mjITEM_EDITNUM, // editable array, mjtNum values + mjITEM_EDITFLOAT, // editable array, float values mjITEM_EDITTXT, // editable text mjNITEM // number of item types diff --git a/include/mujoco/mjui.h b/include/mujoco/mjui.h index 7246e5a8..2f96d7e2 100644 --- a/include/mujoco/mjui.h +++ b/include/mujoco/mjui.h @@ -55,6 +55,8 @@ #define mjKEY_F10 299 #define mjKEY_F11 300 #define mjKEY_F12 301 +#define mjKEY_NUMPAD_0 320 +#define mjKEY_NUMPAD_9 329 //---------------------------------- primitive types (mjt) ----------------------------------------- @@ -97,6 +99,7 @@ typedef enum mjtItem_ { // UI item type mjITEM_SLIDERNUM, // slider, mjtNum value mjITEM_EDITINT, // editable array, int values mjITEM_EDITNUM, // editable array, mjtNum values + mjITEM_EDITFLOAT, // editable array, float values mjITEM_EDITTXT, // editable text mjNITEM // number of item types diff --git a/introspect/enums.py b/introspect/enums.py index 2b82759b..b9604216 100644 --- a/introspect/enums.py +++ b/introspect/enums.py @@ -654,8 +654,9 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjITEM_SLIDERNUM', 9), ('mjITEM_EDITINT', 10), ('mjITEM_EDITNUM', 11), - ('mjITEM_EDITTXT', 12), - ('mjNITEM', 13), + ('mjITEM_EDITFLOAT', 12), + ('mjITEM_EDITTXT', 13), + ('mjNITEM', 14), ]), )), ]) diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 297a3a72..0373afd2 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -102,6 +102,7 @@ enum { SECT_WATCH, SECT_PHYSICS, SECT_RENDERING, + SECT_VISUALIZATION, SECT_GROUP, NSECT0, @@ -565,7 +566,7 @@ void UpdateWatch(mj::Simulate* sim, const mjModel* m, const mjData* d) { if (!mju::strcmp_arr(#NAME, sim->field) && \ !mju::strcmp_arr(#TYPE, "mjtNum")) { \ if (sim->index >= 0 && sim->index < m->NR * NC) { \ - PrintField(sim->ui0.sect[SECT_WATCH].item[2].multi.name[0],d->NAME + sim->index); \ + PrintField(sim->ui0.sect[SECT_WATCH].item[2].multi.name[0], d->NAME + sim->index); \ } else { \ mju::strcpy_arr(sim->ui0.sect[SECT_WATCH].item[2].multi.name[0], "invalid index"); \ } \ @@ -768,7 +769,64 @@ void MakeRenderingSection(mj::Simulate* sim, const mjModel* m, int oldstate) { } } +// make visualization section of UI +void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m, int oldstate) { + mjStatistic* stat = sim->fully_managed_ ? &sim->m_->stat : &sim->scnstate_.model.stat; + mjVisual* vis = sim->fully_managed_ ? &sim->m_->vis : &sim->scnstate_.model.vis; + mjuiDef defVisualization[] = { + {mjITEM_SECTION, "Visualization", oldstate, nullptr, "AV"}, + {mjITEM_SEPARATOR, "Headlight", 1}, + {mjITEM_RADIO, "Active", 5, &(vis->headlight.active), "Off\nOn"}, + {mjITEM_EDITFLOAT, "Ambient", 2, &(vis->headlight.ambient), "3"}, + {mjITEM_EDITFLOAT, "Diffuse", 2, &(vis->headlight.diffuse), "3"}, + {mjITEM_EDITFLOAT, "Specular", 2, &(vis->headlight.specular), "3"}, + {mjITEM_SEPARATOR, "Initial Free Camera", 1}, + {mjITEM_EDITNUM, "Center", 2, &(stat->center), "3"}, + {mjITEM_EDITFLOAT, "Azimuth", 2, &(vis->global.azimuth), "1"}, + {mjITEM_EDITFLOAT, "Elevation", 2, &(vis->global.elevation), "1"}, + {mjITEM_BUTTON, "Align", 2, nullptr, "CA"}, + {mjITEM_SEPARATOR, "Global", 1}, + {mjITEM_EDITNUM, "Extent", 2, &(stat->extent), "1"}, + {mjITEM_EDITFLOAT, "Field of view", 2, &(vis->global.fovy), "1"}, + {mjITEM_RADIO, "Inertia", 5, &(vis->global.ellipsoidinertia), "Box\nEllipsoid"}, + {mjITEM_SEPARATOR, "Map", 1}, + {mjITEM_EDITFLOAT, "Stiffness", 2, &(vis->map.stiffness), "1"}, + {mjITEM_EDITFLOAT, "Rot stiffness", 2, &(vis->map.stiffnessrot), "1"}, + {mjITEM_EDITFLOAT, "Force", 2, &(vis->map.force), "1"}, + {mjITEM_EDITFLOAT, "Torque", 2, &(vis->map.torque), "1"}, + {mjITEM_EDITFLOAT, "Alpha", 2, &(vis->map.alpha), "1"}, + {mjITEM_EDITFLOAT, "Fog start", 2, &(vis->map.fogstart), "1"}, + {mjITEM_EDITFLOAT, "Fog end", 2, &(vis->map.fogend), "1"}, + {mjITEM_EDITFLOAT, "Z near", 2, &(vis->map.znear), "1"}, + {mjITEM_EDITFLOAT, "Z far", 2, &(vis->map.zfar), "1"}, + {mjITEM_EDITFLOAT, "Haze", 2, &(vis->map.haze), "1"}, + {mjITEM_EDITFLOAT, "Shadow clip", 2, &(vis->map.shadowclip), "1"}, + {mjITEM_EDITFLOAT, "Shadow scale", 2, &(vis->map.shadowscale), "1"}, + {mjITEM_SEPARATOR, "Scale", 1}, + {mjITEM_EDITNUM, "All [meansize]", 2, &(stat->meansize), "1"}, + {mjITEM_EDITFLOAT, "Force width", 2, &(vis->scale.forcewidth), "1"}, + {mjITEM_EDITFLOAT, "Contact width", 2, &(vis->scale.contactwidth), "1"}, + {mjITEM_EDITFLOAT, "Contact height", 2, &(vis->scale.contactheight), "1"}, + {mjITEM_EDITFLOAT, "Connect", 2, &(vis->scale.connect), "1"}, + {mjITEM_EDITFLOAT, "Com", 2, &(vis->scale.com), "1"}, + {mjITEM_EDITFLOAT, "Camera", 2, &(vis->scale.camera), "1"}, + {mjITEM_EDITFLOAT, "Light", 2, &(vis->scale.light), "1"}, + {mjITEM_EDITFLOAT, "Select point", 2, &(vis->scale.selectpoint), "1"}, + {mjITEM_EDITFLOAT, "Joint length", 2, &(vis->scale.jointlength), "1"}, + {mjITEM_EDITFLOAT, "Joint width", 2, &(vis->scale.jointwidth), "1"}, + {mjITEM_EDITFLOAT, "Actuator length", 2, &(vis->scale.actuatorlength), "1"}, + {mjITEM_EDITFLOAT, "Actuator width", 2, &(vis->scale.actuatorwidth), "1"}, + {mjITEM_EDITFLOAT, "Frame length", 2, &(vis->scale.framelength), "1"}, + {mjITEM_EDITFLOAT, "Frame width", 2, &(vis->scale.framewidth), "1"}, + {mjITEM_EDITFLOAT, "Constraint", 2, &(vis->scale.constraint), "1"}, + {mjITEM_EDITFLOAT, "Slider-crank", 2, &(vis->scale.slidercrank), "1"}, + {mjITEM_END} + }; + + // add rendering standard + mjui_add(&sim->ui0, defVisualization); +} // make group section of UI void MakeGroupSection(mj::Simulate* sim, int oldstate) { @@ -952,6 +1010,7 @@ void MakeUiSections(mj::Simulate* sim, const mjModel* m, const mjData* d) { // make MakePhysicsSection(sim, oldstate0[SECT_PHYSICS]); MakeRenderingSection(sim, m, oldstate0[SECT_RENDERING]); + MakeVisualizationSection(sim, m, oldstate0[SECT_VISUALIZATION]); MakeGroupSection(sim, oldstate0[SECT_GROUP]); MakeJointSection(sim, oldstate1[SECT_JOINT]); MakeControlSection(sim, oldstate1[SECT_CONTROL]); @@ -1284,6 +1343,13 @@ void UiEvent(mjuiState* state) { } } + // visualization section + else if (it && it->sectionid==SECT_VISUALIZATION) { + if (!mju::strcmp_arr(it->name, "Align")) { + sim->pending_.align = true; + } + } + // group section else if (it && it->sectionid==SECT_GROUP) { // remake joint section if joint group changed diff --git a/src/ui/ui_main.c b/src/ui/ui_main.c index 1fd73d0e..b602255a 100644 --- a/src/ui/ui_main.c +++ b/src/ui/ui_main.c @@ -28,7 +28,7 @@ static const mjuiThemeSpacing themeSpacing0 = { 270, // int total; 15, // int scroll; - 100, // int label; + 120, // int label; 8, // int section; 4, // int itemside; 4, // int itemmid; @@ -743,7 +743,9 @@ static int checkedit(const char* text, const mjuiItem* it) { } // check type - if (it->type!=mjITEM_EDITINT && it->type!=mjITEM_EDITNUM) { + if (it->type!=mjITEM_EDITINT && + it->type!=mjITEM_EDITNUM && + it->type!=mjITEM_EDITFLOAT) { mju_error("Internal error: expected edit control"); } @@ -791,7 +793,7 @@ static int text2array(const char* text, const mjuiItem* it) { } // check type - if (it->type!=mjITEM_EDITINT && it->type!=mjITEM_EDITNUM) { + if (it->type!=mjITEM_EDITINT && it->type!=mjITEM_EDITNUM && it->type!=mjITEM_EDITFLOAT) { mju_error("Internal error: expected edit control"); } @@ -829,9 +831,16 @@ static int text2array(const char* text, const mjuiItem* it) { pdata[i] = (int)val[i]; } } else { - mjtNum* pdata = (mjtNum*)it->pdata; - for (int i=0; itype==mjITEM_EDITNUM) { + mjtNum* pdata = (mjtNum*)it->pdata; + for (int i=0; ipdata; + for (int i=0; itype!=mjITEM_EDITINT && it->type!=mjITEM_EDITNUM) { + if (it->type!=mjITEM_EDITINT && it->type!=mjITEM_EDITNUM && it->type!=mjITEM_EDITFLOAT) { mju_error("Internal error: expected edit control"); } @@ -862,8 +871,10 @@ static void array2text(char* text, const mjuiItem* it) { for (int i=0; itype==mjITEM_EDITINT) { mjSNPRINTF(buf, "%d", ((int*)it->pdata)[i]); - } else { + } else if (it->type==mjITEM_EDITNUM) { mjSNPRINTF(buf, "%.4g", ((mjtNum*)it->pdata)[i]); + } else { + mjSNPRINTF(buf, "%.4g", ((float*)it->pdata)[i]); } strncat(text, buf, mjMAXUITEXT - strlen(text) - 1); if (i='0' && key<='9') || - (key>=mjNUMPAD_0 && key<=mjNUMPAD_9) || - ((key=='e' || key=='E' || key=='.') && type==mjITEM_EDITNUM))) { + (key>=mjKEY_NUMPAD_0 && key<=mjKEY_NUMPAD_9) || + ((key=='e' || key=='E' || key=='.') && + (type==mjITEM_EDITNUM || type==mjITEM_EDITFLOAT)))) { // remap '=' to '+' if (key=='=') { @@ -936,8 +946,8 @@ static int validkey(int key, int sz, int type, const mjuiState* state) { } // remap numberpad to top row - if (key>=mjNUMPAD_0 && key<=mjNUMPAD_9) { - key = key - mjNUMPAD_0 + '0'; + if (key>=mjKEY_NUMPAD_0 && key<=mjKEY_NUMPAD_9) { + key = key - mjKEY_NUMPAD_0 + '0'; } return key; @@ -1271,7 +1281,7 @@ void mjui_add(mjUI* ui, const mjuiDef* def) { } // parse edit numeric - else if (it->type==mjITEM_EDITINT || it->type==mjITEM_EDITNUM) { + else if (it->type==mjITEM_EDITINT || it->type==mjITEM_EDITNUM || it->type==mjITEM_EDITFLOAT) { // check mjMAXUIEDIT if (mjMAXUIEDIT>7) { mju_error("internal error: mjMAXUIEDIT bigger than 7"); @@ -1989,6 +1999,7 @@ void mjui_update(int section, int item, const mjUI* ui, case mjITEM_EDITINT: case mjITEM_EDITNUM: + case mjITEM_EDITFLOAT: case mjITEM_EDITTXT: if (state) { rgbdecor = ui->color.edit; @@ -2165,6 +2176,7 @@ mjuiItem* mjui_event(mjUI* ui, mjuiState* state, const mjrContext* con) { // move edit else if (it_rec->type==mjITEM_EDITINT || it_rec->type==mjITEM_EDITNUM || + it_rec->type==mjITEM_EDITFLOAT || it_rec->type==mjITEM_EDITTXT) { setcursor(it_rec->rect, ui, state, con); } @@ -2326,6 +2338,7 @@ mjuiItem* mjui_event(mjUI* ui, mjuiState* state, const mjrContext* con) { case mjITEM_EDITINT: case mjITEM_EDITNUM: + case mjITEM_EDITFLOAT: case mjITEM_EDITTXT: // set edit text, clear scroll array2text(ui->edittext, it_cur); diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 696cc6fd..df357577 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -2897,6 +2897,11 @@ bool mjCModel::CopyBack(const mjModel* m) { option = m->opt; visual = m->vis; + // runtime-modifiable members of mjStatistic + meansize = m->stat.meansize; + extent = m->stat.extent; + mju_copy3(center, m->stat.center); + // qpos0, qpos_spring for (int i=0; itype) { diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index a5914b62..2c20bdd3 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -1068,5 +1068,27 @@ TEST_F(XMLWriterTest, WriteReadCompare) { } } +// ---------------- test CopyBack functionality (decompiler) ------------------ +using DecompilerTest = MujocoTest; +TEST_F(XMLWriterTest, SavesStatitics) { + static constexpr char xml[] = R"( + + + + )"; + mjModel* model = LoadModelFromString(xml); + ASSERT_THAT(model, NotNull()); + model->stat.meansize = 7; + model->stat.extent = 8; + model->stat.center[0] = 9; + model->stat.center[1] = 10; + model->stat.center[2] = 11; + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("meansize=\"7\"")); + EXPECT_THAT(saved_xml, HasSubstr("extent=\"8\"")); + EXPECT_THAT(saved_xml, HasSubstr("center=\"9 10 11\"")); + mj_deleteModel(model); +} + } // namespace } // namespace mujoco diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 445ceac2..f22f8d59 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -94,6 +94,8 @@ public const int mjKEY_F9 = 298; public const int mjKEY_F10 = 299; public const int mjKEY_F11 = 300; public const int mjKEY_F12 = 301; +public const int mjKEY_NUMPAD_0 = 320; +public const int mjKEY_NUMPAD_9 = 329; public const bool THIRD_PARTY_MUJOCO_MJVISUALIZE_H_ = true; public const int mjNGROUP = 6; public const int mjMAXLIGHT = 100;