浏览代码

Improves transforms

main
Steve Ruiz 4 年前
父节点
当前提交
c3740cacdd

+ 2
- 1
components/canvas/bounds-bg.tsx 查看文件

6
 export default function BoundsBg() {
6
 export default function BoundsBg() {
7
   const rBounds = useRef<SVGRectElement>(null)
7
   const rBounds = useRef<SVGRectElement>(null)
8
   const bounds = useSelector((state) => state.values.selectedBounds)
8
   const bounds = useSelector((state) => state.values.selectedBounds)
9
-
9
+  const isSelecting = useSelector((s) => s.isIn("selecting"))
10
   const rotation = useSelector((s) => {
10
   const rotation = useSelector((s) => {
11
     if (s.data.selectedIds.size === 1) {
11
     if (s.data.selectedIds.size === 1) {
12
       const { shapes } = s.data.document.pages[s.data.currentPageId]
12
       const { shapes } = s.data.document.pages[s.data.currentPageId]
18
   })
18
   })
19
 
19
 
20
   if (!bounds) return null
20
   if (!bounds) return null
21
+  if (!isSelecting) return null
21
 
22
 
22
   const { minX, minY, width, height } = bounds
23
   const { minX, minY, width, height } = bounds
23
 
24
 

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

7
 
7
 
8
 export default function Bounds() {
8
 export default function Bounds() {
9
   const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
9
   const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
10
+  const isSelecting = useSelector((s) => s.isIn("selecting"))
10
   const zoom = useSelector((s) => s.data.camera.zoom)
11
   const zoom = useSelector((s) => s.data.camera.zoom)
11
   const bounds = useSelector((s) => s.values.selectedBounds)
12
   const bounds = useSelector((s) => s.values.selectedBounds)
12
-
13
   const rotation = useSelector((s) => {
13
   const rotation = useSelector((s) => {
14
     if (s.data.selectedIds.size === 1) {
14
     if (s.data.selectedIds.size === 1) {
15
       const { shapes } = s.data.document.pages[s.data.currentPageId]
15
       const { shapes } = s.data.document.pages[s.data.currentPageId]
21
   })
21
   })
22
 
22
 
23
   if (!bounds) return null
23
   if (!bounds) return null
24
+  if (!isSelecting) return null
24
 
25
 
25
   let { minX, minY, maxX, maxY, width, height } = bounds
26
   let { minX, minY, maxX, maxY, width, height } = bounds
26
 
27
 

+ 1
- 1
lib/code/circle.ts 查看文件

18
       rotation: 0,
18
       rotation: 0,
19
       radius: 20,
19
       radius: 20,
20
       style: {
20
       style: {
21
-        fill: "rgba(142, 143, 142, 1.000)",
21
+        fill: "#c6cacb",
22
         stroke: "#000",
22
         stroke: "#000",
23
         strokeWidth: 1,
23
         strokeWidth: 1,
24
       },
24
       },

+ 1
- 1
lib/code/dot.ts 查看文件

17
       point: [0, 0],
17
       point: [0, 0],
18
       rotation: 0,
18
       rotation: 0,
19
       style: {
19
       style: {
20
-        fill: "rgba(142, 143, 142, 1.000)",
20
+        fill: "#c6cacb",
21
         stroke: "#000",
21
         stroke: "#000",
22
         strokeWidth: 1,
22
         strokeWidth: 1,
23
       },
23
       },

+ 1
- 1
lib/code/ellipse.ts 查看文件

19
       radiusY: 20,
19
       radiusY: 20,
20
       rotation: 0,
20
       rotation: 0,
21
       style: {
21
       style: {
22
-        fill: "rgba(142, 143, 142, 1.000)",
22
+        fill: "#c6cacb",
23
         stroke: "#000",
23
         stroke: "#000",
24
         strokeWidth: 1,
24
         strokeWidth: 1,
25
       },
25
       },

+ 1
- 1
lib/code/line.ts 查看文件

19
       direction: [-0.5, 0.5],
19
       direction: [-0.5, 0.5],
20
       rotation: 0,
20
       rotation: 0,
21
       style: {
21
       style: {
22
-        fill: "rgba(142, 143, 142, 1.000)",
22
+        fill: "#c6cacb",
23
         stroke: "#000",
23
         stroke: "#000",
24
         strokeWidth: 1,
24
         strokeWidth: 1,
25
       },
25
       },

+ 1
- 1
lib/code/ray.ts 查看文件

19
       direction: [0, 1],
19
       direction: [0, 1],
20
       rotation: 0,
20
       rotation: 0,
21
       style: {
21
       style: {
22
-        fill: "rgba(142, 143, 142, 1.000)",
22
+        fill: "#c6cacb",
23
         stroke: "#000",
23
         stroke: "#000",
24
         strokeWidth: 1,
24
         strokeWidth: 1,
25
       },
25
       },

+ 1
- 1
lib/code/rectangle.ts 查看文件

19
       size: [100, 100],
19
       size: [100, 100],
20
       rotation: 0,
20
       rotation: 0,
21
       style: {
21
       style: {
22
-        fill: "rgba(142, 143, 142, 1.000)",
22
+        fill: "#c6cacb",
23
         stroke: "#000",
23
         stroke: "#000",
24
         strokeWidth: 1,
24
         strokeWidth: 1,
25
       },
25
       },

+ 5
- 1
lib/shapes/circle.tsx 查看文件

22
       rotation: 0,
22
       rotation: 0,
23
       radius: 20,
23
       radius: 20,
24
       style: {
24
       style: {
25
-        fill: "rgba(142, 143, 142, 1.000)",
25
+        fill: "#c6cacb",
26
         stroke: "#000",
26
         stroke: "#000",
27
       },
27
       },
28
       ...props,
28
       ...props,
157
     return shape
157
     return shape
158
   },
158
   },
159
 
159
 
160
+  transformSingle(shape, bounds, info) {
161
+    return this.transform(shape, bounds, info)
162
+  },
163
+
160
   canTransform: true,
164
   canTransform: true,
161
 })
165
 })
162
 
166
 

+ 5
- 1
lib/shapes/dot.tsx 查看文件

22
       point: [0, 0],
22
       point: [0, 0],
23
       rotation: 0,
23
       rotation: 0,
24
       style: {
24
       style: {
25
-        fill: "rgba(142, 143, 142, 1.000)",
25
+        fill: "#c6cacb",
26
         stroke: "#000",
26
         stroke: "#000",
27
       },
27
       },
28
       ...props,
28
       ...props,
89
     return shape
89
     return shape
90
   },
90
   },
