Steve Ruiz 4 роки тому
джерело
коміт
daa44f9911

+ 7
- 0
.babelrc Переглянути файл

@@ -0,0 +1,7 @@
1
+{
2
+  "env": {
3
+    "test": {
4
+      "presets": ["next/babel"]
5
+    }
6
+  }
7
+}

+ 0
- 1
cypress.json Переглянути файл

@@ -1 +0,0 @@
1
-{}

+ 0
- 5
cypress/fixtures/example.json Переглянути файл

@@ -1,5 +0,0 @@
1
-{
2
-  "name": "Using fixtures to represent data",
3
-  "email": "hello@cypress.io",
4
-  "body": "Fixtures are a great way to mock data for responses to routes"
5
-}

+ 0
- 22
cypress/plugins/index.js Переглянути файл

@@ -1,22 +0,0 @@
1
-/// <reference types="cypress" />
2
-// ***********************************************************
3
-// This example plugins/index.js can be used to load plugins
4
-//
5
-// You can change the location of this file or turn off loading
6
-// the plugins file with the 'pluginsFile' configuration option.
7
-//
8
-// You can read more here:
9
-// https://on.cypress.io/plugins-guide
10
-// ***********************************************************
11
-
12
-// This function is called when a project is opened or re-opened (e.g. due to
13
-// the project's config changing)
14
-
15
-/**
16
- * @type {Cypress.PluginConfig}
17
- */
18
-// eslint-disable-next-line no-unused-vars
19
-module.exports = (on, config) => {
20
-  // `on` is used to hook into various events Cypress emits
21
-  // `config` is the resolved Cypress config
22
-}

+ 0
- 25
cypress/support/commands.js Переглянути файл

@@ -1,25 +0,0 @@
1
-// ***********************************************
2
-// This example commands.js shows you how to
3
-// create various custom commands and overwrite
4
-// existing commands.
5
-//
6
-// For more comprehensive examples of custom
7
-// commands please read more here:
8
-// https://on.cypress.io/custom-commands
9
-// ***********************************************
10
-//
11
-//
12
-// -- This is a parent command --
13
-// Cypress.Commands.add('login', (email, password) => { ... })
14
-//
15
-//
16
-// -- This is a child command --
17
-// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18
-//
19
-//
20
-// -- This is a dual command --
21
-// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22
-//
23
-//
24
-// -- This will overwrite an existing command --
25
-// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

+ 0
- 20
cypress/support/index.js Переглянути файл

@@ -1,20 +0,0 @@
1
-// ***********************************************************
2
-// This example support/index.js is processed and
3
-// loaded automatically before your test files.
4
-//
5
-// This is a great place to put global configuration and
6
-// behavior that modifies Cypress.
7
-//
8
-// You can change the location of this file or turn off
9
-// automatically serving support files with the
10
-// 'supportFile' configuration option.
11
-//
12
-// You can read more here:
13
-// https://on.cypress.io/configuration
14
-// ***********************************************************
15
-
16
-// Import commands.js using ES2015 syntax:
17
-import './commands'
18
-
19
-// Alternatively you can use CommonJS syntax:
20
-// require('./commands')

+ 6
- 6
lib/shape-styles.ts Переглянути файл

@@ -38,9 +38,9 @@ const strokeWidths = {
38 38
 }
39 39
 
40 40
 const dashArrays = {
41
-  [DashStyle.Solid]: () => 'none',
42
-  [DashStyle.Dashed]: (sw: number) => `${sw} ${sw * 2}`,
43
-  [DashStyle.Dotted]: (sw: number) => `0 ${sw * 1.5}`,
41
+  [DashStyle.Solid]: () => [1],
42
+  [DashStyle.Dashed]: (sw: number) => [sw * 2, sw * 4],
43
+  [DashStyle.Dotted]: (sw: number) => [0, sw * 3],
44 44
 }
45 45
 
