123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875 |
- /* eslint-disable @typescript-eslint/no-extra-semi */
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
- /* eslint-disable @typescript-eslint/no-explicit-any */
- import type { TLBounds } from '@tldraw/intersect'
- import { Vec } from '@tldraw/vec'
- import { action, computed, makeObservable, observable, transaction } from 'mobx'
- import { GRID_SIZE } from '../../constants'
- import type {
- TLAsset,
- TLEventMap,
- TLShortcut,
- TLSubscription,
- TLSubscriptionEventName,
- TLCallback,
- TLSubscriptionEventInfo,
- TLStateEvents,
- TLEvents,
- } from '../../types'
- import { KeyUtils, BoundsUtils } from '../../utils'
- import type { TLShape, TLShapeConstructor, TLShapeModel } from '../shapes'
- import { TLApi } from '../TLApi'
- import { TLCursors } from '../TLCursors'
- import { TLHistory } from '../TLHistory'
- import { TLInputs } from '../TLInputs'
- import { type TLPageModel, TLPage } from '../TLPage'
- import { TLSettings } from '../TLSettings'
- import { TLRootState } from '../TLState'
- import type { TLToolConstructor } from '../TLTool'
- import { TLViewport } from '../TLViewport'
- import { TLSelectTool, TLMoveTool } from '../tools'
- export interface TLDocumentModel<S extends TLShape = TLShape, A extends TLAsset = TLAsset> {
- currentPageId: string
- selectedIds: string[]
- pages: TLPageModel<S>[]
- assets?: A[]
- }
- export class TLApp<
- S extends TLShape = TLShape,
- K extends TLEventMap = TLEventMap
- > extends TLRootState<S, K> {
- constructor(
- serializedApp?: TLDocumentModel<S>,
- Shapes?: TLShapeConstructor<S>[],
- Tools?: TLToolConstructor<S, K>[]
- ) {
- super()
- this._states = [TLSelectTool, TLMoveTool]
- this.history.pause()
- if (this.states && this.states.length > 0) {
- this.registerStates(this.states)
- const initialId = this.initial ?? this.states[0].id
- const state = this.children.get(initialId)
- if (state) {
- this.currentState = state
- this.currentState?._events.onEnter({ fromId: 'initial' })
- }
- }
- if (Shapes) this.registerShapes(Shapes)
- if (Tools) this.registerTools(Tools)
- this.history.resume()
- if (serializedApp) this.history.deserialize(serializedApp)
- this.api = new TLApi(this)
- makeObservable(this)
- this.notify('mount', null)
- }
- keybindingRegistered = false
- static id = 'app'
- static initial = 'select'
- readonly api: TLApi<S, K>
- readonly inputs = new TLInputs<K>()
- readonly cursors = new TLCursors()
- readonly viewport = new TLViewport()
- readonly settings = new TLSettings()
- Tools: TLToolConstructor<S, K>[] = []
- dispose() {
- super.dispose()
- this.keybindingRegistered = false
- return this
- }
- initKeyboardShortcuts() {
- if (this.keybindingRegistered) {
- return
- }
- const ownShortcuts: TLShortcut<S, K>[] = [
- {
- keys: 'shift+0',
- fn: () => this.api.resetZoom(),
- },
- {
- keys: 'mod+-',
- fn: () => this.api.zoomToSelection(),
- },
- {
- keys: 'mod+-',
- fn: () => this.api.zoomOut(),
- },
- {
- keys: 'mod+=',
- fn: () => this.api.zoomIn(),
- },
- {
- keys: 'mod+z',
- fn: () => this.undo(),
- },
- {
- keys: 'mod+shift+z',
- fn: () => this.redo(),
- },
- {
- keys: '[',
- fn: () => this.sendBackward(),
- },
- {
- keys: 'shift+[',
- fn: () => this.sendToBack(),
- },
- {
- keys: ']',
- fn: () => this.bringForward(),
- },
- {
- keys: 'shift+]',
- fn: () => this.bringToFront(),
- },
- {
- keys: 'mod+a',
- fn: () => {
- const { selectedTool } = this
- if (
- selectedTool.currentState.id !== 'idle' &&
- !selectedTool.currentState.id.includes('hovering')
- ) {
- return
- }
- if (selectedTool.id !== 'select') {
- this.selectTool('select')
- }
- this.api.selectAll()
- },
- },
- {
- keys: 'mod+s',
- fn: () => {
- this.save()
- this.notify('save', null)
- },
- },
- {
- keys: 'mod+shift+s',
- fn: () => {
- this.saveAs()
- this.notify('saveAs', null)
- },
- },
- {
- keys: 'mod+shift+v',
- fn: (_, __, e) => {
- if (!this.editingShape) {
- e.preventDefault()
- this.paste(undefined, true)
- }
- },
- },
- {
- keys: ['del', 'backspace'],
- fn: () => {
- const { selectedTool } = this
- if (
- selectedTool.currentState.id !== 'idle' &&
- !selectedTool.currentState.id.includes('hovering')
- ) {
- return
- }
- this.api.deleteShapes()
- },
- },
- ]
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- const shortcuts = (this.constructor['shortcuts'] || []) as TLShortcut<S, K>[]
- const childrenShortcuts = Array.from(this.children.values())
- // @ts-expect-error ???
- .filter(c => c.constructor['shortcut'])
- .map(child => {
- return {
- // @ts-expect-error ???
- keys: child.constructor['shortcut'] as string | string[],
- fn: (_: any, __: any, e: Event) => {
- this.transition(child.id)
- e.stopPropagation()
- },
- }
- })
- this._disposables.push(
- ...[...ownShortcuts, ...shortcuts, ...childrenShortcuts].map(({ keys, fn }) => {
- return KeyUtils.registerShortcut(keys, e => {
- fn(this, this, e)
- })
- })
- )
- this.keybindingRegistered = true
- }
- /* --------------------- History -------------------- */
- history = new TLHistory<S, K>(this)
- persist = this.history.persist
- undo = this.history.undo
- redo = this.history.redo
- saving = false // used to capture direct mutations as part of the history stack
- saveState = () => {
- if (this.history.isPaused) return
- this.saving = true
- requestAnimationFrame(() => {
- if (this.saving) {
- this.persist()
- this.saving = false
- }
- })
- }
- /* -------------------------------------------------- */
- /* Document */
- /* -------------------------------------------------- */
- loadDocumentModel(model: TLDocumentModel<S>): this {
- this.history.deserialize(model)
- if (model.assets) this.addAssets(model.assets)
- return this
- }
- load = (): this => {
- // todo
- this.notify('load', null)
- return this
- }
- save = (): this => {
- // todo
- this.notify('save', null)
- return this
- }
- saveAs = (): this => {
- // todo
- this.notify('saveAs', null)
- return this
- }
- @computed get serialized(): TLDocumentModel<S> {
- return {
- currentPageId: this.currentPageId,
- selectedIds: Array.from(this.selectedIds.values()),
- pages: Array.from(this.pages.values()).map(page => page.serialized),
- assets: this.getCleanUpAssets(),
- }
- }
- /* ---------------------- Pages --------------------- */
- @observable pages: Map<string, TLPage<S, K>> = new Map([
- ['page', new TLPage(this, { id: 'page', name: 'page', shapes: [], bindings: {} })],
- ])
- @computed get currentPageId() {
- return this.pages.keys().next().value
- }
- @computed get currentPage(): TLPage<S, K> {
- return this.getPageById(this.currentPageId)
- }
- getPageById = (pageId: string): TLPage<S, K> => {
- const page = this.pages.get(pageId)
- if (!page) throw Error(`Could not find a page named ${pageId}.`)
- return page
- }
- @action addPages(pages: TLPage<S, K>[]): this {
- pages.forEach(page => this.pages.set(page.id, page))
- this.persist()
- return this
- }
- @action removePages(pages: TLPage<S, K>[]): this {
- pages.forEach(page => this.pages.delete(page.id))
- this.persist()
- return this
- }
- /* --------------------- Shapes --------------------- */
- getShapeById = <T extends S>(id: string, pageId = this.currentPage.id): T | undefined => {
- const shape = this.getPageById(pageId)?.shapes.find(shape => shape.id === id) as T
- return shape
- }
- @action readonly createShapes = (shapes: S[] | TLShapeModel[]): this => {
- const newShapes = this.currentPage.addShapes(...shapes)
- if (newShapes) this.notify('create-shapes', newShapes)
- this.persist()
- return this
- }
- @action updateShapes = <T extends S>(shapes: ({ id: string } & Partial<T['props']>)[]): this => {
- shapes.forEach(shape => this.getShapeById(shape.id)?.update(shape))
- this.persist()
- return this
- }
- @action readonly deleteShapes = (shapes: S[] | string[]): this => {
- if (shapes.length === 0) return this
- let ids: Set<string>
- if (typeof shapes[0] === 'string') {
- ids = new Set(shapes as string[])
- } else {
- ids = new Set((shapes as S[]).map(shape => shape.id))
- }
- this.setSelectedShapes(this.selectedShapesArray.filter(shape => !ids.has(shape.id)))
- const removedShapes = this.currentPage.removeShapes(...shapes)
- if (removedShapes) this.notify('delete-shapes', removedShapes)
- this.persist()
- return this
- }
- bringForward = (shapes: S[] | string[] = this.selectedShapesArray): this => {
- if (shapes.length > 0) this.currentPage.bringForward(shapes)
- return this
- }
- sendBackward = (shapes: S[] | string[] = this.selectedShapesArray): this => {
- if (shapes.length > 0) this.currentPage.sendBackward(shapes)
- return this
- }
- sendToBack = (shapes: S[] | string[] = this.selectedShapesArray): this => {
- if (shapes.length > 0) this.currentPage.sendToBack(shapes)
- return this
- }
- bringToFront = (shapes: S[] | string[] = this.selectedShapesArray): this => {
- if (shapes.length > 0) this.currentPage.bringToFront(shapes)
- return this
- }
- flipHorizontal = (shapes: S[] | string[] = this.selectedShapesArray): this => {
- this.currentPage.flip(shapes, 'horizontal')
- return this
- }
- flipVertical = (shapes: S[] | string[] = this.selectedShapesArray): this => {
- this.currentPage.flip(shapes, 'vertical')
- return this
- }
- /* --------------------- Assets --------------------- */
- @observable assets: Record<string, TLAsset> = {}
- @action addAssets<T extends TLAsset>(assets: T[]): this {
- assets.forEach(asset => (this.assets[asset.id] = asset))
- this.persist()
- return this
- }
- @action removeAssets<T extends TLAsset>(assets: T[] | string[]): this {
- if (typeof assets[0] === 'string')
- (assets as string[]).forEach(asset => delete this.assets[asset])
- else (assets as T[]).forEach(asset => delete this.assets[(asset as T).id])
- this.persist()
- return this
- }
- getCleanUpAssets<T extends TLAsset>(): T[] {
- let deleted = false
- const usedAssets = new Set<T>()
- this.pages.forEach(p =>
- p.shapes.forEach(s => {
- if (s.props.assetId && this.assets[s.props.assetId]) {
- // @ts-expect-error ???
- usedAssets.add(this.assets[s.props.assetId])
- }
- })
- )
- return Array.from(usedAssets)
- }
- createAssets<T extends TLAsset>(assets: T[]): this {
- this.addAssets(assets)
- this.notify('create-assets', { assets })
- this.persist()
- return this
- }
- copy = () => {
- if (this.selectedShapesArray.length > 0 && !this.editingShape) {
- const tldrawString = JSON.stringify({
- type: 'logseq/whiteboard-shapes',
- shapes: this.selectedShapesArray.map(shape => shape.serialized),
- // pasting into other whiteboard may require this if any shape uses asset
- assets: this.getCleanUpAssets().filter(asset => {
- return this.selectedShapesArray.some(shape => shape.props.assetId === asset.id)
- }),
- })
- navigator.clipboard.write([
- new ClipboardItem({
- 'text/plain': new Blob([tldrawString], { type: 'text/plain' }),
- }),
- ])
- }
- }
- paste = (e?: ClipboardEvent, shiftKey?: boolean) => {
- if (!this.editingShape) {
- const fileList = e?.clipboardData?.files
- this.notify('paste', {
- point: this.inputs.currentPoint,
- shiftKey: !!shiftKey,
- files: fileList ? Array.from(fileList) : undefined,
- })
- }
- }
- dropFiles = (files: FileList, point?: number[]) => {
- this.notify('drop-files', {
- files: Array.from(files),
- point: point
- ? this.viewport.getPagePoint(point)
- : BoundsUtils.getBoundsCenter(this.viewport.currentView),
- })
- // This callback may be over-written manually, see useSetup.ts in React.
- return void null
- }
- /* ---------------------- Tools --------------------- */
- @computed get selectedTool() {
- return this.currentState
- }
- selectTool = this.transition
- registerTools(tools: TLToolConstructor<S, K>[]) {
- this.Tools = tools
- return this.registerStates(tools)
- }
- /* ------------------ Editing Shape ----------------- */
- @observable editingId?: string
- @computed get editingShape(): S | undefined {
- const { editingId, currentPage } = this
- return editingId ? currentPage.shapes.find(shape => shape.id === editingId) : undefined
- }
- @action readonly setEditingShape = (shape?: string | S): this => {
- this.editingId = typeof shape === 'string' ? shape : shape?.id
- return this
- }
- readonly clearEditingState = (): this => {
- this.selectedTool.transition('idle')
- return this.setEditingShape()
- }
- /* ------------------ Hovered Shape ----------------- */
- @observable hoveredId?: string
- @computed get hoveredShape(): S | undefined {
- const { hoveredId, currentPage } = this
- return hoveredId ? currentPage.shapes.find(shape => shape.id === hoveredId) : undefined
- }
- @action readonly setHoveredShape = (shape?: string | S): this => {
- this.hoveredId = typeof shape === 'string' ? shape : shape?.id
- return this
- }
- /* ----------------- Selected Shapes ---------------- */
- @observable selectedIds: Set<string> = new Set()
- @observable selectedShapes: Set<S> = new Set()
- @observable selectionRotation = 0
- @computed get selectedShapesArray() {
- const { selectedShapes, selectedTool } = this
- const stateId = selectedTool.id
- if (stateId !== 'select') return []
- return Array.from(selectedShapes.values())
- }
- @action setSelectedShapes = (shapes: S[] | string[]): this => {
- const { selectedIds, selectedShapes } = this
- selectedIds.clear()
- selectedShapes.clear()
- if (shapes[0] && typeof shapes[0] === 'string') {
- ;(shapes as string[]).forEach(s => selectedIds.add(s))
- } else {
- ;(shapes as S[]).forEach(s => selectedIds.add(s.id))
- }
- const newSelectedShapes = this.currentPage.shapes.filter(shape => selectedIds.has(shape.id))
- newSelectedShapes.forEach(s => selectedShapes.add(s))
- if (newSelectedShapes.length === 1) {
- this.selectionRotation = newSelectedShapes[0].props.rotation ?? 0
- } else {
- this.selectionRotation = 0
- }
- if (shapes.length === 0) {
- this.setEditingShape()
- }
- return this
- }
- @action setSelectionRotation(radians: number) {
- this.selectionRotation = radians
- }
- /* ------------------ Erasing Shape ----------------- */
- @observable erasingIds: Set<string> = new Set()
- @observable erasingShapes: Set<S> = new Set()
- @computed get erasingShapesArray() {
- return Array.from(this.erasingShapes.values())
- }
- @action readonly setErasingShapes = (shapes: S[] | string[]): this => {
- const { erasingIds, erasingShapes } = this
- erasingIds.clear()
- erasingShapes.clear()
- if (shapes[0] && typeof shapes[0] === 'string') {
- ;(shapes as string[]).forEach(s => erasingIds.add(s))
- } else {
- ;(shapes as S[]).forEach(s => erasingIds.add(s.id))
- }
- const newErasingShapes = this.currentPage.shapes.filter(shape => erasingIds.has(shape.id))
- newErasingShapes.forEach(s => erasingShapes.add(s))
- return this
- }
- /* ------------------ Binding Shape ----------------- */
- @observable bindingIds?: string[]
- @computed get bindingShapes(): S[] | undefined {
- const { bindingIds, currentPage } = this
- return bindingIds
- ? currentPage.shapes.filter(shape => bindingIds?.includes(shape.id))
- : undefined
- }
- @action readonly setBindingShapes = (ids?: string[]): this => {
- this.bindingIds = ids
- return this
- }
- readonly clearBindingShape = (): this => {
- return this.setBindingShapes()
- }
- /* ---------------------- Brush --------------------- */
- @observable brush?: TLBounds
- @action readonly setBrush = (brush?: TLBounds): this => {
- this.brush = brush
- return this
- }
- /* --------------------- Camera --------------------- */
- @action setCamera = (point?: number[], zoom?: number): this => {
- this.viewport.update({ point, zoom })
- return this
- }
- readonly getPagePoint = (point: number[]): number[] => {
- const { camera } = this.viewport
- return Vec.sub(Vec.div(point, camera.zoom), camera.point)
- }
- readonly getScreenPoint = (point: number[]): number[] => {
- const { camera } = this.viewport
- return Vec.mul(Vec.add(point, camera.point), camera.zoom)
- }
- @computed
- get currentGrid() {
- const { zoom } = this.viewport.camera
- if (zoom < 0.15) {
- return GRID_SIZE * 16
- } else if (zoom < 1) {
- return GRID_SIZE * 4
- } else {
- return GRID_SIZE * 1
- }
- }
- /* --------------------- Display -------------------- */
- @computed get shapes(): S[] {
- const {
- currentPage: { shapes },
- } = this
- return Array.from(shapes.values())
- }
- @computed get shapesInViewport(): S[] {
- const {
- selectedShapes,
- currentPage,
- viewport: { currentView },
- } = this
- return currentPage.shapes.filter(shape => {
- return (
- !shape.canUnmount ||
- selectedShapes.has(shape) ||
- BoundsUtils.boundsContain(currentView, shape.rotatedBounds) ||
- BoundsUtils.boundsCollide(currentView, shape.rotatedBounds)
- )
- })
- }
- @computed get selectionDirectionHint(): number[] | undefined {
- const {
- selectionBounds,
- viewport: { currentView },
- } = this
- if (
- !selectionBounds ||
- BoundsUtils.boundsContain(currentView, selectionBounds) ||
- BoundsUtils.boundsCollide(currentView, selectionBounds)
- ) {
- return
- }
- const center = BoundsUtils.getBoundsCenter(selectionBounds)
- return Vec.clampV(
- [
- (center[0] - currentView.minX - currentView.width / 2) / currentView.width,
- (center[1] - currentView.minY - currentView.height / 2) / currentView.height,
- ],
- -1,
- 1
- )
- }
- @computed get selectionBounds(): TLBounds | undefined {
- const { selectedShapesArray } = this
- if (selectedShapesArray.length === 0) return undefined
- if (selectedShapesArray.length === 1) {
- return { ...selectedShapesArray[0].bounds, rotation: selectedShapesArray[0].props.rotation }
- }
- return BoundsUtils.getCommonBounds(this.selectedShapesArray.map(shape => shape.rotatedBounds))
- }
- @computed get showSelection() {
- const { selectedShapesArray } = this
- return (
- this.isIn('select') &&
- !this.isInAny('select.translating', 'select.pinching', 'select.rotating') &&
- ((selectedShapesArray.length === 1 && !selectedShapesArray[0]?.hideSelection) ||
- selectedShapesArray.length > 1)
- )
- }
- @computed get showSelectionDetail() {
- return (
- this.isIn('select') &&
- !this.isInAny('select.translating', 'select.pinching') &&
- this.selectedShapes.size > 0 &&
- !this.selectedShapesArray.every(shape => shape.hideSelectionDetail) &&
- false // FIXME: should we shoult the selection detail?
- )
- }
- @computed get showSelectionRotation() {
- return (
- this.showSelectionDetail && this.isInAny('select.rotating', 'select.pointingRotateHandle')
- )
- }
- @computed get showContextBar() {
- const {
- selectedShapesArray,
- inputs: { ctrlKey },
- } = this
- return (
- !ctrlKey &&
- this.isInAny('select.idle', 'select.hoveringSelectionHandle') &&
- selectedShapesArray.length > 0 &&
- !selectedShapesArray.every(shape => shape.hideContextBar)
- )
- }
- @computed get showRotateHandles() {
- const { selectedShapesArray } = this
- return (
- this.isInAny(
- 'select.idle',
- 'select.hoveringSelectionHandle',
- 'select.pointingRotateHandle',
- 'select.pointingResizeHandle'
- ) &&
- selectedShapesArray.length > 0 &&
- !selectedShapesArray.some(shape => shape.hideRotateHandle)
- )
- }
- @computed get showResizeHandles() {
- const { selectedShapesArray } = this
- return (
- this.isInAny(
- 'select.idle',
- 'select.hoveringSelectionHandle',
- 'select.pointingShape',
- 'select.pointingSelectedShape',
- 'select.pointingRotateHandle',
- 'select.pointingResizeHandle'
- ) &&
- selectedShapesArray.length === 1 &&
- !selectedShapesArray.every(shape => shape.hideResizeHandles)
- )
- }
- /* ------------------ Shape Classes ----------------- */
- Shapes = new Map<string, TLShapeConstructor<S>>()
- registerShapes = (Shapes: TLShapeConstructor<S>[]) => {
- Shapes.forEach(Shape => this.Shapes.set(Shape.id, Shape))
- }
- deregisterShapes = (Shapes: TLShapeConstructor<S>[]) => {
- Shapes.forEach(Shape => this.Shapes.delete(Shape.id))
- }
- getShapeClass = (type: string): TLShapeConstructor<S> => {
- if (!type) throw Error('No shape type provided.')
- const Shape = this.Shapes.get(type)
- if (!Shape) throw Error(`Could not find shape class for ${type}`)
- return Shape
- }
- wrapUpdate = (fn: () => void) => {
- transaction(() => {
- const shouldSave = !this.history.isPaused
- if (shouldSave) {
- this.history.pause()
- }
- fn()
- if (shouldSave) {
- this.history.resume()
- this.persist()
- }
- })
- }
- /* ------------------ Subscriptions ----------------- */
- private subscriptions = new Set<TLSubscription<S, K, this, any>>([])
- subscribe = <E extends TLSubscriptionEventName>(
- event: E,
- callback: TLCallback<S, K, this, E>
- ) => {
- if (callback === undefined) throw Error('Callback is required.')
- const subscription: TLSubscription<S, K, this, E> = { event, callback }
- this.subscriptions.add(subscription)
- return () => this.unsubscribe(subscription)
- }
- unsubscribe = (subscription: TLSubscription<S, K, this, any>) => {
- this.subscriptions.delete(subscription)
- return this
- }
- notify = <E extends TLSubscriptionEventName>(event: E, info: TLSubscriptionEventInfo<E>) => {
- this.subscriptions.forEach(subscription => {
- if (subscription.event === event) {
- subscription.callback(this, info)
- }
- })
- return this
- }
- /* ----------------- Event Handlers ----------------- */
- readonly onTransition: TLStateEvents<S, K>['onTransition'] = () => {
- this.settings.update({ isToolLocked: false })
- }
- readonly onWheel: TLEvents<S, K>['wheel'] = (info, e) => {
- this.viewport.panCamera(info.delta)
- this.inputs.onWheel([...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], e)
- }
- readonly onPointerDown: TLEvents<S, K>['pointer'] = (info, e) => {
- if ('clientX' in e) {
- this.inputs.onPointerDown(
- [...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5],
- e as K['pointer']
- )
- }
- }
- readonly onPointerUp: TLEvents<S, K>['pointer'] = (info, e) => {
- if ('clientX' in e) {
- this.inputs.onPointerUp(
- [...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5],
- e as K['pointer']
- )
- }
- }
- readonly onPointerMove: TLEvents<S, K>['pointer'] = (info, e) => {
- if ('clientX' in e) {
- this.inputs.onPointerMove([...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], e)
- }
- }
- readonly onKeyDown: TLEvents<S, K>['keyboard'] = (info, e) => {
- if (!this.editingShape && e['key'] === ' ' && !this.isIn('move')) {
- e.stopPropagation()
- e.preventDefault()
- const prevTool = this.selectedTool
- this.transition('move', { prevTool })
- this.selectedTool.transition('idleHold')
- return
- }
- this.inputs.onKeyDown(e)
- }
- readonly onKeyUp: TLEvents<S, K>['keyboard'] = (info, e) => {
- if (!this.editingShape && e['key'] === ' ' && this.isIn('move')) {
- this.selectedTool.transition('idle', { exit: true })
- e.stopPropagation()
- e.preventDefault()
- return
- }
- this.inputs.onKeyUp(e)
- }
- readonly onPinchStart: TLEvents<S, K>['pinch'] = (info, e) => {
- this.inputs.onPinchStart([...this.viewport.getPagePoint(info.point), 0.5], e)
- }
- readonly onPinch: TLEvents<S, K>['pinch'] = (info, e) => {
- this.inputs.onPinch([...this.viewport.getPagePoint(info.point), 0.5], e)
- }
- readonly onPinchEnd: TLEvents<S, K>['pinch'] = (info, e) => {
- this.inputs.onPinchEnd([...this.viewport.getPagePoint(info.point), 0.5], e)
- }
- }
|