91
 
91
 
92
+  transformSingle(shape, bounds, info) {
93
+    return this.transform(shape, bounds, info)
94
+  },
95
+
92
   canTransform: false,
96
   canTransform: false,
93
 })
97
 })
94
 
98
 

+ 16
- 5
lib/shapes/ellipse.tsx 查看文件

5
 import { boundsContained } from "utils/bounds"
5
 import { boundsContained } from "utils/bounds"
6
 import { intersectEllipseBounds } from "utils/intersections"
6
 import { intersectEllipseBounds } from "utils/intersections"
7
 import { pointInEllipse } from "utils/hitTests"
7
 import { pointInEllipse } from "utils/hitTests"
8
-import { translateBounds } from "utils/utils"
8
+import {
9
+  getBoundsFromPoints,
10
+  getRotatedCorners,
11
+  rotateBounds,
12
+  translateBounds,
13
+} from "utils/utils"
9
 
14
 
10
 const ellipse = createShape<EllipseShape>({
15
 const ellipse = createShape<EllipseShape>({
11
   boundsCache: new WeakMap([]),
16
   boundsCache: new WeakMap([]),
23
       radiusY: 20,
28
       radiusY: 20,
24
       rotation: 0,
29
       rotation: 0,
25
       style: {
30
       style: {
26
-        fill: "rgba(142, 143, 142, 1.000)",
31
+        fill: "#c6cacb",
27
         stroke: "#000",
32
         stroke: "#000",
28
       },
33
       },
29
       ...props,
34
       ...props,
56
   },
61
   },
57
 
62
 
58
   getRotatedBounds(shape) {
63
   getRotatedBounds(shape) {
59
-    return this.getBounds(shape)
64
+    return getBoundsFromPoints(getRotatedCorners(shape))
60
   },
65
   },
61
 
66
 
62
   getCenter(shape) {
67
   getCenter(shape) {
68
       point,
73
       point,
69
       vec.add(shape.point, [shape.radiusX, shape.radiusY]),
74
       vec.add(shape.point, [shape.radiusX, shape.radiusY]),
70
       shape.radiusX,
75
       shape.radiusX,
71
-      shape.radiusY
76
+      shape.radiusY,
77
+      shape.rotation
72
     )
78
     )
73
   },
79
   },
74
 
80
 
83
         vec.add(shape.point, [shape.radiusX, shape.radiusY]),
89
         vec.add(shape.point, [shape.radiusX, shape.radiusY]),
84
         shape.radiusX,
90
         shape.radiusX,
85
         shape.radiusY,
91
         shape.radiusY,
86
-        brushBounds
92
+        brushBounds,
93
+        shape.rotation
87
       ).length > 0
94
       ).length > 0
88
     )
95
     )
89
   },
96
   },
109
     return shape
116
     return shape
110
   },
117
   },
111
 
118
 
119
+  transformSingle(shape, bounds, info) {
120
+    return this.transform(shape, bounds, info)
121
+  },
122
+
112
   canTransform: true,
123
   canTransform: true,
113
 })
124
 })
114
 
125
 

+ 17
- 0
lib/shapes/index.tsx 查看文件

72
     }
72
     }
73
   ): K
73
   ): K
74
 
74
 
75
+  transformSingle(
76
+    this: ShapeUtility<K>,
77
+    shape: K,
78
+    bounds: Bounds,
79
+    info: {
80
+      type: TransformEdge | TransformCorner
81
+      boundsRotation: number
82
+      initialShape: K
83
+      initialShapeBounds: BoundsSnapshot
84
+      initialBounds: Bounds
85
+      isFlippedX: boolean
86
+      isFlippedY: boolean
87
+      isSingle: boolean
88
+      anchor: TransformEdge | TransformCorner
89
+    }
90
+  ): K
91
+
75
   // Apply a scale to a shape.
92
   // Apply a scale to a shape.
76
   scale(this: ShapeUtility<K>, shape: K, scale: number): K
93
   scale(this: ShapeUtility<K>, shape: K, scale: number): K
77
 
94
 

+ 5
- 1
lib/shapes/line.tsx 查看文件

22
       direction: [0, 0],
22
       direction: [0, 0],
23
       rotation: 0,
23
       rotation: 0,
24
       style: {
24
       style: {
25
-        fill: "rgba(142, 143, 142, 1.000)",
25
+        fill: "#c6cacb",
26
         stroke: "#000",
26
         stroke: "#000",
27
       },
27
       },
28
       ...props,
28
       ...props,
97
     return shape
97
     return shape
98
   },
98
   },
99
 
99
 
100
+  transformSingle(shape, bounds, info) {
101
+    return this.transform(shape, bounds, info)
102
+  },
103
+
100
   canTransform: false,
104
   canTransform: false,
101
 })
105
 })
102
 
106
 

+ 4
- 0
lib/shapes/polyline.tsx 查看文件

123
     return shape
123
     return shape
124
   },
124
   },
125
 
125
 
126
+  transformSingle(shape, bounds, info) {
127
+    return this.transform(shape, bounds, info)
128
+  },
129
+
126
   canTransform: true,
130
   canTransform: true,
127
 })
131
 })
128
 
132
 

+ 1
- 1
lib/shapes/ray.tsx 查看文件

22
       direction: [0, 1],
22
       direction: [0, 1],
23
       rotation: 0,
23
       rotation: 0,
24
       style: {
24
       style: {
25
-        fill: "rgba(142, 143, 142, 1.000)",
25
+        fill: "#c6cacb",
26
         stroke: "#000",
26
         stroke: "#000",
27
         strokeWidth: 1,
27
         strokeWidth: 1,
28
       },
28
       },

+ 119
- 26
lib/shapes/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 { RectangleShape, ShapeType } from "types"
3
+import {
4
+  RectangleShape,
5
+  ShapeType,
6
+  TransformCorner,
7
+  TransformEdge,
8
+} from "types"
4
 import { createShape } from "./index"
