Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022
  1. import { createSelectorHook, createState } from '@state-designer/react'
  2. import { updateFromCode } from './code/generate'
  3. import { createShape, getShapeUtils } from './shape-utils'
  4. import vec from 'utils/vec'
  5. import inputs from './inputs'
  6. import history from './history'
  7. import storage from './storage'
  8. import clipboard from './clipboard'
  9. import * as Sessions from './sessions'
  10. import commands from './commands'
  11. import {
  12. getChildren,
  13. getCommonBounds,
  14. getCurrentCamera,
  15. getPage,
  16. getSelectedBounds,
  17. getSelectedShapes,
  18. getShape,
  19. screenToWorld,
  20. setZoomCSS,
  21. rotateBounds,
  22. getBoundsCenter,
  23. getDocumentBranch,
  24. getCameraZoom,
  25. getSelectedIds,
  26. setSelectedIds,
  27. getPageState,
  28. setToArray,
  29. deepClone,
  30. pointInBounds,
  31. } from 'utils'
  32. import {
  33. Data,
  34. PointerInfo,
  35. Shape,
  36. ShapeType,
  37. Corner,
  38. Edge,
  39. CodeControl,
  40. MoveType,
  41. ShapeStyles,
  42. DistributeType,
  43. AlignType,
  44. StretchType,
  45. DashStyle,
  46. SizeStyle,
  47. ColorStyle,
  48. } from 'types'
  49. import session from './session'
  50. const initialData: Data = {
  51. isReadOnly: false,
  52. settings: {
  53. fontSize: 13,
  54. isDarkMode: false,
  55. isCodeOpen: false,
  56. isStyleOpen: false,
  57. isToolLocked: false,
  58. isPenLocked: false,
  59. nudgeDistanceLarge: 10,
  60. nudgeDistanceSmall: 1,
  61. },
  62. currentStyle: {
  63. size: SizeStyle.Medium,
  64. color: ColorStyle.Black,
  65. dash: DashStyle.Solid,
  66. isFilled: false,
  67. },
  68. activeTool: 'select',
  69. brush: undefined,
  70. boundsRotation: 0,
  71. pointedId: null,
  72. hoveredId: null,
  73. editingId: null,
  74. currentPageId: 'page1',
  75. currentParentId: 'page1',
  76. currentCodeFileId: 'file0',
  77. codeControls: {},
  78. document: {
  79. id: '0001',
  80. name: 'My Document',
  81. pages: {
  82. page1: {
  83. id: 'page1',
  84. type: 'page',
  85. name: 'Page 1',
  86. childIndex: 0,
  87. shapes: {},
  88. },
  89. },
  90. code: {
  91. file0: {
  92. id: 'file0',
  93. name: 'index.ts',
  94. code: `
  95. const draw = new Draw({
  96. points: [
  97. ...Utils.getPointsBetween([0, 0], [20, 50]),
  98. ...Utils.getPointsBetween([20, 50], [100, 20], 3),
  99. ...Utils.getPointsBetween([100, 20], [100, 100], 10),
  100. [100, 100],
  101. ],
  102. })
  103. const rectangle = new Rectangle({
  104. point: [200, 0],
  105. style: {
  106. color: ColorStyle.Blue,
  107. },
  108. })
  109. const ellipse = new Ellipse({
  110. point: [400, 0],
  111. })
  112. const arrow = new Arrow({
  113. start: [600, 0],
  114. end: [700, 100],
  115. })
  116. const radius = 1000
  117. const count = 100
  118. const center = [350, 50]
  119. for (let i = 0; i < count; i++) {
  120. const point = Vec.rotWith(
  121. Vec.add(center, [radius, 0]),
  122. center,
  123. (Math.PI * 2 * i) / count
  124. )
  125. const dot = new Dot({
  126. point,
  127. })
  128. }
  129. `,
  130. },
  131. },
  132. },
  133. pageStates: {
  134. page1: {
  135. id: 'page1',
  136. selectedIds: new Set([]),
  137. camera: {
  138. point: [0, 0],
  139. zoom: 1,
  140. },
  141. },
  142. },
  143. }
  144. const state = createState({
  145. data: initialData,
  146. on: {
  147. UNMOUNTED: { to: 'loading' },
  148. },
  149. initial: 'loading',
  150. states: {
  151. loading: {
  152. on: {
  153. MOUNTED: {
  154. do: 'restoreSavedData',
  155. to: 'ready',
  156. },
  157. },
  158. },
  159. ready: {
  160. onEnter: {
  161. wait: 0.01,
  162. if: 'hasSelection',
  163. do: 'zoomCameraToSelectionActual',
  164. else: ['zoomCameraToActual'],
  165. },
  166. on: {
  167. RESET_PAGE: 'resetPage',
  168. TOGGLED_READ_ONLY: 'toggleReadOnly',
  169. LOADED_FONTS: 'resetShapes',
  170. USED_PEN_DEVICE: 'enablePenLock',
  171. DISABLED_PEN_LOCK: 'disablePenLock',
  172. TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
  173. TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
  174. PANNED_CAMERA: 'panCamera',
  175. POINTED_CANVAS: ['closeStylePanel', 'clearCurrentParentId'],
  176. COPIED_STATE_TO_CLIPBOARD: 'copyStateToClipboard',
  177. COPIED: { if: 'hasSelection', do: 'copyToClipboard' },
  178. PASTED: {
  179. unlessAny: ['isReadOnly', 'isInSession'],
  180. do: 'pasteFromClipboard',
  181. },
  182. PASTED_SHAPES_FROM_CLIPBOARD: {
  183. unlessAny: ['isReadOnly', 'isInSession'],
  184. do: 'pasteShapesFromClipboard',
  185. },
  186. TOGGLED_SHAPE_LOCK: {
  187. unlessAny: ['isReadOnly', 'isInSession'],
  188. if: 'hasSelection',
  189. do: 'lockSelection',
  190. },
  191. TOGGLED_SHAPE_HIDE: {
  192. unlessAny: ['isReadOnly', 'isInSession'],
  193. if: 'hasSelection',
  194. do: 'hideSelection',
  195. },
  196. TOGGLED_SHAPE_ASPECT_LOCK: {
  197. unlessAny: ['isReadOnly', 'isInSession'],
  198. if: 'hasSelection',
  199. do: 'aspectLockSelection',
  200. },
  201. CHANGED_STYLE: {
  202. unlessAny: ['isReadOnly', 'isInSession'],
  203. do: ['updateStyles', 'applyStylesToSelection'],
  204. },
  205. CLEARED_PAGE: {
  206. unlessAny: ['isReadOnly', 'isInSession'],
  207. if: 'hasSelection',
  208. do: 'deleteSelection',
  209. else: ['selectAll', 'deleteSelection'],
  210. },
  211. CREATED_PAGE: {
  212. unless: ['isReadOnly', 'isInSession'],
  213. do: ['clearSelectedIds', 'createPage'],
  214. },
  215. DELETED_PAGE: {
  216. unlessAny: ['isReadOnly', 'isInSession', 'hasOnlyOnePage'],
  217. do: 'deletePage',
  218. },
  219. SELECTED_SELECT_TOOL: {
  220. unless: 'isInSession',
  221. to: 'selecting',
  222. },
  223. SELECTED_DRAW_TOOL: {
  224. unlessAny: ['isReadOnly', 'isInSession'],
  225. to: 'draw',
  226. },
  227. SELECTED_ARROW_TOOL: {
  228. unless: ['isReadOnly', 'isInSession'],
  229. to: 'arrow',
  230. },
  231. SELECTED_DOT_TOOL: {
  232. unless: ['isReadOnly', 'isInSession'],
  233. to: 'dot',
  234. },
  235. SELECTED_ELLIPSE_TOOL: {
  236. unless: ['isReadOnly', 'isInSession'],
  237. to: 'ellipse',
  238. },
  239. SELECTED_RAY_TOOL: {
  240. unless: ['isReadOnly', 'isInSession'],
  241. to: 'ray',
  242. },
  243. SELECTED_LINE_TOOL: {
  244. unless: ['isReadOnly', 'isInSession'],
  245. to: 'line',
  246. },
  247. SELECTED_POLYLINE_TOOL: {
  248. unless: ['isReadOnly', 'isInSession'],
  249. to: 'polyline',
  250. },
  251. SELECTED_RECTANGLE_TOOL: {
  252. unless: ['isReadOnly', 'isInSession'],
  253. to: 'rectangle',
  254. },
  255. SELECTED_TEXT_TOOL: {
  256. unless: ['isReadOnly', 'isInSession'],
  257. to: 'text',
  258. },
  259. GENERATED_FROM_CODE: {
  260. unless: ['isReadOnly', 'isInSession'],
  261. do: ['setCodeControls', 'setGeneratedShapes'],
  262. },
  263. UNDO: {
  264. unless: ['isReadOnly', 'isInSession'],
  265. do: 'undo',
  266. },
  267. REDO: {
  268. unless: ['isReadOnly', 'isInSession'],
  269. do: 'redo',
  270. },
  271. SAVED: {
  272. unlessAny: ['isInSession', 'isReadOnly'],
  273. do: 'forceSave',
  274. },
  275. LOADED_FROM_FILE: {
  276. unless: 'isInSession',
  277. do: ['loadDocumentFromJson', 'resetHistory'],
  278. },
  279. SELECTED_ALL: {
  280. unless: 'isInSession',
  281. to: 'selecting',
  282. do: 'selectAll',
  283. },
  284. CHANGED_PAGE: {
  285. unless: 'isInSession',
  286. do: 'changePage',
  287. },
  288. ZOOMED_TO_ACTUAL: {
  289. if: 'hasSelection',
  290. do: 'zoomCameraToSelectionActual',
  291. else: 'zoomCameraToActual',
  292. },
  293. ZOOMED_CAMERA: 'zoomCamera',
  294. INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
  295. DECREASED_CODE_FONT_SIZE: 'decreaseCodeFontSize',
  296. CHANGED_CODE_CONTROL: { to: 'updatingControls' },
  297. TOGGLED_TOOL_LOCK: 'toggleToolLock',
  298. ZOOMED_TO_SELECTION: {
  299. if: 'hasSelection',
  300. do: 'zoomCameraToSelection',
  301. },
  302. STARTED_PINCHING: {
  303. unless: 'isInSession',
  304. to: 'pinching',
  305. },
  306. ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
  307. ZOOMED_IN: 'zoomIn',
  308. ZOOMED_OUT: 'zoomOut',
  309. RESET_CAMERA: 'resetCamera',
  310. COPIED_TO_SVG: 'copyToSvg',
  311. LOADED_FROM_FILE_STSTEM: 'loadFromFileSystem',
  312. SAVED_AS_TO_FILESYSTEM: 'saveAsToFileSystem',
  313. SAVED_TO_FILESYSTEM: {
  314. unless: 'isReadOnly',
  315. then: {
  316. if: 'isReadOnly',
  317. do: 'saveAsToFileSystem',
  318. else: 'saveToFileSystem',
  319. },
  320. },
  321. },
  322. initial: 'selecting',
  323. states: {
  324. selecting: {
  325. onEnter: ['setActiveToolSelect', 'clearInputs'],
  326. on: {
  327. SAVED: 'forceSave',
  328. DELETED: {
  329. unless: 'isReadOnly',
  330. do: 'deleteSelection',
  331. },
  332. SAVED_CODE: {
  333. unless: 'isReadOnly',
  334. do: 'saveCode',
  335. },
  336. MOVED_TO_PAGE: {
  337. unless: 'isReadOnly',
  338. if: 'hasSelection',
  339. do: ['moveSelectionToPage', 'zoomCameraToSelectionActual'],
  340. },
  341. MOVED: {
  342. unless: 'isReadOnly',
  343. if: 'hasSelection',
  344. do: 'moveSelection',
  345. },
  346. DUPLICATED: {
  347. unless: 'isReadOnly',
  348. if: 'hasSelection',
  349. do: 'duplicateSelection',
  350. },
  351. ROTATED_CCW: {
  352. unless: 'isReadOnly',
  353. if: 'hasSelection',
  354. do: 'rotateSelectionCcw',
  355. },
  356. ALIGNED: {
  357. unless: 'isReadOnly',
  358. if: 'hasMultipleSelection',
  359. do: 'alignSelection',
  360. },
  361. STRETCHED: {
  362. unless: 'isReadOnly',
  363. if: 'hasMultipleSelection',
  364. do: 'stretchSelection',
  365. },
  366. DISTRIBUTED: {
  367. unless: 'isReadOnly',
  368. if: 'hasMultipleSelection',
  369. do: 'distributeSelection',
  370. },
  371. GROUPED: {
  372. unless: 'isReadOnly',
  373. if: 'hasMultipleSelection',
  374. do: 'groupSelection',
  375. },
  376. UNGROUPED: {
  377. unless: 'isReadOnly',
  378. if: ['hasSelection', 'selectionIncludesGroups'],
  379. do: 'ungroupSelection',
  380. },
  381. NUDGED: { do: 'nudgeSelection' },
  382. },
  383. initial: 'notPointing',
  384. states: {
  385. notPointing: {
  386. onEnter: 'clearPointedId',
  387. on: {
  388. CANCELLED: 'clearSelectedIds',
  389. POINTED_CANVAS: { to: 'brushSelecting' },
  390. POINTED_BOUNDS: [
  391. {
  392. if: 'isPressingMetaKey',
  393. to: 'brushSelecting',
  394. },
  395. { to: 'pointingBounds' },
  396. ],
  397. POINTED_BOUNDS_HANDLE: {
  398. unless: 'isReadOnly',
  399. if: 'isPointingRotationHandle',
  400. to: 'rotatingSelection',
  401. else: { to: 'transformingSelection' },
  402. },
  403. STARTED_EDITING_SHAPE: {
  404. unless: 'isReadOnly',
  405. get: 'firstSelectedShape',
  406. if: ['hasSingleSelection', 'canEditSelectedShape'],
  407. do: 'setEditingId',
  408. to: 'editingShape',
  409. },
  410. DOUBLE_POINTED_BOUNDS_HANDLE: {
  411. unless: 'isReadOnly',
  412. if: 'hasSingleSelection',
  413. do: 'resetShapeBounds',
  414. },
  415. POINTED_HANDLE: {
  416. unless: 'isReadOnly',
  417. to: 'translatingHandles',
  418. },
  419. MOVED_OVER_SHAPE: {
  420. if: 'pointHitsShape',
  421. then: {
  422. unless: 'shapeIsHovered',
  423. do: 'setHoveredId',
  424. },
  425. else: {
  426. if: 'shapeIsHovered',
  427. do: 'clearHoveredId',
  428. },
  429. },
  430. UNHOVERED_SHAPE: 'clearHoveredId',
  431. POINTED_SHAPE: [
  432. {
  433. if: 'isPressingMetaKey',
  434. to: 'brushSelecting',
  435. },
  436. 'setPointedId',
  437. {
  438. if: 'pointInSelectionBounds',
  439. to: 'pointingBounds',
  440. },
  441. {
  442. unless: 'isPointedShapeSelected',
  443. then: {
  444. if: 'isPressingShiftKey',
  445. do: ['pushPointedIdToSelectedIds', 'clearPointedId'],
  446. else: ['clearSelectedIds', 'pushPointedIdToSelectedIds'],
  447. },
  448. },
  449. {
  450. to: 'pointingBounds',
  451. },
  452. ],
  453. DOUBLE_POINTED_SHAPE: [
  454. 'setPointedId',
  455. {
  456. if: 'isPointedShapeSelected',
  457. then: {
  458. get: 'firstSelectedShape',
  459. if: 'canEditSelectedShape',
  460. do: 'setEditingId',
  461. to: 'editingShape',
  462. },
  463. },
  464. {
  465. unless: 'isPressingShiftKey',
  466. do: [
  467. 'setDrilledPointedId',
  468. 'clearSelectedIds',
  469. 'pushPointedIdToSelectedIds',
  470. ],
  471. to: 'pointingBounds',
  472. },
  473. ],
  474. RIGHT_POINTED: [
  475. {
  476. if: 'isPointingCanvas',
  477. do: 'clearSelectedIds',
  478. else: {
  479. if: 'isPointingShape',
  480. then: [
  481. 'setPointedId',
  482. {
  483. unless: 'isPointedShapeSelected',
  484. do: [
  485. 'clearSelectedIds',
  486. 'pushPointedIdToSelectedIds',
  487. ],
  488. },
  489. ],
  490. },
  491. },
  492. ],
  493. },
  494. },
  495. pointingBounds: {
  496. on: {
  497. CANCELLED: { to: 'notPointing' },
  498. STOPPED_POINTING_BOUNDS: [],
  499. STOPPED_POINTING: [
  500. {
  501. if: 'isPointingBounds',
  502. do: 'clearSelectedIds',
  503. },
  504. {
  505. if: 'isPressingShiftKey',
  506. then: {
  507. if: 'isPointedShapeSelected',
  508. do: 'pullPointedIdFromSelectedIds',
  509. },
  510. else: {
  511. if: 'isPointingShape',
  512. do: [
  513. 'clearSelectedIds',
  514. 'setPointedId',
  515. 'pushPointedIdToSelectedIds',
  516. ],
  517. },
  518. },
  519. { to: 'notPointing' },
  520. ],
  521. MOVED_POINTER: {
  522. unless: ['isReadOnly', 'isInSession'],
  523. if: 'distanceImpliesDrag',
  524. to: 'translatingSelection',
  525. },
  526. },
  527. },
  528. rotatingSelection: {
  529. onEnter: 'startRotateSession',
  530. onExit: ['completeSession', 'clearBoundsRotation'],
  531. on: {
  532. MOVED_POINTER: 'updateRotateSession',
  533. PANNED_CAMERA: 'updateRotateSession',
  534. PRESSED_SHIFT_KEY: 'keyUpdateRotateSession',
  535. RELEASED_SHIFT_KEY: 'keyUpdateRotateSession',
  536. STOPPED_POINTING: { to: 'selecting' },
  537. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  538. },
  539. },
  540. transformingSelection: {
  541. onEnter: 'startTransformSession',
  542. onExit: 'completeSession',
  543. on: {
  544. // MOVED_POINTER: 'updateTransformSession', using hacks.fastTransform
  545. PANNED_CAMERA: 'updateTransformSession',
  546. PRESSED_SHIFT_KEY: 'keyUpdateTransformSession',
  547. RELEASED_SHIFT_KEY: 'keyUpdateTransformSession',
  548. STOPPED_POINTING: { to: 'selecting' },
  549. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  550. },
  551. },
  552. translatingSelection: {
  553. onEnter: 'startTranslateSession',
  554. onExit: 'completeSession',
  555. on: {
  556. STARTED_PINCHING: { to: 'pinching' },
  557. MOVED_POINTER: 'updateTranslateSession',
  558. PANNED_CAMERA: 'updateTranslateSession',
  559. PRESSED_SHIFT_KEY: 'keyUpdateTranslateSession',
  560. RELEASED_SHIFT_KEY: 'keyUpdateTranslateSession',
  561. PRESSED_ALT_KEY: 'keyUpdateTranslateSession',
  562. RELEASED_ALT_KEY: 'keyUpdateTranslateSession',
  563. STOPPED_POINTING: { to: 'selecting' },
  564. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  565. },
  566. },
  567. translatingHandles: {
  568. onEnter: 'startHandleSession',
  569. onExit: 'completeSession',
  570. on: {
  571. MOVED_POINTER: 'updateHandleSession',
  572. PANNED_CAMERA: 'updateHandleSession',
  573. PRESSED_SHIFT_KEY: 'keyUpdateHandleSession',
  574. RELEASED_SHIFT_KEY: 'keyUpdateHandleSession',
  575. STOPPED_POINTING: { to: 'selecting' },
  576. DOUBLE_POINTED_HANDLE: {
  577. do: ['cancelSession', 'doublePointHandle'],
  578. to: 'selecting',
  579. },
  580. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  581. },
  582. },
  583. brushSelecting: {
  584. onExit: 'completeSession',
  585. onEnter: [
  586. {
  587. unless: ['isPressingMetaKey', 'isPressingShiftKey'],
  588. do: 'clearSelectedIds',
  589. },
  590. 'clearBoundsRotation',
  591. 'startBrushSession',
  592. ],
  593. on: {
  594. // MOVED_POINTER: 'updateBrushSession', using hacks.fastBrushSelect
  595. PANNED_CAMERA: 'updateBrushSession',
  596. STOPPED_POINTING: { to: 'selecting' },
  597. STARTED_PINCHING: { to: 'pinching' },
  598. CANCELLED: { do: 'cancelSession', to: 'selecting' },
  599. },
  600. },
  601. updatingControls: {
  602. onEnter: 'updateControls',
  603. async: {
  604. await: 'getUpdatedShapes',
  605. onResolve: { do: 'updateGeneratedShapes', to: 'selecting' },
  606. },
  607. },
  608. },
  609. },
  610. editingShape: {
  611. onEnter: 'startEditSession',
  612. onExit: ['completeSession', 'clearEditingId'],
  613. on: {
  614. EDITED_SHAPE: { do: 'updateEditSession' },
  615. BLURRED_EDITING_SHAPE: { to: 'selecting' },
  616. CANCELLED: [
  617. {
  618. get: 'editingShape',
  619. if: 'shouldDeleteShape',
  620. do: 'breakSession',
  621. else: 'cancelSession',
  622. },
  623. { to: 'selecting' },
  624. ],
  625. },
  626. },
  627. pinching: {
  628. on: {
  629. // PINCHED: { do: 'pinchCamera' }, using hacks.fastPinchCamera
  630. },
  631. initial: 'selectPinching',
  632. onExit: { secretlyDo: 'updateZoomCSS' },
  633. states: {
  634. selectPinching: {
  635. on: {
  636. STOPPED_PINCHING: { to: 'selecting' },
  637. },
  638. },
  639. toolPinching: {
  640. on: {
  641. STOPPED_PINCHING: { to: 'usingTool.previous' },
  642. },
  643. },
  644. },
  645. },
  646. usingTool: {
  647. initial: 'draw',
  648. onEnter: 'clearSelectedIds',
  649. on: {
  650. STARTED_PINCHING: {
  651. do: 'breakSession',
  652. to: 'pinching.toolPinching',
  653. },
  654. TOGGLED_TOOL_LOCK: 'toggleToolLock',
  655. },
  656. states: {
  657. draw: {
  658. onEnter: 'setActiveToolDraw',
  659. initial: 'creating',
  660. states: {
  661. creating: {
  662. on: {
  663. CANCELLED: { to: 'selecting' },
  664. POINTED_SHAPE: {
  665. get: 'newDraw',
  666. do: 'createShape',
  667. to: 'draw.editing',
  668. },
  669. POINTED_CANVAS: {
  670. get: 'newDraw',
  671. do: 'createShape',
  672. to: 'draw.editing',
  673. },
  674. },
  675. },
  676. editing: {
  677. onEnter: 'startDrawSession',
  678. onExit: 'completeSession',
  679. on: {
  680. CANCELLED: {
  681. do: 'breakSession',
  682. to: 'selecting',
  683. },
  684. STOPPED_POINTING: {
  685. do: 'completeSession',
  686. to: 'draw.creating',
  687. },
  688. PRESSED_SHIFT: 'keyUpdateDrawSession',
  689. RELEASED_SHIFT: 'keyUpdateDrawSession',
  690. // MOVED_POINTER: 'updateDrawSession',
  691. PANNED_CAMERA: 'updateDrawSession',
  692. },
  693. },
  694. },
  695. },
  696. dot: {
  697. onEnter: 'setActiveToolDot',
  698. initial: 'creating',
  699. states: {
  700. creating: {
  701. on: {
  702. CANCELLED: { to: 'selecting' },
  703. POINTED_SHAPE: {
  704. get: 'newDot',
  705. do: 'createShape',
  706. to: 'dot.editing',
  707. },
  708. POINTED_CANVAS: {
  709. get: 'newDot',
  710. do: 'createShape',
  711. to: 'dot.editing',
  712. },
  713. },
  714. },
  715. editing: {
  716. on: {
  717. STOPPED_POINTING: [
  718. 'completeSession',
  719. {
  720. if: 'isToolLocked',
  721. to: 'dot.creating',
  722. else: { to: 'selecting' },
  723. },
  724. ],
  725. CANCELLED: {
  726. do: 'breakSession',
  727. to: 'selecting',
  728. },
  729. },
  730. initial: 'inactive',
  731. states: {
  732. inactive: {
  733. on: {
  734. MOVED_POINTER: {
  735. if: 'distanceImpliesDrag',
  736. to: 'dot.editing.active',
  737. },
  738. },
  739. },
  740. active: {
  741. onExit: 'completeSession',
  742. onEnter: 'startTranslateSession',
  743. on: {
  744. MOVED_POINTER: 'updateTranslateSession',
  745. PANNED_CAMERA: 'updateTranslateSession',
  746. },
  747. },
  748. },
  749. },
  750. },
  751. },
  752. arrow: {
  753. onEnter: 'setActiveToolArrow',
  754. initial: 'creating',
  755. states: {
  756. creating: {
  757. on: {
  758. CANCELLED: { to: 'selecting' },
  759. POINTED_SHAPE: {
  760. get: 'newArrow',
  761. do: 'createShape',
  762. to: 'arrow.editing',
  763. },
  764. POINTED_CANVAS: {
  765. get: 'newArrow',
  766. do: 'createShape',
  767. to: 'arrow.editing',
  768. },
  769. },
  770. },
  771. editing: {
  772. onExit: 'completeSession',
  773. onEnter: 'startArrowSession',
  774. on: {
  775. STOPPED_POINTING: [
  776. 'completeSession',
  777. {
  778. if: 'isToolLocked',
  779. to: 'arrow.creating',
  780. else: { to: 'selecting' },
  781. },
  782. ],
  783. CANCELLED: {
  784. do: 'breakSession',
  785. if: 'isToolLocked',
  786. to: 'arrow.creating',
  787. else: { to: 'selecting' },
  788. },
  789. PRESSED_SHIFT: 'keyUpdateArrowSession',
  790. RELEASED_SHIFT: 'keyUpdateArrowSession',
  791. MOVED_POINTER: 'updateArrowSession',
  792. PANNED_CAMERA: 'updateArrowSession',
  793. },
  794. },
  795. },
  796. },
  797. ellipse: {
  798. onEnter: 'setActiveToolEllipse',
  799. initial: 'creating',
  800. states: {
  801. creating: {
  802. on: {
  803. CANCELLED: { to: 'selecting' },
  804. POINTED_CANVAS: {
  805. to: 'ellipse.editing',
  806. },
  807. },
  808. },
  809. editing: {
  810. on: {
  811. STOPPED_POINTING: { to: 'selecting' },
  812. CANCELLED: { to: 'selecting' },
  813. MOVED_POINTER: {
  814. if: 'distanceImpliesDrag',
  815. then: {
  816. get: 'newEllipse',
  817. do: 'createShape',
  818. to: 'drawingShape.bounds',
  819. },
  820. },
  821. },
  822. },
  823. },
  824. },
  825. rectangle: {
  826. onEnter: 'setActiveToolRectangle',
  827. initial: 'creating',
  828. states: {
  829. creating: {
  830. on: {
  831. CANCELLED: { to: 'selecting' },
  832. POINTED_SHAPE: {
  833. to: 'rectangle.editing',
  834. },
  835. POINTED_CANVAS: {
  836. to: 'rectangle.editing',
  837. },
  838. },
  839. },
  840. editing: {
  841. on: {
  842. STOPPED_POINTING: { to: 'selecting' },
  843. CANCELLED: { to: 'selecting' },
  844. MOVED_POINTER: {
  845. if: 'distanceImpliesDrag',
  846. then: {
  847. get: 'newRectangle',
  848. do: 'createShape',
  849. to: 'drawingShape.bounds',
  850. },
  851. },
  852. },
  853. },
  854. },
  855. },
  856. text: {
  857. onEnter: 'setActiveToolText',
  858. initial: 'creating',
  859. states: {
  860. creating: {
  861. on: {
  862. CANCELLED: { to: 'selecting' },
  863. POINTED_SHAPE: [
  864. {
  865. get: 'newText',
  866. do: 'createShape',
  867. },
  868. {
  869. get: 'firstSelectedShape',
  870. if: 'canEditSelectedShape',
  871. do: 'setEditingId',
  872. to: 'editingShape',
  873. },
  874. ],
  875. POINTED_CANVAS: [
  876. {
  877. get: 'newText',
  878. do: 'createShape',
  879. to: 'editingShape',
  880. },
  881. ],
  882. },
  883. },
  884. },
  885. },
  886. ray: {
  887. onEnter: 'setActiveToolRay',
  888. initial: 'creating',
  889. states: {
  890. creating: {
  891. on: {
  892. CANCELLED: { to: 'selecting' },
  893. POINTED_SHAPE: {
  894. get: 'newRay',
  895. do: 'createShape',
  896. to: 'ray.editing',
  897. },
  898. POINTED_CANVAS: {
  899. get: 'newRay',
  900. do: 'createShape',
  901. to: 'ray.editing',
  902. },
  903. },
  904. },
  905. editing: {
  906. on: {
  907. STOPPED_POINTING: { to: 'selecting' },
  908. CANCELLED: { to: 'selecting' },
  909. MOVED_POINTER: {
  910. if: 'distanceImpliesDrag',
  911. to: 'drawingShape.direction',
  912. },
  913. },
  914. },
  915. },
  916. },
  917. line: {
  918. onEnter: 'setActiveToolLine',
  919. initial: 'creating',
  920. states: {
  921. creating: {
  922. on: {
  923. CANCELLED: { to: 'selecting' },
  924. POINTED_SHAPE: {
  925. get: 'newLine',
  926. do: 'createShape',
  927. to: 'line.editing',
  928. },
  929. POINTED_CANVAS: {
  930. get: 'newLine',
  931. do: 'createShape',
  932. to: 'line.editing',
  933. },
  934. },
  935. },
  936. editing: {
  937. on: {
  938. STOPPED_POINTING: { to: 'selecting' },
  939. CANCELLED: { to: 'selecting' },
  940. MOVED_POINTER: {
  941. if: 'distanceImpliesDrag',
  942. to: 'drawingShape.direction',
  943. },
  944. },
  945. },
  946. },
  947. },
  948. polyline: {
  949. onEnter: 'setActiveToolPolyline',
  950. },
  951. },
  952. },
  953. drawingShape: {
  954. onExit: 'completeSession',
  955. on: {
  956. STOPPED_POINTING: [
  957. 'completeSession',
  958. {
  959. if: 'isToolLocked',
  960. to: 'usingTool.previous',
  961. else: { to: 'selecting' },
  962. },
  963. ],
  964. CANCELLED: {
  965. do: 'breakSession',
  966. to: 'selecting',
  967. },
  968. },
  969. initial: 'drawingShapeBounds',
  970. states: {
  971. bounds: {
  972. onEnter: 'startDrawTransformSession',
  973. on: {
  974. MOVED_POINTER: 'updateTransformSession',
  975. PANNED_CAMERA: 'updateTransformSession',
  976. },
  977. },
  978. direction: {
  979. onEnter: 'startDirectionSession',
  980. onExit: 'completeSession',
  981. on: {
  982. MOVED_POINTER: 'updateDirectionSession',
  983. PANNED_CAMERA: 'updateDirectionSession',
  984. },
  985. },
  986. },
  987. },
  988. },
  989. },
  990. },
  991. results: {
  992. newDot() {
  993. return ShapeType.Dot
  994. },
  995. newRay() {
  996. return ShapeType.Ray
  997. },
  998. newLine() {
  999. return ShapeType.Line
  1000. },
  1001. newText() {
  1002. return ShapeType.Text
  1003. },
  1004. newDraw() {
  1005. return ShapeType.Draw
  1006. },
  1007. newArrow() {
  1008. return ShapeType.Arrow
  1009. },
  1010. newEllipse() {
  1011. return ShapeType.Ellipse
  1012. },
  1013. newRectangle() {
  1014. return ShapeType.Rectangle
  1015. },
  1016. firstSelectedShape(data) {
  1017. return getSelectedShapes(data)[0]
  1018. },
  1019. editingShape(data) {
  1020. return getShape(data, data.editingId)
  1021. },
  1022. },
  1023. conditions: {
  1024. shouldDeleteShape(data, payload, shape: Shape) {
  1025. return getShapeUtils(shape).shouldDelete(shape)
  1026. },
  1027. isPointingCanvas(data, payload: PointerInfo) {
  1028. return payload.target === 'canvas'
  1029. },
  1030. isPointingBounds(data, payload: PointerInfo) {
  1031. return getSelectedIds(data).size > 0 && payload.target === 'bounds'
  1032. },
  1033. isPointingShape(data, payload: PointerInfo) {
  1034. return (
  1035. payload.target &&
  1036. payload.target !== 'canvas' &&
  1037. payload.target !== 'bounds'
  1038. )
  1039. },
  1040. isReadOnly(data) {
  1041. return data.isReadOnly
  1042. },
  1043. isInSession() {
  1044. return session.isInSession
  1045. },
  1046. canEditSelectedShape(data, payload, result: Shape) {
  1047. return getShapeUtils(result).canEdit && !result.isLocked
  1048. },
  1049. distanceImpliesDrag(data, payload: PointerInfo) {
  1050. return vec.dist2(payload.origin, payload.point) > 8
  1051. },
  1052. hasPointedTarget(data, payload: PointerInfo) {
  1053. return payload.target !== undefined
  1054. },
  1055. isPointedShapeSelected(data) {
  1056. return getSelectedIds(data).has(data.pointedId)
  1057. },
  1058. isPressingShiftKey(data, payload: PointerInfo) {
  1059. return payload.shiftKey
  1060. },
  1061. isPressingMetaKey(data, payload: PointerInfo) {
  1062. return payload.metaKey
  1063. },
  1064. shapeIsHovered(data, payload: { target: string }) {
  1065. return data.hoveredId === payload.target
  1066. },
  1067. pointInSelectionBounds(data, payload: PointerInfo) {
  1068. const bounds = getSelectionBounds(data)
  1069. if (!bounds) return false
  1070. return pointInBounds(screenToWorld(payload.point, data), bounds)
  1071. },
  1072. pointHitsShape(data, payload: PointerInfo) {
  1073. const shape = getShape(data, payload.target)
  1074. return getShapeUtils(shape).hitTest(
  1075. shape,
  1076. screenToWorld(payload.point, data)
  1077. )
  1078. },
  1079. hasPointedId(data, payload: PointerInfo) {
  1080. return getShape(data, payload.target) !== undefined
  1081. },
  1082. isPointingRotationHandle(
  1083. data,
  1084. payload: { target: Edge | Corner | 'rotate' }
  1085. ) {
  1086. return payload.target === 'rotate'
  1087. },
  1088. hasSelection(data) {
  1089. return getSelectedIds(data).size > 0
  1090. },
  1091. hasSingleSelection(data) {
  1092. return getSelectedIds(data).size === 1
  1093. },
  1094. hasMultipleSelection(data) {
  1095. return getSelectedIds(data).size > 1
  1096. },
  1097. isToolLocked(data) {
  1098. return data.settings.isToolLocked
  1099. },
  1100. isPenLocked(data) {
  1101. return data.settings.isPenLocked
  1102. },
  1103. hasOnlyOnePage(data) {
  1104. return Object.keys(data.document.pages).length === 1
  1105. },
  1106. selectionIncludesGroups(data) {
  1107. return getSelectedShapes(data).some(
  1108. (shape) => shape.type === ShapeType.Group
  1109. )
  1110. },
  1111. },
  1112. actions: {
  1113. toggleReadOnly(data) {
  1114. data.isReadOnly = !data.isReadOnly
  1115. },
  1116. /* ---------------------- Pages --------------------- */
  1117. changePage(data, payload: { id: string }) {
  1118. commands.changePage(data, payload.id)
  1119. },
  1120. createPage(data) {
  1121. commands.createPage(data, true)
  1122. },
  1123. deletePage(data, payload: { id: string }) {
  1124. commands.deletePage(data, payload.id)
  1125. },
  1126. /* --------------------- Shapes --------------------- */
  1127. resetShapes(data) {
  1128. const page = getPage(data)
  1129. Object.values(page.shapes).forEach((shape) => {
  1130. page.shapes[shape.id] = { ...shape }
  1131. })
  1132. },
  1133. createShape(data, payload, type: ShapeType) {
  1134. const shape = createShape(type, {
  1135. parentId: data.currentPageId,
  1136. point: vec.round(screenToWorld(payload.point, data)),
  1137. style: deepClone(data.currentStyle),
  1138. })
  1139. const siblings = getChildren(data, shape.parentId)
  1140. const childIndex = siblings.length
  1141. ? siblings[siblings.length - 1].childIndex + 1
  1142. : 1
  1143. data.editingId = shape.id
  1144. getShapeUtils(shape).setProperty(shape, 'childIndex', childIndex)
  1145. getPage(data).shapes[shape.id] = shape
  1146. setSelectedIds(data, [shape.id])
  1147. },
  1148. /* -------------------- Sessions -------------------- */
  1149. // Shared
  1150. breakSession(data) {
  1151. session.cancel(data)
  1152. history.disable()
  1153. commands.deleteSelected(data)
  1154. history.enable()
  1155. },
  1156. cancelSession(data) {
  1157. session.cancel(data)
  1158. },
  1159. completeSession(data) {
  1160. session.complete(data)
  1161. },
  1162. // Editing
  1163. startEditSession(data) {
  1164. session.begin(new Sessions.EditSession(data))
  1165. },
  1166. updateEditSession(data, payload: { change: Partial<Shape> }) {
  1167. session.update<Sessions.EditSession>(data, payload.change)
  1168. },
  1169. // Brushing
  1170. startBrushSession(data, payload: PointerInfo) {
  1171. session.begin(
  1172. new Sessions.BrushSession(data, screenToWorld(payload.point, data))
  1173. )
  1174. },
  1175. updateBrushSession(data, payload: PointerInfo) {
  1176. session.update<Sessions.BrushSession>(
  1177. data,
  1178. screenToWorld(payload.point, data)
  1179. )
  1180. },
  1181. // Rotating
  1182. startRotateSession(data, payload: PointerInfo) {
  1183. session.begin(
  1184. new Sessions.RotateSession(data, screenToWorld(payload.point, data))
  1185. )
  1186. },
  1187. keyUpdateRotateSession(data, payload: PointerInfo) {
  1188. session.update<Sessions.RotateSession>(
  1189. data,
  1190. screenToWorld(inputs.pointer.point, data),
  1191. payload.shiftKey
  1192. )
  1193. },
  1194. updateRotateSession(data, payload: PointerInfo) {
  1195. session.update<Sessions.RotateSession>(
  1196. data,
  1197. screenToWorld(payload.point, data),
  1198. payload.shiftKey
  1199. )
  1200. },
  1201. // Dragging / Translating
  1202. startTranslateSession(data) {
  1203. session.begin(
  1204. new Sessions.TranslateSession(
  1205. data,
  1206. screenToWorld(inputs.pointer.origin, data)
  1207. )
  1208. )
  1209. },
  1210. keyUpdateTranslateSession(
  1211. data,
  1212. payload: { shiftKey: boolean; altKey: boolean }
  1213. ) {
  1214. session.update<Sessions.TranslateSession>(
  1215. data,
  1216. screenToWorld(inputs.pointer.point, data),
  1217. payload.shiftKey,
  1218. payload.altKey
  1219. )
  1220. },
  1221. updateTranslateSession(data, payload: PointerInfo) {
  1222. session.update<Sessions.TranslateSession>(
  1223. data,
  1224. screenToWorld(payload.point, data),
  1225. payload.shiftKey,
  1226. payload.altKey
  1227. )
  1228. },
  1229. // Handles
  1230. doublePointHandle(data, payload: PointerInfo) {
  1231. const id = setToArray(getSelectedIds(data))[0]
  1232. commands.doublePointHandle(data, id, payload)
  1233. },
  1234. // Dragging Handle
  1235. startHandleSession(data, payload: PointerInfo) {
  1236. const shapeId = Array.from(getSelectedIds(data).values())[0]
  1237. const handleId = payload.target
  1238. session.begin(
  1239. new Sessions.HandleSession(
  1240. data,
  1241. shapeId,
  1242. handleId,
  1243. screenToWorld(inputs.pointer.origin, data)
  1244. )
  1245. )
  1246. },
  1247. keyUpdateHandleSession(
  1248. data,
  1249. payload: { shiftKey: boolean; altKey: boolean }
  1250. ) {
  1251. session.update<Sessions.HandleSession>(
  1252. data,
  1253. screenToWorld(inputs.pointer.point, data),
  1254. payload.shiftKey
  1255. )
  1256. },
  1257. updateHandleSession(data, payload: PointerInfo) {
  1258. session.update<Sessions.HandleSession>(
  1259. data,
  1260. screenToWorld(payload.point, data),
  1261. payload.shiftKey
  1262. )
  1263. },
  1264. // Transforming
  1265. startTransformSession(
  1266. data,
  1267. payload: PointerInfo & { target: Corner | Edge }
  1268. ) {
  1269. const point = screenToWorld(inputs.pointer.origin, data)
  1270. session.begin(
  1271. getSelectedIds(data).size === 1
  1272. ? new Sessions.TransformSingleSession(data, payload.target, point)
  1273. : new Sessions.TransformSession(data, payload.target, point)
  1274. )
  1275. },
  1276. startDrawTransformSession(data, payload: PointerInfo) {
  1277. session.begin(
  1278. new Sessions.TransformSingleSession(
  1279. data,
  1280. Corner.BottomRight,
  1281. screenToWorld(payload.point, data),
  1282. true
  1283. )
  1284. )
  1285. },
  1286. keyUpdateTransformSession(data, payload: PointerInfo) {
  1287. session.update<Sessions.TransformSession>(
  1288. data,
  1289. screenToWorld(inputs.pointer.point, data),
  1290. payload.shiftKey
  1291. )
  1292. },
  1293. updateTransformSession(data, payload: PointerInfo) {
  1294. session.update<Sessions.TransformSession>(
  1295. data,
  1296. screenToWorld(payload.point, data),
  1297. payload.shiftKey
  1298. )
  1299. },
  1300. // Direction
  1301. startDirectionSession(data) {
  1302. session.begin(
  1303. new Sessions.DirectionSession(
  1304. data,
  1305. screenToWorld(inputs.pointer.origin, data)
  1306. )
  1307. )
  1308. },
  1309. updateDirectionSession(data, payload: PointerInfo) {
  1310. session.update<Sessions.DirectionSession>(
  1311. data,
  1312. screenToWorld(payload.point, data)
  1313. )
  1314. },
  1315. // Drawing
  1316. startDrawSession(data) {
  1317. const id = Array.from(getSelectedIds(data).values())[0]
  1318. session.begin(
  1319. new Sessions.DrawSession(
  1320. data,
  1321. id,
  1322. screenToWorld(inputs.pointer.origin, data)
  1323. )
  1324. )
  1325. },
  1326. keyUpdateDrawSession(data, payload: PointerInfo) {
  1327. session.update<Sessions.DrawSession>(
  1328. data,
  1329. screenToWorld(inputs.pointer.point, data),
  1330. payload.pressure,
  1331. payload.shiftKey
  1332. )
  1333. },
  1334. updateDrawSession(data, payload: PointerInfo) {
  1335. session.update<Sessions.DrawSession>(
  1336. data,
  1337. screenToWorld(payload.point, data),
  1338. payload.pressure,
  1339. payload.shiftKey
  1340. )
  1341. },
  1342. // Arrow
  1343. startArrowSession(data, payload: PointerInfo) {
  1344. const id = Array.from(getSelectedIds(data).values())[0]
  1345. session.begin(
  1346. new Sessions.ArrowSession(
  1347. data,
  1348. id,
  1349. screenToWorld(inputs.pointer.origin, data),
  1350. payload.shiftKey
  1351. )
  1352. )
  1353. },
  1354. keyUpdateArrowSession(data, payload: PointerInfo) {
  1355. session.update<Sessions.ArrowSession>(
  1356. data,
  1357. screenToWorld(inputs.pointer.point, data),
  1358. payload.shiftKey
  1359. )
  1360. },
  1361. updateArrowSession(data, payload: PointerInfo) {
  1362. session.update<Sessions.ArrowSession>(
  1363. data,
  1364. screenToWorld(payload.point, data),
  1365. payload.shiftKey
  1366. )
  1367. },
  1368. /* -------------------- Selection ------------------- */
  1369. // Nudges
  1370. nudgeSelection(data, payload: { delta: number[]; shiftKey: boolean }) {
  1371. commands.nudge(
  1372. data,
  1373. vec.mul(
  1374. payload.delta,
  1375. payload.shiftKey
  1376. ? data.settings.nudgeDistanceLarge
  1377. : data.settings.nudgeDistanceSmall
  1378. )
  1379. )
  1380. },
  1381. clearInputs() {
  1382. inputs.clear()
  1383. },
  1384. selectAll(data) {
  1385. const selectedIds = getSelectedIds(data)
  1386. const page = getPage(data)
  1387. selectedIds.clear()
  1388. for (const id in page.shapes) {
  1389. if (page.shapes[id].parentId === data.currentPageId) {
  1390. selectedIds.add(id)
  1391. }
  1392. }
  1393. },
  1394. setHoveredId(data, payload: PointerInfo) {
  1395. data.hoveredId = payload.target
  1396. },
  1397. clearHoveredId(data) {
  1398. data.hoveredId = undefined
  1399. },
  1400. setPointedId(data, payload: PointerInfo) {
  1401. data.pointedId = getPointedId(data, payload.target)
  1402. data.currentParentId = getParentId(data, data.pointedId)
  1403. },
  1404. setDrilledPointedId(data, payload: PointerInfo) {
  1405. data.pointedId = getDrilledPointedId(data, payload.target)
  1406. data.currentParentId = getParentId(data, data.pointedId)
  1407. },
  1408. clearCurrentParentId(data) {
  1409. data.currentParentId = data.currentPageId
  1410. data.pointedId = undefined
  1411. },
  1412. clearPointedId(data) {
  1413. data.pointedId = undefined
  1414. },
  1415. clearSelectedIds(data) {
  1416. setSelectedIds(data, [])
  1417. },
  1418. pullPointedIdFromSelectedIds(data) {
  1419. const { pointedId } = data
  1420. const selectedIds = getSelectedIds(data)
  1421. selectedIds.delete(pointedId)
  1422. },
  1423. pushPointedIdToSelectedIds(data) {
  1424. getSelectedIds(data).add(data.pointedId)
  1425. },
  1426. moveSelection(data, payload: { type: MoveType }) {
  1427. commands.move(data, payload.type)
  1428. },
  1429. moveSelectionToPage(data, payload: { id: string }) {
  1430. commands.moveToPage(data, payload.id)
  1431. },
  1432. alignSelection(data, payload: { type: AlignType }) {
  1433. commands.align(data, payload.type)
  1434. },
  1435. stretchSelection(data, payload: { type: StretchType }) {
  1436. commands.stretch(data, payload.type)
  1437. },
  1438. distributeSelection(data, payload: { type: DistributeType }) {
  1439. commands.distribute(data, payload.type)
  1440. },
  1441. duplicateSelection(data) {
  1442. commands.duplicate(data)
  1443. },
  1444. lockSelection(data) {
  1445. commands.toggle(data, 'isLocked')
  1446. },
  1447. hideSelection(data) {
  1448. commands.toggle(data, 'isHidden')
  1449. },
  1450. aspectLockSelection(data) {
  1451. commands.toggle(data, 'isAspectRatioLocked')
  1452. },
  1453. deleteSelection(data) {
  1454. commands.deleteSelected(data)
  1455. },
  1456. rotateSelectionCcw(data) {
  1457. commands.rotateCcw(data)
  1458. },
  1459. groupSelection(data) {
  1460. commands.group(data)
  1461. },
  1462. ungroupSelection(data) {
  1463. commands.ungroup(data)
  1464. },
  1465. resetShapeBounds(data) {
  1466. commands.resetBounds(data)
  1467. },
  1468. resetPage(data) {
  1469. data.document.pages[data.currentPageId].shapes = {}
  1470. },
  1471. /* --------------------- Editing -------------------- */
  1472. setEditingId(data) {
  1473. const selectedShape = getSelectedShapes(data)[0]
  1474. if (getShapeUtils(selectedShape).canEdit) {
  1475. data.editingId = selectedShape.id
  1476. }
  1477. getPageState(data).selectedIds = new Set([selectedShape.id])
  1478. },
  1479. clearEditingId(data) {
  1480. data.editingId = null
  1481. },
  1482. /* ---------------------- Tool ---------------------- */
  1483. setActiveTool(data, payload: { tool: ShapeType | 'select' }) {
  1484. data.activeTool = payload.tool
  1485. },
  1486. setActiveToolSelect(data) {
  1487. data.activeTool = 'select'
  1488. },
  1489. setActiveToolDraw(data) {
  1490. data.activeTool = ShapeType.Draw
  1491. },
  1492. setActiveToolRectangle(data) {
  1493. data.activeTool = ShapeType.Rectangle
  1494. },
  1495. setActiveToolEllipse(data) {
  1496. data.activeTool = ShapeType.Ellipse
  1497. },
  1498. setActiveToolArrow(data) {
  1499. data.activeTool = ShapeType.Arrow
  1500. },
  1501. setActiveToolDot(data) {
  1502. data.activeTool = ShapeType.Dot
  1503. },
  1504. setActiveToolPolyline(data) {
  1505. data.activeTool = ShapeType.Polyline
  1506. },
  1507. setActiveToolRay(data) {
  1508. data.activeTool = ShapeType.Ray
  1509. },
  1510. setActiveToolLine(data) {
  1511. data.activeTool = ShapeType.Line
  1512. },
  1513. setActiveToolText(data) {
  1514. data.activeTool = ShapeType.Text
  1515. },
  1516. /* --------------------- Camera --------------------- */
  1517. zoomIn(data) {
  1518. const camera = getCurrentCamera(data)
  1519. const i = Math.round((camera.zoom * 100) / 25)
  1520. const center = [window.innerWidth / 2, window.innerHeight / 2]
  1521. const p0 = screenToWorld(center, data)
  1522. camera.zoom = getCameraZoom((i + 1) * 0.25)
  1523. const p1 = screenToWorld(center, data)
  1524. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  1525. setZoomCSS(camera.zoom)
  1526. },
  1527. zoomOut(data) {
  1528. const camera = getCurrentCamera(data)
  1529. const i = Math.round((camera.zoom * 100) / 25)
  1530. const center = [window.innerWidth / 2, window.innerHeight / 2]
  1531. const p0 = screenToWorld(center, data)
  1532. camera.zoom = getCameraZoom((i - 1) * 0.25)
  1533. const p1 = screenToWorld(center, data)
  1534. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  1535. setZoomCSS(camera.zoom)
  1536. },
  1537. zoomCameraToActual(data) {
  1538. const camera = getCurrentCamera(data)
  1539. const center = [window.innerWidth / 2, window.innerHeight / 2]
  1540. const p0 = screenToWorld(center, data)
  1541. camera.zoom = 1
  1542. const p1 = screenToWorld(center, data)
  1543. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  1544. setZoomCSS(camera.zoom)
  1545. },
  1546. zoomCameraToSelectionActual(data) {
  1547. const camera = getCurrentCamera(data)
  1548. const bounds = getSelectedBounds(data)
  1549. const mx = (window.innerWidth - bounds.width) / 2
  1550. const my = (window.innerHeight - bounds.height) / 2
  1551. camera.zoom = 1
  1552. camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
  1553. setZoomCSS(camera.zoom)
  1554. },
  1555. zoomCameraToSelection(data) {
  1556. const camera = getCurrentCamera(data)
  1557. const bounds = getSelectedBounds(data)
  1558. const zoom = getCameraZoom(
  1559. bounds.width > bounds.height
  1560. ? (window.innerWidth - 128) / bounds.width
  1561. : (window.innerHeight - 128) / bounds.height
  1562. )
  1563. const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
  1564. const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
  1565. camera.zoom = zoom
  1566. camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
  1567. setZoomCSS(camera.zoom)
  1568. },
  1569. zoomCameraToFit(data) {
  1570. const camera = getCurrentCamera(data)
  1571. const page = getPage(data)
  1572. const shapes = Object.values(page.shapes)
  1573. if (shapes.length === 0) {
  1574. return
  1575. }
  1576. const bounds = getCommonBounds(
  1577. ...Object.values(shapes).map((shape) =>
  1578. getShapeUtils(shape).getBounds(shape)
  1579. )
  1580. )
  1581. const zoom = getCameraZoom(
  1582. bounds.width > bounds.height
  1583. ? (window.innerWidth - 128) / bounds.width
  1584. : (window.innerHeight - 128) / bounds.height
  1585. )
  1586. const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
  1587. const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
  1588. camera.zoom = zoom
  1589. camera.point = vec.add([-bounds.minX, -bounds.minY], [mx, my])
  1590. setZoomCSS(camera.zoom)
  1591. },
  1592. zoomCamera(data, payload: { delta: number; point: number[] }) {
  1593. const camera = getCurrentCamera(data)
  1594. const next = camera.zoom - (payload.delta / 100) * camera.zoom
  1595. const p0 = screenToWorld(payload.point, data)
  1596. camera.zoom = getCameraZoom(next)
  1597. const p1 = screenToWorld(payload.point, data)
  1598. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  1599. setZoomCSS(camera.zoom)
  1600. },
  1601. panCamera(data, payload: { delta: number[] }) {
  1602. const camera = getCurrentCamera(data)
  1603. camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
  1604. },
  1605. updateZoomCSS(data) {
  1606. const camera = getCurrentCamera(data)
  1607. setZoomCSS(camera.zoom)
  1608. },
  1609. pinchCamera(
  1610. data,
  1611. payload: {
  1612. delta: number[]
  1613. distanceDelta: number
  1614. angleDelta: number
  1615. point: number[]
  1616. }
  1617. ) {
  1618. // This is usually replaced with hacks.fastPinchCamera!
  1619. const camera = getCurrentCamera(data)
  1620. camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
  1621. const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
  1622. const p0 = screenToWorld(payload.point, data)
  1623. camera.zoom = getCameraZoom(next)
  1624. const p1 = screenToWorld(payload.point, data)
  1625. camera.point = vec.add(camera.point, vec.sub(p1, p0))
  1626. setZoomCSS(camera.zoom)
  1627. },
  1628. resetCamera(data) {
  1629. const camera = getCurrentCamera(data)
  1630. camera.zoom = 1
  1631. camera.point = [window.innerWidth / 2, window.innerHeight / 2]
  1632. document.documentElement.style.setProperty('--camera-zoom', '1')
  1633. },
  1634. /* ---------------------- History ---------------------- */
  1635. // History
  1636. popHistory() {
  1637. history.pop()
  1638. },
  1639. enableHistory() {
  1640. history.enable()
  1641. },
  1642. disableHistory() {
  1643. history.disable()
  1644. },
  1645. undo(data) {
  1646. history.undo(data)
  1647. },
  1648. redo(data) {
  1649. history.redo(data)
  1650. },
  1651. resetHistory() {
  1652. history.reset()
  1653. },
  1654. /* --------------------- Styles --------------------- */
  1655. toggleStylePanel(data) {
  1656. data.settings.isStyleOpen = !data.settings.isStyleOpen
  1657. },
  1658. closeStylePanel(data) {
  1659. data.settings.isStyleOpen = false
  1660. },
  1661. updateStyles(data, payload: Partial<ShapeStyles>) {
  1662. Object.assign(data.currentStyle, payload)
  1663. },
  1664. applyStylesToSelection(data, payload: Partial<ShapeStyles>) {
  1665. commands.style(data, payload)
  1666. },
  1667. /* ---------------------- Code ---------------------- */
  1668. closeCodePanel(data) {
  1669. data.settings.isCodeOpen = false
  1670. },
  1671. openCodePanel(data) {
  1672. data.settings.isCodeOpen = true
  1673. },
  1674. toggleCodePanel(data) {
  1675. data.settings.isCodeOpen = !data.settings.isCodeOpen
  1676. },
  1677. setGeneratedShapes(
  1678. data,
  1679. payload: { shapes: Shape[]; controls: CodeControl[] }
  1680. ) {
  1681. commands.generate(data, payload.shapes)
  1682. },
  1683. updateGeneratedShapes(data, payload, result: { shapes: Shape[] }) {
  1684. setSelectedIds(data, [])
  1685. history.disable()
  1686. commands.generate(data, result.shapes)
  1687. history.enable()
  1688. },
  1689. setCodeControls(data, payload: { controls: CodeControl[] }) {
  1690. data.codeControls = Object.fromEntries(
  1691. payload.controls.map((control) => [control.id, control])
  1692. )
  1693. },
  1694. increaseCodeFontSize(data) {
  1695. data.settings.fontSize++
  1696. },
  1697. decreaseCodeFontSize(data) {
  1698. data.settings.fontSize--
  1699. },
  1700. updateControls(data, payload: { [key: string]: any }) {
  1701. for (const key in payload) {
  1702. data.codeControls[key].value = payload[key]
  1703. }
  1704. },
  1705. /* -------------------- Settings -------------------- */
  1706. enablePenLock(data) {
  1707. data.settings.isPenLocked = true
  1708. },
  1709. disablePenLock(data) {
  1710. data.settings.isPenLocked = false
  1711. },
  1712. toggleToolLock(data) {
  1713. data.settings.isToolLocked = !data.settings.isToolLocked
  1714. },
  1715. /* ------------------- Clipboard -------------------- */
  1716. copyToSvg(data) {
  1717. clipboard.copySelectionToSvg(data)
  1718. },
  1719. copyToClipboard(data) {
  1720. clipboard.copy(getSelectedShapes(data))
  1721. },
  1722. copyStateToClipboard(data) {
  1723. clipboard.copyStringToClipboard(JSON.stringify(data))
  1724. },
  1725. pasteFromClipboard() {
  1726. clipboard.paste()
  1727. },
  1728. pasteShapesFromClipboard(data, payload: { shapes: Shape[] }) {
  1729. commands.paste(data, payload.shapes)
  1730. },
  1731. /* ---------------------- Data ---------------------- */
  1732. restoreSavedData(data) {
  1733. storage.firstLoad(data)
  1734. },
  1735. saveToFileSystem(data) {
  1736. storage.saveToFileSystem(data)
  1737. },
  1738. saveAsToFileSystem(data) {
  1739. storage.saveAsToFileSystem(data)
  1740. },
  1741. loadFromFileSystem() {
  1742. storage.loadDocumentFromFilesystem()
  1743. },
  1744. loadDocumentFromJson(data, payload: { json: any }) {
  1745. storage.loadDocumentFromJson(data, payload.json)
  1746. },
  1747. forceSave(data) {
  1748. storage.saveToFileSystem(data)
  1749. },
  1750. savePage(data) {
  1751. storage.savePage(data)
  1752. },
  1753. loadPage(data) {
  1754. storage.loadPage(data)
  1755. },
  1756. saveCode(data, payload: { code: string }) {
  1757. data.document.code[data.currentCodeFileId].code = payload.code
  1758. storage.saveDocumentToLocalStorage(data)
  1759. },
  1760. clearBoundsRotation(data) {
  1761. data.boundsRotation = 0
  1762. },
  1763. },
  1764. values: {
  1765. selectedIds(data) {
  1766. return new Set(getSelectedIds(data))
  1767. },
  1768. selectedBounds(data) {
  1769. return getSelectionBounds(data)
  1770. },
  1771. currentShapes(data) {
  1772. const page = getPage(data)
  1773. return Object.values(page.shapes)
  1774. .filter((shape) => shape.parentId === page.id)
  1775. .sort((a, b) => a.childIndex - b.childIndex)
  1776. },
  1777. selectedStyle(data) {
  1778. const selectedIds = Array.from(getSelectedIds(data).values())
  1779. const { currentStyle } = data
  1780. if (selectedIds.length === 0) {
  1781. return currentStyle
  1782. }
  1783. const page = getPage(data)
  1784. const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
  1785. const commonStyle: ShapeStyles = {} as ShapeStyles
  1786. const overrides = new Set<string>([])
  1787. for (const shapeStyle of shapeStyles) {
  1788. for (const key in currentStyle) {
  1789. if (overrides.has(key)) continue
  1790. if (commonStyle[key] === undefined) {
  1791. commonStyle[key] = shapeStyle[key]
  1792. } else {
  1793. if (commonStyle[key] === shapeStyle[key]) continue
  1794. commonStyle[key] = currentStyle[key]
  1795. overrides.add(key)
  1796. }
  1797. }
  1798. }
  1799. return commonStyle
  1800. },
  1801. },
  1802. asyncs: {
  1803. async getUpdatedShapes(data) {
  1804. return updateFromCode(
  1805. data,
  1806. data.document.code[data.currentCodeFileId].code
  1807. )
  1808. },
  1809. },
  1810. })
  1811. export default state
  1812. export const useSelector = createSelectorHook(state)
  1813. function getParentId(data: Data, id: string) {
  1814. const shape = getPage(data).shapes[id]
  1815. return shape.parentId
  1816. }
  1817. function getPointedId(data: Data, id: string) {
  1818. const shape = getPage(data).shapes[id]
  1819. if (!shape) return id
  1820. return shape.parentId === data.currentParentId ||
  1821. shape.parentId === data.currentPageId
  1822. ? id
  1823. : getPointedId(data, shape.parentId)
  1824. }
  1825. function getDrilledPointedId(data: Data, id: string) {
  1826. const shape = getPage(data).shapes[id]
  1827. return shape.parentId === data.currentPageId ||
  1828. shape.parentId === data.pointedId ||
  1829. shape.parentId === data.currentParentId
  1830. ? id
  1831. : getDrilledPointedId(data, shape.parentId)
  1832. }
  1833. // function hasPointedIdInChildren(data: Data, id: string, pointedId: string) {
  1834. // const shape = getPage(data).shapes[id]
  1835. // if (shape.type !== ShapeType.Group) {
  1836. // return false
  1837. // }
  1838. // if (shape.children.includes(pointedId)) {
  1839. // return true
  1840. // }
  1841. // return shape.children.some((childId) =>
  1842. // hasPointedIdInChildren(data, childId, pointedId)
  1843. // )
  1844. // }
  1845. function getSelectionBounds(data: Data) {
  1846. const selectedIds = getSelectedIds(data)
  1847. const page = getPage(data)
  1848. const shapes = getSelectedShapes(data)
  1849. if (selectedIds.size === 0) return null
  1850. if (selectedIds.size === 1) {
  1851. if (!shapes[0]) {
  1852. console.error('Could not find that shape! Clearing selected IDs.')
  1853. setSelectedIds(data, [])
  1854. return null
  1855. }
  1856. const shape = shapes[0]
  1857. const shapeUtils = getShapeUtils(shape)
  1858. if (!shapeUtils.canTransform) return null
  1859. let bounds = shapeUtils.getBounds(shape)
  1860. let parentId = shape.parentId
  1861. while (parentId !== data.currentPageId) {
  1862. const parent = page.shapes[parentId]
  1863. bounds = rotateBounds(
  1864. bounds,
  1865. getBoundsCenter(getShapeUtils(parent).getBounds(parent)),
  1866. parent.rotation
  1867. )
  1868. bounds.rotation = parent.rotation
  1869. parentId = parent.parentId
  1870. }
  1871. return bounds
  1872. }
  1873. const uniqueSelectedShapeIds: string[] = Array.from(
  1874. new Set(
  1875. Array.from(selectedIds.values()).flatMap((id) =>
  1876. getDocumentBranch(data, id)
  1877. )
  1878. ).values()
  1879. )
  1880. const commonBounds = getCommonBounds(
  1881. ...uniqueSelectedShapeIds
  1882. .map((id) => page.shapes[id])
  1883. .filter((shape) => shape.type !== ShapeType.Group)
  1884. .map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
  1885. )
  1886. return commonBounds
  1887. }
  1888. // state.enableLog(true)
  1889. // state.onUpdate((s) => console.log(s.log.filter((l) => l !== 'MOVED_POINTER')))