Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

state.ts 38KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302
  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. isToolLocked: false,
  44. isPenLocked: false,
  45. nudgeDistanceLarge: 10,
  46. nudgeDistanceSmall: 1,
  47. },
  48. currentStyle: {
  49. fill: shades.lightGray,
  50. stroke: shades.darkGray,
  51. strokeWidth: 2,
  52. },
  53. camera: {
  54. point: [0, 0],
  55. zoom: 1,
  56. },
  57. brush: undefined,
  58. boundsRotation: 0,
  59. pointedId: null,
  60. hoveredId: null,
  61. selectedIds: new Set([]),
  62. currentPageId: 'page0',
  63. currentCodeFileId: 'file0',
  64. codeControls: {},
  65. document: defaultDocument,
  66. }
  67. const state = createState({
  68. data: initialData,
  69. on: {
  70. UNMOUNTED: [{ unless: 'isReadOnly', do: 'forceSave' }, { to: 'loading' }],
  71. },
  72. initial: 'loading',
  73. states: {
  74. loading: {
  75. on: {
  76. MOUNTED: [
  77. 'restoreSavedData',
  78. {
  79. to: 'ready',
  80. },
  81. ],
  82. },
  83. },
  84. ready: {
  85. onEnter: {
  86. wait: 0.01,
  87. if: 'hasSelection',
  88. do: 'zoomCameraToSelectionActual',
  89. else: ['zoomCameraToFit', 'zoomCameraToActual'],
  90. },
  91. on: {
  92. ZOOMED_CAMERA: {
  93. do: 'zoomCamera',
  94. },
  95. PANNED_CAMERA: {
  96. do: 'panCamera',
  97. },
  98. ZOOMED_TO_ACTUAL: {
  99. if: 'hasSelection',
  100. do: 'zoomCameraToSelectionActual',
  101. else: 'zoomCameraToActual',
  102. },
  103. ZOOMED_TO_SELECTION: {
  104. if: 'hasSelection',
  105. do: 'zoomCameraToSelection',
  106. },
  107. ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
  108. ZOOMED_IN: 'zoomIn',
  109. ZOOMED_OUT: 'zoomOut',
  110. RESET_CAMERA: 'resetCamera',
  111. TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
  112. TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
  113. TOGGLED_SHAPE_ASPECT_LOCK: {
  114. if: 'hasSelection',
  115. do: 'aspectLockSelection',
  116. },
  117. SELECTED_SELECT_TOOL: { to: 'selecting' },
  118. SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
  119. SELECTED_ARROW_TOOL: { unless: 'isReadOnly', to: 'arrow' },
  120. SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
  121. SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
  122. SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
  123. SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
  124. SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
  125. SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
  126. SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
  127. TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
  128. TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
  129. CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
  130. SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
  131. NUDGED: { do: 'nudgeSelection' },
  132. USED_PEN_DEVICE: 'enablePenLock',
  133. DISABLED_PEN_LOCK: 'disablePenLock',
  134. CLEARED_PAGE: ['selectAll', 'deleteSelection'],
  135. },
  136. initial: 'selecting',
  137. states: {
  138. selecting: {
  139. on: {
  140. SAVED: 'forceSave',
  141. UNDO: 'undo',
  142. REDO: 'redo',
  143. SAVED_CODE: 'saveCode',
  144. DELETED: 'deleteSelection',
  145. INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
  146. DECREASED_CODE_FONT_SIZE: 'decreaseCodeFontSize',
  147. CHANGED_CODE_CONTROL: 'updateControls',
  148. GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
  149. TOGGLED_TOOL_LOCK: 'toggleToolLock',
  150. MOVED: { if: 'hasSelection', do: 'moveSelection' },
  151. ALIGNED: { if: 'hasSelection', do: 'alignSelection' },
  152. STRETCHED: { if: 'hasSelection', do: 'stretchSelection' },
  153. DISTRIBUTED: { if: 'hasSelection', do: 'distributeSelection' },
  154. DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' },
  155. ROTATED_CCW: { if: 'hasSelection', do: 'rotateSelectionCcw' },
  156. },
  157. initial: 'notPointing',
  158. states: {
  159. notPointing: {
  160. on: {
  161. CANCELLED: 'clearSelectedIds',
  162. STARTED_PINCHING: { to: 'pinching' },
  163. POINTED_CANVAS: { to: 'brushSelecting' },
  164. POINTED_BOUNDS: { to: 'pointingBounds' },
  165. POINTED_BOUNDS_HANDLE: {
  166. if: 'isPointingRotationHandle',
  167. to: 'rotatingSelection',
  168. else: { to: 'transformingSelection' },
  169. },
  170. POINTED_HANDLE: { to: 'translatingHandles' },
  171. MOVED_OVER_SHAPE: {
  172. if: 'pointHitsShape',
  173. then: {
  174. unless: 'shapeIsHovered',
  175. do: 'setHoveredId',
  176. },
  177. else: { if: 'shapeIsHovered', do: 'clearHoveredId' },
  178. },
  179. UNHOVERED_SHAPE: 'clearHoveredId',
  180. POINTED_SHAPE: [
  181. {
  182. if: 'isPressingMetaKey',
  183. to: 'brushSelecting',
  184. },
  185. 'setPointedId',
  186. {
  187. unless: 'isPointedShapeSelected',
  188. then: {
  189. if: 'isPressingShiftKey',
  190. do: ['pushPointedIdToSelectedIds', 'clearPointedId'],
  191. else: ['clearSelectedIds', 'pushPointedIdToSelectedIds'],
  192. },
  193. },
  194. {
  195. to: 'pointingBounds',
  196. },
  197. ],
  198. },
  199. },
  200. pointingBounds: {
  201. on: {
  202. STOPPED_POINTING: [
  203. {
  204. if: 'isPressingShiftKey',
  205. then: {
  206. if: 'isPointedShapeSelected',
  207. do: 'pullPointedIdFromSelectedIds',
  208. },
  209. else: {
  210. unless: 'isPointingBounds',
  211. do: ['clearSelectedIds', 'pushPointedIdToSelectedIds'],
  212. },
  213. },
  214. { to: 'notPointing' },
  215. ],
  216. MOVED_POINTER: {
  217. unless: 'isReadOnly',
  218. if: 'distanceImpliesDrag',
  219. to: 'translatingSelection',
  220. },
  221. },
  222. },
  223. rotatingSelection: {
  224. onEnter: 'startRotateSession',
  225. onExit: 'clearBoundsRotation',
  226. on: {
  227. MOVED_POINTER: 'updateRotateSession',
  228. PANNED_CAMERA: 'updateRotateSession',
  229. PRESSED_SHIFT_KEY: 'keyUpdateRotateSession',
  230. RELEASED_SHIFT_KEY: 'keyUpdateRotateSession',
  231. STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
  232. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  233. },
  234. },
  235. transformingSelection: {
  236. onEnter: 'startTransformSession',
  237. on: {
  238. MOVED_POINTER: 'updateTransformSession',
  239. PANNED_CAMERA: 'updateTransformSession',
  240. PRESSED_SHIFT_KEY: 'keyUpdateTransformSession',
  241. RELEASED_SHIFT_KEY: 'keyUpdateTransformSession',
  242. STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
  243. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  244. },
  245. },
  246. translatingSelection: {
  247. onEnter: 'startTranslateSession',
  248. on: {
  249. MOVED_POINTER: 'updateTranslateSession',
  250. PANNED_CAMERA: 'updateTranslateSession',
  251. PRESSED_SHIFT_KEY: 'keyUpdateTranslateSession',
  252. RELEASED_SHIFT_KEY: 'keyUpdateTranslateSession',
  253. PRESSED_ALT_KEY: 'keyUpdateTranslateSession',
  254. RELEASED_ALT_KEY: 'keyUpdateTranslateSession',
  255. STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
  256. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  257. },
  258. },
  259. translatingHandles: {
  260. onEnter: 'startHandleSession',
  261. on: {
  262. MOVED_POINTER: 'updateHandleSession',
  263. PANNED_CAMERA: 'updateHandleSession',
  264. PRESSED_SHIFT_KEY: 'keyUpdateHandleSession',
  265. RELEASED_SHIFT_KEY: 'keyUpdateHandleSession',
  266. STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
  267. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  268. },
  269. },
  270. brushSelecting: {
  271. onEnter: [
  272. {
  273. unless: ['isPressingMetaKey', 'isPressingShiftKey'],
  274. do: 'clearSelectedIds',
  275. },
  276. 'clearBoundsRotation',
  277. 'startBrushSession',
  278. ],
  279. on: {
  280. STARTED_PINCHING: { do: 'completeSession', to: 'pinching' },
  281. MOVED_POINTER: 'updateBrushSession',
  282. PANNED_CAMERA: 'updateBrushSession',
  283. STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
  284. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  285. },
  286. },
  287. },
  288. },
  289. pinching: {
  290. on: {
  291. PINCHED: { do: 'pinchCamera' },
  292. },
  293. initial: 'selectPinching',
  294. states: {
  295. selectPinching: {
  296. on: {
  297. STOPPED_PINCHING: { to: 'selecting' },
  298. },
  299. },
  300. toolPinching: {
  301. on: {
  302. STOPPED_PINCHING: { to: 'usingTool.previous' },
  303. },
  304. },
  305. },
  306. },
  307. usingTool: {
  308. initial: 'draw',
  309. onEnter: 'clearSelectedIds',
  310. on: {
  311. STARTED_PINCHING: {
  312. do: 'breakSession',
  313. to: 'pinching.toolPinching',
  314. },
  315. TOGGLED_TOOL_LOCK: 'toggleToolLock',
  316. },
  317. states: {
  318. draw: {
  319. initial: 'creating',
  320. states: {
  321. creating: {
  322. on: {
  323. CANCELLED: { to: 'selecting' },
  324. POINTED_SHAPE: {
  325. get: 'newDraw',
  326. do: 'createShape',
  327. to: 'draw.editing',
  328. },
  329. POINTED_CANVAS: {
  330. get: 'newDraw',
  331. do: 'createShape',
  332. to: 'draw.editing',
  333. },
  334. UNDO: { do: 'undo' },
  335. REDO: { do: 'redo' },
  336. },
  337. },
  338. editing: {
  339. onEnter: 'startDrawSession',
  340. on: {
  341. STOPPED_POINTING: {
  342. do: 'completeSession',
  343. to: 'draw.creating',
  344. },
  345. CANCELLED: {
  346. do: 'breakSession',
  347. to: 'selecting',
  348. },
  349. PRESSED_SHIFT: 'keyUpdateDrawSession',
  350. RELEASED_SHIFT: 'keyUpdateDrawSession',
  351. MOVED_POINTER: 'updateDrawSession',
  352. PANNED_CAMERA: 'updateDrawSession',
  353. },
  354. },
  355. },
  356. },
  357. dot: {
  358. initial: 'creating',
  359. states: {
  360. creating: {
  361. on: {
  362. CANCELLED: { to: 'selecting' },
  363. POINTED_SHAPE: {
  364. get: 'newDot',
  365. do: 'createShape',
  366. to: 'dot.editing',
  367. },
  368. POINTED_CANVAS: {
  369. get: 'newDot',
  370. do: 'createShape',
  371. to: 'dot.editing',
  372. },
  373. },
  374. },
  375. editing: {
  376. on: {
  377. STOPPED_POINTING: [
  378. 'completeSession',
  379. {
  380. if: 'isToolLocked',
  381. to: 'dot.creating',
  382. else: {
  383. to: 'selecting',
  384. },
  385. },
  386. ],
  387. CANCELLED: {
  388. do: 'breakSession',
  389. to: 'selecting',
  390. },
  391. },
  392. initial: 'inactive',
  393. states: {
  394. inactive: {
  395. on: {
  396. MOVED_POINTER: {
  397. if: 'distanceImpliesDrag',
  398. to: 'dot.editing.active',
  399. },
  400. },
  401. },
  402. active: {
  403. onEnter: 'startTranslateSession',
  404. on: {
  405. MOVED_POINTER: 'updateTranslateSession',
  406. PANNED_CAMERA: 'updateTranslateSession',
  407. },
  408. },
  409. },
  410. },
  411. },
  412. },
  413. arrow: {
  414. initial: 'creating',
  415. states: {
  416. creating: {
  417. on: {
  418. CANCELLED: { to: 'selecting' },
  419. POINTED_SHAPE: {
  420. get: 'newArrow',
  421. do: 'createShape',
  422. to: 'arrow.editing',
  423. },
  424. POINTED_CANVAS: {
  425. get: 'newArrow',
  426. do: 'createShape',
  427. to: 'arrow.editing',
  428. },
  429. UNDO: { do: 'undo' },
  430. REDO: { do: 'redo' },
  431. },
  432. },
  433. editing: {
  434. onEnter: 'startArrowSession',
  435. on: {
  436. STOPPED_POINTING: [
  437. 'completeSession',
  438. {
  439. if: 'isToolLocked',
  440. to: 'arrow.creating',
  441. else: { to: 'selecting' },
  442. },
  443. ],
  444. CANCELLED: {
  445. do: 'breakSession',
  446. if: 'isToolLocked',
  447. to: 'arrow.creating',
  448. else: { to: 'selecting' },
  449. },
  450. PRESSED_SHIFT: 'keyUpdateArrowSession',
  451. RELEASED_SHIFT: 'keyUpdateArrowSession',
  452. MOVED_POINTER: 'updateArrowSession',
  453. PANNED_CAMERA: 'updateArrowSession',
  454. },
  455. },
  456. },
  457. },
  458. circle: {
  459. initial: 'creating',
  460. states: {
  461. creating: {
  462. on: {
  463. CANCELLED: { to: 'selecting' },
  464. POINTED_SHAPE: {
  465. to: 'circle.editing',
  466. },
  467. POINTED_CANVAS: {
  468. to: 'circle.editing',
  469. },
  470. },
  471. },
  472. editing: {
  473. on: {
  474. STOPPED_POINTING: { to: 'selecting' },
  475. CANCELLED: { to: 'selecting' },
  476. MOVED_POINTER: {
  477. if: 'distanceImpliesDrag',
  478. then: {
  479. get: 'newCircle',
  480. do: 'createShape',
  481. to: 'drawingShape.bounds',
  482. },
  483. },
  484. },
  485. },
  486. },
  487. },
  488. ellipse: {
  489. initial: 'creating',
  490. states: {
  491. creating: {
  492. on: {
  493. CANCELLED: { to: 'selecting' },
  494. POINTED_CANVAS: {
  495. to: 'ellipse.editing',
  496. },
  497. },
  498. },
  499. editing: {
  500. on: {
  501. STOPPED_POINTING: { to: 'selecting' },
  502. CANCELLED: { to: 'selecting' },
  503. MOVED_POINTER: {
  504. if: 'distanceImpliesDrag',
  505. then: {
  506. get: 'newEllipse',
  507. do: 'createShape',
  508. to: 'drawingShape.bounds',
  509. },
  510. },
  511. },
  512. },
  513. },
  514. },
  515. rectangle: {
  516. initial: 'creating',
  517. states: {
  518. creating: {
  519. on: {
  520. CANCELLED: { to: 'selecting' },
  521. POINTED_SHAPE: {
  522. to: 'rectangle.editing',
  523. },
  524. POINTED_CANVAS: {
  525. to: 'rectangle.editing',
  526. },
  527. },
  528. },
  529. editing: {
  530. on: {
  531. STOPPED_POINTING: { to: 'selecting' },
  532. CANCELLED: { to: 'selecting' },
  533. MOVED_POINTER: {
  534. if: 'distanceImpliesDrag',
  535. then: {
  536. get: 'newRectangle',
  537. do: 'createShape',
  538. to: 'drawingShape.bounds',
  539. },
  540. },
  541. },
  542. },
  543. },
  544. },
  545. ray: {
  546. initial: 'creating',
  547. states: {
  548. creating: {
  549. on: {
  550. CANCELLED: { to: 'selecting' },
  551. POINTED_SHAPE: {
  552. get: 'newRay',
  553. do: 'createShape',
  554. to: 'ray.editing',
  555. },
  556. POINTED_CANVAS: {
  557. get: 'newRay',
  558. do: 'createShape',
  559. to: 'ray.editing',
  560. },
  561. },
  562. },
  563. editing: {
  564. on: {
  565. STOPPED_POINTING: { to: 'selecting' },
  566. CANCELLED: { to: 'selecting' },
  567. MOVED_POINTER: {
  568. if: 'distanceImpliesDrag',
  569. to: 'drawingShape.direction',
  570. },
  571. },
  572. },
  573. },
  574. },
  575. line: {
  576. initial: 'creating',
  577. states: {
  578. creating: {
  579. on: {
  580. CANCELLED: { to: 'selecting' },
  581. POINTED_SHAPE: {
  582. get: 'newLine',
  583. do: 'createShape',
  584. to: 'line.editing',
  585. },
  586. POINTED_CANVAS: {
  587. get: 'newLine',
  588. do: 'createShape',
  589. to: 'line.editing',
  590. },
  591. },
  592. },
  593. editing: {
  594. on: {
  595. STOPPED_POINTING: { to: 'selecting' },
  596. CANCELLED: { to: 'selecting' },
  597. MOVED_POINTER: {
  598. if: 'distanceImpliesDrag',
  599. to: 'drawingShape.direction',
  600. },
  601. },
  602. },
  603. },
  604. },
  605. polyline: {},
  606. },
  607. },
  608. drawingShape: {
  609. on: {
  610. STOPPED_POINTING: [
  611. 'completeSession',
  612. {
  613. if: 'isToolLocked',
  614. to: 'usingTool.previous',
  615. else: { to: 'selecting' },
  616. },
  617. ],
  618. CANCELLED: {
  619. do: 'breakSession',
  620. to: 'selecting',
  621. },
  622. },
  623. initial: 'drawingShapeBounds',
  624. states: {
  625. bounds: {
  626. onEnter: 'startDrawTransformSession',
  627. on: {
  628. MOVED_POINTER: 'updateTransformSession',
  629. PANNED_CAMERA: 'updateTransformSession',
  630. },
  631. },
  632. direction: {
  633. onEnter: 'startDirectionSession',
  634. on: {
  635. MOVED_POINTER: 'updateDirectionSession',
  636. PANNED_CAMERA: 'updateDirectionSession',
  637. },
  638. },
  639. },
  640. },
  641. },
  642. },
  643. },
  644. results: {
  645. newArrow() {
  646. return ShapeType.Arrow
  647. },
  648. newDraw() {
  649. return ShapeType.Draw
  650. },
  651. newDot() {
  652. return ShapeType.Dot
  653. },
  654. newRay() {
  655. return ShapeType.Ray
  656. },
  657. newLine() {
  658. return ShapeType.Line
  659. },
  660. newCircle() {
  661. return ShapeType.Circle
  662. },
  663. newEllipse() {
  664. return ShapeType.Ellipse
  665. },
  666. newRectangle() {
  667. return ShapeType.Rectangle
  668. },
  669. },
  670. conditions: {
  671. isPointingBounds(data, payload: PointerInfo) {
  672. return payload.target === 'bounds'
  673. },
  674. isReadOnly(data) {
  675. return data.isReadOnly
  676. },
  677. distanceImpliesDrag(data, payload: PointerInfo) {
  678. return vec.dist2(payload.origin, payload.point) > 8
  679. },
  680. isPointedShapeSelected(data) {
  681. return data.selectedIds.has(data.pointedId)
  682. },
  683. isPressingShiftKey(data, payload: PointerInfo) {
  684. return payload.shiftKey
  685. },
  686. isPressingMetaKey(data, payload: PointerInfo) {
  687. return payload.metaKey
  688. },
  689. shapeIsHovered(data, payload: { target: string }) {
  690. return data.hoveredId === payload.target
  691. },
  692. pointHitsShape(data, payload: { target: string; point: number[] }) {
  693. const shape = getShape(data, payload.target)
  694. return getShapeUtils(shape).hitTest(
  695. shape,
  696. screenToWorld(payload.point, data)
  697. )
  698. },
  699. isPointingRotationHandle(
  700. data,
  701. payload: { target: Edge | Corner | 'rotate' }
  702. ) {
  703. return payload.target === 'rotate'
  704. },
  705. hasSelection(data) {
  706. return data.selectedIds.size > 0
  707. },
  708. isToolLocked(data) {
  709. return data.settings.isToolLocked
  710. },
  711. isPenLocked(data) {
  712. return data.settings.isPenLocked
  713. },
  714. },
  715. actions: {
  716. /* --------------------- Shapes --------------------- */
  717. createShape(data, payload, type: ShapeType) {
  718. const shape = createShape(type, {
  719. point: screenToWorld(payload.point, data),
  720. style: getCurrent(data.currentStyle),
  721. })
  722. const siblings = getChildren(data, shape.parentId)
  723. const childIndex = siblings.length
  724. ? siblings[siblings.length - 1].childIndex + 1
  725. : 1
  726. getShapeUtils(shape).setProperty(shape, 'childIndex', childIndex)
  727. getPage(data).shapes[shape.id] = shape
  728. data.selectedIds.clear()
  729. data.selectedIds.add(shape.id)
  730. },
  731. /* -------------------- Sessions -------------------- */
  732. // Shared
  733. breakSession(data) {
  734. session?.cancel(data)
  735. session = undefined
  736. history.disable()
  737. commands.deleteSelected(data)
  738. history.enable()
  739. },
  740. cancelSession(data) {
  741. session?.cancel(data)
  742. session = undefined
  743. },
  744. completeSession(data) {
  745. session?.complete(data)
  746. session = undefined
  747. },
  748. // Brushing
  749. startBrushSession(data, payload: PointerInfo) {
  750. session = new Sessions.BrushSession(
  751. data,
  752. screenToWorld(payload.point, data)
  753. )
  754. },
  755. updateBrushSession(data, payload: PointerInfo) {
  756. session.update(data, screenToWorld(payload.point, data))
  757. },
  758. // Rotating
  759. startRotateSession(data, payload: PointerInfo) {
  760. session = new Sessions.RotateSession(
  761. data,
  762. screenToWorld(payload.point, data)
  763. )
  764. },
  765. keyUpdateRotateSession(data, payload: PointerInfo) {
  766. session.update(
  767. data,
  768. screenToWorld(inputs.pointer.point, data),
  769. payload.shiftKey
  770. )
  771. },
  772. updateRotateSession(data, payload: PointerInfo) {
  773. session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
  774. },
  775. // Dragging / Translating
  776. startTranslateSession(data) {
  777. session = new Sessions.TranslateSession(
  778. data,
  779. screenToWorld(inputs.pointer.origin, data)
  780. )
  781. },
  782. keyUpdateTranslateSession(
  783. data,
  784. payload: { shiftKey: boolean; altKey: boolean }
  785. ) {
  786. session.update(
  787. data,
  788. screenToWorld(inputs.pointer.point, data),
  789. payload.shiftKey,
  790. payload.altKey
  791. )
  792. },
  793. updateTranslateSession(data, payload: PointerInfo) {
  794. session.update(
  795. data,
  796. screenToWorld(payload.point, data),
  797. payload.shiftKey,
  798. payload.altKey
  799. )
  800. },
  801. // Dragging Handle
  802. startHandleSession(data, payload: PointerInfo) {
  803. const shapeId = Array.from(data.selectedIds.values())[0]
  804. const handleId = payload.target
  805. session = new Sessions.HandleSession(
  806. data,
  807. shapeId,
  808. handleId,
  809. screenToWorld(inputs.pointer.origin, data)
  810. )
  811. },
  812. keyUpdateHandleSession(
  813. data,
  814. payload: { shiftKey: boolean; altKey: boolean }
  815. ) {
  816. session.update(
  817. data,
  818. screenToWorld(inputs.pointer.point, data),
  819. payload.shiftKey,
  820. payload.altKey
  821. )
  822. },
  823. updateHandleSession(data, payload: PointerInfo) {
  824. session.update(
  825. data,
  826. screenToWorld(payload.point, data),
  827. payload.shiftKey,
  828. payload.altKey
  829. )
  830. },
  831. // Transforming
  832. startTransformSession(
  833. data,
  834. payload: PointerInfo & { target: Corner | Edge }
  835. ) {
  836. const point = screenToWorld(inputs.pointer.origin, data)
  837. session =
  838. data.selectedIds.size === 1
  839. ? new Sessions.TransformSingleSession(data, payload.target, point)
  840. : new Sessions.TransformSession(data, payload.target, point)
  841. },
  842. startDrawTransformSession(data, payload: PointerInfo) {
  843. session = new Sessions.TransformSingleSession(
  844. data,
  845. Corner.BottomRight,
  846. screenToWorld(payload.point, data),
  847. true
  848. )
  849. },
  850. keyUpdateTransformSession(data, payload: PointerInfo) {
  851. session.update(
  852. data,
  853. screenToWorld(inputs.pointer.point, data),
  854. payload.shiftKey,
  855. payload.altKey
  856. )
  857. },
  858. updateTransformSession(data, payload: PointerInfo) {
  859. session.update(
  860. data,
  861. screenToWorld(payload.point, data),
  862. payload.shiftKey,
  863. payload.altKey
  864. )
  865. },
  866. // Direction
  867. startDirectionSession(data, payload: PointerInfo) {
  868. session = new Sessions.DirectionSession(
  869. data,
  870. screenToWorld(inputs.pointer.origin, data)
  871. )
  872. },
  873. updateDirectionSession(data, payload: PointerInfo) {
  874. session.update(data, screenToWorld(payload.point, data))
  875. },
  876. // Drawing
  877. startDrawSession(data, payload: PointerInfo) {
  878. const id = Array.from(data.selectedIds.values())[0]
  879. session = new Sessions.DrawSession(
  880. data,
  881. id,
  882. screenToWorld(inputs.pointer.origin, data),
  883. payload.shiftKey
  884. )
  885. },
  886. keyUpdateDrawSession(data, payload: PointerInfo) {
  887. session.update(
  888. data,
  889. screenToWorld(inputs.pointer.point, data),
  890. payload.shiftKey
  891. )
  892. },
  893. updateDrawSession(data, payload: PointerInfo) {
  894. session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
  895. },
  896. // Arrow
  897. startArrowSession(data, payload: PointerInfo) {
  898. const id = Array.from(data.selectedIds.values())[0]
  899. session = new Sessions.ArrowSession(
  900. data,
  901. id,
  902. screenToWorld(inputs.pointer.origin, data),
  903. payload.shiftKey
  904. )
  905. },
  906. keyUpdateArrowSession(data, payload: PointerInfo) {
  907. session.update(
  908. data,
  909. screenToWorld(inputs.pointer.point, data),
  910. payload.shiftKey
  911. )
  912. },
  913. updateArrowSession(data, payload: PointerInfo) {
  914. session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
  915. },
  916. // Nudges
  917. nudgeSelection(data, payload: { delta: number[]; shiftKey: boolean }) {
  918. commands.nudge(
  919. data,
  920. vec.mul(
  921. payload.delta,
  922. payload.shiftKey
  923. ? data.settings.nudgeDistanceLarge
  924. : data.settings.nudgeDistanceSmall
  925. )
  926. )
  927. },
  928. /* -------------------- Selection ------------------- */
  929. selectAll(data) {
  930. const { selectedIds } = data
  931. const page = getPage(data)
  932. selectedIds.clear()
  933. for (let id in page.shapes) {
  934. selectedIds.add(id)
  935. }
  936. },
  937. setHoveredId(data, payload: PointerInfo) {
  938. data.hoveredId = payload.target
  939. },
  940. clearHoveredId(data) {
  941. data.hoveredId = undefined
  942. },
  943. setPointedId(data, payload: PointerInfo) {
  944. data.pointedId = payload.target
  945. },
  946. clearPointedId(data) {
  947. data.pointedId = undefined
  948. },
  949. clearSelectedIds(data) {
  950. data.selectedIds.clear()
  951. },
  952. pullPointedIdFromSelectedIds(data) {
  953. const { selectedIds, pointedId } = data
  954. selectedIds.delete(pointedId)
  955. },
  956. pushPointedIdToSelectedIds(data) {
  957. data.selectedIds.add(data.pointedId)
  958. },
  959. moveSelection(data, payload: { type: MoveType }) {
  960. commands.move(data, payload.type)
  961. },
  962. alignSelection(data, payload: { type: AlignType }) {
  963. commands.align(data, payload.type)
  964. },
  965. stretchSelection(data, payload: { type: StretchType }) {
  966. commands.stretch(data, payload.type)
  967. },
  968. distributeSelection(data, payload: { type: DistributeType }) {
  969. commands.distribute(data, payload.type)
  970. },
  971. duplicateSelection(data) {
  972. commands.duplicate(data)
  973. },
  974. lockSelection(data) {
  975. commands.toggle(data, 'isLocked')
  976. },
  977. hideSelection(data) {
  978. commands.toggle(data, 'isHidden')
  979. },
  980. aspectLockSelection(data) {
  981. commands.toggle(data, 'isAspectRatioLocked')
  982. },
  983. deleteSelection(data) {
  984. commands.deleteSelected(data)
  985. },
  986. rotateSelectionCcw(data) {
  987. commands.rotateCcw(data)
  988. },
  989. /* --------------------- Camera --------------------- */
  990. zoomIn(data) {
  991. const { camera } = data
  992. const i = Math.round((camera.zoom * 100) / 25)
  993. const center = [window.innerWidth / 2, window.innerHeight / 2]
  994. const p0 = screenToWorld(center, data)
  995. camera.zoom = Math.min(3, (i + 1) * 0.25)
  996. const p1 = screenToWorld(center, data)
  997. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  998. setZoomCSS(camera.zoom)
  999. },
  1000. zoomOut(data) {
  1001. const { camera } = data
  1002. const i = Math.round((camera.zoom * 100) / 25)
  1003. const center = [window.innerWidth / 2, window.innerHeight / 2]
  1004. const p0 = screenToWorld(center, data)
  1005. camera.zoom = Math.max(0.1, (i - 1) * 0.25)
  1006. const p1 = screenToWorld(center, data)
  1007. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  1008. setZoomCSS(camera.zoom)
  1009. },
  1010. zoomCameraToActual(data) {
  1011. const { camera } = data
  1012. const center = [window.innerWidth / 2, window.innerHeight / 2]
  1013. const p0 = screenToWorld(center, data)
  1014. camera.zoom = 1
  1015. const p1 = screenToWorld(center, data)
  1016. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  1017. setZoomCSS(camera.zoom)
  1018. },
  1019. zoomCameraToSelectionActual(data) {
  1020. const { camera } = data
  1021. const bounds = getSelectedBounds(data)
  1022. const mx = (window.innerWidth - bounds.width) / 2
  1023. const my = (window.innerHeight - bounds.height) / 2
  1024. camera.zoom = 1
  1025. camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
  1026. setZoomCSS(camera.zoom)
  1027. },
  1028. zoomCameraToSelection(data) {
  1029. const { camera } = data
  1030. const bounds = getSelectedBounds(data)
  1031. const zoom =
  1032. bounds.width > bounds.height
  1033. ? (window.innerWidth - 128) / bounds.width
  1034. : (window.innerHeight - 128) / bounds.height
  1035. const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
  1036. const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
  1037. camera.zoom = zoom
  1038. camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
  1039. setZoomCSS(camera.zoom)
  1040. },
  1041. zoomCameraToFit(data) {
  1042. const { camera } = data
  1043. const page = getPage(data)
  1044. const shapes = Object.values(page.shapes)
  1045. if (shapes.length === 0) {
  1046. return
  1047. }
  1048. const bounds = getCommonBounds(
  1049. ...Object.values(shapes).map((shape) =>
  1050. getShapeUtils(shape).getBounds(shape)
  1051. )
  1052. )
  1053. const zoom =
  1054. bounds.width > bounds.height
  1055. ? (window.innerWidth - 128) / bounds.width
  1056. : (window.innerHeight - 128) / bounds.height
  1057. const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
  1058. const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
  1059. camera.zoom = zoom
  1060. camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
  1061. setZoomCSS(camera.zoom)
  1062. },
  1063. zoomCamera(data, payload: { delta: number; point: number[] }) {
  1064. const { camera } = data
  1065. const next = camera.zoom - (payload.delta / 100) * camera.zoom
  1066. const p0 = screenToWorld(payload.point, data)
  1067. camera.zoom = clamp(next, 0.1, 3)
  1068. const p1 = screenToWorld(payload.point, data)
  1069. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  1070. setZoomCSS(camera.zoom)
  1071. },
  1072. panCamera(data, payload: { delta: number[] }) {
  1073. const { camera } = data
  1074. camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
  1075. },
  1076. pinchCamera(
  1077. data,
  1078. payload: {
  1079. delta: number[]
  1080. distanceDelta: number
  1081. angleDelta: number
  1082. point: number[]
  1083. }
  1084. ) {
  1085. const { camera } = data
  1086. camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
  1087. const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
  1088. const p0 = screenToWorld(payload.point, data)
  1089. camera.zoom = clamp(next, 0.1, 3)
  1090. const p1 = screenToWorld(payload.point, data)
  1091. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  1092. setZoomCSS(camera.zoom)
  1093. },
  1094. resetCamera(data) {
  1095. data.camera.zoom = 1
  1096. data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
  1097. document.documentElement.style.setProperty('--camera-zoom', '1')
  1098. },
  1099. /* ---------------------- History ---------------------- */
  1100. // History
  1101. popHistory() {
  1102. history.pop()
  1103. },
  1104. forceSave(data) {
  1105. history.save(data)
  1106. },
  1107. enableHistory() {
  1108. history.enable()
  1109. },
  1110. disableHistory() {
  1111. history.disable()
  1112. },
  1113. undo(data) {
  1114. history.undo(data)
  1115. },
  1116. redo(data) {
  1117. history.redo(data)
  1118. },
  1119. /* --------------------- Styles --------------------- */
  1120. toggleStylePanel(data) {
  1121. data.settings.isStyleOpen = !data.settings.isStyleOpen
  1122. },
  1123. updateStyles(data, payload: Partial<ShapeStyles>) {
  1124. Object.assign(data.currentStyle, payload)
  1125. },
  1126. applyStylesToSelection(data, payload: Partial<ShapeStyles>) {
  1127. commands.style(data, payload)
  1128. },
  1129. /* ---------------------- Code ---------------------- */
  1130. closeCodePanel(data) {
  1131. data.settings.isCodeOpen = false
  1132. },
  1133. openCodePanel(data) {
  1134. data.settings.isCodeOpen = true
  1135. },
  1136. toggleCodePanel(data) {
  1137. data.settings.isCodeOpen = !data.settings.isCodeOpen
  1138. },
  1139. setGeneratedShapes(
  1140. data,
  1141. payload: { shapes: Shape[]; controls: CodeControl[] }
  1142. ) {
  1143. commands.generate(data, data.currentPageId, payload.shapes)
  1144. },
  1145. setCodeControls(data, payload: { controls: CodeControl[] }) {
  1146. data.codeControls = Object.fromEntries(
  1147. payload.controls.map((control) => [control.id, control])
  1148. )
  1149. },
  1150. increaseCodeFontSize(data) {
  1151. data.settings.fontSize++
  1152. },
  1153. decreaseCodeFontSize(data) {
  1154. data.settings.fontSize--
  1155. },
  1156. updateControls(data, payload: { [key: string]: any }) {
  1157. for (let key in payload) {
  1158. data.codeControls[key].value = payload[key]
  1159. }
  1160. history.disable()
  1161. data.selectedIds.clear()
  1162. try {
  1163. const { shapes } = updateFromCode(
  1164. data.document.code[data.currentCodeFileId].code,
  1165. data.codeControls
  1166. )
  1167. commands.generate(data, data.currentPageId, shapes)
  1168. } catch (e) {
  1169. console.error(e)
  1170. }
  1171. history.enable()
  1172. },
  1173. /* -------------------- Settings -------------------- */
  1174. enablePenLock(data) {
  1175. data.settings.isPenLocked = true
  1176. },
  1177. disablePenLock(data) {
  1178. data.settings.isPenLocked = false
  1179. },
  1180. toggleToolLock(data) {
  1181. data.settings.isToolLocked = !data.settings.isToolLocked
  1182. },
  1183. /* ---------------------- Data ---------------------- */
  1184. saveCode(data, payload: { code: string }) {
  1185. data.document.code[data.currentCodeFileId].code = payload.code
  1186. history.save(data)
  1187. },
  1188. restoreSavedData(data) {
  1189. history.load(data)
  1190. },
  1191. clearBoundsRotation(data) {
  1192. data.boundsRotation = 0
  1193. },
  1194. },
  1195. values: {
  1196. selectedIds(data) {
  1197. return new Set(data.selectedIds)
  1198. },
  1199. selectedBounds(data) {
  1200. const { selectedIds } = data
  1201. const page = getPage(data)
  1202. const shapes = Array.from(selectedIds.values())
  1203. .map((id) => page.shapes[id])
  1204. .filter(Boolean)
  1205. if (selectedIds.size === 0) return null
  1206. if (selectedIds.size === 1) {
  1207. if (!shapes[0]) {
  1208. console.error('Could not find that shape! Clearing selected IDs.')
  1209. data.selectedIds.clear()
  1210. return null
  1211. }
  1212. const shapeUtils = getShapeUtils(shapes[0])
  1213. if (!shapeUtils.canTransform) return null
  1214. return shapeUtils.getBounds(shapes[0])
  1215. }
  1216. return getCommonBounds(
  1217. ...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
  1218. )
  1219. },
  1220. },
  1221. })
  1222. let session: Sessions.BaseSession
  1223. export default state
  1224. export const useSelector = createSelectorHook(state)