fix(canvas): 人物质感变体自动触发生成,修复缩略图不变 #91 (#92)

- use-canvas-media-tools.ts: createPortraitTextureNode 改为 generatePortraitTextureNode,
  节点创建后立即触发后端生成任务(与情绪/多视角/局部编辑保持一致):
  使用 nodeReferenceImage 获取源图引用,buildPortraitTexturePrompt 构建提示词,
  runBackendCanvasGenerationTask 执行生成,完成后更新缩略图
- project.tsx: 同步重命名 createPortraitTextureNode → generatePortraitTextureNode
- canvas-drawing-excalidraw-editor.tsx: getDimensions 参数补充 number 类型注解
- canvas-drawing-editor-modal.tsx: Pick 类型补充 engine 字段

Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
爱笑的毛毛虫
2026-08-03 20:11:13 +08:00
committed by GitHub
parent d387b25e2e
commit 9b3ec218e2
4 changed files with 42 additions and 33 deletions

View File

@@ -16,7 +16,7 @@ type CanvasDrawingEditorModalProps = {
projectId: string;
node: CanvasNodeData | null;
onClose: () => void;
onSaved: (nodeId: string, summary: Pick<CanvasDrawingSnapshot, "revision" | "updatedAt" | "shapeCount" | "pageCount">) => void;
onSaved: (nodeId: string, summary: Pick<CanvasDrawingSnapshot, "engine" | "revision" | "updatedAt" | "shapeCount" | "pageCount">) => void;
};
export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSaved }: CanvasDrawingEditorModalProps) {

View File

@@ -31,7 +31,7 @@ export const CanvasDrawingExcalidrawEditor = forwardRef<CanvasDrawingEditorHandl
files,
mimeType: "image/png",
exportPadding: DRAWING_RENDER_PADDING,
getDimensions: (width, height) => {
getDimensions: (width: number, height: number) => {
const scale = Math.min(4, DRAWING_RENDER_MAX_DIMENSION / Math.max(1, width, height));
outputWidth = Math.max(1, Math.round(width * scale));
outputHeight = Math.max(1, Math.round(height * scale));

View File

@@ -509,7 +509,7 @@ function InfiniteCanvasPage() {
emotionNodeId,
annotationNodeId,
createImageReversePromptNodes,
createPortraitTextureNode,
generatePortraitTextureNode,
cropImageNode,
cropNodeId,
extractVideoLastFrame,
@@ -1557,7 +1557,7 @@ function InfiniteCanvasPage() {
onAnnotate={(node) => setAnnotationNodeId(node.id)}
onMaskEdit={(node) => setMaskEditNodeId(node.id)}
onEmotion={(node) => { setDialogNodeId(null); setEmotionNodeId((current) => current === node.id ? null : node.id); }}
onPortraitTexture={createPortraitTextureNode}
onPortraitTexture={generatePortraitTextureNode}
onCrop={(node) => setCropNodeId(node.id)}
onSplit={(node) => setSplitNodeId(node.id)}
onUpscale={(node) => setUpscaleNodeId(node.id)}

View File

@@ -21,7 +21,7 @@ import {
} from "@/lib/canvas/canvas-project-generation";
import { fitNodeSize, VIDEO_NODE_MAX_SIZE } from "@/lib/canvas/canvas-node-size";
import { compositeEmotionImage, emotionGenerationSize } from "@/lib/canvas/canvas-emotion";
import { DEFAULT_PORTRAIT_TEXTURE_SETTINGS } from "@/lib/canvas/canvas-portrait-texture";
import { DEFAULT_PORTRAIT_TEXTURE_SETTINGS, buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
import { captureVideoLastFrame } from "@/lib/canvas/canvas-video-frame";
import { mergeVideos, type MergeVideoProgress } from "@/lib/canvas/canvas-video-merge";
import { generationErrorMessage } from "@/lib/generation-error";
@@ -129,41 +129,50 @@ export function useCanvasMediaTools({
setContextMenu(null);
}, [effectiveConfig.model, effectiveConfig.textModel, message, setConnections, setContextMenu, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
const createPortraitTextureNode = useCallback((node: CanvasNodeData) => {
const generatePortraitTextureNode = useCallback(async (node: CanvasNodeData) => {
if (node.type !== CanvasNodeType.Image || !node.metadata?.content) {
message.warning("图片节点为空,无法调节人物质感");
return;
}
const generationConfig = { ...buildGenerationConfig(effectiveConfig, node, "image"), count: "1" };
if (!isAiConfigReady(generationConfig, generationConfig.model)) {
navigateToSettings({ continueCreation: true });
return;
}
const childId = nanoid();
const imageSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
const composerContent = `@[node:${node.id}]`;
const child = createCanvasNode(
CanvasNodeType.Image,
{
x: node.position.x + node.width + 96 + imageSpec.width / 2,
y: node.position.y + node.height / 2,
},
{
generationMode: "image",
model: effectiveConfig.imageModel || effectiveConfig.model,
size: node.metadata.size || effectiveConfig.size,
quality: node.metadata.quality || effectiveConfig.quality,
transparentBackground: node.metadata.transparentBackground || effectiveConfig.transparentBackground,
count: 1,
prompt: composerContent,
composerContent,
portraitTexture: { ...DEFAULT_PORTRAIT_TEXTURE_SETTINGS },
},
);
child.title = "人物质感调节";
setNodes((current) => [...current, child]);
setConnections((current) => [...current, { id: nanoid(), fromNodeId: node.id, toNodeId: child.id }]);
setSelectedNodeIds(new Set([child.id]));
setSelectedConnectionId(null);
setDialogNodeId(child.id);
setContextMenu(null);
const source = nodeReferenceImage(node);
if (!source) return;
const prompt = buildPortraitTexturePrompt(composerContent, { ...DEFAULT_PORTRAIT_TEXTURE_SETTINGS, ...node.metadata?.portraitTexture });
const generationMetadata = buildImageGenerationMetadata("edit", generationConfig, 1, [source]);
const portraitTextureSettings = { ...DEFAULT_PORTRAIT_TEXTURE_SETTINGS, ...node.metadata?.portraitTexture };
setHoveredNodeId(null);
setToolbarNodeId(null);
}, [effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.quality, effectiveConfig.size, effectiveConfig.transparentBackground, message, setConnections, setContextMenu, setDialogNodeId, setHoveredNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, setToolbarNodeId]);
setRunningNodeId(childId);
setNodes((current) => [...current, { id: childId, type: CanvasNodeType.Image, title: "人物质感调节", position: { x: node.position.x + node.width + 96 + imageSpec.width / 2, y: node.position.y + node.height / 2 }, width: imageSpec.width, height: imageSpec.height, metadata: { prompt, status: NODE_STATUS_LOADING, composerContent, portraitTexture: portraitTextureSettings, ...generationMetadata } }]);
setConnections((current) => [...current, { id: nanoid(), fromNodeId: node.id, toNodeId: childId }]);
setSelectedNodeIds(new Set([childId]));
setSelectedConnectionId(null);
setDialogNodeId(childId);
const controller = startGenerationRequest(childId, node.id, childId);
try {
const result = await runBackendCanvasGenerationTask({ projectId, nodeId: childId, mode: "image", prompt, config: generationConfig, referenceImages: [source], signal: controller.signal, metadata: { sourceNodeId: node.id, edit: "portraitTexture", portraitTexture: portraitTextureSettings }, onTaskCreated: (task) => bindGenerationTask(childId, task) });
const image = result.images?.[0];
if (!image?.dataUrl) throw new Error("后端任务没有返回图片");
const uploaded = await uploadImage(image.dataUrl);
const size = fitNodeSize(uploaded.width, uploaded.height, imageSpec.width, imageSpec.height);
setNodes((current) => current.map((item) => item.id === childId ? { ...item, width: size.width, height: size.height, metadata: { ...item.metadata, ...imageMetadata(uploaded), prompt, ...generationMetadata, portraitTexture: portraitTextureSettings } } : item));
} catch (error) {
if (isGenerationCanceled(error)) return;
const details = generationErrorMessage(error);
message.error(details);
setNodes((current) => current.map((item) => item.id === childId ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails: details } } : item));
} finally {
finishGenerationRequest(childId, controller);
setRunningNodeId(null);
}
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, projectId, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedConnectionId, setSelectedNodeIds, startGenerationRequest]);
const cropImageNode = useCallback(async (node: CanvasNodeData, crop: CanvasImageCropRect) => {
if (!node.metadata?.content) return;
@@ -467,7 +476,7 @@ export function useCanvasMediaTools({
emotionNodeId,
annotationNodeId,
createImageReversePromptNodes,
createPortraitTextureNode,
generatePortraitTextureNode,
cropImageNode,
cropNodeId,
extractVideoLastFrame,