Просмотр исходного кода

refactors bounds, improves transforming rotating shapes

main
Steve Ruiz 4 лет назад
Родитель
Сommit
b752782753

+ 0
- 47
components/canvas/bounds-bg.tsx Просмотреть файл

1
-import { useRef } from "react"
2
-import state, { useSelector } from "state"
3
-import inputs from "state/inputs"
4
-import styled from "styles"
5
-
6
-export default function BoundsBg() {
7
-  const rBounds = useRef<SVGRectElement>(null)
8
-  const bounds = useSelector((state) => state.values.selectedBounds)
9
-  const isSelecting = useSelector((s) => s.isIn("selecting"))
10
-  const rotation = useSelector((s) => {
11
-    if (s.data.selectedIds.size === 1) {
12
-      const { shapes } = s.data.document.pages[s.data.currentPageId]
13
-      const selected = Array.from(s.data.selectedIds.values())[0]
14
-      return shapes[selected].rotation
15
-    } else {
16
-      return 0
17
-    }
18
-  })
19
-
20
-  if (!bounds) return null
21
-  if (!isSelecting) return null
22
-
23
-  const { minX, minY, width, height } = bounds
24
-
25
-  return (
26
-    <StyledBoundsBg
27
-      ref={rBounds}
28
-      x={minX}
29
-      y={minY}
30
-      width={Math.max(1, width)}
31
-      height={Math.max(1, height)}
32
-      onPointerDown={(e) => {
33
-        if (e.buttons !== 1) return
34
-        e.stopPropagation()
35
-        rBounds.current.setPointerCapture(e.pointerId)
36
-        state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
37
-      }}
38
-      transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
39
-        minY + height / 2
40
-      })`}
41
-    />
42
-  )
43
-}
44
-
45
-const StyledBoundsBg = styled("rect", {
46
-  fill: "$boundsBg",
47
-})

+ 0
- 285
components/canvas/bounds.tsx Просмотреть файл

1
-import state, { useSelector } from "state"
2
-import styled from "styles"
3
-import inputs from "state/inputs"
4
-import { useRef } from "react"
5
-import { TransformCorner, TransformEdge } from "types"
6
-import { lerp } from "utils/utils"
7
-
8
-export default function Bounds() {
9
-  const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
10
-  const isSelecting = useSelector((s) => s.isIn("selecting"))
11
-  const zoom = useSelector((s) => s.data.camera.zoom)
12
-  const bounds = useSelector((s) => s.values.selectedBounds)
13
-  const rotation = useSelector((s) => {
14
-    if (s.data.selectedIds.size === 1) {
15
-      const { shapes } = s.data.document.pages[s.data.currentPageId]
16
-      const selected = Array.from(s.data.selectedIds.values())[0]
17
-      return shapes[selected].rotation
18
-    } else {
19
-      return 0
20
-    }
21
-  })
22
-
23
-  if (!bounds) return null
24
-  if (!isSelecting) return null
25
-
26
-  let { minX, minY, maxX, maxY, width, height } = bounds
27
-
28
-  const p = 4 / zoom
29
-  const cp = p * 2
30
-
31
-  return (
32
-    <g
33
-      pointerEvents={isBrushing ? "none" : "all"}
34
-      transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
35
-        minY + height / 2
36
-      })`}
37
-    >
38
-      <StyledBounds
39
-        x={minX}
40
-        y={minY}
41
-        width={width}
42
-        height={height}
43
-        pointerEvents="none"
44
-      />
45
-      <EdgeHorizontal
46
-        x={minX + p}
47
-        y={minY}
48
-        width={Math.max(0, width - p * 2)}
49
-        height={p}
50
-        edge={TransformEdge.Top}
51
-      />
52
-      <EdgeVertical
53
-        x={maxX}
54
-        y={minY + p}
55
-        width={p}
56
-        height={Math.max(0, height - p * 2)}
57
-        edge={TransformEdge.Right}
58
-      />
59
-      <EdgeHorizontal
60
-        x={minX + p}
61
-        y={maxY}
62
-        width={Math.max(0, width - p * 2)}
63
-        height={p}
64
-        edge={TransformEdge.Bottom}
65
-      />
66
-      <EdgeVertical
67
-        x={minX}
68
-        y={minY + p}
69
-        width={p}
70
-        height={Math.max(0, height - p * 2)}
71
-        edge={TransformEdge.Left}
72
-      />
73
-      <Corner
74
-        x={minX}
75
-        y={minY}
76
-        width={cp}
77
-        height={cp}
78
-        corner={TransformCorner.TopLeft}
79
-      />
80
-      <Corner
81
-        x={maxX}
82
-        y={minY}
83
-        width={cp}
84
-        height={cp}
85
-        corner={TransformCorner.TopRight}
86
-      />
87
-      <Corner
88
-        x={maxX}
89
-        y={maxY}
90
-        width={cp}
91
-        height={cp}
92
-        corner={TransformCorner.BottomRight}
93
-      />
94
-      <Corner
95
-        x={minX}
96
-        y={maxY}
97
-        width={cp}
98
-        height={cp}
99
-        corner={TransformCorner.BottomLeft}
100
-      />
101
-      <RotateHandle x={minX + width / 2} y={minY - cp * 2} r={cp / 2} />
102
-    </g>
103
-  )
104
-}
105
-
106
-function RotateHandle({ x, y, r }: { x: number; y: number; r: number }) {
107
-  const rRotateHandle = useRef<SVGCircleElement>(null)
108
-
109
-  return (
110
-    <StyledRotateHandle
111
-      ref={rRotateHandle}
112
-      cx={x}
113
-      cy={y}
114
-      r={r}
115
-      onPointerDown={(e) => {
116
-        e.stopPropagation()
117
-        rRotateHandle.current.setPointerCapture(e.pointerId)
118
-        state.send("POINTED_ROTATE_HANDLE", inputs.pointerDown(e, "rotate"))
119
-      }}
120
-      onPointerUp={(e) => {
121
-        e.stopPropagation()
122
-        rRotateHandle.current.releasePointerCapture(e.pointerId)
123
-        rRotateHandle.current.replaceWith(rRotateHandle.current)
124
-        state.send("STOPPED_POINTING", inputs.pointerDown(e, "rotate"))
125
-      }}
126
-    />
127
-  )
128
-}
129
-
130
-function Corner({
131
-  x,
132
-  y,
133
-  width,
134
-  height,
135
-  corner,
136
-}: {
137
-  x: number
138
-  y: number
139
-  width: number
140
-  height: number
141
-  corner: TransformCorner
142
-}) {
143
-  const rCorner = useRef<SVGRectElement>(null)
144
-
145
-  return (
146
-    <g>
147
-      <StyledCorner
148
-        ref={rCorner}
149
-        x={x + width * -0.5}
150
-        y={y + height * -0.5}
151
-        width={width}
152
-        height={height}
153
-        corner={corner}
154
-        onPointerDown={(e) => {
155
-          e.stopPropagation()
156
-          rCorner.current.setPointerCapture(e.pointerId)
157
-          state.send("POINTED_BOUNDS_CORNER", inputs.pointerDown(e, corner))
158
-        }}
159
-        onPointerUp={(e) => {
160
-          e.stopPropagation()
161
-          rCorner.current.releasePointerCapture(e.pointerId)
162
-          rCorner.current.replaceWith(rCorner.current)
163
-          state.send("STOPPED_POINTING", inputs.pointerDown(e, corner))
164
-        }}
165
-      />
166
-    </g>
167
-  )
168
-}
169
-
170
-function EdgeHorizontal({
171
-  x,
172
-  y,
173
-  width,
174
-  height,
175
-  edge,
176
-}: {
177
-  x: number
178
-  y: number
179
-  width: number
180
-  height: number
181
-  edge: TransformEdge.Top | TransformEdge.Bottom
182
-}) {
183
-  const rEdge = useRef<SVGRectElement>(null)
184
-
185
-  return (
186
-    <StyledEdge
187
-      ref={rEdge}
188
-      x={x}
189
-      y={y - height / 2}
190
-      width={width}
191
-      height={height}
192
-      onPointerDown={(e) => {
193
-        e.stopPropagation()
194
-        rEdge.current.setPointerCapture(e.pointerId)
195
-        state.send("POINTED_BOUNDS_EDGE", inputs.pointerDown(e, edge))
196
-      }}
197
-      onPointerUp={(e) => {
198
-        e.stopPropagation()
199
-        e.preventDefault()
200
-        state.send("STOPPED_POINTING", inputs.pointerUp(e))
201
-        rEdge.current.releasePointerCapture(e.pointerId)
202
-        rEdge.current.replaceWith(rEdge.current)
203
-      }}
204
-      edge={edge}
205
-    />
206
-  )
207
-}
208
-
209
-function EdgeVertical({
210
-  x,
211
-  y,
212
-  width,
213
-  height,
214
-  edge,
215
-}: {
216
-  x: number
217
-  y: number
218
-  width: number
219
-  height: number
220
-  edge: TransformEdge.Right | TransformEdge.Left
221
-}) {
222
-  const rEdge = useRef<SVGRectElement>(null)
223
-
224
-  return (
225
-    <StyledEdge
226
-      ref={rEdge}
227
-      x={x - width / 2}
228
-      y={y}
229
-      width={width}
230
-      height={height}
231
-      onPointerDown={(e) => {
232
-        e.stopPropagation()
233
-        state.send("POINTED_BOUNDS_EDGE", inputs.pointerDown(e, edge))
234
-        rEdge.current.setPointerCapture(e.pointerId)
235
-      }}
236
-      onPointerUp={(e) => {
237
-        e.stopPropagation()
238
-        state.send("STOPPED_POINTING", inputs.pointerUp(e))
239
-        rEdge.current.releasePointerCapture(e.pointerId)
240
-        rEdge.current.replaceWith(rEdge.current)
241
-      }}
242
-      edge={edge}
243
-    />
244
-  )
245
-}
246
-
247
-const StyledEdge = styled("rect", {
248
-  stroke: "none",
249
-  fill: "none",
250
-  variants: {
251
-    edge: {
252
-      bottom_edge: { cursor: "ns-resize" },
253
-      right_edge: { cursor: "ew-resize" },
254
-      top_edge: { cursor: "ns-resize" },
255
-      left_edge: { cursor: "ew-resize" },
256
-    },
257
-  },
258
-})
259
-
260
-const StyledCorner = styled("rect", {
261
-  stroke: "$bounds",
262
-  fill: "#fff",
263
-  zStrokeWidth: 2,
264
-  variants: {
265
-    corner: {
266
-      top_left_corner: { cursor: "nwse-resize" },
267
-      top_right_corner: { cursor: "nesw-resize" },
268
-      bottom_right_corner: { cursor: "nwse-resize" },
269
-      bottom_left_corner: { cursor: "nesw-resize" },
270
-    },
271
-  },
272
-})
273
-
274
-const StyledRotateHandle = styled("circle", {
275
-  stroke: "$bounds",
276
-  fill: "#fff",
277
-  zStrokeWidth: 2,
278
-  cursor: "grab",
279
-})
280
-
281
-const StyledBounds = styled("rect", {
282
-  fill: "none",
283
-  stroke: "$bounds",
284
-  zStrokeWidth: 2,
285
-})

+ 46
- 0
components/canvas/bounds/bounding-box.tsx Просмотреть файл

1
+import * as React from "react"
2
+import { Edge, Corner } from "types"
3
+import { useSelector } from "state"
4
+import { getSelectedShapes, isMobile } from "utils/utils"
5
+
6
+import CenterHandle from "./center-handle"
7
+import CornerHandle from "./corner-handle"
8
+import EdgeHandle from "./edge-handle"
9
+import RotateHandle from "./rotate-handle"
10
+
11
+export default function Bounds() {
12
+  const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
13
+  const isSelecting = useSelector((s) => s.isIn("selecting"))
14
+  const zoom = useSelector((s) => s.data.camera.zoom)
15
+  const bounds = useSelector((s) => s.values.selectedBounds)
16
+  const rotation = useSelector(({ data }) =>
17
+    data.selectedIds.size === 1 ? getSelectedShapes(data)[0].rotation : 0
18
+  )
19
+
20
+  if (!bounds) return null
21
+  if (!isSelecting) return null
22
+
23
+  const size = (isMobile().any ? 16 : 8) / zoom // Touch target size
24
+
25
+  return (
26
+    <g
27
+      pointerEvents={isBrushing ? "none" : "all"}
28
+      transform={`
29
+        rotate(${rotation * (180 / Math.PI)}, 
30
+        ${(bounds.minX + bounds.maxX) / 2}, 
31
+        ${(bounds.minY + bounds.maxY) / 2})
32
+        translate(${bounds.minX},${bounds.minY})`}
33
+    >
34
+      <CenterHandle bounds={bounds} />
35
+      <EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
36
+      <EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
37
+      <EdgeHandle size={size} bounds={bounds} edge={Edge.Bottom} />
38
+      <EdgeHandle size={size} bounds={bounds} edge={Edge.Left} />
39
+      <CornerHandle size={size} bounds={bounds} corner={Corner.TopLeft} />
40
+      <CornerHandle size={size} bounds={bounds} corner={Corner.TopRight} />
41
+      <CornerHandle size={size} bounds={bounds} corner={Corner.BottomRight} />
42
+      <CornerHandle size={size} bounds={bounds} corner={Corner.BottomLeft} />
43
+      <RotateHandle size={size} bounds={bounds} />
44
+    </g>
45
+  )
46
+}

