You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

state.ts 28KB

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