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

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