+ 58
- 0
components/canvas/bounds/bounds-bg.tsx Просмотреть файл

1
+import { useCallback, useRef } from "react"
2
+import state, { useSelector } from "state"
3
+import inputs from "state/inputs"
4
+import styled from "styles"
5
+import { getPage } from "utils/utils"
6
+
7
+function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
8
+  if (e.buttons !== 1) return
9
+  e.stopPropagation()
10
+  e.currentTarget.setPointerCapture(e.pointerId)
11
+  state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
12
+}
13
+
14
+function handlePointerUp(e: React.PointerEvent<SVGRectElement>) {
15
+  if (e.buttons !== 1) return
16
+  e.stopPropagation()
17
+  e.currentTarget.releasePointerCapture(e.pointerId)
18
+  state.send("STOPPED_POINTING", inputs.pointerUp(e))
19
+}
20
+
21
+export default function BoundsBg() {
22
+  const rBounds = useRef<SVGRectElement>(null)
23
+  const bounds = useSelector((state) => state.values.selectedBounds)
24
+  const isSelecting = useSelector((s) => s.isIn("selecting"))
25
+  const rotation = useSelector((s) => {
26
+    if (s.data.selectedIds.size === 1) {
27
+      const { shapes } = getPage(s.data)
28
+      const selected = Array.from(s.data.selectedIds.values())[0]
29
+      return shapes[selected].rotation
30
+    } else {
31
+      return 0
32
+    }
33
+  })
34
+
35
+  if (!bounds) return null
36
+  if (!isSelecting) return null
37
+
38
+  const { width, height } = bounds
39
+
40
+  return (
41
+    <StyledBoundsBg
42
+      ref={rBounds}
43
+      width={Math.max(1, width)}
44
+      height={Math.max(1, height)}
45
+      transform={`
46
+        rotate(${rotation * (180 / Math.PI)}, 
47
+        ${(bounds.minX + bounds.maxX) / 2}, 
48
+        ${(bounds.minY + bounds.maxY) / 2})
49
+        translate(${bounds.minX},${bounds.minY})`}
50
+      onPointerDown={handlePointerDown}
51
+      onPointerUp={handlePointerUp}
52
+    />
53
+  )
54
+}
55
+
56
+const StyledBoundsBg = styled("rect", {
57
+  fill: "$boundsBg",
58
+})

+ 18
- 0
components/canvas/bounds/center-handle.tsx Просмотреть файл

1
+import styled from "styles"
2
+import { Bounds } from "types"
3
+
4
+export default function CenterHandle({ bounds }: { bounds: Bounds }) {
5
+  return (
6
+    <StyledBounds
7
+      width={bounds.width}
8
+      height={bounds.height}
9
+      pointerEvents="none"
10
+    />
11
+  )
12
+}
13
+
14
+const StyledBounds = styled("rect", {
15
+  fill: "none",
16
+  stroke: "$bounds",
17
+  zStrokeWidth: 2,
18
+})

+ 43
- 0
components/canvas/bounds/corner-handle.tsx Просмотреть файл

1
+import useHandleEvents from "hooks/useBoundsHandleEvents"
2
+import styled from "styles"
3
+import { Corner, Bounds } from "types"
4
+
5
+export default function CornerHandle({
6
+  size,
7
+  corner,
8
+  bounds,
9
+}: {
10
+  size: number
11
+  bounds: Bounds
12
+  corner: Corner
13
+}) {
14
+  const events = useHandleEvents(corner)
15
+
16
+  const isTop = corner === Corner.TopLeft || corner === Corner.TopRight
17
+  const isLeft = corner === Corner.TopLeft || corner === Corner.BottomLeft
18
+
19
+  return (
20
+    <StyledCorner
21
+      corner={corner}
22
+      x={(isLeft ? 0 : bounds.width) - size / 2}
23
+      y={(isTop ? 0 : bounds.height) - size / 2}
24
+      width={size}
25
+      height={size}
26
+      {...events}
27
+    />
28
+  )
29
+}
30
+
31
+const StyledCorner = styled("rect", {
32
+  stroke: "$bounds",
33
+  fill: "#fff",
34
+  zStrokeWidth: 2,
35
+  variants: {
36
+    corner: {
37
+      [Corner.TopLeft]: { cursor: "nwse-resize" },
38
+      [Corner.TopRight]: { cursor: "nesw-resize" },
39
+      [Corner.BottomRight]: { cursor: "nwse-resize" },
40
+      [Corner.BottomLeft]: { cursor: "nesw-resize" },
41
+    },
42
+  },
43
+})

+ 42
- 0
components/canvas/bounds/edge-handle.tsx Просмотреть файл

1
+import useHandleEvents from "hooks/useBoundsHandleEvents"
2
+import styled from "styles"
3
+import { Edge, Bounds } from "types"
4
+
5
+export default function EdgeHandle({
6
+  size,
7
+  bounds,
8
+  edge,
9
+}: {
10
+  size: number
11
+  bounds: Bounds
12
+  edge: Edge
13
+}) {
14
+  const events = useHandleEvents(edge)
15
+
16
+  const isHorizontal = edge === Edge.Top || edge === Edge.Bottom
17
+  const isFarEdge = edge === Edge.Right || edge === Edge.Bottom
18
+
19
+  return (
20
+    <StyledEdge
21
+      edge={edge}
22
+      x={isHorizontal ? size / 2 : (isFarEdge ? bounds.width : 0) - size / 2}
23
+      y={isHorizontal ? (isFarEdge ? bounds.height : 0) - size / 2 : size / 2}
24
+      width={isHorizontal ? Math.max(0, bounds.width - size) : size}
25
+      height={isHorizontal ? size : Math.max(0, bounds.height - size)}
26
+      {...events}
27
+    />
28
+  )
29
+}
30
+
31
+const StyledEdge = styled("rect", {
32
+  stroke: "none",
33
+  fill: "none",
34
+  variants: {
35
+    edge: {
36
+      [Edge.Top]: { cursor: "ns-resize" },
37
+      [Edge.Right]: { cursor: "ew-resize" },
38
+      [Edge.Bottom]: { cursor: "ns-resize" },
39
+      [Edge.Left]: { cursor: "ew-resize" },
40
+    },
41
+  },
42
+})

+ 30
- 0
components/canvas/bounds/rotate-handle.tsx Просмотреть файл

1
+import useHandleEvents from "hooks/useBoundsHandleEvents"
2
+import styled from "styles"
3
+import { Bounds } from "types"
4
+
5
+export default function Rotate({
6
+  bounds,
7
+  size,
8
+}: {
9
+  bounds: Bounds
10
+  size: number
11
+}) {
12
+  const events = useHandleEvents("rotate")
13
+
14
+  return (
15
+    <StyledRotateHandle
16
+      cursor="grab"
17
+      cx={bounds.width / 2}
18
+      cy={size * -2}
19
+      r={size / 2}
20
+      {...events}
21
+    />
22
+  )
23
+}
24
+
25
+const StyledRotateHandle = styled("circle", {
26
+  stroke: "$bounds",
27
+  fill: "#fff",
28
+  zStrokeWidth: 2,
29
+  cursor: "grab",
30
+})

+ 2
- 2
components/canvas/canvas.tsx Просмотреть файл

5
 import Page from "./page"
5
 import Page from "./page"
6
 import Brush from "./brush"
6
 import Brush from "./brush"
7
 import state from "state"
7
 import state from "state"
8
-import Bounds from "./bounds"
9
-import BoundsBg from "./bounds-bg"
8
+import Bounds from "./bounds/bounding-box"
9
+import BoundsBg from "./bounds/bounds-bg"
10
 import inputs from "state/inputs"
10
 import inputs from "state/inputs"
11
 
11
 
