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

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