Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

state.ts 46KB

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