import type { Ref } from "vue"; import type { ProjectFile } from "../../domain/project"; import type { KeyNode, Node } from "../../domain/phy"; import { cloneNodesWithNewIds, insertNodes, reparent, reparentToVariant } from "../../domain/phyCommands"; import { worldTransform, type Affine } from "../../domain/geometry"; import { importKle } from "../../io/kleImport"; import { treeIndex, pasteLocalTransform } from "./logic"; type ClipboardNode = { node: Node; world: Affine }; type Options = { project: () => ProjectFile; mismatch: () => boolean; selected: Ref; cut: Ref; grid: Ref; node: (id: string) => Node | undefined; selectedContainer: (id: string) => string | null; insertionParent: () => string | null; parentMatrix: (project: ProjectFile, parent: string | null) => Affine; dispatch: (label: string, update: (project: ProjectFile) => ProjectFile) => void; setSelected: (ids: string[]) => void; }; export function usePhyClipboard(options: Options) { let clipboard: ClipboardNode[] = []; let marker = ""; function copyEvent(event: ClipboardEvent) { if (!options.selected.value.length || !event.clipboardData) return; const project = options.project(); clipboard = options.selected.value.map((id) => ({ node: structuredClone(options.node(id)!), world: worldTransform(project.phy, id), })); marker = `beyond-kle-clipboard:${crypto.randomUUID()}`; event.clipboardData.setData("text/plain", marker); if (event.type === "cut") options.cut.value = [...options.selected.value]; event.preventDefault(); } function pasteEvent(event: ClipboardEvent) { if (options.mismatch()) return; const text = event.clipboardData?.getData("text/plain") || ""; if (text === marker && clipboard.length) { event.preventDefault(); const parent = options.selected.value.length ? options.selectedContainer(options.selected.value[0]!) : options.insertionParent(); if (options.cut.value.length) { const ids = [...options.cut.value]; options.dispatch("カット貼り付け", (project) => ({ ...ids.reduce((next, id) => { const isVariant = parent !== null && treeIndex(next.phy.root).groupForVariant(parent) !== null; return { ...next, phy: isVariant ? reparentToVariant(next.phy, id, parent!) : reparent(next.phy, id, parent), }; }, project), })); options.setSelected(ids); options.cut.value = []; } else { let ids: string[] = []; options.dispatch("貼り付け", (project) => { const copies = cloneNodesWithNewIds(clipboard.map((item) => item.node)); const parentWorld = options.parentMatrix(project, parent); copies.forEach((node, index) => { node.transform = pasteLocalTransform(clipboard[index]!.world, parentWorld, options.grid.value); ids.push(node.id); }); return { ...project, phy: insertNodes(project.phy, parent, copies) }; }); options.setSelected(ids); } return; } try { const { phy, keyOrder } = importKle(JSON.parse(text)); if (!event.clipboardData) return; event.preventDefault(); let ids: string[] = []; options.dispatch("KLE貼り付け", (project) => { const nodes = phy.root .filter((node): node is KeyNode => node.type === "key") .map((key) => ({ ...key, profile: project.phy.profiles[0]?.id || key.profile })); ids = nodes.map((node) => node.id); const parent = options.selected.value.length ? options.selectedContainer(options.selected.value[0]!) : options.insertionParent(); return { ...project, phy: insertNodes(project.phy, parent, nodes), idx: { ...project.idx, order: [...project.idx.order, ...keyOrder] }, }; }); options.setSelected(ids); } catch { // 外部クリップボードがKLE JSONでなければ無視する。 } } return { copyEvent, pasteEvent }; }