import { useCallback, type ChangeEvent } from "react"; import { useDemoActions, useDemoCurrentTime, useDemoDuration, useDemoIsPlaying, useDemoRecording, useDemoSpeed, } from "./DemoProvider"; import { buildSerializableDiagnosticsJson, buildSerializableDiagnosticsSnapshot, useEngineSelector, useEngineStoreApi, } from "../state"; import styles from "./DemoControls.module.css"; const SPEED_OPTIONS = [0.25, 0.5, 1, 2, 4]; function formatTime(seconds: number): string { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, "0")}`; } function formatBytes(value: number | undefined): string { if (!Number.isFinite(value) || value == null) { return "n/a"; } if (value < 1024) return `${Math.round(value)} B`; if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} KB`; if (value < 1024 ** 3) return `${(value / 1024 ** 2).toFixed(1)} MB`; return `${(value / 1024 ** 3).toFixed(2)} GB`; } export function DemoControls() { const recording = useDemoRecording(); const isPlaying = useDemoIsPlaying(); const currentTime = useDemoCurrentTime(); const duration = useDemoDuration(); const speed = useDemoSpeed(); const { play, pause, seek, setSpeed } = useDemoActions(); const engineStore = useEngineStoreApi(); const webglContextLost = useEngineSelector( (state) => state.diagnostics.webglContextLost, ); const rendererSampleCount = useEngineSelector( (state) => state.diagnostics.rendererSamples.length, ); const latestRendererSample = useEngineSelector((state) => { const samples = state.diagnostics.rendererSamples; return samples.length > 0 ? samples[samples.length - 1] : null; }); const playbackEventCount = useEngineSelector( (state) => state.diagnostics.playbackEvents.length, ); const latestPlaybackEvent = useEngineSelector((state) => { const events = state.diagnostics.playbackEvents; return events.length > 0 ? events[events.length - 1] : null; }); const handleSeek = useCallback( (e: ChangeEvent) => { seek(parseFloat(e.target.value)); }, [seek], ); const handleSpeedChange = useCallback( (e: ChangeEvent) => { setSpeed(parseFloat(e.target.value)); }, [setSpeed], ); const handleDumpDiagnostics = useCallback(() => { const state = engineStore.getState(); const snapshot = buildSerializableDiagnosticsSnapshot(state); const json = buildSerializableDiagnosticsJson(state); console.log("[demo diagnostics dump]", snapshot); console.log("[demo diagnostics dump json]", json); }, [engineStore]); const handleClearDiagnostics = useCallback(() => { engineStore.getState().clearPlaybackDiagnostics(); console.info("[demo diagnostics] Cleared playback diagnostics"); }, [engineStore]); if (!recording) return null; return (
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()} > {`${formatTime(currentTime)} / ${formatTime(duration)}`}
{webglContextLost ? "WebGL context: LOST" : "WebGL context: ok"}
{latestRendererSample ? ( <> geom {latestRendererSample.geometries} tex{" "} {latestRendererSample.textures} prog{" "} {latestRendererSample.programs} draw {latestRendererSample.renderCalls} tri{" "} {latestRendererSample.renderTriangles} scene {latestRendererSample.visibleSceneObjects}/ {latestRendererSample.sceneObjects} heap {formatBytes(latestRendererSample.jsHeapUsed)} ) : ( No renderer samples yet )}
samples {rendererSampleCount} events {playbackEventCount} {latestPlaybackEvent ? ( last event: {latestPlaybackEvent.kind} ) : ( last event: none )}
); }