Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

state.ts 26KB

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