@@ -1,5 +1,5 @@
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { fitNodeSize } from "@/lib/canvas/canvas-node-size";
|
||||
import { fitNodeSize, 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";
|
||||
@@ -17,8 +17,6 @@ type BackendGenerationResult = {
|
||||
text?: string;
|
||||
};
|
||||
|
||||
const VIDEO_NODE_MAX_WIDTH = 420;
|
||||
const VIDEO_NODE_MAX_HEIGHT = 420;
|
||||
|
||||
export function parseBackendGenerationResult(task: GenerationTask): BackendGenerationResult {
|
||||
if (!task.resultJson) throw new Error("后端任务没有返回结果");
|
||||
@@ -102,7 +100,7 @@ export async function buildGenerationTaskNodeResult(node: CanvasNodeData, task:
|
||||
const video = result.video.storageKey
|
||||
? { url: await resolveMediaUrl(result.video.storageKey, result.video.dataUrl), storageKey: result.video.storageKey, width: result.video.width, height: result.video.height, durationMs: result.video.durationMs, bytes: result.video.bytes || 0, mimeType: result.video.mimeType || "video/mp4" }
|
||||
: await storeGeneratedVideo({ url: result.video.dataUrl, mimeType: result.video.mimeType || "video/mp4" });
|
||||
const videoSize = fitNodeSize(video.width || node.width || VIDEO_NODE_MAX_WIDTH, video.height || node.height || VIDEO_NODE_MAX_HEIGHT, VIDEO_NODE_MAX_WIDTH, VIDEO_NODE_MAX_HEIGHT);
|
||||
const videoSize = fitNodeSize(video.width || node.width || VIDEO_NODE_MAX_SIZE.width, video.height || node.height || VIDEO_NODE_MAX_SIZE.height, VIDEO_NODE_MAX_SIZE.width, VIDEO_NODE_MAX_SIZE.height);
|
||||
return {
|
||||
...node,
|
||||
type: CanvasNodeType.Video,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SelectionBox, ViewportTransform } from "@/types/canvas";
|
||||
|
||||
export const CANVAS_VIEWPORT_PREVIEW_EVENT = "canvas:viewport-preview";
|
||||
export const CANVAS_GRAPHICS_VIEWPORT_PREVIEW_EVENT = "canvas:graphics-viewport-preview";
|
||||
export const CANVAS_SELECTION_PREVIEW_EVENT = "canvas:selection-preview";
|
||||
|
||||
export function applyCanvasLiveViewport(container: HTMLDivElement | null, viewport: ViewportTransform, notify = true) {
|
||||
@@ -15,6 +16,8 @@ export function applyCanvasLiveViewport(container: HTMLDivElement | null, viewpo
|
||||
container.style.setProperty("--canvas-grid-x", `${viewport.x % gridSize}px`);
|
||||
container.style.setProperty("--canvas-grid-y", `${viewport.y % gridSize}px`);
|
||||
container.style.setProperty("--canvas-dot-size", viewport.k < 0.12 ? "0.8px" : "1.15px");
|
||||
// 图形层必须逐帧跟随 DOM 世界层;浮层和滚动通知仍可按原频率节流。
|
||||
container.dispatchEvent(new CustomEvent<ViewportTransform>(CANVAS_GRAPHICS_VIEWPORT_PREVIEW_EVENT, { detail: viewport }));
|
||||
if (notify) {
|
||||
container.dispatchEvent(new CustomEvent<ViewportTransform>(CANVAS_VIEWPORT_PREVIEW_EVENT, { detail: viewport }));
|
||||
// Ant Design overlays watch scrollable ancestors, but CSS transforms do not emit layout events.
|
||||
@@ -22,6 +25,12 @@ export function applyCanvasLiveViewport(container: HTMLDivElement | null, viewpo
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeCanvasGraphicsViewportPreview(container: HTMLDivElement, listener: (viewport: ViewportTransform) => void) {
|
||||
const handlePreview = (event: Event) => listener((event as CustomEvent<ViewportTransform>).detail);
|
||||
container.addEventListener(CANVAS_GRAPHICS_VIEWPORT_PREVIEW_EVENT, handlePreview);
|
||||
return () => container.removeEventListener(CANVAS_GRAPHICS_VIEWPORT_PREVIEW_EVENT, handlePreview);
|
||||
}
|
||||
|
||||
export function subscribeCanvasViewportPreview(container: HTMLDivElement, listener: (viewport: ViewportTransform) => void) {
|
||||
const handlePreview = (event: Event) => listener((event as CustomEvent<ViewportTransform>).detail);
|
||||
container.addEventListener(CANVAS_VIEWPORT_PREVIEW_EVENT, handlePreview);
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
export function fitNodeSize(width: number, height: number, maxWidth = 640, maxHeight = 640) {
|
||||
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
export const MEDIA_NODE_MIN_SIZE = { width: 300, height: 220 } as const;
|
||||
export const VIDEO_NODE_MAX_SIZE = { width: 480, height: 480 } as const;
|
||||
|
||||
export function fitNodeSize(width: number, height: number, maxWidth = 640, maxHeight = 640, minWidth = MEDIA_NODE_MIN_SIZE.width, minHeight = MEDIA_NODE_MIN_SIZE.height) {
|
||||
const w = Math.max(1, width);
|
||||
const h = Math.max(1, height);
|
||||
const scale = Math.min(1, maxWidth / w, maxHeight / h);
|
||||
// 媒体节点既要保留原始比例,也要给生成状态、操作按钮留下稳定的可读空间。
|
||||
const preferredScale = Math.min(1, maxWidth / w, maxHeight / h);
|
||||
const minimumScale = Math.max(minWidth / w, minHeight / h);
|
||||
const scale = Math.max(preferredScale, minimumScale);
|
||||
return { width: w * scale, height: h * scale };
|
||||
}
|
||||
|
||||
@@ -16,5 +24,23 @@ export function nodeSizeFromRatio(size: string, baseWidth: number, baseHeight: n
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
|
||||
const ratio = width / Math.max(1, height);
|
||||
if (ratio < 0.25 || ratio > 4) return { width: baseWidth, height: baseHeight };
|
||||
return ratio >= baseWidth / baseHeight ? { width: baseWidth, height: baseWidth / ratio } : { width: baseHeight * ratio, height: baseHeight };
|
||||
const candidateSize = ratio >= baseWidth / baseHeight ? { width: baseWidth, height: baseWidth / ratio } : { width: baseHeight * ratio, height: baseHeight };
|
||||
return fitNodeSize(candidateSize.width, candidateSize.height, baseWidth, baseHeight);
|
||||
}
|
||||
|
||||
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;
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: node.position.x + node.width / 2 - width / 2,
|
||||
y: node.position.y + node.height / 2 - height / 2,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import { isSeedanceVideoConfig } from "@/lib/seedance-video";
|
||||
import { imageMetadata, parseBackendGenerationResult } from "@/lib/canvas/canvas-generation-task-sync";
|
||||
import { ensureMediaNodeMinimumSize } from "@/lib/canvas/canvas-node-size";
|
||||
import type { CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel";
|
||||
import { CanvasNodeType, type CanvasAssistantSession, type CanvasConnection, type CanvasImageGenerationType, type CanvasNodeData, type CanvasNodeMetadata, type CanvasVideoEditOperation } from "@/types/canvas";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
@@ -363,7 +364,8 @@ export function supportsVideoReferenceAudio(config: AiConfig) {
|
||||
export function resetInterruptedGeneration(nodes: CanvasNodeData[]) {
|
||||
const configHeight = NODE_DEFAULT_SIZE[CanvasNodeType.Config].height;
|
||||
return nodes.map((node) => {
|
||||
const resizedNode = node.type === CanvasNodeType.Config && node.height < configHeight ? { ...node, height: configHeight } : node.type === CanvasNodeType.Script && node.height < NODE_DEFAULT_SIZE[CanvasNodeType.Script].height ? { ...node, height: NODE_DEFAULT_SIZE[CanvasNodeType.Script].height } : node;
|
||||
const mediaNode = ensureMediaNodeMinimumSize(node);
|
||||
const resizedNode = mediaNode.type === CanvasNodeType.Config && mediaNode.height < configHeight ? { ...mediaNode, height: configHeight } : mediaNode.type === CanvasNodeType.Script && mediaNode.height < NODE_DEFAULT_SIZE[CanvasNodeType.Script].height ? { ...mediaNode, height: NODE_DEFAULT_SIZE[CanvasNodeType.Script].height } : mediaNode;
|
||||
return resizedNode.metadata?.status === "loading" ? { ...resizedNode, metadata: { ...resizedNode.metadata, errorDetails: "正在从任务中心恢复生成状态..." } } : resizedNode;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user