diff --git a/doc/python/magnum.trade.rst b/doc/python/magnum.trade.rst index f7e6647..ec23d8b 100644 --- a/doc/python/magnum.trade.rst +++ b/doc/python/magnum.trade.rst @@ -702,6 +702,163 @@ >>> attribute.custom_value 17 +.. py:class:: magnum.trade.SceneFieldData + + Associates a pair of typed data views with a name, type and other scene + field properties. The mapping data view is always one-dimensional. The + field 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) + .. + >>> mapping_data = array.array('I', [0, 2, 7]) + >>> field_data = np.array([(-0.5, 0.0), (+0.5, 0.0), ( 0.0, 0.5)], dtype='2f') + >>> translations = trade.SceneFieldData(trade.SceneField.TRANSLATION, trade.SceneMappingType.UNSIGNED_INT, mapping_data, trade.SceneFieldType.VECTOR2, field_data) + + .. code:: py + + mapping_data = array.array('I', [0, 2, 7]) + field_data = np.array([(-0.5, 0.0), + (+0.5, 0.0), + ( 0.0, 0.5)], dtype='2f') + translations = trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_INT, mapping_data, + trade.SceneFieldType.VECTOR2, field_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 + .. + >>> field_data = array.array('f', [-0.5, 0.0, +0.5, 0.0, 0.0, 0.5]) + >>> translations = trade.SceneFieldData(trade.SceneField.TRANSLATION, trade.SceneMappingType.UNSIGNED_INT, mapping_data, trade.SceneFieldType.VECTOR2, containers.StridedArrayView1D(field_data).expanded(0, (3, 2))) + + .. code:: py + + field_data = array.array('f', [-0.5, 0.0, + +0.5, 0.0, + 0.0, 0.5]) + translations = trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_INT, mapping_data, + trade.SceneFieldType.VECTOR2, + containers.StridedArrayView1D(field_data).expanded(0, (3, 2))) + + `Memory ownership and reference counting`_ + ========================================== + + On initialization, the instance inherits the + :ref:`containers.StridedArrayView1D.owner ` + objects of both views, storing it in the :ref:`mapping_owner` and + :ref:`field_owner` fields, meaning that calling :py:`del` on the original + data will *not* invalidate the instance. + + `Data access`_ + ============== + + Similarly to :ref:`SceneData`, the class makes use of Python's dynamic + nature and provides direct access to attribute data in their concrete type + via :ref:`mapping_data` and :ref:`field_data`. However, the + :ref:`SceneFieldData` is considered a low level API and thus a + :ref:`containers.StridedArrayView2D ` + / :ref:`containers.StridedBitArrayView2D ` + is returned always for field data, even for non-array attributes. The + returned views inherit the :ref:`mapping_owner` or :ref:`field_owner` and + element access coverts to a type corresponding to a particular + :ref:`SceneMappingType` or :ref:`SceneFieldType`. For example, extracting + the data from the :py:`translations` field created above: + + .. code:: pycon + + >>> mapping = translations.mapping_data + >>> field = translations.field_data + >>> mapping.owner is mapping_data + True + >>> field.owner is field_data + True + >>> mapping[2] + 7 + >>> field[2][0] + Vector(0, 0.5) + +.. py:function:: magnum.trade.SceneFieldData.__init__(self, name: magnum.trade.SceneField, mapping_type: magnum.trade.SceneMappingType, mapping_data: corrade.containers.StridedArrayView1D, field_type: magnum.trade.SceneFieldType, field_data: corrade.containers.StridedArrayView1D, field_array_size: int, flags: magnum.trade.SceneFieldFlags) + :raise AssertionError: If :p:`mapping_data` and :p:`field_data` don't have + the same size + :raise AssertionError: If :p:`field_type` is not valid for :p:`name` + :raise AssertionError: If :p:`field_type` is a string type or + :ref:`SceneFieldType.BIT` + :raise AssertionError: If :p:`mapping_data` stride doesn't fit into 16 bits + :raise AssertionError: If :p:`mapping_data` format size is smaller than + size of :p:`mapping_type` + :raise AssertionError: If :p:`field_data` stride doesn't fit into 16 bits + :raise AssertionError: If :p:`field_data` format size is smaller than size + of :p:`field_type` at given :p:`field_array_size` + :raise AssertionError: If :p:`field_array_size` is non-zero and :p:`name` + can't be an array field + :raise AssertionError: If :p:`flags` contain + :ref:`SceneFieldFlags.OFFSET_ONLY`, + :ref:`SceneFieldFlags.NULL_TERMINATED_STRING` or values disallowed for + a particular :p:`name` +.. py:function:: magnum.trade.SceneFieldData.__init__(self, name: magnum.trade.SceneField, mapping_type: magnum.trade.SceneMappingType, mapping_data: corrade.containers.StridedArrayView1D, field_type: magnum.trade.SceneFieldType, field_data: corrade.containers.StridedArrayView2D, field_array_size: int, flags: magnum.trade.SceneFieldFlags) + :raise AssertionError: If :p:`mapping_data` and first dimension of + :p:`field_data` don't have the same size + :raise AssertionError: If :p:`field_type` is not valid for :p:`name` + :raise AssertionError: If :p:`field_type` is a string type or + :ref:`SceneFieldType.BIT` + :raise AssertionError: If :p:`mapping_data` stride doesn't fit into 16 bits + :raise AssertionError: If :p:`mapping_data` format size is smaller than + size of :p:`mapping_type` + :raise AssertionError: If :p:`field_data` first dimension stride doesn't + fit into 16 bits + :raise AssertionError: If :p:`field_data` second dimension isn't contiguous + :raise AssertionError: If :p:`field_data` format size times second + dimension size is smaller than size of :p:`field_type` at given :p:`field_array_size` + :raise AssertionError: If :p:`field_array_size` is non-zero and :p:`name` + can't be an array field + :raise AssertionError: If :p:`flags` contain + :ref:`SceneFieldFlags.OFFSET_ONLY`, + :ref:`SceneFieldFlags.NULL_TERMINATED_STRING` or values disallowed for + a particular :p:`name` + +.. py:function:: magnum.trade.SceneFieldData.__init__(self, name: magnum.trade.SceneField, mapping_type: magnum.trade.SceneMappingType, mapping_data: corrade.containers.StridedArrayView1D, field_data: corrade.containers.StridedBitArrayView1D, flags: magnum.trade.SceneFieldFlags) + :raise AssertionError: If :p:`mapping_data` and :p:`field_data` don't have + the same size + :raise AssertionError: If :ref:`SceneFieldType.BIT` is not valid for + :p:`name` + :raise AssertionError: If :p:`mapping_data` stride doesn't fit into 16 bits + :raise AssertionError: If :p:`mapping_data` format size is smaller than + size of :p:`mapping_type` + :raise AssertionError: If :p:`field_data` stride doesn't fit into 16 bits + :raise AssertionError: If :p:`flags` contain + :ref:`SceneFieldFlags.OFFSET_ONLY`, + :ref:`SceneFieldFlags.NULL_TERMINATED_STRING` or values disallowed for + a particular :p:`name` +.. py:function:: magnum.trade.SceneFieldData.__init__(self, name: magnum.trade.SceneField, mapping_type: magnum.trade.SceneMappingType, mapping_data: corrade.containers.StridedArrayView1D, field_data: corrade.containers.StridedBitArrayView2D, flags: magnum.trade.SceneFieldFlags) + :raise AssertionError: If :p:`mapping_data` and first dimension of + :p:`field_data` don't have the same size + :raise AssertionError: If :ref:`SceneFieldType.BIT` is not valid for + :p:`name` + :raise AssertionError: If :p:`mapping_data` stride doesn't fit into 16 bits + :raise AssertionError: If :p:`mapping_data` format size is smaller than + size of :p:`mapping_type` + :raise AssertionError: If :p:`field_data` first dimension stride doesn't + fit into 16 bits + :raise AssertionError: If :p:`field_data` second dimension isn't contiguous + :raise AssertionError: If :p:`flags` contain + :ref:`SceneFieldFlags.OFFSET_ONLY`, + :ref:`SceneFieldFlags.NULL_TERMINATED_STRING` or values disallowed for + a particular :p:`name` + +.. py:property:: magnum.trade.SceneFieldData.field_data + :raise NotImplementedError: If :ref:`field_type` is a half-float or string + type + + A :ref:`containers.StridedArrayView2D ` + or :ref:`containers.StridedBitArrayView2D ` + is returned always, non-array attributes have the second dimension size + :py:`1`. + .. py:class:: magnum.trade.SceneData :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 baaee26..313b4d2 100644 --- a/doc/python/pages/changelog.rst +++ b/doc/python/pages/changelog.rst @@ -154,9 +154,10 @@ Changelog - Exposed the whole interface of :ref:`trade.MaterialData` including typed access to attribute data, together with :ref:`trade.AbstractImporter.material()` and related importer APIs -- Exposed the whole interface of :ref:`trade.SceneData` including typed - access to mapping and field data, together with - :ref:`trade.AbstractImporter.scene()` and related importer APIs +- Exposed the whole interface of :ref:`trade.SceneData` and + :ref:`trade.SceneFieldData` including typed access to mapping and field + data, together with :ref:`trade.AbstractImporter.scene()` and related + importer APIs - Exposed :ref:`Color3.red()` and other convenience constructors (see :gh:`mosra/magnum-bindings#12`) - Exposed the :ref:`materialtools`, :ref:`scenetools` and :ref:`text` diff --git a/src/Magnum/Trade/PythonBindings.h b/src/Magnum/Trade/PythonBindings.h index 2f57144..9e06fcb 100644 --- a/src/Magnum/Trade/PythonBindings.h +++ b/src/Magnum/Trade/PythonBindings.h @@ -31,6 +31,7 @@ #include "Magnum/Trade/Data.h" #include "Magnum/Trade/MaterialData.h" /* :( */ #include "Magnum/Trade/MeshData.h" /* :( */ +#include "Magnum/Trade/SceneData.h" namespace Magnum { namespace Trade { @@ -75,8 +76,27 @@ template PyDataHolder pyDataHolder(T&& data, pybind11::object owner) return PyDataHolder{new T{std::move(data)}, std::move(owner)}; } +/* Compared to PyDataHolder this stores two owner objects. Has to be a template + even though it's only ever used for a single type because + PYBIND11_DECLARE_HOLDER_TYPE() wants it to be so. */ +template struct PySceneFieldDataHolder: std::unique_ptr { + explicit PySceneFieldDataHolder(SceneFieldData* object): PySceneFieldDataHolder{object, pybind11::none{}, pybind11::none{}} { + /* Data without owners can only be empty */ + CORRADE_INTERNAL_ASSERT(!object->mappingData().data() && !object->fieldData().data()); + } + + explicit PySceneFieldDataHolder(SceneFieldData* object, pybind11::object mappingOwner, pybind11::object fieldOwner): std::unique_ptr{object}, mappingOwner{std::move(mappingOwner)}, fieldOwner{std::move(fieldOwner)} {} + + pybind11::object mappingOwner, fieldOwner; +}; + +inline PySceneFieldDataHolder pySceneFieldDataHolder(SceneFieldData&& data, pybind11::object mappingOwner, pybind11::object fieldOwner) { + return PySceneFieldDataHolder{new SceneFieldData{std::move(data)}, std::move(mappingOwner), std::move(fieldOwner)}; +} + }} PYBIND11_DECLARE_HOLDER_TYPE(T, Magnum::Trade::PyDataHolder) +PYBIND11_DECLARE_HOLDER_TYPE(T, Magnum::Trade::PySceneFieldDataHolder) #endif diff --git a/src/python/magnum/test/test_trade.py b/src/python/magnum/test/test_trade.py index f9eaca8..b9b6133 100644 --- a/src/python/magnum/test/test_trade.py +++ b/src/python/magnum/test/test_trade.py @@ -1277,6 +1277,493 @@ class MeshData(unittest.TestCase): with self.assertRaisesRegex(NotImplementedError, "access to VertexFormat.MATRIX2X2 is not implemented yet, sorry"): mesh.mutable_attribute(custom_attribute) +class SceneFieldData(unittest.TestCase): + def test_init_1d(self): + a = array.array('Q', [3, 7, 166, 2872]) + b = array.array('h', [2, -1, 37, -1]) + a_refcount = sys.getrefcount(a) + b_refcount = sys.getrefcount(b) + + c = trade.SceneFieldData(trade.SceneField.MESH_MATERIAL, + trade.SceneMappingType.UNSIGNED_LONG, a, + trade.SceneFieldType.SHORT, b, + flags=trade.SceneFieldFlags.MULTI_ENTRY|trade.SceneFieldFlags.ORDERED_MAPPING) + c_refcount = sys.getrefcount(c) + self.assertEqual(c.flags, trade.SceneFieldFlags.MULTI_ENTRY|trade.SceneFieldFlags.ORDERED_MAPPING) + self.assertEqual(c.name, trade.SceneField.MESH_MATERIAL) + self.assertEqual(c.size, 4) + self.assertEqual(c.mapping_type, trade.SceneMappingType.UNSIGNED_LONG) + self.assertEqual(c.field_type, trade.SceneFieldType.SHORT) + self.assertEqual(c.field_array_size, 0) + self.assertIs(c.mapping_owner, a) + self.assertIs(c.field_owner, b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + mapping_data = c.mapping_data + self.assertEqual(mapping_data.size, (4,)) + self.assertEqual(mapping_data.stride, (8,)) + self.assertEqual(mapping_data.format, 'Q') + self.assertIs(mapping_data.owner, a) + self.assertEqual(list(mapping_data), [3, 7, 166, 2872]) + + # The data reference the original array, not the SceneFieldData + # instance + self.assertEqual(sys.getrefcount(c), c_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + field_data = c.field_data + self.assertEqual(field_data.size, (4, 1)) + self.assertEqual(field_data.stride, (2, 2)) + self.assertEqual(field_data.format, 'h') + self.assertIs(field_data.owner, b) + # Returns a 2D view always, transpose and take the first element to + # "flatten" it. + self.assertEqual(list(field_data.transposed(0, 1)[0]), [2, -1, 37, -1]) + + # The data reference the original array, not the SceneFieldData + # instance + self.assertEqual(sys.getrefcount(c), c_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + self.assertEqual(sys.getrefcount(b), b_refcount + 2) + + del c + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + del mapping_data + del field_data + self.assertEqual(sys.getrefcount(a), a_refcount) + self.assertEqual(sys.getrefcount(b), b_refcount) + + def test_init_2d(self): + a = array.array('I', [0, 1]) + b = array.array('f', [1.0, 0.0, 0.0, + 0.0, -1.0, 0.0]) + a_refcount = sys.getrefcount(a) + b_refcount = sys.getrefcount(b) + + c = trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_INT, a, + trade.SceneFieldType.VECTOR3, containers.StridedArrayView1D(b).expanded(0, (2, 3)), + flags=trade.SceneFieldFlags.IMPLICIT_MAPPING) + c_refcount = sys.getrefcount(c) + self.assertEqual(c.flags, trade.SceneFieldFlags.IMPLICIT_MAPPING) + self.assertEqual(c.name, trade.SceneField.TRANSLATION) + self.assertEqual(c.size, 2) + self.assertEqual(c.mapping_type, trade.SceneMappingType.UNSIGNED_INT) + self.assertEqual(c.field_type, trade.SceneFieldType.VECTOR3) + self.assertEqual(c.field_array_size, 0) + self.assertIs(c.mapping_owner, a) + self.assertIs(c.field_owner, b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + mapping_data = c.mapping_data + self.assertEqual(mapping_data.size, (2,)) + self.assertEqual(mapping_data.stride, (4,)) + self.assertEqual(mapping_data.format, 'I') + self.assertIs(mapping_data.owner, a) + self.assertEqual(list(mapping_data), [0, 1]) + + # The data reference the original array, not the SceneFieldData + # instance + self.assertEqual(sys.getrefcount(c), c_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + field_data = c.field_data + self.assertEqual(field_data.size, (2, 1)) + self.assertEqual(field_data.stride, (12, 12)) + self.assertEqual(field_data.format, '3f') + self.assertIs(field_data.owner, b) + # Returns a 2D view always, transpose and take the first element to + # "flatten" it. + self.assertEqual(list(field_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 SceneFieldData + # instance + self.assertEqual(sys.getrefcount(c), c_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + self.assertEqual(sys.getrefcount(b), b_refcount + 2) + + del c + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + del mapping_data + del field_data + self.assertEqual(sys.getrefcount(a), a_refcount) + self.assertEqual(sys.getrefcount(b), b_refcount) + + def test_init_1d_array(self): + a = array.array('B', [3, 7, 166, 72]) + b = array.array('Q', [0x0000ffff66663333, 0x00009999aaaacccc, 0x0000bbbb22227777, 0x00001111eeee8888]) + c = trade.SceneFieldData(trade.SceneField.CUSTOM(666), + trade.SceneMappingType.UNSIGNED_BYTE, a, + trade.SceneFieldType.UNSIGNED_SHORT, b, + field_array_size=3) + self.assertEqual(c.flags, trade.SceneFieldFlags.NONE) + self.assertEqual(c.name, trade.SceneField.CUSTOM(666)) + self.assertEqual(c.size, 4) + self.assertEqual(c.mapping_type, trade.SceneMappingType.UNSIGNED_BYTE) + self.assertEqual(c.field_type, trade.SceneFieldType.UNSIGNED_SHORT) + self.assertEqual(c.field_array_size, 3) + + mapping_data = c.mapping_data + self.assertEqual(mapping_data.size, (4,)) + self.assertEqual(mapping_data.stride, (1,)) + self.assertEqual(mapping_data.format, 'B') + self.assertEqual(list(mapping_data), [3, 7, 166, 72]) + + field_data = c.field_data + self.assertEqual(field_data.size, (4, 3)) + self.assertEqual(field_data.stride, (8, 2)) + self.assertEqual(field_data.format, 'H') + # Getting all first, second and third array elements. Assumes Little + # Endian. + self.assertEqual(list(field_data.transposed(0, 1)[0]), [0x3333, 0xcccc, 0x7777, 0x8888]) + self.assertEqual(list(field_data.transposed(0, 1)[1]), [0x6666, 0xaaaa, 0x2222, 0xeeee]) + self.assertEqual(list(field_data.transposed(0, 1)[2]), [0xffff, 0x9999, 0xbbbb, 0x1111]) + + def test_init_2d_array(self): + a = array.array('B', [3, 7, 166, 72]) + b = array.array('H', [0x3333, 0x6666, 0xffff, 0xcccc, 0xaaaa, 0x9999, 0x7777, 0x2222, 0xbbbb, 0x8888, 0xeeee, 0x1111]) + c = trade.SceneFieldData(trade.SceneField.CUSTOM(666), + trade.SceneMappingType.UNSIGNED_BYTE, a, + trade.SceneFieldType.UNSIGNED_SHORT, containers.StridedArrayView1D(b).expanded(0, (4, 3)), + field_array_size=3) + self.assertEqual(c.flags, trade.SceneFieldFlags.NONE) + self.assertEqual(c.name, trade.SceneField.CUSTOM(666)) + self.assertEqual(c.size, 4) + self.assertEqual(c.mapping_type, trade.SceneMappingType.UNSIGNED_BYTE) + self.assertEqual(c.field_type, trade.SceneFieldType.UNSIGNED_SHORT) + self.assertEqual(c.field_array_size, 3) + + mapping_data = c.mapping_data + self.assertEqual(mapping_data.size, (4,)) + self.assertEqual(mapping_data.stride, (1,)) + self.assertEqual(mapping_data.format, 'B') + self.assertEqual(list(mapping_data), [3, 7, 166, 72]) + + field_data = c.field_data + self.assertEqual(field_data.size, (4, 3)) + self.assertEqual(field_data.stride, (6, 2)) + self.assertEqual(field_data.format, 'H') + # Getting all first, second and third array elements. Assumes Little + # Endian. + self.assertEqual(list(field_data.transposed(0, 1)[0]), [0x3333, 0xcccc, 0x7777, 0x8888]) + self.assertEqual(list(field_data.transposed(0, 1)[1]), [0x6666, 0xaaaa, 0x2222, 0xeeee]) + self.assertEqual(list(field_data.transposed(0, 1)[2]), [0xffff, 0x9999, 0xbbbb, 0x1111]) + + def test_init_bit_1d(self): + a = array.array('H', [3, 7, 166, 2872]) + b = array.array('b', [1, 0, 1, 0]) + a_refcount = sys.getrefcount(a) + b_refcount = sys.getrefcount(b) + + c = trade.SceneFieldData(trade.SceneField.CUSTOM(1337), + trade.SceneMappingType.UNSIGNED_SHORT, a, + containers.StridedArrayView1D(b).slice_bit(0), + flags=trade.SceneFieldFlags.MULTI_ENTRY|trade.SceneFieldFlags.ORDERED_MAPPING) + c_refcount = sys.getrefcount(c) + self.assertEqual(c.flags, trade.SceneFieldFlags.MULTI_ENTRY|trade.SceneFieldFlags.ORDERED_MAPPING) + self.assertEqual(c.name, trade.SceneField.CUSTOM(1337)) + self.assertEqual(c.size, 4) + self.assertEqual(c.mapping_type, trade.SceneMappingType.UNSIGNED_SHORT) + self.assertEqual(c.field_type, trade.SceneFieldType.BIT) + self.assertEqual(c.field_array_size, 0) + self.assertIs(c.mapping_owner, a) + self.assertIs(c.field_owner, b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + mapping_data = c.mapping_data + self.assertEqual(mapping_data.size, (4,)) + self.assertEqual(mapping_data.stride, (2,)) + self.assertEqual(mapping_data.format, 'H') + self.assertIs(mapping_data.owner, a) + self.assertEqual(list(mapping_data), [3, 7, 166, 2872]) + + # The data reference the original array, not the SceneFieldData + # instance + self.assertEqual(sys.getrefcount(c), c_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + field_data = c.field_data + self.assertEqual(field_data.size, (4, 1)) + self.assertEqual(field_data.offset, 0) + self.assertEqual(field_data.stride, (8, 1)) + self.assertIs(field_data.owner, b) + # Returns a 2D view always, transpose and take the first element to + # "flatten" it. + self.assertEqual(list(field_data.transposed(0, 1)[0]), [True, False, True, False]) + + # The data reference the original array, not the SceneFieldData + # instance + self.assertEqual(sys.getrefcount(c), c_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + self.assertEqual(sys.getrefcount(b), b_refcount + 2) + + del c + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + del mapping_data + del field_data + self.assertEqual(sys.getrefcount(a), a_refcount) + self.assertEqual(sys.getrefcount(b), b_refcount) + + def test_init_bit_2d(self): + a = array.array('H', [3, 7, 166, 2872]) + b = containers.BitArray.value_init(4*2) + b[0] = True + b[1] = True + b[4] = True + b[7] = True + a_refcount = sys.getrefcount(a) + b_refcount = sys.getrefcount(b) + + c = trade.SceneFieldData(trade.SceneField.CUSTOM(1337), + trade.SceneMappingType.UNSIGNED_SHORT, a, + containers.StridedBitArrayView1D(b).expanded(0, (4, 2)), + flags=trade.SceneFieldFlags.MULTI_ENTRY|trade.SceneFieldFlags.ORDERED_MAPPING) + c_refcount = sys.getrefcount(c) + self.assertEqual(c.flags, trade.SceneFieldFlags.MULTI_ENTRY|trade.SceneFieldFlags.ORDERED_MAPPING) + self.assertEqual(c.name, trade.SceneField.CUSTOM(1337)) + self.assertEqual(c.size, 4) + self.assertEqual(c.mapping_type, trade.SceneMappingType.UNSIGNED_SHORT) + self.assertEqual(c.field_type, trade.SceneFieldType.BIT) + self.assertEqual(c.field_array_size, 2) + self.assertIs(c.mapping_owner, a) + self.assertIs(c.field_owner, b) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + mapping_data = c.mapping_data + self.assertEqual(mapping_data.size, (4,)) + self.assertEqual(mapping_data.stride, (2,)) + self.assertEqual(mapping_data.format, 'H') + self.assertIs(mapping_data.owner, a) + self.assertEqual(list(mapping_data), [3, 7, 166, 2872]) + + # The data reference the original array, not the SceneFieldData + # instance + self.assertEqual(sys.getrefcount(c), c_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + field_data = c.field_data + self.assertEqual(field_data.size, (4, 2)) + self.assertEqual(field_data.offset, 0) + self.assertEqual(field_data.stride, (2, 1)) + self.assertIs(field_data.owner, b) + # Getting all first and second array elements + self.assertEqual(list(field_data.transposed(0, 1)[0]), [True, False, True, False]) + self.assertEqual(list(field_data.transposed(0, 1)[1]), [True, False, False, True]) + + # The data reference the original array, not the SceneFieldData + # instance + self.assertEqual(sys.getrefcount(c), c_refcount) + self.assertEqual(sys.getrefcount(a), a_refcount + 2) + self.assertEqual(sys.getrefcount(b), b_refcount + 2) + + del c + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + self.assertEqual(sys.getrefcount(b), b_refcount + 1) + + del mapping_data + del field_data + self.assertEqual(sys.getrefcount(a), a_refcount) + self.assertEqual(sys.getrefcount(b), b_refcount) + + 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, "expected SceneField.TRANSLATION mapping and field view to have the same size but got 2 and 3"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_SHORT, data[:2], + trade.SceneFieldType.VECTOR2, data) + with self.assertRaisesRegex(AssertionError, "SceneFieldType.UNSIGNED_SHORT is not a valid type for SceneField.TRANSLATION"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_LONG, data, + trade.SceneFieldType.UNSIGNED_SHORT, data) + with self.assertRaisesRegex(AssertionError, "data type B has 1 bytes but SceneMappingType.UNSIGNED_SHORT expects at least 2"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_SHORT, data_byte, + trade.SceneFieldType.VECTOR2, data) + with self.assertRaisesRegex(AssertionError, "data type B has 1 bytes but SceneMappingType.UNSIGNED_SHORT expects at least 2"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_SHORT, data_byte_no_format, + trade.SceneFieldType.VECTOR2, data) + with self.assertRaisesRegex(AssertionError, "data type Q has 8 bytes but SceneFieldType.VECTOR3 expects at least 12"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_SHORT, data, + trade.SceneFieldType.VECTOR3, data) + with self.assertRaisesRegex(AssertionError, "data type Q has 8 bytes but array of 3 SceneFieldType.FLOAT expects at least 12"): + trade.SceneFieldData(trade.SceneField.CUSTOM(76), + trade.SceneMappingType.UNSIGNED_SHORT, data, + trade.SceneFieldType.FLOAT, data, field_array_size=3) + with self.assertRaisesRegex(AssertionError, "data type B has 1 bytes but SceneFieldType.SHORT expects at least 2"): + trade.SceneFieldData(trade.SceneField.MESH_MATERIAL, + trade.SceneMappingType.UNSIGNED_INT, data, + trade.SceneFieldType.SHORT, data_byte_no_format) + with self.assertRaisesRegex(AssertionError, "expected mapping view stride to fit into 16 bits but got 32768"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_SHORT, containers.StridedArrayView1D(data)[::4096], + trade.SceneFieldType.VECTOR2, data[:1]) + with self.assertRaisesRegex(AssertionError, "expected mapping view stride to fit into 16 bits but got -32769"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_BYTE, containers.StridedArrayView1D(data_byte)[::32769].flipped(0), + trade.SceneFieldType.VECTOR2, data[:1]) + with self.assertRaisesRegex(AssertionError, "expected field view stride to fit into 16 bits but got 32768"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_SHORT, data[:1], + trade.SceneFieldType.VECTOR2, containers.StridedArrayView1D(data)[::4096]) + with self.assertRaisesRegex(AssertionError, "expected field view stride to fit into 16 bits but got -32769"): + trade.SceneFieldData(trade.SceneField.CAMERA, + trade.SceneMappingType.UNSIGNED_SHORT, data[:1], + trade.SceneFieldType.UNSIGNED_BYTE, containers.StridedArrayView1D(data_byte)[::32769].flipped(0)) + with self.assertRaisesRegex(AssertionError, "SceneField.MESH can't be an array field"): + trade.SceneFieldData(trade.SceneField.MESH, + trade.SceneMappingType.UNSIGNED_SHORT, data, + trade.SceneFieldType.UNSIGNED_SHORT, data, field_array_size=3) + with self.assertRaisesRegex(AssertionError, "can't pass SceneFieldFlags.MULTI_ENTRY for a SceneField.TRANSLATION view of SceneFieldType.VECTOR2"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_LONG, data, + trade.SceneFieldType.VECTOR2, data, flags=trade.SceneFieldFlags.MULTI_ENTRY) + with self.assertRaisesRegex(AssertionError, "use a string constructor for SceneFieldType.STRING_OFFSET16"): + trade.SceneFieldData(trade.SceneField.CUSTOM(333), + trade.SceneMappingType.UNSIGNED_LONG, data, + trade.SceneFieldType.STRING_OFFSET16, data) + with self.assertRaisesRegex(AssertionError, "use a bit constructor for SceneFieldType.BIT"): + trade.SceneFieldData(trade.SceneField.CUSTOM(333), + trade.SceneMappingType.UNSIGNED_LONG, data, + trade.SceneFieldType.BIT, data) + + def test_init_2d_invalid(self): + data_1d = array.array('Q', [0, 0, 0]) + 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, "expected SceneField.TRANSLATION mapping and field view to have the same size but got 2 and 3"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_SHORT, data_1d[:2], + trade.SceneFieldType.VECTOR2, data) + with self.assertRaisesRegex(AssertionError, "SceneFieldType.UNSIGNED_SHORT is not a valid type for SceneField.TRANSLATION"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_LONG, data_1d, + trade.SceneFieldType.UNSIGNED_SHORT, data) + # SceneMappingType size checks are shared with the 1D variant, not + # testing here again + with self.assertRaisesRegex(AssertionError, "2-item second dimension of data type I has 8 bytes but SceneFieldType.VECTOR3 expects at least 12"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_SHORT, data_1d, + trade.SceneFieldType.VECTOR3, data) + with self.assertRaisesRegex(AssertionError, "2-item second dimension of data type I has 8 bytes but array of 3 SceneFieldType.FLOAT expects at least 12"): + trade.SceneFieldData(trade.SceneField.CUSTOM(76), + trade.SceneMappingType.UNSIGNED_SHORT, data_1d, + trade.SceneFieldType.FLOAT, data, field_array_size=3) + with self.assertRaisesRegex(AssertionError, "1-item second dimension of data type B has 1 bytes but SceneFieldType.SHORT expects at least 2"): + trade.SceneFieldData(trade.SceneField.MESH_MATERIAL, + trade.SceneMappingType.UNSIGNED_INT, data_1d, + trade.SceneFieldType.SHORT, data_byte_no_format) + with self.assertRaisesRegex(AssertionError, "second field view dimension is not contiguous"): + trade.SceneFieldData(trade.SceneField.MESH, + trade.SceneMappingType.UNSIGNED_SHORT, data_1d, + trade.SceneFieldType.UNSIGNED_SHORT, data[::1,::2]) + # SceneMappingType stride checks are shared with the 1D variant, not + # testing here again + with self.assertRaisesRegex(AssertionError, "expected field view stride to fit into 16 bits but got 32768"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_SHORT, data_1d[:1], + trade.SceneFieldType.VECTOR2, data[::4096]) + with self.assertRaisesRegex(AssertionError, "expected field view stride to fit into 16 bits but got -32769"): + trade.SceneFieldData(trade.SceneField.CAMERA, + trade.SceneMappingType.UNSIGNED_SHORT, data_1d[:1], + trade.SceneFieldType.UNSIGNED_BYTE, data_byte[::32769].flipped(0)) + with self.assertRaisesRegex(AssertionError, "SceneField.MESH can't be an array field"): + trade.SceneFieldData(trade.SceneField.MESH, + trade.SceneMappingType.UNSIGNED_SHORT, data_1d, + trade.SceneFieldType.UNSIGNED_SHORT, data, field_array_size=3) + with self.assertRaisesRegex(AssertionError, "can't pass SceneFieldFlags.MULTI_ENTRY for a SceneField.TRANSLATION view of SceneFieldType.VECTOR2"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_LONG, data_1d, + trade.SceneFieldType.VECTOR2, data, flags=trade.SceneFieldFlags.MULTI_ENTRY) + with self.assertRaisesRegex(AssertionError, "use a string constructor for SceneFieldType.STRING_OFFSET16"): + trade.SceneFieldData(trade.SceneField.CUSTOM(333), + trade.SceneMappingType.UNSIGNED_LONG, data_1d, + trade.SceneFieldType.STRING_OFFSET16, data) + with self.assertRaisesRegex(AssertionError, "use a bit constructor for SceneFieldType.BIT"): + trade.SceneFieldData(trade.SceneField.CUSTOM(333), + trade.SceneMappingType.UNSIGNED_LONG, data_1d, + trade.SceneFieldType.BIT, data) + + def test_init_bit_1d_invalid(self): + data = array.array('Q', [0, 0, 0]) + data_bits = containers.BitArray.value_init(3) + + with self.assertRaisesRegex(AssertionError, "expected SceneField.CUSTOM\\(33\\) mapping and field view to have the same size but got 2 and 3"): + trade.SceneFieldData(trade.SceneField.CUSTOM(33), + trade.SceneMappingType.UNSIGNED_SHORT, data[:2], + data_bits) + with self.assertRaisesRegex(AssertionError, "SceneFieldType.BIT is not a valid type for SceneField.TRANSLATION"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_LONG, data, + data_bits) + # SceneMappingType size and stride checks are shared with the non-bit + # variant, not testing here again + with self.assertRaisesRegex(AssertionError, "expected field view stride to fit into 16 bits but got 32768"): + trade.SceneFieldData(trade.SceneField.CUSTOM(33), + trade.SceneMappingType.UNSIGNED_SHORT, data[:1], + containers.StridedBitArrayView1D(data_bits)[::32768]) + with self.assertRaisesRegex(AssertionError, "expected field view stride to fit into 16 bits but got -32769"): + trade.SceneFieldData(trade.SceneField.CUSTOM(33), + trade.SceneMappingType.UNSIGNED_SHORT, data[:1], + containers.StridedBitArrayView1D(data_bits)[::32769].flipped(0)) + with self.assertRaisesRegex(AssertionError, "can't pass SceneFieldFlags.OFFSET_ONLY for a SceneField.CUSTOM\\(33\\) view of SceneFieldType.BIT"): + trade.SceneFieldData(trade.SceneField.CUSTOM(33), + trade.SceneMappingType.UNSIGNED_LONG, data, + data_bits, flags=trade.SceneFieldFlags.OFFSET_ONLY) + + def test_init_bit_2d_invalid(self): + data = array.array('Q', [0, 0, 0]) + data_bits = containers.StridedBitArrayView1D(containers.BitArray.value_init(3*2)).expanded(0, (3, 2)) + data_bits_one_element = containers.StridedBitArrayView1D(containers.BitArray.value_init(3)).expanded(0, (3, 1)) + + with self.assertRaisesRegex(AssertionError, "expected SceneField.CUSTOM\\(33\\) mapping and field view to have the same size but got 2 and 3"): + trade.SceneFieldData(trade.SceneField.CUSTOM(33), + trade.SceneMappingType.UNSIGNED_SHORT, data[:2], + data_bits) + with self.assertRaisesRegex(AssertionError, "SceneFieldType.BIT is not a valid type for SceneField.TRANSLATION"): + trade.SceneFieldData(trade.SceneField.TRANSLATION, + trade.SceneMappingType.UNSIGNED_LONG, data, + data_bits) + # SceneMappingType size and stride checks are shared with the non-bit + # variant, not testing here again + with self.assertRaisesRegex(AssertionError, "expected field view stride to fit into 16 bits but got 32768"): + trade.SceneFieldData(trade.SceneField.CUSTOM(33), + trade.SceneMappingType.UNSIGNED_SHORT, data[:1], + data_bits[::16384]) + with self.assertRaisesRegex(AssertionError, "expected field view stride to fit into 16 bits but got -32769"): + trade.SceneFieldData(trade.SceneField.CUSTOM(33), + trade.SceneMappingType.UNSIGNED_SHORT, data[:1], + data_bits_one_element[::32769].flipped(0)) + with self.assertRaisesRegex(AssertionError, "can't pass SceneFieldFlags.OFFSET_ONLY for a SceneField.CUSTOM\\(33\\) view of SceneFieldType.BIT"): + trade.SceneFieldData(trade.SceneField.CUSTOM(33), + trade.SceneMappingType.UNSIGNED_LONG, data, + data_bits, + flags=trade.SceneFieldFlags.OFFSET_ONLY) + class SceneData(unittest.TestCase): def test_custom_field(self): # Creating a custom attribute diff --git a/src/python/magnum/trade.cpp b/src/python/magnum/trade.cpp index ceafe47..60503ba 100644 --- a/src/python/magnum/trade.cpp +++ b/src/python/magnum/trade.cpp @@ -739,6 +739,51 @@ template Containers::PyArrayViewHolder{data.template transposed<0, 1>()[0], formatStringGetitemSetitem.first(), itemsize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, std::move(owner)); } +void sceneFieldDataConstructorChecks(const Trade::SceneField name, const Trade::SceneMappingType mappingType, const Containers::PyStridedArrayView<1, const char>& mappingData, const Trade::SceneFieldType fieldType, const std::size_t fieldDataSize, const std::ptrdiff_t fieldDataStride, const UnsignedShort fieldArraySize, const Trade::SceneFieldFlag flags) { + if(mappingData.size() != fieldDataSize) { + PyErr_Format(PyExc_AssertionError, "expected %S mapping and field view to have the same size but got %zu and %zu", py::cast(name).ptr(), mappingData.size(), fieldDataSize); + throw py::error_already_set{}; + } + const UnsignedInt mappingTypeSize = Trade::sceneMappingTypeSize(mappingType); + if(mappingData.itemsize < mappingTypeSize) { + const char* const dataFormat = mappingData.format ? mappingData.format.data() : "B"; + PyErr_Format(PyExc_AssertionError, "data type %s has %zu bytes but %S expects at least %u", dataFormat, mappingData.itemsize, py::cast(mappingType).ptr(), mappingTypeSize); + throw py::error_already_set{}; + } + if(!Trade::Implementation::isSceneFieldTypeCompatibleWithField(name, fieldType)) { + PyErr_Format(PyExc_AssertionError, "%S is not a valid type for %S", py::cast(fieldType).ptr(), py::cast(name).ptr()); + throw py::error_already_set{}; + } + if(mappingData.stride() < -32768 || mappingData.stride() > 32767) { + PyErr_Format(PyExc_AssertionError, "expected mapping view stride to fit into 16 bits but got %zi", mappingData.stride()); + throw py::error_already_set{}; + } + if(fieldDataStride < -32768 || fieldDataStride > 32767) { + PyErr_Format(PyExc_AssertionError, "expected field view stride to fit into 16 bits but got %zi", fieldDataStride); + throw py::error_already_set{}; + } + if(fieldArraySize && !Trade::Implementation::isSceneFieldArrayAllowed(name)) { + PyErr_Format(PyExc_AssertionError, "%S can't be an array field", py::cast(name).ptr()); + throw py::error_already_set{}; + } + if(const Trade::SceneFieldFlags disallowedFlags = flags & (Trade::SceneFieldFlag::OffsetOnly|Trade::SceneFieldFlag::NullTerminatedString|Trade::Implementation::disallowedSceneFieldFlagsFor(name))) { + PyErr_Format(PyExc_AssertionError, "can't pass %S for a %S view of %S", py::cast(Trade::SceneFieldFlag(Containers::enumCastUnderlyingType(disallowedFlags))).ptr(), py::cast(name).ptr(), py::cast(fieldType).ptr()); + throw py::error_already_set{}; + } +} + +void sceneFieldDataConstructorChecks(const Trade::SceneField name, const Trade::SceneMappingType mappingType, const Containers::PyStridedArrayView<1, const char>& mappingData, const Trade::SceneFieldType fieldType, const Containers::StridedArrayView1D& fieldData, const UnsignedShort fieldArraySize, const Trade::SceneFieldFlag flags) { + sceneFieldDataConstructorChecks(name, mappingType, mappingData, fieldType, fieldData.size(), fieldData.stride(), fieldArraySize, flags); + if(Trade::Implementation::isSceneFieldTypeString(fieldType)) { + PyErr_Format(PyExc_AssertionError, "use a string constructor for %S", py::cast(fieldType).ptr()); + throw py::error_already_set{}; + } + if(fieldType == Trade::SceneFieldType::Bit) { + PyErr_Format(PyExc_AssertionError, "use a bit constructor for %S", py::cast(fieldType).ptr()); + throw py::error_already_set{}; + } +} + Containers::Triple accessorsForSceneMappingType(const Trade::SceneMappingType type) { switch(type) { #define _c(type) \ @@ -2538,6 +2583,106 @@ void trade(py::module_& m) { .value("NONE", Trade::SceneFieldFlag{}); corrade::enumOperators(sceneFieldFlag); + py::class_>{m, "SceneFieldData", "Scene field data"} + .def(py::init([](Trade::SceneField name, Trade::SceneMappingType mappingType, const Containers::PyStridedArrayView<1, const char>& mappingData, Trade::SceneFieldType fieldType, const Containers::PyStridedArrayView<1, const char>& fieldData, UnsignedShort fieldArraySize, Trade::SceneFieldFlag flags) { + sceneFieldDataConstructorChecks(name, mappingType, mappingData, fieldType, fieldData, fieldArraySize, flags); + const UnsignedInt fieldTypeSize = Trade::sceneFieldTypeSize(fieldType)*(fieldArraySize ? fieldArraySize : 1); + if(fieldData.itemsize < fieldTypeSize) { + const char* const dataFormat = fieldData.format ? fieldData.format.data() : "B"; + if(fieldArraySize) + PyErr_Format(PyExc_AssertionError, "data type %s has %zu bytes but array of %u %S expects at least %u", dataFormat, fieldData.itemsize, UnsignedInt(fieldArraySize), py::cast(fieldType).ptr(), fieldTypeSize); + else + PyErr_Format(PyExc_AssertionError, "data type %s has %zu bytes but %S expects at least %u", dataFormat, fieldData.itemsize, py::cast(fieldType).ptr(), fieldTypeSize); + throw py::error_already_set{}; + } + return Trade::pySceneFieldDataHolder(Trade::SceneFieldData{name, mappingType, Containers::StridedArrayView1D{mappingData}, fieldType, Containers::StridedArrayView1D{fieldData}, fieldArraySize, flags}, pyObjectHolderFor(mappingData).owner, pyObjectHolderFor(fieldData).owner); + }), "Construct from a 1D field view", py::arg("name"), py::arg("mapping_type"), py::arg("mapping_data"), py::arg("field_type"), py::arg("field_data"), + #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 206 + py::kw_only{}, /* new in pybind11 2.6 */ + #endif + py::arg("field_array_size") = 0, py::arg("flags") = Trade::SceneFieldFlag{}) + .def(py::init([](Trade::SceneField name, Trade::SceneMappingType mappingType, const Containers::PyStridedArrayView<1, const char>& mappingData, Trade::SceneFieldType fieldType, const Containers::PyStridedArrayView<2, const char>& fieldData, UnsignedShort fieldArraySize, Trade::SceneFieldFlag flags) { + if(fieldData.itemsize != std::size_t(fieldData.stride()[1])) { + PyErr_SetString(PyExc_AssertionError, "second field view dimension is not contiguous"); + throw py::error_already_set{}; + } + const Containers::StridedArrayView1D fieldData1D = fieldData.transposed<0, 1>()[0]; + sceneFieldDataConstructorChecks(name, mappingType, mappingData, fieldType, fieldData1D, fieldArraySize, flags); + const std::size_t secondDimensionSize = fieldData.itemsize*fieldData.size()[1]; + const UnsignedInt fieldTypeSize = Trade::sceneFieldTypeSize(fieldType)*(fieldArraySize ? fieldArraySize : 1); + if(secondDimensionSize < fieldTypeSize) { + const char* const dataFormat = fieldData.format ? fieldData.format.data() : "B"; + if(fieldArraySize) + PyErr_Format(PyExc_AssertionError, "%zu-item second dimension of data type %s has %zu bytes but array of %u %S expects at least %u", fieldData.size()[1], dataFormat, secondDimensionSize, UnsignedInt(fieldArraySize), py::cast(fieldType).ptr(), fieldTypeSize); + else + PyErr_Format(PyExc_AssertionError, "%zu-item second dimension of data type %s has %zu bytes but %S expects at least %u", fieldData.size()[1], dataFormat, secondDimensionSize, py::cast(fieldType).ptr(), fieldTypeSize); + throw py::error_already_set{}; + } + return Trade::pySceneFieldDataHolder(Trade::SceneFieldData{name, mappingType, Containers::StridedArrayView1D{mappingData}, fieldType, fieldData1D, fieldArraySize, flags}, pyObjectHolderFor(mappingData).owner, pyObjectHolderFor(fieldData).owner); + }), "Construct from a 2D field view", py::arg("name"), py::arg("mapping_type"), py::arg("mapping_data"), py::arg("field_type"), py::arg("field_data"), + #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 206 + py::kw_only{}, /* new in pybind11 2.6 */ + #endif + py::arg("field_array_size") = 0, py::arg("flags") = Trade::SceneFieldFlag{}) + .def(py::init([](Trade::SceneField name, Trade::SceneMappingType mappingType, const Containers::PyStridedArrayView<1, const char>& mappingData, const Containers::StridedBitArrayView1D& fieldData, Trade::SceneFieldFlag flags) { + sceneFieldDataConstructorChecks(name, mappingType, mappingData, Trade::SceneFieldType::Bit, fieldData.size(), fieldData.stride(), 0, flags); + return Trade::pySceneFieldDataHolder(Trade::SceneFieldData{name, mappingType, Containers::StridedArrayView1D{mappingData}, fieldData, flags}, pyObjectHolderFor(mappingData).owner, pyObjectHolderFor(fieldData).owner); + }), "Construct a bit field", py::arg("name"), py::arg("mapping_type"), py::arg("mapping_data"), py::arg("field_data"), + #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 206 + py::kw_only{}, /* new in pybind11 2.6 */ + #endif + py::arg("flags") = Trade::SceneFieldFlag{}) + .def(py::init([](Trade::SceneField name, Trade::SceneMappingType mappingType, const Containers::PyStridedArrayView<1, const char>& mappingData, const Containers::StridedBitArrayView2D& fieldData, Trade::SceneFieldFlag flags) { + if(fieldData.stride()[1] != 1) { + PyErr_SetString(PyExc_AssertionError, "second field view dimension is not contiguous"); + throw py::error_already_set{}; + } + sceneFieldDataConstructorChecks(name, mappingType, mappingData, Trade::SceneFieldType::Bit, fieldData.size()[0], fieldData.stride()[0], fieldData.size()[1], flags); + return Trade::pySceneFieldDataHolder(Trade::SceneFieldData{name, mappingType, Containers::StridedArrayView1D{mappingData}, fieldData, flags}, pyObjectHolderFor(mappingData).owner, pyObjectHolderFor(fieldData).owner); + }), "Construct an array bit field", py::arg("name"), py::arg("mapping_type"), py::arg("mapping_data"), py::arg("field_data"), + #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 206 + py::kw_only{}, /* new in pybind11 2.6 */ + #endif + py::arg("flags") = Trade::SceneFieldFlag{}) + .def_property_readonly("flags", [](const Trade::SceneFieldData& self) { + return Trade::SceneFieldFlag(Containers::enumCastUnderlyingType(self.flags())); + }, "Field flags") + .def_property_readonly("name", &Trade::SceneFieldData::name, "Field name") + .def_property_readonly("size", &Trade::SceneFieldData::size, "Number of entries") + .def_property_readonly("mapping_type", &Trade::SceneFieldData::mappingType, "Object mapping type") + .def_property_readonly("mapping_data", [](const Trade::SceneFieldData& self) { + return sceneMappingView(self.mappingType(), + /* The sceneMappingView() helper needs a 2D array with the + second dimension being element bytes, but information about + the type and its size is subsequently discarded so we don't + need any extra logic here. */ + Containers::arrayCast<2, const char>(self.mappingData(), 1), + pyObjectHolderFor(self).mappingOwner); + }, "Object mapping data") + .def_property_readonly("field_type", &Trade::SceneFieldData::fieldType, "Field type") + .def_property_readonly("field_array_size", &Trade::SceneFieldData::fieldArraySize, "Field array size") + .def_property_readonly("field_data", [](const Trade::SceneFieldData& self) { + /** @todo annotate the return type properly in the docs */ + if(self.fieldType() == Trade::SceneFieldType::Bit) + return pyCastButNotShitty(Containers::pyArrayViewHolder(self.fieldBitData(), pyObjectHolderFor(self).fieldOwner)); + /** @todo handle strings, so far they can't even be constructed so + this isn't needed */ + + const Containers::Triple formatStringGetitemSetitem = accessorsForSceneFieldType(self.fieldType()); + if(!formatStringGetitemSetitem.first()) { + PyErr_Format(PyExc_NotImplementedError, "access to %S is not implemented yet, sorry", py::cast(self.fieldType()).ptr()); + throw py::error_already_set{}; + } + const std::size_t fieldTypeSize = Trade::sceneFieldTypeSize(self.fieldType()); + return pyCastButNotShitty(Containers::pyArrayViewHolder(Containers::PyStridedArrayView<2, const char>{Containers::arrayCast<2, const char>(self.fieldData(), fieldTypeSize*(self.fieldArraySize() ? self.fieldArraySize() : 1)).every({1, fieldTypeSize}), formatStringGetitemSetitem.first(), fieldTypeSize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, pyObjectHolderFor(self).fieldOwner)); + }, "Field data") + .def_property_readonly("mapping_owner", [](const Trade::SceneFieldData& self) { + return pyObjectHolderFor(self).mappingOwner; + }, "Mapping memory owner") + .def_property_readonly("field_owner", [](const Trade::SceneFieldData& self) { + return pyObjectHolderFor(self).fieldOwner; + }, "Field memory owner"); + py::class_>{m, "SceneData", "Scene data"} .def_property_readonly("data_flags", [](const Trade::SceneData& self) { return Trade::DataFlag(Containers::enumCastUnderlyingType(self.dataFlags()));