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-12-25 17:06:33 +00:00
TLDropEventHandler ,
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-12-25 18:39:50 +00:00
TDAssetType ,
TDAsset ,
2022-01-10 16:36:28 +00:00
TDExportTypes ,
TDAssets ,
TDExport ,
ImageShape ,
2022-01-30 21:13:57 +00:00
ArrowShape ,
2021-08-13 09:28:09 +00:00
} from '~types'
2021-11-06 11:16:30 +00:00
import {
migrate ,
FileSystemHandle ,
loadFileHandle ,
openFromFileSystem ,
saveToFileSystem ,
2021-12-25 17:06:33 +00:00
openAssetFromFileSystem ,
fileToBase64 ,
2022-01-14 19:17:28 +00:00
getImageSizeFromSrc ,
getVideoSizeFromSrc ,
2021-11-06 11:16:30 +00:00
} 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-12-25 17:06:33 +00:00
import {
USER_COLORS ,
FIT_TO_SCREEN_PADDING ,
GRID_SIZE ,
IMAGE_EXTENSIONS ,
VIDEO_EXTENSIONS ,
SVG_EXPORT_PADDING ,
} from '~constants'
2022-01-10 16:36:28 +00:00
import type { BaseTool } from './tools/BaseTool'
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-12-09 22:29:09 +00:00
import { TriangleTool } from './tools/TriangleTool'
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'
2022-01-30 21:13:57 +00:00
import { deepCopy } from './StateManager/copy'
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-12-25 17:06:33 +00:00
/ * *
* ( optional ) A callback to run when the opens a file to upload .
* /
onOpenMedia ? : ( state : TldrawApp ) = > 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 > ,
2022-01-10 15:13:52 +00:00
bindings : Record < string , TDBinding | undefined > ,
assets : Record < string , TDAsset | undefined >
2021-11-22 14:00:24 +00:00
) = > void
/ * *
* ( optional ) A callback to run when the user creates a new project .
* /
onChangePresence ? : ( state : TldrawApp , user : TDUser ) = > void
2022-01-10 15:13:52 +00:00
/ * *
* ( optional ) A callback to run when an asset will be deleted .
* /
onAssetDelete ? : ( assetId : string ) = > void
/ * *
* ( optional ) A callback to run when an asset will be created . Should return the value for the image / video ' s ` src ` property .
* /
onAssetCreate ? : ( file : File , id : string ) = > Promise < string | false >
2022-01-10 16:36:28 +00:00
/ * *
* ( optional ) A callback to run when the user exports their page or selection .
* /
onExport ? : ( info : TDExport ) = > Promise < 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-12-09 22:29:09 +00:00
[ TDShapeType . Triangle ] : new TriangleTool ( 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
2022-01-05 14:47:07 +00:00
isPointing = false
isForcePanning = false
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-12-25 18:39:50 +00:00
assets : TDAsset [ ]
2021-11-16 16:01:29 +00:00
}
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 = > {
2022-01-18 10:00:58 +00:00
const next : TDSnapshot = { . . . state }
2021-11-18 18:18:30 +00:00
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-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 ]
2022-01-30 21:13:57 +00:00
const fromShape = page . shapes [ binding . fromId ] as ArrowShape
2021-09-24 13:27:22 +00:00
2021-12-27 18:44:47 +00:00
if ( ! ( toShape && fromShape ) ) {
delete next . document . pages [ pageId ] . bindings [ binding . id ]
return
}
2021-08-11 12:26:34 +00:00
2022-01-30 21:13:57 +00:00
// We only need to update the binding's "from" shape (an arrow)
const fromDelta = TLDR . updateArrowBindings ( page , fromShape )
2021-10-13 13:55:31 +00:00
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-11-28 22:00:01 +00:00
TLDR . warn ( ` Could not find the binding of ${ 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-11-28 22:00:01 +00:00
TLDR . warn ( 'Could not find the editing shape!' )
2021-08-16 14:01:03 +00:00
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
}
2022-01-19 12:33:57 +00:00
Object . keys ( next . document . assets ? ? { } ) . forEach ( ( id ) = > {
if ( ! next . document . assets ? . [ id ] ) {
delete next . document . assets ? . [ id ]
2022-01-10 15:13:52 +00:00
}
} )
2021-12-25 17:06:33 +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-12-02 12:49:07 +00:00
// If we are part of a room, send our changes to the server
if ( this . callbacks . onChangePage ) {
this . broadcastPageChanges ( )
}
2021-12-04 14:51:40 +00:00
this . callbacks . onPersist ? . ( this )
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 ----------- */
2021-12-02 12:49:07 +00:00
private justSent = false
2021-11-22 14:00:24 +00:00
private prevShapes = this . page . shapes
private prevBindings = this . page . bindings
2022-01-10 15:13:52 +00:00
private prevAssets = this . document . assets
2021-11-22 14:00:24 +00:00
private broadcastPageChanges = ( ) = > {
const visited = new Set < string > ( )
const changedShapes : Record < string , TDShape | undefined > = { }
const changedBindings : Record < string , TDBinding | undefined > = { }
2022-01-10 15:13:52 +00:00
const changedAssets : Record < string , TDAsset | undefined > = { }
2021-11-22 14:00:24 +00:00
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 ) = > {
2021-12-02 12:49:07 +00:00
// After visiting all the current shapes, if we haven't visited a
// previously present shape, then it was deleted
2021-11-22 14:00:24 +00:00
changedShapes [ id ] = undefined
} )
this . bindings . forEach ( ( binding ) = > {
visited . add ( binding . id )
if ( this . prevBindings [ binding . id ] !== binding ) {
changedBindings [ binding . id ] = binding
}
} )
2021-12-01 13:48:37 +00:00
Object . keys ( this . prevBindings )
2021-11-22 14:00:24 +00:00
. filter ( ( id ) = > ! visited . has ( id ) )
. forEach ( ( id ) = > {
2021-12-02 12:49:07 +00:00
// After visiting all the current bindings, if we haven't visited a
// previously present shape, then it was deleted
2021-11-22 14:00:24 +00:00
changedBindings [ id ] = undefined
} )
2022-01-10 15:13:52 +00:00
this . assets . forEach ( ( asset ) = > {
visited . add ( asset . id )
if ( this . prevAssets [ asset . id ] !== asset ) {
changedAssets [ asset . id ] = asset
}
} )
Object . keys ( this . prevAssets )
. filter ( ( id ) = > ! visited . has ( id ) )
. forEach ( ( id ) = > {
changedAssets [ id ] = undefined
} )
2021-12-15 21:14:40 +00:00
// Only trigger update if shapes or bindings have changed
2022-01-10 15:13:52 +00:00
if (
Object . keys ( changedBindings ) . length > 0 ||
Object . keys ( changedShapes ) . length > 0 ||
Object . keys ( changedAssets ) . length > 0
) {
2021-12-15 21:14:40 +00:00
this . justSent = true
2022-01-10 15:13:52 +00:00
this . callbacks . onChangePage ? . ( this , changedShapes , changedBindings , changedAssets )
2021-12-15 21:14:40 +00:00
this . prevShapes = this . page . shapes
this . prevBindings = this . page . bindings
2022-01-10 15:13:52 +00:00
this . prevAssets = this . document . assets
2021-12-15 21:14:40 +00:00
}
2021-09-02 12:51:39 +00:00
}
2021-12-02 12:49:07 +00:00
getReservedContent = ( ids : string [ ] , pageId = this . currentPageId ) = > {
const { bindings } = this . document . pages [ pageId ]
// We want to know which shapes we need to
const reservedShapes : Record < string , TDShape > = { }
const reservedBindings : Record < string , TDBinding > = { }
// Quick lookup maps for bindings
const bindingsArr = Object . values ( bindings )
const boundTos = new Map ( bindingsArr . map ( ( binding ) = > [ binding . toId , binding ] ) )
const boundFroms = new Map ( bindingsArr . map ( ( binding ) = > [ binding . fromId , binding ] ) )
const bindingMaps = [ boundTos , boundFroms ]
// Unique set of shape ids that are going to be reserved
const reservedShapeIds : string [ ] = [ ]
if ( this . session ) ids . forEach ( ( id ) = > reservedShapeIds . push ( id ) )
const strongReservedShapeIds = new Set ( reservedShapeIds )
// Which shape ids have we already visited?
const visited = new Set < string > ( )
// Time to visit every reserved shape and every related shape and binding.
while ( reservedShapeIds . length > 0 ) {
const id = reservedShapeIds . pop ( )
if ( ! id ) break
if ( visited . has ( id ) ) continue
// Add to set so that we don't process this id a second time
visited . add ( id )
// Get the shape and reserve it
const shape = this . getShape ( id )
reservedShapes [ id ] = shape
if ( shape . parentId !== pageId ) reservedShapeIds . push ( shape . parentId )
// If the shape has children, add the shape's children to the list of ids to process
if ( shape . children ) reservedShapeIds . push ( . . . shape . children )
// If there are binding for this shape, reserve the bindings and
// add its related shapes to the list of ids to process
bindingMaps
. map ( ( map ) = > map . get ( shape . id ) ! )
. filter ( Boolean )
. forEach ( ( binding ) = > {
reservedBindings [ binding . id ] = binding
reservedShapeIds . push ( binding . toId , binding . fromId )
} )
}
return { reservedShapes , reservedBindings , strongReservedShapeIds }
}
2021-11-22 14:00:24 +00:00
/ * *
2021-12-13 09:08:35 +00:00
* Manually patch in page content .
2021-11-22 14:00:24 +00:00
* /
public replacePageContent = (
shapes : Record < string , TDShape > ,
bindings : Record < string , TDBinding > ,
2022-01-10 15:13:52 +00:00
assets : Record < string , TDAsset > ,
2021-11-22 14:00:24 +00:00
pageId = this . currentPageId
) : this = > {
2021-12-02 12:49:07 +00:00
if ( this . justSent ) {
2021-12-13 09:08:35 +00:00
// The incoming update was caused by an update that the client sent, noop.
2021-12-02 12:49:07 +00:00
this . justSent = false
return this
}
2021-11-22 14:00:24 +00:00
this . useStore . setState ( ( current ) = > {
const { hoveredId , editingId , bindingId , selectedIds } = current . document . pageStates [ pageId ]
2021-12-02 12:49:07 +00:00
const coreReservedIds = [ . . . selectedIds ]
if ( editingId ) coreReservedIds . push ( editingId )
const { reservedShapes , reservedBindings , strongReservedShapeIds } = this . getReservedContent (
coreReservedIds ,
this . currentPageId
)
// Merge in certain changes to reserved shapes
Object . values ( reservedShapes )
// Don't merge updates to shapes with text (Text or Sticky)
. filter ( ( reservedShape ) = > ! ( 'text' in reservedShape ) )
. forEach ( ( reservedShape ) = > {
const incomingShape = shapes [ reservedShape . id ]
if ( ! incomingShape ) return
// If the shape isn't "strongly reserved", then use the incoming shape;
// note that this is only if the incoming shape exists! If the shape was
// deleted in the incoming shapes, then we'll keep out reserved shape.
// This logic would need more work for arrows, because the incoming shape
// include a binding change that we'll need to resolve with our reserved bindings.
if (
! (
reservedShape . type === TDShapeType . Arrow ||
strongReservedShapeIds . has ( reservedShape . id )
)
) {
reservedShapes [ reservedShape . id ] = incomingShape
return
}
// Only allow certain merges.
// Allow decorations (of an arrow) to be changed
if ( 'decorations' in incomingShape && 'decorations' in reservedShape ) {
reservedShape . decorations = incomingShape . decorations
}
// Allow the shape's style to be changed
reservedShape . style = incomingShape . style
} )
// Use the incoming shapes / bindings as comparisons for what
// will have changed. This is important because we want to restore
// related shapes that may not have changed on our side, but which
// were deleted on the server.
this . prevShapes = shapes
this . prevBindings = bindings
2022-01-10 15:13:52 +00:00
this . prevAssets = assets
2021-12-02 12:49:07 +00:00
const nextShapes = {
. . . shapes ,
. . . reservedShapes ,
2021-12-01 22:31:19 +00:00
}
2021-12-02 12:49:07 +00:00
const nextBindings = {
. . . bindings ,
. . . reservedBindings ,
2021-12-01 22:31:19 +00:00
}
2022-01-10 15:13:52 +00:00
const nextAssets = {
. . . assets ,
}
2021-12-01 22:31:19 +00:00
const next : TDSnapshot = {
2021-11-22 14:00:24 +00:00
. . . current ,
document : {
. . . current . document ,
pages : {
[ pageId ] : {
. . . current . document . pages [ pageId ] ,
2021-12-02 12:49:07 +00:00
shapes : nextShapes ,
bindings : nextBindings ,
2021-11-22 14:00:24 +00:00
} ,
} ,
2022-01-10 15:13:52 +00:00
assets : nextAssets ,
2021-11-22 14:00:24 +00:00
pageStates : {
. . . current . document . pageStates ,
[ pageId ] : {
. . . current . document . pageStates [ pageId ] ,
2021-12-02 12:49:07 +00:00
selectedIds : selectedIds.filter ( ( id ) = > nextShapes [ id ] !== undefined ) ,
2021-11-22 14:00:24 +00:00
hoveredId : hoveredId
2021-12-02 12:49:07 +00:00
? nextShapes [ hoveredId ] === undefined
2021-11-22 14:00:24 +00:00
? undefined
: hoveredId
: undefined ,
2021-12-01 22:31:19 +00:00
editingId : editingId ,
2021-11-22 14:00:24 +00:00
bindingId : bindingId
2021-12-02 12:49:07 +00:00
? nextBindings [ bindingId ] === undefined
2021-11-22 14:00:24 +00:00
? undefined
: bindingId
: undefined ,
} ,
} ,
} ,
}
2021-12-02 12:49:07 +00:00
// Get bindings related to the changed shapes
const bindingsToUpdate = TLDR . getRelatedBindings ( next , Object . keys ( nextShapes ) , pageId )
const page = next . document . pages [ pageId ]
// Update all of the bindings we've just collected
bindingsToUpdate . forEach ( ( binding ) = > {
if ( ! page . bindings [ binding . id ] ) {
return
}
2022-01-30 21:13:57 +00:00
const fromShape = page . shapes [ binding . fromId ] as ArrowShape
2021-12-02 12:49:07 +00:00
2022-01-30 21:13:57 +00:00
// We only need to update the binding's "from" shape (an arrow)
const fromDelta = TLDR . updateArrowBindings ( page , fromShape )
2021-12-02 12:49:07 +00:00
if ( fromDelta ) {
const nextShape = {
. . . fromShape ,
. . . fromDelta ,
} as TDShape
page . shapes [ fromShape . id ] = nextShape
}
} )
Object . values ( nextShapes ) . forEach ( ( shape ) = > {
if ( shape . type !== TDShapeType . Group ) return
const children = shape . children . filter ( ( id ) = > page . shapes [ id ] !== undefined )
const commonBounds = Utils . getCommonBounds (
children
. map ( ( id ) = > page . shapes [ id ] )
. filter ( Boolean )
. map ( ( shape ) = > TLDR . getRotatedBounds ( shape ) )
)
page . shapes [ shape . id ] = {
. . . shape ,
point : [ commonBounds . minX , commonBounds . minY ] ,
size : [ commonBounds . width , commonBounds . height ] ,
children ,
}
} )
2021-11-22 14:00:24 +00:00
this . state . document = next . document
2021-12-02 12:49:07 +00:00
// this.prevShapes = nextShapes
// this.prevBindings = nextBindings
2021-11-22 14:00:24 +00:00
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-12-27 12:30:35 +00:00
this . editingStartTime = performance . 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
/ * *
2021-12-06 18:23:53 +00:00
* Toggles the state if menu is opened
2021-09-02 12:51:39 +00:00
* /
2021-12-06 18:23:53 +00:00
setMenuOpen = ( isOpen : boolean ) : this = > {
this . patchState ( { appState : { isMenuOpen : isOpen } } , 'ui:toggled_menu_opened' )
2021-08-30 13:04:12 +00:00
this . persist ( )
return this
2021-08-10 16:12:55 +00:00
}
2021-12-25 17:06:33 +00:00
/ * *
* Toggles the state if something is loading
* /
setIsLoading = ( isLoading : boolean ) : this = > {
this . patchState ( { appState : { isLoading } } , 'ui:toggled_is_loading' )
this . persist ( )
return this
}
setDisableAssets = ( disableAssets : boolean ) : this = > {
this . patchState ( { appState : { disableAssets } } , 'ui:toggled_disable_images' )
return this
}
get isMenuOpen ( ) : boolean {
return this . appState . isMenuOpen
}
get isLoading ( ) : boolean {
return this . appState . isLoading
}
get disableAssets ( ) : boolean {
return this . appState . disableAssets
}
2021-12-06 18:23:53 +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
2022-01-05 14:47:07 +00:00
this . isPointing = false // reset pointer state, in case something weird happened
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-12-25 17:06:33 +00:00
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 ,
2022-01-10 21:09:26 +00:00
settings : {
. . . this . state . settings ,
} ,
2021-11-16 16:01:29 +00:00
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 ,
2022-01-10 21:09:26 +00:00
. . . this . state . appState ,
2021-09-08 11:09:03 +00:00
currentPageId : Object.keys ( document . pages ) [ 0 ] ,
2021-12-25 17:06:33 +00:00
disableAssets : this.disableAssets ,
2021-09-08 11:09:03 +00:00
} ,
} ,
'loaded_document'
)
2022-01-14 20:57:54 +00:00
const { point , zoom } = this . pageState . camera
this . updateViewport ( point , zoom )
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 {
2021-12-25 17:06:33 +00:00
const fileHandle = await saveToFileSystem (
migrate ( this . document , TldrawApp . version ) ,
this . fileSystemHandle
)
2021-11-05 14:13:14 +00:00
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-12-25 17:06:33 +00:00
/ * *
* Upload media from file
* /
openAsset = async ( ) = > {
if ( ! this . disableAssets )
try {
const file = await openAssetFromFileSystem ( )
if ( ! file ) return
this . addMediaFromFile ( file )
} catch ( e ) {
console . error ( e )
} finally {
this . persist ( )
}
}
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
2022-01-10 15:13:52 +00:00
/ * *
* The document ' s assets ( as an array ) .
* /
get assets ( ) : TDAsset [ ] {
return Object . values ( this . document . assets )
}
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 )
)
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-12-25 17:06:33 +00:00
const copyingAssets = copyingShapes
. map ( ( shape ) = > {
if ( ! shape . assetId ) return
2022-01-10 16:36:28 +00:00
2021-12-25 17:06:33 +00:00
return this . document . assets [ shape . assetId ]
} )
2021-12-25 18:39:50 +00:00
. filter ( Boolean ) as TDAsset [ ]
2021-09-24 12:47:11 +00:00
this . clipboard = {
shapes : copyingShapes ,
bindings : copyingBindings ,
2021-12-25 17:06:33 +00:00
assets : copyingAssets ,
2021-09-24 12:47:11 +00:00
}
2021-09-21 15:47:04 +00:00
try {
2021-09-24 12:47:11 +00:00
const text = JSON . stringify ( {
type : 'tldr/clipboard' ,
2021-12-25 17:06:33 +00:00
. . . this . clipboard ,
2021-09-24 12:47:11 +00:00
} )
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-12-25 18:39:50 +00:00
const pasteInCurrentPage = ( shapes : TDShape [ ] , bindings : TDBinding [ ] , assets : TDAsset [ ] ) = > {
2021-09-24 12:47:11 +00:00
const idsMap : Record < string , string > = { }
2021-12-25 17:06:33 +00:00
const newAssets = assets . filter ( ( asset ) = > this . document . assets [ asset . id ] === undefined )
if ( newAssets . length ) {
this . patchState ( {
document : {
assets : Object.fromEntries ( newAssets . map ( ( asset ) = > [ asset . id , asset ] ) ) ,
} ,
} )
}
2021-09-24 12:47:11 +00:00
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 ]
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
} ) )
const commonBounds = Utils . getCommonBounds ( shapesToPaste . map ( TLDR . getBounds ) )
2021-11-26 15:14:10 +00:00
let center = Vec . toFixed ( this . getPagePoint ( point || this . centerPoint ) )
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 )
const delta = Vec . sub (
Utils . getBoundsCenter ( centeredBounds ) ,
Utils . getBoundsCenter ( commonBounds )
)
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
)
}
2022-01-06 10:45:11 +00:00
if ( ! ( 'clipboard' in navigator && navigator . clipboard . readText ) ) {
TLDR . warn ( 'This browser does not support the Clipboard API!' )
if ( this . clipboard ) {
pasteInCurrentPage ( this . clipboard . shapes , this . clipboard . bindings , this . clipboard . assets )
2021-09-22 15:00:20 +00:00
}
2022-01-06 10:45:11 +00:00
return
}
2021-09-22 15:00:20 +00:00
2022-01-06 10:45:11 +00:00
navigator . clipboard
. readText ( )
. then ( ( result ) = > {
const data : {
type : string
shapes : TDShape [ ]
bindings : TDBinding [ ]
assets : TDAsset [ ]
} = JSON . parse ( result )
if ( data . type === 'tldr/clipboard' ) {
2021-12-25 17:06:33 +00:00
pasteInCurrentPage ( data . shapes , data . bindings , data . assets )
2022-01-06 10:45:11 +00:00
} else {
TLDR . warn ( 'The selected shape was not a tldraw shape, treating as text.' )
2021-09-21 15:47:04 +00:00
const shapeId = Utils . uniqueId ( )
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 } ,
} )
this . select ( shapeId )
}
} )
2022-01-06 10:45:11 +00:00
. catch ( ( ) = > {
TLDR . warn ( 'Read permissions denied!' )
if ( this . clipboard ) {
pasteInCurrentPage ( this . clipboard . shapes , this . clipboard . bindings , this . clipboard . assets )
}
} )
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-22 16:15:51 +00:00
const svg = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'svg' )
2021-12-25 17:06:33 +00:00
// Embed our custom fonts
2021-11-22 16:15:51 +00:00
const defs = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'defs' )
const style = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'style' )
2021-12-28 11:23:17 +00:00
style . textContent = ` @import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block'); `
2021-11-22 16:15:51 +00:00
defs . appendChild ( style )
svg . appendChild ( defs )
2021-12-25 17:06:33 +00:00
// Get the shapes in order
const shapes = ids
. map ( ( id ) = > this . getShape ( id , pageId ) )
. sort ( ( a , b ) = > a . childIndex - b . childIndex )
// Find their common bounding box. S hapes will be positioned relative to this box
const commonBounds = Utils . getCommonBounds ( shapes . map ( TLDR . getRotatedBounds ) )
// A quick routine to get an SVG element for each shape
const getSvgElementForShape = ( shape : TDShape ) = > {
2021-11-22 16:15:51 +00:00
const util = TLDR . getShapeUtil ( shape )
const bounds = util . getBounds ( shape )
2021-12-25 17:06:33 +00:00
const elm = util . getSvgElement ( shape )
if ( ! elm ) return
2022-01-10 16:36:28 +00:00
2021-12-25 17:06:33 +00:00
// If the element is an image, set the asset src as the xlinkhref
if ( shape . type === TDShapeType . Image ) {
elm . setAttribute ( 'xlink:href' , this . document . assets [ shape . assetId ] . src )
2022-01-10 16:36:28 +00:00
} else if ( shape . type === TDShapeType . Video ) {
elm . setAttribute ( 'xlink:href' , this . serializeVideo ( shape . id ) )
2021-12-25 17:06:33 +00:00
}
// Put the element in the correct position relative to the common bounds
elm . setAttribute (
2021-09-23 09:48:08 +00:00
'transform' ,
2021-12-25 17:06:33 +00:00
` translate( ${ SVG_EXPORT_PADDING + shape . point [ 0 ] - commonBounds . minX } , ${
SVG_EXPORT_PADDING + shape . point [ 1 ] - commonBounds . minY
2021-11-22 16:15:51 +00:00
} ) rotate ( $ { ( ( shape . rotation || 0 ) * 180 ) / Math . PI } , $ { bounds . width / 2 } , $ {
bounds . height / 2
} ) `
2021-09-23 09:48:08 +00:00
)
2021-12-25 17:06:33 +00:00
return elm
2021-09-23 09:48:08 +00:00
}
2021-12-25 17:06:33 +00:00
// Assemble the final SVG by iterating through each shape and its children
2021-09-23 09:48:08 +00:00
shapes . forEach ( ( shape ) = > {
2021-12-25 17:06:33 +00:00
// The shape is a group! Just add the children.
2021-09-23 09:48:08 +00:00
if ( shape . children ? . length ) {
2021-12-25 17:06:33 +00:00
// Create a group <g> elm for shape
2021-09-23 09:48:08 +00:00
const g = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'g' )
2021-12-25 17:06:33 +00:00
// Get the shape's children as elms and add them to the group
shape . children . forEach ( ( childId ) = > {
const shape = this . getShape ( childId , pageId )
const elm = getSvgElementForShape ( shape )
if ( elm ) g . appendChild ( elm )
} )
// Add the group elm to the SVG
2021-09-23 09:48:08 +00:00
svg . appendChild ( g )
return
}
2021-12-25 17:06:33 +00:00
// Just add the shape's element to the
const elm = getSvgElementForShape ( shape )
if ( elm ) svg . appendChild ( elm )
2021-09-01 08:57:46 +00:00
} )
2021-12-25 17:06:33 +00:00
// Resize the elm to the bounding box
2021-09-01 08:57:46 +00:00
svg . setAttribute (
'viewBox' ,
2021-12-25 17:06:33 +00:00
[
0 ,
0 ,
commonBounds . width + SVG_EXPORT_PADDING * 2 ,
commonBounds . height + SVG_EXPORT_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-12-17 15:23:03 +00:00
svg . setAttribute ( 'fill' , 'transparent' )
2021-12-25 17:06:33 +00:00
// Clean up the SVG by removing any hidden elements
2021-12-17 15:23:03 +00:00
svg
. querySelectorAll ( '.tl-fill-hitarea, .tl-stroke-hitarea, .tl-binding-indicator' )
2021-12-25 17:06:33 +00:00
. forEach ( ( elm ) = > elm . remove ( ) )
// Serialize the SVG to a string
const svgString = new XMLSerializer ( )
2021-09-01 08:57:46 +00:00
. serializeToString ( svg )
. replaceAll ( ' ' , '' )
. replaceAll ( /((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g , '$1' )
2021-12-25 17:06:33 +00:00
// Copy the string to the clipboard
2021-09-01 08:57:46 +00:00
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 = > {
2022-01-10 16:36:28 +00:00
const {
shapes ,
pageState : { camera } ,
} = this
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-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
)
2022-01-10 16:36:28 +00:00
zoom = camera . zoom === zoom || 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-28 22:00:01 +00:00
TLDR . warn ( ` Already in a session! ( ${ this . session . constructor . name } ) ` )
2021-11-18 16:38:49 +00:00
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-12-25 17:06:33 +00:00
createImageOrVideoShapeAtPoint (
id : string ,
type : TDShapeType . Image | TDShapeType . Video ,
point : number [ ] ,
size : number [ ] ,
assetId : string
) : this {
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
const Shape = shapeUtils [ type ]
const newShape = Shape . create ( {
id ,
parentId : currentPageId ,
childIndex ,
point ,
size ,
style : { . . . currentStyle } ,
assetId ,
} )
const bounds = Shape . getBounds ( newShape as never )
newShape . point = Vec . sub ( newShape . point , [ bounds . width / 2 , bounds . height / 2 ] )
this . createShapes ( newShape )
return this
}
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
2022-01-16 10:00:46 +00:00
const drawCommand = Commands . deleteShapes ( this , ids )
if (
this . callbacks . onAssetDelete &&
drawCommand . before . document ? . assets &&
drawCommand . after . document ? . assets
) {
const beforeAssetIds = Object . keys ( drawCommand . before . document . assets ) . filter (
( k ) = > ! ! drawCommand . before . document ! . assets ! [ k ]
)
const afterAssetIds = Object . keys ( drawCommand . after . document . assets ) . filter (
( k ) = > ! ! drawCommand . after . document ! . assets ! [ k ]
)
const intersection = beforeAssetIds . filter ( ( x ) = > ! afterAssetIds . includes ( x ) )
intersection . forEach ( ( id ) = > this . callbacks . onAssetDelete ! ( id ) )
}
return this . setState ( drawCommand )
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-12-25 17:06:33 +00:00
private addMediaFromFile = async ( file : File , point = this . centerPoint ) = > {
this . setIsLoading ( true )
const id = Utils . uniqueId ( )
2022-01-10 15:13:52 +00:00
const pagePoint = this . getPagePoint ( point )
const extension = file . name . match ( /\.[0-9a-z]+$/i )
if ( ! extension ) throw Error ( 'No extension' )
const isImage = IMAGE_EXTENSIONS . includes ( extension [ 0 ] . toLowerCase ( ) )
const isVideo = VIDEO_EXTENSIONS . includes ( extension [ 0 ] . toLowerCase ( ) )
if ( ! ( isImage || isVideo ) ) throw Error ( 'Wrong extension' )
const shapeType = isImage ? TDShapeType.Image : TDShapeType.Video
const assetType = isImage ? TDAssetType.Image : TDAssetType.Video
let src : string | ArrayBuffer | null
2021-12-25 17:06:33 +00:00
try {
2022-01-10 15:13:52 +00:00
if ( this . callbacks . onAssetCreate ) {
const result = await this . callbacks . onAssetCreate ( file , id )
if ( ! result ) throw Error ( 'Asset creation callback returned false' )
src = result
} else {
src = await fileToBase64 ( file )
}
if ( typeof src === 'string' ) {
2022-01-14 19:17:28 +00:00
const size = isImage ? await getImageSizeFromSrc ( src ) : await getVideoSizeFromSrc ( src )
2021-12-25 17:06:33 +00:00
const match = Object . values ( this . document . assets ) . find (
2022-01-10 15:13:52 +00:00
( asset ) = > asset . type === assetType && asset . src === src
2021-12-25 17:06:33 +00:00
)
2022-01-10 15:13:52 +00:00
let assetId : string
2021-12-25 17:06:33 +00:00
if ( ! match ) {
2022-01-10 15:13:52 +00:00
assetId = Utils . uniqueId ( )
const asset = {
id : assetId ,
type : assetType ,
src ,
size ,
}
2021-12-25 17:06:33 +00:00
this . patchState ( {
document : {
assets : {
2022-01-10 15:13:52 +00:00
[ assetId ] : asset ,
2021-12-25 17:06:33 +00:00
} ,
} ,
} )
} else assetId = match . id
this . createImageOrVideoShapeAtPoint ( id , shapeType , pagePoint , size , assetId )
}
} catch ( error ) {
2022-01-10 15:13:52 +00:00
console . warn ( error )
2021-12-25 17:06:33 +00:00
this . setIsLoading ( false )
return this
}
2022-01-10 15:13:52 +00:00
2021-12-25 17:06:33 +00:00
this . setIsLoading ( false )
return this
}
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 '/' : {
2022-01-14 20:35:11 +00:00
if ( this . status === 'idle' && ! this . pageState . editingId ) {
2021-11-24 18:07:31 +00:00
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 ' ' : {
2022-01-05 14:47:07 +00:00
this . isForcePanning = true
2021-11-16 16:01:29 +00:00
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 ' ' : {
2022-01-05 14:47:07 +00:00
this . isForcePanning = false
2021-11-16 16:01:29 +00:00
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-12-25 17:06:33 +00:00
onDragOver : TLDropEventHandler = ( e ) = > {
e . preventDefault ( )
}
onDrop : TLDropEventHandler = async ( e ) = > {
e . preventDefault ( )
if ( this . disableAssets ) return this
if ( e . dataTransfer . files ? . length ) {
const file = e . dataTransfer . files [ 0 ]
this . addMediaFromFile ( file , [ e . clientX , e . clientY ] )
}
return this
}
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
2022-01-05 14:47:07 +00:00
// When panning, we also want to call onPointerMove, except when "force panning" via spacebar / middle wheel button (it's called elsewhere in that case)
if ( ! this . isForcePanning ) 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
2021-12-01 14:25:56 +00:00
this . zoomBy ( delta , this . centerPoint )
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 ) = > {
2022-01-31 12:25:51 +00:00
this . currentPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 )
2022-01-05 14:47:07 +00:00
if ( this . isForcePanning && this . isPointing ) {
this . onPan ? . ( { . . . info , delta : Vec.neg ( info . delta ) } , e as unknown as WheelEvent )
return
}
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 ) = > {
2022-01-05 14:47:07 +00:00
if ( e . buttons === 4 ) {
this . isForcePanning = true
} else if ( this . isPointing ) {
return
}
this . isPointing = true
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
this . updateInputs ( info , e )
2022-01-05 14:47:07 +00:00
if ( this . isForcePanning ) return
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-05 14:47:07 +00:00
this . isPointing = false
if ( ! this . shiftKey ) this . isForcePanning = false
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
this . updateInputs ( info , e )
this . currentTool . onDoubleClickBoundsHandle ? . ( info , e )
2021-12-25 17:06:33 +00:00
// hack time to reset the size / clipping of an image
if ( this . selectedIds . length !== 1 ) return
const shape = this . getShape ( this . selectedIds [ 0 ] )
if ( shape . type === TDShapeType . Image || shape . type === TDShapeType . Video ) {
const asset = this . document . assets [ shape . assetId ]
const util = TLDR . getShapeUtil ( shape )
const centerA = util . getCenter ( shape )
const centerB = util . getCenter ( { . . . shape , size : asset.size } )
const delta = Vec . sub ( centerB , centerA )
this . updateShapes ( {
id : shape.id ,
point : Vec.sub ( shape . point , delta ) ,
size : asset.size ,
} )
}
2021-11-16 16:01:29 +00:00
}
2021-08-10 16:12:55 +00:00
2021-11-16 16:01:29 +00:00
onRightPointBoundsHandle : TLBoundsHandleEventHandler = ( info , e ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-16 16:01:29 +00:00
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
2021-12-27 12:30:35 +00:00
if ( performance . now ( ) - this . editingStartTime < 50 ) return
2021-11-09 14:26:41 +00:00
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 ) = > {
2022-01-31 12:25:51 +00:00
this . originPoint = this . getPagePoint ( info . point ) . concat ( info . pressure )
2021-11-26 17:12:27 +00:00
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 )
}
2022-01-10 16:36:28 +00:00
/* ----------------- Export ----------------- */
/ * *
* Get a snapshot of a video at current frame as base64 encoded image
* @param id ID of video shape
* @returns base64 encoded frame
* @throws Error if video shape with given ID does not exist
* /
serializeVideo ( id : string ) : string {
const video = document . getElementById ( id + '_video' ) as HTMLVideoElement
if ( video ) {
const canvas = document . createElement ( 'canvas' )
canvas . width = video . videoWidth
canvas . height = video . videoHeight
2022-01-19 12:33:57 +00:00
canvas . getContext ( '2d' ) ! . drawImage ( video , 0 , 0 )
2022-01-10 16:36:28 +00:00
return canvas . toDataURL ( 'image/png' )
} else throw new Error ( 'Video with id ' + id + ' not found' )
}
2022-01-19 12:33:57 +00:00
/ * *
* Get a snapshot of a image ( e . g . a GIF ) as base64 encoded image
* @param id ID of image shape
* @returns base64 encoded frame
* @throws Error if image shape with given ID does not exist
* /
serializeImage ( id : string ) : string {
const image = document . getElementById ( id + '_image' ) as HTMLImageElement
if ( image ) {
const canvas = document . createElement ( 'canvas' )
canvas . width = image . width
canvas . height = image . height
canvas . getContext ( '2d' ) ! . drawImage ( image , 0 , 0 )
return canvas . toDataURL ( 'image/png' )
} else throw new Error ( 'Image with id ' + id + ' not found' )
}
2022-01-10 16:36:28 +00:00
patchAssets ( assets : TDAssets ) {
this . document . assets = {
. . . this . document . assets ,
. . . assets ,
}
}
async exportAllShapesAs ( type : TDExportTypes ) {
const initialSelectedIds = [ . . . this . selectedIds ]
this . selectAll ( )
const { width , height } = Utils . expandBounds ( TLDR . getSelectedBounds ( this . state ) , 64 )
const allIds = [ . . . this . selectedIds ]
this . setSelectedIds ( initialSelectedIds )
await this . exportShapesAs ( allIds , [ width , height ] , type )
}
async exportSelectedShapesAs ( type : TDExportTypes ) {
const { width , height } = Utils . expandBounds ( TLDR . getSelectedBounds ( this . state ) , 64 )
await this . exportShapesAs ( this . selectedIds , [ width , height ] , type )
}
async exportShapesAs ( shapeIds : string [ ] , size : number [ ] , type : TDExportTypes ) {
2022-01-19 12:33:57 +00:00
if ( ! this . callbacks . onExport ) return
2022-01-10 16:36:28 +00:00
this . setIsLoading ( true )
2022-01-19 12:33:57 +00:00
try {
const assets : TDAssets = { }
2022-01-10 16:36:28 +00:00
2022-01-19 12:33:57 +00:00
const shapes : TDShape [ ] = shapeIds . map ( ( id ) = > {
const shape = { . . . this . getShape ( id ) }
2022-01-10 16:36:28 +00:00
2022-01-19 12:33:57 +00:00
if ( shape . assetId ) {
const asset = { . . . this . document . assets [ shape . assetId ] }
2022-01-10 16:36:28 +00:00
2022-01-19 12:33:57 +00:00
// If the asset is a GIF, then serialize an image
if ( asset . src . toLowerCase ( ) . endsWith ( 'gif' ) ) {
asset . src = this . serializeImage ( shape . id )
}
// If the asset is an image, then serialize an image
if ( shape . type === TDShapeType . Video ) {
asset . src = this . serializeVideo ( shape . id )
asset . type = TDAssetType . Image
// Cast shape to image shapes to properly display snapshots
; ( shape as unknown as ImageShape ) . type = TDShapeType . Image
}
// Patch asset table
assets [ shape . assetId ] = asset
}
return shape
} )
// Create serialized data for JSON or SVGs
let serialized : string | undefined
if ( type === TDExportTypes . SVG ) {
serialized = this . copySvg ( shapeIds )
} else if ( type === TDExportTypes . JSON ) {
serialized = this . copyJson ( shapeIds )
}
const exportInfo : TDExport = {
currentPageId : this.currentPageId ,
name : this.page.name ? ? 'export' ,
shapes ,
assets ,
type ,
serialized ,
size : type === 'png' ? Vec . mul ( size , 2 ) : size ,
2022-01-10 16:36:28 +00:00
}
2022-01-19 12:33:57 +00:00
await this . callbacks . onExport ( exportInfo )
} catch ( error ) {
console . error ( error )
} finally {
this . setIsLoading ( false )
2022-01-10 16:36:28 +00:00
}
}
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-12-27 19:21:30 +00:00
static version = 15.3
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-12-27 18:44:47 +00:00
version : TldrawApp.version ,
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-12-25 17:06:33 +00:00
assets : { } ,
2021-10-16 19:34:34 +00:00
}
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 ,
2021-12-06 18:23:53 +00:00
isMenuOpen : false ,
2021-10-16 19:34:34 +00:00
isEmptyCanvas : false ,
2021-10-18 13:30:42 +00:00
snapLines : [ ] ,
2021-12-25 17:06:33 +00:00
isLoading : false ,
disableAssets : false ,
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
}