From ca0fbc5fd47227ee534ccbd40270ba785601517e Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 6 Apr 2026 23:56:44 +0800 Subject: [PATCH] Scale node content when video canvas modules shrink --- components/canvas/nodes/ImageNode.tsx | 18 +++++++++++++ components/canvas/nodes/VideoNode.tsx | 20 +++++++++++++- components/canvas/nodes/WorkflowNode.tsx | 18 +++++++++++-- lib/canvas/node-content-scale.ts | 33 ++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 lib/canvas/node-content-scale.ts diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 51b3bad50..713dd225b 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -69,6 +69,10 @@ import { import type { DetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter"; import { buildImageProxyUrl } from "@/lib/image-delivery"; import { inferOriginalImageUrl } from "@/lib/image-url"; +import { + getNodeContentScale, + getScaledNodeContentStyle, +} from "@/lib/canvas/node-content-scale"; import { cn } from "@/lib/utils"; import type { CanvasTextEditPayloadItem, @@ -350,6 +354,18 @@ interface ImageNodeProps { export function ImageNode({ node, isSelected }: ImageNodeProps) { const data = node.data as ImageNodeData; + const contentScale = useMemo( + () => + getNodeContentScale(node.size, { + width: 220, + height: 220, + }), + [node.size.height, node.size.width] + ); + const scaledContentStyle = useMemo( + () => getScaledNodeContentStyle(contentScale), + [contentScale] + ); const detailProgressLabel = typeof data.metadata?.detailProgressLabel === "string" ? data.metadata.detailProgressLabel.trim() @@ -2867,6 +2883,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { onMouseLeave={() => setIsHovered(false)} ref={containerRef} > +
{/* 图片内容 - 使用 object-contain 保持原始比例 */} {data.src ? (
@@ -3298,6 +3315,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { 快照 {shortAgentExecutionSnapshotId} ) : null} +
{/* 角度编辑对话框 */} diff --git a/components/canvas/nodes/VideoNode.tsx b/components/canvas/nodes/VideoNode.tsx index a20d46cfc..60c331b31 100644 --- a/components/canvas/nodes/VideoNode.tsx +++ b/components/canvas/nodes/VideoNode.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useRef } from "react"; +import React, { useMemo, useRef, useState } from "react"; import { Video, Loader2, AlertCircle, MoreVertical, Download, Trash2, Play, Pause, Volume2, VolumeX } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -13,6 +13,10 @@ import { import { Slider } from "@/components/ui/slider"; import { AgentExecutionSnapshotDialog } from "@/components/agents/AgentExecutionSnapshotDialog"; import { toast } from "sonner"; +import { + getNodeContentScale, + getScaledNodeContentStyle, +} from "@/lib/canvas/node-content-scale"; import { useCanvasStore } from "@/store/canvas"; import type { CanvasNode, VideoNodeData } from "@/types/canvas"; import { cn } from "@/lib/utils"; @@ -24,6 +28,18 @@ interface VideoNodeProps { export function VideoNode({ node, isSelected }: VideoNodeProps) { const data = node.data as VideoNodeData; + const contentScale = useMemo( + () => + getNodeContentScale(node.size, { + width: 240, + height: 220, + }), + [node.size.height, node.size.width] + ); + const scaledContentStyle = useMemo( + () => getScaledNodeContentStyle(contentScale), + [contentScale] + ); const agentExecutionSnapshotId = typeof data.metadata?.agentExecutionSnapshotId === "string" ? data.metadata.agentExecutionSnapshotId.trim() @@ -146,6 +162,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) { onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > +
{/* 视频内容 */} {data.src ? (
@@ -306,6 +323,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {

{data.prompt}

)} +
); } diff --git a/components/canvas/nodes/WorkflowNode.tsx b/components/canvas/nodes/WorkflowNode.tsx index 1cc264660..779fef9f7 100644 --- a/components/canvas/nodes/WorkflowNode.tsx +++ b/components/canvas/nodes/WorkflowNode.tsx @@ -13,6 +13,10 @@ import { import { Badge } from "@/components/ui/badge"; import { useCanvasStore } from "@/store/canvas"; import { cn } from "@/lib/utils"; +import { + getNodeContentScale, + getScaledNodeContentStyle, +} from "@/lib/canvas/node-content-scale"; import { getVideoWorkflowNodeDefinition, getVideoWorkflowQuickConnectOutputs, @@ -131,6 +135,14 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) { const deleteNode = useCanvasStore((state) => state.deleteNode); const data = node.data as WorkflowNodeData; const definition = getVideoWorkflowNodeDefinition(data.templateType); + const contentScale = getNodeContentScale( + node.size, + definition?.size || { + width: 300, + height: 180, + } + ); + const scaledContentStyle = getScaledNodeContentStyle(contentScale); const metadata = (data.metadata || {}) as Record; const quickConnectOutputs = data.templateType && definition @@ -257,7 +269,8 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) { style={{ backgroundColor: accentBarColor }} /> -
+
+
)) - ) : ( + ) : ( 结束节点 )}
+
); diff --git a/lib/canvas/node-content-scale.ts b/lib/canvas/node-content-scale.ts new file mode 100644 index 000000000..f05f17e3d --- /dev/null +++ b/lib/canvas/node-content-scale.ts @@ -0,0 +1,33 @@ +import type { CSSProperties } from "react"; +import type { Size } from "@/types/canvas"; + +const DEFAULT_MIN_SCALE = 0.56; + +export function getNodeContentScale( + size: Size, + baseSize: Size, + minScale = DEFAULT_MIN_SCALE +) { + if (!size.width || !size.height || !baseSize.width || !baseSize.height) { + return 1; + } + + const widthScale = size.width / baseSize.width; + const heightScale = size.height / baseSize.height; + return Math.min(1, Math.max(minScale, Math.min(widthScale, heightScale))); +} + +export function getScaledNodeContentStyle( + scale: number +): CSSProperties | undefined { + if (scale >= 0.999) { + return undefined; + } + + return { + width: `${100 / scale}%`, + height: `${100 / scale}%`, + transform: `scale(${scale})`, + transformOrigin: "top left", + }; +}