Timeline keys can be copied and pasted at the playhead, duplicated with Ctrl+drag, and frames inserted or removed from a right-click menu (#102)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
195fc94e08
commit
bf02c9e8b3
11 files changed
+666
-23
No files matched your search
@@ -6,13 +6,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
AddAnimationTrackCommand,
|
||||
copyAnimationKeys,
|
||||
MoveAnimationKeysCommand,
|
||||
PasteAnimationKeysCommand,
|
||||
RemoveAnimationKeysCommand,
|
||||
RetargetAnimationTrackCommand,
|
||||
SetAnimationKeyCommand,
|
||||
SetAnimationRootCommand,
|
||||
SetKeyInterpolationCommand,
|
||||
SetTrackRelativeCommand,
|
||||
ShiftAnimationFramesCommand,
|
||||
shiftedFrame,
|
||||
} from '../../src/core/commands/animationCommands';
|
||||
import {
|
||||
CreateAssetCommand,
|
||||
@@ -386,6 +390,136 @@ describe('editing the timeline', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('copy, paste, duplicate and frames (#102)', () => {
|
||||
it('pastes relative to the frame, into the same rows, replacing a key there, and undoes in one step', () => {
|
||||
const f = makeSpriteScene();
|
||||
const walk = addAnimation(f, f.spriteId);
|
||||
const x = addRow(f, walk, 'x', [
|
||||
[2, 20],
|
||||
[4, 40],
|
||||
[9, 90],
|
||||
]);
|
||||
const rot = addRow(f, walk, 'rotation', [[3, 1]]);
|
||||
f.stack.execute(new SetKeyInterpolationCommand(walk.id, [{ trackId: x, frame: 2 }], 'ease-in'));
|
||||
const clip = copyAnimationKeys(f.project, walk, [
|
||||
{ trackId: x, frame: 2 },
|
||||
{ trackId: x, frame: 4 },
|
||||
{ trackId: rot, frame: 3 },
|
||||
])!;
|
||||
expect(clip.rows.map((r) => r.keys.map((k) => k.offset))).toEqual([[0, 2], [1]]);
|
||||
|
||||
const paste = new PasteAnimationKeysCommand(walk.id, clip, 7);
|
||||
f.stack.execute(paste);
|
||||
expect(rowOf(walk, x).keys).toEqual([
|
||||
{ frame: 2, value: 20, interpolation: 'ease-in' },
|
||||
{ frame: 4, value: 40 },
|
||||
{ frame: 7, value: 20, interpolation: 'ease-in' },
|
||||
{ frame: 9, value: 40 },
|
||||
]);
|
||||
expect(rowOf(walk, rot).keys).toEqual(keys([3, 1], [8, 1]));
|
||||
expect(paste.pasted).toEqual([
|
||||
{ trackId: x, frame: 7 },
|
||||
{ trackId: x, frame: 9 },
|
||||
{ trackId: rot, frame: 8 },
|
||||
]);
|
||||
f.stack.undo();
|
||||
expect(rowOf(walk, x).keys).toHaveLength(3);
|
||||
expect(rowOf(walk, rot).keys).toEqual(keys([3, 1]));
|
||||
});
|
||||
|
||||
it('pastes one row into the row picked, only of the same kind', () => {
|
||||
const f = makeSpriteScene();
|
||||
const walk = addAnimation(f, f.spriteId);
|
||||
const x = addRow(f, walk, 'x', [[0, 5]]);
|
||||
const rot = addRow(f, walk, 'rotation', []);
|
||||
const color = addRow(f, walk, 'color', []);
|
||||
const clip = copyAnimationKeys(f.project, walk, [{ trackId: x, frame: 0 }])!;
|
||||
|
||||
f.stack.execute(new PasteAnimationKeysCommand(walk.id, clip, 3, rot));
|
||||
expect(rowOf(walk, rot).keys).toEqual(keys([3, 5]));
|
||||
expect(rowOf(walk, x).keys).toEqual(keys([0, 5]));
|
||||
expect(() => f.stack.execute(new PasteAnimationKeysCommand(walk.id, clip, 3, color))).toThrow(
|
||||
CommandError,
|
||||
);
|
||||
expect(rowOf(walk, color).keys).toEqual([]);
|
||||
});
|
||||
|
||||
it('finds the same property in another animation, and leaves out rows it has not', () => {
|
||||
const f = makeSpriteScene();
|
||||
const walk = addAnimation(f, f.spriteId);
|
||||
const x = addRow(f, walk, 'x', [[1, 10]]);
|
||||
const rot = addRow(f, walk, 'rotation', [[1, 2]]);
|
||||
const run = addAnimation(f, f.spriteId, {}, 'Run');
|
||||
const runX = addRow(f, run, 'x', []);
|
||||
const clip = copyAnimationKeys(f.project, walk, [
|
||||
{ trackId: x, frame: 1 },
|
||||
{ trackId: rot, frame: 1 },
|
||||
])!;
|
||||
|
||||
const paste = new PasteAnimationKeysCommand(run.id, clip, 0);
|
||||
f.stack.execute(paste);
|
||||
expect(rowOf(current(f, run), runX).keys).toEqual(keys([0, 10]));
|
||||
expect(paste.skipped).toBe(1);
|
||||
|
||||
const empty = addAnimation(f, f.spriteId, {}, 'Idle');
|
||||
expect(() => f.stack.execute(new PasteAnimationKeysCommand(empty.id, clip, 0))).toThrow(CommandError);
|
||||
});
|
||||
|
||||
it('duplicates keys: the originals stay, the copies replace what they land on', () => {
|
||||
const f = makeSpriteScene();
|
||||
const walk = addAnimation(f, f.spriteId);
|
||||
const id = addRow(f, walk, 'x', [
|
||||
[0, 0],
|
||||
[2, 20],
|
||||
[5, 50],
|
||||
]);
|
||||
const picked = [
|
||||
{ trackId: id, frame: 0 },
|
||||
{ trackId: id, frame: 2 },
|
||||
];
|
||||
f.stack.execute(new MoveAnimationKeysCommand(walk.id, picked, 3, true));
|
||||
expect(rowOf(walk, id).keys).toEqual(keys([0, 0], [2, 20], [3, 0], [5, 20]));
|
||||
expect(() => f.stack.execute(new MoveAnimationKeysCommand(walk.id, picked, 0, true))).toThrow(
|
||||
CommandError,
|
||||
);
|
||||
f.stack.undo();
|
||||
expect(rowOf(walk, id).keys).toEqual(keys([0, 0], [2, 20], [5, 50]));
|
||||
});
|
||||
|
||||
it('inserts frames over every row from the frame on, and removes them taking their keys along', () => {
|
||||
const f = makeSpriteScene();
|
||||
const walk = addAnimation(f, f.spriteId);
|
||||
const x = addRow(f, walk, 'x', [
|
||||
[0, 0],
|
||||
[4, 40],
|
||||
[9, 90],
|
||||
]);
|
||||
const rot = addRow(f, walk, 'rotation', [
|
||||
[3, 1],
|
||||
[6, 2],
|
||||
]);
|
||||
|
||||
f.stack.execute(new ShiftAnimationFramesCommand(walk.id, 4, 3));
|
||||
expect(rowOf(walk, x).keys).toEqual(keys([0, 0], [7, 40], [12, 90]));
|
||||
expect(rowOf(walk, rot).keys).toEqual(keys([3, 1], [9, 2]));
|
||||
expect(frameCount(walk)).toBe(13);
|
||||
f.stack.undo();
|
||||
|
||||
f.stack.execute(new ShiftAnimationFramesCommand(walk.id, 3, -2));
|
||||
expect(rowOf(walk, x).keys).toEqual(keys([0, 0], [7, 90]));
|
||||
expect(rowOf(walk, rot).keys).toEqual(keys([4, 2]));
|
||||
f.stack.undo();
|
||||
expect(rowOf(walk, x).keys).toEqual(keys([0, 0], [4, 40], [9, 90]));
|
||||
expect(rowOf(walk, rot).keys).toEqual(keys([3, 1], [6, 2]));
|
||||
|
||||
// Past the last key nothing would change – no empty undo step.
|
||||
expect(() => f.stack.execute(new ShiftAnimationFramesCommand(walk.id, 10, 5))).toThrow(CommandError);
|
||||
expect(shiftedFrame(4, 3, -2)).toBeNull();
|
||||
expect(shiftedFrame(5, 3, -2)).toBe(3);
|
||||
expect(shiftedFrame(2, 3, -2)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saving', () => {
|
||||
it('keeps root, rows and keys through a round trip, and cleans what a hand-edited file brings', () => {
|
||||
const f = makeSpriteScene();
|
||||
|
||||
Reference in new issue
Block a user