diff --git a/web/src/components/canvas/canvas-node-search-modal.tsx b/web/src/components/canvas/canvas-node-search-modal.tsx index a45c1913e..adc981884 100644 --- a/web/src/components/canvas/canvas-node-search-modal.tsx +++ b/web/src/components/canvas/canvas-node-search-modal.tsx @@ -3,6 +3,7 @@ import { Input, Modal } from "antd"; import { BookOpenText, FileText, Image, Pencil, Search, Video } from "lucide-react"; import { WorkspaceState } from "@/components/layout/workspace-state"; +import { getNodeListLabel } from "@/lib/canvas/node-registry"; import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas"; export function CanvasNodeSearchModal({ open, nodes, onClose, onFocus }: { open: boolean; nodes: CanvasNodeData[]; onClose: () => void; onFocus: (nodeId: string) => void }) { @@ -10,7 +11,7 @@ export function CanvasNodeSearchModal({ open, nodes, onClose, onFocus }: { open: const results = useMemo(() => { const keyword = query.trim().toLocaleLowerCase(); if (!keyword) return nodes.slice(0, 40); - return nodes.filter((node) => [node.title, node.type, nodeTypeLabel(node.type), node.metadata?.prompt, node.metadata?.composerContent, node.metadata?.model, node.metadata?.chapterTitle, node.metadata?.workflowTitle, node.metadata?.workflowDescription, typeof node.metadata?.shotIndex === "number" ? `镜头 ${node.metadata.shotIndex + 1}` : "", ...(node.metadata?.assetTags || [])] + return nodes.filter((node) => [node.title, node.type, getNodeListLabel(node.type), node.metadata?.prompt, node.metadata?.composerContent, node.metadata?.model, node.metadata?.chapterTitle, node.metadata?.workflowTitle, node.metadata?.workflowDescription, typeof node.metadata?.shotIndex === "number" ? `镜头 ${node.metadata.shotIndex + 1}` : "", ...(node.metadata?.assetTags || [])] .some((value) => typeof value === "string" && value.toLocaleLowerCase().includes(keyword))).slice(0, 80); }, [nodes, query]); @@ -44,19 +45,5 @@ export function CanvasNodeSearchModal({ open, nodes, onClose, onFocus }: { open: function nodeContextLabel(node: CanvasNodeData) { const location = [node.metadata?.chapterTitle, typeof node.metadata?.shotIndex === "number" ? `镜头 ${node.metadata.shotIndex + 1}` : ""].filter(Boolean).join(" · "); - return location || node.metadata?.prompt || node.metadata?.composerContent || node.metadata?.workflowDescription || nodeTypeLabel(node.type); -} - -function nodeTypeLabel(type: CanvasNodeType) { - return ({ - [CanvasNodeType.Image]: "图片节点", - [CanvasNodeType.Text]: "文本节点", - [CanvasNodeType.Drawing]: "绘图节点", - [CanvasNodeType.Script]: "分镜脚本节点", - [CanvasNodeType.Skill]: "技能节点", - [CanvasNodeType.Config]: "生成配置节点", - [CanvasNodeType.Video]: "视频节点", - [CanvasNodeType.Audio]: "音频节点", - [CanvasNodeType.Frame]: "背板", - } as Record)[type] || "未知节点"; + return location || node.metadata?.prompt || node.metadata?.composerContent || node.metadata?.workflowDescription || getNodeListLabel(node.type); } diff --git a/web/src/components/canvas/canvas-node.tsx b/web/src/components/canvas/canvas-node.tsx index fc936d5bb..07955f6d4 100644 --- a/web/src/components/canvas/canvas-node.tsx +++ b/web/src/components/canvas/canvas-node.tsx @@ -10,7 +10,7 @@ import { useThemeStore } from "@/stores/use-theme-store"; import { storyboardMinNodeHeight } from "./canvas-script-node"; import { CanvasNodeType, type CanvasNodeData, type Position } from "@/types/canvas"; import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; -import { MEDIA_NODE_MIN_SIZE } from "@/lib/canvas/canvas-node-size"; +import { getNodeMinSize, shouldKeepAspectRatio } from "@/lib/canvas/node-registry"; import { CanvasNodeContent, CanvasNodeImageInfo } from "./canvas-node-content"; type ResizeCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right"; @@ -176,9 +176,10 @@ export const CanvasNode = React.memo(function CanvasNode({ const dx = (event.clientX - resizeRef.current.startX) / scale; const dy = (event.clientY - resizeRef.current.startY) / scale; - const isMediaNode = data.type === CanvasNodeType.Image || data.type === CanvasNodeType.Video; - const minWidth = data.type === CanvasNodeType.Script ? 800 : isMediaNode ? MEDIA_NODE_MIN_SIZE.width : 220; - const minHeight = scriptMinHeight || (isMediaNode ? MEDIA_NODE_MIN_SIZE.height : 160); + const minSize = getNodeMinSize(data.type); + const minWidth = minSize.width; + // 分镜脚本的高度由表格内容动态撑开,覆盖注册表里的静态下限。 + const minHeight = scriptMinHeight || minSize.height; const startRight = resizeRef.current.startLeft + resizeRef.current.startWidth; const startBottom = resizeRef.current.startTop + resizeRef.current.startHeight; const fromLeft = resizeRef.current.corner.includes("left"); @@ -230,7 +231,7 @@ export const CanvasNode = React.memo(function CanvasNode({ startTop: data.position.y, startWidth: data.width, startHeight: data.height, - keepRatio: (data.type === CanvasNodeType.Image && !data.metadata?.freeResize) || data.type === CanvasNodeType.Video, + keepRatio: shouldKeepAspectRatio(data), ratio: (data.metadata?.naturalWidth || data.width) / (data.metadata?.naturalHeight || data.height || 1), }; window.addEventListener("mousemove", handleResizeMove); diff --git a/web/src/lib/canvas/node-registry/definitions/builtin-nodes.tsx b/web/src/lib/canvas/node-registry/definitions/builtin-nodes.tsx new file mode 100644 index 000000000..8af8ab33f --- /dev/null +++ b/web/src/lib/canvas/node-registry/definitions/builtin-nodes.tsx @@ -0,0 +1,97 @@ +import { Clapperboard, Image as ImageIcon, Music2, PanelTop, Pencil, Settings2, Sparkles, Type, Video } 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,下方装配时统一读取。 + * + * `satisfies Record` 是刻意的:往 CanvasNodeType 加成员后若不补定义, + * 这里会编译失败,而不是在运行期悄悄回落成兜底值。 + */ +const BUILTIN_NODE_TRAITS = { + [CanvasNodeType.Image]: { + label: "图片", + icon: , + minSize: MEDIA_NODE_MIN_SIZE, + keepAspectRatio: (node: CanvasNodeData) => !node.metadata?.freeResize, + showInCreateMenu: true, + }, + [CanvasNodeType.Text]: { + label: "文本", + icon: , + minSize: DEFAULT_MIN_SIZE, + showInCreateMenu: true, + }, + [CanvasNodeType.Drawing]: { + label: "绘图", + icon: , + minSize: DEFAULT_MIN_SIZE, + showInCreateMenu: true, + }, + [CanvasNodeType.Script]: { + label: "分镜脚本", + icon: , + // 分镜脚本的表格布局需要更宽的下限;高度仍由内容动态撑开(见 canvas-node.tsx)。 + minSize: { width: 800, height: DEFAULT_MIN_SIZE.height }, + showInCreateMenu: true, + }, + [CanvasNodeType.Skill]: { + label: "技能", + icon: , + minSize: DEFAULT_MIN_SIZE, + // 技能节点由技能库插入,不占创建菜单格位。 + showInCreateMenu: false, + }, + [CanvasNodeType.Config]: { + label: "生成配置", + icon: , + minSize: DEFAULT_MIN_SIZE, + // 生成配置由连线时自动创建。 + showInCreateMenu: false, + }, + [CanvasNodeType.Video]: { + label: "视频", + icon: