LineShape.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. import { Decoration, TLLineShape, TLLineShapeProps } from '@tldraw/core'
  3. import { SVGContainer, TLComponentProps, useApp } from '@tldraw/react'
  4. import Vec from '@tldraw/vec'
  5. import { observer } from 'mobx-react-lite'
  6. import * as React from 'react'
  7. import { Arrow } from './arrow/Arrow'
  8. import { getArrowPath } from './arrow/arrowHelpers'
  9. import { CustomStyleProps, withClampedStyles } from './style-props'
  10. import { getTextLabelSize } from './text/getTextSize'
  11. import { LabelMask } from './text/LabelMask'
  12. import { TextLabel } from './text/TextLabel'
  13. interface LineShapeProps extends CustomStyleProps, TLLineShapeProps {
  14. type: 'line'
  15. label: string
  16. }
  17. const font = '18px / 1 var(--ls-font-family)'
  18. export class LineShape extends TLLineShape<LineShapeProps> {
  19. static id = 'line'
  20. static defaultProps: LineShapeProps = {
  21. id: 'line',
  22. parentId: 'page',
  23. type: 'line',
  24. point: [0, 0],
  25. handles: {
  26. start: { id: 'start', canBind: true, point: [0, 0] },
  27. end: { id: 'end', canBind: true, point: [1, 1] },
  28. },
  29. stroke: 'var(--ls-primary-text-color, #000)',
  30. fill: 'var(--ls-secondary-background-color)',
  31. noFill: true,
  32. strokeType: 'line',
  33. strokeWidth: 1,
  34. opacity: 1,
  35. decorations: {
  36. end: Decoration.Arrow,
  37. },
  38. label: '',
  39. }
  40. hideSelection = true
  41. canEdit = true
  42. ReactComponent = observer(({ events, isErasing, isEditing, onEditingEnd }: TLComponentProps) => {
  43. const {
  44. stroke,
  45. handles: { start, end },
  46. opacity,
  47. label,
  48. id,
  49. } = this.props
  50. const app = useApp()
  51. const labelSize = label || isEditing ? getTextLabelSize(label, font) : [0, 0]
  52. const midPoint = Vec.med(start.point, end.point)
  53. const dist = Vec.dist(start.point, end.point)
  54. const scale = Math.max(
  55. 0.5,
  56. Math.min(1, Math.max(dist / (labelSize[1] + 128), dist / (labelSize[0] + 128)))
  57. )
  58. const bounds = this.getBounds()
  59. const offset = React.useMemo(() => {
  60. const offset = Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
  61. return offset
  62. }, [bounds, scale, midPoint])
  63. const handleLabelChange = React.useCallback(
  64. (label: string) => {
  65. this.update?.({ label })
  66. app.persist()
  67. },
  68. [label]
  69. )
  70. return (
  71. <div {...events} style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
  72. <TextLabel
  73. font={font}
  74. text={label}
  75. color={stroke}
  76. offsetX={offset[0]}
  77. offsetY={offset[1]}
  78. scale={scale}
  79. isEditing={isEditing}
  80. onChange={handleLabelChange}
  81. onBlur={onEditingEnd}
  82. />
  83. <SVGContainer opacity={isErasing ? 0.2 : opacity} id={id + '_svg'}>
  84. <LabelMask id={id} bounds={bounds} labelSize={labelSize} offset={offset} scale={scale} />
  85. <g pointerEvents="none" mask={label || isEditing ? `url(#${id}_clip)` : ``}>
  86. {this.getShapeSVGJsx({ preview: false })}
  87. </g>
  88. </SVGContainer>
  89. </div>
  90. )
  91. })
  92. ReactIndicator = observer(() => {
  93. const {
  94. id,
  95. decorations,
  96. label,
  97. strokeWidth,
  98. handles: { start, end },
  99. } = this.props
  100. const bounds = this.getBounds()
  101. const labelSize = label ? getTextLabelSize(label, font) : [0, 0]
  102. const midPoint = Vec.med(start.point, end.point)
  103. const dist = Vec.dist(start.point, end.point)
  104. const scale = Math.max(
  105. 0.5,
  106. Math.min(1, Math.max(dist / (labelSize[1] + 128), dist / (labelSize[0] + 128)))
  107. )
  108. const offset = React.useMemo(() => {
  109. const offset = Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
  110. return offset
  111. }, [bounds, scale, midPoint])
  112. return (
  113. <g>
  114. <LabelMask id={id} bounds={bounds} labelSize={labelSize} offset={offset} scale={scale} />
  115. <path
  116. mask={label ? `url(#${id}_clip)` : ``}
  117. d={getArrowPath(
  118. { strokeWidth },
  119. start.point,
  120. end.point,
  121. decorations?.start,
  122. decorations?.end
  123. )}
  124. />
  125. {label && (
  126. <rect
  127. x={bounds.width / 2 - (labelSize[0] / 2) * scale + offset[0]}
  128. y={bounds.height / 2 - (labelSize[1] / 2) * scale + offset[1]}
  129. width={labelSize[0] * scale}
  130. height={labelSize[1] * scale}
  131. rx={4 * scale}
  132. ry={4 * scale}
  133. fill="transparent"
  134. />
  135. )}
  136. </g>
  137. )
  138. })
  139. validateProps = (props: Partial<LineShapeProps>) => {
  140. return withClampedStyles(this, props)
  141. }
  142. getShapeSVGJsx({ preview }: any) {
  143. const {
  144. stroke,
  145. fill,
  146. strokeWidth,
  147. strokeType,
  148. decorations,
  149. label,
  150. handles: { start, end },
  151. } = this.props
  152. const midPoint = Vec.med(start.point, end.point)
  153. return (
  154. <>
  155. <Arrow
  156. style={{
  157. stroke,
  158. fill,
  159. strokeWidth,
  160. strokeType,
  161. }}
  162. start={start.point}
  163. end={end.point}
  164. decorationStart={decorations?.start}
  165. decorationEnd={decorations?.end}
  166. />
  167. {preview && (
  168. <>
  169. <text
  170. style={{
  171. transformOrigin: 'top left',
  172. }}
  173. fontFamily="Inter"
  174. fontSize={20}
  175. transform={`translate(${midPoint[0]}, ${midPoint[1]})`}
  176. textAnchor="middle"
  177. stroke={stroke}
  178. fill={stroke}
  179. >
  180. {label}
  181. </text>
  182. </>
  183. )}
  184. </>
  185. )
  186. }
  187. }