Browse Source

Improves pan and zoom gestures

main
Steve Ruiz 3 years ago
parent
commit
b00e0d3a95

+ 147191
- 0
.yarn/releases/yarn-1.19.0.cjs
File diff suppressed because it is too large
View File


+ 5
- 0
.yarnrc View File

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 View File

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

+ 1
- 1
packages/core/package.json View File

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

+ 2
- 3
packages/core/src/components/canvas/canvas.tsx View File

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

+ 3
- 3
packages/core/src/components/page/page.tsx View File

27
   hideIndicators,
27
   hideIndicators,
28
   meta,
28
   meta,
29
 }: PageProps<T>): JSX.Element {
29
 }: PageProps<T>): JSX.Element {
30
-  const { callbacks, shapeUtils } = useTLContext()
30
+  const { callbacks, shapeUtils, inputs } = useTLContext()
31
 
31
 
32
   useRenderOnResize()
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
   const { shapeWithHandles } = useHandles(page, pageState)
36
   const { shapeWithHandles } = useHandles(page, pageState)
37
 
37
 
47
     <>
47
     <>
48
       {bounds && !hideBounds && <BoundsBg bounds={bounds} rotation={rotation} />}
48
       {bounds && !hideBounds && <BoundsBg bounds={bounds} rotation={rotation} />}
49
       {shapeTree.map((node) => (
49
       {shapeTree.map((node) => (
50
-        <ShapeNode key={node.shape.id} {...node} />
50
+        <ShapeNode key={node.shape.id} utils={shapeUtils} {...node} />
51
       ))}
51
       ))}
52
       {bounds && !hideBounds && (
52
       {bounds && !hideBounds && (
53
         <Bounds zoom={zoom} bounds={bounds} isLocked={isLocked} rotation={rotation} />
53
         <Bounds zoom={zoom} bounds={bounds} isLocked={isLocked} rotation={rotation} />

+ 7
- 3
packages/core/src/components/shape/shape-node.tsx View File

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

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

+ 4
- 6
packages/core/src/components/shape/shape.tsx View File

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

+ 5
- 12
packages/core/src/hooks/useBoundsEvents.tsx View File

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

+ 5
- 10
packages/core/src/hooks/useBoundsHandleEvents.tsx View File

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

+ 3
- 0
packages/core/src/hooks/useCanvasEvents.tsx View File

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

+ 5
- 10
packages/core/src/hooks/useHandleEvents.tsx View File

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

+ 24
- 13
packages/core/src/hooks/useResizeObserver.ts View File

1
 import { useTLContext } from '+hooks'
1
 import { useTLContext } from '+hooks'
2
 import * as React from 'react'
2
 import * as React from 'react'
3
+import { Utils } from '+utils'
3
 
4
 
4
 export function useResizeObserver<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
5
 export function useResizeObserver<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
5
   const { inputs } = useTLContext()
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
     return () => {
21
     return () => {
17
-      window.removeEventListener('scroll', handleScroll)
22
+      window.removeEventListener('scroll', debouncedUpdateOffsets)
23
+      window.removeEventListener('resize', debouncedUpdateOffsets)
18
     }
24
     }
19
   }, [inputs])
25
   }, [inputs])
20
 
26
 
21
   React.useEffect(() => {
27
   React.useEffect(() => {
22
     const resizeObserver = new ResizeObserver((entries) => {
28
     const resizeObserver = new ResizeObserver((entries) => {
23
-      if (inputs.isPinching) return
29
+      if (inputs.isPinching) {
30
+        return
31
+      }
24
 
32
 
25
       if (entries[0].contentRect) {
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
       resizeObserver.disconnect()
43
       resizeObserver.disconnect()
39
     }
44
     }
40
   }, [ref, inputs])
45
   }, [ref, inputs])
46
+
47
+  React.useEffect(() => {
48
+    setTimeout(() => {
49
+      updateOffsets()
50
+    })
51
+  }, [ref])
41
 }
52
 }

+ 5
- 10
packages/core/src/hooks/useShapeEvents.tsx View File

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

+ 52
- 33
packages/core/src/hooks/useShapeTree.tsx View File

1
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
1
 import * as React from 'react'
2
 import * as React from 'react'
2
 import type {
3
 import type {
3
   IShapeTreeNode,
4
   IShapeTreeNode,
7
   TLShapeUtils,
8
   TLShapeUtils,
8
   TLCallbacks,
9
   TLCallbacks,
9
   TLBinding,
10
   TLBinding,
11
+  TLBounds,
10
 } from '+types'
12
 } from '+types'
11
 import { Utils, Vec } from '+utils'
13
 import { Utils, Vec } from '+utils'
12
 
14
 
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
 export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
61
 export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
56
   page: TLPage<T, TLBinding>,
62
   page: TLPage<T, TLBinding>,
57
   pageState: TLPageState,
63
   pageState: TLPageState,
58
   shapeUtils: TLShapeUtils<T>,
64
   shapeUtils: TLShapeUtils<T>,
65
+  size: number[],
59
   meta?: M,
66
   meta?: M,
60
   onChange?: TLCallbacks['onChange']
67
   onChange?: TLCallbacks['onChange']
61
 ) {
68
 ) {
69
+  const rTimeout = React.useRef<unknown>()
62
   const rPreviousCount = React.useRef(0)
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
   const { selectedIds, camera } = pageState
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
   const [minX, minY] = Vec.sub(Vec.div([0, 0], camera.zoom), camera.point)
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
   const viewport = {
83
   const viewport = {
78
     minX,
84
     minX,
79
     minY,
85
     minY,
83
     width: maxY - minY,
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
   // Call onChange callback when number of rendering shapes changes
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
   const bindingTargetId = pageState.bindingId ? page.bindings[pageState.bindingId].toId : undefined
131
   const bindingTargetId = pageState.bindingId ? page.bindings[pageState.bindingId].toId : undefined
113
 
134
 
114
   const tree: IShapeTreeNode<M>[] = []
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
   return tree
141
   return tree
123
 }
142
 }

+ 1
- 0
packages/core/src/hooks/useStyle.tsx View File

201
     height: 100%;
201
     height: 100%;
202
     padding: 0px;
202
     padding: 0px;
203
     margin: 0px;
203
     margin: 0px;
204
+    touch-action: none;
204
     overscroll-behavior: none;
205
     overscroll-behavior: none;
205
     overscroll-behavior-x: none;
206
     overscroll-behavior-x: none;
206
     background-color: var(--tl-background);
207
     background-color: var(--tl-background);

+ 71
- 58
packages/core/src/hooks/useZoomEvents.ts View File

1
+/* eslint-disable @typescript-eslint/ban-ts-comment */
1
 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2
 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2
 import * as React from 'react'
3
 import * as React from 'react'
3
 import { useTLContext } from './useTLContext'
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
 // Capture zoom gestures (pinches, wheels and pans)
8
 // Capture zoom gestures (pinches, wheels and pans)
8
 export function useZoomEvents<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
9
 export function useZoomEvents<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
9
-  const rPinchDa = React.useRef<number[] | undefined>(undefined)
10
   const rOriginPoint = React.useRef<number[] | undefined>(undefined)
10
   const rOriginPoint = React.useRef<number[] | undefined>(undefined)
11
   const rPinchPoint = React.useRef<number[] | undefined>(undefined)
11
   const rPinchPoint = React.useRef<number[] | undefined>(undefined)
12
+  const rDelta = React.useRef<number[]>([0, 0])
12
 
13
 
13
   const { inputs, callbacks } = useTLContext()
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
         inputs.isPinching = false
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
         rPinchPoint.current = undefined
67
         rPinchPoint.current = undefined
49
         rOriginPoint.current = undefined
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
       eventOptions: { passive: false },
97
       eventOptions: { passive: false },
85
     }
98
     }
