diff --git a/src/Magnum/Trade/AbstractImporter.cpp b/src/Magnum/Trade/AbstractImporter.cpp index a35976ae1..036ca9ed5 100644 --- a/src/Magnum/Trade/AbstractImporter.cpp +++ b/src/Magnum/Trade/AbstractImporter.cpp @@ -531,7 +531,7 @@ void AbstractImporter::populateCachedScenes() { compatibility code path anyway, so just skip the processing altogether in that case. */ if(_cachedScenes->scenes[i]->hasField(SceneField::Parent)) - _cachedScenes->scenes[i] = Implementation::sceneConvertToSingleFunctionObjects(*_cachedScenes->scenes[i], Containers::arrayView({SceneField::Mesh, SceneField::Camera, SceneField::Light}), newObjectOffset); + _cachedScenes->scenes[i] = Implementation::sceneConvertToSingleFunctionObjects(*_cachedScenes->scenes[i], Containers::arrayView({SceneField::Mesh, SceneField::Camera, SceneField::Light}), Containers::arrayView({SceneField::Skin}), newObjectOffset); /* Return the 2D/3D object count based on which scenes are 2D and which not. The objectCount() provided by the importer is ignored diff --git a/src/Magnum/Trade/Implementation/sceneTools.h b/src/Magnum/Trade/Implementation/sceneTools.h index 9c1e05605..a7a67e67b 100644 --- a/src/Magnum/Trade/Implementation/sceneTools.h +++ b/src/Magnum/Trade/Implementation/sceneTools.h @@ -191,20 +191,28 @@ inline SceneData sceneCombine(const SceneMappingType mappingType, const Unsigned return SceneData{mappingType, mappingBound, std::move(outData), std::move(outFields)}; } +inline Containers::Optional findField(Containers::ArrayView fields, SceneField field) { + for(std::size_t i = 0; i != fields.size(); ++i) + if(fields[i] == field) return i; + return {}; +} + /* Creates a SceneData copy where each object has at most one of the fields - listed in the passed array. This is done by enlarging the parents array - and moving extraneous features to new objects that are marked as a child of - the original. No transformations or other fields are added for the new - objects. Fields that are connected together (such as meshes and materials) - are assumed to share the same object mapping with only one of them passed in - the fieldsToConvert array, which will result for all fields from the same - set being reassociated to the new object. + listed in the passed `fieldsToConvert` array. This is done by enlarging the + parents array and moving extraneous features to new objects that are marked + as a child of the original. Fields that are connected together (such as + meshes and materials) are assumed to share the same object mapping with only + one of them passed in the fieldsToConvert array, which will result for all + fields from the same set being reassociated to the new object. + + Fields listed in `fieldsToCopy` are copied from the original object. This + is useful for e.g. skins, to preserve them for the separated meshes. Requies a SceneField::Parent to be present -- otherwise it wouldn't be possible to know where to attach the new objects. */ /** @todo when published, (again) add an initializer_list overload and turn all internal asserts into (tested!) message asserts */ -inline SceneData sceneConvertToSingleFunctionObjects(const SceneData& scene, Containers::ArrayView fieldsToConvert, const UnsignedInt newObjectOffset) { +inline SceneData sceneConvertToSingleFunctionObjects(const SceneData& scene, Containers::ArrayView fieldsToConvert, Containers::ArrayView fieldsToCopy, const UnsignedInt newObjectOffset) { /** @todo assert for really high object counts (where this cast would fail) */ Containers::Array objectAttachmentCount{ValueInit, std::size_t(scene.mappingBound())}; for(const SceneField field: fieldsToConvert) { @@ -224,6 +232,28 @@ inline SceneData sceneConvertToSingleFunctionObjects(const SceneData& scene, Con } } + /* entriesToAddToFieldsToCopy[i] specifies how many fields to add for the + fieldsToCopy[i] field */ + Containers::Array fieldsToCopyAdditionCount{ValueInit, fieldsToCopy.size()}; + for(std::size_t i = 0; i != fieldsToCopy.size(); ++i) { + const SceneField field = fieldsToCopy[i]; + CORRADE_INTERNAL_ASSERT(field != SceneField::Parent); + CORRADE_INTERNAL_ASSERT(!findField(fieldsToConvert, field)); + + /* Skip fields that are not present */ + const Containers::Optional fieldId = scene.findFieldId(field); + if(!fieldId) continue; + + /** @todo use a statically-allocated array & Into() in a loop instead + once this is more than a private backwards-compatibility utility + where PERF WHATEVER WHO CARES */ + for(const UnsignedInt object: scene.mappingAsArray(*fieldId)) { + CORRADE_INTERNAL_ASSERT(object < objectAttachmentCount.size()); + if(objectAttachmentCount[object]) + fieldsToCopyAdditionCount[i] += objectAttachmentCount[object] - 1; + } + } + UnsignedInt objectsToAdd = 0; for(const UnsignedInt count: objectAttachmentCount) if(count > 1) objectsToAdd += count - 1; @@ -239,9 +269,26 @@ inline SceneData sceneConvertToSingleFunctionObjects(const SceneData& scene, Con for(std::size_t i = 0; i != scene.fieldCount(); ++i) { const SceneFieldData& field = scene.fieldData(i); + /* If this field is among the fields we want to copy, enlarge it for + the new entries */ + if(Containers::Optional fieldToCopy = findField(fieldsToCopy, field.name())) { + /** @todo wow this placeholder construction is HIDEOUS */ + fields[i] = SceneFieldData{field.name(), + field.mappingType(), + Containers::ArrayView{nullptr, std::size_t(field.size() + fieldsToCopyAdditionCount[*fieldToCopy])}, + field.fieldType(), + Containers::StridedArrayView1D{ + {nullptr, ~std::size_t{}}, + std::size_t(field.size() + fieldsToCopyAdditionCount[*fieldToCopy]), + std::ptrdiff_t((field.fieldArraySize() ? field.fieldArraySize() : 1)*sceneFieldTypeSize(field.fieldType())) + }, + field.fieldArraySize(), + field.flags() & ~SceneFieldFlag::ImplicitMapping + }; + /* If this is a parent, enlarge it for the newly added objects, and if it was implicit make it ordered */ - if(field.name() == SceneField::Parent) { + } else if(field.name() == SceneField::Parent) { /** @todo some nicer constructor for placeholders once this is in public interest */ fields[i] = SceneFieldData{SceneField::Parent, Containers::ArrayView{nullptr, std::size_t(field.size() + objectsToAdd)}, Containers::ArrayView{nullptr, std::size_t(field.size() + objectsToAdd)}, @@ -269,6 +316,19 @@ inline SceneData sceneConvertToSingleFunctionObjects(const SceneData& scene, Con const Containers::StridedArrayView1D outParents = out.mutableField(parentFieldId); CORRADE_INTERNAL_ASSERT_OUTPUT(scene.parentsInto(0, outParentMapping, outParents) == scene.fieldSize(parentFieldId)); + /* Copy existing field-to-copy data to a prefix of the output */ + for(std::size_t i = 0; i != fieldsToCopy.size(); ++i) { + const SceneField field = fieldsToCopy[i]; + + const Containers::Optional fieldId = scene.findFieldId(field); + if(!fieldId) continue; + + const Containers::StridedArrayView1D outMapping = out.mutableMapping(*fieldId); + const Containers::StridedArrayView2D outField = out.mutableField(*fieldId); + CORRADE_INTERNAL_ASSERT_OUTPUT(scene.mappingInto(*fieldId, 0, outMapping) == scene.fieldSize(*fieldId)); + Utility::copy(scene.field(*fieldId), outField.prefix(scene.fieldSize(*fieldId))); + } + /* List new objects at the end of the extended parent field */ const Containers::StridedArrayView1D newParentMapping = outParentMapping.suffix(scene.fieldSize(parentFieldId)); const Containers::StridedArrayView1D newParents = outParents.suffix(scene.fieldSize(parentFieldId)); @@ -282,6 +342,9 @@ inline SceneData sceneConvertToSingleFunctionObjects(const SceneData& scene, Con constexpr UnsignedInt zero[1]{}; Utility::copy(Containers::stridedArrayView(zero).broadcasted<0>(scene.mappingBound()), objectAttachmentCount); + /* Clear the fieldsToCopyAdditionCount array to reuse it below */ + Utility::copy(Containers::stridedArrayView(zero).broadcasted<0>(fieldsToCopy.size()), fieldsToCopyAdditionCount); + /* For objects with multiple fields move the extra fields to newly added children */ { @@ -298,6 +361,35 @@ inline SceneData sceneConvertToSingleFunctionObjects(const SceneData& scene, Con attached, then attach the field to a new object and make that new object a child of the previous one. */ if(fieldObject < objectAttachmentCount.size() && objectAttachmentCount[fieldObject]) { + /* Go through all fields to copy and copy each entry that + was assigned to the original object */ + for(std::size_t i = 0; i != fieldsToCopy.size(); ++i) { + const Containers::Optional fieldToCopyId = scene.findFieldId(fieldsToCopy[i]); + if(!fieldToCopyId) continue; + + /* View to copy the data from */ + const Containers::StridedArrayView2D fieldToCopyDataSrc = scene.field(*fieldToCopyId); + + /* Views to put the mapping to and copy the data to */ + const std::size_t newFieldToCopyOffset = scene.fieldSize(*fieldToCopyId); + const Containers::StridedArrayView1D newFieldToCopyMapping = out.mutableMapping(*fieldToCopyId).suffix(newFieldToCopyOffset); + const Containers::StridedArrayView2D newFieldToCopy = out.mutableField(*fieldToCopyId).suffix(newFieldToCopyOffset); + + /* As long as there are entries attached to the + original objects, copy them */ + std::size_t offset = 0; + while(Containers::Optional found = scene.findFieldObjectOffset(*fieldToCopyId, fieldObject, offset)) { + /* Assgn a new field entry to the new object */ + newFieldToCopyMapping[fieldsToCopyAdditionCount[i]] = newParentMapping[newParentIndex]; + + /* Copy the data from the old entry to it */ + Utility::copy(fieldToCopyDataSrc[*found], newFieldToCopy[fieldsToCopyAdditionCount[i]]); + + ++fieldsToCopyAdditionCount[i]; + offset = *found + 1; + } + } + /* Use the old object as a parent of the new object */ newParents[newParentIndex] = fieldObject; /* Assign the field to the new object */ diff --git a/src/Magnum/Trade/Test/AbstractImporterTest.cpp b/src/Magnum/Trade/Test/AbstractImporterTest.cpp index 836f46417..ad0df45df 100644 --- a/src/Magnum/Trade/Test/AbstractImporterTest.cpp +++ b/src/Magnum/Trade/Test/AbstractImporterTest.cpp @@ -2710,13 +2710,19 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { UnsignedInt object; UnsignedInt camera; }; + struct Skin { + UnsignedInt object; + UnsignedInt skin; + }; Containers::StridedArrayView1D parents; Containers::StridedArrayView1D meshes; Containers::StridedArrayView1D cameras; + Containers::StridedArrayView1D skins; Containers::Array dataData = Containers::ArrayTuple{ {NoInit, 5, parents}, {NoInit, 7, meshes}, {NoInit, 2, cameras}, + {NoInit, 2, skins}, }; Utility::copy({{15, -1}, {21, -1}, {22, 21}, {23, 22}, {1, -1}}, parents); Utility::copy({ @@ -2729,6 +2735,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { {21, 5, -1} }, meshes); Utility::copy({{22, 1}, {1, 5}}, cameras); + Utility::copy({{15, 9}, {21, 10}}, skins); /* Second scene that also has a duplicate, to verify the newly added object IDs don't conflict with each other. A potential downside is that @@ -2752,6 +2759,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { SceneFieldData{SceneField::Mesh, meshes.slice(&Mesh::object), meshes.slice(&Mesh::mesh)}, SceneFieldData{SceneField::MeshMaterial, meshes.slice(&Mesh::object), meshes.slice(&Mesh::meshMaterial)}, SceneFieldData{SceneField::Camera, cameras.slice(&Camera::object), cameras.slice(&Camera::camera)}, + SceneFieldData{SceneField::Skin, skins.slice(&Skin::object), skins.slice(&Skin::skin)}, /* Just to disambiguate this as a 2D scene */ SceneFieldData{SceneField::Transformation, SceneMappingType::UnsignedInt, nullptr, SceneFieldType::Matrix3x3, nullptr}, }}; @@ -2844,6 +2852,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { TestSuite::Compare::Container); MeshObjectData2D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 2); + CORRADE_COMPARE(mo.skin(), -1); } { Containers::Pointer o = importer.object2D(15); CORRADE_VERIFY(o); @@ -2854,6 +2863,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { TestSuite::Compare::Container); MeshObjectData2D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 4); + CORRADE_COMPARE(mo.skin(), 9); } { Containers::Pointer o = importer.object2D(21); CORRADE_VERIFY(o); @@ -2864,6 +2874,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { TestSuite::Compare::Container); MeshObjectData2D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), -1); + CORRADE_COMPARE(mo.skin(), 10); } { Containers::Pointer o = importer.object2D(22); CORRADE_VERIFY(o); @@ -2882,6 +2893,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { TestSuite::Compare::Container); MeshObjectData2D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 0); + CORRADE_COMPARE(mo.skin(), -1); } { Containers::Pointer o = importer.object2D(63); CORRADE_VERIFY(o); @@ -2892,6 +2904,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { TestSuite::Compare::Container); MeshObjectData2D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 3); + CORRADE_COMPARE(mo.skin(), -1); } { Containers::Pointer o = importer.object2D(64); CORRADE_VERIFY(o); @@ -2902,6 +2915,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { TestSuite::Compare::Container); MeshObjectData2D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 2); + CORRADE_COMPARE(mo.skin(), -1); } { Containers::Pointer o = importer.object2D(65); CORRADE_VERIFY(o); @@ -2912,6 +2926,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects2D() { TestSuite::Compare::Container); MeshObjectData2D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 1); + CORRADE_COMPARE(mo.skin(), 9); } { Containers::Pointer o = importer.object2D(66); CORRADE_VERIFY(o); @@ -2977,13 +2992,19 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { UnsignedInt object; UnsignedInt camera; }; + struct Skin { + UnsignedInt object; + UnsignedInt skin; + }; Containers::StridedArrayView1D parents; Containers::StridedArrayView1D meshes; Containers::StridedArrayView1D cameras; + Containers::StridedArrayView1D skins; Containers::Array dataData = Containers::ArrayTuple{ {NoInit, 5, parents}, {NoInit, 7, meshes}, {NoInit, 2, cameras}, + {NoInit, 2, skins}, }; Utility::copy({{15, -1}, {21, -1}, {22, 21}, {23, 22}, {1, -1}}, parents); Utility::copy({ @@ -2996,6 +3017,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { {21, 5, -1} }, meshes); Utility::copy({{22, 1}, {1, 5}}, cameras); + Utility::copy({{15, 9}, {21, 10}}, skins); /* Second scene that also has a duplicate, to verify the newly added object IDs don't conflict with each other. A potential downside is that @@ -3019,6 +3041,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { SceneFieldData{SceneField::Mesh, meshes.slice(&Mesh::object), meshes.slice(&Mesh::mesh)}, SceneFieldData{SceneField::MeshMaterial, meshes.slice(&Mesh::object), meshes.slice(&Mesh::meshMaterial)}, SceneFieldData{SceneField::Camera, cameras.slice(&Camera::object), cameras.slice(&Camera::camera)}, + SceneFieldData{SceneField::Skin, skins.slice(&Skin::object), skins.slice(&Skin::skin)}, /* Just to disambiguate this as a 3D scene */ SceneFieldData{SceneField::Transformation, SceneMappingType::UnsignedInt, nullptr, SceneFieldType::Matrix4x4, nullptr}, }}; @@ -3111,6 +3134,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { TestSuite::Compare::Container); MeshObjectData3D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 2); + CORRADE_COMPARE(mo.skin(), -1); } { Containers::Pointer o = importer.object3D(15); CORRADE_VERIFY(o); @@ -3121,6 +3145,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { TestSuite::Compare::Container); MeshObjectData3D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 4); + CORRADE_COMPARE(mo.skin(), 9); } { Containers::Pointer o = importer.object3D(21); CORRADE_VERIFY(o); @@ -3131,6 +3156,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { TestSuite::Compare::Container); MeshObjectData3D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), -1); + CORRADE_COMPARE(mo.skin(), 10); } { Containers::Pointer o = importer.object3D(22); CORRADE_VERIFY(o); @@ -3149,6 +3175,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { TestSuite::Compare::Container); MeshObjectData3D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 0); + CORRADE_COMPARE(mo.skin(), -1); } { Containers::Pointer o = importer.object3D(63); CORRADE_VERIFY(o); @@ -3159,6 +3186,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { TestSuite::Compare::Container); MeshObjectData3D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 3); + CORRADE_COMPARE(mo.skin(), -1); } { Containers::Pointer o = importer.object3D(64); CORRADE_VERIFY(o); @@ -3169,6 +3197,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { TestSuite::Compare::Container); MeshObjectData3D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 2); + CORRADE_COMPARE(mo.skin(), -1); } { Containers::Pointer o = importer.object3D(65); CORRADE_VERIFY(o); @@ -3179,6 +3208,7 @@ void AbstractImporterTest::sceneDeprecatedFallbackMultiFunctionObjects3D() { TestSuite::Compare::Container); MeshObjectData3D& mo = static_cast(*o); CORRADE_COMPARE(mo.material(), 1); + CORRADE_COMPARE(mo.skin(), 9); } { Containers::Pointer o = importer.object3D(66); CORRADE_VERIFY(o); diff --git a/src/Magnum/Trade/Test/SceneToolsTest.cpp b/src/Magnum/Trade/Test/SceneToolsTest.cpp index aadad0f51..ccc3d3c76 100644 --- a/src/Magnum/Trade/Test/SceneToolsTest.cpp +++ b/src/Magnum/Trade/Test/SceneToolsTest.cpp @@ -43,6 +43,7 @@ struct SceneToolsTest: TestSuite::Tester { void combineObjectSharedFieldPlaceholder(); void convertToSingleFunctionObjects(); + void convertToSingleFunctionObjectsFieldsToCopy(); }; struct { @@ -83,6 +84,8 @@ SceneToolsTest::SceneToolsTest() { addInstancedTests({&SceneToolsTest::convertToSingleFunctionObjects}, Containers::arraySize(ConvertToSingleFunctionObjectsData)); + + addTests({&SceneToolsTest::convertToSingleFunctionObjectsFieldsToCopy}); } using namespace Math::Literals; @@ -419,7 +422,7 @@ void SceneToolsTest::convertToSingleFunctionObjects() { sceneFieldCustom(15), /* Include also a field that's not present -- it should get skipped */ SceneField::ImporterState - }), 63); + }), {}, 63); /* There should be three more objects, or the original count preserved if it's large enough */ @@ -571,7 +574,91 @@ void SceneToolsTest::convertToSingleFunctionObjects() { Containers::arrayView(foo3FieldData), TestSuite::Compare::Container); CORRADE_COMPARE(scene.fieldFlags(sceneFieldCustom(17)), SceneFieldFlags{}); +} + +void SceneToolsTest::convertToSingleFunctionObjectsFieldsToCopy() { + const UnsignedShort parentMappingData[]{2, 15, 21, 22}; + const Byte parentFieldData[]{-1, -1, -1, 21}; + + const UnsignedShort meshMappingData[]{15, 21, 21, 21, 22, 15}; + const UnsignedInt meshFieldData[]{6, 1, 2, 4, 7, 3}; + + const UnsignedShort skinMappingData[]{22, 21}; + const UnsignedInt skinFieldData[]{5, 13}; + + const UnsignedLong fooMappingData[]{15, 23, 15, 21}; + const Int fooFieldData[]{0, 1, 2, 3, 4, 5, 6, 7}; + + SceneData original = Implementation::sceneCombine(SceneMappingType::UnsignedShort, 50, Containers::arrayView({ + SceneFieldData{SceneField::Parent, Containers::arrayView(parentMappingData), Containers::arrayView(parentFieldData)}, + SceneFieldData{SceneField::Mesh, Containers::arrayView(meshMappingData), Containers::arrayView(meshFieldData)}, + SceneFieldData{SceneField::Skin, Containers::arrayView(skinMappingData), Containers::arrayView(skinFieldData)}, + /* Array field */ + SceneFieldData{sceneFieldCustom(15), Containers::arrayView(fooMappingData), Containers::StridedArrayView2D{fooFieldData, {4, 2}}}, + /* Just to disambiguate between 2D and 3D */ + SceneFieldData{SceneField::Transformation, SceneMappingType::UnsignedShort, nullptr, SceneFieldType::Matrix4x4, nullptr} + })); + + SceneData scene = Implementation::sceneConvertToSingleFunctionObjects(original, + Containers::arrayView({ + /* Include also a field that's not present -- it should get skipped */ + SceneField::ImporterState, + /* Three additional mesh assignments that go to new objects */ + SceneField::Mesh + }), + Containers::arrayView({ + /* One assignment is to an object that has just one mesh, it should + not be copied anywhere, the other should be duplicated two + times */ + SceneField::Skin, + /* Array field with multiple assignments per object -- all should + be copied */ + sceneFieldCustom(15), + /* Include also a field that's not present -- it should get skipped */ + SceneField::Camera + }), 60); + + CORRADE_COMPARE_AS(scene.parentsAsArray(), (Containers::arrayView>({ + {2, -1}, + {15, -1}, + {21, -1}, + {22, 21}, + {60, 21}, /* duplicated mesh assignment to object 21 */ + {61, 21}, /* duplicated mesh assignment to object 21 */ + {62, 15} /* duplicated mesh assignment to object 15 */ + })), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.meshesMaterialsAsArray(), (Containers::arrayView>>({ + {15, {6, -1}}, + {21, {1, -1}}, + {60, {2, -1}}, /* duplicated mesh assignment to object 21 */ + {61, {4, -1}}, /* duplicated mesh assignment to object 21 */ + {22, {7, -1}}, + {62, {3, -1}} /* duplicated mesh assignment to object 15 */ + })), TestSuite::Compare::Container); + + CORRADE_COMPARE_AS(scene.skinsAsArray(), (Containers::arrayView>({ + {22, 5}, + {21, 13}, + {60, 13}, /* duplicated from object 21 */ + {61, 13}, /* duplicated from object 21 */ + })), TestSuite::Compare::Container); + + CORRADE_COMPARE_AS(scene.mapping(sceneFieldCustom(15)), Containers::arrayView({ + 15, 23, 15, 21, + 60, 61, /* duplicated from object 21 (two duplicates of one object) */ + 62, 62, /* duplicated from object 15 (two entries for one object) */ + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS((scene.field(sceneFieldCustom(15)).transposed<0, 1>()[0]), Containers::arrayView({ + 0, 2, 4, 6, + 6, 6, /* duplicated from object 21 (two duplicates of one object) */ + 0, 4, /* duplicated from object 15 (two entries for one object) */ + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS((scene.field(sceneFieldCustom(15)).transposed<0, 1>()[1]), Containers::arrayView({ + 1, 3, 5, 7, + 7, 7, /* duplicated from object 21 (two duplicates of one object) */ + 1, 5, /* duplicated from object 15 (two entries for one object) */ + }), TestSuite::Compare::Container); } }}}}