Ver código fonte

Adds copy and paste

main
Steve Ruiz 4 anos atrás
pai
commit
eccf5f6307
6 arquivos alterados com 181 adições e 18 exclusões
  1. 9
    18
      components/canvas/page.tsx
  2. 48
    0
      state/clipboard.ts
  3. 2
    0
      state/commands/index.ts
  4. 82
    0
      state/commands/paste.ts
  5. 23
    0
      state/state.ts
  6. 17
    0
      utils/utils.ts

+ 9
- 18
components/canvas/page.tsx Ver arquivo

@@ -2,7 +2,12 @@ import { getShapeUtils } from 'lib/shape-utils'
2 2
 import state, { useSelector } from 'state'
3 3
 import { Bounds, GroupShape, PageState } from 'types'
4 4
 import { boundsCollide, boundsContain } from 'utils/bounds'
5
-import { deepCompareArrays, getPage, screenToWorld } from 'utils/utils'
5
+import {
6
+  deepCompareArrays,
7
+  getPage,
8
+  getViewport,
9
+  screenToWorld,
10
+} from 'utils/utils'
6 11
 import Shape from './shape'
7 12
 
8 13
 /* 
@@ -21,26 +26,13 @@ export default function Page() {
21 26
     const pageState = s.data.pageStates[page.id]
22 27
 
23 28
     if (!viewportCache.has(pageState)) {
24
-      const [minX, minY] = screenToWorld([0, 0], s.data)
25
-      const [maxX, maxY] = screenToWorld(
26
-        [window.innerWidth, window.innerHeight],
27
-        s.data
28
-      )
29
-
30
-      viewportCache.set(pageState, {
31
-        minX,
32
-        minY,
33
-        maxX,
34
-        maxY,
35
-        height: maxX - minX,
36
-        width: maxY - minY,
37
-      })
29
+      const viewport = getViewport(s.data)
30
+      viewportCache.set(pageState, viewport)
38 31
     }
39 32
 
40 33
     const viewport = viewportCache.get(pageState)
41 34
 
42
-    return Object.values(page.shapes)
43
-      .filter((shape) => shape.parentId === page.id)
35
+    return s.values.currentShapes
44 36
       .filter((shape) => {
45 37
         const shapeBounds = getShapeUtils(shape).getBounds(shape)
46 38
         return (
@@ -48,7 +40,6 @@ export default function Page() {
48 40
           boundsCollide(viewport, shapeBounds)
49 41
         )
50 42
       })
51
-      .sort((a, b) => a.childIndex - b.childIndex)
52 43
       .map((shape) => shape.id)
53 44
   }, deepCompareArrays)
54 45
 

+ 48
- 0
state/clipboard.ts Ver arquivo

@@ -0,0 +1,48 @@
1
+import { Data, Shape } from 'types'
2
+import state from './state'
3
+
4
+class Clipboard {
5
+  current: string
6
+  fallback = false
7
+
8
+  copy = (shapes: Shape[], onComplete?: () => void) => {
9
+    this.current = JSON.stringify({ id: 'tldr', shapes })
10
+
11
+    navigator.permissions.query({ name: 'clipboard-write' }).then((result) => {
12
+      if (result.state == 'granted' || result.state == 'prompt') {
13
+        navigator.clipboard.writeText(this.current).then(onComplete, () => {
14
+          console.warn('Error, could not copy to clipboard. Fallback?')
15
+          this.fallback = true
16
+        })
17
+      } else {
18
+        this.fallback = true
19
+      }
20
+    })
21
+  }
22
+
23
+  paste = () => {
24
+    navigator.clipboard
25
+      .readText()
26
+      .then(this.sendPastedTextToState, this.sendPastedTextToState)
27
+  }
28
+
29
+  sendPastedTextToState(text = this.current) {
30
+    if (text === undefined) return
31
+
32
+    try {
33
+      const clipboardData = JSON.parse(text)
34
+      state.send('PASTED_SHAPES_FROM_CLIPBOARD', {
35
+        shapes: clipboardData.shapes,
36
+      })
37
+    } catch (e) {
38
+      // The text wasn't valid JSON, or it wasn't ours, so paste it as a text object
39
+      state.send('PASTED_TEXT_FROM_CLIPBOARD', { text })
40
+    }
41
+  }
42
+
43
+  clear = () => {
44
+    this.current = undefined
45
+  }
46
+}
47
+
48
+export default new Clipboard()

+ 2
- 0
state/commands/index.ts Ver arquivo

@@ -15,6 +15,7 @@ import move from './move'
15 15
 import moveToPage from './move-to-page'
16 16
 import nudge from './nudge'
17 17
 import rotate from './rotate'
18
+import paste from './paste'
18 19
 import rotateCcw from './rotate-ccw'
19 20
 import stretch from './stretch'
20 21
 import style from './style'
@@ -44,6 +45,7 @@ const commands = {
44 45
   move,
45 46
   moveToPage,
46 47
   nudge,
48
+  paste,
47 49
   resetBounds,
48 50
   rotate,
49 51
   rotateCcw,

+ 82
- 0
state/commands/paste.ts Ver arquivo

@@ -0,0 +1,82 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data, Shape } from 'types'
4
+import {
5
+  getChildIndexAbove,
6
+  getCommonBounds,
7
+  getCurrentCamera,
8
+  getPage,
9
+  getSelectedIds,
10
+  getSelectedShapes,
11
+  getViewport,
12
+  screenToWorld,
13
+  setSelectedIds,
14
+  setToArray,
15
+} from 'utils/utils'
16
+import { uniqueId } from 'utils/utils'
17
+import { current } from 'immer'
18
+import vec from 'utils/vec'
19
+import { getShapeUtils } from 'lib/shape-utils'
20
+import state from 'state/state'
21
+
22
+export default function pasteCommand(data: Data, initialShapes: Shape[]) {
23
+  const { currentPageId } = data
24
+
25
+  const center = screenToWorld(
26
+    [window.innerWidth / 2, window.innerHeight / 2],
27
+    data
28
+  )
29
+
30
+  const bounds = getCommonBounds(
31
+    ...initialShapes.map((shape) =>
32
+      getShapeUtils(shape).getRotatedBounds(shape)
33
+    )
34
+  )
35
+
36
+  const topLeft = vec.sub(center, [bounds.width / 2, bounds.height / 2])
37
+
38
+  const newIdMap = Object.fromEntries(
39
+    initialShapes.map((shape) => [shape.id, uniqueId()])
40
+  )
41
+
42
+  const oldSelectedIds = setToArray(getSelectedIds(data))
43
+
44
+  history.execute(
45
+    data,
46
+    new Command({
47
+      name: 'pasting_new_shapes',
48
+      category: 'canvas',
49
+      manualSelection: true,
50
+      do(data) {
51
+        const { shapes } = getPage(data, currentPageId)
52
+
53
+        let childIndex =
54
+          (state.values.currentShapes[state.values.currentShapes.length - 1]
55
+            ?.childIndex || 0) + 1
56
+
57
+        for (const shape of initialShapes) {
58
+          const topLeftOffset = vec.sub(shape.point, [bounds.minX, bounds.minY])
59
+
60
+          const newId = newIdMap[shape.id]
61
+
62
+          shapes[newId] = {
63
+            ...shape,
64
+            id: newId,
65
+            parentId: oldSelectedIds[shape.parentId] || data.currentPageId,
66
+            childIndex: childIndex++,
67
+            point: vec.add(topLeft, topLeftOffset),
68
+          }
69
+        }
70
+
71
+        setSelectedIds(data, Object.values(newIdMap))
72
+      },
73
+      undo(data) {
74
+        const { shapes } = getPage(data, currentPageId)
75
+
76
+        Object.values(newIdMap).forEach((id) => delete shapes[id])
77
+
78
+        setSelectedIds(data, oldSelectedIds)
79
+      },
80
+    })
81
+  )
82
+}

+ 23
- 0
state/state.ts Ver arquivo

@@ -5,6 +5,7 @@ import vec from 'utils/vec'
5 5
 import inputs from './inputs'
6 6
 import history from './history'
7 7
 import storage from './storage'
8
+import clipboard from './clipboard'
8 9
 import * as Sessions from './sessions'
9 10
 import commands from './commands'
10 11
 import {
@@ -133,6 +134,9 @@ const state = createState({
133 134
         else: ['zoomCameraToFit', 'zoomCameraToActual'],
134 135
       },
135 136
       on: {
137
+        COPIED: { if: 'hasSelection', do: 'copyToClipboard' },
138
+        PASTED: { do: 'pasteFromClipboard' },
139
+        PASTED_SHAPES_FROM_CLIPBOARD: 'pasteShapesFromClipboard',
136 140
         LOADED_FONTS: 'resetShapes',
137 141
         TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
138 142
         TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
@@ -1657,6 +1661,18 @@ const state = createState({
1657 1661
 
1658 1662
     /* ---------------------- Data ---------------------- */
