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

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