provisional/phy-editor-feedback.md の合意事項と phy-editor-keybindings.md の割り当て案に沿って、PHY 画面を一から書き直した。 App.vue は共通のシェル(入出力・履歴・タブ)だけにし、PHY は src/ui/phy/ に、コンポーネント・composable・純粋モジュール(logic.ts, viewport.ts)を分けて置いた。 - 画面確認で見つかった不具合(読み込み時の全体表示、Shift+クリック、削除の確認の大きさ、Alt+↑、フォーカス中の二重描画、長押し中の選択など)を直し、キャンバス上の実際の操作で確かめる e2e を足した - 階層パネルでクラスターの子を余分な折りたたみで包まず、件数を配下のキー・部品の数にそろえた - 実装は Codex(Luna)。検証: vue-tsc 0件、Vitest 64件、Playwright 30件、prettier --check Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbfgHT848s3JbuFw8MKFF8
104 lines
4.0 KiB
TypeScript
104 lines
4.0 KiB
TypeScript
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<string[]>;
|
|
cut: Ref<string[]>;
|
|
grid: Ref<number>;
|
|
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 };
|
|
}
|