Browse Source

Improves status, create session handling

main
Steve Ruiz 4 years ago
parent
commit
40cbf2d92b

+ 5
- 2
packages/tldraw/src/components/tools-panel/status-bar.tsx View File

@@ -3,16 +3,19 @@ import { useTLDrawContext } from '~hooks'
3 3
 import type { Data } from '~types'
4 4
 import styled from '~styles'
5 5
 
6
+const statusSelector = (s: Data) => s.appState.status.current
6 7
 const activeToolSelector = (s: Data) => s.appState.activeTool
7 8
 
8 9
 export function StatusBar(): JSX.Element | null {
9 10
   const { useSelector } = useTLDrawContext()
11
+  const status = useSelector(statusSelector)
10 12
   const activeTool = useSelector(activeToolSelector)
11 13
 
12 14
   return (
13 15
     <StatusBarContainer size={{ '@sm': 'small' }}>
14
-      <Section>{activeTool}</Section>
15
-      {/* <Section>{shapesInView || '0'} Shapes</Section> */}
16
+      <Section>
17
+        {activeTool} | {status}
18
+      </Section>
16 19
     </StatusBarContainer>
17 20
   )
18 21
 }

+ 234
- 115
packages/tldraw/src/shape/shapes/arrow/arrow.tsx View File

@@ -8,7 +8,6 @@ import {
8 8
   Intersect,
9 9
   TLHandle,
10 10
   TLPointerInfo,
11
-  Svg,
12 11
 } from '@tldraw/core'
13 12
 import getStroke from 'perfect-freehand'
14 13
 import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
@@ -27,7 +26,7 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
27 26
   type = TLDrawShapeType.Arrow as const
28 27
   toolType = TLDrawToolType.Handle
29 28
   canStyleFill = false
30
-  simplePathCache = new WeakMap<ArrowShape, string>()
29
+  simplePathCache = new WeakMap<ArrowShape['handles'], string>()
31 30
   pathCache = new WeakMap<ArrowShape, string>()
32 31
 
33 32
   defaultProps = {
@@ -72,12 +71,63 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
72 71
   }
73 72
 
