diff --git a/components/canvas/world3d/World3DViewer.tsx b/components/canvas/world3d/World3DViewer.tsx index c6e18307f..c4051ebf7 100644 --- a/components/canvas/world3d/World3DViewer.tsx +++ b/components/canvas/world3d/World3DViewer.tsx @@ -185,7 +185,7 @@ const SPARK_MINIMAL_FRAME_LOD_RENDER_SCALE = 1.6; const SPARK_MIN_SORT_INTERVAL_MS = 24; const SPARK_MINIMAL_FRAME_SORT_INTERVAL_MS = 40; const SPARK_SPLAT_FETCH_TIMEOUT_MS = 18_000; -const SPARK_SPLAT_LOAD_TIMEOUT_MS = 18_000; +const SPARK_SPLAT_LOAD_TIMEOUT_MS = 8_000; const KEY_TO_MOVEMENT: Partial> = { KeyW: "forward", @@ -1718,6 +1718,9 @@ function WorldSplatCaptureBridge({ function WorldSplatViewer({ splatUrl, splatUrls, + fallbackMeshUrl, + fallbackMeshSettings, + fallbackMeshLayerVisibility, worldUrl, previewImageUrl, firstPerson, @@ -1725,6 +1728,8 @@ function WorldSplatViewer({ interactionMode, onPick, onHotspotSelect, + onFallbackMeshLayersChange, + onFallbackMeshStatsChange, semanticsMetadata, resetViewToken, minimalFrame = false, @@ -1734,6 +1739,9 @@ function WorldSplatViewer({ }: { splatUrl?: string | null; splatUrls?: string[] | null; + fallbackMeshUrl?: string | null; + fallbackMeshSettings?: Model3DViewerSettings; + fallbackMeshLayerVisibility?: Record; worldUrl?: string | null; previewImageUrl?: string | null; minimalFrame?: boolean; @@ -1742,6 +1750,8 @@ function WorldSplatViewer({ interactionMode?: "navigate" | "hotspot"; onPick?: (pick: World3DPickResult) => void; onHotspotSelect?: (hotspotId: string) => void; + onFallbackMeshLayersChange?: (layers: Model3DLayer[]) => void; + onFallbackMeshStatsChange?: (stats: Model3DStats) => void; semanticsMetadata?: Record | null; resetViewToken?: number; onCaptureReady?: (capture: (() => Promise) | null) => void; @@ -1758,6 +1768,7 @@ function WorldSplatViewer({ const [prefetchedSplat, setPrefetchedSplat] = useState( null ); + const [useMeshFallback, setUseMeshFallback] = useState(false); const resolvedSplatUrls = useMemo(() => { const rawUrls = [ splatUrl, @@ -1775,13 +1786,16 @@ function WorldSplatViewer({ }, [splatUrl, splatUrls]); const resolvedSplatSignature = resolvedSplatUrls.join("|"); const resolvedSplatUrl = resolvedSplatUrls[activeSplatIndex] || ""; + const resolvedFallbackMeshUrl = String(fallbackMeshUrl || "").trim() || null; + const hasMeshFallback = Boolean(resolvedFallbackMeshUrl); const loadStartedAtRef = useRef(0); const movementStateRef = useRef(createMovementState()); useEffect(() => { setActiveSplatIndex(0); setFailedSplatLoads([]); - }, [resolvedSplatSignature]); + setUseMeshFallback(false); + }, [resolvedFallbackMeshUrl, resolvedSplatSignature]); useEffect(() => { if (!resolvedSplatUrl) return; @@ -1834,6 +1848,9 @@ function WorldSplatViewer({ const failedUrl = resolvedSplatUrl; const nextIndex = activeSplatIndex + 1; const nextUrl = resolvedSplatUrls[nextIndex] || ""; + const shouldUseMeshFallback = + hasMeshFallback && + (message.includes("SPZ 加载超时") || !nextUrl); if (failedUrl) { setFailedSplatLoads((items) => [ @@ -1842,6 +1859,20 @@ function WorldSplatViewer({ ]); } + if (shouldUseMeshFallback) { + console.warn("[WorldSplatViewer] Switching to mesh fallback", { + error: message, + failedUrl, + attemptedUrls: resolvedSplatUrls.slice(0, nextIndex), + fallbackMeshUrl: resolvedFallbackMeshUrl, + }); + setPrefetchedSplat(null); + setLoadProgress(null); + setOverlayPhase("hidden"); + setUseMeshFallback(true); + return; + } + if (nextUrl) { console.warn("[WorldSplatViewer] Falling back to next SPZ asset", { error: message, @@ -1864,11 +1895,17 @@ function WorldSplatViewer({ setPrefetchedSplat(null); setLoadState("error"); }, - [activeSplatIndex, resolvedSplatUrl, resolvedSplatUrls] + [ + activeSplatIndex, + hasMeshFallback, + resolvedFallbackMeshUrl, + resolvedSplatUrl, + resolvedSplatUrls, + ] ); useEffect(() => { - if (!resolvedSplatUrl) return; + if (!resolvedSplatUrl || useMeshFallback) return; const abortController = new AbortController(); let cancelled = false; @@ -1905,7 +1942,25 @@ function WorldSplatViewer({ globalThis.clearTimeout(timeoutId); abortController.abort(); }; - }, [handleSplatLoadStateChange, resolvedSplatUrl]); + }, [handleSplatLoadStateChange, resolvedSplatUrl, useMeshFallback]); + + if ((useMeshFallback || !resolvedSplatUrl) && resolvedFallbackMeshUrl) { + return ( + + ); + } if (!resolvedSplatUrl) { return ( @@ -2108,8 +2163,13 @@ export function World3DViewer({