diff --git a/doc/python/magnum.trade.rst b/doc/python/magnum.trade.rst index 5d7bf5b..476a493 100644 --- a/doc/python/magnum.trade.rst +++ b/doc/python/magnum.trade.rst @@ -66,6 +66,26 @@ .. py:property:: magnum.trade.ImageData3D.pixels :raise AttributeError: If :ref:`is_compressed` is :py:`True` +.. py:enum:: magnum.trade.MeshAttribute + + The equivalent to C++ :dox:`Trade::meshAttributeCustom()` is creating an + enum value using a ``CUSTOM()`` named constructor. The ``is_custom`` + property then matches :dox:`Trade::isMeshAttributeCustom()` and you can + retrieve the custom ID again with a ``custom_value`` property. + + .. + >>> from magnum import trade + + .. code:: pycon + + >>> attribute = trade.MeshAttribute.CUSTOM(17) + >>> attribute.name + 'CUSTOM(17)' + >>> attribute.is_custom + True + >>> attribute.custom_value + 17 + .. py:class:: magnum.trade.MeshData Compared to the C++ API, there's no diff --git a/src/python/magnum/test/test_trade.py b/src/python/magnum/test/test_trade.py index 19ef715..1f68195 100644 --- a/src/python/magnum/test/test_trade.py +++ b/src/python/magnum/test/test_trade.py @@ -101,6 +101,46 @@ class ImageData(unittest.TestCase): mutable_view = MutableImageView2D(image) class MeshData(unittest.TestCase): + def test_custom_attribute(self): + # Creating a custom attribute + a = trade.MeshAttribute.CUSTOM(17) + self.assertTrue(a.is_custom) + self.assertEqual(a.value, 32768 + 17) + self.assertEqual(a.custom_value, 17) + self.assertEqual(a.name, "CUSTOM(17)") + self.assertEqual(str(a), "MeshAttribute.CUSTOM(17)") + self.assertEqual(repr(a), "") + + # Lowest possible custom value, test that it's correctly recognized as + # custom by all APIs + zero = trade.MeshAttribute.CUSTOM(0) + self.assertTrue(zero.is_custom) + self.assertEqual(zero.value, 32768) + self.assertEqual(zero.custom_value, 0) + self.assertEqual(zero.name, "CUSTOM(0)") + self.assertEqual(str(zero), "MeshAttribute.CUSTOM(0)") + self.assertEqual(repr(zero), "") + + # Largest possible custom value + largest = trade.MeshAttribute.CUSTOM(32767) + self.assertTrue(largest.is_custom) + self.assertEqual(largest.value, 65535) + self.assertEqual(largest.custom_value, 32767) + + # Creating a custom attribute with a value that won't fit + with self.assertRaisesRegex(ValueError, "custom value too large"): + trade.MeshAttribute.CUSTOM(32768) + + # Accessing properties on builtin values should still work as expected + b = trade.MeshAttribute.BITANGENT + self.assertFalse(b.is_custom) + self.assertEqual(b.value, 3) + with self.assertRaisesRegex(AttributeError, "not a custom value"): + b.custom_value + self.assertEqual(b.name, "BITANGENT") + self.assertEqual(str(b), "MeshAttribute.BITANGENT") + self.assertEqual(repr(b), "") + def test(self): importer = trade.ImporterManager().load_and_instantiate('GltfImporter') importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) @@ -127,8 +167,7 @@ class MeshData(unittest.TestCase): # Attribute properties by ID self.assertEqual(mesh.attribute_name(3), trade.MeshAttribute.POSITION) # Custom attribute - # TODO better API for custom IDs? - self.assertEqual(mesh.attribute_name(8), trade.MeshAttribute(32768 + 9)) + self.assertEqual(mesh.attribute_name(8), trade.MeshAttribute.CUSTOM(9)) self.assertEqual(mesh.attribute_id(3), 0) # Attribute 5 is the second TEXTURE_COORDINATES attribute self.assertEqual(mesh.attribute_id(5), 1) @@ -163,8 +202,7 @@ class MeshData(unittest.TestCase): # Attribute properties by ID self.assertEqual(mesh.attribute_name(2), trade.MeshAttribute.POSITION) # Custom attribute - # TODO better API for custom IDs? - self.assertEqual(mesh.attribute_name(6), trade.MeshAttribute(32768 + 7)) + self.assertEqual(mesh.attribute_name(6), trade.MeshAttribute.CUSTOM(7)) self.assertEqual(mesh.attribute_id(2), 0) # Attribute 4 is the second TEXTURE_COORDINATES attribute self.assertEqual(mesh.attribute_id(4), 1) @@ -647,13 +685,12 @@ class Importer(unittest.TestCase): # Asking for custom mesh attribute names should work even if not # opened, returns None - # TODO better API for custom IDs? # TODO once configuration is exposed, disable the JOINTS/WEIGHTS # backwards compatibility to avoid this mess if magnum.BUILD_DEPRECATED: - self.assertIsNone(importer.mesh_attribute_name(trade.MeshAttribute(32768 + 9))) + self.assertIsNone(importer.mesh_attribute_name(trade.MeshAttribute.CUSTOM(9))) else: - self.assertIsNone(importer.mesh_attribute_name(trade.MeshAttribute(32768 + 7))) + self.assertIsNone(importer.mesh_attribute_name(trade.MeshAttribute.CUSTOM(7))) self.assertIsNone(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE")) importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) @@ -663,15 +700,14 @@ class Importer(unittest.TestCase): self.assertEqual(importer.mesh_for_name('Indexed mesh'), 0) # It should work after opening - # TODO better API for custom IDs? # TODO once configuration is exposed, disable the JOINTS/WEIGHTS # backwards compatibility to avoid this mess if magnum.BUILD_DEPRECATED: - self.assertEqual(importer.mesh_attribute_name(trade.MeshAttribute(32768 + 9)), "_CUSTOM_ATTRIBUTE") - self.assertEqual(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE"), trade.MeshAttribute(32768 + 9)) + self.assertEqual(importer.mesh_attribute_name(trade.MeshAttribute.CUSTOM(9)), "_CUSTOM_ATTRIBUTE") + self.assertEqual(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE"), trade.MeshAttribute.CUSTOM(9)) else: - self.assertEqual(importer.mesh_attribute_name(trade.MeshAttribute(32768 + 7)), "_CUSTOM_ATTRIBUTE") - self.assertEqual(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE"), trade.MeshAttribute(32768 + 7)) + self.assertEqual(importer.mesh_attribute_name(trade.MeshAttribute.CUSTOM(7)), "_CUSTOM_ATTRIBUTE") + self.assertEqual(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE"), trade.MeshAttribute.CUSTOM(7)) mesh = importer.mesh(0) self.assertEqual(mesh.primitive, MeshPrimitive.TRIANGLES) diff --git a/src/python/magnum/trade.cpp b/src/python/magnum/trade.cpp index 7eb8779..d971386 100644 --- a/src/python/magnum/trade.cpp +++ b/src/python/magnum/trade.cpp @@ -57,6 +57,66 @@ namespace magnum { namespace { +/* Adapted from pybind11's base_enum internals -- if enum_name returns ???, + replace it with CUSTOM(id) */ +template::type baseCustomValue> inline py::str enumWithCustomValuesName(const py::object& arg) { + py::str name = py::detail::enum_name(arg); + /* Haha what the hell is this comparison */ + if(std::string{name} == "???") + return py::str("CUSTOM({})").format(typename std::underlying_type::type(py::int_(arg)) - baseCustomValue); + return name; +} + +/* Not using the meshAttributeCustom() etc helpers as it would be too painful + to pass them all, and I'd need to make my own handling of the OOB cases + anyway */ +template::type baseCustomValue> void enumWithCustomValues(py::enum_& enum_) { + static_assert(!typename std::underlying_type::type(baseCustomValue << 1), + "base custom value expected to be a single highest bit"); + + enum_ + .def("CUSTOM", [](typename std::underlying_type::type value) { + /* Assuming the base custom value is a single highest bit, the + custom value should not have the same bit set (or, in other + words, should be smaller) */ + if(baseCustomValue & value) { + PyErr_SetString(PyExc_ValueError, "custom value too large"); + throw py::error_already_set{}; + } + return T(baseCustomValue + value); + }) + .def_property_readonly("is_custom", [](T value) { + return typename std::underlying_type::type(value) >= baseCustomValue; + }) + .def_property_readonly("custom_value", [](T value) { + if(typename std::underlying_type::type(value) < baseCustomValue) { + PyErr_SetString(PyExc_AttributeError, "not a custom value"); + throw py::error_already_set{}; + } + return typename std::underlying_type::type(value) - baseCustomValue; + }); + + /* Adapted from pybind11's base_enum internals, just calling our + customEnumName instead of py::detail::enum_name */ + enum_.attr("__repr__") = py::cpp_function( + [](const py::object& arg) -> py::str { + py::handle type = py::type::handle_of(arg); + py::object type_name = type.attr("__name__"); + return py::str("<{}.{}: {}>") + .format(std::move(type_name), enumWithCustomValuesName(arg), py::int_(arg)); + }, + py::name("__repr__"), + py::is_method(enum_)); + enum_.attr("name") = py::handle(reinterpret_cast(&PyProperty_Type))(py::cpp_function(&enumWithCustomValuesName, py::name("name"), py::is_method(enum_))); + enum_.attr("__str__") = py::cpp_function( + [](const py::object& arg) -> py::str { + py::object type_name = py::type::handle_of(arg).attr("__name__"); + return pybind11::str("{}.{}").format(std::move(type_name), enumWithCustomValuesName(arg)); + }, + py::name("name"), + py::is_method(enum_)); +} + template PyObject* implicitlyConvertibleToImageView(PyObject* obj, PyTypeObject*) { py::detail::make_caster> caster; if(!caster.load(obj, false)) { @@ -451,7 +511,8 @@ void trade(py::module_& m) { .value("MUTABLE", Trade::DataFlag::Mutable); corrade::enumOperators(dataFlag); - py::enum_{m, "MeshAttribute", "Mesh attribute name"} + py::enum_ meshAttribute{m, "MeshAttribute", "Mesh attribute name"}; + meshAttribute .value("POSITION", Trade::MeshAttribute::Position) .value("TANGENT", Trade::MeshAttribute::Tangent) .value("BITANGENT", Trade::MeshAttribute::Bitangent) @@ -461,6 +522,7 @@ void trade(py::module_& m) { .value("JOINT_IDS", Trade::MeshAttribute::JointIds) .value("WEIGHTS", Trade::MeshAttribute::Weights) .value("OBJECT_ID", Trade::MeshAttribute::ObjectId); + enumWithCustomValues(meshAttribute); py::class_{m, "MeshData", "Mesh data"} .def_property_readonly("primitive", &Trade::MeshData::primitive, "Primitive") @@ -751,7 +813,7 @@ void trade(py::module_& m) { if(const Containers::String attribute = self.meshAttributeName(name)) return std::string{attribute}; return {}; - }, "String name for given mesh attribute", py::arg("name")) + }, "String name for given custom mesh attribute", py::arg("name")) .def_property_readonly("image1d_count", checkOpened, "One-dimensional image count") .def_property_readonly("image2d_count", checkOpened, "Two-dimensional image count")