Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

state.ts 28KB

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