소스 검색

Adds toolbar, tools shortcuts, dot creation state

main
Steve Ruiz 4 년 전
부모
커밋
5420b0365f
8개의 변경된 파일271개의 추가작업 그리고 10개의 파일을 삭제
  1. 1
    1
      components/code-panel/code-panel.tsx
  2. 3
    1
      components/editor.tsx
  3. 128
    0
      components/toolbar.tsx
  4. 37
    3
      hooks/useKeyboardEvents.ts
  5. 10
    3
      lib/shapes/ray.tsx
  6. 12
    0
      state/data.ts
  7. 75
    2
      state/state.ts
  8. 5
    0
      utils/utils.ts

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

@@ -180,7 +180,7 @@ export default function CodePanel() {
180 180
 
181 181
 const PanelContainer = styled(motion.div, {
182 182
   position: "absolute",
183
-  top: "8px",
183
+  top: "48px",
184 184
   right: "8px",
185 185
   bottom: "48px",
186 186
   backgroundColor: "$panel",

+ 3
- 1
components/editor.tsx 파일 보기

@@ -1,6 +1,7 @@
1 1
 import useKeyboardEvents from "hooks/useKeyboardEvents"
2 2
 import Canvas from "./canvas/canvas"
3 3
 import StatusBar from "./status-bar"
4
+import Toolbar from "./toolbar"
4 5
 import CodePanel from "./code-panel/code-panel"
5 6
 
6 7
 export default function Editor() {
@@ -10,7 +11,8 @@ export default function Editor() {
10 11
     <>
11 12
       <Canvas />
12 13
       <StatusBar />
13
-      <CodePanel />
14
+      <Toolbar />
15
+      {/* <CodePanel /> */}
14 16
     </>
15 17
   )
16 18
 }

+ 128
- 0
components/toolbar.tsx 파일 보기

@@ -0,0 +1,128 @@
1
+import state, { useSelector } from "state"
2
+import styled from "styles"
3
+import { Menu } from "react-feather"
4
+
5
+export default function Toolbar() {
6
+  const activeTool = useSelector((state) =>
7
+    state.whenIn({
8
+      selecting: "select",
9
+      creatingDot: "dot",
10
+      creatingCircle: "circle",
11
+      creatingEllipse: "ellipse",
12
+      creatingRay: "ray",
13
+      creatingLine: "line",
14
+      creatingPolyline: "polyline",
15
+      creatingRectangle: "rectangle",
16
+    })
17
+  )
18
+
19
+  return (
20
+    <ToolbarContainer>
21
+      <Section>
22
+        <Button>
23
+          <Menu />
24
+        </Button>
25
+        <Button
26
+          isSelected={activeTool === "select"}
27
+          onClick={() => state.send("SELECTED_SELECT_TOOL")}
28
+        >
29
+          Select
30
+        </Button>
31
+        <Button
32
+          isSelected={activeTool === "dot"}
33
+          onClick={() => state.send("SELECTED_DOT_TOOL")}
34
+        >
35
+          Dot
36
+        </Button>
37
+        <Button
38
+          isSelected={activeTool === "circle"}
39
+          onClick={() => state.send("SELECTED_CIRCLE_TOOL")}
40
+        >
41
+          Circle
42
+        </Button>
43
+        <Button
44
+          isSelected={activeTool === "ellipse"}
45
+          onClick={() => state.send("SELECTED_ELLIPSE_TOOL")}
46
+        >
47
+          Ellipse
48
+        </Button>
49
+        <Button
50
+          isSelected={activeTool === "ray"}
51
+          onClick={() => state.send("SELECTED_RAY_TOOL")}
52
+        >
53
+          Ray
54
+        </Button>
55
+        <Button
56
+          isSelected={activeTool === "line"}
57
+          onClick={() => state.send("SELECTED_LINE_TOOL")}
58
+        >
59
+          Line
60
+        </Button>
61
+        <Button
62
+          isSelected={activeTool === "polyline"}
63
+          onClick={() => state.send("SELECTED_POLYLINE_TOOL")}
64
+        >
65
+          Polyline
66
+        </Button>
67
+        <Button
68
+          isSelected={activeTool === "rectangle"}
69
+          onClick={() => state.send("SELECTED_RECTANGLE_TOOL")}
70
+        >
71
+          Rectangle
72
+        </Button>
73
+      </Section>
74
+    </ToolbarContainer>
75
+  )
76
+}
77
+
78
+const ToolbarContainer = styled("div", {
79
+  position: "absolute",
80
+  top: 0,
81
+  left: 0,
82
+  width: "100%",
83
+  height: 40,
84
+  userSelect: "none",
85
+  borderBottom: "1px solid black",
86
+  gridArea: "status",
87
+  display: "grid",
88
+  gridTemplateColumns: "auto 1fr auto",
89
+  alignItems: "center",
90
+  backgroundColor: "white",
91
+  gap: 8,
92
+  fontSize: "$1",
93
+  zIndex: 200,
94
+})
95
+
96
+const Section = styled("div", {
97
+  whiteSpace: "nowrap",
98
+  overflow: "hidden",
99
+  display: "flex",
100
+})
101
+
102
+const Button = styled("button", {
103
+  display: "flex",
104
+  alignItems: "center",
105
+  cursor: "pointer",
106
+  font: "$ui",
107
+  fontSize: "$ui",
108
+  height: "40px",
109
+  borderRadius: 0,
110
+  border: "none",
111
+  padding: "0 12px",
112
+  background: "none",
113
+  "&:hover": {
114
+    backgroundColor: "$hint",
115
+  },
116
+  "& svg": {
117
+    height: 16,
118
+    width: 16,
119
+  },
120
+  variants: {
121
+    isSelected: {
122
+      true: {
123
+        color: "$selected",
124
+      },
125
+      false: {},
126
+    },
127
+  },
128
+})

+ 37
- 3
hooks/useKeyboardEvents.ts 파일 보기

@@ -1,13 +1,13 @@
1 1
 import { useEffect } from "react"
2 2
 import state from "state"
3
-import { getKeyboardEventInfo, isDarwin } from "utils/utils"
3
+import { getKeyboardEventInfo, isDarwin, metaKey } from "utils/utils"
4 4
 
5 5
 export default function useKeyboardEvents() {
6 6
   useEffect(() => {
7 7
     function handleKeyDown(e: KeyboardEvent) {
8 8
       if (e.key === "Escape") {
9 9
         state.send("CANCELLED")
10
-      } else if (e.key === "z" && (isDarwin() ? e.metaKey : e.ctrlKey)) {
10
+      } else if (e.key === "z" && metaKey(e)) {
11 11
         if (e.shiftKey) {
12 12
           state.send("REDO")
13 13
         } else {
@@ -15,7 +15,41 @@ export default function useKeyboardEvents() {
15 15
         }
16 16
       }
17 17
 
18
-      state.send("PRESSED_KEY", getKeyboardEventInfo(e))
18
+      if (e.key === "Backspace" && !(metaKey(e) || e.shiftKey || e.altKey)) {
19
+        state.send("DELETED", getKeyboardEventInfo(e))
20
+      }
21
+
22
+      if (e.key === "v" && !(metaKey(e) || e.shiftKey || e.altKey)) {
23
+        state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
24
+      }
25
+
26
+      if (e.key === "d" && !(metaKey(e) || e.shiftKey || e.altKey)) {
27
+        state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e))
28
+      }
29
+
30
+      if (e.key === "c" && !(metaKey(e) || e.shiftKey || e.altKey)) {
31
+        state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e))
32
+      }
33
+
34
+      if (e.key === "i" && !(metaKey(e) || e.shiftKey || e.altKey)) {
35
+        state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e))
36
+      }
37
+
38
+      if (e.key === "l" && !(metaKey(e) || e.shiftKey || e.altKey)) {
39
+        state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e))
40
+      }
41
+
42
+      if (e.key === "y" && !(metaKey(e) || e.shiftKey || e.altKey)) {
43
+        state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e))
44
+      }
45
+
46
+      if (e.key === "p" && !(metaKey(e) || e.shiftKey || e.altKey)) {
47
+        state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e))
48
+      }
49
+
50
+      if (e.key === "r" && !(metaKey(e) || e.shiftKey || e.altKey)) {
51
+        state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e))
52
+      }
19 53
     }
20 54
 
21 55
     function handleKeyUp(e: KeyboardEvent) {

+ 10
- 3
lib/shapes/ray.tsx 파일 보기

@@ -24,8 +24,15 @@ const ray = createShape<RayShape>({
24 24
     }
25 25
   },
26 26
 
27
-  render({ id }) {
28
-    return <circle id={id} cx={4} cy={4} r={4} />
27
+  render({ id, direction }) {
28
+    const [x2, y2] = vec.add([0, 0], vec.mul(direction, 100000))
29
+
30
+    return (
31
+      <g id={id}>
32
+        <line x1={0} y1={0} x2={x2} y2={y2} />
33
+        <circle cx={0} cy={0} r={4} />
34
+      </g>
35
+    )
29 36
   },
30 37
 
31 38
   getBounds(shape) {
@@ -52,7 +59,7 @@ const ray = createShape<RayShape>({
52 59
   },
53 60
 
54 61
   hitTest(shape, test) {
55
-    return vec.dist(shape.point, test) < 4
62
+    return true
56 63
   },
57 64
 
58 65
   hitTestBounds(this, shape, brushBounds) {

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

@@ -9,6 +9,18 @@ export const defaultDocument: Data["document"] = {
9 9
       name: "Page 0",
10 10
       childIndex: 0,
11 11
       shapes: {
12
+        rayShape: shapeUtils[ShapeType.Ray].create({
13
+          id: "rayShape",
14
+          name: "Ray",
15
+          childIndex: 3,
16
+          point: [300, 300],
17
+          direction: [0.5, 0.5],
18
+          style: {
19
+            fill: "#AAA",
20
+            stroke: "#777",
21
+            strokeWidth: 1,
22
+          },
23
+        }),
12 24
         // shape3: shapeUtils[ShapeType.Dot].create({
13 25
         //   id: "shape3",
14 26
         //   name: "Shape 3",

+ 75
- 2
state/state.ts 파일 보기

@@ -1,9 +1,17 @@
1 1
 import { createSelectorHook, createState } from "@state-designer/react"
2 2
 import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
3 3
 import * as vec from "utils/vec"
4
-import { Data, PointerInfo, Shape, TransformCorner, TransformEdge } from "types"
4
+import {
5
+  Data,
6
+  PointerInfo,
7
+  Shape,
8
+  ShapeType,
9
+  Shapes,
10
+  TransformCorner,
11
+  TransformEdge,
12
+} from "types"
5 13
 import { defaultDocument } from "./data"
6
-import { getShapeUtils } from "lib/shapes"
14
+import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
7 15
 import history from "state/history"
8 16
 import * as Sessions from "./sessions"
9 17
 import commands from "./commands"
@@ -35,6 +43,14 @@ const state = createState({
35 43
     PANNED_CAMERA: {
36 44
       do: "panCamera",
37 45
     },
46
+    SELECTED_SELECT_TOOL: { to: "selecting" },
47
+    SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "creatingDot" },
48
+    SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "creatingCircle" },
49
+    SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "creatingEllipse" },
50
+    SELECTED_RAY_TOOL: { unless: "isReadOnly", to: "creatingRay" },
51
+    SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "creatingLine" },
52
+    SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "creatingPolyline" },
53
+    SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "creatingRectangle" },
38 54
   },
39 55
   initial: "selecting",
40 56
   states: {
@@ -42,6 +58,7 @@ const state = createState({
42 58
       on: {
43 59
         UNDO: { do: "undo" },
44 60
         REDO: { do: "redo" },
61
+        DELETED: { do: "deleteSelection" },
45 62
         GENERATED_SHAPES_FROM_CODE: "setGeneratedShapes",
46 63
         INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
47 64
         DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
@@ -136,6 +153,37 @@ const state = createState({
136 153
         },
137 154
       },
138 155
     },
156
+    creatingDot: {
157
+      initial: "creating",
158
+      states: {
159
+        creating: {
160
+          on: {
161
+            POINTED_CANVAS: {
162
+              do: "createDot",
163
+              to: "creatingDot.positioning",
164
+            },
165
+          },
166
+        },
167
+        positioning: {
168
+          onEnter: "startTranslateSession",
169
+          on: {
170
+            MOVED_POINTER: "updateTranslateSession",
171
+            PANNED_CAMERA: "updateTranslateSession",
172
+            STOPPED_POINTING: { do: "completeSession", to: "selecting" },
173
+            CANCELLED: {
174
+              do: ["cancelSession", "deleteSelection"],
175
+              to: "selecting",
176
+            },
177
+          },
178
+        },
179
+      },
180
+    },
181
+    creatingCircle: {},
182
+    creatingEllipse: {},
183
+    creatingRay: {},
184
+    creatingLine: {},
185
+    creatingPolyline: {},
186
+    creatingRectangle: {},
139 187
   },
140 188
   conditions: {
141 189
     isPointingBounds(data, payload: PointerInfo) {
@@ -167,6 +215,17 @@ const state = createState({
167 215
     },
168 216
   },
169 217
   actions: {
218
+    // Shapes
219
+    createDot(data, payload: PointerInfo) {
220
+      const shape = shapeUtilityMap[ShapeType.Dot].create({
221
+        point: screenToWorld(payload.point, data),
222
+      })
223
+
224
+      data.selectedIds.clear()
225
+      data.selectedIds.add(shape.id)
226
+      data.document.pages[data.currentPageId].shapes[shape.id] = shape
227
+    },
228
+
170 229
     // History
171 230
     enableHistory() {
172 231
       history.enable()
@@ -240,6 +299,20 @@ const state = createState({
240 299
     },
241 300
 
242 301
     // Selection
302
+    deleteSelection(data) {
303
+      const { document, currentPageId } = data
304
+      const shapes = document.pages[currentPageId].shapes
305
+
306
+      data.selectedIds.forEach((id) => {
307
+        delete shapes[id]
308
+        // TODO: recursively delete children
309
+      })
310
+
311
+      data.selectedIds.clear()
312
+      data.hoveredId = undefined
313
+      data.pointedId = undefined
314
+    },
315
+
243 316
     setHoveredId(data, payload: PointerInfo) {
244 317
       data.hoveredId = payload.target
245 318
     },

+ 5
- 0
utils/utils.ts 파일 보기

@@ -1,3 +1,4 @@
1
+import React from "react"
1 2
 import { Data, Bounds, TransformEdge, TransformCorner } from "types"
2 3
 import * as svg from "./svg"
3 4
 import * as vec from "./vec"
@@ -892,6 +893,10 @@ export function isDarwin() {
892 893
   return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
893 894
 }
894 895
 
896
+export function metaKey(e: KeyboardEvent | React.KeyboardEvent) {
897
+  return isDarwin() ? e.metaKey : e.ctrlKey
898
+}
899
+
895 900
 export function getTransformAnchor(
896 901
   type: TransformEdge | TransformCorner,
897 902
   isFlippedX: boolean,

Loading…
취소
저장