Преглед на файлове

Adds copy and paste

main
Steve Ruiz преди 4 години
родител
ревизия
eccf5f6307
променени са 6 файла, в които са добавени 181 реда и са изтрити 18 реда
  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 Целия файл

2
 import state, { useSelector } from 'state'
2
 import state, { useSelector } from 'state'
3
 import { Bounds, GroupShape, PageState } from 'types'
3
 import { Bounds, GroupShape, PageState } from 'types'
4
 import { boundsCollide, boundsContain } from 'utils/bounds'
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
 import Shape from './shape'
11
 import Shape from './shape'
7
 
12
 
8
 /* 
13
 /* 
21
     const pageState = s.data.pageStates[page.id]
26
     const pageState = s.data.pageStates[page.id]
22
 
27
 
23
     if (!viewportCache.has(pageState)) {
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
     const viewport = viewportCache.get(pageState)
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
       .filter((shape) => {
36
       .filter((shape) => {
45
         const shapeBounds = getShapeUtils(shape).getBounds(shape)
37
         const shapeBounds = getShapeUtils(shape).getBounds(shape)
46
         return (
38
         return (
48
           boundsCollide(viewport, shapeBounds)
40
           boundsCollide(viewport, shapeBounds)
49
         )
41
         )
50
       })
42
       })
51
-      .sort((a, b) => a.childIndex - b.childIndex)
52
       .map((shape) => shape.id)
43
       .map((shape) => shape.id)
53
   }, deepCompareArrays)
44
   }, deepCompareArrays)
54
 
45
 

+ 48
- 0
state/clipboard.ts Целия файл

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 Целия файл

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

+ 82
- 0
state/commands/paste.ts Целия файл

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 Целия файл

5
 import inputs from './inputs'
5
 import inputs from './inputs'
6
 import history from './history'
6
 import history from './history'
7
 import storage from './storage'
7
 import storage from './storage'
8
+import clipboard from './clipboard'
8
 import * as Sessions from './sessions'
9
 import * as Sessions from './sessions'
9
 import commands from './commands'
10
 import commands from './commands'
10
 import {
11
 import {
133
         else: ['zoomCameraToFit', 'zoomCameraToActual'],
134
         else: ['zoomCameraToFit', 'zoomCameraToActual'],
134
       },
135
       },
135
       on: {
136
       on: {
137
+        COPIED: { if: 'hasSelection', do: 'copyToClipboard' },
138
+        PASTED: { do: 'pasteFromClipboard' },
139
+        PASTED_SHAPES_FROM_CLIPBOARD: 'pasteShapesFromClipboard',
136
         LOADED_FONTS: 'resetShapes',
140
         LOADED_FONTS: 'resetShapes',
137
         TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
141
         TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
138
         TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
142
         TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
1657
 
1661
 
1658
     /* ---------------------- Data ---------------------- */
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
     restoreSavedData(data) {
1676
     restoreSavedData(data) {
1661
       storage.firstLoad(data)
1677
       storage.firstLoad(data)
1662
     },
1678
     },
1705
     selectedBounds(data) {
1721
     selectedBounds(data) {
1706
       return getSelectionBounds(data)
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
     selectedStyle(data) {
1731
     selectedStyle(data) {
1709
       const selectedIds = Array.from(getSelectedIds(data).values())
1732
       const selectedIds = Array.from(getSelectedIds(data).values())
1710
       const { currentStyle } = data
1733
       const { currentStyle } = data

+ 17
- 0
utils/utils.ts Целия файл

15
   return vec.sub(vec.div(point, camera.zoom), camera.point)
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
  * Get a bounding box that includes two bounding boxes.
36
  * Get a bounding box that includes two bounding boxes.
20
  * @param a Bounding box
37
  * @param a Bounding box

Loading…
Отказ
Запис