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.

bounds.tsx 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import state, { useSelector } from "state"
  2. import { motion } from "framer-motion"
  3. import styled from "styles"
  4. import inputs from "state/inputs"
  5. import { useRef } from "react"
  6. export default function Bounds() {
  7. const zoom = useSelector((state) => state.data.camera.zoom)
  8. const bounds = useSelector((state) => state.values.selectedBounds)
  9. const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
  10. if (!bounds) return null
  11. const { minX, minY, maxX, maxY, width, height } = bounds
  12. const p = 4 / zoom
  13. const cp = p * 2
  14. return (
  15. <g pointerEvents={isBrushing ? "none" : "all"}>
  16. <StyledBounds
  17. x={minX}
  18. y={minY}
  19. width={width}
  20. height={height}
  21. pointerEvents="none"
  22. />
  23. {width * zoom > 8 && (
  24. <>
  25. <Corner
  26. x={minX}
  27. y={minY}
  28. width={cp}
  29. height={cp}
  30. corner="top_left_corner"
  31. />
  32. <Corner
  33. x={maxX}
  34. y={minY}
  35. width={cp}
  36. height={cp}
  37. corner="top_right_corner"
  38. />
  39. <Corner
  40. x={maxX}
  41. y={maxY}
  42. width={cp}
  43. height={cp}
  44. corner="bottom_right_corner"
  45. />
  46. <Corner
  47. x={minX}
  48. y={maxY}
  49. width={cp}
  50. height={cp}
  51. corner="bottom_left_corner"
  52. />
  53. <EdgeHorizontal
  54. x={minX + p}
  55. y={minY}
  56. width={Math.max(0, width - p * 2)}
  57. height={p}
  58. edge="top_edge"
  59. />
  60. <EdgeVertical
  61. x={maxX}
  62. y={minY + p}
  63. width={p}
  64. height={Math.max(0, height - p * 2)}
  65. edge="right_edge"
  66. />
  67. <EdgeHorizontal
  68. x={minX + p}
  69. y={maxY}
  70. width={Math.max(0, width - p * 2)}
  71. height={p}
  72. edge="bottom_edge"
  73. />
  74. <EdgeVertical
  75. x={minX}
  76. y={minY + p}
  77. width={p}
  78. height={Math.max(0, height - p * 2)}
  79. edge="left_edge"
  80. />
  81. </>
  82. )}
  83. </g>
  84. )
  85. }
  86. function Corner({
  87. x,
  88. y,
  89. width,
  90. height,
  91. corner,
  92. }: {
  93. x: number
  94. y: number
  95. width: number
  96. height: number
  97. corner:
  98. | "top_left_corner"
  99. | "top_right_corner"
  100. | "bottom_right_corner"
  101. | "bottom_left_corner"
  102. }) {
  103. const rRotateCorner = useRef<SVGRectElement>(null)
  104. const rCorner = useRef<SVGRectElement>(null)
  105. const isTop = corner.includes("top")
  106. const isLeft = corner.includes("bottom")
  107. return (
  108. <g>
  109. <StyledRotateCorner
  110. ref={rRotateCorner}
  111. x={x + width * (isLeft ? -1.25 : -0.5)}
  112. y={y + width * (isTop ? -1.25 : -0.5)}
  113. width={width * 1.75}
  114. height={height * 1.75}
  115. onPointerDown={(e) => {
  116. e.stopPropagation()
  117. rRotateCorner.current.setPointerCapture(e.pointerId)
  118. state.send("POINTED_ROTATE_CORNER", inputs.pointerDown(e, corner))
  119. }}
  120. onPointerUp={(e) => {
  121. e.stopPropagation()
  122. rRotateCorner.current.releasePointerCapture(e.pointerId)
  123. rRotateCorner.current.replaceWith(rRotateCorner.current)
  124. state.send("STOPPED_POINTING", inputs.pointerDown(e, corner))
  125. }}
  126. />
  127. <StyledCorner
  128. ref={rCorner}
  129. x={x + width * -0.5}
  130. y={y + height * -0.5}
  131. width={width}
  132. height={height}
  133. corner={corner}
  134. onPointerDown={(e) => {
  135. e.stopPropagation()
  136. rCorner.current.setPointerCapture(e.pointerId)
  137. state.send("POINTED_BOUNDS_CORNER", inputs.pointerDown(e, corner))
  138. }}
  139. onPointerUp={(e) => {
  140. e.stopPropagation()
  141. rCorner.current.releasePointerCapture(e.pointerId)
  142. rCorner.current.replaceWith(rCorner.current)
  143. state.send("STOPPED_POINTING", inputs.pointerDown(e, corner))
  144. }}
  145. />
  146. </g>
  147. )
  148. }
  149. function EdgeHorizontal({
  150. x,
  151. y,
  152. width,
  153. height,
  154. edge,
  155. }: {
  156. x: number
  157. y: number
  158. width: number
  159. height: number
  160. edge: "top_edge" | "bottom_edge"
  161. }) {
  162. const rEdge = useRef<SVGRectElement>(null)
  163. return (
  164. <StyledEdge
  165. ref={rEdge}
  166. x={x}
  167. y={y - height / 2}
  168. width={width}
  169. height={height}
  170. onPointerDown={(e) => {
  171. e.stopPropagation()
  172. rEdge.current.setPointerCapture(e.pointerId)
  173. state.send("POINTED_BOUNDS_EDGE", inputs.pointerDown(e, edge))
  174. }}
  175. onPointerUp={(e) => {
  176. e.stopPropagation()
  177. e.preventDefault()
  178. state.send("STOPPED_POINTING", inputs.pointerUp(e))
  179. rEdge.current.releasePointerCapture(e.pointerId)
  180. rEdge.current.replaceWith(rEdge.current)
  181. }}
  182. edge={edge}
  183. />
  184. )
  185. }
  186. function EdgeVertical({
  187. x,
  188. y,
  189. width,
  190. height,
  191. edge,
  192. }: {
  193. x: number
  194. y: number
  195. width: number
  196. height: number
  197. edge: "right_edge" | "left_edge"
  198. }) {
  199. const rEdge = useRef<SVGRectElement>(null)
  200. return (
  201. <StyledEdge
  202. ref={rEdge}
  203. x={x - width / 2}
  204. y={y}
  205. width={width}
  206. height={height}
  207. onPointerDown={(e) => {
  208. e.stopPropagation()
  209. state.send("POINTED_BOUNDS_EDGE", inputs.pointerDown(e, edge))
  210. rEdge.current.setPointerCapture(e.pointerId)
  211. }}
  212. onPointerUp={(e) => {
  213. e.stopPropagation()
  214. state.send("STOPPED_POINTING", inputs.pointerUp(e))
  215. rEdge.current.releasePointerCapture(e.pointerId)
  216. rEdge.current.replaceWith(rEdge.current)
  217. }}
  218. edge={edge}
  219. />
  220. )
  221. }
  222. function restoreCursor(e: PointerEvent) {
  223. state.send("STOPPED_POINTING", { id: "bounds", ...inputs.pointerUp(e) })
  224. document.body.style.cursor = "default"
  225. }
  226. const StyledEdge = styled("rect", {
  227. stroke: "none",
  228. fill: "none",
  229. variants: {
  230. edge: {
  231. bottom_edge: { cursor: "ns-resize" },
  232. right_edge: { cursor: "ew-resize" },
  233. top_edge: { cursor: "ns-resize" },
  234. left_edge: { cursor: "ew-resize" },
  235. },
  236. },
  237. })
  238. const StyledCorner = styled("rect", {
  239. stroke: "$bounds",
  240. fill: "#fff",
  241. zStrokeWidth: 2,
  242. variants: {
  243. corner: {
  244. top_left_corner: { cursor: "nwse-resize" },
  245. top_right_corner: { cursor: "nesw-resize" },
  246. bottom_right_corner: { cursor: "nwse-resize" },
  247. bottom_left_corner: { cursor: "nesw-resize" },
  248. },
  249. },
  250. })
  251. const StyledBounds = styled("rect", {
  252. fill: "none",
  253. stroke: "$bounds",
  254. zStrokeWidth: 2,
  255. })
  256. const StyledRotateCorner = styled("rect", {
  257. cursor: "grab",
  258. fill: "transparent",
  259. })