t2-mapper/src/components/DemoControls.tsx

179 lines
5.5 KiB
TypeScript
Raw Normal View History

2026-02-20 15:48:15 -08:00
import { useCallback, type ChangeEvent } from "react";
2026-02-28 17:58:09 -08:00
import {
useDemoActions,
useDemoCurrentTime,
useDemoDuration,
useDemoIsPlaying,
useDemoRecording,
useDemoSpeed,
} from "./DemoProvider";
import {
buildSerializableDiagnosticsJson,
buildSerializableDiagnosticsSnapshot,
useEngineSelector,
useEngineStoreApi,
} from "../state";
2026-03-01 09:40:17 -08:00
import styles from "./DemoControls.module.css";
2026-02-20 15:48:15 -08:00
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")}`;
}
2026-02-28 17:58:09 -08:00
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`;
}
2026-02-20 15:48:15 -08:00
export function DemoControls() {
2026-02-28 17:58:09 -08:00
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;
});
2026-02-20 15:48:15 -08:00
const handleSeek = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
seek(parseFloat(e.target.value));
},
[seek],
);
const handleSpeedChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
setSpeed(parseFloat(e.target.value));
},
[setSpeed],
);
2026-02-28 17:58:09 -08:00
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]);
2026-02-20 15:48:15 -08:00
if (!recording) return null;
return (
<div
2026-03-01 09:40:17 -08:00
className={styles.Root}
2026-02-20 15:48:15 -08:00
onKeyDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<button
2026-03-01 09:40:17 -08:00
className={styles.PlayPause}
2026-02-20 15:48:15 -08:00
onClick={isPlaying ? pause : play}
aria-label={isPlaying ? "Pause" : "Play"}
>
{isPlaying ? "\u275A\u275A" : "\u25B6"}
</button>
2026-03-01 09:40:17 -08:00
<span className={styles.Time}>
2026-02-28 17:58:09 -08:00
{`${formatTime(currentTime)} / ${formatTime(duration)}`}
2026-02-20 15:48:15 -08:00
</span>
<input
2026-03-01 09:40:17 -08:00
className={styles.Seek}
2026-02-20 15:48:15 -08:00
type="range"
min={0}
max={duration}
step={0.01}
value={currentTime}
onChange={handleSeek}
/>
<select
2026-03-01 09:40:17 -08:00
className={styles.Speed}
2026-02-20 15:48:15 -08:00
value={speed}
onChange={handleSpeedChange}
>
{SPEED_OPTIONS.map((s) => (
<option key={s} value={s}>
{s}x
</option>
))}
</select>
2026-02-28 17:58:09 -08:00
<div
2026-03-01 09:40:17 -08:00
className={styles.DiagnosticsPanel}
2026-02-28 17:58:09 -08:00
data-context-lost={webglContextLost ? "true" : undefined}
>
2026-03-01 09:40:17 -08:00
<div className={styles.DiagnosticsStatus}>
2026-02-28 17:58:09 -08:00
{webglContextLost ? "WebGL context: LOST" : "WebGL context: ok"}
</div>
2026-03-01 09:40:17 -08:00
<div className={styles.DiagnosticsMetrics}>
2026-02-28 17:58:09 -08:00
{latestRendererSample ? (
<>
<span>
geom {latestRendererSample.geometries} tex{" "}
{latestRendererSample.textures} prog{" "}
{latestRendererSample.programs}
</span>
<span>
draw {latestRendererSample.renderCalls} tri{" "}
{latestRendererSample.renderTriangles}
</span>
<span>
scene {latestRendererSample.visibleSceneObjects}/
{latestRendererSample.sceneObjects}
</span>
<span>heap {formatBytes(latestRendererSample.jsHeapUsed)}</span>
</>
) : (
<span>No renderer samples yet</span>
)}
</div>
2026-03-01 09:40:17 -08:00
<div className={styles.DiagnosticsFooter}>
2026-02-28 17:58:09 -08:00
<span>
samples {rendererSampleCount} events {playbackEventCount}
</span>
{latestPlaybackEvent ? (
<span title={latestPlaybackEvent.message}>
last event: {latestPlaybackEvent.kind}
</span>
) : (
<span>last event: none</span>
)}
<button type="button" onClick={handleDumpDiagnostics}>
Dump
</button>
<button type="button" onClick={handleClearDiagnostics}>
Clear
</button>
</div>
</div>
2026-02-20 15:48:15 -08:00
</div>
);
}