Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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