12
 export default function Canvas() {
12
 export default function Canvas() {

+ 2
- 2
components/canvas/page.tsx Просмотреть файл

1
 import { useSelector } from "state"
1
 import { useSelector } from "state"
2
-import { deepCompareArrays } from "utils/utils"
2
+import { deepCompareArrays, getPage } from "utils/utils"
3
 import Shape from "./shape"
3
 import Shape from "./shape"
4
 
4
 
5
 /* 
5
 /* 
10
 
10
 
11
 export default function Page() {
11
 export default function Page() {
12
   const currentPageShapeIds = useSelector(
12
   const currentPageShapeIds = useSelector(
13
-    ({ data }) => Object.keys(data.document.pages[data.currentPageId].shapes),
13
+    ({ data }) => Object.keys(getPage(data).shapes),
14
     deepCompareArrays
14
     deepCompareArrays
15
   )
15
   )
16
 
16
 

+ 2
- 3
components/canvas/shape.tsx Просмотреть файл

3
 import inputs from "state/inputs"
3
 import inputs from "state/inputs"
4
 import { getShapeUtils } from "lib/shape-utils"
4
 import { getShapeUtils } from "lib/shape-utils"
5
 import styled from "styles"
5
 import styled from "styles"
6
+import { getPage } from "utils/utils"
6
 
7
 
7
 function Shape({ id }: { id: string }) {
8
 function Shape({ id }: { id: string }) {
8
   const rGroup = useRef<SVGGElement>(null)
9
   const rGroup = useRef<SVGGElement>(null)
11
 
12
 
12
   const isSelected = useSelector((state) => state.values.selectedIds.has(id))
13
   const isSelected = useSelector((state) => state.values.selectedIds.has(id))
13
 
14
 
14
-  const shape = useSelector(
15
-    ({ data }) => data.document.pages[data.currentPageId].shapes[id]
16
-  )
15
+  const shape = useSelector(({ data }) => getPage(data).shapes[id])
17
 
16
 
18
   const handlePointerDown = useCallback(
17
   const handlePointerDown = useCallback(
19
     (e: React.PointerEvent) => {
18
     (e: React.PointerEvent) => {

+ 38
- 0
hooks/useBoundsHandleEvents.ts Просмотреть файл

1
+import { useCallback, useRef } from "react"
2
+import inputs from "state/inputs"
3
+import { Edge, Corner } from "types"
4
+
5
+import state from "../state"
6
+
7
+export default function useBoundsHandleEvents(
8
+  handle: Edge | Corner | "rotate"
9
+) {
10
+  const onPointerDown = useCallback(
11
+    (e) => {
12
+      if (e.buttons !== 1) return
13
+      e.stopPropagation()
14
+      e.currentTarget.setPointerCapture(e.pointerId)
15
+      state.send("POINTED_BOUNDS_HANDLE", inputs.pointerDown(e, handle))
16
+    },
17
+    [handle]
18
+  )
19
+
20
+  const onPointerMove = useCallback(
21
+    (e) => {
22
+      if (e.buttons !== 1) return
23
+      e.stopPropagation()
24
+      state.send("MOVED_POINTER", inputs.pointerMove(e))
25
+    },
26
+    [handle]
27
+  )
28
+
29
+  const onPointerUp = useCallback((e) => {
30
+    if (e.buttons !== 1) return
31
+    e.stopPropagation()
32
+    e.currentTarget.releasePointerCapture(e.pointerId)
33
+    e.currentTarget.replaceWith(e.currentTarget)
34
+    state.send("STOPPED_POINTING", inputs.pointerUp(e))
35
+  }, [])
36
+
37
+  return { onPointerDown, onPointerMove, onPointerUp }
38
+}

+ 9
- 9
lib/shape-utils/circle.tsx Просмотреть файл

1
 import { v4 as uuid } from "uuid"
1
 import { v4 as uuid } from "uuid"
2
 import * as vec from "utils/vec"
2
 import * as vec from "utils/vec"
3
-import { CircleShape, ShapeType, TransformCorner, TransformEdge } from "types"
3
+import { CircleShape, ShapeType, Corner, Edge } from "types"
4
 import { registerShapeUtils } from "./index"
4
 import { registerShapeUtils } from "./index"
5
 import { boundsContained } from "utils/bounds"
5
 import { boundsContained } from "utils/bounds"
6
 import { intersectCircleBounds } from "utils/intersections"
6
 import { intersectCircleBounds } from "utils/intersections"
99
 
99
 
100
     // Set the new corner or position depending on the anchor
100
     // Set the new corner or position depending on the anchor
101
     switch (anchor) {
101
     switch (anchor) {
102
-      case TransformCorner.TopLeft: {
102
+      case Corner.TopLeft: {
103
         shape.radius = Math.min(bounds.width, bounds.height) / 2
103
         shape.radius = Math.min(bounds.width, bounds.height) / 2
104
         shape.point = [
104
         shape.point = [
105
           bounds.maxX - shape.radius * 2,
105
           bounds.maxX - shape.radius * 2,
107
         ]
107
         ]
108
         break
108
         break
109
       }
109
       }
110
-      case TransformCorner.TopRight: {
110
+      case Corner.TopRight: {
111
         shape.radius = Math.min(bounds.width, bounds.height) / 2
111
         shape.radius = Math.min(bounds.width, bounds.height) / 2
112
         shape.point = [bounds.minX, bounds.maxY - shape.radius * 2]
112
         shape.point = [bounds.minX, bounds.maxY - shape.radius * 2]
113
         break
113
         break
114
       }
114
       }
115
-      case TransformCorner.BottomRight: {
115
+      case Corner.BottomRight: {
116
         shape.radius = Math.min(bounds.width, bounds.height) / 2
116
         shape.radius = Math.min(bounds.width, bounds.height) / 2
117
         shape.point = [
117
         shape.point = [
118
           bounds.maxX - shape.radius * 2,
118
           bounds.maxX - shape.radius * 2,
121
         break
121
         break
122
         break
122
         break
123
       }
123
       }
124
-      case TransformCorner.BottomLeft: {
124
+      case Corner.BottomLeft: {
125
         shape.radius = Math.min(bounds.width, bounds.height) / 2
125
         shape.radius = Math.min(bounds.width, bounds.height) / 2
126
         shape.point = [bounds.maxX - shape.radius * 2, bounds.minY]
126
         shape.point = [bounds.maxX - shape.radius * 2, bounds.minY]
127
         break
127
         break
128
       }
128
       }
129
-      case TransformEdge.Top: {
129
+      case Edge.Top: {
130
         shape.radius = bounds.height / 2
130
         shape.radius = bounds.height / 2
131
         shape.point = [
131
         shape.point = [
132
           bounds.minX + (bounds.width / 2 - shape.radius),
132
           bounds.minX + (bounds.width / 2 - shape.radius),
134
         ]
134
         ]
135
         break
135
         break
136
       }
136
       }
137
-      case TransformEdge.Right: {
137
+      case Edge.Right: {
138
         shape.radius = bounds.width / 2
138
         shape.radius = bounds.width / 2
139
         shape.point = [
139
         shape.point = [
140
           bounds.maxX - shape.radius * 2,
140
           bounds.maxX - shape.radius * 2,
142
         ]
142
         ]
143
         break
143
         break
144
       }
144
       }
145
-      case TransformEdge.Bottom: {
145
+      case Edge.Bottom: {
146
         shape.radius = bounds.height / 2
146
         shape.radius = bounds.height / 2
147
         shape.point = [
147
         shape.point = [
148
           bounds.minX + (bounds.width / 2 - shape.radius),
148
           bounds.minX + (bounds.width / 2 - shape.radius),
150
         ]
150
         ]
151
         break
151
         break
152
       }
152
       }
153
-      case TransformEdge.Left: {
153
+      case Edge.Left: {
154
         shape.radius = bounds.width / 2
154
         shape.radius = bounds.width / 2
155
         shape.point = [
155
         shape.point = [
156
           bounds.minX,
156
           bounds.minX,

+ 6
- 4
lib/shape-utils/index.tsx Просмотреть файл

4
   Shape,
4
   Shape,
5
   Shapes,
5
   Shapes,
6
   ShapeType,
6
   ShapeType,
7
-  TransformCorner,
8
-  TransformEdge,
7
+  Corner,
8
+  Edge,
9
 } from "types"
9
 } from "types"
10
 import circle from "./circle"
10
 import circle from "./circle"
11
 import dot from "./dot"
11
 import dot from "./dot"
60
     shape: K,
60
     shape: K,
61
     bounds: Bounds,
61
     bounds: Bounds,
62
     info: {
62
     info: {
63
-      type: TransformEdge | TransformCorner | "center"
63
+      type: Edge | Corner | "center"
64
       initialShape: K
64
       initialShape: K
65
       scaleX: number
65
       scaleX: number
66
       scaleY: number
66
       scaleY: number
67
+      transformOrigin: number[]
67
     }
68
     }
68
   ): K
69
   ): K
69
 
70
 
72
     shape: K,
73
     shape: K,
73
     bounds: Bounds,
74
     bounds: Bounds,
74
     info: {
75
     info: {
75
-      type: TransformEdge | TransformCorner | "center"
76
+      type: Edge | Corner | "center"
76
       initialShape: K
77
       initialShape: K
77
       scaleX: number
78
       scaleX: number
78
       scaleY: number
79
       scaleY: number
80
+      transformOrigin: number[]
79
     }
81
     }
80
   ): K
82
   ): K
81
 
83
 

+ 20
- 17
lib/shape-utils/rectangle.tsx Просмотреть файл

1
 import { v4 as uuid } from "uuid"
1
 import { v4 as uuid } from "uuid"
2
 import * as vec from "utils/vec"
2
 import * as vec from "utils/vec"
3
-import {
4
-  RectangleShape,
5
-  ShapeType,
6
-  TransformCorner,
7
-  TransformEdge,
8
-} from "types"
3
+import { RectangleShape, ShapeType, Corner, Edge } from "types"
9
 import { registerShapeUtils } from "./index"
4
 import { registerShapeUtils } from "./index"
10
 import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
5
 import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
11
 import {
6
 import {
12
   getBoundsFromPoints,
7
   getBoundsFromPoints,
13
   getRotatedCorners,
8
   getRotatedCorners,
9
+  getRotatedSize,
10
+  lerp,
14
   rotateBounds,
11
   rotateBounds,
15
   translateBounds,
12
   translateBounds,
16
 } from "utils/utils"
13
 } from "utils/utils"
99
     return shape
96
     return shape
100
   },
97
   },
101
 
98
 
102
-  transform(shape, bounds, { initialShape, scaleX, scaleY }) {
99
+  transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
103
     if (shape.rotation === 0) {
100
     if (shape.rotation === 0) {
104
       shape.size = [bounds.width, bounds.height]
101
       shape.size = [bounds.width, bounds.height]
105
       shape.point = [bounds.minX, bounds.minY]
102
       shape.point = [bounds.minX, bounds.minY]
106
     } else {
103
     } else {
107
-      // Center shape in resized bounds
104
+      // Size
108
       shape.size = vec.mul(
105
       shape.size = vec.mul(
109
         initialShape.size,
106
         initialShape.size,
110
         Math.min(Math.abs(scaleX), Math.abs(scaleY))
107
         Math.min(Math.abs(scaleX), Math.abs(scaleY))
111
       )
108
       )
112
 
109
 
113
-      shape.point = vec.sub(
114
-        vec.med([bounds.minX, bounds.minY], [bounds.maxX, bounds.maxY]),
115
-        vec.div(shape.size, 2)
116
-      )
110
+      // Point
111
+      shape.point = [
112
+        bounds.minX +
113
+          (bounds.width - shape.size[0]) *
114
+            (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
115
+        bounds.minY +
116
+          (bounds.height - shape.size[1]) *
117
+            (scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
118
+      ]
119
+
120
+      // Rotation
121
+      shape.rotation =
122
+        (scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
123
+          ? -initialShape.rotation
124
+          : initialShape.rotation
117
     }
125
     }
118
 
126
 
119
-    // Set rotation for flipped shapes
120
-    shape.rotation = initialShape.rotation
121
-    if (scaleX < 0) shape.rotation *= -1
122
-    if (scaleY < 0) shape.rotation *= -1
123
-
124
     return shape
127
     return shape
125
   },
128
   },
126
 
129
 

+ 1
- 0
package.json Просмотреть файл

13
     "@stitches/react": "^0.1.9",
13
     "@stitches/react": "^0.1.9",
14
     "@types/uuid": "^8.3.0",
14
     "@types/uuid": "^8.3.0",
15
     "framer-motion": "^4.1.16",
15
     "framer-motion": "^4.1.16",
16
+    "ismobilejs": "^1.1.1",
16
     "next": "10.2.0",
17
     "next": "10.2.0",
17
     "perfect-freehand": "^0.4.7",
18
     "perfect-freehand": "^0.4.7",
18
     "prettier": "^2.3.0",
19
     "prettier": "^2.3.0",

+ 5
- 4
state/commands/create-shape.ts Просмотреть файл

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

+ 3
- 2
state/commands/direct.ts Просмотреть файл

2
 import history from "../history"
2
 import history from "../history"
3
 import { DirectionSnapshot } from "state/sessions/direction-session"
3
 import { DirectionSnapshot } from "state/sessions/direction-session"
4
 import { Data, LineShape, RayShape } from "types"
4
 import { Data, LineShape, RayShape } from "types"
5
+import { getPage } from "utils/utils"
5
 
6
 
6
 export default function directCommand(
7
 export default function directCommand(
7
   data: Data,
8
   data: Data,
14
       name: "set_direction",
15
       name: "set_direction",
15
       category: "canvas",
16
       category: "canvas",
16
       do(data) {
17
       do(data) {
17
-        const { shapes } = data.document.pages[after.currentPageId]
18
+        const { shapes } = getPage(data)
18
 
19
 
19
         for (let { id, direction } of after.shapes) {
20
         for (let { id, direction } of after.shapes) {
20
           const shape = shapes[id] as RayShape | LineShape
21
           const shape = shapes[id] as RayShape | LineShape
23
         }
24
         }
24
       },
25
       },
25
       undo(data) {
26
       undo(data) {
26
-        const { shapes } = data.document.pages[before.currentPageId]
27
+        const { shapes } = getPage(data, before.currentPageId)
27
 
28
 
28
         for (let { id, direction } of after.shapes) {
29
         for (let { id, direction } of after.shapes) {
29
           const shape = shapes[id] as RayShape | LineShape
30
           const shape = shapes[id] as RayShape | LineShape

+ 8
- 6
state/commands/generate.ts Просмотреть файл

2
 import history from "../history"
2
 import history from "../history"
3
 import { CodeControl, Data, Shape } from "types"
3
 import { CodeControl, Data, Shape } from "types"
4
 import { current } from "immer"
4
 import { current } from "immer"
5
+import { getPage } from "utils/utils"
5
 
6
 
6
 export default function generateCommand(
7
 export default function generateCommand(
7
   data: Data,
8
   data: Data,
9
   generatedShapes: Shape[]
10
   generatedShapes: Shape[]
10
 ) {
11
 ) {
11
   const cData = current(data)
12
   const cData = current(data)
13
+  const page = getPage(cData)
12
 
14
 
13
-  const prevGeneratedShapes = Object.values(
14
-    cData.document.pages[currentPageId].shapes
15
-  ).filter((shape) => shape.isGenerated)
15
+  const currentShapes = page.shapes
16
 
16
 
17
-  const currentShapes = data.document.pages[currentPageId].shapes
17
+  const prevGeneratedShapes = Object.values(currentShapes).filter(
18
+    (shape) => shape.isGenerated
19
+  )
18
 
20
 
19
   // Remove previous generated shapes
21
   // Remove previous generated shapes
20
   for (let id in currentShapes) {
22
   for (let id in currentShapes) {
34
       name: "translate_shapes",
36
       name: "translate_shapes",
35
       category: "canvas",
37
       category: "canvas",
36
       do(data) {
38
       do(data) {
37
-        const { shapes } = data.document.pages[currentPageId]
39
+        const { shapes } = getPage(data)
38
 
40
 
39
         data.selectedIds.clear()
41
         data.selectedIds.clear()
40
 
42
 
51
         }
53
         }
52
       },
54
       },
53
       undo(data) {
55
       undo(data) {
54
-        const { shapes } = data.document.pages[currentPageId]
56
+        const { shapes } = getPage(data)
55
 
57
 
56
         // Remove generated shapes
58
         // Remove generated shapes
57
         for (let id in shapes) {
59
         for (let id in shapes) {

+ 3
- 2
state/commands/rotate.ts Просмотреть файл

2
 import history from "../history"
2
 import history from "../history"
3
 import { Data } from "types"
3
 import { Data } from "types"
4
 import { RotateSnapshot } from "state/sessions/rotate-session"
4
 import { RotateSnapshot } from "state/sessions/rotate-session"
5
+import { getPage } from "utils/utils"
5
 
6
 
6
 export default function rotateCommand(
7
 export default function rotateCommand(
7
   data: Data,
8
   data: Data,
14
       name: "translate_shapes",
15
       name: "translate_shapes",
15
       category: "canvas",
16
       category: "canvas",
16
       do(data) {
17
       do(data) {
17
-        const { shapes } = data.document.pages[after.currentPageId]
18
+        const { shapes } = getPage(data)
18
 
19
 
19
         for (let { id, point, rotation } of after.shapes) {
20
         for (let { id, point, rotation } of after.shapes) {
20
           const shape = shapes[id]
21
           const shape = shapes[id]
25
         data.boundsRotation = after.boundsRotation
26
         data.boundsRotation = after.boundsRotation
26
       },
27
       },
27
       undo(data) {
28
       undo(data) {
28
-        const { shapes } = data.document.pages[before.currentPageId]
29
+        const { shapes } = getPage(data, before.currentPageId)
29
 
30
 
30
         for (let { id, point, rotation } of before.shapes) {
31
         for (let { id, point, rotation } of before.shapes) {
31
           const shape = shapes[id]
32
           const shape = shapes[id]

+ 14
- 9
state/commands/transform-single.ts Просмотреть файл

1
 import Command from "./command"
1
 import Command from "./command"
2
 import history from "../history"
2
 import history from "../history"
3
-import { Data, TransformCorner, TransformEdge } from "types"
3
+import { Data, Corner, Edge } from "types"
4
 import { getShapeUtils } from "lib/shape-utils"
4
 import { getShapeUtils } from "lib/shape-utils"
5
 import { current } from "immer"
5
 import { current } from "immer"
6
 import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
6
 import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
7
+import { getPage } from "utils/utils"
7
 
8
 
8
 export default function transformSingleCommand(
9
 export default function transformSingleCommand(
9
   data: Data,
10
   data: Data,
13
   scaleY: number,
14
   scaleY: number,
14
   isCreating: boolean
15
   isCreating: boolean
15
 ) {
16
 ) {
16
-  const shape =
17
-    current(data).document.pages[after.currentPageId].shapes[after.id]
17
+  const shape = getPage(data, after.currentPageId).shapes[after.id]
18
 
18
 
19
   history.execute(
19
   history.execute(
20
     data,
20
     data,
23
       category: "canvas",
23
       category: "canvas",
24
       manualSelection: true,
24
       manualSelection: true,
25
       do(data) {
25
       do(data) {
26
-        const { id, currentPageId, type, initialShape, initialShapeBounds } =
27
-          after
26
+        const { id, type, initialShape, initialShapeBounds } = after
27
+
28
+        const { shapes } = getPage(data, after.currentPageId)
28
 
29
 
29
         data.selectedIds.clear()
30
         data.selectedIds.clear()
30
         data.selectedIds.add(id)
31
         data.selectedIds.add(id)
31
 
32
 
32
         if (isCreating) {
33
         if (isCreating) {
33
-          data.document.pages[currentPageId].shapes[id] = shape
34
+          shapes[id] = shape
34
         } else {
35
         } else {
35
           getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
36
           getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
36
             type,
37
             type,
37
             initialShape,
38
             initialShape,
38
             scaleX,
39
             scaleX,
39
             scaleY,
40
             scaleY,
41
+            transformOrigin: [0.5, 0.5],
40
           })
42
           })
41
         }
43
         }
42
       },
44
       },
43
       undo(data) {
45
       undo(data) {
44
-        const { id, currentPageId, type, initialShapeBounds } = before
46
+        const { id, type, initialShapeBounds } = before
47
+
48
+        const { shapes } = getPage(data, before.currentPageId)
45
 
49
 
46
         data.selectedIds.clear()
50
         data.selectedIds.clear()
47
 
51
 
48
         if (isCreating) {
52
         if (isCreating) {
49
-          delete data.document.pages[currentPageId].shapes[id]
53
+          delete shapes[id]
50
         } else {
54
         } else {
51
-          const shape = data.document.pages[currentPageId].shapes[id]
55
+          const shape = shapes[id]
52
           data.selectedIds.add(id)
56
           data.selectedIds.add(id)
53
 
57
 
54
           getShapeUtils(shape).transform(shape, initialShapeBounds, {
58
           getShapeUtils(shape).transform(shape, initialShapeBounds, {
56
             initialShape: after.initialShape,
60
             initialShape: after.initialShape,
57
             scaleX: 1,
61
             scaleX: 1,
58
             scaleY: 1,
62
             scaleY: 1,
63
+            transformOrigin: [0.5, 0.5],
59
           })
64
           })
60
         }
65
         }
61
       },
66
       },

+ 16
- 7
state/commands/transform.ts Просмотреть файл

1
 import Command from "./command"
1
 import Command from "./command"
2
 import history from "../history"
2
 import history from "../history"
3
-import { Data, TransformCorner, TransformEdge } from "types"
3
+import { Data, Corner, Edge } from "types"
4
 import { TransformSnapshot } from "state/sessions/transform-session"
4
 import { TransformSnapshot } from "state/sessions/transform-session"
5
 import { getShapeUtils } from "lib/shape-utils"
5
 import { getShapeUtils } from "lib/shape-utils"
6
+import { getPage } from "utils/utils"
6
 
7
 
7
 export default function transformCommand(
8
 export default function transformCommand(
8
   data: Data,
9
   data: Data,
17
       name: "translate_shapes",
18
       name: "translate_shapes",
18
       category: "canvas",
19
       category: "canvas",
19
       do(data) {
20
       do(data) {
20
-        const { type, currentPageId, selectedIds } = after
21
+        const { type, selectedIds } = after
22
+
23
+        const { shapes } = getPage(data)
21
 
24
 
22
         selectedIds.forEach((id) => {
25
         selectedIds.forEach((id) => {
23
-          const { initialShape, initialShapeBounds } = after.shapeBounds[id]
24
-          const shape = data.document.pages[currentPageId].shapes[id]
26
+          const { initialShape, initialShapeBounds, transformOrigin } =
27
+            after.shapeBounds[id]
28
+          const shape = shapes[id]
25
 
29
 
26
           getShapeUtils(shape).transform(shape, initialShapeBounds, {
30
           getShapeUtils(shape).transform(shape, initialShapeBounds, {
27
             type,
31
             type,
28
             initialShape,
32
             initialShape,
29
             scaleX: 1,
33
             scaleX: 1,
30
             scaleY: 1,
34
             scaleY: 1,
35
+            transformOrigin,
31
           })
36
           })
32
         })
37
         })
33
       },
38
       },
34
       undo(data) {
39
       undo(data) {
35
-        const { type, currentPageId, selectedIds } = before
40
+        const { type, selectedIds } = before
41
+
42
+        const { shapes } = getPage(data)
36
 
43
 
37
         selectedIds.forEach((id) => {
44
         selectedIds.forEach((id) => {
38
-          const { initialShape, initialShapeBounds } = before.shapeBounds[id]
39
-          const shape = data.document.pages[currentPageId].shapes[id]
45
+          const { initialShape, initialShapeBounds, transformOrigin } =
46
+            before.shapeBounds[id]
47
+          const shape = shapes[id]
40
 
48
 
41
           getShapeUtils(shape).transform(shape, initialShapeBounds, {
49
           getShapeUtils(shape).transform(shape, initialShapeBounds, {
42
             type,
50
             type,
43
             initialShape,
51
             initialShape,
44
             scaleX: 1,
52
             scaleX: 1,
45
             scaleY: 1,
53
             scaleY: 1,
54
+            transformOrigin,
46
           })
55
           })
47
         })
56
         })
48
       },
57
       },

+ 5
- 4
state/commands/translate.ts Просмотреть файл

2
 import history from "../history"
2
 import history from "../history"
3
 import { TranslateSnapshot } from "state/sessions/translate-session"
3
 import { TranslateSnapshot } from "state/sessions/translate-session"
4
 import { Data } from "types"
4
 import { Data } from "types"
5
+import { getPage } from "utils/utils"
5
 
6
 
6
 export default function translateCommand(
7
 export default function translateCommand(
7
   data: Data,
8
   data: Data,
18
       do(data, initial) {
19
       do(data, initial) {
19
         if (initial) return
20
         if (initial) return
20
 
21
 
21
-        const { shapes } = data.document.pages[after.currentPageId]
22
-        const { initialShapes } = after
22
+        const { initialShapes, currentPageId } = after
23
+        const { shapes } = getPage(data, currentPageId)
23
         const { clones } = before // !
24
         const { clones } = before // !
24
 
25
 
25
         data.selectedIds.clear()
26
         data.selectedIds.clear()
36
         }
37
         }
37
       },
38
       },
38
       undo(data) {
39
       undo(data) {
39
-        const { shapes } = data.document.pages[before.currentPageId]
40
-        const { initialShapes, clones } = before
40
+        const { initialShapes, clones, currentPageId } = before
41
+        const { shapes } = getPage(data, currentPageId)
41
 
42
 
42
         data.selectedIds.clear()
43
         data.selectedIds.clear()
43
 
44
 

+ 23
- 34
state/sessions/brush-session.ts Просмотреть файл

1
 import { current } from "immer"
1
 import { current } from "immer"
2
-import { ShapeUtil, Bounds, Data, Shapes } from "types"
2
+import { Bounds, Data } from "types"
3
 import BaseSession from "./base-session"
3
 import BaseSession from "./base-session"
4
-import shapes, { getShapeUtils } from "lib/shape-utils"
5
-import { getBoundsFromPoints } from "utils/utils"
4
+import { getShapeUtils } from "lib/shape-utils"
5
+import { getBoundsFromPoints, getShapes } from "utils/utils"
6
 import * as vec from "utils/vec"
6
 import * as vec from "utils/vec"
7
 
7
 
8
-interface BrushSnapshot {
9
-  selectedIds: Set<string>
10
-  shapes: { id: string; test: (bounds: Bounds) => boolean }[]
11
-}
12
-
13
 export default class BrushSession extends BaseSession {
8
 export default class BrushSession extends BaseSession {
14
   origin: number[]
9
   origin: number[]
15
   snapshot: BrushSnapshot
10
   snapshot: BrushSnapshot
19
 
14
 
20
     this.origin = vec.round(point)
15
     this.origin = vec.round(point)
21
 
16
 
22
-    this.snapshot = BrushSession.getSnapshot(data)
17
+    this.snapshot = getBrushSnapshot(data)
23
   }
18
   }
24
 
19
 
25
   update = (data: Data, point: number[]) => {
20
   update = (data: Data, point: number[]) => {
27
 
22
 
28
     const brushBounds = getBoundsFromPoints([origin, point])
23
     const brushBounds = getBoundsFromPoints([origin, point])
29
 
24
 
30
-    for (let { test, id } of snapshot.shapes) {
25
+    for (let id in snapshot.shapeHitTests) {
26
+      const test = snapshot.shapeHitTests[id]
31
       if (test(brushBounds)) {
27
       if (test(brushBounds)) {
32
         data.selectedIds.add(id)
28
         data.selectedIds.add(id)
33
       } else if (data.selectedIds.has(id)) {
29
       } else if (data.selectedIds.has(id)) {
46
   complete = (data: Data) => {
42
   complete = (data: Data) => {
47
     data.brush = undefined
43
     data.brush = undefined
48
   }
44
   }
45
+}
49
 
46
 
50
-  /**
51
-   * Get a snapshot of the current selected ids, for each shape that is
52
-   * not already selected, the shape's id and a test to see whether the
53
-   * brush will intersect that shape. For tests, start broad -> fine.
54
-   * @param data
55
-   * @returns
56
-   */
57
-  static getSnapshot(data: Data): BrushSnapshot {
58
-    const {
59
-      selectedIds,
60
-      document: { pages },
61
-      currentPageId,
62
-    } = current(data)
63
-
64
-    return {
65
-      selectedIds: new Set(data.selectedIds),
66
-      shapes: Object.values(pages[currentPageId].shapes)
67
-        .filter((shape) => !selectedIds.has(shape.id))
68
-        .map((shape) => ({
69
-          id: shape.id,
70
-          test: (brushBounds: Bounds): boolean =>
71
-            getShapeUtils(shape).hitTestBounds(shape, brushBounds),
72
-        })),
73
-    }
47
+/**
48
+ * Get a snapshot of the current selected ids, for each shape that is
49
+ * not already selected, the shape's id and a test to see whether the
50
+ * brush will intersect that shape. For tests, start broad -> fine.
51
+ */
52
+export function getBrushSnapshot(data: Data) {
53
+  return {
54
+    selectedIds: new Set(data.selectedIds),
55
+    shapeHitTests: Object.fromEntries(
56
+      getShapes(current(data)).map((shape) => [
57
+        shape.id,
58
+        (bounds: Bounds) => getShapeUtils(shape).hitTestBounds(shape, bounds),
59
+      ])
60
+    ),
74
   }
61
   }
75
 }
62
 }
63
+
64
+export type BrushSnapshot = ReturnType<typeof getBrushSnapshot>

+ 9
- 17
state/sessions/direction-session.ts Просмотреть файл

3
 import BaseSession from "./base-session"
3
 import BaseSession from "./base-session"
4
 import commands from "state/commands"
4
 import commands from "state/commands"
5
 import { current } from "immer"
5
 import { current } from "immer"
6
+import { getPage } from "utils/utils"
6
 
7
 
7
 export default class DirectionSession extends BaseSession {
8
 export default class DirectionSession extends BaseSession {
8
   delta = [0, 0]
9
   delta = [0, 0]
16
   }
17
   }
17
 
18
 
18
   update(data: Data, point: number[]) {
19
   update(data: Data, point: number[]) {
19
-    const { currentPageId, shapes } = this.snapshot
20
-    const { document } = data
20
+    const { shapes } = this.snapshot
21
+
22
+    const page = getPage(data)
21
 
23
 
22
     for (let { id } of shapes) {
24
     for (let { id } of shapes) {
23
-      const shape = document.pages[currentPageId].shapes[id] as
24
-        | RayShape
25
-        | LineShape
25
+      const shape = page.shapes[id] as RayShape | LineShape
26
 
26
 
27
       shape.direction = vec.uni(vec.vec(shape.point, point))
27
       shape.direction = vec.uni(vec.vec(shape.point, point))
28
     }
28
     }
29
   }
29
   }
30
 
30
 
31
   cancel(data: Data) {
31
   cancel(data: Data) {
32
-    const { document } = data
32
+    const page = getPage(data, this.snapshot.currentPageId)
33
 
33
 
34
     for (let { id, direction } of this.snapshot.shapes) {
34
     for (let { id, direction } of this.snapshot.shapes) {
35
-      const shape = document.pages[this.snapshot.currentPageId].shapes[id] as
36
-        | RayShape
37
-        | LineShape
38
-
35
+      const shape = page.shapes[id] as RayShape | LineShape
39
       shape.direction = direction
36
       shape.direction = direction
40
     }
37
     }
41
   }
38
   }
46
 }
