1 parent
9747c4b17e
commit
0d821cfa22
4 files changed
+140
-60
No files matched your search
@@ -1,13 +1,15 @@
|
||||
import { useCallback, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { App } from "antd";
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { FRAME_HEADER_HEIGHT, getFrameChildIds, getFrameChildren, isFrameNode } from "@/lib/canvas/canvas-frame";
|
||||
import { alignCanvasNodes, layoutCanvasFlow, layoutCanvasNodes, nextCanvasVersionLabel, type CanvasAlignmentMode } from "@/lib/canvas/canvas-layout";
|
||||
import { createCanvasNode, removeCanvasNodes } from "@/lib/canvas/canvas-project-domain";
|
||||
import { getGenerationCount } from "@/lib/canvas/canvas-project-generation";
|
||||
import { isolateCopiedNodeMetadata } from "@/lib/canvas/canvas-node-copy";
|
||||
import { useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type ContextMenuState, type Position } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ContextMenuState, type Position } from "@/types/canvas";
|
||||
import { cloneCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage";
|
||||
|
||||
type CanvasClipboard = {
|
||||
@@ -30,9 +32,6 @@ type UseCanvasNodeOperationsOptions = {
|
||||
onNodesDeleted: (removedIds: Set<string>, nextNodes: CanvasNodeData[], removedNodes: CanvasNodeData[]) => void;
|
||||
};
|
||||
|
||||
const NODE_STATUS_IDLE = "idle" as const;
|
||||
const NODE_STATUS_SUCCESS = "success" as const;
|
||||
|
||||
export function useCanvasNodeOperations({
|
||||
projectId,
|
||||
nodesRef,
|
||||
@@ -50,8 +49,30 @@ export function useCanvasNodeOperations({
|
||||
const { message } = App.useApp();
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const clipboardRef = useRef<CanvasClipboard | null>(null);
|
||||
const preferCopiedNodesRef = useRef(false);
|
||||
const markerWritePendingRef = useRef(false);
|
||||
const markerWriteSequenceRef = useRef(0);
|
||||
const [hasCopiedNodes, setHasCopiedNodes] = useState(false);
|
||||
|
||||
const releaseCopiedNodesPastePriority = useCallback(() => {
|
||||
markerWriteSequenceRef.current += 1;
|
||||
markerWritePendingRef.current = false;
|
||||
preferCopiedNodesRef.current = false;
|
||||
}, []);
|
||||
|
||||
const shouldPreferCopiedNodes = useCallback(() => markerWritePendingRef.current || preferCopiedNodesRef.current, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("copy", releaseCopiedNodesPastePriority);
|
||||
window.addEventListener("cut", releaseCopiedNodesPastePriority);
|
||||
window.addEventListener("blur", releaseCopiedNodesPastePriority);
|
||||
return () => {
|
||||
window.removeEventListener("copy", releaseCopiedNodesPastePriority);
|
||||
window.removeEventListener("cut", releaseCopiedNodesPastePriority);
|
||||
window.removeEventListener("blur", releaseCopiedNodesPastePriority);
|
||||
};
|
||||
}, [releaseCopiedNodesPastePriority]);
|
||||
|
||||
const commitNodes = useCallback((nextNodes: CanvasNodeData[]) => {
|
||||
nodesRef.current = nextNodes;
|
||||
setNodes(nextNodes);
|
||||
@@ -256,7 +277,7 @@ export function useCanvasNodeOperations({
|
||||
const versionRootId = isFrameNode(source) ? undefined : source.metadata?.versionOfNodeId || source.id;
|
||||
const versionLabel = versionRootId ? nextCanvasVersionLabel(versionRootId, nodesRef.current) : undefined;
|
||||
const copiedNodes = sources.map((node) => {
|
||||
const metadata: CanvasNodeMetadata = { ...node.metadata, frame: node.metadata?.frame ? { ...node.metadata.frame } : undefined };
|
||||
const metadata = isolateCopiedNodeMetadata(node, idMap);
|
||||
if (node.type === CanvasNodeType.Drawing) {
|
||||
metadata.drawingId = `${idMap.get(node.id)}-document`;
|
||||
metadata.drawingRevision = 0;
|
||||
@@ -265,20 +286,6 @@ export function useCanvasNodeOperations({
|
||||
metadata.drawingPageCount = 1;
|
||||
}
|
||||
if (node.id === source.id && versionRootId) {
|
||||
delete metadata.taskId;
|
||||
delete metadata.taskStatus;
|
||||
delete metadata.taskProgress;
|
||||
delete metadata.taskStage;
|
||||
delete metadata.taskCreatedAt;
|
||||
delete metadata.taskUpdatedAt;
|
||||
delete metadata.errorDetails;
|
||||
delete metadata.batchRootId;
|
||||
delete metadata.batchChildIds;
|
||||
delete metadata.isBatchRoot;
|
||||
delete metadata.primaryImageId;
|
||||
delete metadata.imageBatchExpanded;
|
||||
delete metadata.batchUsesReferenceImages;
|
||||
metadata.status = metadata.content ? NODE_STATUS_SUCCESS : NODE_STATUS_IDLE;
|
||||
metadata.versionOfNodeId = versionRootId;
|
||||
metadata.versionLabel = versionLabel;
|
||||
metadata.versionPrimary = false;
|
||||
@@ -338,17 +345,21 @@ export function useCanvasNodeOperations({
|
||||
connections: connectionsRef.current.filter((connection) => copyIds.has(connection.fromNodeId) && copyIds.has(connection.toNodeId)).map((connection) => ({ ...connection })),
|
||||
};
|
||||
setHasCopiedNodes(true);
|
||||
// 写入系统剪贴板标记,覆盖旧图片,避免 Ctrl+V 仍优先粘贴系统图片。
|
||||
// 写入完成前或写入失败时优先内部节点,避免快速粘贴读到系统残留图片。
|
||||
const marker = `open-ai-canvas-nodes:${Date.now()}:${copiedNodes.length}`;
|
||||
try {
|
||||
if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
|
||||
void navigator.clipboard.write([new ClipboardItem({ "text/plain": new Blob([marker], { type: "text/plain" }) })]);
|
||||
} else if (navigator.clipboard?.writeText) {
|
||||
void navigator.clipboard.writeText(marker);
|
||||
}
|
||||
} catch {
|
||||
// 忽略剪贴板权限失败,内部剪贴板仍可用。
|
||||
}
|
||||
const sequence = markerWriteSequenceRef.current + 1;
|
||||
markerWriteSequenceRef.current = sequence;
|
||||
markerWritePendingRef.current = true;
|
||||
preferCopiedNodesRef.current = true;
|
||||
void copyToClipboard(marker, { format: "text/plain" }).then((written) => {
|
||||
if (markerWriteSequenceRef.current !== sequence) return;
|
||||
markerWritePendingRef.current = false;
|
||||
preferCopiedNodesRef.current = !written;
|
||||
}, () => {
|
||||
if (markerWriteSequenceRef.current !== sequence) return;
|
||||
markerWritePendingRef.current = false;
|
||||
preferCopiedNodesRef.current = true;
|
||||
});
|
||||
}, [connectionsRef, nodesRef]);
|
||||
|
||||
const copySelectedNodes = useCallback(() => {
|
||||
@@ -370,29 +381,7 @@ export function useCanvasNodeOperations({
|
||||
const idMap = new Map(clipboard.nodes.map((node, index) => [node.id, `${node.type}-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 7)}`]));
|
||||
const copiedSourceIds = new Set(clipboard.nodes.map((node) => node.id));
|
||||
const nextNodes = clipboard.nodes.map((node) => {
|
||||
const metadata = node.metadata ? { ...node.metadata, frame: node.metadata.frame ? { ...node.metadata.frame } : undefined } : undefined;
|
||||
if (metadata) {
|
||||
// 粘贴必须切断与源批次/任务的绑定,否则拖拽会通过 batchChildIds 带动旧结果。
|
||||
delete metadata.taskId;
|
||||
delete metadata.taskStatus;
|
||||
delete metadata.taskProgress;
|
||||
delete metadata.taskStage;
|
||||
delete metadata.taskCreatedAt;
|
||||
delete metadata.taskUpdatedAt;
|
||||
delete metadata.errorDetails;
|
||||
delete metadata.generationErrorCode;
|
||||
delete metadata.failedPromptFingerprint;
|
||||
delete metadata.batchRootId;
|
||||
delete metadata.batchChildIds;
|
||||
delete metadata.isBatchRoot;
|
||||
delete metadata.primaryImageId;
|
||||
delete metadata.imageBatchExpanded;
|
||||
delete metadata.batchUsesReferenceImages;
|
||||
delete metadata.versionOfNodeId;
|
||||
delete metadata.versionLabel;
|
||||
delete metadata.versionPrimary;
|
||||
metadata.status = metadata.content ? NODE_STATUS_SUCCESS : NODE_STATUS_IDLE;
|
||||
}
|
||||
const metadata = isolateCopiedNodeMetadata(node, idMap);
|
||||
if (node.type === CanvasNodeType.Drawing && metadata) {
|
||||
metadata.drawingId = `${idMap.get(node.id)}-document`;
|
||||
metadata.drawingRevision = 0;
|
||||
@@ -451,7 +440,9 @@ export function useCanvasNodeOperations({
|
||||
duplicateNode,
|
||||
hasCopiedNodes,
|
||||
pasteCopiedNodes,
|
||||
releaseCopiedNodesPastePriority,
|
||||
setPrimaryVersion,
|
||||
shouldPreferCopiedNodes,
|
||||
toggleNodeLocked,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user