refactor: white-label 3d world workspace
This commit is contained in:
parent
7eba6d2fb8
commit
6c7f02c142
@ -190,6 +190,15 @@ const WORLDLABS_MODEL_OPTIONS = [
|
||||
"marble-1.0",
|
||||
"marble-1.0-draft",
|
||||
] as const;
|
||||
const WORLD_MODEL_DISPLAY_LABELS: Record<
|
||||
(typeof WORLDLABS_MODEL_OPTIONS)[number],
|
||||
string
|
||||
> = {
|
||||
"marble-1.1": "标准质量",
|
||||
"marble-1.1-plus": "高保真",
|
||||
"marble-1.0": "兼容质量",
|
||||
"marble-1.0-draft": "快速草稿",
|
||||
};
|
||||
|
||||
const ENV_PRESET_OPTIONS: Model3DViewerSettings["envPreset"][] = [
|
||||
"city",
|
||||
@ -540,6 +549,11 @@ function worldModeToTaskMode(mode: WorldGenerationMode): TaskMode {
|
||||
}
|
||||
}
|
||||
|
||||
function getWorldModelDisplayLabel(value?: string | null) {
|
||||
const normalized = String(value || "").trim() as (typeof WORLDLABS_MODEL_OPTIONS)[number];
|
||||
return WORLD_MODEL_DISPLAY_LABELS[normalized] || "标准质量";
|
||||
}
|
||||
|
||||
export default function CanvasModel3DPage() {
|
||||
const router = useRouter();
|
||||
const routeParams = useParams();
|
||||
@ -1666,9 +1680,7 @@ export default function CanvasModel3DPage() {
|
||||
status: "completed",
|
||||
activeView: primarySplatUrl
|
||||
? "splat"
|
||||
: worldAssetPayload?.worldUrl
|
||||
? "world"
|
||||
: worldImageUrl
|
||||
: worldImageUrl
|
||||
? "image"
|
||||
: "mesh",
|
||||
firstPerson: false,
|
||||
@ -1829,15 +1841,6 @@ export default function CanvasModel3DPage() {
|
||||
}
|
||||
}, [currentTask, updateWorldShareState]);
|
||||
|
||||
const handleOpenWorldUrl = () => {
|
||||
const worldUrl = currentTask?.assetPayload?.worldUrl;
|
||||
if (!worldUrl) {
|
||||
toast.error("当前任务暂无可打开的 World 链接");
|
||||
return;
|
||||
}
|
||||
window.open(worldUrl, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
const handleOpenPanoUrl = () => {
|
||||
const imageUrl =
|
||||
currentTask?.assetPayload?.panoUrl ||
|
||||
@ -2139,8 +2142,6 @@ export default function CanvasModel3DPage() {
|
||||
|
||||
const currentWorldAsset =
|
||||
currentTask?.provider === "worldlabs-3d" ? currentTask.assetPayload ?? null : null;
|
||||
const currentWorldId =
|
||||
currentWorldAsset?.worldId || currentTask?.sourceTaskId || null;
|
||||
const currentWorldShareUrl =
|
||||
currentTask?.provider === "worldlabs-3d" && currentTask?.shareEnabled
|
||||
? buildWorldShareUrl(currentTask.shareToken)
|
||||
@ -2166,21 +2167,18 @@ export default function CanvasModel3DPage() {
|
||||
const availableWorldViewportModes = useMemo(() => {
|
||||
const modes: WorldViewportMode[] = [];
|
||||
if (currentPrimarySpzUrl) modes.push("splat");
|
||||
if (currentWorldAsset?.worldUrl) modes.push("world");
|
||||
if (currentWorldImageUrl) modes.push("imagery");
|
||||
if (currentModelUrl) modes.push("mesh");
|
||||
return modes;
|
||||
}, [currentModelUrl, currentPrimarySpzUrl, currentWorldAsset?.worldUrl, currentWorldImageUrl]);
|
||||
}, [currentModelUrl, currentPrimarySpzUrl, currentWorldImageUrl]);
|
||||
const preferredWorldViewportMode: WorldViewportMode =
|
||||
currentPrimarySpzUrl
|
||||
? "splat"
|
||||
: currentWorldAsset?.worldUrl
|
||||
? "world"
|
||||
: currentWorldImageUrl
|
||||
? "imagery"
|
||||
: currentModelUrl
|
||||
? "mesh"
|
||||
: "world";
|
||||
: "splat";
|
||||
const activeWorkspaceTitle =
|
||||
workspaceMode === "world" ? "3D 世界工作台" : "3D 工作台";
|
||||
const viewerEmptyStateTitle =
|
||||
@ -2245,15 +2243,6 @@ export default function CanvasModel3DPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* DCC Bridge (Mock) */}
|
||||
<Button
|
||||
className="hidden h-8 gap-2 text-zinc-400 hover:bg-white/5 hover:text-white sm:flex"
|
||||
variant="ghost"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
<span className="text-xs">DCC Bridge</span>
|
||||
</Button>
|
||||
|
||||
<div className="flex h-8 items-center rounded-full bg-blue-500/10 px-3 text-blue-400">
|
||||
<span className="font-medium text-xs">积分</span>
|
||||
<span className="ml-1 font-semibold text-sm">{pointsLabel}</span>
|
||||
@ -2821,7 +2810,7 @@ export default function CanvasModel3DPage() {
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-4 pt-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-zinc-400">World 模型</Label>
|
||||
<Label className="text-xs text-zinc-400">生成质量</Label>
|
||||
<Select
|
||||
onValueChange={(value) => setWorldModel(value as any)}
|
||||
value={worldModel}
|
||||
@ -2836,7 +2825,7 @@ export default function CanvasModel3DPage() {
|
||||
key={item}
|
||||
value={item}
|
||||
>
|
||||
{item}
|
||||
{getWorldModelDisplayLabel(item)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@ -2873,14 +2862,6 @@ export default function CanvasModel3DPage() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between text-xs text-zinc-400">
|
||||
<span>生成公开 World</span>
|
||||
<Switch
|
||||
checked={worldMakePublic}
|
||||
className="data-[state=checked]:bg-yellow-500"
|
||||
onCheckedChange={setWorldMakePublic}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
@ -3029,24 +3010,14 @@ export default function CanvasModel3DPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
className="w-full justify-start border-white/10 bg-transparent text-zinc-300 hover:bg-white/10"
|
||||
disabled={!currentWorldAsset?.worldUrl}
|
||||
onClick={handleOpenWorldUrl}
|
||||
variant="outline"
|
||||
>
|
||||
<Globe className="mr-2 h-4 w-4" /> 打开 World
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full justify-start border-white/10 bg-transparent text-zinc-300 hover:bg-white/10"
|
||||
disabled={!currentWorldImageUrl}
|
||||
onClick={handleOpenPanoUrl}
|
||||
variant="outline"
|
||||
>
|
||||
<ImageIcon className="mr-2 h-4 w-4" /> 打开图像
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full justify-start border-white/10 bg-transparent text-zinc-300 hover:bg-white/10"
|
||||
disabled={!currentWorldImageUrl}
|
||||
onClick={handleOpenPanoUrl}
|
||||
variant="outline"
|
||||
>
|
||||
<ImageIcon className="mr-2 h-4 w-4" /> 打开图像
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
@ -3070,14 +3041,14 @@ export default function CanvasModel3DPage() {
|
||||
|
||||
<Card className="border-white/10 bg-zinc-950/60 text-zinc-100">
|
||||
<CardHeader className="space-y-1 pb-3">
|
||||
<CardTitle className="text-sm">当前世界资产</CardTitle>
|
||||
<CardTitle className="text-sm">当前内容</CardTitle>
|
||||
<CardDescription className="text-xs text-zinc-500">
|
||||
当前支持以原生 world3d 节点导入画布,并保留 World、Splat、图像与 Mesh 入口。
|
||||
这里保留当前世界可直接使用的核心内容入口。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-xs text-zinc-400">
|
||||
<div className="space-y-1">
|
||||
<div className="text-[11px] text-zinc-500">描述</div>
|
||||
<div className="text-[11px] text-zinc-500">场景描述</div>
|
||||
<div className="line-clamp-3 rounded-md bg-white/5 px-3 py-2 text-zinc-200">
|
||||
{currentWorldAsset?.caption ||
|
||||
currentTask?.prompt ||
|
||||
@ -3086,35 +3057,21 @@ export default function CanvasModel3DPage() {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="rounded-md bg-white/5 px-3 py-2">
|
||||
<div className="text-[11px] text-zinc-500">World ID</div>
|
||||
<div className="text-[11px] text-zinc-500">生成质量</div>
|
||||
<div className="truncate text-zinc-200">
|
||||
{currentWorldId || "暂无"}
|
||||
{getWorldModelDisplayLabel(
|
||||
currentWorldAsset?.model || currentTask?.modelVersion
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-white/5 px-3 py-2">
|
||||
<div className="text-[11px] text-zinc-500">模型</div>
|
||||
<div className="text-[11px] text-zinc-500">共享状态</div>
|
||||
<div className="truncate text-zinc-200">
|
||||
{currentWorldAsset?.model ||
|
||||
currentTask?.modelVersion ||
|
||||
"默认"}
|
||||
{currentWorldShareUrl ? "已开启" : "未开启"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<Link2 className="h-3.5 w-3.5" />
|
||||
World 页面
|
||||
</span>
|
||||
<Badge
|
||||
className="text-[10px]"
|
||||
variant={
|
||||
currentWorldAsset?.worldUrl ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{currentWorldAsset?.worldUrl ? "可用" : "暂无"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<ImageIcon className="h-3.5 w-3.5" />
|
||||
@ -3124,7 +3081,7 @@ export default function CanvasModel3DPage() {
|
||||
className="text-[10px]"
|
||||
variant={currentWorldImageUrl ? "default" : "secondary"}
|
||||
>
|
||||
{currentWorldImageUrl ? "可用" : "暂无"}
|
||||
{currentWorldImageUrl ? "可查看" : "暂无"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2">
|
||||
@ -3141,23 +3098,17 @@ export default function CanvasModel3DPage() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<Link2 className="h-3.5 w-3.5" />
|
||||
分享页
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
World 视图
|
||||
</span>
|
||||
<Badge
|
||||
className="text-[10px]"
|
||||
variant={currentWorldShareUrl ? "default" : "secondary"}
|
||||
variant={currentPrimarySpzUrl ? "default" : "secondary"}
|
||||
>
|
||||
{currentWorldShareUrl ? "已开启" : "未开启"}
|
||||
{currentPrimarySpzUrl ? "可查看" : "暂无"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{currentWorldShareUrl ? (
|
||||
<div className="rounded-md bg-white/5 px-3 py-2 text-zinc-200">
|
||||
<div className="text-[11px] text-zinc-500">分享链接</div>
|
||||
<div className="mt-1 truncate">{currentWorldShareUrl}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@ -3244,21 +3195,21 @@ export default function CanvasModel3DPage() {
|
||||
<div className="font-bold text-lg text-white">
|
||||
{currentSpzEntries.length}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">SPZ 资产</div>
|
||||
<div className="text-xs text-zinc-500">World 资产</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="border-white/10 bg-zinc-950/60 text-zinc-100">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm">世界摘要</CardTitle>
|
||||
<CardTitle className="text-sm">内容概览</CardTitle>
|
||||
<CardDescription className="text-xs text-zinc-500">
|
||||
这里展示 World Labs 返回的主要资产信息,便于在当前工作台里做导入和跳转。
|
||||
这里展示当前世界的核心内容状态,方便在工作台里继续使用。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-xs text-zinc-400">
|
||||
<div className="rounded-md bg-white/5 px-3 py-2">
|
||||
<div className="text-[11px] text-zinc-500">Caption</div>
|
||||
<div className="text-[11px] text-zinc-500">场景描述</div>
|
||||
<div className="mt-1 whitespace-pre-wrap text-zinc-200">
|
||||
{currentWorldAsset?.caption ||
|
||||
currentTask?.prompt ||
|
||||
@ -3267,15 +3218,11 @@ export default function CanvasModel3DPage() {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="rounded-md bg-white/5 px-3 py-2">
|
||||
<div className="text-[11px] text-zinc-500">World ID</div>
|
||||
<div className="text-[11px] text-zinc-500">生成质量</div>
|
||||
<div className="truncate text-zinc-200">
|
||||
{currentWorldId || "暂无"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-white/5 px-3 py-2">
|
||||
<div className="text-[11px] text-zinc-500">公开链接</div>
|
||||
<div className="truncate text-zinc-200">
|
||||
{currentWorldAsset?.worldUrl ? "已生成" : "暂无"}
|
||||
{getWorldModelDisplayLabel(
|
||||
currentWorldAsset?.model || currentTask?.modelVersion
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-white/5 px-3 py-2">
|
||||
@ -3291,48 +3238,17 @@ export default function CanvasModel3DPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-white/5 px-3 py-2">
|
||||
<div className="text-[11px] text-zinc-500">分享页</div>
|
||||
<div className="text-[11px] text-zinc-500">共享状态</div>
|
||||
<div className="truncate text-zinc-200">
|
||||
{currentWorldShareUrl ? "已开启" : "未开启"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentWorldShareUrl ? (
|
||||
<div className="rounded-md bg-white/5 px-3 py-2 text-zinc-200">
|
||||
<div className="text-[11px] text-zinc-500">分享链接</div>
|
||||
<div className="mt-1 truncate">{currentWorldShareUrl}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-xs text-zinc-500">
|
||||
SPZ 列表
|
||||
</h4>
|
||||
{currentSpzEntries.length ? (
|
||||
currentSpzEntries.map(([label, url]) => (
|
||||
<div
|
||||
className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2"
|
||||
key={label}
|
||||
>
|
||||
<span className="truncate text-zinc-200">
|
||||
{label}
|
||||
</span>
|
||||
<Button
|
||||
className="h-7 border-white/10 bg-transparent text-zinc-300 hover:bg-white/10"
|
||||
onClick={() =>
|
||||
window.open(url, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
打开
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-md bg-white/5 px-3 py-3 text-zinc-500">
|
||||
当前结果没有返回 SPZ 资产。
|
||||
<div className="rounded-md bg-white/5 px-3 py-2">
|
||||
<div className="text-[11px] text-zinc-500">World 资产</div>
|
||||
<div className="truncate text-zinc-200">
|
||||
{currentSpzEntries.length ? `${currentSpzEntries.length} 项` : "暂无"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -3349,16 +3265,10 @@ export default function CanvasModel3DPage() {
|
||||
{[
|
||||
{
|
||||
id: "splat" as WorldViewportMode,
|
||||
label: "Splat",
|
||||
label: "World",
|
||||
icon: Sparkles,
|
||||
disabled: !currentPrimarySpzUrl,
|
||||
},
|
||||
{
|
||||
id: "world" as WorldViewportMode,
|
||||
label: "World",
|
||||
icon: Globe,
|
||||
disabled: !currentWorldAsset?.worldUrl,
|
||||
},
|
||||
{
|
||||
id: "imagery" as WorldViewportMode,
|
||||
label: "图像",
|
||||
|
||||
@ -7,8 +7,6 @@ import {
|
||||
Copy,
|
||||
Crosshair,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Globe,
|
||||
ImageIcon,
|
||||
Loader2,
|
||||
MoreVertical,
|
||||
@ -41,16 +39,46 @@ const VIEW_META: Array<{
|
||||
label: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
}> = [
|
||||
{ id: "splat", label: "S", icon: Sparkles },
|
||||
{ id: "world", label: "W", icon: Globe },
|
||||
{ id: "splat", label: "W", icon: Sparkles },
|
||||
{ id: "image", label: "I", icon: ImageIcon },
|
||||
{ id: "mesh", label: "M", icon: Box },
|
||||
];
|
||||
|
||||
function getWorldNodeModelLabel(value?: string | null) {
|
||||
const normalized = String(value || "").trim();
|
||||
switch (normalized) {
|
||||
case "marble-1.1":
|
||||
return "标准质量";
|
||||
case "marble-1.1-plus":
|
||||
return "高保真";
|
||||
case "marble-1.0":
|
||||
return "兼容质量";
|
||||
case "marble-1.0-draft":
|
||||
return "快速草稿";
|
||||
case "":
|
||||
return "原生世界节点";
|
||||
default:
|
||||
return "自定义质量";
|
||||
}
|
||||
}
|
||||
|
||||
function getWorldNodeViewLabel(view: World3DViewMode) {
|
||||
switch (view) {
|
||||
case "splat":
|
||||
return "WORLD";
|
||||
case "image":
|
||||
return "IMAGE";
|
||||
case "mesh":
|
||||
return "MESH";
|
||||
case "world":
|
||||
default:
|
||||
return "ONLINE";
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAvailableViews(data: World3DNodeData) {
|
||||
const views: World3DViewMode[] = [];
|
||||
if (data.splatUrl) views.push("splat");
|
||||
if (data.worldUrl) views.push("world");
|
||||
if (data.imageUrl || data.previewImage) views.push("image");
|
||||
if (data.meshUrl) views.push("mesh");
|
||||
return views;
|
||||
@ -89,14 +117,14 @@ function inferDownloadTarget(data: World3DNodeData, activeView: World3DViewMode)
|
||||
function resolvePrimaryUrl(data: World3DNodeData, activeView: World3DViewMode) {
|
||||
switch (activeView) {
|
||||
case "world":
|
||||
return data.worldUrl || data.splatUrl || data.meshUrl || data.imageUrl || null;
|
||||
return data.splatUrl || data.meshUrl || data.imageUrl || null;
|
||||
case "image":
|
||||
return data.imageUrl || data.previewImage || data.worldUrl || data.meshUrl || null;
|
||||
return data.imageUrl || data.previewImage || data.meshUrl || data.splatUrl || null;
|
||||
case "mesh":
|
||||
return data.meshUrl || data.splatUrl || data.worldUrl || null;
|
||||
return data.meshUrl || data.splatUrl || data.imageUrl || null;
|
||||
case "splat":
|
||||
default:
|
||||
return data.splatUrl || data.worldUrl || data.meshUrl || data.imageUrl || null;
|
||||
return data.splatUrl || data.imageUrl || data.meshUrl || null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,14 +199,6 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
|
||||
toast.success("已添加世界热点");
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
if (!primaryUrl) {
|
||||
toast.error("暂无可打开的世界资产");
|
||||
return;
|
||||
}
|
||||
window.open(primaryUrl, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!primaryUrl) {
|
||||
toast.error("暂无可复制的世界地址");
|
||||
@ -244,11 +264,11 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
|
||||
<div className="flex items-end justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-white">
|
||||
{data.prompt || "World Labs 世界"}
|
||||
{data.prompt || "3D 世界"}
|
||||
</p>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<Badge className="border-white/10 bg-black/55 text-zinc-200" variant="outline">
|
||||
World Labs
|
||||
3D 世界
|
||||
</Badge>
|
||||
{hotspotCount ? (
|
||||
<Badge className="border-amber-500/30 bg-amber-500/10 text-amber-200" variant="outline">
|
||||
@ -256,12 +276,12 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
|
||||
</Badge>
|
||||
) : null}
|
||||
<Badge className="border-white/10 bg-black/55 text-zinc-300" variant="outline">
|
||||
{activeView.toUpperCase()}
|
||||
{getWorldNodeViewLabel(activeView)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] text-zinc-400">
|
||||
{data.modelVersion || "原生世界节点"}
|
||||
{getWorldNodeModelLabel(data.modelVersion)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -310,15 +330,6 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
|
||||
|
||||
{isHovered && data.status === "completed" && (
|
||||
<div className="absolute right-3 top-3 z-20 flex items-center gap-1">
|
||||
<Button
|
||||
className="h-7 w-7 rounded-full"
|
||||
onClick={handleOpen}
|
||||
size="icon"
|
||||
title="打开世界"
|
||||
variant="secondary"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="h-7 w-7 rounded-full" size="icon" variant="secondary">
|
||||
@ -326,18 +337,6 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{data.worldUrl ? (
|
||||
<DropdownMenuItem onClick={() => window.open(data.worldUrl, "_blank", "noopener,noreferrer")}>
|
||||
<Globe className="mr-2 h-4 w-4" />
|
||||
打开 World
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{data.splatUrl ? (
|
||||
<DropdownMenuItem onClick={() => window.open(data.splatUrl, "_blank", "noopener,noreferrer")}>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
打开 Splat
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{data.meshUrl ? (
|
||||
<DropdownMenuItem onClick={() => window.open(data.meshUrl, "_blank", "noopener,noreferrer")}>
|
||||
<Box className="mr-2 h-4 w-4" />
|
||||
@ -370,4 +369,3 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -525,11 +525,11 @@ function WorldSplatLoadingOverlay({
|
||||
<div className="rounded-2xl border border-white/10 bg-black/45 px-3 py-2 shadow-[0_20px_60px_rgba(0,0,0,0.38)] backdrop-blur-xl">
|
||||
<div className="flex items-center gap-2 text-[11px] text-zinc-100">
|
||||
<Sparkles className="h-3.5 w-3.5 text-cyan-200" />
|
||||
<span>{isRevealing ? "真实世界颗粒正在聚合" : "正在载入真实 Gaussian Splats"}</span>
|
||||
<span>{isRevealing ? "世界数据正在聚合" : "正在载入世界数据"}</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-[10px] text-zinc-400">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span>{isRevealing ? "真实 splat 正在从颗粒态恢复成世界" : "SPZ 数据准备中"}</span>
|
||||
<span>{isRevealing ? "当前世界正在从颗粒态恢复细节" : "空间数据准备中"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -990,10 +990,10 @@ function WorldSplatViewer({
|
||||
if (!splatUrl) {
|
||||
return (
|
||||
<WorldEmptyState
|
||||
description="当前结果没有返回可用的 SPZ 世界资产。"
|
||||
description="当前结果没有返回可用的世界数据。"
|
||||
icon={Sparkles}
|
||||
previewImageUrl={previewImageUrl}
|
||||
title="准备加载 Splat 世界"
|
||||
title="准备加载 World 视图"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1001,10 +1001,10 @@ function WorldSplatViewer({
|
||||
if (loadState === "error") {
|
||||
return (
|
||||
<WorldEmptyState
|
||||
description="SPZ 资源暂时无法加载,可以切换到 World、图像或 Mesh 视图继续查看。"
|
||||
description="当前世界数据暂时无法加载,可以切换到图像或 Mesh 视图继续查看。"
|
||||
icon={AlertCircle}
|
||||
previewImageUrl={previewImageUrl}
|
||||
title="Splat 视图加载失败"
|
||||
title="World 视图加载失败"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1085,11 +1085,11 @@ export function World3DViewer({
|
||||
className="h-full w-full border-0"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
src={worldUrl}
|
||||
title="World Labs 世界预览"
|
||||
title="3D 世界在线预览"
|
||||
/>
|
||||
) : (
|
||||
<WorldEmptyState
|
||||
description="当前结果还没有可嵌入的 World 页面链接,可以先切换到 Splat、图像或 Mesh 视图。"
|
||||
description="当前结果还没有可嵌入的在线预览入口,可以先切换到 World、图像或 Mesh 视图。"
|
||||
icon={Globe}
|
||||
previewImageUrl={previewImageUrl}
|
||||
title={emptyStateTitle}
|
||||
@ -1115,7 +1115,7 @@ export function World3DViewer({
|
||||
/>
|
||||
) : (
|
||||
<WorldEmptyState
|
||||
description="当前结果还没有可用的图像资产,可以先切换到 Splat 或 Mesh 视图。"
|
||||
description="当前结果还没有可用的图像资产,可以先切换到 World 或 Mesh 视图。"
|
||||
icon={ImageIcon}
|
||||
previewImageUrl={previewImageUrl}
|
||||
title={emptyStateTitle}
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import {
|
||||
Box,
|
||||
Download,
|
||||
Globe,
|
||||
ImageIcon,
|
||||
Link2,
|
||||
Sparkles,
|
||||
@ -20,7 +19,6 @@ type SharedWorldPageProps = {
|
||||
title: string;
|
||||
prompt?: string | null;
|
||||
caption?: string | null;
|
||||
model?: string | null;
|
||||
worldUrl?: string | null;
|
||||
imageUrl?: string | null;
|
||||
meshUrl?: string | null;
|
||||
@ -35,8 +33,7 @@ const VIEW_OPTIONS: Array<{
|
||||
label: string;
|
||||
icon: typeof Sparkles;
|
||||
}> = [
|
||||
{ id: "splat", label: "Splat", icon: Sparkles },
|
||||
{ id: "world", label: "World", icon: Globe },
|
||||
{ id: "splat", label: "World", icon: Sparkles },
|
||||
{ id: "image", label: "图像", icon: ImageIcon },
|
||||
{ id: "mesh", label: "Mesh", icon: Box },
|
||||
];
|
||||
@ -49,7 +46,6 @@ function resolveAvailableViews(params: {
|
||||
}) {
|
||||
const views: World3DViewMode[] = [];
|
||||
if (params.splatUrl) views.push("splat");
|
||||
if (params.worldUrl) views.push("world");
|
||||
if (params.imageUrl) views.push("image");
|
||||
if (params.meshUrl) views.push("mesh");
|
||||
return views;
|
||||
@ -57,7 +53,6 @@ function resolveAvailableViews(params: {
|
||||
|
||||
function inferInitialView(views: World3DViewMode[]) {
|
||||
if (views.includes("splat")) return "splat";
|
||||
if (views.includes("world")) return "world";
|
||||
if (views.includes("image")) return "image";
|
||||
if (views.includes("mesh")) return "mesh";
|
||||
return "image";
|
||||
@ -67,7 +62,6 @@ export function WorldSharePage({
|
||||
title,
|
||||
prompt,
|
||||
caption,
|
||||
model,
|
||||
worldUrl,
|
||||
imageUrl,
|
||||
meshUrl,
|
||||
@ -116,14 +110,9 @@ export function WorldSharePage({
|
||||
<Badge className="border-cyan-500/30 bg-cyan-500/10 text-cyan-200" variant="outline">
|
||||
3D 世界分享
|
||||
</Badge>
|
||||
{model ? (
|
||||
<Badge className="border-white/10 bg-white/5 text-zinc-200" variant="outline">
|
||||
{model}
|
||||
</Badge>
|
||||
) : null}
|
||||
{availableViews.map((view) => (
|
||||
<Badge className="border-white/10 bg-white/5 text-zinc-300" key={view} variant="outline">
|
||||
{view.toUpperCase()}
|
||||
{VIEW_OPTIONS.find((item) => item.id === view)?.label || view.toUpperCase()}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@ -225,22 +214,17 @@ export function WorldSharePage({
|
||||
<div className="rounded-3xl border border-white/10 bg-black/30 p-4 shadow-2xl backdrop-blur">
|
||||
<h2 className="text-sm font-medium text-white">查看说明</h2>
|
||||
<div className="mt-3 space-y-2 text-sm leading-6 text-zinc-300">
|
||||
<p>`Splat` 视图最接近原生 3D 世界体验,支持方向键或 `WASD` 六向移动。</p>
|
||||
<p>`World` 视图会直接打开 World Labs 提供的公开页面。</p>
|
||||
<p>`World` 视图最接近原生 3D 世界体验,支持方向键或 `WASD` 六向移动。</p>
|
||||
<p>当前页面是只读分享页,不会修改原项目或原画布内容。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-white/10 bg-black/30 p-4 shadow-2xl backdrop-blur">
|
||||
<h2 className="text-sm font-medium text-white">资源状态</h2>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 text-xs text-zinc-300">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 px-3 py-2">
|
||||
<div className="text-zinc-500">Splat</div>
|
||||
<div className="mt-1">{splatUrl ? "可查看" : "暂无"}</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-1 gap-2 text-xs text-zinc-300">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 px-3 py-2">
|
||||
<div className="text-zinc-500">World</div>
|
||||
<div className="mt-1">{worldUrl ? "可打开" : "暂无"}</div>
|
||||
<div className="mt-1">{splatUrl ? "可查看" : "暂无"}</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 px-3 py-2">
|
||||
<div className="text-zinc-500">图像</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user