t2-mapper/src/components/TickProvider.tsx

86 lines
2.1 KiB
TypeScript

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<TickContextValue | null>(null);
interface TickProviderProps {
children: ReactNode;
}
export function TickProvider({ children }: TickProviderProps) {
const callbacksRef = useRef<Set<TickCallback> | 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 (
<TickContext.Provider value={context}>{children}</TickContext.Provider>
);
}
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;
}