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

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