Переглянути джерело

Adds cloning, fixes some undo bugs

main
Steve Ruiz 4 роки тому
джерело
коміт
e8b13103ac

+ 8
- 0
hooks/useKeyboardEvents.ts Переглянути файл

@@ -15,6 +15,10 @@ export default function useKeyboardEvents() {
15 15
         }
16 16
       }
17 17
 
18
+      if (e.altKey) {
19
+        state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e))
20
+      }
21
+
18 22
       if (e.key === "Backspace" && !(metaKey(e) || e.shiftKey || e.altKey)) {
19 23
         state.send("DELETED", getKeyboardEventInfo(e))
20 24
       }
@@ -66,6 +70,10 @@ export default function useKeyboardEvents() {
66 70
         state.send("CANCELLED")
67 71
       }
68 72
 
73
+      if (e.altKey) {
74
+        state.send("RELEASED_ALT_KEY")
75
+      }
76
+
69 77
       state.send("RELEASED_KEY", getKeyboardEventInfo(e))
70 78
     }
71 79
 

+ 34
- 17
state/commands/transform-single.ts Переглянути файл

@@ -2,6 +2,7 @@ import Command from "./command"
2 2
 import history from "../history"
3 3
 import { Data, TransformCorner, TransformEdge } from "types"
4 4
 import { getShapeUtils } from "lib/shapes"
5
+import { current } from "immer"
5 6
 import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
6 7
 
7 8
 export default function transformSingleCommand(
@@ -9,38 +10,54 @@ export default function transformSingleCommand(
9 10
   before: TransformSingleSnapshot,
10 11
   after: TransformSingleSnapshot,
11 12
   scaleX: number,
12
-  scaleY: number
13
+  scaleY: number,
14
+  isCreating: boolean
13 15
 ) {
16
+  const shape =
17
+    current(data).document.pages[after.currentPageId].shapes[after.id]
18
+
14 19
   history.execute(
15 20
     data,
16 21
     new Command({
17 22
       name: "transform_single_shape",
18 23
       category: "canvas",
24
+      manualSelection: true,
19 25
       do(data) {
20 26
         const { id, currentPageId, type, initialShape, initialShapeBounds } =
21 27
           after
22 28
 
23
-        const shape = data.document.pages[currentPageId].shapes[id]
29
+        data.selectedIds.clear()
30
+        data.selectedIds.add(id)
24 31
 
25
-        getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
26
-          type,
27
-          initialShape,
28
-          scaleX,
29
-          scaleY,
30
-        })
32
+        if (isCreating) {
33
+          data.document.pages[currentPageId].shapes[id] = shape
34
+        } else {
35
+          getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
36
+            type,
37
+            initialShape,
38
+            scaleX,
39
+            scaleY,
40
+          })
41
+        }
31 42
       },
32 43
       undo(data) {
33
-        const { id, currentPageId, type, initialShape, initialShapeBounds } =
34
-          before
44
+        const { id, currentPageId, type, initialShapeBounds } = before
45
+
46
+        data.selectedIds.clear()
35 47
 
36
-        const shape = data.document.pages[currentPageId].shapes[id]
48
+        if (isCreating) {
49
+          delete data.document.pages[currentPageId].shapes[id]
50
+        } else {
51
+          const shape = data.document.pages[currentPageId].shapes[id]
52
+          data.selectedIds.add(id)
37 53
 
38
-        getShapeUtils(shape).transform(shape, initialShapeBounds, {
39
-          type,
40
-          initialShape: after.initialShape,
41
-          scaleX: 1,
42
-          scaleY: 1,
43
-        })
54
+          getShapeUtils(shape).transform(shape, initialShapeBounds, {
55
+            type,
56
+            initialShape: after.initialShape,
57
+            scaleX: 1,
58
+            scaleY: 1,
59
+          })
60
+        }
44 61
       },
45 62
     })
46 63
   )

+ 28
- 7
state/commands/translate.ts Переглянути файл

