Browse Source

Improves undo/redo, fixes pinching and multitouch

main
Steve Ruiz 3 years ago
parent
commit
76a4ccdfcb

+ 12
- 10
components/canvas/bounds/bounds-bg.tsx View File

@@ -1,27 +1,29 @@
1
-import { useCallback, useRef } from "react"
2
-import state, { useSelector } from "state"
3
-import inputs from "state/inputs"
4
-import styled from "styles"
5
-import { getPage } from "utils/utils"
1
+import { useCallback, useRef } from 'react'
2
+import state, { useSelector } from 'state'
3
+import inputs from 'state/inputs'
4
+import styled from 'styles'
5
+import { getPage } from 'utils/utils'
6 6
 
7 7
 function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
8 8
   if (e.buttons !== 1) return
9
+  if (!inputs.canAccept(e.pointerId)) return
9 10
   e.stopPropagation()
10 11
   e.currentTarget.setPointerCapture(e.pointerId)
11
-  state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
12
+  state.send('POINTED_BOUNDS', inputs.pointerDown(e, 'bounds'))
12 13
 }
13 14
 
14 15
 function handlePointerUp(e: React.PointerEvent<SVGRectElement>) {
15 16
   if (e.buttons !== 1) return
17
+  if (!inputs.canAccept(e.pointerId)) return
16 18
   e.stopPropagation()
17 19
   e.currentTarget.releasePointerCapture(e.pointerId)
18
-  state.send("STOPPED_POINTING", inputs.pointerUp(e))
20
+  state.send('STOPPED_POINTING', inputs.pointerUp(e))
19 21
 }
20 22
 
