/** * 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 ? 'immersive-ar' : 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; }