diff --git a/python/mujoco/codegen/generate_spec_bindings.py b/python/mujoco/codegen/generate_spec_bindings.py index 68e13202..6ec1e24c 100644 --- a/python/mujoco/codegen/generate_spec_bindings.py +++ b/python/mujoco/codegen/generate_spec_bindings.py @@ -463,6 +463,19 @@ def generate_add() -> None: f'py::arg("{f.name}") = py::none()', ) elif isinstance(f.type, ast_nodes.ArrayType): + inner_type = f.type.inner_type.decl() + if inner_type == 'char': + return ( + ( + f'set_char_array(out->{f.name}, {f.name},' + f' {f.type.extents[0]}, "{f.name}");' + ), + 'char_array', + f.name, + 'str | list[str]', + f'py::object& {f.name}', + f'py::arg("{f.name}") = py::none()', + ) if f.name == 'size' and f.type.extents[0] == 3: return ( f'set_array_size(out->{f.name}, {f.name});', @@ -759,6 +772,38 @@ def generate_add() -> None: } }; """ + elif t == 'char_array': + code += """\n + auto set_char_array = [](auto&& des, py::object& obj, int size, const char* name) { + if (obj.is_none()) { + return; + } + std::string chars; + if (py::isinstance(obj)) { + chars = py::cast(obj); + } else if (py::isinstance(obj)) { + py::list list = py::cast(obj); + chars.reserve(py::len(list)); + for (auto item : list) { + std::string s = py::cast(item); + if (s.size() != 1) { + throw pybind11::value_error(std::string(name) + " list elements must be single characters."); + } + chars.push_back(s[0]); + } + } else { + throw pybind11::type_error(std::string(name) + " must be a string or a list of single-character strings."); + } + if (chars.size() != size) { + std::string msg = std::string(name) + " should have length " + std::to_string(size) + ", got " + std::to_string(chars.size()) + "."; + throw pybind11::value_error(msg); + } + int idx = 0; + for (char val : chars) { + des[idx++] = val; + } + }; + """ elif t == 'array_size': code += """\n auto set_array_size = [](auto&& des, const std::optional>& array) { diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 09bd37fd..d77308fc 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -1120,6 +1120,27 @@ class SpecsTest(absltest.TestCase): self.assertIsNone(spec.texture('none')) self.assertIsNone(spec.mesh('none')) + def test_texture_gridlayout(self): + spec = mujoco.MjSpec() + + texture = spec.add_texture(name='test', gridlayout='.U..LFRB.D..') + self.assertEqual(list(texture.gridlayout), list('.U..LFRB.D..')) + + texture2 = spec.add_texture(name='test2', gridlayout=list('.U..LFRB.D..')) + self.assertEqual(list(texture2.gridlayout), list('.U..LFRB.D..')) + + with self.assertRaises(ValueError) as cm: + spec.add_texture(name='test3', gridlayout='.U..') + self.assertIn('should have length 12', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + spec.add_texture(name='test4', gridlayout=['.', 'U', '.', '.']) + self.assertIn('should have length 12', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + spec.add_texture(name='test5', gridlayout=['..', 'U'] + ['.'] * 10) + self.assertIn('list elements must be single characters', str(cm.exception)) + def test_attach_units(self): child = mujoco.MjSpec() parent = mujoco.MjSpec()