feat: add four-direction quick-add controls to selected nodes
This commit is contained in:
+36
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user