Commit 96333af4 authored by Jouni Pentikäinen's avatar Jouni Pentikäinen

Add command to move multiple keyframes at once

This allows the moves to be validated as one operation. In particular,
animation cycle ranges can be updated based on moved or overwritten
keyframes.
parent 2d4b7f09
......@@ -27,6 +27,13 @@ KisAnimationCycle::KisAnimationCycle(KisKeyframeChannel *channel, KisKeyframeSP
, m_lastSourceKeyframe(lastKeyframe)
{}
KisAnimationCycle::KisAnimationCycle(const KisAnimationCycle &cycle, KisKeyframeSP firstKeyframe, KisKeyframeSP lastKeyframe)
: KisKeyframeBase(cycle.channel(), firstKeyframe->time())
, m_firstSourceKeyframe(firstKeyframe)
, m_lastSourceKeyframe(lastKeyframe)
, m_repeats(cycle.m_repeats)
{}
KisKeyframeSP KisAnimationCycle::firstSourceKeyframe() const
{
return m_firstSourceKeyframe;
......
......@@ -30,6 +30,8 @@ class KRITAIMAGE_EXPORT KisAnimationCycle : public KisKeyframeBase {
public:
KisAnimationCycle(KisKeyframeChannel *channel, KisKeyframeSP firstKeyframe, KisKeyframeSP lastKeyframe);
KisAnimationCycle(const KisAnimationCycle &cycle, KisKeyframeSP firstKeyframe, KisKeyframeSP lastKeyframe);
KisKeyframeSP firstSourceKeyframe() const;
KisKeyframeSP lastSourceKeyframe() const;
KisTimeSpan originalRange() const;
......
......@@ -553,6 +553,11 @@ KisVisibleKeyframeIterator KisKeyframeChannel::visibleKeyframesFrom(int time) co
return KisVisibleKeyframeIterator(visibleKeyframeAt(time));
}
QList<QSharedPointer<KisAnimationCycle>> KisKeyframeChannel::cycles() const
{
return m_d->cycles.values();
}
KisTimeSpan KisKeyframeChannel::cycledRangeAt(int time) const
{
QSharedPointer<KisRepeatFrame> repeat = activeRepeatAt(time);
......
......@@ -101,6 +101,8 @@ public:
KisVisibleKeyframeIterator visibleKeyframesFrom(int time) const;
QList<QSharedPointer<KisAnimationCycle>> cycles() const;
/**
* Finds the original range of the cycle defined or repeated at the given time.
* @arg time a time at any frame within the original cycle or any repeat of it.
......@@ -199,6 +201,7 @@ private:
void addCycle(QSharedPointer<KisAnimationCycle> cycle);
void removeCycle(QSharedPointer<KisAnimationCycle> cycle);
friend class KisMoveKeyframesCommand;
friend class KisMoveFrameCommand;
friend class KisReplaceKeyframeCommand;
friend class KisSwapFramesCommand;
......
......@@ -2,8 +2,307 @@
#include "kis_keyframe_commands.h"
#include <kis_pointer_utils.h>
#include <KisCollectionUtils.h>
#include "kis_time_range.h"
#include "kis_animation_cycle.h"
using CycleSP = QSharedPointer<KisAnimationCycle>;
using KeyframeMove = KisKeyframeCommands::KeyframeMove;
KisKeyframeCommands::KeyframeMove::KeyframeMove(KisKeyframeBaseSP keyframe, int newTime)
: keyframe(keyframe)
, oldTime(keyframe->time())
, newTime(newTime)
{}
class KisMoveKeyframesCommand : public KUndo2Command
{
public:
KisMoveKeyframesCommand(QVector<KeyframeMove> moves, KUndo2Command *parentCommand)
: KUndo2Command(parentCommand)
, m_moves(moves)
{}
void redo() override
{
Q_FOREACH(const KeyframeMove &move, m_moves) {
move.keyframe->channel()->moveKeyframeImpl(move.keyframe, move.newTime);
}
}
void undo() override
{
Q_FOREACH(const KeyframeMove &move, m_moves) {
move.keyframe->channel()->moveKeyframeImpl(move.keyframe, move.oldTime);
}
}
private:
QVector<KisKeyframeCommands::KeyframeMove> m_moves;
};
struct KeyframeMapping
{
KisKeyframeChannel *channel;
QMap<KisKeyframeBaseSP, int> destinations;
QMap<int, KisKeyframeBaseSP> sources;
KeyframeMapping(KisKeyframeChannel *channel, const QVector<KeyframeMove> &moves)
: channel(channel)
{
const int count = moves.count();
for (int i = 0; i < count; i++) {
const int time = moves[i].newTime;
const KisKeyframeBaseSP key = moves[i].keyframe;
if (key->channel() == channel) {
destinations[key] = time;
sources[time] = key;
}
}
}
bool isEmpty() const
{
return destinations.isEmpty();
}
int destination(const KisKeyframeSP &keyframe) const
{
const int oldTime = keyframe->time();
const int newTime = destinations.value(keyframe, -1);
if (newTime == -1 && !sources.value(oldTime, KisKeyframeBaseSP())) {
return oldTime;
}
return newTime;
}
KisKeyframeSP firstTrueKeyframeAfter(int destinationTime) const
{
KisKeyframeSP unmovedKeyframe;
{
int time = destinationTime;
KisKeyframeSP key;
do {
key = channel->nextKeyframe(time);
time = key ? key->time() : -1;
} while(destinations.contains(key));
unmovedKeyframe = key;
}
KisKeyframeSP movedKeyframe;
int movedTime;
{
auto sourceIt = KisCollectionUtils::firstAfter(sources, destinationTime);
// Skip non-keyframes (e.g. repeat frames)
while (sourceIt != sources.cend() && !sourceIt.value().dynamicCast<KisKeyframe>()) {
sourceIt++;
}
movedTime = (sourceIt != sources.cend()) ? sourceIt.key() : -1;
}
if (movedKeyframe && unmovedKeyframe) {
return (movedTime < unmovedKeyframe->time()) ? movedKeyframe : unmovedKeyframe;
} else {
return movedKeyframe ? movedKeyframe : unmovedKeyframe;
}
}
};
bool areValidMoveSources(const KisKeyframeChannel *channel, QVector<KeyframeMove> moves)
{
Q_FOREACH(const KeyframeMove &move, moves) {
if (move.keyframe->channel() != channel) return false;
}
std::sort(moves.begin(), moves.end(),
[](const KeyframeMove &lhs, const KeyframeMove &rhs){ return lhs.oldTime < rhs.oldTime; }
);
for (int i = 1; i < moves.size(); i++) {
if (moves[i - 1].keyframe == moves[i].keyframe) return false;
}
return true;
}
CycleSP cycleAfterMove(const CycleSP &cycle, const KeyframeMapping &movedKeyframes)
{
const KisKeyframeChannel *channel = cycle->channel();
KisTimeSpan newRange;
int firstDestination = -1, lastDestination = -1;
KisKeyframeSP newFirstKeyframe, newLastKeyframe;
KisKeyframeSP key = cycle->firstSourceKeyframe();
while (!key.isNull() && key->time() <= cycle->lastSourceKeyframe()->time()) {
const int destination = movedKeyframes.destinations.value(key, -1);
const int newTime = (destination >=0) ? destination : key->time();
const KisKeyframeBaseSP overwritingKey = (destination >= 0)
? nullptr
: movedKeyframes.sources.value(key->time(), nullptr);
if (!overwritingKey) {
if (!newFirstKeyframe || newTime < firstDestination) {
firstDestination = newTime;
newFirstKeyframe = key;
}
if (!newLastKeyframe || newTime > lastDestination) {
lastDestination = newTime;
newLastKeyframe = key;
}
}
key = channel->nextKeyframe(key->time());
}
if (!newLastKeyframe) return CycleSP();
if (newFirstKeyframe == cycle->firstSourceKeyframe() && newLastKeyframe == cycle->lastSourceKeyframe()) {
return cycle;
}
return toQShared(new KisAnimationCycle(*cycle, newFirstKeyframe, newLastKeyframe));
}
QVector<CycleSP> resolveCycleOverlaps(QVector<CycleSP> &cycles, const KeyframeMapping &movedKeyframes)
{
std::sort(cycles.begin(), cycles.end(), [&movedKeyframes](const CycleSP &lhs, const CycleSP &rhs) {
const int lhsTime = movedKeyframes.destination(lhs->firstSourceKeyframe());
const int rhsTime = movedKeyframes.destination(rhs->firstSourceKeyframe());
return lhsTime < rhsTime;
});
CycleSP lhs = cycles[0];
for (int i = 1; i < cycles.size(); i++) {
const CycleSP &rhs = cycles[i];
const int lhsEnd = movedKeyframes.destination(lhs->lastSourceKeyframe());
const int rhsStart = movedKeyframes.destination(rhs->firstSourceKeyframe());
if (rhsStart < lhsEnd) {
const int rhsEnd = movedKeyframes.destination(rhs->lastSourceKeyframe());
if (rhsEnd < lhsEnd) {
// Rhs cycle is entirely inside lhs one: drop it
cycles[i] = CycleSP();
continue;
} else {
// TODO: logic for picking the cycle to truncate?
KisKeyframeSP truncatedStart = movedKeyframes.firstTrueKeyframeAfter(lhsEnd);
cycles[i] = toQShared(new KisAnimationCycle(*rhs, truncatedStart, rhs->lastSourceKeyframe()));
}
}
lhs = cycles[i];
}
return cycles;
}
QVector<CycleSP> cyclesAfterMoves(const KeyframeMapping &movedKeyframes)
{
const KisKeyframeChannel *channel = movedKeyframes.destinations.begin().key()->channel();
QVector<CycleSP> cycles;
Q_FOREACH(const CycleSP cycle, channel->cycles()) {
const CycleSP newCycle = cycleAfterMove(cycle, movedKeyframes);
if (newCycle) {
cycles << newCycle;
}
}
return resolveCycleOverlaps(cycles, movedKeyframes);
}
bool validateRepeats(const QVector<CycleSP > &cycles, const KeyframeMapping &movedKeyframes)
{
Q_FOREACH(const CycleSP &cycle, cycles) {
if (!cycle) continue;
const int cycleStart = movedKeyframes.destination(cycle->firstSourceKeyframe());
const int cycleEnd = movedKeyframes.destination(cycle->lastSourceKeyframe());
// If any repeat frame would land within the cycle, refuse the operation.
auto keyIt = KisCollectionUtils::firstAfter(movedKeyframes.sources, cycleStart);
while (keyIt != movedKeyframes.sources.cend() && keyIt.key() < cycleEnd) {
const QSharedPointer<KisRepeatFrame> repeat = keyIt.value().dynamicCast<KisRepeatFrame>();
if (repeat) return false;
keyIt++;
}
}
return true;
}
void updateCycles(const KisKeyframeChannel *channel, QVector<CycleSP> cyclesAfter, KUndo2Command *parentCommand)
{
const QList<CycleSP> &cyclesBefore = channel->cycles();
// Remove out-of-date definitions
Q_FOREACH(const CycleSP &cycle, cyclesBefore) {
if (!cyclesAfter.contains(cycle)) {
new KisDefineCycleCommand(cycle, nullptr, parentCommand);
}
}
// Add new cycle definitions
Q_FOREACH(const CycleSP &cycle, cyclesAfter) {
if (!cyclesBefore.contains(cycle)) {
new KisDefineCycleCommand(nullptr, cycle, parentCommand);
}
}
}
void deleteOverwrittenKeys(KeyframeMapping moves, KUndo2Command *parentCommand)
{
QVector<KisKeyframeBaseSP> deletedKeys;
for (auto it = moves.destinations.cbegin(); it != moves.destinations.cend(); ++it) {
const KisKeyframeBaseSP &keyframe = it.key();
const int newTime = it.value();
const KisKeyframeBaseSP &originalKey = keyframe->channel()->itemAt(newTime);
if (originalKey) {
const bool isOverwritten = !moves.destinations.contains(originalKey);
if (isOverwritten) {
deletedKeys.append(originalKey);
}
}
}
Q_FOREACH(KisKeyframeBaseSP keyframe, deletedKeys) {
new KisReplaceKeyframeCommand(keyframe->channel(), keyframe->time(), KisKeyframeBaseSP(), parentCommand);
}
}
KUndo2CommandSP KisKeyframeCommands::tryMoveKeyframes(KisKeyframeChannel *channel, QVector<KeyframeMove> moves, KUndo2Command *parentCommand)
{
KUndo2Command *command = new KUndo2Command(parentCommand);
if (!areValidMoveSources(channel, moves)) return nullptr;
const KeyframeMapping movedKeyframes(channel, moves);
if (movedKeyframes.isEmpty()) return nullptr;
const QVector<CycleSP> cycles = cyclesAfterMoves(movedKeyframes);
if (!validateRepeats(cycles, movedKeyframes)) return nullptr;
deleteOverwrittenKeys(movedKeyframes, command);
new KisMoveKeyframesCommand(moves, command);
updateCycles(channel, cycles, command);
return toQShared(command);
}
KisReplaceKeyframeCommand::KisReplaceKeyframeCommand(KisKeyframeChannel *channel, int time, KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand)
: KUndo2Command(parentCommand),
m_channel(channel),
......@@ -56,7 +355,7 @@ void KisSwapFramesCommand::undo()
m_channel->swapKeyframesImpl(m_lhsFrame, m_rhsFrame);
}
KisDefineCycleCommand::KisDefineCycleCommand(QSharedPointer<KisAnimationCycle> oldCycle, QSharedPointer<KisAnimationCycle> newCycle, KUndo2Command *parentCommand)
KisDefineCycleCommand::KisDefineCycleCommand(CycleSP oldCycle, CycleSP newCycle, KUndo2Command *parentCommand)
: KUndo2Command(parentCommand)
, m_channel(oldCycle ? oldCycle->channel() : newCycle->channel())
, m_oldCycle(oldCycle)
......
......@@ -21,6 +21,23 @@
#include "kundo2command.h"
#include "kritaimage_export.h"
namespace KisKeyframeCommands
{
struct KRITAIMAGE_EXPORT KeyframeMove
{
KisKeyframeBaseSP keyframe;
int oldTime, newTime;
KeyframeMove() = default;
KeyframeMove(KisKeyframeBaseSP keyframe, int newTime);
};
/**
* Returns either a new command for operations needed to move the keyframes or null if the operation is invalid against the current state of the channel
*/
KRITAIMAGE_EXPORT KUndo2CommandSP tryMoveKeyframes(KisKeyframeChannel *channel, QVector<KeyframeMove> moves, KUndo2Command *parentCommand);
}
class KRITAIMAGE_EXPORT KisReplaceKeyframeCommand : public KUndo2Command
{
public:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment