Pārlūkot izejas kodu

Adds drawing

main
Steve Ruiz 4 gadus atpakaļ
vecāks
revīzija
7ef83dc508

+ 17
- 15
components/panel.tsx Parādīt failu

@@ -31,7 +31,7 @@ export const Layout = styled("div", {
31 31
   gridTemplateRows: "auto 1fr",
32 32
   gridAutoRows: "28px",
33 33
   height: "100%",
34
-  width: "100%",
34
+  width: "auto",
35 35
   minWidth: "100%",
36 36
   maxWidth: 560,
37 37
   overflow: "hidden",
@@ -41,30 +41,32 @@ export const Layout = styled("div", {
41 41
 
42 42
 export const Header = styled("div", {
43 43
   pointerEvents: "all",
44
-  display: "grid",
45
-  gridTemplateColumns: "auto 1fr auto",
44
+  display: "flex",
45
+  width: "100%",
46 46
   alignItems: "center",
47
-  justifyContent: "center",
47
+  justifyContent: "space-between",
48 48
   borderBottom: "1px solid $border",
49
-
50
-  "& button": {
51
-    gridColumn: "1",
52
-    gridRow: "1",
53
-  },
49
+  position: "relative",
54 50
 
55 51
   "& h3": {
56
-    gridColumn: "1 / span 3",
57
-    gridRow: "1",
52
+    position: "absolute",
53
+    top: 0,
54
+    left: 0,
55
+    width: "100%",
56
+    height: "100%",
58 57
     textAlign: "center",
59
-    margin: "0",
60
-    padding: "0",
58
+    padding: 0,
59
+    margin: 0,
60
+    display: "flex",
61
+    justifyContent: "center",
62
+    alignItems: "center",
61 63
     fontSize: "13px",
64
+    pointerEvents: "none",
65
+    userSelect: "none",
62 66
   },
63 67
 })
64 68
 
65 69
 export const ButtonsGroup = styled("div", {
66
-  gridRow: "1",
67
-  gridColumn: "3",
68 70
   display: "flex",
69 71
 })
70 72
 

+ 2
- 2
components/style-panel/color-picker.tsx Parādīt failu

@@ -98,8 +98,8 @@ const CurrentColor = styled(DropdownMenu.Trigger, {
98 98
     content: "''",
99 99
     position: "absolute",
100 100
     top: 0,
101
-    left: 4,
102
-    right: 4,
101
+    left: 0,
102
+    right: 0,
103 103
     bottom: 0,
104 104
     pointerEvents: "none",
105 105
     zIndex: -1,

+ 4
- 3
components/style-panel/style-panel.tsx Parādīt failu

@@ -73,9 +73,6 @@ function SelectedShapeStyles({}: {}) {
73 73
   return (
74 74
     <Panel.Layout>
75 75
       <Panel.Header>
76
-        <IconButton onClick={() => state.send("TOGGLED_STYLE_PANEL_OPEN")}>
77
-          <X />
78
-        </IconButton>
79 76
         <h3>Style</h3>
80 77
         <Panel.ButtonsGroup>
81 78
           <IconButton
@@ -85,6 +82,9 @@ function SelectedShapeStyles({}: {}) {
85 82
             <Trash />
86 83
           </IconButton>
87 84
         </Panel.ButtonsGroup>
85
+        <IconButton onClick={() => state.send("TOGGLED_STYLE_PANEL_OPEN")}>
86
+          <X />
87
+        </IconButton>
88 88
       </Panel.Header>
89 89
       <Content>
90 90
         <ColorPicker
@@ -112,6 +112,7 @@ const StylePanelRoot = styled(Panel.Root, {
112 112
   minWidth: 1,
113 113
   width: 184,
114 114
   maxWidth: 184,
115
+  overflow: "hidden",
115 116
   position: "relative",
116 117
 
117 118
   variants: {

+ 7
- 0
components/toolbar.tsx Parādīt failu

@@ -13,6 +13,7 @@ export default function Toolbar() {
13 13
       line: "line",
14 14
       polyline: "polyline",
15 15
       rectangle: "rectangle",
16
+      draw: "draw",
16 17
     })
17 18
   )
18 19
 
@@ -28,6 +29,12 @@ export default function Toolbar() {
28 29
         >
29 30
           Select
30 31
         </Button>
32
+        <Button
33
+          isSelected={activeTool === "draw"}
34
+          onClick={() => state.send("SELECTED_DRAW_TOOL")}
35
+        >
36
+          Draw
37
+        </Button>
31 38
         <Button
32 39
           isSelected={activeTool === "dot"}
33 40
           onClick={() => state.send("SELECTED_DOT_TOOL")}

+ 4
- 0
hooks/useKeyboardEvents.ts Parādīt failu

@@ -115,6 +115,10 @@ export default function useKeyboardEvents() {
115 115
           break
116 116
         }
117 117
         case "d": {
118
+          state.send("SELECTED_DRAW_TOOL", getKeyboardEventInfo(e))
119
+          break
120
+        }
121
+        case "t": {
118 122
           if (metaKey(e)) {
119 123
             state.send("DUPLICATED", getKeyboardEventInfo(e))
120 124
           } else {

+ 162
- 0
lib/shape-utils/draw.tsx Parādīt failu

@@ -0,0 +1,162 @@
1
+import { v4 as uuid } from "uuid"
2
+import * as vec from "utils/vec"
3
+import { DrawShape, ShapeType } from "types"
4
+import { registerShapeUtils } from "./index"
5
+import { intersectPolylineBounds } from "utils/intersections"
6
+import { boundsContainPolygon } from "utils/bounds"
7
+import { getBoundsFromPoints, translateBounds } from "utils/utils"
8
+
9
+const draw = registerShapeUtils<DrawShape>({
10
+  boundsCache: new WeakMap([]),
11
+
12
+  create(props) {
13
+    return {
14
+      id: uuid(),
15
+      type: ShapeType.Draw,
16
+      isGenerated: false,
17
+      name: "Draw",
18
+      parentId: "page0",
19
+      childIndex: 0,
20
+      point: [0, 0],
21
+      points: [[0, 0]],
22
+      rotation: 0,
23
+      ...props,
24
+      style: {
25
+        strokeWidth: 2,
26
+        strokeLinecap: "round",
27
+        strokeLinejoin: "round",
28
+        ...props.style,
29
+        fill: "transparent",
30
+      },
31
+    }
32
+  },
33
+
34
+  render({ id, points }) {
35
+    return <polyline id={id} points={points.toString()} />
36
+  },
37
+
38
+  applyStyles(shape, style) {
39
+    Object.assign(shape.style, style)
40
+    shape.style.fill = "transparent"
41
+    return this
42
+  },
43
+
44
+  getBounds(shape) {
45
+    if (!this.boundsCache.has(shape)) {
46
+      const bounds = getBoundsFromPoints(shape.points)
47
+      this.boundsCache.set(shape, bounds)
48
+    }
49
+
50
+    return translateBounds(this.boundsCache.get(shape), shape.point)
51
+  },
52
+
53
+  getRotatedBounds(shape) {
54
+    return this.getBounds(shape)
55
+  },
56
+
57
+  getCenter(shape) {
58
+    const bounds = this.getBounds(shape)
59
+    return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
60
+  },
61
+
62
+  hitTest(shape, point) {
63
+    let pt = vec.sub(point, shape.point)
64
+    let prev = shape.points[0]
65
+
66
+    for (let i = 1; i < shape.points.length; i++) {
67
+      let curr = shape.points[i]
68
+      if (vec.distanceToLineSegment(prev, curr, pt) < 4) {
69
+        return true
70
+      }
71
+      prev = curr
72
+    }
73
+
74
+    return false
75
+  },
76
+
77
+  hitTestBounds(this, shape, brushBounds) {
78
+    const b = this.getBounds(shape)
79
+    const center = [b.minX + b.width / 2, b.minY + b.height / 2]
80
+
81
+    const rotatedCorners = [
82
+      [b.minX, b.minY],
83
+      [b.maxX, b.minY],
84
+      [b.maxX, b.maxY],
85
+      [b.minX, b.maxY],
86
+    ].map((point) => vec.rotWith(point, center, shape.rotation))
87
+
88
+    return (
89
+      boundsContainPolygon(brushBounds, rotatedCorners) ||
90
+      intersectPolylineBounds(
91
+        shape.points.map((point) => vec.add(point, shape.point)),
92
+        brushBounds
93
+      ).length > 0
94
+    )
95
+  },
96
+
97
+  rotateTo(shape, rotation) {
98
+    shape.rotation = rotation
99
+    return this
100
+  },
101
+
102
+  translateTo(shape, point) {
103
+    shape.point = point
104
+    return this
105
+  },
106
+
107
+  transform(shape, bounds, { initialShape, scaleX, scaleY }) {
108
+    const initialShapeBounds = this.boundsCache.get(initialShape)
109
+    shape.points = initialShape.points.map(([x, y]) => {
110
+      return [
111
+        bounds.width *
112
+          (scaleX < 0
113
+            ? 1 - x / initialShapeBounds.width
114
+            : x / initialShapeBounds.width),
115
+        bounds.height *
116
+          (scaleY < 0
117
+            ? 1 - y / initialShapeBounds.height
118
+            : y / initialShapeBounds.height),
119
+      ]
120
+    })
121
+
122
+    const newBounds = getBoundsFromPoints(shape.points)
123
+
124
+    shape.point = vec.sub(
125
+      [bounds.minX, bounds.minY],
126
+      [newBounds.minX, newBounds.minY]
127
+    )
128
+    return this
129
+  },
130
+
131
+  transformSingle(shape, bounds, info) {
132
+    this.transform(shape, bounds, info)
133
+    return this
134
+  },
135
+
136
+  setParent(shape, parentId) {
137
+    shape.parentId = parentId
138
+    return this
139
+  },
140
+
141
+  setChildIndex(shape, childIndex) {
142
+    shape.childIndex = childIndex
143
+    return this
144
+  },
145
+
146
+  setPoints(shape, points) {
147
+    // const bounds = getBoundsFromPoints(points)
148
+    // const corner = [bounds.minX, bounds.minY]
149
+    // const nudged = points.map((point) => vec.sub(point, corner))
150
+    // this.boundsCache.set(shape, translategetBoundsFromPoints(nudged))
151
+    // shape.point = vec.add(shape.point, corner)
152
+
153
+    shape.points = points
154
+
155
+    return this
156
+  },
157
+
158
+  canTransform: true,
159
+  canChangeAspectRatio: true,
160
+})
161
+
162
+export default draw

+ 9
- 0
lib/shape-utils/index.tsx Parādīt failu

@@ -16,6 +16,7 @@ import rectangle from "./rectangle"
16 16
 import ellipse from "./ellipse"
17 17
 import line from "./line"
18 18
 import ray from "./ray"
19
+import draw from "./draw"
19 20
 
20 21
 /*
21 22
 Shape Utiliies
@@ -91,6 +92,13 @@ export interface ShapeUtility<K extends Readonly<Shape>> {
91 92
     childIndex: number
92 93
   ): ShapeUtility<K>
93 94
 
95
+  // Add a point
96
+  setPoints?(
97
+    this: ShapeUtility<K>,
98
+    shape: K,
99
+    points: number[][]
100
+  ): ShapeUtility<K>
101
+
94 102
   // Render a shape to JSX.
95 103
   render(this: ShapeUtility<K>, shape: K): JSX.Element
96 104
 
@@ -119,6 +127,7 @@ const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
119 127
   [ShapeType.Ellipse]: ellipse,
120 128
   [ShapeType.Line]: line,
121 129
   [ShapeType.Ray]: ray,
130
+  [ShapeType.Draw]: draw,
122 131
 }
123 132
 
124 133
 /**

+ 2
- 0
state/commands/index.ts Parādīt failu

@@ -4,6 +4,7 @@ import direct from "./direct"
4 4
 import distribute from "./distribute"
5 5
 import generate from "./generate"
6 6
 import move from "./move"
7
+import points from "./points"
7 8
 import rotate from "./rotate"
8 9
 import stretch from "./stretch"
9 10
 import style from "./style"
@@ -18,6 +19,7 @@ const commands = {
18 19
   distribute,
19 20
   generate,
20 21
   move,
22
+  points,
21 23
   rotate,
22 24
   stretch,
23 25
   style,

+ 28
- 0
state/commands/points.ts Parādīt failu

@@ -0,0 +1,28 @@
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
+
7
+export default function pointsCommand(
8
+  data: Data,
9
+  id: string,
10
+  before: number[][],
11
+  after: number[][]
12
+) {
13
+  history.execute(
14
+    data,
15
+    new Command({
16
+      name: "set_points",
17
+      category: "canvas",
18
+      do(data) {
19
+        const shape = getPage(data).shapes[id]
20
+        getShapeUtils(shape).setPoints!(shape, after)
21
+      },
22
+      undo(data) {
23
+        const shape = getPage(data).shapes[id]
24
+        getShapeUtils(shape).setPoints!(shape, before)
25
+      },
26
+    })
27
+  )
28
+}

+ 57
- 0
state/sessions/draw-session.ts Parādīt failu

@@ -0,0 +1,57 @@
1
+import { current } from "immer"
2
+import { Data, DrawShape } from "types"
3
+import BaseSession from "./base-session"
4
+import { getShapeUtils } from "lib/shape-utils"
5
+import { getPage } from "utils/utils"
6
+import * as vec from "utils/vec"
7
+import commands from "state/commands"
8
+
9
+export default class BrushSession extends BaseSession {
10
+  origin: number[]
11
+  points: number[][]
12
+  snapshot: DrawSnapshot
13
+  shapeId: string
14
+
15
+  constructor(data: Data, id: string, point: number[]) {
16
+    super(data)
17
+    this.shapeId = id
18
+    this.origin = point
19
+    this.points = [[0, 0]]
20
+    this.snapshot = getDrawSnapshot(data, id)
21
+
22
+    const page = getPage(data)
23
+    const shape = page.shapes[id]
24
+    getShapeUtils(shape).translateTo(shape, point)
25
+  }
26
+
27
+  update = (data: Data, point: number[]) => {
28
+    const { shapeId } = this
29
+
30
+    this.points.push(vec.sub(point, this.origin))
31
+
32
+    const page = getPage(data)
33
+    const shape = page.shapes[shapeId]
34
+    getShapeUtils(shape).setPoints!(shape, [...this.points])
35
+  }
36
+
37
+  cancel = (data: Data) => {
38
+    const { shapeId, snapshot } = this
39
+    const page = getPage(data)
40
+    const shape = page.shapes[shapeId]
41
+    getShapeUtils(shape).setPoints!(shape, snapshot.points)
42
+  }
43
+
44
+  complete = (data: Data) => {
45
+    commands.points(data, this.shapeId, this.snapshot.points, this.points)
46
+  }
47
+}
48
+
49
+export function getDrawSnapshot(data: Data, shapeId: string) {
50
+  const page = getPage(current(data))
51
+  const { points } = page.shapes[shapeId] as DrawShape
52
+  return {
53
+    points,
54
+  }
55
+}
56
+
57
+export type DrawSnapshot = ReturnType<typeof getDrawSnapshot>

+ 9
- 7
state/sessions/index.ts Parādīt failu

@@ -1,17 +1,19 @@
1 1
 import BaseSession from "./base-session"
2 2
 import BrushSession from "./brush-session"
3
-import TranslateSession from "./translate-session"
4
-import TransformSession from "./transform-session"
5
-import TransformSingleSession from "./transform-single-session"
6 3
 import DirectionSession from "./direction-session"
4
+import DrawSession from "./draw-session"
7 5
 import RotateSession from "./rotate-session"
6
+import TransformSession from "./transform-session"
7
+import TransformSingleSession from "./transform-single-session"
8
+import TranslateSession from "./translate-session"
8 9
 
9 10
 export {
10
-  BrushSession,
11 11
   BaseSession,
12
-  TranslateSession,
13
-  TransformSession,
14
-  TransformSingleSession,
12
+  BrushSession,
15 13
   DirectionSession,
14
+  DrawSession,
16 15
   RotateSession,
16
+  TransformSession,
17
+  TransformSingleSession,
18
+  TranslateSession,
17 19
 }

+ 44
- 0
state/state.ts Parādīt failu

@@ -31,6 +31,7 @@ import {
31 31
   DistributeType,
32 32
   AlignType,
33 33
   StretchType,
34
+  DrawShape,
34 35
 } from "types"
35 36
 
36 37
 const initialData: Data = {
@@ -70,6 +71,7 @@ const state = createState({
70 71
       do: "panCamera",
71 72
     },
72 73
     SELECTED_SELECT_TOOL: { to: "selecting" },
74
+    SELECTED_DRAW_TOOL: { unless: "isReadOnly", to: "draw" },
73 75
     SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "dot" },
74 76
     SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "circle" },
75 77
     SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "ellipse" },
@@ -246,6 +248,32 @@ const state = createState({
246 248
             },
247 249
           },
248 250
         },
251
+        draw: {
252
+          initial: "creating",
253
+          states: {
254
+            creating: {
255
+              on: {
256
+                POINTED_CANVAS: {
257
+                  get: "newDraw",
258
+                  do: "createShape",
259
+                  to: "draw.editing",
260
+                },
261
+              },
262
+            },
263
+            editing: {
264
+              onEnter: "startDrawSession",
265
+              on: {
266
+                STOPPED_POINTING: { do: "completeSession", to: "selecting" },
267
+                CANCELLED: {
268
+                  do: ["cancelSession", "deleteSelectedIds"],
269
+                  to: "selecting",
270
+                },
271
+                MOVED_POINTER: "updateDrawSession",
272
+                PANNED_CAMERA: "updateDrawSession",
273
+              },
274
+            },
275
+          },
276
+        },
249 277
         dot: {
250 278
           initial: "creating",
251 279
           states: {
@@ -451,6 +479,9 @@ const state = createState({
451 479
     },
452 480
   },
453 481
   results: {
482
+    newDraw() {
483
+      return ShapeType.Draw
484
+    },
454 485
     newDot() {
455 486
       return ShapeType.Dot
456 487
     },
@@ -646,6 +677,19 @@ const state = createState({
646 677
       session.update(data, screenToWorld(payload.point, data))
647 678
     },
648 679
 
680
+    // Drawing
681
+    startDrawSession(data) {
682
+      const id = Array.from(data.selectedIds.values())[0]
683
+      session = new Sessions.DrawSession(
684
+        data,
685
+        id,
686
+        screenToWorld(inputs.pointer.origin, data)
687
+      )
688
+    },
689
+    updateDrawSession(data, payload: PointerInfo) {
690
+      session.update(data, screenToWorld(payload.point, data))
691
+    },
692
+
649 693
     /* -------------------- Selection ------------------- */
650 694
 
651 695
     selectAll(data) {

+ 8
- 0
types.ts Parādīt failu

@@ -53,6 +53,7 @@ export enum ShapeType {
53 53
   Ray = "ray",
54 54
   Polyline = "polyline",
55 55
   Rectangle = "rectangle",
56
+  Draw = "draw",
56 57
 }
57 58
 
58 59
 // Consider:
@@ -111,6 +112,11 @@ export interface RectangleShape extends BaseShape {
111 112
   radius: number
112 113
 }
113 114
 
115
+export interface DrawShape extends BaseShape {
116
+  type: ShapeType.Draw
117
+  points: number[][]
118
+}
119
+
114 120
 export type MutableShape =
115 121
   | DotShape
116 122
   | CircleShape
@@ -118,6 +124,7 @@ export type MutableShape =
118 124
   | LineShape
119 125
   | RayShape
120 126
   | PolylineShape
127
+  | DrawShape
121 128
   | RectangleShape
122 129
 
123 130
 export type Shape = Readonly<MutableShape>
@@ -129,6 +136,7 @@ export interface Shapes {
129 136
   [ShapeType.Line]: Readonly<LineShape>
130 137
   [ShapeType.Ray]: Readonly<RayShape>
131 138
   [ShapeType.Polyline]: Readonly<PolylineShape>
139
+  [ShapeType.Draw]: Readonly<DrawShape>
132 140
   [ShapeType.Rectangle]: Readonly<RectangleShape>
133 141
 }
134 142
 

Notiek ielāde…
Atcelt
Saglabāt