mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 17:04:27 +08:00
Merge branch 'fork/stupid-h4er/feat/ark-agentplan-seedance-video'
# Conflicts: # docs/content/docs/progress/pending-test.mdx
This commit is contained in:
@@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项
|
|||||||
|
|
||||||
# 待测试
|
# 待测试
|
||||||
|
|
||||||
|
- 画布连线支持右键打开删除菜单;拖拽连线到目标卡片内部、连接点附近或卡片边缘外扩范围内会自动吸附并连接,拖到已有但不可连接的节点附近不会再误弹创建节点菜单。
|
||||||
- 外部软件可通过 URL 查询参数 `baseUrl`/`baseurl` 和 `apiKey`/`apikey` 跳转到前端;读取后会从地址栏移除这些参数,后台允许自定义渠道时会自动切到自定义渠道、填入配置并打开配置弹窗,未允许时会打开配置弹窗并提示无法导入。
|
- 外部软件可通过 URL 查询参数 `baseUrl`/`baseurl` 和 `apiKey`/`apikey` 跳转到前端;读取后会从地址栏移除这些参数,后台允许自定义渠道时会自动切到自定义渠道、填入配置并打开配置弹窗,未允许时会打开配置弹窗并提示无法导入。
|
||||||
- 生图工作台和画布生图会把参考图按当前顺序显示为 `图片1`、`图片2` 等编号,并在图生图请求的实际提示词中注入编号说明;需要验证 `/image` 参考图排序、画布配置节点输入顺序和画布助手参考图编号一致。
|
- 生图工作台和画布生图会把参考图按当前顺序显示为 `图片1`、`图片2` 等编号,并在图生图请求的实际提示词中注入编号说明;需要验证 `/image` 参考图排序、画布配置节点输入顺序和画布助手参考图编号一致。
|
||||||
- GPT Image 生图请求会在前端把 `9:16`、`16:9` 等比例转换成合法 `WIDTHxHEIGHT` 尺寸,并在非法尺寸时直接显示中文错误,避免上游返回 `invalid_value Invalid size`。
|
- GPT Image 生图请求会在前端把 `9:16`、`16:9` 等比例转换成合法 `WIDTHxHEIGHT` 尺寸,并在非法尺寸时直接显示中文错误,避免上游返回 `invalid_value Invalid size`。
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ type PendingConnectionCreate = {
|
|||||||
position: Position;
|
position: Position;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ConnectionDropTarget = {
|
||||||
|
nodeId: string | null;
|
||||||
|
isNearNode: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type CanvasHistoryEntry = Pick<CanvasClipboard, "nodes" | "connections"> & {
|
type CanvasHistoryEntry = Pick<CanvasClipboard, "nodes" | "connections"> & {
|
||||||
chatSessions: CanvasAssistantSession[];
|
chatSessions: CanvasAssistantSession[];
|
||||||
activeChatId: string | null;
|
activeChatId: string | null;
|
||||||
@@ -73,6 +78,8 @@ type CanvasHistoryEntry = Pick<CanvasClipboard, "nodes" | "connections"> & {
|
|||||||
|
|
||||||
const VIDEO_NODE_MAX_WIDTH = 420;
|
const VIDEO_NODE_MAX_WIDTH = 420;
|
||||||
const VIDEO_NODE_MAX_HEIGHT = 420;
|
const VIDEO_NODE_MAX_HEIGHT = 420;
|
||||||
|
const CONNECTION_HANDLE_HIT_RADIUS = 40;
|
||||||
|
const CONNECTION_NODE_HIT_PADDING = 32;
|
||||||
const NODE_STATUS_LOADING = "loading" as const;
|
const NODE_STATUS_LOADING = "loading" as const;
|
||||||
const NODE_STATUS_SUCCESS = "success" as const;
|
const NODE_STATUS_SUCCESS = "success" as const;
|
||||||
const NODE_STATUS_ERROR = "error" as const;
|
const NODE_STATUS_ERROR = "error" as const;
|
||||||
@@ -509,23 +516,35 @@ function InfiniteCanvasPage() {
|
|||||||
setConnecting(null);
|
setConnecting(null);
|
||||||
}, [setConnecting]);
|
}, [setConnecting]);
|
||||||
|
|
||||||
const getConnectableNodeAtPoint = useCallback(
|
const getConnectionDropTarget = useCallback(
|
||||||
(clientX: number, clientY: number, current: ConnectionHandle) => {
|
(clientX: number, clientY: number, current: ConnectionHandle): ConnectionDropTarget => {
|
||||||
const world = screenToCanvas(clientX, clientY);
|
const world = screenToCanvas(clientX, clientY);
|
||||||
return (
|
const scale = Math.max(viewportRef.current.k, 0.05);
|
||||||
|
const padding = CONNECTION_NODE_HIT_PADDING / scale;
|
||||||
|
const handleRadius = CONNECTION_HANDLE_HIT_RADIUS / scale;
|
||||||
|
let isNearNode = false;
|
||||||
|
let best: { nodeId: string; priority: number } | null = null;
|
||||||
|
|
||||||
[...nodesRef.current]
|
[...nodesRef.current]
|
||||||
.filter((node) => !isHiddenBatchChild(node, nodesRef.current))
|
.filter((node) => !isHiddenBatchChild(node, nodesRef.current))
|
||||||
.reverse()
|
.reverse()
|
||||||
.find(
|
.forEach((node) => {
|
||||||
(node) =>
|
const anchor = getConnectionTargetAnchor(node, current);
|
||||||
node.id !== current.nodeId &&
|
const dx = world.x - anchor.x;
|
||||||
Boolean(normalizeConnection(current.nodeId, node.id, nodesRef.current, current.handleType)) &&
|
const dy = world.y - anchor.y;
|
||||||
world.x >= node.position.x &&
|
const hitsHandle = dx * dx + dy * dy <= handleRadius * handleRadius;
|
||||||
world.x <= node.position.x + node.width &&
|
const hitsInside = world.x >= node.position.x && world.x <= node.position.x + node.width && world.y >= node.position.y && world.y <= node.position.y + node.height;
|
||||||
world.y >= node.position.y &&
|
const hitsExpanded = world.x >= node.position.x - padding && world.x <= node.position.x + node.width + padding && world.y >= node.position.y - padding && world.y <= node.position.y + node.height + padding;
|
||||||
world.y <= node.position.y + node.height,
|
|
||||||
)?.id || null
|
if (!hitsHandle && !hitsInside && !hitsExpanded) return;
|
||||||
);
|
isNearNode = true;
|
||||||
|
if (node.id === current.nodeId || !normalizeConnection(current.nodeId, node.id, nodesRef.current, current.handleType)) return;
|
||||||
|
|
||||||
|
const priority = hitsInside ? 0 : hitsHandle ? 1 : 2;
|
||||||
|
if (!best || priority < best.priority) best = { nodeId: node.id, priority };
|
||||||
|
});
|
||||||
|
|
||||||
|
return { nodeId: best?.nodeId || null, isNearNode };
|
||||||
},
|
},
|
||||||
[screenToCanvas],
|
[screenToCanvas],
|
||||||
);
|
);
|
||||||
@@ -657,12 +676,18 @@ function InfiniteCanvasPage() {
|
|||||||
setAngleNodeId((current) => (current && allIds.has(current) ? null : current));
|
setAngleNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||||
setPreviewNodeId((current) => (current && allIds.has(current) ? null : current));
|
setPreviewNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||||
setRunningNodeId((current) => (current && allIds.has(current) ? null : current));
|
setRunningNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||||
setContextMenu((current) => (current && allIds.has(current.nodeId) ? null : current));
|
setContextMenu((current) => (current?.type === "node" && allIds.has(current.nodeId) ? null : current));
|
||||||
cleanupCanvasFiles({ projectId, nodes: nodesRef.current.filter((node) => !allIds.has(node.id)), chatSessions });
|
cleanupCanvasFiles({ projectId, nodes: nodesRef.current.filter((node) => !allIds.has(node.id)), chatSessions });
|
||||||
},
|
},
|
||||||
[chatSessions, cleanupCanvasFiles, projectId],
|
[chatSessions, cleanupCanvasFiles, projectId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const deleteConnection = useCallback((connectionId: string) => {
|
||||||
|
setConnections((prev) => prev.filter((conn) => conn.id !== connectionId));
|
||||||
|
setSelectedConnectionId((current) => (current === connectionId ? null : current));
|
||||||
|
setContextMenu((current) => (current?.type === "connection" && current.connectionId === connectionId ? null : current));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const deselectCanvas = useCallback(() => {
|
const deselectCanvas = useCallback(() => {
|
||||||
cancelPendingConnectionCreate();
|
cancelPendingConnectionCreate();
|
||||||
setSelectedNodeIds(new Set());
|
setSelectedNodeIds(new Set());
|
||||||
@@ -986,13 +1011,13 @@ function InfiniteCanvasPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (connectingParamsRef.current && !pendingConnectionCreateRef.current) {
|
if (connectingParamsRef.current && !pendingConnectionCreateRef.current) {
|
||||||
const targetNodeId = getConnectableNodeAtPoint(event.clientX, event.clientY, connectingParamsRef.current);
|
const dropTarget = getConnectionDropTarget(event.clientX, event.clientY, connectingParamsRef.current);
|
||||||
connectionTargetNodeIdRef.current = targetNodeId;
|
connectionTargetNodeIdRef.current = dropTarget.nodeId;
|
||||||
setConnectionTargetNodeId(targetNodeId);
|
setConnectionTargetNodeId(dropTarget.nodeId);
|
||||||
setMouseWorld(screenToCanvas(event.clientX, event.clientY));
|
setMouseWorld(screenToCanvas(event.clientX, event.clientY));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[finishNodeDrag, getConnectableNodeAtPoint, screenToCanvas],
|
[finishNodeDrag, getConnectionDropTarget, screenToCanvas],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGlobalPointerMove = useCallback(
|
const handleGlobalPointerMove = useCallback(
|
||||||
@@ -1040,9 +1065,11 @@ function InfiniteCanvasPage() {
|
|||||||
|
|
||||||
const currentConnection = connectingParamsRef.current;
|
const currentConnection = connectingParamsRef.current;
|
||||||
if (currentConnection) {
|
if (currentConnection) {
|
||||||
const targetNodeId = getConnectableNodeAtPoint(event.clientX, event.clientY, currentConnection) || connectionTargetNodeIdRef.current;
|
const dropTarget = getConnectionDropTarget(event.clientX, event.clientY, currentConnection);
|
||||||
if (targetNodeId) {
|
if (dropTarget.nodeId) {
|
||||||
connectNodes(currentConnection, targetNodeId);
|
connectNodes(currentConnection, dropTarget.nodeId);
|
||||||
|
setConnecting(null);
|
||||||
|
} else if (dropTarget.isNearNode) {
|
||||||
setConnecting(null);
|
setConnecting(null);
|
||||||
} else {
|
} else {
|
||||||
setMouseWorld(screenToCanvas(event.clientX, event.clientY));
|
setMouseWorld(screenToCanvas(event.clientX, event.clientY));
|
||||||
@@ -1050,7 +1077,7 @@ function InfiniteCanvasPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[connectNodes, finishNodeDrag, getConnectableNodeAtPoint, screenToCanvas, setConnecting],
|
[connectNodes, finishNodeDrag, getConnectionDropTarget, screenToCanvas, setConnecting],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1217,8 +1244,7 @@ function InfiniteCanvasPage() {
|
|||||||
if (selectedNodeIdsRef.current.size) {
|
if (selectedNodeIdsRef.current.size) {
|
||||||
deleteNodes(new Set(selectedNodeIdsRef.current));
|
deleteNodes(new Set(selectedNodeIdsRef.current));
|
||||||
} else if (selectedConnectionId) {
|
} else if (selectedConnectionId) {
|
||||||
setConnections((prev) => prev.filter((conn) => conn.id !== selectedConnectionId));
|
deleteConnection(selectedConnectionId);
|
||||||
setSelectedConnectionId(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1240,7 +1266,7 @@ function InfiniteCanvasPage() {
|
|||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [copySelectedNodes, deleteNodes, pasteCopiedNodes, pasteSystemClipboard, redoCanvas, selectedConnectionId, setConnecting, undoCanvas]);
|
}, [copySelectedNodes, deleteConnection, deleteNodes, pasteCopiedNodes, pasteSystemClipboard, redoCanvas, selectedConnectionId, setConnecting, undoCanvas]);
|
||||||
|
|
||||||
const handleConnectStart = useCallback(
|
const handleConnectStart = useCallback(
|
||||||
(event: ReactMouseEvent, nodeId: string, handleType: "source" | "target") => {
|
(event: ReactMouseEvent, nodeId: string, handleType: "source" | "target") => {
|
||||||
@@ -2105,10 +2131,15 @@ function InfiniteCanvasPage() {
|
|||||||
setSelectedNodeIds(new Set());
|
setSelectedNodeIds(new Set());
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
|
onContextMenu={(event) => {
|
||||||
|
setSelectedConnectionId(connection.id);
|
||||||
|
setSelectedNodeIds(new Set());
|
||||||
|
setContextMenu({ type: "connection", x: event.clientX, y: event.clientY, connectionId: connection.id });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{connectingParams ? <ActiveConnectionPath node={nodeById.get(connectingParams.nodeId)} handle={connectingParams} mouseWorld={mouseWorld} /> : null}
|
{connectingParams ? <ActiveConnectionPath node={nodeById.get(connectingParams.nodeId)} handle={connectingParams} mouseWorld={mouseWorld} target={connectionTargetNodeId ? nodeById.get(connectionTargetNodeId) : undefined} /> : null}
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{visibleNodes.map((node) => (
|
{visibleNodes.map((node) => (
|
||||||
@@ -2258,11 +2289,16 @@ function InfiniteCanvasPage() {
|
|||||||
menu={contextMenu}
|
menu={contextMenu}
|
||||||
onClose={() => setContextMenu(null)}
|
onClose={() => setContextMenu(null)}
|
||||||
onDuplicate={() => {
|
onDuplicate={() => {
|
||||||
|
if (contextMenu.type !== "node") return;
|
||||||
duplicateNode(contextMenu.nodeId);
|
duplicateNode(contextMenu.nodeId);
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
|
if (contextMenu.type === "node") {
|
||||||
deleteNodes(new Set([contextMenu.nodeId]));
|
deleteNodes(new Set([contextMenu.nodeId]));
|
||||||
|
} else {
|
||||||
|
deleteConnection(contextMenu.connectionId);
|
||||||
|
}
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -2631,6 +2667,13 @@ function applyNodeConfigPatch(node: CanvasNodeData, patch: Partial<CanvasNodeDat
|
|||||||
return size && (node.type === CanvasNodeType.Image || node.type === CanvasNodeType.Video) ? { ...next, ...size, position: { x: node.position.x + node.width / 2 - size.width / 2, y: node.position.y + node.height / 2 - size.height / 2 } } : next;
|
return size && (node.type === CanvasNodeType.Image || node.type === CanvasNodeType.Video) ? { ...next, ...size, position: { x: node.position.x + node.width / 2 - size.width / 2, y: node.position.y + node.height / 2 - size.height / 2 } } : next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getConnectionTargetAnchor(node: CanvasNodeData, current: ConnectionHandle) {
|
||||||
|
return {
|
||||||
|
x: current.handleType === "source" ? node.position.x : node.position.x + node.width,
|
||||||
|
y: node.position.y + node.height / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeConnection(firstNodeId: string, secondNodeId: string, nodes: CanvasNodeData[], firstHandleType: "source" | "target") {
|
function normalizeConnection(firstNodeId: string, secondNodeId: string, nodes: CanvasNodeData[], firstHandleType: "source" | "target") {
|
||||||
const first = nodes.find((node) => node.id === firstNodeId);
|
const first = nodes.find((node) => node.id === firstNodeId);
|
||||||
const second = nodes.find((node) => node.id === secondNodeId);
|
const second = nodes.find((node) => node.id === secondNodeId);
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
|
import type { MouseEvent as ReactMouseEvent } from "react";
|
||||||
|
|
||||||
import { canvasThemes } from "@/lib/canvas-theme";
|
import { canvasThemes } from "@/lib/canvas-theme";
|
||||||
import { useThemeStore } from "@/stores/use-theme-store";
|
import { useThemeStore } from "@/stores/use-theme-store";
|
||||||
import type { CanvasConnection, CanvasNodeData, ConnectionHandle, Position } from "../types";
|
import type { CanvasConnection, CanvasNodeData, ConnectionHandle, Position } from "../types";
|
||||||
|
|
||||||
export function ConnectionPath({ connection, from, to, active, onSelect }: { connection: CanvasConnection; from: CanvasNodeData; to: CanvasNodeData; active: boolean; onSelect: () => void }) {
|
export function ConnectionPath({
|
||||||
|
connection,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
active,
|
||||||
|
onSelect,
|
||||||
|
onContextMenu,
|
||||||
|
}: {
|
||||||
|
connection: CanvasConnection;
|
||||||
|
from: CanvasNodeData;
|
||||||
|
to: CanvasNodeData;
|
||||||
|
active: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
onContextMenu?: (event: ReactMouseEvent<SVGPathElement>) => void;
|
||||||
|
}) {
|
||||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||||
const startX = from.position.x + from.width;
|
const startX = from.position.x + from.width;
|
||||||
const startY = from.position.y + from.height / 2;
|
const startY = from.position.y + from.height / 2;
|
||||||
@@ -25,6 +41,11 @@ export function ConnectionPath({ connection, from, to, active, onSelect }: { con
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onSelect();
|
onSelect();
|
||||||
}}
|
}}
|
||||||
|
onContextMenu={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onContextMenu?.(event);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={pathD}
|
d={pathD}
|
||||||
@@ -38,7 +59,7 @@ export function ConnectionPath({ connection, from, to, active, onSelect }: { con
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActiveConnectionPath({ node, handle, mouseWorld }: { node?: CanvasNodeData; handle: ConnectionHandle; mouseWorld: Position }) {
|
export function ActiveConnectionPath({ node, handle, mouseWorld, target }: { node?: CanvasNodeData; handle: ConnectionHandle; mouseWorld: Position; target?: CanvasNodeData }) {
|
||||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
|
|
||||||
@@ -46,8 +67,12 @@ export function ActiveConnectionPath({ node, handle, mouseWorld }: { node?: Canv
|
|||||||
const startY = handle.handleType === "source" ? node.position.y + node.height / 2 : mouseWorld.y;
|
const startY = handle.handleType === "source" ? node.position.y + node.height / 2 : mouseWorld.y;
|
||||||
const endX = handle.handleType === "source" ? mouseWorld.x : node.position.x;
|
const endX = handle.handleType === "source" ? mouseWorld.x : node.position.x;
|
||||||
const endY = handle.handleType === "source" ? mouseWorld.y : node.position.y + node.height / 2;
|
const endY = handle.handleType === "source" ? mouseWorld.y : node.position.y + node.height / 2;
|
||||||
const distance = Math.abs(endX - startX);
|
const snappedStartX = handle.handleType === "target" && target ? target.position.x + target.width : startX;
|
||||||
const pathD = `M ${startX} ${startY} C ${startX + distance * 0.5} ${startY}, ${endX - distance * 0.5} ${endY}, ${endX} ${endY}`;
|
const snappedStartY = handle.handleType === "target" && target ? target.position.y + target.height / 2 : startY;
|
||||||
|
const snappedEndX = handle.handleType === "source" && target ? target.position.x : endX;
|
||||||
|
const snappedEndY = handle.handleType === "source" && target ? target.position.y + target.height / 2 : endY;
|
||||||
|
const distance = Math.abs(snappedEndX - snappedStartX);
|
||||||
|
const pathD = `M ${snappedStartX} ${snappedStartY} C ${snappedStartX + distance * 0.5} ${snappedStartY}, ${snappedEndX - distance * 0.5} ${snappedEndY}, ${snappedEndX} ${snappedEndY}`;
|
||||||
|
|
||||||
return <path d={pathD} stroke={theme.node.activeStroke} strokeWidth="2" fill="none" strokeDasharray="5,5" />;
|
return <path d={pathD} stroke={theme.node.activeStroke} strokeWidth="2" fill="none" strokeDasharray="5,5" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function CanvasNodeContextMenu({ menu, onClose, onDuplicate, onDelete }:
|
|||||||
style={{ left: menu.x, top: menu.y, background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}
|
style={{ left: menu.x, top: menu.y, background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}
|
||||||
onPointerDown={(event) => event.stopPropagation()}
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MenuButton icon={<Plus className="size-4" />} label="Duplicate" onClick={onDuplicate} />
|
{menu.type === "node" ? <MenuButton icon={<Plus className="size-4" />} label="Duplicate" onClick={onDuplicate} /> : null}
|
||||||
<MenuButton icon={<Trash2 className="size-4" />} label="Delete" onClick={onDelete} danger />
|
<MenuButton icon={<Trash2 className="size-4" />} label="Delete" onClick={onDelete} danger />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -118,9 +118,16 @@ export type SelectionBox = {
|
|||||||
initialSelectedNodeIds: string[];
|
initialSelectedNodeIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextMenuState = {
|
export type ContextMenuState =
|
||||||
|
| {
|
||||||
type: "node";
|
type: "node";
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "connection";
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
connectionId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user