浏览代码

Adds context menu, "move to page"

main
Steve Ruiz 4 年前
父节点
当前提交
f2d3231315

+ 1
- 1
components/canvas/bounds/bounding-box.tsx 查看文件

51
 
51
 
52
   if (isSingleHandles) return null
52
   if (isSingleHandles) return null
53
 
53
 
54
-  const size = (isMobile().any ? 10 : 8) / zoom // Touch target size
54
+  const size = (isMobile() ? 10 : 8) / zoom // Touch target size
55
   const center = getBoundsCenter(bounds)
55
   const center = getBoundsCenter(bounds)
56
 
56
 
57
   return (
57
   return (

+ 7
- 4
components/canvas/bounds/bounds-bg.tsx 查看文件

5
 import { deepCompareArrays, getPage } from 'utils/utils'
5
 import { deepCompareArrays, getPage } from 'utils/utils'
6
 
6
 
7
 function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
7
 function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
8
-  if (e.buttons !== 1) return
9
   if (!inputs.canAccept(e.pointerId)) return
8
   if (!inputs.canAccept(e.pointerId)) return
10
   e.stopPropagation()
9
   e.stopPropagation()
11
   e.currentTarget.setPointerCapture(e.pointerId)
10
   e.currentTarget.setPointerCapture(e.pointerId)
12
-  state.send('POINTED_BOUNDS', inputs.pointerDown(e, 'bounds'))
11
+
12
+  if (e.button === 0) {
13
+    state.send('POINTED_BOUNDS', inputs.pointerDown(e, 'bounds'))
14
+  } else if (e.button === 2) {
15
+    state.send('RIGHT_POINTED', inputs.pointerDown(e, 'bounds'))
16
+  }
13
 }
17
 }
14
 
18
 
15
 function handlePointerUp(e: React.PointerEvent<SVGRectElement>) {
19
 function handlePointerUp(e: React.PointerEvent<SVGRectElement>) {
16
-  if (e.buttons !== 1) return
17
   if (!inputs.canAccept(e.pointerId)) return
20
   if (!inputs.canAccept(e.pointerId)) return
18
   e.stopPropagation()
21
   e.stopPropagation()
19
   e.currentTarget.releasePointerCapture(e.pointerId)
22
   e.currentTarget.releasePointerCapture(e.pointerId)
36
     if (selectedIds.length === 1) {
39
     if (selectedIds.length === 1) {
37
       const { shapes } = getPage(s.data)
40
       const { shapes } = getPage(s.data)
38
       const selected = Array.from(s.values.selectedIds.values())[0]
41
       const selected = Array.from(s.values.selectedIds.values())[0]
39
-      return shapes[selected].rotation
42
+      return shapes[selected]?.rotation
40
     } else {
43
     } else {
41
       return 0
44
       return 0
42
     }
45
     }

+ 3
- 1
components/canvas/bounds/handles.tsx 查看文件

21
     s.isInAny('notPointing', 'pinching', 'translatingHandles')
21
     s.isInAny('notPointing', 'pinching', 'translatingHandles')
22
   )
22
   )
23
 
23
 
24
-  if (!shape.handles || !isSelecting) return null
24
+  if (!shape || !shape.handles || !isSelecting) return null
25
 
25
 
26
   const center = getShapeUtils(shape).getCenter(shape)
26
   const center = getShapeUtils(shape).getCenter(shape)
27
 
27
 
