diff --git a/web/src/components/canvas/canvas-node-action-context.ts b/web/src/components/canvas/canvas-node-action-context.ts index f0d69993e..aa1954e94 100644 --- a/web/src/components/canvas/canvas-node-action-context.ts +++ b/web/src/components/canvas/canvas-node-action-context.ts @@ -11,6 +11,8 @@ export type CanvasNodeActionContextValue = { deleteNode?: (node: CanvasNodeData) => void; /** 合并式更新节点 metadata;扩展节点(如调色)在自己的面板里改参数时用。 */ updateMetadata?: (nodeId: string, patch: CanvasNodeMetadata) => void; + /** 改节点宽高;图片首次量到真实尺寸后按比例校正节点用。 */ + resizeNode?: (nodeId: string, size: { width: number; height: number }) => void; }; export const CanvasNodeActionContext = createContext({}); diff --git a/web/src/components/canvas/canvas-node-content.tsx b/web/src/components/canvas/canvas-node-content.tsx index 41eb8f7cb..8bd8546e4 100644 --- a/web/src/components/canvas/canvas-node-content.tsx +++ b/web/src/components/canvas/canvas-node-content.tsx @@ -5,6 +5,7 @@ import { VideoPlayer } from "@/components/video-player"; import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerationError } from "@/lib/generation-error"; import { generationTaskShowsProgress, generationTaskStageLabel, generationTaskStatusLabel, isGenerationTaskSubmissionUncertain } from "@/lib/generation-task-display"; import { canvasRichTextHTML } from "@/lib/canvas/canvas-rich-text"; +import { fitNodeSize } from "@/lib/canvas/canvas-node-size"; import { loadCanvasDrawingPreview } from "@/lib/canvas/canvas-drawing-storage"; import { buildLibTVImagePreviewUrl } from "@/lib/canvas/libtv-import"; import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; @@ -16,6 +17,7 @@ import { cacheResourceObjectUrl, getCachedResourceObjectUrl } from "@/services/r import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas"; import { createDefaultSubtitleStyle } from "@/types/timeline"; import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textarea"; +import { useCanvasNodeActions } from "./canvas-node-action-context"; import { CanvasSubtitleOverlay } from "./canvas-subtitle-overlay"; import { MarkdownNodeContent } from "./nodes/markdown-node"; import { ChartNodeContent } from "./nodes/chart-node"; @@ -432,11 +434,36 @@ function ImageContent({ node, theme, isBatchRoot, batchCount, batchExpanded, bat const nearViewport = useNearViewport(imageContainerRef); const { url, loading } = useNodeResourceUrl(node, nearViewport); const importedFromLibTV = node.metadata?.importSource?.provider === "libtv"; + const { resizeNode, updateMetadata } = useCanvasNodeActions(); + + /** + * 让节点跟随图片真实比例。 + * + * 上传接口的 width/height 是可选的(services/api/resources.ts),拿不到时节点会落到 + * 默认的横向比例,竖图就被放进一个宽盒子、两侧留黑。这里在图片解码后量真实尺寸并校正。 + * + * 判据是「用户有没有手动定过尺寸」,**不是**「有没有量过尺寸」——后者会让所有已经 + * 存过 naturalWidth 的旧节点永远得不到修正(第一版就是这么写的,所以没生效)。 + * 手动拉过(manualSize)或自由比例(freeResize)的节点只补记尺寸、不动宽高。 + */ + const fitToImage = (element: HTMLImageElement) => { + const naturalWidth = element.naturalWidth; + const naturalHeight = element.naturalHeight; + if (!naturalWidth || !naturalHeight) return; + if (node.metadata?.naturalWidth !== naturalWidth || node.metadata?.naturalHeight !== naturalHeight) { + updateMetadata?.(node.id, { naturalWidth, naturalHeight }); + } + if (node.metadata?.freeResize || node.metadata?.manualSize) return; + const size = fitNodeSize(naturalWidth, naturalHeight); + // 差不到 1px 就别动,避免无意义的状态写入。 + if (Math.abs(size.width - node.width) < 1 && Math.abs(size.height - node.height) < 1) return; + resizeNode?.(node.id, size); + }; return (
- {url ? {node.title} event.preventDefault()} className={`pointer-events-none block h-full w-full select-none ${node.metadata?.freeResize ? "object-fill" : "object-contain"}`} /> :
{loading ? : }
} + {url ? {node.title} event.preventDefault()} onLoad={(event) => fitToImage(event.currentTarget)} className={`pointer-events-none block h-full w-full select-none ${node.metadata?.freeResize ? "object-fill" : "object-contain"}`} /> :
{loading ? : }
}
); diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index 6c8e9c44c..20a9a0c86 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -1872,7 +1872,7 @@ function InfiniteCanvasPage() { onFileDragLeave={handleFileDragLeave} onFileDragOver={handleFileDragOver} > - duplicateNode(node.id), deleteNode: (node) => deleteNodes(new Set([node.id])), updateMetadata: (nodeId, patch) => setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...patch } } : node))) }}> + duplicateNode(node.id), deleteNode: (node) => deleteNodes(new Set([node.id])), updateMetadata: (nodeId, patch) => setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...patch } } : node))), resizeNode: (nodeId, size) => setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, width: size.width, height: size.height } : node))) }}>