Steve Ruiz пре 3 година
родитељ
комит
f11c35e941

+ 4
- 0
components/canvas/canvas.tsx Прегледај датотеку

@@ -56,6 +56,10 @@ const MainSVG = styled("svg", {
56 56
   height: "100%",
57 57
   touchAction: "none",
58 58
   zIndex: 100,
59
+
60
+  "& *": {
61
+    userSelect: "none",
62
+  },
59 63
 })
60 64
 
61 65
 const MainGroup = styled("g", {})

+ 5
- 4
components/canvas/page.tsx Прегледај датотеку

@@ -9,10 +9,11 @@ here; and still cheaper than any other pattern I've found.
9 9
 */
10 10
 
11 11
 export default function Page() {
12
-  const currentPageShapeIds = useSelector(
13
-    ({ data }) => Object.keys(getPage(data).shapes),
14
-    deepCompareArrays
15
-  )
12
+  const currentPageShapeIds = useSelector(({ data }) => {
13
+    return Object.values(getPage(data).shapes)
14
+      .sort((a, b) => a.childIndex - b.childIndex)
15
+      .map((shape) => shape.id)
16
+  }, deepCompareArrays)
16 17
 
17 18
   return (
18 19
     <>

+ 1
- 1
components/canvas/shape.tsx Прегледај датотеку

@@ -1,8 +1,8 @@
1 1
 import React, { useCallback, useRef, memo } from "react"
2 2
 import state, { useSelector } from "state"
3 3
 import inputs from "state/inputs"
4
-import { getShapeUtils } from "lib/shape-utils"
5 4
 import styled from "styles"
5
+import { getShapeUtils } from "lib/shape-utils"
6 6
 import { getPage } from "utils/utils"
7 7
 
8 8
 function Shape({ id }: { id: string }) {

+ 123
- 59
hooks/useKeyboardEvents.ts Прегледај датотеку

@@ -1,71 +1,135 @@
1 1
 import { useEffect } from "react"
2 2
 import state from "state"
3
-import { getKeyboardEventInfo, isDarwin, metaKey } from "utils/utils"
3
+import { getKeyboardEventInfo, metaKey } from "utils/utils"
4 4
 
5 5
 export default function useKeyboardEvents() {
6 6
   useEffect(() => {
7 7
     function handleKeyDown(e: KeyboardEvent) {
8
-      if (e.key === "Escape") {
9
-        state.send("CANCELLED")
10
-      } else if (e.key === "z" && metaKey(e)) {
11
-        if (e.shiftKey) {
12
-          state.send("REDO")
13
-        } else {
14
-          state.send("UNDO")
15
-        }
16
-      }
17
-
18
-      if (e.key === "Shift") {
19
-        state.send("PRESSED_SHIFT_KEY", getKeyboardEventInfo(e))
20
-      }
21
-
22
-      if (e.key === "Alt") {
23
-        state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e))
24
-      }
25
-
26
-      if (e.key === "Backspace" && !(metaKey(e) || e.shiftKey || e.altKey)) {
27
-        state.send("DELETED", getKeyboardEventInfo(e))
28
-      }
29
-
30
-      if (e.key === "s" && metaKey(e)) {
8
+      if (metaKey(e) && !["i", "r", "j"].includes(e.key)) {
31 9
         e.preventDefault()
32
-        state.send("SAVED")
33
-      }
34
-      if (e.key === "a" && metaKey(e)) {
35
-        e.preventDefault()
36
-        state.send("SELECTED_ALL")
37
-      }
38
-
39
-      if (e.key === "v" && !(metaKey(e) || e.shiftKey || e.altKey)) {
40
-        state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
41
-      }
42
-
43
-      if (e.key === "d" && !(metaKey(e) || e.shiftKey || e.altKey)) {
44
-        state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e))
45
-      }
46
-
47
-      if (e.key === "c" && !(metaKey(e) || e.shiftKey || e.altKey)) {
48
-        state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e))
49
-      }
50
-
51
-      if (e.key === "i" && !(metaKey(e) || e.shiftKey || e.altKey)) {
52
-        state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e))
53
-      }
54
-
55
-      if (e.key === "l" && !(metaKey(e) || e.shiftKey || e.altKey)) {
56
-        state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e))
57 10
       }
58 11
 
