170 lines
3.8 KiB
TypeScript
170 lines
3.8 KiB
TypeScript
import type {
|
|
CanvasConfigNodeMetadata,
|
|
CanvasImageNodeMetadata,
|
|
CanvasNode,
|
|
CanvasNodeType,
|
|
CanvasPosition,
|
|
} from "@/lib/canvas/types";
|
|
import type { HistoryItem } from "@/lib/image-workflow";
|
|
|
|
export const DEFAULT_NODE_WIDTH = 280;
|
|
export const DEFAULT_NODE_HEIGHT = 170;
|
|
export const DEFAULT_IMAGE_WIDTH = 320;
|
|
export const DEFAULT_IMAGE_HEIGHT = 240;
|
|
|
|
export const defaultCanvasConfig: CanvasConfigNodeMetadata = {
|
|
model: "gpt-image-2-2k",
|
|
size: "1024x1024",
|
|
quality: "high",
|
|
outputFormat: "png",
|
|
preserveIdentity: true,
|
|
};
|
|
|
|
export function createCanvasNode(
|
|
type: CanvasNodeType,
|
|
position: CanvasPosition,
|
|
): CanvasNode {
|
|
const id = crypto.randomUUID();
|
|
|
|
if (type === "prompt") {
|
|
return {
|
|
id,
|
|
type,
|
|
title: "提示词",
|
|
position,
|
|
width: DEFAULT_NODE_WIDTH,
|
|
height: DEFAULT_NODE_HEIGHT,
|
|
metadata: {
|
|
prompt: "描述你想生成的画面...",
|
|
},
|
|
};
|
|
}
|
|
|
|
if (type === "config") {
|
|
return {
|
|
id,
|
|
type,
|
|
title: "生成配置",
|
|
position,
|
|
width: DEFAULT_NODE_WIDTH,
|
|
height: DEFAULT_NODE_HEIGHT,
|
|
metadata: { ...defaultCanvasConfig },
|
|
};
|
|
}
|
|
|
|
return {
|
|
id,
|
|
type,
|
|
title: "参考图片",
|
|
position,
|
|
width: DEFAULT_IMAGE_WIDTH,
|
|
height: DEFAULT_IMAGE_HEIGHT,
|
|
metadata: {
|
|
imageUrl: "",
|
|
mode: "reference",
|
|
status: "idle",
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createImageNodeFromHistory(
|
|
item: HistoryItem,
|
|
position: CanvasPosition,
|
|
): CanvasNode {
|
|
const size = getImageNodeSize(item);
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
type: "image",
|
|
title: "历史图片",
|
|
position,
|
|
width: size.width,
|
|
height: size.height,
|
|
metadata: {
|
|
historyId: item.id,
|
|
imageUrl: item.imageUrl,
|
|
mode: "reference",
|
|
prompt: item.prompt,
|
|
model: item.model,
|
|
size: item.size,
|
|
quality: item.quality,
|
|
outputFormat: item.outputFormat,
|
|
status: "success",
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createImageNodeFromUpload(
|
|
file: File,
|
|
imageUrl: string,
|
|
position: CanvasPosition,
|
|
): CanvasNode {
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
type: "image",
|
|
title: file.name || "上传图片",
|
|
position,
|
|
width: DEFAULT_IMAGE_WIDTH,
|
|
height: DEFAULT_IMAGE_HEIGHT,
|
|
metadata: {
|
|
imageUrl,
|
|
mode: "reference",
|
|
status: "success",
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createImageResultNode(
|
|
result: HistoryItem,
|
|
position: CanvasPosition,
|
|
): CanvasNode {
|
|
const size = getImageNodeSize(result);
|
|
const metadata: CanvasImageNodeMetadata = {
|
|
imageUrl: result.imageUrl,
|
|
mode: "result",
|
|
historyId: result.id,
|
|
prompt: result.prompt,
|
|
model: result.model,
|
|
size: result.size,
|
|
quality: result.quality,
|
|
outputFormat: result.outputFormat,
|
|
status: "success",
|
|
};
|
|
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
type: "image",
|
|
title: "生成结果",
|
|
position,
|
|
width: size.width,
|
|
height: size.height,
|
|
metadata,
|
|
};
|
|
}
|
|
|
|
function getImageNodeSize(item: Pick<HistoryItem, "size">) {
|
|
const match = /^(\d+)x(\d+)$/.exec(item.size || "");
|
|
if (!match) {
|
|
return { width: DEFAULT_IMAGE_WIDTH, height: DEFAULT_IMAGE_HEIGHT };
|
|
}
|
|
|
|
const naturalWidth = Number(match[1]);
|
|
const naturalHeight = Number(match[2]);
|
|
if (!naturalWidth || !naturalHeight) {
|
|
return { width: DEFAULT_IMAGE_WIDTH, height: DEFAULT_IMAGE_HEIGHT };
|
|
}
|
|
|
|
const maxSide = 360;
|
|
const minSide = 180;
|
|
const scale = Math.min(maxSide / naturalWidth, maxSide / naturalHeight, 1);
|
|
const imageWidth = Math.max(minSide, Math.round(naturalWidth * scale));
|
|
const imageHeight = Math.max(minSide, Math.round(naturalHeight * scale));
|
|
const padding = 16;
|
|
const headerHeight = 34;
|
|
const gap = 12;
|
|
|
|
return {
|
|
width: imageWidth + padding * 2,
|
|
height: imageHeight + padding * 2 + headerHeight + gap,
|
|
};
|
|
}
|