9
 import { createShape } from "./index"
5
 import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
10
 import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
6
-import { getBoundsFromPoints, rotateBounds, translateBounds } from "utils/utils"
11
+import {
12
+  getBoundsFromPoints,
13
+  getRotatedCorners,
14
+  rotateBounds,
15
+  translateBounds,
16
+} from "utils/utils"
7
 
17
 
8
 const rectangle = createShape<RectangleShape>({
18
 const rectangle = createShape<RectangleShape>({
9
   boundsCache: new WeakMap([]),
19
   boundsCache: new WeakMap([]),
20
       size: [1, 1],
30
       size: [1, 1],
21
       rotation: 0,
31
       rotation: 0,
22
       style: {
32
       style: {
23
-        fill: "rgba(142, 143, 142, 1.000)",
33
+        fill: "#c6cacb",
24
         stroke: "#000",
34
         stroke: "#000",
25
       },
35
       },
26
       ...props,
36
       ...props,
50
   },
60
   },
51
 
61
 
52
   getRotatedBounds(shape) {
62
   getRotatedBounds(shape) {
53
-    const b = this.getBounds(shape)
54
-    const center = [b.minX + b.width / 2, b.minY + b.height / 2]
55
-
56
-    // Rotate corners of the shape, then find the minimum among those points.
57
-    const rotatedCorners = [
58
-      [b.minX, b.minY],
59
-      [b.maxX, b.minY],
60
-      [b.maxX, b.maxY],
61
-      [b.minX, b.maxY],
62
-    ].map((point) => vec.rotWith(point, center, shape.rotation))
63
-
64
-    return getBoundsFromPoints(rotatedCorners)
63
+    return getBoundsFromPoints(
64
+      getRotatedCorners(this.getBounds(shape), shape.rotation)
65
+    )
65
   },
66
   },
66
 
67
 
67
   getCenter(shape) {
68
   getCenter(shape) {
74
   },
75
   },
75
 
76
 
76
   hitTestBounds(shape, brushBounds) {
77
   hitTestBounds(shape, brushBounds) {
77
-    const b = this.getBounds(shape)
78
-    const center = [b.minX + b.width / 2, b.minY + b.height / 2]
79
-
80
-    const rotatedCorners = [
81
-      [b.minX, b.minY],
82
-      [b.maxX, b.minY],
83
-      [b.maxX, b.maxY],
84
-      [b.minX, b.maxY],
85
-    ].map((point) => vec.rotWith(point, center, shape.rotation))
78
+    const rotatedCorners = getRotatedCorners(
79
+      this.getBounds(shape),
80
+      shape.rotation
81
+    )
86
 
82
 
87
     return (
83
     return (
88
       boundsContainPolygon(brushBounds, rotatedCorners) ||
84
       boundsContainPolygon(brushBounds, rotatedCorners) ||
108
     shapeBounds,
104
     shapeBounds,
109
     { initialShape, isSingle, initialShapeBounds, isFlippedX, isFlippedY }
105
     { initialShape, isSingle, initialShapeBounds, isFlippedX, isFlippedY }
110
   ) {
106
   ) {
111
-    // TODO: Apply rotation to single-selection items
112
-
113
     if (shape.rotation === 0 || isSingle) {
107
     if (shape.rotation === 0 || isSingle) {
114
       shape.size = [shapeBounds.width, shapeBounds.height]
108
       shape.size = [shapeBounds.width, shapeBounds.height]
115
       shape.point = [shapeBounds.minX, shapeBounds.minY]
109
       shape.point = [shapeBounds.minX, shapeBounds.minY]
145
     return shape
139
     return shape
146
   },
140
   },
147
 
141
 
142
+  transformSingle(
143
+    shape,
144
+    bounds,
145
+    { initialShape, initialShapeBounds, anchor, isFlippedY, isFlippedX }
146
+  ) {
147
+    shape.size = [bounds.width, bounds.height]
148
+    shape.point = [bounds.minX, bounds.minY]
149
+
150
+    // const prevCorners = getRotatedCorners(
151
+    //   initialShapeBounds,
152
+    //   initialShape.rotation
153
+    // )
154
+
155
+    // let currCorners = getRotatedCorners(this.getBounds(shape), shape.rotation)
156
+
157
+    // if (isFlippedX) {
158
+    //   let t = currCorners[3]
159
+    //   currCorners[3] = currCorners[2]
160
+    //   currCorners[2] = t
161
+
162
+    //   t = currCorners[0]
163
+    //   currCorners[0] = currCorners[1]
164
+    //   currCorners[1] = t
165
+    // }
166
+
167
+    // if (isFlippedY) {
168
+    //   let t = currCorners[3]
169
+    //   currCorners[3] = currCorners[0]
170
+    //   currCorners[0] = t
171
+
172
+    //   t = currCorners[2]
173
+    //   currCorners[2] = currCorners[1]
174
+    //   currCorners[1] = t
175
+    // }
176
+
177
+    // switch (anchor) {
178
+    //   case TransformCorner.TopLeft: {
179
+    //     shape.point = vec.sub(
180
+    //       shape.point,
181
+    //       vec.sub(currCorners[2], prevCorners[2])
182
+    //     )
183
+    //     break
184
+    //   }
185
+    //   case TransformCorner.TopRight: {
186
+    //     shape.point = vec.sub(
187
+    //       shape.point,
188
+    //       vec.sub(currCorners[3], prevCorners[3])
189
+    //     )
190
+    //     break
191
+    //   }
192
+    //   case TransformCorner.BottomRight: {
193
+    //     shape.point = vec.sub(
194
+    //       shape.point,
195
+    //       vec.sub(currCorners[0], prevCorners[0])
196
+    //     )
197
+    //     break
198
+    //   }
199
+    //   case TransformCorner.BottomLeft: {
200
+    //     shape.point = vec.sub(
201
+    //       shape.point,
202
+    //       vec.sub(currCorners[1], prevCorners[1])
203
+    //     )
204
+    //     break
205
+    //   }
206
+    //   case TransformEdge.Top: {
207
+    //     shape.point = vec.sub(
208
+    //       shape.point,
209
+    //       vec.sub(currCorners[3], prevCorners[3])
210
+    //     )
211
+    //     break
212
+    //   }
213
+    //   case TransformEdge.Right: {
214
+    //     shape.point = vec.sub(
215
+    //       shape.point,
216
+    //       vec.sub(currCorners[3], prevCorners[3])
217
+    //     )
218
+    //     break
219
+    //   }
220
+    //   case TransformEdge.Bottom: {
221
+    //     shape.point = vec.sub(
222
+    //       shape.point,
223
+    //       vec.sub(currCorners[0], prevCorners[0])
224
+    //     )
225
+    //     break
226
+    //   }
227
+    //   case TransformEdge.Left: {
228
+    //     shape.point = vec.sub(
229
+    //       shape.point,
230
+    //       vec.sub(currCorners[2], prevCorners[2])
231
+    //     )
232
+    //     break
233
+    //   }
234
+    // }
235
+
236
+    // console.log(shape.point, shape.size)
237
+
238
+    return shape
239
+  },
240
+
148
   canTransform: true,
241
   canTransform: true,
149
 })
242
 })
150
 
243
 

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

2
 import history from "../history"
2
 import history from "../history"
3
 import { Data, Shape } from "types"
3
 import { Data, Shape } from "types"
4
 
4
 
5
-export default function createShape(data: Data, shape: Shape) {
5
+export default function createShapeCommand(data: Data, shape: Shape) {
6
   const { currentPageId } = data
6
   const { currentPageId } = data
7
 
7
 
8
   history.execute(
8
   history.execute(

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

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
 
5
 
6
-export default function translateCommand(
6
+export default function directCommand(
7
   data: Data,
7
   data: Data,
8
   before: DirectionSnapshot,
8
   before: DirectionSnapshot,
9
   after: DirectionSnapshot
9
   after: DirectionSnapshot

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

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
 
5
 
6
-export default function setGeneratedShapes(
6
+export default function generateCommand(
7
   data: Data,
7
   data: Data,
8
   currentPageId: string,
8
   currentPageId: string,
9
   generatedShapes: Shape[]
9
   generatedShapes: Shape[]

+ 2
- 0
state/commands/index.ts 查看文件

1
 import translate from "./translate"
1
 import translate from "./translate"
2
 import transform from "./transform"
2
 import transform from "./transform"
3
+import transformSingle from "./transform-single"
3
 import generate from "./generate"
4
 import generate from "./generate"
4
 import createShape from "./create-shape"
5
 import createShape from "./create-shape"
5
 import direct from "./direct"
6
 import direct from "./direct"
8
 const commands = {
9
 const commands = {
9
   translate,
10
   translate,
10
   transform,
11
   transform,
12
+  transformSingle,
11
   generate,
13
   generate,
12
   createShape,
14
   createShape,
13
   direct,
15
   direct,

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

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
 
5
 
6
-export default function translateCommand(
6
+export default function rotateCommand(
7
   data: Data,
7
   data: Data,
8
   before: RotateSnapshot,
8
   before: RotateSnapshot,
9
   after: RotateSnapshot
9
   after: RotateSnapshot

+ 72
- 0
state/commands/transform-single.ts 查看文件

1
+import Command from "./command"
2
+import history from "../history"
3
+import { Data, TransformCorner, TransformEdge } from "types"
4
+import { getShapeUtils } from "lib/shapes"
5
+import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
6
+
7
+export default function transformSingleCommand(
8
+  data: Data,
9
+  before: TransformSingleSnapshot,
10
+  after: TransformSingleSnapshot,
11
+  anchor: TransformCorner | TransformEdge
12
+) {
13
+  history.execute(
14
+    data,
15
+    new Command({
16
+      name: "translate_shapes",
17
+      category: "canvas",
18
+      do(data) {
19
+        const {
20
+          type,
21
+          initialShape,
22
+          initialShapeBounds,
23
+          currentPageId,
24
+          id,
25
+          boundsRotation,
26
+        } = after
27
+
28
+        const { shapes } = data.document.pages[currentPageId]
29
+
30
+        const shape = shapes[id]
31
+
32
+        getShapeUtils(shape).transform(shape, initialShapeBounds, {
33
+          type,
34
+          initialShape,
35
+          initialShapeBounds,
36
+          initialBounds: initialShapeBounds,
37
+          boundsRotation,
38
+          isFlippedX: false,
39
+          isFlippedY: false,
40
+          isSingle: false,
41
+          anchor,
42
+        })
43
+      },
44
+      undo(data) {
45
+        const {
46
+          type,
47
+          initialShape,
48
+          initialShapeBounds,
49
+          currentPageId,
50
+          id,
51
+          boundsRotation,
52
+        } = before
53
+
54
+        const { shapes } = data.document.pages[currentPageId]
55
+
56
+        const shape = shapes[id]
57
+
58
+        getShapeUtils(shape).transform(shape, initialShapeBounds, {
59
+          type,
60
+          initialShape,
61
+          initialShapeBounds,
62
+          initialBounds: initialShapeBounds,
63
+          boundsRotation,
64
+          isFlippedX: false,
65
+          isFlippedY: false,
66
+          isSingle: false,
67
+          anchor,
68
+        })
69
+      },
70
+    })
71
+  )
72
+}

+ 3
- 5
state/commands/transform.ts 查看文件

4
 import { TransformSnapshot } from "state/sessions/transform-session"
4
 import { TransformSnapshot } from "state/sessions/transform-session"
5
 import { getShapeUtils } from "lib/shapes"
5
 import { getShapeUtils } from "lib/shapes"
6
 
6
 
7
-export default function translateCommand(
7
+export default function transformCommand(
8
   data: Data,
8
   data: Data,
9
   before: TransformSnapshot,
9
   before: TransformSnapshot,
10
   after: TransformSnapshot,
10
   after: TransformSnapshot,
22
           initialBounds,
22
           initialBounds,
23
           currentPageId,
23
           currentPageId,
24
           selectedIds,
24
           selectedIds,
25
-          isSingle,
26
           boundsRotation,
25
           boundsRotation,
27
         } = after
26
         } = after
28
 
27
 
40
             boundsRotation,
39
             boundsRotation,
41
             isFlippedX: false,
40
             isFlippedX: false,
42
             isFlippedY: false,
41
             isFlippedY: false,
43
-            isSingle,
42
+            isSingle: false,
44
             anchor,
43
             anchor,
45
           })
44
           })
46
         })
45
         })
52
           initialBounds,
51
           initialBounds,
53
           currentPageId,
52
           currentPageId,
54
           selectedIds,
53
           selectedIds,
55
-          isSingle,
56
           boundsRotation,
54
           boundsRotation,
57
         } = before
55
         } = before
58
 
56
 
70
             boundsRotation,
68
             boundsRotation,
71
             isFlippedX: false,
69
             isFlippedX: false,
72
             isFlippedY: false,
70
             isFlippedY: false,
73
-            isSingle,
71
+            isSingle: false,
74
             anchor: type,
72
             anchor: type,
75
           })
73
           })
