flyyt / src /useKeyboardControls.ts
gpue's picture
Deploy 771c7d3cfb0b08d632f7fe3d02e59b7bcdff47c4
5a3b695 verified
Raw History Blame Contribute Delete
3.4 kB
import { useEffect, useRef } from "react";
interface KeyboardControlsArgs {
selectedFlyId: string | null;
joystickRef: React.RefObject<{ x: number; y: number }>;
onToggleFlying: () => void;
onAltitudeStep: (direction: 1 | -1) => void;
onFeed: () => void;
}
const ARROW_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]);
// Keyboard alternative to Joystick.tsx's pointer drag (arrow keys) and the
// flight switch/altitude slider (Space, +/-) -- writes straight into the same
// joystickRef the pointer joystick writes to, so FlyInstances.tsx needs no
// awareness of where a command came from.
export function useKeyboardControls({ selectedFlyId, joystickRef, onToggleFlying, onAltitudeStep, onFeed }: KeyboardControlsArgs) {
const heldRef = useRef(new Set<string>());
// Latest-callback refs: onToggleFlying/onAltitudeStep/onFeed are fresh
// closures every App render (they're not memoized there), but
// flying/altitude changes are themselves state updates that would
// otherwise re-run this effect and clearAll() the joystick mid-press.
// Keeping only selectedFlyId/joystickRef as real effect deps means
// listener registration only churns on an actual selection change.
const onToggleFlyingRef = useRef(onToggleFlying);
onToggleFlyingRef.current = onToggleFlying;
const onAltitudeStepRef = useRef(onAltitudeStep);
onAltitudeStepRef.current = onAltitudeStep;
const onFeedRef = useRef(onFeed);
onFeedRef.current = onFeed;
useEffect(() => {
const applyFromHeld = () => {
const held = heldRef.current;
let x = 0;
let y = 0;
if (held.has("ArrowUp")) y += 1;
if (held.has("ArrowDown")) y -= 1;
if (held.has("ArrowLeft")) x -= 1;
if (held.has("ArrowRight")) x += 1;
const mag = Math.hypot(x, y);
if (mag > 1) {
x /= mag;
y /= mag;
}
joystickRef.current.x = x;
joystickRef.current.y = y;
};
const clearAll = () => {
heldRef.current.clear();
joystickRef.current.x = 0;
joystickRef.current.y = 0;
};
// Selection changed -- don't let a key held before switching flies stick.
clearAll();
if (!selectedFlyId) return;
const onKeyDown = (e: KeyboardEvent) => {
if (ARROW_KEYS.has(e.key)) {
e.preventDefault();
heldRef.current.add(e.key);
applyFromHeld();
} else if (e.code === "Space") {
e.preventDefault();
onToggleFlyingRef.current();
} else if (e.key === "+" || e.key === "=") {
e.preventDefault();
onAltitudeStepRef.current(1);
} else if (e.key === "-" || e.key === "_") {
e.preventDefault();
onAltitudeStepRef.current(-1);
} else if (e.key === "Enter") {
e.preventDefault();
onFeedRef.current();
}
};
const onKeyUp = (e: KeyboardEvent) => {
if (ARROW_KEYS.has(e.key)) {
e.preventDefault();
heldRef.current.delete(e.key);
applyFromHeld();
}
};
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
window.addEventListener("blur", clearAll);
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keyup", onKeyUp);
window.removeEventListener("blur", clearAll);
clearAll();
};
}, [selectedFlyId, joystickRef]);
}