From d29c5554d8e7fbab95f0332c65ec059ccc074b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 8 Mar 2023 22:49:57 +0100 Subject: [PATCH] python: expose trade.AbstractSceneConverter batch mode for files. Just meshes for now. --- doc/python/magnum.trade.rst | 18 +++++ package/ci/appveyor-desktop-gles.bat | 1 + package/ci/appveyor-desktop.bat | 1 + package/ci/unix-desktop-gles.sh | 1 + package/ci/unix-desktop.sh | 1 + src/python/magnum/test/mesh.gltf | 21 ++++++ src/python/magnum/test/test_trade.py | 101 ++++++++++++++++++++++++++- src/python/magnum/trade.cpp | 55 ++++++++++++++- 8 files changed, 197 insertions(+), 2 deletions(-) diff --git a/doc/python/magnum.trade.rst b/doc/python/magnum.trade.rst index 1a51564..da06f33 100644 --- a/doc/python/magnum.trade.rst +++ b/doc/python/magnum.trade.rst @@ -513,3 +513,21 @@ all backslashes in :p:`filename` to forward slashes before passing it to :dox:`Trade::AbstractSceneConverter::convertToFile()`, which expects forward slashes as directory separators on all platforms. + +.. py:function:: magnum.trade.AbstractSceneConverter.begin_file + :raise RuntimeError: If beginning the conversion fails + +.. py:function:: magnum.trade.AbstractSceneConverter.end_file + :raise AssertionError: If no conversion is in progress + :raise RuntimeError: If ending the conversion fails + +.. py:property:: magnum.trade.AbstractSceneConverter.mesh_count + :raise AssertionError: If no conversion is in progress + +.. py:function:: magnum.trade.AbstractSceneConverter.add + :raise AssertionError: If no conversion is in progress + :raise RuntimeError: If adding the data fails + +.. 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 diff --git a/package/ci/appveyor-desktop-gles.bat b/package/ci/appveyor-desktop-gles.bat index 3a354b3..45d460f 100644 --- a/package/ci/appveyor-desktop-gles.bat +++ b/package/ci/appveyor-desktop-gles.bat @@ -78,6 +78,7 @@ cmake .. ^ -DMAGNUM_BUILD_STATIC=%BUILD_STATIC% ^ -DMAGNUM_WITH_DDSIMPORTER=ON ^ -DMAGNUM_WITH_GLTFIMPORTER=ON ^ + -DMAGNUM_WITH_GLTFSCENECONVERTER=ON ^ -DMAGNUM_WITH_MESHOPTIMIZERSCENECONVERTER=ON ^ -DMAGNUM_WITH_STANFORDSCENECONVERTER=ON ^ -DMAGNUM_WITH_STBIMAGECONVERTER=ON ^ diff --git a/package/ci/appveyor-desktop.bat b/package/ci/appveyor-desktop.bat index 663b61e..68ab338 100644 --- a/package/ci/appveyor-desktop.bat +++ b/package/ci/appveyor-desktop.bat @@ -88,6 +88,7 @@ cmake .. ^ -DMAGNUM_BUILD_STATIC=%BUILD_STATIC% ^ -DMAGNUM_WITH_DDSIMPORTER=ON ^ -DMAGNUM_WITH_GLTFIMPORTER=ON ^ + -DMAGNUM_WITH_GLTFSCENECONVERTER=ON ^ -DMAGNUM_WITH_MESHOPTIMIZERSCENECONVERTER=%EXCEPT_MSVC2017% ^ -DMAGNUM_WITH_STANFORDSCENECONVERTER=ON ^ -DMAGNUM_WITH_STBIMAGECONVERTER=ON ^ diff --git a/package/ci/unix-desktop-gles.sh b/package/ci/unix-desktop-gles.sh index 8f98ff5..cd78f2f 100755 --- a/package/ci/unix-desktop-gles.sh +++ b/package/ci/unix-desktop-gles.sh @@ -65,6 +65,7 @@ cmake .. \ -DMAGNUM_BUILD_STATIC=$BUILD_STATIC \ -DMAGNUM_WITH_DDSIMPORTER=ON \ -DMAGNUM_WITH_GLTFIMPORTER=ON \ + -DMAGNUM_WITH_GLTFSCENECONVERTER=ON \ -DMAGNUM_WITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DMAGNUM_WITH_STANFORDSCENECONVERTER=ON \ -DMAGNUM_WITH_STBIMAGECONVERTER=ON \ diff --git a/package/ci/unix-desktop.sh b/package/ci/unix-desktop.sh index e97eb2e..f408b45 100755 --- a/package/ci/unix-desktop.sh +++ b/package/ci/unix-desktop.sh @@ -69,6 +69,7 @@ cmake .. \ -DMAGNUM_BUILD_STATIC=$BUILD_STATIC \ -DMAGNUM_WITH_DDSIMPORTER=ON \ -DMAGNUM_WITH_GLTFIMPORTER=ON \ + -DMAGNUM_WITH_GLTFSCENECONVERTER=ON \ -DMAGNUM_WITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DMAGNUM_WITH_STANFORDSCENECONVERTER=ON \ -DMAGNUM_WITH_STBIMAGECONVERTER=ON \ diff --git a/src/python/magnum/test/mesh.gltf b/src/python/magnum/test/mesh.gltf index abda4e0..22a50dc 100644 --- a/src/python/magnum/test/mesh.gltf +++ b/src/python/magnum/test/mesh.gltf @@ -39,6 +39,27 @@ "mode": 666 } ] + }, + { + "name": "Point mesh", + "primitives": [ + { + "attributes": { + "POSITION": 1 + }, + "mode": 0 + } + ] + }, + { + "name": "Custom mesh attribute", + "primitives": [ + { + "attributes": { + "_FOOBARTHINGY": 1 + } + } + ] } ], "accessors": [ diff --git a/src/python/magnum/test/test_trade.py b/src/python/magnum/test/test_trade.py index 1363560..ea60193 100644 --- a/src/python/magnum/test/test_trade.py +++ b/src/python/magnum/test/test_trade.py @@ -1306,7 +1306,7 @@ class Importer(unittest.TestCase): self.assertIsNone(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE")) importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) - self.assertEqual(importer.mesh_count, 3) + self.assertEqual(importer.mesh_count, 5) self.assertEqual(importer.mesh_level_count(0), 1) self.assertEqual(importer.mesh_name(0), 'Indexed mesh') self.assertEqual(importer.mesh_for_name('Indexed mesh'), 0) @@ -1542,3 +1542,102 @@ class SceneConverter(unittest.TestCase): with tempfile.TemporaryDirectory() as tmp: with self.assertRaisesRegex(RuntimeError, "conversion failed"): converter.convert_to_file(mesh, os.path.join(tmp, "mesh.obj")) + + def test_batch_file(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + mesh = importer.mesh(1) + + converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + converter.begin_file(os.path.join(tmp, "mesh.ply")) + self.assertTrue(converter.is_converting) + self.assertEqual(converter.mesh_count, 0) + + self.assertEqual(converter.add(mesh), 0) + self.assertEqual(converter.mesh_count, 1) + + converter.end_file() + self.assertFalse(converter.is_converting) + + self.assertTrue(os.path.exists(os.path.join(tmp, "mesh.ply"))) + + def test_batch_file_begin_failed(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + mesh = importer.mesh(1) + + converter = trade.SceneConverterManager().load_and_instantiate('AnySceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + with self.assertRaisesRegex(RuntimeError, "beginning the conversion failed"): + converter.begin_file(os.path.join(tmp, "mesh.obj")) + + def test_batch_file_end_failed(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + mesh = importer.mesh(1) + + converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + converter.begin_file(os.path.join(tmp, "mesh.ply")) + with self.assertRaisesRegex(RuntimeError, "ending the conversion failed"): + converter.end_file() + + def test_batch_add_mesh_failed(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + mesh = importer.mesh('Point mesh') + + converter = trade.SceneConverterManager().load_and_instantiate('StanfordSceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + converter.begin_file(os.path.join(tmp, "mesh.ply")) + with self.assertRaisesRegex(RuntimeError, "adding the mesh failed"): + converter.add(mesh) + + def test_batch_set_mesh_attribute_name(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf')) + mesh = importer.mesh('Custom mesh attribute') + + converter = trade.SceneConverterManager().load_and_instantiate('GltfSceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + filename = os.path.join(tmp, "mesh.gltf") + converter.begin_file(filename) + converter.set_mesh_attribute_name(importer.mesh_attribute_for_name('_FOOBARTHINGY'), '_FOOBARTHINGY') + converter.add(mesh) + converter.end_file() + + with open(filename, 'r') as f: + self.assertIn('_FOOBARTHINGY', f.read()) + + def test_batch_set_mesh_attribute_name_not_custom(self): + converter = trade.SceneConverterManager().load_and_instantiate('GltfSceneConverter') + + with tempfile.TemporaryDirectory() as tmp: + filename = os.path.join(tmp, "mesh.gltf") + converter.begin_file(filename) + + with self.assertRaisesRegex(AssertionError, "not a custom attribute"): + converter.set_mesh_attribute_name(trade.MeshAttribute.POSITION, 'foo') + + 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')) + mesh = importer.mesh('Custom mesh attribute') + + converter = trade.SceneConverterManager().load_and_instantiate('GltfSceneConverter') + self.assertFalse(converter.is_converting) + + with self.assertRaisesRegex(AssertionError, "no conversion in progress"): + converter.end_file() + with self.assertRaisesRegex(AssertionError, "no conversion in progress"): + converter.mesh_count + with self.assertRaisesRegex(AssertionError, "no conversion in progress"): + converter.add(mesh) + with self.assertRaisesRegex(AssertionError, "no conversion in progress"): + converter.set_mesh_attribute_name(trade.MeshAttribute.CUSTOM(1), 'foobar') diff --git a/src/python/magnum/trade.cpp b/src/python/magnum/trade.cpp index 262077f..82a2f87 100644 --- a/src/python/magnum/trade.cpp +++ b/src/python/magnum/trade.cpp @@ -1676,7 +1676,60 @@ void trade(py::module_& m) { PyErr_SetString(PyExc_RuntimeError, "conversion failed"); throw py::error_already_set{}; } - }, "Convert a mesh to a file", py::arg("mesh"), py::arg("filename")); + }, "Convert a mesh to a file", py::arg("mesh"), py::arg("filename")) + .def_property_readonly("is_converting", &Trade::AbstractSceneConverter::isConverting, "Whether any conversion is in progress") + .def("abort", &Trade::AbstractSceneConverter::abort, "Abort any in-progress conversion") + /** @todo begin/end (MeshOptimizer), begin/end data */ + /** @todo drop std::string in favor of our own string caster */ + .def("begin_file", [](Trade::AbstractSceneConverter& self, const std::string& filename) { + if(!self.beginFile(filename)) { + PyErr_SetString(PyExc_RuntimeError, "beginning the conversion failed"); + throw py::error_already_set{}; + } + }, "Begin converting a scene to a file", py::arg("filename")) + .def("end_file", [](Trade::AbstractSceneConverter& self) { + /** @todo this doesn't catch a mismatch (e.g., when beginData() was + called instead */ + if(!self.isConverting()) { + PyErr_SetString(PyExc_AssertionError, "no conversion in progress"); + throw py::error_already_set{}; + } + if(!self.endFile()) { + PyErr_SetString(PyExc_RuntimeError, "ending the conversion failed"); + throw py::error_already_set{}; + } + }, "End converting a scene to a file") + .def_property_readonly("mesh_count", [](Trade::AbstractSceneConverter& self) { + if(!self.isConverting()) { + PyErr_SetString(PyExc_AssertionError, "no conversion in progress"); + throw py::error_already_set{}; + } + return self.meshCount(); + }, "Count of added meshes") + /** @todo drop std::string in favor of our own string caster */ + .def("add", [](Trade::AbstractSceneConverter& self, const Trade::MeshData& mesh, const std::string& name) { + if(!self.isConverting()) { + PyErr_SetString(PyExc_AssertionError, "no conversion in progress"); + throw py::error_already_set{}; + } + if(const Containers::Optional out = self.add(mesh, name)) + return *out; + + PyErr_SetString(PyExc_RuntimeError, "adding the mesh failed"); + throw py::error_already_set{}; + }, "Add a mesh", py::arg("mesh"), py::arg("name") = std::string{}) + /** @todo mesh levels */ + .def("set_mesh_attribute_name", [](Trade::AbstractSceneConverter& self, const Trade::MeshAttribute attribute, const std::string& name) { + if(!Trade::isMeshAttributeCustom(attribute)) { + PyErr_SetString(PyExc_AssertionError, "not a custom attribute"); + throw py::error_already_set{}; + } + if(!self.isConverting()) { + PyErr_SetString(PyExc_AssertionError, "no conversion in progress"); + throw py::error_already_set{}; + } + self.setMeshAttributeName(attribute, name); + }, "Set name of a custom mesh attribute", py::arg("attribute"), py::arg("name")); corrade::plugin(abstractSceneConverter); py::class_, PluginManager::AbstractManager> sceneConverterManager{m, "SceneConverterManager", "Manager for scene converter plugins"};