|
|
@@ -1,6 +1,13 @@
|
|
1
|
1
|
import { v4 as uuid } from 'uuid'
|
|
2
|
2
|
import * as vec from 'utils/vec'
|
|
3
|
|
-import * as svg from 'utils/svg'
|
|
|
3
|
+import {
|
|
|
4
|
+ ease,
|
|
|
5
|
+ getSvgPathFromStroke,
|
|
|
6
|
+ rng,
|
|
|
7
|
+ getBoundsFromPoints,
|
|
|
8
|
+ translateBounds,
|
|
|
9
|
+ pointsBetween,
|
|
|
10
|
+} from 'utils/utils'
|
|
4
|
11
|
import { ArrowShape, Bounds, ShapeHandle, ShapeType } from 'types'
|
|
5
|
12
|
import { registerShapeUtils } from './index'
|
|
6
|
13
|
import { circleFromThreePoints, isAngleBetween } from 'utils/utils'
|
|
|
@@ -9,11 +16,12 @@ import {
|
|
9
|
16
|
intersectArcBounds,
|
|
10
|
17
|
intersectLineSegmentBounds,
|
|
11
|
18
|
} from 'utils/intersections'
|
|
12
|
|
-import { getBoundsFromPoints, translateBounds } from 'utils/utils'
|
|
13
|
19
|
import { pointInCircle } from 'utils/hitTests'
|
|
14
|
20
|
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
|
|
|
21
|
+import getStroke from 'perfect-freehand'
|
|
15
|
22
|
|
|
16
|
23
|
const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
|
|
|
24
|
+const pathCache = new WeakMap<ArrowShape, string>([])
|
|
17
|
25
|
|
|
18
|
26
|
function getCtp(shape: ArrowShape) {
|
|
19
|
27
|
if (!ctpCache.has(shape.handles)) {
|
|
|
@@ -100,7 +108,6 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|
100
|
108
|
const style = getShapeStyle(shape.style)
|
|
101
|
109
|
|
|
102
|
110
|
let body: JSX.Element
|
|
103
|
|
- let endAngle: number
|
|
104
|
111
|
|
|
105
|
112
|
if (showCircle) {
|
|
106
|
113
|
if (!ctpCache.has(handles)) {
|
|
|
@@ -112,54 +119,48 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|
112
|
119
|
|
|
113
|
120
|
const circle = getCtp(shape)
|
|
114
|
121
|
|
|
115
|
|
- body = (
|
|
116
|
|
- <path
|
|
117
|
|
- d={getArrowArcPath(start, end, circle, bend)}
|
|
118
|
|
- fill="none"
|
|
119
|
|
- strokeLinecap="round"
|
|
120
|
|
- />
|
|
121
|
|
- )
|
|
|
122
|
+ if (!pathCache.has(shape)) {
|
|
|
123
|
+ renderPath(
|
|
|
124
|
+ shape,
|
|
|
125
|
+ vec.angle([circle[0], circle[1]], end.point) -
|
|
|
126
|
+ vec.angle(start.point, end.point) +
|
|
|
127
|
+ (Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
|
|
128
|
+ )
|
|
|
129
|
+ }
|
|
122
|
130
|
|
|
123
|
|
- const CE =
|
|
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)
|
|
|
131
|
+ const path = pathCache.get(shape)
|
|
127
|
132
|
|
|
128
|
|
- endAngle = CE
|
|
129
|
|
- } else {
|
|
130
|
133
|
body = (
|
|
131
|
|
- <polyline
|
|
132
|
|
- points={[start.point, end.point].join(' ')}
|
|
133
|
|
- strokeLinecap="round"
|
|
134
|
|
- />
|
|
|
134
|
+ <>
|
|
|
135
|
+ <path
|
|
|
136
|
+ d={getArrowArcPath(start, end, circle, bend)}
|
|
|
137
|
+ fill="none"
|
|
|
138
|
+ strokeWidth={+style.strokeWidth * 1.85}
|
|
|
139
|
+ strokeLinecap="round"
|
|
|
140
|
+ />
|
|
|
141
|
+ <path d={path} strokeWidth={+style.strokeWidth * 1.5} />
|
|
|
142
|
+ </>
|
|
135
|
143
|
)
|
|
136
|
|
- endAngle = 0
|
|
137
|
|
- }
|
|
|
144
|
+ } else {
|
|
|
145
|
+ if (!pathCache.has(shape)) {
|
|
|
146
|
+ renderPath(shape)
|
|
|
147
|
+ }
|
|
138
|
148
|
|
|
139
|
|
- // Arrowhead
|
|
140
|
|
- const length = Math.min(arrowDist / 2, 16 + +style.strokeWidth * 2)
|
|
141
|
|
- const u = vec.uni(vec.vec(start.point, end.point))
|
|
142
|
|
- const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
|
|
143
|
|
- const b = vec.add(end.point, vec.rot(v, Math.PI / 6))
|
|
144
|
|
- const c = vec.add(end.point, vec.rot(v, -(Math.PI / 6)))
|
|
|
149
|
+ const path = pathCache.get(shape)
|
|
|
150
|
+
|
|
|
151
|
+ body = <path d={path} />
|
|
|
152
|
+ }
|
|
145
|
153
|
|
|
146
|
154
|
return (
|
|
147
|
155
|
<g id={id}>
|
|
148
|
156
|
{body}
|
|
149
|
|
- <circle
|
|
|
157
|
+ {/* <circle
|
|
150
|
158
|
cx={start.point[0]}
|
|
151
|
159
|
cy={start.point[1]}
|
|
152
|
|
- r={+style.strokeWidth}
|
|
|
160
|
+ r={Math.max(4, +style.strokeWidth)}
|
|
153
|
161
|
fill={style.stroke}
|
|
154
|
162
|
strokeDasharray="none"
|
|
155
|
|
- />
|
|
156
|
|
- <polyline
|
|
157
|
|
- points={[b, end.point, c].join()}
|
|
158
|
|
- strokeLinecap="round"
|
|
159
|
|
- strokeLinejoin="round"
|
|
160
|
|
- fill="none"
|
|
161
|
|
- strokeDasharray="none"
|
|
162
|
|
- />
|
|
|
163
|
+ /> */}
|
|
163
|
164
|
</g>
|
|
164
|
165
|
)
|
|
165
|
166
|
},
|
|
|
@@ -426,3 +427,82 @@ function getResizeOffset(a: Bounds, b: Bounds) {
|
|
426
|
427
|
|
|
427
|
428
|
return delta
|
|
428
|
429
|
}
|
|
|
430
|
+
|
|
|
431
|
+function renderPath(shape: ArrowShape, endAngle = 0) {
|
|
|
432
|
+ const { style, id } = shape
|
|
|
433
|
+ const { start, end, bend } = shape.handles
|
|
|
434
|
+
|
|
|
435
|
+ const getRandom = rng(id)
|
|
|
436
|
+ const offsetA = getRandom()
|
|
|
437
|
+
|
|
|
438
|
+ const strokeWidth = +getShapeStyle(style).strokeWidth * 2
|
|
|
439
|
+
|
|
|
440
|
+ const arrowDist = vec.dist(start.point, end.point)
|
|
|
441
|
+
|
|
|
442
|
+ const styles = getShapeStyle(shape.style)
|
|
|
443
|
+
|
|
|
444
|
+ const sw = +styles.strokeWidth
|
|
|
445
|
+
|
|
|
446
|
+ const length = Math.min(arrowDist / 2, 24 + sw * 2)
|
|
|
447
|
+ const u = vec.uni(vec.vec(start.point, end.point))
|
|
|
448
|
+ const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
|
|
|
449
|
+
|
|
|
450
|
+ // Start
|
|
|
451
|
+ const a = start.point
|
|
|
452
|
+
|
|
|
453
|
+ // Middle
|
|
|
454
|
+ const m = vec.add(
|
|
|
455
|
+ vec.lrp(start.point, end.point, 0.25 + Math.abs(getRandom()) / 2),
|
|
|
456
|
+ [getRandom() * sw, getRandom() * sw]
|
|
|
457
|
+ )
|
|
|
458
|
+
|
|
|
459
|
+ // End
|
|
|
460
|
+ const b = end.point
|
|
|
461
|
+
|
|
|
462
|
+ // Left
|
|
|
463
|
+ let c = vec.add(
|
|
|
464
|
+ end.point,
|
|
|
465
|
+ vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())
|
|
|
466
|
+ )
|
|
|
467
|
+
|
|
|
468
|
+ // Right
|
|
|
469
|
+ let d = vec.add(
|
|
|
470
|
+ end.point,
|
|
|
471
|
+ vec.rot(v, -(Math.PI / 6) + (Math.PI / 8) * getRandom())
|
|
|
472
|
+ )
|
|
|
473
|
+
|
|
|
474
|
+ if (getRandom() > 0.5) {
|
|
|
475
|
+ ;[c, d] = [d, c]
|
|
|
476
|
+ }
|
|
|
477
|
+
|
|
|
478
|
+ const points = endAngle
|
|
|
479
|
+ ? [
|
|
|
480
|
+ // Just the arrowhead
|
|
|
481
|
+ ...pointsBetween(b, c),
|
|
|
482
|
+ ...pointsBetween(c, b),
|
|
|
483
|
+ ...pointsBetween(b, d),
|
|
|
484
|
+ ...pointsBetween(d, b),
|
|
|
485
|
+ ]
|
|
|
486
|
+ : [
|
|
|
487
|
+ // The shaft too
|
|
|
488
|
+ b,
|
|
|
489
|
+ a,
|
|
|
490
|
+ ...pointsBetween(a, m),
|
|
|
491
|
+ ...pointsBetween(m, b),
|
|
|
492
|
+ ...pointsBetween(b, c),
|
|
|
493
|
+ ...pointsBetween(c, b),
|
|
|
494
|
+ ...pointsBetween(b, d),
|
|
|
495
|
+ ...pointsBetween(d, b),
|
|
|
496
|
+ ]
|
|
|
497
|
+
|
|
|
498
|
+ const stroke = getStroke(points, {
|
|
|
499
|
+ size: 1 + strokeWidth,
|
|
|
500
|
+ thinning: 0.6,
|
|
|
501
|
+ easing: (t) => t * t * t * t,
|
|
|
502
|
+ end: { taper: strokeWidth * 20 },
|
|
|
503
|
+ start: { taper: strokeWidth * 20 },
|
|
|
504
|
+ simulatePressure: false,
|
|
|
505
|
+ })
|
|
|
506
|
+
|
|
|
507
|
+ pathCache.set(shape, getSvgPathFromStroke(stroke))
|
|
|
508
|
+}
|