Quellcode durchsuchen

Fix bug on missing others, adds new tests

main
Steve Ruiz vor 4 Jahren
Ursprung
Commit
8ee78d1b90
59 geänderte Dateien mit 3168 neuen und 1873 gelöschten Zeilen
  1. 34
    0
      .vscode/snippets.code-snippets
  2. 117
    0
      __tests__/__snapshots__/project.test.ts.snap
  3. 72
    0
      __tests__/bounds.test.ts
  4. 300
    0
      __tests__/children.test.ts
  5. 46
    0
      __tests__/coop.test.ts
  6. 4
    0
      __tests__/create.test.ts
  7. 1
    1
      __tests__/dashes.test.ts
  8. 79
    116
      __tests__/delete.test.ts
  9. 61
    0
      __tests__/locked.test.ts
  10. 24
    0
      __tests__/project.test.ts
  11. 52
    110
      __tests__/selection.test.ts
  12. 21
    9
      __tests__/shapes/arrow.test.ts
  13. 28
    0
      __tests__/style.test.ts
  14. 567
    64
      __tests__/test-utils.ts
  15. 91
    0
      __tests__/transform.test.ts
  16. 38
    0
      __tests__/translate.test.ts
  17. 1
    1
      components/canvas/canvas.tsx
  18. 16
    15
      components/canvas/coop/coop.tsx
  19. 32
    13
      components/canvas/shape.tsx
  20. 84
    58
      components/code-panel/types-import.ts
  21. 1
    0
      hooks/useLoadOnMount.ts
  22. 19
    22
      hooks/useShapeEvents.ts
  23. 2
    2
      package.json
  24. 1
    1
      state/code/arrow.ts
  25. 1
    1
      state/code/control.ts
  26. 1
    1
      state/code/dot.ts
  27. 1
    1
      state/code/draw.ts
  28. 1
    1
      state/code/ellipse.ts
  29. 1
    1
      state/code/line.ts
  30. 1
    1
      state/code/polyline.ts
  31. 1
    1
      state/code/ray.ts
  32. 1
    1
      state/code/rectangle.ts
  33. 1
    1
      state/code/text.ts
  34. 2
    0
      state/commands/change-page.ts
  35. 1
    1
      state/commands/create-page.ts
  36. 1
    1
      state/commands/duplicate.ts
  37. 1
    0
      state/commands/move.ts
  38. 1
    1
      state/commands/paste.ts
  39. 1
    1
      state/coop/client-liveblocks.ts
  40. 1
    0
      state/inputs.tsx
  41. 1
    1
      state/sessions/translate-session.ts
  42. 55
    43
      state/shape-utils/arrow.tsx
  43. 14
    22
      state/shape-utils/dot.tsx
  44. 15
    22
      state/shape-utils/draw.tsx
  45. 16
    20
      state/shape-utils/ellipse.tsx
  46. 16
    21
      state/shape-utils/group.tsx
  47. 15
    23
      state/shape-utils/line.tsx
  48. 15
    19
      state/shape-utils/polyline.tsx
  49. 15
    23
      state/shape-utils/ray.tsx
  50. 17
    21
      state/shape-utils/rectangle.tsx
  51. 13
    14
      state/shape-utils/register.tsx
  52. 16
    20
      state/shape-utils/text.tsx
  53. 50
    11
      state/state.ts
  54. 1
    1
      state/storage.ts
  55. 4
    1
      types.ts
  56. 0
    41
      utils/dashes.ts
  57. 4
    0
      utils/index.ts
  58. 2
    2
      utils/tld.ts
  59. 1191
    1143
      utils/utils.ts

+ 34
- 0
.vscode/snippets.code-snippets Datei anzeigen

@@ -0,0 +1,34 @@
1
+{
2
+  // Place your tldraw workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
3
+  // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
4
+  // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
5
+  // used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
6
+  // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
7
+  // Placeholders with the same ids are connected.
8
+  // Example:
9
+  // "Print to console": {
10
+  // 	"scope": "javascript,typescript",
11
+  // 	"prefix": "log",
12
+  // 	"body": [
13
+  // 		"console.log('$1');",
14
+  // 		"$2"
15
+  // 	],
16
+  // 	"description": "Log output to console"
17
+  // }
18
+  "createComment": {
19
+    "scope": "typescript",
20
+    "prefix": "/**",
21
+    "body": [
22
+      "/**",
23
+      " * ${1:description}",
24
+      " *",
25
+      " * ### Example",
26
+      " *",
27
+      " *```ts",
28
+      " * ${2:example}",
29
+      " *```",
30
+      " */"
31
+    ],
32
+    "description": "comment"
33
+  }
34
+}

+ 117
- 0
__tests__/__snapshots__/project.test.ts.snap Datei anzeigen

@@ -116,3 +116,120 @@ Object {
116 116
   },
117 117
 }
