2026-03-31 03:04:15 +02:00
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
|
|
|
|
|
|
import type { FirstPersonTelemetry } from "../runtime-three/navigation-controller";
|
|
|
|
|
import { RuntimeHost } from "../runtime-three/runtime-host";
|
|
|
|
|
import type { RuntimeNavigationMode, RuntimeSceneDefinition } from "../runtime-three/runtime-scene-build";
|
2026-03-31 05:11:08 +02:00
|
|
|
import { createWorldBackgroundStyle } from "../shared-ui/world-background-style";
|
2026-03-31 03:04:15 +02:00
|
|
|
|
|
|
|
|
interface RunnerCanvasProps {
|
|
|
|
|
runtimeScene: RuntimeSceneDefinition;
|
|
|
|
|
navigationMode: RuntimeNavigationMode;
|
|
|
|
|
onRuntimeMessageChange(message: string | null): void;
|
|
|
|
|
onFirstPersonTelemetryChange(telemetry: FirstPersonTelemetry | null): void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function RunnerCanvas({
|
|
|
|
|
runtimeScene,
|
|
|
|
|
navigationMode,
|
|
|
|
|
onRuntimeMessageChange,
|
|
|
|
|
onFirstPersonTelemetryChange
|
|
|
|
|
}: RunnerCanvasProps) {
|
|
|
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
const hostRef = useRef<RuntimeHost | null>(null);
|
|
|
|
|
const [runnerMessage, setRunnerMessage] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const container = containerRef.current;
|
|
|
|
|
|
|
|
|
|
if (container === null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const testCanvas = document.createElement("canvas");
|
|
|
|
|
const hasWebGl =
|
|
|
|
|
testCanvas.getContext("webgl2") !== null ||
|
|
|
|
|
testCanvas.getContext("webgl") !== null ||
|
|
|
|
|
testCanvas.getContext("experimental-webgl") !== null;
|
|
|
|
|
|
|
|
|
|
if (!hasWebGl) {
|
|
|
|
|
setRunnerMessage("WebGL is unavailable in this browser environment. The runner shell is visible, but runtime rendering is disabled.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const runtimeHost = new RuntimeHost();
|
|
|
|
|
hostRef.current = runtimeHost;
|
|
|
|
|
runtimeHost.mount(container);
|
|
|
|
|
runtimeHost.setRuntimeMessageHandler(onRuntimeMessageChange);
|
|
|
|
|
runtimeHost.setFirstPersonTelemetryHandler(onFirstPersonTelemetryChange);
|
|
|
|
|
setRunnerMessage(null);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
runtimeHost.dispose();
|
|
|
|
|
hostRef.current = null;
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message = error instanceof Error ? error.message : "Runner initialization failed.";
|
|
|
|
|
setRunnerMessage(`Runner initialization failed: ${message}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}, [onFirstPersonTelemetryChange, onRuntimeMessageChange]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
hostRef.current?.loadScene(runtimeScene);
|
|
|
|
|
}, [runtimeScene]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
hostRef.current?.setNavigationMode(navigationMode);
|
|
|
|
|
}, [navigationMode]);
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-31 05:11:08 +02:00
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
className="runner-canvas"
|
|
|
|
|
data-testid="runner-shell"
|
|
|
|
|
aria-label="Built-in scene runner"
|
|
|
|
|
style={createWorldBackgroundStyle(runtimeScene.world.background)}
|
|
|
|
|
>
|
2026-03-31 03:04:15 +02:00
|
|
|
{runnerMessage === null ? null : (
|
|
|
|
|
<div className="runner-canvas__fallback" role="status">
|
|
|
|
|
<div className="runner-canvas__fallback-title">Runner Unavailable</div>
|
|
|
|
|
<div>{runnerMessage}</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|