Scale node content when video canvas modules shrink

This commit is contained in:
Your Name 2026-04-06 23:56:44 +08:00
parent 5763ed1c94
commit ca0fbc5fd4
4 changed files with 86 additions and 3 deletions

View File

@ -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}
>
<div className="relative h-full w-full" style={scaledContentStyle}>
{/* 图片内容 - 使用 object-contain 保持原始比例 */}
{data.src ? (
<div className="relative h-full w-full">
@ -3298,6 +3315,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
快照 {shortAgentExecutionSnapshotId}
</button>
) : null}
</div>
</div>
{/* 角度编辑对话框 */}

View File

@ -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)}
>
<div className="relative h-full w-full" style={scaledContentStyle}>
{/* 视频内容 */}
{data.src ? (
<div className="relative w-full h-full">
@ -306,6 +323,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
<p className="line-clamp-2 text-xs leading-5 text-[#556072]">{data.prompt}</p>
</div>
)}
</div>
</div>
);
}

View File

@ -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<string, any>;
const quickConnectOutputs =
data.templateType && definition
@ -257,7 +269,8 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
style={{ backgroundColor: accentBarColor }}
/>
<div className="flex h-full flex-col gap-3 p-3">
<div className="h-full w-full" style={scaledContentStyle}>
<div className="flex h-full flex-col gap-3 p-3">
<div
className="rounded-[20px] border px-3 py-3"
style={{
@ -385,12 +398,13 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
{item.label}
</span>
))
) : (
) : (
<span className="rounded-full border border-dashed border-[#d8def0] px-2.5 py-1 text-[11px] text-[#6f7483] dark:border-zinc-800">
结束节点
</span>
)}
</div>
</div>
</div>
</div>
);

View File

@ -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",
};
}