118 118
 `;
119
+
120
+exports[`restoring project remounts the state after mutating the current state: data after re-mount from file 1`] = `
121
+Object {
122
+  "code": Object {
123
+    "file0": Object {
124
+      "code": "",
125
+      "id": "file0",
126
+      "name": "index.ts",
127
+    },
128
+  },
129
+  "id": "0001",
130
+  "name": "My Document",
131
+  "pages": Object {
132
+    "page1": Object {
133
+      "childIndex": 0,
134
+      "id": "page1",
135
+      "name": "Page 1",
136
+      "shapes": Object {
137
+        "1f6c251c-e12e-40b4-8dd2-c1847d80b72f": Object {
138
+          "childIndex": 24,
139
+          "id": "1f6c251c-e12e-40b4-8dd2-c1847d80b72f",
140
+          "isAspectRatioLocked": false,
141
+          "isGenerated": false,
142
+          "isHidden": false,
143
+          "isLocked": false,
144
+          "name": "Rectangle",
145
+          "parentId": "page1",
146
+          "point": Array [
147
+            0,
148
+            0,
149
+          ],
150
+          "radius": 2,
151
+          "rotation": 0,
152
+          "seed": 0.6440313303074272,
153
+          "size": Array [
154
+            67.22075383450237,
155
+            72.92795609221832,
156
+          ],
157
+          "style": Object {
158
+            "color": "Black",
159
+            "dash": "Solid",
160
+            "isFilled": false,
161
+            "size": "Small",
162
+          },
163
+          "type": "rectangle",
164
+        },
165
+        "5ca167d7-54de-47c9-aa8f-86affa25e44d": Object {
166
+          "bend": 0,
167
+          "childIndex": 16,
168
+          "decorations": Object {
169
+            "end": null,
170
+            "middle": null,
171
+            "start": null,
172
+          },
173
+          "handles": Object {
174
+            "bend": Object {
175
+              "id": "bend",
176
+              "index": 2,
177
+              "point": Array [
178
+                3.2518097616315345,
179
+                140.54510317291172,
180
+              ],
181
+            },
182
+            "end": Object {
183
+              "id": "end",
184
+              "index": 1,
185
+              "point": Array [
186
+                0,
187
+                0,
188
+              ],
189
+            },
190
+            "start": Object {
191
+              "id": "start",
192
+              "index": 0,
193
+              "point": Array [
194
+                6.503619523263069,
195
+                281.09020634582345,
196
+              ],
197
+            },
198
+          },
199
+          "id": "5ca167d7-54de-47c9-aa8f-86affa25e44d",
200
+          "isAspectRatioLocked": false,
201
+          "isGenerated": false,
202
+          "isHidden": false,
203
+          "isLocked": false,
204
+          "name": "Arrow",
205
+          "parentId": "page1",
206
+          "point": Array [
207
+            100,
208
+            100,
209
+          ],
210
+          "points": Array [
211
+            Array [
212
+              6.503619523263069,
213
+              281.09020634582345,
214
+            ],
215
+            Array [
216
+              0,
217
+              0,
218
+            ],
219
+          ],
220
+          "rotation": 0,
221
+          "seed": 0.08116783083496548,
222
+          "style": Object {
223
+            "color": "Black",
224
+            "dash": "Solid",
225
+            "isFilled": false,
226
+            "size": "Small",
227
+          },
228
+          "type": "arrow",
229
+        },
230
+      },
231
+      "type": "page",
232
+    },
233
+  },
234
+}
235
+`;

+ 72
- 0
__tests__/bounds.test.ts Datei anzeigen

@@ -0,0 +1,72 @@
1
+import { getShapeUtils } from 'state/shape-utils'
2
+import { getCommonBounds } from 'utils'
3
+import TestState, { arrowId, rectangleId } from './test-utils'
4
+
5
+describe('selection', () => {
6
+  const tt = new TestState()
7
+
8
+  it('measures correct bounds for selected item', () => {
9
+    // Note: Each item should test its own bounds in its ./shapes/[shape].tsx file
10
+
11
+    const shape = tt.getShape(rectangleId)
12
+
13
+    tt.deselectAll().clickShape(rectangleId)
14
+
15
+    expect(tt.state.values.selectedBounds).toStrictEqual(
16
+      getShapeUtils(shape).getBounds(shape)
17
+    )
18
+  })
19
+
20
+  it('measures correct bounds for rotated selected item', () => {
21
+    const shape = tt.getShape(rectangleId)
22
+
23
+    getShapeUtils(shape).rotateBy(shape, Math.PI * 2 * Math.random())
24
+
25
+    tt.deselectAll().clickShape(rectangleId)
26
+
27
+    expect(tt.state.values.selectedBounds).toStrictEqual(
28
+      getShapeUtils(shape).getBounds(shape)
29
+    )
30
+
31
+    getShapeUtils(shape).rotateBy(shape, -Math.PI * 2 * Math.random())
32
+
33
+    expect(tt.state.values.selectedBounds).toStrictEqual(
34
+      getShapeUtils(shape).getBounds(shape)
35
+    )
36
+  })
37
+
38
+  it('measures correct bounds for selected items', () => {
39
+    const shape1 = tt.getShape(rectangleId)
40
+    const shape2 = tt.getShape(arrowId)
41
+
42
+    tt.deselectAll()
43
+      .clickShape(shape1.id)
44
+      .clickShape(shape2.id, { shiftKey: true })
45
+
46
+    expect(tt.state.values.selectedBounds).toStrictEqual(
47
+      getCommonBounds(
48
+        getShapeUtils(shape1).getRotatedBounds(shape1),
49
+        getShapeUtils(shape2).getRotatedBounds(shape2)
50
+      )
51
+    )
52
+  })
53
+
54
+  it('measures correct bounds for rotated selected items', () => {
55
+    const shape1 = tt.getShape(rectangleId)
56
+    const shape2 = tt.getShape(arrowId)
57
+
58
+    getShapeUtils(shape1).rotateBy(shape1, Math.PI * 2 * Math.random())
59
+    getShapeUtils(shape2).rotateBy(shape2, Math.PI * 2 * Math.random())
60
+
61
+    tt.deselectAll()
62
+      .clickShape(shape1.id)
63
+      .clickShape(shape2.id, { shiftKey: true })
64
+
65
+    expect(tt.state.values.selectedBounds).toStrictEqual(
66
+      getCommonBounds(
67
+        getShapeUtils(shape1).getRotatedBounds(shape1),
68
+        getShapeUtils(shape2).getRotatedBounds(shape2)
69
+      )
70
+    )
71
+  })
72
+})

+ 300
- 0
__tests__/children.test.ts Datei anzeigen

@@ -0,0 +1,300 @@
1
+import { MoveType, ShapeType } from 'types'
2
+import TestState from './test-utils'
3
+
4
+describe('shapes with children', () => {
5
+  const tt = new TestState()
6
+
7
+  tt.resetDocumentState()
8
+    .createShape(
9
+      {
10
+        type: ShapeType.Rectangle,
11
+        point: [0, 0],
12
+        size: [100, 100],
13
+        childIndex: 1,
14
+      },
15
+      'delete-me-bottom'
16
+    )
17
+    .createShape(
18
+      {
19
+        type: ShapeType.Rectangle,
20
+        point: [0, 0],
21
+        size: [100, 100],
22
+        childIndex: 2,
23
+      },
24
+      '1'
25
+    )
26
+    .createShape(
27
+      {
28
+        type: ShapeType.Rectangle,
29
+        point: [300, 0],
30
+        size: [100, 100],
31
+        childIndex: 3,
32
+      },
33
+      '2'
34
+    )
35
+    .createShape(
36
+      {
37
+        type: ShapeType.Rectangle,
38
+        point: [0, 300],
39
+        size: [100, 100],
40
+        childIndex: 4,
41
+      },
42
+      'delete-me-middle'
43
+    )
44
+    .createShape(
45
+      {
46
+        type: ShapeType.Rectangle,
47
+        point: [0, 300],
48
+        size: [100, 100],
49
+        childIndex: 5,
50
+      },
51
+      '3'
52
+    )
53
+    .createShape(
54
+      {
55
+        type: ShapeType.Rectangle,
56
+        point: [300, 300],
57
+        size: [100, 100],
58
+        childIndex: 6,
59
+      },
60
+      '4'
61
+    )
62
+
63
+  // Delete shapes at the start and in the middle of the list
64
+
65
+  tt.clickShape('delete-me-bottom')
66
+    .send('DELETED')
67
+    .clickShape('delete-me-middle')
68
+    .send('DELETED')
69
+
70
+  it('has shapes in order', () => {
71
+    expect(
72
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
73
+        .sort((a, b) => a.childIndex - b.childIndex)
74
+        .map((shape) => shape.childIndex)
75
+    ).toStrictEqual([2, 3, 5, 6])
76
+  })
77
+
78
+  it('moves a shape to back', () => {
79
+    tt.clickShape('3').send('MOVED', {
80
+      type: MoveType.ToBack,
81
+    })
82
+
83
+    expect(
84
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
85
+        .sort((a, b) => a.childIndex - b.childIndex)
86
+        .map((shape) => shape.id)
87
+    ).toStrictEqual(['3', '1', '2', '4'])
88
+  })
89
+
90
+  it('moves two adjacent siblings to back', () => {
91
+    tt.clickShape('4').clickShape('2', { shiftKey: true }).send('MOVED', {
92
+      type: MoveType.ToBack,
93
+    })
94
+
95
+    expect(
96
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
97
+        .sort((a, b) => a.childIndex - b.childIndex)
98
+        .map((shape) => shape.id)
99
+    ).toStrictEqual(['2', '4', '3', '1'])
100
+  })
101
+
102
+  it('moves two non-adjacent siblings to back', () => {
103
+    tt.clickShape('4').clickShape('1', { shiftKey: true }).send('MOVED', {
104
+      type: MoveType.ToBack,
105
+    })
106
+
107
+    expect(
108
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
109
+        .sort((a, b) => a.childIndex - b.childIndex)
110
+        .map((shape) => shape.id)
111
+    ).toStrictEqual(['4', '1', '2', '3'])
112
+  })
113
+
114
+  it('moves a shape backward', () => {
115
+    tt.clickShape('3').send('MOVED', {
116
+      type: MoveType.Backward,
117
+    })
118
+
119
+    expect(
120
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
121
+        .sort((a, b) => a.childIndex - b.childIndex)
122
+        .map((shape) => shape.id)
123
+    ).toStrictEqual(['4', '1', '3', '2'])
124
+  })
125
+
126
+  it('moves a shape at first index backward', () => {
127
+    tt.clickShape('4').send('MOVED', {
128
+      type: MoveType.Backward,
129
+    })
130
+
131
+    expect(
132
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
133
+        .sort((a, b) => a.childIndex - b.childIndex)
134
+        .map((shape) => shape.id)
135
+    ).toStrictEqual(['4', '1', '3', '2'])
136
+  })
137
+
138
+  it('moves two adjacent siblings backward', () => {
139
+    tt.clickShape('3').clickShape('2', { shiftKey: true }).send('MOVED', {
140
+      type: MoveType.Backward,
141
+    })
142
+
143
+    expect(
144
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
145
+        .sort((a, b) => a.childIndex - b.childIndex)
146
+        .map((shape) => shape.id)
147
+    ).toStrictEqual(['4', '3', '2', '1'])
148
+  })
149
+
150
+  it('moves two non-adjacent siblings backward', () => {
151
+    tt.clickShape('3').clickShape('1', { shiftKey: true }).send('MOVED', {
152
+      type: MoveType.Backward,
153
+    })
154
+
155
+    expect(
156
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
157
+        .sort((a, b) => a.childIndex - b.childIndex)
158
+        .map((shape) => shape.id)
159
+    ).toStrictEqual(['3', '4', '1', '2'])
160
+  })
161
+
162
+  it('moves two adjacent siblings backward at zero index', () => {
163
+    tt.clickShape('3').clickShape('4', { shiftKey: true }).send('MOVED', {
164
+      type: MoveType.Backward,
165
+    })
166
+
167
+    expect(
168
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
169
+        .sort((a, b) => a.childIndex - b.childIndex)
170
+        .map((shape) => shape.id)
171
+    ).toStrictEqual(['3', '4', '1', '2'])
172
+  })
173
+
174
+  it('moves a shape forward', () => {
175
+    tt.clickShape('4').send('MOVED', {
176
+      type: MoveType.Forward,
177
+    })
178
+
179
+    expect(
180
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
181
+        .sort((a, b) => a.childIndex - b.childIndex)
182
+        .map((shape) => shape.id)
183
+    ).toStrictEqual(['3', '1', '4', '2'])
184
+  })
185
+
186
+  it('moves a shape forward at the top index', () => {
187
+    tt.clickShape('2').send('MOVED', {
188
+      type: MoveType.Forward,
189
+    })
190
+
191
+    expect(
192
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
193
+        .sort((a, b) => a.childIndex - b.childIndex)
194
+        .map((shape) => shape.id)
195
+    ).toStrictEqual(['3', '1', '4', '2'])
196
+  })
197
+
198
+  it('moves two adjacent siblings forward', () => {
199
+    tt.deselectAll()
200
+      .clickShape('4')
201
+      .clickShape('1', { shiftKey: true })
202
+      .send('MOVED', {
203
+        type: MoveType.Forward,
204
+      })
205
+
206
+    expect(tt.idsAreSelected(['1', '4'])).toBe(true)
207
+
208
+    expect(
209
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
210
+        .sort((a, b) => a.childIndex - b.childIndex)
211
+        .map((shape) => shape.id)
212
+    ).toStrictEqual(['3', '2', '1', '4'])
213
+  })
214
+
215
+  it('moves two non-adjacent siblings forward', () => {
216
+    tt.deselectAll()
217
+      .clickShape('3')
218
+      .clickShape('1', { shiftKey: true })
219
+      .send('MOVED', {
220
+        type: MoveType.Forward,
221
+      })
222
+
223
+    expect(
224
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
225
+        .sort((a, b) => a.childIndex - b.childIndex)
226
+        .map((shape) => shape.id)
227
+    ).toStrictEqual(['2', '3', '4', '1'])
228
+  })
229
+
230
+  it('moves two adjacent siblings forward at top index', () => {
231
+    tt.deselectAll()
232
+      .clickShape('3')
233
+      .clickShape('1', { shiftKey: true })
234
+      .send('MOVED', {
235
+        type: MoveType.Forward,
236
+      })
237
+    expect(
238
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
239
+        .sort((a, b) => a.childIndex - b.childIndex)
240
+        .map((shape) => shape.id)
241
+    ).toStrictEqual(['2', '4', '3', '1'])
242
+  })
243
+
244
+  it('moves a shape to front', () => {
245
+    tt.deselectAll().clickShape('2').send('MOVED', {
246
+      type: MoveType.ToFront,
247
+    })
248
+
249
+    expect(
250
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
251
+        .sort((a, b) => a.childIndex - b.childIndex)
252
+        .map((shape) => shape.id)
253
+    ).toStrictEqual(['4', '3', '1', '2'])
254
+  })
255
+
256
+  it('moves two adjacent siblings to front', () => {
257
+    tt.deselectAll()
258
+      .clickShape('3')
259
+      .clickShape('1', { shiftKey: true })
260
+      .send('MOVED', {
261
+        type: MoveType.ToFront,
262
+      })
263
+
264
+    expect(
265
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
266
+        .sort((a, b) => a.childIndex - b.childIndex)
267
+        .map((shape) => shape.id)
268
+    ).toStrictEqual(['4', '2', '3', '1'])
269
+  })
270
+
271
+  it('moves two non-adjacent siblings to front', () => {
272
+    tt.deselectAll()
273
+      .clickShape('4')
274
+      .clickShape('3', { shiftKey: true })
275
+      .send('MOVED', {
276
+        type: MoveType.ToFront,
277
+      })
278
+
279
+    expect(
280
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
281
+        .sort((a, b) => a.childIndex - b.childIndex)
282
+        .map((shape) => shape.id)
283
+    ).toStrictEqual(['2', '1', '4', '3'])
284
+  })
285
+
286
+  it('moves siblings already at front to front', () => {
287
+    tt.deselectAll()
288
+      .clickShape('4')
289
+      .clickShape('3', { shiftKey: true })
290
+      .send('MOVED', {
291
+        type: MoveType.ToFront,
292
+      })
293
+
294
+    expect(
295
+      Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
296
+        .sort((a, b) => a.childIndex - b.childIndex)
297
+        .map((shape) => shape.id)
298
+    ).toStrictEqual(['2', '1', '4', '3'])
299
+  })
300
+})

+ 46
- 0
__tests__/coop.test.ts Datei anzeigen

@@ -0,0 +1,46 @@
1
+import state from 'state'
2
+import coopState from 'state/coop/coop-state'
3
+import * as json from './__mocks__/document.json'
4
+
5
+state.reset()
6
+state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
7
+state.send('CLEARED_PAGE')
8
+
9
+coopState.reset()
10
+
11
+describe('coop', () => {
12
+  it('joins a room', () => {
13
+    // TODO
14
+    null
15
+  })
16
+
17
+  it('leaves a room', () => {
18
+    // TODO
19
+    null
20
+  })
21
+
22
+  it('rejoins a room', () => {
23
+    // TODO
24
+    null
25
+  })
26
+
27
+  it('handles another user joining room', () => {
28
+    // TODO
29
+    null
30
+  })
31
+
32
+  it('handles another user leaving room', () => {
33
+    // TODO
34
+    null
35
+  })
36
+
37
+  it('sends mouse movements', () => {
38
+    // TODO
39
+    null
40
+  })
41
+
42
+  it('receives mouse movements', () => {
43
+    // TODO
44
+    null
45
+  })
46
+})

+ 4
- 0
__tests__/create.test.ts Datei anzeigen

@@ -7,18 +7,22 @@ state.send('CLEARED_PAGE')
7 7
 
8 8
 describe('arrow shape', () => {
9 9
   it('creates a shape', () => {
10
+    // TODO
10 11
     null
11 12
   })
12 13
 
13 14
   it('cancels shape while creating', () => {
15
+    // TODO
14 16
     null
15 17
   })
16 18
 
17 19
   it('removes shape on undo and restores it on redo', () => {
20
+    // TODO
18 21
     null
19 22
   })
20 23
 
21 24
   it('does not create shape when readonly', () => {
25
+    // TODO
22 26
     null
23 27
   })
24 28
 })

+ 1
- 1
__tests__/dashes.test.ts Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { getPerfectDashProps } from 'utils/dashes'
1
+import { getPerfectDashProps } from 'utils'
2 2
 
3 3
 describe('ellipse dash props', () => {
4 4
   it('renders dashed props on a circle correctly', () => {

+ 79
- 116
__tests__/delete.test.ts Datei anzeigen

@@ -1,157 +1,120 @@
1
-import state from 'state'
2
-import inputs from 'state/inputs'
3 1
 import { ShapeType } from 'types'
4
-import {
5
-  idsAreSelected,
6
-  point,
7
-  rectangleId,
8
-  arrowId,
9
-  getOnlySelectedShape,
10
-  assertShapeProps,
11
-} from './test-utils'
12
-import tld from 'utils/tld'
13
-import * as json from './__mocks__/document.json'
2
+import TestState, { rectangleId, arrowId } from './test-utils'
14 3
 
15 4
 describe('deleting single shapes', () => {
16
-  state.reset()
17
-  state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
5
+  const tt = new TestState()
18 6
 
19
-  it('deletes a shape and undoes the delete', () => {
20
-    state
21
-      .send('CANCELED')
22
-      .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
23
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
7
+  describe('deleting single shapes', () => {
8
+    it('deletes a shape and undoes the delete', () => {
9
+      tt.deselectAll().clickShape(rectangleId).pressDelete()
24 10
 
25
-    expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
11
+      expect(tt.idsAreSelected([])).toBe(true)
26 12
 
27
-    state.send('DELETED')
13
+      expect(tt.getShape(rectangleId)).toBe(undefined)
28 14
 
29
-    expect(idsAreSelected(state.data, [])).toBe(true)
30
-    expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
15
+      tt.undo()
31 16
 
32
-    state.send('UNDO')
17
+      expect(tt.getShape(rectangleId)).toBeTruthy()
18
+      expect(tt.idsAreSelected([rectangleId])).toBe(true)
33 19
 
34
-    expect(tld.getShape(state.data, rectangleId)).toBeTruthy()
35
-    expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
20
+      tt.redo()
36 21
 
37
-    state.send('REDO')
22
+      expect(tt.getShape(rectangleId)).toBe(undefined)
23
+    })
24
+  })
38 25
 
39
-    expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
26
+  describe('deleting and restoring grouped shapes', () => {
27
+    it('creates a group', () => {
28
+      tt.reset()
29
+        .deselectAll()
30
+        .clickShape(rectangleId)
31
+        .clickShape(arrowId, { shiftKey: true })
32
+        .send('GROUPED')
40 33
 
41
-    state.send('UNDO')
42
-  })
43
-})
34
+      const group = tt.getOnlySelectedShape()
35
+
36
+      // Should select the group
37
+      expect(tt.assertShapeProps(group, { type: ShapeType.Group })).toBe(true)
44 38
 
45
-describe('deletes and restores grouped shapes', () => {
46
-  state.reset()
47
-  state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
39
+      const arrow = tt.getShape(arrowId)
48 40
 
49
-  it('creates a group', () => {
50
-    state
51
-      .send('CANCELED')
52
-      .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
53
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
54
-      .send(
55
-        'POINTED_SHAPE',
56
-        inputs.pointerDown(point({ shiftKey: true }), arrowId)
57
-      )
58
-      .send(
59
-        'STOPPED_POINTING',
60
-        inputs.pointerUp(point({ shiftKey: true }), arrowId)
61
-      )
41
+      // The arrow should be have the group as its parent
42
+      expect(tt.assertShapeProps(arrow, { parentId: group.id })).toBe(true)
43
+    })
62 44
 
63
-    expect(idsAreSelected(state.data, [rectangleId, arrowId])).toBe(true)
45
+    it('selects the new group', () => {
46
+      const groupId = tt.getShape(arrowId).parentId
64 47
 
65
-    state.send('GROUPED')
48
+      expect(tt.idsAreSelected([groupId])).toBe(true)
49
+    })
66 50
 
67
-    const group = getOnlySelectedShape(state.data)
51
+    it('assigns a new parent', () => {
52
+      const groupId = tt.getShape(arrowId).parentId
68 53
 
69
-    // Should select the group
70
-    expect(assertShapeProps(group, { type: ShapeType.Group }))
54
+      expect(groupId === tt.data.currentPageId).toBe(false)
55
+    })
71 56
 
72
-    const arrow = tld.getShape(state.data, arrowId)
57
+    // Rectangle has the same new parent?
58
+    it('assigns new parent to all selected shapes', () => {
59
+      const groupId = tt.getShape(arrowId).parentId
73 60
 
74
-    // The arrow should be have the group as its parent
75
-    expect(assertShapeProps(arrow, { parentId: group.id }))
61
+      expect(tt.hasParent(arrowId, groupId)).toBe(true)
62
+    })
76 63
   })
77 64
 
78
-  // it('selects the new group', () => {
79
-  //   expect(idsAreSelected(state.data, [groupId])).toBe(true)
80
-  // })
65
+  describe('selecting within the group', () => {
66
+    it('selects the group when pointing a shape', () => {
67
+      const groupId = tt.getShape(arrowId).parentId
81 68
 
82
-  // it('assigns a new parent', () => {
83
-  //   expect(groupId === state.data.currentPageId).toBe(false)
84
-  // })
69
+      tt.deselectAll().clickShape(rectangleId)
85 70
 
86
-  // // Rectangle has the same new parent?
87
-  // it('assigns new parent to all selected shapes', () => {
88
-  //   expect(hasParent(state.data, arrowId, groupId)).toBe(true)
89
-  // })
71
+      expect(tt.idsAreSelected([groupId])).toBe(true)
72
+    })
90 73
 
91
-  // // New parent is selected?
92
-  // it('selects the new parent', () => {
93
-  //   expect(idsAreSelected(state.data, [groupId])).toBe(true)
94
-  // })
95
-})
74
+    it('keeps selection when pointing group shape', () => {
75
+      const groupId = tt.getShape(arrowId).parentId
96 76
 
97
-//   // it('selects the group when pointing a shape', () => {
98
-//   //   state
99
-//   //     .send('CANCELED')
100
-//   //     .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
101
-//   //     .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
77
+      tt.deselectAll().clickShape(groupId)
102 78
 
103
-//   //   expect(idsAreSelected(state.data, [groupId])).toBe(true)
104
-//   // })
79
+      expect(tt.idsAreSelected([groupId])).toBe(true)
80
+    })
105 81
 
106
-//   // it('keeps selection when pointing bounds', () => {
107
-//   //   state
108
-//   //     .send('CANCELED')
109
-//   //     .send('POINTED_BOUNDS', inputs.pointerDown(point(), 'bounds'))
110
-//   //     .send('STOPPED_POINTING', inputs.pointerUp(point(), 'bounds'))
82
+    it('selects a grouped shape by double-pointing', () => {
83
+      tt.deselectAll().doubleClickShape(rectangleId)
111 84
 
112
-//   //   expect(idsAreSelected(state.data, [groupId])).toBe(true)
113
-//   // })
85
+      expect(tt.idsAreSelected([rectangleId])).toBe(true)
86
+    })
114 87
 
115
-//   // it('selects a grouped shape by double-pointing', () => {
116
-//   //   state
117
-//   //     .send('CANCELED')
118
-//   //     .send('DOUBLE_POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
119
-//   //     .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
88
+    it('selects a sibling on point after double-pointing into a grouped shape children', () => {
89
+      tt.deselectAll().doubleClickShape(rectangleId).clickShape(arrowId)
120 90
 
121
-//   //   expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
122
-//   // })
91
+      expect(tt.idsAreSelected([arrowId])).toBe(true)
92
+    })
123 93
 
124
-//   // it('selects a sibling on point when selecting a grouped shape', () => {
125
-//   //   state
126
-//   //     .send('POINTED_SHAPE', inputs.pointerDown(point(), arrowId))
127
-//   //     .send('STOPPED_POINTING', inputs.pointerUp(point(), arrowId))
94
+    it('rises up a selection level when escape is pressed', () => {
95
+      const groupId = tt.getShape(arrowId).parentId
128 96
 
129
-//   //   expect(idsAreSelected(state.data, [arrowId])).toBe(true)
130
-//   // })
97
+      tt.deselectAll().doubleClickShape(rectangleId).send('CANCELLED')
131 98
 
132
-//   // it('rises up a selection level when escape is pressed', () => {
133
-//   //   state
134
-//   //     .send('CANCELED')
135
-//   //     .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
136
-//   //     .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
99
+      tt.clickShape(rectangleId)
137 100
 
138
-//   //   expect(idsAreSelected(state.data, [groupId])).toBe(true)
139
-//   // })
101
+      expect(tt.idsAreSelected([groupId])).toBe(true)
102
+    })
140 103
 
141
-//   // it('deletes and restores one shape', () => {
142
-//   //   // Delete the rectangle first
143
-//   //   state.send('UNDO')
104
+    // it('deletes and restores one shape', () => {
105
+    //   // Delete the rectangle first
106
+    //   state.send('UNDO')
144 107
 
145
-//   //   expect(tld.getShape(state.data, rectangleId)).toBeTruthy()
146
-//   //   expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
108
+    //   expect(tld.getShape(tt.data, rectangleId)).toBeTruthy()
109
+    //   expect(tt.idsAreSelected([rectangleId])).toBe(true)
147 110
 
148
-//   //   state.send('REDO')
111
+    //   state.send('REDO')
149 112
 
150
-//   //   expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
113
+    //   expect(tld.getShape(tt.data, rectangleId)).toBe(undefined)
151 114
 
152
-//   //   state.send('UNDO')
115
+    //   state.send('UNDO')
153 116
 
154
-//   //   expect(tld.getShape(state.data, rectangleId)).toBeTruthy()
155
-//   //   expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
156
-//   // })
157
-// })
117
+    //   expect(tld.getShape(tt.data, rectangleId)).toBeTruthy()
118
+    //   expect(tt.idsAreSelected([rectangleId])).toBe(true)
119
+  })
120
+})

+ 61
- 0
__tests__/locked.test.ts Datei anzeigen

@@ -0,0 +1,61 @@
1
+import TestState from './test-utils'
2
+
3
+describe('locked shapes', () => {
4
+  const tt = new TestState()
5
+  tt.resetDocumentState()
6
+
7
+  it('toggles a locked shape', () => {
8
+    // TODO
9
+    null
10
+  })
11
+
12
+  it('selects a locked shape', () => {
13
+    // TODO
14
+    null
15
+  })
16
+
17
+  it('does not translate a locked shape', () => {
18
+    // TODO
19
+    null
20
+  })
21
+
22
+  it('does not translate a locked shape in a group', () => {
23
+    // TODO
24
+    null
25
+  })
26
+
27
+  it('does not rotate a locked shape', () => {
28
+    // TODO
29
+    null
30
+  })
31
+
32
+  it('does not rotate a locked shape in a group', () => {
33
+    // TODO
34
+    null
35
+  })
36
+
37
+  it('dpes not transform a locked single shape', () => {
38
+    // TODO
39
+    null
40
+  })
41
+
42
+  it('does not transform a locked shape in a multiple selection', () => {
43
+    // TODO
44
+    null
45
+  })
46
+
47
+  it('does not transform a locked shape in a group', () => {
48
+    // TODO
49
+    null
50
+  })
51
+
52
+  it('does not change the style of a locked shape', () => {
53
+    // TODO
54
+    null
55
+  })
56
+
57
+  it('does not change the handles of a locked shape', () => {
58
+    // TODO
59
+    null
60
+  })
61
+})

+ 24
- 0
__tests__/project.test.ts Datei anzeigen

@@ -7,12 +7,36 @@ describe('project', () => {
7 7
 
8 8
   it('mounts the state', () => {
9 9
     state.send('MOUNTED')
10
+
10 11
     expect(state.isIn('ready')).toBe(true)
11 12
   })
12 13
 
13 14
   it('loads file from json', () => {
14 15
     state.send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
16
+
15 17
     expect(state.isIn('ready')).toBe(true)
16 18
     expect(state.data.document).toMatchSnapshot('data after mount from file')
17 19
   })
18 20
 })
21
+
22
+describe('restoring project', () => {
23
+  state.reset()
24
+  state.enableLog(true)
25
+
26
+  it('remounts the state after mutating the current state', () => {
27
+    state
28
+      .send('MOUNTED')
29
+      .send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
30
+      .send('CLEARED_PAGE')
31
+
32
+    expect(
33
+      state.data.document.pages[state.data.currentPageId].shapes
34
+    ).toStrictEqual({})
35
+
36
+    state
37
+      .send('MOUNTED')
38
+      .send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
39
+
40
+    expect(state.data.document).toMatchSnapshot('data after re-mount from file')
41
+  })
42
+})

+ 52
- 110
__tests__/selection.test.ts Datei anzeigen

@@ -1,139 +1,81 @@
1
-import state from 'state'
2
-import inputs from 'state/inputs'
3
-import { idsAreSelected, point, rectangleId, arrowId } from './test-utils'
4
-import * as json from './__mocks__/document.json'
5
-
6
-// Mount the state and load the test file from json
7
-state.reset()
8
-state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
1
+import TestState, { rectangleId, arrowId } from './test-utils'
9 2
 
10 3
 describe('selection', () => {
4
+  const tt = new TestState()
5
+
11 6
   it('selects a shape', () => {
12
-    state
13
-      .send('CANCELED')
14
-      .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
15
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
7
+    tt.deselectAll().clickShape(rectangleId)
16 8
 
17
-    expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
9
+    expect(tt.idsAreSelected([rectangleId])).toBe(true)
18 10
   })
19 11
 
20 12
   it('selects and deselects a shape', () => {
21
-    state
22
-      .send('CANCELED')
23
-      .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
24
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
25
-
26
-    expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
27
-
28
-    state
29
-      .send('POINTED_CANVAS', inputs.pointerDown(point(), 'canvas'))
30
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), 'canvas'))
13
+    tt.deselectAll().clickShape(rectangleId).clickCanvas()
31 14
 
32
-    expect(idsAreSelected(state.data, [])).toBe(true)
15
+    expect(tt.idsAreSelected([])).toBe(true)
33 16
   })
34 17
 
35 18
   it('selects multiple shapes', () => {
36
-    expect(idsAreSelected(state.data, [])).toBe(true)
37
-
38
-    state
39
-      .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
40
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
41
-      .send(
42
-        'POINTED_SHAPE',
43
-        inputs.pointerDown(point({ shiftKey: true }), arrowId)
44
-      )
45
-      .send(
46
-        'STOPPED_POINTING',
47
-        inputs.pointerUp(point({ shiftKey: true }), arrowId)
48
-      )
49
-
50
-    expect(idsAreSelected(state.data, [rectangleId, arrowId])).toBe(true)
19
+    tt.deselectAll()
20
+      .clickShape(rectangleId)
21
+      .clickShape(arrowId, { shiftKey: true })
22
+
23
+    expect(tt.idsAreSelected([rectangleId, arrowId])).toBe(true)
51 24
   })
52 25
 
53 26
   it('shift-selects to deselect shapes', () => {
54
-    state
55
-      .send('CANCELLED')
56
-      .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
57
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
58
-      .send(
59
-        'POINTED_SHAPE',
60
-        inputs.pointerDown(point({ shiftKey: true }), arrowId)
61
-      )
62
-      .send(
63
-        'STOPPED_POINTING',
64
-        inputs.pointerUp(point({ shiftKey: true }), arrowId)
65
-      )
66
-      .send(
67
-        'POINTED_SHAPE',
68
-        inputs.pointerDown(point({ shiftKey: true }), rectangleId)
69
-      )
70
-      .send(
71
-        'STOPPED_POINTING',
72
-        inputs.pointerUp(point({ shiftKey: true }), rectangleId)
73
-      )
74
-
75
-    expect(idsAreSelected(state.data, [arrowId])).toBe(true)
27
+    tt.deselectAll()
28
+      .clickShape(rectangleId)
29
+      .clickShape(arrowId, { shiftKey: true })
30
+      .clickShape(rectangleId, { shiftKey: true })
31
+
32
+    expect(tt.idsAreSelected([arrowId])).toBe(true)
33
+  })
34
+
35
+  it('single-selects shape in selection on click', () => {
36
+    tt.deselectAll()
37
+      .clickShape(rectangleId)
38
+      .clickShape(arrowId, { shiftKey: true })
39
+      .clickShape(arrowId)
40
+
41
+    expect(tt.idsAreSelected([arrowId])).toBe(true)
76 42
   })
77 43
 
78
-  it('single-selects shape in selection on pointerup', () => {
79
-    state
80
-      .send('CANCELLED')
81
-      .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
82
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
83
-      .send(
84
-        'POINTED_SHAPE',
85
-        inputs.pointerDown(point({ shiftKey: true }), arrowId)
86
-      )
87
-      .send(
88
-        'STOPPED_POINTING',
89
-        inputs.pointerUp(point({ shiftKey: true }), arrowId)
90
-      )
44
+  it('single-selects shape in selection on pointerup only', () => {
45
+    tt.deselectAll()
46
+      .clickShape(rectangleId)
47
+      .clickShape(arrowId, { shiftKey: true })
91 48
 
92
-    expect(idsAreSelected(state.data, [rectangleId, arrowId])).toBe(true)
49
+    expect(tt.idsAreSelected([rectangleId, arrowId])).toBe(true)
93 50
 
94
-    state.send('POINTED_SHAPE', inputs.pointerDown(point(), arrowId))
51
+    tt.startClick(arrowId)
95 52
 
96
-    expect(idsAreSelected(state.data, [rectangleId, arrowId])).toBe(true)
53
+    expect(tt.idsAreSelected([rectangleId, arrowId])).toBe(true)
97 54
 
98
-    state.send('STOPPED_POINTING', inputs.pointerUp(point(), arrowId))
55
+    tt.stopClick(arrowId)
99 56
 
100
-    expect(idsAreSelected(state.data, [arrowId])).toBe(true)
57
+    expect(tt.idsAreSelected([arrowId])).toBe(true)
101 58
   })
102 59
 
103 60
   it('selects shapes if shift key is lifted before pointerup', () => {
104
-    state
105
-      .send('CANCELLED')
106
-      .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
107
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
108
-      .send(
109
-        'POINTED_SHAPE',
110
-        inputs.pointerDown(point({ shiftKey: true }), arrowId)
111
-      )
112
-      .send(
113
-        'STOPPED_POINTING',
114
-        inputs.pointerUp(point({ shiftKey: true }), arrowId)
115
-      )
116
-      .send(
117
-        'POINTED_SHAPE',
118
-        inputs.pointerDown(point({ shiftKey: true }), arrowId)
119
-      )
120
-      .send('STOPPED_POINTING', inputs.pointerUp(point(), arrowId))
121
-
122
-    expect(idsAreSelected(state.data, [arrowId])).toBe(true)
61
+    tt.deselectAll()
62
+      .clickShape(rectangleId)
63
+      .clickShape(arrowId, { shiftKey: true })
64
+      .startClick(rectangleId, { shiftKey: true })
65
+      .stopClick(rectangleId)
66
+
67
+    expect(tt.idsAreSelected([rectangleId])).toBe(true)
123 68
   })
124 69
 
125 70
   it('does not select on meta-click', () => {
126
-    state
127
-      .send('CANCELLED')
128
-      .send(
129
-        'POINTED_SHAPE',
130
-        inputs.pointerDown(point({ ctrlKey: true }), rectangleId)
131
-      )
132
-      .send(
133
-        'STOPPED_POINTING',
134
-        inputs.pointerUp(point({ ctrlKey: true }), rectangleId)
135
-      )
136
-
137
-    expect(idsAreSelected(state.data, [])).toBe(true)
71
+    tt.deselectAll().clickShape(rectangleId, { ctrlKey: true })
72
+
73
+    expect(tt.idsAreSelected([])).toBe(true)
74
+  })
75
+
76
+  it('does not select on meta-shift-click', () => {
77
+    tt.deselectAll().clickShape(rectangleId, { ctrlKey: true, shiftKey: true })
78
+
79
+    expect(tt.idsAreSelected([])).toBe(true)
138 80
   })
139 81
 })

+ 21
- 9
__tests__/shapes/arrow.test.ts Datei anzeigen

@@ -7,62 +7,74 @@ state.send('CLEARED_PAGE')
7 7
 
8 8
 describe('arrow shape', () => {
9 9
   it('creates shape', () => {
10
+    // TODO
10 11
     null
11 12
   })
12 13
 
13 14
   it('cancels shape while creating', () => {
15
+    // TODO
14 16
     null
15 17
   })
16 18
 
17 19
   it('moves shape', () => {
20
+    // TODO
18 21
     null
19 22
   })
20 23
 
21 24
   it('rotates shape', () => {
25
+    // TODO
22 26
     null
23 27
   })
24 28
 
25
-  it('measures bounds', () => {
29
+  it('rotates shape in a group', () => {
30
+    // TODO
26 31
     null
27 32
   })
28 33
 
29
-  it('measures rotated bounds', () => {
34
+  it('measures shape bounds', () => {
35
+    // TODO
30 36
     null
31 37
   })
32 38
 
33
-  it('transforms single', () => {
39
+  it('measures shape rotated bounds', () => {
40
+    // TODO
41
+    null
42
+  })
43
+
44
+  it('transforms single shape', () => {
45
+    // TODO
34 46
     null
35 47
   })
36 48
 
37 49
   it('transforms in a group', () => {
50
+    // TODO
38 51
     null
39 52
   })
40 53
 
41 54
   /* -------------------- Specific -------------------- */
42 55
 
43 56
   it('creates compass-aligned shape with shift key', () => {
57
+    // TODO
44 58
     null
45 59
   })
46 60
 
47 61
   it('changes start handle', () => {
62
+    // TODO
48 63
     null
49 64
   })
50 65
 
51 66
   it('changes end handle', () => {
67
+    // TODO
52 68
     null
53 69
   })
54 70
 
55 71
   it('changes bend handle', () => {
72
+    // TODO
56 73
     null
57 74
   })
58 75
 
59 76
   it('resets bend handle when double-pointed', () => {
60
-    null
61
-  })
62
-
63
-  /* -------------------- Readonly -------------------- */
64
-
65
-  it('does not create shape when readonly', () => {
77
+    // TODO
66 78
     null
67 79
   })
68 80
 })

+ 28
- 0
__tests__/style.test.ts Datei anzeigen

@@ -0,0 +1,28 @@
1
+import state from 'state'
2
+import * as json from './__mocks__/document.json'
3
+
4
+state.reset()
5
+state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
6
+state.send('CLEARED_PAGE')
7
+
8
+describe('shape styles', () => {
9
+  it('sets the color style of a shape', () => {
10
+    // TODO
11
+    null
12
+  })
13
+
14
+  it('sets the size style of a shape', () => {
15
+    // TODO
16
+    null
17
+  })
18
+
19
+  it('sets the dash style of a shape', () => {
20
+    // TODO
21
+    null
22
+  })
23
+
24
+  it('sets the isFilled style of a shape', () => {
25
+    // TODO
26
+    null
27
+  })
28
+})

+ 567
- 64
__tests__/test-utils.ts Datei anzeigen

@@ -1,11 +1,18 @@
1
-import { Data, Shape, ShapeType } from 'types'
1
+import _state from 'state'
2 2
 import tld from 'utils/tld'
3
+import inputs from 'state/inputs'
4
+import { createShape, getShapeUtils } from 'state/shape-utils'
5
+import { Data, Shape, ShapeType, ShapeUtility } from 'types'
6
+import { deepCompareArrays, uniqueId, vec } from 'utils'
7
+import * as json from './__mocks__/document.json'
8
+
9
+type State = typeof _state
3 10
 
4 11
 export const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
5 12
 export const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
6 13
 
7 14
 interface PointerOptions {
8
-  id?: string
15
+  id?: number
9 16
   x?: number
10 17
   y?: number
11 18
   shiftKey?: boolean
@@ -13,77 +20,573 @@ interface PointerOptions {
13 20
   ctrlKey?: boolean
14 21
 }
15 22
 
16
-export function point(
17
-  options: PointerOptions = {} as PointerOptions
18
-): PointerEvent {
19
-  const {
20
-    id = '1',
21
-    x = 0,
22
-    y = 0,
23
-    shiftKey = false,
24
-    altKey = false,
25
-    ctrlKey = false,
26
-  } = options
27
-
28
-  return {
29
-    shiftKey,
30
-    altKey,
31
-    ctrlKey,
32
-    pointerId: id,
33
-    clientX: x,
34
-    clientY: y,
35
-  } as any
36
-}
23
+class TestState {
24
+  state: State
37 25
 
38
-export function idsAreSelected(
39
-  data: Data,
40
-  ids: string[],
41
-  strict = true
42
-): boolean {
43
-  const selectedIds = tld.getSelectedIds(data)
44
-  return (
45
-    (strict ? selectedIds.size === ids.length : true) &&
46
-    ids.every((id) => selectedIds.has(id))
47
-  )
48
-}
26
+  constructor() {
27
+    this.state = _state
28
+    this.reset()
29
+  }
49 30
 
50
-export function hasParent(
51
-  data: Data,
52
-  childId: string,
53
-  parentId: string
54
-): boolean {
55
-  return tld.getShape(data, childId).parentId === parentId
56
-}
31
+  /**
32
+   * Reset the test state.
33
+   *
34
+   * ### Example
35
+   *
36
+   *```ts
37
+   * tt.reset()
38
+   *```
39
+   */
40
+  reset(): TestState {
41
+    this.state.reset()
42
+    this.state
43
+      .send('MOUNTED')
44
+      .send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
57 45
 
58
-export function getOnlySelectedShape(data: Data): Shape {
59
-  const selectedShapes = tld.getSelectedShapes(data)
60
-  return selectedShapes.length === 1 ? selectedShapes[0] : undefined
61
-}
46
+    return this
47
+  }
48
+
49
+  /**
50
+   * Reset the document state. Will remove all shapes and extra pages.
51
+   *
52
+   * ### Example
53
+   *
54
+   *```ts
55
+   * tt.resetDocumentState()
56
+   *```
57
+   */
58
+  resetDocumentState(): TestState {
59
+    this.state.send('RESET_DOCUMENT_STATE')
60
+    return this
61
+  }
62
+
63
+  /**
64
+   * Send a message to the state.
65
+   *
66
+   * ### Example
67
+   *
68
+   *```ts
69
+   * tt.send("MOVED_TO_FRONT")
70
+   *```
71
+   */
72
+  send(eventName: string, payload?: unknown): TestState {
73
+    this.state.send(eventName, payload)
74
+    return this
75
+  }
76
+
77
+  /**
78
+   * Create a new shape on the current page. Optionally provide an id.
79
+   *
80
+   * ### Example
81
+   *
82
+   *```ts
83
+   * tt.createShape({ type: ShapeType.Rectangle, point: [100, 100]})
84
+   * tt.createShape({ type: ShapeType.Rectangle, point: [100, 100]}, "myId")
85
+   *```
86
+   */
87
+  createShape(props: Partial<Shape>, id = uniqueId()): TestState {
88
+    const shape = createShape(props.type, props)
89
+    getShapeUtils(shape).setProperty(shape, 'id', id)
90
+    this.data.document.pages[this.data.currentPageId].shapes[shape.id] = shape
91
+    return this
92
+  }
62 93
 
63
-export function assertShapeType(
64
-  data: Data,
65
-  shapeId: string,
66
-  type: ShapeType
67
-): boolean {
68
-  const shape = tld.getShape(data, shapeId)
69
-  if (shape.type !== type) {
70
-    throw new TypeError(
71
-      `expected shape ${shapeId} to be of type ${type}, found ${shape?.type} instead`
94
+  /**
95
+   * Get whether the provided ids are the current selected ids. If the `strict` argument is `true`, then the result will be false if the state has selected ids in addition to those provided.
96
+   *
97
+   * ### Example
98
+   *
99
+   *```ts
100
+   * tt.idsAreSelected(state.data, ['rectangleId', 'ellipseId'])
101
+   * tt.idsAreSelected(state.data, ['rectangleId', 'ellipseId'], true)
102
+   *```
103
+   */
104
+  idsAreSelected(ids: string[], strict = true): boolean {
105
+    const selectedIds = tld.getSelectedIds(this.data)
106
+    return (
107
+      (strict ? selectedIds.size === ids.length : true) &&
108
+      ids.every((id) => selectedIds.has(id))
72 109
     )
73 110
   }
74
-  return true
75
-}
76 111
 
77
-export function assertShapeProps<T extends Shape>(
78
-  shape: T,
79
-  props: { [K in keyof Partial<T>]: T[K] }
80
-): boolean {
81
-  for (const key in props) {
82
-    if (shape[key] !== props[key]) {
112
+  /**
113
+   * Get whether the shape with the provided id has the provided parent id.
114
+   *
115
+   * ### Example
116
+   *
117
+   *```ts
118
+   * tt.hasParent('childId', 'parentId')
119
+   *```
120
+   */
121
+  hasParent(childId: string, parentId: string): boolean {
122
+    return tld.getShape(this.data, childId).parentId === parentId
123
+  }
124
+
125
+  /**
126
+   * Get the only selected shape. If more than one shape is selected, the test will fail.
127
+   *
128
+   * ### Example
129
+   *
130
+   *```ts
131
+   * tt.getOnlySelectedShape()
132
+   *```
133
+   */
134
+  getOnlySelectedShape(): Shape {
135
+    const selectedShapes = tld.getSelectedShapes(this.data)
136
+    return selectedShapes.length === 1 ? selectedShapes[0] : undefined
137
+  }
138
+
139
+  /**
140
+   * Assert that a shape has the provided type.
141
+   *
142
+   * ### Example
143
+   *
144
+   *```ts
145
+   * tt.example
146
+   *```
147
+   */
148
+  assertShapeType(shapeId: string, type: ShapeType): boolean {
149
+    const shape = tld.getShape(this.data, shapeId)
150
+    if (shape.type !== type) {
83 151
       throw new TypeError(
84
-        `expected shape ${shape.id} to have property ${key}: ${props[key]}, found ${key}: ${shape[key]} instead`
152
+        `expected shape ${shapeId} to be of type ${type}, found ${shape?.type} instead`
85 153
       )
86 154
     }
155
+    return true
156
+  }
157
+
158
+  /**
159
+   * Assert that the provided shape has the provided props.
160
+   *
161
+   * ### Example
162
+   *
163
+   *```
164
+   * tt.assertShapeProps(myShape, { point: [0,0], style: { color: ColorStyle.Blue } } )
165
+   *```
166
+   */
167
+  assertShapeProps<T extends Shape>(
168
+    shape: T,
169
+    props: { [K in keyof Partial<T>]: T[K] }
170
+  ): boolean {
171
+    for (const key in props) {
172
+      let result: boolean
173
+      const value = props[key]
174
+
175
+      if (Array.isArray(value)) {
176
+        result = deepCompareArrays(value, shape[key] as typeof value)
177
+      } else if (typeof value === 'object') {
178
+        const target = shape[key] as typeof value
179
+        result =
180
+          target &&
181
+          Object.entries(value).every(([k, v]) => target[k] === props[key][v])
182
+      } else {
183
+        result = shape[key] === value
184
+      }
185
+
186
+      if (!result) {
187
+        throw new TypeError(
188
+          `expected shape ${shape.id} to have property ${key}: ${props[key]}, found ${key}: ${shape[key]} instead`
189
+        )
190
+      }
191
+    }
192
+
193
+    return true
194
+  }
195
+
196
+  /**
197
+   * Click a shape.
198
+   *
199
+   * ### Example
200
+   *
201
+   *```ts
202
+   * tt.clickShape("myShapeId")
203
+   *```
204
+   */
205
+  clickShape(id: string, options: PointerOptions = {}): TestState {
206
+    this.state
207
+      .send('POINTED_SHAPE', inputs.pointerDown(TestState.point(options), id))
208
+      .send('STOPPED_POINTING', inputs.pointerUp(TestState.point(options), id))
209
+
210
+    return this
211
+  }
212
+
213
+  /**
214
+   * Start a click (but do not stop it).
215
+   *
216
+   * ### Example
217
+   *
218
+   *```ts
219
+   * tt.startClick("myShapeId")
220
+   *```
221
+   */
222
+  startClick(id: string, options: PointerOptions = {}): TestState {
223
+    this.state.send(
224
+      'POINTED_SHAPE',
225
+      inputs.pointerDown(TestState.point(options), id)
226
+    )
227
+
228
+    return this
229
+  }
230
+
231
+  /**
232
+   * Stop a click (after starting it).
233
+   *
234
+   * ### Example
235
+   *
236
+   *```ts
237
+   * tt.stopClick("myShapeId")
238
+   *```
239
+   */
240
+  stopClick(id: string, options: PointerOptions = {}): TestState {
241
+    this.state.send(
242
+      'STOPPED_POINTING',
243
+      inputs.pointerUp(TestState.point(options), id)
244
+    )
245
+
246
+    return this
247
+  }
248
+
249
+  /**
250
+   * Double click a shape.
251
+   *
252
+   * ### Example
253
+   *
254
+   *```ts
255
+   * tt.clickShape("myShapeId")
256
+   *```
257
+   */
258
+  doubleClickShape(id: string, options: PointerOptions = {}): TestState {
259
+    this.state
260
+      .send(
261
+        'DOUBLE_POINTED_SHAPE',
262
+        inputs.pointerDown(TestState.point(options), id)
263
+      )
264
+      .send('STOPPED_POINTING', inputs.pointerUp(TestState.point(options), id))
265
+
266
+    return this
267
+  }
268
+
269
+  /**
270
+   * Click the canvas.
271
+   *
272
+   * ### Example
273
+   *
274
+   *```ts
275
+   * tt.clickCanvas("myShapeId")
276
+   *```
277
+   */
278
+  clickCanvas(options: PointerOptions = {}): TestState {
279
+    this.state
280
+      .send(
281
+        'POINTED_CANVAS',
282
+        inputs.pointerDown(TestState.point(options), 'canvas')
283
+      )
284
+      .send(
285
+        'STOPPED_POINTING',
286
+        inputs.pointerUp(TestState.point(options), 'canvas')
287
+      )
288
+
289
+    return this
290
+  }
291
+
292
+  /**
293
+   * Click the background / body of the bounding box.
294
+   *
295
+   * ### Example
296
+   *
297
+   *```ts
298
+   * tt.clickBounds()
299
+   *```
300
+   */
301
+  clickBounds(options: PointerOptions = {}): TestState {
302
+    this.state
303
+      .send(
304
+        'POINTED_BOUNDS',
305
+        inputs.pointerDown(TestState.point(options), 'bounds')
306
+      )
307
+      .send(
308
+        'STOPPED_POINTING',
309
+        inputs.pointerUp(TestState.point(options), 'bounds')
310
+      )
311
+
312
+    return this
313
+  }
314
+
315
+  /**
316
+   * Move the pointer to a new point, or to several points in order.
317
+   *
318
+   * ### Example
319
+   *
320
+   *```ts
321
+   * tt.movePointerTo([100, 100])
322
+   * tt.movePointerTo([100, 100], { shiftKey: true })
323
+   * tt.movePointerTo([[100, 100], [150, 150], [200, 200]])
324
+   *```
325
+   */
326
+  movePointerTo(
327
+    to: number[] | number[][],
328
+    options: Omit<PointerOptions, 'x' | 'y'> = {}
329
+  ): TestState {
330
+    if (Array.isArray(to[0])) {
331
+      ;(to as number[][]).forEach(([x, y]) => {
332
+        this.state.send(
333
+          'MOVED_POINTER',
334
+          inputs.pointerMove(TestState.point({ x, y, ...options }))
335
+        )
336
+      })
337
+    } else {
338
+      const [x, y] = to as number[]
339
+      this.state.send(
340
+        'MOVED_POINTER',
341
+        inputs.pointerMove(TestState.point({ x, y, ...options }))
342
+      )
343
+    }
344
+
345
+    return this
346
+  }
347
+
348
+  /**
349
+   * Move the pointer by a delta.
350
+   *
351
+   * ### Example
352
+   *
353
+   *```ts
354
+   * tt.movePointerBy([10,10])
355
+   * tt.movePointerBy([10,10], { shiftKey: true })
356
+   *```
357
+   */
358
+  movePointerBy(
359
+    by: number[] | number[][],
360
+    options: Omit<PointerOptions, 'x' | 'y'> = {}
361
+  ): TestState {
362
+    let pt = inputs.pointer?.point || [0, 0]
363
+
364
+    if (Array.isArray(by[0])) {
365
+      ;(by as number[][]).forEach((delta) => {
366
+        pt = vec.add(pt, delta)
367
+
368
+        this.state.send(
369
+          'MOVED_POINTER',
370
+          inputs.pointerMove(
371
+            TestState.point({ x: pt[0], y: pt[1], ...options })
372
+          )
373
+        )
374
+      })
375
+    } else {
376
+      pt = vec.add(pt, by as number[])
377
+
378
+      this.state.send(
379
+        'MOVED_POINTER',
380
+        inputs.pointerMove(TestState.point({ x: pt[0], y: pt[1], ...options }))
381
+      )
382
+    }
383
+
384
+    return this
385
+  }
386
+
387
+  /**
388
+   * Move pointer over a shape. Will move the pointer to the top-left corner of the shape.
389
+   *
390
+   * ###
391
+   * ```
392
+   * tt.movePointerOverShape('myShapeId', [100, 100])
393
+   * ```
394
+   */
395
+  movePointerOverShape(
396
+    id: string,
397
+    options: Omit<PointerOptions, 'x' | 'y'> = {}
398
+  ): TestState {
399
+    const shape = tld.getShape(this.state.data, id)
400
+    const [x, y] = vec.add(shape.point, [1, 1])
401
+
402
+    this.state.send(
403
+      'MOVED_OVER_SHAPE',
404
+      inputs.pointerEnter(TestState.point({ x, y, ...options }), id)
405
+    )
406
+
407
+    return this
408
+  }
409
+
410
+  /**
411
+   * Move the pointer over a group. Will move the pointer to the top-left corner of the group.
412
+   *
413
+   * ### Example
414
+   *
415
+   *```ts
416
+   * tt.movePointerOverHandle('myGroupId')
417
+   * tt.movePointerOverHandle('myGroupId', { shiftKey: true })
418
+   *```
419
+   */
420
+  movePointerOverGroup(
421
+    id: string,
422
+    options: Omit<PointerOptions, 'x' | 'y'> = {}
423
+  ): TestState {
424
+    const shape = tld.getShape(this.state.data, id)
425
+    const [x, y] = vec.add(shape.point, [1, 1])
426
+
427
+    this.state.send(
428
+      'MOVED_OVER_GROUP',
429
+      inputs.pointerEnter(TestState.point({ x, y, ...options }), id)
430
+    )
431
+
432
+    return this
433
+  }
434
+
435
+  /**
436
+   * Move the pointer over a handle. Will move the pointer to the top-left corner of the handle.
437
+   *
438
+   * ### Example
439
+   *
440
+   *```ts
441
+   * tt.movePointerOverHandle('bend')
442
+   * tt.movePointerOverHandle('bend', { shiftKey: true })
443
+   *```
444
+   */
445
+  movePointerOverHandle(
446
+    id: string,
447
+    options: Omit<PointerOptions, 'x' | 'y'> = {}
448
+  ): TestState {
449
+    const shape = tld.getShape(this.state.data, id)
450
+    const handle = shape.handles?.[id]
451
+    const [x, y] = vec.add(handle.point, [1, 1])
452
+
453
+    this.state.send(
454
+      'MOVED_OVER_HANDLE',
455
+      inputs.pointerEnter(TestState.point({ x, y, ...options }), id)
456
+    )
457
+
458
+    return this
459
+  }
460
+
461
+  /**
462
+   * Deselect all shapes.
463
+   *
464
+   * ### Example
465
+   *
466
+   *```ts
467
+   * tt.deselectAll()
468
+   *```
469
+   */
470
+  deselectAll(): TestState {
471
+    this.state.send('DESELECTED_ALL')
472
+    return this
473
+  }
474
+
475
+  /**
476
+   * Delete the selected shapes
477
+   *
478
+   * ### Example
479
+   *
480
+   *```ts
481
+   * tt.pressDelete()
482
+   *```
483
+   */
484
+  pressDelete(): TestState {
485
+    this.state.send('DELETED')
486
+    return this
487
+  }
488
+
489
+  /**
490
+   * Get a shape and test it.
491
+   *
492
+   * ### Example
493
+   *
494
+   *```ts
495
+   * tt.testShape("myShapeId", myShape => myShape )
496
+   *```
497
+   */
498
+  testShape<T extends Shape>(
499
+    id: string,
500
+    fn: (shape: T, shapeUtils: ShapeUtility<T>) => boolean
501
+  ): boolean {
502
+    const shape = this.getShape<T>(id)
503
+    return fn(shape, shape && getShapeUtils(shape))
504
+  }
505
+
506
+  /**
507
+   * Get a shape
508
+   *
509
+   * ### Example
510
+   *
511
+   *```ts
512
+   * tt.getShape("myShapeId")
513
+   *```
514
+   */
515
+  getShape<T extends Shape>(id: string): T {
516
+    return tld.getShape(this.data, id) as T
517
+  }
518
+
519
+  /**
520
+   * Undo.
521
+   *
522
+   * ### Example
523
+   *
524
+   *```ts
525
+   * tt.undo()
526
+   *```
527
+   */
528
+  undo(): TestState {
529
+    this.state.send('UNDO')
530
+    return this
531
+  }
532
+
533
+  /**
534
+   * Redo.
535
+   *
536
+   * ### Example
537
+   *
538
+   *```ts
539
+   * tt.redo()
540
+   *```
541
+   */
542
+  redo(): TestState {
543
+    this.state.send('REDO')
544
+    return this
545
+  }
546
+
547
+  /**
548
+   * Get the state's current data.
549
+   *
550
+   * ### Example
551
+   *
552
+   *```ts
553
+   * tt.data
554
+   *```
555
+   */
556
+  get data(): Readonly<Data> {
557
+    return this.state.data
558
+  }
559
+
560
+  /**
561
+   * Get a fake PointerEvent.
562
+   *
563
+   * ### Example
564
+   *
565
+   *```ts
566
+   * tt.point()
567
+   * tt.point({ x: 0, y: 0})
568
+   * tt.point({ x: 0, y: 0, shiftKey: true } )
569
+   *```
570
+   */
571
+  static point(options: PointerOptions = {} as PointerOptions): PointerEvent {
572
+    const {
573
+      id = 1,
574
+      x = 0,
575
+      y = 0,
576
+      shiftKey = false,
577
+      altKey = false,
578
+      ctrlKey = false,
579
+    } = options
580
+
581
+    return {
582
+      shiftKey,
583
+      altKey,
584
+      ctrlKey,
585
+      pointerId: id,
586
+      clientX: x,
587
+      clientY: y,
588
+    } as any
87 589
   }
88
-  return true
89 590
 }
591
+
592
+export default TestState

+ 91
- 0
__tests__/transform.test.ts Datei anzeigen

@@ -0,0 +1,91 @@
1
+import state from 'state'
2
+import * as json from './__mocks__/document.json'
3
+
4
+state.reset()
5
+state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
6
+state.send('CLEARED_PAGE')
7
+
8
+describe('transforms shapes', () => {
9
+  it('transforms from the top edge', () => {
10
+    // TODO
11
+    null
12
+  })
13
+
14
+  it('transforms from the right edge', () => {
15
+    // TODO
16
+    null
17
+  })
18
+
19
+  it('transforms from the bottom edge', () => {
20
+    // TODO
21
+    null
22
+  })
23
+
24
+  it('transforms from the left edge', () => {
25
+    // TODO
26
+    null
27
+  })
28
+
29
+  it('transforms from the top-left corner', () => {
30
+    // TODO
31
+    null
32
+  })
33
+
34
+  it('transforms from the top-right corner', () => {
35
+    // TODO
36
+    null
37
+  })
38
+
39
+  it('transforms from the bottom-right corner', () => {
40
+    // TODO
41
+    null
42
+  })
43
+
44
+  it('transforms from the bottom-left corner', () => {
45
+    // TODO
46
+    null
47
+  })
48
+})
49
+
50
+describe('transforms shapes while aspect-ratio locked', () => {
51
+  // Fixed
52
+
53
+  it('transforms from the top edge while aspect-ratio locked', () => {
54
+    // TODO
55
+    null
56
+  })
57
+
58
+  it('transforms from the right edge while aspect-ratio locked', () => {
59
+    // TODO
60
+    null
61
+  })
62
+
63
+  it('transforms from the bottom edge while aspect-ratio locked', () => {
64
+    // TODO
65
+    null
66
+  })
67
+  it('transforms from the left edge while aspect-ratio locked', () => {
68
+    // TODO
69
+    null
70
+  })
71
+
72
+  it('transforms from the top-left corner while aspect-ratio locked', () => {
73
+    // TODO
74
+    null
75
+  })
76
+
77
+  it('transforms from the top-right corner while aspect-ratio locked', () => {
78
+    // TODO
79
+    null
80
+  })
81
+
82
+  it('transforms from the bottom-right corner while aspect-ratio locked', () => {
83
+    // TODO
84
+    null
85
+  })
86
+
87
+  it('transforms from the bottom-left corner while aspect-ratio locked', () => {
88
+    // TODO
89
+    null
90
+  })
91
+})

+ 38
- 0
__tests__/translate.test.ts Datei anzeigen

@@ -0,0 +1,38 @@
1
+import state from 'state'
2
+import * as json from './__mocks__/document.json'
3
+
4
+state.reset()
5
+state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
6
+state.send('CLEARED_PAGE')
7
+
8
+describe('translates shapes', () => {
9
+  it('translates a single selected shape', () => {
10
+    // TODO
11
+    null
12
+  })
13
+
14
+  it('translates multiple selected shape', () => {
15
+    // TODO
16
+    null
17
+  })
18
+
19
+  it('translates while axis-locked', () => {
20
+    // TODO
21
+    null
22
+  })
23
+
24
+  it('translates after leaving axis-locked state', () => {
25
+    // TODO
26
+    null
27
+  })
28
+
29
+  it('creates clones while translating', () => {
30
+    // TODO
31
+    null
32
+  })
33
+
34
+  it('removes clones after leaving cloning state', () => {
35
+    // TODO
36
+    null
37
+  })
38
+})

+ 1
- 1
components/canvas/canvas.tsx Datei anzeigen

@@ -71,8 +71,8 @@ const MainSVG = styled('svg', {
71 71
 
72 72
 function ErrorFallback({ error, resetErrorBoundary }) {
73 73
   React.useEffect(() => {
74
-    console.error(error)
75 74
     const copy = 'Sorry, something went wrong. Clear canvas and continue?'
75
+    console.error(error)
76 76
     if (window.confirm(copy)) {
77 77
       state.send('CLEARED_PAGE')
78 78
       resetErrorBoundary()

+ 16
- 15
components/canvas/coop/coop.tsx Datei anzeigen

@@ -6,23 +6,24 @@ export default function Presence(): JSX.Element {
6 6
   const others = useCoopSelector((s) => s.data.others)
7 7
   const currentPageId = useSelector((s) => s.data.currentPageId)
8 8
 
9
+  if (!others) return null
10
+
9 11
   return (
10 12
     <>
11
-      {Object.values(others).map(({ connectionId, presence }) => {
12
-        if (presence === null) return null
13
-        if (presence.pageId !== currentPageId) return null
14
-
15
-        return (
16
-          <Cursor
17
-            key={`cursor-${connectionId}`}
18
-            color={'red'}
19
-            duration={presence.duration}
20
-            times={presence.times}
21
-            bufferedXs={presence.bufferedXs}
22
-            bufferedYs={presence.bufferedYs}
23
-          />
24
-        )
25
-      })}
13
+      {Object.values(others)
14
+        .filter(({ presence }) => presence?.pageId === currentPageId)
15
+        .map(({ connectionId, presence }) => {
16
+          return (
17
+            <Cursor
18
+              key={`cursor-${connectionId}`}
19
+              color={'red'}
20
+              duration={presence.duration}
21
+              times={presence.times}
22
+              bufferedXs={presence.bufferedXs}
23
+              bufferedYs={presence.bufferedYs}
24
+            />
25
+          )
26
+        })}
26 27
     </>
27 28
   )
28 29
 }

+ 32
- 13
components/canvas/shape.tsx Datei anzeigen

@@ -8,11 +8,11 @@ import useShapeEvents from 'hooks/useShapeEvents'
8 8
 import vec from 'utils/vec'
9 9
 import { getShapeStyle } from 'state/shape-styles'
10 10
 import useShapeDef from 'hooks/useShape'
11
-import { ShapeUtility } from 'types'
11
+import { BooleanArraySupportOption } from 'prettier'
12 12
 
13 13
 interface ShapeProps {
14 14
   id: string
15
-  isSelecting: boolean
15
+  isSelecting: BooleanArraySupportOption
16 16
 }
17 17
 
18 18
 function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
@@ -51,28 +51,27 @@ function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
51 51
   `
