Pārlūkot izejas kodu

Improves pan and zoom gestures

main
Steve Ruiz 3 gadus atpakaļ
vecāks
revīzija
b00e0d3a95

+ 147191
- 0
.yarn/releases/yarn-1.19.0.cjs
Failā izmaiņas netiks attēlotas, jo tās ir par lielu
Parādīt failu


+ 5
- 0
.yarnrc Parādīt failu

@@ -0,0 +1,5 @@
1
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2
+# yarn lockfile v1
3
+
4
+
5
+yarn-path ".yarn/releases/yarn-1.19.0.cjs"

+ 3
- 2
package.json Parādīt failu

@@ -44,8 +44,9 @@
44 44
     "babel-jest": "^27.1.0",
45 45
     "eslint": "^7.32.0",
46 46
     "fake-indexeddb": "^3.1.3",
47
+    "init-package-json": "^2.0.4",
47 48
     "jest": "^27.1.0",
48
-    "lerna": "^3.15.0",
49
+    "lerna": "^3.22.1",
49 50
     "react": "^17.0.2",
50 51
     "react-dom": "^17.0.2",
51 52
     "resize-observer-polyfill": "^1.5.1",
@@ -115,4 +116,4 @@
115 116
       "\\+(.*)": "<rootDir>/packages/core/src/$1"
116 117
     }
117 118
   }
118
-}
119
+}

+ 1
- 1
packages/core/package.json Parādīt failu

@@ -55,7 +55,7 @@
55 55
     "react-dom": "^17.0.2"
56 56
   },
57 57
   "dependencies": {
58
-    "react-use-gesture": "^9.1.3"
58
+    "@use-gesture/react": "^10.0.0-beta.24"
59 59
   },
60 60
   "gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf"
61 61
 }

+ 2
- 3
packages/core/src/components/canvas/canvas.tsx Parādīt failu

