ソースを参照

Improves handles for arrows

main
Steve Ruiz 4年前
コミット
72b6db12c4

+ 28
- 13
components/canvas/bounds/handles.tsx ファイルの表示

@@ -5,7 +5,6 @@ import { useSelector } from 'state'
5 5
 import styled from 'styles'
6 6
 import { deepCompareArrays, getPage } from 'utils/utils'
7 7
 import * as vec from 'utils/vec'
8
-import { DotCircle } from '../misc'
9 8
 
10 9
 export default function Handles() {
11 10
   const selectedIds = useSelector(
@@ -18,7 +17,9 @@ export default function Handles() {
18 17
       selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]]
19 18
   )
20 19
 
21
-  const isSelecting = useSelector((s) => s.isIn('selecting.notPointing'))
20
+  const isSelecting = useSelector((s) =>
21
+    s.isInAny('notPointing', 'pinching', 'translatingHandles')
22
+  )
22 23
 
23 24
   if (!shape.handles || !isSelecting) return null
24 25
 
@@ -49,29 +50,43 @@ function Handle({
49 50
   const events = useHandleEvents(id, rGroup)
50 51
 
51 52
   return (
52
-    <g
53
+    <StyledGroup
53 54
       key={id}
55
+      className="handles"
54 56
       ref={rGroup}
55 57
       {...events}
56
-      cursor="pointer"
57 58
       pointerEvents="all"
58 59
       transform={`translate(${point})`}
59 60
     >
60 61
       <HandleCircleOuter r={12} />
61
-      <DotCircle r={4} />
62
-    </g>
62
+      <use href="#handle" pointerEvents="none" />
63
+    </StyledGroup>
63 64
   )
64 65
 }
65 66
 
67
+const StyledGroup = styled('g', {
68
+  '&:hover': {
69
+    cursor: 'pointer',
70
+  },
71
+  '&:active': {
72
+    cursor: 'none',
73
+  },
74
+})
75
+
66 76
 const HandleCircleOuter = styled('circle', {
67 77
   fill: 'transparent',
78
+  stroke: 'none',
79
+  opacity: 0.2,
68 80
   pointerEvents: 'all',
69 81
   cursor: 'pointer',
70
-})
71
-
72
-const HandleCircle = styled('circle', {
73
-  zStrokeWidth: 2,
74
-  stroke: '$text',
75
-  fill: '$panel',
76
-  pointerEvents: 'none',
82
+  transform: 'scale(var(--scale))',
83
+  '&:hover': {
84
+    fill: '$selected',
85
+    '& > *': {
86
+      stroke: '$selected',
87
+    },
88
+  },
89
+  '&:active': {
90
+    fill: '$selected',
91
+  },
77 92
 })

+ 1
- 1
components/canvas/canvas.tsx ファイルの表示

@@ -62,7 +62,7 @@ export default function Canvas() {
62 62
         <g ref={rGroup}>
63 63
           <BoundsBg />
64 64
           <Page />
65
-          {/* <Selected /> */}
65
+          <Selected />
66 66
           <Bounds />
67 67
           <Handles />
68 68
           <Brush />

+ 3
- 0
components/canvas/defs.tsx ファイルの表示

@@ -2,6 +2,7 @@ import { getShapeUtils } from 'lib/shape-utils'
2 2
 import { memo } from 'react'
3 3
 import { useSelector } from 'state'
4 4
 import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
5
+import { DotCircle, Handle } from './misc'
5 6
 
6 7
 export default function Defs() {
7 8
   const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
@@ -17,6 +18,8 @@ export default function Defs() {
17 18
       {currentPageShapeIds.map((id) => (
18 19
         <Def key={id} id={id} />
19 20
       ))}
21
+      <DotCircle id="dot" r={4} />
22
+      <Handle id="handle" r={4} />
20 23
       <filter id="expand">
21 24
         <feMorphology operator="dilate" radius={2 / zoom} />
22 25
       </filter>

+ 7
- 0
components/canvas/misc.tsx ファイルの表示

@@ -7,6 +7,13 @@ export const DotCircle = styled('circle', {
7 7
   strokeWidth: '2',
8 8
 })
9 9
 
10
+export const Handle = styled('circle', {
11
+  transform: 'scale(var(--scale))',
12
+  fill: '$canvas',
13
+  stroke: '$selected',
14
+  strokeWidth: '2',
15
+})
16
+
10 17
 export const ThinLine = styled('line', {
11 18
   zStrokeWidth: 1,
12 19
 })

+ 1
- 2
components/canvas/selected.tsx ファイルの表示

@@ -12,7 +12,7 @@ export default function Selected() {
12 12
     return Array.from(data.selectedIds.values())
13 13
   }, deepCompareArrays)
14 14
 
15
-  const isSelecting = useSelector((s) => s.isIn('selecting'))
15
+  const isSelecting = useSelector((s) => s.isInAny('notPointing', 'pinching'))
16 16
 
17 17
   if (!isSelecting) return null
18 18
 
@@ -44,7 +44,6 @@ export const ShapeOutline = memo(function ShapeOutline({ id }: { id: string }) {
44 44
   rotate(${shape.rotation * (180 / Math.PI)}, 
45 45
   ${center})
46 46
   translate(${bounds.minX},${bounds.minY})
47
-  rotate(${(bounds.rotation || 0) * (180 / Math.PI)}, 0, 0)
48 47
   `
49 48
 
50 49
   return (

+ 14
- 4
lib/shape-utils/arrow.tsx ファイルの表示

@@ -93,10 +93,11 @@ const arrow = registerShapeUtils<ArrowShape>({
93 93
   },
94 94
 
95 95
   render(shape) {
96
-    const { id, bend, points, handles } = shape
96
+    const { id, bend, handles } = shape
97 97
     const { start, end, bend: _bend } = handles
98 98
 
99 99
     const arrowDist = vec.dist(start.point, end.point)
100
+
100 101
     const showCircle = !vec.isEqual(
101 102
       _bend.point,
102 103
       vec.med(start.point, end.point)
@@ -145,8 +146,8 @@ const arrow = registerShapeUtils<ArrowShape>({
145 146
     const length = Math.min(arrowDist / 2, 16 + +style.strokeWidth * 2)
146 147
     const u = vec.uni(vec.vec(start.point, end.point))
147 148
     const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
148
-    const b = vec.add(points[1], vec.rot(v, Math.PI / 6))
149
-    const c = vec.add(points[1], vec.rot(v, -(Math.PI / 6)))
149
+    const b = vec.add(end.point, vec.rot(v, Math.PI / 6))
150
+    const c = vec.add(end.point, vec.rot(v, -(Math.PI / 6)))
150 151
 
151 152
     return (
152 153
       <g id={id}>
@@ -159,7 +160,7 @@ const arrow = registerShapeUtils<ArrowShape>({
159 160
           strokeDasharray="none"
160 161
         />
161 162
         <polyline
162
-          points={[b, points[1], c].join()}
163
+          points={[b, end.point, c].join()}
163 164
           strokeLinecap="round"
164 165
           strokeLinejoin="round"
165 166
           fill="none"
@@ -170,6 +171,15 @@ const arrow = registerShapeUtils<ArrowShape>({
170 171
   },
171 172
 
172 173
   getBounds(shape) {
174
+    if (!this.boundsCache.has(shape)) {
175
+      const { start, end } = shape.handles
176
+      this.boundsCache.set(shape, getBoundsFromPoints([start.point, end.point]))
177
+    }
178
+
179
+    return translateBounds(this.boundsCache.get(shape), shape.point)
180
+  },
181
+
182
+  getRotatedBounds(shape) {
173 183
     if (!this.boundsCache.has(shape)) {
174 184
       this.boundsCache.set(shape, getBoundsFromPoints(shape.points))
175 185
     }

+ 1
- 2
lib/shape-utils/dot.tsx ファイルの表示

@@ -4,7 +4,6 @@ import { DotShape, ShapeType } from 'types'
4 4
 import { registerShapeUtils } from './index'
5 5
 import { boundsContained } from 'utils/bounds'
6 6
 import { intersectCircleBounds } from 'utils/intersections'
7
-import { DotCircle } from 'components/canvas/misc'
8 7
 import { translateBounds } from 'utils/utils'
9 8
 import { defaultStyle } from 'lib/shape-styles'
10 9
 
@@ -34,7 +33,7 @@ const dot = registerShapeUtils<DotShape>({
34 33
   },
35 34
 
36 35
   render({ id }) {
37
-    return <DotCircle id={id} cx={0} cy={0} r={3} />
36
+    return <use href="#dot" />
38 37
   },
39 38
 
40 39
   getBounds(shape) {

+ 2
- 2
lib/shape-utils/line.tsx ファイルの表示

@@ -4,7 +4,7 @@ import { LineShape, ShapeType } from 'types'
4 4
 import { registerShapeUtils } from './index'
5 5
 import { boundsContained } from 'utils/bounds'
6 6
 import { intersectCircleBounds } from 'utils/intersections'
7
-import { DotCircle, ThinLine } from 'components/canvas/misc'
7
+import { ThinLine } from 'components/canvas/misc'
8 8
 import { translateBounds } from 'utils/utils'
9 9
 import styled from 'styles'
10 10
 import { defaultStyle } from 'lib/shape-styles'
@@ -42,7 +42,7 @@ const line = registerShapeUtils<LineShape>({
42 42
     return (
43 43
       <g id={id}>
44 44
         <ThinLine x1={x1} y1={y1} x2={x2} y2={y2} />
45
-        <DotCircle cx={0} cy={0} r={3} />
45
+        <use href="dot" />
46 46
       </g>
47 47
     )
48 48
   },

+ 2
- 2
lib/shape-utils/ray.tsx ファイルの表示

@@ -4,7 +4,7 @@ import { RayShape, ShapeType } from 'types'
4 4
 import { registerShapeUtils } from './index'
5 5
 import { boundsContained } from 'utils/bounds'
6 6
 import { intersectCircleBounds } from 'utils/intersections'
7
-import { DotCircle, ThinLine } from 'components/canvas/misc'
7
+import { ThinLine } from 'components/canvas/misc'
8 8
 import { translateBounds } from 'utils/utils'
9 9
 import { defaultStyle } from 'lib/shape-styles'
10 10
 
@@ -40,7 +40,7 @@ const ray = registerShapeUtils<RayShape>({
40 40
     return (
41 41
       <g id={id}>
42 42
         <ThinLine x1={0} y1={0} x2={x2} y2={y2} />
43
-        <DotCircle cx={0} cy={0} r={3} />
43
+        <use href="#dot" />
44 44
       </g>
45 45
     )
46 46
   },

+ 4
- 1
state/commands/arrow.ts ファイルの表示

@@ -1,6 +1,7 @@
1 1
 import Command from './command'
2 2
 import history from '../history'
3 3
 import { ArrowShape, Data } from 'types'
4
+import * as vec from 'utils/vec'
4 5
 import { getPage } from 'utils/utils'
5 6
 import { ArrowSnapshot } from 'state/sessions/arrow-session'
6 7
 import { getShapeUtils } from 'lib/shape-utils'
@@ -21,7 +22,9 @@ export default function arrowCommand(
21 22
 
22 23
         const { initialShape, currentPageId } = after
23 24
 
24
-        getPage(data, currentPageId).shapes[initialShape.id] = initialShape
25
+        const page = getPage(data, currentPageId)
26
+
27
+        page.shapes[initialShape.id] = initialShape
25 28
 
26 29
         data.selectedIds.clear()
27 30
         data.selectedIds.add(initialShape.id)

+ 16
- 2
state/commands/handle.ts ファイルの表示

@@ -4,6 +4,7 @@ import { Data } from 'types'
4 4
 import { getPage } from 'utils/utils'
5 5
 import { HandleSnapshot } from 'state/sessions/handle-session'
6 6
 import { getShapeUtils } from 'lib/shape-utils'
7
+import * as vec from 'utils/vec'
7 8
 
8 9
 export default function handleCommand(
9 10
   data: Data,
@@ -16,13 +17,26 @@ export default function handleCommand(
16 17
       name: 'moved_handle',
17 18
       category: 'canvas',
18 19
       do(data, isInitial) {
19
-        if (isInitial) return
20
+        // if (isInitial) return
20 21
 
21 22
         const { initialShape, currentPageId } = after
22 23
 
23
-        const shape = getPage(data, currentPageId).shapes[initialShape.id]
24
+        const page = getPage(data, currentPageId)
25
+        const shape = page.shapes[initialShape.id]
24 26
 
25 27
         getShapeUtils(shape).onHandleChange(shape, initialShape.handles)
28
+
29
+        const bounds = getShapeUtils(shape).getBounds(shape)
30
+
31
+        const offset = vec.sub([bounds.minX, bounds.minY], shape.point)
32
+
33
+        getShapeUtils(shape).translateTo(shape, vec.add(shape.point, offset))
34
+
35
+        const { start, end, bend } = page.shapes[initialShape.id].handles
36
+
37
+        start.point = vec.sub(start.point, offset)
38
+        end.point = vec.sub(end.point, offset)
39
+        bend.point = vec.sub(bend.point, offset)
26 40
       },
27 41
       undo(data) {
28 42
         const { initialShape, currentPageId } = before

+ 14
- 14
state/state.ts ファイルの表示

@@ -330,22 +330,22 @@ const state = createState({
330 330
                 CANCELLED: { do: 'cancelSession', to: 'selecting' },
331 331
               },
332 332
             },
333
-          },
334
-        },
335
-        pinching: {
336
-          on: {
337
-            PINCHED: { do: 'pinchCamera' },
338
-          },
339
-          initial: 'selectPinching',
340
-          states: {
341
-            selectPinching: {
333
+            pinching: {
342 334
               on: {
343
-                STOPPED_PINCHING: { to: 'selecting' },
335
+                PINCHED: { do: 'pinchCamera' },
344 336
               },
345
-            },
346
-            toolPinching: {
347
-              on: {
348
-                STOPPED_PINCHING: { to: 'usingTool.previous' },
337
+              initial: 'selectPinching',
338
+              states: {
339
+                selectPinching: {
340
+                  on: {
341
+                    STOPPED_PINCHING: { to: 'selecting' },
342
+                  },
343
+                },
344
+                toolPinching: {
345
+                  on: {
346
+                    STOPPED_PINCHING: { to: 'usingTool.previous' },
347
+                  },
348
+                },
349 349
               },
350 350
             },
351 351
           },

+ 3
- 0
todo.md ファイルの表示

@@ -8,3 +8,6 @@
8 8
 - Allow single-selected groups to transform their children correctly
9 9
 - (merge transform-session and transform-single-session)
10 10
 - fix drift when moving children of rotated group
11
+- shift dragging arrow handles should lock to directions
12
+- arrow rotation with handles
13
+- fix ellipse when scaleX < 0 or scaleY < 0

読み込み中…
キャンセル
保存