Skip to content

Instantly share code, notes, and snippets.

@dmitrymatveev
Last active April 20, 2025 01:38
Show Gist options
  • Save dmitrymatveev/3a08c1fb10c6588fa41d727ab0c93a9e to your computer and use it in GitHub Desktop.
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.
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