import { useCallback, useEffect, useRef } from 'react' import { useThree } from '@react-three/fiber' import { useCameraDollyGestures } from '../camera/useCameraDollyGestures' import { useButterflyStore } from './store' import { isEditableTarget } from '../../utils/dom' export interface ButterflyInput { keys: React.MutableRefObject> yaw: React.MutableRefObject pitch: React.MutableRefObject distance: React.MutableRefObject targetDistance: React.MutableRefObject touchMoveVec: React.MutableRefObject<{ x: number; y: number }> touchVertical: React.MutableRefObject } const PITCH_LIMIT = Math.PI / 2.1 const DOLLY_UNITS_PER_PIXEL = 0.01 export function useButterflyInput(): ButterflyInput { const { gl } = useThree() const keys = useRef(new Set()) const yaw = useRef(useButterflyStore.getState().defaultYaw) const pitch = useRef(useButterflyStore.getState().defaultPitch) const distance = useRef(useButterflyStore.getState().defaultDistance) const targetDistance = useRef(useButterflyStore.getState().defaultDistance) const touchMoveVec = useRef({ x: 0, y: 0 }) const touchVertical = useRef(0) const applyDolly = useCallback((deltaY: number) => { const s = useButterflyStore.getState() targetDistance.current = Math.max( s.minDistance, Math.min(s.maxDistance, targetDistance.current + deltaY * DOLLY_UNITS_PER_PIXEL * s.zoomSpeed), ) }, []) useCameraDollyGestures({ domElement: gl.domElement, onDollyPixels: applyDolly }) useEffect(() => { const onKey = (e: KeyboardEvent) => { if (isEditableTarget(e.target)) { keys.current.delete(e.code) return } if (e.type === 'keydown') { keys.current.add(e.code) if (e.code === 'KeyP') { const s = useButterflyStore.getState() s.setParam('paused', !s.paused) } } else { keys.current.delete(e.code) } } window.addEventListener('keydown', onKey) window.addEventListener('keyup', onKey) const onBlur = () => keys.current.clear() window.addEventListener('blur', onBlur) const onWheel = (e: WheelEvent) => { e.preventDefault() const s = useButterflyStore.getState() targetDistance.current = Math.max( s.minDistance, Math.min(s.maxDistance, targetDistance.current + e.deltaY * 0.01 * s.zoomSpeed), ) } gl.domElement.addEventListener('wheel', onWheel, { passive: false }) const touches = new Map() let moveTouchId: number | null = null let lookTouchId: number | null = null let moveOrigin = { x: 0, y: 0 } const onTouchStart = (e: TouchEvent) => { const changedTouches = Array.from(e.changedTouches) for (const t of changedTouches) { touches.set(t.identifier, { x: t.clientX, y: t.clientY }) } if (e.touches.length >= 2) { touchMoveVec.current.x = 0 touchMoveVec.current.y = 0 return } for (const t of changedTouches) { const isLeft = t.clientX < window.innerWidth / 2 if (isLeft && moveTouchId === null) { moveTouchId = t.identifier moveOrigin = { x: t.clientX, y: t.clientY } } else if (!isLeft && lookTouchId === null) { lookTouchId = t.identifier } } } const onTouchMove = (e: TouchEvent) => { for (const t of Array.from(e.changedTouches)) { const prev = touches.get(t.identifier) touches.set(t.identifier, { x: t.clientX, y: t.clientY }) if (e.touches.length >= 2) continue if (t.identifier === lookTouchId && prev) { const st = useButterflyStore.getState() const s = st.mouseSensitivity const ySign = st.invertY ? 1 : -1 yaw.current -= (t.clientX - prev.x) * s * 1.5 pitch.current += (t.clientY - prev.y) * s * 1.5 * ySign pitch.current = Math.max(-PITCH_LIMIT, Math.min(PITCH_LIMIT, pitch.current)) } if (t.identifier === moveTouchId) { const dx = (t.clientX - moveOrigin.x) / 50 const dy = (t.clientY - moveOrigin.y) / 50 touchMoveVec.current.x = Math.max(-1, Math.min(1, dx)) touchMoveVec.current.y = Math.max(-1, Math.min(1, -dy)) } } if (e.touches.length >= 2) { touchMoveVec.current.x = 0 touchMoveVec.current.y = 0 } } const onTouchEnd = (e: TouchEvent) => { for (const t of Array.from(e.changedTouches)) { touches.delete(t.identifier) if (t.identifier === moveTouchId) { moveTouchId = null touchMoveVec.current.x = 0 touchMoveVec.current.y = 0 } if (t.identifier === lookTouchId) lookTouchId = null } } gl.domElement.addEventListener('touchstart', onTouchStart, { passive: true }) gl.domElement.addEventListener('touchmove', onTouchMove, { passive: true }) gl.domElement.addEventListener('touchend', onTouchEnd, { passive: true }) gl.domElement.addEventListener('touchcancel', onTouchEnd, { passive: true }) return () => { window.removeEventListener('keydown', onKey) window.removeEventListener('keyup', onKey) window.removeEventListener('blur', onBlur) gl.domElement.removeEventListener('wheel', onWheel) gl.domElement.removeEventListener('touchstart', onTouchStart) gl.domElement.removeEventListener('touchmove', onTouchMove) gl.domElement.removeEventListener('touchend', onTouchEnd) gl.domElement.removeEventListener('touchcancel', onTouchEnd) } }, [gl]) return { keys, yaw, pitch, distance, targetDistance, touchMoveVec, touchVertical } }