TLApp.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. /* eslint-disable @typescript-eslint/no-extra-semi */
  2. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  3. /* eslint-disable @typescript-eslint/no-explicit-any */
  4. import { Vec } from '@tldraw/vec'
  5. import { action, computed, makeObservable, observable, toJS, transaction } from 'mobx'
  6. import { BoundsUtils, KeyUtils } from '~utils'
  7. import {
  8. TLSelectTool,
  9. TLInputs,
  10. TLPage,
  11. TLViewport,
  12. TLShape,
  13. TLPageModel,
  14. TLToolConstructor,
  15. TLShapeConstructor,
  16. TLShapeModel,
  17. } from '~lib'
  18. import type {
  19. TLBounds,
  20. TLEvents,
  21. TLSubscription,
  22. TLSubscriptionEventInfo,
  23. TLSubscriptionEventName,
  24. TLCallback,
  25. TLShortcut,
  26. TLEventMap,
  27. TLStateEvents,
  28. TLAsset,
  29. } from '~types'
  30. import { TLHistory } from '../TLHistory'
  31. import { TLSettings } from '../TLSettings'
  32. import { TLRootState } from '../TLState'
  33. import { TLApi } from '~lib/TLApi'
  34. import { TLCursors } from '~lib/TLCursors'
  35. import { GRID_SIZE } from '~constants'
  36. export interface TLDocumentModel<S extends TLShape = TLShape, A extends TLAsset = TLAsset> {
  37. currentPageId: string
  38. selectedIds: string[]
  39. pages: TLPageModel<S>[]
  40. assets?: A[]
  41. }
  42. export class TLApp<
  43. S extends TLShape = TLShape,
  44. K extends TLEventMap = TLEventMap
  45. > extends TLRootState<S, K> {
  46. constructor(
  47. serializedApp?: TLDocumentModel<S>,
  48. Shapes?: TLShapeConstructor<S>[],
  49. Tools?: TLToolConstructor<S, K>[]
  50. ) {
  51. super()
  52. this._states = [TLSelectTool]
  53. this.history.pause()
  54. if (this.states && this.states.length > 0) {
  55. this.registerStates(this.states)
  56. const initialId = this.initial ?? this.states[0].id
  57. const state = this.children.get(initialId)
  58. if (state) {
  59. this.currentState = state
  60. this.currentState?._events.onEnter({ fromId: 'initial' })
  61. }
  62. }
  63. if (Shapes) this.registerShapes(Shapes)
  64. if (Tools) this.registerTools(Tools)
  65. this.history.resume()
  66. if (serializedApp) this.history.deserialize(serializedApp)
  67. const ownShortcuts: TLShortcut<S, K>[] = [
  68. {
  69. keys: 'mod+shift+g',
  70. fn: () => this.api.toggleGrid(),
  71. },
  72. {
  73. keys: 'shift+0',
  74. fn: () => this.api.resetZoom(),
  75. },
  76. {
  77. keys: 'mod+-',
  78. fn: () => this.api.zoomToSelection(),
  79. },
  80. {
  81. keys: 'mod+-',
  82. fn: () => this.api.zoomOut(),
  83. },
  84. {
  85. keys: 'mod+=',
  86. fn: () => this.api.zoomIn(),
  87. },
  88. {
  89. keys: 'mod+z',
  90. fn: () => this.undo(),
  91. },
  92. {
  93. keys: 'mod+shift+z',
  94. fn: () => this.redo(),
  95. },
  96. {
  97. keys: '[',
  98. fn: () => this.sendBackward(),
  99. },
  100. {
  101. keys: 'shift+[',
  102. fn: () => this.sendToBack(),
  103. },
  104. {
  105. keys: ']',
  106. fn: () => this.bringForward(),
  107. },
  108. {
  109. keys: 'shift+]',
  110. fn: () => this.bringToFront(),
  111. },
  112. {
  113. keys: 'mod+a',
  114. fn: () => {
  115. const { selectedTool } = this
  116. if (selectedTool.currentState.id !== 'idle') return
  117. if (selectedTool.id !== 'select') {
  118. this.selectTool('select')
  119. }
  120. this.api.selectAll()
  121. },
  122. },
  123. {
  124. keys: 'mod+s',
  125. fn: () => {
  126. this.save()
  127. this.notify('save', null)
  128. },
  129. },
  130. {
  131. keys: 'mod+shift+s',
  132. fn: () => {
  133. this.saveAs()
  134. this.notify('saveAs', null)
  135. },
  136. },
  137. ]
  138. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  139. // @ts-ignore
  140. const shortcuts = (this.constructor['shortcuts'] || []) as TLShortcut<S, K>[]
  141. this._disposables.push(
  142. ...[...ownShortcuts, ...shortcuts].map(({ keys, fn }) => {
  143. return KeyUtils.registerShortcut(keys, e => {
  144. fn(this, this, e)
  145. })
  146. })
  147. )
  148. this.api = new TLApi(this)
  149. makeObservable(this)
  150. this.notify('mount', null)
  151. }
  152. static id = 'app'
  153. static initial = 'select'
  154. readonly api: TLApi<S, K>
  155. readonly inputs = new TLInputs<K>()
  156. readonly cursors = new TLCursors()
  157. readonly viewport = new TLViewport()
  158. readonly settings = new TLSettings()
  159. /* --------------------- History -------------------- */
  160. history = new TLHistory<S, K>(this)
  161. persist = this.history.persist
  162. undo = this.history.undo
  163. redo = this.history.redo
  164. saving = false // used to capture direct mutations as part of the history stack
  165. saveState = () => {
  166. if (this.history.isPaused) return
  167. this.saving = true
  168. requestAnimationFrame(() => {
  169. if (this.saving) {
  170. this.persist()
  171. this.saving = false
  172. }
  173. })
  174. }
  175. /* -------------------------------------------------- */
  176. /* Document */
  177. /* -------------------------------------------------- */
  178. loadDocumentModel(model: TLDocumentModel<S>): this {
  179. this.history.deserialize(model)
  180. if (model.assets) this.addAssets(model.assets)
  181. return this
  182. }
  183. load = (): this => {
  184. // todo
  185. this.notify('load', null)
  186. return this
  187. }
  188. save = (): this => {
  189. // todo
  190. this.notify('save', null)
  191. return this
  192. }
  193. saveAs = (): this => {
  194. // todo
  195. this.notify('saveAs', null)
  196. return this
  197. }
  198. @computed get serialized(): TLDocumentModel<S> {
  199. return {
  200. currentPageId: this.currentPageId,
  201. selectedIds: Array.from(this.selectedIds.values()),
  202. pages: Array.from(this.pages.values()).map(page => page.serialized),
  203. assets: Object.values(this.assets),
  204. }
  205. }
  206. /* ---------------------- Pages --------------------- */
  207. @observable pages: Map<string, TLPage<S, K>> = new Map([
  208. ['page', new TLPage(this, { id: 'page', name: 'page', shapes: [], bindings: {} })],
  209. ])
  210. @observable currentPageId = 'page'
  211. @computed get currentPage(): TLPage<S, K> {
  212. return this.getPageById(this.currentPageId)
  213. }
  214. getPageById = (pageId: string): TLPage<S, K> => {
  215. const page = this.pages.get(pageId)
  216. if (!page) throw Error(`Could not find a page named ${pageId}.`)
  217. return page
  218. }
  219. @action setCurrentPage(page: string | TLPage<S, K>): this {
  220. this.currentPageId = typeof page === 'string' ? page : page.id
  221. return this
  222. }
  223. @action addPages(pages: TLPage<S, K>[]): this {
  224. pages.forEach(page => this.pages.set(page.id, page))
  225. this.persist()
  226. return this
  227. }
  228. @action removePages(pages: TLPage<S, K>[]): this {
  229. pages.forEach(page => this.pages.delete(page.id))
  230. this.persist()
  231. return this
  232. }
  233. /* --------------------- Shapes --------------------- */
  234. getShapeById = <T extends S>(id: string, pageId = this.currentPage.id): T | undefined => {
  235. const shape = this.getPageById(pageId)?.shapes.find(shape => shape.id === id) as T
  236. return shape
  237. }
  238. @action readonly createShapes = (shapes: S[] | TLShapeModel[]): this => {
  239. const newShapes = this.currentPage.addShapes(...shapes)
  240. if (newShapes) this.notify('create-shapes', newShapes)
  241. this.persist()
  242. return this
  243. }
  244. @action updateShapes = <T extends S>(shapes: ({ id: string } & Partial<T['props']>)[]): this => {
  245. shapes.forEach(shape => this.getShapeById(shape.id)?.update(shape))
  246. this.persist()
  247. return this
  248. }
  249. @action readonly deleteShapes = (shapes: S[] | string[]): this => {
  250. if (shapes.length === 0) return this
  251. let ids: Set<string>
  252. if (typeof shapes[0] === 'string') {
  253. ids = new Set(shapes as string[])
  254. } else {
  255. ids = new Set((shapes as S[]).map(shape => shape.id))
  256. }
  257. this.setSelectedShapes(this.selectedShapesArray.filter(shape => !ids.has(shape.id)))
  258. const removedShapes = this.currentPage.removeShapes(...shapes)
  259. if (removedShapes) this.notify('delete-shapes', removedShapes)
  260. this.persist()
  261. return this
  262. }
  263. bringForward = (shapes: S[] | string[] = this.selectedShapesArray): this => {
  264. if (shapes.length > 0) this.currentPage.bringForward(shapes)
  265. return this
  266. }
  267. sendBackward = (shapes: S[] | string[] = this.selectedShapesArray): this => {
  268. if (shapes.length > 0) this.currentPage.sendBackward(shapes)
  269. return this
  270. }
  271. sendToBack = (shapes: S[] | string[] = this.selectedShapesArray): this => {
  272. if (shapes.length > 0) this.currentPage.sendToBack(shapes)
  273. return this
  274. }
  275. bringToFront = (shapes: S[] | string[] = this.selectedShapesArray): this => {
  276. if (shapes.length > 0) this.currentPage.bringToFront(shapes)
  277. return this
  278. }
  279. flipHorizontal = (shapes: S[] | string[] = this.selectedShapesArray): this => {
  280. this.currentPage.flip(shapes, 'horizontal')
  281. return this
  282. }
  283. flipVertical = (shapes: S[] | string[] = this.selectedShapesArray): this => {
  284. this.currentPage.flip(shapes, 'vertical')
  285. return this
  286. }
  287. /* --------------------- Assets --------------------- */
  288. @observable assets: Record<string, TLAsset> = {}
  289. @action addAssets<T extends TLAsset>(assets: T[]): this {
  290. assets.forEach(asset => (this.assets[asset.id] = asset))
  291. this.persist()
  292. return this
  293. }
  294. @action removeAssets<T extends TLAsset>(assets: T[] | string[]): this {
  295. if (typeof assets[0] === 'string')
  296. (assets as string[]).forEach(asset => delete this.assets[asset])
  297. else (assets as T[]).forEach(asset => delete this.assets[(asset as T).id])
  298. this.persist()
  299. return this
  300. }
  301. createAssets<T extends TLAsset>(assets: T[]): this {
  302. this.addAssets(assets)
  303. this.notify('create-assets', { assets })
  304. this.persist()
  305. return this
  306. }
  307. copy = () => {
  308. if (this.selectedShapesArray.length > 0) {
  309. const tldrawString = JSON.stringify({
  310. type: 'logseq/whiteboard-shapes',
  311. shapes: this.selectedShapesArray.map(shape => shape.serialized),
  312. })
  313. navigator.clipboard.write([
  314. new ClipboardItem({
  315. 'text/plain': new Blob([tldrawString], { type: 'text/plain' }),
  316. }),
  317. ])
  318. }
  319. }
  320. paste = (e?: ClipboardEvent) => {
  321. this.notify('paste', {
  322. point: this.inputs.currentPoint,
  323. })
  324. // This callback may be over-written manually, see useSetup.ts in React.
  325. return void null
  326. }
  327. dropFiles = (files: FileList, point?: number[]) => {
  328. this.notify('drop-files', {
  329. files: Array.from(files),
  330. point: point
  331. ? this.viewport.getPagePoint(point)
  332. : BoundsUtils.getBoundsCenter(this.viewport.currentView),
  333. })
  334. // This callback may be over-written manually, see useSetup.ts in React.
  335. return void null
  336. }
  337. /* ---------------------- Tools --------------------- */
  338. @computed get selectedTool() {
  339. return this.currentState
  340. }
  341. selectTool = this.transition
  342. registerTools = this.registerStates
  343. /* ------------------ Editing Shape ----------------- */
  344. @observable editingId?: string
  345. @computed get editingShape(): S | undefined {
  346. const { editingId, currentPage } = this
  347. return editingId ? currentPage.shapes.find(shape => shape.id === editingId) : undefined
  348. }
  349. @action readonly setEditingShape = (shape?: string | S): this => {
  350. this.editingId = typeof shape === 'string' ? shape : shape?.id
  351. return this
  352. }
  353. readonly clearEditingShape = (): this => {
  354. return this.setEditingShape()
  355. }
  356. /* ------------------ Hovered Shape ----------------- */
  357. @observable hoveredId?: string
  358. @computed get hoveredShape(): S | undefined {
  359. const { hoveredId, currentPage } = this
  360. return hoveredId ? currentPage.shapes.find(shape => shape.id === hoveredId) : undefined
  361. }
  362. @action readonly setHoveredShape = (shape?: string | S): this => {
  363. this.hoveredId = typeof shape === 'string' ? shape : shape?.id
  364. return this
  365. }
  366. /* ----------------- Activated Shapes ---------------- */
  367. @observable activatedIds: Set<string> = new Set()
  368. @computed get activatedShapes(): S[] {
  369. const { activatedIds, currentPage, selectedTool } = this
  370. const stateId = selectedTool.id
  371. if (stateId !== 'select') return []
  372. return currentPage.shapes.filter(shape => activatedIds.has(shape.id))
  373. }
  374. @action readonly setActivatedShapes = (shapes: S[] | string[]): this => {
  375. this.activatedIds.clear()
  376. if (typeof shapes[0] === 'string')
  377. (shapes as string[]).forEach(shape => this.activatedIds.add(shape))
  378. else (shapes as S[]).forEach(shape => this.activatedIds.add(shape.id))
  379. return this
  380. }
  381. /* ----------------- Selected Shapes ---------------- */
  382. @observable selectedIds: Set<string> = new Set()
  383. @observable selectedShapes: Set<S> = new Set()
  384. @observable selectionRotation = 0
  385. @computed get selectedShapesArray() {
  386. const { selectedShapes, selectedTool } = this
  387. const stateId = selectedTool.id
  388. if (stateId !== 'select') return []
  389. return Array.from(selectedShapes.values())
  390. }
  391. @action setSelectedShapes = (shapes: S[] | string[]): this => {
  392. const { selectedIds, selectedShapes } = this
  393. selectedIds.clear()
  394. selectedShapes.clear()
  395. if (shapes[0] && typeof shapes[0] === 'string') {
  396. ;(shapes as string[]).forEach(s => selectedIds.add(s))
  397. } else {
  398. ;(shapes as S[]).forEach(s => selectedIds.add(s.id))
  399. }
  400. const newSelectedShapes = this.currentPage.shapes.filter(shape => selectedIds.has(shape.id))
  401. newSelectedShapes.forEach(s => selectedShapes.add(s))
  402. if (newSelectedShapes.length === 1) {
  403. this.selectionRotation = newSelectedShapes[0].props.rotation ?? 0
  404. } else {
  405. this.selectionRotation = 0
  406. }
  407. return this
  408. }
  409. @action setSelectionRotation(radians: number) {
  410. this.selectionRotation = radians
  411. }
  412. /* ------------------ Erasing Shape ----------------- */
  413. @observable erasingIds: Set<string> = new Set()
  414. @observable erasingShapes: Set<S> = new Set()
  415. @computed get erasingShapesArray() {
  416. return Array.from(this.erasingShapes.values())
  417. }
  418. @action readonly setErasingShapes = (shapes: S[] | string[]): this => {
  419. const { erasingIds, erasingShapes } = this
  420. erasingIds.clear()
  421. erasingShapes.clear()
  422. if (shapes[0] && typeof shapes[0] === 'string') {
  423. ;(shapes as string[]).forEach(s => erasingIds.add(s))
  424. } else {
  425. ;(shapes as S[]).forEach(s => erasingIds.add(s.id))
  426. }
  427. const newErasingShapes = this.currentPage.shapes.filter(shape => erasingIds.has(shape.id))
  428. newErasingShapes.forEach(s => erasingShapes.add(s))
  429. return this
  430. }
  431. /* ------------------ Binding Shape ----------------- */
  432. @observable bindingIds?: string[]
  433. @computed get bindingShapes(): S[] | undefined {
  434. const { bindingIds, currentPage } = this
  435. return bindingIds
  436. ? currentPage.shapes.filter(shape => bindingIds?.includes(shape.id))
  437. : undefined
  438. }
  439. @action readonly setBindingShapes = (ids?: string[]): this => {
  440. this.bindingIds = ids
  441. return this
  442. }
  443. readonly clearBindingShape = (): this => {
  444. return this.setBindingShapes()
  445. }
  446. /* ---------------------- Brush --------------------- */
  447. @observable brush?: TLBounds
  448. @action readonly setBrush = (brush?: TLBounds): this => {
  449. this.brush = brush
  450. return this
  451. }
  452. /* --------------------- Camera --------------------- */
  453. @action setCamera = (point?: number[], zoom?: number): this => {
  454. this.viewport.update({ point, zoom })
  455. return this
  456. }
  457. readonly getPagePoint = (point: number[]): number[] => {
  458. const { camera } = this.viewport
  459. return Vec.sub(Vec.div(point, camera.zoom), camera.point)
  460. }
  461. readonly getScreenPoint = (point: number[]): number[] => {
  462. const { camera } = this.viewport
  463. return Vec.mul(Vec.add(point, camera.point), camera.zoom)
  464. }
  465. @computed
  466. get currentGrid() {
  467. const { zoom } = this.viewport.camera
  468. if (zoom < 0.15) {
  469. return GRID_SIZE * 16
  470. } else if (zoom < 1) {
  471. return GRID_SIZE * 4
  472. } else {
  473. return GRID_SIZE * 1
  474. }
  475. }
  476. /* --------------------- Display -------------------- */
  477. @computed get shapes(): S[] {
  478. const {
  479. currentPage: { shapes },
  480. } = this
  481. return Array.from(shapes.values())
  482. }
  483. @computed get shapesInViewport(): S[] {
  484. const {
  485. selectedShapes,
  486. currentPage,
  487. viewport: { currentView },
  488. } = this
  489. return currentPage.shapes.filter(shape => {
  490. return (
  491. shape.props.parentId === currentPage.id &&
  492. (!shape.canUnmount ||
  493. selectedShapes.has(shape) ||
  494. BoundsUtils.boundsContain(currentView, shape.rotatedBounds) ||
  495. BoundsUtils.boundsCollide(currentView, shape.rotatedBounds))
  496. )
  497. })
  498. }
  499. @computed get selectionDirectionHint(): number[] | undefined {
  500. const {
  501. selectionBounds,
  502. viewport: { currentView },
  503. } = this
  504. if (
  505. !selectionBounds ||
  506. BoundsUtils.boundsContain(currentView, selectionBounds) ||
  507. BoundsUtils.boundsCollide(currentView, selectionBounds)
  508. ) {
  509. return
  510. }
  511. const center = BoundsUtils.getBoundsCenter(selectionBounds)
  512. return Vec.clampV(
  513. [
  514. (center[0] - currentView.minX - currentView.width / 2) / currentView.width,
  515. (center[1] - currentView.minY - currentView.height / 2) / currentView.height,
  516. ],
  517. -1,
  518. 1
  519. )
  520. }
  521. @computed get selectionBounds(): TLBounds | undefined {
  522. const { selectedShapesArray } = this
  523. if (selectedShapesArray.length === 0) return undefined
  524. if (selectedShapesArray.length === 1) {
  525. return { ...selectedShapesArray[0].bounds, rotation: selectedShapesArray[0].props.rotation }
  526. }
  527. return BoundsUtils.getCommonBounds(this.selectedShapesArray.map(shape => shape.rotatedBounds))
  528. }
  529. @computed get showSelection() {
  530. const { selectedShapesArray } = this
  531. return (
  532. this.isIn('select') &&
  533. !this.isInAny('select.translating', 'select.pinching') &&
  534. ((selectedShapesArray.length === 1 && !selectedShapesArray[0]?.hideSelection) ||
  535. selectedShapesArray.length > 1)
  536. )
  537. }
  538. @computed get showSelectionDetail() {
  539. return (
  540. this.isIn('select') &&
  541. !this.isInAny('select.translating', 'select.pinching') &&
  542. this.selectedShapes.size > 0 &&
  543. !this.selectedShapesArray.every(shape => shape.hideSelectionDetail)
  544. )
  545. }
  546. @computed get showSelectionRotation() {
  547. return (
  548. this.showSelectionDetail && this.isInAny('select.rotating', 'select.pointingRotateHandle')
  549. )
  550. }
  551. @computed get showContextBar() {
  552. const {
  553. selectedShapesArray,
  554. inputs: { ctrlKey },
  555. } = this
  556. return (
  557. !ctrlKey &&
  558. this.isInAny('select.idle', 'select.hoveringSelectionHandle') &&
  559. selectedShapesArray.length > 0 &&
  560. !selectedShapesArray.every(shape => shape.hideContextBar)
  561. )
  562. }
  563. @computed get showRotateHandles() {
  564. const { selectedShapesArray } = this
  565. return (
  566. this.isInAny(
  567. 'select.idle',
  568. 'select.hoveringSelectionHandle',
  569. 'select.pointingRotateHandle',
  570. 'select.pointingResizeHandle'
  571. ) &&
  572. selectedShapesArray.length > 0 &&
  573. !selectedShapesArray.every(shape => shape.hideRotateHandle)
  574. )
  575. }
  576. @computed get showResizeHandles() {
  577. const { selectedShapesArray } = this
  578. return (
  579. this.isInAny(
  580. 'select.idle',
  581. 'select.hoveringSelectionHandle',
  582. 'select.pointingShape',
  583. 'select.pointingSelectedShape',
  584. 'select.pointingRotateHandle',
  585. 'select.pointingResizeHandle'
  586. ) &&
  587. selectedShapesArray.length === 1 &&
  588. !selectedShapesArray.every(shape => shape.hideResizeHandles)
  589. )
  590. }
  591. /* ------------------ Shape Classes ----------------- */
  592. Shapes = new Map<string, TLShapeConstructor<S>>()
  593. get SmartShape() {
  594. for (const S of this.Shapes.values()) {
  595. if (S.smart) {
  596. return S
  597. }
  598. }
  599. return null
  600. }
  601. registerShapes = (Shapes: TLShapeConstructor<S>[]) => {
  602. Shapes.forEach(Shape => this.Shapes.set(Shape.id, Shape))
  603. }
  604. deregisterShapes = (Shapes: TLShapeConstructor<S>[]) => {
  605. Shapes.forEach(Shape => this.Shapes.delete(Shape.id))
  606. }
  607. getShapeClass = (type: string): TLShapeConstructor<S> => {
  608. if (!type) throw Error('No shape type provided.')
  609. const Shape = this.Shapes.get(type)
  610. if (!Shape) throw Error(`Could not find shape class for ${type}`)
  611. return Shape
  612. }
  613. wrapUpdate = (fn: () => void) => {
  614. transaction(() => {
  615. const shouldSave = !this.history.isPaused
  616. if (shouldSave) {
  617. this.history.pause()
  618. }
  619. fn()
  620. if (shouldSave) {
  621. this.history.resume()
  622. this.persist()
  623. }
  624. })
  625. }
  626. /* ------------------ Subscriptions ----------------- */
  627. private subscriptions = new Set<TLSubscription<S, K, this, any>>([])
  628. subscribe = <E extends TLSubscriptionEventName>(
  629. event: E,
  630. callback: TLCallback<S, K, this, E>
  631. ) => {
  632. if (callback === undefined) throw Error('Callback is required.')
  633. const subscription: TLSubscription<S, K, this, E> = { event, callback }
  634. this.subscriptions.add(subscription)
  635. return () => this.unsubscribe(subscription)
  636. }
  637. unsubscribe = (subscription: TLSubscription<S, K, this, any>) => {
  638. this.subscriptions.delete(subscription)
  639. return this
  640. }
  641. notify = <E extends TLSubscriptionEventName>(event: E, info: TLSubscriptionEventInfo<E>) => {
  642. this.subscriptions.forEach(subscription => {
  643. if (subscription.event === event) {
  644. subscription.callback(this, info)
  645. }
  646. })
  647. return this
  648. }
  649. /* ----------------- Event Handlers ----------------- */
  650. readonly onTransition: TLStateEvents<S, K>['onTransition'] = () => {
  651. this.settings.update({ isToolLocked: false })
  652. }
  653. readonly onWheel: TLEvents<S, K>['wheel'] = (info, e) => {
  654. this.viewport.panCamera(info.delta)
  655. this.inputs.onWheel([...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], e)
  656. }
  657. readonly onPointerDown: TLEvents<S, K>['pointer'] = (info, e) => {
  658. if ('clientX' in e) {
  659. this.inputs.onPointerDown(
  660. [...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5],
  661. e as K['pointer']
  662. )
  663. }
  664. }
  665. readonly onPointerUp: TLEvents<S, K>['pointer'] = (info, e) => {
  666. if ('clientX' in e) {
  667. this.inputs.onPointerUp(
  668. [...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5],
  669. e as K['pointer']
  670. )
  671. }
  672. }
  673. readonly onPointerMove: TLEvents<S, K>['pointer'] = (info, e) => {
  674. if ('clientX' in e) {
  675. this.inputs.onPointerMove([...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], e)
  676. }
  677. }
  678. readonly onKeyDown: TLEvents<S, K>['keyboard'] = (info, e) => {
  679. this.inputs.onKeyDown(e)
  680. }
  681. readonly onKeyUp: TLEvents<S, K>['keyboard'] = (info, e) => {
  682. this.inputs.onKeyUp(e)
  683. }
  684. readonly onPinchStart: TLEvents<S, K>['pinch'] = (info, e) => {
  685. this.inputs.onPinchStart([...this.viewport.getPagePoint(info.point), 0.5], e)
  686. }
  687. readonly onPinch: TLEvents<S, K>['pinch'] = (info, e) => {
  688. this.inputs.onPinch([...this.viewport.getPagePoint(info.point), 0.5], e)
  689. }
  690. readonly onPinchEnd: TLEvents<S, K>['pinch'] = (info, e) => {
  691. this.inputs.onPinchEnd([...this.viewport.getPagePoint(info.point), 0.5], e)
  692. }
  693. }