@@ -6,25 +6,46 @@ import { Data } from "types"
6 6
 export default function translateCommand(
7 7
   data: Data,
8 8
   before: TranslateSnapshot,
9
-  after: TranslateSnapshot
9
+  after: TranslateSnapshot,
10
+  isCloning: boolean
10 11
 ) {
11 12
   history.execute(
12 13
     data,
13 14
     new Command({
14
-      name: "translate_shapes",
15
+      name: isCloning ? "clone_shapes" : "translate_shapes",
15 16
       category: "canvas",
16
-      do(data) {
17
+      manualSelection: true,
18
+      do(data, initial) {
19
+        if (initial) return
20
+
17 21
         const { shapes } = data.document.pages[after.currentPageId]
22
+        const { initialShapes, clones } = after
23
+
24
+        data.selectedIds.clear()
18 25
 
19
-        for (let { id, point } of after.shapes) {
20
-          shapes[id].point = point
26
+        for (let id in initialShapes) {
27
+          if (isCloning) {
28
+            shapes[id] = initialShapes[id]
29
+          } else {
30
+            shapes[id].point = initialShapes[id].point
31
+          }
32
+          data.selectedIds.add(id)
21 33
         }
22 34
       },
23 35
       undo(data) {
24 36
         const { shapes } = data.document.pages[before.currentPageId]
37
+        const { initialShapes, clones } = before
38
+
39
+        data.selectedIds.clear()
40
+
41
+        for (let id in initialShapes) {
42
+          shapes[id].point = initialShapes[id].point
43
+          data.selectedIds.add(id)
25 44
 
26
-        for (let { id, point } of before.shapes) {
27
-          shapes[id].point = point
45
+          if (isCloning) {
46
+            const clone = clones[id]
47
+            delete shapes[clone.id]
48
+          }
28 49
         }
29 50
       },
30 51
     })

+ 7
- 0
state/history.ts Переглянути файл

@@ -77,6 +77,13 @@ class BaseHistory<T> {
77 77
     return { ...data }
78 78
   }
79 79
 
80
+  pop() {
81
+    if (this.stack.length > 0) {
82
+      this.stack.pop()
83
+      this.pointer--
84
+    }
85
+  }
86
+
80 87
   get disabled() {
81 88
     return !this._enabled
82 89
   }

+ 4
- 0
state/inputs.tsx Переглянути файл

@@ -86,6 +86,10 @@ class Inputs {
86 86
     const { shiftKey, ctrlKey, metaKey, altKey } = e
87 87
     return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
88 88
   }
89
+
90
+  get pointer() {
91
+    return this.points[Object.keys(this.points)[0]]
92
+  }
89 93
 }
90 94
 
91 95
 export default new Inputs()

+ 8
- 5
state/sessions/transform-single-session.ts Переглянути файл

@@ -12,22 +12,24 @@ import {
12 12
 } from "utils/utils"
13 13
 
14 14
 export default class TransformSingleSession extends BaseSession {
15
-  delta = [0, 0]
15
+  transformType: TransformEdge | TransformCorner
16
+  origin: number[]
16 17
   scaleX = 1
17 18
   scaleY = 1
18
-  transformType: TransformEdge | TransformCorner
19 19
   snapshot: TransformSingleSnapshot
20
-  origin: number[]
20
+  isCreating: boolean
21 21
 
22 22
   constructor(
23 23
     data: Data,
24 24
     transformType: TransformCorner | TransformEdge,
25
-    point: number[]
25
+    point: number[],
26
+    isCreating = false
26 27
   ) {
27 28
     super(data)
28 29
     this.origin = point
29 30
     this.transformType = transformType
30 31
     this.snapshot = getTransformSingleSnapshot(data, transformType)
32
+    this.isCreating = isCreating
31 33
   }
32 34
 
33 35
   update(data: Data, point: number[]) {
@@ -78,7 +80,8 @@ export default class TransformSingleSession extends BaseSession {
78 80
       this.snapshot,
79 81
       getTransformSingleSnapshot(data, this.transformType),
80 82
       this.scaleX,
81
-      this.scaleY
83
+      this.scaleY,
84
+      this.isCreating
82 85
     )
83 86
   }
84 87
 }

+ 54
- 15
state/sessions/translate-session.ts Переглянути файл

@@ -3,11 +3,13 @@ import * as vec from "utils/vec"
3 3
 import BaseSession from "./base-session"
4 4
 import commands from "state/commands"
5 5
 import { current } from "immer"
6
+import { v4 as uuid } from "uuid"
6 7
 
7 8
 export default class TranslateSession extends BaseSession {
8 9
   delta = [0, 0]
9 10
   origin: number[]
10 11
   snapshot: TranslateSnapshot
12
+  isCloning = false
11 13
 
12 14
   constructor(data: Data, point: number[]) {
13 15
     super(data)
@@ -15,31 +17,62 @@ export default class TranslateSession extends BaseSession {
15 17
     this.snapshot = getTranslateSnapshot(data)
16 18
   }
17 19
 
18
-  update(data: Data, point: number[]) {
19
-    const { currentPageId, shapes } = this.snapshot
20
+  update(data: Data, point: number[], isCloning: boolean) {
21
+    const { currentPageId, clones, initialShapes } = this.snapshot
20 22
     const { document } = data
23
+    const { shapes } = document.pages[this.snapshot.currentPageId]
21 24
 
22 25
     const delta = vec.vec(this.origin, point)
23 26
 
24
-    for (let shape of shapes) {
25
-      document.pages[currentPageId].shapes[shape.id].point = vec.add(
26
-        shape.point,
27
-        delta
28
-      )
27
+    if (isCloning && !this.isCloning) {
28
+      // Enter cloning state, create clones at shape points and move shapes
29
+      // back to initial point.
30
+      this.isCloning = true
31
+      data.selectedIds.clear()
32
+      for (let id in clones) {
33
+        const clone = clones[id]
34
+        data.selectedIds.add(clone.id)
35
+        shapes[id].point = initialShapes[id].point
36
+        data.document.pages[currentPageId].shapes[clone.id] = clone
37
+      }
38
+    } else if (!isCloning && this.isCloning) {
39
+      // Exit cloning state, delete up clones and move shapes to clone points
40
+      this.isCloning = false
41
+      data.selectedIds.clear()
42
+      for (let id in clones) {
43
+        const clone = clones[id]
44
+        data.selectedIds.add(id)
45
+        delete data.document.pages[currentPageId].shapes[clone.id]
46
+      }
47
+    }
48
+
49
+    // Calculate the new points and update data
50
+    for (let id in initialShapes) {
51
+      const point = vec.add(initialShapes[id].point, delta)
52
+      const targetId = this.isCloning ? clones[id].id : id
53
+      document.pages[currentPageId].shapes[targetId].point = point
29 54
     }
30 55
   }
31 56
 
32 57
   cancel(data: Data) {
33 58
     const { document } = data
59
+    const { initialShapes, clones, currentPageId } = this.snapshot
34 60
 
35
-    for (let shape of this.snapshot.shapes) {
36
-      document.pages[this.snapshot.currentPageId].shapes[shape.id].point =
37
-        shape.point
61
+    const { shapes } = document.pages[currentPageId]
62
+
63
+    for (let id in initialShapes) {
64
+      shapes[id].point = initialShapes[id].point
65
+      delete shapes[clones[id].id]
38 66
     }
39 67
   }
40 68
 
41 69
   complete(data: Data) {
42
-    commands.translate(data, this.snapshot, getTranslateSnapshot(data))
70
+    commands.translate(
71
+      data,
72
+      this.snapshot,
73
+      getTranslateSnapshot(data),
74
+      this.isCloning
75
+    )
43 76
   }
44 77
 }
45 78
 
@@ -49,13 +82,19 @@ export function getTranslateSnapshot(data: Data) {
49 82
     currentPageId,
50 83
   } = current(data)
51 84
 
52
-  const { shapes } = pages[currentPageId]
85
+  const shapes = Array.from(data.selectedIds.values()).map(
86
+    (id) => pages[currentPageId].shapes[id]
87
+  )
88
+
89
+  // Clones and shapes are keyed under the same id, though the clone itself
90
+  // has a different id.
53 91
 
54 92
   return {
55 93
     currentPageId,
56
-    shapes: Array.from(data.selectedIds.values())
57
-      .map((id) => shapes[id])
58
-      .map(({ id, point }) => ({ id, point })),
94
+    initialShapes: Object.fromEntries(shapes.map((shape) => [shape.id, shape])),
95
+    clones: Object.fromEntries(
96
+      shapes.map((shape) => [shape.id, { ...shape, id: uuid() }])
97
+    ),
59 98
   }
60 99
 }
61 100
 

+ 40
- 19
state/state.ts Переглянути файл

@@ -10,6 +10,7 @@ import {
10 10
   TransformEdge,
11 11
   CodeControl,
12 12
 } from "types"
13
+import inputs from "./inputs"
13 14
 import { defaultDocument } from "./data"
14 15
 import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
15 16
 import history from "state/history"
@@ -174,6 +175,8 @@ const state = createState({
174 175
               on: {
175 176
                 MOVED_POINTER: "updateTranslateSession",
176 177
                 PANNED_CAMERA: "updateTranslateSession",
178
+                PRESSED_ALT_KEY: "updateCloningTranslateSession",
179
+                RELEASED_ALT_KEY: "updateCloningTranslateSession",
177 180
                 STOPPED_POINTING: { do: "completeSession", to: "selecting" },
178 181
                 CANCELLED: { do: "cancelSession", to: "selecting" },
179 182
               },
@@ -261,6 +264,7 @@ const state = createState({
261 264
           states: {
262 265
             creating: {
263 266
               on: {
267
+                CANCELLED: { to: "selecting" },
264 268
                 POINTED_CANVAS: {
265 269
                   to: "ellipse.editing",
266 270
                 },
@@ -284,6 +288,7 @@ const state = createState({
284 288
           states: {
285 289
             creating: {
286 290
               on: {
291
+                CANCELLED: { to: "selecting" },
287 292
                 POINTED_CANVAS: {
288 293
                   to: "rectangle.editing",
289 294
                 },
@@ -307,6 +312,7 @@ const state = createState({
307 312
           states: {
308 313
             creating: {
309 314
               on: {
315
+                CANCELLED: { to: "selecting" },
310 316
                 POINTED_CANVAS: {
311 317
                   do: "createRay",
312 318
                   to: "ray.editing",
@@ -315,11 +321,8 @@ const state = createState({
315 321
             },
316 322
             editing: {
317 323
               on: {
318
-                STOPPED_POINTING: { do: "completeSession", to: "selecting" },
319
-                CANCELLED: {
320
-                  do: ["cancelSession", "deleteSelectedIds"],
321
-                  to: "selecting",
322
-                },
324
+                STOPPED_POINTING: { to: "selecting" },
325
+                CANCELLED: { to: "selecting" },
323 326
                 MOVED_POINTER: {
324 327
                   if: "distanceImpliesDrag",
325 328
                   to: "drawingShape.direction",
@@ -333,6 +336,7 @@ const state = createState({
333 336
           states: {
334 337
             creating: {
335 338
               on: {
339
+                CANCELLED: { to: "selecting" },
336 340
                 POINTED_CANVAS: {
337 341
                   do: "createLine",
338 342
                   to: "line.editing",
@@ -341,11 +345,8 @@ const state = createState({
341 345
             },
342 346
             editing: {
343 347
               on: {
344
-                STOPPED_POINTING: { do: "completeSession", to: "selecting" },
345
-                CANCELLED: {
346
-                  do: ["cancelSession", "deleteSelectedIds"],
347
-                  to: "selecting",
348
-                },
348
+                STOPPED_POINTING: { to: "selecting" },
349
+                CANCELLED: { to: "selecting" },
349 350
                 MOVED_POINTER: {
350 351
                   if: "distanceImpliesDrag",
351 352
                   to: "drawingShape.direction",
@@ -359,7 +360,10 @@ const state = createState({
359 360
     },
360 361
     drawingShape: {
361 362
       on: {
362
-        STOPPED_POINTING: { do: "completeSession", to: "selecting" },
363
+        STOPPED_POINTING: {
364
+          do: "completeSession",
365
+          to: "selecting",
366
+        },
363 367
         CANCELLED: {
364 368
           do: ["cancelSession", "deleteSelectedIds"],
365 369
           to: "selecting",
@@ -422,7 +426,8 @@ const state = createState({
422 426
         point: screenToWorld(payload.point, data),
423 427
       })
424 428
 
425
-      commands.createShape(data, shape)
429
+      data.document.pages[data.currentPageId].shapes[shape.id] = shape
430
+      data.selectedIds.clear()
426 431
       data.selectedIds.add(shape.id)
427 432
     },
428 433
 
@@ -432,7 +437,8 @@ const state = createState({
432 437
         point: screenToWorld(payload.point, data),
433 438
       })
434 439
 
435
-      commands.createShape(data, shape)
440
+      data.document.pages[data.currentPageId].shapes[shape.id] = shape
441
+      data.selectedIds.clear()
436 442
       data.selectedIds.add(shape.id)
437 443
     },
438 444
 
@@ -443,7 +449,8 @@ const state = createState({
443 449
         direction: [0, 1],
444 450
       })
445 451
 
446
-      commands.createShape(data, shape)
452
+      data.document.pages[data.currentPageId].shapes[shape.id] = shape
453
+      data.selectedIds.clear()
447 454
       data.selectedIds.add(shape.id)
448 455
     },
449 456
 
@@ -453,7 +460,8 @@ const state = createState({
453 460
         radius: 1,
454 461
       })
455 462
 
456
-      commands.createShape(data, shape)
463
+      data.document.pages[data.currentPageId].shapes[shape.id] = shape
464
+      data.selectedIds.clear()
457 465
       data.selectedIds.add(shape.id)
458 466
     },
459 467
 
@@ -464,7 +472,8 @@ const state = createState({
464 472
         radiusY: 1,
465 473
       })
466 474
 
467
-      commands.createShape(data, shape)
475
+      data.document.pages[data.currentPageId].shapes[shape.id] = shape
476
+      data.selectedIds.clear()
468 477
       data.selectedIds.add(shape.id)
469 478
     },
470 479
 
@@ -474,7 +483,8 @@ const state = createState({
474 483
         size: [1, 1],
475 484
       })
476 485
 
477
-      commands.createShape(data, shape)
486
+      data.document.pages[data.currentPageId].shapes[shape.id] = shape
487
+      data.selectedIds.clear()
478 488
       data.selectedIds.add(shape.id)
479 489
     },
480 490
     /* -------------------- Sessions -------------------- */
@@ -518,8 +528,15 @@ const state = createState({
518 528
         screenToWorld(payload.point, data)
519 529
       )
520 530
     },
531
+    updateCloningTranslateSession(data, payload: { altKey: boolean }) {
532
+      session.update(
533
+        data,
534
+        screenToWorld(inputs.pointer.point, data),
535
+        payload.altKey
536
+      )
537
+    },
521 538
     updateTranslateSession(data, payload: PointerInfo) {
522
-      session.update(data, screenToWorld(payload.point, data))
539
+      session.update(data, screenToWorld(payload.point, data), payload.altKey)
523 540
     },
524 541
 
525 542
     // Dragging / Translating
@@ -544,7 +561,8 @@ const state = createState({
544 561
       session = new Sessions.TransformSingleSession(
545 562
         data,
546 563
         TransformCorner.BottomRight,
547
-        screenToWorld(payload.point, data)
564
+        screenToWorld(payload.point, data),
565
+        true
548 566
       )
549 567
     },
550 568
     updateTransformSession(data, payload: PointerInfo) {
@@ -642,6 +660,9 @@ const state = createState({
642 660
     /* ---------------------- History ---------------------- */
643 661
 
644 662
     // History
663
+    popHistory() {
664
+      history.pop()
665
+    },
645 666
     forceSave(data) {
646 667
       history.save(data)
647 668
     },

Завантаження…
Відмінити
Зберегти