2021-08-10 16:12:55 +00:00
import {
2022-08-02 13:56:12 +00:00
TLBounds ,
2021-08-10 16:12:55 +00:00
TLBoundsEventHandler ,
TLBoundsHandleEventHandler ,
TLCanvasEventHandler ,
2022-08-02 13:56:12 +00:00
TLDropEventHandler ,
TLKeyboardEventHandler ,
2021-08-10 16:12:55 +00:00
TLPageState ,
TLPinchEventHandler ,
TLPointerEventHandler ,
2022-08-02 13:56:12 +00:00
TLShapeCloneHandler ,
2021-08-10 16:12:55 +00:00
TLWheelEventHandler ,
Utils ,
} from '@tldraw/core'
2022-08-02 13:56:12 +00:00
import { Vec } from '@tldraw/vec'
2021-08-12 13:39:41 +00:00
import {
2022-08-02 13:56:12 +00:00
FIT_TO_SCREEN_PADDING ,
GRID_SIZE ,
IMAGE_EXTENSIONS ,
SVG_EXPORT_PADDING ,
USER_COLORS ,
VIDEO_EXTENSIONS ,
} from '~constants'
import { shapeUtils } from '~state/shapes'
import { defaultStyle } from '~state/shapes/shared'
import {
AlignStyle ,
2021-08-13 09:28:09 +00:00
AlignType ,
2022-08-02 13:56:12 +00:00
ArrowShape ,
2021-08-13 09:28:09 +00:00
DistributeType ,
2022-08-02 13:56:12 +00:00
FlipType ,
2021-09-02 12:51:39 +00:00
GroupShape ,
2022-08-02 13:56:12 +00:00
MoveType ,
2021-10-13 13:55:31 +00:00
SessionType ,
2022-08-02 13:56:12 +00:00
ShapeStyles ,
StretchType ,
2021-12-25 18:39:50 +00:00
TDAsset ,
2022-08-02 13:56:12 +00:00
TDAssetType ,
2022-01-10 16:36:28 +00:00
TDAssets ,
2022-08-02 13:56:12 +00:00
TDBinding ,
TDDocument ,
2022-01-10 16:36:28 +00:00
TDExport ,
2022-08-02 13:56:12 +00:00
TDExportBackground ,
2022-05-11 13:25:08 +00:00
TDExportType ,
2022-08-02 13:56:12 +00:00
TDPage ,
TDShape ,
TDShapeType ,
TDSnapshot ,
TDStatus ,
TDToolType ,
TDUser ,
TldrawCommand ,
2022-06-25 14:38:43 +00:00
TldrawPatch ,
2021-08-13 09:28:09 +00:00
} from '~types'
2022-08-02 13:56:12 +00:00
import { getClipboard , setClipboard } from './IdbClipboard'
import { StateManager } from './StateManager'
import { deepCopy } from './StateManager/copy'
import { TLDR } from './TLDR'
import * as Commands from './commands'
2021-11-06 11:16:30 +00:00
import {
FileSystemHandle ,
2021-12-25 17:06:33 +00:00
fileToBase64 ,
2022-03-02 14:59:54 +00:00
fileToText ,
2022-01-14 19:17:28 +00:00
getImageSizeFromSrc ,
getVideoSizeFromSrc ,
2022-08-02 13:56:12 +00:00
loadFileHandle ,
migrate ,
openAssetsFromFileSystem ,
openFromFileSystem ,
saveToFileSystem ,
2021-11-06 11:16:30 +00:00
} from './data'
2022-08-02 13:56:12 +00:00
import { SessionArgsOfType , TldrawSession , getSession } from './sessions'
import { clearPrevSize } from './shapes/shared/getTextSize'
import { ArrowTool } from './tools/ArrowTool'
2022-01-10 16:36:28 +00:00
import type { BaseTool } from './tools/BaseTool'
2021-11-16 16:01:29 +00:00
import { DrawTool } from './tools/DrawTool'
import { EllipseTool } from './tools/EllipseTool'
2022-08-02 13:56:12 +00:00
import { EraseTool } from './tools/EraseTool'
2021-11-22 12:28:56 +00:00
import { LineTool } from './tools/LineTool'
2022-08-02 13:56:12 +00:00
import { RectangleTool } from './tools/RectangleTool'
import { SelectTool } from './tools/SelectTool'
2021-11-16 16:01:29 +00:00
import { StickyTool } from './tools/StickyTool'
2022-08-02 13:56:12 +00:00
import { TextTool } from './tools/TextTool'
import { TriangleTool } from './tools/TriangleTool'
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 .
* /
2022-05-11 13:25:08 +00:00
onMount ? : ( app : TldrawApp ) = > void
2021-11-08 14:21:37 +00:00
/ * *
* ( optional ) A callback to run when the component ' s state changes .
* /
2022-05-11 13:25:08 +00:00
onChange ? : ( app : 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 .
* /
2022-05-11 13:25:08 +00:00
onNewProject ? : ( app : 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 .
* /
2022-05-11 13:25:08 +00:00
onSaveProject ? : ( app : 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 .
* /
2022-05-11 13:25:08 +00:00
onSaveProjectAs ? : ( app : 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 .
* /
2022-05-11 13:25:08 +00:00
onOpenProject ? : ( app : TldrawApp , e? : KeyboardEvent ) = > void
2021-12-25 17:06:33 +00:00
/ * *
* ( optional ) A callback to run when the opens a file to upload .
* /
2022-05-11 13:25:08 +00:00
onOpenMedia ? : ( app : TldrawApp ) = > void
2021-11-08 14:21:37 +00:00
/ * *
* ( optional ) A callback to run when the state is patched .
* /
2022-06-25 14:38:43 +00:00
onPatch ? : ( app : TldrawApp , patch : TldrawPatch , reason? : string ) = > void
2021-11-08 14:21:37 +00:00
/ * *
* ( optional ) A callback to run when the state is changed with a command .
* /
2022-06-25 14:38:43 +00:00
onCommand ? : ( app : TldrawApp , command : TldrawCommand , reason? : string ) = > void
2021-11-08 14:21:37 +00:00
/ * *
* ( optional ) A callback to run when the state is persisted .
* /
2022-05-11 13:25:08 +00:00
onPersist ? : ( app : TldrawApp ) = > void
2021-11-08 14:21:37 +00:00
/ * *
* ( optional ) A callback to run when the user undos .
* /
2022-05-11 13:25:08 +00:00
onUndo ? : ( app : TldrawApp ) = > void
2021-11-08 14:21:37 +00:00
/ * *
* ( optional ) A callback to run when the user redos .
* /
2022-05-11 13:25:08 +00:00
onRedo ? : ( app : 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 > ,
2022-06-25 14:38:43 +00:00
assets : Record < string , TDAsset | undefined > ,
addToHistory : boolean
2021-11-22 14:00:24 +00:00
) = > void
/ * *
* ( optional ) A callback to run when the user creates a new project .
* /
2022-05-11 13:25:08 +00:00
onChangePresence ? : ( app : TldrawApp , user : TDUser ) = > void
2022-01-10 15:13:52 +00:00
/ * *
* ( optional ) A callback to run when an asset will be deleted .
* /
2022-05-11 13:25:08 +00:00
onAssetDelete ? : ( app : TldrawApp , assetId : string ) = > void
2022-01-10 15:13:52 +00:00
/ * *
* ( optional ) A callback to run when an asset will be created . Should return the value for the image / video ' s ` src ` property .
* /
2022-05-11 13:25:08 +00:00
onAssetCreate ? : ( app : TldrawApp , file : File , id : string ) = > Promise < string | false >
2022-05-18 21:04:04 +00:00
/ * *
* ( optional ) A callback to run when an asset will be uploaded . Should return the value for the image / video ' s ` src ` property .
* /
onAssetUpload ? : ( app : TldrawApp , 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 .
* /
2022-05-11 13:25:08 +00:00
onExport ? : ( app : TldrawApp , info : TDExport ) = > Promise < void >
2022-06-25 11:28:18 +00:00
/ * *
* ( optional ) A callback to run when a session begins .
* /
onSessionStart ? : ( app : TldrawApp , id : string ) = > void
/ * *
* ( optional ) A callback to run when a session ends .
* /
onSessionEnd ? : ( app : TldrawApp , id : string ) = > 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
2022-08-11 07:41:50 +00:00
fileSystemHandle : FileSystemFileHandle | null = null
2021-11-16 16:01:29 +00:00
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
constructor ( id? : string , callbacks = { } as TDCallbacks ) {
super ( TldrawApp . defaultState , id , TldrawApp . version , ( prev , next , prevVersion ) = > {
2022-07-10 21:05:43 +00:00
return migrate (
{
. . . next ,
document : { . . . next . document , . . . prev . document , version : prevVersion } ,
} ,
TldrawApp . version
)
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 -------------------- */
2022-03-31 13:16:43 +00:00
protected migrate = ( state : TDSnapshot ) : TDSnapshot = > {
2022-07-10 21:05:43 +00:00
return migrate ( state , TldrawApp . version )
2022-03-31 13:16:43 +00:00
}
2021-11-08 14:21:37 +00:00
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 ( {
2022-07-10 21:05:43 +00:00
. . . migrate ( this . state , TldrawApp . version ) ,
2021-10-16 19:34:34 +00:00
appState : {
2021-11-16 16:01:29 +00:00
status : TDStatus.Idle ,
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
2022-02-03 10:09:06 +00:00
const visitedShapes = new Set < ArrowShape > ( )
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-02-03 10:09:06 +00:00
if ( visitedShapes . has ( fromShape ) ) {
return
}
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 )
2022-02-03 10:09:06 +00:00
visitedShapes . add ( fromShape )
2021-10-13 13:55:31 +00:00
if ( fromDelta ) {
const nextShape = {
. . . fromShape ,
. . . fromDelta ,
2022-02-03 10:09:06 +00:00
} as ArrowShape
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
}
2022-06-25 14:38:43 +00:00
private broadcastPatch = ( patch : TldrawPatch , addToHistory : boolean ) = > {
const changedShapes : Record < string , TDShape | undefined > = { }
const changedBindings : Record < string , TDBinding | undefined > = { }
const changedAssets : Record < string , TDAsset | undefined > = { }
const shapes = patch ? . document ? . pages ? . [ this . currentPageId ] ? . shapes
const bindings = patch ? . document ? . pages ? . [ this . currentPageId ] ? . bindings
const assets = patch ? . document ? . assets
if ( shapes ) {
Object . keys ( shapes ) . forEach ( ( id ) = > {
changedShapes [ id ! ] = this . getShape ( id , this . currentPageId )
} )
}
if ( bindings ) {
Object . keys ( bindings ) . forEach ( ( id ) = > {
changedBindings [ id ] = this . getBinding ( id , this . currentPageId )
} )
}
if ( assets ) {
Object . keys ( assets ) . forEach ( ( id ) = > {
changedAssets [ id ] = this . document . assets [ id ]
} )
}
this . callbacks . onChangePage ? . ( this , changedShapes , changedBindings , changedAssets , addToHistory )
}
onPatch = ( state : TDSnapshot , patch : TldrawPatch , id? : string ) = > {
if (
( this . callbacks . onChangePage && patch ? . document ? . pages ? . [ this . currentPageId ] ) ||
patch ? . document ? . assets
) {
if (
2022-06-28 16:55:50 +00:00
patch ? . document ? . assets ||
( this . session &&
this . session . type !== SessionType . Brush &&
this . session . type !== SessionType . Erase &&
this . session . type !== SessionType . Draw )
2022-06-25 14:38:43 +00:00
) {
this . broadcastPatch ( patch , false )
}
}
this . callbacks . onPatch ? . ( this , patch , id )
2021-11-08 14:21:37 +00:00
}
2022-06-25 14:38:43 +00:00
onCommand = ( state : TDSnapshot , command : TldrawCommand , id? : string ) = > {
2021-11-08 14:21:37 +00:00
this . clearSelectHistory ( )
this . isDirty = true
2022-06-25 14:38:43 +00:00
this . callbacks . onCommand ? . ( this , command , id )
2021-11-08 14:21:37 +00:00
}
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 )
}
2022-06-25 14:38:43 +00:00
onPersist = ( state : TDSnapshot , patch : TldrawPatch ) = > {
2021-12-02 12:49:07 +00:00
// If we are part of a room, send our changes to the server
2022-06-25 14:38:43 +00:00
2021-12-04 14:51:40 +00:00
this . callbacks . onPersist ? . ( this )
2022-06-25 14:38:43 +00:00
this . broadcastPatch ( patch , true )
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
* /
2022-06-25 14:38:43 +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 ,
2022-06-25 14:38:43 +00:00
session : ! ! this . session ,
2021-11-22 14:00:24 +00:00
} )
this . prevSelectedIds = this . selectedIds
}
}
/* ----------- Managing Multiplayer State ----------- */
2021-12-02 12:49:07 +00:00
private justSent = false
2021-09-02 12:51:39 +00:00
2022-02-11 21:35:24 +00:00
getReservedContent = ( coreReservedIds : string [ ] , pageId = this . currentPageId ) = > {
2021-12-02 12:49:07 +00:00
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 [ ] = [ ]
2022-02-11 21:35:24 +00:00
if ( this . session ) coreReservedIds . forEach ( ( id ) = > reservedShapeIds . push ( id ) )
if ( this . pageState . editingId ) reservedShapeIds . push ( this . pageState . editingId )
2021-12-02 12:49:07 +00:00
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
}
2022-03-17 12:42:18 +00:00
const page = this . document . pages [ this . currentPageId ]
Object . values ( shapes ) . forEach ( ( shape ) = > {
if ( shape . parentId !== pageId && ! ( page . shapes [ shape . parentId ] || shapes [ shape . parentId ] ) ) {
console . warn ( 'Added a shape without a parent on the page' )
shape . parentId = pageId
}
} )
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 ]
2022-02-11 21:35:24 +00:00
const editingShape = editingId && current . document . pages [ this . currentPageId ] . shapes [ editingId ]
if ( editingShape ) coreReservedIds . push ( editingShape . id )
2021-12-02 12:49:07 +00:00
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 )
)
) {
2022-02-11 21:35:24 +00:00
shapes [ reservedShape . id ] = incomingShape
2021-12-02 12:49:07 +00:00
return
}
// Only allow certain merges.
// Allow decorations (of an arrow) to be changed
if ( 'decorations' in incomingShape && 'decorations' in reservedShape ) {
2022-02-11 21:35:24 +00:00
shapes [ reservedShape . id ] = { . . . reservedShape , decorations : incomingShape.decorations }
2021-12-02 12:49:07 +00:00
}
// 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.
const nextShapes = {
. . . shapes ,
. . . reservedShapes ,
2021-12-01 22:31:19 +00:00
}
2022-02-11 21:35:24 +00:00
if ( editingShape ) {
nextShapes [ editingShape . id ] = editingShape
}
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 ,
} ,
} ,
} ,
}
2022-02-03 10:09:06 +00:00
const page = next . document . pages [ pageId ]
2021-11-22 14:00:24 +00:00
2021-12-02 12:49:07 +00:00
// Get bindings related to the changed shapes
const bindingsToUpdate = TLDR . getRelatedBindings ( next , Object . keys ( nextShapes ) , pageId )
2022-02-03 10:09:06 +00:00
const visitedShapes = new Set < ArrowShape > ( )
2021-12-02 12:49:07 +00:00
// 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-02-03 10:09:06 +00:00
if ( visitedShapes . has ( fromShape ) ) {
return
}
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 )
2022-02-03 10:09:06 +00:00
visitedShapes . add ( 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
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
2022-05-11 13:25:08 +00:00
const { point , zoom } = this . camera
2021-11-16 16:01:29 +00:00
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 ]
* /
2022-06-25 14:38:43 +00:00
setEditingId = ( id? : string , isCreating = false ) = > {
2021-11-11 11:37:57 +00:00
if ( this . readOnly ) return
2022-06-25 14:38:43 +00:00
if ( id ) {
// Start a new editing session
this . startSession ( SessionType . Edit , id , isCreating )
} else {
// If we're clearing the editing id and we don't have one, bail
if ( ! this . pageState . editingId ) return
// If we're clearing the editing id and we do have one, complete the session
this . completeSession ( )
}
2021-12-27 12:30:35 +00:00
this . editingStartTime = performance . now ( )
2022-06-25 14:38:43 +00:00
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
2022-06-25 14:38:43 +00:00
const patch = {
settings : {
[ name ] : typeof value === 'function' ? value ( this . settings [ name ] as V ) : value ,
2021-10-19 13:29:55 +00:00
} ,
2022-06-25 14:38:43 +00:00
}
this . patchState ( patch , ` settings: ${ name } ` )
this . persist ( patch )
2021-11-08 14:21:37 +00:00
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
2022-06-25 14:38:43 +00:00
const patch = {
settings : {
isFocusMode : ! this . settings . isFocusMode ,
2021-09-22 11:28:55 +00:00
} ,
2022-06-25 14:38:43 +00:00
}
this . patchState ( patch , ` settings:toggled_focus_mode ` )
this . persist ( patch )
2021-11-08 14:21:37 +00:00
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
2022-06-25 14:38:43 +00:00
const patch = {
settings : {
isPenMode : ! this . settings . isPenMode ,
2021-08-10 16:12:55 +00:00
} ,
2022-06-25 14:38:43 +00:00
}
this . patchState ( patch , ` settings:toggled_pen_mode ` )
this . persist ( patch )
2021-11-08 14:21:37 +00:00
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
2022-06-25 14:38:43 +00:00
const patch = { settings : { isDarkMode : ! this . settings . isDarkMode } }
this . patchState ( patch , ` settings:toggled_dark_mode ` )
this . persist ( patch )
2021-08-30 13:04:12 +00:00
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
2022-06-25 14:38:43 +00:00
const patch = { settings : { isZoomSnap : ! this . settings . isZoomSnap } }
this . patchState ( patch , ` settings:toggled_zoom_snap ` )
this . persist ( patch )
2021-09-02 20:13:54 +00:00
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
2022-06-25 14:38:43 +00:00
const patch = { settings : { isDebugMode : ! this . settings . isDebugMode } }
this . patchState ( patch , ` settings:toggled_debug ` )
this . persist ( patch )
2021-08-30 13:04:12 +00:00
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 = > {
2022-06-25 14:38:43 +00:00
const patch = { appState : { isMenuOpen : isOpen } }
this . patchState ( patch , 'ui:toggled_menu_opened' )
this . persist ( patch )
2021-08-30 13:04:12 +00:00
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 = > {
2022-06-25 14:38:43 +00:00
const patch = { appState : { isLoading } }
this . patchState ( patch , 'ui:toggled_is_loading' )
this . persist ( patch )
2021-12-25 17:06:33 +00:00
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
2022-06-25 14:38:43 +00:00
const patch = { settings : { showGrid : ! this . settings . showGrid } }
this . patchState ( patch , 'settings:toggled_grid' )
this . persist ( patch )
2021-11-26 15:14:10 +00:00
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-16 07:33:25 +00:00
this . currentTool = this . tools . select
2022-07-04 14:17:47 +00:00
const doc = TldrawApp . defaultDocument
// Set the default page name to the localized version of "Page"
2022-07-08 13:09:08 +00:00
doc . pages [ 'page' ] . name = 'Page 1'
2022-07-04 14:17:47 +00:00
2022-07-10 21:05:43 +00:00
this . resetHistory ( ) . clearSelectHistory ( ) . loadDocument ( TldrawApp . defaultDocument ) . 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 ( {
2022-07-10 21:05:43 +00:00
. . . migrate (
{
. . . this . state ,
document ,
} ,
TldrawApp . version
) ,
2021-10-08 23:05:24 +00:00
appState : {
. . . this . appState ,
currentPageId : Object.keys ( document . pages ) [ 0 ] ,
} ,
} )
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 (
{
2022-07-10 21:05:43 +00:00
. . . migrate (
{ . . . this . state , document : { . . . document , pageStates : currentPageStates } } ,
TldrawApp . version
) ,
2021-10-08 23:05:24 +00:00
appState : nextAppState ,
} ,
'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
2022-07-28 07:25:46 +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 ]
)
}
}
2022-07-10 21:05:43 +00:00
return this . replaceState (
migrate ( nextState , nextState . document . version || 0 ) ,
` ${ 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
2022-07-10 21:05:43 +00:00
const state = {
. . . TldrawApp . defaultState ,
settings : {
. . . this . state . settings ,
2022-07-08 13:38:21 +00:00
} ,
2022-07-10 21:05:43 +00:00
document ,
appState : {
. . . TldrawApp . defaultState . appState ,
. . . this . state . appState ,
currentPageId : Object.keys ( document . pages ) [ 0 ] ,
disableAssets : this.disableAssets ,
} ,
}
this . replaceState ( migrate ( state , TldrawApp . version ) , 'loaded_document' )
2022-05-11 13:25:08 +00:00
const { point , zoom } = this . camera
2022-01-14 20:57:54 +00:00
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 (
2022-07-10 21:05:43 +00:00
migrate ( this . state , TldrawApp . version ) . document ,
2021-12-25 17:06:33 +00:00
this . fileSystemHandle
)
2021-11-05 14:13:14 +00:00
this . fileSystemHandle = fileHandle
2022-06-25 14:38:43 +00:00
this . persist ( { } )
2021-11-05 14:13:14 +00:00
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
2022-06-25 14:38:43 +00:00
this . persist ( { } )
2021-11-05 14:13:14 +00:00
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 ( )
2022-06-25 14:38:43 +00:00
this . persist ( { } )
2021-11-05 14:13:14 +00:00
} catch ( e ) {
console . error ( e )
} finally {
2022-06-25 14:38:43 +00:00
this . persist ( { } )
2021-11-05 14:13:14 +00:00
}
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 {
2022-07-21 18:16:01 +00:00
const file = await openAssetsFromFileSystem ( )
if ( Array . isArray ( file ) ) {
this . addMediaFromFiles ( file , this . centerPoint )
} else {
if ( ! file ) return
this . addMediaFromFiles ( [ file ] )
}
2021-12-25 17:06:33 +00:00
} catch ( e ) {
console . error ( e )
} finally {
2022-06-25 14:38:43 +00:00
this . persist ( { } )
2021-12-25 17:06:33 +00:00
}
}
2021-09-05 09:51:21 +00:00
/ * *
* Sign out of the current account .
* Should move to the www layer .
* @todo
* /
2021-09-02 12:51:39 +00:00
signOut = ( ) = > {
2021-11-05 14:13:14 +00:00
// todo
2021-08-10 16:12:55 +00:00
}
2021-08-29 13:33:43 +00:00
/* -------------------- Getters --------------------- */
2021-08-16 14:01:03 +00:00
2021-09-02 12:51:39 +00:00
/ * *
* Get the current app state .
* /
2021-11-16 16:01:29 +00:00
getAppState = ( ) : TDSnapshot [ 'appState' ] = > {
2021-09-02 12:51:39 +00:00
return this . appState
2021-08-10 16:12:55 +00:00
}
2021-09-02 12:51:39 +00:00
/ * *
* Get a page .
* @param pageId ( optional ) The page ' s id .
* /
2021-11-16 16:01:29 +00:00
getPage = ( pageId = this . currentPageId ) : TDPage = > {
2021-08-29 13:33:43 +00:00
return TLDR . getPage ( this . state , pageId || this . currentPageId )
}
2021-08-10 16:12:55 +00:00
2021-09-02 12:51:39 +00:00
/ * *
* Get the shapes ( as an array ) from a given page .
* @param pageId ( optional ) The page ' s id .
* /
2021-11-16 16:01:29 +00:00
getShapes = ( pageId = this . currentPageId ) : TDShape [ ] = > {
2021-08-29 13:33:43 +00:00
return TLDR . getShapes ( this . state , pageId || this . currentPageId )
}
2021-09-02 12:51:39 +00:00
/ * *
* Get the bindings from a given page .
* @param pageId ( optional ) The page ' s id .
* /
2021-11-16 16:01:29 +00:00
getBindings = ( pageId = this . currentPageId ) : TDBinding [ ] = > {
2021-08-29 13:33:43 +00:00
return TLDR . getBindings ( this . state , pageId || this . currentPageId )
}
2021-09-02 12:51:39 +00:00
/ * *
* Get a shape from a given page .
* @param id The shape ' s id .
* @param pageId ( optional ) The page ' s id .
* /
2021-11-16 16:01:29 +00:00
getShape = < T extends TDShape = TDShape > ( id : string , pageId = this . currentPageId ) : T = > {
2021-09-02 12:51:39 +00:00
return TLDR . getShape < T > ( this . state , id , pageId )
2021-08-29 13:33:43 +00:00
}
2021-09-06 11:07:15 +00:00
/ * *
* Get the bounds of a shape on a given page .
* @param id The shape ' s id .
* @param pageId ( optional ) The page ' s id .
* /
getShapeBounds = ( id : string , pageId = this . currentPageId ) : TLBounds = > {
return TLDR . getBounds ( this . getShape ( id , pageId ) )
}
2021-09-02 12:51:39 +00:00
/ * *
* Get a binding from a given page .
* @param id The binding ' s id .
* @param pageId ( optional ) The page ' s id .
* /
2021-11-16 16:01:29 +00:00
getBinding = ( id : string , pageId = this . currentPageId ) : TDBinding = > {
2021-09-02 12:51:39 +00:00
return TLDR . getBinding ( this . state , id , pageId )
}
/ * *
* Get the page state for a given page .
* @param pageId ( optional ) The page ' s id .
* /
getPageState = ( pageId = this . currentPageId ) : TLPageState = > {
return TLDR . getPageState ( this . state , pageId || this . currentPageId )
2021-08-29 13:33:43 +00:00
}
2021-09-02 12:51:39 +00:00
/ * *
* Turn a screen point into a point on the page .
* @param point The screen point
* @param pageId ( optional ) The page to use
* /
2021-08-29 13:33:43 +00:00
getPagePoint = ( point : number [ ] , pageId = this . currentPageId ) : number [ ] = > {
const { camera } = this . getPageState ( pageId )
return Vec . sub ( Vec . div ( point , camera . zoom ) , camera . point )
}
2021-09-06 12:14:43 +00:00
/ * *
* Get the current undo / redo stack .
* /
get history() {
2021-09-06 12:50:15 +00:00
return this . stack . slice ( 0 , this . pointer + 1 )
2021-09-06 12:14:43 +00:00
}
2021-09-06 12:43:56 +00:00
/ * *
* Replace the current history stack .
* /
2021-11-16 16:01:29 +00:00
set history ( commands : TldrawCommand [ ] ) {
2021-09-06 12:43:56 +00:00
this . replaceHistory ( commands )
}
2021-09-02 12:51:39 +00:00
/ * *
* The current document .
* /
2021-11-16 16:01:29 +00:00
get document ( ) : TDDocument {
2021-09-02 12:51:39 +00:00
return this . state . document
2021-08-29 13:33:43 +00:00
}
2021-09-02 12:51:39 +00:00
/ * *
* The current app state .
* /
2021-11-16 16:01:29 +00:00
get settings ( ) : TDSnapshot [ 'settings' ] {
return this . state . settings
}
/ * *
* The current app state .
* /
get appState ( ) : TDSnapshot [ 'appState' ] {
2021-08-29 13:33:43 +00:00
return this . state . appState
}
2021-09-02 12:51:39 +00:00
/ * *
* The current page id .
* /
2021-08-29 13:33:43 +00:00
get currentPageId ( ) : string {
return this . state . appState . currentPageId
}
2021-09-02 12:51:39 +00:00
/ * *
* The current page .
* /
2021-11-16 16:01:29 +00:00
get page ( ) : TDPage {
2021-09-02 12:51:39 +00:00
return this . state . document . pages [ this . currentPageId ]
2021-08-10 16:12:55 +00:00
}
2021-09-02 12:51:39 +00:00
/ * *
* The current page ' s shapes ( as an array ) .
* /
2021-11-16 16:01:29 +00:00
get shapes ( ) : TDShape [ ] {
2021-09-02 12:51:39 +00:00
return Object . values ( this . page . shapes )
2021-08-16 07:49:31 +00:00
}
2021-08-10 16:12:55 +00:00
2021-09-02 12:51:39 +00:00
/ * *
* The current page ' s bindings .
* /
2021-11-16 16:01:29 +00:00
get bindings ( ) : TDBinding [ ] {
2021-09-02 12:51:39 +00:00
return Object . values ( this . page . bindings )
2021-08-16 07:49:31 +00:00
}
2021-08-10 16:12:55 +00:00
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
}
2022-05-11 13:25:08 +00:00
get camera ( ) : {
point : number [ ]
zoom : number
} {
return this . pageState . camera
}
get zoom ( ) : number {
return this . pageState . camera . zoom
}
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 .
* /
2022-06-15 17:54:03 +00:00
createPage = ( id? : 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
const { width , height } = this . rendererBounds
2022-06-15 17:54:03 +00:00
return this . setState ( Commands . createPage ( this , [ - width / 2 , - height / 2 ] , id , name ) )
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
/ * *
2022-06-29 09:25:00 +00:00
* Move a page above another .
* @param pageId The page to move .
* @param index The page above which to move .
* /
movePage = ( pageId : string , index : number ) : this = > {
if ( this . readOnly ) return this
return this . setState ( Commands . movePage ( this , pageId , index ) )
}
/ * *
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 */
/* -------------------------------------------------- */
2022-05-11 13:25:08 +00:00
/ * *
* Cut ( copy and delete ) one or more shapes to the clipboard .
* @param ids The ids of the shapes to cut .
* /
2022-06-09 14:33:35 +00:00
cut = ( ids = this . selectedIds , e? : ClipboardEvent ) : this = > {
2022-05-13 12:00:53 +00:00
e ? . preventDefault ( )
2022-06-09 14:33:35 +00:00
this . copy ( ids , e )
2022-06-15 17:54:03 +00:00
2022-05-11 13:25:08 +00:00
if ( ! this . readOnly ) {
this . delete ( ids )
2021-09-21 15:47:04 +00:00
}
2022-06-09 15:00:47 +00:00
2021-08-29 13:33:43 +00:00
return this
}
2021-11-07 13:45:48 +00:00
/ * *
2022-05-11 13:25:08 +00:00
* Copy one or more shapes to the clipboard .
* @param ids The ids of the shapes to copy .
2021-11-07 13:45:48 +00:00
* /
2022-06-09 14:33:35 +00:00
copy = ( ids = this . selectedIds , e? : ClipboardEvent ) : this = > {
2022-06-09 15:00:47 +00:00
// Allow when in readOnly mode
2022-06-15 17:54:03 +00:00
2022-05-13 12:00:53 +00:00
e ? . preventDefault ( )
2022-06-19 13:47:43 +00:00
this . clipboard = this . getContent ( ids )
2022-05-11 13:25:08 +00:00
2022-05-14 13:15:55 +00:00
const jsonString = JSON . stringify ( {
2022-05-11 13:25:08 +00:00
type : 'tldr/clipboard' ,
. . . this . clipboard ,
} )
2022-05-14 13:15:55 +00:00
const tldrawString = ` <tldraw> ${ jsonString } </tldraw> `
2022-05-14 13:59:47 +00:00
setClipboard ( tldrawString )
2022-05-11 13:25:08 +00:00
if ( e ) {
2022-05-13 12:00:53 +00:00
e . clipboardData ? . setData ( 'text/html' , tldrawString )
2022-05-11 13:25:08 +00:00
}
2022-05-14 13:59:47 +00:00
if ( navigator . clipboard && window . ClipboardItem ) {
2022-05-11 13:25:08 +00:00
navigator . clipboard . write ( [
new ClipboardItem ( {
'text/html' : new Blob ( [ tldrawString ] , { type : 'text/html' } ) ,
} ) ,
] )
}
2021-11-07 13:45:48 +00:00
return this
}
2021-09-02 12:51:39 +00:00
/ * *
* Paste shapes ( or text ) from clipboard to a certain point .
* @param point
* /
2022-05-11 13:25:08 +00:00
paste = async ( point? : number [ ] , e? : ClipboardEvent ) = > {
2021-11-05 14:13:14 +00:00
if ( this . readOnly ) return
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
const filesToPaste : File [ ] = [ ]
2022-08-04 20:43:57 +00:00
const shapesToCreate : TDShape [ ] = [ ]
2022-07-21 18:16:01 +00:00
let clipboardData : any
const getSvgFromText = async ( text : string ) = > {
2022-05-11 13:25:08 +00:00
const div = document . createElement ( 'div' )
div . innerHTML = text
const svg = div . firstChild as SVGSVGElement
svg . style . setProperty ( 'background-color' , 'transparent' )
const imageBlob = await TLDR . getImageForSvg ( svg , TDExportType . SVG , {
scale : 1 ,
quality : 1 ,
} )
if ( imageBlob ) {
const file = new File ( [ imageBlob ] , 'image.svg' )
2022-07-21 18:16:01 +00:00
filesToPaste . push ( file )
2022-05-11 13:25:08 +00:00
} else {
2022-07-21 18:16:01 +00:00
getShapeFromText ( text )
2021-09-22 15:00:20 +00:00
}
2022-01-06 10:45:11 +00:00
}
2021-09-22 15:00:20 +00:00
2022-07-21 18:16:01 +00:00
const getShapeFromText = ( text : string ) = > {
const pagePoint = this . getPagePoint ( point ? ? this . centerPoint , this . currentPageId )
2022-05-11 13:25:08 +00:00
2022-07-21 18:16:01 +00:00
const isMultiline = text . includes ( '\n' )
2022-05-11 13:25:08 +00:00
2022-07-21 18:16:01 +00:00
shapesToCreate . push (
TLDR . getShapeUtil ( TDShapeType . Text ) . getShape ( {
id : Utils.uniqueId ( ) ,
type : TDShapeType . Text ,
parentId : this.appState.currentPageId ,
text : TLDR.normalizeText ( text . trim ( ) ) ,
point : pagePoint ,
style : {
. . . this . appState . currentStyle ,
textAlign : isMultiline ? AlignStyle.Start : this.appState.currentStyle.textAlign ,
} ,
} )
)
2022-05-11 13:25:08 +00:00
}
2022-07-21 18:16:01 +00:00
const getShapeFromHtml = ( html : string ) = > {
2022-05-11 13:25:08 +00:00
try {
2022-05-14 13:15:55 +00:00
const maybeJson = html . match ( /<tldraw>(.*)<\/tldraw>/ ) ? . [ 1 ]
2022-05-13 12:00:53 +00:00
if ( ! maybeJson ) return
2022-05-11 13:25:08 +00:00
const json : {
2022-01-06 10:45:11 +00:00
type : string
2022-07-14 18:30:03 +00:00
shapes : ( TDShape & { text : string } ) [ ]
2022-01-06 10:45:11 +00:00
bindings : TDBinding [ ]
assets : TDAsset [ ]
2022-05-11 13:25:08 +00:00
} = JSON . parse ( maybeJson )
if ( json . type === 'tldr/clipboard' ) {
2022-07-21 18:16:01 +00:00
clipboardData = json
2022-05-11 13:25:08 +00:00
return
2022-01-06 10:45:11 +00:00
} else {
2022-05-11 13:25:08 +00:00
throw Error ( 'Not tldraw data!' )
}
} catch ( e ) {
2022-07-21 18:16:01 +00:00
getShapeFromText ( html )
2022-05-11 13:25:08 +00:00
}
}
2022-05-06 08:14:20 +00:00
2022-05-11 13:25:08 +00:00
if ( e !== undefined ) {
2022-07-21 18:16:01 +00:00
const items = Array . from ( e . clipboardData ? . items ? ? [ ] )
await Promise . all (
items . map ( async ( item ) = > {
const { type , kind } = item
switch ( kind ) {
2022-05-13 12:00:53 +00:00
case 'string' : {
2022-07-21 18:16:01 +00:00
const str : string = await new Promise ( ( resolve ) = > item . getAsString ( resolve ) )
switch ( type ) {
case 'text/html' : {
if ( str . match ( /<tldraw>(.*)<\/tldraw>/ ) ? . [ 1 ] ) {
getShapeFromHtml ( str )
return
}
break
2022-05-13 12:00:53 +00:00
}
2022-07-21 18:16:01 +00:00
case 'text/plain' : {
if ( str . startsWith ( '<svg' ) ) {
2022-08-04 20:43:57 +00:00
await getSvgFromText ( str )
2022-07-21 18:16:01 +00:00
} else {
getShapeFromText ( str )
}
break
}
}
break
2022-05-11 13:25:08 +00:00
}
2022-05-13 12:00:53 +00:00
case 'file' : {
2022-06-09 14:33:35 +00:00
const file = item . getAsFile ( )
2022-07-21 18:16:01 +00:00
if ( file ) filesToPaste . push ( file )
break
2022-05-13 12:00:53 +00:00
}
2022-05-11 13:25:08 +00:00
}
2022-07-21 18:16:01 +00:00
} )
)
2022-05-11 13:25:08 +00:00
}
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
if ( clipboardData ) {
this . insertContent ( clipboardData , { point , select : true } )
return this
}
2022-05-14 13:59:47 +00:00
2022-07-21 18:16:01 +00:00
if ( filesToPaste . length ) {
this . addMediaFromFiles ( filesToPaste , point )
return this
}
2022-05-11 13:25:08 +00:00
2022-07-21 18:16:01 +00:00
if ( shapesToCreate . length ) {
const pagePoint = this . getPagePoint ( point ? ? this . centerPoint , this . currentPageId )
2022-05-14 13:59:47 +00:00
2022-07-21 18:16:01 +00:00
const currentPoint = Vec . add ( pagePoint , [ 0 , 0 ] )
2022-05-11 13:25:08 +00:00
2022-07-21 18:16:01 +00:00
shapesToCreate . forEach ( ( shape , i ) = > {
const bounds = TLDR . getBounds ( shape )
2022-05-11 13:25:08 +00:00
2022-07-21 18:16:01 +00:00
if ( i === 0 ) {
// For the first shape, offset the current point so
// that the first shape's center is at the page point
currentPoint [ 0 ] -= bounds . width / 2
currentPoint [ 1 ] -= bounds . height / 2
}
2022-05-11 13:25:08 +00:00
2022-07-21 18:16:01 +00:00
// Set the shape's point the current point
shape . point = [ . . . currentPoint ]
2022-05-11 13:25:08 +00:00
2022-07-21 18:16:01 +00:00
// Then bump the page current point by this shape's width
currentPoint [ 0 ] += bounds . width
} )
2022-05-11 13:25:08 +00:00
2022-07-21 18:16:01 +00:00
this . createShapes ( . . . shapesToCreate )
return this
}
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
if ( this . clipboard ) {
// try to get clipboard data from the scene itself
this . insertContent ( this . clipboard )
2022-05-11 13:25:08 +00:00
} else {
2022-07-21 18:16:01 +00:00
// last chance to get the clipboard data, is it in storage?
getClipboard ( ) . then ( ( text ) = > {
if ( text ) getShapeFromHtml ( text )
} )
2022-05-11 13:25:08 +00:00
}
2022-05-06 08:14:20 +00:00
2021-08-29 13:33:43 +00:00
return this
}
2022-05-11 13:25:08 +00:00
getSvg = async (
ids = this . selectedIds . length ? this . selectedIds : Object.keys ( this . page . shapes ) ,
2022-07-23 08:33:02 +00:00
opts = { } as Partial < { includeFonts : boolean } >
2022-05-11 13:25:08 +00:00
) : Promise < SVGElement | undefined > = > {
2021-11-22 16:53:24 +00:00
if ( ids . length === 0 ) return
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
// Embed our custom fonts
2022-05-11 15:58:40 +00:00
const svg = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'svg' )
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' )
2022-05-11 15:58:40 +00:00
2022-06-19 13:47:43 +00:00
if ( typeof window !== 'undefined' ) {
window . focus ( ) // weird but necessary
}
2022-05-11 13:25:08 +00:00
if ( opts . includeFonts ) {
try {
2022-05-11 15:58:40 +00:00
const { fonts } = await fetch ( TldrawApp . assetSrc , { mode : 'no-cors' } ) . then ( ( d ) = > d . json ( ) )
2022-05-11 13:25:08 +00:00
style . textContent = `
@font - face {
font - family : 'Caveat Brush' ;
src : url ( data :application / x - font - woff ; charset = utf - 8 ; base64 , $ { fonts . caveat } ) format ( 'woff' ) ;
font - weight : 500 ;
font - style : normal ;
}
@font - face {
font - family : 'Source Code Pro' ;
src : url ( data :application / x - font - woff ; charset = utf - 8 ; base64 , $ { fonts . source_code_pro } ) format ( 'woff' ) ;
font - weight : 500 ;
font - style : normal ;
}
@font - face {
font - family : 'Source Sans Pro' ;
src : url ( data :application / x - font - woff ; charset = utf - 8 ; base64 , $ { fonts . source_sans_pro } ) format ( 'woff' ) ;
font - weight : 500 ;
font - style : normal ;
}
@font - face {
font - family : 'Crimson Pro' ;
src : url ( data :application / x - font - woff ; charset = utf - 8 ; base64 , $ { fonts . crimson_pro } ) format ( 'woff' ) ;
font - weight : 500 ;
font - style : normal ;
}
`
} catch ( e ) {
TLDR . warn ( 'Could not find tldraw-assets.json file.' )
}
} else {
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'); `
}
defs . append ( style )
svg . append ( defs )
2021-12-25 17:06:33 +00:00
// Get the shapes in order
const shapes = ids
2022-05-11 13:25:08 +00:00
. map ( ( id ) = > this . getShape ( id , this . currentPageId ) )
2021-12-25 17:06:33 +00:00
. sort ( ( a , b ) = > a . childIndex - b . childIndex )
2022-05-11 13:25:08 +00:00
// Find their common bounding box. Shapes will be positioned relative to this box
2021-12-25 17:06:33 +00:00
const commonBounds = Utils . getCommonBounds ( shapes . map ( TLDR . getRotatedBounds ) )
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
// 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 )
2022-05-14 11:13:37 +00:00
const elm = util . getSvgElement ( shape , this . settings . isDarkMode )
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
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
}
2022-05-11 13:25:08 +00:00
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' ,
2022-05-11 13:25:08 +00:00
` translate( ${ ( SVG_EXPORT_PADDING + shape . point [ 0 ] - commonBounds . minX ) . toFixed ( 2 ) } , ${ (
SVG_EXPORT_PADDING +
shape . point [ 1 ] -
commonBounds . minY
) . toFixed ( 2 ) } ) rotate ( $ { ( ( ( shape . rotation || 0 ) * 180 ) / Math . PI ) . toFixed ( 2 ) } , $ { (
bounds . width / 2
) . toFixed ( 2 ) } , $ { ( bounds . height / 2 ) . toFixed ( 2 ) } ) `
2021-09-23 09:48:08 +00:00
)
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
return elm
2021-09-23 09:48:08 +00:00
}
2022-05-11 13:25: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' )
2022-05-11 13:25:08 +00:00
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 ) = > {
2022-05-11 13:25:08 +00:00
const shape = this . getShape ( childId , this . currentPageId )
2021-12-25 17:06:33 +00:00
const elm = getSvgElementForShape ( shape )
2022-05-11 13:25:08 +00:00
if ( elm ) {
g . append ( elm )
}
2021-12-25 17:06:33 +00:00
} )
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
// Add the group elm to the SVG
2022-05-11 13:25:08 +00:00
svg . append ( g )
2021-09-23 09:48:08 +00:00
return
}
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
// Just add the shape's element to the
const elm = getSvgElementForShape ( shape )
2022-05-11 13:25:08 +00:00
if ( elm ) {
svg . append ( elm )
}
2021-09-01 08:57:46 +00:00
} )
2022-05-11 13:25:08 +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
)
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
// Clean up the SVG by removing any hidden elements
2022-05-11 13:25:08 +00:00
svg . setAttribute ( 'width' , commonBounds . width . toString ( ) )
svg . setAttribute ( 'height' , commonBounds . height . toString ( ) )
2022-07-23 08:33:02 +00:00
// Set export background
2022-07-23 14:05:48 +00:00
const exportBackground : TDExportBackground = this . settings . exportBackground
const darkBackground = '#212529'
2022-07-23 08:33:02 +00:00
const lightBackground = 'rgb(248, 249, 250)'
2022-07-23 14:05:48 +00:00
switch ( exportBackground ) {
2022-07-23 08:33:02 +00:00
case TDExportBackground . Auto : {
svg . style . setProperty (
'background-color' ,
this . settings . isDarkMode ? darkBackground : lightBackground
)
2022-07-23 14:05:48 +00:00
break
2022-07-23 08:33:02 +00:00
}
case TDExportBackground . Dark : {
svg . style . setProperty ( 'background-color' , darkBackground )
2022-07-23 14:05:48 +00:00
break
2022-07-23 08:33:02 +00:00
}
case TDExportBackground . Light : {
svg . style . setProperty ( 'background-color' , lightBackground )
2022-07-23 14:05:48 +00:00
break
2022-07-23 08:33:02 +00:00
}
2022-07-23 14:05:48 +00:00
case TDExportBackground . Transparent :
2022-07-23 08:33:02 +00:00
default : {
svg . style . setProperty ( 'background-color' , 'transparent' )
2022-07-23 14:05:48 +00:00
break
2022-07-23 08:33:02 +00:00
}
2022-05-11 13:25:08 +00:00
}
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 ( ) )
2022-05-11 13:25:08 +00:00
return svg
}
/ * *
* 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 .
* /
copySvg = async (
ids = this . selectedIds . length ? this . selectedIds : Object.keys ( this . page . shapes )
) = > {
if ( ids . length === 0 ) return
const svg = await this . getSvg ( ids )
if ( ! svg ) return
const svgString = TLDR . getSvgString ( svg , 1 )
2022-06-19 13:47:43 +00:00
this . clipboard = this . getContent ( ids )
2022-05-11 13:25:08 +00:00
const tldrawString = JSON . stringify ( {
type : 'tldr/clipboard' ,
. . . this . clipboard ,
} )
2022-05-14 13:59:47 +00:00
if ( navigator . clipboard && window . ClipboardItem ) {
2022-05-11 13:25:08 +00:00
navigator . clipboard . write ( [
new ClipboardItem ( {
'text/html' : new Blob ( [ tldrawString ] , { type : 'text/html' } ) ,
'text/plain' : new Blob ( [ svgString ] , { type : 'text/plain' } ) ,
} ) ,
] )
}
2021-09-01 08:57:46 +00:00
return svgString
2021-08-29 13:33:43 +00:00
}
2022-06-19 13:47:43 +00:00
/ * *
* Get the shapes and bindings for the current selection , if any , or else the current page .
*
* @param ids The ids of the shapes to get content for .
* /
getContent = ( ids? : string [ ] ) = > {
const page = this . getPage ( this . currentPageId )
// If ids is explicitly empty ([]) return
if ( ids && ids . length === 0 ) return
// If ids was not provided, use the selected ids
if ( ! ids ) ids = this . selectedIds
// If there are no selected ids, use all the page's shape ids
if ( ids . length === 0 ) ids = Object . keys ( page . shapes )
// If the page was empty, return
if ( ids . length === 0 ) return
const shapes = ids
. map ( ( id ) = > page . shapes [ id ] )
. flatMap ( ( shape ) = > [ shape , . . . ( shape . children ? ? [ ] ) . map ( ( childId ) = > page . shapes [ childId ] ) ] )
. map ( deepCopy )
const idsSet = new Set ( shapes . map ( ( s ) = > s . id ) )
shapes . forEach ( ( shape ) = > {
if ( shape . parentId === this . currentPageId ) {
shape . parentId = 'currentPageId'
}
} )
// If a binding's from and to are included, then include the binding;
// but if only one shape is included, discard the binding
const bindings = Object . values ( page . bindings )
. filter ( ( binding ) = > {
2022-06-20 19:36:23 +00:00
if ( idsSet . has ( binding . fromId ) || idsSet . has ( binding . toId ) ) {
2022-06-19 13:47:43 +00:00
return true
}
if ( idsSet . has ( binding . fromId ) ) {
const shape = shapes . find ( ( s ) = > s . id === binding . fromId )
const handles = shape ! . handles
if ( handles ) {
Object . values ( handles ) . forEach ( ( handle ) = > {
if ( handle ! . bindingId === binding . id ) {
handle ! . bindingId = undefined
}
} )
}
}
if ( idsSet . has ( binding . toId ) ) {
const shape = shapes . find ( ( s ) = > s . id === binding . toId )
const handles = shape ! . handles
if ( handles ) {
Object . values ( handles ) . forEach ( ( handle ) = > {
if ( handle ! . bindingId === binding . id ) {
handle ! . bindingId = undefined
}
} )
}
}
return false
} )
. map ( deepCopy )
const assets = [
. . . new Set (
shapes
. map ( ( shape ) = > {
if ( ! shape . assetId ) return
return this . document . assets [ shape . assetId ]
} )
. filter ( Boolean )
. map ( deepCopy )
) ,
] as TDAsset [ ]
return { shapes , bindings , assets }
}
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 .
* /
2022-06-19 13:47:43 +00:00
copyJson = ( ids = this . selectedIds ) = > {
const content = this . getContent ( ids )
2022-05-11 13:25:08 +00:00
2022-06-19 13:47:43 +00:00
if ( content ) {
TLDR . copyStringToClipboard ( JSON . stringify ( content ) )
}
2022-05-11 13:25:08 +00:00
2022-06-19 13:47:43 +00:00
return this
2021-08-29 13:33:43 +00:00
}
2022-05-11 13:25:08 +00:00
/ * *
* Export one or more shapes as JSON .
* @param ids The ids of the shapes to copy from the current page .
* @returns A string containing the JSON .
* /
2022-06-19 13:47:43 +00:00
exportJson = ( ids = this . selectedIds ) = > {
const content = this . getContent ( ids )
if ( content ) {
const blob = new Blob ( [ JSON . stringify ( content ) ] , { type : 'application/json' } )
const url = URL . createObjectURL ( blob )
const link = document . createElement ( 'a' )
link . href = url
link . download = ` export.json `
link . click ( )
}
2022-05-11 13:25:08 +00:00
2022-06-19 13:47:43 +00:00
return this
}
2022-05-11 13:25:08 +00:00
2022-06-19 13:47:43 +00:00
/ * *
* Insert content .
*
* @param content The content to insert .
* @param content . shapes An array of TDShape objects .
* @param content . bindings ( optional ) An array of TDBinding objects .
* @param content . assets ( optional ) An array of TDAsset objects .
* @param opts ( optional ) An options object
* @param opts . point ( optional ) A point at which to paste the content .
2022-06-20 19:36:23 +00:00
* @param opts . select ( optional ) When true , the inserted shapes will be selected . Defaults to false .
* @param opts . overwrite ( optional ) When true , the inserted shapes and bindings will overwrite any existing shapes and bindings . Defaults to false .
2022-06-19 13:47:43 +00:00
* /
insertContent = (
content : { shapes : TDShape [ ] ; bindings? : TDBinding [ ] ; assets? : TDAsset [ ] } ,
2022-06-20 19:36:23 +00:00
opts = { } as { point? : number [ ] ; select? : boolean ; overwrite? : boolean }
2022-06-19 13:47:43 +00:00
) = > {
return this . setState ( Commands . insertContent ( this , content , opts ) , 'insert_content' )
2022-05-11 13:25:08 +00:00
}
/ * *
* Get an image of the selected shapes .
*
* @param format The format to export the image as .
* @param opts ( optional ) An object containing options for the image .
* @param opts . ids ( optional ) The ids of the shapes ( on the current page ) to get an image for .
* @param opts . scale ( optional ) The id of the page from which to get an image .
* @param opts . quality ( optional ) The quality ( between 0 and 1 ) for the image if lossy format .
* /
getImage = async (
format : Exclude < TDExportType , TDExportType.JSON > = TDExportType . PNG ,
opts = { } as Partial < {
ids : string [ ]
scale : number
quality : number
transparentBackground : boolean
} >
) : Promise < Blob | undefined > = > {
const { ids = this . selectedIds . length ? this . selectedIds : Object.keys ( this . page . shapes ) } =
opts
const svg = await this . getSvg ( ids , {
2022-07-23 14:05:48 +00:00
includeFonts : format !== TDExportType . SVG ,
2022-05-11 13:25:08 +00:00
} )
if ( ! svg ) return
if ( format === TDExportType . SVG ) {
const svgString = TLDR . getSvgString ( svg , 1 )
const blob = new Blob ( [ svgString ] , { type : 'image/svg+xml' } )
return blob
}
const imageBlob = await TLDR . getImageForSvg ( svg , format , opts )
if ( ! imageBlob ) return
return imageBlob
}
/ * *
* Copy an image of the selected shapes .
*
* @param format The format to export the image as .
* @param opts ( optional ) An object containing options for the image .
* @param opts . ids ( optional ) The ids of the shapes ( on the current page ) to get an image for .
* @param opts . scale ( optional ) The id of the page from which to get an image .
* @param opts . quality ( optional ) The quality ( between 0 and 1 ) for the image if lossy format .
* /
copyImage = async (
format : TDExportType.PNG | TDExportType . SVG = TDExportType . PNG ,
opts = { } as Partial < {
ids : string [ ]
scale : number
quality : number
transparentBackground : boolean
} >
) = > {
if ( format === TDExportType . SVG ) {
this . copySvg ( opts . ids )
return
}
2022-05-14 13:59:47 +00:00
if ( ! ( navigator . clipboard && window . ClipboardItem ) ) {
console . warn ( 'Sorry, your browser does not support copying images.' )
return
}
2022-05-11 13:25:08 +00:00
const blob = await this . getImage ( format , opts )
if ( ! blob ) return
navigator . clipboard . write ( [
new ClipboardItem ( {
[ blob . type ] : blob ,
} ) ,
] )
}
exportImage = async (
format : Exclude < TDExportType , TDExportType.JSON > = TDExportType . PNG ,
opts = { } as Partial < {
ids : string [ ]
pageId : string
scale : number
quality : number
} >
) = > {
2022-06-09 14:33:35 +00:00
const { pageId = this . currentPageId } = opts
2022-05-11 13:25:08 +00:00
const blob = await this . getImage ( format , opts )
if ( ! blob ) return
const name = this . document . pages [ pageId ] . name ? ? 'export'
if ( this . callbacks . onExport ) {
this . callbacks . onExport ( this , {
name ,
type : format ,
blob ,
} )
} else {
const url = URL . createObjectURL ( blob )
const link = document . createElement ( 'a' )
link . href = url
link . download = ` ${ name } . ${ format } `
link . click ( )
}
}
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 = > {
2022-05-11 13:25:08 +00:00
const { zoom , point } = this . camera
2021-09-02 12:51:39 +00:00
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 = > {
2022-05-11 13:25:08 +00:00
const i = Math . round ( ( this . camera . zoom * 100 ) / 25 )
2021-09-02 12:51:39 +00:00
const nextZoom = TLDR . getCameraZoom ( ( i + 1 ) * 0.25 )
return this . zoomTo ( nextZoom )
}
/ * *
* Zoom in by 25 % .
* /
zoomOut = ( ) : this = > {
2022-05-11 13:25:08 +00:00
const i = Math . round ( ( this . camera . zoom * 100 ) / 25 )
2021-09-02 12:51:39 +00:00
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
)
2022-05-11 13:25:08 +00:00
zoom = this . camera . zoom === zoom || this . camera . zoom < 1 ? Math . min ( 1 , zoom ) : zoom
2021-10-17 08:47:41 +00:00
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 ] ) ) ,
2022-05-11 13:25:08 +00:00
this . camera . zoom ,
2021-09-02 12:51:39 +00:00
` 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 = > {
2022-05-11 13:25:08 +00:00
const { zoom } = this . camera
2021-09-02 12:51:39 +00:00
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 .
2022-06-25 14:38:43 +00:00
* @param type The session type
2021-09-01 08:57:46 +00:00
* @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
2022-06-25 14:38:43 +00:00
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
}
2022-06-25 14:38:43 +00:00
const Session = getSession ( type ) as any
2021-11-16 16:01:29 +00:00
this . session = new Session ( this , . . . args )
2021-08-30 11:06:42 +00:00
2022-06-25 14:38:43 +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 ) {
2022-06-25 14:38:43 +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
2022-06-25 14:38:43 +00:00
this . callbacks . onSessionStart ? . ( this , this . session ! . constructor . name )
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
// @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
}
2022-06-25 14:38:43 +00:00
this . setEditingId ( )
this . callbacks . onSessionEnd ? . ( this , 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
2022-06-25 14:38:43 +00:00
2021-08-10 16:12:55 +00:00
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
2022-06-25 11:28:18 +00:00
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
2022-06-25 14:38:43 +00:00
this . callbacks . onSessionEnd ? . ( this , session . constructor . name )
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'
)
}
2022-06-25 14:38:43 +00:00
createTextShapeAtPoint ( point : number [ ] , id? : string , patch? : boolean ) : 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 ] )
2022-06-25 14:38:43 +00:00
if ( patch ) {
this . patchCreate ( [ TLDR . getShapeUtil ( newShape . type ) . create ( newShape ) ] )
} else {
this . createShapes ( newShape )
}
this . setEditingId ( newShape . id , true )
2021-11-05 14:13:14 +00:00
return this
2021-10-16 07:33:25 +00:00
}
2022-07-21 18:16:01 +00:00
getImageOrVideoShapeAtPoint (
2021-12-25 17:06:33 +00:00
id : string ,
type : TDShapeType . Image | TDShapeType . Video ,
point : number [ ] ,
size : number [ ] ,
assetId : string
2022-07-21 18:16:01 +00:00
) {
2021-12-25 17:06:33 +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
const Shape = shapeUtils [ type ]
2022-05-06 08:14:20 +00:00
// Ensure that the pasted shape fits inside of the current viewport
if ( size [ 0 ] > this . viewport . width ) {
2022-06-09 14:33:35 +00:00
const r = size [ 1 ] / size [ 0 ]
2022-05-11 13:25:08 +00:00
size [ 0 ] = this . viewport . width - ( FIT_TO_SCREEN_PADDING / this . camera . zoom ) * 2
2022-05-06 08:14:20 +00:00
size [ 1 ] = size [ 0 ] * r
if ( size [ 1 ] < 32 || size [ 1 ] < 32 ) {
size [ 1 ] = 32
size [ 0 ] = size [ 1 ] / r
}
} else if ( size [ 1 ] > this . viewport . height ) {
2022-06-09 14:33:35 +00:00
const r = size [ 0 ] / size [ 1 ]
2022-05-11 13:25:08 +00:00
size [ 1 ] = this . viewport . height - ( FIT_TO_SCREEN_PADDING / this . camera . zoom ) * 2
2022-05-06 08:14:20 +00:00
size [ 0 ] = size [ 1 ] * r
if ( size [ 1 ] < 32 || size [ 1 ] < 32 ) {
size [ 0 ] = 32
size [ 1 ] = size [ 0 ] / r
}
}
2021-12-25 17:06:33 +00:00
const newShape = Shape . create ( {
id ,
parentId : currentPageId ,
childIndex ,
point ,
size ,
style : { . . . currentStyle } ,
assetId ,
} )
2022-07-21 18:16:01 +00:00
return newShape
2021-12-25 17:06:33 +00:00
}
2021-09-01 08:37:07 +00:00
/ * *
* Create one or more shapes .
* @param shapes An array of shapes .
* @command
* /
2021-11-16 16:01:29 +00:00
create = ( shapes : TDShape [ ] = [ ] , bindings : TDBinding [ ] = [ ] ) : this = > {
2021-09-01 08:37:07 +00:00
if ( shapes . length === 0 ) return this
2021-11-16 16:01:29 +00:00
return this . setState ( Commands . createShapes ( this , shapes , bindings ) )
2021-09-01 08:37:07 +00:00
}
2021-10-22 13:49:29 +00:00
/ * *
* Patch in a new set of shapes
* @param shapes
* @param bindings
* /
2021-11-16 16:01:29 +00:00
patchCreate = ( shapes : TDShape [ ] = [ ] , bindings : TDBinding [ ] = [ ] ) : this = > {
2021-10-22 13:49:29 +00:00
if ( shapes . length === 0 ) return this
2021-11-16 16:01:29 +00:00
return this . patchState ( Commands . createShapes ( this , shapes , bindings ) . after )
2021-10-22 13:49:29 +00:00
}
2021-09-01 08:37:07 +00:00
/ * *
* Delete one or more shapes .
* @param ids The ids of the shapes to delete .
* @command
* /
delete = ( ids = this . selectedIds ) : this = > {
if ( ids . length === 0 ) return this
2022-07-28 07:26:27 +00:00
if ( this . session ) 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 ) )
2022-05-11 13:25:08 +00:00
intersection . forEach ( ( id ) = > this . callbacks . onAssetDelete ! ( this , id ) )
2022-01-16 10:00:46 +00:00
}
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 )
2022-05-06 08:14:20 +00:00
if ( ! command ) {
return this
}
2021-09-05 09:51:21 +00:00
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 ? . ( )
2022-05-06 08:14:20 +00:00
2021-08-29 13:33:43 +00:00
return this
2021-08-10 16:12:55 +00:00
}
2022-07-21 18:16:01 +00:00
addMediaFromFiles = async ( files : File [ ] , point = this . centerPoint ) = > {
2021-12-25 17:06:33 +00:00
this . setIsLoading ( true )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
// Rather than creating each shape individually (which will produce undo / redo entries
// for each shape), create an array of all the shapes that we'll need to create. We'll
// iterate through these at the bottom of the function to set their points, then create
// them through a single call to `createShapes`.
const shapesToCreate : TDShape [ ] = [ ]
2022-01-10 15:13:52 +00:00
const pagePoint = this . getPagePoint ( point )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
for ( const file of files ) {
const id = Utils . uniqueId ( )
const extension = file . name . match ( /\.[0-9a-z]+$/i )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
if ( ! extension ) throw Error ( 'No extension' )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
const isImage = IMAGE_EXTENSIONS . includes ( extension [ 0 ] . toLowerCase ( ) )
const isVideo = VIDEO_EXTENSIONS . includes ( extension [ 0 ] . toLowerCase ( ) )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
if ( ! ( isImage || isVideo ) ) throw Error ( 'Wrong extension' )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
const shapeType = isImage ? TDShapeType.Image : TDShapeType.Video
const assetType = isImage ? TDAssetType.Image : TDAssetType.Video
2022-01-10 15:13:52 +00:00
2022-07-21 18:16:01 +00:00
let src : string | ArrayBuffer | null
try {
if ( this . callbacks . onAssetCreate ) {
const result = await this . callbacks . onAssetCreate ( this , file , id )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
if ( ! result ) throw Error ( 'Asset creation callback returned false' )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
src = result
} else {
src = await fileToBase64 ( file )
}
2022-03-02 14:59:54 +00:00
2022-07-21 18:16:01 +00:00
if ( typeof src === 'string' ) {
let size = [ 0 , 0 ]
2022-05-11 13:25:08 +00:00
2022-07-21 18:16:01 +00:00
if ( isImage ) {
// attempt to get actual svg size from viewBox attribute as
if ( extension [ 0 ] == '.svg' ) {
let viewBox : string [ ]
const svgString = await fileToText ( file )
const viewBoxAttribute = this . getViewboxFromSVG ( svgString )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
if ( viewBoxAttribute ) {
viewBox = viewBoxAttribute . split ( ' ' )
size [ 0 ] = parseFloat ( viewBox [ 2 ] )
size [ 1 ] = parseFloat ( viewBox [ 3 ] )
}
2022-03-02 14:59:54 +00:00
}
2022-07-21 18:16:01 +00:00
if ( Vec . isEqual ( size , [ 0 , 0 ] ) ) {
size = await getImageSizeFromSrc ( src )
}
} else {
size = await getVideoSizeFromSrc ( src )
2022-03-02 14:59:54 +00:00
}
2022-03-03 07:09:19 +00:00
2022-07-21 18:16:01 +00:00
const match = Object . values ( this . document . assets ) . find (
( asset ) = > asset . type === assetType && asset . src === src
)
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
let assetId : string
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
if ( ! match ) {
assetId = Utils . uniqueId ( )
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
const asset = {
id : assetId ,
type : assetType ,
name : file.name ,
src ,
size ,
}
2022-05-06 08:14:20 +00:00
2022-07-21 18:16:01 +00:00
this . patchState ( {
document : {
assets : {
[ assetId ] : asset ,
} ,
2021-12-25 17:06:33 +00:00
} ,
2022-07-21 18:16:01 +00:00
} )
} else {
assetId = match . id
}
shapesToCreate . push ( this . getImageOrVideoShapeAtPoint ( id , shapeType , point , size , assetId ) )
}
} catch ( error ) {
// Even if one shape errors, keep going (we might have had other shapes that didn't error)
console . warn ( error )
}
}
if ( shapesToCreate . length ) {
const currentPoint = Vec . add ( pagePoint , [ 0 , 0 ] )
shapesToCreate . forEach ( ( shape , i ) = > {
const bounds = TLDR . getBounds ( shape )
if ( i === 0 ) {
// For the first shape, offset the current point so
// that the first shape's center is at the page point
currentPoint [ 0 ] -= bounds . width / 2
currentPoint [ 1 ] -= bounds . height / 2
2022-05-06 08:14:20 +00:00
}
2022-07-21 18:16:01 +00:00
// Set the shape's point the current point
shape . point = [ . . . currentPoint ]
// Then bump the page current point by this shape's width
currentPoint [ 0 ] += bounds . width
} )
const commonBounds = Utils . getCommonBounds ( shapesToCreate . map ( TLDR . getBounds ) )
this . createShapes ( . . . shapesToCreate )
// Are the common bounds too big for the viewport?
if ( ! Utils . boundsContain ( this . viewport , commonBounds ) ) {
this . zoomToSelection ( )
if ( this . zoom > 1 ) {
this . resetZoom ( )
}
2021-12-25 17:06:33 +00:00
}
}
2022-01-10 15:13:52 +00:00
2021-12-25 17:06:33 +00:00
this . setIsLoading ( false )
return this
}
2022-03-02 14:59:54 +00:00
private getViewboxFromSVG = ( svgStr : string | ArrayBuffer | null ) = > {
2022-03-03 07:09:19 +00:00
const viewBoxRegex = /.*?viewBox=["'](-?[\d.]+[, ]+-?[\d.]+[, ][\d.]+[, ][\d.]+)["']/
2022-05-06 08:14:20 +00:00
2022-03-02 14:59:54 +00:00
if ( typeof svgStr === 'string' ) {
const matches = svgStr . match ( viewBoxRegex )
2022-03-03 07:09:19 +00:00
return matches && matches . length >= 2 ? matches [ 1 ] : null
2022-03-02 14:59:54 +00:00
}
2022-05-06 08:14:20 +00:00
2022-03-02 14:59:54 +00:00
console . warn ( 'could not get viewbox from svg string' )
2022-05-06 08:14:20 +00:00
2022-03-02 14:59:54 +00:00
this . setIsLoading ( false )
2022-05-06 08:14:20 +00:00
2022-03-02 14:59:54 +00:00
return null
}
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
}
2022-03-09 12:39:41 +00:00
/** Force bounding boxes to reset when the document loads. */
refreshBoundingBoxes = ( ) = > {
// force a change to every text shape
const force = this . shapes . map ( ( shape ) = > {
return [
shape . id ,
{
point : [ . . . shape . point ] ,
. . . ( 'label' in shape && { label : '' } ) ,
} ,
]
} )
const restore = this . shapes . map ( ( shape ) = > {
return [
shape . id ,
{
point : [ . . . shape . point ] ,
. . . ( 'label' in shape && { label : shape.label } ) ,
} ,
]
} )
clearPrevSize ( )
this . patchState ( {
document : {
pages : {
[ this . currentPageId ] : {
shapes : Object.fromEntries ( force ) ,
} ,
} ,
} ,
} )
this . patchState ( {
document : {
pages : {
[ this . currentPageId ] : {
shapes : Object.fromEntries ( restore ) ,
} ,
} ,
} ,
} )
}
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 ) {
2022-07-21 18:16:01 +00:00
this . addMediaFromFiles ( Object . values ( e . dataTransfer . files ) , [ e . clientX , e . clientY ] )
2021-12-25 17:06:33 +00:00
}
return this
}
2022-02-15 12:21:31 +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!
2022-05-11 13:25:08 +00:00
const delta = Vec . div ( info . delta , this . camera . zoom )
const prev = this . 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
2022-02-15 14:15:03 +00:00
const delta = info . delta [ 2 ] / 50
2022-02-15 12:21:31 +00:00
this . zoomBy ( delta , info . point )
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 ) ,
2022-06-25 14:38:43 +00:00
session : ! ! this . session ,
2021-10-16 20:24:31 +00:00
} )
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 )
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
// hack time to reset the size / clipping of an image
if ( this . selectedIds . length !== 1 ) return
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
const shape = this . getShape ( this . selectedIds [ 0 ] )
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
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 )
2022-05-11 13:25:08 +00:00
2021-12-25 17:06:33 +00:00
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 > ) = > {
2022-06-25 14:38:43 +00:00
const pageShapes = this . document . pages [ this . currentPageId ] . shapes
const shapeToUpdate = { . . . ( pageShapes [ shape . id ] as any ) , . . . shape }
const patch = Commands . updateShapes ( this , [ shapeToUpdate ] , this . currentPageId ) . after
return this . patchState ( patch , 'patched_shapes' )
// 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 ,
}
}
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() {
2022-05-11 13:25:08 +00:00
const { zoom } = this . camera
2021-11-26 15:14:10 +00:00
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
2022-07-23 14:05:48 +00:00
static version = 15.5
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 : {
2022-03-09 10:19:58 +00:00
isCadSelectMode : false ,
2021-10-16 19:34:34 +00:00
isPenMode : false ,
isDarkMode : false ,
isZoomSnap : false ,
isFocusMode : false ,
2021-10-19 13:29:55 +00:00
isSnapping : false ,
2022-05-06 08:14:20 +00:00
isDebugMode : false ,
2021-10-16 19:34:34 +00:00
isReadonlyMode : false ,
2022-05-20 12:56:16 +00:00
keepStyleMenuOpen : false ,
2021-10-16 19:34:34 +00:00
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 ,
2022-06-09 14:33:35 +00:00
language : 'en' ,
2022-07-07 10:59:47 +00:00
dockPosition : 'bottom' ,
2022-07-23 14:05:48 +00:00
exportBackground : TDExportBackground.Transparent ,
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 ,
2022-06-01 14:21:36 +00:00
eraseLine : [ ] ,
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
}
2022-05-11 15:58:40 +00:00
static assetSrc = 'tldraw-assets.json'
2021-08-10 16:12:55 +00:00
}