76
         })
74
         })

+ 8
- 8
state/data.ts 查看文件

17
           direction: [0.5, 0.5],
17
           direction: [0.5, 0.5],
18
           style: {
18
           style: {
19
             fill: "#AAA",
19
             fill: "#AAA",
20
-            stroke: "rgba(142, 143, 142, 1.000)",
20
+            stroke: "#c6cacb",
21
             strokeWidth: 1,
21
             strokeWidth: 1,
22
           },
22
           },
23
         }),
23
         }),
28
         //   point: [400, 500],
28
         //   point: [400, 500],
29
         //   style: {
29
         //   style: {
30
         //     fill: "#AAA",
30
         //     fill: "#AAA",
31
-        //     stroke: "rgba(142, 143, 142, 1.000)",
31
+        //     stroke: "#c6cacb",
32
         //     strokeWidth: 1,
32
         //     strokeWidth: 1,
33
         //   },
33
         //   },
34
         // }),
34
         // }),
40
           radius: 50,
40
           radius: 50,
41
           style: {
41
           style: {
42
             fill: "#AAA",
42
             fill: "#AAA",
43
-            stroke: "rgba(142, 143, 142, 1.000)",
43
+            stroke: "#c6cacb",
44
             strokeWidth: 1,
44
             strokeWidth: 1,
45
           },
45
           },