43
 }
47
 
44
 
48
 export function getDirectionSnapshot(data: Data) {
45
 export function getDirectionSnapshot(data: Data) {
49
-  const {
50
-    document: { pages },
51
-    currentPageId,
52
-  } = current(data)
53
-
54
-  const { shapes } = pages[currentPageId]
46
+  const { shapes } = getPage(current(data))
55
 
47
 
56
   let snapshapes: { id: string; direction: number[] }[] = []
48
   let snapshapes: { id: string; direction: number[] }[] = []
57
 
49
 
63
   })
55
   })
64
 
56
 
65
   return {
57
   return {
66
-    currentPageId,
58
+    currentPageId: data.currentPageId,
67
     shapes: snapshapes,
59
     shapes: snapshapes,
68
   }
60
   }
69
 }
61
 }

+ 30
- 34
state/sessions/rotate-session.ts Просмотреть файл

3
 import BaseSession from "./base-session"
3
 import BaseSession from "./base-session"
4
 import commands from "state/commands"
4
 import commands from "state/commands"
5
 import { current } from "immer"
5
 import { current } from "immer"
6
-import { getCommonBounds } from "utils/utils"
7
-import { getShapeUtils } from "lib/shape-utils"
6
+import {
7
+  getBoundsCenter,
8
+  getCommonBounds,
9
+  getPage,
10
+  getSelectedShapes,
11
+  getShapeBounds,
12
+} from "utils/utils"
13
+
14
+const PI2 = Math.PI * 2
8
 
