Select composition items with a band and copy clips too (#417)
game-maker / gate (push) Successful in 4m57s
game-maker / suite (push) Successful in 18m29s

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:
adminandClaude Opus 5.5 committed 2026-10-13 06:56:25 +02:00
1 parent 5df2173559
commit 0648853356
8 files changed
+289 -13

No files matched your search

+8 -2
View File
@@ -14,7 +14,7 @@ import {
import { editorTransform, parentTransform, positionOf } from '../properties/transform';
import { findObject, subtreeIds, subtreeRoots, treeOrder } from './hierarchy';
import { isLiveInstance, isTemplateContent, resolvedHolder, splitInstanceId } from './instance';
import type { CompositionItem, SpriteAsset } from './Asset';
import type { CompositionClip, CompositionItem, SpriteAsset } from './Asset';
import { isCompositionSprite } from './composition';
import type { Mat } from './matrix';
import type { Position } from './Properties';
@@ -50,7 +50,13 @@ export interface GraphClip {
annotations: { kind: AnnotationKind; annotation: GraphAnnotation }[];
}
export type Clip = ObjectClip | GraphClip;
/** Animation clips of a composition (#417), for a paste into the same one or another. */
export interface CompositionClipsClip {
kind: 'compositionClips';
clips: CompositionClip[];
}
export type Clip = ObjectClip | GraphClip | CompositionClipsClip;
/**
* The selected objects with everything below them. A placed container keeps showing its template,
+33
View File
@@ -136,6 +136,39 @@ export function pastedItems(
trackIds: readonly string[],
row?: number,
): CompositionItem[] {
return pasted(items, ids, at, trackIds, row);
}
/**
* Copied clips placed as `pastedItems` places items, then moved on frame by frame as long as one
* would lie over a clip of `clips` or over another of the copy – clips of a track never overlap.
*/
export function pastedClips(
copied: readonly CompositionClip[],
clips: readonly CompositionClip[],
ids: ReadonlyMap<string, string>,
at: number,
trackIds: readonly string[],
lengthOf: (clip: CompositionClip) => number,
row?: number,
): CompositionClip[] {
const placed = pasted(copied, ids, at, trackIds, row);
if (placed.length === 0) return [];
// Past the end of every clip there is room, so the search ends there at the latest.
const last = Math.max(0, ...clips.map((c) => c.start + Math.max(1, lengthOf(c))));
for (let by = 0; ; by++) {
const moved = placed.map((c) => ({ ...c, start: c.start + by }));
if (apart([...clips, ...moved], lengthOf) || moved.every((c) => c.start >= last)) return moved;
}
}
function pasted<T extends Placed>(
items: readonly T[],
ids: ReadonlyMap<string, string>,
at: number,
trackIds: readonly string[],
row?: number,
): T[] {
if (items.length === 0) return [];
const first = Math.min(...items.map((i) => i.start));
const index = new Map(trackIds.map((id, i) => [id, i]));