86
   )
99
   )

+ 49
- 5
packages/core/src/inputs.ts View File

11
   isPinching = false
11
   isPinching = false
12
 
12
 
13
   offset = [0, 0]
13
   offset = [0, 0]
14
+  size = [10, 10]
14
 
15
 
15
   pointerUpTime = 0
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
   touchStart<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
33
   touchStart<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
18
     const { shiftKey, ctrlKey, metaKey, altKey } = e
34
     const { shiftKey, ctrlKey, metaKey, altKey } = e
19
-    e.preventDefault()
20
 
35
 
21
     const touch = e.changedTouches[0]
36
     const touch = e.changedTouches[0]
22
 
37
 
38
+    this.activePointer = touch.identifier
39
+
23
     const info: TLPointerInfo<T> = {
40
     const info: TLPointerInfo<T> = {
24
       target,
41
       target,
25
       pointerId: touch.identifier,
42
       pointerId: touch.identifier,
38
     return info
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
   touchMove<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
83
   touchMove<T extends string>(e: TouchEvent | React.TouchEvent, target: T): TLPointerInfo<T> {
42
     const { shiftKey, ctrlKey, metaKey, altKey } = e
84
     const { shiftKey, ctrlKey, metaKey, altKey } = e
43
-    e.preventDefault()
44
 
85
 
45
     const touch = e.changedTouches[0]
86
     const touch = e.changedTouches[0]
46
 
87
 
74
 
115
 
75
     const point = Inputs.getPoint(e, this.offset)
116
     const point = Inputs.getPoint(e, this.offset)
76
 
117
 
118
+    this.activePointer = e.pointerId
119
+
77
     const info: TLPointerInfo<T> = {
120
     const info: TLPointerInfo<T> = {
78
       target,
121
       target,
79
       pointerId: e.pointerId,
122
       pointerId: e.pointerId,
155
 
198
 
156
     const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
199
     const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
157
 
200
 
201
+    this.activePointer = undefined
202
+
158
     const info: TLPointerInfo<T> = {
203
     const info: TLPointerInfo<T> = {
159
       origin: point,
204
       origin: point,
160
       ...prev,
205
       ...prev,
277
   pinch(point: number[], origin: number[]) {
322
   pinch(point: number[], origin: number[]) {
278
     const { shiftKey, ctrlKey, metaKey, altKey } = this.keys
323
     const { shiftKey, ctrlKey, metaKey, altKey } = this.keys
279
 
324
 
280
-    const prev = this.pointer
281
-
282
     const delta = Vec.sub(origin, point)
325
     const delta = Vec.sub(origin, point)
283
 
326
 
284
     const info: TLPointerInfo<'pinch'> = {
327
     const info: TLPointerInfo<'pinch'> = {
285
       pointerId: 0,
328
       pointerId: 0,
286
       target: 'pinch',
329
       target: 'pinch',
287
-      origin: prev?.origin || Vec.sub(Vec.round(point), this.offset),
330
+      origin,
288
       delta: delta,
331
       delta: delta,
289
       point: Vec.sub(Vec.round(point), this.offset),
332
       point: Vec.sub(Vec.round(point), this.offset),
290
       pressure: 0.5,
333
       pressure: 0.5,
303
     this.pointerUpTime = 0
346
     this.pointerUpTime = 0
304
     this.pointer = undefined
347
     this.pointer = undefined
305
     this.keyboard = undefined
348
     this.keyboard = undefined
349
+    this.activePointer = undefined
306
     this.keys = {}
350
     this.keys = {}
307
   }
351
   }
308
 
352
 

+ 7
- 1
packages/core/src/types.ts View File

98
 ) => void
98
 ) => void
99
 export type TLPinchEventHandler = (
99
 export type TLPinchEventHandler = (
100
   info: TLPointerInfo<string>,
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
 ) => void
108
 ) => void
103
 export type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void
109
 export type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void
104
 export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void
110
 export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void

+ 10
- 6
packages/core/src/utils/utils.ts View File

1639
   /**
1639
   /**
1640
    * Debounce a function.
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
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
1643
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
1644
     let timeoutId: number | any
1644
     let timeoutId: number | any
1645
     return function (...args: Parameters<T>) {
1645
     return function (...args: Parameters<T>) {
1655
   static getSvgPathFromStroke(stroke: number[][]): string {
1655
   static getSvgPathFromStroke(stroke: number[][]): string {
1656
     if (!stroke.length) return ''
1656
     if (!stroke.length) return ''
1657
 
1657
 
1658
+    const max = stroke.length - 1
1659
+
1658
     const d = stroke.reduce(
1660
     const d = stroke.reduce(
1659
       (acc, [x0, y0], i, arr) => {
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
         acc.push(` ${x0},${y0} ${(x0 + x1) / 2},${(y0 + y1) / 2}`)
1664
         acc.push(` ${x0},${y0} ${(x0 + x1) / 2},${(y0 + y1) / 2}`)
1662
         return acc
1665
         return acc
1663
       },
1666
       },
1664
       ['M ', `${stroke[0][0]},${stroke[0][1]}`, ' Q']
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
 
1706
 
1703
         // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1707
         // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1704
         // @ts-ignore
1708
         // @ts-ignore
1705
-        lastResult = func.apply(this, ...args)
1709
+        lastResult = func(...args)
1706
       }
1710
       }
1707
 
1711
 
1708
       return lastResult
1712
       return lastResult

+ 6
- 7
packages/dev/esbuild.config.mjs View File

9
   fs.mkdirSync('./dist')
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
 esbuild
18
 esbuild
21
   .build({
19
   .build({
25
     minify: false,
23
     minify: false,
26
     sourcemap: true,
24
     sourcemap: true,
27
     incremental: isDevServer,
25
     incremental: isDevServer,
26
+    platform: 'browser',
28
     target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
27
     target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
29
     define: {
28
     define: {
30
       'process.env.NODE_ENV': isDevServer ? '"development"' : '"production"',
29
       'process.env.NODE_ENV': isDevServer ? '"development"' : '"production"',

+ 4
- 1
packages/dev/package.json View File

21
     "@tldraw/tldraw": "^0.0.85",
21
     "@tldraw/tldraw": "^0.0.85",
22
     "idb": "^6.1.2",
22
     "idb": "^6.1.2",
23
     "react": "^17.0.2",
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
   "devDependencies": {
28
   "devDependencies": {
27
     "@types/node": "^14.14.35",
29
     "@types/node": "^14.14.35",
28
     "@types/react": "^17.0.3",
30
     "@types/react": "^17.0.3",
29
     "@types/react-dom": "^17.0.2",
31
     "@types/react-dom": "^17.0.2",
32
+    "@types/react-router-dom": "^5.1.8",
30
     "concurrently": "6.0.1",
33
     "concurrently": "6.0.1",
31
     "create-serve": "1.0.1",
34
     "create-serve": "1.0.1",
32
     "esbuild": "0.11.5",
35
     "esbuild": "0.11.5",

+ 43
- 2
packages/dev/src/app.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
+import { Switch, Route, Link } from 'react-router-dom'
2
 import Basic from './basic'
3
 import Basic from './basic'
3
 import Controlled from './controlled'
4
 import Controlled from './controlled'
4
 import Imperative from './imperative'
5
 import Imperative from './imperative'
5
-import Small from './small'
6
+import Embedded from './embedded'
7
+import ChangingId from './changing-id'
6
 
8
 
7
 export default function App(): JSX.Element {
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 View File

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

packages/dev/src/newId.tsx → packages/dev/src/changing-id.tsx View File

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

packages/dev/src/small.tsx → packages/dev/src/embedded.tsx View File

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

+ 0
- 1
packages/dev/src/index.html View File

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

+ 4
- 1
packages/dev/src/index.tsx View File

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

+ 0
- 1
packages/tldraw/src/state/session/sessions/text/text.session.ts View File

90
 
90
 
91
     // if (initialShape.text.trim() === '' && shape.text.trim() === '') {
91
     // if (initialShape.text.trim() === '' && shape.text.trim() === '') {
92
     //   // delete shape
92
     //   // delete shape
93
-    //   console.log('deleting shape')
94
     //   return {
93
     //   return {
95
     //     id: 'text',
94
     //     id: 'text',
96
     //     before: {
95
     //     before: {

+ 6
- 3
packages/tldraw/src/state/tlstate.ts View File

1005
    */
1005
    */
1006
   pinchZoom = (point: number[], delta: number[], zoomDelta: number): this => {
1006
   pinchZoom = (point: number[], delta: number[], zoomDelta: number): this => {
1007
     const { camera } = this.pageState
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
     const nextZoom = TLDR.getCameraZoom(camera.zoom - zoomDelta * camera.zoom)
1009
     const nextZoom = TLDR.getCameraZoom(camera.zoom - zoomDelta * camera.zoom)
1010
     const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
1010
     const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
1011
     const p1 = Vec.sub(Vec.div(point, nextZoom), nextPoint)
1011
     const p1 = Vec.sub(Vec.div(point, nextZoom), nextPoint)
2227
   /* ------------- Renderer Event Handlers ------------ */
2227
   /* ------------- Renderer Event Handlers ------------ */
2228
 
2228
 
2229
   onPinchStart: TLPinchEventHandler = () => {
2229
   onPinchStart: TLPinchEventHandler = () => {
2230
+    if (this.session) {
2231
+      this.cancelSession()
2232
+    }
2230
     this.setStatus(TLDrawStatus.Pinching)
2233
     this.setStatus(TLDrawStatus.Pinching)
2231
   }
2234
   }
2232
 
2235
 
2236
     //   const nextZoom = TLDR.getCameraZoom(i * 0.25)
2239
     //   const nextZoom = TLDR.getCameraZoom(i * 0.25)
2237
     //   this.zoomTo(nextZoom, inputs.pointer?.point)
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
   onPinch: TLPinchEventHandler = (info) => {
2245
   onPinch: TLPinchEventHandler = (info) => {
2243
     if (this.appState.status.current !== TLDrawStatus.Pinching) return
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
     this.updateOnPointerMove(info)
2249
     this.updateOnPointerMove(info)
2247
   }
2250
   }
2248
 
2251
 

+ 552
- 424
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save