15
 
9
 export default class RotateSession extends BaseSession {
16
 export default class RotateSession extends BaseSession {
10
   delta = [0, 0]
17
   delta = [0, 0]
17
     this.snapshot = getRotateSnapshot(data)
24
     this.snapshot = getRotateSnapshot(data)
18
   }
25
   }
19
 
26
 
20
-  update(data: Data, point: number[]) {
21
-    const { currentPageId, boundsCenter, shapes } = this.snapshot
22
-    const { document } = data
27
+  update(data: Data, point: number[], isLocked: boolean) {
28
+    const { boundsCenter, shapes } = this.snapshot
23
 
29
 
30
+    const page = getPage(data)
24
     const a1 = vec.angle(boundsCenter, this.origin)
31
     const a1 = vec.angle(boundsCenter, this.origin)
25
     const a2 = vec.angle(boundsCenter, point)
32
     const a2 = vec.angle(boundsCenter, point)
26
 
33
 
27
-    data.boundsRotation =
28
-      (this.snapshot.boundsRotation + (a2 - a1)) % (Math.PI * 2)
34
+    let rot = (PI2 + (a2 - a1)) % PI2
35
+
36
+    if (isLocked) {
37
+      rot = Math.floor((rot + Math.PI / 8) / (Math.PI / 4)) * (Math.PI / 4)
38
+    }
39
+
40
+    data.boundsRotation = (PI2 + (this.snapshot.boundsRotation + rot)) % PI2
29
 
41
 
30
     for (let { id, center, offset, rotation } of shapes) {
42
     for (let { id, center, offset, rotation } of shapes) {
31
-      const shape = document.pages[currentPageId].shapes[id]
32
-      shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2))
33
-      const newCenter = vec.rotWith(
34
-        center,
35
-        boundsCenter,
36
-        (a2 - a1) % (Math.PI * 2)
37
-      )
43
+      const shape = page.shapes[id]
44
+      shape.rotation = (PI2 + (rotation + rot)) % PI2
45
+      const newCenter = vec.rotWith(center, boundsCenter, rot % PI2)
38
       shape.point = vec.sub(newCenter, offset)
46
       shape.point = vec.sub(newCenter, offset)
39
     }
47
     }
40
   }
48
   }
41
 
49
 
42
   cancel(data: Data) {
50
   cancel(data: Data) {
43
-    const { document } = data
51
+    const page = getPage(data, this.snapshot.currentPageId)
44
 
52
 
45
     for (let { id, point, rotation } of this.snapshot.shapes) {
53
     for (let { id, point, rotation } of this.snapshot.shapes) {
46
-      const shape = document.pages[this.snapshot.currentPageId].shapes[id]
54
+      const shape = page.shapes[id]
47
       shape.rotation = rotation
55
       shape.rotation = rotation
48
       shape.point = point
56
       shape.point = point
49
     }
57
     }
55
 }
63
 }
56
 
64
 
