EllipseShape.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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. <TextLabel
  92. font={font}
  93. text={label}
  94. color={getComputedColor(stroke, 'text')}
  95. offsetX={offset[0]}
  96. offsetY={offset[1]}
  97. scale={scale}
  98. isEditing={isEditing}
  99. onChange={handleLabelChange}
  100. onBlur={onEditingEnd}
  101. fontStyle={italic ? 'italic' : 'normal'}
  102. fontSize={fontSize}
  103. fontWeight={fontWeight}
  104. pointerEvents={!!label}
  105. />
  106. <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
  107. <ellipse
  108. className={isSelected || !noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
  109. cx={w / 2}
  110. cy={h / 2}
  111. rx={Math.max(0.01, (w - strokeWidth) / 2)}
  112. ry={Math.max(0.01, (h - strokeWidth) / 2)}
  113. />
  114. <ellipse
  115. cx={w / 2}
  116. cy={h / 2}
  117. rx={Math.max(0.01, (w - strokeWidth) / 2)}
  118. ry={Math.max(0.01, (h - strokeWidth) / 2)}
  119. strokeWidth={strokeWidth}
  120. stroke={getComputedColor(stroke, 'stroke')}
  121. strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
  122. fill={noFill ? 'none' : getComputedColor(fill, 'background')}
  123. />
  124. </SVGContainer>
  125. </div>
  126. )
  127. }
  128. )
  129. @computed get scaleLevel() {
  130. return this.props.scaleLevel ?? 'md'
  131. }
  132. @action setScaleLevel = async (v?: SizeLevel) => {
  133. this.update({
  134. scaleLevel: v,
  135. fontSize: levelToScale[v ?? 'md'],
  136. strokeWidth: levelToScale[v ?? 'md'] / 10,
  137. })
  138. this.onResetBounds()
  139. }
  140. ReactIndicator = observer(() => {
  141. const {
  142. size: [w, h],
  143. isLocked,
  144. } = this.props
  145. return (
  146. <g>
  147. <ellipse cx={w / 2} cy={h / 2} rx={w / 2} ry={h / 2} strokeWidth={2} fill="transparent" strokeDasharray={isLocked ? "8 2" : "undefined"}/>
  148. </g>
  149. )
  150. })
  151. validateProps = (props: Partial<EllipseShapeProps>) => {
  152. if (props.size !== undefined) {
  153. props.size[0] = Math.max(props.size[0], 1)
  154. props.size[1] = Math.max(props.size[1], 1)
  155. }
  156. return withClampedStyles(this, props)
  157. }
  158. /**
  159. * Get a svg group element that can be used to render the shape with only the props data. In the
  160. * base, draw any shape as a box. Can be overridden by subclasses.
  161. */
  162. getShapeSVGJsx(opts: any) {
  163. const {
  164. size: [w, h],
  165. stroke,
  166. fill,
  167. noFill,
  168. strokeWidth,
  169. strokeType,
  170. opacity,
  171. } = this.props
  172. return (
  173. <g opacity={opacity}>
  174. <ellipse
  175. className={!noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
  176. cx={w / 2}
  177. cy={h / 2}
  178. rx={Math.max(0.01, (w - strokeWidth) / 2)}
  179. ry={Math.max(0.01, (h - strokeWidth) / 2)}
  180. />
  181. <ellipse
  182. cx={w / 2}
  183. cy={h / 2}
  184. rx={Math.max(0.01, (w - strokeWidth) / 2)}
  185. ry={Math.max(0.01, (h - strokeWidth) / 2)}
  186. strokeWidth={strokeWidth}
  187. stroke={getComputedColor(stroke, 'stroke')}
  188. strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
  189. fill={noFill ? 'none' : getComputedColor(fill, 'background')}
  190. />
  191. </g>
  192. )
  193. }
  194. }