UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

4 lines • 493 kB
{ "version": 3, "sources": ["../../../src/lib/editor/Editor.ts"], "sourcesContent": ["import {\n\tAtom,\n\tEMPTY_ARRAY,\n\tatom,\n\tcomputed,\n\treact,\n\ttransact,\n\tunsafe__withoutCapture,\n} from '@tldraw/state'\nimport {\n\tComputedCache,\n\tRecordType,\n\tStoreSideEffects,\n\tStoreSnapshot,\n\tUnknownRecord,\n\treverseRecordsDiff,\n} from '@tldraw/store'\nimport {\n\tCameraRecordType,\n\tInstancePageStateRecordType,\n\tPageRecordType,\n\tStyleProp,\n\tStylePropValue,\n\tTLArrowShape,\n\tTLAsset,\n\tTLAssetId,\n\tTLAssetPartial,\n\tTLBinding,\n\tTLBindingCreate,\n\tTLBindingId,\n\tTLBindingUpdate,\n\tTLCamera,\n\tTLCursor,\n\tTLCursorType,\n\tTLDOCUMENT_ID,\n\tTLDocument,\n\tTLFrameShape,\n\tTLGeoShape,\n\tTLGroupShape,\n\tTLHandle,\n\tTLINSTANCE_ID,\n\tTLImageAsset,\n\tTLInstance,\n\tTLInstancePageState,\n\tTLInstancePresence,\n\tTLNoteShape,\n\tTLPOINTER_ID,\n\tTLPage,\n\tTLPageId,\n\tTLParentId,\n\tTLRecord,\n\tTLShape,\n\tTLShapeId,\n\tTLShapePartial,\n\tTLStore,\n\tTLStoreSnapshot,\n\tTLUnknownBinding,\n\tTLUnknownShape,\n\tTLVideoAsset,\n\tcreateBindingId,\n\tcreateShapeId,\n\tgetShapePropKeysByStyle,\n\tisPageId,\n\tisShapeId,\n} from '@tldraw/tlschema'\nimport {\n\tFileHelpers,\n\tIndexKey,\n\tJsonObject,\n\tPerformanceTracker,\n\tResult,\n\tannotateError,\n\tassert,\n\tassertExists,\n\tbind,\n\tcompact,\n\tdebounce,\n\tdedupe,\n\texhaustiveSwitchError,\n\tfetch,\n\tgetIndexAbove,\n\tgetIndexBetween,\n\tgetIndices,\n\tgetIndicesAbove,\n\tgetIndicesBetween,\n\tgetOwnProperty,\n\thasOwnProperty,\n\tlast,\n\tlerp,\n\tmaxBy,\n\tminBy,\n\tsortById,\n\tsortByIndex,\n\tstructuredClone,\n\tuniqueId,\n} from '@tldraw/utils'\nimport EventEmitter from 'eventemitter3'\nimport {\n\tTLEditorSnapshot,\n\tTLLoadSnapshotOptions,\n\tgetSnapshot,\n\tloadSnapshot,\n} from '../config/TLEditorSnapshot'\nimport { TLUser, createTLUser } from '../config/createTLUser'\nimport { TLAnyBindingUtilConstructor, checkBindings } from '../config/defaultBindings'\nimport { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from '../config/defaultShapes'\nimport {\n\tDEFAULT_ANIMATION_OPTIONS,\n\tDEFAULT_CAMERA_OPTIONS,\n\tINTERNAL_POINTER_IDS,\n\tLEFT_MOUSE_BUTTON,\n\tMIDDLE_MOUSE_BUTTON,\n\tRIGHT_MOUSE_BUTTON,\n\tSTYLUS_ERASER_BUTTON,\n\tZOOM_TO_FIT_PADDING,\n} from '../constants'\nimport { exportToSvg } from '../exports/exportToSvg'\nimport { getSvgAsImage } from '../exports/getSvgAsImage'\nimport { tlenv } from '../globals/environment'\nimport { tlmenus } from '../globals/menus'\nimport { tltime } from '../globals/time'\nimport { TldrawOptions, defaultTldrawOptions } from '../options'\nimport { Box, BoxLike } from '../primitives/Box'\nimport { Mat, MatLike } from '../primitives/Mat'\nimport { Vec, VecLike } from '../primitives/Vec'\nimport { EASINGS } from '../primitives/easings'\nimport { Geometry2d } from '../primitives/geometry/Geometry2d'\nimport { Group2d } from '../primitives/geometry/Group2d'\nimport { intersectPolygonPolygon } from '../primitives/intersect'\nimport { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from '../primitives/utils'\nimport { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'\nimport { areShapesContentEqual } from '../utils/areShapesContentEqual'\nimport { dataUrlToFile } from '../utils/assets'\nimport { debugFlags } from '../utils/debug-flags'\nimport {\n\tTLDeepLink,\n\tTLDeepLinkOptions,\n\tcreateDeepLinkString,\n\tparseDeepLinkString,\n} from '../utils/deepLinks'\nimport { getIncrementedName } from '../utils/getIncrementedName'\nimport { isAccelKey } from '../utils/keyboard'\nimport { getReorderingShapesChanges } from '../utils/reorderShapes'\nimport { TLTextOptions, TiptapEditor } from '../utils/richText'\nimport { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'\nimport { BindingOnDeleteOptions, BindingUtil } from './bindings/BindingUtil'\nimport { bindingsIndex } from './derivations/bindingsIndex'\nimport { notVisibleShapes } from './derivations/notVisibleShapes'\nimport { parentsToChildren } from './derivations/parentsToChildren'\nimport { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'\nimport { ClickManager } from './managers/ClickManager/ClickManager'\nimport { EdgeScrollManager } from './managers/EdgeScrollManager/EdgeScrollManager'\nimport { FocusManager } from './managers/FocusManager/FocusManager'\nimport { FontManager } from './managers/FontManager/FontManager'\nimport { HistoryManager } from './managers/HistoryManager/HistoryManager'\nimport { ScribbleManager } from './managers/ScribbleManager/ScribbleManager'\nimport { SnapManager } from './managers/SnapManager/SnapManager'\nimport { TextManager } from './managers/TextManager/TextManager'\nimport { TickManager } from './managers/TickManager/TickManager'\nimport { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'\nimport { ShapeUtil, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'\nimport { RootState } from './tools/RootState'\nimport { StateNode, TLStateNodeConstructor } from './tools/StateNode'\nimport { TLContent } from './types/clipboard-types'\nimport { TLEventMap } from './types/emit-types'\nimport {\n\tTLEventInfo,\n\tTLPinchEventInfo,\n\tTLPointerEventInfo,\n\tTLWheelEventInfo,\n} from './types/event-types'\nimport { TLExternalAsset, TLExternalContent } from './types/external-content'\nimport { TLHistoryBatchOptions } from './types/history-types'\nimport {\n\tOptionalKeys,\n\tRequiredKeys,\n\tTLCameraMoveOptions,\n\tTLCameraOptions,\n\tTLImageExportOptions,\n\tTLSvgExportOptions,\n} from './types/misc-types'\nimport { TLAdjacentDirection, TLResizeHandle } from './types/selection-types'\n\n/** @public */\nexport type TLResizeShapeOptions = Partial<{\n\tinitialBounds: Box\n\tscaleOrigin: VecLike\n\tscaleAxisRotation: number\n\tinitialShape: TLShape\n\tinitialPageTransform: MatLike\n\tdragHandle: TLResizeHandle\n\tisAspectRatioLocked: boolean\n\tmode: TLResizeMode\n\tskipStartAndEndCallbacks: boolean\n}>\n\n/** @public */\nexport interface TLEditorOptions {\n\t/**\n\t * The Store instance to use for keeping the app's data. This may be prepopulated, e.g. by loading\n\t * from a server or database.\n\t */\n\tstore: TLStore\n\t/**\n\t * An array of shapes to use in the editor. These will be used to create and manage shapes in the editor.\n\t */\n\tshapeUtils: readonly TLAnyShapeUtilConstructor[]\n\t/**\n\t * An array of bindings to use in the editor. These will be used to create and manage bindings in the editor.\n\t */\n\tbindingUtils: readonly TLAnyBindingUtilConstructor[]\n\t/**\n\t * An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor.\n\t */\n\ttools: readonly TLStateNodeConstructor[]\n\t/**\n\t * Should return a containing html element which has all the styles applied to the editor. If not\n\t * given, the body element will be used.\n\t */\n\tgetContainer(): HTMLElement\n\t/**\n\t * A user defined externally to replace the default user.\n\t */\n\tuser?: TLUser\n\t/**\n\t * The editor's initial active tool (or other state node id).\n\t */\n\tinitialState?: string\n\t/**\n\t * Whether to automatically focus the editor when it mounts.\n\t */\n\tautoFocus?: boolean\n\t/**\n\t * Whether to infer dark mode from the user's system preferences. Defaults to false.\n\t */\n\tinferDarkMode?: boolean\n\t/**\n\t * Options for the editor's camera.\n\t */\n\tcameraOptions?: Partial<TLCameraOptions>\n\ttextOptions?: TLTextOptions\n\toptions?: Partial<TldrawOptions>\n\tlicenseKey?: string\n\tfontAssetUrls?: { [key: string]: string | undefined }\n\t/**\n\t * A predicate that should return true if the given shape should be hidden.\n\t *\n\t * @deprecated Use {@link Editor#getShapeVisibility} instead.\n\t *\n\t * @param shape - The shape to check.\n\t * @param editor - The editor instance.\n\t */\n\tisShapeHidden?(shape: TLShape, editor: Editor): boolean\n\n\t/**\n\t * Provides a way to hide shapes.\n\t *\n\t * @example\n\t * ```ts\n\t * getShapeVisibility={(shape, editor) => shape.meta.hidden ? 'hidden' : 'inherit'}\n\t * ```\n\t *\n\t * - `'inherit' | undefined` - (default) The shape will be visible unless its parent is hidden.\n\t * - `'hidden'` - The shape will be hidden.\n\t * - `'visible'` - The shape will be visible.\n\t *\n\t * @param shape - The shape to check.\n\t * @param editor - The editor instance.\n\t */\n\tgetShapeVisibility?(\n\t\tshape: TLShape,\n\t\teditor: Editor\n\t): 'visible' | 'hidden' | 'inherit' | null | undefined\n}\n\n/**\n * Options for {@link Editor.(run:1)}.\n * @public\n */\nexport interface TLEditorRunOptions extends TLHistoryBatchOptions {\n\tignoreShapeLock?: boolean\n}\n\n/** @public */\nexport interface TLRenderingShape {\n\tid: TLShapeId\n\tshape: TLShape\n\tutil: ShapeUtil\n\tindex: number\n\tbackgroundIndex: number\n\topacity: number\n}\n\n/** @public */\nexport class Editor extends EventEmitter<TLEventMap> {\n\treadonly id = uniqueId()\n\tconstructor({\n\t\tstore,\n\t\tuser,\n\t\tshapeUtils,\n\t\tbindingUtils,\n\t\ttools,\n\t\tgetContainer,\n\t\tcameraOptions,\n\t\ttextOptions,\n\t\tinitialState,\n\t\tautoFocus,\n\t\tinferDarkMode,\n\t\toptions,\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tisShapeHidden,\n\t\tgetShapeVisibility,\n\t\tfontAssetUrls,\n\t}: TLEditorOptions) {\n\t\tsuper()\n\t\tassert(\n\t\t\t!(isShapeHidden && getShapeVisibility),\n\t\t\t'Cannot use both isShapeHidden and getShapeVisibility'\n\t\t)\n\n\t\tthis._getShapeVisibility = isShapeHidden\n\t\t\t? // eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t(shape: TLShape, editor: Editor) => (isShapeHidden(shape, editor) ? 'hidden' : 'inherit')\n\t\t\t: getShapeVisibility\n\n\t\tthis.options = { ...defaultTldrawOptions, ...options }\n\n\t\tthis.store = store\n\t\tthis.history = new HistoryManager<TLRecord>({\n\t\t\tstore,\n\t\t\tannotateError: (error: any) => {\n\t\t\t\tthis.annotateError(error, { origin: 'history.batch', willCrashApp: true })\n\t\t\t\tthis.crash(error)\n\t\t\t},\n\t\t})\n\n\t\tthis.snaps = new SnapManager(this)\n\n\t\tthis.disposables.add(this.timers.dispose)\n\n\t\tthis._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })\n\n\t\tthis._textOptions = atom('text options', textOptions ?? null)\n\n\t\tthis.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)\n\t\tthis.disposables.add(() => this.user.dispose())\n\n\t\tthis.getContainer = getContainer\n\n\t\tthis.textMeasure = new TextManager(this)\n\t\tthis.disposables.add(() => this.textMeasure.dispose())\n\n\t\tthis.fonts = new FontManager(this, fontAssetUrls)\n\n\t\tthis._tickManager = new TickManager(this)\n\n\t\tclass NewRoot extends RootState {\n\t\t\tstatic override initial = initialState ?? ''\n\t\t}\n\n\t\tthis.root = new NewRoot(this)\n\t\tthis.root.children = {}\n\n\t\tconst allShapeUtils = checkShapesAndAddCore(shapeUtils)\n\n\t\tconst _shapeUtils = {} as Record<string, ShapeUtil<any>>\n\t\tconst _styleProps = {} as Record<string, Map<StyleProp<unknown>, string>>\n\t\tconst allStylesById = new Map<string, StyleProp<unknown>>()\n\n\t\tfor (const Util of allShapeUtils) {\n\t\t\tconst util = new Util(this)\n\t\t\t_shapeUtils[Util.type] = util\n\n\t\t\tconst propKeysByStyle = getShapePropKeysByStyle(Util.props ?? {})\n\t\t\t_styleProps[Util.type] = propKeysByStyle\n\n\t\t\tfor (const style of propKeysByStyle.keys()) {\n\t\t\t\tif (!allStylesById.has(style.id)) {\n\t\t\t\t\tallStylesById.set(style.id, style)\n\t\t\t\t} else if (allStylesById.get(style.id) !== style) {\n\t\t\t\t\tthrow Error(\n\t\t\t\t\t\t`Multiple style props with id \"${style.id}\" in use. Style prop IDs must be unique.`\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.shapeUtils = _shapeUtils\n\t\tthis.styleProps = _styleProps\n\n\t\tconst allBindingUtils = checkBindings(bindingUtils)\n\t\tconst _bindingUtils = {} as Record<string, BindingUtil<any>>\n\t\tfor (const Util of allBindingUtils) {\n\t\t\tconst util = new Util(this)\n\t\t\t_bindingUtils[Util.type] = util\n\t\t}\n\t\tthis.bindingUtils = _bindingUtils\n\n\t\t// Tools.\n\t\t// Accept tools from constructor parameters which may not conflict with the root note's default or\n\t\t// \"baked in\" tools, select and zoom.\n\t\tfor (const Tool of [...tools]) {\n\t\t\tif (hasOwnProperty(this.root.children!, Tool.id)) {\n\t\t\t\tthrow Error(`Can't override tool with id \"${Tool.id}\"`)\n\t\t\t}\n\t\t\tthis.root.children![Tool.id] = new Tool(this, this.root)\n\t\t}\n\n\t\tthis.scribbles = new ScribbleManager(this)\n\n\t\t// Cleanup\n\n\t\tconst cleanupInstancePageState = (\n\t\t\tprevPageState: TLInstancePageState,\n\t\t\tshapesNoLongerInPage: Set<TLShapeId>\n\t\t) => {\n\t\t\tlet nextPageState = null as null | TLInstancePageState\n\n\t\t\tconst selectedShapeIds = prevPageState.selectedShapeIds.filter(\n\t\t\t\t(id) => !shapesNoLongerInPage.has(id)\n\t\t\t)\n\t\t\tif (selectedShapeIds.length !== prevPageState.selectedShapeIds.length) {\n\t\t\t\tif (!nextPageState) nextPageState = { ...prevPageState }\n\t\t\t\tnextPageState.selectedShapeIds = selectedShapeIds\n\t\t\t}\n\n\t\t\tconst erasingShapeIds = prevPageState.erasingShapeIds.filter(\n\t\t\t\t(id) => !shapesNoLongerInPage.has(id)\n\t\t\t)\n\t\t\tif (erasingShapeIds.length !== prevPageState.erasingShapeIds.length) {\n\t\t\t\tif (!nextPageState) nextPageState = { ...prevPageState }\n\t\t\t\tnextPageState.erasingShapeIds = erasingShapeIds\n\t\t\t}\n\n\t\t\tif (prevPageState.hoveredShapeId && shapesNoLongerInPage.has(prevPageState.hoveredShapeId)) {\n\t\t\t\tif (!nextPageState) nextPageState = { ...prevPageState }\n\t\t\t\tnextPageState.hoveredShapeId = null\n\t\t\t}\n\n\t\t\tif (prevPageState.editingShapeId && shapesNoLongerInPage.has(prevPageState.editingShapeId)) {\n\t\t\t\tif (!nextPageState) nextPageState = { ...prevPageState }\n\t\t\t\tnextPageState.editingShapeId = null\n\t\t\t}\n\n\t\t\tconst hintingShapeIds = prevPageState.hintingShapeIds.filter(\n\t\t\t\t(id) => !shapesNoLongerInPage.has(id)\n\t\t\t)\n\t\t\tif (hintingShapeIds.length !== prevPageState.hintingShapeIds.length) {\n\t\t\t\tif (!nextPageState) nextPageState = { ...prevPageState }\n\t\t\t\tnextPageState.hintingShapeIds = hintingShapeIds\n\t\t\t}\n\n\t\t\tif (prevPageState.focusedGroupId && shapesNoLongerInPage.has(prevPageState.focusedGroupId)) {\n\t\t\t\tif (!nextPageState) nextPageState = { ...prevPageState }\n\t\t\t\tnextPageState.focusedGroupId = null\n\t\t\t}\n\t\t\treturn nextPageState\n\t\t}\n\n\t\tthis.sideEffects = this.store.sideEffects\n\n\t\tlet deletedBindings = new Map<TLBindingId, BindingOnDeleteOptions<any>>()\n\t\tconst deletedShapeIds = new Set<TLShapeId>()\n\t\tconst invalidParents = new Set<TLShapeId>()\n\t\tlet invalidBindingTypes = new Set<string>()\n\t\tthis.disposables.add(\n\t\t\tthis.sideEffects.registerOperationCompleteHandler(() => {\n\t\t\t\t// this needs to be cleared here because further effects may delete more shapes\n\t\t\t\t// and we want the next invocation of this handler to handle those separately\n\t\t\t\tdeletedShapeIds.clear()\n\n\t\t\t\tfor (const parentId of invalidParents) {\n\t\t\t\t\tinvalidParents.delete(parentId)\n\t\t\t\t\tconst parent = this.getShape(parentId)\n\t\t\t\t\tif (!parent) continue\n\n\t\t\t\t\tconst util = this.getShapeUtil(parent)\n\t\t\t\t\tconst changes = util.onChildrenChange?.(parent)\n\n\t\t\t\t\tif (changes?.length) {\n\t\t\t\t\t\tthis.updateShapes(changes)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (invalidBindingTypes.size) {\n\t\t\t\t\tconst t = invalidBindingTypes\n\t\t\t\t\tinvalidBindingTypes = new Set()\n\t\t\t\t\tfor (const type of t) {\n\t\t\t\t\t\tconst util = this.getBindingUtil(type)\n\t\t\t\t\t\tutil.onOperationComplete?.()\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (deletedBindings.size) {\n\t\t\t\t\tconst t = deletedBindings\n\t\t\t\t\tdeletedBindings = new Map()\n\t\t\t\t\tfor (const opts of t.values()) {\n\t\t\t\t\t\tthis.getBindingUtil(opts.binding).onAfterDelete?.(opts)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.emit('update')\n\t\t\t})\n\t\t)\n\n\t\tthis.disposables.add(\n\t\t\tthis.sideEffects.register({\n\t\t\t\tshape: {\n\t\t\t\t\tafterChange: (shapeBefore, shapeAfter) => {\n\t\t\t\t\t\tfor (const binding of this.getBindingsInvolvingShape(shapeAfter)) {\n\t\t\t\t\t\t\tinvalidBindingTypes.add(binding.type)\n\t\t\t\t\t\t\tif (binding.fromId === shapeAfter.id) {\n\t\t\t\t\t\t\t\tthis.getBindingUtil(binding).onAfterChangeFromShape?.({\n\t\t\t\t\t\t\t\t\tbinding,\n\t\t\t\t\t\t\t\t\tshapeBefore,\n\t\t\t\t\t\t\t\t\tshapeAfter,\n\t\t\t\t\t\t\t\t\treason: 'self',\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (binding.toId === shapeAfter.id) {\n\t\t\t\t\t\t\t\tthis.getBindingUtil(binding).onAfterChangeToShape?.({\n\t\t\t\t\t\t\t\t\tbinding,\n\t\t\t\t\t\t\t\t\tshapeBefore,\n\t\t\t\t\t\t\t\t\tshapeAfter,\n\t\t\t\t\t\t\t\t\treason: 'self',\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// if the shape's parent changed and it has a binding, update the binding\n\t\t\t\t\t\tif (shapeBefore.parentId !== shapeAfter.parentId) {\n\t\t\t\t\t\t\tconst notifyBindingAncestryChange = (id: TLShapeId) => {\n\t\t\t\t\t\t\t\tconst descendantShape = this.getShape(id)\n\t\t\t\t\t\t\t\tif (!descendantShape) return\n\n\t\t\t\t\t\t\t\tfor (const binding of this.getBindingsInvolvingShape(descendantShape)) {\n\t\t\t\t\t\t\t\t\tinvalidBindingTypes.add(binding.type)\n\n\t\t\t\t\t\t\t\t\tif (binding.fromId === descendantShape.id) {\n\t\t\t\t\t\t\t\t\t\tthis.getBindingUtil(binding).onAfterChangeFromShape?.({\n\t\t\t\t\t\t\t\t\t\t\tbinding,\n\t\t\t\t\t\t\t\t\t\t\tshapeBefore: descendantShape,\n\t\t\t\t\t\t\t\t\t\t\tshapeAfter: descendantShape,\n\t\t\t\t\t\t\t\t\t\t\treason: 'ancestry',\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif (binding.toId === descendantShape.id) {\n\t\t\t\t\t\t\t\t\t\tthis.getBindingUtil(binding).onAfterChangeToShape?.({\n\t\t\t\t\t\t\t\t\t\t\tbinding,\n\t\t\t\t\t\t\t\t\t\t\tshapeBefore: descendantShape,\n\t\t\t\t\t\t\t\t\t\t\tshapeAfter: descendantShape,\n\t\t\t\t\t\t\t\t\t\t\treason: 'ancestry',\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnotifyBindingAncestryChange(shapeAfter.id)\n\t\t\t\t\t\t\tthis.visitDescendants(shapeAfter.id, notifyBindingAncestryChange)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// if this shape moved to a new page, clean up any previous page's instance state\n\t\t\t\t\t\tif (shapeBefore.parentId !== shapeAfter.parentId && isPageId(shapeAfter.parentId)) {\n\t\t\t\t\t\t\tconst allMovingIds = new Set([shapeBefore.id])\n\t\t\t\t\t\t\tthis.visitDescendants(shapeBefore.id, (id) => {\n\t\t\t\t\t\t\t\tallMovingIds.add(id)\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\t\tfor (const instancePageState of this.getPageStates()) {\n\t\t\t\t\t\t\t\tif (instancePageState.pageId === shapeAfter.parentId) continue\n\t\t\t\t\t\t\t\tconst nextPageState = cleanupInstancePageState(instancePageState, allMovingIds)\n\n\t\t\t\t\t\t\t\tif (nextPageState) {\n\t\t\t\t\t\t\t\t\tthis.store.put([nextPageState])\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (shapeBefore.parentId && isShapeId(shapeBefore.parentId)) {\n\t\t\t\t\t\t\tinvalidParents.add(shapeBefore.parentId)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (shapeAfter.parentId !== shapeBefore.parentId && isShapeId(shapeAfter.parentId)) {\n\t\t\t\t\t\t\tinvalidParents.add(shapeAfter.parentId)\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tbeforeDelete: (shape) => {\n\t\t\t\t\t\t// if we triggered this delete with a recursive call, don't do anything\n\t\t\t\t\t\tif (deletedShapeIds.has(shape.id)) return\n\t\t\t\t\t\t// if the deleted shape has a parent shape make sure we call it's onChildrenChange callback\n\t\t\t\t\t\tif (shape.parentId && isShapeId(shape.parentId)) {\n\t\t\t\t\t\t\tinvalidParents.add(shape.parentId)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdeletedShapeIds.add(shape.id)\n\n\t\t\t\t\t\tconst deleteBindingIds: TLBindingId[] = []\n\t\t\t\t\t\tfor (const binding of this.getBindingsInvolvingShape(shape)) {\n\t\t\t\t\t\t\tinvalidBindingTypes.add(binding.type)\n\t\t\t\t\t\t\tdeleteBindingIds.push(binding.id)\n\t\t\t\t\t\t\tconst util = this.getBindingUtil(binding)\n\t\t\t\t\t\t\tif (binding.fromId === shape.id) {\n\t\t\t\t\t\t\t\tutil.onBeforeIsolateToShape?.({ binding, removedShape: shape })\n\t\t\t\t\t\t\t\tutil.onBeforeDeleteFromShape?.({ binding, shape })\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tutil.onBeforeIsolateFromShape?.({ binding, removedShape: shape })\n\t\t\t\t\t\t\t\tutil.onBeforeDeleteToShape?.({ binding, shape })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (deleteBindingIds.length) {\n\t\t\t\t\t\t\tthis.deleteBindings(deleteBindingIds)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst deletedIds = new Set([shape.id])\n\t\t\t\t\t\tconst updates = compact(\n\t\t\t\t\t\t\tthis.getPageStates().map((pageState) => {\n\t\t\t\t\t\t\t\treturn cleanupInstancePageState(pageState, deletedIds)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\tif (updates.length) {\n\t\t\t\t\t\t\tthis.store.put(updates)\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbinding: {\n\t\t\t\t\tbeforeCreate: (binding) => {\n\t\t\t\t\t\tconst next = this.getBindingUtil(binding).onBeforeCreate?.({ binding })\n\t\t\t\t\t\tif (next) return next\n\t\t\t\t\t\treturn binding\n\t\t\t\t\t},\n\t\t\t\t\tafterCreate: (binding) => {\n\t\t\t\t\t\tinvalidBindingTypes.add(binding.type)\n\t\t\t\t\t\tthis.getBindingUtil(binding).onAfterCreate?.({ binding })\n\t\t\t\t\t},\n\t\t\t\t\tbeforeChange: (bindingBefore, bindingAfter) => {\n\t\t\t\t\t\tconst updated = this.getBindingUtil(bindingAfter).onBeforeChange?.({\n\t\t\t\t\t\t\tbindingBefore,\n\t\t\t\t\t\t\tbindingAfter,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif (updated) return updated\n\t\t\t\t\t\treturn bindingAfter\n\t\t\t\t\t},\n\t\t\t\t\tafterChange: (bindingBefore, bindingAfter) => {\n\t\t\t\t\t\tinvalidBindingTypes.add(bindingAfter.type)\n\t\t\t\t\t\tthis.getBindingUtil(bindingAfter).onAfterChange?.({ bindingBefore, bindingAfter })\n\t\t\t\t\t},\n\t\t\t\t\tbeforeDelete: (binding) => {\n\t\t\t\t\t\tthis.getBindingUtil(binding).onBeforeDelete?.({ binding })\n\t\t\t\t\t},\n\t\t\t\t\tafterDelete: (binding) => {\n\t\t\t\t\t\tthis.getBindingUtil(binding).onAfterDelete?.({ binding })\n\t\t\t\t\t\tinvalidBindingTypes.add(binding.type)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpage: {\n\t\t\t\t\tafterCreate: (record) => {\n\t\t\t\t\t\tconst cameraId = CameraRecordType.createId(record.id)\n\t\t\t\t\t\tconst _pageStateId = InstancePageStateRecordType.createId(record.id)\n\t\t\t\t\t\tif (!this.store.has(cameraId)) {\n\t\t\t\t\t\t\tthis.store.put([CameraRecordType.create({ id: cameraId })])\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!this.store.has(_pageStateId)) {\n\t\t\t\t\t\t\tthis.store.put([\n\t\t\t\t\t\t\t\tInstancePageStateRecordType.create({ id: _pageStateId, pageId: record.id }),\n\t\t\t\t\t\t\t])\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tafterDelete: (record, source) => {\n\t\t\t\t\t\t// page was deleted, need to check whether it's the current page and select another one if so\n\t\t\t\t\t\tif (this.getInstanceState()?.currentPageId === record.id) {\n\t\t\t\t\t\t\tconst backupPageId = this.getPages().find((p) => p.id !== record.id)?.id\n\t\t\t\t\t\t\tif (backupPageId) {\n\t\t\t\t\t\t\t\tthis.store.put([{ ...this.getInstanceState(), currentPageId: backupPageId }])\n\t\t\t\t\t\t\t} else if (source === 'user') {\n\t\t\t\t\t\t\t\t// fall back to ensureStoreIsUsable:\n\t\t\t\t\t\t\t\tthis.store.ensureStoreIsUsable()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// delete the camera and state for the page if necessary\n\t\t\t\t\t\tconst cameraId = CameraRecordType.createId(record.id)\n\t\t\t\t\t\tconst instance_PageStateId = InstancePageStateRecordType.createId(record.id)\n\t\t\t\t\t\tthis.store.remove([cameraId, instance_PageStateId])\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tinstance: {\n\t\t\t\t\tafterChange: (prev, next, source) => {\n\t\t\t\t\t\t// instance should never be updated to a page that no longer exists (this can\n\t\t\t\t\t\t// happen when undoing a change that involves switching to a page that has since\n\t\t\t\t\t\t// been deleted by another user)\n\t\t\t\t\t\tif (!this.store.has(next.currentPageId)) {\n\t\t\t\t\t\t\tconst backupPageId = this.store.has(prev.currentPageId)\n\t\t\t\t\t\t\t\t? prev.currentPageId\n\t\t\t\t\t\t\t\t: this.getPages()[0]?.id\n\t\t\t\t\t\t\tif (backupPageId) {\n\t\t\t\t\t\t\t\tthis.store.update(next.id, (instance) => ({\n\t\t\t\t\t\t\t\t\t...instance,\n\t\t\t\t\t\t\t\t\tcurrentPageId: backupPageId,\n\t\t\t\t\t\t\t\t}))\n\t\t\t\t\t\t\t} else if (source === 'user') {\n\t\t\t\t\t\t\t\t// fall back to ensureStoreIsUsable:\n\t\t\t\t\t\t\t\tthis.store.ensureStoreIsUsable()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tinstance_page_state: {\n\t\t\t\t\tafterChange: (prev, next) => {\n\t\t\t\t\t\tif (prev?.selectedShapeIds !== next?.selectedShapeIds) {\n\t\t\t\t\t\t\t// ensure that descendants and ancestors are not selected at the same time\n\t\t\t\t\t\t\tconst filtered = next.selectedShapeIds.filter((id) => {\n\t\t\t\t\t\t\t\tlet parentId = this.getShape(id)?.parentId\n\t\t\t\t\t\t\t\twhile (isShapeId(parentId)) {\n\t\t\t\t\t\t\t\t\tif (next.selectedShapeIds.includes(parentId)) {\n\t\t\t\t\t\t\t\t\t\treturn false\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tparentId = this.getShape(parentId)?.parentId\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\t\tlet nextFocusedGroupId: null | TLShapeId = null\n\n\t\t\t\t\t\t\tif (filtered.length > 0) {\n\t\t\t\t\t\t\t\tconst commonGroupAncestor = this.findCommonAncestor(\n\t\t\t\t\t\t\t\t\tcompact(filtered.map((id) => this.getShape(id))),\n\t\t\t\t\t\t\t\t\t(shape) => this.isShapeOfType<TLGroupShape>(shape, 'group')\n\t\t\t\t\t\t\t\t)\n\n\t\t\t\t\t\t\t\tif (commonGroupAncestor) {\n\t\t\t\t\t\t\t\t\tnextFocusedGroupId = commonGroupAncestor\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (next?.focusedGroupId) {\n\t\t\t\t\t\t\t\t\tnextFocusedGroupId = next.focusedGroupId\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tfiltered.length !== next.selectedShapeIds.length ||\n\t\t\t\t\t\t\t\tnextFocusedGroupId !== next.focusedGroupId\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tthis.store.put([\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t...next,\n\t\t\t\t\t\t\t\t\t\tselectedShapeIds: filtered,\n\t\t\t\t\t\t\t\t\t\tfocusedGroupId: nextFocusedGroupId ?? null,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t)\n\n\t\tthis._currentPageShapeIds = deriveShapeIdsInCurrentPage(this.store, () =>\n\t\t\tthis.getCurrentPageId()\n\t\t)\n\t\tthis._parentIdsToChildIds = parentsToChildren(this.store)\n\n\t\tthis.disposables.add(\n\t\t\tthis.store.listen((changes) => {\n\t\t\t\tthis.emit('change', changes)\n\t\t\t})\n\t\t)\n\t\tthis.disposables.add(this.history.dispose)\n\n\t\tthis.run(\n\t\t\t() => {\n\t\t\t\tthis.store.ensureStoreIsUsable()\n\n\t\t\t\t// clear ephemeral state\n\t\t\t\tthis._updateCurrentPageState({\n\t\t\t\t\teditingShapeId: null,\n\t\t\t\t\thoveredShapeId: null,\n\t\t\t\t\terasingShapeIds: [],\n\t\t\t\t})\n\t\t\t},\n\t\t\t{ history: 'ignore' }\n\t\t)\n\n\t\tif (initialState && this.root.children[initialState] === undefined) {\n\t\t\tthrow Error(`No state found for initialState \"${initialState}\".`)\n\t\t}\n\n\t\tthis.root.enter(undefined, 'initial')\n\n\t\tthis.edgeScrollManager = new EdgeScrollManager(this)\n\t\tthis.focusManager = new FocusManager(this, autoFocus)\n\t\tthis.disposables.add(this.focusManager.dispose.bind(this.focusManager))\n\n\t\tif (this.getInstanceState().followingUserId) {\n\t\t\tthis.stopFollowingUser()\n\t\t}\n\n\t\tthis.on('tick', this._flushEventsForTick)\n\n\t\tthis.timers.requestAnimationFrame(() => {\n\t\t\tthis._tickManager.start()\n\t\t})\n\n\t\tthis.performanceTracker = new PerformanceTracker()\n\n\t\tif (this.store.props.collaboration?.mode) {\n\t\t\tconst mode = this.store.props.collaboration.mode\n\t\t\tthis.disposables.add(\n\t\t\t\treact('update collaboration mode', () => {\n\t\t\t\t\tthis.store.put([{ ...this.getInstanceState(), isReadonly: mode.get() === 'readonly' }])\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t}\n\n\tprivate readonly _getShapeVisibility?: TLEditorOptions['getShapeVisibility']\n\t@computed\n\tprivate getIsShapeHiddenCache() {\n\t\tif (!this._getShapeVisibility) return null\n\t\treturn this.store.createComputedCache<boolean, TLShape>('isShapeHidden', (shape: TLShape) => {\n\t\t\tconst visibility = this._getShapeVisibility!(shape, this)\n\t\t\tconst isParentHidden = PageRecordType.isId(shape.parentId)\n\t\t\t\t? false\n\t\t\t\t: this.isShapeHidden(shape.parentId)\n\n\t\t\tif (isParentHidden) return visibility !== 'visible'\n\t\t\treturn visibility === 'hidden'\n\t\t})\n\t}\n\tisShapeHidden(shapeOrId: TLShape | TLShapeId): boolean {\n\t\tif (!this._getShapeVisibility) return false\n\t\treturn !!this.getIsShapeHiddenCache!()!.get(\n\t\t\ttypeof shapeOrId === 'string' ? shapeOrId : shapeOrId.id\n\t\t)\n\t}\n\n\treadonly options: TldrawOptions\n\n\treadonly contextId = uniqueId()\n\n\t/**\n\t * The editor's store\n\t *\n\t * @public\n\t */\n\treadonly store: TLStore\n\n\t/**\n\t * The root state of the statechart.\n\t *\n\t * @public\n\t */\n\treadonly root: StateNode\n\n\t/**\n\t * A set of functions to call when the app is disposed.\n\t *\n\t * @public\n\t */\n\treadonly disposables = new Set<() => void>()\n\n\t/**\n\t * Whether the editor is disposed.\n\t *\n\t * @public\n\t */\n\tisDisposed = false\n\n\t/** @internal */\n\tprivate readonly _tickManager\n\n\t/**\n\t * A manager for the app's snapping feature.\n\t *\n\t * @public\n\t */\n\treadonly snaps: SnapManager\n\n\t/**\n\t * A manager for the any asynchronous events and making sure they're\n\t * cleaned up upon disposal.\n\t *\n\t * @public\n\t */\n\treadonly timers = tltime.forContext(this.contextId)\n\n\t/**\n\t * A manager for the user and their preferences.\n\t *\n\t * @public\n\t */\n\treadonly user: UserPreferencesManager\n\n\t/**\n\t * A helper for measuring text.\n\t *\n\t * @public\n\t */\n\treadonly textMeasure: TextManager\n\n\t/**\n\t * A utility for managing the set of fonts that should be rendered in the document.\n\t *\n\t * @public\n\t */\n\treadonly fonts: FontManager\n\n\t/**\n\t * A manager for the editor's environment.\n\t *\n\t * @deprecated This is deprecated and will be removed in a future version. Use the `tlenv` global export instead.\n\t * @public\n\t */\n\treadonly environment = tlenv\n\n\t/**\n\t * A manager for the editor's scribbles.\n\t *\n\t * @public\n\t */\n\treadonly scribbles: ScribbleManager\n\n\t/**\n\t * A manager for side effects and correct state enforcement. See {@link @tldraw/store#StoreSideEffects} for details.\n\t *\n\t * @public\n\t */\n\treadonly sideEffects: StoreSideEffects<TLRecord>\n\n\t/**\n\t * A manager for moving the camera when the mouse is at the edge of the screen.\n\t *\n\t * @public\n\t */\n\tedgeScrollManager: EdgeScrollManager\n\n\t/**\n\t * A manager for ensuring correct focus. See FocusManager for details.\n\t *\n\t * @internal\n\t */\n\tprivate focusManager: FocusManager\n\n\t/**\n\t * The current HTML element containing the editor.\n\t *\n\t * @example\n\t * ```ts\n\t * const container = editor.getContainer()\n\t * ```\n\t *\n\t * @public\n\t */\n\tgetContainer: () => HTMLElement\n\n\t/**\n\t * Dispose the editor.\n\t *\n\t * @public\n\t */\n\tdispose() {\n\t\tthis.disposables.forEach((dispose) => dispose())\n\t\tthis.disposables.clear()\n\t\tthis.store.dispose()\n\t\tthis.isDisposed = true\n\t}\n\n\t/* ------------------- Shape Utils ------------------ */\n\n\t/**\n\t * A map of shape utility classes (TLShapeUtils) by shape type.\n\t *\n\t * @public\n\t */\n\tshapeUtils: { readonly [K in string]?: ShapeUtil<TLUnknownShape> }\n\n\tstyleProps: { [key: string]: Map<StyleProp<any>, string> }\n\n\t/**\n\t * Get a shape util from a shape itself.\n\t *\n\t * @example\n\t * ```ts\n\t * const util = editor.getShapeUtil(myArrowShape)\n\t * const util = editor.getShapeUtil('arrow')\n\t * const util = editor.getShapeUtil<TLArrowShape>(myArrowShape)\n\t * const util = editor.getShapeUtil(TLArrowShape)('arrow')\n\t * ```\n\t *\n\t * @param shape - A shape, shape partial, or shape type.\n\t *\n\t * @public\n\t */\n\tgetShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): ShapeUtil<S>\n\tgetShapeUtil<S extends TLUnknownShape>(type: S['type']): ShapeUtil<S>\n\tgetShapeUtil<T extends ShapeUtil>(type: T extends ShapeUtil<infer R> ? R['type'] : string): T\n\tgetShapeUtil(arg: string | { type: string }) {\n\t\tconst type = typeof arg === 'string' ? arg : arg.type\n\t\tconst shapeUtil = getOwnProperty(this.shapeUtils, type)\n\t\tassert(shapeUtil, `No shape util found for type \"${type}\"`)\n\t\treturn shapeUtil\n\t}\n\n\t/**\n\t * Returns true if the editor has a shape util for the given shape / shape type.\n\t *\n\t * @param shape - A shape, shape partial, or shape type.\n\t */\n\thasShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): boolean\n\thasShapeUtil<S extends TLUnknownShape>(type: S['type']): boolean\n\thasShapeUtil<T extends ShapeUtil>(\n\t\ttype: T extends ShapeUtil<infer R> ? R['type'] : string\n\t): boolean\n\thasShapeUtil(arg: string | { type: string }): boolean {\n\t\tconst type = typeof arg === 'string' ? arg : arg.type\n\t\treturn hasOwnProperty(this.shapeUtils, type)\n\t}\n\n\t/* ------------------- Binding Utils ------------------ */\n\t/**\n\t * A map of shape utility classes (TLShapeUtils) by shape type.\n\t *\n\t * @public\n\t */\n\tbindingUtils: { readonly [K in string]?: BindingUtil<TLUnknownBinding> }\n\n\t/**\n\t * Get a binding util from a binding itself.\n\t *\n\t * @example\n\t * ```ts\n\t * const util = editor.getBindingUtil(myArrowBinding)\n\t * const util = editor.getBindingUtil('arrow')\n\t * const util = editor.getBindingUtil<TLArrowBinding>(myArrowBinding)\n\t * const util = editor.getBindingUtil(TLArrowBinding)('arrow')\n\t * ```\n\t *\n\t * @param binding - A binding, binding partial, or binding type.\n\t *\n\t * @public\n\t */\n\tgetBindingUtil<S extends TLUnknownBinding>(binding: S | { type: S['type'] }): BindingUtil<S>\n\tgetBindingUtil<S extends TLUnknownBinding>(type: S['type']): BindingUtil<S>\n\tgetBindingUtil<T extends BindingUtil>(\n\t\ttype: T extends BindingUtil<infer R> ? R['type'] : string\n\t): T\n\tgetBindingUtil(arg: string | { type: string }) {\n\t\tconst type = typeof arg === 'string' ? arg : arg.type\n\t\tconst bindingUtil = getOwnProperty(this.bindingUtils, type)\n\t\tassert(bindingUtil, `No binding util found for type \"${type}\"`)\n\t\treturn bindingUtil\n\t}\n\n\t/* --------------------- History -------------------- */\n\n\t/**\n\t * A manager for the app's history.\n\t *\n\t * @readonly\n\t */\n\tprotected readonly history: HistoryManager<TLRecord>\n\n\t/**\n\t * Undo to the last mark.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.undo()\n\t * ```\n\t *\n\t * @public\n\t */\n\tundo(): this {\n\t\tthis._flushEventsForTick(0)\n\t\tthis.complete()\n\t\tthis.history.undo()\n\t\treturn this\n\t}\n\n\t/**\n\t * Whether the app can undo.\n\t *\n\t * @public\n\t */\n\t@computed getCanUndo(): boolean {\n\t\treturn this.history.getNumUndos() > 0\n\t}\n\n\t/**\n\t * Redo to the next mark.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.redo()\n\t * ```\n\t *\n\t * @public\n\t */\n\tredo(): this {\n\t\tthis._flushEventsForTick(0)\n\t\tthis.complete()\n\t\tthis.history.redo()\n\t\treturn this\n\t}\n\n\tclearHistory() {\n\t\tthis.history.clear()\n\t\treturn this\n\t}\n\n\t/**\n\t * Whether the app can redo.\n\t *\n\t * @public\n\t */\n\t@computed getCanRedo(): boolean {\n\t\treturn this.history.getNumRedos() > 0\n\t}\n\n\t/**\n\t * Create a new \"mark\", or stopping point, in the undo redo history. Creating a mark will clear\n\t * any redos.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.mark()\n\t * editor.mark('flip shapes')\n\t * ```\n\t *\n\t * @param markId - The mark's id, usually the reason for adding the mark.\n\t *\n\t * @public\n\t * @deprecated use {@link Editor.markHistoryStoppingPoint} instead\n\t */\n\tmark(markId?: string): this {\n\t\tif (typeof markId === 'string') {\n\t\t\tconsole.warn(\n\t\t\t\t`[tldraw] \\`editor.history.mark(\"${markId}\")\\` is deprecated. Please use \\`const myMarkId = editor.markHistoryStoppingPoint()\\` instead.`\n\t\t\t)\n\t\t} else {\n\t\t\tconsole.warn(\n\t\t\t\t'[tldraw] `editor.mark()` is deprecated. Use `editor.markHistoryStoppingPoint()` instead.'\n\t\t\t)\n\t\t}\n\t\tthis.history._mark(markId ?? uniqueId())\n\t\treturn this\n\t}\n\n\t/**\n\t * Create a new \"mark\", or stopping point, in the undo redo history. Creating a mark will clear\n\t * any redos. You typically want to do this just before a user interaction begins or is handled.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.markHistoryStoppingPoint()\n\t * editor.flipShapes(editor.getSelectedShapes())\n\t * ```\n\t * @example\n\t * ```ts\n\t * const beginRotateMark = editor.markHistoryStoppingPoint()\n\t * // if the use cancels the rotation, you can bail back to this mark\n\t * editor.bailToMark(beginRotateMark)\n\t * ```\n\t *\n\t * @public\n\t * @param name - The name of the mark, useful for debugging the undo/redo stacks\n\t * @returns a unique id for the mark that can be used with `squashToMark` or `bailToMark`.\n\t */\n\tmarkHistoryStoppingPoint(name?: string): string {\n\t\tconst id = `[${name ?? 'stop'}]_${uniqueId()}`\n\t\tthis.history._mark(id)\n\t\treturn id\n\t}\n\n\t/**\n\t * @internal this is only used to implement some backwards-compatibility logic. Should be fine to delete after 6 months or whatever.\n\t */\n\tgetMarkIdMatching(idSubstring: string) {\n\t\treturn this.history.getMarkIdMatching(idSubstring)\n\t}\n\n\t/**\n\t * Coalesces all changes since the given mark into a single change, removing any intermediate marks.\n\t *\n\t * This is useful if you need to 'compress' the recent history to simplify the undo/redo experience of a complex interaction.\n\t *\n\t * @example\n\t * ```ts\n\t * const bumpShapesMark = editor.markHistoryStoppingPoint()\n\t * // ... some changes\n\t * editor.squashToMark(bumpShapesMark)\n\t * ```\n\t *\n\t * @param markId - The mark id to squash to.\n\t */\n\tsquashToMark(markId: string): this {\n\t\tthis.history.squashToMark(markId)\n\t\treturn this\n\t}\n\n\t/**\n\t * Undo to the closest mark, discarding the changes so they cannot be redone.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.bail()\n\t * ```\n\t *\n\t * @public\n\t */\n\tbail() {\n\t\tthis.history.bail()\n\t\treturn this\n\t}\n\n\t/**\n\t * Undo to the given mark, discarding the changes so they cannot be redone.\n\t *\n\t * @example\n\t * ```ts\n\t * const beginDrag = editor.markHistoryStoppingPoint()\n\t * // ... some changes\n\t * editor.bailToMark(beginDrag)\n\t * ```\n\t *\n\t * @public\n\t */\n\tbailToMark(id: string): this {\n\t\tthis.history.bailToMark(id)\n\t\treturn this\n\t}\n\n\tprivate _shouldIgnoreShapeLock = false\n\n\t/**\n\t * Run a function in a transaction with optional options for context.\n\t * You can use the options to change the way that history is treated\n\t * or allow changes to locked shapes.\n\t *\n\t * @example\n\t * ```ts\n\t * // updating with\n\t * editor.run(() => {\n\t * \teditor.updateShape({ ...myShape, x: 100 })\n\t * }, { history: \"ignore\" })\n\t *\n\t * // forcing changes / deletions for locked shapes\n\t * editor.toggleLock([myShape])\n\t * editor.run(() => {\n\t * \teditor.updateShape({ ...myShape, x: 100 })\n\t * \teditor.deleteShape(myShape)\n\t * }, { ignoreShapeLock: true }, )\n\t * ```\n\t *\n\t * @param fn - The callback function to run.\n\t * @param opts - The options for the batch.\n\t *\n\t *\n\t * @public\n\t */\n\trun(fn: () => void, opts?: TLEditorRunOptions): this {\n\t\tconst previousIgnoreShapeLock = this._shouldIgnoreShapeLock\n\t\tthis._shouldIgnoreShapeLock = opts?.ignoreShapeLock ?? previousIgnoreShapeLock\n\t\ttry {\n\t\t\tthis.history.batch(fn, opts)\n\t\t} finally {\n\t\t\tthis._shouldIgnoreShapeLock = previousIgnoreShapeLock\n\t\t}\n\n\t\treturn this\n\t}\n\n\t/**\n\t * @deprecated Use `Editor.run` instead.\n\t */\n\tbatch(fn: () => void, opts?: TLEditorRunOptions): this {\n\t\treturn this.run(fn, opts)\n\t}\n\n\t/* --------------------- Errors --------------------- */\n\n\t/** @internal */\n\tannotateError(\n\t\terror: unknown,\n\t\t{\n\t\t\torigin,\n\t\t\twillCrashApp,\n\t\t\ttags,\n\t\t\textras,\n\t\t}: {\n\t\t\torigin: string\n\t\t\twillCrashApp: boolean\n\t\t\ttags?: Record<string, string | boolean | number>\n\t\t\textras?: Record<string, unknown>\n\t\t}\n\t): this {\n\t\tconst defaultAnnotations = this.createErrorAnnotations(origin, willCrashApp)\n\t\tannotateError(error, {\n\t\t\ttags: { ...defaultAnnotations.tags, ...tags },\n\t\t\textras: { ...defaultAnnotations.extras, ...extras },\n\t\t})\n\t\tif (willCrashApp) {\n\t\t\tthis.store.markAsPossiblyCorrupted()\n\t\t}\n\t\treturn this\n\t}\n\n\t/** @internal */\n\tcreateErrorAnnotations(origin: string, willCrashApp: boolean | 'unknown') {\n\t\ttry {\n\t\t\tconst editingShapeId = this.getEditingShapeId()\n\t\t\treturn {\n\t\t\t\ttags: {\n\t\t\t\t\torigin: origin,\n\t\t\t\t\twillCrashApp,\n\t\t\t\t},\n\t\t\t\textras: {\n\t\t\t\t\tactiveStateNode: this.root.getPath(),\n\t\t\t\t\tselectedShapes: this.getSelectedShapes().map((s) => {\n\t\t\t\t\t\tconst { props, ...rest } = s\n\t\t\t\t\t\tconst { text: _text, richText: _richText, ...restProps } = props as any\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...rest,\n\t\t\t\t\t\t\tprops: restProps,\n\t\t\t\t\t\t}\n\t\t\t\t\t}),\n\t\t\t\t\tselectionCount: this.getSelectedShapes().length,\n\t\t\t\t\teditingShape: editingShapeId ? this.getShape(editingShapeId) : undefined,\n\t\t\t\t\tinputs: this.inputs,\n\t\t\t\t\tpageState: this.getCurrentPageState(),\n\t\t\t\t\tinstanceState: this.getInstanceState(),\n\t\t\t\t\tcollaboratorCount: this.getCollaboratorsOnCurrentPage().length,\n\t\t\t\t},\n\t\t\t}\n\t\t} catch {\n\t\t\treturn {\n\t\t\t\ttags: {\n\t\t\t\t\torigin: origin,\n\t\t\t\t\twillCrashApp,\n\t\t\t\t},\n\t\t\t\textras: {},\n\t\t\t}\n\t\t}\n\t}\n\n\t/** @internal */\n\tprivate _crashingError: unknown | null = null\n\n\t/**\n\t * We can't use an `atom` here because there's a chance that when `crashAndReportError` is called,\n\t * we're in a transaction that's about to be rolled back due to the same error we're currently\n\t * reporting.\n\t *\n\t * Instead, to listen to changes to this value, you need to listen to app's `crash` event.\n\t *\n\t * @internal\n\t */\n\tgetCrashingError() {\n\t\treturn this._crashingError\n\t}\n\n\t/** @internal */\n\tcrash(error: unknown): this {\n\t\tthis._crashingError = error\n\t\tthis.store.markAsPossiblyCorrupted()\n\t\tthis.emit('crash', { error })\n\t\treturn this\n\t}\n\n\t/* ------------------- Statechart ------------------- */\n\n\t/**\n\t * The editor's current path of active states.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.getPath() // \"select.idle\"\n\t * ```\n\t *\n\t * @public\n\t */\n\t@computed getPath() {\n\t\treturn this.root.getPath().split('root.')[1]\n\t}\n\n\t/**\n\t * Get whether a certain tool (or other state node) is currently active.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.isIn('select')\n\t * editor.isIn('select.brushing')\n\t * ```\n\t *\n\t * @param path - The path of active states, separated by periods.\n\t *\n\t * @public\n\t */\n\tisIn(path: string): boolean {\n\t\tconst ids = path.split('.').reverse()\n\t\tlet state = this.root as StateNode\n\t\twhile (ids.length > 0) {\n\t\t\tconst id = ids.pop()\n\t\t\tif (!id) return true\n\t\t\tconst current = state.getCurrent()\n\t\t\tif (current?.id === id) {\n\t\t\t\tif (ids.length === 0) return true\n\t\t\t\tstate = current\n\t\t\t\tcontinue\n\t\t\t} else return false\n\t\t}\n\t\treturn false\n\t}\n\n\t/**\n\t * Get whether the state node is in any of the given active paths.\n\t *\n\t * @example\n\t * ```ts\n\t * state.isInAny('select', 'erase')\n\t * state.isInAny('select.brushing', 'erase.idle')\n\t * ```\n\t *\n\t * @public\n\t */\n\tisInAny(...paths: string[]): boolean {\n\t\treturn paths.some((path) => this.isIn(path))\n\t}\n\n\t/**\n\t * Set the selected tool.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.setCurrentTool('hand')\n\t * editor.setCurrentTool('hand', { date: Date.now() })\n\t * ```\n\t *\n\t * @param id - The id of the tool to select.\n\t * @param info - Arbitrary data to pass along into the transition.\n\t *\n\t * @public\n\t */\n\tsetCurrentTool(id: string, info = {}): this {\n\t\tthis.root.transition(id, info)\n\t\treturn this\n\t}\n\n\t/**\n\t * The current selected tool.\n\t *\n\t * @public\n\t */\n\t@computed getCurrentTool(): StateNode {\n\t\treturn this.root.getCurrent()!\n\t}\n\n\t/**\n\t * The id of the current selected tool.\n\t *\n\t * @public\n\t */\n\t@computed getCurrentToolId(): string {\n\t\tconst currentTool = this.getCurrentTool()\n\t\tif (!currentTool) return ''\n\t\treturn currentTool.getCurrentToolIdMask() ?? currentTool.id\n\t}\n\n\t/**\n\t * Get a descendant by its path.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.getStateDescendant('select')\n\t * editor.getStateDescendant('select.brushing')\n\t * ```\n\t *\n\t * @param path - The descendant's path of state ids, separated by periods.\n\t *\n\t * @public\n\t */\n\tgetStateDescendant<T extends StateNode>(path: string): T | undefined {\n\t\tconst ids = path.split('.').reverse()\n\t\tlet state = this.root as StateNode\n\t\twhile (ids.length > 0) {\n\t\t\tconst id = ids.pop()\n\t\t\tif (!id) return state as T\n\t\t\tconst childState = state.children?.[id]\n\t\t\tif (!childState) return undefined\n\t\t\tstate = childState\n\t\t}\n\t\treturn state as T\n\t}\n\n\t/* ---------------- Document Settings --------------- */\n\n\t/**\n\t * The global document settings that apply to all users.\n\t *\n\t * @public\n\t **/\n\t@computed getDocumentSettings() {\n\t\treturn this.store.get(TLDOCUMENT_ID)!\n\t}\n\n\t/**\n\t * Update the global document settings that apply to all users.\n\t *\n\t * @public\n\t **/\n\tupdateDocumentSettings(settings: Partial<TLDocument>): this {\n\t\tthis.run(\n\t\t\t() => {\n\t\t\t\tthis.store.put([{ ...this.getDocumentSettings(), ...settings }])\n\t\t\t},\n\t\t\t{ history: 'ignore' }\n\t\t)\n\t\treturn this\n\t}\n\n\t/* ----------------- Instance State ----------------- */\n\n\t/**\n\t * The current instance's state.\n\t *\n\t * @public\n\t */\n\t@computed getInstanceState(): TLInstance {\n\t\treturn this.store.get(TLINSTANCE_ID)!\n\t}\n\n\t/**\n\t * Update the instance's state.\n\t *\n\t * @param partial - A partial object to update the instance state with.\n\t * @param historyOptions - History batch options.\n\t *\n\t * @public\n\t */\n\tupdateInstanceState(\n\t\tpartial: Partial<Omit<TLInstance, 'currentPageId'>>,\n\t\thistoryOptions?: TLHistoryBatchOptions\n\t): this {\n\t\tthis._updateInstanceState(partial, { history: 'ignore', ...historyOptions })\n\n\t\tif (partial.isChangingStyle !== undefined) {\n\t\t\tclearTimeout(this._isChangingStyleTimeout)\n\t\t\tif (partial.isChangingStyle === true) {\n\t\t\t\t// If we've set to true, set a new reset timeout to change the value back to false after 1 seconds\n\t\t\t\tthis._isChangingStyleTimeout = this.timers.setTimeout(() => {\n\t\t\t\t\tthis._updateInstanceState({ isChangingStyle: false }, { history: 'ignore' })\n\t\t\t\t}, 1000)\n\t\t\t}\n\t\t}\n\n\t\treturn this\n\t}\n\n\t/** @internal */\n\t_updateInstanceState(\n\t\tpartial: Partial<Omit<TLInstance, 'currentPageId'>>,\n\t\topts?: TLHistoryBatchOptions\n\t) {\n\t\tthis.run(() => {\n\t\t\tthis.store.put([\n\t\t\t\t{\n\t\t\t\t\t...this.getInstanceState(),\n\t\t\t\t\t...partial,\n\t\t\t\t},\n\t\t\t])\n\t\t}, opts)\n\t}\n\n\t/** @internal */\n\tprivate _isChangingStyleTimeout = -1 as any\n\n\t// Menus\n\n\tmenus = tlmenus.forContext(this.contextId)\n\n\t/**\n\t * @deprecated Use `editor.menus.getOpenMenus` instead.\n\t *\n\t * @public\n\t */\n\t@computed getOpenMenus(): string[] {\n\t\treturn this.menus.getOpenMenus()\n\t}\n\n\t/**\n\t * @deprecated Use `editor.menus.addOpenMenu` instead.\n\t *\n\t * @public\n\t */\n\taddOpenMenu(id: string): this {\n\t\tthis.menus.addOpenMenu(id)\n\t\treturn this\n\t}\n\n\t/**\n\t * @deprecated Use `editor.menus.deleteOpenMenu` instead.\n\t *\n\t * @public\n\t */\n\tdeleteOpenMenu(id: string): this {\n\t\tthis.menus.deleteOpenMenu(id)\n\t\treturn this\n\t}\n\n\t/**\n\t * @deprecated Use `editor.menus.clearOpenMenus` instead.\n\t *\n\t * @public\n\t */\n\tclearOpenMenus(): this {\n\t\tthis.menus.clearOpenMenus()\n\t\treturn this\n\t}\n\n\t/**\n\t * @deprecated Use `editor.menus.hasAnyOpenMenus` instead.\n\t *\n\t * @public\n\t */\n\t@computed getIsMenuOpen(): boolean {\n\t\treturn this.menus.hasAnyOpenMenus()\n\t}\n\n\t/* --------------------- Cursor --------------------- */\n\n\t/**\n\t * Set the cursor.\n\t *\n\t * @param cursor - The cursor to set.\n\t * @public\n\t */\n\tsetCursor(cursor: Partial<TLCursor>) {\n\t\tthis.updateInstanceState({ cursor: { ...this.getInstanceState().cursor, ...cursor } })\n\t\treturn this\n\t}\n\n\t/* ------------------- Page State ------------------- */\n\n\t/**\n\t * Page states.\n\t *\n\t * @public\n\t */\n\t@computed getPageStates(): TLInstancePageState[] {\n\t\treturn this._getPageStatesQuery().get()\n\t}\n\n\t/** @internal */\n\t@computed private _getPageStatesQuery() {\n\t\treturn this.store.query.records('instance_page_state')\n\t}\n\n\t/**\n\t * The current page state.\n\t *\n\t * @public\n\t */\n\t@computed getCurrentPageState(): TLInstancePageState {\n\t\treturn this.store.get(this._getCurrentPageStateId())!\n\t}\n\n\t/** @internal */\n\t@computed private _getCurrentPageStateId() {\n\t\treturn InstancePageStateRecordType.createId(this.getCurrentPageId())\n\t}\n\n\t/**\n\t * Update this instance's page state.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.updateCurrentPageState({ id: 'page1', editingSha