浏览代码

Adds boolean props for lock, hide, aspect lock

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

+ 55
- 55
components/code-panel/code-panel.tsx 查看文件

@@ -1,15 +1,15 @@
1 1
 /* eslint-disable @typescript-eslint/ban-ts-comment */
2
-import styled from "styles"
3
-import { useStateDesigner } from "@state-designer/react"
4
-import React, { useEffect, useRef } from "react"
5
-import { motion } from "framer-motion"
6
-import state, { useSelector } from "state"
7
-import { CodeFile } from "types"
8
-import CodeDocs from "./code-docs"
9
-import CodeEditor from "./code-editor"
10
-import { generateFromCode } from "lib/code/generate"
11
-import * as Panel from "../panel"
12
-import { IconButton } from "../shared"
2
+import styled from 'styles'
3
+import { useStateDesigner } from '@state-designer/react'
4
+import React, { useEffect, useRef } from 'react'
5
+import { motion } from 'framer-motion'
6
+import state, { useSelector } from 'state'
7
+import { CodeFile } from 'types'
8
+import CodeDocs from './code-docs'
9
+import CodeEditor from './code-editor'
10
+import { generateFromCode } from 'lib/code/generate'
11
+import * as Panel from '../panel'
12
+import { IconButton } from '../shared'
13 13
 import {
14 14
   X,
15 15
   Code,
@@ -17,10 +17,10 @@ import {
17 17
   PlayCircle,
18 18
   ChevronUp,
19 19
   ChevronDown,
20
-} from "react-feather"
20
+} from 'react-feather'
21 21
 
