EllipseShape.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. import {
  3. TLEllipseShapeProps,
  4. TLEllipseShape,
  5. getComputedColor,
  6. getTextLabelSize,
  7. } from '@tldraw/core'
  8. import { SVGContainer, TLComponentProps } from '@tldraw/react'
  9. import Vec from '@tldraw/vec'
  10. import * as React from 'react'
  11. import { observer } from 'mobx-react-lite'
  12. import { CustomStyleProps, withClampedStyles } from './style-props'
  13. import { TextLabel } from './text/TextLabel'
  14. import type { SizeLevel } from '.'
  15. import { action, computed } from 'mobx'
  16. export interface EllipseShapeProps extends TLEllipseShapeProps, CustomStyleProps {
  17. type: 'ellipse'
  18. size: number[]
  19. label: string
  20. fontSize: number
  21. fontWeight: number
  22. italic: boolean
  23. scaleLevel?: SizeLevel
  24. }
  25. const font = '18px / 1 var(--ls-font-family)'
  26. const levelToScale = {
  27. xs: 10,
  28. sm: 16,
  29. md: 20,
  30. lg: 32,
  31. xl: 48,
  32. xxl: 60,
  33. }
  34. export class EllipseShape extends TLEllipseShape<EllipseShapeProps> {
  35. static id = 'ellipse'
  36. static defaultProps: EllipseShapeProps = {
  37. id: 'ellipse',
  38. parentId: 'page',
  39. type: 'ellipse',
  40. point: [0, 0],
  41. size: [100, 100],
  42. stroke: '',
  43. fill: '',
  44. noFill: false,
  45. fontWeight: 400,
  46. fontSize: 20,
  47. italic: false,
  48. strokeType: 'line',
  49. strokeWidth: 2,
  50. opacity: 1,
  51. label: '',
  52. }
  53. canEdit = true
  54. ReactComponent = observer(
  55. ({ isSelected, isErasing, events, isEditing, onEditingEnd }: TLComponentProps) => {
  56. const {
  57. size: [w, h],
  58. stroke,
  59. fill,
  60. noFill,
  61. strokeWidth,
  62. strokeType,
  63. opacity,
  64. label,
  65. italic,
  66. fontWeight,
  67. fontSize,
  68. } = this.props
  69. const labelSize =
  70. label || isEditing
  71. ? getTextLabelSize(
  72. label,
  73. { fontFamily: 'var(--ls-font-family)', fontSize, lineHeight: 1, fontWeight },
  74. 4
  75. )
  76. : [0, 0]
  77. const midPoint = Vec.mul(this.props.size, 0.5)
  78. const scale = Math.max(0.5, Math.min(1, w / labelSize[0], h / labelSize[1]))
  79. const bounds = this.getBounds()
  80. const offset = React.useMemo(() => {
  81. return Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
  82. }, [bounds, scale, midPoint])
  83. const handleLabelChange = React.useCallback(
  84. (label: string) => {
  85. this.update?.({ label })
  86. },
  87. [label]
  88. )
  89. return (
  90. <div {...events} style={{ width: '100%', height: '100%', overflow: 'hidden' }}
  91. className="tl-ellipse-container">
  92. <TextLabel
  93. font={font}
  94. text={label}
  95. color={getComputedColor(stroke, 'text')}
  96. offsetX={offset[0]}
  97. offsetY={offset[1]}
  98. scale={scale}
  99. isEditing={isEditing}
  100. onChange={handleLabelChange}
  101. onBlur={onEditingEnd}
  102. fontStyle={italic ? 'italic' : 'normal'}
  103. fontSize={fontSize}
  104. fontWeight={fontWeight}
  105. pointerEvents={!!label}
  106. />
  107. <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
  108. <ellipse
  109. className={isSelected || !noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
  110. cx={w / 2}
  111. cy={h / 2}
  112. rx={Math.max(0.01, (w - strokeWidth) / 2)}
  113. ry={Math.max(0.01, (h - strokeWidth) / 2)}
  114. />
  115. <ellipse
  116. cx={w / 2}
  117. cy={h / 2}
  118. rx={Math.max(0.01, (w - strokeWidth) / 2)}
  119. ry={Math.max(0.01, (h - strokeWidth) / 2)}
  120. strokeWidth={strokeWidth}
  121. stroke={getComputedColor(stroke, 'stroke')}
  122. strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
  123. fill={noFill ? 'none' : getComputedColor(fill, 'background')}
  124. />
  125. </SVGContainer>
  126. </div>
  127. )
  128. }
  129. )
  130. @computed get scaleLevel() {
  131. return this.props.scaleLevel ?? 'md'
  132. }
  133. @action setScaleLevel = async (v?: SizeLevel) => {
  134. this.update({
  135. scaleLevel: v,
  136. fontSize: levelToScale[v ?? 'md'],
  137. strokeWidth: levelToScale[v ?? 'md'] / 10,
  138. })
  139. this.onResetBounds()
  140. }
  141. ReactIndicator = observer(() => {
  142. const {
  143. size: [w, h],
  144. isLocked,
  145. } = this.props
  146. return (
  147. <g>
  148. <ellipse
  149. cx={w / 2}
  150. cy={h / 2}
  151. rx={w / 2}
  152. ry={h / 2}
  153. strokeWidth={2}
  154. fill="transparent"
  155. strokeDasharray={isLocked ? '8 2' : 'undefined'}
  156. />
  157. </g>
  158. )
  159. })
  160. validateProps = (props: Partial<EllipseShapeProps>) => {
  161. if (props.size !== undefined) {
  162. props.size[0] = Math.max(props.size[0], 1)
  163. props.size[1] = Math.max(props.size[1], 1)
  164. }
  165. return withClampedStyles(this, props)
  166. }
  167. /**
  168. * Get a svg group element that can be used to render the shape with only the props data. In the
  169. * base, draw any shape as a box. Can be overridden by subclasses.
  170. */
  171. getShapeSVGJsx(opts: any) {
  172. const {
  173. size: [w, h],
  174. stroke,
  175. fill,
  176. noFill,
  177. strokeWidth,
  178. strokeType,
  179. opacity,
  180. } = this.props
  181. return (
  182. <g opacity={opacity}>
  183. <ellipse
  184. className={!noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
  185. cx={w / 2}
  186. cy={h / 2}
  187. rx={Math.max(0.01, (w - strokeWidth) / 2)}
  188. ry={Math.max(0.01, (h - strokeWidth) / 2)}
  189. />
  190. <ellipse
  191. cx={w / 2}
  192. cy={h / 2}
  193. rx={Math.max(0.01, (w - strokeWidth) / 2)}
  194. ry={Math.max(0.01, (h - strokeWidth) / 2)}
  195. strokeWidth={strokeWidth}
  196. stroke={getComputedColor(stroke, 'stroke')}
  197. strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
  198. fill={noFill ? 'none' : getComputedColor(fill, 'background')}
  199. />
  200. </g>
  201. )
  202. }
  203. }