From a802d6cd11f91071ce28c19cdab9a8f780a80c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Tue, 14 Dec 2021 21:09:59 +0100 Subject: [PATCH] SceneTools: utility to reorder & cluster the parent field. Useful for calculating hierarchic transformations. --- doc/snippets/CMakeLists.txt | 8 + doc/snippets/MagnumSceneTools.cpp | 65 ++++ src/Magnum/SceneTools/CMakeLists.txt | 52 ++- src/Magnum/SceneTools/OrderClusterParents.cpp | 122 +++++++ src/Magnum/SceneTools/OrderClusterParents.h | 77 +++++ src/Magnum/SceneTools/Test/CMakeLists.txt | 2 + .../Test/OrderClusterParentsTest.cpp | 324 ++++++++++++++++++ 7 files changed, 637 insertions(+), 13 deletions(-) create mode 100644 doc/snippets/MagnumSceneTools.cpp create mode 100644 src/Magnum/SceneTools/OrderClusterParents.cpp create mode 100644 src/Magnum/SceneTools/OrderClusterParents.h create mode 100644 src/Magnum/SceneTools/Test/OrderClusterParentsTest.cpp diff --git a/doc/snippets/CMakeLists.txt b/doc/snippets/CMakeLists.txt index f1c6299e6..a5210bb65 100644 --- a/doc/snippets/CMakeLists.txt +++ b/doc/snippets/CMakeLists.txt @@ -188,6 +188,14 @@ if(WITH_SCENEGRAPH) endif() endif() +if(WITH_SCENETOOLS) + add_library(snippets-MagnumSceneTools STATIC + MagnumSceneTools.cpp) + target_link_libraries(snippets-MagnumSceneTools PRIVATE MagnumSceneTools) + set_target_properties(snippets-MagnumSceneTools + PROPERTIES FOLDER "Magnum/doc/snippets") +endif() + if(WITH_VK) add_library(snippets-MagnumVk STATIC MagnumVk.cpp) target_link_libraries(snippets-MagnumVk PRIVATE MagnumVk) diff --git a/doc/snippets/MagnumSceneTools.cpp b/doc/snippets/MagnumSceneTools.cpp new file mode 100644 index 000000000..867849ee0 --- /dev/null +++ b/doc/snippets/MagnumSceneTools.cpp @@ -0,0 +1,65 @@ +/* + 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 "Magnum/Math/Matrix4.h" +#include "Magnum/SceneTools/OrderClusterParents.h" +#include "Magnum/Trade/SceneData.h" + +#define DOXYGEN_ELLIPSIS(...) __VA_ARGS__ + +using namespace Magnum; + +int main() { +{ +/* [orderClusterParents-transformations] */ +Trade::SceneData scene = DOXYGEN_ELLIPSIS(Trade::SceneData{{}, 0, nullptr, {}}); + +/* Put all transformations into an array indexed by object ID. Objects + implicitly have an identity transformation, first element is reserved for + the global transformation. */ +Containers::Array transformations{std::size_t(scene.mappingBound() + 1)}; +for(const Containers::Pair& transformation: + scene.transformations3DAsArray()) +{ + transformations[transformation.first() + 1] = transformation.second(); +} + +/* Go through ordered parents and compose absolute transformations for all + nodes in the hierarchy, objects in the root use transformations[0]. The + function ensures that the parent transformation is already calculated when + referenced by child nodes. */ +for(const Containers::Pair& parent: + SceneTools::orderClusterParents(scene)) +{ + transformations[parent.first() + 1] = + transformations[parent.second() + 1]* + transformations[parent.first() + 1]; +} +/* [orderClusterParents-transformations] */ +} +} diff --git a/src/Magnum/SceneTools/CMakeLists.txt b/src/Magnum/SceneTools/CMakeLists.txt index 622156ad1..aaa2da78d 100644 --- a/src/Magnum/SceneTools/CMakeLists.txt +++ b/src/Magnum/SceneTools/CMakeLists.txt @@ -23,7 +23,15 @@ # DEALINGS IN THE SOFTWARE. # +# Files shared between main library and unit test library +set(MagnumSceneTools_SRCS ) + +# Files compiled with different flags for main library and unit test library +set(MagnumSceneTools_GracefulAssert_SRCS + OrderClusterParents.cpp) + set(MagnumSceneTools_HEADERS + OrderClusterParents.h SceneTools.h visibility.h) @@ -32,8 +40,24 @@ set(MagnumSceneTools_PRIVATE_HEADERS Implementation/combine.h Implementation/convertToSingleFunctionObjects.h) +## Objects shared between main and test library +#add_library(MagnumSceneToolsObjects OBJECT + #${MagnumSceneTools_SRCS} + #${MagnumSceneTools_HEADERS} + #${MagnumSceneTools_PRIVATE_HEADERS}) +#target_include_directories(MagnumSceneToolsObjects PUBLIC $) +#if(NOT BUILD_STATIC) + #target_compile_definitions(MagnumSceneToolsObjects PRIVATE "MagnumSceneToolsObjects_EXPORTS") +#endif() +#if(NOT BUILD_STATIC OR BUILD_STATIC_PIC) + #set_target_properties(MagnumSceneToolsObjects PROPERTIES POSITION_INDEPENDENT_CODE ON) +#endif() +#set_target_properties(MagnumSceneToolsObjects PROPERTIES FOLDER "Magnum/SceneTools") + # Main SceneTools library -add_library(MagnumSceneTools INTERFACE +add_library(MagnumSceneTools ${SHARED_OR_STATIC} + #$ + ${MagnumSceneTools_GracefulAssert_SRCS} ${MagnumSceneTools_HEADERS} ${MagnumSceneTools_PRIVATE_HEADERS}) set_target_properties(MagnumSceneTools PROPERTIES @@ -44,9 +68,9 @@ if(NOT BUILD_STATIC) elseif(BUILD_STATIC_PIC) set_target_properties(MagnumSceneTools PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() -#target_link_libraries(MagnumSceneTools PUBLIC - #Magnum - #MagnumTrade) +target_link_libraries(MagnumSceneTools PUBLIC + Magnum + MagnumTrade) install(TARGETS MagnumSceneTools RUNTIME DESTINATION ${MAGNUM_BINARY_INSTALL_DIR} @@ -70,18 +94,20 @@ endif() if(BUILD_TESTS) # Library with graceful assert for testing - add_library(MagnumSceneToolsTestLib INTERFACE ) - #set_target_properties(MagnumSceneToolsTestLib PROPERTIES - #DEBUG_POSTFIX "-d" - #FOLDER "Magnum/SceneTools") - #target_compile_definitions(MagnumSceneToolsTestLib PRIVATE - #"CORRADE_GRACEFUL_ASSERT" "MagnumSceneTools_EXPORTS") + add_library(MagnumSceneToolsTestLib ${SHARED_OR_STATIC} + #$ + ${MagnumSceneTools_GracefulAssert_SRCS}) + set_target_properties(MagnumSceneToolsTestLib PROPERTIES + DEBUG_POSTFIX "-d" + FOLDER "Magnum/SceneTools") + target_compile_definitions(MagnumSceneToolsTestLib PRIVATE + "CORRADE_GRACEFUL_ASSERT" "MagnumSceneTools_EXPORTS") if(BUILD_STATIC_PIC) set_target_properties(MagnumSceneToolsTestLib PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() - #target_link_libraries(MagnumSceneToolsTestLib PUBLIC - #Magnum - #MagnumTrade) + target_link_libraries(MagnumSceneToolsTestLib PUBLIC + Magnum + MagnumTrade) add_subdirectory(Test) endif() diff --git a/src/Magnum/SceneTools/OrderClusterParents.cpp b/src/Magnum/SceneTools/OrderClusterParents.cpp new file mode 100644 index 000000000..db41636ab --- /dev/null +++ b/src/Magnum/SceneTools/OrderClusterParents.cpp @@ -0,0 +1,122 @@ +/* + 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 "OrderClusterParents.h" + +#include +#include +#include +#include + +#include "Magnum/Trade/SceneData.h" + +namespace Magnum { namespace SceneTools { + +Containers::Array> orderClusterParents(const Trade::SceneData& scene) { + const Containers::Optional parentFieldId = scene.findFieldId(Trade::SceneField::Parent); + CORRADE_ASSERT(parentFieldId, + "SceneTools::orderClusterParents(): the scene has no hierarchy", {}); + Containers::Array> out{NoInit, scene.fieldSize(*parentFieldId)}; + Containers::StridedArrayView1D mapping{out, &out.data()->first(), out.size(), sizeof(decltype(out)::Type)}; + Containers::StridedArrayView1D parentOffset{out, &out.data()->second(), out.size(), sizeof(decltype(out)::Type)}; + orderClusterParentsInto(scene, mapping, parentOffset); + return out; +} + +void orderClusterParentsInto(const Trade::SceneData& scene, const Containers::StridedArrayView1D& mappingDestination, const Containers::StridedArrayView1D& parentDestination) { + #ifndef CORRADE_NO_ASSERT + const Containers::Optional parentFieldId = scene.findFieldId(Trade::SceneField::Parent); + CORRADE_ASSERT(parentFieldId, + "SceneTools::orderClusterParentsInto(): the scene has no hierarchy", ); + const std::size_t parentFieldSize = scene.fieldSize(*parentFieldId); + CORRADE_ASSERT(mappingDestination.size() == parentFieldSize, + "SceneTools::orderClusterParentsInto(): expected mapping destination view with" << parentFieldSize << "elements but got" << mappingDestination.size(), ); + CORRADE_ASSERT(parentDestination.size() == scene.fieldSize(*parentFieldId), + "SceneTools::orderClusterParentsInto(): expected parent destination view with" << parentFieldSize << "elements but got" << parentDestination.size(), ); + #endif + + /* Convert the parent list to a child list to sort them toplogically */ + const Containers::Array> parents = scene.parentsAsArray(); + + /* Children offset for each node including root. First calculate the count + of children for each ... */ + Containers::Array childrenOffsets{DirectInit, scene.mappingBound() + 2, 0u}; + for(const Containers::Pair& parent: parents) { + CORRADE_INTERNAL_ASSERT(parent.first() < scene.mappingBound() && (parent.second() == -1 || UnsignedInt(parent.second()) < scene.mappingBound())); + ++childrenOffsets[parent.second() + 1]; + } + + /* ... then convert the counts to a running offset. Now + `[childrenOffsets[i + 1], childrenOffsets[i + 2])` contains a range in + which the `children` array below contains a list of children for `i`. */ + UnsignedInt offset = 0; + for(UnsignedInt& i: childrenOffsets) { + UnsignedInt nextOffset = offset + i; + i = offset; + offset = nextOffset; + } + CORRADE_INTERNAL_ASSERT(offset == parents.size()); + + /* Go through the parent list again, convert that to child ranges */ + Containers::Array children{NoInit, parents.size()}; + { + Containers::Array currentChildrenOffsets{DirectInit, scene.mappingBound() + 1, 0u}; + for(const Containers::Pair& parent: parents) { + UnsignedInt& currentChildrenOffset = currentChildrenOffsets[parent.second() + 1]; + children[childrenOffsets[parent.second() + 1] + currentChildrenOffset] = parent.first(); + ++currentChildrenOffset; + } + } + + /* Go breadth-first (so we have nodes sharing the same parent next to each + other) and build a list of (id, parent id) where a parent is always + before its children */ + std::size_t outputOffset = 0; + Containers::Array parentsToProcess; + arrayAppend(parentsToProcess, -1); + for(std::size_t i = 0; i != parentsToProcess.size(); ++i) { + const Int objectId = parentsToProcess[i]; + for(std::size_t j = childrenOffsets[objectId + 1], jMax = childrenOffsets[objectId + 2]; j != jMax; ++j) { + /** @todo better diagnostic once we can use a BitArray to detect + which nodes are parented more than once (OTOH maybe that's + unnecessary extra work which isn't desired to be done here but + should be instead in a dedicated cycle/sparse checker utility?) */ + CORRADE_ASSERT_OUTPUT(outputOffset < parents.size(), + "SceneTools::orderClusterParents(): hierarchy is cyclic", ); + arrayAppend(parentsToProcess, children[j]); + mappingDestination[outputOffset] = children[j]; + parentDestination[outputOffset] = objectId; + ++outputOffset; + } + } + + /** @todo better diagnostic once we can use a BitArray to detect which + nodes are unreachable from root (OTOH again maybe that's undesirable + extra work that doesn't belong here?) */ + CORRADE_ASSERT(outputOffset == parents.size(), + "SceneTools::orderClusterParents(): hierarchy is sparse", ); +} + +}} diff --git a/src/Magnum/SceneTools/OrderClusterParents.h b/src/Magnum/SceneTools/OrderClusterParents.h new file mode 100644 index 000000000..902032697 --- /dev/null +++ b/src/Magnum/SceneTools/OrderClusterParents.h @@ -0,0 +1,77 @@ +#ifndef Magnum_SceneTools_OrderClusterParents_h +#define Magnum_SceneTools_OrderClusterParents_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::SceneTools::orderClusterParents(), @ref Magnum::SceneTools::orderClusterParentsInto() + * @m_since_latest + */ + +#include "Magnum/Magnum.h" +#include "Magnum/SceneTools/visibility.h" +#include "Magnum/Trade/Trade.h" + +namespace Magnum { namespace SceneTools { + +/** +@brief Calculate ordered and clustered parents +@m_since_latest + +Extracts the @ref Trade::SceneField::Parent field from @p scene and converts it +to match the following rules: + +- a parent object reference appears always before any of its children +- the array is clustered so children sharing the same parent are together + +This form is useful primarily for calculating absolute object transformations, +for example: + +@snippet MagnumSceneTools.cpp orderClusterParents-transformations + +The operation is done in an @f$ \mathcal{O}(n) @f$ execution time and memory +complexity, with @f$ n @f$ being @ref Trade::SceneData::mappingBound(). The +@ref Trade::SceneField::Parent field is expected to be contained in the scene, +having no cycles (i.e., every node listed just once) and not being sparse +(i.e., every node listed in the field reachable from the root). +@see @ref Trade::SceneData::hasField() +*/ +MAGNUM_SCENETOOLS_EXPORT Containers::Array> orderClusterParents(const Trade::SceneData& scene); + +/** +@brief Calculate ordered and clustered parents into a pre-allocated view +@m_since_latest + +Like @ref orderClusterParents(), but puts the result into +@p mappingDestination and @p parentDestination instead of allocating a new +array. Expect that both views have a size equal to size of the +@ref Trade::SceneField::Parent view in @p scene. +@see @ref Trade::SceneData::fieldSize(SceneField) const +*/ +MAGNUM_SCENETOOLS_EXPORT void orderClusterParentsInto(const Trade::SceneData& scene, const Containers::StridedArrayView1D& mappingDestination, const Containers::StridedArrayView1D& parentDestination); + +}} + +#endif diff --git a/src/Magnum/SceneTools/Test/CMakeLists.txt b/src/Magnum/SceneTools/Test/CMakeLists.txt index 319ecdae3..89536b95c 100644 --- a/src/Magnum/SceneTools/Test/CMakeLists.txt +++ b/src/Magnum/SceneTools/Test/CMakeLists.txt @@ -25,8 +25,10 @@ corrade_add_test(SceneToolsCombineTest CombineTest.cpp LIBRARIES MagnumTrade) corrade_add_test(SceneToolsConvertToSingleFun___Test ConvertToSingleFunctionObjectsTest.cpp LIBRARIES MagnumTrade) +corrade_add_test(SceneToolsOrderClusterParentsTest OrderClusterParentsTest.cpp LIBRARIES MagnumSceneToolsTestLib) set_target_properties( SceneToolsCombineTest SceneToolsConvertToSingleFun___Test + SceneToolsOrderClusterParentsTest PROPERTIES FOLDER "Magnum/SceneTools/Test") diff --git a/src/Magnum/SceneTools/Test/OrderClusterParentsTest.cpp b/src/Magnum/SceneTools/Test/OrderClusterParentsTest.cpp new file mode 100644 index 000000000..c08f1cc08 --- /dev/null +++ b/src/Magnum/SceneTools/Test/OrderClusterParentsTest.cpp @@ -0,0 +1,324 @@ +/* + 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 +#include + +#include "Magnum/SceneTools/OrderClusterParents.h" +#include "Magnum/Trade/SceneData.h" + +namespace Magnum { namespace SceneTools { namespace Test { namespace { + +struct OrderClusterParentsTest: TestSuite::Tester { + explicit OrderClusterParentsTest(); + + void test(); + void noParentField(); + void emptyParentField(); + + void intoNoParentField(); + void intoEmptyParentField(); + void intoWrongDestinationSize(); + + void sparse(); + void cyclic(); + void cyclicDeep(); + void sparseAndCyclic(); +}; + +OrderClusterParentsTest::OrderClusterParentsTest() { + addTests({&OrderClusterParentsTest::test, + &OrderClusterParentsTest::noParentField, + &OrderClusterParentsTest::emptyParentField, + + &OrderClusterParentsTest::intoNoParentField, + &OrderClusterParentsTest::intoEmptyParentField, + &OrderClusterParentsTest::intoWrongDestinationSize, + + &OrderClusterParentsTest::sparse, + &OrderClusterParentsTest::cyclic, + &OrderClusterParentsTest::cyclicDeep, + &OrderClusterParentsTest::sparseAndCyclic}); +} + +void OrderClusterParentsTest::test() { + struct Field { + /* To verify we don't have unnecessarily hardcoded 32-bit types */ + UnsignedShort object; + Byte parent; + } data[]{ + /* Backward parent reference */ + {5, 1}, + /* Forward parent reference */ + {6, 9}, + /* Root elements */ + {3, -1}, + {1, -1}, + /* Deep hierarchy */ + {9, 10}, + {10, 3}, + /* Multiple children */ + {7, 3}, + {157, 3}, + {143, 6}, + /* More root elements */ + {2, -1} + /* Elements 0, 4, 8, 11-142, 144-156 deliberately not used */ + }; + Containers::StridedArrayView1D view = data; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedShort, 158, {}, data, { + /* To verify it doesn't just pick the first field ever */ + Trade::SceneFieldData{Trade::SceneField::Mesh, Trade::SceneMappingType::UnsignedShort, nullptr, Trade::SceneFieldType::UnsignedInt, nullptr}, + Trade::SceneFieldData{Trade::SceneField::Parent, view.slice(&Field::object), view.slice(&Field::parent)} + }}; + + CORRADE_COMPARE_AS(orderClusterParents(scene), (Containers::arrayView>({ + /* Root objects first, in order as found */ + {3, -1}, + {1, -1}, + {2, -1}, + /* Then children of node 3, clustered together, in order as found */ + {10, 3}, + {7, 3}, + {157, 3}, + /* Then children of node 1 */ + {5, 1}, + /* Children of node 10 */ + {9, 10}, + /* Children of node 9 */ + {6, 9}, + /* Children of node 6 */ + {143, 6}, + })), TestSuite::Compare::Container); +} + +void OrderClusterParentsTest::noParentField() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedByte, 0, nullptr, {}}; + + std::ostringstream out; + Error redirectError{&out}; + orderClusterParents(scene); + CORRADE_COMPARE(out.str(), + "SceneTools::orderClusterParents(): the scene has no hierarchy\n"); +} + +void OrderClusterParentsTest::emptyParentField() { + Trade::SceneData scene{Trade::SceneMappingType::UnsignedInt, 0, nullptr, { + Trade::SceneFieldData{Trade::SceneField::Parent, Trade::SceneMappingType::UnsignedInt, nullptr, Trade::SceneFieldType::Int, nullptr} + }}; + + CORRADE_COMPARE_AS(orderClusterParents(scene), + (Containers::ArrayView>{}), + TestSuite::Compare::Container); +} + +void OrderClusterParentsTest::intoNoParentField() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedByte, 0, nullptr, {}}; + + std::ostringstream out; + Error redirectError{&out}; + orderClusterParentsInto(scene, nullptr, nullptr); + CORRADE_COMPARE(out.str(), + "SceneTools::orderClusterParentsInto(): the scene has no hierarchy\n"); +} + +void OrderClusterParentsTest::intoEmptyParentField() { + Trade::SceneData scene{Trade::SceneMappingType::UnsignedInt, 0, nullptr, { + Trade::SceneFieldData{Trade::SceneField::Parent, Trade::SceneMappingType::UnsignedInt, nullptr, Trade::SceneFieldType::Int, nullptr} + }}; + + orderClusterParentsInto(scene, nullptr, nullptr); + CORRADE_VERIFY(true); +} + +void OrderClusterParentsTest::intoWrongDestinationSize() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct Field { + UnsignedInt object; + Int parent; + } data[]{ + {2, -1}, + {3, 2}, + {7, -1} + }; + Containers::StridedArrayView1D view = data; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedInt, 8, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Parent, view.slice(&Field::object), view.slice(&Field::parent)} + }}; + + UnsignedInt mappingCorrect[3]; + UnsignedInt mapping[2]; + Int parentOffsetCorrect[3]; + Int parentOffset[2]; + + std::ostringstream out; + Error redirectError{&out}; + orderClusterParentsInto(scene, mappingCorrect, parentOffset); + orderClusterParentsInto(scene, mapping, parentOffsetCorrect); + CORRADE_COMPARE(out.str(), + "SceneTools::orderClusterParentsInto(): expected parent destination view with 3 elements but got 2\n" + "SceneTools::orderClusterParentsInto(): expected mapping destination view with 3 elements but got 2\n"); +} + +void OrderClusterParentsTest::sparse() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct Field { + UnsignedInt object; + Int parent; + } data[]{ + {2, -1}, + {3, 2}, + {7, -1}, + /* Not reachable from root */ + {15, 6}, + {14, 6}, + {11, 15}, + }; + Containers::StridedArrayView1D view = data; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedInt, 16, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Parent, view.slice(&Field::object), view.slice(&Field::parent)} + }}; + + std::ostringstream out; + Error redirectError{&out}; + orderClusterParents(scene); + CORRADE_COMPARE(out.str(), + "SceneTools::orderClusterParents(): hierarchy is sparse\n"); +} + +void OrderClusterParentsTest::cyclic() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct Field { + UnsignedInt object; + Int parent; + } data[]{ + {2, -1}, + {3, 2}, + {7, -1}, + /* Cycle of length 1, which will be treated as sparse hierarchy */ + {13, 13} + }; + Containers::StridedArrayView1D view = data; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedInt, 16, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Parent, view.slice(&Field::object), view.slice(&Field::parent)} + }}; + + std::ostringstream out; + Error redirectError{&out}; + orderClusterParents(scene); + CORRADE_COMPARE(out.str(), + "SceneTools::orderClusterParents(): hierarchy is sparse\n"); +} + +void OrderClusterParentsTest::cyclicDeep() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct Field { + UnsignedInt object; + Int parent; + } data[]{ + {2, -1}, + {3, 2}, + {7, -1}, + /* Cycle of length 3 */ + {13, -1}, + {5, 13}, + {13, 3} + }; + Containers::StridedArrayView1D view = data; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedInt, 16, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Parent, view.slice(&Field::object), view.slice(&Field::parent)} + }}; + + std::ostringstream out; + Error redirectError{&out}; + orderClusterParents(scene); + CORRADE_COMPARE(out.str(), + "SceneTools::orderClusterParents(): hierarchy is cyclic\n"); +} + +void OrderClusterParentsTest::sparseAndCyclic() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct Field { + UnsignedInt object; + Int parent; + } data[]{ + {2, -1}, + {3, 2}, + {7, -1}, + /* Cycle of length 3 */ + {13, -1}, + {5, 13}, + {13, 3}, + /* Not reachable from root */ + {15, 6} + }; + Containers::StridedArrayView1D view = data; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedInt, 16, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Parent, view.slice(&Field::object), view.slice(&Field::parent)} + }}; + + std::ostringstream out; + Error redirectError{&out}; + orderClusterParents(scene); + CORRADE_EXPECT_FAIL("The implementation needs to track already visited objects with a BitArray to detect this, it'd also provide a much better diagnostic."); + CORRADE_COMPARE(out.str(), + "SceneTools::orderClusterParents(): hierarchy is cyclic\n"); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::SceneTools::Test::OrderClusterParentsTest)