/** * 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, }); }