46
         }),
46
         }),
53
           radiusY: 30,
53
           radiusY: 30,
54
           style: {
54
           style: {
55
             fill: "#AAA",
55
             fill: "#AAA",
56
-            stroke: "rgba(142, 143, 142, 1.000)",
56
+            stroke: "#c6cacb",
57
             strokeWidth: 1,
57
             strokeWidth: 1,
58
           },
58
           },
59
         }),
59
         }),
66
         //   radiusY: 30,
66
         //   radiusY: 30,
67
         //   style: {
67
         //   style: {
68
         //     fill: "#AAA",
68
         //     fill: "#AAA",
69
-        //     stroke: "rgba(142, 143, 142, 1.000)",
69
+        //     stroke: "#c6cacb",
70
         //     strokeWidth: 1,
70
         //     strokeWidth: 1,
71
         //   },
71
         //   },
72
         // }),
72
         // }),
82
         //   ],
82
         //   ],
83
         //   style: {
83
         //   style: {
84
         //     fill: "none",
84
         //     fill: "none",
85
-        //     stroke: "rgba(142, 143, 142, 1.000)",
85
+        //     stroke: "#c6cacb",
86
         //     strokeWidth: 2,
86
         //     strokeWidth: 2,
87
         //     strokeLinecap: "round",
87
         //     strokeLinecap: "round",
88
         //     strokeLinejoin: "round",
88
         //     strokeLinejoin: "round",
96
           size: [200, 200],
96
           size: [200, 200],
97
           style: {
97
           style: {
98
             fill: "#AAA",
98
             fill: "#AAA",
99
-            stroke: "rgba(142, 143, 142, 1.000)",
99
+            stroke: "#c6cacb",
100
             strokeWidth: 1,
100
             strokeWidth: 1,
101
           },
101
           },
102
         }),
102
         }),
108
         //   direction: [0.2, 0.2],
108
         //   direction: [0.2, 0.2],
109
         //   style: {
109
         //   style: {
110
         //     fill: "#AAA",
110
         //     fill: "#AAA",
111
-        //     stroke: "rgba(142, 143, 142, 1.000)",
111
+        //     stroke: "#c6cacb",
112
         //     strokeWidth: 1,
112
         //     strokeWidth: 1,
113
         //   },
113
         //   },
114
         // }),
114
         // }),

+ 2
- 0
state/sessions/index.ts 查看文件

2
 import BrushSession from "./brush-session"
2
 import BrushSession from "./brush-session"
3
 import TranslateSession from "./translate-session"
3
 import TranslateSession from "./translate-session"
4
 import TransformSession from "./transform-session"
4
 import TransformSession from "./transform-session"
5
+import TransformSingleSession from "./transform-single-session"
5
 import DirectionSession from "./direction-session"
6
 import DirectionSession from "./direction-session"
6
 import RotateSession from "./rotate-session"
7
 import RotateSession from "./rotate-session"
7
 
8
 
10
   BaseSession,
11
   BaseSession,
11
   TranslateSession,
12
   TranslateSession,
12
   TransformSession,
13
   TransformSession,
14
+  TransformSingleSession,
13
   DirectionSession,
15
   DirectionSession,
14
   RotateSession,
16
   RotateSession,
15
 }
17
 }

+ 17
- 6
state/sessions/transform-session.ts 查看文件

66
       initialBounds,
66
       initialBounds,
67
       currentPageId,
67
       currentPageId,
68
       selectedIds,
68
       selectedIds,
69
-      isSingle,
70
     } = this.snapshot
69
     } = this.snapshot
71
 
70
 
72
     const { shapes } = data.document.pages[currentPageId]
71
     const { shapes } = data.document.pages[currentPageId]
73
 
72
 
