diff --git a/web/src/lib/canvas/canvas-generation-task-sync.ts b/web/src/lib/canvas/canvas-generation-task-sync.ts index a12c762e1..a2f5b202f 100644 --- a/web/src/lib/canvas/canvas-generation-task-sync.ts +++ b/web/src/lib/canvas/canvas-generation-task-sync.ts @@ -1,5 +1,5 @@ import { NODE_DEFAULT_SIZE } from "@/constant/canvas"; -import { fitNodeSize, VIDEO_NODE_MAX_SIZE } from "@/lib/canvas/canvas-node-size"; +import { fitNodeSize, nodeSizeFromRatio, VIDEO_NODE_MAX_SIZE } from "@/lib/canvas/canvas-node-size"; import { compositeEmotionImage } from "@/lib/canvas/canvas-emotion"; import { storeGeneratedAudio } from "@/services/api/audio"; import { storeGeneratedVideo } from "@/services/api/video"; @@ -82,16 +82,22 @@ export async function buildGenerationTaskNodeResult(node: CanvasNodeData, task: ? { url: await resolveImageUrl(image.storageKey, image.dataUrl), storageKey: image.storageKey, width: image.width || 1024, height: image.height || 1024, bytes: image.bytes || 0, mimeType: image.mimeType || "image/png" } : await uploadImage(resultDataUrl); const imageConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Image]; - const imageSize = node.metadata?.generationType === "edit" + const requestedImageSize = nodeSizeFromRatio(node.metadata?.size || "auto", imageConfig.width, imageConfig.height); + const imageSizeBounds = requestedImageSize || { width: node.width || imageConfig.width, height: node.height || imageConfig.height }; + const hasReportedImageSize = Boolean(image.width && image.width > 0 && image.height && image.height > 0); + const resultWidth = image.storageKey && !hasReportedImageSize && requestedImageSize ? requestedImageSize.width : uploaded.width; + const resultHeight = image.storageKey && !hasReportedImageSize && requestedImageSize ? requestedImageSize.height : uploaded.height; + const normalizedImage = resultWidth === uploaded.width && resultHeight === uploaded.height ? uploaded : { ...uploaded, width: resultWidth, height: resultHeight }; + const imageSize = node.metadata?.generationType === "edit" && !requestedImageSize ? { width: node.width || imageConfig.width, height: node.height || imageConfig.height } - : fitNodeSize(uploaded.width, uploaded.height, node.width || imageConfig.width, node.height || imageConfig.height); + : fitNodeSize(resultWidth, resultHeight, imageSizeBounds.width, imageSizeBounds.height); return { ...node, type: CanvasNodeType.Image, width: imageSize.width, height: imageSize.height, position: { x: node.position.x + node.width / 2 - imageSize.width / 2, y: node.position.y + node.height / 2 - imageSize.height / 2 }, - metadata: { ...node.metadata, ...imageMetadata(uploaded), prompt, ...completedTaskMetadata(task), errorDetails: undefined }, + metadata: { ...node.metadata, ...imageMetadata(normalizedImage), prompt, ...completedTaskMetadata(task), errorDetails: undefined }, }; } diff --git a/web/src/lib/canvas/canvas-node-size.ts b/web/src/lib/canvas/canvas-node-size.ts index c621ae919..4c5d6fb65 100644 --- a/web/src/lib/canvas/canvas-node-size.ts +++ b/web/src/lib/canvas/canvas-node-size.ts @@ -30,10 +30,27 @@ export function nodeSizeFromRatio(size: string, baseWidth: number, baseHeight: n export function ensureMediaNodeMinimumSize(node: CanvasNodeData) { if (node.type !== CanvasNodeType.Image && node.type !== CanvasNodeType.Video) return node; - if (node.width >= MEDIA_NODE_MIN_SIZE.width && node.height >= MEDIA_NODE_MIN_SIZE.height) return node; - const scale = Math.max(1, MEDIA_NODE_MIN_SIZE.width / Math.max(1, node.width), MEDIA_NODE_MIN_SIZE.height / Math.max(1, node.height)); - const width = node.width * scale; - const height = node.height * scale; + let width = node.width; + let height = node.height; + const naturalWidth = node.metadata?.naturalWidth || 0; + const naturalHeight = node.metadata?.naturalHeight || 0; + const requestedSize = node.type === CanvasNodeType.Image && node.metadata?.generationType === "edit" + ? nodeSizeFromRatio(node.metadata.size || "auto", node.width, node.height) + : null; + const naturalRatio = naturalWidth / Math.max(1, naturalHeight); + const nodeRatio = node.width / Math.max(1, node.height); + // 修复旧版图生图无条件继承参考节点尺寸造成的比例错误,不覆盖自由拉伸或锁定布局。 + if (requestedSize && naturalWidth > 0 && naturalHeight > 0 && !node.metadata?.freeResize && !node.metadata?.locked && Math.abs(naturalRatio - nodeRatio) > 0.01) { + const alignedSize = fitNodeSize(naturalWidth, naturalHeight, requestedSize.width, requestedSize.height); + width = alignedSize.width; + height = alignedSize.height; + } + if (width < MEDIA_NODE_MIN_SIZE.width || height < MEDIA_NODE_MIN_SIZE.height) { + const scale = Math.max(1, MEDIA_NODE_MIN_SIZE.width / Math.max(1, width), MEDIA_NODE_MIN_SIZE.height / Math.max(1, height)); + width *= scale; + height *= scale; + } + if (width === node.width && height === node.height) return node; return { ...node, position: { diff --git a/web/src/pages/canvas/canvas-image-generation-executor.ts b/web/src/pages/canvas/canvas-image-generation-executor.ts index 0577ee5ce..a853efc10 100644 --- a/web/src/pages/canvas/canvas-image-generation-executor.ts +++ b/web/src/pages/canvas/canvas-image-generation-executor.ts @@ -48,10 +48,11 @@ export async function executeImageGeneration({ const parentConfig = NODE_DEFAULT_SIZE[isConfigNode ? CanvasNodeType.Config : isImageNode ? CanvasNodeType.Image : CanvasNodeType.Text]; const imageDefaults = NODE_DEFAULT_SIZE[CanvasNodeType.Image]; // 生成中占位框按设置比例显示,避免 16:9 任务显示成默认 340x240。 - const imageConfig = nodeSizeFromRatio(generationConfig.size || "auto", imageDefaults.width, imageDefaults.height) || imageDefaults; - // 单图图生图沿用来源节点的可视尺寸,避免竖图回落到默认横向框后被压成窄卡片。 + const requestedImageSize = nodeSizeFromRatio(generationConfig.size || "auto", imageDefaults.width, imageDefaults.height); + const imageConfig = requestedImageSize || imageDefaults; + // auto 图生图沿用来源节点尺寸;用户明确选择比例时必须以目标比例创建节点。 const referenceNode = referenceImages.length === 1 ? canvasNodes.find((node) => node.id === referenceImages[0].id && node.type === CanvasNodeType.Image) : undefined; - const imageSizeSource = isImageNode && sourceNode?.metadata?.content ? sourceNode : referenceNode; + const imageSizeSource = requestedImageSize ? undefined : (isImageNode && sourceNode?.metadata?.content ? sourceNode : referenceNode); const outputNodeSize = imageSizeSource ? { width: imageSizeSource.width, height: imageSizeSource.height } : imageConfig; const parentPosition = sourceNode?.position || { x: 0, y: 0 }; const parentWidth = sourceNode?.width || parentConfig.width; diff --git a/web/src/pages/canvas/use-canvas-generation-retry.ts b/web/src/pages/canvas/use-canvas-generation-retry.ts index 4a7fba428..569908748 100644 --- a/web/src/pages/canvas/use-canvas-generation-retry.ts +++ b/web/src/pages/canvas/use-canvas-generation-retry.ts @@ -5,7 +5,7 @@ import { buildNodeGenerationContext, hydrateNodeGenerationContext } from "@/comp import type { CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel"; import { NODE_DEFAULT_SIZE } from "@/constant/canvas"; import { audioMetadata, imageMetadata, videoMetadata } from "@/lib/canvas/canvas-generation-task-sync"; -import { fitNodeSize, VIDEO_NODE_MAX_SIZE } from "@/lib/canvas/canvas-node-size"; +import { fitNodeSize, nodeSizeFromRatio, VIDEO_NODE_MAX_SIZE } from "@/lib/canvas/canvas-node-size"; import { buildEmotionImageArtifacts, compositeEmotionImage, emotionGenerationSize } from "@/lib/canvas/canvas-emotion"; import { buildAudioGenerationMetadata, @@ -192,13 +192,14 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, activated if (!image?.dataUrl) throw new Error("后端任务没有返回图片"); const uploadedImage = await uploadImage(image.dataUrl); const imageConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Image]; - const imageSize = useReferenceImages + const requestedImageSize = nodeSizeFromRatio(generationConfig.size || "auto", imageConfig.width, imageConfig.height); + const imageSize = useReferenceImages && !requestedImageSize ? { width: node.width, height: node.height } - : fitNodeSize(uploadedImage.width, uploadedImage.height, imageConfig.width, imageConfig.height); + : fitNodeSize(uploadedImage.width, uploadedImage.height, requestedImageSize?.width || imageConfig.width, requestedImageSize?.height || imageConfig.height); const generationMetadata = savedImageMetadata?.generationType ? { generationType: savedImageMetadata.generationType, model: generationConfig.model, size: generationConfig.size, quality: generationConfig.quality, transparentBackground: generationConfig.transparentBackground, count: savedImageMetadata.count || 1, references: savedImageMetadata.references } : buildImageGenerationMetadata(useReferenceImages ? "edit" : "generation", generationConfig, 1, retryImages); - setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, type: CanvasNodeType.Image, width: imageSize.width, height: imageSize.height, metadata: { ...item.metadata, ...imageMetadata(uploadedImage), prompt, ...generationMetadata } } : item))); + setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, type: CanvasNodeType.Image, position: { x: item.position.x + item.width / 2 - imageSize.width / 2, y: item.position.y + item.height / 2 - imageSize.height / 2 }, width: imageSize.width, height: imageSize.height, metadata: { ...item.metadata, ...imageMetadata(uploadedImage), prompt, ...generationMetadata } } : item))); } catch (error) { if (isGenerationCanceled(error)) return; const failure = generationFailureMetadata(error, retryPromptSource);