59
-      if (e.key === "y" && !(metaKey(e) || e.shiftKey || e.altKey)) {
60
-        state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e))
61
-      }
62
-
63
-      if (e.key === "p" && !(metaKey(e) || e.shiftKey || e.altKey)) {
64
-        state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e))
65
-      }
66
-
67
-      if (e.key === "r" && !(metaKey(e) || e.shiftKey || e.altKey)) {
68
-        state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e))
12
+      switch (e.key) {
13
+        case "Escape": {
14
+          state.send("CANCELLED")
15
+          break
16
+        }
17
+        case "z": {
18
+          if (metaKey(e)) {
19
+            if (e.shiftKey) {
20
+              state.send("REDO", getKeyboardEventInfo(e))
21
+            } else {
22
+              state.send("UNDO", getKeyboardEventInfo(e))
23
+            }
24
+          }
25
+          break
26
+        }
27
+        case "]": {
28
+          if (metaKey(e)) {
29
+            if (e.altKey) {
30
+              state.send("MOVED_TO_FRONT", getKeyboardEventInfo(e))
31
+            } else {
32
+              state.send("MOVED_FORWARD", getKeyboardEventInfo(e))
33
+            }
34
+          }
35
+          break
36
+        }
37
+        case "[": {
38
+          if (metaKey(e)) {
39
+            if (e.altKey) {
40
+              state.send("MOVED_TO_BACK", getKeyboardEventInfo(e))
41
+            } else {
42
+              state.send("MOVED_BACKWARD", getKeyboardEventInfo(e))
43
+            }
44
+          }
45
+          break
46
+        }
47
+        case "Shift": {
48
+          state.send("PRESSED_SHIFT_KEY", getKeyboardEventInfo(e))
49
+          break
50
+        }
51
+        case "Alt": {
52
+          state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e))
53
+          break
54
+        }
55
+        case "Backspace": {
56
+          state.send("DELETED", getKeyboardEventInfo(e))
57
+          break
58
+        }
59
+        case "s": {
60
+          if (metaKey(e)) {
61
+            state.send("SAVED", getKeyboardEventInfo(e))
62
+          }
63
+          break
64
+        }
65
+        case "a": {
66
+          if (metaKey(e)) {
67
+            state.send("SELECTED_ALL", getKeyboardEventInfo(e))
68
+          }
69
+          break
70
+        }
71
+        case "v": {
72
+          if (metaKey(e)) {
73
+            state.send("PASTED", getKeyboardEventInfo(e))
74
+          } else {
75
+            state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
76
+          }
77
+          break
78
+        }
79
+        case "d": {
80
+          if (metaKey(e)) {
81
+            state.send("DUPLICATED", getKeyboardEventInfo(e))
82
+          } else {
83
+            state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e))
84
+          }
85
+          break
86
+        }
87
+        case "c": {
88
+          if (metaKey(e)) {
89
+            state.send("COPIED", getKeyboardEventInfo(e))
90
+          } else {
91
+            state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e))
92
+          }
93
+          break
94
+        }
95
+        case "i": {
96
+          if (metaKey(e)) {
97
+          } else {
98
+            state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e))
99
+          }
100
+          break
101
+        }
102
+        case "l": {
103
+          if (metaKey(e)) {
104
+          } else {
105
+            state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e))
106
+          }
107
+          break
108
+        }
109
+        case "y": {
110
+          if (metaKey(e)) {
111
+          } else {
112
+            state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e))
113
+          }
114
+          break
115
+        }
116
+        case "p": {
117
+          if (metaKey(e)) {
118
+          } else {
119
+            state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e))
120
+          }
121
+          break
122
+        }
123
+        case "r": {
124
+          if (metaKey(e)) {
125
+          } else {
126
+            state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e))
127
+          }
128
+          break
129
+        }
130
+        default: {
131
+          state.send("PRESSED_KEY", getKeyboardEventInfo(e))
132
+        }
69 133
       }
70 134
     }
71 135
 

+ 16
- 66
lib/shape-utils/circle.tsx Прегледај датотеку

@@ -94,80 +94,30 @@ const circle = registerShapeUtils<CircleShape>({
94 94
     return shape
95 95
   },
96 96
 
97
-  transform(shape, bounds, { type, initialShape, scaleX, scaleY }) {
98
-    const anchor = getTransformAnchor(type, scaleX < 0, scaleY < 0)
99
-
100
-    // Set the new corner or position depending on the anchor
101
-    switch (anchor) {
102
-      case Corner.TopLeft: {
103
-        shape.radius = Math.min(bounds.width, bounds.height) / 2
104
-        shape.point = [
105
-          bounds.maxX - shape.radius * 2,
106
-          bounds.maxY - shape.radius * 2,
107
-        ]
108
-        break
109
-      }
110
-      case Corner.TopRight: {
111
-        shape.radius = Math.min(bounds.width, bounds.height) / 2
112
-        shape.point = [bounds.minX, bounds.maxY - shape.radius * 2]
113
-        break
114
-      }
115
-      case Corner.BottomRight: {
116
-        shape.radius = Math.min(bounds.width, bounds.height) / 2
117
-        shape.point = [
118
-          bounds.maxX - shape.radius * 2,
119
-          bounds.maxY - shape.radius * 2,
120
-        ]
121
-        break
122
-        break
123
-      }
124
-      case Corner.BottomLeft: {
125
-        shape.radius = Math.min(bounds.width, bounds.height) / 2
126
-        shape.point = [bounds.maxX - shape.radius * 2, bounds.minY]
127
-        break
128
-      }
129
-      case Edge.Top: {
130
-        shape.radius = bounds.height / 2
131
-        shape.point = [
132
-          bounds.minX + (bounds.width / 2 - shape.radius),
133
-          bounds.minY,
134
-        ]
135
-        break
136
-      }
137
-      case Edge.Right: {
138
-        shape.radius = bounds.width / 2
139
-        shape.point = [
140
-          bounds.maxX - shape.radius * 2,
141
-          bounds.minY + (bounds.height / 2 - shape.radius),
142
-        ]
143
-        break
144
-      }
145
-      case Edge.Bottom: {
146
-        shape.radius = bounds.height / 2
147
-        shape.point = [
148
-          bounds.minX + (bounds.width / 2 - shape.radius),
149
-          bounds.maxY - shape.radius * 2,
150
-        ]
151
-        break
152
-      }
153
-      case Edge.Left: {
154
-        shape.radius = bounds.width / 2
155
-        shape.point = [
156
-          bounds.minX,
157
-          bounds.minY + (bounds.height / 2 - shape.radius),
158
-        ]
159
-        break
160
-      }
161
-    }
97
+  transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
98
+    shape.radius =
99
+      initialShape.radius * Math.min(Math.abs(scaleX), Math.abs(scaleY))
100
+
101
+    shape.point = [
102
+      bounds.minX +
103
+        (bounds.width - shape.radius * 2) *
104
+          (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
105
+      bounds.minY +
106
+        (bounds.height - shape.radius * 2) *
107
+          (scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
108
+    ]
162 109
 
163 110
     return shape
164 111
   },
165 112
 
166 113
   transformSingle(shape, bounds, info) {
167
-    return this.transform(shape, bounds, info)
114
+    shape.radius = Math.min(bounds.width, bounds.height) / 2
115
+    shape.point = [bounds.minX, bounds.minY]
116
+    return shape
168 117
   },
169 118
 
170 119
   canTransform: true,
120
+  canChangeAspectRatio: false,
171 121
 })
172 122
 
173 123
 export default circle

+ 1
- 0
lib/shape-utils/dot.tsx Прегледај датотеку

@@ -94,6 +94,7 @@ const dot = registerShapeUtils<DotShape>({
94 94
   },
95 95
 
96 96
   canTransform: false,
97
+  canChangeAspectRatio: false,
97 98
 })
