11import { useEffect , useRef , type RefObject } from 'react'
2- import { contentGroupRef } from '../contentGroupRef'
3- import { stageRef } from '../stageRef'
4- import { useAppStore } from '../store'
5- import { getMapPointer } from '../utils/mapPointer'
6- import { mapToLayerLocal } from '../utils/viewport'
72import type { Viewport } from '../types'
3+ import { pinchViewport } from '../utils/viewport'
84
95const MIN_DISTANCE = 10
106
@@ -25,6 +21,12 @@ function getCenter(
2521 }
2622}
2723
24+ /**
25+ * Two-finger pinch-zoom and pan on the canvas. The viewport is the stage's
26+ * transform: the point between the fingers stays under them while the scale
27+ * and position change. onGestureStart/onGestureEnd let the canvas stop any
28+ * one-finger drag that is still running, so the two do not fight over the stage.
29+ */
2830export function usePinchZoom (
2931 containerRef : RefObject < HTMLElement | null > ,
3032 enabled : boolean ,
@@ -33,6 +35,8 @@ export function usePinchZoom(
3335 onCommit : ( viewport : Viewport ) => void ,
3436 minScale : number ,
3537 maxScale : number ,
38+ onGestureStart ?: ( ) => void ,
39+ onGestureEnd ?: ( ) => void ,
3640) {
3741 const pointersRef = useRef (
3842 new Map < number , { clientX : number ; clientY : number } > ( ) ,
@@ -43,41 +47,20 @@ export function usePinchZoom(
4347 const getViewportRef = useRef ( getViewport )
4448 const onPreviewRef = useRef ( onPreview )
4549 const onCommitRef = useRef ( onCommit )
50+ const onGestureStartRef = useRef ( onGestureStart )
51+ const onGestureEndRef = useRef ( onGestureEnd )
4652 getViewportRef . current = getViewport
4753 onPreviewRef . current = onPreview
4854 onCommitRef . current = onCommit
55+ onGestureStartRef . current = onGestureStart
56+ onGestureEndRef . current = onGestureEnd
4957
5058 useEffect ( ( ) => {
5159 if ( ! enabled ) return
5260 const container = containerRef . current
5361 if ( ! container ) return
5462 const pointers = pointersRef . current
5563
56- const getStagePoint = ( clientX : number , clientY : number ) => {
57- const rect = container . getBoundingClientRect ( )
58- return { x : clientX - rect . left , y : clientY - rect . top }
59- }
60-
61- const mapPointAtClient = ( clientX : number , clientY : number ) => {
62- const stage = stageRef . current
63- const vp = viewportRef . current
64- const stagePoint = getStagePoint ( clientX , clientY )
65-
66- if ( stage && contentGroupRef . current ) {
67- stage . setPointersPositions ( { clientX, clientY } )
68- const mapPoint = getMapPointer ( stage )
69- if ( mapPoint ) return { mapPoint, stagePoint }
70- }
71-
72- return {
73- mapPoint : {
74- x : ( stagePoint . x - vp . x ) / vp . scale ,
75- y : ( stagePoint . y - vp . y ) / vp . scale ,
76- } ,
77- stagePoint,
78- }
79- }
80-
8164 const onPointerDown = ( e : PointerEvent ) => {
8265 if ( e . pointerType !== 'touch' ) return
8366 pointers . set ( e . pointerId , {
@@ -88,11 +71,18 @@ export function usePinchZoom(
8871 const pts = [ ...pointers . values ( ) ]
8972 viewportRef . current = getViewportRef . current ( )
9073 lastPinchDistRef . current = getDistance ( pts [ 0 ] , pts [ 1 ] )
91- lastPanCenterRef . current = getCenter ( pts [ 0 ] , pts [ 1 ] )
74+ lastPanCenterRef . current = toCanvas ( getCenter ( pts [ 0 ] , pts [ 1 ] ) )
75+ onGestureStartRef . current ?.( )
9276 e . preventDefault ( )
9377 }
9478 }
9579
80+ /** Finger centre in canvas (container) coordinates, matching the viewport's x/y. */
81+ const toCanvas = ( point : { x : number ; y : number } ) => {
82+ const rect = container . getBoundingClientRect ( )
83+ return { x : point . x - rect . left , y : point . y - rect . top }
84+ }
85+
9686 const onPointerMove = ( e : PointerEvent ) => {
9787 if ( ! pointers . has ( e . pointerId ) ) return
9888 pointers . set ( e . pointerId , {
@@ -105,38 +95,23 @@ export function usePinchZoom(
10595
10696 const pts = [ ...pointers . values ( ) ]
10797 const dist = getDistance ( pts [ 0 ] , pts [ 1 ] )
108- const center = getCenter ( pts [ 0 ] , pts [ 1 ] )
98+ const center = toCanvas ( getCenter ( pts [ 0 ] , pts [ 1 ] ) )
10999 const vp = viewportRef . current
110100
111- if ( lastPinchDistRef . current !== null && lastPinchDistRef . current > 0 ) {
112- const scaleFactor = dist / lastPinchDistRef . current
113- const oldScale = vp . scale
114- const newScale = Math . min (
101+ const lastCenter = lastPanCenterRef . current
102+ if (
103+ lastPinchDistRef . current !== null &&
104+ lastPinchDistRef . current > 0 &&
105+ lastCenter
106+ ) {
107+ const next = pinchViewport (
108+ vp ,
109+ lastCenter ,
110+ center ,
111+ dist / lastPinchDistRef . current ,
112+ minScale ,
115113 maxScale ,
116- Math . max ( minScale , oldScale * scaleFactor ) ,
117114 )
118-
119- const { mapPoint, stagePoint } = mapPointAtClient ( center . x , center . y )
120- const mapImage = useAppStore . getState ( ) . mapImage
121- const mapCenter = mapImage
122- ? { x : mapImage . width / 2 , y : mapImage . height / 2 }
123- : { x : 0 , y : 0 }
124- const layerPoint = mapToLayerLocal ( mapPoint , mapCenter , vp . rotation )
125-
126- let nextX = stagePoint . x - layerPoint . x * newScale
127- let nextY = stagePoint . y - layerPoint . y * newScale
128-
129- if ( lastPanCenterRef . current ) {
130- nextX += center . x - lastPanCenterRef . current . x
131- nextY += center . y - lastPanCenterRef . current . y
132- }
133-
134- const next = {
135- scale : newScale ,
136- x : nextX ,
137- y : nextY ,
138- rotation : vp . rotation ,
139- }
140115 viewportRef . current = next
141116 onPreviewRef . current ( next )
142117 }
@@ -149,7 +124,10 @@ export function usePinchZoom(
149124 const wasPinching = lastPinchDistRef . current !== null
150125 pointers . delete ( e . pointerId )
151126 if ( pointers . size < 2 ) {
152- if ( wasPinching ) onCommitRef . current ( viewportRef . current )
127+ if ( wasPinching ) {
128+ onCommitRef . current ( viewportRef . current )
129+ onGestureEndRef . current ?.( )
130+ }
153131 lastPinchDistRef . current = null
154132 lastPanCenterRef . current = null
155133 }
0 commit comments