Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

shape.tsx 4.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import React, { useRef, memo } from 'react'
  2. import state, { useSelector } from 'state'
  3. import styled from 'styles'
  4. import { getShapeUtils } from 'lib/shape-utils'
  5. import { getBoundsCenter, getPage } from 'utils/utils'
  6. import { ShapeStyles, ShapeType } from 'types'
  7. import useShapeEvents from 'hooks/useShapeEvents'
  8. import vec from 'utils/vec'
  9. import { getShapeStyle } from 'lib/shape-styles'
  10. import ContextMenu from 'components/context-menu'
  11. interface ShapeProps {
  12. id: string
  13. isSelecting: boolean
  14. parentPoint: number[]
  15. }
  16. function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
  17. const shape = useSelector((s) => getPage(s.data).shapes[id])
  18. const isEditing = useSelector((s) => s.data.editingId === id)
  19. const rGroup = useRef<SVGGElement>(null)
  20. const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup)
  21. // This is a problem with deleted shapes. The hooks in this component
  22. // may sometimes run before the hook in the Page component, which means
  23. // a deleted shape will still be pulled here before the page component
  24. // detects the change and pulls this component.
  25. if (!shape) return null
  26. const style = getShapeStyle(shape.style)
  27. const shapeUtils = getShapeUtils(shape)
  28. const { isShy, isParent, isForeignObject } = shapeUtils
  29. const bounds = shapeUtils.getBounds(shape)
  30. const center = shapeUtils.getCenter(shape)
  31. const rotation = shape.rotation * (180 / Math.PI)
  32. const transform = `
  33. translate(${vec.neg(parentPoint)})
  34. rotate(${rotation}, ${center})
  35. translate(${shape.point})
  36. `
  37. return (
  38. <StyledGroup
  39. ref={rGroup}
  40. transform={transform}
  41. onBlur={() => state.send('BLURRED_SHAPE', { target: id })}
  42. >
  43. {isSelecting &&
  44. !isShy &&
  45. (isForeignObject ? (
  46. <HoverIndicator
  47. as="rect"
  48. width={bounds.width}
  49. height={bounds.height}
  50. strokeWidth={1.5}
  51. variant={'ghost'}
  52. {...events}
  53. />
  54. ) : (
  55. <HoverIndicator
  56. as="use"
  57. href={'#' + id}
  58. strokeWidth={+style.strokeWidth + 4}
  59. variant={getShapeUtils(shape).canStyleFill ? 'filled' : 'hollow'}
  60. {...events}
  61. />
  62. ))}
  63. {!shape.isHidden &&
  64. (isForeignObject ? (
  65. shapeUtils.render(shape, { isEditing })
  66. ) : (
  67. <RealShape
  68. isParent={isParent}
  69. id={id}
  70. style={style}
  71. isEditing={isEditing}
  72. />
  73. ))}
  74. {isParent &&
  75. shape.children.map((shapeId) => (
  76. <Shape
  77. key={shapeId}
  78. id={shapeId}
  79. isSelecting={isSelecting}
  80. parentPoint={shape.point}
  81. />
  82. ))}
  83. </StyledGroup>
  84. )
  85. }
  86. interface RealShapeProps {
  87. id: string
  88. style: Partial<React.SVGProps<SVGUseElement>>
  89. isParent: boolean
  90. isEditing: boolean
  91. }
  92. const RealShape = memo(function RealShape({
  93. id,
  94. style,
  95. isParent,
  96. isEditing,
  97. }: RealShapeProps) {
  98. return <StyledShape as="use" data-shy={isParent} href={'#' + id} {...style} />
  99. })
  100. const StyledShape = styled('path', {
  101. strokeLinecap: 'round',
  102. strokeLinejoin: 'round',
  103. pointerEvents: 'none',
  104. })
  105. const HoverIndicator = styled('path', {
  106. stroke: '$selected',
  107. strokeLinecap: 'round',
  108. strokeLinejoin: 'round',
  109. fill: 'transparent',
  110. filter: 'url(#expand)',
  111. variants: {
  112. variant: {
  113. ghost: {
  114. pointerEvents: 'all',
  115. filter: 'none',
  116. opacity: 0,
  117. },
  118. hollow: {
  119. pointerEvents: 'stroke',
  120. },
  121. filled: {
  122. pointerEvents: 'all',
  123. },
  124. },
  125. },
  126. })
  127. const StyledGroup = styled('g', {
  128. outline: 'none',
  129. [`& *[data-shy="true"]`]: {
  130. opacity: '0',
  131. },
  132. [`& ${HoverIndicator}`]: {
  133. opacity: '0',
  134. },
  135. [`&:hover ${HoverIndicator}`]: {
  136. opacity: '0.16',
  137. },
  138. [`&:hover *[data-shy="true"]`]: {
  139. opacity: '1',
  140. },
  141. variants: {
  142. isSelected: {
  143. true: {
  144. [`& *[data-shy="true"]`]: {
  145. opacity: '1',
  146. },
  147. [`& ${HoverIndicator}`]: {
  148. opacity: '0.2',
  149. },
  150. [`&:hover ${HoverIndicator}`]: {
  151. opacity: '0.3',
  152. },
  153. [`&:active ${HoverIndicator}`]: {
  154. opacity: '0.3',
  155. },
  156. },
  157. false: {
  158. [`& ${HoverIndicator}`]: {
  159. opacity: '0',
  160. },
  161. },
  162. },
  163. },
  164. })
  165. function Label({ children }: { children: React.ReactNode }) {
  166. return (
  167. <text
  168. y={4}
  169. x={4}
  170. fontSize={12}
  171. fill="black"
  172. stroke="none"
  173. alignmentBaseline="text-before-edge"
  174. pointerEvents="none"
  175. >
  176. {children}
  177. </text>
  178. )
  179. }
  180. function pp(n: number[]) {
  181. return '[' + n.map((v) => v.toFixed(1)).join(', ') + ']'
  182. }
  183. export { HoverIndicator }
  184. export default memo(Shape)