28
+  console.log(shape)
29
+
28
   return (
30
   return (
29
     <g transform={`rotate(${shape.rotation * (180 / Math.PI)},${center})`}>
31
     <g transform={`rotate(${shape.rotation * (180 / Math.PI)},${center})`}>
30
       {Object.values(shape.handles).map((handle) => (
32
       {Object.values(shape.handles).map((handle) => (

+ 17
- 15
components/canvas/canvas.tsx 查看文件

1
 import styled from 'styles'
1
 import styled from 'styles'
2
 import state, { useSelector } from 'state'
2
 import state, { useSelector } from 'state'
3
-import inputs from 'state/inputs'
4
-import React, { useCallback, useRef } from 'react'
3
+import React, { useRef } from 'react'
5
 import useZoomEvents from 'hooks/useZoomEvents'
4
 import useZoomEvents from 'hooks/useZoomEvents'
6
 import useCamera from 'hooks/useCamera'
5
 import useCamera from 'hooks/useCamera'
7
 import Defs from './defs'
6
 import Defs from './defs'
12
 import Selected from './selected'
11
 import Selected from './selected'
13
 import Handles from './bounds/handles'
12
 import Handles from './bounds/handles'
14
 import useCanvasEvents from 'hooks/useCanvasEvents'
13
 import useCanvasEvents from 'hooks/useCanvasEvents'
14
+import ContextMenu from 'components/context-menu'
15
 
15
 
16
 export default function Canvas() {
16
 export default function Canvas() {
17
   const rCanvas = useRef<SVGSVGElement>(null)
17
   const rCanvas = useRef<SVGSVGElement>(null)
26
   const isReady = useSelector((s) => s.isIn('ready'))
26
   const isReady = useSelector((s) => s.isIn('ready'))
27
 
27
 
28
   return (
28
   return (
29
-    <MainSVG ref={rCanvas} {...events}>
30
-      <Defs />
31
-      {isReady && (
32
-        <g ref={rGroup}>
33
-          <BoundsBg />
34
-          <Page />
35
-          <Selected />
36
-          <Bounds />
37
-          <Handles />
38
-          <Brush />
39
-        </g>
40
-      )}
41
-    </MainSVG>
29
+    <ContextMenu>
30
+      <MainSVG ref={rCanvas} {...events}>
31
+        <Defs />
32
+        {isReady && (
33
+          <g ref={rGroup}>
34
+            <BoundsBg />
35
+            <Page />
36
+            <Selected />
37
+            <Bounds />
38
+            <Handles />
39
+            <Brush />
40
+          </g>
41
+        )}
42
+      </MainSVG>
43
+    </ContextMenu>
42
   )
44
   )
43
 }
45
 }
44
 
46
 

+ 2
- 0
components/canvas/shape.tsx 查看文件

7
 import useShapeEvents from 'hooks/useShapeEvents'
7
 import useShapeEvents from 'hooks/useShapeEvents'
8
 import * as vec from 'utils/vec'
8
 import * as vec from 'utils/vec'
9
 import { getShapeStyle } from 'lib/shape-styles'
9
 import { getShapeStyle } from 'lib/shape-styles'
10
+import ContextMenu from 'components/context-menu'
10
 
11
 
11
 interface ShapeProps {
12
 interface ShapeProps {
12
   id: string
13
   id: string
51
           {...events}
52
           {...events}
52
         />
53
         />
53
       )}
54
       )}
55
+
54
       {!shape.isHidden && <RealShape isGroup={isGroup} id={id} style={style} />}
56
       {!shape.isHidden && <RealShape isGroup={isGroup} id={id} style={style} />}
55
       {isGroup &&
57
       {isGroup &&
56
         shape.children.map((shapeId) => (
58
         shape.children.map((shapeId) => (

+ 297
- 0
components/context-menu.tsx 查看文件

1
+import * as _ContextMenu from '@radix-ui/react-context-menu'
2
+import * as _Dropdown from '@radix-ui/react-dropdown-menu'
3
+import styled from 'styles'
4
+import { RowButton } from './shared'
5
+import {
6
+  commandKey,
7
+  deepCompareArrays,
8
+  getSelectedShapes,
9
+  isMobile,
10
+} from 'utils/utils'
11
+import state, { useSelector } from 'state'
12
+import { MoveType, ShapeType } from 'types'
13
+import React, { useRef } from 'react'
14
+
15
+export default function ContextMenu({
16
+  children,
17
+}: {
18
+  children: React.ReactNode
19
+}) {
20
+  const selectedShapes = useSelector(
21
+    (s) => getSelectedShapes(s.data),
22
+    deepCompareArrays
23
+  )
24
+
25
+  const rContent = useRef<HTMLDivElement>(null)
26
+
27
+  const hasGroupSelectd = selectedShapes.some((s) => s.type === ShapeType.Group)
28
+  const hasMultipleSelected = selectedShapes.length > 1
29
+
30
+  return (
31
+    <_ContextMenu.Root>
32
+      <_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
33
+      <StyledContent ref={rContent} isMobile={isMobile()}>
34
+        {selectedShapes.length ? (
35
+          <>
36
+            {/* <Button onSelect={() => state.send('COPIED')}>
37
+              <span>Copy</span>
38
+              <kbd>
39
+                <span>{commandKey()}</span>
40
+                <span>C</span>
41
+              </kbd>
42
+            </Button>
43
+            <Button onSelect={() => state.send('CUT')}>
44
+              <span>Cut</span>
45
+              <kbd>
46
+                <span>{commandKey()}</span>
47
+                <span>X</span>
48
+              </kbd>
49
+            </Button>
50
+             */}
51
+            <Button onSelect={() => state.send('DUPLICATED')}>
52
+              <span>Duplicate</span>
53
+              <kbd>
54
+                <span>{commandKey()}</span>
55
+                <span>D</span>
56
+              </kbd>
57
+            </Button>
58
+            <StyledDivider />
59
+            <Button
60
+              onSelect={() =>
61
+                state.send('MOVED', {
62
+                  type: MoveType.ToFront,
63
+                })
64
+              }
65
+            >
66
+              <span>Move To Front</span>
67
+              <kbd>
68
+                <span>{commandKey()}</span>
69
+                <span>⇧</span>
70
+                <span>]</span>
71
+              </kbd>
72
+            </Button>
73
+
74
+            <Button
75
+              onSelect={() =>
76
+                state.send('MOVED', {
77
+                  type: MoveType.Forward,
78
+                })
79
+              }
80
+            >
81
+              <span>Move Forward</span>
82
+              <kbd>
83
+                <span>{commandKey()}</span>
84
+                <span>]</span>
85
+              </kbd>
86
+            </Button>
87
+            <Button
88
+              onSelect={() =>
89
+                state.send('MOVED', {
90
+                  type: MoveType.Backward,
91
+                })
92
+              }
93
+            >
94
+              <span>Move Backward</span>
95
+              <kbd>
96
+                <span>{commandKey()}</span>
97
+                <span>[</span>
98
+              </kbd>
99
+            </Button>
100
+            <Button
101
+              onSelect={() =>
102
+                state.send('MOVED', {
103
+                  type: MoveType.ToBack,
104
+                })
105
+              }
106
+            >
107
+              <span>Move to Back</span>
108
+              <kbd>
109
+                <span>{commandKey()}</span>
110
+                <span>⇧</span>
111
+                <span>[</span>
112
+              </kbd>
113
+            </Button>
114
+            {hasGroupSelectd ||
115
+              (hasMultipleSelected && (
116
+                <>
117
+                  <StyledDivider />
118
+                  {hasGroupSelectd && (
119
+                    <Button onSelect={() => state.send('UNGROUPED')}>
120
+                      <span>Ungroup</span>
121
+                      <kbd>
122
+                        <span>{commandKey()}</span>
123
+                        <span>⇧</span>
124
+                        <span>G</span>
125
+                      </kbd>
126
+                    </Button>
127
+                  )}
128
+                  {hasMultipleSelected && (
129
+                    <Button onSelect={() => state.send('GROUPED')}>
130
+                      <span>Group</span>
131
+                      <kbd>
132
+                        <span>{commandKey()}</span>
133
+                        <span>G</span>
134
+                      </kbd>
135
+                    </Button>
136
+                  )}
137
+                </>
138
+              ))}
139
+            <StyledDivider />
140
+
141
+            {/* <Button onSelect={() => state.send('MOVED_TO_PAGE')}> */}
142
+            <_ContextMenu.Item>
143
+              <MoveToPageDropDown>Move to Page</MoveToPageDropDown>
144
+            </_ContextMenu.Item>
145
+            {/* </Button> */}
146
+            <Button onSelect={() => state.send('DELETED')}>
147
+              <span>Delete</span>
148
+              <kbd>
149
+                <span>⌫</span>
150
+              </kbd>
151
+            </Button>
152
+          </>
153
+        ) : (
154
+          <>
155
+            <Button onSelect={() => state.send('UNDO')}>
156
+              <span>Undo</span>
157
+              <kbd>
158
+                <span>{commandKey()}</span>
159
+                <span>Z</span>
160
+              </kbd>
161
+            </Button>
162
+            <Button onSelect={() => state.send('REDO')}>
163
+              <span>Redo</span>
164
+              <kbd>
165
+                <span>{commandKey()}</span>
166
+                <span>⇧</span>
167
+                <span>Z</span>
168
+              </kbd>
169
+            </Button>
170
+          </>
171
+        )}
172
+      </StyledContent>
173
+    </_ContextMenu.Root>
174
+  )
175
+}
176
+
177
+const StyledContent = styled(_ContextMenu.Content, {
178
+  position: 'relative',
179
+  backgroundColor: '$panel',
180
+  borderRadius: '4px',
181
+  overflow: 'hidden',
182
+  pointerEvents: 'all',
183
+  userSelect: 'none',
184
+  zIndex: 200,
185
+  padding: 2,
186
+  border: '1px solid $panel',
187
+  boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
188
+  minWidth: 128,
189
+
190
+  '& kbd': {
191
+    marginLeft: '32px',
192
+    fontSize: '$1',
193
+    fontFamily: '$ui',
194
+  },
195
+
196
+  '& kbd > span': {
197
+    display: 'inline-block',
198
+    width: '12px',
199
+  },
200
+
201
+  variants: {
202
+    isMobile: {
203
+      true: {
204
+        '& kbd': {
205
+          display: 'none',
206
+        },
207
+      },
208
+    },
209
+  },
210
+})
211
+
212
+const StyledDivider = styled(_ContextMenu.Separator, {
213
+  backgroundColor: '$hover',
214
+  height: 1,
215
+  margin: '2px -2px',
216
+})
217
+
218
+function Button({
219
+  onSelect,
220
+  children,
221
+  disabled = false,
222
+}: {
223
+  onSelect: () => void
224
+  disabled?: boolean
225
+  children: React.ReactNode
226
+}) {
227
+  return (
228
+    <_ContextMenu.Item
229
+      as={RowButton}
230
+      disabled={disabled}
231
+      bp={{ '@initial': 'mobile', '@sm': 'small' }}
232
+      onSelect={onSelect}
233
+    >
234
+      {children}
235
+    </_ContextMenu.Item>
236
+  )
237
+}
238
+
239
+function MoveToPageDropDown({ children }: { children: React.ReactNode }) {
240
+  const documentPages = useSelector((s) => s.data.document.pages)
241
+  const currentPageId = useSelector((s) => s.data.currentPageId)
242
+
243
+  if (!documentPages[currentPageId]) return null
244
+
245
+  const sorted = Object.values(documentPages)
246
+    .sort((a, b) => a.childIndex - b.childIndex)
247
+    .filter((a) => a.id !== currentPageId)
248
+
249
+  return (
250
+    <_Dropdown.Root>
251
+      <_Dropdown.Trigger
252
+        as={RowButton}
253
+        bp={{ '@initial': 'mobile', '@sm': 'small' }}
254
+      >
255
+        {children}
256
+      </_Dropdown.Trigger>
257
+      <StyledDialogContent side="right" sideOffset={8}>
258
+        {sorted.map(({ id, name }) => (
259
+          <_Dropdown.Item
260
+            as={RowButton}
261
+            key={id}
262
+            bp={{ '@initial': 'mobile', '@sm': 'small' }}
263
+            disabled={id === currentPageId}
264
+            onSelect={() => state.send('MOVED_TO_PAGE', { id })}
265
+          >
266
+            <span>{name}</span>
267
+          </_Dropdown.Item>
268
+        ))}
269
+      </StyledDialogContent>
270
+    </_Dropdown.Root>
271
+  )
272
+}
273
+
274
+const StyledDialogContent = styled(_Dropdown.Content, {
275
+  // position: 'fixed',
276
+  // top: '50%',
277
+  // left: '50%',
278
+  // transform: 'translate(-50%, -50%)',
279
+  // minWidth: 200,
280
+  // maxWidth: 'fit-content',
281
+  // maxHeight: '85vh',
282
+  // marginTop: '-5vh',
283
+  minWidth: 128,
284
+  backgroundColor: '$panel',
285
+  borderRadius: '4px',
286
+  overflow: 'hidden',
287
+  pointerEvents: 'all',
288
+  userSelect: 'none',
289
+  zIndex: 200,
290
+  padding: 2,
291
+  border: '1px solid $panel',
292
+  boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
293
+
294
+  '&:focus': {
295
+    outline: 'none',
296
+  },
297
+})

+ 1
- 5
components/editor.tsx 查看文件

9
 import { useSelector } from 'state'
9
 import { useSelector } from 'state'
10
 import styled from 'styles'
10
 import styled from 'styles'
11
 import PagePanel from './page-panel/page-panel'
11
 import PagePanel from './page-panel/page-panel'
12
+import ContextMenu from './context-menu'
12
 
13
 
13
 export default function Editor() {
14
 export default function Editor() {
14
   useKeyboardEvents()
15
   useKeyboardEvents()
25
       <Spacer />
26
       <Spacer />
26
       <StylePanel />
27
       <StylePanel />
27
       <Canvas />
28
       <Canvas />
28
-
29
-      {/* <LeftPanels>
30
-        <CodePanel />
31
-        {hasControls && <ControlsPanel />}
32
-      </LeftPanels> */}
33
       <ToolsPanel />
29
       <ToolsPanel />
34
       <StatusBar />
30
       <StatusBar />
35
     </Layout>
31
     </Layout>

+ 4
- 0
components/shared.tsx 查看文件

105
     zIndex: 1,
105
     zIndex: 1,
106
   },
106
   },
107
 
107
 
108
+  '& :disabled': {
109
+    opacity: 0.5,
110
+  },
111
+
108
   variants: {
112
   variants: {
109
     bp: {
113
     bp: {
110
       mobile: {},
114
       mobile: {},

+ 7
- 1
hooks/useCanvasEvents.ts 查看文件

9
 ) {
9
 ) {
10
   const handlePointerDown = useCallback((e: React.PointerEvent) => {
10
   const handlePointerDown = useCallback((e: React.PointerEvent) => {
11
     if (!inputs.canAccept(e.pointerId)) return
11
     if (!inputs.canAccept(e.pointerId)) return
12
+
12
     rCanvas.current.setPointerCapture(e.pointerId)
13
     rCanvas.current.setPointerCapture(e.pointerId)
13
-    state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
14
+
15
+    if (e.button === 0) {
16
+      state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
17
+    } else if (e.button === 2) {
18
+      state.send('RIGHT_POINTED', inputs.pointerDown(e, 'canvas'))
19
+    }
14
   }, [])
20
   }, [])
15
 
21
 
16
   const handleTouchStart = useCallback((e: React.TouchEvent) => {
22
   const handleTouchStart = useCallback((e: React.TouchEvent) => {

+ 8
- 3
hooks/useShapeEvents.ts 查看文件

14
       e.stopPropagation()
14
       e.stopPropagation()
15
       rGroup.current.setPointerCapture(e.pointerId)
15
       rGroup.current.setPointerCapture(e.pointerId)
16
 
16
 
17
-      if (inputs.isDoubleClick()) {
18
-        state.send('DOUBLE_POINTED_SHAPE', inputs.pointerDown(e, id))
17
+      if (e.button === 0) {
18
+        if (inputs.isDoubleClick()) {
19
+          state.send('DOUBLE_POINTED_SHAPE', inputs.pointerDown(e, id))
20
+        }
21
+
22
+        state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
23
+      } else {
24
+        state.send('RIGHT_POINTED', inputs.pointerDown(e, id))
19
       }
25
       }
20
-      state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
21
     },
26
     },
22
     [id]
27
     [id]
23
   )
28
   )

+ 7
- 5
state/commands/index.ts 查看文件

2
 import arrow from './arrow'
2
 import arrow from './arrow'
3
 import changePage from './change-page'
3
 import changePage from './change-page'
4
 import createPage from './create-page'
4
 import createPage from './create-page'
5
-import deleteSelected from './delete-selected'
6
 import deletePage from './delete-page'
5
 import deletePage from './delete-page'
6
+import deleteSelected from './delete-selected'
7
 import direct from './direct'
7
 import direct from './direct'
8
 import distribute from './distribute'
8
 import distribute from './distribute'
9
 import draw from './draw'
9
 import draw from './draw'
10
 import duplicate from './duplicate'
10
 import duplicate from './duplicate'
11
 import generate from './generate'
11
 import generate from './generate'
12
+import group from './group'
13
+import handle from './handle'
12
 import move from './move'
14
 import move from './move'
15
+import moveToPage from './move-to-page'
13
 import nudge from './nudge'
16
 import nudge from './nudge'
14
 import rotate from './rotate'
17
 import rotate from './rotate'
15
 import rotateCcw from './rotate-ccw'
18
 import rotateCcw from './rotate-ccw'
19
 import transform from './transform'
22
 import transform from './transform'
20
 import transformSingle from './transform-single'
23
 import transformSingle from './transform-single'
21
 import translate from './translate'
24
 import translate from './translate'
22
-import handle from './handle'
23
-import group from './group'
24
 import ungroup from './ungroup'
25
 import ungroup from './ungroup'
25
 
26
 
26
 const commands = {
27
 const commands = {
35
   draw,
36
   draw,
36
   duplicate,
37
   duplicate,
37
   generate,
38
   generate,
39
+  group,
40
+  handle,
38
   move,
41
   move,
42
+  moveToPage,
39
   nudge,
43
   nudge,
40
   rotate,
44
   rotate,
41
   rotateCcw,
45
   rotateCcw,
45
   transform,
49
   transform,
46
   transformSingle,
50
   transformSingle,
47
   translate,
51
   translate,
48
-  handle,
49
-  group,
50
   ungroup,
52
   ungroup,
51
 }
53
 }
52
 
54
 

+ 107
- 0
state/commands/move-to-page.ts 查看文件

1
+import Command from './command'
2
+import history from '../history'
3
+import { Data } from 'types'
4
+import {
5
+  getDocumentBranch,
6
+  getPage,
7
+  getPageState,
8
+  getSelectedIds,
9
+  getSelectedShapes,
10
+  getTopParentId,
11
+  setToArray,
12
+  uniqueArray,
13
+} from 'utils/utils'
14
+import { getShapeUtils } from 'lib/shape-utils'
15
+import * as vec from 'utils/vec'
16
+import storage from 'state/storage'
17
+
18
+export default function nudgeCommand(data: Data, toPageId: string) {
19
+  const { currentPageId: fromPageId } = data
20
+  const selectedIds = setToArray(getSelectedIds(data))
21
+
22
+  const selectedParents = uniqueArray(
23
+    ...selectedIds.map((id) => getTopParentId(data, id))
24
+  )
25
+
26
+  history.execute(
27
+    data,
28
+    new Command({
29
+      name: 'set_direction',
30
+      category: 'canvas',
31
+      manualSelection: true,
32
+      do(data) {
33
+        // The page we're moving the shapes from
34
+        const fromPage = getPage(data, fromPageId)
35
+
36
+        // Get all of the selected shapes and their descendents
37
+        const shapesToMove = selectedParents.flatMap((id) =>
38
+          getDocumentBranch(data, id).map((id) => fromPage.shapes[id])
39
+        )
40
+
41
+        // Delete the shapes from the "from" page
42
+        shapesToMove.forEach((shape) => delete fromPage.shapes[shape.id])
43
+
44
+        // Clear the current page state's selected ids
45
+        getPageState(data, fromPageId).selectedIds.clear()
46
+
47
+        // Save the "from" page
48
+        storage.savePage(data, fromPageId)
49
+
50
+        // Load the "to" page
51
+        storage.loadPage(data, toPageId)
52
+
53
+        // The page we're moving the shapes to
54
+        const toPage = getPage(data, toPageId)
55
+
56
+        // Add all of the selected shapes to the "from" page. Any shapes that
57
+        // were children of the "from" page should become children of the "to"
58
+        // page. Grouped shapes should keep their same parent.
59
+
60
+        // What about shapes that were children of a group that we haven't moved?
61
+        shapesToMove.forEach((shape) => {
62
+          toPage.shapes[shape.id] = shape
63
+          if (shape.parentId === fromPageId) {
64
+            getShapeUtils(shape).setProperty(shape, 'parentId', toPageId)
65
+          }
66
+        })
67
+
68
+        console.log('from', getPage(data, fromPageId))
69
+        console.log('to', getPage(data, toPageId))
70
+
71
+        // Select the selected ids on the new page
72
+        getPageState(data, toPageId).selectedIds = new Set(selectedIds)
73
+
74
+        // Move to the new page
75
+        data.currentPageId = toPageId
76
+      },
77
+      undo(data) {
78
+        const toPage = getPage(data, fromPageId)
79
+
80
+        const shapesToMove = selectedParents.flatMap((id) =>
81
+          getDocumentBranch(data, id).map((id) => toPage.shapes[id])
82
+        )
83
+
84
+        shapesToMove.forEach((shape) => delete toPage.shapes[shape.id])
85
+
86
+        getPageState(data, toPageId).selectedIds.clear()
87
+
88
+        storage.savePage(data, toPageId)
89
+
90
+        storage.loadPage(data, fromPageId)
91
+
92
+        const fromPage = getPage(data, toPageId)
93
+
94
+        shapesToMove.forEach((shape) => {
95
+          fromPage.shapes[shape.id] = shape
96
+          if (shape.parentId === toPageId) {
97
+            getShapeUtils(shape).setProperty(shape, 'parentId', fromPageId)
98
+          }
99
+        })
100
+
101
+        getPageState(data, fromPageId).selectedIds = new Set(selectedIds)
102
+
103
+        data.currentPageId = fromPageId
104
+      },
105
+    })
106
+  )
107
+}

+ 2
- 9
state/sessions/brush-session.ts 查看文件

7
   getPage,
7
   getPage,
8
   getPageState,
8
   getPageState,
9
   getShapes,
9
   getShapes,
10
+  getTopParentId,
10
   setSelectedIds,
11
   setSelectedIds,
11
   setToArray,
12
   setToArray,
12
 } from 'utils/utils'
13
 } from 'utils/utils'
77
   const { selectedIds } = getPageState(cData)
78
   const { selectedIds } = getPageState(cData)
78
 
79
 
79
   const shapesToTest = getShapes(cData)
80
   const shapesToTest = getShapes(cData)
80
-    .filter((shape) => shape.type !== ShapeType.Group)
81
+    .filter((shape) => shape.type !== ShapeType.Group && !shape.isHidden)
81
     .filter(
82
     .filter(
82
       (shape) => !(selectedIds.has(shape.id) || selectedIds.has(shape.parentId))
83
       (shape) => !(selectedIds.has(shape.id) || selectedIds.has(shape.parentId))
83
     )
84
     )
100
 }
101
 }
101
 
102
 
102
 export type BrushSnapshot = ReturnType<typeof getBrushSnapshot>
103
 export type BrushSnapshot = ReturnType<typeof getBrushSnapshot>
103
-
104
-function getTopParentId(data: Data, id: string): string {
105
-  const shape = getPage(data).shapes[id]
106
-  return shape.parentId === data.currentPageId ||
107
-    shape.parentId === data.currentParentId
108
-    ? id
109
-    : getTopParentId(data, shape.parentId)
110
-}

+ 39
- 0
state/state.ts 查看文件

188
             CHANGED_CODE_CONTROL: 'updateControls',
188
             CHANGED_CODE_CONTROL: 'updateControls',
189
             GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
189
             GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
190
             TOGGLED_TOOL_LOCK: 'toggleToolLock',
190
             TOGGLED_TOOL_LOCK: 'toggleToolLock',
191
+            MOVED_TO_PAGE: {
192
+              if: 'hasSelection',
193
+              do: ['moveSelectionToPage', 'zoomCameraToSelectionActual'],
194
+            },
191
             MOVED: { if: 'hasSelection', do: 'moveSelection' },
195
             MOVED: { if: 'hasSelection', do: 'moveSelection' },
192
             DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' },
196
             DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' },
193
             ROTATED_CCW: { if: 'hasSelection', do: 'rotateSelectionCcw' },
197
             ROTATED_CCW: { if: 'hasSelection', do: 'rotateSelectionCcw' },
268
                     to: 'pointingBounds',
272
                     to: 'pointingBounds',
269
                   },
273
                   },
270
                 ],
274
                 ],
275
+                RIGHT_POINTED: [
276
+                  {
277
+                    if: 'isPointingCanvas',
278
+                    do: 'clearSelectedIds',
279
+                    else: {
280
+                      if: 'isPointingShape',
281
+                      then: [
282
+                        'setPointedId',
283
+                        {
284
+                          unless: 'isPointedShapeSelected',
285
+                          do: [
286
+                            'clearSelectedIds',
287
+                            'pushPointedIdToSelectedIds',
288
+                          ],
289
+                        },
290
+                      ],
291
+                    },
292
+                  },
293
+                ],
271
               },
294
               },
272
             },
295
             },
273
             pointingBounds: {
296
             pointingBounds: {
756
     },
779
     },
757
   },
780
   },
758
   conditions: {
781
   conditions: {
782
+    isPointingCanvas(data, payload: PointerInfo) {
783
+      return payload.target === 'canvas'
784
+    },
759
     isPointingBounds(data, payload: PointerInfo) {
785
     isPointingBounds(data, payload: PointerInfo) {
760
       return getSelectedIds(data).size > 0 && payload.target === 'bounds'
786
       return getSelectedIds(data).size > 0 && payload.target === 'bounds'
761
     },
787
     },
788
+    isPointingShape(data, payload: PointerInfo) {
789
+      return (
790
+        payload.target &&
791
+        payload.target !== 'canvas' &&
792
+        payload.target !== 'bounds'
793
+      )
794
+    },
762
     isReadOnly(data) {
795
     isReadOnly(data) {
763
       return data.isReadOnly
796
       return data.isReadOnly
764
     },
797
     },
765
     distanceImpliesDrag(data, payload: PointerInfo) {
798
     distanceImpliesDrag(data, payload: PointerInfo) {
766
       return vec.dist2(payload.origin, payload.point) > 8
799
       return vec.dist2(payload.origin, payload.point) > 8
767
     },
800
     },
801
+    hasPointedTarget(data, payload: PointerInfo) {
802
+      return payload.target !== undefined
803
+    },
768
     isPointedShapeSelected(data) {
804
     isPointedShapeSelected(data) {
769
       return getSelectedIds(data).has(data.pointedId)
805
       return getSelectedIds(data).has(data.pointedId)
770
     },
806
     },
1121
     moveSelection(data, payload: { type: MoveType }) {
1157
     moveSelection(data, payload: { type: MoveType }) {
1122
       commands.move(data, payload.type)
1158
       commands.move(data, payload.type)
1123
     },
1159
     },
1160
+    moveSelectionToPage(data, payload: { id: string }) {
1161
+      commands.moveToPage(data, payload.id)
1162
+    },
1124
     alignSelection(data, payload: { type: AlignType }) {
1163
     alignSelection(data, payload: { type: AlignType }) {
1125
       commands.align(data, payload.type)
1164
       commands.align(data, payload.type)
1126
     },
1165
     },

+ 2
- 1
styles/stitches.config.ts 查看文件

11
       hint: 'rgba(216, 226, 249, 1.000)',
11
       hint: 'rgba(216, 226, 249, 1.000)',
12
       selected: 'rgba(66, 133, 244, 1.000)',
12
       selected: 'rgba(66, 133, 244, 1.000)',
13
       bounds: 'rgba(65, 132, 244, 1.000)',
13
       bounds: 'rgba(65, 132, 244, 1.000)',
14
-      boundsBg: 'rgba(65, 132, 244, 0.050)',
14
+      boundsBg: 'rgba(65, 132, 244, 0.05)',
15
+      overlay: 'rgba(0, 0, 0, 0.15)',
15
       border: '#aaa',
16
       border: '#aaa',
16
       canvas: '#fafafa',
17
       canvas: '#fafafa',
17
       panel: '#fefefe',
18
       panel: '#fefefe',

+ 11
- 1
todo.md 查看文件

24
 ## Pages
24
 ## Pages
25
 
25
 
26
 - [x] Make selection part of page state
26
 - [x] Make selection part of page state
27
-- [ ] Allow only one page to be in the document at a time
27
+- [x] Allow only one page to be in the document at a time
28
+
29
+## Context Menu
30
+
31
+- [x] Create context Menu
32
+- [ ] Wire up events
33
+
34
+## Move to Page
35
+
36
+- [ ] Move to Page Command
37
+- [ ] Dialog

+ 18
- 1
utils/utils.ts 查看文件

1398
 }
1398
 }
1399
 
1399
 
1400
 export function isMobile() {
1400
 export function isMobile() {
1401
-  return _isMobile()
1401
+  return _isMobile().any
1402
 }
1402
 }
1403
 
1403
 
1404
 export function getRotatedBounds(shape: Shape) {
1404
 export function getRotatedBounds(shape: Shape) {
1661
 
1661
 
1662
 export function getDocumentBranch(data: Data, id: string): string[] {
1662
 export function getDocumentBranch(data: Data, id: string): string[] {
1663
   const shape = getPage(data).shapes[id]
1663
   const shape = getPage(data).shapes[id]
1664
+
1664
   if (shape.type !== ShapeType.Group) return [id]
1665
   if (shape.type !== ShapeType.Group) return [id]
1665
 
1666
 
1666
   return [
1667
   return [
1741
 export function shuffleArr<T>(arr: T[], offset: number): T[] {
1742
 export function shuffleArr<T>(arr: T[], offset: number): T[] {
1742
   return arr.map((_, i) => arr[(i + offset) % arr.length])
1743
   return arr.map((_, i) => arr[(i + offset) % arr.length])
1743
 }
1744
 }
1745
+
1746
+export function commandKey() {
1747
+  return isDarwin() ? '⌘' : 'Ctrl'
1748
+}
1749
+
1750
+export function getTopParentId(data: Data, id: string): string {
1751
+  const shape = getPage(data).shapes[id]
1752
+  return shape.parentId === data.currentPageId ||
1753
+    shape.parentId === data.currentParentId
1754
+    ? id
1755
+    : getTopParentId(data, shape.parentId)
1756
+}
1757
+
1758
+export function uniqueArray<T extends string | number | Symbol>(...items: T[]) {
1759
+  return Array.from(new Set(items).values())
1760
+}

正在加载...
取消
保存