Files
ttstd-infinite-canvas/web/src/components/canvas/canvas-image-toolbar-tools.tsx
ddcat666 8dcf0a252b feat(previs): 图片生成预演台 - 从图片节点交给 Agent 复现为可编辑 3D 预演
图片工具栏新增「生成预演台」,自动选用支持看图的文本模型并提交复现指令。对话用节点卡片展示「此图」,普通图片演员只写名称时不再要求角色卡绑定。
2026-10-09 17:17:28 +08:00

396 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isValidElement, type ReactNode } from "react";
import { Brush, Camera, Clapperboard, Contrast, Copy, FastForward, FileText, Globe2, Grid2x2, Grid3x3, Layers3, Lock, LockOpen, Maximize2, Package, PencilLine, PersonStanding, Crop, Rewind, ScanFace, SlidersHorizontal, Smile, Sun, Upload, Scaling, WandSparkles } from "lucide-react";
import type { CanvasNodeData } from "@/types/canvas";
import type { NodeToolbarGroup } from "@/lib/canvas/tool-registry";
type ImageNodeActionToolId = "copyPrompt" | "reversePrompt" | "replace" | "resize" | "annotation" | "annotationEdit" | "textEdit" | "maskEdit" | "removeBackground" | "layerDecomposition" | "emotion" | "portraitTexture" | "crop" | "split" | "upscale" | "superResolve" | "angle" | "lighting" | "panorama" | "view" | "previs" | "multi_camera_nine_grid" | "story_pitch_four_grid" | "character_face_three_view" | "product_three_view" | "storyboard_25_grid" | "character_three_view_generation" | "cinematic_light_correction" | "image_projection_after_3s" | "image_projection_before_5s";
type ImageToolHandlers = {
onUpload: (node: CanvasNodeData) => void;
onToggleFreeResize: (node: CanvasNodeData) => void;
onAnnotate: (node: CanvasNodeData) => void;
onAnnotationEdit: (node: CanvasNodeData) => void;
onTextEdit: (node: CanvasNodeData) => void;
onMaskEdit: (node: CanvasNodeData) => void;
onRemoveBackground: (node: CanvasNodeData) => void;
onLayerDecomposition: (node: CanvasNodeData) => void;
onEmotion: (node: CanvasNodeData) => void;
onPortraitTexture: (node: CanvasNodeData) => void;
onCrop: (node: CanvasNodeData) => void;
onUpscale: (node: CanvasNodeData) => void;
onSuperResolve: (node: CanvasNodeData) => void;
onAngle: (node: CanvasNodeData) => void;
onLighting: (node: CanvasNodeData) => void;
onPanorama: (node: CanvasNodeData) => void;
onPrevis: (node: CanvasNodeData) => void;
onViewImage: (node: CanvasNodeData) => void;
onCopyPrompt: (node: CanvasNodeData) => void;
onReversePrompt: (node: CanvasNodeData) => void;
onNineGrid: (node: CanvasNodeData, toolId: number, label: string, icon: string) => void;
};
type ImageToolDefinition = {
id: ImageNodeActionToolId;
label: string | ((node: CanvasNodeData) => string);
icon: (node: CanvasNodeData) => ReactNode;
group: NodeToolbarGroup;
order: number;
section?: string;
description?: string;
active?: (node: CanvasNodeData) => boolean;
run: (node: CanvasNodeData, handlers: ImageToolHandlers, tool: ImageToolDefinition) => void;
/** 仅 group === "nine_grid" 时使用,对应后端工具 ID */
toolId?: number;
};
/** lucide 组件把 PascalCase 图标名写入 displayName,从 icon 渲染结果反查图标名,未命中时回退 Grid3x3。 */
function resolveToolIconName(tool: ImageToolDefinition, node: CanvasNodeData) {
const element = tool.icon(node);
const icon_type = isValidElement(element) ? element.type : null;
const name = icon_type && typeof icon_type !== "string" ? (icon_type as { displayName?: string }).displayName : undefined;
return name || "Grid3x3";
}
/** 九宫格工具共用行为:run 收到自身定义后直接取 toolId/label,图标名从 icon 反查。 */
function nineGridRun(node: CanvasNodeData, handlers: ImageToolHandlers, tool: ImageToolDefinition) {
if (tool.toolId == null) return;
handlers.onNineGrid(node, tool.toolId, resolveToolText(tool.label, node), resolveToolIconName(tool, node));
}
const imageToolDefinitions: ImageToolDefinition[] = [
{
id: "previs",
label: "生成预演台",
icon: () => <Layers3 className="size-3.5" />,
group: "primary",
order: 20,
section: "3D 预演",
description: "让 Agent 识别当前图片并复现为可编辑的 3D 预演台",
run: (node, handlers) => handlers.onPrevis(node),
},
{
id: "copyPrompt",
section: "生成信息",
label: "复制提示词",
icon: () => <Copy className="size-3.5" />,
group: "more",
order: 50,
run: (node, handlers) => handlers.onCopyPrompt(node),
},
{
id: "reversePrompt",
section: "生成信息",
label: "反推提示词",
icon: () => <FileText className="size-3.5" />,
group: "more",
order: 60,
run: (node, handlers) => handlers.onReversePrompt(node),
},
{
id: "replace",
label: "替换图片",
icon: () => <Upload className="size-3.5" />,
group: "more",
section: "素材",
order: 45,
run: (node, handlers) => handlers.onUpload(node),
},
{
id: "resize",
label: "锁定宽高比",
section: "节点管理",
active: (node) => !node.metadata?.freeResize,
icon: (node) => (node.metadata?.freeResize ? <LockOpen className="size-3.5" /> : <Lock className="size-3.5" />),
group: "more",
order: 70,
run: (node, handlers) => handlers.onToggleFreeResize(node),
},
{
id: "annotation",
section: "拆分与标记",
description: "添加标记,另存为新图片",
label: "标注",
icon: () => <PencilLine className="size-3.5" />,
group: "process",
order: 40,
run: (node, handlers) => handlers.onAnnotate(node),
},
{
id: "annotationEdit",
section: "拆分与标记",
description: "用画笔标记区域并让模型按标记修改",
label: "标注编辑",
icon: () => <Brush className="size-3.5" />,
group: "process",
order: 45,
run: (node, handlers) => handlers.onAnnotationEdit(node),
},
{
id: "maskEdit",
label: "局部重绘",
description: "涂抹要修改的区域,生成新图片",
icon: () => <Brush className="size-3.5" />,
group: "primary",
order: 10,
run: (node, handlers) => handlers.onMaskEdit(node),
},
{
id: "textEdit",
label: "文字编辑",
description: "识别图片中的文字并逐行修改",
icon: () => <FileText className="size-3.5" />,
group: "primary",
order: 15,
run: (node, handlers) => handlers.onTextEdit(node),
},
{
id: "removeBackground",
label: "去除背景",
description: "保留主体并生成透明背景图片",
icon: () => <WandSparkles className="size-3.5" />,
group: "process",
order: 50,
run: (node, handlers) => handlers.onRemoveBackground(node),
},
{
id: "layerDecomposition",
label: "AI 图层拆分",
description: "识别并拆分为多个可独立编辑的图片图层",
icon: () => <Layers3 className="size-3.5" />,
group: "process",
order: 55,
run: (node, handlers) => handlers.onLayerDecomposition(node),
},
{
id: "emotion",
label: "表情调整",
section: "人像调整",
description: "调整人物表情,生成新图片",
icon: () => <Smile className="size-3.5" />,
group: "portrait",
order: 50,
run: (node, handlers) => handlers.onEmotion(node),
},
{
id: "portraitTexture",
label: "质感调整",
section: "人像调整",
description: "设置肤质、光影与融合,再执行生成",
icon: () => <SlidersHorizontal className="size-3.5" />,
group: "portrait",
order: 60,
run: (node, handlers) => handlers.onPortraitTexture(node),
},
{
id: "crop",
label: "裁剪",
section: "构图与尺寸",
description: "保留所选区域,生成新图片",
icon: () => <Crop className="size-3.5" />,
group: "process",
order: 10,
run: (node, handlers) => handlers.onCrop(node),
},
{
id: "split",
label: "宫格切分",
section: "拆分与标记",
description: "按行列拆成多个图片节点",
icon: () => <Grid2x2 className="size-3.5" />,
group: "process",
order: 45,
run: () => undefined,
},
{
id: "upscale",
label: "调整尺寸",
section: "构图与尺寸",
description: "插值放大像素尺寸,不是 AI 超分",
icon: () => <Scaling className="size-3.5" />,
group: "process",
order: 20,
run: (node, handlers) => handlers.onUpscale(node),
},
{
id: "angle",
label: "多角度",
section: "视角",
description: "调整摄影机角度,生成新视角",
icon: () => <Camera className="size-3.5" />,
group: "viewpoint",
order: 20,
run: (node, handlers) => handlers.onAngle(node),
},
{
id: "lighting",
label: "打光",
section: "视角",
description: "调整光线方向、亮度与光效,生成新图片",
icon: () => <Sun className="size-3.5" />,
group: "lighting",
order: 30,
run: (node, handlers) => handlers.onLighting(node),
},
{
id: "view",
label: "预览",
icon: () => <Maximize2 className="size-3.5" />,
group: "utility",
order: 10,
run: (node, handlers) => handlers.onViewImage(node),
},
{
id: "panorama",
label: "全景图",
section: "视角",
description: "基于该图片创建 360° 全景查看节点",
icon: () => <Globe2 className="size-3.5" />,
group: "panorama",
order: 80,
run: (node, handlers) => handlers.onPanorama(node),
},
// 九宫格工具组——对应后端 nine_grid 种子数据
{
id: "multi_camera_nine_grid",
label: "多机位九宫格",
section: "宫格生成",
description: "生成 3x3 多机位联系表",
icon: () => <Grid3x3 className="size-3.5" />,
group: "nine_grid",
order: 10,
toolId: 79,
run: nineGridRun,
},
{
id: "story_pitch_four_grid",
label: "剧情推演四宫格",
section: "宫格生成",
description: "生成 2x2 剧情推演联系表",
icon: () => <Grid2x2 className="size-3.5" />,
group: "nine_grid",
order: 20,
toolId: 80,
run: nineGridRun,
},
{
id: "character_face_three_view",
label: "角色脸部三视图",
section: "设定图",
description: "生成 3x2 角色脸部联系表",
icon: () => <ScanFace className="size-3.5" />,
group: "nine_grid",
order: 30,
toolId: 81,
run: nineGridRun,
},
{
id: "product_three_view",
label: "产品三视图",
section: "设定图",
description: "生成 3x2 产品联系表",
icon: () => <Package className="size-3.5" />,
group: "nine_grid",
order: 40,
toolId: 82,
run: nineGridRun,
},
{
id: "storyboard_25_grid",
label: "25宫格连贯分镜",
section: "宫格生成",
description: "生成 5x5 连贯分镜联系表",
icon: () => <Clapperboard className="size-3.5" />,
group: "nine_grid",
order: 50,
toolId: 83,
run: nineGridRun,
},
{
id: "character_three_view_generation",
label: "角色三视图",
section: "设定图",
description: "生成 16:9 角色三视图联系表",
icon: () => <PersonStanding className="size-3.5" />,
group: "nine_grid",
order: 60,
toolId: 85,
run: nineGridRun,
},
{
id: "cinematic_light_correction",
label: "电影级光影校正",
section: "光影",
description: "修正电影灯光,使场景更真实",
icon: () => <Contrast className="size-3.5" />,
group: "nine_grid",
order: 70,
toolId: 84,
run: nineGridRun,
},
{
id: "image_projection_after_3s",
label: "画面推演-3秒后",
section: "画面推演",
description: "生成 3 秒后的画面帧",
icon: () => <FastForward className="size-3.5" />,
group: "nine_grid",
order: 80,
toolId: 86,
run: nineGridRun,
},
{
id: "image_projection_before_5s",
label: "画面推演-5秒前",
section: "画面推演",
description: "生成 5 秒前的画面帧",
icon: () => <Rewind className="size-3.5" />,
group: "nine_grid",
order: 90,
toolId: 87,
run: nineGridRun,
},
];
export function buildImageToolbarTools(node: CanvasNodeData, handlers: ImageToolHandlers) {
return imageToolDefinitions.map((tool) => ({
id: tool.id,
label: resolveToolText(tool.label, node),
icon: tool.icon(node),
group: tool.group,
order: tool.order,
section: tool.section,
description: tool.description,
active: tool.active?.(node),
onClick: () => tool.run(node, handlers, tool),
}));
}
function resolveToolText(value: string | ((node: CanvasNodeData) => string), node: CanvasNodeData) {
return typeof value === "function" ? value(node) : value;
}
export type NineGridMenuItem = {
id: string;
label: string;
section: string;
description: string;
toolId: number;
toolIconName: string;
};
/** 从 imageToolDefinitions 中筛选 nine_grid 组,返回九宫格菜单项数据 */
export function getNineGridMenuItems(): NineGridMenuItem[] {
return imageToolDefinitions
.filter((tool) => tool.group === "nine_grid" && tool.toolId != null)
.sort((a, b) => a.order - b.order)
.map((tool) => ({
id: tool.id,
label: typeof tool.label === "function" ? tool.label({} as CanvasNodeData) : tool.label,
section: tool.section || "常用操作",
description: tool.description || "",
toolId: tool.toolId!,
toolIconName: resolveToolIconName(tool, {} as CanvasNodeData),
}));
}