選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

state.ts 24KB

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