diff --git a/doc/changelog.rst b/doc/changelog.rst index cbd6c416..e42fc33c 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -5,6 +5,15 @@ Changelog Upcoming version (not yet released) ----------------------------------- +General +^^^^^^^ + +- The MJCF grammar is now defined in a single source of truth schema file, + `src/xml/mjcf.schema `__. The parser's + grammar table, presence constraints, keyword maps, typed attribute bindings and save policies are generated from it + and gated by tests, as are the schema's enum keywords and declared defaults against the C headers and + default-constructors. + Actuation ^^^^^^^^^ - Added the :ref:`pid` actuator: a PID controller with real position and velocity setpoint inputs, diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 5c107ba3..dc3ffa07 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -20,7 +20,6 @@ import math import os import textwrap import typing -import warnings import zipfile # pylint: disable=unused-import from absl import flags @@ -61,7 +60,7 @@ class SpecsTest(absltest.TestCase): """ spec = mujoco.MjSpec.from_string(xml) - model = spec.compile() + spec.compile() self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TOTAL], 0) self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_ASSETS], 0) self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TEXTURE], 0) @@ -152,7 +151,7 @@ class SpecsTest(absltest.TestCase): - + diff --git a/src/user/user_objects.h b/src/user/user_objects.h index d64da8fe..341cc980 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -1138,6 +1138,7 @@ class mjCMesh_ : public mjCBase { class mjCMesh: public mjCMesh_, private mjsMesh { friend class mjCModel; + friend class mjXWriter; public: explicit mjCMesh(mjCModel* = nullptr, mjCDef* = nullptr); diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index d65d697c..e2c23b5e 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -38,6 +38,9 @@ #include "xml/xml_util.h" #include "tinyxml2.h" +// typed attribute rows, generated from mjcf.schema; shared with the reader +#include "mjcf_read_table.inc" + namespace { using std::string; @@ -133,14 +136,12 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) { // common attributes WriteAttrTxt(elem, "name", flex->name); - WriteAttr(elem, "radius", 1, &flex->radius, &defflex.radius); + WriteAttrTable(elem, static_cast(flex), + static_cast(&defflex), kFlexAttrs, + kFlexAttrsN); if (flex->get_material() != defflex.get_material()) { WriteAttrTxt(elem, "material", flex->get_material()); } - WriteAttr(elem, "rgba", 4, flex->rgba, defflex.rgba); - WriteAttrKey(elem, "flatskin", bool_map, 2, flex->flatskin, defflex.flatskin); - WriteAttrInt(elem, "dim", flex->dim, defflex.dim); - WriteAttrInt(elem, "group", flex->group, defflex.group); WriteAttr(elem, "cellcount", 3, flex->spec.cellcount, defflex.spec.cellcount); if (flex->spec.order != defflex.spec.order) { string dof_str = "full"; @@ -177,20 +178,9 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) { // contact subelement XMLElement* cont = InsertEnd(elem, "contact"); - WriteAttrInt(cont, "contype", flex->contype, defflex.contype); - WriteAttrInt(cont, "conaffinity", flex->conaffinity, defflex.conaffinity); - WriteAttrInt(cont, "condim", flex->condim, defflex.condim); - WriteAttrInt(cont, "priority", flex->priority, defflex.priority); - WriteAttr(cont, "friction", 3, flex->friction, defflex.friction); - WriteAttr(cont, "solmix", 1, &flex->solmix, &defflex.solmix); - WriteAttr(cont, "solref", mjNREF, flex->solref, defflex.solref); - WriteAttr(cont, "solimp", mjNIMP, flex->solimp, defflex.solimp); - WriteAttr(cont, "margin", 1, &flex->margin, &defflex.margin); - WriteAttr(cont, "gap", 1, &flex->gap, &defflex.gap); - WriteAttrKey(cont, "internal", bool_map, 2, flex->internal, defflex.internal); - WriteAttrKey(cont, "selfcollide", flexself_map, 5, flex->selfcollide, defflex.selfcollide); - WriteAttrInt(cont, "activelayers", flex->activelayers, defflex.activelayers); - WriteAttrKey(cont, "passive", bool_map, 2, flex->passive, defflex.passive); + WriteAttrTable(cont, static_cast(flex), + static_cast(&defflex), kFlexcomp_contactAttrs, + kFlexcomp_contactAttrsN); // remove contact is no attributes if (!cont->FirstAttribute()) { @@ -199,16 +189,15 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) { // elasticity subelement XMLElement* elastic = InsertEnd(elem, "elasticity"); - WriteAttr(elastic, "young", 1, &flex->young, &defflex.young); - WriteAttr(elastic, "poisson", 1, &flex->poisson, &defflex.poisson); - WriteAttr(elastic, "thickness", 1, &flex->thickness, &defflex.thickness); - WriteAttr(elastic, "damping", 1, &flex->damping, &defflex.damping); - WriteAttrKey(elastic, "elastic2d", elastic2d_map, 4, flex->elastic2d, defflex.elastic2d); + WriteAttrTable(elastic, static_cast(flex), + static_cast(&defflex), kElasticityAttrs, + kElasticityAttrsN); // edge subelement XMLElement* edge = InsertEnd(elem, "edge"); - WriteAttr(edge, "stiffness", 1, &flex->edgestiffness, &defflex.edgestiffness); - WriteAttr(edge, "damping", 1, &flex->edgedamping, &defflex.edgedamping); + WriteAttrTable(edge, static_cast(flex), + static_cast(&defflex), kFlex_edgeAttrs, + kFlex_edgeAttrsN); // remove edge if no attributes if (!edge->FirstAttribute()) { @@ -257,14 +246,11 @@ void mjXWriter::OneMesh(XMLElement* elem, const mjCMesh* mesh, mjCDef* def) { } // defaults and regular - if (mesh->Inertia() != def->Mesh().Inertia()) { - WriteAttrTxt(elem, "inertia", FindValue(meshinertia_map, 4, mesh->Inertia())); + WriteAttrTable(elem, static_cast(mesh), &def->Mesh().spec, + kMeshAttrs, kMeshAttrsN); + if (mesh->Material() != def->Mesh().Material()) { + WriteAttrTxt(elem, "material", mesh->Material()); } - WriteAttr(elem, "refpos", 3, mesh->Refpos(), def->Mesh().Refpos()); - WriteAttr(elem, "refquat", 4, mesh->Refquat(), def->Mesh().Refquat()); - WriteAttr(elem, "scale", 3, mesh->Scale(), def->Mesh().Scale()); - WriteAttrKey(elem, "smoothnormal", bool_map, 2, mesh->SmoothNormal(), - def->Mesh().SmoothNormal()); } @@ -272,16 +258,16 @@ void mjXWriter::OneMesh(XMLElement* elem, const mjCMesh* mesh, mjCDef* def) { // write skin void mjXWriter::OneSkin(XMLElement* elem, const mjCSkin* skin) { string text; - mjCDef mydef; - float zero = 0; + mjCSkin defskin; // write attributes WriteAttrTxt(elem, "name", skin->name); WriteAttrTxt(elem, "file", skin->File()); WriteAttrTxt(elem, "material", skin->get_material()); WriteAttrInt(elem, "group", skin->group, 0); - WriteAttr(elem, "rgba", 4, skin->rgba, mydef.Geom().rgba); - WriteAttr(elem, "inflate", 1, &skin->inflate, &zero); + WriteAttrTable(elem, static_cast(skin), + static_cast(&defskin), kSkinAttrs, + kSkinAttrsN); // write data if no file if (skin->File().empty()) { @@ -358,15 +344,8 @@ void mjXWriter::OneMaterial(XMLElement* elem, const mjCMaterial* material, mjCDe } } - WriteAttrKey(elem, "texuniform", bool_map, 2, material->texuniform, def->Material().texuniform); - WriteAttr(elem, "texrepeat", 2, material->texrepeat, def->Material().texrepeat); - WriteAttr(elem, "emission", 1, &material->emission, &def->Material().emission); - WriteAttr(elem, "specular", 1, &material->specular, &def->Material().specular); - WriteAttr(elem, "shininess", 1, &material->shininess, &def->Material().shininess); - WriteAttr(elem, "reflectance", 1, &material->reflectance, &def->Material().reflectance); - WriteAttr(elem, "metallic", 1, &material->metallic, &def->Material().metallic); - WriteAttr(elem, "roughness", 1, &material->roughness, &def->Material().roughness); - WriteAttr(elem, "rgba", 4, material->rgba, def->Material().rgba); + WriteAttrTable(elem, static_cast(material), + &def->Material().spec, kMaterialAttrs, kMaterialAttrsN); } @@ -374,8 +353,6 @@ void mjXWriter::OneMaterial(XMLElement* elem, const mjCMaterial* material, mjCDe // write joint void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def, string_view classname) { - double zero = 0; - // regular if (!writingdefaults) { WriteAttrTxt(elem, "name", joint->name); @@ -391,43 +368,15 @@ void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def, } // defaults and regular - if (joint->type != def->Joint().type) { - WriteAttrTxt(elem, "type", FindValue(jointtype_map, jointtype_sz, joint->type)); - } - WriteAttrInt(elem, "group", joint->group, def->Joint().group); - WriteAttr(elem, "ref", 1, &joint->ref, &zero); - WriteAttr(elem, "springref", 1, &joint->springref, &zero); - WriteAttr(elem, "solreflimit", mjNREF, joint->solref_limit, def->Joint().solref_limit, true); - WriteAttr(elem, "solimplimit", mjNIMP, joint->solimp_limit, def->Joint().solimp_limit, true); - WriteAttr(elem, "solreffriction", mjNREF, joint->solref_friction, def->Joint().solref_friction, - true); - WriteAttr(elem, "solimpfriction", mjNIMP, joint->solimp_friction, def->Joint().solimp_friction, - true); - { - int nstiff = 1+mjNPOLY; - while (nstiff > 1 && joint->stiffness[nstiff-1] == 0 - && def->Joint().stiffness[nstiff-1] == 0) nstiff--; - WriteAttr(elem, "stiffness", nstiff, joint->stiffness, def->Joint().stiffness); - } + WriteAttrTable(elem, static_cast(joint), &def->Joint().spec, + kJointAttrs, kJointAttrsN); if (joint->type != mjJNT_FREE) { WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, joint->limited, def->Joint().limited); } - WriteAttr(elem, "range", 2, joint->range, def->Joint().range); if (joint->type != mjJNT_FREE && joint->type != mjJNT_BALL) { WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, joint->actfrclimited, def->Joint().actfrclimited); } - WriteAttrKey(elem, "actuatorgravcomp", bool_map, 2, joint->actgravcomp, def->Joint().actgravcomp); - WriteAttr(elem, "actuatorfrcrange", 2, joint->actfrcrange, def->Joint().actfrcrange); - WriteAttr(elem, "margin", 1, &joint->margin, &def->Joint().margin); - WriteAttr(elem, "armature", 1, &joint->armature, &def->Joint().armature); - { - int ndamp = 1+mjNPOLY; - while (ndamp > 1 && joint->damping[ndamp-1] == 0 - && def->Joint().damping[ndamp-1] == 0) ndamp--; - WriteAttr(elem, "damping", ndamp, joint->damping, def->Joint().damping); - } - WriteAttr(elem, "frictionloss", 1, &joint->frictionloss, &def->Joint().frictionloss); // userdata if (writingdefaults) { @@ -485,23 +434,10 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri } // defaults and regular - WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, geom->type, def->Geom().type); - WriteAttrInt(elem, "contype", geom->contype, def->Geom().contype); - WriteAttrInt(elem, "conaffinity", geom->conaffinity, def->Geom().conaffinity); - WriteAttrInt(elem, "condim", geom->condim, def->Geom().condim); - WriteAttrInt(elem, "group", geom->group, def->Geom().group); - WriteAttrInt(elem, "priority", geom->priority, def->Geom().priority); - WriteAttr(elem, "friction", 3, geom->friction, def->Geom().friction, true); - WriteAttr(elem, "solmix", 1, &geom->solmix, &def->Geom().solmix); - WriteAttr(elem, "solref", mjNREF, geom->solref, def->Geom().solref, true); - WriteAttr(elem, "solimp", mjNIMP, geom->solimp, def->Geom().solimp, true); - WriteAttr(elem, "margin", 1, &geom->margin, &def->Geom().margin); - WriteAttr(elem, "gap", 1, &geom->gap, &def->Geom().gap); - WriteAttr(elem, "surfacevel", 6, geom->surfacevel, def->Geom().surfacevel, true); - WriteAttr(elem, "adhesion", 1, &geom->adhesion, &def->Geom().adhesion); + WriteAttrTable(elem, static_cast(geom), &def->Geom().spec, + kGeomAttrs, kGeomAttrsN); WriteAttrKey(elem, "fluidshape", fluidshape_map, 2, geom->fluid_ellipsoid, def->Geom().fluid_ellipsoid); - WriteAttr(elem, "fluidcoef", 5, geom->fluid_coefs, def->Geom().fluid_coefs); if (geom->type != mjGEOM_MESH) { WriteAttrKey(elem, "shellinertia", bool_map, 2, geom->typeinertia, def->Geom().typeinertia); @@ -514,7 +450,6 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri if (geom->get_material() != def->Geom().get_material()) { WriteAttrTxt(elem, "material", geom->get_material()); } - WriteAttr(elem, "rgba", 4, geom->rgba, def->Geom().rgba); // hfield and mesh attributes if (geom->type == mjGEOM_HFIELD) { @@ -547,7 +482,6 @@ void mjXWriter::OneSite(XMLElement* elem, const mjCSite* site, mjCDef* def, stri if (classname != site->classname && site->classname != "main") { WriteAttrTxt(elem, "class", site->classname); } - WriteAttr(elem, "pos", 3, site->pos); WriteAttr(elem, "quat", 4, site->quat, unitq); if (mjGEOMINFO[site->type]) { WriteAttr(elem, "size", mjGEOMINFO[site->type], site->size, def->Site().size); @@ -557,12 +491,11 @@ void mjXWriter::OneSite(XMLElement* elem, const mjCSite* site, mjCDef* def, stri } // defaults and regular - WriteAttrInt(elem, "group", site->group, def->Site().group); - WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, site->type, def->Site().type); + WriteAttrTable(elem, static_cast(site), &def->Site().spec, + kSiteAttrs, kSiteAttrsN); if (site->get_material() != def->Site().get_material()) { WriteAttrTxt(elem, "material", site->get_material()); } - WriteAttr(elem, "rgba", 4, site->rgba, def->Site().rgba); // userdata if (writingdefaults) { @@ -584,28 +517,12 @@ void mjXWriter::OneCamera(XMLElement* elem, const mjCCamera* camera, mjCDef* def WriteAttrTxt(elem, "class", camera->classname); } WriteAttrTxt(elem, "target", camera->get_targetbody()); - WriteAttr(elem, "pos", 3, camera->pos); WriteAttr(elem, "quat", 4, camera->quat, unitq); } // defaults and regular - WriteAttr(elem, "ipd", 1, &camera->ipd, &def->Camera().ipd); - WriteAttrKey(elem, "mode", camlight_map, camlight_sz, camera->mode, def->Camera().mode); - WriteAttr(elem, "resolution", 2, camera->resolution, def->Camera().resolution); - - // write output attribute if different from default - if (camera->output != def->Camera().output) { - int data[mjNCAMOUT]; - int ndata = 0; - for (int i = 0; i < mjNCAMOUT; i++) { - if (camera->output & camout_map[i].value) { - data[ndata++] = camout_map[i].value; - } - } - WriteAttrKeys(elem, "output", camout_map, camout_sz, data, ndata, 0); - } - - WriteAttrKey(elem, "projection", projection_map, projection_sz, camera->proj, def->Camera().proj); + WriteAttrTable(elem, static_cast(camera), + &def->Camera().spec, kCameraAttrs, kCameraAttrsN); // camera intrinsics if specified if (camera->sensor_size[0] > 0 && camera->sensor_size[1] > 0) { @@ -636,28 +553,86 @@ void mjXWriter::OneLight(XMLElement* elem, const mjCLight* light, mjCDef* def, WriteAttrTxt(elem, "class", light->classname); } WriteAttrTxt(elem, "target", light->get_targetbody()); - WriteAttr(elem, "pos", 3, light->pos); - WriteAttr(elem, "dir", 3, light->dir); } // defaults and regular - WriteAttr(elem, "bulbradius", 1, &light->bulbradius, &def->Light().bulbradius); - WriteAttr(elem, "intensity", 1, &light->intensity, &def->Light().intensity); - WriteAttr(elem, "range", 1, &light->range, &def->Light().range); + WriteAttrTable(elem, static_cast(light), &def->Light().spec, + kLightAttrs, kLightAttrsN); WriteAttrKey(elem, "type", lighttype_map, lighttype_sz, light->type, def->Light().type); WriteAttrTxt(elem, "texture", light->get_texture()); - WriteAttrKey(elem, "castshadow", bool_map, 2, light->castshadow, def->Light().castshadow); - WriteAttrKey(elem, "active", bool_map, 2, light->active, def->Light().active); - WriteAttr(elem, "attenuation", 3, light->attenuation, def->Light().attenuation); - WriteAttr(elem, "cutoff", 1, &light->cutoff, &def->Light().cutoff); - WriteAttr(elem, "exponent", 1, &light->exponent, &def->Light().exponent); - WriteAttr(elem, "ambient", 3, light->ambient, def->Light().ambient); - WriteAttr(elem, "diffuse", 3, light->diffuse, def->Light().diffuse); - WriteAttr(elem, "specular", 3, light->specular, def->Light().specular); - WriteAttrKey(elem, "mode", camlight_map, camlight_sz, light->mode, def->Light().mode); } // write pair +// write the mechanical attributes of an element, driven by the same +// generated rows the reader uses; see the declaration for the contract +template +void mjXWriter::WriteAttrTable(XMLElement* elem, const T* obj, const T* def, + const mjXAttr* rows, int nrow) { + const char* live = reinterpret_cast(obj); + const char* dflt = reinterpret_cast(def); + for (int i = 0; i < nrow; i++) { + const mjXAttr& row = rows[i]; + if (row.handwrite || (writingdefaults && row.nodefault)) { + continue; + } + const char* base = live + row.offset; + // a null default object means the element has no defaults to compare + // against: every defined value is written + const char* dbase = dflt ? dflt + row.offset : nullptr; + const int dkey = dflt ? 0 : -12345; // WriteAttrKey's write-always default + switch (row.kind) { + case mjXAttr::kInt: + WriteAttr(elem, row.attr, row.len, (const int*)base, (const int*)dbase, + /*trim=*/!row.exact); + break; + case mjXAttr::kDouble: + WriteAttr(elem, row.attr, row.len, (const double*)base, + (const double*)dbase, /*trim=*/!row.exact); + break; + case mjXAttr::kNum: + WriteAttr(elem, row.attr, row.len, (const mjtNum*)base, + (const mjtNum*)dbase, /*trim=*/!row.exact); + break; + case mjXAttr::kFloat: + WriteAttr(elem, row.attr, row.len, (const float*)base, + (const float*)dbase, /*trim=*/!row.exact); + break; + case mjXAttr::kEnum: + WriteAttrKey(elem, row.attr, row.map, row.mapsz, *(const int*)base, + dbase ? *(const int*)dbase : dkey); + break; + case mjXAttr::kEnumByte: + WriteAttrKey(elem, row.attr, row.map, row.mapsz, *(const mjtByte*)base, + dbase ? *(const mjtByte*)dbase : dkey); + break; + case mjXAttr::kBool: + WriteAttrKey(elem, row.attr, bool_map, 2, *(const mjtByte*)base, + dbase ? *(const mjtByte*)dbase : dkey); + break; + case mjXAttr::kFlags: + if (!dbase || *(const int*)base != *(const int*)dbase) { + int value = *(const int*)base; + std::vector data; + for (int j = 0; j < row.mapsz; j++) { + if (value & row.map[j].value) { + data.push_back(row.map[j].value); + } + } + if (!data.empty()) { + WriteAttrKeys(elem, row.attr, row.map, row.mapsz, data.data(), + data.size(), 0); + } + } + break; + default: + // names, strings, files and custom-read attributes: OneX() remnant + break; + } + } +} + + + void mjXWriter::OnePair(XMLElement* elem, const mjCPair* pair, mjCDef* def) { // regular if (!writingdefaults) { @@ -670,15 +645,8 @@ void mjXWriter::OnePair(XMLElement* elem, const mjCPair* pair, mjCDef* def) { // defaults and regular WriteAttrTxt(elem, "name", pair->name); - WriteAttrInt(elem, "condim", pair->condim, def->Pair().spec.condim); - WriteAttr(elem, "margin", 1, &pair->margin, &def->Pair().spec.margin); - WriteAttr(elem, "gap", 1, &pair->gap, &def->Pair().spec.gap); - WriteAttr(elem, "adhesion", 1, &pair->adhesion, &def->Pair().spec.adhesion); - WriteAttr(elem, "solref", mjNREF, pair->solref, def->Pair().spec.solref, true); - WriteAttr(elem, "solreffriction", mjNREF, pair->solreffriction, def->Pair().spec.solreffriction, - true); - WriteAttr(elem, "solimp", mjNIMP, pair->solimp, def->Pair().spec.solimp, true); - WriteAttr(elem, "friction", 5, pair->friction, def->Pair().spec.friction); // all 5 values + WriteAttrTable(elem, static_cast(pair), &def->Pair().spec, + kPairAttrs, kPairAttrsN); } @@ -745,9 +713,9 @@ void mjXWriter::OneEquality(XMLElement* elem, const mjCEquality* equality, mjCDe } // defaults and regular - WriteAttrKey(elem, "active", bool_map, 2, equality->active, def->Equality().active); - WriteAttr(elem, "solref", mjNREF, equality->solref, def->Equality().solref, true); - WriteAttr(elem, "solimp", mjNIMP, equality->solimp, def->Equality().solimp, true); + WriteAttrTable(elem, static_cast(equality), + &def->Equality().spec, kEqualityBaseAttrs, + kEqualityBaseAttrsN); } @@ -764,46 +732,23 @@ void mjXWriter::OneTendon(XMLElement* elem, const mjCTendon* tendon, mjCDef* def } } - // defaults and regular - WriteAttrInt(elem, "group", tendon->group, def->Tendon().group); - WriteAttr(elem, "solreflimit", mjNREF, tendon->solref_limit, def->Tendon().solref_limit, true); - WriteAttr(elem, "solimplimit", mjNIMP, tendon->solimp_limit, def->Tendon().solimp_limit, true); - WriteAttr(elem, "solreffriction", mjNREF, tendon->solref_friction, def->Tendon().solref_friction, - true); - WriteAttr(elem, "solimpfriction", mjNIMP, tendon->solimp_friction, def->Tendon().solimp_friction, - true); - WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, tendon->limited, def->Tendon().limited); - WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, tendon->actfrclimited, def->Tendon().actfrclimited); - WriteAttr(elem, "range", 2, tendon->range, def->Tendon().range); - WriteAttr(elem, "actuatorfrcrange", 2, tendon->actfrcrange, def->Tendon().actfrcrange); - WriteAttr(elem, "margin", 1, &tendon->margin, &def->Tendon().margin); - { - int nstiff = 1+mjNPOLY; - while (nstiff > 1 && tendon->stiffness[nstiff-1] == 0 - && def->Tendon().stiffness[nstiff-1] == 0) nstiff--; - WriteAttr(elem, "stiffness", nstiff, tendon->stiffness, def->Tendon().stiffness); + // defaults and regular; the fixed rows are the spatial rows without the + // appearance attributes, which is exactly the tag difference + if (fixed) { + WriteAttrTable(elem, static_cast(tendon), + &def->Tendon().spec, kFixedAttrs, kFixedAttrsN); + } else { + WriteAttrTable(elem, static_cast(tendon), + &def->Tendon().spec, kSpatialAttrs, kSpatialAttrsN); } - { - int ndamp = 1+mjNPOLY; - while (ndamp > 1 && tendon->damping[ndamp-1] == 0 - && def->Tendon().damping[ndamp-1] == 0) ndamp--; - WriteAttr(elem, "damping", ndamp, tendon->damping, def->Tendon().damping); - } - WriteAttr(elem, "armature", 1, &tendon->armature, &def->Tendon().armature); - WriteAttr(elem, "frictionloss", 1, &tendon->frictionloss, &def->Tendon().frictionloss); if (tendon->springlength[0] != tendon->springlength[1] || def->Tendon().springlength[0] != def->Tendon().springlength[1]) { WriteAttr(elem, "springlength", 2, tendon->springlength, def->Tendon().springlength); } else { WriteAttr(elem, "springlength", 1, tendon->springlength, def->Tendon().springlength); } - // spatial only - if (!fixed) { - if (tendon->get_material() != def->Tendon().get_material()) { - WriteAttrTxt(elem, "material", tendon->get_material()); - } - WriteAttr(elem, "width", 1, &tendon->width, &def->Tendon().width); - WriteAttr(elem, "rgba", 4, tendon->rgba, def->Tendon().rgba); + if (!fixed && tendon->get_material() != def->Tendon().get_material()) { + WriteAttrTxt(elem, "material", tendon->get_material()); } // userdata @@ -859,28 +804,9 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe } // defaults and regular - WriteAttrInt(elem, "group", actuator->group, def->Actuator().group); - WriteAttrInt(elem, "nsample", actuator->nsample, def->Actuator().nsample); - WriteAttrKey(elem, "interp", interp_map, interp_sz, actuator->interp, def->Actuator().interp); - WriteAttr(elem, "delay", 1, &actuator->delay, &def->Actuator().delay); - WriteAttrKey(elem, "ctrllimited", FalseTrueAuto_map, 3, actuator->ctrllimited, def->Actuator().ctrllimited); - WriteAttr(elem, "ctrlrange", 2, actuator->ctrlrange, def->Actuator().ctrlrange); - WriteAttrKey(elem, "forcelimited", FalseTrueAuto_map, 3, actuator->forcelimited, def->Actuator().forcelimited); - WriteAttr(elem, "forcerange", 2, actuator->forcerange, def->Actuator().forcerange); - WriteAttrKey(elem, "actlimited", FalseTrueAuto_map, 3, actuator->actlimited, def->Actuator().actlimited); - WriteAttr(elem, "actrange", 2, actuator->actrange, def->Actuator().actrange); - WriteAttr(elem, "lengthrange", 2, actuator->lengthrange, def->Actuator().lengthrange); - WriteAttr(elem, "gear", 6, actuator->gear, def->Actuator().gear); - { - int ndamp = 1+mjNPOLY; - while (ndamp > 1 && actuator->damping[ndamp-1] == 0 - && def->Actuator().damping[ndamp-1] == 0) ndamp--; - WriteAttr(elem, "damping", ndamp, actuator->damping, def->Actuator().damping); - } - WriteAttr(elem, "armature", 1, &actuator->armature, &def->Actuator().armature); + WriteAttrTable(elem, static_cast(actuator), + &def->Actuator().spec, kGeneralAttrs, kGeneralAttrsN); WriteAttr(elem, "cranklength", 1, &actuator->cranklength, &def->Actuator().cranklength); - WriteAttrKey(elem, "actearly", bool_map, 2, actuator->actearly, - def->Actuator().actearly); // special handling of actdim which has default value of -1 if (writingdefaults) { WriteAttrInt(elem, "actdim", actuator->actdim, def->Actuator().actdim); @@ -888,8 +814,6 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe int default_actdim = (actuator->dyntype != mjDYN_NONE && actuator->dyntype != mjDYN_DCMOTOR); WriteAttrInt(elem, "actdim", actuator->actdim, default_actdim); } - WriteAttrKey(elem, "dyntype", dyn_map, dyn_sz, actuator->dyntype, def->Actuator().dyntype); - WriteAttr(elem, "dynprm", mjNDYN, actuator->dynprm, def->Actuator().dynprm); // plugins: write config attributes if (actuator->plugin.active) { @@ -911,8 +835,6 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe } WriteAttrTxt(elem, "input", tokens); } - WriteAttr(elem, "velrange", 2, actuator->velrange, def->Actuator().velrange); - WriteAttr(elem, "ffrange", 2, actuator->ffrange, def->Actuator().ffrange); WriteAttrKey(elem, "biastype", bias_map, bias_sz, actuator->biastype, def->Actuator().biastype); WriteAttr(elem, "gainprm", mjNGAIN, actuator->gainprm, def->Actuator().gainprm, true); WriteAttr(elem, "biasprm", mjNBIAS, actuator->biasprm, def->Actuator().biasprm, true); @@ -1000,21 +922,21 @@ string mjXWriter::Write(char *error, size_t error_sz) { Compiler(root); Option(root); Size(root); - Visual(root); Statistic(root); + Visual(root); writingdefaults = true; Default(root, model->Default()); writingdefaults = false; Extension(root); - Custom(root); Asset(root); Body(InsertEnd(root, "worldbody"), model->GetWorld(), nullptr); - Contact(root); Deformable(root); - Equality(root); + Contact(root); Tendon(root); + Equality(root); Actuator(root); Sensor(root); + Custom(root); Keyframe(root); return WriteDoc(doc, error, error_sz); @@ -1063,39 +985,8 @@ void mjXWriter::Option(XMLElement* root) { XMLElement* section = InsertEnd(root, "option"); - // option - WriteAttr(section, "timestep", 1, &model->option.timestep, &opt.timestep); - WriteAttr(section, "impratio", 1, &model->option.impratio, &opt.impratio); - WriteAttr(section, "tolerance", 1, &model->option.tolerance, &opt.tolerance); - WriteAttr(section, "ls_tolerance", 1, &model->option.ls_tolerance, &opt.ls_tolerance); - WriteAttr(section, "noslip_tolerance", 1, &model->option.noslip_tolerance, &opt.noslip_tolerance); - WriteAttr(section, "ccd_tolerance", 1, &model->option.ccd_tolerance, &opt.ccd_tolerance); - WriteAttr(section, "sleep_tolerance", 1, &model->option.sleep_tolerance, &opt.sleep_tolerance); - WriteAttr(section, "gravity", 3, model->option.gravity, opt.gravity); - WriteAttr(section, "wind", 3, model->option.wind, opt.wind); - WriteAttr(section, "magnetic", 3, model->option.magnetic, opt.magnetic); - WriteAttr(section, "density", 1, &model->option.density, &opt.density); - WriteAttr(section, "viscosity", 1, &model->option.viscosity, &opt.viscosity); - - WriteAttr(section, "o_margin", 1, &model->option.o_margin, &opt.o_margin); - WriteAttr(section, "o_solref", mjNREF, model->option.o_solref, opt.o_solref); - WriteAttr(section, "o_solimp", mjNIMP, model->option.o_solimp, opt.o_solimp); - WriteAttr(section, "o_friction", 5, model->option.o_friction, opt.o_friction); - - WriteAttrKey(section, "integrator", integrator_map, integrator_sz, - model->option.integrator, opt.integrator); - WriteAttrKey(section, "cone", cone_map, cone_sz, - model->option.cone, opt.cone); - WriteAttrKey(section, "jacobian", jacobian_map, jacobian_sz, - model->option.jacobian, opt.jacobian); - WriteAttrKey(section, "solver", solver_map, solver_sz, - model->option.solver, opt.solver); - WriteAttrInt(section, "iterations", model->option.iterations, opt.iterations); - WriteAttrInt(section, "ls_iterations", model->option.ls_iterations, opt.ls_iterations); - WriteAttrInt(section, "noslip_iterations", model->option.noslip_iterations, opt.noslip_iterations); - WriteAttrInt(section, "ccd_iterations", model->option.ccd_iterations, opt.ccd_iterations); - WriteAttrInt(section, "sdf_iterations", model->option.sdf_iterations, opt.sdf_iterations); - WriteAttrInt(section, "sdf_initpoints", model->option.sdf_initpoints, opt.sdf_initpoints); + // option; the freshly-defaulted struct is the comparison object + WriteAttrTable(section, &model->option, &opt, kOptionAttrs, kOptionAttrsN); // actuator group disable int disabled_groups[31]; @@ -1165,20 +1056,16 @@ void mjXWriter::Size(XMLElement* root) { WriteAttrTxt(section, "memory", mju_writeNumBytes(model->memory)); } - // write sizes + // deprecated sizes, hand-read into locals with range checks WriteAttrInt(section, "njmax", model->njmax, -1); WriteAttrInt(section, "nconmax", model->nconmax, -1); WriteAttrInt(section, "nstack", model->nstack, -1); - WriteAttrInt(section, "nuserdata", model->nuserdata, 0); - WriteAttrInt(section, "nkey", model->nkey, 0); - WriteAttrInt(section, "nuser_body", model->nuser_body, 0); - WriteAttrInt(section, "nuser_jnt", model->nuser_jnt, 0); - WriteAttrInt(section, "nuser_geom", model->nuser_geom, 0); - WriteAttrInt(section, "nuser_site", model->nuser_site, 0); - WriteAttrInt(section, "nuser_cam", model->nuser_cam, 0); - WriteAttrInt(section, "nuser_tendon", model->nuser_tendon, 0); - WriteAttrInt(section, "nuser_actuator", model->nuser_actuator, 0); - WriteAttrInt(section, "nuser_sensor", model->nuser_sensor, 0); + + // write sizes; the spec defaults are -1 (auto), but compilation resolves + // them to the actual counts, so the comparison object is zero-initialized + mjSpec zerospec = {}; + WriteAttrTable(section, static_cast(model), &zerospec, + kSizeAttrs, kSizeAttrsN); // remove entire section if no attributes if (!section->FirstAttribute()) root->DeleteChild(section); @@ -1189,13 +1076,11 @@ void mjXWriter::Size(XMLElement* root) { // statistic section void mjXWriter::Statistic(XMLElement* root) { XMLElement* section = InsertEnd(root, "statistic"); - mjStatistic* s = &model->stat; - if (mjuu_defined(s->meaninertia)) WriteAttr(section, "meaninertia", 1, &s->meaninertia); - if (mjuu_defined(s->meanmass)) WriteAttr(section, "meanmass", 1, &s->meanmass); - if (mjuu_defined(s->meansize)) WriteAttr(section, "meansize", 1, &s->meansize); - if (mjuu_defined(s->extent)) WriteAttr(section, "extent", 1, &s->extent); - if (mjuu_defined(s->center[0])) WriteAttr(section, "center", 3, s->center); + // statistics are unset (mjNAN) rather than defaulted: there is nothing to + // compare against, and WriteAttr skips the undefined values by itself + WriteAttrTable(section, &model->stat, (const mjStatistic*)nullptr, + kStatisticAttrs, kStatisticAttrsN); // remove entire section if no attributes if (!section->FirstAttribute()) root->DeleteChild(section); @@ -1207,123 +1092,25 @@ void mjXWriter::Statistic(XMLElement* root) { void mjXWriter::Visual(XMLElement* root) { mjVisual visdef, *vis = &model->visual; mj_defaultVisual(&visdef); - XMLElement* elem; XMLElement* section = InsertEnd(root, "visual"); - // global - elem = InsertEnd(section, "global"); - WriteAttrInt(elem, "cameraid", vis->global.cameraid, visdef.global.cameraid); - WriteAttrKey(elem, "orthographic", - bool_map, 2, vis->global.orthographic, visdef.global.orthographic); - WriteAttr(elem, "fovy", 1, &vis->global.fovy, &visdef.global.fovy); - WriteAttr(elem, "ipd", 1, &vis->global.ipd, &visdef.global.ipd); - WriteAttr(elem, "azimuth", 1, &vis->global.azimuth, &visdef.global.azimuth); - WriteAttr(elem, "elevation", 1, &vis->global.elevation, &visdef.global.elevation); - WriteAttr(elem, "linewidth", 1, &vis->global.linewidth, &visdef.global.linewidth); - WriteAttr(elem, "glow", 1, &vis->global.glow, &visdef.global.glow); - WriteAttr(elem, "realtime", 1, &vis->global.realtime, &visdef.global.realtime); - WriteAttrInt(elem, "offwidth", vis->global.offwidth, visdef.global.offwidth); - WriteAttrInt(elem, "offheight", vis->global.offheight, visdef.global.offheight); - WriteAttrKey(elem, "ellipsoidinertia", - bool_map, 2, vis->global.ellipsoidinertia, visdef.global.ellipsoidinertia); - WriteAttrKey(elem, "bvactive", bool_map, 2, vis->global.bvactive, visdef.global.bvactive); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // quality - elem = InsertEnd(section, "quality"); - WriteAttrInt(elem, "shadowsize", vis->quality.shadowsize, visdef.quality.shadowsize); - WriteAttrInt(elem, "offsamples", vis->quality.offsamples, visdef.quality.offsamples); - WriteAttrInt(elem, "numslices", vis->quality.numslices, visdef.quality.numslices); - WriteAttrInt(elem, "numstacks", vis->quality.numstacks, visdef.quality.numstacks); - WriteAttrInt(elem, "numquads", vis->quality.numquads, visdef.quality.numquads); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // headlight - elem = InsertEnd(section, "headlight"); - WriteAttr(elem, "ambient", 3, vis->headlight.ambient, visdef.headlight.ambient); - WriteAttr(elem, "diffuse", 3, vis->headlight.diffuse, visdef.headlight.diffuse); - WriteAttr(elem, "specular", 3, vis->headlight.specular, visdef.headlight.specular); - WriteAttrInt(elem, "active", vis->headlight.active, visdef.headlight.active); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // map - elem = InsertEnd(section, "map"); - WriteAttr(elem, "stiffness", 1, &vis->map.stiffness, &visdef.map.stiffness); - WriteAttr(elem, "stiffnessrot", 1, &vis->map.stiffnessrot, &visdef.map.stiffnessrot); - WriteAttr(elem, "force", 1, &vis->map.force, &visdef.map.force); - WriteAttr(elem, "torque", 1, &vis->map.torque, &visdef.map.torque); - WriteAttr(elem, "alpha", 1, &vis->map.alpha, &visdef.map.alpha); - WriteAttr(elem, "fogstart", 1, &vis->map.fogstart, &visdef.map.fogstart); - WriteAttr(elem, "fogend", 1, &vis->map.fogend, &visdef.map.fogend); - WriteAttr(elem, "znear", 1, &vis->map.znear, &visdef.map.znear); - WriteAttr(elem, "zfar", 1, &vis->map.zfar, &visdef.map.zfar); - WriteAttr(elem, "haze", 1, &vis->map.haze, &visdef.map.haze); - WriteAttr(elem, "shadowclip", 1, &vis->map.shadowclip, &visdef.map.shadowclip); - WriteAttr(elem, "shadowscale", 1, &vis->map.shadowscale, &visdef.map.shadowscale); - WriteAttr(elem, "actuatortendon", 1, &vis->map.actuatortendon, &visdef.map.actuatortendon); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // scale - elem = InsertEnd(section, "scale"); - WriteAttr(elem, "forcewidth", 1, &vis->scale.forcewidth, &visdef.scale.forcewidth); - WriteAttr(elem, "contactwidth", 1, &vis->scale.contactwidth, &visdef.scale.contactwidth); - WriteAttr(elem, "contactheight", 1, &vis->scale.contactheight, &visdef.scale.contactheight); - WriteAttr(elem, "connect", 1, &vis->scale.connect, &visdef.scale.connect); - WriteAttr(elem, "com", 1, &vis->scale.com, &visdef.scale.com); - WriteAttr(elem, "camera", 1, &vis->scale.camera, &visdef.scale.camera); - WriteAttr(elem, "light", 1, &vis->scale.light, &visdef.scale.light); - WriteAttr(elem, "selectpoint", 1, &vis->scale.selectpoint, &visdef.scale.selectpoint); - WriteAttr(elem, "jointlength", 1, &vis->scale.jointlength, &visdef.scale.jointlength); - WriteAttr(elem, "jointwidth", 1, &vis->scale.jointwidth, &visdef.scale.jointwidth); - WriteAttr(elem, "actuatorlength", 1, &vis->scale.actuatorlength, &visdef.scale.actuatorlength); - WriteAttr(elem, "actuatorwidth", 1, &vis->scale.actuatorwidth, &visdef.scale.actuatorwidth); - WriteAttr(elem, "framelength", 1, &vis->scale.framelength, &visdef.scale.framelength); - WriteAttr(elem, "framewidth", 1, &vis->scale.framewidth, &visdef.scale.framewidth); - WriteAttr(elem, "constraint", 1, &vis->scale.constraint, &visdef.scale.constraint); - WriteAttr(elem, "slidercrank", 1, &vis->scale.slidercrank, &visdef.scale.slidercrank); - WriteAttr(elem, "frustum", 1, &vis->scale.frustum, &visdef.scale.frustum); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // rgba - elem = InsertEnd(section, "rgba"); - WriteAttr(elem, "fog", 4, vis->rgba.fog, visdef.rgba.fog); - WriteAttr(elem, "haze", 4, vis->rgba.haze, visdef.rgba.haze); - WriteAttr(elem, "force", 4, vis->rgba.force, visdef.rgba.force); - WriteAttr(elem, "inertia", 4, vis->rgba.inertia, visdef.rgba.inertia); - WriteAttr(elem, "joint", 4, vis->rgba.joint, visdef.rgba.joint); - WriteAttr(elem, "actuator", 4, vis->rgba.actuator, visdef.rgba.actuator); - WriteAttr(elem, "actuatornegative", 4, vis->rgba.actuatornegative, visdef.rgba.actuatornegative); - WriteAttr(elem, "actuatorpositive", 4, vis->rgba.actuatorpositive, visdef.rgba.actuatorpositive); - WriteAttr(elem, "com", 4, vis->rgba.com, visdef.rgba.com); - WriteAttr(elem, "camera", 4, vis->rgba.camera, visdef.rgba.camera); - WriteAttr(elem, "light", 4, vis->rgba.light, visdef.rgba.light); - WriteAttr(elem, "selectpoint", 4, vis->rgba.selectpoint, visdef.rgba.selectpoint); - WriteAttr(elem, "connect", 4, vis->rgba.connect, visdef.rgba.connect); - WriteAttr(elem, "contactpoint", 4, vis->rgba.contactpoint, visdef.rgba.contactpoint); - WriteAttr(elem, "contactforce", 4, vis->rgba.contactforce, visdef.rgba.contactforce); - WriteAttr(elem, "contactfriction", 4, vis->rgba.contactfriction, visdef.rgba.contactfriction); - WriteAttr(elem, "contacttorque", 4, vis->rgba.contacttorque, visdef.rgba.contacttorque); - WriteAttr(elem, "contactgap", 4, vis->rgba.contactgap, visdef.rgba.contactgap); - WriteAttr(elem, "rangefinder", 4, vis->rgba.rangefinder, visdef.rgba.rangefinder); - WriteAttr(elem, "constraint", 4, vis->rgba.constraint, visdef.rgba.constraint); - WriteAttr(elem, "slidercrank", 4, vis->rgba.slidercrank, visdef.rgba.slidercrank); - WriteAttr(elem, "crankbroken", 4, vis->rgba.crankbroken, visdef.rgba.crankbroken); - WriteAttr(elem, "frustum", 4, vis->rgba.frustum, visdef.rgba.frustum); - WriteAttr(elem, "bv", 4, vis->rgba.bv, visdef.rgba.bv); - WriteAttr(elem, "bvactive", 4, vis->rgba.bvactive, visdef.rgba.bvactive); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); + // the sub-sections are projections into mjVisual: their rows carry + // member-path offsets, so one struct pair drives them all + struct { const char* tag; const mjXAttr* rows; int n; } subs[] = { + {"global", kGlobalAttrs, kGlobalAttrsN}, + {"quality", kQualityAttrs, kQualityAttrsN}, + {"headlight", kHeadlightAttrs, kHeadlightAttrsN}, + {"map", kMapAttrs, kMapAttrsN}, + {"scale", kScaleAttrs, kScaleAttrsN}, + {"rgba", kRgbaAttrs, kRgbaAttrsN}, + }; + for (const auto& sub : subs) { + XMLElement* elem = InsertEnd(section, sub.tag); + WriteAttrTable(elem, vis, &visdef, sub.rows, sub.n); + if (!elem->FirstAttribute()) { + section->DeleteChild(elem); + } } // remove entire section if no elements diff --git a/src/xml/xml_native_writer.h b/src/xml/xml_native_writer.h index 09962522..7b31fff5 100644 --- a/src/xml/xml_native_writer.h +++ b/src/xml/xml_native_writer.h @@ -62,6 +62,16 @@ class mjXWriter : public mjXBase { // body/world section void Body(tinyxml2::XMLElement* elem, mjCBody* body, mjCFrame* frame, std::string_view childclass = ""); + // table-driven attribute writing: the mechanical attributes of an element, + // driven by the same generated mjXAttr rows the reader uses. obj and def + // are the element's bound spec struct and its class-default counterpart; + // each bound field is compared against the default at the same offset, so + // attributes equal to their default are skipped. Strings, files and + // custom-read attributes remain in the OneX() remnants. + template + void WriteAttrTable(tinyxml2::XMLElement* elem, const T* obj, const T* def, + const struct mjXAttr* rows, int nrow); + // single element writers, used in defaults and main body void OneFlex(tinyxml2::XMLElement* elem, const mjCFlex* pflex); void OneMesh(tinyxml2::XMLElement* elem, const mjCMesh* pmesh, mjCDef* def); diff --git a/src/xml/xml_util.cc b/src/xml/xml_util.cc index aa45b429..64119458 100644 --- a/src/xml/xml_util.cc +++ b/src/xml/xml_util.cc @@ -1270,6 +1270,10 @@ void mjXUtil::WriteAttrKey(XMLElement* elem, std::string name, // write attribute- space-separated keywords void mjXUtil::WriteAttrKeys(XMLElement* elem, std::string name, const mjMap* map, int mapsz, int* data, int ndata, int def) { + if (ndata <= 0) { + return; + } + // skip default if (ndata == 1 && data[0] == def) { return; diff --git a/test/engine/engine_plugin_test.cc b/test/engine/engine_plugin_test.cc index 4299c39a..853556a7 100644 --- a/test/engine/engine_plugin_test.cc +++ b/test/engine/engine_plugin_test.cc @@ -551,9 +551,11 @@ TEST_F(EnginePluginTest, SaveXml) { int actuator_end = expected_xml.find(actuator_close) + actuator_close.size(); ASSERT_NE(actuator_end, std::string::npos); ASSERT_LE(actuator_start, actuator_end); + // saved attributes follow schema order; dynprm round-trips as written auto expected_actuator_section = absl::StrReplaceAll( expected_xml.substr(actuator_start, actuator_end - actuator_start), - {{"dynprm=\"0.9\"", "dynprm=\"0.9 0 0 0 0 0 0 0 0 0\""}}); + {{"actdim=\"4\" dyntype=\"filter\" dynprm=\"0.9\"", + "dyntype=\"filter\" dynprm=\"0.9\" actdim=\"4\""}}); EXPECT_THAT(saved_xml, HasSubstr(expected_extension_section)); EXPECT_THAT(saved_xml, HasSubstr(expected_sensor_section)); diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index ca305bdc..11fee00f 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -324,7 +324,7 @@ TEST_F(XMLWriterTest, KeepsJointLimitedFalseIfAutoLimits) { MjModelPtr model = LoadModelFromString(xml); ASSERT_THAT(model.get(), NotNull()); std::string saved_xml = SaveAndReadXml(model.get()); - EXPECT_THAT(saved_xml, HasSubstr("limited=\"false\" range=\"-1 1\"")); + EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\" limited=\"false\"")); } TEST_F(XMLWriterTest, DoesNotKeepInferredTendonLimited) { @@ -537,7 +537,7 @@ TEST_F(XMLWriterTest, KeepsCtrllimitedFalse) { MjModelPtr model = LoadModelFromString(xml); ASSERT_THAT(model.get(), NotNull()); std::string saved_xml = SaveAndReadXml(model.get()); - EXPECT_THAT(saved_xml, HasSubstr("ctrllimited=\"false\" ctrlrange=\"-1 1\"")); + EXPECT_THAT(saved_xml, HasSubstr("ctrlrange=\"-1 1\" ctrllimited=\"false\"")); } TEST_F(XMLWriterTest, DoesNotKeepInferredForcelimited) { @@ -702,8 +702,8 @@ TEST_F(XMLWriterTest, WritesActuatorDefaults) { ASSERT_THAT(model.get(), NotNull()); std::string saved_xml = SaveAndReadXml(model.get()); EXPECT_THAT(saved_xml, Not(HasSubstr("mass"))); - EXPECT_THAT(saved_xml, HasSubstr( - " - + @@ -930,8 +930,8 @@ TEST_F(XMLWriterTest, WritesSkin) { EXPECT_THAT(model->nskin, 1); char error[1024]; - MjModelPtr mtemp = LoadModelFromString(SaveAndReadXml(model.get()), - error, sizeof(error)); + MjModelPtr mtemp = + LoadModelFromString(SaveAndReadXml(model.get()), error, sizeof(error)); ASSERT_THAT(mtemp.get(), NotNull()) << error; EXPECT_THAT(mtemp->nskin, 1); } @@ -950,7 +950,7 @@ TEST_F(XMLWriterTest, WritesHfield) { // load model MjModelPtr model = LoadModelFromString(xml); ASSERT_THAT(model.get(), NotNull()); - int size = model->hfield_nrow[0]*model->hfield_ncol[0]; + int size = model->hfield_nrow[0] * model->hfield_ncol[0]; EXPECT_EQ(size, 6); // check that the data is normalized and in row-major, bottom-to-top order @@ -1256,9 +1256,7 @@ class XMLWriterLocaleTest : public MujocoTest { } } - void TearDown() override { - std::setlocale(LC_ALL, old_locale_.c_str()); - } + void TearDown() override { std::setlocale(LC_ALL, old_locale_.c_str()); } private: std::string old_locale_; @@ -1304,7 +1302,6 @@ TEST_F(XMLWriterTest, NonRGBTextures) { mj_deleteModel(model); } - // ---------------- test CopyBack functionality (decompiler) ------------------ using DecompilerTest = MujocoTest; TEST_F(DecompilerTest, SavesStatistics) { @@ -1469,7 +1466,8 @@ TEST_F(XMLWriterTest, ExpandAttach) { mj_addBufferVFS(vfs.get(), "b.xml", xml_child, sizeof(xml_child)); std::array er; - MjModelPtr m = LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get()); + MjModelPtr m = + LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get()); ASSERT_THAT(m.get(), NotNull()) << er.data(); std::string saved_xml = SaveAndReadXml(m.get()); @@ -1626,5 +1624,19 @@ TEST_F(XMLWriterTest, BodySimpleRoundtrip) { EXPECT_GT(model->nC, model_auto->nC); } +TEST_F(XMLWriterTest, EmptyFlagsAttribute) { + static constexpr char xml[] = R"( + + + + + + )"; + MjModelPtr model = LoadModelFromString(xml); + ASSERT_THAT(model.get(), NotNull()); + std::string saved_xml = SaveAndReadXml(model.get()); + EXPECT_THAT(saved_xml, Not(HasSubstr("output="))); +} + } // namespace } // namespace mujoco