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.

shape.tsx 3.8KB

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