From 40824c6078f53ed5af77489755791a93b15dacb9 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sat, 11 Apr 2026 04:17:19 +0200 Subject: [PATCH] Add scene loading screen and error handling to RunnerCanvas --- src/runner-web/RunnerCanvas.tsx | 102 +++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/src/runner-web/RunnerCanvas.tsx b/src/runner-web/RunnerCanvas.tsx index e25aac13..c3dec552 100644 --- a/src/runner-web/RunnerCanvas.tsx +++ b/src/runner-web/RunnerCanvas.tsx @@ -4,14 +4,17 @@ import type { LoadedAudioAsset } from "../assets/audio-assets"; import type { LoadedModelAsset } from "../assets/gltf-model-import"; import type { LoadedImageAsset } from "../assets/image-assets"; import type { ProjectAssetRecord } from "../assets/project-assets"; +import type { SceneLoadingScreenSettings } from "../document/scene-document"; import type { FirstPersonTelemetry } from "../runtime-three/navigation-controller"; -import { RuntimeHost } from "../runtime-three/runtime-host"; +import { RuntimeHost, type RuntimeSceneLoadState } from "../runtime-three/runtime-host"; import type { RuntimeInteractionPrompt } from "../runtime-three/runtime-interaction-system"; import type { RuntimeNavigationMode, RuntimeSceneDefinition } from "../runtime-three/runtime-scene-build"; import { createWorldBackgroundStyle } from "../shared-ui/world-background-style"; interface RunnerCanvasProps { runtimeScene: RuntimeSceneDefinition; + sceneName: string; + sceneLoadingScreen: SceneLoadingScreenSettings; projectAssets: Record; loadedModelAssets: Record; loadedImageAssets: Record; @@ -24,6 +27,8 @@ interface RunnerCanvasProps { export function RunnerCanvas({ runtimeScene, + sceneName, + sceneLoadingScreen, projectAssets, loadedModelAssets, loadedImageAssets, @@ -36,8 +41,15 @@ export function RunnerCanvas({ const containerRef = useRef(null); const hostRef = useRef(null); const [runnerMessage, setRunnerMessage] = useState(null); + const [sceneLoadState, setSceneLoadState] = useState({ + status: "loading", + message: null + }); const [interactionPrompt, setInteractionPrompt] = useState(null); const [firstPersonTelemetry, setFirstPersonTelemetry] = useState(null); + const overlayMessage = runnerMessage ?? sceneLoadState.message; + const overlayStatus = overlayMessage !== null ? "error" : sceneLoadState.status; + const runnerReady = overlayStatus === "ready"; useEffect(() => { const container = containerRef.current; @@ -53,6 +65,7 @@ export function RunnerCanvas({ hostRef.current = runtimeHost; runtimeHost.mount(container); runtimeHost.setRuntimeMessageHandler(onRuntimeMessageChange); + runtimeHost.setSceneLoadStateHandler(setSceneLoadState); runtimeHost.setFirstPersonTelemetryHandler((telemetry) => { setFirstPersonTelemetry(telemetry); onFirstPersonTelemetryChange(telemetry); @@ -65,14 +78,24 @@ export function RunnerCanvas({ return () => { onInteractionPromptChange(null); + onFirstPersonTelemetryChange(null); setFirstPersonTelemetry(null); + setInteractionPrompt(null); runtimeHost.dispose(); hostRef.current = null; }; } catch (error) { - const message = error instanceof Error ? error.message : "Runner initialization failed."; - setRunnerMessage(`Runner initialization failed: ${message}`); + const message = + error instanceof Error ? error.message : "Runner initialization failed."; + const failureMessage = `Runner initialization failed: ${message}`; + setRunnerMessage(failureMessage); + setSceneLoadState({ + status: "error", + message: failureMessage + }); + onRuntimeMessageChange(failureMessage); onInteractionPromptChange(null); + onFirstPersonTelemetryChange(null); return; } }, [onFirstPersonTelemetryChange, onInteractionPromptChange, onRuntimeMessageChange]); @@ -82,8 +105,18 @@ export function RunnerCanvas({ }, [projectAssets, loadedModelAssets, loadedImageAssets, loadedAudioAssets]); useEffect(() => { + setRunnerMessage(null); + setSceneLoadState({ + status: "loading", + message: null + }); + setInteractionPrompt(null); + setFirstPersonTelemetry(null); + onInteractionPromptChange(null); + onFirstPersonTelemetryChange(null); + onRuntimeMessageChange(null); hostRef.current?.loadScene(runtimeScene); - }, [runtimeScene]); + }, [onFirstPersonTelemetryChange, onInteractionPromptChange, onRuntimeMessageChange, runtimeScene]); useEffect(() => { hostRef.current?.setNavigationMode(navigationMode); @@ -95,14 +128,69 @@ export function RunnerCanvas({ className={`runner-canvas ${navigationMode === "firstPerson" && firstPersonTelemetry?.cameraSubmerged ? "runner-canvas--underwater" : ""}`} data-testid="runner-shell" aria-label="Built-in scene runner" + aria-busy={!runnerReady} style={createWorldBackgroundStyle( runtimeScene.world.background, runtimeScene.world.background.mode === "image" ? loadedImageAssets[runtimeScene.world.background.assetId]?.sourceUrl ?? null : null )} > - {navigationMode === "firstPerson" && firstPersonTelemetry?.cameraSubmerged ?