diff --git a/doc/python/magnum.scenetools.rst b/doc/python/magnum.scenetools.rst index 5381ad1..ff71eb5 100644 --- a/doc/python/magnum.scenetools.rst +++ b/doc/python/magnum.scenetools.rst @@ -27,6 +27,14 @@ :raise AssertionError: If size of :p:`fields_to_keep` is different than :ref:`trade.SceneData.field_count` +.. py:function:: magnum.scenetools.filter_field_entries + :raise AssertionError: If any field in :p:`entries_to_keep` does not exist + in :p:`scene` + :raise AssertionError: If any field in :p:`entries_to_keep` is listed more + than once + :raise AssertionError: If size of any array in :p:`entries_to_keep` does + not match :ref:`trade.SceneData.field_size()` for given field + .. 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 1cf491f..ca35358 100644 --- a/src/python/magnum/scenetools.cpp +++ b/src/python/magnum/scenetools.cpp @@ -26,8 +26,10 @@ #include #include /* for std::vector */ #include +#include #include #include +#include #include #include #include @@ -74,6 +76,80 @@ void scenetools(py::module_& m) { 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")) + /** @todo ew, especially the cast .. i hope they have compatible + layout, not like std::tuple */ + /* The enum-based overloads NEEDS to be before the integer overload, + otherwise pybind happily uses the enums as integer values!!! */ + .def("filter_field_entries", [](const Trade::SceneData& scene, const std::vector> entriesToKeepStl) { + const auto entriesToKeep = Containers::arrayCast>(Containers::arrayView(entriesToKeepStl)); + Containers::BitArray usedFields{ValueInit, scene.fieldCount()}; + for(std::size_t i = 0; i != entriesToKeep.size(); ++i) { + const Containers::Optional fieldId = scene.findFieldId(entriesToKeep[i].first()); + if(!fieldId) { + PyErr_Format(PyExc_AssertionError, "field at index %zu not found", i, scene.fieldCount()); + throw py::error_already_set{}; + } + if(usedFields[*fieldId]) { + PyErr_Format(PyExc_AssertionError, "field at index %zu listed more than once", i); + throw py::error_already_set{}; + } + usedFields.set(*fieldId); + const Containers::BitArrayView mask = entriesToKeep[i].second(); + if(mask.size() != scene.fieldSize(*fieldId)) { + PyErr_Format(PyExc_AssertionError, "expected %zu bits for field at index %zu but got %zu", scene.fieldSize(*fieldId), i, mask.size()); + throw py::error_already_set{}; + } + const Trade::SceneFieldType fieldType = scene.fieldType(*fieldId); + if(Trade::Implementation::isSceneFieldTypeString(fieldType)) { + PyErr_SetString(PyExc_NotImplementedError, "filtering string fields is not implemented yet, sorry"); + throw py::error_already_set{}; + } + if(fieldType == Trade::SceneFieldType::Bit) { + PyErr_SetString(PyExc_NotImplementedError, "filtering bit fields is not implemented yet, sorry"); + throw py::error_already_set{}; + } + } + /** @todo check field sharing as well to avoid an assertion -- + make an internal helper in SceneTools or some such, it makes no + sense to duplicate the whole logic here */ + + return SceneTools::filterFieldEntries(scene, entriesToKeep); + }, "Filter individual entries of named fields in a scene", py::arg("scene"), py::arg("entries_to_keep")) + .def("filter_field_entries", [](const Trade::SceneData& scene, const std::vector> entriesToKeepStl) { + const auto entriesToKeep = Containers::arrayCast>(Containers::arrayView(entriesToKeepStl)); + Containers::BitArray usedFields{ValueInit, scene.fieldCount()}; + for(std::size_t i = 0; i != entriesToKeep.size(); ++i) { + const UnsignedInt fieldId = entriesToKeep[i].first(); + if(fieldId >= scene.fieldCount()) { + PyErr_Format(PyExc_AssertionError, "index %u out of range for %u fields", fieldId, scene.fieldCount()); + throw py::error_already_set{}; + } + if(usedFields[fieldId]) { + PyErr_Format(PyExc_AssertionError, "field %u listed more than once", fieldId); + throw py::error_already_set{}; + } + usedFields.set(fieldId); + const Containers::BitArrayView mask = entriesToKeep[i].second(); + if(mask.size() != scene.fieldSize(fieldId)) { + PyErr_Format(PyExc_AssertionError, "expected %zu bits for field %u but got %zu", scene.fieldSize(fieldId), fieldId, mask.size()); + throw py::error_already_set{}; + } + const Trade::SceneFieldType fieldType = scene.fieldType(fieldId); + if(Trade::Implementation::isSceneFieldTypeString(fieldType)) { + PyErr_SetString(PyExc_NotImplementedError, "filtering string fields is not implemented yet, sorry"); + throw py::error_already_set{}; + } + if(fieldType == Trade::SceneFieldType::Bit) { + PyErr_SetString(PyExc_NotImplementedError, "filtering bit fields is not implemented yet, sorry"); + throw py::error_already_set{}; + } + } + /** @todo check field sharing as well to avoid an assertion -- + make an internal helper in SceneTools or some such, it makes no + sense to duplicate the whole logic here */ + + return SceneTools::filterFieldEntries(scene, entriesToKeep); + }, "Filter individual entries of fields in a scene", py::arg("scene"), py::arg("entries_to_keep")) .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 77ea044..dcc2328 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 os import sys import unittest @@ -168,6 +169,111 @@ class Filter(unittest.TestCase): del filtered2 self.assertEqual(sys.getrefcount(scene), scene_refcount) + def test_field_entries(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "scene.gltf")) + + scene = importer.scene(0) + scene_refcount = sys.getrefcount(scene) + self.assertEqual(scene.field_count, 8) + self.assertEqual(scene.field_size(trade.SceneField.PARENT), 4) + self.assertEqual(scene.field_size(trade.SceneField.IMPORTER_STATE), 4) + self.assertEqual(scene.field_size(trade.SceneField.TRANSFORMATION), 4) + self.assertEqual(scene.field_size(trade.SceneField.CAMERA), 2) + + # Remove two parents (and importer state, which is linked), one camera + # and all but one transformation + parents_to_keep = containers.BitArray.direct_init(scene.field_size(trade.SceneField.PARENT), True) + parents_to_keep[0] = False + parents_to_keep[2] = False + + transformations_to_keep = containers.BitArray.direct_init(scene.field_size(trade.SceneField.TRANSFORMATION), False) + transformations_to_keep[3] = True + + cameras_to_keep = containers.BitArray.direct_init(scene.field_size(trade.SceneField.CAMERA), True) + cameras_to_keep[1] = False + + filtered1 = scenetools.filter_field_entries(scene, [ + (trade.SceneField.PARENT, parents_to_keep), + (trade.SceneField.IMPORTER_STATE, parents_to_keep), + (trade.SceneField.TRANSFORMATION, transformations_to_keep), + (trade.SceneField.CAMERA, cameras_to_keep) + ]) + filtered2 = scenetools.filter_field_entries(scene, [ + (scene.field_id(trade.SceneField.PARENT), parents_to_keep), + (scene.field_id(trade.SceneField.IMPORTER_STATE), parents_to_keep), + (scene.field_id(trade.SceneField.TRANSFORMATION), transformations_to_keep), + (scene.field_id(trade.SceneField.CAMERA), cameras_to_keep) + ]) + self.assertEqual(filtered1.field_count, 8) + self.assertEqual(filtered2.field_count, 8) + self.assertEqual(filtered1.field_size(trade.SceneField.PARENT), 2) + self.assertEqual(filtered2.field_size(trade.SceneField.PARENT), 2) + self.assertEqual(filtered1.field_size(trade.SceneField.TRANSFORMATION), 1) + self.assertEqual(filtered2.field_size(trade.SceneField.TRANSFORMATION), 1) + self.assertEqual(filtered1.field_size(trade.SceneField.CAMERA), 1) + self.assertEqual(filtered2.field_size(trade.SceneField.CAMERA), 1) + # The original scene isn't referenced by these, it's a full copy + self.assertEqual(sys.getrefcount(scene), scene_refcount) + + def test_field_entries_invalid(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "scene.gltf")) + + scene = importer.scene(0) + scene_refcount = sys.getrefcount(scene) + self.assertEqual(scene.field_count, 8) + + with self.assertRaisesRegex(AssertionError, "index 8 out of range for 8 fields"): + scenetools.filter_field_entries(scene, [ + (8, containers.BitArrayView()) + ]) + with self.assertRaisesRegex(AssertionError, "field at index 1 not found"): + scenetools.filter_field_entries(scene, [ + (trade.SceneField.CAMERA, containers.BitArray.value_init(2)), + (trade.SceneField.LIGHT, containers.BitArrayView()) + ]) + + with self.assertRaisesRegex(AssertionError, "field at index 2 listed more than once"): + scenetools.filter_field_entries(scene, [ + (trade.SceneField.CAMERA, containers.BitArray.value_init(2)), + (trade.SceneField.TRANSLATION, containers.BitArray.value_init(3)), + (trade.SceneField.CAMERA, containers.BitArray.value_init(2)) + ]) + with self.assertRaisesRegex(AssertionError, "field 4 listed more than once"): + scenetools.filter_field_entries(scene, [ + (scene.field_id(trade.SceneField.CAMERA), containers.BitArray.value_init(2)), + (scene.field_id(trade.SceneField.TRANSLATION), containers.BitArray.value_init(3)), + (scene.field_id(trade.SceneField.CAMERA), containers.BitArray.value_init(2)) + ]) + + with self.assertRaisesRegex(AssertionError, "expected 3 bits for field 3 but got 4"): + scenetools.filter_field_entries(scene, [ + (scene.field_id(trade.SceneField.TRANSLATION), containers.BitArray.value_init(4)) + ]) + with self.assertRaisesRegex(AssertionError, "expected 3 bits for field at index 1 but got 4"): + scenetools.filter_field_entries(scene, [ + (trade.SceneField.CAMERA, containers.BitArray.value_init(2)), + (trade.SceneField.TRANSLATION, containers.BitArray.value_init(4)) + ]) + + with self.assertRaisesRegex(NotImplementedError, "filtering string fields is not implemented yet, sorry"): + scenetools.filter_field_entries(scene, [ + (scene.field_id(importer.scene_field_for_name('aString')), containers.BitArray.value_init(1)) + ]) + with self.assertRaisesRegex(NotImplementedError, "filtering string fields is not implemented yet, sorry"): + scenetools.filter_field_entries(scene, [ + (importer.scene_field_for_name('aString'), containers.BitArray.value_init(1)) + ]) + with self.assertRaisesRegex(NotImplementedError, "filtering bit fields is not implemented yet, sorry"): + scenetools.filter_field_entries(scene, [ + (scene.field_id(importer.scene_field_for_name('yes')), containers.BitArray.value_init(2)) + ]) + with self.assertRaisesRegex(NotImplementedError, "filtering bit fields is not implemented yet, sorry"): + scenetools.filter_field_entries(scene, [ + (importer.scene_field_for_name('yes'), containers.BitArray.value_init(2)) + ]) + class Hierarchy(unittest.TestCase): def test_absolute_field_transformations2d(self): # Static builds with non-static plugins cause assertions with non-owned