"use client"; import React, { useMemo, useState } from "react"; import { AlertCircle, Box, Copy, Download, ExternalLink, Globe, ImageIcon, Loader2, MoreVertical, Trash2, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import useSWR from "swr"; import { toast } from "sonner"; import { DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG, resolveThreeDModelDisplayName, type PublicThreeDModelAliasConfig, } from "@/lib/model3d/model-alias-config"; import { useCanvasStore } from "@/store/canvas"; import type { CanvasNode, Model3DNodeData } from "@/types/canvas"; import { cn, fetcher } from "@/lib/utils"; interface Model3DNodeProps { node: CanvasNode; isSelected: boolean; } function inferModelFileName(data: Model3DNodeData, nodeId: string) { const raw = String(data.src || "").trim(); if (!raw) return `model3d-${nodeId.slice(0, 6)}.glb`; try { const url = new URL(raw); const filename = url.pathname.split("/").filter(Boolean).pop(); if (filename) return decodeURIComponent(filename); } catch {} return raw.split("/").pop() || `model3d-${nodeId.slice(0, 6)}.glb`; } function inferModelFormat(data: Model3DNodeData) { if (data.format) return data.format.toUpperCase(); const src = String(data.src || "").toLowerCase(); if (src.endsWith(".gltf")) return "GLTF"; if (src.endsWith(".glb")) return "GLB"; if (src.endsWith(".usdz")) return "USDZ"; if (src.endsWith(".fbx")) return "FBX"; if (src.endsWith(".obj")) return "OBJ"; if (src.endsWith(".stl")) return "STL"; if (src.endsWith(".3mf")) return "3MF"; return "3D"; } function readWorldAssetPayload(data: Model3DNodeData) { const raw = data.metadata?.assetPayload; return raw && typeof raw === "object" ? (raw as Record) : null; } function readOptionalUrl(value: unknown) { return typeof value === "string" && value.trim() ? value.trim() : null; } export function Model3DNode({ node, isSelected }: Model3DNodeProps) { const data = node.data as Model3DNodeData; const [isHovered, setIsHovered] = useState(false); const deleteNode = useCanvasStore((state) => state.deleteNode); const { data: modelAliasConfigResponse } = useSWR<{ success?: boolean; data?: PublicThreeDModelAliasConfig; }>("/api/canvas/model-aliases", fetcher); const providerLabel = data.metadata?.provider === "worldlabs-3d" ? "World Labs 结果" : "Tripo3D 结果"; const publicModelAliasConfig = modelAliasConfigResponse?.data || DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG; const modelVersionLabel = useMemo( () => resolveThreeDModelDisplayName( data.modelVersion, publicModelAliasConfig.tripoModels, providerLabel, ), [data.modelVersion, providerLabel, publicModelAliasConfig.tripoModels], ); const worldAssetPayload = readWorldAssetPayload(data); const worldUrl = readOptionalUrl(worldAssetPayload?.worldUrl); const worldImageUrl = readOptionalUrl(worldAssetPayload?.panoUrl) || readOptionalUrl(worldAssetPayload?.thumbnailUrl) || readOptionalUrl(data.previewImage); const primaryOpenUrl = worldUrl || data.src || null; const primaryOpenLabel = worldUrl ? "打开 World" : "打开 3D"; const handleOpen = () => { if (!primaryOpenUrl) { toast.error("暂无可打开的地址"); return; } window.open(primaryOpenUrl, "_blank", "noopener,noreferrer"); }; const handleOpenMesh = () => { if (!data.src) { toast.error("暂无可打开的 Mesh 地址"); return; } window.open(data.src, "_blank", "noopener,noreferrer"); }; const handleOpenImage = () => { if (!worldImageUrl) { toast.error("暂无可打开的图像预览"); return; } window.open(worldImageUrl, "_blank", "noopener,noreferrer"); }; const handleDownload = () => { if (!data.src) { toast.error("暂无可下载的 3D 文件"); return; } const link = document.createElement("a"); link.href = data.src; link.download = inferModelFileName(data, node.id); link.click(); }; const handleCopy = async () => { if (!primaryOpenUrl) { toast.error("暂无可复制的地址"); return; } try { await navigator.clipboard.writeText(primaryOpenUrl); toast.success(worldUrl ? "已复制 World 链接" : "已复制 3D 链接"); } catch { toast.error("复制失败"); } }; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {data.status === "generating" ? (
{data.metadata?.provider === "worldlabs-3d" ? "3D 世界生成中..." : "3D 生成中..."}
) : data.status === "failed" ? (
{data.metadata?.provider === "worldlabs-3d" ? "3D 世界生成失败" : "3D 生成失败"}
) : (
{data.previewImage ? ( {data.prompt ) : (
)}
{inferModelFormat(data)}

{data.prompt || inferModelFileName(data, node.id)}

{data.modelVersion ? `模型: ${modelVersionLabel}` : providerLabel}

)} {isHovered && data.status === "completed" && (
{worldUrl ? ( ) : ( )} {primaryOpenLabel} {worldUrl && data.src && data.src !== worldUrl && ( 打开 Mesh )} {worldImageUrl && ( 打开图像 )} 下载 复制链接 deleteNode(node.id)}> 删除
)}
); }