74
-    const delta = vec.vec(this.origin, point)
73
+    let delta = vec.vec(this.origin, point)
74
+
75
+    // if (isSingle) {
76
+    //   const center = [
77
+    //     initialBounds.minX + initialBounds.width / 2,
78
+    //     initialBounds.minY + initialBounds.height / 2,
79
+    //   ]
80
+
81
+    //   const rotation = shapes[Array.from(selectedIds.values())[0]].rotation
82
+
83
+    //   const rotatedOrigin = vec.rotWith(this.origin, center, -rotation)
84
+    //   const rotatedPoint = vec.rotWith(point, center, -rotation)
85
+
86
+    //   delta = vec.vec(rotatedOrigin, rotatedPoint)
87
+    // }
75
 
88
 
76
     /*
89
     /*
77
     Transforms
90
     Transforms
173
         boundsRotation,
186
         boundsRotation,
174
         isFlippedX: this.isFlippedX,
187
         isFlippedX: this.isFlippedX,
175
         isFlippedY: this.isFlippedY,
188
         isFlippedY: this.isFlippedY,
176
-        isSingle,
189
+        isSingle: false,
177
         anchor: getTransformAnchor(
190
         anchor: getTransformAnchor(
178
           this.transformType,
191
           this.transformType,
179
           this.isFlippedX,
192
           this.isFlippedX,
190
       initialBounds,
203
       initialBounds,
191
       currentPageId,
204
       currentPageId,
192
       selectedIds,
205
       selectedIds,
193
-      isSingle,
194
     } = this.snapshot
206
     } = this.snapshot
195
 
207
 
196
     const { shapes } = data.document.pages[currentPageId]
208
     const { shapes } = data.document.pages[currentPageId]
208
         boundsRotation,
220
         boundsRotation,
209
         isFlippedX: false,
221
         isFlippedX: false,
210
         isFlippedY: false,
222
         isFlippedY: false,
211
-        isSingle,
223
+        isSingle: false,
212
         anchor: getTransformAnchor(this.transformType, false, false),
224
         anchor: getTransformAnchor(this.transformType, false, false),
213
       })
225
       })
214
     })
226
     })
255
     type: transformType,
267
     type: transformType,
256
     initialBounds: bounds,
268
     initialBounds: bounds,
257
     boundsRotation,
269
     boundsRotation,
258
-    isSingle: selectedIds.size === 1,
259
     selectedIds: new Set(selectedIds),
270
     selectedIds: new Set(selectedIds),
260
     shapeBounds: Object.fromEntries(
271
     shapeBounds: Object.fromEntries(
261
       Array.from(selectedIds.values()).map((id) => {
272
       Array.from(selectedIds.values()).map((id) => {

+ 247
- 0
state/sessions/transform-single-session.ts 查看文件

1
+import { Data, TransformEdge, TransformCorner } from "types"
2
+import * as vec from "utils/vec"
3
+import BaseSession from "./base-session"
4
+import commands from "state/commands"
5
+import { current } from "immer"
6
+import { getShapeUtils } from "lib/shapes"
7
+import {
8
+  getTransformedBoundingBox,
9
+  getCommonBounds,
10
+  getRotatedCorners,
11
+  getTransformAnchor,
12
+} from "utils/utils"
13
+
14
+export default class TransformSingleSession extends BaseSession {
15
+  delta = [0, 0]
16
+  isFlippedX = false
17
+  isFlippedY = false
18
+  transformType: TransformEdge | TransformCorner
19
+  origin: number[]
20
+  center: number[]
21
+  snapshot: TransformSingleSnapshot
22
+  corners: {
23
+    a: number[]
24
+    b: number[]
25
+  }
26
+  rotatedCorners: number[][]
27
+
28
+  constructor(
29
+    data: Data,
30
+    transformType: TransformCorner | TransformEdge,
31
+    point: number[]
32
+  ) {
33
+    super(data)
34
+    this.origin = point
35
+    this.transformType = transformType
36
+
37
+    this.snapshot = getTransformSingleSnapshot(data, transformType)
38
+
39
+    const { minX, minY, maxX, maxY } = this.snapshot.initialShapeBounds
40
+
41
+    this.center = [(minX + maxX) / 2, (minY + maxY) / 2]
42
+
43
+    this.corners = {
44
+      a: [minX, minY],
45
+      b: [maxX, maxY],
46
+    }
47
+
48
+    this.rotatedCorners = getRotatedCorners(
49
+      this.snapshot.initialShapeBounds,
50
+      this.snapshot.initialShape.rotation
51
+    )
52
+  }
53
+
54
+  update(data: Data, point: number[]) {
55
+    const {
56
+      corners: { a, b },
57
+      transformType,
58
+    } = this
59
+
60
+    const {
61
+      boundsRotation,
62
+      initialShapeBounds,
63
+      currentPageId,
64
+      initialShape,
65
+      id,
66
+    } = this.snapshot
67
+
68
+    const { shapes } = data.document.pages[currentPageId]
69
+
70
+    const shape = shapes[id]
71
+    const rotation = shape.rotation
72
+
73
+    // 1. Create a new bounding box.
74
+    // Counter rotate the delta and apply this to the original bounding box.
75
+
76
+    const delta = vec.vec(this.origin, point)
77
+
78
+    /*
79
+    Transforms
80
+    
81
+    Corners a and b are the original top-left and bottom-right corners of the
82
+    bounding box. Depending on what the user is dragging, change one or both
83
+    points. To keep things smooth, calculate based by adding the delta (the 
84
+    vector between the current point and its original point) to the original
85
+    bounding box values.
86
+    */
87
+
88
+    const newBoundingBox = getTransformedBoundingBox(
89
+      initialShapeBounds,
90
+      transformType,
91
+      delta,
92
+      shape.rotation
93
+    )
94
+
95
+    // console.log(newBoundingBox)
96
+
97
+    switch (transformType) {
98
+      case TransformEdge.Top: {
99
+        a[1] = initialShapeBounds.minY + delta[1]
100
+        break
101
+      }
102
+      case TransformEdge.Right: {
103
+        b[0] = initialShapeBounds.maxX + delta[0]
104
+        break
105
+      }
106
+      case TransformEdge.Bottom: {
107
+        b[1] = initialShapeBounds.maxY + delta[1]
108
+        break
109
+      }
110
+      case TransformEdge.Left: {
111
+        a[0] = initialShapeBounds.minX + delta[0]
112
+        break
113
+      }
114
+      case TransformCorner.TopLeft: {
115
+        a[0] = initialShapeBounds.minX + delta[0]
116
+        a[1] = initialShapeBounds.minY + delta[1]
117
+        break
118
+      }
119
+      case TransformCorner.TopRight: {
120
+        a[1] = initialShapeBounds.minY + delta[1]
121
+        b[0] = initialShapeBounds.maxX + delta[0]
122
+        break
123
+      }
124
+      case TransformCorner.BottomRight: {
125
+        b[0] = initialShapeBounds.maxX + delta[0]
126
+        b[1] = initialShapeBounds.maxY + delta[1]
127
+        break
128
+      }
129
+      case TransformCorner.BottomLeft: {
130
+        a[0] = initialShapeBounds.minX + delta[0]
131
+        b[1] = initialShapeBounds.maxY + delta[1]
132
+        break
133
+      }
134
+    }
135
+
136
+    // Calculate new common (externior) bounding box
137
+    const newBounds = {
138
+      minX: Math.min(a[0], b[0]),
139
+      minY: Math.min(a[1], b[1]),
140
+      maxX: Math.max(a[0], b[0]),
141
+      maxY: Math.max(a[1], b[1]),
142
+      width: Math.abs(b[0] - a[0]),
143
+      height: Math.abs(b[1] - a[1]),
144
+    }
145
+
146
+    this.isFlippedX = b[0] < a[0]
147
+    this.isFlippedY = b[1] < a[1]
148
+
149
+    const anchor = this.transformType
150
+
151
+    // Pass the new data to the shape's transform utility for mutation.
152
+    // Most shapes should be able to transform using only the bounding box,
153
+    // however some shapes (e.g. those with internal points) will need more
154
+    // data here too.
155
+
156
+    getShapeUtils(shape).transformSingle(shape, newBoundingBox, {
157
+      type: this.transformType,
158
+      initialShape,
159
+      initialShapeBounds,
160
+      initialBounds: initialShapeBounds,
161
+      boundsRotation,
162
+      isFlippedX: this.isFlippedX,
163
+      isFlippedY: this.isFlippedY,
164
+      isSingle: true,
165
+      anchor,
166
+    })
167
+  }
168
+
169
+  cancel(data: Data) {
170
+    const {
171
+      id,
172
+      boundsRotation,
173
+      initialShape,
174
+      initialShapeBounds,
175
+      currentPageId,
176
+      isSingle,
177
+    } = this.snapshot
178
+
179
+    const { shapes } = data.document.pages[currentPageId]
180
+
181
+    // selectedIds.forEach((id) => {
182
+    //   const shape = shapes[id]
183
+
184
+    //   const { initialShape, initialShapeBounds } = shapeBounds[id]
185
+
186
+    //   getShapeUtils(shape).transform(shape, initialShapeBounds, {
187
+    //     type: this.transformType,
188
+    //     initialShape,
189
+    //     initialShapeBounds,
190
+    //     initialBounds,
191
+    //     boundsRotation,
192
+    //     isFlippedX: false,
193
+    //     isFlippedY: false,
194
+    //     isSingle,
195
+    //     anchor: getTransformAnchor(this.transformType, false, false),
196
+    //   })
197
+    // })
198
+  }
199
+
200
+  complete(data: Data) {
201
+    commands.transformSingle(
202
+      data,
203
+      this.snapshot,
204
+      getTransformSingleSnapshot(data, this.transformType),
205
+      getTransformAnchor(this.transformType, false, false)
206
+    )
207
+  }
208
+}
209
+
210
+export function getTransformSingleSnapshot(
211
+  data: Data,
212
+  transformType: TransformEdge | TransformCorner
213
+) {
214
+  const {
215
+    document: { pages },
216
+    selectedIds,
217
+    currentPageId,
218
+  } = current(data)
219
+
220
+  const pageShapes = pages[currentPageId].shapes
221
+
222
+  const id = Array.from(selectedIds)[0]
223
+  const shape = pageShapes[id]
224
+  const bounds = getShapeUtils(shape).getBounds(shape)
225
+
226
+  return {
227
+    id,
228
+    currentPageId,
229
+    type: transformType,
230
+    initialShape: shape,
231
+    initialShapeBounds: {
232
+      ...bounds,
233
+      nx: 0,
234
+      ny: 0,
235
+      nmx: 1,
236
+      nmy: 1,
237
+      nw: 1,
238
+      nh: 1,
239
+    },
240
+    boundsRotation: shape.rotation,
241
+    isSingle: true,
242
+  }
243
+}
244
+
245
+export type TransformSingleSnapshot = ReturnType<
246
+  typeof getTransformSingleSnapshot
247
+>