21 23
 export default function BoundsBg() {
22 24
   const rBounds = useRef<SVGRectElement>(null)
23 25
   const bounds = useSelector((state) => state.values.selectedBounds)
24
-  const isSelecting = useSelector((s) => s.isIn("selecting"))
26
+  const isSelecting = useSelector((s) => s.isIn('selecting'))
25 27
   const rotation = useSelector((s) => {
26 28
     if (s.data.selectedIds.size === 1) {
27 29
       const { shapes } = getPage(s.data)
@@ -53,6 +55,6 @@ export default function BoundsBg() {
53 55
   )
54 56
 }
55 57
 
56
-const StyledBoundsBg = styled("rect", {
57
-  fill: "$boundsBg",
58
+const StyledBoundsBg = styled('rect', {
59
+  fill: '$boundsBg',
58 60
 })

+ 14
- 2
components/canvas/canvas.tsx View File

@@ -21,15 +21,26 @@ export default function Canvas() {
21 21
   const isReady = useSelector((s) => s.isIn('ready'))
22 22
 
23 23
   const handlePointerDown = useCallback((e: React.PointerEvent) => {
24
+    if (!inputs.canAccept(e.pointerId)) return
24 25
     rCanvas.current.setPointerCapture(e.pointerId)
25 26
     state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
26 27
   }, [])
27 28
 
29
+  const handleTouchStart = useCallback((e: React.TouchEvent) => {
30
+    if (e.touches.length === 2) {
31
+      state.send('TOUCH_UNDO')
32
+    }
33
+  }, [])
34
+
28 35
   const handlePointerMove = useCallback((e: React.PointerEvent) => {
29
-    state.send('MOVED_POINTER', inputs.pointerMove(e))
36
+    if (!inputs.canAccept(e.pointerId)) return
37
+    if (inputs.canAccept(e.pointerId)) {
38
+      state.send('MOVED_POINTER', inputs.pointerMove(e))
39
+    }
30 40
   }, [])
31 41
 
32 42
   const handlePointerUp = useCallback((e: React.PointerEvent) => {
43
+    if (!inputs.canAccept(e.pointerId)) return
33 44
     rCanvas.current.releasePointerCapture(e.pointerId)
34 45
     state.send('STOPPED_POINTING', { id: 'canvas', ...inputs.pointerUp(e) })
35 46
   }, [])
@@ -41,14 +52,15 @@ export default function Canvas() {
41 52
       onPointerDown={handlePointerDown}
42 53
       onPointerMove={handlePointerMove}
43 54
       onPointerUp={handlePointerUp}
55
+      onTouchStart={handleTouchStart}
44 56
     >
45 57
       <Defs />
46 58
       {isReady && (
47 59
         <g ref={rGroup}>
48 60
           <BoundsBg />
49 61
           <Page />
50
-          <Bounds />
51 62
           <Selected />
63
+          <Bounds />
52 64
           <Brush />
53 65
         </g>
54 66
       )}

+ 1
- 2
components/shared.tsx View File

@@ -26,12 +26,11 @@ export const IconButton = styled('button', {
26 26
   '& > svg': {
27 27
     height: '16px',
28 28
     width: '16px',
29
-    // strokeWidth: '2px',
30
-    // stroke: '$text',
31 29
   },
32 30
 
33 31
   variants: {
34 32
     size: {
33
+      small: {},
35 34
       medium: {
36 35
         height: 44,
37 36
         width: 44,

+ 39
- 25
components/status-bar.tsx View File

@@ -1,48 +1,62 @@
1
-import { useStateDesigner } from "@state-designer/react"
2
-import state from "state"
3
-import styled from "styles"
4
-import { useRef } from "react"
1
+import { useStateDesigner } from '@state-designer/react'
2
+import state from 'state'
3
+import styled from 'styles'
4
+import { useRef } from 'react'
5 5
 
6 6
 export default function StatusBar() {
7 7
   const local = useStateDesigner(state)
8 8
   const { count, time } = useRenderCount()
9 9
 
10
-  const active = local.active.slice(1).map((s) => s.split("root.")[1])
10
+  const active = local.active.slice(1).map((s) => s.split('root.')[1])
11 11
   const log = local.log[0]
12 12
 
13 13
   return (
14
-    <StatusBarContainer>
15
-      <Section>{active.join(" | ")}</Section>
14
+    <StatusBarContainer
15
+      size={{
16
+        '@sm': 'small',
17
+      }}
18
+    >
19
+      <Section>{active.join(' | ')}</Section>
16 20
       <Section>| {log}</Section>
17
-      <Section title="Renders | Time">
18
-        {count} | {time.toString().padStart(3, "0")}
19
-      </Section>
21
+      {/* <Section
22
+        title="Renders | Time"
23
+      >
24
+        {count} | {time.toString().padStart(3, '0')}
25
+      </Section> */}
20 26
     </StatusBarContainer>
21 27
   )
22 28
 }
23 29
 
24
-const StatusBarContainer = styled("div", {
25
-  position: "absolute",
30
+const StatusBarContainer = styled('div', {
31
+  position: 'absolute',
26 32
   bottom: 0,
27 33
   left: 0,
28
-  width: "100%",
34
+  width: '100%',
29 35
   height: 40,
30
-  userSelect: "none",
31
-  borderTop: "1px solid black",
32
-  gridArea: "status",
33
-  display: "grid",
34
-  gridTemplateColumns: "auto 1fr auto",
35
-  alignItems: "center",
36
-  backgroundColor: "white",
36
+  userSelect: 'none',
37
+  borderTop: '1px solid black',
38
+  gridArea: 'status',
39
+  display: 'grid',
40
+  gridTemplateColumns: 'auto 1fr auto',
41
+  alignItems: 'center',
42
+  backgroundColor: 'white',
37 43
   gap: 8,
38
-  fontSize: "$1",
39
-  padding: "0 16px",
44
+  fontSize: '$0',
45
+  padding: '0 16px',
40 46
   zIndex: 200,
47
+
48
+  variants: {
49
+    size: {
50
+      small: {
51
+        fontSize: '$1',
52
+      },
53
+    },
54
+  },
41 55
 })
42 56
 
43
-const Section = styled("div", {
44
-  whiteSpace: "nowrap",
45
-  overflow: "hidden",
57
+const Section = styled('div', {
58
+  whiteSpace: 'nowrap',
59
+  overflow: 'hidden',
46 60
 })
47 61
 
48 62
 function useRenderCount() {

+ 98
- 84
components/tools-panel/tools-panel.tsx View File

@@ -52,101 +52,117 @@ export default function ToolsPanel() {
52 52
   return (
53 53
     <OuterContainer>
54 54
       <Zoom />
55
-      <Container>
56
-        <IconButton
57
-          name="select"
58
-          size="large"
59
-          onClick={selectSelectTool}
60
-          isActive={activeTool === 'select'}
61
-        >
62
-          <CursorArrowIcon />
63
-        </IconButton>
64
-      </Container>
65
-      <Container>
66
-        <IconButton
67
-          name={ShapeType.Draw}
68
-          size="large"
69
-          onClick={selectDrawTool}
70
-          isActive={activeTool === ShapeType.Draw}
71
-        >
72
-          <Pencil1Icon />
73
-        </IconButton>
74
-        <IconButton
75
-          name={ShapeType.Rectangle}
76
-          size="large"
77
-          onClick={selectRectangleTool}
78
-          isActive={activeTool === ShapeType.Rectangle}
79
-        >
80
-          <SquareIcon />
81
-        </IconButton>
82
-        <IconButton
83
-          name={ShapeType.Circle}
84
-          size="large"
85
-          onClick={selectCircleTool}
86
-          isActive={activeTool === ShapeType.Circle}
87
-        >
88
-          <CircleIcon />
89
-        </IconButton>
90
-        <IconButton
91
-          name={ShapeType.Ellipse}
92
-          size="large"
93
-          onClick={selectEllipseTool}
94
-          isActive={activeTool === ShapeType.Ellipse}
95
-        >
96
-          <CircleIcon transform="rotate(-45) scale(1, .8)" />
97
-        </IconButton>
98
-        <IconButton
99
-          name={ShapeType.Line}
100
-          size="large"
101
-          onClick={selectLineTool}
102
-          isActive={activeTool === ShapeType.Line}
103
-        >
104
-          <DividerHorizontalIcon transform="rotate(-45)" />
105
-        </IconButton>
106
-        <IconButton
107
-          name={ShapeType.Ray}
108
-          size="large"
109
-          onClick={selectRayTool}
110
-          isActive={activeTool === ShapeType.Ray}
111
-        >
112
-          <SewingPinIcon transform="rotate(-135)" />
113
-        </IconButton>
114
-        <IconButton
115
-          name={ShapeType.Dot}
116
-          size="large"
117
-          onClick={selectDotTool}
118
-          isActive={activeTool === ShapeType.Dot}
119
-        >
120
-          <DotIcon />
121
-        </IconButton>
122
-      </Container>
123
-      <Container>
124
-        <IconButton size="medium" onClick={selectToolLock}>
125
-          {isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
126
-        </IconButton>
127
-        {isPenLocked && (
128
-          <IconButton size="medium" onClick={selectToolLock}>
129
-            <Pencil2Icon />
55
+      <Flex>
56
+        <Container>
57
+          <IconButton
58
+            name="select"
59
+            size={{ '@sm': 'small', '@md': 'large' }}
60
+            onClick={selectSelectTool}
61
+            isActive={activeTool === 'select'}
62
+          >
63
+            <CursorArrowIcon />
130 64
           </IconButton>
131
-        )}
132
-      </Container>
65
+        </Container>
66
+        <Container>
67
+          <IconButton
68
+            name={ShapeType.Draw}
69
+            size={{ '@sm': 'small', '@md': 'large' }}
70
+            onClick={selectDrawTool}
71
+            isActive={activeTool === ShapeType.Draw}
72
+          >
73
+            <Pencil1Icon />
74
+          </IconButton>
75
+          <IconButton
76
+            name={ShapeType.Rectangle}
77
+            size={{ '@sm': 'small', '@md': 'large' }}
78
+            onClick={selectRectangleTool}
79
+            isActive={activeTool === ShapeType.Rectangle}
80
+          >
81
+            <SquareIcon />
82
+          </IconButton>
83
+          <IconButton
84
+            name={ShapeType.Circle}
85
+            size={{ '@sm': 'small', '@md': 'large' }}
86
+            onClick={selectCircleTool}
87
+            isActive={activeTool === ShapeType.Circle}
88
+          >
89
+            <CircleIcon />
90
+          </IconButton>
91
+          <IconButton
92
+            name={ShapeType.Ellipse}
93
+            size={{ '@sm': 'small', '@md': 'large' }}
94
+            onClick={selectEllipseTool}
95
+            isActive={activeTool === ShapeType.Ellipse}
96
+          >
97
+            <CircleIcon transform="rotate(-45) scale(1, .8)" />
98
+          </IconButton>
99
+          <IconButton
100
+            name={ShapeType.Line}
101
+            size={{ '@sm': 'small', '@md': 'large' }}
102
+            onClick={selectLineTool}
103
+            isActive={activeTool === ShapeType.Line}
104
+          >
105
+            <DividerHorizontalIcon transform="rotate(-45)" />
106
+          </IconButton>
107
+          <IconButton
108
+            name={ShapeType.Ray}
109
+            size={{ '@sm': 'small', '@md': 'large' }}
110
+            onClick={selectRayTool}
111
+            isActive={activeTool === ShapeType.Ray}
112
+          >
113
+            <SewingPinIcon transform="rotate(-135)" />
114
+          </IconButton>
115
+          <IconButton
116
+            name={ShapeType.Dot}
117
+            size={{ '@sm': 'small', '@md': 'large' }}
118
+            onClick={selectDotTool}
119
+            isActive={activeTool === ShapeType.Dot}
120
+          >
121
+            <DotIcon />
122
+          </IconButton>
123
+        </Container>
124
+        <Container>
125
+          <IconButton
126
+            size={{ '@sm': 'small', '@md': 'large' }}
127
+            onClick={selectToolLock}
128
+          >
129
+            {isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
130
+          </IconButton>
131
+          {isPenLocked && (
132
+            <IconButton
133
+              size={{ '@sm': 'small', '@md': 'large' }}
134
+              onClick={selectToolLock}
135
+            >
136
+              <Pencil2Icon />
137
+            </IconButton>
138
+          )}
139
+        </Container>
140
+      </Flex>
133 141
       <UndoRedo />
134 142
     </OuterContainer>
135 143
   )
136 144
 }
137 145
 
138
-const Spacer = styled('div', { flexGrow: 2 })
139
-
140 146
 const OuterContainer = styled('div', {
141
-  position: 'relative',
142
-  gridArea: 'tools',
147
+  position: 'fixed',
148
+  bottom: 40,
149
+  left: 0,
150
+  right: 0,
143 151
   padding: '0 8px 12px 8px',
144
-  height: '100%',
145 152
   width: '100%',
146 153
   display: 'flex',
147 154
   alignItems: 'center',
148 155
   justifyContent: 'center',
156
+  flexWrap: 'wrap',
149 157
   gap: 16,
158
+  zIndex: 200,
159
+})
160
+
161
+const Flex = styled('div', {
162
+  display: 'flex',
163
+  '& > *:nth-child(n+2)': {
164
+    marginLeft: 16,
165
+  },
150 166
 })
151 167
 
152 168
 const Container = styled('div', {
@@ -157,8 +173,6 @@ const Container = styled('div', {
157 173
   border: '1px solid $border',
158 174
   pointerEvents: 'all',
159 175
   userSelect: 'none',
160
-  zIndex: 200,
161
-  boxShadow: '0px 2px 25px rgba(0,0,0,.16)',
162 176
   height: '100%',
163 177
   display: 'flex',
164 178
   padding: 4,

+ 10
- 2
components/tools-panel/undo-redo.tsx View File

@@ -9,7 +9,7 @@ const clear = () => state.send('CLEARED_PAGE')
9 9
 
10 10
 export default function UndoRedo() {
11 11
   return (
12
-    <Container>
12
+    <Container size={{ '@sm': 'small' }}>
13 13
       <IconButton onClick={undo}>
14 14
         <RotateCcw />
15 15
       </IconButton>
@@ -25,7 +25,7 @@ export default function UndoRedo() {
25 25
 
26 26
 const Container = styled('div', {
27 27
   position: 'absolute',
28
-  bottom: 12,
28
+  bottom: 64,
29 29
   right: 12,
30 30
   backgroundColor: '$panel',
31 31
   borderRadius: '4px',
@@ -43,4 +43,12 @@ const Container = styled('div', {
43 43
     height: 13,
44 44
     width: 13,
45 45
   },
46
+
47
+  variants: {
48
+    size: {
49
+      small: {
50
+        bottom: 12,
51
+      },
52
+    },
53
+  },
46 54
 })

+ 10
- 2
components/tools-panel/zoom.tsx View File

@@ -10,7 +10,7 @@ const zoomToActual = () => state.send('ZOOMED_TO_ACTUAL')
10 10
 
11 11
 export default function Zoom() {
12 12
   return (
13
-    <Container>
13
+    <Container size={{ '@sm': 'small' }}>
14 14
       <IconButton onClick={zoomOut}>
15 15
         <ZoomOutIcon />
16 16
       </IconButton>
@@ -33,8 +33,8 @@ function ZoomCounter() {
33 33
 
34 34
 const Container = styled('div', {
35 35
   position: 'absolute',
36
-  bottom: 12,
37 36
   left: 12,
37
+  bottom: 64,
38 38
   backgroundColor: '$panel',
39 39
   borderRadius: '4px',
40 40
   overflow: 'hidden',
@@ -50,6 +50,14 @@ const Container = styled('div', {
50 50
   '& svg': {
51 51
     strokeWidth: 0,
52 52
   },
53
+
54
+  variants: {
55
+    size: {
56
+      small: {
57
+        bottom: 12,
58
+      },
59
+    },
60
+  },
53 61
 })
54 62
 
55 63
 const ZoomButton = styled(IconButton, {

+ 11
- 8
hooks/useBoundsHandleEvents.ts View File

@@ -1,18 +1,19 @@
1
-import { useCallback, useRef } from "react"
2
-import inputs from "state/inputs"
3
-import { Edge, Corner } from "types"
1
+import { useCallback, useRef } from 'react'
2
+import inputs from 'state/inputs'
3
+import { Edge, Corner } from 'types'
4 4
 
5
-import state from "../state"
5
+import state from '../state'
6 6
 
7 7
 export default function useBoundsHandleEvents(
8
-  handle: Edge | Corner | "rotate"
8
+  handle: Edge | Corner | 'rotate'
9 9
 ) {
10 10
   const onPointerDown = useCallback(
11 11
     (e) => {
12 12
       if (e.buttons !== 1) return
13
+      if (!inputs.canAccept(e.pointerId)) return
13 14
       e.stopPropagation()
14 15
       e.currentTarget.setPointerCapture(e.pointerId)
15
-      state.send("POINTED_BOUNDS_HANDLE", inputs.pointerDown(e, handle))
16
+      state.send('POINTED_BOUNDS_HANDLE', inputs.pointerDown(e, handle))
16 17
     },
17 18
     [handle]
18 19
   )
@@ -20,18 +21,20 @@ export default function useBoundsHandleEvents(
20 21
   const onPointerMove = useCallback(
21 22
     (e) => {
22 23
       if (e.buttons !== 1) return
24
+      if (!inputs.canAccept(e.pointerId)) return
23 25
       e.stopPropagation()
24
-      state.send("MOVED_POINTER", inputs.pointerMove(e))
26
+      state.send('MOVED_POINTER', inputs.pointerMove(e))
25 27
     },
26 28
     [handle]
27 29
   )
28 30
 
29 31
   const onPointerUp = useCallback((e) => {
30 32
     if (e.buttons !== 1) return
33
+    if (!inputs.canAccept(e.pointerId)) return
31 34
     e.stopPropagation()
32 35
     e.currentTarget.releasePointerCapture(e.pointerId)
33 36
     e.currentTarget.replaceWith(e.currentTarget)
34
-    state.send("STOPPED_POINTING", inputs.pointerUp(e))
37
+    state.send('STOPPED_POINTING', inputs.pointerUp(e))
35 38
   }, [])
36 39
 
37 40
   return { onPointerDown, onPointerMove, onPointerUp }

+ 10
- 3
hooks/useShapeEvents.ts View File

@@ -8,7 +8,8 @@ export default function useShapeEvents(
8 8
 ) {
9 9
   const handlePointerDown = useCallback(
10 10
     (e: React.PointerEvent) => {
11
-      e.stopPropagation()
11
+      if (!inputs.canAccept(e.pointerId)) return
12
+      // e.stopPropagation()
12 13
       rGroup.current.setPointerCapture(e.pointerId)
13 14
       state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
14 15
     },
@@ -17,7 +18,8 @@ export default function useShapeEvents(
17 18
 
18 19
   const handlePointerUp = useCallback(
19 20
     (e: React.PointerEvent) => {
20
-      e.stopPropagation()
21
+      if (!inputs.canAccept(e.pointerId)) return
22
+      // e.stopPropagation()
21 23
       rGroup.current.releasePointerCapture(e.pointerId)
22 24
       state.send('STOPPED_POINTING', inputs.pointerUp(e))
23 25
     },
@@ -26,6 +28,7 @@ export default function useShapeEvents(
26 28
 
27 29
   const handlePointerEnter = useCallback(
28 30
     (e: React.PointerEvent) => {
31
+      if (!inputs.canAccept(e.pointerId)) return
29 32
       state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
30 33
     },
31 34
     [id]
@@ -33,13 +36,17 @@ export default function useShapeEvents(
33 36
 
34 37
   const handlePointerMove = useCallback(
35 38
     (e: React.PointerEvent) => {
39
+      if (!inputs.canAccept(e.pointerId)) return
36 40
       state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
37 41
     },
38 42
     [id]
39 43
   )
40 44
 
41 45
   const handlePointerLeave = useCallback(
42
-    () => state.send('UNHOVERED_SHAPE', { target: id }),
46
+    (e: React.PointerEvent) => {
47
+      if (!inputs.canAccept(e.pointerId)) return
48
+      state.send('UNHOVERED_SHAPE', { target: id })
49
+    },
43 50
     [id]
44 51
   )
45 52
 

+ 47
- 81
hooks/useZoomEvents.ts View File

@@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react'
2 2
 import state from 'state'
3 3
 import inputs from 'state/inputs'
4 4
 import * as vec from 'utils/vec'
5
-import { usePinch } from 'react-use-gesture'
5
+import { useGesture } from 'react-use-gesture'
6 6
 
7 7
 /**
8 8
  * Capture zoom gestures (pinches, wheels and pans) and send to the state.
@@ -12,91 +12,57 @@ import { usePinch } from 'react-use-gesture'
12 12
 export default function useZoomEvents(
13 13
   ref: React.MutableRefObject<SVGSVGElement>
14 14
 ) {
15
-  const rTouchDist = useRef(0)
16
-
17
-  useEffect(() => {
18
-    const element = ref.current
19
-
20
-    if (!element) return
21
-
22
-    function handleWheel(e: WheelEvent) {
23
-      e.preventDefault()
24
-      e.stopPropagation()
25
-
26
-      if (e.ctrlKey) {
27
-        state.send('ZOOMED_CAMERA', {
28
-          delta: e.deltaY,
29
-          ...inputs.wheel(e),
30
-        })
31
-        return
32
-      }
33
-
34
-      state.send('PANNED_CAMERA', {
35
-        delta: [e.deltaX, e.deltaY],
36
-        ...inputs.wheel(e),
37
-      })
38
-    }
39
-
40
-    function handleTouchMove(e: TouchEvent) {
41
-      e.preventDefault()
42
-      e.stopPropagation()
43
-
44
-      if (e.touches.length === 2) {
45
-        const { clientX: x0, clientY: y0 } = e.touches[0]
46
-        const { clientX: x1, clientY: y1 } = e.touches[1]
47
-
48
-        const dist = vec.dist([x0, y0], [x1, y1])
49
-        const point = vec.med([x0, y0], [x1, y1])
50
-
51
-        state.send('WHEELED', {
52
-          delta: dist - rTouchDist.current,
53
-          point,
54
-        })
55
-
56
-        rTouchDist.current = dist
57
-      }
58
-    }
59
-
60
-    element.addEventListener('wheel', handleWheel, { passive: false })
61
-    element.addEventListener('touchstart', handleTouchMove, { passive: false })
62
-    element.addEventListener('touchmove', handleTouchMove, { passive: false })
63
-
64
-    return () => {
65
-      element.removeEventListener('wheel', handleWheel)
66
-      element.removeEventListener('touchstart', handleTouchMove)
67
-      element.removeEventListener('touchmove', handleTouchMove)
68
-    }
69
-  }, [ref])
70
-
71 15
   const rPinchDa = useRef<number[] | undefined>(undefined)
72 16
   const rPinchPoint = useRef<number[] | undefined>(undefined)
73 17
 
74
-  const bind = usePinch(({ pinching, da, origin }) => {
75
-    if (!pinching) {
76
-      state.send('STOPPED_PINCHING')
77
-      rPinchDa.current = undefined
78
-      rPinchPoint.current = undefined
79
-      return
80
-    }
18
+  const bind = useGesture(
19
+    {
20
+      onWheel: ({ event, delta }) => {
21
+        if (event.ctrlKey) {
22
+          state.send('ZOOMED_CAMERA', {
23
+            delta: delta[1],
24
+            ...inputs.wheel(event as WheelEvent),
25
+          })
26
+          return
27
+        }
28
+
29
+        state.send('PANNED_CAMERA', {
30
+          delta,
31
+          ...inputs.wheel(event as WheelEvent),
32
+        })
33
+      },
34
+      onPinch: ({ pinching, da, origin }) => {
35
+        if (!pinching) {
36
+          state.send('STOPPED_PINCHING')
37
+          rPinchDa.current = undefined
38
+          rPinchPoint.current = undefined
39
+          return
40
+        }
41
+
42
+        if (rPinchPoint.current === undefined) {
43
+          state.send('STARTED_PINCHING')
44
+          rPinchDa.current = da
45
+          rPinchPoint.current = origin
46
+        }
47
+
48
+        const [distanceDelta, angleDelta] = vec.sub(rPinchDa.current, da)
49
+
50
+        state.send('PINCHED', {
51
+          delta: vec.sub(rPinchPoint.current, origin),
52
+          point: origin,
53
+          distanceDelta,
54
+          angleDelta,
55
+        })
81 56
 
82
-    if (rPinchPoint.current === undefined) {
83
-      state.send('STARTED_PINCHING')
84
-      rPinchDa.current = da
85
-      rPinchPoint.current = origin
57
+        rPinchDa.current = da
58
+        rPinchPoint.current = origin
59
+      },
60
+    },
61
+    {
62
+      domTarget: document.body,
63
+      eventOptions: { passive: false },
86 64
     }
87
-
88
-    const [distanceDelta, angleDelta] = vec.sub(rPinchDa.current, da)
89
-
90
-    state.send('PINCHED', {
91
-      delta: vec.sub(rPinchPoint.current, origin),
92
-      point: origin,
93
-      distanceDelta,
94
-      angleDelta,
95
-    })
96
-
97
-    rPinchDa.current = da
98
-    rPinchPoint.current = origin
99
-  })
65
+  )
100 66
 
101 67
   return { ...bind() }
102 68
 }

+ 15
- 14
state/history.ts View File

@@ -1,6 +1,6 @@
1
-import { Data } from "types"
2
-import { BaseCommand } from "./commands/command"
3
-import state from "./state"
1
+import { Data } from 'types'
2
+import { BaseCommand } from './commands/command'
3
+import state from './state'
4 4
 
5 5
 // A singleton to manage history changes.
6 6
 
@@ -11,10 +11,11 @@ class BaseHistory<T> {
11 11
   private _enabled = true
12 12
 
13 13
   execute = (data: T, command: BaseCommand<T>) => {
14
+    command.redo(data, true)
15
+
14 16
     if (this.disabled) return
15 17
     this.stack = this.stack.slice(0, this.pointer + 1)
16 18
     this.stack.push(command)
17
-    command.redo(data, true)
18 19
     this.pointer++
19 20
 
20 21
     if (this.stack.length > this.maxLength) {
@@ -26,26 +27,26 @@ class BaseHistory<T> {
26 27
   }
27 28
 
28 29
   undo = (data: T) => {
29
-    if (this.disabled) return
30 30
     if (this.pointer === -1) return
31 31
     const command = this.stack[this.pointer]
32 32
     command.undo(data)
33
+    if (this.disabled) return
33 34
     this.pointer--
34 35
     this.save(data)
35 36
   }
36 37
 
37 38
   redo = (data: T) => {
38
-    if (this.disabled) return
39 39
     if (this.pointer === this.stack.length - 1) return
40 40
     const command = this.stack[this.pointer + 1]
41 41
     command.redo(data, false)
42
+    if (this.disabled) return
42 43
     this.pointer++
43 44
     this.save(data)
44 45
   }
45 46
 
46
-  load(data: T, id = "code_slate_0.0.1") {
47
-    if (typeof window === "undefined") return
48
-    if (typeof localStorage === "undefined") return
47
+  load(data: T, id = 'code_slate_0.0.1') {
48
+    if (typeof window === 'undefined') return
49
+    if (typeof localStorage === 'undefined') return
49 50
 
50 51
     const savedData = localStorage.getItem(id)
51 52
 
@@ -54,9 +55,9 @@ class BaseHistory<T> {
54 55
     }
55 56
   }
56 57
 
57
-  save = (data: T, id = "code_slate_0.0.1") => {
58
-    if (typeof window === "undefined") return
59
-    if (typeof localStorage === "undefined") return
58
+  save = (data: T, id = 'code_slate_0.0.1') => {
59
+    if (typeof window === 'undefined') return
60
+    if (typeof localStorage === 'undefined') return
60 61
 
61 62
     localStorage.setItem(id, JSON.stringify(this.prepareDataForSave(data)))
62 63
   }
@@ -110,14 +111,14 @@ class History extends BaseHistory<Data> {
110 111
     restoredData.selectedIds = new Set(restoredData.selectedIds)
111 112
 
112 113
     // Also restore camera position, which is saved separately in this app
113
-    const cameraInfo = localStorage.getItem("code_slate_camera")
114
+    const cameraInfo = localStorage.getItem('code_slate_camera')
114 115
 
115 116
     if (cameraInfo !== null) {
116 117
       Object.assign(restoredData.camera, JSON.parse(cameraInfo))
117 118
 
118 119
       // And update the CSS property
119 120
       document.documentElement.style.setProperty(
120
-        "--camera-zoom",
121
+        '--camera-zoom',
121 122
         restoredData.camera.zoom.toString()
122 123
       )
123 124
     }

+ 11
- 2
state/inputs.tsx View File

@@ -1,7 +1,8 @@
1
-import { PointerInfo } from "types"
2
-import { isDarwin } from "utils/utils"
1
+import { PointerInfo } from 'types'
2
+import { isDarwin } from 'utils/utils'
3 3
 
4 4
 class Inputs {
5
+  activePointerId?: number
5 6
   points: Record<string, PointerInfo> = {}
6 7
 
7 8
   pointerDown(e: PointerEvent | React.PointerEvent, target: string) {
@@ -19,6 +20,7 @@ class Inputs {
19 20
     }
20 21
 
21 22
     this.points[e.pointerId] = info
23
+    this.activePointerId = e.pointerId
22 24
 
23 25
     return info
24 26
   }
@@ -78,6 +80,7 @@ class Inputs {
78 80
     }
79 81
 
80 82
     delete this.points[e.pointerId]
83
+    delete this.activePointerId
81 84
 
82 85
     return info
83 86
   }
@@ -87,6 +90,12 @@ class Inputs {
87 90
     return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
88 91
   }
89 92
 
93
+  canAccept(pointerId: PointerEvent['pointerId']) {
94
+    return (
95
+      this.activePointerId === undefined || this.activePointerId === pointerId
96
+    )
97
+  }
98
+
90 99
   get pointer() {
91 100
     return this.points[Object.keys(this.points)[0]]
92 101
   }

+ 11
- 0
state/sessions/rotate-session.ts View File

@@ -45,6 +45,11 @@ export default class RotateSession extends BaseSession {
45 45
     for (let { id, center, offset, rotation } of initialShapes) {
46 46
       const shape = page.shapes[id]
47 47
 
48
+      // const rotationOffset = vec.sub(
49
+      //   getBoundsCenter(getShapeBounds(shape)),
50
+      //   getBoundsCenter(getRotatedBounds(shape))
51
+      // )
52
+
48 53
       const nextRotation = isLocked
49 54
         ? clampToRotationToSegments(rotation + rot, 24)
50 55
         : rotation + rot
@@ -100,11 +105,17 @@ export function getRotateSnapshot(data: Data) {
100 105
       const center = getBoundsCenter(bounds)
101 106
       const offset = vec.sub(center, shape.point)
102 107
 
108
+      const rotationOffset = vec.sub(
109
+        center,
110
+        getBoundsCenter(getRotatedBounds(shape))
111
+      )
112
+
103 113
       return {
104 114
         id: shape.id,
105 115
         point: shape.point,
106 116
         rotation: shape.rotation,
107 117
         offset,
118
+        rotationOffset,
108 119
         center,
109 120
       }
110 121
     }),

+ 72
- 52
state/state.ts View File

@@ -69,47 +69,7 @@ const initialData: Data = {
69 69
 const state = createState({
70 70
   data: initialData,
71 71
   on: {
72
-    ZOOMED_CAMERA: {
73
-      do: 'zoomCamera',
74
-    },
75
-    PANNED_CAMERA: {
76
-      do: 'panCamera',
77
-    },
78
-    ZOOMED_TO_ACTUAL: {
79
-      if: 'hasSelection',
80
-      do: 'zoomCameraToSelectionActual',
81
-      else: 'zoomCameraToActual',
82
-    },
83
-    ZOOMED_TO_SELECTION: {
84
-      if: 'hasSelection',
85
-      do: 'zoomCameraToSelection',
86
-    },
87
-    ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
88
-    ZOOMED_IN: 'zoomIn',
89
-    ZOOMED_OUT: 'zoomOut',
90
-    RESET_CAMERA: 'resetCamera',
91
-    TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
92
-    TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
93
-    TOGGLED_SHAPE_ASPECT_LOCK: {
94
-      if: 'hasSelection',
95
-      do: 'aspectLockSelection',
96
-    },
97
-    SELECTED_SELECT_TOOL: { to: 'selecting' },
98
-    SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
99
-    SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
100
-    SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
101
-    SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
102
-    SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
103
-    SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
104
-    SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
105
-    SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
106
-    TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
107
-    TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
108
-    CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
109
-    SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
110
-    NUDGED: { do: 'nudgeSelection' },
111
-    USED_PEN_DEVICE: 'enablePenLock',
112
-    DISABLED_PEN_LOCK: 'disablePenLock',
72
+    UNMOUNTED: [{ unless: 'isReadOnly', do: 'forceSave' }, { to: 'loading' }],
113 73
   },
114 74
   initial: 'loading',
115 75
   states: {
@@ -131,10 +91,48 @@ const state = createState({
131 91
         else: ['zoomCameraToFit', 'zoomCameraToActual'],
132 92
       },
133 93
       on: {
134
-        UNMOUNTED: [
135
-          { unless: 'isReadOnly', do: 'forceSave' },
136
-          { to: 'loading' },
137
-        ],
94
+        ZOOMED_CAMERA: {
95
+          do: 'zoomCamera',
96
+        },
97
+        PANNED_CAMERA: {
98
+          do: 'panCamera',
99
+        },
100
+        ZOOMED_TO_ACTUAL: {
101
+          if: 'hasSelection',
102
+          do: 'zoomCameraToSelectionActual',
103
+          else: 'zoomCameraToActual',
104
+        },
105
+        ZOOMED_TO_SELECTION: {
106
+          if: 'hasSelection',
107
+          do: 'zoomCameraToSelection',
108
+        },
109
+        ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
110
+        ZOOMED_IN: 'zoomIn',
111
+        ZOOMED_OUT: 'zoomOut',
112
+        RESET_CAMERA: 'resetCamera',
113
+        TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
114
+        TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
115
+        TOGGLED_SHAPE_ASPECT_LOCK: {
116
+          if: 'hasSelection',
117
+          do: 'aspectLockSelection',
118
+        },
119
+        SELECTED_SELECT_TOOL: { to: 'selecting' },
120
+        SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
121
+        SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
122
+        SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
123
+        SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
124
+        SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
125
+        SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
126
+        SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
127
+        SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
128
+        TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
129
+        TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
130
+        CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
131
+        SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
132
+        NUDGED: { do: 'nudgeSelection' },
133
+        USED_PEN_DEVICE: 'enablePenLock',
134
+        DISABLED_PEN_LOCK: 'disablePenLock',
135
+        CLEARED_PAGE: ['selectAll', 'deleteSelection'],
138 136
       },
139 137
       initial: 'selecting',
140 138
       states: {
@@ -143,10 +141,8 @@ const state = createState({
143 141
             SAVED: 'forceSave',
144 142
             UNDO: 'undo',
145 143
             REDO: 'redo',
146
-            CLEARED_PAGE: ['selectAll', 'deleteSelection'],
147 144
             SAVED_CODE: 'saveCode',
148 145
             DELETED: 'deleteSelection',
149
-            STARTED_PINCHING: { to: 'pinching' },
150 146
             INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
151 147
             DECREASED_CODE_FONT_SIZE: 'decreaseCodeFontSize',
152 148
             CHANGED_CODE_CONTROL: 'updateControls',
@@ -164,6 +160,7 @@ const state = createState({
164 160
             notPointing: {
165 161
               on: {
166 162
                 CANCELLED: 'clearSelectedIds',
163
+                STARTED_PINCHING: { to: 'pinching' },
167 164
                 POINTED_CANVAS: { to: 'brushSelecting' },
168 165
                 POINTED_BOUNDS: { to: 'pointingBounds' },
169 166
                 POINTED_BOUNDS_HANDLE: {
@@ -269,7 +266,7 @@ const state = createState({
269 266
                 'startBrushSession',
270 267
               ],
271 268
               on: {
272
-                STARTED_PINCHING: { to: 'pinching' },
269
+                STARTED_PINCHING: { do: 'completeSession', to: 'pinching' },
273 270
                 MOVED_POINTER: 'updateBrushSession',
274 271
                 PANNED_CAMERA: 'updateBrushSession',
275 272
                 STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
@@ -280,14 +277,30 @@ const state = createState({
280 277
         },
281 278
         pinching: {
282 279
           on: {
283
-            STOPPED_PINCHING: { to: 'selecting' },
284 280
             PINCHED: { do: 'pinchCamera' },
285 281
           },
282
+          initial: 'selectPinching',
283
+          states: {
284
+            selectPinching: {
285
+              on: {
286
+                STOPPED_PINCHING: { to: 'selecting' },
287
+              },
288
+            },
289
+            toolPinching: {
290
+              on: {
291
+                STOPPED_PINCHING: { to: 'usingTool.previous' },
292
+              },
293
+            },
294
+          },
286 295
         },
287 296
         usingTool: {
288 297
           initial: 'draw',
289 298
           onEnter: 'clearSelectedIds',
290 299
           on: {
300
+            STARTED_PINCHING: {
301
+              do: 'breakSession',
302
+              to: 'pinching.toolPinching',
303
+            },
291 304
             TOGGLED_TOOL_LOCK: 'toggleToolLock',
292 305
           },
293 306
           states: {
@@ -319,7 +332,7 @@ const state = createState({
319 332
                       to: 'draw.creating',
320 333
                     },
321 334
                     CANCELLED: {
322
-                      do: ['cancelSession', 'deleteSelection'],
335
+                      do: 'breakSession',
323 336
                       to: 'selecting',
324 337
                     },
325 338
                     MOVED_POINTER: 'updateDrawSession',
@@ -359,7 +372,7 @@ const state = createState({
359 372
                       },
360 373
                     ],
361 374
                     CANCELLED: {
362
-                      do: ['cancelSession', 'deleteSelection'],
375
+                      do: 'breakSession',
363 376
                       to: 'selecting',
364 377
                     },
365 378
                   },
@@ -545,7 +558,7 @@ const state = createState({
545 558
               },
546 559
             ],
547 560
             CANCELLED: {
548
-              do: ['cancelSession', 'deleteSelection'],
561
+              do: 'breakSession',
549 562
               to: 'selecting',
550 563
             },
551 564
           },
@@ -662,6 +675,13 @@ const state = createState({
662 675
     /* -------------------- Sessions -------------------- */
663 676
 
664 677
     // Shared
678
+    breakSession(data) {
679
+      session?.cancel(data)
680
+      session = undefined
681
+      history.disable()
682
+      commands.deleteSelected(data)
683
+      history.enable()
684
+    },
665 685
     cancelSession(data) {
666 686
       session?.cancel(data)
667 687
       session = undefined

+ 4
- 0
styles/stitches.config.ts View File

@@ -42,6 +42,10 @@ const { styled, global, css, theme, getCssString } = createCss({
42 42
     zIndices: {},
43 43
     transitions: {},
44 44
   },
45
+  media: {
46
+    sm: '(min-width: 640px)',
47
+    md: '(min-width: 768px)',
48
+  },
45 49
   utils: {
46 50
     zDash: () => (value: number) => {
47 51
       return {

Loading…
Cancel
Save