46 46
 const fontSizes = {
@@ -50,11 +50,11 @@ const fontSizes = {
50 50
   auto: 'auto',
51 51
 }
52 52
 
53
-function getStrokeWidth(size: SizeStyle) {
53
+export function getStrokeWidth(size: SizeStyle) {
54 54
   return strokeWidths[size]
55 55
 }
56 56
 
57
-function getStrokeDashArray(dash: DashStyle, strokeWidth: number) {
57
+export function getStrokeDashArray(dash: DashStyle, strokeWidth: number) {
58 58
   return dashArrays[dash](strokeWidth)
59 59
 }
60 60
 
@@ -74,7 +74,7 @@ export function getShapeStyle(
74 74
   const { color, size, dash, isFilled } = style
75 75
 
76 76
   const strokeWidth = getStrokeWidth(size)
77
-  const strokeDasharray = getStrokeDashArray(dash, strokeWidth)
77
+  const strokeDasharray = getStrokeDashArray(dash, strokeWidth).join()
78 78
 
79 79
   return {
80 80
     stroke: strokes[color],

+ 211
- 140
lib/shape-utils/arrow.tsx Переглянути файл

@@ -1,4 +1,4 @@
1
-import { uniqueId } from 'utils/utils'
1
+import { getArcLength, lerp, uniqueId } from 'utils/utils'
2 2
 import vec from 'utils/vec'
3 3
 import {
4 4
   getSvgPathFromStroke,
@@ -7,7 +7,7 @@ import {
7 7
   translateBounds,
8 8
   pointsBetween,
9 9
 } from 'utils/utils'
10
-import { ArrowShape, Bounds, ShapeHandle, ShapeType } from 'types'
10
+import { ArrowShape, Bounds, DashStyle, ShapeHandle, ShapeType } from 'types'
11 11
 import { registerShapeUtils } from './index'
12 12
 import { circleFromThreePoints, isAngleBetween } from 'utils/utils'
13 13
 import { pointInBounds } from 'utils/bounds'
@@ -16,22 +16,20 @@ import {
16 16
   intersectLineSegmentBounds,
17 17
 } from 'utils/intersections'
18 18
 import { pointInCircle } from 'utils/hitTests'
19
-import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
19
+import {
20
+  defaultStyle,
21
+  getShapeStyle,
22
+  getStrokeDashArray,
23
+} from 'lib/shape-styles'
20 24
 import getStroke from 'perfect-freehand'
25
+import React from 'react'
21 26
 
22
-const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
23 27
 const pathCache = new WeakMap<ArrowShape, string>([])
24 28
 
29
+// A cache for semi-expensive circles calculated from three points
25 30
 function getCtp(shape: ArrowShape) {
26
-  if (!ctpCache.has(shape.handles)) {
27
-    const { start, end, bend } = shape.handles
28
-    ctpCache.set(
29
-      shape.handles,
30
-      circleFromThreePoints(start.point, end.point, bend.point)
31
-    )
32
-  }
33
-
34
-  return ctpCache.get(shape.handles)
31
+  const { start, end, bend } = shape.handles
32
+  return circleFromThreePoints(start.point, end.point, bend.point)
35 33
 }
36 34
 
37 35
 const arrow = registerShapeUtils<ArrowShape>({
@@ -40,10 +38,6 @@ const arrow = registerShapeUtils<ArrowShape>({
40 38
   create(props) {
41 39
     const {
42 40
       point = [0, 0],
43
-      points = [
44
-        [0, 0],
45
-        [0, 1],
46
-      ],
47 41
       handles = {
48 42
         start: {
49 43
           id: 'start',
@@ -77,7 +71,6 @@ const arrow = registerShapeUtils<ArrowShape>({
77 71
       isLocked: false,
78 72
       isHidden: false,
79 73
       bend: 0,
80
-      points,
81 74
       handles,
82 75
       decorations: {
83 76
         start: null,
@@ -94,63 +87,123 @@ const arrow = registerShapeUtils<ArrowShape>({
94 87
   },
95 88
 
96 89
   render(shape) {
97
-    const { id, bend, handles } = shape
90
+    const { id, bend, handles, style } = shape
98 91
     const { start, end, bend: _bend } = handles
99 92
 
100
-    const arrowDist = vec.dist(start.point, end.point)
101
-
102
-    const showCircle = !vec.isEqual(
93
+    const isStraightLine = vec.isEqual(
103 94
       _bend.point,
104 95
       vec.med(start.point, end.point)
105 96
     )
106 97
 
107
-    const style = getShapeStyle(shape.style)
98
+    const styles = getShapeStyle(style)
108 99
 
109
-    let body: JSX.Element
110
-
111
-    if (showCircle) {
112
-      if (!ctpCache.has(handles)) {
113
-        ctpCache.set(
114
-          handles,
115
-          circleFromThreePoints(start.point, end.point, _bend.point)
116
-        )
117
-      }
118
-
119
-      const circle = getCtp(shape)
100
+    const strokeWidth = +styles.strokeWidth
120 101
 
102
+    if (isStraightLine) {
103
+      // Render a straight arrow as a freehand path.
121 104
       if (!pathCache.has(shape)) {
122
-        renderPath(
123
-          shape,
124
-          vec.angle([circle[0], circle[1]], end.point) -
125
-            vec.angle(start.point, end.point) +
126
-            (Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
127
-        )
105
+        renderPath(shape)
128 106
       }
129 107
 
108
+      const offset = -vec.dist(start.point, end.point) + strokeWidth
109
+
130 110
       const path = pathCache.get(shape)
131 111
 
132
-      body = (
133
-        <>
112
+      return (
113
+        <g id={id}>
114
+          {/* Improves hit testing */}
134 115
           <path
135
-            d={getArrowArcPath(start, end, circle, bend)}
116
+            d={path}
117
+            stroke="transparent"
136 118
             fill="none"
137
-            strokeWidth={(+style.strokeWidth * 1.85).toString()}
119
+            strokeWidth={Math.max(8, strokeWidth * 2)}
138 120
             strokeLinecap="round"
121
+            strokeDasharray="none"
139 122
           />
140
-          <path d={path} strokeWidth={+style.strokeWidth * 1.5} />
141
-        </>
123
+          {/* Arrowshaft */}
124
+          <circle
125
+            cx={start.point[0]}
126
+            cy={start.point[1]}
127
+            r={strokeWidth}
128
+            fill={styles.stroke}
129
+            stroke="none"
130
+          />
131
+          <path
132
+            d={path}
133
+            fill="none"
134
+            strokeWidth={
135
+              strokeWidth * (style.dash === DashStyle.Solid ? 1 : 1.618)
136
+            }
137
+            strokeDashoffset={offset}
138
+            strokeLinecap="round"
139
+          />
140
+          {/* Arrowhead */}
141
+          {style.dash !== DashStyle.Solid && (
142
+            <path
143
+              d={getArrowHeadPath(shape, 0)}
144
+              strokeWidth={strokeWidth * 1.618}
145
+              strokeDasharray="none"
146
+              fill="none"
147
+            />
148
+          )}
149
+        </g>
142 150
       )
143
-    } else {
144
-      if (!pathCache.has(shape)) {
145
-        renderPath(shape)
146
-      }
151
+    }
147 152
 
148
-      const path = pathCache.get(shape)
153
+    const circle = getCtp(shape)
149 154
 
150
-      body = <path d={path} />
155
+    if (!pathCache.has(shape)) {
156
+      renderPath(
157
+        shape,
158
+        vec.angle([circle[0], circle[1]], end.point) -
159
+          vec.angle(start.point, end.point) +
160
+          (Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
161
+      )
151 162
     }
152 163
 
153
-    return <g id={id}>{body}</g>
164
+    const path = getArrowArcPath(start, end, circle, bend)
165
+
166
+    const strokeDashOffset = getStrokeDashOffsetForArc(
167
+      shape,
168
+      circle,
169
+      strokeWidth
170
+    )
171
+
172
+    return (
173
+      <g id={id}>
174
+        {/* Improves hit testing */}
175
+        <path
176
+          d={path}
177
+          stroke="transparent"
178
+          fill="none"
179
+          strokeWidth={Math.max(8, strokeWidth * 2)}
180
+          strokeLinecap="round"
181
+          strokeDasharray="none"
182
+        />
183
+        {/* Arrow Shaft */}
184
+        <circle
185
+          cx={start.point[0]}
186
+          cy={start.point[1]}
187
+          r={strokeWidth}
188
+          fill={styles.stroke}
189
+          stroke="none"
190
+        />
191
+        <path
192
+          d={path}
193
+          fill="none"
194
+          strokeWidth={strokeWidth * 1.618}
195
+          strokeLinecap="round"
196
+          strokeDashoffset={strokeDashOffset}
197
+        />
198
+        {/* Arrowhead */}
199
+        <path
200
+          d={pathCache.get(shape)}
201
+          strokeWidth={strokeWidth * 1.618}
202
+          strokeDasharray="none"
203
+          fill="none"
204
+        />
205
+      </g>
206
+    )
154 207
   },
155 208
 
156 209
   rotateBy(shape, delta) {
@@ -179,17 +232,20 @@ const arrow = registerShapeUtils<ArrowShape>({
179 232
 
180 233
   getBounds(shape) {
181 234
     if (!this.boundsCache.has(shape)) {
182
-      const { start, end } = shape.handles
183
-      this.boundsCache.set(shape, getBoundsFromPoints([start.point, end.point]))
235
+      const { start, bend, end } = shape.handles
236
+      this.boundsCache.set(
237
+        shape,
238
+        getBoundsFromPoints([start.point, bend.point, end.point])
239
+      )
184 240
     }
185 241
 
186 242
     return translateBounds(this.boundsCache.get(shape), shape.point)
187 243
   },
188 244
 
189 245
   getRotatedBounds(shape) {
190
-    const { start, end } = shape.handles
246
+    const { start, bend, end } = shape.handles
191 247
     return translateBounds(
192
-      getBoundsFromPoints([start.point, end.point], shape.rotation),
248
+      getBoundsFromPoints([start.point, bend.point, end.point], shape.rotation),
193 249
       shape.point
194 250
     )
195 251
   },
@@ -200,7 +256,7 @@ const arrow = registerShapeUtils<ArrowShape>({
200 256
   },
201 257
 
202 258
   hitTest(shape, point) {
203
-    const { start, end, bend } = shape.handles
259
+    const { start, end } = shape.handles
204 260
     if (shape.bend === 0) {
205 261
       return (
206 262
         vec.distanceToLineSegment(
@@ -239,33 +295,42 @@ const arrow = registerShapeUtils<ArrowShape>({
239 295
   transform(shape, bounds, { initialShape, scaleX, scaleY }) {
240 296
     const initialShapeBounds = this.getBounds(initialShape)
241 297
 
298
+    // let nw = initialShape.point[0] / initialShapeBounds.width
299
+    // let nh = initialShape.point[1] / initialShapeBounds.height
300
+
301
+    // shape.point = [
302
+    //   bounds.width * (scaleX < 0 ? 1 - nw : nw),
303
+    //   bounds.height * (scaleY < 0 ? 1 - nh : nh),
304
+    // ]
305
+
242 306
     shape.point = [bounds.minX, bounds.minY]
243 307
 
244
-    shape.points = shape.points.map((_, i) => {
245
-      const [x, y] = initialShape.points[i]
308
+    const handles = ['start', 'end']
309
+
310
+    handles.forEach((handle) => {
311
+      const [x, y] = initialShape.handles[handle].point
246 312
       let nw = x / initialShapeBounds.width
247 313
       let nh = y / initialShapeBounds.height
248 314
 
249
-      if (i === 1) {
250
-        let [x0, y0] = initialShape.points[0]
251
-        if (x0 === x) nw = 1
252
-        if (y0 === y) nh = 1
253
-      }
254
-
255
-      return [
315
+      shape.handles[handle].point = [
256 316
         bounds.width * (scaleX < 0 ? 1 - nw : nw),
257 317
         bounds.height * (scaleY < 0 ? 1 - nh : nh),
258 318
       ]
259 319
     })
260 320
 
261
-    const { start, end, bend } = shape.handles
321
+    const { start, bend, end } = shape.handles
322
+
323
+    const dist = vec.dist(start.point, end.point)
324
+
325
+    const midPoint = vec.med(start.point, end.point)
326
+
327
+    const bendDist = (dist / 2) * initialShape.bend
262 328
 
263
-    start.point = shape.points[0]
264
-    end.point = shape.points[1]
329
+    const u = vec.uni(vec.vec(start.point, end.point))
265 330
 
266
-    bend.point = getBendPoint(shape)
331
+    const point = vec.add(midPoint, vec.mul(vec.per(u), bendDist))
267 332
 
268
-    shape.points = [shape.handles.start.point, shape.handles.end.point]
333
+    bend.point = Math.abs(bendDist) < 10 ? midPoint : point
269 334
 
270 335
     return this
271 336
   },
@@ -279,10 +344,6 @@ const arrow = registerShapeUtils<ArrowShape>({
279 344
 
280 345
       shape.handles[handle.id] = handle
281 346
 
282
-      if (handle.index < 2) {
283
-        shape.points[handle.index] = handle.point
284
-      }
285
-
286 347
       const { start, end, bend } = shape.handles
287 348
 
288 349
       const dist = vec.dist(start.point, end.point)
@@ -327,6 +388,8 @@ const arrow = registerShapeUtils<ArrowShape>({
327 388
     end.point = vec.sub(end.point, offset)
328 389
     bend.point = vec.sub(bend.point, offset)
329 390
 
391
+    shape.handles = { ...shape.handles }
392
+
330 393
     return this
331 394
   },
332 395
 
@@ -375,47 +438,6 @@ function getBendPoint(shape: ArrowShape) {
375 438
     : vec.add(midPoint, vec.mul(vec.per(u), bendDist))
376 439
 }
377 440
 
378
-function getResizeOffset(a: Bounds, b: Bounds) {
379
-  const { minX: x0, minY: y0, width: w0, height: h0 } = a
380
-  const { minX: x1, minY: y1, width: w1, height: h1 } = b
381
-
382
-  let delta: number[]
383
-
384
-  if (h0 === h1 && w0 !== w1) {
385
-    if (x0 !== x1) {
386
-      // moving left edge, pin right edge
387
-      delta = vec.sub([x1, y1 + h1 / 2], [x0, y0 + h0 / 2])
388
-    } else {
389
-      // moving right edge, pin left edge
390
-      delta = vec.sub([x1 + w1, y1 + h1 / 2], [x0 + w0, y0 + h0 / 2])
391
-    }
392
-  } else if (h0 !== h1 && w0 === w1) {
393
-    if (y0 !== y1) {
394
-      // moving top edge, pin bottom edge
395
-      delta = vec.sub([x1 + w1 / 2, y1], [x0 + w0 / 2, y0])
396
-    } else {
397
-      // moving bottom edge, pin top edge
398
-      delta = vec.sub([x1 + w1 / 2, y1 + h1], [x0 + w0 / 2, y0 + h0])
399
-    }
400
-  } else if (x0 !== x1) {
401
-    if (y0 !== y1) {
402
-      // moving top left, pin bottom right
403
-      delta = vec.sub([x1, y1], [x0, y0])
404
-    } else {
405
-      // moving bottom left, pin top right
406
-      delta = vec.sub([x1, y1 + h1], [x0, y0 + h0])
407
-    }
408
-  } else if (y0 !== y1) {
409
-    // moving top right, pin bottom left
410
-    delta = vec.sub([x1 + w1, y1], [x0 + w0, y0])
411
-  } else {
412
-    // moving bottom right, pin top left
413
-    delta = vec.sub([x1 + w1, y1 + h1], [x0 + w0, y0 + h0])
414
-  }
415
-
416
-  return delta
417
-}
418
-
419 441
 function renderPath(shape: ArrowShape, endAngle = 0) {
420 442
   const { style, id } = shape
421 443
   const { start, end } = shape.handles
@@ -424,42 +446,34 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
424 446
 
425 447
   const strokeWidth = +getShapeStyle(style).strokeWidth * 2
426 448
 
427
-  const arrowDist = vec.dist(start.point, end.point)
428
-
429 449
   const styles = getShapeStyle(shape.style)
430 450
 
431
-  const sw = +styles.strokeWidth
432
-
433
-  const length = Math.min(arrowDist / 2, 24 + sw * 2)
434
-  const u = vec.uni(vec.vec(start.point, end.point))
435
-  const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
451
+  const sw = strokeWidth
436 452
 
437 453
   // Start
438 454
   const a = start.point
439 455
 
456
+  // End
457
+  const b = end.point
458
+
440 459
   // Middle
441 460
   const m = vec.add(
442 461
     vec.lrp(start.point, end.point, 0.25 + Math.abs(getRandom()) / 2),
443 462
     [getRandom() * sw, getRandom() * sw]
444 463
   )
445 464
 
446
-  // End
447
-  const b = end.point
465
+  // Left and right sides of the arrowhead
466
+  let { left: c, right: d } = getArrowHeadPoints(shape, endAngle)
448 467
 
449
-  // Left
450
-  let c = vec.add(
451
-    end.point,
452
-    vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())
453
-  )
468
+  // Switch which side of the arrow is drawn first
469
+  if (getRandom() > 0) [c, d] = [d, c]
454 470
 
455
-  // Right
456
-  let d = vec.add(
457
-    end.point,
458
-    vec.rot(v, -(Math.PI / 6) + (Math.PI / 8) * getRandom())
459
-  )
460
-
461
-  if (getRandom() > 0.5) {
462
-    ;[c, d] = [d, c]
471
+  if (style.dash !== DashStyle.Solid) {
472
+    pathCache.set(
473
+      shape,
474
+      (endAngle ? ['M', c, 'L', b, d] : ['M', a, 'L', b]).join(' ')
475
+    )
476
+    return
463 477
   }
464 478
 
465 479
   const points = endAngle
@@ -471,7 +485,7 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
471 485
         ...pointsBetween(d, b),
472 486
       ]
473 487
     : [
474
-        // The shaft too
488
+        // The arrow shaft
475 489
         b,
476 490
         a,
477 491
         ...pointsBetween(a, m),
@@ -493,3 +507,60 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
493 507
 
494 508
   pathCache.set(shape, getSvgPathFromStroke(stroke))
495 509
 }
510
+
511
+function getArrowHeadPath(shape: ArrowShape, endAngle = 0) {
512
+  const { end } = shape.handles
513
+  const { left, right } = getArrowHeadPoints(shape, endAngle)
514
+  return ['M', left, 'L', end.point, right].join(' ')
515
+}
516
+
517
+function getArrowHeadPoints(shape: ArrowShape, endAngle = 0) {
518
+  const { start, end } = shape.handles
519
+
520
+  const stroke = +getShapeStyle(shape.style).strokeWidth * 2
521
+
522
+  const arrowDist = vec.dist(start.point, end.point)
523
+
524
+  const arrowHeadlength = Math.min(arrowDist / 3, stroke * 4)
525
+
526
+  // Unit vector from start to end
527
+  const u = vec.uni(vec.vec(start.point, end.point))
528
+
529
+  // The end of the arrowhead wings
530
+  const v = vec.rot(vec.mul(vec.neg(u), arrowHeadlength), endAngle)
531
+
532
+  // Use the shape's random seed to create minor offsets for the angles
533
+  const getRandom = rng(shape.id)
534
+
535
+  return {
536
+    left: vec.add(
537
+      end.point,
538
+      vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())
539
+    ),
540
+    right: vec.add(
541
+      end.point,
542
+      vec.rot(v, -(Math.PI / 6) + (Math.PI / 8) * getRandom())
543
+    ),
544
+  }
545
+}
546
+
547
+function getStrokeDashOffsetForArc(
548
+  shape: ArrowShape,
549
+  circle: number[],
550
+  strokeWidth: number
551
+) {
552
+  const { start, end } = shape.handles
553
+
554
+  const sweep = getArcLength(
555
+    [circle[0], circle[1]],
556
+    circle[2],
557
+    start.point,
558
+    end.point
559
+  )
560
+
561
+  return Math.abs(shape.bend) === 1
562
+    ? -strokeWidth / 2
563
+    : shape.bend < 0
564
+    ? sweep + strokeWidth
565
+    : -sweep + strokeWidth
566
+}

+ 52
- 32
lib/shape-utils/text.tsx Переглянути файл

@@ -2,7 +2,12 @@ import { uniqueId, isMobile } from 'utils/utils'
2 2
 import vec from 'utils/vec'
3 3
 import { TextShape, ShapeType, FontSize, SizeStyle } from 'types'
4 4
 import { registerShapeUtils } from './index'
5
-import { defaultStyle, getFontStyle, getShapeStyle } from 'lib/shape-styles'
5
+import {
6
+  defaultStyle,
7
+  getFontSize,
8
+  getFontStyle,
9
+  getShapeStyle,
10
+} from 'lib/shape-styles'
6 11
 import styled from 'styles'
7 12
 import state from 'state'
8 13
 import { useEffect, useRef } from 'react'
@@ -98,6 +103,32 @@ const text = registerShapeUtils<TextShape>({
98 103
       state.send('FOCUSED_EDITING_SHAPE')
99 104
     }
100 105
 
106
+    const fontSize = getFontSize(shape.style.size) * shape.scale
107
+    const gap = fontSize * 0.4
108
+
109
+    if (!isEditing) {
110
+      return (
111
+        <g id={id} pointerEvents="none">
112
+          {text.split('\n').map((str, i) => (
113
+            <text
114
+              key={i}
115
+              x={4}
116
+              y={4 + gap / 2 + i * (fontSize + gap)}
117
+              fontFamily="Verveine Regular"
118
+              fontStyle="normal"
119
+              fontWeight="regular"
120
+              fontSize={fontSize}
121
+              width={bounds.width}
122
+              height={bounds.height}
123
+              dominant-baseline="hanging"
124
+            >
125
+              {str}
126
+            </text>
127
+          ))}
128
+        </g>
129
+      )
130
+    }
131
+
101 132
     return (
102 133
       <foreignObject
103 134
         id={id}
@@ -107,37 +138,26 @@ const text = registerShapeUtils<TextShape>({
107 138
         height={bounds.height}
108 139
         pointerEvents="none"
109 140
       >
110
-        {isEditing ? (
111
-          <StyledTextArea
112
-            ref={ref}
113
-            style={{
114
-              font,
115
-              color: styles.stroke,
116
-            }}
117
-            value={text}
118
-            tabIndex={0}
119
-            autoComplete="false"
120
-            autoCapitalize="false"
121
-            autoCorrect="false"
122
-            autoSave="false"
123
-            placeholder=""
124
-            name="text"
125
-            autoFocus={isMobile() ? true : false}
126
-            onFocus={handleFocus}
127
-            onBlur={handleBlur}
128
-            onKeyDown={handleKeyDown}
129
-            onChange={handleChange}
130
-          />
131
-        ) : (
132
-          <StyledText
133
-            style={{
134
-              font,
135
-              color: styles.stroke,
136
-            }}
137
-          >
138
-            {text}
139
-          </StyledText>
140
-        )}
141
+        <StyledTextArea
142
+          ref={ref}
143
+          style={{
144
+            font,
145
+            color: styles.stroke,
146
+          }}
147
+          value={text}
148
+          tabIndex={0}
149
+          autoComplete="false"
150
+          autoCapitalize="false"
151
+          autoCorrect="false"
152
+          autoSave="false"
153
+          placeholder=""
154
+          name="text"
155
+          autoFocus={isMobile() ? true : false}
156
+          onFocus={handleFocus}
157
+          onBlur={handleBlur}
158
+          onKeyDown={handleKeyDown}
159
+          onChange={handleChange}
160
+        />
141 161
       </foreignObject>
142 162
     )
143 163
   },

+ 6
- 2
package.json Переглянути файл

@@ -5,7 +5,10 @@
5 5
   "scripts": {
6 6
     "dev": "next dev",
7 7
     "build": "next build",
8
-    "start": "next start"
8
+    "start": "next start",
9
+    "test": "yarn test:app",
10
+    "test:all": "yarn test:code",
11
+    "test:update": "yarn test:app --updateSnapshot --watchAll=false"
9 12
   },
10 13
   "dependencies": {
11 14
     "@monaco-editor/react": "^4.1.3",
@@ -46,7 +49,8 @@
46 49
     "@types/react": "^17.0.5",
47 50
     "@types/react-dom": "^17.0.3",
48 51
     "@types/uuid": "^8.3.0",
49
-    "cypress": "^7.3.0",
52
+    "babel-jest": "^27.0.2",
53
+    "jest": "^27.0.4",
50 54
     "monaco-editor": "^0.24.0",
51 55
     "typescript": "^4.2.4"
52 56
   }

+ 2
- 0
state/commands/index.ts Переглянути файл

@@ -19,6 +19,7 @@ import paste from './paste'
19 19
 import rotateCcw from './rotate-ccw'
20 20
 import stretch from './stretch'
21 21
 import style from './style'
22
+import mutate from './mutate'
22 23
 import toggle from './toggle'
23 24
 import transform from './transform'
24 25
 import transformSingle from './transform-single'
@@ -28,6 +29,7 @@ import edit from './edit'
28 29
 import resetBounds from './reset-bounds'
29 30
 
30 31
 const commands = {
32
+  mutate,
31 33
   align,
32 34
   arrow,
33 35
   changePage,

+ 49
- 0
state/commands/mutate.ts Переглянути файл

@@ -0,0 +1,49 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data, Shape } from 'types'
4
+import { getShapeUtils } from 'lib/shape-utils'
5
+import { getPage, updateParents } from 'utils/utils'
6
+
7
+// Used when changing the properties of one or more shapes,
8
+// without changing selection or deleting any shapes.
9
+
10
+export default function mutateShapesCommand(
11
+  data: Data,
12
+  before: Shape[],
13
+  after: Shape[],
14
+  name = 'mutate_shapes'
15
+) {
16
+  history.execute(
17
+    data,
18
+    new Command({
19
+      name,
20
+      category: 'canvas',
21
+      do(data) {
22
+        const { shapes } = getPage(data)
23
+
24
+        after.forEach((shape) => {
25
+          shapes[shape.id] = shape
26
+          getShapeUtils(shape).onSessionComplete(shape)
27
+        })
28
+
29
+        // updateParents(
30
+        //   data,
31
+        //   after.map((shape) => shape.id)
32
+        // )
33
+      },
34
+      undo(data) {
35
+        const { shapes } = getPage(data)
36
+
37
+        before.forEach((shape) => {
38
+          shapes[shape.id] = shape
39
+          getShapeUtils(shape).onSessionComplete(shape)
40
+        })
41
+
42
+        updateParents(
43
+          data,
44
+          before.map((shape) => shape.id)
45
+        )
46
+      },
47
+    })
48
+  )
49
+}

+ 2
- 14
state/commands/transform.ts Переглянути файл

@@ -18,24 +18,12 @@ export default function transformCommand(
18 18
       name: 'transform_shapes',
19 19
       category: 'canvas',
20 20
       do(data) {
21
-        const { type, shapeBounds } = after
21
+        const { shapeBounds } = after
22 22
 
23 23
         const { shapes } = getPage(data)
24 24
 
25 25
         for (let id in shapeBounds) {
26
-          const { initialShapeBounds: bounds } = after.shapeBounds[id]
27
-          const { initialShape, transformOrigin } = before.shapeBounds[id]
28
-          const shape = shapes[id]
29
-
30
-          getShapeUtils(shape)
31
-            .transform(shape, bounds, {
32
-              type,
33
-              initialShape,
34
-              transformOrigin,
35
-              scaleX,
36
-              scaleY,
37
-            })
38
-            .onSessionComplete(shape)
26
+          shapes[id] = shapeBounds[id].initialShape
39 27
         }
40 28
 
41 29
         updateParents(data, Object.keys(shapeBounds))

+ 16
- 12
state/sessions/transform-session.ts Переглянути файл

@@ -5,6 +5,7 @@ import commands from 'state/commands'
5 5
 import { current, freeze } from 'immer'
6 6
 import { getShapeUtils } from 'lib/shape-utils'
7 7
 import {
8
+  deepClone,
8 9
   getBoundsCenter,
9 10
   getBoundsFromPoints,
10 11
   getCommonBounds,
@@ -103,26 +104,28 @@ export default class TransformSession extends BaseSession {
103 104
   }
104 105
 
105 106
   complete(data: Data) {
106
-    if (!this.snapshot.hasUnlockedShapes) return
107
-
108
-    commands.transform(
109
-      data,
110
-      this.snapshot,
111
-      getTransformSnapshot(data, this.transformType),
112
-      this.scaleX,
113
-      this.scaleY
107
+    const { initialShapes, hasUnlockedShapes } = this.snapshot
108
+
109
+    if (!hasUnlockedShapes) return
110
+
111
+    const page = getPage(data)
112
+
113
+    const finalShapes = initialShapes.map((shape) =>
114
+      deepClone(page.shapes[shape.id])
114 115
     )
116
+
117
+    commands.mutate(data, initialShapes, finalShapes)
115 118
   }
116 119
 }
117 120
 
118 121
 export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
119
-  const cData = current(data)
120
-  const { currentPageId } = cData
121
-  const page = getPage(cData)
122
+  const { currentPageId } = data
123
+  const page = getPage(data)
122 124
 
123 125
   const initialShapes = setToArray(getSelectedIds(data))
124
-    .flatMap((id) => getDocumentBranch(cData, id).map((id) => page.shapes[id]))
126
+    .flatMap((id) => getDocumentBranch(data, id).map((id) => page.shapes[id]))
125 127
     .filter((shape) => !shape.isLocked)
128
+    .map((shape) => deepClone(shape))
126 129
 
127 130
   const hasUnlockedShapes = initialShapes.length > 0
128 131
 
@@ -151,6 +154,7 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
151 154
     hasUnlockedShapes,
152 155
     isAllAspectRatioLocked,
153 156
     currentPageId,
157
+    initialShapes,
154 158
     initialBounds: commonBounds,
155 159
     shapeBounds: Object.fromEntries(
156 160
       initialShapes.map((shape) => {

+ 1
- 2
state/state.ts Переглянути файл

@@ -66,7 +66,6 @@ const initialData: Data = {
66 66
     size: SizeStyle.Medium,
67 67
     color: ColorStyle.Black,
68 68
     dash: DashStyle.Solid,
69
-    fontSize: FontSize.Medium,
70 69
     isFilled: false,
71 70
   },
72 71
   activeTool: 'select',
@@ -131,7 +130,7 @@ const state = createState({
131 130
         wait: 0.01,
132 131
         if: 'hasSelection',
133 132
         do: 'zoomCameraToSelectionActual',
134
-        else: ['zoomCameraToFit', 'zoomCameraToActual'],
133
+        else: ['zoomCameraToActual'],
135 134
       },
136 135
       on: {
137 136
         COPIED: { if: 'hasSelection', do: 'copyToClipboard' },

+ 7
- 2
todo.md Переглянути файл

@@ -47,5 +47,10 @@
47 47
 
48 48
 ## Clipboard
49 49
 
50
-- [ ] Copy
51
-- [ ] Paste
50
+- [x] Copy
51
+- [x] Paste shapes
52
+- [ ] Paste as text
53
+
54
+## Copy to SVG
55
+
56
+- [ ] Copy to SVG

+ 1
- 6
types.ts Переглянути файл

@@ -16,7 +16,7 @@ export interface Data {
16 16
     isToolLocked: boolean
17 17
     isPenLocked: boolean
18 18
   }
19
-  currentStyle: ShapeStyles & TextStyles
19
+  currentStyle: ShapeStyles
20 20
   activeTool: ShapeType | 'select'
21 21
   brush?: Bounds
22 22
   boundsRotation: number
@@ -114,10 +114,6 @@ export type ShapeStyles = {
114 114
   isFilled: boolean
115 115
 }
116 116
 
117
-export type TextStyles = {
118
-  fontSize: FontSize
119
-}
120
-
121 117
 export interface BaseShape {
122 118
   id: string
123 119
   seed: number
@@ -180,7 +176,6 @@ export interface DrawShape extends BaseShape {
180 176
 
181 177
 export interface ArrowShape extends BaseShape {
182 178
   type: ShapeType.Arrow
183
-  points: number[][]
184 179
   handles: Record<string, ShapeHandle>
185 180
   bend: number
186 181
   decorations?: {

+ 2
- 2
utils/bounds.ts Переглянути файл

@@ -1,8 +1,8 @@
1
-import { Bounds } from "types"
1
+import { Bounds } from 'types'
2 2
 import {
3 3
   intersectPolygonBounds,
4 4
   intersectPolylineBounds,
5
-} from "./intersections"
5
+} from './intersections'
6 6
 
7 7
 /**
8 8
  * Get whether two bounds collide.

+ 62
- 0
utils/utils.ts Переглянути файл

@@ -1850,3 +1850,65 @@ export function decompress(s: string) {
1850 1850
 
1851 1851
   return out.join('')
1852 1852
 }
1853
+
1854
+function getResizeOffset(a: Bounds, b: Bounds) {
1855
+  const { minX: x0, minY: y0, width: w0, height: h0 } = a
1856
+  const { minX: x1, minY: y1, width: w1, height: h1 } = b
1857
+
1858
+  let delta: number[]
1859
+
1860
+  if (h0 === h1 && w0 !== w1) {
1861
+    if (x0 !== x1) {
1862
+      // moving left edge, pin right edge
1863
+      delta = vec.sub([x1, y1 + h1 / 2], [x0, y0 + h0 / 2])
1864
+    } else {
1865
+      // moving right edge, pin left edge
1866
+      delta = vec.sub([x1 + w1, y1 + h1 / 2], [x0 + w0, y0 + h0 / 2])
1867
+    }
1868
+  } else if (h0 !== h1 && w0 === w1) {
1869
+    if (y0 !== y1) {
1870
+      // moving top edge, pin bottom edge
1871
+      delta = vec.sub([x1 + w1 / 2, y1], [x0 + w0 / 2, y0])
1872
+    } else {
1873
+      // moving bottom edge, pin top edge
1874
+      delta = vec.sub([x1 + w1 / 2, y1 + h1], [x0 + w0 / 2, y0 + h0])
1875
+    }
1876
+  } else if (x0 !== x1) {
1877
+    if (y0 !== y1) {
1878
+      // moving top left, pin bottom right
1879
+      delta = vec.sub([x1, y1], [x0, y0])
1880
+    } else {
1881
+      // moving bottom left, pin top right
1882
+      delta = vec.sub([x1, y1 + h1], [x0, y0 + h0])
1883
+    }
1884
+  } else if (y0 !== y1) {
1885
+    // moving top right, pin bottom left
1886
+    delta = vec.sub([x1 + w1, y1], [x0 + w0, y0])
1887
+  } else {
1888
+    // moving bottom right, pin top left
1889
+    delta = vec.sub([x1 + w1, y1 + h1], [x0 + w0, y0 + h0])
1890
+  }
1891
+
1892
+  return delta
1893
+}
1894
+
1895
+export function deepClone<T extends unknown[] | object>(obj: T): T {
1896
+  if (obj === null) return null
1897
+
1898
+  let clone = { ...obj }
1899
+
1900
+  Object.keys(obj).forEach(
1901
+    (key) =>
1902
+      (clone[key] =
1903
+        typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
1904
+  )
1905
+
1906
+  if (Array.isArray(obj)) {
1907
+    // @ts-ignore
1908
+    clone.length = obj.length
1909
+    // @ts-ignore
1910
+    return Array.from(clone) as T
1911
+  }
1912
+
1913
+  return clone
1914
+}

+ 1028
- 498
yarn.lock
Різницю між файлами не показано, бо вона завелика
Переглянути файл


Завантаження…
Відмінити
Зберегти