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 {