diff --git a/src/devkit/SceneStatsProbe.tsx b/src/devkit/SceneStatsProbe.tsx new file mode 100644 index 0000000..14fc30c --- /dev/null +++ b/src/devkit/SceneStatsProbe.tsx @@ -0,0 +1,48 @@ +/** + * DEVKIT — samples scene + XR session every frame, writes to sceneStatsStore. + * Mounted inside . No visual output. + */ +import { useEffect, useRef } from 'react'; +import { useFrame, useThree } from '@react-three/fiber'; +import { useXR } from '@react-three/xr'; +import { sceneStats, updateModelStats } from './sceneStatsStore'; + +export function SceneStatsProbe() { + const { scene } = useThree(); + const session = useXR((s) => s.session); + const frameCount = useRef(0); + const lastFpsTime = useRef(performance.now()); + + useEffect(() => { + sceneStats.xrMode = session?.mode ?? null; + }, [session]); + + useFrame((_state, _dt, frame: XRFrame | undefined) => { + frameCount.current++; + const now = performance.now(); + if (now - lastFpsTime.current >= 500) { + sceneStats.fps = Math.round((frameCount.current * 1000) / (now - lastFpsTime.current)); + frameCount.current = 0; + lastFpsTime.current = now; + } + + // Find the grabbable model group by traversing for first userData marker, fallback: scan + let modelGroup: import('three').Object3D | null = null; + scene.traverse((o) => { + if (!modelGroup && o.userData?.__xrModelRoot) modelGroup = o; + }); + updateModelStats(modelGroup); + + if (frame) { + try { + // @ts-expect-error detectedPlanes is experimental + const planes = frame.detectedPlanes; + sceneStats.xrPlanesCount = planes ? (planes.size ?? planes.length ?? 0) : 0; + } catch { sceneStats.xrPlanesCount = 0; } + sceneStats.xrInputCount = frame.session?.inputSources?.length ?? 0; + } + sceneStats.lastUpdate = now; + }); + + return null; +}