From 68ec362b37e2bd896a412f80c404f910ff5e5326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 6 May 2023 19:47:26 +0200 Subject: [PATCH] SceneTools: add childrenDepthFirst(). A somewhat inverse / complementary utility for parentsBreadthFirst() -- while the former is useful mainly for convenient parent referencing, this is for children and nested children. Currently the main use case is extracting scene subtrees, which is also what the example snippet shows. Getting a list of direct children is also possible, although for that it's possible to use the parentsBreadthFirst() as well as the Parent field directly, simply by scanning for all field entries with given value. --- doc/snippets/MagnumSceneTools.cpp | 33 ++++ src/Magnum/SceneTools/Filter.h | 1 + src/Magnum/SceneTools/Hierarchy.cpp | 120 ++++++++++++++ src/Magnum/SceneTools/Hierarchy.h | 58 ++++++- src/Magnum/SceneTools/Test/HierarchyTest.cpp | 157 ++++++++++++++----- 5 files changed, 329 insertions(+), 40 deletions(-) diff --git a/doc/snippets/MagnumSceneTools.cpp b/doc/snippets/MagnumSceneTools.cpp index 9369efc94..15d09f6bd 100644 --- a/doc/snippets/MagnumSceneTools.cpp +++ b/doc/snippets/MagnumSceneTools.cpp @@ -100,6 +100,39 @@ Trade::MeshData concatenated = MeshTools::concatenate(flattenedMeshes); /* [absoluteFieldTransformations3D-mesh-concatenate] */ } +{ +/* [childrenDepthFirst-extract-tree] */ +Trade::SceneData scene = DOXYGEN_ELLIPSIS(Trade::SceneData{{}, 0, nullptr, {}}); + +Containers::Array> childrenRanges = + SceneTools::childrenDepthFirst(scene); + +/* Bit array of objects to keep from the scene */ +UnsignedInt objectToLookFor = DOXYGEN_ELLIPSIS(0); +Containers::BitArray objectsToKeep{ValueInit, std::size_t(scene.mappingBound())}; + +/* Look for the object in the list */ +for(const Containers::Pair& i: childrenRanges) { + if(i.first() != objectToLookFor) continue; + + /* Right after the object appearing in the list is all its (nested) + children, mark them in the bit array */ + for(const Containers::Pair& j: + childrenRanges.sliceSize(&i, i.second() + 1)) + objectsToKeep.set(j.first()); + + break; +} + +/* Filter the scene to contain just given object and its children, and reparent + it to be in scene root */ +Trade::SceneData filtered = SceneTools::filterObjects(scene, objectsToKeep); +filtered.mutableField(Trade::SceneField::Parent)[ + filtered.fieldObjectOffset(Trade::SceneField::Parent, objectToLookFor) +] = -1; +/* [childrenDepthFirst-extract-tree] */ +} + { /* [parentsBreadthFirst-transformations] */ Trade::SceneData scene = DOXYGEN_ELLIPSIS(Trade::SceneData{{}, 0, nullptr, {}}); diff --git a/src/Magnum/SceneTools/Filter.h b/src/Magnum/SceneTools/Filter.h index 7d064e938..f0ecaea40 100644 --- a/src/Magnum/SceneTools/Filter.h +++ b/src/Magnum/SceneTools/Filter.h @@ -177,6 +177,7 @@ other fields such as @ref Trade::SceneField::Parent, it's the responsibility of the caller to deal with them either before or after calling this API, otherwise the returned data may end up being unusable. @experimental +@see @ref childrenDepthFirst() */ MAGNUM_SCENETOOLS_EXPORT Trade::SceneData filterObjects(const Trade::SceneData& scene, Containers::BitArrayView objectsToKeep); diff --git a/src/Magnum/SceneTools/Hierarchy.cpp b/src/Magnum/SceneTools/Hierarchy.cpp index 5ab96c3db..2cf0886d4 100644 --- a/src/Magnum/SceneTools/Hierarchy.cpp +++ b/src/Magnum/SceneTools/Hierarchy.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include "Magnum/DimensionTraits.h" #include "Magnum/Math/Matrix3.h" @@ -137,6 +138,125 @@ void parentsBreadthFirstInto(const Trade::SceneData& scene, const Containers::St "SceneTools::parentsBreadthFirst(): hierarchy is sparse", ); } +Containers::Array> childrenDepthFirst(const Trade::SceneData& scene) { + const Containers::Optional parentFieldId = scene.findFieldId(Trade::SceneField::Parent); + CORRADE_ASSERT(parentFieldId, + "SceneTools::childrenDepthFirst(): the scene has no hierarchy", {}); + Containers::Array> out{NoInit, scene.fieldSize(*parentFieldId)}; + childrenDepthFirstInto(scene, + stridedArrayView(out).slice(&decltype(out)::Type::first), + stridedArrayView(out).slice(&decltype(out)::Type::second)); + return out; +} + +void childrenDepthFirstInto(const Trade::SceneData& scene, const Containers::StridedArrayView1D& mappingDestination, const Containers::StridedArrayView1D& childCountDestination) { + const Containers::Optional parentFieldId = scene.findFieldId(Trade::SceneField::Parent); + CORRADE_ASSERT(parentFieldId, + "SceneTools::childrenDepthFirstInto(): the scene has no hierarchy", ); + const std::size_t parentFieldSize = scene.fieldSize(*parentFieldId); + CORRADE_ASSERT(mappingDestination.size() == parentFieldSize, + "SceneTools::childrenDepthFirstInto(): expected mapping destination view with" << parentFieldSize << "elements but got" << mappingDestination.size(), ); + CORRADE_ASSERT(childCountDestination.size() == parentFieldSize, + "SceneTools::childrenDepthFirstInto(): expected child count destination view with" << parentFieldSize << "elements but got" << childCountDestination.size(), ); + + /* Allocate a single storage for all temporary data */ + Containers::ArrayView> parents; + Containers::ArrayView childrenOffsets; + Containers::ArrayView children; + /* Parent ID, offset of the first child in `childCountDestination`, offset + of next child in `children` to process */ + Containers::ArrayView> parentsToProcess; + Containers::ArrayTuple storage{ + /* Output of scene.parentsInto() */ + {NoInit, parentFieldSize, parents}, + /* Running children offset (+1) for each node including root (+1), plus + one more element when we shift the array by one below */ + {ValueInit, std::size_t(scene.mappingBound() + 3), childrenOffsets}, + {NoInit, parentFieldSize, children}, + /* A stack of parents to process. It only reaches `parentFieldSize + 1` + if the hierarchy is a single branch, usually it's shorter. */ + {NoInit, parentFieldSize + 1, parentsToProcess} + }; + + /* Convert the parent list to a child list to sort them toplogically */ + scene.parentsInto( + stridedArrayView(parents).slice(&decltype(parents)::Type::first), + stridedArrayView(parents).slice(&decltype(parents)::Type::second) + ); + + /* Children offset for each node including root. First calculate the count + of children for each, skipping the first element (parent.second() can be + -1, accounting for that as well)... */ + for(const Containers::Pair& parent: parents) { + CORRADE_INTERNAL_ASSERT(parent.first() < scene.mappingBound() && (parent.second() == -1 || UnsignedInt(parent.second()) < scene.mappingBound())); + ++childrenOffsets[parent.second() + 2]; + } + + /* ... then convert the counts to a running offset. Now + `[childrenOffsets[i + 2], childrenOffsets[i + 3])` 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. The + childrenOffsets array gets shifted by one element by the process, thus + now `[childrenOffsets[i + 1], childrenOffsets[i + 2])` contains a range + in which the `children` array below contains a list of children for + `i`. */ + for(const Containers::Pair& parent: parents) + children[childrenOffsets[parent.second() + 2]++] = parent.first(); + + UnsignedInt outputOffset = 0; + std::size_t parentsToProcessOffset = 0; + parentsToProcess[parentsToProcessOffset++] = {-1, outputOffset, childrenOffsets[-1 + 1]}; + while(parentsToProcessOffset) { + const Int objectId = parentsToProcess[parentsToProcessOffset - 1].first(); + UnsignedInt& childrenOffset = parentsToProcess[parentsToProcessOffset - 1].third(); + + /* If all children were processed, we're done with this object */ + if(childrenOffset == childrenOffsets[objectId + 2]) { + /* Save the total size. Only if it's not the root objects, for them + the total size is implicitly the whole output size. */ + if(objectId != -1) { + const UnsignedInt firstChildOutputOffset = parentsToProcess[parentsToProcessOffset - 1].second(); + childCountDestination[firstChildOutputOffset - 1] = outputOffset - firstChildOutputOffset; + } + + /* Remove from the processing stack and continue with next */ + --parentsToProcessOffset; + continue; + } + + CORRADE_INTERNAL_DEBUG_ASSERT(childrenOffset < childrenOffsets[objectId + 2]); + CORRADE_INTERNAL_DEBUG_ASSERT(parentsToProcessOffset < parentFieldSize + 1); + /** @todo better diagnostic with BitArray to detect which nodes are + parented more than once (OTOH maybe that's undesirable extra work + that would be duplicated here and in parentsBreadthFirst()?) */ + CORRADE_ASSERT(outputOffset < parents.size(), + "SceneTools::childrenDepthFirst(): hierarchy is cyclic", ); + + /* Add the current child to the mapping output and to the list of + parents to process next. Increment all offsets for the next + round. */ + const UnsignedInt childObjectId = children[childrenOffset++]; + mappingDestination[outputOffset] = childObjectId; + parentsToProcess[parentsToProcessOffset++] = {Int(childObjectId), ++outputOffset, childrenOffsets[childObjectId + 1]}; + } + + CORRADE_INTERNAL_ASSERT(parentsToProcessOffset == 0); + + /** @todo better diagnostic with BitArray to detect which nodes are + unreachable from root (OTOH again maybe that's undesirable extra work + that would be duplicated here and in parentsBreadthFirst()?) */ + CORRADE_ASSERT(outputOffset == parents.size(), + "SceneTools::childrenDepthFirst(): hierarchy is sparse", ); +} + namespace { template struct SceneDataDimensionTraits; diff --git a/src/Magnum/SceneTools/Hierarchy.h b/src/Magnum/SceneTools/Hierarchy.h index e87930bff..ddd1b33ee 100644 --- a/src/Magnum/SceneTools/Hierarchy.h +++ b/src/Magnum/SceneTools/Hierarchy.h @@ -26,7 +26,7 @@ */ /** @file - * @brief Function @ref Magnum::SceneTools::parentsBreadthFirst(), @ref Magnum::SceneTools::parentsBreadthFirstInto(), @ref Magnum::SceneTools::absoluteFieldTransformations2D(), @ref Magnum::SceneTools::absoluteFieldTransformations2DInto(), @ref Magnum::SceneTools::absoluteFieldTransformations3D(), @ref Magnum::SceneTools::absoluteFieldTransformations3DInto() + * @brief Function @ref Magnum::SceneTools::parentsBreadthFirst(), @ref Magnum::SceneTools::parentsBreadthFirstInto(), @ref Magnum::SceneTools::childrenDepthFirst(), @ref Magnum::SceneTools::childrenDepthFirstInto(), @ref Magnum::SceneTools::absoluteFieldTransformations2D(), @ref Magnum::SceneTools::absoluteFieldTransformations2DInto(), @ref Magnum::SceneTools::absoluteFieldTransformations3D(), @ref Magnum::SceneTools::absoluteFieldTransformations3DInto() * @m_since_latest */ @@ -59,7 +59,7 @@ having no cycles (i.e., every node listed just once) and not being sparse @experimental -@see @ref Trade::SceneData::hasField() +@see @ref Trade::SceneData::hasField(), @ref childrenDepthFirst() */ MAGNUM_SCENETOOLS_EXPORT Containers::Array> parentsBreadthFirst(const Trade::SceneData& scene); @@ -74,10 +74,62 @@ views have a size equal to size of the @ref Trade::SceneField::Parent view in @experimental -@see @ref Trade::SceneData::fieldSize(SceneField) const +@see @ref Trade::SceneData::fieldSize(SceneField) const, + @ref childrenDepthFirstInto() */ MAGNUM_SCENETOOLS_EXPORT void parentsBreadthFirstInto(const Trade::SceneData& scene, const Containers::StridedArrayView1D& mappingDestination, const Containers::StridedArrayView1D& parentDestination); +/** +@brief Retrieve children in a depth-first order +@m_since_latest + +Extracts the @ref Trade::SceneField::Parent field mapping and data from +@p scene and converts it to a list of (object index, children count) pairs +such that: + +- children of given object are directly following the object itself +- the count includes direct children as well as nested children, next object + in the same level, if exists, is thus after another `count` items + +Objects in particular level keep the same order as they had in the +@ref Trade::SceneField::Parent field. Size of the returned list is equal to the +@ref Trade::SceneField::Parent field size. Implicitly, the whole returned list +contains (nested) children of the root, which implies that the first returned +object is the first top-level object (i.e., with a parent equal to +@cpp -1 @ce), and size of the list is the count of all objects. + +This form is useful primarily for marking and extracting subtrees, for example: + +@snippet MagnumSceneTools.cpp childrenDepthFirst-extract-tree + +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). + +@experimental + +@see @ref Trade::SceneData::hasField(), @ref parentsBreadthFirst() +*/ +MAGNUM_SCENETOOLS_EXPORT Containers::Array> childrenDepthFirst(const Trade::SceneData& scene); + +/** +@brief Retrieve childrem in a depth-first order into a pre-allocated view +@m_since_latest + +Like @ref childrenDepthFirst(), but puts the result into @p mappingDestination +and @p childCountDestination 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. + +@experimental + +@see @ref Trade::SceneData::fieldSize(SceneField) const, + @ref parentsBreadthFirstInto() +*/ +MAGNUM_SCENETOOLS_EXPORT void childrenDepthFirstInto(const Trade::SceneData& scene, const Containers::StridedArrayView1D& mappingDestination, const Containers::StridedArrayView1D& childCountDestination); + /** @brief Calculate absolute 2D transformations for given field @m_since_latest diff --git a/src/Magnum/SceneTools/Test/HierarchyTest.cpp b/src/Magnum/SceneTools/Test/HierarchyTest.cpp index 7d56addd5..7e38bc127 100644 --- a/src/Magnum/SceneTools/Test/HierarchyTest.cpp +++ b/src/Magnum/SceneTools/Test/HierarchyTest.cpp @@ -40,18 +40,19 @@ namespace Magnum { namespace SceneTools { namespace Test { namespace { struct HierarchyTest: TestSuite::Tester { explicit HierarchyTest(); - void parentsBreadthFirst(); - void parentsBreadthFirstNoParentField(); - void parentsBreadthFirstEmptyParentField(); + void parentsBreadthFirstChildrenDepthFirst(); + void parentsBreadthFirstChildrenDepthFirstSingleBranch(); + void parentsBreadthFirstChildrenDepthFirstNoParentField(); + void parentsBreadthFirstChildrenDepthFirstEmptyParentField(); - void parentsBreadthFirstIntoNoParentField(); - void parentsBreadthFirstIntoEmptyParentField(); - void parentsBreadthFirstIntoWrongDestinationSize(); + void parentsBreadthFirstChildrenDepthFirstIntoNoParentField(); + void parentsBreadthFirstChildrenDepthFirstIntoEmptyParentField(); + void parentsBreadthFirstChildrenDepthFirstIntoWrongDestinationSize(); - void parentsBreadthFirstSparse(); - void parentsBreadthFirstCyclic(); - void parentsBreadthFirstCyclicDeep(); - void parentsBreadthFirstSparseAndCyclic(); + void parentsBreadthFirstChildrenDepthFirstSparse(); + void parentsBreadthFirstChildrenDepthFirstCyclic(); + void parentsBreadthFirstChildrenDepthFirstCyclicDeep(); + void parentsBreadthFirstChildrenDepthFirstSparseAndCyclic(); void absoluteFieldTransformations2D(); void absoluteFieldTransformations3D(); @@ -117,18 +118,19 @@ const struct { }; HierarchyTest::HierarchyTest() { - addTests({&HierarchyTest::parentsBreadthFirst, - &HierarchyTest::parentsBreadthFirstNoParentField, - &HierarchyTest::parentsBreadthFirstEmptyParentField, + addTests({&HierarchyTest::parentsBreadthFirstChildrenDepthFirst, + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstSingleBranch, + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstNoParentField, + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstEmptyParentField, - &HierarchyTest::parentsBreadthFirstIntoNoParentField, - &HierarchyTest::parentsBreadthFirstIntoEmptyParentField, - &HierarchyTest::parentsBreadthFirstIntoWrongDestinationSize, + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstIntoNoParentField, + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstIntoEmptyParentField, + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstIntoWrongDestinationSize, - &HierarchyTest::parentsBreadthFirstSparse, - &HierarchyTest::parentsBreadthFirstCyclic, - &HierarchyTest::parentsBreadthFirstCyclicDeep, - &HierarchyTest::parentsBreadthFirstSparseAndCyclic}); + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstSparse, + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstCyclic, + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstCyclicDeep, + &HierarchyTest::parentsBreadthFirstChildrenDepthFirstSparseAndCyclic}); addInstancedTests({&HierarchyTest::absoluteFieldTransformations2D, &HierarchyTest::absoluteFieldTransformations3D}, @@ -145,7 +147,7 @@ HierarchyTest::HierarchyTest() { addTests({&HierarchyTest::absoluteFieldTransformationsIntoInvalidSize}); } -void HierarchyTest::parentsBreadthFirst() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirst() { struct Field { /* To verify we don't have unnecessarily hardcoded 32-bit types */ UnsignedShort mapping; @@ -199,9 +201,68 @@ void HierarchyTest::parentsBreadthFirst() { /* Children of node 6 */ {143, 6}, })), TestSuite::Compare::Container); + + CORRADE_COMPARE_AS(SceneTools::childrenDepthFirst(scene), (Containers::arrayView>({ + /* Node 3, root */ + {3, 6}, + /* Children of node 3, in order as found */ + {10, 3}, + /* Children of node 10 */ + {9, 2}, + /* Children of node 9 */ + {6, 1}, + /* Children of node 6 */ + {143, 0}, + {7, 0}, + {157, 0}, + /* Node 1, root */ + {1, 1}, + /* Children of node 1 */ + {5, 0}, + /* Node 2, root */ + {2, 0}, + })), TestSuite::Compare::Container); +} + +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstSingleBranch() { + /* Verifies just that the internal allocation routines are correctly sized, + as this should lead to the longest stack in childrenDepthFirst(). + Shouldn't trigger anything special in parentsBreadthFirst() but testing + that one as well to be sure. */ + + struct Field { + UnsignedLong mapping; + Long parent; + } data[]{ + {2, 1}, + {1, 0}, + {3, 2}, + {0, -1}, + }; + Containers::StridedArrayView1D view = data; + + Trade::SceneData scene{Trade::SceneMappingType::UnsignedLong, 4, {}, data, { + Trade::SceneFieldData{Trade::SceneField::Parent, + view.slice(&Field::mapping), + view.slice(&Field::parent)} + }}; + + CORRADE_COMPARE_AS(SceneTools::parentsBreadthFirst(scene), (Containers::arrayView>({ + {0, -1}, + {1, 0}, + {2, 1}, + {3, 2}, + })), TestSuite::Compare::Container); + + CORRADE_COMPARE_AS(SceneTools::childrenDepthFirst(scene), (Containers::arrayView>({ + {0, 3}, + {1, 2}, + {2, 1}, + {3, 0}, + })), TestSuite::Compare::Container); } -void HierarchyTest::parentsBreadthFirstNoParentField() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstNoParentField() { CORRADE_SKIP_IF_NO_ASSERT(); Trade::SceneData scene{Trade::SceneMappingType::UnsignedByte, 0, nullptr, {}}; @@ -209,11 +270,13 @@ void HierarchyTest::parentsBreadthFirstNoParentField() { std::ostringstream out; Error redirectError{&out}; SceneTools::parentsBreadthFirst(scene); + SceneTools::childrenDepthFirst(scene); CORRADE_COMPARE(out.str(), - "SceneTools::parentsBreadthFirst(): the scene has no hierarchy\n"); + "SceneTools::parentsBreadthFirst(): the scene has no hierarchy\n" + "SceneTools::childrenDepthFirst(): the scene has no hierarchy\n"); } -void HierarchyTest::parentsBreadthFirstEmptyParentField() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstEmptyParentField() { Trade::SceneData scene{Trade::SceneMappingType::UnsignedInt, 0, nullptr, { Trade::SceneFieldData{Trade::SceneField::Parent, Trade::SceneMappingType::UnsignedInt, nullptr, Trade::SceneFieldType::Int, nullptr} }}; @@ -221,9 +284,12 @@ void HierarchyTest::parentsBreadthFirstEmptyParentField() { CORRADE_COMPARE_AS(SceneTools::parentsBreadthFirst(scene), (Containers::ArrayView>{}), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(SceneTools::childrenDepthFirst(scene), + (Containers::ArrayView>{}), + TestSuite::Compare::Container); } -void HierarchyTest::parentsBreadthFirstIntoNoParentField() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstIntoNoParentField() { CORRADE_SKIP_IF_NO_ASSERT(); Trade::SceneData scene{Trade::SceneMappingType::UnsignedByte, 0, nullptr, {}}; @@ -231,20 +297,23 @@ void HierarchyTest::parentsBreadthFirstIntoNoParentField() { std::ostringstream out; Error redirectError{&out}; parentsBreadthFirstInto(scene, nullptr, nullptr); + childrenDepthFirstInto(scene, nullptr, nullptr); CORRADE_COMPARE(out.str(), - "SceneTools::parentsBreadthFirstInto(): the scene has no hierarchy\n"); + "SceneTools::parentsBreadthFirstInto(): the scene has no hierarchy\n" + "SceneTools::childrenDepthFirstInto(): the scene has no hierarchy\n"); } -void HierarchyTest::parentsBreadthFirstIntoEmptyParentField() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstIntoEmptyParentField() { Trade::SceneData scene{Trade::SceneMappingType::UnsignedInt, 0, nullptr, { Trade::SceneFieldData{Trade::SceneField::Parent, Trade::SceneMappingType::UnsignedInt, nullptr, Trade::SceneFieldType::Int, nullptr} }}; parentsBreadthFirstInto(scene, nullptr, nullptr); + childrenDepthFirstInto(scene, nullptr, nullptr); CORRADE_VERIFY(true); } -void HierarchyTest::parentsBreadthFirstIntoWrongDestinationSize() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstIntoWrongDestinationSize() { CORRADE_SKIP_IF_NO_ASSERT(); struct Field { @@ -267,17 +336,23 @@ void HierarchyTest::parentsBreadthFirstIntoWrongDestinationSize() { UnsignedInt mapping[2]; Int parentOffsetCorrect[3]; Int parentOffset[2]; + UnsignedInt childCountCorrect[3]; + UnsignedInt childCount[2]; std::ostringstream out; Error redirectError{&out}; parentsBreadthFirstInto(scene, mappingCorrect, parentOffset); parentsBreadthFirstInto(scene, mapping, parentOffsetCorrect); + childrenDepthFirstInto(scene, mappingCorrect, childCount); + childrenDepthFirstInto(scene, mapping, childCountCorrect); CORRADE_COMPARE(out.str(), "SceneTools::parentsBreadthFirstInto(): expected parent destination view with 3 elements but got 2\n" - "SceneTools::parentsBreadthFirstInto(): expected mapping destination view with 3 elements but got 2\n"); + "SceneTools::parentsBreadthFirstInto(): expected mapping destination view with 3 elements but got 2\n" + "SceneTools::childrenDepthFirstInto(): expected child count destination view with 3 elements but got 2\n" + "SceneTools::childrenDepthFirstInto(): expected mapping destination view with 3 elements but got 2\n"); } -void HierarchyTest::parentsBreadthFirstSparse() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstSparse() { CORRADE_SKIP_IF_NO_ASSERT(); struct Field { @@ -303,11 +378,13 @@ void HierarchyTest::parentsBreadthFirstSparse() { std::ostringstream out; Error redirectError{&out}; SceneTools::parentsBreadthFirst(scene); + SceneTools::childrenDepthFirst(scene); CORRADE_COMPARE(out.str(), - "SceneTools::parentsBreadthFirst(): hierarchy is sparse\n"); + "SceneTools::parentsBreadthFirst(): hierarchy is sparse\n" + "SceneTools::childrenDepthFirst(): hierarchy is sparse\n"); } -void HierarchyTest::parentsBreadthFirstCyclic() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstCyclic() { CORRADE_SKIP_IF_NO_ASSERT(); struct Field { @@ -331,11 +408,13 @@ void HierarchyTest::parentsBreadthFirstCyclic() { std::ostringstream out; Error redirectError{&out}; SceneTools::parentsBreadthFirst(scene); + SceneTools::childrenDepthFirst(scene); CORRADE_COMPARE(out.str(), - "SceneTools::parentsBreadthFirst(): hierarchy is sparse\n"); + "SceneTools::parentsBreadthFirst(): hierarchy is sparse\n" + "SceneTools::childrenDepthFirst(): hierarchy is sparse\n"); } -void HierarchyTest::parentsBreadthFirstCyclicDeep() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstCyclicDeep() { CORRADE_SKIP_IF_NO_ASSERT(); struct Field { @@ -361,11 +440,13 @@ void HierarchyTest::parentsBreadthFirstCyclicDeep() { std::ostringstream out; Error redirectError{&out}; SceneTools::parentsBreadthFirst(scene); + SceneTools::childrenDepthFirst(scene); CORRADE_COMPARE(out.str(), - "SceneTools::parentsBreadthFirst(): hierarchy is cyclic\n"); + "SceneTools::parentsBreadthFirst(): hierarchy is cyclic\n" + "SceneTools::childrenDepthFirst(): hierarchy is cyclic\n"); } -void HierarchyTest::parentsBreadthFirstSparseAndCyclic() { +void HierarchyTest::parentsBreadthFirstChildrenDepthFirstSparseAndCyclic() { CORRADE_SKIP_IF_NO_ASSERT(); struct Field { @@ -393,9 +474,11 @@ void HierarchyTest::parentsBreadthFirstSparseAndCyclic() { std::ostringstream out; Error redirectError{&out}; SceneTools::parentsBreadthFirst(scene); + SceneTools::childrenDepthFirst(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::parentsBreadthFirst(): hierarchy is cyclic\n"); + "SceneTools::parentsBreadthFirst(): hierarchy is cyclic\n" + "SceneTools::childrenDepthFirst(): hierarchy is cyclic\n"); } const struct Scene {