1659 1663
 
1664
+    copyToClipboard(data) {
1665
+      clipboard.copy(getSelectedShapes(data))
1666
+    },
1667
+
1668
+    pasteFromClipboard(data) {
1669
+      clipboard.paste()
1670
+    },
1671
+
1672
+    pasteShapesFromClipboard(data, payload: { shapes: Shape[] }) {
1673
+      commands.paste(data, payload.shapes)
1674
+    },
1675
+
1660 1676
     restoreSavedData(data) {
1661 1677
       storage.firstLoad(data)
1662 1678
     },
@@ -1705,6 +1721,13 @@ const state = createState({
1705 1721
     selectedBounds(data) {
1706 1722
       return getSelectionBounds(data)
1707 1723
     },
1724
+    currentShapes(data) {
1725
+      const page = getPage(data)
1726
+
1727
+      return Object.values(page.shapes)
1728
+        .filter((shape) => shape.parentId === page.id)
1729
+        .sort((a, b) => a.childIndex - b.childIndex)
1730
+    },
1708 1731
     selectedStyle(data) {
1709 1732
       const selectedIds = Array.from(getSelectedIds(data).values())
1710 1733
       const { currentStyle } = data

+ 17
- 0
utils/utils.ts Ver arquivo

@@ -15,6 +15,23 @@ export function screenToWorld(point: number[], data: Data) {
15 15
   return vec.sub(vec.div(point, camera.zoom), camera.point)
16 16
 }
17 17
 
18
+export function getViewport(data: Data): Bounds {
19
+  const [minX, minY] = screenToWorld([0, 0], data)
20
+  const [maxX, maxY] = screenToWorld(
21
+    [window.innerWidth, window.innerHeight],
22
+    data
23
+  )
24
+
25
+  return {
26
+    minX,
27
+    minY,
28
+    maxX,
29
+    maxY,
30
+    height: maxX - minX,
31
+    width: maxY - minY,
32
+  }
33
+}
34
+
18 35
 /**
19 36
  * Get a bounding box that includes two bounding boxes.
20 37
  * @param a Bounding box

Carregando…
Cancelar
Salvar