57
 export function getRotateSnapshot(data: Data) {
65
 export function getRotateSnapshot(data: Data) {
58
-  const {
59
-    boundsRotation,
60
-    selectedIds,
61
-    currentPageId,
62
-    document: { pages },
63
-  } = current(data)
64
-
65
-  const shapes = Array.from(selectedIds.values()).map(
66
-    (id) => pages[currentPageId].shapes[id]
67
-  )
66
+  const shapes = getSelectedShapes(current(data))
68
 
67
 
69
   // A mapping of selected shapes and their bounds
68
   // A mapping of selected shapes and their bounds
70
   const shapesBounds = Object.fromEntries(
69
   const shapesBounds = Object.fromEntries(
71
-    shapes.map((shape) => [shape.id, getShapeUtils(shape).getBounds(shape)])
70
+    shapes.map((shape) => [shape.id, getShapeBounds(shape)])
72
   )
71
   )
73
 
72
 
74
   // The common (exterior) bounds of the selected shapes
73
   // The common (exterior) bounds of the selected shapes
75
   const bounds = getCommonBounds(...Object.values(shapesBounds))
74
   const bounds = getCommonBounds(...Object.values(shapesBounds))
76
 
75
 
77
-  const boundsCenter = [
78
-    bounds.minX + bounds.width / 2,
79
-    bounds.minY + bounds.height / 2,
80
-  ]
76
+  const boundsCenter = getBoundsCenter(bounds)
81
 
77
 
82
   return {
78
   return {
83
-    currentPageId,
84
     boundsCenter,
79
     boundsCenter,
85
-    boundsRotation,
80
+    currentPageId: data.currentPageId,
81
+    boundsRotation: data.boundsRotation,
86
     shapes: shapes.map(({ id, point, rotation }) => {
82
     shapes: shapes.map(({ id, point, rotation }) => {
87
       const bounds = shapesBounds[id]
83
       const bounds = shapesBounds[id]
88
       const offset = [bounds.width / 2, bounds.height / 2]
84
       const offset = [bounds.width / 2, bounds.height / 2]
89
-      const center = vec.add(offset, [bounds.minX, bounds.minY])
85
+      const center = getBoundsCenter(bounds)
90
 
86
 
91
       return {
87
       return {
92
         id,
88
         id,

+ 47
- 12
state/sessions/transform-session.ts Просмотреть файл

1
-import { Data, TransformEdge, TransformCorner } from "types"
1
+import { Data, Edge, Corner } from "types"
2
 import * as vec from "utils/vec"
2
 import * as vec from "utils/vec"
3
 import BaseSession from "./base-session"
3
 import BaseSession from "./base-session"
4
 import commands from "state/commands"
4
 import commands from "state/commands"
5
 import { current } from "immer"
5
 import { current } from "immer"
6
 import { getShapeUtils } from "lib/shape-utils"
6
 import { getShapeUtils } from "lib/shape-utils"
7
 import {
7
 import {
8
+  getBoundsCenter,
9
+  getBoundsFromPoints,
8
   getCommonBounds,
10
   getCommonBounds,
11
+  getPage,
9
   getRelativeTransformedBoundingBox,
12
   getRelativeTransformedBoundingBox,
13
+  getShapes,
10
   getTransformedBoundingBox,
14
   getTransformedBoundingBox,
11
 } from "utils/utils"
15
 } from "utils/utils"
12
 
16
 
13
 export default class TransformSession extends BaseSession {
17
 export default class TransformSession extends BaseSession {
14
   scaleX = 1
18
   scaleX = 1
15
   scaleY = 1
19
   scaleY = 1
16
-  transformType: TransformEdge | TransformCorner
20
+  transformType: Edge | Corner | "center"
17
   origin: number[]
21
   origin: number[]
18
   snapshot: TransformSnapshot
22
   snapshot: TransformSnapshot
19
 
23
 
20
   constructor(
24
   constructor(
21
     data: Data,
25
     data: Data,
22
-    transformType: TransformCorner | TransformEdge,
26
+    transformType: Corner | Edge | "center",
23
     point: number[]
27
     point: number[]
24
   ) {
28
   ) {
25
     super(data)
29
     super(data)
31
   update(data: Data, point: number[], isAspectRatioLocked = false) {
35
   update(data: Data, point: number[], isAspectRatioLocked = false) {
32
     const { transformType } = this
36
     const { transformType } = this
33
 
37
 
34
-    const { currentPageId, selectedIds, shapeBounds, initialBounds } =
35
-      this.snapshot
38
+    const { selectedIds, shapeBounds, initialBounds } = this.snapshot
39
+
40
+    const { shapes } = getPage(data)
36
 
41
 
37
     const newBoundingBox = getTransformedBoundingBox(
42
     const newBoundingBox = getTransformedBoundingBox(
38
       initialBounds,
43
       initialBounds,
48
     // Now work backward to calculate a new bounding box for each of the shapes.
53
     // Now work backward to calculate a new bounding box for each of the shapes.
49
 
54
 
50
     selectedIds.forEach((id) => {
55
     selectedIds.forEach((id) => {
51
-      const { initialShape, initialShapeBounds } = shapeBounds[id]
56
+      const { initialShape, initialShapeBounds, transformOrigin } =
57
+        shapeBounds[id]
52
 
58
 
53
       const newShapeBounds = getRelativeTransformedBoundingBox(
59
       const newShapeBounds = getRelativeTransformedBoundingBox(
54
         newBoundingBox,
60
         newBoundingBox,
58
         this.scaleY < 0
64
         this.scaleY < 0
59
       )
65
       )
60
 
66
 
61
-      const shape = data.document.pages[currentPageId].shapes[id]
67
+      const shape = shapes[id]
68
+
69
+      // const transformOrigins = {
70
+      //   [Edge.Top]: [0.5, 1],
71
+      //   [Edge.Right]: [0, 0.5],
72
+      //   [Edge.Bottom]: [0.5, 0],
73
+      //   [Edge.Left]: [1, 0.5],
74
+      //   [Corner.TopLeft]: [1, 1],
75
+      //   [Corner.TopRight]: [0, 1],
76
+      //   [Corner.BottomLeft]: [1, 0],
77
+      //   [Corner.BottomRight]: [0, 0],
78
+      // }
79
+
80
+      // const origin = transformOrigins[this.transformType]
62
 
81
 
63
       getShapeUtils(shape).transform(shape, newShapeBounds, {
82
       getShapeUtils(shape).transform(shape, newShapeBounds, {
64
         type: this.transformType,
83
         type: this.transformType,
65
         initialShape,
84
         initialShape,
66
         scaleX: this.scaleX,
85
         scaleX: this.scaleX,
67
         scaleY: this.scaleY,
86
         scaleY: this.scaleY,
87
+        transformOrigin,
68
       })
88
       })
69
     })
89
     })
70
   }
90
   }
72
   cancel(data: Data) {
92
   cancel(data: Data) {
73
     const { currentPageId, selectedIds, shapeBounds } = this.snapshot
93
     const { currentPageId, selectedIds, shapeBounds } = this.snapshot
74
 
94
 
95
+    const page = getPage(data, currentPageId)
96
+
75
     selectedIds.forEach((id) => {
97
     selectedIds.forEach((id) => {
76
-      const shape = data.document.pages[currentPageId].shapes[id]
98
+      const shape = page.shapes[id]
77
 
99
 
78
-      const { initialShape, initialShapeBounds } = shapeBounds[id]
100
+      const { initialShape, initialShapeBounds, transformOrigin } =
101
+        shapeBounds[id]
79
 
102
 
80
       getShapeUtils(shape).transform(shape, initialShapeBounds, {
103
       getShapeUtils(shape).transform(shape, initialShapeBounds, {
81
         type: this.transformType,
104
         type: this.transformType,
82
         initialShape,
105
         initialShape,
83
         scaleX: 1,
106
         scaleX: 1,
84
         scaleY: 1,
107
         scaleY: 1,
108
+        transformOrigin,
85
       })
109
       })
86
     })
110
     })
87
   }
111
   }
99
 
123
 
100
 export function getTransformSnapshot(
124
 export function getTransformSnapshot(
101
   data: Data,
125
   data: Data,
102
-  transformType: TransformEdge | TransformCorner
126
+  transformType: Edge | Corner | "center"
103
 ) {
127
 ) {
104
   const {
128
   const {
105
     document: { pages },
129
     document: { pages },
117
     })
141
     })
118
   )
142
   )
119
 
143
 
144
+  const boundsArr = Object.values(shapesBounds)
145
+
120
   // The common (exterior) bounds of the selected shapes
146
   // The common (exterior) bounds of the selected shapes
121
-  const bounds = getCommonBounds(...Object.values(shapesBounds))
147
+  const bounds = getCommonBounds(...boundsArr)
148
+
149
+  const initialInnerBounds = getBoundsFromPoints(boundsArr.map(getBoundsCenter))
122
 
150
 
123
   // Return a mapping of shapes to bounds together with the relative
151
   // Return a mapping of shapes to bounds together with the relative
124
   // positions of the shape's bounds within the common bounds shape.
152
   // positions of the shape's bounds within the common bounds shape.
129
     initialBounds: bounds,
157
     initialBounds: bounds,
130
     shapeBounds: Object.fromEntries(
158
     shapeBounds: Object.fromEntries(
131
       Array.from(selectedIds.values()).map((id) => {
159
       Array.from(selectedIds.values()).map((id) => {
160
+        const initialShapeBounds = shapesBounds[id]
161
+        const ic = getBoundsCenter(initialShapeBounds)
162
+
163
+        let ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width
164
+        let iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height
165
+
132
         return [
166
         return [
133
           id,
167
           id,
134
           {
168
           {
135
             initialShape: pageShapes[id],
169
             initialShape: pageShapes[id],
136
-            initialShapeBounds: shapesBounds[id],
170
+            initialShapeBounds,
171
+            transformOrigin: [ix, iy],
137
           },
172
           },
138
         ]
173
         ]
139
       })
174
       })

+ 14
- 18
state/sessions/transform-single-session.ts Просмотреть файл

1
-import { Data, TransformEdge, TransformCorner } from "types"
1
+import { Data, Edge, Corner } from "types"
2
 import * as vec from "utils/vec"
2
 import * as vec from "utils/vec"
3
 import BaseSession from "./base-session"
3
 import BaseSession from "./base-session"
4
 import commands from "state/commands"
4
 import commands from "state/commands"
9
   getCommonBounds,
9
   getCommonBounds,
10
   getRotatedCorners,
10
   getRotatedCorners,
11
   getTransformAnchor,
11
   getTransformAnchor,
12
+  getPage,
13
+  getShape,
14
+  getSelectedShapes,
12
 } from "utils/utils"
15
 } from "utils/utils"
13
 
16
 
14
 export default class TransformSingleSession extends BaseSession {
17
 export default class TransformSingleSession extends BaseSession {
15
-  transformType: TransformEdge | TransformCorner
18
+  transformType: Edge | Corner
16
   origin: number[]
19
   origin: number[]
17
   scaleX = 1
20
   scaleX = 1
18
   scaleY = 1
21
   scaleY = 1
21
 
24
 
22
   constructor(
25
   constructor(
23
     data: Data,
26
     data: Data,
24
-    transformType: TransformCorner | TransformEdge,
27
+    transformType: Corner | Edge,
25
     point: number[],
28
     point: number[],
26
     isCreating = false
29
     isCreating = false
27
   ) {
30
   ) {
38
     const { initialShapeBounds, currentPageId, initialShape, id } =
41
     const { initialShapeBounds, currentPageId, initialShape, id } =
39
       this.snapshot
42
       this.snapshot
40
 
43
 
41
-    const shape = data.document.pages[currentPageId].shapes[id]
44
+    const shape = getShape(data, id, currentPageId)
42
 
45
 
43
     const newBoundingBox = getTransformedBoundingBox(
46
     const newBoundingBox = getTransformedBoundingBox(
44
       initialShapeBounds,
47
       initialShapeBounds,
56
       type: this.transformType,
59
       type: this.transformType,
57
       scaleX: this.scaleX,
60
       scaleX: this.scaleX,
58
       scaleY: this.scaleY,
61
       scaleY: this.scaleY,
62
+      transformOrigin: [0.5, 0.5],
59
     })
63
     })
60
   }
64
   }
61
 
65
 
63
     const { id, initialShape, initialShapeBounds, currentPageId } =
67
     const { id, initialShape, initialShapeBounds, currentPageId } =
64
       this.snapshot
68
       this.snapshot
65
 
69
 
66
-    const { shapes } = data.document.pages[currentPageId]
67
-
68
-    const shape = shapes[id]
70
+    const shape = getShape(data, id, currentPageId)
69
 
71
 
70
     getShapeUtils(shape).transform(shape, initialShapeBounds, {
72
     getShapeUtils(shape).transform(shape, initialShapeBounds, {
71
       initialShape,
73
       initialShape,
72
       type: this.transformType,
74
       type: this.transformType,
73
       scaleX: this.scaleX,
75
       scaleX: this.scaleX,
74
       scaleY: this.scaleY,
76
       scaleY: this.scaleY,
77
+      transformOrigin: [0.5, 0.5],
75
     })
78
     })
76
   }
79
   }
77
 
80
 
89
 
92
 
90
 export function getTransformSingleSnapshot(
93
 export function getTransformSingleSnapshot(
91
   data: Data,
94
   data: Data,
92
-  transformType: TransformEdge | TransformCorner
95
+  transformType: Edge | Corner
93
 ) {
96
 ) {
94
-  const {
95
-    document: { pages },
96
-    selectedIds,
97
-    currentPageId,
98
-  } = current(data)
99
-
100
-  const id = Array.from(selectedIds)[0]
101
-  const shape = pages[currentPageId].shapes[id]
97
+  const shape = getSelectedShapes(current(data))[0]
102
   const bounds = getShapeUtils(shape).getBounds(shape)
98
   const bounds = getShapeUtils(shape).getBounds(shape)
103
 
99
 
104
   return {
100
   return {
105
-    id,
106
-    currentPageId,
101
+    id: shape.id,
102
+    currentPageId: data.currentPageId,
107
     type: transformType,
103
     type: transformType,
108
     initialShape: shape,
104
     initialShape: shape,
109
     initialShapeBounds: bounds,
105
     initialShapeBounds: bounds,

+ 5
- 8
state/sessions/translate-session.ts Просмотреть файл

4
 import commands from "state/commands"
4
 import commands from "state/commands"
5
 import { current } from "immer"
5
 import { current } from "immer"
6
 import { v4 as uuid } from "uuid"
6
 import { v4 as uuid } from "uuid"
7
+import { getPage, getSelectedShapes } from "utils/utils"
7
 
8
 
8
 export default class TranslateSession extends BaseSession {
9
 export default class TranslateSession extends BaseSession {
9
   delta = [0, 0]
10
   delta = [0, 0]
19
 
20
 
20
   update(data: Data, point: number[], isAligned: boolean, isCloning: boolean) {
21
   update(data: Data, point: number[], isAligned: boolean, isCloning: boolean) {
21
     const { currentPageId, clones, initialShapes } = this.snapshot
22
     const { currentPageId, clones, initialShapes } = this.snapshot
22
-    const { shapes } = data.document.pages[currentPageId]
23
+    const { shapes } = getPage(data, currentPageId)
23
 
24
 
24
     const delta = vec.vec(this.origin, point)
25
     const delta = vec.vec(this.origin, point)
25
 
26
 
71
 
72
 
72
   cancel(data: Data) {
73
   cancel(data: Data) {
73
     const { initialShapes, clones, currentPageId } = this.snapshot
74
     const { initialShapes, clones, currentPageId } = this.snapshot
74
-    const { shapes } = data.document.pages[currentPageId]
75
+    const { shapes } = getPage(data, currentPageId)
75
 
76
 
76
     for (const { id, point } of initialShapes) {
77
     for (const { id, point } of initialShapes) {
77
       shapes[id].point = point
78
       shapes[id].point = point
93
 }
94
 }
94
 
95
 
95
 export function getTranslateSnapshot(data: Data) {
96
 export function getTranslateSnapshot(data: Data) {
96
-  const { document, selectedIds, currentPageId } = current(data)
97
-
98
-  const shapes = Array.from(selectedIds.values()).map(
99
-    (id) => document.pages[currentPageId].shapes[id]
100
-  )
97
+  const shapes = getSelectedShapes(current(data))
101
 
98
 
102
   return {
99
   return {
103
-    currentPageId,
100
+    currentPageId: data.currentPageId,
104
     initialShapes: shapes.map(({ id, point }) => ({ id, point })),
101
     initialShapes: shapes.map(({ id, point }) => ({ id, point })),
105
     clones: shapes.map((shape) => ({ ...shape, id: uuid() })),
102
     clones: shapes.map((shape) => ({ ...shape, id: uuid() })),
106
   }
103
   }

+ 57
- 29
state/state.ts Просмотреть файл

1
 import { createSelectorHook, createState } from "@state-designer/react"
1
 import { createSelectorHook, createState } from "@state-designer/react"
2
-import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
2
+import {
3
+  clamp,
4
+  getCommonBounds,
5
+  getPage,
6
+  getShape,
7
+  screenToWorld,
8
+} from "utils/utils"
3
 import * as vec from "utils/vec"
9
 import * as vec from "utils/vec"
4
 import {
10
 import {
5
   Data,
11
   Data,
6
   PointerInfo,
12
   PointerInfo,
7
   Shape,
13
   Shape,
8
   ShapeType,
14
   ShapeType,
9
-  TransformCorner,
10
-  TransformEdge,
15
+  Corner,
16
+  Edge,
11
   CodeControl,
17
   CodeControl,
12
 } from "types"
18
 } from "types"
13
 import inputs from "./inputs"
19
 import inputs from "./inputs"
99
                 SELECTED_ALL: "selectAll",
105
                 SELECTED_ALL: "selectAll",
100
                 POINTED_CANVAS: { to: "brushSelecting" },
106
                 POINTED_CANVAS: { to: "brushSelecting" },
101
                 POINTED_BOUNDS: { to: "pointingBounds" },
107
                 POINTED_BOUNDS: { to: "pointingBounds" },
102
-                POINTED_BOUNDS_EDGE: { to: "transformingSelection" },
103
-                POINTED_BOUNDS_CORNER: { to: "transformingSelection" },
104
-                POINTED_ROTATE_HANDLE: { to: "rotatingSelection" },
108
+                POINTED_BOUNDS_HANDLE: {
109
+                  if: "isPointingRotationHandle",
110
+                  to: "rotatingSelection",
111
+                  else: { to: "transformingSelection" },
112
+                },
105
                 MOVED_OVER_SHAPE: {
113
                 MOVED_OVER_SHAPE: {
106
                   if: "pointHitsShape",
114
                   if: "pointHitsShape",
107
                   then: {
115
                   then: {
156
             },
164
             },
157
             rotatingSelection: {
165
             rotatingSelection: {
158
               onEnter: "startRotateSession",
166
               onEnter: "startRotateSession",
167
+              onExit: "clearBoundsRotation",
159
               on: {
168
               on: {
160
                 MOVED_POINTER: "updateRotateSession",
169
                 MOVED_POINTER: "updateRotateSession",
161
                 PANNED_CAMERA: "updateRotateSession",
170
                 PANNED_CAMERA: "updateRotateSession",
171
+                PRESSED_SHIFT_KEY: "keyUpdateRotateSession",
172
+                RELEASED_SHIFT_KEY: "keyUpdateRotateSession",
162
                 STOPPED_POINTING: { do: "completeSession", to: "selecting" },
173
                 STOPPED_POINTING: { do: "completeSession", to: "selecting" },
163
                 CANCELLED: { do: "cancelSession", to: "selecting" },
174
                 CANCELLED: { do: "cancelSession", to: "selecting" },
164
               },
175
               },
420
       return data.hoveredId === payload.target
431
       return data.hoveredId === payload.target
421
     },
432
     },
422
     pointHitsShape(data, payload: { target: string; point: number[] }) {
433
     pointHitsShape(data, payload: { target: string; point: number[] }) {
423
-      const shape =
424
-        data.document.pages[data.currentPageId].shapes[payload.target]
434
+      const shape = getShape(data, payload.target)
425
 
435
 
426
       return getShapeUtils(shape).hitTest(
436
       return getShapeUtils(shape).hitTest(
427
         shape,
437
         shape,
428
         screenToWorld(payload.point, data)
438
         screenToWorld(payload.point, data)
429
       )
439
       )
430
     },
440
     },
441
+    isPointingRotationHandle(
442
+      data,
443
+      payload: { target: Edge | Corner | "rotate" }
444
+    ) {
445
+      return payload.target === "rotate"
446
+    },
431
   },
447
   },
432
   actions: {
448
   actions: {
433
     /* --------------------- Shapes --------------------- */
449
     /* --------------------- Shapes --------------------- */
438
         point: screenToWorld(payload.point, data),
454
         point: screenToWorld(payload.point, data),
439
       })
455
       })
440
 
456
 
441
-      data.document.pages[data.currentPageId].shapes[shape.id] = shape
457
+      getPage(data).shapes[shape.id] = shape
442
       data.selectedIds.clear()
458
       data.selectedIds.clear()
443
       data.selectedIds.add(shape.id)
459
       data.selectedIds.add(shape.id)
444
     },
460
     },
449
         point: screenToWorld(payload.point, data),
465
         point: screenToWorld(payload.point, data),
450
       })
466
       })
451
 
467
 
452
-      data.document.pages[data.currentPageId].shapes[shape.id] = shape
468
+      getPage(data).shapes[shape.id] = shape
453
       data.selectedIds.clear()
469
       data.selectedIds.clear()
454
       data.selectedIds.add(shape.id)
470
       data.selectedIds.add(shape.id)
455
     },
471
     },
461
         direction: [0, 1],
477
         direction: [0, 1],
462
       })
478
       })
463
 
479
 
464
-      data.document.pages[data.currentPageId].shapes[shape.id] = shape
480
+      getPage(data).shapes[shape.id] = shape
465
       data.selectedIds.clear()
481
       data.selectedIds.clear()
466
       data.selectedIds.add(shape.id)
482
       data.selectedIds.add(shape.id)
467
     },
483
     },
472
         radius: 1,
488
         radius: 1,
473
       })
489
       })
474
 
490
 
475
-      data.document.pages[data.currentPageId].shapes[shape.id] = shape
491
+      getPage(data).shapes[shape.id] = shape
476
       data.selectedIds.clear()
492
       data.selectedIds.clear()
477
       data.selectedIds.add(shape.id)
493
       data.selectedIds.add(shape.id)
478
     },
494
     },
484
         radiusY: 1,
500
         radiusY: 1,
485
       })
