Add interaction prompt handling in RunnerCanvas

This commit is contained in:
2026-03-31 06:46:48 +02:00
parent e5013503ed
commit 2f46fea52a

View File

@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
import type { FirstPersonTelemetry } from "../runtime-three/navigation-controller"; import type { FirstPersonTelemetry } from "../runtime-three/navigation-controller";
import { RuntimeHost } from "../runtime-three/runtime-host"; import { RuntimeHost } 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 type { RuntimeNavigationMode, RuntimeSceneDefinition } from "../runtime-three/runtime-scene-build";
import { createWorldBackgroundStyle } from "../shared-ui/world-background-style"; import { createWorldBackgroundStyle } from "../shared-ui/world-background-style";
@@ -10,17 +11,20 @@ interface RunnerCanvasProps {
navigationMode: RuntimeNavigationMode; navigationMode: RuntimeNavigationMode;
onRuntimeMessageChange(message: string | null): void; onRuntimeMessageChange(message: string | null): void;
onFirstPersonTelemetryChange(telemetry: FirstPersonTelemetry | null): void; onFirstPersonTelemetryChange(telemetry: FirstPersonTelemetry | null): void;
onInteractionPromptChange(prompt: RuntimeInteractionPrompt | null): void;
} }
export function RunnerCanvas({ export function RunnerCanvas({
runtimeScene, runtimeScene,
navigationMode, navigationMode,
onRuntimeMessageChange, onRuntimeMessageChange,
onFirstPersonTelemetryChange onFirstPersonTelemetryChange,
onInteractionPromptChange
}: RunnerCanvasProps) { }: RunnerCanvasProps) {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const hostRef = useRef<RuntimeHost | null>(null); const hostRef = useRef<RuntimeHost | null>(null);
const [runnerMessage, setRunnerMessage] = useState<string | null>(null); const [runnerMessage, setRunnerMessage] = useState<string | null>(null);
const [interactionPrompt, setInteractionPrompt] = useState<RuntimeInteractionPrompt | null>(null);
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;
@@ -43,20 +47,26 @@ export function RunnerCanvas({
runtimeHost.mount(container); runtimeHost.mount(container);
runtimeHost.setRuntimeMessageHandler(onRuntimeMessageChange); runtimeHost.setRuntimeMessageHandler(onRuntimeMessageChange);
runtimeHost.setFirstPersonTelemetryHandler(onFirstPersonTelemetryChange); runtimeHost.setFirstPersonTelemetryHandler(onFirstPersonTelemetryChange);
runtimeHost.setInteractionPromptHandler((prompt) => {
setInteractionPrompt(prompt);
onInteractionPromptChange(prompt);
});
setRunnerMessage( setRunnerMessage(
hasWebGl ? null : "WebGL is unavailable in this browser environment. The runner shell is visible, but runtime rendering is disabled." hasWebGl ? null : "WebGL is unavailable in this browser environment. The runner shell is visible, but runtime rendering is disabled."
); );
return () => { return () => {
onInteractionPromptChange(null);
runtimeHost.dispose(); runtimeHost.dispose();
hostRef.current = null; hostRef.current = null;
}; };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Runner initialization failed."; const message = error instanceof Error ? error.message : "Runner initialization failed.";
setRunnerMessage(`Runner initialization failed: ${message}`); setRunnerMessage(`Runner initialization failed: ${message}`);
onInteractionPromptChange(null);
return; return;
} }
}, [onFirstPersonTelemetryChange, onRuntimeMessageChange]); }, [onFirstPersonTelemetryChange, onInteractionPromptChange, onRuntimeMessageChange]);
useEffect(() => { useEffect(() => {
hostRef.current?.loadScene(runtimeScene); hostRef.current?.loadScene(runtimeScene);
@@ -74,6 +84,18 @@ export function RunnerCanvas({
aria-label="Built-in scene runner" aria-label="Built-in scene runner"
style={createWorldBackgroundStyle(runtimeScene.world.background)} style={createWorldBackgroundStyle(runtimeScene.world.background)}
> >
{navigationMode === "firstPerson" ? <div className="runner-canvas__crosshair" aria-hidden="true" /> : null}
{navigationMode === "firstPerson" && interactionPrompt !== null ? (
<div className="runner-canvas__prompt" data-testid="runner-interaction-prompt" role="status" aria-live="polite">
<div className="runner-canvas__prompt-badge">Click</div>
<div className="runner-canvas__prompt-text" data-testid="runner-interaction-prompt-text">
{interactionPrompt.prompt}
</div>
<div className="runner-canvas__prompt-meta" data-testid="runner-interaction-prompt-meta">
{interactionPrompt.distance.toFixed(1)}m away · {interactionPrompt.range.toFixed(1)}m range
</div>
</div>
) : null}
{runnerMessage === null ? null : ( {runnerMessage === null ? null : (
<div className="runner-canvas__fallback" role="status"> <div className="runner-canvas__fallback" role="status">
<div className="runner-canvas__fallback-title">Runner Unavailable</div> <div className="runner-canvas__fallback-title">Runner Unavailable</div>