Commit b6c39175 authored by Dmitry Kazakov's avatar Dmitry Kazakov

Fix copy-paste frames operation when the timeline layout changes

Now "application/x-krita-frame" mime type also saves the UUID of the
source node, so pasting the frames after the row layout changed is still
valid.

The patch also implements a new version of
KisAnimationUtils::createMoveKeyframesCommand which is safe to use
when the frame moves have cyclic dependencies (one frame is going to
overwrite another frame during the move). Theoretically, we don't need
to use sortPointsForSafeMove() anymore. But it should be tested.
parent 1d6a7e2e
......@@ -525,6 +525,45 @@ void KisKeyframeChannel::loadXML(const QDomElement &channelNode)
}
}
bool KisKeyframeChannel::swapExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand)
{
if (srcChannel->id() != id()) {
warnKrita << "Cannot copy frames from different ids:" << ppVar(srcChannel->id()) << ppVar(id());
return KisKeyframeSP();
}
LAZY_INITIALIZE_PARENT_COMMAND(parentCommand);
KisKeyframeSP srcFrame = srcChannel->keyframeAt(srcTime);
KisKeyframeSP dstFrame = keyframeAt(dstTime);
if (!dstFrame && srcFrame) {
copyExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand);
srcChannel->deleteKeyframe(srcFrame, parentCommand);
} else if (dstFrame && !srcFrame) {
srcChannel->copyExternalKeyframe(this, dstTime, srcTime, parentCommand);
deleteKeyframe(dstFrame, parentCommand);
} else if (dstFrame && srcFrame) {
const int fakeFrameTime = -1;
KisKeyframeSP newKeyframe = createKeyframe(fakeFrameTime, KisKeyframeSP(), parentCommand);
uploadExternalKeyframe(srcChannel, srcTime, newKeyframe);
srcChannel->copyExternalKeyframe(this, dstTime, srcTime, parentCommand);
// do not recreate frame!
deleteKeyframeImpl(dstFrame, parentCommand, false);
newKeyframe->setTime(dstTime);
KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newKeyframe->time(), newKeyframe, parentCommand);
cmd->redo();
}
return true;
}
KisKeyframeSP KisKeyframeChannel::copyExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand)
{
if (srcChannel->id() != id()) {
......
......@@ -70,6 +70,8 @@ public:
KisKeyframeSP copyKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0);
KisKeyframeSP copyExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0);
bool swapExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0);
KisKeyframeSP keyframeAt(int time) const;
KisKeyframeSP activeKeyframeAt(int time) const;
KisKeyframeSP currentlyActiveKeyframe() const;
......
......@@ -179,92 +179,162 @@ namespace KisAnimationUtils {
bool copy,
KUndo2Command *parentCommand) {
FrameMovePairList movedFrames;
for (int i = 0; i < srcFrames.size(); i++) {
movedFrames << std::make_pair(srcFrames[i], dstFrames[i]);
}
return createMoveKeyframesCommand(movedFrames, copy, parentCommand);
}
bool supportsContentFrames(KisNodeSP node)
{
return node->inherits("KisPaintLayer") || node->inherits("KisFilterMask") || node->inherits("KisTransparencyMask") || node->inherits("KisSelectionBasedLayer");
}
void swapOneFrameItem(const FrameItem &src, const FrameItem &dst, KUndo2Command *parentCommand)
{
const int srcTime = src.time;
KisNodeSP srcNode = src.node;
KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(src.channel);
const int dstTime = dst.time;
KisNodeSP dstNode = dst.node;
KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dst.channel, true);
if (srcNode == dstNode) {
// TODO: add warning!
if (!srcChannel) return;
srcChannel->swapFrames(srcTime, dstTime, parentCommand);
} else {
// TODO: add warning!
if (!srcChannel || !dstChannel) return;
dstChannel->swapExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand);
}
}
void moveOneFrameItem(const FrameItem &src, const FrameItem &dst, bool copy, KUndo2Command *parentCommand)
{
const int srcTime = src.time;
KisNodeSP srcNode = src.node;
KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(src.channel);
const int dstTime = dst.time;
KisNodeSP dstNode = dst.node;
KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dst.channel, true);
if (srcNode == dstNode) {
// TODO: add warning!
if (!srcChannel) return;
KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime);
if (srcKeyframe) {
if (copy) {
srcChannel->copyKeyframe(srcKeyframe, dstTime, parentCommand);
} else {
srcChannel->moveKeyframe(srcKeyframe, dstTime, parentCommand);
}
}
} else {
// TODO: add warning!
if (!srcChannel || !dstChannel) return;
KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime);
// TODO: add warning!
if (!srcKeyframe) return;
dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand);
if (!copy) {
srcChannel->deleteKeyframe(srcKeyframe, parentCommand);
}
}
}
KUndo2Command *createMoveKeyframesCommand(const FrameMovePairList &movePairs, bool copy, KUndo2Command *parentCommand)
{
KUndo2Command *cmd = new KisCommandUtils::LambdaCommand(
!copy ?
kundo2_i18np("Move Keyframe",
"Move %1 Keyframes",
srcFrames.size()) :
movePairs.size()) :
kundo2_i18np("Copy Keyframe",
"Copy %1 Keyframes",
srcFrames.size()),
movePairs.size()),
parentCommand,
[srcFrames, dstFrames, copy] () -> KUndo2Command* {
[movePairs, copy] () -> KUndo2Command* {
bool result = false;
QScopedPointer<KUndo2Command> cmd(new KUndo2Command());
for (int i = 0; i < srcFrames.size(); i++) {
const int srcTime = srcFrames[i].time;
KisNodeSP srcNode = srcFrames[i].node;
KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(srcFrames[i].channel);
const int dstTime = dstFrames[i].time;
KisNodeSP dstNode = dstFrames[i].node;
KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dstFrames[i].channel, true);
using MoveChain = QList<FrameItem>;
if (srcNode == dstNode) {
if (!srcChannel) continue;
KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime);
if (srcKeyframe) {
if (copy) {
srcChannel->copyKeyframe(srcKeyframe, dstTime, cmd.data());
} else {
srcChannel->moveKeyframe(srcKeyframe, dstTime, cmd.data());
}
}
} else {
if (!srcChannel|| !dstChannel) continue;
QHash<FrameItem, MoveChain> moveMap;
Q_FOREACH (const FrameMovePair &pair, movePairs) {
moveMap.insert(pair.first, {pair.second});
}
KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime);
if (!srcKeyframe) continue;
auto it = moveMap.begin();
while (it != moveMap.end()) {
MoveChain &chain = it.value();
const FrameItem &lastFrame = chain.last();
dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, cmd.data());
auto tailIt = moveMap.find(lastFrame);
if (!copy) {
srcChannel->deleteKeyframe(srcKeyframe, cmd.data());
}
if (tailIt == it || tailIt == moveMap.end()) {
++it;
continue;
}
result = true;
chain.append(tailIt.value());
tailIt = moveMap.erase(tailIt);
// no incrementing! we are going to check the new tail now!
}
return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0;
});
for (it = moveMap.begin(); it != moveMap.end(); ++it) {
MoveChain &chain = it.value();
chain.prepend(it.key());
KIS_SAFE_ASSERT_RECOVER(chain.size() > 1) { continue; }
return cmd;
}
bool isCycle = false;
if (chain.last() == chain.first()) {
isCycle = true;
chain.takeLast();
}
void moveKeyframes(KisImageSP image,
const FrameItemList &srcFrames,
const FrameItemList &dstFrames,
bool copy) {
auto frameIt = chain.rbegin();
KIS_SAFE_ASSERT_RECOVER_RETURN(srcFrames.size() != dstFrames.size());
KIS_SAFE_ASSERT_RECOVER_RETURN(!image->locked());
FrameItem dstItem = *frameIt++;
KUndo2Command *cmd =
createMoveKeyframesCommand(srcFrames, dstFrames, copy);
while (frameIt != chain.rend()) {
FrameItem srcItem = *frameIt++;
KisProcessingApplicator::runSingleCommandStroke(image, cmd, KisStrokeJobData::BARRIER);
}
if (!isCycle) {
moveOneFrameItem(srcItem, dstItem, copy, cmd.data());
} else {
swapOneFrameItem(srcItem, dstItem, cmd.data());
}
void moveKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int srcTime, int dstTime) {
QVector<FrameItem> srcFrames;
srcFrames << FrameItem(node, channel, srcTime);
dstItem = srcItem;
result = true;
}
}
QVector<FrameItem> dstFrames;
dstFrames << FrameItem(node, channel, dstTime);
return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0;
});
moveKeyframes(image, srcFrames, dstFrames);
return cmd;
}
bool supportsContentFrames(KisNodeSP node)
QDebug operator<<(QDebug dbg, const FrameItem &item)
{
return node->inherits("KisPaintLayer") || node->inherits("KisFilterMask") || node->inherits("KisTransparencyMask") || node->inherits("KisSelectionBasedLayer");
dbg.nospace() << "FrameItem(" << item.node->name() << ", " << item.channel << ", " << item.time << ")";
return dbg.space();
}
}
......
......@@ -21,23 +21,43 @@
#include "kis_types.h"
#include <boost/operators.hpp>
#include <QModelIndexList>
#include <kritaanimationdocker_export.h>
namespace KisAnimationUtils
{
KUndo2Command* createKeyframeCommand(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy, KUndo2Command *parentCommand = 0);
void createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channel, int time, bool copy);
struct FrameItem {
struct KRITAANIMATIONDOCKER_EXPORT FrameItem : public boost::equality_comparable<FrameItem>
{
FrameItem() : time(-1) {}
FrameItem(KisNodeSP _node, const QString &_channel, int _time) : node(_node), channel(_channel), time(_time) {}
bool operator==(const FrameItem &rhs) const {
return rhs.node == node && rhs.channel == channel && rhs.time == time;
}
KisNodeSP node;
QString channel;
int time;
};
KRITAANIMATIONDOCKER_EXPORT QDebug operator<<(QDebug dbg, const FrameItem &item);
inline uint qHash(const FrameItem &item)
{
return ::qHash(item.node.data()) + ::qHash(item.channel) + ::qHash(item.time);
}
typedef QVector<FrameItem> FrameItemList;
typedef std::pair<FrameItem, FrameItem> FrameMovePair;
typedef QVector<FrameMovePair> FrameMovePairList;
void removeKeyframes(KisImageSP image, const FrameItemList &frames);
void removeKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int time);
......@@ -48,12 +68,17 @@ namespace KisAnimationUtils
const FrameItemList &dstFrames,
bool copy, KUndo2Command *parentCommand = 0);
void moveKeyframes(KisImageSP image,
const FrameItemList &srcFrames,
const FrameItemList &dstFrames,
bool copy = false);
void moveKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int srcTime, int dstTime);
/**
* @brief implements safe moves of the frames (even if there are cycling move dependencies)
* @param movePairs the jobs for the moves
* @param copy shows if the frames should be copied or not
* @param parentCommand the command that should be a parent of the created command
* @return a created undo command
*/
KRITAANIMATIONDOCKER_EXPORT
KUndo2Command* createMoveKeyframesCommand(const FrameMovePairList &movePairs,
bool copy, KUndo2Command *parentCommand = 0);
bool supportsContentFrames(KisNodeSP node);
......@@ -75,4 +100,5 @@ namespace KisAnimationUtils
extern const QString removeTransformKeyframeActionName;
};
#endif /* __KIS_ANIMATION_UTILS_H */
......@@ -321,22 +321,6 @@ KUndo2Command* KisTimeBasedItemModel::createOffsetFramesCommand(QModelIndexList
parentCommand);
}
bool KisTimeBasedItemModel::offsetFrames(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames)
{
KUndo2Command *cmd = 0;
{
KisImageBarrierLockerWithFeedback locker(m_d->image);
cmd = createOffsetFramesCommand(srcIndexes, offset, copyFrames);
}
if (cmd) {
KisProcessingApplicator::runSingleCommandStroke(m_d->image, cmd, KisStrokeJobData::BARRIER);
}
return cmd;
}
bool KisTimeBasedItemModel::removeFramesAndOffset(QModelIndexList indexes)
{
if (indexes.isEmpty()) return true;
......
......@@ -52,7 +52,6 @@ public:
bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) override;
bool removeFrames(const QModelIndexList &indexes);
bool offsetFrames(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames);
bool removeFramesAndOffset(QModelIndexList indexes);
......
set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_SOURCE_DIR}/sdk/tests )
include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_SOURCE_DIR}/sdk/tests ${CMAKE_CURRENT_BINARY_DIR}/..)
macro_add_unittest_definitions()
......@@ -8,3 +8,7 @@ macro_add_unittest_definitions()
krita_add_broken_unit_test(timeline_model_test.cpp
TEST_NAME krita-animation-TimelineModelTest
LINK_LIBRARIES ${KDE4_KDEUI_LIBS} kritaanimationdocker kritaui kritaimage Qt5::Test)
krita_add_broken_unit_test(kis_animation_utils_test.cpp
TEST_NAME krita-animation-KisAnimationUtilsTest
LINK_LIBRARIES ${KDE4_KDEUI_LIBS} kritaanimationdocker kritaui kritaimage Qt5::Test)
#include "kis_animation_utils_test.h"
#include <KoColor.h>
#include <KoColorSpace.h>
#include <KoColorSpaceRegistry.h>
#include "kis_image_animation_interface.h"
#include "kis_paint_layer.h"
#include "kis_keyframe_channel.h"
#include <testutil.h>
#include "kis_animation_utils.h"
#include <tuple>
bool verifyFrames(TestUtil::MaskParent &p,
const QVector<KisNodeSP> &nodes,
const QVector<std::tuple<int, QRect, QRect>> &offsets)
{
KisImageAnimationInterface *i = p.image->animationInterface();
Q_FOREACH (const auto &offset, offsets) {
int time = 0;
QRect rc1;
QRect rc2;
std::tie(time, rc1, rc2) = offset;
i->switchCurrentTimeAsync(time);
p.image->waitForDone();
KIS_SAFE_ASSERT_RECOVER_NOOP(nodes[0]->paintDevice()->defaultBounds()->currentTime() == time);
KIS_SAFE_ASSERT_RECOVER_NOOP(nodes[1]->paintDevice()->defaultBounds()->currentTime() == time);
KisKeyframeChannel *channel1 = nodes[0]->getKeyframeChannel("content");
KisKeyframeChannel *channel2 = nodes[1]->getKeyframeChannel("content");
if (!rc1.isValid() && !channel1->keyframeAt(time)) {
// noop
} else if (nodes[0]->paintDevice()->exactBounds() != rc1) {
qWarning() << "Compared values are not the same:";
qWarning() << " " << ppVar(nodes[0]->paintDevice()->exactBounds());
qWarning() << " " << ppVar(rc1);
qWarning() << " " << ppVar(time);
return false;
}
if (!rc2.isValid() && !channel2->keyframeAt(time)) {
// noop
} else if (nodes[1]->paintDevice()->exactBounds() != rc2) {
qWarning() << "Compared values are not the same:";
qWarning() << " " << ppVar(nodes[1]->paintDevice()->exactBounds());
qWarning() << " " << ppVar(rc2);
qWarning() << " " << ppVar(time);
return false;
}
}
return true;
}
void KisAnimationUtilsTest::test()
{
QRect refRect(QRect(0,0,512,512));
TestUtil::MaskParent p(refRect);
const KoColor fillColor(Qt::red, p.image->colorSpace());
KisPaintLayerSP layer1 = p.layer;
KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8);
p.image->addNode(layer2);
QVector<KisNodeSP> nodes({layer1, layer2});
KisPaintDeviceSP dev1 = layer1->paintDevice();
KisPaintDeviceSP dev2 = layer2->paintDevice();
KisKeyframeChannel *channel1 = layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true);
KisKeyframeChannel *channel2 = layer2->getKeyframeChannel(KisKeyframeChannel::Content.id(), true);
channel1->addKeyframe(0);
channel2->addKeyframe(0);
channel1->addKeyframe(10);
channel2->addKeyframe(10);
channel1->addKeyframe(20);
channel2->addKeyframe(20);
KisImageAnimationInterface *i = p.image->animationInterface();
i->switchCurrentTimeAsync(0);
p.image->waitForDone();
dev1->fill(QRect(0, 0, 10, 10), fillColor);
dev2->fill(QRect(0, 10, 10, 10), fillColor);
i->switchCurrentTimeAsync(10);
p.image->waitForDone();
dev1->fill(QRect(10, 0, 10, 10), fillColor);
dev2->fill(QRect(10, 10, 10, 10), fillColor);
i->switchCurrentTimeAsync(20);
p.image->waitForDone();
dev1->fill(QRect(20, 0, 10, 10), fillColor);
dev2->fill(QRect(20, 10, 10, 10), fillColor);
QVector<std::tuple<int, QRect, QRect>> initialReferenceRects;
initialReferenceRects << std::make_tuple( 0, QRect( 0, 0, 10, 10), QRect( 0, 10, 10, 10));
initialReferenceRects << std::make_tuple(10, QRect(10, 0, 10, 10), QRect(10, 10, 10, 10));
initialReferenceRects << std::make_tuple(20, QRect(20, 0, 10, 10), QRect(20, 10, 10, 10));
QVERIFY(verifyFrames(p, nodes, initialReferenceRects));
using namespace KisAnimationUtils;
FrameMovePairList frameMoves;
//
// Cycling single-layer move
//
frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer1, "content", 10));
frameMoves << std::make_pair(FrameItem(layer1, "content", 10), FrameItem(layer1, "content", 20));
frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer1, "content", 0));
QScopedPointer<KUndo2Command> cmd(createMoveKeyframesCommand(frameMoves, false));
cmd->redo();
QVector<std::tuple<int, QRect, QRect>> referenceRects;
referenceRects << std::make_tuple( 0, QRect(20, 0, 10, 10), QRect( 0, 10, 10, 10));
referenceRects << std::make_tuple(10, QRect( 0, 0, 10, 10), QRect(10, 10, 10, 10));
referenceRects << std::make_tuple(20, QRect(10, 0, 10, 10), QRect(20, 10, 10, 10));
QVERIFY(verifyFrames(p, nodes, referenceRects));
cmd->undo();
QVERIFY(verifyFrames(p, nodes, initialReferenceRects));
frameMoves.clear();
referenceRects.clear();
cmd.reset();
//
// Just a complex non-cycling move
//
frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer2, "content", 10));
frameMoves << std::make_pair(FrameItem(layer2, "content", 10), FrameItem(layer1, "content", 10));
frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer2, "content", 20));
cmd.reset(createMoveKeyframesCommand(frameMoves, false));
cmd->redo();
referenceRects << std::make_tuple( 0, QRect() , QRect( 0, 10, 10, 10));
referenceRects << std::make_tuple(10, QRect(10, 10, 10, 10), QRect( 0, 0, 10, 10));
referenceRects << std::make_tuple(20, QRect() , QRect(20, 0, 10, 10));
QVERIFY(verifyFrames(p, nodes, referenceRects));
cmd->undo();
QVERIFY(verifyFrames(p, nodes, initialReferenceRects));
frameMoves.clear();
referenceRects.clear();
cmd.reset();
//
// Cross-node swap of the frames
//
frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer2, "content", 0));
frameMoves << std::make_pair(FrameItem(layer1, "content", 10), FrameItem(layer2, "content", 10));
frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer2, "content", 20));
frameMoves << std::make_pair(FrameItem(layer2, "content", 0), FrameItem(layer1, "content", 0));
frameMoves << std::make_pair(FrameItem(layer2, "content", 10), FrameItem(layer1, "content", 10));
frameMoves << std::make_pair(FrameItem(layer2, "content", 20), FrameItem(layer1, "content", 20));
cmd.reset(createMoveKeyframesCommand(frameMoves, false));
cmd->redo();
referenceRects << std::make_tuple( 0, QRect( 0, 10, 10, 10), QRect( 0, 0, 10, 10));
referenceRects << std::make_tuple(10, QRect(10, 10, 10, 10), QRect(10, 0, 10, 10));
referenceRects << std::make_tuple(20, QRect(20, 10, 10, 10), QRect(20, 0, 10, 10));
QVERIFY(verifyFrames(p, nodes, referenceRects));
cmd->undo();
QVERIFY(verifyFrames(p, nodes, initialReferenceRects));
frameMoves.clear();
referenceRects.clear();
cmd.reset();
//
// Cross-node move and swap
//
frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer2, "content", 0));
frameMoves << std::make_pair(FrameItem(layer1, "content", 10), FrameItem(layer2, "content", 10));
frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer2, "content", 20));
frameMoves << std::make_pair(FrameItem(layer2, "content", 0), FrameItem(layer1, "content", 0));
frameMoves << std::make_pair(FrameItem(layer2, "content", 10), FrameItem(layer1, "content", 9));
frameMoves << std::make_pair(FrameItem(layer2, "content", 20), FrameItem(layer1, "content", 20));
cmd.reset(createMoveKeyframesCommand(frameMoves, false));
cmd->redo();
referenceRects << std::make_tuple( 0, QRect( 0, 10, 10, 10), QRect( 0, 0, 10, 10));
referenceRects << std::make_tuple( 9, QRect(10, 10, 10, 10), QRect() );
referenceRects << std::make_tuple(10, QRect() , QRect(10, 0, 10, 10));
referenceRects << std::make_tuple(20, QRect(20, 10, 10, 10), QRect(20, 0, 10, 10));
QVERIFY(verifyFrames(p, nodes, referenceRects));
cmd->undo();
QVERIFY(verifyFrames(p, nodes, initialReferenceRects));
frameMoves.clear();
referenceRects.clear();
cmd.reset();
}
QTEST_MAIN(KisAnimationUtilsTest)
#ifndef ANIMATION_UTILS_TEST_H
#define ANIMATION_UTILS_TEST_H
#include <QtTest>
class KisAnimationUtilsTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void test();
};
#endif // ANIMATION_UTILS_TEST_H