501
       })
486
 
502
 
487
-      data.document.pages[data.currentPageId].shapes[shape.id] = shape
503
+      getPage(data).shapes[shape.id] = shape
488
       data.selectedIds.clear()
504
       data.selectedIds.clear()
489
       data.selectedIds.add(shape.id)
505
       data.selectedIds.add(shape.id)
490
     },
506
     },
495
         size: [1, 1],
511
         size: [1, 1],
496
       })
512
       })
497
 
513
 
498
-      data.document.pages[data.currentPageId].shapes[shape.id] = shape
514
+      getPage(data).shapes[shape.id] = shape
499
       data.selectedIds.clear()
515
       data.selectedIds.clear()
500
       data.selectedIds.add(shape.id)
516
       data.selectedIds.add(shape.id)
501
     },
517
     },
529
         screenToWorld(payload.point, data)
545
         screenToWorld(payload.point, data)
530
       )
546
       )
531
     },
547
     },
548
+    keyUpdateRotateSession(data, payload: PointerInfo) {
549
+      session.update(
550
+        data,
551
+        screenToWorld(inputs.pointer.point, data),
552
+        payload.shiftKey
553
+      )
554
+    },
532
     updateRotateSession(data, payload: PointerInfo) {
555
     updateRotateSession(data, payload: PointerInfo) {
533
-      session.update(data, screenToWorld(payload.point, data))
556
+      session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
534
     },
557
     },
535
 
558
 
536
     // Dragging / Translating
559
     // Dragging / Translating
564
     // Dragging / Translating
587
     // Dragging / Translating
565
     startTransformSession(
588
     startTransformSession(
566
       data,
589
       data,
567
-      payload: PointerInfo & { target: TransformCorner | TransformEdge }
590
+      payload: PointerInfo & { target: Corner | Edge }
568
     ) {
591
     ) {
569
       session =
592
       session =
570
         data.selectedIds.size === 1
593
         data.selectedIds.size === 1
583
     startDrawTransformSession(data, payload: PointerInfo) {
606
     startDrawTransformSession(data, payload: PointerInfo) {
584
       session = new Sessions.TransformSingleSession(
607
       session = new Sessions.TransformSingleSession(
585
         data,
608
         data,
586
-        TransformCorner.BottomRight,
609
+        Corner.BottomRight,
587
         screenToWorld(payload.point, data),
610
         screenToWorld(payload.point, data),
588
         true
611
         true
589
       )
612
       )
619
     /* -------------------- Selection ------------------- */
642
     /* -------------------- Selection ------------------- */
620
 
643
 
621
     selectAll(data) {
644
     selectAll(data) {
622
-      const { selectedIds, document, currentPageId } = data
645
+      const { selectedIds } = data
646
+      const page = getPage(data)
623
       selectedIds.clear()
647
       selectedIds.clear()
624
-      for (let id in document.pages[currentPageId].shapes) {
648
+      for (let id in page.shapes) {
625
         selectedIds.add(id)
649
         selectedIds.add(id)
626
       }
650
       }
627
     },
651
     },
654
 
678
 
655
       document.documentElement.style.setProperty("--camera-zoom", "1")
679
       document.documentElement.style.setProperty("--camera-zoom", "1")
656
     },
680
     },
681
+    centerCamera(data) {
682
+      const { shapes } = getPage(data)
683
+      getCommonBounds()
684
+      data.camera.zoom = 1
685
+      data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
686
+
687
+      document.documentElement.style.setProperty("--camera-zoom", "1")
688
+    },
657
     zoomCamera(data, payload: { delta: number; point: number[] }) {
689
     zoomCamera(data, payload: { delta: number; point: number[] }) {
658
       const { camera } = data
690
       const { camera } = data
659
       const p0 = screenToWorld(payload.point, data)
691
       const p0 = screenToWorld(payload.point, data)
678
       )
710
       )
679
     },
711
     },
680
     deleteSelectedIds(data) {
712
     deleteSelectedIds(data) {
681
-      const { document, currentPageId } = data
682
-      const { shapes } = document.pages[currentPageId]
713
+      const page = getPage(data)
683
 
714
 
684
       data.hoveredId = undefined
715
       data.hoveredId = undefined
685
       data.pointedId = undefined
716
       data.pointedId = undefined
686
 
717
 
687
       data.selectedIds.forEach((id) => {
718
       data.selectedIds.forEach((id) => {
688
-        delete shapes[id]
719
+        delete page.shapes[id]
689
         // TODO: recursively delete children
720
         // TODO: recursively delete children
690
       })
721
       })
691
 
722
 
692
-      data.document.pages[currentPageId].shapes = shapes
693
       data.selectedIds.clear()
723
       data.selectedIds.clear()
694
     },
724
     },
695
 
725
 
784
       return new Set(data.selectedIds)
814
       return new Set(data.selectedIds)
785
     },
815
     },
786
     selectedBounds(data) {
816
     selectedBounds(data) {
787
-      const {
788
-        selectedIds,
789
-        currentPageId,
790
-        document: { pages },
791
-      } = data
817
+      const { selectedIds } = data
818
+
819
+      const page = getPage(data)
792
 
820
 
793
       const shapes = Array.from(selectedIds.values())
821
       const shapes = Array.from(selectedIds.values())
794
-        .map((id) => pages[currentPageId].shapes[id])
822
+        .map((id) => page.shapes[id])
795
         .filter(Boolean)
823
         .filter(Boolean)
796
 
824
 
797
       if (selectedIds.size === 0) return null
825
       if (selectedIds.size === 0) return null

+ 2
- 2
types.ts Просмотреть файл

146
   altKey: boolean
146
   altKey: boolean
147
 }
147
 }
148
 
148
 
149
-export enum TransformEdge {
149
+export enum Edge {
150
   Top = "top_edge",
150
   Top = "top_edge",
151
   Right = "right_edge",
151
   Right = "right_edge",
152
   Bottom = "bottom_edge",
152
   Bottom = "bottom_edge",
153
   Left = "left_edge",
153
   Left = "left_edge",
154
 }
154
 }
155
 
155
 
156
-export enum TransformCorner {
156
+export enum Corner {
157
   TopLeft = "top_left_corner",
157
   TopLeft = "top_left_corner",
158
   TopRight = "top_right_corner",
158
   TopRight = "top_right_corner",
159
   BottomRight = "bottom_right_corner",
159
   BottomRight = "bottom_right_corner",

+ 128
- 76
utils/utils.ts Просмотреть файл

1
 import Vector from "lib/code/vector"
1
 import Vector from "lib/code/vector"
2
-import { getShapeUtils } from "lib/shape-utils"
3
 import React from "react"
2
 import React from "react"
4
-import { Data, Bounds, TransformEdge, TransformCorner, Shape } from "types"
5
-import * as svg from "./svg"
3
+import { Data, Bounds, Edge, Corner, Shape } from "types"
6
 import * as vec from "./vec"
4
 import * as vec from "./vec"
5
+import _isMobile from "ismobilejs"
6
+import { getShapeUtils } from "lib/shape-utils"
7
 
7
 
8
 export function screenToWorld(point: number[], data: Data) {
8
 export function screenToWorld(point: number[], data: Data) {
9
   return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
9
   return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
42
   return bounds
42
   return bounds
43
 }
43
 }
44
 
44
 
45
-// export function getBoundsFromPoints(a: number[], b: number[]) {
45
+// export function getBoundsFromTwoPoints(a: number[], b: number[]) {
46
 //   const minX = Math.min(a[0], b[0])
46
 //   const minX = Math.min(a[0], b[0])
47
 //   const maxX = Math.max(a[0], b[0])
47
 //   const maxX = Math.max(a[0], b[0])
48
 //   const minY = Math.min(a[1], b[1])
48
 //   const minY = Math.min(a[1], b[1])
900
 }
900
 }
901
 
901
 
902
 export function getTransformAnchor(
902
 export function getTransformAnchor(
903
-  type: TransformEdge | TransformCorner,
903
+  type: Edge | Corner,
904
   isFlippedX: boolean,
904
   isFlippedX: boolean,
905
   isFlippedY: boolean
905
   isFlippedY: boolean
906
 ) {
906
 ) {
907
-  let anchor: TransformCorner | TransformEdge = type
907
+  let anchor: Corner | Edge = type
908
 
908
 
909
   // Change corner anchors if flipped
909
   // Change corner anchors if flipped
910
   switch (type) {
910
   switch (type) {
911
-    case TransformCorner.TopLeft: {
911
+    case Corner.TopLeft: {
912
       if (isFlippedX && isFlippedY) {
912
       if (isFlippedX && isFlippedY) {
913
-        anchor = TransformCorner.BottomRight
913
+        anchor = Corner.BottomRight
914
       } else if (isFlippedX) {
914
       } else if (isFlippedX) {
915
-        anchor = TransformCorner.TopRight
915
+        anchor = Corner.TopRight
916
       } else if (isFlippedY) {
916
       } else if (isFlippedY) {
917
-        anchor = TransformCorner.BottomLeft
917
+        anchor = Corner.BottomLeft
918
       } else {
918
       } else {
919
-        anchor = TransformCorner.BottomRight
919
+        anchor = Corner.BottomRight
920
       }
920
       }
921
       break
921
       break
922
     }
922
     }
923
-    case TransformCorner.TopRight: {
923
+    case Corner.TopRight: {
924
       if (isFlippedX && isFlippedY) {
924
       if (isFlippedX && isFlippedY) {
925
-        anchor = TransformCorner.BottomLeft
925
+        anchor = Corner.BottomLeft
926
       } else if (isFlippedX) {
926
       } else if (isFlippedX) {
927
-        anchor = TransformCorner.TopLeft
927
+        anchor = Corner.TopLeft
928
       } else if (isFlippedY) {
928
       } else if (isFlippedY) {
929
-        anchor = TransformCorner.BottomRight
929
+        anchor = Corner.BottomRight
930
       } else {
930
       } else {
931
-        anchor = TransformCorner.BottomLeft
931
+        anchor = Corner.BottomLeft
932
       }
932
       }
933
       break
933
       break
934
     }
934
     }
935
-    case TransformCorner.BottomRight: {
935
+    case Corner.BottomRight: {
936
       if (isFlippedX && isFlippedY) {
936
       if (isFlippedX && isFlippedY) {
937
-        anchor = TransformCorner.TopLeft
937
+        anchor = Corner.TopLeft
938
       } else if (isFlippedX) {
938
       } else if (isFlippedX) {
939
-        anchor = TransformCorner.BottomLeft
939
+        anchor = Corner.BottomLeft
940
       } else if (isFlippedY) {
940
       } else if (isFlippedY) {
941
-        anchor = TransformCorner.TopRight
941
+        anchor = Corner.TopRight
942
       } else {
942
       } else {
943
-        anchor = TransformCorner.TopLeft
943
+        anchor = Corner.TopLeft
944
       }
944
       }
945
       break
945
       break
946
     }
946
     }
947
-    case TransformCorner.BottomLeft: {
947
+    case Corner.BottomLeft: {
948
       if (isFlippedX && isFlippedY) {
948
       if (isFlippedX && isFlippedY) {
949
-        anchor = TransformCorner.TopRight
949
+        anchor = Corner.TopRight
950
       } else if (isFlippedX) {
950
       } else if (isFlippedX) {
951
-        anchor = TransformCorner.BottomRight
951
+        anchor = Corner.BottomRight
952
       } else if (isFlippedY) {
952
       } else if (isFlippedY) {
953
-        anchor = TransformCorner.TopLeft
953
+        anchor = Corner.TopLeft
954
       } else {
954
       } else {
955
-        anchor = TransformCorner.TopRight
955
+        anchor = Corner.TopRight
956
       }
956
       }
957
       break
957
       break
958
     }
958
     }
1030
   }
1030
   }
1031
 }
1031
 }
1032
 
1032
 
