refactor(canvas): 节点注册表 - 收敛节点展示与几何知识到单一数据源
每加一种画布节点,叫什么、长什么样、能不能自由拉伸要在 4-5 处各写一遍。 实际后果是节点标签出现三份且互相矛盾(NODE_DEFAULT_SIZE.title / 创建菜单 label / 搜索弹窗 nodeTypeLabel),canvas-node-size.ts 里还有第四份硬编码兜底。 新增 lib/canvas/node-registry/,镜像现有 tool-registry/ 的结构: 静态定义带 satisfies Record<CanvasNodeType, ...> 做编译期穷尽性检查 (往枚举加成员不补定义即编译失败),再注册进模块级 Map 并按 ownerId 归属, 为后续扩展留出隔离位。 尺寸与初始 metadata 的数据源仍是 constant/canvas.ts 的 NODE_SPECS,定义文件读它 而非复制,现有约 40 处布局计算未动;渲染层 nodeContentRenderers 已是查表结构, 本次不改。 标签刻意分三个字段各司其职,界面文案零变化: - defaultTitle 新建节点默认名(Note/Audio/未命名背板 原样保留) - label UI 短标签 - listLabel 列表标签,缺省派生为 label + 节点;背板显式钉住为背板 接入 3 处调用点:创建菜单标签与图标、搜索弹窗类型标签、canvas-node.tsx 的 minWidth/minHeight 与 keepRatio。导演台落在节点分区但开的是工作台、不是节点类型, 故不走注册表。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<CanvasNodeType, string>)[type] || "未知节点";
|
||||
return location || node.metadata?.prompt || node.metadata?.composerContent || node.metadata?.workflowDescription || getNodeListLabel(node.type);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user