소스 검색

Adds page control, pages

main
Steve Ruiz 4 년 전
부모
커밋
5ba56216d0

+ 3
- 2
components/canvas/bounds/bounding-box.tsx 파일 보기

@@ -1,8 +1,9 @@
1 1
 import * as React from 'react'
2
-import { Edge, Corner, LineShape, ArrowShape } from 'types'
2
+import { Edge, Corner } from 'types'
3 3
 import { useSelector } from 'state'
4 4
 import {
5 5
   deepCompareArrays,
6
+  getCurrentCamera,
6 7
   getPage,
7 8
   getSelectedShapes,
8 9
   isMobile,
@@ -17,7 +18,7 @@ import Handles from './handles'
17 18
 export default function Bounds() {
18 19
   const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
19 20
   const isSelecting = useSelector((s) => s.isIn('selecting'))
20
-  const zoom = useSelector((s) => s.data.camera.zoom)
21
+  const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
21 22
   const bounds = useSelector((s) => s.values.selectedBounds)
22 23
 
23 24
   const selectedIds = useSelector(

+ 2
- 2
components/canvas/defs.tsx 파일 보기

@@ -1,10 +1,10 @@
1 1
 import { getShapeUtils } from 'lib/shape-utils'
2 2
 import { memo } from 'react'
3 3
 import { useSelector } from 'state'
4
-import { deepCompareArrays, getPage } from 'utils/utils'
4
+import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
5 5
 
6 6
 export default function Defs() {
7
-  const zoom = useSelector((s) => s.data.camera.zoom)
7
+  const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
8 8
 
9 9
   const currentPageShapeIds = useSelector(({ data }) => {
10 10
     return Object.values(getPage(data).shapes)

+ 2
- 0
components/editor.tsx 파일 보기

@@ -8,6 +8,7 @@ import ToolsPanel from './tools-panel/tools-panel'
8 8
 import StylePanel from './style-panel/style-panel'
9 9
 import { useSelector } from 'state'
10 10
 import styled from 'styles'
11
+import PagePanel from './page-panel/page-panel'
11 12
 
12 13
 export default function Editor() {
13 14
   useKeyboardEvents()
@@ -20,6 +21,7 @@ export default function Editor() {
20 21
   return (
21 22
     <Layout>
22 23
       <Canvas />
24
+      <PagePanel />
23 25
       <LeftPanels>
24 26
         <CodePanel />
25 27
         {hasControls && <ControlsPanel />}

+ 100
- 0
components/page-panel/page-panel.tsx 파일 보기

@@ -0,0 +1,100 @@
1
+import styled from 'styles'
2
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
3
+import * as RadioGroup from '@radix-ui/react-radio-group'
4
+import { IconWrapper, RowButton } from 'components/shared'
5
+import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons'
6
+import * as Panel from '../panel'
7
+import state, { useSelector } from 'state'
8
+import { getPage } from 'utils/utils'
9
+
10
+export default function PagePanel() {
11
+  const currentPageId = useSelector((s) => s.data.currentPageId)
12
+  const documentPages = useSelector((s) => s.data.document.pages)
13
+
14
+  const sorted = Object.values(documentPages).sort(
15
+    (a, b) => a.childIndex - b.childIndex
16
+  )
17
+
18
+  return (
19
+    <OuterContainer>
20
+      <DropdownMenu.Root>
21
+        <PanelRoot>
22
+          <DropdownMenu.Trigger as={RowButton}>
23
+            <span>{documentPages[currentPageId].name}</span>
24
+            <IconWrapper size="small">
25
+              <ChevronDownIcon />
26
+            </IconWrapper>
27
+          </DropdownMenu.Trigger>
28
+          <DropdownMenu.Content sideOffset={8}>
29
+            <PanelRoot>
30
+              <DropdownMenu.RadioGroup
31
+                as={Content}
32
+                value={currentPageId}
33
+                onValueChange={(id) =>
34
+                  state.send('CHANGED_CURRENT_PAGE', { id })
35
+                }
36
+              >
37
+                {sorted.map(({ id, name }) => (
38
+                  <StyledRadioItem key={id} value={id}>
39
+                    <span>{name}</span>
40
+                    <DropdownMenu.ItemIndicator as={IconWrapper} size="small">
41
+                      <CheckIcon />
42
+                    </DropdownMenu.ItemIndicator>
43
+                  </StyledRadioItem>
44
+                ))}
45
+              </DropdownMenu.RadioGroup>
46
+            </PanelRoot>
47
+          </DropdownMenu.Content>
48
+        </PanelRoot>
49
+      </DropdownMenu.Root>
50
+    </OuterContainer>
51
+  )
52
+}
53
+
54
+const PanelRoot = styled('div', {
55
+  minWidth: 1,
56
+  width: 184,
57
+  maxWidth: 184,
58
+  overflow: 'hidden',
59
+  position: 'relative',
60
+  display: 'flex',
61
+  alignItems: 'center',
62
+  pointerEvents: 'all',
63
+  padding: '2px',
64
+  borderRadius: '4px',
65
+  backgroundColor: '$panel',
66
+  border: '1px solid $panel',
67
+  boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
68
+})
69
+
70
+const Content = styled(Panel.Content, {
71
+  width: '100%',
72
+})
73
+
74
+const StyledRadioItem = styled(DropdownMenu.RadioItem, {
75
+  height: 32,
76
+  width: '100%',
77
+  display: 'flex',
78
+  alignItems: 'center',
79
+  justifyContent: 'space-between',
80
+  padding: '0 6px 0 12px',
81
+  cursor: 'pointer',
82
+  borderRadius: '4px',
83
+  backgroundColor: 'transparent',
84
+  outline: 'none',
85
+  '&:hover': {
86
+    backgroundColor: '$hover',
87
+  },
88
+})
89
+
90
+const OuterContainer = styled('div', {
91
+  position: 'fixed',
92
+  top: 8,
93
+  left: 0,
94
+  display: 'flex',
95
+  alignItems: 'center',
96
+  justifyContent: 'center',
97
+  width: '100%',
98
+  zIndex: 200,
99
+  height: 44,
100
+})

+ 236
- 0
components/shared.tsx 파일 보기

@@ -1,3 +1,6 @@
1
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
+import * as RadioGroup from '@radix-ui/react-radio-group'
3
+import * as Panel from './panel'
1 4
 import styled from 'styles'
2 5
 
3 6
 export const IconButton = styled('button', {
@@ -60,3 +63,236 @@ export const IconButton = styled('button', {
60 63
     },
61 64
   },
62 65
 })
66
+
67
+export const RowButton = styled('button', {
68
+  position: 'relative',
69
+  display: 'flex',
70
+  width: '100%',
71
+  background: 'none',
72
+  height: '32px',
73
+  border: 'none',
74
+  cursor: 'pointer',
75
+  outline: 'none',
76
+  alignItems: 'center',
77
+  justifyContent: 'space-between',
78
+  padding: '4px 6px 4px 12px',
79
+
80
+  '&::before': {
81
+    content: "''",
82
+    position: 'absolute',
83
+    top: 0,
84
+    left: 0,
85
+    right: 0,
86
+    bottom: 0,
87
+    pointerEvents: 'none',
88
+    zIndex: -1,
89
+  },
90
+
91
+  '&:hover::before': {
92
+    backgroundColor: '$hover',
93
+    borderRadius: 4,
94
+  },
95
+
96
+  '& label': {
97
+    fontFamily: '$ui',
98
+    fontSize: '$2',
99
+    fontWeight: '$1',
100
+    margin: 0,
101
+    padding: 0,
102
+  },
103
+
104
+  '& svg': {
105
+    position: 'relative',
106
+    stroke: 'rgba(0,0,0,.2)',
107
+    strokeWidth: 1,
108
+    zIndex: 1,
109
+  },
110
+
111
+  variants: {
112
+    size: {
113
+      icon: {
114
+        padding: '4px ',
115
+        width: 'auto',
116
+      },
117
+    },
118
+  },
119
+})
120
+
121
+export const StylePanelRoot = styled(Panel.Root, {
122
+  minWidth: 1,
123
+  width: 184,
124
+  maxWidth: 184,
125
+  overflow: 'hidden',
126
+  position: 'relative',
127
+  border: '1px solid $panel',
128
+  boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
129
+
130
+  variants: {
131
+    isOpen: {
132
+      true: {},
133
+      false: {
134
+        padding: 2,
135
+        height: 38,
136
+        width: 38,
137
+      },
138
+    },
139
+  },
140
+})
141
+
142
+export const Group = styled(RadioGroup.Root, {
143
+  display: 'flex',
144
+})
145
+
146
+export const Item = styled('button', {
147
+  height: '32px',
148
+  width: '32px',
149
+  backgroundColor: '$panel',
150
+  borderRadius: '4px',
151
+  padding: '0',
152
+  margin: '0',
153
+  display: 'flex',
154
+  alignItems: 'center',
155
+  justifyContent: 'center',
156
+  outline: 'none',
157
+  border: 'none',
158
+  pointerEvents: 'all',
159
+  cursor: 'pointer',
160
+
161
+  '&:hover:not(:disabled)': {
162
+    backgroundColor: '$hover',
163
+    '& svg': {
164
+      stroke: '$text',
165
+      fill: '$text',
166
+      strokeWidth: '0',
167
+    },
168
+  },
169
+
170
+  '&:disabled': {
171
+    opacity: '0.5',
172
+  },
173
+
174
+  variants: {
175
+    isActive: {
176
+      true: {
177
+        '& svg': {
178
+          fill: '$text',
179
+          stroke: '$text',
180
+        },
181
+      },
182
+      false: {
183
+        '& svg': {
184
+          fill: '$inactive',
185
+          stroke: '$inactive',
186
+        },
187
+      },
188
+    },
189
+  },
190
+})
191
+
192
+export const IconWrapper = styled('div', {
193
+  height: '100%',
194
+  borderRadius: '4px',
195
+  marginRight: '1px',
196
+  display: 'grid',
197
+  alignItems: 'center',
198
+  justifyContent: 'center',
199
+  outline: 'none',
200
+  border: 'none',
201
+  pointerEvents: 'all',
202
+  cursor: 'pointer',
203
+
204
+  '& svg': {
205
+    height: 22,
206
+    width: 22,
207
+    strokeWidth: 1,
208
+  },
209
+
210
+  '& > *': {
211
+    gridRow: 1,
212
+    gridColumn: 1,
213
+  },
214
+
215
+  variants: {
216
+    size: {
217
+      small: {
218
+        '& svg': {
219
+          height: '16px',
220
+          width: '16px',
221
+        },
222
+      },
223
+      medium: {
224
+        '& svg': {
225
+          height: '22px',
226
+          width: '22px',
227
+        },
228
+      },
229
+    },
230
+  },
231
+})
232
+
233
+export const DropdownContent = styled(DropdownMenu.Content, {
234
+  display: 'grid',
235
+  padding: 4,
236
+  gridTemplateColumns: 'repeat(4, 1fr)',
237
+  backgroundColor: '$panel',
238
+  borderRadius: 4,
239
+  border: '1px solid $panel',
240
+  boxShadow: '0px 2px 4px rgba(0,0,0,.28)',
241
+
242
+  variants: {
243
+    direction: {
244
+      vertical: {
245
+        gridTemplateColumns: '1fr',
246
+      },
247
+    },
248
+  },
249
+})
250
+
251
+export function DashSolidIcon() {
252
+  return (
253
+    <svg width="24" height="24" stroke="currentColor">
254
+      <circle
255
+        cx={12}
256
+        cy={12}
257
+        r={8}
258
+        fill="none"
259
+        strokeWidth={2}
260
+        strokeLinecap="round"
261
+      />
262
+    </svg>
263
+  )
264
+}
265
+
266
+export function DashDashedIcon() {
267
+  return (
268
+    <svg width="24" height="24" stroke="currentColor">
269
+      <circle
270
+        cx={12}
271
+        cy={12}
272
+        r={8}
273
+        fill="none"
274
+        strokeWidth={2.5}
275
+        strokeLinecap="round"
276
+        strokeDasharray={50.26548 * 0.1}
277
+      />
278
+    </svg>
279
+  )
280
+}
281
+
282
+const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
283
+
284
+export function DashDottedIcon() {
285
+  return (
286
+    <svg width="24" height="24" stroke="currentColor">
287
+      <circle
288
+        cx={12}
289
+        cy={12}
290
+        r={8}
291
+        fill="none"
292
+        strokeWidth={2.5}
293
+        strokeLinecap="round"
294
+        strokeDasharray={dottedDasharray}
295
+      />
296
+    </svg>
297
+  )
298
+}

+ 1
- 1
components/style-panel/color-content.tsx 파일 보기

@@ -4,7 +4,7 @@ import { ColorStyle } from 'types'
4 4
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
5 5
 import { Square } from 'react-feather'
6 6
 import styled from 'styles'
7
-import { DropdownContent } from './shared'
7
+import { DropdownContent } from '../shared'
8 8
 
9 9
 export default function ColorContent({
10 10
   onChange,

+ 1
- 1
components/style-panel/color-picker.tsx 파일 보기

@@ -1,7 +1,7 @@
1 1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2 2
 import { strokes } from 'lib/shape-styles'
3 3
 import { ColorStyle } from 'types'
4
-import { IconWrapper, RowButton } from './shared'
4
+import { RowButton, IconWrapper } from '../shared'
5 5
 import { Square } from 'react-feather'
6 6
 import ColorContent from './color-content'
7 7
 

+ 1
- 1
components/style-panel/dash-picker.tsx 파일 보기

@@ -4,7 +4,7 @@ import {
4 4
   DashDashedIcon,
5 5
   DashDottedIcon,
6 6
   DashSolidIcon,
7
-} from './shared'
7
+} from '../shared'
8 8
 import * as RadioGroup from '@radix-ui/react-radio-group'
9 9
 import { DashStyle } from 'types'
10 10
 import state from 'state'

+ 1
- 1
components/style-panel/is-filled-picker.tsx 파일 보기

@@ -2,7 +2,7 @@ import * as Checkbox from '@radix-ui/react-checkbox'
2 2
 import { CheckIcon } from '@radix-ui/react-icons'
3 3
 import { strokes } from 'lib/shape-styles'
4 4
 import { Square } from 'react-feather'
5
-import { IconWrapper, RowButton } from './shared'
5
+import { IconWrapper, RowButton } from '../shared'
6 6
 
7 7
 interface Props {
8 8
   isFilled: boolean

+ 1
- 1
components/style-panel/quick-dash-select.tsx 파일 보기

@@ -9,7 +9,7 @@ import {
9 9
   DashDottedIcon,
10 10
   DashSolidIcon,
11 11
   DashDashedIcon,
12
-} from './shared'
12
+} from '../shared'
13 13
 
14 14
 const dashes = {
15 15
   [DashStyle.Solid]: <DashSolidIcon />,

+ 1
- 1
components/style-panel/quick-size-select.tsx 파일 보기

@@ -4,7 +4,7 @@ import Tooltip from 'components/tooltip'
4 4
 import { Circle } from 'react-feather'
5 5
 import state, { useSelector } from 'state'
6 6
 import { SizeStyle } from 'types'
7
-import { DropdownContent, Item } from './shared'
7
+import { DropdownContent, Item } from '../shared'
8 8
 
9 9
 const sizes = {
10 10
   [SizeStyle.Small]: 6,

+ 0
- 219
components/style-panel/shared.tsx 파일 보기

@@ -1,219 +0,0 @@
1
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
-import * as RadioGroup from '@radix-ui/react-radio-group'
3
-import * as Panel from '../panel'
4
-import styled from 'styles'
5
-
6
-export const StylePanelRoot = styled(Panel.Root, {
7
-  minWidth: 1,
8
-  width: 184,
9
-  maxWidth: 184,
10
-  overflow: 'hidden',
11
-  position: 'relative',
12
-  border: '1px solid $panel',
13
-  boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
14
-
15
-  variants: {
16
-    isOpen: {
17
-      true: {},
18
-      false: {
19
-        padding: 2,
20
-        height: 38,
21
-        width: 38,
22
-      },
23
-    },
24
-  },
25
-})
26
-
27
-export const Group = styled(RadioGroup.Root, {
28
-  display: 'flex',
29
-})
30
-
31
-export const Item = styled('button', {
32
-  height: '32px',
33
-  width: '32px',
34
-  backgroundColor: '$panel',
35
-  borderRadius: '4px',
36
-  padding: '0',
37
-  margin: '0',
38
-  display: 'flex',
39
-  alignItems: 'center',
40
-  justifyContent: 'center',
41
-  outline: 'none',
42
-  border: 'none',
43
-  pointerEvents: 'all',
44
-  cursor: 'pointer',
45
-
46
-  '&:hover:not(:disabled)': {
47
-    backgroundColor: '$hover',
48
-    '& svg': {
49
-      stroke: '$text',
50
-      fill: '$text',
51
-      strokeWidth: '0',
52
-    },
53
-  },
54
-
55
-  '&:disabled': {
56
-    opacity: '0.5',
57
-  },
58
-
59
-  variants: {
60
-    isActive: {
61
-      true: {
62
-        '& svg': {
63
-          fill: '$text',
64
-          stroke: '$text',
65
-        },
66
-      },
67
-      false: {
68
-        '& svg': {
69
-          fill: '$inactive',
70
-          stroke: '$inactive',
71
-        },
72
-      },
73
-    },
74
-  },
75
-})
76
-
77
-export const RowButton = styled('button', {
78
-  position: 'relative',
79
-  display: 'flex',
80
-  width: '100%',
81
-  background: 'none',
82
-  border: 'none',
83
-  cursor: 'pointer',
84
-  outline: 'none',
85
-  alignItems: 'center',
86
-  justifyContent: 'space-between',
87
-  padding: '4px 6px 4px 12px',
88
-
89
-  '&::before': {
90
-    content: "''",
91
-    position: 'absolute',
92
-    top: 0,
93
-    left: 0,
94
-    right: 0,
95
-    bottom: 0,
96
-    pointerEvents: 'none',
97
-    zIndex: -1,
98
-  },
99
-
100
-  '&:hover::before': {
101
-    backgroundColor: '$hover',
102
-    borderRadius: 4,
103
-  },
104
-
105
-  '& label': {
106
-    fontFamily: '$ui',
107
-    fontSize: '$2',
108
-    fontWeight: '$1',
109
-    margin: 0,
110
-    padding: 0,
111
-  },
112
-
113
-  '& svg': {
114
-    position: 'relative',
115
-    stroke: 'rgba(0,0,0,.2)',
116
-    strokeWidth: 1,
117
-    zIndex: 1,
118
-  },
119
-
120
-  variants: {
121
-    size: {
122
-      icon: {
123
-        padding: '4px ',
124
-        width: 'auto',
125
-      },
126
-    },
127
-  },
128
-})
129
-
130
-export const IconWrapper = styled('div', {
131
-  height: '100%',
132
-  borderRadius: '4px',
133
-  marginRight: '1px',
134
-  display: 'grid',
135
-  alignItems: 'center',
136
-  justifyContent: 'center',
137
-  outline: 'none',
138
-  border: 'none',
139
-  pointerEvents: 'all',
140
-  cursor: 'pointer',
141
-
142
-  '& svg': {
143
-    height: 22,
144
-    width: 22,
145
-    strokeWidth: 1,
146
-  },
147
-
148
-  '& > *': {
149
-    gridRow: 1,
150
-    gridColumn: 1,
151
-  },
152
-})
153
-
154
-export const DropdownContent = styled(DropdownMenu.Content, {
155
-  display: 'grid',
156
-  padding: 4,
157
-  gridTemplateColumns: 'repeat(4, 1fr)',
158
-  backgroundColor: '$panel',
159
-  borderRadius: 4,
160
-  border: '1px solid $panel',
161
-  boxShadow: '0px 2px 4px rgba(0,0,0,.28)',
162
-
163
-  variants: {
164
-    direction: {
165
-      vertical: {
166
-        gridTemplateColumns: '1fr',
167
-      },
168
-    },
169
-  },
170
-})
171
-
172
-export function DashSolidIcon() {
173
-  return (
174
-    <svg width="24" height="24" stroke="currentColor">
175
-      <circle
176
-        cx={12}
177
-        cy={12}
178
-        r={8}
179
-        fill="none"
180
-        strokeWidth={2}
181
-        strokeLinecap="round"
182
-      />
183
-    </svg>
184
-  )
185
-}
186
-
187
-export function DashDashedIcon() {
188
-  return (
189
-    <svg width="24" height="24" stroke="currentColor">
190
-      <circle
191
-        cx={12}
192
-        cy={12}
193
-        r={8}
194
-        fill="none"
195
-        strokeWidth={2.5}
196
-        strokeLinecap="round"
197
-        strokeDasharray={50.26548 * 0.1}
198
-      />
199
-    </svg>
200
-  )
201
-}
202
-
203
-const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
204
-
205
-export function DashDottedIcon() {
206
-  return (
207
-    <svg width="24" height="24" stroke="currentColor">
208
-      <circle
209
-        cx={12}
210
-        cy={12}
211
-        r={8}
212
-        fill="none"
213
-        strokeWidth={2.5}
214
-        strokeLinecap="round"
215
-        strokeDasharray={dottedDasharray}
216
-      />
217
-    </svg>
218
-  )
219
-}

+ 1
- 1
components/style-panel/size-picker.tsx 파일 보기

@@ -1,4 +1,4 @@
1
-import { Group, Item } from './shared'
1
+import { Group, Item } from '../shared'
2 2
 import * as RadioGroup from '@radix-ui/react-radio-group'
3 3
 import { ChangeEvent } from 'react'
4 4
 import { Circle } from 'react-feather'

+ 1
- 8
components/style-panel/style-panel.tsx 파일 보기

@@ -3,10 +3,8 @@ import state, { useSelector } from 'state'
3 3
 import * as Panel from 'components/panel'
4 4
 import { useRef } from 'react'
5 5
 import { IconButton } from 'components/shared'
6
-import * as Checkbox from '@radix-ui/react-checkbox'
7
-import { ChevronDown, Square, Tool, Trash2, X } from 'react-feather'
6
+import { ChevronDown, Trash2, X } from 'react-feather'
8 7
 import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
9
-import { strokes } from 'lib/shape-styles'
10 8
 import AlignDistribute from './align-distribute'
11 9
 import { MoveType } from 'types'
12 10
 import SizePicker from './size-picker'
@@ -15,9 +13,7 @@ import {
15 13
   ArrowUpIcon,
16 14
   AspectRatioIcon,
17 15
   BoxIcon,
18
-  CheckIcon,
19 16
   CopyIcon,
20
-  DotsVerticalIcon,
21 17
   EyeClosedIcon,
22 18
   EyeOpenIcon,
23 19
   LockClosedIcon,
@@ -28,10 +24,7 @@ import {
28 24
 } from '@radix-ui/react-icons'
29 25
 import DashPicker from './dash-picker'
30 26
 import QuickColorSelect from './quick-color-select'
31
-import ColorContent from './color-content'
32
-import { RowButton, IconWrapper } from './shared'
33 27
 import ColorPicker from './color-picker'
34
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
35 28
 import IsFilledPicker from './is-filled-picker'
36 29
 import QuickSizeSelect from './quick-size-select'
37 30
 import QuickdashSelect from './quick-dash-select'

+ 4
- 2
components/tools-panel/zoom.tsx 파일 보기

@@ -2,6 +2,7 @@ import { ZoomInIcon, ZoomOutIcon } from '@radix-ui/react-icons'
2 2
 import { IconButton } from 'components/shared'
3 3
 import state, { useSelector } from 'state'
4 4
 import styled from 'styles'
5
+import { getCurrentCamera } from 'utils/utils'
5 6
 import Tooltip from '../tooltip'
6 7
 
7 8
 const zoomIn = () => state.send('ZOOMED_IN')
@@ -30,10 +31,11 @@ export default function Zoom() {
30 31
 }
31 32
 
32 33
 function ZoomCounter() {
33
-  const camera = useSelector((s) => s.data.camera)
34
+  const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
35
+
34 36
   return (
35 37
     <ZoomButton onClick={zoomToActual} onDoubleClick={zoomToFit}>
36
-      {Math.round(camera.zoom * 100)}%
38
+      {Math.round(zoom * 100)}%
37 39
     </ZoomButton>
38 40
   )
39 41
 }

+ 14
- 10
hooks/useCamera.ts 파일 보기

@@ -1,5 +1,6 @@
1
-import React, { useEffect } from "react"
2
-import state from "state"
1
+import React, { useEffect } from 'react'
2
+import state from 'state'
3
+import { getCurrentCamera } from 'utils/utils'
3 4
 
4 5
 /**
5 6
  * When the state's camera changes, update the transform of
@@ -8,24 +9,27 @@ import state from "state"
8 9
  */
9 10
 export default function useCamera(ref: React.MutableRefObject<SVGGElement>) {
10 11
   useEffect(() => {
11
-    let { camera } = state.data
12
+    let prev = getCurrentCamera(state.data)
12 13
 
13
-    return state.onUpdate(({ data }) => {
14
+    return state.onUpdate(() => {
14 15
       const g = ref.current
15 16
       if (!g) return
16 17
 
17
-      const { point, zoom } = data.camera
18
+      const { point, zoom } = getCurrentCamera(state.data)
18 19
 
19
-      if (point !== camera.point || zoom !== camera.zoom) {
20
+      if (point !== prev.point || zoom !== prev.zoom) {
20 21
         g.setAttribute(
21
-          "transform",
22
+          'transform',
22 23
           `scale(${zoom}) translate(${point[0]} ${point[1]})`
23 24
         )
24 25
 
25
-        localStorage.setItem("code_slate_camera", JSON.stringify(data.camera))
26
-      }
26
+        localStorage.setItem(
27
+          'code_slate_camera',
28
+          JSON.stringify({ point, zoom })
29
+        )
27 30
 
28
-      camera = data.camera
31
+        prev = getCurrentCamera(state.data)
32
+      }
29 33
     })
30 34
   }, [state])
31 35
 }

+ 24
- 0
state/commands/change-page.ts 파일 보기

@@ -0,0 +1,24 @@
1
+import Command from './command'
2
+import history from '../history'
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'
7
+
8
+export default function nudgeCommand(data: Data, pageId: string) {
9
+  const { currentPageId: prevPageId } = data
10
+
11
+  history.execute(
12
+    data,
13
+    new Command({
14
+      name: 'change_page',
15
+      category: 'canvas',
16
+      do(data) {
17
+        data.currentPageId = pageId
18
+      },
19
+      undo(data) {
20
+        data.currentPageId = prevPageId
21
+      },
22
+    })
23
+  )
24
+}

+ 2
- 2
state/commands/duplicate.ts 파일 보기

@@ -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, getSelectedShapes } from 'utils/utils'
4
+import { getCurrentCamera, getPage, getSelectedShapes } from 'utils/utils'
5 5
 import { v4 as uuid } from 'uuid'
6 6
 import { current } from 'immer'
7 7
 import * as vec from 'utils/vec'
@@ -12,7 +12,7 @@ export default function duplicateCommand(data: Data) {
12 12
   const duplicates = selectedShapes.map((shape) => ({
13 13
     ...shape,
14 14
     id: uuid(),
15
-    point: vec.add(shape.point, vec.div([16, 16], data.camera.zoom)),
15
+    point: vec.add(shape.point, vec.div([16, 16], getCurrentCamera(data).zoom)),
16 16
   }))
17 17
 
18 18
   history.execute(

+ 2
- 0
state/commands/index.ts 파일 보기

@@ -1,5 +1,6 @@
1 1
 import align from './align'
2 2
 import arrow from './arrow'
3
+import changePage from './change-page'
3 4
 import deleteSelected from './delete-selected'
4 5
 import direct from './direct'
5 6
 import distribute from './distribute'
@@ -21,6 +22,7 @@ import handle from './handle'
21 22
 const commands = {
22 23
   align,
23 24
   arrow,
25
+  changePage,
24 26
   deleteSelected,
25 27
   direct,
26 28
   distribute,

+ 2
- 14
state/commands/transform-single.ts 파일 보기

@@ -10,8 +10,6 @@ export default function transformSingleCommand(
10 10
   data: Data,
11 11
   before: TransformSingleSnapshot,
12 12
   after: TransformSingleSnapshot,
13
-  scaleX: number,
14
-  scaleY: number,
15 13
   isCreating: boolean
16 14
 ) {
17 15
   const shape = current(getPage(data, after.currentPageId).shapes[after.id])
@@ -23,24 +21,14 @@ export default function transformSingleCommand(
23 21
       category: 'canvas',
24 22
       manualSelection: true,
25 23
       do(data) {
26
-        const { id, type, initialShapeBounds } = after
24
+        const { id } = after
27 25
 
28 26
         const { shapes } = getPage(data, after.currentPageId)
29 27
 
30 28
         data.selectedIds.clear()
31 29
         data.selectedIds.add(id)
32 30
 
33
-        if (isCreating) {
34
-          shapes[id] = shape
35
-        } else {
36
-          getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
37
-            type,
38
-            initialShape: before.initialShape,
39
-            scaleX,
40
-            scaleY,
41
-            transformOrigin: [0.5, 0.5],
42
-          })
43
-        }
31
+        shapes[id] = shape
44 32
       },
45 33
       undo(data) {
46 34
         const { id, type, initialShapeBounds } = before

+ 7
- 0
state/data.ts 파일 보기

@@ -128,6 +128,13 @@ export const defaultDocument: Data['document'] = {
128 128
         // }),
129 129
       },
130 130
     },
131
+    page1: {
132
+      id: 'page1',
133
+      type: 'page',
134
+      name: 'Page 1',
135
+      childIndex: 1,
136
+      shapes: {},
137
+    },
131 138
   },
132 139
   code: {
133 140
     file0: {

+ 6
- 3
state/history.ts 파일 보기

@@ -106,7 +106,7 @@ class History extends BaseHistory<Data> {
106 106
   }
107 107
 
108 108
   restoreSavedData(data: any): Data {
109
-    const restoredData = { ...data }
109
+    const restoredData: Data = { ...data }
110 110
 
111 111
     restoredData.selectedIds = new Set(restoredData.selectedIds)
112 112
 
@@ -114,12 +114,15 @@ class History extends BaseHistory<Data> {
114 114
     const cameraInfo = localStorage.getItem('code_slate_camera')
115 115
 
116 116
     if (cameraInfo !== null) {
117
-      Object.assign(restoredData.camera, JSON.parse(cameraInfo))
117
+      Object.assign(
118
+        restoredData.pageStates[data.currentPageId].camera,
119
+        JSON.parse(cameraInfo)
120
+      )
118 121
 
119 122
       // And update the CSS property
120 123
       document.documentElement.style.setProperty(
121 124
         '--camera-zoom',
122
-        restoredData.camera.zoom.toString()
125
+        restoredData.pageStates[data.currentPageId].camera.zoom.toString()
123 126
       )
124 127
     }
125 128
 

+ 0
- 2
state/sessions/transform-single-session.ts 파일 보기

@@ -85,8 +85,6 @@ export default class TransformSingleSession extends BaseSession {
85 85
       data,
86 86
       this.snapshot,
87 87
       getTransformSingleSnapshot(data, this.transformType),
88
-      this.scaleX,
89
-      this.scaleY,
90 88
       this.isCreating
91 89
     )
92 90
   }

+ 33
- 19
state/state.ts 파일 보기

@@ -12,6 +12,7 @@ import {
12 12
   getChildren,
13 13
   getCommonBounds,
14 14
   getCurrent,
15
+  getCurrentCamera,
15 16
   getPage,
16 17
   getSelectedBounds,
17 18
   getShape,
@@ -54,10 +55,6 @@ const initialData: Data = {
54 55
     dash: DashStyle.Solid,
55 56
     isFilled: false,
56 57
   },
57
-  camera: {
58
-    point: [0, 0],
59
-    zoom: 1,
60
-  },
61 58
   activeTool: 'select',
62 59
   brush: undefined,
63 60
   boundsRotation: 0,
@@ -68,6 +65,20 @@ const initialData: Data = {
68 65
   currentCodeFileId: 'file0',
69 66
   codeControls: {},
70 67
   document: defaultDocument,
68
+  pageStates: {
69
+    page0: {
70
+      camera: {
71
+        point: [0, 0],
72
+        zoom: 1,
73
+      },
74
+    },
75
+    page1: {
76
+      camera: {
77
+        point: [0, 0],
78
+        zoom: 1,
79
+      },
80
+    },
81
+  },
71 82
 }
72 83
 
73 84
 const state = createState({
@@ -139,6 +150,7 @@ const state = createState({
139 150
         USED_PEN_DEVICE: 'enablePenLock',
140 151
         DISABLED_PEN_LOCK: 'disablePenLock',
141 152
         CLEARED_PAGE: ['selectAll', 'deleteSelection'],
153
+        CHANGED_CURRENT_PAGE: ['clearSelectedIds', 'setCurrentPage'],
142 154
       },
143 155
       initial: 'selecting',
144 156
       states: {
@@ -732,6 +744,11 @@ const state = createState({
732 744
     },
733 745
   },
734 746
   actions: {
747
+    /* ---------------------- Pages --------------------- */
748
+    setCurrentPage(data, payload: { id: string }) {
749
+      commands.changePage(data, payload.id)
750
+    },
751
+
735 752
     /* --------------------- Shapes --------------------- */
736 753
     createShape(data, payload, type: ShapeType) {
737 754
       const shape = createShape(type, {
@@ -1062,7 +1079,7 @@ const state = createState({
1062 1079
     /* --------------------- Camera --------------------- */
1063 1080
 
1064 1081
     zoomIn(data) {
1065
-      const { camera } = data
1082
+      const camera = getCurrentCamera(data)
1066 1083
       const i = Math.round((camera.zoom * 100) / 25)
1067 1084
       const center = [window.innerWidth / 2, window.innerHeight / 2]
1068 1085
 
@@ -1074,7 +1091,7 @@ const state = createState({
1074 1091
       setZoomCSS(camera.zoom)
1075 1092
     },
1076 1093
     zoomOut(data) {
1077
-      const { camera } = data
1094
+      const camera = getCurrentCamera(data)
1078 1095
       const i = Math.round((camera.zoom * 100) / 25)
1079 1096
       const center = [window.innerWidth / 2, window.innerHeight / 2]
1080 1097
 
@@ -1086,8 +1103,7 @@ const state = createState({
1086 1103
       setZoomCSS(camera.zoom)
1087 1104
     },
1088 1105
     zoomCameraToActual(data) {
1089
-      const { camera } = data
1090
-
1106
+      const camera = getCurrentCamera(data)
1091 1107
       const center = [window.innerWidth / 2, window.innerHeight / 2]
1092 1108
 
1093 1109
       const p0 = screenToWorld(center, data)
@@ -1098,7 +1114,7 @@ const state = createState({
1098 1114
       setZoomCSS(camera.zoom)
1099 1115
     },
1100 1116
     zoomCameraToSelectionActual(data) {
1101
-      const { camera } = data
1117
+      const camera = getCurrentCamera(data)
1102 1118
 
1103 1119
       const bounds = getSelectedBounds(data)
1104 1120
 
@@ -1111,8 +1127,7 @@ const state = createState({
1111 1127
       setZoomCSS(camera.zoom)
1112 1128
     },
1113 1129
     zoomCameraToSelection(data) {
1114
-      const { camera } = data
1115
-
1130
+      const camera = getCurrentCamera(data)
1116 1131
       const bounds = getSelectedBounds(data)
1117 1132
 
1118 1133
       const zoom = getCameraZoom(
@@ -1130,7 +1145,7 @@ const state = createState({
1130 1145
       setZoomCSS(camera.zoom)
1131 1146
     },
1132 1147
     zoomCameraToFit(data) {
1133
-      const { camera } = data
1148
+      const camera = getCurrentCamera(data)
1134 1149
       const page = getPage(data)
1135 1150
 
1136 1151
       const shapes = Object.values(page.shapes)
@@ -1160,7 +1175,7 @@ const state = createState({
1160 1175
       setZoomCSS(camera.zoom)
1161 1176
     },
1162 1177
     zoomCamera(data, payload: { delta: number; point: number[] }) {
1163
-      const { camera } = data
1178
+      const camera = getCurrentCamera(data)
1164 1179
       const next = camera.zoom - (payload.delta / 100) * camera.zoom
1165 1180
 
1166 1181
       const p0 = screenToWorld(payload.point, data)
@@ -1171,7 +1186,7 @@ const state = createState({
1171 1186
       setZoomCSS(camera.zoom)
1172 1187
     },
1173 1188
     panCamera(data, payload: { delta: number[] }) {
1174
-      const { camera } = data
1189
+      const camera = getCurrentCamera(data)
1175 1190
       camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
1176 1191
     },
1177 1192
     pinchCamera(
@@ -1183,8 +1198,7 @@ const state = createState({
1183 1198
         point: number[]
1184 1199
       }
1185 1200
     ) {
1186
-      const { camera } = data
1187
-
1201
+      const camera = getCurrentCamera(data)
1188 1202
       camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
1189 1203
 
1190 1204
       const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
@@ -1197,9 +1211,9 @@ const state = createState({
1197 1211
       setZoomCSS(camera.zoom)
1198 1212
     },
1199 1213
     resetCamera(data) {
1200
-      data.camera.zoom = 1
1201
-      data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
1202
-
1214
+      const camera = getCurrentCamera(data)
1215
+      camera.zoom = 1
1216
+      camera.point = [window.innerWidth / 2, window.innerHeight / 2]
1203 1217
       document.documentElement.style.setProperty('--camera-zoom', '1')
1204 1218
     },
1205 1219
 

+ 9
- 4
types.ts 파일 보기

@@ -19,10 +19,6 @@ export interface Data {
19 19
     isPenLocked: boolean
20 20
   }
21 21
   currentStyle: ShapeStyles
22
-  camera: {
23
-    point: number[]
24
-    zoom: number
25
-  }
26 22
   activeTool: ShapeType | 'select'
27 23
   brush?: Bounds
28 24
   boundsRotation: number
@@ -36,6 +32,15 @@ export interface Data {
36 32
     pages: Record<string, Page>
37 33
     code: Record<string, CodeFile>
38 34
   }
35
+  pageStates: Record<
36
+    string,
37
+    {
38
+      camera: {
39
+        point: number[]
40
+        zoom: number
41
+      }
42
+    }
43
+  >
39 44
 }
40 45
 
41 46
 /* -------------------------------------------------- */

+ 6
- 1
utils/utils.ts 파일 보기

@@ -6,7 +6,8 @@ import _isMobile from 'ismobilejs'
6 6
 import { getShapeUtils } from 'lib/shape-utils'
7 7
 
8 8
 export function screenToWorld(point: number[], data: Data) {
9
-  return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
9
+  const camera = getCurrentCamera(data)
10
+  return vec.sub(vec.div(point, camera.zoom), camera.point)
10 11
 }
11 12
 
12 13
 /**
@@ -1581,3 +1582,7 @@ export function isAngleBetween(a: number, b: number, c: number) {
1581 1582
   const AC = (c - a + PI2) % PI2
1582 1583
   return AB <= Math.PI !== AC > AB
1583 1584
 }
1585
+
1586
+export function getCurrentCamera(data: Data) {
1587
+  return data.pageStates[data.currentPageId].camera
1588
+}

Loading…
취소
저장