Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

state.ts 34KB

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