Files
feuerwerk/src/core/model/clipboard.ts
T
adminandClaude Opus 5.5 0648853356
game-maker / gate (push) Successful in 4m57s
game-maker / suite (push) Successful in 18m29s
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>
2026-10-13 06:56:25 +02:00

129 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* What Ctrl+C takes (#240): a snapshot of placed objects or of a piece of graph, detached from the
* model so later edits to the originals do not change what is pasted. Ids are the originals'; the
* paste commands hand out fresh ones.
*/
import {
type AnnotationKind,
findAnnotation,
type Graph,
type GraphAnnotation,
type GraphEdge,
type GraphNode,
} from '../graph/Graph';
import { editorTransform, parentTransform, positionOf } from '../properties/transform';
import { findObject, subtreeIds, subtreeRoots, treeOrder } from './hierarchy';
import { isLiveInstance, isTemplateContent, resolvedHolder, splitInstanceId } from './instance';
import type { CompositionClip, CompositionItem, SpriteAsset } from './Asset';
import { isCompositionSprite } from './composition';
import type { Mat } from './matrix';
import type { Position } from './Properties';
import type { ObjectHolder, Project } from './Project';
import type { SceneObject } from './SceneObject';
/**
* A copied subtree's top: where the editor showed it, editor offsets included (#263), and the
* frame its own rows were meant in.
*/
export interface ClipRoot {
id: string;
shown: Position;
parentMatrix: Mat;
}
export interface ObjectClip {
kind: 'objects';
/** Parents before children; a root's `parentId` is the one it had where it was copied. */
objects: SceneObject[];
roots: ClipRoot[];
/** Copied out of a composition (#417): the items of the roots that are items, for a paste there. */
items?: CompositionItem[];
/** The sprites of a composition's own the objects wear (#419), back for a paste after a cut. */
sprites?: SpriteAsset[];
}
export interface GraphClip {
kind: 'graph';
nodes: GraphNode[];
/** Only the edges between copied nodes – a link to a node left behind has nowhere to go. */
edges: GraphEdge[];
annotations: { kind: AnnotationKind; annotation: GraphAnnotation }[];
}
/** 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,
* so its resolved content stays behind; content picked out of a placed container is copied as the
* ordinary objects it is shown as.
*/
export function copyObjects(
project: Project,
holder: ObjectHolder,
ids: readonly string[],
): ObjectClip | null {
const view = resolvedHolder(project, holder);
const roots = subtreeRoots(
view,
ids.filter((id) => findObject(view, id)),
);
if (roots.length === 0) return null;
const members = subtreeIds(view, roots);
const instances = new Set(
view.objects
.filter(
(o) => members.has(o.id) && !isTemplateContent(holder, o.id) && isLiveInstance(project, holder, o),
)
.map((o) => o.id),
);
const shownByInstance = (id: string) =>
isTemplateContent(holder, id) && instances.has(splitInstanceId(id)?.instanceId ?? '');
const objects = treeOrder(view)
.map((entry) => entry.object)
.filter((o) => members.has(o.id) && !shownByInstance(o.id))
.map((o) => ({ ...structuredClone(o), editorOffset: { x: 0, y: 0 } }));
const items = holder.type === 'composition' ? holder.items.filter((i) => roots.includes(i.id)) : [];
const worn = new Set(objects.map((o) => o.assetId));
const sprites = project.assets.filter(isCompositionSprite).filter((a) => worn.has(a.id));
return {
kind: 'objects',
objects,
...(items.length > 0 && { items: structuredClone(items) }),
...(sprites.length > 0 && { sprites: structuredClone(sprites) }),
roots: roots.map((id) => {
const object = findObject(view, id)!;
return {
id,
shown: positionOf(editorTransform(project, view, object)),
parentMatrix: parentTransform(project, view, object.parentId).m,
};
}),
};
}
export function copyGraph(
graph: Graph,
nodeIds: readonly string[],
annotationIds: readonly string[],
): GraphClip | null {
const ids = new Set(nodeIds);
const nodes = graph.nodes.filter((n) => ids.has(n.id));
const annotations = annotationIds.flatMap((id) => {
const found = findAnnotation(graph, id);
return found ? [found] : [];
});
if (nodes.length === 0 && annotations.length === 0) return null;
return structuredClone({
kind: 'graph',
nodes,
edges: graph.edges.filter((e) => ids.has(e.from.node) && ids.has(e.to.node)),
annotations,
});
}