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

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