From e319f098ebcd70b408a6d35f3d814eeddbab3317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 9 Mar 2023 00:49:20 +0100 Subject: [PATCH] python: expose trade.AbstractConverter.add*_importer_contents(). --- doc/python/magnum.trade.rst | 14 ++++ src/python/magnum/test/test_trade.py | 89 ++++++++++++++++++++++++++ src/python/magnum/test/two-meshes.gltf | 89 ++++++++++++++++++++++++++ src/python/magnum/trade.cpp | 56 +++++++++++++++- 4 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 src/python/magnum/test/two-meshes.gltf diff --git a/doc/python/magnum.trade.rst b/doc/python/magnum.trade.rst index da06f33..642ecf8 100644 --- a/doc/python/magnum.trade.rst +++ b/doc/python/magnum.trade.rst @@ -476,6 +476,12 @@ :ref:`AbstractSceneConverter.manager`, ensuring the manager is not deleted before the plugin instances are. +.. py:enum:: magnum.trade.SceneContents + + The equivalent to C++ :dox:`Trade::sceneContentsFor()` is creating an enum + value using a ``FOR()`` named constructor, passing either an + :ref:`AbstractSceneConverter` or an opened :ref:`AbstractImporter` to it. + .. TODO couldn't the plugin_interface etc. docs be parsed from pybind's docs? repeating them for every plugin is annoying @@ -531,3 +537,11 @@ .. py:function:: magnum.trade.AbstractSceneConverter.set_mesh_attribute_name :raise AssertionError: If no conversion is in progress :raise AssertionError: If :p:`attribute` is not custom + +.. py:function:: magnum.trade.AbstractSceneConverter.add_importer_contents + :raise AssertionError: If no conversion is in progress + :raise RuntimeError: If adding the importer contents fails + +.. py:function:: magnum.trade.AbstractSceneConverter.add_supported_importer_contents + :raise AssertionError: If no conversion is in progress + :raise RuntimeError: If adding the importer contents fails diff --git a/src/python/magnum/test/test_trade.py b/src/python/magnum/test/test_trade.py index ea60193..332b819 100644 --- a/src/python/magnum/test/test_trade.py +++ b/src/python/magnum/test/test_trade.py @@ -1457,6 +1457,27 @@ class ImageConverter(unittest.TestCase): converter.convert_to_file(image, os.path.join(tmp, "image.hdr")) class SceneConverter(unittest.TestCase): + def test_scenecontents_for_importer(self): + # Silly, yes, but don't want to enable StanfordImporter just for this + # test case + importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'rgb.png')) + + self.assertEqual(trade.SceneContents.FOR(importer), trade.SceneContents.IMAGES2D|trade.SceneContents.NAMES) + + def test_scenecontents_for_importer_not_opened(self): + # Silly, yes, but don't want to enable StanfordImporter just for this + # test case + importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') + + with self.assertRaisesRegex(AssertionError, "no file opened"): + trade.SceneContents.FOR(importer) + + def test_scenecontents_for_converter(self): + converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter') + + self.assertEqual(trade.SceneContents.FOR(converter), trade.SceneContents.MESHES|trade.SceneContents.NAMES) + def test(self): converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter') self.assertEqual(converter.features, trade.SceneConverterFeatures.CONVERT_MESH_TO_FILE|trade.SceneConverterFeatures.CONVERT_MESH_TO_DATA) @@ -1625,6 +1646,70 @@ class SceneConverter(unittest.TestCase): with self.assertRaisesRegex(AssertionError, "not a custom attribute"): converter.set_mesh_attribute_name(trade.MeshAttribute.POSITION, 'foo') + def test_batch_add_importer_contents(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'two-meshes.gltf')) + + converter = trade.SceneConverterManager().load_and_instantiate('GltfSceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + filename = os.path.join(tmp, "two-meshes.gltf") + converter.begin_file(filename) + self.assertEqual(converter.mesh_count, 0) + + # Nothing like that in the file + converter.add_importer_contents(importer, trade.SceneContents.SCENES|trade.SceneContents.CAMERAS) + self.assertEqual(converter.mesh_count, 0) + + converter.add_importer_contents(importer) + self.assertEqual(converter.mesh_count, 2) + + def test_batch_add_importer_contents_failed(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'two-meshes.gltf')) + + converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + filename = os.path.join(tmp, "two-meshes.gltf") + converter.begin_file(filename) + + with self.assertRaisesRegex(RuntimeError, "adding importer contents failed"): + converter.add_importer_contents(importer) + + def test_batch_add_supported_importer_contents(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'scene.gltf')) + + converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + filename = os.path.join(tmp, "two-meshes.gltf") + converter.begin_file(filename) + self.assertEqual(converter.mesh_count, 0) + + # Nothing like that in the file + converter.add_supported_importer_contents(importer, trade.SceneContents.MESHES) + self.assertEqual(converter.mesh_count, 0) + + # It contains cameras, nodes and scenes, none of which is supported + # by the converter + converter.add_supported_importer_contents(importer) + self.assertEqual(converter.mesh_count, 0) + + def test_batch_add_supported_importer_contents_failed(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'two-meshes.gltf')) + + converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + filename = os.path.join(tmp, "two-meshes.gltf") + converter.begin_file(filename) + + with self.assertRaisesRegex(RuntimeError, "adding importer contents failed"): + converter.add_supported_importer_contents(importer) + def test_batch_no_conversion_in_progress(self): importer = trade.ImporterManager().load_and_instantiate('GltfImporter') importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) @@ -1641,3 +1726,7 @@ class SceneConverter(unittest.TestCase): converter.add(mesh) with self.assertRaisesRegex(AssertionError, "no conversion in progress"): converter.set_mesh_attribute_name(trade.MeshAttribute.CUSTOM(1), 'foobar') + with self.assertRaisesRegex(AssertionError, "no conversion in progress"): + converter.add_importer_contents(importer) + with self.assertRaisesRegex(AssertionError, "no conversion in progress"): + converter.add_supported_importer_contents(importer) diff --git a/src/python/magnum/test/two-meshes.gltf b/src/python/magnum/test/two-meshes.gltf new file mode 100644 index 0000000..cc3eba5 --- /dev/null +++ b/src/python/magnum/test/two-meshes.gltf @@ -0,0 +1,89 @@ +{ + "asset": { + "version": "2.0", + "note": "same as meshes.gltf, just with no errors, no custom attribs, no attribs that aren't valid glTF and no skinning attribs either" + }, + "meshes": [ + { + "name": "Indexed mesh", + "primitives": [ + { + "attributes": { + "POSITION": 1, + "TEXCOORD_0": 2, + "COLOR": 3, + "TEXCOORD_1": 2 + }, + "indices": 0 + } + ] + }, + { + "name": "Non-indexed mesh", + "primitives": [ + { + "attributes": { + "POSITION": 1 + } + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 2, + "componentType": 5123, + "count": 3, + "type": "SCALAR" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 3, + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 12, + "componentType": 5126, + "count": 3, + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 20, + "componentType": 5121, + "normalized": true, + "count": 3, + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 24, + "componentType": 5125, + "count": 3, + "type": "SCALAR" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 8 + }, + { + "buffer": 0, + "byteOffset": 8, + "byteLength": 84, + "byteStride": 28 + } + ], + "buffers": [ + { + "byteLength": 92, + "uri": "mesh.bin" + } + ] +} + diff --git a/src/python/magnum/trade.cpp b/src/python/magnum/trade.cpp index 82a2f87..194b5ce 100644 --- a/src/python/magnum/trade.cpp +++ b/src/python/magnum/trade.cpp @@ -1630,6 +1630,36 @@ void trade(py::module_& m) { .value("NONE", Trade::SceneConverterFlag{}); corrade::enumOperators(sceneConverterFlags); + py::enum_ sceneContents{m, "SceneContents", "Scene contents"}; + sceneContents + .value("SCENES", Trade::SceneContent::Scenes) + .value("ANIMATIONS", Trade::SceneContent::Animations) + .value("LIGHTS", Trade::SceneContent::Lights) + .value("CAMERAS", Trade::SceneContent::Cameras) + .value("SKINS2D", Trade::SceneContent::Skins2D) + .value("SKINS3D", Trade::SceneContent::Skins3D) + .value("MESHES", Trade::SceneContent::Meshes) + .value("MATERIALS", Trade::SceneContent::Materials) + .value("TEXTURES", Trade::SceneContent::Textures) + .value("IMAGES1D", Trade::SceneContent::Images1D) + .value("IMAGES2D", Trade::SceneContent::Images2D) + .value("IMAGES3D", Trade::SceneContent::Images3D) + .value("MESH_LEVELS", Trade::SceneContent::MeshLevels) + .value("IMAGE_LEVELS", Trade::SceneContent::ImageLevels) + .value("NAMES", Trade::SceneContent::Names) + .value("ALL", Trade::SceneContent(Containers::enumCastUnderlyingType(~Trade::SceneContent{}))) + .def("FOR", [](Trade::AbstractImporter& importer) { + if(!importer.isOpened()) { + PyErr_SetString(PyExc_AssertionError, "no file opened"); + throw py::error_already_set{}; + } + return Trade::SceneContent(Containers::enumCastUnderlyingType(Trade::sceneContentsFor(importer))); + }) + .def("FOR", [](Trade::AbstractSceneConverter& converter) { + return Trade::SceneContent(Containers::enumCastUnderlyingType(Trade::sceneContentsFor(converter))); + }); + corrade::enumOperators(sceneContents); + py::class_, PluginManager::AbstractPlugin> abstractSceneConverter{m, "AbstractSceneConverter", "Interface for scene converter plugins"}; abstractSceneConverter .def_property_readonly("features", [](Trade::AbstractSceneConverter& self) { @@ -1729,7 +1759,31 @@ void trade(py::module_& m) { throw py::error_already_set{}; } self.setMeshAttributeName(attribute, name); - }, "Set name of a custom mesh attribute", py::arg("attribute"), py::arg("name")); + }, "Set name of a custom mesh attribute", py::arg("attribute"), py::arg("name")) + .def("add_importer_contents", [](Trade::AbstractSceneConverter& self, Trade::AbstractImporter& importer, Trade::SceneContent contents) { + if(!self.isConverting()) { + PyErr_SetString(PyExc_AssertionError, "no conversion in progress"); + throw py::error_already_set{}; + } + /** @todo check if contents present in the file are supported? or + make that a runtime failure in Magnum for easier use? */ + if(!self.addImporterContents(importer, contents)) { + PyErr_SetString(PyExc_RuntimeError, "adding importer contents failed"); + throw py::error_already_set{}; + } + }, "Add importer contents", py::arg("importer"), py::arg("contents") = Trade::SceneContent(Containers::enumCastUnderlyingType(~Trade::SceneContent{}))) + .def("add_supported_importer_contents", [](Trade::AbstractSceneConverter& self, Trade::AbstractImporter& importer, Trade::SceneContent contents) { + if(!self.isConverting()) { + PyErr_SetString(PyExc_AssertionError, "no conversion in progress"); + throw py::error_already_set{}; + } + /** @todo check if contents present in the file are supported? or + make that a runtime failure in Magnum for easier use? */ + if(!self.addSupportedImporterContents(importer, contents)) { + PyErr_SetString(PyExc_RuntimeError, "adding importer contents failed"); + throw py::error_already_set{}; + } + }, "Add supported importer contents", py::arg("importer"), py::arg("contents") = Trade::SceneContent(Containers::enumCastUnderlyingType(~Trade::SceneContent{}))); corrade::plugin(abstractSceneConverter); py::class_, PluginManager::AbstractManager> sceneConverterManager{m, "SceneConverterManager", "Manager for scene converter plugins"};