feat: add four-direction quick-add controls to selected nodes

This commit is contained in:
2026-11-06 01:47:27 +08:00
parent fc28632aba
commit e28ddb673f
7 changed files with 121 additions and 11 deletions
+36 -1
View File
@@ -7,6 +7,8 @@ export type CanvasNode = Node<{
text: string;
src?: string;
}, "material">;
export type NodeDirection = "top" | "right" | "bottom" | "left";
export const nodeDirections: NodeDirection[] = ["top", "right", "bottom", "left"];
export type CanvasDocument = { schema_version: 1; nodes: CanvasNode[]; edges: Edge[]; viewport: Viewport };
export type CanvasHistory = { past: CanvasDocument[]; present: CanvasDocument; future: CanvasDocument[] };
export type CanvasAction =
@@ -66,6 +68,34 @@ export function makeCanvasNode(kind: CanvasNode["data"]["kind"], position: XYPos
data: { kind, title: kind === "image" ? "视觉参考" : kind === "script" ? "脚本" : "灵感笔记", text: "", ...(src ? { src } : {}) },
};
}
export function branchCanvasNode(document: CanvasDocument, id: string, direction: NodeDirection): CanvasDocument {
const source = document.nodes.find((node) => node.id === id);
if (!source) throw new Error("节点不存在。");
if (document.nodes.length >= 500) throw new Error("当前图谱已达到 500 个节点。");
const width = source.measured?.width || 260;
const height = source.measured?.height || 200;
const gap = 80;
const position = {
x: direction === "right" ? source.position.x + width + gap : direction === "left" ? source.position.x - 260 - gap : source.position.x + (width - 260) / 2,
y: direction === "bottom" ? source.position.y + height + gap : direction === "top" ? source.position.y - 200 - gap : source.position.y + (height - 200) / 2,
};
// Keep repeated additions on the same side from covering one another.
for (let attempts = 0; attempts < 500; attempts++) {
const overlap = document.nodes.some((node) => position.x < node.position.x + (node.measured?.width || 260) + 12 &&
position.x + 260 + 12 > node.position.x && position.y < node.position.y + (node.measured?.height || 200) + 12 && position.y + 200 + 12 > node.position.y);
if (!overlap) break;
if (direction === "left" || direction === "right") position.y += 240;
else position.x += 300;
}
const child = { ...makeCanvasNode("text", position), selected: true };
const opposite: Record<NodeDirection, NodeDirection> = { top: "bottom", bottom: "top", left: "right", right: "left" };
return {
...document,
nodes: [...document.nodes.map((node) => ({ ...node, selected: false })), child],
edges: [...document.edges, { id: crypto.randomUUID(), source: id, target: child.id, sourceHandle: direction, targetHandle: opposite[direction] }],
};
}
function validImage(src: unknown): src is string {
if (typeof src !== "string") return false;
if (/^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/=]+$/.test(src)) return src.length < 3_000_000;
@@ -93,8 +123,13 @@ export function decodeCanvas(raw: string): CanvasDocument {
const edges: Edge[] = value.edges.map((edge: Edge) => {
if (!edge || typeof edge.id !== "string" || !/^[\w-]{1,128}$/.test(edge.id) || edgeIds.has(edge.id) || !ids.has(edge.source) || !ids.has(edge.target))
throw new Error("图谱连线格式无效。");
if ([edge.sourceHandle, edge.targetHandle].some((handle) => handle != null && !nodeDirections.includes(handle as NodeDirection)))
throw new Error("图谱连接方向无效。");
edgeIds.add(edge.id);
return { id: edge.id, source: edge.source, target: edge.target };
return { id: edge.id, source: edge.source, target: edge.target,
...(edge.sourceHandle ? { sourceHandle: edge.sourceHandle } : {}),
...(edge.targetHandle ? { targetHandle: edge.targetHandle } : {}),
};
});
const viewport = value.viewport;
if (!viewport || ![viewport.x, viewport.y, viewport.zoom].every(Number.isFinite) || viewport.zoom < 0.1 || viewport.zoom > 4)