import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, } from "react"; import { useFrame } from "@react-three/fiber"; /** Ticks per second, matching the Torque engine tick rate. */ export const TICK_RATE = 32; const TICK_INTERVAL = 1 / TICK_RATE; export type TickCallback = (tick: number) => void; interface TickContextValue { subscribe: (callback: TickCallback) => () => void; getTick: () => number; } const TickContext = createContext(null); interface TickProviderProps { children: ReactNode; } export function TickProvider({ children }: TickProviderProps) { const callbacksRef = useRef | undefined>(undefined); const accumulatorRef = useRef(0); const tickRef = useRef(0); useFrame((_, delta) => { accumulatorRef.current += delta; while (accumulatorRef.current >= TICK_INTERVAL) { accumulatorRef.current -= TICK_INTERVAL; tickRef.current++; if (callbacksRef.current) { for (const callback of callbacksRef.current) { callback(tickRef.current); } } } }); const subscribe = useCallback((callback: TickCallback) => { callbacksRef.current ??= new Set(); callbacksRef.current.add(callback); return () => { callbacksRef.current!.delete(callback); }; }, []); const getTick = useCallback(() => tickRef.current, []); const context = useMemo(() => ({ subscribe, getTick }), [subscribe, getTick]); return ( {children} ); } export function useTick(callback: TickCallback) { const context = useContext(TickContext); if (!context) { throw new Error("useTick must be used within a TickProvider"); } const callbackRef = useRef(callback); callbackRef.current = callback; useEffect(() => { return context.subscribe((tick) => callbackRef.current(tick)); }, [context]); } export function useGetTick() { const context = useContext(TickContext); if (!context) { throw new Error("useGetTick must be used within a TickProvider"); } return context.getTick; }