import { Box3, EventDispatcher, MathUtils, Matrix4, OrthographicCamera, PerspectiveCamera, Quaternion, Raycaster, Sphere, Spherical, Vector2, Vector3, Vector4, } from 'three' import * as React from 'react' import { forwardRef, useMemo, useEffect } from 'react' import { extend, useFrame, useThree, ReactThreeFiber, EventManager } from '@react-three/fiber' import CameraControlsImpl from 'camera-controls' import { ForwardRefComponent } from '../helpers/ts-utils' export type CameraControlsProps = Omit< ReactThreeFiber.Overwrite< ReactThreeFiber.Node, { camera?: PerspectiveCamera | OrthographicCamera domElement?: HTMLElement makeDefault?: boolean onStart?: (e?: { type: 'controlstart' }) => void onEnd?: (e?: { type: 'controlend' }) => void onChange?: (e?: { type: 'update' }) => void events?: boolean // Wether to enable events during controls interaction regress?: boolean } >, 'ref' | keyof EventDispatcher > export const CameraControls: ForwardRefComponent = /* @__PURE__ */ forwardRef< CameraControlsImpl, CameraControlsProps >((props, ref) => { // useMemo is used here instead of useEffect, otherwise the useMemo below runs first and throws useMemo(() => { // to allow for tree shaking, we only import the subset of THREE that is used by camera-controls // see https://github.com/yomotsu/camera-controls#important const subsetOfTHREE = { Box3, MathUtils: { clamp: MathUtils.clamp, }, Matrix4, Quaternion, Raycaster, Sphere, Spherical, Vector2, Vector3, Vector4, } CameraControlsImpl.install({ THREE: subsetOfTHREE }) extend({ CameraControlsImpl }) }, []) const { camera, domElement, makeDefault, onStart, onEnd, onChange, regress, ...restProps } = props const defaultCamera = useThree((state) => state.camera) const gl = useThree((state) => state.gl) const invalidate = useThree((state) => state.invalidate) const events = useThree((state) => state.events) as EventManager const setEvents = useThree((state) => state.setEvents) const set = useThree((state) => state.set) const get = useThree((state) => state.get) const performance = useThree((state) => state.performance) const explCamera = camera || defaultCamera const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement const controls = useMemo(() => new CameraControlsImpl(explCamera), [explCamera]) useFrame((state, delta) => { if (controls.enabled) controls.update(delta) }, -1) useEffect(() => { controls.connect(explDomElement) return () => void controls.disconnect() }, [explDomElement, controls]) useEffect(() => { const callback = (e) => { invalidate() if (regress) performance.regress() if (onChange) onChange(e) } const onStartCb: CameraControlsProps['onStart'] = (e) => { if (onStart) onStart(e) } const onEndCb: CameraControlsProps['onEnd'] = (e) => { if (onEnd) onEnd(e) } controls.addEventListener('update', callback) controls.addEventListener('controlstart', onStartCb) controls.addEventListener('controlend', onEndCb) controls.addEventListener('control', callback) controls.addEventListener('transitionstart', callback) controls.addEventListener('wake', callback) return () => { controls.removeEventListener('update', callback) controls.removeEventListener('controlstart', onStartCb) controls.removeEventListener('controlend', onEndCb) controls.removeEventListener('control', callback) controls.removeEventListener('transitionstart', callback) controls.removeEventListener('wake', callback) } }, [controls, onStart, onEnd, invalidate, setEvents, regress, onChange]) useEffect(() => { if (makeDefault) { const old = get().controls set({ controls: controls as unknown as EventDispatcher }) return () => set({ controls: old }) } }, [makeDefault, controls]) return }) export type CameraControls = CameraControlsImpl