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.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import React, { useCallback, useRef } from "react"
  2. import state, { useSelector } from "state"
  3. import styled from "styles"
  4. import { getPointerEventInfo } from "utils/utils"
  5. import { memo } from "react"
  6. import Shapes from "lib/shapes"
  7. /*
  8. Gets the shape from the current page's shapes, using the
  9. provided ID. Depending on the shape's type, return the
  10. component for that type.
  11. This component takes an SVG shape as its children. It handles
  12. events for the shape as well as provides indicators for hover
  13. and selected status
  14. */
  15. function Shape({ id }: { id: string }) {
  16. const rGroup = useRef<SVGGElement>(null)
  17. const shape = useSelector((state) => {
  18. const { currentPageId, document } = state.data
  19. return document.pages[currentPageId].shapes[id]
  20. })
  21. const isSelected = useSelector((state) => state.values.selectedIds.has(id))
  22. const handlePointerDown = useCallback(
  23. (e: React.PointerEvent) => {
  24. e.stopPropagation()
  25. rGroup.current.setPointerCapture(e.pointerId)
  26. state.send("POINTED_SHAPE", { id, ...getPointerEventInfo(e) })
  27. },
  28. [id]
  29. )
  30. const handlePointerUp = useCallback(
  31. (e: React.PointerEvent) => {
  32. e.stopPropagation()
  33. rGroup.current.releasePointerCapture(e.pointerId)
  34. state.send("STOPPED_POINTING_SHAPE", { id, ...getPointerEventInfo(e) })
  35. },
  36. [id]
  37. )
  38. const handlePointerEnter = useCallback(
  39. (e: React.PointerEvent) =>
  40. state.send("HOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
  41. [id]
  42. )
  43. const handlePointerLeave = useCallback(
  44. (e: React.PointerEvent) =>
  45. state.send("UNHOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
  46. [id]
  47. )
  48. return (
  49. <StyledGroup
  50. ref={rGroup}
  51. isSelected={isSelected}
  52. transform={`translate(${shape.point})`}
  53. onPointerDown={handlePointerDown}
  54. onPointerUp={handlePointerUp}
  55. onPointerEnter={handlePointerEnter}
  56. onPointerLeave={handlePointerLeave}
  57. >
  58. <defs>
  59. {Shapes[shape.type] ? Shapes[shape.type].render(shape) : null}
  60. </defs>
  61. <HoverIndicator as="use" xlinkHref={"#" + id} />
  62. <use xlinkHref={"#" + id} {...shape.style} />
  63. <Indicator as="use" xlinkHref={"#" + id} />
  64. </StyledGroup>
  65. )
  66. }
  67. const Indicator = styled("path", {
  68. fill: "none",
  69. stroke: "transparent",
  70. strokeWidth: "max(1, calc(2 / var(--camera-zoom)))",
  71. pointerEvents: "none",
  72. strokeLineCap: "round",
  73. strokeLinejoin: "round",
  74. })
  75. const HoverIndicator = styled("path", {
  76. fill: "none",
  77. stroke: "transparent",
  78. strokeWidth: "max(1, calc(8 / var(--camera-zoom)))",
  79. pointerEvents: "all",
  80. strokeLinecap: "round",
  81. strokeLinejoin: "round",
  82. })
  83. const StyledGroup = styled("g", {
  84. [`& ${HoverIndicator}`]: {
  85. opacity: "0",
  86. },
  87. variants: {
  88. isSelected: {
  89. true: {
  90. [`& ${Indicator}`]: {
  91. stroke: "$selected",
  92. },
  93. [`&:hover ${HoverIndicator}`]: {
  94. opacity: "1",
  95. stroke: "$hint",
  96. },
  97. },
  98. false: {
  99. [`&:hover ${HoverIndicator}`]: {
  100. opacity: "1",
  101. stroke: "$hint",
  102. },
  103. },
  104. },
  105. },
  106. })
  107. export { Indicator, HoverIndicator }
  108. export default memo(Shape)