瀏覽代碼

refactors bounds, improves transforming rotating shapes

main
Steve Ruiz 4 年之前
父節點
當前提交
b752782753

+ 0
- 47
components/canvas/bounds-bg.tsx 查看文件

@@ -1,47 +0,0 @@
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,285 +0,0 @@
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 查看文件

@@ -0,0 +1,46 @@
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 查看文件

@@ -0,0 +1,58 @@
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 查看文件

@@ -0,0 +1,18 @@
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 查看文件

@@ -0,0 +1,43 @@
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 查看文件

@@ -0,0 +1,42 @@
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 查看文件

@@ -0,0 +1,30 @@
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,8 +5,8 @@ import useCamera from "hooks/useCamera"
5 5
 import Page from "./page"
6 6
 import Brush from "./brush"
7 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 10
 import inputs from "state/inputs"
11 11
 
12 12
 export default function Canvas() {

+ 2
- 2
components/canvas/page.tsx 查看文件

@@ -1,5 +1,5 @@
1 1
 import { useSelector } from "state"
2
-import { deepCompareArrays } from "utils/utils"
2
+import { deepCompareArrays, getPage } from "utils/utils"
3 3
 import Shape from "./shape"
4 4
 
5 5
 /* 
@@ -10,7 +10,7 @@ here; and still cheaper than any other pattern I've found.
10 10
 
11 11
 export default function Page() {
12 12
   const currentPageShapeIds = useSelector(
13
-    ({ data }) => Object.keys(data.document.pages[data.currentPageId].shapes),
13
+    ({ data }) => Object.keys(getPage(data).shapes),
14 14
     deepCompareArrays
15 15
   )
16 16
 

+ 2
- 3
components/canvas/shape.tsx 查看文件

@@ -3,6 +3,7 @@ import state, { useSelector } from "state"
3 3
 import inputs from "state/inputs"
4 4
 import { getShapeUtils } from "lib/shape-utils"
5 5
 import styled from "styles"
6
+import { getPage } from "utils/utils"
6 7
 
7 8
 function Shape({ id }: { id: string }) {
8 9
   const rGroup = useRef<SVGGElement>(null)
@@ -11,9 +12,7 @@ function Shape({ id }: { id: string }) {
11 12
 
12 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 17
   const handlePointerDown = useCallback(
19 18
     (e: React.PointerEvent) => {

+ 38
- 0
hooks/useBoundsHandleEvents.ts 查看文件

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

+ 6
- 4
lib/shape-utils/index.tsx 查看文件

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

+ 20
- 17
lib/shape-utils/rectangle.tsx 查看文件

@@ -1,16 +1,13 @@
1 1
 import { v4 as uuid } from "uuid"
2 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 4
 import { registerShapeUtils } from "./index"
10 5
 import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
11 6
 import {
12 7
   getBoundsFromPoints,
13 8
   getRotatedCorners,
9
+  getRotatedSize,
10
+  lerp,
14 11
   rotateBounds,
15 12
   translateBounds,
16 13
 } from "utils/utils"
@@ -99,28 +96,34 @@ const rectangle = registerShapeUtils<RectangleShape>({
99 96
     return shape
100 97
   },
101 98
 
102
-  transform(shape, bounds, { initialShape, scaleX, scaleY }) {
99
+  transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
103 100
     if (shape.rotation === 0) {
104 101
       shape.size = [bounds.width, bounds.height]
105 102
       shape.point = [bounds.minX, bounds.minY]
106 103
     } else {
107
-      // Center shape in resized bounds
104
+      // Size
108 105
       shape.size = vec.mul(
109 106
         initialShape.size,
110 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 127
     return shape
125 128
   },
126 129
 

+ 1
- 0
package.json 查看文件

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

+ 5
- 4
state/commands/create-shape.ts 查看文件

@@ -1,6 +1,7 @@
1 1
 import Command from "./command"
2 2
 import history from "../history"
3 3
 import { Data, Shape } from "types"
4
+import { getPage } from "utils/utils"
4 5
 
5 6
 export default function registerShapeUtilsCommand(data: Data, shape: Shape) {
6 7
   const { currentPageId } = data
@@ -11,17 +12,17 @@ export default function registerShapeUtilsCommand(data: Data, shape: Shape) {
11 12
       name: "translate_shapes",
12 13
       category: "canvas",
13 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 18
         data.selectedIds.clear()
18 19
         data.pointedId = undefined
19 20
         data.hoveredId = undefined
20 21
       },
21 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 27
         data.selectedIds.clear()
27 28
         data.pointedId = undefined

+ 3
- 2
state/commands/direct.ts 查看文件

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

+ 8
- 6
state/commands/generate.ts 查看文件

@@ -2,6 +2,7 @@ import Command from "./command"
2 2
 import history from "../history"
3 3
 import { CodeControl, Data, Shape } from "types"
4 4
 import { current } from "immer"
5
+import { getPage } from "utils/utils"
5 6
 
6 7
 export default function generateCommand(
7 8
   data: Data,
@@ -9,12 +10,13 @@ export default function generateCommand(
9 10
   generatedShapes: Shape[]
10 11
 ) {
11 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 21
   // Remove previous generated shapes
20 22
   for (let id in currentShapes) {
@@ -34,7 +36,7 @@ export default function generateCommand(
34 36
       name: "translate_shapes",
35 37
       category: "canvas",
36 38
       do(data) {
37
-        const { shapes } = data.document.pages[currentPageId]
39
+        const { shapes } = getPage(data)
38 40
 
39 41
         data.selectedIds.clear()
40 42
 
@@ -51,7 +53,7 @@ export default function generateCommand(
51 53
         }
52 54
       },
53 55
       undo(data) {
54
-        const { shapes } = data.document.pages[currentPageId]
56
+        const { shapes } = getPage(data)
55 57
 
56 58
         // Remove generated shapes
57 59
         for (let id in shapes) {

+ 3
- 2
state/commands/rotate.ts 查看文件

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

+ 14
- 9
state/commands/transform-single.ts 查看文件

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

+ 16
- 7
state/commands/transform.ts 查看文件

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

+ 5
- 4
state/commands/translate.ts 查看文件

@@ -2,6 +2,7 @@ import Command from "./command"
2 2
 import history from "../history"
3 3
 import { TranslateSnapshot } from "state/sessions/translate-session"
4 4
 import { Data } from "types"
5
+import { getPage } from "utils/utils"
5 6
 
6 7
 export default function translateCommand(
7 8
   data: Data,
@@ -18,8 +19,8 @@ export default function translateCommand(
18 19
       do(data, initial) {
19 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 24
         const { clones } = before // !
24 25
 
25 26
         data.selectedIds.clear()
@@ -36,8 +37,8 @@ export default function translateCommand(
36 37
         }
37 38
       },
38 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 43
         data.selectedIds.clear()
43 44
 

+ 23
- 34
state/sessions/brush-session.ts 查看文件

@@ -1,15 +1,10 @@
1 1
 import { current } from "immer"
2
-import { ShapeUtil, Bounds, Data, Shapes } from "types"
2
+import { Bounds, Data } from "types"
3 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 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 8
 export default class BrushSession extends BaseSession {
14 9
   origin: number[]
15 10
   snapshot: BrushSnapshot
@@ -19,7 +14,7 @@ export default class BrushSession extends BaseSession {
19 14
 
20 15
     this.origin = vec.round(point)
21 16
 
22
-    this.snapshot = BrushSession.getSnapshot(data)
17
+    this.snapshot = getBrushSnapshot(data)
23 18
   }
24 19
 
25 20
   update = (data: Data, point: number[]) => {
@@ -27,7 +22,8 @@ export default class BrushSession extends BaseSession {
27 22
 
28 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 27
       if (test(brushBounds)) {
32 28
         data.selectedIds.add(id)
33 29
       } else if (data.selectedIds.has(id)) {
@@ -46,30 +42,23 @@ export default class BrushSession extends BaseSession {
46 42
   complete = (data: Data) => {
47 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,6 +3,7 @@ import * as vec from "utils/vec"
3 3
 import BaseSession from "./base-session"
4 4
 import commands from "state/commands"
5 5
 import { current } from "immer"
6
+import { getPage } from "utils/utils"
6 7
 
7 8
 export default class DirectionSession extends BaseSession {
8 9
   delta = [0, 0]
@@ -16,26 +17,22 @@ export default class DirectionSession extends BaseSession {
16 17
   }
17 18
 
18 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 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 27
       shape.direction = vec.uni(vec.vec(shape.point, point))
28 28
     }
29 29
   }
30 30
 
31 31
   cancel(data: Data) {
32
-    const { document } = data
32
+    const page = getPage(data, this.snapshot.currentPageId)
33 33
 
34 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 36
       shape.direction = direction
40 37
     }
41 38
   }
@@ -46,12 +43,7 @@ export default class DirectionSession extends BaseSession {
46 43
 }
47 44
 
48 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 48
   let snapshapes: { id: string; direction: number[] }[] = []
57 49
 
@@ -63,7 +55,7 @@ export function getDirectionSnapshot(data: Data) {
63 55
   })
64 56
 
65 57
   return {
66
-    currentPageId,
58
+    currentPageId: data.currentPageId,
67 59
     shapes: snapshapes,
68 60
   }
69 61
 }

+ 30
- 34
state/sessions/rotate-session.ts 查看文件

@@ -3,8 +3,15 @@ import * as vec from "utils/vec"
3 3
 import BaseSession from "./base-session"
4 4
 import commands from "state/commands"
5 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 16
 export default class RotateSession extends BaseSession {
10 17
   delta = [0, 0]
@@ -17,33 +24,34 @@ export default class RotateSession extends BaseSession {
17 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 31
     const a1 = vec.angle(boundsCenter, this.origin)
25 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 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 46
       shape.point = vec.sub(newCenter, offset)
39 47
     }
40 48
   }
41 49
 
42 50
   cancel(data: Data) {
43
-    const { document } = data
51
+    const page = getPage(data, this.snapshot.currentPageId)
44 52
 
45 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 55
       shape.rotation = rotation
48 56
       shape.point = point
49 57
     }
@@ -55,38 +63,26 @@ export default class RotateSession extends BaseSession {
55 63
 }
56 64
 
57 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 68
   // A mapping of selected shapes and their bounds
70 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 73
   // The common (exterior) bounds of the selected shapes
75 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 78
   return {
83
-    currentPageId,
84 79
     boundsCenter,
85
-    boundsRotation,
80
+    currentPageId: data.currentPageId,
81
+    boundsRotation: data.boundsRotation,
86 82
     shapes: shapes.map(({ id, point, rotation }) => {
87 83
       const bounds = shapesBounds[id]
88 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 87
       return {
92 88
         id,

+ 47
- 12
state/sessions/transform-session.ts 查看文件

@@ -1,25 +1,29 @@
1
-import { Data, TransformEdge, TransformCorner } from "types"
1
+import { Data, Edge, Corner } from "types"
2 2
 import * as vec from "utils/vec"
3 3
 import BaseSession from "./base-session"
4 4
 import commands from "state/commands"
5 5
 import { current } from "immer"
6 6
 import { getShapeUtils } from "lib/shape-utils"
7 7
 import {
8
+  getBoundsCenter,
9
+  getBoundsFromPoints,
8 10
   getCommonBounds,
11
+  getPage,
9 12
   getRelativeTransformedBoundingBox,
13
+  getShapes,
10 14
   getTransformedBoundingBox,
11 15
 } from "utils/utils"
12 16
 
13 17
 export default class TransformSession extends BaseSession {
14 18
   scaleX = 1
15 19
   scaleY = 1
16
-  transformType: TransformEdge | TransformCorner
20
+  transformType: Edge | Corner | "center"
17 21
   origin: number[]
18 22
   snapshot: TransformSnapshot
19 23
 
20 24
   constructor(
21 25
     data: Data,
22
-    transformType: TransformCorner | TransformEdge,
26
+    transformType: Corner | Edge | "center",
23 27
     point: number[]
24 28
   ) {
25 29
     super(data)
@@ -31,8 +35,9 @@ export default class TransformSession extends BaseSession {
31 35
   update(data: Data, point: number[], isAspectRatioLocked = false) {
32 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 42
     const newBoundingBox = getTransformedBoundingBox(
38 43
       initialBounds,
@@ -48,7 +53,8 @@ export default class TransformSession extends BaseSession {
48 53
     // Now work backward to calculate a new bounding box for each of the shapes.
49 54
 
50 55
     selectedIds.forEach((id) => {
51
-      const { initialShape, initialShapeBounds } = shapeBounds[id]
56
+      const { initialShape, initialShapeBounds, transformOrigin } =
57
+        shapeBounds[id]
52 58
 
53 59
       const newShapeBounds = getRelativeTransformedBoundingBox(
54 60
         newBoundingBox,
@@ -58,13 +64,27 @@ export default class TransformSession extends BaseSession {
58 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 82
       getShapeUtils(shape).transform(shape, newShapeBounds, {
64 83
         type: this.transformType,
65 84
         initialShape,
66 85
         scaleX: this.scaleX,
67 86
         scaleY: this.scaleY,
87
+        transformOrigin,
68 88
       })
69 89
     })
70 90
   }
@@ -72,16 +92,20 @@ export default class TransformSession extends BaseSession {
72 92
   cancel(data: Data) {
73 93
     const { currentPageId, selectedIds, shapeBounds } = this.snapshot
74 94
 
95
+    const page = getPage(data, currentPageId)
96
+
75 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 103
       getShapeUtils(shape).transform(shape, initialShapeBounds, {
81 104
         type: this.transformType,
82 105
         initialShape,
83 106
         scaleX: 1,
84 107
         scaleY: 1,
108
+        transformOrigin,
85 109
       })
86 110
     })
87 111
   }
@@ -99,7 +123,7 @@ export default class TransformSession extends BaseSession {
99 123
 
100 124
 export function getTransformSnapshot(
101 125
   data: Data,
102
-  transformType: TransformEdge | TransformCorner
126
+  transformType: Edge | Corner | "center"
103 127
 ) {
104 128
   const {
105 129
     document: { pages },
@@ -117,8 +141,12 @@ export function getTransformSnapshot(
117 141
     })
118 142
   )
119 143
 
144
+  const boundsArr = Object.values(shapesBounds)
145
+
120 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 151
   // Return a mapping of shapes to bounds together with the relative
124 152
   // positions of the shape's bounds within the common bounds shape.
@@ -129,11 +157,18 @@ export function getTransformSnapshot(
129 157
     initialBounds: bounds,
130 158
     shapeBounds: Object.fromEntries(
131 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 166
         return [
133 167
           id,
134 168
           {
135 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,4 +1,4 @@
1
-import { Data, TransformEdge, TransformCorner } from "types"
1
+import { Data, Edge, Corner } from "types"
2 2
 import * as vec from "utils/vec"
3 3
 import BaseSession from "./base-session"
4 4
 import commands from "state/commands"
@@ -9,10 +9,13 @@ import {
9 9
   getCommonBounds,
10 10
   getRotatedCorners,
11 11
   getTransformAnchor,
12
+  getPage,
13
+  getShape,
14
+  getSelectedShapes,
12 15
 } from "utils/utils"
13 16
 
14 17
 export default class TransformSingleSession extends BaseSession {
15
-  transformType: TransformEdge | TransformCorner
18
+  transformType: Edge | Corner
16 19
   origin: number[]
17 20
   scaleX = 1
18 21
   scaleY = 1
@@ -21,7 +24,7 @@ export default class TransformSingleSession extends BaseSession {
21 24
 
22 25
   constructor(
23 26
     data: Data,
24
-    transformType: TransformCorner | TransformEdge,
27
+    transformType: Corner | Edge,
25 28
     point: number[],
26 29
     isCreating = false
27 30
   ) {
@@ -38,7 +41,7 @@ export default class TransformSingleSession extends BaseSession {
38 41
     const { initialShapeBounds, currentPageId, initialShape, id } =
39 42
       this.snapshot
40 43
 
41
-    const shape = data.document.pages[currentPageId].shapes[id]
44
+    const shape = getShape(data, id, currentPageId)
42 45
 
43 46
     const newBoundingBox = getTransformedBoundingBox(
44 47
       initialShapeBounds,
@@ -56,6 +59,7 @@ export default class TransformSingleSession extends BaseSession {
56 59
       type: this.transformType,
57 60
       scaleX: this.scaleX,
58 61
       scaleY: this.scaleY,
62
+      transformOrigin: [0.5, 0.5],
59 63
     })
60 64
   }
61 65
 
@@ -63,15 +67,14 @@ export default class TransformSingleSession extends BaseSession {
63 67
     const { id, initialShape, initialShapeBounds, currentPageId } =
64 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 72
     getShapeUtils(shape).transform(shape, initialShapeBounds, {
71 73
       initialShape,
72 74
       type: this.transformType,
73 75
       scaleX: this.scaleX,
74 76
       scaleY: this.scaleY,
77
+      transformOrigin: [0.5, 0.5],
75 78
     })
76 79
   }
77 80
 
@@ -89,21 +92,14 @@ export default class TransformSingleSession extends BaseSession {
89 92
 
90 93
 export function getTransformSingleSnapshot(
91 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 98
   const bounds = getShapeUtils(shape).getBounds(shape)
103 99
 
104 100
   return {
105
-    id,
106
-    currentPageId,
101
+    id: shape.id,
102
+    currentPageId: data.currentPageId,
107 103
     type: transformType,
108 104
     initialShape: shape,
109 105
     initialShapeBounds: bounds,

+ 5
- 8
state/sessions/translate-session.ts 查看文件

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

+ 57
- 29
state/state.ts 查看文件

@@ -1,13 +1,19 @@
1 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 9
 import * as vec from "utils/vec"
4 10
 import {
5 11
   Data,
6 12
   PointerInfo,
7 13
   Shape,
8 14
   ShapeType,
9
-  TransformCorner,
10
-  TransformEdge,
15
+  Corner,
16
+  Edge,
11 17
   CodeControl,
12 18
 } from "types"
13 19
 import inputs from "./inputs"
@@ -99,9 +105,11 @@ const state = createState({
99 105
                 SELECTED_ALL: "selectAll",
100 106
                 POINTED_CANVAS: { to: "brushSelecting" },
101 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 113
                 MOVED_OVER_SHAPE: {
106 114
                   if: "pointHitsShape",
107 115
                   then: {
@@ -156,9 +164,12 @@ const state = createState({
156 164
             },
157 165
             rotatingSelection: {
158 166
               onEnter: "startRotateSession",
167
+              onExit: "clearBoundsRotation",
159 168
               on: {
160 169
                 MOVED_POINTER: "updateRotateSession",
161 170
                 PANNED_CAMERA: "updateRotateSession",
171
+                PRESSED_SHIFT_KEY: "keyUpdateRotateSession",
172
+                RELEASED_SHIFT_KEY: "keyUpdateRotateSession",
162 173
                 STOPPED_POINTING: { do: "completeSession", to: "selecting" },
163 174
                 CANCELLED: { do: "cancelSession", to: "selecting" },
164 175
               },
@@ -420,14 +431,19 @@ const state = createState({
420 431
       return data.hoveredId === payload.target
421 432
     },
422 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 436
       return getShapeUtils(shape).hitTest(
427 437
         shape,
428 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 448
   actions: {
433 449
     /* --------------------- Shapes --------------------- */
@@ -438,7 +454,7 @@ const state = createState({
438 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 458
       data.selectedIds.clear()
443 459
       data.selectedIds.add(shape.id)
444 460
     },
@@ -449,7 +465,7 @@ const state = createState({
449 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 469
       data.selectedIds.clear()
454 470
       data.selectedIds.add(shape.id)
455 471
     },
@@ -461,7 +477,7 @@ const state = createState({
461 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 481
       data.selectedIds.clear()
466 482
       data.selectedIds.add(shape.id)
467 483
     },
@@ -472,7 +488,7 @@ const state = createState({
472 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 492
       data.selectedIds.clear()
477 493
       data.selectedIds.add(shape.id)
478 494
     },
@@ -484,7 +500,7 @@ const state = createState({
484 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 504
       data.selectedIds.clear()
489 505
       data.selectedIds.add(shape.id)
490 506
     },
@@ -495,7 +511,7 @@ const state = createState({
495 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 515
       data.selectedIds.clear()
500 516
       data.selectedIds.add(shape.id)
501 517
     },
@@ -529,8 +545,15 @@ const state = createState({
529 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 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 559
     // Dragging / Translating
@@ -564,7 +587,7 @@ const state = createState({
564 587
     // Dragging / Translating
565 588
     startTransformSession(
566 589
       data,
567
-      payload: PointerInfo & { target: TransformCorner | TransformEdge }
590
+      payload: PointerInfo & { target: Corner | Edge }
568 591
     ) {
569 592
       session =
570 593
         data.selectedIds.size === 1
@@ -583,7 +606,7 @@ const state = createState({
583 606
     startDrawTransformSession(data, payload: PointerInfo) {
584 607
       session = new Sessions.TransformSingleSession(
585 608
         data,
586
-        TransformCorner.BottomRight,
609
+        Corner.BottomRight,
587 610
         screenToWorld(payload.point, data),
588 611
         true
589 612
       )
@@ -619,9 +642,10 @@ const state = createState({
619 642
     /* -------------------- Selection ------------------- */
620 643
 
621 644
     selectAll(data) {
622
-      const { selectedIds, document, currentPageId } = data
645
+      const { selectedIds } = data
646
+      const page = getPage(data)
623 647
       selectedIds.clear()
624
-      for (let id in document.pages[currentPageId].shapes) {
648
+      for (let id in page.shapes) {
625 649
         selectedIds.add(id)
626 650
       }
627 651
     },
@@ -654,6 +678,14 @@ const state = createState({
654 678
 
655 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 689
     zoomCamera(data, payload: { delta: number; point: number[] }) {
658 690
       const { camera } = data
659 691
       const p0 = screenToWorld(payload.point, data)
@@ -678,18 +710,16 @@ const state = createState({
678 710
       )
679 711
     },
680 712
     deleteSelectedIds(data) {
681
-      const { document, currentPageId } = data
682
-      const { shapes } = document.pages[currentPageId]
713
+      const page = getPage(data)
683 714
 
684 715
       data.hoveredId = undefined
685 716
       data.pointedId = undefined
686 717
 
687 718
       data.selectedIds.forEach((id) => {
688
-        delete shapes[id]
719
+        delete page.shapes[id]
689 720
         // TODO: recursively delete children
690 721
       })
691 722
 
692
-      data.document.pages[currentPageId].shapes = shapes
693 723
       data.selectedIds.clear()
694 724
     },
695 725
 
@@ -784,14 +814,12 @@ const state = createState({
784 814
       return new Set(data.selectedIds)
785 815
     },
786 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 821
       const shapes = Array.from(selectedIds.values())
794
-        .map((id) => pages[currentPageId].shapes[id])
822
+        .map((id) => page.shapes[id])
795 823
         .filter(Boolean)
796 824
 
797 825
       if (selectedIds.size === 0) return null

+ 2
- 2
types.ts 查看文件

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

+ 128
- 76
utils/utils.ts 查看文件

@@ -1,9 +1,9 @@
1 1
 import Vector from "lib/code/vector"
2
-import { getShapeUtils } from "lib/shape-utils"
3 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 4
 import * as vec from "./vec"
5
+import _isMobile from "ismobilejs"
6
+import { getShapeUtils } from "lib/shape-utils"
7 7
 
8 8
 export function screenToWorld(point: number[], data: Data) {
9 9
   return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
@@ -42,7 +42,7 @@ export function getCommonBounds(...b: Bounds[]) {
42 42
   return bounds
43 43
 }
44 44
 
45
-// export function getBoundsFromPoints(a: number[], b: number[]) {
45
+// export function getBoundsFromTwoPoints(a: number[], b: number[]) {
46 46
 //   const minX = Math.min(a[0], b[0])
47 47
 //   const maxX = Math.max(a[0], b[0])
48 48
 //   const minY = Math.min(a[1], b[1])
@@ -900,59 +900,59 @@ export function metaKey(e: KeyboardEvent | React.KeyboardEvent) {
900 900
 }
901 901
 
902 902
 export function getTransformAnchor(
903
-  type: TransformEdge | TransformCorner,
903
+  type: Edge | Corner,
904 904
   isFlippedX: boolean,
905 905
   isFlippedY: boolean
906 906
 ) {
907
-  let anchor: TransformCorner | TransformEdge = type
907
+  let anchor: Corner | Edge = type
908 908
 
909 909
   // Change corner anchors if flipped
910 910
   switch (type) {
911
-    case TransformCorner.TopLeft: {
911
+    case Corner.TopLeft: {
912 912
       if (isFlippedX && isFlippedY) {
913
-        anchor = TransformCorner.BottomRight
913
+        anchor = Corner.BottomRight
914 914
       } else if (isFlippedX) {
915
-        anchor = TransformCorner.TopRight
915
+        anchor = Corner.TopRight
916 916
       } else if (isFlippedY) {
917
-        anchor = TransformCorner.BottomLeft
917
+        anchor = Corner.BottomLeft
918 918
       } else {
919
-        anchor = TransformCorner.BottomRight
919
+        anchor = Corner.BottomRight
920 920
       }
921 921
       break
922 922
     }
923
-    case TransformCorner.TopRight: {
923
+    case Corner.TopRight: {
924 924
       if (isFlippedX && isFlippedY) {
925
-        anchor = TransformCorner.BottomLeft
925
+        anchor = Corner.BottomLeft
926 926
       } else if (isFlippedX) {
927
-        anchor = TransformCorner.TopLeft
927
+        anchor = Corner.TopLeft
928 928
       } else if (isFlippedY) {
929
-        anchor = TransformCorner.BottomRight
929
+        anchor = Corner.BottomRight
930 930
       } else {
931
-        anchor = TransformCorner.BottomLeft
931
+        anchor = Corner.BottomLeft
932 932
       }
933 933
       break
934 934
     }
935
-    case TransformCorner.BottomRight: {
935
+    case Corner.BottomRight: {
936 936
       if (isFlippedX && isFlippedY) {
937
-        anchor = TransformCorner.TopLeft
937
+        anchor = Corner.TopLeft
938 938
       } else if (isFlippedX) {
939
-        anchor = TransformCorner.BottomLeft
939
+        anchor = Corner.BottomLeft
940 940
       } else if (isFlippedY) {
941
-        anchor = TransformCorner.TopRight
941
+        anchor = Corner.TopRight
942 942
       } else {
943
-        anchor = TransformCorner.TopLeft
943
+        anchor = Corner.TopLeft
944 944
       }
945 945
       break
946 946
     }
947
-    case TransformCorner.BottomLeft: {
947
+    case Corner.BottomLeft: {
948 948
       if (isFlippedX && isFlippedY) {
949
-        anchor = TransformCorner.TopRight
949
+        anchor = Corner.TopRight
950 950
       } else if (isFlippedX) {
951
-        anchor = TransformCorner.BottomRight
951
+        anchor = Corner.BottomRight
952 952
       } else if (isFlippedY) {
953
-        anchor = TransformCorner.TopLeft
953
+        anchor = Corner.TopLeft
954 954
       } else {
955
-        anchor = TransformCorner.TopRight
955
+        anchor = Corner.TopRight
956 956
       }
957 957
       break
958 958
     }
@@ -1030,6 +1030,18 @@ export function rotateBounds(
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 1045
 export function getRotatedCorners(b: Bounds, rotation: number) {
1034 1046
   const center = [b.minX + b.width / 2, b.minY + b.height / 2]
1035 1047
 
@@ -1043,7 +1055,7 @@ export function getRotatedCorners(b: Bounds, rotation: number) {
1043 1055
 
1044 1056
 export function getTransformedBoundingBox(
1045 1057
   bounds: Bounds,
1046
-  handle: TransformCorner | TransformEdge | "center",
1058
+  handle: Corner | Edge | "center",
1047 1059
   delta: number[],
1048 1060
   rotation = 0,
1049 1061
   isAspectRatioLocked = false
@@ -1082,30 +1094,30 @@ export function getTransformedBoundingBox(
1082 1094
   corners should change.
1083 1095
   */
1084 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 1100
       by0 += dy
1089 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 1106
       by1 += dy
1095 1107
       break
1096 1108
     }
1097 1109
   }
1098 1110
 
1099 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 1115
       bx0 += dx
1104 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 1121
       bx1 += dx
1110 1122
       break
1111 1123
     }
@@ -1117,6 +1129,9 @@ export function getTransformedBoundingBox(
1117 1129
   const scaleX = (bx1 - bx0) / aw
1118 1130
   const scaleY = (by1 - by0) / ah
1119 1131
 
1132
+  const flipX = scaleX < 0
1133
+  const flipY = scaleY < 0
1134
+
1120 1135
   const bw = Math.abs(bx1 - bx0)
1121 1136
   const bh = Math.abs(by1 - by0)
1122 1137
 
@@ -1134,36 +1149,36 @@ export function getTransformedBoundingBox(
1134 1149
     const th = bh * (scaleX < 0 ? 1 : -1) * ar
1135 1150
 
1136 1151
     switch (handle) {
1137
-      case TransformCorner.TopLeft: {
1152
+      case Corner.TopLeft: {
1138 1153
         if (isTall) by0 = by1 + tw
1139 1154
         else bx0 = bx1 + th
1140 1155
         break
1141 1156
       }
1142
-      case TransformCorner.TopRight: {
1157
+      case Corner.TopRight: {
1143 1158
         if (isTall) by0 = by1 + tw
1144 1159
         else bx1 = bx0 - th
1145 1160
         break
1146 1161
       }
1147
-      case TransformCorner.BottomRight: {
1162
+      case Corner.BottomRight: {
1148 1163
         if (isTall) by1 = by0 - tw
1149 1164
         else bx1 = bx0 - th
1150 1165
         break
1151 1166
       }
1152
-      case TransformCorner.BottomLeft: {
1167
+      case Corner.BottomLeft: {
1153 1168
         if (isTall) by1 = by0 - tw
1154 1169
         else bx0 = bx1 + th
1155 1170
         break
1156 1171
       }
1157
-      case TransformEdge.Bottom:
1158
-      case TransformEdge.Top: {
1172
+      case Edge.Bottom:
1173
+      case Edge.Top: {
1159 1174
         const m = (bx0 + bx1) / 2
1160 1175
         const w = bh * ar
1161 1176
         bx0 = m - w / 2
1162 1177
         bx1 = m + w / 2
1163 1178
         break
1164 1179
       }
1165
-      case TransformEdge.Left:
1166
-      case TransformEdge.Right: {
1180
+      case Edge.Left:
1181
+      case Edge.Right: {
1167 1182
         const m = (by0 + by1) / 2
1168 1183
         const h = bw / ar
1169 1184
         by0 = m - h / 2
@@ -1189,56 +1204,56 @@ export function getTransformedBoundingBox(
1189 1204
     const c1 = vec.med([bx0, by0], [bx1, by1])
1190 1205
 
1191 1206
     switch (handle) {
1192
-      case TransformCorner.TopLeft: {
1207
+      case Corner.TopLeft: {
1193 1208
         cv = vec.sub(
1194 1209
           vec.rotWith([bx1, by1], c1, rotation),
1195 1210
           vec.rotWith([ax1, ay1], c0, rotation)
1196 1211
         )
1197 1212
         break
1198 1213
       }
1199
-      case TransformCorner.TopRight: {
1214
+      case Corner.TopRight: {
1200 1215
         cv = vec.sub(
1201 1216
           vec.rotWith([bx0, by1], c1, rotation),
1202 1217
           vec.rotWith([ax0, ay1], c0, rotation)
1203 1218
         )
1204 1219
         break
1205 1220
       }
1206
-      case TransformCorner.BottomRight: {
1221
+      case Corner.BottomRight: {
1207 1222
         cv = vec.sub(
1208 1223
           vec.rotWith([bx0, by0], c1, rotation),
1209 1224
           vec.rotWith([ax0, ay0], c0, rotation)
1210 1225
         )
1211 1226
         break
1212 1227
       }
1213
-      case TransformCorner.BottomLeft: {
1228
+      case Corner.BottomLeft: {
1214 1229
         cv = vec.sub(
1215 1230
           vec.rotWith([bx1, by0], c1, rotation),
1216 1231
           vec.rotWith([ax1, ay0], c0, rotation)
1217 1232
         )
1218 1233
         break
1219 1234
       }
1220
-      case TransformEdge.Top: {
1235
+      case Edge.Top: {
1221 1236
         cv = vec.sub(
1222 1237
           vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation),
1223 1238
           vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation)
1224 1239
         )
1225 1240
         break
1226 1241
       }
1227
-      case TransformEdge.Left: {
1242
+      case Edge.Left: {
1228 1243
         cv = vec.sub(
1229 1244
           vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation),
1230 1245
           vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation)
1231 1246
         )
1232 1247
         break
1233 1248
       }
1234
-      case TransformEdge.Bottom: {
1249
+      case Edge.Bottom: {
1235 1250
         cv = vec.sub(
1236 1251
           vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation),
1237 1252
           vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation)
1238 1253
         )
1239 1254
         break
1240 1255
       }
1241
-      case TransformEdge.Right: {
1256
+      case Edge.Right: {
1242 1257
         cv = vec.sub(
1243 1258
           vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation),
1244 1259
           vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation)
@@ -1273,8 +1288,8 @@ export function getTransformedBoundingBox(
1273 1288
     maxY: by1,
1274 1289
     width: bx1 - bx0,
1275 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,25 +1300,23 @@ export function getRelativeTransformedBoundingBox(
1285 1300
   isFlippedX: boolean,
1286 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 1321
   return {
1309 1322
     minX,
@@ -1314,3 +1327,42 @@ export function getRelativeTransformedBoundingBox(
1314 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,6 +4154,11 @@ isexe@^2.0.0:
4154 4154
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
4155 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 4162
 isobject@^2.0.0:
4158 4163
   version "2.1.0"
4159 4164
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"

Loading…
取消
儲存