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

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