diff --git a/doc/changelog.dox b/doc/changelog.dox index cd06a0a0e..cafc2ff23 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -158,6 +158,9 @@ See also: - Added @ref MeshTools::generateQuadIndices() for quad triangulation including non-convex and non-planar quads +- New @ref MeshTools::filterOnlyAttributes() and + @ref MeshTools::filterExceptAttributes() utilities for filtering mesh data + attribute lists - New family of @ref MeshTools::transform2D(), @ref MeshTools::transform3D() and @ref MeshTools::transformTextureCoordinates2D() APIs for converting positions, normals, tangents, bitangents and texture coordinates directly diff --git a/src/Magnum/MeshTools/CMakeLists.txt b/src/Magnum/MeshTools/CMakeLists.txt index 3d4a98c99..1bfc7e271 100644 --- a/src/Magnum/MeshTools/CMakeLists.txt +++ b/src/Magnum/MeshTools/CMakeLists.txt @@ -33,6 +33,7 @@ set(MagnumMeshTools_GracefulAssert_SRCS CompressIndices.cpp Concatenate.cpp Duplicate.cpp + FilterAttributes.cpp FlipNormals.cpp GenerateIndices.cpp GenerateNormals.cpp @@ -46,6 +47,7 @@ set(MagnumMeshTools_HEADERS CompressIndices.h Concatenate.h Duplicate.h + FilterAttributes.h FlipNormals.h GenerateIndices.h GenerateNormals.h diff --git a/src/Magnum/MeshTools/FilterAttributes.cpp b/src/Magnum/MeshTools/FilterAttributes.cpp new file mode 100644 index 000000000..7a98b9e4b --- /dev/null +++ b/src/Magnum/MeshTools/FilterAttributes.cpp @@ -0,0 +1,216 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "FilterAttributes.h" + +#include +#include + +#include "Magnum/Trade/MeshData.h" + +namespace Magnum { namespace MeshTools { + +namespace { + +bool hasAttribute(const Containers::ArrayView attributes, const Trade::MeshAttribute attribute) { + for(const Trade::MeshAttribute i: attributes) + if(i == attribute) return true; + return false; +} + +/** @todo drop this insanity in favor of a BitArray */ +bool hasAttribute(const Containers::ArrayView attributes, const UnsignedInt attribute) { + for(const UnsignedInt i: attributes) + if(i == attribute) return true; + return false; +} + +} + +Trade::MeshData filterOnlyAttributes(const Trade::MeshData& data, const Containers::ArrayView attributes) { + /* Not asserting here for existence of attributes since that'd be another + O(n^2) operation */ + /** @todo but that's not consistent with the ID-based variant, or maybe do + this via a BitArray in the second loop and check that all attributes + got referenced afterwards? */ + + /* Pick just attributes from the list */ + Containers::Array filtered; + arrayReserve(filtered, data.attributeCount()); + for(UnsignedInt i = 0; i != data.attributeCount(); ++i) { + if(hasAttribute(attributes, data.attributeName(i))) + arrayAppend(filtered, data.attributeData(i)); + } + + /* Convert back to a default deleter to make this usable in plugins */ + arrayShrink(filtered, DefaultInit); + + /* Can't do just Trade::MeshIndexData{data.indices()} as that would discard + implementation-specific types. And can't do + Trade::meshIndexData{data.indexType(), view} + because asking for index type would assert on non-indexed meshes. */ + Trade::MeshIndexData indices; + if(data.isIndexed()) indices = Trade::MeshIndexData{ + data.indexType(), + Containers::StridedArrayView1D{ + data.indexData(), + data.indexData().data() + data.indexOffset(), + data.indexCount(), + data.indexStride()}}; + + return Trade::MeshData{data.primitive(), + {}, data.indexData(), indices, + {}, data.vertexData(), std::move(filtered), + data.vertexCount()}; +} + +Trade::MeshData filterOnlyAttributes(const Trade::MeshData& data, std::initializer_list attributes) { + return filterOnlyAttributes(data, Containers::arrayView(attributes)); +} + +Trade::MeshData filterOnlyAttributes(const Trade::MeshData& data, const Containers::ArrayView attributes) { + #ifndef CORRADE_NO_ASSERT + for(const UnsignedInt i: attributes) CORRADE_ASSERT(i < data.attributeCount(), + "MeshTools::filterOnlyAttributes(): index" << i << "out of range for" << data.attributeCount() << "attributes", + (Trade::MeshData{MeshPrimitive{}, 0})); + #endif + + /* Pick just attributes from the list */ + Containers::Array filtered; + arrayReserve(filtered, data.attributeCount()); + for(UnsignedInt i = 0; i != data.attributeCount(); ++i) { + if(hasAttribute(attributes, i)) + arrayAppend(filtered, data.attributeData(i)); + } + + /* Convert back to a default deleter to make this usable in plugins */ + arrayShrink(filtered, DefaultInit); + + /* Can't do just Trade::MeshIndexData{data.indices()} as that would discard + implementation-specific types. And can't do + Trade::meshIndexData{data.indexType(), view} + because asking for index type would assert on non-indexed meshes. */ + Trade::MeshIndexData indices; + if(data.isIndexed()) indices = Trade::MeshIndexData{ + data.indexType(), + Containers::StridedArrayView1D{ + data.indexData(), + data.indexData().data() + data.indexOffset(), + data.indexCount(), + data.indexStride()}}; + + return Trade::MeshData{data.primitive(), + {}, data.indexData(), indices, + {}, data.vertexData(), std::move(filtered), + data.vertexCount()}; +} + +Trade::MeshData filterOnlyAttributes(const Trade::MeshData& data, std::initializer_list attributes) { + return filterOnlyAttributes(data, Containers::arrayView(attributes)); +} + +Trade::MeshData filterExceptAttributes(const Trade::MeshData& data, const Containers::ArrayView attributes) { + /* Not asserting here for existence of attributes since that'd be another + O(n^2) operation */ + /** @todo but that's not consistent with the ID-based variant, or maybe do + this via a BitArray in the second loop and check that all attributes + got referenced afterwards? */ + + /* Pick just attributes from the list */ + Containers::Array filtered; + arrayReserve(filtered, data.attributeCount()); + for(UnsignedInt i = 0; i != data.attributeCount(); ++i) { + if(!hasAttribute(attributes, data.attributeName(i))) + arrayAppend(filtered, data.attributeData(i)); + } + + /* Convert back to a default deleter to make this usable in plugins */ + arrayShrink(filtered, DefaultInit); + + /* Can't do just Trade::MeshIndexData{data.indices()} as that would discard + implementation-specific types. And can't do + Trade::meshIndexData{data.indexType(), view} + because asking for index type would assert on non-indexed meshes. */ + Trade::MeshIndexData indices; + if(data.isIndexed()) indices = Trade::MeshIndexData{ + data.indexType(), + Containers::StridedArrayView1D{ + data.indexData(), + data.indexData().data() + data.indexOffset(), + data.indexCount(), + data.indexStride()}}; + + return Trade::MeshData{data.primitive(), + {}, data.indexData(), indices, + {}, data.vertexData(), std::move(filtered), + data.vertexCount()}; +} + +Trade::MeshData filterExceptAttributes(const Trade::MeshData& data, std::initializer_list attributes) { + return filterExceptAttributes(data, Containers::arrayView(attributes)); +} + +Trade::MeshData filterExceptAttributes(const Trade::MeshData& data, const Containers::ArrayView attributes) { + #ifndef CORRADE_NO_ASSERT + for(const UnsignedInt i: attributes) CORRADE_ASSERT(i < data.attributeCount(), + "MeshTools::filterExceptAttributes(): index" << i << "out of range for" << data.attributeCount() << "attributes", + (Trade::MeshData{MeshPrimitive{}, 0})); + #endif + + /* Pick just attributes from the list */ + Containers::Array filtered; + arrayReserve(filtered, data.attributeCount()); + for(UnsignedInt i = 0; i != data.attributeCount(); ++i) { + if(!hasAttribute(attributes, i)) + arrayAppend(filtered, data.attributeData(i)); + } + + /* Convert back to a default deleter to make this usable in plugins */ + arrayShrink(filtered, DefaultInit); + + /* Can't do just Trade::MeshIndexData{data.indices()} as that would discard + implementation-specific types. And can't do + Trade::meshIndexData{data.indexType(), view} + because asking for index type would assert on non-indexed meshes. */ + Trade::MeshIndexData indices; + if(data.isIndexed()) indices = Trade::MeshIndexData{ + data.indexType(), + Containers::StridedArrayView1D{ + data.indexData(), + data.indexData().data() + data.indexOffset(), + data.indexCount(), + data.indexStride()}}; + + return Trade::MeshData{data.primitive(), + {}, data.indexData(), indices, + {}, data.vertexData(), std::move(filtered), + data.vertexCount()}; +} + +Trade::MeshData filterExceptAttributes(const Trade::MeshData& data, std::initializer_list attributes) { + return filterExceptAttributes(data, Containers::arrayView(attributes)); +} + +}} diff --git a/src/Magnum/MeshTools/FilterAttributes.h b/src/Magnum/MeshTools/FilterAttributes.h new file mode 100644 index 000000000..6cf28b943 --- /dev/null +++ b/src/Magnum/MeshTools/FilterAttributes.h @@ -0,0 +1,140 @@ +#ifndef Magnum_MeshTools_FilterAttributes_h +#define Magnum_MeshTools_FilterAttributes_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Function @ref Magnum::MeshTools::filterOnlyAttributes(), @ref Magnum::MeshTools::filterExceptAttributes() + * @m_since_latest + */ + +#include + +#include "Magnum/MeshTools/visibility.h" +#include "Magnum/Trade/Trade.h" + +namespace Magnum { namespace MeshTools { + +/** +@brief Filter a mesh to contain only the selected subset of named attributes +@m_since_latest + +Returns a non-owning reference to the vertex and index buffer from @p data with +only the attributes that are listed in @p attributes. The index buffer, if +present, is left untouched. Attributes from the list that are not present in +@p data are skipped. All duplicates of a listed attribute are kept --- if you +want a different behavior, use the @ref filterOnlyAttributes(const Trade::MeshData&, Containers::ArrayView) +overload and pick attributes by their IDs instead. + +This function only operates on the attribute metadata --- if you'd like to have +the vertex data repacked to contain just the remaining attributes as well, pass +the output to @ref interleave(const Trade::MeshData&, Containers::ArrayView, InterleaveFlags) "interleave()" +without @ref InterleaveFlag::PreserveInterleavedAttributes set. +@see @ref reference() +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData filterOnlyAttributes(const Trade::MeshData& data, Containers::ArrayView attributes); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData filterOnlyAttributes(const Trade::MeshData& data, std::initializer_list attributes); + +/** +@brief Filter a mesh to contain only the selected subset of attributes +@m_since_latest + +Returns a non-owning reference to the vertex and index buffer from @p data with +only the attribute IDs listed in @p attributes. IDs specified more than once +don't result in given attribute being added multiple times. The index buffer, +if present, is left untouched. All attribute IDs are expected to be smaller +than @ref Trade::MeshData::attributeCount() const. + +This function only operates on the attribute metadata --- if you'd like to have +the vertex data repacked to contain just the remaining attributes as well, pass +the output to @ref interleave(const Trade::MeshData&, Containers::ArrayView, InterleaveFlags) "interleave()" +without @ref InterleaveFlag::PreserveInterleavedAttributes set. +@see @ref reference() +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData filterOnlyAttributes(const Trade::MeshData& data, Containers::ArrayView attributes); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData filterOnlyAttributes(const Trade::MeshData& data, std::initializer_list attributes); + +/** +@brief Filter a mesh to contain everything except the selected subset of named attributes +@m_since_latest + +Returns a non-owning reference to the vertex and index buffer from @p data with +only the attributes that are not listed in @p attributes. The index buffer, if +present, is left untouched. Attributes from the list that are not present in +@p data are skipped. All duplicates of a listed attribute are removed --- if +you want a different behavior, use the @ref filterExceptAttributes(const Trade::MeshData&, Containers::ArrayView) +overload and pick attributes by their IDs instead. If @p attributes is empty, +the behavior is equivalent to @ref reference(). + +This function only operates on the attribute metadata --- if you'd like to have +the vertex data repacked to contain just the remaining attributes as well, pass +the output to @ref interleave(const Trade::MeshData&, Containers::ArrayView, InterleaveFlags) "interleave()" +without @ref InterleaveFlag::PreserveInterleavedAttributes set. +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData filterExceptAttributes(const Trade::MeshData& data, Containers::ArrayView attributes); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData filterExceptAttributes(const Trade::MeshData& data, std::initializer_list attributes); + +/** +@brief Filter a mesh to contain everything except the selected subset of attributes +@m_since_latest + +Returns a non-owning reference to the vertex and index buffer from @p data with +only the attribute IDs that are not listed in @p attributes. IDs specified +multiple times behave like if specified just once. The index buffer, if +present, is left untouched. All attribute IDs are expected to be smaller than +@ref Trade::MeshData::attributeCount() const. If @p attributes is empty, the +behavior is equivalent to @ref reference(). + +This function only operates on the attribute metadata --- if you'd like to have +the vertex data repacked to contain just the remaining attributes as well, pass +the output to @ref interleave(const Trade::MeshData&, Containers::ArrayView, InterleaveFlags) "interleave()" +without @ref InterleaveFlag::PreserveInterleavedAttributes set. +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData filterExceptAttributes(const Trade::MeshData& data, Containers::ArrayView attributes); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData filterExceptAttributes(const Trade::MeshData& data, std::initializer_list attributes); + +}} + +#endif diff --git a/src/Magnum/MeshTools/Test/CMakeLists.txt b/src/Magnum/MeshTools/Test/CMakeLists.txt index 5d1d9bb1c..df8b7df77 100644 --- a/src/Magnum/MeshTools/Test/CMakeLists.txt +++ b/src/Magnum/MeshTools/Test/CMakeLists.txt @@ -27,6 +27,7 @@ corrade_add_test(MeshToolsCombineTest CombineTest.cpp LIBRARIES MagnumMeshToolsT corrade_add_test(MeshToolsCompressIndicesTest CompressIndicesTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsConcatenateTest ConcatenateTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsDuplicateTest DuplicateTest.cpp LIBRARIES MagnumMeshToolsTestLib) +corrade_add_test(MeshToolsFilterAttributesTest FilterAttributesTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsFlipNormalsTest FlipNormalsTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsGenerateIndicesTest GenerateIndicesTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsGenerateNormalsTest GenerateNormalsTest.cpp LIBRARIES MagnumMeshToolsTestLib MagnumPrimitives) @@ -51,6 +52,7 @@ set_target_properties( MeshToolsCompressIndicesTest MeshToolsConcatenateTest MeshToolsDuplicateTest + MeshToolsFilterAttributesTest MeshToolsFlipNormalsTest MeshToolsGenerateIndicesTest MeshToolsGenerateNormalsTest diff --git a/src/Magnum/MeshTools/Test/FilterAttributesTest.cpp b/src/Magnum/MeshTools/Test/FilterAttributesTest.cpp new file mode 100644 index 000000000..2a41a1923 --- /dev/null +++ b/src/Magnum/MeshTools/Test/FilterAttributesTest.cpp @@ -0,0 +1,570 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include + +#include "Magnum/Math/Vector4.h" +#include "Magnum/MeshTools/FilterAttributes.h" +#include "Magnum/Trade/MeshData.h" + +namespace Magnum { namespace MeshTools { namespace Test { namespace { + +struct FilterAttributesTest: TestSuite::Tester { + explicit FilterAttributesTest(); + + void filterOnlyAttributeNames(); + void filterOnlyAttributeNamesNoIndexData(); + void filterOnlyAttributeNamesNoAttributeData(); + + void filterOnlyAttributeIds(); + void filterOnlyAttributeIdsOutOfBounds(); + void filterOnlyAttributeIdsNoIndexData(); + void filterOnlyAttributeIdsNoAttributeData(); + + void filterExceptAttributeNames(); + void filterExceptAttributeNamesNoIndexData(); + void filterExceptAttributeNamesNoAttributeData(); + + void filterExceptAttributeIds(); + void filterExceptAttributeIdsOutOfBounds(); + void filterExceptAttributeIdsNoIndexData(); + void filterExceptAttributeIdsNoAttributeData(); +}; + +const struct { + const char* name; + MeshIndexType indexType; +} ImplementationSpecificIndexTypeData[]{ + {"", MeshIndexType::UnsignedShort}, + {"implementation-specific index type", meshIndexTypeWrap(0xcaca)} +}; + +FilterAttributesTest::FilterAttributesTest() { + addInstancedTests({&FilterAttributesTest::filterOnlyAttributeNames}, + Containers::arraySize(ImplementationSpecificIndexTypeData)); + + addTests({&FilterAttributesTest::filterOnlyAttributeNamesNoIndexData, + &FilterAttributesTest::filterOnlyAttributeNamesNoAttributeData}); + + addInstancedTests({&FilterAttributesTest::filterOnlyAttributeIds}, + Containers::arraySize(ImplementationSpecificIndexTypeData)); + + addTests({&FilterAttributesTest::filterOnlyAttributeIdsOutOfBounds, + &FilterAttributesTest::filterOnlyAttributeIdsNoIndexData, + &FilterAttributesTest::filterOnlyAttributeIdsNoAttributeData}); + + addInstancedTests({&FilterAttributesTest::filterExceptAttributeNames}, + Containers::arraySize(ImplementationSpecificIndexTypeData)); + + addTests({&FilterAttributesTest::filterExceptAttributeNamesNoIndexData, + &FilterAttributesTest::filterExceptAttributeNamesNoAttributeData}); + + addInstancedTests({&FilterAttributesTest::filterExceptAttributeIds}, + Containers::arraySize(ImplementationSpecificIndexTypeData)); + + addTests({&FilterAttributesTest::filterExceptAttributeIdsOutOfBounds, + &FilterAttributesTest::filterExceptAttributeIdsNoIndexData, + &FilterAttributesTest::filterExceptAttributeIdsNoAttributeData}); +} + +struct Vertex { + Vector3 position; + Vector4 tangent; + Vector2 textureCoordinates1, textureCoordinates2; +}; + +void FilterAttributesTest::filterOnlyAttributeNames() { + auto&& data = ImplementationSpecificIndexTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array indexData{5*sizeof(UnsignedShort)}; + Containers::StridedArrayView1D indices = Containers::arrayCast(indexData); + Containers::Array vertexData{3*sizeof(Vertex)}; + Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(indexData), Trade::MeshIndexData{data.indexType, indices}, + std::move(vertexData), { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, vertices.slice(&Vertex::tangent)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates1)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates2)}, + }}; + + Trade::MeshData filtered = filterOnlyAttributes(mesh, { + Trade::MeshAttribute::Position, + Trade::MeshAttribute::Normal, /* not present, ignored */ + Trade::MeshAttribute::TextureCoordinates, /* present twice */ + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::TriangleStrip); + + CORRADE_VERIFY(filtered.isIndexed()); + CORRADE_COMPARE(filtered.indexCount(), 5); + CORRADE_COMPARE(filtered.indexType(), data.indexType); + CORRADE_COMPARE(filtered.indexData().data(), indices.data()); + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.vertexCount(), 3); + CORRADE_COMPARE(filtered.vertexData().data(), vertices.data()); + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + /* Testing just the offset if it matches expectations, the + MeshAttributeData is copied directly so no metadata should get lost */ + CORRADE_COMPARE(filtered.attributeCount(), 3); + CORRADE_COMPARE(filtered.attributeName(0), Trade::MeshAttribute::Position); + CORRADE_COMPARE(filtered.attributeOffset(0), offsetof(Vertex, position)); + CORRADE_COMPARE(filtered.attributeName(1), Trade::MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(filtered.attributeOffset(1), offsetof(Vertex, textureCoordinates1)); + CORRADE_COMPARE(filtered.attributeName(2), Trade::MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(filtered.attributeOffset(2), offsetof(Vertex, textureCoordinates2)); + + /* The attribute data should not be a growable array to make this usable in + plugins */ + Containers::Array attributeData = filtered.releaseAttributeData(); + CORRADE_VERIFY(!attributeData.deleter()); +} + +void FilterAttributesTest::filterOnlyAttributeNamesNoIndexData() { + /* A trivial subset of filterOnlyAttributeNames() testing it doesn't blow + up if the mesh is not indexed */ + + Containers::Array vertexData{3*sizeof(Vertex)}; + Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + + Trade::MeshData mesh{MeshPrimitive::TriangleFan, + std::move(vertexData), { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates1)} + }}; + + Trade::MeshData filtered = filterOnlyAttributes(mesh, { + Trade::MeshAttribute::TextureCoordinates + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::TriangleFan); + + CORRADE_VERIFY(!filtered.isIndexed()); + /* Consistent with behavior in reference() for index-less meshes */ + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.vertexCount(), 3); + CORRADE_COMPARE(filtered.vertexData().data(), vertices.data()); + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + /* Testing just the offset if it matches expectations, the + MeshAttributeData is copied directly so no metadata should get lost */ + CORRADE_COMPARE(filtered.attributeCount(), 1); + CORRADE_COMPARE(filtered.attributeName(0), Trade::MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(filtered.attributeOffset(0), offsetof(Vertex, textureCoordinates1)); +} + +void FilterAttributesTest::filterOnlyAttributeNamesNoAttributeData() { + Containers::Array indexData{5*sizeof(UnsignedShort)}; + Containers::StridedArrayView1D indices = Containers::arrayCast(indexData); + + Trade::MeshData mesh{MeshPrimitive::Points, + std::move(indexData), Trade::MeshIndexData{indices}, 15}; + + Trade::MeshData filtered = filterOnlyAttributes(mesh, { + Trade::MeshAttribute::Position + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::Points); + + CORRADE_VERIFY(filtered.isIndexed()); + CORRADE_COMPARE(filtered.indexCount(), 5); + CORRADE_COMPARE(filtered.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(filtered.indexData().data(), indices.data()); + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + /* The vertex count should get preserved even if there are no attributes */ + CORRADE_COMPARE(filtered.vertexCount(), 15); + /* Consistent with behavior in reference() for vertex-less meshes */ + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.attributeCount(), 0); +} + +void FilterAttributesTest::filterOnlyAttributeIds() { + auto&& data = ImplementationSpecificIndexTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array indexData{5*sizeof(UnsignedShort)}; + Containers::StridedArrayView1D indices = Containers::arrayCast(indexData); + Containers::Array vertexData{3*sizeof(Vertex)}; + Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(indexData), Trade::MeshIndexData{data.indexType, indices}, + std::move(vertexData), { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, vertices.slice(&Vertex::tangent)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates1)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates2)}, + }}; + + Trade::MeshData filtered = filterOnlyAttributes(mesh, { + /* The attribute 1 is specified twice, but that won't result in the + same attribute being added twice */ + 1, 1, 3 + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::TriangleStrip); + + CORRADE_VERIFY(filtered.isIndexed()); + CORRADE_COMPARE(filtered.indexCount(), 5); + CORRADE_COMPARE(filtered.indexType(), data.indexType); + CORRADE_COMPARE(filtered.indexData().data(), indices.data()); + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.vertexCount(), 3); + CORRADE_COMPARE(filtered.vertexData().data(), vertices.data()); + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + /* Testing just the offset if it matches expectations, the + MeshAttributeData is copied directly so no metadata should get lost */ + CORRADE_COMPARE(filtered.attributeCount(), 2); + CORRADE_COMPARE(filtered.attributeName(0), Trade::MeshAttribute::Tangent); + CORRADE_COMPARE(filtered.attributeOffset(0), offsetof(Vertex, tangent)); + CORRADE_COMPARE(filtered.attributeName(1), Trade::MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(filtered.attributeOffset(1), offsetof(Vertex, textureCoordinates2)); + + /* The attribute data should not be a growable array to make this usable in + plugins */ + Containers::Array attributeData = filtered.releaseAttributeData(); + CORRADE_VERIFY(!attributeData.deleter()); +} + +void FilterAttributesTest::filterOnlyAttributeIdsOutOfBounds() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Vertex vertices[3]{}; + + Trade::MeshData mesh{MeshPrimitive::TriangleFan, + {}, vertices, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, Containers::stridedArrayView(vertices).slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, Containers::stridedArrayView(vertices).slice(&Vertex::textureCoordinates1)} + }}; + + std::ostringstream out; + Error redirectError{&out}; + filterOnlyAttributes(mesh, {0, 0, 2}); + CORRADE_COMPARE(out.str(), "MeshTools::filterOnlyAttributes(): index 2 out of range for 2 attributes\n"); +} + +void FilterAttributesTest::filterOnlyAttributeIdsNoIndexData() { + /* A trivial subset of filterOnlyAttributeIds() testing it doesn't blow up + if the mesh is not indexed */ + + Containers::Array vertexData{3*sizeof(Vertex)}; + Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + + Trade::MeshData mesh{MeshPrimitive::TriangleFan, + std::move(vertexData), { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates1)} + }}; + + Trade::MeshData filtered = filterOnlyAttributes(mesh, { + 0 + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::TriangleFan); + + CORRADE_VERIFY(!filtered.isIndexed()); + /* Consistent with behavior in reference() for index-less meshes */ + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.vertexCount(), 3); + CORRADE_COMPARE(filtered.vertexData().data(), vertices.data()); + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + /* Testing just the offset if it matches expectations, the + MeshAttributeData is copied directly so no metadata should get lost */ + CORRADE_COMPARE(filtered.attributeCount(), 1); + CORRADE_COMPARE(filtered.attributeName(0), Trade::MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(filtered.attributeOffset(0), offsetof(Vertex, textureCoordinates1)); +} + +void FilterAttributesTest::filterOnlyAttributeIdsNoAttributeData() { + Containers::Array indexData{5*sizeof(UnsignedShort)}; + Containers::StridedArrayView1D indices = Containers::arrayCast(indexData); + + Trade::MeshData mesh{MeshPrimitive::Points, + std::move(indexData), Trade::MeshIndexData{indices}, 15}; + + Trade::MeshData filtered = filterOnlyAttributes(mesh, std::initializer_list{}); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::Points); + + CORRADE_VERIFY(filtered.isIndexed()); + CORRADE_COMPARE(filtered.indexCount(), 5); + CORRADE_COMPARE(filtered.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(filtered.indexData().data(), indices.data()); + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + /* The vertex count should get preserved even if there are no attributes */ + CORRADE_COMPARE(filtered.vertexCount(), 15); + /* Consistent with behavior in reference() for vertex-less meshes */ + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.attributeCount(), 0); +} + +void FilterAttributesTest::filterExceptAttributeNames() { + auto&& data = ImplementationSpecificIndexTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array indexData{5*sizeof(UnsignedShort)}; + Containers::StridedArrayView1D indices = Containers::arrayCast(indexData); + Containers::Array vertexData{3*sizeof(Vertex)}; + Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(indexData), Trade::MeshIndexData{data.indexType, indices}, + std::move(vertexData), { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, vertices.slice(&Vertex::tangent)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates1)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates2)}, + /* Positions again, just under a different name. Should be kept. */ + Trade::MeshAttributeData{Trade::meshAttributeCustom(0xbaf), vertices.slice(&Vertex::position)}, + }}; + + Trade::MeshData filtered = filterExceptAttributes(mesh, { + Trade::MeshAttribute::Position, + Trade::MeshAttribute::Normal, /* not present, ignored */ + Trade::MeshAttribute::TextureCoordinates, /* present twice */ + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::TriangleStrip); + + CORRADE_VERIFY(filtered.isIndexed()); + CORRADE_COMPARE(filtered.indexCount(), 5); + CORRADE_COMPARE(filtered.indexType(), data.indexType); + CORRADE_COMPARE(filtered.indexData().data(), indices.data()); + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.vertexCount(), 3); + CORRADE_COMPARE(filtered.vertexData().data(), vertices.data()); + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + /* Testing just the offset if it matches expectations, the + MeshAttributeData is copied directly so no metadata should get lost */ + CORRADE_COMPARE(filtered.attributeCount(), 2); + CORRADE_COMPARE(filtered.attributeName(0), Trade::MeshAttribute::Tangent); + CORRADE_COMPARE(filtered.attributeOffset(0), offsetof(Vertex, tangent)); + CORRADE_COMPARE(filtered.attributeName(1), Trade::meshAttributeCustom(0xbaf)); + CORRADE_COMPARE(filtered.attributeOffset(1), offsetof(Vertex, position)); + + /* The attribute data should not be a growable array to make this usable in + plugins */ + Containers::Array attributeData = filtered.releaseAttributeData(); + CORRADE_VERIFY(!attributeData.deleter()); +} + +void FilterAttributesTest::filterExceptAttributeNamesNoIndexData() { + /* A trivial subset of filterExceptAttributeNames() testing it doesn't blow + up if the mesh is not indexed */ + + Containers::Array vertexData{3*sizeof(Vertex)}; + Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + + Trade::MeshData mesh{MeshPrimitive::TriangleFan, + std::move(vertexData), { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates1)} + }}; + + Trade::MeshData filtered = filterExceptAttributes(mesh, { + Trade::MeshAttribute::Position + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::TriangleFan); + + CORRADE_VERIFY(!filtered.isIndexed()); + /* Consistent with behavior in reference() for index-less meshes */ + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.vertexCount(), 3); + CORRADE_COMPARE(filtered.vertexData().data(), vertices.data()); + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + /* Testing just the offset if it matches expectations, the + MeshAttributeData is copied directly so no metadata should get lost */ + CORRADE_COMPARE(filtered.attributeCount(), 1); + CORRADE_COMPARE(filtered.attributeName(0), Trade::MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(filtered.attributeOffset(0), offsetof(Vertex, textureCoordinates1)); +} + +void FilterAttributesTest::filterExceptAttributeNamesNoAttributeData() { + Containers::Array indexData{5*sizeof(UnsignedShort)}; + Containers::StridedArrayView1D indices = Containers::arrayCast(indexData); + + Trade::MeshData mesh{MeshPrimitive::Points, + std::move(indexData), Trade::MeshIndexData{indices}, 15}; + + Trade::MeshData filtered = filterExceptAttributes(mesh, { + Trade::MeshAttribute::Position + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::Points); + + CORRADE_VERIFY(filtered.isIndexed()); + CORRADE_COMPARE(filtered.indexCount(), 5); + CORRADE_COMPARE(filtered.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(filtered.indexData().data(), indices.data()); + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + /* The vertex count should get preserved even if there are no attributes */ + CORRADE_COMPARE(filtered.vertexCount(), 15); + /* Consistent with behavior in reference() for vertex-less meshes */ + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.attributeCount(), 0); +} + +void FilterAttributesTest::filterExceptAttributeIds() { + auto&& data = ImplementationSpecificIndexTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array indexData{5*sizeof(UnsignedShort)}; + Containers::StridedArrayView1D indices = Containers::arrayCast(indexData); + Containers::Array vertexData{3*sizeof(Vertex)}; + Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(indexData), Trade::MeshIndexData{data.indexType, indices}, + std::move(vertexData), { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, vertices.slice(&Vertex::tangent)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates1)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates2)}, + }}; + + Trade::MeshData filtered = filterExceptAttributes(mesh, { + /* The attribute 1 is specified twice, but that won't result in + attribute 1 being removed and then again */ + 1, 1, 3 + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::TriangleStrip); + + CORRADE_VERIFY(filtered.isIndexed()); + CORRADE_COMPARE(filtered.indexCount(), 5); + CORRADE_COMPARE(filtered.indexType(), data.indexType); + CORRADE_COMPARE(filtered.indexData().data(), indices.data()); + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.vertexCount(), 3); + CORRADE_COMPARE(filtered.vertexData().data(), vertices.data()); + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + /* Testing just the offset if it matches expectations, the + MeshAttributeData is copied directly so no metadata should get lost */ + CORRADE_COMPARE(filtered.attributeCount(), 2); + CORRADE_COMPARE(filtered.attributeName(0), Trade::MeshAttribute::Position); + CORRADE_COMPARE(filtered.attributeOffset(0), offsetof(Vertex, position)); + CORRADE_COMPARE(filtered.attributeName(1), Trade::MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(filtered.attributeOffset(1), offsetof(Vertex, textureCoordinates1)); + + /* The attribute data should not be a growable array to make this usable in + plugins */ + Containers::Array attributeData = filtered.releaseAttributeData(); + CORRADE_VERIFY(!attributeData.deleter()); +} + +void FilterAttributesTest::filterExceptAttributeIdsOutOfBounds() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Vertex vertices[3]{}; + + Trade::MeshData mesh{MeshPrimitive::TriangleFan, + {}, vertices, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, Containers::stridedArrayView(vertices).slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, Containers::stridedArrayView(vertices).slice(&Vertex::textureCoordinates1)} + }}; + + std::ostringstream out; + Error redirectError{&out}; + filterExceptAttributes(mesh, {0, 0, 2}); + CORRADE_COMPARE(out.str(), "MeshTools::filterExceptAttributes(): index 2 out of range for 2 attributes\n"); +} + +void FilterAttributesTest::filterExceptAttributeIdsNoIndexData() { + /* A trivial subset of filterExceptAttributeIds() testing it doesn't blow up + if the mesh is not indexed */ + + Containers::Array vertexData{3*sizeof(Vertex)}; + Containers::StridedArrayView1D vertices = Containers::arrayCast(vertexData); + + Trade::MeshData mesh{MeshPrimitive::TriangleFan, + std::move(vertexData), { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates1)} + }}; + + Trade::MeshData filtered = filterExceptAttributes(mesh, { + 0 + }); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::TriangleFan); + + CORRADE_VERIFY(!filtered.isIndexed()); + /* Consistent with behavior in reference() for index-less meshes */ + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.vertexCount(), 3); + CORRADE_COMPARE(filtered.vertexData().data(), vertices.data()); + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + /* Testing just the offset if it matches expectations, the + MeshAttributeData is copied directly so no metadata should get lost */ + CORRADE_COMPARE(filtered.attributeCount(), 1); + CORRADE_COMPARE(filtered.attributeName(0), Trade::MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(filtered.attributeOffset(0), offsetof(Vertex, textureCoordinates1)); +} + +void FilterAttributesTest::filterExceptAttributeIdsNoAttributeData() { + Containers::Array indexData{5*sizeof(UnsignedShort)}; + Containers::StridedArrayView1D indices = Containers::arrayCast(indexData); + + Trade::MeshData mesh{MeshPrimitive::Points, + std::move(indexData), Trade::MeshIndexData{indices}, 15}; + + Trade::MeshData filtered = filterExceptAttributes(mesh, std::initializer_list{}); + CORRADE_COMPARE(filtered.primitive(), MeshPrimitive::Points); + + CORRADE_VERIFY(filtered.isIndexed()); + CORRADE_COMPARE(filtered.indexCount(), 5); + CORRADE_COMPARE(filtered.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(filtered.indexData().data(), indices.data()); + CORRADE_COMPARE(filtered.indexDataFlags(), Trade::DataFlags{}); + + /* The vertex count should get preserved even if there are no attributes */ + CORRADE_COMPARE(filtered.vertexCount(), 15); + /* Consistent with behavior in reference() for vertex-less meshes */ + CORRADE_COMPARE(filtered.vertexDataFlags(), Trade::DataFlags{}); + + CORRADE_COMPARE(filtered.attributeCount(), 0); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::MeshTools::Test::FilterAttributesTest)