Browse Source

Moves selectedIds into page state, state mounts only one page state / page at a time

main
Steve Ruiz 4 years ago
parent
commit
350c1debde

+ 2
- 1
components/canvas/bounds/bounding-box.tsx View File

@@ -6,6 +6,7 @@ import {
6 6
   getBoundsCenter,
7 7
   getCurrentCamera,
8 8
   getPage,
9
+  getSelectedIds,
9 10
   getSelectedShapes,
10 11
   isMobile,
11 12
 } from 'utils/utils'
@@ -28,7 +29,7 @@ export default function Bounds() {
28 29
   )
29 30
 
30 31
   const rotation = useSelector(({ data }) =>
31
-    data.selectedIds.size === 1 ? getSelectedShapes(data)[0].rotation : 0
32
+    getSelectedIds(data).size === 1 ? getSelectedShapes(data)[0].rotation : 0
32 33
   )
33 34
 
34 35
   const isAllLocked = useSelector((s) => {

+ 0
- 1
components/canvas/page.tsx View File

@@ -43,7 +43,6 @@ export default function Page() {
43 43
         .filter((shape) => shape.parentId === page.id)
44 44
         // .filter((shape) => {
45 45
         //   const shapeBounds = getShapeUtils(shape).getBounds(shape)
46
-        //   console.log(shapeBounds, viewport)
47 46
         //   return boundsContain(viewport, shapeBounds)
48 47
         // })
49 48
         .sort((a, b) => a.childIndex - b.childIndex)

+ 11
- 4
components/canvas/selected.tsx View File

@@ -1,6 +1,12 @@
1 1
 import styled from 'styles'
2 2
 import { useSelector } from 'state'
3
-import { deepCompareArrays, getBoundsCenter, getPage } from 'utils/utils'
3
+import {
4
+  deepCompareArrays,
5
+  getBoundsCenter,
6
+  getPage,
7
+  getSelectedIds,
8
+  setToArray,
9
+} from 'utils/utils'
4 10
 import { getShapeUtils } from 'lib/shape-utils'
5 11
 import useShapeEvents from 'hooks/useShapeEvents'
6 12
 import { memo, useRef } from 'react'
@@ -8,9 +14,10 @@ import { ShapeType } from 'types'
8 14
 import * as vec from 'utils/vec'
9 15
 
10 16
 export default function Selected() {
11
-  const currentSelectedShapeIds = useSelector(({ data }) => {
12
-    return Array.from(data.selectedIds.values())
13
-  }, deepCompareArrays)
17
+  const currentSelectedShapeIds = useSelector(
18
+    ({ data }) => setToArray(getSelectedIds(data)),
19
+    deepCompareArrays
20
+  )
14 21
 
15 22
   const isSelecting = useSelector((s) => s.isIn('selecting'))
16 23
 

+ 1
- 1
components/page-panel/page-panel.tsx View File

@@ -52,7 +52,7 @@ export default function PagePanel() {
52 52
               value={currentPageId}
53 53
               onValueChange={(id) => {
54 54
                 setIsOpen(false)
55
-                state.send('CHANGED_CURRENT_PAGE', { id })
55
+                state.send('CHANGED_PAGE', { id })
56 56
               }}
57 57
             >
58 58
               {sorted.map(({ id, name }) => (

+ 8
- 2
components/style-panel/style-panel.tsx View File

@@ -4,7 +4,13 @@ import * as Panel from 'components/panel'
4 4
 import { useRef } from 'react'
5 5
 import { IconButton } from 'components/shared'
6 6
 import { ChevronDown, Trash2, X } from 'react-feather'
7
-import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
7
+import {
8
+  deepCompare,
9
+  deepCompareArrays,
10
+  getPage,
11
+  getSelectedIds,
12
+  setToArray,
13
+} from 'utils/utils'
8 14
 import AlignDistribute from './align-distribute'
9 15
 import { MoveType } from 'types'
10 16
 import SizePicker from './size-picker'
@@ -65,7 +71,7 @@ export default function StylePanel() {
65 71
 
66 72
 function SelectedShapeStyles() {
67 73
   const selectedIds = useSelector(
68
-    (s) => Array.from(s.data.selectedIds.values()),
74
+    (s) => setToArray(getSelectedIds(s.data)),
69 75
     deepCompareArrays
70 76
   )
71 77
 

+ 24
- 8
lib/shape-utils/draw.tsx View File

@@ -96,7 +96,10 @@ const draw = registerShapeUtils<DrawShape>({
96 96
     if (shape.rotation === 0) {
97 97
       return (
98 98
         boundsContain(brushBounds, this.getBounds(shape)) ||
99
-        intersectPolylineBounds(shape.points, brushBounds).length > 0
99
+        intersectPolylineBounds(
100
+          shape.points,
101
+          translateBounds(brushBounds, vec.neg(shape.point))
102
+        ).length > 0
100 103
       )
101 104
     }
102 105
 
@@ -104,18 +107,19 @@ const draw = registerShapeUtils<DrawShape>({
104 107
     const rBounds = this.getRotatedBounds(shape)
105 108
 
106 109
     if (!rotatedCache.has(shape)) {
107
-      const c = getBoundsCenter(rBounds)
110
+      const c = getBoundsCenter(getBoundsFromPoints(shape.points))
108 111
       rotatedCache.set(
109 112
         shape,
110
-        shape.points.map((pt) =>
111
-          vec.rotWith(vec.add(pt, shape.point), c, shape.rotation)
112
-        )
113
+        shape.points.map((pt) => vec.rotWith(pt, c, shape.rotation))
113 114
       )
114 115
     }
115 116
 
116 117
     return (
117 118
       boundsContain(brushBounds, rBounds) ||
118
-      intersectPolylineBounds(rotatedCache.get(shape), brushBounds).length > 0
119
+      intersectPolylineBounds(
120
+        rotatedCache.get(shape),
121
+        translateBounds(brushBounds, vec.neg(shape.point))
122
+      ).length > 0
119 123
     )
120 124
   },
121 125
 
@@ -152,6 +156,18 @@ const draw = registerShapeUtils<DrawShape>({
152 156
     return this
153 157
   },
154 158
 
159
+  onSessionComplete(shape) {
160
+    const bounds = this.getBounds(shape)
161
+
162
+    const [x1, y1] = vec.sub([bounds.minX, bounds.minY], shape.point)
163
+
164
+    shape.points = shape.points.map(([x0, y0, p]) => [x0 - x1, y0 - y1, p])
165
+
166
+    this.translateTo(shape, vec.add(shape.point, [x1, y1]))
167
+
168
+    return this
169
+  },
170
+
155 171
   canStyleFill: false,
156 172
 })
157 173
 
@@ -164,8 +180,8 @@ const simulatePressureSettings = {
164 180
 const realPressureSettings = {
165 181
   easing: (t: number) => t * t,
166 182
   simulatePressure: false,
167
-  // start: { taper: 1 },
168
-  // end: { taper: 1 },
183
+  start: { taper: 1 },
184
+  end: { taper: 1 },
169 185
 }
170 186
 
171 187
 function renderPath(shape: DrawShape, style: ShapeStyles) {

+ 6
- 4
state/commands/arrow.ts View File

@@ -1,7 +1,7 @@
1 1
 import Command from './command'
2 2
 import history from '../history'
3 3
 import { Data } from 'types'
4
-import { getPage } from 'utils/utils'
4
+import { getPage, getSelectedIds } from 'utils/utils'
5 5
 import { ArrowSnapshot } from 'state/sessions/arrow-session'
6 6
 
7 7
 export default function arrowCommand(
@@ -24,8 +24,9 @@ export default function arrowCommand(
24 24
 
25 25
         page.shapes[initialShape.id] = initialShape
26 26
 
27
-        data.selectedIds.clear()
28
-        data.selectedIds.add(initialShape.id)
27
+        const selectedIds = getSelectedIds(data)
28
+        selectedIds.clear()
29
+        selectedIds.add(initialShape.id)
29 30
         data.hoveredId = undefined
30 31
         data.pointedId = undefined
31 32
       },
@@ -35,7 +36,8 @@ export default function arrowCommand(
35 36
 
36 37
         delete shapes[initialShape.id]
37 38
 
38
-        data.selectedIds.clear()
39
+        const selectedIds = getSelectedIds(data)
40
+        selectedIds.clear()
39 41
         data.hoveredId = undefined
40 42
         data.pointedId = undefined
41 43
       },

+ 5
- 3
state/commands/change-page.ts View File

@@ -1,9 +1,7 @@
1 1
 import Command from './command'
2 2
 import history from '../history'
3 3
 import { Data } from 'types'
4
-import { getPage, getSelectedShapes } from 'utils/utils'
5
-import { getShapeUtils } from 'lib/shape-utils'
6
-import * as vec from 'utils/vec'
4
+import storage from 'state/storage'
7 5
 
8 6
 export default function changePage(data: Data, pageId: string) {
9 7
   const { currentPageId: prevPageId } = data
@@ -13,11 +11,15 @@ export default function changePage(data: Data, pageId: string) {
13 11
     new Command({
14 12
       name: 'change_page',
15 13
       category: 'canvas',
14
+      manualSelection: true,
16 15
       do(data) {
16
+        storage.savePage(data, data.currentPageId)
17 17
         data.currentPageId = pageId
18
+        storage.loadPage(data, data.currentPageId)
18 19
       },
19 20
       undo(data) {
20 21
         data.currentPageId = prevPageId
22
+        storage.loadPage(data, prevPageId)
21 23
       },
22 24
     })
23 25
   )

+ 15
- 6
state/commands/command.ts View File

@@ -1,4 +1,5 @@
1
-import { Data } from "types"
1
+import { Data } from 'types'
2
+import { getSelectedIds, setSelectedIds, setToArray } from 'utils/utils'
2 3
 
3 4
 /* ------------------ Command Class ----------------- */
4 5
 
@@ -52,6 +53,12 @@ export class BaseCommand<T extends any> {
52 53
   }
53 54
 
54 55
   redo = (data: T, initial = false) => {
56
+    if (this.manualSelection) {
57
+      this.doFn(data, initial)
58
+
59
+      return
60
+    }
61
+
55 62
     if (initial) {
56 63
       this.restoreBeforeSelectionState = this.saveSelectionState(data)
57 64
     } else {
@@ -76,11 +83,13 @@ export class BaseCommand<T extends any> {
76 83
  */
77 84
 export default class Command extends BaseCommand<Data> {
78 85
   saveSelectionState = (data: Data) => {
79
-    const selectedIds = new Set(data.selectedIds)
80
-    return (data: Data) => {
81
-      data.hoveredId = undefined
82
-      data.pointedId = undefined
83
-      data.selectedIds = selectedIds
86
+    const { currentPageId } = data
87
+    const selectedIds = setToArray(getSelectedIds(data))
88
+    return (next: Data) => {
89
+      next.currentPageId = currentPageId
90
+      next.hoveredId = undefined
91
+      next.pointedId = undefined
92
+      setSelectedIds(next, selectedIds)
84 93
     }
85 94
   }
86 95
 }

+ 6
- 4
state/commands/create-page.ts View File

@@ -1,8 +1,10 @@
1 1
 import Command from './command'
2 2
 import history from '../history'
3
-import { Data, Page } from 'types'
3
+import { Data, Page, PageState } from 'types'
4 4
 import { v4 as uuid } from 'uuid'
5 5
 import { current } from 'immer'
6
+import { getSelectedIds } from 'utils/utils'
7
+import storage from 'state/storage'
6 8
 
7 9
 export default function createPage(data: Data) {
8 10
   const snapshot = getSnapshot(data)
@@ -13,14 +15,13 @@ export default function createPage(data: Data) {
13 15
       name: 'change_page',
14 16
       category: 'canvas',
15 17
       do(data) {
16
-        data.selectedIds.clear()
17 18
         const { page, pageState } = snapshot
18 19
         data.document.pages[page.id] = page
19 20
         data.pageStates[page.id] = pageState
20 21
         data.currentPageId = page.id
22
+        storage.savePage(data, page.id)
21 23
       },
22 24
       undo(data) {
23
-        data.selectedIds.clear()
24 25
         const { page, currentPageId } = snapshot
25 26
         delete data.document.pages[page.id]
26 27
         delete data.pageStates[page.id]
@@ -44,7 +45,8 @@ function getSnapshot(data: Data) {
44 45
     childIndex: pages.length,
45 46
     shapes: {},
46 47
   }
47
-  const pageState = {
48
+  const pageState: PageState = {
49
+    selectedIds: new Set<string>(),
48 50
     camera: {
49 51
       point: [0, 0],
50 52
       zoom: 1,

+ 15
- 7
state/commands/delete-selected.ts View File

@@ -2,22 +2,30 @@ import Command from './command'
2 2
 import history from '../history'
3 3
 import { TranslateSnapshot } from 'state/sessions/translate-session'
4 4
 import { Data, ShapeType } from 'types'
5
-import { getDocumentBranch, getPage, updateParents } from 'utils/utils'
5
+import {
6
+  getDocumentBranch,
7
+  getPage,
8
+  getSelectedIds,
9
+  setSelectedIds,
10
+  setToArray,
11
+  updateParents,
12
+} from 'utils/utils'
6 13
 import { current } from 'immer'
7 14
 import { getShapeUtils } from 'lib/shape-utils'
8 15
 
9 16
 export default function deleteSelected(data: Data) {
10 17
   const { currentPageId } = data
11 18
 
12
-  const selectedIds = Array.from(data.selectedIds.values())
19
+  const selectedIds = getSelectedIds(data)
20
+  const selectedIdsArr = setToArray(selectedIds)
13 21
 
14 22
   const page = getPage(current(data))
15 23
 
16
-  const childrenToDelete = selectedIds
24
+  const childrenToDelete = selectedIdsArr
17 25
     .flatMap((id) => getDocumentBranch(data, id))
18 26
     .map((id) => page.shapes[id])
19 27
 
20
-  data.selectedIds.clear()
28
+  selectedIds.clear()
21 29
 
22 30
   history.execute(
23 31
     data,
@@ -28,7 +36,7 @@ export default function deleteSelected(data: Data) {
28 36
       do(data) {
29 37
         const page = getPage(data, currentPageId)
30 38
 
31
-        for (let id of selectedIds) {
39
+        for (let id of selectedIdsArr) {
32 40
           const shape = page.shapes[id]
33 41
           if (!shape) {
34 42
             console.error('no shape ' + id)
@@ -54,7 +62,7 @@ export default function deleteSelected(data: Data) {
54 62
           delete page.shapes[shape.id]
55 63
         }
56 64
 
57
-        data.selectedIds.clear()
65
+        setSelectedIds(data, [])
58 66
       },
59 67
       undo(data) {
60 68
         const page = getPage(data, currentPageId)
@@ -75,7 +83,7 @@ export default function deleteSelected(data: Data) {
75 83
           }
76 84
         }
77 85
 
78
-        data.selectedIds = new Set(selectedIds)
86
+        setSelectedIds(data, selectedIdsArr)
79 87
       },
80 88
     })
81 89
   )

+ 5
- 11
state/commands/draw.ts View File

@@ -1,17 +1,11 @@
1 1
 import Command from './command'
2 2
 import history from '../history'
3 3
 import { Data, DrawShape } from 'types'
4
-import { getPage } from 'utils/utils'
5
-import { getShapeUtils } from 'lib/shape-utils'
4
+import { getPage, setSelectedIds } from 'utils/utils'
6 5
 import { current } from 'immer'
7 6
 
8
-export default function drawCommand(
9
-  data: Data,
10
-  id: string,
11
-  points: number[][]
12
-) {
13
-  const restoreShape = current(getPage(data)).shapes[id] as DrawShape
14
-  getShapeUtils(restoreShape).setProperty(restoreShape, 'points', points)
7
+export default function drawCommand(data: Data, id: string) {
8
+  const restoreShape = getPage(current(data)).shapes[id] as DrawShape
15 9
 
16 10
   history.execute(
17 11
     data,
@@ -24,11 +18,11 @@ export default function drawCommand(
24 18
           getPage(data).shapes[id] = restoreShape
25 19
         }
26 20
 
27
-        data.selectedIds.clear()
21
+        setSelectedIds(data, [])
28 22
       },
29 23
       undo(data) {
24
+        setSelectedIds(data, [])
30 25
         delete getPage(data).shapes[id]
31
-        data.selectedIds.clear()
32 26
       },
33 27
     })
34 28
   )

+ 16
- 8
state/commands/duplicate.ts View File

@@ -1,7 +1,13 @@
1 1
 import Command from './command'
2 2
 import history from '../history'
3 3
 import { Data } from 'types'
4
-import { getCurrentCamera, getPage, getSelectedShapes } from 'utils/utils'
4
+import {
5
+  getCurrentCamera,
6
+  getPage,
7
+  getSelectedIds,
8
+  getSelectedShapes,
9
+  setSelectedIds,
10
+} from 'utils/utils'
5 11
 import { v4 as uuid } from 'uuid'
6 12
 import { current } from 'immer'
7 13
 import * as vec from 'utils/vec'
@@ -24,24 +30,26 @@ export default function duplicateCommand(data: Data) {
24 30
       do(data) {
25 31
         const { shapes } = getPage(data, currentPageId)
26 32
 
27
-        data.selectedIds.clear()
28
-
29 33
         for (const duplicate of duplicates) {
30 34
           shapes[duplicate.id] = duplicate
31
-          data.selectedIds.add(duplicate.id)
32 35
         }
36
+
37
+        setSelectedIds(
38
+          data,
39
+          duplicates.map((d) => d.id)
40
+        )
33 41
       },
34 42
       undo(data) {
35 43
         const { shapes } = getPage(data, currentPageId)
36
-        data.selectedIds.clear()
37 44
 
38 45
         for (const duplicate of duplicates) {
39 46
           delete shapes[duplicate.id]
40 47
         }
41 48
 
42
-        for (let id in selectedShapes) {
43
-          data.selectedIds.add(id)
44
-        }
49
+        setSelectedIds(
50
+          data,
51
+          selectedShapes.map((d) => d.id)
52
+        )
45 53
       },
46 54
     })
47 55
   )

+ 8
- 8
state/commands/generate.ts View File

@@ -1,8 +1,8 @@
1
-import Command from "./command"
2
-import history from "../history"
3
-import { CodeControl, Data, Shape } from "types"
4
-import { current } from "immer"
5
-import { getPage } from "utils/utils"
1
+import Command from './command'
2
+import history from '../history'
3
+import { CodeControl, Data, Shape } from 'types'
4
+import { current } from 'immer'
5
+import { getPage, getSelectedIds, setSelectedIds } from 'utils/utils'
6 6
 
7 7
 export default function generateCommand(
8 8
   data: Data,
@@ -33,12 +33,12 @@ export default function generateCommand(
33 33
   history.execute(
34 34
     data,
35 35
     new Command({
36
-      name: "translate_shapes",
37
-      category: "canvas",
36
+      name: 'translate_shapes',
37
+      category: 'canvas',
38 38
       do(data) {
39 39
         const { shapes } = getPage(data)
40 40
 
41
-        data.selectedIds.clear()
41
+        setSelectedIds(data, [])
42 42
 
43 43
         // Remove previous generated shapes
44 44
         for (let id in shapes) {

+ 8
- 5
state/commands/group.ts View File

@@ -4,8 +4,10 @@ import { Data, GroupShape, Shape, ShapeType } from 'types'
4 4
 import {
5 5
   getCommonBounds,
6 6
   getPage,
7
+  getSelectedIds,
7 8
   getSelectedShapes,
8 9
   getShape,
10
+  setSelectedIds,
9 11
 } from 'utils/utils'
10 12
 import { current } from 'immer'
11 13
 import { createShape, getShapeUtils } from 'lib/shape-utils'
@@ -15,7 +17,9 @@ import commands from '.'
15 17
 
16 18
 export default function groupCommand(data: Data) {
17 19
   const cData = current(data)
18
-  const { currentPageId, selectedIds } = cData
20
+  const { currentPageId } = cData
21
+
22
+  const oldSelectedIds = getSelectedIds(cData)
19 23
 
20 24
   const initialShapes = getSelectedShapes(cData).sort(
21 25
     (a, b) => a.childIndex - b.childIndex
@@ -108,7 +112,7 @@ export default function groupCommand(data: Data) {
108 112
             getShapeUtils(oldParent).setProperty(
109 113
               oldParent,
110 114
               'children',
111
-              oldParent.children.filter((id) => !selectedIds.has(id))
115
+              oldParent.children.filter((id) => !oldSelectedIds.has(id))
112 116
             )
113 117
           }
114 118
 
@@ -119,8 +123,7 @@ export default function groupCommand(data: Data) {
119 123
             .setProperty(shape, 'parentId', newGroupShape.id)
120 124
         })
121 125
 
122
-        data.selectedIds.clear()
123
-        data.selectedIds.add(newGroupShape.id)
126
+        setSelectedIds(data, [newGroupShape.id])
124 127
       },
125 128
       undo(data) {
126 129
         const { shapes } = getPage(data, currentPageId)
@@ -157,7 +160,7 @@ export default function groupCommand(data: Data) {
157 160
         delete shapes[newGroupShape.id]
158 161
 
159 162
         // Reselect the children of the group
160
-        data.selectedIds = new Set(initialShapeIds)
163
+        setSelectedIds(data, initialShapeIds)
161 164
       },
162 165
     })
163 166
   )

+ 8
- 2
state/commands/move.ts View File

@@ -1,7 +1,13 @@
1 1
 import Command from './command'
2 2
 import history from '../history'
3 3
 import { Data, MoveType, Shape } from 'types'
4
-import { forceIntegerChildIndices, getChildren, getPage } from 'utils/utils'
4
+import {
5
+  forceIntegerChildIndices,
6
+  getChildren,
7
+  getPage,
8
+  getSelectedIds,
9
+  setToArray,
10
+} from 'utils/utils'
5 11
 import { getShapeUtils } from 'lib/shape-utils'
6 12
 
7 13
 export default function moveCommand(data: Data, type: MoveType) {
@@ -9,7 +15,7 @@ export default function moveCommand(data: Data, type: MoveType) {
9 15
 
10 16
   const page = getPage(data)
11 17
 
12
-  const selectedIds = Array.from(data.selectedIds.values())
18
+  const selectedIds = setToArray(getSelectedIds(data))
13 19
 
14 20
   const initialIndices = Object.fromEntries(
15 21
     selectedIds.map((id) => [id, page.shapes[id].childIndex])

+ 10
- 2
state/commands/style.ts View File

@@ -1,7 +1,13 @@
1 1
 import Command from './command'
2 2
 import history from '../history'
3 3
 import { Data, ShapeStyles } from 'types'
4
-import { getDocumentBranch, getPage, getSelectedShapes } from 'utils/utils'
4
+import {
5
+  getDocumentBranch,
6
+  getPage,
7
+  getSelectedIds,
8
+  getSelectedShapes,
9
+  setToArray,
10
+} from 'utils/utils'
5 11
 import { getShapeUtils } from 'lib/shape-utils'
6 12
 import { current } from 'immer'
7 13
 
@@ -10,7 +16,9 @@ export default function styleCommand(data: Data, styles: Partial<ShapeStyles>) {
10 16
   const page = getPage(cData)
11 17
   const { currentPageId } = cData
12 18
 
13
-  const shapesToStyle = Array.from(data.selectedIds.values())
19
+  const selectedIds = setToArray(getSelectedIds(data))
20
+
21
+  const shapesToStyle = selectedIds
14 22
     .flatMap((id) => getDocumentBranch(data, id))
15 23
     .map((id) => page.shapes[id])
16 24
 

+ 9
- 5
state/commands/transform-single.ts View File

@@ -4,7 +4,12 @@ import { Data, Corner, Edge } from 'types'
4 4
 import { getShapeUtils } from 'lib/shape-utils'
5 5
 import { current } from 'immer'
6 6
 import { TransformSingleSnapshot } from 'state/sessions/transform-single-session'
7
-import { getPage, updateParents } from 'utils/utils'
7
+import {
8
+  getPage,
9
+  getSelectedIds,
10
+  setSelectedIds,
11
+  updateParents,
12
+} from 'utils/utils'
8 13
 
9 14
 export default function transformSingleCommand(
10 15
   data: Data,
@@ -25,8 +30,7 @@ export default function transformSingleCommand(
25 30
 
26 31
         const { shapes } = getPage(data, after.currentPageId)
27 32
 
28
-        data.selectedIds.clear()
29
-        data.selectedIds.add(id)
33
+        setSelectedIds(data, [id])
30 34
 
31 35
         shapes[id] = shape
32 36
 
@@ -38,13 +42,13 @@ export default function transformSingleCommand(
38 42
         const { shapes } = getPage(data, before.currentPageId)
39 43
 
40 44
         if (isCreating) {
41
-          data.selectedIds.clear()
45
+          setSelectedIds(data, [])
42 46
           delete shapes[id]
43 47
         } else {
44 48
           const page = getPage(data)
45 49
           page.shapes[id] = initialShape
46 50
           updateParents(data, [id])
47
-          data.selectedIds = new Set([id])
51
+          setSelectedIds(data, [id])
48 52
         }
49 53
       },
50 54
     })

+ 14
- 3
state/commands/translate.ts View File

@@ -2,7 +2,12 @@ import Command from './command'
2 2
 import history from '../history'
3 3
 import { TranslateSnapshot } from 'state/sessions/translate-session'
4 4
 import { Data, GroupShape, Shape, ShapeType } from 'types'
5
-import { getDocumentBranch, getPage, updateParents } from 'utils/utils'
5
+import {
6
+  getDocumentBranch,
7
+  getPage,
8
+  setSelectedIds,
9
+  updateParents,
10
+} from 'utils/utils'
6 11
 import { getShapeUtils } from 'lib/shape-utils'
7 12
 import { v4 as uuid } from 'uuid'
8 13
 
@@ -50,7 +55,10 @@ export default function translateCommand(
50 55
         }
51 56
 
52 57
         // Set selected shapes
53
-        data.selectedIds = new Set(initialShapes.map((s) => s.id))
58
+        setSelectedIds(
59
+          data,
60
+          initialShapes.map((s) => s.id)
61
+        )
54 62
 
55 63
         // Update parents
56 64
         updateParents(
@@ -72,7 +80,10 @@ export default function translateCommand(
72 80
         if (isCloning) for (const { id } of clones) delete shapes[id]
73 81
 
74 82
         // Set selected shapes
75
-        data.selectedIds = new Set(initialShapes.map((s) => s.id))
83
+        setSelectedIds(
84
+          data,
85
+          initialShapes.map((s) => s.id)
86
+        )
76 87
 
77 88
         // Restore children on parents
78 89
         initialParents.forEach(({ id, children }) => {

+ 10
- 6
state/commands/ungroup.ts View File

@@ -6,6 +6,7 @@ import {
6 6
   getPage,
7 7
   getSelectedShapes,
8 8
   getShape,
9
+  setSelectedIds,
9 10
 } from 'utils/utils'
10 11
 import { current } from 'immer'
11 12
 import { createShape, getShapeUtils } from 'lib/shape-utils'
@@ -14,7 +15,7 @@ import { v4 as uuid } from 'uuid'
14 15
 
15 16
 export default function ungroupCommand(data: Data) {
16 17
   const cData = current(data)
17
-  const { currentPageId, selectedIds } = cData
18
+  const { currentPageId } = cData
18 19
 
19 20
   const selectedGroups = getSelectedShapes(cData)
20 21
     .filter((shape) => shape.type === ShapeType.Group)
@@ -55,14 +56,11 @@ export default function ungroupCommand(data: Data) {
55 56
               (oldGroupShape.children.length + 1)
56 57
           }
57 58
 
58
-          data.selectedIds.clear()
59
-
60 59
           // Move shapes to page
61 60
           oldGroupShape.children
62 61
             .map((id) => shapes[id])
63 62
             .forEach(({ id }, i) => {
64 63
               const shape = shapes[id]
65
-              data.selectedIds.add(id)
66 64
               getShapeUtils(shape)
67 65
                 .setProperty(shape, 'parentId', oldGroupShape.parentId)
68 66
                 .setProperty(
@@ -72,14 +70,15 @@ export default function ungroupCommand(data: Data) {
72 70
                 )
73 71
             })
74 72
 
73
+          setSelectedIds(data, oldGroupShape.children)
74
+
75 75
           delete shapes[oldGroupShape.id]
76 76
         }
77 77
       },
78 78
       undo(data) {
79 79
         const { shapes } = getPage(data, currentPageId)
80
-        selectedIds.clear()
80
+
81 81
         selectedGroups.forEach((group) => {
82
-          selectedIds.add(group.id)
83 82
           shapes[group.id] = group
84 83
           group.children.forEach((id, i) => {
85 84
             const shape = shapes[id]
@@ -88,6 +87,11 @@ export default function ungroupCommand(data: Data) {
88 87
               .setProperty(shape, 'childIndex', i)
89 88
           })
90 89
         })
90
+
91
+        setSelectedIds(
92
+          data,
93
+          selectedGroups.map((g) => g.id)
94
+        )
91 95
       },
92 96
     })
93 97
   )

+ 3
- 3
state/hacks.ts View File

@@ -2,7 +2,9 @@ import { PointerInfo } from 'types'
2 2
 import {
3 3
   getCameraZoom,
4 4
   getCurrentCamera,
5
+  getSelectedIds,
5 6
   screenToWorld,
7
+  setToArray,
6 8
   setZoomCSS,
7 9
 } from 'utils/utils'
8 10
 import session from './session'
@@ -26,7 +28,7 @@ export function fastDrawUpdate(info: PointerInfo) {
26 28
     info.shiftKey
27 29
   )
28 30
 
29
-  const selectedId = Array.from(data.selectedIds.values())[0]
31
+  const selectedId = setToArray(getSelectedIds(data))[0]
30 32
 
31 33
   const shape = data.document.pages[data.currentPageId].shapes[selectedId]
32 34
 
@@ -88,7 +90,5 @@ export function fastBrushSelect(point: number[]) {
88 90
   const data = { ...state.data }
89 91
   session.current.update(data, screenToWorld(point, data))
90 92
 
91
-  data.selectedIds = new Set(data.selectedIds)
92
-
93 93
   state.forceData(Object.freeze(data))
94 94
 }

+ 6
- 73
state/history.ts View File

@@ -1,11 +1,10 @@
1
-import { Data } from 'types'
1
+import { Data, Page, PageState } from 'types'
2 2
 import { BaseCommand } from './commands/command'
3
-
4
-const CURRENT_VERSION = 'code_slate_0.0.3'
3
+import storage from './storage'
5 4
 
6 5
 // A singleton to manage history changes.
7 6
 
8
-class BaseHistory<T> {
7
+class History<T extends Data> {
9 8
   private stack: BaseCommand<T>[] = []
10 9
   private pointer = -1
11 10
   private maxLength = 100
@@ -24,7 +23,7 @@ class BaseHistory<T> {
24 23
       this.pointer = this.maxLength - 1
25 24
     }
26 25
 
27
-    this.save(data)
26
+    storage.save(data)
28 27
   }
29 28
 
30 29
   undo = (data: T) => {
@@ -33,7 +32,7 @@ class BaseHistory<T> {
33 32
     command.undo(data)
34 33
     if (this.disabled) return
35 34
     this.pointer--
36
-    this.save(data)
35
+    storage.save(data)
37 36
   }
38 37
 
39 38
   redo = (data: T) => {
@@ -42,25 +41,7 @@ class BaseHistory<T> {
42 41
     command.redo(data, false)
43 42
     if (this.disabled) return
44 43
     this.pointer++
45
-    this.save(data)
46
-  }
47
-
48
-  load(data: T, id = CURRENT_VERSION) {
49
-    if (typeof window === 'undefined') return
50
-    if (typeof localStorage === 'undefined') return
51
-
52
-    const savedData = localStorage.getItem(id)
53
-
54
-    if (savedData !== null) {
55
-      Object.assign(data, this.restoreSavedData(JSON.parse(savedData)))
56
-    }
57
-  }
58
-
59
-  save = (data: T, id = CURRENT_VERSION) => {
60
-    if (typeof window === 'undefined') return
61
-    if (typeof localStorage === 'undefined') return
62
-
63
-    localStorage.setItem(id, JSON.stringify(this.prepareDataForSave(data)))
44
+    storage.save(data)
64 45
   }
65 46
 
66 47
   disable = () => {
@@ -71,14 +52,6 @@ class BaseHistory<T> {
71 52
     this._enabled = true
72 53
   }
73 54
 
74
-  prepareDataForSave(data: T): any {
75
-    return { ...data }
76
-  }
77
-
78
-  restoreSavedData(data: any): T {
79
-    return { ...data }
80
-  }
81
-
82 55
   pop() {
83 56
     if (this.stack.length > 0) {
84 57
       this.stack.pop()
@@ -91,44 +64,4 @@ class BaseHistory<T> {
91 64
   }
92 65
 }
93 66
 
94
-// App-specific
95
-
96
-class History extends BaseHistory<Data> {
97
-  constructor() {
98
-    super()
99
-  }
100
-
101
-  prepareDataForSave(data: Data): any {
102
-    const dataToSave: any = { ...data }
103
-
104
-    dataToSave.selectedIds = Array.from(data.selectedIds.values())
105
-
106
-    return dataToSave
107
-  }
108
-
109
-  restoreSavedData(data: any): Data {
110
-    const restoredData: Data = { ...data }
111
-
112
-    restoredData.selectedIds = new Set(restoredData.selectedIds)
113
-
114
-    // Also restore camera position, which is saved separately in this app
115
-    const cameraInfo = localStorage.getItem('code_slate_camera')
116
-
117
-    if (cameraInfo !== null) {
118
-      Object.assign(
119
-        restoredData.pageStates[data.currentPageId].camera,
120
-        JSON.parse(cameraInfo)
121
-      )
122
-
123
-      // And update the CSS property
124
-      document.documentElement.style.setProperty(
125
-        '--camera-zoom',
126
-        restoredData.pageStates[data.currentPageId].camera.zoom.toString()
127
-      )
128
-    }
129
-
130
-    return restoredData
131
-  }
132
-}
133
-
134 67
 export default new History()

+ 8
- 2
state/sessions/arrow-session.ts View File

@@ -3,7 +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 { getBoundsFromPoints, getPage, updateParents } from 'utils/utils'
6
+import {
7
+  getBoundsFromPoints,
8
+  getPage,
9
+  getSelectedIds,
10
+  setToArray,
11
+  updateParents,
12
+} from 'utils/utils'
7 13
 import { getShapeUtils } from 'lib/shape-utils'
8 14
 
9 15
 export default class ArrowSession extends BaseSession {
@@ -116,7 +122,7 @@ export function getArrowSnapshot(data: Data, id: string) {
116 122
   return {
117 123
     id,
118 124
     initialShape,
119
-    selectedIds: new Set(data.selectedIds),
125
+    selectedIds: setToArray(getSelectedIds(data)),
120 126
     currentPageId: data.currentPageId,
121 127
   }
122 128
 }

+ 16
- 7
state/sessions/brush-session.ts View File

@@ -2,7 +2,14 @@ import { current } from 'immer'
2 2
 import { Bounds, Data, ShapeType } from 'types'
3 3
 import BaseSession from './base-session'
4 4
 import { getShapeUtils } from 'lib/shape-utils'
5
-import { getBoundsFromPoints, getPage, getShapes } from 'utils/utils'
5
+import {
6
+  getBoundsFromPoints,
7
+  getPage,
8
+  getSelectedIds,
9
+  getShapes,
10
+  setSelectedIds,
11
+  setToArray,
12
+} from 'utils/utils'
6 13
 import * as vec from 'utils/vec'
7 14
 import state from 'state/state'
8 15
 
@@ -25,6 +32,8 @@ export default class BrushSession extends BaseSession {
25 32
 
26 33
     const hits = new Set<string>([])
27 34
 
35
+    const selectedIds = getSelectedIds(data)
36
+
28 37
     for (let id in snapshot.shapeHitTests) {
29 38
       const { test, selectId } = snapshot.shapeHitTests[id]
30 39
       if (!hits.has(selectId)) {
@@ -32,11 +41,11 @@ export default class BrushSession extends BaseSession {
32 41
           hits.add(selectId)
33 42
 
34 43
           // When brushing a shape, select its top group parent.
35
-          if (!data.selectedIds.has(selectId)) {
36
-            data.selectedIds.add(selectId)
44
+          if (!selectedIds.has(selectId)) {
45
+            selectedIds.add(selectId)
37 46
           }
38
-        } else if (data.selectedIds.has(selectId)) {
39
-          data.selectedIds.delete(selectId)
47
+        } else if (selectedIds.has(selectId)) {
48
+          selectedIds.delete(selectId)
40 49
         }
41 50
       }
42 51
     }
@@ -46,7 +55,7 @@ export default class BrushSession extends BaseSession {
46 55
 
47 56
   cancel = (data: Data) => {
48 57
     data.brush = undefined
49
-    data.selectedIds = new Set(this.snapshot.selectedIds)
58
+    setSelectedIds(data, this.snapshot.selectedIds)
50 59
   }
51 60
 
52 61
   complete = (data: Data) => {
@@ -61,7 +70,7 @@ export default class BrushSession extends BaseSession {
61 70
  */
62 71
 export function getBrushSnapshot(data: Data) {
63 72
   return {
64
-    selectedIds: new Set(data.selectedIds),
73
+    selectedIds: setToArray(getSelectedIds(data)),
65 74
     shapeHitTests: Object.fromEntries(
66 75
       getShapes(state.data)
67 76
         .filter((shape) => shape.type !== ShapeType.Group)

+ 8
- 8
state/sessions/direction-session.ts View File

@@ -1,9 +1,9 @@
1
-import { Data, LineShape, RayShape } from "types"
2
-import * as vec from "utils/vec"
3
-import BaseSession from "./base-session"
4
-import commands from "state/commands"
5
-import { current } from "immer"
6
-import { getPage } from "utils/utils"
1
+import { Data, LineShape, RayShape } from 'types'
2
+import * as vec from 'utils/vec'
3
+import BaseSession from './base-session'
4
+import commands from 'state/commands'
5
+import { current } from 'immer'
6
+import { getPage, getSelectedIds } from 'utils/utils'
7 7
 
8 8
 export default class DirectionSession extends BaseSession {
9 9
   delta = [0, 0]
@@ -47,9 +47,9 @@ export function getDirectionSnapshot(data: Data) {
47 47
 
48 48
   let snapshapes: { id: string; direction: number[] }[] = []
49 49
 
50
-  data.selectedIds.forEach((id) => {
50
+  getSelectedIds(data).forEach((id) => {
51 51
     const shape = shapes[id]
52
-    if ("direction" in shape) {
52
+    if ('direction' in shape) {
53 53
       snapshapes.push({ id: shape.id, direction: shape.direction })
54 54
     }
55 55
   })

+ 5
- 24
state/sessions/draw-session.ts View File

@@ -95,31 +95,12 @@ export default class BrushSession extends BaseSession {
95 95
   }
96 96
 
97 97
   complete = (data: Data) => {
98
-    if (this.points.length > 1) {
99
-      let minX = Infinity
100
-      let minY = Infinity
101
-      const pts = [...this.points]
102
-
103
-      for (let pt of pts) {
104
-        minX = Math.min(pt[0], minX)
105
-        minY = Math.min(pt[1], minY)
106
-      }
107
-
108
-      for (let pt of pts) {
109
-        pt[0] -= minX
110
-        pt[1] -= minY
111
-      }
112
-
113
-      const { snapshot } = this
114
-      const page = getPage(data)
115
-      const shape = page.shapes[snapshot.id] as DrawShape
116
-
117
-      getShapeUtils(shape)
118
-        .setProperty(shape, 'points', pts)
119
-        .setProperty(shape, 'point', vec.add(shape.point, [minX, minY]))
120
-    }
98
+    const { snapshot } = this
99
+    const page = getPage(data)
100
+    const shape = page.shapes[snapshot.id] as DrawShape
121 101
 
122
-    commands.draw(data, this.snapshot.id, this.points)
102
+    getShapeUtils(shape).onSessionComplete(shape)
103
+    commands.draw(data, this.snapshot.id)
123 104
   }
124 105
 }
125 106
 

+ 3
- 1
state/sessions/rotate-session.ts View File

@@ -13,6 +13,8 @@ import {
13 13
   getShapeBounds,
14 14
   updateParents,
15 15
   getDocumentBranch,
16
+  setToArray,
17
+  getSelectedIds,
16 18
 } from 'utils/utils'
17 19
 import { getShapeUtils } from 'lib/shape-utils'
18 20
 
@@ -101,7 +103,7 @@ export function getRotateSnapshot(data: Data) {
101 103
   const cData = current(data)
102 104
   const page = getPage(cData)
103 105
 
104
-  const initialShapes = Array.from(cData.selectedIds.values())
106
+  const initialShapes = setToArray(getSelectedIds(data))
105 107
     .flatMap((id) => getDocumentBranch(cData, id).map((id) => page.shapes[id]))
106 108
     .filter((shape) => !shape.isLocked)
107 109
 

+ 3
- 1
state/sessions/transform-session.ts View File

@@ -11,9 +11,11 @@ import {
11 11
   getDocumentBranch,
12 12
   getPage,
13 13
   getRelativeTransformedBoundingBox,
14
+  getSelectedIds,
14 15
   getSelectedShapes,
15 16
   getShapes,
16 17
   getTransformedBoundingBox,
18
+  setToArray,
17 19
   updateParents,
18 20
 } from 'utils/utils'
19 21
 
@@ -118,7 +120,7 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
118 120
   const { currentPageId } = cData
119 121
   const page = getPage(cData)
120 122
 
121
-  const initialShapes = Array.from(cData.selectedIds.values())
123
+  const initialShapes = setToArray(getSelectedIds(data))
122 124
     .flatMap((id) => getDocumentBranch(cData, id).map((id) => page.shapes[id]))
123 125
     .filter((shape) => !shape.isLocked)
124 126
 

+ 10
- 8
state/sessions/translate-session.ts View File

@@ -9,6 +9,7 @@ import {
9 9
   getDocumentBranch,
10 10
   getPage,
11 11
   getSelectedShapes,
12
+  setSelectedIds,
12 13
   updateParents,
13 14
 } from 'utils/utils'
14 15
 import { getShapeUtils } from 'lib/shape-utils'
@@ -47,17 +48,13 @@ export default class TranslateSession extends BaseSession {
47 48
     if (isCloning) {
48 49
       if (!this.isCloning) {
49 50
         this.isCloning = true
50
-        data.selectedIds.clear()
51 51
 
52 52
         for (const { id, point } of initialShapes) {
53 53
           const shape = shapes[id]
54 54
           getShapeUtils(shape).translateTo(shape, point)
55 55
         }
56 56
 
57
-        data.selectedIds.clear()
58
-
59 57
         for (const clone of clones) {
60
-          data.selectedIds.add(clone.id)
61 58
           shapes[clone.id] = { ...clone }
62 59
           const parent = shapes[clone.parentId]
63 60
           if (!parent) continue
@@ -66,6 +63,11 @@ export default class TranslateSession extends BaseSession {
66 63
             clone.id,
67 64
           ])
68 65
         }
66
+
67
+        setSelectedIds(
68
+          data,
69
+          clones.map((c) => c.id)
70
+        )
69 71
       }
70 72
 
71 73
       for (const { id, point } of clones) {
@@ -80,11 +82,11 @@ export default class TranslateSession extends BaseSession {
80 82
     } else {
81 83
       if (this.isCloning) {
82 84
         this.isCloning = false
83
-        data.selectedIds.clear()
84 85
 
85
-        for (const { id } of initialShapes) {
86
-          data.selectedIds.add(id)
87
-        }
86
+        setSelectedIds(
87
+          data,
88
+          initialShapes.map((c) => c.id)
89
+        )
88 90
 
89 91
         for (const clone of clones) {
90 92
           delete shapes[clone.id]

+ 42
- 29
state/state.ts View File

@@ -1,12 +1,13 @@
1 1
 import { createSelectorHook, createState } from '@state-designer/react'
2
+import { updateFromCode } from 'lib/code/generate'
3
+import { createShape, getShapeUtils } from 'lib/shape-utils'
2 4
 import * as vec from 'utils/vec'
3 5
 import inputs from './inputs'
4 6
 import { defaultDocument } from './data'
5
-import { createShape, getShapeUtils } from 'lib/shape-utils'
6
-import history from 'state/history'
7
+import history from './history'
8
+import storage from './storage'
7 9
 import * as Sessions from './sessions'
8 10
 import commands from './commands'
9
-import { updateFromCode } from 'lib/code/generate'
10 11
 import {
11 12
   clamp,
12 13
   getChildren,
@@ -26,6 +27,8 @@ import {
26 27
   getBoundsCenter,
27 28
   getDocumentBranch,
28 29
   getCameraZoom,
30
+  getSelectedIds,
31
+  setSelectedIds,
29 32
 } from 'utils/utils'
30 33
 import {
31 34
   Data,
@@ -69,7 +72,6 @@ const initialData: Data = {
69 72
   boundsRotation: 0,
70 73
   pointedId: null,
71 74
   hoveredId: null,
72
-  selectedIds: new Set([]),
73 75
   currentPageId: 'page1',
74 76
   currentParentId: 'page1',
75 77
   currentCodeFileId: 'file0',
@@ -77,12 +79,14 @@ const initialData: Data = {
77 79
   document: defaultDocument,
78 80
   pageStates: {
79 81
     page1: {
82
+      selectedIds: new Set([]),
80 83
       camera: {
81 84
         point: [0, 0],
82 85
         zoom: 1,
83 86
       },
84 87
     },
85 88
     page2: {
89
+      selectedIds: new Set([]),
86 90
       camera: {
87 91
         point: [0, 0],
88 92
         zoom: 1,
@@ -164,7 +168,7 @@ const state = createState({
164 168
           do: 'deleteSelection',
165 169
           else: ['selectAll', 'deleteSelection'],
166 170
         },
167
-        CHANGED_CURRENT_PAGE: ['clearSelectedIds', 'setCurrentPage'],
171
+        CHANGED_PAGE: 'changePage',
168 172
         CREATED_PAGE: ['clearSelectedIds', 'createPage'],
169 173
         DELETED_PAGE: { unless: 'hasOnlyOnePage', do: 'deletePage' },
170 174
       },
@@ -743,7 +747,7 @@ const state = createState({
743 747
       return vec.dist2(payload.origin, payload.point) > 8
744 748
     },
745 749
     isPointedShapeSelected(data) {
746
-      return data.selectedIds.has(data.pointedId)
750
+      return getSelectedIds(data).has(data.pointedId)
747 751
     },
748 752
     isPressingShiftKey(data, payload: PointerInfo) {
749 753
       return payload.shiftKey
@@ -769,10 +773,10 @@ const state = createState({
769 773
       return payload.target === 'rotate'
770 774
     },
771 775
     hasSelection(data) {
772
-      return data.selectedIds.size > 0
776
+      return getSelectedIds(data).size > 0
773 777
     },
774 778
     hasMultipleSelection(data) {
775
-      return data.selectedIds.size > 1
779
+      return getSelectedIds(data).size > 1
776 780
     },
777 781
     isToolLocked(data) {
778 782
       return data.settings.isToolLocked
@@ -791,7 +795,7 @@ const state = createState({
791 795
   },
792 796
   actions: {
793 797
     /* ---------------------- Pages --------------------- */
794
-    setCurrentPage(data, payload: { id: string }) {
798
+    changePage(data, payload: { id: string }) {
795 799
       commands.changePage(data, payload.id)
796 800
     },
797 801
     createPage(data) {
@@ -818,8 +822,7 @@ const state = createState({
818 822
 
819 823
       getPage(data).shapes[shape.id] = shape
820 824
 
821
-      data.selectedIds.clear()
822
-      data.selectedIds.add(shape.id)
825
+      setSelectedIds(data, [shape.id])
823 826
     },
824 827
     /* -------------------- Sessions -------------------- */
825 828
 
@@ -902,7 +905,7 @@ const state = createState({
902 905
 
903 906
     // Dragging Handle
904 907
     startHandleSession(data, payload: PointerInfo) {
905
-      const shapeId = Array.from(data.selectedIds.values())[0]
908
+      const shapeId = Array.from(getSelectedIds(data).values())[0]
906 909
       const handleId = payload.target
907 910
 
908 911
       session.current = new Sessions.HandleSession(
@@ -939,7 +942,7 @@ const state = createState({
939 942
     ) {
940 943
       const point = screenToWorld(inputs.pointer.origin, data)
941 944
       session.current =
942
-        data.selectedIds.size === 1
945
+        getSelectedIds(data).size === 1
943 946
           ? new Sessions.TransformSingleSession(data, payload.target, point)
944 947
           : new Sessions.TransformSession(data, payload.target, point)
945 948
     },
@@ -981,7 +984,7 @@ const state = createState({
981 984
 
982 985
     // Drawing
983 986
     startDrawSession(data, payload: PointerInfo) {
984
-      const id = Array.from(data.selectedIds.values())[0]
987
+      const id = Array.from(getSelectedIds(data).values())[0]
985 988
       session.current = new Sessions.DrawSession(
986 989
         data,
987 990
         id,
@@ -1008,7 +1011,7 @@ const state = createState({
1008 1011
 
1009 1012
     // Arrow
1010 1013
     startArrowSession(data, payload: PointerInfo) {
1011
-      const id = Array.from(data.selectedIds.values())[0]
1014
+      const id = Array.from(getSelectedIds(data).values())[0]
1012 1015
       session.current = new Sessions.ArrowSession(
1013 1016
         data,
1014 1017
         id,
@@ -1047,7 +1050,7 @@ const state = createState({
1047 1050
     /* -------------------- Selection ------------------- */
1048 1051
 
1049 1052
     selectAll(data) {
1050
-      const { selectedIds } = data
1053
+      const selectedIds = getSelectedIds(data)
1051 1054
       const page = getPage(data)
1052 1055
       selectedIds.clear()
1053 1056
       for (let id in page.shapes) {
@@ -1078,14 +1081,15 @@ const state = createState({
1078 1081
       data.pointedId = undefined
1079 1082
     },
1080 1083
     clearSelectedIds(data) {
1081
-      data.selectedIds.clear()
1084
+      setSelectedIds(data, [])
1082 1085
     },
1083 1086
     pullPointedIdFromSelectedIds(data) {
1084
-      const { selectedIds, pointedId } = data
1087
+      const { pointedId } = data
1088
+      const selectedIds = getSelectedIds(data)
1085 1089
       selectedIds.delete(pointedId)
1086 1090
     },
1087 1091
     pushPointedIdToSelectedIds(data) {
1088
-      data.selectedIds.add(data.pointedId)
1092
+      getSelectedIds(data).add(data.pointedId)
1089 1093
     },
1090 1094
     moveSelection(data, payload: { type: MoveType }) {
1091 1095
       commands.move(data, payload.type)
@@ -1311,9 +1315,6 @@ const state = createState({
1311 1315
     popHistory() {
1312 1316
       history.pop()
1313 1317
     },
1314
-    forceSave(data) {
1315
-      history.save(data)
1316
-    },
1317 1318
     enableHistory() {
1318 1319
       history.enable()
1319 1320
     },
@@ -1377,7 +1378,7 @@ const state = createState({
1377 1378
 
1378 1379
       history.disable()
1379 1380
 
1380
-      data.selectedIds.clear()
1381
+      setSelectedIds(data, [])
1381 1382
 
1382 1383
       try {
1383 1384
         const { shapes } = updateFromCode(
@@ -1407,13 +1408,25 @@ const state = createState({
1407 1408
 
1408 1409
     /* ---------------------- Data ---------------------- */
1409 1410
 
1411
+    forceSave(data) {
1412
+      storage.save(data)
1413
+    },
1414
+
1415
+    savePage(data) {
1416
+      storage.savePage(data, data.currentPageId)
1417
+    },
1418
+
1419
+    loadPage(data) {
1420
+      storage.loadPage(data, data.currentPageId)
1421
+    },
1422
+
1410 1423
     saveCode(data, payload: { code: string }) {
1411 1424
       data.document.code[data.currentCodeFileId].code = payload.code
1412
-      history.save(data)
1425
+      storage.save(data)
1413 1426
     },
1414 1427
 
1415 1428
     restoreSavedData(data) {
1416
-      history.load(data)
1429
+      storage.load(data)
1417 1430
     },
1418 1431
 
1419 1432
     clearBoundsRotation(data) {
@@ -1422,10 +1435,10 @@ const state = createState({
1422 1435
   },
1423 1436
   values: {
1424 1437
     selectedIds(data) {
1425
-      return new Set(data.selectedIds)
1438
+      return new Set(getSelectedIds(data))
1426 1439
     },
1427 1440
     selectedBounds(data) {
1428
-      const { selectedIds } = data
1441
+      const selectedIds = getSelectedIds(data)
1429 1442
 
1430 1443
       const page = getPage(data)
1431 1444
 
@@ -1438,7 +1451,7 @@ const state = createState({
1438 1451
       if (selectedIds.size === 1) {
1439 1452
         if (!shapes[0]) {
1440 1453
           console.error('Could not find that shape! Clearing selected IDs.')
1441
-          data.selectedIds.clear()
1454
+          setSelectedIds(data, [])
1442 1455
           return null
1443 1456
         }
1444 1457
 
@@ -1497,7 +1510,7 @@ const state = createState({
1497 1510
       return commonBounds
1498 1511
     },
1499 1512
     selectedStyle(data) {
1500
-      const selectedIds = Array.from(data.selectedIds.values())
1513
+      const selectedIds = Array.from(getSelectedIds(data).values())
1501 1514
       const { currentStyle } = data
1502 1515
 
1503 1516
       if (selectedIds.length === 0) {

+ 139
- 0
state/storage.ts View File

@@ -0,0 +1,139 @@
1
+import { Data, Page, PageState } from 'types'
2
+import { setToArray } from 'utils/utils'
3
+
4
+const CURRENT_VERSION = 'code_slate_0.0.4'
5
+const DOCUMENT_ID = '0001'
6
+
7
+function storageId(label: string, id: string) {
8
+  return `${CURRENT_VERSION}_doc_${DOCUMENT_ID}_${label}_${id}`
9
+}
10
+
11
+class Storage {
12
+  // Saving
13
+  load(data: Data, id = CURRENT_VERSION) {
14
+    if (typeof window === 'undefined') return
15
+    if (typeof localStorage === 'undefined') return
16
+
17
+    // Load data from local storage
18
+    const savedData = localStorage.getItem(id)
19
+
20
+    if (savedData !== null) {
21
+      const restoredData = JSON.parse(savedData)
22
+
23
+      // Empty shapes in state for each page
24
+      for (let key in restoredData.document.pages) {
25
+        restoredData.document.pages[key].shapes = {}
26
+      }
27
+
28
+      // Empty page states for each page
29
+      for (let key in restoredData.pageStates) {
30
+        restoredData.document.pages[key].shapes = {}
31
+      }
32
+
33
+      // Merge restored data into state
34
+      Object.assign(data, restoredData)
35
+
36
+      // Load current page
37
+      this.loadPage(data, data.currentPageId)
38
+    }
39
+  }
40
+
41
+  save = (data: Data, id = CURRENT_VERSION) => {
42
+    if (typeof window === 'undefined') return
43
+    if (typeof localStorage === 'undefined') return
44
+
45
+    const dataToSave: any = { ...data }
46
+
47
+    // Don't save pageStates
48
+    dataToSave.pageStates = {}
49
+
50
+    // Save current data to local storage
51
+    localStorage.setItem(id, JSON.stringify(dataToSave))
52
+
53
+    // Save current page
54
+    this.savePage(data, data.currentPageId)
55
+  }
56
+
57
+  savePage(data: Data, pageId: string) {
58
+    if (typeof window === 'undefined') return
59
+    if (typeof localStorage === 'undefined') return
60
+
61
+    // Save page
62
+    const page = data.document.pages[pageId]
63
+
64
+    localStorage.setItem(storageId('page', pageId), JSON.stringify(page))
65
+
66
+    // Save page state
67
+
68
+    let currentPageState = {
69
+      camera: {
70
+        point: [0, 0],
71
+        zoom: 1,
72
+      },
73
+      selectedIds: new Set([]),
74
+      ...data.pageStates[pageId],
75
+    }
76
+
77
+    const pageState = {
78
+      ...currentPageState,
79
+      selectedIds: setToArray(currentPageState.selectedIds),
80
+    }
81
+
82
+    localStorage.setItem(
83
+      storageId('pageState', pageId),
84
+      JSON.stringify(pageState)
85
+    )
86
+  }
87
+
88
+  loadPage(data: Data, pageId: string) {
89
+    if (typeof window === 'undefined') return
90
+    if (typeof localStorage === 'undefined') return
91
+
92
+    // Load page and merge into state
93
+    const savedPage = localStorage.getItem(storageId('page', pageId))
94
+
95
+    if (savedPage !== null) {
96
+      const restored: Page = JSON.parse(savedPage)
97
+      data.document.pages[pageId] = restored
98
+    }
99
+
100
+    // Load page state and merge into state
101
+    const savedPageState = localStorage.getItem(storageId('pageState', pageId))
102
+
103
+    if (savedPageState !== null) {
104
+      const restored: PageState = JSON.parse(savedPageState)
105
+      restored.selectedIds = new Set(restored.selectedIds)
106
+      data.pageStates[pageId] = restored
107
+    } else {
108
+      data.pageStates[pageId] = {
109
+        camera: {
110
+          point: [0, 0],
111
+          zoom: 1,
112
+        },
113
+        selectedIds: new Set([]),
114
+      }
115
+    }
116
+
117
+    // Empty shapes in state for other pages
118
+    for (let key in data.document.pages) {
119
+      if (key === pageId) continue
120
+      data.document.pages[key].shapes = {}
121
+    }
122
+
123
+    // Empty page states for other pages
124
+    for (let key in data.pageStates) {
125
+      if (key === pageId) continue
126
+      data.document.pages[key].shapes = {}
127
+    }
128
+
129
+    // Update camera
130
+    document.documentElement.style.setProperty(
131
+      '--camera-zoom',
132
+      data.pageStates[data.currentPageId].camera.zoom.toString()
133
+    )
134
+  }
135
+}
136
+
137
+const storage = new Storage()
138
+
139
+export default storage

+ 6
- 0
todo.md View File

@@ -19,3 +19,9 @@
19 19
 
20 20
 - shift dragging arrow handles should lock to directions
21 21
 - fix undo/redo on rotated arrows
22
+- fix shift-rotation
23
+
24
+## Pages
25
+
26
+- [x] Make selection part of page state
27
+- [ ] Allow only one page to be in the document at a time

+ 1
- 1
types.ts View File

@@ -22,7 +22,6 @@ export interface Data {
22 22
   activeTool: ShapeType | 'select'
23 23
   brush?: Bounds
24 24
   boundsRotation: number
25
-  selectedIds: Set<string>
26 25
   pointedId?: string
27 26
   hoveredId?: string
28 27
   currentPageId: string
@@ -49,6 +48,7 @@ export interface Page {
49 48
 }
50 49
 
51 50
 export interface PageState {
51
+  selectedIds: Set<string>
52 52
   camera: {
53 53
     point: number[]
54 54
     zoom: number

+ 14
- 1
utils/utils.ts View File

@@ -1381,7 +1381,7 @@ export function getShapes(data: Data, pageId = data.currentPageId) {
1381 1381
 
1382 1382
 export function getSelectedShapes(data: Data, pageId = data.currentPageId) {
1383 1383
   const page = getPage(data, pageId)
1384
-  const ids = Array.from(data.selectedIds.values())
1384
+  const ids = setToArray(getSelectedIds(data))
1385 1385
   return ids.map((id) => page.shapes[id])
1386 1386
 }
1387 1387
 
@@ -1664,3 +1664,16 @@ export function getDocumentBranch(data: Data, id: string): string[] {
1664 1664
     ...shape.children.flatMap((childId) => getDocumentBranch(data, childId)),
1665 1665
   ]
1666 1666
 }
1667
+
1668
+export function getSelectedIds(data: Data) {
1669
+  return data.pageStates[data.currentPageId].selectedIds
1670
+}
1671
+
1672
+export function setSelectedIds(data: Data, ids: string[]) {
1673
+  data.pageStates[data.currentPageId].selectedIds = new Set(ids)
1674
+  return data.pageStates[data.currentPageId].selectedIds
1675
+}
1676
+
1677
+export function setToArray<T>(set: Set<T>): T[] {
1678
+  return Array.from(set.values())
1679
+}

Loading…
Cancel
Save