world-simulator / frontend /src /scene /KeyboardOrbitControls.tsx
DeltaZN
feat: create 2 villages - with qwen control
33a406f
Raw
History Blame Contribute Delete
4.38 kB
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;
}