22 22
 const getErrorLineAndColumn = (e: any) => {
23
-  if ("line" in e) {
23
+  if ('line' in e) {
24 24
     return { line: Number(e.line), column: e.column }
25 25
   }
26 26
 
@@ -46,23 +46,23 @@ export default function CodePanel() {
46 46
       error: null as { message: string; line: number; column: number } | null,
47 47
     },
48 48
     on: {
49
-      MOUNTED: "setCode",
50
-      CHANGED_FILE: "loadFile",
49
+      MOUNTED: 'setCode',
50
+      CHANGED_FILE: 'loadFile',
51 51
     },
52
-    initial: "editingCode",
52
+    initial: 'editingCode',
53 53
     states: {
54 54
       editingCode: {
55 55
         on: {
56
-          RAN_CODE: ["saveCode", "runCode"],
57
-          SAVED_CODE: ["saveCode", "runCode"],
58
-          CHANGED_CODE: { secretlyDo: "setCode" },
59
-          CLEARED_ERROR: { if: "hasError", do: "clearError" },
60
-          TOGGLED_DOCS: { to: "viewingDocs" },
56
+          RAN_CODE: ['saveCode', 'runCode'],
57
+          SAVED_CODE: ['saveCode', 'runCode'],
58
+          CHANGED_CODE: { secretlyDo: 'setCode' },
59
+          CLEARED_ERROR: { if: 'hasError', do: 'clearError' },
60
+          TOGGLED_DOCS: { to: 'viewingDocs' },
61 61
         },
62 62
       },
63 63
       viewingDocs: {
64 64
         on: {
65
-          TOGGLED_DOCS: { to: "editingCode" },
65
+          TOGGLED_DOCS: { to: 'editingCode' },
66 66
         },
67 67
       },
68 68
     },
@@ -83,7 +83,7 @@ export default function CodePanel() {
83 83
 
84 84
         try {
85 85
           const { shapes, controls } = generateFromCode(data.code)
86
-          state.send("GENERATED_FROM_CODE", { shapes, controls })
86
+          state.send('GENERATED_FROM_CODE', { shapes, controls })
87 87
         } catch (e) {
88 88
           console.error(e)
89 89
           error = { message: e.message, ...getErrorLineAndColumn(e) }
@@ -93,7 +93,7 @@ export default function CodePanel() {
93 93
       },
94 94
       saveCode(data) {
95 95
         const { code } = data
96
-        state.send("SAVED_CODE", { code })
96
+        state.send('SAVED_CODE', { code })
97 97
       },
98 98
       clearError(data) {
99 99
         data.error = null
@@ -102,13 +102,13 @@ export default function CodePanel() {
102 102
   })
103 103
 
104 104
   useEffect(() => {
105
-    local.send("CHANGED_FILE", { file })
105
+    local.send('CHANGED_FILE', { file })
106 106
   }, [file])
107 107
 
108 108
   useEffect(() => {
109
-    local.send("MOUNTED", { code: state.data.document.code[fileId].code })
109
+    local.send('MOUNTED', { code: state.data.document.code[fileId].code })
110 110
     return () => {
111
-      state.send("CHANGED_CODE", { fileId, code: local.data.code })
111
+      state.send('CHANGED_CODE', { fileId, code: local.data.code })
112 112
     }
113 113
   }, [])
114 114
 
@@ -118,32 +118,32 @@ export default function CodePanel() {
118 118
     <Panel.Root data-bp-desktop ref={rContainer} isOpen={isOpen}>
119 119
       {isOpen ? (
120 120
         <Panel.Layout>
121
-          <Panel.Header>
122
-            <IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}>
121
+          <Panel.Header side="left">
122
+            <IconButton onClick={() => state.send('TOGGLED_CODE_PANEL_OPEN')}>
123 123
               <X />
124 124
             </IconButton>
125 125
             <h3>Code</h3>
126 126
             <ButtonsGroup>
127 127
               <FontSizeButtons>
128 128
                 <IconButton
129
-                  disabled={!local.isIn("editingCode")}
130
-                  onClick={() => state.send("INCREASED_CODE_FONT_SIZE")}
129
+                  disabled={!local.isIn('editingCode')}
130
+                  onClick={() => state.send('INCREASED_CODE_FONT_SIZE')}
131 131
                 >
132 132
                   <ChevronUp />
133 133
                 </IconButton>
134 134
                 <IconButton
135
-                  disabled={!local.isIn("editingCode")}
136
-                  onClick={() => state.send("DECREASED_CODE_FONT_SIZE")}
135
+                  disabled={!local.isIn('editingCode')}
136
+                  onClick={() => state.send('DECREASED_CODE_FONT_SIZE')}
137 137
                 >
138 138
                   <ChevronDown />
139 139
                 </IconButton>
140 140
               </FontSizeButtons>
141
-              <IconButton onClick={() => local.send("TOGGLED_DOCS")}>
141
+              <IconButton onClick={() => local.send('TOGGLED_DOCS')}>
142 142
                 <Info />
143 143
               </IconButton>
144 144
               <IconButton
145
-                disabled={!local.isIn("editingCode")}
146
-                onClick={() => local.send("SAVED_CODE")}
145
+                disabled={!local.isIn('editingCode')}
146
+                onClick={() => local.send('SAVED_CODE')}
147 147
               >
148 148
                 <PlayCircle />
149 149
               </IconButton>
@@ -155,11 +155,11 @@ export default function CodePanel() {
155 155
               readOnly={isReadOnly}
156 156
               value={file.code}
157 157
               error={error}
158
-              onChange={(code) => local.send("CHANGED_CODE", { code })}
159
-              onSave={() => local.send("SAVED_CODE")}
160
-              onKey={() => local.send("CLEARED_ERROR")}
158
+              onChange={(code) => local.send('CHANGED_CODE', { code })}
159
+              onSave={() => local.send('SAVED_CODE')}
160
+              onKey={() => local.send('CLEARED_ERROR')}
161 161
             />
162
-            <CodeDocs isHidden={!local.isIn("viewingDocs")} />
162
+            <CodeDocs isHidden={!local.isIn('viewingDocs')} />
163 163
           </Panel.Content>
164 164
           <Panel.Footer>
165 165
             {error &&
@@ -169,7 +169,7 @@ export default function CodePanel() {
169 169
           </Panel.Footer>
170 170
         </Panel.Layout>
171 171
       ) : (
172
-        <IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}>
172
+        <IconButton onClick={() => state.send('TOGGLED_CODE_PANEL_OPEN')}>
173 173
           <Code />
174 174
         </IconButton>
175 175
       )}
@@ -177,28 +177,28 @@ export default function CodePanel() {
177 177
   )
178 178
 }
179 179
 
180
-const ButtonsGroup = styled("div", {
181
-  gridRow: "1",
182
-  gridColumn: "3",
183
-  display: "flex",
180
+const ButtonsGroup = styled('div', {
181
+  gridRow: '1',
182
+  gridColumn: '3',
183
+  display: 'flex',
184 184
 })
185 185
 
186
-const FontSizeButtons = styled("div", {
186
+const FontSizeButtons = styled('div', {
187 187
   paddingRight: 4,
188
-  display: "flex",
189
-  flexDirection: "column",
188
+  display: 'flex',
189
+  flexDirection: 'column',
190 190
 
191
-  "& > button": {
192
-    height: "50%",
193
-    "&:nth-of-type(1)": {
194
-      alignItems: "flex-end",
191
+  '& > button': {
192
+    height: '50%',
193
+    '&:nth-of-type(1)': {
194
+      alignItems: 'flex-end',
195 195
     },
196 196
 
197
-    "&:nth-of-type(2)": {
198
-      alignItems: "flex-start",
197
+    '&:nth-of-type(2)': {
198
+      alignItems: 'flex-start',
199 199
     },
200 200
 
201
-    "& svg": {
201
+    '& svg': {
202 202
       height: 12,
203 203
     },
204 204
   },

+ 30
- 27
components/editor.tsx 查看文件

@@ -1,13 +1,14 @@
1
-import useKeyboardEvents from "hooks/useKeyboardEvents"
2
-import useLoadOnMount from "hooks/useLoadOnMount"
3
-import Canvas from "./canvas/canvas"
4
-import StatusBar from "./status-bar"
5
-import Toolbar from "./toolbar"
6
-import CodePanel from "./code-panel/code-panel"
7
-import ControlsPanel from "./controls-panel/controls-panel"
8
-import styled from "styles"
9
-import StylePanel from "./style-panel/style-panel"
10
-import { useSelector } from "state"
1
+import useKeyboardEvents from 'hooks/useKeyboardEvents'
2
+import useLoadOnMount from 'hooks/useLoadOnMount'
3
+import Canvas from './canvas/canvas'
4
+import StatusBar from './status-bar'
5
+import Toolbar from './toolbar'
6
+import CodePanel from './code-panel/code-panel'
7
+import ControlsPanel from './controls-panel/controls-panel'
8
+import ToolsPanel from './tools-panel/tools-panel'
9
+import StylePanel from './style-panel/style-panel'
10
+import { useSelector } from 'state'
11
+import styled from 'styles'
11 12
 
12 13
 export default function Editor() {
13 14
   useKeyboardEvents()
@@ -19,9 +20,8 @@ export default function Editor() {
19 20
 
20 21
   return (
21 22
     <Layout>
22
-      <Canvas />
23
-      <StatusBar />
24 23
       <Toolbar />
24
+      <Canvas />
25 25
       <LeftPanels>
26 26
         <CodePanel />
27 27
         {hasControls && <ControlsPanel />}
@@ -29,40 +29,43 @@ export default function Editor() {
29 29
       <RightPanels>
30 30
         <StylePanel />
31 31
       </RightPanels>
32
+      <ToolsPanel />
33
+      <StatusBar />
32 34
     </Layout>
33 35
   )
34 36
 }
35 37
 
36
-const Layout = styled("div", {
37
-  position: "fixed",
38
+const Layout = styled('div', {
39
+  position: 'fixed',
38 40
   top: 0,
39 41
   left: 0,
40 42
   bottom: 0,
41 43
   right: 0,
42
-  display: "grid",
43
-  gridTemplateRows: "40px 1fr 40px",
44
-  gridTemplateColumns: "minmax(50%, 400px) 1fr auto",
44
+  display: 'grid',
45
+  gridTemplateRows: '40px 1fr auto 40px',
46
+  gridTemplateColumns: 'minmax(50%, 400px) 1fr auto',
45 47
   gridTemplateAreas: `
46 48
     "toolbar toolbar toolbar"
47 49
     "leftPanels main rightPanels"
50
+    "tools tools tools"
48 51
     "statusbar statusbar statusbar"
49 52
   `,
50 53
 })
51 54
 
52
-const LeftPanels = styled("main", {
53
-  display: "grid",
54
-  gridArea: "leftPanels",
55
-  gridTemplateRows: "1fr auto",
55
+const LeftPanels = styled('main', {
56
+  display: 'grid',
57
+  gridArea: 'leftPanels',
58
+  gridTemplateRows: '1fr auto',
56 59
   padding: 8,
57 60
   gap: 8,
58 61
 })
59 62
 
60
-const RightPanels = styled("main", {
61
-  display: "grid",
62
-  gridArea: "rightPanels",
63
-  gridTemplateRows: "auto",
64
-  height: "fit-content",
65
-  justifyContent: "flex-end",
63
+const RightPanels = styled('main', {
64
+  display: 'grid',
65
+  gridArea: 'rightPanels',
66
+  gridTemplateRows: 'auto',
67
+  height: 'fit-content',
68
+  justifyContent: 'flex-end',
66 69
   padding: 8,
67 70
   gap: 8,
68 71
 })

+ 52
- 24
components/shared.tsx 查看文件

@@ -1,32 +1,60 @@
1
-import styled from "styles"
1
+import styled from 'styles'
2 2
 
3
-export const IconButton = styled("button", {
4
-  height: "32px",
5
-  width: "32px",
6
-  backgroundColor: "$panel",
7
-  borderRadius: "4px",
8
-  padding: "0",
9
-  margin: "0",
10
-  display: "flex",
11
-  alignItems: "center",
12
-  justifyContent: "center",
13
-  outline: "none",
14
-  border: "none",
15
-  pointerEvents: "all",
16
-  cursor: "pointer",
3
+export const IconButton = styled('button', {
4
+  height: '32px',
5
+  width: '32px',
6
+  backgroundColor: '$panel',
7
+  borderRadius: '4px',
8
+  padding: '0',
9
+  margin: '0',
10
+  display: 'flex',
11
+  alignItems: 'center',
12
+  justifyContent: 'center',
13
+  outline: 'none',
14
+  border: 'none',
15
+  pointerEvents: 'all',
16
+  cursor: 'pointer',
17 17
 
18
-  "&:hover:not(:disabled)": {
19
-    backgroundColor: "$hover",
18
+  '&:hover:not(:disabled)': {
19
+    backgroundColor: '$hover',
20 20
   },
21 21
 
22
-  "&:disabled": {
23
-    opacity: "0.5",
22
+  '&:disabled': {
23
+    opacity: '0.5',
24 24
   },
25 25
 
26
-  svg: {
27
-    height: "16px",
28
-    width: "16px",
29
-    strokeWidth: "2px",
30
-    stroke: "$text",
26
+  '& > svg': {
27
+    height: '16px',
28
+    width: '16px',
29
+    // strokeWidth: '2px',
30
+    // stroke: '$text',
31
+  },
32
+
33
+  variants: {
34
+    size: {
35
+      medium: {
36
+        height: 44,
37
+        width: 44,
38
+        '& svg': {
39
+          height: 16,
40
+          width: 16,
41
+          strokeWidth: 0,
42
+        },
43
+      },
44
+      large: {
45
+        height: 44,
46
+        width: 44,
47
+        '& svg': {
48
+          height: 24,
49
+          width: 24,
50
+          strokeWidth: 0,
51
+        },
52
+      },
53
+    },
54
+    isActive: {
55
+      true: {
56
+        color: '$selected',
57
+      },
58
+    },
31 59
   },
32 60
 })

+ 72
- 21
components/style-panel/style-panel.tsx 查看文件

@@ -4,14 +4,29 @@ import * as Panel from 'components/panel'
4 4
 import { useRef } from 'react'
5 5
 import { IconButton } from 'components/shared'
6 6
 import { Circle, Copy, Lock, Trash, Unlock, X } from 'react-feather'
7
-import { deepCompare, deepCompareArrays, getSelectedShapes } from 'utils/utils'
7
+import {
8
+  deepCompare,
9
+  deepCompareArrays,
10
+  getPage,
11
+  getSelectedShapes,
12
+} from 'utils/utils'
8 13
 import { shades, fills, strokes } from 'lib/colors'
9 14
 
10 15
 import ColorPicker from './color-picker'
11 16
 import AlignDistribute from './align-distribute'
12 17
 import { ShapeStyles } from 'types'
13 18
 import WidthPicker from './width-picker'
14
-import { CopyIcon } from '@radix-ui/react-icons'
19
+import {
20
+  AspectRatioIcon,
21
+  BoxIcon,
22
+  CopyIcon,
23
+  EyeClosedIcon,
24
+  EyeOpenIcon,
25
+  LockClosedIcon,
26
+  LockOpen1Icon,
27
+  RotateCounterClockwiseIcon,
28
+  TrashIcon,
29
+} from '@radix-ui/react-icons'
15 30
 
16 31
 const fillColors = { ...shades, ...fills }
17 32
 const strokeColors = { ...shades, ...strokes }
@@ -43,31 +58,47 @@ function SelectedShapeStyles({}: {}) {
43 58
     deepCompareArrays
44 59
   )
45 60
 
46
-  const shapesStyle = useSelector((s) => {
61
+  const isAllLocked = useSelector((s) => {
62
+    const page = getPage(s.data)
63
+    return selectedIds.every((id) => page.shapes[id].isLocked)
64
+  })
65
+
66
+  const isAllAspectLocked = useSelector((s) => {
67
+    const page = getPage(s.data)
68
+    return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
69
+  })
70
+
71
+  const isAllHidden = useSelector((s) => {
72
+    const page = getPage(s.data)
73
+    return selectedIds.every((id) => page.shapes[id].isHidden)
74
+  })
75
+
76
+  const commonStyle = useSelector((s) => {
47 77
     const { currentStyle } = s.data
48
-    const shapes = getSelectedShapes(s.data)
49 78
 
50
-    if (shapes.length === 0) {
79
+    if (selectedIds.length === 0) {
51 80
       return currentStyle
52 81
     }
82
+    const page = getPage(s.data)
83
+    const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
53 84
 
54
-    const style: Partial<ShapeStyles> = {}
85
+    const commonStyle: Partial<ShapeStyles> = {}
55 86
     const overrides = new Set<string>([])
56 87
 
57
-    for (const shape of shapes) {
88
+    for (const shapeStyle of shapeStyles) {
58 89
       for (let key in currentStyle) {
59 90
         if (overrides.has(key)) continue
60
-        if (style[key] === undefined) {
61
-          style[key] = shape.style[key]
91
+        if (commonStyle[key] === undefined) {
92
+          commonStyle[key] = shapeStyle[key]
62 93
         } else {
63
-          if (style[key] === shape.style[key]) continue
64
-          style[key] = currentStyle[key]
94
+          if (commonStyle[key] === shapeStyle[key]) continue
95
+          commonStyle[key] = currentStyle[key]
65 96
           overrides.add(key)
66 97
         }
67 98
       }
68 99
     }
69 100
 
70
-    return style
101
+    return commonStyle
71 102
   }, deepCompare)
72 103
 
73 104
   const hasSelection = selectedIds.length > 0
@@ -83,40 +114,60 @@ function SelectedShapeStyles({}: {}) {
83 114
       <Content>
84 115
         <ColorPicker
85 116
           label="Fill"
86
-          color={shapesStyle.fill}
117
+          color={commonStyle.fill}
87 118
           colors={fillColors}
88 119
           onChange={(color) => state.send('CHANGED_STYLE', { fill: color })}
89 120
         />
90 121
         <ColorPicker
91 122
           label="Stroke"
92
-          color={shapesStyle.stroke}
123
+          color={commonStyle.stroke}
93 124
           colors={strokeColors}
94 125
           onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })}
95 126
         />
96 127
         <Row>
97 128
           <label htmlFor="width">Width</label>
98
-          <WidthPicker strokeWidth={Number(shapesStyle.strokeWidth)} />
129
+          <WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
99 130
         </Row>
100 131
         <AlignDistribute
101 132
           hasTwoOrMore={selectedIds.length > 1}
102 133
           hasThreeOrMore={selectedIds.length > 2}
103 134
         />
104 135
         <ButtonsRow>
136
+          <IconButton
137
+            disabled={!hasSelection}
138
+            onClick={() => state.send('DUPLICATED')}
139
+          >
140
+            <CopyIcon />
141
+          </IconButton>
142
+          <IconButton
143
+            disabled={!hasSelection}
144
+            onClick={() => state.send('ROTATED_CCW')}
145
+          >
146
+            <RotateCounterClockwiseIcon />
147
+          </IconButton>
105 148
           <IconButton
106 149
             disabled={!hasSelection}
107 150
             onClick={() => state.send('DELETED')}
108 151
           >
109
-            <Trash />
152
+            <TrashIcon />
110 153
           </IconButton>
111 154
           <IconButton
112 155
             disabled={!hasSelection}
113
-            onClick={() => state.send('DUPLICATED')}
156
+            onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
114 157
           >
115
-            <Copy />
158
+            {isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
116 159
           </IconButton>
117
-
118
-          <IconButton>
119
-            <Unlock />
160
+          <IconButton
161
+            disabled={!hasSelection}
162
+            onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
163
+          >
164
+            {isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
165
+          </IconButton>
166
+          <IconButton
167
+            disabled={!hasSelection}
168
+            onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
169
+          >
170
+            {isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
120 171
           </IconButton>
121 172
         </ButtonsRow>
122 173
       </Content>

+ 1
- 3
components/style-panel/width-picker.tsx 查看文件

@@ -1,6 +1,4 @@
1
-import { DotsHorizontalIcon } from '@radix-ui/react-icons'
2 1
 import * as RadioGroup from '@radix-ui/react-radio-group'
3
-import { IconButton } from 'components/shared'
4 2
 import { ChangeEvent } from 'react'
5 3
 import { Circle } from 'react-feather'
6 4
 import state from 'state'
@@ -26,7 +24,7 @@ export default function WidthPicker({
26 24
         <Circle size={12} />
27 25
       </RadioItem>
28 26
       <RadioItem value="8" isActive={strokeWidth === 8}>
29
-        <Circle size={18} />
27
+        <Circle size={22} />
30 28
       </RadioItem>
31 29
     </Group>
32 30
   )

+ 0
- 57
components/toolbar.tsx 查看文件

@@ -26,63 +26,6 @@ export default function Toolbar() {
26 26
         <Button>
27 27
           <Menu />
28 28
         </Button>
29
-        <Button onClick={() => state.send('TOGGLED_TOOL_LOCK')}>
30
-          {isToolLocked ? <Lock /> : <Unlock />}
31
-        </Button>
32
-        <Button
33
-          isSelected={activeTool === 'select'}
34
-          onClick={() => state.send('SELECTED_SELECT_TOOL')}
35
-        >
36
-          Select
37
-        </Button>
38
-        <Button
39
-          isSelected={activeTool === 'draw'}
40
-          onClick={() => state.send('SELECTED_DRAW_TOOL')}
41
-        >
42
-          Draw
43
-        </Button>
44
-        <Button
45
-          isSelected={activeTool === 'dot'}
46
-          onClick={() => state.send('SELECTED_DOT_TOOL')}
47
-        >
48
-          Dot
49
-        </Button>
50
-        <Button
51
-          isSelected={activeTool === 'circle'}
52
-          onClick={() => state.send('SELECTED_CIRCLE_TOOL')}
53
-        >
54
-          Circle
55
-        </Button>
56
-        <Button
57
-          isSelected={activeTool === 'ellipse'}
58
-          onClick={() => state.send('SELECTED_ELLIPSE_TOOL')}
59
-        >
60
-          Ellipse
61
-        </Button>
62
-        <Button
63
-          isSelected={activeTool === 'ray'}
64
-          onClick={() => state.send('SELECTED_RAY_TOOL')}
65
-        >
66
-          Ray
67
-        </Button>
68
-        <Button
69
-          isSelected={activeTool === 'line'}
70
-          onClick={() => state.send('SELECTED_LINE_TOOL')}
71
-        >
72
-          Line
73
-        </Button>
74
-        <Button
75
-          isSelected={activeTool === 'polyline'}
76
-          onClick={() => state.send('SELECTED_POLYLINE_TOOL')}
77
-        >
78
-          Polyline
79
-        </Button>
80
-        <Button
81
-          isSelected={activeTool === 'rectangle'}
82
-          onClick={() => state.send('SELECTED_RECTANGLE_TOOL')}
83
-        >
84
-          Rectangle
85
-        </Button>
86 29
         <Button onClick={() => state.send('RESET_CAMERA')}>Reset Camera</Button>
87 30
       </Section>
88 31
       <Section>

+ 162
- 0
components/tools-panel/tools-panel.tsx 查看文件

@@ -0,0 +1,162 @@
1
+import {
2
+  CircleIcon,
3
+  CursorArrowIcon,
4
+  DividerHorizontalIcon,
5
+  DotIcon,
6
+  LineHeightIcon,
7
+  LockClosedIcon,
8
+  LockOpen1Icon,
9
+  Pencil1Icon,
10
+  Pencil2Icon,
11
+  SewingPinIcon,
12
+  SquareIcon,
13
+} from '@radix-ui/react-icons'
14
+import { IconButton } from 'components/shared'
15
+import React from 'react'
16
+import state, { useSelector } from 'state'
17
+import styled from 'styles'
18
+import { ShapeType } from 'types'
19
+
20
+const selectSelectTool = () => state.send('SELECTED_SELECT_TOOL')
21
+const selectDrawTool = () => state.send('SELECTED_DRAW_TOOL')
22
+const selectDotTool = () => state.send('SELECTED_DOT_TOOL')
23
+const selectCircleTool = () => state.send('SELECTED_CIRCLE_TOOL')
24
+const selectEllipseTool = () => state.send('SELECTED_ELLIPSE_TOOL')
25
+const selectRayTool = () => state.send('SELECTED_RAY_TOOL')
26
+const selectLineTool = () => state.send('SELECTED_LINE_TOOL')
27
+const selectPolylineTool = () => state.send('SELECTED_POLYLINE_TOOL')
28
+const selectRectangleTool = () => state.send('SELECTED_RECTANGLE_TOOL')
29
+const selectToolLock = () => state.send('TOGGLED_TOOL_LOCK')
30
+
31
+export default function ToolsPanel() {
32
+  const activeTool = useSelector((state) =>
33
+    state.whenIn({
34
+      selecting: 'select',
35
+      dot: ShapeType.Dot,
36
+      circle: ShapeType.Circle,
37
+      ellipse: ShapeType.Ellipse,
38
+      ray: ShapeType.Ray,
39
+      line: ShapeType.Line,
40
+      polyline: ShapeType.Polyline,
41
+      rectangle: ShapeType.Rectangle,
42
+      draw: ShapeType.Draw,
43
+    })
44
+  )
45
+
46
+  const isToolLocked = useSelector((s) => s.data.settings.isToolLocked)
47
+
48
+  const isPenLocked = useSelector((s) => s.data.settings.isPenLocked)
49
+
50
+  return (
51
+    <OuterContainer>
52
+      <Container>
53
+        <IconButton
54
+          name="select"
55
+          size="large"
56
+          onClick={selectSelectTool}
57
+          isActive={activeTool === 'select'}
58
+        >
59
+          <CursorArrowIcon />
60
+        </IconButton>
61
+      </Container>
62
+      <Container>
63
+        <IconButton
64
+          name={ShapeType.Draw}
65
+          size="large"
66
+          onClick={selectDrawTool}
67
+          isActive={activeTool === ShapeType.Draw}
68
+        >
69
+          <Pencil1Icon />
70
+        </IconButton>
71
+        <IconButton
72
+          name={ShapeType.Rectangle}
73
+          size="large"
74
+          onClick={selectRectangleTool}
75
+          isActive={activeTool === ShapeType.Rectangle}
76
+        >
77
+          <SquareIcon />
78
+        </IconButton>
79
+        <IconButton
80
+          name={ShapeType.Circle}
81
+          size="large"
82
+          onClick={selectCircleTool}
83
+          isActive={activeTool === ShapeType.Circle}
84
+        >
85
+          <CircleIcon />
86
+        </IconButton>
87
+        <IconButton
88
+          name={ShapeType.Ellipse}
89
+          size="large"
90
+          onClick={selectEllipseTool}
91
+          isActive={activeTool === ShapeType.Ellipse}
92
+        >
93
+          <CircleIcon transform="rotate(-45) scale(1, .8)" />
94
+        </IconButton>
95
+        <IconButton
96
+          name={ShapeType.Line}
97
+          size="large"
98
+          onClick={selectLineTool}
99
+          isActive={activeTool === ShapeType.Line}
100
+        >
101
+          <DividerHorizontalIcon transform="rotate(-45)" />
102
+        </IconButton>
103
+        <IconButton
104
+          name={ShapeType.Ray}
105
+          size="large"
106
+          onClick={selectRayTool}
107
+          isActive={activeTool === ShapeType.Ray}
108
+        >
109
+          <SewingPinIcon transform="rotate(-135)" />
110
+        </IconButton>
111
+        <IconButton
112
+          name={ShapeType.Dot}
113
+          size="large"
114
+          onClick={selectDotTool}
115
+          isActive={activeTool === ShapeType.Dot}
116
+        >
117
+          <DotIcon />
118
+        </IconButton>
119
+      </Container>
120
+      <Container>
121
+        <IconButton size="medium" onClick={selectToolLock}>
122
+          {isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
123
+        </IconButton>
124
+        {isPenLocked && (
125
+          <IconButton size="medium" onClick={selectToolLock}>
126
+            <Pencil2Icon />
127
+          </IconButton>
128
+        )}
129
+      </Container>
130
+    </OuterContainer>
131
+  )
132
+}
133
+
134
+const OuterContainer = styled('div', {
135
+  gridArea: 'tools',
136
+  padding: '0 8px 12px 8px',
137
+  height: '100%',
138
+  width: '100%',
139
+  display: 'flex',
140
+  alignItems: 'center',
141
+  justifyContent: 'center',
142
+  gap: 16,
143
+})
144
+
145
+const Container = styled('div', {
146
+  position: 'relative',
147
+  backgroundColor: '$panel',
148
+  borderRadius: '4px',
149
+  overflow: 'hidden',
150
+  border: '1px solid $border',
151
+  pointerEvents: 'all',
152
+  userSelect: 'none',
153
+  zIndex: 200,
154
+  boxShadow: '0px 2px 25px rgba(0,0,0,.16)',
155
+  height: '100%',
156
+  display: 'flex',
157
+  padding: 4,
158
+
159
+  '& svg': {
160
+    strokeWidth: 0,
161
+  },
162
+})

+ 11
- 8
lib/code/circle.ts 查看文件

@@ -1,7 +1,7 @@
1
-import CodeShape from "./index"
2
-import { v4 as uuid } from "uuid"
3
-import { CircleShape, ShapeType } from "types"
4
-import { vectorToPoint } from "utils/utils"
1
+import CodeShape from './index'
2
+import { v4 as uuid } from 'uuid'
3
+import { CircleShape, ShapeType } from 'types'
4
+import { vectorToPoint } from 'utils/utils'
5 5
 
6 6
 export default class Circle extends CodeShape<CircleShape> {
7 7
   constructor(props = {} as Partial<CircleShape>) {
@@ -11,15 +11,18 @@ export default class Circle extends CodeShape<CircleShape> {
11 11
       id: uuid(),
12 12
       type: ShapeType.Circle,
13 13
       isGenerated: true,
14
-      name: "Circle",
15
-      parentId: "page0",
14
+      name: 'Circle',
15
+      parentId: 'page0',
16 16
       childIndex: 0,
17 17
       point: [0, 0],
18 18
       rotation: 0,
19 19
       radius: 20,
20
+      isAspectRatioLocked: false,
21
+      isLocked: false,
22
+      isHidden: false,
20 23
       style: {
21
-        fill: "#c6cacb",
22
-        stroke: "#000",
24
+        fill: '#c6cacb',
25
+        stroke: '#000',
23 26
         strokeWidth: 1,
24 27
       },
25 28
       ...props,

+ 11
- 8
lib/code/dot.ts 查看文件

@@ -1,7 +1,7 @@
1
-import CodeShape from "./index"
2
-import { v4 as uuid } from "uuid"
3
-import { DotShape, ShapeType } from "types"
4
-import { vectorToPoint } from "utils/utils"
1
+import CodeShape from './index'
2
+import { v4 as uuid } from 'uuid'
3
+import { DotShape, ShapeType } from 'types'
4
+import { vectorToPoint } from 'utils/utils'
5 5
 
6 6
 export default class Dot extends CodeShape<DotShape> {
7 7
   constructor(props = {} as Partial<DotShape>) {
@@ -11,14 +11,17 @@ export default class Dot extends CodeShape<DotShape> {
11 11
       id: uuid(),
12 12
       type: ShapeType.Dot,
13 13
       isGenerated: true,
14
-      name: "Dot",
15
-      parentId: "page0",
14
+      name: 'Dot',
15
+      parentId: 'page0',
16 16
       childIndex: 0,
17 17
       point: [0, 0],
18 18
       rotation: 0,
19
+      isAspectRatioLocked: false,
20
+      isLocked: false,
21
+      isHidden: false,
19 22
       style: {
20
-        fill: "#c6cacb",
21
-        stroke: "#000",
23
+        fill: '#c6cacb',
24
+        stroke: '#000',
22 25
         strokeWidth: 1,
23 26
       },
24 27
       ...props,

+ 11
- 8
lib/code/ellipse.ts 查看文件

@@ -1,7 +1,7 @@
1
-import CodeShape from "./index"
2
-import { v4 as uuid } from "uuid"
3
-import { EllipseShape, ShapeType } from "types"
4
-import { vectorToPoint } from "utils/utils"
1
+import CodeShape from './index'
2
+import { v4 as uuid } from 'uuid'
3
+import { EllipseShape, ShapeType } from 'types'
4
+import { vectorToPoint } from 'utils/utils'
5 5
 
6 6
 export default class Ellipse extends CodeShape<EllipseShape> {
7 7
   constructor(props = {} as Partial<EllipseShape>) {
@@ -11,16 +11,19 @@ export default class Ellipse extends CodeShape<EllipseShape> {
11 11
       id: uuid(),
12 12
       type: ShapeType.Ellipse,
13 13
       isGenerated: true,
14
-      name: "Ellipse",
15
-      parentId: "page0",
14
+      name: 'Ellipse',
15
+      parentId: 'page0',
16 16
       childIndex: 0,
17 17
       point: [0, 0],
18 18
       radiusX: 20,
19 19
       radiusY: 20,
20 20
       rotation: 0,
21
+      isAspectRatioLocked: false,
22
+      isLocked: false,
23
+      isHidden: false,
21 24
       style: {
22
-        fill: "#c6cacb",
23
-        stroke: "#000",
25
+        fill: '#c6cacb',
26
+        stroke: '#000',
24 27
         strokeWidth: 1,
25 28
       },
26 29
       ...props,

+ 11
- 8
lib/code/line.ts 查看文件

@@ -1,7 +1,7 @@
1
-import CodeShape from "./index"
2
-import { v4 as uuid } from "uuid"
3
-import { LineShape, ShapeType } from "types"
4
-import { vectorToPoint } from "utils/utils"
1
+import CodeShape from './index'
2
+import { v4 as uuid } from 'uuid'
3
+import { LineShape, ShapeType } from 'types'
4
+import { vectorToPoint } from 'utils/utils'
5 5
 
6 6
 export default class Line extends CodeShape<LineShape> {
7 7
   constructor(props = {} as Partial<LineShape>) {
@@ -12,15 +12,18 @@ export default class Line extends CodeShape<LineShape> {
12 12
       id: uuid(),
13 13
       type: ShapeType.Line,
14 14
       isGenerated: true,
15
-      name: "Line",
16
-      parentId: "page0",
15
+      name: 'Line',
16
+      parentId: 'page0',
17 17
       childIndex: 0,
18 18
       point: [0, 0],
19 19
       direction: [-0.5, 0.5],
20 20
       rotation: 0,
21
+      isAspectRatioLocked: false,
22
+      isLocked: false,
23
+      isHidden: false,
21 24
       style: {
22
-        fill: "#c6cacb",
23
-        stroke: "#000",
25
+        fill: '#c6cacb',
26
+        stroke: '#000',
24 27
         strokeWidth: 1,
25 28
       },
26 29
       ...props,

+ 11
- 8
lib/code/polyline.ts 查看文件

@@ -1,7 +1,7 @@
1
-import CodeShape from "./index"
2
-import { v4 as uuid } from "uuid"
3
-import { PolylineShape, ShapeType } from "types"
4
-import { vectorToPoint } from "utils/utils"
1
+import CodeShape from './index'
2
+import { v4 as uuid } from 'uuid'
3
+import { PolylineShape, ShapeType } from 'types'
4
+import { vectorToPoint } from 'utils/utils'
5 5
 
6 6
 export default class Polyline extends CodeShape<PolylineShape> {
7 7
   constructor(props = {} as Partial<PolylineShape>) {
@@ -12,15 +12,18 @@ export default class Polyline extends CodeShape<PolylineShape> {
12 12
       id: uuid(),
13 13
       type: ShapeType.Polyline,
14 14
       isGenerated: true,
15
-      name: "Polyline",
16
-      parentId: "page0",
15
+      name: 'Polyline',
16
+      parentId: 'page0',
17 17
       childIndex: 0,
18 18
       point: [0, 0],
19 19
       points: [[0, 0]],
20 20
       rotation: 0,
21
+      isAspectRatioLocked: false,
22
+      isLocked: false,
23
+      isHidden: false,
21 24
       style: {
22
-        fill: "none",
23
-        stroke: "#000",
25
+        fill: 'none',
26
+        stroke: '#000',
24 27
         strokeWidth: 1,
25 28
       },
26 29
       ...props,

+ 11
- 8
lib/code/ray.ts 查看文件

@@ -1,7 +1,7 @@
1
-import CodeShape from "./index"
2
-import { v4 as uuid } from "uuid"
3
-import { RayShape, ShapeType } from "types"
4
-import { vectorToPoint } from "utils/utils"
1
+import CodeShape from './index'
2
+import { v4 as uuid } from 'uuid'
3
+import { RayShape, ShapeType } from 'types'
4
+import { vectorToPoint } from 'utils/utils'
5 5
 
6 6
 export default class Ray extends CodeShape<RayShape> {
7 7
   constructor(props = {} as Partial<RayShape>) {
@@ -12,15 +12,18 @@ export default class Ray extends CodeShape<RayShape> {
12 12
       id: uuid(),
13 13
       type: ShapeType.Ray,
14 14
       isGenerated: true,
15
-      name: "Ray",
16
-      parentId: "page0",
15
+      name: 'Ray',
16
+      parentId: 'page0',
17 17
       childIndex: 0,
18 18
       point: [0, 0],
19 19
       direction: [0, 1],
20 20
       rotation: 0,
21
+      isAspectRatioLocked: false,
22
+      isLocked: false,
23
+      isHidden: false,
21 24
       style: {
22
-        fill: "#c6cacb",
23
-        stroke: "#000",
25
+        fill: '#c6cacb',
26
+        stroke: '#000',
24 27
         strokeWidth: 1,
25 28
       },
26 29
       ...props,

+ 11
- 8
lib/code/rectangle.ts 查看文件

@@ -1,7 +1,7 @@
1
-import CodeShape from "./index"
2
-import { v4 as uuid } from "uuid"
3
-import { RectangleShape, ShapeType } from "types"
4
-import { vectorToPoint } from "utils/utils"
1
+import CodeShape from './index'
2
+import { v4 as uuid } from 'uuid'
3
+import { RectangleShape, ShapeType } from 'types'
4
+import { vectorToPoint } from 'utils/utils'
5 5
 
6 6
 export default class Rectangle extends CodeShape<RectangleShape> {
7 7
   constructor(props = {} as Partial<RectangleShape>) {
@@ -12,16 +12,19 @@ export default class Rectangle extends CodeShape<RectangleShape> {
12 12
       id: uuid(),
13 13
       type: ShapeType.Rectangle,
14 14
       isGenerated: true,
15
-      name: "Rectangle",
16
-      parentId: "page0",
15
+      name: 'Rectangle',
16
+      parentId: 'page0',
17 17
       childIndex: 0,
18 18
       point: [0, 0],
19 19
       size: [100, 100],
20 20
       rotation: 0,
21 21
       radius: 2,
22
+      isAspectRatioLocked: false,
23
+      isLocked: false,
24
+      isHidden: false,
22 25
       style: {
23
-        fill: "#c6cacb",
24
-        stroke: "#000",
26
+        fill: '#c6cacb',
27
+        stroke: '#000',
25 28
         strokeWidth: 1,
26 29
       },
27 30
       ...props,

+ 5
- 7
lib/shape-utils/circle.tsx 查看文件

@@ -21,6 +21,9 @@ const circle = registerShapeUtils<CircleShape>({
21 21
       point: [0, 0],
22 22
       rotation: 0,
23 23
       radius: 1,
24
+      isAspectRatioLocked: false,
25
+      isLocked: false,
26
+      isHidden: false,
24 27
       style: {
25 28
         fill: '#c6cacb',
26 29
         stroke: '#000',
@@ -125,13 +128,8 @@ const circle = registerShapeUtils<CircleShape>({
125 128
     return this
126 129
   },
127 130
 
128
-  setParent(shape, parentId) {
129
-    shape.parentId = parentId
130
-    return this
131
-  },
132
-
133
-  setChildIndex(shape, childIndex) {
134
-    shape.childIndex = childIndex
131
+  setProperty(shape, prop, value) {
132
+    shape[prop] = value
135 133
     return this
136 134
   },
137 135
 

+ 5
- 7
lib/shape-utils/dot.tsx 查看文件

@@ -20,6 +20,9 @@ const dot = registerShapeUtils<DotShape>({
20 20
       childIndex: 0,
21 21
       point: [0, 0],
22 22
       rotation: 0,
23
+      isAspectRatioLocked: false,
24
+      isLocked: false,
25
+      isHidden: false,
23 26
       style: {
24 27
         fill: '#c6cacb',
25 28
         strokeWidth: '0',
@@ -94,13 +97,8 @@ const dot = registerShapeUtils<DotShape>({
94 97
     return this
95 98
   },
96 99
 
97
-  setParent(shape, parentId) {
98
-    shape.parentId = parentId
99
-    return this
100
-  },
101
-
102
-  setChildIndex(shape, childIndex) {
103
-    shape.childIndex = childIndex
100
+  setProperty(shape, prop, value) {
101
+    shape[prop] = value
104 102
     return this
105 103
   },
106 104
 

+ 5
- 19
lib/shape-utils/draw.tsx 查看文件

@@ -28,6 +28,9 @@ const draw = registerShapeUtils<DrawShape>({
28 28
       point: [0, 0],
29 29
       points: [[0, 0]],
30 30
       rotation: 0,
31
+      isAspectRatioLocked: false,
32
+      isLocked: false,
33
+      isHidden: false,
31 34
       ...props,
32 35
       style: {
33 36
         strokeWidth: 2,
@@ -169,25 +172,8 @@ const draw = registerShapeUtils<DrawShape>({
169 172
     return this
170 173
   },
171 174
 
172
-  setParent(shape, parentId) {
173
-    shape.parentId = parentId
174
-    return this
175
-  },
176
-
177
-  setChildIndex(shape, childIndex) {
178
-    shape.childIndex = childIndex
179
-    return this
180
-  },
181
-
182
-  setPoints(shape, points) {
183
-    // const bounds = getBoundsFromPoints(points)
184
-    // const corner = [bounds.minX, bounds.minY]
185
-    // const nudged = points.map((point) => vec.sub(point, corner))
186
-    // this.boundsCache.set(shape, translategetBoundsFromPoints(nudged))
187
-    // shape.point = vec.add(shape.point, corner)
188
-
189
-    shape.points = points
190
-
175
+  setProperty(shape, prop, value) {
176
+    shape[prop] = value
191 177
     return this
192 178
   },
193 179
 

+ 5
- 7
lib/shape-utils/ellipse.tsx 查看文件

@@ -27,6 +27,9 @@ const ellipse = registerShapeUtils<EllipseShape>({
27 27
       radiusX: 1,
28 28
       radiusY: 1,
29 29
       rotation: 0,
30
+      isAspectRatioLocked: false,
31
+      isLocked: false,
32
+      isHidden: false,
30 33
       style: {
31 34
         fill: '#c6cacb',
32 35
         stroke: '#000',
@@ -137,13 +140,8 @@ const ellipse = registerShapeUtils<EllipseShape>({
137 140
     return this.transform(shape, bounds, info)
138 141
   },
139 142
 
140
-  setParent(shape, parentId) {
141
-    shape.parentId = parentId
142
-    return this
143
-  },
144
-
145
-  setChildIndex(shape, childIndex) {
146
-    shape.childIndex = childIndex
143
+  setProperty(shape, prop, value) {
144
+    shape[prop] = value
147 145
     return this
148 146
   },
149 147
 

+ 14
- 23
lib/shape-utils/index.tsx 查看文件

@@ -8,15 +8,16 @@ import {
8 8
   Edge,
9 9
   ShapeByType,
10 10
   ShapeStyles,
11
-} from "types"
12
-import circle from "./circle"
13
-import dot from "./dot"
14
-import polyline from "./polyline"
15
-import rectangle from "./rectangle"
16
-import ellipse from "./ellipse"
17
-import line from "./line"
18
-import ray from "./ray"
19
-import draw from "./draw"
11
+  PropsOfType,
12
+} from 'types'
13
+import circle from './circle'
14
+import dot from './dot'
15
+import polyline from './polyline'
16
+import rectangle from './rectangle'
17
+import ellipse from './ellipse'
18
+import line from './line'
19
+import ray from './ray'
20
+import draw from './draw'
20 21
 
21 22
 /*
22 23
 Shape Utiliies
@@ -82,21 +83,11 @@ export interface ShapeUtility<K extends Readonly<Shape>> {
82 83
     }
83 84
   ): ShapeUtility<K>
84 85
 
85
-  // Move a shape to a new parent.
86
-  setParent(this: ShapeUtility<K>, shape: K, parentId: string): ShapeUtility<K>
87
-
88
-  // Change the child index of a shape
89
-  setChildIndex(
90
-    this: ShapeUtility<K>,
91
-    shape: K,
92
-    childIndex: number
93
-  ): ShapeUtility<K>
94
-
95
-  // Add a point
96
-  setPoints?(
86
+  setProperty<P extends keyof K>(
97 87
     this: ShapeUtility<K>,
98 88
     shape: K,
99
-    points: number[][]
89
+    prop: P,
90
+    value: K[P]
100 91
   ): ShapeUtility<K>
101 92
 
102 93
   // Render a shape to JSX.
@@ -151,7 +142,7 @@ export function registerShapeUtils<T extends Shape>(
151 142
 }
152 143
 
153 144
 export function createShape<T extends Shape>(
154
-  type: T["type"],
145
+  type: T['type'],
155 146
   props: Partial<T>
156 147
 ) {
157 148
   return shapeUtilityMap[type].create(props) as T

+ 5
- 7
lib/shape-utils/line.tsx 查看文件

@@ -21,6 +21,9 @@ const line = registerShapeUtils<LineShape>({
21 21
       point: [0, 0],
22 22
       direction: [0, 0],
23 23
       rotation: 0,
24
+      isAspectRatioLocked: false,
25
+      isLocked: false,
26
+      isHidden: false,
24 27
       style: {
25 28
         fill: '#c6cacb',
26 29
         stroke: '#000',
@@ -102,13 +105,8 @@ const line = registerShapeUtils<LineShape>({
102 105
     return this.transform(shape, bounds, info)
103 106
   },
104 107
 
105
-  setParent(shape, parentId) {
106
-    shape.parentId = parentId
107
-    return this
108
-  },
109
-
110
-  setChildIndex(shape, childIndex) {
111
-    shape.childIndex = childIndex
108
+  setProperty(shape, prop, value) {
109
+    shape[prop] = value
112 110
     return this
113 111
   },
114 112
 

+ 5
- 7
lib/shape-utils/polyline.tsx 查看文件

@@ -20,6 +20,9 @@ const polyline = registerShapeUtils<PolylineShape>({
20 20
       point: [0, 0],
21 21
       points: [[0, 0]],
22 22
       rotation: 0,
23
+      isAspectRatioLocked: false,
24
+      isLocked: false,
25
+      isHidden: false,
23 26
       style: {
24 27
         strokeWidth: 2,
25 28
         strokeLinecap: 'round',
@@ -127,13 +130,8 @@ const polyline = registerShapeUtils<PolylineShape>({
127 130
     return this
128 131
   },
129 132
 
130
-  setParent(shape, parentId) {
131
-    shape.parentId = parentId
132
-    return this
133
-  },
134
-
135
-  setChildIndex(shape, childIndex) {
136
-    shape.childIndex = childIndex
133
+  setProperty(shape, prop, value) {
134
+    shape[prop] = value
137 135
     return this
138 136
   },
139 137
 

+ 5
- 7
lib/shape-utils/ray.tsx 查看文件

@@ -21,6 +21,9 @@ const ray = registerShapeUtils<RayShape>({
21 21
       point: [0, 0],
22 22
       direction: [0, 1],
23 23
       rotation: 0,
24
+      isAspectRatioLocked: false,
25
+      isLocked: false,
26
+      isHidden: false,
24 27
       style: {
25 28
         fill: '#c6cacb',
26 29
         stroke: '#000',
@@ -102,13 +105,8 @@ const ray = registerShapeUtils<RayShape>({
102 105
     return this.transform(shape, bounds, info)
103 106
   },
104 107
 
105
-  setParent(shape, parentId) {
106
-    shape.parentId = parentId
107
-    return this
108
-  },
109
-
110
-  setChildIndex(shape, childIndex) {
111
-    shape.childIndex = childIndex
108
+  setProperty(shape, prop, value) {
109
+    shape[prop] = value
112 110
     return this
113 111
   },
114 112
 

+ 5
- 7
lib/shape-utils/rectangle.tsx 查看文件

@@ -24,6 +24,9 @@ const rectangle = registerShapeUtils<RectangleShape>({
24 24
       size: [1, 1],
25 25
       radius: 2,
26 26
       rotation: 0,
27
+      isAspectRatioLocked: false,
28
+      isLocked: false,
29
+      isHidden: false,
27 30
       style: {
28 31
         fill: '#c6cacb',
29 32
         stroke: '#000',
@@ -140,13 +143,8 @@ const rectangle = registerShapeUtils<RectangleShape>({
140 143
     return this
141 144
   },
142 145
 
143
-  setParent(shape, parentId) {
144
-    shape.parentId = parentId
145
-    return this
146
-  },
147
-
148
-  setChildIndex(shape, childIndex) {
149
-    shape.childIndex = childIndex
146
+  setProperty(shape, prop, value) {
147
+    shape[prop] = value
150 148
     return this
151 149
   },
152 150
 

+ 13
- 13
lib/shape-utils/utils-next.tsx 查看文件

@@ -9,16 +9,16 @@ interface Core {
9 9
 interface Instance extends Props, Core {}
10 10
 
11 11
 const defaults: Props = {
12
-  name: "Spot",
12
+  name: 'Spot',
13 13
 }
14 14
 
15 15
 const core: Core = {
16
-  id: "0",
16
+  id: '0',
17 17
 }
18 18
 
19 19
 class ClassInstance<T extends object = {}> implements Instance {
20
-  id = "0"
21
-  name = "Spot"
20
+  id = '0'
21
+  name = 'Spot'
22 22
 
23 23
   constructor(
24 24
     props: Partial<Props> &
@@ -51,7 +51,7 @@ function getInstance<T extends object = {}>(
51 51
 }
52 52
 
53 53
 const instance = getInstance({
54
-  name: "Steve",
54
+  name: 'Steve',
55 55
   age: 93,
56 56
   wag(this: Instance) {
57 57
     return this.name
@@ -76,29 +76,29 @@ const getAnimal = <T extends object>(
76 76
 ): Animal & T => {
77 77
   return {
78 78
     // Defaults
79
-    name: "Animal",
79
+    name: 'Animal',
80 80
     greet(name) {
81
-      return "Hey " + name
81
+      return 'Hey ' + name
82 82
     },
83 83
     // Overrides
84 84
     ...props,
85 85
     // Core
86
-    id: "hi",
86
+    id: 'hi',
87 87
     sleep() {},
88 88
   }
89 89
 }
90 90
 
91 91
 const dog = getAnimal({
92
-  name: "doggo",
92
+  name: 'doggo',
93 93
   greet(name) {
94
-    return "Woof " + this.name
94
+    return 'Woof ' + this.name
95 95
   },
96 96
   wag() {
97
-    return "wagging..."
97
+    return 'wagging...'
98 98
   },
99 99
 })
100 100
 
101
-dog.greet("steve")
101
+dog.greet('steve')
102 102
 dog.wag()
103 103
 dog.sleep()
104 104
 
@@ -111,5 +111,5 @@ export default shapeTest
111 111
 type Greet = (name: string) => string
112 112
 
113 113
 const greet: Greet = (name: string | number) => {
114
-  return "hello " + name
114
+  return 'hello ' + name
115 115
 }

+ 12
- 16
state/commands/draw.ts 查看文件

@@ -1,24 +1,20 @@
1
-import Command from "./command"
2
-import history from "../history"
3
-import { Data } from "types"
4
-import { getPage } from "utils/utils"
5
-import { getShapeUtils } from "lib/shape-utils"
6
-import { current } from "immer"
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data, DrawShape } from 'types'
4
+import { getPage } from 'utils/utils'
5
+import { getShapeUtils } from 'lib/shape-utils'
6
+import { current } from 'immer'
7 7
 
8
-export default function drawCommand(
9
-  data: Data,
10
-  id: string,
11
-  before: number[][],
12
-  after: number[][]
13
-) {
14
-  const restoreShape = current(getPage(data).shapes[id])
15
-  getShapeUtils(restoreShape).setPoints!(restoreShape, after)
8
+export default function drawCommand(data: Data, id: string, after: number[][]) {
9
+  const restoreShape = current(getPage(data)).shapes[id] as DrawShape
10
+
11
+  getShapeUtils(restoreShape).setProperty!(restoreShape, 'points', after)
16 12
 
17 13
   history.execute(
18 14
     data,
19 15
     new Command({
20
-      name: "set_points",
21
-      category: "canvas",
16
+      name: 'set_points',
17
+      category: 'canvas',
22 18
       manualSelection: true,
23 19
       do(data, initial) {
24 20
         if (!initial) {

+ 2
- 0
state/commands/index.ts 查看文件

@@ -13,6 +13,7 @@ import transform from './transform'
13 13
 import transformSingle from './transform-single'
14 14
 import translate from './translate'
15 15
 import nudge from './nudge'
16
+import toggle from './toggle'
16 17
 
17 18
 const commands = {
18 19
   align,
@@ -30,6 +31,7 @@ const commands = {
30 31
   transformSingle,
31 32
   translate,
32 33
   nudge,
34
+  toggle,
33 35
 }
34 36
 
35 37
 export default commands

+ 46
- 0
state/commands/toggle.ts 查看文件

@@ -0,0 +1,46 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data, Shape } from 'types'
4
+import { getPage, getSelectedShapes } from 'utils/utils'
5
+import { getShapeUtils } from 'lib/shape-utils'
6
+import { PropsOfType } from 'types'
7
+
8
+export default function toggleCommand(
9
+  data: Data,
10
+  prop: PropsOfType<Shape, boolean>
11
+) {
12
+  const { currentPageId } = data
13
+  const selectedShapes = getSelectedShapes(data)
14
+  const isAllToggled = selectedShapes.every((shape) => shape[prop])
15
+  const initialShapes = Object.fromEntries(
16
+    selectedShapes.map((shape) => [shape.id, shape[prop]])
17
+  )
18
+
19
+  history.execute(
20
+    data,
21
+    new Command({
22
+      name: 'hide_shapes',
23
+      category: 'canvas',
24
+      do(data) {
25
+        const { shapes } = getPage(data, currentPageId)
26
+
27
+        for (const id in initialShapes) {
28
+          const shape = shapes[id]
29
+          getShapeUtils(shape).setProperty(
30
+            shape,
31
+            prop,
32
+            isAllToggled ? false : true
33
+          )
34
+        }
35
+      },
36
+      undo(data) {
37
+        const { shapes } = getPage(data, currentPageId)
38
+
39
+        for (const id in initialShapes) {
40
+          const shape = shapes[id]
41
+          getShapeUtils(shape).setProperty(shape, prop, initialShapes[id])
42
+        }
43
+      },
44
+    })
45
+  )
46
+}

+ 16
- 1
state/state.ts 查看文件

@@ -75,6 +75,12 @@ const state = createState({
75 75
     PANNED_CAMERA: {
76 76
       do: 'panCamera',
77 77
     },
78
+    TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
79
+    TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
80
+    TOGGLED_SHAPE_ASPECT_LOCK: {
81
+      if: 'hasSelection',
82
+      do: 'aspectLockSelection',
83
+    },
78 84
     SELECTED_SELECT_TOOL: { to: 'selecting' },
79 85
     SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
80 86
     SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
@@ -638,7 +644,7 @@ const state = createState({
638 644
         ? siblings[siblings.length - 1].childIndex + 1
639 645
         : 1
640 646
 
641
-      getShapeUtils(shape).setChildIndex(shape, childIndex)
647
+      getShapeUtils(shape).setProperty(shape, 'childIndex', childIndex)
642 648
 
643 649
       getPage(data).shapes[shape.id] = shape
644 650
 
@@ -834,6 +840,15 @@ const state = createState({
834 840
     duplicateSelection(data) {
835 841
       commands.duplicate(data)
836 842
     },
843
+    lockSelection(data) {
844
+      commands.toggle(data, 'isLocked')
845
+    },
846
+    hideSelection(data) {
847
+      commands.toggle(data, 'isHidden')
848
+    },
849
+    aspectLockSelection(data) {
850
+      commands.toggle(data, 'isAspectRatioLocked')
851
+    },
837 852
 
838 853
     /* --------------------- Camera --------------------- */
839 854
 

+ 7
- 0
types.ts 查看文件

@@ -78,6 +78,9 @@ export interface BaseShape {
78 78
   point: number[]
79 79
   rotation: number
80 80
   style: ShapeStyles
81
+  isLocked: boolean
82
+  isHidden: boolean
83
+  isAspectRatioLocked: boolean
81 84
 }
82 85
 
83 86
 export interface DotShape extends BaseShape {
@@ -313,3 +316,7 @@ export type CodeControl =
313 316
   | VectorCodeControl
314 317
   | TextCodeControl
315 318
   | SelectCodeControl
319
+
320
+export type PropsOfType<T extends object, K> = {
321
+  [K in keyof T]: T[K] extends boolean ? K : never
322
+}[keyof T]

正在加载...
取消
保存