1033
+export function getRotatedSize(size: number[], rotation: number) {
1034
+  const center = vec.div(size, 2)
1035
+
1036
+  const points = [[0, 0], [size[0], 0], size, [0, size[1]]].map((point) =>
1037
+    vec.rotWith(point, center, rotation)
1038
+  )
1039
+
1040
+  const bounds = getBoundsFromPoints(points)
1041
+
1042
+  return [bounds.width, bounds.height]
1043
+}
1044
+
1033
 export function getRotatedCorners(b: Bounds, rotation: number) {
1045
 export function getRotatedCorners(b: Bounds, rotation: number) {
1034
   const center = [b.minX + b.width / 2, b.minY + b.height / 2]
1046
   const center = [b.minX + b.width / 2, b.minY + b.height / 2]
1035
 
1047
 
1043
 
1055
 
1044
 export function getTransformedBoundingBox(
1056
 export function getTransformedBoundingBox(
1045
   bounds: Bounds,
1057
   bounds: Bounds,
1046
-  handle: TransformCorner | TransformEdge | "center",
1058
+  handle: Corner | Edge | "center",
1047
   delta: number[],
1059
   delta: number[],
1048
   rotation = 0,
1060
   rotation = 0,
1049
   isAspectRatioLocked = false
1061
   isAspectRatioLocked = false
1082
   corners should change.
1094
   corners should change.
1083
   */
1095
   */
1084
   switch (handle) {
1096
   switch (handle) {
1085
-    case TransformEdge.Top:
1086
-    case TransformCorner.TopLeft:
1087
-    case TransformCorner.TopRight: {
1097
+    case Edge.Top:
1098
+    case Corner.TopLeft:
1099
+    case Corner.TopRight: {
1088
       by0 += dy
1100
       by0 += dy
1089
       break
1101
       break
1090
     }
1102
     }
1091
-    case TransformEdge.Bottom:
1092
-    case TransformCorner.BottomLeft:
1093
-    case TransformCorner.BottomRight: {
1103
+    case Edge.Bottom:
1104
+    case Corner.BottomLeft:
1105
+    case Corner.BottomRight: {
1094
       by1 += dy
1106
       by1 += dy
1095
       break
1107
       break
1096
     }
1108
     }
1097
   }
1109
   }
1098
 
1110
 
1099
   switch (handle) {
1111
   switch (handle) {
1100
-    case TransformEdge.Left:
1101
-    case TransformCorner.TopLeft:
1102
-    case TransformCorner.BottomLeft: {
1112
+    case Edge.Left:
1113
+    case Corner.TopLeft:
1114
+    case Corner.BottomLeft: {
1103
       bx0 += dx
1115
       bx0 += dx
1104
       break
1116
       break
1105
     }
1117
     }
1106
-    case TransformEdge.Right:
1107
-    case TransformCorner.TopRight:
1108
-    case TransformCorner.BottomRight: {
1118
+    case Edge.Right:
1119
+    case Corner.TopRight:
1120
+    case Corner.BottomRight: {
1109
       bx1 += dx
1121
       bx1 += dx
1110
       break
1122
       break
1111
     }
1123
     }
1117
   const scaleX = (bx1 - bx0) / aw
1129
   const scaleX = (bx1 - bx0) / aw
1118
   const scaleY = (by1 - by0) / ah
1130
   const scaleY = (by1 - by0) / ah
1119
 
1131
 
1132
+  const flipX = scaleX < 0
1133
+  const flipY = scaleY < 0
1134
+
1120
   const bw = Math.abs(bx1 - bx0)
1135
   const bw = Math.abs(bx1 - bx0)
1121
   const bh = Math.abs(by1 - by0)
1136
   const bh = Math.abs(by1 - by0)
1122
 
1137
 
1134
     const th = bh * (scaleX < 0 ? 1 : -1) * ar
1149
     const th = bh * (scaleX < 0 ? 1 : -1) * ar
1135
 
1150
 
1136
     switch (handle) {
1151
     switch (handle) {
1137
-      case TransformCorner.TopLeft: {
1152
+      case Corner.TopLeft: {
1138
         if (isTall) by0 = by1 + tw
1153
         if (isTall) by0 = by1 + tw
1139
         else bx0 = bx1 + th
1154
         else bx0 = bx1 + th
1140
         break
1155
         break
1141
       }
1156
       }
1142
-      case TransformCorner.TopRight: {
1157
+      case Corner.TopRight: {
1143
         if (isTall) by0 = by1 + tw
1158
         if (isTall) by0 = by1 + tw
1144
         else bx1 = bx0 - th
1159
         else bx1 = bx0 - th
1145
         break
1160
         break
1146
       }
1161
       }
1147
-      case TransformCorner.BottomRight: {
1162
+      case Corner.BottomRight: {
1148
         if (isTall) by1 = by0 - tw
1163
         if (isTall) by1 = by0 - tw
1149
         else bx1 = bx0 - th
1164
         else bx1 = bx0 - th
1150
         break
1165
         break
1151
       }
1166
       }
1152
-      case TransformCorner.BottomLeft: {
1167
+      case Corner.BottomLeft: {
1153
         if (isTall) by1 = by0 - tw
1168
         if (isTall) by1 = by0 - tw
1154
         else bx0 = bx1 + th
1169
         else bx0 = bx1 + th
1155
         break
1170
         break
1156
       }
1171
       }
1157
-      case TransformEdge.Bottom:
1158
-      case TransformEdge.Top: {
1172
+      case Edge.Bottom:
1173
+      case Edge.Top: {
1159
         const m = (bx0 + bx1) / 2
1174
         const m = (bx0 + bx1) / 2
1160
         const w = bh * ar
1175
         const w = bh * ar
1161
         bx0 = m - w / 2
1176
         bx0 = m - w / 2
1162
         bx1 = m + w / 2
1177
         bx1 = m + w / 2
1163
         break
1178
         break
1164
       }
1179
       }
1165
-      case TransformEdge.Left:
1166
-      case TransformEdge.Right: {
1180
+      case Edge.Left:
1181
+      case Edge.Right: {
1167
         const m = (by0 + by1) / 2
1182
         const m = (by0 + by1) / 2
1168
         const h = bw / ar
1183
         const h = bw / ar
1169
         by0 = m - h / 2
1184
         by0 = m - h / 2
1189
     const c1 = vec.med([bx0, by0], [bx1, by1])
1204
     const c1 = vec.med([bx0, by0], [bx1, by1])
1190
 
1205
 
1191
     switch (handle) {
1206
     switch (handle) {
1192
-      case TransformCorner.TopLeft: {
1207
+      case Corner.TopLeft: {
1193
         cv = vec.sub(
1208
         cv = vec.sub(
1194
           vec.rotWith([bx1, by1], c1, rotation),
1209
           vec.rotWith([bx1, by1], c1, rotation),
1195
           vec.rotWith([ax1, ay1], c0, rotation)
1210
           vec.rotWith([ax1, ay1], c0, rotation)
1196
         )
1211
         )
1197
         break
1212
         break
1198
       }
1213
       }
1199
-      case TransformCorner.TopRight: {
1214
+      case Corner.TopRight: {
1200
         cv = vec.sub(
1215
         cv = vec.sub(
1201
           vec.rotWith([bx0, by1], c1, rotation),
1216
           vec.rotWith([bx0, by1], c1, rotation),
1202
           vec.rotWith([ax0, ay1], c0, rotation)
1217
           vec.rotWith([ax0, ay1], c0, rotation)
1203
         )
1218
         )
1204
         break
1219
         break
1205
       }
1220
       }
1206
-      case TransformCorner.BottomRight: {
1221
+      case Corner.BottomRight: {
1207
         cv = vec.sub(
1222
         cv = vec.sub(
1208
           vec.rotWith([bx0, by0], c1, rotation),
1223
           vec.rotWith([bx0, by0], c1, rotation),
1209
           vec.rotWith([ax0, ay0], c0, rotation)
1224
           vec.rotWith([ax0, ay0], c0, rotation)
1210
         )
1225
         )
1211
         break
1226
         break
1212
       }
1227
       }
1213
-      case TransformCorner.BottomLeft: {
1228
+      case Corner.BottomLeft: {
1214
         cv = vec.sub(
1229
         cv = vec.sub(
1215
           vec.rotWith([bx1, by0], c1, rotation),
1230
           vec.rotWith([bx1, by0], c1, rotation),
1216
           vec.rotWith([ax1, ay0], c0, rotation)
1231
           vec.rotWith([ax1, ay0], c0, rotation)
1217
         )
1232
         )
1218
         break
1233
         break
1219
       }
1234
       }
1220
-      case TransformEdge.Top: {
1235
+      case Edge.Top: {
1221
         cv = vec.sub(
1236
         cv = vec.sub(
1222
           vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation),
1237
           vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation),
1223
           vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation)
1238
           vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation)
1224
         )
1239
         )
1225
         break
1240
         break
1226
       }
1241
       }
1227
-      case TransformEdge.Left: {
1242
+      case Edge.Left: {
1228
         cv = vec.sub(
1243
         cv = vec.sub(
1229
           vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation),
1244
           vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation),
1230
           vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation)
1245
           vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation)
1231
         )
1246
         )
1232
         break
1247
         break
1233
       }
1248
       }
1234
-      case TransformEdge.Bottom: {
1249
+      case Edge.Bottom: {
1235
         cv = vec.sub(
1250
         cv = vec.sub(
1236
           vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation),
1251
           vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation),
1237
           vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation)
1252
           vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation)
1238
         )
1253
         )
1239
         break
1254
         break
1240
       }
1255
       }
1241
-      case TransformEdge.Right: {
1256
+      case Edge.Right: {
1242
         cv = vec.sub(
1257
         cv = vec.sub(
1243
           vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation),
1258
           vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation),
1244
           vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation)
1259
           vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation)
1273
     maxY: by1,
1288
     maxY: by1,
1274
     width: bx1 - bx0,
1289
     width: bx1 - bx0,
1275
     height: by1 - by0,
1290
     height: by1 - by0,
1276
-    scaleX,
1277
-    scaleY,
1291
+    scaleX: ((bx1 - bx0) / (ax1 - ax0)) * (flipX ? -1 : 1),
1292
+    scaleY: ((by1 - by0) / (ay1 - ay0)) * (flipY ? -1 : 1),
1278
   }
1293
   }
1279
 }
1294
 }
1280
 
1295
 
1285
   isFlippedX: boolean,
1300
   isFlippedX: boolean,
1286
   isFlippedY: boolean
1301
   isFlippedY: boolean
1287
 ) {
1302
 ) {
1288
-  const minX =
1289
-    bounds.minX +
1290
-    bounds.width *
1291
-      ((isFlippedX
1292
-        ? initialBounds.maxX - initialShapeBounds.maxX
1293
-        : initialShapeBounds.minX - initialBounds.minX) /
1294
-        initialBounds.width)
1295
-
1296
-  const minY =
1297
-    bounds.minY +
1298
-    bounds.height *
1299
-      ((isFlippedY
1300
-        ? initialBounds.maxY - initialShapeBounds.maxY
1301
-        : initialShapeBounds.minY - initialBounds.minY) /
1302
-        initialBounds.height)
1303
-
1304
-  const width = (initialShapeBounds.width / initialBounds.width) * bounds.width
1305
-  const height =
1306
-    (initialShapeBounds.height / initialBounds.height) * bounds.height
1303
+  const nx =
1304
+    (isFlippedX
1305
+      ? initialBounds.maxX - initialShapeBounds.maxX
1306
+      : initialShapeBounds.minX - initialBounds.minX) / initialBounds.width
1307
+
1308
+  const ny =
1309
+    (isFlippedY
1310
+      ? initialBounds.maxY - initialShapeBounds.maxY
1311
+      : initialShapeBounds.minY - initialBounds.minY) / initialBounds.height
1312
+
1313
+  const nw = initialShapeBounds.width / initialBounds.width
1314
+  const nh = initialShapeBounds.height / initialBounds.height
1315
+
1316
+  const minX = bounds.minX + bounds.width * nx
1317
+  const minY = bounds.minY + bounds.height * ny
1318
+  const width = bounds.width * nw
1319
+  const height = bounds.height * nh
1307
 
1320
 
1308
   return {
1321
   return {
1309
     minX,
1322
     minX,
1314
     height,
1327
     height,
1315
   }
1328
   }
1316
 }
1329
 }
1330
+
1331
+export function getShape(
1332
+  data: Data,
1333
+  shapeId: string,
1334
+  pageId = data.currentPageId
1335
+) {
1336
+  return data.document.pages[pageId].shapes[shapeId]
1337
+}
1338
+
1339
+export function getPage(data: Data, pageId = data.currentPageId) {
1340
+  return data.document.pages[pageId]
1341
+}
1342
+
1343
+export function getCurrentCode(data: Data, fileId = data.currentCodeFileId) {
1344
+  return data.document.code[fileId]
1345
+}
1346
+
1347
+export function getShapes(data: Data, pageId = data.currentPageId) {
1348
+  const page = getPage(data, pageId)
1349
+  return Object.values(page.shapes)
1350
+}
1351
+
1352
+export function getSelectedShapes(data: Data, pageId = data.currentPageId) {
1353
+  const page = getPage(data, pageId)
1354
+  const ids = Array.from(data.selectedIds.values())
1355
+  return ids.map((id) => page.shapes[id])
1356
+}
1357
+
1358
+export function isMobile() {
1359
+  return _isMobile()
1360
+}
1361
+
1362
+export function getShapeBounds(shape: Shape) {
1363
+  return getShapeUtils(shape).getBounds(shape)
1364
+}
1365
+
1366
+export function getBoundsCenter(bounds: Bounds) {
1367
+  return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
1368
+}

+ 5
- 0
yarn.lock Просмотреть файл

4154
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
4154
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
4155
   integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
4155
   integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
4156
 
4156
 
4157
+ismobilejs@^1.1.1:
4158
+  version "1.1.1"
4159
+  resolved "https://registry.yarnpkg.com/ismobilejs/-/ismobilejs-1.1.1.tgz#c56ca0ae8e52b24ca0f22ba5ef3215a2ddbbaa0e"
4160
+  integrity sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==
4161
+
4157
 isobject@^2.0.0:
4162
 isobject@^2.0.0:
4158
   version "2.1.0"
4163
   version "2.1.0"
4159
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
4164
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"

Загрузка…
Отмена
Сохранить