diff --git a/doc/python/magnum.trade.rst b/doc/python/magnum.trade.rst index ee0abec..3d7db56 100644 --- a/doc/python/magnum.trade.rst +++ b/doc/python/magnum.trade.rst @@ -135,6 +135,117 @@ >>> attribute.custom_value 17 +.. py:class:: magnum.trade.MeshAttributeData + + Associates a typed data view with a name, vertex format and other mesh + attribute properties. The data view can be either one-dimensional, for + example a NumPy array: + + .. + Just to verify the snippet below actually works (don't want the arrows + shown in the docs, want to have it nicely wrapped) + .. + >>> from magnum import * + >>> import numpy as np + >>> data = np.array([(-0.5, 0.0), (+0.5, 0.0), ( 0.0, 0.5)], dtype='2f') + >>> positions = trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR2, data) + + .. code:: py + + data = np.array([(-0.5, 0.0), + (+0.5, 0.0), + ( 0.0, 0.5)], dtype='2f') + positions = trade.MeshAttributeData( + trade.MeshAttribute.POSITION, + VertexFormat.VECTOR2, + data) + + Or it can be two-dimensional, for example by expanding a flat array into a + list of two-component vectors: + + .. + Again to verify the snippet below actually works + .. + >>> from corrade import containers + >>> import array + >>> data = array.array('f', [-0.5, 0.0, +0.5, 0.0, 0.0, 0.5]) + >>> positions = trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR2, containers.StridedArrayView1D(data).expanded(0, (3, 2))) + + .. code:: py + + data = array.array('f', [-0.5, 0.0, + +0.5, 0.0, + 0.0, 0.5]) + positions = trade.MeshAttributeData( + trade.MeshAttribute.POSITION, + VertexFormat.VECTOR2, + containers.StridedArrayView1D(data).expanded(0, (3, 2))) + + `Memory ownership and reference counting`_ + ========================================== + + On initialization, the instance inherits the + :ref:`containers.StridedArrayView1D.owner ` + object, storing it in the :ref:`owner` field, meaning that calling + :py:`del` on the original data will *not* invalidate the instance. + + `Data access`_ + ============== + + Similarly to :ref:`MeshData`, the class makes use of Python's dynamic + nature and provides direct access to attribute data in their concrete type + via :ref:`data`. However, the :ref:`MeshAttributeData` is considered a low + level API and thus a :ref:`containers.StridedArrayView2D ` + is returned always, even for non-array attributes. The returned view + inherits the :ref:`owner` and element access coverts to a type + corresponding to a particular :ref:`VertexFormat`. For example, extracting + the data from the :py:`positions` attribute created above: + + .. code:: pycon + + >>> view = positions.data + >>> view.owner is data + True + >>> view[1][0] + Vector(0.5, 0) + +.. py:function:: magnum.trade.MeshAttributeData.__init__(self, name: magnum.trade.MeshAttribute, format: magnum.VertexFormat, data: corrade.containers.StridedArrayView1D, array_size: int, morph_target_id: int) + :raise AssertionError: If :p:`format` is not valid for :p:`name` + :raise AssertionError: If :p:`data` size doesn't fit into 32 bits + :raise AssertionError: If :p:`data` stride doesn't fit into 16 bits + :raise AssertionError: If :p:`data` format size is smaller than size of + :p:`format` at given :p:`array_size` + :raise AssertionError: If :p:`morph_target_id` is less than :py:`-1` or + greater than :py:`127` + :raise AssertionError: If :p:`morph_target_id` is not allowed for :p:`name` + :raise AssertionError: If :p:`array_size` is zero and :p:`name` is an array + attribute + :raise AssertionError: If :p:`array_size` is non-zero and :p:`name` can't + be an array attribute +.. py:function:: magnum.trade.MeshAttributeData.__init__(self, name: magnum.trade.MeshAttribute, format: magnum.VertexFormat, data: corrade.containers.StridedArrayView2D, array_size: int, morph_target_id: int) + :raise AssertionError: If :p:`format` is not valid for :p:`name` + :raise AssertionError: If :p:`data` first dimension size doesn't fit into + 32 bits + :raise AssertionError: If :p:`data` first dimension stride doesn't fit into + 16 bits + :raise AssertionError: If :p:`data` second dimension isn't contiguous + :raise AssertionError: If :p:`data` format size times second dimension size + is smaller than size of :p:`format` at given :p:`array_size` + :raise AssertionError: If :p:`morph_target_id` is less than :py:`-1` or + greater than :py:`127` + :raise AssertionError: If :p:`morph_target_id` is not allowed for :p:`name` + :raise AssertionError: If :p:`array_size` is zero and :p:`name` is an array + attribute + :raise AssertionError: If :p:`array_size` is non-zero and :p:`name` can't + be an array attribute + +.. py:property:: magnum.trade.MeshAttributeData.data + :raise NotImplementedError: If :ref:`format ` is + a half-float or matrix type + + A 2D view is returned always, non-array attributes have the second + dimension size :py:`1`. + .. py:class:: magnum.trade.MeshData :TODO: remove this line once m.css stops ignoring first caption on a page diff --git a/doc/python/pages/changelog.rst b/doc/python/pages/changelog.rst index 1efa02d..baaee26 100644 --- a/doc/python/pages/changelog.rst +++ b/doc/python/pages/changelog.rst @@ -146,10 +146,11 @@ Changelog :ref:`trade.AbstractImporter.flags` and corresponding enums - Exposed a basic interface of :ref:`trade.AbstractImageConverter` and :ref:`trade.AbstractSceneConverter` -- Exposed the whole interface of :ref:`trade.MeshData` including typed access - to index and attribute data, together with :ref:`VertexFormat`, - :ref:`trade.DataFlags`, :ref:`trade.AbstractImporter.mesh_attribute_name()` - and :ref:`trade.AbstractImporter.mesh_attribute_for_name()` +- Exposed the whole interface of :ref:`trade.MeshData` and + :ref:`trade.MeshAttributeData` including typed access to index and + attribute data, together with :ref:`VertexFormat`, :ref:`trade.DataFlags`, + :ref:`trade.AbstractImporter.mesh_attribute_name()` and + :ref:`trade.AbstractImporter.mesh_attribute_for_name()` - Exposed the whole interface of :ref:`trade.MaterialData` including typed access to attribute data, together with :ref:`trade.AbstractImporter.material()` and related importer APIs diff --git a/src/Magnum/Trade/PythonBindings.h b/src/Magnum/Trade/PythonBindings.h index db6e145..2f57144 100644 --- a/src/Magnum/Trade/PythonBindings.h +++ b/src/Magnum/Trade/PythonBindings.h @@ -50,6 +50,9 @@ inline bool pyDataFlagsNeedOwner(const MeshData& data) { !(data.indexDataFlags() & (DataFlag::Owned|DataFlag::Global)) || !(data.vertexDataFlags() & (DataFlag::Owned|DataFlag::Global)); } +inline bool pyDataFlagsNeedOwner(const MeshAttributeData& data) { + return data.data().data(); +} } @@ -59,7 +62,7 @@ inline bool pyDataFlagsNeedOwner(const MeshData& data) { unnecessarily complex */ template struct PyDataHolder: std::unique_ptr { explicit PyDataHolder(T* object): PyDataHolder{object, pybind11::none{}} { - /* Data without an owner can only be self-owned or global */ + /* Data without an owner can only be self-owned, global or empty */ CORRADE_INTERNAL_ASSERT(!Implementation::pyDataFlagsNeedOwner(*object)); } diff --git a/src/python/magnum/test/test_trade.py b/src/python/magnum/test/test_trade.py index a24b9dd..f9eaca8 100644 --- a/src/python/magnum/test/test_trade.py +++ b/src/python/magnum/test/test_trade.py @@ -23,6 +23,7 @@ # DEALINGS IN THE SOFTWARE. # +import array import os import platform import sys @@ -603,6 +604,180 @@ class MaterialData(unittest.TestCase): with self.assertRaisesRegex(KeyError, "MaterialAttribute.DIFFUSE_TEXTURE not found in the base material"): material.attribute(trade.MaterialAttribute.DIFFUSE_TEXTURE) +class MeshAttributeData(unittest.TestCase): + def test_init_1d(self): + a = array.array('H', [3, 7, 16, 29998]) + a_refcount = sys.getrefcount(a) + + b = trade.MeshAttributeData(trade.MeshAttribute.OBJECT_ID, VertexFormat.UNSIGNED_SHORT, a) + b_refcount = sys.getrefcount(b) + self.assertEqual(b.name, trade.MeshAttribute.OBJECT_ID) + self.assertEqual(b.format, VertexFormat.UNSIGNED_SHORT) + self.assertEqual(b.array_size, 0) + self.assertEqual(b.morph_target_id, -1) + self.assertIs(b.owner, a) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + data = b.data + self.assertEqual(data.size, (4, 1)) + self.assertEqual(data.stride, (2, 2)) + self.assertEqual(data.format, 'H') + self.assertIs(data.owner, a) + # Returns a 2D view always, transpose and take the first element to + # "flatten" it. + self.assertEqual(list(data.transposed(0, 1)[0]), [3, 7, 16, 29998]) + # The data reference the original array, not the MeshAttributeData + # instance + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + + del b + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + del data + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_init_2d(self): + a = array.array('f', [1.0, 0.0, 0.0, + 0.0, -1.0, 0.0]) + a_refcount = sys.getrefcount(a) + + b = trade.MeshAttributeData(trade.MeshAttribute.NORMAL, VertexFormat.VECTOR3, containers.StridedArrayView1D(a).expanded(0, (2, 3)), morph_target_id=37) + b_refcount = sys.getrefcount(b) + self.assertEqual(b.name, trade.MeshAttribute.NORMAL) + self.assertEqual(b.format, VertexFormat.VECTOR3) + self.assertEqual(b.array_size, 0) + self.assertEqual(b.morph_target_id, 37) + self.assertIs(b.owner, a) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + data = b.data + self.assertEqual(data.size, (2, 1)) + self.assertEqual(data.stride, (12, 12)) + self.assertEqual(data.format, '3f') + self.assertIs(data.owner, a) + # Returns a 2D view always, transpose and take the first element to + # "flatten" it. + self.assertEqual(list(data.transposed(0, 1)[0]), [(1.0, 0.0, 0.0), (0.0, -1.0, 0.0)]) + # The data reference the original array, not the MeshAttributeData + # instance + self.assertEqual(sys.getrefcount(b), b_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + + del b + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + del data + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_init_1d_array(self): + data = array.array('Q', [0x0000ffff66663333, 0x00009999aaaacccc, 0x0000bbbb22227777, 0x00001111eeee8888]) + a = trade.MeshAttributeData(trade.MeshAttribute.JOINT_IDS, VertexFormat.UNSIGNED_SHORT, data, array_size=3) + self.assertEqual(a.name, trade.MeshAttribute.JOINT_IDS) + self.assertEqual(a.format, VertexFormat.UNSIGNED_SHORT) + self.assertEqual(a.array_size, 3) + self.assertEqual(a.morph_target_id, -1) + + data = a.data + self.assertEqual(data.size, (4, 3)) + self.assertEqual(data.stride, (8, 2)) + self.assertEqual(data.format, 'H') + # Getting all first, second and third array elements. Assumes Little + # Endian. + self.assertEqual(list(data.transposed(0, 1)[0]), [0x3333, 0xcccc, 0x7777, 0x8888]) + self.assertEqual(list(data.transposed(0, 1)[1]), [0x6666, 0xaaaa, 0x2222, 0xeeee]) + self.assertEqual(list(data.transposed(0, 1)[2]), [0xffff, 0x9999, 0xbbbb, 0x1111]) + + def test_init_2d_array(self): + data = array.array('H', [0x3333, 0x6666, 0xffff, 0xcccc, 0xaaaa, 0x9999, 0x7777, 0x2222, 0xbbbb, 0x8888, 0xeeee, 0x1111]) + a = trade.MeshAttributeData(trade.MeshAttribute.JOINT_IDS, VertexFormat.UNSIGNED_SHORT, containers.StridedArrayView1D(data).expanded(0, (4, 3)), array_size=3) + self.assertEqual(a.name, trade.MeshAttribute.JOINT_IDS) + self.assertEqual(a.format, VertexFormat.UNSIGNED_SHORT) + self.assertEqual(a.array_size, 3) + self.assertEqual(a.morph_target_id, -1) + + data = a.data + self.assertEqual(data.size, (4, 3)) + self.assertEqual(data.stride, (6, 2)) + self.assertEqual(data.format, 'H') + # Getting all first, second and third array elements + self.assertEqual(list(data.transposed(0, 1)[0]), [0x3333, 0xcccc, 0x7777, 0x8888]) + self.assertEqual(list(data.transposed(0, 1)[1]), [0x6666, 0xaaaa, 0x2222, 0xeeee]) + self.assertEqual(list(data.transposed(0, 1)[2]), [0xffff, 0x9999, 0xbbbb, 0x1111]) + + def test_init_1d_invalid(self): + data = array.array('Q', [0, 0, 0]) + data_byte = array.array('B', [0, 0, 0]) + # To check that messages properly handle the case of no format string + data_byte_no_format = containers.ArrayView(data_byte) + self.assertEqual(containers.StridedArrayView1D(data_byte_no_format).format, None) + + with self.assertRaisesRegex(AssertionError, "VertexFormat.UNSIGNED_INT is not a valid format for MeshAttribute.POSITION"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.UNSIGNED_INT, data) + with self.assertRaisesRegex(AssertionError, "data type Q has 8 bytes but VertexFormat.MATRIX3X3B_NORMALIZED expects at least 9"): + trade.MeshAttributeData(trade.MeshAttribute.CUSTOM(57), VertexFormat.MATRIX3X3B_NORMALIZED, data) + with self.assertRaisesRegex(AssertionError, "data type Q has 8 bytes but array of 3 VertexFormat.VECTOR3UB expects at least 9"): + trade.MeshAttributeData(trade.MeshAttribute.CUSTOM(57), VertexFormat.VECTOR3UB, data, array_size=3) + with self.assertRaisesRegex(AssertionError, "data type B has 1 bytes but VertexFormat.UNSIGNED_SHORT expects at least 2"): + trade.MeshAttributeData(trade.MeshAttribute.OBJECT_ID, VertexFormat.UNSIGNED_SHORT, data_byte_no_format) + with self.assertRaisesRegex(AssertionError, "expected vertex count to fit into 32 bits but got 4294967296"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, containers.StridedArrayView1D(data)[:1].broadcasted(0, 0x100000000)) + with self.assertRaisesRegex(AssertionError, "expected stride to fit into 16 bits but got 32768"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, containers.StridedArrayView1D(data)[::4096]) + with self.assertRaisesRegex(AssertionError, "expected stride to fit into 16 bits but got -32769"): + trade.MeshAttributeData(trade.MeshAttribute.OBJECT_ID, VertexFormat.UNSIGNED_BYTE, containers.StridedArrayView1D(data_byte)[::32769].flipped(0)) + with self.assertRaisesRegex(AssertionError, "MeshAttribute.POSITION can't be an array attribute"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, data, array_size=2) + with self.assertRaisesRegex(AssertionError, "MeshAttribute.JOINT_IDS has to be an array attribute"): + trade.MeshAttributeData(trade.MeshAttribute.JOINT_IDS, VertexFormat.UNSIGNED_INT, data) + with self.assertRaisesRegex(AssertionError, "expected morph target ID to be either -1 or less than 128 but got -2"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, data, morph_target_id=-2) + with self.assertRaisesRegex(AssertionError, "expected morph target ID to be either -1 or less than 128 but got 128"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, data, morph_target_id=128) + with self.assertRaisesRegex(AssertionError, "morph target not allowed for MeshAttribute.OBJECT_ID"): + trade.MeshAttributeData(trade.MeshAttribute.OBJECT_ID, VertexFormat.UNSIGNED_INT, data, morph_target_id=3) + + def test_init_2d_invalid(self): + data = containers.StridedArrayView1D(array.array('I', [0, 0, 0, 0, 0, 0])).expanded(0, (3, 2)) + data_byte = containers.StridedArrayView1D(array.array('B', [0, 0, 0])).expanded(0, (3, 1)) + # To check that messages properly handle the case of no format string + data_byte_no_format = containers.StridedArrayView1D(containers.ArrayView(array.array('B', [0, 0, 0]))).expanded(0, (3, 1)) + self.assertEqual(data_byte_no_format.format, None) + + with self.assertRaisesRegex(AssertionError, "VertexFormat.UNSIGNED_INT is not a valid format for MeshAttribute.POSITION"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.UNSIGNED_INT, data) + with self.assertRaisesRegex(AssertionError, "2-item second dimension of data type I has 8 bytes but VertexFormat.MATRIX3X3B_NORMALIZED expects at least 9"): + trade.MeshAttributeData(trade.MeshAttribute.CUSTOM(57), VertexFormat.MATRIX3X3B_NORMALIZED, data) + with self.assertRaisesRegex(AssertionError, "2-item second dimension of data type I has 8 bytes but array of 3 VertexFormat.VECTOR3UB expects at least 9"): + trade.MeshAttributeData(trade.MeshAttribute.CUSTOM(57), VertexFormat.VECTOR3UB, data, array_size=3) + with self.assertRaisesRegex(AssertionError, "1-item second dimension of data type B has 1 bytes but VertexFormat.UNSIGNED_SHORT expects at least 2"): + trade.MeshAttributeData(trade.MeshAttribute.OBJECT_ID, VertexFormat.UNSIGNED_SHORT, data_byte_no_format) + with self.assertRaisesRegex(AssertionError, "second view dimension is not contiguous"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3UB, data[::1,::2]) + with self.assertRaisesRegex(AssertionError, "expected vertex count to fit into 32 bits but got 4294967296"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, data[:1].broadcasted(0, 0x100000000)) + with self.assertRaisesRegex(AssertionError, "expected stride to fit into 16 bits but got 32768"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, data[::4096]) + with self.assertRaisesRegex(AssertionError, "expected stride to fit into 16 bits but got -32769"): + trade.MeshAttributeData(trade.MeshAttribute.OBJECT_ID, VertexFormat.UNSIGNED_BYTE, data_byte[::32769].flipped(0)) + with self.assertRaisesRegex(AssertionError, "MeshAttribute.POSITION can't be an array attribute"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, data, array_size=2) + with self.assertRaisesRegex(AssertionError, "MeshAttribute.JOINT_IDS has to be an array attribute"): + trade.MeshAttributeData(trade.MeshAttribute.JOINT_IDS, VertexFormat.UNSIGNED_INT, data) + with self.assertRaisesRegex(AssertionError, "expected morph target ID to be either -1 or less than 128 but got -2"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, data, morph_target_id=-2) + with self.assertRaisesRegex(AssertionError, "expected morph target ID to be either -1 or less than 128 but got 128"): + trade.MeshAttributeData(trade.MeshAttribute.POSITION, VertexFormat.VECTOR3B, data, morph_target_id=128) + with self.assertRaisesRegex(AssertionError, "morph target not allowed for MeshAttribute.OBJECT_ID"): + trade.MeshAttributeData(trade.MeshAttribute.OBJECT_ID, VertexFormat.UNSIGNED_INT, data, morph_target_id=3) + + def test_data_access_unsupported_format(self): + data = array.array('Q', [0, 0, 0]) + a = trade.MeshAttributeData(trade.MeshAttribute.CUSTOM(57), VertexFormat.MATRIX3X2B_NORMALIZED, data) + + with self.assertRaisesRegex(NotImplementedError, "access to VertexFormat.MATRIX3X2B_NORMALIZED is not implemented yet, sorry"): + a.data + class MeshData(unittest.TestCase): def test_custom_attribute(self): # Creating a custom attribute diff --git a/src/python/magnum/trade.cpp b/src/python/magnum/trade.cpp index ffcb6ae..0c08b5e 100644 --- a/src/python/magnum/trade.cpp +++ b/src/python/magnum/trade.cpp @@ -569,6 +569,39 @@ py::object materialAttribute(const Trade::MaterialData& material, const Unsigned CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ } +void meshAttributeDataConstructorChecks(const Trade::MeshAttribute name, const VertexFormat format, const Containers::StridedArrayView1D& data, const UnsignedShort arraySize, const Int morphTargetId) { + if(!Trade::Implementation::isVertexFormatCompatibleWithAttribute(name, format)) { + PyErr_Format(PyExc_AssertionError, "%S is not a valid format for %S", py::cast(format).ptr(), py::cast(name).ptr()); + throw py::error_already_set{}; + } + #ifndef CORRADE_TARGET_32BIT + if(data.size() > 0xffffffffull) { + PyErr_Format(PyExc_AssertionError, "expected vertex count to fit into 32 bits but got %zu", data.size()); + throw py::error_already_set{}; + } + #endif + if(data.stride() < -32768 || data.stride() > 32767) { + PyErr_Format(PyExc_AssertionError, "expected stride to fit into 16 bits but got %zi", data.stride()); + throw py::error_already_set{}; + } + if(morphTargetId < -1 || morphTargetId >= 128) { + PyErr_Format(PyExc_AssertionError, "expected morph target ID to be either -1 or less than 128 but got %i", morphTargetId); + throw py::error_already_set{}; + } + if(morphTargetId != -1 && !Trade::Implementation::isMorphTargetAllowed(name)) { + PyErr_Format(PyExc_AssertionError, "morph target not allowed for %S", py::cast(name).ptr()); + throw py::error_already_set{}; + } + if(arraySize != 0 && !Trade::Implementation::isAttributeArrayAllowed(name)) { + PyErr_Format(PyExc_AssertionError, "%S can't be an array attribute", py::cast(name).ptr()); + throw py::error_already_set{}; + } + if(arraySize == 0 && Trade::Implementation::isAttributeArrayExpected(name)) { + PyErr_Format(PyExc_AssertionError, "%S has to be an array attribute", py::cast(name).ptr()); + throw py::error_already_set{}; + } +} + Containers::Triple accessorsForMeshIndexType(const MeshIndexType type) { switch(type) { #define _c(type) \ @@ -931,6 +964,66 @@ void trade(py::module_& m) { .value("OBJECT_ID", Trade::MeshAttribute::ObjectId); enumWithCustomValues(meshAttribute); + py::class_>{m, "MeshAttributeData", "Mesh attribute data"} + .def(py::init([](Trade::MeshAttribute name, VertexFormat format, const Containers::PyStridedArrayView<1, const char>& data, UnsignedShort arraySize, Int morphTargetId) { + const UnsignedInt formatSize = vertexFormatSize(format)*(arraySize ? arraySize : 1); + if(data.itemsize < formatSize) { + const char* const dataFormat = data.format ? data.format.data() : "B"; + if(arraySize) + PyErr_Format(PyExc_AssertionError, "data type %s has %zu bytes but array of %u %S expects at least %u", dataFormat, data.itemsize, UnsignedInt(arraySize), py::cast(format).ptr(), formatSize); + else + PyErr_Format(PyExc_AssertionError, "data type %s has %zu bytes but %S expects at least %u", dataFormat, data.itemsize, py::cast(format).ptr(), formatSize); + throw py::error_already_set{}; + } + meshAttributeDataConstructorChecks(name, format, data, arraySize, morphTargetId); + return Trade::pyDataHolder(Trade::MeshAttributeData{name, format, Containers::StridedArrayView1D{data}, arraySize, morphTargetId}, pyObjectHolderFor(data).owner); + }), "Construct from a 1D view", py::arg("name"), py::arg("format"), py::arg("data"), + #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 206 + py::kw_only{}, /* new in pybind11 2.6 */ + #endif + py::arg("array_size") = 0, py::arg("morph_target_id") = -1) + .def(py::init([](Trade::MeshAttribute name, VertexFormat format, const Containers::PyStridedArrayView<2, const char>& data, UnsignedShort arraySize, Int morphTargetId) { + if(data.itemsize != std::size_t(data.stride()[1])) { + PyErr_SetString(PyExc_AssertionError, "second view dimension is not contiguous"); + throw py::error_already_set{}; + } + const std::size_t secondDimensionSize = data.itemsize*data.size()[1]; + const UnsignedInt formatSize = vertexFormatSize(format)*(arraySize ? arraySize : 1); + if(secondDimensionSize < formatSize) { + const char* const dataFormat = data.format ? data.format.data() : "B"; + if(arraySize) + PyErr_Format(PyExc_AssertionError, "%zu-item second dimension of data type %s has %zu bytes but array of %u %S expects at least %u", data.size()[1], dataFormat, secondDimensionSize, UnsignedInt(arraySize), py::cast(format).ptr(), formatSize); + else + PyErr_Format(PyExc_AssertionError, "%zu-item second dimension of data type %s has %zu bytes but %S expects at least %u", data.size()[1], dataFormat, secondDimensionSize, py::cast(format).ptr(), formatSize); + throw py::error_already_set{}; + } + /* All checks on the second dimension are done now, drop it */ + const Containers::StridedArrayView1D data1D = data.transposed<0, 1>()[0]; + meshAttributeDataConstructorChecks(name, format, data1D, arraySize, morphTargetId); + return Trade::pyDataHolder(Trade::MeshAttributeData{name, format, data1D, arraySize, morphTargetId}, pyObjectHolderFor(data).owner); + }), "Construct from a 2D view", py::arg("name"), py::arg("format"), py::arg("data"), + #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 206 + py::kw_only{}, /* new in pybind11 2.6 */ + #endif + py::arg("array_size") = 0, py::arg("morph_target_id") = -1) + .def_property_readonly("name", &Trade::MeshAttributeData::name, "Attribute name") + .def_property_readonly("format", &Trade::MeshAttributeData::format, "Attribute format") + .def_property_readonly("array_size", &Trade::MeshAttributeData::arraySize, "Attribute array size") + .def_property_readonly("morph_target_id", &Trade::MeshAttributeData::morphTargetId, "Morph target ID") + .def_property_readonly("data", [](const Trade::MeshAttributeData& self) { + const Containers::Triple formatStringGetitemSetitem = accessorsForVertexFormat(self.format()); + if(!formatStringGetitemSetitem.first()) { + PyErr_Format(PyExc_NotImplementedError, "access to %S is not implemented yet, sorry", py::cast(self.format()).ptr()); + throw py::error_already_set{}; + } + + const std::size_t formatSize = vertexFormatSize(self.format()); + return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<2, const char>{Containers::arrayCast<2, const char>(self.data(), formatSize*(self.arraySize() ? self.arraySize() : 1)).every({1, formatSize}), formatStringGetitemSetitem.first(), formatSize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, pyObjectHolderFor(self).owner); + }, "Attribute data") + .def_property_readonly("owner", [](Trade::MeshAttributeData& self) { + return pyObjectHolderFor(self).owner; + }, "Memory owner"); + py::class_>{m, "MeshData", "Mesh data"} .def_property_readonly("primitive", &Trade::MeshData::primitive, "Primitive") .def_property_readonly("index_data_flags", [](const Trade::MeshData& self) {