Last active
April 20, 2025 01:38
-
-
Save dmitrymatveev/3a08c1fb10c6588fa41d727ab0c93a9e to your computer and use it in GitHub Desktop.
Calculates world position from mouse pointer and/or camera direction to a given plane.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CameraControls } from '@react-three/drei'; | |
import { | |
Dispatch, | |
MutableRefObject, | |
SetStateAction, | |
useEffect, | |
useRef, | |
} from 'react'; | |
import { Plane, Raycaster, Vector2, Vector3 } from 'three'; | |
/** Tracks camera position and mouse pointer in world coordinates */ | |
export default function WorldPointerProvider(props: { | |
/** Camera providing perspective for intersection tests */ | |
camera: MutableRefObject<CameraControls | undefined>; | |
/** Canvas element used to read client view dimensions */ | |
canvas: MutableRefObject<HTMLCanvasElement | undefined>; | |
/** Pointer world position updated with mouse move if set */ | |
pointerRef?: MutableRefObject<Vector3>; | |
/** Pointer world position updated on click if set */ | |
clickedState?: [Vector3, Dispatch<SetStateAction<Vector3>>]; | |
/** Camera world position updated together with camera update if set */ | |
viewRef?: MutableRefObject<Vector3>; | |
/** Camera world position updated together with camera update if set */ | |
viewState?: [Vector3, Dispatch<SetStateAction<Vector3>>]; | |
/** Optional plane to test intersections, default to 0,1,0 */ | |
plane?: Plane; | |
}) { | |
const { | |
canvas, | |
camera, | |
pointerRef, | |
clickedState, | |
viewRef, | |
viewState, | |
plane = new Plane(new Vector3(0, 1, 0), 0), | |
} = props; | |
const raycasterRef = useRef(new Raycaster()); | |
const mouseNormalizedRef = useRef(new Vector2()); | |
const raycastResultRef = useRef(new Vector3()); | |
const zero2 = new Vector2(); | |
function normalizePointer(ev: MouseEvent, normalized: Vector2): Vector2 { | |
return normalized.set( | |
(ev.clientX / canvas.current!.clientWidth) * 2 - 1, | |
-(ev.clientY / canvas.current!.clientHeight) * 2 + 1 | |
); | |
} | |
function useRaycast(coords: Vector2): Vector3 | null { | |
if (camera.current) { | |
camera.current.camera.updateWorldMatrix(true, true); | |
raycasterRef.current.setFromCamera(coords, camera.current.camera); | |
} | |
return raycasterRef.current.ray.intersectPlane( | |
plane, | |
raycastResultRef.current | |
); | |
} | |
function handleClick(ev: MouseEvent) { | |
const normalized = normalizePointer(ev, mouseNormalizedRef.current); | |
const worldPosition = useRaycast(normalized); | |
if (worldPosition) { | |
if (clickedState) { | |
const [, setClicked] = clickedState; | |
setClicked(worldPosition.clone()); | |
} | |
} | |
} | |
function handleMove(ev: MouseEvent) { | |
const normalized = normalizePointer(ev, mouseNormalizedRef.current); | |
const worldPosition = useRaycast(normalized); | |
if (worldPosition) { | |
if (pointerRef) { | |
pointerRef.current.copy(worldPosition); | |
} | |
} | |
} | |
function handleControl() { | |
const worldPosition = useRaycast(zero2); | |
if (worldPosition) { | |
viewRef?.current.copy(worldPosition); | |
viewState?.[1](worldPosition.clone()); | |
} | |
} | |
useEffect(() => { | |
if (canvas.current) { | |
if (clickedState) { | |
canvas.current.addEventListener('click', handleClick); | |
} | |
if (pointerRef) { | |
canvas.current.addEventListener('mousemove', handleMove); | |
} | |
if (camera.current && (viewRef || viewState)) { | |
camera.current.addEventListener('control', handleControl); | |
} | |
} | |
return () => { | |
canvas.current?.removeEventListener('click', handleClick); | |
canvas.current?.removeEventListener('mousemove', handleMove); | |
camera.current?.removeEventListener('control', handleControl); | |
}; | |
}, [pointerRef, clickedState]); | |
return null; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment