Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

state.ts 27KB

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