Spaces:
Runtime error
Runtime error
| import { OrbitControls } from "@react-three/drei"; | |
| import { useFrame } from "@react-three/fiber"; | |
| import { useEffect, useMemo, useRef } from "react"; | |
| import { MathUtils, Vector3 } from "three"; | |
| import type { OrbitControls as OrbitControlsImpl } from "three-stdlib"; | |
| type KeyboardOrbitControlsProps = { | |
| terrainWidth: number; | |
| terrainDepth: number; | |
| }; | |
| export function KeyboardOrbitControls({ | |
| terrainWidth, | |
| terrainDepth, | |
| }: KeyboardOrbitControlsProps) { | |
| const controlsRef = useRef<OrbitControlsImpl>(null); | |
| const pressedKeysRef = useRef<Set<string>>(new Set()); | |
| const forward = useMemo(() => new Vector3(), []); | |
| const right = useMemo(() => new Vector3(), []); | |
| const movement = useMemo(() => new Vector3(), []); | |
| const previousTarget = useMemo(() => new Vector3(), []); | |
| const appliedMovement = useMemo(() => new Vector3(), []); | |
| useEffect(() => { | |
| const trackedKeys = new Set([ | |
| "arrowup", | |
| "arrowdown", | |
| "arrowleft", | |
| "arrowright", | |
| "w", | |
| "a", | |
| "s", | |
| "d", | |
| ]); | |
| const keyAliases = new Map([ | |
| ["ArrowUp", "arrowup"], | |
| ["ArrowDown", "arrowdown"], | |
| ["ArrowLeft", "arrowleft"], | |
| ["ArrowRight", "arrowright"], | |
| ["KeyW", "w"], | |
| ["KeyA", "a"], | |
| ["KeyS", "s"], | |
| ["KeyD", "d"], | |
| ]); | |
| const updateKey = (event: KeyboardEvent, isPressed: boolean) => { | |
| const key = keyAliases.get(event.code) ?? event.key.toLowerCase(); | |
| if (!trackedKeys.has(key) || isTypingTarget(event.target)) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| if (isPressed) { | |
| pressedKeysRef.current.add(key); | |
| } else { | |
| pressedKeysRef.current.delete(key); | |
| } | |
| }; | |
| const handleKeyDown = (event: KeyboardEvent) => updateKey(event, true); | |
| const handleKeyUp = (event: KeyboardEvent) => updateKey(event, false); | |
| const clearKeys = () => pressedKeysRef.current.clear(); | |
| window.addEventListener("keydown", handleKeyDown); | |
| window.addEventListener("keyup", handleKeyUp); | |
| window.addEventListener("blur", clearKeys); | |
| return () => { | |
| window.removeEventListener("keydown", handleKeyDown); | |
| window.removeEventListener("keyup", handleKeyUp); | |
| window.removeEventListener("blur", clearKeys); | |
| }; | |
| }, []); | |
| useFrame(({ camera }, delta) => { | |
| const keys = pressedKeysRef.current; | |
| const zInput = | |
| Number(keys.has("w") || keys.has("arrowup")) - | |
| Number(keys.has("s") || keys.has("arrowdown")); | |
| const xInput = | |
| Number(keys.has("d") || keys.has("arrowright")) - | |
| Number(keys.has("a") || keys.has("arrowleft")); | |
| if (!zInput && !xInput) { | |
| return; | |
| } | |
| camera.getWorldDirection(forward); | |
| forward.y = 0; | |
| if (forward.lengthSq() < 0.001) { | |
| forward.set(0, 0, -1); | |
| } | |
| forward.normalize(); | |
| right.crossVectors(forward, camera.up).normalize(); | |
| movement.set(0, 0, 0).addScaledVector(forward, zInput).addScaledVector(right, xInput); | |
| if (movement.lengthSq() === 0) { | |
| return; | |
| } | |
| movement.normalize().multiplyScalar(54 * delta); | |
| moveCamera( | |
| camera.position, | |
| movement, | |
| controlsRef.current, | |
| terrainWidth, | |
| terrainDepth, | |
| previousTarget, | |
| appliedMovement, | |
| ); | |
| }); | |
| return ( | |
| <OrbitControls | |
| ref={controlsRef} | |
| makeDefault | |
| enableDamping | |
| dampingFactor={0.08} | |
| maxPolarAngle={Math.PI / 2.2} | |
| minDistance={16} | |
| maxDistance={105} | |
| target={[0, 0, 0]} | |
| /> | |
| ); | |
| } | |
| function moveCamera( | |
| cameraPosition: Vector3, | |
| moveDelta: Vector3, | |
| controls: OrbitControlsImpl | null, | |
| width: number, | |
| depth: number, | |
| previousTarget: Vector3, | |
| appliedMovement: Vector3, | |
| ) { | |
| if (!controls) { | |
| cameraPosition.add(moveDelta); | |
| return; | |
| } | |
| previousTarget.copy(controls.target); | |
| controls.target.add(moveDelta); | |
| controls.target.x = MathUtils.clamp(controls.target.x, -width / 2, width / 2); | |
| controls.target.z = MathUtils.clamp(controls.target.z, -depth / 2, depth / 2); | |
| appliedMovement.copy(controls.target).sub(previousTarget); | |
| cameraPosition.add(appliedMovement); | |
| controls.update(); | |
| } | |
| function isTypingTarget(target: EventTarget | null): boolean { | |
| if (!(target instanceof HTMLElement)) { | |
| return false; | |
| } | |
| const tagName = target.tagName.toLowerCase(); | |
| return tagName === "input" || tagName === "textarea" || target.isContentEditable; | |
| } | |