+ 12
- 5
state/state.ts 查看文件

527
       data,
527
       data,
528
       payload: PointerInfo & { target: TransformCorner | TransformEdge }
528
       payload: PointerInfo & { target: TransformCorner | TransformEdge }
529
     ) {
529
     ) {
530
-      session = new Sessions.TransformSession(
531
-        data,
532
-        payload.target,
533
-        screenToWorld(payload.point, data)
534
-      )
530
+      session =
531
+        data.selectedIds.size === 1
532
+          ? new Sessions.TransformSingleSession(
533
+              data,
534
+              payload.target,
535
+              screenToWorld(payload.point, data)
536
+            )
537
+          : new Sessions.TransformSession(
538
+              data,
539
+              payload.target,
540
+              screenToWorld(payload.point, data)
541
+            )
535
     },
542
     },
536
     startDrawTransformSession(data, payload: PointerInfo) {
543
     startDrawTransformSession(data, payload: PointerInfo) {
537
       session = new Sessions.TransformSession(
544
       session = new Sessions.TransformSession(

+ 151
- 1
utils/utils.ts 查看文件

1
 import Vector from "lib/code/vector"
1
 import Vector from "lib/code/vector"
2
+import { getShapeUtils } from "lib/shapes"
2
 import React from "react"
3
 import React from "react"
3
-import { Data, Bounds, TransformEdge, TransformCorner } from "types"
4
+import { Data, Bounds, TransformEdge, TransformCorner, Shape } from "types"
4
 import * as svg from "./svg"
5
 import * as svg from "./svg"
5
 import * as vec from "./vec"
6
 import * as vec from "./vec"
6
 
7
 
1020
     height: bounds.height,
1021
     height: bounds.height,
1021
   }
1022
   }
1022
 }
1023
 }
