From 9b3ec218e278d74fc77b6db1fcb0a6af98f74e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=AC=91=E7=9A=84=E6=AF=9B=E6=AF=9B=E8=99=AB?= <315515767@qq.com> Date: Mon, 3 Aug 2026 20:11:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20=E4=BA=BA=E7=89=A9=E8=B4=A8?= =?UTF-8?q?=E6=84=9F=E5=8F=98=E4=BD=93=E8=87=AA=E5=8A=A8=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E7=94=9F=E6=88=90=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=BC=A9=E7=95=A5?= =?UTF-8?q?=E5=9B=BE=E4=B8=8D=E5=8F=98=20#91=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- .../canvas/canvas-drawing-editor-modal.tsx | 2 +- .../canvas-drawing-excalidraw-editor.tsx | 2 +- web/src/pages/canvas/project.tsx | 4 +- .../pages/canvas/use-canvas-media-tools.ts | 67 +++++++++++-------- 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/web/src/components/canvas/canvas-drawing-editor-modal.tsx b/web/src/components/canvas/canvas-drawing-editor-modal.tsx index 5d6d2b3c8..efb9615ed 100644 --- a/web/src/components/canvas/canvas-drawing-editor-modal.tsx +++ b/web/src/components/canvas/canvas-drawing-editor-modal.tsx @@ -16,7 +16,7 @@ type CanvasDrawingEditorModalProps = { projectId: string; node: CanvasNodeData | null; onClose: () => void; - onSaved: (nodeId: string, summary: Pick) => void; + onSaved: (nodeId: string, summary: Pick) => void; }; export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSaved }: CanvasDrawingEditorModalProps) { diff --git a/web/src/components/canvas/canvas-drawing-excalidraw-editor.tsx b/web/src/components/canvas/canvas-drawing-excalidraw-editor.tsx index 2f328b544..8c362468f 100644 --- a/web/src/components/canvas/canvas-drawing-excalidraw-editor.tsx +++ b/web/src/components/canvas/canvas-drawing-excalidraw-editor.tsx @@ -31,7 +31,7 @@ export const CanvasDrawingExcalidrawEditor = forwardRef { + 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)); diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index 12c3e640a..817b29b28 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -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)} diff --git a/web/src/pages/canvas/use-canvas-media-tools.ts b/web/src/pages/canvas/use-canvas-media-tools.ts index d838eb153..3b7c83cae 100644 --- a/web/src/pages/canvas/use-canvas-media-tools.ts +++ b/web/src/pages/canvas/use-canvas-media-tools.ts @@ -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,