diff --git a/doc/snippets/MagnumSceneTools.cpp b/doc/snippets/MagnumSceneTools.cpp index f7c02ca88..9369efc94 100644 --- a/doc/snippets/MagnumSceneTools.cpp +++ b/doc/snippets/MagnumSceneTools.cpp @@ -24,6 +24,8 @@ */ #include +#include +#include #include #include #include @@ -32,6 +34,7 @@ #include "Magnum/Math/Matrix4.h" #include "Magnum/MeshTools/Concatenate.h" #include "Magnum/MeshTools/Transform.h" +#include "Magnum/SceneTools/Filter.h" #include "Magnum/SceneTools/Hierarchy.h" #include "Magnum/Trade/SceneData.h" #include "Magnum/Trade/MeshData.h" @@ -41,6 +44,22 @@ using namespace Magnum; int main() { +{ +/* [filterFieldEntries-shared-mapping] */ +Trade::SceneData scene = DOXYGEN_ELLIPSIS(Trade::SceneData{{}, 0, nullptr, {}}); + +Containers::BitArray transformationsToKeep = DOXYGEN_ELLIPSIS({}); +Containers::BitArray lightsToKeep = DOXYGEN_ELLIPSIS({}); + +/* Mesh and MeshMaterial fields stay unchanged */ +Trade::SceneData filtered = SceneTools::filterFieldEntries(scene, { + {Trade::SceneField::Translation, transformationsToKeep}, + {Trade::SceneField::Rotation, transformationsToKeep}, + {Trade::SceneField::Light, lightsToKeep} +}); +/* [filterFieldEntries-shared-mapping] */ +} + { /* [absoluteFieldTransformations2D-mesh-concatenate] */ Trade::SceneData scene = DOXYGEN_ELLIPSIS(Trade::SceneData{{}, 0, nullptr, {}}); diff --git a/src/Magnum/SceneTools/Filter.cpp b/src/Magnum/SceneTools/Filter.cpp index 1aa795c26..fdda8e841 100644 --- a/src/Magnum/SceneTools/Filter.cpp +++ b/src/Magnum/SceneTools/Filter.cpp @@ -25,10 +25,14 @@ #include "Filter.h" +#include #include #include #include +#include +#include +#include "Magnum/SceneTools/Combine.h" #include "Magnum/Trade/SceneData.h" namespace Magnum { namespace SceneTools { @@ -76,4 +80,183 @@ Trade::SceneData filterExceptFields(const Trade::SceneData& scene, std::initiali return filterExceptFields(scene, Containers::arrayView(fields)); } +Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, const Containers::ArrayView> entriesToKeep) { + /* Track unique mapping views (pointer, size, stride) so fields that shared + a mapping before stay shared after as well -- if they're filtered, they + will have the mapping allocated in SharedMapping::filteredMapping() + instead of just a null placeholder when passing the filtered fields to + combineFields(), which will ensure they stay shared. If they're not + filtered, the original field view will get passed through, which ensures + the same. This also conveniently handles all cases of enforced mapping + such as for TRS fields so we don't need to special-case that here again. */ + struct SharedMapping { + /* How many times given mapping is shared */ + UnsignedInt count = 1; + /* How many times given mapping is filtered. Should be either 0 or same + as `count`. */ + UnsignedInt filteredCount = 0; + #ifndef CORRADE_NO_ASSERT + /* Index in `entriesToKeep` that contains the filtering mask. All other + entries should use the same view (same pointer, offset and size). */ + UnsignedInt maskIndex = ~UnsignedInt{}; + #endif + /* Data array allocated for this mapping, in order to have + combineFields() preserve their sharing in the output. Doesn't + contain any actual data, it's used just to have a unique + (pointer, size, stride) combination. */ + /** @todo any idea how to do this without the throwaway allocations? */ + Containers::Array filteredMapping; + }; + /* A map is used because it has conveniently implemented ordering, + an unordered_map couldn't be used without manually implementing a + std::tuple hash because STL DOES NOT HAVE IT, UGH. */ + std::map, SharedMapping> uniqueMappings; + for(UnsignedInt i = 0; i != scene.fieldCount(); ++i) { + /* Skip empty fields as those make no sense to include for sharing */ + if(!scene.fieldSize(i)) + continue; + + const Containers::StridedArrayView2D mapping = scene.mapping(i); + const std::pair, SharedMapping>::iterator, bool> inserted = uniqueMappings.emplace(std::make_tuple(mapping.data(), mapping.size()[0], mapping.stride()[0]), SharedMapping{}); + if(!inserted.second) + ++inserted.first->second.count; + } + + /* Copy all field metadata. By default, if the field isn't referenced, it's + kept in full. Can't use Utility::copy() on the whole fieldData() array + as those can be offset-only. */ + Containers::Array fields{ValueInit, scene.fieldCount()}; + for(std::size_t i = 0; i != scene.fieldCount(); ++i) + fields[i] = scene.fieldData(i); + + const std::size_t mappingTypeSize = Trade::sceneMappingTypeSize(scene.mappingType()); + + /* For fields that are being filtered update the field size and turn it + into a placeholder */ + #ifndef CORRADE_NO_ASSERT + Containers::BitArray usedFields{ValueInit, scene.fieldCount()}; + #endif + for(std::size_t i = 0; i != entriesToKeep.size(); ++i) { + const UnsignedInt fieldId = entriesToKeep[i].first(); + const Containers::BitArrayView mask = entriesToKeep[i].second(); + + CORRADE_ASSERT(fieldId < scene.fieldCount(), + "SceneTools::filterFieldEntries(): index" << fieldId << "out of range for" << scene.fieldCount() << "fields", (Trade::SceneData{Trade::SceneMappingType::UnsignedInt, 0, nullptr, {}})); + CORRADE_ASSERT(!usedFields[fieldId], + "SceneTools::filterFieldEntries(): field" << scene.fieldName(fieldId) << "listed more than once", (Trade::SceneData{Trade::SceneMappingType::UnsignedInt, 0, nullptr, {}})); + #ifndef CORRADE_NO_ASSERT + usedFields.set(fieldId); + #endif + CORRADE_ASSERT(scene.fieldSize(fieldId) == mask.size(), + "SceneTools::filterFieldEntries(): expected" << scene.fieldSize(fieldId) << "bits for" << scene.fieldName(fieldId) << "but got" << mask.size(), + (Trade::SceneData{Trade::SceneMappingType::UnsignedInt, 0, nullptr, {}})); + + const Trade::SceneFieldType fieldType = scene.fieldType(fieldId); + CORRADE_ASSERT(!Trade::Implementation::isSceneFieldTypeString(fieldType), + "SceneTools::filterFieldEntries(): filtering string fields is not implemented yet, sorry", (Trade::SceneData{Trade::SceneMappingType::UnsignedInt, 0, nullptr, {}})); + CORRADE_ASSERT(fieldType != Trade::SceneFieldType::Bit, + "SceneTools::filterFieldEntries(): filtering bit fields is not implemented yet, sorry", (Trade::SceneData{Trade::SceneMappingType::UnsignedInt, 0, nullptr, {}})); + + /* Skip empty fields as there's nothing to do for them and they don't + even have an entry in the uniqueMappings map. But do that only after + doing all checks for them for consistent behavior. */ + if(!scene.fieldSize(fieldId)) + continue; + + const Containers::StridedArrayView2D mapping = scene.mapping(fieldId); + SharedMapping& sharedMapping = uniqueMappings.at(std::make_tuple(mapping.data(), mapping.size()[0], mapping.stride()[0])); + + /* If the mapping is shared, pass a pre-allocated array with the final + contents to combineFields() to keep the sharing */ + const std::size_t filteredFieldSize = mask.count(); + Containers::StridedArrayView1D filteredMapping; + if(sharedMapping.count > 1) { + /* This is the first mask that filters a shared mapping, allocate + the output for it and copy the filtered mapping there */ + if(!sharedMapping.filteredCount) { + sharedMapping.filteredMapping = Containers::Array{NoInit, mappingTypeSize*filteredFieldSize}; + Utility::copyMasked(scene.mapping(fieldId), mask, Containers::StridedArrayView2D{sharedMapping.filteredMapping, {filteredFieldSize, mappingTypeSize}}); + #ifndef CORRADE_NO_ASSERT + sharedMapping.maskIndex = i; + #endif + } + #ifndef CORRADE_NO_ASSERT + /* Otherwise check that all shared fields use the same filter + view */ + else { + #ifndef CORRADE_STANDARD_ASSERT + const UnsignedInt originalFieldId = entriesToKeep[sharedMapping.maskIndex].first(); + #endif + const Containers::BitArrayView originalMask = entriesToKeep[sharedMapping.maskIndex].second(); + CORRADE_ASSERT( + originalMask.data() == mask.data() && + originalMask.offset() == mask.offset() && + originalMask.size() == mask.size(), + "SceneTools::filterFieldEntries(): field" << scene.fieldName(fieldId) << "shares mapping with" << scene.fieldName(originalFieldId) << "but was passed a different mask view", + (Trade::SceneData{Trade::SceneMappingType::UnsignedInt, 0, nullptr, {}})); + } + #endif + + filteredMapping = {sharedMapping.filteredMapping, filteredFieldSize, std::ptrdiff_t(mappingTypeSize)}; + } else { + CORRADE_INTERNAL_ASSERT(sharedMapping.count == 1); + filteredMapping = {{nullptr, mappingTypeSize*filteredFieldSize}, filteredFieldSize, std::ptrdiff_t(mappingTypeSize)}; + } + + const std::size_t fieldTypeSize = Trade::sceneFieldTypeSize(fieldType); + fields[fieldId] = Trade::SceneFieldData{scene.fieldName(fieldId), + scene.mappingType(), filteredMapping, + fieldType, Containers::StridedArrayView1D{{nullptr, fieldTypeSize*filteredFieldSize}, filteredFieldSize, std::ptrdiff_t(fieldTypeSize)}, scene.fieldArraySize(fieldId)}; + + ++sharedMapping.filteredCount; + } + + #ifndef CORRADE_NO_ASSERT + for(const std::pair, SharedMapping>& i: uniqueMappings) { + CORRADE_ASSERT(!i.second.filteredCount || i.second.count == i.second.filteredCount, + "SceneTools::filterFieldEntries(): field" << scene.fieldName(entriesToKeep[i.second.maskIndex].first()) << "shares mapping with" << i.second.count << "fields but only" << i.second.filteredCount << "are filtered", + (Trade::SceneData{Trade::SceneMappingType::UnsignedInt, 0, nullptr, {}})); + } + #endif + + Trade::SceneData out = combineFields(scene.mappingType(), scene.mappingBound(), fields); + + for(const Containers::Pair& i: entriesToKeep) { + /* Skip empty fields as there's nothing to do for them and they don't + even have an entry in the uniqueMappings map */ + if(!scene.fieldSize(i.first())) + continue; + + /* Copy the mapping only if it isn't shared among more fields -- in + that case it got already copied above */ + const Containers::StridedArrayView2D mapping = scene.mapping(i.first()); + if(uniqueMappings.at(std::make_tuple(mapping.data(), mapping.size()[0], mapping.stride()[0])).count == 1) + Utility::copyMasked(mapping, i.second(), out.mutableMapping(i.first())); + + Utility::copyMasked(scene.field(i.first()), i.second(), out.mutableField(i.first())); + } + + return out; +} + +Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, const std::initializer_list> entriesToKeep) { + return filterFieldEntries(scene, Containers::arrayView(entriesToKeep)); +} + +Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, const Containers::ArrayView> entriesToKeep) { + Containers::Array> out{NoInit, entriesToKeep.size()}; + for(std::size_t i = 0; i != entriesToKeep.size(); ++i) { + const Containers::Optional fieldId = scene.findFieldId(entriesToKeep[i].first()); + CORRADE_ASSERT(fieldId, + "SceneTools::filterFieldEntries(): field" << entriesToKeep[i].first() << "not found", (Trade::SceneData{Trade::SceneMappingType::UnsignedInt, 0, nullptr, {}})); + out[i] = {*fieldId, entriesToKeep[i].second()}; + } + + return filterFieldEntries(scene, out); +} + +Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, const std::initializer_list> entriesToKeep) { + return filterFieldEntries(scene, Containers::arrayView(entriesToKeep)); +} + }} diff --git a/src/Magnum/SceneTools/Filter.h b/src/Magnum/SceneTools/Filter.h index 6ae8f50b6..83afadd97 100644 --- a/src/Magnum/SceneTools/Filter.h +++ b/src/Magnum/SceneTools/Filter.h @@ -26,7 +26,7 @@ */ /** @file - * @brief Function @ref Magnum::SceneTools::filterFields(), @ref Magnum::SceneTools::filterOnlyFields(), @ref Magnum::SceneTools::filterExceptFields() + * @brief Function @ref Magnum::SceneTools::filterFields(), @ref Magnum::SceneTools::filterOnlyFields(), @ref Magnum::SceneTools::filterExceptFields(), @ref Magnum::SceneTools::filterFieldEntries() * @m_since_latest */ @@ -93,6 +93,70 @@ MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterExceptFields(const Trade::SceneD */ MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterExceptFields(const Trade::SceneData& scene, std::initializer_list fields); +/** +@brief Filter individual entries of fields in a scene +@m_since_latest + +Returns a copy of @p scene containing the same fields but only with entries for +which the corresponding bit in @p entriesToKeep is set. Each item in +@p entriesToKeep is a pair of a field ID and a mask of entries to keep in that +field. The field ID is expected to be unique in the list and less than +@ref Trade::SceneData::fieldCount(), size of the mask then equal to +@ref Trade::SceneData::fieldSize() for that field. Fields not listed in the +@p entriesToKeep array are passed through unchanged, use @ref filterFields(), +@ref filterExceptFields() or @ref filterOnlyFields() to deal with them as a +whole if needed. + +Fields that fully share their mapping views (such as @ref Trade::SceneField::Mesh +and @relativeref{Trade::SceneField,MeshMaterial}, including fields for which +this isn't enforced) either need to be listed all in @p entriesToKeep with the +same mask view, or all omitted so they're passed through. Fields that share the +mapping only partially don't have any special handling. The data repacking is +performed using @ref combineFields(), see its documentation for more +information. + +As an example, let's assume in the following snippet the scene contains +@ref Trade::SceneField::Translation, +@relativeref{Trade::SceneField,Rotation}, @relativeref{Trade::SceneField,Mesh}, +@relativeref{Trade::SceneField,MeshMaterial} and +@relativeref{Trade::SceneField,Light}, with the intent to filter some +translations and lights away. Filtering translations means the rotations have +to be filtered as well, however neither meshes nor materials (which share the +mapping as well) need to be listed if they're not filtered: + +@snippet MagnumSceneTools.cpp filterFieldEntries-shared-mapping + +At the moment, @ref Trade::SceneFieldType::Bit and string fields can't be +filtered, only passed through. +@experimental +*/ +MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, Containers::ArrayView> entriesToKeep); + +/** +@overload +@m_since_latest +*/ +MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, std::initializer_list> entriesToKeep); + +/** +@brief Filter individual entries of named fields in a scene +@m_since_latest + +Translates field names in @p entriesToKeep to field IDs using +@ref Trade::SceneData::fieldId() and delegates to +@ref filterFieldEntries(const Trade::SceneData&, Containers::ArrayView>). +Expects that all listed fields exist in @p scene, see the referenced function +documentation for other expectations. +@experimental +*/ +MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, Containers::ArrayView> entriesToKeep); + +/** +@overload +@m_since_latest +*/ +MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterFieldEntries(const Trade::SceneData& scene, std::initializer_list> entriesToKeep); + }} #endif diff --git a/src/Magnum/SceneTools/Test/FilterTest.cpp b/src/Magnum/SceneTools/Test/FilterTest.cpp index ca53921ad..b07cff326 100644 --- a/src/Magnum/SceneTools/Test/FilterTest.cpp +++ b/src/Magnum/SceneTools/Test/FilterTest.cpp @@ -25,10 +25,14 @@ #include #include +#include #include #include +#include #include +#include "Magnum/Math/Complex.h" +#include "Magnum/Math/Vector2.h" #include "Magnum/SceneTools/Filter.h" #include "Magnum/Trade/SceneData.h" @@ -45,6 +49,26 @@ struct FilterTest: TestSuite::Tester { void exceptFields(); void exceptFieldsNoFieldData(); + + void fieldEntries(); + void fieldEntriesFieldNotFound(); + void fieldEntriesDuplicated(); + void fieldEntriesWrongBitCount(); + void fieldEntriesStringField(); + void fieldEntriesBitField(); + + void fieldEntriesSharedMapping(); + void fieldEntriesSharedMappingInvalid(); +}; + +using namespace Math::Literals; + +const struct { + const char* name; + bool byName; +} FieldEntriesData[]{ + {"by ID", false}, + {"by name", true} }; FilterTest::FilterTest() { @@ -56,6 +80,18 @@ FilterTest::FilterTest() { &FilterTest::exceptFields, &FilterTest::exceptFieldsNoFieldData}); + + addInstancedTests({&FilterTest::fieldEntries}, + Containers::arraySize(FieldEntriesData)); + + addTests({&FilterTest::fieldEntriesFieldNotFound, + &FilterTest::fieldEntriesDuplicated, + &FilterTest::fieldEntriesWrongBitCount, + &FilterTest::fieldEntriesStringField, + &FilterTest::fieldEntriesBitField, + + &FilterTest::fieldEntriesSharedMapping, + &FilterTest::fieldEntriesSharedMappingInvalid}); } void FilterTest::fields() { @@ -259,6 +295,471 @@ void FilterTest::exceptFieldsNoFieldData() { CORRADE_COMPARE(filtered.dataFlags(), Trade::DataFlags{}); } +void FilterTest::fieldEntries() { + auto&& data = FieldEntriesData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + const struct Data { + UnsignedShort meshMapping[5]{7, 8, 6666, 3, 6666}; + UnsignedByte mesh[5]{2, 3, 222, 1, 222}; + UnsignedShort lightMapping[4]{3, 1, 2, 2}; + UnsignedInt light[4]{12, 23, 32, 31}; + UnsignedShort arrayMapping[3]{6666, 3, 2}; + Float array[3][2]{{77.0f, 88.0f}, {1.0f, 2.0f}, {3.0f, 4.0f}}; + UnsignedShort visibilityMapping[2]{12, 33}; + bool visible[2]{true, false}; + UnsignedShort parentMapping[3]{}; + Short parents[3]{}; + } sceneData[1]{}; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 76, {}, sceneData, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(sceneData->meshMapping), + Containers::arrayView(sceneData->mesh)}, + /* Offset-only, to verify it get converted to absolute when it reaches + combine() at the end */ + Trade::SceneFieldData{Trade::SceneField::Light, 4, + Trade::SceneMappingType::UnsignedShort, offsetof(Data, lightMapping), sizeof(UnsignedShort), + Trade::SceneFieldType::UnsignedInt, offsetof(Data, light), sizeof(UnsignedInt)}, + /* Array */ + Trade::SceneFieldData{Trade::sceneFieldCustom(333), + Containers::arrayView(sceneData->arrayMapping), + Containers::arrayCast<2, const Float>(Containers::stridedArrayView(sceneData->array))}, + /* Bit field. Should cause no assert as it's just passed through. */ + Trade::SceneFieldData{Trade::sceneFieldCustom(15), + Containers::arrayView(sceneData->visibilityMapping), + Containers::stridedArrayView(sceneData->visible).sliceBit(0)}, + /* This one gets all entries removed */ + Trade::SceneFieldData{Trade::SceneField::Parent, + Containers::arrayView(sceneData->parentMapping), + Containers::arrayView(sceneData->parents)}, + /* This one is already empty */ + Trade::SceneFieldData{Trade::SceneField::Camera, + Containers::ArrayView{}, + Containers::ArrayView{}}, + }}; + + Containers::BitArray meshesToKeep{DirectInit, Containers::arraySize(sceneData->mesh), true}; + meshesToKeep.reset(2); + meshesToKeep.reset(4); + + Containers::BitArray arraysToKeep{DirectInit, Containers::arraySize(sceneData->array), true}; + arraysToKeep.reset(0); + + Containers::BitArray parentsToKeep{DirectInit, Containers::arraySize(sceneData->parents), false}; + + Containers::BitArray camerasToKeep; + + Trade::SceneData filtered = data.byName ? + filterFieldEntries(scene, { + {Trade::sceneFieldCustom(333), arraysToKeep}, + {Trade::SceneField::Parent, parentsToKeep}, + {Trade::SceneField::Mesh, meshesToKeep}, + {Trade::SceneField::Camera, camerasToKeep} + }) : + filterFieldEntries(scene, { + {2, arraysToKeep}, + {4, parentsToKeep}, + {0, meshesToKeep}, + {5, camerasToKeep} + }); + + CORRADE_COMPARE(filtered.fieldCount(), 6); + CORRADE_COMPARE(filtered.mappingType(), Trade::SceneMappingType::UnsignedShort); + CORRADE_COMPARE(filtered.mappingBound(), 76); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Mesh)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Mesh), + Containers::arrayView({7, 8, 3}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Mesh), + Containers::arrayView({2, 3, 1}), + TestSuite::Compare::Container); + + /* Lights weren't listed and thus stayed untouched */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Light)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Light), + Containers::arrayView(sceneData->lightMapping), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Light), + Containers::arrayView(sceneData->light), + TestSuite::Compare::Container); + + CORRADE_VERIFY(filtered.hasField(Trade::sceneFieldCustom(333))); + CORRADE_COMPARE_AS(filtered.mapping(Trade::sceneFieldCustom(333)), + Containers::arrayView({3, 2}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS((Containers::arrayCast<1, const Vector2>(filtered.field(Trade::sceneFieldCustom(333)))), + Containers::arrayView({{1.0f, 2.0f}, {3.0f, 4.0f}}), + TestSuite::Compare::Container); + + /* Bits weren't listed and thus stayed untouched */ + CORRADE_VERIFY(filtered.hasField(Trade::sceneFieldCustom(15))); + CORRADE_COMPARE_AS(filtered.mapping(Trade::sceneFieldCustom(15)), + Containers::arrayView(sceneData->visibilityMapping), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.fieldBits(Trade::sceneFieldCustom(15)), + Containers::stridedArrayView(sceneData->visible).sliceBit(0), + TestSuite::Compare::Container); + + /* Parents are all removed */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Parent)); + CORRADE_COMPARE(filtered.fieldSize(Trade::SceneField::Parent), 0); + + /* Cameras were empty before already */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Camera)); + CORRADE_COMPARE(filtered.fieldSize(Trade::SceneField::Camera), 0); + + /* The attribute data should not be a growable array to make this usable in + plugins */ + Containers::Array fieldData = filtered.releaseFieldData(); + CORRADE_VERIFY(!fieldData.deleter()); +} + +void FilterTest::fieldEntriesFieldNotFound() { + CORRADE_SKIP_IF_NO_ASSERT(); + + const struct { + UnsignedShort meshMapping[5]; + UnsignedByte mesh[5]; + UnsignedShort lightMapping[4]; + UnsignedInt light[4]; + } data[1]{}; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 76, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::SceneField::Light, + Containers::arrayView(data->lightMapping), + Containers::arrayView(data->light)}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + filterFieldEntries(scene, { + {Trade::SceneField::Light, Containers::BitArray{ValueInit, 4}}, + {Trade::SceneField::Parent, {}} + }); + filterFieldEntries(scene, { + {1, Containers::BitArray{ValueInit, 4}}, + {2, {}} + }); + CORRADE_COMPARE(out.str(), + "SceneTools::filterFieldEntries(): field Trade::SceneField::Parent not found\n" + "SceneTools::filterFieldEntries(): index 2 out of range for 2 fields\n"); +} + +void FilterTest::fieldEntriesDuplicated() { + CORRADE_SKIP_IF_NO_ASSERT(); + + const struct { + UnsignedShort meshMapping[5]; + UnsignedByte mesh[5]; + UnsignedShort lightMapping[4]; + UnsignedInt light[4]; + } data[1]{}; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 76, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::SceneField::Light, + Containers::arrayView(data->lightMapping), + Containers::arrayView(data->light)}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + /* The name-based variant just delegates to this one, no need to test it + as well */ + filterFieldEntries(scene, { + {1, Containers::BitArray{ValueInit, 4}}, + {0, Containers::BitArray{ValueInit, 5}}, + {1, Containers::BitArray{ValueInit, 4}}, + }); + CORRADE_COMPARE(out.str(), "SceneTools::filterFieldEntries(): field Trade::SceneField::Light listed more than once\n"); +} + +void FilterTest::fieldEntriesWrongBitCount() { + CORRADE_SKIP_IF_NO_ASSERT(); + + const struct { + UnsignedShort meshMapping[5]; + UnsignedByte mesh[5]; + UnsignedShort lightMapping[4]; + UnsignedInt light[4]; + } data[1]{}; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 76, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::SceneField::Light, + Containers::arrayView(data->lightMapping), + Containers::arrayView(data->light)}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + /* The name-based variant just delegates to this one, no need to test it + as well */ + filterFieldEntries(scene, { + {1, Containers::BitArray{ValueInit, 4}}, + {0, Containers::BitArray{ValueInit, 6}} + }); + CORRADE_COMPARE(out.str(), "SceneTools::filterFieldEntries(): expected 5 bits for Trade::SceneField::Mesh but got 6\n"); +} + +void FilterTest::fieldEntriesBitField() { + CORRADE_SKIP_IF_NO_ASSERT(); + + const struct { + UnsignedShort meshMapping[5]; + UnsignedByte mesh[5]; + UnsignedShort visibilityMapping[2]; + bool visible[2]; + } data[1]{}; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 76, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::sceneFieldCustom(15), + Containers::arrayView(data->visibilityMapping), + Containers::stridedArrayView(data->visible).sliceBit(0)}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + /* The name-based variant just delegates to this one, no need to test it + as well */ + filterFieldEntries(scene, { + {0, Containers::BitArray{ValueInit, 5}}, + {1, Containers::BitArray{ValueInit, 2}} + }); + CORRADE_COMPARE(out.str(), "SceneTools::filterFieldEntries(): filtering bit fields is not implemented yet, sorry\n"); +} + +void FilterTest::fieldEntriesStringField() { + CORRADE_SKIP_IF_NO_ASSERT(); + + const struct { + UnsignedShort meshMapping[5]; + UnsignedByte mesh[5]; + UnsignedShort nameMapping[2]; + UnsignedInt nameRangeNullTerminated[2]; + char nameString[1]; + } data[1]{}; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 76, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::sceneFieldCustom(15), + Containers::arrayView(data->nameMapping), + data->nameString, Trade::SceneFieldType::StringRangeNullTerminated32, + Containers::arrayView(data->nameRangeNullTerminated)}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + /* The name-based variant just delegates to this one, no need to test it + as well */ + filterFieldEntries(scene, { + {0, Containers::BitArray{ValueInit, 5}}, + {1, Containers::BitArray{ValueInit, 2}} + }); + CORRADE_COMPARE(out.str(), "SceneTools::filterFieldEntries(): filtering string fields is not implemented yet, sorry\n"); +} + +void FilterTest::fieldEntriesSharedMapping() { + const struct { + UnsignedShort meshMaterialMapping[5]{7, 8, 6666, 6666, 3}; + UnsignedByte mesh[5]{2, 3, 222, 222, 1}; + Byte meshMaterial[5]{-1, 7, 111, 111, 0}; + UnsignedShort trsMapping[5]{1, 6666, 7, 6666, 3}; + Vector2 translation[5]{ + {1.0f, 2.0f}, + {}, + {3.0f, 4.0f}, + {}, + {5.0f, 6.0f} + }; + Complex rotation[5]{ + Complex::rotation(15.0_degf), + {}, + Complex::rotation(30.0_degf), + {}, + Complex::rotation(45.0_degf) + }; + Float uniformScale[5]{10.0f, 0.0f, -5.0f, 0.0f, 555.0f}; + UnsignedInt light[2]{34, 25}; + Int parent[3]{-1, 0, 3}; + } data[1]{}; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 176, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMaterialMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::SceneField::MeshMaterial, + Containers::arrayView(data->meshMaterialMapping), + Containers::arrayView(data->meshMaterial)}, + Trade::SceneFieldData{Trade::SceneField::Translation, + Containers::arrayView(data->trsMapping), + Containers::arrayView(data->translation)}, + Trade::SceneFieldData{Trade::SceneField::Rotation, + Containers::arrayView(data->trsMapping), + Containers::arrayView(data->rotation)}, + /* Shares trsMapping, sharing should be preserved even though not + enforced */ + Trade::SceneFieldData{Trade::sceneFieldCustom(15), + Containers::arrayView(data->trsMapping), + Containers::arrayView(data->uniformScale)}, + /* Shares a prefix of meshMaterialMapping, should not be preserved */ + Trade::SceneFieldData{Trade::SceneField::Light, + Containers::arrayView(data->meshMaterialMapping).prefix(2), + Containers::arrayView(data->light)}, + /* Shares every 2nd item of trsMapping, should not be preserved */ + Trade::SceneFieldData{Trade::SceneField::Parent, + Containers::stridedArrayView(data->trsMapping).every(2), + Containers::arrayView(data->parent)}, + }}; + + Containers::BitArray meshesToKeep{DirectInit, Containers::arraySize(data->mesh), true}; + meshesToKeep.reset(2); + meshesToKeep.reset(3); + + Containers::BitArray transformationsToKeep{DirectInit, Containers::arraySize(data->trsMapping), true}; + transformationsToKeep.reset(1); + transformationsToKeep.reset(3); + + Trade::SceneData filtered = filterFieldEntries(scene, { + /* All shared fields have to be listed with the same view */ + {Trade::SceneField::Mesh, meshesToKeep}, + {Trade::SceneField::MeshMaterial, meshesToKeep}, + + {Trade::SceneField::Translation, transformationsToKeep}, + {Trade::SceneField::Rotation, transformationsToKeep}, + {Trade::sceneFieldCustom(15), transformationsToKeep}, + }); + + CORRADE_COMPARE(filtered.fieldCount(), 7); + CORRADE_COMPARE(filtered.mappingType(), Trade::SceneMappingType::UnsignedShort); + CORRADE_COMPARE(filtered.mappingBound(), 176); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Mesh)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Mesh), + Containers::arrayView({7, 8, 3}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Mesh), + Containers::arrayView({2, 3, 1}), + TestSuite::Compare::Container); + + /* Mapping shared with Mesh */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::MeshMaterial)); + CORRADE_COMPARE(filtered.mapping(Trade::SceneField::MeshMaterial).data(), + filtered.mapping(Trade::SceneField::Mesh).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::MeshMaterial), + Containers::arrayView({-1, 7, 0}), + TestSuite::Compare::Container); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Translation)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Translation), + Containers::arrayView({1, 7, 3}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Translation), + Containers::arrayView({{1.0f, 2.0f}, {3.0f, 4.0f}, {5.0f, 6.0f}}), + TestSuite::Compare::Container); + + /* Mapping shared with Translation */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Rotation)); + CORRADE_COMPARE(filtered.mapping(Trade::SceneField::Rotation).data(), + filtered.mapping(Trade::SceneField::Translation).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Rotation), + Containers::arrayView({Complex::rotation(15.0_degf), Complex::rotation(30.0_degf), Complex::rotation(45.0_degf)}), + TestSuite::Compare::Container); + + /* Mapping shared with Translation again */ + CORRADE_VERIFY(filtered.hasField(Trade::sceneFieldCustom(15))); + CORRADE_COMPARE(filtered.mapping(Trade::sceneFieldCustom(15)).data(), + filtered.mapping(Trade::SceneField::Translation).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::sceneFieldCustom(15)), + Containers::arrayView({10.0f, -5.0f, 555.0f}), + TestSuite::Compare::Container); + + /* These fields are kept unfiltered and they don't share any mapping even + though they could */ + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Light)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Light), + Containers::arrayView({7, 8}), + TestSuite::Compare::Container); + CORRADE_VERIFY(filtered.mapping(Trade::SceneField::Light).data() != filtered.mapping(Trade::SceneField::Mesh).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Light), + Containers::arrayView(data->light), + TestSuite::Compare::Container); + + CORRADE_VERIFY(filtered.hasField(Trade::SceneField::Parent)); + CORRADE_COMPARE_AS(filtered.mapping(Trade::SceneField::Parent), + Containers::arrayView({1, 7, 3}), + TestSuite::Compare::Container); + CORRADE_VERIFY(filtered.mapping(Trade::SceneField::Parent).data() != filtered.mapping(Trade::SceneField::Translation).data()); + CORRADE_COMPARE_AS(filtered.field(Trade::SceneField::Parent), + Containers::arrayView(data->parent), + TestSuite::Compare::Container); +} + +void FilterTest::fieldEntriesSharedMappingInvalid() { + CORRADE_SKIP_IF_NO_ASSERT(); + + const struct { + UnsignedShort lightMapping[4]; + UnsignedInt light[4]; + UnsignedShort meshMaterialMapping[5]; + UnsignedByte mesh[5]; + Byte meshMaterial[5]; + UnsignedLong meshIndexOffset[5]; + } data[1]{}; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 176, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Light, + Containers::arrayView(data->lightMapping), + Containers::arrayView(data->light)}, + Trade::SceneFieldData{Trade::SceneField::Mesh, + Containers::arrayView(data->meshMaterialMapping), + Containers::arrayView(data->mesh)}, + Trade::SceneFieldData{Trade::sceneFieldCustom(1), + Containers::arrayView(data->meshMaterialMapping), + Containers::arrayView(data->meshIndexOffset)}, + Trade::SceneFieldData{Trade::SceneField::MeshMaterial, + Containers::arrayView(data->meshMaterialMapping), + Containers::arrayView(data->meshMaterial)}, + }}; + + Containers::BitArray meshesToKeep{ValueInit, 5}; + Containers::BitArray meshesToKeepDifferent{DirectInit, 5, true}; + + std::ostringstream out; + Error redirectError{&out}; + filterFieldEntries(scene, { + {Trade::SceneField::MeshMaterial, meshesToKeep}, + {Trade::SceneField::Mesh, meshesToKeep}, + {Trade::sceneFieldCustom(1), meshesToKeepDifferent}, + }); + filterFieldEntries(scene, { + {Trade::SceneField::Mesh, meshesToKeep}, + {Trade::SceneField::MeshMaterial, meshesToKeep}, + }); + filterFieldEntries(scene, { + {Trade::sceneFieldCustom(1), meshesToKeep}, + {Trade::SceneField::MeshMaterial, meshesToKeep}, + }); + CORRADE_COMPARE(out.str(), + "SceneTools::filterFieldEntries(): field Trade::SceneField::Custom(1) shares mapping with Trade::SceneField::MeshMaterial but was passed a different mask view\n" + "SceneTools::filterFieldEntries(): field Trade::SceneField::Mesh shares mapping with 3 fields but only 2 are filtered\n" + "SceneTools::filterFieldEntries(): field Trade::SceneField::Custom(1) shares mapping with 3 fields but only 2 are filtered\n"); +} + }}}} CORRADE_TEST_MAIN(Magnum::SceneTools::Test::FilterTest)