52 52
   })
53 53
 
54
-  // From here on, not reactive—if we're here, we can trust that the
55
-  // shape in state is a shape with changes that we need to render.
54
+  const isCurrentParent = useSelector((s) => {
55
+    return s.data.currentParentId === id
56
+  })
56 57
 
57
-  const shape = tld.getShape(state.data, id)
58
+  const events = useShapeEvents(id, isCurrentParent, rGroup)
58 59
 
59
-  const shapeUtils = shape ? getShapeUtils(shape) : ({} as ShapeUtility<any>)
60
+  const shape = tld.getShape(state.data, id)
60 61
 
61
-  const {
62
-    isParent = false,
63
-    isForeignObject = false,
64
-    canStyleFill = false,
65
-  } = shapeUtils
62
+  if (!shape) return null
66 63
 
67
-  const events = useShapeEvents(id, isParent, rGroup)
64
+  // From here on, not reactive—if we're here, we can trust that the
65
+  // shape in state is a shape with changes that we need to render.
68 66
 
69
-  if (!shape) return null
67
+  const { isParent, isForeignObject, canStyleFill } = getShapeUtils(shape)
70 68
 
71 69
   return (
72 70
     <StyledGroup
73 71
       id={id + '-group'}
74 72
       ref={rGroup}
75 73
       transform={transform}
74
+      isCurrentParent={isCurrentParent}
76 75
       {...events}
77 76
     >
78 77
       {isSelecting &&
@@ -204,4 +203,24 @@ const EventSoak = styled('use', {
204 203
 
205 204
 const StyledGroup = styled('g', {
206 205
   outline: 'none',
206
+
207
+  '& > *[data-shy=true]': {
208
+    opacity: 0,
209
+  },
210
+
211
+  '&:hover': {
212
+    '& > *[data-shy=true]': {
213
+      opacity: 1,
214
+    },
215
+  },
216
+
217
+  variants: {
218
+    isCurrentParent: {
219
+      true: {
220
+        '& > *[data-shy=true]': {
221
+          opacity: 1,
222
+        },
223
+      },
224
+    },
225
+  },
207 226
 })

+ 84
- 58
components/code-panel/types-import.ts Datei anzeigen

@@ -145,43 +145,39 @@ interface GroupShape extends BaseShape {
145 145
   size: number[]
146 146
 }
147 147
 
148
-// type DeepPartial<T> = {
149
-//   [P in keyof T]?: DeepPartial<T[P]>
150
-// }
151
-
152 148
 type ShapeProps<T extends Shape> = {
153 149
   [P in keyof T]?: P extends 'style' ? Partial<T[P]> : T[P]
154 150
 }
155 151
 
156
-type MutableShape =
157
-  | DotShape
158
-  | EllipseShape
159
-  | LineShape
160
-  | RayShape
161
-  | PolylineShape
162
-  | DrawShape
163
-  | RectangleShape
164
-  | ArrowShape
165
-  | TextShape
166
-  | GroupShape
167
-
168
-interface Shapes {
169
-  [ShapeType.Dot]: Readonly<DotShape>
170
-  [ShapeType.Ellipse]: Readonly<EllipseShape>
171
-  [ShapeType.Line]: Readonly<LineShape>
172
-  [ShapeType.Ray]: Readonly<RayShape>
173
-  [ShapeType.Polyline]: Readonly<PolylineShape>
174
-  [ShapeType.Draw]: Readonly<DrawShape>
175
-  [ShapeType.Rectangle]: Readonly<RectangleShape>
176
-  [ShapeType.Arrow]: Readonly<ArrowShape>
177
-  [ShapeType.Text]: Readonly<TextShape>
178
-  [ShapeType.Group]: Readonly<GroupShape>
152
+interface MutableShapes {
153
+  [ShapeType.Dot]: DotShape
154
+  [ShapeType.Ellipse]: EllipseShape
155
+  [ShapeType.Line]: LineShape
156
+  [ShapeType.Ray]: RayShape
157
+  [ShapeType.Polyline]: PolylineShape
158
+  [ShapeType.Draw]: DrawShape
159
+  [ShapeType.Rectangle]: RectangleShape
160
+  [ShapeType.Arrow]: ArrowShape
161
+  [ShapeType.Text]: TextShape
162
+  [ShapeType.Group]: GroupShape
179 163
 }
180 164
 
165
+type MutableShape = MutableShapes[keyof MutableShapes]
166
+
167
+type Shapes = { [K in keyof MutableShapes]: Readonly<MutableShapes[K]> }
168
+
181 169
 type Shape = Readonly<MutableShape>
182 170
 
183 171
 type ShapeByType<T extends ShapeType> = Shapes[T]
184 172
 
173
+type IsParent<T> = 'children' extends RequiredKeys<T> ? T : never
174
+
175
+type ParentShape = {
176
+  [K in keyof MutableShapes]: IsParent<MutableShapes[K]>
177
+}[keyof MutableShapes]
178
+
179
+type ParentTypes = ParentShape['type'] & 'page'
180
+
185 181
 enum Decoration {
186 182
   Arrow = 'Arrow',
187 183
 }
@@ -232,6 +228,15 @@ interface PointerInfo {
232 228
   altKey: boolean
233 229
 }
234 230
 
231
+interface KeyboardInfo {
232
+  key: string
233
+  keys: string[]
234
+  shiftKey: boolean
235
+  ctrlKey: boolean
236
+  metaKey: boolean
237
+  altKey: boolean
238
+}
239
+
235 240
 enum Edge {
236 241
   Top = 'top_edge',
237 242
   Right = 'right_edge',
@@ -276,8 +281,6 @@ interface BoundsSnapshot extends PointSnapshot {
276 281
   nh: number
277 282
 }
278 283
 
279
-type Difference<A, B> = A extends B ? never : A
280
-
281 284
 type ShapeSpecificProps<T extends Shape> = Pick<
282 285
   T,
283 286
   Difference<keyof T, keyof BaseShape>
@@ -561,6 +564,16 @@ interface ShapeUtility<K extends Shape> {
561 564
   shouldRender(this: ShapeUtility<K>, shape: K, previous: K): boolean
562 565
 }
563 566
 
567
+/* -------------------------------------------------- */
568
+/*                      Utilities                     */
569
+/* -------------------------------------------------- */
570
+
571
+type Difference<A, B> = A extends B ? never : A
572
+
573
+type RequiredKeys<T> = {
574
+  [K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? never : K
575
+}[keyof T]
576
+
564 577
 
565 578
 
566 579
 
@@ -695,43 +708,39 @@ interface GroupShape extends BaseShape {
695 708
   size: number[]
696 709
 }
697 710
 
698
-// type DeepPartial<T> = {
699
-//   [P in keyof T]?: DeepPartial<T[P]>
700
-// }
701
-
702 711
 type ShapeProps<T extends Shape> = {
703 712
   [P in keyof T]?: P extends 'style' ? Partial<T[P]> : T[P]
704 713
 }
705 714
 
706
-type MutableShape =
707
-  | DotShape
708
-  | EllipseShape
709
-  | LineShape
710
-  | RayShape
711
-  | PolylineShape
712
-  | DrawShape
713
-  | RectangleShape
714
-  | ArrowShape
715
-  | TextShape
716
-  | GroupShape
717
-
718
-interface Shapes {
719
-  [ShapeType.Dot]: Readonly<DotShape>
720
-  [ShapeType.Ellipse]: Readonly<EllipseShape>
721
-  [ShapeType.Line]: Readonly<LineShape>
722
-  [ShapeType.Ray]: Readonly<RayShape>
723
-  [ShapeType.Polyline]: Readonly<PolylineShape>
724
-  [ShapeType.Draw]: Readonly<DrawShape>
725
-  [ShapeType.Rectangle]: Readonly<RectangleShape>
726
-  [ShapeType.Arrow]: Readonly<ArrowShape>
727
-  [ShapeType.Text]: Readonly<TextShape>
728
-  [ShapeType.Group]: Readonly<GroupShape>
715
+interface MutableShapes {
716
+  [ShapeType.Dot]: DotShape
717
+  [ShapeType.Ellipse]: EllipseShape
718
+  [ShapeType.Line]: LineShape
719
+  [ShapeType.Ray]: RayShape
720
+  [ShapeType.Polyline]: PolylineShape
721
+  [ShapeType.Draw]: DrawShape
722
+  [ShapeType.Rectangle]: RectangleShape
723
+  [ShapeType.Arrow]: ArrowShape
724
+  [ShapeType.Text]: TextShape
725
+  [ShapeType.Group]: GroupShape
729 726
 }
730 727
 
728
+type MutableShape = MutableShapes[keyof MutableShapes]
729
+
730
+type Shapes = { [K in keyof MutableShapes]: Readonly<MutableShapes[K]> }
731
+
731 732
 type Shape = Readonly<MutableShape>
732 733
 
733 734
 type ShapeByType<T extends ShapeType> = Shapes[T]
734 735
 
736
+type IsParent<T> = 'children' extends RequiredKeys<T> ? T : never
737
+
738
+type ParentShape = {
739
+  [K in keyof MutableShapes]: IsParent<MutableShapes[K]>
740
+}[keyof MutableShapes]
741
+
742
+type ParentTypes = ParentShape['type'] & 'page'
743
+
735 744
 enum Decoration {
736 745
   Arrow = 'Arrow',
737 746
 }
@@ -782,6 +791,15 @@ interface PointerInfo {
782 791
   altKey: boolean
783 792
 }
784 793
 
794
+interface KeyboardInfo {
795
+  key: string
796
+  keys: string[]
797
+  shiftKey: boolean
798
+  ctrlKey: boolean
799
+  metaKey: boolean
800
+  altKey: boolean
801
+}
802
+
785 803
 enum Edge {
786 804
   Top = 'top_edge',
787 805
   Right = 'right_edge',
@@ -826,8 +844,6 @@ interface BoundsSnapshot extends PointSnapshot {
826 844
   nh: number
827 845
 }
828 846
 
829
-type Difference<A, B> = A extends B ? never : A
830
-
831 847
 type ShapeSpecificProps<T extends Shape> = Pick<
832 848
   T,
833 849
   Difference<keyof T, keyof BaseShape>
@@ -1111,6 +1127,16 @@ interface ShapeUtility<K extends Shape> {
1111 1127
   shouldRender(this: ShapeUtility<K>, shape: K, previous: K): boolean
1112 1128
 }
1113 1129
 
1130
+/* -------------------------------------------------- */
1131
+/*                      Utilities                     */
1132
+/* -------------------------------------------------- */
1133
+
1134
+type Difference<A, B> = A extends B ? never : A
1135
+
1136
+type RequiredKeys<T> = {
1137
+  [K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? never : K
1138
+}[keyof T]
1139
+
1114 1140
 
1115 1141
 
1116 1142
 

+ 1
- 0
hooks/useLoadOnMount.ts Datei anzeigen

@@ -8,6 +8,7 @@ export default function useLoadOnMount(roomId?: string) {
8 8
 
9 9
     fonts.load('12px Verveine Regular', 'Fonts are loaded!').then(() => {
10 10
       state.send('MOUNTED')
11
+
11 12
       if (roomId !== undefined) {
12 13
         state.send('RT_LOADED_ROOM', { id: roomId })
13 14
       }

+ 19
- 22
hooks/useShapeEvents.ts Datei anzeigen

@@ -6,14 +6,15 @@ import Vec from 'utils/vec'
6 6
 
7 7
 export default function useShapeEvents(
8 8
   id: string,
9
-  isParent: boolean,
9
+  isCurrentParent: boolean,
10 10
   rGroup: MutableRefObject<SVGElement>
11 11
 ) {
12 12
   const handlePointerDown = useCallback(
13 13
     (e: React.PointerEvent) => {
14
-      if (isParent) return
14
+      if (isCurrentParent) return
15 15
       if (!inputs.canAccept(e.pointerId)) return
16 16
       e.stopPropagation()
17
+
17 18
       rGroup.current.setPointerCapture(e.pointerId)
18 19
 
19 20
       const info = inputs.pointerDown(e, id)
@@ -28,30 +29,30 @@ export default function useShapeEvents(
28 29
         state.send('RIGHT_POINTED', info)
29 30
       }
30 31
     },
31
-    [id]
32
+    [id, isCurrentParent]
32 33
   )
33 34
 
34 35
   const handlePointerUp = useCallback(
35 36
     (e: React.PointerEvent) => {
37
+      if (isCurrentParent) return
36 38
       if (!inputs.canAccept(e.pointerId)) return
37 39
       e.stopPropagation()
40
+
38 41
       rGroup.current.releasePointerCapture(e.pointerId)
39 42
       state.send('STOPPED_POINTING', inputs.pointerUp(e, id))
40 43
     },
41
-    [id]
44
+    [id, isCurrentParent]
42 45
   )
43 46
 
44 47
   const handlePointerEnter = useCallback(
45 48
     (e: React.PointerEvent) => {
49
+      if (isCurrentParent) return
46 50
       if (!inputs.canAccept(e.pointerId)) return
51
+      e.stopPropagation()
47 52
 
48
-      if (isParent) {
49
-        state.send('HOVERED_GROUP', inputs.pointerEnter(e, id))
50
-      } else {
51
-        state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
52
-      }
53
+      state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
53 54
     },
54
-    [id]
55
+    [id, isCurrentParent]
55 56
   )
56 57
 
57 58
   const handlePointerMove = useCallback(
@@ -72,26 +73,22 @@ export default function useShapeEvents(
72 73
         return
73 74
       }
74 75
 
75
-      if (isParent) {
76
-        state.send('MOVED_OVER_GROUP', inputs.pointerEnter(e, id))
77
-      } else {
78
-        state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
79
-      }
76
+      if (isCurrentParent) return
77
+
78
+      state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
80 79
     },
81
-    [id]
80
+    [id, isCurrentParent]
82 81
   )
83 82
 
84 83
   const handlePointerLeave = useCallback(
85 84
     (e: React.PointerEvent) => {
85
+      if (isCurrentParent) return
86 86
       if (!inputs.canAccept(e.pointerId)) return
87
+      e.stopPropagation()
87 88
 
88
-      if (isParent) {
89
-        state.send('UNHOVERED_GROUP', { target: id })
90
-      } else {
91
-        state.send('UNHOVERED_SHAPE', { target: id })
92
-      }
89
+      state.send('UNHOVERED_SHAPE', { target: id })
93 90
     },
94
-    [id]
91
+    [id, isCurrentParent]
95 92
   )
96 93
 
97 94
   const handleTouchStart = useCallback((e: React.TouchEvent) => {

+ 2
- 2
package.json Datei anzeigen

@@ -12,7 +12,7 @@
12 12
     "start": "next start",
13 13
     "test-all": "yarn lint && yarn type-check && yarn test",
14 14
     "test:update": "jest --updateSnapshot",
15
-    "test:watch": "jest --watchAll --verbose=false --silent=false",
15
+    "test:watch": "jest --watchAll",
16 16
     "test": "jest --watchAll=false",
17 17
     "type-check": "tsc --pretty --noEmit"
18 18
   },
@@ -96,4 +96,4 @@
96 96
     "tabWidth": 2,
97 97
     "useTabs": false
98 98
   }
99
-}
99
+}

+ 1
- 1
state/code/arrow.ts Datei anzeigen

@@ -1,5 +1,5 @@
1 1
 import CodeShape from './index'
2
-import { uniqueId } from 'utils'
2
+import { uniqueId } from 'utils/utils'
3 3
 import { ArrowShape, Decoration, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 import { getShapeUtils } from 'state/shape-utils'

+ 1
- 1
state/code/control.ts Datei anzeigen

@@ -5,7 +5,7 @@ import {
5 5
   TextCodeControl,
6 6
   VectorCodeControl,
7 7
 } from 'types'
8
-import { uniqueId } from 'utils'
8
+import { uniqueId } from 'utils/utils'
9 9
 
10 10
 export const controls: Record<string, any> = {}
11 11
 

+ 1
- 1
state/code/dot.ts Datei anzeigen

@@ -1,5 +1,5 @@
1 1
 import CodeShape from './index'
2
-import { uniqueId } from 'utils'
2
+import { uniqueId } from 'utils/utils'
3 3
 import { DotShape, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 

+ 1
- 1
state/code/draw.ts Datei anzeigen

@@ -1,5 +1,5 @@
1 1
 import CodeShape from './index'
2
-import { uniqueId } from 'utils'
2
+import { uniqueId } from 'utils/utils'
3 3
 import { DrawShape, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 

+ 1
- 1
state/code/ellipse.ts Datei anzeigen

@@ -1,5 +1,5 @@
1 1
 import CodeShape from './index'
2
-import { uniqueId } from 'utils'
2
+import { uniqueId } from 'utils/utils'
3 3
 import { EllipseShape, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 

+ 1
- 1
state/code/line.ts Datei anzeigen

@@ -1,5 +1,5 @@
1 1
 import CodeShape from './index'
2
-import { uniqueId } from 'utils'
2
+import { uniqueId } from 'utils/utils'
3 3
 import { LineShape, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 

+ 1
- 1
state/code/polyline.ts Datei anzeigen

@@ -1,5 +1,5 @@
1 1
 import CodeShape from './index'
2
-import { uniqueId } from 'utils'
2
+import { uniqueId } from 'utils/utils'
3 3
 import { PolylineShape, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 

+ 1
- 1
state/code/ray.ts Datei anzeigen

@@ -1,5 +1,5 @@
1 1
 import CodeShape from './index'
2
-import { uniqueId } from 'utils'
2
+import { uniqueId } from 'utils/utils'
3 3
 import { RayShape, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 

+ 1
- 1
state/code/rectangle.ts Datei anzeigen

@@ -1,5 +1,5 @@
1 1
 import CodeShape from './index'
2
-import { uniqueId } from 'utils'
2
+import { uniqueId } from 'utils/utils'
3 3
 import { RectangleShape, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 import { getShapeUtils } from 'state/shape-utils'

+ 1
- 1
state/code/text.ts Datei anzeigen

@@ -1,5 +1,5 @@
1 1
 import CodeShape from './index'
2
-import { uniqueId } from 'utils'
2
+import { uniqueId } from 'utils/utils'
3 3
 import { TextShape, ShapeProps, ShapeType } from 'types'
4 4
 import { defaultStyle } from 'state/shape-styles'
5 5
 import { getShapeUtils } from 'state/shape-utils'

+ 2
- 0
state/commands/change-page.ts Datei anzeigen

@@ -16,10 +16,12 @@ export default function changePage(data: Data, toPageId: string): void {
16 16
         storage.savePage(data, data.document.id, fromPageId)
17 17
         storage.loadPage(data, data.document.id, toPageId)
18 18
         data.currentPageId = toPageId
19
+        data.currentParentId = toPageId
19 20
       },
20 21
       undo(data) {
21 22
         storage.loadPage(data, data.document.id, fromPageId)
22 23
         data.currentPageId = fromPageId
24
+        data.currentParentId = fromPageId
23 25
       },
24 26
     })
25 27
   )

+ 1
- 1
state/commands/create-page.ts Datei anzeigen

@@ -1,7 +1,7 @@
1 1
 import Command from './command'
2 2
 import history from '../history'
3 3
 import { Data, Page, PageState } from 'types'
4
-import { uniqueId } from 'utils'
4
+import { uniqueId } from 'utils/utils'
5 5
 import storage from 'state/storage'
6 6
 
7 7
 export default function createPage(data: Data, goToPage = true): void {

+ 1
- 1
state/commands/duplicate.ts Datei anzeigen

@@ -3,7 +3,7 @@ import history from '../history'
3 3
 import { Data } from 'types'
4 4
 import { deepClone } from 'utils'
5 5
 import tld from 'utils/tld'
6
-import { uniqueId } from 'utils'
6
+import { uniqueId } from 'utils/utils'
7 7
 import vec from 'utils/vec'
8 8
 
9 9
 export default function duplicateCommand(data: Data): void {

+ 1
- 0
state/commands/move.ts Datei anzeigen

@@ -57,6 +57,7 @@ export default function moveCommand(data: Data, type: MoveType): void {
57 57
                 .sort((a, b) => b.childIndex - a.childIndex)
58 58
                 .forEach((shape) => moveForward(shape, siblings, visited))
59 59
             }
60
+
60 61
             break
61 62
           }
62 63
           case MoveType.Backward: {

+ 1
- 1
state/commands/paste.ts Datei anzeigen

@@ -3,7 +3,7 @@ import history from '../history'
3 3
 import { Data, Shape } from 'types'
4 4
 import { getCommonBounds, setToArray } from 'utils'
5 5
 import tld from 'utils/tld'
6
-import { uniqueId } from 'utils'
6
+import { uniqueId } from 'utils/utils'
7 7
 import vec from 'utils/vec'
8 8
 import { getShapeUtils } from 'state/shape-utils'
9 9
 import state from 'state/state'

+ 1
- 1
state/coop/client-liveblocks.ts Datei anzeigen

@@ -6,7 +6,7 @@ import {
6 6
   MyPresenceCallback,
7 7
   OthersEventCallback,
8 8
 } from '@liveblocks/client/lib/cjs/types'
9
-import { uniqueId } from 'utils'
9
+import { uniqueId } from 'utils/utils'
10 10
 
11 11
 class CoopClient {
12 12
   id = uniqueId()

+ 1
- 0
state/inputs.tsx Datei anzeigen

@@ -160,6 +160,7 @@ class Inputs {
160 160
     }
161 161
 
162 162
     this.pointer = info
163
+
163 164
     return info
164 165
   }
165 166
 

+ 1
- 1
state/sessions/translate-session.ts Datei anzeigen

@@ -2,7 +2,7 @@ import { Data, GroupShape, Shape, ShapeType } from 'types'
2 2
 import vec from 'utils/vec'
3 3
 import BaseSession from './base-session'
4 4
 import commands from 'state/commands'
5
-import { uniqueId } from 'utils'
5
+import { uniqueId } from 'utils/utils'
6 6
 import { getShapeUtils } from 'state/shape-utils'
7 7
 import tld from 'utils/tld'
8 8
 

+ 55
- 43
state/shape-utils/arrow.tsx Datei anzeigen

@@ -1,12 +1,16 @@
1
-import { getArcLength, uniqueId } from 'utils'
2 1
 import vec from 'utils/vec'
3 2
 import {
3
+  getArcLength,
4
+  uniqueId,
4 5
   getSvgPathFromStroke,
5 6
   rng,
6 7
   getBoundsFromPoints,
7 8
   translateBounds,
8 9
   pointInBounds,
9 10
   pointInCircle,
11
+  circleFromThreePoints,
12
+  isAngleBetween,
13
+  getPerfectDashProps,
10 14
 } from 'utils'
11 15
 import {
12 16
   ArrowShape,
@@ -15,7 +19,6 @@ import {
15 19
   ShapeHandle,
16 20
   ShapeType,
17 21
 } from 'types'
18
-import { circleFromThreePoints, isAngleBetween } from 'utils'
19 22
 import {
20 23
   intersectArcBounds,
21 24
   intersectLineSegmentBounds,
@@ -24,7 +27,6 @@ import { defaultStyle, getShapeStyle } from 'state/shape-styles'
24 27
 import getStroke from 'perfect-freehand'
25 28
 import React from 'react'
26 29
 import { registerShapeUtils } from './register'
27
-import { getPerfectDashProps } from 'utils/dashes'
28 30
 
29 31
 const pathCache = new WeakMap<ArrowShape, string>([])
30 32
 
@@ -37,55 +39,65 @@ function getCtp(shape: ArrowShape) {
37 39
 const arrow = registerShapeUtils<ArrowShape>({
38 40
   boundsCache: new WeakMap([]),
39 41
 
40
-  create(props) {
41
-    const {
42
-      point = [0, 0],
43
-      handles = {
44
-        start: {
45
-          id: 'start',
46
-          index: 0,
47
-          point: [0, 0],
48
-        },
49
-        end: {
50
-          id: 'end',
51
-          index: 1,
52
-          point: [1, 1],
53
-        },
54
-        bend: {
55
-          id: 'bend',
56
-          index: 2,
57
-          point: [0.5, 0.5],
58
-        },
42
+  defaultProps: {
43
+    id: uniqueId(),
44
+    type: ShapeType.Arrow,
45
+    isGenerated: false,
46
+    name: 'Arrow',
47
+    parentId: 'page1',
48
+    childIndex: 0,
49
+    point: [0, 0],
50
+    rotation: 0,
51
+    isAspectRatioLocked: false,
52
+    isLocked: false,
53
+    isHidden: false,
54
+    bend: 0,
55
+    handles: {
56
+      start: {
57
+        id: 'start',
58
+        index: 0,
59
+        point: [0, 0],
59 60
       },
60
-    } = props
61
-
62
-    return {
63
-      id: uniqueId(),
61
+      end: {
62
+        id: 'end',
63
+        index: 1,
64
+        point: [1, 1],
65
+      },
66
+      bend: {
67
+        id: 'bend',
68
+        index: 2,
69
+        point: [0.5, 0.5],
70
+      },
71
+    },
72
+    decorations: {
73
+      start: null,
74
+      middle: null,
75
+      end: Decoration.Arrow,
76
+    },
77
+    style: {
78
+      ...defaultStyle,
79
+      isFilled: false,
80
+    },
81
+  },
64 82
 
65
-      type: ShapeType.Arrow,
66
-      isGenerated: false,
67
-      name: 'Arrow',
68
-      parentId: 'page1',
69
-      childIndex: 0,
70
-      point,
71
-      rotation: 0,
72
-      isAspectRatioLocked: false,
73
-      isLocked: false,
74
-      isHidden: false,
75
-      bend: 0,
76
-      handles,
83
+  create(props) {
84
+    const shape = {
85
+      ...this.defaultProps,
86
+      ...props,
77 87
       decorations: {
78
-        start: null,
79
-        middle: null,
80
-        end: Decoration.Arrow,
88
+        ...this.defaultProps.decorations,
89
+        ...props.decorations,
81 90
       },
82
-      ...props,
83 91
       style: {
84
-        ...defaultStyle,
92
+        ...this.defaultProps.style,
85 93
         ...props.style,
86 94
         isFilled: false,
87 95
       },
88 96
     }
97
+
98
+    // shape.handles.bend.point = getBendPoint(shape)
99
+
100
+    return shape
89 101
   },
90 102
 
91 103
   shouldRender(shape, prev) {

+ 14
- 22
state/shape-utils/dot.tsx Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { uniqueId } from 'utils'
1
+import { uniqueId } from 'utils/utils'
2 2
 import { DotShape, ShapeType } from 'types'
3 3
 import { intersectCircleBounds } from 'utils/intersections'
4 4
 import { boundsContained, translateBounds } from 'utils'
@@ -8,27 +8,19 @@ import { registerShapeUtils } from './register'
8 8
 const dot = registerShapeUtils<DotShape>({
9 9
   boundsCache: new WeakMap([]),
10 10
 
11
-  create(props) {
12
-    return {
13
-      id: uniqueId(),
14
-
15
-      type: ShapeType.Dot,
16
-      isGenerated: false,
17
-      name: 'Dot',
18
-      parentId: 'page1',
19
-      childIndex: 0,
20
-      point: [0, 0],
21
-      rotation: 0,
22
-      isAspectRatioLocked: false,
23
-      isLocked: false,
24
-      isHidden: false,
25
-      ...props,
26
-      style: {
27
-        ...defaultStyle,
28
-        ...props.style,
29
-        isFilled: false,
30
-      },
31
-    }
11
+  defaultProps: {
12
+    id: uniqueId(),
13
+    type: ShapeType.Dot,
14
+    isGenerated: false,
15
+    name: 'Dot',
16
+    parentId: 'page1',
17
+    childIndex: 0,
18
+    point: [0, 0],
19
+    rotation: 0,
20
+    isAspectRatioLocked: false,
21
+    isLocked: false,
22
+    isHidden: false,
23
+    style: defaultStyle,
32 24
   },
33 25
 
34 26
   render({ id }) {

+ 15
- 22
state/shape-utils/draw.tsx Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { uniqueId } from 'utils'
1
+import { uniqueId } from 'utils/utils'
2 2
 import vec from 'utils/vec'
3 3
 import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
4 4
 import { intersectPolylineBounds } from 'utils/intersections'
@@ -22,27 +22,20 @@ const draw = registerShapeUtils<DrawShape>({
22 22
 
23 23
   canStyleFill: true,
24 24
 
25
-  create(props) {
26
-    return {
27
-      id: uniqueId(),
28
-
29
-      type: ShapeType.Draw,
30
-      isGenerated: false,
31
-      name: 'Draw',
32
-      parentId: 'page1',
33
-      childIndex: 0,
34
-      point: [0, 0],
35
-      points: [],
36
-      rotation: 0,
37
-      isAspectRatioLocked: false,
38
-      isLocked: false,
39
-      isHidden: false,
40
-      ...props,
41
-      style: {
42
-        ...defaultStyle,
43
-        ...props.style,
44
-      },
45
-    }
25
+  defaultProps: {
26
+    id: uniqueId(),
27
+    type: ShapeType.Draw,
28
+    isGenerated: false,
29
+    name: 'Draw',
30
+    parentId: 'page1',
31
+    childIndex: 0,
32
+    point: [0, 0],
33
+    points: [],
34
+    rotation: 0,
35
+    isAspectRatioLocked: false,
36
+    isLocked: false,
37
+    isHidden: false,
38
+    style: defaultStyle,
46 39
   },
47 40
 
48 41
   shouldRender(shape, prev) {

+ 16
- 20
state/shape-utils/ellipse.tsx Datei anzeigen

@@ -1,4 +1,3 @@
1
-import { getPerfectDashProps } from 'utils/dashes'
2 1
 import vec from 'utils/vec'
3 2
 import { DashStyle, EllipseShape, ShapeType } from 'types'
4 3
 import { getShapeUtils } from './index'
@@ -11,6 +10,7 @@ import {
11 10
   pointInEllipse,
12 11
   boundsContained,
13 12
   getRotatedEllipseBounds,
13
+  getPerfectDashProps,
14 14
 } from 'utils'
15 15
 import { defaultStyle, getShapeStyle } from 'state/shape-styles'
16 16
 import getStroke from 'perfect-freehand'
@@ -21,25 +21,21 @@ const pathCache = new WeakMap<EllipseShape, string>([])
21 21
 const ellipse = registerShapeUtils<EllipseShape>({
22 22
   boundsCache: new WeakMap([]),
23 23
 
24
-  create(props) {
25
-    return {
26
-      id: uniqueId(),
27
-
28
-      type: ShapeType.Ellipse,
29
-      isGenerated: false,
30
-      name: 'Ellipse',
31
-      parentId: 'page1',
32
-      childIndex: 0,
33
-      point: [0, 0],
34
-      radiusX: 1,
35
-      radiusY: 1,
36
-      rotation: 0,
37
-      isAspectRatioLocked: false,
38
-      isLocked: false,
39
-      isHidden: false,
40
-      style: defaultStyle,
41
-      ...props,
42
-    }
24
+  defaultProps: {
25
+    id: uniqueId(),
26
+    type: ShapeType.Ellipse,
27
+    isGenerated: false,
28
+    name: 'Ellipse',
29
+    parentId: 'page1',
30
+    childIndex: 0,
31
+    point: [0, 0],
32
+    radiusX: 1,
33
+    radiusY: 1,
34
+    rotation: 0,
35
+    isAspectRatioLocked: false,
36
+    isLocked: false,
37
+    isHidden: false,
38
+    style: defaultStyle,
43 39
   },
44 40
 
45 41
   shouldRender(shape, prev) {

+ 16
- 21
state/shape-utils/group.tsx Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { uniqueId } from 'utils'
1
+import { uniqueId } from 'utils/utils'
2 2
 import vec from 'utils/vec'
3 3
 import { GroupShape, ShapeType } from 'types'
4 4
 import { getShapeUtils } from './index'
@@ -12,26 +12,21 @@ const group = registerShapeUtils<GroupShape>({
12 12
   isShy: true,
13 13
   isParent: true,
14 14
 
15
-  create(props) {
16
-    return {
17
-      id: uniqueId(),
18
-
19
-      type: ShapeType.Group,
20
-      isGenerated: false,
21
-      name: 'Group',
22
-      parentId: 'page1',
23
-      childIndex: 0,
24
-      point: [0, 0],
25
-      size: [1, 1],
26
-      radius: 2,
27
-      rotation: 0,
28
-      isAspectRatioLocked: false,
29
-      isLocked: false,
30
-      isHidden: false,
31
-      style: defaultStyle,
32
-      children: [],
33
-      ...props,
34
-    }
15
+  defaultProps: {
16
+    id: uniqueId(),
17
+    type: ShapeType.Group,
18
+    isGenerated: false,
19
+    name: 'Group',
20
+    parentId: 'page1',
21
+    childIndex: 0,
22
+    point: [0, 0],
23
+    size: [1, 1],
24
+    rotation: 0,
25
+    isAspectRatioLocked: false,
26
+    isLocked: false,
27
+    isHidden: false,
28
+    style: defaultStyle,
29
+    children: [],
35 30
   },
36 31
 
37 32
   render(shape) {

+ 15
- 23
state/shape-utils/line.tsx Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { uniqueId } from 'utils'
1
+import { uniqueId } from 'utils/utils'
2 2
 import vec from 'utils/vec'
3 3
 import { LineShape, ShapeType } from 'types'
4 4
 import { intersectCircleBounds } from 'utils/intersections'
@@ -10,28 +10,20 @@ import { registerShapeUtils } from './register'
10 10
 const line = registerShapeUtils<LineShape>({
11 11
   boundsCache: new WeakMap([]),
12 12
 
13
-  create(props) {
14
-    return {
15
-      id: uniqueId(),
16
-
17
-      type: ShapeType.Line,
18
-      isGenerated: false,
19
-      name: 'Line',
20
-      parentId: 'page1',
21
-      childIndex: 0,
22
-      point: [0, 0],
23
-      direction: [0, 0],
24
-      rotation: 0,
25
-      isAspectRatioLocked: false,
26
-      isLocked: false,
27
-      isHidden: false,
28
-      ...props,
29
-      style: {
30
-        ...defaultStyle,
31
-        ...props.style,
32
-        isFilled: false,
33
-      },
34
-    }
13
+  defaultProps: {
14
+    id: uniqueId(),
15
+    type: ShapeType.Line,
16
+    isGenerated: false,
17
+    name: 'Line',
18
+    parentId: 'page1',
19
+    childIndex: 0,
20
+    point: [0, 0],
21
+    direction: [0, 0],
22
+    rotation: 0,
23
+    isAspectRatioLocked: false,
24
+    isLocked: false,
25
+    isHidden: false,
26
+    style: defaultStyle,
35 27
   },
36 28
 
37 29
   shouldRender(shape, prev) {

+ 15
- 19
state/shape-utils/polyline.tsx Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { uniqueId } from 'utils'
1
+import { uniqueId } from 'utils/utils'
2 2
 import vec from 'utils/vec'
3 3
 import { PolylineShape, ShapeType } from 'types'
4 4
 import { intersectPolylineBounds } from 'utils/intersections'
@@ -13,24 +13,20 @@ import { registerShapeUtils } from './register'
13 13
 const polyline = registerShapeUtils<PolylineShape>({
14 14
   boundsCache: new WeakMap([]),
15 15
 
16
-  create(props) {
17
-    return {
18
-      id: uniqueId(),
19
-
20
-      type: ShapeType.Polyline,
21
-      isGenerated: false,
22
-      name: 'Polyline',
23
-      parentId: 'page1',
24
-      childIndex: 0,
25
-      point: [0, 0],
26
-      points: [[0, 0]],
27
-      rotation: 0,
28
-      isAspectRatioLocked: false,
29
-      isLocked: false,
30
-      isHidden: false,
31
-      style: defaultStyle,
32
-      ...props,
33
-    }
16
+  defaultProps: {
17
+    id: uniqueId(),
18
+    type: ShapeType.Polyline,
19
+    isGenerated: false,
20
+    name: 'Polyline',
21
+    parentId: 'page1',
22
+    childIndex: 0,
23
+    point: [0, 0],
24
+    points: [[0, 0]],
25
+    rotation: 0,
26
+    isAspectRatioLocked: false,
27
+    isLocked: false,
28
+    isHidden: false,
29
+    style: defaultStyle,
34 30
   },
35 31
 
36 32
   shouldRender(shape, prev) {

+ 15
- 23
state/shape-utils/ray.tsx Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { uniqueId } from 'utils'
1
+import { uniqueId } from 'utils/utils'
2 2
 import vec from 'utils/vec'
3 3
 import { RayShape, ShapeType } from 'types'
4 4
 import { intersectCircleBounds } from 'utils/intersections'
@@ -10,28 +10,20 @@ import { registerShapeUtils } from './register'
10 10
 const ray = registerShapeUtils<RayShape>({
11 11
   boundsCache: new WeakMap([]),
12 12
 
13
-  create(props) {
14
-    return {
15
-      id: uniqueId(),
16
-
17
-      type: ShapeType.Ray,
18
-      isGenerated: false,
19
-      name: 'Ray',
20
-      parentId: 'page1',
21
-      childIndex: 0,
22
-      point: [0, 0],
23
-      direction: [0, 1],
24
-      rotation: 0,
25
-      isAspectRatioLocked: false,
26
-      isLocked: false,
27
-      isHidden: false,
28
-      ...props,
29
-      style: {
30
-        ...defaultStyle,
31
-        ...props.style,
32
-        isFilled: false,
33
-      },
34
-    }
13
+  defaultProps: {
14
+    id: uniqueId(),
15
+    type: ShapeType.Ray,
16
+    isGenerated: false,
17
+    name: 'Ray',
18
+    parentId: 'page1',
19
+    childIndex: 0,
20
+    point: [0, 0],
21
+    direction: [0, 1],
22
+    rotation: 0,
23
+    isAspectRatioLocked: false,
24
+    isLocked: false,
25
+    isHidden: false,
26
+    style: defaultStyle,
35 27
   },
36 28
 
37 29
   shouldRender(shape, prev) {

+ 17
- 21
state/shape-utils/rectangle.tsx Datei anzeigen

@@ -1,36 +1,32 @@
1
-import { uniqueId } from 'utils'
1
+import { uniqueId, getPerfectDashProps } from 'utils/utils'
2 2
 import vec from 'utils/vec'
3 3
 import { DashStyle, RectangleShape, ShapeType } from 'types'
4 4
 import { getSvgPathFromStroke, translateBounds, rng, shuffleArr } from 'utils'
5 5
 import { defaultStyle, getShapeStyle } from 'state/shape-styles'
6 6
 import getStroke from 'perfect-freehand'
7 7
 import { registerShapeUtils } from './register'
8
-import { getPerfectDashProps } from 'utils/dashes'
9 8
 
10 9
 const pathCache = new WeakMap<number[], string>([])
11 10
 
12 11
 const rectangle = registerShapeUtils<RectangleShape>({
13 12
   boundsCache: new WeakMap([]),
14 13
 
15
-  create(props) {
16
-    return {
17
-      id: uniqueId(),
18
-
19
-      type: ShapeType.Rectangle,
20
-      isGenerated: false,
21
-      name: 'Rectangle',
22
-      parentId: 'page1',
23
-      childIndex: 0,
24
-      point: [0, 0],
25
-      size: [1, 1],
26
-      radius: 2,
27
-      rotation: 0,
28
-      isAspectRatioLocked: false,
29
-      isLocked: false,
30
-      isHidden: false,
31
-      style: defaultStyle,
32
-      ...props,
33
-    }
14
+  defaultProps: {
15
+    id: uniqueId(),
16
+
17
+    type: ShapeType.Rectangle,
18
+    isGenerated: false,
19
+    name: 'Rectangle',
20
+    parentId: 'page1',
21
+    childIndex: 0,
22
+    point: [0, 0],
23
+    size: [1, 1],
24
+    radius: 2,
25
+    rotation: 0,
26
+    isAspectRatioLocked: false,
27
+    isLocked: false,
28
+    isHidden: false,
29
+    style: defaultStyle,
34 30
   },
35 31
 
36 32
   shouldRender(shape, prev) {

+ 13
- 14
state/shape-utils/register.tsx Datei anzeigen

@@ -1,6 +1,6 @@
1
-import { Shape, ShapeUtility } from 'types'
2
-import vec from 'utils/vec'
1
+import React from 'react'
3 2
 import {
3
+  vec,
4 4
   pointInBounds,
5 5
   getBoundsCenter,
6 6
   getBoundsFromPoints,
@@ -8,8 +8,7 @@ import {
8 8
   boundsCollidePolygon,
9 9
   boundsContainPolygon,
10 10
 } from 'utils'
11
-import { uniqueId } from 'utils'
12
-import React from 'react'
11
+import { Shape, ShapeUtility } from 'types'
13 12
 
14 13
 function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
15 14
   return {
@@ -22,19 +21,19 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
22 21
     isParent: false,
23 22
     isForeignObject: false,
24 23
 
24
+    defaultProps: {} as T,
25
+
25 26
     create(props) {
26 27
       return {
27
-        id: uniqueId(),
28
-        isGenerated: false,
29
-        point: [0, 0],
30
-        name: 'Shape',
31
-        parentId: 'page1',
32
-        childIndex: 0,
33
-        rotation: 0,
34
-        isAspectRatioLocked: false,
35
-        isLocked: false,
36
-        isHidden: false,
28
+        ...this.defaultProps,
37 29
         ...props,
30
+        style: {
31
+          ...this.defaultProps.style,
32
+          ...props.style,
33
+          isFilled: this.canStyleFill
34
+            ? props.style?.isFilled || this.defaultProps.style.isFilled
35
+            : false,
36
+        },
38 37
       } as T
39 38
     },
40 39
 

+ 16
- 20
state/shape-utils/text.tsx Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { uniqueId, isMobile } from 'utils'
1
+import { uniqueId, isMobile } from 'utils/utils'
2 2
 import vec from 'utils/vec'
3 3
 import { TextShape, ShapeType } from 'types'
4 4
 import {
@@ -47,27 +47,23 @@ const text = registerShapeUtils<TextShape>({
47 47
   isForeignObject: true,
48 48
   canChangeAspectRatio: false,
49 49
   canEdit: true,
50
-
51 50
   boundsCache: new WeakMap([]),
52 51
 
53
-  create(props) {
54
-    return {
55
-      id: uniqueId(),
56
-      type: ShapeType.Text,
57
-      isGenerated: false,
58
-      name: 'Text',
59
-      parentId: 'page1',
60
-      childIndex: 0,
61
-      point: [0, 0],
62
-      rotation: 0,
63
-      isAspectRatioLocked: false,
64
-      isLocked: false,
65
-      isHidden: false,
66
-      style: defaultStyle,
67
-      text: '',
68
-      scale: 1,
69
-      ...props,
70
-    }
52
+  defaultProps: {
53
+    id: uniqueId(),
54
+    type: ShapeType.Text,
55
+    isGenerated: false,
56
+    name: 'Text',
57
+    parentId: 'page1',
58
+    childIndex: 0,
59
+    point: [0, 0],
60
+    rotation: 0,
61
+    isAspectRatioLocked: false,
62
+    isLocked: false,
63
+    isHidden: false,
64
+    style: defaultStyle,
65
+    text: '',
66
+    scale: 1,
71 67
   },
72 68
 
73 69
   shouldRender(shape, prev) {

+ 50
- 11
state/state.ts Datei anzeigen

@@ -1,15 +1,16 @@
1 1
 import { createSelectorHook, createState } from '@state-designer/react'
2 2
 import { updateFromCode } from './code/generate'
3 3
 import { createShape, getShapeUtils } from './shape-utils'
4
-import vec from 'utils/vec'
4
+import * as Sessions from './sessions'
5 5
 import inputs from './inputs'
6 6
 import history from './history'
7 7
 import storage from './storage'
8
+import session from './session'
8 9
 import clipboard from './clipboard'
9
-import * as Sessions from './sessions'
10 10
 import coopClient from './coop/client-liveblocks'
11 11
 import commands from './commands'
12 12
 import {
13
+  vec,
13 14
   getCommonBounds,
14 15
   rotateBounds,
15 16
   getBoundsCenter,
@@ -18,7 +19,7 @@ import {
18 19
   pointInBounds,
19 20
   uniqueId,
20 21
 } from 'utils'
21
-import tld from 'utils/tld'
22
+import tld from '../utils/tld'
22 23
 import {
23 24
   Data,
24 25
   PointerInfo,
@@ -36,7 +37,6 @@ import {
36 37
   SizeStyle,
37 38
   ColorStyle,
38 39
 } from 'types'
39
-import session from './session'
40 40
 
41 41
 const initialData: Data = {
42 42
   isReadOnly: false,
@@ -288,10 +288,15 @@ const state = createState({
288 288
           unless: 'isInSession',
289 289
           do: ['loadDocumentFromJson', 'resetHistory'],
290 290
         },
291
-        SELECTED_ALL: {
291
+        DESELECTED_ALL: {
292 292
           unless: 'isInSession',
293
+          do: 'deselectAll',
293 294
           to: 'selecting',
295
+        },
296
+        SELECTED_ALL: {
297
+          unless: 'isInSession',
294 298
           do: 'selectAll',
299
+          to: 'selecting',
295 300
         },
296 301
         CHANGED_PAGE: {
297 302
           unless: 'isInSession',
@@ -398,8 +403,15 @@ const state = createState({
398 403
             notPointing: {
399 404
               onEnter: 'clearPointedId',
400 405
               on: {
401
-                CANCELLED: 'clearSelectedIds',
402
-                POINTED_CANVAS: { to: 'brushSelecting' },
406
+                CANCELLED: {
407
+                  if: 'hasCurrentParentShape',
408
+                  do: ['selectCurrentParentId', 'raiseCurrentParentId'],
409
+                  else: 'clearSelectedIds',
410
+                },
411
+                POINTED_CANVAS: {
412
+                  to: 'brushSelecting',
413
+                  do: 'setCurrentParentIdToPage',
414
+                },
403 415
                 POINTED_BOUNDS: [
404 416
                   {
405 417
                     if: 'isPressingMetaKey',
@@ -477,7 +489,7 @@ const state = createState({
477 489
                   {
478 490
                     unless: 'isPressingShiftKey',
479 491
                     do: [
480
-                      'setDrilledPointedId',
492
+                      'setCurrentParentId',
481 493
                       'clearSelectedIds',
482 494
                       'pushPointedIdToSelectedIds',
483 495
                     ],
@@ -1120,6 +1132,9 @@ const state = createState({
1120 1132
     hasMultipleSelection(data) {
1121 1133
       return tld.getSelectedIds(data).size > 1
1122 1134
     },
1135
+    hasCurrentParentShape(data) {
1136
+      return data.currentParentId !== data.currentPageId
1137
+    },
1123 1138
     isToolLocked(data) {
1124 1139
       return data.settings.isToolLocked
1125 1140
     },
@@ -1180,6 +1195,14 @@ const state = createState({
1180 1195
 
1181 1196
       data.currentPageId = newId
1182 1197
 
1198
+      data.pointedId = null
1199
+      data.hoveredId = null
1200
+      data.editingId = null
1201
+      data.currentPageId = 'page1'
1202
+      data.currentParentId = 'page1'
1203
+      data.currentCodeFileId = 'file0'
1204
+      data.codeControls = {}
1205
+
1183 1206
       data.document.pages = {
1184 1207
         [newId]: {
1185 1208
           id: newId,
@@ -1234,6 +1257,7 @@ const state = createState({
1234 1257
 
1235 1258
     createShape(data, payload, type: ShapeType) {
1236 1259
       const shape = createShape(type, {
1260
+        id: uniqueId(),
1237 1261
         parentId: data.currentPageId,
1238 1262
         point: vec.round(tld.screenToWorld(payload.point, data)),
1239 1263
         style: deepClone(data.currentStyle),
@@ -1500,11 +1524,12 @@ const state = createState({
1500 1524
         )
1501 1525
       )
1502 1526
     },
1503
-
1504 1527
     clearInputs() {
1505 1528
       inputs.clear()
1506 1529
     },
1507
-
1530
+    deselectAll(data) {
1531
+      tld.getSelectedIds(data).clear()
1532
+    },
1508 1533
     selectAll(data) {
1509 1534
       const selectedIds = tld.getSelectedIds(data)
1510 1535
       const page = tld.getPage(data)
@@ -1525,10 +1550,24 @@ const state = createState({
1525 1550
       data.pointedId = getPointedId(data, payload.target)
1526 1551
       data.currentParentId = getParentId(data, data.pointedId)
1527 1552
     },
1528
-    setDrilledPointedId(data, payload: PointerInfo) {
1553
+    setCurrentParentId(data, payload: PointerInfo) {
1529 1554
       data.pointedId = getDrilledPointedId(data, payload.target)
1530 1555
       data.currentParentId = getParentId(data, data.pointedId)
1531 1556
     },
1557
+    raiseCurrentParentId(data) {
1558
+      const currentParent = tld.getShape(data, data.currentParentId)
1559
+
1560
+      data.currentParentId =
1561
+        currentParent.parentId === data.currentPageId
1562
+          ? data.currentPageId
1563
+          : currentParent.parentId
1564
+    },
1565
+    setCurrentParentIdToPage(data) {
1566
+      data.currentParentId = data.currentPageId
1567
+    },
1568
+    selectCurrentParentId(data) {
1569
+      tld.setSelectedIds(data, [data.currentParentId])
1570
+    },
1532 1571
     clearCurrentParentId(data) {
1533 1572
       data.currentParentId = data.currentPageId
1534 1573
       data.pointedId = undefined

+ 1
- 1
state/storage.ts Datei anzeigen

@@ -1,7 +1,7 @@
1 1
 import { Data, PageState, TLDocument } from 'types'
2 2
 import { decompress, compress, setToArray } from 'utils'
3 3
 import state from './state'
4
-import { uniqueId } from 'utils'
4
+import { uniqueId } from 'utils/utils'
5 5
 import * as idb from 'idb-keyval'
6 6
 
7 7
 const CURRENT_VERSION = 'code_slate_0.0.8'

+ 4
- 1
types.ts Datei anzeigen

@@ -458,6 +458,9 @@ export type PropsOfType<T extends Record<string, unknown>> = {
458 458
 export type Mutable<T extends Shape> = { -readonly [K in keyof T]: T[K] }
459 459
 
460 460
 export interface ShapeUtility<K extends Shape> {
461
+  // Default properties when creating a new shape
462
+  defaultProps: K
463
+
461 464
   // A cache for the computed bounds of this kind of shape.
462 465
   boundsCache: WeakMap<K, Bounds>
463 466
 
@@ -483,7 +486,7 @@ export interface ShapeUtility<K extends Shape> {
483 486
   isShy: boolean
484 487
 
485 488
   // Create a new shape.
486
-  create(props: Partial<K>): K
489
+  create(this: ShapeUtility<K>, props: Partial<K>): K
487 490
 
488 491
   // Update a shape's styles
489 492
   applyStyles(

+ 0
- 41
utils/dashes.ts Datei anzeigen

@@ -1,41 +0,0 @@
1
-/**
2
- * Get balanced dash-strokearray and dash-strokeoffset properties for a path of a given length.
3
- * @param length The length of the path.
4
- * @param strokeWidth The shape's stroke-width property.
5
- * @param style The stroke's style: "dashed" or "dotted" (default "dashed").
6
- * @param snap An interval for dashes (e.g. 4 will produce arrays with 4, 8, 16, etc dashes).
7
- */
8
-export function getPerfectDashProps(
9
-  length: number,
10
-  strokeWidth: number,
11
-  style: 'dashed' | 'dotted' = 'dashed',
12
-  snap = 1
13
-): {
14
-  strokeDasharray: string
15
-  strokeDashoffset: string
16
-} {
17
-  let dashLength: number
18
-  let strokeDashoffset: string
19
-  let ratio: number
20
-
21
-  if (style === 'dashed') {
22
-    dashLength = strokeWidth * 2
23
-    ratio = 1
24
-    strokeDashoffset = (dashLength / 2).toString()
25
-  } else {
26
-    dashLength = strokeWidth / 100
27
-    ratio = 100
28
-    strokeDashoffset = '0'
29
-  }
30
-
31
-  let dashes = Math.floor(length / dashLength / (2 * ratio))
32
-  dashes -= dashes % snap
33
-  if (dashes === 0) dashes = 1
34
-
35
-  const gapLength = (length - dashes * dashLength) / dashes
36
-
37
-  return {
38
-    strokeDasharray: [dashLength, gapLength].join(' '),
39
-    strokeDashoffset,
40
-  }
41
-}

+ 4
- 0
utils/index.ts Datei anzeigen

@@ -1 +1,5 @@
1
+import vec from './vec'
2
+import svg from './svg'
1 3
 export * from './utils'
4
+
5
+export { vec, svg }

+ 2
- 2
utils/tld.ts Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { clamp, deepClone, getCommonBounds, setToArray } from './utils'
1
+import { clamp, deepClone, getCommonBounds, setToArray } from 'utils'
2 2
 import { getShapeUtils } from 'state/shape-utils'
3 3
 import vec from './vec'
4 4
 import {
@@ -15,7 +15,7 @@ import {
15 15
 } from 'types'
16 16
 import { AssertionError } from 'assert'
17 17
 
18
-export default class ProjectUtils {
18
+export default class StateUtils {
19 19
   static getCameraZoom(zoom: number): number {
20 20
     return clamp(zoom, 0.1, 5)
21 21
   }

+ 1191
- 1143
utils/utils.ts
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


Laden…
Abbrechen
Speichern