import { useThree } from '@react-three/fiber' ; import type { ThreeEvent } from '@react-three/fiber/dist/declarations/src/core/events' ; import { clamp } from 'lodash' ; import { useRef , useCallback , useEffect } from 'react' ; import { Vector2 , Vector3 } from 'three' ; import { useWheelCapture } from '../hooks' ; import { useAxisSystemContext } from './AxisSystemContext' ; const ZOOM_FACTOR = 0.95 ; const CAMERA_TOP_RIGHT = new Vector3 ( 1 , 1 , 0 ) ; function PanZoomMesh ( ) { const { abscissaScale , ordinateScale , visSize } = useAxisSystemContext ( ) ; const { width : visWidth , height : visHeight } = visSize ; const camera = useThree ( ( state ) => state . camera ) ; const { width , height } = useThree ( ( state ) => state . size ) ; const invalidate = useThree ( ( state ) => state . invalidate ) ; const startOffsetPosition = useRef < Vector3 > ( ) ; const viewportCenter = useRef < Vector2 > ( ) ; const moveCameraTo = useCallback ( ( x , y ) => { viewportCenter . current = new Vector2 ( abscissaScale . invert ( x ) , ordinateScale . invert ( y ) ) ; const { position , projectionMatrixInverse } = camera ; const cameraLocalBounds = CAMERA_TOP_RIGHT . clone ( ) . applyMatrix4 ( projectionMatrixInverse ) ; const xBound = Math . max ( visWidth / 2 - cameraLocalBounds . x , 0 ) ; const yBound = Math . max ( visHeight / 2 - cameraLocalBounds . y , 0 ) ; position . set ( clamp ( x , - xBound , xBound ) , clamp ( y , - yBound , yBound ) , position . z ) ; camera . updateMatrixWorld ( ) ; invalidate ( ) ; } , [ abscissaScale , ordinateScale , camera , visWidth , visHeight , invalidate ] ) ; const onPointerDown = useCallback ( ( evt ) => { const { sourceEvent , unprojectedPoint } = evt ; const { target , pointerId } = sourceEvent ; ( target as Element ) . setPointerCapture ( pointerId ) ; const projectedPoint = camera . worldToLocal ( unprojectedPoint . clone ( ) ) ; startOffsetPosition . current = camera . position . clone ( ) . add ( projectedPoint ) ; } , [ camera ] ) ; const onPointerUp = useCallback ( ( evt ) => { const { sourceEvent } = evt ; const { target , pointerId } = sourceEvent ; ( target as Element ) . releasePointerCapture ( pointerId ) ; startOffsetPosition . current = undefined ; } , [ ] ) ; const onPointerMove = useCallback ( ( evt ) => { if ( ! startOffsetPosition . current ) { return ; } evt . stopPropagation ( ) ; const projectedPoint = camera . worldToLocal ( evt . unprojectedPoint . clone ( ) ) ; const { x : pointerX , y : pointerY } = projectedPoint ; const { x : startX , y : startY } = startOffsetPosition . current ; const targetX = startX - pointerX ; const targetY = startY - pointerY ; moveCameraTo ( targetX , targetY ) ; } , [ camera , moveCameraTo ] ) ; const onWheel = useCallback ( ( evt ) => { const { sourceEvent } = evt ; const factor = sourceEvent . deltaY > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR ; camera . zoom = Math . max ( 1 , camera . zoom * factor ) ; camera . updateProjectionMatrix ( ) ; const projectedPoint = camera . worldToLocal ( evt . unprojectedPoint . clone ( ) ) ; const { x : pointerX , y : pointerY } = projectedPoint ; const { x : camX , y : camY } = camera . position ; moveCameraTo ( camX + pointerX * ( 1 - 1 / factor ) , camY + pointerY * ( 1 - 1 / factor ) ) ; } , [ camera , moveCameraTo ] ) ; useEffect ( ( ) => { if ( viewportCenter . current ) { const { x , y } = viewportCenter . current ; moveCameraTo ( abscissaScale ( x ) , ordinateScale ( y ) ) ; } } , [ abscissaScale , viewportCenter , moveCameraTo , ordinateScale ] ) ; useWheelCapture ( ) ; return ( < mesh { ... { onPointerMove , onPointerUp , onPointerDown , onWheel } } > < meshBasicMaterial opacity = { 0 } transparent / > < planeGeometry args = { [ width , height ] } /> < /mesh>); } export default PanZoomMesh ;