refactor: extract image node utility helpers
This commit is contained in:
parent
0950e6827b
commit
2bcb975606
@ -77,6 +77,19 @@ import {
|
||||
getNormalizedMinCropSize,
|
||||
updateCropRectWithPointer,
|
||||
} from "@/components/canvas/nodes/image-node/image-node-crop";
|
||||
import {
|
||||
getImageNodeTitle,
|
||||
guessNearestAspectRatio,
|
||||
inferModelFormat,
|
||||
inferTripoImageType,
|
||||
normalizeImageIdentityUrl,
|
||||
normalizeImageTitle,
|
||||
normalizeMediaUrl,
|
||||
resolveRegeneratePromptSeed,
|
||||
resolveOriginalImageUrl,
|
||||
sanitizeFileName,
|
||||
triggerBrowserDownload,
|
||||
} from "@/components/canvas/nodes/image-node/image-node-utils";
|
||||
import {
|
||||
PromptRegenerateDialog,
|
||||
ThreeDGenerationDialog,
|
||||
@ -118,7 +131,6 @@ import {
|
||||
} from "@/lib/detail-ui-subject-analysis";
|
||||
import type { DetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter";
|
||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||
import { inferOriginalImageUrl } from "@/lib/image-url";
|
||||
import {
|
||||
DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG,
|
||||
getDefaultThreeDModelAliasId,
|
||||
@ -853,144 +865,6 @@ async function renderAndUploadCroppedImage(
|
||||
}
|
||||
}
|
||||
|
||||
function inferModelFormat(modelUrl?: string | null) {
|
||||
const value = String(modelUrl || "").toLowerCase();
|
||||
if (!value) return "glb";
|
||||
if (value.includes(".gltf")) return "gltf";
|
||||
if (value.includes(".glb")) return "glb";
|
||||
if (value.includes(".usdz")) return "usdz";
|
||||
if (value.includes(".fbx")) return "fbx";
|
||||
if (value.includes(".obj")) return "obj";
|
||||
if (value.includes(".stl")) return "stl";
|
||||
if (value.includes(".3mf")) return "3mf";
|
||||
return "glb";
|
||||
}
|
||||
|
||||
function inferTripoImageType(imageUrl?: string | null) {
|
||||
const value = String(imageUrl || "").toLowerCase();
|
||||
if (value.includes(".png")) return "png";
|
||||
if (value.includes(".webp")) return "webp";
|
||||
return "jpg";
|
||||
}
|
||||
|
||||
const normalizeMediaUrl = (url: string): string => {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
return `${parsed.origin}${parsed.pathname}`
|
||||
.replace(/\/+$/, "")
|
||||
.toLowerCase();
|
||||
} catch {
|
||||
return raw.split("#")[0].split("?")[0].replace(/\/+$/, "").toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeImageIdentityUrl = (url: string): string => {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
const canonical = inferOriginalImageUrl(raw);
|
||||
return normalizeMediaUrl(canonical || raw);
|
||||
};
|
||||
|
||||
const normalizeImageTitle = (raw?: string) => {
|
||||
const title = (raw || "").trim();
|
||||
return title || "";
|
||||
};
|
||||
|
||||
const getImageNodeTitle = (data: ImageNodeData, nodeId: string) => {
|
||||
return (
|
||||
normalizeImageTitle(data.title) ||
|
||||
normalizeImageTitle(data.prompt) ||
|
||||
`图片-${nodeId.slice(0, 6)}`
|
||||
);
|
||||
};
|
||||
|
||||
const sanitizeFileName = (name: string) => {
|
||||
const safe = name
|
||||
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, 120);
|
||||
return safe || "image";
|
||||
};
|
||||
|
||||
const triggerBrowserDownload = (
|
||||
url: string,
|
||||
filename?: string,
|
||||
options?: { openInNewTab?: boolean }
|
||||
) => {
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
if (filename) {
|
||||
link.download = filename;
|
||||
}
|
||||
if (options?.openInNewTab) {
|
||||
link.target = "_blank";
|
||||
}
|
||||
link.rel = "noopener noreferrer";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const GENERIC_CANVAS_PROMPTS = new Set([
|
||||
"对话生成",
|
||||
"图片生成",
|
||||
"AI生成",
|
||||
"智能生成",
|
||||
"自动生成",
|
||||
]);
|
||||
|
||||
const normalizePromptText = (value: unknown): string => {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
};
|
||||
|
||||
const resolveRegeneratePromptSeed = (data: ImageNodeData): string => {
|
||||
const metadataPrompt = normalizePromptText(data.metadata?.generationPrompt);
|
||||
if (metadataPrompt) return metadataPrompt;
|
||||
|
||||
const nodePrompt = normalizePromptText(data.prompt);
|
||||
if (nodePrompt && !GENERIC_CANVAS_PROMPTS.has(nodePrompt)) {
|
||||
return nodePrompt;
|
||||
}
|
||||
|
||||
return nodePrompt;
|
||||
};
|
||||
|
||||
const COMMON_ASPECT_RATIOS = [
|
||||
"1:1",
|
||||
"2:3",
|
||||
"3:2",
|
||||
"3:4",
|
||||
"4:3",
|
||||
"4:5",
|
||||
"5:4",
|
||||
"9:16",
|
||||
"16:9",
|
||||
"21:9",
|
||||
] as const;
|
||||
|
||||
function guessNearestAspectRatio(width: number, height: number): string {
|
||||
if (!width || !height) return "auto";
|
||||
const target = width / height;
|
||||
let best = "auto";
|
||||
let minDiff = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const ratio of COMMON_ASPECT_RATIOS) {
|
||||
const [w, h] = ratio.split(":").map(Number);
|
||||
const value = w / h;
|
||||
const diff = Math.abs(target - value);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
best = ratio;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在图片上绘制标记点,返回带标记的图片 base64
|
||||
*/
|
||||
@ -1802,7 +1676,7 @@ export function ImageNode({
|
||||
[data.src, displayImageWidth]
|
||||
);
|
||||
const fullscreenPreviewSrc = useMemo(() => {
|
||||
const originalUrl = inferOriginalImageUrl(data.src || "");
|
||||
const originalUrl = resolveOriginalImageUrl(data.src || "");
|
||||
return buildImageProxyUrl(originalUrl || data.src || "", {
|
||||
delivery: "proxy",
|
||||
width: 2400,
|
||||
@ -7040,7 +6914,7 @@ export function ImageNode({
|
||||
// 下载图片 - 通过 fetch 获取图片数据实现真正的下载(避免跨域问题导致跳转)
|
||||
const handleDownload = async () => {
|
||||
if (!data.src) return;
|
||||
const originalUrl = String(inferOriginalImageUrl(data.src) || data.src || "").trim();
|
||||
const originalUrl = String(resolveOriginalImageUrl(data.src) || data.src || "").trim();
|
||||
if (!originalUrl) {
|
||||
toast.error("下载失败,请重试");
|
||||
return;
|
||||
|
||||
145
components/canvas/nodes/image-node/image-node-utils.ts
Normal file
145
components/canvas/nodes/image-node/image-node-utils.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { inferOriginalImageUrl } from "@/lib/image-url";
|
||||
import type { ImageNodeData } from "@/types/canvas";
|
||||
|
||||
export function inferModelFormat(modelUrl?: string | null) {
|
||||
const value = String(modelUrl || "").toLowerCase();
|
||||
if (!value) return "glb";
|
||||
if (value.includes(".gltf")) return "gltf";
|
||||
if (value.includes(".glb")) return "glb";
|
||||
if (value.includes(".usdz")) return "usdz";
|
||||
if (value.includes(".fbx")) return "fbx";
|
||||
if (value.includes(".obj")) return "obj";
|
||||
if (value.includes(".stl")) return "stl";
|
||||
if (value.includes(".3mf")) return "3mf";
|
||||
return "glb";
|
||||
}
|
||||
|
||||
export function inferTripoImageType(imageUrl?: string | null) {
|
||||
const value = String(imageUrl || "").toLowerCase();
|
||||
if (value.includes(".png")) return "png";
|
||||
if (value.includes(".webp")) return "webp";
|
||||
return "jpg";
|
||||
}
|
||||
|
||||
export const normalizeMediaUrl = (url: string): string => {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
return `${parsed.origin}${parsed.pathname}`
|
||||
.replace(/\/+$/, "")
|
||||
.toLowerCase();
|
||||
} catch {
|
||||
return raw.split("#")[0].split("?")[0].replace(/\/+$/, "").toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeImageIdentityUrl = (url: string): string => {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
const canonical = inferOriginalImageUrl(raw);
|
||||
return normalizeMediaUrl(canonical || raw);
|
||||
};
|
||||
|
||||
export const resolveOriginalImageUrl = (url?: string | null): string => {
|
||||
const raw = String(url || "").trim();
|
||||
return String(inferOriginalImageUrl(raw) || raw).trim();
|
||||
};
|
||||
|
||||
export const normalizeImageTitle = (raw?: string) => {
|
||||
const title = (raw || "").trim();
|
||||
return title || "";
|
||||
};
|
||||
|
||||
export const getImageNodeTitle = (data: ImageNodeData, nodeId: string) => {
|
||||
return (
|
||||
normalizeImageTitle(data.title) ||
|
||||
normalizeImageTitle(data.prompt) ||
|
||||
`图片-${nodeId.slice(0, 6)}`
|
||||
);
|
||||
};
|
||||
|
||||
export const sanitizeFileName = (name: string) => {
|
||||
const safe = name
|
||||
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, 120);
|
||||
return safe || "image";
|
||||
};
|
||||
|
||||
export const triggerBrowserDownload = (
|
||||
url: string,
|
||||
filename?: string,
|
||||
options?: { openInNewTab?: boolean }
|
||||
) => {
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
if (filename) {
|
||||
link.download = filename;
|
||||
}
|
||||
if (options?.openInNewTab) {
|
||||
link.target = "_blank";
|
||||
}
|
||||
link.rel = "noopener noreferrer";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const GENERIC_CANVAS_PROMPTS = new Set([
|
||||
"对话生成",
|
||||
"图片生成",
|
||||
"AI生成",
|
||||
"智能生成",
|
||||
"自动生成",
|
||||
]);
|
||||
|
||||
export const normalizePromptText = (value: unknown): string => {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
};
|
||||
|
||||
export const resolveRegeneratePromptSeed = (data: ImageNodeData): string => {
|
||||
const metadataPrompt = normalizePromptText(data.metadata?.generationPrompt);
|
||||
if (metadataPrompt) return metadataPrompt;
|
||||
|
||||
const nodePrompt = normalizePromptText(data.prompt);
|
||||
if (nodePrompt && !GENERIC_CANVAS_PROMPTS.has(nodePrompt)) {
|
||||
return nodePrompt;
|
||||
}
|
||||
|
||||
return nodePrompt;
|
||||
};
|
||||
|
||||
const COMMON_ASPECT_RATIOS = [
|
||||
"1:1",
|
||||
"2:3",
|
||||
"3:2",
|
||||
"3:4",
|
||||
"4:3",
|
||||
"4:5",
|
||||
"5:4",
|
||||
"9:16",
|
||||
"16:9",
|
||||
"21:9",
|
||||
] as const;
|
||||
|
||||
export function guessNearestAspectRatio(width: number, height: number): string {
|
||||
if (!width || !height) return "auto";
|
||||
const target = width / height;
|
||||
let best = "auto";
|
||||
let minDiff = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const ratio of COMMON_ASPECT_RATIOS) {
|
||||
const [w, h] = ratio.split(":").map(Number);
|
||||
const value = w / h;
|
||||
const diff = Math.abs(target - value);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
best = ratio;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user