diff --git a/app/canvas/[id]/model3d/page.tsx b/app/canvas/[id]/model3d/page.tsx index 87d2fcaec..8d3d2b791 100644 --- a/app/canvas/[id]/model3d/page.tsx +++ b/app/canvas/[id]/model3d/page.tsx @@ -42,7 +42,7 @@ import { WandSparkles, X, } from "lucide-react"; -import { useParams, useRouter } from "next/navigation"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useSession } from "next-auth/react"; import { type MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; @@ -339,6 +339,36 @@ function resolveTaskModelUrl(task?: TaskRecord | null): string | null { }); } +function getTaskImportContext(task?: TaskRecord | null) { + const isWorldTask = task?.provider === "worldlabs-3d"; + const modelUrl = resolveTaskModelUrl(task); + const worldAssetPayload = isWorldTask ? task?.assetPayload ?? null : null; + const primarySplatUrl = + isWorldTask && worldAssetPayload + ? pickBestArchivedWorldLabsSplatUrl(worldAssetPayload.spzUrls) + : null; + const worldImageUrl = + worldAssetPayload?.panoUrl || + worldAssetPayload?.thumbnailUrl || + task?.previewImageUrl || + null; + const canImportWorld = Boolean( + modelUrl || + worldAssetPayload?.worldUrl || + worldImageUrl || + primarySplatUrl + ); + + return { + canImport: Boolean(task?.status === "success") && (isWorldTask ? canImportWorld : Boolean(modelUrl)), + isWorldTask, + modelUrl, + primarySplatUrl, + worldAssetPayload, + worldImageUrl, + }; +} + function createMultiViewSlots(count = 4): MultiViewSlot[] { return Array.from({ length: count }, (_, index) => ({ id: `slot_${index + 1}`, @@ -642,15 +672,28 @@ function CostPill({ export default function CanvasModel3DPage() { const router = useRouter(); + const searchParams = useSearchParams(); const routeParams = useParams(); const id = String(routeParams?.id || ""); const legacyHistoryStorageKey = useMemo(() => makeStorageKey(id), [id]); + const returnTo = useMemo(() => { + const raw = searchParams.get("returnTo")?.trim(); + return raw || `/canvas/${id}`; + }, [id, searchParams]); + const shouldReturnToVideoCanvas = useMemo( + () => + searchParams.get("source") === "video-canvas" || + /(?:\?|&)mode=video(?:&|$)|(?:\?|&)workspaceType=video(?:&|$)/.test(returnTo), + [returnTo, searchParams] + ); const { data: session } = useSession(); const user = session?.user; const viewport = useCanvasStore(canvasSelectors.viewport); - const addNode = useCanvasStore((state) => state.addNode); - const getCanvasState = useCanvasStore((state) => state.getCanvasState); + const addNode = useCanvasStore((state) => state.addNode); + const addNodesBulk = useCanvasStore((state) => state.addNodesBulk); + const selectNodes = useCanvasStore((state) => state.selectNodes); + const getCanvasState = useCanvasStore((state) => state.getCanvasState); const [workspaceMode, setWorkspaceMode] = useState("model"); const [mode, setMode] = useState("text"); @@ -724,11 +767,12 @@ export default function CanvasModel3DPage() { const [viewerResetToken, setViewerResetToken] = useState(0); const [taskHistory, setTaskHistory] = useState([]); - const [currentTask, setCurrentTask] = useState(null); - const [selectedWorldShareTaskIds, setSelectedWorldShareTaskIds] = useState([]); - const [autoDownloadTaskId, setAutoDownloadTaskId] = useState( - null - ); + const [currentTask, setCurrentTask] = useState(null); + const [selectedWorldShareTaskIds, setSelectedWorldShareTaskIds] = useState([]); + //多选"添加到画布":独立于分享多选,让用户从历史列表中挑选多个世界/模型一次性上画布 + const [selectedAddTaskIds, setSelectedAddTaskIds] = useState>(new Set()); + const [autoDownloadTaskId, setAutoDownloadTaskId] = useState( null + ); const [advancedOpen, setAdvancedOpen] = useState(false); @@ -1131,7 +1175,9 @@ export default function CanvasModel3DPage() { const pending = normalized.find( (item) => !FINAL_TASK_STATUS.has(item.status) ); - setCurrentTask(pending || normalized[0] || null); + const initialTask = pending || normalized[0] || null; + setCurrentTask(initialTask); + setWorkspaceMode(inferWorkspaceModeFromTask(initialTask)); } catch (error) { console.error("[Model3DPage] 加载 3D 资产库失败:", error); const migrated = await migrateLegacyHistory(); @@ -1261,21 +1307,31 @@ export default function CanvasModel3DPage() { return next; }, [isolatedLayerId, layerVisibility, viewerLayers]); - const visibleTaskHistory = useMemo( - () => taskHistory.filter((task) => taskMatchesWorkspace(task, workspaceMode)), - [taskHistory, workspaceMode] - ); - const shareableWorldTasks = useMemo( - () => - visibleTaskHistory.filter( - (task) => task.provider === "worldlabs-3d" && task.status === "success" - ), - [visibleTaskHistory] - ); - const selectedWorldShareTasks = useMemo(() => { - const selectedIds = new Set(selectedWorldShareTaskIds); - return shareableWorldTasks.filter((task) => selectedIds.has(task.taskId)); - }, [selectedWorldShareTaskIds, shareableWorldTasks]); + const visibleTaskHistory = useMemo( + () => taskHistory.filter((task) => taskMatchesWorkspace(task, workspaceMode)), + [taskHistory, workspaceMode] + ); + const shareableWorldTasks = useMemo( + () => + visibleTaskHistory.filter( + (task) => task.provider === "worldlabs-3d" && task.status === "success" + ), + [visibleTaskHistory] + ); + const selectedWorldShareTasks = useMemo(() => { + const selectedIds = new Set(selectedWorldShareTaskIds); + return shareableWorldTasks.filter((task) => selectedIds.has(task.taskId)); + }, [selectedWorldShareTaskIds, shareableWorldTasks]); + + // 可添加到画布的历史记录(世界 + 模型,仅成功状态) + const addableTasks = useMemo( + () => visibleTaskHistory.filter((task) => task.status === "success"), + [visibleTaskHistory] + ); + const selectedAddTasks = useMemo( + () => addableTasks.filter((task) => selectedAddTaskIds.has(task.taskId)), + [addableTasks, selectedAddTaskIds] + ); useEffect(() => { const shareableIds = new Set(shareableWorldTasks.map((task) => task.taskId)); @@ -1991,31 +2047,40 @@ export default function CanvasModel3DPage() { } }; - const handleImportToCanvas = async () => { - if (!currentTask || currentTask.status !== "success") { + const buildReturnUrl = useCallback( + (taskId?: string) => { + if (typeof window === "undefined") return returnTo; + const url = new URL(returnTo, window.location.origin); + if (taskId) { + url.searchParams.set("directorImport", "1"); + url.searchParams.set("directorTaskId", taskId); + } + return `${url.pathname}${url.search}`; + }, + [returnTo] + ); + + const handleImportToCanvas = async (task = currentTask) => { + if (!task || task.status !== "success") { toast.error("暂无可添加的 3D 资产"); return; } - const isWorldTask = currentTask.provider === "worldlabs-3d"; - const worldAssetPayload = currentTask.assetPayload ?? null; - const primarySplatUrl = isWorldTask - ? pickBestArchivedWorldLabsSplatUrl(worldAssetPayload?.spzUrls) - : null; - const worldImageUrl = - worldAssetPayload?.panoUrl || - worldAssetPayload?.thumbnailUrl || - currentTask.previewImageUrl || - null; + if (shouldReturnToVideoCanvas) { + router.push(buildReturnUrl(task.taskId)); + return; + } - const canImportWorld = Boolean( - currentModelUrl || - worldAssetPayload?.worldUrl || - worldImageUrl || - primarySplatUrl - ); + const { + canImport, + isWorldTask, + modelUrl, + primarySplatUrl, + worldAssetPayload, + worldImageUrl, + } = getTaskImportContext(task); - if ((!isWorldTask && !currentModelUrl) || (isWorldTask && !canImportWorld)) { + if (!canImport) { toast.error(isWorldTask ? "暂无可添加的 3D 世界" : "暂无可添加的 3D 模型"); return; } @@ -2041,14 +2106,14 @@ export default function CanvasModel3DPage() { data: { worldUrl: worldAssetPayload?.worldUrl || undefined, imageUrl: worldImageUrl || undefined, - meshUrl: currentModelUrl || worldAssetPayload?.meshUrl || undefined, + meshUrl: modelUrl || worldAssetPayload?.meshUrl || undefined, splatUrl: primarySplatUrl || undefined, previewImage: - currentTask.previewImageUrl || + task.previewImageUrl || worldAssetPayload?.thumbnailUrl || undefined, - prompt: currentTask.prompt, - modelVersion: currentTask.modelVersion, + prompt: task.prompt, + modelVersion: task.modelVersion, status: "completed", activeView: primarySplatUrl ? "splat" @@ -2059,15 +2124,15 @@ export default function CanvasModel3DPage() { hotspots: [], semanticsMetadata: worldAssetPayload?.semanticsMetadata || undefined, metadata: { - taskId: currentTask.taskId, - provider: currentTask.provider, - mode: currentTask.mode, - sourceTaskId: currentTask.sourceTaskId, - progress: currentTask.progress, + taskId: task.taskId, + provider: task.provider, + mode: task.mode, + sourceTaskId: task.sourceTaskId, + progress: task.progress, triangles: viewerStats.triangles, vertices: viewerStats.vertices, meshCount: viewerStats.meshCount, - assetPayload: currentTask.assetPayload ?? null, + assetPayload: task.assetPayload ?? null, }, }, connections: [], @@ -2077,22 +2142,22 @@ export default function CanvasModel3DPage() { position: { x, y }, size: { width: targetWidth, height: targetHeight }, data: { - src: currentModelUrl!, - previewImage: currentTask.previewImageUrl || undefined, - prompt: currentTask.prompt, - modelVersion: currentTask.modelVersion, - format: extractModelFormat(currentModelUrl!), + src: modelUrl!, + previewImage: task.previewImageUrl || undefined, + prompt: task.prompt, + modelVersion: task.modelVersion, + format: extractModelFormat(modelUrl!), status: "completed", metadata: { - taskId: currentTask.taskId, - provider: currentTask.provider, - mode: currentTask.mode, - sourceTaskId: currentTask.sourceTaskId, - progress: currentTask.progress, + taskId: task.taskId, + provider: task.provider, + mode: task.mode, + sourceTaskId: task.sourceTaskId, + progress: task.progress, triangles: viewerStats.triangles, vertices: viewerStats.vertices, meshCount: viewerStats.meshCount, - assetPayload: currentTask.assetPayload ?? null, + assetPayload: task.assetPayload ?? null, }, }, connections: [], @@ -2531,6 +2596,7 @@ export default function CanvasModel3DPage() { const handleSelectTask = (task: TaskRecord) => { setWorkspaceMode(inferWorkspaceModeFromTask(task)); setCurrentTask(task); + setSelectedWorldShareTaskIds([]); if (task.status === "success" && task.modelUrl) { setViewerLayers([]); setLayerVisibility({}); @@ -2615,17 +2681,7 @@ export default function CanvasModel3DPage() { currentWorldAsset?.thumbnailUrl || (workspaceMode === "world" ? currentPreviewImage : null) || null; - const importDisabled = - !currentTask || - currentTask.status !== "success" || - (currentTask.provider === "worldlabs-3d" - ? !Boolean( - currentModelUrl || - currentWorldAsset?.worldUrl || - currentWorldImageUrl || - currentPrimarySpzUrl - ) - : !currentModelUrl); + const importDisabled = !getTaskImportContext(currentTask).canImport; const availableWorldViewportModes = useMemo(() => { const modes: WorldViewportMode[] = []; if (currentPrimarySpzUrl) modes.push("splat"); @@ -2746,7 +2802,7 @@ export default function CanvasModel3DPage() {