diff --git a/doc/python/magnum.trade.rst b/doc/python/magnum.trade.rst index 112d06e..fbe7b30 100644 --- a/doc/python/magnum.trade.rst +++ b/doc/python/magnum.trade.rst @@ -183,6 +183,63 @@ :raise AttributeError: If :ref:`vertex_data_flags` doesn't contain :ref:`DataFlag.MUTABLE` +.. py:enum:: magnum.trade.SceneField + + The equivalent to C++ :dox:`Trade::sceneFieldCustom()` is creating an enum + value using a ``CUSTOM()`` named constructor. The ``is_custom`` + property then matches :dox:`Trade::isSceneFieldCustom()` and you can + retrieve the custom ID again with a ``custom_value`` property. + + .. + >>> from magnum import trade + + .. code:: pycon + + >>> attribute = trade.SceneField.CUSTOM(17) + >>> attribute.name + 'CUSTOM(17)' + >>> attribute.is_custom + True + >>> attribute.custom_value + 17 + +.. py:function:: magnum.trade.SceneData.field_name + :raise IndexError: If :p:`id` is negative or not less than + :ref:`field_count` +.. py:function:: magnum.trade.SceneData.field_flags + :raise IndexError: If :p:`id` is negative or not less than + :ref:`field_count` + :raise KeyError: If :p:`name` does not exist +.. py:function:: magnum.trade.SceneData.field_type + :raise IndexError: If :p:`id` is negative or not less than + :ref:`field_count` + :raise KeyError: If :p:`name` does not exist +.. py:function:: magnum.trade.SceneData.field_size + :raise IndexError: If :p:`id` is negative or not less than + :ref:`field_count` + :raise KeyError: If :p:`name` does not exist +.. py:function:: magnum.trade.SceneData.field_array_size + :raise IndexError: If :p:`id` is negative or not less than + :ref:`field_count` + :raise KeyError: If :p:`name` does not exist +.. py:function:: magnum.trade.SceneData.field_id + :raise KeyError: If :p:`name` does not exist +.. py:function:: magnum.trade.SceneData.field_object_offset + :raise IndexError: If :p:`field_id` is negative or not less than + :ref:`field_count` + :raise KeyError: If :p:`field_name` does not exist + :raise IndexError: If :p:`object` is negative or not less than + :ref:`mapping_bound` + :raise IndexError: If :p:`offset` is negative or larger than + :ref:`field_size()` for given field + :raise LookupError: If :p:`object` is not found +.. py:function:: magnum.trade.SceneData.has_field_object + :raise IndexError: If :p:`field_id` is negative or not less than + :ref:`field_count` + :raise KeyError: If :p:`field_name` does not exist + :raise IndexError: If :p:`object` is negative or not less than + :ref:`mapping_bound` + .. py:class:: magnum.trade.ImporterManager :summary: Manager for :ref:`AbstractImporter` plugin instances @@ -218,6 +275,31 @@ :dox:`Trade::AbstractImporter::openFile()`, which expects forward slashes as directory separators on all platforms. +.. py:property:: magnum.trade.AbstractImporter.default_scene + :raise AssertionError: If no file is opened +.. py:property:: magnum.trade.AbstractImporter.scene_count + :raise AssertionError: If no file is opened +.. py:property:: magnum.trade.AbstractImporter.object_count + :raise AssertionError: If no file is opened +.. py:function:: magnum.trade.AbstractImporter.scene_for_name + :raise AssertionError: If no file is opened +.. py:function:: magnum.trade.AbstractImporter.object_for_name + :raise AssertionError: If no file is opened +.. py:function:: magnum.trade.AbstractImporter.scene_name + :raise AssertionError: If no file is opened + :raise IndexError: If :p:`id` is negative or not less than :ref:`scene_count` +.. py:function:: magnum.trade.AbstractImporter.object_name + :raise AssertionError: If no file is opened + :raise IndexError: If :p:`id` is negative or not less than :ref:`object_count` + +.. TODO this needs distinction by parameter names, at least + +.. py:function:: magnum.trade.AbstractImporter.scene + :raise AssertionError: If no file is opened + :raise RuntimeError: If scene import fails + :raise IndexError: If :p:`id` is negative or not less than :ref:`scene` + :raise KeyError: If :p:`name` was not found + .. py:property:: magnum.trade.AbstractImporter.mesh_count :raise AssertionError: If no file is opened .. py:function:: magnum.trade.AbstractImporter.mesh_level_count diff --git a/src/python/magnum/test/scene.gltf b/src/python/magnum/test/scene.gltf new file mode 100644 index 0000000..28a83eb --- /dev/null +++ b/src/python/magnum/test/scene.gltf @@ -0,0 +1,71 @@ +{ + "asset": { + "version": "2.0" + }, + "cameras": [ + { + "type": "orthographic", + "orthographic": { + "xmag": 1.0, + "ymag": 1.0, + "znear": 0.1, + "zfar": 100.0 + } + }, + { + "type": "perspective", + "perspective": { + "yfov": 0.6108652, + "znear": 0.1, + "zfar": 100.0 + } + } + ], + "nodes": [ + { + "translation": [7, 8, 9] + }, + { + "name": "Translated node", + "translation": [1, 2, 3] + }, + { + "name": "Camera node", + "matrix": [ + 1, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 2, 0, + 0, 0, 0, 1 + ], + "camera": 1, + "extras": { + "aNumber": 5, + "aString": "hello!" + }, + "children": [3] + }, + { + "camera": 0, + "translation": [4, 5, 6], + "children": [0] + }, + { + "name": "A broken node", + "mesh": 666 + } + ], + "scenes": [ + { + "name": "A scene", + "nodes": [1, 2] + }, + { + "name": "A default scene that's empty" + }, + { + "name": "A broken scene", + "nodes": [4] + } + ], + "scene": 1 +} diff --git a/src/python/magnum/test/test_trade.py b/src/python/magnum/test/test_trade.py index 72e6cad..40447d5 100644 --- a/src/python/magnum/test/test_trade.py +++ b/src/python/magnum/test/test_trade.py @@ -574,6 +574,162 @@ class MeshData(unittest.TestCase): with self.assertRaisesRegex(NotImplementedError, "access to this vertex format is not implemented yet, sorry"): mesh.mutable_attribute(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE")) +class SceneData(unittest.TestCase): + def test_custom_field(self): + # Creating a custom attribute + a = trade.SceneField.CUSTOM(17) + self.assertTrue(a.is_custom) + if hasattr(a, 'value'): # only since pybind11 2.6.2 + self.assertEqual(a.value, 0x80000000 + 17) + self.assertEqual(a.custom_value, 17) + self.assertEqual(a.name, "CUSTOM(17)") + self.assertEqual(str(a), "SceneField.CUSTOM(17)") + self.assertEqual(repr(a), "") + + # Lowest possible custom value, test that it's correctly recognized as + # custom by all APIs + zero = trade.SceneField.CUSTOM(0) + self.assertTrue(zero.is_custom) + if hasattr(zero, 'value'): # only since pybind11 2.6.2 + self.assertEqual(zero.value, 0x80000000) + self.assertEqual(zero.custom_value, 0) + self.assertEqual(zero.name, "CUSTOM(0)") + self.assertEqual(str(zero), "SceneField.CUSTOM(0)") + self.assertEqual(repr(zero), "") + + # Largest possible custom value + largest = trade.SceneField.CUSTOM(0x7fffffff) + self.assertTrue(largest.is_custom) + if hasattr(largest, 'value'): # only since pybind11 2.6.2 + self.assertEqual(largest.value, 0xffffffff) + self.assertEqual(largest.custom_value, 0x7fffffff) + + # Creating a custom attribute with a value that won't fit + with self.assertRaisesRegex(ValueError, "custom value too large"): + trade.SceneField.CUSTOM(0x80000000) + + # Accessing properties on builtin values should still work as expected + b = trade.SceneField.SKIN + self.assertFalse(b.is_custom) + if hasattr(b, 'value'): # only since pybind11 2.6.2 + self.assertEqual(b.value, 10) + with self.assertRaisesRegex(AttributeError, "not a custom value"): + b.custom_value + self.assertEqual(b.name, "SKIN") + self.assertEqual(str(b), "SceneField.SKIN") + self.assertEqual(repr(b), "") + + def test(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf')) + + scene = importer.scene(0) + self.assertEqual(scene.mapping_type, trade.SceneMappingType.UNSIGNED_INT) + self.assertEqual(scene.mapping_bound, 4) + self.assertEqual(scene.field_count, 7) + # TODO add some array extras once supported to have this different from + # the mapping bound + self.assertEqual(scene.field_size_bound, 4) + self.assertFalse(scene.is_2d) + self.assertTrue(scene.is_3d) + + # Field properties by ID + self.assertEqual(scene.field_name(2), trade.SceneField.TRANSFORMATION) + self.assertEqual(scene.field_name(6), trade.SceneField.CUSTOM(1)) + # TODO some field flags in glTF please? + self.assertEqual(scene.field_flags(2), trade.SceneFieldFlag(0)) + self.assertEqual(scene.field_type(2), trade.SceneFieldType.MATRIX4X4) + self.assertEqual(scene.field_size(3), 3) + # TODO add some array extras once supported to have this non-zero for + # some fields + self.assertEqual(scene.field_array_size(2), 0) + self.assertTrue(scene.has_field_object(2, 3)) + self.assertFalse(scene.has_field_object(4, 1)) + self.assertEqual(scene.field_object_offset(2, 3), 2) + self.assertEqual(scene.field_object_offset(2, 3, 1), 2) + + # Field properties by name + self.assertEqual(scene.field_id(trade.SceneField.CUSTOM(0)), 5) + self.assertTrue(scene.has_field(trade.SceneField.IMPORTER_STATE)) + self.assertFalse(scene.has_field(trade.SceneField.SKIN)) + self.assertTrue(scene.has_field_object(trade.SceneField.TRANSFORMATION, 3)) + self.assertFalse(scene.has_field_object(trade.SceneField.CAMERA, 1)) + self.assertEqual(scene.field_object_offset(trade.SceneField.TRANSFORMATION, 3), 2) + self.assertEqual(scene.field_object_offset(trade.SceneField.TRANSFORMATION, 3, 1), 2) + # TODO some field flags in glTF please? + self.assertEqual(scene.field_flags(trade.SceneField.PARENT), trade.SceneFieldFlag(0)) + self.assertEqual(scene.field_type(6), trade.SceneFieldType.STRING_OFFSET32) + self.assertEqual(scene.field_size(trade.SceneField.CUSTOM(0)), 1) + # TODO add some array extras once supported to have this non-zero for + # some fields + self.assertEqual(scene.field_array_size(trade.SceneField.TRANSLATION), 0) + + def test_field_oob(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf')) + + scene = importer.scene(0) + + # Access by OOB field ID + with self.assertRaises(IndexError): + scene.field_name(scene.field_count) + with self.assertRaises(IndexError): + scene.field_flags(scene.field_count) + with self.assertRaises(IndexError): + scene.field_type(scene.field_count) + with self.assertRaises(IndexError): + scene.field_size(scene.field_count) + with self.assertRaises(IndexError): + scene.field_array_size(scene.field_count) + with self.assertRaisesRegex(IndexError, "field out of range"): + scene.has_field_object(scene.field_count, 0) + with self.assertRaisesRegex(IndexError, "field out of range"): + scene.field_object_offset(scene.field_count, 0) + + # Access by nonexistent field name + with self.assertRaises(KeyError): + scene.field_id(trade.SceneField.SCALING) + with self.assertRaises(KeyError): + scene.field_flags(trade.SceneField.SCALING) + with self.assertRaises(KeyError): + scene.field_type(trade.SceneField.SCALING) + with self.assertRaises(KeyError): + scene.field_size(trade.SceneField.SCALING) + with self.assertRaises(KeyError): + scene.field_array_size(trade.SceneField.SCALING) + with self.assertRaises(KeyError): + scene.has_field_object(trade.SceneField.SCALING, 0) + with self.assertRaises(KeyError): + scene.field_object_offset(trade.SceneField.SCALING, 0) + + # OOB object ID + with self.assertRaisesRegex(IndexError, "object out of range"): + scene.has_field_object(0, 4) # PARENT + with self.assertRaisesRegex(IndexError, "object out of range"): + scene.has_field_object(trade.SceneField.PARENT, 4) + with self.assertRaisesRegex(IndexError, "object out of range"): + scene.field_object_offset(0, 4) # PARENT + with self.assertRaisesRegex(IndexError, "object out of range"): + scene.field_object_offset(trade.SceneField.PARENT, 4) + + # Lookup error + with self.assertRaises(LookupError): + scene.field_object_offset(4, 1) # CAMERA + with self.assertRaises(LookupError): + scene.field_object_offset(trade.SceneField.CAMERA, 1) + + # Lookup error due to field offset being at the end + with self.assertRaises(LookupError): + scene.field_object_offset(0, 1, scene.field_size(0)) # PARENT + with self.assertRaises(LookupError): + scene.field_object_offset(trade.SceneField.PARENT, 1, scene.field_size(trade.SceneField.PARENT)) + + # OOB field offset (offset == size is allowed, tested above) + with self.assertRaisesRegex(IndexError, "offset out of range"): + scene.field_object_offset(0, 1, scene.field_size(0) + 1) # PARENT + with self.assertRaisesRegex(IndexError, "offset out of range"): + scene.field_object_offset(trade.SceneField.PARENT, 1, scene.field_size(trade.SceneField.PARENT) + 1) + class Importer(unittest.TestCase): def test(self): manager = trade.ImporterManager() @@ -592,6 +748,25 @@ class Importer(unittest.TestCase): importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') self.assertFalse(importer.is_opened) + with self.assertRaisesRegex(AssertionError, "no file opened"): + importer.default_scene + with self.assertRaisesRegex(AssertionError, "no file opened"): + importer.scene_count + with self.assertRaisesRegex(AssertionError, "no file opened"): + importer.object_count + with self.assertRaisesRegex(AssertionError, "no file opened"): + importer.scene_for_name('') + with self.assertRaisesRegex(AssertionError, "no file opened"): + importer.object_for_name('') + with self.assertRaisesRegex(AssertionError, "no file opened"): + importer.scene_name(0) + with self.assertRaisesRegex(AssertionError, "no file opened"): + importer.object_name(0) + with self.assertRaisesRegex(AssertionError, "no file opened"): + importer.scene(0) + with self.assertRaisesRegex(AssertionError, "no file opened"): + importer.scene('') + with self.assertRaisesRegex(AssertionError, "no file opened"): importer.mesh_count with self.assertRaisesRegex(AssertionError, "no file opened"): @@ -646,6 +821,13 @@ class Importer(unittest.TestCase): importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') importer.open_file(os.path.join(os.path.dirname(__file__), 'rgb.png')) + with self.assertRaises(IndexError): + importer.scene_name(0) + with self.assertRaises(IndexError): + importer.object_name(0) + with self.assertRaises(IndexError): + importer.scene(0) + with self.assertRaises(IndexError): importer.mesh_level_count(0) with self.assertRaises(IndexError): @@ -684,6 +866,55 @@ class Importer(unittest.TestCase): with self.assertRaisesRegex(RuntimeError, "opening data failed"): importer.open_data(b'') + def test_scene(self): + # importer refcounting tested in image2d + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + + # Asking for custom scene field names should work even if not opened, + # returns None + self.assertIsNone(importer.scene_field_name(trade.SceneField.CUSTOM(1))) + self.assertIsNone(importer.scene_field_for_name('aString')) + + importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf')) + self.assertEqual(importer.default_scene, 1) + self.assertEqual(importer.scene_count, 3) + self.assertEqual(importer.scene_name(1), "A default scene that's empty") + self.assertEqual(importer.scene_for_name("A default scene that's empty"), 1) + self.assertEqual(importer.object_count, 5) + self.assertEqual(importer.object_name(2), "Camera node") + self.assertEqual(importer.object_for_name("Camera node"), 2) + + # It should work after opening + self.assertEqual(importer.scene_field_name(trade.SceneField.CUSTOM(1)), 'aString') + self.assertEqual(importer.scene_field_for_name('aString'), trade.SceneField.CUSTOM(1)) + + scene = importer.scene(0) + self.assertEqual(scene.field_count, 7) + self.assertTrue(scene.has_field(importer.scene_field_for_name('aString'))) + + def test_scene_by_name(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf')) + + scene = importer.scene("A scene") + self.assertEqual(scene.field_count, 7) + + def test_scebne_by_name_not_found(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf')) + + with self.assertRaises(KeyError): + importer.scene('Nonexistent') + + def test_scene_failed(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf')) + + with self.assertRaisesRegex(RuntimeError, "import failed"): + importer.scene(2) + with self.assertRaisesRegex(RuntimeError, "import failed"): + importer.scene("A broken scene") + def test_mesh(self): # importer refcounting tested in image2d importer = trade.ImporterManager().load_and_instantiate('GltfImporter') diff --git a/src/python/magnum/trade.cpp b/src/python/magnum/trade.cpp index 81026f2..d1ec252 100644 --- a/src/python/magnum/trade.cpp +++ b/src/python/magnum/trade.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include "Corrade/Containers/PythonBindings.h" #include "Corrade/Containers/OptionalPythonBindings.h" @@ -321,6 +322,29 @@ template(Trade::AbstractImporter::*f)(UnsignedI return *std::move(out); } +/** @todo drop std::string in favor of our own string caster */ +template(Trade::AbstractImporter::*f)(UnsignedInt), Int(Trade::AbstractImporter::*indexForName)(Containers::StringView)> R checkOpenedBoundsResultString(Trade::AbstractImporter& self, const std::string& name) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_AssertionError, "no file opened"); + throw py::error_already_set{}; + } + + const Int id = (self.*indexForName)(name); + if(id == -1) { + PyErr_SetNone(PyExc_KeyError); + throw py::error_already_set{}; + } + + /** @todo log redirection -- but we'd need assertions to not be part of + that so when it dies, the user can still see why */ + Containers::Optional out = (self.*f)(id); + if(!out) { + PyErr_SetString(PyExc_RuntimeError, "import failed"); + throw py::error_already_set{}; + } + + return *std::move(out); +} template(Trade::AbstractImporter::*f)(UnsignedInt, UnsignedInt), UnsignedInt(Trade::AbstractImporter::*bounds)() const, UnsignedInt(Trade::AbstractImporter::*levelBounds)(UnsignedInt)> R checkOpenedBoundsResult(Trade::AbstractImporter& self, UnsignedInt id, UnsignedInt level) { if(!self.isOpened()) { @@ -793,6 +817,294 @@ void trade(py::module_& m) { imageData(imageData2D); imageData(imageData3D); + py::enum_{m, "SceneMappingType", "Scene object mapping type"} + .value("UNSIGNED_BYTE", Trade::SceneMappingType::UnsignedByte) + .value("UNSIGNED_SHORT", Trade::SceneMappingType::UnsignedShort) + .value("UNSIGNED_INT", Trade::SceneMappingType::UnsignedInt) + .value("UNSIGNED_LONG", Trade::SceneMappingType::UnsignedLong); + + py::enum_ sceneField{m, "SceneField", "Scene field name"}; + sceneField + .value("PARENT", Trade::SceneField::Parent) + .value("TRANSFORMATION", Trade::SceneField::Transformation) + .value("TRANSLATION", Trade::SceneField::Translation) + .value("ROTATION", Trade::SceneField::Rotation) + .value("SCALING", Trade::SceneField::Scaling) + .value("MESH", Trade::SceneField::Mesh) + .value("MESH_MATERIAL", Trade::SceneField::MeshMaterial) + .value("LIGHT", Trade::SceneField::Light) + .value("CAMERA", Trade::SceneField::Camera) + .value("SKIN", Trade::SceneField::Skin) + .value("IMPORTER_STATE", Trade::SceneField::ImporterState); + enumWithCustomValues(sceneField); + + py::enum_{m, "SceneFieldType", "Scene field type"} + .value("FLOAT", Trade::SceneFieldType::Float) + .value("HALF", Trade::SceneFieldType::Half) + .value("DOUBLE", Trade::SceneFieldType::Double) + .value("UNSIGNED_BYTE", Trade::SceneFieldType::UnsignedByte) + .value("BYTE", Trade::SceneFieldType::Byte) + .value("UNSIGNED_SHORT", Trade::SceneFieldType::UnsignedShort) + .value("SHORT", Trade::SceneFieldType::Short) + .value("UNSIGNED_INT", Trade::SceneFieldType::UnsignedInt) + .value("INT", Trade::SceneFieldType::Int) + .value("UNSIGNED_LONG", Trade::SceneFieldType::UnsignedLong) + .value("LONG", Trade::SceneFieldType::Long) + .value("VECTOR2", Trade::SceneFieldType::Vector2) + .value("VECTOR2H", Trade::SceneFieldType::Vector2h) + .value("VECTOR2D", Trade::SceneFieldType::Vector2d) + .value("VECTOR2UB", Trade::SceneFieldType::Vector2ub) + .value("VECTOR2B", Trade::SceneFieldType::Vector2b) + .value("VECTOR2US", Trade::SceneFieldType::Vector2us) + .value("VECTOR2S", Trade::SceneFieldType::Vector2s) + .value("VECTOR2UI", Trade::SceneFieldType::Vector2ui) + .value("VECTOR2I", Trade::SceneFieldType::Vector2i) + .value("VECTOR3", Trade::SceneFieldType::Vector3) + .value("VECTOR3H", Trade::SceneFieldType::Vector3h) + .value("VECTOR3D", Trade::SceneFieldType::Vector3d) + .value("VECTOR3UB", Trade::SceneFieldType::Vector3ub) + .value("VECTOR3B", Trade::SceneFieldType::Vector3b) + .value("VECTOR3US", Trade::SceneFieldType::Vector3us) + .value("VECTOR3S", Trade::SceneFieldType::Vector3s) + .value("VECTOR3UI", Trade::SceneFieldType::Vector3ui) + .value("VECTOR3I", Trade::SceneFieldType::Vector3i) + .value("VECTOR4", Trade::SceneFieldType::Vector4) + .value("VECTOR4H", Trade::SceneFieldType::Vector4h) + .value("VECTOR4D", Trade::SceneFieldType::Vector4d) + .value("VECTOR4UB", Trade::SceneFieldType::Vector4ub) + .value("VECTOR4B", Trade::SceneFieldType::Vector4b) + .value("VECTOR4US", Trade::SceneFieldType::Vector4us) + .value("VECTOR4S", Trade::SceneFieldType::Vector4s) + .value("VECTOR4UI", Trade::SceneFieldType::Vector4ui) + .value("VECTOR4I", Trade::SceneFieldType::Vector4i) + .value("MATRIX2X2", Trade::SceneFieldType::Matrix2x2) + .value("MATRIX2X2H", Trade::SceneFieldType::Matrix2x2h) + .value("MATRIX2X2D", Trade::SceneFieldType::Matrix2x2d) + .value("MATRIX2X3", Trade::SceneFieldType::Matrix2x3) + .value("MATRIX2X3H", Trade::SceneFieldType::Matrix2x3h) + .value("MATRIX2X3D", Trade::SceneFieldType::Matrix2x3d) + .value("MATRIX2X4", Trade::SceneFieldType::Matrix2x4) + .value("MATRIX2X4H", Trade::SceneFieldType::Matrix2x4h) + .value("MATRIX2X4D", Trade::SceneFieldType::Matrix2x4d) + .value("MATRIX3X2", Trade::SceneFieldType::Matrix3x2) + .value("MATRIX3X2H", Trade::SceneFieldType::Matrix3x2h) + .value("MATRIX3X2D", Trade::SceneFieldType::Matrix3x2d) + .value("MATRIX3X3", Trade::SceneFieldType::Matrix3x3) + .value("MATRIX3X3H", Trade::SceneFieldType::Matrix3x3h) + .value("MATRIX3X3D", Trade::SceneFieldType::Matrix3x3d) + .value("MATRIX3X4", Trade::SceneFieldType::Matrix3x4) + .value("MATRIX3X4H", Trade::SceneFieldType::Matrix3x4h) + .value("MATRIX3X4D", Trade::SceneFieldType::Matrix3x4d) + .value("MATRIX4X2", Trade::SceneFieldType::Matrix4x2) + .value("MATRIX4X2H", Trade::SceneFieldType::Matrix4x2h) + .value("MATRIX4X2D", Trade::SceneFieldType::Matrix4x2d) + .value("MATRIX4X3", Trade::SceneFieldType::Matrix4x3) + .value("MATRIX4X3H", Trade::SceneFieldType::Matrix4x3h) + .value("MATRIX4X3D", Trade::SceneFieldType::Matrix4x3d) + .value("MATRIX4X4", Trade::SceneFieldType::Matrix4x4) + .value("MATRIX4X4H", Trade::SceneFieldType::Matrix4x4h) + .value("MATRIX4X4D", Trade::SceneFieldType::Matrix4x4d) + .value("RANGE1D", Trade::SceneFieldType::Range1D) + .value("RANGE1DH", Trade::SceneFieldType::Range1Dh) + .value("RANGE1DD", Trade::SceneFieldType::Range1Dd) + .value("RANGE1DI", Trade::SceneFieldType::Range1Di) + .value("RANGE2D", Trade::SceneFieldType::Range2D) + .value("RANGE2DH", Trade::SceneFieldType::Range2Dh) + .value("RANGE2DD", Trade::SceneFieldType::Range2Dd) + .value("RANGE2DI", Trade::SceneFieldType::Range2Di) + .value("RANGE3D", Trade::SceneFieldType::Range3D) + .value("RANGE3DH", Trade::SceneFieldType::Range3Dh) + .value("RANGE3DD", Trade::SceneFieldType::Range3Dd) + .value("RANGE3DI", Trade::SceneFieldType::Range3Di) + .value("COMPLEX", Trade::SceneFieldType::Complex) + .value("COMPLEXD", Trade::SceneFieldType::Complexd) + .value("DUAL_COMPLEX", Trade::SceneFieldType::DualComplex) + .value("DUAL_COMPLEXD", Trade::SceneFieldType::DualComplexd) + .value("QUATERNION", Trade::SceneFieldType::Quaternion) + .value("QUATERNIOND", Trade::SceneFieldType::Quaterniond) + .value("DUAL_QUATERNION", Trade::SceneFieldType::DualQuaternion) + .value("DUAL_QUATERNIOND", Trade::SceneFieldType::DualQuaterniond) + .value("DEG", Trade::SceneFieldType::Deg) + .value("DEGH", Trade::SceneFieldType::Degh) + .value("DEGD", Trade::SceneFieldType::Degd) + .value("RAD", Trade::SceneFieldType::Rad) + .value("RADH", Trade::SceneFieldType::Radh) + .value("RADD", Trade::SceneFieldType::Radd) + .value("POINTER", Trade::SceneFieldType::Pointer) + .value("MUTABLE_POINTER", Trade::SceneFieldType::MutablePointer) + .value("STRING_OFFSET32", Trade::SceneFieldType::StringOffset32) + .value("STRING_OFFSET8", Trade::SceneFieldType::StringOffset8) + .value("STRING_OFFSET16", Trade::SceneFieldType::StringOffset16) + .value("STRING_OFFSET64", Trade::SceneFieldType::StringOffset64) + .value("STRING_RANGE32", Trade::SceneFieldType::StringRange32) + .value("STRING_RANGE8", Trade::SceneFieldType::StringRange8) + .value("STRING_RANGE16", Trade::SceneFieldType::StringRange16) + .value("STRING_RANGE64", Trade::SceneFieldType::StringRange64) + .value("STRING_RANGE_NULL_TERMINATED32", Trade::SceneFieldType::StringRangeNullTerminated32) + .value("STRING_RANGE_NULL_TERMINATED8", Trade::SceneFieldType::StringRangeNullTerminated8) + .value("STRING_RANGE_NULL_TERMINATED16", Trade::SceneFieldType::StringRangeNullTerminated16) + .value("STRING_RANGE_NULL_TERMINATED64", Trade::SceneFieldType::StringRangeNullTerminated64); + + py::enum_ sceneFieldFlag{m, "SceneFieldFlag", "Scene field flag"}; + sceneFieldFlag + .value("OFFSET_ONLY", Trade::SceneFieldFlag::OffsetOnly) + .value("ORDERED_MAPPING", Trade::SceneFieldFlag::OrderedMapping) + .value("IMPLICIT_MAPPING", Trade::SceneFieldFlag::ImplicitMapping) + .value("NULL_TERMINATED_STRING", Trade::SceneFieldFlag::NullTerminatedString); + corrade::enumOperators(sceneFieldFlag); + + py::class_{m, "SceneData", "Scene data"} + .def_property_readonly("mapping_type", &Trade::SceneData::mappingType, "Type used for object mapping") + .def_property_readonly("mapping_bound", &Trade::SceneData::mappingBound, "Object mapping bound") + .def_property_readonly("field_count", &Trade::SceneData::fieldCount, "Field count") + .def_property_readonly("field_size_bound", &Trade::SceneData::fieldSizeBound, "Field size bound") + .def_property_readonly("is_2d", &Trade::SceneData::is2D, "Whether the scene is two-dimensional") + .def_property_readonly("is_3d", &Trade::SceneData::is3D, "Whether the scene is three-dimensional") + + /* IMPORTANT: due to yet-uninvestigated pybind11 platform-specific + behavioral differences the following overloads need to have the + SceneField overload *before* the UnsignedInt overload, otherwise + the integer overload gets picked even if an enum is passed from + Python, causing massive suffering */ + .def("field_name", [](Trade::SceneData& self, UnsignedInt id) { + if(id >= self.fieldCount()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + return self.fieldName(id); + }, "Field name", py::arg("id")) + .def("field_flags", [](Trade::SceneData& self, Trade::SceneField fieldName) { + const Containers::Optional foundField = self.findFieldId(fieldName); + if(!foundField) { + PyErr_SetNone(PyExc_KeyError); + throw py::error_already_set{}; + } + return Trade::SceneFieldFlag(Containers::enumCastUnderlyingType(self.fieldFlags(*foundField))); + }, "Flags of a named field", py::arg("name")) + .def("field_flags", [](Trade::SceneData& self, UnsignedInt id) { + if(id >= self.fieldCount()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + return Trade::SceneFieldFlag(Containers::enumCastUnderlyingType(self.fieldFlags(id))); + }, "Field flags", py::arg("id")) + .def("field_type", [](Trade::SceneData& self, Trade::SceneField fieldName) { + const Containers::Optional foundField = self.findFieldId(fieldName); + if(!foundField) { + PyErr_SetNone(PyExc_KeyError); + throw py::error_already_set{}; + } + return self.fieldType(*foundField); + }, "Type of a named field", py::arg("name")) + .def("field_type", [](Trade::SceneData& self, UnsignedInt id) { + if(id >= self.fieldCount()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + return self.fieldType(id); + }, "Field type", py::arg("id")) + .def("field_size", [](Trade::SceneData& self, Trade::SceneField fieldName) { + const Containers::Optional foundField = self.findFieldId(fieldName); + if(!foundField) { + PyErr_SetNone(PyExc_KeyError); + throw py::error_already_set{}; + } + return self.fieldSize(*foundField); + }, "Number of entries in a named field", py::arg("name")) + .def("field_size", [](Trade::SceneData& self, UnsignedInt id) { + if(id >= self.fieldCount()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + return self.fieldSize(id); + }, "Number of entries in a field", py::arg("id")) + .def("field_array_size", [](Trade::SceneData& self, Trade::SceneField fieldName) { + const Containers::Optional foundField = self.findFieldId(fieldName); + if(!foundField) { + PyErr_SetNone(PyExc_KeyError); + throw py::error_already_set{}; + } + return self.fieldArraySize(*foundField); + }, "Array size of a named field", py::arg("name")) + .def("field_array_size", [](Trade::SceneData& self, UnsignedInt id) { + if(id >= self.fieldCount()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + return self.fieldArraySize(id); + }, "Field array size", py::arg("id")) + .def("field_id", [](Trade::SceneData& self, Trade::SceneField name) { + if(const Containers::Optional found = self.findFieldId(name)) + return *found; + PyErr_SetNone(PyExc_KeyError); + throw py::error_already_set{}; + }, "Absolute ID of a named field", py::arg("name")) + .def("has_field", &Trade::SceneData::hasField, "Whether the scene has given field") + .def("field_object_offset", [](Trade::SceneData& self, Trade::SceneField fieldName, UnsignedLong object, std::size_t offset) { + const Containers::Optional foundField = self.findFieldId(fieldName); + if(!foundField) { + PyErr_SetNone(PyExc_KeyError); + throw py::error_already_set{}; + } + if(object >= self.mappingBound()) { + PyErr_SetString(PyExc_IndexError, "object out of range"); + throw py::error_already_set{}; + } + if(offset >= self.fieldSize(*foundField)) { + PyErr_SetString(PyExc_IndexError, "offset out of range"); + throw py::error_already_set{}; + } + const Containers::Optional found = self.findFieldObjectOffset(*foundField, object, offset); + if(!found) { + PyErr_SetNone(PyExc_LookupError); + throw py::error_already_set{}; + } + return *found; + }, "Offset of an object in given name field", py::arg("field_name"), py::arg("object"), py::arg("offset") = 0) + .def("field_object_offset", [](Trade::SceneData& self, UnsignedInt fieldId, UnsignedLong object, std::size_t offset) { + if(fieldId >= self.fieldCount()) { + PyErr_SetString(PyExc_IndexError, "field out of range"); + throw py::error_already_set{}; + } + if(object >= self.mappingBound()) { + PyErr_SetString(PyExc_IndexError, "object out of range"); + throw py::error_already_set{}; + } + if(offset >= self.fieldSize(fieldId)) { + PyErr_SetString(PyExc_IndexError, "offset out of range"); + throw py::error_already_set{}; + } + const Containers::Optional found = self.findFieldObjectOffset(fieldId, object, offset); + if(!found) { + PyErr_SetNone(PyExc_LookupError); + throw py::error_already_set{}; + } + return *found; + }, "Offset of an object in given field", py::arg("field_id"), py::arg("object"), py::arg("offset") = 0) + .def("has_field_object", [](Trade::SceneData& self, Trade::SceneField fieldName, UnsignedLong object) { + const Containers::Optional foundField = self.findFieldId(fieldName); + if(!foundField) { + PyErr_SetNone(PyExc_KeyError); + throw py::error_already_set{}; + } + if(object >= self.mappingBound()) { + PyErr_SetString(PyExc_IndexError, "object out of range"); + throw py::error_already_set{}; + } + return self.hasFieldObject(*foundField, object); + }, "Whether a scene field has given object", py::arg("field_name"), py::arg("object")) + .def("has_field_object", [](Trade::SceneData& self, UnsignedInt fieldId, UnsignedLong object) { + if(fieldId >= self.fieldCount()) { + PyErr_SetString(PyExc_IndexError, "field out of range"); + throw py::error_already_set{}; + } + if(object >= self.mappingBound()) { + PyErr_SetString(PyExc_IndexError, "object out of range"); + throw py::error_already_set{}; + } + return self.hasFieldObject(fieldId, object); + }, "Whether a scene field has given object", py::arg("field_id"), py::arg("object")); + /* Importer. Skipping file callbacks and openState as those operate with void*. Leaving the name as AbstractImporter (instead of Importer) to avoid needless name differences and because in the future there *might* @@ -830,6 +1142,29 @@ void trade(py::module_& m) { }, "Open a file", py::arg("filename")) .def("close", &Trade::AbstractImporter::close, "Close currently opened file") + .def_property_readonly("default_scene", checkOpened, "Default scene") + .def_property_readonly("scene_count", checkOpened, "Scene count") + .def_property_readonly("object_count", checkOpened, "Object count") + .def("scene_for_name", checkOpenedString, "Scene ID for given name", py::arg("name")) + .def("object_for_name", checkOpenedString, "Object ID for given name", py::arg("name")) + .def("scene_name", checkOpenedBoundsReturnsString, "Scene name", py::arg("id")) + .def("object_name", checkOpenedBoundsReturnsString, "Scene name", py::arg("id")) + .def("scene", checkOpenedBoundsResult, "Scene", py::arg("id")) + .def("scene", checkOpenedBoundsResultString, "Scene for given name", py::arg("name")) + /** @todo drop std::string in favor of our own string caster */ + .def("scene_field_for_name", [](Trade::AbstractImporter& self, const std::string& name) -> Containers::Optional { + const Trade::SceneField field = self.sceneFieldForName(name); + if(field == Trade::SceneField{}) + return {}; + return field; + }, "Scene field for given name", py::arg("name")) + /** @todo drop std::string in favor of our own string caster */ + .def("scene_field_name", [](Trade::AbstractImporter& self, Trade::SceneField name) -> Containers::Optional { + if(const Containers::String field = self.sceneFieldName(name)) + return std::string{field}; + return {}; + }, "String name for given custom scene field", py::arg("name")) + /** @todo all other data types */ .def_property_readonly("mesh_count", checkOpened, "Mesh count") .def("mesh_level_count", checkOpenedBounds, "Mesh level count", py::arg("id"))