You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

draw.tsx 4.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { v4 as uuid } from 'uuid'
  2. import * as vec from 'utils/vec'
  3. import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
  4. import { registerShapeUtils } from './index'
  5. import { intersectPolylineBounds } from 'utils/intersections'
  6. import { boundsContainPolygon } from 'utils/bounds'
  7. import getStroke from 'perfect-freehand'
  8. import {
  9. getBoundsCenter,
  10. getBoundsFromPoints,
  11. getSvgPathFromStroke,
  12. translateBounds,
  13. } from 'utils/utils'
  14. import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
  15. const pathCache = new WeakMap<DrawShape['points'], string>([])
  16. const draw = registerShapeUtils<DrawShape>({
  17. boundsCache: new WeakMap([]),
  18. create(props) {
  19. return {
  20. id: uuid(),
  21. type: ShapeType.Draw,
  22. isGenerated: false,
  23. name: 'Draw',
  24. parentId: 'page0',
  25. childIndex: 0,
  26. point: [0, 0],
  27. points: [],
  28. rotation: 0,
  29. isAspectRatioLocked: false,
  30. isLocked: false,
  31. isHidden: false,
  32. ...props,
  33. style: {
  34. ...defaultStyle,
  35. ...props.style,
  36. isFilled: false,
  37. },
  38. }
  39. },
  40. render(shape) {
  41. const { id, points, style } = shape
  42. const styles = getShapeStyle(style)
  43. if (!pathCache.has(points)) {
  44. renderPath(shape, style)
  45. }
  46. if (points.length < 2) {
  47. return (
  48. <circle id={id} r={+styles.strokeWidth * 0.618} fill={styles.stroke} />
  49. )
  50. }
  51. return <path id={id} d={pathCache.get(points)} fill={styles.stroke} />
  52. },
  53. getBounds(shape) {
  54. if (!this.boundsCache.has(shape)) {
  55. const bounds = getBoundsFromPoints(shape.points)
  56. this.boundsCache.set(shape, bounds)
  57. }
  58. return translateBounds(this.boundsCache.get(shape), shape.point)
  59. },
  60. getRotatedBounds(shape) {
  61. const bounds =
  62. this.boundsCache.get(shape) || getBoundsFromPoints(shape.points)
  63. const center = getBoundsCenter(bounds)
  64. const rotatedPts = shape.points.map((pt) =>
  65. vec.rotWith(pt, center, shape.rotation)
  66. )
  67. const rotatedBounds = translateBounds(
  68. getBoundsFromPoints(rotatedPts),
  69. shape.point
  70. )
  71. return rotatedBounds
  72. },
  73. getCenter(shape) {
  74. const bounds = this.getRotatedBounds(shape)
  75. return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
  76. },
  77. hitTest(shape, point) {
  78. let pt = vec.sub(point, shape.point)
  79. const min = +getShapeStyle(shape.style).strokeWidth
  80. return shape.points.some(
  81. (curr, i) =>
  82. i > 0 && vec.distanceToLineSegment(shape.points[i - 1], curr, pt) < min
  83. )
  84. },
  85. hitTestBounds(this, shape, brushBounds) {
  86. const b = this.getBounds(shape)
  87. const center = [b.minX + b.width / 2, b.minY + b.height / 2]
  88. const rotatedCorners = [
  89. [b.minX, b.minY],
  90. [b.maxX, b.minY],
  91. [b.maxX, b.maxY],
  92. [b.minX, b.maxY],
  93. ].map((point) => vec.rotWith(point, center, shape.rotation))
  94. return (
  95. boundsContainPolygon(brushBounds, rotatedCorners) ||
  96. intersectPolylineBounds(
  97. shape.points.map((point) => vec.add(point, shape.point)),
  98. brushBounds
  99. ).length > 0
  100. )
  101. },
  102. transform(shape, bounds, { initialShape, scaleX, scaleY }) {
  103. const initialShapeBounds = this.boundsCache.get(initialShape)
  104. shape.points = initialShape.points.map(([x, y]) => {
  105. return [
  106. bounds.width *
  107. (scaleX < 0
  108. ? 1 - x / initialShapeBounds.width
  109. : x / initialShapeBounds.width),
  110. bounds.height *
  111. (scaleY < 0
  112. ? 1 - y / initialShapeBounds.height
  113. : y / initialShapeBounds.height),
  114. ]
  115. })
  116. const newBounds = getBoundsFromPoints(shape.points)
  117. shape.point = vec.sub(
  118. [bounds.minX, bounds.minY],
  119. [newBounds.minX, newBounds.minY]
  120. )
  121. return this
  122. },
  123. applyStyles(shape, style) {
  124. const styles = { ...shape.style, ...style }
  125. styles.isFilled = false
  126. styles.dash = DashStyle.Solid
  127. shape.style = styles
  128. shape.points = [...shape.points]
  129. return this
  130. },
  131. canStyleFill: false,
  132. })
  133. export default draw
  134. function renderPath(shape: DrawShape, style: ShapeStyles) {
  135. const styles = getShapeStyle(style)
  136. pathCache.set(
  137. shape.points,
  138. getSvgPathFromStroke(
  139. getStroke(shape.points, {
  140. size: 1 + +styles.strokeWidth * 2,
  141. thinning: 0.85,
  142. end: { taper: +styles.strokeWidth * 20 },
  143. start: { taper: +styles.strokeWidth * 20 },
  144. })
  145. )
  146. )
  147. }