From 4be2d4d9c0a560a4cacdef4ba4a6e0e2eb3c0439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 17 May 2023 17:12:47 +0200 Subject: [PATCH] python: expose scenetools.filter_fields(), filter_{only,except}_fields(). --- doc/python/magnum.scenetools.rst | 4 + src/python/magnum/scenetools.cpp | 30 ++++- src/python/magnum/test/test_scenetools.py | 138 ++++++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) diff --git a/doc/python/magnum.scenetools.rst b/doc/python/magnum.scenetools.rst index 19d6b0e..5381ad1 100644 --- a/doc/python/magnum.scenetools.rst +++ b/doc/python/magnum.scenetools.rst @@ -23,6 +23,10 @@ DEALINGS IN THE SOFTWARE. .. +.. py:function:: magnum.scenetools.filter_fields + :raise AssertionError: If size of :p:`fields_to_keep` is different than + :ref:`trade.SceneData.field_count` + .. py:function:: magnum.scenetools.absolute_field_transformations2d :raise KeyError: If :p:`field` does not exist in :p:`scene` :raise IndexError: If :p:`field_id` negative or not less than diff --git a/src/python/magnum/scenetools.cpp b/src/python/magnum/scenetools.cpp index de5ecba..1cf491f 100644 --- a/src/python/magnum/scenetools.cpp +++ b/src/python/magnum/scenetools.cpp @@ -25,13 +25,18 @@ #include #include /* for std::vector */ -#include #include +#include +#include #include #include +#include #include #include +#include "Corrade/PythonBindings.h" +#include "Magnum/Trade/PythonBindings.h" + #include "magnum/bootstrap.h" namespace magnum { @@ -46,6 +51,29 @@ void scenetools(py::module_& m) { #endif m + .def("filter_fields", [](const Trade::SceneData& scene, const Containers::BitArrayView fieldsToKeep) { + if(fieldsToKeep.size() != scene.fieldCount()) { + PyErr_Format(PyExc_AssertionError, "expected %u bits but got %zu", scene.fieldCount(), fieldsToKeep.size()); + throw py::error_already_set{}; + } + + /* If the scene already has an owner, use that instead to avoid + long reference chains */ + py::object sceneOwner = pyObjectHolderFor(scene).owner; + return Trade::pyDataHolder(SceneTools::filterFields(scene, fieldsToKeep), sceneOwner.is_none() ? py::cast(scene) : std::move(sceneOwner)); + }, "Filter a scene to contain only the selected subset of fields", py::arg("scene"), py::arg("fields_to_keep")) + .def("filter_only_fields", [](const Trade::SceneData& scene, const std::vector fields) { + /* If the scene already has an owner, use that instead to avoid + long reference chains */ + py::object sceneOwner = pyObjectHolderFor(scene).owner; + return Trade::pyDataHolder(SceneTools::filterOnlyFields(scene, fields), sceneOwner.is_none() ? py::cast(scene) : std::move(sceneOwner)); + }, "Filter a scene to contain only the selected subset of named fields", py::arg("scene"), py::arg("fields")) + .def("filter_except_fields", [](const Trade::SceneData& scene, const std::vector fields) { + /* If the scene already has an owner, use that instead to avoid + long reference chains */ + py::object sceneOwner = pyObjectHolderFor(scene).owner; + return Trade::pyDataHolder(SceneTools::filterExceptFields(scene, fields), sceneOwner.is_none() ? py::cast(scene) : std::move(sceneOwner)); + }, "Filter a scene to contain everything the selected subset of named fields", py::arg("scene"), py::arg("fields")) .def("absolute_field_transformations2d", [](const Trade::SceneData& scene, Trade::SceneField field, const Matrix3& globalTransformation) { const Containers::Optional fieldId = scene.findFieldId(field); if(!fieldId) { diff --git a/src/python/magnum/test/test_scenetools.py b/src/python/magnum/test/test_scenetools.py index 7268cd2..77ea044 100644 --- a/src/python/magnum/test/test_scenetools.py +++ b/src/python/magnum/test/test_scenetools.py @@ -23,6 +23,7 @@ # DEALINGS IN THE SOFTWARE. # +import sys import unittest from corrade import containers @@ -30,6 +31,143 @@ from magnum import * from magnum import scenetools, trade import magnum +class Filter(unittest.TestCase): + def test_fields(self): + # Static builds with non-static plugins cause assertions with non-owned + # array deleters used by PrimitiveImporter, skip in that case + if magnum.BUILD_STATIC: + self.skipTest("dynamic PrimitiveImporter doesn't work with a static build") + + importer = trade.ImporterManager().load_and_instantiate('PrimitiveImporter') + importer.open_data(containers.ArrayView()) + + scene = importer.scene(0) + scene_refcount = sys.getrefcount(scene) + + self.assertEqual(scene.field_count, 3) + self.assertTrue(scene.has_field(trade.SceneField.TRANSLATION)) + self.assertTrue(scene.has_field(trade.SceneField.MESH)) + + fields_to_keep = containers.BitArray.value_init(scene.field_count) + fields_to_keep[scene.field_id(trade.SceneField.TRANSLATION)] = True + fields_to_keep[scene.field_id(trade.SceneField.MESH)] = True + + filtered = scenetools.filter_fields(scene, fields_to_keep) + filtered_refcount = sys.getrefcount(filtered) + self.assertEqual(filtered.field_count, 2) + self.assertTrue(filtered.has_field(trade.SceneField.TRANSLATION)) + self.assertTrue(filtered.has_field(trade.SceneField.MESH)) + self.assertEqual(sys.getrefcount(scene), scene_refcount + 1) + self.assertIs(filtered.owner, scene) + + # Subsequent filtering will still reference the original scene, not the + # intermediates + filtered2 = scenetools.filter_fields(filtered, containers.BitArray.direct_init(filtered.field_count, True)) + self.assertEqual(filtered2.field_count, 2) + self.assertTrue(filtered2.has_field(trade.SceneField.TRANSLATION)) + self.assertTrue(filtered2.has_field(trade.SceneField.MESH)) + self.assertEqual(sys.getrefcount(filtered), filtered_refcount) + self.assertEqual(sys.getrefcount(scene), scene_refcount + 2) + self.assertIs(filtered2.owner, scene) + + del filtered + self.assertEqual(sys.getrefcount(scene), scene_refcount + 1) + + del filtered2 + self.assertEqual(sys.getrefcount(scene), scene_refcount) + + def test_fields_invalid_size(self): + # Static builds with non-static plugins cause assertions with non-owned + # array deleters used by PrimitiveImporter, skip in that case + if magnum.BUILD_STATIC: + self.skipTest("dynamic PrimitiveImporter doesn't work with a static build") + + importer = trade.ImporterManager().load_and_instantiate('PrimitiveImporter') + importer.open_data(containers.ArrayView()) + + scene = importer.scene(0) + + with self.assertRaisesRegex(AssertionError, "expected 3 bits but got 4"): + scenetools.filter_fields(scene, containers.BitArray.value_init(4)) + + def test_only_fields(self): + # Static builds with non-static plugins cause assertions with non-owned + # array deleters used by PrimitiveImporter, skip in that case + if magnum.BUILD_STATIC: + self.skipTest("dynamic PrimitiveImporter doesn't work with a static build") + + importer = trade.ImporterManager().load_and_instantiate('PrimitiveImporter') + importer.open_data(containers.ArrayView()) + + scene = importer.scene(0) + scene_refcount = sys.getrefcount(scene) + + self.assertEqual(scene.field_count, 3) + + # Fields that are not present in the mesh are deliberately ignored + filtered = scenetools.filter_only_fields(scene, [trade.SceneField.LIGHT, trade.SceneField.MESH, trade.SceneField.TRANSLATION]) + filtered_refcount = sys.getrefcount(filtered) + self.assertEqual(filtered.field_count, 2) + self.assertTrue(filtered.has_field(trade.SceneField.TRANSLATION)) + self.assertTrue(filtered.has_field(trade.SceneField.MESH)) + self.assertEqual(sys.getrefcount(scene), scene_refcount + 1) + self.assertIs(filtered.owner, scene) + + # Subsequent filtering will still reference the original scene, not the + # intermediates + filtered2 = scenetools.filter_only_fields(filtered, [trade.SceneField.MESH, trade.SceneField.TRANSLATION]) + self.assertEqual(filtered2.field_count, 2) + self.assertTrue(filtered2.has_field(trade.SceneField.TRANSLATION)) + self.assertTrue(filtered2.has_field(trade.SceneField.MESH)) + self.assertEqual(sys.getrefcount(filtered), filtered_refcount) + self.assertEqual(sys.getrefcount(scene), scene_refcount + 2) + self.assertIs(filtered2.owner, scene) + + del filtered + self.assertEqual(sys.getrefcount(scene), scene_refcount + 1) + + del filtered2 + self.assertEqual(sys.getrefcount(scene), scene_refcount) + + def test_except_fields(self): + # Static builds with non-static plugins cause assertions with non-owned + # array deleters used by PrimitiveImporter, skip in that case + if magnum.BUILD_STATIC: + self.skipTest("dynamic PrimitiveImporter doesn't work with a static build") + + importer = trade.ImporterManager().load_and_instantiate('PrimitiveImporter') + importer.open_data(containers.ArrayView()) + + scene = importer.scene(0) + scene_refcount = sys.getrefcount(scene) + + self.assertEqual(scene.field_count, 3) + + # Fields that are not present in the mesh are deliberately ignored + filtered = scenetools.filter_except_fields(scene, [trade.SceneField.SKIN, trade.SceneField.PARENT]) + filtered_refcount = sys.getrefcount(filtered) + self.assertEqual(filtered.field_count, 2) + self.assertTrue(filtered.has_field(trade.SceneField.TRANSLATION)) + self.assertTrue(filtered.has_field(trade.SceneField.MESH)) + self.assertEqual(sys.getrefcount(scene), scene_refcount + 1) + self.assertIs(filtered.owner, scene) + + # Subsequent filtering will still reference the original scene, not the + # intermediates + filtered2 = scenetools.filter_except_fields(filtered, [trade.SceneField.PARENT]) + self.assertEqual(filtered2.field_count, 2) + self.assertTrue(filtered2.has_field(trade.SceneField.TRANSLATION)) + self.assertTrue(filtered2.has_field(trade.SceneField.MESH)) + self.assertEqual(sys.getrefcount(filtered), filtered_refcount) + self.assertEqual(sys.getrefcount(scene), scene_refcount + 2) + self.assertIs(filtered2.owner, scene) + + del filtered + self.assertEqual(sys.getrefcount(scene), scene_refcount + 1) + + del filtered2 + self.assertEqual(sys.getrefcount(scene), scene_refcount) + class Hierarchy(unittest.TestCase): def test_absolute_field_transformations2d(self): # Static builds with non-static plugins cause assertions with non-owned