Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

state.ts 26KB

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