Browse Source

improves arrow rotation

main
Steve Ruiz 4 years ago
parent
commit
81141e7bb5

+ 1
- 10
components/canvas/bounds/handles.tsx View File

@@ -30,7 +30,6 @@ export default function Handles() {
30 30
       {Object.values(shape.handles).map((handle) => (
31 31
         <Handle
32 32
           key={handle.id}
33
-          shapeId={shape.id}
34 33
           id={handle.id}
35 34
           point={vec.add(handle.point, shape.point)}
36 35
         />
@@ -39,15 +38,7 @@ export default function Handles() {
39 38
   )
40 39
 }
41 40
 
42
-function Handle({
43
-  shapeId,
44
-  id,
45
-  point,
46
-}: {
47
-  shapeId: string
48
-  id: string
49
-  point: number[]
50
-}) {
41
+function Handle({ id, point }: { id: string; point: number[] }) {
51 42
   const rGroup = useRef<SVGGElement>(null)
52 43
   const events = useHandleEvents(id, rGroup)
53 44
 

+ 97
- 18
lib/shape-utils/arrow.tsx View File

@@ -3,6 +3,7 @@ import * as vec from 'utils/vec'
3 3
 import * as svg from 'utils/svg'
4 4
 import {
5 5
   ArrowShape,
6
+  Bounds,
6 7
   ColorStyle,
7 8
   DashStyle,
8 9
   ShapeHandle,
@@ -10,7 +11,13 @@ import {
10 11
   SizeStyle,
11 12
 } from 'types'
12 13
 import { registerShapeUtils } from './index'
13
-import { circleFromThreePoints, clamp, isAngleBetween } from 'utils/utils'
14
+import {
15
+  circleFromThreePoints,
16
+  clamp,
17
+  getBoundsCenter,
18
+  isAngleBetween,
19
+  rotateBounds,
20
+} from 'utils/utils'
14 21
 import { pointInBounds } from 'utils/bounds'
15 22
 import {
16 23
   intersectArcBounds,
@@ -170,25 +177,27 @@ const arrow = registerShapeUtils<ArrowShape>({
170 177
     )
171 178
   },
172 179
 
173
-  rotateTo(shape, rotation, delta) {
180
+  rotateBy(shape, delta) {
174 181
     const { start, end, bend } = shape.handles
175
-    // const mp = vec.med(start.point, end.point)
176
-    // start.point = vec.rotWith(start.point, mp, delta)
177
-    // end.point = vec.rotWith(end.point, mp, delta)
178
-    // bend.point = vec.rotWith(bend.point, mp, delta)
179
-    // this.onHandleChange(shape, shape.handles)
182
+    const mp = vec.med(start.point, end.point)
183
+    start.point = vec.rotWith(start.point, mp, delta)
184
+    end.point = vec.rotWith(end.point, mp, delta)
185
+    bend.point = vec.rotWith(bend.point, mp, delta)
180 186
 
181
-    // const bounds = this.getBounds(shape)
187
+    this.onHandleChange(shape, shape.handles)
182 188
 
183
-    // const offset = vec.sub([bounds.minX, bounds.minY], shape.point)
189
+    return this
190
+  },
184 191
 
185
-    // this.translateTo(shape, vec.add(shape.point, offset))
192
+  rotateTo(shape, rotation, delta) {
193
+    const { start, end, bend } = shape.handles
194
+    const mp = vec.med(start.point, end.point)
195
+    start.point = vec.rotWith(start.point, mp, delta)
196
+    end.point = vec.rotWith(end.point, mp, delta)
197
+    bend.point = vec.rotWith(bend.point, mp, delta)
186 198
 
187
-    // start.point = vec.sub(start.point, offset)
188
-    // end.point = vec.sub(end.point, offset)
189
-    // bend.point = vec.sub(bend.point, offset)
199
+    this.onHandleChange(shape, shape.handles)
190 200
 
191
-    shape.rotation = rotation
192 201
     return this
193 202
   },
194 203
 
@@ -202,11 +211,16 @@ const arrow = registerShapeUtils<ArrowShape>({
202 211
   },
203 212
 
204 213
   getRotatedBounds(shape) {
205
-    if (!this.boundsCache.has(shape)) {
206
-      this.boundsCache.set(shape, getBoundsFromPoints(shape.points))
207
-    }
214
+    const { start, end } = shape.handles
215
+    return translateBounds(
216
+      getBoundsFromPoints([start.point, end.point], shape.rotation),
217
+      shape.point
218
+    )
219
+  },
208 220
 
209
-    return translateBounds(this.boundsCache.get(shape), shape.point)
221
+  getCenter(shape) {
222
+    const { start, end } = shape.handles
223
+    return vec.add(shape.point, vec.med(start.point, end.point))
210 224
   },
211 225
 
212 226
   hitTest(shape, point) {
@@ -281,6 +295,9 @@ const arrow = registerShapeUtils<ArrowShape>({
281 295
   },
282 296
 
283 297
   onHandleChange(shape, handles) {
298
+    // const oldBounds = this.getRotatedBounds(shape)
299
+    // const prevCenter = getBoundsCenter(oldBounds)
300
+
284 301
     for (let id in handles) {
285 302
       const handle = handles[id]
286 303
 
@@ -313,6 +330,27 @@ const arrow = registerShapeUtils<ArrowShape>({
313 330
 
314 331
     shape.handles.bend.point = getBendPoint(shape)
315 332
 
333
+    // const newBounds = this.getRotatedBounds(shape)
334
+    // const newCenter = getBoundsCenter(newBounds)
335
+
336
+    // shape.point = vec.add(shape.point, vec.neg(vec.sub(newCenter, prevCenter)))
337
+
338
+    return this
339
+  },
340
+
341
+  onSessionComplete(shape) {
342
+    const bounds = this.getBounds(shape)
343
+
344
+    const offset = vec.sub([bounds.minX, bounds.minY], shape.point)
345
+
346
+    this.translateTo(shape, vec.add(shape.point, offset))
347
+
348
+    const { start, end, bend } = shape.handles
349
+
350
+    start.point = vec.sub(start.point, offset)
351
+    end.point = vec.sub(end.point, offset)
352
+    bend.point = vec.sub(bend.point, offset)
353
+
316 354
     return this
317 355
   },
318 356
 
@@ -360,3 +398,44 @@ function getBendPoint(shape: ArrowShape) {
360 398
     ? midPoint
361 399
     : vec.add(midPoint, vec.mul(vec.per(u), bendDist))
362 400
 }
401
+
402
+function getResizeOffset(a: Bounds, b: Bounds) {
403
+  const { minX: x0, minY: y0, width: w0, height: h0 } = a
404
+  const { minX: x1, minY: y1, width: w1, height: h1 } = b
405
+
406
+  let delta: number[]
407
+
408
+  if (h0 === h1 && w0 !== w1) {
409
+    if (x0 !== x1) {
410
+      // moving left edge, pin right edge
411
+      delta = vec.sub([x1, y1 + h1 / 2], [x0, y0 + h0 / 2])
412
+    } else {
413
+      // moving right edge, pin left edge
414
+      delta = vec.sub([x1 + w1, y1 + h1 / 2], [x0 + w0, y0 + h0 / 2])
415
+    }
416
+  } else if (h0 !== h1 && w0 === w1) {
417
+    if (y0 !== y1) {
418
+      // moving top edge, pin bottom edge
419
+      delta = vec.sub([x1 + w1 / 2, y1], [x0 + w0 / 2, y0])
420
+    } else {
421
+      // moving bottom edge, pin top edge
422
+      delta = vec.sub([x1 + w1 / 2, y1 + h1], [x0 + w0 / 2, y0 + h0])
423
+    }
424
+  } else if (x0 !== x1) {
425
+    if (y0 !== y1) {
426
+      // moving top left, pin bottom right
427
+      delta = vec.sub([x1, y1], [x0, y0])
428
+    } else {
429
+      // moving bottom left, pin top right
430
+      delta = vec.sub([x1, y1 + h1], [x0, y0 + h0])
431
+    }
432
+  } else if (y0 !== y1) {
433
+    // moving top right, pin bottom left
434
+    delta = vec.sub([x1 + w1, y1], [x0 + w0, y0])
435
+  } else {
436
+    // moving bottom right, pin top left
437
+    delta = vec.sub([x1 + w1, y1 + h1], [x0 + w0, y0 + h0])
438
+  }
439
+
440
+  return delta
441
+}

+ 7
- 0
lib/shape-utils/index.tsx View File

@@ -147,6 +147,9 @@ export interface ShapeUtility<K extends Shape> {
147 147
     handle: Partial<K['handles']>
148 148
   ): ShapeUtility<K>
149 149
 
150
+  // Clean up changes when a session ends.
151
+  onSessionComplete(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
152
+
150 153
   // Render a shape to JSX.
151 154
   render(this: ShapeUtility<K>, shape: K): JSX.Element
152 155
 
@@ -258,6 +261,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
258 261
       return this
259 262
     },
260 263
 
264
+    onSessionComplete() {
265
+      return this
266
+    },
267
+
261 268
     getBounds(shape) {
262 269
       const [x, y] = shape.point
263 270
       return {

+ 12
- 11
state/commands/handle.ts View File

@@ -24,26 +24,27 @@ export default function handleCommand(
24 24
         const page = getPage(data, currentPageId)
25 25
         const shape = page.shapes[initialShape.id]
26 26
 
27
-        getShapeUtils(shape).onHandleChange(shape, initialShape.handles)
27
+        getShapeUtils(shape)
28
+          .onHandleChange(shape, initialShape.handles)
29
+          .onSessionComplete(shape)
28 30
 
29
-        const bounds = getShapeUtils(shape).getBounds(shape)
31
+        // const bounds = getShapeUtils(shape).getBounds(shape)
30 32
 
31
-        const offset = vec.sub([bounds.minX, bounds.minY], shape.point)
33
+        // const offset = vec.sub([bounds.minX, bounds.minY], shape.point)
32 34
 
33
-        getShapeUtils(shape).translateTo(shape, vec.add(shape.point, offset))
35
+        // getShapeUtils(shape).translateTo(shape, vec.add(shape.point, offset))
34 36
 
35
-        const { start, end, bend } = page.shapes[initialShape.id].handles
37
+        // const { start, end, bend } = page.shapes[initialShape.id].handles
36 38
 
37
-        start.point = vec.sub(start.point, offset)
38
-        end.point = vec.sub(end.point, offset)
39
-        bend.point = vec.sub(bend.point, offset)
39
+        // start.point = vec.sub(start.point, offset)
40
+        // end.point = vec.sub(end.point, offset)
41
+        // bend.point = vec.sub(bend.point, offset)
40 42
       },
41 43
       undo(data) {
42 44
         const { initialShape, currentPageId } = before
43 45
 
44
-        const shape = getPage(data, currentPageId).shapes[initialShape.id]
45
-
46
-        getShapeUtils(shape).onHandleChange(shape, initialShape.handles)
46
+        const page = getPage(data, currentPageId)
47
+        page.shapes[initialShape.id] = initialShape
47 48
       },
48 49
     })
49 50
   )

+ 6
- 6
state/commands/rotate.ts View File

@@ -20,10 +20,10 @@ export default function rotateCommand(
20 20
 
21 21
         for (let { id, point, rotation } of after.initialShapes) {
22 22
           const shape = shapes[id]
23
-          const utils = getShapeUtils(shape)
24
-          utils
25
-            .rotateTo(shape, rotation, rotation - shape.rotation)
23
+          getShapeUtils(shape)
24
+            .rotateBy(shape, rotation - shape.rotation)
26 25
             .translateTo(shape, point)
26
+            .onSessionComplete(shape)
27 27
         }
28 28
 
29 29
         data.boundsRotation = after.boundsRotation
@@ -33,10 +33,10 @@ export default function rotateCommand(
33 33
 
34 34
         for (let { id, point, rotation } of before.initialShapes) {
35 35
           const shape = shapes[id]
36
-          const utils = getShapeUtils(shape)
37
-          utils
38
-            .rotateTo(shape, rotation, rotation - shape.rotation)
36
+          getShapeUtils(shape)
37
+            .rotateBy(shape, rotation - shape.rotation)
39 38
             .translateTo(shape, point)
39
+            .onSessionComplete(shape)
40 40
         }
41 41
 
42 42
         data.boundsRotation = before.boundsRotation

+ 25
- 25
state/data.ts View File

@@ -115,31 +115,31 @@ export const defaultDocument: Data['document'] = {
115 115
         //   },
116 116
         // }),
117 117
         // Groups Testing
118
-        shapeA: shapeUtils[ShapeType.Rectangle].create({
119
-          id: 'shapeA',
120
-          name: 'Shape A',
121
-          childIndex: 1,
122
-          point: [0, 0],
123
-          size: [200, 200],
124
-          parentId: 'groupA',
125
-        }),
126
-        shapeB: shapeUtils[ShapeType.Rectangle].create({
127
-          id: 'shapeB',
128
-          name: 'Shape B',
129
-          childIndex: 2,
130
-          point: [220, 100],
131
-          size: [200, 200],
132
-          parentId: 'groupA',
133
-        }),
134
-        groupA: shapeUtils[ShapeType.Group].create({
135
-          id: 'groupA',
136
-          name: 'Group A',
137
-          childIndex: 2,
138
-          point: [0, 0],
139
-          size: [420, 300],
140
-          parentId: 'page1',
141
-          children: ['shapeA', 'shapeB'],
142
-        }),
118
+        // shapeA: shapeUtils[ShapeType.Rectangle].create({
119
+        //   id: 'shapeA',
120
+        //   name: 'Shape A',
121
+        //   childIndex: 1,
122
+        //   point: [0, 0],
123
+        //   size: [200, 200],
124
+        //   parentId: 'groupA',
125
+        // }),
126
+        // shapeB: shapeUtils[ShapeType.Rectangle].create({
127
+        //   id: 'shapeB',
128
+        //   name: 'Shape B',
129
+        //   childIndex: 2,
130
+        //   point: [220, 100],
131
+        //   size: [200, 200],
132
+        //   parentId: 'groupA',
133
+        // }),
134
+        // groupA: shapeUtils[ShapeType.Group].create({
135
+        //   id: 'groupA',
136
+        //   name: 'Group A',
137
+        //   childIndex: 2,
138
+        //   point: [0, 0],
139
+        //   size: [420, 300],
140
+        //   parentId: 'page1',
141
+        //   children: ['shapeA', 'shapeB'],
142
+        // }),
143 143
       },
144 144
     },
145 145
     page2: {

+ 6
- 2
state/sessions/handle-session.ts View File

@@ -33,17 +33,21 @@ export default class HandleSession extends BaseSession {
33 33
 
34 34
     const handles = initialShape.handles
35 35
 
36
+    // rotate the delta ?
37
+    // rotate the handle ?
38
+    // rotate the shape around the previous center point
39
+
36 40
     getShapeUtils(shape).onHandleChange(shape, {
37 41
       [handleId]: {
38 42
         ...handles[handleId],
39
-        point: vec.add(handles[handleId].point, delta),
43
+        point: vec.add(handles[handleId].point, delta), // vec.rot(delta, shape.rotation)),
40 44
       },
41 45
     })
42 46
   }
43 47
 
44 48
   cancel(data: Data) {
45 49
     const { currentPageId, handleId, initialShape } = this.snapshot
46
-    const shape = getPage(data, currentPageId).shapes[initialShape.id]
50
+    getPage(data, currentPageId).shapes[initialShape.id] = initialShape
47 51
   }
48 52
 
49 53
   complete(data: Data) {

+ 10
- 3
state/state.ts View File

@@ -925,7 +925,7 @@ const state = createState({
925 925
       payload: PointerInfo & { target: Corner | Edge }
926 926
     ) {
927 927
       const point = screenToWorld(inputs.pointer.origin, data)
928
-      // session = new Sessions.TransformSession(data, payload.target, point)
928
+      session = new Sessions.TransformSession(data, payload.target, point)
929 929
       session =
930 930
         data.selectedIds.size === 1
931 931
           ? new Sessions.TransformSingleSession(data, payload.target, point)
@@ -1442,9 +1442,16 @@ const state = createState({
1442 1442
         return bounds
1443 1443
       }
1444 1444
 
1445
+      const uniqueSelectedShapeIds: string[] = Array.from(
1446
+        new Set(
1447
+          Array.from(selectedIds.values()).flatMap((id) =>
1448
+            getDocumentBranch(data, id)
1449
+          )
1450
+        ).values()
1451
+      )
1452
+
1445 1453
       const commonBounds = getCommonBounds(
1446
-        ...shapes
1447
-          .flatMap((shape) => getDocumentBranch(data, shape.id))
1454
+        ...uniqueSelectedShapeIds
1448 1455
           .map((id) => page.shapes[id])
1449 1456
           .filter((shape) => shape.type !== ShapeType.Group)
1450 1457
           .map((shape) => {

+ 13
- 9
todo.md View File

@@ -1,17 +1,21 @@
1 1
 # Todo
2 2
 
3
-## Done
3
+## Groups
4 4
 
5
-- fix select indicator placement for arrow
5
+- fix drift when moving children of rotated group
6 6
 
7
-## Todo
7
+## Select
8 8
 
9 9
 - Restore select highlight, fix for children of rotated groups
10
-- Transforming on rotated shapes
11
-- Fix bounding box for rotated shapes
12
-- Allow single-selected groups to transform their children correctly
10
+
11
+# Transforms
12
+
13 13
 - (merge transform-session and transform-single-session)
14
-- fix drift when moving children of rotated group
15
-- shift dragging arrow handles should lock to directions
16
-- arrow rotation with handles
14
+- Allow single-selected groups to transform their children correctly
17 15
 - fix ellipse when scaleX < 0 or scaleY < 0
16
+- Transforming on rotated shapes
17
+
18
+## Arrows
19
+
20
+- shift dragging arrow handles should lock to directions
21
+- fix undo/redo on rotated arrows

Loading…
Cancel
Save