184 lines
7.8 KiB
TypeScript
184 lines
7.8 KiB
TypeScript
import { ChartColumn, Clapperboard, Code, Columns2, FileText, Globe, Image as ImageIcon, Music2, PanelTop, Palette, Pencil, Settings2, Shapes, Sparkles, Table2, Type, Video, WandSparkles } from "lucide-react";
|
||
|
||
import { NODE_SPECS } from "@/constant/canvas";
|
||
import { MEDIA_NODE_MIN_SIZE } from "@/lib/canvas/canvas-node-size";
|
||
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||
|
||
import type { CanvasNodeDefinition } from "../node-definition";
|
||
import { registerNodeDefinitions } from "../node-registry";
|
||
|
||
/** 非媒体节点的拉伸下限 */
|
||
const DEFAULT_MIN_SIZE = { width: 220, height: 160 } as const;
|
||
|
||
/**
|
||
* 每种内置节点的自有特征。
|
||
*
|
||
* 尺寸、默认标题与初始 metadata 不在这里重复——它们的数据源是 constant/canvas.ts 的
|
||
* NODE_SPECS,下方装配时统一读取。
|
||
*
|
||
* 内置节点保持集中定义;第三方节点通过同一 registry 以命名空间 ID 动态注册。
|
||
*/
|
||
const BUILTIN_NODE_TRAITS = {
|
||
[CanvasNodeType.Image]: {
|
||
label: "图片",
|
||
icon: <ImageIcon />,
|
||
minSize: MEDIA_NODE_MIN_SIZE,
|
||
keepAspectRatio: (node: CanvasNodeData) => !node.metadata?.freeResize,
|
||
showInCreateMenu: true,
|
||
resourceKind: (node: CanvasNodeData) => (node.metadata?.content || node.metadata?.storageKey ? "image" : null),
|
||
generationMode: () => "image",
|
||
inputKind: "image",
|
||
},
|
||
[CanvasNodeType.Text]: {
|
||
label: "文本",
|
||
icon: <Type />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
resourceKind: (node: CanvasNodeData) => (node.metadata?.content || node.metadata?.prompt ? "text" : null),
|
||
generationMode: () => "text",
|
||
inputKind: "text",
|
||
},
|
||
[CanvasNodeType.Drawing]: {
|
||
label: "绘图",
|
||
icon: <Pencil />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
// 绘图产出的是图像,所以作为素材与输入都按图片计。
|
||
resourceKind: (node: CanvasNodeData) => (node.metadata?.drawingId ? "image" : null),
|
||
inputKind: "image",
|
||
},
|
||
[CanvasNodeType.Script]: {
|
||
label: "分镜脚本",
|
||
icon: <Clapperboard />,
|
||
// 分镜脚本的表格布局需要更宽的下限;高度仍由内容动态撑开(见 canvas-node.tsx)。
|
||
minSize: { width: 800, height: DEFAULT_MIN_SIZE.height },
|
||
showInCreateMenu: true,
|
||
generationMode: () => "text",
|
||
inputKind: "text",
|
||
},
|
||
[CanvasNodeType.Skill]: {
|
||
label: "技能",
|
||
icon: <Sparkles />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
// 技能节点由技能库插入,不占创建菜单格位。
|
||
showInCreateMenu: false,
|
||
// 技能注入的是提示词文本,故按文本素材计。
|
||
resourceKind: (node: CanvasNodeData) => (node.metadata?.skillSnapshot || node.metadata?.content ? "text" : null),
|
||
inputKind: "text",
|
||
},
|
||
[CanvasNodeType.Config]: {
|
||
label: "生成配置",
|
||
icon: <Settings2 />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
// 生成配置由连线时自动创建。
|
||
showInCreateMenu: false,
|
||
// 生成模式由用户在配置节点上选择,缺省按图片。
|
||
generationMode: (node: CanvasNodeData) => node.metadata?.generationMode || "image",
|
||
// 不设 inputKind:配置节点本身不是参考素材,不参与容量计数。
|
||
},
|
||
[CanvasNodeType.Video]: {
|
||
label: "视频",
|
||
icon: <Video />,
|
||
minSize: MEDIA_NODE_MIN_SIZE,
|
||
keepAspectRatio: () => true,
|
||
showInCreateMenu: true,
|
||
resourceKind: (node: CanvasNodeData) => (node.metadata?.content || node.metadata?.storageKey ? "video" : null),
|
||
generationMode: () => "video",
|
||
inputKind: "video",
|
||
},
|
||
[CanvasNodeType.Audio]: {
|
||
label: "音频",
|
||
icon: <Music2 />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
resourceKind: (node: CanvasNodeData) => (node.metadata?.content ? "audio" : null),
|
||
generationMode: () => "audio",
|
||
inputKind: "audio",
|
||
},
|
||
[CanvasNodeType.Frame]: {
|
||
label: "背板",
|
||
// 背板在列表里就叫「背板」,不是「背板节点」——显式钉住,避免被 label 派生改写。
|
||
listLabel: "背板",
|
||
icon: <PanelTop />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
// 背板只是视觉容器,既不是素材也不参与计数。
|
||
},
|
||
[CanvasNodeType.Markdown]: {
|
||
label: "Markdown",
|
||
icon: <FileText />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
// 渲染的是 Markdown 源码,作为素材与输入都按文本计。
|
||
resourceKind: (node: CanvasNodeData) => (node.metadata?.content ? "text" : null),
|
||
inputKind: "text",
|
||
// 不设 generationMode:它是展示节点,自身不发起生成。
|
||
},
|
||
[CanvasNodeType.Svg]: {
|
||
label: "SVG",
|
||
icon: <Shapes />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
// 内容是 SVG 源码,按文本素材计;渲染成图但不产出图片资源。
|
||
resourceKind: (node: CanvasNodeData) => (node.metadata?.content ? "text" : null),
|
||
inputKind: "text",
|
||
},
|
||
[CanvasNodeType.Html]: {
|
||
label: "HTML",
|
||
icon: <Code />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
resourceKind: (node: CanvasNodeData) => (node.metadata?.content ? "text" : null),
|
||
inputKind: "text",
|
||
},
|
||
[CanvasNodeType.Panorama]: {
|
||
label: "全景",
|
||
icon: <Globe />,
|
||
minSize: MEDIA_NODE_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
// 查看器:消费上游图片,自身不作为素材被引用,故不设 resourceKind。
|
||
inputKind: "image",
|
||
},
|
||
[CanvasNodeType.Compare]: {
|
||
label: "对比",
|
||
icon: <Columns2 />,
|
||
minSize: MEDIA_NODE_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
// 只看不产出:消费两张上游图片,自身不作为素材。
|
||
inputKind: "image",
|
||
},
|
||
[CanvasNodeType.Chart]: {
|
||
label: "图表",
|
||
icon: <ChartColumn />,
|
||
minSize: DEFAULT_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
inputKind: "text",
|
||
},
|
||
[CanvasNodeType.BatchTable]: { label: "批量创作表", icon: <Table2 />, minSize: { width: 720, height: 360 }, showInCreateMenu: true, acceptsInputKind: "image", inputKind: "text", resourceKind: () => null },
|
||
[CanvasNodeType.MediaConversion]: { label: "转换", icon: <WandSparkles />, minSize: { width: 400, height: 360 }, showInCreateMenu: true, resourceKind: (node: CanvasNodeData) => { const c = node.metadata?.mediaConversion; return c?.status === "completed" && c.resultStorageKey ? (c.outputKind === "video" ? "video" : "image") : null; }, acceptsInputKind: ["image", "video"], maxInputCount: 1, inputKind: "image" },
|
||
[CanvasNodeType.ColorGrade]: {
|
||
label: "调色",
|
||
icon: <Palette />,
|
||
minSize: MEDIA_NODE_MIN_SIZE,
|
||
showInCreateMenu: true,
|
||
// 无条件返回 image:它的图来自上游而不是自身 metadata,用「有没有 content」判断
|
||
// 会让它永远不被当成素材、从而进不了生成输入。没有上游时由
|
||
// readReferenceImage 返回 null 跳过,不需要在这里提前判空。
|
||
resourceKind: () => "image",
|
||
inputKind: "image",
|
||
},
|
||
} satisfies Record<string, Omit<CanvasNodeDefinition, "type" | "defaultTitle" | "defaultSize" | "defaultMetadata">>;
|
||
|
||
export const BUILTIN_NODE_DEFINITIONS: CanvasNodeDefinition[] = (Object.keys(BUILTIN_NODE_TRAITS) as CanvasNodeType[]).map((type) => {
|
||
const spec = NODE_SPECS[type];
|
||
return {
|
||
type,
|
||
...BUILTIN_NODE_TRAITS[type],
|
||
defaultTitle: spec.title,
|
||
defaultSize: { width: spec.width, height: spec.height },
|
||
defaultMetadata: spec.metadata,
|
||
};
|
||
});
|
||
|
||
registerNodeDefinitions(BUILTIN_NODE_DEFINITIONS);
|