Download src/useKeyboardControls.ts from gpue/flyyt: direct link, hf CLI and curl.
- Browser
- Download file 3.4 kB
-
https://huggingface.co/spaces/gpue/flyyt/resolve/main/src/useKeyboardControls.ts
- Command line
-
hf download hf://spaces/gpue/flyyt/src/useKeyboardControls.ts
-
curl -L -o useKeyboardControls.ts https://huggingface.co/spaces/gpue/flyyt/resolve/main/src/useKeyboardControls.ts
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]); | |
| } | |