Select composition items with a band and copy clips too (#417)
The composition tab only took single clicks and Ctrl-clicks, and animation clips could not be copied at all. A band selection and Copy/Paste for clips make longer compositions quicker to arrange. - Dragging from an empty spot draws a band. It selects every item it touches, across tracks. Ctrl adds to the selection, because Shift plus drag already pans the view. - With Alt held the band selects clips instead and may start on an item, since items fill most of a track. - Ctrl+C copies the selected clips as their own clipboard kind. Ctrl+V places them like items: under the pointer over the tracks, otherwise at the playhead on the selected track. - Clips of a track never overlap. A paste onto taken frames moves the whole copy on frame by frame until every clip fits (`pastedClips`). - The paste is one undo step and selects the new clips. Tests: - Gate on melektaus-atx: tsc, eslint, Prettier clean, 305 files, 4724 tests (2 new). - e2e 69: ALL PASSED, 17 checks (69m new). Band over three items on two tracks, Alt band from an item picks both clips, first paste at frames 50 and 60, second paste moves on to 56 and 66. - Counter-check: without moving on, the paste unit test is red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5df2173559
commit
0648853356
8 files changed
+289
-13
No files matched your search
@@ -25,13 +25,20 @@ import {
|
||||
mediaMarks,
|
||||
movedClips,
|
||||
movedItems,
|
||||
pastedClips,
|
||||
pastedItems,
|
||||
resizedItem,
|
||||
snappedToMedia,
|
||||
trimmedItem,
|
||||
} from '../../src/core/model/compositionEdit';
|
||||
import { copyObjects } from '../../src/core/model/clipboard';
|
||||
import { hitAt, overlaps, uncoveredEnd } from '../../src/editor/composition/compositionView';
|
||||
import {
|
||||
clipsInBand,
|
||||
hitAt,
|
||||
itemsInBand,
|
||||
overlaps,
|
||||
uncoveredEnd,
|
||||
} from '../../src/editor/composition/compositionView';
|
||||
import { placementRejection } from '../../src/core/model/placement';
|
||||
import { findAsset, findAssetOf } from '../../src/core/model/Project';
|
||||
import { makeProject } from '../helpers';
|
||||
@@ -282,6 +289,54 @@ describe('items and their objects', () => {
|
||||
expect(pastedItems(copied, ids, 0, tracks, 2).map((i) => i.track)).toEqual(['t2', 't3']);
|
||||
});
|
||||
|
||||
it('pasted clips keep their timing and move on until none lies over another clip', () => {
|
||||
const clip = (id: string, track: string, start: number) =>
|
||||
({ id, track, animation: 'anim', start }) satisfies CompositionClip;
|
||||
// Every clip is 10 frames long.
|
||||
const lengthOf = () => 10;
|
||||
const tracks = ['t1', 't2'];
|
||||
const copied = [clip('a', 't1', 0), clip('b', 't2', 15)];
|
||||
const ids = new Map([
|
||||
['a', 'a2'],
|
||||
['b', 'b2'],
|
||||
]);
|
||||
// Room where they land: there they stay.
|
||||
expect(pastedClips(copied, [clip('x', 't1', 50)], ids, 30, tracks, lengthOf)).toEqual([
|
||||
clip('a2', 't1', 30),
|
||||
clip('b2', 't2', 45),
|
||||
]);
|
||||
// `a2` at 45 would lie over `x` (50–60), `b2` over `y` (60–70): `a2` waits until 60, `b2` comes along.
|
||||
const taken = [clip('x', 't1', 50), clip('y', 't2', 60)];
|
||||
expect(pastedClips(copied, taken, ids, 45, tracks, lengthOf)).toEqual([
|
||||
clip('a2', 't1', 60),
|
||||
clip('b2', 't2', 75),
|
||||
]);
|
||||
// Onto the clips they were copied from, they go right behind them, shifted to a row as far as it goes.
|
||||
expect(pastedClips([clip('a', 't1', 0)], [clip('a', 't1', 0)], ids, 0, tracks, lengthOf, 1)).toEqual([
|
||||
clip('a2', 't2', 0),
|
||||
]);
|
||||
expect(pastedClips([clip('a', 't1', 0)], [clip('a', 't1', 0)], ids, 0, tracks, lengthOf)).toEqual([
|
||||
clip('a2', 't1', 10),
|
||||
]);
|
||||
});
|
||||
|
||||
it('a band picks the items and clips it touches', () => {
|
||||
const view = { panX: 0, pxPerTick: 1, panY: 0, trackHeight: 40 };
|
||||
const tracks = [
|
||||
{ id: 't1', name: 'One' },
|
||||
{ id: 't2', name: 'Two' },
|
||||
];
|
||||
const items = [item('a', 't1', 0, 50), item('b', 't1', 100, 50), item('c', 't2', 0, 50)];
|
||||
const clips = [{ id: 'k', track: 't2', animation: 'anim', start: 20 }];
|
||||
// Across the first track, from 40 to 60: only `a` reaches in.
|
||||
expect(itemsInBand(view, tracks, items, { x: 40, y: 5 }, { x: 60, y: 30 })).toEqual(['a']);
|
||||
// Down over both tracks at the start, drawn from bottom right to top left.
|
||||
expect(itemsInBand(view, tracks, items, { x: 10, y: 70 }, { x: 5, y: 10 })).toEqual(['a', 'c']);
|
||||
// Clips lie in the lower part of their track: a band through its upper part misses them.
|
||||
expect(clipsInBand(view, tracks, clips, () => 10, { x: 0, y: 42 }, { x: 60, y: 50 })).toEqual([]);
|
||||
expect(clipsInBand(view, tracks, clips, () => 10, { x: 0, y: 42 }, { x: 60, y: 70 })).toEqual(['k']);
|
||||
});
|
||||
|
||||
it('a copied item is pasted with its object and overrides under new ids, in one undo step', () => {
|
||||
const cut = makeCut();
|
||||
const id = cut.add(10, 60);
|
||||
|
||||
Reference in new issue
Block a user