Revision control
++ // Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtTest/QTest>
++ #include <Qt3DAnimation/private/animationclip_p.h>
++ #include <Qt3DAnimation/private/animationutils_p.h>
++ #include <Qt3DAnimation/private/blendedclipanimator_p.h>
++ #include <Qt3DAnimation/private/clock_p.h>
++ #include <Qt3DAnimation/private/channelmapper_p.h>
++ #include <Qt3DAnimation/private/channelmapping_p.h>
++ #include <Qt3DAnimation/private/clipblendvalue_p.h>
++ #include <Qt3DAnimation/private/handler_p.h>
++ #include <Qt3DAnimation/private/additiveclipblend_p.h>
++ #include <Qt3DAnimation/private/lerpclipblend_p.h>
++ #include <Qt3DAnimation/private/managers_p.h>
++ #include <QtGui/qvector2d.h>
++ #include <QtGui/qvector3d.h>
++ #include <QtGui/qvector4d.h>
++ #include <QtGui/qquaternion.h>
++ #include <QtGui/qcolor.h>
++ #include <QtCore/qbitarray.h>
++
++ #include <qbackendnodetester.h>
++ #include <testarbiter.h>
++
++ using namespace Qt3DAnimation::Animation;
++
++ Q_DECLARE_METATYPE(Qt3DAnimation::Animation::Handler*)
++ Q_DECLARE_METATYPE(QVector<ChannelMapping *>)
++ Q_DECLARE_METATYPE(Clock *)
++ Q_DECLARE_METATYPE(ChannelMapper *)
++ Q_DECLARE_METATYPE(AnimationClip *)
++ Q_DECLARE_METATYPE(QVector<MappingData>)
++ Q_DECLARE_METATYPE(Channel)
++ Q_DECLARE_METATYPE(AnimatorEvaluationData)
++ Q_DECLARE_METATYPE(ClipEvaluationData)
++ Q_DECLARE_METATYPE(ClipAnimator *)
++ Q_DECLARE_METATYPE(BlendedClipAnimator *)
++ Q_DECLARE_METATYPE(QVector<ChannelNameAndType>)
++ Q_DECLARE_METATYPE(QVector<AnimationCallbackAndValue>)
++ Q_DECLARE_METATYPE(ClipFormat)
++ Q_DECLARE_METATYPE(ChannelNameAndType)
++
++ namespace {
++
++ class MeanBlendNode : public ClipBlendNode
++ {
++ public:
++ MeanBlendNode()
++ : ClipBlendNode(ClipBlendNode::LerpBlendType)
++ {}
++
++ void setValueNodeIds(Qt3DCore::QNodeId value1Id,
++ Qt3DCore::QNodeId value2Id)
++ {
++ m_value1Id = value1Id;
++ m_value2Id = value2Id;
++ }
++
++ inline QList<Qt3DCore::QNodeId> allDependencyIds() const override
++ {
++ return currentDependencyIds();
++ }
++
++ QList<Qt3DCore::QNodeId> currentDependencyIds() const final
++ {
++ return { m_value1Id, m_value2Id };
++ }
++
++ using ClipBlendNode::setClipResults;
++
++ double duration() const final { return 0.0f; }
++
++ protected:
++ ClipResults doBlend(const QList<ClipResults> &blendData) const final
++ {
++ Q_ASSERT(blendData.size() == 2);
++ const qsizetype elementCount = blendData.first().size();
++ ClipResults blendResults(elementCount);
++
++ for (qsizetype i = 0; i < elementCount; ++i)
++ blendResults[i] = 0.5f * (blendData[0][i] + blendData[1][i]);
++
++ return blendResults;
++ }
++
++ private:
++ Qt3DCore::QNodeId m_value1Id;
++ Qt3DCore::QNodeId m_value2Id;
++ };
++
++ bool fuzzyCompare(float x1, float x2)
++ {
++ if (qFuzzyIsNull(x1) && qFuzzyIsNull(x2)) {
++ return true;
++ } else if ((qFuzzyIsNull(x1) && !qFuzzyIsNull(x2)) ||
++ (!qFuzzyIsNull(x1) && qFuzzyIsNull(x2))) {
++ return false;
++ } else {
++ return qFuzzyCompare(x1, x2);
++ }
++ }
++
++ class DummyCallback : public Qt3DAnimation::QAnimationCallback
++ {
++ public:
++ void valueChanged(const QVariant &) override { }
++ };
++
++ } // anonymous
++
++
++ class tst_AnimationUtils : public Qt3DCore::QBackendNodeTester
++ {
++ Q_OBJECT
++
++ public:
++ ChannelMapping *createChannelMapping(Handler *handler,
++ const QString &channelName,
++ const Qt3DCore::QNodeId targetId,
++ const char *propertyName,
++ int type,
++ int componentCount)
++ {
++ auto channelMappingId = Qt3DCore::QNodeId::createId();
++ ChannelMapping *channelMapping = handler->channelMappingManager()->getOrCreateResource(channelMappingId);
++ setPeerId(channelMapping, channelMappingId);
++ channelMapping->setHandler(handler);
++ channelMapping->setTargetId(targetId);
++ channelMapping->setPropertyName(propertyName);
++ channelMapping->setChannelName(channelName);
++ channelMapping->setType(type);
++ channelMapping->setMappingType(ChannelMapping::ChannelMappingType);
++ channelMapping->setComponentCount(componentCount);
++ return channelMapping;
++ }
++
++ ChannelMapping *createChannelMapping(Handler *handler,
++ const Qt3DCore::QNodeId skeletonId)
++ {
++ auto channelMappingId = Qt3DCore::QNodeId::createId();
++ ChannelMapping *channelMapping = handler->channelMappingManager()->getOrCreateResource(channelMappingId);
++ setPeerId(channelMapping, channelMappingId);
++ channelMapping->setHandler(handler);
++ channelMapping->setSkeletonId(skeletonId);
++ channelMapping->setMappingType(ChannelMapping::SkeletonMappingType);
++ return channelMapping;
++ }
++
++ ChannelMapper *createChannelMapper(Handler *handler,
++ const QList<Qt3DCore::QNodeId> &mappingIds)
++ {
++ auto channelMapperId = Qt3DCore::QNodeId::createId();
++ ChannelMapper *channelMapper = handler->channelMapperManager()->getOrCreateResource(channelMapperId);
++ setPeerId(channelMapper, channelMapperId);
++ channelMapper->setMappingIds(mappingIds);
++ return channelMapper;
++ }
++
++ AnimationClip *createAnimationClipLoader(Handler *handler,
++ const QUrl &source)
++ {
++ auto clipId = Qt3DCore::QNodeId::createId();
++ AnimationClip *clip = handler->animationClipLoaderManager()->getOrCreateResource(clipId);
++ setPeerId(clip, clipId);
++ clip->setDataType(AnimationClip::File);
++ clip->setSource(source);
++ clip->loadAnimation();
++ return clip;
++ }
++
++ ClipAnimator *createClipAnimator(Handler *handler,
++ qint64 globalStartTimeNS,
++ int loops)
++ {
++ auto animatorId = Qt3DCore::QNodeId::createId();
++ ClipAnimator *animator = handler->clipAnimatorManager()->getOrCreateResource(animatorId);
++ setPeerId(animator, animatorId);
++ animator->setStartTime(globalStartTimeNS);
++ animator->setLoops(loops);
++ return animator;
++ }
++
++ BlendedClipAnimator *createBlendedClipAnimator(Handler *handler,
++ qint64 globalStartTimeNS,
++ int loops)
++ {
++ auto animatorId = Qt3DCore::QNodeId::createId();
++ BlendedClipAnimator *animator = handler->blendedClipAnimatorManager()->getOrCreateResource(animatorId);
++ setPeerId(animator, animatorId);
++ animator->setStartTime(globalStartTimeNS);
++ animator->setLoops(loops);
++ return animator;
++ }
++
++ LerpClipBlend *createLerpClipBlend(Handler *handler)
++ {
++ auto lerpId = Qt3DCore::QNodeId::createId();
++ LerpClipBlend *lerp = new LerpClipBlend();
++ setPeerId(lerp, lerpId);
++ lerp->setClipBlendNodeManager(handler->clipBlendNodeManager());
++ lerp->setHandler(handler);
++ handler->clipBlendNodeManager()->appendNode(lerpId, lerp);
++ return lerp;
++ }
++
++ AdditiveClipBlend *createAdditiveClipBlend(Handler *handler)
++ {
++ auto additiveId = Qt3DCore::QNodeId::createId();
++ AdditiveClipBlend *additive = new AdditiveClipBlend();
++ setPeerId(additive, additiveId);
++ additive->setClipBlendNodeManager(handler->clipBlendNodeManager());
++ additive->setHandler(handler);
++ handler->clipBlendNodeManager()->appendNode(additiveId, additive);
++ return additive;
++ }
++
++ ClipBlendValue *createClipBlendValue(Handler *handler)
++ {
++ auto valueId = Qt3DCore::QNodeId::createId();
++ ClipBlendValue *value = new ClipBlendValue();
++ setPeerId(value, valueId);
++ value->setClipBlendNodeManager(handler->clipBlendNodeManager());
++ value->setHandler(handler);
++ handler->clipBlendNodeManager()->appendNode(valueId, value);
++ return value;
++ }
++
++ MeanBlendNode *createMeanBlendNode(Handler *handler)
++ {
++ auto id = Qt3DCore::QNodeId::createId();
++ MeanBlendNode *node = new MeanBlendNode();
++ setPeerId(node, id);
++ node->setClipBlendNodeManager(handler->clipBlendNodeManager());
++ node->setHandler(handler);
++ handler->clipBlendNodeManager()->appendNode(id, node);
++ return node;
++ }
++
++ Skeleton *createSkeleton(Handler *handler, int jointCount)
++ {
++ auto skeletonId = Qt3DCore::QNodeId::createId();
++ Skeleton *skeleton = handler->skeletonManager()->getOrCreateResource(skeletonId);
++ setPeerId(skeleton, skeletonId);
++ skeleton->setJointCount(jointCount);
++ return skeleton;
++ }
++
++ private Q_SLOTS:
++ void checkBuildPropertyMappings_data()
++ {
++ QTest::addColumn<Handler *>("handler");
++ QTest::addColumn<QVector<ChannelMapping *>>("channelMappings");
++ QTest::addColumn<QVector<ChannelNameAndType>>("channelNamesAndTypes");
++ QTest::addColumn<QVector<ComponentIndices>>("channelComponentIndices");
++ QTest::addColumn<QVector<QBitArray>>("sourceClipMask");
++ QTest::addColumn<QVector<MappingData>>("expectedResults");
++
++ // Single ChannelMapping
++ {
++ Handler *handler = new Handler();
++
++ auto channelMapping = createChannelMapping(handler,
++ QLatin1String("Location"),
++ Qt3DCore::QNodeId::createId(),
++ "translation",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++
++ const QVector<ChannelMapping *> channelMappings = { channelMapping };
++
++ // Create a few channels in the format description
++ const ChannelNameAndType rotation = { QLatin1String("Rotation"),
++ static_cast<int>(QMetaType::QQuaternion),
++ 4,
++ channelMapping->peerId() };
++ const ChannelNameAndType location = { QLatin1String("Location"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping->peerId() };
++ const ChannelNameAndType baseColor = { QLatin1String("BaseColor"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping->peerId() };
++ const ChannelNameAndType metalness = { QLatin1String("Metalness"),
++ static_cast<int>(QMetaType::Double),
++ 1,
++ channelMapping->peerId() };
++ const ChannelNameAndType roughness = { QLatin1String("Roughness"),
++ static_cast<int>(QMetaType::Double),
++ 1,
++ channelMapping->peerId() };
++ const ChannelNameAndType morphTargetWeightsList = { QLatin1String("MorphTargetWeightsList"),
++ static_cast<int>(QMetaType::QVariantList),
++ 5,
++ channelMapping->peerId() };
++ const ChannelNameAndType morphTargetWeightsVec = { QLatin1String("MorphTargetWeightsVec"),
++ qMetaTypeId<QVector<float>>(),
++ 6,
++ channelMapping->peerId() };
++ const ChannelNameAndType rgbColor = { QLatin1String("rgbColor"),
++ static_cast<int>(QMetaType::QColor),
++ 3,
++ channelMapping->peerId() };
++ const ChannelNameAndType rgbaColor = { QLatin1String("rgbaColor"),
++ static_cast<int>(QMetaType::QColor),
++ 4,
++ channelMapping->peerId() };
++
++ const QVector<ChannelNameAndType> channelNamesAndTypes
++ = { rotation, location, baseColor, metalness, roughness,
++ morphTargetWeightsList, morphTargetWeightsVec, rgbColor, rgbaColor };
++
++ // And the matching indices
++ const ComponentIndices rotationIndices = { 0, 1, 2, 3 };
++ const ComponentIndices locationIndices = { 4, 5, 6 };
++ const ComponentIndices baseColorIndices = { 7, 8, 9 };
++ const ComponentIndices metalnessIndices = { 10 };
++ const ComponentIndices roughnessIndices = { 11 };
++ const ComponentIndices morphTargetListIndices = { 12, 13, 14, 15, 16 };
++ const ComponentIndices morphTargetVecIndices = { 17, 18, 19, 20, 21, 22 };
++ const ComponentIndices rgbColorIndices = { 23, 24, 25 };
++ const ComponentIndices rgbaColorIndices = { 26, 27, 28, 29 };
++ const QVector<ComponentIndices> channelComponentIndices
++ = { rotationIndices, locationIndices, baseColorIndices,
++ metalnessIndices, roughnessIndices, morphTargetListIndices,
++ morphTargetVecIndices, rgbColorIndices, rgbaColorIndices };
++
++ const QVector<QBitArray> sourceClipMask = { QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, true),
++ QBitArray(1, true),
++ QBitArray(1, true),
++ QBitArray(5, true),
++ QBitArray(6, true),
++ QBitArray(3, true),
++ QBitArray(4, true) };
++
++ MappingData expectedMapping;
++ expectedMapping.targetId = channelMapping->targetId();
++ expectedMapping.propertyName = channelMapping->propertyName();
++ expectedMapping.type = channelMapping->type();
++ expectedMapping.channelIndices = locationIndices;
++ const QVector<MappingData> expectedResults = { expectedMapping };
++
++ QTest::newRow("single mapping")
++ << handler
++ << channelMappings
++ << channelNamesAndTypes
++ << channelComponentIndices
++ << sourceClipMask
++ << expectedResults;
++ }
++
++ // Multiple ChannelMappings
++ {
++ Handler *handler = new Handler();
++
++ auto locationMapping = createChannelMapping(handler,
++ QLatin1String("Location"),
++ Qt3DCore::QNodeId::createId(),
++ "translation",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++
++ auto metalnessMapping = createChannelMapping(handler,
++ QLatin1String("Metalness"),
++ Qt3DCore::QNodeId::createId(),
++ "metalness",
++ static_cast<int>(QMetaType::Double),
++ 1);
++
++ auto baseColorMapping = createChannelMapping(handler,
++ QLatin1String("BaseColor"),
++ Qt3DCore::QNodeId::createId(),
++ "baseColor",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++
++ auto roughnessMapping = createChannelMapping(handler,
++ QLatin1String("Roughness"),
++ Qt3DCore::QNodeId::createId(),
++ "roughness",
++ static_cast<int>(QMetaType::Double),
++ 1);
++
++ auto rotationMapping = createChannelMapping(handler,
++ QLatin1String("Rotation"),
++ Qt3DCore::QNodeId::createId(),
++ "rotation",
++ static_cast<int>(QMetaType::QQuaternion),
++ 4);
++
++ auto morphTargetMapping = createChannelMapping(handler,
++ QLatin1String("MorphTargetWeights"),
++ Qt3DCore::QNodeId::createId(),
++ "weights",
++ static_cast<int>(QMetaType::QVariantList),
++ 5);
++
++
++ const QVector<ChannelMapping *> channelMappings
++ = { locationMapping, metalnessMapping,
++ baseColorMapping, roughnessMapping,
++ rotationMapping, morphTargetMapping };
++
++ // Create a few channels in the format description
++ const ChannelNameAndType rotation = { QLatin1String("Rotation"),
++ static_cast<int>(QMetaType::QQuaternion),
++ 4,
++ rotationMapping->peerId() };
++ const ChannelNameAndType location = { QLatin1String("Location"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ locationMapping->peerId() };
++ const ChannelNameAndType baseColor = { QLatin1String("BaseColor"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ baseColorMapping->peerId() };
++ const ChannelNameAndType metalness = { QLatin1String("Metalness"),
++ static_cast<int>(QMetaType::Double),
++ 1,
++ metalnessMapping->peerId() };
++ const ChannelNameAndType roughness = { QLatin1String("Roughness"),
++ static_cast<int>(QMetaType::Double),
++ 1,
++ roughnessMapping->peerId() };
++ const ChannelNameAndType morphTarget = { QLatin1String("MorphTargetWeights"),
++ static_cast<int>(QMetaType::QVariantList),
++ 5,
++ morphTargetMapping->peerId() };
++ const QVector<ChannelNameAndType> channelNamesAndTypes
++ = { rotation, location, baseColor, metalness, roughness,
++ morphTarget };
++
++ // And the matching indices
++ const ComponentIndices rotationIndices = { 0, 1, 2, 3 };
++ const ComponentIndices locationIndices = { 4, 5, 6 };
++ const ComponentIndices baseColorIndices = { 7, 8, 9 };
++ const ComponentIndices metalnessIndices = { 10 };
++ const ComponentIndices roughnessIndices = { 11 };
++ const ComponentIndices morphTargetIndices = { 12, 13, 14, 15, 16 };
++ const QVector<ComponentIndices> channelComponentIndices
++ = { rotationIndices, locationIndices, baseColorIndices,
++ metalnessIndices, roughnessIndices, morphTargetIndices };
++
++ const QVector<QBitArray> sourceClipMask = { QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, true),
++ QBitArray(1, true),
++ QBitArray(1, true),
++ QBitArray(5, true) };
++
++ MappingData expectedLocationMapping;
++ expectedLocationMapping.targetId = locationMapping->targetId();
++ expectedLocationMapping.propertyName = locationMapping->propertyName();
++ expectedLocationMapping.type = locationMapping->type();
++ expectedLocationMapping.channelIndices = locationIndices;
++
++ MappingData expectedMetalnessMapping;
++ expectedMetalnessMapping.targetId = metalnessMapping->targetId();
++ expectedMetalnessMapping.propertyName = metalnessMapping->propertyName();
++ expectedMetalnessMapping.type = metalnessMapping->type();
++ expectedMetalnessMapping.channelIndices = metalnessIndices;
++
++ MappingData expectedBaseColorMapping;
++ expectedBaseColorMapping.targetId = baseColorMapping->targetId();
++ expectedBaseColorMapping.propertyName = baseColorMapping->propertyName();
++ expectedBaseColorMapping.type = baseColorMapping->type();
++ expectedBaseColorMapping.channelIndices = baseColorIndices;
++
++ MappingData expectedRoughnessMapping;
++ expectedRoughnessMapping.targetId = roughnessMapping->targetId();
++ expectedRoughnessMapping.propertyName = roughnessMapping->propertyName();
++ expectedRoughnessMapping.type = roughnessMapping->type();
++ expectedRoughnessMapping.channelIndices = roughnessIndices;
++
++ MappingData expectedRotationMapping;
++ expectedRotationMapping.targetId = rotationMapping->targetId();
++ expectedRotationMapping.propertyName = rotationMapping->propertyName();
++ expectedRotationMapping.type = rotationMapping->type();
++ expectedRotationMapping.channelIndices = rotationIndices;
++
++ MappingData expectedMorphTargetMapping;
++ expectedMorphTargetMapping.targetId = morphTargetMapping->targetId();
++ expectedMorphTargetMapping.propertyName = morphTargetMapping->propertyName();
++ expectedMorphTargetMapping.type = morphTargetMapping->type();
++ expectedMorphTargetMapping.channelIndices = morphTargetIndices;
++
++ const QVector<MappingData> expectedResults
++ = { expectedLocationMapping,
++ expectedMetalnessMapping,
++ expectedBaseColorMapping,
++ expectedRoughnessMapping,
++ expectedRotationMapping,
++ expectedMorphTargetMapping };
++
++ QTest::newRow("multiple mappings")
++ << handler
++ << channelMappings
++ << channelNamesAndTypes
++ << channelComponentIndices
++ << sourceClipMask
++ << expectedResults;
++ }
++
++ // Single skeleton mapping
++ {
++ Handler *handler = new Handler();
++ const qsizetype jointCount = 4;
++ auto skeleton = createSkeleton(handler, jointCount);
++ auto channelMapping = createChannelMapping(handler, skeleton->peerId());
++
++ const QVector<ChannelMapping *> channelMappings = { channelMapping };
++
++ // Create a few channels in the format description
++ QVector<ChannelNameAndType> channelNamesAndTypes;
++ for (qsizetype i = 0; i < jointCount; ++i) {
++ ChannelNameAndType locationDescription = { QLatin1String("Location"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping->peerId() };
++ locationDescription.jointIndex = i;
++ locationDescription.jointTransformComponent = Translation;
++ channelNamesAndTypes.push_back(locationDescription);
++
++ ChannelNameAndType rotationDescription = { QLatin1String("Rotation"),
++ static_cast<int>(QMetaType::QQuaternion),
++ 4,
++ channelMapping->peerId() };
++ rotationDescription.jointIndex = i;
++ rotationDescription.jointTransformComponent = Rotation;
++ channelNamesAndTypes.push_back(rotationDescription);
++
++ ChannelNameAndType scaleDescription = { QLatin1String("Scale"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping->peerId() };
++ scaleDescription.jointIndex = i;
++ scaleDescription.jointTransformComponent = Scale;
++ channelNamesAndTypes.push_back(scaleDescription);
++ }
++
++ // And the matching indices
++ const QVector<ComponentIndices> channelComponentIndices = {
++ { 0, 1, 2 }, { 3, 4, 5, 6 }, { 7, 8, 9 },
++ { 10, 11, 12 }, { 13, 14, 15, 16 }, { 17, 18, 19 },
++ { 20, 21, 22 }, { 23, 24, 25, 26 }, { 27, 28, 29 },
++ { 30, 31, 32 }, { 33, 34, 35, 36 }, { 37, 38, 39 }
++ };
++
++ const QVector<QBitArray> sourceClipMask = { QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, true) };
++
++ QVector<MappingData> expectedResults;
++ int componentIndicesIndex = 0;
++ for (qsizetype i = 0; i < jointCount; ++i) {
++ MappingData locationMapping;
++ locationMapping.targetId = channelMapping->skeletonId();
++ locationMapping.propertyName = "translation";
++ locationMapping.type = static_cast<int>(QMetaType::QVector3D);
++ locationMapping.channelIndices = channelComponentIndices[componentIndicesIndex++];
++ locationMapping.jointIndex = i;
++
++ MappingData rotationMapping;
++ rotationMapping.targetId = channelMapping->skeletonId();
++ rotationMapping.propertyName = "rotation";
++ rotationMapping.type = static_cast<int>(QMetaType::QQuaternion);
++ rotationMapping.channelIndices = channelComponentIndices[componentIndicesIndex++];
++ rotationMapping.jointIndex = i;
++
++ MappingData scaleMapping;
++ scaleMapping.targetId = channelMapping->skeletonId();
++ scaleMapping.propertyName = "scale";
++ scaleMapping.type = static_cast<int>(QMetaType::QVector3D);
++ scaleMapping.channelIndices = channelComponentIndices[componentIndicesIndex++];
++ scaleMapping.jointIndex = i;
++
++ expectedResults << locationMapping << rotationMapping << scaleMapping;
++ }
++
++ QTest::newRow("single skeleton mapping")
++ << handler
++ << channelMappings
++ << channelNamesAndTypes
++ << channelComponentIndices
++ << sourceClipMask
++ << expectedResults;
++ }
++ }
++
++ void checkBuildPropertyMappings()
++ {
++ // GIVEN
++ QFETCH(Handler *, handler);
++ QFETCH(QVector<ChannelMapping *>, channelMappings);
++ QFETCH(QVector<ChannelNameAndType>, channelNamesAndTypes);
++ QFETCH(QVector<ComponentIndices>, channelComponentIndices);
++ QFETCH(QVector<QBitArray>, sourceClipMask);
++ QFETCH(QVector<MappingData>, expectedResults);
++
++ // WHEN
++ const auto actualResults = buildPropertyMappings(channelMappings,
++ channelNamesAndTypes,
++ channelComponentIndices,
++ sourceClipMask);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (qsizetype i = 0; i < actualResults.size(); ++i) {
++ const auto actualMapping = actualResults[i];
++ const auto expectedMapping = expectedResults[i];
++
++ QCOMPARE(actualMapping.targetId, expectedMapping.targetId);
++ QCOMPARE(actualMapping.jointIndex, expectedMapping.jointIndex);
++ QCOMPARE(actualMapping.propertyName, expectedMapping.propertyName);
++ QCOMPARE(actualMapping.type, expectedMapping.type);
++ QCOMPARE(actualMapping.channelIndices.size(), expectedMapping.channelIndices.size());
++ for (int j = 0; j < actualMapping.channelIndices.size(); ++j) {
++ QCOMPARE(actualMapping.channelIndices[j], expectedMapping.channelIndices[j]);
++ }
++ }
++
++ // Cleanup
++ delete handler;
++ }
++
++ void checkLocalTimeFromElapsedTime_data()
++ {
++ QTest::addColumn<double>("elapsedTime");
++ QTest::addColumn<double>("currentTime");
++ QTest::addColumn<double>("playbackRate");
++ QTest::addColumn<double>("duration");
++ QTest::addColumn<int>("loopCount");
++ QTest::addColumn<int>("currentLoop");
++ QTest::addColumn<double>("expectedLocalTime");
++ QTest::addColumn<int>("expectedCurrentLoop");
++
++ double elapsedTime;
++ double currentTime;
++ double playbackRate;
++ double duration;
++ int loopCount;
++ int currentLoop;
++ double expectedLocalTime;
++ int expectedCurrentLoop;
++
++ elapsedTime = 0.0;
++ currentTime = 0.0;
++ playbackRate = 1.0;
++ duration = 1.0;
++ loopCount = 1;
++ currentLoop = 0;
++ expectedLocalTime = 0.0;
++ expectedCurrentLoop = 0;
++ QTest::newRow("simple, t_current = 0, t_elapsed = 0, loop_current = 0")
++ << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
++ << expectedLocalTime << expectedCurrentLoop;
++
++ elapsedTime = 0.5;
++ currentTime = 0.0;
++ playbackRate = 1.0;
++ duration = 1.0;
++ loopCount = 1;
++ currentLoop = 0;
++ expectedLocalTime = 0.5;
++ expectedCurrentLoop = 0;
++ QTest::newRow("simple, t_current = 0, t_elapsed = 0.5, loop_current = 0")
++ << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
++ << expectedLocalTime << expectedCurrentLoop;
++
++ elapsedTime = 1.5;
++ currentTime = 0.0;
++ playbackRate = 1.0;
++ duration = 1.0;
++ loopCount = 1;
++ currentLoop = 0;
++ expectedLocalTime = 1.0;
++ expectedCurrentLoop = 0;
++ QTest::newRow("simple, t_current = 0, t_elapsed = 1.5, loop_current = 0")
++ << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
++ << expectedLocalTime << expectedCurrentLoop;
++
++ elapsedTime = 0.5;
++ currentTime = 0.6;
++ playbackRate = 1.0;
++ duration = 1.0;
++ loopCount = 1;
++ currentLoop = 0;
++ expectedLocalTime = 1.0;
++ expectedCurrentLoop = 0;
++ QTest::newRow("simple, t_current = 0.5, t_elapsed = 0.6, loop_current = 0")
++ << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
++ << expectedLocalTime << expectedCurrentLoop;
++
++ elapsedTime = 0.5;
++ currentTime = 0.6;
++ playbackRate = 1.0;
++ duration = 1.0;
++ loopCount = 2;
++ currentLoop = 0;
++ expectedLocalTime = 0.1;
++ expectedCurrentLoop = 1;
++ QTest::newRow("simple, t_current = 0.5, t_elapsed = 0.6, loop_current = 0, loop_count = 2")
++ << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
++ << expectedLocalTime << expectedCurrentLoop;
++
++ elapsedTime = 0.5;
++ currentTime = 0.6;
++ playbackRate = 1.0;
++ duration = 1.0;
++ loopCount = 2;
++ currentLoop = 1;
++ expectedLocalTime = 1.0;
++ expectedCurrentLoop = 1; // We clamp at end of final loop
++ QTest::newRow("simple, t_current = 0.5, t_elapsed = 0.6, loop_current = 1, loop_count = 2")
++ << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
++ << expectedLocalTime << expectedCurrentLoop;
++
++ elapsedTime = 0.5;
++ currentTime = 0.6;
++ playbackRate = 0.1;
++ duration = 1.0;
++ loopCount = 2;
++ currentLoop = 1;
++ expectedLocalTime = 0.65;
++ expectedCurrentLoop = 1;
++ QTest::newRow("simple, t_current = 0.5, t_elapsed = 0.6, loop_current = 1, loop_count = 2")
++ << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
++ << expectedLocalTime << expectedCurrentLoop;
++ }
++
++ void checkLocalTimeFromElapsedTime()
++ {
++ // GIVEN
++ QFETCH(double, elapsedTime);
++ QFETCH(double, currentTime);
++ QFETCH(double, playbackRate);
++ QFETCH(double, duration);
++ QFETCH(int, loopCount);
++ QFETCH(int, currentLoop);
++ QFETCH(double, expectedLocalTime);
++ QFETCH(int, expectedCurrentLoop);
++
++ // WHEN
++ int actualCurrentLoop = currentLoop;
++ double actualLocalTime = localTimeFromElapsedTime(currentTime,
++ elapsedTime,
++ playbackRate,
++ duration,
++ loopCount,
++ actualCurrentLoop);
++
++ // THEN
++ QCOMPARE(actualCurrentLoop, expectedCurrentLoop);
++ QCOMPARE(actualLocalTime, expectedLocalTime);
++ }
++
++ void checkPreparePropertyChanges_data()
++ {
++ QTest::addColumn<Qt3DCore::QNodeId>("animatorId");
++ QTest::addColumn<QVector<MappingData>>("mappingData");
++ QTest::addColumn<QVector<float>>("channelResults");
++ QTest::addColumn<AnimationRecord>("expectedChanges");
++
++ Qt3DCore::QNodeId animatorId;
++ QVector<MappingData> mappingData;
++ QVector<float> channelResults;
++ AnimationRecord expectedChanges;
++
++ // Single property, vec3
++ {
++ animatorId = Qt3DCore::QNodeId::createId();
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "translation";
++ mapping.type = static_cast<int>(QMetaType::QVector3D);
++ mapping.channelIndices = QVector<qsizetype>{ 0, 1, 2 };
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 1.0f, 2.0f, 3.0f };
++ expectedChanges.normalizedTime = 1.1f; // Invalid
++ expectedChanges.finalFrame = false;
++ expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QVector3D(1.0f, 2.0f, 3.0f))});
++
++ QTest::newRow("vec3 translation, final = false")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ expectedChanges.normalizedTime = 1.0f;
++ expectedChanges.finalFrame = true;
++
++ QTest::newRow("vec3 translation, final = true, normalizedTime = 1.0f")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedChanges.targetChanges.clear();
++ }
++
++ // Multiple properties, all vec3
++ {
++ animatorId = Qt3DCore::QNodeId::createId();
++ MappingData translationMapping;
++ translationMapping.targetId = Qt3DCore::QNodeId::createId();
++ translationMapping.propertyName = "translation";
++ translationMapping.type = static_cast<int>(QMetaType::QVector3D);
++ translationMapping.channelIndices = QVector<qsizetype>{ 0, 1, 2 };
++ mappingData.push_back(translationMapping);
++
++ MappingData scaleMapping;
++ scaleMapping.targetId = Qt3DCore::QNodeId::createId();
++ scaleMapping.propertyName = "scale";
++ scaleMapping.type = static_cast<int>(QMetaType::QVector3D);
++ scaleMapping.channelIndices = QVector<qsizetype>{ 3, 4, 5 };
++ mappingData.push_back(scaleMapping);
++
++ channelResults = QVector<float> { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f };
++ expectedChanges.finalFrame = false;
++ expectedChanges.normalizedTime = -0.1f; // Invalid
++
++ expectedChanges.targetChanges.push_back({translationMapping.targetId, translationMapping.propertyName, QVariant::fromValue(QVector3D(1.0f, 2.0f, 3.0f))});
++ expectedChanges.targetChanges.push_back({scaleMapping.targetId, scaleMapping.propertyName, QVariant::fromValue(QVector3D(4.0f, 5.0f, 6.0f))});
++
++ QTest::newRow("vec3 translation, vec3 scale, final = false")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ expectedChanges.normalizedTime = 0.5f;
++ expectedChanges.finalFrame = true;
++
++ QTest::newRow("vec3 translation, vec3 scale, final = true")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedChanges.targetChanges.clear();
++ }
++
++ // Single property, double
++ {
++ animatorId = Qt3DCore::QNodeId::createId();
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "mass";
++ mapping.type = static_cast<int>(QMetaType::Double);
++ mapping.channelIndices = QVector<qsizetype>{ 0 };
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 3.5f };
++ expectedChanges.finalFrame = false;
++ expectedChanges.normalizedTime = -1.0f; // Invalid
++ expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(3.5f)});
++
++ QTest::newRow("double mass")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedChanges.targetChanges.clear();
++ }
++
++ // Single property, vec2
++ {
++ animatorId = Qt3DCore::QNodeId::createId();
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "pos";
++ mapping.type = static_cast<int>(QMetaType::QVector2D);
++ mapping.channelIndices = QVector<qsizetype>{ 0, 1 };
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 2.0f, 1.0f };
++ expectedChanges.finalFrame = false;
++ expectedChanges.normalizedTime = 1.1f; // Invalid
++ expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QVector2D(2.0f, 1.0f))});
++
++ QTest::newRow("vec2 pos")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedChanges.targetChanges.clear();
++ }
++
++ // Single property, vec4
++ {
++ animatorId = Qt3DCore::QNodeId::createId();
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "foo";
++ mapping.type = static_cast<int>(QMetaType::QVector4D);
++ mapping.channelIndices = QVector<qsizetype>{ 0, 1, 2, 3 };
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 4.0f, 3.0f, 2.0f, 1.0f };
++ expectedChanges.finalFrame = false;
++ expectedChanges.normalizedTime = 1.1f; // Invalid
++ expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QVector4D(4.0f, 3.0f, 2.0f, 1.0f))});
++
++ QTest::newRow("vec4 foo")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedChanges.targetChanges.clear();
++ }
++
++ // Single property, quaternion
++ {
++ animatorId = Qt3DCore::QNodeId::createId();
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "rotation";
++ mapping.type = static_cast<int>(QMetaType::QQuaternion);
++ mapping.channelIndices = QVector<qsizetype>{ 0, 1, 2, 3 };
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 1.0f, 0.0f, 0.0f, 1.0f };
++ expectedChanges.finalFrame = false;
++ expectedChanges.normalizedTime = -0.1f; // Invalid
++ expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QQuaternion(1.0f, 0.0f, 0.0f, 1.0f).normalized())});
++
++ QTest::newRow("quaternion rotation")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedChanges.targetChanges.clear();
++ }
++
++ // Single property, QColor
++ {
++ animatorId = Qt3DCore::QNodeId::createId();
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "color";
++ mapping.type = static_cast<int>(QMetaType::QColor);
++ mapping.channelIndices = QVector<qsizetype>{ 0, 1, 2 };
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 0.5f, 0.4f, 0.3f };
++ expectedChanges.finalFrame = false;
++ expectedChanges.normalizedTime = 1.1f; // Invalid
++ expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QColor::fromRgbF(0.5f, 0.4f, 0.3f))});
++
++ QTest::newRow("QColor rgb color")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedChanges.targetChanges.clear();
++ }
++
++ // Single property, QColor
++ {
++ animatorId = Qt3DCore::QNodeId::createId();
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "color";
++ mapping.type = static_cast<int>(QMetaType::QColor);
++ mapping.channelIndices = QVector<qsizetype>{ 0, 1, 2, 3 };
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 0.5f, 0.4f, 0.3f, 0.2f };
++ expectedChanges.finalFrame = false;
++ expectedChanges.normalizedTime = 1.1f; // Invalid
++ expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QColor::fromRgbF(0.5f, 0.4f, 0.3f, 0.2f))});
++
++ QTest::newRow("QColor rgba color")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedChanges.targetChanges.clear();
++ }
++
++ // Single property, QVariantList
++ {
++ animatorId = Qt3DCore::QNodeId::createId();
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "weights";
++ mapping.type = static_cast<int>(QMetaType::QVariantList);
++ mapping.channelIndices = QVector<qsizetype>{ 0, 1, 2, 3, 4, 5, 6 };
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 0.5f, 0.4f, 0.3f, 0.0f, 1.0f, 0.6f, 0.9f };
++ expectedChanges.finalFrame = false;
++ expectedChanges.normalizedTime = 1.1f; // Invalid
++ QVariantList expectedValue = QVariantList { 0.5f, 0.4f, 0.3f, 0.0f, 1.0f, 0.6f, 0.9f };
++ expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(expectedValue)});
++
++ QTest::newRow("QVariantList weights")
++ << animatorId << mappingData << channelResults << expectedChanges;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedChanges.targetChanges.clear();
++ }
++
++ }
++
++ void checkPreparePropertyChanges()
++ {
++ // GIVEN
++ QFETCH(Qt3DCore::QNodeId, animatorId);
++ QFETCH(QVector<MappingData>, mappingData);
++ QFETCH(QVector<float>, channelResults);
++ QFETCH(AnimationRecord, expectedChanges);
++
++ // WHEN
++ AnimationRecord actualChanges = prepareAnimationRecord(animatorId, mappingData, channelResults,
++ expectedChanges.finalFrame, expectedChanges.normalizedTime);
++
++ // THEN
++ QCOMPARE(actualChanges.targetChanges.size(), expectedChanges.targetChanges.size());
++ for (int i = 0; i < actualChanges.targetChanges.size(); ++i) {
++ const auto &expectedChange = expectedChanges.targetChanges[i];
++ const auto &actualChange = actualChanges.targetChanges[i];
++
++ QCOMPARE(actualChange.targetId, expectedChange.targetId);
++ QCOMPARE(actualChange.propertyName, expectedChange.propertyName);
++ QCOMPARE(actualChange.value, expectedChange.value);
++ }
++ }
++
++ void checkPrepareCallbacks_data()
++ {
++ QTest::addColumn<QVector<MappingData>>("mappingData");
++ QTest::addColumn<QVector<float>>("channelResults");
++ QTest::addColumn<QVector<AnimationCallbackAndValue> >("expectedValues");
++
++ QVector<MappingData> mappingData;
++ QVector<float> channelResults;
++ QVector<AnimationCallbackAndValue> expectedValues;
++
++ // vec3
++ {
++ DummyCallback callback; // safe since the object is never used, just the address
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "translation";
++ mapping.type = static_cast<int>(QMetaType::QVector3D);
++ mapping.channelIndices = QVector<qsizetype>{ 0, 1, 2 };
++ mapping.callback = &callback;
++ mapping.callbackFlags = {};
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 1.0f, 2.0f, 3.0f };
++
++ AnimationCallbackAndValue cbv;
++ cbv.callback = mapping.callback;
++ cbv.flags = mapping.callbackFlags;
++ cbv.value = QVariant::fromValue<QVector3D>(QVector3D(1.0f, 2.0f, 3.0f));
++ expectedValues.push_back(cbv);
++
++ QTest::newRow("vec3 translation, no flags") << mappingData << channelResults << expectedValues;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedValues.clear();
++ }
++
++ // double
++ {
++ DummyCallback callback;
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "something";
++ mapping.type = static_cast<int>(QMetaType::Double);
++ mapping.channelIndices = QVector<qsizetype>{ 0 };
++ mapping.callback = &callback;
++ mapping.callbackFlags = {};
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 1.0f };
++
++ AnimationCallbackAndValue cbv;
++ cbv.callback = mapping.callback;
++ cbv.flags = mapping.callbackFlags;
++ cbv.value = QVariant(double(1.0));
++ expectedValues.push_back(cbv);
++
++ QTest::newRow("double, no flags") << mappingData << channelResults << expectedValues;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedValues.clear();
++ }
++
++ // float, set a flag
++ {
++ DummyCallback callback;
++ MappingData mapping;
++ mapping.targetId = Qt3DCore::QNodeId::createId();
++ mapping.propertyName = "opacity";
++ mapping.type = static_cast<int>(QMetaType::Float);
++ mapping.channelIndices = QVector<qsizetype>{ 0 };
++ mapping.callback = &callback;
++ mapping.callbackFlags = Qt3DAnimation::QAnimationCallback::OnThreadPool;
++ mappingData.push_back(mapping);
++ channelResults = QVector<float> { 0.5f };
++
++ AnimationCallbackAndValue cbv;
++ cbv.callback = mapping.callback;
++ cbv.flags = mapping.callbackFlags;
++ cbv.value = QVariant(float(0.5f));
++ expectedValues.push_back(cbv);
++
++ QTest::newRow("float, OnThreadPool") << mappingData << channelResults << expectedValues;
++
++ mappingData.clear();
++ channelResults.clear();
++ expectedValues.clear();
++ }
++ }
++
++ void checkPrepareCallbacks()
++ {
++ // GIVEN
++ QFETCH(QVector<MappingData>, mappingData);
++ QFETCH(QVector<float>, channelResults);
++ QFETCH(QVector<AnimationCallbackAndValue>, expectedValues);
++
++ // WHEN
++ QVector<AnimationCallbackAndValue> callbacks = prepareCallbacks(mappingData, channelResults);
++
++ // THEN
++ QCOMPARE(callbacks.size(), expectedValues.size());
++ for (int i = 0; i < callbacks.size(); ++i) {
++ auto expected = expectedValues[i];
++ auto actual = callbacks[i];
++
++ QCOMPARE(actual.callback, expected.callback);
++ QCOMPARE(actual.flags, expected.flags);
++ QCOMPARE(actual.value, expected.value);
++ }
++ }
++
++ void checkEvaluateClipAtLocalTime_data()
++ {
++ QTest::addColumn<Handler *>("handler");
++ QTest::addColumn<AnimationClip *>("clip");
++ QTest::addColumn<float>("localTime");
++ QTest::addColumn<ClipResults>("expectedResults");
++
++ Handler *handler;
++ AnimationClip *clip;
++ float localTime;
++ ClipResults expectedResults;
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ localTime = 0.0f;
++ expectedResults = QVector<float> { 0.0f, 0.0f, 0.0f };
++
++ QTest::newRow("clip1.json, t = 0.0")
++ << handler << clip << localTime << expectedResults;
++ expectedResults.clear();
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ localTime = clip->duration();
++ expectedResults = QVector<float> { 5.0f, 0.0f, 0.0f };
++
++ QTest::newRow("clip1.json, t = duration")
++ << handler << clip << localTime << expectedResults;
++ expectedResults.clear();
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ localTime = clip->duration() / 2.0f;
++ expectedResults = QVector<float> { 2.5f, 0.0f, 0.0f };
++
++ QTest::newRow("clip1.json, t = duration/2")
++ << handler << clip << localTime << expectedResults;
++ expectedResults.clear();
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
++ localTime = 0.0f;
++ expectedResults = QVector<float> { 0.0f, 0.0f, 0.0f, // Translation
++ 1.0f, 0.0f, 0.0f, 0.0f }; // Rotation
++
++ QTest::newRow("clip2.json, t = 0.0")
++ << handler << clip << localTime << expectedResults;
++ expectedResults.clear();
++ }
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
++ localTime = clip->duration();
++ expectedResults = QVector<float> { 5.0f, 0.0f, 0.0f, // Translation
++ 0.0f, 0.0f, -1.0f, 0.0f }; // Rotation
++
++ QTest::newRow("clip2.json, t = duration")
++ << handler << clip << localTime << expectedResults;
++ expectedResults.clear();
++ }
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
++ localTime = clip->duration() / 2.0f;
++ expectedResults = QVector<float> { 2.5f, 0.0f, 0.0f, // Translation
++ 0.5f, 0.0f, -0.5f, 0.0f }; // Rotation
++
++ QTest::newRow("clip2.json, t = duration/2")
++ << handler << clip << localTime << expectedResults;
++ expectedResults.clear();
++ }
++ {
++ // a clip with linear interpolation
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip4.json"));
++ localTime = clip->duration();
++ expectedResults = QVector<float> { 5.0, -2.0f, 6.0f };
++
++ QTest::newRow("clip4.json, linear, t = duration")
++ << handler << clip << localTime << expectedResults;
++ expectedResults.clear();
++ }
++ {
++ // a clip with linear interpolation
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip4.json"));
++ localTime = clip->duration() / 2.0f;
++ expectedResults = QVector<float> { 2.5f, -1.0f, 3.0f };
++
++ QTest::newRow("clip4.json, linear, t = duration/2")
++ << handler << clip << localTime << expectedResults;
++ expectedResults.clear();
++ }
++ {
++ // a clip with slerp interpolation
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip6.json"));
++ localTime = clip->duration() / 2.0f;
++ expectedResults = QVector<float> { 0.923822f, 0.382626f, 0.0f, 0.0f };
++
++ QTest::newRow("clip6.json, slerp, t = duration/2")
++ << handler << clip << localTime << expectedResults;
++ expectedResults.clear();
++ }
++ }
++
++ void checkEvaluateClipAtLocalTime()
++ {
++ // GIVEN
++ QFETCH(Handler *, handler);
++ QFETCH(AnimationClip *, clip);
++ QFETCH(float, localTime);
++ QFETCH(ClipResults, expectedResults);
++
++ // WHEN
++ const ClipResults actualResults = evaluateClipAtLocalTime(clip, localTime);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (int i = 0; i < actualResults.size(); ++i) {
++ auto actual = actualResults[i];
++ auto expected = expectedResults[i];
++
++ QVERIFY(fuzzyCompare(actual, expected) == true);
++ }
++
++ // Cleanup
++ delete handler;
++ }
++
++ void checkEvaluateClipAtPhase_data()
++ {
++ QTest::addColumn<Handler *>("handler");
++ QTest::addColumn<AnimationClip *>("clip");
++ QTest::addColumn<float>("phase");
++ QTest::addColumn<ClipResults>("expectedResults");
++
++ Handler *handler;
++ AnimationClip *clip;
++ float phase;
++ ClipResults expectedResults;
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ phase = 0.0f;
++ expectedResults = QVector<float> { 0.0f, 0.0f, 0.0f };
++
++ QTest::newRow("clip1.json, phi = 0.0")
++ << handler << clip << phase << expectedResults;
++ expectedResults.clear();
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ phase = 1.0f;
++ expectedResults = QVector<float> { 5.0f, 0.0f, 0.0f };
++
++ QTest::newRow("clip1.json, phi = 1.0")
++ << handler << clip << phase << expectedResults;
++ expectedResults.clear();
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ phase = 0.5f;
++ expectedResults = QVector<float> { 2.5f, 0.0f, 0.0f };
++
++ QTest::newRow("clip1.json, phi = 0.5")
++ << handler << clip << phase << expectedResults;
++ expectedResults.clear();
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
++ phase = 0.0f;
++ expectedResults = QVector<float> { 0.0f, 0.0f, 0.0f, // Translation
++ 1.0f, 0.0f, 0.0f, 0.0f }; // Rotation
++
++ QTest::newRow("clip2.json, phi = 0.0")
++ << handler << clip << phase << expectedResults;
++ expectedResults.clear();
++ }
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
++ phase = 1.0f;
++ expectedResults = QVector<float> { 5.0f, 0.0f, 0.0f, // Translation
++ 0.0f, 0.0f, -1.0f, 0.0f }; // Rotation
++
++ QTest::newRow("clip2.json, t = 1.0")
++ << handler << clip << phase << expectedResults;
++ expectedResults.clear();
++ }
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
++ phase = 0.5f;
++ expectedResults = QVector<float> { 2.5f, 0.0f, 0.0f, // Translation
++ 0.5f, 0.0f, -0.5f, 0.0f }; // Rotation
++
++ QTest::newRow("clip2.json, phi = 0.5")
++ << handler << clip << phase << expectedResults;
++ expectedResults.clear();
++ }
++ }
++
++ void checkEvaluateClipAtPhase()
++ {
++ // GIVEN
++ QFETCH(Handler *, handler);
++ QFETCH(AnimationClip *, clip);
++ QFETCH(float, phase);
++ QFETCH(ClipResults, expectedResults);
++
++ // WHEN
++ const ClipResults actualResults = evaluateClipAtPhase(clip, phase);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (int i = 0; i < actualResults.size(); ++i) {
++ auto actual = actualResults[i];
++ auto expected = expectedResults[i];
++
++ QVERIFY(fuzzyCompare(actual, expected) == true);
++ }
++
++ // Cleanup
++ delete handler;
++ }
++
++ void checkChannelComponentsToIndicesHelper_data()
++ {
++ QTest::addColumn<Channel>("channel");
++ QTest::addColumn<int>("dataType");
++ QTest::addColumn<qsizetype>("expectedChannelComponentCount");
++ QTest::addColumn<qsizetype>("offset");
++ QTest::addColumn<QList<char>>("suffixes");
++ QTest::addColumn<QList<qsizetype>>("expectedResults");
++
++ Channel channel;
++ int dataType;
++ qsizetype expectedChannelComponentCount;
++ qsizetype offset;
++ QList<char> suffixes;
++ QList<qsizetype> expectedResults;
++
++ // already sorted vec3, no component names, with and without offset
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Location");
++ channel.channelComponents.resize(3);
++ // leave 'name' empty
++
++ dataType = static_cast<int>(QMetaType::QVector3D);
++ expectedChannelComponentCount = 3;
++ offset = 0;
++ // suffixes expected to be ignored
++ expectedResults = QList<qsizetype>{ 0, 1, 2 };
++
++ QTest::newRow("vec3 location, pre-sorted, no component names, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 4;
++ expectedResults = QList<qsizetype>{ 4, 5, 6 };
++ QTest::newRow("vec3 location, pre-sorted, no component names, offset = 4")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++ expectedResults.clear();
++ }
++
++ // vec3 with and without offset
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Location");
++ channel.channelComponents.resize(3);
++ channel.channelComponents[0].name = QLatin1String("Location X");
++ channel.channelComponents[1].name = QLatin1String("Location Y");
++ channel.channelComponents[2].name = QLatin1String("Location Z");
++
++ dataType = static_cast<int>(QMetaType::QVector3D);
++ expectedChannelComponentCount = 3;
++ offset = 0;
++ suffixes = QList<char> { 'X', 'Y', 'Z', 'W' };
++ expectedResults = QList<qsizetype>{ 0, 1, 2 };
++
++ QTest::newRow("vec3 location, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 4;
++ expectedResults = QList<qsizetype>{ 4, 5, 6 };
++ QTest::newRow("vec3 location, offset = 4")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ suffixes.clear();
++ expectedResults.clear();
++ }
++
++ // vec2 with and without offset
++ {
++ channel = Channel();
++ channel.name = QLatin1String("pos");
++ channel.channelComponents.resize(2);
++ channel.channelComponents[0].name = QLatin1String("pos X");
++ channel.channelComponents[1].name = QLatin1String("pos Y");
++
++ dataType = static_cast<int>(QMetaType::QVector2D);
++ expectedChannelComponentCount = 2;
++ offset = 0;
++ suffixes = QList<char> { 'X', 'Y', 'Z', 'W' };
++ expectedResults = QList<qsizetype>{ 0, 1 };
++
++ QTest::newRow("vec2 pos, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 2;
++ expectedResults = QList<qsizetype>{ 2, 3 };
++ QTest::newRow("vec2 pos, offset = 2")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ suffixes.clear();
++ expectedResults.clear();
++ }
++
++ // vec4 with and without offset
++ {
++ channel = Channel();
++ channel.name = QLatin1String("foo");
++ channel.channelComponents.resize(4);
++ channel.channelComponents[0].name = QLatin1String("foo X");
++ channel.channelComponents[1].name = QLatin1String("foo Y");
++ channel.channelComponents[2].name = QLatin1String("foo Z");
++ channel.channelComponents[3].name = QLatin1String("foo W");
++
++ dataType = static_cast<int>(QMetaType::QVector4D);
++ expectedChannelComponentCount = 4;
++ offset = 0;
++ suffixes = QList<char> { 'X', 'Y', 'Z', 'W' };
++ expectedResults = QList<qsizetype>{ 0, 1, 2, 3 };
++
++ QTest::newRow("vec4 foo, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 10, 11, 12, 13 };
++ QTest::newRow("vec4 foo, offset = 10")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ suffixes.clear();
++ expectedResults.clear();
++ }
++
++ // double with and without offset
++ {
++ channel = Channel();
++ channel.name = QLatin1String("foo");
++ channel.channelComponents.resize(1);
++ channel.channelComponents[0].name = QLatin1String("Mass X");
++
++ dataType = static_cast<int>(QMetaType::Double);
++ expectedChannelComponentCount = 1;
++ offset = 0;
++ suffixes = QList<char> { 'X', 'Y', 'Z', 'W' };
++ expectedResults = QList<qsizetype>{ 0 };
++
++ QTest::newRow("double Mass, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 5;
++ expectedResults = QList<qsizetype>{ 5 };
++ QTest::newRow("double Mass, offset = 5")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ suffixes.clear();
++ expectedResults.clear();
++ }
++
++ // quaternion with and without offset
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Rotation");
++ channel.channelComponents.resize(4);
++ channel.channelComponents[0].name = QLatin1String("Rotation W");
++ channel.channelComponents[1].name = QLatin1String("Rotation X");
++ channel.channelComponents[2].name = QLatin1String("Rotation Y");
++ channel.channelComponents[3].name = QLatin1String("Rotation Z");
++
++ dataType = static_cast<int>(QMetaType::QQuaternion);
++ expectedChannelComponentCount = 4;
++ offset = 0;
++ suffixes = QList<char> { 'W', 'X', 'Y', 'Z' };
++ expectedResults = QList<qsizetype>{ 0, 1, 2, 3 };
++
++ QTest::newRow("quaternion Rotation, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 10, 11, 12, 13 };
++ QTest::newRow("quaternion Rotation, offset = 10")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ suffixes.clear();
++ expectedResults.clear();
++ }
++
++ // quaternion with and without offset, randomized
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Rotation");
++ channel.channelComponents.resize(4);
++ channel.channelComponents[0].name = QLatin1String("Rotation X");
++ channel.channelComponents[1].name = QLatin1String("Rotation W");
++ channel.channelComponents[2].name = QLatin1String("Rotation Z");
++ channel.channelComponents[3].name = QLatin1String("Rotation Y");
++
++ dataType = static_cast<int>(QMetaType::QQuaternion);
++ expectedChannelComponentCount = 4;
++ offset = 0;
++ suffixes = QList<char> { 'W', 'X', 'Y', 'Z' };
++ expectedResults = QList<qsizetype>{ 1, 0, 3, 2 };
++
++ QTest::newRow("quaternion Rotation, offset = 0, randomized")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 11, 10, 13, 12 };
++ QTest::newRow("quaternion Rotation, offset = 10, randomized")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ suffixes.clear();
++ expectedResults.clear();
++ }
++
++ // color with and without offset 3 components
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Color");
++ channel.channelComponents.resize(3);
++ channel.channelComponents[0].name = QLatin1String("Color R");
++ channel.channelComponents[1].name = QLatin1String("Color G");
++ channel.channelComponents[2].name = QLatin1String("Color B");
++
++ dataType = static_cast<int>(QMetaType::QColor);
++ expectedChannelComponentCount = 3;
++ offset = 0;
++ suffixes = QList<char> { 'R', 'G', 'B' };
++ expectedResults = QList<qsizetype>{ 0, 1, 2 };
++
++ QTest::newRow("QColor RGB Color, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 10, 11, 12 };
++ QTest::newRow("QColor RGB Color, offset = 10")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ suffixes.clear();
++ expectedResults.clear();
++ }
++
++ // color with and without offset 4 components
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Color");
++ channel.channelComponents.resize(4);
++ channel.channelComponents[0].name = QLatin1String("Color R");
++ channel.channelComponents[1].name = QLatin1String("Color G");
++ channel.channelComponents[2].name = QLatin1String("Color B");
++ channel.channelComponents[3].name = QLatin1String("Color A");
++
++ dataType = static_cast<int>(QMetaType::QColor);
++ expectedChannelComponentCount = 4;
++ offset = 0;
++ suffixes = QList<char> { 'R', 'G', 'B', 'A' };
++ expectedResults = QList<qsizetype>{ 0, 1, 2, 3 };
++
++ QTest::newRow("QColor RGBA Color, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 10, 11, 12, 13 };
++ QTest::newRow("QColor RGBA Color, offset = 10")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++
++ suffixes.clear();
++ expectedResults.clear();
++ }
++
++ // weights as list with and without offset
++ {
++ channel = Channel();
++ channel.name = QLatin1String("MorphWeights");
++ channel.channelComponents.resize(6);
++ // leave channel component names empty
++
++ dataType = static_cast<int>(QMetaType::QVariantList);
++ expectedChannelComponentCount = 6;
++ offset = 0;
++ // suffixes expected to be ignored
++ expectedResults = QList<qsizetype>{ 0, 1, 2, 3, 4, 5 };
++
++ QTest::newRow("MorphWeights List count = 6, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 10, 11, 12, 13, 14, 15 };
++ QTest::newRow("MorphWeights List count = 6, offset = 10")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++ suffixes.clear();
++ expectedResults.clear();
++ }
++
++ // weights as vec of float with and without offset
++ {
++ channel = Channel();
++ channel.name = QLatin1String("MorphWeights");
++ channel.channelComponents.resize(6);
++ // leave channel component names empty
++
++ dataType = qMetaTypeId<QList<float>>();
++ expectedChannelComponentCount = 6;
++ offset = 0;
++ // suffixes expected to be ignored
++ expectedResults = QList<qsizetype>{ 0, 1, 2, 3, 4, 5 };
++
++ QTest::newRow("MorphWeights Vec count = 6, offset = 0")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 10, 11, 12, 13, 14, 15 };
++ QTest::newRow("MorphWeights Vec count = 6, offset = 10")
++ << channel << dataType << expectedChannelComponentCount
++ << offset << suffixes << expectedResults;
++ suffixes.clear();
++ expectedResults.clear();
++ }
++ }
++
++ void checkChannelComponentsToIndicesHelper()
++ {
++ // GIVEN
++ QFETCH(Channel, channel);
++ QFETCH(qsizetype, offset);
++ QFETCH(qsizetype, expectedChannelComponentCount);
++ QFETCH(QList<char>, suffixes);
++ QFETCH(QList<qsizetype>, expectedResults);
++
++ // WHEN
++ const QVector<qsizetype> actualResults = channelComponentsToIndicesHelper(channel,
++ expectedChannelComponentCount, offset, suffixes);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (qsizetype i = 0; i < actualResults.size(); ++i) {
++ QCOMPARE(actualResults[i], expectedResults[i]);
++ }
++ }
++
++ void checkChannelComponentsToIndices_data()
++ {
++ QTest::addColumn<Channel>("channel");
++ QTest::addColumn<int>("dataType");
++ QTest::addColumn<qsizetype>("componentCount");
++ QTest::addColumn<qsizetype>("offset");
++ QTest::addColumn<QList<qsizetype>>("expectedResults");
++
++ Channel channel;
++ int dataType;
++ qsizetype componentCount;
++ qsizetype offset;
++ QList<qsizetype> expectedResults;
++
++ // Quaternion
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Rotation");
++ channel.channelComponents.resize(4);
++ channel.channelComponents[0].name = QLatin1String("Rotation W");
++ channel.channelComponents[1].name = QLatin1String("Rotation X");
++ channel.channelComponents[2].name = QLatin1String("Rotation Y");
++ channel.channelComponents[3].name = QLatin1String("Rotation Z");
++
++ dataType = static_cast<int>(QMetaType::QQuaternion);
++ componentCount = 4;
++ offset = 0;
++ expectedResults = QList<qsizetype>{ 0, 1, 2, 3 };
++
++ QTest::newRow("quaternion Rotation, offset = 0")
++ << channel << dataType << componentCount << offset << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 10, 11, 12, 13 };
++ QTest::newRow("quaternion Rotation, offset = 10")
++ << channel << dataType << componentCount << offset << expectedResults;
++
++ expectedResults.clear();
++ }
++
++ // vec3 with and without offset
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Location");
++ channel.channelComponents.resize(3);
++ channel.channelComponents[0].name = QLatin1String("Location X");
++ channel.channelComponents[1].name = QLatin1String("Location Y");
++ channel.channelComponents[2].name = QLatin1String("Location Z");
++
++ dataType = static_cast<int>(QMetaType::QVector3D);
++ componentCount = 3;
++ offset = 0;
++ expectedResults = QList<qsizetype>{ 0, 1, 2 };
++
++ QTest::newRow("vec3 location, offset = 0")
++ << channel << dataType << componentCount << offset << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 4;
++ expectedResults = QList<qsizetype>{ 4, 5, 6 };
++ QTest::newRow("vec3 location, offset = 4")
++ << channel << dataType << componentCount << offset << expectedResults;
++
++ expectedResults.clear();
++ }
++
++ // QColor
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Color");
++ channel.channelComponents.resize(3);
++ channel.channelComponents[0].name = QLatin1String("Color R");
++ channel.channelComponents[1].name = QLatin1String("Color G");
++ channel.channelComponents[2].name = QLatin1String("Color B");
++
++ dataType = static_cast<int>(QMetaType::QColor);
++ componentCount = 3;
++ offset = 0;
++ expectedResults = QList<qsizetype>{ 0, 1, 2 };
++
++ QTest::newRow("QColor RGB Color, offset = 0")
++ << channel << dataType << componentCount << offset << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 10, 11, 12 };
++ QTest::newRow("QColor RGB Color, offset = 10")
++ << channel << dataType << componentCount << offset << expectedResults;
++
++ expectedResults.clear();
++ }
++
++ {
++ channel = Channel();
++ channel.name = QLatin1String("Color");
++ channel.channelComponents.resize(4);
++ channel.channelComponents[0].name = QLatin1String("Color R");
++ channel.channelComponents[1].name = QLatin1String("Color G");
++ channel.channelComponents[2].name = QLatin1String("Color B");
++ channel.channelComponents[3].name = QLatin1String("Color A");
++
++ dataType = static_cast<int>(QMetaType::QColor);
++ componentCount = 4;
++ offset = 0;
++ expectedResults = QList<qsizetype>{ 0, 1, 2, 3 };
++
++ QTest::newRow("QColor RGBA Color, offset = 0")
++ << channel << dataType << componentCount << offset << expectedResults;
++
++ expectedResults.clear();
++
++ offset = 10;
++ expectedResults = QList<qsizetype>{ 10, 11, 12, 13 };
++ QTest::newRow("QColor RGBA Color, offset = 10")
++ << channel << dataType << componentCount << offset << expectedResults;
++
++ expectedResults.clear();
++ }
++ }
++
++ void checkChannelComponentsToIndices()
++ {
++ QFETCH(Channel, channel);
++ QFETCH(int, dataType);
++ QFETCH(qsizetype, componentCount);
++ QFETCH(qsizetype, offset);
++ QFETCH(QList<qsizetype>, expectedResults);
++
++ // WHEN
++ const QVector<qsizetype> actualResults = channelComponentsToIndices(channel,
++ dataType, componentCount, offset);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (qsizetype i = 0; i < actualResults.size(); ++i) {
++ QCOMPARE(actualResults[i], expectedResults[i]);
++ }
++ }
++
++ void checkEvaluationDataForClip_data()
++ {
++ QTest::addColumn<Handler *>("handler");
++ QTest::addColumn<AnimationClip *>("clip");
++ QTest::addColumn<AnimatorEvaluationData>("animatorData");
++ QTest::addColumn<ClipEvaluationData>("expectedClipData");
++
++ Handler *handler;
++ AnimationClip *clip;
++ AnimatorEvaluationData animatorData;
++ ClipEvaluationData clipData;
++ auto* clock = new Clock;
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ const qint64 globalStartTimeNS = 0;
++ const int loops = 1;
++ auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ animator->setCurrentLoop(0);
++ clipData.currentLoop = animator->currentLoop();
++ const qint64 elapsedTimeNS = 0;
++ animatorData = evaluationDataForAnimator(animator, clock, elapsedTimeNS); // Tested elsewhere
++
++ clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
++ animatorData.elapsedTime,
++ animatorData.playbackRate,
++ clip->duration(),
++ animatorData.loopCount,
++ clipData.currentLoop); // Tested elsewhere
++ clipData.isFinalFrame = false;
++
++ QTest::newRow("clip1.json, globalTime = 0")
++ << handler << clip << animatorData << clipData;
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ const qint64 globalStartTimeNS = 0;
++ const int loops = 1;
++ auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ animator->setCurrentLoop(0);
++ clipData.currentLoop = animator->currentLoop();
++ const qint64 elapsedTimeNS = toNsecs(clip->duration()+1); // +1 to ensure beyond end
++ animatorData = evaluationDataForAnimator(animator, nullptr, elapsedTimeNS); // Tested elsewhere
++
++ clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
++ animatorData.elapsedTime,
++ animatorData.playbackRate,
++ clip->duration(),
++ animatorData.loopCount,
++ clipData.currentLoop); // Tested elsewhere
++ clipData.isFinalFrame = true;
++
++ QTest::newRow("clip1.json, elapsedTime = duration + 1")
++ << handler << clip << animatorData << clipData;
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ const qint64 globalStartTimeNS = 0;
++ const int loops = 0; // Infinite loops
++ auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ animator->setCurrentLoop(0);
++ clipData.currentLoop = animator->currentLoop();
++ const qint64 elapsedTimeNS = toNsecs(2.0 * clip->duration());
++ animatorData = evaluationDataForAnimator(animator, clock, elapsedTimeNS); // Tested elsewhere
++
++ clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
++ animatorData.elapsedTime,
++ animatorData.playbackRate,
++ clip->duration(),
++ animatorData.loopCount,
++ clipData.currentLoop); // Tested elsewhere
++ clipData.isFinalFrame = false;
++
++ QTest::newRow("clip1.json, elapsedTime = 2 * duration, loops = infinite")
++ << handler << clip << animatorData << clipData;
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ const qint64 globalStartTimeNS = 0;
++ const int loops = 2;
++ auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ animator->setCurrentLoop(0);
++ clipData.currentLoop = animator->currentLoop();
++ const qint64 elapsedTimeNS = toNsecs(2.0 * clip->duration() + 1.0); // +1 to ensure beyond end of clip
++ animatorData = evaluationDataForAnimator(animator, nullptr, elapsedTimeNS); // Tested elsewhere
++
++ clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
++ animatorData.elapsedTime,
++ animatorData.playbackRate,
++ clip->duration(),
++ animatorData.loopCount,
++ clipData.currentLoop); // Tested elsewhere
++ clipData.isFinalFrame = true;
++
++ QTest::newRow("clip1.json, elapsedTime = 2 * duration + 1, loops = 2")
++ << handler << clip << animatorData << clipData;
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ const qint64 globalStartTimeNS = 0;
++ const int loops = 2;
++ auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ animator->setCurrentLoop(1);
++ clipData.currentLoop = animator->currentLoop();
++ const qint64 elapsedTimeNS = toNsecs(clip->duration() + 1.0); // +1 to ensure beyond end of clip
++ animatorData = evaluationDataForAnimator(animator, nullptr, elapsedTimeNS); // Tested elsewhere
++
++ clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
++ animatorData.elapsedTime,
++ animatorData.playbackRate,
++ clip->duration(),
++ animatorData.loopCount,
++ clipData.currentLoop); // Tested elsewhere
++ clipData.isFinalFrame = true;
++
++ QTest::newRow("clip1.json, elapsedTime = duration + 1, loops = 2, current_loop = 1")
++ << handler << clip << animatorData << clipData;
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ const qint64 globalStartTimeNS = clip->duration();
++ const int loops = 1;
++ auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ animator->setCurrentLoop(1);
++ clipData.currentLoop = animator->currentLoop();
++ const qint64 elapsedTimeNS = toNsecs(clip->duration() * 0.5); // +1 to ensure beyond end of clip
++
++ Clock clock;
++ clock.setPlaybackRate(-1.0);
++
++ animatorData = evaluationDataForAnimator(animator, &clock, elapsedTimeNS); // Tested elsewhere
++
++ clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
++ animatorData.elapsedTime,
++ animatorData.playbackRate,
++ clip->duration(),
++ animatorData.loopCount,
++ clipData.currentLoop); // Tested elsewhere
++ clipData.isFinalFrame = false;
++
++ QTest::newRow("clip1.json, elapsedTime = duration / 2, loops = 1, current_loop = 1, playback_rate = -1")
++ << handler << clip << animatorData << clipData;
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ const qint64 globalStartTimeNS = clip->duration();
++ const int loops = 1;
++ auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ animator->setCurrentLoop(1);
++ clipData.currentLoop = animator->currentLoop();
++ const qint64 elapsedTimeNS = toNsecs(clip->duration() + 1); // +1 to ensure beyond end of clip
++
++ Clock clock;
++ clock.setPlaybackRate(-1.0);
++
++ animatorData = evaluationDataForAnimator(animator, &clock, elapsedTimeNS); // Tested elsewhere
++
++ clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
++ animatorData.elapsedTime,
++ animatorData.playbackRate,
++ clip->duration(),
++ animatorData.loopCount,
++ clipData.currentLoop); // Tested elsewhere
++ clipData.isFinalFrame = true;
++
++ QTest::newRow("clip1.json, elapsedTime = duration + 1, loops = 1, current_loop = 1, playback_rate = -1")
++ << handler << clip << animatorData << clipData;
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ const qint64 globalStartTimeNS = clip->duration();
++ const int loops = 2;
++ auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ animator->setCurrentLoop(0);
++ clipData.currentLoop = animator->currentLoop();
++ const qint64 elapsedTimeNS = toNsecs(clip->duration() + 1); // +1 to ensure beyond end of clip
++
++ Clock clock;
++ clock.setPlaybackRate(-1.0);
++
++ animatorData = evaluationDataForAnimator(animator, &clock, elapsedTimeNS); // Tested elsewhere
++
++ clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
++ animatorData.elapsedTime,
++ animatorData.playbackRate,
++ clip->duration(),
++ animatorData.loopCount,
++ clipData.currentLoop); // Tested elsewhere
++ clipData.isFinalFrame = true;
++
++ QTest::newRow("clip1.json, elapsedTime = duration + 1, loops = 2, current_loop = 0, playback_rate = -1")
++ << handler << clip << animatorData << clipData;
++ }
++
++ {
++ handler = new Handler();
++ clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
++ const qint64 globalStartTimeNS = clip->duration();
++ const int loops = 2;
++ auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ animator->setCurrentLoop(1);
++ clipData.currentLoop = animator->currentLoop();
++ const qint64 elapsedTimeNS = toNsecs(clip->duration() * 2.0 + 1); // +1 to ensure beyond end of clip
++
++ Clock clock;
++ clock.setPlaybackRate(-1.0);
++
++ animatorData = evaluationDataForAnimator(animator, &clock, elapsedTimeNS); // Tested elsewhere
++
++ clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
++ animatorData.elapsedTime,
++ animatorData.playbackRate,
++ clip->duration(),
++ animatorData.loopCount,
++ clipData.currentLoop); // Tested elsewhere
++ clipData.isFinalFrame = true;
++
++ QTest::newRow("clip1.json, elapsedTime = duration + 1, loops = 2, current_loop = 1, playback_rate = -1")
++ << handler << clip << animatorData << clipData;
++ }
++ }
++
++ void checkEvaluationDataForClip()
++ {
++ // GIVEN
++ QFETCH(Handler *, handler);
++ QFETCH(AnimationClip *, clip);
++ QFETCH(AnimatorEvaluationData, animatorData);
++ QFETCH(ClipEvaluationData, expectedClipData);
++
++ // WHEN
++ ClipEvaluationData actualClipData = evaluationDataForClip(clip, animatorData);
++
++ // THEN
++ QCOMPARE(actualClipData.currentLoop, expectedClipData.currentLoop);
++ QVERIFY(fuzzyCompare(actualClipData.localTime, expectedClipData.localTime) == true);
++ QCOMPARE(actualClipData.isFinalFrame, expectedClipData.isFinalFrame);
++
++ // Cleanup
++ delete handler;
++ }
++
++ void checkEvaluationDataForAnimator_data()
++ {
++ QTest::addColumn<Handler *>("handler");
++ QTest::addColumn<ClipAnimator *>("animator");
++ QTest::addColumn<qint64>("elapsedTime");
++ QTest::addColumn<AnimatorEvaluationData>("expectedAnimatorData");
++
++ Handler *handler;
++ ClipAnimator *animator;
++ qint64 elapsedTimeNS;
++ AnimatorEvaluationData expectedAnimatorData;
++
++ {
++ handler = new Handler();
++ const qint64 globalStartTimeNS = 0;
++ const int loops = 1;
++ animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ elapsedTimeNS = 0;
++
++ expectedAnimatorData.loopCount = loops;
++ expectedAnimatorData.playbackRate = 1.0; // hard-wired for now
++ expectedAnimatorData.elapsedTime = 0.0;
++
++ QTest::newRow("globalStartTime = 0, elapsedTime = 0, loops = 1")
++ << handler << animator << elapsedTimeNS << expectedAnimatorData;
++ }
++
++ {
++ handler = new Handler();
++ const qint64 globalStartTimeNS = 0;
++ const int loops = 5;
++ animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ elapsedTimeNS = 0;
++
++ expectedAnimatorData.loopCount = loops;
++ expectedAnimatorData.playbackRate = 1.0; // hard-wired for now
++ expectedAnimatorData.elapsedTime = 0.0;
++
++ QTest::newRow("globalStartTime = 0, elapsedTime = 0, loops = 5")
++ << handler << animator << elapsedTimeNS << expectedAnimatorData;
++ }
++
++ {
++ handler = new Handler();
++ const qint64 globalStartTimeNS = 0;
++ const int loops = 1;
++ animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ elapsedTimeNS = 5000000000;
++
++ expectedAnimatorData.loopCount = loops;
++ expectedAnimatorData.playbackRate = 1.0; // hard-wired for now
++ expectedAnimatorData.elapsedTime = 5.0;
++
++ QTest::newRow("globalStartTime = 0, elapsedTime = 5, loops = 1")
++ << handler << animator << elapsedTimeNS << expectedAnimatorData;
++ }
++
++ {
++ handler = new Handler();
++ const qint64 globalStartTimeNS = 3000000000;
++ const int loops = 1;
++ animator = createClipAnimator(handler, globalStartTimeNS, loops);
++ elapsedTimeNS = 2000000000;
++
++ expectedAnimatorData.loopCount = loops;
++ expectedAnimatorData.playbackRate = 1.0; // hard-wired for now
++ expectedAnimatorData.elapsedTime = 2.0;
++
++ QTest::newRow("globalStartTime = 3, elapsedTime = 2, loops = 1")
++ << handler << animator << elapsedTimeNS << expectedAnimatorData;
++ }
++ }
++
++ void checkEvaluationDataForAnimator()
++ {
++ // GIVEN
++ QFETCH(Handler *, handler);
++ QFETCH(ClipAnimator *, animator);
++ QFETCH(qint64, elapsedTime);
++ QFETCH(AnimatorEvaluationData, expectedAnimatorData);
++
++ // WHEN
++ AnimatorEvaluationData actualAnimatorData = evaluationDataForAnimator(animator, nullptr, elapsedTime);
++
++ // THEN
++ QCOMPARE(actualAnimatorData.loopCount, expectedAnimatorData.loopCount);
++ QVERIFY(fuzzyCompare(actualAnimatorData.playbackRate, expectedAnimatorData.playbackRate) == true);
++ QVERIFY(fuzzyCompare(actualAnimatorData.elapsedTime, expectedAnimatorData.elapsedTime) == true);
++
++ // Cleanup
++ delete handler;
++ }
++
++ void checkGatherValueNodesToEvaluate_data()
++ {
++ QTest::addColumn<Handler *>("handler");
++ QTest::addColumn<Qt3DCore::QNodeId>("blendTreeRootId");
++ QTest::addColumn<QList<Qt3DCore::QNodeId>>("expectedIds");
++
++ {
++ Handler *handler = new Handler;
++
++ const auto lerp = createLerpClipBlend(handler);
++ const auto value1 = createClipBlendValue(handler);
++ const auto clip1Id = Qt3DCore::QNodeId::createId();
++ value1->setClipId(clip1Id);
++ lerp->setStartClipId(value1->peerId());
++
++ const auto value2 = createClipBlendValue(handler);
++ const auto clip2Id = Qt3DCore::QNodeId::createId();
++ value2->setClipId(clip2Id);
++ lerp->setEndClipId(value2->peerId());
++
++ const QList<Qt3DCore::QNodeId> expectedIds = { value1->peerId(), value2->peerId() };
++
++ QTest::newRow("simple lerp") << handler << lerp->peerId() << expectedIds;
++ }
++
++ {
++ Handler *handler = new Handler;
++
++ const auto value1 = createClipBlendValue(handler);
++ const auto clip1Id = Qt3DCore::QNodeId::createId();
++ value1->setClipId(clip1Id);
++
++ const QList<Qt3DCore::QNodeId> expectedIds = { value1->peerId() };
++
++ QTest::newRow("value only") << handler << value1->peerId() << expectedIds;
++ }
++ }
++
++ void checkGatherValueNodesToEvaluate()
++ {
++ // GIVEN
++ QFETCH(Handler *, handler);
++ QFETCH(Qt3DCore::QNodeId, blendTreeRootId);
++ QFETCH(QList<Qt3DCore::QNodeId>, expectedIds);
++
++ // WHEN
++ const QVector<Qt3DCore::QNodeId> actualIds = gatherValueNodesToEvaluate(handler, blendTreeRootId);
++
++ // THEN
++ QCOMPARE(actualIds.size(), expectedIds.size());
++ for (int i = 0; i < actualIds.size(); ++i)
++ QCOMPARE(actualIds[i], expectedIds[i]);
++
++ // Cleanup
++ delete handler;
++ }
++
++ void checkEvaluateBlendTree_data()
++ {
++ QTest::addColumn<Handler *>("handler");
++ QTest::addColumn<BlendedClipAnimator *>("animator");
++ QTest::addColumn<Qt3DCore::QNodeId>("blendNodeId");
++ QTest::addColumn<ClipResults>("expectedResults");
++
++ {
++ /*
++ ValueNode1----
++ |
++ MeanBlendNode
++ |
++ ValueNode2----
++ */
++
++ auto handler = new Handler();
++ const qint64 globalStartTimeNS = 0;
++ const int loopCount = 1;
++ auto animator = createBlendedClipAnimator(handler, globalStartTimeNS, loopCount);
++
++ // Set up the blend node and dependencies (evaluated clip results of the
++ // dependent nodes in the animator indexed by their ids).
++ MeanBlendNode *blendNode = createMeanBlendNode(handler);
++
++ // First clip to use in the mean
++ auto valueNode1 = createClipBlendValue(handler);
++ ClipResults valueNode1Results = { 0.0f, 0.0f, 0.0f };
++ valueNode1->setClipResults(animator->peerId(), valueNode1Results);
++
++ // Second clip to use in the mean
++ auto valueNode2 = createClipBlendValue(handler);
++ ClipResults valueNode2Results = { 1.0f, 1.0f, 1.0f };
++ valueNode2->setClipResults(animator->peerId(), valueNode2Results);
++
++ blendNode->setValueNodeIds(valueNode1->peerId(), valueNode2->peerId());
++
++ ClipResults expectedResults = { 0.5f, 0.5f, 0.5f };
++
++ QTest::newRow("mean node, 1 channel")
++ << handler << animator << blendNode->peerId() << expectedResults;
++ }
++
++ {
++ /*
++ ValueNode1----
++ |
++ MeanBlendNode
++ |
++ ValueNode2----
++ */
++
++ auto handler = new Handler();
++ const qint64 globalStartTimeNS = 0;
++ const int loopCount = 1;
++ auto animator = createBlendedClipAnimator(handler, globalStartTimeNS, loopCount);
++
++ // Set up the blend node and dependencies (evaluated clip results of the
++ // dependent nodes in the animator indexed by their ids).
++ MeanBlendNode *blendNode = createMeanBlendNode(handler);
++
++ // First clip to use in the mean
++ auto valueNode1 = createClipBlendValue(handler);
++ ClipResults valueNode1Results = { 0.0f, 0.0f, 0.0f, 1.0f, 2.0f, 3.0f };
++ valueNode1->setClipResults(animator->peerId(), valueNode1Results);
++
++ // Second clip to use in the mean
++ auto valueNode2 = createClipBlendValue(handler);
++ ClipResults valueNode2Results = { 1.0f, 1.0f, 1.0f, 2.0f, 4.0f, 6.0f };
++ valueNode2->setClipResults(animator->peerId(), valueNode2Results);
++
++ blendNode->setValueNodeIds(valueNode1->peerId(), valueNode2->peerId());
++
++ ClipResults expectedResults = { 0.5f, 0.5f, 0.5f, 1.5f, 3.0f, 4.5f };
++
++ QTest::newRow("mean node, 2 channels")
++ << handler << animator << blendNode->peerId() << expectedResults;
++ }
++
++ {
++ /*
++ ValueNode1----
++ |
++ MeanBlendNode1------
++ | |
++ ValueNode2---- |
++ MeanBlendNode3
++ ValueNode3---- |
++ | |
++ MeanBlendNode2------
++ |
++ ValueNode4----
++ */
++
++ auto handler = new Handler();
++ const qint64 globalStartTimeNS = 0;
++ const int loopCount = 1;
++ auto animator = createBlendedClipAnimator(handler, globalStartTimeNS, loopCount);
++
++ // Set up the blend node and dependencies (evaluated clip results of the
++ // dependent nodes in the animator indexed by their ids).
++
++ // MeanBlendNode1
++ MeanBlendNode *meanNode1 = createMeanBlendNode(handler);
++
++ // First clip to use in mean1
++ auto valueNode1 = createClipBlendValue(handler);
++ ClipResults valueNode1Results = { 0.0f, 0.0f, 0.0f };
++ valueNode1->setClipResults(animator->peerId(), valueNode1Results);
++
++ // Second clip to use in mean1
++ auto valueNode2 = createClipBlendValue(handler);
++ ClipResults valueNode2Results = { 2.0f, 2.0f, 2.0f };
++ valueNode2->setClipResults(animator->peerId(), valueNode2Results);
++
++ meanNode1->setValueNodeIds(valueNode1->peerId(), valueNode2->peerId());
++
++
++ // MeanBlendNode2
++ MeanBlendNode *meanNode2 = createMeanBlendNode(handler);
++
++ // First clip to use in mean1
++ auto valueNode3 = createClipBlendValue(handler);
++ ClipResults valueNode3Results = { 10.0f, 10.0f, 10.0f };
++ valueNode3->setClipResults(animator->peerId(), valueNode3Results);
++
++ // Second clip to use in mean1
++ auto valueNode4 = createClipBlendValue(handler);
++ ClipResults valueNode4Results = { 20.0f, 20.0f, 20.0f };
++ valueNode4->setClipResults(animator->peerId(), valueNode4Results);
++
++ meanNode2->setValueNodeIds(valueNode3->peerId(), valueNode4->peerId());
++
++
++ // MeanBlendNode3
++ MeanBlendNode *meanNode3 = createMeanBlendNode(handler);
++ meanNode3->setValueNodeIds(meanNode1->peerId(), meanNode2->peerId());
++
++ // Mean1 = 1
++ // Mean2 = 15
++ // Mean3 = (1 + 15 ) / 2 = 8
++ ClipResults expectedResults = { 8.0f, 8.0f, 8.0f };
++
++ QTest::newRow("3 mean nodes, 1 channel")
++ << handler << animator << meanNode3->peerId() << expectedResults;
++ }
++
++ {
++ /*
++ ValueNode1----
++ |
++ MeanBlendNode1------
++ | |
++ ValueNode2---- |
++ MeanBlendNode3---
++ ValueNode3---- | |
++ | | |
++ MeanBlendNode2------ AdditiveBlendNode1
++ | |
++ ValueNode4---- |
++ ValueNode5-------
++ */
++
++ auto handler = new Handler();
++ const qint64 globalStartTimeNS = 0;
++ const int loopCount = 1;
++ auto animator = createBlendedClipAnimator(handler, globalStartTimeNS, loopCount);
++
++ // Set up the blend node and dependencies (evaluated clip results of the
++ // dependent nodes in the animator indexed by their ids).
++
++ // MeanBlendNode1
++ MeanBlendNode *meanNode1 = createMeanBlendNode(handler);
++
++ // First clip to use in mean1
++ auto valueNode1 = createClipBlendValue(handler);
++ ClipResults valueNode1Results = { 0.0f, 0.0f, 0.0f };
++ valueNode1->setClipResults(animator->peerId(), valueNode1Results);
++
++ // Second clip to use in mean1
++ auto valueNode2 = createClipBlendValue(handler);
++ ClipResults valueNode2Results = { 2.0f, 2.0f, 2.0f };
++ valueNode2->setClipResults(animator->peerId(), valueNode2Results);
++
++ meanNode1->setValueNodeIds(valueNode1->peerId(), valueNode2->peerId());
++
++
++ // MeanBlendNode2
++ MeanBlendNode *meanNode2 = createMeanBlendNode(handler);
++
++ // First clip to use in mean2
++ auto valueNode3 = createClipBlendValue(handler);
++ ClipResults valueNode3Results = { 10.0f, 10.0f, 10.0f };
++ valueNode3->setClipResults(animator->peerId(), valueNode3Results);
++
++ // Second clip to use in mean2
++ auto valueNode4 = createClipBlendValue(handler);
++ ClipResults valueNode4Results = { 20.0f, 20.0f, 20.0f };
++ valueNode4->setClipResults(animator->peerId(), valueNode4Results);
++
++ meanNode2->setValueNodeIds(valueNode3->peerId(), valueNode4->peerId());
++
++
++ // MeanBlendNode3
++ MeanBlendNode *meanNode3 = createMeanBlendNode(handler);
++ meanNode3->setValueNodeIds(meanNode1->peerId(), meanNode2->peerId());
++
++
++ // AdditiveBlendNode1
++ AdditiveClipBlend *additiveBlendNode1 = createAdditiveClipBlend(handler);
++ auto valueNode5 = createClipBlendValue(handler);
++ ClipResults valueNode5Results = { 1.0f, 2.0f, 3.0f };
++ valueNode5->setClipResults(animator->peerId(), valueNode5Results);
++
++ additiveBlendNode1->setBaseClipId(meanNode3->peerId());
++ additiveBlendNode1->setAdditiveClipId(valueNode5->peerId());
++ additiveBlendNode1->setAdditiveFactor(0.5);
++
++ // Mean1 = 1
++ // Mean2 = 15
++ // Mean3 = (1 + 15 ) / 2 = 8
++ // Additive1 = 8 + 0.5 * (1, 2, 3) = (8.5, 9, 9.5)
++ ClipResults expectedResults = { 8.5f, 9.0f, 9.5f };
++
++ QTest::newRow("3 mean nodes + additive, 1 channel")
++ << handler << animator << additiveBlendNode1->peerId() << expectedResults;
++ }
++ }
++
++ void checkEvaluateBlendTree()
++ {
++ // GIVEN
++ QFETCH(Handler *, handler);
++ QFETCH(BlendedClipAnimator *, animator);
++ QFETCH(Qt3DCore::QNodeId, blendNodeId);
++ QFETCH(ClipResults, expectedResults);
++
++ // WHEN
++ const ClipResults actualResults = evaluateBlendTree(handler, animator, blendNodeId);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (int i = 0; i < actualResults.size(); ++i)
++ QCOMPARE(actualResults[i], expectedResults[i]);
++
++ // Cleanup
++ delete handler;
++ }
++
++ void checkFormatClipResults_data()
++ {
++ QTest::addColumn<ClipResults>("rawClipResults");
++ QTest::addColumn<ComponentIndices>("format");
++ QTest::addColumn<ClipResults>("expectedResults");
++
++ {
++ ClipResults rawClipResults = { 1.0f, 2.0f, 3.0f };
++ ComponentIndices format = { 0, 1, 2 };
++ ClipResults expectedResults = { 1.0f, 2.0f, 3.0f };
++
++ QTest::newRow("identity")
++ << rawClipResults << format << expectedResults;
++ }
++
++ {
++ ClipResults rawClipResults = { 1.0f, 2.0f };
++ ComponentIndices format = { 1, 0 };
++ ClipResults expectedResults = { 2.0f, 1.0f };
++
++ QTest::newRow("swap")
++ << rawClipResults << format << expectedResults;
++ }
++
++ {
++ ClipResults rawClipResults = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };
++ ComponentIndices format = { 0, 2, 1, 3, 4 };
++ ClipResults expectedResults = { 1.0f, 3.0f, 2.0f, 4.0f, 5.0f };
++
++ QTest::newRow("swap subset")
++ << rawClipResults << format << expectedResults;
++ }
++
++ {
++ ClipResults rawClipResults = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };
++ ComponentIndices format = { 4, 3, 2, 1, 0 };
++ ClipResults expectedResults = { 5.0f, 4.0f, 3.0f, 2.0f, 1.0f };
++
++ QTest::newRow("reverse")
++ << rawClipResults << format << expectedResults;
++ }
++
++ {
++ ClipResults rawClipResults = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };
++ ComponentIndices format = { 0, 1, -1, 3, 4 };
++ ClipResults expectedResults = { 1.0f, 2.0f, 0.0f, 4.0f, 5.0f };
++
++ QTest::newRow("include missing")
++ << rawClipResults << format << expectedResults;
++ }
++ }
++
++ void checkFormatClipResults()
++ {
++ // GIVEN
++ QFETCH(ClipResults, rawClipResults);
++ QFETCH(ComponentIndices, format);
++ QFETCH(ClipResults, expectedResults);
++
++ // WHEN
++ const ClipResults actualResults = formatClipResults(rawClipResults, format);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (int i = 0; i < actualResults.size(); ++i)
++ QCOMPARE(actualResults[i], expectedResults[i]);
++ }
++
++ void checkBuildRequiredChannelsAndTypes_data()
++ {
++ QTest::addColumn<Handler *>("handler");
++ QTest::addColumn<ChannelMapper *>("mapper");
++ QTest::addColumn<QList<ChannelNameAndType>>("expectedResults");
++
++ {
++ auto handler = new Handler();
++ auto channelMapping = createChannelMapping(handler,
++ QLatin1String("Location"),
++ Qt3DCore::QNodeId::createId(),
++ "translation",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++ auto channelMapper = createChannelMapper(handler,
++ QList<Qt3DCore::QNodeId> { channelMapping->peerId() });
++
++ const QList<ChannelNameAndType> expectedResults = {
++ { QLatin1String("Location"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping->peerId() }
++ };
++
++ QTest::addRow("Location, vec3") << handler << channelMapper << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ auto channelMapping1 = createChannelMapping(handler,
++ QLatin1String("Location"),
++ Qt3DCore::QNodeId::createId(),
++ "translation",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++ auto channelMapping2 = createChannelMapping(handler,
++ QLatin1String("Rotation"),
++ Qt3DCore::QNodeId::createId(),
++ "rotation",
++ static_cast<int>(QMetaType::QQuaternion),
++ 4);
++
++ const QList<Qt3DCore::QNodeId> channelMappingIds
++ = { channelMapping1->peerId(), channelMapping2->peerId() };
++ auto channelMapper = createChannelMapper(handler, channelMappingIds);
++
++ const QList<ChannelNameAndType> expectedResults = {
++ { QLatin1String("Location"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping1->peerId() },
++ { QLatin1String("Rotation"),
++ static_cast<int>(QMetaType::QQuaternion),
++ 4,
++ channelMapping2->peerId() }
++ };
++
++ QTest::addRow("Multiple unique channels") << handler << channelMapper << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ auto channelMapping1 = createChannelMapping(handler,
++ QLatin1String("Location"),
++ Qt3DCore::QNodeId::createId(),
++ "translation",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++ auto channelMapping2 = createChannelMapping(handler,
++ QLatin1String("Rotation"),
++ Qt3DCore::QNodeId::createId(),
++ "rotation",
++ static_cast<int>(QMetaType::QQuaternion),
++ 4);
++ auto channelMapping3 = createChannelMapping(handler,
++ QLatin1String("Location"),
++ Qt3DCore::QNodeId::createId(),
++ "translation",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++ auto channelMapping4 = createChannelMapping(handler,
++ QLatin1String("Location"),
++ Qt3DCore::QNodeId::createId(),
++ "translation",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++
++ const QList<Qt3DCore::QNodeId> channelMappingIds
++ = { channelMapping1->peerId(), channelMapping2->peerId(),
++ channelMapping3->peerId(), channelMapping4->peerId() };
++ auto channelMapper = createChannelMapper(handler, channelMappingIds);
++
++ QList<ChannelNameAndType> expectedResults = {
++ { QLatin1String("Location"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping1->peerId() },
++ { QLatin1String("Rotation"),
++ static_cast<int>(QMetaType::QQuaternion),
++ 4,
++ channelMapping2->peerId() },
++ { QLatin1String("Location"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping3->peerId() },
++ { QLatin1String("Location"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping4->peerId() }
++ };
++
++ QTest::addRow("Multiple channels with repeats") << handler << channelMapper << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ const int jointCount = 10;
++ auto skeleton = createSkeleton(handler, jointCount);
++ auto channelMapping = createChannelMapping(handler, skeleton->peerId());
++ auto channelMapper = createChannelMapper(handler,
++ QList<Qt3DCore::QNodeId> { channelMapping->peerId() });
++
++ QList<ChannelNameAndType> expectedResults;
++ for (int i = 0; i < jointCount; ++i) {
++ ChannelNameAndType locationDescription = { QLatin1String("Location"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping->peerId() };
++ locationDescription.jointIndex = i;
++ locationDescription.jointTransformComponent = Translation;
++ expectedResults.push_back(locationDescription);
++
++ ChannelNameAndType rotationDescription = { QLatin1String("Rotation"),
++ static_cast<int>(QMetaType::QQuaternion),
++ 4,
++ channelMapping->peerId() };
++ rotationDescription.jointIndex = i;
++ rotationDescription.jointTransformComponent = Rotation;
++ expectedResults.push_back(rotationDescription);
++
++ ChannelNameAndType scaleDescription = { QLatin1String("Scale"),
++ static_cast<int>(QMetaType::QVector3D),
++ 3,
++ channelMapping->peerId() };
++ scaleDescription.jointIndex = i;
++ scaleDescription.jointTransformComponent = Scale;
++ expectedResults.push_back(scaleDescription);
++ }
++
++ QTest::addRow("Skeleton, 10 joints") << handler << channelMapper << expectedResults;
++ }
++ }
++
++ void checkBuildRequiredChannelsAndTypes()
++ {
++ // GIVEN
++ QFETCH(Handler *, handler);
++ QFETCH(ChannelMapper *, mapper);
++ QFETCH(QList<ChannelNameAndType>, expectedResults);
++
++ // WHEN
++ const QVector<ChannelNameAndType> actualResults = buildRequiredChannelsAndTypes(handler, mapper);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (int i = 0; i < actualResults.size(); ++i)
++ QCOMPARE(actualResults[i], expectedResults[i]);
++
++ // Cleanup
++ delete handler;
++ }
++
++ void checkAssignChannelComponentIndices_data()
++ {
++ QTest::addColumn<QVector<ChannelNameAndType>>("allChannels");
++ QTest::addColumn<QVector<ComponentIndices>>("expectedResults");
++
++ {
++ const QVector<ChannelNameAndType> allChannels = {
++ { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 }
++ };
++
++ const QVector<ComponentIndices> expectedResults = { { 0, 1, 2 } };
++
++ QTest::newRow("vec3 location") << allChannels << expectedResults;
++ }
++
++ {
++ const QVector<ChannelNameAndType> allChannels = {
++ { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 }
++ };
++
++ const QVector<ComponentIndices> expectedResults = { { 0, 1, 2 },
++ { 3, 4, 5, 6 } };
++
++ QTest::newRow("vec3 location, quaterion rotation") << allChannels << expectedResults;
++ }
++
++ {
++ const QVector<ChannelNameAndType> allChannels = {
++ { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
++ { QLatin1String("BaseColor"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
++ { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 },
++ { QLatin1String("MorphWeights"), static_cast<int>(QMetaType::QVariantList), 6 }
++ };
++
++ const QVector<ComponentIndices> expectedResults = {
++ { 0, 1, 2 },
++ { 3, 4, 5, 6 },
++ { 7, 8, 9 },
++ { 10 },
++ { 11 },
++ { 12, 13, 14, 15, 16, 17 } };
++
++ QTest::newRow("vec3 location, quaterion rotation, pbr metal-rough morphweights") << allChannels << expectedResults;
++ }
++
++ {
++ QVector<ChannelNameAndType> allChannels;
++ const int jointCount = 4;
++ for (int i = 0; i < jointCount; ++i) {
++ ChannelNameAndType locationDescription = { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 };
++ locationDescription.jointIndex = i;
++ allChannels.push_back(locationDescription);
++
++ ChannelNameAndType rotationDescription = { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 };
++ rotationDescription.jointIndex = i;
++ allChannels.push_back(rotationDescription);
++
++ ChannelNameAndType scaleDescription = { QLatin1String("Scale"), static_cast<int>(QMetaType::QVector3D), 3 };
++ scaleDescription.jointIndex = i;
++ allChannels.push_back(scaleDescription);
++ }
++
++ const QVector<ComponentIndices> expectedResults = {
++ { 0, 1, 2 }, { 3, 4, 5, 6 }, { 7, 8, 9 },
++ { 10, 11, 12 }, { 13, 14, 15, 16 }, { 17, 18, 19 },
++ { 20, 21, 22 }, { 23, 24, 25, 26 }, { 27, 28, 29 },
++ { 30, 31, 32 }, { 33, 34, 35, 36 }, { 37, 38, 39 }
++ };
++
++ QTest::newRow("skeleton, 4 joints") << allChannels << expectedResults;
++ }
++ }
++
++ void checkAssignChannelComponentIndices()
++ {
++ // GIVEN
++ QFETCH(QVector<ChannelNameAndType>, allChannels);
++ QFETCH(QVector<ComponentIndices>, expectedResults);
++
++ // WHEN
++ const QVector<ComponentIndices> actualResults = assignChannelComponentIndices(allChannels);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (int i = 0; i < actualResults.size(); ++i) {
++ const ComponentIndices &actualResult = actualResults[i];
++ const ComponentIndices &expectedResult = expectedResults[i];
++
++ for (int j = 0; j < actualResult.size(); ++j)
++ QCOMPARE(actualResult[j], expectedResult[j]);
++ }
++ }
++
++ void checkGenerateClipFormatIndices_data()
++ {
++ QTest::addColumn<QVector<ChannelNameAndType>>("targetChannels");
++ QTest::addColumn<QVector<ComponentIndices>>("targetIndices");
++ QTest::addColumn<AnimationClip *>("clip");
++ QTest::addColumn<ClipFormat>("expectedResults");
++
++ {
++ const QVector<ChannelNameAndType> targetChannels = {
++ { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
++ { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Base Color"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
++ { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 }
++ };
++
++ const QVector<ComponentIndices> targetIndices = {
++ { 0, 1, 2, 3 },
++ { 4, 5, 6 },
++ { 7, 8, 9 },
++ { 10 },
++ { 11 }
++ };
++
++ auto *clip = new AnimationClip();
++ clip->setDataType(AnimationClip::File);
++ clip->setSource(QUrl("qrc:/clip3.json"));
++ clip->loadAnimation();
++
++ ClipFormat expectedResults;
++ expectedResults.sourceClipIndices = { 0, 1, 3, 2, // Rotation (y/z swapped in clip3.json)
++ 4, 6, 5, // Location (y/z swapped in clip3.json)
++ 7, 8, 9, // Base Color
++ 10, // Metalness
++ 11 }; // Roughness
++ expectedResults.sourceClipMask = { QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, true),
++ QBitArray(1, true),
++ QBitArray(1, true) };
++ expectedResults.namesAndTypes = targetChannels;
++ expectedResults.formattedComponentIndices = targetIndices;
++
++ QTest::newRow("rotation, location, pbr metal-rough")
++ << targetChannels << targetIndices << clip << expectedResults;
++ }
++
++ {
++ const QVector<ChannelNameAndType> targetChannels = {
++ { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
++ { QLatin1String("Base Color"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
++ { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 }
++ };
++
++ const QVector<ComponentIndices> targetIndices = {
++ { 0, 1, 2 },
++ { 3, 4, 5, 6 },
++ { 7, 8, 9 },
++ { 10 },
++ { 11 }
++ };
++
++ auto *clip = new AnimationClip();
++ clip->setDataType(AnimationClip::File);
++ clip->setSource(QUrl("qrc:/clip3.json"));
++ clip->loadAnimation();
++
++ ClipFormat expectedResults;
++ expectedResults.sourceClipIndices = { 4, 6, 5, // Location (y/z swapped in clip3.json)
++ 0, 1, 3, 2, // Rotation (y/z swapped in clip3.json)
++ 7, 8, 9, // Base Color
++ 10, // Metalness
++ 11 }; // Roughness
++ expectedResults.sourceClipMask = { QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(1, true),
++ QBitArray(1, true) };
++ expectedResults.namesAndTypes = targetChannels;
++ expectedResults.formattedComponentIndices = targetIndices;
++
++ QTest::newRow("location, rotation, pbr metal-rough")
++ << targetChannels << targetIndices << clip << expectedResults;
++ }
++
++ {
++ const QVector<ChannelNameAndType> targetChannels = {
++ { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
++ { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Albedo"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
++ { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 }
++ };
++
++ const QVector<ComponentIndices> targetIndices = {
++ { 0, 1, 2, 3 },
++ { 4, 5, 6 },
++ { 7, 8, 9 },
++ { 10 },
++ { 11 }
++ };
++
++ auto *clip = new AnimationClip();
++ clip->setDataType(AnimationClip::File);
++ clip->setSource(QUrl("qrc:/clip3.json"));
++ clip->loadAnimation();
++
++ ClipFormat expectedResults;
++ expectedResults.sourceClipIndices = { 0, 1, 3, 2, // Rotation (y/z swapped in clip3.json)
++ 4, 6, 5, // Location (y/z swapped in clip3.json)
++ -1, -1, -1, // Albedo (missing from clip)
++ 10, // Metalness
++ 11 }; // Roughness
++ expectedResults.sourceClipMask = { QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, false),
++ QBitArray(1, true),
++ QBitArray(1, true) };
++ expectedResults.namesAndTypes = targetChannels;
++ expectedResults.formattedComponentIndices = targetIndices;
++
++ QTest::newRow("rotation, location, albedo (missing), metal-rough")
++ << targetChannels << targetIndices << clip << expectedResults;
++ }
++
++ {
++ const QVector<ChannelNameAndType> targetChannels = {
++ { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
++ { QLatin1String("Albedo"), static_cast<int>(QMetaType::QVector3D), 3 },
++ { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
++ { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 }
++ };
++
++ const QVector<ComponentIndices> targetIndices = {
++ { 0, 1, 2 },
++ { 3, 4, 5, 6 },
++ { 7, 8, 9 },
++ { 10 },
++ { 11 }
++ };
++
++ auto *clip = new AnimationClip();
++ clip->setDataType(AnimationClip::File);
++ clip->setSource(QUrl("qrc:/clip3.json"));
++ clip->loadAnimation();
++
++ ClipFormat expectedResults;
++ expectedResults.sourceClipIndices = { 4, 6, 5, // Location (y/z swapped in clip3.json)
++ 0, 1, 3, 2, // Rotation (y/z swapped in clip3.json)
++ -1, -1, -1, // Albedo (missing from clip)
++ 10, // Metalness
++ 11 }; // Roughness
++ expectedResults.sourceClipMask = { QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, false),
++ QBitArray(1, true),
++ QBitArray(1, true) };
++ expectedResults.namesAndTypes = targetChannels;
++ expectedResults.formattedComponentIndices = targetIndices;
++
++ QTest::newRow("location, rotation, albedo (missing), metal-rough")
++ << targetChannels << targetIndices << clip << expectedResults;
++ }
++
++ {
++ QVector<ChannelNameAndType> targetChannels;
++ const int jointCount = 4;
++ for (int i = 0; i < jointCount; ++i) {
++ ChannelNameAndType locationDescription = { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 };
++ locationDescription.jointIndex = i;
++ targetChannels.push_back(locationDescription);
++
++ ChannelNameAndType rotationDescription = { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 };
++ rotationDescription.jointIndex = i;
++ targetChannels.push_back(rotationDescription);
++
++ ChannelNameAndType scaleDescription = { QLatin1String("Scale"), static_cast<int>(QMetaType::QVector3D), 3 };
++ scaleDescription.jointIndex = i;
++ targetChannels.push_back(scaleDescription);
++ }
++
++ const QVector<ComponentIndices> targetIndices = {
++ { 0, 1, 2 }, { 3, 4, 5, 6 }, { 7, 8, 9 },
++ { 10, 11, 12 }, { 13, 14, 15, 16 }, { 17, 18, 19 },
++ { 20, 21, 22 }, { 23, 24, 25, 26 }, { 27, 28, 29 },
++ { 30, 31, 32 }, { 33, 34, 35, 36 }, { 37, 38, 39 }
++ };
++
++ auto *clip = new AnimationClip();
++ clip->setDataType(AnimationClip::File);
++ clip->setSource(QUrl("qrc:/clip5.json"));
++ clip->loadAnimation();
++
++ ClipFormat expectedResults;
++ expectedResults.sourceClipIndices = { 4, 6, 5, // Location, joint 0 (y/z swapped in clip5.json)
++ 0, 1, 3, 2, // Rotation, joint 0 (y/z swapped in clip5.json)
++ 7, 8, 9, // Scale, joint 0
++ 14, 16, 15, // Location, joint 1 (y/z swapped in clip5.json)
++ 10, 11, 13, 12, // Rotation, joint 1 (y/z swapped in clip5.json)
++ 17, 18, 19, // Scale, joint 1
++ 24, 26, 25, // Location, joint 2 (y/z swapped in clip5.json)
++ 20, 21, 23, 22, // Rotation, joint 2 (y/z swapped in clip5.json)
++ 27, 28, 29, // Scale, joint 2
++ 34, 36, 35, // Location, joint 3 (y/z swapped in clip5.json)
++ 30, 31, 33, 32, // Rotation, joint 3 (y/z swapped in clip5.json)
++ 37, 38, 39 }; // Scale, joint 3
++ expectedResults.sourceClipMask = { QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, true),
++ QBitArray(3, true),
++ QBitArray(4, true),
++ QBitArray(3, true) };
++ expectedResults.namesAndTypes = targetChannels;
++ expectedResults.formattedComponentIndices = targetIndices;
++
++ QTest::newRow("skeleton (SQT), 4 joints")
++ << targetChannels << targetIndices << clip << expectedResults;
++ }
++ }
++
++ void checkGenerateClipFormatIndices()
++ {
++ // GIVEN
++ QFETCH(QVector<ChannelNameAndType>, targetChannels);
++ QFETCH(QVector<ComponentIndices>, targetIndices);
++ QFETCH(AnimationClip *, clip);
++ QFETCH(ClipFormat, expectedResults);
++
++ // WHEN
++ const ClipFormat actualResults = generateClipFormatIndices(targetChannels,
++ targetIndices,
++ clip);
++
++ // THEN
++ QCOMPARE(actualResults.sourceClipIndices.size(), expectedResults.sourceClipIndices.size());
++ for (int i = 0; i < actualResults.sourceClipIndices.size(); ++i)
++ QCOMPARE(actualResults.sourceClipIndices[i], expectedResults.sourceClipIndices[i]);
++
++ QCOMPARE(actualResults.sourceClipMask.size(), expectedResults.sourceClipMask.size());
++ for (int i = 0; i < actualResults.sourceClipMask.size(); ++i)
++ QCOMPARE(actualResults.sourceClipMask[i], expectedResults.sourceClipMask[i]);
++
++ QCOMPARE(actualResults.formattedComponentIndices.size(), expectedResults.formattedComponentIndices.size());
++ for (int i = 0; i < actualResults.formattedComponentIndices.size(); ++i)
++ QCOMPARE(actualResults.formattedComponentIndices[i], expectedResults.formattedComponentIndices[i]);
++
++ QCOMPARE(actualResults.namesAndTypes.size(), expectedResults.namesAndTypes.size());
++ for (int i = 0; i < actualResults.namesAndTypes.size(); ++i)
++ QCOMPARE(actualResults.namesAndTypes[i], expectedResults.namesAndTypes[i]);
++
++ // Cleanup
++ delete clip;
++ }
++
++ void checkDefaultValueForChannel_data()
++ {
++ QTest::addColumn<Handler *>("handler");
++ QTest::addColumn<ChannelNameAndType>("channelDescription");
++ QTest::addColumn<QVector<float>>("expectedResults");
++
++ {
++ auto handler = new Handler();
++ auto channelMapping = createChannelMapping(handler,
++ QLatin1String("Location"),
++ Qt3DCore::QNodeId::createId(),
++ "translation",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++ ChannelNameAndType channelDescription;
++ channelDescription.mappingId = channelMapping->peerId();
++ channelDescription.type = static_cast<int>(QMetaType::QVector3D);
++ channelDescription.name = QLatin1String("translation");
++ const QVector<float> expectedResults = { 0.0f, 0.0f, 0.0f };
++ QTest::newRow("translation") << handler << channelDescription << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ auto channelMapping = createChannelMapping(handler,
++ QLatin1String("Rotation"),
++ Qt3DCore::QNodeId::createId(),
++ "rotation",
++ static_cast<int>(QMetaType::QQuaternion),
++ 4);
++ ChannelNameAndType channelDescription;
++ channelDescription.mappingId = channelMapping->peerId();
++ channelDescription.type = static_cast<int>(QMetaType::QQuaternion);
++ channelDescription.name = QLatin1String("rotation");
++ const QVector<float> expectedResults = { 1.0f, 0.0f, 0.0f, 0.0f };
++ QTest::newRow("rotation") << handler << channelDescription << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ auto channelMapping = createChannelMapping(handler,
++ QLatin1String("Scale"),
++ Qt3DCore::QNodeId::createId(),
++ "scale",
++ static_cast<int>(QMetaType::QVector3D),
++ 3);
++ ChannelNameAndType channelDescription;
++ channelDescription.mappingId = channelMapping->peerId();
++ channelDescription.type = static_cast<int>(QMetaType::QVector3D);
++ channelDescription.name = QLatin1String("scale");
++ const QVector<float> expectedResults = { 1.0f, 1.0f, 1.0f };
++ QTest::newRow("scale") << handler << channelDescription << expectedResults;
++ }
++
++ // Test skeleton cases
++ {
++ auto handler = new Handler();
++ auto skeleton = createSkeleton(handler, 2);
++ skeleton->setJointScale(0, QVector3D(2.0f, 3.0f, 4.0f));
++
++ auto channelMapping = createChannelMapping(handler, skeleton->peerId());
++ ChannelNameAndType channelDescription;
++ channelDescription.mappingId = channelMapping->peerId();
++ channelDescription.type = static_cast<int>(QMetaType::QVector3D);
++ channelDescription.jointIndex = 0;
++ channelDescription.jointTransformComponent = Scale;
++ const QVector<float> expectedResults = { 2.0f, 3.0f, 4.0f };
++ QTest::newRow("joint 0 scale") << handler << channelDescription << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ auto skeleton = createSkeleton(handler, 2);
++ skeleton->setJointRotation(0, QQuaternion(1.0f, 0.0f, 0.0f, 0.0f));
++
++ auto channelMapping = createChannelMapping(handler, skeleton->peerId());
++ ChannelNameAndType channelDescription;
++ channelDescription.mappingId = channelMapping->peerId();
++ channelDescription.type = static_cast<int>(QMetaType::QVector3D);
++ channelDescription.jointIndex = 0;
++ channelDescription.jointTransformComponent = Rotation;
++ const QVector<float> expectedResults = { 1.0f, 0.0f, 0.0f, 0.0f };
++ QTest::newRow("joint 0 rotation") << handler << channelDescription << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ auto skeleton = createSkeleton(handler, 2);
++ skeleton->setJointTranslation(0, QVector3D(2.0f, 3.0f, 4.0f));
++
++ auto channelMapping = createChannelMapping(handler, skeleton->peerId());
++ ChannelNameAndType channelDescription;
++ channelDescription.mappingId = channelMapping->peerId();
++ channelDescription.type = static_cast<int>(QMetaType::QVector3D);
++ channelDescription.jointIndex = 0;
++ channelDescription.jointTransformComponent = Translation;
++ const QVector<float> expectedResults = { 2.0f, 3.0f, 4.0f };
++ QTest::newRow("joint 0 translation") << handler << channelDescription << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ auto skeleton = createSkeleton(handler, 2);
++ skeleton->setJointScale(1, QVector3D(20.0f, 30.0f, 40.0f));
++
++ auto channelMapping = createChannelMapping(handler, skeleton->peerId());
++ ChannelNameAndType channelDescription;
++ channelDescription.mappingId = channelMapping->peerId();
++ channelDescription.type = static_cast<int>(QMetaType::QVector3D);
++ channelDescription.jointIndex = 1;
++ channelDescription.jointTransformComponent = Scale;
++ const QVector<float> expectedResults = { 20.0f, 30.0f, 40.0f };
++ QTest::newRow("joint 1 scale") << handler << channelDescription << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ auto skeleton = createSkeleton(handler, 2);
++ skeleton->setJointRotation(1, QQuaternion(1.0f, 0.0f, 0.0f, 0.0f));
++
++ auto channelMapping = createChannelMapping(handler, skeleton->peerId());
++ ChannelNameAndType channelDescription;
++ channelDescription.mappingId = channelMapping->peerId();
++ channelDescription.type = static_cast<int>(QMetaType::QVector3D);
++ channelDescription.jointIndex = 1;
++ channelDescription.jointTransformComponent = Rotation;
++ const QVector<float> expectedResults = { 1.0f, 0.0f, 0.0f, 0.0f };
++ QTest::newRow("joint 1 rotation") << handler << channelDescription << expectedResults;
++ }
++
++ {
++ auto handler = new Handler();
++ auto skeleton = createSkeleton(handler, 2);
++ skeleton->setJointTranslation(1, QVector3D(4.0f, 5.0f, 6.0f));
++
++ auto channelMapping = createChannelMapping(handler, skeleton->peerId());
++ ChannelNameAndType channelDescription;
++ channelDescription.mappingId = channelMapping->peerId();
++ channelDescription.type = static_cast<int>(QMetaType::QVector3D);
++ channelDescription.jointIndex = 1;
++ channelDescription.jointTransformComponent = Translation;
++ const QVector<float> expectedResults = { 4.0f, 5.0f, 6.0f };
++ QTest::newRow("joint 1 translation") << handler << channelDescription << expectedResults;
++ }
++ }
++
++ void checkDefaultValueForChannel()
++ {
++ // GIVEN
++ QFETCH(Handler *, handler);
++ QFETCH(ChannelNameAndType, channelDescription);
++ QFETCH(QVector<float>, expectedResults);
++
++ // WHEN
++ auto actualResults = defaultValueForChannel(handler, channelDescription);
++
++ // THEN
++ QCOMPARE(actualResults.size(), expectedResults.size());
++ for (int i = 0; i < actualResults.size(); ++i) {
++ QCOMPARE(actualResults[i], expectedResults[i]);
++ }
++ }
++ };
++
++ QTEST_MAIN(tst_AnimationUtils)
++
++ #include "tst_animationutils.moc"