Browse Source

python: implement handling for custom trade.MeshAttribute values.

So it's possible to create them, print them and get the custom ID back.
Will be also used for SceneField and other yet-to-be-exposed enums.
next
Vladimír Vondruš 3 years ago
parent
commit
d95c60942b
  1. 20
      doc/python/magnum.trade.rst
  2. 60
      src/python/magnum/test/test_trade.py
  3. 66
      src/python/magnum/trade.cpp

20
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

60
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), "<MeshAttribute.CUSTOM(17): 32785>")
# 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), "<MeshAttribute.CUSTOM(0): 32768>")
# 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), "<MeshAttribute.BITANGENT: 3>")
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)

66
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<class T, typename std::underlying_type<T>::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<T>::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<class T, typename std::underlying_type<T>::type baseCustomValue> void enumWithCustomValues(py::enum_<T>& enum_) {
static_assert(!typename std::underlying_type<T>::type(baseCustomValue << 1),
"base custom value expected to be a single highest bit");
enum_
.def("CUSTOM", [](typename std::underlying_type<T>::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<T>::type(value) >= baseCustomValue;
})
.def_property_readonly("custom_value", [](T value) {
if(typename std::underlying_type<T>::type(value) < baseCustomValue) {
PyErr_SetString(PyExc_AttributeError, "not a custom value");
throw py::error_already_set{};
}
return typename std::underlying_type<T>::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<T, baseCustomValue>(arg), py::int_(arg));
},
py::name("__repr__"),
py::is_method(enum_));
enum_.attr("name") = py::handle(reinterpret_cast<PyObject*>(&PyProperty_Type))(py::cpp_function(&enumWithCustomValuesName<T, baseCustomValue>, 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<T, baseCustomValue>(arg));
},
py::name("name"),
py::is_method(enum_));
}
template<UnsignedInt dimensions, class T> PyObject* implicitlyConvertibleToImageView(PyObject* obj, PyTypeObject*) {
py::detail::make_caster<Trade::ImageData<dimensions>> 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_<Trade::MeshAttribute>{m, "MeshAttribute", "Mesh attribute name"}
py::enum_<Trade::MeshAttribute> 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<Trade::MeshAttribute, Trade::Implementation::MeshAttributeCustom>(meshAttribute);
py::class_<Trade::MeshData>{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<UnsignedInt, &Trade::AbstractImporter::image1DCount>, "One-dimensional image count")
.def_property_readonly("image2d_count", checkOpened<UnsignedInt, &Trade::AbstractImporter::image2DCount>, "Two-dimensional image count")

Loading…
Cancel
Save