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 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. import { createSelectorHook, createState } from "@state-designer/react"
  2. import {
  3. clamp,
  4. getCommonBounds,
  5. getPage,
  6. getShape,
  7. screenToWorld,
  8. } from "utils/utils"
  9. import * as vec from "utils/vec"
  10. import {
  11. Data,
  12. PointerInfo,
  13. Shape,
  14. ShapeType,
  15. Corner,
  16. Edge,
  17. CodeControl,
  18. } from "types"
  19. import inputs from "./inputs"
  20. import { defaultDocument } from "./data"
  21. import shapeUtilityMap, { getShapeUtils } from "lib/shape-utils"
  22. import history from "state/history"
  23. import * as Sessions from "./sessions"
  24. import commands from "./commands"
  25. import { updateFromCode } from "lib/code/generate"
  26. const initialData: Data = {
  27. isReadOnly: false,
  28. settings: {
  29. fontSize: 13,
  30. isDarkMode: false,
  31. isCodeOpen: false,
  32. },
  33. camera: {
  34. point: [0, 0],
  35. zoom: 1,
  36. },
  37. brush: undefined,
  38. boundsRotation: 0,
  39. pointedId: null,
  40. hoveredId: null,
  41. selectedIds: new Set([]),
  42. currentPageId: "page0",
  43. currentCodeFileId: "file0",
  44. codeControls: {},
  45. document: defaultDocument,
  46. }
  47. const state = createState({
  48. data: initialData,
  49. on: {
  50. ZOOMED_CAMERA: {
  51. do: "zoomCamera",
  52. },
  53. PANNED_CAMERA: {
  54. do: "panCamera",
  55. },
  56. SELECTED_SELECT_TOOL: { to: "selecting" },
  57. SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "dot" },
  58. SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "circle" },
  59. SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "ellipse" },
  60. SELECTED_RAY_TOOL: { unless: "isReadOnly", to: "ray" },
  61. SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
  62. SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
  63. SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
  64. TOGGLED_CODE_PANEL_OPEN: "toggleCodePanel",
  65. RESET_CAMERA: "resetCamera",
  66. },
  67. initial: "loading",
  68. states: {
  69. loading: {
  70. on: {
  71. MOUNTED: {
  72. do: "restoreSavedData",
  73. to: "ready",
  74. },
  75. },
  76. },
  77. ready: {
  78. on: {
  79. UNMOUNTED: [
  80. { unless: "isReadOnly", do: "forceSave" },
  81. { to: "loading" },
  82. ],
  83. },
  84. initial: "selecting",
  85. states: {
  86. selecting: {
  87. on: {
  88. SAVED: "forceSave",
  89. UNDO: { do: "undo" },
  90. REDO: { do: "redo" },
  91. CANCELLED: { do: "clearSelectedIds" },
  92. DELETED: { do: "deleteSelectedIds" },
  93. SAVED_CODE: "saveCode",
  94. GENERATED_FROM_CODE: ["setCodeControls", "setGeneratedShapes"],
  95. INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
  96. DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
  97. CHANGED_CODE_CONTROL: "updateControls",
  98. },
  99. initial: "notPointing",
  100. states: {
  101. notPointing: {
  102. on: {
  103. SELECTED_ALL: "selectAll",
  104. POINTED_CANVAS: { to: "brushSelecting" },
  105. POINTED_BOUNDS: { to: "pointingBounds" },
  106. POINTED_BOUNDS_HANDLE: {
  107. if: "isPointingRotationHandle",
  108. to: "rotatingSelection",
  109. else: { to: "transformingSelection" },
  110. },
  111. MOVED_OVER_SHAPE: {
  112. if: "pointHitsShape",
  113. then: {
  114. unless: "shapeIsHovered",
  115. do: "setHoveredId",
  116. },
  117. else: { if: "shapeIsHovered", do: "clearHoveredId" },
  118. },
  119. UNHOVERED_SHAPE: "clearHoveredId",
  120. POINTED_SHAPE: [
  121. {
  122. if: "isPressingMetaKey",
  123. to: "brushSelecting",
  124. },
  125. "setPointedId",
  126. {
  127. unless: "isPointedShapeSelected",
  128. then: {
  129. if: "isPressingShiftKey",
  130. do: ["pushPointedIdToSelectedIds", "clearPointedId"],
  131. else: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
  132. },
  133. },
  134. {
  135. to: "pointingBounds",
  136. },
  137. ],
  138. },
  139. },
  140. pointingBounds: {
  141. on: {
  142. STOPPED_POINTING: [
  143. {
  144. if: "isPressingShiftKey",
  145. then: {
  146. if: "isPointedShapeSelected",
  147. do: "pullPointedIdFromSelectedIds",
  148. },
  149. else: {
  150. unless: "isPointingBounds",
  151. do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
  152. },
  153. },
  154. { to: "notPointing" },
  155. ],
  156. MOVED_POINTER: {
  157. unless: "isReadOnly",
  158. if: "distanceImpliesDrag",
  159. to: "draggingSelection",
  160. },
  161. },
  162. },
  163. rotatingSelection: {
  164. onEnter: "startRotateSession",
  165. onExit: "clearBoundsRotation",
  166. on: {
  167. MOVED_POINTER: "updateRotateSession",
  168. PANNED_CAMERA: "updateRotateSession",
  169. PRESSED_SHIFT_KEY: "keyUpdateRotateSession",
  170. RELEASED_SHIFT_KEY: "keyUpdateRotateSession",
  171. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  172. CANCELLED: { do: "cancelSession", to: "selecting" },
  173. },
  174. },
  175. transformingSelection: {
  176. onEnter: "startTransformSession",
  177. on: {
  178. MOVED_POINTER: "updateTransformSession",
  179. PANNED_CAMERA: "updateTransformSession",
  180. PRESSED_SHIFT_KEY: "keyUpdateTransformSession",
  181. RELEASED_SHIFT_KEY: "keyUpdateTransformSession",
  182. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  183. CANCELLED: { do: "cancelSession", to: "selecting" },
  184. },
  185. },
  186. draggingSelection: {
  187. onEnter: "startTranslateSession",
  188. on: {
  189. MOVED_POINTER: "updateTranslateSession",
  190. PANNED_CAMERA: "updateTranslateSession",
  191. PRESSED_SHIFT_KEY: "keyUpdateTranslateSession",
  192. RELEASED_SHIFT_KEY: "keyUpdateTranslateSession",
  193. PRESSED_ALT_KEY: "keyUpdateTranslateSession",
  194. RELEASED_ALT_KEY: "keyUpdateTranslateSession",
  195. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  196. CANCELLED: { do: "cancelSession", to: "selecting" },
  197. },
  198. },
  199. brushSelecting: {
  200. onEnter: [
  201. {
  202. unless: ["isPressingMetaKey", "isPressingShiftKey"],
  203. do: "clearSelectedIds",
  204. },
  205. "clearBoundsRotation",
  206. "startBrushSession",
  207. ],
  208. on: {
  209. MOVED_POINTER: "updateBrushSession",
  210. PANNED_CAMERA: "updateBrushSession",
  211. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  212. CANCELLED: { do: "cancelSession", to: "selecting" },
  213. },
  214. },
  215. },
  216. },
  217. dot: {
  218. initial: "creating",
  219. states: {
  220. creating: {
  221. on: {
  222. POINTED_CANVAS: {
  223. do: "createDot",
  224. to: "dot.editing",
  225. },
  226. },
  227. },
  228. editing: {
  229. on: {
  230. STOPPED_POINTING: { do: "completeSession", to: "selecting" },
  231. CANCELLED: {
  232. do: ["cancelSession", "deleteSelectedIds"],
  233. to: "selecting",
  234. },
  235. },
  236. initial: "inactive",
  237. states: {
  238. inactive: {
  239. on: {
  240. MOVED_POINTER: {
  241. if: "distanceImpliesDrag",
  242. to: "dot.editing.active",
  243. },
  244. },
  245. },
  246. active: {
  247. onEnter: "startTranslateSession",
  248. on: {
  249. MOVED_POINTER: "updateTranslateSession",
  250. PANNED_CAMERA: "updateTranslateSession",
  251. },
  252. },
  253. },
  254. },
  255. },
  256. },
  257. circle: {
  258. initial: "creating",
  259. states: {
  260. creating: {
  261. on: {
  262. POINTED_CANVAS: {
  263. to: "circle.editing",
  264. },
  265. },
  266. },
  267. editing: {
  268. on: {
  269. STOPPED_POINTING: { to: "selecting" },
  270. CANCELLED: { to: "selecting" },
  271. MOVED_POINTER: {
  272. if: "distanceImpliesDrag",
  273. do: "createCircle",
  274. to: "drawingShape.bounds",
  275. },
  276. },
  277. },
  278. },
  279. },
  280. ellipse: {
  281. initial: "creating",
  282. states: {
  283. creating: {
  284. on: {
  285. CANCELLED: { to: "selecting" },
  286. POINTED_CANVAS: {
  287. to: "ellipse.editing",
  288. },
  289. },
  290. },
  291. editing: {
  292. on: {
  293. STOPPED_POINTING: { to: "selecting" },
  294. CANCELLED: { to: "selecting" },
  295. MOVED_POINTER: {
  296. if: "distanceImpliesDrag",
  297. do: "createEllipse",
  298. to: "drawingShape.bounds",
  299. },
  300. },
  301. },
  302. },
  303. },
  304. rectangle: {
  305. initial: "creating",
  306. states: {
  307. creating: {
  308. on: {
  309. CANCELLED: { to: "selecting" },
  310. POINTED_CANVAS: {
  311. to: "rectangle.editing",
  312. },
  313. },
  314. },
  315. editing: {
  316. on: {
  317. STOPPED_POINTING: { to: "selecting" },
  318. CANCELLED: { to: "selecting" },
  319. MOVED_POINTER: {
  320. if: "distanceImpliesDrag",
  321. do: "createRectangle",
  322. to: "drawingShape.bounds",
  323. },
  324. },
  325. },
  326. },
  327. },
  328. ray: {
  329. initial: "creating",
  330. states: {
  331. creating: {
  332. on: {
  333. CANCELLED: { to: "selecting" },
  334. POINTED_CANVAS: {
  335. do: "createRay",
  336. to: "ray.editing",
  337. },
  338. },
  339. },
  340. editing: {
  341. on: {
  342. STOPPED_POINTING: { to: "selecting" },
  343. CANCELLED: { to: "selecting" },
  344. MOVED_POINTER: {
  345. if: "distanceImpliesDrag",
  346. to: "drawingShape.direction",
  347. },
  348. },
  349. },
  350. },
  351. },
  352. line: {
  353. initial: "creating",
  354. states: {
  355. creating: {
  356. on: {
  357. CANCELLED: { to: "selecting" },
  358. POINTED_CANVAS: {
  359. do: "createLine",
  360. to: "line.editing",
  361. },
  362. },
  363. },
  364. editing: {
  365. on: {
  366. STOPPED_POINTING: { to: "selecting" },
  367. CANCELLED: { to: "selecting" },
  368. MOVED_POINTER: {
  369. if: "distanceImpliesDrag",
  370. to: "drawingShape.direction",
  371. },
  372. },
  373. },
  374. },
  375. },
  376. polyline: {},
  377. },
  378. },
  379. drawingShape: {
  380. on: {
  381. STOPPED_POINTING: {
  382. do: "completeSession",
  383. to: "selecting",
  384. },
  385. CANCELLED: {
  386. do: ["cancelSession", "deleteSelectedIds"],
  387. to: "selecting",
  388. },
  389. },
  390. initial: "drawingShapeBounds",
  391. states: {
  392. bounds: {
  393. onEnter: "startDrawTransformSession",
  394. on: {
  395. MOVED_POINTER: "updateTransformSession",
  396. PANNED_CAMERA: "updateTransformSession",
  397. },
  398. },
  399. direction: {
  400. onEnter: "startDirectionSession",
  401. on: {
  402. MOVED_POINTER: "updateDirectionSession",
  403. PANNED_CAMERA: "updateDirectionSession",
  404. },
  405. },
  406. },
  407. },
  408. },
  409. conditions: {
  410. isPointingBounds(data, payload: PointerInfo) {
  411. return payload.target === "bounds"
  412. },
  413. isReadOnly(data) {
  414. return data.isReadOnly
  415. },
  416. distanceImpliesDrag(data, payload: PointerInfo) {
  417. return vec.dist2(payload.origin, payload.point) > 16
  418. },
  419. isPointedShapeSelected(data) {
  420. return data.selectedIds.has(data.pointedId)
  421. },
  422. isPressingShiftKey(data, payload: PointerInfo) {
  423. return payload.shiftKey
  424. },
  425. isPressingMetaKey(data, payload: PointerInfo) {
  426. return payload.metaKey
  427. },
  428. shapeIsHovered(data, payload: { target: string }) {
  429. return data.hoveredId === payload.target
  430. },
  431. pointHitsShape(data, payload: { target: string; point: number[] }) {
  432. const shape = getShape(data, payload.target)
  433. return getShapeUtils(shape).hitTest(
  434. shape,
  435. screenToWorld(payload.point, data)
  436. )
  437. },
  438. isPointingRotationHandle(
  439. data,
  440. payload: { target: Edge | Corner | "rotate" }
  441. ) {
  442. return payload.target === "rotate"
  443. },
  444. },
  445. actions: {
  446. /* --------------------- Shapes --------------------- */
  447. // Dot
  448. createDot(data, payload: PointerInfo) {
  449. const shape = shapeUtilityMap[ShapeType.Dot].create({
  450. point: screenToWorld(payload.point, data),
  451. })
  452. getPage(data).shapes[shape.id] = shape
  453. data.selectedIds.clear()
  454. data.selectedIds.add(shape.id)
  455. },
  456. // Ray
  457. createRay(data, payload: PointerInfo) {
  458. const shape = shapeUtilityMap[ShapeType.Ray].create({
  459. point: screenToWorld(payload.point, data),
  460. })
  461. getPage(data).shapes[shape.id] = shape
  462. data.selectedIds.clear()
  463. data.selectedIds.add(shape.id)
  464. },
  465. // Line
  466. createLine(data, payload: PointerInfo) {
  467. const shape = shapeUtilityMap[ShapeType.Line].create({
  468. point: screenToWorld(payload.point, data),
  469. direction: [0, 1],
  470. })
  471. getPage(data).shapes[shape.id] = shape
  472. data.selectedIds.clear()
  473. data.selectedIds.add(shape.id)
  474. },
  475. createCircle(data, payload: PointerInfo) {
  476. const shape = shapeUtilityMap[ShapeType.Circle].create({
  477. point: screenToWorld(payload.point, data),
  478. radius: 1,
  479. })
  480. getPage(data).shapes[shape.id] = shape
  481. data.selectedIds.clear()
  482. data.selectedIds.add(shape.id)
  483. },
  484. createEllipse(data, payload: PointerInfo) {
  485. const shape = shapeUtilityMap[ShapeType.Ellipse].create({
  486. point: screenToWorld(payload.point, data),
  487. radiusX: 1,
  488. radiusY: 1,
  489. })
  490. getPage(data).shapes[shape.id] = shape
  491. data.selectedIds.clear()
  492. data.selectedIds.add(shape.id)
  493. },
  494. createRectangle(data, payload: PointerInfo) {
  495. const shape = shapeUtilityMap[ShapeType.Rectangle].create({
  496. point: screenToWorld(payload.point, data),
  497. size: [1, 1],
  498. })
  499. getPage(data).shapes[shape.id] = shape
  500. data.selectedIds.clear()
  501. data.selectedIds.add(shape.id)
  502. },
  503. /* -------------------- Sessions -------------------- */
  504. // Shared
  505. cancelSession(data) {
  506. session?.cancel(data)
  507. session = undefined
  508. },
  509. completeSession(data) {
  510. session?.complete(data)
  511. session = undefined
  512. },
  513. // Brushing
  514. startBrushSession(data, payload: PointerInfo) {
  515. session = new Sessions.BrushSession(
  516. data,
  517. screenToWorld(payload.point, data)
  518. )
  519. },
  520. updateBrushSession(data, payload: PointerInfo) {
  521. session.update(data, screenToWorld(payload.point, data))
  522. },
  523. // Rotating
  524. startRotateSession(data, payload: PointerInfo) {
  525. session = new Sessions.RotateSession(
  526. data,
  527. screenToWorld(payload.point, data)
  528. )
  529. },
  530. keyUpdateRotateSession(data, payload: PointerInfo) {
  531. session.update(
  532. data,
  533. screenToWorld(inputs.pointer.point, data),
  534. payload.shiftKey
  535. )
  536. },
  537. updateRotateSession(data, payload: PointerInfo) {
  538. session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
  539. },
  540. // Dragging / Translating
  541. startTranslateSession(data, payload: PointerInfo) {
  542. session = new Sessions.TranslateSession(
  543. data,
  544. screenToWorld(payload.point, data),
  545. payload.altKey
  546. )
  547. },
  548. keyUpdateTranslateSession(
  549. data,
  550. payload: { shiftKey: boolean; altKey: boolean }
  551. ) {
  552. session.update(
  553. data,
  554. screenToWorld(inputs.pointer.point, data),
  555. payload.shiftKey,
  556. payload.altKey
  557. )
  558. },
  559. updateTranslateSession(data, payload: PointerInfo) {
  560. session.update(
  561. data,
  562. screenToWorld(payload.point, data),
  563. payload.shiftKey,
  564. payload.altKey
  565. )
  566. },
  567. // Dragging / Translating
  568. startTransformSession(
  569. data,
  570. payload: PointerInfo & { target: Corner | Edge }
  571. ) {
  572. session =
  573. data.selectedIds.size === 1
  574. ? new Sessions.TransformSingleSession(
  575. data,
  576. payload.target,
  577. screenToWorld(payload.point, data),
  578. false
  579. )
  580. : new Sessions.TransformSession(
  581. data,
  582. payload.target,
  583. screenToWorld(payload.point, data)
  584. )
  585. },
  586. startDrawTransformSession(data, payload: PointerInfo) {
  587. session = new Sessions.TransformSingleSession(
  588. data,
  589. Corner.BottomRight,
  590. screenToWorld(payload.point, data),
  591. true
  592. )
  593. },
  594. keyUpdateTransformSession(data, payload: PointerInfo) {
  595. session.update(
  596. data,
  597. screenToWorld(inputs.pointer.point, data),
  598. payload.shiftKey,
  599. payload.altKey
  600. )
  601. },
  602. updateTransformSession(data, payload: PointerInfo) {
  603. session.update(
  604. data,
  605. screenToWorld(payload.point, data),
  606. payload.shiftKey,
  607. payload.altKey
  608. )
  609. },
  610. // Direction
  611. startDirectionSession(data, payload: PointerInfo) {
  612. session = new Sessions.DirectionSession(
  613. data,
  614. screenToWorld(payload.point, data)
  615. )
  616. },
  617. updateDirectionSession(data, payload: PointerInfo) {
  618. session.update(data, screenToWorld(payload.point, data))
  619. },
  620. /* -------------------- Selection ------------------- */
  621. selectAll(data) {
  622. const { selectedIds } = data
  623. const page = getPage(data)
  624. selectedIds.clear()
  625. for (let id in page.shapes) {
  626. selectedIds.add(id)
  627. }
  628. },
  629. setHoveredId(data, payload: PointerInfo) {
  630. data.hoveredId = payload.target
  631. },
  632. clearHoveredId(data) {
  633. data.hoveredId = undefined
  634. },
  635. setPointedId(data, payload: PointerInfo) {
  636. data.pointedId = payload.target
  637. },
  638. clearPointedId(data) {
  639. data.pointedId = undefined
  640. },
  641. clearSelectedIds(data) {
  642. data.selectedIds.clear()
  643. },
  644. pullPointedIdFromSelectedIds(data) {
  645. const { selectedIds, pointedId } = data
  646. selectedIds.delete(pointedId)
  647. },
  648. pushPointedIdToSelectedIds(data) {
  649. data.selectedIds.add(data.pointedId)
  650. },
  651. // Camera
  652. resetCamera(data) {
  653. data.camera.zoom = 1
  654. data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
  655. document.documentElement.style.setProperty("--camera-zoom", "1")
  656. },
  657. centerCamera(data) {
  658. const { shapes } = getPage(data)
  659. getCommonBounds()
  660. data.camera.zoom = 1
  661. data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
  662. document.documentElement.style.setProperty("--camera-zoom", "1")
  663. },
  664. zoomCamera(data, payload: { delta: number; point: number[] }) {
  665. const { camera } = data
  666. const p0 = screenToWorld(payload.point, data)
  667. camera.zoom = clamp(
  668. camera.zoom - (payload.delta / 100) * camera.zoom,
  669. 0.5,
  670. 3
  671. )
  672. const p1 = screenToWorld(payload.point, data)
  673. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  674. document.documentElement.style.setProperty(
  675. "--camera-zoom",
  676. camera.zoom.toString()
  677. )
  678. },
  679. panCamera(data, payload: { delta: number[]; point: number[] }) {
  680. const { camera } = data
  681. data.camera.point = vec.sub(
  682. camera.point,
  683. vec.div(payload.delta, camera.zoom)
  684. )
  685. },
  686. deleteSelectedIds(data) {
  687. const page = getPage(data)
  688. data.hoveredId = undefined
  689. data.pointedId = undefined
  690. data.selectedIds.forEach((id) => {
  691. delete page.shapes[id]
  692. // TODO: recursively delete children
  693. })
  694. data.selectedIds.clear()
  695. },
  696. /* ---------------------- History ---------------------- */
  697. // History
  698. popHistory() {
  699. history.pop()
  700. },
  701. forceSave(data) {
  702. history.save(data)
  703. },
  704. enableHistory() {
  705. history.enable()
  706. },
  707. disableHistory() {
  708. history.disable()
  709. },
  710. undo(data) {
  711. history.undo(data)
  712. },
  713. redo(data) {
  714. history.redo(data)
  715. },
  716. /* ---------------------- Code ---------------------- */
  717. closeCodePanel(data) {
  718. data.settings.isCodeOpen = false
  719. },
  720. openCodePanel(data) {
  721. data.settings.isCodeOpen = true
  722. },
  723. toggleCodePanel(data) {
  724. data.settings.isCodeOpen = !data.settings.isCodeOpen
  725. },
  726. setGeneratedShapes(
  727. data,
  728. payload: { shapes: Shape[]; controls: CodeControl[] }
  729. ) {
  730. commands.generate(data, data.currentPageId, payload.shapes)
  731. },
  732. setCodeControls(data, payload: { controls: CodeControl[] }) {
  733. data.codeControls = Object.fromEntries(
  734. payload.controls.map((control) => [control.id, control])
  735. )
  736. },
  737. increaseCodeFontSize(data) {
  738. data.settings.fontSize++
  739. },
  740. decreaseCodeFontSize(data) {
  741. data.settings.fontSize--
  742. },
  743. updateControls(data, payload: { [key: string]: any }) {
  744. for (let key in payload) {
  745. data.codeControls[key].value = payload[key]
  746. }
  747. history.disable()
  748. data.selectedIds.clear()
  749. try {
  750. const { shapes } = updateFromCode(
  751. data.document.code[data.currentCodeFileId].code,
  752. data.codeControls
  753. )
  754. commands.generate(data, data.currentPageId, shapes)
  755. } catch (e) {
  756. console.error(e)
  757. }
  758. history.enable()
  759. },
  760. // Data
  761. saveCode(data, payload: { code: string }) {
  762. data.document.code[data.currentCodeFileId].code = payload.code
  763. history.save(data)
  764. },
  765. restoreSavedData(data) {
  766. history.load(data)
  767. },
  768. clearBoundsRotation(data) {
  769. data.boundsRotation = 0
  770. },
  771. },
  772. values: {
  773. selectedIds(data) {
  774. return new Set(data.selectedIds)
  775. },
  776. selectedBounds(data) {
  777. const { selectedIds } = data
  778. const page = getPage(data)
  779. const shapes = Array.from(selectedIds.values())
  780. .map((id) => page.shapes[id])
  781. .filter(Boolean)
  782. if (selectedIds.size === 0) return null
  783. if (selectedIds.size === 1) {
  784. if (!shapes[0]) {
  785. console.error("Could not find that shape! Clearing selected IDs.")
  786. data.selectedIds.clear()
  787. return null
  788. }
  789. const shapeUtils = getShapeUtils(shapes[0])
  790. if (!shapeUtils.canTransform) return null
  791. return shapeUtils.getBounds(shapes[0])
  792. }
  793. return getCommonBounds(
  794. ...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
  795. )
  796. },
  797. },
  798. })
  799. let session: Sessions.BaseSession
  800. export default state
  801. export const useSelector = createSelectorHook(state)