File size: 4,225 Bytes
1829efb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { useEffect } from 'react'

interface Props {
  domElement: HTMLElement
  onDollyPixels: (delta: number) => void
  onTumblePixels: (dx: number, dy: number) => void
}

export function useCameraGestures({ domElement, onDollyPixels, onTumblePixels }: Props) {
  useEffect(() => {
    let rightDragging = false
    let lastMouseX = 0
    let lastMouseY = 0
    let twoFingerActive = false
    let lastPinchDist = 0
    let lastTouchX = 0
    let lastTouchY = 0

    const onContextMenu = (e: MouseEvent) => e.preventDefault()

    const onMouseDown = (e: MouseEvent) => {
      if (e.button !== 2) return
      rightDragging = true
      lastMouseX = e.clientX
      lastMouseY = e.clientY
      e.preventDefault()
    }

    const onMouseMove = (e: MouseEvent) => {
      if (!rightDragging) return
      if (document.pointerLockElement === domElement) return
      const dx = e.clientX - lastMouseX
      const dy = e.clientY - lastMouseY
      lastMouseX = e.clientX
      lastMouseY = e.clientY
      onTumblePixels(dx, dy)
    }

    const onMouseUp = (e: MouseEvent) => {
      if (e.button === 2) rightDragging = false
    }

    const onWheel = (e: WheelEvent) => {
      e.preventDefault()
      if (e.ctrlKey) {
        // trackpad pinch-to-zoom
        onDollyPixels(e.deltaY)
        return
      }
      // Trackpad two-finger drag reports as pixel-mode wheel, including vertical drags.
      // Treat it as look/tumble; translation stays on fly keys and zoom stays on pinch/wheel.
      if (e.deltaMode === 0) {
        onTumblePixels(-e.deltaX, -e.deltaY)
        return
      }
      // Mouse wheel zooms on non-pixel wheel events.
      onDollyPixels(e.deltaY)
    }

    const touchDist = (t: TouchList) =>
      Math.hypot(t[1].clientX - t[0].clientX, t[1].clientY - t[0].clientY)

    const touchCenter = (t: TouchList) => ({
      x: (t[0].clientX + t[1].clientX) / 2,
      y: (t[0].clientY + t[1].clientY) / 2,
    })

    const onTouchStart = (e: TouchEvent) => {
      if (e.touches.length !== 2) { twoFingerActive = false; return }
      twoFingerActive = true
      lastPinchDist = touchDist(e.touches)
      const c = touchCenter(e.touches)
      lastTouchX = c.x
      lastTouchY = c.y
    }

    const onTouchMove = (e: TouchEvent) => {
      if (e.touches.length !== 2) { twoFingerActive = false; return }
      e.preventDefault()
      const dist = touchDist(e.touches)
      const c = touchCenter(e.touches)
      if (twoFingerActive) {
        const dx = c.x - lastTouchX
        const dy = c.y - lastTouchY
        const centerMove = Math.hypot(dx, dy)
        const pinchDelta = lastPinchDist - dist
        if (centerMove > 0.2) {
          onTumblePixels(-dx, -dy)
        } else if (Math.abs(pinchDelta) > 4) {
          onDollyPixels(pinchDelta * 2.5)
        }
      }
      lastPinchDist = dist
      lastTouchX = c.x
      lastTouchY = c.y
      twoFingerActive = true
    }

    const onTouchEnd = (e: TouchEvent) => {
      if (e.touches.length < 2) twoFingerActive = false
    }

    domElement.addEventListener('contextmenu', onContextMenu)
    domElement.addEventListener('mousedown', onMouseDown)
    window.addEventListener('mousemove', onMouseMove)
    window.addEventListener('mouseup', onMouseUp)
    domElement.addEventListener('wheel', onWheel, { passive: false })
    domElement.addEventListener('touchstart', onTouchStart, { passive: true })
    domElement.addEventListener('touchmove', onTouchMove, { passive: false })
    domElement.addEventListener('touchend', onTouchEnd)
    domElement.addEventListener('touchcancel', onTouchEnd)

    return () => {
      domElement.removeEventListener('contextmenu', onContextMenu)
      domElement.removeEventListener('mousedown', onMouseDown)
      window.removeEventListener('mousemove', onMouseMove)
      window.removeEventListener('mouseup', onMouseUp)
      domElement.removeEventListener('wheel', onWheel)
      domElement.removeEventListener('touchstart', onTouchStart)
      domElement.removeEventListener('touchmove', onTouchMove)
      domElement.removeEventListener('touchend', onTouchEnd)
      domElement.removeEventListener('touchcancel', onTouchEnd)
    }
  }, [domElement, onDollyPixels, onTumblePixels])
}