@@ -37,11 +37,8 @@ export function Canvas<T extends TLShape>({
37 37
 }: CanvasProps<T>): JSX.Element {
38 38
   const rCanvas = React.useRef<SVGSVGElement>(null)
39 39
   const rContainer = React.useRef<HTMLDivElement>(null)
40
-
41 40
   const rGroup = useCameraCss(pageState)
42 41
 
43
-  useResizeObserver(rCanvas)
44
-
45 42
   useZoomEvents(rCanvas)
46 43
 
47 44
   useSafariFocusOutFix()
@@ -50,6 +47,8 @@ export function Canvas<T extends TLShape>({
50 47
 
51 48
   const events = useCanvasEvents()
52 49
 
50
+  useResizeObserver(rCanvas)
51
+
53 52
   return (
54 53
     <div className="tl-container" ref={rContainer}>
55 54
       <svg id="canvas" className="tl-canvas" ref={rCanvas} {...events}>

+ 3
- 3
packages/core/src/components/page/page.tsx Parādīt failu

@@ -27,11 +27,11 @@ export function Page<T extends TLShape>({
27 27
   hideIndicators,
28 28
   meta,
29 29
 }: PageProps<T>): JSX.Element {
30
-  const { callbacks, shapeUtils } = useTLContext()
30
+  const { callbacks, shapeUtils, inputs } = useTLContext()
31 31
 
32 32
   useRenderOnResize()
33 33
 
34
-  const shapeTree = useShapeTree(page, pageState, shapeUtils, meta, callbacks.onChange)
34
+  const shapeTree = useShapeTree(page, pageState, shapeUtils, inputs.size, meta, callbacks.onChange)
35 35
 
36 36
   const { shapeWithHandles } = useHandles(page, pageState)
37 37
 
@@ -47,7 +47,7 @@ export function Page<T extends TLShape>({
47 47
     <>
48 48
       {bounds && !hideBounds && <BoundsBg bounds={bounds} rotation={rotation} />}
49 49
       {shapeTree.map((node) => (
50
-        <ShapeNode key={node.shape.id} {...node} />
50
+        <ShapeNode key={node.shape.id} utils={shapeUtils} {...node} />
51 51
       ))}
52 52
       {bounds && !hideBounds && (
53 53
         <Bounds zoom={zoom} bounds={bounds} isLocked={isLocked} rotation={rotation} />

+ 7
- 3
packages/core/src/components/shape/shape-node.tsx Parādīt failu

@@ -1,10 +1,11 @@
1 1
 import * as React from 'react'
2
-import type { IShapeTreeNode } from '+types'
2
+import type { IShapeTreeNode, TLShape, TLShapeUtils } from '+types'
3 3
 import { Shape } from './shape'
4 4
 
5 5
 export const ShapeNode = React.memo(
6 6
   <M extends Record<string, unknown>>({
7 7
     shape,
8
+    utils,
8 9
     children,
9 10
     isEditing,
10 11
     isBinding,
@@ -12,7 +13,7 @@ export const ShapeNode = React.memo(
12 13
     isSelected,
13 14
     isCurrentParent,
14 15
     meta,
15
-  }: IShapeTreeNode<M>) => {
16
+  }: { utils: TLShapeUtils<TLShape> } & IShapeTreeNode<M>) => {
16 17
     return (
17 18
       <>
18 19
         <Shape
@@ -22,10 +23,13 @@ export const ShapeNode = React.memo(
22 23
           isHovered={isHovered}
23 24
           isSelected={isSelected}
24 25
           isCurrentParent={isCurrentParent}
26
+          utils={utils[shape.type]}
25 27
           meta={meta}
26 28
         />
27 29
         {children &&
28
-          children.map((childNode) => <ShapeNode key={childNode.shape.id} {...childNode} />)}
30
+          children.map((childNode) => (
31
+            <ShapeNode key={childNode.shape.id} utils={utils} {...childNode} />
32
+          ))}
29 33
       </>
30 34
     )
31 35
   }

+ 1
- 0
packages/core/src/components/shape/shape.test.tsx Parādīt failu

@@ -7,6 +7,7 @@ describe('shape', () => {
7 7
     renderWithSvg(
8 8
       <Shape
9 9
         shape={mockUtils.box.create({})}
10
+        utils={mockUtils[mockUtils.box.type]}
10 11
         isEditing={false}
11 12
         isBinding={false}
12 13
         isHovered={false}

+ 4
- 6
packages/core/src/components/shape/shape.tsx Parādīt failu

@@ -1,22 +1,20 @@
1 1
 import * as React from 'react'
2
-import { useShapeEvents, useTLContext } from '+hooks'
3
-import type { IShapeTreeNode } from '+types'
2
+import { useShapeEvents } from '+hooks'
3
+import type { IShapeTreeNode, TLShape, TLShapeUtil } from '+types'
4 4
 import { RenderedShape } from './rendered-shape'
5 5
 import { EditingTextShape } from './editing-text-shape'
6 6
 
7 7
 export const Shape = <M extends Record<string, unknown>>({
8 8
   shape,
9
+  utils,
9 10
   isEditing,
10 11
   isBinding,
11 12
   isHovered,
12 13
   isSelected,
13 14
   isCurrentParent,
14 15
   meta,
15
-}: IShapeTreeNode<M>) => {
16
-  const { shapeUtils } = useTLContext()
16
+}: { utils: TLShapeUtil<TLShape> } & IShapeTreeNode<M>) => {
17 17
   const events = useShapeEvents(shape.id, isCurrentParent)
18
-  const utils = shapeUtils[shape.type]
19
-
20 18
   const center = utils.getCenter(shape)
21 19
   const rotation = (shape.rotation || 0) * (180 / Math.PI)
22 20
   const transform = `rotate(${rotation}, ${center}) translate(${shape.point})`

+ 5
- 12
packages/core/src/hooks/useBoundsEvents.tsx Parādīt failu

@@ -7,6 +7,7 @@ export function useBoundsEvents() {
7 7
   const onPointerDown = React.useCallback(
8 8
     (e: React.PointerEvent) => {
9 9
       if (e.button !== 0) return
10
+      if (!inputs.pointerIsValid(e)) return
10 11
       e.stopPropagation()
11 12
       e.currentTarget?.setPointerCapture(e.pointerId)
12 13
       const info = inputs.pointerDown(e, 'bounds')
@@ -20,6 +21,7 @@ export function useBoundsEvents() {
20 21
   const onPointerUp = React.useCallback(
21 22
     (e: React.PointerEvent) => {
22 23
       if (e.button !== 0) return
24
+      if (!inputs.pointerIsValid(e)) return
23 25
       e.stopPropagation()
24 26
       const isDoubleClick = inputs.isDoubleClick()
25 27
       const info = inputs.pointerUp(e, 'bounds')
@@ -40,8 +42,7 @@ export function useBoundsEvents() {
40 42
 
41 43
   const onPointerMove = React.useCallback(
42 44
     (e: React.PointerEvent) => {
43
-      if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
44
-
45
+      if (!inputs.pointerIsValid(e)) return
45 46
       if (e.currentTarget.hasPointerCapture(e.pointerId)) {
46 47
         callbacks.onDragBounds?.(inputs.pointerMove(e, 'bounds'), e)
47 48
       }
@@ -53,6 +54,7 @@ export function useBoundsEvents() {
53 54
 
54 55
   const onPointerEnter = React.useCallback(
55 56
     (e: React.PointerEvent) => {
57
+      if (!inputs.pointerIsValid(e)) return
56 58
       callbacks.onHoverBounds?.(inputs.pointerEnter(e, 'bounds'), e)
57 59
     },
58 60
     [callbacks, inputs]
@@ -60,26 +62,17 @@ export function useBoundsEvents() {
60 62
 
61 63
   const onPointerLeave = React.useCallback(
62 64
     (e: React.PointerEvent) => {
65
+      if (!inputs.pointerIsValid(e)) return
63 66
       callbacks.onUnhoverBounds?.(inputs.pointerEnter(e, 'bounds'), e)
64 67
     },
65 68
     [callbacks, inputs]
66 69
   )
67 70
 
68
-  const onTouchStart = React.useCallback((e: React.TouchEvent) => {
69
-    e.preventDefault()
70
-  }, [])
71
-
72
-  const onTouchEnd = React.useCallback((e: React.TouchEvent) => {
73
-    e.preventDefault()
74
-  }, [])
75
-
76 71
   return {
77 72
     onPointerDown,
78 73
     onPointerUp,
79 74
     onPointerEnter,
80 75
     onPointerMove,
81 76
     onPointerLeave,
82
-    onTouchStart,
83
-    onTouchEnd,
84 77
   }
85 78
 }

+ 5
- 10
packages/core/src/hooks/useBoundsHandleEvents.tsx Parādīt failu

@@ -8,6 +8,7 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
8 8
   const onPointerDown = React.useCallback(
9 9
     (e: React.PointerEvent) => {
10 10
       if (e.button !== 0) return
11
+      if (!inputs.pointerIsValid(e)) return
11 12
       e.stopPropagation()
12 13
       e.currentTarget?.setPointerCapture(e.pointerId)
13 14
       const info = inputs.pointerDown(e, id)
@@ -21,6 +22,7 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
21 22
   const onPointerUp = React.useCallback(
22 23
     (e: React.PointerEvent) => {
23 24
       if (e.button !== 0) return
25
+      if (!inputs.pointerIsValid(e)) return
24 26
       e.stopPropagation()
25 27
       const isDoubleClick = inputs.isDoubleClick()
26 28
       const info = inputs.pointerUp(e, id)
@@ -41,6 +43,7 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
41 43
 
42 44
   const onPointerMove = React.useCallback(
43 45
     (e: React.PointerEvent) => {
46
+      if (!inputs.pointerIsValid(e)) return
44 47
       if (e.currentTarget.hasPointerCapture(e.pointerId)) {
45 48
         callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e)
46 49
       }
@@ -52,6 +55,7 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
52 55
 
53 56
   const onPointerEnter = React.useCallback(
54 57
     (e: React.PointerEvent) => {
58
+      if (!inputs.pointerIsValid(e)) return
55 59
       callbacks.onHoverBoundsHandle?.(inputs.pointerEnter(e, id), e)
56 60
     },
57 61
     [inputs, callbacks, id]
@@ -59,26 +63,17 @@ export function useBoundsHandleEvents(id: TLBoundsCorner | TLBoundsEdge | 'rotat
59 63
 
60 64
   const onPointerLeave = React.useCallback(
61 65
     (e: React.PointerEvent) => {
66
+      if (!inputs.pointerIsValid(e)) return
62 67
       callbacks.onUnhoverBoundsHandle?.(inputs.pointerEnter(e, id), e)
63 68
     },
64 69
     [inputs, callbacks, id]
65 70
   )
66 71
 
67
-  const onTouchStart = React.useCallback((e: React.TouchEvent) => {
68
-    e.preventDefault()
69
-  }, [])
70
-
71
-  const onTouchEnd = React.useCallback((e: React.TouchEvent) => {
72
-    e.preventDefault()
73
-  }, [])
74
-
75 72
   return {
76 73
     onPointerDown,
77 74
     onPointerUp,
78 75
     onPointerEnter,
79 76
     onPointerMove,
80 77
     onPointerLeave,
81
-    onTouchStart,
82
-    onTouchEnd,
83 78
   }
84 79
 }

+ 3
- 0
packages/core/src/hooks/useCanvasEvents.tsx Parādīt failu

@@ -7,6 +7,7 @@ export function useCanvasEvents() {
7 7
   const onPointerDown = React.useCallback(
8 8
     (e: React.PointerEvent) => {
9 9
       if (e.button !== 0) return
10
+      if (!inputs.pointerIsValid(e)) return
10 11
       e.currentTarget.setPointerCapture(e.pointerId)
11 12
 
12 13
       if (e.button === 0) {
@@ -20,6 +21,7 @@ export function useCanvasEvents() {
20 21
 
21 22
   const onPointerMove = React.useCallback(
22 23
     (e: React.PointerEvent) => {
24
+      if (!inputs.pointerIsValid(e)) return
23 25
       if (e.currentTarget.hasPointerCapture(e.pointerId)) {
24 26
         const info = inputs.pointerMove(e, 'canvas')
25 27
         callbacks.onDragCanvas?.(info, e)
@@ -33,6 +35,7 @@ export function useCanvasEvents() {
33 35
   const onPointerUp = React.useCallback(
34 36
     (e: React.PointerEvent) => {
35 37
       if (e.button !== 0) return
38
+      if (!inputs.pointerIsValid(e)) return
36 39
       const isDoubleClick = inputs.isDoubleClick()
37 40
       const info = inputs.pointerUp(e, 'canvas')
38 41
 

+ 5
- 10
packages/core/src/hooks/useHandleEvents.tsx Parādīt failu

@@ -7,6 +7,7 @@ export function useHandleEvents(id: string) {
7 7
   const onPointerDown = React.useCallback(
8 8
     (e: React.PointerEvent) => {
9 9
       if (e.button !== 0) return
10
+      if (!inputs.pointerIsValid(e)) return
10 11
       e.stopPropagation()
11 12
       e.currentTarget?.setPointerCapture(e.pointerId)
12 13
 
@@ -20,6 +21,7 @@ export function useHandleEvents(id: string) {
20 21
   const onPointerUp = React.useCallback(
21 22
     (e: React.PointerEvent) => {
22 23
       if (e.button !== 0) return
24
+      if (!inputs.pointerIsValid(e)) return
23 25
       e.stopPropagation()
24 26
       const isDoubleClick = inputs.isDoubleClick()
25 27
       const info = inputs.pointerUp(e, id)
@@ -40,6 +42,7 @@ export function useHandleEvents(id: string) {
40 42
 
41 43
   const onPointerMove = React.useCallback(
42 44
     (e: React.PointerEvent) => {
45
+      if (!inputs.pointerIsValid(e)) return
43 46
       if (e.currentTarget.hasPointerCapture(e.pointerId)) {
44 47
         const info = inputs.pointerMove(e, id)
45 48
         callbacks.onDragHandle?.(info, e)
@@ -52,6 +55,7 @@ export function useHandleEvents(id: string) {
52 55
 
53 56
   const onPointerEnter = React.useCallback(
54 57
     (e: React.PointerEvent) => {
58
+      if (!inputs.pointerIsValid(e)) return
55 59
       const info = inputs.pointerEnter(e, id)
56 60
       callbacks.onHoverHandle?.(info, e)
57 61
     },
@@ -60,27 +64,18 @@ export function useHandleEvents(id: string) {
60 64
 
61 65
   const onPointerLeave = React.useCallback(
62 66
     (e: React.PointerEvent) => {
67
+      if (!inputs.pointerIsValid(e)) return
63 68
       const info = inputs.pointerEnter(e, id)
64 69
       callbacks.onUnhoverHandle?.(info, e)
65 70
     },
66 71
     [inputs, callbacks, id]
67 72
   )
68 73
 
69
-  const onTouchStart = React.useCallback((e: React.TouchEvent) => {
70
-    e.preventDefault()
71
-  }, [])
72
-
73
-  const onTouchEnd = React.useCallback((e: React.TouchEvent) => {
74
-    e.preventDefault()
75
-  }, [])
76
-
77 74
   return {
78 75
     onPointerDown,
79 76
     onPointerUp,
80 77
     onPointerEnter,
81 78
     onPointerMove,
82 79
     onPointerLeave,
83
-    onTouchStart,
84
-    onTouchEnd,
85 80
   }
86 81
 }

+ 24
- 13
packages/core/src/hooks/useResizeObserver.ts Parādīt failu

@@ -1,32 +1,37 @@
1 1
 import { useTLContext } from '+hooks'
2 2
 import * as React from 'react'
3
+import { Utils } from '+utils'
3 4
 
4 5
 export function useResizeObserver<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
5 6
   const { inputs } = useTLContext()
6 7
 
7
-  React.useEffect(() => {
8
-    function handleScroll() {
9
-      const rect = ref.current?.getBoundingClientRect()
10
-      if (rect) {
11
-        inputs.offset = [rect.left, rect.top]
12
-      }
8
+  const updateOffsets = React.useCallback(() => {
9
+    const rect = ref.current?.getBoundingClientRect()
10
+    if (rect) {
11
+      inputs.offset = [rect.left, rect.top]
12
+      inputs.size = [rect.width, rect.height]
13 13
     }
14
+  }, [ref])
14 15
 
15
-    window.addEventListener('scroll', handleScroll)
16
+  React.useEffect(() => {
17
+    const debouncedUpdateOffsets = Utils.debounce(updateOffsets, 100)
18
+    window.addEventListener('scroll', debouncedUpdateOffsets)
19
+    window.addEventListener('resize', debouncedUpdateOffsets)
20
+    updateOffsets()
16 21
     return () => {
17
-      window.removeEventListener('scroll', handleScroll)
22
+      window.removeEventListener('scroll', debouncedUpdateOffsets)
23
+      window.removeEventListener('resize', debouncedUpdateOffsets)
18 24
     }
19 25
   }, [inputs])
20 26
 
21 27
   React.useEffect(() => {
22 28
     const resizeObserver = new ResizeObserver((entries) => {
23
-      if (inputs.isPinching) return
29
+      if (inputs.isPinching) {
30
+        return
31
+      }
24 32
 
25 33
       if (entries[0].contentRect) {
26
-        const rect = ref.current?.getBoundingClientRect()
27
-        if (rect) {
28
-          inputs.offset = [rect.left, rect.top]
29
-        }
34
+        updateOffsets()
30 35
       }
31 36
     })
32 37
 
@@ -38,4 +43,10 @@ export function useResizeObserver<T extends HTMLElement | SVGElement>(ref: React
38 43
       resizeObserver.disconnect()
39 44
     }
40 45
   }, [ref, inputs])
46
+
47
+  React.useEffect(() => {
48
+    setTimeout(() => {
49
+      updateOffsets()
50
+    })
51
+  }, [ref])
41 52
 }

+ 5
- 10
packages/core/src/hooks/useShapeEvents.tsx Parādīt failu

@@ -8,6 +8,7 @@ export function useShapeEvents(id: string, disable = false) {
8 8
   const onPointerDown = React.useCallback(
9 9
     (e: React.PointerEvent) => {
10 10
       if (disable) return
11
+      if (!inputs.pointerIsValid(e)) return
11 12
 
12 13
       if (e.button === 2) {
13 14
         callbacks.onRightPointShape?.(inputs.pointerDown(e, id), e)
@@ -43,6 +44,7 @@ export function useShapeEvents(id: string, disable = false) {
43 44
   const onPointerUp = React.useCallback(
44 45
     (e: React.PointerEvent) => {
45 46
       if (e.button !== 0) return
47
+      if (!inputs.pointerIsValid(e)) return
46 48
       if (disable) return
47 49
       e.stopPropagation()
48 50
       const isDoubleClick = inputs.isDoubleClick()
@@ -64,6 +66,7 @@ export function useShapeEvents(id: string, disable = false) {
64 66
 
65 67
   const onPointerMove = React.useCallback(
66 68
     (e: React.PointerEvent) => {
69
+      if (!inputs.pointerIsValid(e)) return
67 70
       if (disable) return
68 71
 
69 72
       if (inputs.pointer && e.pointerId !== inputs.pointer.pointerId) return
@@ -81,6 +84,7 @@ export function useShapeEvents(id: string, disable = false) {
81 84
 
82 85
   const onPointerEnter = React.useCallback(
83 86
     (e: React.PointerEvent) => {
87
+      if (!inputs.pointerIsValid(e)) return
84 88
       if (disable) return
85 89
       const info = inputs.pointerEnter(e, id)
86 90
       callbacks.onHoverShape?.(info, e)
@@ -91,27 +95,18 @@ export function useShapeEvents(id: string, disable = false) {
91 95
   const onPointerLeave = React.useCallback(
92 96
     (e: React.PointerEvent) => {
93 97
       if (disable) return
98
+      if (!inputs.pointerIsValid(e)) return
94 99
       const info = inputs.pointerEnter(e, id)
95 100
       callbacks.onUnhoverShape?.(info, e)
96 101
     },
97 102
     [inputs, callbacks, id, disable]
98 103
   )
99 104
 
100
-  const onTouchStart = React.useCallback((e: React.TouchEvent) => {
101
-    e.preventDefault()
102
-  }, [])
103
-
104
-  const onTouchEnd = React.useCallback((e: React.TouchEvent) => {
105
-    e.preventDefault()
106
-  }, [])
107
-
108 105
   return {
109 106
     onPointerDown,
110 107
     onPointerUp,
111 108
     onPointerEnter,
112 109
     onPointerMove,
113 110
     onPointerLeave,
114
-    onTouchStart,
115
-    onTouchEnd,
116 111
   }
117 112
 }

+ 52
- 33
packages/core/src/hooks/useShapeTree.tsx Parādīt failu

@@ -1,3 +1,4 @@
1
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
1 2
 import * as React from 'react'
2 3
 import type {
3 4
   IShapeTreeNode,
@@ -7,6 +8,7 @@ import type {
7 8
   TLShapeUtils,
8 9
   TLCallbacks,
9 10
   TLBinding,
11
+  TLBounds,
10 12
 } from '+types'
11 13
 import { Utils, Vec } from '+utils'
12 14
 
@@ -52,28 +54,32 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
52 54
   }
53 55
 }
54 56
 
57
+function shapeIsInViewport(shape: TLShape, bounds: TLBounds, viewport: TLBounds) {
58
+  return Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds)
59
+}
60
+
55 61
 export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
56 62
   page: TLPage<T, TLBinding>,
57 63
   pageState: TLPageState,
58 64
   shapeUtils: TLShapeUtils<T>,
65
+  size: number[],
59 66
   meta?: M,
60 67
   onChange?: TLCallbacks['onChange']
61 68
 ) {
69
+  const rTimeout = React.useRef<unknown>()
62 70
   const rPreviousCount = React.useRef(0)
63
-
64
-  if (typeof window === 'undefined') return []
71
+  const rShapesIdsToRender = React.useRef(new Set<string>())
72
+  const rShapesToRender = React.useRef(new Set<TLShape>())
65 73
 
66 74
   const { selectedIds, camera } = pageState
67 75
 
68
-  // Find viewport
76
+  // Filter the page's shapes down to only those that:
77
+  // - are the direct child of the page
78
+  // - collide with or are contained by the viewport
79
+  // - OR are selected
69 80
 
70 81
   const [minX, minY] = Vec.sub(Vec.div([0, 0], camera.zoom), camera.point)
71
-
72
-  const [maxX, maxY] = Vec.sub(
73
-    Vec.div([window.innerWidth, window.innerHeight], camera.zoom),
74
-    camera.point
75
-  )
76
-
82
+  const [maxX, maxY] = Vec.sub(Vec.div(size, camera.zoom), camera.point)
77 83
   const viewport = {
78 84
     minX,
79 85
     minY,
@@ -83,28 +89,43 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
83 89
     width: maxY - minY,
84 90
   }
85 91
 
86
-  // Filter shapes that are in view, and that are the direct child of
87
-  // the page. Other shapes are not visible, or will be rendered as
88
-  // the children of groups.
89
-
90
-  const shapesToRender = Object.values(page.shapes).filter((shape) => {
91
-    if (shape.parentId !== page.id) return false
92
-
93
-    // Don't hide selected shapes (this breaks certain drag interactions)
94
-    if (selectedIds.includes(shape.id)) return true
95
-
96
-    const shapeBounds = shapeUtils[shape.type as T['type']].getBounds(shape)
97
-
98
-    return Utils.boundsContain(viewport, shapeBounds) || Utils.boundsCollide(viewport, shapeBounds)
99
-  })
92
+  const shapesToRender = rShapesToRender.current
93
+  const shapesIdsToRender = rShapesIdsToRender.current
94
+
95
+  shapesToRender.clear()
96
+  shapesIdsToRender.clear()
97
+
98
+  Object.values(page.shapes)
99
+    .filter((shape) => {
100
+      // Don't hide selected shapes (this breaks certain drag interactions)
101
+      if (
102
+        selectedIds.includes(shape.id) ||
103
+        shapeIsInViewport(shape, shapeUtils[shape.type as T['type']].getBounds(shape), viewport)
104
+      ) {
105
+        if (shape.parentId === page.id) {
106
+          shapesIdsToRender.add(shape.id)
107
+          shapesToRender.add(shape)
108
+        } else {
109
+          shapesIdsToRender.add(shape.parentId)
110
+          shapesToRender.add(page.shapes[shape.parentId])
111
+        }
112
+      }
113
+    })
114
+    .sort((a, b) => a.childIndex - b.childIndex)
100 115
 
101 116
   // Call onChange callback when number of rendering shapes changes
102 117
 
103
-  if (shapesToRender.length !== rPreviousCount.current) {
104
-    // Use a timeout to clear call stack, in case the onChange handleer
105
-    // produces a new state change (React won't like that)
106
-    setTimeout(() => onChange?.(shapesToRender.map((shape) => shape.id)), 0)
107
-    rPreviousCount.current = shapesToRender.length
118
+  if (shapesToRender.size !== rPreviousCount.current) {
119
+    // Use a timeout to clear call stack, in case the onChange handler
120
+    // produces a new state change, which could cause nested state
121
+    // changes, which is bad in React.
122
+    if (rTimeout.current) {
123
+      clearTimeout(rTimeout.current as number)
124
+    }
125
+    rTimeout.current = setTimeout(() => {
126
+      onChange?.(Array.from(shapesIdsToRender.values()))
127
+    }, 100)
128
+    rPreviousCount.current = shapesToRender.size
108 129
   }
109 130
 
110 131
   const bindingTargetId = pageState.bindingId ? page.bindings[pageState.bindingId].toId : undefined
@@ -113,11 +134,9 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
113 134
 
114 135
   const tree: IShapeTreeNode<M>[] = []
115 136
 
116
-  shapesToRender
117
-    .sort((a, b) => a.childIndex - b.childIndex)
118
-    .forEach((shape) =>
119
-      addToShapeTree(shape, tree, page.shapes, { ...pageState, bindingTargetId }, meta)
120
-    )
137
+  const info = { ...pageState, bindingTargetId }
138
+
139
+  shapesToRender.forEach((shape) => addToShapeTree(shape, tree, page.shapes, info, meta))
121 140
 
122 141
   return tree
123 142
 }

+ 1
- 0
packages/core/src/hooks/useStyle.tsx Parādīt failu

@@ -201,6 +201,7 @@ const tlcss = css`
201 201
     height: 100%;
202 202
     padding: 0px;
203 203
     margin: 0px;
204
+    touch-action: none;
204 205
     overscroll-behavior: none;
205 206
     overscroll-behavior-x: none;
206 207
     background-color: var(--tl-background);

+ 71
- 58
packages/core/src/hooks/useZoomEvents.ts Parādīt failu

@@ -1,86 +1,99 @@
1
+/* eslint-disable @typescript-eslint/ban-ts-comment */
1 2
 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2 3
 import * as React from 'react'
3 4
 import { useTLContext } from './useTLContext'
4
-import { Vec } from '+utils'
5
-import { useWheel, usePinch } from 'react-use-gesture'
5
+import Utils, { Vec } from '+utils'
6
+import { useGesture } from '@use-gesture/react'
6 7
 
7 8
 // Capture zoom gestures (pinches, wheels and pans)
8 9
 export function useZoomEvents<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
9
-  const rPinchDa = React.useRef<number[] | undefined>(undefined)
10 10
   const rOriginPoint = React.useRef<number[] | undefined>(undefined)
11 11
   const rPinchPoint = React.useRef<number[] | undefined>(undefined)
12
+  const rDelta = React.useRef<number[]>([0, 0])
12 13
 
13 14
   const { inputs, callbacks } = useTLContext()
14 15
 
15
-  useWheel(
16
-    ({ event: e, delta }) => {
17
-      const elm = ref.current
18
-      if (!(e.target === elm || elm?.contains(e.target as Node))) return
19
-
20
-      e.preventDefault()
16
+  React.useEffect(() => {
17
+    const preventGesture = (event: TouchEvent) => {
18
+      event.preventDefault()
19
+    }
21 20
 
22
-      if (Vec.isEqual(delta, [0, 0])) return
21
+    // @ts-ignore
22
+    document.addEventListener('gesturestart', preventGesture)
23
+    // @ts-ignore
24
+    document.addEventListener('gesturechange', preventGesture)
23 25
 
24
-      const info = inputs.pan(delta, e as WheelEvent)
26
+    return () => {
27
+      // @ts-ignore
28
+      document.removeEventListener('gesturestart', preventGesture)
29
+      // @ts-ignore
30
+      document.removeEventListener('gesturechange', preventGesture)
31
+    }
32
+  }, [])
25 33
 
26
-      callbacks.onPan?.(info, e)
27
-    },
34
+  useGesture(
28 35
     {
29
-      domTarget: window,
30
-      eventOptions: { passive: false },
31
-    }
32
-  )
36
+      onWheel: ({ event: e, delta }) => {
37
+        const elm = ref.current
38
+        if (!(e.target === elm || elm?.contains(e.target as Node))) return
39
+        e.preventDefault()
40
+
41
+        if (inputs.isPinching) return
33 42
 
34
-  usePinch(
35
-    ({ pinching, da, origin, event: e }) => {
36
-      const elm = ref.current
37
-      if (!(e.target === elm || elm?.contains(e.target as Node))) return
43
+        if (Vec.isEqual(delta, [0, 0])) return
38 44
 
39
-      const info = inputs.pinch(origin, origin)
45
+        const info = inputs.pan(delta, e as WheelEvent)
46
+        callbacks.onPan?.(info, e)
47
+      },
48
+      onPinchStart: ({ origin, event }) => {
49
+        const elm = ref.current
50
+        if (!(event.target === elm || elm?.contains(event.target as Node))) return
51
+
52
+        const info = inputs.pinch(origin, origin)
53
+        inputs.isPinching = true
54
+        callbacks.onPinchStart?.(info, event)
55
+        rPinchPoint.current = info.point
56
+        rOriginPoint.current = info.origin
57
+        rDelta.current = [0, 0]
58
+      },
59
+      onPinchEnd: ({ origin, event }) => {
60
+        const elm = ref.current
61
+        if (!(event.target === elm || elm?.contains(event.target as Node))) return
62
+
63
+        const info = inputs.pinch(origin, origin)
40 64
 
41
-      if (!pinching) {
42 65
         inputs.isPinching = false
43
-        callbacks.onPinchEnd?.(
44
-          info,
45
-          e as React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent
46
-        )
47
-        rPinchDa.current = undefined
66
+        callbacks.onPinchEnd?.(info, event)
48 67
         rPinchPoint.current = undefined
49 68
         rOriginPoint.current = undefined
50
-        return
51
-      }
69
+        rDelta.current = [0, 0]
70
+      },
71
+      onPinch: ({ delta, origin, event }) => {
72
+        const elm = ref.current
73
+        if (!(event.target === elm || elm?.contains(event.target as Node))) return
74
+        if (!rOriginPoint.current) throw Error('No origin point!')
52 75
 
53
-      if (rPinchPoint.current === undefined) {
54
-        inputs.isPinching = true
55
-        callbacks.onPinchStart?.(
56
-          info,
57
-          e as React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent
76
+        const info = inputs.pinch(origin, rOriginPoint.current)
77
+
78
+        const trueDelta = Vec.sub(info.delta, rDelta.current)
79
+
80
+        rDelta.current = info.delta
81
+
82
+        callbacks.onPinch?.(
83
+          {
84
+            ...info,
85
+            point: info.point,
86
+            origin: rOriginPoint.current,
87
+            delta: [...trueDelta, -delta[0]],
88
+          },
89
+          event
58 90
         )
59
-        rPinchDa.current = da
60
-        rPinchPoint.current = info.point
61
-        rOriginPoint.current = info.point
62
-      }
63
-
64
-      if (!rPinchDa.current) throw Error('No pinch direction!')
65
-      if (!rOriginPoint.current) throw Error('No origin point!')
66
-
67
-      const [distanceDelta] = Vec.sub(rPinchDa.current, da)
68
-
69
-      callbacks.onPinch?.(
70
-        {
71
-          ...info,
72
-          point: origin,
73
-          origin: rOriginPoint.current,
74
-          delta: [...info.delta, distanceDelta],
75
-        },
76
-        e as React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent
77
-      )
78
-
79
-      rPinchDa.current = da
80
-      rPinchPoint.current = origin
91
+
92
+        rPinchPoint.current = origin
93
+      },
81 94
     },
82 95
     {
83
-      domTarget: window,
96
+      target: ref.current,
84 97
       eventOptions: { passive: false },
85 98
     }
86 99
   )

+ 49
- 5
packages/core/src/inputs.ts Parādīt failu

@@ -11,15 +11,32 @@ export class Inputs {
11 11
   isPinching = false
12 12
 
13 13
   offset = [0, 0]
14
+  size = [10, 10]
14 15
 
15 16
   pointerUpTime = 0
16 17
 
18
+  activePointer?: number
19
+
20
+  pointerIsValid(e: TouchEvent | React.TouchEvent | PointerEvent | React.PointerEvent) {
21
+    if ('pointerId' in e) {
22
+      if (this.activePointer && this.activePointer !== e.pointerId) return false
23
+    }
24
+
25
+    if ('touches' in e) {
26
+      const touch = e.changedTouches[0]
27
+      if (this.activePointer && this.activePointer !== touch.identifier) return false
28
+    }
29
+
30
+    return true
31
+  }
32
+
17 33
   touchStart<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
18 34
     const { shiftKey, ctrlKey, metaKey, altKey } = e
19
-    e.preventDefault()
20 35
 
21 36
     const touch = e.changedTouches[0]
22 37
 
38
+    this.activePointer = touch.identifier
39
+
23 40
     const info: TLPointerInfo<T> = {
24 41
       target,
25 42
       pointerId: touch.identifier,
@@ -38,9 +55,33 @@ export class Inputs {
38 55
     return info
39 56
   }
40 57
 
58
+  touchEnd<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
59
+    const { shiftKey, ctrlKey, metaKey, altKey } = e
60
+
61
+    const touch = e.changedTouches[0]
62
+
63
+    const info: TLPointerInfo<T> = {
64
+      target,
65
+      pointerId: touch.identifier,
66
+      origin: Inputs.getPoint(touch),
67
+      delta: [0, 0],
68
+      point: Inputs.getPoint(touch),
69
+      pressure: Inputs.getPressure(touch),
70
+      shiftKey,
71
+      ctrlKey,
72
+      metaKey: Utils.isDarwin() ? metaKey : ctrlKey,
73
+      altKey,
74
+    }
75
+
76
+    this.pointer = info
77
+
78
+    this.activePointer = undefined
79
+
80
+    return info
81
+  }
82
+
41 83
   touchMove<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
42 84
     const { shiftKey, ctrlKey, metaKey, altKey } = e
43
-    e.preventDefault()
44 85
 
45 86
     const touch = e.changedTouches[0]
46 87
 
@@ -74,6 +115,8 @@ export class Inputs {
74 115
 
75 116
     const point = Inputs.getPoint(e, this.offset)
76 117
 
118
+    this.activePointer = e.pointerId
119
+
77 120
     const info: TLPointerInfo<T> = {
78 121
       target,
79 122
       pointerId: e.pointerId,
@@ -155,6 +198,8 @@ export class Inputs {
155 198
 
156 199
     const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
157 200
 
201
+    this.activePointer = undefined
202
+
158 203
     const info: TLPointerInfo<T> = {
159 204
       origin: point,
160 205
       ...prev,
@@ -277,14 +322,12 @@ export class Inputs {
277 322
   pinch(point: number[], origin: number[]) {
278 323
     const { shiftKey, ctrlKey, metaKey, altKey } = this.keys
279 324
 
280
-    const prev = this.pointer
281
-
282 325
     const delta = Vec.sub(origin, point)
283 326
 
284 327
     const info: TLPointerInfo<'pinch'> = {
285 328
       pointerId: 0,
286 329
       target: 'pinch',
287
-      origin: prev?.origin || Vec.sub(Vec.round(point), this.offset),
330
+      origin,
288 331
       delta: delta,
289 332
       point: Vec.sub(Vec.round(point), this.offset),
290 333
       pressure: 0.5,
@@ -303,6 +346,7 @@ export class Inputs {
303 346
     this.pointerUpTime = 0
304 347
     this.pointer = undefined
305 348
     this.keyboard = undefined
349
+    this.activePointer = undefined
306 350
     this.keys = {}
307 351
   }
308 352
 

+ 7
- 1
packages/core/src/types.ts Parādīt failu

@@ -98,7 +98,13 @@ export type TLWheelEventHandler = (
98 98
 ) => void
99 99
 export type TLPinchEventHandler = (
100 100
   info: TLPointerInfo<string>,
101
-  e: React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent
101
+  e:
102
+    | React.WheelEvent<Element>
103
+    | WheelEvent
104
+    | React.TouchEvent<Element>
105
+    | TouchEvent
106
+    | React.PointerEvent<Element>
107
+    | PointerEventInit
102 108
 ) => void
103 109
 export type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void
104 110
 export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void

+ 10
- 6
packages/core/src/utils/utils.ts Parādīt failu

@@ -1639,7 +1639,7 @@ left past the initial left edge) then swap points on that axis.
1639 1639
   /**
1640 1640
    * Debounce a function.
1641 1641
    */
1642
-  static debounce<T extends (...args: unknown[]) => void>(fn: T, ms = 0) {
1642
+  static debounce<T extends (...args: any[]) => void>(fn: T, ms = 0) {
1643 1643
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
1644 1644
     let timeoutId: number | any
1645 1645
     return function (...args: Parameters<T>) {
@@ -1655,18 +1655,22 @@ left past the initial left edge) then swap points on that axis.
1655 1655
   static getSvgPathFromStroke(stroke: number[][]): string {
1656 1656
     if (!stroke.length) return ''
1657 1657
 
1658
+    const max = stroke.length - 1
1659
+
1658 1660
     const d = stroke.reduce(
1659 1661
       (acc, [x0, y0], i, arr) => {
1660
-        const [x1, y1] = arr[(i + 1) % arr.length]
1662
+        if (i === max) return acc
1663
+        const [x1, y1] = arr[i + 1]
1661 1664
         acc.push(` ${x0},${y0} ${(x0 + x1) / 2},${(y0 + y1) / 2}`)
1662 1665
         return acc
1663 1666
       },
1664 1667
       ['M ', `${stroke[0][0]},${stroke[0][1]}`, ' Q']
1665 1668
     )
1666 1669
 
1667
-    d.push(' Z')
1668
-
1669
-    return d.join('').replaceAll(/(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g, '$1')
1670
+    return d
1671
+      .concat('Z')
1672
+      .join('')
1673
+      .replaceAll(/(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g, '$1')
1670 1674
   }
1671 1675
 
1672 1676
   /* -------------------------------------------------- */
@@ -1702,7 +1706,7 @@ left past the initial left edge) then swap points on that axis.
1702 1706
 
1703 1707
         // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1704 1708
         // @ts-ignore
1705
-        lastResult = func.apply(this, ...args)
1709
+        lastResult = func(...args)
1706 1710
       }
1707 1711
 
1708 1712
       return lastResult

+ 6
- 7
packages/dev/esbuild.config.mjs Parādīt failu

@@ -9,13 +9,11 @@ if (!fs.existsSync('./dist')) {
9 9
   fs.mkdirSync('./dist')
10 10
 }
11 11
 
12
-fs.copyFile('./src/styles.css', './dist/styles.css', (err) => {
13
-  if (err) throw err
14
-})
15
-
16
-fs.copyFile('./src/index.html', './dist/index.html', (err) => {
17
-  if (err) throw err
18
-})
12
+for (const file of ['styles.css', 'index.html']) {
13
+  fs.copyFile(`./src/${file}`, './dist/${file}', (err) => {
14
+    if (err) throw err
15
+  })
16
+}
19 17
 
20 18
 esbuild
21 19
   .build({
@@ -25,6 +23,7 @@ esbuild
25 23
     minify: false,
26 24
     sourcemap: true,
27 25
     incremental: isDevServer,
26
+    platform: 'browser',
28 27
     target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
29 28
     define: {
30 29
       'process.env.NODE_ENV': isDevServer ? '"development"' : '"production"',

+ 4
- 1
packages/dev/package.json Parādīt failu

@@ -21,12 +21,15 @@
21 21
     "@tldraw/tldraw": "^0.0.85",
22 22
     "idb": "^6.1.2",
23 23
     "react": "^17.0.2",
24
-    "react-dom": "^17.0.2"
24
+    "react-dom": "^17.0.2",
25
+    "react-router": "^5.2.1",
26
+    "react-router-dom": "^5.3.0"
25 27
   },
26 28
   "devDependencies": {
27 29
     "@types/node": "^14.14.35",
28 30
     "@types/react": "^17.0.3",
29 31
     "@types/react-dom": "^17.0.2",
32
+    "@types/react-router-dom": "^5.1.8",
30 33
     "concurrently": "6.0.1",
31 34
     "create-serve": "1.0.1",
32 35
     "esbuild": "0.11.5",

+ 43
- 2
packages/dev/src/app.tsx Parādīt failu

@@ -1,9 +1,50 @@
1 1
 import * as React from 'react'
2
+import { Switch, Route, Link } from 'react-router-dom'
2 3
 import Basic from './basic'
3 4
 import Controlled from './controlled'
4 5
 import Imperative from './imperative'
5
-import Small from './small'
6
+import Embedded from './embedded'
7
+import ChangingId from './changing-id'
6 8
 
7 9
 export default function App(): JSX.Element {
8
-  return <Small />
10
+  return (
11
+    <main>
12
+      <Switch>
13
+        <Route path="/basic">
14
+          <Basic />
15
+        </Route>
16
+        <Route path="/controlled">
17
+          <Controlled />
18
+        </Route>
19
+        <Route path="/imperative">
20
+          <Imperative />
21
+        </Route>
22
+        <Route path="/changing-id">
23
+          <ChangingId />
24
+        </Route>
25
+        <Route path="/embedded">
26
+          <Embedded />
27
+        </Route>
28
+        <Route path="/">
29
+          <ul>
30
+            <li>
31
+              <Link to="/basic">basic</Link>
32
+            </li>
33
+            <li>
34
+              <Link to="/controlled">controlled</Link>
35
+            </li>
36
+            <li>
37
+              <Link to="/imperative">imperative</Link>
38
+            </li>
39
+            <li>
40
+              <Link to="/changing-id">changing id</Link>
41
+            </li>
42
+            <li>
43
+              <Link to="/embedded">embedded</Link>
44
+            </li>
45
+          </ul>
46
+        </Route>
47
+      </Switch>
48
+    </main>
49
+  )
9 50
 }

+ 1
- 1
packages/dev/src/basic.tsx Parādīt failu

@@ -1,6 +1,6 @@
1 1
 import * as React from 'react'
2 2
 import Editor from './components/editor'
3 3
 
4
-export default function BasicUsage(): JSX.Element {
4
+export default function Basic(): JSX.Element {
5 5
   return <Editor />
6 6
 }

packages/dev/src/newId.tsx → packages/dev/src/changing-id.tsx Parādīt failu

@@ -1,7 +1,7 @@
1 1
 import * as React from 'react'
2 2
 import { TLDraw } from '@tldraw/tldraw'
3 3
 
4
-export default function NewId() {
4
+export default function ChangingId() {
5 5
   const [id, setId] = React.useState('example')
6 6
 
7 7
   React.useEffect(() => {

packages/dev/src/small.tsx → packages/dev/src/embedded.tsx Parādīt failu

@@ -1,7 +1,7 @@
1 1
 import * as React from 'react'
2 2
 import Editor from './components/editor'
3 3
 
4
-export default function BasicUsage(): JSX.Element {
4
+export default function Embedded(): JSX.Element {
5 5
   return (
6 6
     <div>
7 7
       <div

+ 0
- 1
packages/dev/src/index.html Parādīt failu

@@ -2,7 +2,6 @@
2 2
 <html lang="en">
3 3
   <head>
4 4
     <meta charset="utf-8" />
5
-    <link rel="icon" href="favicon.ico" />
6 5
     <link rel="stylesheet" href="styles.css" />
7 6
     <meta name="viewport" content="width=device-width, initial-scale=1" />
8 7
     <title>tldraw</title>

+ 4
- 1
packages/dev/src/index.tsx Parādīt failu

@@ -1,10 +1,13 @@
1 1
 import React from 'react'
2 2
 import ReactDOM from 'react-dom'
3 3
 import App from './app'
4
+import { HashRouter } from 'react-router-dom'
4 5
 
5 6
 ReactDOM.render(
6 7
   <React.StrictMode>
7
-    <App />
8
+    <HashRouter>
9
+      <App />
10
+    </HashRouter>
8 11
   </React.StrictMode>,
9 12
   document.getElementById('root')
10 13
 )

+ 0
- 1
packages/tldraw/src/state/session/sessions/text/text.session.ts Parādīt failu

@@ -90,7 +90,6 @@ export class TextSession implements Session {
90 90
 
91 91
     // if (initialShape.text.trim() === '' && shape.text.trim() === '') {
92 92
     //   // delete shape
93
-    //   console.log('deleting shape')
94 93
     //   return {
95 94
     //     id: 'text',
96 95
     //     before: {

+ 6
- 3
packages/tldraw/src/state/tlstate.ts Parādīt failu

@@ -1005,7 +1005,7 @@ export class TLDrawState extends StateManager<Data> {
1005 1005
    */
1006 1006
   pinchZoom = (point: number[], delta: number[], zoomDelta: number): this => {
1007 1007
     const { camera } = this.pageState
1008
-    const nextPoint = Vec.add(camera.point, Vec.div(delta, camera.zoom))
1008
+    const nextPoint = Vec.sub(camera.point, Vec.div(delta, camera.zoom))
1009 1009
     const nextZoom = TLDR.getCameraZoom(camera.zoom - zoomDelta * camera.zoom)
1010 1010
     const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
1011 1011
     const p1 = Vec.sub(Vec.div(point, nextZoom), nextPoint)
@@ -2227,6 +2227,9 @@ export class TLDrawState extends StateManager<Data> {
2227 2227
   /* ------------- Renderer Event Handlers ------------ */
2228 2228
 
2229 2229
   onPinchStart: TLPinchEventHandler = () => {
2230
+    if (this.session) {
2231
+      this.cancelSession()
2232
+    }
2230 2233
     this.setStatus(TLDrawStatus.Pinching)
2231 2234
   }
2232 2235
 
@@ -2236,13 +2239,13 @@ export class TLDrawState extends StateManager<Data> {
2236 2239
     //   const nextZoom = TLDR.getCameraZoom(i * 0.25)
2237 2240
     //   this.zoomTo(nextZoom, inputs.pointer?.point)
2238 2241
     // }
2239
-    this.setStatus(this.appState.status.previous)
2242
+    this.setStatus(TLDrawStatus.Idle)
2240 2243
   }
2241 2244
 
2242 2245
   onPinch: TLPinchEventHandler = (info) => {
2243 2246
     if (this.appState.status.current !== TLDrawStatus.Pinching) return
2244 2247
 
2245
-    this.pinchZoom(info.origin, info.delta, info.delta[2] / 350)
2248
+    this.pinchZoom(info.point, info.delta, info.delta[2])
2246 2249
     this.updateOnPointerMove(info)
2247 2250
   }
2248 2251
 

+ 552
- 424
yarn.lock
Failā izmaiņas netiks attēlotas, jo tās ir par lielu
Parādīt failu


Notiek ielāde…
Atcelt
Saglabāt