98 99
 
99 100
 export default dot

+ 1
- 0
lib/shape-utils/ellipse.tsx Прегледај датотеку

@@ -130,6 +130,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
130 130
   },
131 131
 
132 132
   canTransform: true,
133
+  canChangeAspectRatio: true,
133 134
 })
134 135
 
135 136
 export default ellipse

+ 5
- 2
lib/shape-utils/index.tsx Прегледај датотеку

@@ -60,7 +60,7 @@ export interface ShapeUtility<K extends Shape> {
60 60
     shape: K,
61 61
     bounds: Bounds,
62 62
     info: {
63
-      type: Edge | Corner | "center"
63
+      type: Edge | Corner
64 64
       initialShape: K
65 65
       scaleX: number
66 66
       scaleY: number
@@ -73,7 +73,7 @@ export interface ShapeUtility<K extends Shape> {
73 73
     shape: K,
74 74
     bounds: Bounds,
75 75
     info: {
76
-      type: Edge | Corner | "center"
76
+      type: Edge | Corner
77 77
       initialShape: K
78 78
       scaleX: number
79 79
       scaleY: number
@@ -89,6 +89,9 @@ export interface ShapeUtility<K extends Shape> {
89 89
 
90 90
   // Whether to show transform controls when this shape is selected.
91 91
   canTransform: boolean
92
+
93
+  // Whether the shape's aspect ratio can change
94
+  canChangeAspectRatio: boolean
92 95
 }
93 96
 
94 97
 // A mapping of shape types to shape utilities.

+ 1
- 0
lib/shape-utils/line.tsx Прегледај датотеку

@@ -102,6 +102,7 @@ const line = registerShapeUtils<LineShape>({
102 102
   },
103 103
 
104 104
   canTransform: false,
105
+  canChangeAspectRatio: false,
105 106
 })
106 107
 
107 108
 export default line

+ 5
- 7
lib/shape-utils/polyline.tsx Прегледај датотеку

@@ -99,21 +99,18 @@ const polyline = registerShapeUtils<PolylineShape>({
99 99
     return shape
100 100
   },
101 101
 
102
-  transform(
103
-    shape,
104
-    bounds,
105
-    { initialShape, initialShapeBounds, isFlippedX, isFlippedY }
106
-  ) {
102
+  transform(shape, bounds, { initialShape, scaleX, scaleY }) {
103
+    const initialShapeBounds = this.getBounds(initialShape)
107 104
     shape.points = shape.points.map((_, i) => {
108 105
       const [x, y] = initialShape.points[i]
109 106
 
110 107
       return [
111 108
         bounds.width *
112
-          (isFlippedX
109
+          (scaleX < 0
113 110
             ? 1 - x / initialShapeBounds.width
114 111
             : x / initialShapeBounds.width),
115 112
         bounds.height *
116
-          (isFlippedY
113
+          (scaleY < 0
117 114
             ? 1 - y / initialShapeBounds.height
118 115
             : y / initialShapeBounds.height),
119 116
       ]
@@ -128,6 +125,7 @@ const polyline = registerShapeUtils<PolylineShape>({
128 125
   },
129 126
 
130 127
   canTransform: true,
128
+  canChangeAspectRatio: true,
131 129
 })
132 130
 
133 131
 export default polyline

+ 5
- 0
lib/shape-utils/ray.tsx Прегледај датотеку

@@ -97,7 +97,12 @@ const ray = registerShapeUtils<RayShape>({
97 97
     return shape
98 98
   },
99 99
 
100
+  transformSingle(shape, bounds, info) {
101
+    return this.transform(shape, bounds, info)
102
+  },
103
+
100 104
   canTransform: false,
105
+  canChangeAspectRatio: false,
101 106
 })
102 107
 
103 108
 export default ray

+ 18
- 2
lib/shape-utils/rectangle.tsx Прегледај датотеку

@@ -31,8 +31,23 @@ const rectangle = registerShapeUtils<RectangleShape>({
31 31
     }
32 32
   },
33 33
 
34
-  render({ id, size }) {
35
-    return <rect id={id} width={size[0]} height={size[1]} />
34
+  render({ id, size, parentId, childIndex }) {
35
+    return (
36
+      <g id={id}>
37
+        <rect id={id} width={size[0]} height={size[1]} />
38
+        <text
39
+          y={4}
40
+          x={4}
41
+          fontSize={18}
42
+          fill="black"
43
+          stroke="none"
44
+          alignmentBaseline="text-before-edge"
45
+          pointerEvents="none"
46
+        >
47
+          {childIndex}
48
+        </text>
49
+      </g>
50
+    )
36 51
   },
37 52
 
38 53
   getBounds(shape) {
@@ -128,6 +143,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
128 143
   },
129 144
 
130 145
   canTransform: true,
146
+  canChangeAspectRatio: true,
131 147
 })
132 148
 
133 149
 export default rectangle

+ 0
- 33
state/commands/create-shape.ts Прегледај датотеку

@@ -1,33 +0,0 @@
1
-import Command from "./command"
2
-import history from "../history"
3
-import { Data, Shape } from "types"
4
-import { getPage } from "utils/utils"
5
-
6
-export default function registerShapeUtilsCommand(data: Data, shape: Shape) {
7
-  const { currentPageId } = data
8
-
9
-  history.execute(
10
-    data,
11
-    new Command({
12
-      name: "translate_shapes",
13
-      category: "canvas",
14
-      do(data) {
15
-        const page = getPage(data)
16
-
17
-        page.shapes[shape.id] = shape
18
-        data.selectedIds.clear()
19
-        data.pointedId = undefined
20
-        data.hoveredId = undefined
21
-      },
22
-      undo(data) {
23
-        const page = getPage(data)
24
-
25
-        delete page.shapes[shape.id]
26
-
27
-        data.selectedIds.clear()
28
-        data.pointedId = undefined
29
-        data.hoveredId = undefined
30
-      },
31
-    })
32
-  )
33
-}

+ 0
- 2
state/commands/index.ts Прегледај датотеку

@@ -2,7 +2,6 @@ import translate from "./translate"
2 2
 import transform from "./transform"
3 3
 import transformSingle from "./transform-single"
4 4
 import generate from "./generate"
5
-import registerShapeUtils from "./create-shape"
6 5
 import direct from "./direct"
7 6
 import rotate from "./rotate"
8 7
 
@@ -11,7 +10,6 @@ const commands = {
11 10
   transform,
12 11
   transformSingle,
13 12
   generate,
14
-  registerShapeUtils,
15 13
   direct,
16 14
   rotate,
17 15
 }

+ 5
- 11
state/sessions/transform-session.ts Прегледај датотеку

@@ -17,15 +17,11 @@ import {
17 17
 export default class TransformSession extends BaseSession {
18 18
   scaleX = 1
19 19
   scaleY = 1
20
-  transformType: Edge | Corner | "center"
20
+  transformType: Edge | Corner
21 21
   origin: number[]
22 22
   snapshot: TransformSnapshot
23 23
 
24
-  constructor(
25
-    data: Data,
26
-    transformType: Corner | Edge | "center",
27
-    point: number[]
28
-  ) {
24
+  constructor(data: Data, transformType: Corner | Edge, point: number[]) {
29 25
     super(data)
30 26
     this.origin = point
31 27
     this.transformType = transformType
@@ -108,10 +104,7 @@ export default class TransformSession extends BaseSession {
108 104
   }
109 105
 }
110 106
 
111
-export function getTransformSnapshot(
112
-  data: Data,
113
-  transformType: Edge | Corner | "center"
114
-) {
107
+export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
115 108
   const {
116 109
     document: { pages },
117 110
     selectedIds,
@@ -144,6 +137,7 @@ export function getTransformSnapshot(
144 137
     initialBounds: bounds,
145 138
     shapeBounds: Object.fromEntries(
146 139
       Array.from(selectedIds.values()).map((id) => {
140
+        const shape = pageShapes[id]
147 141
         const initialShapeBounds = shapesBounds[id]
148 142
         const ic = getBoundsCenter(initialShapeBounds)
149 143
 
@@ -153,7 +147,7 @@ export function getTransformSnapshot(
153 147
         return [
154 148
           id,
155 149
           {
156
-            initialShape: pageShapes[id],
150
+            initialShape: shape,
157 151
             initialShapeBounds,
158 152
             transformOrigin: [ix, iy],
159 153
           },

+ 1
- 1
state/sessions/transform-single-session.ts Прегледај датотеку

@@ -48,7 +48,7 @@ export default class TransformSingleSession extends BaseSession {
48 48
       transformType,
49 49
       vec.vec(this.origin, point),
50 50
       shape.rotation,
51
-      isAspectRatioLocked
51
+      isAspectRatioLocked || !getShapeUtils(initialShape).canChangeAspectRatio
52 52
     )
53 53
 
54 54
     this.scaleX = newBoundingBox.scaleX

+ 8
- 3
state/sessions/translate-session.ts Прегледај датотеку

@@ -4,7 +4,7 @@ import BaseSession from "./base-session"
4 4
 import commands from "state/commands"
5 5
 import { current } from "immer"
6 6
 import { v4 as uuid } from "uuid"
7
-import { getPage, getSelectedShapes } from "utils/utils"
7
+import { getChildIndexAbove, getPage, getSelectedShapes } from "utils/utils"
8 8
 
9 9
 export default class TranslateSession extends BaseSession {
10 10
   delta = [0, 0]
@@ -94,12 +94,17 @@ export default class TranslateSession extends BaseSession {
94 94
 }
95 95
 
96 96
 export function getTranslateSnapshot(data: Data) {
97
-  const shapes = getSelectedShapes(current(data))
97
+  const cData = current(data)
98
+  const shapes = getSelectedShapes(cData)
98 99
 
99 100
   return {
100 101
     currentPageId: data.currentPageId,
101 102
     initialShapes: shapes.map(({ id, point }) => ({ id, point })),
102
-    clones: shapes.map((shape) => ({ ...shape, id: uuid() })),
103
+    clones: shapes.map((shape) => ({
104
+      ...shape,
105
+      id: uuid(),
106
+      childIndex: getChildIndexAbove(cData, shape.id),
107
+    })),
103 108
   }
104 109
 }
105 110
 

+ 189
- 73
state/state.ts Прегледај датотеку

@@ -1,9 +1,11 @@
1 1
 import { createSelectorHook, createState } from "@state-designer/react"
2 2
 import {
3 3
   clamp,
4
+  getChildren,
4 5
   getCommonBounds,
5 6
   getPage,
6 7
   getShape,
8
+  getSiblings,
7 9
   screenToWorld,
8 10
 } from "utils/utils"
9 11
 import * as vec from "utils/vec"
@@ -97,6 +99,10 @@ const state = createState({
97 99
             INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
98 100
             DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
99 101
             CHANGED_CODE_CONTROL: "updateControls",
102
+            MOVED_TO_FRONT: "moveSelectionToFront",
103
+            MOVED_TO_BACK: "moveSelectionToBack",
104
+            MOVED_FORWARD: "moveSelectionForward",
105
+            MOVED_BACKWARD: "moveSelectionBackward",
100 106
           },
101 107
           initial: "notPointing",
102 108
           states: {
@@ -222,7 +228,8 @@ const state = createState({
222 228
             creating: {
223 229
               on: {
224 230
                 POINTED_CANVAS: {
225
-                  do: "createDot",
231
+                  get: "newDot",
232
+                  do: "createShape",
226 233
                   to: "dot.editing",
227 234
                 },
228 235
               },
@@ -272,8 +279,11 @@ const state = createState({
272 279
                 CANCELLED: { to: "selecting" },
273 280
                 MOVED_POINTER: {
274 281
                   if: "distanceImpliesDrag",
275
-                  do: "createCircle",
276
-                  to: "drawingShape.bounds",
282
+                  then: {
283
+                    get: "newDot",
284
+                    do: "createShape",
285
+                    to: "drawingShape.bounds",
286
+                  },
277 287
                 },
278 288
               },
279 289
             },
@@ -296,8 +306,11 @@ const state = createState({
296 306
                 CANCELLED: { to: "selecting" },
297 307
                 MOVED_POINTER: {
298 308
                   if: "distanceImpliesDrag",
299
-                  do: "createEllipse",
300
-                  to: "drawingShape.bounds",
309
+                  then: {
310
+                    get: "newEllipse",
311
+                    do: "createShape",
312
+                    to: "drawingShape.bounds",
313
+                  },
301 314
                 },
302 315
               },
303 316
             },
@@ -320,8 +333,11 @@ const state = createState({
320 333
                 CANCELLED: { to: "selecting" },
321 334
                 MOVED_POINTER: {
322 335
                   if: "distanceImpliesDrag",
323
-                  do: "createRectangle",
324
-                  to: "drawingShape.bounds",
336
+                  then: {
337
+                    get: "newRectangle",
338
+                    do: "createShape",
339
+                    to: "drawingShape.bounds",
340
+                  },
325 341
                 },
326 342
               },
327 343
             },
@@ -334,7 +350,8 @@ const state = createState({
334 350
               on: {
335 351
                 CANCELLED: { to: "selecting" },
336 352
                 POINTED_CANVAS: {
337
-                  do: "createRay",
353
+                  get: "newRay",
354
+                  do: "createShape",
338 355
                   to: "ray.editing",
339 356
                 },
340 357
               },
@@ -358,7 +375,8 @@ const state = createState({
358 375
               on: {
359 376
                 CANCELLED: { to: "selecting" },
360 377
                 POINTED_CANVAS: {
361
-                  do: "createLine",
378
+                  get: "newLine",
379
+                  do: "createShape",
362 380
                   to: "line.editing",
363 381
                 },
364 382
               },
@@ -408,6 +426,51 @@ const state = createState({
408 426
       },
409 427
     },
410 428
   },
429
+  results: {
430
+    // Dot
431
+    newDot(data, payload: PointerInfo) {
432
+      return shapeUtilityMap[ShapeType.Dot].create({
433
+        point: screenToWorld(payload.point, data),
434
+      })
435
+    },
436
+
437
+    // Ray
438
+    newRay(data, payload: PointerInfo) {
439
+      return shapeUtilityMap[ShapeType.Ray].create({
440
+        point: screenToWorld(payload.point, data),
441
+      })
442
+    },
443
+
444
+    // Line
445
+    newLine(data, payload: PointerInfo) {
446
+      return shapeUtilityMap[ShapeType.Line].create({
447
+        point: screenToWorld(payload.point, data),
448
+        direction: [0, 1],
449
+      })
450
+    },
451
+
452
+    newCircle(data, payload: PointerInfo) {
453
+      return shapeUtilityMap[ShapeType.Circle].create({
454
+        point: screenToWorld(payload.point, data),
455
+        radius: 1,
456
+      })
457
+    },
458
+
459
+    newEllipse(data, payload: PointerInfo) {
460
+      return shapeUtilityMap[ShapeType.Ellipse].create({
461
+        point: screenToWorld(payload.point, data),
462
+        radiusX: 1,
463
+        radiusY: 1,
464
+      })
465
+    },
466
+
467
+    newRectangle(data, payload: PointerInfo) {
468
+      return shapeUtilityMap[ShapeType.Rectangle].create({
469
+        point: screenToWorld(payload.point, data),
470
+        size: [1, 1],
471
+      })
472
+    },
473
+  },
411 474
   conditions: {
412 475
     isPointingBounds(data, payload: PointerInfo) {
413 476
       return payload.target === "bounds"
@@ -447,69 +510,10 @@ const state = createState({
447 510
   },
448 511
   actions: {
449 512
     /* --------------------- Shapes --------------------- */
450
-
451
-    // Dot
452
-    createDot(data, payload: PointerInfo) {
453
-      const shape = shapeUtilityMap[ShapeType.Dot].create({
454
-        point: screenToWorld(payload.point, data),
455
-      })
456
-
457
-      getPage(data).shapes[shape.id] = shape
458
-      data.selectedIds.clear()
459
-      data.selectedIds.add(shape.id)
460
-    },
461
-
462
-    // Ray
463
-    createRay(data, payload: PointerInfo) {
464
-      const shape = shapeUtilityMap[ShapeType.Ray].create({
465
-        point: screenToWorld(payload.point, data),
466
-      })
467
-
468
-      getPage(data).shapes[shape.id] = shape
469
-      data.selectedIds.clear()
470
-      data.selectedIds.add(shape.id)
471
-    },
472
-
473
-    // Line
474
-    createLine(data, payload: PointerInfo) {
475
-      const shape = shapeUtilityMap[ShapeType.Line].create({
476
-        point: screenToWorld(payload.point, data),
477
-        direction: [0, 1],
478
-      })
479
-
480
-      getPage(data).shapes[shape.id] = shape
481
-      data.selectedIds.clear()
482
-      data.selectedIds.add(shape.id)
483
-    },
484
-
485
-    createCircle(data, payload: PointerInfo) {
486
-      const shape = shapeUtilityMap[ShapeType.Circle].create({
487
-        point: screenToWorld(payload.point, data),
488
-        radius: 1,
489
-      })
490
-
491
-      getPage(data).shapes[shape.id] = shape
492
-      data.selectedIds.clear()
493
-      data.selectedIds.add(shape.id)
494
-    },
495
-
496
-    createEllipse(data, payload: PointerInfo) {
497
-      const shape = shapeUtilityMap[ShapeType.Ellipse].create({
498
-        point: screenToWorld(payload.point, data),
499
-        radiusX: 1,
500
-        radiusY: 1,
501
-      })
502
-
503
-      getPage(data).shapes[shape.id] = shape
504
-      data.selectedIds.clear()
505
-      data.selectedIds.add(shape.id)
506
-    },
507
-
508
-    createRectangle(data, payload: PointerInfo) {
509
-      const shape = shapeUtilityMap[ShapeType.Rectangle].create({
510
-        point: screenToWorld(payload.point, data),
511
-        size: [1, 1],
512
-      })
513
+    createShape(data, payload: PointerInfo, shape: Shape) {
514
+      const siblings = getChildren(data, shape.parentId)
515
+      shape.childIndex =
516
+        siblings.length > 0 ? siblings[siblings.length - 1].childIndex + 1 : 1
513 517
 
514 518
       getPage(data).shapes[shape.id] = shape
515 519
       data.selectedIds.clear()
@@ -671,7 +675,119 @@ const state = createState({
671 675
     pushPointedIdToSelectedIds(data) {
672 676
       data.selectedIds.add(data.pointedId)
673 677
     },
674
-    // Camera
678
+    moveSelectionToFront(data) {
679
+      const { selectedIds } = data
680
+    },
681
+    moveSelectionToBack(data) {
682
+      const { selectedIds } = data
683
+    },
684
+    moveSelectionForward(data) {
685
+      const { selectedIds } = data
686
+
687
+      const page = getPage(data)
688
+
689
+      const shapes = Array.from(selectedIds.values()).map(
690
+        (id) => page.shapes[id]
691
+      )
692
+
693
+      const shapesByParentId = shapes.reduce<Record<string, Shape[]>>(
694
+        (acc, shape) => {
695
+          if (acc[shape.parentId] === undefined) {
696
+            acc[shape.parentId] = []
697
+          }
698
+          acc[shape.parentId].push(shape)
699
+          return acc
700
+        },
701
+        {}
702
+      )
703
+
704
+      const visited = new Set<string>()
705
+
706
+      for (let id in shapesByParentId) {
707
+        const children = getChildren(data, id)
708
+
709
+        shapesByParentId[id]
710
+          .sort((a, b) => b.childIndex - a.childIndex)
711
+          .forEach((shape) => {
712
+            visited.add(shape.id)
713
+            children.sort((a, b) => a.childIndex - b.childIndex)
714
+            const index = children.indexOf(shape)
715
+
716
+            const nextSibling = children[index + 1]
717
+
718
+            if (!nextSibling || visited.has(nextSibling.id)) {
719
+              // At the top already, no change
720
+              return
721
+            }
722
+
723
+            const nextNextSibling = children[index + 2]
724
+
725
+            if (!nextNextSibling) {
726
+              // Moving to the top
727
+              shape.childIndex = nextSibling.childIndex + 1
728
+              return
729
+            }
730
+
731
+            shape.childIndex =
732
+              (nextSibling.childIndex + nextNextSibling.childIndex) / 2
733
+          })
734
+      }
735
+    },
736
+    moveSelectionBackward(data) {
737
+      const { selectedIds } = data
738
+
739
+      const page = getPage(data)
740
+
741
+      const shapes = Array.from(selectedIds.values()).map(
742
+        (id) => page.shapes[id]
743
+      )
744
+
745
+      const shapesByParentId = shapes.reduce<Record<string, Shape[]>>(
746
+        (acc, shape) => {
747
+          if (acc[shape.parentId] === undefined) {
748
+            acc[shape.parentId] = []
749
+          }
750
+          acc[shape.parentId].push(shape)
751
+          return acc
752
+        },
753
+        {}
754
+      )
755
+
756
+      const visited = new Set<string>()
757
+
758
+      for (let id in shapesByParentId) {
759
+        const children = getChildren(data, id)
760
+
761
+        shapesByParentId[id]
762
+          .sort((a, b) => a.childIndex - b.childIndex)
763
+          .forEach((shape) => {
764
+            visited.add(shape.id)
765
+            children.sort((a, b) => a.childIndex - b.childIndex)
766
+            const index = children.indexOf(shape)
767
+
768
+            const nextSibling = children[index - 1]
769
+
770
+            if (!nextSibling || visited.has(nextSibling.id)) {
771
+              // At the bottom already, no change
772
+              return
773
+            }
774
+
775
+            const nextNextSibling = children[index - 2]
776
+
777
+            if (!nextNextSibling) {
778
+              // Moving to the bottom
779
+              shape.childIndex = nextSibling.childIndex / 2
780
+              return
781
+            }
782
+
783
+            shape.childIndex =
784
+              (nextSibling.childIndex + nextNextSibling.childIndex) / 2
785
+          })
786
+      }
787
+    },
788
+
789
+    /* --------------------- Camera --------------------- */
790
+
675 791
     resetCamera(data) {
676 792
       data.camera.zoom = 1
677 793
       data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]

+ 0
- 251
utils/transforms.ts Прегледај датотеку

@@ -1,251 +0,0 @@
1
-import { Bounds, BoundsSnapshot, ShapeBounds } from "types"
2
-
3
-export function stretchshapesX(shapes: ShapeBounds[]) {
4
-  const [first, ...rest] = shapes
5
-  let min = first.minX
6
-  let max = first.minX + first.width
7
-  for (let box of rest) {
8
-    min = Math.min(min, box.minX)
9
-    max = Math.max(max, box.minX + box.width)
10
-  }
11
-  return shapes.map((box) => ({ ...box, x: min, width: max - min }))
12
-}
13
-
14
-export function stretchshapesY(shapes: ShapeBounds[]) {
15
-  const [first, ...rest] = shapes
16
-  let min = first.minY
17
-  let max = first.minY + first.height
18
-  for (let box of rest) {
19
-    min = Math.min(min, box.minY)
20
-    max = Math.max(max, box.minY + box.height)
21
-  }
22
-  return shapes.map((box) => ({ ...box, y: min, height: max - min }))
23
-}
24
-
25
-export function distributeshapesX(shapes: ShapeBounds[]) {
26
-  const len = shapes.length
27
-  const sorted = [...shapes].sort((a, b) => a.minX - b.minX)
28
-  let min = sorted[0].minX
29
-
30
-  sorted.sort((a, b) => a.minX + a.width - b.minX - b.width)
31
-  let last = sorted[len - 1]
32
-  let max = last.minX + last.width
33
-
34
-  let range = max - min
35
-  let step = range / len
36
-  return sorted.map((box, i) => ({ ...box, x: min + step * i }))
37
-}
38
-
39
-export function distributeshapesY(shapes: ShapeBounds[]) {
40
-  const len = shapes.length
41
-  const sorted = [...shapes].sort((a, b) => a.minY - b.minY)
42
-  let min = sorted[0].minY
43
-
44
-  sorted.sort((a, b) => a.minY + a.height - b.minY - b.height)
45
-  let last = sorted[len - 1]
46
-  let max = last.minY + last.height
47
-
48
-  let range = max - min
49
-  let step = range / len
50
-  return sorted.map((box, i) => ({ ...box, y: min + step * i }))
51
-}
52
-
53
-export function alignshapesCenterX(shapes: ShapeBounds[]) {
54
-  let midX = 0
55
-  for (let box of shapes) midX += box.minX + box.width / 2
56
-  midX /= shapes.length
57
-  return shapes.map((box) => ({ ...box, x: midX - box.width / 2 }))
58
-}
59
-
60
-export function alignshapesCenterY(shapes: ShapeBounds[]) {
61
-  let midY = 0
62
-  for (let box of shapes) midY += box.minY + box.height / 2
63
-  midY /= shapes.length
64
-  return shapes.map((box) => ({ ...box, y: midY - box.height / 2 }))
65
-}
66
-
67
-export function alignshapesTop(shapes: ShapeBounds[]) {
68
-  const [first, ...rest] = shapes
69
-  let y = first.minY
70
-  for (let box of rest) if (box.minY < y) y = box.minY
71
-  return shapes.map((box) => ({ ...box, y }))
72
-}
73
-
74
-export function alignshapesBottom(shapes: ShapeBounds[]) {
75
-  const [first, ...rest] = shapes
76
-  let maxY = first.minY + first.height
77
-  for (let box of rest)
78
-    if (box.minY + box.height > maxY) maxY = box.minY + box.height
79
-  return shapes.map((box) => ({ ...box, y: maxY - box.height }))
80
-}
81
-
82
-export function alignshapesLeft(shapes: ShapeBounds[]) {
83
-  const [first, ...rest] = shapes
84
-  let x = first.minX
85
-  for (let box of rest) if (box.minX < x) x = box.minX
86
-  return shapes.map((box) => ({ ...box, x }))
87
-}
88
-
89
-export function alignshapesRight(shapes: ShapeBounds[]) {
90
-  const [first, ...rest] = shapes
91
-  let maxX = first.minX + first.width
92
-  for (let box of rest)
93
-    if (box.minX + box.width > maxX) maxX = box.minX + box.width
94
-  return shapes.map((box) => ({ ...box, x: maxX - box.width }))
95
-}
96
-
97
-// Resizers
98
-
99
-export function getBoundingBox(shapes: ShapeBounds[]): Bounds {
100
-  if (shapes.length === 0) {
101
-    return {
102
-      minX: 0,
103
-      minY: 0,
104
-      maxX: 0,
105
-      maxY: 0,
106
-      width: 0,
107
-      height: 0,
108
-    }
109
-  }
110
-
111
-  const first = shapes[0]
112
-
113
-  let minX = first.minX
114
-  let minY = first.minY
115
-  let maxX = first.minX + first.width
116
-  let maxY = first.minY + first.height
117
-
118
-  for (let box of shapes) {
119
-    minX = Math.min(minX, box.minX)
120
-    minY = Math.min(minY, box.minY)
121
-    maxX = Math.max(maxX, box.minX + box.width)
122
-    maxY = Math.max(maxY, box.minY + box.height)
123
-  }
124
-
125
-  return {
126
-    minX,
127
-    minY,
128
-    maxX,
129
-    maxY,
130
-    width: maxX - minX,
131
-    height: maxY - minY,
132
-  }
133
-}
134
-
135
-export function getSnapshots(
136
-  shapes: ShapeBounds[],
137
-  bounds: Bounds
138
-): Record<string, BoundsSnapshot> {
139
-  const acc = {} as Record<string, BoundsSnapshot>
140
-
141
-  const w = bounds.maxX - bounds.minX
142
-  const h = bounds.maxY - bounds.minY
143
-
144
-  for (let box of shapes) {
145
-    acc[box.id] = {
146
-      ...box,
147
-      nx: (box.minX - bounds.minX) / w,
148
-      ny: (box.minY - bounds.minY) / h,
149
-      nmx: 1 - (box.minX + box.width - bounds.minX) / w,
150
-      nmy: 1 - (box.minY + box.height - bounds.minY) / h,
151
-      nw: box.width / w,
152
-      nh: box.height / h,
153
-    }
154
-  }
155
-
156
-  return acc
157
-}
158
-
159
-export function getEdgeResizer(shapes: ShapeBounds[], edge: number) {
160
-  const initial = getBoundingBox(shapes)
161
-  const snapshots = getSnapshots(shapes, initial)
162
-  const mshapes = [...shapes]
163
-
164
-  let { minX: x0, minY: y0, maxX: x1, maxY: y1 } = initial
165
-  let { minX: mx, minY: my } = initial
166
-  let mw = x1 - x0
167
-  let mh = y1 - y0
168
-
169
-  return function edgeResize({ x, y }) {
170
-    if (edge === 0 || edge === 2) {
171
-      edge === 0 ? (y0 = y) : (y1 = y)
172
-      my = y0 < y1 ? y0 : y1
173
-      mh = Math.abs(y1 - y0)
174
-      for (let box of mshapes) {
175
-        const { ny, nmy, nh } = snapshots[box.id]
176
-        box.minY = my + (y1 < y0 ? nmy : ny) * mh
177
-        box.height = nh * mh
178
-      }
179
-    } else {
180
-      edge === 1 ? (x1 = x) : (x0 = x)
181
-      mx = x0 < x1 ? x0 : x1
182
-      mw = Math.abs(x1 - x0)
183
-      for (let box of mshapes) {
184
-        const { nx, nmx, nw } = snapshots[box.id]
185
-        box.minX = mx + (x1 < x0 ? nmx : nx) * mw
186
-        box.width = nw * mw
187
-      }
188
-    }
189
-
190
-    return [
191
-      mshapes,
192
-      {
193
-        x: mx,
194
-        y: my,
195
-        width: mw,
196
-        height: mh,
197
-        maxX: mx + mw,
198
-        maxY: my + mh,
199
-      },
200
-    ]
201
-  }
202
-}
203
-
204
-/**
205
- * Returns a function that can be used to calculate corner resize transforms.
206
- * @param shapes An array of the shapes being resized.
207
- * @param corner A number representing the corner being dragged. Top Left: 0, Top Right: 1, Bottom Right: 2, Bottom Left: 3.
208
- * @example
209
- * const resizer = getCornerResizer(selectedshapes, 3)
210
- * resizer(selectedshapes, )
211
- */
212
-export function getCornerResizer(shapes: ShapeBounds[], corner: number) {
213
-  const initial = getBoundingBox(shapes)
214
-  const snapshots = getSnapshots(shapes, initial)
215
-  const mshapes = [...shapes]
216
-
217
-  let { minX: x0, minY: y0, maxX: x1, maxY: y1 } = initial
218
-  let { minX: mx, minY: my } = initial
219
-  let mw = x1 - x0
220
-  let mh = y1 - y0
221
-
222
-  return function cornerResizer({ x, y }) {
223
-    corner < 2 ? (y0 = y) : (y1 = y)
224
-    my = y0 < y1 ? y0 : y1
225
-    mh = Math.abs(y1 - y0)
226
-
227
-    corner === 1 || corner === 2 ? (x1 = x) : (x0 = x)
228
-    mx = x0 < x1 ? x0 : x1
229
-    mw = Math.abs(x1 - x0)
230
-
231
-    for (let box of mshapes) {
232
-      const { nx, nmx, nw, ny, nmy, nh } = snapshots[box.id]
233
-      box.minX = mx + (x1 < x0 ? nmx : nx) * mw
234
-      box.minY = my + (y1 < y0 ? nmy : ny) * mh
235
-      box.width = nw * mw
236
-      box.height = nh * mh
237
-    }
238
-
239
-    return [
240
-      mshapes,
241
-      {
242
-        x: mx,
243
-        y: my,
244
-        width: mw,
245
-        height: mh,
246
-        maxX: mx + mw,
247
-        maxY: my + mh,
248
-      },
249
-    ]
250
-  }
251
-}

+ 79
- 0
utils/utils.ts Прегледај датотеку

@@ -1375,3 +1375,82 @@ export function clampToRotationToSegments(r: number, segments: number) {
1375 1375
   const seg = (Math.PI * 2) / segments
1376 1376
   return Math.floor((clampRadians(r) + seg / 2) / seg) * seg
1377 1377
 }
1378
+
1379
+export function getParent(data: Data, id: string, pageId = data.currentPageId) {
1380
+  const page = getPage(data, pageId)
1381
+  const shape = page.shapes[id]
1382
+
1383
+  return page.shapes[shape.parentId] || data.document.pages[shape.parentId]
1384
+}
1385
+
1386
+export function getChildren(
1387
+  data: Data,
1388
+  id: string,
1389
+  pageId = data.currentPageId
1390
+) {
1391
+  const page = getPage(data, pageId)
1392
+  return Object.values(page.shapes)
1393
+    .filter(({ parentId }) => parentId === id)
1394
+    .sort((a, b) => a.childIndex - b.childIndex)
1395
+}
1396
+
1397
+export function getSiblings(
1398
+  data: Data,
1399
+  id: string,
1400
+  pageId = data.currentPageId
1401
+) {
1402
+  const page = getPage(data, pageId)
1403
+  const shape = page.shapes[id]
1404
+
1405
+  return Object.values(page.shapes)
1406
+    .filter(({ parentId }) => parentId === shape.parentId)
1407
+    .sort((a, b) => a.childIndex - b.childIndex)
1408
+}
1409
+
1410
+export function getChildIndexAbove(
1411
+  data: Data,
1412
+  id: string,
1413
+  pageId = data.currentPageId
1414
+) {
1415
+  const page = getPage(data, pageId)
1416
+
1417
+  const shape = page.shapes[id]
1418
+
1419
+  const siblings = Object.values(page.shapes)
1420
+    .filter(({ parentId }) => parentId === shape.parentId)
1421
+    .sort((a, b) => a.childIndex - b.childIndex)
1422
+
1423
+  const index = siblings.indexOf(shape)
1424
+
1425
+  const nextSibling = siblings[index + 1]
1426
+
1427
+  if (!nextSibling) {
1428
+    return shape.childIndex + 1
1429
+  }
1430
+
1431
+  return (shape.childIndex + nextSibling.childIndex) / 2
1432
+}
1433
+
1434
+export function getChildIndexBelow(
1435
+  data: Data,
1436
+  id: string,
1437
+  pageId = data.currentPageId
1438
+) {
1439
+  const page = getPage(data, pageId)
1440
+
1441
+  const shape = page.shapes[id]
1442
+
1443
+  const siblings = Object.values(page.shapes)
1444
+    .filter(({ parentId }) => parentId === shape.parentId)
1445
+    .sort((a, b) => a.childIndex - b.childIndex)
1446
+
1447
+  const index = siblings.indexOf(shape)
1448
+
1449
+  const prevSibling = siblings[index - 1]
1450
+
1451
+  if (!prevSibling) {
1452
+    return shape.childIndex / 2
1453
+  }
1454
+
1455
+  return (shape.childIndex + prevSibling.childIndex) / 2
1456
+}

Loading…
Откажи
Сачувај