diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index f0960356..c5cade98 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -3154,9 +3154,10 @@ unit quaternions. the desired time constant (first value) and damping ratio (second value). This is done by taking into account the joint inertia in the model reference configuration. Note that the format is the same as the solref parameter of the constraint solver. -:at:`limited`: :at-val:`[false, true], "false"` - This attribute specifies if the joint has limits. It interacts with the range attribute below. Both must be set to - enable joint limits. If this attribute is "false", any joint range data will be ignored. +:at:`limited`: :at-val:`[false, true, auto], "auto"` + This attribute specifies if the joint has limits. It interacts with the range attribute below. If this attribute + is "false", joint limits are disabled. If this attribute is "true", joint limits are enabled. If this + attribute is "auto", joint limits will be enabled if range is defined and disabled otherwise. :at:`solreflimit`, :at:`solimplimit` Constraint solver parameters for simulating joint limits. See :ref:`CSolver`. :at:`solreffriction`, :at:`solimpfriction` @@ -3167,7 +3168,7 @@ unit quaternions. :at:`range`: :at-val:`real(2), "0 0"` The joint limits. Limits can be imposed on all joint types except for free joints. For hinge and ball joints, the range is specified in degrees or radians depending on the angle attribute of :ref:`compiler `. For ball - joints, the limit is imposed on the angle of rotation (relative to the the reference configuration) regardless of the + joints, the limit is imposed on the angle of rotation (relative to the reference configuration) regardless of the axis of rotation. Only the second range parameter is used for ball joints; the first range parameter should be set to 0. See the :ref:`Limit ` section in the Computation chapter for more information. :at:`margin`: :at-val:`real, "0"` @@ -4103,9 +4104,9 @@ the obstacle geom. :at:`group`: :at-val:`int, "0"` Integer group to which the tendon belongs. This attribute can be used for custom tags. It is also used by the visualizer to enable and disable the rendering of entire groups of tendons. -:at:`limited`: :at-val:`[false, true], "false"` +:at:`limited`: :at-val:`[false, true, auto], "auto"` If this attribute is "true", the length limits defined by the range attribute below are imposed by the constraint - solver. + solver. If this attribute is "auto", length limits will be enabled if range is defined and disabled otherwise. :at:`range`: :at-val:`real(2), "0 0"` Range of allowed tendon lengths. To enable length limits, set the limited attribute to "true" in addition to defining the present value. @@ -4250,17 +4251,20 @@ specify them independently. :at:`group`: :at-val:`int, "0"` Integer group to which the actuator belongs. This attribute can be used for custom tags. It is also used by the visualizer to enable and disable the rendering of entire groups of actuators. -:at:`ctrllimited`: :at-val:`[false, true], "false"` +:at:`ctrllimited`: :at-val:`[false, true, auto], "auto"` If true, the control input to this actuator is automatically clamped to :at:`ctrlrange` at runtime. If false, control - input clamping is disabled. Note that control input clamping can also be globally disabled with the :at:`clampctrl` - attribute of :ref:`option/flag `. -:at:`forcelimited`: :at-val:`[false, true], "false"` + input clamping is disabled. If auto, control clamping will automatically be set to true if :at:`ctrlrange` is + defined without explicitly setting this attribute to "true". Note that control input clamping can also be globally + disabled with the :at:`clampctrl` attribute of :ref:`option/flag `. +:at:`forcelimited`: :at-val:`[false, true, auto], "auto"` If true, the force output of this actuator is automatically clamped to :at:`forcerange` at runtime. If false, force - clamping is disabled. -:at:`actlimited`: :at-val:`[false, true], "false"` + clamping is disabled. If auto, force clamping will automatically be set to true if :at:`forcerange` is + defined without explicitly setting this attribute to "true". +:at:`actlimited`: :at-val:`[false, true, auto], "auto"` If true, the internal state (activation) associated with this actuator is automatically clamped to :at:`actrange` at - runtime. If false, activation clamping is disabled. See the :ref:`Activation clamping ` section for more - details. + runtime. If false, activation clamping is disabled. If auto, activation clamping will automatically be set to true + if :at:`actrange` is defined without explicitly setting this attribute to "true". See the :ref:`Activation clamping ` + section for more details. :at:`ctrlrange`: :at-val:`real(2), "0 0"` Range for clamping the control input. The compiler expects the first value to be smaller than the second value. :at:`forcerange`: :at-val:`real(2), "0 0"` diff --git a/doc/changelog.rst b/doc/changelog.rst index 6b6e9314..602315a7 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -20,6 +20,13 @@ General the free camera at model load time. - Added ``mjv_defaultFreeCamera`` which sets the default free camera, respecting the above attributes. - ``simulate`` now supports taking a screenshot via a button in the File section or via ``Ctrl-P``. +- Joint and tendon ``limited`` attribute and actuator ``ctrllimited``, ``forcelimited`` and ``actlimited`` attributes + now default to ``"auto"``, which means they are automatically set to ``true`` if the corresponding range is + defined and disabled otherwise. + + .. attention:: + This is a minor breaking change. Code where a range was defined and limited was unspecified will now + be limited. Please explicitly set limited to ``false`` to revert to the previous behavior. Deleted/deprecated features ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index d7d49905..e8a068b4 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -797,7 +797,7 @@ mjCJoint::mjCJoint(mjCModel* _model, mjCDef* _def) { group = 0; mjuu_setvec(pos, 0, 0, 0); mjuu_setvec(axis, 0, 0, 1); - limited = false; + limited = 2; stiffness = 0; range[0] = 0; range[1] = 0; @@ -851,6 +851,15 @@ int mjCJoint::Compile(void) { } } + // free joints cannot be limited + if (type==mjJNT_FREE) { + limited = 0; + } + // otherwise if limited is auto, set according to whether range is specified + else if (limited==2) { + limited = (range[0]==0 && range[1]==0) ? 0 : 1; + } + // resolve limits if (limited) { // check data @@ -2975,7 +2984,7 @@ mjCTendon::mjCTendon(mjCModel* _model, mjCDef* _def) { group = 0; material.clear(); width = 0.003; - limited = false; + limited = 2; range[0] = 0; range[1] = 0; mj_defaultSolRefImp(solref_limit, solimp_limit); @@ -3205,6 +3214,11 @@ void mjCTendon::Compile(void) { } } + // if limited is auto, set to 1 if range is specified, otherwise unlimited + if (limited==2) { + limited = (range[0]==0 && range[1]==0) ? 0 : 1; + } + // check limits if (range[0]>=range[1] && limited) { throw mjCError(this, "invalid limits in tendon '%s (id = %d)'", name.c_str(), id); @@ -3317,9 +3331,9 @@ void mjCWrap::Compile(void) { mjCActuator::mjCActuator(mjCModel* _model, mjCDef* _def) { // actuator defaults group = 0; - ctrllimited = false; - forcelimited = false; - actlimited = false; + ctrllimited = 2; + forcelimited = 2; + actlimited = 2; trntype = mjTRN_UNDEFINED; dyntype = mjDYN_NONE; gaintype = mjGAIN_FIXED; @@ -3366,6 +3380,17 @@ void mjCActuator::Compile(void) { } userdata.resize(model->nuser_actuator); + // if limited is auto, set to 1 if range is specified, otherwise unlimited + if (forcelimited==2) { + forcelimited = (forcerange[0]==0 && forcerange[1]==0) ? 0 : 1; + } + if (ctrllimited==2) { + ctrllimited = (ctrlrange[0]==0 && ctrlrange[1]==0) ? 0 : 1; + } + if (actlimited==2) { + actlimited = (actrange[0]==0 && actrange[1]==0) ? 0 : 1; + } + // check limits if (forcerange[0]>=forcerange[1] && forcelimited) { throw mjCError(this, "invalid force range for actuator '%s' (id = %d)", name.c_str(), id); @@ -3437,7 +3462,7 @@ void mjCActuator::Compile(void) { if (pjnt->urdfeffort>0) { forcerange[0] = -pjnt->urdfeffort; forcerange[1] = pjnt->urdfeffort; - forcelimited = true; + forcelimited = 1; } break; diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 3fa44217..d61c47c6 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -228,7 +228,7 @@ class mjCJoint : public mjCBase { // variables set by user: joint properties mjtJoint type; // type of Joint int group; // used for rendering - bool limited; // does joint have limits + int limited; // does joint have limits: 0 false, 1 true, 2 auto double pos[3]; // anchor position double axis[3]; // joint axis double stiffness; // stiffness coefficient @@ -783,7 +783,7 @@ class mjCTendon : public mjCBase { // variables set by user int group; // group for visualization std::string material; // name of material for rendering - bool limited; // does tendon have limits + int limited; // does tendon have limits: 0 false, 1 true, 2 auto double width; // width for rendering mjtNum solref_limit[mjNREF]; // solver reference: tendon limits mjtNum solimp_limit[mjNIMP]; // solver impedance: tendon limits @@ -843,9 +843,9 @@ class mjCActuator : public mjCBase { public: // variables set by user or API int group; // group for visualization - bool ctrllimited; // are control limits defined - bool forcelimited; // are force limits defined - bool actlimited; // are activation limits defined + int ctrllimited; // are control limits defined: 0 false, 1 true, 2 auto + int forcelimited; // are force limits defined: 0 false, 1 true, 2 auto + int actlimited; // are activation limits defined: 0 false, 1 true, 2 auto mjtDyn dyntype; // dynamics type mjtTrn trntype; // transmission type mjtGain gaintype; // gain type diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 488ecc03..538fc2ca 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -1092,9 +1092,7 @@ void mjXReader::OneJoint(XMLElement* elem, mjCJoint* pjoint) { if (MapValue(elem, "type", &n, joint_map, joint_sz)) { pjoint->type = (mjtJoint)n; } - if (MapValue(elem, "limited", &n, bool_map, 2)) { - pjoint->limited = (n==1); - } + MapValue(elem, "limited", &pjoint->limited, TFAuto_map, 3); ReadAttrInt(elem, "group", &pjoint->group); ReadAttr(elem, "solreflimit", mjNREF, pjoint->solref_limit, text, false, false); ReadAttr(elem, "solimplimit", mjNIMP, pjoint->solimp_limit, text, false, false); @@ -1350,7 +1348,6 @@ void mjXReader::OneEquality(XMLElement* elem, mjCEquality* pequality) { // tendon element parser void mjXReader::OneTendon(XMLElement* elem, mjCTendon* pten) { - int n; string text; // read attributes @@ -1358,9 +1355,7 @@ void mjXReader::OneTendon(XMLElement* elem, mjCTendon* pten) { ReadAttrTxt(elem, "class", pten->classname); ReadAttrInt(elem, "group", &pten->group); ReadAttrTxt(elem, "material", pten->material); - if (MapValue(elem, "limited", &n, bool_map, 2)) { - pten->limited = (n==1); - } + MapValue(elem, "limited", &pten->limited, TFAuto_map, 3); ReadAttr(elem, "width", 1, &pten->width, text); ReadAttr(elem, "solreflimit", mjNREF, pten->solref_limit, text, false, false); ReadAttr(elem, "solimplimit", mjNIMP, pten->solimp_limit, text, false, false); @@ -1392,15 +1387,9 @@ void mjXReader::OneActuator(XMLElement* elem, mjCActuator* pact) { ReadAttrTxt(elem, "name", pact->name); ReadAttrTxt(elem, "class", pact->classname); ReadAttrInt(elem, "group", &pact->group); - if (MapValue(elem, "ctrllimited", &n, bool_map, 2)) { - pact->ctrllimited = (n==1); - } - if (MapValue(elem, "forcelimited", &n, bool_map, 2)) { - pact->forcelimited = (n==1); - } - if (MapValue(elem, "actlimited", &n, bool_map, 2)) { - pact->actlimited = (n==1); - } + MapValue(elem, "ctrllimited", &pact->ctrllimited, TFAuto_map, 3); + MapValue(elem, "forcelimited", &pact->forcelimited, TFAuto_map, 3); + MapValue(elem, "actlimited", &pact->actlimited, TFAuto_map, 3); ReadAttr(elem, "ctrlrange", 2, pact->ctrlrange, text); ReadAttr(elem, "forcerange", 2, pact->forcerange, text); ReadAttr(elem, "actrange", 2, pact->actrange, text); @@ -1518,7 +1507,7 @@ void mjXReader::OneActuator(XMLElement* elem, mjCActuator* pact) { pact->dyntype = mjDYN_INTEGRATOR; pact->gaintype = mjGAIN_FIXED; pact->biastype = mjBIAS_AFFINE; - pact->actlimited = true; + pact->actlimited = 1; pact->biasprm[1] = -pact->gainprm[0]; // require actrange if (!ReadAttr(elem, "actrange", 2, pact->actrange, text)) { @@ -1544,7 +1533,7 @@ void mjXReader::OneActuator(XMLElement* elem, mjCActuator* pact) { } // implied parameters - pact->ctrllimited = true; + pact->ctrllimited = 1; pact->dyntype = mjDYN_NONE; pact->gaintype = mjGAIN_AFFINE; pact->biastype = mjBIAS_NONE; @@ -1622,7 +1611,7 @@ void mjXReader::OneActuator(XMLElement* elem, mjCActuator* pact) { } // implied parameters - pact->ctrllimited = true; + pact->ctrllimited = 1; pact->dyntype = mjDYN_NONE; pact->gaintype = mjGAIN_FIXED; pact->biastype = mjBIAS_NONE; @@ -1728,9 +1717,7 @@ void mjXReader::OneComposite(XMLElement* elem, mjCBody* pbody, mjCDef* def) { ReadAttr(ejnt, "solimpfix", mjNIMP, comp.def[kind].equality.solimp, text, false, false); // joint attributes - if (MapValue(ejnt, "limited", &n, bool_map, 2)) { - comp.def[kind].joint.limited = (n==1); - } + MapValue(elem, "limited", &comp.def[kind].joint.limited, TFAuto_map, 3); ReadAttrInt(ejnt, "group", &comp.def[kind].joint.group); ReadAttr(ejnt, "solreflimit", mjNREF, comp.def[kind].joint.solref_limit, text, false, false); ReadAttr(ejnt, "solimplimit", mjNIMP, comp.def[kind].joint.solimp_limit, text, false, false); @@ -1762,9 +1749,7 @@ void mjXReader::OneComposite(XMLElement* elem, mjCBody* pbody, mjCDef* def) { ReadAttr(eten, "solimpfix", mjNIMP, comp.def[kind].equality.solimp, text, false, false); // tendon attributes - if (MapValue(eten, "limited", &n, bool_map, 2)) { - comp.def[kind].tendon.limited = (n==1); - } + MapValue(elem, "limited", &comp.def[kind].tendon.limited, TFAuto_map, 3); ReadAttrInt(eten, "group", &comp.def[kind].tendon.group); ReadAttr(eten, "solreflimit", mjNREF, comp.def[kind].tendon.solref_limit, text, false, false); ReadAttr(eten, "solimplimit", mjNIMP, comp.def[kind].tendon.solimp_limit, text, false, false); diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 886e4a91..e22280b6 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -207,6 +207,13 @@ void mjXWriter::OneJoint(XMLElement* elem, mjCJoint* pjoint, mjCDef* def) { } } + // special handling of limits + bool range_defined = pjoint->range[0]!=0 || pjoint->range[1]!=0; + bool limited_inferred = def->joint.limited==2 && pjoint->limited==range_defined; + if (writingdefaults || !limited_inferred) { + WriteAttrKey(elem, "limited", TFAuto_map, 3, pjoint->limited, def->joint.limited); + } + // defaults and regular if (pjoint->type != def->joint.type) { WriteAttrTxt(elem, "type", FindValue(joint_map, joint_sz, pjoint->type)); @@ -214,7 +221,6 @@ void mjXWriter::OneJoint(XMLElement* elem, mjCJoint* pjoint, mjCDef* def) { WriteAttrInt(elem, "group", pjoint->group, def->joint.group); WriteAttr(elem, "ref", 1, &pjoint->ref, &zero); WriteAttr(elem, "springref", 1, &pjoint->springref, &zero); - WriteAttrKey(elem, "limited", bool_map, 2, pjoint->limited, def->joint.limited); WriteAttr(elem, "solreflimit", mjNREF, pjoint->solref_limit, def->joint.solref_limit); WriteAttr(elem, "solimplimit", mjNIMP, pjoint->solimp_limit, def->joint.solimp_limit); WriteAttr(elem, "solreffriction", mjNREF, pjoint->solref_friction, def->joint.solref_friction); @@ -491,9 +497,15 @@ void mjXWriter::OneTendon(XMLElement* elem, mjCTendon* pten, mjCDef* def) { WriteAttrTxt(elem, "class", pten->classname); } + // special handling of limits + bool range_defined = pten->range[0]!=0 || pten->range[1]!=0; + bool limited_inferred = def->tendon.limited==2 && pten->limited==range_defined; + if (writingdefaults || !limited_inferred) { + WriteAttrKey(elem, "limited", TFAuto_map, 3, pten->limited, def->tendon.limited); + } + // defaults and regular WriteAttrInt(elem, "group", pten->group, def->tendon.group); - WriteAttrKey(elem, "limited", bool_map, 2, pten->limited, def->tendon.limited); WriteAttr(elem, "solreflimit", mjNREF, pten->solref_limit, def->tendon.solref_limit); WriteAttr(elem, "solimplimit", mjNIMP, pten->solimp_limit, def->tendon.solimp_limit); WriteAttr(elem, "solreffriction", mjNREF, pten->solref_friction, def->tendon.solref_friction); @@ -563,11 +575,26 @@ void mjXWriter::OneActuator(XMLElement* elem, mjCActuator* pact, mjCDef* def) { } } + // special handling of limits + bool range_defined, limited_inferred; + range_defined = pact->ctrlrange[0]!=0 || pact->ctrlrange[1]!=0; + limited_inferred = def->actuator.ctrllimited==2 && pact->ctrllimited==range_defined; + if (writingdefaults || !limited_inferred) { + WriteAttrKey(elem, "ctrllimited", TFAuto_map, 3, pact->ctrllimited, def->actuator.ctrllimited); + } + range_defined = pact->forcerange[0]!=0 || pact->forcerange[1]!=0; + limited_inferred = def->actuator.forcelimited==2 && pact->forcelimited==range_defined; + if (writingdefaults || !limited_inferred) { + WriteAttrKey(elem, "forcelimited", TFAuto_map, 3, pact->forcelimited, def->actuator.forcelimited); + } + range_defined = pact->actrange[0]!=0 || pact->actrange[1]!=0; + limited_inferred = def->actuator.actlimited==2 && pact->actlimited==range_defined; + if (writingdefaults || !limited_inferred) { + WriteAttrKey(elem, "actlimited", TFAuto_map, 3, pact->actlimited, def->actuator.actlimited); + } + // defaults and regular WriteAttrInt(elem, "group", pact->group, def->actuator.group); - WriteAttrKey(elem, "ctrllimited", bool_map, 2, pact->ctrllimited, def->actuator.ctrllimited); - WriteAttrKey(elem, "forcelimited", bool_map, 2, pact->forcelimited, def->actuator.forcelimited); - WriteAttrKey(elem, "actlimited", bool_map, 2, pact->actlimited, def->actuator.actlimited); WriteAttr(elem, "ctrlrange", 2, pact->ctrlrange, def->actuator.ctrlrange); WriteAttr(elem, "forcerange", 2, pact->forcerange, def->actuator.forcerange); WriteAttr(elem, "actrange", 2, pact->actrange, def->actuator.actrange); diff --git a/test/user/testdata/auto_limits.xml b/test/user/testdata/auto_limits.xml new file mode 100644 index 00000000..e7995658 --- /dev/null +++ b/test/user/testdata/auto_limits.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/user/user_objects_test.cc b/test/user/user_objects_test.cc index 5fa9224d..d9631aec 100644 --- a/test/user/user_objects_test.cc +++ b/test/user/user_objects_test.cc @@ -39,7 +39,7 @@ using ::testing::NotNull; // ------------------------ test keyframes ------------------------------------- -static const char* const kKeyframePath = "user/testdata/keyframe.xml"; +constexpr char kKeyframePath[] = "user/testdata/keyframe.xml"; TEST_F(MujocoTest, KeyFrameTest) { const std::string xml_path = GetTestDataFilePath(kKeyframePath); @@ -567,5 +567,25 @@ TEST_F(UserDataTest, NSensorTooSmall) { EXPECT_THAT(error.data(), HasSubstr("nuser_sensor")); } +// ------------- test for auto parsing of *limited fields ------------- + +using LimitedTest = MujocoTest; + +constexpr char kKeyAutoLimits[] = "user/testdata/auto_limits.xml"; + +// check joint limit values when automatically inferred based on range +TEST_F(LimitedTest, JointLimited) { + const std::string xml_path = GetTestDataFilePath(kKeyAutoLimits); + mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); + ASSERT_THAT(model, NotNull()); + + // see `user/testdata/auto_limits.xml` for expected values + for (int i=0; i < model->njnt; i++) { + EXPECT_EQ(model->jnt_limited[i], (mjtByte)model->jnt_user[i]); + } + + mj_deleteModel(model); +} + } // namespace } // namespace mujoco diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index 248dab2c..ad5a8f09 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -127,7 +127,164 @@ TEST_F(XMLWriterTest, DropsInertialIfFromGeom) { mj_deleteModel(model); } -TEST_F(XMLWriterTest, KeepsActlimited) { +TEST_F(XMLWriterTest, DoesNotKeepInferredJointLimited) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("limited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, DoesNotKeepExplicitJointLimited) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("limited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, KeepsJointLimitedFalse) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("limited=\"false\" range=\"-1 1\"")); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, DoesNotKeepInferredTendonLimited) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("limited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, DoesNotKeepExplicitTendonLimited) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("limited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, KeepsTendonLimitedFalse) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("limited=\"false\" range=\"-1 1\"")); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, DoesNotKeepInferredActlimited) { + static constexpr char xml[] = R"( + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("actrange=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("actlimited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, DoesNotKeepExplicitActlimited) { static constexpr char xml[] = R"( @@ -143,7 +300,152 @@ TEST_F(XMLWriterTest, KeepsActlimited) { )"; mjModel* model = LoadModelFromString(xml); std::string saved_xml = SaveAndReadXml(model); - EXPECT_THAT(saved_xml, HasSubstr("actlimited=\"true\" actrange=\"-1 1\"")); + EXPECT_THAT(saved_xml, HasSubstr("actrange=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("actlimited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, KeepsActlimitedFalse) { + static constexpr char xml[] = R"( + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("actlimited=\"false\" actrange=\"-1 1\"")); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, DoesNotKeepInferredCtrllimited) { + static constexpr char xml[] = R"( + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("ctrlrange=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("ctrllimited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, DoesNotKeepExplicitCtrllimited) { + static constexpr char xml[] = R"( + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("ctrlrange=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("ctrllimited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, KeepsCtrllimitedFalse) { + static constexpr char xml[] = R"( + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("ctrllimited=\"false\" ctrlrange=\"-1 1\"")); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, DoesNotKeepInferredForcelimited) { + static constexpr char xml[] = R"( + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("forcerange=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("forcelimited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, DoesNotKeepExplicitForcelimited) { + static constexpr char xml[] = R"( + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("forcerange=\"-1 1\"")); + EXPECT_THAT(saved_xml, Not(HasSubstr("forcelimited=\"true\""))); + mj_deleteModel(model); +} + +TEST_F(XMLWriterTest, KeepsForcelimitedFalse) { + static constexpr char xml[] = R"( + + + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("forcelimited=\"false\" forcerange=\"-1 1\"")); mj_deleteModel(model); }