2021-11-05 14:13:14 +00:00
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2021-10-28 21:49:00 +00:00
|
|
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
2021-09-17 21:29:45 +00:00
|
|
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
2021-10-16 19:34:34 +00:00
|
|
|
import { Vec } from '@tldraw/vec'
|
2021-08-10 16:12:55 +00:00
|
|
|
import {
|
|
|
|
TLBoundsEventHandler,
|
|
|
|
TLBoundsHandleEventHandler,
|
2021-09-23 11:14:22 +00:00
|
|
|
TLKeyboardEventHandler,
|
2021-10-14 15:37:52 +00:00
|
|
|
TLShapeCloneHandler,
|
2021-08-10 16:12:55 +00:00
|
|
|
TLCanvasEventHandler,
|
|
|
|
TLPageState,
|
|
|
|
TLPinchEventHandler,
|
|
|
|
TLPointerEventHandler,
|
|
|
|
TLWheelEventHandler,
|
|
|
|
Utils,
|
2021-09-06 11:07:15 +00:00
|
|
|
TLBounds,
|
2021-08-10 16:12:55 +00:00
|
|
|
} from '@tldraw/core'
|
2021-08-12 13:39:41 +00:00
|
|
|
import {
|
2021-08-13 09:28:09 +00:00
|
|
|
FlipType,
|
2021-11-16 16:01:29 +00:00
|
|
|
TDDocument,
|
2021-08-13 09:28:09 +00:00
|
|
|
MoveType,
|
|
|
|
AlignType,
|
|
|
|
StretchType,
|
|
|
|
DistributeType,
|
2021-08-12 13:39:41 +00:00
|
|
|
ShapeStyles,
|
2021-11-16 16:01:29 +00:00
|
|
|
TDShape,
|
|
|
|
TDShapeType,
|
|
|
|
TDSnapshot,
|
|
|
|
TDStatus,
|
|
|
|
TDPage,
|
|
|
|
TDBinding,
|
2021-09-02 12:51:39 +00:00
|
|
|
GroupShape,
|
2021-11-16 16:01:29 +00:00
|
|
|
TldrawCommand,
|
|
|
|
TDUser,
|
2021-10-13 13:55:31 +00:00
|
|
|
SessionType,
|
2021-11-16 16:01:29 +00:00
|
|
|
TDToolType,
|
2021-08-13 09:28:09 +00:00
|
|
|
} from '~types'
|
2021-11-06 11:16:30 +00:00
|
|
|
import {
|
|
|
|
migrate,
|
|
|
|
FileSystemHandle,
|
|
|
|
loadFileHandle,
|
|
|
|
openFromFileSystem,
|
|
|
|
saveToFileSystem,
|
|
|
|
} from './data'
|
|
|
|
import { TLDR } from './TLDR'
|
|
|
|
import { shapeUtils } from '~state/shapes'
|
2021-11-16 16:01:29 +00:00
|
|
|
import { defaultStyle } from '~state/shapes/shared/shape-styles'
|
2021-11-06 11:16:30 +00:00
|
|
|
import * as Commands from './commands'
|
2021-11-16 16:01:29 +00:00
|
|
|
import { SessionArgsOfType, getSession, TldrawSession } from './sessions'
|
2021-11-06 11:16:30 +00:00
|
|
|
import type { BaseTool } from './tools/BaseTool'
|
2021-11-26 15:14:10 +00:00
|
|
|
import { USER_COLORS, FIT_TO_SCREEN_PADDING, GRID_SIZE } from '~constants'
|
2021-11-16 16:01:29 +00:00
|
|
|
import { SelectTool } from './tools/SelectTool'
|
|
|
|
import { EraseTool } from './tools/EraseTool'
|
|
|
|
import { TextTool } from './tools/TextTool'
|
|
|
|
import { DrawTool } from './tools/DrawTool'
|
|
|
|
import { EllipseTool } from './tools/EllipseTool'
|
|
|
|
import { RectangleTool } from './tools/RectangleTool'
|
2021-11-22 12:28:56 +00:00
|
|
|
import { LineTool } from './tools/LineTool'
|
2021-11-16 16:01:29 +00:00
|
|
|
import { ArrowTool } from './tools/ArrowTool'
|
|
|
|
import { StickyTool } from './tools/StickyTool'
|
2021-11-22 14:00:24 +00:00
|
|
|
import { StateManager } from './StateManager'
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-10-09 13:57:44 +00:00
|
|
|
const uuid = Utils.uniqueId()
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
export interface TDCallbacks {
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the component mounts.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onMount?: (state: TldrawApp) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the component's state changes.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onChange?: (state: TldrawApp, reason?: string) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user creates a new project through the menu or through a keyboard shortcut.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onNewProject?: (state: TldrawApp, e?: KeyboardEvent) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user saves a project through the menu or through a keyboard shortcut.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onSaveProject?: (state: TldrawApp, e?: KeyboardEvent) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user saves a project as a new project through the menu or through a keyboard shortcut.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onSaveProjectAs?: (state: TldrawApp, e?: KeyboardEvent) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user opens new project through the menu or through a keyboard shortcut.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onOpenProject?: (state: TldrawApp, e?: KeyboardEvent) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user signs in via the menu.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onSignIn?: (state: TldrawApp) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user signs out via the menu.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onSignOut?: (state: TldrawApp) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the state is patched.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onPatch?: (state: TldrawApp, reason?: string) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the state is changed with a command.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onCommand?: (state: TldrawApp, reason?: string) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the state is persisted.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onPersist?: (state: TldrawApp) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user undos.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onUndo?: (state: TldrawApp) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user redos.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
onRedo?: (state: TldrawApp) => void
|
2021-11-22 14:00:24 +00:00
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user changes the current page's shapes.
|
|
|
|
*/
|
|
|
|
onChangePage?: (
|
|
|
|
app: TldrawApp,
|
|
|
|
shapes: Record<string, TDShape | undefined>,
|
|
|
|
bindings: Record<string, TDBinding | undefined>
|
|
|
|
) => void
|
|
|
|
/**
|
|
|
|
* (optional) A callback to run when the user creates a new project.
|
|
|
|
*/
|
|
|
|
onChangePresence?: (state: TldrawApp, user: TDUser) => void
|
2021-11-08 14:21:37 +00:00
|
|
|
}
|
2021-11-05 14:13:14 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
export class TldrawApp extends StateManager<TDSnapshot> {
|
|
|
|
callbacks: TDCallbacks = {}
|
2021-09-17 21:29:45 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
tools = {
|
|
|
|
select: new SelectTool(this),
|
|
|
|
erase: new EraseTool(this),
|
|
|
|
[TDShapeType.Text]: new TextTool(this),
|
|
|
|
[TDShapeType.Draw]: new DrawTool(this),
|
|
|
|
[TDShapeType.Ellipse]: new EllipseTool(this),
|
|
|
|
[TDShapeType.Rectangle]: new RectangleTool(this),
|
2021-11-22 12:28:56 +00:00
|
|
|
[TDShapeType.Line]: new LineTool(this),
|
2021-11-16 16:01:29 +00:00
|
|
|
[TDShapeType.Arrow]: new ArrowTool(this),
|
|
|
|
[TDShapeType.Sticky]: new StickyTool(this),
|
2021-08-15 14:35:23 +00:00
|
|
|
}
|
2021-08-17 21:38:37 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
currentTool: BaseTool = this.tools.select
|
2021-08-30 20:17:04 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
session?: TldrawSession
|
2021-10-13 13:55:31 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
readOnly = false
|
|
|
|
|
|
|
|
isDirty = false
|
|
|
|
|
|
|
|
isCreating = false
|
|
|
|
|
|
|
|
originPoint = [0, 0]
|
|
|
|
|
|
|
|
currentPoint = [0, 0]
|
|
|
|
|
|
|
|
previousPoint = [0, 0]
|
|
|
|
|
|
|
|
shiftKey = false
|
|
|
|
|
|
|
|
altKey = false
|
|
|
|
|
|
|
|
metaKey = false
|
|
|
|
|
|
|
|
ctrlKey = false
|
|
|
|
|
|
|
|
spaceKey = false
|
2021-10-13 13:55:31 +00:00
|
|
|
|
2021-11-09 14:26:41 +00:00
|
|
|
editingStartTime = -1
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
fileSystemHandle: FileSystemHandle | null = null
|
|
|
|
|
|
|
|
viewport = Utils.getBoundsFromPoints([
|
|
|
|
[0, 0],
|
|
|
|
[100, 100],
|
|
|
|
])
|
|
|
|
|
|
|
|
rendererBounds = Utils.getBoundsFromPoints([
|
|
|
|
[0, 0],
|
|
|
|
[100, 100],
|
|
|
|
])
|
2021-08-30 20:17:04 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
selectHistory = {
|
|
|
|
stack: [[]] as string[][],
|
|
|
|
pointer: 0,
|
2021-09-22 08:45:09 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
clipboard?: {
|
|
|
|
shapes: TDShape[]
|
|
|
|
bindings: TDBinding[]
|
|
|
|
}
|
2021-10-16 20:24:31 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
rotationInfo = {
|
|
|
|
selectedIds: [] as string[],
|
2021-09-21 15:47:04 +00:00
|
|
|
center: [0, 0],
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
pasteInfo = {
|
|
|
|
center: [0, 0],
|
|
|
|
offset: [0, 0],
|
|
|
|
}
|
2021-11-05 14:13:14 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
constructor(id?: string, callbacks = {} as TDCallbacks) {
|
|
|
|
super(TldrawApp.defaultState, id, TldrawApp.version, (prev, next, prevVersion) => {
|
2021-10-16 19:34:34 +00:00
|
|
|
return {
|
|
|
|
...next,
|
2021-11-05 07:03:44 +00:00
|
|
|
document: migrate(
|
|
|
|
{ ...next.document, ...prev.document, version: prevVersion },
|
2021-11-16 16:01:29 +00:00
|
|
|
TldrawApp.version
|
2021-11-05 07:03:44 +00:00
|
|
|
),
|
2021-10-16 19:34:34 +00:00
|
|
|
}
|
|
|
|
})
|
2021-08-30 20:17:04 +00:00
|
|
|
|
2021-11-08 14:21:37 +00:00
|
|
|
this.callbacks = callbacks
|
|
|
|
}
|
|
|
|
|
|
|
|
/* -------------------- Internal -------------------- */
|
|
|
|
|
|
|
|
protected onReady = () => {
|
2021-11-04 15:48:39 +00:00
|
|
|
this.loadDocument(this.document)
|
|
|
|
|
2021-11-05 14:13:14 +00:00
|
|
|
loadFileHandle().then((fileHandle) => {
|
|
|
|
this.fileSystemHandle = fileHandle
|
|
|
|
})
|
|
|
|
|
2021-10-16 19:34:34 +00:00
|
|
|
try {
|
|
|
|
this.patchState({
|
|
|
|
appState: {
|
2021-11-16 16:01:29 +00:00
|
|
|
status: TDStatus.Idle,
|
2021-10-16 19:34:34 +00:00
|
|
|
},
|
2021-11-16 16:01:29 +00:00
|
|
|
document: migrate(this.document, TldrawApp.version),
|
2021-10-16 19:34:34 +00:00
|
|
|
})
|
|
|
|
} catch (e) {
|
|
|
|
console.error('The data appears to be corrupted. Resetting!', e)
|
2021-11-05 06:52:28 +00:00
|
|
|
localStorage.setItem(this.document.id + '_corrupted', JSON.stringify(this.document))
|
2021-10-16 19:34:34 +00:00
|
|
|
|
|
|
|
this.patchState({
|
2021-11-16 16:01:29 +00:00
|
|
|
...TldrawApp.defaultState,
|
2021-10-16 19:34:34 +00:00
|
|
|
appState: {
|
2021-11-16 16:01:29 +00:00
|
|
|
...TldrawApp.defaultState.appState,
|
|
|
|
status: TDStatus.Idle,
|
2021-10-16 19:34:34 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-11-08 14:21:37 +00:00
|
|
|
this.callbacks.onMount?.(this)
|
2021-09-04 15:40:23 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Cleanup the state after each state change.
|
|
|
|
* @param state The new state
|
|
|
|
* @param prev The previous state
|
|
|
|
* @protected
|
|
|
|
* @returns The final state
|
|
|
|
*/
|
2021-11-20 09:37:42 +00:00
|
|
|
protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => {
|
2021-11-18 18:18:30 +00:00
|
|
|
const next = { ...state }
|
|
|
|
|
2021-08-13 09:28:09 +00:00
|
|
|
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
|
2021-11-18 18:18:30 +00:00
|
|
|
if (next.document !== prev.document) {
|
|
|
|
Object.entries(next.document.pages).forEach(([pageId, page]) => {
|
2021-08-17 21:38:37 +00:00
|
|
|
if (page === undefined) {
|
2021-08-29 13:33:43 +00:00
|
|
|
// If page is undefined, delete the page and pagestate
|
2021-11-18 18:18:30 +00:00
|
|
|
delete next.document.pages[pageId]
|
|
|
|
delete next.document.pageStates[pageId]
|
2021-08-17 21:38:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const prevPage = prev.document.pages[pageId]
|
|
|
|
|
2021-11-22 14:00:24 +00:00
|
|
|
const changedShapes: Record<string, TDShape | undefined> = {}
|
|
|
|
|
2021-08-17 23:11:00 +00:00
|
|
|
if (!prevPage || page.shapes !== prevPage.shapes || page.bindings !== prevPage.bindings) {
|
|
|
|
page.shapes = { ...page.shapes }
|
|
|
|
page.bindings = { ...page.bindings }
|
2021-08-11 12:26:34 +00:00
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
const groupsToUpdate = new Set<GroupShape>()
|
|
|
|
|
2021-08-29 13:33:43 +00:00
|
|
|
// If shape is undefined, delete the shape
|
2021-11-18 18:18:30 +00:00
|
|
|
Object.entries(page.shapes).forEach(([id, shape]) => {
|
2021-09-02 12:51:39 +00:00
|
|
|
let parentId: string
|
|
|
|
|
|
|
|
if (!shape) {
|
2021-11-22 14:00:24 +00:00
|
|
|
parentId = prevPage?.shapes[id]?.parentId
|
2021-09-02 12:51:39 +00:00
|
|
|
delete page.shapes[id]
|
|
|
|
} else {
|
|
|
|
parentId = shape.parentId
|
|
|
|
}
|
|
|
|
|
2021-11-22 14:00:24 +00:00
|
|
|
if (page.id === next.appState.currentPageId) {
|
|
|
|
if (prevPage?.shapes[id] !== shape) {
|
|
|
|
changedShapes[id] = shape
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-02 13:25:17 +00:00
|
|
|
// If the shape is the child of a group, then update the group
|
|
|
|
// (unless the group is being deleted too)
|
2021-09-02 12:51:39 +00:00
|
|
|
if (parentId && parentId !== pageId) {
|
2021-09-02 13:25:17 +00:00
|
|
|
const group = page.shapes[parentId]
|
2021-09-02 20:13:54 +00:00
|
|
|
if (group !== undefined) {
|
2021-09-02 13:25:17 +00:00
|
|
|
groupsToUpdate.add(page.shapes[parentId] as GroupShape)
|
|
|
|
}
|
2021-09-02 12:51:39 +00:00
|
|
|
}
|
2021-08-17 21:38:37 +00:00
|
|
|
})
|
2021-08-11 12:26:34 +00:00
|
|
|
|
2021-08-29 13:33:43 +00:00
|
|
|
// If binding is undefined, delete the binding
|
2021-08-17 23:11:00 +00:00
|
|
|
Object.keys(page.bindings).forEach((id) => {
|
2021-10-16 18:40:59 +00:00
|
|
|
if (!page.bindings[id]) {
|
|
|
|
delete page.bindings[id]
|
|
|
|
}
|
2021-08-17 21:38:37 +00:00
|
|
|
})
|
2021-08-11 12:26:34 +00:00
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
next.document.pages[pageId] = page
|
2021-08-16 21:52:03 +00:00
|
|
|
|
2021-11-22 14:00:24 +00:00
|
|
|
// Find which shapes have changed
|
|
|
|
// const changedShapes = Object.entries(page.shapes).filter(
|
|
|
|
// ([id, shape]) => prevPage?.shapes[shape.id] !== shape
|
|
|
|
// )
|
|
|
|
|
2021-08-17 21:38:37 +00:00
|
|
|
// Get bindings related to the changed shapes
|
2021-11-22 14:00:24 +00:00
|
|
|
const bindingsToUpdate = TLDR.getRelatedBindings(next, Object.keys(changedShapes), pageId)
|
2021-08-11 12:26:34 +00:00
|
|
|
|
2021-08-17 21:38:37 +00:00
|
|
|
// Update all of the bindings we've just collected
|
|
|
|
bindingsToUpdate.forEach((binding) => {
|
2021-10-16 18:40:59 +00:00
|
|
|
if (!page.bindings[binding.id]) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-08-17 23:11:00 +00:00
|
|
|
const toShape = page.shapes[binding.toId]
|
|
|
|
const fromShape = page.shapes[binding.fromId]
|
2021-09-24 13:27:22 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const toUtils = TLDR.getShapeUtil(toShape)
|
2021-08-11 12:26:34 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const fromUtils = TLDR.getShapeUtil(fromShape)
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-10-16 22:17:41 +00:00
|
|
|
// We only need to update the binding's "from" shape
|
2021-10-27 15:15:01 +00:00
|
|
|
const fromDelta = fromUtils.onBindingChange?.(
|
2021-10-13 13:55:31 +00:00
|
|
|
fromShape,
|
|
|
|
binding,
|
|
|
|
toShape,
|
|
|
|
toUtils.getBounds(toShape),
|
|
|
|
toUtils.getCenter(toShape)
|
|
|
|
)
|
|
|
|
|
|
|
|
if (fromDelta) {
|
|
|
|
const nextShape = {
|
|
|
|
...fromShape,
|
|
|
|
...fromDelta,
|
2021-11-16 16:01:29 +00:00
|
|
|
} as TDShape
|
2021-10-13 13:55:31 +00:00
|
|
|
|
|
|
|
page.shapes[fromShape.id] = nextShape
|
2021-08-17 21:38:37 +00:00
|
|
|
}
|
|
|
|
})
|
2021-09-02 12:51:39 +00:00
|
|
|
|
|
|
|
groupsToUpdate.forEach((group) => {
|
2021-09-03 10:24:50 +00:00
|
|
|
if (!group) throw Error('no group!')
|
2021-09-02 12:51:39 +00:00
|
|
|
const children = group.children.filter((id) => page.shapes[id] !== undefined)
|
|
|
|
|
|
|
|
const commonBounds = Utils.getCommonBounds(
|
|
|
|
children
|
|
|
|
.map((id) => page.shapes[id])
|
|
|
|
.filter(Boolean)
|
2021-09-03 10:15:03 +00:00
|
|
|
.map((shape) => TLDR.getRotatedBounds(shape))
|
2021-09-02 12:51:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
page.shapes[group.id] = {
|
|
|
|
...group,
|
|
|
|
point: [commonBounds.minX, commonBounds.minY],
|
|
|
|
size: [commonBounds.width, commonBounds.height],
|
|
|
|
children,
|
|
|
|
}
|
|
|
|
})
|
2021-08-17 21:38:37 +00:00
|
|
|
}
|
2021-08-16 14:01:03 +00:00
|
|
|
|
|
|
|
// Clean up page state, preventing hovers on deleted shapes
|
|
|
|
|
|
|
|
const nextPageState: TLPageState = {
|
2021-11-18 18:18:30 +00:00
|
|
|
...next.document.pageStates[pageId],
|
2021-08-11 12:26:34 +00:00
|
|
|
}
|
|
|
|
|
2021-09-17 21:29:45 +00:00
|
|
|
if (!nextPageState.brush) {
|
|
|
|
delete nextPageState.brush
|
|
|
|
}
|
|
|
|
|
2021-08-17 23:11:00 +00:00
|
|
|
if (nextPageState.hoveredId && !page.shapes[nextPageState.hoveredId]) {
|
2021-08-16 14:01:03 +00:00
|
|
|
delete nextPageState.hoveredId
|
|
|
|
}
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-08-17 23:11:00 +00:00
|
|
|
if (nextPageState.bindingId && !page.bindings[nextPageState.bindingId]) {
|
2021-08-18 07:19:13 +00:00
|
|
|
console.warn('Could not find the binding binding!', pageId)
|
2021-08-16 14:01:03 +00:00
|
|
|
delete nextPageState.bindingId
|
|
|
|
}
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-08-18 07:19:13 +00:00
|
|
|
if (nextPageState.editingId && !page.shapes[nextPageState.editingId]) {
|
2021-08-16 14:01:03 +00:00
|
|
|
console.warn('Could not find the editing shape!')
|
|
|
|
delete nextPageState.editingId
|
|
|
|
}
|
2021-08-12 13:39:41 +00:00
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
next.document.pageStates[pageId] = nextPageState
|
2021-08-16 21:52:03 +00:00
|
|
|
})
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
const currentPageId = next.appState.currentPageId
|
2021-10-12 14:59:04 +00:00
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
const currentPageState = next.document.pageStates[currentPageId]
|
2021-10-16 18:55:18 +00:00
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
if (next.room && next.room !== prev.room) {
|
|
|
|
const room = { ...next.room, users: { ...next.room.users } }
|
2021-10-16 18:55:18 +00:00
|
|
|
|
|
|
|
// Remove any exited users
|
2021-10-16 20:24:31 +00:00
|
|
|
if (prev.room) {
|
|
|
|
Object.values(prev.room.users)
|
|
|
|
.filter(Boolean)
|
|
|
|
.forEach((user) => {
|
|
|
|
if (room.users[user.id] === undefined) {
|
|
|
|
delete room.users[user.id]
|
|
|
|
}
|
|
|
|
})
|
2021-10-16 18:55:18 +00:00
|
|
|
}
|
2021-10-12 14:59:04 +00:00
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
next.room = room
|
2021-10-16 18:55:18 +00:00
|
|
|
}
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
if (next.room) {
|
|
|
|
next.room.users[next.room.userId] = {
|
|
|
|
...next.room.users[next.room.userId],
|
2021-11-16 16:01:29 +00:00
|
|
|
point: this.currentPoint,
|
2021-10-16 20:24:31 +00:00
|
|
|
selectedIds: currentPageState.selectedIds,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-05 14:13:14 +00:00
|
|
|
// Temporary block on editing pages while in readonly mode.
|
|
|
|
// This is a broad solution but not a very good one: the UX
|
|
|
|
// for interacting with a readOnly document will be more nuanced.
|
|
|
|
if (this.readOnly) {
|
2021-11-18 18:18:30 +00:00
|
|
|
next.document.pages = prev.document.pages
|
2021-11-05 14:13:14 +00:00
|
|
|
}
|
|
|
|
|
2021-11-18 18:18:30 +00:00
|
|
|
return next
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onPatch = (state: TDSnapshot, id?: string) => {
|
2021-11-08 14:21:37 +00:00
|
|
|
this.callbacks.onPatch?.(this, id)
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onCommand = (state: TDSnapshot, id?: string) => {
|
2021-11-08 14:21:37 +00:00
|
|
|
this.clearSelectHistory()
|
|
|
|
this.isDirty = true
|
|
|
|
this.callbacks.onCommand?.(this, id)
|
|
|
|
}
|
|
|
|
|
|
|
|
onReplace = () => {
|
|
|
|
this.clearSelectHistory()
|
|
|
|
this.isDirty = false
|
|
|
|
}
|
|
|
|
|
|
|
|
onUndo = () => {
|
2021-11-16 16:01:29 +00:00
|
|
|
this.rotationInfo.selectedIds = [...this.selectedIds]
|
2021-11-08 14:21:37 +00:00
|
|
|
this.callbacks.onUndo?.(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
onRedo = () => {
|
2021-11-16 16:01:29 +00:00
|
|
|
this.rotationInfo.selectedIds = [...this.selectedIds]
|
2021-11-08 14:21:37 +00:00
|
|
|
this.callbacks.onRedo?.(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
onPersist = () => {
|
2021-11-22 14:00:24 +00:00
|
|
|
this.broadcastPageChanges()
|
2021-11-08 14:21:37 +00:00
|
|
|
}
|
|
|
|
|
2021-11-22 14:00:24 +00:00
|
|
|
private prevSelectedIds = this.selectedIds
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Clear the selection history after each new command, undo or redo.
|
|
|
|
* @param state
|
|
|
|
* @param id
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
protected onStateDidChange = (_state: TDSnapshot, id?: string): void => {
|
2021-11-08 14:21:37 +00:00
|
|
|
this.callbacks.onChange?.(this, id)
|
2021-11-22 14:00:24 +00:00
|
|
|
|
|
|
|
if (this.room && this.selectedIds !== this.prevSelectedIds) {
|
|
|
|
this.callbacks.onChangePresence?.(this, {
|
|
|
|
...this.room.users[this.room.userId],
|
|
|
|
selectedIds: this.selectedIds,
|
|
|
|
})
|
|
|
|
this.prevSelectedIds = this.selectedIds
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ----------- Managing Multiplayer State ----------- */
|
|
|
|
|
|
|
|
private prevShapes = this.page.shapes
|
|
|
|
private prevBindings = this.page.bindings
|
|
|
|
|
|
|
|
private broadcastPageChanges = () => {
|
|
|
|
const visited = new Set<string>()
|
|
|
|
|
|
|
|
const changedShapes: Record<string, TDShape | undefined> = {}
|
|
|
|
const changedBindings: Record<string, TDBinding | undefined> = {}
|
|
|
|
|
|
|
|
this.shapes.forEach((shape) => {
|
|
|
|
visited.add(shape.id)
|
|
|
|
if (this.prevShapes[shape.id] !== shape) {
|
|
|
|
changedShapes[shape.id] = shape
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
Object.keys(this.prevShapes)
|
|
|
|
.filter((id) => !visited.has(id))
|
|
|
|
.forEach((id) => {
|
|
|
|
changedShapes[id] = undefined
|
|
|
|
})
|
|
|
|
|
|
|
|
this.bindings.forEach((binding) => {
|
|
|
|
visited.add(binding.id)
|
|
|
|
if (this.prevBindings[binding.id] !== binding) {
|
|
|
|
changedBindings[binding.id] = binding
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
Object.keys(this.prevShapes)
|
|
|
|
.filter((id) => !visited.has(id))
|
|
|
|
.forEach((id) => {
|
|
|
|
changedBindings[id] = undefined
|
|
|
|
})
|
|
|
|
|
|
|
|
this.callbacks.onChangePage?.(this, changedShapes, changedBindings)
|
|
|
|
|
|
|
|
this.callbacks.onPersist?.(this)
|
|
|
|
this.prevShapes = this.page.shapes
|
|
|
|
this.prevBindings = this.page.bindings
|
2021-09-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
|
2021-11-22 14:00:24 +00:00
|
|
|
/**
|
|
|
|
* Manually patch a set of shapes.
|
|
|
|
* @param shapes An array of shape partials, containing the changes to be made to each shape.
|
|
|
|
* @command
|
|
|
|
*/
|
|
|
|
public replacePageContent = (
|
|
|
|
shapes: Record<string, TDShape>,
|
|
|
|
bindings: Record<string, TDBinding>,
|
|
|
|
pageId = this.currentPageId
|
|
|
|
): this => {
|
|
|
|
this.useStore.setState((current) => {
|
|
|
|
const { hoveredId, editingId, bindingId, selectedIds } = current.document.pageStates[pageId]
|
|
|
|
|
|
|
|
const next = {
|
|
|
|
...current,
|
|
|
|
document: {
|
|
|
|
...current.document,
|
|
|
|
pages: {
|
|
|
|
[pageId]: {
|
|
|
|
...current.document.pages[pageId],
|
|
|
|
shapes,
|
|
|
|
bindings,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
pageStates: {
|
|
|
|
...current.document.pageStates,
|
|
|
|
[pageId]: {
|
|
|
|
...current.document.pageStates[pageId],
|
|
|
|
selectedIds: selectedIds.filter((id) => shapes[id] !== undefined),
|
|
|
|
hoveredId: hoveredId
|
|
|
|
? shapes[hoveredId] === undefined
|
|
|
|
? undefined
|
|
|
|
: hoveredId
|
|
|
|
: undefined,
|
|
|
|
editingId: editingId
|
|
|
|
? shapes[editingId] === undefined
|
|
|
|
? undefined
|
|
|
|
: hoveredId
|
|
|
|
: undefined,
|
|
|
|
bindingId: bindingId
|
|
|
|
? bindings[bindingId] === undefined
|
|
|
|
? undefined
|
|
|
|
: bindingId
|
|
|
|
: undefined,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
this.state.document = next.document
|
|
|
|
this.prevShapes = next.document.pages[this.currentPageId].shapes
|
|
|
|
this.prevBindings = next.document.pages[this.currentPageId].bindings
|
|
|
|
|
|
|
|
return next
|
|
|
|
}, true)
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
2021-11-08 14:21:37 +00:00
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Set the current status.
|
|
|
|
* @param status The new status to set.
|
|
|
|
* @private
|
|
|
|
* @returns
|
|
|
|
*/
|
2021-10-14 12:32:48 +00:00
|
|
|
setStatus(status: string) {
|
2021-09-06 11:07:15 +00:00
|
|
|
return this.patchState(
|
|
|
|
{
|
2021-10-14 12:32:48 +00:00
|
|
|
appState: { status },
|
2021-09-06 11:07:15 +00:00
|
|
|
},
|
|
|
|
`set_status:${status}`
|
|
|
|
)
|
2021-08-16 14:01:03 +00:00
|
|
|
}
|
|
|
|
|
2021-09-22 08:45:09 +00:00
|
|
|
/**
|
|
|
|
* Update the bounding box when the renderer's bounds change.
|
|
|
|
* @param bounds
|
|
|
|
*/
|
|
|
|
updateBounds = (bounds: TLBounds) => {
|
2021-11-16 16:01:29 +00:00
|
|
|
this.rendererBounds = bounds
|
|
|
|
const { point, zoom } = this.pageState.camera
|
|
|
|
this.updateViewport(point, zoom)
|
|
|
|
|
|
|
|
if (!this.readOnly && this.session) {
|
|
|
|
this.session.update()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateViewport = (point: number[], zoom: number) => {
|
|
|
|
const { width, height } = this.rendererBounds
|
|
|
|
const [minX, minY] = Vec.sub(Vec.div([0, 0], zoom), point)
|
|
|
|
const [maxX, maxY] = Vec.sub(Vec.div([width, height], zoom), point)
|
|
|
|
|
|
|
|
this.viewport = {
|
|
|
|
minX,
|
|
|
|
minY,
|
|
|
|
maxX,
|
|
|
|
maxY,
|
|
|
|
height: maxX - minX,
|
|
|
|
width: maxY - minY,
|
2021-10-21 18:54:54 +00:00
|
|
|
}
|
2021-09-22 08:45:09 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
/**
|
|
|
|
* Set or clear the editing id
|
|
|
|
* @param id [string]
|
|
|
|
*/
|
|
|
|
setEditingId = (id?: string) => {
|
2021-11-11 11:37:57 +00:00
|
|
|
if (this.readOnly) return
|
|
|
|
|
2021-11-09 14:26:41 +00:00
|
|
|
this.editingStartTime = Date.now()
|
2021-10-13 13:55:31 +00:00
|
|
|
this.patchState(
|
|
|
|
{
|
|
|
|
document: {
|
|
|
|
pageStates: {
|
|
|
|
[this.currentPageId]: {
|
|
|
|
editingId: id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
`set_editing_id`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set or clear the hovered id
|
|
|
|
* @param id [string]
|
|
|
|
*/
|
|
|
|
setHoveredId = (id?: string) => {
|
|
|
|
this.patchState(
|
|
|
|
{
|
|
|
|
document: {
|
|
|
|
pageStates: {
|
|
|
|
[this.currentPageId]: {
|
|
|
|
hoveredId: id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
`set_hovered_id`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Settings & UI */
|
|
|
|
/* -------------------------------------------------- */
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-10-19 13:29:55 +00:00
|
|
|
/**
|
|
|
|
* Set a setting.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
setSetting = <T extends keyof TDSnapshot['settings'], V extends TDSnapshot['settings'][T]>(
|
2021-10-19 13:29:55 +00:00
|
|
|
name: T,
|
|
|
|
value: V | ((value: V) => V)
|
|
|
|
): this => {
|
|
|
|
if (this.session) return this
|
|
|
|
|
2021-11-08 14:21:37 +00:00
|
|
|
this.patchState(
|
2021-10-19 13:29:55 +00:00
|
|
|
{
|
|
|
|
settings: {
|
2021-11-16 16:01:29 +00:00
|
|
|
[name]: typeof value === 'function' ? value(this.settings[name] as V) : value,
|
2021-10-19 13:29:55 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
`settings:${name}`
|
|
|
|
)
|
2021-11-08 14:21:37 +00:00
|
|
|
this.persist()
|
|
|
|
return this
|
2021-10-19 13:29:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-22 11:28:55 +00:00
|
|
|
/**
|
|
|
|
* Toggle pen mode.
|
|
|
|
*/
|
|
|
|
toggleFocusMode = (): this => {
|
|
|
|
if (this.session) return this
|
2021-11-08 14:21:37 +00:00
|
|
|
this.patchState(
|
2021-09-22 11:28:55 +00:00
|
|
|
{
|
|
|
|
settings: {
|
2021-11-16 16:01:29 +00:00
|
|
|
isFocusMode: !this.settings.isFocusMode,
|
2021-09-22 11:28:55 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
`settings:toggled_focus_mode`
|
|
|
|
)
|
2021-11-08 14:21:37 +00:00
|
|
|
this.persist()
|
|
|
|
return this
|
2021-09-22 11:28:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Toggle pen mode.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
togglePenMode = (): this => {
|
2021-09-03 10:24:50 +00:00
|
|
|
if (this.session) return this
|
2021-11-08 14:21:37 +00:00
|
|
|
this.patchState(
|
2021-08-17 21:38:37 +00:00
|
|
|
{
|
|
|
|
settings: {
|
2021-11-16 16:01:29 +00:00
|
|
|
isPenMode: !this.settings.isPenMode,
|
2021-08-17 21:38:37 +00:00
|
|
|
},
|
2021-08-10 16:12:55 +00:00
|
|
|
},
|
2021-08-17 21:38:37 +00:00
|
|
|
`settings:toggled_pen_mode`
|
|
|
|
)
|
2021-11-08 14:21:37 +00:00
|
|
|
this.persist()
|
|
|
|
return this
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
2021-08-17 21:38:37 +00:00
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Toggle dark mode.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
toggleDarkMode = (): this => {
|
2021-09-03 10:24:50 +00:00
|
|
|
if (this.session) return this
|
2021-08-30 13:04:12 +00:00
|
|
|
this.patchState(
|
2021-11-16 16:01:29 +00:00
|
|
|
{ settings: { isDarkMode: !this.settings.isDarkMode } },
|
2021-08-17 21:38:37 +00:00
|
|
|
`settings:toggled_dark_mode`
|
|
|
|
)
|
2021-08-30 13:04:12 +00:00
|
|
|
this.persist()
|
|
|
|
return this
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
2021-08-23 16:13:10 +00:00
|
|
|
|
2021-09-02 20:13:54 +00:00
|
|
|
/**
|
|
|
|
* Toggle zoom snap.
|
|
|
|
*/
|
|
|
|
toggleZoomSnap = () => {
|
2021-09-03 10:24:50 +00:00
|
|
|
if (this.session) return this
|
2021-09-02 20:13:54 +00:00
|
|
|
this.patchState(
|
2021-11-16 16:01:29 +00:00
|
|
|
{ settings: { isZoomSnap: !this.settings.isZoomSnap } },
|
2021-09-02 20:13:54 +00:00
|
|
|
`settings:toggled_zoom_snap`
|
|
|
|
)
|
|
|
|
this.persist()
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Toggle debug mode.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
toggleDebugMode = () => {
|
2021-09-03 10:24:50 +00:00
|
|
|
if (this.session) return this
|
2021-08-30 13:04:12 +00:00
|
|
|
this.patchState(
|
2021-11-16 16:01:29 +00:00
|
|
|
{ settings: { isDebugMode: !this.settings.isDebugMode } },
|
2021-08-29 13:33:43 +00:00
|
|
|
`settings:toggled_debug`
|
|
|
|
)
|
2021-08-30 13:04:12 +00:00
|
|
|
this.persist()
|
|
|
|
return this
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
2021-08-16 14:01:03 +00:00
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Toggle the style panel.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
toggleStylePanel = (): this => {
|
2021-09-03 10:24:50 +00:00
|
|
|
if (this.session) return this
|
2021-08-30 13:04:12 +00:00
|
|
|
this.patchState(
|
2021-08-29 13:33:43 +00:00
|
|
|
{ appState: { isStyleOpen: !this.appState.isStyleOpen } },
|
|
|
|
'ui:toggled_style_panel'
|
|
|
|
)
|
2021-08-30 13:04:12 +00:00
|
|
|
this.persist()
|
|
|
|
return this
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-26 15:14:10 +00:00
|
|
|
/**
|
|
|
|
* Toggle grids.
|
|
|
|
*/
|
|
|
|
toggleGrid = (): this => {
|
|
|
|
if (this.session) return this
|
|
|
|
this.patchState({ settings: { showGrid: !this.settings.showGrid } }, 'settings:toggled_grid')
|
|
|
|
this.persist()
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Select a tool.
|
2021-09-05 09:51:21 +00:00
|
|
|
* @param tool The tool to select, or "select".
|
2021-09-02 12:51:39 +00:00
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
selectTool = (type: TDToolType): this => {
|
2021-11-11 11:37:57 +00:00
|
|
|
if (this.readOnly || this.session) return this
|
2021-09-11 22:58:22 +00:00
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
const tool = this.tools[type]
|
|
|
|
|
2021-11-11 13:03:13 +00:00
|
|
|
if (tool === this.currentTool) {
|
|
|
|
this.patchState({
|
|
|
|
appState: {
|
|
|
|
isToolLocked: false,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return this
|
|
|
|
}
|
2021-10-13 13:55:31 +00:00
|
|
|
|
2021-10-16 07:33:25 +00:00
|
|
|
this.currentTool.onExit()
|
2021-11-16 16:01:29 +00:00
|
|
|
tool.previous = this.currentTool.type
|
2021-10-13 13:55:31 +00:00
|
|
|
this.currentTool = tool
|
2021-10-16 07:33:25 +00:00
|
|
|
this.currentTool.onEnter()
|
|
|
|
|
2021-08-29 13:33:43 +00:00
|
|
|
return this.patchState(
|
2021-08-17 21:38:37 +00:00
|
|
|
{
|
|
|
|
appState: {
|
2021-10-13 13:55:31 +00:00
|
|
|
activeTool: type,
|
2021-11-11 13:03:13 +00:00
|
|
|
isToolLocked: false,
|
2021-08-17 21:38:37 +00:00
|
|
|
},
|
2021-08-10 16:12:55 +00:00
|
|
|
},
|
2021-10-13 13:55:31 +00:00
|
|
|
`selected_tool:${type}`
|
2021-08-17 21:38:37 +00:00
|
|
|
)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Toggle the tool lock option.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
toggleToolLock = (): this => {
|
2021-09-03 10:24:50 +00:00
|
|
|
if (this.session) return this
|
2021-08-29 13:33:43 +00:00
|
|
|
return this.patchState(
|
2021-08-17 21:38:37 +00:00
|
|
|
{
|
|
|
|
appState: {
|
2021-08-30 10:59:31 +00:00
|
|
|
isToolLocked: !this.appState.isToolLocked,
|
2021-08-17 21:38:37 +00:00
|
|
|
},
|
2021-08-10 16:12:55 +00:00
|
|
|
},
|
2021-08-17 21:38:37 +00:00
|
|
|
`toggled_tool_lock`
|
|
|
|
)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Document */
|
|
|
|
/* -------------------------------------------------- */
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-03 10:52:40 +00:00
|
|
|
/**
|
|
|
|
* Reset the document to a blank state.
|
|
|
|
*/
|
2021-09-02 12:51:39 +00:00
|
|
|
resetDocument = (): this => {
|
2021-09-03 10:24:50 +00:00
|
|
|
if (this.session) return this
|
|
|
|
this.session = undefined
|
2021-10-14 12:51:21 +00:00
|
|
|
this.pasteInfo.offset = [0, 0]
|
2021-10-16 07:33:25 +00:00
|
|
|
this.currentTool = this.tools.select
|
|
|
|
|
2021-09-03 10:24:50 +00:00
|
|
|
this.resetHistory()
|
|
|
|
.clearSelectHistory()
|
2021-11-16 16:01:29 +00:00
|
|
|
.loadDocument(migrate(TldrawApp.defaultDocument, TldrawApp.version))
|
2021-09-03 10:24:50 +00:00
|
|
|
.persist()
|
2021-09-02 12:51:39 +00:00
|
|
|
return this
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-10-09 13:57:44 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param document
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
updateUsers = (users: TDUser[], isOwnUpdate = false) => {
|
2021-10-09 13:57:44 +00:00
|
|
|
this.patchState(
|
|
|
|
{
|
|
|
|
room: {
|
|
|
|
users: Object.fromEntries(users.map((user) => [user.id, user])),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
isOwnUpdate ? 'room:self:update' : 'room:user:update'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
removeUser = (userId: string) => {
|
|
|
|
this.patchState({
|
|
|
|
room: {
|
|
|
|
users: {
|
|
|
|
[userId]: undefined,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-08 23:05:24 +00:00
|
|
|
/**
|
|
|
|
* Merge a new document patch into the current document.
|
|
|
|
* @param document
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
mergeDocument = (document: TDDocument): this => {
|
2021-10-08 23:05:24 +00:00
|
|
|
// If it's a new document, do a full change.
|
|
|
|
if (this.document.id !== document.id) {
|
|
|
|
this.replaceState({
|
|
|
|
...this.state,
|
|
|
|
appState: {
|
|
|
|
...this.appState,
|
|
|
|
currentPageId: Object.keys(document.pages)[0],
|
|
|
|
},
|
2021-11-16 16:01:29 +00:00
|
|
|
document: migrate(document, TldrawApp.version),
|
2021-10-08 23:05:24 +00:00
|
|
|
})
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// Have we deleted any pages? If so, drop everything and change
|
|
|
|
// to the first page. This is an edge case.
|
|
|
|
const currentPageStates = { ...this.document.pageStates }
|
|
|
|
|
|
|
|
// Update the app state's current page id if needed
|
|
|
|
const nextAppState = {
|
|
|
|
...this.appState,
|
|
|
|
currentPageId: document.pages[this.currentPageId]
|
|
|
|
? this.currentPageId
|
|
|
|
: Object.keys(document.pages)[0],
|
|
|
|
pages: Object.values(document.pages).map((page, i) => ({
|
|
|
|
id: page.id,
|
|
|
|
name: page.name,
|
|
|
|
childIndex: page.childIndex || i,
|
|
|
|
})),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset the history (for now)
|
|
|
|
this.resetHistory()
|
|
|
|
|
|
|
|
Object.keys(this.document.pages).forEach((pageId) => {
|
|
|
|
if (!document.pages[pageId]) {
|
|
|
|
if (pageId === this.appState.currentPageId) {
|
|
|
|
this.cancelSession()
|
2021-11-07 13:45:48 +00:00
|
|
|
this.selectNone()
|
2021-10-08 23:05:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
currentPageStates[pageId] = undefined as unknown as TLPageState
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Don't allow the selected ids to be deleted during a session—if
|
|
|
|
// they've been removed, put them back in the client's document.
|
|
|
|
if (this.session) {
|
|
|
|
this.selectedIds
|
|
|
|
.filter((id) => !document.pages[this.currentPageId].shapes[id])
|
|
|
|
.forEach((id) => (document.pages[this.currentPageId].shapes[id] = this.page.shapes[id]))
|
|
|
|
}
|
|
|
|
|
|
|
|
// For other pages, remove any selected ids that were deleted.
|
|
|
|
Object.entries(currentPageStates).forEach(([pageId, pageState]) => {
|
|
|
|
pageState.selectedIds = pageState.selectedIds.filter(
|
|
|
|
(id) => !!document.pages[pageId].shapes[id]
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
// If the user is currently creating a shape (ie drawing), then put that
|
|
|
|
// shape back onto the page for the client.
|
|
|
|
const { editingId } = this.pageState
|
|
|
|
|
|
|
|
if (editingId) {
|
|
|
|
document.pages[this.currentPageId].shapes[editingId] = this.page.shapes[editingId]
|
|
|
|
currentPageStates[this.currentPageId].selectedIds = [editingId]
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.replaceState(
|
|
|
|
{
|
|
|
|
...this.state,
|
|
|
|
appState: nextAppState,
|
|
|
|
document: {
|
2021-11-16 16:01:29 +00:00
|
|
|
...migrate(document, TldrawApp.version),
|
2021-10-08 23:05:24 +00:00
|
|
|
pageStates: currentPageStates,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'merge'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
2021-09-08 09:01:45 +00:00
|
|
|
* Update the current document.
|
|
|
|
* @param document
|
2021-09-02 12:51:39 +00:00
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
updateDocument = (document: TDDocument, reason = 'updated_document'): this => {
|
2021-09-08 10:16:10 +00:00
|
|
|
const prevState = this.state
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-09-08 10:16:10 +00:00
|
|
|
const nextState = { ...prevState, document: { ...prevState.document } }
|
2021-09-08 09:01:45 +00:00
|
|
|
|
2021-09-08 10:16:10 +00:00
|
|
|
if (!document.pages[this.currentPageId]) {
|
|
|
|
nextState.appState = {
|
|
|
|
...prevState.appState,
|
|
|
|
currentPageId: Object.keys(document.pages)[0],
|
|
|
|
}
|
|
|
|
}
|
2021-09-08 09:01:45 +00:00
|
|
|
|
2021-09-08 10:16:10 +00:00
|
|
|
let i = 1
|
2021-09-08 09:01:45 +00:00
|
|
|
|
2021-09-08 10:16:10 +00:00
|
|
|
for (const nextPage of Object.values(document.pages)) {
|
|
|
|
if (nextPage !== prevState.document.pages[nextPage.id]) {
|
|
|
|
nextState.document.pages[nextPage.id] = nextPage
|
2021-09-08 09:01:45 +00:00
|
|
|
|
2021-09-08 10:16:10 +00:00
|
|
|
if (!nextPage.name) {
|
|
|
|
nextState.document.pages[nextPage.id].name = `Page ${i + 1}`
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-09-08 09:01:45 +00:00
|
|
|
|
2021-09-08 10:16:10 +00:00
|
|
|
for (const nextPageState of Object.values(document.pageStates)) {
|
|
|
|
if (nextPageState !== prevState.document.pageStates[nextPageState.id]) {
|
|
|
|
nextState.document.pageStates[nextPageState.id] = nextPageState
|
2021-09-08 09:01:45 +00:00
|
|
|
|
2021-09-08 10:16:10 +00:00
|
|
|
const nextPage = document.pages[nextPageState.id]
|
|
|
|
const keysToCheck = ['bindingId', 'editingId', 'hoveredId', 'pointedId'] as const
|
|
|
|
|
|
|
|
for (const key of keysToCheck) {
|
|
|
|
if (!nextPage.shapes[key]) {
|
|
|
|
nextPageState[key] = undefined
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nextPageState.selectedIds = nextPageState.selectedIds.filter(
|
|
|
|
(id) => !!document.pages[nextPage.id].shapes[id]
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-06 16:49:53 +00:00
|
|
|
nextState.document = migrate(nextState.document, nextState.document.version || 0)
|
|
|
|
|
2021-09-08 10:16:10 +00:00
|
|
|
return this.replaceState(nextState, `${reason}:${document.id}`)
|
2021-09-08 09:01:45 +00:00
|
|
|
}
|
|
|
|
|
2021-10-16 20:24:31 +00:00
|
|
|
/**
|
|
|
|
* Load a fresh room into the state.
|
|
|
|
* @param roomId
|
|
|
|
*/
|
2021-11-16 19:41:16 +00:00
|
|
|
loadRoom = (roomId: string): this => {
|
2021-10-16 20:24:31 +00:00
|
|
|
this.patchState({
|
|
|
|
room: {
|
|
|
|
id: roomId,
|
|
|
|
userId: uuid,
|
|
|
|
users: {
|
|
|
|
[uuid]: {
|
|
|
|
id: uuid,
|
2021-11-06 11:16:30 +00:00
|
|
|
color: USER_COLORS[Math.floor(Math.random() * USER_COLORS.length)],
|
2021-10-16 20:24:31 +00:00
|
|
|
point: [100, 100],
|
|
|
|
selectedIds: [],
|
|
|
|
activeShapes: [],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2021-11-16 19:41:16 +00:00
|
|
|
return this
|
2021-10-16 20:24:31 +00:00
|
|
|
}
|
|
|
|
|
2021-09-08 09:01:45 +00:00
|
|
|
/**
|
|
|
|
* Load a new document.
|
|
|
|
* @param document The document to load
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
loadDocument = (document: TDDocument): this => {
|
2021-11-07 13:45:48 +00:00
|
|
|
this.selectNone()
|
2021-09-08 09:01:45 +00:00
|
|
|
this.resetHistory()
|
|
|
|
this.clearSelectHistory()
|
|
|
|
this.session = undefined
|
2021-11-16 16:01:29 +00:00
|
|
|
|
2021-11-08 14:21:37 +00:00
|
|
|
this.replaceState(
|
2021-09-08 11:09:03 +00:00
|
|
|
{
|
2021-11-16 16:01:29 +00:00
|
|
|
...TldrawApp.defaultState,
|
|
|
|
document: migrate(document, TldrawApp.version),
|
2021-09-08 11:09:03 +00:00
|
|
|
appState: {
|
2021-11-16 16:01:29 +00:00
|
|
|
...TldrawApp.defaultState.appState,
|
2021-09-08 11:09:03 +00:00
|
|
|
currentPageId: Object.keys(document.pages)[0],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'loaded_document'
|
|
|
|
)
|
2021-11-08 14:21:37 +00:00
|
|
|
return this
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-05 14:13:14 +00:00
|
|
|
// Should we move this to the app layer? onSave, onSaveAs, etc?
|
|
|
|
|
2021-09-05 09:51:21 +00:00
|
|
|
/**
|
|
|
|
* Create a new project.
|
|
|
|
*/
|
2021-09-02 12:51:39 +00:00
|
|
|
newProject = () => {
|
2021-11-05 14:13:14 +00:00
|
|
|
if (!this.isLocal) return
|
|
|
|
this.fileSystemHandle = null
|
2021-09-14 11:17:49 +00:00
|
|
|
this.resetDocument()
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-05 09:51:21 +00:00
|
|
|
/**
|
|
|
|
* Save the current project.
|
|
|
|
*/
|
2021-11-05 14:13:14 +00:00
|
|
|
saveProject = async () => {
|
|
|
|
if (this.readOnly) return
|
|
|
|
try {
|
|
|
|
const fileHandle = await saveToFileSystem(this.document, this.fileSystemHandle)
|
|
|
|
this.fileSystemHandle = fileHandle
|
|
|
|
this.persist()
|
|
|
|
this.isDirty = false
|
|
|
|
} catch (e: any) {
|
|
|
|
// Likely cancelled
|
|
|
|
console.error(e.message)
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save the current project as a new file.
|
|
|
|
*/
|
|
|
|
saveProjectAs = async () => {
|
|
|
|
try {
|
|
|
|
const fileHandle = await saveToFileSystem(this.document, null)
|
|
|
|
this.fileSystemHandle = fileHandle
|
|
|
|
this.persist()
|
|
|
|
this.isDirty = false
|
|
|
|
} catch (e: any) {
|
|
|
|
// Likely cancelled
|
|
|
|
console.error(e.message)
|
|
|
|
}
|
|
|
|
return this
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-05 09:51:21 +00:00
|
|
|
/**
|
|
|
|
* Load a project from the filesystem.
|
|
|
|
* @todo
|
|
|
|
*/
|
2021-11-05 14:13:14 +00:00
|
|
|
openProject = async () => {
|
|
|
|
if (!this.isLocal) return
|
|
|
|
|
|
|
|
try {
|
|
|
|
const result = await openFromFileSystem()
|
|
|
|
if (!result) {
|
|
|
|
throw Error()
|
|
|
|
}
|
|
|
|
|
|
|
|
const { fileHandle, document } = result
|
|
|
|
this.loadDocument(document)
|
|
|
|
this.fileSystemHandle = fileHandle
|
|
|
|
this.zoomToFit()
|
|
|
|
this.persist()
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e)
|
|
|
|
} finally {
|
|
|
|
this.persist()
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-05 09:51:21 +00:00
|
|
|
/**
|
|
|
|
* Sign out of the current account.
|
|
|
|
* Should move to the www layer.
|
|
|
|
* @todo
|
|
|
|
*/
|
2021-09-02 12:51:39 +00:00
|
|
|
signOut = () => {
|
2021-11-05 14:13:14 +00:00
|
|
|
// todo
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
2021-08-29 13:33:43 +00:00
|
|
|
/* -------------------- Getters --------------------- */
|
2021-08-16 14:01:03 +00:00
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Get the current app state.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
getAppState = (): TDSnapshot['appState'] => {
|
2021-09-02 12:51:39 +00:00
|
|
|
return this.appState
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Get a page.
|
|
|
|
* @param pageId (optional) The page's id.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
getPage = (pageId = this.currentPageId): TDPage => {
|
2021-08-29 13:33:43 +00:00
|
|
|
return TLDR.getPage(this.state, pageId || this.currentPageId)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Get the shapes (as an array) from a given page.
|
|
|
|
* @param pageId (optional) The page's id.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
getShapes = (pageId = this.currentPageId): TDShape[] => {
|
2021-08-29 13:33:43 +00:00
|
|
|
return TLDR.getShapes(this.state, pageId || this.currentPageId)
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Get the bindings from a given page.
|
|
|
|
* @param pageId (optional) The page's id.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
getBindings = (pageId = this.currentPageId): TDBinding[] => {
|
2021-08-29 13:33:43 +00:00
|
|
|
return TLDR.getBindings(this.state, pageId || this.currentPageId)
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Get a shape from a given page.
|
|
|
|
* @param id The shape's id.
|
|
|
|
* @param pageId (optional) The page's id.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
getShape = <T extends TDShape = TDShape>(id: string, pageId = this.currentPageId): T => {
|
2021-09-02 12:51:39 +00:00
|
|
|
return TLDR.getShape<T>(this.state, id, pageId)
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-06 11:07:15 +00:00
|
|
|
/**
|
|
|
|
* Get the bounds of a shape on a given page.
|
|
|
|
* @param id The shape's id.
|
|
|
|
* @param pageId (optional) The page's id.
|
|
|
|
*/
|
|
|
|
getShapeBounds = (id: string, pageId = this.currentPageId): TLBounds => {
|
|
|
|
return TLDR.getBounds(this.getShape(id, pageId))
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Get a binding from a given page.
|
|
|
|
* @param id The binding's id.
|
|
|
|
* @param pageId (optional) The page's id.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
getBinding = (id: string, pageId = this.currentPageId): TDBinding => {
|
2021-09-02 12:51:39 +00:00
|
|
|
return TLDR.getBinding(this.state, id, pageId)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the page state for a given page.
|
|
|
|
* @param pageId (optional) The page's id.
|
|
|
|
*/
|
|
|
|
getPageState = (pageId = this.currentPageId): TLPageState => {
|
|
|
|
return TLDR.getPageState(this.state, pageId || this.currentPageId)
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Turn a screen point into a point on the page.
|
|
|
|
* @param point The screen point
|
|
|
|
* @param pageId (optional) The page to use
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
getPagePoint = (point: number[], pageId = this.currentPageId): number[] => {
|
|
|
|
const { camera } = this.getPageState(pageId)
|
|
|
|
return Vec.sub(Vec.div(point, camera.zoom), camera.point)
|
|
|
|
}
|
|
|
|
|
2021-09-06 12:14:43 +00:00
|
|
|
/**
|
|
|
|
* Get the current undo/redo stack.
|
|
|
|
*/
|
|
|
|
get history() {
|
2021-09-06 12:50:15 +00:00
|
|
|
return this.stack.slice(0, this.pointer + 1)
|
2021-09-06 12:14:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-06 12:43:56 +00:00
|
|
|
/**
|
|
|
|
* Replace the current history stack.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
set history(commands: TldrawCommand[]) {
|
2021-09-06 12:43:56 +00:00
|
|
|
this.replaceHistory(commands)
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* The current document.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
get document(): TDDocument {
|
2021-09-02 12:51:39 +00:00
|
|
|
return this.state.document
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* The current app state.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
get settings(): TDSnapshot['settings'] {
|
|
|
|
return this.state.settings
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The current app state.
|
|
|
|
*/
|
|
|
|
get appState(): TDSnapshot['appState'] {
|
2021-08-29 13:33:43 +00:00
|
|
|
return this.state.appState
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* The current page id.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
get currentPageId(): string {
|
|
|
|
return this.state.appState.currentPageId
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* The current page.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
get page(): TDPage {
|
2021-09-02 12:51:39 +00:00
|
|
|
return this.state.document.pages[this.currentPageId]
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* The current page's shapes (as an array).
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
get shapes(): TDShape[] {
|
2021-09-02 12:51:39 +00:00
|
|
|
return Object.values(this.page.shapes)
|
2021-08-16 07:49:31 +00:00
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* The current page's bindings.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
get bindings(): TDBinding[] {
|
2021-09-02 12:51:39 +00:00
|
|
|
return Object.values(this.page.bindings)
|
2021-08-16 07:49:31 +00:00
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* The current page's state.
|
|
|
|
*/
|
|
|
|
get pageState(): TLPageState {
|
|
|
|
return this.state.document.pageStates[this.currentPageId]
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* The page's current selected ids.
|
|
|
|
*/
|
|
|
|
get selectedIds(): string[] {
|
|
|
|
return this.pageState.selectedIds
|
2021-08-16 07:49:31 +00:00
|
|
|
}
|
|
|
|
|
2021-08-29 13:33:43 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Pages */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
2021-09-04 21:40:37 +00:00
|
|
|
* Create a new page.
|
2021-09-02 12:51:39 +00:00
|
|
|
* @param pageId (optional) The new page's id.
|
|
|
|
*/
|
|
|
|
createPage = (id?: string): this => {
|
2021-11-11 11:37:57 +00:00
|
|
|
if (this.readOnly) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
const { width, height } = this.rendererBounds
|
|
|
|
return this.setState(Commands.createPage(this, [-width / 2, -height / 2], id))
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Change the current page.
|
|
|
|
* @param pageId The new current page's id.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
changePage = (pageId: string): this => {
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.changePage(this, pageId))
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Rename a page.
|
|
|
|
* @param pageId The id of the page to rename.
|
|
|
|
* @param name The page's new name
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
renamePage = (pageId: string, name: string): this => {
|
2021-11-11 11:37:57 +00:00
|
|
|
if (this.readOnly) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.renamePage(this, pageId, name))
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Duplicate a page.
|
|
|
|
* @param pageId The id of the page to duplicate.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
duplicatePage = (pageId: string): this => {
|
2021-11-05 14:13:14 +00:00
|
|
|
if (this.readOnly) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.duplicatePage(this, pageId))
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Delete a page.
|
|
|
|
* @param pageId The id of the page to delete.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
deletePage = (pageId?: string): this => {
|
2021-11-05 14:13:14 +00:00
|
|
|
if (this.readOnly) return this
|
2021-08-29 13:33:43 +00:00
|
|
|
if (Object.values(this.document.pages).length <= 1) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.deletePage(this, pageId ? pageId : this.currentPageId))
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Clipboard */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Copy one or more shapes to the clipboard.
|
|
|
|
* @param ids The ids of the shapes to copy.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
copy = (ids = this.selectedIds): this => {
|
2021-09-24 12:47:11 +00:00
|
|
|
const copyingShapeIds = ids.flatMap((id) =>
|
|
|
|
TLDR.getDocumentBranch(this.state, id, this.currentPageId)
|
|
|
|
)
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-24 12:47:11 +00:00
|
|
|
const copyingShapes = copyingShapeIds.map((id) =>
|
|
|
|
Utils.deepClone(this.getShape(id, this.currentPageId))
|
|
|
|
)
|
|
|
|
|
|
|
|
if (copyingShapes.length === 0) return this
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const copyingBindings: TDBinding[] = Object.values(this.page.bindings).filter(
|
2021-09-24 12:47:11 +00:00
|
|
|
(binding) =>
|
|
|
|
copyingShapeIds.includes(binding.fromId) && copyingShapeIds.includes(binding.toId)
|
|
|
|
)
|
2021-09-21 15:47:04 +00:00
|
|
|
|
2021-09-24 12:47:11 +00:00
|
|
|
this.clipboard = {
|
|
|
|
shapes: copyingShapes,
|
|
|
|
bindings: copyingBindings,
|
|
|
|
}
|
2021-09-21 15:47:04 +00:00
|
|
|
|
|
|
|
try {
|
2021-09-24 12:47:11 +00:00
|
|
|
const text = JSON.stringify({
|
|
|
|
type: 'tldr/clipboard',
|
|
|
|
shapes: copyingShapes,
|
|
|
|
bindings: copyingBindings,
|
|
|
|
})
|
2021-09-21 15:47:04 +00:00
|
|
|
|
|
|
|
navigator.clipboard.writeText(text).then(
|
|
|
|
() => {
|
|
|
|
// success
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
// failure
|
2021-09-03 11:07:34 +00:00
|
|
|
}
|
2021-09-21 15:47:04 +00:00
|
|
|
)
|
|
|
|
} catch (e) {
|
|
|
|
// Browser does not support copying to clipboard
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pasteInfo.offset = [0, 0]
|
|
|
|
this.pasteInfo.center = [0, 0]
|
2021-08-29 13:33:43 +00:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-11-07 13:45:48 +00:00
|
|
|
/**
|
|
|
|
* Cut (copy and delete) one or more shapes to the clipboard.
|
|
|
|
* @param ids The ids of the shapes to cut.
|
|
|
|
*/
|
|
|
|
cut = (ids = this.selectedIds): this => {
|
|
|
|
this.copy(ids)
|
|
|
|
this.delete(ids)
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/**
|
|
|
|
* Paste shapes (or text) from clipboard to a certain point.
|
|
|
|
* @param point
|
|
|
|
*/
|
2021-09-21 15:47:04 +00:00
|
|
|
paste = (point?: number[]) => {
|
2021-11-05 14:13:14 +00:00
|
|
|
if (this.readOnly) return
|
2021-11-16 16:01:29 +00:00
|
|
|
const pasteInCurrentPage = (shapes: TDShape[], bindings: TDBinding[]) => {
|
2021-09-24 12:47:11 +00:00
|
|
|
const idsMap: Record<string, string> = {}
|
|
|
|
|
|
|
|
shapes.forEach((shape) => (idsMap[shape.id] = Utils.uniqueId()))
|
|
|
|
|
|
|
|
bindings.forEach((binding) => (idsMap[binding.id] = Utils.uniqueId()))
|
|
|
|
|
|
|
|
let startIndex = TLDR.getTopChildIndex(this.state, this.currentPageId)
|
|
|
|
|
|
|
|
const shapesToPaste = shapes
|
|
|
|
.sort((a, b) => a.childIndex - b.childIndex)
|
|
|
|
.map((shape) => {
|
|
|
|
const parentShapeId = idsMap[shape.parentId]
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-24 12:47:11 +00:00
|
|
|
const copy = {
|
|
|
|
...shape,
|
|
|
|
id: idsMap[shape.id],
|
|
|
|
parentId: parentShapeId || this.currentPageId,
|
|
|
|
}
|
|
|
|
|
2021-10-22 13:28:12 +00:00
|
|
|
if (shape.children) {
|
|
|
|
copy.children = shape.children.map((id) => idsMap[id])
|
|
|
|
}
|
|
|
|
|
2021-09-24 12:47:11 +00:00
|
|
|
if (!parentShapeId) {
|
|
|
|
copy.childIndex = startIndex
|
|
|
|
startIndex++
|
|
|
|
}
|
|
|
|
|
|
|
|
if (copy.handles) {
|
|
|
|
Object.values(copy.handles).forEach((handle) => {
|
|
|
|
if (handle.bindingId) {
|
|
|
|
handle.bindingId = idsMap[handle.bindingId]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return copy
|
|
|
|
})
|
|
|
|
|
|
|
|
const bindingsToPaste = bindings.map((binding) => ({
|
|
|
|
...binding,
|
|
|
|
id: idsMap[binding.id],
|
|
|
|
toId: idsMap[binding.toId],
|
|
|
|
fromId: idsMap[binding.fromId],
|
2021-09-21 15:47:04 +00:00
|
|
|
}))
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-21 15:47:04 +00:00
|
|
|
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-11-26 15:14:10 +00:00
|
|
|
let center = Vec.toFixed(this.getPagePoint(point || this.centerPoint))
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-21 15:47:04 +00:00
|
|
|
if (
|
|
|
|
Vec.dist(center, this.pasteInfo.center) < 2 ||
|
2021-11-26 15:14:10 +00:00
|
|
|
Vec.dist(center, Vec.toFixed(Utils.getBoundsCenter(commonBounds))) < 2
|
2021-09-21 15:47:04 +00:00
|
|
|
) {
|
2021-10-14 12:51:21 +00:00
|
|
|
center = Vec.add(center, this.pasteInfo.offset)
|
2021-11-26 15:14:10 +00:00
|
|
|
this.pasteInfo.offset = Vec.add(this.pasteInfo.offset, [GRID_SIZE, GRID_SIZE])
|
2021-09-21 15:47:04 +00:00
|
|
|
} else {
|
|
|
|
this.pasteInfo.center = center
|
|
|
|
this.pasteInfo.offset = [0, 0]
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-21 15:47:04 +00:00
|
|
|
const centeredBounds = Utils.centerBounds(commonBounds, center)
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-21 15:47:04 +00:00
|
|
|
const delta = Vec.sub(
|
|
|
|
Utils.getBoundsCenter(centeredBounds),
|
|
|
|
Utils.getBoundsCenter(commonBounds)
|
|
|
|
)
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-24 12:47:11 +00:00
|
|
|
this.create(
|
|
|
|
shapesToPaste.map((shape) =>
|
2021-11-16 16:01:29 +00:00
|
|
|
TLDR.getShapeUtil(shape.type).create({
|
2021-09-24 12:47:11 +00:00
|
|
|
...shape,
|
2021-11-26 15:14:10 +00:00
|
|
|
point: Vec.toFixed(Vec.add(shape.point, delta)),
|
2021-09-24 12:47:11 +00:00
|
|
|
parentId: shape.parentId || this.currentPageId,
|
|
|
|
})
|
|
|
|
),
|
|
|
|
bindingsToPaste
|
2021-09-21 15:47:04 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
try {
|
2021-09-22 15:00:20 +00:00
|
|
|
if (!('clipboard' in navigator && navigator.clipboard.readText)) {
|
|
|
|
throw Error('This browser does not support the clipboard API.')
|
|
|
|
}
|
|
|
|
|
2021-09-21 15:47:04 +00:00
|
|
|
navigator.clipboard.readText().then((result) => {
|
|
|
|
try {
|
2021-11-16 16:01:29 +00:00
|
|
|
const data: { type: string; shapes: TDShape[]; bindings: TDBinding[] } =
|
2021-09-24 12:47:11 +00:00
|
|
|
JSON.parse(result)
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-21 15:47:04 +00:00
|
|
|
if (data.type !== 'tldr/clipboard') {
|
2021-11-16 16:01:29 +00:00
|
|
|
throw Error('The pasted string was not from the Tldraw clipboard.')
|
2021-09-21 15:47:04 +00:00
|
|
|
}
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-24 12:47:11 +00:00
|
|
|
pasteInCurrentPage(data.shapes, data.bindings)
|
2021-09-21 15:47:04 +00:00
|
|
|
} catch (e) {
|
2021-10-13 13:55:31 +00:00
|
|
|
console.warn(e)
|
2021-09-22 15:00:20 +00:00
|
|
|
|
2021-09-21 15:47:04 +00:00
|
|
|
const shapeId = Utils.uniqueId()
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-21 15:47:04 +00:00
|
|
|
this.createShapes({
|
|
|
|
id: shapeId,
|
2021-11-16 16:01:29 +00:00
|
|
|
type: TDShapeType.Text,
|
2021-09-21 15:47:04 +00:00
|
|
|
parentId: this.appState.currentPageId,
|
2021-11-16 16:01:29 +00:00
|
|
|
text: TLDR.normalizeText(result),
|
2021-09-22 08:45:09 +00:00
|
|
|
point: this.getPagePoint(this.centerPoint, this.currentPageId),
|
2021-09-21 15:47:04 +00:00
|
|
|
style: { ...this.appState.currentStyle },
|
|
|
|
})
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-09-21 15:47:04 +00:00
|
|
|
this.select(shapeId)
|
|
|
|
}
|
|
|
|
})
|
2021-09-24 12:47:11 +00:00
|
|
|
} catch (e) {
|
2021-09-21 15:47:04 +00:00
|
|
|
// Navigator does not support clipboard. Note that this fallback will
|
|
|
|
// not support pasting from one document to another.
|
|
|
|
if (this.clipboard) {
|
2021-09-24 12:47:11 +00:00
|
|
|
pasteInCurrentPage(this.clipboard.shapes, this.clipboard.bindings)
|
2021-09-21 15:47:04 +00:00
|
|
|
}
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-09-01 08:57:46 +00:00
|
|
|
/**
|
|
|
|
* Copy one or more shapes as SVG.
|
|
|
|
* @param ids The ids of the shapes to copy.
|
|
|
|
* @param pageId The page from which to copy the shapes.
|
|
|
|
* @returns A string containing the JSON.
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
|
2021-11-18 11:39:00 +00:00
|
|
|
if (ids.length === 0) ids = Object.keys(this.page.shapes)
|
2021-11-22 16:53:24 +00:00
|
|
|
if (ids.length === 0) return
|
2021-11-18 11:39:00 +00:00
|
|
|
|
2021-09-23 09:48:08 +00:00
|
|
|
const shapes = ids.map((id) => this.getShape(id, pageId))
|
2021-11-22 16:15:51 +00:00
|
|
|
const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
|
|
|
|
const padding = 16
|
2021-09-23 09:48:08 +00:00
|
|
|
|
2021-11-22 16:15:51 +00:00
|
|
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
|
|
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
|
|
|
|
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
2021-09-19 17:20:23 +00:00
|
|
|
|
2021-11-22 16:15:51 +00:00
|
|
|
style.textContent = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');`
|
|
|
|
defs.appendChild(style)
|
|
|
|
svg.appendChild(defs)
|
2021-09-19 17:20:23 +00:00
|
|
|
|
2021-11-22 16:15:51 +00:00
|
|
|
function getSvgElementForShape(shape: TDShape) {
|
|
|
|
const util = TLDR.getShapeUtil(shape)
|
|
|
|
const element = util.getSvgElement(shape)
|
|
|
|
const bounds = util.getBounds(shape)
|
2021-09-23 09:48:08 +00:00
|
|
|
|
2021-11-22 16:15:51 +00:00
|
|
|
if (!element) return
|
2021-09-23 09:48:08 +00:00
|
|
|
|
|
|
|
element.setAttribute(
|
|
|
|
'transform',
|
2021-11-22 16:15:51 +00:00
|
|
|
`translate(${padding + shape.point[0] - commonBounds.minX}, ${
|
|
|
|
padding + shape.point[1] - commonBounds.minY
|
|
|
|
}) rotate(${((shape.rotation || 0) * 180) / Math.PI}, ${bounds.width / 2}, ${
|
|
|
|
bounds.height / 2
|
|
|
|
})`
|
2021-09-23 09:48:08 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return element
|
|
|
|
}
|
|
|
|
|
|
|
|
shapes.forEach((shape) => {
|
|
|
|
if (shape.children?.length) {
|
|
|
|
// Create a group <g> element for shape
|
|
|
|
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
|
|
|
|
|
|
|
// Get the shape's children as elements
|
|
|
|
shape.children
|
|
|
|
.map((childId) => this.getShape(childId, pageId))
|
|
|
|
.map(getSvgElementForShape)
|
|
|
|
.filter(Boolean)
|
|
|
|
.forEach((element) => g.appendChild(element!))
|
|
|
|
|
|
|
|
// Add the group element to the SVG
|
|
|
|
svg.appendChild(g)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const element = getSvgElementForShape(shape)
|
|
|
|
|
|
|
|
if (element) {
|
|
|
|
svg.appendChild(element)
|
2021-09-01 08:57:46 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Resize the element to the bounding box
|
|
|
|
svg.setAttribute(
|
|
|
|
'viewBox',
|
2021-11-22 16:15:51 +00:00
|
|
|
[0, 0, commonBounds.width + padding * 2, commonBounds.height + padding * 2].join(' ')
|
2021-09-01 08:57:46 +00:00
|
|
|
)
|
|
|
|
|
2021-11-22 16:15:51 +00:00
|
|
|
svg.setAttribute('width', String(commonBounds.width))
|
|
|
|
svg.setAttribute('height', String(commonBounds.height))
|
2021-09-01 08:57:46 +00:00
|
|
|
|
|
|
|
const s = new XMLSerializer()
|
|
|
|
|
|
|
|
const svgString = s
|
|
|
|
.serializeToString(svg)
|
|
|
|
.replaceAll(' ', '')
|
|
|
|
.replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1')
|
|
|
|
|
|
|
|
TLDR.copyStringToClipboard(svgString)
|
|
|
|
|
|
|
|
return svgString
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:57:46 +00:00
|
|
|
/**
|
2021-09-04 21:40:37 +00:00
|
|
|
* Copy one or more shapes as JSON.
|
2021-09-01 08:57:46 +00:00
|
|
|
* @param ids The ids of the shapes to copy.
|
|
|
|
* @param pageId The page from which to copy the shapes.
|
|
|
|
* @returns A string containing the JSON.
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
copyJson = (ids = this.selectedIds, pageId = this.currentPageId) => {
|
2021-11-18 11:39:00 +00:00
|
|
|
if (ids.length === 0) ids = Object.keys(this.page.shapes)
|
2021-11-22 16:53:24 +00:00
|
|
|
if (ids.length === 0) return
|
|
|
|
|
2021-09-01 08:57:46 +00:00
|
|
|
const shapes = ids.map((id) => this.getShape(id, pageId))
|
|
|
|
const json = JSON.stringify(shapes, null, 2)
|
|
|
|
TLDR.copyStringToClipboard(json)
|
|
|
|
return json
|
2021-08-29 13:33:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Camera */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the camera to a specific point and zoom.
|
|
|
|
* @param point The camera point (top left of the viewport).
|
|
|
|
* @param zoom The zoom level.
|
|
|
|
* @param reason Why did the camera change?
|
|
|
|
*/
|
|
|
|
setCamera = (point: number[], zoom: number, reason: string): this => {
|
2021-11-16 16:01:29 +00:00
|
|
|
this.updateViewport(point, zoom)
|
2021-10-21 18:54:54 +00:00
|
|
|
this.patchState(
|
2021-09-02 12:51:39 +00:00
|
|
|
{
|
|
|
|
document: {
|
|
|
|
pageStates: {
|
|
|
|
[this.currentPageId]: { camera: { point, zoom } },
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
reason
|
|
|
|
)
|
2021-10-21 18:54:54 +00:00
|
|
|
|
|
|
|
return this
|
2021-09-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reset the camera to the default position
|
|
|
|
*/
|
|
|
|
resetCamera = (): this => {
|
2021-09-22 08:45:09 +00:00
|
|
|
return this.setCamera(this.centerPoint, 1, `reset_camera`)
|
2021-09-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pan the camera
|
|
|
|
* @param delta
|
|
|
|
*/
|
|
|
|
pan = (delta: number[]): this => {
|
|
|
|
const { camera } = this.pageState
|
2021-11-26 15:14:10 +00:00
|
|
|
return this.setCamera(Vec.toFixed(Vec.sub(camera.point, delta)), camera.zoom, `panned`)
|
2021-09-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pinch to a new zoom level, possibly together with a pan.
|
|
|
|
* @param point The current point under the cursor.
|
|
|
|
* @param delta The movement delta.
|
|
|
|
* @param zoomDelta The zoom detal
|
|
|
|
*/
|
2021-10-06 11:55:09 +00:00
|
|
|
pinchZoom = (point: number[], delta: number[], zoom: number): this => {
|
2021-09-02 12:51:39 +00:00
|
|
|
const { camera } = this.pageState
|
2021-09-09 12:32:08 +00:00
|
|
|
const nextPoint = Vec.sub(camera.point, Vec.div(delta, camera.zoom))
|
2021-10-06 11:55:09 +00:00
|
|
|
const nextZoom = zoom
|
2021-09-02 12:51:39 +00:00
|
|
|
const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
|
|
|
|
const p1 = Vec.sub(Vec.div(point, nextZoom), nextPoint)
|
2021-11-26 15:14:10 +00:00
|
|
|
return this.setCamera(
|
|
|
|
Vec.toFixed(Vec.add(nextPoint, Vec.sub(p1, p0))),
|
|
|
|
nextZoom,
|
|
|
|
`pinch_zoomed`
|
|
|
|
)
|
2021-09-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom to a new zoom level, keeping the point under the cursor in the same position
|
|
|
|
* @param next The new zoom level.
|
2021-09-05 09:51:21 +00:00
|
|
|
* @param center The point to zoom towards (defaults to screen center).
|
2021-09-02 12:51:39 +00:00
|
|
|
*/
|
2021-09-22 08:45:09 +00:00
|
|
|
zoomTo = (next: number, center = this.centerPoint): this => {
|
2021-09-02 12:51:39 +00:00
|
|
|
const { zoom, point } = this.pageState.camera
|
|
|
|
const p0 = Vec.sub(Vec.div(center, zoom), point)
|
|
|
|
const p1 = Vec.sub(Vec.div(center, next), point)
|
2021-11-26 15:14:10 +00:00
|
|
|
return this.setCamera(Vec.toFixed(Vec.add(point, Vec.sub(p1, p0))), next, `zoomed_camera`)
|
2021-09-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom out by 25%
|
|
|
|
*/
|
|
|
|
zoomIn = (): this => {
|
|
|
|
const i = Math.round((this.pageState.camera.zoom * 100) / 25)
|
|
|
|
const nextZoom = TLDR.getCameraZoom((i + 1) * 0.25)
|
|
|
|
return this.zoomTo(nextZoom)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom in by 25%.
|
|
|
|
*/
|
|
|
|
zoomOut = (): this => {
|
|
|
|
const i = Math.round((this.pageState.camera.zoom * 100) / 25)
|
|
|
|
const nextZoom = TLDR.getCameraZoom((i - 1) * 0.25)
|
|
|
|
return this.zoomTo(nextZoom)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom to fit the page's shapes.
|
|
|
|
*/
|
|
|
|
zoomToFit = (): this => {
|
2021-11-16 16:01:29 +00:00
|
|
|
const shapes = this.shapes
|
2021-09-02 12:51:39 +00:00
|
|
|
|
|
|
|
if (shapes.length === 0) return this
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const { rendererBounds } = this
|
|
|
|
|
|
|
|
const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds))
|
2021-09-02 12:51:39 +00:00
|
|
|
|
2021-10-17 08:47:41 +00:00
|
|
|
let zoom = TLDR.getCameraZoom(
|
2021-10-18 13:30:42 +00:00
|
|
|
Math.min(
|
2021-11-16 16:01:29 +00:00
|
|
|
(rendererBounds.width - FIT_TO_SCREEN_PADDING) / commonBounds.width,
|
|
|
|
(rendererBounds.height - FIT_TO_SCREEN_PADDING) / commonBounds.height
|
2021-10-18 13:30:42 +00:00
|
|
|
)
|
2021-09-02 12:51:39 +00:00
|
|
|
)
|
|
|
|
|
2021-10-17 08:47:41 +00:00
|
|
|
zoom =
|
|
|
|
this.pageState.camera.zoom === zoom || this.pageState.camera.zoom < 1
|
|
|
|
? Math.min(1, zoom)
|
|
|
|
: zoom
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const mx = (rendererBounds.width - commonBounds.width * zoom) / 2 / zoom
|
|
|
|
const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom
|
2021-09-02 12:51:39 +00:00
|
|
|
|
|
|
|
return this.setCamera(
|
2021-11-26 15:14:10 +00:00
|
|
|
Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
|
2021-09-22 15:00:20 +00:00
|
|
|
zoom,
|
2021-09-02 12:51:39 +00:00
|
|
|
`zoomed_to_fit`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom to the selected shapes.
|
|
|
|
*/
|
|
|
|
zoomToSelection = (): this => {
|
2021-09-04 15:00:13 +00:00
|
|
|
if (this.selectedIds.length === 0) return this
|
2021-09-02 12:51:39 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const { rendererBounds } = this
|
|
|
|
const selectedBounds = TLDR.getSelectedBounds(this.state)
|
2021-09-02 12:51:39 +00:00
|
|
|
|
2021-10-17 08:47:41 +00:00
|
|
|
let zoom = TLDR.getCameraZoom(
|
2021-10-18 13:30:42 +00:00
|
|
|
Math.min(
|
2021-11-16 16:01:29 +00:00
|
|
|
(rendererBounds.width - FIT_TO_SCREEN_PADDING) / selectedBounds.width,
|
|
|
|
(rendererBounds.height - FIT_TO_SCREEN_PADDING) / selectedBounds.height
|
2021-10-18 13:30:42 +00:00
|
|
|
)
|
2021-09-02 12:51:39 +00:00
|
|
|
)
|
|
|
|
|
2021-10-17 08:47:41 +00:00
|
|
|
zoom =
|
|
|
|
this.pageState.camera.zoom === zoom || this.pageState.camera.zoom < 1
|
|
|
|
? Math.min(1, zoom)
|
|
|
|
: zoom
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const mx = (rendererBounds.width - selectedBounds.width * zoom) / 2 / zoom
|
|
|
|
const my = (rendererBounds.height - selectedBounds.height * zoom) / 2 / zoom
|
2021-09-02 12:51:39 +00:00
|
|
|
|
|
|
|
return this.setCamera(
|
2021-11-26 15:14:10 +00:00
|
|
|
Vec.toFixed(Vec.sub([mx, my], [selectedBounds.minX, selectedBounds.minY])),
|
2021-09-04 15:00:13 +00:00
|
|
|
zoom,
|
2021-09-02 12:51:39 +00:00
|
|
|
`zoomed_to_selection`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom back to content when the canvas is empty.
|
|
|
|
*/
|
|
|
|
zoomToContent = (): this => {
|
2021-11-16 16:01:29 +00:00
|
|
|
const shapes = this.shapes
|
2021-09-02 12:51:39 +00:00
|
|
|
const pageState = this.pageState
|
|
|
|
|
|
|
|
if (shapes.length === 0) return this
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const { rendererBounds } = this
|
2021-09-02 12:51:39 +00:00
|
|
|
const { zoom } = pageState.camera
|
2021-11-16 16:01:29 +00:00
|
|
|
const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds))
|
2021-10-17 08:47:41 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const mx = (rendererBounds.width - commonBounds.width * zoom) / 2 / zoom
|
|
|
|
const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom
|
2021-09-02 12:51:39 +00:00
|
|
|
|
|
|
|
return this.setCamera(
|
2021-11-26 15:14:10 +00:00
|
|
|
Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
|
2021-09-02 12:51:39 +00:00
|
|
|
this.pageState.camera.zoom,
|
|
|
|
`zoomed_to_content`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom the camera to 100%.
|
|
|
|
*/
|
2021-11-07 13:45:48 +00:00
|
|
|
resetZoom = (): this => {
|
2021-09-02 12:51:39 +00:00
|
|
|
return this.zoomTo(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom the camera by a certain delta.
|
2021-09-05 09:51:21 +00:00
|
|
|
* @param delta The zoom delta.
|
|
|
|
* @param center The point to zoom toward.
|
2021-09-02 12:51:39 +00:00
|
|
|
*/
|
2021-11-08 14:21:37 +00:00
|
|
|
zoomBy = Utils.throttle((delta: number, center?: number[]): this => {
|
2021-09-02 12:51:39 +00:00
|
|
|
const { zoom } = this.pageState.camera
|
|
|
|
const nextZoom = TLDR.getCameraZoom(zoom - delta * zoom)
|
2021-09-05 09:51:21 +00:00
|
|
|
return this.zoomTo(nextZoom, center)
|
2021-09-02 12:51:39 +00:00
|
|
|
}, 16)
|
|
|
|
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Selection */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear the selection history (undo/redo stack for selection).
|
|
|
|
*/
|
|
|
|
private clearSelectHistory = (): this => {
|
|
|
|
this.selectHistory.pointer = 0
|
|
|
|
this.selectHistory.stack = [this.selectedIds]
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a selection to the selection history (undo/redo stack for selection).
|
|
|
|
*/
|
|
|
|
private addToSelectHistory = (ids: string[]): this => {
|
|
|
|
if (this.selectHistory.pointer < this.selectHistory.stack.length) {
|
|
|
|
this.selectHistory.stack = this.selectHistory.stack.slice(0, this.selectHistory.pointer + 1)
|
|
|
|
}
|
|
|
|
this.selectHistory.pointer++
|
|
|
|
this.selectHistory.stack.push(ids)
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the current selection.
|
|
|
|
* @param ids The ids to select
|
|
|
|
* @param push Whether to add the ids to the current selection instead.
|
|
|
|
*/
|
|
|
|
private setSelectedIds = (ids: string[], push = false): this => {
|
2021-10-12 14:59:04 +00:00
|
|
|
const nextIds = push ? [...this.pageState.selectedIds, ...ids] : [...ids]
|
|
|
|
|
2021-10-16 20:24:31 +00:00
|
|
|
return this.patchState(
|
|
|
|
{
|
|
|
|
appState: {
|
|
|
|
activeTool: 'select',
|
2021-10-12 14:59:04 +00:00
|
|
|
},
|
2021-10-16 20:24:31 +00:00
|
|
|
document: {
|
|
|
|
pageStates: {
|
|
|
|
[this.currentPageId]: {
|
|
|
|
selectedIds: nextIds,
|
2021-09-02 12:51:39 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2021-10-16 20:24:31 +00:00
|
|
|
},
|
|
|
|
`selected`
|
|
|
|
)
|
2021-09-02 12:51:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Undo the most recent selection.
|
|
|
|
*/
|
|
|
|
undoSelect = (): this => {
|
|
|
|
if (this.selectHistory.pointer > 0) {
|
|
|
|
this.selectHistory.pointer--
|
|
|
|
this.setSelectedIds(this.selectHistory.stack[this.selectHistory.pointer])
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Redo the previous selection.
|
|
|
|
*/
|
|
|
|
redoSelect = (): this => {
|
|
|
|
if (this.selectHistory.pointer < this.selectHistory.stack.length - 1) {
|
|
|
|
this.selectHistory.pointer++
|
|
|
|
this.setSelectedIds(this.selectHistory.stack[this.selectHistory.pointer])
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Select one or more shapes.
|
|
|
|
* @param ids The shape ids to select.
|
|
|
|
*/
|
|
|
|
select = (...ids: string[]): this => {
|
2021-09-04 15:00:13 +00:00
|
|
|
ids.forEach((id) => {
|
|
|
|
if (!this.page.shapes[id]) {
|
|
|
|
throw Error(`That shape does not exist on page ${this.currentPageId}`)
|
|
|
|
}
|
|
|
|
})
|
2021-09-02 12:51:39 +00:00
|
|
|
this.setSelectedIds(ids)
|
|
|
|
this.addToSelectHistory(ids)
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Select all shapes on the page.
|
|
|
|
*/
|
2021-09-23 09:48:08 +00:00
|
|
|
selectAll = (pageId = this.currentPageId): this => {
|
2021-09-02 12:51:39 +00:00
|
|
|
if (this.session) return this
|
2021-09-23 09:48:08 +00:00
|
|
|
|
|
|
|
// Select only shapes that are the direct child of the page
|
|
|
|
this.setSelectedIds(
|
|
|
|
Object.values(this.document.pages[pageId].shapes)
|
|
|
|
.filter((shape) => shape.parentId === pageId)
|
|
|
|
.map((shape) => shape.id)
|
|
|
|
)
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
this.addToSelectHistory(this.selectedIds)
|
2021-09-23 09:48:08 +00:00
|
|
|
|
2021-11-21 11:53:13 +00:00
|
|
|
this.selectTool('select')
|
2021-09-23 09:48:08 +00:00
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deselect any selected shapes.
|
|
|
|
*/
|
2021-11-07 13:45:48 +00:00
|
|
|
selectNone = (): this => {
|
2021-09-02 12:51:39 +00:00
|
|
|
this.setSelectedIds([])
|
|
|
|
this.addToSelectHistory(this.selectedIds)
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-08-16 07:49:31 +00:00
|
|
|
/* -------------------------------------------------- */
|
2021-10-13 13:55:31 +00:00
|
|
|
/* Sessions p */
|
2021-08-16 07:49:31 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
2021-09-01 08:57:46 +00:00
|
|
|
/**
|
|
|
|
* Start a new session.
|
|
|
|
* @param session The new session
|
|
|
|
* @param args arguments of the session's start method.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
startSession = <T extends SessionType>(type: T, ...args: SessionArgsOfType<T>): this => {
|
2021-11-05 14:13:14 +00:00
|
|
|
if (this.readOnly && type !== SessionType.Brush) return this
|
2021-10-16 07:33:25 +00:00
|
|
|
if (this.session) {
|
2021-11-18 16:38:49 +00:00
|
|
|
console.warn(`Already in a session! (${this.session.constructor.name})`)
|
|
|
|
this.cancelSession()
|
2021-10-16 07:33:25 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
const Session = getSession(type)
|
2021-10-30 09:04:33 +00:00
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
// @ts-ignore
|
2021-11-16 16:01:29 +00:00
|
|
|
this.session = new Session(this, ...args)
|
2021-08-30 11:06:42 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const result = this.session.start()
|
2021-08-30 11:06:42 +00:00
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
if (result) {
|
2021-11-16 16:01:29 +00:00
|
|
|
this.patchState(result, `session:start_${this.session.constructor.name}`)
|
2021-08-16 21:52:03 +00:00
|
|
|
}
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-10-16 14:32:55 +00:00
|
|
|
return this
|
|
|
|
// return this.setStatus(this.session.status)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:57:46 +00:00
|
|
|
/**
|
2021-10-13 13:55:31 +00:00
|
|
|
* updateSession.
|
2021-09-01 08:57:46 +00:00
|
|
|
* @param args The arguments of the current session's update method.
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
updateSession = (): this => {
|
2021-08-10 16:12:55 +00:00
|
|
|
const { session } = this
|
|
|
|
if (!session) return this
|
2021-10-18 13:30:42 +00:00
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
2021-11-16 16:01:29 +00:00
|
|
|
const patch = session.update()
|
2021-08-29 13:33:43 +00:00
|
|
|
if (!patch) return this
|
2021-10-16 18:40:59 +00:00
|
|
|
return this.patchState(patch, `session:${session?.constructor.name}`)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:57:46 +00:00
|
|
|
/**
|
|
|
|
* Cancel the current session.
|
|
|
|
* @param args The arguments of the current session's cancel method.
|
|
|
|
*/
|
2021-10-13 13:55:31 +00:00
|
|
|
cancelSession = (): this => {
|
2021-08-10 16:12:55 +00:00
|
|
|
const { session } = this
|
|
|
|
if (!session) return this
|
|
|
|
this.session = undefined
|
2021-08-15 08:49:38 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const result = session.cancel()
|
2021-10-14 12:32:48 +00:00
|
|
|
|
|
|
|
if (result) {
|
2021-10-15 16:14:36 +00:00
|
|
|
this.patchState(result, `session:cancel:${session.constructor.name}`)
|
2021-10-14 12:32:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:57:46 +00:00
|
|
|
/**
|
|
|
|
* Complete the current session.
|
|
|
|
* @param args The arguments of the current session's complete method.
|
|
|
|
*/
|
2021-10-13 13:55:31 +00:00
|
|
|
completeSession = (): this => {
|
2021-08-10 16:12:55 +00:00
|
|
|
const { session } = this
|
2021-08-30 20:17:04 +00:00
|
|
|
|
2021-08-10 16:12:55 +00:00
|
|
|
if (!session) return this
|
|
|
|
this.session = undefined
|
2021-11-16 16:01:29 +00:00
|
|
|
const result = session.complete()
|
2021-09-17 21:29:45 +00:00
|
|
|
|
2021-08-16 21:52:03 +00:00
|
|
|
if (result === undefined) {
|
2021-08-29 13:33:43 +00:00
|
|
|
this.isCreating = false
|
2021-09-11 22:58:22 +00:00
|
|
|
|
2021-08-29 13:33:43 +00:00
|
|
|
return this.patchState(
|
2021-08-17 21:38:37 +00:00
|
|
|
{
|
2021-10-14 13:14:47 +00:00
|
|
|
appState: {
|
2021-11-16 16:01:29 +00:00
|
|
|
status: TDStatus.Idle,
|
2021-10-14 13:14:47 +00:00
|
|
|
},
|
2021-08-18 07:19:13 +00:00
|
|
|
document: {
|
|
|
|
pageStates: {
|
|
|
|
[this.currentPageId]: {
|
|
|
|
editingId: undefined,
|
2021-10-14 12:32:48 +00:00
|
|
|
bindingId: undefined,
|
|
|
|
hoveredId: undefined,
|
2021-08-18 07:19:13 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2021-08-17 21:38:37 +00:00
|
|
|
},
|
2021-10-13 13:55:31 +00:00
|
|
|
`session:complete:${session.constructor.name}`
|
2021-08-17 21:38:37 +00:00
|
|
|
)
|
2021-08-16 21:52:03 +00:00
|
|
|
} else if ('after' in result) {
|
2021-08-15 08:49:38 +00:00
|
|
|
// Session ended with a command
|
|
|
|
|
|
|
|
if (this.isCreating) {
|
|
|
|
// We're currently creating a shape. Override the command's
|
|
|
|
// before state so that when we undo the command, we remove
|
|
|
|
// the shape we just created.
|
|
|
|
result.before = {
|
2021-10-14 13:14:47 +00:00
|
|
|
appState: {
|
2021-10-18 13:30:42 +00:00
|
|
|
...result.before.appState,
|
2021-11-16 16:01:29 +00:00
|
|
|
status: TDStatus.Idle,
|
2021-10-14 13:14:47 +00:00
|
|
|
},
|
2021-08-16 14:01:03 +00:00
|
|
|
document: {
|
|
|
|
pages: {
|
|
|
|
[this.currentPageId]: {
|
|
|
|
shapes: Object.fromEntries(this.selectedIds.map((id) => [id, undefined])),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
pageStates: {
|
|
|
|
[this.currentPageId]: {
|
|
|
|
selectedIds: [],
|
2021-09-11 22:58:22 +00:00
|
|
|
editingId: null,
|
|
|
|
bindingId: null,
|
|
|
|
hoveredId: null,
|
2021-08-18 07:19:13 +00:00
|
|
|
},
|
|
|
|
},
|
2021-08-17 21:38:37 +00:00
|
|
|
},
|
2021-08-18 07:19:13 +00:00
|
|
|
}
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-08-30 10:59:31 +00:00
|
|
|
if (this.appState.isToolLocked) {
|
|
|
|
const pageState = result.after?.document?.pageStates?.[this.currentPageId] || {}
|
|
|
|
pageState.selectedIds = []
|
|
|
|
}
|
|
|
|
|
2021-08-29 13:33:43 +00:00
|
|
|
this.isCreating = false
|
2021-08-18 07:19:13 +00:00
|
|
|
}
|
|
|
|
|
2021-10-14 13:14:47 +00:00
|
|
|
result.after.appState = {
|
2021-10-18 13:30:42 +00:00
|
|
|
...result.after.appState,
|
2021-11-16 16:01:29 +00:00
|
|
|
status: TDStatus.Idle,
|
2021-10-14 13:14:47 +00:00
|
|
|
}
|
|
|
|
|
2021-09-11 22:58:22 +00:00
|
|
|
result.after.document = {
|
|
|
|
...result.after.document,
|
|
|
|
pageStates: {
|
|
|
|
...result.after.document?.pageStates,
|
|
|
|
[this.currentPageId]: {
|
|
|
|
...(result.after.document?.pageStates || {})[this.currentPageId],
|
|
|
|
editingId: null,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
this.setState(result, `session:complete:${session.constructor.name}`)
|
2021-08-15 08:49:38 +00:00
|
|
|
} else {
|
2021-08-29 13:33:43 +00:00
|
|
|
this.patchState(
|
2021-08-17 21:38:37 +00:00
|
|
|
{
|
|
|
|
...result,
|
2021-10-14 13:14:47 +00:00
|
|
|
appState: {
|
|
|
|
...result.appState,
|
2021-11-16 16:01:29 +00:00
|
|
|
status: TDStatus.Idle,
|
2021-10-14 13:14:47 +00:00
|
|
|
},
|
2021-08-18 07:19:13 +00:00
|
|
|
document: {
|
2021-11-26 15:14:10 +00:00
|
|
|
...result.document,
|
2021-08-18 07:19:13 +00:00
|
|
|
pageStates: {
|
2021-08-29 13:33:43 +00:00
|
|
|
[this.currentPageId]: {
|
2021-09-17 21:29:45 +00:00
|
|
|
...result.document?.pageStates?.[this.currentPageId],
|
2021-09-11 22:58:22 +00:00
|
|
|
editingId: null,
|
2021-08-29 13:33:43 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2021-10-13 13:55:31 +00:00
|
|
|
`session:complete:${session.constructor.name}`
|
2021-08-29 13:33:43 +00:00
|
|
|
)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-08-29 13:33:43 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Shape Functions */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Manually create shapes on the page.
|
|
|
|
* @param shapes An array of shape partials, containing the initial props for the shapes.
|
|
|
|
* @command
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
createShapes = (...shapes: ({ id: string; type: TDShapeType } & Partial<TDShape>)[]): this => {
|
2021-09-01 08:37:07 +00:00
|
|
|
if (shapes.length === 0) return this
|
2021-10-13 16:03:33 +00:00
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
return this.create(
|
2021-10-13 16:03:33 +00:00
|
|
|
shapes.map((shape) => {
|
2021-11-16 16:01:29 +00:00
|
|
|
return TLDR.getShapeUtil(shape.type).create({
|
2021-10-13 16:03:33 +00:00
|
|
|
parentId: this.currentPageId,
|
2021-09-01 12:00:51 +00:00
|
|
|
...shape,
|
|
|
|
})
|
2021-10-13 16:03:33 +00:00
|
|
|
})
|
2021-09-01 08:37:07 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manually update a set of shapes.
|
|
|
|
* @param shapes An array of shape partials, containing the changes to be made to each shape.
|
|
|
|
* @command
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
updateShapes = (...shapes: ({ id: string } & Partial<TDShape>)[]): this => {
|
2021-09-08 13:40:04 +00:00
|
|
|
const pageShapes = this.document.pages[this.currentPageId].shapes
|
|
|
|
const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
|
|
|
|
if (shapesToUpdate.length === 0) return this
|
|
|
|
return this.setState(
|
2021-11-20 09:37:42 +00:00
|
|
|
Commands.updateShapes(this, shapesToUpdate, this.currentPageId),
|
2021-09-08 13:40:04 +00:00
|
|
|
'updated_shapes'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
createTextShapeAtPoint(point: number[], id?: string): this {
|
2021-10-16 07:33:25 +00:00
|
|
|
const {
|
|
|
|
shapes,
|
|
|
|
appState: { currentPageId, currentStyle },
|
|
|
|
} = this
|
|
|
|
|
|
|
|
const childIndex =
|
|
|
|
shapes.length === 0
|
|
|
|
? 1
|
|
|
|
: shapes
|
|
|
|
.filter((shape) => shape.parentId === currentPageId)
|
|
|
|
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const Text = shapeUtils[TDShapeType.Text]
|
|
|
|
|
2021-10-16 07:33:25 +00:00
|
|
|
const newShape = Text.create({
|
2021-11-16 16:01:29 +00:00
|
|
|
id: id || Utils.uniqueId(),
|
2021-10-16 07:33:25 +00:00
|
|
|
parentId: currentPageId,
|
|
|
|
childIndex,
|
|
|
|
point,
|
|
|
|
style: { ...currentStyle },
|
|
|
|
})
|
2021-11-16 16:01:29 +00:00
|
|
|
|
2021-10-16 07:33:25 +00:00
|
|
|
const bounds = Text.getBounds(newShape)
|
|
|
|
newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2])
|
|
|
|
this.createShapes(newShape)
|
2021-11-16 16:01:29 +00:00
|
|
|
this.setEditingId(newShape.id)
|
2021-11-05 14:13:14 +00:00
|
|
|
|
|
|
|
return this
|
2021-10-16 07:33:25 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Create one or more shapes.
|
|
|
|
* @param shapes An array of shapes.
|
|
|
|
* @command
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
create = (shapes: TDShape[] = [], bindings: TDBinding[] = []): this => {
|
2021-09-01 08:37:07 +00:00
|
|
|
if (shapes.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.createShapes(this, shapes, bindings))
|
2021-09-01 08:37:07 +00:00
|
|
|
}
|
|
|
|
|
2021-10-22 13:49:29 +00:00
|
|
|
/**
|
|
|
|
* Patch in a new set of shapes
|
|
|
|
* @param shapes
|
|
|
|
* @param bindings
|
|
|
|
*/
|
2021-11-16 16:01:29 +00:00
|
|
|
patchCreate = (shapes: TDShape[] = [], bindings: TDBinding[] = []): this => {
|
2021-10-22 13:49:29 +00:00
|
|
|
if (shapes.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.patchState(Commands.createShapes(this, shapes, bindings).after)
|
2021-10-22 13:49:29 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Delete one or more shapes.
|
|
|
|
* @param ids The ids of the shapes to delete.
|
|
|
|
* @command
|
|
|
|
*/
|
|
|
|
delete = (ids = this.selectedIds): this => {
|
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.deleteShapes(this, ids))
|
2021-09-01 08:37:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete all shapes on the page.
|
|
|
|
*/
|
2021-11-08 14:21:37 +00:00
|
|
|
deleteAll = (): this => {
|
2021-09-01 08:37:07 +00:00
|
|
|
this.selectAll()
|
|
|
|
this.delete()
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change the style for one or more shapes.
|
|
|
|
* @param style A style partial to apply to the shapes.
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
style = (style: Partial<ShapeStyles>, ids = this.selectedIds): this => {
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.styleShapes(this, ids, style))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Align one or more shapes.
|
|
|
|
* @param direction Whether to align horizontally or vertically.
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
align = (type: AlignType, ids = this.selectedIds): this => {
|
|
|
|
if (ids.length < 2) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.alignShapes(this, ids, type))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Distribute one or more shapes.
|
|
|
|
* @param direction Whether to distribute horizontally or vertically..
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
distribute = (direction: DistributeType, ids = this.selectedIds): this => {
|
|
|
|
if (ids.length < 3) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.distributeShapes(this, ids, direction))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Stretch one or more shapes to their common bounds.
|
|
|
|
* @param direction Whether to stretch horizontally or vertically.
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
stretch = (direction: StretchType, ids = this.selectedIds): this => {
|
|
|
|
if (ids.length < 2) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.stretchShapes(this, ids, direction))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Flip one or more shapes horizontally.
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
flipHorizontal = (ids = this.selectedIds): this => {
|
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.flipShapes(this, ids, FlipType.Horizontal))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Flip one or more shapes vertically.
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
flipVertical = (ids = this.selectedIds): this => {
|
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.flipShapes(this, ids, FlipType.Vertical))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-04 15:00:13 +00:00
|
|
|
/**
|
|
|
|
* Move one or more shapes to a new page. Will also break or move bindings.
|
2021-09-05 09:51:21 +00:00
|
|
|
* @param toPageId The id of the page to move the shapes to.
|
|
|
|
* @param fromPageId The id of the page to move the shapes from (defaults to current page).
|
2021-09-04 15:00:13 +00:00
|
|
|
* @param ids The ids of the shapes to move (defaults to selection).
|
|
|
|
*/
|
|
|
|
moveToPage = (
|
|
|
|
toPageId: string,
|
|
|
|
fromPageId = this.currentPageId,
|
|
|
|
ids = this.selectedIds
|
|
|
|
): this => {
|
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
const { rendererBounds } = this
|
|
|
|
this.setState(Commands.moveShapesToPage(this, ids, rendererBounds, fromPageId, toPageId))
|
2021-09-04 15:00:13 +00:00
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Move one or more shapes to the back of the page.
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
moveToBack = (ids = this.selectedIds): this => {
|
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.reorderShapes(this, ids, MoveType.ToBack))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Move one or more shapes backward on of the page.
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
moveBackward = (ids = this.selectedIds): this => {
|
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.reorderShapes(this, ids, MoveType.Backward))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Move one or more shapes forward on the page.
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
moveForward = (ids = this.selectedIds): this => {
|
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.reorderShapes(this, ids, MoveType.Forward))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Move one or more shapes to the front of the page.
|
|
|
|
* @param ids The ids of the shapes to change (defaults to selection).
|
|
|
|
*/
|
2021-09-01 11:18:50 +00:00
|
|
|
moveToFront = (ids = this.selectedIds): this => {
|
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.reorderShapes(this, ids, MoveType.ToFront))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Nudge one or more shapes in a direction.
|
|
|
|
* @param delta The direction to nudge the shapes.
|
|
|
|
* @param isMajor Whether this is a major (i.e. shift) nudge.
|
|
|
|
* @param ids The ids to change (defaults to selection).
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
nudge = (delta: number[], isMajor = false, ids = this.selectedIds): this => {
|
2021-09-01 11:18:50 +00:00
|
|
|
if (ids.length === 0) return this
|
2021-11-26 15:14:10 +00:00
|
|
|
const size = isMajor
|
|
|
|
? this.settings.showGrid
|
|
|
|
? this.currentGrid * 4
|
|
|
|
: 10
|
|
|
|
: this.settings.showGrid
|
|
|
|
? this.currentGrid
|
|
|
|
: 1
|
|
|
|
|
|
|
|
return this.setState(Commands.translateShapes(this, ids, Vec.mul(delta, size)))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Duplicate one or more shapes.
|
|
|
|
* @param ids The ids to duplicate (defaults to selection).
|
|
|
|
*/
|
2021-10-15 16:14:36 +00:00
|
|
|
duplicate = (ids = this.selectedIds, point?: number[]): this => {
|
2021-11-05 14:13:14 +00:00
|
|
|
if (this.readOnly) return this
|
2021-09-01 11:18:50 +00:00
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.duplicateShapes(this, ids, point))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-06 11:07:15 +00:00
|
|
|
/**
|
|
|
|
* Reset the bounds for one or more shapes. Usually when the
|
|
|
|
* bounding box of a shape is double-clicked. Different shapes may
|
|
|
|
* handle this differently.
|
|
|
|
* @param ids The ids to change (defaults to selection).
|
|
|
|
*/
|
|
|
|
resetBounds = (ids = this.selectedIds): this => {
|
2021-11-16 16:01:29 +00:00
|
|
|
const command = Commands.resetBounds(this, ids, this.currentPageId)
|
|
|
|
return this.setState(Commands.resetBounds(this, ids, this.currentPageId), command.id)
|
2021-09-06 11:07:15 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Toggle the hidden property of one or more shapes.
|
|
|
|
* @param ids The ids to change (defaults to selection).
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
toggleHidden = (ids = this.selectedIds): this => {
|
2021-09-01 11:18:50 +00:00
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.toggleShapeProp(this, ids, 'isHidden'))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Toggle the locked property of one or more shapes.
|
|
|
|
* @param ids The ids to change (defaults to selection).
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
toggleLocked = (ids = this.selectedIds): this => {
|
2021-09-01 11:18:50 +00:00
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.toggleShapeProp(this, ids, 'isLocked'))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Toggle the fixed-aspect-ratio property of one or more shapes.
|
|
|
|
* @param ids The ids to change (defaults to selection).
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
toggleAspectRatioLocked = (ids = this.selectedIds): this => {
|
2021-09-01 11:18:50 +00:00
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.toggleShapeProp(this, ids, 'isAspectRatioLocked'))
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Toggle the decoration at a handle of one or more shapes.
|
|
|
|
* @param handleId The handle to toggle.
|
|
|
|
* @param ids The ids of the shapes to toggle the decoration on.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
toggleDecoration = (handleId: string, ids = this.selectedIds): this => {
|
2021-09-01 11:18:50 +00:00
|
|
|
if (ids.length === 0 || !(handleId === 'start' || handleId === 'end')) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
return this.setState(Commands.toggleShapesDecoration(this, ids, handleId))
|
2021-08-11 13:34:17 +00:00
|
|
|
}
|
|
|
|
|
2021-11-20 09:37:42 +00:00
|
|
|
/**
|
|
|
|
* Set the props of one or more shapes
|
|
|
|
* @param props The props to set on the shapes.
|
|
|
|
* @param ids The ids of the shapes to set props on.
|
|
|
|
*/
|
|
|
|
setShapeProps = <T extends TDShape>(props: Partial<T>, ids = this.selectedIds) => {
|
|
|
|
return this.setState(Commands.setShapesProps(this, ids, props))
|
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Rotate one or more shapes by a delta.
|
|
|
|
* @param delta The delta in radians.
|
|
|
|
* @param ids The ids to rotate (defaults to selection).
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
rotate = (delta = Math.PI * -0.5, ids = this.selectedIds): this => {
|
2021-09-01 11:18:50 +00:00
|
|
|
if (ids.length === 0) return this
|
2021-11-16 16:01:29 +00:00
|
|
|
const change = Commands.rotateShapes(this, ids, delta)
|
2021-09-19 13:53:52 +00:00
|
|
|
if (!change) return this
|
|
|
|
return this.setState(change)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
2021-09-02 12:51:39 +00:00
|
|
|
* Group the selected shapes.
|
2021-09-05 09:51:21 +00:00
|
|
|
* @param ids The ids to group (defaults to selection).
|
|
|
|
* @param groupId The new group's id.
|
2021-09-02 12:51:39 +00:00
|
|
|
*/
|
2021-09-05 09:51:21 +00:00
|
|
|
group = (
|
|
|
|
ids = this.selectedIds,
|
|
|
|
groupId = Utils.uniqueId(),
|
|
|
|
pageId = this.currentPageId
|
|
|
|
): this => {
|
2021-11-05 14:13:14 +00:00
|
|
|
if (this.readOnly) return this
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
if (ids.length === 1 && this.getShape(ids[0], pageId).type === TDShapeType.Group) {
|
2021-10-10 13:08:41 +00:00
|
|
|
return this.ungroup(ids, pageId)
|
|
|
|
}
|
|
|
|
|
2021-09-02 12:51:39 +00:00
|
|
|
if (ids.length < 2) return this
|
2021-10-10 13:08:41 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const command = Commands.groupShapes(this, ids, groupId, pageId)
|
2021-09-02 12:51:39 +00:00
|
|
|
if (!command) return this
|
|
|
|
return this.setState(command)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ungroup the selected groups.
|
2021-09-01 08:37:07 +00:00
|
|
|
* @todo
|
|
|
|
*/
|
2021-10-10 13:08:41 +00:00
|
|
|
ungroup = (ids = this.selectedIds, pageId = this.currentPageId): this => {
|
2021-11-05 14:13:14 +00:00
|
|
|
if (this.readOnly) return this
|
|
|
|
|
2021-10-10 13:08:41 +00:00
|
|
|
const groups = ids
|
|
|
|
.map((id) => this.getShape(id, pageId))
|
2021-11-16 16:01:29 +00:00
|
|
|
.filter((shape) => shape.type === TDShapeType.Group)
|
2021-10-10 13:08:41 +00:00
|
|
|
|
|
|
|
if (groups.length === 0) return this
|
2021-09-05 09:51:21 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
const command = Commands.ungroupShapes(this, ids, groups as GroupShape[], pageId)
|
2021-09-05 09:51:21 +00:00
|
|
|
if (!command) return this
|
|
|
|
return this.setState(command)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-01 08:37:07 +00:00
|
|
|
/**
|
|
|
|
* Cancel the current session.
|
|
|
|
*/
|
2021-08-29 13:33:43 +00:00
|
|
|
cancel = (): this => {
|
2021-10-13 13:55:31 +00:00
|
|
|
this.currentTool.onCancel?.()
|
2021-08-29 13:33:43 +00:00
|
|
|
return this
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-08-29 13:33:43 +00:00
|
|
|
/* -------------------------------------------------- */
|
|
|
|
/* Event Handlers */
|
|
|
|
/* -------------------------------------------------- */
|
|
|
|
|
|
|
|
/* ----------------- Keyboard Events ---------------- */
|
|
|
|
|
2021-10-13 16:03:33 +00:00
|
|
|
onKeyDown: TLKeyboardEventHandler = (key, info, e) => {
|
2021-11-16 16:01:29 +00:00
|
|
|
switch (e.key) {
|
2021-11-26 15:14:10 +00:00
|
|
|
case '/': {
|
2021-11-24 18:07:31 +00:00
|
|
|
if (this.status === 'idle') {
|
|
|
|
const { shiftKey, metaKey, altKey, ctrlKey, spaceKey } = this
|
|
|
|
|
|
|
|
this.onPointerDown(
|
|
|
|
{
|
|
|
|
target: 'canvas',
|
|
|
|
pointerId: 0,
|
|
|
|
origin: info.point,
|
|
|
|
point: info.point,
|
|
|
|
delta: [0, 0],
|
|
|
|
pressure: 0.5,
|
|
|
|
shiftKey,
|
|
|
|
ctrlKey,
|
|
|
|
metaKey,
|
|
|
|
altKey,
|
|
|
|
spaceKey,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
shiftKey,
|
|
|
|
altKey,
|
|
|
|
ctrlKey,
|
|
|
|
pointerId: 0,
|
|
|
|
clientX: info.point[0],
|
|
|
|
clientY: info.point[1],
|
|
|
|
} as unknown as React.PointerEvent<HTMLDivElement>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
2021-11-16 16:01:29 +00:00
|
|
|
case 'Escape': {
|
|
|
|
this.cancel()
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'Meta': {
|
|
|
|
this.metaKey = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'Alt': {
|
|
|
|
this.altKey = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'Control': {
|
|
|
|
this.ctrlKey = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case ' ': {
|
|
|
|
this.spaceKey = true
|
|
|
|
break
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 16:03:33 +00:00
|
|
|
this.currentTool.onKeyDown?.(key, info, e)
|
2021-08-11 14:51:24 +00:00
|
|
|
|
2021-10-13 16:03:33 +00:00
|
|
|
return this
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 16:03:33 +00:00
|
|
|
onKeyUp: TLKeyboardEventHandler = (key, info, e) => {
|
2021-09-17 21:29:45 +00:00
|
|
|
if (!info) return
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
switch (e.key) {
|
2021-11-26 15:14:10 +00:00
|
|
|
case '/': {
|
2021-11-24 18:07:31 +00:00
|
|
|
const { currentPoint, shiftKey, metaKey, altKey, ctrlKey, spaceKey } = this
|
|
|
|
|
|
|
|
this.onPointerUp(
|
|
|
|
{
|
|
|
|
target: 'canvas',
|
|
|
|
pointerId: 0,
|
|
|
|
origin: currentPoint,
|
|
|
|
point: currentPoint,
|
|
|
|
delta: [0, 0],
|
|
|
|
pressure: 0.5,
|
|
|
|
shiftKey,
|
|
|
|
ctrlKey,
|
|
|
|
metaKey,
|
|
|
|
altKey,
|
|
|
|
spaceKey,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
shiftKey,
|
|
|
|
altKey,
|
|
|
|
ctrlKey,
|
|
|
|
pointerId: 0,
|
|
|
|
clientX: currentPoint[0],
|
|
|
|
clientY: currentPoint[1],
|
|
|
|
} as unknown as React.PointerEvent<HTMLDivElement>
|
|
|
|
)
|
|
|
|
break
|
|
|
|
}
|
2021-11-16 16:01:29 +00:00
|
|
|
case 'Meta': {
|
|
|
|
this.metaKey = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'Alt': {
|
|
|
|
this.altKey = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'Control': {
|
|
|
|
this.ctrlKey = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case ' ': {
|
|
|
|
this.spaceKey = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-13 16:03:33 +00:00
|
|
|
this.currentTool.onKeyUp?.(key, info, e)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* ------------- Renderer Event Handlers ------------ */
|
2021-08-29 13:33:43 +00:00
|
|
|
|
2021-10-14 12:32:48 +00:00
|
|
|
onPinchStart: TLPinchEventHandler = (info, e) => this.currentTool.onPinchStart?.(info, e)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-10-14 12:32:48 +00:00
|
|
|
onPinchEnd: TLPinchEventHandler = (info, e) => this.currentTool.onPinchEnd?.(info, e)
|
2021-10-13 13:55:31 +00:00
|
|
|
|
2021-10-14 12:32:48 +00:00
|
|
|
onPinch: TLPinchEventHandler = (info, e) => this.currentTool.onPinch?.(info, e)
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
onPan: TLWheelEventHandler = (info, e) => {
|
2021-10-14 12:32:48 +00:00
|
|
|
if (this.appState.status === 'pinching') return
|
2021-08-10 16:12:55 +00:00
|
|
|
// TODO: Pan and pinchzoom are firing at the same time. Considering turning one of them off!
|
|
|
|
|
2021-08-17 21:38:37 +00:00
|
|
|
const delta = Vec.div(info.delta, this.pageState.camera.zoom)
|
|
|
|
const prev = this.pageState.camera.point
|
2021-08-10 16:12:55 +00:00
|
|
|
const next = Vec.sub(prev, delta)
|
|
|
|
|
|
|
|
if (Vec.isEqual(next, prev)) return
|
|
|
|
|
|
|
|
this.pan(delta)
|
2021-10-14 12:32:48 +00:00
|
|
|
|
2021-11-24 10:01:17 +00:00
|
|
|
// onPan is called by onPointerMove when spaceKey & middle wheel button is pressed,
|
2021-10-16 20:24:31 +00:00
|
|
|
// so we shouldn't call this again.
|
2021-11-24 10:01:17 +00:00
|
|
|
if (!info.spaceKey && !(e.buttons === 4)) {
|
2021-10-14 12:32:48 +00:00
|
|
|
this.onPointerMove(info, e as unknown as React.PointerEvent)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
onZoom: TLWheelEventHandler = (info, e) => {
|
2021-11-16 16:01:29 +00:00
|
|
|
if (this.state.appState.status !== TDStatus.Idle) return
|
|
|
|
|
|
|
|
const delta =
|
|
|
|
e.deltaMode === WheelEvent.DOM_DELTA_PIXEL
|
|
|
|
? info.delta[2] / 500
|
|
|
|
: e.deltaMode === WheelEvent.DOM_DELTA_LINE
|
|
|
|
? info.delta[2] / 100
|
|
|
|
: info.delta[2] / 2
|
|
|
|
|
|
|
|
this.zoomBy(delta, info.delta)
|
2021-10-13 13:55:31 +00:00
|
|
|
this.onPointerMove(info, e as unknown as React.PointerEvent)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-08-30 18:17:46 +00:00
|
|
|
/* ----------------- Pointer Events ----------------- */
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
updateInputs: TLPointerEventHandler = (info) => {
|
|
|
|
this.currentPoint = [...this.getPagePoint(info.point), info.pressure]
|
|
|
|
this.shiftKey = info.shiftKey
|
|
|
|
this.altKey = info.altKey
|
|
|
|
this.ctrlKey = info.ctrlKey
|
|
|
|
this.metaKey = info.metaKey
|
|
|
|
}
|
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
onPointerMove: TLPointerEventHandler = (info, e) => {
|
2021-11-16 16:01:29 +00:00
|
|
|
this.previousPoint = this.currentPoint
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
|
2021-10-14 12:32:48 +00:00
|
|
|
// Several events (e.g. pan) can trigger the same "pointer move" behavior
|
|
|
|
this.currentTool.onPointerMove?.(info, e)
|
|
|
|
|
2021-10-16 20:24:31 +00:00
|
|
|
// Move this to an emitted event
|
2021-10-09 13:57:44 +00:00
|
|
|
if (this.state.room) {
|
|
|
|
const { users, userId } = this.state.room
|
|
|
|
|
2021-11-22 14:00:24 +00:00
|
|
|
this.callbacks.onChangePresence?.(this, {
|
2021-10-16 20:24:31 +00:00
|
|
|
...users[userId],
|
|
|
|
point: this.getPagePoint(info.point),
|
|
|
|
})
|
2021-10-09 13:57:44 +00:00
|
|
|
}
|
2021-10-13 13:55:31 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onPointerDown: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onPointerDown?.(info, e)
|
|
|
|
}
|
2021-10-13 13:55:31 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onPointerUp: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onPointerUp?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
// Canvas (background)
|
2021-11-16 16:01:29 +00:00
|
|
|
onPointCanvas: TLCanvasEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onPointCanvas?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDoubleClickCanvas: TLCanvasEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDoubleClickCanvas?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onRightPointCanvas: TLCanvasEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onRightPointCanvas?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDragCanvas: TLCanvasEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDragCanvas?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onReleaseCanvas: TLCanvasEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onReleaseCanvas?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
// Shape
|
2021-11-16 16:01:29 +00:00
|
|
|
onPointShape: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onPointShape?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onReleaseShape: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onReleaseShape?.(info, e)
|
|
|
|
}
|
2021-09-03 09:45:36 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDoubleClickShape: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDoubleClickShape?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onRightPointShape: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onRightPointShape?.(info, e)
|
|
|
|
}
|
2021-09-03 09:45:36 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDragShape: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDragShape?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onHoverShape: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onHoverShape?.(info, e)
|
|
|
|
}
|
2021-08-15 13:19:05 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onUnhoverShape: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onUnhoverShape?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
// Bounds (bounding box background)
|
2021-11-16 16:01:29 +00:00
|
|
|
onPointBounds: TLBoundsEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onPointBounds?.(info, e)
|
|
|
|
}
|
2021-09-03 11:07:34 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDoubleClickBounds: TLBoundsEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDoubleClickBounds?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onRightPointBounds: TLBoundsEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onRightPointBounds?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDragBounds: TLBoundsEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDragBounds?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onHoverBounds: TLBoundsEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onHoverBounds?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onUnhoverBounds: TLBoundsEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onUnhoverBounds?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onReleaseBounds: TLBoundsEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onReleaseBounds?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
// Bounds handles (corners, edges)
|
2021-11-16 16:01:29 +00:00
|
|
|
onPointBoundsHandle: TLBoundsHandleEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onPointBoundsHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDoubleClickBoundsHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onRightPointBoundsHandle: TLBoundsHandleEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onRightPointBoundsHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDragBoundsHandle: TLBoundsHandleEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDragBoundsHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onHoverBoundsHandle: TLBoundsHandleEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onHoverBoundsHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onUnhoverBoundsHandle: TLBoundsHandleEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onUnhoverBoundsHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onReleaseBoundsHandle: TLBoundsHandleEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onReleaseBoundsHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
|
|
|
// Handles (ie the handles of a selected arrow)
|
2021-11-16 16:01:29 +00:00
|
|
|
onPointHandle: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onPointHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDoubleClickHandle: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDoubleClickHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onRightPointHandle: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onRightPointHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onDragHandle: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onDragHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onHoverHandle: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onHoverHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onUnhoverHandle: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onUnhoverHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onReleaseHandle: TLPointerEventHandler = (info, e) => {
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onReleaseHandle?.(info, e)
|
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
onShapeChange = (shape: { id: string } & Partial<TDShape>) => {
|
2021-10-13 13:55:31 +00:00
|
|
|
this.updateShapes(shape)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-09-11 22:17:54 +00:00
|
|
|
onShapeBlur = () => {
|
2021-11-09 14:26:41 +00:00
|
|
|
// This prevents an auto-blur event from Safari
|
|
|
|
if (Date.now() - this.editingStartTime < 50) return
|
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
const { editingId } = this.pageState
|
2021-11-21 11:53:13 +00:00
|
|
|
const { isToolLocked } = this.getAppState()
|
2021-10-13 13:55:31 +00:00
|
|
|
|
|
|
|
if (editingId) {
|
|
|
|
// If we're editing text, then delete the text if it's empty
|
|
|
|
const shape = this.getShape(editingId)
|
|
|
|
this.setEditingId()
|
2021-11-16 16:01:29 +00:00
|
|
|
if (shape.type === TDShapeType.Text) {
|
2021-10-13 16:03:33 +00:00
|
|
|
if (shape.text.trim().length <= 0) {
|
2021-11-16 16:01:29 +00:00
|
|
|
this.patchState(Commands.deleteShapes(this, [editingId]).after, 'delete_empty_text')
|
2021-11-21 11:53:13 +00:00
|
|
|
} else if (!isToolLocked) {
|
2021-10-13 16:03:33 +00:00
|
|
|
this.select(editingId)
|
|
|
|
}
|
2021-09-11 22:17:54 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-13 13:55:31 +00:00
|
|
|
|
|
|
|
this.currentTool.onShapeBlur?.()
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-11-26 17:12:27 +00:00
|
|
|
onShapeClone: TLShapeCloneHandler = (info, e) => {
|
|
|
|
this.originPoint = this.getPagePoint(info.point)
|
|
|
|
this.updateInputs(info, e)
|
|
|
|
this.currentTool.onShapeClone?.(info, e)
|
|
|
|
}
|
2021-10-14 15:37:52 +00:00
|
|
|
|
2021-09-11 22:17:54 +00:00
|
|
|
onRenderCountChange = (ids: string[]) => {
|
2021-08-10 16:12:55 +00:00
|
|
|
const appState = this.getAppState()
|
|
|
|
if (appState.isEmptyCanvas && ids.length > 0) {
|
2021-08-29 13:33:43 +00:00
|
|
|
this.patchState(
|
2021-08-17 21:38:37 +00:00
|
|
|
{
|
|
|
|
appState: {
|
|
|
|
isEmptyCanvas: false,
|
|
|
|
},
|
2021-08-10 16:12:55 +00:00
|
|
|
},
|
2021-08-17 21:38:37 +00:00
|
|
|
'empty_canvas:false'
|
|
|
|
)
|
2021-08-10 16:12:55 +00:00
|
|
|
} else if (!appState.isEmptyCanvas && ids.length <= 0) {
|
2021-08-29 13:33:43 +00:00
|
|
|
this.patchState(
|
2021-08-17 21:38:37 +00:00
|
|
|
{
|
|
|
|
appState: {
|
2021-09-02 12:51:39 +00:00
|
|
|
isEmptyCanvas: true,
|
2021-08-17 21:38:37 +00:00
|
|
|
},
|
2021-08-10 16:12:55 +00:00
|
|
|
},
|
2021-08-17 21:38:37 +00:00
|
|
|
'empty_canvas:true'
|
|
|
|
)
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-13 09:28:09 +00:00
|
|
|
onError = () => {
|
2021-08-10 16:12:55 +00:00
|
|
|
// TODO
|
|
|
|
}
|
2021-09-22 08:45:09 +00:00
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
isSelected(id: string) {
|
|
|
|
return this.selectedIds.includes(id)
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
get room() {
|
|
|
|
return this.state.room
|
|
|
|
}
|
|
|
|
|
2021-11-05 14:13:14 +00:00
|
|
|
get isLocal() {
|
|
|
|
return this.state.room === undefined || this.state.room.id === 'local'
|
|
|
|
}
|
|
|
|
|
2021-10-13 13:55:31 +00:00
|
|
|
get status() {
|
2021-10-14 12:32:48 +00:00
|
|
|
return this.appState.status
|
2021-10-13 13:55:31 +00:00
|
|
|
}
|
|
|
|
|
2021-10-12 14:59:04 +00:00
|
|
|
get currentUser() {
|
2021-10-16 18:55:18 +00:00
|
|
|
if (!this.state.room) return
|
2021-10-12 14:59:04 +00:00
|
|
|
return this.state.room.users[this.state.room.userId]
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
// The center of the component (in screen space)
|
2021-09-22 08:45:09 +00:00
|
|
|
get centerPoint() {
|
2021-11-16 16:01:29 +00:00
|
|
|
const { width, height } = this.rendererBounds
|
2021-11-26 15:14:10 +00:00
|
|
|
return Vec.toFixed([width / 2, height / 2])
|
|
|
|
}
|
|
|
|
|
|
|
|
get currentGrid() {
|
|
|
|
const { zoom } = this.pageState.camera
|
|
|
|
if (zoom < 0.15) {
|
|
|
|
return GRID_SIZE * 16
|
|
|
|
} else if (zoom < 1) {
|
|
|
|
return GRID_SIZE * 4
|
|
|
|
} else {
|
|
|
|
return GRID_SIZE * 1
|
|
|
|
}
|
2021-09-22 08:45:09 +00:00
|
|
|
}
|
2021-10-16 19:34:34 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
getShapeUtil = TLDR.getShapeUtil
|
2021-10-18 13:30:42 +00:00
|
|
|
|
2021-11-20 09:37:42 +00:00
|
|
|
static version = 14
|
2021-10-16 19:34:34 +00:00
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static defaultDocument: TDDocument = {
|
2021-10-16 19:34:34 +00:00
|
|
|
id: 'doc',
|
2021-11-05 14:13:14 +00:00
|
|
|
name: 'New Document',
|
2021-11-20 09:37:42 +00:00
|
|
|
version: 14,
|
2021-10-16 19:34:34 +00:00
|
|
|
pages: {
|
|
|
|
page: {
|
|
|
|
id: 'page',
|
|
|
|
name: 'Page 1',
|
|
|
|
childIndex: 1,
|
|
|
|
shapes: {},
|
|
|
|
bindings: {},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
pageStates: {
|
|
|
|
page: {
|
|
|
|
id: 'page',
|
|
|
|
selectedIds: [],
|
|
|
|
camera: {
|
|
|
|
point: [0, 0],
|
|
|
|
zoom: 1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-11-16 16:01:29 +00:00
|
|
|
static defaultState: TDSnapshot = {
|
2021-10-16 19:34:34 +00:00
|
|
|
settings: {
|
|
|
|
isPenMode: false,
|
|
|
|
isDarkMode: false,
|
|
|
|
isZoomSnap: false,
|
|
|
|
isFocusMode: false,
|
2021-10-19 13:29:55 +00:00
|
|
|
isSnapping: false,
|
2021-10-16 19:34:34 +00:00
|
|
|
isDebugMode: process.env.NODE_ENV === 'development',
|
|
|
|
isReadonlyMode: false,
|
|
|
|
nudgeDistanceLarge: 16,
|
|
|
|
nudgeDistanceSmall: 1,
|
2021-10-19 13:29:55 +00:00
|
|
|
showRotateHandles: true,
|
|
|
|
showBindingHandles: true,
|
2021-11-03 16:46:33 +00:00
|
|
|
showCloneHandles: false,
|
2021-11-26 15:14:10 +00:00
|
|
|
showGrid: false,
|
2021-10-16 19:34:34 +00:00
|
|
|
},
|
|
|
|
appState: {
|
2021-11-20 09:37:42 +00:00
|
|
|
status: TDStatus.Idle,
|
2021-10-16 19:34:34 +00:00
|
|
|
activeTool: 'select',
|
|
|
|
hoveredId: undefined,
|
|
|
|
currentPageId: 'page',
|
|
|
|
currentStyle: defaultStyle,
|
|
|
|
isToolLocked: false,
|
|
|
|
isStyleOpen: false,
|
|
|
|
isEmptyCanvas: false,
|
2021-10-18 13:30:42 +00:00
|
|
|
snapLines: [],
|
2021-10-16 19:34:34 +00:00
|
|
|
},
|
2021-11-16 16:01:29 +00:00
|
|
|
document: TldrawApp.defaultDocument,
|
2021-10-16 19:34:34 +00:00
|
|
|
}
|
2021-08-10 16:12:55 +00:00
|
|
|
}
|