74 73
   render = (shape: ArrowShape, { isDarkMode }: TLRenderInfo) => {
75
-    const { bend, handles, style } = shape
76
-    const { start, end, bend: _bend } = handles
77
-
78
-    const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
74
+    const {
75
+      handles: { start, bend, end },
76
+      decorations = {},
77
+      style,
78
+    } = shape
79 79
 
80
-    const isDraw = shape.style.dash === DashStyle.Draw
80
+    const isDraw = style.dash === DashStyle.Draw
81
+
82
+    // if (!isDraw) {
83
+    //   const styles = getShapeStyle(style, isDarkMode)
84
+
85
+    //   const { strokeWidth } = styles
86
+
87
+    //   const arrowDist = Vec.dist(start.point, end.point)
88
+
89
+    //   const sw = strokeWidth * 1.618
90
+
91
+    //   const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
92
+    //     arrowDist,
93
+    //     sw,
94
+    //     shape.style.dash,
95
+    //     2
96
+    //   )
97
+
98
+    //   const path = getArrowPath(shape)
99
+
100
+    //   return (
101
+    //     <g pointerEvents="none">
102
+    //       <path
103
+    //         d={path}
104
+    //         fill="none"
105
+    //         stroke="transparent"
106
+    //         strokeWidth={Math.max(8, strokeWidth * 2)}
107
+    //         strokeDasharray="none"
108
+    //         strokeDashoffset="none"
109
+    //         strokeLinecap="round"
110
+    //         strokeLinejoin="round"
111
+    //         pointerEvents="stroke"
112
+    //       />
113
+    //       <path
114
+    //         d={path}
115
+    //         fill={isDraw ? styles.stroke : 'none'}
116
+    //         stroke={styles.stroke}
117
+    //         strokeWidth={sw}
118
+    //         strokeDasharray={strokeDasharray}
119
+    //         strokeDashoffset={strokeDashoffset}
120
+    //         strokeLinecap="round"
121
+    //         strokeLinejoin="round"
122
+    //         pointerEvents="stroke"
123
+    //       />
124
+    //     </g>
125
+    //   )
126
+    // }
127
+
128
+    // TODO: Improve drawn arrows
129
+
130
+    const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
81 131
 
82 132
     const styles = getShapeStyle(style, isDarkMode)
83 133
 
@@ -85,11 +135,11 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
85 135
 
86 136
     const arrowDist = Vec.dist(start.point, end.point)
87 137
 
88
-    const arrowHeadlength = Math.min(arrowDist / 3, strokeWidth * 8)
138
+    const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
89 139
 
90 140
     let shaftPath: JSX.Element
91
-    let insetStart: number[]
92
-    let insetEnd: number[]
141
+    let startArrowHead: { left: number[]; right: number[] } | undefined
142
+    let endArrowHead: { left: number[]; right: number[] } | undefined
93 143
 
94 144
     if (isStraightLine) {
95 145
       const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
@@ -107,8 +157,13 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
107 157
         2
108 158
       )
109 159
 
110
-      insetStart = Vec.nudge(start.point, end.point, arrowHeadlength)
111
-      insetEnd = Vec.nudge(end.point, start.point, arrowHeadlength)
160
+      if (decorations.start) {
161
+        startArrowHead = getStraightArrowHeadPoints(start.point, end.point, arrowHeadLength)
162
+      }
163
+
164
+      if (decorations.end) {
165
+        endArrowHead = getStraightArrowHeadPoints(end.point, start.point, arrowHeadLength)
166
+      }
112 167
 
113 168
       // Straight arrow path
114 169
       shaftPath = (
@@ -144,31 +199,37 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
144 199
       const path = Utils.getFromCache(this.pathCache, shape, () =>
145 200
         isDraw
146 201
           ? renderCurvedFreehandArrowShaft(shape, circle)
147
-          : getArrowArcPath(start, end, circle, bend)
202
+          : getArrowArcPath(start, end, circle, shape.bend)
148 203
       )
149 204
 
150
-      const arcLength = Utils.getArcLength(
151
-        [circle[0], circle[1]],
152
-        circle[2],
153
-        start.point,
154
-        end.point
155
-      )
205
+      const { center, radius, length } = getArrowArc(shape)
156 206
 
157 207
       const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
158
-        arcLength - 1,
208
+        length - 1,
159 209
         sw,
160 210
         shape.style.dash,
161 211
         2
162 212
       )
163 213
 
164
-      const center = [circle[0], circle[1]]
165
-      const radius = circle[2]
166
-      const sa = Vec.angle(center, start.point)
167
-      const ea = Vec.angle(center, end.point)
168
-      const t = arrowHeadlength / Math.abs(arcLength)
214
+      if (decorations.start) {
215
+        startArrowHead = getCurvedArrowHeadPoints(
216
+          start.point,
217
+          arrowHeadLength,
218
+          center,
219
+          radius,
220
+          length < 0
221
+        )
222
+      }
169 223
 
170
-      insetStart = Vec.nudgeAtAngle(center, Utils.lerpAngles(sa, ea, t), radius)
171
-      insetEnd = Vec.nudgeAtAngle(center, Utils.lerpAngles(ea, sa, t), radius)
224
+      if (decorations.end) {
225
+        endArrowHead = getCurvedArrowHeadPoints(
226
+          end.point,
227
+          arrowHeadLength,
228
+          center,
229
+          radius,
230
+          length >= 0
231
+        )
232
+      }
172 233
 
173 234
       // Curved arrow path
174 235
       shaftPath = (
@@ -204,9 +265,9 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
204 265
     return (
205 266
       <g pointerEvents="none">
206 267
         {shaftPath}
207
-        {shape.decorations?.start === Decoration.Arrow && (
268
+        {startArrowHead && (
208 269
           <path
209
-            d={getArrowHeadPath(shape, start.point, insetStart)}
270
+            d={`M ${startArrowHead.left} L ${start.point} ${startArrowHead.right}`}
210 271
             fill="none"
211 272
             stroke={styles.stroke}
212 273
             strokeWidth={sw}
@@ -217,9 +278,9 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
217 278
             pointerEvents="stroke"
218 279
           />
219 280
         )}
220
-        {shape.decorations?.end === Decoration.Arrow && (
281
+        {endArrowHead && (
221 282
           <path
222
-            d={getArrowHeadPath(shape, end.point, insetEnd)}
283
+            d={`M ${endArrowHead.left} L ${end.point} ${endArrowHead.right}`}
223 284
             fill="none"
224 285
             stroke={styles.stroke}
225 286
             strokeWidth={sw}
@@ -235,91 +296,9 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
235 296
   }
236 297
 
237 298
   renderIndicator(shape: ArrowShape) {
238
-    const {
239
-      decorations,
240
-      handles: { start, end, bend: _bend },
241
-      style,
242
-    } = shape
243
-
244
-    const { strokeWidth } = getShapeStyle(style, false)
245
-
246
-    const arrowDist = Vec.dist(start.point, end.point)
247
-
248
-    const arrowHeadlength = Math.min(arrowDist / 3, strokeWidth * 8)
249
-
250
-    const aw = arrowHeadlength / 2
251
-
252
-    const path: (string | number)[] = []
253
-
254
-    const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
255
-
256
-    if (isStraightLine) {
257
-      path.push(Svg.moveTo(start.point), Svg.lineTo(end.point))
258
-
259
-      if (decorations?.start) {
260
-        const point = start.point
261
-        const ints = Intersect.circle.lineSegment(start.point, aw, start.point, end.point).points
262
-        const int = ints[0]
299
+    const path = Utils.getFromCache(this.simplePathCache, shape.handles, () => getArrowPath(shape))
263 300
 
264
-        path.push(
265
-          Svg.moveTo(Vec.nudge(Vec.rotWith(int, point, Math.PI / 6), point, -aw)),
266
-          Svg.lineTo(start.point),
267
-          Svg.lineTo(Vec.nudge(Vec.rotWith(int, point, -Math.PI / 6), point, -aw))
268
-        )
269
-      }
270
-
271
-      if (decorations?.end) {
272
-        const point = end.point
273
-        const ints = Intersect.circle.lineSegment(end.point, aw, start.point, end.point).points
274
-        const int = ints[0]
275
-
276
-        path.push(
277
-          Svg.moveTo(Vec.nudge(Vec.rotWith(int, point, Math.PI / 6), point, -aw)),
278
-          Svg.lineTo(end.point),
279
-          Svg.lineTo(Vec.nudge(Vec.rotWith(int, point, -Math.PI / 6), point, -aw))
280
-        )
281
-      }
282
-    } else {
283
-      const circle = getCtp(shape)
284
-      const center = [circle[0], circle[1]]
285
-      const radius = circle[2]
286
-      const sweep = Utils.getArcLength(center, radius, start.point, end.point) > 0
287
-
288
-      path.push(
289
-        Svg.moveTo(start.point),
290
-        `A ${radius} ${radius} 0 0 ${sweep ? '1' : '0'} ${end.point}`
291
-      )
292
-
293
-      if (decorations?.start) {
294
-        const point = start.point
295
-        const ints = Intersect.circle.circle(center, radius, point, aw).points
296
-        const int = sweep ? ints[0] : ints[1]
297
-
298
-        path.push(
299
-          Svg.moveTo(Vec.nudge(Vec.rotWith(int, point, Math.PI / 6), point, -aw)),
300
-          Svg.lineTo(start.point),
301
-          Svg.lineTo(Vec.nudge(Vec.rotWith(int, point, -Math.PI / 6), point, -aw))
302
-        )
303
-      }
304
-
305
-      if (decorations?.end) {
306
-        const point = end.point
307
-        const ints = Intersect.circle.circle(center, radius, point, aw).points
308
-        const int = sweep ? ints[1] : ints[0]
309
-
310
-        path.push(
311
-          Svg.moveTo(Vec.nudge(Vec.rotWith(int, point, Math.PI / 6), point, -aw)),
312
-          Svg.lineTo(end.point),
313
-          Svg.lineTo(Vec.nudge(Vec.rotWith(int, point, -Math.PI / 6), point, -aw))
314
-        )
315
-      }
316
-    }
317
-
318
-    return (
319
-      <g>
320
-        <path d={path.join()} />
321
-      </g>
322
-    )
301
+    return <path d={path} />
323 302
   }
324 303
 
325 304
   getBounds = (shape: ArrowShape) => {
@@ -764,3 +743,143 @@ function getCtp(shape: ArrowShape) {
764 743
   const { start, end, bend } = shape.handles
765 744
   return Utils.circleFromThreePoints(start.point, end.point, bend.point)
766 745
 }
746
+
747
+function getArrowArc(shape: ArrowShape) {
748
+  const { start, end, bend } = shape.handles
749
+  const [cx, cy, radius] = Utils.circleFromThreePoints(start.point, end.point, bend.point)
750
+  const center = [cx, cy]
751
+  const length = Utils.getArcLength(center, radius, start.point, end.point)
752
+  return { center, radius, length }
753
+}
754
+
755
+function getCurvedArrowHeadPoints(
756
+  A: number[],
757
+  r1: number,
758
+  C: number[],
759
+  r2: number,
760
+  sweep: boolean
761
+) {
762
+  const ints = Intersect.circle.circle(A, r1 * 0.618, C, r2).points
763
+  const int = sweep ? ints[0] : ints[1]
764
+  const left = Vec.nudge(Vec.rotWith(int, A, Math.PI / 6), A, r1 * -0.382)
765
+  const right = Vec.nudge(Vec.rotWith(int, A, -Math.PI / 6), A, r1 * -0.382)
766
+  return { left, right }
767
+}
768
+
769
+function getStraightArrowHeadPoints(A: number[], B: number[], r: number) {
770
+  const ints = Intersect.circle.lineSegment(A, r, A, B).points
771
+  const int = ints[0]
772
+  const left = Vec.rotWith(int, A, Math.PI / 6)
773
+  const right = Vec.rotWith(int, A, -Math.PI / 6)
774
+  return { left, right }
775
+}
776
+
777
+function getCurvedArrowHeadPath(A: number[], r1: number, C: number[], r2: number, sweep: boolean) {
778
+  const { left, right } = getCurvedArrowHeadPoints(A, r1, C, r2, sweep)
779
+  return `M ${left} L ${A} ${right}`
780
+}
781
+
782
+function getStraightArrowHeadPath(A: number[], B: number[], r: number) {
783
+  const { left, right } = getStraightArrowHeadPoints(A, B, r)
784
+  return `M ${left} L ${A} ${right}`
785
+}
786
+
787
+function getArrowPath(shape: ArrowShape) {
788
+  const {
789
+    decorations,
790
+    handles: { start, end, bend: _bend },
791
+    style,
792
+  } = shape
793
+
794
+  const { strokeWidth } = getShapeStyle(style, false)
795
+
796
+  const arrowDist = Vec.dist(start.point, end.point)
797
+
798
+  const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
799
+
800
+  const path: (string | number)[] = []
801
+
802
+  const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
803
+
804
+  if (isStraightLine) {
805
+    // Path (line segment)
806
+    path.push(`M ${start.point} L ${end.point}`)
807
+
808
+    // Start arrow head
809
+    if (decorations?.start) {
810
+      path.push(getStraightArrowHeadPath(start.point, end.point, arrowHeadLength))
811
+    }
812
+
813
+    // End arrow head
814
+    if (decorations?.end) {
815
+      path.push(getStraightArrowHeadPath(end.point, start.point, arrowHeadLength))
816
+    }
817
+  } else {
818
+    const { center, radius, length } = getArrowArc(shape)
819
+
820
+    // Path (arc)
821
+    path.push(`M ${start.point} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end.point}`)
822
+
823
+    // Start Arrow head
824
+    if (decorations?.start) {
825
+      path.push(getCurvedArrowHeadPath(start.point, arrowHeadLength, center, radius, length < 0))
826
+    }
827
+
828
+    // End arrow head
829
+    if (decorations?.end) {
830
+      path.push(getCurvedArrowHeadPath(end.point, arrowHeadLength, center, radius, length >= 0))
831
+    }
832
+  }
833
+
834
+  return path.join(' ')
835
+}
836
+
837
+function getDrawArrowPath(shape: ArrowShape) {
838
+  const {
839
+    decorations,
840
+    handles: { start, end, bend: _bend },
841
+    style,
842
+  } = shape
843
+
844
+  const { strokeWidth } = getShapeStyle(style, false)
845
+
846
+  const arrowDist = Vec.dist(start.point, end.point)
847
+
848
+  const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
849
+
850
+  const path: (string | number)[] = []
851
+
852
+  const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
853
+
854
+  if (isStraightLine) {
855
+    // Path (line segment)
856
+    path.push(`M ${start.point} L ${end.point}`)
857
+
858
+    // Start arrow head
859
+    if (decorations?.start) {
860
+      path.push(getStraightArrowHeadPath(start.point, end.point, arrowHeadLength))
861
+    }
862
+
863
+    // End arrow head
864
+    if (decorations?.end) {
865
+      path.push(getStraightArrowHeadPath(end.point, start.point, arrowHeadLength))
866
+    }
867
+  } else {
868
+    const { center, radius, length } = getArrowArc(shape)
869
+
870
+    // Path (arc)
871
+    path.push(`M ${start.point} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end.point}`)
872
+
873
+    // Start Arrow head
874
+    if (decorations?.start) {
875
+      path.push(getCurvedArrowHeadPath(start.point, arrowHeadLength, center, radius, length < 0))
876
+    }
877
+
878
+    // End arrow head
879
+    if (decorations?.end) {
880
+      path.push(getCurvedArrowHeadPath(end.point, arrowHeadLength, center, radius, length >= 0))
881
+    }
882
+  }
883
+
884
+  return path.join(' ')
885
+}

+ 20
- 20
packages/tldraw/src/state/command/move/move.command.spec.ts View File

@@ -45,11 +45,11 @@ describe('Move command', () => {
45 45
     tlstate.loadDocument(doc)
46 46
     tlstate.setSelectedIds(['b'])
47 47
     tlstate.moveToBack()
48
-    expect(getSortedShapeIds(tlstate.getState())).toBe('bacd')
48
+    expect(getSortedShapeIds(tlstate.data)).toBe('bacd')
49 49
     tlstate.undo()
50
-    expect(getSortedShapeIds(tlstate.getState())).toBe('abcd')
50
+    expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
51 51
     tlstate.redo()
52
-    expect(getSortedShapeIds(tlstate.getState())).toBe('bacd')
52
+    expect(getSortedShapeIds(tlstate.data)).toBe('bacd')
53 53
   })
54 54
 
55 55
   describe('to back', () => {
@@ -57,21 +57,21 @@ describe('Move command', () => {
57 57
       tlstate.loadDocument(doc)
58 58
       tlstate.setSelectedIds(['b'])
59 59
       tlstate.moveToBack()
60
-      expect(getSortedShapeIds(tlstate.getState())).toBe('bacd')
60
+      expect(getSortedShapeIds(tlstate.data)).toBe('bacd')
61 61
     })
62 62
 
63 63
     it('moves two adjacent siblings to back', () => {
64 64
       tlstate.loadDocument(doc)
65 65
       tlstate.setSelectedIds(['b', 'c'])
66 66
       tlstate.moveToBack()
67
-      expect(getSortedShapeIds(tlstate.getState())).toBe('bcad')
67
+      expect(getSortedShapeIds(tlstate.data)).toBe('bcad')
68 68
     })
69 69
 
70 70
     it('moves two non-adjacent siblings to back', () => {
71 71
       tlstate.loadDocument(doc)
72 72
       tlstate.setSelectedIds(['b', 'd'])
73 73
       tlstate.moveToBack()
74
-      expect(getSortedShapeIds(tlstate.getState())).toBe('bdac')
74
+      expect(getSortedShapeIds(tlstate.data)).toBe('bdac')
75 75
     })
76 76
   })
77 77
 
@@ -80,35 +80,35 @@ describe('Move command', () => {
80 80
       tlstate.loadDocument(doc)
81 81
       tlstate.setSelectedIds(['c'])
82 82
       tlstate.moveBackward()
83
-      expect(getSortedShapeIds(tlstate.getState())).toBe('acbd')
83
+      expect(getSortedShapeIds(tlstate.data)).toBe('acbd')
84 84
     })
85 85
 
86 86
     it('moves a shape at first index backward', () => {
87 87
       tlstate.loadDocument(doc)
88 88
       tlstate.setSelectedIds(['a'])
89 89
       tlstate.moveBackward()
90
-      expect(getSortedShapeIds(tlstate.getState())).toBe('abcd')
90
+      expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
91 91
     })
92 92
 
93 93
     it('moves two adjacent siblings backward', () => {
94 94
       tlstate.loadDocument(doc)
95 95
       tlstate.setSelectedIds(['c', 'd'])
96 96
       tlstate.moveBackward()
97
-      expect(getSortedShapeIds(tlstate.getState())).toBe('acdb')
97
+      expect(getSortedShapeIds(tlstate.data)).toBe('acdb')
98 98
     })
99 99
 
100 100
     it('moves two non-adjacent siblings backward', () => {
101 101
       tlstate.loadDocument(doc)
102 102
       tlstate.setSelectedIds(['b', 'd'])
103 103
       tlstate.moveBackward()
104
-      expect(getSortedShapeIds(tlstate.getState())).toBe('badc')
104
+      expect(getSortedShapeIds(tlstate.data)).toBe('badc')
105 105
     })
106 106
 
107 107
     it('moves two adjacent siblings backward at zero index', () => {
108 108
       tlstate.loadDocument(doc)
109 109
       tlstate.setSelectedIds(['a', 'b'])
110 110
       tlstate.moveBackward()
111
-      expect(getSortedShapeIds(tlstate.getState())).toBe('abcd')
111
+      expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
112 112
     })
113 113
   })
114 114
 
@@ -117,7 +117,7 @@ describe('Move command', () => {
117 117
       tlstate.loadDocument(doc)
118 118
       tlstate.setSelectedIds(['c'])
119 119
       tlstate.moveForward()
120
-      expect(getSortedShapeIds(tlstate.getState())).toBe('abdc')
120
+      expect(getSortedShapeIds(tlstate.data)).toBe('abdc')
121 121
     })
122 122
 
123 123
     it('moves a shape forward at the top index', () => {
@@ -126,28 +126,28 @@ describe('Move command', () => {
126 126
       tlstate.moveForward()
127 127
       tlstate.moveForward()
128 128
       tlstate.moveForward()
129
-      expect(getSortedShapeIds(tlstate.getState())).toBe('acdb')
129
+      expect(getSortedShapeIds(tlstate.data)).toBe('acdb')
130 130
     })
131 131
 
132 132
     it('moves two adjacent siblings forward', () => {
133 133
       tlstate.loadDocument(doc)
134 134
       tlstate.setSelectedIds(['a', 'b'])
135 135
       tlstate.moveForward()
136
-      expect(getSortedShapeIds(tlstate.getState())).toBe('cabd')
136
+      expect(getSortedShapeIds(tlstate.data)).toBe('cabd')
137 137
     })
138 138
 
139 139
     it('moves two non-adjacent siblings forward', () => {
140 140
       tlstate.loadDocument(doc)
141 141
       tlstate.setSelectedIds(['a', 'c'])
142 142
       tlstate.moveForward()
143
-      expect(getSortedShapeIds(tlstate.getState())).toBe('badc')
143
+      expect(getSortedShapeIds(tlstate.data)).toBe('badc')
144 144
     })
145 145
 
146 146
     it('moves two adjacent siblings forward at top index', () => {
147 147
       tlstate.loadDocument(doc)
148 148
       tlstate.setSelectedIds(['c', 'd'])
149 149
       tlstate.moveForward()
150
-      expect(getSortedShapeIds(tlstate.getState())).toBe('abcd')
150
+      expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
151 151
     })
152 152
   })
153 153
 
@@ -156,28 +156,28 @@ describe('Move command', () => {
156 156
       tlstate.loadDocument(doc)
157 157
       tlstate.setSelectedIds(['b'])
158 158
       tlstate.moveToFront()
159
-      expect(getSortedShapeIds(tlstate.getState())).toBe('acdb')
159
+      expect(getSortedShapeIds(tlstate.data)).toBe('acdb')
160 160
     })
161 161
 
162 162
     it('moves two adjacent siblings to front', () => {
163 163
       tlstate.loadDocument(doc)
164 164
       tlstate.setSelectedIds(['a', 'b'])
165 165
       tlstate.moveToFront()
166
-      expect(getSortedShapeIds(tlstate.getState())).toBe('cdab')
166
+      expect(getSortedShapeIds(tlstate.data)).toBe('cdab')
167 167
     })
168 168
 
169 169
     it('moves two non-adjacent siblings to front', () => {
170 170
       tlstate.loadDocument(doc)
171 171
       tlstate.setSelectedIds(['a', 'c'])
172 172
       tlstate.moveToFront()
173
-      expect(getSortedShapeIds(tlstate.getState())).toBe('bdac')
173
+      expect(getSortedShapeIds(tlstate.data)).toBe('bdac')
174 174
     })
175 175
 
176 176
     it('moves siblings already at front to front', () => {
177 177
       tlstate.loadDocument(doc)
178 178
       tlstate.setSelectedIds(['c', 'd'])
179 179
       tlstate.moveToFront()
180
-      expect(getSortedShapeIds(tlstate.getState())).toBe('abcd')
180
+      expect(getSortedShapeIds(tlstate.data)).toBe('abcd')
181 181
     })
182 182
   })
183 183
 })

+ 10
- 0
packages/tldraw/src/state/notes.md View File

@@ -33,3 +33,13 @@ When we mutate shapes inside of a command, we:
33 33
 - When the history "does" the command, merge the "redo" data into the current `Data`.
34 34
 - When the history "undoes" the command, merge the "undo" data into the current `Data`.
35 35
 - When the history "redoes" the command, merge the "redo" data into the current `Data`.
36
+
37
+## onChange Events
38
+
39
+When something changes in the state, we need to produce an onChange event that is compatible with multiplayer implementations. This still requires some research, however at minimum we want to include:
40
+
41
+- The current user's id
42
+- The current document id
43
+- The event patch (what's changed)
44
+
45
+The first step would be to implement onChange events for commands. These are already set up as patches and always produce a history entry.

+ 10
- 1
packages/tldraw/src/state/session/sessions/arrow/arrow.session.ts View File

@@ -1,9 +1,18 @@
1
-import type { ArrowBinding, ArrowShape, TLDrawShape, TLDrawBinding, Data, Session } from '~types'
1
+import {
2
+  ArrowBinding,
3
+  ArrowShape,
4
+  TLDrawShape,
5
+  TLDrawBinding,
6
+  Data,
7
+  Session,
8
+  TLDrawStatus,
9
+} from '~types'
2 10
 import { Vec, Utils } from '@tldraw/core'
3 11
 import { TLDR } from '~state/tldr'
4 12
 
5 13
 export class ArrowSession implements Session {
6 14
   id = 'transform_single'
15
+  status = TLDrawStatus.TranslatingHandle
7 16
   newBindingId = Utils.uniqueId()
8 17
   delta = [0, 0]
9 18
   offset = [0, 0]

+ 2
- 1
packages/tldraw/src/state/session/sessions/brush/brush.session.ts View File

@@ -1,10 +1,11 @@
1 1
 import { brushUpdater, Utils, Vec } from '@tldraw/core'
2
-import type { Data, Session } from '~types'
2
+import { Data, Session, TLDrawStatus } from '~types'
3 3
 import { getShapeUtils } from '~shape'
4 4
 import { TLDR } from '~state/tldr'
5 5
 
6 6
 export class BrushSession implements Session {
7 7
   id = 'brush'
8
+  status = TLDrawStatus.Brushing
8 9
   origin: number[]
9 10
   snapshot: BrushSnapshot
10 11
 

+ 2
- 1
packages/tldraw/src/state/session/sessions/draw/draw.session.ts View File

@@ -1,9 +1,10 @@
1 1
 import { Utils, Vec } from '@tldraw/core'
2
-import type { Data, DrawShape, Session } from '~types'
2
+import { Data, DrawShape, Session, TLDrawStatus } from '~types'
3 3
 import { TLDR } from '~state/tldr'
4 4
 
5 5
 export class DrawSession implements Session {
6 6
   id = 'draw'
7
+  status = TLDrawStatus.Creating
7 8
   origin: number[]
8 9
   previous: number[]
9 10
   last: number[]

+ 2
- 1
packages/tldraw/src/state/session/sessions/handle/handle.session.ts View File

@@ -1,11 +1,12 @@
1 1
 import { Vec } from '@tldraw/core'
2
-import type { ShapesWithProp } from '~types'
2
+import { ShapesWithProp, TLDrawStatus } from '~types'
3 3
 import type { Session } from '~types'
4 4
 import type { Data } from '~types'
5 5
 import { TLDR } from '~state/tldr'
6 6
 
7 7
 export class HandleSession implements Session {
8 8
   id = 'transform_single'
9
+  status = TLDrawStatus.TranslatingHandle
9 10
   commandId: string
10 11
   delta = [0, 0]
11 12
   origin: number[]

+ 2
- 1
packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts View File

@@ -1,5 +1,5 @@
1 1
 import { Utils, Vec } from '@tldraw/core'
2
-import type { Session } from '~types'
2
+import { Session, TLDrawStatus } from '~types'
3 3
 import type { Data } from '~types'
4 4
 import { TLDR } from '~state/tldr'
5 5
 
@@ -7,6 +7,7 @@ const PI2 = Math.PI * 2
7 7
 
8 8
 export class RotateSession implements Session {
9 9
   id = 'rotate'
10
+  status = TLDrawStatus.Transforming
10 11
   delta = [0, 0]
11 12
   origin: number[]
12 13
   snapshot: RotateSnapshot

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

@@ -1,10 +1,11 @@
1
-import type { TextShape } from '~types'
1
+import { TextShape, TLDrawStatus } from '~types'
2 2
 import type { Session } from '~types'
3 3
 import type { Data } from '~types'
4 4
 import { TLDR } from '~state/tldr'
5 5
 
6 6
 export class TextSession implements Session {
7 7
   id = 'text'
8
+  status = TLDrawStatus.EditingText
8 9
   initialShape: TextShape
9 10
 
10 11
   constructor(data: Data, id?: string) {

+ 2
- 1
packages/tldraw/src/state/session/sessions/transform-single/transform-single.session.ts View File

@@ -1,11 +1,12 @@
1 1
 import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core'
2
-import type { TLDrawShape } from '~types'
2
+import { TLDrawShape, TLDrawStatus } from '~types'
3 3
 import type { Session } from '~types'
4 4
 import type { Data } from '~types'
5 5
 import { TLDR } from '~state/tldr'
6 6
 
7 7
 export class TransformSingleSession implements Session {
8 8
   id = 'transform_single'
9
+  status = TLDrawStatus.Transforming
9 10
   commandId: string
10 11
   transformType: TLBoundsEdge | TLBoundsCorner
11 12
   origin: number[]

+ 2
- 1
packages/tldraw/src/state/session/sessions/transform/transform.session.ts View File

@@ -1,10 +1,11 @@
1 1
 import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core'
2
-import type { Session } from '~types'
2
+import { Session, TLDrawStatus } from '~types'
3 3
 import type { Data } from '~types'
4 4
 import { TLDR } from '~state/tldr'
5 5
 
6 6
 export class TransformSession implements Session {
7 7
   id = 'transform'
8
+  status = TLDrawStatus.Transforming
8 9
   scaleX = 1
9 10
   scaleY = 1
10 11
   transformType: TLBoundsEdge | TLBoundsCorner

+ 10
- 1
packages/tldraw/src/state/session/sessions/translate/translate.session.ts View File

@@ -1,9 +1,18 @@
1 1
 import { Utils, Vec } from '@tldraw/core'
2
-import type { TLDrawShape, TLDrawBinding, PagePartial, Session, Data, Command } from '~types'
2
+import {
3
+  TLDrawShape,
4
+  TLDrawBinding,
5
+  PagePartial,
6
+  Session,
7
+  Data,
8
+  Command,
9
+  TLDrawStatus,
10
+} from '~types'
3 11
 import { TLDR } from '~state/tldr'
4 12
 
5 13
 export class TranslateSession implements Session {
6 14
   id = 'translate'
15
+  status = TLDrawStatus.Translating
7 16
   delta = [0, 0]
8 17
   prev = [0, 0]
9 18
   origin: number[]

+ 160
- 108
packages/tldraw/src/state/tlstate.ts View File

@@ -62,6 +62,10 @@ const initialData: Data = {
62 62
     isToolLocked: false,
63 63
     isStyleOpen: false,
64 64
     isEmptyCanvas: false,
65
+    status: {
66
+      current: TLDrawStatus.Idle,
67
+      previous: TLDrawStatus.Idle,
68
+    },
65 69
   },
66 70
   page: {
67 71
     id: 'page',
@@ -87,10 +91,6 @@ export class TLDrawState implements TLCallbacks {
87 91
   }
88 92
   clipboard?: TLDrawShape[]
89 93
   session?: Session
90
-  status: { current: TLDrawStatus; previous: TLDrawStatus } = {
91
-    current: 'idle',
92
-    previous: 'idle',
93
-  }
94 94
   pointedId?: string
95 95
   pointedHandle?: string
96 96
   editingId?: string
@@ -99,12 +99,28 @@ export class TLDrawState implements TLCallbacks {
99 99
   currentPageId = 'page'
100 100
   pages: Record<string, TLPage<TLDrawShape, TLDrawBinding>> = { page: initialData.page }
101 101
   pageStates: Record<string, TLPageState> = { page: initialData.pageState }
102
+  isCreating = false
102 103
   _onChange?: (state: TLDrawState, reason: string) => void
103 104
 
104 105
   // Low API
105
-  getState = this.store.getState
106
+  private getState = this.store.getState
106 107
 
107
-  setState = <T extends keyof Data>(data: Partial<Data> | ((data: Data) => Partial<Data>)) => {
108
+  private setStatus(status: TLDrawStatus) {
109
+    this.store.setState((state) => ({
110
+      appState: {
111
+        ...state.appState,
112
+        status: {
113
+          current: status,
114
+          previous: state.appState.status.current,
115
+        },
116
+      },
117
+    }))
118
+  }
119
+
120
+  private setState = <T extends keyof Data>(
121
+    data: Partial<Data> | ((data: Data) => Partial<Data>),
122
+    status?: TLDrawStatus
123
+  ) => {
108 124
     const current = this.getState()
109 125
 
110 126
     // Apply incoming change
@@ -190,6 +206,16 @@ export class TLDrawState implements TLCallbacks {
190 206
       }
191 207
     }
192 208
 
209
+    if (status) {
210
+      next.appState = {
211
+        ...next.appState,
212
+        status: {
213
+          current: status,
214
+          previous: next.appState.status.current,
215
+        },
216
+      }
217
+    }
218
+
193 219
     this.store.setState(next as PartialState<Data, T, T, T>)
194 220
     this.pages[next.page.id] = next.page
195 221
     this.pageStates[next.page.id] = next.pageState
@@ -249,12 +275,12 @@ export class TLDrawState implements TLCallbacks {
249 275
     return this
250 276
   }
251 277
   /* --------------------- Status --------------------- */
252
-  setStatus(status: TLDrawStatus) {
253
-    this.status.previous = this.status.current
254
-    this.status.current = status
255
-    // console.log(this.status.previous, ' -> ', this.status.current)
256
-    return this
257
-  }
278
+  // setStatus(status: TLDrawStatus) {
279
+  //   this.status.previous = this.status.current
280
+  //   this.status.current = status
281
+  //   // console.log(this.status.previous, ' -> ', this.status.current)
282
+  //   return this
283
+  // }
258 284
   /* -------------------- App State ------------------- */
259 285
   reset = () => {
260 286
     this.setState((data) => ({
@@ -525,7 +551,7 @@ export class TLDrawState implements TLCallbacks {
525 551
   /* -------------------- Sessions -------------------- */
526 552
   startSession<T extends Session>(session: T, ...args: ParametersExceptFirst<T['start']>) {
527 553
     this.session = session
528
-    this.setState((data) => session.start(data, ...args))
554
+    this.setState((data) => session.start(data, ...args), session.status)
529 555
     this._onChange?.(this, `session:start_${session.id}`)
530 556
     return this
531 557
   }
@@ -542,10 +568,13 @@ export class TLDrawState implements TLCallbacks {
542 568
     const { session } = this
543 569
     if (!session) return this
544 570
 
545
-    this.setState((data) => session.cancel(data, ...args))
546
-    this.setStatus('idle')
571
+    this.setState((data) => session.cancel(data, ...args), TLDrawStatus.Idle)
572
+
547 573
     this.session = undefined
574
+    this.isCreating = false
575
+
548 576
     this._onChange?.(this, `session:cancel:${session.id}`)
577
+
549 578
     return this
550 579
   }
551 580
 
@@ -553,16 +582,9 @@ export class TLDrawState implements TLCallbacks {
553 582
     const { session } = this
554 583
     if (!session) return this
555 584
 
556
-    this.setStatus('idle')
557
-
558
-    const result = session.complete(this.store.getState(), ...args)
585
+    const current = this.getState()
559 586
 
560
-    if ('after' in result) {
561
-      this.do(result)
562
-    } else {
563
-      this.setState((data) => Utils.deepMerge<Data>(data, result))
564
-      this._onChange?.(this, `session:complete:${session.id}`)
565
-    }
587
+    const result = session.complete(current, ...args)
566 588
 
567 589
     const { isToolLocked, activeTool } = this.appState
568 590
 
@@ -571,6 +593,35 @@ export class TLDrawState implements TLCallbacks {
571 593
     }
572 594
 
573 595
     this.session = undefined
596
+
597
+    if ('after' in result) {
598
+      // Session ended with a command
599
+
600
+      if (this.isCreating) {
601
+        // We're currently creating a shape. Override the command's
602
+        // before state so that when we undo the command, we remove
603
+        // the shape we just created.
604
+        result.before = {
605
+          page: {
606
+            shapes: Object.fromEntries(current.pageState.selectedIds.map((id) => [id, undefined])),
607
+          },
608
+          pageState: {
609
+            selectedIds: [],
610
+            editingId: undefined,
611
+            bindingId: undefined,
612
+            hoveredId: undefined,
613
+          },
614
+        }
615
+      }
616
+
617
+      this.isCreating = false
618
+
619
+      this.do(result)
620
+    } else {
621
+      this.setState((data) => Utils.deepMerge<Data>(data, result), TLDrawStatus.Idle)
622
+      this._onChange?.(this, `session:complete:${session.id}`)
623
+    }
624
+
574 625
     return this
575 626
   }
576 627
 
@@ -586,12 +637,14 @@ export class TLDrawState implements TLCallbacks {
586 637
 
587 638
     history.pointer = history.stack.length - 1
588 639
 
589
-    this.setState((data) =>
590
-      Object.fromEntries(
591
-        Object.entries(command.after).map(([key, partial]) => {
592
-          return [key, Utils.deepMerge(data[key as keyof Data], partial)]
593
-        })
594
-      )
640
+    this.setState(
641
+      (data) =>
642
+        Object.fromEntries(
643
+          Object.entries(command.after).map(([key, partial]) => {
644
+            return [key, Utils.deepMerge(data[key as keyof Data], partial)]
645
+          })
646
+        ),
647
+      TLDrawStatus.Idle
595 648
     )
596 649
 
597 650
     this._onChange?.(this, `command:${command.id}`)
@@ -606,12 +659,14 @@ export class TLDrawState implements TLCallbacks {
606 659
 
607 660
     const command = history.stack[history.pointer]
608 661
 
609
-    this.setState((data) =>
610
-      Object.fromEntries(
611
-        Object.entries(command.before).map(([key, partial]) => {
612
-          return [key, Utils.deepMerge(data[key as keyof Data], partial)]
613
-        })
614
-      )
662
+    this.setState(
663
+      (data) =>
664
+        Object.fromEntries(
665
+          Object.entries(command.before).map(([key, partial]) => {
666
+            return [key, Utils.deepMerge(data[key as keyof Data], partial)]
667
+          })
668
+        ),
669
+      TLDrawStatus.Idle
615 670
     )
616 671
 
617 672
     history.pointer--
@@ -630,13 +685,16 @@ export class TLDrawState implements TLCallbacks {
630 685
 
631 686
     const command = history.stack[history.pointer]
632 687
 
633
-    this.setState((data) =>
634
-      Object.fromEntries(
635
-        Object.entries(command.after).map(([key, partial]) => {
636
-          return [key, Utils.deepMerge(data[key as keyof Data], partial)]
637
-        })
638
-      )
688
+    this.setState(
689
+      (data) =>
690
+        Object.fromEntries(
691
+          Object.entries(command.after).map(([key, partial]) => {
692
+            return [key, Utils.deepMerge(data[key as keyof Data], partial)]
693
+          })
694
+        ),
695
+      TLDrawStatus.Idle
639 696
     )
697
+
640 698
     this._onChange?.(this, `redo:${command.id}`)
641 699
 
642 700
     return this
@@ -834,7 +892,14 @@ export class TLDrawState implements TLCallbacks {
834 892
   }
835 893
 
836 894
   cancel = () => {
837
-    switch (this.status.current) {
895
+    if (this.isCreating) {
896
+      this.cancelSession()
897
+      this.delete()
898
+      this.isCreating = false
899
+      return
900
+    }
901
+
902
+    switch (this.appState.status.current) {
838 903
       case 'idle': {
839 904
         this.deselectAll()
840 905
         this.selectTool('select')
@@ -857,11 +922,6 @@ export class TLDrawState implements TLCallbacks {
857 922
         this.cancelSession()
858 923
         break
859 924
       }
860
-      case 'creating': {
861
-        this.cancelSession()
862
-        this.delete()
863
-        break
864
-      }
865 925
     }
866 926
 
867 927
     return this
@@ -968,7 +1028,6 @@ export class TLDrawState implements TLCallbacks {
968 1028
   }
969 1029
   /* -------------------- Sessions -------------------- */
970 1030
   startBrushSession = (point: number[]) => {
971
-    this.setStatus('brushing')
972 1031
     this.startSession(new Sessions.BrushSession(this.store.getState(), point))
973 1032
     return this
974 1033
   }
@@ -979,7 +1038,6 @@ export class TLDrawState implements TLCallbacks {
979 1038
   }
980 1039
 
981 1040
   startTranslateSession = (point: number[]) => {
982
-    this.setStatus('translating')
983 1041
     this.startSession(new Sessions.TranslateSession(this.store.getState(), point))
984 1042
     return this
985 1043
   }
@@ -998,8 +1056,6 @@ export class TLDrawState implements TLCallbacks {
998 1056
 
999 1057
     if (selectedIds.length === 0) return this
1000 1058
 
1001
-    this.setStatus('transforming')
1002
-
1003 1059
     this.pointedBoundsHandle = handle
1004 1060
 
1005 1061
     if (this.pointedBoundsHandle === 'rotate') {
@@ -1032,7 +1088,6 @@ export class TLDrawState implements TLCallbacks {
1032 1088
 
1033 1089
   startTextSession = (id?: string) => {
1034 1090
     this.editingId = id
1035
-    this.setStatus('editing-text')
1036 1091
     this.startSession(new Sessions.TextSession(this.store.getState(), id))
1037 1092
     return this
1038 1093
   }
@@ -1043,7 +1098,6 @@ export class TLDrawState implements TLCallbacks {
1043 1098
   }
1044 1099
 
1045 1100
   startDrawSession = (id: string, point: number[]) => {
1046
-    this.setStatus('creating')
1047 1101
     this.startSession(new Sessions.DrawSession(this.store.getState(), id, point))
1048 1102
     return this
1049 1103
   }
@@ -1077,44 +1131,41 @@ export class TLDrawState implements TLCallbacks {
1077 1131
     return this
1078 1132
   }
1079 1133
 
1080
-  updatenPointerMove: TLPointerEventHandler = (info) => {
1134
+  updateOnPointerMove: TLPointerEventHandler = (info) => {
1081 1135
     switch (this.status.current) {
1082
-      case 'pointingBoundsHandle': {
1136
+      case TLDrawStatus.PointingBoundsHandle: {
1083 1137
         if (!this.pointedBoundsHandle) throw Error('No pointed bounds handle')
1084 1138
         if (Vec.dist(info.origin, info.point) > 4) {
1085
-          this.setStatus('transforming')
1086 1139
           this.startTransformSession(this.getPagePoint(info.origin), this.pointedBoundsHandle)
1087 1140
         }
1088 1141
         break
1089 1142
       }
1090
-      case 'pointingHandle': {
1143
+      case TLDrawStatus.PointingHandle: {
1091 1144
         if (!this.pointedHandle) throw Error('No pointed handle')
1092 1145
         if (Vec.dist(info.origin, info.point) > 4) {
1093
-          this.setStatus('translatingHandle')
1094 1146
           this.startHandleSession(this.getPagePoint(info.origin), this.pointedHandle)
1095 1147
         }
1096 1148
         break
1097 1149
       }
1098
-      case 'pointingBounds': {
1150
+      case TLDrawStatus.PointingBounds: {
1099 1151
         if (Vec.dist(info.origin, info.point) > 4) {
1100
-          this.setStatus('translating')
1101 1152
           this.startTranslateSession(this.getPagePoint(info.origin))
1102 1153
         }
1103 1154
         break
1104 1155
       }
1105
-      case 'brushing': {
1156
+      case TLDrawStatus.Brushing: {
1106 1157
         this.updateBrushSession(this.getPagePoint(info.point), info.metaKey)
1107 1158
         break
1108 1159
       }
1109
-      case 'translating': {
1160
+      case TLDrawStatus.Translating: {
1110 1161
         this.updateTranslateSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
1111 1162
         break
1112 1163
       }
1113
-      case 'transforming': {
1164
+      case TLDrawStatus.Transforming: {
1114 1165
         this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
1115 1166
         break
1116 1167
       }
1117
-      case 'translatingHandle': {
1168
+      case TLDrawStatus.TranslatingHandle: {
1118 1169
         this.updateHandleSession(
1119 1170
           this.getPagePoint(info.point),
1120 1171
           info.shiftKey,
@@ -1123,7 +1174,7 @@ export class TLDrawState implements TLCallbacks {
1123 1174
         )
1124 1175
         break
1125 1176
       }
1126
-      case 'creating': {
1177
+      case TLDrawStatus.Creating: {
1127 1178
         switch (this.appState.activeToolType) {
1128 1179
           case 'draw': {
1129 1180
             this.updateDrawSession(this.getPagePoint(info.point), info.pressure, info.shiftKey)
@@ -1193,7 +1244,9 @@ export class TLDrawState implements TLCallbacks {
1193 1244
           selectedIds: [id],
1194 1245
         },
1195 1246
       }
1196
-    })
1247
+    }, TLDrawStatus.Creating)
1248
+
1249
+    this.isCreating = true
1197 1250
 
1198 1251
     const { activeTool, activeToolType } = this.getAppState()
1199 1252
 
@@ -1231,10 +1284,10 @@ export class TLDrawState implements TLCallbacks {
1231 1284
     }
1232 1285
 
1233 1286
     switch (this.status.current) {
1234
-      case 'idle': {
1287
+      case TLDrawStatus.Idle: {
1235 1288
         break
1236 1289
       }
1237
-      case 'brushing': {
1290
+      case TLDrawStatus.Brushing: {
1238 1291
         if (key === 'Meta' || key === 'Control') {
1239 1292
           this.updateBrushSession(this.getPagePoint(info.point), info.metaKey)
1240 1293
           return
@@ -1242,7 +1295,7 @@ export class TLDrawState implements TLCallbacks {
1242 1295
 
1243 1296
         break
1244 1297
       }
1245
-      case 'translating': {
1298
+      case TLDrawStatus.Translating: {
1246 1299
         if (key === 'Escape') {
1247 1300
           this.cancelSession(this.getPagePoint(info.point))
1248 1301
         }
@@ -1252,7 +1305,7 @@ export class TLDrawState implements TLCallbacks {
1252 1305
         }
1253 1306
         break
1254 1307
       }
1255
-      case 'transforming': {
1308
+      case TLDrawStatus.Transforming: {
1256 1309
         if (key === 'Escape') {
1257 1310
           this.cancelSession(this.getPagePoint(info.point))
1258 1311
         }
@@ -1262,7 +1315,7 @@ export class TLDrawState implements TLCallbacks {
1262 1315
         }
1263 1316
         break
1264 1317
       }
1265
-      case 'translatingHandle': {
1318
+      case TLDrawStatus.TranslatingHandle: {
1266 1319
         if (key === 'Escape') {
1267 1320
           this.cancelSession(this.getPagePoint(info.point))
1268 1321
         }
@@ -1282,25 +1335,25 @@ export class TLDrawState implements TLCallbacks {
1282 1335
 
1283 1336
   onKeyUp = (key: string, info: TLKeyboardInfo) => {
1284 1337
     switch (this.status.current) {
1285
-      case 'brushing': {
1338
+      case TLDrawStatus.Brushing: {
1286 1339
         if (key === 'Meta' || key === 'Control') {
1287 1340
           this.updateBrushSession(this.getPagePoint(info.point), info.metaKey)
1288 1341
         }
1289 1342
         break
1290 1343
       }
1291
-      case 'transforming': {
1344
+      case TLDrawStatus.Transforming: {
1292 1345
         if (key === 'Shift' || key === 'Alt') {
1293 1346
           this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
1294 1347
         }
1295 1348
         break
1296 1349
       }
1297
-      case 'translating': {
1350
+      case TLDrawStatus.Translating: {
1298 1351
         if (key === 'Shift' || key === 'Alt') {
1299 1352
           this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
1300 1353
         }
1301 1354
         break
1302 1355
       }
1303
-      case 'translatingHandle': {
1356
+      case TLDrawStatus.TranslatingHandle: {
1304 1357
         if (key === 'Escape') {
1305 1358
           this.cancelSession(this.getPagePoint(info.point))
1306 1359
         }
@@ -1320,7 +1373,7 @@ export class TLDrawState implements TLCallbacks {
1320 1373
 
1321 1374
   /* ------------- Renderer Event Handlers ------------ */
1322 1375
   onPinchStart: TLPinchEventHandler = () => {
1323
-    this.setStatus('pinching')
1376
+    this.setStatus(TLDrawStatus.Pinching)
1324 1377
   }
1325 1378
 
1326 1379
   onPinchEnd: TLPinchEventHandler = () => {
@@ -1328,14 +1381,14 @@ export class TLDrawState implements TLCallbacks {
1328 1381
   }
1329 1382
 
1330 1383
   onPinch: TLPinchEventHandler = (info, e) => {
1331
-    if (this.status.current !== 'pinching') return
1384
+    if (this.status.current !== TLDrawStatus.Pinching) return
1332 1385
 
1333 1386
     this.pinchZoom(info.origin, info.delta, info.delta[2] / 350)
1334
-    this.updatenPointerMove(info, e as any)
1387
+    this.updateOnPointerMove(info, e as any)
1335 1388
   }
1336 1389
 
1337 1390
   onPan: TLWheelEventHandler = (info, e) => {
1338
-    if (this.status.current === 'pinching') return
1391
+    if (this.status.current === TLDrawStatus.Pinching) return
1339 1392
     // TODO: Pan and pinchzoom are firing at the same time. Considering turning one of them off!
1340 1393
 
1341 1394
     const delta = Vec.div(info.delta, this.getPageState().camera.zoom)
@@ -1345,36 +1398,32 @@ export class TLDrawState implements TLCallbacks {
1345 1398
     if (Vec.isEqual(next, prev)) return
1346 1399
 
1347 1400
     this.pan(delta)
1348
-    this.updatenPointerMove(info, e as any)
1401
+    this.updateOnPointerMove(info, e as any)
1349 1402
   }
1350 1403
 
1351 1404
   onZoom: TLWheelEventHandler = (info, e) => {
1352 1405
     this.zoom(info.delta[2] / 100)
1353
-    this.updatenPointerMove(info, e as any)
1406
+    this.updateOnPointerMove(info, e as any)
1354 1407
   }
1355 1408
 
1356 1409
   // Pointer Events
1357 1410
   onPointerDown: TLPointerEventHandler = (info) => {
1358 1411
     switch (this.status.current) {
1359
-      case 'idle': {
1412
+      case TLDrawStatus.Idle: {
1360 1413
         switch (this.appState.activeTool) {
1361 1414
           case 'draw': {
1362
-            this.setStatus('creating')
1363 1415
             this.createActiveToolShape(info.point)
1364 1416
             break
1365 1417
           }
1366 1418
           case 'rectangle': {
1367
-            this.setStatus('creating')
1368 1419
             this.createActiveToolShape(info.point)
1369 1420
             break
1370 1421
           }
1371 1422
           case 'ellipse': {
1372
-            this.setStatus('creating')
1373 1423
             this.createActiveToolShape(info.point)
1374 1424
             break
1375 1425
           }
1376 1426
           case 'arrow': {
1377
-            this.setStatus('creating')
1378 1427
             this.createActiveToolShape(info.point)
1379 1428
             break
1380 1429
           }
@@ -1384,14 +1433,14 @@ export class TLDrawState implements TLCallbacks {
1384 1433
   }
1385 1434
 
1386 1435
   onPointerMove: TLPointerEventHandler = (info, e) => {
1387
-    this.updatenPointerMove(info, e)
1436
+    this.updateOnPointerMove(info, e)
1388 1437
   }
1389 1438
 
1390 1439
   onPointerUp: TLPointerEventHandler = (info) => {
1391 1440
     const data = this.getState()
1392 1441
 
1393 1442
     switch (this.status.current) {
1394
-      case 'pointingBounds': {
1443
+      case TLDrawStatus.PointingBounds: {
1395 1444
         if (info.target === 'bounds') {
1396 1445
           // If we just clicked the selecting bounds's background, clear the selection
1397 1446
           this.deselectAll()
@@ -1412,41 +1461,41 @@ export class TLDrawState implements TLCallbacks {
1412 1461
           this.pointedId = undefined
1413 1462
         }
1414 1463
 
1415
-        this.setStatus('idle')
1464
+        this.setStatus(TLDrawStatus.Idle)
1416 1465
         this.pointedId = undefined
1417 1466
         break
1418 1467
       }
1419
-      case 'pointingBoundsHandle': {
1420
-        this.setStatus('idle')
1468
+      case TLDrawStatus.PointingBoundsHandle: {
1469
+        this.setStatus(TLDrawStatus.Idle)
1421 1470
         this.pointedBoundsHandle = undefined
1422 1471
         break
1423 1472
       }
1424
-      case 'pointingHandle': {
1425
-        this.setStatus('idle')
1473
+      case TLDrawStatus.PointingHandle: {
1474
+        this.setStatus(TLDrawStatus.Idle)
1426 1475
         this.pointedHandle = undefined
1427 1476
         break
1428 1477
       }
1429
-      case 'translatingHandle': {
1478
+      case TLDrawStatus.TranslatingHandle: {
1430 1479
         this.completeSession<Sessions.HandleSession>()
1431 1480
         this.pointedHandle = undefined
1432 1481
         break
1433 1482
       }
1434
-      case 'brushing': {
1483
+      case TLDrawStatus.Brushing: {
1435 1484
         this.completeSession<Sessions.BrushSession>()
1436 1485
         brushUpdater.clear()
1437 1486
         break
1438 1487
       }
1439
-      case 'translating': {
1440
-        this.completeSession(this.getPagePoint(info.point))
1488
+      case TLDrawStatus.Translating: {
1489
+        this.completeSession<Sessions.TranslateSession>()
1441 1490
         this.pointedId = undefined
1442 1491
         break
1443 1492
       }
1444
-      case 'transforming': {
1445
-        this.completeSession(this.getPagePoint(info.point))
1493
+      case TLDrawStatus.Transforming: {
1494
+        this.completeSession<Sessions.TransformSession>()
1446 1495
         this.pointedBoundsHandle = undefined
1447 1496
         break
1448 1497
       }
1449
-      case 'creating': {
1498
+      case TLDrawStatus.Creating: {
1450 1499
         this.completeSession(this.getPagePoint(info.point))
1451 1500
         this.pointedHandle = undefined
1452 1501
       }
@@ -1485,7 +1534,6 @@ export class TLDrawState implements TLCallbacks {
1485 1534
         switch (this.appState.activeTool) {
1486 1535
           case 'text': {
1487 1536
             // Create a text shape
1488
-            this.setStatus('creating')
1489 1537
             this.createActiveToolShape(info.point)
1490 1538
             break
1491 1539
           }
@@ -1530,7 +1578,7 @@ export class TLDrawState implements TLCallbacks {
1530 1578
               this.setSelectedIds([info.target], info.shiftKey)
1531 1579
             }
1532 1580
 
1533
-            this.setStatus('pointingBounds')
1581
+            this.setStatus(TLDrawStatus.PointingBounds)
1534 1582
             break
1535 1583
           }
1536 1584
         }
@@ -1581,7 +1629,7 @@ export class TLDrawState implements TLCallbacks {
1581 1629
 
1582 1630
   // Bounds (bounding box background)
1583 1631
   onPointBounds: TLBoundsEventHandler = () => {
1584
-    this.setStatus('pointingBounds')
1632
+    this.setStatus(TLDrawStatus.PointingBounds)
1585 1633
   }
1586 1634
 
1587 1635
   onDoubleClickBounds: TLBoundsEventHandler = () => {
@@ -1606,11 +1654,11 @@ export class TLDrawState implements TLCallbacks {
1606 1654
 
1607 1655
   onReleaseBounds: TLBoundsEventHandler = (info) => {
1608 1656
     switch (this.status.current) {
1609
-      case 'translating': {
1657
+      case TLDrawStatus.Translating: {
1610 1658
         this.completeSession(this.getPagePoint(info.point))
1611 1659
         break
1612 1660
       }
1613
-      case 'brushing': {
1661
+      case TLDrawStatus.Brushing: {
1614 1662
         this.completeSession<Sessions.BrushSession>()
1615 1663
         brushUpdater.clear()
1616 1664
         break
@@ -1621,7 +1669,7 @@ export class TLDrawState implements TLCallbacks {
1621 1669
   // Bounds handles (corners, edges)
1622 1670
   onPointBoundsHandle: TLBoundsHandleEventHandler = (info) => {
1623 1671
     this.pointedBoundsHandle = info.target
1624
-    this.setStatus('pointingBoundsHandle')
1672
+    this.setStatus(TLDrawStatus.PointingBoundsHandle)
1625 1673
   }
1626 1674
 
1627 1675
   onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = () => {
@@ -1651,7 +1699,7 @@ export class TLDrawState implements TLCallbacks {
1651 1699
   // Handles (ie the handles of a selected arrow)
1652 1700
   onPointHandle: TLPointerEventHandler = (info) => {
1653 1701
     this.pointedHandle = info.target
1654
-    this.setStatus('pointingHandle')
1702
+    this.setStatus(TLDrawStatus.PointingHandle)
1655 1703
   }
1656 1704
 
1657 1705
   onDoubleClickHandle: TLPointerEventHandler = (info) => {
@@ -1766,4 +1814,8 @@ export class TLDrawState implements TLCallbacks {
1766 1814
   get appState() {
1767 1815
     return this.data.appState
1768 1816
   }
1817
+
1818
+  get status() {
1819
+    return this.appState.status
1820
+  }
1769 1821
 }

+ 16
- 13
packages/tldraw/src/types.ts View File

@@ -35,6 +35,7 @@ export interface Data {
35 35
     isToolLocked: boolean
36 36
     isStyleOpen: boolean
37 37
     isEmptyCanvas: boolean
38
+    status: { current: TLDrawStatus; previous: TLDrawStatus }
38 39
   }
39 40
 }
40 41
 export interface PagePartial {
@@ -63,25 +64,27 @@ export interface History {
63 64
 
64 65
 export interface Session {
65 66
   id: string
67
+  status: TLDrawStatus
66 68
   start: (data: Readonly<Data>, ...args: any[]) => Partial<Data>
67 69
   update: (data: Readonly<Data>, ...args: any[]) => Partial<Data>
68 70
   complete: (data: Readonly<Data>, ...args: any[]) => Partial<Data> | Command
69 71
   cancel: (data: Readonly<Data>, ...args: any[]) => Partial<Data>
70 72
 }
71 73
 
72
-export type TLDrawStatus =
73
-  | 'idle'
74
-  | 'pointingHandle'
75
-  | 'pointingBounds'
76
-  | 'pointingBoundsHandle'
77
-  | 'translatingHandle'
78
-  | 'translating'
79
-  | 'transforming'
80
-  | 'rotating'
81
-  | 'pinching'
82
-  | 'brushing'
83
-  | 'creating'
84
-  | 'editing-text'
74
+export enum TLDrawStatus {
75
+  Idle = 'idle',
76
+  PointingHandle = 'pointingHandle',
77
+  PointingBounds = 'pointingBounds',
78
+  PointingBoundsHandle = 'pointingBoundsHandle',
79
+  TranslatingHandle = 'translatingHandle',
80
+  Translating = 'translating',
81
+  Transforming = 'transforming',
82
+  Rotating = 'rotating',
83
+  Pinching = 'pinching',
84
+  Brushing = 'brushing',
85
+  Creating = 'creating',
86
+  EditingText = 'editing-text',
87
+}
85 88
 
86 89
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
87 90
 export type ParametersExceptFirst<F> = F extends (arg0: any, ...rest: infer R) => any ? R : never

Loading…
Cancel
Save