1024
+
1025
+export function getRotatedCorners(b: Bounds, rotation: number) {
1026
+  const center = [b.minX + b.width / 2, b.minY + b.height / 2]
1027
+
1028
+  return [
1029
+    [b.minX, b.minY],
1030
+    [b.maxX, b.minY],
1031
+    [b.maxX, b.maxY],
1032
+    [b.minX, b.maxY],
1033
+  ].map((point) => vec.rotWith(point, center, rotation))
1034
+}
1035
+
1036
+export function getTransformedBoundingBox(
1037
+  bounds: Bounds,
1038
+  handle: TransformCorner | TransformEdge,
1039
+  delta: number[],
1040
+  rotation = 0
1041
+) {
1042
+  // Create top left and bottom right corners.
1043
+  let [ax0, ay0] = [bounds.minX, bounds.minY]
1044
+  let [ax1, ay1] = [bounds.maxX, bounds.maxY]
1045
+
1046
+  // Create a second set of corners for the result.
1047
+  let [bx0, by0] = [bounds.minX, bounds.minY]
1048
+  let [bx1, by1] = [bounds.maxX, bounds.maxY]
1049
+
1050
+  // Counter rotate the delta. This lets us make changes as if
1051
+  // the (possibly rotated) boxes were axis aligned.
1052
+  const [dx, dy] = vec.rot(delta, -rotation)
1053
+
1054
+  // Depending on the dragging handle (an edge or corner of
1055
+  // the bounding box), find the anchor corner and use the delta
1056
+  // to adjust the result's corners.
1057
+
1058
+  let anchor: TransformCorner | TransformEdge
1059
+
1060
+  switch (handle) {
1061
+    case TransformEdge.Top: {
1062
+      anchor = TransformCorner.BottomRight
1063
+      by0 += dy
1064
+      break
1065
+    }
1066
+    case TransformEdge.Right: {
1067
+      anchor = TransformCorner.TopLeft
1068
+      bx1 += dx
1069
+      break
1070
+    }
1071
+    case TransformEdge.Bottom: {
1072
+      anchor = TransformCorner.TopLeft
1073
+      by1 += dy
1074
+      break
1075
+    }
1076
+    case TransformEdge.Left: {
1077
+      anchor = TransformCorner.BottomRight
1078
+      bx0 += dx
1079
+      break
1080
+    }
1081
+    case TransformCorner.TopLeft: {
1082
+      anchor = TransformCorner.BottomRight
1083
+      bx0 += dx
1084
+      by0 += dy
1085
+      break
1086
+    }
1087
+    case TransformCorner.TopRight: {
1088
+      anchor = TransformCorner.BottomLeft
1089
+      bx1 += dx
1090
+      by0 += dy
1091
+      break
1092
+    }
1093
+    case TransformCorner.BottomRight: {
1094
+      anchor = TransformCorner.TopLeft
1095
+      bx1 += dx
1096
+      by1 += dy
1097
+      break
1098
+    }
1099
+    case TransformCorner.BottomLeft: {
1100
+      anchor = TransformCorner.TopRight
1101
+      bx0 += dx
1102
+      by1 += dy
1103
+      break
1104
+    }
1105
+  }
1106
+
1107
+  // If the bounds are rotated, get a vector from the rotated anchor
1108
+  // corner in the inital bounds to the rotated anchor corner in the
1109
+  // result's bounds. Subtract this vector from the result's corners,
1110
+  // so that the two anchor points (initial and result) will be equal.
1111
+
1112
+  if (rotation % (Math.PI * 2) !== 0) {
1113
+    let cv = [0, 0]
1114
+
1115
+    const c0 = vec.med([ax0, ay0], [ax1, ay1])
1116
+    const c1 = vec.med([bx0, by0], [bx1, by1])
1117
+
1118
+    switch (anchor) {
1119
+      case TransformCorner.TopLeft: {
1120
+        cv = vec.sub(
1121
+          vec.rotWith([bx0, by0], c1, rotation),
1122
+          vec.rotWith([ax0, ay0], c0, rotation)
1123
+        )
1124
+        break
1125
+      }
1126
+      case TransformCorner.TopRight: {
1127
+        cv = vec.sub(
1128
+          vec.rotWith([bx1, by0], c1, rotation),
1129
+          vec.rotWith([ax1, ay0], c0, rotation)
1130
+        )
1131
+        break
1132
+      }
1133
+      case TransformCorner.BottomRight: {
1134
+        cv = vec.sub(
1135
+          vec.rotWith([bx1, by1], c1, rotation),
1136
+          vec.rotWith([ax1, ay1], c0, rotation)
1137
+        )
1138
+        break
1139
+      }
1140
+      case TransformCorner.BottomLeft: {
1141
+        cv = vec.sub(
1142
+          vec.rotWith([bx0, by1], c1, rotation),
1143
+          vec.rotWith([ax0, ay1], c0, rotation)
1144
+        )
1145
+        break
1146
+      }
1147
+    }
1148
+
1149
+    ;[bx0, by0] = vec.sub([bx0, by0], cv)
1150
+    ;[bx1, by1] = vec.sub([bx1, by1], cv)
1151
+  }
1152
+
1153
+  // If the axes are flipped (e.g. if the right edge has been dragged
1154
+  // left past the initial left edge) then swap points on that axis.
1155
+
1156
+  if (bx1 < bx0) {
1157
+    ;[bx1, bx0] = [bx0, bx1]
1158
+  }
1159
+
1160
+  if (by1 < by0) {
1161
+    ;[by1, by0] = [by0, by1]
1162
+  }
1163
+
1164
+  return {
1165
+    minX: bx0,
1166
+    minY: by0,
1167
+    maxX: bx1,
1168
+    maxY: by1,
1169
+    width: bx1 - bx0,
1170
+    height: by1 - by0,
1171
+  }
1172
+}

正在加载...
取消
保存