TLApp.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  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 } 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.history.pause()
  53. if (this.states && this.states.length > 0) {
  54. this.registerStates(this.states)
  55. const initialId = this.initial ?? this.states[0].id
  56. const state = this.children.get(initialId)
  57. if (state) {
  58. this.currentState = state
  59. this.currentState?._events.onEnter({ fromId: 'initial' })
  60. }
  61. }
  62. if (Shapes) this.registerShapes(Shapes)
  63. if (Tools) this.registerTools(Tools)
  64. this.history.resume()
  65. if (serializedApp) this.history.deserialize(serializedApp)
  66. const ownShortcuts: TLShortcut<S, K>[] = [
  67. {
  68. keys: 'mod+shift+g',
  69. fn: () => this.api.toggleGrid(),
  70. },
  71. {
  72. keys: 'shift+0',
  73. fn: () => this.api.resetZoom(),
  74. },
  75. {
  76. keys: 'mod+-',
  77. fn: () => this.api.zoomToSelection(),
  78. },
  79. {
  80. keys: 'mod+-',
  81. fn: () => this.api.zoomOut(),
  82. },
  83. {
  84. keys: 'mod+=',
  85. fn: () => this.api.zoomIn(),
  86. },
  87. {
  88. keys: 'mod+z',
  89. fn: () => this.undo(),
  90. },
  91. {
  92. keys: 'mod+shift+z',
  93. fn: () => this.redo(),
  94. },
  95. {
  96. keys: '[',
  97. fn: () => this.sendBackward(),
  98. },
  99. {
  100. keys: 'shift+[',
  101. fn: () => this.sendToBack(),
  102. },
  103. {
  104. keys: ']',
  105. fn: () => this.bringForward(),
  106. },
  107. {
  108. keys: 'shift+]',
  109. fn: () => this.bringToFront(),
  110. },
  111. {
  112. keys: 'mod+a',
  113. fn: () => {
  114. const { selectedTool } = this
  115. if (selectedTool.currentState.id !== 'idle') return
  116. if (selectedTool.id !== 'select') {
  117. this.selectTool('select')
  118. }
  119. this.api.selectAll()
  120. },
  121. },
  122. {
  123. keys: 'mod+s',
  124. fn: () => {
  125. this.save()
  126. this.notify('save', null)
  127. },
  128. },
  129. {
  130. keys: 'mod+shift+s',
  131. fn: () => {
  132. this.saveAs()
  133. this.notify('saveAs', null)
  134. },
  135. },
  136. ]
  137. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  138. // @ts-ignore
  139. const shortcuts = (this.constructor['shortcuts'] || []) as TLShortcut<S, K>[]
  140. this._disposables.push(
  141. ...[...ownShortcuts, ...shortcuts].map(({ keys, fn }) => {
  142. return KeyUtils.registerShortcut(keys, e => {
  143. fn(this, this, e)
  144. })
  145. })
  146. )
  147. this.api = new TLApi(this)
  148. makeObservable(this)
  149. this.notify('mount', null)
  150. }
  151. static id = 'app'
  152. static states: TLToolConstructor<any, any>[] = [TLSelectTool]
  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. }
  204. }
  205. /* ---------------------- Pages --------------------- */
  206. @observable pages: Map<string, TLPage<S, K>> = new Map([
  207. ['page', new TLPage(this, { id: 'page', name: 'page', shapes: [], bindings: {} })],
  208. ])
  209. @observable currentPageId = 'page'
  210. @computed get currentPage(): TLPage<S, K> {
  211. return this.getPageById(this.currentPageId)
  212. }
  213. getPageById = (pageId: string): TLPage<S, K> => {
  214. const page = this.pages.get(pageId)
  215. if (!page) throw Error(`Could not find a page named ${pageId}.`)
  216. return page
  217. }
  218. @action setCurrentPage(page: string | TLPage<S, K>): this {
  219. this.currentPageId = typeof page === 'string' ? page : page.id
  220. return this
  221. }
  222. @action addPages(pages: TLPage<S, K>[]): this {
  223. pages.forEach(page => this.pages.set(page.id, page))
  224. this.persist()
  225. return this
  226. }
  227. @action removePages(pages: TLPage<S, K>[]): this {
  228. pages.forEach(page => this.pages.delete(page.id))
  229. this.persist()
  230. return this
  231. }
  232. /* --------------------- Shapes --------------------- */
  233. getShapeById = <T extends S>(id: string, pageId = this.currentPage.id): T => {
  234. const shape = this.getPageById(pageId)?.shapes.find(shape => shape.id === id) as T
  235. if (!shape) throw Error(`Could not find that shape: ${id} on page ${pageId}`)
  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. dropFiles = (files: FileList, point?: number[]) => {
  308. this.notify('drop-files', {
  309. files: Array.from(files),
  310. point: point
  311. ? this.viewport.getPagePoint(point)
  312. : BoundsUtils.getBoundsCenter(this.viewport.currentView),
  313. })
  314. // This callback may be over-written manually, see useSetup.ts in React.
  315. return void null
  316. }
  317. /* ---------------------- Tools --------------------- */
  318. @computed get selectedTool() {
  319. return this.currentState
  320. }
  321. selectTool = this.transition
  322. registerTools = this.registerStates
  323. /* ------------------ Editing Shape ----------------- */
  324. @observable editingId?: string
  325. @computed get editingShape(): S | undefined {
  326. const { editingId, currentPage } = this
  327. return editingId ? currentPage.shapes.find(shape => shape.id === editingId) : undefined
  328. }
  329. @action readonly setEditingShape = (shape?: string | S): this => {
  330. this.editingId = typeof shape === 'string' ? shape : shape?.id
  331. return this
  332. }
  333. readonly clearEditingShape = (): this => {
  334. return this.setEditingShape()
  335. }
  336. /* ------------------ Hovered Shape ----------------- */
  337. @observable hoveredId?: string
  338. @computed get hoveredShape(): S | undefined {
  339. const { hoveredId, currentPage } = this
  340. return hoveredId ? currentPage.shapes.find(shape => shape.id === hoveredId) : undefined
  341. }
  342. @action readonly setHoveredShape = (shape?: string | S): this => {
  343. this.hoveredId = typeof shape === 'string' ? shape : shape?.id
  344. return this
  345. }
  346. /* ----------------- Selected Shapes ---------------- */
  347. @observable selectedIds: Set<string> = new Set()
  348. @observable selectedShapes: Set<S> = new Set()
  349. @observable selectionRotation = 0
  350. @computed get selectedShapesArray() {
  351. const { selectedShapes, selectedTool } = this
  352. const stateId = selectedTool.id
  353. if (stateId !== 'select') return []
  354. return Array.from(selectedShapes.values())
  355. }
  356. @action setSelectedShapes = (shapes: S[] | string[]): this => {
  357. const { selectedIds, selectedShapes } = this
  358. selectedIds.clear()
  359. selectedShapes.clear()
  360. if (shapes[0] && typeof shapes[0] === 'string') {
  361. ;(shapes as string[]).forEach(s => selectedIds.add(s))
  362. } else {
  363. ;(shapes as S[]).forEach(s => selectedIds.add(s.id))
  364. }
  365. const newSelectedShapes = this.currentPage.shapes.filter(shape => selectedIds.has(shape.id))
  366. newSelectedShapes.forEach(s => selectedShapes.add(s))
  367. if (newSelectedShapes.length === 1) {
  368. this.selectionRotation = newSelectedShapes[0].props.rotation ?? 0
  369. } else {
  370. this.selectionRotation = 0
  371. }
  372. return this
  373. }
  374. @action setSelectionRotation(radians: number) {
  375. this.selectionRotation = radians
  376. }
  377. /* ------------------ Erasing Shape ----------------- */
  378. @observable erasingIds: Set<string> = new Set()
  379. @observable erasingShapes: Set<S> = new Set()
  380. @computed get erasingShapesArray() {
  381. return Array.from(this.erasingShapes.values())
  382. }
  383. @action readonly setErasingShapes = (shapes: S[] | string[]): this => {
  384. const { erasingIds, erasingShapes } = this
  385. erasingIds.clear()
  386. erasingShapes.clear()
  387. if (shapes[0] && typeof shapes[0] === 'string') {
  388. ;(shapes as string[]).forEach(s => erasingIds.add(s))
  389. } else {
  390. ;(shapes as S[]).forEach(s => erasingIds.add(s.id))
  391. }
  392. const newErasingShapes = this.currentPage.shapes.filter(shape => erasingIds.has(shape.id))
  393. newErasingShapes.forEach(s => erasingShapes.add(s))
  394. return this
  395. }
  396. /* ------------------ Binding Shape ----------------- */
  397. @observable bindingIds?: string[]
  398. @computed get bindingShapes(): S[] | undefined {
  399. const { bindingIds, currentPage } = this
  400. return bindingIds
  401. ? currentPage.shapes.filter(shape => bindingIds?.includes(shape.id))
  402. : undefined
  403. }
  404. @action readonly setBindingShapes = (ids?: string[]): this => {
  405. this.bindingIds = ids
  406. return this
  407. }
  408. readonly clearBindingShape = (): this => {
  409. return this.setBindingShapes()
  410. }
  411. /* ---------------------- Brush --------------------- */
  412. @observable brush?: TLBounds
  413. @action readonly setBrush = (brush?: TLBounds): this => {
  414. this.brush = brush
  415. return this
  416. }
  417. /* --------------------- Camera --------------------- */
  418. @action setCamera = (point?: number[], zoom?: number): this => {
  419. this.viewport.update({ point, zoom })
  420. return this
  421. }
  422. readonly getPagePoint = (point: number[]): number[] => {
  423. const { camera } = this.viewport
  424. return Vec.sub(Vec.div(point, camera.zoom), camera.point)
  425. }
  426. readonly getScreenPoint = (point: number[]): number[] => {
  427. const { camera } = this.viewport
  428. return Vec.mul(Vec.add(point, camera.point), camera.zoom)
  429. }
  430. @computed
  431. get currentGrid() {
  432. const { zoom } = this.viewport.camera
  433. if (zoom < 0.15) {
  434. return GRID_SIZE * 16
  435. } else if (zoom < 1) {
  436. return GRID_SIZE * 4
  437. } else {
  438. return GRID_SIZE * 1
  439. }
  440. }
  441. /* --------------------- Display -------------------- */
  442. @computed get shapes(): S[] {
  443. const {
  444. currentPage: { shapes },
  445. } = this
  446. return Array.from(shapes.values())
  447. }
  448. @computed get shapesInViewport(): S[] {
  449. const {
  450. selectedShapes,
  451. currentPage,
  452. viewport: { currentView },
  453. } = this
  454. return currentPage.shapes.filter(shape => {
  455. return (
  456. shape.props.parentId === currentPage.id &&
  457. (!shape.canUnmount ||
  458. selectedShapes.has(shape) ||
  459. BoundsUtils.boundsContain(currentView, shape.rotatedBounds) ||
  460. BoundsUtils.boundsCollide(currentView, shape.rotatedBounds))
  461. )
  462. })
  463. }
  464. @computed get selectionDirectionHint(): number[] | undefined {
  465. const {
  466. selectionBounds,
  467. viewport: { currentView },
  468. } = this
  469. if (
  470. !selectionBounds ||
  471. BoundsUtils.boundsContain(currentView, selectionBounds) ||
  472. BoundsUtils.boundsCollide(currentView, selectionBounds)
  473. ) {
  474. return
  475. }
  476. const center = BoundsUtils.getBoundsCenter(selectionBounds)
  477. return Vec.clampV(
  478. [
  479. (center[0] - currentView.minX - currentView.width / 2) / currentView.width,
  480. (center[1] - currentView.minY - currentView.height / 2) / currentView.height,
  481. ],
  482. -1,
  483. 1
  484. )
  485. }
  486. @computed get selectionBounds(): TLBounds | undefined {
  487. const { selectedShapesArray } = this
  488. if (selectedShapesArray.length === 0) return undefined
  489. if (selectedShapesArray.length === 1) {
  490. return { ...selectedShapesArray[0].bounds, rotation: selectedShapesArray[0].props.rotation }
  491. }
  492. return BoundsUtils.getCommonBounds(this.selectedShapesArray.map(shape => shape.rotatedBounds))
  493. }
  494. @computed get showSelection() {
  495. const { selectedShapesArray } = this
  496. return (
  497. this.isIn('select') &&
  498. ((selectedShapesArray.length === 1 && !selectedShapesArray[0]?.hideSelection) ||
  499. selectedShapesArray.length > 1)
  500. )
  501. }
  502. @computed get showSelectionDetail() {
  503. return (
  504. this.isIn('select') &&
  505. this.selectedShapes.size > 0 &&
  506. !this.selectedShapesArray.every(shape => shape.hideSelectionDetail)
  507. )
  508. }
  509. @computed get showSelectionRotation() {
  510. return (
  511. this.showSelectionDetail && this.isInAny('select.rotating', 'select.pointingRotateHandle')
  512. )
  513. }
  514. @computed get showContextBar() {
  515. const {
  516. selectedShapesArray,
  517. inputs: { ctrlKey },
  518. } = this
  519. return (
  520. !ctrlKey &&
  521. this.isInAny('select.idle', 'select.hoveringSelectionHandle') &&
  522. selectedShapesArray.length > 0 &&
  523. !selectedShapesArray.every(shape => shape.hideContextBar)
  524. )
  525. }
  526. @computed get showRotateHandles() {
  527. const { selectedShapesArray } = this
  528. return (
  529. this.isInAny(
  530. 'select.idle',
  531. 'select.hoveringSelectionHandle',
  532. 'select.pointingRotateHandle',
  533. 'select.pointingResizeHandle'
  534. ) &&
  535. selectedShapesArray.length > 0 &&
  536. !selectedShapesArray.every(shape => shape.hideRotateHandle)
  537. )
  538. }
  539. @computed get showResizeHandles() {
  540. const { selectedShapesArray } = this
  541. return (
  542. this.isInAny(
  543. 'select.idle',
  544. 'select.hoveringSelectionHandle',
  545. 'select.pointingShape',
  546. 'select.pointingSelectedShape',
  547. 'select.pointingRotateHandle',
  548. 'select.pointingResizeHandle'
  549. ) &&
  550. selectedShapesArray.length > 0 &&
  551. !selectedShapesArray.every(shape => shape.hideResizeHandles)
  552. )
  553. }
  554. /* ------------------ Shape Classes ----------------- */
  555. Shapes = new Map<string, TLShapeConstructor<S>>()
  556. registerShapes = (Shapes: TLShapeConstructor<S>[]) => {
  557. Shapes.forEach(Shape => this.Shapes.set(Shape.id, Shape))
  558. }
  559. deregisterShapes = (Shapes: TLShapeConstructor<S>[]) => {
  560. Shapes.forEach(Shape => this.Shapes.delete(Shape.id))
  561. }
  562. getShapeClass = (type: string): TLShapeConstructor<S> => {
  563. if (!type) throw Error('No shape type provided.')
  564. const Shape = this.Shapes.get(type)
  565. if (!Shape) throw Error(`Could not find shape class for ${type}`)
  566. return Shape
  567. }
  568. /* ------------------ Subscriptions ----------------- */
  569. private subscriptions = new Set<TLSubscription<S, K, this, any>>([])
  570. subscribe = <E extends TLSubscriptionEventName>(
  571. event: E,
  572. callback: TLCallback<S, K, this, E>
  573. ) => {
  574. if (callback === undefined) throw Error('Callback is required.')
  575. const subscription: TLSubscription<S, K, this, E> = { event, callback }
  576. this.subscriptions.add(subscription)
  577. return () => this.unsubscribe(subscription)
  578. }
  579. unsubscribe = (subscription: TLSubscription<S, K, this, any>) => {
  580. this.subscriptions.delete(subscription)
  581. return this
  582. }
  583. notify = <E extends TLSubscriptionEventName>(event: E, info: TLSubscriptionEventInfo<E>) => {
  584. this.subscriptions.forEach(subscription => {
  585. if (subscription.event === event) {
  586. subscription.callback(this, info)
  587. }
  588. })
  589. return this
  590. }
  591. /* ----------------- Event Handlers ----------------- */
  592. readonly onTransition: TLStateEvents<S, K>['onTransition'] = () => {
  593. this.settings.update({ isToolLocked: false })
  594. }
  595. readonly onWheel: TLEvents<S, K>['wheel'] = (info, e) => {
  596. this.viewport.panCamera(info.delta)
  597. this.inputs.onWheel([...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], e)
  598. }
  599. readonly onPointerDown: TLEvents<S, K>['pointer'] = (info, e) => {
  600. if ('clientX' in e) {
  601. this.inputs.onPointerDown(
  602. [...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5],
  603. e as K['pointer']
  604. )
  605. }
  606. }
  607. readonly onPointerUp: TLEvents<S, K>['pointer'] = (info, e) => {
  608. if ('clientX' in e) {
  609. this.inputs.onPointerUp(
  610. [...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5],
  611. e as K['pointer']
  612. )
  613. }
  614. }
  615. readonly onPointerMove: TLEvents<S, K>['pointer'] = (info, e) => {
  616. if ('clientX' in e) {
  617. this.inputs.onPointerMove([...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], e)
  618. }
  619. }
  620. readonly onKeyDown: TLEvents<S, K>['keyboard'] = (info, e) => {
  621. this.inputs.onKeyDown(e)
  622. }
  623. readonly onKeyUp: TLEvents<S, K>['keyboard'] = (info, e) => {
  624. this.inputs.onKeyUp(e)
  625. }
  626. readonly onPinchStart: TLEvents<S, K>['pinch'] = (info, e) => {
  627. this.inputs.onPinchStart([...this.viewport.getPagePoint(info.point), 0.5], e)
  628. }
  629. readonly onPinch: TLEvents<S, K>['pinch'] = (info, e) => {
  630. this.inputs.onPinch([...this.viewport.getPagePoint(info.point), 0.5], e)
  631. }
  632. readonly onPinchEnd: TLEvents<S, K>['pinch'] = (info, e) => {
  633. this.inputs.onPinchEnd([...this.viewport.getPagePoint(info.point), 0.5], e)
  634. }
  635. }