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.

state.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import { createSelectorHook, createState } from "@state-designer/react"
  2. import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
  3. import * as vec from "utils/vec"
  4. import {
  5. Data,
  6. PointerInfo,
  7. Shape,
  8. ShapeType,
  9. Shapes,
  10. TransformCorner,
  11. TransformEdge,
  12. } from "types"
  13. import { defaultDocument } from "./data"
  14. import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
  15. import history from "state/history"
  16. import * as Sessions from "./sessions"
  17. import commands from "./commands"
  18. const initialData: Data = {
  19. isReadOnly: false,
  20. settings: {
  21. fontSize: 13,
  22. darkMode: false,
  23. },
  24. camera: {
  25. point: [0, 0],
  26. zoom: 1,
  27. },
  28. brush: undefined,
  29. pointedId: null,
  30. hoveredId: null,
  31. selectedIds: new Set([]),
  32. currentPageId: "page0",
  33. document: defaultDocument,
  34. }
  35. const state = createState({
  36. data: initialData,
  37. on: {
  38. ZOOMED_CAMERA: {
  39. do: "zoomCamera",
  40. },
  41. PANNED_CAMERA: {
  42. do: "panCamera",
  43. },
  44. SELECTED_SELECT_TOOL: { to: "selecting" },
  45. SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "creatingDot" },
  46. SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "creatingCircle" },
  47. SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "creatingEllipse" },
  48. SELECTED_RAY_TOOL: { unless: "isReadOnly", to: "creatingRay" },
  49. SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "creatingLine" },
  50. SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "creatingPolyline" },
  51. SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "creatingRectangle" },
  52. },
  53. initial: "selecting",
  54. states: {
  55. selecting: {
  56. on: {
  57. UNDO: { do: "undo" },
  58. REDO: { do: "redo" },
  59. CANCELLED: { do: "clearSelectedIds" },
  60. DELETED: { do: "deleteSelectedIds" },
  61. GENERATED_SHAPES_FROM_CODE: "setGeneratedShapes",
  62. INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
  63. DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
  64. },
  65. initial: "notPointing",
  66. states: {
  67. notPointing: {
  68. on: {
  69. POINTED_CANVAS: { to: "brushSelecting" },
  70. POINTED_BOUNDS: { to: "pointingBounds" },
  71. POINTED_BOUNDS_EDGE: { to: "transformingSelection" },
  72. POINTED_BOUNDS_CORNER: { to: "transformingSelection" },
  73. MOVED_OVER_SHAPE: {
  74. if: "pointHitsShape",
  75. then: {
  76. unless: "shapeIsHovered",
  77. do: "setHoveredId",
  78. },
  79. else: { if: "shapeIsHovered", do: "clearHoveredId" },
  80. },
  81. UNHOVERED_SHAPE: "clearHoveredId",
  82. POINTED_SHAPE: [
  83. "setPointedId",
  84. {
  85. if: "isPressingShiftKey",
  86. then: {
  87. if: "isPointedShapeSelected",
  88. do: "pullPointedIdFromSelectedIds",
  89. else: {
  90. do: "pushPointedIdToSelectedIds",
  91. to: "pointingBounds",
  92. },
  93. },
  94. else: [
  95. {
  96. unless: "isPointedShapeSelected",
  97. do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
  98. },
  99. {
  100. to: "pointingBounds",
  101. },
  102. ],
  103. },
  104. ],
  105. },
  106. },
  107. pointingBounds: {
  108. on: {
  109. STOPPED_POINTING: [
  110. {
  111. unless: ["isPointingBounds", "isPressingShiftKey"],
  112. do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
  113. },
  114. { to: "notPointing" },
  115. ],
  116. MOVED_POINTER: {
  117. unless: "isReadOnly",
  118. if: "distanceImpliesDrag",
  119. to: "draggingSelection",
  120. },
  121. },
  122. },
  123. transformingSelection: {
  124. onEnter: "startTransformSession",
  125. on: {
  126. MOVED_POINTER: "updateTransformSession",
  127. PANNED_CAMERA: "updateTransformSession",
  128. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  129. CANCELLED: { do: "cancelSession", to: "selecting" },
  130. },
  131. },
  132. draggingSelection: {
  133. onEnter: "startTranslateSession",
  134. on: {
  135. MOVED_POINTER: "updateTranslateSession",
  136. PANNED_CAMERA: "updateTranslateSession",
  137. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  138. CANCELLED: { do: "cancelSession", to: "selecting" },
  139. },
  140. },
  141. brushSelecting: {
  142. onEnter: [
  143. { unless: "isPressingShiftKey", do: "clearSelectedIds" },
  144. "startBrushSession",
  145. ],
  146. on: {
  147. MOVED_POINTER: "updateBrushSession",
  148. PANNED_CAMERA: "updateBrushSession",
  149. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  150. CANCELLED: { do: "cancelSession", to: "selecting" },
  151. },
  152. },
  153. },
  154. },
  155. creatingDot: {
  156. initial: "creating",
  157. states: {
  158. creating: {
  159. on: {
  160. POINTED_CANVAS: {
  161. do: "createDot",
  162. to: "creatingDot.positioning",
  163. },
  164. },
  165. },
  166. positioning: {
  167. onEnter: "startTranslateSession",
  168. on: {
  169. MOVED_POINTER: "updateTranslateSession",
  170. PANNED_CAMERA: "updateTranslateSession",
  171. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  172. CANCELLED: {
  173. do: ["cancelSession", "deleteSelectedIds"],
  174. to: "selecting",
  175. },
  176. },
  177. },
  178. },
  179. },
  180. creatingCircle: {},
  181. creatingEllipse: {},
  182. creatingRay: {},
  183. creatingLine: {},
  184. creatingPolyline: {},
  185. creatingRectangle: {},
  186. },
  187. conditions: {
  188. isPointingBounds(data, payload: PointerInfo) {
  189. return payload.target === "bounds"
  190. },
  191. isReadOnly(data) {
  192. return data.isReadOnly
  193. },
  194. distanceImpliesDrag(data, payload: PointerInfo) {
  195. return vec.dist2(payload.origin, payload.point) > 16
  196. },
  197. isPointedShapeSelected(data) {
  198. return data.selectedIds.has(data.pointedId)
  199. },
  200. isPressingShiftKey(data, payload: { shiftKey: boolean }) {
  201. return payload.shiftKey
  202. },
  203. shapeIsHovered(data, payload: { target: string }) {
  204. return data.hoveredId === payload.target
  205. },
  206. pointHitsShape(data, payload: { target: string; point: number[] }) {
  207. const shape =
  208. data.document.pages[data.currentPageId].shapes[payload.target]
  209. return getShapeUtils(shape).hitTest(
  210. shape,
  211. screenToWorld(payload.point, data)
  212. )
  213. },
  214. },
  215. actions: {
  216. // Shapes
  217. createDot(data, payload: PointerInfo) {
  218. const shape = shapeUtilityMap[ShapeType.Dot].create({
  219. point: screenToWorld(payload.point, data),
  220. })
  221. commands.createShape(data, shape)
  222. },
  223. // History
  224. enableHistory() {
  225. history.enable()
  226. },
  227. disableHistory() {
  228. history.disable()
  229. },
  230. undo(data) {
  231. history.undo(data)
  232. },
  233. redo(data) {
  234. history.redo(data)
  235. },
  236. // Code
  237. setGeneratedShapes(data, payload: { shapes: Shape[] }) {
  238. commands.generateShapes(data, data.currentPageId, payload.shapes)
  239. },
  240. increaseCodeFontSize(data) {
  241. data.settings.fontSize++
  242. },
  243. decreaseCodeFontSize(data) {
  244. data.settings.fontSize--
  245. },
  246. // Sessions
  247. cancelSession(data) {
  248. session.cancel(data)
  249. session = undefined
  250. },
  251. completeSession(data) {
  252. session.complete(data)
  253. session = undefined
  254. },
  255. // Brushing
  256. startBrushSession(data, payload: PointerInfo) {
  257. session = new Sessions.BrushSession(
  258. data,
  259. screenToWorld(payload.point, data)
  260. )
  261. },
  262. updateBrushSession(data, payload: PointerInfo) {
  263. session.update(data, screenToWorld(payload.point, data))
  264. },
  265. // Dragging / Translating
  266. startTranslateSession(data, payload: PointerInfo) {
  267. session = new Sessions.TranslateSession(
  268. data,
  269. screenToWorld(payload.point, data)
  270. )
  271. },
  272. updateTranslateSession(data, payload: PointerInfo) {
  273. session.update(data, screenToWorld(payload.point, data))
  274. },
  275. // Dragging / Translating
  276. startTransformSession(
  277. data,
  278. payload: PointerInfo & { target: TransformCorner | TransformEdge }
  279. ) {
  280. session = new Sessions.TransformSession(
  281. data,
  282. payload.target,
  283. screenToWorld(payload.point, data)
  284. )
  285. },
  286. updateTransformSession(data, payload: PointerInfo) {
  287. session.update(data, screenToWorld(payload.point, data))
  288. },
  289. // Selection
  290. deleteSelectedIds(data) {
  291. const { document, currentPageId } = data
  292. const shapes = document.pages[currentPageId].shapes
  293. data.selectedIds.forEach((id) => {
  294. delete shapes[id]
  295. // TODO: recursively delete children
  296. })
  297. data.selectedIds.clear()
  298. data.hoveredId = undefined
  299. data.pointedId = undefined
  300. },
  301. setHoveredId(data, payload: PointerInfo) {
  302. data.hoveredId = payload.target
  303. },
  304. clearHoveredId(data) {
  305. data.hoveredId = undefined
  306. },
  307. setPointedId(data, payload: PointerInfo) {
  308. data.pointedId = payload.target
  309. },
  310. clearPointedId(data) {
  311. data.pointedId = undefined
  312. },
  313. clearSelectedIds(data) {
  314. data.selectedIds.clear()
  315. },
  316. pullPointedIdFromSelectedIds(data) {
  317. const { selectedIds, pointedId } = data
  318. selectedIds.delete(pointedId)
  319. },
  320. pushPointedIdToSelectedIds(data) {
  321. data.selectedIds.add(data.pointedId)
  322. },
  323. // Camera
  324. zoomCamera(data, payload: { delta: number; point: number[] }) {
  325. const { camera } = data
  326. const p0 = screenToWorld(payload.point, data)
  327. camera.zoom = clamp(
  328. camera.zoom - (payload.delta / 100) * camera.zoom,
  329. 0.5,
  330. 3
  331. )
  332. const p1 = screenToWorld(payload.point, data)
  333. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  334. document.documentElement.style.setProperty(
  335. "--camera-zoom",
  336. camera.zoom.toString()
  337. )
  338. },
  339. panCamera(data, payload: { delta: number[]; point: number[] }) {
  340. const { camera } = data
  341. data.camera.point = vec.sub(
  342. camera.point,
  343. vec.div(payload.delta, camera.zoom)
  344. )
  345. },
  346. },
  347. values: {
  348. selectedIds(data) {
  349. return new Set(data.selectedIds)
  350. },
  351. selectedBounds(data) {
  352. const {
  353. selectedIds,
  354. currentPageId,
  355. document: { pages },
  356. } = data
  357. const shapes = Array.from(selectedIds.values()).map(
  358. (id) => pages[currentPageId].shapes[id]
  359. )
  360. if (selectedIds.size === 0) return null
  361. if (selectedIds.size === 1 && !getShapeUtils(shapes[0]).canTransform) {
  362. return null
  363. }
  364. return getCommonBounds(
  365. ...shapes.map((shape) => getShapeUtils(shape).getBounds(shape))
  366. )
  367. },
  368. },
  369. })
  370. let session: Sessions.BaseSession
  371. export default state
  372. export const useSelector = createSelectorHook(state)